Network/HTTP/Base.hs

{-# LANGUAGE ScopedTypeVariables #-}
-----------------------------------------------------------------------------
-- |
-- Module : Network.HTTP.Base
-- Copyright : See LICENSE file
-- License : BSD
-- 
-- Maintainer : Ganesh Sittampalam <http@projects.haskell.org>
-- Stability : experimental
-- Portability : non-portable (not tested)
--
-- 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 -- :: String

 -- ** HTTP
 , Request(..)
 , Response(..)
 , RequestMethod(..)
 
 , Request_String
 , Response_String
 , HTTPRequest
 , HTTPResponse
 
 -- ** URL Encoding
 , urlEncode
 , urlDecode
 , urlEncodeVars

 -- ** URI authority parsing
 , URIAuthority(..)
 , parseURIAuthority
 
 -- internal
 , uriToAuthorityString -- :: URI -> String
 , uriAuthToString -- :: URIAuth -> String
 , 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
 , findConnClose

 -- internal export (for the use by Network.HTTP.{Stream,ByteStream} )
 , linearTransfer
 , hopefulTransfer
 , chunkedTransfer
 , uglyDeathTransfer
 , readTillEmpty1
 , readTillEmpty2
 
 , defaultGETRequest
 , defaultGETRequest_
 , mkRequest
 , setRequestBody

 , defaultUserAgent
 , httpPackageVersion
 , libUA {- backwards compatibility, will disappear..soon -}
 
 , catchIO
 , catchIO_
 , responseParseError
 
 , getRequestVersion
 , getResponseVersion
 , setRequestVersion
 , setResponseVersion

 , failHTTPS
 
 ) where

import Network.URI
 ( URI(uriAuthority, uriPath, uriScheme)
 , URIAuth(URIAuth, uriUserInfo, uriRegName, uriPort)
 , parseURIReference
 )

import Control.Monad ( guard )
import Control.Monad.Error ()
import Data.Bits ( (.&.), (.|.), shiftL, shiftR )
import Data.Word ( Word8 )
import Data.Char ( digitToInt, intToDigit, toLower, isDigit,
 isAscii, isAlphaNum, ord, chr )
import Data.List ( partition, find )
import Data.Maybe ( listToMaybe, fromMaybe )
import Numeric ( readHex )

import Network.Stream
import Network.BufferType ( BufferOp(..), BufferType(..) )
import Network.HTTP.Headers
import Network.HTTP.Utils ( trim, crlf, sp, readsOne )
import qualified Network.HTTP.Base64 as Base64 (encode)

import Text.Read.Lex (readDecP)
import Text.ParserCombinators.ReadP
 ( ReadP, readP_to_S, char, (<++), look, munch )

import Control.Exception as Exception (catch, IOException)

import qualified Paths_HTTP as Self (version)
import Data.Version (showVersion)

-----------------------------------------------------------------
------------------ URI Authority parsing ------------------------
-----------------------------------------------------------------

data URIAuthority = URIAuthority { user :: Maybe String, 
				 password :: Maybe String,
				 host :: String,
				 port :: Maybe Int
				 } deriving (Eq,Show)

-- | Parse the authority part of a URL.
--
-- > RFC 1732, section 3.1:
-- >
-- > //<user>:<password>@<host>:<port>/<url-path>
-- > Some or all of the parts "<user>:<password>@", ":<password>",
-- > ":<port>", and "/<url-path>" may be excluded.
parseURIAuthority :: String -> Maybe URIAuthority
parseURIAuthority s = listToMaybe (map fst (readP_to_S pURIAuthority s))


pURIAuthority :: ReadP URIAuthority
pURIAuthority = do
		(u,pw) <- (pUserInfo `before` char '@') 
			 <++ return (Nothing, Nothing)
		h <- munch (/=':')
		p <- orNothing (char ':' >> readDecP)
		look >>= guard . null 
		return URIAuthority{ user=u, password=pw, host=h, port=p }

pUserInfo :: ReadP (Maybe String, Maybe String)
pUserInfo = do
	 u <- orNothing (munch (`notElem` ":@"))
	 p <- orNothing (char ':' >> munch (/='@'))
	 return (u,p)

before :: Monad m => m a -> m b -> m a
before a b = a >>= \x -> b >> return x

orNothing :: ReadP a -> ReadP (Maybe a)
orNothing p = fmap Just p <++ return Nothing

-- This function duplicates old Network.URI.authority behaviour.
uriToAuthorityString :: URI -> String
uriToAuthorityString u = maybe "" uriAuthToString (uriAuthority u)

uriAuthToString :: URIAuth -> String
uriAuthToString ua = 
 concat [ uriUserInfo ua 
 , uriRegName ua
	 , uriPort ua
	 ]

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

failHTTPS :: Monad m => URI -> m ()
failHTTPS uri
 | map toLower (uriScheme uri) == "https:" = fail "https not supported"
 | otherwise = return ()

-- 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 --------------------------------
-----------------------------------------------------------------


-- Protocol version
httpVersion :: String
httpVersion = "HTTP/1.1"


-- | The HTTP request method, to be used in the 'Request' object.
-- We are missing a few of the stranger methods, but these are
-- not really necessary until we add full TLS.
data RequestMethod = HEAD | PUT | GET | POST | DELETE | OPTIONS | TRACE | 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),
	 ("GET", GET),
	 ("POST", POST),
 ("DELETE", DELETE),
	 ("OPTIONS", OPTIONS),
	 ("TRACE", TRACE),
	 ("CONNECT", CONNECT)]

-- 
-- for backwards-ish compatibility; suggest
-- migrating to new Req/Resp by adding type param.
-- 
type Request_String = Request String
type Response_String = Response String

-- Hmm..I really want to use these for the record
-- type, but it will upset codebases wanting to
-- migrate (and live with using pre-HTTPbis versions.)
type HTTPRequest a = Request a
type HTTPResponse a = Response a

-- | An HTTP Request.
-- The 'Show' instance of this type is used for message serialisation,
-- which means no body data is output.
data Request a =
 Request { rqURI :: URI -- ^ might need changing in future
 -- 1) to support '*' uri in OPTIONS request
 -- 2) transparent support for both relative
 -- & absolute uris, although this should
 -- already work (leave scheme & host parts empty).
 , rqMethod :: RequestMethod
 , rqHeaders :: [Header]
 , rqBody :: a
 }

-- 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 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

instance HasHeaders (Request a) where
 getHeaders = rqHeaders
 setHeaders rq hdrs = rq { rqHeaders=hdrs }

-- | For easy pattern matching, HTTP response codes @xyz@ are
-- represented as @(x,y,z)@.
type ResponseCode = (Int,Int,Int)

-- | @ResponseData@ contains the head of a response payload;
-- HTTP response code, accompanying text description + header
-- fields.
type ResponseData = (ResponseCode,String,[Header])

-- | @RequestData@ contains the head of a HTTP request; method,
-- its URL along with the auxillary/supporting header data.
type RequestData = (RequestMethod,URI,[Header])

-- | An HTTP Response.
-- The 'Show' instance of this type is used for message serialisation,
-- which means no body data is output, additionally the output will
-- show an HTTP version of 1.1 instead of the actual version returned
-- by a server.
data Response a =
 Response { rspCode :: ResponseCode
 , rspReason :: String
 , rspHeaders :: [Header]
 , rspBody :: a
 }
 
-- This is an invalid representation of a received response, 
-- since we have made the assumption that all responses are HTTP/1.1
instance Show (Response a) where
 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 ------------------------------
------------------------------------------------------------------

-- | Deprecated. Use 'defaultUserAgent'
libUA :: String
libUA = "hs-HTTP-400009"
{-# DEPRECATED libUA "Use defaultUserAgent instead (but note the user agent name change)" #-}

-- | A default user agent string. The string is @\"haskell-HTTP/$version\"@
-- where @$version@ is the version of this HTTP package.
--
defaultUserAgent :: String
defaultUserAgent = "haskell-HTTP/" ++ httpPackageVersion

-- | The version of this HTTP package as a string, e.g. @\"4000.1.2\"@. This
-- may be useful to include in a user agent string so that you can determine
-- from server logs what version of this package HTTP clients are using.
-- This can be useful for tracking down HTTP compatibility quirks.
--
httpPackageVersion :: String
httpPackageVersion = showVersion Self.version

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)

-- set rqBody, Content-Type and Content-Length headers.
setRequestBody :: Request_String -> (String, String) -> Request_String
setRequestBody req (typ, body) = req' { rqBody=body }
 where
 req' = replaceHeader HdrContentType typ .
 replaceHeader HdrContentLength (show $ length body) $
 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 --------------------------------------
-----------------------------------------------------------------

-- Parsing a request
parseRequestHead :: [String] -> Result RequestData
parseRequestHead [] = Left ErrorClosed
parseRequestHead (com:hdrs) = do
 (version,rqm,uri) <- requestCommand com (words com)
 hdrs' <- parseHeaders 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
 | otherwise = parse_err l

 parse_err l = responseParseError "parseRequestHead"
 ("Request command line parse failure: " ++ l)

-- Parsing a response
parseResponseHead :: [String] -> Result ResponseData
parseResponseHead [] = failWith ErrorClosed
parseResponseHead (sts:hdrs) = do
 (version,code,reason) <- responseStatus sts (words sts)
 hdrs' <- parseHeaders hdrs
 return (code,reason, withVersion version hdrs')
 where
 responseStatus _l _yes@(version:code:reason) =
 return (version,match code,concatMap (++" ") reason)
 responseStatus l _no 
 | null l = failWith ErrorClosed -- an assumption
 | otherwise = parse_err l

 parse_err l = 
 responseParseError 
 "parseResponseHead"
 ("Response status line parse failure: " ++ l)

 match [a,b,c] = (digitToInt a,
 digitToInt b,
 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 ----------------------------------
-----------------------------------------------------------------

data ResponseNextStep
 = Continue
 | Retry
 | Done
 | ExpectEntity
 | DieHorribly String

matchResponse :: RequestMethod -> ResponseCode -> ResponseNextStep
matchResponse rqst rsp =
 case rsp of
 (1,0,0) -> Continue
 (1,0,1) -> Done -- upgrade to TLS
 (1,_,_) -> Continue -- default
 (2,0,4) -> Done
 (2,0,5) -> Done
 (2,_,_) -> ans
 (3,0,4) -> Done
 (3,0,5) -> Done
 (3,_,_) -> ans
 (4,1,7) -> Retry -- Expectation failed
 (4,_,_) -> ans
 (5,_,_) -> ans
 (a,b,c) -> DieHorribly ("Response code " ++ map intToDigit [a,b,c] ++ " not recognised")
 where
 ans | rqst == HEAD = Done
 | otherwise = ExpectEntity
 

 
-----------------------------------------------------------------
------------------ A little friendly funtionality ---------------
-----------------------------------------------------------------


{-
 I had a quick look around but couldn't find any RFC about
 the encoding of data on the query string. I did find an
 IETF memo, however, so this is how I justify the urlEncode
 and urlDecode methods.

 Doc name: draft-tiwari-appl-wxxx-forms-01.txt (look on www.ietf.org)

 Reserved chars: ";", "/", "?", ":", "@", "&", "=", "+", ",", and "$" are reserved.
 Unwise: "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"
 URI delims: "<" | ">" | "#" | "%" | <">
 Unallowed ASCII: <US-ASCII coded characters 00-1F and 7F hexadecimal>
 <US-ASCII coded character 20 hexadecimal>
 Also unallowed: any non-us-ascii character

 Escape method: char -> '%' a b where a, b :: Hex digits
-}

replacement_character :: Char
replacement_character = '\xfffd'

-- | Encode a single Haskell Char to a list of Word8 values, in UTF8 format.
--
-- Shamelessly stolen from utf-8string-0.3.7
encodeChar :: Char -> [Word8]
encodeChar = map fromIntegral . go . ord
 where
 go oc
 | oc <= 0x7f = [oc]

 | oc <= 0x7ff = [ 0xc0 + (oc `shiftR` 6)
 , 0x80 + oc .&. 0x3f
 ]

 | oc <= 0xffff = [ 0xe0 + (oc `shiftR` 12)
 , 0x80 + ((oc `shiftR` 6) .&. 0x3f)
 , 0x80 + oc .&. 0x3f
 ]
 | otherwise = [ 0xf0 + (oc `shiftR` 18)
 , 0x80 + ((oc `shiftR` 12) .&. 0x3f)
 , 0x80 + ((oc `shiftR` 6) .&. 0x3f)
 , 0x80 + oc .&. 0x3f
 ]

-- | Decode a UTF8 string packed into a list of Word8 values, directly to String
--
-- Shamelessly stolen from utf-8string-0.3.7
decode :: [Word8] -> String
decode [ ] = ""
decode (c:cs)
 | c < 0x80 = chr (fromEnum c) : decode cs
 | c < 0xc0 = replacement_character : decode cs
 | c < 0xe0 = multi1
 | c < 0xf0 = multi_byte 2 0xf 0x800
 | c < 0xf8 = multi_byte 3 0x7 0x10000
 | c < 0xfc = multi_byte 4 0x3 0x200000
 | c < 0xfe = multi_byte 5 0x1 0x4000000
 | otherwise = replacement_character : decode cs
 where
 multi1 = case cs of
 c1 : ds | c1 .&. 0xc0 == 0x80 ->
 let d = ((fromEnum c .&. 0x1f) `shiftL` 6) .|. fromEnum (c1 .&. 0x3f)
 in if d >= 0x000080 then toEnum d : decode ds
 else replacement_character : decode ds
 _ -> replacement_character : decode cs

 multi_byte :: Int -> Word8 -> Int -> [Char]
 multi_byte i mask overlong = aux i cs (fromEnum (c .&. mask))
 where
 aux 0 rs acc
 | overlong <= acc && acc <= 0x10ffff &&
 (acc < 0xd800 || 0xdfff < acc) &&
 (acc < 0xfffe || 0xffff < acc) = chr acc : decode rs
 | otherwise = replacement_character : decode rs

 aux n (r:rs) acc
 | r .&. 0xc0 == 0x80 = aux (n-1) rs
 $ shiftL acc 6 .|. fromEnum (r .&. 0x3f)

 aux _ rs _ = replacement_character : decode rs


-- This function is a bit funny because potentially the input String could contain some actual Unicode
-- characters (though this shouldn't happen for most use cases), so we have to preserve those characters
-- while simultaneously decoding any UTF-8 data
urlDecode :: String -> String
urlDecode = go []
 where
 go bs ('%':a:b:rest) = go (fromIntegral (16 * digitToInt a + digitToInt b) : bs) rest
 go bs (h:t) | fromEnum h < 256 = go (fromIntegral (fromEnum h) : bs) t -- Treat ASCII as just another byte of UTF-8
 go [] [] = []
 go [] (h:t) = h : go [] t -- h >= 256, so can't be part of any UTF-8 byte sequence
 go bs rest = decode (reverse bs) ++ go [] rest


urlEncode :: String -> String
urlEncode [] = []
urlEncode (ch:t) 
 | (isAscii ch && isAlphaNum ch) || ch `elem` "-_.~" = ch : urlEncode t
 | not (isAscii ch) = foldr escape (urlEncode t) (encodeChar ch)
 | otherwise = escape (fromIntegral (fromEnum ch)) (urlEncode t)
 where
 escape b rs = '%':showH (b `div` 16) (showH (b `mod` 16) rs)

 showH :: Word8 -> String -> String
 showH x xs
 | x <= 9 = to (o_0 + x) : xs
 | otherwise = to (o_A + (x-10)) : xs
 where
 to = toEnum . fromIntegral
 fro = fromIntegral . fromEnum

 o_0 = fro '0'
 o_A = fro 'A'

-- Encode form variables, useable in either the
-- query part of a URI, or the body of a POST request.
-- I have no source for this information except experience,
-- this sort of encoding worked fine in CGI programming.
urlEncodeVars :: [(String,String)] -> String
urlEncodeVars ((n,v):t) =
 let (same,diff) = partition ((==n) . fst) t
 in urlEncode n ++ '=' : foldl (\x y -> x ++ ',' : urlEncode y) (urlEncode $ v) (map snd same)
 ++ urlEncodeRest diff
 where urlEncodeRest [] = []
 urlEncodeRest diff = '&' : urlEncodeVars diff
urlEncodeVars [] = []

-- | @getAuth req@ fishes out the authority portion of the URL in a request's @Host@
-- header.
getAuth :: Monad m => Request ty -> m URIAuthority
getAuth r = 
 -- ToDo: verify that Network.URI functionality doesn't take care of this (now.)
 case parseURIAuthority auth of
 Just x -> return x 
 Nothing -> fail $ "Network.HTTP.Base.getAuth: Error parsing URI authority '" ++ auth ++ "'"
 where 
 auth = maybe (uriToAuthorityString uri) id (findHeader HdrHost r)
 uri = rqURI r

{-# 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) $
 insertHeaderIfMissing HdrHost h $
 r { rqURI = (rqURI r){ uriScheme = ""
 , uriAuthority = Nothing
			 }}

-- | @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
 : normalizeBasicAuth
 : 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
 Just u | u /= defaultUserAgent -> req
 _ -> replaceHeader HdrUserAgent ua 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

-- | @normalizeBasicAuth opts req@ sets the header @Authorization: Basic...@
-- if the "user:pass@" part is present in the "http://user:pass@host/path"
-- of the URI. If Authorization header was present already it is not replaced.
normalizeBasicAuth :: RequestNormalizer ty
normalizeBasicAuth _ req =
 case getAuth req of
 Just uriauth ->
 case (user uriauth, password uriauth) of
 (Just u, Just p) ->
 insertHeaderIfMissing HdrAuthorization astr req
 where
 astr = "Basic " ++ base64encode (u ++ ":" ++ p)
 base64encode = Base64.encode . stringToOctets :: String -> String
 stringToOctets = map (fromIntegral . fromEnum) :: String -> [Word8]
 (_, _) -> req
 Nothing ->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 -> insertHeaderIfMissing HdrHost h req 
 | otherwise -> replaceHeader HdrHost h req{rqURI=uri_abs} -- Note: _not_ stubbing out user:pass
 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
 (uriToAuthorityString $ rqURI rq)
			rq
 
-- Looks for a "Connection" header with the value "close".
-- Returns True when this is found.
findConnClose :: [Header] -> Bool
findConnClose hdrs =
 maybe False
 (\ x -> map toLower (trim x) == "close")
	(lookupHeader HdrConnection hdrs)

-- | Used when we know exactly how many bytes to expect.
linearTransfer :: (Int -> IO (Result a)) -> Int -> IO (Result ([Header],a))
linearTransfer readBlk n = fmapE (\str -> Right ([],str)) (readBlk n)

-- | Used when nothing about data is known,
-- Unfortunately waiting for a socket closure
-- causes bad behaviour. Here we just
-- take data once and give up the rest.
hopefulTransfer :: BufferOp a
 -> IO (Result a)
		-> [a]
		-> IO (Result ([Header],a))
hopefulTransfer bufOps readL strs 
 = readL >>= 
 either (\v -> return $ Left v)
 (\more -> if (buf_isEmpty bufOps more)
 then return (Right ([], buf_concat bufOps $ reverse strs))
 else hopefulTransfer bufOps readL (more:strs))

-- | A necessary feature of HTTP\/1.1
-- Also the only transfer variety likely to
-- return any footers.
chunkedTransfer :: BufferOp a
		-> IO (Result a)
 -> (Int -> IO (Result a))
 -> IO (Result ([Header], a))
chunkedTransfer bufOps readL readBlk = chunkedTransferC bufOps readL readBlk [] 0

chunkedTransferC :: BufferOp a
 -> IO (Result a)
 -> (Int -> IO (Result a))
		 -> [a]
		 -> Int
		 -> 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)
		 -- 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
	 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
 | otherwise = 
	 case readHex (buf_toStr bufOps line) of
 (hx,_):_ -> hx
 _ -> 0

-- | Maybe in the future we will have a sensible thing
-- to do here, at that time we might want to change
-- the name.
uglyDeathTransfer :: String -> IO (Result ([Header],a))
uglyDeathTransfer loc = return (responseParseError loc "Unknown Transfer-Encoding")

-- | Remove leading crlfs then call readTillEmpty2 (not required by RFC)
readTillEmpty1 :: BufferOp a
	 -> IO (Result a)
 -> IO (Result [a])
readTillEmpty1 bufOps readL =
 readL >>=
 either (return . Left)
 (\ s -> 
	 if buf_isLineTerm bufOps s
 then readTillEmpty1 bufOps readL
 else readTillEmpty2 bufOps readL [s])

-- | Read lines until an empty line (CRLF),
-- also accepts a connection close as end of
-- input, which is not an HTTP\/1.1 compliant
-- thing to do - so probably indicates an
-- error condition.
readTillEmpty2 :: BufferOp a
	 -> IO (Result a)
	 -> [a]
	 -> IO (Result [a])
readTillEmpty2 bufOps readL list =
 readL >>=
 either (return . Left)
 (\ s ->
	 if buf_isLineTerm bufOps s || buf_isEmpty bufOps s
 then return (Right $ reverse (s:list))
 else readTillEmpty2 bufOps readL (s:list))

--
-- Misc
--

-- | @catchIO a h@ handles IO action exceptions throughout codebase; version-specific
-- tweaks better go here.
catchIO :: IO a -> (IOException -> IO a) -> IO a
catchIO a h = Exception.catch a h

catchIO_ :: IO a -> IO a -> IO a
catchIO_ a h = Exception.catch a (\(_ :: IOException) -> h)

responseParseError :: String -> String -> Result a
responseParseError loc v = failWith (ErrorParse (loc ++ ' ':v))

AltStyle によって変換されたページ (->オリジナル) /