|
Packit |
acf257 |
-----------------------------------------------------------------------------
|
|
Packit |
acf257 |
-- |
|
|
Packit |
acf257 |
-- Module : Network.HTTP.Headers
|
|
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 |
-- This module provides the data types for representing HTTP headers, and
|
|
Packit |
acf257 |
-- operations for looking up header values and working with sequences of
|
|
Packit |
acf257 |
-- header values in 'Request's and 'Response's. To avoid having to provide
|
|
Packit |
acf257 |
-- separate set of operations for doing so, we introduce a type class 'HasHeaders'
|
|
Packit |
acf257 |
-- to facilitate writing such processing using overloading instead.
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
-----------------------------------------------------------------------------
|
|
Packit |
acf257 |
module Network.HTTP.Headers
|
|
Packit |
acf257 |
( HasHeaders(..) -- type class
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, Header(..)
|
|
Packit |
acf257 |
, mkHeader -- :: HeaderName -> String -> Header
|
|
Packit |
acf257 |
, hdrName -- :: Header -> HeaderName
|
|
Packit |
acf257 |
, hdrValue -- :: Header -> String
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, HeaderName(..)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, insertHeader -- :: HasHeaders a => HeaderName -> String -> a -> a
|
|
Packit |
acf257 |
, insertHeaderIfMissing -- :: HasHeaders a => HeaderName -> String -> a -> a
|
|
Packit |
acf257 |
, insertHeaders -- :: HasHeaders a => [Header] -> a -> a
|
|
Packit |
acf257 |
, retrieveHeaders -- :: HasHeaders a => HeaderName -> a -> [Header]
|
|
Packit |
acf257 |
, replaceHeader -- :: HasHeaders a => HeaderName -> String -> a -> a
|
|
Packit |
acf257 |
, findHeader -- :: HasHeaders a => HeaderName -> a -> Maybe String
|
|
Packit |
acf257 |
, lookupHeader -- :: HeaderName -> [Header] -> Maybe String
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, parseHeader -- :: parseHeader :: String -> Result Header
|
|
Packit |
acf257 |
, parseHeaders -- :: [String] -> Result [Header]
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, headerMap -- :: [(String, HeaderName)]
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
, HeaderSetter
|
|
Packit |
acf257 |
) where
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
import Data.Char (toLower)
|
|
Packit |
acf257 |
import Network.Stream (Result, failParse)
|
|
Packit |
acf257 |
import Network.HTTP.Utils ( trim, split, crlf )
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | The @Header@ data type pairs header names & values.
|
|
Packit |
acf257 |
data Header = Header HeaderName String
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
hdrName :: Header -> HeaderName
|
|
Packit |
acf257 |
hdrName (Header h _) = h
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
hdrValue :: Header -> String
|
|
Packit |
acf257 |
hdrValue (Header _ v) = v
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | Header constructor as a function, hiding above rep.
|
|
Packit |
acf257 |
mkHeader :: HeaderName -> String -> Header
|
|
Packit |
acf257 |
mkHeader = Header
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
instance Show Header where
|
|
Packit |
acf257 |
show (Header key value) = shows key (':':' ':value ++ crlf)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | HTTP @HeaderName@ type, a Haskell data constructor for each
|
|
Packit |
acf257 |
-- specification-defined header, prefixed with @Hdr@ and CamelCased,
|
|
Packit |
acf257 |
-- (i.e., eliding the @-@ in the process.) Should you require using
|
|
Packit |
acf257 |
-- a custom header, there's the @HdrCustom@ constructor which takes
|
|
Packit |
acf257 |
-- a @String@ argument.
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
-- Encoding HTTP header names differently, as Strings perhaps, is an
|
|
Packit |
acf257 |
-- equally fine choice..no decidedly clear winner, but let's stick
|
|
Packit |
acf257 |
-- with data constructors here.
|
|
Packit |
acf257 |
--
|
|
Packit |
acf257 |
data HeaderName
|
|
Packit |
acf257 |
-- Generic Headers --
|
|
Packit |
acf257 |
= HdrCacheControl
|
|
Packit |
acf257 |
| HdrConnection
|
|
Packit |
acf257 |
| HdrDate
|
|
Packit |
acf257 |
| HdrPragma
|
|
Packit |
acf257 |
| HdrTransferEncoding
|
|
Packit |
acf257 |
| HdrUpgrade
|
|
Packit |
acf257 |
| HdrVia
|
|
Packit |
acf257 |
-- Request Headers --
|
|
Packit |
acf257 |
| HdrAccept
|
|
Packit |
acf257 |
| HdrAcceptCharset
|
|
Packit |
acf257 |
| HdrAcceptEncoding
|
|
Packit |
acf257 |
| HdrAcceptLanguage
|
|
Packit |
acf257 |
| HdrAuthorization
|
|
Packit |
acf257 |
| HdrCookie
|
|
Packit |
acf257 |
| HdrExpect
|
|
Packit |
acf257 |
| HdrFrom
|
|
Packit |
acf257 |
| HdrHost
|
|
Packit |
acf257 |
| HdrIfModifiedSince
|
|
Packit |
acf257 |
| HdrIfMatch
|
|
Packit |
acf257 |
| HdrIfNoneMatch
|
|
Packit |
acf257 |
| HdrIfRange
|
|
Packit |
acf257 |
| HdrIfUnmodifiedSince
|
|
Packit |
acf257 |
| HdrMaxForwards
|
|
Packit |
acf257 |
| HdrProxyAuthorization
|
|
Packit |
acf257 |
| HdrRange
|
|
Packit |
acf257 |
| HdrReferer
|
|
Packit |
acf257 |
| HdrUserAgent
|
|
Packit |
acf257 |
-- Response Headers
|
|
Packit |
acf257 |
| HdrAge
|
|
Packit |
acf257 |
| HdrLocation
|
|
Packit |
acf257 |
| HdrProxyAuthenticate
|
|
Packit |
acf257 |
| HdrPublic
|
|
Packit |
acf257 |
| HdrRetryAfter
|
|
Packit |
acf257 |
| HdrServer
|
|
Packit |
acf257 |
| HdrSetCookie
|
|
Packit |
acf257 |
| HdrTE
|
|
Packit |
acf257 |
| HdrTrailer
|
|
Packit |
acf257 |
| HdrVary
|
|
Packit |
acf257 |
| HdrWarning
|
|
Packit |
acf257 |
| HdrWWWAuthenticate
|
|
Packit |
acf257 |
-- Entity Headers
|
|
Packit |
acf257 |
| HdrAllow
|
|
Packit |
acf257 |
| HdrContentBase
|
|
Packit |
acf257 |
| HdrContentEncoding
|
|
Packit |
acf257 |
| HdrContentLanguage
|
|
Packit |
acf257 |
| HdrContentLength
|
|
Packit |
acf257 |
| HdrContentLocation
|
|
Packit |
acf257 |
| HdrContentMD5
|
|
Packit |
acf257 |
| HdrContentRange
|
|
Packit |
acf257 |
| HdrContentType
|
|
Packit |
acf257 |
| HdrETag
|
|
Packit |
acf257 |
| HdrExpires
|
|
Packit |
acf257 |
| HdrLastModified
|
|
Packit |
acf257 |
-- | MIME entity headers (for sub-parts)
|
|
Packit |
acf257 |
| HdrContentTransferEncoding
|
|
Packit |
acf257 |
-- | Allows for unrecognised or experimental headers.
|
|
Packit |
acf257 |
| HdrCustom String -- not in header map below.
|
|
Packit |
acf257 |
deriving(Eq)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @headerMap@ is a straight assoc list for translating between header names
|
|
Packit |
acf257 |
-- and values.
|
|
Packit |
acf257 |
headerMap :: [ (String,HeaderName) ]
|
|
Packit |
acf257 |
headerMap =
|
|
Packit |
acf257 |
[ p "Cache-Control" HdrCacheControl
|
|
Packit |
acf257 |
, p "Connection" HdrConnection
|
|
Packit |
acf257 |
, p "Date" HdrDate
|
|
Packit |
acf257 |
, p "Pragma" HdrPragma
|
|
Packit |
acf257 |
, p "Transfer-Encoding" HdrTransferEncoding
|
|
Packit |
acf257 |
, p "Upgrade" HdrUpgrade
|
|
Packit |
acf257 |
, p "Via" HdrVia
|
|
Packit |
acf257 |
, p "Accept" HdrAccept
|
|
Packit |
acf257 |
, p "Accept-Charset" HdrAcceptCharset
|
|
Packit |
acf257 |
, p "Accept-Encoding" HdrAcceptEncoding
|
|
Packit |
acf257 |
, p "Accept-Language" HdrAcceptLanguage
|
|
Packit |
acf257 |
, p "Authorization" HdrAuthorization
|
|
Packit |
acf257 |
, p "Cookie" HdrCookie
|
|
Packit |
acf257 |
, p "Expect" HdrExpect
|
|
Packit |
acf257 |
, p "From" HdrFrom
|
|
Packit |
acf257 |
, p "Host" HdrHost
|
|
Packit |
acf257 |
, p "If-Modified-Since" HdrIfModifiedSince
|
|
Packit |
acf257 |
, p "If-Match" HdrIfMatch
|
|
Packit |
acf257 |
, p "If-None-Match" HdrIfNoneMatch
|
|
Packit |
acf257 |
, p "If-Range" HdrIfRange
|
|
Packit |
acf257 |
, p "If-Unmodified-Since" HdrIfUnmodifiedSince
|
|
Packit |
acf257 |
, p "Max-Forwards" HdrMaxForwards
|
|
Packit |
acf257 |
, p "Proxy-Authorization" HdrProxyAuthorization
|
|
Packit |
acf257 |
, p "Range" HdrRange
|
|
Packit |
acf257 |
, p "Referer" HdrReferer
|
|
Packit |
acf257 |
, p "User-Agent" HdrUserAgent
|
|
Packit |
acf257 |
, p "Age" HdrAge
|
|
Packit |
acf257 |
, p "Location" HdrLocation
|
|
Packit |
acf257 |
, p "Proxy-Authenticate" HdrProxyAuthenticate
|
|
Packit |
acf257 |
, p "Public" HdrPublic
|
|
Packit |
acf257 |
, p "Retry-After" HdrRetryAfter
|
|
Packit |
acf257 |
, p "Server" HdrServer
|
|
Packit |
acf257 |
, p "Set-Cookie" HdrSetCookie
|
|
Packit |
acf257 |
, p "TE" HdrTE
|
|
Packit |
acf257 |
, p "Trailer" HdrTrailer
|
|
Packit |
acf257 |
, p "Vary" HdrVary
|
|
Packit |
acf257 |
, p "Warning" HdrWarning
|
|
Packit |
acf257 |
, p "WWW-Authenticate" HdrWWWAuthenticate
|
|
Packit |
acf257 |
, p "Allow" HdrAllow
|
|
Packit |
acf257 |
, p "Content-Base" HdrContentBase
|
|
Packit |
acf257 |
, p "Content-Encoding" HdrContentEncoding
|
|
Packit |
acf257 |
, p "Content-Language" HdrContentLanguage
|
|
Packit |
acf257 |
, p "Content-Length" HdrContentLength
|
|
Packit |
acf257 |
, p "Content-Location" HdrContentLocation
|
|
Packit |
acf257 |
, p "Content-MD5" HdrContentMD5
|
|
Packit |
acf257 |
, p "Content-Range" HdrContentRange
|
|
Packit |
acf257 |
, p "Content-Type" HdrContentType
|
|
Packit |
acf257 |
, p "ETag" HdrETag
|
|
Packit |
acf257 |
, p "Expires" HdrExpires
|
|
Packit |
acf257 |
, p "Last-Modified" HdrLastModified
|
|
Packit |
acf257 |
, p "Content-Transfer-Encoding" HdrContentTransferEncoding
|
|
Packit |
acf257 |
]
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
p a b = (a,b)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
instance Show HeaderName where
|
|
Packit |
acf257 |
show (HdrCustom s) = s
|
|
Packit |
acf257 |
show x = case filter ((==x).snd) headerMap of
|
|
Packit |
acf257 |
[] -> error "headerMap incomplete"
|
|
Packit |
acf257 |
(h:_) -> fst h
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @HasHeaders@ is a type class for types containing HTTP headers, allowing
|
|
Packit |
acf257 |
-- you to write overloaded header manipulation functions
|
|
Packit |
acf257 |
-- for both 'Request' and 'Response' data types, for instance.
|
|
Packit |
acf257 |
class HasHeaders x where
|
|
Packit |
acf257 |
getHeaders :: x -> [Header]
|
|
Packit |
acf257 |
setHeaders :: x -> [Header] -> x
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- Header manipulation functions
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
type HeaderSetter a = HeaderName -> String -> a -> a
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @insertHeader hdr val x@ inserts a header with the given header name
|
|
Packit |
acf257 |
-- and value. Does not check for existing headers with same name, allowing
|
|
Packit |
acf257 |
-- duplicates to be introduce (use 'replaceHeader' if you want to avoid this.)
|
|
Packit |
acf257 |
insertHeader :: HasHeaders a => HeaderSetter a
|
|
Packit |
acf257 |
insertHeader name value x = setHeaders x newHeaders
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
newHeaders = (Header name value) : getHeaders x
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @insertHeaderIfMissing hdr val x@ adds the new header only if no previous
|
|
Packit |
acf257 |
-- header with name @hdr@ exists in @x@.
|
|
Packit |
acf257 |
insertHeaderIfMissing :: HasHeaders a => HeaderSetter a
|
|
Packit |
acf257 |
insertHeaderIfMissing name value x = setHeaders x (newHeaders $ getHeaders x)
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
newHeaders list@(h@(Header n _): rest)
|
|
Packit |
acf257 |
| n == name = list
|
|
Packit |
acf257 |
| otherwise = h : newHeaders rest
|
|
Packit |
acf257 |
newHeaders [] = [Header name value]
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @replaceHeader hdr val o@ replaces the header @hdr@ with the
|
|
Packit |
acf257 |
-- value @val@, dropping any existing
|
|
Packit |
acf257 |
replaceHeader :: HasHeaders a => HeaderSetter a
|
|
Packit |
acf257 |
replaceHeader name value h = setHeaders h newHeaders
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
newHeaders = Header name value : [ x | x@(Header n _) <- getHeaders h, name /= n ]
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @insertHeaders hdrs x@ appends multiple headers to @x@'s existing
|
|
Packit |
acf257 |
-- set.
|
|
Packit |
acf257 |
insertHeaders :: HasHeaders a => [Header] -> a -> a
|
|
Packit |
acf257 |
insertHeaders hdrs x = setHeaders x (getHeaders x ++ hdrs)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @retrieveHeaders hdrNm x@ gets a list of headers with 'HeaderName' @hdrNm@.
|
|
Packit |
acf257 |
retrieveHeaders :: HasHeaders a => HeaderName -> a -> [Header]
|
|
Packit |
acf257 |
retrieveHeaders name x = filter matchname (getHeaders x)
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
matchname (Header n _) = n == name
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @findHeader hdrNm x@ looks up @hdrNm@ in @x@, returning the first
|
|
Packit |
acf257 |
-- header that matches, if any.
|
|
Packit |
acf257 |
findHeader :: HasHeaders a => HeaderName -> a -> Maybe String
|
|
Packit |
acf257 |
findHeader n x = lookupHeader n (getHeaders x)
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @lookupHeader hdr hdrs@ locates the first header matching @hdr@ in the
|
|
Packit |
acf257 |
-- list @hdrs@.
|
|
Packit |
acf257 |
lookupHeader :: HeaderName -> [Header] -> Maybe String
|
|
Packit |
acf257 |
lookupHeader _ [] = Nothing
|
|
Packit |
acf257 |
lookupHeader v (Header n s:t)
|
|
Packit |
acf257 |
| v == n = Just s
|
|
Packit |
acf257 |
| otherwise = lookupHeader v t
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @parseHeader headerNameAndValueString@ tries to unscramble a
|
|
Packit |
acf257 |
-- @header: value@ pairing and returning it as a 'Header'.
|
|
Packit |
acf257 |
parseHeader :: String -> Result Header
|
|
Packit |
acf257 |
parseHeader str =
|
|
Packit |
acf257 |
case split ':' str of
|
|
Packit |
acf257 |
Nothing -> failParse ("Unable to parse header: " ++ str)
|
|
Packit |
acf257 |
Just (k,v) -> return $ Header (fn k) (trim $ drop 1 v)
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
fn k = case map snd $ filter (match k . fst) headerMap of
|
|
Packit |
acf257 |
[] -> (HdrCustom k)
|
|
Packit |
acf257 |
(h:_) -> h
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
match :: String -> String -> Bool
|
|
Packit |
acf257 |
match s1 s2 = map toLower s1 == map toLower s2
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- | @parseHeaders hdrs@ takes a sequence of strings holding header
|
|
Packit |
acf257 |
-- information and parses them into a set of headers (preserving their
|
|
Packit |
acf257 |
-- order in the input argument.) Handles header values split up over
|
|
Packit |
acf257 |
-- multiple lines.
|
|
Packit |
acf257 |
parseHeaders :: [String] -> Result [Header]
|
|
Packit |
acf257 |
parseHeaders = catRslts [] .
|
|
Packit |
acf257 |
map (parseHeader . clean) .
|
|
Packit |
acf257 |
joinExtended ""
|
|
Packit |
acf257 |
where
|
|
Packit |
acf257 |
-- Joins consecutive lines where the second line
|
|
Packit |
acf257 |
-- begins with ' ' or '\t'.
|
|
Packit |
acf257 |
joinExtended old [] = [old]
|
|
Packit |
acf257 |
joinExtended old (h : t)
|
|
Packit |
acf257 |
| isLineExtension h = joinExtended (old ++ ' ' : tail h) t
|
|
Packit |
acf257 |
| otherwise = old : joinExtended h t
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
isLineExtension (x:_) = x == ' ' || x == '\t'
|
|
Packit |
acf257 |
isLineExtension _ = False
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
clean [] = []
|
|
Packit |
acf257 |
clean (h:t) | h `elem` "\t\r\n" = ' ' : clean t
|
|
Packit |
acf257 |
| otherwise = h : clean t
|
|
Packit |
acf257 |
|
|
Packit |
acf257 |
-- tolerant of errors? should parse
|
|
Packit |
acf257 |
-- errors here be reported or ignored?
|
|
Packit |
acf257 |
-- currently ignored.
|
|
Packit |
acf257 |
catRslts :: [a] -> [Result a] -> Result [a]
|
|
Packit |
acf257 |
catRslts list (h:t) =
|
|
Packit |
acf257 |
case h of
|
|
Packit |
acf257 |
Left _ -> catRslts list t
|
|
Packit |
acf257 |
Right v -> catRslts (v:list) t
|
|
Packit |
acf257 |
catRslts list [] = Right $ reverse list
|