module Network.WebSockets.Http
( Headers
, RequestHead (..)
, Request (..)
, ResponseHead (..)
, Response (..)
, HandshakeException (..)
, encodeRequestHead
, encodeRequest
, decodeRequestHead
, encodeResponseHead
, encodeResponse
, decodeResponseHead
, decodeResponse
, response101
, response400
, getRequestHeader
, getResponseHeader
, getRequestSecWebSocketVersion
, getRequestSubprotocols
) where
import qualified Blaze.ByteString.Builder as Builder
import qualified Blaze.ByteString.Builder.Char.Utf8 as Builder
import Control.Applicative (pure, (*>), (<$>), (<*),
(<*>))
import Control.Exception (Exception, throw)
import qualified Data.Attoparsec.ByteString as A
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.ByteString.Char8 ()
import qualified Data.ByteString.Char8 as BC
import Data.ByteString.Internal (c2w)
import qualified Data.CaseInsensitive as CI
import Data.Dynamic (Typeable)
import Data.Monoid (mappend, mconcat)
type Headers = [(CI.CI ByteString, ByteString)]
data RequestHead = RequestHead
{ requestPath :: !B.ByteString
, requestHeaders :: Headers
, requestSecure :: Bool
} deriving (Show)
data Request = Request RequestHead B.ByteString
deriving (Show)
data ResponseHead = ResponseHead
{ responseCode :: !Int
, responseMessage :: !B.ByteString
, responseHeaders :: Headers
} deriving (Show)
data Response = Response ResponseHead B.ByteString
deriving (Show)
data HandshakeException
= NotSupported
| MalformedRequest RequestHead String
| MalformedResponse ResponseHead String
| RequestRejected Request String
| OtherHandshakeException String
deriving (Show, Typeable)
instance Exception HandshakeException
encodeRequestHead :: RequestHead -> Builder.Builder
encodeRequestHead (RequestHead path headers _) =
Builder.copyByteString "GET " `mappend`
Builder.copyByteString path `mappend`
Builder.copyByteString " HTTP/1.1" `mappend`
Builder.fromByteString "\r\n" `mappend`
mconcat (map header headers) `mappend`
Builder.copyByteString "\r\n"
where
header (k, v) = mconcat $ map Builder.copyByteString
[CI.original k, ": ", v, "\r\n"]
encodeRequest :: Request -> Builder.Builder
encodeRequest (Request head' body) =
encodeRequestHead head' `mappend` Builder.copyByteString body
decodeRequestHead :: Bool -> A.Parser RequestHead
decodeRequestHead isSecure = RequestHead
<$> requestLine
<*> A.manyTill decodeHeaderLine newline
<*> pure isSecure
where
space = A.word8 (c2w ' ')
newline = A.string "\r\n"
requestLine = A.string "GET" *> space *> A.takeWhile1 (/= c2w ' ')
<* space
<* A.string "HTTP/1.1" <* newline
encodeResponseHead :: ResponseHead -> Builder.Builder
encodeResponseHead (ResponseHead code msg headers) =
Builder.copyByteString "HTTP/1.1 " `mappend`
Builder.fromString (show code) `mappend`
Builder.fromChar ' ' `mappend`
Builder.fromByteString msg `mappend`
Builder.fromByteString "\r\n" `mappend`
mconcat (map header headers) `mappend`
Builder.copyByteString "\r\n"
where
header (k, v) = mconcat $ map Builder.copyByteString
[CI.original k, ": ", v, "\r\n"]
encodeResponse :: Response -> Builder.Builder
encodeResponse (Response head' body) =
encodeResponseHead head' `mappend` Builder.copyByteString body
response101 :: Headers -> B.ByteString -> Response
response101 headers = Response
(ResponseHead 101 "WebSocket Protocol Handshake"
(("Upgrade", "websocket") : ("Connection", "Upgrade") : headers))
response400 :: Headers -> B.ByteString -> Response
response400 headers = Response (ResponseHead 400 "Bad Request" headers)
decodeResponseHead :: A.Parser ResponseHead
decodeResponseHead = ResponseHead
<$> fmap (read . BC.unpack) code
<*> message
<*> A.manyTill decodeHeaderLine newline
where
space = A.word8 (c2w ' ')
newline = A.string "\r\n"
code = A.string "HTTP/1.1" *> space *> A.takeWhile1 (/= c2w ' ') <* space
message = A.takeWhile1 (/= c2w '\r') <* newline
decodeResponse :: A.Parser Response
decodeResponse = Response <$> decodeResponseHead <*> A.takeByteString
getRequestHeader :: RequestHead
-> CI.CI ByteString
-> ByteString
getRequestHeader rq key = case lookup key (requestHeaders rq) of
Just t -> t
Nothing -> throw $ MalformedRequest rq $
"Header missing: " ++ BC.unpack (CI.original key)
getResponseHeader :: ResponseHead
-> CI.CI ByteString
-> ByteString
getResponseHeader rsp key = case lookup key (responseHeaders rsp) of
Just t -> t
Nothing -> throw $ MalformedResponse rsp $
"Header missing: " ++ BC.unpack (CI.original key)
getRequestSecWebSocketVersion :: RequestHead -> Maybe B.ByteString
getRequestSecWebSocketVersion p =
lookup "Sec-WebSocket-Version" (requestHeaders p)
getRequestSubprotocols :: RequestHead -> [B.ByteString]
getRequestSubprotocols rh = maybe [] parse mproto
where
mproto = lookup "Sec-WebSocket-Protocol" $ requestHeaders rh
parse = filter (not . B.null) . BC.splitWith (\o -> o == ',' || o == ' ')
decodeHeaderLine :: A.Parser (CI.CI ByteString, ByteString)
decodeHeaderLine = (,)
<$> (CI.mk <$> A.takeWhile1 (/= c2w ':'))
<* A.word8 (c2w ':')
<* A.option (c2w ' ') (A.word8 (c2w ' '))
<*> A.takeWhile (/= c2w '\r')
<* A.string "\r\n"