packages feed

HTTP 4000.0.4 → 4000.0.5

raw patch · 14 files changed

+1163/−675 lines, 14 filesdep ~basePVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependency ranges changed: base

API changes (from Hackage documentation)

- Network.Browser: uriTrimHost :: URI -> URI
- Network.HTTP.Base: crlf :: String
- Network.HTTP.Base: sp :: String
+ Network.Browser: AlgMD5 :: Algorithm
+ Network.Browser: AlgMD5sess :: Algorithm
+ Network.Browser: ChalBasic :: String -> Challenge
+ Network.Browser: ChalDigest :: String -> [URI] -> String -> Maybe String -> Bool -> Maybe Algorithm -> [Qop] -> Challenge
+ Network.Browser: MkCookie :: String -> String -> String -> Maybe String -> Maybe String -> Maybe String -> Cookie
+ Network.Browser: QopAuth :: Qop
+ Network.Browser: QopAuthInt :: Qop
+ Network.Browser: chAlgorithm :: Challenge -> Maybe Algorithm
+ Network.Browser: chDomain :: Challenge -> [URI]
+ Network.Browser: chNonce :: Challenge -> String
+ Network.Browser: chOpaque :: Challenge -> Maybe String
+ Network.Browser: chQop :: Challenge -> [Qop]
+ Network.Browser: chRealm :: Challenge -> String
+ Network.Browser: chStale :: Challenge -> Bool
+ 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: data Algorithm
+ Network.Browser: data Challenge
+ Network.Browser: data Qop
+ Network.Browser: getCookieFilter :: BrowserAction t (URI -> Cookie -> IO Bool)
+ Network.Browser: getMaxAuthAttempts :: BrowserAction t (Maybe Int)
+ Network.Browser: getMaxErrorRetries :: BrowserAction t (Maybe Int)
+ Network.Browser: getMaxRedirects :: BrowserAction t (Maybe Int)
+ Network.Browser: getProxy :: BrowserAction t Proxy
+ Network.Browser: setMaxAuthAttempts :: Maybe Int -> BrowserAction t ()
+ Network.Browser: setMaxErrorRetries :: Maybe Int -> BrowserAction t ()
+ Network.Browser: setMaxRedirects :: Maybe Int -> BrowserAction t ()
+ Network.Browser: type FormVar = (String, String)
+ Network.BufferType: buf_concat :: BufferOp a -> [a] -> a
+ Network.HTTP: getRequest :: String -> Request_String
+ Network.HTTP: getResponseBody :: Result (Response ty) -> IO ty
+ Network.HTTP: postRequest :: String -> Request_String
+ Network.HTTP: receiveHTTP :: (HStream ty) => HandleStream ty -> IO (Result (Request ty))
+ Network.HTTP: respondHTTP :: (HStream ty) => HandleStream ty -> Response ty -> IO ()
+ Network.HTTP: sendHTTP :: (HStream ty) => HandleStream ty -> Request ty -> IO (Result (Response ty))
+ Network.HTTP: sendHTTP_notify :: (HStream ty) => HandleStream ty -> Request ty -> IO () -> IO (Result (Response ty))
+ Network.HTTP: simpleHTTP :: (HStream ty) => Request ty -> IO (Result (Response ty))
+ Network.HTTP: simpleHTTP_ :: (HStream ty) => HandleStream ty -> Request ty -> IO (Result (Response ty))
+ Network.HTTP.Base: CONNECT :: RequestMethod
+ Network.HTTP.Base: Custom :: String -> RequestMethod
+ Network.HTTP.Base: NormalizeRequestOptions :: Bool -> Bool -> Maybe String -> [RequestNormalizer ty] -> NormalizeRequestOptions ty
+ Network.HTTP.Base: data NormalizeRequestOptions ty
+ Network.HTTP.Base: defaultGETRequest :: URI -> Request_String
+ Network.HTTP.Base: defaultGETRequest_ :: (BufferType a) => URI -> Request a
+ Network.HTTP.Base: defaultNormalizeRequestOptions :: NormalizeRequestOptions ty
+ Network.HTTP.Base: defaultUserAgent :: String
+ Network.HTTP.Base: getRequestVersion :: Request a -> Maybe String
+ Network.HTTP.Base: getResponseVersion :: Response a -> Maybe String
+ Network.HTTP.Base: libUA :: String
+ Network.HTTP.Base: mkRequest :: (BufferType ty) => RequestMethod -> URI -> Request ty
+ Network.HTTP.Base: normCustoms :: NormalizeRequestOptions ty -> [RequestNormalizer ty]
+ Network.HTTP.Base: normDoClose :: NormalizeRequestOptions ty -> Bool
+ Network.HTTP.Base: normForProxy :: NormalizeRequestOptions ty -> Bool
+ Network.HTTP.Base: normUserAgent :: NormalizeRequestOptions ty -> Maybe String
+ Network.HTTP.Base: normalizeRequest :: NormalizeRequestOptions ty -> Request ty -> Request ty
+ Network.HTTP.Base: reqURIAuth :: Request ty -> URIAuth
+ Network.HTTP.Base: setRequestVersion :: String -> Request a -> Request a
+ Network.HTTP.Base: setResponseVersion :: String -> Response a -> Response a
+ Network.HTTP.Base: splitRequestURI :: URI -> (String, URI)
+ Network.HTTP.Base: type RequestNormalizer ty = NormalizeRequestOptions ty -> Request ty -> Request ty
+ Network.HTTP.Base: uriAuthPort :: Maybe URI -> URIAuth -> Int
+ Network.HTTP.Headers: HdrTE :: HeaderName
+ Network.HTTP.Headers: HdrTrailer :: HeaderName
+ Network.HTTP.Headers: mkHeader :: HeaderName -> String -> Header
+ Network.TCP: hstreamToConnection :: HandleStream String -> Connection
- Network.Browser: setEventHandler :: (BrowserEvent ty -> BrowserAction ty ()) -> BrowserAction ty ()
+ Network.Browser: setEventHandler :: Maybe (BrowserEvent ty -> BrowserAction ty ()) -> BrowserAction ty ()
- 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: BufferOp :: (Handle -> Int -> IO a) -> (Handle -> IO a) -> (Handle -> a -> IO ()) -> (Handle -> IO a) -> a -> (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

Files

+ CHANGES view
@@ -0,0 +1,20 @@+Version 4004.0.5: release 2009-03-30; changes from 4004.0.4++ * Get serious about comments and Haddock documentation.+ * Cleaned up normalization of requests, fixing bugs and bringing together+   previous disparate attempts at handling this.+ * RequestMethod now supports custom verbs; use the (Custom String) constructor+ * Beef up Network.HTTP.Base's support for normalizing requests and URIs:+ +    * added splitRequestURI which divides a URI into two; the Authority portion+      (as a String) and the input URI sans the authority portion. Useful when+      wanting to split up a request's URI into its Host: and abs_path pieces.+    * added normalizeRequest :: Bool -> Request ty -> Request ty, which +      fixes up a requests URI path and Host: info depending on whether it is+      destined for a proxy or not (controlled by the Bool.)+    * moved defaultRequest, defaultRequest_, libUA from Network.Browser+      to Network.HTTP.Base+    * added mkRequest :: RequestMethod -> URI -> Bool -> Request ty+      for constructing normalized&sane Request bases on top of which+      you can add custom headers, body payload etc.+
HTTP.cabal view
@@ -1,5 +1,5 @@ Name: HTTP-Version: 4000.0.4+Version: 4000.0.5 Cabal-Version: >= 1.2 Build-type: Simple License: BSD3@@ -12,7 +12,7 @@   Copyright (c) 2004, Ganesh Sittampalam   Copyright (c) 2004-2005, Dominic Steinitz   Copyright 2007 Robin Bate Boerop-  Copyright 2008-2009 Sigbjorn Finne+  Copyright 2008- Sigbjorn Finne Author: Warrick Gray <warrick.gray@hotmail.com> Maintainer: Sigbjorn Finne <sigbjorn.finne@gmail.com> Homepage: http://projects.haskell.org/http/@@ -20,16 +20,29 @@ Synopsis: A library for client-side HTTP Description:  - A library for client-side HTTP, version 4. 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@.+ A package for sending and receiving HTTP requests and responses, all implemented+ in Haskell (assuming you've already got a network stack to spare, via the 'network'+ package!) The representation of content of in requests and responses is user-controllable,+ letting you pick a representation that fits your code better (e.g., use @ByteString@s rather+ than the default Haskell @String@s.) Example uses:  .- Inspired in part by Jonas Aadahl et al's work on ByteString'ifying HTTP- a couple of years ago.+ >+ >    do+ >      rsp <- Network.HTTP.simpleHTTP (getRequest "http://www.haskell.org/")+ >              -- fetch document and return it (as a 'String'.)+ >      fmap (take 100) (getResponseBody rsp)+ >+ >    do + >      rsp <- Network.Browser.browse $ do+ >               setAllowRedirects True -- handle HTTP redirects+ >               request $ getRequest "http://google.com/"+ >      fmap (take 100) (getResponseBody rsp)+ >   .  Git repository available at <git://code.galois.com/HTTPbis.git> +Extra-Source-Files: CHANGES+ Flag old-base   description: Old, monolithic base   default: False@@ -53,11 +66,8 @@                  Network.HTTP.MD5Aux,                  Network.HTTP.Utils   GHC-options: -fwarn-missing-signatures -Wall-  Build-depends: network, parsec, bytestring, mtl-+  Build-depends: base >= 2, network, parsec, mtl   if flag(old-base)     Build-depends: base < 3   else-    Build-depends: base >= 3-    -  Build-depends: array, old-time+    Build-depends: base >= 3, array, old-time, bytestring
Network/Browser.hs view
@@ -1,91 +1,113 @@--------------------------------------------------------------------------------- |--- Module      :  Network.Browser--- Copyright   :  (c) Warrick Gray 2002--- License     :  BSD--- --- Maintainer  :  Sigbjorn Finne <sigbjorn.finne@gmail.com>--- Stability   :  experimental--- Portability :  non-portable (not tested)------ An HTTP\/1.1 compatible wrapper for the HTTP module.------------------------------------------------------------------------------+{- |++Module      :  Network.Browser+Copyright   :  (c) Warrick Gray 2002+License     :  BSD  -{--  Changes by Robin Bate Boerop <robin@bateboerop.name>:-   - Made dependencies explicit in import statements.-   - Added type signatures.-   - Imported new StreamDebugger module.+Maintainer  :  Sigbjorn Finne <sigbjorn.finne@gmail.com>+Stability   :  experimental+Portability :  non-portable (not tested) -  Change Log:-   - altered 'closeTCP' to 'close', for consistency with altered HTTP-   - added debugging settings to browser.+Session-level interactions over HTTP.+ +The "Network.Browser" goes beyond the basic "Network.HTTP" functionality in +providing support for more involved, and real, request/response interactions over +HTTP. Additional features supported are: -  To Do: -   - testing!!!-   - remove BrowserAction type? Possibly replace with IORef?-   - (more todo's in the HTTP mod)+* HTTP Authentication handling --}+* Transparent handling of redirects -module Network.Browser (-    BrowserState,-    BrowserAction,      -- browser monad, effectively a state monad.-    Cookie,-    Form(..),-    Proxy(..),-    -    browse,             -- BrowserAction a -> IO a-    request,            -- Request -> BrowserAction Response-    -    getBrowserState,-    withBrowserState,-    -    setAllowRedirects,-    getAllowRedirects,-    -    Authority(..),-    getAuthorities, -    setAuthorities, -    addAuthority, -    getAuthorityGen, -    setAuthorityGen, -    setAllowBasicAuth,+* Cookie stores + transmission. -    setCookieFilter,-    defaultCookieFilter,-    userCookieFilter,-    -    getCookies,-    setCookies,-    addCookie,+* Transaction logging -    setErrHandler,         -- :: (String -> IO ()) -> BrowserAction t ()-    setOutHandler,         -- :: (String -> IO ()) -> BrowserAction t ()-    -    setEventHandler,         -- :: (BrowserEvent t -> BrowserAction t ()) -> BrowserAction t ()+* Proxy-mediated connections.++Example use:++>    do +>      rsp <- Network.Browser.browse $ do+>               setAllowRedirects True -- handle HTTP redirects+>               request $ getRequest "http://google.com/"+>      fmap (take 100) (getResponseBody rsp)+ +-}+module Network.Browser +       ( BrowserState+       , BrowserAction      -- browser monad, effectively a state monad.+       , Proxy(..)+       +       , browse             -- :: BrowserAction a -> IO a+       , request            -- :: Request -> BrowserAction Response     -    BrowserEvent(..),-    BrowserEventType(..),-    RequestID,+       , getBrowserState    -- :: BrowserAction t (BrowserState t)+       , withBrowserState   -- :: BrowserState t -> BrowserAction t a -> BrowserAction t a+       +       , setAllowRedirects  -- :: Bool -> BrowserAction t ()+       , getAllowRedirects  -- :: BrowserAction t Bool -    setProxy,+       , setMaxRedirects    -- :: Int -> BrowserAction t ()+       , getMaxRedirects    -- :: BrowserAction t (Maybe Int)+       +       , Authority(..)+       , getAuthorities+       , setAuthorities+       , addAuthority+       , Challenge(..)+       , Qop(..)+       , Algorithm(..)+       +       , getAuthorityGen+       , setAuthorityGen+       , setAllowBasicAuth+       +       , setMaxErrorRetries  -- :: Maybe Int -> BrowserAction t ()+       , getMaxErrorRetries  -- :: BrowserAction t (Maybe Int) -    setDebugLog,+       , setMaxAuthAttempts  -- :: Maybe Int -> BrowserAction t ()+       , getMaxAuthAttempts  -- :: BrowserAction t (Maybe Int) -    out,-    err,-    ioAction,           -- :: IO a -> BrowserAction a+       , setCookieFilter     -- :: (URI -> Cookie -> IO Bool) -> BrowserAction t ()+       , getCookieFilter     -- :: BrowserAction t (URI -> Cookie -> IO Bool)+       , defaultCookieFilter -- :: URI -> Cookie -> IO Bool+       , userCookieFilter    -- :: URI -> Cookie -> IO Bool+       +       , Cookie(..)+       , getCookies        -- :: BrowserAction t [Cookie]+       , setCookies        -- :: [Cookie] -> BrowserAction t ()+       , addCookie         -- :: Cookie   -> BrowserAction t ()+       +       , setErrHandler     -- :: (String -> IO ()) -> BrowserAction t ()+       , setOutHandler     -- :: (String -> IO ()) -> BrowserAction t ()+    +       , setEventHandler   -- :: (BrowserEvent t -> BrowserAction t ()) -> BrowserAction t ()+       +       , BrowserEvent(..)+       , BrowserEventType(..)+       , RequestID+       +       , setProxy         -- :: Proxy -> BrowserAction t ()+       , getProxy         -- :: BrowserAction t Proxy+       , setDebugLog+       +       , out              -- :: String -> BrowserAction t ()+       , err              -- :: String -> BrowserAction t ()+       , ioAction         -- :: IO a -> BrowserAction a -    defaultGETRequest,-    defaultGETRequest_,-    formToRequest,-    uriDefaultTo,-    uriTrimHost-) where+       , defaultGETRequest+       , defaultGETRequest_+       +       , formToRequest+       , uriDefaultTo+       +         -- old and half-baked; don't use:+       , Form(..)+       , FormVar+       ) where  import Network.URI-   ( URI(uriAuthority, uriScheme, uriPath, uriQuery)+   ( URI(uriAuthority, uriPath, uriQuery)    , URIAuth(..)    , parseURI, parseURIReference, relativeTo    )@@ -101,7 +123,8 @@ import Data.Char (toLower,isAlphaNum,isSpace) import Data.List (isPrefixOf,isSuffixOf) import Data.Maybe (fromMaybe, listToMaybe, catMaybes, fromJust, isJust)-import Control.Monad (foldM, filterM, liftM, when)+import Control.Monad (filterM, liftM, when)+ import Text.ParserCombinators.Parsec    ( Parser, char, many, many1, satisfy, parse, option, try    , (<|>), spaces, sepBy1@@ -114,40 +137,12 @@  import Data.Word (Word8) -type Octet = Word8- ------------------------------------------------------------------------------------------ Miscellaneous -------------------------------------------------------------------------------------------------word, quotedstring :: Parser String-quotedstring =-    do { char '"'  -- "-       ; str <- many (satisfy $ not . (=='"'))-       ; char '"'-       ; return str-       }--word = many1 (satisfy (\x -> isAlphaNum x || x=='_' || x=='.' || x=='-' || x==':'))----- | 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--- determining a full URI then anarchy reins.-uriDefaultTo :: URI -> URI -> URI-uriDefaultTo a b = maybe a id (a `relativeTo` b)--uriTrimHost :: URI -> URI-uriTrimHost uri = uri { uriScheme="", uriAuthority=Nothing }-------------------------------------------------------------------- ----------------------- Cookie Stuff ----------------------------- ------------------------------------------------------------------ --- Some conventions: ---     assume ckDomain is lowercase---+-- | @Cookie@ is the Haskell representation of HTTP cookie values.+-- See its relevant specs for authoritative details. data Cookie   = MkCookie      { ckDomain  :: String@@ -159,17 +154,19 @@     }     deriving(Show,Read) - instance Eq Cookie where     a == b  =  ckDomain a == ckDomain b              && ckName a == ckName b              && ckPath a == ckPath b --+-- | @defaultCookieFilter@ is the initial cookie acceptance filter.+-- It welcomes them all into the store @:-)@ defaultCookieFilter :: URI -> Cookie -> IO Bool defaultCookieFilter _url _cky = return True +-- | @userCookieFilter@ is a handy acceptance filter, asking the+-- user if he/she is willing to accept an incoming cookie before+-- adding it to the store. userCookieFilter :: URI -> Cookie -> IO Bool userCookieFilter url cky =     do putStrLn ("Set-Cookie received when requesting: " ++ show url)@@ -188,7 +185,6 @@        return (toLower x == 'y')         - -- | Serialise a Cookie for inclusion in a request. cookieToHeader :: Cookie -> Header cookieToHeader ck = Header HdrCookie text@@ -255,15 +251,20 @@         --- | Adds a cookie to the browser state, removing duplicates.+-- | @addCookie c@ adds a cookie to the browser state, removing duplicates. addCookie :: Cookie -> BrowserAction t () addCookie c = alterBS (\b -> b { bsCookies=c : fn (bsCookies b) })     where         fn = filter (not . (==c)) +-- | @setCookies cookies@ replaces the set of cookies known to+-- the browser to @cookies@. Useful when wanting to restore cookies+-- used across 'browse' invocations. setCookies :: [Cookie] -> BrowserAction t () setCookies cs = alterBS (\b -> b { bsCookies=cs }) +-- | @getCookies@ returns the current set of cookies known to+-- the browser. getCookies :: BrowserAction t [Cookie] getCookies = getBS bsCookies @@ -282,9 +283,11 @@                              Just p  -> p `isPrefixOf` path        +-- | @setCookieFilter fn@ sets the cookie acceptance filter to @fn@. setCookieFilter :: (URI -> Cookie -> IO Bool) -> BrowserAction t () setCookieFilter f = alterBS (\b -> b { bsCookieFilter=f }) +-- | @getCookieFilter@ returns the current cookie acceptance filter. getCookieFilter :: BrowserAction t (URI -> Cookie -> IO Bool) getCookieFilter = getBS bsCookieFilter @@ -331,7 +334,7 @@  -} -+-- | @Algorithm@ controls the digest algorithm to, @MD5@ or @MD5Session@. data Algorithm = AlgMD5 | AlgMD5sess     deriving(Eq) @@ -339,22 +342,24 @@     show AlgMD5 = "md5"     show AlgMD5sess = "md5-sess" -+-- |  data Qop = QopAuth | QopAuthInt     deriving(Eq,Show)  -data Challenge = ChalBasic  { chRealm   :: String }-               | ChalDigest { chRealm   :: String-                            , chDomain  :: [URI]-                            , chNonce   :: String-                            , chOpaque  :: Maybe String-                            , chStale   :: Bool-                            , chAlgorithm ::Maybe Algorithm-                            , chQop     :: [Qop]-                            }+data Challenge + = ChalBasic  { chRealm   :: String }+ | ChalDigest { chRealm   :: String+              , chDomain  :: [URI]+              , chNonce   :: String+              , chOpaque  :: Maybe String+              , chStale   :: Bool+              , chAlgorithm ::Maybe Algorithm+              , chQop     :: [Qop]+              } --- | Convert WWW-Authenticate header into a Challenge object+-- | @headerChallenge base www_auth@ tries to convert the @WWW-Authenticate@ header +-- @www_auth@  into a 'Challenge' value. headerToChallenge :: URI -> Header -> Maybe Challenge headerToChallenge baseURI (Header _ str) =     case parse challenge "" str of@@ -424,67 +429,91 @@             "md5-sess" -> Just AlgMD5sess             _          -> Nothing --data Authority = AuthBasic { auRealm    :: String-                           , auUsername :: String-                           , auPassword :: String-                           , auSite     :: URI-                           }-               | AuthDigest { auRealm     :: String-                            , auUsername  :: String-                            , auPassword  :: String-                            , auNonce     :: String-                            , auAlgorithm :: Maybe Algorithm-                            , auDomain    :: [URI]-                            , auOpaque    :: Maybe String-                            , auQop       :: [Qop]-                            }-+-- | @Authority@ specifies the HTTP Authentication method to use for+-- a given domain/realm; @Basic@ or @Digest@.+data Authority + = AuthBasic { auRealm    :: String+             , auUsername :: String+             , auPassword :: String+             , auSite     :: URI+             }+ | AuthDigest{ auRealm     :: String+             , auUsername  :: String+             , auPassword  :: String+             , auNonce     :: String+             , auAlgorithm :: Maybe Algorithm+             , auDomain    :: [URI]+             , auOpaque    :: Maybe String+             , auQop       :: [Qop]+             }  -- | Return authorities for a given domain and path. -- Assumes "dom" is lower case getAuthFor :: String -> String -> BrowserAction t [Authority]-getAuthFor dom pth =-    do { list <- getAuthorities-       ; return (filter match list)-       }-    where-        match :: Authority -> Bool-        match (AuthBasic _ _ _ s) = matchURI s-        match (AuthDigest _ _ _ _ _ ds _ _) = or (map matchURI ds)            +getAuthFor dom pth = getAuthorities >>= return . (filter match)+   where+    match :: Authority -> Bool+    match au@AuthBasic{}  = matchURI (auSite au)+    match au@AuthDigest{} = or (map matchURI (auDomain au)) -        matchURI :: URI -> Bool-        matchURI s = (uriToAuthorityString s == dom) && (uriPath s `isPrefixOf` pth)+    matchURI :: URI -> Bool+    matchURI s = (uriToAuthorityString s == dom) && (uriPath s `isPrefixOf` pth)      --- | Interacting with browser state:+-- | @getAuthorities@ return the current set of @Authority@s known+-- to the browser. getAuthorities :: BrowserAction t [Authority] getAuthorities = getBS bsAuthorities +-- @setAuthorities as@ replaces the Browser's known set+-- of 'Authority's to @as@. setAuthorities :: [Authority] -> BrowserAction t () setAuthorities as = alterBS (\b -> b { bsAuthorities=as }) +-- @addAuthority a@ adds 'Authority' @a@ to the Browser's+-- set of known authorities. addAuthority :: Authority -> BrowserAction t () addAuthority a = alterBS (\b -> b { bsAuthorities=a:bsAuthorities b }) +-- | @getAuthorityGen@ returns the current authority generator getAuthorityGen :: BrowserAction t (URI -> String -> IO (Maybe (String,String))) getAuthorityGen = getBS bsAuthorityGen +-- | @setAuthorityGen genAct@ sets the auth generator to @genAct@. setAuthorityGen :: (URI -> String -> IO (Maybe (String,String))) -> BrowserAction t () setAuthorityGen f = alterBS (\b -> b { bsAuthorityGen=f }) +-- | @setAllowBasicAuth onOff@ enables\/disables HTTP Basic Authentication. setAllowBasicAuth :: Bool -> BrowserAction t () setAllowBasicAuth ba = alterBS (\b -> b { bsAllowBasicAuth=ba }) +-- | @setMaxAuthAttempts mbMax@ sets the maximum number of authentication attempts+-- to do. If @Nothing@, rever to default max.+setMaxAuthAttempts :: Maybe Int -> BrowserAction t ()+setMaxAuthAttempts mb + | fromMaybe 0 mb < 0 = return ()+ | otherwise          = alterBS (\ b -> b{bsMaxAuthAttempts=mb}) +-- | @getMaxAuthAttempts@ returns the current max auth attempts. If @Nothing@,+-- the browser's default is used.+getMaxAuthAttempts :: BrowserAction t (Maybe Int)+getMaxAuthAttempts = getBS bsMaxAuthAttempts +-- | @setMaxErrorRetries mbMax@ sets the maximum number of attempts at+-- transmitting a request. If @Nothing@, rever to default max.+setMaxErrorRetries :: Maybe Int -> BrowserAction t ()+setMaxErrorRetries mb+ | fromMaybe 0 mb < 0 = return ()+ | otherwise          = alterBS (\ b -> b{bsMaxErrorRetries=mb}) +-- | @getMaxErrorRetries@ returns the current max number of error retries.+getMaxErrorRetries :: BrowserAction t (Maybe Int)+getMaxErrorRetries = getBS bsMaxErrorRetries+ -- TO BE CHANGED!!! pickChallenge :: [Challenge] -> Maybe Challenge pickChallenge = listToMaybe -- -- | Retrieve a likely looking authority for a Request. anticipateChallenge :: Request ty -> BrowserAction t (Maybe Authority) anticipateChallenge rq =@@ -493,7 +522,6 @@        ; return (listToMaybe authlist)        } - -- | Asking the user to respond to a challenge challengeToAuthority :: URI -> Challenge -> BrowserAction t (Maybe Authority) challengeToAuthority uri ch =@@ -572,6 +600,8 @@         digesturi = show (rqURI rq)         noncevalue = auNonce a +type Octet = Word8+ -- FIXME: these probably only work right for latin-1 strings stringToOctets :: String -> [Octet] stringToOctets = map (fromIntegral . fromEnum)@@ -593,29 +623,35 @@ ------------------ Proxy Stuff ----------------------------------- ------------------------------------------------------------------ --- | Specifies if a proxy should be used for the request.-data Proxy = NoProxy -- ^ Don't use a proxy.-           | Proxy String (Maybe Authority) -- ^ Use the proxy given. Should be of the form "http:\/\/host:port", "host", "host:port", or "http:\/\/host"-+-- | @Proxy@ specifies if a proxy should be used for the request.+data Proxy + = NoProxy                 -- ^ Don't use a proxy.+ | Proxy String+         (Maybe Authority) -- ^ Use the proxy given. Should be of the form "http:\/\/host:port", "host", "host:port", or "http:\/\/host". Optional 'Authority' to authenticate with the proxy as.  ------------------------------------------------------------------ ------------------ Browser State Actions ------------------------- ------------------------------------------------------------------  +-- | @BrowserState@ is the (large) record type tracking the current+-- settings of the browser. 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-      , bsEvent          :: Maybe (BrowserEvent connection -> BrowserAction connection ())-      , bsRequestID      :: RequestID+ = 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+      , bsMaxRedirects    :: Maybe Int+      , bsMaxErrorRetries :: Maybe Int+      , bsMaxAuthAttempts :: Maybe Int+      , bsConnectionPool  :: [connection]+      , bsProxy           :: Proxy+      , bsDebug           :: Maybe String+      , bsEvent           :: Maybe (BrowserEvent connection -> BrowserAction connection ())+      , bsRequestID       :: RequestID       }  instance Show (BrowserState t) where@@ -624,23 +660,25 @@            {- ++ show (bsAuthorities bs) ++ "\n"-}             ++ "AllowRedirects: " ++ shows (bsAllowRedirects bs) "} ") ---- Simple DIY stateful behaviour, with IO+-- | @BrowserAction@ is the IO monad, but carrying along a 'BrowserState'. data BrowserAction conn a   = BA { lift :: BrowserState conn -> IO (BrowserState conn,a) }  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))+    fail x   =  BA (\_ -> fail x)  instance Functor (BrowserAction conn) where     fmap f   = liftM f --- | Apply a browser action to a state.+-- | @browse act@ is the toplevel action to perform a 'BrowserAction'.+-- Example use: @browse (request (getRequest yourURL))@. browse :: BrowserAction conn a -> IO a browse act = do x <- lift act defaultBrowserState                 return (snd x) +-- | Default browser state.. defaultBrowserState :: BrowserState t defaultBrowserState = res  where@@ -649,11 +687,16 @@      , bsOut              = putStrLn      , bsCookies          = []      , bsCookieFilter     = defaultCookieFilter-     , bsAuthorityGen     = \ _uri _realm -> -          (bsErr res) ("No action for prompting/generating user+password credentials provided (use: setAuthorityGen); returning Nothing") >> return Nothing+     , bsAuthorityGen     = \ _uri _realm -> do+          bsErr res "No action for prompting/generating user+password credentials \+                     \ provided (use: setAuthorityGen); returning Nothing"+          return Nothing      , bsAuthorities      = []      , bsAllowRedirects   = True      , bsAllowBasicAuth   = False+     , bsMaxRedirects     = Nothing+     , bsMaxErrorRetries  = Nothing+     , bsMaxAuthAttempts  = Nothing      , bsConnectionPool   = []      , bsProxy            = NoProxy      , bsDebug            = Nothing @@ -668,9 +711,12 @@ getBS :: (BrowserState t -> a) -> BrowserAction t a getBS f = BA (\b -> return (b,f b)) +-- | @getBrowserState@ returns the current browser config. Useful+-- for restoring state across 'BrowserAction's. getBrowserState :: BrowserAction t (BrowserState t) getBrowserState = getBS id +-- | @withBrowserAction st act@ performs @act@ with 'BrowserState' @st@. withBrowserState :: BrowserState t -> BrowserAction t a -> BrowserAction t a withBrowserState bs act = BA $ \ _ -> lift act bs @@ -684,46 +730,76 @@   alterBS updReqID   act --- | Do an io action+-- | Lifts an IO action into the 'BrowserAction' monad. ioAction :: IO a -> BrowserAction t a ioAction a = BA (\b -> a >>= \v -> return (b,v)) --- Stream handlers-setErrHandler, setOutHandler :: (String -> IO ()) -> BrowserAction t ()+-- | @setErrHandler@ sets the IO action to call when+-- the browser reports running errors. To disable any+-- such, set it to @const (return ())@.+setErrHandler :: (String -> IO ()) -> BrowserAction t () setErrHandler h = alterBS (\b -> b { bsErr=h })++-- | @setErrHandler@ sets the IO action to call when+-- the browser chatters info on its running. To disable any+-- such, set it to @const (return ())@.+setOutHandler :: (String -> IO ()) -> BrowserAction t () setOutHandler h = alterBS (\b -> b { bsOut=h })  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 onOff@ toggles the willingness to+-- follow redirects (HTTP responses with 3xx status codes). setAllowRedirects :: Bool -> BrowserAction t () setAllowRedirects bl = alterBS (\b -> b {bsAllowRedirects=bl}) +-- | @getAllowRedirects@ returns current setting of the do-chase-redirects flag. getAllowRedirects :: BrowserAction t Bool getAllowRedirects = getBS bsAllowRedirects +-- | @setMaxRedirects maxCount@ sets the maxiumum number of forwarding hops+-- we are willing to jump through. A no-op if the count is negative; if zero,+-- the max is set to whatever default applies. Notice that setting the max+-- redirects count does /not/ enable following of redirects itself; use+-- 'setAllowRedirects' to do so.+setMaxRedirects :: Maybe Int -> BrowserAction t ()+setMaxRedirects c + | fromMaybe 0 c < 0  = return ()+ | otherwise          = alterBS (\b -> b{bsMaxRedirects=c}) --- Proxy+-- | @getMaxRedirects@ returns the current setting for the max-redirect count.+-- If @Nothing@, the "Network.Browser"'s default is used.+getMaxRedirects :: BrowserAction t (Maybe Int)+getMaxRedirects = getBS bsMaxRedirects++-- | @setProxy p@ will disable proxy usage if @p@ is @NoProxy@.+-- If @p@ is @Proxy proxyURL mbAuth@, then @proxyURL@ is interpreted+-- as the URL of the proxy to use, possibly authenticating via +-- 'Authority' information in @mbAuth@. setProxy :: Proxy -> BrowserAction t () setProxy p = alterBS (\b -> b {bsProxy = p}) +-- | @getProxy@ returns the current proxy settings. getProxy :: BrowserAction t Proxy getProxy = getBS bsProxy ---- Debug+-- | @setDebugLog mbFile@ turns off debug logging iff @mbFile@+-- is @Nothing@. If set to @Just fStem@, logs of browser activity+-- is appended to files of the form @fStem-url-authority@, i.e.,+-- @fStem@ is just the prefix for a set of log files, one per host/authority. setDebugLog :: Maybe String -> BrowserAction t () setDebugLog v = alterBS (\b -> b {bsDebug=v}) ---- Page control+-- | @RequestState@ is an internal tallying type keeping track of various +-- per-connection counters, like the number of authorization attempts and +-- forwards we've gone through. 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+      , reqRetries    :: Int   -- ^ number of retries so far       , reqStopOnDeny :: Bool  -- ^ whether to pre-empt 401 response       } @@ -737,9 +813,10 @@       , reqStopOnDeny = True       } --- | 'BrowserEvent' is the event record type that a user-defined handler, set+-- | @BrowserEvent@ is the event record type that a user-defined handler, set -- via 'setEventHandler', will be passed. It indicates various state changes--- in the processing of a given Request ID.+-- encountered in the processing of a given 'RequestID', along with timestamps+-- at which they occurred. data BrowserEvent ty  = BrowserEvent       { browserTimestamp  :: ClockTime@@ -762,8 +839,13 @@  | ResponseEnd ResponseData  | ResponseFinish  -setEventHandler :: (BrowserEvent ty -> BrowserAction ty ()) -> BrowserAction ty ()-setEventHandler h = alterBS (\b -> b { bsEvent=Just h})+-- | @setEventHandler onBrowserEvent@ configures event handling.+-- If @onBrowserEvent@ is @Nothing@, event handling is turned off;+-- setting it to @Just onEv@ causes the @onEv@ IO action to be+-- notified of browser events during the processing of a request+-- by the Browser pipeline.+setEventHandler :: Maybe (BrowserEvent ty -> BrowserAction ty ()) -> BrowserAction ty ()+setEventHandler mbH = alterBS (\b -> b { bsEvent=mbH})  buildBrowserEvent :: BrowserEventType t -> {-URI-}String -> RequestID -> IO (BrowserEvent t) buildBrowserEvent bt uri reqID = do@@ -784,34 +866,57 @@        evt <- ioAction $ buildBrowserEvent bt uri (bsRequestID st)        evH evt -- if it fails, we fail. --- limits we are willing to not go beyond for method retries and number of auth deny responses.-maxRetries :: Int-maxRetries = 4+-- | The default number of hops we are willing not to go beyond for +-- request forwardings.+defaultMaxRetries :: Int+defaultMaxRetries = 4 -maxDenies :: Int-maxDenies = 2+-- | The default number of error retries we are willing to perform.+defaultMaxErrorRetries :: Int+defaultMaxErrorRetries = 4 --- Surely the most important bit:++-- | The default maximum HTTP Authentication attempts we will make for+-- a single request.+defaultMaxAuthAttempts :: Int+defaultMaxAuthAttempts = 2++-- | @request httpRequest@ tries to submit the 'Request' @httpRequest@+-- to some HTTP server (possibly going via a /proxy/, see 'setProxy'.)+-- Upon successful delivery, the URL where the response was fetched from+-- is returned along with the 'Response' itself. request :: HStream ty         => Request ty 	-> BrowserAction (HandleStream ty) (URI,Response ty) request req = newRequest $ do                  res <- request' nullVal initialState req 		 reportEvent ResponseFinish (show (rqURI req))-		 return res+		 case res of+		   Left e  -> do+		      let errStr = ("Error raised during request handling: " ++ show e)+		      err errStr+		      fail errStr+		   Right r -> return r   where    initialState = nullRequestState    nullVal      = buf_empty bufferOps +-- | Internal helper function, carrying along per-request counts. request' :: HStream ty          => ty 	 -> RequestState 	 -> Request ty-	 -> BrowserAction (HandleStream ty) (URI,Response ty)+	 -> BrowserAction (HandleStream ty) (Result (URI,Response ty)) request' nullVal rqState rq = do-     -- add cookies to request    let uri = rqURI rq-   cookies <- getCookiesFor (uriAuthToString $ reqURIAuth rq) (uriPath uri)+   let uria = reqURIAuth rq +     -- add cookies to request+   cookies <- getCookiesFor (uriAuthToString uria) (uriPath uri)+{- Not for now:+   (case uriUserInfo uria of+     "" -> id+     xs -> case break (==':') xs of { (as,_:bs) -> withAuth AuthBasic{auUsername=as,auPassword=bs,auRealm="/",auSite=uri} ; _ -> id}) $ do+-}    when (not $ null cookies)          (out $ "Adding cookies to request.  Cookie names: "  ++                foldl spaceappend "" (map ckName cookies))@@ -826,19 +931,23 @@          Just x  -> return (insertHeader HdrAuthorization (withAuthority x rq) rq)    let rq'' = insertHeaders (map cookieToHeader cookies) rq'    p <- getProxy-   let rq_to_go = normalizeRequestURI False{-no close-}-                                      (uriToAuthorityString $ rqURI rq'')-				      rq''-   out ("Sending:\n" ++ show rq_to_go) +   let defaultOpts = +         case p of +	   NoProxy     -> defaultNormalizeRequestOptions+	   Proxy _ ath ->+	      defaultNormalizeRequestOptions+	        { normForProxy=True+		, normCustoms = +		    maybe []+		          (\ authS -> [\ _ r -> insertHeader HdrProxyAuthorization (withAuthority authS r) r])+			  ath+		}+   let final_req = normalizeRequest defaultOpts rq''+   out ("Sending:\n" ++ show final_req)    e_rsp <-       case p of-       NoProxy       -> dorequest (reqURIAuth rq'') rq_to_go-       Proxy str ath -> do-            -- note: don't split off the authority for proxies..-          let rq_to_go' = maybe rq''-	                    (\x -> insertHeader HdrProxyAuthorization-			                        (withAuthority x rq'') rq'')-		           ath+       NoProxy        -> dorequest (reqURIAuth rq'') final_req+       Proxy str _ath -> do           let notURI  	       | null pt || null hst =  	         URIAuth{ uriUserInfo = ""@@ -860,12 +969,15 @@                       (parseURI str)            out $ "proxy uri host: " ++ uriRegName proxyURIAuth ++ ", port: " ++ uriPort proxyURIAuth-          dorequest proxyURIAuth rq_to_go'+          dorequest proxyURIAuth final_req+   mbMx <- getMaxErrorRetries    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)+     | (reqRetries rqState < fromMaybe defaultMaxErrorRetries mbMx) && +       (v == ErrorReset || v == ErrorClosed) ->+       request' nullVal rqState{reqRetries=succ (reqRetries rqState)} rq+     | otherwise -> +       return (Left v)     Right rsp -> do       out ("Received:\n" ++ show rsp)       -- add new cookies to browser state@@ -875,79 +987,88 @@      when (not $ null newcookies)           (out $ foldl (\x y -> x ++ "\n  " ++ show y) "Cookies received:" newcookies)                -     filterfn <- getCookieFilter+     filterfn    <- getCookieFilter      newcookies' <- ioAction (filterM (filterfn uri) newcookies)-     foldM (\_ -> addCookie) () newcookies'+     mapM_ addCookie newcookies'       when (not $ null newcookies)           (out $ "Accepting cookies with names: " ++ foldl spaceappend "" (map ckName newcookies'))        +     mbMxAuths <- getMaxAuthAttempts      case rspCode rsp of       (4,0,1) -- Credentials not sent or refused.-        | reqDenies rqState > maxDenies -> do+        | reqDenies rqState > fromMaybe defaultMaxAuthAttempts mbMxAuths -> do           out "401 - credentials again refused; exceeded retry count (2)"-	  return (uri,rsp)+	  return (Right (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 -}+            Nothing -> return (Right (uri,rsp))   {- do nothing -}             Just x  -> do               au <- challengeToAuthority uri x               case au of-                Nothing  -> return (uri,rsp)   {- do nothing -}+                Nothing  -> return (Right (uri,rsp)) {- do nothing -}                 Just au' -> do                   out "Retrying request with new credentials" 		  request' nullVal-			   rqState{reqDenies=reqDenies rqState + 1, reqStopOnDeny=False}+			   rqState{ reqDenies     = succ(reqDenies rqState)+			          , reqStopOnDeny = False+				  }                            (insertHeader HdrAuthorization (withAuthority au' rq) rq)        (4,0,7)  -- Proxy Authentication required-        | reqDenies rqState > maxDenies -> do+        | reqDenies rqState > fromMaybe defaultMaxAuthAttempts mbMxAuths -> do           out "407 - proxy authentication required; max deny count exceeeded (2)"-          return (uri,rsp)+          return (Right (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 -}+            Nothing -> return (Right (uri,rsp))   {- do nothing -}             Just x  -> do               au <- challengeToAuthority uri x               case au of-               Nothing  -> return (uri,rsp)   {- do nothing -}+               Nothing  -> return (Right (uri,rsp))  {- do nothing -}                Just au' -> do                  pxy <- getBS bsProxy                  case pxy of                    NoProxy -> do                      err "Proxy authentication required without proxy!"-                     return (uri,rsp)+                     return (Right (uri,rsp))                    Proxy px _ -> do                      out "Retrying with proxy authentication"                      setProxy (Proxy px (Just au'))                      request' nullVal-			      rqState{reqDenies=reqDenies rqState + 1, reqStopOnDeny=False}+			      rqState{ reqDenies     = succ(reqDenies rqState)+			             , reqStopOnDeny = False+				     } 			      rq        (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)+	mbMxRetries <- getMaxRedirects+        if not rd || reqRedirects rqState > fromMaybe defaultMaxRetries mbMxRetries+	 then return (Right (uri,rsp)) 	 else            case retrieveHeaders HdrLocation rsp of            [] -> do -	     err "No Location header in redirect response"-             return (uri,rsp)+	     err "No Location: header in redirect response"+             return (Right (uri,rsp))            (Header _ u:_) ->  	     case parseURIReference u of                Nothing -> do-                 err ("Parse of Location header in a redirect response failed: " ++ u)-                 return (uri,rsp)+                 err ("Parse of Location: header in a redirect response failed: " ++ u)+                 return (Right (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}+			  rqState{ reqDenies     = 0+			         , reqRedirects  = succ(reqRedirects rqState)+				 , reqStopOnDeny = True+				 }                           (replaceHeader HdrContentLength "0" rq1)                 where                   newuri' = maybe newuri id (newuri `relativeTo` uri)@@ -956,50 +1077,54 @@         case retrieveHeaders HdrLocation rsp of          [] -> do  	   err "No Location header in proxy redirect response."-           return (uri,rsp)+           return (Right (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)+             return (Right (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+             request' nullVal rqState{ reqDenies     = 0+	                             , reqRedirects  = 0+				     , reqRetries    = succ (reqRetries rqState)+				     , reqStopOnDeny = True 				     } 				     rq       (3,_,_) ->  redirect uri rsp-      _       -> return (uri,rsp)+      _       -> return (Right (uri,rsp))     where            redirect uri rsp = do-       rd <- getAllowRedirects-       if not rd || reqRedirects rqState > maxRetries-        then return (uri,rsp) +       rd   <- getAllowRedirects+       mbMxRetries <- getMaxRedirects+       if not rd || reqRedirects rqState > fromMaybe defaultMaxRetries mbMxRetries+        then return (Right (uri,rsp)) 	else do          case retrieveHeaders HdrLocation rsp of           [] -> do  	    err "No Location header in redirect response."-            return (uri,rsp)+            return (Right (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}+		         rqState{ reqDenies     = 0+			        , reqRedirects  = succ (reqRedirects rqState)+			        , reqStopOnDeny = True+				} 		         rq{rqURI=newuri'}               Nothing -> do                 err ("Parse of Location header in a redirect response failed: " ++ u)-                return (uri,rsp)+                return (Right (uri,rsp))  spaceappend :: String -> String -> String spaceappend x y = x ++ ' ' : y -+-- | The internal request handling state machine. dorequest :: (HStream ty)           => URIAuth 	  -> Request ty@@ -1010,27 +1135,17 @@                ; conn <- ioAction $ filterM (\c -> c `isTCPConnectedTo` uriAuthToString hst) pool                ; rsp <- case conn of                     [] -> do { out ("Creating new connection to " ++ uriAuthToString hst)-                             ; let aport = case uriPort hst of-                                            (':':s) -> -					      case reads s of { ((v,_):_) -> v ; _ -> 80}-                                            _       -> 80+                             ; let uPort = uriAuthPort Nothing{-ToDo: feed in complete URL-} hst 		             ; reportEvent OpenConnection (show (rqURI rqst))-                             ; c <- ioAction $ openStream (uriRegName hst) aport-			     ; let len_pool = length pool-                             ; when (len_pool > 5)-                                    (ioAction $ close (last pool))-                             ; let pool' -			            | len_pool > 5 = init pool-				    | otherwise    = pool-                             ; alterBS (\b -> b { bsConnectionPool=c:pool' })-                             ; dorequest2 c rqst+                             ; c <- ioAction $ openStream (uriRegName hst) uPort+			     ; updateConnectionPool c+			     ; dorequest2 c rqst                              }                     (c:_) ->                         do { out ("Recovering connection to " ++ uriAuthToString hst) 			   ; reportEvent ReuseConnection (show (rqURI rqst))                            ; dorequest2 c rqst                            }-               ;  	       ; case rsp of { Right (Response a b c _) -> reportEvent (ResponseEnd (a,b,c)) (show (rqURI rqst)) ; _ -> return ()}                ; return rsp                }@@ -1053,44 +1168,23 @@ 	   sendHTTP_notify c' r onSendComplete  -reqURIAuth :: Request ty -> URIAuth-reqURIAuth req = -  case uriAuthority (rqURI req) of-    Just ua -> ua-    _ -> case lookupHeader HdrHost (rqHeaders req) of-           Nothing -> error ("reqURIAuth: no URI authority for: " ++ show req)-	   Just h  -> URIAuth { uriUserInfo = ""-	                      , uriRegName  = h-			      , uriPort     = ""-			      }--------------------------------------------------------------------------------------- Request Building ---------------------------------------------------------------------------------------------------libUA :: String-libUA = "hs-HTTP/4.0.3"--defaultGETRequest :: URI -> Request_String-defaultGETRequest uri = defaultGETRequest_ uri---defaultGETRequest_ :: BufferType a => URI -> Request a-defaultGETRequest_ uri = req- where-  empty = buf_empty (toBufOps req)-  -  req = -    Request { rqURI=uri-            , rqBody=empty-            , rqHeaders=[ Header HdrContentLength "0"-                        , Header HdrUserAgent libUA-                        ]-            , rqMethod=GET-            }--  toBufOps :: BufferType a => Request a -> BufferOp a-  toBufOps _ = bufferOps+updateConnectionPool :: HStream hTy+                     => HandleStream hTy+		     -> BrowserAction (HandleStream hTy) ()+updateConnectionPool c = do+   pool <- getBS bsConnectionPool+   let len_pool = length pool+   when (len_pool > maxPoolSize)+        (ioAction $ close (last pool))+   let pool' +	| len_pool > maxPoolSize = init pool+	| otherwise              = pool+   alterBS (\b -> b { bsConnectionPool=c:pool' })+   return ()+                             +-- | Maximum number of open connections we are willing to have active.+maxPoolSize :: Int+maxPoolSize = 5  -- This form junk is completely untested... @@ -1098,7 +1192,6 @@  data Form = Form RequestMethod URI [FormVar] - formToRequest :: Form -> Request_String formToRequest (Form m u vs) =     let enc = urlEncodeVars vs@@ -1115,3 +1208,26 @@                         , rqURI=u                         }         _ -> error ("unexpected request: " ++ show m)+++------------------------------------------------------------------+----------------------- Miscellaneous ----------------------------+------------------------------------------------------------------++-- | @uriDefaultTo a b@ returns a URI that is consistent with the first+-- argument URI @a@ when read in the context of the second URI @b@.+-- If the second argument is not sufficient context for determining+-- a full URI then anarchy reins.+uriDefaultTo :: URI -> URI -> URI+uriDefaultTo a b = maybe a id (a `relativeTo` b)+++word, quotedstring :: Parser String+quotedstring =+    do { char '"'  -- "+       ; str <- many (satisfy $ not . (=='"'))+       ; char '"'+       ; return str+       }++word = many1 (satisfy (\x -> isAlphaNum x || x=='_' || x=='.' || x=='-' || x==':'))
Network/BufferType.hs view
@@ -30,6 +30,8 @@ import System.IO ( Handle ) import Data.Word ( Word8 ) +import Network.HTTP.Utils ( crlf )+ -- | 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@@ -58,6 +60,7 @@      , buf_hGetLine     :: Handle -> IO a      , buf_empty        :: a      , buf_append       :: a -> a -> a+     , buf_concat       :: [a] -> a      , buf_fromStr      :: String -> a      , buf_toStr        :: a -> String      , buf_snoc         :: a -> Word8 -> a@@ -78,17 +81,18 @@       , buf_hPut         = Strict.hPut       , buf_hGetLine     = Strict.hGetLine       , buf_append       = Strict.append+      , buf_concat       = Strict.concat       , 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_isLineTerm   = \ b -> Strict.length b == 2 && p_crlf == b       , buf_isEmpty      = Strict.null        }    where-    crlf = Strict.pack "\r\n"+    p_crlf = Strict.pack crlf  lazyBufferOp :: BufferOp Lazy.ByteString lazyBufferOp = @@ -98,17 +102,18 @@       , buf_hPut         = Lazy.hPut       , buf_hGetLine     = \ h -> Strict.hGetLine h >>= \ l -> return (Lazy.fromChunks [l])       , buf_append       = Lazy.append+      , buf_concat       = Lazy.concat       , 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_isLineTerm   = \ b -> Lazy.length b == 2 && p_crlf == b       , buf_isEmpty      = Lazy.null        }    where-    crlf = Lazy.pack "\r\n"+    p_crlf = Lazy.pack crlf  stringBufferOp :: BufferOp String stringBufferOp =BufferOp @@ -117,6 +122,7 @@       , buf_hPut         = \ h s -> Strict.hPut h (Strict.pack s)       , buf_hGetLine     = \ h   -> Strict.hGetLine h >>= return . Strict.unpack       , buf_append       = (++)+      , buf_concat       = concat       , buf_fromStr      = id       , buf_toStr        = id       , buf_snoc         = \ a x -> a ++ [toEnum (fromIntegral x)]@@ -128,5 +134,4 @@       , buf_isLineTerm   = \ b -> b == crlf       , buf_isEmpty      = null        }-   where-    crlf = "\r\n"+
Network/HTTP.hs view
@@ -4,23 +4,164 @@ -- Copyright   :  (c) Warrick Gray 2002, Bjorn Bringert 2003-2005, 2007 Robin Bate Boerop -- License     :  BSD -- --- Maintainer  :  bjorn@bringert.net+-- Maintainer  :  Sigbjorn Finne <sigbjorn.finne@gmail.com> -- Stability   :  experimental -- Portability :  non-portable (not tested) ----- An easy HTTP interface, toplevel module.+-- The 'Network.HTTP' module provides a simple interface for sending and+-- receiving content over HTTP in Haskell. Here's how to fetch a document from+-- a URL and return it as a String: ----- The 'Network.HTTP' module provides functionality for sending--- HTTP requests and processing their responses, along with a supporting--- cast of types and utility functions.+-- >+-- >    simpleHTTP (getRequest "http://www.haskell.org/") >>= fmap (take 100) . getResponseBody+-- >        -- fetch document and return it (as a 'String'.) --+-- Other functions let you control the submission and transfer of HTTP+-- 'Request's and 'Response's more carefully, letting you integrate the use+-- of 'Network.HTTP' functionality into your application.+--+-- The module also exports the main types of the package, 'Request' and 'Response',+-- along with 'Header' and functions for working with these.+-- -- The actual functionality is implemented by modules in the @Network.HTTP.*@--- namespace, allowing the user to either use the default implementation --- by importing @Network.HTTP@ or, for more fine-grained control, selectively+-- namespace, letting you either use the default implementation here+-- by importing @Network.HTTP@ or, for more specific uses, selectively -- import the modules in @Network.HTTP.*@. To wit, more than one kind of -- representation of the bulk data that flows across a HTTP connection is --- supported. Now selectable by importing @Network.HTTP.HandleStream@ (say.)+-- supported. (see "Network.HTTP.HandleStream".) -- +-- /NOTE:/ The 'Request' send actions will normalize the @Request@ prior to transmission.+-- Normalization such as having the request path be in the expected form and, possibly,+-- introduce a default @Host:@ header if one isn't already present. If you do not +-- want the requests tampered with, but sent as-is, please import and use the+-- the "Network.HTTP.HandleStream" or "Network.HTTP.Stream" modules instead. They+-- export the same functions, but leaves construction and any normalization of +-- @Request@s to the user.+--+-----------------------------------------------------------------------------+module Network.HTTP +       ( module Network.HTTP.Base+       , module Network.HTTP.Headers++         {- the functionality that the implementation modules, +	    Network.HTTP.HandleStream and Network.HTTP.Stream,+	    exposes:+	 -}+       , simpleHTTP      -- :: Request -> IO (Result Response)+       , simpleHTTP_     -- :: Stream s => s -> Request -> IO (Result Response)+       , sendHTTP        -- :: Stream s => s -> Request -> IO (Result Response)+       , sendHTTP_notify -- :: Stream s => s -> Request -> IO () -> IO (Result Response)+       , receiveHTTP     -- :: Stream s => s -> IO (Result Request)+       , respondHTTP     -- :: Stream s => s -> Response -> IO ()++       , module Network.TCP+       +       , getRequest      -- :: String -> Request_String+       , postRequest     -- :: String -> Request_String+       +       , getResponseBody -- :: Requesty ty -> ty+       ) where++-----------------------------------------------------------------+------------------ Imports --------------------------------------+-----------------------------------------------------------------++import Network.HTTP.Headers+import Network.HTTP.Base+import qualified Network.HTTP.HandleStream as S+-- old implementation: import Network.HTTP.Stream+import Network.TCP+import Network.Stream ( Result )+import Network.URI    ( parseURI )++import Data.Maybe ( fromMaybe )++{-+ Note: if you switch over/back to using Network.HTTP.Stream here, you'll+ have to wrap the results from 'openStream' as Connections via 'hstreamToConnection'+ prior to delegating to the Network.HTTP.Stream functions.+-}++-- | @simpleHTTP req@ transmits the 'Request' @req@ by opening a /direct/, non-persistent+-- connection to the HTTP server that @req@ is destined for, followed by transmitting+-- it and gathering up the response as a 'Result'. Prior to sending the request,+-- it is normalized (via 'normalizeRequest'). If you have to mediate the request+-- via an HTTP proxy, you will have to normalize the request yourself. Or switch to+-- using 'Network.Browser' instead.+--+-- Examples:+--+-- > simpleHTTP (getRequest "http://hackage.haskell.org/")+-- > simpleHTTP (getRequest "http://hackage.haskell.org:8012/")++simpleHTTP :: (HStream ty) => Request ty -> IO (Result (Response ty))+simpleHTTP r = do+  auth <- getAuth r+  c <- openStream (host auth) (fromMaybe 80 (port auth))+  let norm_r = normalizeRequest defaultNormalizeRequestOptions{normDoClose=True} r+  simpleHTTP_ c norm_r+   +-- | Identical to 'simpleHTTP', but acting on an already opened stream.+simpleHTTP_ :: HStream ty => HandleStream ty -> Request ty -> IO (Result (Response ty))+simpleHTTP_ s r = do +  let norm_r = normalizeRequest defaultNormalizeRequestOptions{normDoClose=True} r+  S.sendHTTP s norm_r++-- | @sendHTTP hStream httpRequest@ transmits @httpRequest@ (after normalization) over+-- @hStream@, but does not alter the status of the connection, nor request it to be+-- closed upon receiving the response.+sendHTTP :: HStream ty => HandleStream ty -> Request ty -> IO (Result (Response ty))+sendHTTP conn rq = do+  let norm_r = normalizeRequest defaultNormalizeRequestOptions rq +  S.sendHTTP conn norm_r++-- | @sendHTTP_notify hStream httpRequest action@ behaves like 'sendHTTP', but+-- lets you supply an IO @action@ to execute once the request has been successfully+-- transmitted over the connection. Useful when you want to set up tracing of+-- request transmission and its performance.+sendHTTP_notify :: HStream ty+                => HandleStream ty+		-> Request ty+		-> IO ()+		-> IO (Result (Response ty))+sendHTTP_notify conn rq onSendComplete = do+  let norm_r = normalizeRequest defaultNormalizeRequestOptions rq +  S.sendHTTP_notify conn norm_r onSendComplete++-- | @receiveHTTP hStream@ reads a 'Request' from the 'HandleStream' @hStream@+receiveHTTP :: HStream ty => HandleStream ty -> IO (Result (Request ty))+receiveHTTP conn = S.receiveHTTP conn++-- | @respondHTTP hStream httpResponse@ transmits an HTTP 'Response' over+-- the 'HandleStream' @hStream@. It could be used to implement simple web+-- server interactions, performing the dual role to 'sendHTTP'.+respondHTTP :: HStream ty => HandleStream ty -> Response ty -> IO ()+respondHTTP conn rsp = S.respondHTTP conn rsp++-- | @getRequest urlString@ is convenience constructor for basic GET 'Request's. If+-- @urlString@ isn't a syntactically valid URL, the function raises an error.+getRequest :: String -> Request_String+getRequest urlString = +  case parseURI urlString of+    Nothing -> error ("getRequest: Not a valid URL - " ++ urlString)+    Just u  -> mkRequest GET u++-- | @postRequest urlString@ is convenience constructor for POST 'Request's. If+-- @urlString@ isn\'t a syntactically valid URL, the function raises an error.+postRequest :: String -> Request_String+postRequest urlString = +  case parseURI urlString of+    Nothing -> error ("postRequest: Not a valid URL - " ++ urlString)+    Just u  -> mkRequest POST u++-- | @getResponseBody response@ takes the response of a HTTP requesting action and+-- tries to extricate the body of the 'Response' @response@. If the request action+-- returned an error, an IO exception is raised.+getResponseBody :: Result (Response ty) -> IO ty+getResponseBody (Left err) = fail (show err)+getResponseBody (Right r)  = return (rspBody r)++-- -- * TODO --     - request pipelining --     - https upgrade (includes full TLS, i.e. SSL, implementation)@@ -70,34 +211,3 @@ --             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 -       ( module Network.HTTP.HandleStream-       , module Network.HTTP.Base-       , module Network.HTTP.Headers--{- the functionality that Network.HTTP.HandleStream and Network.HTTP.Stream-   exposes:-       , simpleHTTP      -- :: Request -> IO (Result Response)-       , simpleHTTP_     -- :: Stream s => s -> Request -> IO (Result Response)-       , sendHTTP        -- :: Stream s => s -> Request -> IO (Result Response)-       , sendHTTP_notify -- :: Stream s => s -> Request -> IO () -> IO (Result Response)-       , receiveHTTP     -- :: Stream s => s -> IO (Result Request)-       , respondHTTP     -- :: Stream s => s -> Response -> IO ()--}-       , module Network.TCP-       ) where-------------------------------------------------------------------------------------- Imports ----------------------------------------------------------------------------------------------------------import Network.HTTP.Headers-import Network.HTTP.Base---import Network.HTTP.Stream-import Network.HTTP.HandleStream-import Network.TCP---
Network/HTTP/Base.hs view
@@ -8,13 +8,19 @@ -- Stability   :  experimental -- Portability :  non-portable (not tested) ----- An easy HTTP interface; base types.+-- Definitions of @Request@ and @Response@ types along with functions+-- for normalizing them. It is assumed to be an internal module; user+-- code should, if possible, import @Network.HTTP@ to access the functionality+-- that this module provides. --+-- Additionally, the module exports internal functions for working with URLs,+-- and for handling the processing of requests and responses coming back.+-- ----------------------------------------------------------------------------- module Network.HTTP.Base        (           -- ** Constants-         httpVersion+         httpVersion                 -- :: String            -- ** HTTP        , Request(..)@@ -36,18 +42,28 @@        , parseURIAuthority                   -- internal-       , crlf-       , sp-       , uriToAuthorityString   -- :: URI    -> String+       , uriToAuthorityString   -- :: URI     -> String        , uriAuthToString        -- :: URIAuth -> String-       , parseResponseHead-       , parseRequestHead+       , uriAuthPort            -- :: Maybe URI -> URIAuth -> Int+       , reqURIAuth             -- :: Request ty -> URIAuth++       , parseResponseHead      -- :: [String] -> Result ResponseData+       , parseRequestHead       -- :: [String] -> Result RequestData+        , ResponseNextStep(..)        , matchResponse        , ResponseData        , ResponseCode        , RequestData        +       , NormalizeRequestOptions(..) +       , defaultNormalizeRequestOptions -- :: NormalizeRequestOptions ty+       , RequestNormalizer++       , normalizeRequest   -- :: NormalizeRequestOptions ty -> Request ty -> Request ty++       , splitRequestURI+        , getAuth        , normalizeRequestURI        , normalizeHostHeader@@ -61,28 +77,42 @@        , readTillEmpty1        , readTillEmpty2        +       , defaultGETRequest+       , defaultGETRequest_+       , mkRequest++       , defaultUserAgent+       , libUA  {- backwards compatibility, will disappear..soon -}+               , catchIO        , catchIO_        , responseParseError+       +       , getRequestVersion+       , getResponseVersion+       , setRequestVersion+       , setResponseVersion+               ) where  import Network.URI    ( URI(uriAuthority, uriPath, uriScheme)-   , URIAuth(uriUserInfo, uriRegName, uriPort)+   , URIAuth(URIAuth, uriUserInfo, uriRegName, uriPort)    , parseURIReference    )  import Control.Monad ( guard )-import Control.Monad.Error-import Data.Char     ( ord, digitToInt, intToDigit, toLower )-import Data.List     ( partition )-import Data.Maybe    ( listToMaybe )-import Numeric       ( showHex, readHex )+import Control.Monad.Error ()+import Data.Char     ( digitToInt, intToDigit, toLower, isDigit,+                       isAscii, isAlphaNum )+import Data.List     ( partition, find )+import Data.Maybe    ( listToMaybe, fromMaybe )+import Numeric       ( readHex )  import Network.Stream-import Network.BufferType ( BufferOp(..) )+import Network.BufferType ( BufferOp(..), BufferType(..) ) import Network.HTTP.Headers-import Network.HTTP.Utils ( trim )+import Network.HTTP.Utils ( trim, crlf, sp, readsOne )  import Text.Read.Lex (readDecP) import Text.ParserCombinators.ReadP@@ -143,6 +173,43 @@ 	 , uriPort ua 	 ] +uriAuthPort :: Maybe URI -> URIAuth -> Int+uriAuthPort mbURI u = +  case uriPort u of+    (':':s) -> readsOne id (default_port mbURI) s+    _       -> default_port mbURI+ where+  default_port Nothing = default_http+  default_port (Just url) = +    case map toLower $ uriScheme url of+      "http:" -> default_http+      "https:" -> default_https+        -- todo: refine+      _ -> default_http++  default_http  = 80+  default_https = 443++-- Fish out the authority from a possibly normalized Request, i.e.,+-- the information may either be in the request's URI or inside+-- the Host: header.+reqURIAuth :: Request ty -> URIAuth+reqURIAuth req = +  case uriAuthority (rqURI req) of+    Just ua -> ua+    _ -> case lookupHeader HdrHost (rqHeaders req) of+           Nothing -> error ("reqURIAuth: no URI authority for: " ++ show req)+	   Just h  -> +	      case toHostPort h of+	        (ht,p) -> URIAuth { uriUserInfo = ""+	                          , uriRegName  = ht+			          , uriPort     = p+			          }+  where+    -- Note: just in case you're wondering..the convention is to include the ':'+    -- in the port part..+   toHostPort h = break (==':') h+ ----------------------------------------------------------------- ------------------ HTTP Messages -------------------------------- -----------------------------------------------------------------@@ -156,9 +223,22 @@ -- | 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)+data RequestMethod = HEAD | PUT | GET | POST | DELETE | OPTIONS | TRACE | CONNECT | Custom String+    deriving(Eq) +instance Show RequestMethod where+  show x = +    case x of+      HEAD     -> "HEAD"+      PUT      -> "PUT"+      GET      -> "GET"+      POST     -> "POST"+      DELETE   -> "DELETE"+      OPTIONS  -> "OPTIONS"+      TRACE    -> "TRACE"+      CONNECT  -> "CONNECT"+      Custom c -> c+ rqMethodMap :: [(String, RequestMethod)] rqMethodMap = [("HEAD",    HEAD), 	       ("PUT",     PUT),@@ -166,7 +246,8 @@ 	       ("POST",    POST),                ("DELETE",  DELETE), 	       ("OPTIONS", OPTIONS),-	       ("TRACE",   TRACE)]+	       ("TRACE",   TRACE),+	       ("CONNECT", CONNECT)]  --  -- for backwards-ish compatibility; suggest@@ -190,26 +271,21 @@                                     --  2) transparent support for both relative                                     --     & absolute uris, although this should                                     --     already work (leave scheme & host parts empty).-             , rqMethod    :: RequestMethod             +             , 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 (Request a) where-    show (Request u m h _) =-        show m ++ sp ++ alt_uri ++ sp ++ httpVersion ++ crlf-        ++ foldr (++) [] (map show h) ++ crlf+    show req@(Request u m h _) =+        show m ++ sp ++ alt_uri ++ sp ++ ver ++ crlf+        ++ foldr (++) [] (map show (dropHttpVersion h)) ++ crlf         where+	    ver = fromMaybe httpVersion (getRequestVersion req)             alt_uri = show $ if null (uriPath u) || head (uriPath u) /= '/'                          then u { uriPath = '/' : uriPath u }                          else u@@ -246,14 +322,69 @@ -- 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 a) where-    show (Response (a,b,c) reason headers _) =-        httpVersion ++ ' ' : map intToDigit [a,b,c] ++ ' ' : reason ++ crlf-        ++ foldr (++) [] (map show headers) ++ crlf+    show rsp@(Response (a,b,c) reason headers _) =+        ver ++ ' ' : map intToDigit [a,b,c] ++ ' ' : reason ++ crlf+        ++ foldr (++) [] (map show (dropHttpVersion headers)) ++ crlf+     where+      ver = fromMaybe httpVersion (getResponseVersion rsp)  instance HasHeaders (Response a) where     getHeaders = rspHeaders     setHeaders rsp hdrs = rsp { rspHeaders=hdrs } ++------------------------------------------------------------------+------------------ Request Building ------------------------------+------------------------------------------------------------------+libUA :: String+libUA = "hs-HTTP-4000.0.5"++defaultUserAgent :: String+defaultUserAgent = libUA++defaultGETRequest :: URI -> Request_String+defaultGETRequest uri = defaultGETRequest_ uri++defaultGETRequest_ :: BufferType a => URI -> Request a+defaultGETRequest_ uri = mkRequest GET uri ++-- | 'mkRequest method uri' constructs a well formed+-- request for the given HTTP method and URI. It does not+-- normalize the URI for the request _nor_ add the required +-- Host: header. That is done either explicitly by the user+-- or when requests are normalized prior to transmission.+mkRequest :: BufferType ty => RequestMethod -> URI -> Request ty+mkRequest meth uri = req+ where+  req = +    Request { rqURI      = uri+            , rqBody     = empty+            , rqHeaders  = [ Header HdrContentLength "0"+                           , Header HdrUserAgent     defaultUserAgent+                           ]+            , rqMethod   = meth+            }++  empty = buf_empty (toBufOps req)++{-+    -- stub out the user info.+  updAuth = fmap (\ x -> x{uriUserInfo=""}) (uriAuthority uri)++  withHost = +    case uriToAuthorityString uri{uriAuthority=updAuth} of+      "" -> id+      h  -> ((Header HdrHost h):)++  uri_req +   | forProxy  = uri+   | otherwise = snd (splitRequestURI uri)+-}+++toBufOps :: BufferType a => Request a -> BufferOp a+toBufOps _ = bufferOps+ ----------------------------------------------------------------- ------------------ Parsing -------------------------------------- -----------------------------------------------------------------@@ -262,13 +393,17 @@ parseRequestHead :: [String] -> Result RequestData parseRequestHead         [] = Left ErrorClosed parseRequestHead (com:hdrs) = do-  (_version,rqm,uri) <- requestCommand com (words com)+  (version,rqm,uri) <- requestCommand com (words com)   hdrs'              <- parseHeaders hdrs-  return (rqm,uri,hdrs')+  return (rqm,uri,withVer version hdrs')  where+  withVer [] hs = hs+  withVer (h:_) hs = withVersion h hs+   requestCommand l _yes@(rqm:uri:version) =     case (parseURIReference uri, lookup rqm rqMethodMap) of      (Just u, Just r) -> return (version,r,u)+     (Just u, Nothing) -> return (version,Custom rqm,u)      _                -> parse_err l   requestCommand l _    | null l    = failWith ErrorClosed@@ -281,9 +416,9 @@ parseResponseHead :: [String] -> Result ResponseData parseResponseHead []         = failWith ErrorClosed parseResponseHead (sts:hdrs) = do-  (_version,code,reason) <- responseStatus sts (words sts)+  (version,code,reason)  <- responseStatus sts (words sts)   hdrs'                  <- parseHeaders hdrs-  return (code,reason,hdrs')+  return (code,reason, withVersion version hdrs')  where   responseStatus _l _yes@(version:code:reason) =     return (version,match code,concatMap (++" ") reason)@@ -301,9 +436,69 @@                    digitToInt c)   match _ = (-1,-1,-1)  -- will create appropriate behaviour +-- To avoid changing the @RequestData@ and @ResponseData@ types+-- just for this (and the upstream backwards compat. woes that+-- will result in), encode version info as a custom header.+-- Used by 'parseResponseData' and 'parseRequestData'.+--+-- Note: the Request and Response types do not currently represent+-- the version info explicitly in their record types. You have to use+-- {get,set}{Request,Response}Version for that.+withVersion :: String -> [Header] -> [Header]+withVersion v hs + | v == httpVersion = hs  -- don't bother adding it if the default.+ | otherwise        = (Header (HdrCustom "X-HTTP-Version") v) : hs -        +-- | @getRequestVersion req@ returns the HTTP protocol version of+-- the request @req@. If @Nothing@, the default 'httpVersion' can be assumed.+getRequestVersion :: Request a -> Maybe String+getRequestVersion r = getHttpVersion r +-- | @setRequestVersion v req@ returns a new request, identical to+-- @req@, but with its HTTP version set to @v@.+setRequestVersion :: String -> Request a -> Request a+setRequestVersion s r = setHttpVersion r s+++-- | @getResponseVersion rsp@ returns the HTTP protocol version of+-- the response @rsp@. If @Nothing@, the default 'httpVersion' can be +-- assumed.+getResponseVersion :: Response a -> Maybe String+getResponseVersion r = getHttpVersion r++-- | @setResponseVersion v rsp@ returns a new response, identical to+-- @rsp@, but with its HTTP version set to @v@.+setResponseVersion :: String -> Response a -> Response a+setResponseVersion s r = setHttpVersion r s++-- internal functions for accessing HTTP-version info in+-- requests and responses. Not exported as it exposes ho+-- version info is represented internally.++getHttpVersion :: HasHeaders a => a -> Maybe String+getHttpVersion r = +  fmap toVersion      $+   find isHttpVersion $+    getHeaders r+ where+  toVersion (Header _ x) = x++setHttpVersion :: HasHeaders a => a -> String -> a+setHttpVersion r v = +  setHeaders r $+   withVersion v  $+    dropHttpVersion $+     getHeaders r++dropHttpVersion :: [Header] -> [Header]+dropHttpVersion hs = filter (not.isHttpVersion) hs++isHttpVersion :: Header -> Bool+isHttpVersion (Header (HdrCustom "X-HTTP-Version") _) = True+isHttpVersion _ = False    +++ ----------------------------------------------------------------- ------------------ HTTP Send / Recv ---------------------------------- -----------------------------------------------------------------@@ -360,34 +555,33 @@     Escape method: char -> '%' a b  where a, b :: Hex digits -} -urlEncode, urlDecode :: String -> String-+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 [] = []-            +urlEncode :: String -> String+urlEncode     [] = []+urlEncode (ch:t) +  | (isAscii ch && isAlphaNum ch) || ch `elem` "-_.~" = ch : urlEncode t+  | not (isAscii ch) = foldr escape (urlEncode t) (eightBs [] (fromEnum ch))+  | otherwise = escape (fromEnum ch) (urlEncode t)+    where+     escape b rs = '%':showH (b `div` 16) (showH (b `mod` 16) rs)+     +     showH x xs+       | x <= 9    = toEnum (o_0 + x) : xs+       | otherwise = toEnum (o_A + (x-10)) : xs+      where+       o_0 = fromEnum '0'+       o_A = fromEnum 'A' +     eightBs :: [Int]  -> Int -> [Int]+     eightBs acc x+      | x <= 0xff = (x:acc)+      | otherwise = eightBs ((x `mod` 256) : acc) (x `div` 256)  -- Encode form variables, useable in either the -- query part of a URI, or the body of a POST request.@@ -411,21 +605,10 @@     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)+  auth = maybe (uriToAuthorityString uri) id (findHeader HdrHost r)+  uri  = 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.+{-# DEPRECATED normalizeRequestURI "Please use Network.HTTP.Base.normalizeRequest instead" #-} normalizeRequestURI :: Bool{-do close-} -> {-URI-}String -> Request ty -> Request ty normalizeRequestURI doClose h r =    (if doClose then replaceHeader HdrConnection "close" else id) $@@ -434,7 +617,121 @@                          , uriAuthority = Nothing 			 }} --- Adds a Host header if one is NOT ALREADY PRESENT+-- | @NormalizeRequestOptions@ brings together the various defaulting\/normalization options+-- over 'Request's. Use 'defaultNormalizeRequestOptions' for the standard selection of option+data NormalizeRequestOptions ty+ = NormalizeRequestOptions+     { normDoClose   :: Bool+     , normForProxy  :: Bool+     , normUserAgent :: Maybe String+     , normCustoms   :: [RequestNormalizer ty]+     }++-- | @RequestNormalizer@ is the shape of a (pure) function that rewrites+-- a request into some normalized form.+type RequestNormalizer ty = NormalizeRequestOptions ty -> Request ty -> Request ty++defaultNormalizeRequestOptions :: NormalizeRequestOptions ty+defaultNormalizeRequestOptions = NormalizeRequestOptions+     { normDoClose   = False+     , normForProxy  = False+     , normUserAgent = Just defaultUserAgent+     , normCustoms   = []+     }++-- | @normalizeRequest opts req@ is the entry point to use to normalize your+-- request prior to transmission (or other use.) Normalization is controlled+-- via the @NormalizeRequestOptions@ record.+normalizeRequest :: NormalizeRequestOptions ty+                 -> Request ty+		 -> Request ty+normalizeRequest opts req = foldr (\ f -> f opts) req normalizers+ where+  --normalizers :: [RequestNormalizer ty]+  normalizers = +     ( normalizeHostURI+     : normalizeConnectionClose+     : normalizeUserAgent +     : normCustoms opts+     )++-- | @normalizeUserAgent ua x req@ augments the request @req@ with +-- a @User-Agent: ua@ header if @req@ doesn't already have a +-- a @User-Agent:@ set.+normalizeUserAgent :: RequestNormalizer ty+normalizeUserAgent opts req = +  case normUserAgent opts of+    Nothing -> req+    Just ua -> +     case findHeader HdrUserAgent req of+       Nothing -> setHeaders req (mkHeader HdrUserAgent ua : getHeaders req)+       Just{}  -> req++-- | @normalizeConnectionClose opts req@ sets the header @Connection: close@ +-- to indicate one-shot behavior iff @normDoClose@ is @True@. i.e., it then+-- _replaces_ any an existing @Connection:@ header in @req@.+normalizeConnectionClose :: RequestNormalizer ty+normalizeConnectionClose opts req + | normDoClose opts = replaceHeader HdrConnection "close" req+ | otherwise        = req++-- | @normalizeHostURI forProxy req@ rewrites your request to have it+-- follow the expected formats by the receiving party (proxy or server.)+-- +normalizeHostURI :: RequestNormalizer ty+normalizeHostURI opts req = +  case splitRequestURI uri of+    ("",_uri_abs)+      | forProxy -> +         case findHeader HdrHost req of+	   Nothing -> req -- no host/authority in sight..not much we can do.+	   Just h  -> req{rqURI=uri{ uriAuthority=Just URIAuth{uriUserInfo="", uriRegName=hst, uriPort=pNum}+	                           , uriScheme=if (null (uriScheme uri)) then "http" else uriScheme uri+				   }}+            where +	      hst = case span (/='@') user_hst of+	               (as,'@':bs) -> +		          case span (/=':') as of+			    (_,_:_) -> bs+			    _ -> user_hst+		       _ -> user_hst++	      (user_hst, pNum) = +	         case span isDigit (reverse h) of+		   (ds,':':bs) -> (reverse bs, ':':reverse ds)+		   _ -> (h,"")+      | otherwise -> +         case findHeader HdrHost req of+	   Nothing -> req -- no host/authority in sight..not much we can do...complain?+	   Just{}  -> req+    (h,uri_abs) +      | forProxy  -> req -- Note: _not_ stubbing out user:pass+      | otherwise -> replaceHeader HdrHost h req{rqURI=uri_abs}+ where+   uri0     = rqURI req +     -- stub out the user:pass +   uri      = uri0{uriAuthority=fmap (\ x -> x{uriUserInfo=""}) (uriAuthority uri0)}++   forProxy = normForProxy opts++{- Comments re: above rewriting:+    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.+-}++splitRequestURI :: URI -> ({-authority-}String, URI)+splitRequestURI uri = (uriToAuthorityString uri, uri{uriScheme="", uriAuthority=Nothing})++-- Adds a Host header if one is NOT ALREADY PRESENT..+{-# DEPRECATED normalizeHostHeader "Please use Network.HTTP.Base.normalizeRequest instead" #-} normalizeHostHeader :: Request ty -> Request ty normalizeHostHeader rq =    insertHeaderIfMissing HdrHost@@ -449,7 +746,6 @@         (\ 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 = fmapE (\str -> Right ([],str)) (readBlk n)@@ -476,36 +772,36 @@ 		-> IO (Result a)                 -> (Int -> IO (Result a))                 -> IO (Result ([Header], a))-chunkedTransfer bufOps readL readBlk = -  fmapE (\ (ftrs,count,info) ->-           let myftrs = Header HdrContentLength (show count) : ftrs              -           in Right (myftrs,info))-	(chunkedTransferC bufOps readL readBlk 0)+chunkedTransfer bufOps readL readBlk = chunkedTransferC bufOps readL readBlk [] 0  chunkedTransferC :: BufferOp a                  -> IO (Result a)                  -> (Int -> IO (Result a))+		 -> [a] 		 -> Int-		 -> IO (Result ([Header],Int,a))-chunkedTransferC bufOps readL readBlk n = do+		 -> IO (Result ([Header], a))+chunkedTransferC bufOps readL readBlk acc n = do   v <- readL   case v of     Left e -> return (Left e)     Right line       | size == 0 -> +         -- last chunk read; look for trailing headers..         fmapE (\ strs -> do 	         ftrs <- parseHeaders (map (buf_toStr bufOps) strs)-                 return (ftrs,n,buf_empty bufOps))+		   -- insert (computed) Content-Length header.+		 let ftrs' = Header HdrContentLength (show n) : ftrs+                 return (ftrs',buf_concat bufOps (reverse acc)))+ 	      (readTillEmpty2 bufOps readL [])       | otherwise -> do          some <- readBlk size-         readL-         more <- chunkedTransferC bufOps readL readBlk (n+size)-         return $ do-           cdata          <- some-	   (ftrs,m,mdata) <- more-           return (ftrs,m,buf_append bufOps cdata mdata) +	 case some of+	   Left e -> return (Left e)+	   Right cdata -> do+	       readL -- CRLF is mandated after the chunk block; ToDo: check that the line is empty.?+	       chunkedTransferC bufOps readL readBlk (cdata:acc) (n+size)      where       size         | buf_isEmpty bufOps line = 0
Network/HTTP/HandleStream.hs view
@@ -1,15 +1,21 @@ ----------------------------------------------------------------------------- -- | -- Module      :  Network.HTTP.HandleStream--- Copyright   :  (c) Warrick Gray 2002, Bjorn Bringert 2003-2005, 2007 Robin Bate Boerop, 2008 Sigbjorn Finne+-- Copyright   :  (c) 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.+-- A 'HandleStream'-based version of "Network.HTTP" interface. --+-- For more detailed information about what the individual exports do, please consult+-- the documentation for "Network.HTTP". /Notice/ however that the functions here do+-- not perform any kind of normalization prior to transmission (or receipt); you are+-- responsible for doing any such yourself, or, if you prefer, just switch to using+-- "Network.HTTP" function instead.+--  ----------------------------------------------------------------------------- module Network.HTTP.HandleStream         ( simpleHTTP      -- :: Request ty -> IO (Result (Response ty))@@ -27,13 +33,13 @@ -----------------------------------------------------------------  import Network.BufferType-import Network.Stream ( ConnError(..), fmapE, Result )+import Network.Stream ( fmapE, Result ) import Network.StreamDebugger ( debugByteStream ) import Network.TCP (HStream(..), HandleStream )  import Network.HTTP.Base import Network.HTTP.Headers-import Network.HTTP.Utils ( trim )+import Network.HTTP.Utils ( trim, readsOne )  import Data.Char (toLower) import Data.Maybe (fromMaybe)@@ -43,53 +49,43 @@ ------------------ 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@ transmits a resource across a non-persistent connection. simpleHTTP :: HStream ty => Request ty -> IO (Result (Response ty)) simpleHTTP r = do    auth <- getAuth r   c <- openStream (host auth) (fromMaybe 80 (port auth))   simpleHTTP_ c r +-- | @simpleHTTP_debug debugFile req@ behaves like 'simpleHTTP', but logs+-- the HTTP operation via the debug file @debugFile@. simpleHTTP_debug :: HStream ty => FilePath -> Request ty -> IO (Result (Response ty)) simpleHTTP_debug httpLogFile r = do    auth <- getAuth r-  c0 <- openStream (host auth) (fromMaybe 80 (port auth))-  c <- debugByteStream httpLogFile c0+  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 -> Request ty -> IO (Result (Response ty))-simpleHTTP_ s r = do -  auth <- getAuth r-  let r' = normalizeRequestURI True{-do close-} (host auth) r -  rsp <- sendHTTP s r'-  return rsp+simpleHTTP_ s r = sendHTTP s r +-- | @sendHTTP hStream httpRequest@ transmits @httpRequest@ over+-- @hStream@, but does not alter the status of the connection, nor request it to be+-- closed upon receiving the response. sendHTTP :: HStream ty => HandleStream ty -> Request ty -> IO (Result (Response ty))-sendHTTP conn rq = do-  let a_rq = normalizeHostHeader rq-  rsp <- catchIO (sendMain conn a_rq (return ()))-                 (\e -> do { close conn; ioError e })-  let fn list = when (or $ map findConnClose list)-                     (close conn)-  either (\_ -> fn [rqHeaders rq])-         (\r -> fn [rqHeaders rq,rspHeaders r])-         rsp-  return rsp+sendHTTP conn rq = sendHTTP_notify conn rq (return ()) +-- | @sendHTTP_notify hStream httpRequest action@ behaves like 'sendHTTP', but+-- lets you supply an IO @action@ to execute once the request has been successfully+-- transmitted over the connection. Useful when you want to set up tracing of+-- request transmission and its performance. sendHTTP_notify :: HStream ty                 => HandleStream ty 		-> Request ty 		-> IO () 		-> IO (Result (Response ty)) sendHTTP_notify conn rq onSendComplete = do-  let a_rq = normalizeHostHeader rq-  rsp <- catchIO (sendMain conn a_rq onSendComplete)+  rsp <- catchIO (sendMain conn rq onSendComplete)                  (\e -> do { close conn; ioError e })   let fn list = when (or $ map findConnClose list)                      (close conn)@@ -98,8 +94,6 @@          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-@@ -188,8 +182,7 @@    fmapE (\es -> parseResponseHead (map (buf_toStr bufferOps) es))          (readTillEmpty1 bufferOps (readLine conn)) --- | Receive and parse a HTTP request from the given Stream. Should be used ---   for server side interactions.+-- | @receiveHTTP hStream@ reads a 'Request' from the 'HandleStream' @hStream@ receiveHTTP :: HStream bufTy => HandleStream bufTy -> IO (Result (Request bufTy)) receiveHTTP conn = getRequestHead >>= either (return . Left) processRequest   where@@ -217,8 +210,9 @@      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 httpResponse@ transmits an HTTP 'Response' over+-- the 'HandleStream' @hStream@. It could be used to implement simple web+-- server interactions, performing the dual role to 'sendHTTP'. respondHTTP :: HStream ty => HandleStream ty -> Response ty -> IO () respondHTTP conn rsp = do    writeBlock conn (buf_fromStr bufferOps $ show rsp)@@ -227,12 +221,6 @@   return ()  --------------------------------------------------------------------------------readsOne :: Read a => (a -> b) -> b -> String -> b-readsOne f n str = - case reads str of-   ((v,_):_) -> f v-   _ -> n  headerName :: String -> String headerName x = map toLower (trim x)
Network/HTTP/Headers.hs view
@@ -1,10 +1,10 @@ ----------------------------------------------------------------------------- -- | -- Module      :  Network.HTTP.Headers--- Copyright   :  (c) Warrick Gray 2002, Bjorn Bringert 2003-2005, 2007 Robin Bate Boerop+-- Copyright   :  (c) Warrick Gray 2002, Bjorn Bringert 2003-2005, 2007 Robin Bate Boerop, 2008- Sigbjorn Finne -- License     :  BSD -- --- Maintainer  :  bjorn@bringert.net+-- Maintainer  :  Sigbjorn Finne <sigbjorn.finne@gmail.com> -- Stability   :  experimental -- Portability :  non-portable (not tested) --@@ -47,6 +47,7 @@    ( HasHeaders(..)    , Header(..)    , HeaderName(..)+   , mkHeader     , insertHeader    , insertHeaderIfMissing@@ -66,6 +67,10 @@ -- | The @Header@ data type pairs header names & values. data Header = Header HeaderName String +-- | Header constructor as a function, hiding above rep.+mkHeader :: HeaderName -> String -> Header+mkHeader = Header+ instance Show Header where     show (Header key value) = shows key (':':' ':value ++ crlf) @@ -118,6 +123,8 @@  | HdrRetryAfter  | HdrServer  | HdrSetCookie+ | HdrTE+ | HdrTrailer  | HdrVary  | HdrWarning  | HdrWWWAuthenticate@@ -156,6 +163,8 @@    , p "Accept-Encoding"      HdrAcceptEncoding    , p "Accept-Language"      HdrAcceptLanguage    , p "Authorization"        HdrAuthorization+   , p "Cookie"               HdrCookie+   , p "Expect"               HdrExpect    , p "From"                 HdrFrom    , p "Host"                 HdrHost    , p "If-Modified-Since"    HdrIfModifiedSince@@ -174,6 +183,9 @@    , p "Public"               HdrPublic    , p "Retry-After"          HdrRetryAfter    , p "Server"               HdrServer+   , p "Set-Cookie"           HdrSetCookie+   , p "TE"                   HdrTE+   , p "Trailer"              HdrTrailer    , p "Vary"                 HdrVary    , p "Warning"              HdrWarning    , p "WWW-Authenticate"     HdrWWWAuthenticate@@ -189,9 +201,7 @@    , p "ETag"                 HdrETag    , p "Expires"              HdrExpires    , p "Last-Modified"        HdrLastModified-   , p "Set-Cookie"           HdrSetCookie-   , p "Cookie"               HdrCookie-   , p "Expect"               HdrExpect+   , p "Content-Transfer-Encoding" HdrContentTransferEncoding    ]  where   p a b = (a,b)
Network/HTTP/Stream.hs view
@@ -1,97 +1,26 @@ ----------------------------------------------------------------------------- -- |--- Module      :  Network.HTTP+-- Module      :  Network.HTTP.Stream -- Copyright   :  (c) Warrick Gray 2002, Bjorn Bringert 2003-2005, 2007 Robin Bate Boerop -- License     :  BSD -- --- Maintainer  :  bjorn@bringert.net+-- Maintainer  :  Sigbjorn Finne <sigbjorn.finne@gmail.com> -- 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:+-- Transmitting HTTP requests and responses holding @String@ in their payload bodies.+-- This is one of the implementation modules for the "Network.HTTP" interface, representing+-- request and response content as @String@s and transmitting them in non-packed form+-- (cf. "Network.HTTP.HandleStream" and its use of @ByteString@s.) over 'Stream' handles.+-- It is mostly here for backwards compatibility, representing how requests and responses+-- were transmitted up until the 4.x releases of the HTTP package. -----   [@1xx@]   \"100 Continue\" will cause any unsent request body to be sent.---             \"101 Upgrade\" will be returned.---             Other 1xx responses are ignored.+-- For more detailed information about what the individual exports do, please consult+-- the documentation for "Network.HTTP". /Notice/ however that the functions here do+-- not perform any kind of normalization prior to transmission (or receipt); you are+-- responsible for doing any such yourself, or, if you prefer, just switch to using+-- "Network.HTTP" function instead. -- ---   [@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@@ -136,59 +65,36 @@ -----------------------------------------------------------------  --- | 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"+-- | Simple way to transmit a resource across a non-persistent connection. simpleHTTP :: Request_String -> IO (Result Response_String)-simpleHTTP r = -    do -       auth <- getAuth r-       c <- openTCPPort (host auth) (fromMaybe 80 (port auth))-       simpleHTTP_ c r+simpleHTTP r = do +   auth <- getAuth r+   c    <- openTCPPort (host auth) (fromMaybe 80 (port auth))+   simpleHTTP_ c r  -- | Like 'simpleHTTP', but acting on an already opened stream. simpleHTTP_ :: Stream s => s -> Request_String -> IO (Result Response_String)-simpleHTTP_ s r =-    do -       auth <- getAuth r-       let r' = normalizeRequestURI True{-do close-} (host auth) r -       rsp <- if debug then do-	        s' <- debugStream httpLogFile s-	        sendHTTP s' r'-	       else-	        sendHTTP s r'-       -- already done by sendHTTP because of "Connection: close" header-       --; close s -       return rsp+simpleHTTP_ s r+ | not debug    = sendHTTP s r+ | otherwise    = do+      s' <- debugStream httpLogFile s+      sendHTTP s' r+    -- already done by sendHTTP because of "Connection: close" header+    --; close s   sendHTTP :: Stream s => s -> Request_String -> IO (Result Response_String)-sendHTTP conn rq = -    do { let a_rq = normalizeHostHeader rq-       ; rsp <- catchIO (sendMain conn a_rq (return ()))-                        (\e -> do { close conn; ioError e })-       ; let fn list = when (or $ map findConnClose list)-                            (close conn)-       ; either (\_ -> fn [rqHeaders rq])-                (\r -> fn [rqHeaders rq,rspHeaders r])-                rsp-       ; return rsp-       }+sendHTTP conn rq = sendHTTP_notify conn rq (return ())  sendHTTP_notify :: Stream s => s -> Request_String -> IO () -> IO (Result Response_String)-sendHTTP_notify conn rq onSendComplete = -    do { let a_rq = normalizeHostHeader rq-       ; rsp <- catchIO (sendMain conn a_rq onSendComplete)-                        (\e -> do { close conn; ioError e })-       ; let fn list = when (or $ map findConnClose list)-                            (close conn)-       ; either (\_ -> fn [rqHeaders rq])-                (\r -> fn [rqHeaders rq,rspHeaders r])-                rsp-       ; return rsp-       }+sendHTTP_notify conn rq onSendComplete = do +   rsp <- catchIO (sendMain conn rq onSendComplete)+                  (\e -> do { close conn; ioError e })+   let fn list = when (or $ map findConnClose list)+                      (close conn)+   either (\_ -> fn [rqHeaders rq])+          (\r -> fn [rqHeaders rq,rspHeaders r])+          rsp+   return rsp  -- From RFC 2616, section 8.2.3: -- 'Because of the presence of older implementations, the protocol allows@@ -202,21 +108,21 @@ -- Since we would wait forever, I have disabled use of 100-continue for now. sendMain :: Stream s => s -> Request_String -> IO () -> IO (Result Response_String) sendMain conn rqst onSendComplete =  do -   --let str = if null (rqBody rqst)-   --              then show rqst-   --              else show (insertHeader HdrExpect "100-continue" rqst)-  writeBlock conn (show rqst)-  -- write body immediately, don't wait for 100 CONTINUE-  writeBlock conn (rqBody rqst)-  onSendComplete-  rsp <- getResponseHead conn-  switchResponse conn True False rsp rqst+    --let str = if null (rqBody rqst)+    --              then show rqst+    --              else show (insertHeader HdrExpect "100-continue" rqst)+   writeBlock conn (show rqst)+    -- write body immediately, don't wait for 100 CONTINUE+   writeBlock conn (rqBody rqst)+   onSendComplete+   rsp <- getResponseHead conn+   switchResponse conn True False rsp rqst          -- reads and parses headers getResponseHead :: Stream s => s -> IO (Result ResponseData) getResponseHead conn = do-  lor <- readTillEmpty1 stringBufferOp (readLine conn)-  return $ lor >>= parseResponseHead+   lor <- readTillEmpty1 stringBufferOp (readLine conn)+   return $ lor >>= parseResponseHead  -- Hmmm, this could go bad if we keep getting "100 Continue" -- responses...  Except this should never happen according
Network/HTTP/Utils.hs view
@@ -1,7 +1,7 @@ ----------------------------------------------------------------------------- -- | -- Module      :  Network.HTTP.Utils--- Copyright   :  (c) Warrick Gray 2002, Bjorn Bringert 2003-2004, Simon Foster 2004, 2007 Robin Bate Boerop, 2008 Sigbjorn Finne+-- 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>@@ -11,15 +11,18 @@ -- Set of utility functions and definitions used by package modules. -- module Network.HTTP.Utils-       ( trim    -- :: String -> String-       , trimL   -- :: String -> String-       , trimR   -- :: String -> String+       ( trim     -- :: String -> String+       , trimL    -- :: String -> String+       , trimR    -- :: String -> String        -       , crlf    -- :: String+       , crlf     -- :: String+       , sp       -- :: String -       , split   -- :: Eq a => a -> [a] -> Maybe ([a],[a])-       , splitBy -- :: Eq a => a -> [a] -> [[a]]+       , split    -- :: Eq a => a -> [a] -> Maybe ([a],[a])+       , splitBy  -- :: Eq a => a -> [a] -> [[a]]        +       , readsOne -- :: Read a => (a -> b) -> b -> String -> b+               ) where         import Data.Char@@ -30,6 +33,10 @@ crlf :: String crlf = "\r\n" +-- | @sp@ lets you save typing one character.+sp :: String+sp   = " "+ -- | @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.@@ -64,4 +71,13 @@     case break (==c) xs of       (_,[]) -> [xs]       (as,_:bs) -> as : splitBy c bs++-- | @readsOne f def str@ tries to 'read' @str@, taking+-- the first result and passing it to @f@. If the 'read'+-- doesn't succeed, return @def@.+readsOne :: Read a => (a -> b) -> b -> String -> b+readsOne f n str = + case reads str of+   ((v,_):_) -> f v+   _ -> n 
Network/Stream.hs view
@@ -4,7 +4,7 @@ -- Copyright   :  (c) Warrick Gray 2002, Bjorn Bringert 2003-2004, Simon Foster 2004, 2007 Robin Bate Boerop -- License     :  BSD ----- Maintainer  :  bjorn@bringert.net+-- Maintainer  :  Sigbjorn Finne <sigbjorn.finne@gmail.com> -- Stability   :  experimental -- Portability :  non-portable (not tested) --
Network/StreamDebugger.hs view
@@ -4,7 +4,7 @@ -- Copyright   :  (c) Warrick Gray 2002, Bjorn Bringert 2003-2004, Simon Foster 2004, 2007 Robin Bate Boerop -- License     :  BSD ----- Maintainer  :  bjorn@bringert.net+-- Maintainer  :  Sigbjorn Finne <sigbjorn.finne@gmail.com> -- Stability   :  experimental -- Portability :  non-portable (not tested) --
Network/StreamSocket.hs view
@@ -4,7 +4,7 @@ -- Copyright   :  (c) Warrick Gray 2002, Bjorn Bringert 2003-2004, Simon Foster 2004, 2007 Robin Bate Boerop. -- License     :  BSD ----- Maintainer  :  bjorn@bringert.net+-- Maintainer  :  Sigbjorn Finne <sigbjorn.finne@gmail.com> -- Stability   :  experimental -- Portability :  non-portable (not tested) --
Network/TCP.hs view
@@ -5,7 +5,7 @@ -- Copyright   :  (c) Warrick Gray 2002, Bjorn Bringert 2003-2004, Simon Foster 2004 -- License     :  BSD ----- Maintainer  :  bjorn@bringert.net+-- Maintainer  :  Sigbjorn Finne <sigbjorn.finne@gmail.com> -- Stability   :  experimental -- Portability :  non-portable (not tested) --@@ -35,6 +35,8 @@    , StreamHooks(..)    , nullHooks    , setStreamHooks+   , hstreamToConnection+    ) where  import Network.BSD (getHostByName, hostAddresses)@@ -91,6 +93,9 @@  | ConnClosed    deriving(Eq) +hstreamToConnection :: HandleStream String -> Connection+hstreamToConnection h = Connection h+ connHooks' :: Conn a -> Maybe (StreamHooks a) connHooks' ConnClosed{} = Nothing connHooks' x = connHooks x@@ -109,10 +114,10 @@  nullHooks :: StreamHooks ty nullHooks = StreamHooks -     { hook_readLine  = \ _ _ -> return ()-     , hook_readBlock = \ _ _ _ -> return ()+     { hook_readLine   = \ _ _   -> return ()+     , hook_readBlock  = \ _ _ _ -> return ()      , hook_writeBlock = \ _ _ _ -> return ()-     , hook_close = return ()+     , hook_close      = return ()      }  setStreamHooks :: HandleStream ty -> StreamHooks ty -> IO ()@@ -162,10 +167,9 @@     -- (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.---+-- | @openTCPPort uri port@  establishes a connection to a remote+-- host, using 'getHostByName' which possibly queries the DNS system, hence +-- may trigger a network connection. openTCPPort :: String -> Int -> IO Connection openTCPPort uri port = openTCPConnection uri port >>= return.Connection @@ -173,31 +177,39 @@ -- 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 <- 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-       ; 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)-       }+openTCPConnection_ uri port stashInput = do+    s <- socket AF_INET Stream 6+    setSocketOption s KeepAlive 1+    hostA <- getHostAddr uri+    let a = SockAddrInet (toEnum port) hostA+    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)+ where+  getHostAddr h = do+    catchIO (inet_addr uri)    -- handles ascii IP numbers+            (\ _ -> do+	        host <- getHostByName_safe uri+                case hostAddresses host of+                  []     -> fail ("openTCPConnection: no addresses in host entry for " ++ show h)+                  (ha:_) -> return ha) +  getHostByName_safe h = +    catchIO (getHostByName h)+            (\ _ -> fail ("openTCPConnection: host lookup failure for " ++ show h))+ closeConnection :: HandleStream a -> IO Bool -> IO () closeConnection ref readL = do     -- won't hold onto the lock for the duration@@ -245,7 +257,6 @@       | map toLower (connHost v) == map toLower name -> sIsConnected (connSock v)       | otherwise -> return False - readBlockBS :: HandleStream a -> Int -> IO (Result a) readBlockBS ref n = onNonClosedDo ref $ \ conn -> do    x <- bufferGetBlock ref n@@ -255,7 +266,7 @@    return x  -- 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)+-- (however many bytes this is is left for the user to decipher) readLineBS :: HandleStream a -> IO (Result a) readLineBS ref = onNonClosedDo ref $ \ conn -> do    x <- bufferReadLine ref@@ -328,5 +339,5 @@   x <- readMVar (getRef h)   case x of     ConnClosed{} -> return (failWith ErrorClosed)-    c -> act c-    +    _ -> act x+