|
Packit |
acf257 |
{-# LANGUAGE ScopedTypeVariables #-}
|
|
Packit |
acf257 |
-----------------------------------------------------------------------------
|
|
Packit |
acf257 |
-- |
|
|
Packit |
acf257 |
-- Module : Network.HTTP.Base
|
|
Packit |
acf257 |
-- Copyright : See LICENSE file
|
|
Packit |
acf257 |
-- License : BSD
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
-- Maintainer : Ganesh Sittampalam <ganesh@earth.li>
|
|
Packit |
acf257 |
-- Stability : experimental
|
|
Packit |
acf257 |
-- Portability : non-portable (not tested)
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
-- Definitions of @Request@ and @Response@ types along with functions
|
|
Packit |
acf257 |
-- for normalizing them. It is assumed to be an internal module; user
|
|
Packit |
acf257 |
-- code should, if possible, import @Network.HTTP@ to access the functionality
|
|
Packit |
acf257 |
-- that this module provides.
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
-- Additionally, the module exports internal functions for working with URLs,
|
|
Packit |
acf257 |
-- and for handling the processing of requests and responses coming back.
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
-----------------------------------------------------------------------------
|
|
Packit |
acf257 |
module Network.HTTP.Base
|
|
Packit |
acf257 |
(
|
|
Packit |
acf257 |
-- ** Constants
|
|
Packit |
acf257 |
httpVersion -- :: String
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- ** HTTP
|
|
Packit |
acf257 |
, Request(..)
|
|
Packit |
acf257 |
, Response(..)
|
|
Packit |
acf257 |
, RequestMethod(..)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, Request_String
|
|
Packit |
acf257 |
, Response_String
|
|
Packit |
acf257 |
, HTTPRequest
|
|
Packit |
acf257 |
, HTTPResponse
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- ** URL Encoding
|
|
Packit |
acf257 |
, urlEncode
|
|
Packit |
acf257 |
, urlDecode
|
|
Packit |
acf257 |
, urlEncodeVars
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- ** URI authority parsing
|
|
Packit |
acf257 |
, URIAuthority(..)
|
|
Packit |
acf257 |
, parseURIAuthority
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- internal
|
|
Packit |
acf257 |
, uriToAuthorityString -- :: URI -> String
|
|
Packit |
acf257 |
, uriAuthToString -- :: URIAuth -> String
|
|
Packit |
acf257 |
, uriAuthPort -- :: Maybe URI -> URIAuth -> Int
|
|
Packit |
acf257 |
, reqURIAuth -- :: Request ty -> URIAuth
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, parseResponseHead -- :: [String] -> Result ResponseData
|
|
Packit |
acf257 |
, parseRequestHead -- :: [String] -> Result RequestData
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, ResponseNextStep(..)
|
|
Packit |
acf257 |
, matchResponse
|
|
Packit |
acf257 |
, ResponseData
|
|
Packit |
acf257 |
, ResponseCode
|
|
Packit |
acf257 |
, RequestData
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, NormalizeRequestOptions(..)
|
|
Packit |
acf257 |
, defaultNormalizeRequestOptions -- :: NormalizeRequestOptions ty
|
|
Packit |
acf257 |
, RequestNormalizer
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, normalizeRequest -- :: NormalizeRequestOptions ty -> Request ty -> Request ty
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, splitRequestURI
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, getAuth
|
|
Packit |
acf257 |
, normalizeRequestURI
|
|
Packit |
acf257 |
, normalizeHostHeader
|
|
Packit |
acf257 |
, findConnClose
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- internal export (for the use by Network.HTTP.{Stream,ByteStream} )
|
|
Packit |
acf257 |
, linearTransfer
|
|
Packit |
acf257 |
, hopefulTransfer
|
|
Packit |
acf257 |
, chunkedTransfer
|
|
Packit |
acf257 |
, uglyDeathTransfer
|
|
Packit |
acf257 |
, readTillEmpty1
|
|
Packit |
acf257 |
, readTillEmpty2
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, defaultGETRequest
|
|
Packit |
acf257 |
, defaultGETRequest_
|
|
Packit |
acf257 |
, mkRequest
|
|
Packit |
acf257 |
, setRequestBody
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, defaultUserAgent
|
|
Packit |
acf257 |
, httpPackageVersion
|
|
Packit |
acf257 |
, libUA {- backwards compatibility, will disappear..soon -}
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, catchIO
|
|
Packit |
acf257 |
, catchIO_
|
|
Packit |
acf257 |
, responseParseError
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, getRequestVersion
|
|
Packit |
acf257 |
, getResponseVersion
|
|
Packit |
acf257 |
, setRequestVersion
|
|
Packit |
acf257 |
, setResponseVersion
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, failHTTPS
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
) where
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
import Network.URI
|
|
Packit |
acf257 |
( URI(uriAuthority, uriPath, uriScheme)
|
|
Packit |
acf257 |
, URIAuth(URIAuth, uriUserInfo, uriRegName, uriPort)
|
|
Packit |
acf257 |
, parseURIReference
|
|
Packit |
acf257 |
)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
import Control.Monad ( guard )
|
|
Packit |
acf257 |
import Control.Monad.Error ()
|
|
Packit |
acf257 |
import Data.Bits ( (.&.), (.|.), shiftL, shiftR )
|
|
Packit |
acf257 |
import Data.Word ( Word8 )
|
|
Packit |
acf257 |
import Data.Char ( digitToInt, intToDigit, toLower, isDigit,
|
|
Packit |
acf257 |
isAscii, isAlphaNum, ord, chr )
|
|
Packit |
acf257 |
import Data.List ( partition, find )
|
|
Packit |
acf257 |
import Data.Maybe ( listToMaybe, fromMaybe )
|
|
Packit |
acf257 |
import Numeric ( readHex )
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
import Network.Stream
|
|
Packit |
acf257 |
import Network.BufferType ( BufferOp(..), BufferType(..) )
|
|
Packit |
acf257 |
import Network.HTTP.Headers
|
|
Packit |
acf257 |
import Network.HTTP.Utils ( trim, crlf, sp, readsOne )
|
|
Packit |
acf257 |
import qualified Network.HTTP.Base64 as Base64 (encode)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
import Text.Read.Lex (readDecP)
|
|
Packit |
acf257 |
import Text.ParserCombinators.ReadP
|
|
Packit |
acf257 |
( ReadP, readP_to_S, char, (<++), look, munch, munch1 )
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
import Control.Exception as Exception (catch, IOException)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
import qualified Paths_HTTP as Self (version)
|
|
Packit |
acf257 |
import Data.Version (showVersion)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-----------------------------------------------------------------
|
|
Packit |
acf257 |
------------------ URI Authority parsing ------------------------
|
|
Packit |
acf257 |
-----------------------------------------------------------------
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
data URIAuthority = URIAuthority { user :: Maybe String,
|
|
Packit |
acf257 |
password :: Maybe String,
|
|
Packit |
acf257 |
host :: String,
|
|
Packit |
acf257 |
port :: Maybe Int
|
|
Packit |
acf257 |
} deriving (Eq,Show)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | Parse the authority part of a URL.
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
-- > RFC 1732, section 3.1:
|
|
Packit |
acf257 |
-- >
|
|
Packit |
acf257 |
-- > //<user>:<password>@<host>:<port>/<url-path>
|
|
Packit |
acf257 |
-- > Some or all of the parts "<user>:<password>@", ":<password>",
|
|
Packit |
acf257 |
-- > ":<port>", and "/<url-path>" may be excluded.
|
|
Packit |
acf257 |
parseURIAuthority :: String -> Maybe URIAuthority
|
|
Packit |
acf257 |
parseURIAuthority s = listToMaybe (map fst (readP_to_S pURIAuthority s))
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
pURIAuthority :: ReadP URIAuthority
|
|
Packit |
acf257 |
pURIAuthority = do
|
|
Packit |
acf257 |
(u,pw) <- (pUserInfo `before` char '@')
|
|
Packit |
acf257 |
<++ return (Nothing, Nothing)
|
|
Packit |
acf257 |
h <- rfc2732host <++ munch (/=':')
|
|
Packit |
acf257 |
p <- orNothing (char ':' >> readDecP)
|
|
Packit |
acf257 |
look >>= guard . null
|
|
Packit |
acf257 |
return URIAuthority{ user=u, password=pw, host=h, port=p }
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- RFC2732 adds support for '[literal-ipv6-address]' in the host part of a URL
|
|
Packit |
acf257 |
rfc2732host :: ReadP String
|
|
Packit |
acf257 |
rfc2732host = do
|
|
Packit |
acf257 |
_ <- char '['
|
|
Packit |
acf257 |
res <- munch1 (/=']')
|
|
Packit |
acf257 |
_ <- char ']'
|
|
Packit |
acf257 |
return res
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
pUserInfo :: ReadP (Maybe String, Maybe String)
|
|
Packit |
acf257 |
pUserInfo = do
|
|
Packit |
acf257 |
u <- orNothing (munch (`notElem` ":@"))
|
|
Packit |
acf257 |
p <- orNothing (char ':' >> munch (/='@'))
|
|
Packit |
acf257 |
return (u,p)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
before :: Monad m => m a -> m b -> m a
|
|
Packit |
acf257 |
before a b = a >>= \x -> b >> return x
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
orNothing :: ReadP a -> ReadP (Maybe a)
|
|
Packit |
acf257 |
orNothing p = fmap Just p <++ return Nothing
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- This function duplicates old Network.URI.authority behaviour.
|
|
Packit |
acf257 |
uriToAuthorityString :: URI -> String
|
|
Packit |
acf257 |
uriToAuthorityString u = maybe "" uriAuthToString (uriAuthority u)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
uriAuthToString :: URIAuth -> String
|
|
Packit |
acf257 |
uriAuthToString ua =
|
|
Packit |
acf257 |
concat [ uriUserInfo ua
|
|
Packit |
acf257 |
, uriRegName ua
|
|
Packit |
acf257 |
, uriPort ua
|
|
Packit |
acf257 |
]
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
uriAuthPort :: Maybe URI -> URIAuth -> Int
|
|
Packit |
acf257 |
uriAuthPort mbURI u =
|
|
Packit |
acf257 |
case uriPort u of
|
|
Packit |
acf257 |
(':':s) -> readsOne id (default_port mbURI) s
|
|
Packit |
acf257 |
_ -> default_port mbURI
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
default_port Nothing = default_http
|
|
Packit |
acf257 |
default_port (Just url) =
|
|
Packit |
acf257 |
case map toLower $ uriScheme url of
|
|
Packit |
acf257 |
"http:" -> default_http
|
|
Packit |
acf257 |
"https:" -> default_https
|
|
Packit |
acf257 |
-- todo: refine
|
|
Packit |
acf257 |
_ -> default_http
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
default_http = 80
|
|
Packit |
acf257 |
default_https = 443
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
failHTTPS :: Monad m => URI -> m ()
|
|
Packit |
acf257 |
failHTTPS uri
|
|
Packit |
acf257 |
| map toLower (uriScheme uri) == "https:" = fail "https not supported"
|
|
Packit |
acf257 |
| otherwise = return ()
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- Fish out the authority from a possibly normalized Request, i.e.,
|
|
Packit |
acf257 |
-- the information may either be in the request's URI or inside
|
|
Packit |
acf257 |
-- the Host: header.
|
|
Packit |
acf257 |
reqURIAuth :: Request ty -> URIAuth
|
|
Packit |
acf257 |
reqURIAuth req =
|
|
Packit |
acf257 |
case uriAuthority (rqURI req) of
|
|
Packit |
acf257 |
Just ua -> ua
|
|
Packit |
acf257 |
_ -> case lookupHeader HdrHost (rqHeaders req) of
|
|
Packit |
acf257 |
Nothing -> error ("reqURIAuth: no URI authority for: " ++ show req)
|
|
Packit |
acf257 |
Just h ->
|
|
Packit |
acf257 |
case toHostPort h of
|
|
Packit |
acf257 |
(ht,p) -> URIAuth { uriUserInfo = ""
|
|
Packit |
acf257 |
, uriRegName = ht
|
|
Packit |
acf257 |
, uriPort = p
|
|
Packit |
acf257 |
}
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
-- Note: just in case you're wondering..the convention is to include the ':'
|
|
Packit |
acf257 |
-- in the port part..
|
|
Packit |
acf257 |
toHostPort h = break (==':') h
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-----------------------------------------------------------------
|
|
Packit |
acf257 |
------------------ HTTP Messages --------------------------------
|
|
Packit |
acf257 |
-----------------------------------------------------------------
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- Protocol version
|
|
Packit |
acf257 |
httpVersion :: String
|
|
Packit |
acf257 |
httpVersion = "HTTP/1.1"
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | The HTTP request method, to be used in the 'Request' object.
|
|
Packit |
acf257 |
-- We are missing a few of the stranger methods, but these are
|
|
Packit |
acf257 |
-- not really necessary until we add full TLS.
|
|
Packit |
acf257 |
data RequestMethod = HEAD | PUT | GET | POST | DELETE | OPTIONS | TRACE | CONNECT | Custom String
|
|
Packit |
acf257 |
deriving(Eq)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
instance Show RequestMethod where
|
|
Packit |
acf257 |
show x =
|
|
Packit |
acf257 |
case x of
|
|
Packit |
acf257 |
HEAD -> "HEAD"
|
|
Packit |
acf257 |
PUT -> "PUT"
|
|
Packit |
acf257 |
GET -> "GET"
|
|
Packit |
acf257 |
POST -> "POST"
|
|
Packit |
acf257 |
DELETE -> "DELETE"
|
|
Packit |
acf257 |
OPTIONS -> "OPTIONS"
|
|
Packit |
acf257 |
TRACE -> "TRACE"
|
|
Packit |
acf257 |
CONNECT -> "CONNECT"
|
|
Packit |
acf257 |
Custom c -> c
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
rqMethodMap :: [(String, RequestMethod)]
|
|
Packit |
acf257 |
rqMethodMap = [("HEAD", HEAD),
|
|
Packit |
acf257 |
("PUT", PUT),
|
|
Packit |
acf257 |
("GET", GET),
|
|
Packit |
acf257 |
("POST", POST),
|
|
Packit |
acf257 |
("DELETE", DELETE),
|
|
Packit |
acf257 |
("OPTIONS", OPTIONS),
|
|
Packit |
acf257 |
("TRACE", TRACE),
|
|
Packit |
acf257 |
("CONNECT", CONNECT)]
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
-- for backwards-ish compatibility; suggest
|
|
Packit |
acf257 |
-- migrating to new Req/Resp by adding type param.
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
type Request_String = Request String
|
|
Packit |
acf257 |
type Response_String = Response String
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- Hmm..I really want to use these for the record
|
|
Packit |
acf257 |
-- type, but it will upset codebases wanting to
|
|
Packit |
acf257 |
-- migrate (and live with using pre-HTTPbis versions.)
|
|
Packit |
acf257 |
type HTTPRequest a = Request a
|
|
Packit |
acf257 |
type HTTPResponse a = Response a
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | An HTTP Request.
|
|
Packit |
acf257 |
-- The 'Show' instance of this type is used for message serialisation,
|
|
Packit |
acf257 |
-- which means no body data is output.
|
|
Packit |
acf257 |
data Request a =
|
|
Packit |
acf257 |
Request { rqURI :: URI -- ^ might need changing in future
|
|
Packit |
acf257 |
-- 1) to support '*' uri in OPTIONS request
|
|
Packit |
acf257 |
-- 2) transparent support for both relative
|
|
Packit |
acf257 |
-- & absolute uris, although this should
|
|
Packit |
acf257 |
-- already work (leave scheme & host parts empty).
|
|
Packit |
acf257 |
, rqMethod :: RequestMethod
|
|
Packit |
acf257 |
, rqHeaders :: [Header]
|
|
Packit |
acf257 |
, rqBody :: a
|
|
Packit |
acf257 |
}
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- Notice that request body is not included,
|
|
Packit |
acf257 |
-- this show function is used to serialise
|
|
Packit |
acf257 |
-- a request for the transport link, we send
|
|
Packit |
acf257 |
-- the body separately where possible.
|
|
Packit |
acf257 |
instance Show (Request a) where
|
|
Packit |
acf257 |
show req@(Request u m h _) =
|
|
Packit |
acf257 |
show m ++ sp ++ alt_uri ++ sp ++ ver ++ crlf
|
|
Packit |
acf257 |
++ foldr (++) [] (map show (dropHttpVersion h)) ++ crlf
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
ver = fromMaybe httpVersion (getRequestVersion req)
|
|
Packit |
acf257 |
alt_uri = show $ if null (uriPath u) || head (uriPath u) /= '/'
|
|
Packit |
acf257 |
then u { uriPath = '/' : uriPath u }
|
|
Packit |
acf257 |
else u
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
instance HasHeaders (Request a) where
|
|
Packit |
acf257 |
getHeaders = rqHeaders
|
|
Packit |
acf257 |
setHeaders rq hdrs = rq { rqHeaders=hdrs }
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | For easy pattern matching, HTTP response codes @xyz@ are
|
|
Packit |
acf257 |
-- represented as @(x,y,z)@.
|
|
Packit |
acf257 |
type ResponseCode = (Int,Int,Int)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @ResponseData@ contains the head of a response payload;
|
|
Packit |
acf257 |
-- HTTP response code, accompanying text description + header
|
|
Packit |
acf257 |
-- fields.
|
|
Packit |
acf257 |
type ResponseData = (ResponseCode,String,[Header])
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @RequestData@ contains the head of a HTTP request; method,
|
|
Packit |
acf257 |
-- its URL along with the auxillary/supporting header data.
|
|
Packit |
acf257 |
type RequestData = (RequestMethod,URI,[Header])
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | An HTTP Response.
|
|
Packit |
acf257 |
-- The 'Show' instance of this type is used for message serialisation,
|
|
Packit |
acf257 |
-- which means no body data is output, additionally the output will
|
|
Packit |
acf257 |
-- show an HTTP version of 1.1 instead of the actual version returned
|
|
Packit |
acf257 |
-- by a server.
|
|
Packit |
acf257 |
data Response a =
|
|
Packit |
acf257 |
Response { rspCode :: ResponseCode
|
|
Packit |
acf257 |
, rspReason :: String
|
|
Packit |
acf257 |
, rspHeaders :: [Header]
|
|
Packit |
acf257 |
, rspBody :: a
|
|
Packit |
acf257 |
}
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- This is an invalid representation of a received response,
|
|
Packit |
acf257 |
-- since we have made the assumption that all responses are HTTP/1.1
|
|
Packit |
acf257 |
instance Show (Response a) where
|
|
Packit |
acf257 |
show rsp@(Response (a,b,c) reason headers _) =
|
|
Packit |
acf257 |
ver ++ ' ' : map intToDigit [a,b,c] ++ ' ' : reason ++ crlf
|
|
Packit |
acf257 |
++ foldr (++) [] (map show (dropHttpVersion headers)) ++ crlf
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
ver = fromMaybe httpVersion (getResponseVersion rsp)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
instance HasHeaders (Response a) where
|
|
Packit |
acf257 |
getHeaders = rspHeaders
|
|
Packit |
acf257 |
setHeaders rsp hdrs = rsp { rspHeaders=hdrs }
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
------------------------------------------------------------------
|
|
Packit |
acf257 |
------------------ Request Building ------------------------------
|
|
Packit |
acf257 |
------------------------------------------------------------------
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | Deprecated. Use 'defaultUserAgent'
|
|
Packit |
acf257 |
libUA :: String
|
|
Packit |
acf257 |
libUA = "hs-HTTP-4000.0.9"
|
|
Packit |
acf257 |
{-# DEPRECATED libUA "Use defaultUserAgent instead (but note the user agent name change)" #-}
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | A default user agent string. The string is @\"haskell-HTTP/$version\"@
|
|
Packit |
acf257 |
-- where @$version@ is the version of this HTTP package.
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
defaultUserAgent :: String
|
|
Packit |
acf257 |
defaultUserAgent = "haskell-HTTP/" ++ httpPackageVersion
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | The version of this HTTP package as a string, e.g. @\"4000.1.2\"@. This
|
|
Packit |
acf257 |
-- may be useful to include in a user agent string so that you can determine
|
|
Packit |
acf257 |
-- from server logs what version of this package HTTP clients are using.
|
|
Packit |
acf257 |
-- This can be useful for tracking down HTTP compatibility quirks.
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
httpPackageVersion :: String
|
|
Packit |
acf257 |
httpPackageVersion = showVersion Self.version
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
defaultGETRequest :: URI -> Request_String
|
|
Packit |
acf257 |
defaultGETRequest uri = defaultGETRequest_ uri
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
defaultGETRequest_ :: BufferType a => URI -> Request a
|
|
Packit |
acf257 |
defaultGETRequest_ uri = mkRequest GET uri
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | 'mkRequest method uri' constructs a well formed
|
|
Packit |
acf257 |
-- request for the given HTTP method and URI. It does not
|
|
Packit |
acf257 |
-- normalize the URI for the request _nor_ add the required
|
|
Packit |
acf257 |
-- Host: header. That is done either explicitly by the user
|
|
Packit |
acf257 |
-- or when requests are normalized prior to transmission.
|
|
Packit |
acf257 |
mkRequest :: BufferType ty => RequestMethod -> URI -> Request ty
|
|
Packit |
acf257 |
mkRequest meth uri = req
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
req =
|
|
Packit |
acf257 |
Request { rqURI = uri
|
|
Packit |
acf257 |
, rqBody = empty
|
|
Packit |
acf257 |
, rqHeaders = [ Header HdrContentLength "0"
|
|
Packit |
acf257 |
, Header HdrUserAgent defaultUserAgent
|
|
Packit |
acf257 |
]
|
|
Packit |
acf257 |
, rqMethod = meth
|
|
Packit |
acf257 |
}
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
empty = buf_empty (toBufOps req)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- set rqBody, Content-Type and Content-Length headers.
|
|
Packit |
acf257 |
setRequestBody :: Request_String -> (String, String) -> Request_String
|
|
Packit |
acf257 |
setRequestBody req (typ, body) = req' { rqBody=body }
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
req' = replaceHeader HdrContentType typ .
|
|
Packit |
acf257 |
replaceHeader HdrContentLength (show $ length body) $
|
|
Packit |
acf257 |
req
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
{-
|
|
Packit |
acf257 |
-- stub out the user info.
|
|
Packit |
acf257 |
updAuth = fmap (\ x -> x{uriUserInfo=""}) (uriAuthority uri)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
withHost =
|
|
Packit |
acf257 |
case uriToAuthorityString uri{uriAuthority=updAuth} of
|
|
Packit |
acf257 |
"" -> id
|
|
Packit |
acf257 |
h -> ((Header HdrHost h):)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
uri_req
|
|
Packit |
acf257 |
| forProxy = uri
|
|
Packit |
acf257 |
| otherwise = snd (splitRequestURI uri)
|
|
Packit |
acf257 |
-}
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
toBufOps :: BufferType a => Request a -> BufferOp a
|
|
Packit |
acf257 |
toBufOps _ = bufferOps
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-----------------------------------------------------------------
|
|
Packit |
acf257 |
------------------ Parsing --------------------------------------
|
|
Packit |
acf257 |
-----------------------------------------------------------------
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- Parsing a request
|
|
Packit |
acf257 |
parseRequestHead :: [String] -> Result RequestData
|
|
Packit |
acf257 |
parseRequestHead [] = Left ErrorClosed
|
|
Packit |
acf257 |
parseRequestHead (com:hdrs) = do
|
|
Packit |
acf257 |
(version,rqm,uri) <- requestCommand com (words com)
|
|
Packit |
acf257 |
hdrs' <- parseHeaders hdrs
|
|
Packit |
acf257 |
return (rqm,uri,withVer version hdrs')
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
withVer [] hs = hs
|
|
Packit |
acf257 |
withVer (h:_) hs = withVersion h hs
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
requestCommand l _yes@(rqm:uri:version) =
|
|
Packit |
acf257 |
case (parseURIReference uri, lookup rqm rqMethodMap) of
|
|
Packit |
acf257 |
(Just u, Just r) -> return (version,r,u)
|
|
Packit |
acf257 |
(Just u, Nothing) -> return (version,Custom rqm,u)
|
|
Packit |
acf257 |
_ -> parse_err l
|
|
Packit |
acf257 |
requestCommand l _
|
|
Packit |
acf257 |
| null l = failWith ErrorClosed
|
|
Packit |
acf257 |
| otherwise = parse_err l
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
parse_err l = responseParseError "parseRequestHead"
|
|
Packit |
acf257 |
("Request command line parse failure: " ++ l)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- Parsing a response
|
|
Packit |
acf257 |
parseResponseHead :: [String] -> Result ResponseData
|
|
Packit |
acf257 |
parseResponseHead [] = failWith ErrorClosed
|
|
Packit |
acf257 |
parseResponseHead (sts:hdrs) = do
|
|
Packit |
acf257 |
(version,code,reason) <- responseStatus sts (words sts)
|
|
Packit |
acf257 |
hdrs' <- parseHeaders hdrs
|
|
Packit |
acf257 |
return (code,reason, withVersion version hdrs')
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
responseStatus _l _yes@(version:code:reason) =
|
|
Packit |
acf257 |
return (version,match code,concatMap (++" ") reason)
|
|
Packit |
acf257 |
responseStatus l _no
|
|
Packit |
acf257 |
| null l = failWith ErrorClosed -- an assumption
|
|
Packit |
acf257 |
| otherwise = parse_err l
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
parse_err l =
|
|
Packit |
acf257 |
responseParseError
|
|
Packit |
acf257 |
"parseResponseHead"
|
|
Packit |
acf257 |
("Response status line parse failure: " ++ l)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
match [a,b,c] = (digitToInt a,
|
|
Packit |
acf257 |
digitToInt b,
|
|
Packit |
acf257 |
digitToInt c)
|
|
Packit |
acf257 |
match _ = (-1,-1,-1) -- will create appropriate behaviour
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- To avoid changing the @RequestData@ and @ResponseData@ types
|
|
Packit |
acf257 |
-- just for this (and the upstream backwards compat. woes that
|
|
Packit |
acf257 |
-- will result in), encode version info as a custom header.
|
|
Packit |
acf257 |
-- Used by 'parseResponseData' and 'parseRequestData'.
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
-- Note: the Request and Response types do not currently represent
|
|
Packit |
acf257 |
-- the version info explicitly in their record types. You have to use
|
|
Packit |
acf257 |
-- {get,set}{Request,Response}Version for that.
|
|
Packit |
acf257 |
withVersion :: String -> [Header] -> [Header]
|
|
Packit |
acf257 |
withVersion v hs
|
|
Packit |
acf257 |
| v == httpVersion = hs -- don't bother adding it if the default.
|
|
Packit |
acf257 |
| otherwise = (Header (HdrCustom "X-HTTP-Version") v) : hs
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @getRequestVersion req@ returns the HTTP protocol version of
|
|
Packit |
acf257 |
-- the request @req@. If @Nothing@, the default 'httpVersion' can be assumed.
|
|
Packit |
acf257 |
getRequestVersion :: Request a -> Maybe String
|
|
Packit |
acf257 |
getRequestVersion r = getHttpVersion r
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @setRequestVersion v req@ returns a new request, identical to
|
|
Packit |
acf257 |
-- @req@, but with its HTTP version set to @v@.
|
|
Packit |
acf257 |
setRequestVersion :: String -> Request a -> Request a
|
|
Packit |
acf257 |
setRequestVersion s r = setHttpVersion r s
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @getResponseVersion rsp@ returns the HTTP protocol version of
|
|
Packit |
acf257 |
-- the response @rsp@. If @Nothing@, the default 'httpVersion' can be
|
|
Packit |
acf257 |
-- assumed.
|
|
Packit |
acf257 |
getResponseVersion :: Response a -> Maybe String
|
|
Packit |
acf257 |
getResponseVersion r = getHttpVersion r
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @setResponseVersion v rsp@ returns a new response, identical to
|
|
Packit |
acf257 |
-- @rsp@, but with its HTTP version set to @v@.
|
|
Packit |
acf257 |
setResponseVersion :: String -> Response a -> Response a
|
|
Packit |
acf257 |
setResponseVersion s r = setHttpVersion r s
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- internal functions for accessing HTTP-version info in
|
|
Packit |
acf257 |
-- requests and responses. Not exported as it exposes ho
|
|
Packit |
acf257 |
-- version info is represented internally.
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
getHttpVersion :: HasHeaders a => a -> Maybe String
|
|
Packit |
acf257 |
getHttpVersion r =
|
|
Packit |
acf257 |
fmap toVersion $
|
|
Packit |
acf257 |
find isHttpVersion $
|
|
Packit |
acf257 |
getHeaders r
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
toVersion (Header _ x) = x
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
setHttpVersion :: HasHeaders a => a -> String -> a
|
|
Packit |
acf257 |
setHttpVersion r v =
|
|
Packit |
acf257 |
setHeaders r $
|
|
Packit |
acf257 |
withVersion v $
|
|
Packit |
acf257 |
dropHttpVersion $
|
|
Packit |
acf257 |
getHeaders r
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
dropHttpVersion :: [Header] -> [Header]
|
|
Packit |
acf257 |
dropHttpVersion hs = filter (not.isHttpVersion) hs
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
isHttpVersion :: Header -> Bool
|
|
Packit |
acf257 |
isHttpVersion (Header (HdrCustom "X-HTTP-Version") _) = True
|
|
Packit |
acf257 |
isHttpVersion _ = False
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-----------------------------------------------------------------
|
|
Packit |
acf257 |
------------------ HTTP Send / Recv ----------------------------------
|
|
Packit |
acf257 |
-----------------------------------------------------------------
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
data ResponseNextStep
|
|
Packit |
acf257 |
= Continue
|
|
Packit |
acf257 |
| Retry
|
|
Packit |
acf257 |
| Done
|
|
Packit |
acf257 |
| ExpectEntity
|
|
Packit |
acf257 |
| DieHorribly String
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
matchResponse :: RequestMethod -> ResponseCode -> ResponseNextStep
|
|
Packit |
acf257 |
matchResponse rqst rsp =
|
|
Packit |
acf257 |
case rsp of
|
|
Packit |
acf257 |
(1,0,0) -> Continue
|
|
Packit |
acf257 |
(1,0,1) -> Done -- upgrade to TLS
|
|
Packit |
acf257 |
(1,_,_) -> Continue -- default
|
|
Packit |
acf257 |
(2,0,4) -> Done
|
|
Packit |
acf257 |
(2,0,5) -> Done
|
|
Packit |
acf257 |
(2,_,_) -> ans
|
|
Packit |
acf257 |
(3,0,4) -> Done
|
|
Packit |
acf257 |
(3,0,5) -> Done
|
|
Packit |
acf257 |
(3,_,_) -> ans
|
|
Packit |
acf257 |
(4,1,7) -> Retry -- Expectation failed
|
|
Packit |
acf257 |
(4,_,_) -> ans
|
|
Packit |
acf257 |
(5,_,_) -> ans
|
|
Packit |
acf257 |
(a,b,c) -> DieHorribly ("Response code " ++ map intToDigit [a,b,c] ++ " not recognised")
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
ans | rqst == HEAD = Done
|
|
Packit |
acf257 |
| otherwise = ExpectEntity
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-----------------------------------------------------------------
|
|
Packit |
acf257 |
------------------ A little friendly funtionality ---------------
|
|
Packit |
acf257 |
-----------------------------------------------------------------
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
{-
|
|
Packit |
acf257 |
I had a quick look around but couldn't find any RFC about
|
|
Packit |
acf257 |
the encoding of data on the query string. I did find an
|
|
Packit |
acf257 |
IETF memo, however, so this is how I justify the urlEncode
|
|
Packit |
acf257 |
and urlDecode methods.
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
Doc name: draft-tiwari-appl-wxxx-forms-01.txt (look on www.ietf.org)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
Reserved chars: ";", "/", "?", ":", "@", "&", "=", "+", ",", and "$" are reserved.
|
|
Packit |
acf257 |
Unwise: "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"
|
|
Packit |
acf257 |
URI delims: "<" | ">" | "#" | "%" | <">
|
|
Packit |
acf257 |
Unallowed ASCII: <US-ASCII coded characters 00-1F and 7F hexadecimal>
|
|
Packit |
acf257 |
<US-ASCII coded character 20 hexadecimal>
|
|
Packit |
acf257 |
Also unallowed: any non-us-ascii character
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
Escape method: char -> '%' a b where a, b :: Hex digits
|
|
Packit |
acf257 |
-}
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
replacement_character :: Char
|
|
Packit |
acf257 |
replacement_character = '\xfffd'
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | Encode a single Haskell Char to a list of Word8 values, in UTF8 format.
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
-- Shamelessly stolen from utf-8string-0.3.7
|
|
Packit |
acf257 |
encodeChar :: Char -> [Word8]
|
|
Packit |
acf257 |
encodeChar = map fromIntegral . go . ord
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
go oc
|
|
Packit |
acf257 |
| oc <= 0x7f = [oc]
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
| oc <= 0x7ff = [ 0xc0 + (oc `shiftR` 6)
|
|
Packit |
acf257 |
, 0x80 + oc .&. 0x3f
|
|
Packit |
acf257 |
]
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
| oc <= 0xffff = [ 0xe0 + (oc `shiftR` 12)
|
|
Packit |
acf257 |
, 0x80 + ((oc `shiftR` 6) .&. 0x3f)
|
|
Packit |
acf257 |
, 0x80 + oc .&. 0x3f
|
|
Packit |
acf257 |
]
|
|
Packit |
acf257 |
| otherwise = [ 0xf0 + (oc `shiftR` 18)
|
|
Packit |
acf257 |
, 0x80 + ((oc `shiftR` 12) .&. 0x3f)
|
|
Packit |
acf257 |
, 0x80 + ((oc `shiftR` 6) .&. 0x3f)
|
|
Packit |
acf257 |
, 0x80 + oc .&. 0x3f
|
|
Packit |
acf257 |
]
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | Decode a UTF8 string packed into a list of Word8 values, directly to String
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
-- Shamelessly stolen from utf-8string-0.3.7
|
|
Packit |
acf257 |
decode :: [Word8] -> String
|
|
Packit |
acf257 |
decode [ ] = ""
|
|
Packit |
acf257 |
decode (c:cs)
|
|
Packit |
acf257 |
| c < 0x80 = chr (fromEnum c) : decode cs
|
|
Packit |
acf257 |
| c < 0xc0 = replacement_character : decode cs
|
|
Packit |
acf257 |
| c < 0xe0 = multi1
|
|
Packit |
acf257 |
| c < 0xf0 = multi_byte 2 0xf 0x800
|
|
Packit |
acf257 |
| c < 0xf8 = multi_byte 3 0x7 0x10000
|
|
Packit |
acf257 |
| c < 0xfc = multi_byte 4 0x3 0x200000
|
|
Packit |
acf257 |
| c < 0xfe = multi_byte 5 0x1 0x4000000
|
|
Packit |
acf257 |
| otherwise = replacement_character : decode cs
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
multi1 = case cs of
|
|
Packit |
acf257 |
c1 : ds | c1 .&. 0xc0 == 0x80 ->
|
|
Packit |
acf257 |
let d = ((fromEnum c .&. 0x1f) `shiftL` 6) .|. fromEnum (c1 .&. 0x3f)
|
|
Packit |
acf257 |
in if d >= 0x000080 then toEnum d : decode ds
|
|
Packit |
acf257 |
else replacement_character : decode ds
|
|
Packit |
acf257 |
_ -> replacement_character : decode cs
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
multi_byte :: Int -> Word8 -> Int -> [Char]
|
|
Packit |
acf257 |
multi_byte i mask overlong = aux i cs (fromEnum (c .&. mask))
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
aux 0 rs acc
|
|
Packit |
acf257 |
| overlong <= acc && acc <= 0x10ffff &&
|
|
Packit |
acf257 |
(acc < 0xd800 || 0xdfff < acc) &&
|
|
Packit |
acf257 |
(acc < 0xfffe || 0xffff < acc) = chr acc : decode rs
|
|
Packit |
acf257 |
| otherwise = replacement_character : decode rs
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
aux n (r:rs) acc
|
|
Packit |
acf257 |
| r .&. 0xc0 == 0x80 = aux (n-1) rs
|
|
Packit |
acf257 |
$ shiftL acc 6 .|. fromEnum (r .&. 0x3f)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
aux _ rs _ = replacement_character : decode rs
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- This function is a bit funny because potentially the input String could contain some actual Unicode
|
|
Packit |
acf257 |
-- characters (though this shouldn't happen for most use cases), so we have to preserve those characters
|
|
Packit |
acf257 |
-- while simultaneously decoding any UTF-8 data
|
|
Packit |
acf257 |
urlDecode :: String -> String
|
|
Packit |
acf257 |
urlDecode = go []
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
go bs ('%':a:b:rest) = go (fromIntegral (16 * digitToInt a + digitToInt b) : bs) rest
|
|
Packit |
acf257 |
go bs (h:t) | fromEnum h < 256 = go (fromIntegral (fromEnum h) : bs) t -- Treat ASCII as just another byte of UTF-8
|
|
Packit |
acf257 |
go [] [] = []
|
|
Packit |
acf257 |
go [] (h:t) = h : go [] t -- h >= 256, so can't be part of any UTF-8 byte sequence
|
|
Packit |
acf257 |
go bs rest = decode (reverse bs) ++ go [] rest
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
urlEncode :: String -> String
|
|
Packit |
acf257 |
urlEncode [] = []
|
|
Packit |
acf257 |
urlEncode (ch:t)
|
|
Packit |
acf257 |
| (isAscii ch && isAlphaNum ch) || ch `elem` "-_.~" = ch : urlEncode t
|
|
Packit |
acf257 |
| not (isAscii ch) = foldr escape (urlEncode t) (encodeChar ch)
|
|
Packit |
acf257 |
| otherwise = escape (fromIntegral (fromEnum ch)) (urlEncode t)
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
escape b rs = '%':showH (b `div` 16) (showH (b `mod` 16) rs)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
showH :: Word8 -> String -> String
|
|
Packit |
acf257 |
showH x xs
|
|
Packit |
acf257 |
| x <= 9 = to (o_0 + x) : xs
|
|
Packit |
acf257 |
| otherwise = to (o_A + (x-10)) : xs
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
to = toEnum . fromIntegral
|
|
Packit |
acf257 |
fro = fromIntegral . fromEnum
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
o_0 = fro '0'
|
|
Packit |
acf257 |
o_A = fro 'A'
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- Encode form variables, useable in either the
|
|
Packit |
acf257 |
-- query part of a URI, or the body of a POST request.
|
|
Packit |
acf257 |
-- I have no source for this information except experience,
|
|
Packit |
acf257 |
-- this sort of encoding worked fine in CGI programming.
|
|
Packit |
acf257 |
urlEncodeVars :: [(String,String)] -> String
|
|
Packit |
acf257 |
urlEncodeVars ((n,v):t) =
|
|
Packit |
acf257 |
let (same,diff) = partition ((==n) . fst) t
|
|
Packit |
acf257 |
in urlEncode n ++ '=' : foldl (\x y -> x ++ ',' : urlEncode y) (urlEncode $ v) (map snd same)
|
|
Packit |
acf257 |
++ urlEncodeRest diff
|
|
Packit |
acf257 |
where urlEncodeRest [] = []
|
|
Packit |
acf257 |
urlEncodeRest diff = '&' : urlEncodeVars diff
|
|
Packit |
acf257 |
urlEncodeVars [] = []
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @getAuth req@ fishes out the authority portion of the URL in a request's @Host@
|
|
Packit |
acf257 |
-- header.
|
|
Packit |
acf257 |
getAuth :: Monad m => Request ty -> m URIAuthority
|
|
Packit |
acf257 |
getAuth r =
|
|
Packit |
acf257 |
-- ToDo: verify that Network.URI functionality doesn't take care of this (now.)
|
|
Packit |
acf257 |
case parseURIAuthority auth of
|
|
Packit |
acf257 |
Just x -> return x
|
|
Packit |
acf257 |
Nothing -> fail $ "Network.HTTP.Base.getAuth: Error parsing URI authority '" ++ auth ++ "'"
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
auth = maybe (uriToAuthorityString uri) id (findHeader HdrHost r)
|
|
Packit |
acf257 |
uri = rqURI r
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
{-# DEPRECATED normalizeRequestURI "Please use Network.HTTP.Base.normalizeRequest instead" #-}
|
|
Packit |
acf257 |
normalizeRequestURI :: Bool{-do close-} -> {-URI-}String -> Request ty -> Request ty
|
|
Packit |
acf257 |
normalizeRequestURI doClose h r =
|
|
Packit |
acf257 |
(if doClose then replaceHeader HdrConnection "close" else id) $
|
|
Packit |
acf257 |
insertHeaderIfMissing HdrHost h $
|
|
Packit |
acf257 |
r { rqURI = (rqURI r){ uriScheme = ""
|
|
Packit |
acf257 |
, uriAuthority = Nothing
|
|
Packit |
acf257 |
}}
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @NormalizeRequestOptions@ brings together the various defaulting\/normalization options
|
|
Packit |
acf257 |
-- over 'Request's. Use 'defaultNormalizeRequestOptions' for the standard selection of option
|
|
Packit |
acf257 |
data NormalizeRequestOptions ty
|
|
Packit |
acf257 |
= NormalizeRequestOptions
|
|
Packit |
acf257 |
{ normDoClose :: Bool
|
|
Packit |
acf257 |
, normForProxy :: Bool
|
|
Packit |
acf257 |
, normUserAgent :: Maybe String
|
|
Packit |
acf257 |
, normCustoms :: [RequestNormalizer ty]
|
|
Packit |
acf257 |
}
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @RequestNormalizer@ is the shape of a (pure) function that rewrites
|
|
Packit |
acf257 |
-- a request into some normalized form.
|
|
Packit |
acf257 |
type RequestNormalizer ty = NormalizeRequestOptions ty -> Request ty -> Request ty
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
defaultNormalizeRequestOptions :: NormalizeRequestOptions ty
|
|
Packit |
acf257 |
defaultNormalizeRequestOptions = NormalizeRequestOptions
|
|
Packit |
acf257 |
{ normDoClose = False
|
|
Packit |
acf257 |
, normForProxy = False
|
|
Packit |
acf257 |
, normUserAgent = Just defaultUserAgent
|
|
Packit |
acf257 |
, normCustoms = []
|
|
Packit |
acf257 |
}
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @normalizeRequest opts req@ is the entry point to use to normalize your
|
|
Packit |
acf257 |
-- request prior to transmission (or other use.) Normalization is controlled
|
|
Packit |
acf257 |
-- via the @NormalizeRequestOptions@ record.
|
|
Packit |
acf257 |
normalizeRequest :: NormalizeRequestOptions ty
|
|
Packit |
acf257 |
-> Request ty
|
|
Packit |
acf257 |
-> Request ty
|
|
Packit |
acf257 |
normalizeRequest opts req = foldr (\ f -> f opts) req normalizers
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
--normalizers :: [RequestNormalizer ty]
|
|
Packit |
acf257 |
normalizers =
|
|
Packit |
acf257 |
( normalizeHostURI
|
|
Packit |
acf257 |
: normalizeBasicAuth
|
|
Packit |
acf257 |
: normalizeConnectionClose
|
|
Packit |
acf257 |
: normalizeUserAgent
|
|
Packit |
acf257 |
: normCustoms opts
|
|
Packit |
acf257 |
)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @normalizeUserAgent ua x req@ augments the request @req@ with
|
|
Packit |
acf257 |
-- a @User-Agent: ua@ header if @req@ doesn't already have a
|
|
Packit |
acf257 |
-- a @User-Agent:@ set.
|
|
Packit |
acf257 |
normalizeUserAgent :: RequestNormalizer ty
|
|
Packit |
acf257 |
normalizeUserAgent opts req =
|
|
Packit |
acf257 |
case normUserAgent opts of
|
|
Packit |
acf257 |
Nothing -> req
|
|
Packit |
acf257 |
Just ua ->
|
|
Packit |
acf257 |
case findHeader HdrUserAgent req of
|
|
Packit |
acf257 |
Just u | u /= defaultUserAgent -> req
|
|
Packit |
acf257 |
_ -> replaceHeader HdrUserAgent ua req
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @normalizeConnectionClose opts req@ sets the header @Connection: close@
|
|
Packit |
acf257 |
-- to indicate one-shot behavior iff @normDoClose@ is @True@. i.e., it then
|
|
Packit |
acf257 |
-- _replaces_ any an existing @Connection:@ header in @req@.
|
|
Packit |
acf257 |
normalizeConnectionClose :: RequestNormalizer ty
|
|
Packit |
acf257 |
normalizeConnectionClose opts req
|
|
Packit |
acf257 |
| normDoClose opts = replaceHeader HdrConnection "close" req
|
|
Packit |
acf257 |
| otherwise = req
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @normalizeBasicAuth opts req@ sets the header @Authorization: Basic...@
|
|
Packit |
acf257 |
-- if the "user:pass@" part is present in the "http://user:pass@host/path"
|
|
Packit |
acf257 |
-- of the URI. If Authorization header was present already it is not replaced.
|
|
Packit |
acf257 |
normalizeBasicAuth :: RequestNormalizer ty
|
|
Packit |
acf257 |
normalizeBasicAuth _ req =
|
|
Packit |
acf257 |
case getAuth req of
|
|
Packit |
acf257 |
Just uriauth ->
|
|
Packit |
acf257 |
case (user uriauth, password uriauth) of
|
|
Packit |
acf257 |
(Just u, Just p) ->
|
|
Packit |
acf257 |
insertHeaderIfMissing HdrAuthorization astr req
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
astr = "Basic " ++ base64encode (u ++ ":" ++ p)
|
|
Packit |
acf257 |
base64encode = Base64.encode . stringToOctets :: String -> String
|
|
Packit |
acf257 |
stringToOctets = map (fromIntegral . fromEnum) :: String -> [Word8]
|
|
Packit |
acf257 |
(_, _) -> req
|
|
Packit |
acf257 |
Nothing ->req
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @normalizeHostURI forProxy req@ rewrites your request to have it
|
|
Packit |
acf257 |
-- follow the expected formats by the receiving party (proxy or server.)
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
normalizeHostURI :: RequestNormalizer ty
|
|
Packit |
acf257 |
normalizeHostURI opts req =
|
|
Packit |
acf257 |
case splitRequestURI uri of
|
|
Packit |
acf257 |
("",_uri_abs)
|
|
Packit |
acf257 |
| forProxy ->
|
|
Packit |
acf257 |
case findHeader HdrHost req of
|
|
Packit |
acf257 |
Nothing -> req -- no host/authority in sight..not much we can do.
|
|
Packit |
acf257 |
Just h -> req{rqURI=uri{ uriAuthority=Just URIAuth{uriUserInfo="", uriRegName=hst, uriPort=pNum}
|
|
Packit |
acf257 |
, uriScheme=if (null (uriScheme uri)) then "http" else uriScheme uri
|
|
Packit |
acf257 |
}}
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
hst = case span (/='@') user_hst of
|
|
Packit |
acf257 |
(as,'@':bs) ->
|
|
Packit |
acf257 |
case span (/=':') as of
|
|
Packit |
acf257 |
(_,_:_) -> bs
|
|
Packit |
acf257 |
_ -> user_hst
|
|
Packit |
acf257 |
_ -> user_hst
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
(user_hst, pNum) =
|
|
Packit |
acf257 |
case span isDigit (reverse h) of
|
|
Packit |
acf257 |
(ds,':':bs) -> (reverse bs, ':':reverse ds)
|
|
Packit |
acf257 |
_ -> (h,"")
|
|
Packit |
acf257 |
| otherwise ->
|
|
Packit |
acf257 |
case findHeader HdrHost req of
|
|
Packit |
acf257 |
Nothing -> req -- no host/authority in sight..not much we can do...complain?
|
|
Packit |
acf257 |
Just{} -> req
|
|
Packit |
acf257 |
(h,uri_abs)
|
|
Packit |
acf257 |
| forProxy -> insertHeaderIfMissing HdrHost h req
|
|
Packit |
acf257 |
| otherwise -> replaceHeader HdrHost h req{rqURI=uri_abs} -- Note: _not_ stubbing out user:pass
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
uri0 = rqURI req
|
|
Packit |
acf257 |
-- stub out the user:pass
|
|
Packit |
acf257 |
uri = uri0{uriAuthority=fmap (\ x -> x{uriUserInfo=""}) (uriAuthority uri0)}
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
forProxy = normForProxy opts
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
{- Comments re: above rewriting:
|
|
Packit |
acf257 |
RFC 2616, section 5.1.2:
|
|
Packit |
acf257 |
"The most common form of Request-URI is that used to identify a
|
|
Packit |
acf257 |
resource on an origin server or gateway. In this case the absolute
|
|
Packit |
acf257 |
path of the URI MUST be transmitted (see section 3.2.1, abs_path) as
|
|
Packit |
acf257 |
the Request-URI, and the network location of the URI (authority) MUST
|
|
Packit |
acf257 |
be transmitted in a Host header field."
|
|
Packit |
acf257 |
We assume that this is the case, so we take the host name from
|
|
Packit |
acf257 |
the Host header if there is one, otherwise from the request-URI.
|
|
Packit |
acf257 |
Then we make the request-URI an abs_path and make sure that there
|
|
Packit |
acf257 |
is a Host header.
|
|
Packit |
acf257 |
-}
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
splitRequestURI :: URI -> ({-authority-}String, URI)
|
|
Packit |
acf257 |
splitRequestURI uri = (uriToAuthorityString uri, uri{uriScheme="", uriAuthority=Nothing})
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- Adds a Host header if one is NOT ALREADY PRESENT..
|
|
Packit |
acf257 |
{-# DEPRECATED normalizeHostHeader "Please use Network.HTTP.Base.normalizeRequest instead" #-}
|
|
Packit |
acf257 |
normalizeHostHeader :: Request ty -> Request ty
|
|
Packit |
acf257 |
normalizeHostHeader rq =
|
|
Packit |
acf257 |
insertHeaderIfMissing HdrHost
|
|
Packit |
acf257 |
(uriToAuthorityString $ rqURI rq)
|
|
Packit |
acf257 |
rq
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- Looks for a "Connection" header with the value "close".
|
|
Packit |
acf257 |
-- Returns True when this is found.
|
|
Packit |
acf257 |
findConnClose :: [Header] -> Bool
|
|
Packit |
acf257 |
findConnClose hdrs =
|
|
Packit |
acf257 |
maybe False
|
|
Packit |
acf257 |
(\ x -> map toLower (trim x) == "close")
|
|
Packit |
acf257 |
(lookupHeader HdrConnection hdrs)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | Used when we know exactly how many bytes to expect.
|
|
Packit |
acf257 |
linearTransfer :: (Int -> IO (Result a)) -> Int -> IO (Result ([Header],a))
|
|
Packit |
acf257 |
linearTransfer readBlk n = fmapE (\str -> Right ([],str)) (readBlk n)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | Used when nothing about data is known,
|
|
Packit |
acf257 |
-- Unfortunately waiting for a socket closure
|
|
Packit |
acf257 |
-- causes bad behaviour. Here we just
|
|
Packit |
acf257 |
-- take data once and give up the rest.
|
|
Packit |
acf257 |
hopefulTransfer :: BufferOp a
|
|
Packit |
acf257 |
-> IO (Result a)
|
|
Packit |
acf257 |
-> [a]
|
|
Packit |
acf257 |
-> IO (Result ([Header],a))
|
|
Packit |
acf257 |
hopefulTransfer bufOps readL strs
|
|
Packit |
acf257 |
= readL >>=
|
|
Packit |
acf257 |
either (\v -> return $ Left v)
|
|
Packit |
acf257 |
(\more -> if (buf_isEmpty bufOps more)
|
|
Packit |
acf257 |
then return (Right ([], buf_concat bufOps $ reverse strs))
|
|
Packit |
acf257 |
else hopefulTransfer bufOps readL (more:strs))
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | A necessary feature of HTTP\/1.1
|
|
Packit |
acf257 |
-- Also the only transfer variety likely to
|
|
Packit |
acf257 |
-- return any footers.
|
|
Packit |
acf257 |
chunkedTransfer :: BufferOp a
|
|
Packit |
acf257 |
-> IO (Result a)
|
|
Packit |
acf257 |
-> (Int -> IO (Result a))
|
|
Packit |
acf257 |
-> IO (Result ([Header], a))
|
|
Packit |
acf257 |
chunkedTransfer bufOps readL readBlk = chunkedTransferC bufOps readL readBlk [] 0
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
chunkedTransferC :: BufferOp a
|
|
Packit |
acf257 |
-> IO (Result a)
|
|
Packit |
acf257 |
-> (Int -> IO (Result a))
|
|
Packit |
acf257 |
-> [a]
|
|
Packit |
acf257 |
-> Int
|
|
Packit |
acf257 |
-> IO (Result ([Header], a))
|
|
Packit |
acf257 |
chunkedTransferC bufOps readL readBlk acc n = do
|
|
Packit |
acf257 |
v <- readL
|
|
Packit |
acf257 |
case v of
|
|
Packit |
acf257 |
Left e -> return (Left e)
|
|
Packit |
acf257 |
Right line
|
|
Packit |
acf257 |
| size == 0 ->
|
|
Packit |
acf257 |
-- last chunk read; look for trailing headers..
|
|
Packit |
acf257 |
fmapE (\ strs -> do
|
|
Packit |
acf257 |
ftrs <- parseHeaders (map (buf_toStr bufOps) strs)
|
|
Packit |
acf257 |
-- insert (computed) Content-Length header.
|
|
Packit |
acf257 |
let ftrs' = Header HdrContentLength (show n) : ftrs
|
|
Packit |
acf257 |
return (ftrs',buf_concat bufOps (reverse acc)))
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
(readTillEmpty2 bufOps readL [])
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
| otherwise -> do
|
|
Packit |
acf257 |
some <- readBlk size
|
|
Packit |
acf257 |
case some of
|
|
Packit |
acf257 |
Left e -> return (Left e)
|
|
Packit |
acf257 |
Right cdata -> do
|
|
Packit |
acf257 |
_ <- readL -- CRLF is mandated after the chunk block; ToDo: check that the line is empty.?
|
|
Packit |
acf257 |
chunkedTransferC bufOps readL readBlk (cdata:acc) (n+size)
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
size
|
|
Packit |
acf257 |
| buf_isEmpty bufOps line = 0
|
|
Packit |
acf257 |
| otherwise =
|
|
Packit |
acf257 |
case readHex (buf_toStr bufOps line) of
|
|
Packit |
acf257 |
(hx,_):_ -> hx
|
|
Packit |
acf257 |
_ -> 0
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | Maybe in the future we will have a sensible thing
|
|
Packit |
acf257 |
-- to do here, at that time we might want to change
|
|
Packit |
acf257 |
-- the name.
|
|
Packit |
acf257 |
uglyDeathTransfer :: String -> IO (Result ([Header],a))
|
|
Packit |
acf257 |
uglyDeathTransfer loc = return (responseParseError loc "Unknown Transfer-Encoding")
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | Remove leading crlfs then call readTillEmpty2 (not required by RFC)
|
|
Packit |
acf257 |
readTillEmpty1 :: BufferOp a
|
|
Packit |
acf257 |
-> IO (Result a)
|
|
Packit |
acf257 |
-> IO (Result [a])
|
|
Packit |
acf257 |
readTillEmpty1 bufOps readL =
|
|
Packit |
acf257 |
readL >>=
|
|
Packit |
acf257 |
either (return . Left)
|
|
Packit |
acf257 |
(\ s ->
|
|
Packit |
acf257 |
if buf_isLineTerm bufOps s
|
|
Packit |
acf257 |
then readTillEmpty1 bufOps readL
|
|
Packit |
acf257 |
else readTillEmpty2 bufOps readL [s])
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | Read lines until an empty line (CRLF),
|
|
Packit |
acf257 |
-- also accepts a connection close as end of
|
|
Packit |
acf257 |
-- input, which is not an HTTP\/1.1 compliant
|
|
Packit |
acf257 |
-- thing to do - so probably indicates an
|
|
Packit |
acf257 |
-- error condition.
|
|
Packit |
acf257 |
readTillEmpty2 :: BufferOp a
|
|
Packit |
acf257 |
-> IO (Result a)
|
|
Packit |
acf257 |
-> [a]
|
|
Packit |
acf257 |
-> IO (Result [a])
|
|
Packit |
acf257 |
readTillEmpty2 bufOps readL list =
|
|
Packit |
acf257 |
readL >>=
|
|
Packit |
acf257 |
either (return . Left)
|
|
Packit |
acf257 |
(\ s ->
|
|
Packit |
acf257 |
if buf_isLineTerm bufOps s || buf_isEmpty bufOps s
|
|
Packit |
acf257 |
then return (Right $ reverse (s:list))
|
|
Packit |
acf257 |
else readTillEmpty2 bufOps readL (s:list))
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
-- Misc
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @catchIO a h@ handles IO action exceptions throughout codebase; version-specific
|
|
Packit |
acf257 |
-- tweaks better go here.
|
|
Packit |
acf257 |
catchIO :: IO a -> (IOException -> IO a) -> IO a
|
|
Packit |
acf257 |
catchIO a h = Exception.catch a h
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
catchIO_ :: IO a -> IO a -> IO a
|
|
Packit |
acf257 |
catchIO_ a h = Exception.catch a (\(_ :: IOException) -> h)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
responseParseError :: String -> String -> Result a
|
|
Packit |
acf257 |
responseParseError loc v = failWith (ErrorParse (loc ++ ' ':v))
|