Network/HTTP/Stream.hs
-----------------------------------------------------------------------------
-- |
-- Module : Network.HTTP
-- Copyright : (c) Warrick Gray 2002, Bjorn Bringert 2003-2005, 2007 Robin Bate Boerop
-- License : BSD
--
-- Maintainer : bjorn@bringert.net
-- Stability : experimental
-- Portability : non-portable (not tested)
--
-- An easy HTTP interface enjoy.
--
-- * Changes by Robin Bate Boerop <robin@bateboerop.name>:
-- - Made dependencies explicit in import statements.
-- - Removed false dependencies in import statements.
-- - Added missing type signatures.
-- - Moved Header-related code to Network.HTTP.Headers module.
--
-- * Changes by Simon Foster:
-- - Split module up into to sepearate Network.[Stream,TCP,HTTP] modules
-- - Created functions receiveHTTP and responseHTTP to allow server side interactions
-- (although 100-continue is unsupported and I haven't checked for standard compliancy).
-- - Pulled the transfer functions from sendHTTP to global scope to allow access by
-- above functions.
--
-- * Changes by Graham Klyne:
-- - export httpVersion
-- - use new URI module (similar to old, but uses revised URI datatype)
--
-- * Changes by Bjorn Bringert:
--
-- - handle URIs with a port number
-- - added debugging toggle
-- - disabled 100-continue transfers to get HTTP\/1.0 compatibility
-- - change 'ioError' to 'throw'
-- - Added simpleHTTP_, which takes a stream argument.
--
-- * Changes from 0.1
-- - change 'openHTTP' to 'openTCP', removed 'closeTCP' - use 'close' from 'Stream' class.
-- - added use of inet_addr to openHTTP, allowing use of IP "dot" notation addresses.
-- - reworking of the use of Stream, including alterations to make 'sendHTTP' generic
-- and the addition of a debugging stream.
-- - simplified error handling.
--
-- * TODO
-- - request pipelining
-- - https upgrade (includes full TLS, i.e. SSL, implementation)
-- - use of Stream classes will pay off
-- - consider C implementation of encryption\/decryption
-- - comm timeouts
-- - MIME & entity stuff (happening in separate module)
-- - support \"*\" uri-request-string for OPTIONS request method
--
--
-- * Header notes:
--
-- [@Host@]
-- Required by HTTP\/1.1, if not supplied as part
-- of a request a default Host value is extracted
-- from the request-uri.
--
-- [@Connection@]
-- If this header is present in any request or
-- response, and it's value is "close", then
-- the current request\/response is the last
-- to be allowed on that connection.
--
-- [@Expect@]
-- Should a request contain a body, an Expect
-- header will be added to the request. The added
-- header has the value \"100-continue\". After
-- a 417 \"Expectation Failed\" response the request
-- is attempted again without this added Expect
-- header.
--
-- [@TransferEncoding,ContentLength,...@]
-- if request is inconsistent with any of these
-- header values then you may not receive any response
-- or will generate an error response (probably 4xx).
--
--
-- * Response code notes
-- Some response codes induce special behaviour:
--
-- [@1xx@] \"100 Continue\" will cause any unsent request body to be sent.
-- \"101 Upgrade\" will be returned.
-- Other 1xx responses are ignored.
--
-- [@417@] The reason for this code is \"Expectation failed\", indicating
-- that the server did not like the Expect \"100-continue\" header
-- added to a request. Receipt of 417 will induce another
-- request attempt (without Expect header), unless no Expect header
-- had been added (in which case 417 response is returned).
--
-----------------------------------------------------------------------------
module Network.HTTP.Stream
( module Network.Stream
, simpleHTTP -- :: Request_String -> IO (Result Response_String)
, simpleHTTP_ -- :: Stream s => s -> Request_String -> IO (Result Response_String)
, sendHTTP -- :: Stream s => s -> Request_String -> IO (Result Response_String)
, sendHTTP_notify -- :: Stream s => s -> Request_String -> IO () -> IO (Result Response_String)
, receiveHTTP -- :: Stream s => s -> IO (Result Request_String)
, respondHTTP -- :: Stream s => s -> Response_String -> IO ()
) where
-----------------------------------------------------------------
------------------ Imports --------------------------------------
-----------------------------------------------------------------
import Network.Stream
import Network.StreamDebugger (debugStream)
import Network.TCP (openTCPPort)
import Network.BufferType ( stringBufferOp )
import Network.HTTP.Base
import Network.HTTP.Headers
import Network.HTTP.Utils ( trim )
import Data.Char (toLower)
import Data.Maybe (fromMaybe)
import Control.Monad (when)
-- Turn on to enable HTTP traffic logging
debug :: Bool
debug = False
-- File that HTTP traffic logs go to
httpLogFile :: String
httpLogFile = "http-debug.log"
-----------------------------------------------------------------
------------------ Misc -----------------------------------------
-----------------------------------------------------------------
-- | Simple way to get a resource across a non-persistant connection.
-- Headers that may be altered:
-- Host Altered only if no Host header is supplied, HTTP\/1.1
-- requires a Host header.
-- Connection Where no allowance is made for persistant connections
-- the Connection header will be set to "close"
simpleHTTP :: Request_String -> IO (Result Response_String)
simpleHTTP r =
do
auth <- getAuth r
c <- openTCPPort (host auth) (fromMaybe 80 (port auth))
simpleHTTP_ c r
-- | Like 'simpleHTTP', but acting on an already opened stream.
simpleHTTP_ :: Stream s => s -> Request_String -> IO (Result Response_String)
simpleHTTP_ s r =
do
auth <- getAuth r
let r' = normalizeRequestURI True{-do close-} (host auth) r
rsp <- if debug then do
s' <- debugStream httpLogFile s
sendHTTP s' r'
else
sendHTTP s r'
-- already done by sendHTTP because of "Connection: close" header
--; close s
return rsp
sendHTTP :: Stream s => s -> Request_String -> IO (Result Response_String)
sendHTTP conn rq =
do { let a_rq = normalizeHostHeader rq
; rsp <- catchIO (sendMain conn a_rq (return ()))
(\e -> do { close conn; ioError e })
; let fn list = when (or $ map findConnClose list)
(close conn)
; either (\_ -> fn [rqHeaders rq])
(\r -> fn [rqHeaders rq,rspHeaders r])
rsp
; return rsp
}
sendHTTP_notify :: Stream s => s -> Request_String -> IO () -> IO (Result Response_String)
sendHTTP_notify conn rq onSendComplete =
do { let a_rq = normalizeHostHeader rq
; rsp <- catchIO (sendMain conn a_rq onSendComplete)
(\e -> do { close conn; ioError e })
; let fn list = when (or $ map findConnClose list)
(close conn)
; either (\_ -> fn [rqHeaders rq])
(\r -> fn [rqHeaders rq,rspHeaders r])
rsp
; return rsp
}
-- From RFC 2616, section 8.2.3:
-- 'Because of the presence of older implementations, the protocol allows
-- ambiguous situations in which a client may send "Expect: 100-
-- continue" without receiving either a 417 (Expectation Failed) status
-- or a 100 (Continue) status. Therefore, when a client sends this
-- header field to an origin server (possibly via a proxy) from which it
-- has never seen a 100 (Continue) status, the client SHOULD NOT wait
-- for an indefinite period before sending the request body.'
--
-- Since we would wait forever, I have disabled use of 100-continue for now.
sendMain :: Stream s => s -> Request_String -> IO () -> IO (Result Response_String)
sendMain conn rqst onSendComplete = do
--let str = if null (rqBody rqst)
-- then show rqst
-- else show (insertHeader HdrExpect "100-continue" rqst)
writeBlock conn (show rqst)
-- write body immediately, don't wait for 100 CONTINUE
writeBlock conn (rqBody rqst)
onSendComplete
rsp <- getResponseHead conn
switchResponse conn True False rsp rqst
-- reads and parses headers
getResponseHead :: Stream s => s -> IO (Result ResponseData)
getResponseHead conn = do
lor <- readTillEmpty1 stringBufferOp (readLine conn)
return $ lor >>= parseResponseHead
-- Hmmm, this could go bad if we keep getting "100 Continue"
-- responses... Except this should never happen according
-- to the RFC.
switchResponse :: Stream s
=> s
-> Bool {- allow retry? -}
-> Bool {- is body sent? -}
-> Result ResponseData
-> Request_String
-> IO (Result Response_String)
switchResponse _ _ _ (Left e) _ = return (Left e)
-- retry on connreset?
-- if we attempt to use the same socket then there is an excellent
-- chance that the socket is not in a completely closed state.
switchResponse conn allow_retry bdy_sent (Right (cd,rn,hdrs)) rqst =
case matchResponse (rqMethod rqst) cd of
Continue
| not bdy_sent -> {- Time to send the body -}
do { val <- writeBlock conn (rqBody rqst)
; case val of
Left e -> return (Left e)
Right _ ->
do { rsp <- getResponseHead conn
; switchResponse conn allow_retry True rsp rqst
}
}
| otherwise -> {- keep waiting -}
do { rsp <- getResponseHead conn
; switchResponse conn allow_retry bdy_sent rsp rqst
}
Retry -> {- Request with "Expect" header failed.
Trouble is the request contains Expects
other than "100-Continue" -}
do { writeBlock conn (show rqst ++ rqBody rqst)
; rsp <- getResponseHead conn
; switchResponse conn False bdy_sent rsp rqst
}
Done ->
return (Right $ Response cd rn hdrs "")
DieHorribly str ->
return $ responseParseError "sendHTTP" ("Invalid response: " ++ str)
ExpectEntity ->
let tc = lookupHeader HdrTransferEncoding hdrs
cl = lookupHeader HdrContentLength hdrs
in
do { rslt <- case tc of
Nothing ->
case cl of
Just x -> linearTransfer (readBlock conn) (read x :: Int)
Nothing -> hopefulTransfer stringBufferOp {-null (++) []-} (readLine conn) []
Just x ->
case map toLower (trim x) of
"chunked" -> chunkedTransfer stringBufferOp
(readLine conn) (readBlock conn)
_ -> uglyDeathTransfer "sendHTTP"
; return $ do
(ftrs,bdy) <- rslt
return (Response cd rn (hdrs++ftrs) bdy)
}
-- | Receive and parse a HTTP request from the given Stream. Should be used
-- for server side interactions.
receiveHTTP :: Stream s => s -> IO (Result Request_String)
receiveHTTP conn = getRequestHead >>= processRequest
where
-- reads and parses headers
getRequestHead :: IO (Result RequestData)
getRequestHead =
do { lor <- readTillEmpty1 stringBufferOp (readLine conn)
; return $ lor >>= parseRequestHead
}
processRequest (Left e) = return $ Left e
processRequest (Right (rm,uri,hdrs)) =
do -- FIXME : Also handle 100-continue.
let tc = lookupHeader HdrTransferEncoding hdrs
cl = lookupHeader HdrContentLength hdrs
rslt <- case tc of
Nothing ->
case cl of
Just x -> linearTransfer (readBlock conn) (read x :: Int)
Nothing -> return (Right ([], "")) -- hopefulTransfer ""
Just x ->
case map toLower (trim x) of
"chunked" -> chunkedTransfer stringBufferOp
(readLine conn) (readBlock conn)
_ -> uglyDeathTransfer "receiveHTTP"
return $ do
(ftrs,bdy) <- rslt
return (Request uri rm (hdrs++ftrs) bdy)
-- | Very simple function, send a HTTP response over the given stream. This
-- could be improved on to use different transfer types.
respondHTTP :: Stream s => s -> Response_String -> IO ()
respondHTTP conn rsp = do writeBlock conn (show rsp)
-- write body immediately, don't wait for 100 CONTINUE
writeBlock conn (rspBody rsp)
return ()