module Network.Http.Internal (
Hostname,
Port,
Request(..),
EntityBody(..),
ExpectMode(..),
Response(..),
StatusCode,
TransferEncoding(..),
ContentEncoding(..),
getStatusCode,
getStatusMessage,
getHeader,
Method(..),
Headers,
emptyHeaders,
updateHeader,
removeHeader,
buildHeaders,
lookupHeader,
retrieveHeaders,
HttpType (getHeaders),
HttpParseException(..),
composeRequestBytes,
composeResponseBytes
) where
import Prelude hiding (lookup)
import Blaze.ByteString.Builder (Builder)
import qualified Blaze.ByteString.Builder as Builder (copyByteString,
copyByteString,
fromByteString,
fromByteString,
toByteString)
import qualified Blaze.ByteString.Builder.Char8 as Builder (fromChar,
fromShow,
fromString)
import Control.Exception (Exception)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as S
import Data.CaseInsensitive (CI, mk, original)
import Data.HashMap.Strict (HashMap, delete, empty, foldrWithKey, insert,
insertWith, lookup, toList)
import Data.Int (Int64)
import Data.List (foldl')
import Data.Monoid (mconcat, mempty)
import Data.Typeable (Typeable)
import Data.Word (Word16)
type Hostname = ByteString
type Port = Word16
data Method
= GET
| HEAD
| POST
| PUT
| DELETE
| TRACE
| OPTIONS
| CONNECT
| PATCH
| Method ByteString
deriving (Show, Read, Ord)
instance Eq Method where
GET == GET = True
HEAD == HEAD = True
POST == POST = True
PUT == PUT = True
DELETE == DELETE = True
TRACE == TRACE = True
OPTIONS == OPTIONS = True
CONNECT == CONNECT = True
PATCH == PATCH = True
GET == Method "GET" = True
HEAD == Method "HEAD" = True
POST == Method "POST" = True
PUT == Method "PUT" = True
DELETE == Method "DELETE" = True
TRACE == Method "TRACE" = True
OPTIONS == Method "OPTIONS" = True
CONNECT == Method "CONNECT" = True
PATCH == Method "PATCH" = True
Method a == Method b = a == b
m@(Method _) == other = other == m
_ == _ = False
data Request
= Request {
qMethod :: !Method,
qHost :: !(Maybe ByteString),
qPath :: !ByteString,
qBody :: !EntityBody,
qExpect :: !ExpectMode,
qHeaders :: !Headers
} deriving (Eq)
instance Show Request where
show q =
S.unpack $ S.filter (/= '\r') $ Builder.toByteString $ composeRequestBytes q "<default>"
data EntityBody = Empty | Chunking | Static Int64 deriving (Show, Eq, Ord)
data ExpectMode = Normal | Continue deriving (Show, Eq, Ord)
composeRequestBytes :: Request -> ByteString -> Builder
composeRequestBytes q h' =
mconcat
[requestline,
hostLine,
headerFields,
crlf]
where
requestline = mconcat
[method,
sp,
uri,
sp,
version,
crlf]
method = case qMethod q of
GET -> Builder.fromString "GET"
HEAD -> Builder.fromString "HEAD"
POST -> Builder.fromString "POST"
PUT -> Builder.fromString "PUT"
DELETE -> Builder.fromString "DELETE"
TRACE -> Builder.fromString "TRACE"
OPTIONS -> Builder.fromString "OPTIONS"
CONNECT -> Builder.fromString "CONNECT"
PATCH -> Builder.fromString "PATCH"
(Method x) -> Builder.fromByteString x
uri = case qPath q of
"" -> Builder.fromChar '/'
path -> Builder.copyByteString path
version = Builder.fromString "HTTP/1.1"
hostLine = mconcat
[Builder.fromString "Host: ",
hostname,
crlf]
hostname = case qHost q of
Just x' -> Builder.copyByteString x'
Nothing -> Builder.copyByteString h'
headerFields = joinHeaders $ unWrap $ qHeaders q
crlf = Builder.fromString "\r\n"
sp = Builder.fromChar ' '
type StatusCode = Int
data Response
= Response {
pStatusCode :: !StatusCode,
pStatusMsg :: !ByteString,
pTransferEncoding :: !TransferEncoding,
pContentEncoding :: !ContentEncoding,
pContentLength :: !(Maybe Int64),
pHeaders :: !Headers
}
instance Show Response where
show p =
S.unpack $ S.filter (/= '\r') $ Builder.toByteString $ composeResponseBytes p
data TransferEncoding = None | Chunked
data ContentEncoding = Identity | Gzip | Deflate
deriving (Show)
getStatusCode :: Response -> StatusCode
getStatusCode = pStatusCode
getStatusMessage :: Response -> ByteString
getStatusMessage = pStatusMsg
getHeader :: Response -> ByteString -> Maybe ByteString
getHeader p k =
lookupHeader h k
where
h = pHeaders p
class HttpType τ where
getHeaders :: τ -> Headers
instance HttpType Request where
getHeaders q = qHeaders q
instance HttpType Response where
getHeaders p = pHeaders p
composeResponseBytes :: Response -> Builder
composeResponseBytes p =
mconcat
[statusline,
headerFields,
crlf]
where
statusline = mconcat
[version,
sp,
code,
sp,
message,
crlf]
code = Builder.fromShow $ pStatusCode p
message = Builder.copyByteString $ pStatusMsg p
version = Builder.fromString "HTTP/1.1"
headerFields = joinHeaders $ unWrap $ pHeaders p
newtype Headers = Wrap {
unWrap :: HashMap (CI ByteString) ByteString
} deriving (Eq)
instance Show Headers where
show x = S.unpack $ S.filter (/= '\r') $ Builder.toByteString $ joinHeaders $ unWrap x
joinHeaders :: HashMap (CI ByteString) ByteString -> Builder
joinHeaders m = foldrWithKey combine mempty m
combine :: CI ByteString -> ByteString -> Builder -> Builder
combine k v acc =
mconcat [acc, key, Builder.fromString ": ", value, crlf]
where
key = Builder.copyByteString $ original k
value = Builder.fromByteString v
emptyHeaders :: Headers
emptyHeaders =
Wrap empty
updateHeader :: Headers -> ByteString -> ByteString -> Headers
updateHeader x k v =
Wrap result
where
!result = insert (mk k) v m
!m = unWrap x
removeHeader :: Headers -> ByteString -> Headers
removeHeader x k =
Wrap result
where
!result = delete (mk k) m
!m = unWrap x
buildHeaders :: [(ByteString, ByteString)] -> Headers
buildHeaders hs =
Wrap result
where
result = foldl' addHeader empty hs
addHeader
:: HashMap (CI ByteString) ByteString
-> (ByteString,ByteString)
-> HashMap (CI ByteString) ByteString
addHeader m (k,v) =
insertWith f (mk k) v m
where
f new old = S.concat [old, ",", new]
lookupHeader :: Headers -> ByteString -> Maybe ByteString
lookupHeader x k =
lookup (mk k) m
where
!m = unWrap x
retrieveHeaders :: Headers -> [(ByteString, ByteString)]
retrieveHeaders x =
map down $ toList m
where
!m = unWrap x
down :: (CI ByteString, ByteString) -> (ByteString, ByteString)
down (k, v) =
(original k, v)
data HttpParseException = HttpParseException String
deriving (Typeable, Show)
instance Exception HttpParseException