HTTP 3001.1.5 → 4000.0.0
raw patch · 15 files changed
+2140/−1325 lines, 15 filesdep +bytestringdep ~basedep ~networknew-uploaderPVP ok
version bump matches the API change (PVP)
Dependencies added: bytestring
Dependency ranges changed: base, network
API changes (from Hackage documentation)
- Network.Browser: MkCookie :: String -> String -> String -> Maybe String -> Maybe String -> Maybe String -> Cookie
- Network.Browser: ckComment :: Cookie -> Maybe String
- Network.Browser: ckDomain :: Cookie -> String
- Network.Browser: ckName :: Cookie -> String
- Network.Browser: ckPath :: Cookie -> Maybe String
- Network.Browser: ckValue :: Cookie -> String
- Network.Browser: ckVersion :: Cookie -> Maybe String
- Network.Browser: instance Functor BrowserAction
- Network.Browser: instance Monad BrowserAction
- Network.Browser: instance Show BrowserState
- Network.HTTP: Custom :: String -> RequestMethod
- Network.HTTP: DELETE :: RequestMethod
- Network.HTTP: GET :: RequestMethod
- Network.HTTP: HEAD :: RequestMethod
- Network.HTTP: OPTIONS :: RequestMethod
- Network.HTTP: POST :: RequestMethod
- Network.HTTP: PUT :: RequestMethod
- Network.HTTP: Request :: URI -> RequestMethod -> [Header] -> String -> Request
- Network.HTTP: Response :: ResponseCode -> String -> [Header] -> String -> Response
- Network.HTTP: TRACE :: RequestMethod
- Network.HTTP: URIAuthority :: Maybe String -> Maybe String -> String -> Maybe Int -> URIAuthority
- Network.HTTP: data Request
- Network.HTTP: data RequestMethod
- Network.HTTP: data Response
- Network.HTTP: data URIAuthority
- Network.HTTP: getAuth :: (Monad m) => Request -> m URIAuthority
- Network.HTTP: getRequestHead :: (Stream s) => s -> IO (Result RequestData)
- Network.HTTP: host :: URIAuthority -> String
- Network.HTTP: httpVersion :: String
- Network.HTTP: instance Eq RequestMethod
- Network.HTTP: instance Eq URIAuthority
- Network.HTTP: instance HasHeaders Request
- Network.HTTP: instance HasHeaders Response
- Network.HTTP: instance Show Request
- Network.HTTP: instance Show RequestMethod
- Network.HTTP: instance Show Response
- Network.HTTP: instance Show URIAuthority
- Network.HTTP: instance Typeable Request
- Network.HTTP: instance Typeable Response
- Network.HTTP: parseURIAuthority :: String -> Maybe URIAuthority
- Network.HTTP: password :: URIAuthority -> Maybe String
- Network.HTTP: port :: URIAuthority -> Maybe Int
- Network.HTTP: processRequest :: (Stream s) => s -> RequestData -> IO (Result Request)
- Network.HTTP: receiveHTTP :: (Stream s) => s -> IO (Result Request)
- Network.HTTP: respondHTTP :: (Stream s) => s -> Response -> IO ()
- Network.HTTP: rqBody :: Request -> String
- Network.HTTP: rqHeaders :: Request -> [Header]
- Network.HTTP: rqMethod :: Request -> RequestMethod
- Network.HTTP: rqURI :: Request -> URI
- Network.HTTP: rspBody :: Response -> String
- Network.HTTP: rspCode :: Response -> ResponseCode
- Network.HTTP: rspHeaders :: Response -> [Header]
- Network.HTTP: rspReason :: Response -> String
- Network.HTTP: sendHTTP :: (Stream s) => s -> Request -> IO (Result Response)
- Network.HTTP: simpleHTTP :: Request -> IO (Result Response)
- Network.HTTP: simpleHTTP_ :: (Stream s) => s -> Request -> IO (Result Response)
- Network.HTTP: type RequestData = (RequestMethod, URI, [Header])
- Network.HTTP: type ResponseCode = (Int, Int, Int)
- Network.HTTP: urlDecode :: String -> String
- Network.HTTP: urlEncode :: String -> String
- Network.HTTP: urlEncodeVars :: [(String, String)] -> String
- Network.HTTP: user :: URIAuthority -> Maybe String
- Network.HTTP.Headers: headerMap :: [(String, HeaderName)]
- Network.HTTP.Headers: parseHeader :: String -> Result Header
- Network.TCP: instance Eq Conn
+ Network.Browser: instance Functor (BrowserAction conn)
+ Network.Browser: instance Monad (BrowserAction conn)
+ Network.Browser: instance Show (BrowserState t)
+ Network.BufferType: BufferOp :: (Handle -> Int -> IO a) -> (Handle -> IO a) -> (Handle -> a -> IO ()) -> (Handle -> IO a) -> a -> (a -> a -> a) -> (String -> a) -> (a -> String) -> (a -> Word8 -> a) -> (Int -> a -> (a, a)) -> ((Char -> Bool) -> a -> (a, a)) -> (a -> Bool) -> (a -> Bool) -> BufferOp a
+ Network.BufferType: buf_append :: BufferOp a -> a -> a -> a
+ Network.BufferType: buf_empty :: BufferOp a -> a
+ Network.BufferType: buf_fromStr :: BufferOp a -> String -> a
+ Network.BufferType: buf_hGet :: BufferOp a -> Handle -> Int -> IO a
+ Network.BufferType: buf_hGetContents :: BufferOp a -> Handle -> IO a
+ Network.BufferType: buf_hGetLine :: BufferOp a -> Handle -> IO a
+ Network.BufferType: buf_hPut :: BufferOp a -> Handle -> a -> IO ()
+ Network.BufferType: buf_isEmpty :: BufferOp a -> a -> Bool
+ Network.BufferType: buf_isLineTerm :: BufferOp a -> a -> Bool
+ Network.BufferType: buf_snoc :: BufferOp a -> a -> Word8 -> a
+ Network.BufferType: buf_span :: BufferOp a -> (Char -> Bool) -> a -> (a, a)
+ Network.BufferType: buf_splitAt :: BufferOp a -> Int -> a -> (a, a)
+ Network.BufferType: buf_toStr :: BufferOp a -> a -> String
+ Network.BufferType: bufferOps :: (BufferType bufType) => BufferOp bufType
+ Network.BufferType: class BufferType bufType
+ Network.BufferType: data BufferOp a
+ Network.BufferType: instance BufferType ByteString
+ Network.BufferType: instance BufferType String
+ Network.BufferType: instance Eq (BufferOp a)
+ Network.BufferType: lazyBufferOp :: BufferOp ByteString
+ Network.BufferType: strictBufferOp :: BufferOp ByteString
+ Network.BufferType: stringBufferOp :: BufferOp String
+ Network.HTTP.Base: Continue :: ResponseNextStep
+ Network.HTTP.Base: DELETE :: RequestMethod
+ Network.HTTP.Base: DieHorribly :: String -> ResponseNextStep
+ Network.HTTP.Base: Done :: ResponseNextStep
+ Network.HTTP.Base: ExpectEntity :: ResponseNextStep
+ Network.HTTP.Base: GET :: RequestMethod
+ Network.HTTP.Base: HEAD :: RequestMethod
+ Network.HTTP.Base: OPTIONS :: RequestMethod
+ Network.HTTP.Base: POST :: RequestMethod
+ Network.HTTP.Base: PUT :: RequestMethod
+ Network.HTTP.Base: Request :: URI -> RequestMethod -> [Header] -> a -> HTTPRequest a
+ Network.HTTP.Base: Response :: ResponseCode -> String -> [Header] -> a -> HTTPResponse a
+ Network.HTTP.Base: Retry :: ResponseNextStep
+ Network.HTTP.Base: TRACE :: RequestMethod
+ Network.HTTP.Base: URIAuthority :: Maybe String -> Maybe String -> String -> Maybe Int -> URIAuthority
+ Network.HTTP.Base: catchIO :: IO a -> (IOException -> IO a) -> IO a
+ Network.HTTP.Base: catchIO_ :: IO a -> IO a -> IO a
+ Network.HTTP.Base: chunkedTransfer :: BufferOp a -> IO (Result a) -> (Int -> IO (Result a)) -> IO (Result ([Header], a))
+ Network.HTTP.Base: crlf :: String
+ Network.HTTP.Base: data HTTPRequest a
+ Network.HTTP.Base: data HTTPResponse a
+ Network.HTTP.Base: data RequestMethod
+ Network.HTTP.Base: data ResponseNextStep
+ Network.HTTP.Base: data URIAuthority
+ Network.HTTP.Base: findConnClose :: [Header] -> Bool
+ Network.HTTP.Base: getAuth :: (Monad m) => HTTPRequest ty -> m URIAuthority
+ Network.HTTP.Base: hopefulTransfer :: BufferOp a -> IO (Result a) -> [a] -> IO (Result ([Header], a))
+ Network.HTTP.Base: host :: URIAuthority -> String
+ Network.HTTP.Base: httpVersion :: String
+ Network.HTTP.Base: instance Eq RequestMethod
+ Network.HTTP.Base: instance Eq URIAuthority
+ Network.HTTP.Base: instance HasHeaders (HTTPRequest a)
+ Network.HTTP.Base: instance HasHeaders (HTTPResponse a)
+ Network.HTTP.Base: instance Show (HTTPRequest a)
+ Network.HTTP.Base: instance Show (HTTPResponse a)
+ Network.HTTP.Base: instance Show RequestMethod
+ Network.HTTP.Base: instance Show URIAuthority
+ Network.HTTP.Base: linearTransfer :: (Int -> IO (Result a)) -> Int -> IO (Result ([Header], a))
+ Network.HTTP.Base: matchResponse :: RequestMethod -> ResponseCode -> ResponseNextStep
+ Network.HTTP.Base: normalizeHostHeader :: HTTPRequest ty -> HTTPRequest ty
+ Network.HTTP.Base: normalizeRequestURI :: URIAuthority -> HTTPRequest ty -> HTTPRequest ty
+ Network.HTTP.Base: parseRequestHead :: [String] -> Result RequestData
+ Network.HTTP.Base: parseResponseHead :: [String] -> Result ResponseData
+ Network.HTTP.Base: parseURIAuthority :: String -> Maybe URIAuthority
+ Network.HTTP.Base: password :: URIAuthority -> Maybe String
+ Network.HTTP.Base: port :: URIAuthority -> Maybe Int
+ Network.HTTP.Base: readTillEmpty1 :: BufferOp a -> IO (Result a) -> IO (Result [a])
+ Network.HTTP.Base: readTillEmpty2 :: BufferOp a -> IO (Result a) -> [a] -> IO (Result [a])
+ Network.HTTP.Base: rqBody :: HTTPRequest a -> a
+ Network.HTTP.Base: rqHeaders :: HTTPRequest a -> [Header]
+ Network.HTTP.Base: rqMethod :: HTTPRequest a -> RequestMethod
+ Network.HTTP.Base: rqURI :: HTTPRequest a -> URI
+ Network.HTTP.Base: rspBody :: HTTPResponse a -> a
+ Network.HTTP.Base: rspCode :: HTTPResponse a -> ResponseCode
+ Network.HTTP.Base: rspHeaders :: HTTPResponse a -> [Header]
+ Network.HTTP.Base: rspReason :: HTTPResponse a -> String
+ Network.HTTP.Base: sp :: String
+ Network.HTTP.Base: type Request = HTTPRequest String
+ Network.HTTP.Base: type RequestData = (RequestMethod, URI, [Header])
+ Network.HTTP.Base: type Response = HTTPResponse String
+ Network.HTTP.Base: type ResponseCode = (Int, Int, Int)
+ Network.HTTP.Base: type ResponseData = (ResponseCode, String, [Header])
+ Network.HTTP.Base: uglyDeathTransfer :: IO (Result ([Header], a))
+ Network.HTTP.Base: uriAuthToString :: URIAuth -> String
+ Network.HTTP.Base: uriToAuthorityString :: URI -> String
+ Network.HTTP.Base: urlDecode :: String -> String
+ Network.HTTP.Base: urlEncode :: String -> String
+ Network.HTTP.Base: urlEncodeVars :: [(String, String)] -> String
+ Network.HTTP.Base: user :: URIAuthority -> Maybe String
+ Network.HTTP.HandleStream: receiveHTTP :: (HStream bufTy) => HandleStream bufTy -> IO (Result (HTTPRequest bufTy))
+ Network.HTTP.HandleStream: respondHTTP :: (HStream ty) => HandleStream ty -> HTTPResponse ty -> IO ()
+ Network.HTTP.HandleStream: sendHTTP :: (HStream ty) => HandleStream ty -> HTTPRequest ty -> IO (Result (HTTPResponse ty))
+ Network.HTTP.HandleStream: simpleHTTP :: (HStream ty) => HTTPRequest ty -> IO (Result (HTTPResponse ty))
+ Network.HTTP.HandleStream: simpleHTTP_ :: (HStream ty) => HandleStream ty -> HTTPRequest ty -> IO (Result (HTTPResponse ty))
+ Network.HTTP.HandleStream: simpleHTTP_debug :: (HStream ty) => FilePath -> HTTPRequest ty -> IO (Result (HTTPResponse ty))
+ Network.HTTP.Stream: receiveHTTP :: (Stream s) => s -> IO (Result Request)
+ Network.HTTP.Stream: respondHTTP :: (Stream s) => s -> Response -> IO ()
+ Network.HTTP.Stream: sendHTTP :: (Stream s) => s -> Request -> IO (Result Response)
+ Network.HTTP.Stream: simpleHTTP :: Request -> IO (Result Response)
+ Network.HTTP.Stream: simpleHTTP_ :: (Stream s) => s -> Request -> IO (Result Response)
+ Network.StreamDebugger: debugByteStream :: (HStream ty) => FilePath -> HandleStream ty -> IO (HandleStream ty)
+ Network.TCP: StreamHooks :: ((ty -> String) -> Result ty -> IO ()) -> ((ty -> String) -> Int -> Result ty -> IO ()) -> ((ty -> String) -> ty -> Result () -> IO ()) -> IO () -> StreamHooks ty
+ Network.TCP: class (BufferType bufType) => HStream bufType
+ Network.TCP: close :: (HStream bufType) => HandleStream bufType -> IO ()
+ Network.TCP: data HandleStream a
+ Network.TCP: data StreamHooks ty
+ Network.TCP: hook_close :: StreamHooks ty -> IO ()
+ Network.TCP: hook_readBlock :: StreamHooks ty -> (ty -> String) -> Int -> Result ty -> IO ()
+ Network.TCP: hook_readLine :: StreamHooks ty -> (ty -> String) -> Result ty -> IO ()
+ Network.TCP: hook_writeBlock :: StreamHooks ty -> (ty -> String) -> ty -> Result () -> IO ()
+ Network.TCP: instance (Eq a) => Eq (Conn a)
+ Network.TCP: instance (Eq ty) => Eq (StreamHooks ty)
+ Network.TCP: instance HStream ByteString
+ Network.TCP: instance HStream String
+ Network.TCP: isTCPConnectedTo :: HandleStream ty -> String -> IO Bool
+ Network.TCP: nullHooks :: StreamHooks ty
+ Network.TCP: openStream :: (HStream bufType) => String -> Int -> IO (HandleStream bufType)
+ Network.TCP: openTCPConnection :: (BufferType ty) => String -> Int -> IO (HandleStream ty)
+ Network.TCP: readBlock :: (HStream bufType) => HandleStream bufType -> Int -> IO (Result bufType)
+ Network.TCP: readLine :: (HStream bufType) => HandleStream bufType -> IO (Result bufType)
+ Network.TCP: setStreamHooks :: HandleStream ty -> StreamHooks ty -> IO ()
+ Network.TCP: writeBlock :: (HStream bufType) => HandleStream bufType -> bufType -> IO (Result ())
- Network.Browser: addAuthority :: Authority -> BrowserAction ()
+ Network.Browser: addAuthority :: Authority -> BrowserAction t ()
- Network.Browser: addCookie :: Cookie -> BrowserAction ()
+ Network.Browser: addCookie :: Cookie -> BrowserAction t ()
- Network.Browser: browse :: BrowserAction a -> IO a
+ Network.Browser: browse :: BrowserAction conn a -> IO a
- Network.Browser: data BrowserAction a
+ Network.Browser: data BrowserAction conn a
- Network.Browser: data BrowserState
+ Network.Browser: data BrowserState connection
- Network.Browser: err :: String -> BrowserAction ()
+ Network.Browser: err :: String -> BrowserAction t ()
- Network.Browser: getAllowRedirects :: BrowserAction Bool
+ Network.Browser: getAllowRedirects :: BrowserAction t Bool
- Network.Browser: getAuthorities :: BrowserAction [Authority]
+ Network.Browser: getAuthorities :: BrowserAction t [Authority]
- Network.Browser: getAuthorityGen :: BrowserAction (URI -> String -> IO (Maybe (String, String)))
+ Network.Browser: getAuthorityGen :: BrowserAction t (URI -> String -> IO (Maybe (String, String)))
- Network.Browser: getCookies :: BrowserAction [Cookie]
+ Network.Browser: getCookies :: BrowserAction t [Cookie]
- Network.Browser: ioAction :: IO a -> BrowserAction a
+ Network.Browser: ioAction :: IO a -> BrowserAction t a
- Network.Browser: out :: String -> BrowserAction ()
+ Network.Browser: out :: String -> BrowserAction t ()
- Network.Browser: request :: Request -> BrowserAction (URI, Response)
+ Network.Browser: request :: (HStream ty) => HTTPRequest ty -> BrowserAction (HandleStream ty) (URI, HTTPResponse ty)
- Network.Browser: setAllowBasicAuth :: Bool -> BrowserAction ()
+ Network.Browser: setAllowBasicAuth :: Bool -> BrowserAction t ()
- Network.Browser: setAllowRedirects :: Bool -> BrowserAction ()
+ Network.Browser: setAllowRedirects :: Bool -> BrowserAction t ()
- Network.Browser: setAuthorities :: [Authority] -> BrowserAction ()
+ Network.Browser: setAuthorities :: [Authority] -> BrowserAction t ()
- Network.Browser: setAuthorityGen :: (URI -> String -> IO (Maybe (String, String))) -> BrowserAction ()
+ Network.Browser: setAuthorityGen :: (URI -> String -> IO (Maybe (String, String))) -> BrowserAction t ()
- Network.Browser: setCookieFilter :: (URI -> Cookie -> IO Bool) -> BrowserAction ()
+ Network.Browser: setCookieFilter :: (URI -> Cookie -> IO Bool) -> BrowserAction t ()
- Network.Browser: setCookies :: [Cookie] -> BrowserAction ()
+ Network.Browser: setCookies :: [Cookie] -> BrowserAction t ()
- Network.Browser: setDebugLog :: Maybe String -> BrowserAction ()
+ Network.Browser: setDebugLog :: Maybe String -> BrowserAction t ()
- Network.Browser: setErrHandler :: (String -> IO ()) -> BrowserAction ()
+ Network.Browser: setErrHandler :: (String -> IO ()) -> BrowserAction t ()
- Network.Browser: setOutHandler :: (String -> IO ()) -> BrowserAction ()
+ Network.Browser: setOutHandler :: (String -> IO ()) -> BrowserAction t ()
- Network.Browser: setProxy :: Proxy -> BrowserAction ()
+ Network.Browser: setProxy :: Proxy -> BrowserAction t ()
- Network.StreamSocket: handleSocketError :: Socket -> Exception -> IO (Result a)
+ Network.StreamSocket: handleSocketError :: Socket -> IOException -> IO (Result a)
Files
- HTTP.cabal +32/−18
- Network/Browser.hs +344/−366
- Network/BufferType.hs +132/−0
- Network/HTTP.hs +17/−626
- Network/HTTP/Base.hs +552/−0
- Network/HTTP/HandleStream.hs +214/−0
- Network/HTTP/Headers.hs +118/−136
- Network/HTTP/MD5.hs +3/−3
- Network/HTTP/MD5Aux.hs +14/−10
- Network/HTTP/Stream.hs +307/−0
- Network/HTTP/Utils.hs +67/−0
- Network/Stream.hs +6/−6
- Network/StreamDebugger.hs +25/−0
- Network/StreamSocket.hs +40/−35
- Network/TCP.hs +269/−125
HTTP.cabal view
@@ -1,11 +1,10 @@-Name: HTTP-Version: 3001.1.5-Cabal-Version: >= 1.2-Build-type: Simple-License: BSD3-License-file: LICENSE-Category: Network-Copyright:+Name: HTTP+Version: 4000.0.0+Cabal-Version: >= 1.2+Build-type: Simple+License: BSD3+License-file: LICENSE+Copyright: Copyright (c) 2002, Warrick Gray Copyright (c) 2002-2005, Ian Lynagh Copyright (c) 2003-2006, Bjorn Bringert@@ -13,34 +12,49 @@ Copyright (c) 2004, Ganesh Sittampalam Copyright (c) 2004-2005, Dominic Steinitz Copyright 2007 Robin Bate Boerop-Author: Warrick Gray <warrick.gray@hotmail.com>-Maintainer: Bjorn Bringert <bjorn@bringert.net>-Homepage: http://www.haskell.org/http/-Description: A library for client-side HTTP-Synopsis: A library for client-side HTTP +Author: Warrick Gray <warrick.gray@hotmail.com>+Maintainer: Sigbjorn Finne <sigbjorn.finne@gmail.com>+Homepage: http://www.haskell.org/http/+Description: + A library for client-side HTTP, version 2. Rewrite of existing HTTP+ package to allow overloaded representation of HTTP request bodies+ and responses. Provides three such instances: lazy and strict 'ByteString',+ along with the good old @String@.+++ Inspired in part by Jonas Aadahl et al's work on ByteString'ifying HTTP+ a couple of years ago.+++ Git repository available at <http://code.galois.com/HTTPbis.git>+ Flag old-base description: Old, monolithic base default: False Library Exposed-modules: + Network.BufferType, Network.Stream, Network.StreamDebugger, Network.StreamSocket, Network.TCP, Network.HTTP, Network.HTTP.Headers,+ Network.HTTP.Base,+ Network.HTTP.Stream,+ Network.HTTP.HandleStream, Network.Browser Other-modules: Network.HTTP.Base64, Network.HTTP.MD5,- Network.HTTP.MD5Aux- GHC-options: -fwarn-missing-signatures- Extensions: DeriveDataTypeable- Build-depends: network, parsec+ Network.HTTP.MD5Aux,+ Network.HTTP.Utils+ GHC-options: -fwarn-missing-signatures -Wall+ Build-depends: network, parsec, bytestring if flag(old-base) Build-depends: base < 3 else- Build-depends: base >= 3 && < 4, array+ Build-depends: base >= 3, array
Network/Browser.hs view
@@ -31,7 +31,7 @@ module Network.Browser ( BrowserState, BrowserAction, -- browser monad, effectively a state monad.- Cookie(..),+ Cookie, Form(..), Proxy(..), @@ -76,17 +76,20 @@ import Network.URI ( URI(uriAuthority, uriScheme, uriPath, uriQuery)- , URIAuth(URIAuth, uriUserInfo, uriPort, uriRegName)+ , URIAuth(..) , parseURI, parseURIReference, relativeTo )-import Network.StreamDebugger (debugStream)-import Network.TCP (Connection, isConnectedTo)+import Network.StreamDebugger (debugByteStream) import Network.HTTP import qualified Network.HTTP.MD5 as MD5 (hash) import qualified Network.HTTP.Base64 as Base64 (encode)+import Network.Stream ( ConnError(..) )+import Network.BufferType +import Network.HTTP.Utils ( trim, splitBy )+ import Data.Char (toLower,isAlphaNum,isSpace)-import Data.List (isPrefixOf,isSuffixOf,elemIndex,elemIndices)+import Data.List (isPrefixOf,isSuffixOf) import Data.Maybe (fromMaybe, listToMaybe, catMaybes, fromJust, isJust) import Control.Monad (foldM, filterM, liftM, when) import Text.ParserCombinators.Parsec@@ -108,7 +111,7 @@ word, quotedstring :: Parser String quotedstring =- do { char '"'+ do { char '"' -- " ; str <- many (satisfy $ not . (=='"')) ; char '"' ; return str@@ -116,31 +119,7 @@ word = many1 (satisfy (\x -> isAlphaNum x || x=='_' || x=='.' || x=='-' || x==':')) --- misc string fns-trim :: String -> String-trim = let dropspace = dropWhile isSpace in- reverse . dropspace . reverse . dropspace ---- | Splits a list into two parts, the delimiter occurs--- at the head of the second list. Nothing is returned--- when no occurance of the delimiter is found.-split :: Eq a => a -> [a] -> Maybe ([a],[a])-split delim list = case delim `elemIndex` list of- Nothing -> Nothing- Just x -> Just $ splitAt x list- ----- | Removes delimiters.-splitMany :: Eq a => a -> [a] -> [[a]]-splitMany delim str = fn str ixs- where- ixs = elemIndices delim str- fn _ [] = []- fn ls (h:t) = let (a,b) = splitAt h ls in a : fn b t-- -- | Returns a URI that is consistent with the first -- argument uri when read in the context of a second. -- If second argument is not sufficient context for@@ -181,7 +160,7 @@ defaultCookieFilter :: URI -> Cookie -> IO Bool-defaultCookieFilter url cky = return True+defaultCookieFilter _url _cky = return True userCookieFilter :: URI -> Cookie -> IO Bool userCookieFilter url cky =@@ -230,59 +209,62 @@ cookie :: Parser Cookie cookie = do { name <- word- ; spaces+ ; spaces_l ; char '='- ; spaces- ; val <- cvalue+ ; spaces_l+ ; val1 <- cvalue ; args <- cdetail- ; return $ mkCookie name val args+ ; return $ mkCookie name val1 args } cvalue :: Parser String - spaces = many (satisfy isSpace)+ spaces_l = many (satisfy isSpace) - cvalue = quotedstring <|> many (satisfy $ not . (==';'))+ cvalue = quotedstring <|> many1 (satisfy $ not . (==';')) -- all keys in the result list MUST be in lower case cdetail :: Parser [(String,String)] cdetail = many $- try (do { spaces+ try (do { spaces_l ; char ';'- ; spaces+ ; spaces_l ; s1 <- word- ; spaces- ; s2 <- option "" (do { char '=' ; spaces ; v <- cvalue ; return v })+ ; spaces_l+ ; s2 <- option "" (do { char '=' ; spaces_l ; v <- cvalue ; return v }) ; return (map toLower s1,s2) }) mkCookie :: String -> String -> [(String,String)] -> Cookie- mkCookie nm val more = MkCookie { ckName=nm- , ckValue=val- , ckDomain=map toLower (fromMaybe dom (lookup "domain" more))- , ckPath=lookup "path" more- , ckVersion=lookup "version" more- , ckComment=lookup "comment" more- }+ mkCookie nm cval more = + MkCookie { ckName = nm+ , ckValue = cval+ , ckDomain = map toLower (fromMaybe dom (lookup "domain" more))+ , ckPath = lookup "path" more+ , ckVersion = lookup "version" more+ , ckComment = lookup "comment" more+ } +headerToCookies _ _ = []+ -- | Adds a cookie to the browser state, removing duplicates.-addCookie :: Cookie -> BrowserAction ()+addCookie :: Cookie -> BrowserAction t () addCookie c = alterBS (\b -> b { bsCookies=c : fn (bsCookies b) }) where fn = filter (not . (==c)) -setCookies :: [Cookie] -> BrowserAction ()+setCookies :: [Cookie] -> BrowserAction t () setCookies cs = alterBS (\b -> b { bsCookies=cs }) -getCookies :: BrowserAction [Cookie]+getCookies :: BrowserAction t [Cookie] getCookies = getBS bsCookies -- ...get domain specific cookies... -- ... this needs changing for consistency with rfc2109... -- ... currently too broad.-getCookiesFor :: String -> String -> BrowserAction [Cookie]+getCookiesFor :: String -> String -> BrowserAction t [Cookie] getCookiesFor dom path = do cks <- getCookies return (filter cookiematch cks)@@ -294,10 +276,10 @@ Just p -> p `isPrefixOf` path -setCookieFilter :: (URI -> Cookie -> IO Bool) -> BrowserAction ()+setCookieFilter :: (URI -> Cookie -> IO Bool) -> BrowserAction t () setCookieFilter f = alterBS (\b -> b { bsCookieFilter=f }) -getCookieFilter :: BrowserAction (URI -> Cookie -> IO Bool)+getCookieFilter :: BrowserAction t (URI -> Cookie -> IO Bool) getCookieFilter = getBS bsCookieFilter ------------------------------------------------------------------@@ -312,8 +294,8 @@ 3) pick a challenge to respond to, usually the strongest challenge understood by the client, using "pickChallenge" 4) generate a username/password combination using the browsers- "bsAuthorityGen" function (the default behaviour is to do nothing- which means to not retry with a new username/password combination)+ "bsAuthorityGen" function (the default behaviour is to ask+ the user) 5) build an Authority object based upon the challenge and user data, store this new Authority in the browser state 6) convert the Authority to a request header and add this@@ -370,7 +352,7 @@ headerToChallenge :: URI -> Header -> Maybe Challenge headerToChallenge baseURI (Header _ str) = case parse challenge "" str of- Left e -> Nothing+ Left{} -> Nothing Right (name,props) -> case name of "basic" -> mkBasic props "digest" -> mkDigest props@@ -395,13 +377,6 @@ ; return (map toLower nm,val) } - quotedstring =- do { char '"'- ; str <- many (satisfy (not.(=='"')))- ; char '"'- ; return str- } - mkBasic, mkDigest :: [(String,String)] -> Maybe Challenge mkBasic params = fmap ChalBasic (lookup "realm" params)@@ -431,14 +406,14 @@ -- Change These: readQop :: String -> [Qop]- readQop = catMaybes . (map strToQop) . (splitMany ',')+ readQop = catMaybes . (map strToQop) . (splitBy ',') - strToQop str = case map toLower (trim str) of+ strToQop qs = case map toLower (trim qs) of "auth" -> Just QopAuth "auth-int" -> Just QopAuthInt _ -> Nothing - readAlgorithm str = case map toLower (trim str) of+ readAlgorithm astr = case map toLower (trim astr) of "md5" -> Just AlgMD5 "md5-sess" -> Just AlgMD5sess _ -> Nothing@@ -462,7 +437,7 @@ -- | Return authorities for a given domain and path. -- Assumes "dom" is lower case-getAuthFor :: String -> String -> BrowserAction [Authority]+getAuthFor :: String -> String -> BrowserAction t [Authority] getAuthFor dom pth = do { list <- getAuthorities ; return (filter match list)@@ -477,22 +452,22 @@ -- | Interacting with browser state:-getAuthorities :: BrowserAction [Authority]+getAuthorities :: BrowserAction t [Authority] getAuthorities = getBS bsAuthorities -setAuthorities :: [Authority] -> BrowserAction ()+setAuthorities :: [Authority] -> BrowserAction t () setAuthorities as = alterBS (\b -> b { bsAuthorities=as }) -addAuthority :: Authority -> BrowserAction ()+addAuthority :: Authority -> BrowserAction t () addAuthority a = alterBS (\b -> b { bsAuthorities=a:bsAuthorities b }) -getAuthorityGen :: BrowserAction (URI -> String -> IO (Maybe (String,String)))+getAuthorityGen :: BrowserAction t (URI -> String -> IO (Maybe (String,String))) getAuthorityGen = getBS bsAuthorityGen -setAuthorityGen :: (URI -> String -> IO (Maybe (String,String))) -> BrowserAction ()+setAuthorityGen :: (URI -> String -> IO (Maybe (String,String))) -> BrowserAction t () setAuthorityGen f = alterBS (\b -> b { bsAuthorityGen=f }) -setAllowBasicAuth :: Bool -> BrowserAction ()+setAllowBasicAuth :: Bool -> BrowserAction t () setAllowBasicAuth ba = alterBS (\b -> b { bsAllowBasicAuth=ba }) @@ -505,7 +480,7 @@ -- | Retrieve a likely looking authority for a Request.-anticipateChallenge :: Request -> BrowserAction (Maybe Authority)+anticipateChallenge :: HTTPRequest ty -> BrowserAction t (Maybe Authority) anticipateChallenge rq = let uri = rqURI rq in do { authlist <- getAuthFor (uriToAuthorityString uri) (uriPath uri)@@ -514,7 +489,7 @@ -- | Asking the user to respond to a challenge-challengeToAuthority :: URI -> Challenge -> BrowserAction (Maybe Authority)+challengeToAuthority :: URI -> Challenge -> BrowserAction t (Maybe Authority) challengeToAuthority uri ch = -- prompt user for authority if answerable ch then@@ -528,7 +503,7 @@ where answerable :: Challenge -> Bool answerable (ChalBasic _) = True- answerable ch = (chAlgorithm ch) == Just AlgMD5+ answerable chall = (chAlgorithm chall) == Just AlgMD5 buildAuth :: Challenge -> String -> String -> Authority buildAuth (ChalBasic r) u p = @@ -541,7 +516,7 @@ -- note to self: this is a pretty stupid operation -- to perform isn't it? ChalX and AuthX are so very -- similar.- buildAuth (ChalDigest r d n o s a q) u p =+ buildAuth (ChalDigest r d n o _stale a q) u p = AuthDigest { auRealm=r , auUsername=u , auPassword=p@@ -557,12 +532,11 @@ -- the context of a specific request. If a client nonce -- was to be used then this function might need to -- be of type ... -> BrowserAction String-withAuthority :: Authority -> Request -> String+withAuthority :: Authority -> HTTPRequest ty -> String withAuthority a rq = case a of- AuthBasic _ _ user pass ->- "Basic " ++ base64encode (auUsername a ++ ':' : auPassword a)- AuthDigest _ _ _ _ _ _ _ _ ->- "Digest username=\"" ++ auUsername a + AuthBasic{} -> "Basic " ++ base64encode (auUsername a ++ ':' : auPassword a)+ AuthDigest{} ->+ "Digest username=\"" ++ auUsername a -- " ++ "\",realm=\"" ++ auRealm a ++ "\",nonce=\"" ++ auNonce a ++ "\",uri=\"" ++ digesturi@@ -577,19 +551,6 @@ ++ map toLower (kd (md5 a1) (noncevalue ++ ":" ++ md5 a2)) ++ "\"" - -- FIXME: these probably only work right for latin-1 strings- stringToOctets :: String -> [Octet]- stringToOctets = map (fromIntegral . fromEnum)- octetsToString :: [Octet] -> String- octetsToString = map (toEnum . fromIntegral)- base64encode :: String -> String- base64encode = Base64.encode . stringToOctets- md5 :: String -> String- md5 = octetsToString . MD5.hash . stringToOctets-- kd :: String -> String -> String- kd a b = md5 (a ++ ":" ++ b)- a1, a2 :: String a1 = auUsername a ++ ":" ++ auRealm a ++ ":" ++ auPassword a @@ -605,7 +566,23 @@ digesturi = show (rqURI rq) noncevalue = auNonce a +-- FIXME: these probably only work right for latin-1 strings+stringToOctets :: String -> [Octet]+stringToOctets = map (fromIntegral . fromEnum) +octetsToString :: [Octet] -> String+octetsToString = map (toEnum . fromIntegral)++base64encode :: String -> String+base64encode = Base64.encode . stringToOctets++md5 :: String -> String+md5 = octetsToString . MD5.hash . stringToOctets++kd :: String -> String -> String+kd a b = md5 (a ++ ":" ++ b)++ ------------------------------------------------------------------ ------------------ Proxy Stuff ----------------------------------- ------------------------------------------------------------------@@ -620,368 +597,370 @@ ------------------------------------------------------------------ -data BrowserState = BS { bsErr, bsOut :: String -> IO ()- , bsCookies :: [Cookie]- , bsCookieFilter :: URI -> Cookie -> IO Bool- , bsAuthorityGen :: URI -> String -> IO (Maybe (String,String))- , bsAuthorities :: [Authority]- , bsAllowRedirects :: Bool- , bsAllowBasicAuth :: Bool- , bsConnectionPool :: [Connection]- , bsProxy :: Proxy- , bsDebug :: Maybe String- }+data BrowserState connection+ = BS { bsErr, bsOut :: String -> IO ()+ , bsCookies :: [Cookie]+ , bsCookieFilter :: URI -> Cookie -> IO Bool+ , bsAuthorityGen :: URI -> String -> IO (Maybe (String,String))+ , bsAuthorities :: [Authority]+ , bsAllowRedirects :: Bool+ , bsAllowBasicAuth :: Bool+ , bsConnectionPool :: [connection]+ , bsProxy :: Proxy+ , bsDebug :: Maybe String+ } -instance Show BrowserState where+instance Show (BrowserState t) where show bs = "BrowserState { " - ++ show (bsCookies bs) ++ "\n"+ ++ shows (bsCookies bs) ("\n" {- ++ show (bsAuthorities bs) ++ "\n"-}- ++ "AllowRedirects: " ++ show (bsAllowRedirects bs)- ++ "} "+ ++ "AllowRedirects: " ++ shows (bsAllowRedirects bs) "} ") -- Simple DIY stateful behaviour, with IO-data BrowserAction a = BA { lift :: (BrowserState -> IO (BrowserState,a)) }+data BrowserAction conn a + = BA { lift :: BrowserState conn -> IO (BrowserState conn,a) } -instance Monad BrowserAction where+instance Monad (BrowserAction conn) where a >>= f = BA (\b -> do { (nb,v) <- lift a b ; lift (f v) nb}) return x = BA (\b -> return (b,x)) -instance Functor BrowserAction where+instance Functor (BrowserAction conn) where fmap f = liftM f - -- | Apply a browser action to a state.-browse :: BrowserAction a -> IO a-browse act = do (bs, x) <- lift act defaultBrowserState- closePooledConnections bs- return x- where- defaultBrowserState :: BrowserState- defaultBrowserState = - BS { bsErr = putStrLn- , bsOut = putStrLn- , bsCookies = []- , bsCookieFilter = defaultCookieFilter- , bsAuthorityGen = \_ _ -> return Nothing- , bsAuthorities = []- , bsAllowRedirects = True- , bsAllowBasicAuth = False- , bsConnectionPool = []- , bsProxy = NoProxy- , bsDebug = Nothing - }+browse :: BrowserAction conn a -> IO a+browse act = do x <- lift act defaultBrowserState+ return (snd x) --- |--- Close all connections that are in bs' connection pool.--- This should have some sort of exception handling, soldier on until--- all the connections have been closed. Not sure about portability--- issues.-closePooledConnections :: BrowserState -> IO ()-closePooledConnections = mapM_ close . bsConnectionPool+defaultBrowserState :: BrowserState t+defaultBrowserState = + BS { bsErr = putStrLn+ , bsOut = putStrLn+ , bsCookies = []+ , bsCookieFilter = defaultCookieFilter+ , bsAuthorityGen = (error "bsAuthGen wanted")+ , bsAuthorities = []+ , bsAllowRedirects = True+ , bsAllowBasicAuth = False+ , bsConnectionPool = []+ , bsProxy = NoProxy+ , bsDebug = Nothing + } -- | Alter browser state-alterBS :: (BrowserState -> BrowserState) -> BrowserAction ()+alterBS :: (BrowserState t -> BrowserState t) -> BrowserAction t () alterBS f = BA (\b -> return (f b,())) -getBS :: (BrowserState -> a) -> BrowserAction a+getBS :: (BrowserState t -> a) -> BrowserAction t a getBS f = BA (\b -> return (b,f b)) -- | Do an io action-ioAction :: IO a -> BrowserAction a+ioAction :: IO a -> BrowserAction t a ioAction a = BA (\b -> a >>= \v -> return (b,v)) - -- Stream handlers-setErrHandler, setOutHandler :: (String -> IO ()) -> BrowserAction ()+setErrHandler, setOutHandler :: (String -> IO ()) -> BrowserAction t () setErrHandler h = alterBS (\b -> b { bsErr=h }) setOutHandler h = alterBS (\b -> b { bsOut=h }) -out, err :: String -> BrowserAction ()+out, err :: String -> BrowserAction t () out s = do { f <- getBS bsOut ; ioAction $ f s } err s = do { f <- getBS bsErr ; ioAction $ f s } -- Redirects-setAllowRedirects :: Bool -> BrowserAction ()+setAllowRedirects :: Bool -> BrowserAction t () setAllowRedirects bl = alterBS (\b -> b {bsAllowRedirects=bl}) -getAllowRedirects :: BrowserAction Bool+getAllowRedirects :: BrowserAction t Bool getAllowRedirects = getBS bsAllowRedirects -- Proxy-setProxy :: Proxy -> BrowserAction ()+setProxy :: Proxy -> BrowserAction t () setProxy p = alterBS (\b -> b {bsProxy = p}) -getProxy :: BrowserAction Proxy+getProxy :: BrowserAction t Proxy getProxy = getBS bsProxy -- Debug-setDebugLog :: Maybe String -> BrowserAction ()+setDebugLog :: Maybe String -> BrowserAction t () setDebugLog v = alterBS (\b -> b {bsDebug=v}) -- Page control-type RequestState = ( Int -- number of 401 responses so far- , Int -- number of redirects so far- , Int -- number of retrys so far- , Bool -- whether to pre-empt 401 response- )-+data RequestState + = RequestState+ { reqDenies :: Int -- ^ number of 401 responses so far+ , reqRedirects :: Int -- ^ number of redirects so far+ , reqRetries :: Int -- ^ number of retrys so far+ , reqStopOnDeny :: Bool -- ^ whether to pre-empt 401 response+ } +nullRequestState :: RequestState+nullRequestState = RequestState+ { reqDenies = 0+ , reqRedirects = 0+ , reqRetries = 0+ , reqStopOnDeny = True+ } --- Surely the most important bit:-request :: Request -> BrowserAction (URI,Response)-request = request' initialState- where- initialState = (0,0,0,True)+-- limits we are willing to not go beyond for method retries and number of auth deny responses.+maxRetries :: Int+maxRetries = 4 +maxDenies :: Int+maxDenies = 2 -request' :: RequestState -> Request -> BrowserAction (URI,Response)-request' (denycount,redirectcount,retrycount,preempt) rq =- do -- add cookies to request- let uri = rqURI rq- cookies <- getCookiesFor (uriToAuthorityString uri) (uriPath uri)- - when (not $ null cookies) - (out $ "Adding cookies to request. Cookie names: " - ++ foldl spaceappend "" (map ckName cookies))- - -- add credentials to request- rq' <- if not preempt then return rq else- do { auth <- anticipateChallenge rq- ; case auth of- Just x -> return (insertHeader HdrAuthorization (withAuthority x rq) rq)- Nothing -> return rq- }- - let rq'' = insertHeaders (map cookieToHeader cookies) rq'+-- Surely the most important bit:+request :: HStream ty+ => HTTPRequest ty+ -> BrowserAction (HandleStream ty) (URI,HTTPResponse ty)+request req = request' nullVal initialState req+ where+ initialState = nullRequestState+ nullVal = buf_empty bufferOps - p <- getProxy+request' :: HStream ty+ => ty+ -> RequestState+ -> HTTPRequest ty+ -> BrowserAction (HandleStream ty) (URI,HTTPResponse ty)+request' nullVal rqState rq = do+ -- add cookies to request+ let uri = rqURI rq+ cookies <- getCookiesFor (uriToAuthorityString uri) (uriPath uri)+ when (not $ null cookies) + (out $ "Adding cookies to request. Cookie names: " +++ foldl spaceappend "" (map ckName cookies))+ -- add credentials to request+ rq' <- + if not (reqStopOnDeny rqState) + then return rq + else do + auth <- anticipateChallenge rq+ case auth of+ Nothing -> return rq+ Just x -> return (insertHeader HdrAuthorization (withAuthority x rq) rq)+ let rq'' = insertHeaders (map cookieToHeader cookies) rq'+ p <- getProxy - out ("Sending:\n" ++ show rq'') - e_rsp <- case p of- NoProxy -> dorequest (uriAuth $ rqURI rq'') rq''- Proxy str ath ->- let rq''' = case ath of - Nothing -> rq''- Just x -> insertHeader HdrProxyAuthorization (withAuthority x rq'') rq''- -- Proxy can take multiple forms - look for http://host:port first,- -- then host:port. Fall back to just the string given (probably a host name).- proxyURIAuth =- maybe notURI- (\parsed -> maybe notURI- id (uriAuthority parsed))- (parseURI str)- notURI =- -- If the ':' is dropped from port below, dorequest will assume port 80. Leave it!- let (host, port) = span (':'/=) str- in- if null port || null host- then URIAuth "" str ""- else URIAuth "" host port - in- do- out $ "proxy uri host: " ++ uriRegName proxyURIAuth ++ ", port: " ++ uriPort proxyURIAuth- dorequest proxyURIAuth rq'''- case e_rsp of- Left v -> if (retrycount < 4) && (v == ErrorReset || v == ErrorClosed)- then request' (denycount,redirectcount,retrycount+1,preempt) rq- else error ("Exception raised in request: " ++ show v)- Right rsp -> do - out ("Received:\n" ++ show rsp)+ out ("Sending:\n" ++ show rq'') + e_rsp <- + case p of+ NoProxy -> dorequest (uriAuth $ rqURI rq'') rq''+ Proxy str ath -> do+ let rq''' = maybe rq'' + (\x -> insertHeader HdrProxyAuthorization (withAuthority x rq'') rq'')+ ath+ let notURI + | null pt || null hst = + URIAuth{ uriUserInfo = ""+ , uriRegName = str+ , uriPort = ""+ }+ | otherwise = + URIAuth{ uriUserInfo = ""+ , uriRegName = hst+ , uriPort = pt+ }+ -- If the ':' is dropped from port below, dorequest will assume port 80. Leave it!+ where (hst, pt) = span (':'/=) str+ -- Proxy can take multiple forms - look for http://host:port first,+ -- then host:port. Fall back to just the string given (probably a host name).+ let proxyURIAuth =+ maybe notURI+ (\parsed -> maybe notURI id (uriAuthority parsed))+ (parseURI str) - -- add new cookies to browser state- let cookieheaders = retrieveHeaders HdrSetCookie rsp- let newcookies = concat (map (headerToCookies $ uriToAuthorityString uri) cookieheaders)+ out $ "proxy uri host: " ++ uriRegName proxyURIAuth ++ ", port: " ++ uriPort proxyURIAuth+ dorequest proxyURIAuth rq'''+ case e_rsp of+ Left v + | (reqRetries rqState < maxRetries) && (v == ErrorReset || v == ErrorClosed) ->+ request' nullVal rqState{reqRetries=reqRetries rqState + 1} rq+ | otherwise -> error ("Exception raised in request: " ++ show v)+ Right rsp -> do + out ("Received:\n" ++ show rsp)+ -- add new cookies to browser state+ let cookieheaders = retrieveHeaders HdrSetCookie rsp+ let newcookies = concat (map (headerToCookies $ uriToAuthorityString uri) cookieheaders) - when (not $ null newcookies)- (out $ foldl (\x y -> x ++ "\n " ++ show y) "Cookies received:" newcookies)+ when (not $ null newcookies)+ (out $ foldl (\x y -> x ++ "\n " ++ show y) "Cookies received:" newcookies) - filterfn <- getCookieFilter- newcookies' <- ioAction (filterM (filterfn uri) newcookies)- foldM (\_ -> addCookie) () newcookies'+ filterfn <- getCookieFilter+ newcookies' <- ioAction (filterM (filterfn uri) newcookies)+ foldM (\_ -> addCookie) () newcookies' - when (not $ null newcookies)- (out $ "Accepting cookies with names: " ++ foldl spaceappend "" (map ckName newcookies'))+ when (not $ null newcookies)+ (out $ "Accepting cookies with names: " ++ foldl spaceappend "" (map ckName newcookies')) - case rspCode rsp of- (4,0,1) -> -- Credentials not sent or refused.- out "401 - credentials not sent or refused" >>- if denycount > 2 then return (uri,rsp) else- do { let hdrs = retrieveHeaders HdrWWWAuthenticate rsp- ; case pickChallenge (catMaybes $ map (headerToChallenge uri) hdrs) of- Just x ->- do { au <- challengeToAuthority uri x- ; case au of- Just au' ->- out "Retrying request with new credentials" >>- request' (denycount+1,redirectcount,retrycount,False)- (insertHeader HdrAuthorization (withAuthority au' rq) rq)- Nothing -> return (uri,rsp) {- do nothing -}- }- - Nothing -> return (uri,rsp) {- do nothing -}- }- -- (4,0,7) -> -- Proxy Authentication required- out "407 - proxy authentication required" >>- if denycount > 2 then return (uri,rsp) else- do { let hdrs = retrieveHeaders HdrProxyAuthenticate rsp- ; case pickChallenge (catMaybes $ map (headerToChallenge uri) hdrs) of- Just x ->- do { au <- challengeToAuthority uri x- ; case au of- Just au' ->- do { pxy <- getBS bsProxy- ; case pxy of- NoProxy ->- do { err "Proxy authentication required without proxy!"- ; return (uri,rsp)- }- Proxy x y ->- do { out "Retrying with proxy authentication"- ; setProxy (Proxy x $ Just au')- ; request' (denycount+1,redirectcount,retrycount,False) rq- }- } - Nothing -> return (uri,rsp) {- do nothing -}- }- - Nothing -> return (uri,rsp) {- do nothing -}- }+ case rspCode rsp of+ (4,0,1) -- Credentials not sent or refused.+ | reqDenies rqState > maxDenies -> do+ out "401 - credentials again refused; exceeded retry count (2)"+ return (uri,rsp)+ | otherwise -> do+ out "401 - credentials not supplied or refused; retrying.."+ let hdrs = retrieveHeaders HdrWWWAuthenticate rsp+ case pickChallenge (catMaybes $ map (headerToChallenge uri) hdrs) of+ Nothing -> return (uri,rsp) {- do nothing -}+ Just x -> do+ au <- challengeToAuthority uri x+ case au of+ Nothing -> return (uri,rsp) {- do nothing -}+ Just au' -> do+ out "Retrying request with new credentials"+ request' nullVal+ rqState{reqDenies=reqDenies rqState + 1, reqStopOnDeny=False}+ (insertHeader HdrAuthorization (withAuthority au' rq) rq) + (4,0,7) -- Proxy Authentication required+ | reqDenies rqState > maxDenies -> do+ out "407 - proxy authentication required; max deny count exceeeded (2)"+ return (uri,rsp)+ | otherwise -> do+ out "407 - proxy authentication required"+ let hdrs = retrieveHeaders HdrProxyAuthenticate rsp+ case pickChallenge (catMaybes $ map (headerToChallenge uri) hdrs) of+ Nothing -> return (uri,rsp) {- do nothing -}+ Just x -> do+ au <- challengeToAuthority uri x+ case au of+ Nothing -> return (uri,rsp) {- do nothing -}+ Just au' -> do+ pxy <- getBS bsProxy+ case pxy of+ NoProxy -> do+ err "Proxy authentication required without proxy!"+ return (uri,rsp)+ Proxy px _ -> do+ out "Retrying with proxy authentication"+ setProxy (Proxy px (Just au'))+ request' nullVal+ rqState{reqDenies=reqDenies rqState + 1, reqStopOnDeny=False}+ rq - (3,0,3) -> -- Redirect using GET request method.- do { out "303 - redirect using GET"- ; rd <- getAllowRedirects- ; if not rd || redirectcount > 4 then return (uri,rsp) else- case retrieveHeaders HdrLocation rsp of- (Header _ u:_) -> case parseURIReference u of- Just newuri ->- let newuri' = case newuri `relativeTo` uri of- Nothing -> newuri- Just x -> x- in do { out ("Redirecting to " ++ show newuri' ++ " ...") - ; let rq = rq { rqMethod=GET, rqURI=newuri', rqBody="" }- ; request' (0,redirectcount+1,retrycount,True)- (replaceHeader HdrContentLength "0" rq) - }- Nothing ->- do { err ("Parse of Location header in a redirect response failed: " ++ u)- ; return (uri,rsp)- }- [] -> do { err "No Location header in redirect response"- ; return (uri,rsp)- }- }- - (3,0,5) ->- case retrieveHeaders HdrLocation rsp of- (Header _ u:_) -> case parseURIReference u of- Just newuri ->- do { out ("Retrying with proxy " ++ show newuri ++ "...")- ; setProxy (Proxy (uriToAuthorityString newuri) Nothing)- ; request' (0,0,retrycount+1,True) rq- }- Nothing ->- do { err ("Parse of Location header in a proxy redirect response failed: " ++ u)- ; return (uri,rsp)- }- [] -> do { err "No Location header in proxy redirect response."- ; return (uri,rsp)- }- + (3,0,x) | x == 3 || x == 2 -> do -- Redirect using GET request method.+ out ("30" ++ show x ++ " - redirect using GET")+ rd <- getAllowRedirects+ if not rd || reqRedirects rqState > maxRetries + then return (uri,rsp)+ else + case retrieveHeaders HdrLocation rsp of+ [] -> do + err "No Location header in redirect response"+ return (uri,rsp)+ (Header _ u:_) -> + case parseURIReference u of+ Nothing -> do+ err ("Parse of Location header in a redirect response failed: " ++ u)+ return (uri,rsp)+ Just newuri -> do+ out ("Redirecting to " ++ show newuri' ++ " ...") + let rq1 = rq { rqMethod=GET, rqURI=newuri', rqBody=nullVal }+ request' nullVal+ rqState{reqDenies=0, reqRedirects=reqRedirects rqState + 1, reqStopOnDeny=True}+ (replaceHeader HdrContentLength "0" rq1)+ where+ newuri' = maybe newuri id (newuri `relativeTo` uri) - (3,_,_) -> redirect uri rsp- _ -> return (uri,rsp)+ (3,0,5) ->+ case retrieveHeaders HdrLocation rsp of+ [] -> do + err "No Location header in proxy redirect response."+ return (uri,rsp)+ (Header _ u:_) -> + case parseURIReference u of+ Nothing -> do+ err ("Parse of Location header in a proxy redirect response failed: " ++ u)+ return (uri,rsp)+ Just newuri -> do+ out ("Retrying with proxy " ++ show newuri ++ "...")+ setProxy (Proxy (uriToAuthorityString newuri) Nothing)+ request' nullVal rqState{ reqDenies=0+ , reqRedirects=0+ , reqRetries=reqRetries rqState + 1+ , reqStopOnDeny=True+ }+ rq+ (3,_,_) -> redirect uri rsp+ _ -> return (uri,rsp) - where - spaceappend :: String -> String -> String- spaceappend x y = x ++ ' ' : y+ where + redirect uri rsp = do+ rd <- getAllowRedirects+ if not rd || reqRedirects rqState > maxRetries+ then return (uri,rsp) + else do+ case retrieveHeaders HdrLocation rsp of+ [] -> do + err "No Location header in redirect response."+ return (uri,rsp)+ (Header _ u:_) -> + case parseURIReference u of+ Just newuri -> do+ let newuri' = maybe newuri id (newuri `relativeTo` uri)+ out ("Redirecting to " ++ show newuri' ++ " ...") + request' nullVal+ rqState{reqDenies=0, reqRedirects=reqRedirects rqState + 1, reqStopOnDeny=True}+ rq{rqURI=newuri'}+ Nothing -> do+ err ("Parse of Location header in a redirect response failed: " ++ u)+ return (uri,rsp) - redirect uri rsp = do- rd <- getAllowRedirects- if not rd || redirectcount > 4 then return (uri,rsp) else do- case retrieveHeaders HdrLocation rsp of- (Header _ u:_) -> case parseURIReference u of- Just newuri -> do- let newuri' = case newuri `relativeTo` uri of- Nothing -> newuri- Just x -> x- out ("Redirecting to " ++ show newuri' ++ " ...") - request' (0,redirectcount+1,retrycount,True) (rq { rqURI=newuri' }) - Nothing -> do- err ("Parse of Location header in a redirect response failed: " ++ u)- return (uri,rsp)- [] -> do err "No Location header in redirect response."- return (uri,rsp)+spaceappend :: String -> String -> String+spaceappend x y = x ++ ' ' : y - dorequest :: URIAuth -> Request -> BrowserAction (Either ConnError Response)- dorequest hst rqst = +dorequest :: (HStream ty)+ => URIAuth -> HTTPRequest ty -> BrowserAction (HandleStream ty) (Either ConnError (HTTPResponse ty))+dorequest hst rqst = do { pool <- getBS bsConnectionPool- ; conn <- ioAction $ filterM (\c -> c `isConnectedTo` uriAuthToString hst) pool+ ; conn <- ioAction $ filterM (\c -> c `isTCPConnectedTo` uriAuthToString hst) pool ; rsp <- case conn of [] -> do { out ("Creating new connection to " ++ uriAuthToString hst)- ; let port = case uriPort hst of+ ; let aport = case uriPort hst of (':':s) -> read s _ -> 80- ; c <- ioAction $ openTCPPort (uriRegName hst) port+ ; c <- ioAction $ openStream (uriRegName hst) aport ; let pool' = if length pool > 5 then init pool else pool ; when (length pool > 5) (ioAction $ close (last pool)) ; alterBS (\b -> b { bsConnectionPool=c:pool' })- ; dorequest2 hst c rqst+ ; dorequest2 c rqst } (c:_) -> do { out ("Recovering connection to " ++ uriAuthToString hst)- ; dorequest2 hst c rqst+ ; dorequest2 c rqst } ; ; return rsp }-- dorequest2 hst c r =- do { dbg <- getBS bsDebug- ; ioAction $ case dbg of- Nothing -> sendHTTP c r- Just f ->- debugStream (f++'-': uriAuthToString hst) c - >>= \c' -> sendHTTP c' r - }+ where+ dorequest2 c r = do+ dbg <- getBS bsDebug+ ioAction $ + case dbg of+ Nothing -> sendHTTP c r+ Just f -> do+ c' <- debugByteStream (f++'-': uriAuthToString hst) c+ sendHTTP c' r -- uriAuth :: URI -> URIAuth uriAuth x = case uriAuthority x of Just ua -> ua _ -> error ("No uri authority for: "++show x) -uriAuthToString :: URIAuth -> String-uriAuthToString URIAuth { uriUserInfo = uinfo- , uriRegName = regname- , uriPort = port- } =- ((if null uinfo then id else (uinfo++))- . (regname++)- . (port++)) "" --- This function duplicates old Network.URI.authority behaviour.-uriToAuthorityString :: URI -> String-uriToAuthorityString u = maybe "" uriAuthToString (uriAuthority u)--- ------------------------------------------------------------------ ------------------ Request Building ------------------------------ ------------------------------------------------------------------ - libUA :: String libUA = "haskell-libwww/0.1" @@ -995,8 +974,6 @@ , rqMethod=GET } -- -- This form junk is completely untested... type FormVar = (String,String)@@ -1019,3 +996,4 @@ , rqBody=enc , rqURI=u }+ _ -> error ("unexpected request: " ++ show m)
+ Network/BufferType.hs view
@@ -0,0 +1,132 @@+{-# LANGUAGE TypeSynonymInstances #-} +----------------------------------------------------------------------------- +-- | +-- Module : Network.BufferType +-- Copyright : (c) Warrick Gray 2002, Bjorn Bringert 2003-2004, Simon Foster 2004, 2007 Robin Bate Boerop, 2008 Sigbjorn Finne +-- License : BSD +-- +-- Maintainer : Sigbjorn Finne <sigbjorn.finne@gmail.com> +-- Stability : experimental +-- Portability : non-portable (not tested) +-- +-- Operations over wire-transmitted values. +-- +----------------------------------------------------------------------------- +module Network.BufferType + ( + BufferType(..) + + , BufferOp(..) + , strictBufferOp + , lazyBufferOp + , stringBufferOp + ) where + + +import qualified Data.ByteString as Strict hiding ( unpack, pack, span ) +import qualified Data.ByteString.Char8 as Strict ( unpack, pack, span ) +import qualified Data.ByteString.Lazy as Lazy hiding ( pack, unpack,span ) +import qualified Data.ByteString.Lazy.Char8 as Lazy ( pack, unpack, span ) +import System.IO ( Handle ) +import Data.Word ( Word8 ) + +-- | The @BufferType@ class encodes, in a mixed-mode way, the interface +-- that the library requires to operate over data embedded in HTTP +-- requests and responses. That is, we use explicit dictionaries +-- for the operations, but overload the name of these dicts. +-- +class BufferType bufType where + bufferOps :: BufferOp bufType + +instance BufferType Lazy.ByteString where + bufferOps = lazyBufferOp + +instance BufferType Strict.ByteString where + bufferOps = strictBufferOp + +instance BufferType String where + bufferOps = stringBufferOp + +-- Encode the I/O operations of the underlying buffer over a Handle +-- in an (explicit) dictionary type. May not be needed, but gives +-- us flexibility in explicit overriding and wrapping up of these methods. +data BufferOp a + = BufferOp + { buf_hGet :: Handle -> Int -> IO a + , buf_hGetContents :: Handle -> IO a + , buf_hPut :: Handle -> a -> IO () + , buf_hGetLine :: Handle -> IO a + , buf_empty :: a + , buf_append :: a -> a -> a + , buf_fromStr :: String -> a + , buf_toStr :: a -> String + , buf_snoc :: a -> Word8 -> a + , buf_splitAt :: Int -> a -> (a,a) + , buf_span :: (Char -> Bool) -> a -> (a,a) + , buf_isLineTerm :: a -> Bool + , buf_isEmpty :: a -> Bool + } + +instance Eq (BufferOp a) where + _ == _ = False + +strictBufferOp :: BufferOp Strict.ByteString +strictBufferOp = + BufferOp + { buf_hGet = Strict.hGet + , buf_hGetContents = Strict.hGetContents + , buf_hPut = Strict.hPut + , buf_hGetLine = Strict.hGetLine + , buf_append = Strict.append + , buf_fromStr = Strict.pack + , buf_toStr = Strict.unpack + , buf_snoc = Strict.snoc + , buf_splitAt = Strict.splitAt + , buf_span = Strict.span + , buf_empty = Strict.empty + , buf_isLineTerm = \ b -> Strict.length b == 2 && crlf == b + , buf_isEmpty = Strict.null + } + where + crlf = Strict.pack "\r\n" + +lazyBufferOp :: BufferOp Lazy.ByteString +lazyBufferOp = + BufferOp + { buf_hGet = Lazy.hGet + , buf_hGetContents = Lazy.hGetContents + , buf_hPut = Lazy.hPut + , buf_hGetLine = \ h -> Strict.hGetLine h >>= \ l -> return (Lazy.fromChunks [l]) + , buf_append = Lazy.append + , buf_fromStr = Lazy.pack + , buf_toStr = Lazy.unpack + , buf_snoc = Lazy.snoc + , buf_splitAt = \ i x -> Lazy.splitAt (fromIntegral i) x + , buf_span = Lazy.span + , buf_empty = Lazy.empty + , buf_isLineTerm = \ b -> Lazy.length b == 2 && crlf == b + , buf_isEmpty = Lazy.null + } + where + crlf = Lazy.pack "\r\n" + +stringBufferOp :: BufferOp String +stringBufferOp =BufferOp + { buf_hGet = \ h n -> Strict.hGet h n >>= return . Strict.unpack + , buf_hGetContents = \ h -> Strict.hGetContents h >>= return . Strict.unpack + , buf_hPut = \ h s -> Strict.hPut h (Strict.pack s) + , buf_hGetLine = \ h -> Strict.hGetLine h >>= return . Strict.unpack + , buf_append = (++) + , buf_fromStr = id + , buf_toStr = id + , buf_snoc = \ a x -> a ++ [toEnum (fromIntegral x)] + , buf_splitAt = splitAt + , buf_span = \ p a -> + case Strict.span p (Strict.pack a) of + (x,y) -> (Strict.unpack x, Strict.unpack y) + , buf_empty = [] + , buf_isLineTerm = \ b -> b == crlf + , buf_isEmpty = null + } + where + crlf = "\r\n"
Network/HTTP.hs view
@@ -93,640 +93,31 @@ -- had been added (in which case 417 response is returned). -- ------------------------------------------------------------------------------module Network.HTTP (- module Network.Stream,- module Network.TCP,-- -- ** Constants- httpVersion,- - -- ** HTTP - Request(..),- RequestData,- Response(..),- RequestMethod(..),- ResponseCode,- simpleHTTP, simpleHTTP_,- sendHTTP,- receiveHTTP,- processRequest,- getRequestHead,- respondHTTP,-- -- ** Header Functions- module Network.HTTP.Headers,-- -- ** URL Encoding- urlEncode,- urlDecode,- urlEncodeVars,+module Network.HTTP + ( module Network.HTTP.HandleStream+ , module Network.HTTP.Base+ , module Network.HTTP.Headers - -- ** URI authority parsing- URIAuthority(..),- getAuth,- parseURIAuthority-) where+{-+ , simpleHTTP -- :: Request -> IO (Result Response)+ , simpleHTTP_ -- :: Stream s => s -> Request -> IO (Result Response)+ , sendHTTP -- :: Stream s => s -> Request -> IO (Result Response)+ , receiveHTTP -- :: Stream s => s -> IO (Result Request)+ , respondHTTP -- :: Stream s => s -> Response -> IO ()+-}+ , module Network.TCP + ) where ----------------------------------------------------------------- ------------------ Imports -------------------------------------- ----------------------------------------------------------------- -import Network.URI- ( URI(URI, uriScheme, uriAuthority, uriPath)- , URIAuth(uriUserInfo, uriRegName, uriPort)- , parseURIReference- , unEscapeString, escapeURIString, isUnescapedInURI- ) import Network.HTTP.Headers-import Network.Stream-import Network.StreamDebugger (debugStream)-import Network.TCP (openTCPPort)--import Control.Exception as Exception (catch, throw)-import Data.Bits ((.&.))-import Data.Char (isSpace, intToDigit, digitToInt, ord, chr, toLower)-import Data.List (partition, intersperse)-import Data.Maybe (listToMaybe, fromMaybe)-import Control.Monad (when, guard)-import Numeric (readHex)-import Text.Read.Lex (readDecP)-import Text.ParserCombinators.ReadP- ( ReadP, readP_to_S, char, (<++), look, munch )-import Data.Typeable---- 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 --------------------------------------------------------------------------------------------------------------- remove leading and trailing whitespace.-trim :: String -> String-trim = let dropspace = dropWhile isSpace in- reverse . dropspace . reverse . dropspace---crlf, sp :: String-crlf = "\r\n"-sp = " "-------------------------------------------------------------------------------------- URI Authority parsing --------------------------------------------------------------------------------------------data URIAuthority = URIAuthority { user :: Maybe String, - password :: Maybe String,- host :: String,- port :: Maybe Int- } deriving (Eq,Show)---- | Parse the authority part of a URL.------ > RFC 1738, section 3.1:--- >--- > //<user>:<password>@<host>:<port>/<url-path>--- > Some or all of the parts "<user>:<password>@", ":<password>",--- > ":<port>", and "/<url-path>" may be excluded.-parseURIAuthority :: String -> Maybe URIAuthority-parseURIAuthority s = listToMaybe (map fst (readP_to_S pURIAuthority s))---pURIAuthority :: ReadP URIAuthority-pURIAuthority = do- (u,pw) <- (pUserInfo `before` char '@') - <++ return (Nothing, Nothing)- h <- munch (/=':')- p <- orNothing (char ':' >> readDecP)- look >>= guard . null - return URIAuthority{ user=u, password=pw, host=h, port=p }--pUserInfo :: ReadP (Maybe String, Maybe String)-pUserInfo = do- u <- orNothing (munch (`notElem` ":@"))- p <- orNothing (char ':' >> munch (/='@'))- return (u,p)--before :: Monad m => m a -> m b -> m a-before a b = a >>= \x -> b >> return x--orNothing :: ReadP a -> ReadP (Maybe a)-orNothing p = fmap Just p <++ return Nothing-------------------------------------------------------------------------------------- HTTP Messages ------------------------------------------------------------------------------------------------------- Protocol version-httpVersion :: String-httpVersion = "HTTP/1.1"----- | The HTTP request method, to be used in the 'Request' object.--- We are missing a few of the stranger methods, but these are--- not really necessary until we add full TLS.-data RequestMethod = HEAD | PUT | GET | POST | DELETE | OPTIONS | TRACE | Custom String- deriving(Show,Eq)--rqMethodMap :: [(String, RequestMethod)]-rqMethodMap = [("HEAD", HEAD),- ("PUT", PUT),- ("GET", GET),- ("POST", POST),- ("DELETE", DELETE),- ("OPTIONS", OPTIONS),- ("TRACE", TRACE)]---- | An HTTP Request.--- The 'Show' instance of this type is used for message serialisation,--- which means no body data is output.-data Request =- Request { rqURI :: URI -- ^ might need changing in future- -- 1) to support '*' uri in OPTIONS request- -- 2) transparent support for both relative- -- & absolute uris, although this should- -- already work (leave scheme & host parts empty).- , rqMethod :: RequestMethod - , rqHeaders :: [Header]- , rqBody :: String- } deriving (Typeable)+import Network.HTTP.Base+--import Network.HTTP.Stream+import Network.HTTP.HandleStream+import Network.TCP --- Notice that request body is not included,--- this show function is used to serialise--- a request for the transport link, we send--- the body separately where possible.-instance Show Request where- show (Request u m h _) =- show m ++ sp ++ alt_uri ++ sp ++ httpVersion ++ crlf- ++ foldr (++) [] (map show h) ++ crlf- where- alt_uri = show $ if null (uriPath u) || head (uriPath u) /= '/' - then u { uriPath = '/' : uriPath u } - else u--instance HasHeaders Request where- getHeaders = rqHeaders- setHeaders rq hdrs = rq { rqHeaders=hdrs }--type ResponseCode = (Int,Int,Int)-type ResponseData = (ResponseCode,String,[Header])-type RequestData = (RequestMethod,URI,[Header])---- | An HTTP Response.--- The 'Show' instance of this type is used for message serialisation,--- which means no body data is output, additionally the output will--- show an HTTP version of 1.1 instead of the actual version returned--- by a server.-data Response =- Response { rspCode :: ResponseCode- , rspReason :: String- , rspHeaders :: [Header]- , rspBody :: String- } deriving (Typeable)- --- This is an invalid representation of a received response, --- since we have made the assumption that all responses are HTTP/1.1-instance Show Response where- show (Response (a,b,c) reason headers _) =- httpVersion ++ ' ' : map intToDigit [a,b,c] ++ ' ' : reason ++ crlf- ++ foldr (++) [] (map show headers) ++ crlf--instance HasHeaders Response where- getHeaders = rspHeaders- setHeaders rsp hdrs = rsp { rspHeaders=hdrs }-------------------------------------------------------------------------------------- Parsing ------------------------------------------------------------------------------------------------------------ Parsing a request-parseRequestHead :: [String] -> Result RequestData-parseRequestHead [] = Left ErrorClosed-parseRequestHead (com:hdrs) =- requestCommand com `bindE` \(version,rqm,uri) ->- parseHeaders hdrs `bindE` \hdrs' ->- Right (rqm,uri,hdrs')- where- requestCommand line- = case words line of- yes@(rqm:uri:version) -> case (parseURIReference uri, lookup rqm rqMethodMap) of- (Just u, Just r) -> Right (version,r,u)- _ -> Left (ErrorParse $ "Request command line parse failure: " ++ line)- no -> if null line- then Left ErrorClosed- else Left (ErrorParse $ "Request command line parse failure: " ++ line) ---- Parsing a response-parseResponseHead :: [String] -> Result ResponseData-parseResponseHead [] = Left ErrorClosed-parseResponseHead (sts:hdrs) = - responseStatus sts `bindE` \(version,code,reason) ->- parseHeaders hdrs `bindE` \hdrs' ->- Right (code,reason,hdrs')- where-- responseStatus line- = case words line of- yes@(version:code:reason) -> Right (version,match code,concatMap (++" ") reason)- no -> if null line - then Left ErrorClosed -- an assumption- else Left (ErrorParse $ "Response status line parse failure: " ++ line)--- match [a,b,c] = (digitToInt a,- digitToInt b,- digitToInt c)- match _ = (-1,-1,-1) -- will create appropriate behaviour--- -------------------------------------------------------------------------------------- HTTP Send / Recv ------------------------------------------------------------------------------------------------------data Behaviour = Continue- | Retry- | Done- | ExpectEntity- | DieHorribly String--matchResponse :: RequestMethod -> ResponseCode -> Behaviour-matchResponse rqst rsp =- case rsp of- (1,0,0) -> Continue- (1,0,1) -> Done -- upgrade to TLS- (1,_,_) -> Continue -- default- (2,0,4) -> Done- (2,0,5) -> Done- (2,_,_) -> ans- (3,0,4) -> Done- (3,0,5) -> Done- (3,_,_) -> ans- (4,1,7) -> Retry -- Expectation failed- (4,_,_) -> ans- (5,_,_) -> ans- (a,b,c) -> DieHorribly ("Response code " ++ map intToDigit [a,b,c] ++ " not recognised")- where- ans | rqst == HEAD = Done- | otherwise = ExpectEntity- ---- | 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 -> IO (Result Response)-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 -> IO (Result Response)-simpleHTTP_ s r =- do - auth <- getAuth r- let r' = fixReq 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- where- {- RFC 2616, section 5.1.2:- "The most common form of Request-URI is that used to identify a- resource on an origin server or gateway. In this case the absolute- path of the URI MUST be transmitted (see section 3.2.1, abs_path) as- the Request-URI, and the network location of the URI (authority) MUST- be transmitted in a Host header field." -}- -- we assume that this is the case, so we take the host name from- -- the Host header if there is one, otherwise from the request-URI.- -- Then we make the request-URI an abs_path and make sure that there- -- is a Host header.- fixReq :: URIAuthority -> Request -> Request- fixReq URIAuthority{host=h,port=p} r = - let h' = h ++ maybe "" ((':':) . show) p in- replaceHeader HdrConnection "close" $- insertHeaderIfMissing HdrHost h' $- r { rqURI = (rqURI r){ uriScheme = "", - uriAuthority = Nothing } } --getAuth :: Monad m => Request -> m URIAuthority-getAuth r = case parseURIAuthority auth of- Just x -> return x - Nothing -> fail $ "Error parsing URI authority '"- ++ auth ++ "'"- where auth = case findHeader HdrHost r of- Just h -> h- Nothing -> uriToAuthorityString (rqURI r)--sendHTTP :: Stream s => s -> Request -> IO (Result Response)-sendHTTP conn rq = - do { let a_rq = fixHostHeader rq- ; rsp <- Exception.catch (main a_rq)- (\e -> do { close conn; throw e })- ; let fn list = when (or $ map findConnClose list)- (close conn)- ; either (\_ -> fn [rqHeaders rq])- (\r -> fn [rqHeaders rq,rspHeaders r])- rsp- ; return rsp- }- where --- 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.- main :: Request -> IO (Result Response)- main rqst =- 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)- rsp <- getResponseHead - switchResponse True False rsp rqst- - -- reads and parses headers- getResponseHead :: IO (Result ResponseData)- getResponseHead =- do { lor <- readTillEmpty1 conn- ; return $ lor `bindE` parseResponseHead- }-- -- Hmmm, this could go bad if we keep getting "100 Continue"- -- responses... Except this should never happen according- -- to the RFC.- switchResponse :: Bool {- allow retry? -}- -> Bool {- is body sent? -}- -> Result ResponseData- -> Request- -> IO (Result Response)- - 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 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- ; switchResponse allow_retry True rsp rqst- }- }- | otherwise -> {- keep waiting -}- do { rsp <- getResponseHead- ; switchResponse 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- ; switchResponse False bdy_sent rsp rqst- } - - Done ->- return (Right $ Response cd rn hdrs "")-- DieHorribly str ->- return $ Left $ ErrorParse ("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 -> linearTransferStrLen conn x- Nothing -> hopefulTransfer conn ""- Just x -> - case map toLower (trim x) of- "chunked" -> chunkedTransfer conn- _ -> uglyDeathTransfer conn- ; return $ rslt `bindE` \(ftrs,bdy) -> Right (Response cd rn (hdrs++ftrs) bdy) - }-- - -- Adds a Host header if one is NOT ALREADY PRESENT- fixHostHeader :: Request -> Request- fixHostHeader rq =- let uri = rqURI rq- host = uriToAuthorityString uri- in insertHeaderIfMissing HdrHost host rq- - -- Looks for a "Connection" header with the value "close".- -- Returns True when this is found.- findConnClose :: [Header] -> Bool- findConnClose hdrs =- case lookupHeader HdrConnection hdrs of- Nothing -> False- Just x -> map toLower (trim x) == "close"---- This function duplicates old Network.URI.authority behaviour.-uriToAuthorityString :: URI -> String-uriToAuthorityString URI{uriAuthority=Nothing} = ""-uriToAuthorityString URI{uriAuthority=Just ua} = uriUserInfo ua ++- uriRegName ua ++- uriPort ua---- | Receive and parse a HTTP request from the given Stream. Should be used--- for server side interactions.-receiveHTTP :: Stream s => s -> IO (Result Request)-receiveHTTP conn = do rq <- getRequestHead conn- case rq of- Left e -> return (Left e)- Right r -> processRequest conn r---- | Reads and parses request headers.-getRequestHead :: Stream s => s -> IO (Result RequestData)-getRequestHead conn =- do { lor <- readTillEmpty1 conn- ; return $ lor `bindE` parseRequestHead- }---- | Process request body (called after successful getRequestHead)-processRequest :: Stream s => s -> RequestData -> IO (Result Request)-processRequest conn (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 -> linearTransferStrLen conn x- Nothing -> return (Right ([], "")) -- hopefulTransfer ""- Just x ->- case map toLower (trim x) of- "chunked" -> chunkedTransfer conn- _ -> uglyDeathTransfer conn-- return $ rslt `bindE` \(ftrs,bdy) -> Right (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 -> IO ()-respondHTTP conn rsp = do writeBlock conn (show rsp)- -- write body immediately, don't wait for 100 CONTINUE- writeBlock conn (rspBody rsp)- return ()---- The following functions were in the where clause of sendHTTP, they have--- been moved to global scope so other functions can access them. --linearTransferStrLen :: Stream s => s -> String -> IO (Result ([Header],String))-linearTransferStrLen conn ns =- case reads ns of- [(n,"")] -> linearTransfer conn n- _ -> return $ Left $ ErrorParse $ "Content-Length header contains not a number: " ++ show ns---- | Used when we know exactly how many bytes to expect.-linearTransfer :: Stream s => s -> Int -> IO (Result ([Header],String))-linearTransfer conn n- = do info <- readBlock conn n- return $ info `bindE` \str -> Right ([],str)---- | Used when nothing about data is known,--- Unfortunately waiting for a socket closure--- causes bad behaviour. Here we just--- take data once and give up the rest.-hopefulTransfer :: Stream s => s -> String -> IO (Result ([Header],String))-hopefulTransfer conn str- = readLine conn >>= - either (\v -> return $ Left v)- (\more -> if null more - then return (Right ([],str)) - else hopefulTransfer conn (str++more))--- | A necessary feature of HTTP\/1.1--- Also the only transfer variety likely to--- return any footers.-chunkedTransfer :: Stream s => s -> IO (Result ([Header],String))-chunkedTransfer conn- = chunkedTransferC conn 0 >>= \v ->- return $ v `bindE` \(ftrs,count,info) ->- let myftrs = Header HdrContentLength (show count) : ftrs - in Right (myftrs,info)--chunkedTransferC :: Stream s => s -> Int -> IO (Result ([Header],Int,String))-chunkedTransferC conn n- = readLine conn >>= \v -> case v of- Left e -> return (Left e)- Right line ->- let size = ( if null line- then 0- else case readHex line of- (n,_):_ -> n- _ -> 0- )- in if size == 0- then do { rs <- readTillEmpty2 conn []- ; return $- rs `bindE` \strs ->- parseHeaders strs `bindE` \ftrs ->- Right (ftrs,n,"")- }- else do { some <- readBlock conn size- ; readLine conn- ; more <- chunkedTransferC conn (n+size)- ; return $ - some `bindE` \cdata ->- more `bindE` \(ftrs,m,mdata) -> - Right (ftrs,m,cdata++mdata) - } ---- | Maybe in the future we will have a sensible thing--- to do here, at that time we might want to change--- the name.-uglyDeathTransfer :: Stream s => s -> IO (Result ([Header],String))-uglyDeathTransfer conn- = return $ Left $ ErrorParse "Unknown Transfer-Encoding"---- | Remove leading crlfs then call readTillEmpty2 (not required by RFC)-readTillEmpty1 :: Stream s => s -> IO (Result [String])-readTillEmpty1 conn =- do { line <- readLine conn- ; case line of- Left e -> return $ Left e- Right s ->- if s == crlf- then readTillEmpty1 conn- else readTillEmpty2 conn [s]- }---- | Read lines until an empty line (CRLF),--- also accepts a connection close as end of--- input, which is not an HTTP\/1.1 compliant--- thing to do - so probably indicates an--- error condition.-readTillEmpty2 :: Stream s => s -> [String] -> IO (Result [String])-readTillEmpty2 conn list =- do { line <- readLine conn- ; case line of- Left e -> return $ Left e- Right s ->- if s == crlf || null s- then return (Right $ reverse (s:list))- else readTillEmpty2 conn (s:list)- }-- ------------------------------------------------------------------------------------- A little friendly funtionality ------------------------------------------------------------------------------------- | Formats name-value pairs as application\/x-www-form-urlencoded.-urlEncodeVars :: [(String,String)] -> String-urlEncodeVars xs = - concat $ intersperse "&" [urlEncode n ++ "=" ++ urlEncode v | (n,v) <- xs]---- | Converts a single value to the application\/x-www-form-urlencoded encoding.-urlEncode :: String -> String-urlEncode = replace ' ' '+' . escapeURIString okChar- where okChar c = c == ' ' || - (isUnescapedInURI c && c `notElem` "&=+")---- | Converts a single value from the --- application\/x-www-form-urlencoded encoding.-urlDecode :: String -> String-urlDecode = unEscapeString . replace '+' ' '---- | Replaces all instances of a value in a list by another value.-replace :: Eq a =>- a -- ^ Value to look for- -> a -- ^ Value to replace it with- -> [a] -- ^ Input list- -> [a] -- ^ Output list-replace x y = map (\z -> if z == x then y else z)
+ Network/HTTP/Base.hs view
@@ -0,0 +1,552 @@+----------------------------------------------------------------------------- +-- | +-- Module : Network.HTTP.Base +-- Copyright : (c) Warrick Gray 2002, Bjorn Bringert 2003-2005, 2007 Robin Bate Boerop, 2008 Sigbjorn Finne +-- License : BSD +-- +-- Maintainer : Sigbjorn Finne <sigbjorn.finne@gmail.com> +-- Stability : experimental +-- Portability : non-portable (not tested) +-- +-- An easy HTTP interface; base types. +-- +----------------------------------------------------------------------------- +module Network.HTTP.Base + ( + -- ** Constants + httpVersion + + -- ** HTTP + , Request + , Response + , RequestMethod(..) + + , HTTPRequest(..) + , HTTPResponse(..) + + -- ** URL Encoding + , urlEncode + , urlDecode + , urlEncodeVars + + -- ** URI authority parsing + , URIAuthority(..) + , parseURIAuthority + + -- internal + , crlf + , sp + , uriToAuthorityString -- :: URI -> String + , uriAuthToString -- :: URIAuth -> String + , parseResponseHead + , parseRequestHead + , ResponseNextStep(..) + , matchResponse + , ResponseData + , ResponseCode + , RequestData + + , getAuth + , normalizeRequestURI + , normalizeHostHeader + , findConnClose + + -- internal export (for the use by Network.HTTP.{Stream,ByteStream} ) + , linearTransfer + , hopefulTransfer + , chunkedTransfer + , uglyDeathTransfer + , readTillEmpty1 + , readTillEmpty2 + + , catchIO + , catchIO_ + ) where + +import Network.URI + ( URI(uriAuthority, uriPath, uriScheme) + , URIAuth(uriUserInfo, uriRegName, uriPort) + , parseURIReference + ) + +import Control.Monad ( guard ) +import Data.Char ( ord, digitToInt, intToDigit, toLower ) +import Data.List ( partition ) +import Data.Maybe ( listToMaybe ) +import Numeric ( showHex, readHex ) + +import Network.Stream +import Network.BufferType ( BufferOp(..) ) +import Network.HTTP.Headers +import Network.HTTP.Utils ( trim ) + +import Text.Read.Lex (readDecP) +import Text.ParserCombinators.ReadP + ( ReadP, readP_to_S, char, (<++), look, munch ) + +import Control.Exception as Exception (IOException) + +----------------------------------------------------------------- +------------------ URI Authority parsing ------------------------ +----------------------------------------------------------------- + +data URIAuthority = URIAuthority { user :: Maybe String, + password :: Maybe String, + host :: String, + port :: Maybe Int + } deriving (Eq,Show) + +-- | Parse the authority part of a URL. +-- +-- > RFC 1732, section 3.1: +-- > +-- > //<user>:<password>@<host>:<port>/<url-path> +-- > Some or all of the parts "<user>:<password>@", ":<password>", +-- > ":<port>", and "/<url-path>" may be excluded. +parseURIAuthority :: String -> Maybe URIAuthority +parseURIAuthority s = listToMaybe (map fst (readP_to_S pURIAuthority s)) + + +pURIAuthority :: ReadP URIAuthority +pURIAuthority = do + (u,pw) <- (pUserInfo `before` char '@') + <++ return (Nothing, Nothing) + h <- munch (/=':') + p <- orNothing (char ':' >> readDecP) + look >>= guard . null + return URIAuthority{ user=u, password=pw, host=h, port=p } + +pUserInfo :: ReadP (Maybe String, Maybe String) +pUserInfo = do + u <- orNothing (munch (`notElem` ":@")) + p <- orNothing (char ':' >> munch (/='@')) + return (u,p) + +before :: Monad m => m a -> m b -> m a +before a b = a >>= \x -> b >> return x + +orNothing :: ReadP a -> ReadP (Maybe a) +orNothing p = fmap Just p <++ return Nothing + +-- This function duplicates old Network.URI.authority behaviour. +uriToAuthorityString :: URI -> String +uriToAuthorityString u = maybe "" uriAuthToString (uriAuthority u) + +uriAuthToString :: URIAuth -> String +uriAuthToString ua = + concat [ uriUserInfo ua + , uriRegName ua + , uriPort ua + ] + +----------------------------------------------------------------- +------------------ HTTP Messages -------------------------------- +----------------------------------------------------------------- + + +-- Protocol version +httpVersion :: String +httpVersion = "HTTP/1.1" + + +-- | The HTTP request method, to be used in the 'Request' object. +-- We are missing a few of the stranger methods, but these are +-- not really necessary until we add full TLS. +data RequestMethod = HEAD | PUT | GET | POST | DELETE | OPTIONS | TRACE + deriving(Show,Eq) + +rqMethodMap :: [(String, RequestMethod)] +rqMethodMap = [("HEAD", HEAD), + ("PUT", PUT), + ("GET", GET), + ("POST", POST), + ("DELETE", DELETE), + ("OPTIONS", OPTIONS), + ("TRACE", TRACE)] + +type Request = HTTPRequest String +type Response = HTTPResponse String + +-- | An HTTP Request. +-- The 'Show' instance of this type is used for message serialisation, +-- which means no body data is output. +data HTTPRequest a = + Request { rqURI :: URI -- ^ might need changing in future + -- 1) to support '*' uri in OPTIONS request + -- 2) transparent support for both relative + -- & absolute uris, although this should + -- already work (leave scheme & host parts empty). + , rqMethod :: RequestMethod + , rqHeaders :: [Header] + , rqBody :: a + } + + + +crlf, sp :: String +crlf = "\r\n" +sp = " " + +-- Notice that request body is not included, +-- this show function is used to serialise +-- a request for the transport link, we send +-- the body separately where possible. +instance Show (HTTPRequest a) where + show (Request u m h _) = + show m ++ sp ++ alt_uri ++ sp ++ httpVersion ++ crlf + ++ foldr (++) [] (map show h) ++ crlf + where + alt_uri = show $ if null (uriPath u) || head (uriPath u) /= '/' + then u { uriPath = '/' : uriPath u } + else u + +instance HasHeaders (HTTPRequest a) where + getHeaders = rqHeaders + setHeaders rq hdrs = rq { rqHeaders=hdrs } + +-- | For easy pattern matching, HTTP response codes @xyz@ are +-- represented as @(x,y,z)@. +type ResponseCode = (Int,Int,Int) + +-- | @ResponseData@ contains the head of a response payload; +-- HTTP response code, accompanying text description + header +-- fields. +type ResponseData = (ResponseCode,String,[Header]) + +-- | @RequestData@ contains the head of a HTTP request; method, +-- its URL along with the auxillary/supporting header data. +type RequestData = (RequestMethod,URI,[Header]) + +-- | An HTTP Response. +-- The 'Show' instance of this type is used for message serialisation, +-- which means no body data is output, additionally the output will +-- show an HTTP version of 1.1 instead of the actual version returned +-- by a server. +data HTTPResponse a = + Response { rspCode :: ResponseCode + , rspReason :: String + , rspHeaders :: [Header] + , rspBody :: a + } + +-- This is an invalid representation of a received response, +-- since we have made the assumption that all responses are HTTP/1.1 +instance Show (HTTPResponse a) where + show (Response (a,b,c) reason headers _) = + httpVersion ++ ' ' : map intToDigit [a,b,c] ++ ' ' : reason ++ crlf + ++ foldr (++) [] (map show headers) ++ crlf + +instance HasHeaders (HTTPResponse a) where + getHeaders = rspHeaders + setHeaders rsp hdrs = rsp { rspHeaders=hdrs } + +----------------------------------------------------------------- +------------------ Parsing -------------------------------------- +----------------------------------------------------------------- + +-- Parsing a request +parseRequestHead :: [String] -> Result RequestData +parseRequestHead [] = Left ErrorClosed +parseRequestHead (com:hdrs) = + requestCommand com `bindE` \(_version,rqm,uri) -> + parseHeaders hdrs `bindE` \hdrs' -> + Right (rqm,uri,hdrs') + where + requestCommand line = + case words line of + _yes@(rqm:uri:version) -> + case (parseURIReference uri, lookup rqm rqMethodMap) of + (Just u, Just r) -> Right (version,r,u) + _ -> Left parse_err + _no + | null line -> Left ErrorClosed + | otherwise -> Left parse_err + where + parse_err = ErrorParse ("Request command line parse failure: " ++ line) + +-- Parsing a response +parseResponseHead :: [String] -> Result ResponseData +parseResponseHead [] = Left ErrorClosed +parseResponseHead (sts:hdrs) = + responseStatus sts `bindE` \(_version,code,reason) -> + parseHeaders hdrs `bindE` \hdrs' -> + Right (code,reason,hdrs') + where + responseStatus line = + case words line of + _yes@(version:code:reason) -> + Right (version,match code,concatMap (++" ") reason) + _no + | null line -> Left ErrorClosed -- an assumption + | otherwise -> Left parse_err + where + parse_err = (ErrorParse $ "Response status line parse failure: " ++ line) + + match [a,b,c] = (digitToInt a, + digitToInt b, + digitToInt c) + match _ = (-1,-1,-1) -- will create appropriate behaviour + + + + +----------------------------------------------------------------- +------------------ HTTP Send / Recv ---------------------------------- +----------------------------------------------------------------- + +data ResponseNextStep + = Continue + | Retry + | Done + | ExpectEntity + | DieHorribly String + +matchResponse :: RequestMethod -> ResponseCode -> ResponseNextStep +matchResponse rqst rsp = + case rsp of + (1,0,0) -> Continue + (1,0,1) -> Done -- upgrade to TLS + (1,_,_) -> Continue -- default + (2,0,4) -> Done + (2,0,5) -> Done + (2,_,_) -> ans + (3,0,4) -> Done + (3,0,5) -> Done + (3,_,_) -> ans + (4,1,7) -> Retry -- Expectation failed + (4,_,_) -> ans + (5,_,_) -> ans + (a,b,c) -> DieHorribly ("Response code " ++ map intToDigit [a,b,c] ++ " not recognised") + where + ans | rqst == HEAD = Done + | otherwise = ExpectEntity + + + +----------------------------------------------------------------- +------------------ A little friendly funtionality --------------- +----------------------------------------------------------------- + + +{- + I had a quick look around but couldn't find any RFC about + the encoding of data on the query string. I did find an + IETF memo, however, so this is how I justify the urlEncode + and urlDecode methods. + + Doc name: draft-tiwari-appl-wxxx-forms-01.txt (look on www.ietf.org) + + Reserved chars: ";", "/", "?", ":", "@", "&", "=", "+", ",", and "$" are reserved. + Unwise: "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`" + URI delims: "<" | ">" | "#" | "%" | <"> + Unallowed ASCII: <US-ASCII coded characters 00-1F and 7F hexadecimal> + <US-ASCII coded character 20 hexadecimal> + Also unallowed: any non-us-ascii character + + Escape method: char -> '%' a b where a, b :: Hex digits +-} + +urlEncode, urlDecode :: String -> String + +urlDecode ('%':a:b:rest) = toEnum (16 * digitToInt a + digitToInt b) + : urlDecode rest +urlDecode (h:t) = h : urlDecode t +urlDecode [] = [] + +urlEncode (h:t) = + let str = if reserved (ord h) then escape h else [h] + in str ++ urlEncode t + where + reserved x + | x >= ord 'a' && x <= ord 'z' = False + | x >= ord 'A' && x <= ord 'Z' = False + | x >= ord '0' && x <= ord '9' = False + | x <= 0x20 || x >= 0x7F = True + | otherwise = x `elem` map ord [';','/','?',':','@','&' + ,'=','+',',','$','{','}' + ,'|','\\','^','[',']','`' + ,'<','>','#','%','"'] + -- wouldn't it be nice if the compiler + -- optimised the above for us? + + escape x = '%':showHex (ord x) "" + +urlEncode [] = [] + + + +-- Encode form variables, useable in either the +-- query part of a URI, or the body of a POST request. +-- I have no source for this information except experience, +-- this sort of encoding worked fine in CGI programming. +urlEncodeVars :: [(String,String)] -> String +urlEncodeVars ((n,v):t) = + let (same,diff) = partition ((==n) . fst) t + in urlEncode n ++ '=' : foldl (\x y -> x ++ ',' : urlEncode y) (urlEncode $ v) (map snd same) + ++ urlEncodeRest diff + where urlEncodeRest [] = [] + urlEncodeRest diff = '&' : urlEncodeVars diff +urlEncodeVars [] = [] + +-- | @getAuth req@ fishes out the authority portion of the URL in a request's @Host@ +-- header. +getAuth :: Monad m => HTTPRequest ty -> m URIAuthority +getAuth r = + -- ToDo: verify that Network.URI functionality doesn't take care of this (now.) + case parseURIAuthority auth of + Just x -> return x + Nothing -> fail $ "Network.HTTP.Base.getAuth: Error parsing URI authority '" ++ auth ++ "'" + where + auth = + case findHeader HdrHost r of + Just h -> h + Nothing -> uriToAuthorityString (rqURI r) + + {- RFC 2616, section 5.1.2: + "The most common form of Request-URI is that used to identify a + resource on an origin server or gateway. In this case the absolute + path of the URI MUST be transmitted (see section 3.2.1, abs_path) as + the Request-URI, and the network location of the URI (authority) MUST + be transmitted in a Host header field." -} + -- we assume that this is the case, so we take the host name from + -- the Host header if there is one, otherwise from the request-URI. + -- Then we make the request-URI an abs_path and make sure that there + -- is a Host header. +normalizeRequestURI :: URIAuthority -> HTTPRequest ty -> HTTPRequest ty +normalizeRequestURI URIAuthority{host=h} r = + replaceHeader HdrConnection "close" $ + insertHeaderIfMissing HdrHost h $ + r { rqURI = (rqURI r){ uriScheme = "" + , uriAuthority = Nothing + }} + +-- Adds a Host header if one is NOT ALREADY PRESENT +normalizeHostHeader :: HTTPRequest ty -> HTTPRequest ty +normalizeHostHeader rq = + insertHeaderIfMissing HdrHost + (uriToAuthorityString $ rqURI rq) + rq + +-- Looks for a "Connection" header with the value "close". +-- Returns True when this is found. +findConnClose :: [Header] -> Bool +findConnClose hdrs = + maybe False + (\ x -> map toLower (trim x) == "close") + (lookupHeader HdrConnection hdrs) + + +-- | Used when we know exactly how many bytes to expect. +linearTransfer :: (Int -> IO (Result a)) -> Int -> IO (Result ([Header],a)) +linearTransfer readBlk n + = do info <- readBlk n + return $ info `bindE` \str -> Right ([],str) + +-- | Used when nothing about data is known, +-- Unfortunately waiting for a socket closure +-- causes bad behaviour. Here we just +-- take data once and give up the rest. +hopefulTransfer :: BufferOp a + -> IO (Result a) + -> [a] + -> IO (Result ([Header],a)) +hopefulTransfer bufOps readL strs + = readL >>= + either (\v -> return $ Left v) + (\more -> if (buf_isEmpty bufOps more) + then return (Right ([],foldr (flip (buf_append bufOps)) (buf_empty bufOps) strs)) + else hopefulTransfer bufOps readL (more:strs)) + +-- | A necessary feature of HTTP\/1.1 +-- Also the only transfer variety likely to +-- return any footers. +chunkedTransfer :: BufferOp a + -> IO (Result a) + -> (Int -> IO (Result a)) + -> IO (Result ([Header], a)) +chunkedTransfer bufOps readL readBlk = do + v <- chunkedTransferC bufOps readL readBlk 0 + return $ v `bindE` \(ftrs,count,info) -> + let myftrs = Header HdrContentLength (show count) : ftrs + in Right (myftrs,info) + +chunkedTransferC :: BufferOp a + -> IO (Result a) + -> (Int -> IO (Result a)) + -> Int + -> IO (Result ([Header],Int,a)) +chunkedTransferC bufOps readL readBlk n = do + v <- readL + case v of + Left e -> return (Left e) + Right line + | size == 0 -> do + rs <- readTillEmpty2 bufOps readL [] + return $ + rs `bindE` \strs -> + parseHeaders (map (buf_toStr bufOps) strs) `bindE` \ftrs -> + Right (ftrs,n,buf_empty bufOps) + | otherwise -> do + some <- readBlk size + readL + more <- chunkedTransferC bufOps {-nullVal isNull isLineEnd toStr append -} readL readBlk (n+size) + return $ + some `bindE` \cdata -> + more `bindE` \(ftrs,m,mdata) -> + Right (ftrs,m,buf_append bufOps cdata mdata) + where + size + | buf_isEmpty bufOps line = 0 + | otherwise = + case readHex (buf_toStr bufOps line) of + (hx,_):_ -> hx + _ -> 0 + +-- | Maybe in the future we will have a sensible thing +-- to do here, at that time we might want to change +-- the name. +uglyDeathTransfer :: IO (Result ([Header],a)) +uglyDeathTransfer + = return $ Left $ ErrorParse "Unknown Transfer-Encoding" + +-- | Remove leading crlfs then call readTillEmpty2 (not required by RFC) +readTillEmpty1 :: BufferOp a + -> IO (Result a) + -> IO (Result [a]) +readTillEmpty1 bufOps readL = + readL >>= + either (return . Left) + (\ s -> + if buf_isLineTerm bufOps s + then readTillEmpty1 bufOps readL + else readTillEmpty2 bufOps readL [s]) + +-- | Read lines until an empty line (CRLF), +-- also accepts a connection close as end of +-- input, which is not an HTTP\/1.1 compliant +-- thing to do - so probably indicates an +-- error condition. +readTillEmpty2 :: BufferOp a + -> IO (Result a) + -> [a] + -> IO (Result [a]) +readTillEmpty2 bufOps readL list = + readL >>= + either (return . Left) + (\ s -> + if buf_isLineTerm bufOps s || buf_isEmpty bufOps s + then return (Right $ reverse (s:list)) + else readTillEmpty2 bufOps readL (s:list)) + +-- +-- Misc +-- + +-- | @catchIO a h@ handles IO action exceptions throughout codebase; version-specific +-- tweaks better go here. +catchIO :: IO a -> (IOException -> IO a) -> IO a +catchIO a h = Prelude.catch a h + +catchIO_ :: IO a -> IO a -> IO a +catchIO_ a h = Prelude.catch a (const h) +
+ Network/HTTP/HandleStream.hs view
@@ -0,0 +1,214 @@+-----------------------------------------------------------------------------+-- |+-- Module : Network.HTTP.HandleStream+-- Copyright : (c) Warrick Gray 2002, Bjorn Bringert 2003-2005, 2007 Robin Bate Boerop, 2008 Sigbjorn Finne+-- License : BSD+-- +-- Maintainer : Sigbjorn Finne <sigbjorn.finne@gmail.com>+-- Stability : experimental+-- Portability : non-portable (not tested)+--+-- A HandleStream version of Network.HTTP.Stream's public offerings.+--+-----------------------------------------------------------------------------+module Network.HTTP.HandleStream + ( simpleHTTP -- :: HTTPRequest ty -> IO (Result (HTTPResponse ty))+ , simpleHTTP_ -- :: HStream ty => HandleStream ty -> HTTPRequest ty -> IO (Result (HTTPResponse ty))+ , sendHTTP -- :: HStream ty => HandleStream ty -> HTTPRequest ty -> IO (Result (HTTResponse ty))+ , receiveHTTP -- :: HStream ty => HandleStream ty -> IO (Result (HTTPRequest ty))+ , respondHTTP -- :: HStream ty => HandleStream ty -> HTTPResponse ty -> IO ()+ + , simpleHTTP_debug -- :: FilePath -> HTTPRequest DebugString -> IO (HTTPResponse DebugString)+ ) where++-----------------------------------------------------------------+------------------ Imports --------------------------------------+-----------------------------------------------------------------++import Network.BufferType+import Network.Stream ( ConnError(..), bindE, Result )+import Network.StreamDebugger ( debugByteStream )+import Network.TCP (HStream(..), HandleStream )++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)++-----------------------------------------------------------------+------------------ 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 :: HStream ty => HTTPRequest ty -> IO (Result (HTTPResponse ty))+simpleHTTP r = do + auth <- getAuth r+ c <- openStream (host auth) (fromMaybe 80 (port auth))+ simpleHTTP_ c r++simpleHTTP_debug :: HStream ty => FilePath -> HTTPRequest ty -> IO (Result (HTTPResponse ty))+simpleHTTP_debug httpLogFile r = do + auth <- getAuth r+ c0 <- openStream (host auth) (fromMaybe 80 (port auth))+ c <- debugByteStream httpLogFile c0+ simpleHTTP_ c r++-- | Like 'simpleHTTP', but acting on an already opened stream.+simpleHTTP_ :: HStream ty => HandleStream ty -> HTTPRequest ty -> IO (Result (HTTPResponse ty))+simpleHTTP_ s r = do + auth <- getAuth r+ let r' = normalizeRequestURI auth r + rsp <- sendHTTP s r'+ return rsp++sendHTTP :: HStream ty => HandleStream ty -> HTTPRequest ty -> IO (Result (HTTPResponse ty))+sendHTTP conn rq = do+ let a_rq = normalizeHostHeader rq+ rsp <- catchIO (sendMain conn a_rq)+ (\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 :: HStream ty => HandleStream ty -> HTTPRequest ty -> IO (Result (HTTPResponse ty))+sendMain conn rqst = do+ --let str = if null (rqBody rqst)+ -- then show rqst+ -- else show (insertHeader HdrExpect "100-continue" rqst)+ writeBlock conn (buf_fromStr bufferOps $ show rqst)+ -- write body immediately, don't wait for 100 CONTINUE+ writeBlock conn (rqBody rqst)+ rsp <- getResponseHead conn+ switchResponse conn True False rsp rqst++ -- Hmmm, this could go bad if we keep getting "100 Continue"+ -- responses... Except this should never happen according+ -- to the RFC.++switchResponse :: HStream ty+ => HandleStream ty+ -> Bool {- allow retry? -}+ -> Bool {- is body sent? -}+ -> Result ResponseData+ -> HTTPRequest ty+ -> IO (Result (HTTPResponse ty))+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 -> do {- Time to send the body -}+ writeBlock conn (rqBody rqst) >>= either (return . Left)+ (\ _ -> do+ rsp <- getResponseHead conn+ switchResponse conn allow_retry True rsp rqst)+ | otherwise -> do {- keep waiting -}+ rsp <- getResponseHead conn+ switchResponse conn allow_retry bdy_sent rsp rqst++ Retry -> do {- Request with "Expect" header failed.+ Trouble is the request contains Expects+ other than "100-Continue" -}+ writeBlock conn ((buf_append bufferOps)+ (buf_fromStr bufferOps (show rqst))+ (rqBody rqst))+ rsp <- getResponseHead conn+ switchResponse conn False bdy_sent rsp rqst+ + Done -> return (Right $ Response cd rn hdrs (buf_empty bufferOps))++ DieHorribly str -> return $ Left $ ErrorParse ("Invalid response: " ++ str)++ ExpectEntity -> do+ rslt <- + case tc of+ Nothing -> + case cl of+ Nothing -> hopefulTransfer bo (readLine conn) []+ Just x -> + case reads x of+ ((v,_):_) -> linearTransfer (readBlock conn) v+ _ -> return (Left (ErrorParse ("unrecognized content-length value " ++ x)))+ Just x -> + case map toLower (trim x) of+ "chunked" -> chunkedTransfer bo (readLine conn) (readBlock conn)+ _ -> uglyDeathTransfer+ return $ rslt `bindE` \(ftrs,bdy) -> Right (Response cd rn (hdrs++ftrs) bdy)++ where+ tc = lookupHeader HdrTransferEncoding hdrs+ cl = lookupHeader HdrContentLength hdrs+ bo = bufferOps+ +-- reads and parses headers+getResponseHead :: HStream ty => HandleStream ty -> IO (Result ResponseData)+getResponseHead conn = do+ lor <- readTillEmpty1 bufferOps (readLine conn)+ return $ lor `bindE` \es -> parseResponseHead (map (buf_toStr bufferOps) es)++-- | Receive and parse a HTTP request from the given Stream. Should be used +-- for server side interactions.+receiveHTTP :: HStream bufTy => HandleStream bufTy -> IO (Result (HTTPRequest bufTy))+receiveHTTP conn = getRequestHead >>= either (return . Left) processRequest+ where+ -- reads and parses headers+ getRequestHead :: IO (Result RequestData)+ getRequestHead =+ do { lor <- readTillEmpty1 bufferOps (readLine conn)+ ; return $ lor `bindE` \es -> parseRequestHead (map (buf_toStr bufferOps) es)+ }+ + processRequest (rm,uri,hdrs) = do+ rslt <- + case tc of+ Nothing ->+ case cl of+ Nothing -> return (Right ([], buf_empty bo)) -- hopefulTransfer ""+ Just x -> + case reads x of+ ((v,_):_) -> linearTransfer (readBlock conn) v+ _ -> return (Left (ErrorParse ("unrecognized content-length value " ++ x)))+ Just x ->+ case map toLower (trim x) of+ "chunked" -> chunkedTransfer bo (readLine conn) (readBlock conn)+ _ -> uglyDeathTransfer+ + return $ rslt `bindE` \(ftrs,bdy) -> Right (Request uri rm (hdrs++ftrs) bdy)+ where+ -- FIXME : Also handle 100-continue.+ tc = lookupHeader HdrTransferEncoding hdrs+ cl = lookupHeader HdrContentLength hdrs+ bo = bufferOps++-- | Very simple function, send a HTTP response over the given stream. This +-- could be improved on to use different transfer types.+respondHTTP :: HStream ty => HandleStream ty -> HTTPResponse ty -> IO ()+respondHTTP conn rsp = do + writeBlock conn (buf_fromStr bufferOps $ show rsp)+ -- write body immediately, don't wait for 100 CONTINUE+ writeBlock conn (rspBody rsp)+ return ()
Network/HTTP/Headers.hs view
@@ -47,6 +47,7 @@ ( HasHeaders(..) , Header(..) , HeaderName(..)+ , insertHeader , insertHeaderIfMissing , insertHeaders@@ -55,31 +56,14 @@ , findHeader , lookupHeader , parseHeaders- , parseHeader- , headerMap+ ) where -import Data.Char (isSpace, toLower)-import Data.List (elemIndex)+import Data.Char (toLower) import Network.Stream (Result, ConnError(ErrorParse))---- remove leading and trailing whitespace.-trim :: String -> String-trim = let dropspace = dropWhile isSpace in- reverse . dropspace . reverse . dropspace---- Split a list into two parts, the delimiter occurs--- at the head of the second list. Nothing is returned--- when no occurance of the delimiter is found.-split :: Eq a => a -> [a] -> Maybe ([a],[a])-split delim list = case delim `elemIndex` list of- Nothing -> Nothing- Just x -> Just $ splitAt x list--crlf :: String-crlf = "\r\n"+import Network.HTTP.Utils ( trim, split, crlf ) --- | The Header data type pairs header names & values.+-- | The @Header@ data type pairs header names & values. data Header = Header HeaderName String instance Show Header where@@ -95,123 +79,122 @@ -- 1) makes customising header names laborious -- 2) increases code volume. ---data HeaderName = - -- Generic Headers --- HdrCacheControl- | HdrConnection- | HdrDate- | HdrPragma- | HdrTransferEncoding - | HdrUpgrade - | HdrVia-- -- Request Headers --- | HdrAccept- | HdrAcceptCharset- | HdrAcceptEncoding- | HdrAcceptLanguage- | HdrAuthorization- | HdrCookie- | HdrExpect- | HdrFrom- | HdrHost- | HdrIfModifiedSince- | HdrIfMatch- | HdrIfNoneMatch- | HdrIfRange- | HdrIfUnmodifiedSince- | HdrMaxForwards- | HdrProxyAuthorization- | HdrRange- | HdrReferer- | HdrUserAgent-- -- Response Headers- | HdrAge- | HdrLocation- | HdrProxyAuthenticate- | HdrPublic- | HdrRetryAfter- | HdrServer- | HdrSetCookie- | HdrVary- | HdrWarning- | HdrWWWAuthenticate-- -- Entity Headers- | HdrAllow- | HdrContentBase- | HdrContentEncoding- | HdrContentLanguage- | HdrContentLength- | HdrContentLocation- | HdrContentMD5- | HdrContentRange- | HdrContentType- | HdrETag- | HdrExpires- | HdrLastModified-- -- Mime entity headers (for sub-parts)- | HdrContentTransferEncoding-- -- | Allows for unrecognised or experimental headers.- | HdrCustom String -- not in header map below.+-- Long discussions can be had on this topic!+-- +data HeaderName + -- Generic Headers --+ = HdrCacheControl+ | HdrConnection+ | HdrDate+ | HdrPragma+ | HdrTransferEncoding + | HdrUpgrade + | HdrVia+ -- Request Headers --+ | HdrAccept+ | HdrAcceptCharset+ | HdrAcceptEncoding+ | HdrAcceptLanguage+ | HdrAuthorization+ | HdrCookie+ | HdrExpect+ | HdrFrom+ | HdrHost+ | HdrIfModifiedSince+ | HdrIfMatch+ | HdrIfNoneMatch+ | HdrIfRange+ | HdrIfUnmodifiedSince+ | HdrMaxForwards+ | HdrProxyAuthorization+ | HdrRange+ | HdrReferer+ | HdrUserAgent+ -- Response Headers+ | HdrAge+ | HdrLocation+ | HdrProxyAuthenticate+ | HdrPublic+ | HdrRetryAfter+ | HdrServer+ | HdrSetCookie+ | HdrVary+ | HdrWarning+ | HdrWWWAuthenticate+ -- Entity Headers+ | HdrAllow+ | HdrContentBase+ | HdrContentEncoding+ | HdrContentLanguage+ | HdrContentLength+ | HdrContentLocation+ | HdrContentMD5+ | HdrContentRange+ | HdrContentType+ | HdrETag+ | HdrExpires+ | HdrLastModified+ -- | MIME entity headers (for sub-parts)+ | HdrContentTransferEncoding+ -- | Allows for unrecognised or experimental headers.+ | HdrCustom String -- not in header map below. deriving(Eq) -- Translation between header names and values, -- good candidate for improvement. headerMap :: [ (String,HeaderName) ]-headerMap - = [ ("Cache-Control" ,HdrCacheControl )- , ("Connection" ,HdrConnection )- , ("Date" ,HdrDate ) - , ("Pragma" ,HdrPragma )- , ("Transfer-Encoding" ,HdrTransferEncoding ) - , ("Upgrade" ,HdrUpgrade ) - , ("Via" ,HdrVia )- , ("Accept" ,HdrAccept )- , ("Accept-Charset" ,HdrAcceptCharset )- , ("Accept-Encoding" ,HdrAcceptEncoding )- , ("Accept-Language" ,HdrAcceptLanguage )- , ("Authorization" ,HdrAuthorization )- , ("From" ,HdrFrom )- , ("Host" ,HdrHost )- , ("If-Modified-Since" ,HdrIfModifiedSince )- , ("If-Match" ,HdrIfMatch )- , ("If-None-Match" ,HdrIfNoneMatch )- , ("If-Range" ,HdrIfRange ) - , ("If-Unmodified-Since" ,HdrIfUnmodifiedSince )- , ("Max-Forwards" ,HdrMaxForwards )- , ("Proxy-Authorization" ,HdrProxyAuthorization)- , ("Range" ,HdrRange ) - , ("Referer" ,HdrReferer )- , ("User-Agent" ,HdrUserAgent )- , ("Age" ,HdrAge )- , ("Location" ,HdrLocation )- , ("Proxy-Authenticate" ,HdrProxyAuthenticate )- , ("Public" ,HdrPublic )- , ("Retry-After" ,HdrRetryAfter )- , ("Server" ,HdrServer )- , ("Vary" ,HdrVary )- , ("Warning" ,HdrWarning )- , ("WWW-Authenticate" ,HdrWWWAuthenticate )- , ("Allow" ,HdrAllow )- , ("Content-Base" ,HdrContentBase )- , ("Content-Encoding" ,HdrContentEncoding )- , ("Content-Language" ,HdrContentLanguage )- , ("Content-Length" ,HdrContentLength )- , ("Content-Location" ,HdrContentLocation )- , ("Content-MD5" ,HdrContentMD5 )- , ("Content-Range" ,HdrContentRange )- , ("Content-Type" ,HdrContentType )- , ("ETag" ,HdrETag )- , ("Expires" ,HdrExpires )- , ("Last-Modified" ,HdrLastModified )- , ("Set-Cookie" ,HdrSetCookie )- , ("Cookie" ,HdrCookie )- , ("Expect" ,HdrExpect ) ]-+headerMap =+ [ p "Cache-Control" HdrCacheControl+ , p "Connection" HdrConnection+ , p "Date" HdrDate+ , p "Pragma" HdrPragma+ , p "Transfer-Encoding" HdrTransferEncoding+ , p "Upgrade" HdrUpgrade+ , p "Via" HdrVia+ , p "Accept" HdrAccept+ , p "Accept-Charset" HdrAcceptCharset+ , p "Accept-Encoding" HdrAcceptEncoding+ , p "Accept-Language" HdrAcceptLanguage+ , p "Authorization" HdrAuthorization+ , p "From" HdrFrom+ , p "Host" HdrHost+ , p "If-Modified-Since" HdrIfModifiedSince+ , p "If-Match" HdrIfMatch+ , p "If-None-Match" HdrIfNoneMatch+ , p "If-Range" HdrIfRange+ , p "If-Unmodified-Since" HdrIfUnmodifiedSince+ , p "Max-Forwards" HdrMaxForwards+ , p "Proxy-Authorization" HdrProxyAuthorization+ , p "Range" HdrRange+ , p "Referer" HdrReferer+ , p "User-Agent" HdrUserAgent+ , p "Age" HdrAge+ , p "Location" HdrLocation+ , p "Proxy-Authenticate" HdrProxyAuthenticate+ , p "Public" HdrPublic+ , p "Retry-After" HdrRetryAfter+ , p "Server" HdrServer+ , p "Vary" HdrVary+ , p "Warning" HdrWarning+ , p "WWW-Authenticate" HdrWWWAuthenticate+ , p "Allow" HdrAllow+ , p "Content-Base" HdrContentBase+ , p "Content-Encoding" HdrContentEncoding+ , p "Content-Language" HdrContentLanguage+ , p "Content-Length" HdrContentLength+ , p "Content-Location" HdrContentLocation+ , p "Content-MD5" HdrContentMD5+ , p "Content-Range" HdrContentRange+ , p "Content-Type" HdrContentType+ , p "ETag" HdrETag+ , p "Expires" HdrExpires+ , p "Last-Modified" HdrLastModified+ , p "Set-Cookie" HdrSetCookie+ , p "Cookie" HdrCookie+ , p "Expect" HdrExpect+ ]+ where+ p a b = (a,b) instance Show HeaderName where show (HdrCustom s) = s@@ -229,7 +212,6 @@ insertHeader, replaceHeader, insertHeaderIfMissing :: HasHeaders a => HeaderName -> String -> a -> a - -- | Inserts a header with the given name and value. -- Allows duplicate header names. insertHeader name value x = setHeaders x newHeaders@@ -246,9 +228,9 @@ newHeaders [] = [Header name value] -- | Removes old headers with duplicate name.-replaceHeader name value x = setHeaders x newHeaders+replaceHeader name value h = setHeaders h newHeaders where- newHeaders = Header name value : [ x | x@(Header n v) <- getHeaders x, name /= n ]+ newHeaders = Header name value : [ x | x@(Header n _) <- getHeaders h, name /= n ] -- | Inserts multiple headers. insertHeaders :: HasHeaders a => [Header] -> a -> a
Network/HTTP/MD5.hs view
@@ -21,7 +21,6 @@ , Octet ) where -import Data.Char (chr) import Data.List (unfoldr) import Data.Word (Word8) import Numeric (readHex)@@ -36,8 +35,9 @@ hash :: [Octet] -> [Octet] hash xs = - unfoldr f $ md5s $ Str $ map (chr . fromIntegral) xs+ unfoldr f $ md5s $ Str $ map (toEnum . fromIntegral) xs where f :: String -> Maybe (Octet,String) f [] = Nothing- f (x:y:zs) = Just (fromIntegral a,zs)+ f [x] = f ['0',x]+ f (x:y:zs) = Just (a,zs) where [(a,_)] = readHex (x:y:[])
Network/HTTP/MD5Aux.hs view
@@ -4,11 +4,12 @@ Zord64, Str(..), BoolList(..), WordList(..)) where import Data.Char (ord, chr)-import Data.Bits (Bits, rotateL, shiftL, shiftR, (.&.), (.|.), xor, complement)+import Data.Bits (rotateL, shiftL, shiftR, (.&.), (.|.), xor, complement) import Data.Word (Word32, Word64) -rotL :: Bits a => a -> Int -> a+rotL :: Word32 -> Int -> Word32 rotL x = rotateL x+ type Zord64 = Word64 -- ===================== TYPES AND CLASS DEFINTIONS ========================@@ -19,7 +20,7 @@ newtype ABCD = ABCD (Word32, Word32, Word32, Word32) deriving (Eq, Show) newtype Str = Str String newtype BoolList = BoolList [Bool]-newtype WordList = WordList ([Word32], Zord64)+newtype WordList = WordList ([Word32], Word64) -- Anything we want to work out the MD5 of must be an instance of class MD5 @@ -28,7 +29,7 @@ -- \ \ \------ the rest of the input -- \ \--------- the number of bits returned -- \--------------- the bits returned in 32bit words- len_pad :: Zord64 -> a -> a -- append the padding and length+ len_pad :: Word64 -> a -> a -- append the padding and length finished :: a -> Bool -- Have we run out of input yet? @@ -87,10 +88,13 @@ finished (WordList (_, z)) = z == 0 -add :: ABCD -> ABCD -> ABCD-ABCD (a1, b1, c1, d1) `add` ABCD (a2, b2, c2, d2) = ABCD (a1 + a2, b1 + b2, c1 + c2, d1 + d2)-+instance Num ABCD where+ ABCD (a1, b1, c1, d1) + ABCD (a2, b2, c2, d2) = ABCD (a1 + a2, b1 + b2, c1 + c2, d1 + d2) + (*) = error "(*){ABCD}: no instance method defined"+ signum = error "signum{ABCD}: no instance method defined"+ fromInteger = error "fromInteger{ABCD}: no instance method defined"+ abs = error "abs{ABCD}: no instance method defined" -- ===================== EXPORTED FUNCTIONS ======================== @@ -122,14 +126,14 @@ md5_main :: (MD5 a) => Bool -- Have we added padding yet?- -> Zord64 -- The length so far mod 2^64+ -> Word64 -- The length so far mod 2^64 -> ABCD -- The initial state -> a -- The non-processed portion of the message -> ABCD -- The resulting state md5_main padded ilen abcd m = if finished m && padded then abcd- else md5_main padded' (ilen + 512) (abcd `add` abcd') m''+ else md5_main padded' (ilen + 512) (abcd + abcd') m'' where (m16, l, m') = get_next m len' = ilen + fromIntegral l ((m16', _, m''), padded') = if not padded && l < 512@@ -331,7 +335,7 @@ -- Convert the size into a list of characters used by the len_pad function -- for strings -length_to_chars :: Int -> Zord64 -> String+length_to_chars :: Int -> Word64 -> String length_to_chars 0 _ = [] length_to_chars p n = this:length_to_chars (p-1) (shiftR n 8) where this = chr $ fromIntegral $ n .&. 255
+ Network/HTTP/Stream.hs view
@@ -0,0 +1,307 @@+-----------------------------------------------------------------------------+-- |+-- 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 -> IO (Result Response)+ , simpleHTTP_ -- :: Stream s => s -> Request -> IO (Result Response)+ , sendHTTP -- :: Stream s => s -> Request -> IO (Result Response)+ , receiveHTTP -- :: Stream s => s -> IO (Result Request)+ , respondHTTP -- :: Stream s => s -> Response -> 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 -> IO (Result Response)+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 -> IO (Result Response)+simpleHTTP_ s r =+ do + auth <- getAuth r+ let r' = normalizeRequestURI 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 -> IO (Result Response)+sendHTTP conn rq = + do { let a_rq = normalizeHostHeader rq+ ; rsp <- catchIO (main a_rq)+ (\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+ }+ where +-- 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.+ main :: Request -> IO (Result Response)+ main rqst =+ 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)+ rsp <- getResponseHead + switchResponse True False rsp rqst+ + -- reads and parses headers+ getResponseHead :: IO (Result ResponseData)+ getResponseHead =+ do { lor <- readTillEmpty1 stringBufferOp (readLine conn)+ ; return $ lor `bindE` parseResponseHead+ }++ -- Hmmm, this could go bad if we keep getting "100 Continue"+ -- responses... Except this should never happen according+ -- to the RFC.+ switchResponse :: Bool {- allow retry? -}+ -> Bool {- is body sent? -}+ -> Result ResponseData+ -> Request+ -> IO (Result Response)+ + 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 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+ ; switchResponse allow_retry True rsp rqst+ }+ }+ | otherwise -> {- keep waiting -}+ do { rsp <- getResponseHead+ ; switchResponse 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+ ; switchResponse False bdy_sent rsp rqst+ } + + Done ->+ return (Right $ Response cd rn hdrs "")++ DieHorribly str ->+ return $ Left $ ErrorParse ("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+ ; return $ rslt `bindE` \(ftrs,bdy) -> Right (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)+receiveHTTP conn = getRequestHead >>= processRequest+ where+ -- reads and parses headers+ getRequestHead :: IO (Result RequestData)+ getRequestHead =+ do { lor <- readTillEmpty1 stringBufferOp (readLine conn)+ ; return $ lor `bindE` 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+ + return $ rslt `bindE` \(ftrs,bdy) -> Right (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 -> IO ()+respondHTTP conn rsp = do writeBlock conn (show rsp)+ -- write body immediately, don't wait for 100 CONTINUE+ writeBlock conn (rspBody rsp)+ return ()
+ Network/HTTP/Utils.hs view
@@ -0,0 +1,67 @@+----------------------------------------------------------------------------- +-- | +-- Module : Network.HTTP.Utils +-- Copyright : (c) Warrick Gray 2002, Bjorn Bringert 2003-2004, Simon Foster 2004, 2007 Robin Bate Boerop, 2008 Sigbjorn Finne +-- License : BSD +-- +-- Maintainer : Sigbjorn Finne <sigbjorn.finne@gmail.com> +-- Stability : experimental +-- Portability : non-portable (not tested) +-- +-- Set of utility functions and definitions used by package modules. +-- +module Network.HTTP.Utils + ( trim -- :: String -> String + , trimL -- :: String -> String + , trimR -- :: String -> String + + , crlf -- :: String + + , split -- :: Eq a => a -> [a] -> Maybe ([a],[a]) + , splitBy -- :: Eq a => a -> [a] -> [[a]] + + ) where + +import Data.Char +import Data.List ( elemIndex ) +import Data.Maybe ( fromMaybe ) + +-- | @crlf@ is our beloved two-char line terminator. +crlf :: String +crlf = "\r\n" + +-- | @split delim ls@ splits a list into two parts, the @delim@ occurring +-- at the head of the second list. If @delim@ isn't in @ls@, @Nothing@ is +-- returned. +split :: Eq a => a -> [a] -> Maybe ([a],[a]) +split delim list = case delim `elemIndex` list of + Nothing -> Nothing + Just x -> Just $ splitAt x list + +-- | @trim str@ removes leading and trailing whitespace from @str@. +trim :: String -> String +trim xs = trimR (trimL xs) + +-- | @trimL str@ removes leading whitespace (as defined by 'Data.Char.isSpace') +-- from @str@. +trimL :: String -> String +trimL xs = dropWhile isSpace xs + +-- | @trimL str@ removes trailing whitespace (as defined by 'Data.Char.isSpace') +-- from @str@. +trimR :: String -> String +trimR str = fromMaybe "" $ foldr trimIt Nothing str + where + trimIt x (Just xs) = Just (x:xs) + trimIt x Nothing + | isSpace x = Nothing + | otherwise = Just [x] + +-- | @splitMany delim ls@ removes the delimiter @delim@ from @ls@. +splitBy :: Eq a => a -> [a] -> [[a]] +splitBy _ [] = [] +splitBy c xs = + case break (==c) xs of + (_,[]) -> [xs] + (as,_:bs) -> as : splitBy c bs +
Network/Stream.hs view
@@ -27,11 +27,12 @@ , bindE ) where -data ConnError = ErrorReset - | ErrorClosed- | ErrorParse String- | ErrorMisc String- deriving(Show,Eq)+data ConnError + = ErrorReset + | ErrorClosed+ | ErrorParse String+ | ErrorMisc String+ deriving(Show,Eq) bindE :: Result a -> (a -> Result b) -> Result b bindE (Left e) _ = Left e@@ -55,4 +56,3 @@ readBlock :: x -> Int -> IO (Result String) writeBlock :: x -> String -> IO (Result ()) close :: x -> IO ()-
Network/StreamDebugger.hs view
@@ -18,12 +18,14 @@ module Network.StreamDebugger ( StreamDebugger , debugStream+ , debugByteStream ) where import Network.Stream (Stream(..)) import System.IO ( Handle, hFlush, hPutStrLn, IOMode(AppendMode), hClose, openFile )+import Network.TCP ( HandleStream, StreamHooks(..), HStream, setStreamHooks ) -- | Allows stream logging. Refer to 'debugStream' below. data StreamDebugger x@@ -57,3 +59,26 @@ hPutStrLn h ("File \"" ++ file ++ "\" opened for appending.") return (Dbg h stream) +debugByteStream :: HStream ty => FilePath -> HandleStream ty -> IO (HandleStream ty)+debugByteStream file stream = + do h <- openFile file AppendMode+ hPutStrLn h ("File \"" ++ file ++ "\" opened for appending.")+ setStreamHooks stream (debugStreamHooks h)+ return stream++debugStreamHooks :: HStream ty => Handle -> StreamHooks ty+debugStreamHooks h = + StreamHooks+ { hook_readBlock = \ toStr n val -> do+ let eval = case val of { Left e -> Left e ; Right v -> Right $ toStr v}+ hPutStrLn h ("readBlock " ++ show n ++ ' ' : show eval)+ , hook_readLine = \ toStr val -> do+ let eval = case val of { Left e -> Left e ; Right v -> Right $ toStr v}+ hPutStrLn h ("readLine " ++ show eval)+ , hook_writeBlock = \ toStr str val -> do+ hPutStrLn h ("writeBlock " ++ show val ++ ' ' : toStr str)+ , hook_close = do+ hPutStrLn h "closing..."+ hFlush h+ hClose h+ }
Network/StreamSocket.hs view
@@ -32,53 +32,58 @@ , ShutdownCmd(ShutdownBoth), SocketOption(SoError) ) +import Network.HTTP.Base ( catchIO ) import Control.Monad (liftM)-import Control.Exception as Exception (Exception, catch, throw)+import Control.Exception as Exception (IOException) import System.IO.Error (catch, isEOFError) -- | Exception handler for socket operations.-handleSocketError :: Socket -> Exception -> IO (Result a)+handleSocketError :: Socket -> IOException -> IO (Result a) handleSocketError sk e = do se <- getSocketOption sk SoError case se of- 0 -> throw e+ 0 -> ioError e 10054 -> return $ Left ErrorReset -- reset _ -> return $ Left $ ErrorMisc $ show se -instance Stream Socket where- readBlock sk n = (liftM Right $ fn n) `Exception.catch` (handleSocketError sk)- where- fn x = do { str <- myrecv sk x- ; let len = length str- ; if len < x- then ( fn (x-len) >>= \more -> return (str++more) ) - else return str- }-- -- Use of the following function is discouraged.- -- The function reads in one character at a time, - -- which causes many calls to the kernel recv()- -- hence causes many context switches.- readLine sk = (liftM Right $ fn "") `Exception.catch` (handleSocketError sk)- where- fn str =- do { c <- myrecv sk 1 -- like eating through a straw.- ; if null c || c == "\n"- then return (reverse str++c)- else fn (head c:str)- }- - writeBlock sk str = (liftM Right $ fn str) `Exception.catch` (handleSocketError sk)- where- fn [] = return ()- fn x = send sk x >>= \i -> fn (drop i x)-- -- This slams closed the connection (which is considered rude for TCP\/IP)- close sk = shutdown sk ShutdownBoth >> sClose sk- myrecv :: Socket -> Int -> IO String-myrecv _ 0 = return "" myrecv sock len = let handler e = if isEOFError e then return [] else ioError e in System.IO.Error.catch (recv sock len) handler++instance Stream Socket where+ readBlock sk n = readBlockSocket sk n+ readLine sk = readLineSocket sk+ writeBlock sk str = writeBlockSocket sk str+ close sk = shutdown sk ShutdownBoth >> sClose sk+ -- This slams closed the connection (which is considered rude for TCP\/IP)++readBlockSocket :: Socket -> Int -> IO (Result String)+readBlockSocket sk n = (liftM Right $ fn n) `catchIO` (handleSocketError sk)+ where+ fn x = do { str <- myrecv sk x+ ; let len = length str+ ; if len < x+ then ( fn (x-len) >>= \more -> return (str++more) )+ else return str+ }++-- Use of the following function is discouraged.+-- The function reads in one character at a time, +-- which causes many calls to the kernel recv()+-- hence causes many context switches.+readLineSocket :: Socket -> IO (Result String)+readLineSocket sk = (liftM Right $ fn "") `catchIO` (handleSocketError sk)+ where+ fn str = do+ c <- myrecv sk 1 -- like eating through a straw.+ if null c || c == "\n"+ then return (reverse str++c)+ else fn (head c:str)+ +writeBlockSocket :: Socket -> String -> IO (Result ())+writeBlockSocket sk str = (liftM Right $ fn str) `catchIO` (handleSocketError sk)+ where+ fn [] = return ()+ fn x = send sk x >>= \i -> fn (drop i x)
Network/TCP.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE TypeSynonymInstances #-} ----------------------------------------------------------------------------- -- | -- Module : Network.TCP@@ -17,176 +18,319 @@ -- - Removed unused exported functions. -- -- * Changes by Simon Foster:--- - Split module up into to sepearate Network.[Stream,TCP,HTTP] modules+-- - Split module up into to separate Network.[Stream,TCP,HTTP] modules -- ----------------------------------------------------------------------------- module Network.TCP ( Connection , openTCPPort , isConnectedTo++ , openTCPConnection+ , isTCPConnectedTo+ + , HandleStream+ , HStream(..)+ + , StreamHooks(..)+ , nullHooks+ , setStreamHooks ) where import Network.BSD (getHostByName, hostAddresses) import Network.Socket- ( Socket, SockAddr(SockAddrInet), SocketOption(KeepAlive, SoError)- , SocketType(Stream), inet_addr, connect, sendTo+ ( Socket, SockAddr(SockAddrInet), SocketOption(KeepAlive)+ , SocketType(Stream), inet_addr, connect , shutdown, ShutdownCmd(ShutdownSend, ShutdownReceive)- , sClose, sIsConnected, setSocketOption, getSocketOption+ , sClose, sIsConnected, setSocketOption , socket, Family(AF_INET) )+import qualified Network.Stream as Stream+ ( Stream(readBlock, readLine, writeBlock, close) ) import Network.Stream- ( Stream(readBlock, readLine, writeBlock, close)- , ConnError(ErrorMisc, ErrorReset, ErrorClosed)- , bindE+ ( ConnError(..)+ , Result )-import Network.StreamSocket (myrecv, handleSocketError)+import Network.BufferType -import Control.Exception as Exception (catch, catchJust, finally, ioErrors, throw)-import Data.List (elemIndex)-import Data.Char (toLower)-import Data.IORef (IORef, newIORef, readIORef, writeIORef, modifyIORef)+import Network.HTTP.Base ( catchIO )+import Network.Socket ( socketToHandle ) +import Data.Char ( toLower )+import Data.Maybe ( fromMaybe )+import Data.Word ( Word8 )+import Control.Concurrent+import Control.Monad ( liftM )+import System.IO ( Handle, hFlush, IOMode(..), hClose )+import System.IO.Error ( isEOFError )++import qualified Data.ByteString as Strict+import qualified Data.ByteString.Lazy as Lazy+ ----------------------------------------------------------------- ------------------ TCP Connections ------------------------------ ----------------------------------------------------------------- -- | The 'Connection' newtype is a wrapper that allows us to make--- connections an instance of the StreamIn\/Out classes, without ghc extensions.+-- connections an instance of the Stream class, without ghc extensions. -- While this looks sort of like a generic reference to the transport -- layer it is actually TCP specific, which can be seen in the -- implementation of the 'Stream Connection' instance.-newtype Connection = ConnRef {getRef :: IORef Conn}+newtype Connection = Connection (HandleStream String) -data Conn = MkConn { connSock :: ! Socket- , connAddr :: ! SockAddr - , connBffr :: ! String - , connHost :: String- }- | ConnClosed- deriving(Eq)+newtype HandleStream a = HandleStream {getRef :: MVar (Conn a)} +data Conn a + = MkConn { connSock :: ! Socket+ , connHandle :: Handle+ , connBuffer :: BufferOp a+ , connInput :: Maybe a+ , connHost :: String+ , connHooks :: Maybe (StreamHooks a)+ }+ | ConnClosed+ deriving(Eq)++connHooks' :: Conn a -> Maybe (StreamHooks a)+connHooks' ConnClosed{} = Nothing+connHooks' x = connHooks x++-- all of these are post-op hooks+data StreamHooks ty+ = StreamHooks+ { hook_readLine :: (ty -> String) -> Result ty -> IO ()+ , hook_readBlock :: (ty -> String) -> Int -> Result ty -> IO ()+ , hook_writeBlock :: (ty -> String) -> ty -> Result () -> IO ()+ , hook_close :: IO ()+ }++instance Eq ty => Eq (StreamHooks ty) where+ (==) _ _ = True++nullHooks :: StreamHooks ty+nullHooks = StreamHooks + { hook_readLine = \ _ _ -> return ()+ , hook_readBlock = \ _ _ _ -> return ()+ , hook_writeBlock = \ _ _ _ -> return ()+ , hook_close = return ()+ }++setStreamHooks :: HandleStream ty -> StreamHooks ty -> IO ()+setStreamHooks h sh = modifyMVar_ (getRef h) (\ c -> return c{connHooks=Just sh})++class BufferType bufType => HStream bufType where+ openStream :: String -> Int -> IO (HandleStream bufType)+ readLine :: HandleStream bufType -> IO (Result bufType)+ readBlock :: HandleStream bufType -> Int -> IO (Result bufType)+ writeBlock :: HandleStream bufType -> bufType -> IO (Result ())+ close :: HandleStream bufType -> IO ()+ +instance HStream Strict.ByteString where+ openStream = openTCPConnection+ readBlock c n = readBlockBS c n+ readLine c = readLineBS c+ writeBlock c str = writeBlockBS c str+ close c = closeIt c Strict.null++instance HStream Lazy.ByteString where+ openStream = \ a b -> openTCPConnection_ a b True+ readBlock c n = readBlockBS c n+ readLine c = readLineBS c+ writeBlock c str = writeBlockBS c str+ close c = closeIt c Lazy.null++instance Stream.Stream Connection where+ readBlock (Connection c) = Network.TCP.readBlock c+ readLine (Connection c) = Network.TCP.readLine c+ writeBlock (Connection c) = Network.TCP.writeBlock c + close (Connection c) = Network.TCP.close c+ +instance HStream String where+ openStream = openTCPConnection+ readBlock ref n = readBlockBS ref n++ -- This function uses a buffer, at this time the buffer is just 1000 characters.+ -- (however many bytes this is is left to the user to decypher)+ readLine ref = readLineBS ref+ -- The 'Connection' object allows no outward buffering, + -- since in general messages are serialised in their entirety.+ writeBlock ref str = writeBlockBS ref str -- (stringToBuf str)++ -- Closes a Connection. Connection will no longer+ -- allow any of the other Stream functions. Notice that a Connection may close+ -- at any time before a call to this function. This function is idempotent.+ -- (I think the behaviour here is TCP specific)+ close c = closeIt c null+ -- | This function establishes a connection to a remote -- host, it uses "getHostByName" which interrogates the -- DNS system, hence may trigger a network connection. ----- Add a "persistant" option? Current persistant is default.--- Use "Result" type for synchronous exception reporting? openTCPPort :: String -> Int -> IO Connection-openTCPPort uri port = +openTCPPort uri port = openTCPConnection uri port >>= return.Connection++-- Add a "persistent" option? Current persistent is default.+-- Use "Result" type for synchronous exception reporting?+openTCPConnection :: BufferType ty => String -> Int -> IO (HandleStream ty)+openTCPConnection uri port = openTCPConnection_ uri port False+openTCPConnection_ :: BufferType ty => String -> Int -> Bool -> IO (HandleStream ty)+openTCPConnection_ uri port stashInput = do { s <- socket AF_INET Stream 6 ; setSocketOption s KeepAlive 1- ; host <- Exception.catch (inet_addr uri) -- handles ascii IP numbers- (\_ -> getHostByName uri >>= \host ->+ ; host <- catchIO (inet_addr uri) -- handles ascii IP numbers+ (\ _ -> getHostByName uri >>= \host -> case hostAddresses host of [] -> return (error "no addresses in host entry") (h:_) -> return h) ; let a = SockAddrInet (toEnum port) host- ; Exception.catch (connect s a) (\e -> sClose s >> throw e)- ; v <- newIORef (MkConn s a [] uri)- ; return (ConnRef v)+ ; catchIO (connect s a) (\e -> sClose s >> ioError e)+ ; h <- socketToHandle s ReadWriteMode+ ; mb <- case stashInput of { True -> liftM Just $ buf_hGetContents bufferOps h; _ -> return Nothing }+ ; let conn = + (MkConn { connSock = s+ , connHandle = h+ , connBuffer = bufferOps+ , connInput = mb+ , connHost = uri+ , connHooks = Nothing+ })+ ; v <- newMVar conn+ ; return (HandleStream v) } -instance Stream Connection where- readBlock ref n = - readIORef (getRef ref) >>= \conn -> case conn of- ConnClosed -> return (Left ErrorClosed)- (MkConn sk addr bfr hst)- | length bfr >= n ->- do { modifyIORef (getRef ref) (\c -> c { connBffr=(drop n bfr) })- ; return (Right $ take n bfr)- }- | otherwise ->- do { modifyIORef (getRef ref) (\c -> c { connBffr=[] })- ; more <- readBlock sk (n - length bfr)- ; return $ case more of- Left _ -> more- Right s -> (Right $ bfr ++ s)- }+closeConnection :: HandleStream a -> IO Bool -> IO ()+closeConnection ref readL = do+ -- won't hold onto the lock for the duration+ -- we are draining it...ToDo: have Connection+ -- into a shutting-down state so that other+ -- threads will simply back off if/when attempting+ -- to also close it.+ c <- readMVar (getRef ref)+ closeConn c `catchIO` (\_ -> return ())+ modifyMVar_ (getRef ref) (\ _ -> return ConnClosed)+ where+ -- Be kind to peer & close gracefully.+ closeConn ConnClosed = return ()+ closeConn conn = do+ let sk = connSock conn+ shutdown sk ShutdownSend+ suck readL+ hClose (connHandle conn)+ shutdown sk ShutdownReceive+ sClose sk - -- This function uses a buffer, at this time the buffer is just 1000 characters.- -- (however many bytes this is is left to the user to decypher)- readLine ref =- readIORef (getRef ref) >>= \conn -> case conn of- ConnClosed -> return (Left ErrorClosed)- (MkConn sk addr bfr _)- | null bfr -> {- read in buffer -}- do { str <- myrecv sk 1000 -- DON'T use "readBlock sk 1000" !!- -- ... since that call will loop.- ; let len = length str- ; if len == 0 {- indicates a closed connection -}- then return (Right "")- else modifyIORef (getRef ref) (\c -> c { connBffr=str })- >> readLine ref -- recursion- }- | otherwise ->- case elemIndex '\n' bfr of- Nothing -> {- need recursion to finish line -}- do { modifyIORef (getRef ref) (\c -> c { connBffr=[] })- ; more <- readLine ref -- contains extra recursion - ; return $ more `bindE` \str -> Right (bfr++str)- }- Just i -> {- end of line found -}- let (bgn,end) = splitAt i bfr in- do { modifyIORef (getRef ref) (\c -> c { connBffr=(drop 1 end) })- ; return (Right (bgn++['\n']))- }+ suck :: IO Bool -> IO ()+ suck rd = do+ f <- rd+ if f then return () else suck rd +-- | Checks both that the underlying Socket is connected+-- and that the connection peer matches the given+-- host name (which is recorded locally).+isConnectedTo :: Connection -> String -> IO Bool+isConnectedTo (Connection conn) name = do+ v <- readMVar (getRef conn)+ case v of+ ConnClosed -> return False+ _ + | map toLower (connHost v) == map toLower name -> sIsConnected (connSock v)+ | otherwise -> return False +isTCPConnectedTo :: HandleStream ty -> String -> IO Bool+isTCPConnectedTo conn name = do+ v <- readMVar (getRef conn)+ case v of+ ConnClosed -> return False+ _ + | map toLower (connHost v) == map toLower name -> sIsConnected (connSock v)+ | otherwise -> return False - -- The 'Connection' object allows no outward buffering, - -- since in general messages are serialised in their entirety.- writeBlock ref str =- readIORef (getRef ref) >>= \conn -> case conn of- ConnClosed -> return (Left ErrorClosed)- (MkConn sk addr _ _) -> fn sk addr str `Exception.catch` (handleSocketError sk)- where- fn sk addr s- | null s = return (Right ()) -- done- | otherwise =- getSocketOption sk SoError >>= \se ->- if se == 0- then sendTo sk s addr >>= \i -> fn sk addr (drop i s)- else writeIORef (getRef ref) ConnClosed >>- if se == 10054- then return (Left ErrorReset)- else return (Left $ ErrorMisc $ show se) +readBlockBS :: HandleStream a -> Int -> IO (Result a)+readBlockBS ref n = do+ conn <- readMVar (getRef ref)+ x <- + case conn of+ ConnClosed -> return (Left ErrorClosed)+ MkConn{} -> bufferGetBlock ref n -- (connBuffer conn) (connInput conn) (connHandle conn) n+ fromMaybe (return ()) (fmap (\ h -> hook_readBlock h (buf_toStr $ connBuffer conn) n x) (connHooks' conn))+ return x - -- Closes a Connection. Connection will no longer- -- allow any of the other Stream functions. Notice that a Connection may close- -- at any time before a call to this function. This function is idempotent.- -- (I think the behaviour here is TCP specific)- close ref = - do { c <- readIORef (getRef ref)- ; Exception.catchJust Exception.ioErrors (closeConn c) (\_ -> return ())- ; writeIORef (getRef ref) ConnClosed- }- where- -- Be kind to peer & close gracefully.- closeConn (ConnClosed) = return ()- closeConn (MkConn sk addr [] _) =- (`Exception.finally` sClose sk) $- do { shutdown sk ShutdownSend- ; suck ref- ; shutdown sk ShutdownReceive- }- - suck :: Connection -> IO ()- suck cn = readLine cn >>= - either (\_ -> return ()) -- catch errors & ignore- (\x -> if null x then return () else suck cn)+-- This function uses a buffer, at this time the buffer is just 1000 characters.+-- (however many bytes this is is left to the user to decypher)+readLineBS :: HandleStream a -> IO (Result a)+readLineBS ref = do+ conn <- readMVar (getRef ref)+ x <- + case conn of+ ConnClosed -> return (Left ErrorClosed)+ MkConn{} -> bufferReadLine ref -- (connBuffer conn) (connHandle conn) (connInput conn)+ fromMaybe (return ()) (fmap (\ h -> hook_readLine h (buf_toStr $ connBuffer conn) x) (connHooks' conn))+ return x --- | Checks both that the underlying Socket is connected--- and that the connection peer matches the given--- host name (which is recorded locally).-isConnectedTo :: Connection -> String -> IO Bool-isConnectedTo conn name =- do { v <- readIORef (getRef conn)- ; case v of- ConnClosed -> return False- (MkConn sk _ _ h) ->- if (map toLower h == map toLower name)- then sIsConnected sk- else return False- }+-- The 'Connection' object allows no outward buffering, +-- since in general messages are serialised in their entirety.+writeBlockBS :: HandleStream a -> a -> IO (Result ())+writeBlockBS ref b = do+ conn <- readMVar (getRef ref)+ x <- + case conn of+ ConnClosed -> return (Left ErrorClosed)+ MkConn{} -> bufferPutBlock (connBuffer conn) (connHandle conn) b+ fromMaybe (return ()) (fmap (\ h -> hook_writeBlock h (buf_toStr $ connBuffer conn) b x) (connHooks' conn))+ return x +closeIt :: HandleStream ty -> (ty -> Bool) -> IO ()+closeIt c p = do+ closeConnection c (readLineBS c >>= \ x -> case x of { Right xs -> return (p xs); _ -> return True})+ conn <- readMVar (getRef c)+ fromMaybe (return ()) (fmap (\ h -> hook_close h) (connHooks' conn))+++bufferGetBlock :: HandleStream a -> Int -> IO (Result a)+bufferGetBlock ref n = do+ conn <- readMVar (getRef ref)+ case conn of+ ConnClosed -> return (Left ErrorClosed)+ MkConn{} -> do+ case connInput conn of+ Just c -> do+ let (a,b) = buf_splitAt (connBuffer conn) n c+ modifyMVar_ (getRef ref) (\ co -> return co{connInput=Just b})+ return (Right a)+ _ -> do+ Prelude.catch (buf_hGet (connBuffer conn) (connHandle conn) n >>= return.Right)+ (\ e -> if isEOFError e + then return (Right (buf_empty (connBuffer conn)))+ else return (Left (ErrorMisc (show e))))++bufferPutBlock :: BufferOp a -> Handle -> a -> IO (Result ())+bufferPutBlock ops h b = + Prelude.catch (buf_hPut ops h b >> hFlush h >> return (Right ()))+ (\ e -> return (Left (ErrorMisc (show e))))++bufferReadLine :: HandleStream a -> IO (Result a) -- BufferOp a -> Handle -> IO (Result a)+bufferReadLine ref = do+ conn <- readMVar (getRef ref)+ case conn of+ ConnClosed -> return (Left ErrorClosed)+ MkConn{} -> do+ case connInput conn of+ Just c -> do+ let (a,b0) = buf_span (connBuffer conn) (/='\n') c+ let (newl,b1) = buf_splitAt (connBuffer conn) 1 b0+ modifyMVar_ (getRef ref) (\ co -> return co{connInput=Just b1})+ return (Right (buf_append (connBuffer conn) a newl))+ _ -> Prelude.catch + (buf_hGetLine (connBuffer conn) (connHandle conn) >>= return . Right . appendNL (connBuffer conn))+ (\ e -> + if isEOFError e+ then return (Right (buf_empty (connBuffer conn)))+ else return (Left (ErrorMisc (show e))))+ where+ -- yes, this s**ks.. _may_ have to be addressed if perf+ -- suggests worthiness.+ appendNL ops b = buf_snoc ops b nl+ + nl :: Word8+ nl = fromIntegral (fromEnum '\n')