happstack-server 0.5.0.4 → 6.0.0
raw patch · 52 files changed
+5459/−3942 lines, 52 filesdep +blaze-htmldep +network-bytestringdep +textdep −HaXmldep ~happstack-datadep ~happstack-utildep ~networkPVP ok
version bump matches the API change (PVP)
Dependencies added: blaze-html, network-bytestring, text
Dependencies removed: HaXml
Dependency ranges changed: happstack-data, happstack-util, network, sendfile
API changes (from Hackage documentation)
- Happstack.Server.Cookie: cookiesParser :: GenParser Char st [Cookie]
- Happstack.Server.Cookie: getCookie :: Monad m => String -> ByteString -> m Cookie
- Happstack.Server.Cookie: getCookie' :: Monad m => String -> ByteString -> m (Either String Cookie)
- Happstack.Server.Cookie: getCookies :: Monad m => ByteString -> m [Cookie]
- Happstack.Server.Cookie: getCookies' :: Monad m => ByteString -> m (Either String [Cookie])
- Happstack.Server.Cookie: instance [overlap ok] Data Cookie
- Happstack.Server.Cookie: instance [overlap ok] Eq Cookie
- Happstack.Server.Cookie: instance [overlap ok] Read Cookie
- Happstack.Server.Cookie: instance [overlap ok] Show Cookie
- Happstack.Server.Cookie: instance [overlap ok] Typeable Cookie
- Happstack.Server.Cookie: mkCookieHeader :: Seconds -> Cookie -> String
- Happstack.Server.Cookie: parseCookies :: String -> Either String [Cookie]
- Happstack.Server.HTTP.Client: getResponse :: Request -> IO (Either String Response)
- Happstack.Server.HTTP.Client: unproxify :: Request -> Request
- Happstack.Server.HTTP.Client: unrproxify :: String -> [(String, String)] -> Request -> Request
- Happstack.Server.HTTP.FileServe: asContentType :: Monad m => String -> (FilePath -> m String)
- Happstack.Server.HTTP.FileServe: blockDotFiles :: (Request -> IO Response) -> Request -> IO Response
- Happstack.Server.HTTP.FileServe: defaultIxFiles :: [String]
- Happstack.Server.HTTP.FileServe: doIndex :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => [String] -> MimeMap -> String -> m Response
- Happstack.Server.HTTP.FileServe: doIndex' :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => (String -> FilePath -> m Response) -> (FilePath -> m String) -> [String] -> String -> m Response
- Happstack.Server.HTTP.FileServe: doIndexLazy :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => [String] -> MimeMap -> String -> m Response
- Happstack.Server.HTTP.FileServe: doIndexStrict :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => [String] -> MimeMap -> String -> m Response
- Happstack.Server.HTTP.FileServe: errorwrapper :: (MonadIO m, MonadPlus m, FilterMonad Response m) => String -> String -> m Response
- Happstack.Server.HTTP.FileServe: fileNotFound :: (Monad m, FilterMonad Response m) => FilePath -> m Response
- Happstack.Server.HTTP.FileServe: filePathLazy :: (ServerMonad m, MonadIO m) => String -> FilePath -> m Response
- Happstack.Server.HTTP.FileServe: filePathSendFile :: (ServerMonad m, MonadIO m) => String -> FilePath -> m Response
- Happstack.Server.HTTP.FileServe: filePathStrict :: (ServerMonad m, MonadIO m) => String -> FilePath -> m Response
- Happstack.Server.HTTP.FileServe: fileServe :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => [FilePath] -> FilePath -> m Response
- Happstack.Server.HTTP.FileServe: fileServe' :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => (String -> FilePath -> m Response) -> (FilePath -> m String) -> [FilePath] -> FilePath -> m Response
- Happstack.Server.HTTP.FileServe: fileServeLazy :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => [FilePath] -> FilePath -> m Response
- Happstack.Server.HTTP.FileServe: fileServeStrict :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => [FilePath] -> FilePath -> m Response
- Happstack.Server.HTTP.FileServe: guessContentType :: MimeMap -> FilePath -> Maybe String
- Happstack.Server.HTTP.FileServe: guessContentTypeM :: Monad m => MimeMap -> (FilePath -> m String)
- Happstack.Server.HTTP.FileServe: isDot :: String -> Bool
- Happstack.Server.HTTP.FileServe: lazyByteStringResponse :: String -> ByteString -> Maybe (CalendarTime, Request) -> Integer -> Integer -> Response
- Happstack.Server.HTTP.FileServe: mimeTypes :: MimeMap
- Happstack.Server.HTTP.FileServe: sendFileResponse :: String -> FilePath -> Maybe (CalendarTime, Request) -> Integer -> Integer -> Response
- Happstack.Server.HTTP.FileServe: serveFile :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => (FilePath -> m String) -> FilePath -> m Response
- Happstack.Server.HTTP.FileServe: serveFileUsing :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => (String -> FilePath -> m Response) -> (FilePath -> m String) -> FilePath -> m Response
- Happstack.Server.HTTP.FileServe: strictByteStringResponse :: String -> ByteString -> Maybe (CalendarTime, Request) -> Integer -> Integer -> Response
- Happstack.Server.HTTP.FileServe: type MimeMap = Map String String
- Happstack.Server.HTTP.LowLevel: listen :: Conf -> (Request -> IO Response) -> IO ()
- Happstack.Server.HTTP.LowLevel: listen' :: Socket -> Conf -> (Request -> IO Response) -> IO ()
- Happstack.Server.HTTP.LowLevel: listenOn :: Int -> IO Socket
- Happstack.Server.HTTP.LowLevel: parseResponse :: ByteString -> Either String Response
- Happstack.Server.HTTP.LowLevel: putRequest :: Handle -> Request -> IO ()
- Happstack.Server.HTTP.LowLevel: request :: Conf -> Handle -> Host -> (Request -> IO Response) -> IO ()
- Happstack.Server.HTTP.Types: Body :: ByteString -> RqBody
- Happstack.Server.HTTP.Types: CONNECT :: Method
- Happstack.Server.HTTP.Types: Conf :: Int -> Maybe (Response -> IO Response) -> Conf
- Happstack.Server.HTTP.Types: ContentType :: String -> String -> [(String, String)] -> ContentType
- Happstack.Server.HTTP.Types: DELETE :: Method
- Happstack.Server.HTTP.Types: GET :: Method
- Happstack.Server.HTTP.Types: HEAD :: Method
- Happstack.Server.HTTP.Types: HeaderPair :: ByteString -> [ByteString] -> HeaderPair
- Happstack.Server.HTTP.Types: Input :: ByteString -> Maybe String -> ContentType -> Input
- Happstack.Server.HTTP.Types: OPTIONS :: Method
- Happstack.Server.HTTP.Types: POST :: Method
- Happstack.Server.HTTP.Types: PUT :: Method
- Happstack.Server.HTTP.Types: Request :: Method -> [String] -> String -> String -> [(String, Input)] -> [(String, Cookie)] -> Version -> Headers -> RqBody -> Host -> Request
- Happstack.Server.HTTP.Types: Response :: Int -> Headers -> RsFlags -> ByteString -> Maybe (Response -> IO Response) -> Response
- Happstack.Server.HTTP.Types: RsFlags :: Bool -> RsFlags
- Happstack.Server.HTTP.Types: SendFile :: Int -> Headers -> RsFlags -> Maybe (Response -> IO Response) -> FilePath -> Integer -> Integer -> Response
- Happstack.Server.HTTP.Types: TRACE :: Method
- Happstack.Server.HTTP.Types: Version :: Int -> Int -> Version
- Happstack.Server.HTTP.Types: addHeader :: HasHeaders r => String -> String -> r -> r
- Happstack.Server.HTTP.Types: addHeaderBS :: HasHeaders r => ByteString -> ByteString -> r -> r
- Happstack.Server.HTTP.Types: addHeaderUnsafe :: HasHeaders r => ByteString -> HeaderPair -> r -> r
- Happstack.Server.HTTP.Types: continueHTTP :: Request -> Response -> Bool
- Happstack.Server.HTTP.Types: ctParameters :: ContentType -> [(String, String)]
- Happstack.Server.HTTP.Types: ctSubtype :: ContentType -> String
- Happstack.Server.HTTP.Types: ctType :: ContentType -> String
- Happstack.Server.HTTP.Types: data Conf
- Happstack.Server.HTTP.Types: data ContentType
- Happstack.Server.HTTP.Types: data HeaderPair
- Happstack.Server.HTTP.Types: data Input
- Happstack.Server.HTTP.Types: data Method
- Happstack.Server.HTTP.Types: data Request
- Happstack.Server.HTTP.Types: data Response
- Happstack.Server.HTTP.Types: data RsFlags
- Happstack.Server.HTTP.Types: data Version
- Happstack.Server.HTTP.Types: getHeader :: HasHeaders r => String -> r -> Maybe ByteString
- Happstack.Server.HTTP.Types: getHeaderBS :: HasHeaders r => ByteString -> r -> Maybe ByteString
- Happstack.Server.HTTP.Types: getHeaderUnsafe :: HasHeaders r => ByteString -> r -> Maybe ByteString
- Happstack.Server.HTTP.Types: hName :: HeaderPair -> ByteString
- Happstack.Server.HTTP.Types: hValue :: HeaderPair -> [ByteString]
- Happstack.Server.HTTP.Types: hasHeader :: HasHeaders r => String -> r -> Bool
- Happstack.Server.HTTP.Types: hasHeaderBS :: HasHeaders r => ByteString -> r -> Bool
- Happstack.Server.HTTP.Types: hasHeaderUnsafe :: HasHeaders r => ByteString -> r -> Bool
- Happstack.Server.HTTP.Types: inputContentType :: Input -> ContentType
- Happstack.Server.HTTP.Types: inputFilename :: Input -> Maybe String
- Happstack.Server.HTTP.Types: inputValue :: Input -> ByteString
- Happstack.Server.HTTP.Types: instance [overlap ok] Eq Method
- Happstack.Server.HTTP.Types: instance [overlap ok] Eq Version
- Happstack.Server.HTTP.Types: instance [overlap ok] HasHeaders Headers
- Happstack.Server.HTTP.Types: instance [overlap ok] HasHeaders Request
- Happstack.Server.HTTP.Types: instance [overlap ok] HasHeaders Response
- Happstack.Server.HTTP.Types: instance [overlap ok] Read HeaderPair
- Happstack.Server.HTTP.Types: instance [overlap ok] Read Input
- Happstack.Server.HTTP.Types: instance [overlap ok] Read Method
- Happstack.Server.HTTP.Types: instance [overlap ok] Read Request
- Happstack.Server.HTTP.Types: instance [overlap ok] Read RqBody
- Happstack.Server.HTTP.Types: instance [overlap ok] Read RsFlags
- Happstack.Server.HTTP.Types: instance [overlap ok] Read Version
- Happstack.Server.HTTP.Types: instance [overlap ok] Show HeaderPair
- Happstack.Server.HTTP.Types: instance [overlap ok] Show Input
- Happstack.Server.HTTP.Types: instance [overlap ok] Show Method
- Happstack.Server.HTTP.Types: instance [overlap ok] Show Request
- Happstack.Server.HTTP.Types: instance [overlap ok] Show Response
- Happstack.Server.HTTP.Types: instance [overlap ok] Show RqBody
- Happstack.Server.HTTP.Types: instance [overlap ok] Show RsFlags
- Happstack.Server.HTTP.Types: instance [overlap ok] Show Version
- Happstack.Server.HTTP.Types: instance [overlap ok] Typeable Input
- Happstack.Server.HTTP.Types: instance [overlap ok] Typeable Request
- Happstack.Server.HTTP.Types: instance [overlap ok] Typeable Response
- Happstack.Server.HTTP.Types: instance [overlap ok] Typeable RqBody
- Happstack.Server.HTTP.Types: instance [overlap ok] Typeable RsFlags
- Happstack.Server.HTTP.Types: mkHeaders :: [(String, String)] -> Headers
- Happstack.Server.HTTP.Types: newtype RqBody
- Happstack.Server.HTTP.Types: noContentLength :: Response -> Response
- Happstack.Server.HTTP.Types: nullConf :: Conf
- Happstack.Server.HTTP.Types: nullRsFlags :: RsFlags
- Happstack.Server.HTTP.Types: port :: Conf -> Int
- Happstack.Server.HTTP.Types: redirect :: ToSURI s => Int -> s -> Response -> Response
- Happstack.Server.HTTP.Types: result :: Int -> String -> Response
- Happstack.Server.HTTP.Types: resultBS :: Int -> ByteString -> Response
- Happstack.Server.HTTP.Types: rqBody :: Request -> RqBody
- Happstack.Server.HTTP.Types: rqCookies :: Request -> [(String, Cookie)]
- Happstack.Server.HTTP.Types: rqHeaders :: Request -> Headers
- Happstack.Server.HTTP.Types: rqInputs :: Request -> [(String, Input)]
- Happstack.Server.HTTP.Types: rqMethod :: Request -> Method
- Happstack.Server.HTTP.Types: rqPaths :: Request -> [String]
- Happstack.Server.HTTP.Types: rqPeer :: Request -> Host
- Happstack.Server.HTTP.Types: rqQuery :: Request -> String
- Happstack.Server.HTTP.Types: rqURL :: Request -> String
- Happstack.Server.HTTP.Types: rqUri :: Request -> String
- Happstack.Server.HTTP.Types: rqVersion :: Request -> Version
- Happstack.Server.HTTP.Types: rsBody :: Response -> ByteString
- Happstack.Server.HTTP.Types: rsCode :: Response -> Int
- Happstack.Server.HTTP.Types: rsFlags :: Response -> RsFlags
- Happstack.Server.HTTP.Types: rsHeaders :: Response -> Headers
- Happstack.Server.HTTP.Types: rsValidator :: Response -> Maybe (Response -> IO Response)
- Happstack.Server.HTTP.Types: rsfContentLength :: RsFlags -> Bool
- Happstack.Server.HTTP.Types: setHeader :: HasHeaders r => String -> String -> r -> r
- Happstack.Server.HTTP.Types: setHeaderBS :: HasHeaders r => ByteString -> ByteString -> r -> r
- Happstack.Server.HTTP.Types: setHeaderUnsafe :: HasHeaders r => ByteString -> HeaderPair -> r -> r
- Happstack.Server.HTTP.Types: setRsCode :: Monad m => Int -> Response -> m Response
- Happstack.Server.HTTP.Types: sfCount :: Response -> Integer
- Happstack.Server.HTTP.Types: sfFilePath :: Response -> FilePath
- Happstack.Server.HTTP.Types: sfOffset :: Response -> Integer
- Happstack.Server.HTTP.Types: type Headers = Map ByteString HeaderPair
- Happstack.Server.HTTP.Types: type Host = (String, Int)
- Happstack.Server.HTTP.Types: validator :: Conf -> Maybe (Response -> IO Response)
- Happstack.Server.MessageWrap: bodyInput :: Request -> [(String, Input)]
- Happstack.Server.MessageWrap: bodyPartToInput :: BodyPart -> (String, Input)
- Happstack.Server.MessageWrap: class Read a => ReadString a
- Happstack.Server.MessageWrap: decodeBody :: Maybe ContentType -> ByteString -> [(String, Input)]
- Happstack.Server.MessageWrap: defaultInputType :: ContentType
- Happstack.Server.MessageWrap: formDecode :: String -> [(String, Input)]
- Happstack.Server.MessageWrap: instance [overlap ok] ReadString Char
- Happstack.Server.MessageWrap: instance [overlap ok] ReadString Double
- Happstack.Server.MessageWrap: instance [overlap ok] ReadString Float
- Happstack.Server.MessageWrap: instance [overlap ok] ReadString Int
- Happstack.Server.MessageWrap: instance [overlap ok] ReadString SURI
- Happstack.Server.MessageWrap: instance [overlap ok] ReadString [Char]
- Happstack.Server.MessageWrap: multipartDecode :: [(String, String)] -> ByteString -> [(String, Input)]
- Happstack.Server.MessageWrap: pathEls :: String -> [String]
- Happstack.Server.MessageWrap: queryInput :: SURI -> [(String, Input)]
- Happstack.Server.MessageWrap: readString :: ReadString a => String -> a
- Happstack.Server.MessageWrap: simpleInput :: String -> Input
- Happstack.Server.MinHaXML: (</<) :: Name -> [(Name, String)] -> [Element] -> Element
- Happstack.Server.MinHaXML: (<>) :: Name -> [(Name, String)] -> CharData -> Element
- Happstack.Server.MinHaXML: CSS :: StyleURL -> StyleSheet
- Happstack.Server.MinHaXML: ElString :: String -> ElString
- Happstack.Server.MinHaXML: NoStyle :: StyleSheet
- Happstack.Server.MinHaXML: XML :: StyleSheet -> a -> XML a
- Happstack.Server.MinHaXML: XSL :: StyleURL -> StyleSheet
- Happstack.Server.MinHaXML: attrF :: t1 -> (t -> String) -> t -> (t1, String)
- Happstack.Server.MinHaXML: attrFMb :: (a -> String) -> String -> (a1 -> Maybe a) -> a1 -> (String, String)
- Happstack.Server.MinHaXML: attrFS :: Show a => t1 -> (t -> a) -> t -> (t1, String)
- Happstack.Server.MinHaXML: cdataElem :: CharData -> Content
- Happstack.Server.MinHaXML: class ToElement x
- Happstack.Server.MinHaXML: data StyleSheet
- Happstack.Server.MinHaXML: data XML a
- Happstack.Server.MinHaXML: e :: Name -> [(Name, String)] -> Element
- Happstack.Server.MinHaXML: elF :: ToElement b => Name -> (a -> b) -> a -> Element
- Happstack.Server.MinHaXML: elString :: ElString -> String
- Happstack.Server.MinHaXML: emptyElem :: Name -> [(Name, String)] -> Element
- Happstack.Server.MinHaXML: getRoot :: Document -> Element
- Happstack.Server.MinHaXML: hasStyleURL :: StyleSheet -> Bool
- Happstack.Server.MinHaXML: instance [overlap ok] Eq ElString
- Happstack.Server.MinHaXML: instance [overlap ok] Ord ElString
- Happstack.Server.MinHaXML: instance [overlap ok] Read ElString
- Happstack.Server.MinHaXML: instance [overlap ok] Read StyleSheet
- Happstack.Server.MinHaXML: instance [overlap ok] Show ElString
- Happstack.Server.MinHaXML: instance [overlap ok] Show StyleSheet
- Happstack.Server.MinHaXML: instance [overlap ok] ToElement CalendarTime
- Happstack.Server.MinHaXML: instance [overlap ok] ToElement Double
- Happstack.Server.MinHaXML: instance [overlap ok] ToElement Element
- Happstack.Server.MinHaXML: instance [overlap ok] ToElement Float
- Happstack.Server.MinHaXML: instance [overlap ok] ToElement Int
- Happstack.Server.MinHaXML: instance [overlap ok] ToElement Integer
- Happstack.Server.MinHaXML: instance [overlap ok] ToElement String
- Happstack.Server.MinHaXML: instance [overlap ok] ToElement x => ToElement (Maybe x)
- Happstack.Server.MinHaXML: instance [overlap ok] Xml a => ToElement a
- Happstack.Server.MinHaXML: isCSS :: StyleSheet -> Bool
- Happstack.Server.MinHaXML: isXSL :: StyleSheet -> Bool
- Happstack.Server.MinHaXML: l :: Name -> [(Name, String)] -> [Element] -> Element
- Happstack.Server.MinHaXML: listElem :: Name -> [(Name, String)] -> [Element] -> Element
- Happstack.Server.MinHaXML: listToEl :: ToElement a => Name -> [(Name, String)] -> [a] -> Element
- Happstack.Server.MinHaXML: newtype ElString
- Happstack.Server.MinHaXML: nonEmpty :: Name -> String -> Maybe Element
- Happstack.Server.MinHaXML: quoteEsc :: String -> String
- Happstack.Server.MinHaXML: recToEl :: Name -> [a -> (String, String)] -> [a -> Element] -> a -> Element
- Happstack.Server.MinHaXML: simpleDoc :: StyleSheet -> Element -> String
- Happstack.Server.MinHaXML: simpleDoc' :: StyleSheet -> Element -> String
- Happstack.Server.MinHaXML: simpleDocOld :: StyleSheet -> Element -> String
- Happstack.Server.MinHaXML: simpleProlog :: StyleSheet -> Prolog
- Happstack.Server.MinHaXML: styleURL :: StyleSheet -> StyleURL
- Happstack.Server.MinHaXML: t :: Name -> [(Name, String)] -> CharData -> Element
- Happstack.Server.MinHaXML: textElem :: Name -> [(Name, String)] -> CharData -> Element
- Happstack.Server.MinHaXML: toAttrs :: t -> [(t1, t -> t2)] -> [(t1, t2)]
- Happstack.Server.MinHaXML: toElement :: ToElement x => x -> Element
- Happstack.Server.MinHaXML: type Element = Element
- Happstack.Server.MinHaXML: type StyleURL = String
- Happstack.Server.MinHaXML: verbim :: Verbatim a => a -> String
- Happstack.Server.MinHaXML: wrapElem :: ToElement x => Name -> x -> Element
- Happstack.Server.MinHaXML: xmlElem :: (t -> [Content]) -> Name -> [(Name, String)] -> t -> Element
- Happstack.Server.MinHaXML: xmlEscaper :: XmlEscaper
- Happstack.Server.MinHaXML: xmlStdEscape :: Element -> Element
- Happstack.Server.Parts: compressedResponseFilter :: (FilterMonad Response m, MonadPlus m, WebMonad Response m, ServerMonad m) => m String
- Happstack.Server.Parts: deflateFilter :: FilterMonad Response m => m ()
- Happstack.Server.Parts: encodings :: GenParser Char st [([Char], Maybe Double)]
- Happstack.Server.Parts: gzipFilter :: FilterMonad Response m => m ()
- Happstack.Server.SimpleHTTP: Append :: a -> SetAppend a
- Happstack.Server.SimpleHTTP: FilterT :: WriterT (FilterFun a) m b -> FilterT a m b
- Happstack.Server.SimpleHTTP: ServerPartT :: ReaderT Request (WebT m) a -> ServerPartT m a
- Happstack.Server.SimpleHTTP: Set :: a -> SetAppend a
- Happstack.Server.SimpleHTTP: WebT :: ErrorT Response (FilterT (Response) (MaybeT m)) a -> WebT m a
- Happstack.Server.SimpleHTTP: addCookie :: FilterMonad Response m => Seconds -> Cookie -> m ()
- Happstack.Server.SimpleHTTP: addCookies :: FilterMonad Response m => [(Seconds, Cookie)] -> m ()
- Happstack.Server.SimpleHTTP: addHeaderM :: FilterMonad Response m => String -> String -> m ()
- Happstack.Server.SimpleHTTP: anyPath :: (ServerMonad m, MonadPlus m) => m r -> m r
- Happstack.Server.SimpleHTTP: anyPath' :: (ServerMonad m, MonadPlus m) => m r -> m r
- Happstack.Server.SimpleHTTP: anyRequest :: Monad m => WebT m a -> ServerPartT m a
- Happstack.Server.SimpleHTTP: applyRequest :: (ToMessage a, Monad m, Functor m) => ServerPartT m a -> Request -> Either (m Response) b
- Happstack.Server.SimpleHTTP: askRq :: ServerMonad m => m Request
- Happstack.Server.SimpleHTTP: badGateway :: FilterMonad Response m => a -> m a
- Happstack.Server.SimpleHTTP: badRequest :: FilterMonad Response m => a -> m a
- Happstack.Server.SimpleHTTP: basicAuth :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadPlus m) => String -> Map String String -> m a -> m a
- Happstack.Server.SimpleHTTP: class Monad m => FilterMonad a m | m -> a
- Happstack.Server.SimpleHTTP: class FromData a
- Happstack.Server.SimpleHTTP: class FromReqURI a
- Happstack.Server.SimpleHTTP: class Monad m => ServerMonad m
- Happstack.Server.SimpleHTTP: class ToMessage a
- Happstack.Server.SimpleHTTP: class Monad m => WebMonad a m | m -> a
- Happstack.Server.SimpleHTTP: composeFilter :: FilterMonad a m => (a -> a) -> m ()
- Happstack.Server.SimpleHTTP: data SetAppend a
- Happstack.Server.SimpleHTTP: debugFilter :: (MonadIO m, Show a) => ServerPartT m a -> ServerPartT m a
- Happstack.Server.SimpleHTTP: dir :: (ServerMonad m, MonadPlus m) => String -> m a -> m a
- Happstack.Server.SimpleHTTP: dirs :: (ServerMonad m, MonadPlus m) => FilePath -> m a -> m a
- Happstack.Server.SimpleHTTP: doXslt :: MonadIO m => XSLTCmd -> XSLPath -> Response -> m Response
- Happstack.Server.SimpleHTTP: errorHandlerSP :: (Monad m, Error e) => (Request -> e -> WebT m a) -> ServerPartT (ErrorT e m) a -> ServerPartT m a
- Happstack.Server.SimpleHTTP: escape :: (WebMonad a m, FilterMonad a m) => m a -> m b
- Happstack.Server.SimpleHTTP: escape' :: (WebMonad a m, FilterMonad a m) => a -> m b
- Happstack.Server.SimpleHTTP: expireCookie :: FilterMonad Response m => String -> m ()
- Happstack.Server.SimpleHTTP: finishWith :: WebMonad a m => a -> m b
- Happstack.Server.SimpleHTTP: flatten :: (ToMessage a, Functor f) => f a -> f Response
- Happstack.Server.SimpleHTTP: forbidden :: FilterMonad Response m => a -> m a
- Happstack.Server.SimpleHTTP: found :: (FilterMonad Response m, ToSURI uri) => uri -> res -> m res
- Happstack.Server.SimpleHTTP: fromData :: FromData a => RqData a
- Happstack.Server.SimpleHTTP: fromReqURI :: FromReqURI a => String -> Maybe a
- Happstack.Server.SimpleHTTP: getData :: (ServerMonad m, FromData a) => m (Maybe a)
- Happstack.Server.SimpleHTTP: getDataFn :: ServerMonad m => RqData a -> m (Maybe a)
- Happstack.Server.SimpleHTTP: getFilter :: FilterMonad a m => m b -> m (b, a -> a)
- Happstack.Server.SimpleHTTP: getHeaderM :: ServerMonad m => String -> m (Maybe ByteString)
- Happstack.Server.SimpleHTTP: guardRq :: (ServerMonad m, MonadPlus m) => (Request -> Bool) -> m ()
- Happstack.Server.SimpleHTTP: host :: (ServerMonad m, MonadPlus m) => String -> m a -> m a
- Happstack.Server.SimpleHTTP: ifModifiedSince :: CalendarTime -> Request -> Response -> Response
- Happstack.Server.SimpleHTTP: ignoreFilters :: FilterMonad a m => m ()
- Happstack.Server.SimpleHTTP: instance [overlap ok] (Eq a, Show a, Xml a, Data a) => FromData a
- Happstack.Server.SimpleHTTP: instance [overlap ok] (Error e, ServerMonad m) => ServerMonad (ErrorT e m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] (FromData a, FromData b) => FromData (a, b)
- Happstack.Server.SimpleHTTP: instance [overlap ok] (FromData a, FromData b, FromData c) => FromData (a, b, c)
- Happstack.Server.SimpleHTTP: instance [overlap ok] (FromData a, FromData b, FromData c, FromData d) => FromData (a, b, c, d)
- Happstack.Server.SimpleHTTP: instance [overlap ok] (Monad m, Functor m) => Applicative (ServerPartT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] (Monad m, Functor m) => Applicative (WebT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] (Monad m, MonadError e m) => MonadError e (ServerPartT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] (Monad m, MonadReader r m) => MonadReader r (ServerPartT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] (Monad m, MonadWriter w m) => MonadWriter w (ServerPartT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Eq a => Eq (SetAppend a)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Error Response
- Happstack.Server.SimpleHTTP: instance [overlap ok] FromData a => FromData (Maybe a)
- Happstack.Server.SimpleHTTP: instance [overlap ok] FromReqURI Double
- Happstack.Server.SimpleHTTP: instance [overlap ok] FromReqURI Float
- Happstack.Server.SimpleHTTP: instance [overlap ok] FromReqURI Int
- Happstack.Server.SimpleHTTP: instance [overlap ok] FromReqURI Integer
- Happstack.Server.SimpleHTTP: instance [overlap ok] FromReqURI String
- Happstack.Server.SimpleHTTP: instance [overlap ok] Functor SetAppend
- Happstack.Server.SimpleHTTP: instance [overlap ok] Functor m => Functor (FilterT a m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Functor m => Functor (ServerPartT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Functor m => Functor (WebT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] MatchMethod ()
- Happstack.Server.SimpleHTTP: instance [overlap ok] MatchMethod (Method -> Bool)
- Happstack.Server.SimpleHTTP: instance [overlap ok] MatchMethod Method
- Happstack.Server.SimpleHTTP: instance [overlap ok] MatchMethod [Method]
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monad m => FilterMonad Response (ServerPartT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monad m => FilterMonad Response (WebT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monad m => FilterMonad a (FilterT a m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monad m => Monad (FilterT a m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monad m => Monad (ServerPartT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monad m => Monad (WebT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monad m => MonadPlus (ServerPartT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monad m => MonadPlus (WebT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monad m => Monoid (ServerPartT m a)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monad m => Monoid (WebT m a)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monad m => ServerMonad (ServerPartT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monad m => WebMonad Response (ServerPartT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monad m => WebMonad Response (WebT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] MonadError e m => MonadError e (WebT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] MonadIO m => MonadIO (FilterT a m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] MonadIO m => MonadIO (ServerPartT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] MonadIO m => MonadIO (WebT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] MonadReader r m => MonadReader r (WebT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] MonadState st m => MonadState st (WebT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] MonadTrans (FilterT a)
- Happstack.Server.SimpleHTTP: instance [overlap ok] MonadTrans ServerPartT
- Happstack.Server.SimpleHTTP: instance [overlap ok] MonadTrans WebT
- Happstack.Server.SimpleHTTP: instance [overlap ok] MonadWriter w m => MonadWriter w (WebT m)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Monoid a => Monoid (SetAppend a)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Show a => Show (SetAppend a)
- Happstack.Server.SimpleHTTP: instance [overlap ok] ToMessage ()
- Happstack.Server.SimpleHTTP: instance [overlap ok] ToMessage Html
- Happstack.Server.SimpleHTTP: instance [overlap ok] ToMessage Integer
- Happstack.Server.SimpleHTTP: instance [overlap ok] ToMessage Response
- Happstack.Server.SimpleHTTP: instance [overlap ok] ToMessage String
- Happstack.Server.SimpleHTTP: instance [overlap ok] ToMessage [Element]
- Happstack.Server.SimpleHTTP: instance [overlap ok] ToMessage a => ToMessage (Maybe a)
- Happstack.Server.SimpleHTTP: instance [overlap ok] Xml a => ToMessage a
- Happstack.Server.SimpleHTTP: internalServerError :: FilterMonad Response m => a -> m a
- Happstack.Server.SimpleHTTP: lazyProcValidator :: FilePath -> [String] -> Maybe FilePath -> Maybe [(String, String)] -> (Maybe ByteString -> Bool) -> Response -> IO Response
- Happstack.Server.SimpleHTTP: localContext :: Monad m => (WebT m a -> WebT m' a) -> ServerPartT m a -> ServerPartT m' a
- Happstack.Server.SimpleHTTP: localRq :: ServerMonad m => (Request -> Request) -> m a -> m a
- Happstack.Server.SimpleHTTP: look :: String -> RqData String
- Happstack.Server.SimpleHTTP: lookBS :: String -> RqData ByteString
- Happstack.Server.SimpleHTTP: lookCookie :: String -> RqData Cookie
- Happstack.Server.SimpleHTTP: lookCookieValue :: String -> RqData String
- Happstack.Server.SimpleHTTP: lookInput :: String -> RqData Input
- Happstack.Server.SimpleHTTP: lookPairs :: RqData [(String, String)]
- Happstack.Server.SimpleHTTP: lookRead :: Read a => String -> RqData a
- Happstack.Server.SimpleHTTP: mapServerPartT :: (UnWebT m a -> UnWebT n b) -> (ServerPartT m a -> ServerPartT n b)
- Happstack.Server.SimpleHTTP: mapServerPartT' :: (Request -> UnWebT m a -> UnWebT n b) -> (ServerPartT m a -> ServerPartT n b)
- Happstack.Server.SimpleHTTP: mapWebT :: (UnWebT m a -> UnWebT n b) -> (WebT m a -> WebT n b)
- Happstack.Server.SimpleHTTP: method :: (MatchMethod method, Monad m) => method -> WebT m a -> ServerPartT m a
- Happstack.Server.SimpleHTTP: methodM :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m ()
- Happstack.Server.SimpleHTTP: methodOnly :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m ()
- Happstack.Server.SimpleHTTP: methodSP :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m b -> m b
- Happstack.Server.SimpleHTTP: mkWebT :: UnWebT m a -> WebT m a
- Happstack.Server.SimpleHTTP: modifyResponse :: FilterMonad a m => (a -> a) -> m ()
- Happstack.Server.SimpleHTTP: movedPermanently :: (FilterMonad Response m, ToSURI a) => a -> res -> m res
- Happstack.Server.SimpleHTTP: multi :: Monad m => [ServerPartT m a] -> ServerPartT m a
- Happstack.Server.SimpleHTTP: newtype FilterT a m b
- Happstack.Server.SimpleHTTP: newtype ServerPartT m a
- Happstack.Server.SimpleHTTP: newtype WebT m a
- Happstack.Server.SimpleHTTP: noHandle :: MonadPlus m => m a
- Happstack.Server.SimpleHTTP: noopValidator :: Response -> IO Response
- Happstack.Server.SimpleHTTP: notFound :: FilterMonad Response m => a -> m a
- Happstack.Server.SimpleHTTP: nullDir :: (ServerMonad m, MonadPlus m) => m ()
- Happstack.Server.SimpleHTTP: ok :: FilterMonad Response m => a -> m a
- Happstack.Server.SimpleHTTP: path :: (FromReqURI a, MonadPlus m, ServerMonad m) => (a -> m b) -> m b
- Happstack.Server.SimpleHTTP: proxyServe :: (MonadIO m, WebMonad Response m, ServerMonad m, MonadPlus m, FilterMonad Response m) => [String] -> m Response
- Happstack.Server.SimpleHTTP: readCookieValue :: Read a => String -> RqData a
- Happstack.Server.SimpleHTTP: require :: (MonadIO m, MonadPlus m) => IO (Maybe a) -> (a -> m r) -> m r
- Happstack.Server.SimpleHTTP: requireM :: (MonadTrans t, Monad m, MonadPlus (t m)) => m (Maybe a) -> (a -> t m r) -> t m r
- Happstack.Server.SimpleHTTP: resp :: FilterMonad Response m => Int -> b -> m b
- Happstack.Server.SimpleHTTP: rproxyServe :: MonadIO m => String -> [(String, String)] -> ServerPartT m Response
- Happstack.Server.SimpleHTTP: runServerPartT :: ServerPartT m a -> Request -> WebT m a
- Happstack.Server.SimpleHTTP: runValidator :: (Response -> IO Response) -> Response -> IO Response
- Happstack.Server.SimpleHTTP: runWebT :: (Functor m, ToMessage b) => WebT m b -> m (Maybe Response)
- Happstack.Server.SimpleHTTP: seeOther :: (FilterMonad Response m, ToSURI uri) => uri -> res -> m res
- Happstack.Server.SimpleHTTP: setFilter :: FilterMonad a m => (a -> a) -> m ()
- Happstack.Server.SimpleHTTP: setHeaderM :: FilterMonad Response m => String -> String -> m ()
- Happstack.Server.SimpleHTTP: setResponseCode :: FilterMonad Response m => Int -> m ()
- Happstack.Server.SimpleHTTP: setValidator :: (Response -> IO Response) -> Response -> Response
- Happstack.Server.SimpleHTTP: setValidatorSP :: (Monad m, ToMessage r) => (Response -> IO Response) -> m r -> m Response
- Happstack.Server.SimpleHTTP: simpleErrorHandler :: Monad m => String -> ServerPartT m Response
- Happstack.Server.SimpleHTTP: spUnwrapErrorT :: Monad m => (e -> ServerPartT m a) -> Request -> UnWebT (ErrorT e m) a -> UnWebT m a
- Happstack.Server.SimpleHTTP: tempRedirect :: (FilterMonad Response m, ToSURI a) => a -> res -> m res
- Happstack.Server.SimpleHTTP: toContentType :: ToMessage a => a -> ByteString
- Happstack.Server.SimpleHTTP: toMessage :: ToMessage a => a -> ByteString
- Happstack.Server.SimpleHTTP: toResponse :: ToMessage a => a -> Response
- Happstack.Server.SimpleHTTP: toResponseBS :: ByteString -> ByteString -> Response
- Happstack.Server.SimpleHTTP: trailingSlash :: (ServerMonad m, MonadPlus m) => m ()
- Happstack.Server.SimpleHTTP: type FilterFun a = SetAppend (Dual (Endo a))
- Happstack.Server.SimpleHTTP: type RqData a = ReaderT ([(String, Input)], [(String, Cookie)]) Maybe a
- Happstack.Server.SimpleHTTP: type ServerPart a = ServerPartT IO a
- Happstack.Server.SimpleHTTP: type UnWebT m a = m (Maybe (Either Response a, FilterFun Response))
- Happstack.Server.SimpleHTTP: type Web a = WebT IO a
- Happstack.Server.SimpleHTTP: unFilterT :: FilterT a m b -> WriterT (FilterFun a) m b
- Happstack.Server.SimpleHTTP: unServerPartT :: ServerPartT m a -> ReaderT Request (WebT m) a
- Happstack.Server.SimpleHTTP: unWebT :: WebT m a -> ErrorT Response (FilterT (Response) (MaybeT m)) a
- Happstack.Server.SimpleHTTP: unauthorized :: FilterMonad Response m => a -> m a
- Happstack.Server.SimpleHTTP: ununWebT :: WebT m a -> UnWebT m a
- Happstack.Server.SimpleHTTP: uriRest :: ServerMonad m => (String -> m a) -> m a
- Happstack.Server.SimpleHTTP: validateConf :: Conf
- Happstack.Server.SimpleHTTP: wdgHTMLValidator :: (MonadIO m, ToMessage r) => r -> m Response
- Happstack.Server.SimpleHTTP: withData :: (FromData a, MonadPlus m, ServerMonad m) => (a -> m r) -> m r
- Happstack.Server.SimpleHTTP: withDataFn :: (MonadPlus m, ServerMonad m) => RqData a -> (a -> m r) -> m r
- Happstack.Server.SimpleHTTP: withHost :: (ServerMonad m, MonadPlus m) => (String -> m a) -> m a
- Happstack.Server.SimpleHTTP: withRequest :: (Request -> WebT m a) -> ServerPartT m a
- Happstack.Server.SimpleHTTP: xslt :: (MonadIO m, MonadPlus m, ToMessage r) => XSLTCmd -> XSLPath -> m r -> m Response
- Happstack.Server.StdConfig: binarylocation :: String
- Happstack.Server.StdConfig: errWrap :: (MonadPlus m, FilterMonad Response m, MonadIO m) => m Response
- Happstack.Server.StdConfig: loglocation :: String
- Happstack.Server.XSLT: xsltElem :: XSLPath -> Element -> String
+ Happstack.Server.Auth: basicAuth :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadPlus m) => String -> Map String String -> m a -> m a
+ Happstack.Server.Client: getResponse :: Request -> IO (Either String Response)
+ Happstack.Server.Compression: compressedResponseFilter :: (FilterMonad Response m, MonadPlus m, WebMonad Response m, ServerMonad m) => m String
+ Happstack.Server.Cookie: Expired :: CookieLife
+ Happstack.Server.Cookie: Expires :: UTCTime -> CookieLife
+ Happstack.Server.Cookie: MaxAge :: Seconds -> CookieLife
+ Happstack.Server.Cookie: Session :: CookieLife
+ Happstack.Server.Cookie: addCookie :: (MonadIO m, FilterMonad Response m) => CookieLife -> Cookie -> m ()
+ Happstack.Server.Cookie: addCookies :: (MonadIO m, FilterMonad Response m) => [(CookieLife, Cookie)] -> m ()
+ Happstack.Server.Cookie: data CookieLife
+ Happstack.Server.Cookie: expireCookie :: (MonadIO m, FilterMonad Response m) => String -> m ()
+ Happstack.Server.Error: errorHandlerSP :: (Monad m, Error e) => (Request -> e -> WebT m a) -> ServerPartT (ErrorT e m) a -> ServerPartT m a
+ Happstack.Server.Error: simpleErrorHandler :: Monad m => String -> ServerPartT m Response
+ Happstack.Server.Error: spUnwrapErrorT :: Monad m => (e -> ServerPartT m a) -> Request -> UnWebT (ErrorT e m) a -> UnWebT m a
+ Happstack.Server.FileServe: DisableBrowsing :: Browsing
+ Happstack.Server.FileServe: EnableBrowsing :: Browsing
+ Happstack.Server.FileServe: asContentType :: Monad m => String -> (FilePath -> m String)
+ Happstack.Server.FileServe: data Browsing
+ Happstack.Server.FileServe: defaultIxFiles :: [FilePath]
+ Happstack.Server.FileServe: fileServe :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => [FilePath] -> FilePath -> m Response
+ Happstack.Server.FileServe: guessContentTypeM :: Monad m => MimeMap -> (FilePath -> m String)
+ Happstack.Server.FileServe: mimeTypes :: MimeMap
+ Happstack.Server.FileServe: serveDirectory :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => Browsing -> [FilePath] -> FilePath -> m Response
+ Happstack.Server.FileServe: serveFile :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => (FilePath -> m String) -> FilePath -> m Response
+ Happstack.Server.FileServe: type MimeMap = Map String String
+ Happstack.Server.FileServe.BuildingBlocks: DisableBrowsing :: Browsing
+ Happstack.Server.FileServe.BuildingBlocks: EnableBrowsing :: Browsing
+ Happstack.Server.FileServe.BuildingBlocks: asContentType :: Monad m => String -> (FilePath -> m String)
+ Happstack.Server.FileServe.BuildingBlocks: blockDotFiles :: (Request -> IO Response) -> Request -> IO Response
+ Happstack.Server.FileServe.BuildingBlocks: data Browsing
+ Happstack.Server.FileServe.BuildingBlocks: defaultIxFiles :: [FilePath]
+ Happstack.Server.FileServe.BuildingBlocks: doIndex :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => [FilePath] -> MimeMap -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: doIndex' :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => (String -> FilePath -> m Response) -> (FilePath -> m String) -> [String] -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: doIndexLazy :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => [String] -> MimeMap -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: doIndexStrict :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => [String] -> MimeMap -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: fileNotFound :: (Monad m, FilterMonad Response m) => FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: filePathLazy :: (ServerMonad m, MonadIO m) => String -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: filePathSendFile :: (ServerMonad m, MonadIO m) => String -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: filePathStrict :: (ServerMonad m, MonadIO m) => String -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: fileServe :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => [FilePath] -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: fileServe' :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => (String -> FilePath -> m Response) -> (FilePath -> m String) -> (FilePath -> m Response) -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: fileServeLazy :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => [FilePath] -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: fileServeStrict :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => [FilePath] -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: guessContentType :: MimeMap -> FilePath -> Maybe String
+ Happstack.Server.FileServe.BuildingBlocks: guessContentTypeM :: Monad m => MimeMap -> (FilePath -> m String)
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Data Browsing
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Data EntryKind
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Enum Browsing
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Enum EntryKind
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Eq Browsing
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Eq EntryKind
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Ord Browsing
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Ord EntryKind
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Read Browsing
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Read EntryKind
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Show Browsing
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Show EntryKind
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Typeable Browsing
+ Happstack.Server.FileServe.BuildingBlocks: instance [overlap ok] Typeable EntryKind
+ Happstack.Server.FileServe.BuildingBlocks: isDot :: String -> Bool
+ Happstack.Server.FileServe.BuildingBlocks: lazyByteStringResponse :: String -> ByteString -> Maybe (CalendarTime, Request) -> Integer -> Integer -> Response
+ Happstack.Server.FileServe.BuildingBlocks: mimeTypes :: MimeMap
+ Happstack.Server.FileServe.BuildingBlocks: sendFileResponse :: String -> FilePath -> Maybe (CalendarTime, Request) -> Integer -> Integer -> Response
+ Happstack.Server.FileServe.BuildingBlocks: serveDirectory :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => Browsing -> [FilePath] -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: serveFile :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => (FilePath -> m String) -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: serveFileUsing :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => (String -> FilePath -> m Response) -> (FilePath -> m String) -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: strictByteStringResponse :: String -> ByteString -> Maybe (CalendarTime, Request) -> Integer -> Integer -> Response
+ Happstack.Server.FileServe.BuildingBlocks: tryIndex :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => (String -> FilePath -> m Response) -> (FilePath -> m String) -> [String] -> FilePath -> m Response
+ Happstack.Server.FileServe.BuildingBlocks: type MimeMap = Map String String
+ Happstack.Server.Internal.Compression: compressWithFilter :: FilterMonad Response m => (ByteString -> ByteString) -> String -> Bool -> m ()
+ Happstack.Server.Internal.Compression: compressedResponseFilter :: (FilterMonad Response m, MonadPlus m, WebMonad Response m, ServerMonad m) => m String
+ Happstack.Server.Internal.Compression: deflateFilter :: FilterMonad Response m => String -> Bool -> m ()
+ Happstack.Server.Internal.Compression: encodings :: GenParser Char st [([Char], Maybe Double)]
+ Happstack.Server.Internal.Compression: gzipFilter :: FilterMonad Response m => String -> Bool -> m ()
+ Happstack.Server.Internal.Cookie: Cookie :: String -> String -> String -> String -> String -> Bool -> Cookie
+ Happstack.Server.Internal.Cookie: Expired :: CookieLife
+ Happstack.Server.Internal.Cookie: Expires :: UTCTime -> CookieLife
+ Happstack.Server.Internal.Cookie: MaxAge :: Seconds -> CookieLife
+ Happstack.Server.Internal.Cookie: Session :: CookieLife
+ Happstack.Server.Internal.Cookie: calcLife :: CookieLife -> IO (Maybe (Seconds, UTCTime))
+ Happstack.Server.Internal.Cookie: cookieDomain :: Cookie -> String
+ Happstack.Server.Internal.Cookie: cookieName :: Cookie -> String
+ Happstack.Server.Internal.Cookie: cookiePath :: Cookie -> String
+ Happstack.Server.Internal.Cookie: cookieValue :: Cookie -> String
+ Happstack.Server.Internal.Cookie: cookieVersion :: Cookie -> String
+ Happstack.Server.Internal.Cookie: cookiesParser :: GenParser Char st [Cookie]
+ Happstack.Server.Internal.Cookie: data Cookie
+ Happstack.Server.Internal.Cookie: data CookieLife
+ Happstack.Server.Internal.Cookie: getCookie :: Monad m => String -> ByteString -> m Cookie
+ Happstack.Server.Internal.Cookie: getCookie' :: Monad m => String -> ByteString -> m (Either String Cookie)
+ Happstack.Server.Internal.Cookie: getCookies :: Monad m => ByteString -> m [Cookie]
+ Happstack.Server.Internal.Cookie: getCookies' :: Monad m => ByteString -> m (Either String [Cookie])
+ Happstack.Server.Internal.Cookie: instance [overlap ok] Data Cookie
+ Happstack.Server.Internal.Cookie: instance [overlap ok] Eq Cookie
+ Happstack.Server.Internal.Cookie: instance [overlap ok] Eq CookieLife
+ Happstack.Server.Internal.Cookie: instance [overlap ok] Ord CookieLife
+ Happstack.Server.Internal.Cookie: instance [overlap ok] Read Cookie
+ Happstack.Server.Internal.Cookie: instance [overlap ok] Read CookieLife
+ Happstack.Server.Internal.Cookie: instance [overlap ok] Show Cookie
+ Happstack.Server.Internal.Cookie: instance [overlap ok] Show CookieLife
+ Happstack.Server.Internal.Cookie: instance [overlap ok] Typeable Cookie
+ Happstack.Server.Internal.Cookie: instance [overlap ok] Typeable CookieLife
+ Happstack.Server.Internal.Cookie: mkCookie :: String -> String -> Cookie
+ Happstack.Server.Internal.Cookie: mkCookieHeader :: Maybe (Seconds, UTCTime) -> Cookie -> String
+ Happstack.Server.Internal.Cookie: parseCookies :: String -> Either String [Cookie]
+ Happstack.Server.Internal.Cookie: secure :: Cookie -> Bool
+ Happstack.Server.Internal.LowLevel: listen :: Conf -> (Request -> IO Response) -> IO ()
+ Happstack.Server.Internal.LowLevel: listen' :: Socket -> Conf -> (Request -> IO Response) -> IO ()
+ Happstack.Server.Internal.LowLevel: listenOn :: Int -> IO Socket
+ Happstack.Server.Internal.LowLevel: parseResponse :: ByteString -> Either String Response
+ Happstack.Server.Internal.LowLevel: putRequest :: Handle -> Request -> IO ()
+ Happstack.Server.Internal.LowLevel: request :: Handle -> Conf -> Socket -> Host -> (Request -> IO Response) -> IO ()
+ Happstack.Server.Internal.MessageWrap: BodyPolicy :: (Int64 -> Int64 -> Int64 -> InputWorker) -> Int64 -> Int64 -> Int64 -> BodyPolicy
+ Happstack.Server.Internal.MessageWrap: bodyInput :: MonadIO m => BodyPolicy -> Request -> m ([(String, Input)], Maybe String)
+ Happstack.Server.Internal.MessageWrap: class Read a => ReadString a
+ Happstack.Server.Internal.MessageWrap: data BodyPolicy
+ Happstack.Server.Internal.MessageWrap: decodeBody :: BodyPolicy -> Maybe ContentType -> ByteString -> IO ([(String, Input)], Maybe String)
+ Happstack.Server.Internal.MessageWrap: defaultBodyPolicy :: FilePath -> Int64 -> Int64 -> Int64 -> BodyPolicy
+ Happstack.Server.Internal.MessageWrap: defaultInputIter :: FileSaver -> FilePath -> Int64 -> Int64 -> Int64 -> Int64 -> Int64 -> Int64 -> Work -> IO InputIter
+ Happstack.Server.Internal.MessageWrap: formDecode :: String -> [(String, Input)]
+ Happstack.Server.Internal.MessageWrap: inputWorker :: BodyPolicy -> Int64 -> Int64 -> Int64 -> InputWorker
+ Happstack.Server.Internal.MessageWrap: instance [overlap ok] ReadString Char
+ Happstack.Server.Internal.MessageWrap: instance [overlap ok] ReadString Double
+ Happstack.Server.Internal.MessageWrap: instance [overlap ok] ReadString Float
+ Happstack.Server.Internal.MessageWrap: instance [overlap ok] ReadString Int
+ Happstack.Server.Internal.MessageWrap: instance [overlap ok] ReadString SURI
+ Happstack.Server.Internal.MessageWrap: instance [overlap ok] ReadString [Char]
+ Happstack.Server.Internal.MessageWrap: maxDisk :: BodyPolicy -> Int64
+ Happstack.Server.Internal.MessageWrap: maxHeader :: BodyPolicy -> Int64
+ Happstack.Server.Internal.MessageWrap: maxRAM :: BodyPolicy -> Int64
+ Happstack.Server.Internal.MessageWrap: multipartDecode :: InputWorker -> [(String, String)] -> ByteString -> IO ([(String, Input)], Maybe String)
+ Happstack.Server.Internal.MessageWrap: pathEls :: String -> [String]
+ Happstack.Server.Internal.MessageWrap: queryInput :: SURI -> [(String, Input)]
+ Happstack.Server.Internal.MessageWrap: readString :: ReadString a => String -> a
+ Happstack.Server.Internal.Monads: Append :: a -> SetAppend a
+ Happstack.Server.Internal.Monads: FilterT :: WriterT (FilterFun a) m b -> FilterT a m b
+ Happstack.Server.Internal.Monads: ServerPartT :: ReaderT Request (WebT m) a -> ServerPartT m a
+ Happstack.Server.Internal.Monads: Set :: a -> SetAppend a
+ Happstack.Server.Internal.Monads: WebT :: ErrorT Response (FilterT (Response) (MaybeT m)) a -> WebT m a
+ Happstack.Server.Internal.Monads: anyRequest :: Monad m => WebT m a -> ServerPartT m a
+ Happstack.Server.Internal.Monads: askRq :: ServerMonad m => m Request
+ Happstack.Server.Internal.Monads: class Monad m => FilterMonad a m | m -> a
+ Happstack.Server.Internal.Monads: class Monad m => ServerMonad m
+ Happstack.Server.Internal.Monads: class Monad m => WebMonad a m | m -> a
+ Happstack.Server.Internal.Monads: composeFilter :: FilterMonad a m => (a -> a) -> m ()
+ Happstack.Server.Internal.Monads: data SetAppend a
+ Happstack.Server.Internal.Monads: debugFilter :: (MonadIO m, Show a) => ServerPartT m a -> ServerPartT m a
+ Happstack.Server.Internal.Monads: escape :: (WebMonad a m, FilterMonad a m) => m a -> m b
+ Happstack.Server.Internal.Monads: escape' :: (WebMonad a m, FilterMonad a m) => a -> m b
+ Happstack.Server.Internal.Monads: escapeString :: String -> String
+ Happstack.Server.Internal.Monads: extract :: SetAppend t -> t
+ Happstack.Server.Internal.Monads: failHtml :: String -> String
+ Happstack.Server.Internal.Monads: filterFun :: (a -> a) -> FilterFun a
+ Happstack.Server.Internal.Monads: finishWith :: WebMonad a m => a -> m b
+ Happstack.Server.Internal.Monads: getFilter :: FilterMonad a m => m b -> m (b, a -> a)
+ Happstack.Server.Internal.Monads: ignoreFilters :: FilterMonad a m => m ()
+ Happstack.Server.Internal.Monads: instance [overlap ok] (Error e, ServerMonad m) => ServerMonad (ErrorT e m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] (Functor m, MonadPlus m) => Alternative (ServerPartT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] (Functor m, MonadPlus m) => Alternative (WebT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] (Monad m, Functor m) => Applicative (ServerPartT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] (Monad m, Functor m) => Applicative (WebT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] (Monad m, MonadError e m) => MonadError e (ServerPartT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] (Monad m, MonadReader r m) => MonadReader r (ServerPartT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] (Monad m, MonadState s m) => MonadState s (ServerPartT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] (Monad m, MonadWriter w m) => MonadWriter w (ServerPartT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Eq a => Eq (SetAppend a)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Functor SetAppend
+ Happstack.Server.Internal.Monads: instance [overlap ok] Functor m => Functor (FilterT a m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Functor m => Functor (ServerPartT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Functor m => Functor (WebT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monad m => FilterMonad Response (ServerPartT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monad m => FilterMonad Response (WebT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monad m => FilterMonad a (FilterT a m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monad m => Monad (FilterT a m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monad m => Monad (ServerPartT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monad m => Monad (WebT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monad m => MonadPlus (ServerPartT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monad m => MonadPlus (WebT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monad m => Monoid (ServerPartT m a)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monad m => Monoid (WebT m a)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monad m => ServerMonad (ServerPartT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monad m => WebMonad Response (ServerPartT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monad m => WebMonad Response (WebT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] MonadError e m => MonadError e (WebT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] MonadIO m => MonadIO (FilterT a m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] MonadIO m => MonadIO (ServerPartT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] MonadIO m => MonadIO (WebT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] MonadReader r m => MonadReader r (WebT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] MonadState st m => MonadState st (WebT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] MonadTrans (FilterT a)
+ Happstack.Server.Internal.Monads: instance [overlap ok] MonadTrans ServerPartT
+ Happstack.Server.Internal.Monads: instance [overlap ok] MonadTrans WebT
+ Happstack.Server.Internal.Monads: instance [overlap ok] MonadWriter w m => MonadWriter w (WebT m)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Monoid a => Monoid (SetAppend a)
+ Happstack.Server.Internal.Monads: instance [overlap ok] Show a => Show (SetAppend a)
+ Happstack.Server.Internal.Monads: localContext :: Monad m => (WebT m a -> WebT m' a) -> ServerPartT m a -> ServerPartT m' a
+ Happstack.Server.Internal.Monads: localRq :: ServerMonad m => (Request -> Request) -> m a -> m a
+ Happstack.Server.Internal.Monads: mapServerPartT :: (UnWebT m a -> UnWebT n b) -> (ServerPartT m a -> ServerPartT n b)
+ Happstack.Server.Internal.Monads: mapServerPartT' :: (Request -> UnWebT m a -> UnWebT n b) -> (ServerPartT m a -> ServerPartT n b)
+ Happstack.Server.Internal.Monads: mapWebT :: (UnWebT m a -> UnWebT n b) -> (WebT m a -> WebT n b)
+ Happstack.Server.Internal.Monads: mkFailMessage :: (FilterMonad Response m, WebMonad Response m) => String -> m b
+ Happstack.Server.Internal.Monads: mkWebT :: UnWebT m a -> WebT m a
+ Happstack.Server.Internal.Monads: multi :: Monad m => [ServerPartT m a] -> ServerPartT m a
+ Happstack.Server.Internal.Monads: newtype FilterT a m b
+ Happstack.Server.Internal.Monads: newtype ServerPartT m a
+ Happstack.Server.Internal.Monads: newtype WebT m a
+ Happstack.Server.Internal.Monads: outputTraceMessage :: String -> a -> a
+ Happstack.Server.Internal.Monads: runServerPartT :: ServerPartT m a -> Request -> WebT m a
+ Happstack.Server.Internal.Monads: setFilter :: FilterMonad a m => (a -> a) -> m ()
+ Happstack.Server.Internal.Monads: type FilterFun a = SetAppend (Dual (Endo a))
+ Happstack.Server.Internal.Monads: type ServerPart a = ServerPartT IO a
+ Happstack.Server.Internal.Monads: type UnWebT m a = m (Maybe (Either Response a, FilterFun Response))
+ Happstack.Server.Internal.Monads: type Web a = WebT IO a
+ Happstack.Server.Internal.Monads: unFilterFun :: FilterFun a -> (a -> a)
+ Happstack.Server.Internal.Monads: unFilterT :: FilterT a m b -> WriterT (FilterFun a) m b
+ Happstack.Server.Internal.Monads: unServerPartT :: ServerPartT m a -> ReaderT Request (WebT m) a
+ Happstack.Server.Internal.Monads: unWebT :: WebT m a -> ErrorT Response (FilterT (Response) (MaybeT m)) a
+ Happstack.Server.Internal.Monads: ununWebT :: WebT m a -> UnWebT m a
+ Happstack.Server.Internal.Monads: withRequest :: (Request -> WebT m a) -> ServerPartT m a
+ Happstack.Server.Internal.TimeoutManager: cancel :: Handle -> IO ()
+ Happstack.Server.Internal.TimeoutManager: data Handle
+ Happstack.Server.Internal.TimeoutManager: data Manager
+ Happstack.Server.Internal.TimeoutManager: initialize :: Int -> IO Manager
+ Happstack.Server.Internal.TimeoutManager: pause :: Handle -> IO ()
+ Happstack.Server.Internal.TimeoutManager: register :: Manager -> IO () -> IO Handle
+ Happstack.Server.Internal.TimeoutManager: resume :: Handle -> IO ()
+ Happstack.Server.Internal.TimeoutManager: tickle :: Handle -> IO ()
+ Happstack.Server.Internal.TimeoutSocket: iterTickle :: Handle -> IO Iter -> IO ()
+ Happstack.Server.Internal.TimeoutSocket: sGetContents :: Handle -> Socket -> IO ByteString
+ Happstack.Server.Internal.TimeoutSocket: sPutTickle :: Handle -> Socket -> ByteString -> IO ()
+ Happstack.Server.Internal.TimeoutSocket: sendFileTickle :: Handle -> Socket -> FilePath -> Offset -> ByteCount -> IO ()
+ Happstack.Server.Internal.Types: Body :: ByteString -> RqBody
+ Happstack.Server.Internal.Types: CONNECT :: Method
+ Happstack.Server.Internal.Types: Conf :: Int -> Maybe (Response -> IO Response) -> (forall t. FormatTime t => Maybe (String -> String -> t -> String -> Int -> Integer -> String -> String -> IO ())) -> Int -> Conf
+ Happstack.Server.Internal.Types: ContentLength :: Length
+ Happstack.Server.Internal.Types: ContentType :: String -> String -> [(String, String)] -> ContentType
+ Happstack.Server.Internal.Types: DELETE :: Method
+ Happstack.Server.Internal.Types: GET :: Method
+ Happstack.Server.Internal.Types: HEAD :: Method
+ Happstack.Server.Internal.Types: HeaderPair :: ByteString -> [ByteString] -> HeaderPair
+ Happstack.Server.Internal.Types: HttpVersion :: Int -> Int -> HttpVersion
+ Happstack.Server.Internal.Types: Input :: Either FilePath ByteString -> Maybe FilePath -> ContentType -> Input
+ Happstack.Server.Internal.Types: NoContentLength :: Length
+ Happstack.Server.Internal.Types: OPTIONS :: Method
+ Happstack.Server.Internal.Types: POST :: Method
+ Happstack.Server.Internal.Types: PUT :: Method
+ Happstack.Server.Internal.Types: Request :: Method -> [String] -> String -> String -> [(String, Input)] -> MVar [(String, Input)] -> [(String, Cookie)] -> HttpVersion -> Headers -> MVar RqBody -> Host -> Request
+ Happstack.Server.Internal.Types: Response :: Int -> Headers -> RsFlags -> ByteString -> Maybe (Response -> IO Response) -> Response
+ Happstack.Server.Internal.Types: RsFlags :: Length -> RsFlags
+ Happstack.Server.Internal.Types: SendFile :: Int -> Headers -> RsFlags -> Maybe (Response -> IO Response) -> FilePath -> Integer -> Integer -> Response
+ Happstack.Server.Internal.Types: TRACE :: Method
+ Happstack.Server.Internal.Types: TransferEncodingChunked :: Length
+ Happstack.Server.Internal.Types: addHeader :: HasHeaders r => String -> String -> r -> r
+ Happstack.Server.Internal.Types: addHeaderBS :: HasHeaders r => ByteString -> ByteString -> r -> r
+ Happstack.Server.Internal.Types: addHeaderUnsafe :: HasHeaders r => ByteString -> HeaderPair -> r -> r
+ Happstack.Server.Internal.Types: continueHTTP :: Request -> Response -> Bool
+ Happstack.Server.Internal.Types: ctParameters :: ContentType -> [(String, String)]
+ Happstack.Server.Internal.Types: ctSubtype :: ContentType -> String
+ Happstack.Server.Internal.Types: ctType :: ContentType -> String
+ Happstack.Server.Internal.Types: data Conf
+ Happstack.Server.Internal.Types: data ContentType
+ Happstack.Server.Internal.Types: data HeaderPair
+ Happstack.Server.Internal.Types: data HttpVersion
+ Happstack.Server.Internal.Types: data Input
+ Happstack.Server.Internal.Types: data Length
+ Happstack.Server.Internal.Types: data Method
+ Happstack.Server.Internal.Types: data Request
+ Happstack.Server.Internal.Types: data Response
+ Happstack.Server.Internal.Types: data RsFlags
+ Happstack.Server.Internal.Types: getHeader :: HasHeaders r => String -> r -> Maybe ByteString
+ Happstack.Server.Internal.Types: getHeaderBS :: HasHeaders r => ByteString -> r -> Maybe ByteString
+ Happstack.Server.Internal.Types: getHeaderUnsafe :: HasHeaders r => ByteString -> r -> Maybe ByteString
+ Happstack.Server.Internal.Types: hName :: HeaderPair -> ByteString
+ Happstack.Server.Internal.Types: hValue :: HeaderPair -> [ByteString]
+ Happstack.Server.Internal.Types: hasHeader :: HasHeaders r => String -> r -> Bool
+ Happstack.Server.Internal.Types: hasHeaderBS :: HasHeaders r => ByteString -> r -> Bool
+ Happstack.Server.Internal.Types: hasHeaderUnsafe :: HasHeaders r => ByteString -> r -> Bool
+ Happstack.Server.Internal.Types: inputContentType :: Input -> ContentType
+ Happstack.Server.Internal.Types: inputFilename :: Input -> Maybe FilePath
+ Happstack.Server.Internal.Types: inputValue :: Input -> Either FilePath ByteString
+ Happstack.Server.Internal.Types: instance [overlap ok] Data Method
+ Happstack.Server.Internal.Types: instance [overlap ok] Enum Length
+ Happstack.Server.Internal.Types: instance [overlap ok] Eq HttpVersion
+ Happstack.Server.Internal.Types: instance [overlap ok] Eq Length
+ Happstack.Server.Internal.Types: instance [overlap ok] Eq Method
+ Happstack.Server.Internal.Types: instance [overlap ok] Error Response
+ Happstack.Server.Internal.Types: instance [overlap ok] HasHeaders Headers
+ Happstack.Server.Internal.Types: instance [overlap ok] HasHeaders Request
+ Happstack.Server.Internal.Types: instance [overlap ok] HasHeaders Response
+ Happstack.Server.Internal.Types: instance [overlap ok] Ord Length
+ Happstack.Server.Internal.Types: instance [overlap ok] Ord Method
+ Happstack.Server.Internal.Types: instance [overlap ok] Read HeaderPair
+ Happstack.Server.Internal.Types: instance [overlap ok] Read HttpVersion
+ Happstack.Server.Internal.Types: instance [overlap ok] Read Input
+ Happstack.Server.Internal.Types: instance [overlap ok] Read Length
+ Happstack.Server.Internal.Types: instance [overlap ok] Read Method
+ Happstack.Server.Internal.Types: instance [overlap ok] Read RqBody
+ Happstack.Server.Internal.Types: instance [overlap ok] Read RsFlags
+ Happstack.Server.Internal.Types: instance [overlap ok] Show HeaderPair
+ Happstack.Server.Internal.Types: instance [overlap ok] Show HttpVersion
+ Happstack.Server.Internal.Types: instance [overlap ok] Show Input
+ Happstack.Server.Internal.Types: instance [overlap ok] Show Length
+ Happstack.Server.Internal.Types: instance [overlap ok] Show Method
+ Happstack.Server.Internal.Types: instance [overlap ok] Show Request
+ Happstack.Server.Internal.Types: instance [overlap ok] Show Response
+ Happstack.Server.Internal.Types: instance [overlap ok] Show RqBody
+ Happstack.Server.Internal.Types: instance [overlap ok] Show RsFlags
+ Happstack.Server.Internal.Types: instance [overlap ok] Typeable Input
+ Happstack.Server.Internal.Types: instance [overlap ok] Typeable Method
+ Happstack.Server.Internal.Types: instance [overlap ok] Typeable Request
+ Happstack.Server.Internal.Types: instance [overlap ok] Typeable Response
+ Happstack.Server.Internal.Types: instance [overlap ok] Typeable RqBody
+ Happstack.Server.Internal.Types: instance [overlap ok] Typeable RsFlags
+ Happstack.Server.Internal.Types: isHTTP1_0 :: Request -> Bool
+ Happstack.Server.Internal.Types: isHTTP1_1 :: Request -> Bool
+ Happstack.Server.Internal.Types: logAccess :: Conf -> forall t. FormatTime t => Maybe (String -> String -> t -> String -> Int -> Integer -> String -> String -> IO ())
+ Happstack.Server.Internal.Types: mkHeaders :: [(String, String)] -> Headers
+ Happstack.Server.Internal.Types: newtype RqBody
+ Happstack.Server.Internal.Types: noContentLength :: Response -> Response
+ Happstack.Server.Internal.Types: nullConf :: Conf
+ Happstack.Server.Internal.Types: nullRsFlags :: RsFlags
+ Happstack.Server.Internal.Types: port :: Conf -> Int
+ Happstack.Server.Internal.Types: readInputsBody :: Request -> IO (Maybe [(String, Input)])
+ Happstack.Server.Internal.Types: redirect :: ToSURI s => Int -> s -> Response -> Response
+ Happstack.Server.Internal.Types: result :: Int -> String -> Response
+ Happstack.Server.Internal.Types: resultBS :: Int -> ByteString -> Response
+ Happstack.Server.Internal.Types: rqBody :: Request -> MVar RqBody
+ Happstack.Server.Internal.Types: rqCookies :: Request -> [(String, Cookie)]
+ Happstack.Server.Internal.Types: rqHeaders :: Request -> Headers
+ Happstack.Server.Internal.Types: rqInputsBody :: Request -> MVar [(String, Input)]
+ Happstack.Server.Internal.Types: rqInputsQuery :: Request -> [(String, Input)]
+ Happstack.Server.Internal.Types: rqMethod :: Request -> Method
+ Happstack.Server.Internal.Types: rqPaths :: Request -> [String]
+ Happstack.Server.Internal.Types: rqPeer :: Request -> Host
+ Happstack.Server.Internal.Types: rqQuery :: Request -> String
+ Happstack.Server.Internal.Types: rqURL :: Request -> String
+ Happstack.Server.Internal.Types: rqUri :: Request -> String
+ Happstack.Server.Internal.Types: rqVersion :: Request -> HttpVersion
+ Happstack.Server.Internal.Types: rsBody :: Response -> ByteString
+ Happstack.Server.Internal.Types: rsCode :: Response -> Int
+ Happstack.Server.Internal.Types: rsFlags :: Response -> RsFlags
+ Happstack.Server.Internal.Types: rsHeaders :: Response -> Headers
+ Happstack.Server.Internal.Types: rsValidator :: Response -> Maybe (Response -> IO Response)
+ Happstack.Server.Internal.Types: rsfLength :: RsFlags -> Length
+ Happstack.Server.Internal.Types: setHeader :: HasHeaders r => String -> String -> r -> r
+ Happstack.Server.Internal.Types: setHeaderBS :: HasHeaders r => ByteString -> ByteString -> r -> r
+ Happstack.Server.Internal.Types: setHeaderUnsafe :: HasHeaders r => ByteString -> HeaderPair -> r -> r
+ Happstack.Server.Internal.Types: setRsCode :: Monad m => Int -> Response -> m Response
+ Happstack.Server.Internal.Types: sfCount :: Response -> Integer
+ Happstack.Server.Internal.Types: sfFilePath :: Response -> FilePath
+ Happstack.Server.Internal.Types: sfOffset :: Response -> Integer
+ Happstack.Server.Internal.Types: takeRequestBody :: MonadIO m => Request -> m (Maybe RqBody)
+ Happstack.Server.Internal.Types: timeout :: Conf -> Int
+ Happstack.Server.Internal.Types: type Headers = Map ByteString HeaderPair
+ Happstack.Server.Internal.Types: type Host = (String, Int)
+ Happstack.Server.Internal.Types: unBody :: RqBody -> ByteString
+ Happstack.Server.Internal.Types: validator :: Conf -> Maybe (Response -> IO Response)
+ Happstack.Server.Monads: addHeaderM :: FilterMonad Response m => String -> String -> m ()
+ Happstack.Server.Monads: askRq :: ServerMonad m => m Request
+ Happstack.Server.Monads: class Monad m => FilterMonad a m | m -> a
+ Happstack.Server.Monads: class (ServerMonad m, WebMonad Response m, FilterMonad Response m, MonadIO m, MonadPlus m, HasRqData m, Monad m, Functor m) => Happstack m
+ Happstack.Server.Monads: class Monad m => ServerMonad m
+ Happstack.Server.Monads: class Monad m => WebMonad a m | m -> a
+ Happstack.Server.Monads: composeFilter :: FilterMonad a m => (a -> a) -> m ()
+ Happstack.Server.Monads: data ServerPartT m a
+ Happstack.Server.Monads: escape :: (WebMonad a m, FilterMonad a m) => m a -> m b
+ Happstack.Server.Monads: escape' :: (WebMonad a m, FilterMonad a m) => a -> m b
+ Happstack.Server.Monads: filterFun :: (a -> a) -> FilterFun a
+ Happstack.Server.Monads: finishWith :: WebMonad a m => a -> m b
+ Happstack.Server.Monads: getFilter :: FilterMonad a m => m b -> m (b, a -> a)
+ Happstack.Server.Monads: getHeaderM :: ServerMonad m => String -> m (Maybe ByteString)
+ Happstack.Server.Monads: ignoreFilters :: FilterMonad a m => m ()
+ Happstack.Server.Monads: instance [overlap ok] (Functor m, Monad m, MonadPlus m, MonadIO m) => Happstack (ServerPartT m)
+ Happstack.Server.Monads: localRq :: ServerMonad m => (Request -> Request) -> m a -> m a
+ Happstack.Server.Monads: mapServerPartT :: (UnWebT m a -> UnWebT n b) -> (ServerPartT m a -> ServerPartT n b)
+ Happstack.Server.Monads: mapServerPartT' :: (Request -> UnWebT m a -> UnWebT n b) -> (ServerPartT m a -> ServerPartT n b)
+ Happstack.Server.Monads: require :: (MonadIO m, MonadPlus m) => IO (Maybe a) -> (a -> m r) -> m r
+ Happstack.Server.Monads: requireM :: (MonadTrans t, Monad m, MonadPlus (t m)) => m (Maybe a) -> (a -> t m r) -> t m r
+ Happstack.Server.Monads: setFilter :: FilterMonad a m => (a -> a) -> m ()
+ Happstack.Server.Monads: setHeaderM :: FilterMonad Response m => String -> String -> m ()
+ Happstack.Server.Monads: type ServerPart a = ServerPartT IO a
+ Happstack.Server.Monads: type UnWebT m a = m (Maybe (Either Response a, FilterFun Response))
+ Happstack.Server.Proxy: proxyServe :: (MonadIO m, WebMonad Response m, ServerMonad m, MonadPlus m, FilterMonad Response m) => [String] -> m Response
+ Happstack.Server.Proxy: proxyServe' :: (MonadIO m, FilterMonad Response m, WebMonad Response m) => Request -> m Response
+ Happstack.Server.Proxy: rproxyServe :: (ServerMonad m, WebMonad Response m, FilterMonad Response m, MonadIO m) => String -> [(String, String)] -> m Response
+ Happstack.Server.Proxy: unproxify :: Request -> Request
+ Happstack.Server.Proxy: unrproxify :: String -> [(String, String)] -> Request -> Request
+ Happstack.Server.Response: badGateway :: FilterMonad Response m => a -> m a
+ Happstack.Server.Response: badRequest :: FilterMonad Response m => a -> m a
+ Happstack.Server.Response: class ToMessage a
+ Happstack.Server.Response: flatten :: (ToMessage a, Functor f) => f a -> f Response
+ Happstack.Server.Response: forbidden :: FilterMonad Response m => a -> m a
+ Happstack.Server.Response: found :: (FilterMonad Response m, ToSURI uri) => uri -> res -> m res
+ Happstack.Server.Response: ifModifiedSince :: CalendarTime -> Request -> Response -> Response
+ Happstack.Server.Response: instance [overlap ok] ToMessage ()
+ Happstack.Server.Response: instance [overlap ok] ToMessage Html
+ Happstack.Server.Response: instance [overlap ok] ToMessage Integer
+ Happstack.Server.Response: instance [overlap ok] ToMessage Response
+ Happstack.Server.Response: instance [overlap ok] ToMessage String
+ Happstack.Server.Response: instance [overlap ok] ToMessage Text
+ Happstack.Server.Response: instance [overlap ok] ToMessage a => ToMessage (Maybe a)
+ Happstack.Server.Response: internalServerError :: FilterMonad Response m => a -> m a
+ Happstack.Server.Response: movedPermanently :: (FilterMonad Response m, ToSURI a) => a -> res -> m res
+ Happstack.Server.Response: noContent :: FilterMonad Response m => a -> m a
+ Happstack.Server.Response: notFound :: FilterMonad Response m => a -> m a
+ Happstack.Server.Response: ok :: FilterMonad Response m => a -> m a
+ Happstack.Server.Response: resp :: FilterMonad Response m => Int -> b -> m b
+ Happstack.Server.Response: seeOther :: (FilterMonad Response m, ToSURI uri) => uri -> res -> m res
+ Happstack.Server.Response: setResponseCode :: FilterMonad Response m => Int -> m ()
+ Happstack.Server.Response: tempRedirect :: (FilterMonad Response m, ToSURI a) => a -> res -> m res
+ Happstack.Server.Response: toContentType :: ToMessage a => a -> ByteString
+ Happstack.Server.Response: toMessage :: ToMessage a => a -> ByteString
+ Happstack.Server.Response: toResponse :: ToMessage a => a -> Response
+ Happstack.Server.Response: toResponseBS :: ByteString -> ByteString -> Response
+ Happstack.Server.Response: unauthorized :: FilterMonad Response m => a -> m a
+ Happstack.Server.Routing: anyPath :: (ServerMonad m, MonadPlus m) => m r -> m r
+ Happstack.Server.Routing: class FromReqURI a
+ Happstack.Server.Routing: class MatchMethod m
+ Happstack.Server.Routing: dir :: (ServerMonad m, MonadPlus m) => String -> m a -> m a
+ Happstack.Server.Routing: dirs :: (ServerMonad m, MonadPlus m) => FilePath -> m a -> m a
+ Happstack.Server.Routing: fromReqURI :: FromReqURI a => String -> Maybe a
+ Happstack.Server.Routing: guardRq :: (ServerMonad m, MonadPlus m) => (Request -> Bool) -> m ()
+ Happstack.Server.Routing: host :: (ServerMonad m, MonadPlus m) => String -> m a -> m a
+ Happstack.Server.Routing: instance [overlap ok] FromReqURI Double
+ Happstack.Server.Routing: instance [overlap ok] FromReqURI Float
+ Happstack.Server.Routing: instance [overlap ok] FromReqURI Int
+ Happstack.Server.Routing: instance [overlap ok] FromReqURI Integer
+ Happstack.Server.Routing: instance [overlap ok] FromReqURI String
+ Happstack.Server.Routing: instance [overlap ok] MatchMethod ()
+ Happstack.Server.Routing: instance [overlap ok] MatchMethod (Method -> Bool)
+ Happstack.Server.Routing: instance [overlap ok] MatchMethod Method
+ Happstack.Server.Routing: instance [overlap ok] MatchMethod [Method]
+ Happstack.Server.Routing: matchMethod :: MatchMethod m => m -> Method -> Bool
+ Happstack.Server.Routing: method :: (MatchMethod method, Monad m) => method -> WebT m a -> ServerPartT m a
+ Happstack.Server.Routing: methodM :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m ()
+ Happstack.Server.Routing: methodOnly :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m ()
+ Happstack.Server.Routing: methodSP :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m b -> m b
+ Happstack.Server.Routing: nullDir :: (ServerMonad m, MonadPlus m) => m ()
+ Happstack.Server.Routing: path :: (FromReqURI a, MonadPlus m, ServerMonad m) => (a -> m b) -> m b
+ Happstack.Server.Routing: trailingSlash :: (ServerMonad m, MonadPlus m) => m ()
+ Happstack.Server.Routing: uriRest :: ServerMonad m => (String -> m a) -> m a
+ Happstack.Server.Routing: withHost :: (ServerMonad m, MonadPlus m) => (String -> m a) -> m a
+ Happstack.Server.RqData: BodyPolicy :: (Int64 -> Int64 -> Int64 -> InputWorker) -> Int64 -> Int64 -> Int64 -> BodyPolicy
+ Happstack.Server.RqData: Errors :: [a] -> Errors a
+ Happstack.Server.RqData: askRqEnv :: HasRqData m => m RqEnv
+ Happstack.Server.RqData: body :: HasRqData m => m a -> m a
+ Happstack.Server.RqData: checkRq :: (Monad m, HasRqData m) => m a -> (a -> Either String b) -> m b
+ Happstack.Server.RqData: checkRqM :: (Monad m, HasRqData m) => m a -> (a -> m (Either String b)) -> m b
+ Happstack.Server.RqData: class FromData a
+ Happstack.Server.RqData: class HasRqData m
+ Happstack.Server.RqData: data BodyPolicy
+ Happstack.Server.RqData: data RqData a
+ Happstack.Server.RqData: decodeBody :: (ServerMonad m, MonadPlus m, MonadIO m) => BodyPolicy -> m ()
+ Happstack.Server.RqData: defaultBodyPolicy :: FilePath -> Int64 -> Int64 -> Int64 -> BodyPolicy
+ Happstack.Server.RqData: fromData :: FromData a => RqData a
+ Happstack.Server.RqData: getData :: (HasRqData m, MonadIO m, ServerMonad m, FromData a) => m (Either [String] a)
+ Happstack.Server.RqData: getDataFn :: (HasRqData m, ServerMonad m, MonadIO m) => RqData a -> m (Either [String] a)
+ Happstack.Server.RqData: inputWorker :: BodyPolicy -> Int64 -> Int64 -> Int64 -> InputWorker
+ Happstack.Server.RqData: instance [overlap ok] (FromData a, FromData b) => FromData (a, b)
+ Happstack.Server.RqData: instance [overlap ok] (FromData a, FromData b, FromData c) => FromData (a, b, c)
+ Happstack.Server.RqData: instance [overlap ok] (FromData a, FromData b, FromData c, FromData d) => FromData (a, b, c, d)
+ Happstack.Server.RqData: instance [overlap ok] (Monoid e, Error e) => Alternative (ReaderError r e)
+ Happstack.Server.RqData: instance [overlap ok] (Monoid e, Error e) => Applicative (ReaderError r e)
+ Happstack.Server.RqData: instance [overlap ok] Alternative RqData
+ Happstack.Server.RqData: instance [overlap ok] Applicative RqData
+ Happstack.Server.RqData: instance [overlap ok] Data a => Data (Errors a)
+ Happstack.Server.RqData: instance [overlap ok] Eq a => Eq (Errors a)
+ Happstack.Server.RqData: instance [overlap ok] Error (Errors String)
+ Happstack.Server.RqData: instance [overlap ok] Error e => MonadPlus (ReaderError r e)
+ Happstack.Server.RqData: instance [overlap ok] Error e => MonadReader r (ReaderError r e)
+ Happstack.Server.RqData: instance [overlap ok] FromData a => FromData (Maybe a)
+ Happstack.Server.RqData: instance [overlap ok] Functor (ReaderError r e)
+ Happstack.Server.RqData: instance [overlap ok] Functor RqData
+ Happstack.Server.RqData: instance [overlap ok] HasRqData RqData
+ Happstack.Server.RqData: instance [overlap ok] Monad (ReaderError r e)
+ Happstack.Server.RqData: instance [overlap ok] Monad RqData
+ Happstack.Server.RqData: instance [overlap ok] MonadIO m => HasRqData (ServerPartT m)
+ Happstack.Server.RqData: instance [overlap ok] MonadPlus RqData
+ Happstack.Server.RqData: instance [overlap ok] MonadReader RqEnv RqData
+ Happstack.Server.RqData: instance [overlap ok] Monoid (Errors a)
+ Happstack.Server.RqData: instance [overlap ok] Ord a => Ord (Errors a)
+ Happstack.Server.RqData: instance [overlap ok] Read a => Read (Errors a)
+ Happstack.Server.RqData: instance [overlap ok] Show a => Show (Errors a)
+ Happstack.Server.RqData: instance [overlap ok] Typeable1 Errors
+ Happstack.Server.RqData: localRqEnv :: HasRqData m => (RqEnv -> RqEnv) -> m a -> m a
+ Happstack.Server.RqData: look :: (Functor m, Monad m, HasRqData m) => String -> m String
+ Happstack.Server.RqData: lookBS :: (Functor m, Monad m, HasRqData m) => String -> m ByteString
+ Happstack.Server.RqData: lookBSs :: (Functor m, Monad m, HasRqData m) => String -> m [ByteString]
+ Happstack.Server.RqData: lookCookie :: (Monad m, HasRqData m) => String -> m Cookie
+ Happstack.Server.RqData: lookCookieValue :: (Functor m, Monad m, HasRqData m) => String -> m String
+ Happstack.Server.RqData: lookFile :: (Monad m, HasRqData m) => String -> m (FilePath, FilePath, ContentType)
+ Happstack.Server.RqData: lookInput :: (Monad m, HasRqData m) => String -> m Input
+ Happstack.Server.RqData: lookInputs :: (Monad m, HasRqData m) => String -> m [Input]
+ Happstack.Server.RqData: lookPairs :: (Monad m, HasRqData m) => m [(String, Either FilePath String)]
+ Happstack.Server.RqData: lookPairsBS :: (Monad m, HasRqData m) => m [(String, Either FilePath ByteString)]
+ Happstack.Server.RqData: lookRead :: (Functor m, Monad m, HasRqData m, Read a) => String -> m a
+ Happstack.Server.RqData: lookReads :: (Functor m, Monad m, HasRqData m, Read a) => String -> m [a]
+ Happstack.Server.RqData: looks :: (Functor m, Monad m, HasRqData m) => String -> m [String]
+ Happstack.Server.RqData: mapRqData :: (Either (Errors String) a -> Either (Errors String) b) -> RqData a -> RqData b
+ Happstack.Server.RqData: maxDisk :: BodyPolicy -> Int64
+ Happstack.Server.RqData: maxHeader :: BodyPolicy -> Int64
+ Happstack.Server.RqData: maxRAM :: BodyPolicy -> Int64
+ Happstack.Server.RqData: newtype Errors a
+ Happstack.Server.RqData: queryString :: HasRqData m => m a -> m a
+ Happstack.Server.RqData: readCookieValue :: (Functor m, Monad m, HasRqData m, Read a) => String -> m a
+ Happstack.Server.RqData: readRq :: Read a => String -> String -> Either String a
+ Happstack.Server.RqData: rqDataError :: HasRqData m => Errors String -> m a
+ Happstack.Server.RqData: type RqEnv = ([(String, Input)], [(String, Input)], [(String, Cookie)])
+ Happstack.Server.RqData: unErrors :: Errors a -> [a]
+ Happstack.Server.RqData: withData :: (HasRqData m, MonadIO m, FromData a, MonadPlus m, ServerMonad m) => (a -> m r) -> m r
+ Happstack.Server.RqData: withDataFn :: (HasRqData m, MonadIO m, MonadPlus m, ServerMonad m) => RqData a -> (a -> m r) -> m r
+ Happstack.Server.Types: Body :: ByteString -> RqBody
+ Happstack.Server.Types: CONNECT :: Method
+ Happstack.Server.Types: Conf :: Int -> Maybe (Response -> IO Response) -> (forall t. FormatTime t => Maybe (String -> String -> t -> String -> Int -> Integer -> String -> String -> IO ())) -> Int -> Conf
+ Happstack.Server.Types: ContentLength :: Length
+ Happstack.Server.Types: ContentType :: String -> String -> [(String, String)] -> ContentType
+ Happstack.Server.Types: DELETE :: Method
+ Happstack.Server.Types: GET :: Method
+ Happstack.Server.Types: HEAD :: Method
+ Happstack.Server.Types: HeaderPair :: ByteString -> [ByteString] -> HeaderPair
+ Happstack.Server.Types: HttpVersion :: Int -> Int -> HttpVersion
+ Happstack.Server.Types: Input :: Either FilePath ByteString -> Maybe FilePath -> ContentType -> Input
+ Happstack.Server.Types: NoContentLength :: Length
+ Happstack.Server.Types: OPTIONS :: Method
+ Happstack.Server.Types: POST :: Method
+ Happstack.Server.Types: PUT :: Method
+ Happstack.Server.Types: Request :: Method -> [String] -> String -> String -> [(String, Input)] -> MVar [(String, Input)] -> [(String, Cookie)] -> HttpVersion -> Headers -> MVar RqBody -> Host -> Request
+ Happstack.Server.Types: Response :: Int -> Headers -> RsFlags -> ByteString -> Maybe (Response -> IO Response) -> Response
+ Happstack.Server.Types: RsFlags :: Length -> RsFlags
+ Happstack.Server.Types: SendFile :: Int -> Headers -> RsFlags -> Maybe (Response -> IO Response) -> FilePath -> Integer -> Integer -> Response
+ Happstack.Server.Types: TRACE :: Method
+ Happstack.Server.Types: TransferEncodingChunked :: Length
+ Happstack.Server.Types: addHeader :: HasHeaders r => String -> String -> r -> r
+ Happstack.Server.Types: addHeaderBS :: HasHeaders r => ByteString -> ByteString -> r -> r
+ Happstack.Server.Types: addHeaderUnsafe :: HasHeaders r => ByteString -> HeaderPair -> r -> r
+ Happstack.Server.Types: continueHTTP :: Request -> Response -> Bool
+ Happstack.Server.Types: ctParameters :: ContentType -> [(String, String)]
+ Happstack.Server.Types: ctSubtype :: ContentType -> String
+ Happstack.Server.Types: ctType :: ContentType -> String
+ Happstack.Server.Types: data Conf
+ Happstack.Server.Types: data ContentType
+ Happstack.Server.Types: data HeaderPair
+ Happstack.Server.Types: data HttpVersion
+ Happstack.Server.Types: data Input
+ Happstack.Server.Types: data Length
+ Happstack.Server.Types: data Method
+ Happstack.Server.Types: data Request
+ Happstack.Server.Types: data Response
+ Happstack.Server.Types: data RsFlags
+ Happstack.Server.Types: getHeader :: HasHeaders r => String -> r -> Maybe ByteString
+ Happstack.Server.Types: getHeaderBS :: HasHeaders r => ByteString -> r -> Maybe ByteString
+ Happstack.Server.Types: getHeaderUnsafe :: HasHeaders r => ByteString -> r -> Maybe ByteString
+ Happstack.Server.Types: hName :: HeaderPair -> ByteString
+ Happstack.Server.Types: hValue :: HeaderPair -> [ByteString]
+ Happstack.Server.Types: hasHeader :: HasHeaders r => String -> r -> Bool
+ Happstack.Server.Types: hasHeaderBS :: HasHeaders r => ByteString -> r -> Bool
+ Happstack.Server.Types: hasHeaderUnsafe :: HasHeaders r => ByteString -> r -> Bool
+ Happstack.Server.Types: inputContentType :: Input -> ContentType
+ Happstack.Server.Types: inputFilename :: Input -> Maybe FilePath
+ Happstack.Server.Types: inputValue :: Input -> Either FilePath ByteString
+ Happstack.Server.Types: isHTTP1_0 :: Request -> Bool
+ Happstack.Server.Types: isHTTP1_1 :: Request -> Bool
+ Happstack.Server.Types: logAccess :: Conf -> forall t. FormatTime t => Maybe (String -> String -> t -> String -> Int -> Integer -> String -> String -> IO ())
+ Happstack.Server.Types: mkHeaders :: [(String, String)] -> Headers
+ Happstack.Server.Types: newtype RqBody
+ Happstack.Server.Types: noContentLength :: Response -> Response
+ Happstack.Server.Types: nullConf :: Conf
+ Happstack.Server.Types: nullRsFlags :: RsFlags
+ Happstack.Server.Types: port :: Conf -> Int
+ Happstack.Server.Types: readInputsBody :: Request -> IO (Maybe [(String, Input)])
+ Happstack.Server.Types: redirect :: ToSURI s => Int -> s -> Response -> Response
+ Happstack.Server.Types: result :: Int -> String -> Response
+ Happstack.Server.Types: resultBS :: Int -> ByteString -> Response
+ Happstack.Server.Types: rqBody :: Request -> MVar RqBody
+ Happstack.Server.Types: rqCookies :: Request -> [(String, Cookie)]
+ Happstack.Server.Types: rqHeaders :: Request -> Headers
+ Happstack.Server.Types: rqInputsBody :: Request -> MVar [(String, Input)]
+ Happstack.Server.Types: rqInputsQuery :: Request -> [(String, Input)]
+ Happstack.Server.Types: rqMethod :: Request -> Method
+ Happstack.Server.Types: rqPaths :: Request -> [String]
+ Happstack.Server.Types: rqPeer :: Request -> Host
+ Happstack.Server.Types: rqQuery :: Request -> String
+ Happstack.Server.Types: rqURL :: Request -> String
+ Happstack.Server.Types: rqUri :: Request -> String
+ Happstack.Server.Types: rqVersion :: Request -> HttpVersion
+ Happstack.Server.Types: rsBody :: Response -> ByteString
+ Happstack.Server.Types: rsCode :: Response -> Int
+ Happstack.Server.Types: rsFlags :: Response -> RsFlags
+ Happstack.Server.Types: rsHeaders :: Response -> Headers
+ Happstack.Server.Types: rsValidator :: Response -> Maybe (Response -> IO Response)
+ Happstack.Server.Types: rsfLength :: RsFlags -> Length
+ Happstack.Server.Types: setHeader :: HasHeaders r => String -> String -> r -> r
+ Happstack.Server.Types: setHeaderBS :: HasHeaders r => ByteString -> ByteString -> r -> r
+ Happstack.Server.Types: setHeaderUnsafe :: HasHeaders r => ByteString -> HeaderPair -> r -> r
+ Happstack.Server.Types: setRsCode :: Monad m => Int -> Response -> m Response
+ Happstack.Server.Types: sfCount :: Response -> Integer
+ Happstack.Server.Types: sfFilePath :: Response -> FilePath
+ Happstack.Server.Types: sfOffset :: Response -> Integer
+ Happstack.Server.Types: takeRequestBody :: MonadIO m => Request -> m (Maybe RqBody)
+ Happstack.Server.Types: timeout :: Conf -> Int
+ Happstack.Server.Types: type Headers = Map ByteString HeaderPair
+ Happstack.Server.Types: type Host = (String, Int)
+ Happstack.Server.Types: unBody :: RqBody -> ByteString
+ Happstack.Server.Types: validator :: Conf -> Maybe (Response -> IO Response)
+ Happstack.Server.Validation: lazyProcValidator :: FilePath -> [String] -> Maybe FilePath -> Maybe [(String, String)] -> (Maybe ByteString -> Bool) -> Response -> IO Response
+ Happstack.Server.Validation: noopValidator :: Response -> IO Response
+ Happstack.Server.Validation: runValidator :: (Response -> IO Response) -> Response -> IO Response
+ Happstack.Server.Validation: setValidator :: (Response -> IO Response) -> Response -> Response
+ Happstack.Server.Validation: setValidatorSP :: (Monad m, ToMessage r) => (Response -> IO Response) -> m r -> m Response
+ Happstack.Server.Validation: validateConf :: Conf
+ Happstack.Server.Validation: wdgHTMLValidator :: (MonadIO m, ToMessage r) => r -> m Response
+ Happstack.Server.XSLT: doXslt :: MonadIO m => XSLTCmd -> XSLPath -> Response -> m Response
+ Happstack.Server.XSLT: xslt :: (MonadIO m, MonadPlus m, ToMessage r) => XSLTCmd -> XSLPath -> m r -> m Response
Files
- happstack-server.cabal +56/−26
- src/Happstack/Server.hs +89/−13
- src/Happstack/Server/Auth.hs +45/−0
- src/Happstack/Server/Client.hs +26/−0
- src/Happstack/Server/Compression.hs +7/−0
- src/Happstack/Server/Cookie.hs +40/−129
- src/Happstack/Server/Error.hs +57/−0
- src/Happstack/Server/FileServe.hs +19/−0
- src/Happstack/Server/FileServe/BuildingBlocks.hs +671/−0
- src/Happstack/Server/HTTP/Client.hs +0/−53
- src/Happstack/Server/HTTP/Clock.hs +0/−30
- src/Happstack/Server/HTTP/FileServe.hs +0/−354
- src/Happstack/Server/HTTP/Handler.hs +0/−365
- src/Happstack/Server/HTTP/LazyLiner.hs +0/−41
- src/Happstack/Server/HTTP/Listen.hs +0/−94
- src/Happstack/Server/HTTP/LowLevel.hs +0/−28
- src/Happstack/Server/HTTP/Multipart.hs +0/−215
- src/Happstack/Server/HTTP/RFC822Headers.hs +0/−266
- src/Happstack/Server/HTTP/Socket.hs +0/−52
- src/Happstack/Server/HTTP/SocketTH.hs +0/−15
- src/Happstack/Server/HTTP/Types.hs +0/−260
- src/Happstack/Server/Internal/Clock.hs +59/−0
- src/Happstack/Server/Internal/Compression.hs +204/−0
- src/Happstack/Server/Internal/Cookie.hs +187/−0
- src/Happstack/Server/Internal/Handler.hs +388/−0
- src/Happstack/Server/Internal/LazyLiner.hs +41/−0
- src/Happstack/Server/Internal/Listen.hs +103/−0
- src/Happstack/Server/Internal/LowLevel.hs +28/−0
- src/Happstack/Server/Internal/MessageWrap.hs +127/−0
- src/Happstack/Server/Internal/Monads.hs +503/−0
- src/Happstack/Server/Internal/Multipart.hs +257/−0
- src/Happstack/Server/Internal/RFC822Headers.hs +266/−0
- src/Happstack/Server/Internal/Socket.hs +51/−0
- src/Happstack/Server/Internal/SocketTH.hs +15/−0
- src/Happstack/Server/Internal/TimeoutManager.hs +59/−0
- src/Happstack/Server/Internal/TimeoutSocket.hs +66/−0
- src/Happstack/Server/Internal/Types.hs +393/−0
- src/Happstack/Server/MessageWrap.hs +0/−97
- src/Happstack/Server/MinHaXML.hs +0/−214
- src/Happstack/Server/Monads.hs +86/−0
- src/Happstack/Server/Parts.hs +0/−171
- src/Happstack/Server/Proxy.hs +92/−0
- src/Happstack/Server/Response.hs +289/−0
- src/Happstack/Server/Routing.hs +206/−0
- src/Happstack/Server/RqData.hs +568/−0
- src/Happstack/Server/SURI.hs +1/−1
- src/Happstack/Server/SimpleHTTP.hs +160/−1485
- src/Happstack/Server/StdConfig.hs +0/−22
- src/Happstack/Server/Types.hs +18/−0
- src/Happstack/Server/Validation.hs +134/−0
- src/Happstack/Server/XSLT.hs +36/−6
- tests/Happstack/Server/Tests.hs +112/−5
happstack-server.cabal view
@@ -1,7 +1,7 @@ Name: happstack-server-Version: 0.5.0.4+Version: 6.0.0 Synopsis: Web related tools and services.-Description: Web framework+Description: Happstack Server provides an HTTP server and a rich set of functions for routing requests, handling query parameters, generating responses, working with cookies, serving files, and more. For in-depth documentation see the Happstack Crash Course <http://happstack.com/docs/crashcourse/index.html> License: BSD3 License-file: COPYING Author: Happstack team, HAppS LLC@@ -14,11 +14,15 @@ source-repository head type: darcs subdir: happstack-server- location: http://patch-tag.com/r/mae/happstack/pullrepo+ location: http://patch-tag.com/r/mae/happstack Flag base4 Description: Choose the even newer, even smaller, split-up base package. +Flag network_2_2_3+ Description: Choose newer network library with merged in network-bytestring. We can not really use this until hslogger is updated to allow it. (Or we ditch hslogger)+ Default: False+ Flag tests Description: Build the testsuite, and include the tests in the library Default: False@@ -27,18 +31,33 @@ Exposed-modules: Happstack.Server+ Happstack.Server.Auth Happstack.Server.Cookie- Happstack.Server.HTTP.Client- Happstack.Server.HTTP.Types- Happstack.Server.HTTP.LowLevel- Happstack.Server.HTTP.FileServe- Happstack.Server.SimpleHTTP- Happstack.Server.MessageWrap- Happstack.Server.MinHaXML+ Happstack.Server.Compression+ Happstack.Server.Client+ Happstack.Server.Error+ Happstack.Server.FileServe+ Happstack.Server.FileServe.BuildingBlocks+ Happstack.Server.Internal.Compression+ Happstack.Server.Internal.Cookie+ Happstack.Server.Internal.Types+ Happstack.Server.Internal.LowLevel+ Happstack.Server.Internal.MessageWrap+-- Happstack.Server.Internal.NoPush+ Happstack.Server.Internal.TimeoutManager+ Happstack.Server.Internal.TimeoutSocket+ Happstack.Server.Internal.Monads+ Happstack.Server.Monads+ Happstack.Server.Proxy+ Happstack.Server.Response+ Happstack.Server.Routing+ Happstack.Server.RqData Happstack.Server.SURI+ Happstack.Server.SimpleHTTP+ Happstack.Server.Types+ Happstack.Server.Validation Happstack.Server.XSLT- Happstack.Server.StdConfig- Happstack.Server.Parts+ if flag(tests) Exposed-modules: Happstack.Server.Tests@@ -46,42 +65,49 @@ Happstack.Server.HTTPClient.HTTP Happstack.Server.HTTPClient.Stream Happstack.Server.HTTPClient.TCP- Happstack.Server.HTTP.Clock- Happstack.Server.HTTP.Handler- Happstack.Server.HTTP.LazyLiner- Happstack.Server.HTTP.Listen- Happstack.Server.HTTP.Multipart- Happstack.Server.HTTP.RFC822Headers- Happstack.Server.HTTP.Socket- Happstack.Server.HTTP.SocketTH+ Happstack.Server.Internal.Clock+ Happstack.Server.Internal.Handler+ Happstack.Server.Internal.LazyLiner+ Happstack.Server.Internal.Listen+ Happstack.Server.Internal.Multipart+ Happstack.Server.Internal.RFC822Headers+ Happstack.Server.Internal.Socket+ Happstack.Server.Internal.SocketTH Happstack.Server.SURI.ParseURI Paths_happstack_server Build-Depends: base,+ blaze-html >= 0.3 && < 0.5, bytestring, containers, directory, extensible-exceptions, filepath,- HaXml >= 1.13 && < 1.14,+-- HaXml >= 1.13 && < 1.14, hslogger >= 1.0.2,- happstack-data >= 0.5 && < 0.5.1,- happstack-util >= 0.5 && < 0.5.1,+ happstack-data >= 6.0 && < 6.1,+ happstack-util >= 6.0 && < 6.1, html, MaybeT, mtl >= 1.1 && < 2.1,- network, old-locale, old-time, parsec < 4, process,- sendfile >= 0.6.1 && < 0.8,+ sendfile >= 0.7.1 && < 0.8, template-haskell,+ text >= 0.10 && < 0.12, time, utf8-string >= 0.3.4 && < 0.4, xhtml, zlib+ if flag(network_2_2_3)+ Build-Depends: network >= 2.2.3+ else+ Build-Depends: network < 2.2.3, network-bytestring ++ hs-source-dirs: src if flag(tests) hs-source-dirs: tests@@ -100,13 +126,17 @@ FlexibleInstances, UndecidableInstances, ScopedTypeVariables, TypeSynonymInstances, PatternGuards CPP, ForeignFunctionInterface- ghc-options: -Wall+ if impl(ghc >= 6.12)+ ghc-options: -Wall -fno-warn-unused-do-bind+ else+ ghc-options: -Wall GHC-Prof-Options: -auto-all Executable happstack-server-tests Main-Is: Test.hs GHC-Options: -threaded hs-source-dirs: tests, src+-- Other-modules: Happstack.Server.Internal.NoPush if flag(tests) Buildable: True Build-depends: HUnit, parsec < 4
src/Happstack/Server.hs view
@@ -1,15 +1,91 @@+{- |+Module : Happstack.Server+Copyright : (c) Happstack.com 2010; (c) HAppS Inc 2007+License : BSD3++Maintainer : Happstack team <happs@googlegroups.com>+Stability : provisional+Portability : GHC-only, Windows, Linux, FreeBSD, OS X++Happstack.Server provides a self-contained HTTP server and a rich collection of types and functions for routing Requests, generating Responses, working with query parameters, form data, and cookies, serving files and more.++A very simple, \"Hello World!\" web app looks like:+ +> import Happstack.Server+> main = simpleHTTP nullConf $ ok "Hello World!"++By default the server will listen on port 8000. Run the app and point your browser at: <http://localhost:8000/>++At the core of the Happstack server we have the 'simpleHTTP' function which starts the HTTP server:++> simpleHTTP :: ToMessage a => Conf -> ServerPart a -> IO ()++and we have the user supplied 'ServerPart' (also known as,+'ServerPartT' 'IO'), which generates a 'Response' for each incoming+'Request'.++A trivial HTTP app server might just take a user supplied function like:++> myApp :: Request -> IO Response++For each incoming 'Request' the server would fork a new thread, run+@myApp@ to generate a 'Response', and then send the 'Response' back to+the client. But, that would be a pretty barren wasteland to work in.++The model for 'ServerPart' is essential the same, except we use the+much richer 'ServerPart' monad instead of the 'IO' monad. ++For in-depth documentation and runnable examples I highly recommend The Happstack Crash Course <http://happstack.com/docs/crashcourse/index.html>.++-} module Happstack.Server -(module Happstack.Server.XSLT-,module Happstack.Server.SimpleHTTP-,module Happstack.Server.HTTP.Client-,module Happstack.Server.MessageWrap-,module Happstack.Server.HTTP.FileServe-,module Happstack.Server.StdConfig)-where-import Happstack.Server.HTTP.Client-import Happstack.Server.StdConfig-import Happstack.Server.XSLT-import Happstack.Server.SimpleHTTP-import Happstack.Server.MessageWrap-import Happstack.Server.HTTP.FileServe+ ( -- * HTTP Server+ module Happstack.Server.SimpleHTTP+ -- * Request Routing+ , module Happstack.Server.Routing+ -- * Creating Responses+ , module Happstack.Server.Response+ -- * Looking up values in Query String, Request Body, and Cookies+ , module Happstack.Server.RqData+ -- * Create and Set Cookies (see also "Happstack.Server.RqData")+ , module Happstack.Server.Cookie+ -- * File Serving+ , module Happstack.Server.FileServe+ -- * HTTP Realm Authentication+ , module Happstack.Server.Auth+ -- * Error Handling+ , module Happstack.Server.Error+ -- * Web-related Monads+ , module Happstack.Server.Monads+ -- * Proxying+ , module Happstack.Server.Proxy+ -- * Output Validation+ , module Happstack.Server.Validation+ -- * HTTP Types+ , module Happstack.Server.Types+ -- * Other+ , module Happstack.Server.Client+-- , module Happstack.Server.Internal.Monads+ )+ where +import Happstack.Server.Client+import Happstack.Server.SimpleHTTP (simpleHTTP+ , simpleHTTP'+ , simpleHTTP''+ , simpleHTTPWithSocket+ , simpleHTTPWithSocket'+ , bindPort+ , parseConfig)+import Happstack.Server.FileServe+import Happstack.Server.Monads+import Happstack.Server.Auth+import Happstack.Server.Cookie+import Happstack.Server.Error+import Happstack.Server.Response+import Happstack.Server.Routing+import Happstack.Server.Proxy+import Happstack.Server.RqData+import Happstack.Server.Validation+import Happstack.Server.Types+-- import Happstack.Server.Internal.Monads
+ src/Happstack/Server/Auth.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE FlexibleContexts #-}+-- | Support for basic access authentication <http://en.wikipedia.org/wiki/Basic_access_authentication>+module Happstack.Server.Auth where++import Control.Monad (MonadPlus(mzero, mplus))+import qualified Data.ByteString.Char8 as B+import qualified Data.Map as M+import qualified Happstack.Crypto.Base64 as Base64+import Happstack.Server.Monads (FilterMonad, ServerMonad, WebMonad, escape, getHeaderM, setHeaderM)+import Happstack.Server.Types (Response)+import Happstack.Server.Response (unauthorized, toResponse)++-- | A simple HTTP basic authentication guard.+--+-- If authentication fails, this part will call 'mzero'.+-- +-- example:+--+-- > main = simpleHTTP nullConf $ +-- > msum [ basicAuth "127.0.0.1" (fromList [("happstack","rocks")]) $ ok "You are in the secret club"+-- > , ok "You are not in the secret club." +-- > ]+-- +basicAuth :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadPlus m) =>+ String -- ^ the realm name+ -> M.Map String String -- ^ the username password map+ -> m a -- ^ the part to guard+ -> m a+basicAuth realmName authMap xs = basicAuthImpl `mplus` xs+ where+ basicAuthImpl = do+ aHeader <- getHeaderM "authorization"+ case aHeader of+ Nothing -> err+ Just x -> case parseHeader x of+ (name, ':':password) | validLogin name password -> mzero+ | otherwise -> err+ _ -> err+ validLogin name password = M.lookup name authMap == Just password+ parseHeader = break (':'==) . Base64.decode . B.unpack . B.drop 6+ headerName = "WWW-Authenticate"+ headerValue = "Basic realm=\"" ++ realmName ++ "\""+ err = escape $ do+ setHeaderM headerName headerValue+ unauthorized $ toResponse "Not authorized"
+ src/Happstack/Server/Client.hs view
@@ -0,0 +1,26 @@+-- | a very simple interface for acting as an HTTP client. This is mostly used for things like "Happstack.Server.Proxy". You are more likely to want a library like http-enumerator <http://hackage.haskell.org/package/http-enumerator>.+module Happstack.Server.Client where++import Happstack.Server.Internal.Handler (parseResponse, putRequest)+import Happstack.Server.Internal.Types (Response, Request, getHeader)+import Data.Maybe (fromJust)+import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Lazy.Char8 as L +import Network (PortID(PortNumber), connectTo, withSocketsDo)+import System.IO (BufferMode(NoBuffering), hFlush, hSetBuffering)++-- | Sends the serialized request to the host defined in the request+-- and attempts to parse response upon arrival.+getResponse :: Request -> IO (Either String Response)+getResponse rq = withSocketsDo $ do+ let (hostName,p) = span (/=':') $ fromJust $ fmap B.unpack $ getHeader "host" rq + portInt = if null p then 80 else read $ tail p+ portId = PortNumber $ toEnum $ portInt+ h <- connectTo hostName portId + hSetBuffering h NoBuffering++ putRequest h rq+ hFlush h++ inputStr <- L.hGetContents h+ return $ parseResponse inputStr
+ src/Happstack/Server/Compression.hs view
@@ -0,0 +1,7 @@+{-# LANGUAGE NoMonomorphismRestriction, FlexibleContexts #-}+-- | Filter for compressing the 'Response' body.+module Happstack.Server.Compression+ ( compressedResponseFilter+ ) where++import Happstack.Server.Internal.Compression (compressedResponseFilter)
src/Happstack/Server/Cookie.hs view
@@ -1,140 +1,51 @@-{-# LANGUAGE DeriveDataTypeable #-}---- http://tools.ietf.org/html/rfc2109+{-# LANGUAGE DeriveDataTypeable, FlexibleContexts #-}+-- | Functions for creating, adding, and expiring cookies. To lookup cookie values see "Happstack.Server.RqData". module Happstack.Server.Cookie ( Cookie(..)+ , CookieLife(..) , mkCookie- , mkCookieHeader- , getCookies- , getCookie- , getCookies'- , getCookie'- , parseCookies- , cookiesParser+ , addCookie+ , addCookies+ , expireCookie ) where -import qualified Data.ByteString.Char8 as C-import Data.Char-import Data.List-import Data.Generics-import Happstack.Util.Common (Seconds)-import Text.ParserCombinators.Parsec hiding (token)--data Cookie = Cookie- { cookieVersion :: String- , cookiePath :: String- , cookieDomain :: String- , cookieName :: String- , cookieValue :: String- , secure :: Bool- } deriving(Show,Eq,Read,Typeable,Data)---- | Creates a cookie with a default version of 1 and path of "/"-mkCookie :: String -> String -> Cookie-mkCookie key val = Cookie "1" "/" "" key val False+import Control.Monad.Trans (MonadIO(..))+import Data.Data (Data)+import Data.Typeable (Typeable)+import Data.Time.Clock (UTCTime)+import Happstack.Server.Internal.Monads (FilterMonad, composeFilter)+import Happstack.Server.Internal.Cookie (Cookie(..), CookieLife(..), calcLife, mkCookie, mkCookieHeader)+import Happstack.Server.Types (Response, addHeader)+import Happstack.Util.Common (Seconds) --- | Set a Cookie in the Result.--- The values are escaped as per RFC 2109, but some browsers may--- have buggy support for cookies containing e.g. @\'\"\'@ or @\' \'@.+-- | Add the 'Cookie' to 'Response'. ----- Also, it seems that chrome, safari, and other webkit browsers do--- not like cookies which have double quotes around the domain and--- reject/ignore the cookie. So, we no longer quote the domain.-mkCookieHeader :: Seconds -> Cookie -> String-mkCookieHeader sec cookie =- let l = [("Domain=", cookieDomain cookie)- ,("Max-Age=",if sec < 0 then "" else show sec)- ,("Path=", cookiePath cookie)- ,("Version=", s cookieVersion)]- s f | f cookie == "" = ""- s f = '\"' : concatMap e (f cookie) ++ "\""- e c | fctl c || c == '"' = ['\\',c]- | otherwise = [c]- in concat $ intersperse ";" ((cookieName cookie++"="++s cookieValue):[ (k++v) | (k,v) <- l, "" /= v ] ++ if secure cookie then ["Secure"] else [])--fctl :: Char -> Bool-fctl ch = ch == chr 127 || ch <= chr 31---- | Not an supported api. Takes a cookie header and returns--- either a String error message or an array of parsed cookies-parseCookies :: String -> Either String [Cookie]-parseCookies str = either (Left . show) Right $ parse cookiesParser str str---- | not a supported api. A parser for RFC 2109 cookies-cookiesParser :: GenParser Char st [Cookie]-cookiesParser = cookies- where -- Parsers based on RFC 2109- cookies = do- ws- ver<-option "" $ try (cookie_version >>= (\x -> cookieSep >> return x))- cookieList<-(cookie_value ver) `sepBy1` try cookieSep- ws- eof- return cookieList- cookie_value ver = do- name<-name_parser- cookieEq- val<-value- path<-option "" $ try (cookieSep >> cookie_path)- domain<-option "" $ try (cookieSep >> cookie_domain)- return $ Cookie ver path domain (low name) val False- cookie_version = cookie_special "$Version"- cookie_path = cookie_special "$Path"- cookie_domain = cookie_special "$Domain"- cookie_special s = do- string s- cookieEq- value- cookieSep = ws >> oneOf ",;" >> ws- cookieEq = ws >> char '=' >> ws- ws = spaces- value = word- word = try (quoted_string) <|> incomp_token-- -- Parsers based on RFC 2068- quoted_string = do- char '"'- r <-many (oneOf qdtext)- char '"'- return r-- -- Custom parsers, incompatible with RFC 2068, but more forgiving ;)- incomp_token = many1 $ oneOf ((chars \\ ctl) \\ " \t\";")- name_parser = many1 $ oneOf ((chars \\ ctl) \\ "= ;,")-- -- Primitives from RFC 2068- ctl = map chr (127:[0..31])- chars = map chr [0..127]- octet = map chr [0..255]- text = octet \\ ctl- qdtext = text \\ "\""---- | Get all cookies from the HTTP request. The cookies are ordered per RFC from--- the most specific to the least specific. Multiple cookies with the same--- name are allowed to exist.-getCookies :: Monad m => C.ByteString -> m [Cookie]-getCookies h = getCookies' h >>= either (fail. ("Cookie parsing failed!"++)) return---- | Get the most specific cookie with the given name. Fails if there is no such--- cookie or if the browser did not escape cookies in a proper fashion.--- Browser support for escaping cookies properly is very diverse.-getCookie :: Monad m => String -> C.ByteString -> m Cookie-getCookie s h = getCookie' s h >>= either (const $ fail ("getCookie: " ++ show s)) return--getCookies' :: Monad m => C.ByteString -> m (Either String [Cookie])-getCookies' header | C.null header = return $ Right []- | otherwise = return $ parseCookies (C.unpack header)+-- example+-- +-- > main = simpleHTTP nullConf $+-- > do addCookie Session (mkCookie "name" "value")+-- > ok $ "You now have a session cookie."+--+-- see also: 'addCookies'+addCookie :: (MonadIO m, FilterMonad Response m) => CookieLife -> Cookie -> m ()+addCookie life cookie =+ do l <- liftIO $ calcLife life+ (addHeaderM "Set-Cookie") $ mkCookieHeader l cookie+ where+ addHeaderM a v = composeFilter $ \res-> addHeader a v res -getCookie' :: Monad m => String -> C.ByteString -> m (Either String Cookie)-getCookie' s h = do- cs <- getCookies' h- return $ do -- Either- cooks <- cs- case filter (\x->(==) (low s) (cookieName x) ) cooks of- [] -> fail "No cookie found"- f -> return $ head f+-- | Add the list 'Cookie' to the 'Response'.+-- +-- see also: 'addCookie'+addCookies :: (MonadIO m, FilterMonad Response m) => [(CookieLife, Cookie)] -> m ()+addCookies = mapM_ (uncurry addCookie) -low :: String -> String-low = map toLower+-- | Expire the named cookie immediately and set the cookie value to @\"\"@+--+-- > main = simpleHTTP nullConf $+-- > do expireCookie "name"+-- > ok $ "The cookie has been expired." +expireCookie :: (MonadIO m, FilterMonad Response m) => String -> m () +expireCookie cookieName = addCookie Expired (mkCookie cookieName "")
+ src/Happstack/Server/Error.hs view
@@ -0,0 +1,57 @@+-- | Some useful functions if you want to wrap the 'ServerPartT' monad transformer around the 'ErrorT' monad transformer. e.g., @'ServerPartT' ('ErrorT' e m) a@. This allows you to use 'throwError' and 'catchError' inside your monad. +module Happstack.Server.Error where++import Control.Monad.Error (Error, ErrorT(runErrorT))+import Happstack.Server.Monads (ServerPartT)+import Happstack.Server.Internal.Monads (WebT, UnWebT, withRequest, mkWebT, runServerPartT, ununWebT)+import Happstack.Server.Response (ok, toResponse)+import Happstack.Server.Types (Request, Response)++--------------------------------------------------------------+-- Error Handling+--------------------------------------------------------------++-- | Flatten @'ServerPartT' ('ErrorT' e m) a@ into a @'ServerPartT' m+-- a@ so that it can be use with 'simpleHTTP'. Used with+-- 'mapServerPartT'', e.g.,+--+-- > simpleHTTP conf $ mapServerPartT' (spUnWrapErrorT simpleErrorHandler) $ myPart `catchError` errorPart+--+-- Note that in this example, @simpleErrorHandler@ will only be run if @errorPart@ throws an error. You can replace @simpleErrorHandler@ with your own custom error handler.+--+-- see also: 'simpleErrorHandler'+spUnwrapErrorT:: Monad m => (e -> ServerPartT m a)+ -> Request+ -> UnWebT (ErrorT e m) a+ -> UnWebT m a+spUnwrapErrorT handler rq = \x -> do+ err <- runErrorT x+ case err of+ Left e -> ununWebT $ runServerPartT (handler e) rq+ Right a -> return a++-- | A simple error handler which can be used with 'spUnwrapErrorT'.+-- +-- It returns the error message as a plain text message to the+-- browser. More sophisticated behaviour can be achieved by calling+-- your own custom error handler instead.+--+-- see also: 'spUnwrapErrorT'+simpleErrorHandler :: (Monad m) => String -> ServerPartT m Response+simpleErrorHandler err = ok $ toResponse $ ("An error occured: " ++ err)++-- | This 'ServerPart' modifier enables the use of 'throwError' and+-- 'catchError' inside the 'WebT' actions, by adding the 'ErrorT'+-- monad transformer to the stack.+--+-- You can wrap the complete second argument to 'simpleHTTP' in this+-- function.+--+-- DEPRECATED: use 'spUnwrapErrorT' instead.+errorHandlerSP :: (Monad m, Error e) => (Request -> e -> WebT m a) -> ServerPartT (ErrorT e m) a -> ServerPartT m a+errorHandlerSP handler sps = withRequest $ \req -> mkWebT $ do+ eer <- runErrorT $ ununWebT $ runServerPartT sps req+ case eer of+ Left err -> ununWebT (handler req err)+ Right res -> return res+{-# DEPRECATED errorHandlerSP "Use spUnwrapErrorT" #-}
+ src/Happstack/Server/FileServe.hs view
@@ -0,0 +1,19 @@+-- | functions for serving static files from the disk+module Happstack.Server.FileServe + ( -- * Serving Functions + Browsing(..)+ , serveDirectory+ , serveFile+ -- * Content-Type \/ Mime-Type+ , MimeMap+ , mimeTypes+ , asContentType+ , guessContentTypeM+ -- * Index Files+ , defaultIxFiles+ -- * Deprecated+ , fileServe+ )+ where++import Happstack.Server.FileServe.BuildingBlocks
+ src/Happstack/Server/FileServe/BuildingBlocks.hs view
@@ -0,0 +1,671 @@+{-# LANGUAGE DeriveDataTypeable, FlexibleContexts, ScopedTypeVariables, Rank2Types #-}+-- | Build your own file serving functions+--+-- If the functions in "Happstack.Server.FileServe" do not quite do+-- you want you can roll your own by reusing pieces from this module.+--+-- You will likely want to start by copying the source for a function+-- like, 'serveDirectory' and then modifying it to suit your needs.+--+module Happstack.Server.FileServe.BuildingBlocks+ (+ -- * High-Level+ -- ** Serving files from a directory+ fileServe,+ fileServe',+ fileServeLazy,+ fileServeStrict,+ Browsing(..),+ serveDirectory,+ -- ** Serving a single file+ serveFile,+ serveFileUsing,+ -- * Low-Level+ sendFileResponse, + lazyByteStringResponse,+ strictByteStringResponse,+ filePathSendFile,+ filePathLazy,+ filePathStrict,+ -- * Content-Type \/ Mime-Type+ MimeMap,+ mimeTypes,+ asContentType,+ guessContentType,+ guessContentTypeM,+ -- * Other+ blockDotFiles,+ defaultIxFiles,+ tryIndex,+ doIndex,+ doIndex',+ doIndexLazy,+ doIndexStrict,+ fileNotFound,+ isDot+ ) where++import Control.Exception.Extensible (IOException, SomeException, Exception(fromException), bracket, handleJust)+import Control.Monad (MonadPlus(mzero), msum)+import Control.Monad.Trans (MonadIO(liftIO))+import qualified Data.ByteString.Lazy.Char8 as L+import qualified Data.ByteString.Char8 as S+import Data.Data (Data, Typeable)+import Data.Maybe (fromMaybe, maybe)+import Data.Map (Map)+import qualified Data.Map as Map+import Happstack.Server.Monads (Happstack, ServerMonad(askRq), FilterMonad, WebMonad, require)+import Happstack.Server.Response (ToMessage(toResponse), ifModifiedSince, forbidden, ok, seeOther)+import Happstack.Server.Types (Length(ContentLength), Request(rqPaths, rqUri), Response(SendFile), RsFlags(rsfLength), nullRsFlags, result, resultBS, setHeader)+import System.Directory (doesDirectoryExist, doesFileExist, getDirectoryContents, getModificationTime)+import System.FilePath ((</>), addTrailingPathSeparator, joinPath, takeExtension)+import System.IO (IOMode(ReadMode), hFileSize, hClose, openBinaryFile)+import System.Locale (defaultTimeLocale, rfc822DateFormat)+import System.Log.Logger (Priority(DEBUG), logM)+import System.Time (CalendarTime, formatCalendarTime, toCalendarTime, toUTCTime)++import Text.Blaze ((!))+import qualified Text.Blaze.Html5 as H+import qualified Text.Blaze.Html5.Attributes as A++-- FIXME: why are these functions here ?++ioErrors :: SomeException -> Maybe IOException+ioErrors = fromException++errorwrapper :: (MonadIO m, MonadPlus m, FilterMonad Response m) => String -> String -> m Response+errorwrapper binarylocation loglocation+ = require getErrorLog $ \errorLog ->+ return $ toResponse errorLog+ where getErrorLog+ = handleJust ioErrors (const (return Nothing)) $+ do bintime <- getModificationTime binarylocation+ logtime <- getModificationTime loglocation+ if (logtime > bintime)+ then fmap Just $ readFile loglocation+ else return Nothing++-- * Mime-Type / Content-Type++-- |a 'Map' from file extensions to content-types+-- +-- example:+--+-- > myMimeMap :: MimeMap+-- > myMimeMap = Map.fromList [("gz","application/x-gzip"), ... ]+--+-- see also: 'mimeTypes'+type MimeMap = Map String String++-- | try to guess the content-type of a file based on its extension+--+-- see also: 'guessContentTypeM'+guessContentType :: MimeMap -> FilePath -> Maybe String+guessContentType mimeMap filepath =+ case getExt filepath of+ "" -> Nothing+ ext -> Map.lookup ext mimeMap++-- | try to guess the content-type of a file based on its extension+--+-- defaults to "application/octet-stream" if no match was found.+--+-- Useful as an argument to 'serveFile'+--+-- see also: 'guessContentType', 'serveFile'+guessContentTypeM :: (Monad m) => MimeMap -> (FilePath -> m String)+guessContentTypeM mimeMap filePath = return $ fromMaybe "application/octet-stream" $ guessContentType mimeMap filePath++-- | returns a specific content type, completely ignoring the 'FilePath' argument. +--+-- Use this with 'serveFile' if you want to explicitly specify the+-- content-type.+--+-- see also: 'guessContentTypeM', 'serveFile'+asContentType :: (Monad m) => + String -- ^ the content-type to return+ -> (FilePath -> m String)+asContentType = const . return++-- | a list of common index files. Specifically: @index.html@, @index.xml@, @index.gif@+-- +-- Typically used as an argument to 'serveDiretory'.+defaultIxFiles :: [FilePath]+defaultIxFiles= ["index.html","index.xml","index.gif"]++-- | return a simple "File not found 404 page."+fileNotFound :: (Monad m, FilterMonad Response m) => FilePath -> m Response+fileNotFound fp = return $ result 404 $ "File not found " ++ fp++-- | Similar to 'takeExtension' but does not include the extension separator char+getExt :: FilePath -> String+getExt fp = drop 1 $ takeExtension fp++-- | Prevents files of the form '.foo' or 'bar/.foo' from being served+blockDotFiles :: (Request -> IO Response) -> Request -> IO Response+blockDotFiles fn rq+ | isDot (joinPath (rqPaths rq)) = return $ result 403 "Dot files not allowed."+ | otherwise = fn rq++-- | Returns True if the given String either starts with a . or is of the form+-- "foo/.bar", e.g. the typical *nix convention for hidden files.+isDot :: String -> Bool+isDot = isD . reverse+ where+ isD ('.':'/':_) = True+ isD ['.'] = True+ --isD ('/':_) = False+ isD (_:cs) = isD cs+ isD [] = False++-- * Low-level functions for generating a Response++-- | Use sendFile to send the contents of a Handle+sendFileResponse :: String -- ^ content-type string+ -> FilePath -- ^ file path for content to send+ -> Maybe (CalendarTime, Request) -- ^ mod-time for the handle (MUST NOT be later than server's time of message origination), incoming request (used to check for if-modified-since header)+ -> Integer -- ^ offset into Handle+ -> Integer -- ^ number of bytes to send+ -> Response+sendFileResponse ct filePath mModTime _offset count =+ let res = ((setHeader "Content-Type" ct) $ + (SendFile 200 Map.empty (nullRsFlags { rsfLength = ContentLength }) Nothing filePath 0 count)+ )+ in case mModTime of+ Nothing -> res+ (Just (modTime, request)) -> ifModifiedSince modTime request res++-- | Send the contents of a Lazy ByteString+--+lazyByteStringResponse :: String -- ^ content-type string (e.g. @\"text/plain; charset=utf-8\"@)+ -> L.ByteString -- ^ lazy bytestring content to send+ -> Maybe (CalendarTime, Request) -- ^ mod-time for the bytestring, incoming request (used to check for if-modified-since header)+ -> Integer -- ^ offset into the bytestring+ -> Integer -- ^ number of bytes to send (offset + count must be less than or equal to the length of the bytestring)+ -> Response+lazyByteStringResponse ct body mModTime offset count =+ let res = ((setHeader "Content-Type" ct) $+ resultBS 200 (L.take (fromInteger count) $ (L.drop (fromInteger offset)) body)+ )+ in case mModTime of+ Nothing -> res+ (Just (modTime, request)) -> ifModifiedSince modTime request res++-- | Send the contents of a Lazy ByteString+strictByteStringResponse :: String -- ^ content-type string (e.g. @\"text/plain; charset=utf-8\"@)+ -> S.ByteString -- ^ lazy bytestring content to send+ -> Maybe (CalendarTime, Request) -- ^ mod-time for the bytestring, incoming request (used to check for if-modified-since header)+ -> Integer -- ^ offset into the bytestring+ -> Integer -- ^ number of bytes to send (offset + count must be less than or equal to the length of the bytestring)+ -> Response+strictByteStringResponse ct body mModTime offset count =+ let res = ((setHeader "Content-Type" ct) $+ resultBS 200 (L.fromChunks [S.take (fromInteger count) $ S.drop (fromInteger offset) body])+ )+ in case mModTime of+ Nothing -> res+ (Just (modTime, request)) -> ifModifiedSince modTime request res++-- | Send the specified file with the specified mime-type using sendFile()+--+-- NOTE: assumes file exists and is readable by the server. See 'serveFileUsing'.+--+-- WARNING: No security checks are performed.+filePathSendFile :: (ServerMonad m, MonadIO m)+ => String -- ^ content-type string+ -> FilePath -- ^ path to file on disk+ -> m Response+filePathSendFile contentType fp =+ do handle <- liftIO $ openBinaryFile fp ReadMode -- garbage collection should close this+ modtime <- liftIO $ getModificationTime fp+ count <- liftIO $ hFileSize handle+ rq <- askRq+ return $ sendFileResponse contentType fp (Just (toUTCTime modtime, rq)) 0 count++-- | Send the specified file with the specified mime-type using Lazy ByteStrings+--+-- NOTE: assumes file exists and is readable by the server. See 'serveFileUsing'.+--+-- WARNING: No security checks are performed.+filePathLazy :: (ServerMonad m, MonadIO m)+ => String -- ^ content-type string+ -> FilePath -- ^ path to file on disk+ -> m Response+filePathLazy contentType fp =+ do handle <- liftIO $ openBinaryFile fp ReadMode -- garbage collection should close this+ contents <- liftIO $ L.hGetContents handle+ modtime <- liftIO $ getModificationTime fp+ count <- liftIO $ hFileSize handle+ rq <- askRq+ return $ lazyByteStringResponse contentType contents (Just (toUTCTime modtime, rq)) 0 count++-- | Send the specified file with the specified mime-type using Lazy ByteStrings+--+-- NOTE: assumes file exists and is readable by the server. See 'serveFileUsing'.+--+-- WARNING: No security checks are performed.+filePathStrict :: (ServerMonad m, MonadIO m)+ => String -- ^ content-type string+ -> FilePath -- ^ path to file on disk+ -> m Response+filePathStrict contentType fp =+ do contents <- liftIO $ S.readFile fp+ modtime <- liftIO $ getModificationTime fp+ count <- liftIO $ bracket (openBinaryFile fp ReadMode) hClose hFileSize+ rq <- askRq+ return $ strictByteStringResponse contentType contents (Just (toUTCTime modtime, rq)) 0 count++-- * High-level functions for serving files+++-- ** Serve a single file++-- | Serve a single, specified file. The name of the file being served is specified explicity. It is not derived automatically from the 'Request' url.+-- +-- example 1:+-- +-- Serve using sendfile() and the specified content-type+--+-- > serveFileUsing filePathSendFile (asContentType "image/jpeg") "/srv/data/image.jpg"+--+--+-- example 2:+-- +-- Serve using a lazy ByteString and the guess the content-type from the extension+-- +-- > serveFileUsing filePathLazy (guessContentTypeM mimeTypes) "/srv/data/image.jpg"+-- +-- WARNING: No security checks are performed.+serveFileUsing :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) + => (String -> FilePath -> m Response) -- ^ typically 'filePathSendFile', 'filePathLazy', or 'filePathStrict'+ -> (FilePath -> m String) -- ^ function for determining content-type of file. Typically 'asContentType' or 'guessContentTypeM'+ -> FilePath -- ^ path to the file to serve+ -> m Response+serveFileUsing serveFn mimeFn fp = + do fe <- liftIO $ doesFileExist fp+ if fe+ then do mt <- mimeFn fp+ serveFn mt fp+ else mzero++-- | Serve a single, specified file. The name of the file being served is specified explicity. It is not derived automatically from the 'Request' url.+-- +-- example 1:+-- +-- Serve as a specific content-type:+--+-- > serveFile (asContentType "image/jpeg") "/srv/data/image.jpg"+--+--+-- example 2:+-- +-- Serve guessing the content-type from the extension:+-- +-- > serveFile (guessContentTypeM mimeTypes) "/srv/data/image.jpg"+--+-- If the specified path does not exist or is not a file, this function will return 'mzero'.+-- +-- WARNING: No security checks are performed.+--+-- NOTE: alias for 'serveFileUsing' 'filePathSendFile'+serveFile :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => + (FilePath -> m String) -- ^ function for determining content-type of file. Typically 'asContentType' or 'guessContentTypeM'+ -> FilePath -- ^ path to the file to serve+ -> m Response+serveFile = serveFileUsing filePathSendFile++-- ** Serve files from a directory++-- | Serve files from a directory and its subdirectories (parameterizable version)+-- +-- Parameterize this function to create functions like, 'fileServe', 'fileServeLazy', and 'fileServeStrict'+--+-- You supply:+--+-- 1. a low-level function which takes a content-type and 'FilePath' and generates a Response+--+-- 2. a function which determines the content-type from the 'FilePath'+--+-- 3. a list of all the default index files+--+-- NOTE: unlike fileServe, there are no index files by default. See 'defaultIxFiles'.+fileServe' :: ( WebMonad Response m+ , ServerMonad m+ , FilterMonad Response m+ , MonadIO m+ , MonadPlus m+ ) + => (String -> FilePath -> m Response) -- ^ function which takes a content-type and filepath and generates a response (typically 'filePathSendFile', 'filePathLazy', or 'filePathStrict')+ -> (FilePath -> m String) -- ^ function which returns the mime-type for FilePath+-- -> [FilePath] -- ^ index file names, in case the requested path is a directory+ -> (FilePath -> m Response)+ -> FilePath -- ^ file/directory to serve+ -> m Response+fileServe' serveFn mimeFn indexFn localpath = do+ rq <- askRq+ let safepath = filter (\x->not (null x) && x /= ".." && x /= ".") (rqPaths rq)+ fp = joinPath (localpath:safepath)+ fe <- liftIO $ doesFileExist fp+ de <- liftIO $ doesDirectoryExist fp+ let status | de = "DIR"+ | fe = "file"+ | True = "NOT FOUND"+ liftIO $ logM "Happstack.Server.FileServe" DEBUG ("fileServe: "++show fp++" \t"++status)+ if de+ then if last (rqUri rq) == '/'+-- then indexFn serveFn mimeFn (ixFiles++defaultIxFiles) fp+ then indexFn fp+ else do let path' = addTrailingPathSeparator (rqUri rq)+ seeOther path' (toResponse path')+ else if fe + then serveFileUsing serveFn mimeFn fp+ else mzero++-- | Serve files from a directory and its subdirectories using 'sendFile'.+-- +-- Usage:+--+-- > fileServe ["index.html"] "path/to/files/on/disk"+--+-- 'fileServe' does not support directory browsing. See 'serveDirectory'+--+-- DEPRECATED: use 'serveDirectory' instead.+--+-- Note:+-- +-- The list of index files @[\"index.html\"]@ is only used to determine what file to show if the user requests a directory. You *do not* need to explicitly list all the files you want to serve.+-- +fileServe :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) =>+ [FilePath] -- ^ index file names, in case the requested path is a directory+ -> FilePath -- ^ file/directory to serve+ -> m Response+fileServe ixFiles localPath = + fileServe' serveFn mimeFn indexFn localPath+ where+ serveFn = filePathSendFile+ mimeFn = guessContentTypeM mimeTypes+ indexFiles = (ixFiles ++ defaultIxFiles) + indexFn = doIndex' filePathSendFile mimeFn indexFiles+-- indexFn = browseIndex filePathSendFile mimeFn indexFiles+{-# DEPRECATED fileServe "use serveDirectory instead." #-}++-- | Serve files from a directory and its subdirectories (lazy ByteString version).+--+-- WARNING: May leak file handles. You should probably use 'fileServe' instead.+fileServeLazy :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) =>+ [FilePath] -- ^ index file names, in case the requested path is a directory+ -> FilePath -- ^ file/directory to serve+ -> m Response+fileServeLazy ixFiles localPath =+ fileServe' serveFn mimeFn indexFn localPath+ where+ serveFn = filePathLazy+ mimeFn = guessContentTypeM mimeTypes+ indexFiles = (ixFiles ++ defaultIxFiles) + indexFn = doIndex' filePathSendFile mimeFn indexFiles++-- | Serve files from a directory and its subdirectories (strict ByteString version). +--+-- WARNING: the entire file will be read into RAM before being served. You should probably use 'fileServe' instead.+fileServeStrict :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) =>+ [FilePath] -- ^ index file names, in case the next argument is a directory+ -> FilePath -- ^ file/directory to serve+ -> m Response+fileServeStrict ixFiles localPath =+ fileServe' serveFn mimeFn indexFn localPath+ where+ serveFn = filePathStrict+ mimeFn = guessContentTypeM mimeTypes+ indexFiles = (ixFiles ++ defaultIxFiles) + indexFn = doIndex' filePathSendFile mimeFn indexFiles++-- * Index++-- | attempt to serve index files +doIndex :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m)+ => [FilePath] -- ^ list of possible index files (e.g., @index.html@)+ -> MimeMap -- ^ see also 'mimeTypes'+ -> FilePath -- ^ directory on disk to search for index files+ -> m Response+doIndex ixFiles mimeMap localPath = doIndex' filePathSendFile (guessContentTypeM mimeMap) ixFiles localPath++doIndexLazy :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m)+ => [String]+ -> MimeMap+ -> FilePath+ -> m Response+doIndexLazy ixFiles mimeMap localPath = doIndex' filePathLazy (guessContentTypeM mimeMap) ixFiles localPath++doIndexStrict :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m)+ => [String]+ -> MimeMap+ -> FilePath+ -> m Response+doIndexStrict ixFiles mimeMap localPath = doIndex' filePathStrict (guessContentTypeM mimeMap) ixFiles localPath++doIndex' :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m)+ => (String -> FilePath -> m Response)+ -> (FilePath -> m String)+ -> [String]+ -> FilePath+ -> m Response+doIndex' serveFn mimeFn ixFiles fp =+ msum [ tryIndex serveFn mimeFn ixFiles fp+ , forbidden $ toResponse "Directory index forbidden"+ ]++-- | try to find an index file, calls mzero on failure+tryIndex :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m)+ => (String -> FilePath -> m Response) -- ^ usually 'filePathSendFile'+ -> (FilePath -> m String) -- ^ function to calculate mime type, usually 'guessContentTypeM'+ -> [String] -- ^ list of index files. See also 'defaultIxFiles'+ -> FilePath -- ^ directory to search in+ -> m Response+tryIndex _serveFn _mime [] _fp = mzero+tryIndex serveFn mimeFn (index:rest) fp =+ do let path = fp </> index+ fe <- liftIO $ doesFileExist path+ if fe + then serveFileUsing serveFn mimeFn path + else tryIndex serveFn mimeFn rest fp++-- * Directory Browsing++browseIndex :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m, ToMessage b) =>+ (FilePath -> [FilePath] -> m b)+ -> (String -> FilePath -> m Response)+ -> (FilePath -> m String)+ -> [String]+ -> FilePath+ -> m Response+browseIndex renderFn serveFn mimeFn ixFiles localPath =+ do c <- liftIO $ getDirectoryContents localPath+ listing <- renderFn localPath $ filter (/= ".") c+ ok $ toResponse $ listing++data EntryKind = File | Directory | UnknownKind deriving (Eq, Ord, Read, Show, Data, Typeable, Enum)++-- | a function to generate an HTML page showing the contents of a directory on the disk+--+-- see also: 'browseIndex', 'renderDirectoryContentsTable'+renderDirectoryContents :: (MonadIO m) =>+ FilePath -- ^ path to directory on disk+ -> [FilePath] -- ^ list of entries in that path+ -> m H.Html +renderDirectoryContents localPath fps =+ do fps' <- liftIO $ mapM (getMetaData localPath) fps+ return $ H.html $ do + H.head $ do+ H.title $ H.string "Directory Listing"+ H.meta ! A.httpEquiv (H.stringValue "Content-Type") ! A.content (H.stringValue "text/html;charset=utf-8")+ H.style $ H.string $ unlines [ "table { margin: 0 auto; width: 760px; border-collapse: collapse; font-family: 'sans-serif'; }"+ , "table, th, td { border: 1px solid #353948; }" + , "td.size { text-align: right; font-size: 0.7em; width: 50px }"+ , "td.date { text-align: right; font-size: 0.7em; width: 130px }"+ , "td { padding-right: 1em; padding-left: 1em; }"+ , "th.first { background-color: white; width: 24px }"+ , "td.first { padding-right: 0; padding-left: 0; text-align: center }"+ , "tr { background-color: white; }"+ , "tr.alt { background-color: #A3B5BA}"+ , "th { background-color: #3C4569; color: white; font-size: 1em; }"+ , "h1 { width: 760px; margin: 1em auto; font-size: 1em }"+ , "img { width: 20px }"+ , "a { text-decoration: none }"+ ]+{-+ H.style $ H.string $ unlines [ "table { border-collapse: collapse; font-family: 'sans-serif'; }"+ , "table, th, td { border: 1px solid #98BF21; }" + , "td.size { text-align: right; }"+ , "td.date { text-align: right; }"+ , "td { padding-right: 1em; padding-left: 1em; }"+ , "tr { background-color: white; }"+ , "tr.alt { background-color: #EAF2D3 }"+ , "th { background-color: #A7C942; color: white; font-size: 1.125em; }"+ ]+-}+ H.body $ do+ H.h1 $ H.string "Directory Listing"+ renderDirectoryContentsTable fps'++-- | a function to generate an HTML table showing the contents of a directory on the disk+--+-- This function generates most of the content of the+-- 'renderDirectoryContents' page. If you want to style the page+-- differently, or add google analytics code, etc, you can just create+-- a new page template to wrap around this HTML.+--+-- see also: 'getMetaData', 'renderDirectoryContents'+renderDirectoryContentsTable :: [(FilePath, Maybe CalendarTime, Maybe Integer, EntryKind)] -- ^ list of files+meta data, see 'getMetaData'+ -> H.Html+renderDirectoryContentsTable fps =+ H.table $ do H.thead $ do H.th $ H.string ""+ H.th $ H.string "Name"+ H.th $ H.string "Last modified"+ H.th $ H.string "Size"+ H.tbody $ mapM_ mkRow (zip fps $ cycle [False, True])+ where+ mkRow :: ((FilePath, Maybe CalendarTime, Maybe Integer, EntryKind), Bool) -> H.Html+ mkRow ((fp, modTime, count, kind), alt) = + (if alt then (! A.class_ (H.stringValue "alt")) else id) $+ H.tr $ do+ H.td (mkKind kind)+ H.td (H.a ! A.href (H.stringValue fp) $ H.string fp)+ H.td ! A.class_ (H.stringValue "date") $ (H.string $ maybe "-" (formatCalendarTime defaultTimeLocale "%d-%b-%Y %X %Z") modTime)+ (maybe id (\c -> (! A.title (H.stringValue (show c)))) count) (H.td ! A.class_ (H.stringValue "size") $ (H.string $ maybe "-" prettyShow count)) + mkKind :: EntryKind -> H.Html+ mkKind File = return ()+ mkKind Directory = H.string "➦"+ mkKind UnknownKind = return ()+ prettyShow x+ | x > 1024 = prettyShowK $ x `div` 1024+ | otherwise = addCommas "B" x+ prettyShowK x+ | x > 1024 = prettyShowM $ x `div` 1024+ | otherwise = addCommas "KB" x+ prettyShowM x+ | x > 1024 = prettyShowG $ x `div` 1024+ | otherwise = addCommas "MB" x+ prettyShowG x = addCommas "GB" x+ addCommas s = (++ (' ' : s)) . reverse . addCommas' . reverse . show+ addCommas' (a:b:c:d:e) = a : b : c : ',' : addCommas' (d : e)+ addCommas' x = x+++-- | look up the meta data associated with a file+getMetaData :: FilePath -- ^ path to directory on disk containing the entry+ -> FilePath -- ^ entry in that directory+ -> IO (FilePath, Maybe CalendarTime, Maybe Integer, EntryKind)+getMetaData localPath fp =+ do let localFp = localPath </> fp+ modTime <- (fmap Just . toCalendarTime =<< getModificationTime localFp) `catch` + (const $ return Nothing)+ count <- do de <- doesDirectoryExist localFp+ if de+ then do return Nothing+ else do bracket (openBinaryFile localFp ReadMode) hClose (fmap Just . hFileSize) + `catch` (\(e :: IOException) -> return Nothing)+ kind <- do fe <- doesFileExist localFp+ if fe+ then return File+ else do de <- doesDirectoryExist localFp+ if de + then return Directory+ else return UnknownKind+ return (fp, modTime, count, kind)++-- | see 'serveDirectory'+data Browsing + = EnableBrowsing | DisableBrowsing + deriving (Eq, Enum, Ord, Read, Show, Data, Typeable)++-- | Serve files and directories from a directory and its subdirectories using 'sendFile'.+-- +-- Usage:+--+-- > serveDirectory EnableBrowsing ["index.html"] "path/to/files/on/disk"+--+-- If the requested path does not match a file or directory on the+-- disk, then 'serveDirectory' calls 'mzero'.+--+-- If the requested path is a file then the file is served normally. +--+-- If the requested path is a directory, then the result depends on+-- what the first two arguments to the function are.+--+-- The first argument controls whether directory browsing is+-- enabled.+--+-- The second argument is a list of index files (such as+-- index.html).+--+-- When a directory is requested, 'serveDirectory' will first try to+-- find one of the index files (in the order they are listed). If that+-- fails, it will show a directory listing if 'EnableBrowsing' is set,+-- otherwise it will return @forbidden \"Directory index forbidden\"@.+-- +-- Here is an explicit list of all the possible outcomes when the+-- argument is a (valid) directory:+--+-- [@'DisableBrowsing', empty index file list@]+--+-- This will always return, forbidden \"Directory index forbidden\"+--+-- [@'DisableBrowsing', non-empty index file list@]+--+-- 1. If an index file is found it will be shown.+--+-- 2. Otherwise returns, forbidden \"Directory index forbidden\"+--+-- [@'EnableBrowsing', empty index file list@] +--+-- Always shows a directory index.+--+-- [@'EnableBrowsing', non-empty index file list@]+--+-- 1. If an index file is found it will be shown+--+-- 2. Otherwise shows a directory index+--+-- see also: 'defaultIxFiles', 'serveFile'+serveDirectory :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) =>+ Browsing -- ^ allow directory browsing+ -> [FilePath] -- ^ index file names, in case the requested path is a directory+ -> FilePath -- ^ file/directory to serve+ -> m Response+serveDirectory browsing ixFiles localPath = + fileServe' serveFn mimeFn indexFn localPath+ where+ serveFn = filePathSendFile+ mimeFn = guessContentTypeM mimeTypes+ indexFn fp =+ msum [ tryIndex filePathSendFile mimeFn ixFiles fp+ , if browsing == EnableBrowsing+ then browseIndex renderDirectoryContents filePathSendFile mimeFn ixFiles fp+ else forbidden $ toResponse "Directory index forbidden"+ ]++-- | Ready collection of common mime types.+-- Except for the first two entries, the mappings come from an Ubuntu 8.04 \/etc\/mime.types file.+mimeTypes :: MimeMap+mimeTypes = Map.fromList [("gz","application/x-gzip"),("cabal","application/x-cabal"),("%","application/x-trash"),("323","text/h323"),("3gp","video/3gpp"),("7z","application/x-7z-compressed"),("abw","application/x-abiword"),("ai","application/postscript"),("aif","audio/x-aiff"),("aifc","audio/x-aiff"),("aiff","audio/x-aiff"),("alc","chemical/x-alchemy"),("art","image/x-jg"),("asc","text/plain"),("asf","video/x-ms-asf"),("asn","chemical/x-ncbi-asn1"),("aso","chemical/x-ncbi-asn1-binary"),("asx","video/x-ms-asf"),("atom","application/atom"),("atomcat","application/atomcat+xml"),("atomsrv","application/atomserv+xml"),("au","audio/basic"),("avi","video/x-msvideo"),("b","chemical/x-molconn-Z"),("bak","application/x-trash"),("bat","application/x-msdos-program"),("bcpio","application/x-bcpio"),("bib","text/x-bibtex"),("bin","application/octet-stream"),("bmp","image/x-ms-bmp"),("boo","text/x-boo"),("book","application/x-maker"),("bsd","chemical/x-crossfire"),("c","text/x-csrc"),("c++","text/x-c++src"),("c3d","chemical/x-chem3d"),("cab","application/x-cab"),("cac","chemical/x-cache"),("cache","chemical/x-cache"),("cap","application/cap"),("cascii","chemical/x-cactvs-binary"),("cat","application/vnd.ms-pki.seccat"),("cbin","chemical/x-cactvs-binary"),("cbr","application/x-cbr"),("cbz","application/x-cbz"),("cc","text/x-c++src"),("cdf","application/x-cdf"),("cdr","image/x-coreldraw"),("cdt","image/x-coreldrawtemplate"),("cdx","chemical/x-cdx"),("cdy","application/vnd.cinderella"),("cef","chemical/x-cxf"),("cer","chemical/x-cerius"),("chm","chemical/x-chemdraw"),("chrt","application/x-kchart"),("cif","chemical/x-cif"),("class","application/java-vm"),("cls","text/x-tex"),("cmdf","chemical/x-cmdf"),("cml","chemical/x-cml"),("cod","application/vnd.rim.cod"),("com","application/x-msdos-program"),("cpa","chemical/x-compass"),("cpio","application/x-cpio"),("cpp","text/x-c++src"),("cpt","application/mac-compactpro"),("crl","application/x-pkcs7-crl"),("crt","application/x-x509-ca-cert"),("csf","chemical/x-cache-csf"),("csh","application/x-csh"),("csm","chemical/x-csml"),("csml","chemical/x-csml"),("css","text/css"),("csv","text/csv"),("ctab","chemical/x-cactvs-binary"),("ctx","chemical/x-ctx"),("cu","application/cu-seeme"),("cub","chemical/x-gaussian-cube"),("cxf","chemical/x-cxf"),("cxx","text/x-c++src"),("d","text/x-dsrc"),("dat","chemical/x-mopac-input"),("dcr","application/x-director"),("deb","application/x-debian-package"),("dif","video/dv"),("diff","text/x-diff"),("dir","application/x-director"),("djv","image/vnd.djvu"),("djvu","image/vnd.djvu"),("dl","video/dl"),("dll","application/x-msdos-program"),("dmg","application/x-apple-diskimage"),("dms","application/x-dms"),("doc","application/msword"),("dot","application/msword"),("dv","video/dv"),("dvi","application/x-dvi"),("dx","chemical/x-jcamp-dx"),("dxr","application/x-director"),("emb","chemical/x-embl-dl-nucleotide"),("embl","chemical/x-embl-dl-nucleotide"),("eml","message/rfc822"),("ent","chemical/x-ncbi-asn1-ascii"),("eps","application/postscript"),("etx","text/x-setext"),("exe","application/x-msdos-program"),("ez","application/andrew-inset"),("fb","application/x-maker"),("fbdoc","application/x-maker"),("fch","chemical/x-gaussian-checkpoint"),("fchk","chemical/x-gaussian-checkpoint"),("fig","application/x-xfig"),("flac","application/x-flac"),("fli","video/fli"),("fm","application/x-maker"),("frame","application/x-maker"),("frm","application/x-maker"),("gal","chemical/x-gaussian-log"),("gam","chemical/x-gamess-input"),("gamin","chemical/x-gamess-input"),("gau","chemical/x-gaussian-input"),("gcd","text/x-pcs-gcd"),("gcf","application/x-graphing-calculator"),("gcg","chemical/x-gcg8-sequence"),("gen","chemical/x-genbank"),("gf","application/x-tex-gf"),("gif","image/gif"),("gjc","chemical/x-gaussian-input"),("gjf","chemical/x-gaussian-input"),("gl","video/gl"),("gnumeric","application/x-gnumeric"),("gpt","chemical/x-mopac-graph"),("gsf","application/x-font"),("gsm","audio/x-gsm"),("gtar","application/x-gtar"),("h","text/x-chdr"),("h++","text/x-c++hdr"),("hdf","application/x-hdf"),("hh","text/x-c++hdr"),("hin","chemical/x-hin"),("hpp","text/x-c++hdr"),("hqx","application/mac-binhex40"),("hs","text/x-haskell"),("hta","application/hta"),("htc","text/x-component"),("htm","text/html"),("html","text/html"),("hxx","text/x-c++hdr"),("ica","application/x-ica"),("ice","x-conference/x-cooltalk"),("ico","image/x-icon"),("ics","text/calendar"),("icz","text/calendar"),("ief","image/ief"),("iges","model/iges"),("igs","model/iges"),("iii","application/x-iphone"),("inp","chemical/x-gamess-input"),("ins","application/x-internet-signup"),("iso","application/x-iso9660-image"),("isp","application/x-internet-signup"),("ist","chemical/x-isostar"),("istr","chemical/x-isostar"),("jad","text/vnd.sun.j2me.app-descriptor"),("jar","application/java-archive"),("java","text/x-java"),("jdx","chemical/x-jcamp-dx"),("jmz","application/x-jmol"),("jng","image/x-jng"),("jnlp","application/x-java-jnlp-file"),("jpe","image/jpeg"),("jpeg","image/jpeg"),("jpg","image/jpeg"),("js","application/x-javascript"),("kar","audio/midi"),("key","application/pgp-keys"),("kil","application/x-killustrator"),("kin","chemical/x-kinemage"),("kml","application/vnd.google-earth.kml+xml"),("kmz","application/vnd.google-earth.kmz"),("kpr","application/x-kpresenter"),("kpt","application/x-kpresenter"),("ksp","application/x-kspread"),("kwd","application/x-kword"),("kwt","application/x-kword"),("latex","application/x-latex"),("lha","application/x-lha"),("lhs","text/x-literate-haskell"),("lsf","video/x-la-asf"),("lsx","video/x-la-asf"),("ltx","text/x-tex"),("lyx","application/x-lyx"),("lzh","application/x-lzh"),("lzx","application/x-lzx"),("m3u","audio/mpegurl"),("m4a","audio/mpeg"),("maker","application/x-maker"),("man","application/x-troff-man"),("mcif","chemical/x-mmcif"),("mcm","chemical/x-macmolecule"),("mdb","application/msaccess"),("me","application/x-troff-me"),("mesh","model/mesh"),("mid","audio/midi"),("midi","audio/midi"),("mif","application/x-mif"),("mm","application/x-freemind"),("mmd","chemical/x-macromodel-input"),("mmf","application/vnd.smaf"),("mml","text/mathml"),("mmod","chemical/x-macromodel-input"),("mng","video/x-mng"),("moc","text/x-moc"),("mol","chemical/x-mdl-molfile"),("mol2","chemical/x-mol2"),("moo","chemical/x-mopac-out"),("mop","chemical/x-mopac-input"),("mopcrt","chemical/x-mopac-input"),("mov","video/quicktime"),("movie","video/x-sgi-movie"),("mp2","audio/mpeg"),("mp3","audio/mpeg"),("mp4","video/mp4"),("mpc","chemical/x-mopac-input"),("mpe","video/mpeg"),("mpeg","video/mpeg"),("mpega","audio/mpeg"),("mpg","video/mpeg"),("mpga","audio/mpeg"),("ms","application/x-troff-ms"),("msh","model/mesh"),("msi","application/x-msi"),("mvb","chemical/x-mopac-vib"),("mxu","video/vnd.mpegurl"),("nb","application/mathematica"),("nc","application/x-netcdf"),("nwc","application/x-nwc"),("o","application/x-object"),("oda","application/oda"),("odb","application/vnd.oasis.opendocument.database"),("odc","application/vnd.oasis.opendocument.chart"),("odf","application/vnd.oasis.opendocument.formula"),("odg","application/vnd.oasis.opendocument.graphics"),("odi","application/vnd.oasis.opendocument.image"),("odm","application/vnd.oasis.opendocument.text-master"),("odp","application/vnd.oasis.opendocument.presentation"),("ods","application/vnd.oasis.opendocument.spreadsheet"),("odt","application/vnd.oasis.opendocument.text"),("oga","audio/ogg"),("ogg","application/ogg"),("ogv","video/ogg"),("ogx","application/ogg"),("old","application/x-trash"),("otg","application/vnd.oasis.opendocument.graphics-template"),("oth","application/vnd.oasis.opendocument.text-web"),("otp","application/vnd.oasis.opendocument.presentation-template"),("ots","application/vnd.oasis.opendocument.spreadsheet-template"),("ott","application/vnd.oasis.opendocument.text-template"),("oza","application/x-oz-application"),("p","text/x-pascal"),("p7r","application/x-pkcs7-certreqresp"),("pac","application/x-ns-proxy-autoconfig"),("pas","text/x-pascal"),("pat","image/x-coreldrawpattern"),("patch","text/x-diff"),("pbm","image/x-portable-bitmap"),("pcap","application/cap"),("pcf","application/x-font"),("pcf.Z","application/x-font"),("pcx","image/pcx"),("pdb","chemical/x-pdb"),("pdf","application/pdf"),("pfa","application/x-font"),("pfb","application/x-font"),("pgm","image/x-portable-graymap"),("pgn","application/x-chess-pgn"),("pgp","application/pgp-signature"),("php","application/x-httpd-php"),("php3","application/x-httpd-php3"),("php3p","application/x-httpd-php3-preprocessed"),("php4","application/x-httpd-php4"),("phps","application/x-httpd-php-source"),("pht","application/x-httpd-php"),("phtml","application/x-httpd-php"),("pk","application/x-tex-pk"),("pl","text/x-perl"),("pls","audio/x-scpls"),("pm","text/x-perl"),("png","image/png"),("pnm","image/x-portable-anymap"),("pot","text/plain"),("ppm","image/x-portable-pixmap"),("pps","application/vnd.ms-powerpoint"),("ppt","application/vnd.ms-powerpoint"),("prf","application/pics-rules"),("prt","chemical/x-ncbi-asn1-ascii"),("ps","application/postscript"),("psd","image/x-photoshop"),("py","text/x-python"),("pyc","application/x-python-code"),("pyo","application/x-python-code"),("qt","video/quicktime"),("qtl","application/x-quicktimeplayer"),("ra","audio/x-pn-realaudio"),("ram","audio/x-pn-realaudio"),("rar","application/rar"),("ras","image/x-cmu-raster"),("rd","chemical/x-mdl-rdfile"),("rdf","application/rdf+xml"),("rgb","image/x-rgb"),("rhtml","application/x-httpd-eruby"),("rm","audio/x-pn-realaudio"),("roff","application/x-troff"),("ros","chemical/x-rosdal"),("rpm","application/x-redhat-package-manager"),("rss","application/rss+xml"),("rtf","application/rtf"),("rtx","text/richtext"),("rxn","chemical/x-mdl-rxnfile"),("sct","text/scriptlet"),("sd","chemical/x-mdl-sdfile"),("sd2","audio/x-sd2"),("sda","application/vnd.stardivision.draw"),("sdc","application/vnd.stardivision.calc"),("sdd","application/vnd.stardivision.impress"),("sdf","application/vnd.stardivision.math"),("sds","application/vnd.stardivision.chart"),("sdw","application/vnd.stardivision.writer"),("ser","application/java-serialized-object"),("sgf","application/x-go-sgf"),("sgl","application/vnd.stardivision.writer-global"),("sh","application/x-sh"),("shar","application/x-shar"),("shtml","text/html"),("sid","audio/prs.sid"),("sik","application/x-trash"),("silo","model/mesh"),("sis","application/vnd.symbian.install"),("sisx","x-epoc/x-sisx-app"),("sit","application/x-stuffit"),("sitx","application/x-stuffit"),("skd","application/x-koan"),("skm","application/x-koan"),("skp","application/x-koan"),("skt","application/x-koan"),("smi","application/smil"),("smil","application/smil"),("snd","audio/basic"),("spc","chemical/x-galactic-spc"),("spl","application/futuresplash"),("spx","audio/ogg"),("src","application/x-wais-source"),("stc","application/vnd.sun.xml.calc.template"),("std","application/vnd.sun.xml.draw.template"),("sti","application/vnd.sun.xml.impress.template"),("stl","application/vnd.ms-pki.stl"),("stw","application/vnd.sun.xml.writer.template"),("sty","text/x-tex"),("sv4cpio","application/x-sv4cpio"),("sv4crc","application/x-sv4crc"),("svg","image/svg+xml"),("svgz","image/svg+xml"),("sw","chemical/x-swissprot"),("swf","application/x-shockwave-flash"),("swfl","application/x-shockwave-flash"),("sxc","application/vnd.sun.xml.calc"),("sxd","application/vnd.sun.xml.draw"),("sxg","application/vnd.sun.xml.writer.global"),("sxi","application/vnd.sun.xml.impress"),("sxm","application/vnd.sun.xml.math"),("sxw","application/vnd.sun.xml.writer"),("t","application/x-troff"),("tar","application/x-tar"),("taz","application/x-gtar"),("tcl","application/x-tcl"),("tex","text/x-tex"),("texi","application/x-texinfo"),("texinfo","application/x-texinfo"),("text","text/plain"),("tgf","chemical/x-mdl-tgf"),("tgz","application/x-gtar"),("tif","image/tiff"),("tiff","image/tiff"),("tk","text/x-tcl"),("tm","text/texmacs"),("torrent","application/x-bittorrent"),("tr","application/x-troff"),("ts","text/texmacs"),("tsp","application/dsptype"),("tsv","text/tab-separated-values"),("txt","text/plain"),("udeb","application/x-debian-package"),("uls","text/iuls"),("ustar","application/x-ustar"),("val","chemical/x-ncbi-asn1-binary"),("vcd","application/x-cdlink"),("vcf","text/x-vcard"),("vcs","text/x-vcalendar"),("vmd","chemical/x-vmd"),("vms","chemical/x-vamas-iso14976"),("vrm","x-world/x-vrml"),("vrml","model/vrml"),("vsd","application/vnd.visio"),("wad","application/x-doom"),("wav","audio/x-wav"),("wax","audio/x-ms-wax"),("wbmp","image/vnd.wap.wbmp"),("wbxml","application/vnd.wap.wbxml"),("wk","application/x-123"),("wm","video/x-ms-wm"),("wma","audio/x-ms-wma"),("wmd","application/x-ms-wmd"),("wml","text/vnd.wap.wml"),("wmlc","application/vnd.wap.wmlc"),("wmls","text/vnd.wap.wmlscript"),("wmlsc","application/vnd.wap.wmlscriptc"),("wmv","video/x-ms-wmv"),("wmx","video/x-ms-wmx"),("wmz","application/x-ms-wmz"),("wp5","application/wordperfect5.1"),("wpd","application/wordperfect"),("wrl","model/vrml"),("wsc","text/scriptlet"),("wvx","video/x-ms-wvx"),("wz","application/x-wingz"),("xbm","image/x-xbitmap"),("xcf","application/x-xcf"),("xht","application/xhtml+xml"),("xhtml","application/xhtml+xml"),("xlb","application/vnd.ms-excel"),("xls","application/vnd.ms-excel"),("xlt","application/vnd.ms-excel"),("xml","application/xml"),("xpi","application/x-xpinstall"),("xpm","image/x-xpixmap"),("xsl","application/xml"),("xtel","chemical/x-xtel"),("xul","application/vnd.mozilla.xul+xml"),("xwd","image/x-xwindowdump"),("xyz","chemical/x-xyz"),("zip","application/zip"),("zmt","chemical/x-mopac-input"),("~","application/x-trash")]+
− src/Happstack/Server/HTTP/Client.hs
@@ -1,53 +0,0 @@-module Happstack.Server.HTTP.Client where---import Happstack.Server.HTTP.Handler-import Happstack.Server.HTTP.Types-import Data.Maybe-import Control.Monad-import qualified Data.ByteString.Lazy.Char8 as L --import System.IO-import qualified Data.ByteString.Char8 as B -import Network---- | Sends the serialized request to the host defined in the request--- and attempts to parse response upon arrival.-getResponse :: Request -> IO (Either String Response)-getResponse rq = withSocketsDo $ do- let (hostName,p) = span (/=':') $ fromJust $ fmap B.unpack $ getHeader "host" rq - portInt = if null p then 80 else read $ tail p- portId = PortNumber $ toEnum $ portInt- h <- connectTo hostName portId - hSetBuffering h NoBuffering-- putRequest h rq- hFlush h-- inputStr <- L.hGetContents h- return $ parseResponse inputStr--unproxify :: Request -> Request-unproxify rq = rq {rqPaths = tail $ rqPaths rq,- rqHeaders = - forwardedFor $ forwardedHost $ - setHeader "host" (head $ rqPaths rq) $- rqHeaders rq}- where- appendInfo hdr val = setHeader hdr (csv val $- maybe "" B.unpack $- getHeader hdr rq)- forwardedFor = appendInfo "X-Forwarded-For" (fst $ rqPeer rq)- forwardedHost = appendInfo "X-Forwarded-Host" - (B.unpack $ fromJust $ getHeader "host" rq)- csv v "" = v- csv v x = x++", " ++ v--unrproxify :: String -> [(String, String)] -> Request -> Request-unrproxify defaultHost list rq = - let host::String- host = fromMaybe defaultHost $ flip lookup list =<< B.unpack `liftM` getHeader "host" rq - newrq = rq {rqPaths = host: rqPaths rq}- in unproxify newrq--
− src/Happstack/Server/HTTP/Clock.hs
@@ -1,30 +0,0 @@-{-# OPTIONS -fno-cse #-}-module Happstack.Server.HTTP.Clock(getApproximateTime) where--import Control.Concurrent-import Data.IORef-import System.IO.Unsafe-import System.Time-import System.Locale--import qualified Data.ByteString.Char8 as B--mkTime :: IO B.ByteString-mkTime = do now <- getClockTime- return $ B.pack (formatCalendarTime defaultTimeLocale "%a, %d %b %Y %X GMT" (toUTCTime now))---{-# NOINLINE clock #-}-clock :: IORef B.ByteString-clock = unsafePerformIO $ do- ref <- newIORef =<< mkTime- forkIO $ updater ref- return ref--updater :: IORef B.ByteString -> IO ()-updater ref = do threadDelay (10^(6 :: Int)) -- Every second- writeIORef ref =<< mkTime- updater ref--getApproximateTime :: IO B.ByteString-getApproximateTime = readIORef clock
− src/Happstack/Server/HTTP/FileServe.hs
@@ -1,354 +0,0 @@-{-# LANGUAGE FlexibleContexts, Rank2Types #-}--- |File Serving functions-module Happstack.Server.HTTP.FileServe- (- -- * Content-Type \/ Mime-Type- MimeMap,- mimeTypes,- asContentType,- guessContentType,- guessContentTypeM,- -- * Low-Level- sendFileResponse, - lazyByteStringResponse,- strictByteStringResponse,- filePathSendFile,- filePathLazy,- filePathStrict,- -- * High-Level- -- ** Serving a single file- serveFile,- serveFileUsing,- -- ** Serving files from a directory- fileServe',- fileServe,- fileServeLazy,- fileServeStrict,- -- * Other- blockDotFiles,- defaultIxFiles,- doIndex,- doIndex',- doIndexLazy,- doIndexStrict,- errorwrapper,- fileNotFound,- isDot- ) where--import Control.Exception.Extensible (IOException, SomeException, Exception(fromException), bracket, handleJust)-import Control.Monad (MonadPlus(mzero))-import Control.Monad.Trans (MonadIO(liftIO))-import qualified Data.ByteString.Lazy.Char8 as L-import qualified Data.ByteString.Char8 as S-import Data.Maybe (fromMaybe)-import Data.Map (Map)-import qualified Data.Map as Map-import Happstack.Server.SimpleHTTP (FilterMonad, ServerMonad(askRq), Request(..), Response(..), WebMonad, toResponse, resultBS, setHeader, forbidden, nullRsFlags, result, require, rsfContentLength, seeOther, ifModifiedSince )-import System.Directory (doesDirectoryExist, doesFileExist, getModificationTime)-import System.IO (IOMode(ReadMode), hFileSize, hClose, openBinaryFile)-import System.FilePath ((</>), addTrailingPathSeparator, joinPath, takeExtension)-import System.Log.Logger (Priority(DEBUG), logM)-import System.Time (CalendarTime, toUTCTime)--ioErrors :: SomeException -> Maybe IOException-ioErrors = fromException--errorwrapper :: (MonadIO m, MonadPlus m, FilterMonad Response m) => String -> String -> m Response-errorwrapper binarylocation loglocation- = require getErrorLog $ \errorLog ->- return $ toResponse errorLog- where getErrorLog- = handleJust ioErrors (const (return Nothing)) $- do bintime <- getModificationTime binarylocation- logtime <- getModificationTime loglocation- if (logtime > bintime)- then fmap Just $ readFile loglocation- else return Nothing---- * Mime-Type / Content-Type--type MimeMap = Map String String---- | Ready collection of common mime types.--- Except for the first two entries, the mappings come from an Ubuntu 8.04 /etc/mime.types file.-mimeTypes :: MimeMap-mimeTypes = Map.fromList [("gz","application/x-gzip"),("cabal","application/x-cabal"),("%","application/x-trash"),("323","text/h323"),("3gp","video/3gpp"),("7z","application/x-7z-compressed"),("abw","application/x-abiword"),("ai","application/postscript"),("aif","audio/x-aiff"),("aifc","audio/x-aiff"),("aiff","audio/x-aiff"),("alc","chemical/x-alchemy"),("art","image/x-jg"),("asc","text/plain"),("asf","video/x-ms-asf"),("asn","chemical/x-ncbi-asn1"),("aso","chemical/x-ncbi-asn1-binary"),("asx","video/x-ms-asf"),("atom","application/atom"),("atomcat","application/atomcat+xml"),("atomsrv","application/atomserv+xml"),("au","audio/basic"),("avi","video/x-msvideo"),("b","chemical/x-molconn-Z"),("bak","application/x-trash"),("bat","application/x-msdos-program"),("bcpio","application/x-bcpio"),("bib","text/x-bibtex"),("bin","application/octet-stream"),("bmp","image/x-ms-bmp"),("boo","text/x-boo"),("book","application/x-maker"),("bsd","chemical/x-crossfire"),("c","text/x-csrc"),("c++","text/x-c++src"),("c3d","chemical/x-chem3d"),("cab","application/x-cab"),("cac","chemical/x-cache"),("cache","chemical/x-cache"),("cap","application/cap"),("cascii","chemical/x-cactvs-binary"),("cat","application/vnd.ms-pki.seccat"),("cbin","chemical/x-cactvs-binary"),("cbr","application/x-cbr"),("cbz","application/x-cbz"),("cc","text/x-c++src"),("cdf","application/x-cdf"),("cdr","image/x-coreldraw"),("cdt","image/x-coreldrawtemplate"),("cdx","chemical/x-cdx"),("cdy","application/vnd.cinderella"),("cef","chemical/x-cxf"),("cer","chemical/x-cerius"),("chm","chemical/x-chemdraw"),("chrt","application/x-kchart"),("cif","chemical/x-cif"),("class","application/java-vm"),("cls","text/x-tex"),("cmdf","chemical/x-cmdf"),("cml","chemical/x-cml"),("cod","application/vnd.rim.cod"),("com","application/x-msdos-program"),("cpa","chemical/x-compass"),("cpio","application/x-cpio"),("cpp","text/x-c++src"),("cpt","application/mac-compactpro"),("crl","application/x-pkcs7-crl"),("crt","application/x-x509-ca-cert"),("csf","chemical/x-cache-csf"),("csh","application/x-csh"),("csm","chemical/x-csml"),("csml","chemical/x-csml"),("css","text/css"),("csv","text/csv"),("ctab","chemical/x-cactvs-binary"),("ctx","chemical/x-ctx"),("cu","application/cu-seeme"),("cub","chemical/x-gaussian-cube"),("cxf","chemical/x-cxf"),("cxx","text/x-c++src"),("d","text/x-dsrc"),("dat","chemical/x-mopac-input"),("dcr","application/x-director"),("deb","application/x-debian-package"),("dif","video/dv"),("diff","text/x-diff"),("dir","application/x-director"),("djv","image/vnd.djvu"),("djvu","image/vnd.djvu"),("dl","video/dl"),("dll","application/x-msdos-program"),("dmg","application/x-apple-diskimage"),("dms","application/x-dms"),("doc","application/msword"),("dot","application/msword"),("dv","video/dv"),("dvi","application/x-dvi"),("dx","chemical/x-jcamp-dx"),("dxr","application/x-director"),("emb","chemical/x-embl-dl-nucleotide"),("embl","chemical/x-embl-dl-nucleotide"),("eml","message/rfc822"),("ent","chemical/x-ncbi-asn1-ascii"),("eps","application/postscript"),("etx","text/x-setext"),("exe","application/x-msdos-program"),("ez","application/andrew-inset"),("fb","application/x-maker"),("fbdoc","application/x-maker"),("fch","chemical/x-gaussian-checkpoint"),("fchk","chemical/x-gaussian-checkpoint"),("fig","application/x-xfig"),("flac","application/x-flac"),("fli","video/fli"),("fm","application/x-maker"),("frame","application/x-maker"),("frm","application/x-maker"),("gal","chemical/x-gaussian-log"),("gam","chemical/x-gamess-input"),("gamin","chemical/x-gamess-input"),("gau","chemical/x-gaussian-input"),("gcd","text/x-pcs-gcd"),("gcf","application/x-graphing-calculator"),("gcg","chemical/x-gcg8-sequence"),("gen","chemical/x-genbank"),("gf","application/x-tex-gf"),("gif","image/gif"),("gjc","chemical/x-gaussian-input"),("gjf","chemical/x-gaussian-input"),("gl","video/gl"),("gnumeric","application/x-gnumeric"),("gpt","chemical/x-mopac-graph"),("gsf","application/x-font"),("gsm","audio/x-gsm"),("gtar","application/x-gtar"),("h","text/x-chdr"),("h++","text/x-c++hdr"),("hdf","application/x-hdf"),("hh","text/x-c++hdr"),("hin","chemical/x-hin"),("hpp","text/x-c++hdr"),("hqx","application/mac-binhex40"),("hs","text/x-haskell"),("hta","application/hta"),("htc","text/x-component"),("htm","text/html"),("html","text/html"),("hxx","text/x-c++hdr"),("ica","application/x-ica"),("ice","x-conference/x-cooltalk"),("ico","image/x-icon"),("ics","text/calendar"),("icz","text/calendar"),("ief","image/ief"),("iges","model/iges"),("igs","model/iges"),("iii","application/x-iphone"),("inp","chemical/x-gamess-input"),("ins","application/x-internet-signup"),("iso","application/x-iso9660-image"),("isp","application/x-internet-signup"),("ist","chemical/x-isostar"),("istr","chemical/x-isostar"),("jad","text/vnd.sun.j2me.app-descriptor"),("jar","application/java-archive"),("java","text/x-java"),("jdx","chemical/x-jcamp-dx"),("jmz","application/x-jmol"),("jng","image/x-jng"),("jnlp","application/x-java-jnlp-file"),("jpe","image/jpeg"),("jpeg","image/jpeg"),("jpg","image/jpeg"),("js","application/x-javascript"),("kar","audio/midi"),("key","application/pgp-keys"),("kil","application/x-killustrator"),("kin","chemical/x-kinemage"),("kml","application/vnd.google-earth.kml+xml"),("kmz","application/vnd.google-earth.kmz"),("kpr","application/x-kpresenter"),("kpt","application/x-kpresenter"),("ksp","application/x-kspread"),("kwd","application/x-kword"),("kwt","application/x-kword"),("latex","application/x-latex"),("lha","application/x-lha"),("lhs","text/x-literate-haskell"),("lsf","video/x-la-asf"),("lsx","video/x-la-asf"),("ltx","text/x-tex"),("lyx","application/x-lyx"),("lzh","application/x-lzh"),("lzx","application/x-lzx"),("m3u","audio/mpegurl"),("m4a","audio/mpeg"),("maker","application/x-maker"),("man","application/x-troff-man"),("mcif","chemical/x-mmcif"),("mcm","chemical/x-macmolecule"),("mdb","application/msaccess"),("me","application/x-troff-me"),("mesh","model/mesh"),("mid","audio/midi"),("midi","audio/midi"),("mif","application/x-mif"),("mm","application/x-freemind"),("mmd","chemical/x-macromodel-input"),("mmf","application/vnd.smaf"),("mml","text/mathml"),("mmod","chemical/x-macromodel-input"),("mng","video/x-mng"),("moc","text/x-moc"),("mol","chemical/x-mdl-molfile"),("mol2","chemical/x-mol2"),("moo","chemical/x-mopac-out"),("mop","chemical/x-mopac-input"),("mopcrt","chemical/x-mopac-input"),("mov","video/quicktime"),("movie","video/x-sgi-movie"),("mp2","audio/mpeg"),("mp3","audio/mpeg"),("mp4","video/mp4"),("mpc","chemical/x-mopac-input"),("mpe","video/mpeg"),("mpeg","video/mpeg"),("mpega","audio/mpeg"),("mpg","video/mpeg"),("mpga","audio/mpeg"),("ms","application/x-troff-ms"),("msh","model/mesh"),("msi","application/x-msi"),("mvb","chemical/x-mopac-vib"),("mxu","video/vnd.mpegurl"),("nb","application/mathematica"),("nc","application/x-netcdf"),("nwc","application/x-nwc"),("o","application/x-object"),("oda","application/oda"),("odb","application/vnd.oasis.opendocument.database"),("odc","application/vnd.oasis.opendocument.chart"),("odf","application/vnd.oasis.opendocument.formula"),("odg","application/vnd.oasis.opendocument.graphics"),("odi","application/vnd.oasis.opendocument.image"),("odm","application/vnd.oasis.opendocument.text-master"),("odp","application/vnd.oasis.opendocument.presentation"),("ods","application/vnd.oasis.opendocument.spreadsheet"),("odt","application/vnd.oasis.opendocument.text"),("oga","audio/ogg"),("ogg","application/ogg"),("ogv","video/ogg"),("ogx","application/ogg"),("old","application/x-trash"),("otg","application/vnd.oasis.opendocument.graphics-template"),("oth","application/vnd.oasis.opendocument.text-web"),("otp","application/vnd.oasis.opendocument.presentation-template"),("ots","application/vnd.oasis.opendocument.spreadsheet-template"),("ott","application/vnd.oasis.opendocument.text-template"),("oza","application/x-oz-application"),("p","text/x-pascal"),("p7r","application/x-pkcs7-certreqresp"),("pac","application/x-ns-proxy-autoconfig"),("pas","text/x-pascal"),("pat","image/x-coreldrawpattern"),("patch","text/x-diff"),("pbm","image/x-portable-bitmap"),("pcap","application/cap"),("pcf","application/x-font"),("pcf.Z","application/x-font"),("pcx","image/pcx"),("pdb","chemical/x-pdb"),("pdf","application/pdf"),("pfa","application/x-font"),("pfb","application/x-font"),("pgm","image/x-portable-graymap"),("pgn","application/x-chess-pgn"),("pgp","application/pgp-signature"),("php","application/x-httpd-php"),("php3","application/x-httpd-php3"),("php3p","application/x-httpd-php3-preprocessed"),("php4","application/x-httpd-php4"),("phps","application/x-httpd-php-source"),("pht","application/x-httpd-php"),("phtml","application/x-httpd-php"),("pk","application/x-tex-pk"),("pl","text/x-perl"),("pls","audio/x-scpls"),("pm","text/x-perl"),("png","image/png"),("pnm","image/x-portable-anymap"),("pot","text/plain"),("ppm","image/x-portable-pixmap"),("pps","application/vnd.ms-powerpoint"),("ppt","application/vnd.ms-powerpoint"),("prf","application/pics-rules"),("prt","chemical/x-ncbi-asn1-ascii"),("ps","application/postscript"),("psd","image/x-photoshop"),("py","text/x-python"),("pyc","application/x-python-code"),("pyo","application/x-python-code"),("qt","video/quicktime"),("qtl","application/x-quicktimeplayer"),("ra","audio/x-pn-realaudio"),("ram","audio/x-pn-realaudio"),("rar","application/rar"),("ras","image/x-cmu-raster"),("rd","chemical/x-mdl-rdfile"),("rdf","application/rdf+xml"),("rgb","image/x-rgb"),("rhtml","application/x-httpd-eruby"),("rm","audio/x-pn-realaudio"),("roff","application/x-troff"),("ros","chemical/x-rosdal"),("rpm","application/x-redhat-package-manager"),("rss","application/rss+xml"),("rtf","application/rtf"),("rtx","text/richtext"),("rxn","chemical/x-mdl-rxnfile"),("sct","text/scriptlet"),("sd","chemical/x-mdl-sdfile"),("sd2","audio/x-sd2"),("sda","application/vnd.stardivision.draw"),("sdc","application/vnd.stardivision.calc"),("sdd","application/vnd.stardivision.impress"),("sdf","application/vnd.stardivision.math"),("sds","application/vnd.stardivision.chart"),("sdw","application/vnd.stardivision.writer"),("ser","application/java-serialized-object"),("sgf","application/x-go-sgf"),("sgl","application/vnd.stardivision.writer-global"),("sh","application/x-sh"),("shar","application/x-shar"),("shtml","text/html"),("sid","audio/prs.sid"),("sik","application/x-trash"),("silo","model/mesh"),("sis","application/vnd.symbian.install"),("sisx","x-epoc/x-sisx-app"),("sit","application/x-stuffit"),("sitx","application/x-stuffit"),("skd","application/x-koan"),("skm","application/x-koan"),("skp","application/x-koan"),("skt","application/x-koan"),("smi","application/smil"),("smil","application/smil"),("snd","audio/basic"),("spc","chemical/x-galactic-spc"),("spl","application/futuresplash"),("spx","audio/ogg"),("src","application/x-wais-source"),("stc","application/vnd.sun.xml.calc.template"),("std","application/vnd.sun.xml.draw.template"),("sti","application/vnd.sun.xml.impress.template"),("stl","application/vnd.ms-pki.stl"),("stw","application/vnd.sun.xml.writer.template"),("sty","text/x-tex"),("sv4cpio","application/x-sv4cpio"),("sv4crc","application/x-sv4crc"),("svg","image/svg+xml"),("svgz","image/svg+xml"),("sw","chemical/x-swissprot"),("swf","application/x-shockwave-flash"),("swfl","application/x-shockwave-flash"),("sxc","application/vnd.sun.xml.calc"),("sxd","application/vnd.sun.xml.draw"),("sxg","application/vnd.sun.xml.writer.global"),("sxi","application/vnd.sun.xml.impress"),("sxm","application/vnd.sun.xml.math"),("sxw","application/vnd.sun.xml.writer"),("t","application/x-troff"),("tar","application/x-tar"),("taz","application/x-gtar"),("tcl","application/x-tcl"),("tex","text/x-tex"),("texi","application/x-texinfo"),("texinfo","application/x-texinfo"),("text","text/plain"),("tgf","chemical/x-mdl-tgf"),("tgz","application/x-gtar"),("tif","image/tiff"),("tiff","image/tiff"),("tk","text/x-tcl"),("tm","text/texmacs"),("torrent","application/x-bittorrent"),("tr","application/x-troff"),("ts","text/texmacs"),("tsp","application/dsptype"),("tsv","text/tab-separated-values"),("txt","text/plain"),("udeb","application/x-debian-package"),("uls","text/iuls"),("ustar","application/x-ustar"),("val","chemical/x-ncbi-asn1-binary"),("vcd","application/x-cdlink"),("vcf","text/x-vcard"),("vcs","text/x-vcalendar"),("vmd","chemical/x-vmd"),("vms","chemical/x-vamas-iso14976"),("vrm","x-world/x-vrml"),("vrml","model/vrml"),("vsd","application/vnd.visio"),("wad","application/x-doom"),("wav","audio/x-wav"),("wax","audio/x-ms-wax"),("wbmp","image/vnd.wap.wbmp"),("wbxml","application/vnd.wap.wbxml"),("wk","application/x-123"),("wm","video/x-ms-wm"),("wma","audio/x-ms-wma"),("wmd","application/x-ms-wmd"),("wml","text/vnd.wap.wml"),("wmlc","application/vnd.wap.wmlc"),("wmls","text/vnd.wap.wmlscript"),("wmlsc","application/vnd.wap.wmlscriptc"),("wmv","video/x-ms-wmv"),("wmx","video/x-ms-wmx"),("wmz","application/x-ms-wmz"),("wp5","application/wordperfect5.1"),("wpd","application/wordperfect"),("wrl","model/vrml"),("wsc","text/scriptlet"),("wvx","video/x-ms-wvx"),("wz","application/x-wingz"),("xbm","image/x-xbitmap"),("xcf","application/x-xcf"),("xht","application/xhtml+xml"),("xhtml","application/xhtml+xml"),("xlb","application/vnd.ms-excel"),("xls","application/vnd.ms-excel"),("xlt","application/vnd.ms-excel"),("xml","application/xml"),("xpi","application/x-xpinstall"),("xpm","image/x-xpixmap"),("xsl","application/xml"),("xtel","chemical/x-xtel"),("xul","application/vnd.mozilla.xul+xml"),("xwd","image/x-xwindowdump"),("xyz","chemical/x-xyz"),("zip","application/zip"),("zmt","chemical/x-mopac-input"),("~","application/x-trash")]---guessContentType :: MimeMap -> FilePath -> Maybe String-guessContentType mimeMap filepath =- case getExt filepath of- "" -> Nothing- ext -> Map.lookup ext mimeMap--guessContentTypeM :: (Monad m) => MimeMap -> (FilePath -> m String)-guessContentTypeM mimeMap filePath = return $ fromMaybe "text/plain" $ guessContentType mimeMap filePath--asContentType :: (Monad m) => String -> (FilePath -> m String)-asContentType = const . return--defaultIxFiles :: [String]-defaultIxFiles= ["index.html","index.xml","index.gif"]---- | return a simple "File not found 404 page."-fileNotFound :: (Monad m, FilterMonad Response m) => FilePath -> m Response-fileNotFound fp = return $ result 404 $ "File not found " ++ fp---- | Similar to 'takeExtension' but does not include the extension separator char-getExt :: FilePath -> String-getExt fp = drop 1 $ takeExtension fp---- | Prevents files of the form '.foo' or 'bar/.foo' from being served-blockDotFiles :: (Request -> IO Response) -> Request -> IO Response-blockDotFiles fn rq- | isDot (joinPath (rqPaths rq)) = return $ result 403 "Dot files not allowed."- | otherwise = fn rq---- | Returns True if the given String either starts with a . or is of the form--- "foo/.bar", e.g. the typical *nix convention for hidden files.-isDot :: String -> Bool-isDot = isD . reverse- where- isD ('.':'/':_) = True- isD ['.'] = True- --isD ('/':_) = False- isD (_:cs) = isD cs- isD [] = False---- * Low-level functions for generating a Response---- | Use sendFile to send the contents of a Handle-sendFileResponse :: String -- ^ content-type string- -> FilePath -- ^ file path for content to send- -> Maybe (CalendarTime, Request) -- ^ mod-time for the handle (MUST NOT be later than server's time of message origination), incoming request (used to check for if-modified-since header)- -> Integer -- ^ offset into Handle- -> Integer -- ^ number of bytes to send- -> Response-sendFileResponse ct filePath mModTime _offset count =- let res = ((setHeader "Content-Length" (show count)) .- (setHeader "Content-Type" ct) $ - (SendFile 200 Map.empty nullRsFlags{rsfContentLength=False} Nothing filePath 0 count)- )- in case mModTime of- Nothing -> res- (Just (modTime, request)) -> ifModifiedSince modTime request res---- | Send the contents of a Lazy ByteString-lazyByteStringResponse :: String -- ^ content-type string (e.g. @\"text/plain; charset=utf-8\"@)- -> L.ByteString -- ^ lazy bytestring content to send- -> Maybe (CalendarTime, Request) -- ^ mod-time for the bytestring, incoming request (used to check for if-modified-since header)- -> Integer -- ^ offset into the bytestring- -> Integer -- ^ number of bytes to send (offset + count must be less than or equal to the length of the bytestring)- -> Response-lazyByteStringResponse ct body mModTime offset count =- let res = ((setHeader "Content-Type" ct) $- resultBS 200 (L.take (fromInteger count) $ (L.drop (fromInteger offset)) body)- )- in case mModTime of- Nothing -> res- (Just (modTime, request)) -> ifModifiedSince modTime request res---- | Send the contents of a Lazy ByteString-strictByteStringResponse :: String -- ^ content-type string (e.g. @\"text/plain; charset=utf-8\"@)- -> S.ByteString -- ^ lazy bytestring content to send- -> Maybe (CalendarTime, Request) -- ^ mod-time for the bytestring, incoming request (used to check for if-modified-since header)- -> Integer -- ^ offset into the bytestring- -> Integer -- ^ number of bytes to send (offset + count must be less than or equal to the length of the bytestring)- -> Response-strictByteStringResponse ct body mModTime offset count =- let res = ((setHeader "Content-Type" ct) $- resultBS 200 (L.fromChunks [S.take (fromInteger count) $ S.drop (fromInteger offset) body])- )- in case mModTime of- Nothing -> res- (Just (modTime, request)) -> ifModifiedSince modTime request res---- | Send the specified file with the specified mime-type using sendFile()------ NOTE: assumes file exists and is readable by the server. See 'serveFileUsing'.------ WARNING: No security checks are performed.-filePathSendFile :: (ServerMonad m, MonadIO m)- => String -- ^ content-type string- -> FilePath -- ^ path to file on disk- -> m Response-filePathSendFile contentType fp =- do handle <- liftIO $ openBinaryFile fp ReadMode -- garbage collection should close this- modtime <- liftIO $ getModificationTime fp- count <- liftIO $ hFileSize handle- rq <- askRq- return $ sendFileResponse contentType fp (Just (toUTCTime modtime, rq)) 0 count---- | Send the specified file with the specified mime-type using Lazy ByteStrings------ NOTE: assumes file exists and is readable by the server. See 'serveFileUsing'.------ WARNING: No security checks are performed.-filePathLazy :: (ServerMonad m, MonadIO m)- => String -- ^ content-type string- -> FilePath -- ^ path to file on disk- -> m Response-filePathLazy contentType fp =- do handle <- liftIO $ openBinaryFile fp ReadMode -- garbage collection should close this- contents <- liftIO $ L.hGetContents handle- modtime <- liftIO $ getModificationTime fp- count <- liftIO $ hFileSize handle- rq <- askRq- return $ lazyByteStringResponse contentType contents (Just (toUTCTime modtime, rq)) 0 count---- | Send the specified file with the specified mime-type using Lazy ByteStrings------ NOTE: assumes file exists and is readable by the server. See 'serveFileUsing'.------ WARNING: No security checks are performed.-filePathStrict :: (ServerMonad m, MonadIO m)- => String -- ^ content-type string- -> FilePath -- ^ path to file on disk- -> m Response-filePathStrict contentType fp =- do contents <- liftIO $ S.readFile fp- modtime <- liftIO $ getModificationTime fp- count <- liftIO $ bracket (openBinaryFile fp ReadMode) hClose hFileSize- rq <- askRq- return $ strictByteStringResponse contentType contents (Just (toUTCTime modtime, rq)) 0 count---- * High-level functions for serving files----- ** Serve a single file---- | Serve a single, specified file.--- --- example 1:--- --- Serve using sendfile() and the specified content-type------ > serveFileUsing filePathSendFile (asContentType "image/jpeg") "/srv/data/image.jpg"--------- example 2:--- --- Serve using a lazy ByteString and the guess the content-type from the extension--- --- > serveFileUsing filePathLazy (guessContentTypeM mimeTypes) "/srv/data/image.jpg"--- --- WARNING: No security checks are performed.-serveFileUsing :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) - => (String -> FilePath -> m Response) -- ^ typically 'filePathSendFile', 'filePathLazy', or 'filePathStrict'- -> (FilePath -> m String) -- ^ function for determining content-type of file. Typically 'asContentType' or 'guessContentTypeM'- -> FilePath -- ^ path to the file to serve- -> m Response-serveFileUsing serveFn mimeFn fp = - do fe <- liftIO $ doesFileExist fp- if fe- then do mt <- mimeFn fp- serveFn mt fp- else mzero---- | Alias for 'serveFileUsing' 'filePathSendFile'-serveFile :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) => (FilePath -> m String) -> FilePath -> m Response-serveFile = serveFileUsing filePathSendFile---- ** Serve files from a directory---- | Serve files from a directory and its subdirectories (parameterizable version)--- --- Parameterize this function to create functions like, 'fileServe', 'fileServeLazy', and 'fileServeStrict'------ You supply:------ 1. a low-level function which takes a content-type and 'FilePath' and generates a Response--- 2. a function which determines the content-type from the 'FilePath'--- 3. a list of all the default index files------ NOTE: unlike fileServe, there are no index files by default. See 'defaultIxFiles'.-fileServe' :: ( WebMonad Response m- , ServerMonad m- , FilterMonad Response m- , MonadIO m- , MonadPlus m- ) - => (String -> FilePath -> m Response) -- ^ function which takes a content-type and filepath and generates a response (typically 'filePathSendFile', 'filePathLazy', or 'filePathStrict')- -> (FilePath -> m String) -- ^ function which returns the mime-type for FilePath- -> [FilePath] -- ^ index file names, in case the requested path is a directory- -> FilePath -- ^ file/directory to serve- -> m Response-fileServe' serveFn mimeFn ixFiles localpath = do- rq <- askRq- let safepath = filter (\x->not (null x) && x /= ".." && x /= ".") (rqPaths rq)- fp = joinPath (localpath:safepath)- fe <- liftIO $ doesFileExist fp- de <- liftIO $ doesDirectoryExist fp- let status | de = "DIR"- | fe = "file"- | True = "NOT FOUND"- liftIO $ logM "Happstack.Server.HTTP.FileServe" DEBUG ("fileServe: "++show fp++" \t"++status)- if de- then if last (rqUri rq) == '/'- then doIndex' serveFn mimeFn (ixFiles++defaultIxFiles) fp- else do let path' = addTrailingPathSeparator (rqUri rq)- seeOther path' (toResponse path')- else if fe - then serveFileUsing serveFn mimeFn fp- else mzero---- | Serve files from a directory and its subdirectories (sendFile version). Should perform much better than its predecessors.-fileServe :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) =>- [FilePath] -- ^ index file names, in case the requested path is a directory- -> FilePath -- ^ file/directory to serve- -> m Response-fileServe ixFiles localPath = fileServe' filePathSendFile (guessContentTypeM mimeTypes) (ixFiles ++ defaultIxFiles) localPath---- | Serve files from a directory and its subdirectories (lazy ByteString version).--- --- May leak file handles.-fileServeLazy :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) =>- [FilePath] -- ^ index file names, in case the requested path is a directory- -> FilePath -- ^ file/directory to serve- -> m Response-fileServeLazy ixFiles localPath = fileServe' filePathLazy (guessContentTypeM mimeTypes) (ixFiles ++ defaultIxFiles) localPath---- | Serve files from a directory and its subdirectories (strict ByteString version). -fileServeStrict :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) =>- [FilePath] -- ^ index file names, in case the next argument is a directory- -> FilePath -- ^ file/directory to serve- -> m Response-fileServeStrict ixFiles localPath = fileServe' filePathStrict (guessContentTypeM mimeTypes) (ixFiles ++ defaultIxFiles) localPath---- * Index--doIndex :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m)- => [String]- -> MimeMap- -> String- -> m Response-doIndex ixFiles mimeMap localPath = doIndex' filePathSendFile (guessContentTypeM mimeMap) ixFiles localPath--doIndexLazy :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m)- => [String]- -> MimeMap- -> String- -> m Response-doIndexLazy ixFiles mimeMap localPath = doIndex' filePathLazy (guessContentTypeM mimeMap) ixFiles localPath--doIndexStrict :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m)- => [String]- -> MimeMap- -> String- -> m Response-doIndexStrict ixFiles mimeMap localPath = doIndex' filePathStrict (guessContentTypeM mimeMap) ixFiles localPath--doIndex' :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m)- => (String -> FilePath -> m Response)- -> (FilePath -> m String)- -> [String]- -> String- -> m Response-doIndex' _serveFn _mime [] _fp = forbidden $ toResponse "Directory index forbidden"-doIndex' serveFn mimeFn (index:rest) fp =- do let path = fp </> index- fe <- liftIO $ doesFileExist path- if fe - then serveFileUsing serveFn mimeFn path - else doIndex' serveFn mimeFn rest fp
− src/Happstack/Server/HTTP/Handler.hs
@@ -1,365 +0,0 @@-{-# LANGUAGE ScopedTypeVariables, ScopedTypeVariables #-}--module Happstack.Server.HTTP.Handler(request-- version,required- ,parseResponse,putRequest--- ,unchunkBody,val,testChunk,pack-) where--- ,fsepC,crlfC,pversion-import qualified Paths_happstack_server as Paths-import qualified Data.Version as DV-import Control.Exception.Extensible as E-import Control.Monad-import Data.List(elemIndex)-import Data.Char(toLower)-import Data.Maybe ( fromMaybe, fromJust, isJust, isNothing )-import Prelude hiding (last)-import qualified Data.List as List-import qualified Data.ByteString.Char8 as P-import qualified Data.ByteString.Char8 as B-import qualified Data.ByteString.Lazy.Char8 as L-import qualified Data.ByteString.Lazy.Internal as L-import qualified Data.ByteString.Internal as S-import qualified Data.ByteString.Unsafe as S-import qualified Data.ByteString as S-import System.IO.Unsafe-import qualified Data.Map as M-import System.IO-import Numeric-import Data.Int (Int64)-import Happstack.Server.Cookie-import Happstack.Server.HTTP.Clock-import Happstack.Server.HTTP.Types-import Happstack.Server.HTTP.Multipart-import Happstack.Server.HTTP.RFC822Headers-import Happstack.Server.MessageWrap-import Happstack.Server.SURI(SURI(..),path,query)-import Happstack.Server.SURI.ParseURI-import Happstack.Util.TimeOut-import Happstack.Util.LogFormat (formatRequestCombined)-import Data.Time.Clock (getCurrentTime)-import Network.Socket.SendFile (unsafeSendFile')-import System.Log.Logger (Priority(..), logM)---hGetContentsN :: Int -> Handle -> IO L.ByteString-hGetContentsN k h = lazyRead -- TODO close on exceptions- where- lazyRead = unsafeInterleaveIO loop-- loop = do- c <- S.hGetNonBlocking h k- if S.null c- then do eof <- hIsEOF h- if eof then hClose h >> return L.Empty- else hWaitForInput h (-1)- >> loop-- --then hClose h >> return Empty- else do cs <- lazyRead- return (L.Chunk c cs)--hGetContents' :: Handle -> IO L.ByteString-hGetContents' = hGetContentsN L.defaultChunkSize--request :: Conf -> Handle -> Host -> (Request -> IO Response) -> IO ()-request conf h host handler = rloop conf h host handler =<< hGetContents' h--required :: String -> Maybe a -> Either String a-required err Nothing = Left err-required _ (Just a) = Right a--transferEncodingC :: [Char]-transferEncodingC = "transfer-encoding"-rloop :: t- -> Handle- -> Host- -> (Request -> IO Response)- -> L.ByteString- -> IO ()-rloop conf h host handler inputStr- | L.null inputStr = return ()- | otherwise- = join $ withTimeOut (30 * second) $- do let parseRequest- = do- (topStr, restStr) <- required "failed to separate request" $ splitAtEmptyLine inputStr- (rql, headerStr) <- required "failed to separate headers/body" $ splitAtCRLF topStr- let (m,u,v) = requestLine rql- headers' <- parseHeaders "host" (L.unpack headerStr)- let headers = mkHeaders headers'- let contentLength = fromMaybe 0 $ fmap fst (P.readInt =<< getHeaderUnsafe contentlengthC headers)- (body, nextRequest) <- case () of- () | contentLength < 0 -> fail "negative content-length"- | isJust $ getHeader transferEncodingC headers ->- return $ consumeChunks restStr- | otherwise -> return (L.splitAt (fromIntegral contentLength) restStr)- let cookies = [ (cookieName c, c) | cl <- fromMaybe [] (fmap getCookies (getHeader "Cookie" headers)), c <- cl ] -- Ugle- rqTmp = Request m (pathEls (path u)) (path u) (query u)- [] cookies v headers (Body body) host- rq = rqTmp{rqInputs = queryInput u ++ bodyInput rqTmp}- return (rq, nextRequest)- case parseRequest of- Left err -> error $ "failed to parse HTTP request: " ++ err- Right (req, rest)- -> return $- do let ioseq act = act >>= \x -> x `seq` return x- res <- ioseq (handler req) `E.catch` \(e::E.SomeException) -> return $ result 500 $ "Server error: " ++ show e-- -- combined log format- time <- getCurrentTime- let host' = fst host- user = "-"- requestLn = unwords [show $ rqMethod req, rqUri req, show $ rqVersion req]- responseCode = rsCode res- size = maybe (-1) (read . B.unpack) (getHeader "Content-Length" res) -- -1 indicates unknown size- referer = B.unpack $ fromMaybe (B.pack "") $ getHeader "Referer" req- userAgent = B.unpack $ fromMaybe (B.pack "") $ getHeader "User-Agent" req- logM "Happstack.Server.AccessLog.Combined" INFO $ formatRequestCombined host' user time requestLn responseCode size referer userAgent-- putAugmentedResult h req res- when (continueHTTP req res) $ rloop conf h host handler rest---- | Unserializes the bytestring into a response. If there is an--- error it will return @Left msg@.-parseResponse :: L.ByteString -> Either String Response-parseResponse inputStr =- do (topStr,restStr) <- required "failed to separate response" $- splitAtEmptyLine inputStr- (rsl,headerStr) <- required "failed to separate headers/body" $- splitAtCRLF topStr- let (_,code) = responseLine rsl- headers' <- parseHeaders "host" (L.unpack headerStr)- let headers = mkHeaders headers'- let mbCL = fmap fst (B.readInt =<< getHeader "content-length" headers)- (body,_) <-- maybe (if (isNothing $ getHeader "transfer-encoding" headers)- then return (restStr,L.pack "")- else return $ consumeChunks restStr)- (\cl->return (L.splitAt (fromIntegral cl) restStr))- mbCL- return $ Response {rsCode=code,rsHeaders=headers,rsBody=body,rsFlags=RsFlags True,rsValidator=Nothing}---- http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html--- note this does NOT handle extenions-consumeChunks::L.ByteString->(L.ByteString,L.ByteString)-consumeChunks str = let (parts,tr,rest) = consumeChunksImpl str in (L.concat . (++ [tr]) .map snd $ parts,rest)--consumeChunksImpl :: L.ByteString -> ([(Int64, L.ByteString)], L.ByteString, L.ByteString)-consumeChunksImpl str- | L.null str = ([],L.empty,str)- | chunkLen == 0 = let (last,rest') = L.splitAt lenLine1 str- (tr',rest'') = getTrailer rest'- in ([(0,last)],tr',rest'')- | otherwise = ((chunkLen,part):crest,tr,rest2)- where- line1 = head $ lazylines str- lenLine1 = (L.length line1) + 1 -- endchar- chunkLen = (fst $ head $ readHex $ L.unpack line1)- len = chunkLen + lenLine1 + 2- (part,rest) = L.splitAt len str- (crest,tr,rest2) = consumeChunksImpl rest- getTrailer s = L.splitAt index s- where index | crlfLC `L.isPrefixOf` s = 2- | otherwise = let iscrlf = L.zipWith (\a b -> a == '\r' && b == '\n') s . L.tail $ s- Just i = elemIndex True $ zipWith (&&) iscrlf (tail (tail iscrlf))- in fromIntegral $ i+4--crlfLC :: L.ByteString-crlfLC = L.pack "\r\n"---- Properly lazy version of 'lines' for lazy bytestrings-lazylines :: L.ByteString -> [L.ByteString]-lazylines s- | L.null s = []- | otherwise =- let (l,s') = L.break ((==) '\n') s- in l : if L.null s' then []- else lazylines (L.tail s')--requestLine :: L.ByteString -> (Method, SURI, Version)-requestLine l = case P.words ((P.concat . L.toChunks) l) of- [rq,uri,ver] -> (method rq, SURI $ parseURIRef uri, version ver)- [rq,uri] -> (method rq, SURI $ parseURIRef uri,Version 0 9)- x -> error $ "requestLine cannot handle input: " ++ (show x)--responseLine :: L.ByteString -> (B.ByteString, Int)-responseLine l = case B.words ((B.concat . L.toChunks) l) of- (v:c:_) -> version v `seq` (v,fst (fromJust (B.readInt c)))- x -> error $ "responseLine cannot handle input: " ++ (show x)---method :: B.ByteString -> Method-method r = fj $ lookup r mtable- where fj (Just x) = x- fj Nothing = error "invalid request method"- mtable = [(P.pack "GET", GET),- (P.pack "HEAD", HEAD),- (P.pack "POST", POST),- (P.pack "PUT", PUT),- (P.pack "DELETE", DELETE),- (P.pack "TRACE", TRACE),- (P.pack "OPTIONS", OPTIONS),- (P.pack "CONNECT", CONNECT)]---- Result side--staticHeaders :: Headers-staticHeaders =- foldr (uncurry setHeaderBS) (mkHeaders [])- [ (serverC, happsC), (contentTypeC, textHtmlC) ]---- FIXME: we should not be controlling the response headers in mysterious ways in this low level code--- headers should be set by application code and the core http engine should be very lean.-putAugmentedResult :: Handle -> Request -> Response -> IO ()-putAugmentedResult outp req res = do- case res of- -- standard bytestring response- Response {} -> do- sendTop (fromIntegral (L.length (rsBody res)))- when (rqMethod req /= HEAD) (L.hPut outp $ rsBody res)- -- zero-copy sendfile response- -- the handle *should* be closed by the garbage collector- SendFile {} -> do- let infp = sfFilePath res- off = sfOffset res- count = sfCount res- sendTop count- unsafeSendFile' outp infp off count- hFlush outp- where ph (HeaderPair k vs) = map (\v -> P.concat [k, fsepC, v, crlfC]) vs- sendTop cl = do- allHeaders <- augmentHeaders req res cl- mapM_ (P.hPut outp) $ concat- [ (pversion $ rqVersion req) -- Print HTTP version- , [responseMessage $ rsCode res] -- Print responseCode- , concatMap ph (M.elems allHeaders) -- Print all headers- , [crlfC]- ]--augmentHeaders :: Request -> Response -> Integer -> IO Headers-augmentHeaders req res cl = do- -- TODO: Hoist static headers to the toplevel.- raw <- getApproximateTime- let stdHeaders = staticHeaders `M.union`- M.fromList ( [ (dateCLower, HeaderPair dateC [raw])- , (connectionCLower, HeaderPair connectionC [if continueHTTP req res then keepAliveC else closeC])- ] ++ if rsfContentLength (rsFlags res)- then [(contentlengthC, HeaderPair contentLengthC [P.pack (show cl)])]- else [] )- return (rsHeaders res `M.union` stdHeaders) -- 'union' prefers 'headers res' when duplicate keys are encountered.---- | Serializes the request to the given handle-putRequest :: Handle -> Request -> IO ()-putRequest h rq = do- let put = B.hPut h- ph (HeaderPair k vs) = map (\v -> B.concat [k, fsepC, v, crlfC]) vs- sp = [B.pack " "]- mapM_ put $ concat- [[B.pack $ show $ rqMethod rq],sp- ,[B.pack $ rqURL rq],sp- ,(pversion $ rqVersion rq), [crlfC]- ,concatMap ph (M.elems $ rqHeaders rq)- ,[crlfC]- ]- let Body body = rqBody rq- L.hPut h body- hFlush h------ Version--pversion :: Version -> [B.ByteString]-pversion (Version 1 1) = [http11]-pversion (Version 1 0) = [http10]-pversion (Version x y) = [P.pack "HTTP/", P.pack (show x), P.pack ".", P.pack (show y)]--version :: B.ByteString -> Version-version x | x == http09 = Version 0 9- | x == http10 = Version 1 0- | x == http11 = Version 1 1- | otherwise = error "Invalid HTTP version"--http09 :: B.ByteString-http09 = P.pack "HTTP/0.9"-http10 :: B.ByteString-http10 = P.pack "HTTP/1.0"-http11 :: B.ByteString-http11 = P.pack "HTTP/1.1"---- Constants--connectionC :: B.ByteString-connectionC = P.pack "Connection"-connectionCLower :: B.ByteString-connectionCLower = P.map toLower connectionC-closeC :: B.ByteString-closeC = P.pack "close"-keepAliveC :: B.ByteString-keepAliveC = P.pack "Keep-Alive"-crlfC :: B.ByteString-crlfC = P.pack "\r\n"-fsepC :: B.ByteString-fsepC = P.pack ": "-contentTypeC :: B.ByteString-contentTypeC = P.pack "Content-Type"-contentLengthC :: B.ByteString-contentLengthC = P.pack "Content-Length"-contentlengthC :: B.ByteString-contentlengthC = P.pack "content-length"-dateC :: B.ByteString-dateC = P.pack "Date"-dateCLower :: B.ByteString-dateCLower = P.map toLower dateC-serverC :: B.ByteString-serverC = P.pack "Server"-happsC :: B.ByteString-happsC = P.pack $ "Happstack/" ++ DV.showVersion Paths.version-textHtmlC :: B.ByteString-textHtmlC = P.pack "text/html; charset=utf-8"---- Response code names--responseMessage :: (Num t) => t -> B.ByteString-responseMessage 100 = P.pack " 100 Continue\r\n"-responseMessage 101 = P.pack " 101 Switching Protocols\r\n"-responseMessage 200 = P.pack " 200 OK\r\n"-responseMessage 201 = P.pack " 201 Created\r\n"-responseMessage 202 = P.pack " 202 Accepted\r\n"-responseMessage 203 = P.pack " 203 Non-Authoritative Information\r\n"-responseMessage 204 = P.pack " 204 No Content\r\n"-responseMessage 205 = P.pack " 205 Reset Content\r\n"-responseMessage 206 = P.pack " 206 Partial Content\r\n"-responseMessage 300 = P.pack " 300 Multiple Choices\r\n"-responseMessage 301 = P.pack " 301 Moved Permanently\r\n"-responseMessage 302 = P.pack " 302 Found\r\n"-responseMessage 303 = P.pack " 303 See Other\r\n"-responseMessage 304 = P.pack " 304 Not Modified\r\n"-responseMessage 305 = P.pack " 305 Use Proxy\r\n"-responseMessage 307 = P.pack " 307 Temporary Redirect\r\n"-responseMessage 400 = P.pack " 400 Bad Request\r\n"-responseMessage 401 = P.pack " 401 Unauthorized\r\n"-responseMessage 402 = P.pack " 402 Payment Required\r\n"-responseMessage 403 = P.pack " 403 Forbidden\r\n"-responseMessage 404 = P.pack " 404 Not Found\r\n"-responseMessage 405 = P.pack " 405 Method Not Allowed\r\n"-responseMessage 406 = P.pack " 406 Not Acceptable\r\n"-responseMessage 407 = P.pack " 407 Proxy Authentication Required\r\n"-responseMessage 408 = P.pack " 408 Request Time-out\r\n"-responseMessage 409 = P.pack " 409 Conflict\r\n"-responseMessage 410 = P.pack " 410 Gone\r\n"-responseMessage 411 = P.pack " 411 Length Required\r\n"-responseMessage 412 = P.pack " 412 Precondition Failed\r\n"-responseMessage 413 = P.pack " 413 Request Entity Too Large\r\n"-responseMessage 414 = P.pack " 414 Request-URI Too Large\r\n"-responseMessage 415 = P.pack " 415 Unsupported Media Type\r\n"-responseMessage 416 = P.pack " 416 Requested range not satisfiable\r\n"-responseMessage 417 = P.pack " 417 Expectation Failed\r\n"-responseMessage 500 = P.pack " 500 Internal Server Error\r\n"-responseMessage 501 = P.pack " 501 Not Implemented\r\n"-responseMessage 502 = P.pack " 502 Bad Gateway\r\n"-responseMessage 503 = P.pack " 503 Service Unavailable\r\n"-responseMessage 504 = P.pack " 504 Gateway Time-out\r\n"-responseMessage 505 = P.pack " 505 HTTP Version not supported\r\n"-responseMessage x = P.pack (" " ++ show x ++ " \r\n")-
− src/Happstack/Server/HTTP/LazyLiner.hs
@@ -1,41 +0,0 @@-{-# LANGUAGE ForeignFunctionInterface #-}-module Happstack.Server.HTTP.LazyLiner- (Lazy, newLinerHandle, headerLines, getBytes, getBytesStrict, getRest, L.toChunks- ) where--import Control.Concurrent.MVar-import System.IO-import qualified Data.ByteString.Char8 as P-import qualified Data.ByteString.Lazy.Char8 as L--newtype Lazy = Lazy (MVar L.ByteString)--newLinerHandle :: Handle -> IO Lazy-newLinerHandle h = fmap Lazy (newMVar =<< L.hGetContents h)--headerLines :: Lazy -> IO [P.ByteString]-headerLines (Lazy mv) = modifyMVar mv $ \l -> do- let loop acc r0 = let (h,r) = L.break ((==) ch) r0- ph = toStrict h- phl = P.length ph- ph2 = if phl == 0 || P.last ph /= '\x0D' then ph else P.init ph- ch = '\x0A'- r' = if L.null r then r else L.tail r- in if P.length ph2 == 0 then (r', reverse acc) else loop (ph2:acc) r'- return $ loop [] l--getBytesStrict :: Lazy -> Int -> IO P.ByteString-getBytesStrict (Lazy mv) len = modifyMVar mv $ \l -> do- let (h,p) = L.splitAt (fromIntegral len) l- return (p, toStrict h)--getBytes :: Lazy -> Int -> IO L.ByteString-getBytes (Lazy mv) len = modifyMVar mv $ \l -> do- let (h,p) = L.splitAt (fromIntegral len) l- return (p, h)--getRest :: Lazy -> IO L.ByteString-getRest (Lazy mv) = modifyMVar mv $ \l -> return (L.empty, l)--toStrict :: L.ByteString -> P.ByteString-toStrict = P.concat . L.toChunks
− src/Happstack/Server/HTTP/Listen.hs
@@ -1,94 +0,0 @@-{-# LANGUAGE CPP, ScopedTypeVariables, ScopedTypeVariables #-}-module Happstack.Server.HTTP.Listen(listen, listen',listenOn) where--import Happstack.Server.HTTP.Types-import Happstack.Server.HTTP.Handler-import Happstack.Server.HTTP.Socket (acceptLite)-import Control.Exception.Extensible as E-import Control.Concurrent-import Network.BSD (getProtocolNumber)-import Network(sClose, Socket)-import Network.Socket as Socket (SocketOption(KeepAlive), setSocketOption, - socket, Family(..), SockAddr, - SocketOption(..), SockAddr(..), - iNADDR_ANY, maxListenQueue, SocketType(..), - bindSocket)-import qualified Network.Socket as Socket (listen)-import System.IO-{--#ifndef mingw32_HOST_OS--}-import System.Posix.Signals-{--#endif--}-import System.Log.Logger (Priority(..), logM)-log':: Priority -> String -> IO ()-log' = logM "Happstack.Server.HTTP.Listen"---{-- Network.listenOn binds randomly to IPv4 or IPv6 or both,- depending on system and local settings.- Lets make it use IPv4 only for now.--}--listenOn :: Int -> IO Socket-listenOn portm = do- proto <- getProtocolNumber "tcp"- E.bracketOnError- (socket AF_INET Stream proto)- (sClose)- (\sock -> do- setSocketOption sock ReuseAddr 1- bindSocket sock (SockAddrInet (fromIntegral portm) iNADDR_ANY)- Socket.listen sock maxListenQueue- return sock- )----- | Bind and listen port-listen :: Conf -> (Request -> IO Response) -> IO ()-listen conf hand = do- let port' = port conf- socketm <- listenOn port'- setSocketOption socketm KeepAlive 1- listen' socketm conf hand---- | Use a previously bind port and listen-listen' :: Socket -> Conf -> (Request -> IO Response) -> IO ()-listen' s conf hand = do-{--#ifndef mingw32_HOST_OS--}- installHandler openEndedPipe Ignore Nothing-{--#endif--}- let port' = port conf- log' NOTICE ("Listening on port " ++ show port')- let work (h,hn,p) = do -- hSetBuffering h NoBuffering- let eh (x::SomeException) = log' ERROR ("HTTP request failed with: "++show x)- request conf h (hn,fromIntegral p) hand `E.catch` eh- hClose h- let loop = do acceptLite s >>= forkIO . work- loop- let pe e = log' ERROR ("ERROR in accept thread: "++- show e)- let infi = loop `catchSome` pe >> infi -- loop `E.catch` pe >> infi- infi `finally` sClose s-{---#ifndef mingw32_HOST_OS--}- installHandler openEndedPipe Ignore Nothing- return ()-{--#endif--}- where -- why are these handlers needed?-- catchSome op h = op `E.catches` [- Handler $ \(e :: ArithException) -> h (toException e),- Handler $ \(e :: ArrayException) -> h (toException e)- ]-
− src/Happstack/Server/HTTP/LowLevel.hs
@@ -1,28 +0,0 @@-module Happstack.Server.HTTP.LowLevel- (-- * HTTP Implementation- -- $impl-- -- * Problems- -- $problems-- -- * API- module Happstack.Server.HTTP.Handler,- module Happstack.Server.HTTP.Listen,- module Happstack.Server.HTTP.Types- ) where--import Happstack.Server.HTTP.Handler-import Happstack.Server.HTTP.Listen-import Happstack.Server.HTTP.Types---- $impl--- The Happstack HTTP implementation supports HTTP 1.0 and 1.1.--- Multiple request on a connection including pipelining is supported.---- $problems--- Currently if a client sends an invalid HTTP request the whole--- connection is aborted and no further processing is done.------ When the connection times out Happstack closes it. In future it could--- send a 408 response but this may be problematic if the sending--- of a response caused the problem.
− src/Happstack/Server/HTTP/Multipart.hs
@@ -1,215 +0,0 @@--- #hide---------------------------------------------------------------------------------- |--- Module : Happstack.Server.HTTP.Multipart--- Copyright : (c) Peter Thiemann 2001,2002--- (c) Bjorn Bringert 2005-2006--- (c) Lemmih 2007--- License : BSD-style------ Maintainer : lemmih@vo.com--- Stability : experimental--- Portability : non-portable------ Parsing of the multipart format from RFC2046.--- Partly based on code from WASHMail.----------------------------------------------------------------------------------module Happstack.Server.HTTP.Multipart- (- -- * Multi-part messages- MultiPart(..), BodyPart(..), Header- , parseMultipartBody, hGetMultipartBody- -- * Headers- , ContentType(..), ContentTransferEncoding(..)- , ContentDisposition(..)- , parseContentType- , parseContentTransferEncoding- , parseContentDisposition- , getContentType- , getContentTransferEncoding- , getContentDisposition-- , splitAtEmptyLine- , splitAtCRLF- , splitParts- ) where--import Control.Monad-import Data.Int (Int64)-import Data.Maybe-import System.IO (Handle)--import Happstack.Server.HTTP.RFC822Headers--import qualified Data.ByteString.Lazy.Char8 as BS-import Data.ByteString.Lazy.Char8 (ByteString)------- * Multi-part stuff.-----data MultiPart = MultiPart [BodyPart]- deriving (Show, Read, Eq, Ord)--data BodyPart = BodyPart [Header] ByteString- deriving (Show, Read, Eq, Ord)---- | Read a multi-part message from a 'ByteString'.-parseMultipartBody :: String -- ^ Boundary- -> ByteString -> Maybe MultiPart-parseMultipartBody b s = - do- ps <- splitParts (BS.pack b) s- liftM MultiPart $ mapM parseBodyPart ps---- | Read a multi-part message from a 'Handle'.--- Fails on parse errors.-hGetMultipartBody :: String -- ^ Boundary- -> Handle- -> IO MultiPart-hGetMultipartBody b h = - do- s <- BS.hGetContents h- case parseMultipartBody b s of- Nothing -> fail "Error parsing multi-part message"- Just m -> return m----parseBodyPart :: ByteString -> Maybe BodyPart-parseBodyPart s =- do- (hdr,bdy) <- splitAtEmptyLine s- hs <- parseM pHeaders "<input>" (BS.unpack hdr)- return $ BodyPart hs bdy------- * Splitting into multipart parts.------- | Split a multipart message into the multipart parts.-splitParts :: ByteString -- ^ The boundary, without the initial dashes- -> ByteString - -> Maybe [ByteString]-splitParts b s = dropPreamble b s >>= spl- where- spl x = case splitAtBoundary b x of- Nothing -> Nothing- Just (s1,d,s2) | isClose b d -> Just [s1]- | otherwise -> spl s2 >>= Just . (s1:)---- | Drop everything up to and including the first line starting --- with the boundary. Returns 'Nothing' if there is no --- line starting with a boundary.-dropPreamble :: ByteString -- ^ The boundary, without the initial dashes- -> ByteString - -> Maybe ByteString-dropPreamble b s | isBoundary b s = fmap snd (splitAtCRLF s)- | otherwise = dropLine s >>= dropPreamble b---- | Split a string at the first boundary line.-splitAtBoundary :: ByteString -- ^ The boundary, without the initial dashes- -> ByteString -- ^ String to split.- -> Maybe (ByteString,ByteString,ByteString)- -- ^ The part before the boundary, the boundary line,- -- and the part after the boundary line. The CRLF- -- before and the CRLF (if any) after the boundary line- -- are not included in any of the strings returned.- -- Returns 'Nothing' if there is no boundary.-splitAtBoundary b s = spl 0- where- spl i = case findCRLF (BS.drop i s) of- Nothing -> Nothing- Just (j,l) | isBoundary b s2 -> Just (s1,d,s3)- | otherwise -> spl (i+j+l)- where - s1 = BS.take (i+j) s- s2 = BS.drop (i+j+l) s- (d,s3) = splitAtCRLF_ s2---- | Check whether a string starts with two dashes followed by--- the given boundary string.-isBoundary :: ByteString -- ^ The boundary, without the initial dashes- -> ByteString- -> Bool-isBoundary b s = startsWithDashes s && b `BS.isPrefixOf` BS.drop 2 s---- | Check whether a string for which 'isBoundary' returns true--- has two dashes after the boudary string.-isClose :: ByteString -- ^ The boundary, without the initial dashes- -> ByteString - -> Bool-isClose b = startsWithDashes . BS.drop (2+BS.length b)---- | Checks whether a string starts with two dashes.-startsWithDashes :: ByteString -> Bool-startsWithDashes s = BS.pack "--" `BS.isPrefixOf` s-------- * RFC 2046 CRLF------- | Drop everything up to and including the first CRLF.-dropLine :: ByteString -> Maybe ByteString-dropLine = fmap snd . splitAtCRLF---- | Split a string at the first empty line. The CRLF (if any) before the--- empty line is included in the first result. The CRLF after the--- empty line is not included in the result.--- 'Nothing' is returned if there is no empty line.-splitAtEmptyLine :: ByteString -> Maybe (ByteString, ByteString)-splitAtEmptyLine s | startsWithCRLF s = Just (BS.empty, dropCRLF s)- | otherwise = spl 0- where- spl i = case findCRLF (BS.drop i s) of- Nothing -> Nothing- Just (j,l) | startsWithCRLF s2 -> Just (s1, dropCRLF s2)- | otherwise -> spl (i+j+l)- where (s1,s2) = BS.splitAt (i+j+l) s---- | Split a string at the first CRLF. The CRLF is not included--- in any of the returned strings.-splitAtCRLF :: ByteString -- ^ String to split.- -> Maybe (ByteString,ByteString)- -- ^ Returns 'Nothing' if there is no CRLF.-splitAtCRLF s = case findCRLF s of- Nothing -> Nothing- Just (i,l) -> Just (s1, BS.drop l s2)- where (s1,s2) = BS.splitAt i s---- | Like 'splitAtCRLF', but if no CRLF is found, the first--- result is the argument string, and the second result is empty.-splitAtCRLF_ :: ByteString -> (ByteString,ByteString)-splitAtCRLF_ s = fromMaybe (s,BS.empty) (splitAtCRLF s)---- | Get the index and length of the first CRLF, if any.-findCRLF :: ByteString -- ^ String to split.- -> Maybe (Int64,Int64)-findCRLF s = - case findCRorLF s of- Nothing -> Nothing- Just j | BS.null (BS.drop (j+1) s) -> Just (j,1)- Just j -> case (BS.index s j, BS.index s (j+1)) of- ('\r','\n') -> Just (j,2)- _ -> Just (j,1)--findCRorLF :: ByteString -> Maybe Int64-findCRorLF = BS.findIndex (\c -> c == '\n' || c == '\r')--startsWithCRLF :: ByteString -> Bool-startsWithCRLF s = not (BS.null s) && (c == '\n' || c == '\r')- where c = BS.index s 0---- | Drop an initial CRLF, if any. If the string is empty, --- nothing is done. If the string does not start with CRLF,--- the first character is dropped.-dropCRLF :: ByteString -> ByteString-dropCRLF s | BS.null s = BS.empty- | BS.null (BS.drop 1 s) = BS.empty- | c0 == '\r' && c1 == '\n' = BS.drop 2 s- | otherwise = BS.drop 1 s- where c0 = BS.index s 0- c1 = BS.index s 1
− src/Happstack/Server/HTTP/RFC822Headers.hs
@@ -1,266 +0,0 @@--- #hide---------------------------------------------------------------------------------- |--- Module : Network.CGI.RFC822Headers--- Copyright : (c) Peter Thiemann 2001,2002--- (c) Bjorn Bringert 2005-2006--- (c) Lemmih 2007--- License : BSD-style------ Maintainer : lemmih@vo.com--- Stability : experimental--- Portability : portable------ Parsing of RFC822-style headers (name, value pairs)--- Partly based on code from WASHMail.----------------------------------------------------------------------------------module Happstack.Server.HTTP.RFC822Headers- ( -- * Headers- Header, - pHeader,- pHeaders,- parseHeaders,-- -- * Content-type- ContentType(..), - getContentType,- parseContentType,- showContentType,-- -- * Content-transfer-encoding- ContentTransferEncoding(..),- getContentTransferEncoding,- parseContentTransferEncoding,-- -- * Content-disposition- ContentDisposition(..),- getContentDisposition, - parseContentDisposition,- - -- * Utilities- parseM- ) where--import Data.Char-import Data.List-import Text.ParserCombinators.Parsec--type Header = (String, String)--pHeaders :: Parser [Header]-pHeaders = many pHeader--parseHeaders :: Monad m => SourceName -> String -> m [Header]-parseHeaders = parseM pHeaders--pHeader :: Parser Header-pHeader = - do name <- many1 headerNameChar- char ':'- many ws1- line <- lineString- crLf- extraLines <- many extraFieldLine- return (map toLower name, concat (line:extraLines))--extraFieldLine :: Parser String-extraFieldLine = - do sp <- ws1- line <- lineString- crLf- return (sp:line)------- * Parameters (for Content-type etc.)-----showParameters :: [(String,String)] -> String-showParameters = concatMap f- where f (n,v) = "; " ++ n ++ "=\"" ++ concatMap esc v ++ "\""- esc '\\' = "\\\\"- esc '"' = "\\\""- esc c | c `elem` ['\\','"'] = '\\':[c]- | otherwise = [c]--p_parameter :: Parser (String,String)-p_parameter =- do lexeme $ char ';'- p_name <- lexeme $ p_token- lexeme $ char '='- -- Workaround for seemingly standardized web browser bug- -- where nothing is escaped in the filename parameter- -- of the content-disposition header in multipart/form-data- let litStr = if p_name == "filename" - then buggyLiteralString- else literalString- p_value <- litStr <|> p_token- return (map toLower p_name, p_value)----- --- * Content type------- | A MIME media type value.--- The 'Show' instance is derived automatically.--- Use 'showContentType' to obtain the standard--- string representation.--- See <http://www.ietf.org/rfc/rfc2046.txt> for more--- information about MIME media types.-data ContentType = - ContentType {- -- | The top-level media type, the general type- -- of the data. Common examples are- -- \"text\", \"image\", \"audio\", \"video\",- -- \"multipart\", and \"application\".- ctType :: String,- -- | The media subtype, the specific data format.- -- Examples include \"plain\", \"html\",- -- \"jpeg\", \"form-data\", etc.- ctSubtype :: String,- -- | Media type parameters. On common example is- -- the charset parameter for the \"text\" - -- top-level type, e.g. @(\"charset\",\"ISO-8859-1\")@.- ctParameters :: [(String, String)]- }- deriving (Show, Read, Eq, Ord)---- | Produce the standard string representation of a content-type,--- e.g. \"text\/html; charset=ISO-8859-1\".-showContentType :: ContentType -> String-showContentType (ContentType x y ps) = x ++ "/" ++ y ++ showParameters ps--pContentType :: Parser ContentType-pContentType = - do many ws1- c_type <- p_token- lexeme $ char '/'- c_subtype <- lexeme $ p_token- c_parameters <- many p_parameter- return $ ContentType (map toLower c_type) (map toLower c_subtype) c_parameters---- | Parse the standard representation of a content-type.--- If the input cannot be parsed, this function calls--- 'fail' with a (hopefully) informative error message.-parseContentType :: Monad m => String -> m ContentType-parseContentType = parseM pContentType "Content-type"--getContentType :: Monad m => [Header] -> m ContentType-getContentType hs = lookupM "content-type" hs >>= parseContentType------- * Content transfer encoding-----data ContentTransferEncoding =- ContentTransferEncoding String- deriving (Show, Read, Eq, Ord)--pContentTransferEncoding :: Parser ContentTransferEncoding-pContentTransferEncoding =- do many ws1- c_cte <- p_token- return $ ContentTransferEncoding (map toLower c_cte)--parseContentTransferEncoding :: Monad m => String -> m ContentTransferEncoding-parseContentTransferEncoding = - parseM pContentTransferEncoding "Content-transfer-encoding"--getContentTransferEncoding :: Monad m => [Header] -> m ContentTransferEncoding-getContentTransferEncoding hs = - lookupM "content-transfer-encoding" hs >>= parseContentTransferEncoding------- * Content disposition-----data ContentDisposition =- ContentDisposition String [(String, String)]- deriving (Show, Read, Eq, Ord)--pContentDisposition :: Parser ContentDisposition-pContentDisposition =- do many ws1- c_cd <- p_token- c_parameters <- many p_parameter- return $ ContentDisposition (map toLower c_cd) c_parameters--parseContentDisposition :: Monad m => String -> m ContentDisposition-parseContentDisposition = parseM pContentDisposition "Content-disposition"--getContentDisposition :: Monad m => [Header] -> m ContentDisposition-getContentDisposition hs = - lookupM "content-disposition" hs >>= parseContentDisposition------- * Utilities-----parseM :: Monad m => Parser a -> SourceName -> String -> m a-parseM p n inp =- case parse p n inp of- Left e -> fail (show e)- Right x -> return x--lookupM :: (Monad m, Eq a, Show a) => a -> [(a,b)] -> m b-lookupM n = maybe (fail ("No such field: " ++ show n)) return . lookup n---- --- * Parsing utilities------- | RFC 822 LWSP-char-ws1 :: Parser Char-ws1 = oneOf " \t"--lexeme :: Parser a -> Parser a-lexeme p = do x <- p; many ws1; return x---- | RFC 822 CRLF (but more permissive)-crLf :: Parser String-crLf = try (string "\n\r" <|> string "\r\n") <|> string "\n" <|> string "\r"---- | One line-lineString :: Parser String-lineString = many (noneOf "\n\r")--literalString :: Parser String-literalString = do char '\"'- str <- many (noneOf "\"\\" <|> quoted_pair)- char '\"'- return str---- No web browsers seem to implement RFC 2046 correctly,--- since they do not escape double quotes and backslashes--- in the filename parameter in multipart/form-data.------ Note that this eats everything until the last double quote on the line.-buggyLiteralString :: Parser String-buggyLiteralString = - do char '\"'- str <- manyTill anyChar (try lastQuote)- return str- where lastQuote = do char '\"' - notFollowedBy (try (many (noneOf "\"") >> char '\"'))--headerNameChar :: Parser Char-headerNameChar = noneOf "\n\r:"--especials, tokenchar :: [Char]-especials = "()<>@,;:\\\"/[]?.="-tokenchar = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" \\ especials--p_token :: Parser String-p_token = many1 (oneOf tokenchar)--text_chars :: [Char]-text_chars = map chr ([1..9] ++ [11,12] ++ [14..127])--p_text :: Parser Char-p_text = oneOf text_chars--quoted_pair :: Parser Char-quoted_pair = do char '\\'- p_text
− src/Happstack/Server/HTTP/Socket.hs
@@ -1,52 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}-module Happstack.Server.HTTP.Socket(acceptLite) where--import Happstack.Server.HTTP.SocketTH(supportsIPv6)-import Language.Haskell.TH.Syntax-import Happstack.Util.HostAddress-import qualified Network as N- ( PortID(PortNumber)- , socketPort- )-import qualified Network.Socket as S- ( Socket(..)- , PortNumber()- , SockAddr(..)- , HostName- , accept- , socketToHandle- )-import System.IO---- | alternative implementation of accept to work around EAI_AGAIN errors-acceptLite :: S.Socket -> IO (Handle, S.HostName, S.PortNumber)-acceptLite sock = do- (sock', addr) <- S.accept sock- h <- S.socketToHandle sock' ReadWriteMode- (N.PortNumber p) <- N.socketPort sock'- - let peer = $(if supportsIPv6- then- return $ CaseE (VarE (mkName "addr")) - [Match - (ConP (mkName "S.SockAddrInet") - [WildP,VarP (mkName "ha")]) - (NormalB (AppE (VarE (mkName "showHostAddress")) - (VarE (mkName "ha")))) []- ,Match (ConP (mkName "S.SockAddrInet6") [WildP,WildP,VarP (mkName "ha"),WildP])- (NormalB (AppE (VarE (mkName "showHostAddress6")) (VarE (mkName "ha")))) []- ,Match WildP (NormalB (AppE (VarE (mkName "error")) (LitE (StringL "Unsupported socket")))) []]- -- the above mess is the equivalent of this: - {-[| case addr of- (S.SockAddrInet _ ha) -> showHostAddress ha- (S.SockAddrInet6 _ _ ha _) -> showHostAddress6 ha- _ -> error "Unsupported socket"- |]-}- else- [| case addr of- (S.SockAddrInet _ ha) -> showHostAddress ha- _ -> error "Unsupported socket"- |])- - return (h, peer, p)-
− src/Happstack/Server/HTTP/SocketTH.hs
@@ -1,15 +0,0 @@-{-# LANGUAGE TemplateHaskell #-}-module Happstack.Server.HTTP.SocketTH(supportsIPv6) where-import Language.Haskell.TH--import Data.List-import Data.Maybe-import Network.Socket(SockAddr(..))---- find out at compile time if the SockAddr6 / HostAddress6 constructors are available-supportsIPv6 :: Bool-supportsIPv6 = $(let c = ["Network.Socket.SockAddrInet6", "Network.Socket.Internal.SockAddrInet6"] ; d = ''SockAddr in- do TyConI (DataD _ _ _ cs _) <- reify d- if isJust (find (\(NormalC n _) -> show n `elem` c) cs)- then [| True |]- else [| False |] )
− src/Happstack/Server/HTTP/Types.hs
@@ -1,260 +0,0 @@-{-# LANGUAGE TypeSynonymInstances, DeriveDataTypeable #-}--module Happstack.Server.HTTP.Types- (Request(..), Response(..), RqBody(..), Input(..), HeaderPair(..),- rqURL, mkHeaders,- getHeader, getHeaderBS, getHeaderUnsafe,- hasHeader, hasHeaderBS, hasHeaderUnsafe,- setHeader, setHeaderBS, setHeaderUnsafe,- addHeader, addHeaderBS, addHeaderUnsafe,- setRsCode, -- setCookie, setCookies,- Conf(..), nullConf, result, resultBS,- redirect, -- redirect_, redirect', redirect'_,- RsFlags(..), nullRsFlags, noContentLength,- Version(..), Method(..), Headers, continueHTTP,- Host, ContentType(..)- ) where---import qualified Data.Map as M-import Data.Typeable(Typeable)-import Data.Maybe-import qualified Data.ByteString.Char8 as P-import Data.ByteString.Char8 (ByteString,pack)-import qualified Data.ByteString.Lazy.Char8 as L-import Happstack.Server.SURI-import Data.Char (toLower)--import Happstack.Server.HTTP.Multipart ( ContentType(..) )-import Happstack.Server.Cookie-import Data.List-import Text.Show.Functions ()---- | HTTP version-data Version = Version Int Int- deriving(Read,Eq)--instance Show Version where- show (Version x y) = (show x) ++ "." ++ (show y)--isHTTP1_1 :: Request -> Bool-isHTTP1_1 rq = case rqVersion rq of Version 1 1 -> True; _ -> False-isHTTP1_0 :: Request -> Bool-isHTTP1_0 rq = case rqVersion rq of Version 1 0 -> True; _ -> False---- | Should the connection be used for further messages after this.--- | isHTTP1_0 && hasKeepAlive || isHTTP1_1 && hasNotConnectionClose-continueHTTP :: Request -> Response -> Bool---continueHTTP rq res = isHTTP1_1 rq && getHeader' connectionC rq /= Just closeC && rsfContentLength (rsFlags res)-continueHTTP rq res = (isHTTP1_0 rq && checkHeaderBS connectionC keepaliveC rq) ||- (isHTTP1_1 rq && not (checkHeaderBS connectionC closeC rq)) && rsfContentLength (rsFlags res)---- | HTTP configuration-data Conf = Conf { port :: Int -- ^ Port for the server to listen on.- , validator :: Maybe (Response -> IO Response)- } ---- | Default configuration contains no validator and the port is set to 8000-nullConf :: Conf-nullConf = Conf { port = 8000- , validator = Nothing- }---- | HTTP request method-data Method = GET | HEAD | POST | PUT | DELETE | TRACE | OPTIONS | CONNECT- deriving(Show,Read,Eq)--data HeaderPair = HeaderPair { hName :: ByteString, hValue :: [ByteString] } deriving (Read,Show)--- | Combined headers.-type Headers = M.Map ByteString HeaderPair -- lowercased name -> (realname, value)------ | Result flags-data RsFlags = RsFlags - { rsfContentLength :: Bool -- ^ whether a content-length header will be added to the result.- } deriving(Show,Read,Typeable)---- | Default RsFlags that will include the content-length header-nullRsFlags :: RsFlags-nullRsFlags = RsFlags { rsfContentLength = True }--- | Don't display a Content-Lenght field for the 'Result'.-noContentLength :: Response -> Response-noContentLength res = res { rsFlags = upd } where upd = (rsFlags res) { rsfContentLength = False }--data Input = Input- { inputValue :: L.ByteString- , inputFilename :: Maybe String- , inputContentType :: ContentType- } deriving (Show,Read,Typeable)--type Host = (String,Int)--data Response = Response { rsCode :: Int,- rsHeaders :: Headers,- rsFlags :: RsFlags,- rsBody :: L.ByteString,- rsValidator :: Maybe (Response -> IO Response)- }- | SendFile { rsCode :: Int,- rsHeaders :: Headers,- rsFlags :: RsFlags,- rsValidator :: Maybe (Response -> IO Response),- sfFilePath :: FilePath, -- file handle to send from- sfOffset :: Integer, -- offset to start at- sfCount :: Integer -- number of bytes to send- }- deriving (Show,Typeable) --data Request = Request { rqMethod :: Method,- rqPaths :: [String],- rqUri :: String,- rqQuery :: String,- rqInputs :: [(String,Input)],- rqCookies :: [(String,Cookie)],- rqVersion :: Version,- rqHeaders :: Headers,- rqBody :: RqBody,- rqPeer :: Host- } deriving(Show,Read,Typeable)---- | Converts a Request into a String representing the corresponding URL-rqURL :: Request -> String-rqURL rq = '/':intercalate "/" (rqPaths rq) ++ (rqQuery rq)--class HasHeaders a where - updateHeaders::(Headers->Headers)->a->a- headers::a->Headers--instance HasHeaders Response where updateHeaders f rs = rs{rsHeaders=f $ rsHeaders rs}- headers = rsHeaders-instance HasHeaders Request where updateHeaders f rq = rq{rqHeaders = f $ rqHeaders rq} - headers = rqHeaders--instance HasHeaders Headers where updateHeaders f = f- headers = id--newtype RqBody = Body L.ByteString deriving (Read,Show,Typeable)---- | Sets the Response status code to the provided Int and lifts the computation--- into a Monad.-setRsCode :: (Monad m) => Int -> Response -> m Response-setRsCode code rs = return rs {rsCode = code}---- | Takes a list of (key,val) pairs and converts it into Headers. The--- keys will be converted to lowercase-mkHeaders :: [(String,String)] -> Headers-mkHeaders hdrs- = M.fromListWith join [ (P.pack (map toLower key), HeaderPair (P.pack key) [P.pack value]) | (key,value) <- hdrs ]- where join (HeaderPair key vs1) (HeaderPair _ vs2) = HeaderPair key (vs1++vs2)------------------------------------------------------------------- Retrieving header information------------------------------------------------------------------- | Lookup header value. Key is case-insensitive.-getHeader :: HasHeaders r => String -> r -> Maybe ByteString-getHeader = getHeaderBS . pack---- | Lookup header value. Key is a case-insensitive bytestring.-getHeaderBS :: HasHeaders r => ByteString -> r -> Maybe ByteString-getHeaderBS = getHeaderUnsafe . P.map toLower---- | Lookup header value with a case-sensitive key. The key must be lowercase.-getHeaderUnsafe :: HasHeaders r => ByteString -> r -> Maybe ByteString-getHeaderUnsafe key var = listToMaybe =<< fmap hValue (getHeaderUnsafe' key var)---- | Lookup header with a case-sensitive key. The key must be lowercase.-getHeaderUnsafe' :: HasHeaders r => ByteString -> r -> Maybe HeaderPair-getHeaderUnsafe' key = M.lookup key . headers------------------------------------------------------------------- Querying header status------------------------------------------------------------------- | Returns True if the associated key is found in the Headers. The lookup--- is case insensitive.-hasHeader :: HasHeaders r => String -> r -> Bool-hasHeader key r = isJust (getHeader key r)---- | Acts as 'hasHeader' with ByteStrings-hasHeaderBS :: HasHeaders r => ByteString -> r -> Bool-hasHeaderBS key r = isJust (getHeaderBS key r)---- | Acts as 'hasHeaderBS' but the key is case sensitive. It should be--- in lowercase.-hasHeaderUnsafe :: HasHeaders r => ByteString -> r -> Bool-hasHeaderUnsafe key r = isJust (getHeaderUnsafe' key r)--checkHeaderBS :: HasHeaders r => ByteString -> ByteString -> r -> Bool-checkHeaderBS key val = checkHeaderUnsafe (P.map toLower key) (P.map toLower val)--checkHeaderUnsafe :: HasHeaders r => ByteString -> ByteString -> r -> Bool-checkHeaderUnsafe key val r- = case getHeaderUnsafe key r of- Just val' | P.map toLower val' == val -> True- _ -> False-------------------------------------------------------------------- Setting header status------------------------------------------------------------------- | Associates the key/value pair in the headers. Forces the key to be--- lowercase.-setHeader :: HasHeaders r => String -> String -> r -> r-setHeader key val = setHeaderBS (pack key) (pack val)---- | Acts as 'setHeader' but with ByteStrings.-setHeaderBS :: HasHeaders r => ByteString -> ByteString -> r -> r-setHeaderBS key val = setHeaderUnsafe (P.map toLower key) (HeaderPair key [val])---- | Sets the key to the HeaderPair. This is the only way to associate a key--- with multiple values via the setHeader* functions. Does not force the key--- to be in lowercase or guarantee that the given key and the key in the HeaderPair will match. -setHeaderUnsafe :: HasHeaders r => ByteString -> HeaderPair -> r -> r-setHeaderUnsafe key val = updateHeaders (M.insert key val)------------------------------------------------------------------- Adding headers------------------------------------------------------------------- | Add a key/value pair to the header. If the key already has a value--- associated with it, then the value will be appended. --- Forces the key to be lowercase.-addHeader :: HasHeaders r => String -> String -> r -> r-addHeader key val = addHeaderBS (pack key) (pack val)---- | Acts as addHeader except for ByteStrings-addHeaderBS :: HasHeaders r => ByteString -> ByteString -> r -> r-addHeaderBS key val = addHeaderUnsafe (P.map toLower key) (HeaderPair key [val])---- | Add a key/value pair to the header using the underlying HeaderPair data--- type. Does not force the key to be in lowercase or guarantee that the given key and the key in the HeaderPair will match. -addHeaderUnsafe :: HasHeaders r => ByteString -> HeaderPair -> r -> r-addHeaderUnsafe key val = updateHeaders (M.insertWith join key val)- where join (HeaderPair k vs1) (HeaderPair _ vs2) = HeaderPair k (vs1++vs2)---- | Creates a Response with the given Int as the status code and the provided--- String as the body of the Response -result :: Int -> String -> Response-result code = resultBS code . L.pack---- | Acts as 'result' but works with ByteStrings directly.-resultBS :: Int -> L.ByteString -> Response-resultBS code s = Response code M.empty nullRsFlags s Nothing---- | Sets the Response's status code to the given Int and redirects to the given URI-redirect :: (ToSURI s) => Int -> s -> Response -> Response-redirect c s resp = setHeaderBS locationC (pack (render (toSURI s))) resp{rsCode = c}---- constants here-locationC :: ByteString-locationC = P.pack "Location"-closeC :: ByteString-closeC = P.pack "close"-connectionC :: ByteString-connectionC = P.pack "Connection"-keepaliveC :: ByteString-keepaliveC = P.pack "Keep-Alive"-
+ src/Happstack/Server/Internal/Clock.hs view
@@ -0,0 +1,59 @@+{-# OPTIONS -fno-cse #-}+module Happstack.Server.Internal.Clock+ ( getApproximateTime+ , getApproximatePOSIXTime+ , getApproximateUTCTime+ , formatHttpDate+ ) where++import Control.Applicative ((<$>))+import Control.Concurrent+import Data.IORef+import Data.Time.Clock (UTCTime)+import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime, posixSecondsToUTCTime)+import Data.Time.Format (formatTime)+import System.IO.Unsafe+import System.Locale++import qualified Data.ByteString.Char8 as B++data DateCache = DateCache { + cachedPOSIXTime :: !(IORef POSIXTime)+ , cachedHttpDate :: !(IORef B.ByteString)+ }++formatHttpDate :: UTCTime -> String+formatHttpDate = formatTime defaultTimeLocale "%a, %d %b %Y %X GMT"+{-# INLINE formatHttpDate #-}++mkTime :: IO (POSIXTime, B.ByteString)+mkTime = + do now <- getPOSIXTime+ return (now, B.pack $ formatHttpDate (posixSecondsToUTCTime now))++{-# NOINLINE clock #-}+clock :: DateCache+clock = unsafePerformIO $ do+ (now, httpDate) <- mkTime+ nowRef <- newIORef now+ httpDateRef <- newIORef httpDate+ let dateCache = (DateCache nowRef httpDateRef)+ forkIO $ updater dateCache+ return dateCache++updater :: DateCache -> IO ()+updater dateCache = + do threadDelay (10^(6 :: Int)) -- Every second+ (now, httpDate) <- mkTime+ writeIORef (cachedPOSIXTime dateCache) now+ writeIORef (cachedHttpDate dateCache) httpDate+ updater dateCache++getApproximateTime :: IO B.ByteString+getApproximateTime = readIORef (cachedHttpDate clock)++getApproximatePOSIXTime :: IO POSIXTime+getApproximatePOSIXTime = readIORef (cachedPOSIXTime clock)++getApproximateUTCTime :: IO UTCTime+getApproximateUTCTime = posixSecondsToUTCTime <$> getApproximatePOSIXTime
+ src/Happstack/Server/Internal/Compression.hs view
@@ -0,0 +1,204 @@+{-# LANGUAGE NoMonomorphismRestriction, FlexibleContexts #-}+-- | Filter for compressing the 'Response' body.+module Happstack.Server.Internal.Compression+ ( compressedResponseFilter+ , compressWithFilter+ , gzipFilter+ , deflateFilter+ , encodings+ ) where+import Happstack.Server.SimpleHTTP+import Text.ParserCombinators.Parsec+import Control.Monad+import Data.Maybe+import Data.List+import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Lazy.Char8 as L+import qualified Codec.Compression.GZip as GZ+import qualified Codec.Compression.Zlib as Z++-- | reads the @Accept-Encoding@ header. Then, if possible+-- will compress the response body with methods @gzip@ or @deflate@.+--+-- > main = +-- > simpleHTTP nullConf $ +-- > do str <- compressedResponseFilter+-- > return $ toResponse ("This response compressed using: " ++ str)+compressedResponseFilter::+ (FilterMonad Response m, MonadPlus m, WebMonad Response m, ServerMonad m)+ => m String -- ^ name of the encoding chosen+compressedResponseFilter = do+ getHeaderM "Accept-Encoding" >>=+ (maybe (return "identity") installHandler)++ where+ badEncoding = "Encoding returned not in the list of known encodings"++ installHandler accept = do+ let eEncoding = bestEncoding allEncodings $ BS.unpack accept+ (coding,identityAllowed,action) <- case eEncoding of+ Left _ -> do+ setResponseCode 406+ finishWith $ toResponse ""++ Right encs@(a:_) -> return (a + , "identity" `elem` encs+ , fromMaybe (fail badEncoding)+ (lookup a allEncodingHandlers)+ )+ action coding identityAllowed+ return coding++-- | Ignore the @Accept-Encoding@ header in the 'Request' and attempt to compress the body of the response with @gzip@.+--+-- calls 'compressWithFilter' using 'GZ.compress'.+--+-- see also: 'compressedResponseFilter'+gzipFilter::(FilterMonad Response m) => + String -- ^ encoding to use for Content-Encoding header+ -> Bool -- ^ fallback to identity for SendFile+ -> m ()+gzipFilter = compressWithFilter GZ.compress++-- | Ignore the @Accept-Encoding@ header in the 'Request' and attempt compress the body of the response with zlib's+-- @deflate@ method+--+-- calls 'compressWithFilter' using 'Z.compress'.+--+-- see also: 'compressedResponseFilter'+deflateFilter::(FilterMonad Response m) => + String -- ^ encoding to use for Content-Encoding header+ -> Bool -- ^ fallback to identity for SendFile+ -> m ()+deflateFilter = compressWithFilter Z.compress++-- | Ignore the @Accept-Encoding@ header in the 'Request' and attempt to compress the body of the response using the supplied compressor.+--+-- We can not compress files being transfered using 'SendFile'. If+-- @identity@ is an allowed encoding, then just return the 'Response'+-- unmodified. Otherwise we return "406 Not Acceptable".+--+-- see also: 'gzipFilter' and 'defaultFilter'+compressWithFilter :: (FilterMonad Response m) =>+ (L.ByteString -> L.ByteString) -- ^ function to compress the body+ -> String -- ^ encoding to use for Content-Encoding header+ -> Bool -- ^ fallback to identity for SendFile+ -> m ()+compressWithFilter compressor encoding identityAllowed =+ composeFilter $ \r -> + case r of+ Response{} -> setHeader "Content-Encoding" encoding $ r {rsBody = compressor $ rsBody r}+ _ | identityAllowed -> r+ | otherwise -> (toResponse "") { rsCode = 406 }++-- | based on the rules describe in rfc2616 sec. 14.3+bestEncoding :: [String] -> String -> Either String [String]+bestEncoding availableEncodings encs = do+ encList<-either (Left . show) (Right) $ parse encodings "" encs+ case acceptable encList of+ [] -> Left "no encoding found"+ a -> Right $ a+ where+ -- first intersect with the list of encodings we know how to deal with at all+ knownEncodings:: [(String,Maybe Double)] -> [(String, Maybe Double)]+ knownEncodings m = intersectBy (\x y->fst x == fst y) m (map (\x -> (x,Nothing)) availableEncodings)+ -- this expands the wildcard, by figuring out if we need to include "identity" in the list+ -- Then it deletes the wildcard entry, drops all the "q=0" entries (which aren't allowed).+ --+ -- note this implementation is a little conservative. if someone were to specify "*"+ -- without a "q" value, it would be this server is willing to accept any format at all.+ -- We pretty much assume we can't send them /any/ format and that they really+ -- meant just "identity" this seems safe to me.+ knownEncodings':: [(String,Maybe Double)] -> [(String, Maybe Double)]+ knownEncodings' m = filter dropZero $ deleteBy (\(a,_) (b,_)->a==b) ("*",Nothing) $+ case lookup "*" (knownEncodings m) of+ Nothing -> addIdent $ knownEncodings m+ Just (Just a) | a>0 -> addIdent $ knownEncodings m+ | otherwise -> knownEncodings m+ Just (Nothing) -> addIdent $ knownEncodings m+ dropZero (_, Just a) | a==0 = False+ | otherwise = True+ dropZero (_, Nothing) = True+ addIdent:: [(String,Maybe Double)] -> [(String, Maybe Double)]+ addIdent m = if isNothing $ lookup "identity" m+ then m ++ [("identity",Nothing)]+ else m+ -- finally we sort the list of available encodings.+ acceptable:: [(String,Maybe Double)] -> [String]+ acceptable l = map fst $ sortBy (flip cmp) $ knownEncodings' l+ -- let the client choose but break ties with gzip+ encOrder = reverse $ zip (reverse availableEncodings) [1..]+ m0 = maybe (0.0::Double) id+ cmp (s,mI) (t,mJ) | m0 mI == m0 mJ+ = compare (m0 $ lookup s encOrder) (m0 $ lookup t encOrder)+ | otherwise = compare (m0 mI) (m0 mJ)+++allEncodingHandlers:: (FilterMonad Response m) => [(String, String -> Bool -> m ())]+allEncodingHandlers = zip allEncodings handlers++allEncodings :: [String]+allEncodings =+ ["gzip"+ ,"x-gzip"+-- ,"compress" -- as far as I can tell there is no haskell library that supports this+-- ,"x-compress" -- as far as I can tell, there is no haskell library that supports this+ ,"deflate"+ ,"identity"+ ,"*"+ ]++handlers::(FilterMonad Response m) => [String -> Bool -> m ()]+handlers =+ [gzipFilter+ ,gzipFilter+-- ,compressFilter+-- ,compressFilter+ ,deflateFilter+ , \encoding _ -> setHeaderM "Accept-Encoding" encoding+ ,const $ fail "chose * as content encoding"+ ]++-- | a parser for the Accept-Encoding header+encodings :: GenParser Char st [([Char], Maybe Double)]+encodings = ws >> (encoding1 `sepBy` try sep) >>= (\x -> ws >> eof >> return x)+ where+ ws :: GenParser Char st ()+ ws = many space >> return ()++ sep :: GenParser Char st ()+ sep = do+ ws+ _ <- char ','+ ws+ + encoding1 :: GenParser Char st ([Char], Maybe Double)+ encoding1 = do+ encoding <- many1 (alphaNum <|> char '-') <|> string "*"+ ws+ quality<-optionMaybe qual+ return (encoding, fmap read quality)++ qual :: GenParser Char st String+ qual = do+ char ';' >> ws >> char 'q' >> ws >> char '=' >> ws+ q<-float+ return q++ int :: GenParser Char st String+ int = many1 digit++ float :: GenParser Char st String+ float = do+ wholePart<-many1 digit+ fractionalPart<-option "" fraction+ return $ wholePart ++ fractionalPart+ <|>+ do+ fractionalPart<-fraction+ return fractionalPart+ fraction :: GenParser Char st String+ fraction = do+ _ <- char '.'+ fractionalPart<-option "" int+ return $ '.':fractionalPart
+ src/Happstack/Server/Internal/Cookie.hs view
@@ -0,0 +1,187 @@+{-# LANGUAGE DeriveDataTypeable #-}++-- http://tools.ietf.org/html/rfc2109+module Happstack.Server.Internal.Cookie+ ( Cookie(..)+ , CookieLife(..)+ , calcLife+ , mkCookie+ , mkCookieHeader+ , getCookies+ , getCookie+ , getCookies'+ , getCookie'+ , parseCookies+ , cookiesParser+ )+ where++import Control.Applicative ((<$>))+import qualified Data.ByteString.Char8 as C+import Data.Char (chr, toLower)+import Data.Data (Data, Typeable)+import Data.List ((\\), intersperse)+import Data.Time.Clock (UTCTime, addUTCTime, diffUTCTime)+import Data.Time.Clock.POSIX (posixSecondsToUTCTime)+import Data.Time.Format (formatTime)+import Happstack.Util.Common (Seconds)+import Happstack.Server.Internal.Clock (getApproximateUTCTime)+import Text.ParserCombinators.Parsec hiding (token)+import System.Locale (defaultTimeLocale)++-- | a type for HTTP cookies. Usually created using 'mkCookie'.+data Cookie = Cookie+ { cookieVersion :: String+ , cookiePath :: String+ , cookieDomain :: String+ , cookieName :: String+ , cookieValue :: String+ , secure :: Bool+ } deriving(Show,Eq,Read,Typeable,Data)++-- | Specify the lifetime of a cookie.+--+-- Note that we always set the max-age and expires headers because+-- internet explorer does not honor max-age. You can specific 'MaxAge'+-- or 'Expires' and the other will be calculated for you. Choose which+-- ever one makes your life easiest.+--+data CookieLife+ = Session -- ^ session cookie - expires when browser is closed+ | MaxAge Seconds -- ^ life time of cookie in seconds+ | Expires UTCTime -- ^ cookie expiration date+ | Expired -- ^ cookie already expired+ deriving (Eq, Ord, Read, Show, Typeable)++-- convert 'CookieLife' to the argument needed for calling 'mkCookieHeader'+calcLife :: CookieLife -> IO (Maybe (Seconds, UTCTime))+calcLife Session = return Nothing+calcLife (MaxAge s) =+ do now <- getApproximateUTCTime+ return (Just (s, addUTCTime (fromIntegral s) now))+calcLife (Expires expirationDate) =+ do now <- getApproximateUTCTime+ return $ Just (round $ expirationDate `diffUTCTime` now, expirationDate)+calcLife Expired =+ return $ Just (0, posixSecondsToUTCTime 0)+++-- | Creates a cookie with a default version of 1, empty domain, a path of "/", and secure == False+--+-- see also: 'addCookie'+mkCookie :: String -- ^ cookie name+ -> String -- ^ cookie value+ -> Cookie+mkCookie key val = Cookie "1" "/" "" key val False++-- | Set a Cookie in the Result.+-- The values are escaped as per RFC 2109, but some browsers may+-- have buggy support for cookies containing e.g. @\'\"\'@ or @\' \'@.+--+-- Also, it seems that chrome, safari, and other webkit browsers do+-- not like cookies which have double quotes around the domain and+-- reject/ignore the cookie. So, we no longer quote the domain.+--+-- internet explorer does not honor the max-age directive so we set+-- both max-age and expires.+--+-- See 'CookieLife' and 'calcLife' for a convenient way of calculating+-- the first argument to this function.+mkCookieHeader :: Maybe (Seconds, UTCTime) -> Cookie -> String+mkCookieHeader mLife cookie =+ let l = [("Domain=", cookieDomain cookie)+ ,("Max-Age=", maybe "" (show . max 0 . fst) mLife)+ ,("expires=", maybe "" (formatTime defaultTimeLocale "%a, %d-%b-%Y %X GMT" . snd) mLife)+ ,("Path=", cookiePath cookie)+ ,("Version=", s cookieVersion)]+ s f | f cookie == "" = ""+ s f = '\"' : concatMap e (f cookie) ++ "\""+ e c | fctl c || c == '"' = ['\\',c]+ | otherwise = [c]+ in concat $ intersperse ";" ((cookieName cookie++"="++s cookieValue):[ (k++v) | (k,v) <- l, "" /= v ] ++ if secure cookie then ["Secure"] else [])++fctl :: Char -> Bool+fctl ch = ch == chr 127 || ch <= chr 31++-- | Not an supported api. Takes a cookie header and returns+-- either a String error message or an array of parsed cookies+parseCookies :: String -> Either String [Cookie]+parseCookies str = either (Left . show) Right $ parse cookiesParser str str++-- | not a supported api. A parser for RFC 2109 cookies+cookiesParser :: GenParser Char st [Cookie]+cookiesParser = cookies+ where -- Parsers based on RFC 2109+ cookies = do+ ws+ ver<-option "" $ try (cookie_version >>= (\x -> cookieSep >> return x))+ cookieList<-(cookie_value ver) `sepBy1` try cookieSep+ ws+ eof+ return cookieList+ cookie_value ver = do+ name<-name_parser+ cookieEq+ val<-value+ path<-option "" $ try (cookieSep >> cookie_path)+ domain<-option "" $ try (cookieSep >> cookie_domain)+ return $ Cookie ver path domain (low name) val False+ cookie_version = cookie_special "$Version"+ cookie_path = cookie_special "$Path"+ cookie_domain = cookie_special "$Domain"+ cookie_special s = do+ string s+ cookieEq+ value+ cookieSep = ws >> oneOf ",;" >> ws+ cookieEq = ws >> char '=' >> ws+ ws = spaces+ value = word+ word = try (quoted_string) <|> incomp_token++ -- Parsers based on RFC 2068+ quoted_string = do+ char '"'+ r <-many (oneOf qdtext)+ char '"'+ return r++ -- Custom parsers, incompatible with RFC 2068, but more forgiving ;)+ incomp_token = many1 $ oneOf ((chars \\ ctl) \\ " \t\";")+ name_parser = many1 $ oneOf ((chars \\ ctl) \\ "= ;,")++ -- Primitives from RFC 2068+ ctl = map chr (127:[0..31])+ chars = map chr [0..127]+ octet = map chr [0..255]+ text = octet \\ ctl+ qdtext = text \\ "\""++-- | Get all cookies from the HTTP request. The cookies are ordered per RFC from+-- the most specific to the least specific. Multiple cookies with the same+-- name are allowed to exist.+getCookies :: Monad m => C.ByteString -> m [Cookie]+getCookies h = getCookies' h >>= either (fail. ("Cookie parsing failed!"++)) return++-- | Get the most specific cookie with the given name. Fails if there is no such+-- cookie or if the browser did not escape cookies in a proper fashion.+-- Browser support for escaping cookies properly is very diverse.+getCookie :: Monad m => String -> C.ByteString -> m Cookie+getCookie s h = getCookie' s h >>= either (const $ fail ("getCookie: " ++ show s)) return++getCookies' :: Monad m => C.ByteString -> m (Either String [Cookie])+getCookies' header | C.null header = return $ Right []+ | otherwise = return $ parseCookies (C.unpack header)++getCookie' :: Monad m => String -> C.ByteString -> m (Either String Cookie)+getCookie' s h = do+ cs <- getCookies' h+ return $ do -- Either+ cooks <- cs+ case filter (\x->(==) (low s) (cookieName x) ) cooks of+ [] -> fail "No cookie found"+ f -> return $ head f++low :: String -> String+low = map toLower+
+ src/Happstack/Server/Internal/Handler.hs view
@@ -0,0 +1,388 @@+{-# LANGUAGE ScopedTypeVariables, ScopedTypeVariables #-}++module Happstack.Server.Internal.Handler+ ( request+ , parseResponse + , putRequest+ ) where++import qualified Paths_happstack_server as Paths+import qualified Data.Version as DV+import Control.Concurrent (newMVar, newEmptyMVar, tryTakeMVar)+import Control.Exception.Extensible as E+import Control.Monad+import Data.List(elemIndex)+import Data.Char(toLower)+import Data.Maybe ( fromMaybe, fromJust, isJust, isNothing )+import Prelude hiding (last)+import qualified Data.ByteString.Char8 as P+import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Lazy.Char8 as L+import Data.ByteString.Lazy.Internal (ByteString(Chunk, Empty))+import qualified Data.ByteString.Lazy.Char8 as LC+import qualified Data.Map as M+import Data.Int (Int64)+import Happstack.Server.Internal.Cookie+import Happstack.Server.Internal.Clock+import Happstack.Server.Internal.Types+import Happstack.Server.Internal.Multipart+import Happstack.Server.Internal.RFC822Headers+import Happstack.Server.Internal.MessageWrap+-- import Happstack.Server.Internal.NoPush+import Happstack.Server.SURI(SURI(..),path,query)+import Happstack.Server.SURI.ParseURI+import qualified Happstack.Server.Internal.TimeoutManager as TM+import Happstack.Server.Internal.TimeoutSocket (sGetContents, sPutTickle, sendFileTickle)+import Network.Socket (Socket)+import Network.Socket.ByteString (sendAll)+import Numeric+import System.Directory (removeFile)+import System.IO+import System.IO.Error (isDoesNotExistError)++request :: TM.Handle -> Conf -> Socket -> Host -> (Request -> IO Response) -> IO ()+request thandle conf sock host handler = rloop thandle conf sock host handler =<< sGetContents thandle sock++required :: String -> Maybe a -> Either String a+required err Nothing = Left err+required _ (Just a) = Right a++rloop :: TM.Handle+ -> Conf+ -> Socket+ -> Host+ -> (Request -> IO Response)+ -> L.ByteString+ -> IO ()+rloop thandle conf sock host handler inputStr+ | L.null inputStr = return ()+ | otherwise+ = join $+ do let parseRequest+ = do+ (topStr, restStr) <- required "failed to separate request" $ splitAtEmptyLine inputStr+ (rql, headerStr) <- required "failed to separate headers/body" $ splitAtCRLF topStr+ let (m,u,v) = requestLine rql+ headers' <- parseHeaders "host" (L.unpack headerStr)+ let headers = mkHeaders headers'+ let contentLength = fromMaybe 0 $ fmap fst (P.readInt =<< getHeaderUnsafe contentlengthC headers)+ (body, nextRequest) <- case () of+ () | contentLength < 0 -> fail "negative content-length"+ | isJust $ getHeaderBS transferEncodingC headers ->+ return $ consumeChunks restStr+ | otherwise -> return (L.splitAt (fromIntegral contentLength) restStr)+ let cookies = [ (cookieName c, c) | cl <- fromMaybe [] (fmap getCookies (getHeader "Cookie" headers)), c <- cl ] -- Ugle+ return (m, u, cookies, v, headers, body, host, nextRequest)++ case parseRequest of+ Left err -> error $ "failed to parse HTTP request: " ++ err+ Right (m, u, cookies, v, headers, body, host, nextRequest)+ -> return $+ do bodyRef <- newMVar (Body body)+ bodyInputRef <- newEmptyMVar+ let req = Request m (pathEls (path u)) (path u) (query u)+ (queryInput u) bodyInputRef cookies v headers bodyRef host++ let ioseq act = act >>= \x -> x `seq` return x++ res <- ioseq (handler req) `E.catch` \(e::E.SomeException) -> return $ result 500 $ "Server error: " ++ show e++ case logAccess conf of+ Nothing -> return ()+ (Just logger) ->+ do time <- getApproximateUTCTime+ let host' = fst host+ user = "-"+ requestLn = unwords [show $ rqMethod req, rqUri req, show $ rqVersion req]+ responseCode = rsCode res+ size = maybe (-1) (read . B.unpack) (getHeader "Content-Length" res) -- -1 indicates unknown size+ referer = B.unpack $ fromMaybe (B.pack "") $ getHeader "Referer" req+ userAgent = B.unpack $ fromMaybe (B.pack "") $ getHeader "User-Agent" req+ logger host' user time requestLn responseCode size referer userAgent++ -- withNoPush sock $ putAugmentedResult thandle sock req res+ putAugmentedResult thandle sock req res+ -- clean up tmp files+ cleanupTempFiles req+ when (continueHTTP req res) $ rloop thandle conf sock host handler nextRequest++-- NOTE: if someone took the inputs and never put them back, then they are responsible for the cleanup+cleanupTempFiles :: Request -> IO ()+cleanupTempFiles req = + do mInputs <- tryTakeMVar (rqInputsBody req)+ case mInputs of+ Nothing -> return ()+ (Just inputs) -> mapM_ deleteTmpFile inputs+ where+ deleteTmpFile :: (String, Input) -> IO ()+ deleteTmpFile (_, input) =+ case inputValue input of+ (Left fp) -> E.catchJust (guard . isDoesNotExistError) (removeFile fp) (const $ return ())+ _ -> return ()++-- | Unserializes the bytestring into a response. If there is an+-- error it will return @Left msg@.+parseResponse :: L.ByteString -> Either String Response+parseResponse inputStr =+ do (topStr,restStr) <- required "failed to separate response" $+ splitAtEmptyLine inputStr+ (rsl,headerStr) <- required "failed to separate headers/body" $+ splitAtCRLF topStr+ let (_,code) = responseLine rsl+ headers' <- parseHeaders "host" (L.unpack headerStr)+ let headers = mkHeaders headers'+ let mbCL = fmap fst (B.readInt =<< getHeader "content-length" headers)+ (body,_) <-+ maybe (if (isNothing $ getHeader "transfer-encoding" headers)+ then return (restStr,L.pack "")+ else return $ consumeChunks restStr)+ (\cl->return (L.splitAt (fromIntegral cl) restStr))+ mbCL+ return $ Response {rsCode=code,rsHeaders=headers,rsBody=body,rsFlags=RsFlags ContentLength,rsValidator=Nothing}++-- http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html+-- note this does NOT handle extenions+consumeChunks::L.ByteString->(L.ByteString,L.ByteString)+consumeChunks str = let (parts,tr,rest) = consumeChunksImpl str in (L.concat . (++ [tr]) .map snd $ parts,rest)++consumeChunksImpl :: L.ByteString -> ([(Int64, L.ByteString)], L.ByteString, L.ByteString)+consumeChunksImpl str+ | L.null str = ([],L.empty,str)+ | chunkLen == 0 = let (last,rest') = L.splitAt lenLine1 str+ (tr',rest'') = getTrailer rest'+ in ([(0,last)],tr',rest'')+ | otherwise = ((chunkLen,part):crest,tr,rest2)+ where+ line1 = head $ lazylines str+ lenLine1 = (L.length line1) + 1 -- endchar+ chunkLen = (fst $ head $ readHex $ L.unpack line1)+ len = chunkLen + lenLine1 + 2+ (part,rest) = L.splitAt len str+ (crest,tr,rest2) = consumeChunksImpl rest+ getTrailer s = L.splitAt index s+ where index | crlfLC `L.isPrefixOf` s = 2+ | otherwise = let iscrlf = L.zipWith (\a b -> a == '\r' && b == '\n') s . L.tail $ s+ Just i = elemIndex True $ zipWith (&&) iscrlf (tail (tail iscrlf))+ in fromIntegral $ i+4++crlfLC :: L.ByteString+crlfLC = L.pack "\r\n"++-- Properly lazy version of 'lines' for lazy bytestrings+lazylines :: L.ByteString -> [L.ByteString]+lazylines s+ | L.null s = []+ | otherwise =+ let (l,s') = L.break ((==) '\n') s+ in l : if L.null s' then []+ else lazylines (L.tail s')++requestLine :: L.ByteString -> (Method, SURI, HttpVersion)+requestLine l = case P.words ((P.concat . L.toChunks) l) of+ [rq,uri,ver] -> (method rq, SURI $ parseURIRef uri, version ver)+ [rq,uri] -> (method rq, SURI $ parseURIRef uri,HttpVersion 0 9)+ x -> error $ "requestLine cannot handle input: " ++ (show x)++responseLine :: L.ByteString -> (B.ByteString, Int)+responseLine l = case B.words ((B.concat . L.toChunks) l) of+ (v:c:_) -> version v `seq` (v,fst (fromJust (B.readInt c)))+ x -> error $ "responseLine cannot handle input: " ++ (show x)+++method :: B.ByteString -> Method+method r = fj $ lookup r mtable+ where fj (Just x) = x+ fj Nothing = error "invalid request method"+ mtable = [(P.pack "GET", GET),+ (P.pack "HEAD", HEAD),+ (P.pack "POST", POST),+ (P.pack "PUT", PUT),+ (P.pack "DELETE", DELETE),+ (P.pack "TRACE", TRACE),+ (P.pack "OPTIONS", OPTIONS),+ (P.pack "CONNECT", CONNECT)]++-- Result side++staticHeaders :: Headers+staticHeaders =+ foldr (uncurry setHeaderBS) (mkHeaders [])+ [ (serverC, happsC) ]++-- FIXME: we should not be controlling the response headers in mysterious ways in this low level code+-- headers should be set by application code and the core http engine should be very lean.+putAugmentedResult :: TM.Handle -> Socket -> Request -> Response -> IO ()+putAugmentedResult thandle outp req res = do+ case res of+ -- standard bytestring response+ Response {} -> do+ let chunked = rsfLength (rsFlags res) == TransferEncodingChunked && isHTTP1_1 req+ sendTop (if chunked then Nothing else (Just (fromIntegral (L.length (rsBody res))))) chunked+ when (rqMethod req /= HEAD)+ (let body = if chunked+ then chunk (rsBody res)+ else rsBody res+ in sPutTickle thandle outp body)+ -- zero-copy sendfile response+ -- the handle *should* be closed by the garbage collector++ SendFile {} -> do+ let infp = sfFilePath res+ off = sfOffset res+ count = sfCount res+ sendTop (Just count) False+ TM.tickle thandle+ sendFileTickle thandle outp infp off count++ where ph (HeaderPair k vs) = map (\v -> P.concat [k, fsepC, v, crlfC]) vs+ sendTop cl chunked = do+ allHeaders <- augmentHeaders req res cl chunked+ sendAll outp $ B.concat $ concat+ [ (pversion $ rqVersion req) -- Print HTTP version+ , [responseMessage $ rsCode res] -- Print responseCode+ , concatMap ph (M.elems allHeaders) -- Print all headers+ , [crlfC]+ ]+ TM.tickle thandle+ chunk :: L.ByteString -> L.ByteString+ chunk Empty = LC.pack "0\r\n\r\n"+ chunk (Chunk c cs) = Chunk (B.pack $ showHex (B.length c) "\r\n") (Chunk c (Chunk (B.pack "\r\n") (chunk cs)))++augmentHeaders :: Request -> Response -> Maybe Integer -> Bool -> IO Headers+augmentHeaders req res mcl chunked = do+ -- TODO: Hoist static headers to the toplevel.+ raw <- getApproximateTime+ let stdHeaders = staticHeaders `M.union`+ M.fromList ( [ (dateCLower, HeaderPair dateC [raw])+ , (connectionCLower, HeaderPair connectionC [if continueHTTP req res then keepAliveC else closeC])+ ] ++ case rsfLength (rsFlags res) of+ NoContentLength -> []+ ContentLength | not (hasHeader "Content-Length" res) ->+ case mcl of+ (Just cl) -> [(contentlengthC, HeaderPair contentLengthC [P.pack (show cl)])]+ _ -> []+ | otherwise -> []+ TransferEncodingChunked+ -- we check 'chunked' because we might not use this mode if the client is http 1.0+ | chunked -> [(transferEncodingC, HeaderPair transferEncodingC [chunkedC])]+ | otherwise -> []++ )+ return (rsHeaders res `M.union` stdHeaders) -- 'union' prefers 'headers res' when duplicate keys are encountered.++-- | Serializes the request to the given handle+putRequest :: Handle -> Request -> IO ()+putRequest h rq = do+ let put = B.hPut h+ ph (HeaderPair k vs) = map (\v -> B.concat [k, fsepC, v, crlfC]) vs+ sp = [B.pack " "]+ mapM_ put $ concat+ [[B.pack $ show $ rqMethod rq],sp+ ,[B.pack $ rqURL rq],sp+ ,(pversion $ rqVersion rq), [crlfC]+ ,concatMap ph (M.elems $ rqHeaders rq)+ ,[crlfC]+ ]+ mBody <- takeRequestBody rq -- tryTakeMVar (rqBody rq)+ L.hPut h (maybe L.empty unBody mBody) -- FIXME: should this actually be an error if the body is null?+ hFlush h++-- HttpVersion++pversion :: HttpVersion -> [B.ByteString]+pversion (HttpVersion 1 1) = [http11]+pversion (HttpVersion 1 0) = [http10]+pversion (HttpVersion x y) = [P.pack "HTTP/", P.pack (show x), P.pack ".", P.pack (show y)]++version :: B.ByteString -> HttpVersion+version x | x == http09 = HttpVersion 0 9+ | x == http10 = HttpVersion 1 0+ | x == http11 = HttpVersion 1 1+ | otherwise = error "Invalid HTTP version"++http09 :: B.ByteString+http09 = P.pack "HTTP/0.9"+http10 :: B.ByteString+http10 = P.pack "HTTP/1.0"+http11 :: B.ByteString+http11 = P.pack "HTTP/1.1"++-- Constants++connectionC :: B.ByteString+connectionC = P.pack "Connection"+connectionCLower :: B.ByteString+connectionCLower = P.map toLower connectionC+closeC :: B.ByteString+closeC = P.pack "close"+keepAliveC :: B.ByteString+keepAliveC = P.pack "Keep-Alive"+crlfC :: B.ByteString+crlfC = P.pack "\r\n"+fsepC :: B.ByteString+fsepC = P.pack ": "+contentTypeC :: B.ByteString+contentTypeC = P.pack "Content-Type"+contentLengthC :: B.ByteString+contentLengthC = P.pack "Content-Length"+contentlengthC :: B.ByteString+contentlengthC = P.pack "content-length"+dateC :: B.ByteString+dateC = P.pack "Date"+dateCLower :: B.ByteString+dateCLower = P.map toLower dateC+serverC :: B.ByteString+serverC = P.pack "Server"+happsC :: B.ByteString+happsC = P.pack $ "Happstack/" ++ DV.showVersion Paths.version+textHtmlC :: B.ByteString+textHtmlC = P.pack "text/html; charset=utf-8"+transferEncodingC :: B.ByteString+transferEncodingC = P.pack "Transfer-Encoding"+chunkedC :: B.ByteString+chunkedC = P.pack "chunked"++-- Response code names++responseMessage :: (Num t) => t -> B.ByteString+responseMessage 100 = P.pack " 100 Continue\r\n"+responseMessage 101 = P.pack " 101 Switching Protocols\r\n"+responseMessage 200 = P.pack " 200 OK\r\n"+responseMessage 201 = P.pack " 201 Created\r\n"+responseMessage 202 = P.pack " 202 Accepted\r\n"+responseMessage 203 = P.pack " 203 Non-Authoritative Information\r\n"+responseMessage 204 = P.pack " 204 No Content\r\n"+responseMessage 205 = P.pack " 205 Reset Content\r\n"+responseMessage 206 = P.pack " 206 Partial Content\r\n"+responseMessage 300 = P.pack " 300 Multiple Choices\r\n"+responseMessage 301 = P.pack " 301 Moved Permanently\r\n"+responseMessage 302 = P.pack " 302 Found\r\n"+responseMessage 303 = P.pack " 303 See Other\r\n"+responseMessage 304 = P.pack " 304 Not Modified\r\n"+responseMessage 305 = P.pack " 305 Use Proxy\r\n"+responseMessage 307 = P.pack " 307 Temporary Redirect\r\n"+responseMessage 400 = P.pack " 400 Bad Request\r\n"+responseMessage 401 = P.pack " 401 Unauthorized\r\n"+responseMessage 402 = P.pack " 402 Payment Required\r\n"+responseMessage 403 = P.pack " 403 Forbidden\r\n"+responseMessage 404 = P.pack " 404 Not Found\r\n"+responseMessage 405 = P.pack " 405 Method Not Allowed\r\n"+responseMessage 406 = P.pack " 406 Not Acceptable\r\n"+responseMessage 407 = P.pack " 407 Proxy Authentication Required\r\n"+responseMessage 408 = P.pack " 408 Request Time-out\r\n"+responseMessage 409 = P.pack " 409 Conflict\r\n"+responseMessage 410 = P.pack " 410 Gone\r\n"+responseMessage 411 = P.pack " 411 Length Required\r\n"+responseMessage 412 = P.pack " 412 Precondition Failed\r\n"+responseMessage 413 = P.pack " 413 Request Entity Too Large\r\n"+responseMessage 414 = P.pack " 414 Request-URI Too Large\r\n"+responseMessage 415 = P.pack " 415 Unsupported Media Type\r\n"+responseMessage 416 = P.pack " 416 Requested range not satisfiable\r\n"+responseMessage 417 = P.pack " 417 Expectation Failed\r\n"+responseMessage 500 = P.pack " 500 Internal Server Error\r\n"+responseMessage 501 = P.pack " 501 Not Implemented\r\n"+responseMessage 502 = P.pack " 502 Bad Gateway\r\n"+responseMessage 503 = P.pack " 503 Service Unavailable\r\n"+responseMessage 504 = P.pack " 504 Gateway Time-out\r\n"+responseMessage 505 = P.pack " 505 HTTP Version not supported\r\n"+responseMessage x = P.pack (" " ++ show x ++ " \r\n")
+ src/Happstack/Server/Internal/LazyLiner.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE ForeignFunctionInterface #-}+module Happstack.Server.Internal.LazyLiner+ (Lazy, newLinerHandle, headerLines, getBytes, getBytesStrict, getRest, L.toChunks+ ) where++import Control.Concurrent.MVar+import System.IO+import qualified Data.ByteString.Char8 as P+import qualified Data.ByteString.Lazy.Char8 as L++newtype Lazy = Lazy (MVar L.ByteString)++newLinerHandle :: Handle -> IO Lazy+newLinerHandle h = fmap Lazy (newMVar =<< L.hGetContents h)++headerLines :: Lazy -> IO [P.ByteString]+headerLines (Lazy mv) = modifyMVar mv $ \l -> do+ let loop acc r0 = let (h,r) = L.break ((==) ch) r0+ ph = toStrict h+ phl = P.length ph+ ph2 = if phl == 0 || P.last ph /= '\x0D' then ph else P.init ph+ ch = '\x0A'+ r' = if L.null r then r else L.tail r+ in if P.length ph2 == 0 then (r', reverse acc) else loop (ph2:acc) r'+ return $ loop [] l++getBytesStrict :: Lazy -> Int -> IO P.ByteString+getBytesStrict (Lazy mv) len = modifyMVar mv $ \l -> do+ let (h,p) = L.splitAt (fromIntegral len) l+ return (p, toStrict h)++getBytes :: Lazy -> Int -> IO L.ByteString+getBytes (Lazy mv) len = modifyMVar mv $ \l -> do+ let (h,p) = L.splitAt (fromIntegral len) l+ return (p, h)++getRest :: Lazy -> IO L.ByteString+getRest (Lazy mv) = modifyMVar mv $ \l -> return (L.empty, l)++toStrict :: L.ByteString -> P.ByteString+toStrict = P.concat . L.toChunks
+ src/Happstack/Server/Internal/Listen.hs view
@@ -0,0 +1,103 @@+{-# LANGUAGE BangPatterns, CPP, ScopedTypeVariables #-}+module Happstack.Server.Internal.Listen(listen, listen',listenOn) where++import Happstack.Server.Internal.Types (Conf(..), Request, Response)+import Happstack.Server.Internal.Handler (request)+import Happstack.Server.Internal.Socket (acceptLite)+import Happstack.Server.Internal.TimeoutManager (cancel, initialize, register)+import Control.Exception.Extensible as E+import Control.Concurrent (forkIO, killThread, myThreadId)+import Control.Monad (forever, when)+import Network.BSD (getProtocolNumber)+import Network (sClose, Socket)+import Network.Socket as Socket (SocketOption(KeepAlive), setSocketOption, + socket, Family(..), SockAddr, + SocketOption(..), SockAddr(..), + iNADDR_ANY, maxListenQueue, SocketType(..), + bindSocket)+import qualified Network.Socket as Socket (listen)+import System.IO.Error (isFullError)+{-+#ifndef mingw32_HOST_OS+-}+import System.Posix.Signals+{-+#endif+-}+import System.Log.Logger (Priority(..), logM)+log':: Priority -> String -> IO ()+log' = logM "Happstack.Server.HTTP.Listen"+++{-+ Network.listenOn binds randomly to IPv4 or IPv6 or both,+ depending on system and local settings.+ Lets make it use IPv4 only for now.+-}++listenOn :: Int -> IO Socket+listenOn portm = do+ proto <- getProtocolNumber "tcp"+ E.bracketOnError+ (socket AF_INET Stream proto)+ (sClose)+ (\sock -> do+ setSocketOption sock ReuseAddr 1+ bindSocket sock (SockAddrInet (fromIntegral portm) iNADDR_ANY)+ Socket.listen sock (max 1024 maxListenQueue)+ return sock+ )+++-- | Bind and listen port+listen :: Conf -> (Request -> IO Response) -> IO ()+listen conf hand = do+ let port' = port conf+ socketm <- listenOn port'+ setSocketOption socketm KeepAlive 1+ listen' socketm conf hand++-- | Use a previously bind port and listen+listen' :: Socket -> Conf -> (Request -> IO Response) -> IO ()+listen' s conf hand = do+{-+#ifndef mingw32_HOST_OS+-}+ installHandler openEndedPipe Ignore Nothing+{-+#endif+-}+ let port' = port conf+ log' NOTICE ("Listening on port " ++ show port')+ tm <- initialize ((timeout conf) * (10^(6 :: Int)))+ let eh (x::SomeException) = when ((fromException x) /= Just ThreadKilled) $ log' ERROR ("HTTP request failed with: " ++ show x)+ work (sock, hn, p) = + do tid <- myThreadId+ thandle <- register tm (killThread tid)+ request thandle conf sock (hn,fromIntegral p) hand `E.catch` eh+ -- remove thread from timeout table+ cancel thandle+ sClose sock+ loop = forever $ do w <- acceptLite s+ forkIO $ work w+ pe e = log' ERROR ("ERROR in accept thread: " ++ show e)+ infi = loop `catchSome` pe >> infi+ infi `finally` (sClose s) -- >> killThread ttid)+{--+#ifndef mingw32_HOST_OS+-}+ installHandler openEndedPipe Ignore Nothing+ return ()+{-+#endif+-}+ where -- why are these handlers needed?++ catchSome op h = op `E.catches` [+ Handler $ \(e :: ArithException) -> h (toException e),+ Handler $ \(e :: ArrayException) -> h (toException e),+ Handler $ \(e :: IOException) ->+ if isFullError e+ then return () -- h (toException e) -- we could log the exception, but there could be thousands of them+ else throw e+ ]
+ src/Happstack/Server/Internal/LowLevel.hs view
@@ -0,0 +1,28 @@+module Happstack.Server.Internal.LowLevel+ (-- * HTTP Implementation+ -- $impl++ -- * Problems+ -- $problems++ -- * API+ module Happstack.Server.Internal.Handler,+ module Happstack.Server.Internal.Listen,+ module Happstack.Server.Internal.Types+ ) where++import Happstack.Server.Internal.Handler+import Happstack.Server.Internal.Listen+import Happstack.Server.Internal.Types++-- $impl+-- The Happstack HTTP implementation supports HTTP 1.0 and 1.1.+-- Multiple request on a connection including pipelining is supported.++-- $problems+-- Currently if a client sends an invalid HTTP request the whole+-- connection is aborted and no further processing is done.+--+-- When the connection times out Happstack closes it. In future it could+-- send a 408 response but this may be problematic if the sending+-- of a response caused the problem.
+ src/Happstack/Server/Internal/MessageWrap.hs view
@@ -0,0 +1,127 @@+{-# LANGUAGE FlexibleInstances #-}++module Happstack.Server.Internal.MessageWrap (+ module Happstack.Server.Internal.MessageWrap+ ,defaultInputIter+ ) where++import Control.Concurrent.MVar (tryTakeMVar, putMVar)+import Control.Monad.Trans (MonadIO(liftIO))+import qualified Data.ByteString.Char8 as P+import qualified Data.ByteString.Lazy.Char8 as L+import Data.Maybe+import Data.Int (Int64)+import Happstack.Server.Internal.Types as H+import Happstack.Server.Internal.Multipart+import Happstack.Server.Internal.RFC822Headers (parseContentType)+import Happstack.Server.SURI as SURI+import Happstack.Util.Common++queryInput :: SURI -> [(String, Input)]+queryInput uri = formDecode (case SURI.query uri of+ '?':r -> r+ xs -> xs)++-- | see 'defaultBodyPolicy'+data BodyPolicy + = BodyPolicy { inputWorker :: Int64 -> Int64 -> Int64 -> InputWorker+ , maxDisk :: Int64 -- ^ maximum bytes for files uploaded in this 'Request'+ , maxRAM :: Int64 -- ^ maximum bytes for all non-file values in the 'Request' body+ , maxHeader :: Int64 -- ^ maximum bytes of overhead for headers in @multipart/form-data@+ }++-- | create a 'BodyPolicy' for use with decodeBody+defaultBodyPolicy :: FilePath -- ^ temporary directory for file uploads+ -> Int64 -- ^ maximum bytes for files uploaded in this 'Request'+ -> Int64 -- ^ maximum bytes for all non-file values in the 'Request' body+ -> Int64 -- ^ maximum bytes of overhead for headers in @multipart/form-data@+ -> BodyPolicy+defaultBodyPolicy tmpDir md mr mh =+ BodyPolicy { inputWorker = defaultInputIter defaultFileSaver tmpDir 0 0 0+ , maxDisk = md+ , maxRAM = mr+ , maxHeader = mh+ }++bodyInput :: (MonadIO m) => BodyPolicy -> Request -> m ([(String, Input)], Maybe String)+bodyInput _ req | (rqMethod req /= POST) && (rqMethod req /= PUT) = return ([], Nothing)+bodyInput bodyPolicy req =+ liftIO $+ do let ctype = getHeader "content-type" req >>= parseContentType . P.unpack+ getBS (Body bs) = bs+ if (isDecodable ctype)+ then do + mbi <- tryTakeMVar (rqInputsBody req)+ case mbi of+ (Just bi) ->+ do putMVar (rqInputsBody req) bi+ return (bi, Nothing)+ Nothing ->+ do rqBody <- takeRequestBody req+ case rqBody of+ Nothing -> return ([], Just $ "bodyInput: Request body was already consumed.")+ (Just (Body bs)) -> + do r@(inputs, err) <- decodeBody bodyPolicy ctype bs+ putMVar (rqInputsBody req) inputs+ return r+ else return ([], Nothing)+ where+ isDecodable :: Maybe ContentType -> Bool+ isDecodable Nothing = True -- assume it is application/x-www-form-urlencoded+ isDecodable (Just (ContentType "application" "x-www-form-urlencoded" _)) = True+ isDecodable (Just (ContentType "multipart" "form-data" ps)) = True+ isDecodable (Just _) = False++-- | Decodes application\/x-www-form-urlencoded inputs. +-- TODO: should any of the [] be error conditions?+formDecode :: String -> [(String, Input)]+formDecode [] = []+formDecode qString = + if null pairString then rest else + (SURI.unEscape name,simpleInput $ SURI.unEscape val):rest+ where (pairString,qString')= split (=='&') qString+ (name,val)=split (=='=') pairString+ rest=if null qString' then [] else formDecode qString'++-- FIXME: no size limits on application/x-www-form-urlencoded yet+-- FIXME: is usend L.unpack really the right thing to do+decodeBody :: BodyPolicy+ -> Maybe ContentType+ -> L.ByteString+ -> IO ([(String,Input)], Maybe String)+decodeBody bp ctype inp+ = case ctype of+ Just (ContentType "application" "x-www-form-urlencoded" _) ->+ return (formDecode (L.unpack (L.take (maxRAM bp) inp)), Nothing)+ Just (ContentType "multipart" "form-data" ps) ->+ multipartDecode ((inputWorker bp) (maxDisk bp) (maxRAM bp) (maxHeader bp)) ps inp+ Just ct -> + return ([], Just $ "decodeBody: unsupported content-type: " ++ show ct) -- unknown content-type, the user will have to+ -- deal with it by looking at the raw content+ -- No content-type given, assume x-www-form-urlencoded+ Nothing -> return (formDecode (L.unpack (L.take (maxRAM bp) inp)), Nothing)++-- | Decodes multipart\/form-data input.+multipartDecode :: InputWorker+ -> [(String,String)] -- ^ Content-type parameters+ -> L.ByteString -- ^ Request body+ -> IO ([(String,Input)], Maybe String) -- ^ Input variables and values.+multipartDecode inputWorker ps inp =+ case lookup "boundary" ps of+ Just b -> multipartBody inputWorker (L.pack b) inp+ Nothing -> return ([], Just $ "boundary not found in parameters: " ++ show ps)++-- | Get the path components from a String.+pathEls :: String -> [String]+pathEls = (drop 1) . map SURI.unEscape . splitList '/' ++-- | Like 'Read' except Strings and Chars not quoted.+class (Read a)=>ReadString a where readString::String->a; readString =read ++instance ReadString Int +instance ReadString Double +instance ReadString Float +instance ReadString SURI.SURI where readString = read . show+instance ReadString [Char] where readString=id+instance ReadString Char where + readString s= if length t==1 then head t else read t where t=trim s
+ src/Happstack/Server/Internal/Monads.hs view
@@ -0,0 +1,503 @@+{-# LANGUAGE FlexibleContexts, FlexibleInstances, FunctionalDependencies, GeneralizedNewtypeDeriving, MultiParamTypeClasses, RankNTypes, UndecidableInstances #-}+{-| This module defines the Monad stack used by Happstack. You mostly don't want to be looking in here. Look in "Happstack.Server.Monads" instead.+-}+module Happstack.Server.Internal.Monads where++import Control.Applicative (Applicative, pure, (<*>), Alternative(empty,(<|>)))++import Control.Monad ( MonadPlus(mzero, mplus), ap, liftM, msum+ )+import Control.Monad.Trans ( MonadTrans, lift+ , MonadIO, liftIO+ )+import Control.Monad.Reader ( ReaderT(ReaderT), runReaderT+ , MonadReader, ask, local+ )+import Control.Monad.Writer ( WriterT(WriterT), runWriterT+ , MonadWriter, tell, pass+ , listens+ )+import qualified Control.Monad.Writer as Writer (listen) -- So that we can disambiguate 'Listen.listen'+import Control.Monad.State (MonadState, get, put)+import Control.Monad.Error ( ErrorT(ErrorT), runErrorT+ , Error, MonadError, throwError+ , catchError, mapErrorT+ )+import Control.Monad.Maybe (MaybeT(MaybeT), runMaybeT)+import qualified Data.ByteString.Lazy.UTF8 as LU (fromString)+import Data.Char (ord)+import Data.List (inits, isPrefixOf, stripPrefix, tails)+import Data.Monoid (Monoid(mempty, mappend), Dual(..), Endo(..))+import qualified Paths_happstack_server as Cabal+import qualified Data.Version as DV+import Debug.Trace (trace)+import Happstack.Server.Types (Request, Response, resultBS, setHeader)++-- | An alias for 'WebT' when using 'IO'.+type Web a = WebT IO a++-- | An alias for @'ServerPartT' 'IO'@+type ServerPart a = ServerPartT IO a++--------------------------------------+-- HERE BEGINS ServerPartT definitions++-- | 'ServerPartT' is a rich, featureful monad for web development. +--+-- see also: 'simpleHTTP', 'ServerMonad', 'FilterMonad', 'WebMonad', and 'HasRqData'+newtype ServerPartT m a = ServerPartT { unServerPartT :: ReaderT Request (WebT m) a }+ deriving (Monad, MonadPlus, Functor)++instance (MonadIO m) => MonadIO (ServerPartT m) where+ liftIO = ServerPartT . liftIO+ {-# INLINE liftIO #-}++-- | Particularly useful when combined with 'runWebT' to produce+-- a @m ('Maybe' 'Response')@ from a 'Request'.+runServerPartT :: ServerPartT m a -> Request -> WebT m a+runServerPartT = runReaderT . unServerPartT++-- | function for lifting WebT to ServerPartT+--+-- NOTE: This is mostly for internal use. If you want to access the+-- 'Request' in user-code see 'askRq' from 'ServerMonad'.+--+-- > do request <- askRq+-- > ...+withRequest :: (Request -> WebT m a) -> ServerPartT m a+withRequest = ServerPartT . ReaderT++-- | A constructor for a 'ServerPartT' when you don't care about the request.+--+-- NOTE: This is mostly for internal use. If you think you need to use+-- it in your own code, you might consider asking on the mailing list+-- or IRC to find out if there is an alternative solution.+anyRequest :: Monad m => WebT m a -> ServerPartT m a+anyRequest x = withRequest $ \_ -> x++-- | Apply a function to transform the inner monad of+-- @'ServerPartT' m@. +-- +-- Often used when transforming a monad with 'ServerPartT', since+-- 'simpleHTTP' requires a @'ServerPartT' 'IO' a@. Refer to 'UnWebT'+-- for an explanation of the structure of the monad.+--+-- Here is an example. Suppose you want to embed an 'ErrorT' into your+-- 'ServerPartT' to enable 'throwError' and 'catchError' in your 'Monad'.+--+-- > type MyServerPartT e m a = ServerPartT (ErrorT e m) a+--+-- Now suppose you want to pass @MyServerPartT@ into a function that+-- demands a @'ServerPartT' 'IO' a@ (e.g. 'simpleHTTP'). You can+-- provide the function:+--+-- > unpackErrorT :: (Monad m, Show e) => UnWebT (ErrorT e m) a -> UnWebT m a+-- > unpackErrorT et = do+-- > eitherV <- runErrorT et+-- > return $ case eitherV of+-- > Left err -> Just (Left $ toResponse $ +-- > "Catastrophic failure " ++ show err+-- > , filterFun $ \r -> r{rsCode = 500})+-- > Right x -> x+--+-- With @unpackErrorT@ you can now call 'simpleHTTP'. Just wrap your+-- 'ServerPartT' list.+--+-- > simpleHTTP nullConf $ mapServerPartT unpackErrorT (myPart `catchError` myHandler)+--+-- Or alternatively:+--+-- > simpleHTTP' unpackErrorT nullConf (myPart `catchError` myHandler)+--+-- Also see 'Happstack.Server.Error.spUnwrapErrorT' for a more sophisticated version of this+-- function.+--+mapServerPartT :: ( UnWebT m a -> UnWebT n b)+ -> (ServerPartT m a -> ServerPartT n b)+mapServerPartT f ma = withRequest $ \rq -> mapWebT f (runServerPartT ma rq)++-- | A variant of 'mapServerPartT' where the first argument also takes+-- a 'Request'. Useful if you want to 'runServerPartT' on a different+-- 'ServerPartT' inside your monad (see 'spUnwrapErrorT').+mapServerPartT' :: (Request -> UnWebT m a -> UnWebT n b)+ -> ( ServerPartT m a -> ServerPartT n b)+mapServerPartT' f ma = withRequest $ \rq -> mapWebT (f rq) (runServerPartT ma rq)++instance MonadTrans (ServerPartT) where+ lift m = withRequest (\_ -> lift m)++instance (Monad m) => Monoid (ServerPartT m a) where+ mempty = mzero+ mappend = mplus++instance (Monad m, Functor m) => Applicative (ServerPartT m) where+ pure = return+ (<*>) = ap++instance (Functor m, MonadPlus m) => Alternative (ServerPartT m) where+ empty = mzero+ (<|>) = mplus++instance (Monad m, MonadWriter w m) => MonadWriter w (ServerPartT m) where+ tell = lift . tell+ listen m = withRequest $ \rq -> Writer.listen (runServerPartT m rq) >>= return+ pass m = withRequest $ \rq -> pass (runServerPartT m rq) >>= return++instance (Monad m, MonadError e m) => MonadError e (ServerPartT m) where+ throwError e = lift $ throwError e+ catchError action handler = withRequest $ \rq -> (runServerPartT action rq) `catchError` ((flip runServerPartT $ rq) . handler)++instance (Monad m, MonadReader r m) => MonadReader r (ServerPartT m) where+ ask = lift ask+ local fn m = withRequest $ \rq-> local fn (runServerPartT m rq)++instance (Monad m, MonadState s m) => MonadState s (ServerPartT m) where+ get = lift get+ put = lift . put++instance Monad m => FilterMonad Response (ServerPartT m) where+ setFilter = anyRequest . setFilter+ composeFilter = anyRequest . composeFilter+ getFilter m = withRequest $ \rq -> getFilter (runServerPartT m rq)++instance Monad m => WebMonad Response (ServerPartT m) where+ finishWith r = anyRequest $ finishWith r++-- | The 'ServerMonad' class provides methods for reading or locally+-- modifying the 'Request'. It is essentially a specialized version of+-- the 'MonadReader' class. Providing the unique names, 'askRq' and+-- 'localRq' makes it easier to use 'ServerPartT' and 'ReaderT'+-- together.+class Monad m => ServerMonad m where+ askRq :: m Request+ localRq :: (Request -> Request) -> m a -> m a++instance (Monad m) => ServerMonad (ServerPartT m) where+ askRq = ServerPartT $ ask+ localRq f m = ServerPartT $ local f (unServerPartT m)++instance (Error e, ServerMonad m) => ServerMonad (ErrorT e m) where+ askRq = lift askRq+ localRq f = mapErrorT $ localRq f++-------------------------------+-- HERE BEGINS WebT definitions++-- | A monoid operation container. If @a@ is a monoid, then+-- 'SetAppend' is a monoid with the following behaviors:+--+-- > Set x `mappend` Append y = Set (x `mappend` y)+-- > Append x `mappend` Append y = Append (x `mappend` y)+-- > _ `mappend` Set y = Set y+--+-- A simple way of summarizing this is, if the right side is 'Append',+-- then the right is appended to the left. If the right side is+-- 'Set', then the left side is ignored.++data SetAppend a = Set a | Append a+ deriving (Eq, Show)++instance Monoid a => Monoid (SetAppend a) where+ mempty = Append mempty++ Set x `mappend` Append y = Set (x `mappend` y)+ Append x `mappend` Append y = Append (x `mappend` y)+ _ `mappend` Set y = Set y++-- | Extract the value from a 'SetAppend'.+-- Note that a 'SetAppend' is actually a @CoPointed@ from:+-- <http://hackage.haskell.org/packages/archive/category-extras/latest/doc/html/Control-Functor-Pointed.html>+-- But lets not drag in that dependency. yet...+extract :: SetAppend t -> t+extract (Set x) = x+extract (Append x) = x++instance Functor (SetAppend) where+ fmap f (Set x) = Set $ f x+ fmap f (Append x) = Append $ f x++-- | 'FilterFun' is a lot more fun to type than @'SetAppend' ('Dual'+-- ('Endo' a))@.+type FilterFun a = SetAppend (Dual (Endo a))++unFilterFun :: FilterFun a -> (a -> a)+unFilterFun = appEndo . getDual . extract++-- | turn a function into a 'FilterFun'. Primarily used with 'mapServerPartT'+filterFun :: (a -> a) -> FilterFun a+filterFun = Set . Dual . Endo++newtype FilterT a m b = FilterT { unFilterT :: WriterT (FilterFun a) m b }+ deriving (Monad, MonadTrans, Functor)++instance (MonadIO m) => MonadIO (FilterT a m) where+ liftIO = FilterT . liftIO+ {-# INLINE liftIO #-}++-- | A set of functions for manipulating filters. +--+-- 'ServerPartT' implements 'FilterMonad' 'Response' so these methods+-- are the fundamental ways of manipulating 'Response' values.+class Monad m => FilterMonad a m | m->a where+ -- | Ignores all previous alterations to your filter+ --+ -- As an example:+ --+ -- > do+ -- > composeFilter f+ -- > setFilter g+ -- > return "Hello World"+ --+ -- The @'setFilter' g@ will cause the first @'composeFilter' f@ to+ -- be ignored.+ setFilter :: (a->a) -> m ()+ -- | Composes your filter function with the existing filter+ -- function.+ composeFilter :: (a->a) -> m ()+ -- | Retrieves the filter from the environment.+ getFilter :: m b -> m (b, a->a)++-- | Resets all your filters. An alias for @'setFilter' 'id'@.+ignoreFilters :: (FilterMonad a m) => m ()+ignoreFilters = setFilter id++instance (Monad m) => FilterMonad a (FilterT a m) where+ setFilter = FilterT . tell . Set . Dual . Endo+ composeFilter = FilterT . tell . Append . Dual . Endo+ getFilter = FilterT . listens unFilterFun . unFilterT++-- | The basic 'Response' building object.+newtype WebT m a = WebT { unWebT :: ErrorT Response (FilterT (Response) (MaybeT m)) a }+ deriving (Functor)++instance (MonadIO m) => MonadIO (WebT m) where+ liftIO = WebT . liftIO+ {-# INLINE liftIO #-}++-- | 'UnWebT' is almost exclusively used with 'mapServerPartT'. If you+-- are not using 'mapServerPartT' then you do not need to wrap your+-- head around this type. If you are -- the type is not as complex as+-- it first appears.+-- +-- It is worth discussing the unpacked structure of 'WebT' a bit as+-- it's exposed in 'mapServerPartT' and 'mapWebT'.+--+-- A fully unpacked 'WebT' has a structure that looks like:+--+-- > ununWebT $ WebT m a :: m (Maybe (Either Response a, FilterFun Response))+--+-- So, ignoring @m@, as it is just the containing 'Monad', the+-- outermost layer is a 'Maybe'. This is 'Nothing' if 'mzero' was+-- called or @'Just' ('Either' 'Response' a, 'SetAppend' ('Endo'+-- 'Response'))@ if 'mzero' wasn't called. Inside the 'Maybe', there+-- is a pair. The second element of the pair is our filter function+-- @'FilterFun' 'Response'@. @'FilterFun' 'Response'@ is a type+-- alias for @'SetAppend' ('Dual' ('Endo' 'Response'))@. This is+-- just a wrapper for a @'Response' -> 'Response'@ function with a+-- particular 'Monoid' behavior. The value+--+-- > Append (Dual (Endo f))+--+-- Causes @f@ to be composed with the previous filter.+--+-- > Set (Dual (Endo f))+--+-- Causes @f@ to not be composed with the previous filter.+--+-- Finally, the first element of the pair is either @'Left'+-- 'Response'@ or @'Right' a@.+--+-- Another way of looking at all these pieces is from the behaviors+-- they control. The 'Maybe' controls the 'mzero' behavior. @'Set'+-- ('Endo' f)@ comes from the 'setFilter' behavior. Likewise,+-- @'Append' ('Endo' f)@ is from 'composeFilter'. @'Left'+-- 'Response'@ is what you get when you call 'finishWith' and+-- @'Right' a@ is the normal exit.+--+-- An example case statement looks like:+--+-- > ex1 webt = do+-- > val <- ununWebT webt+-- > case val of+-- > Nothing -> Nothing -- this is the interior value when mzero was used+-- > Just (Left r, f) -> Just (Left r, f) -- r is the value that was passed into "finishWith"+-- > -- f is our filter function+-- > Just (Right a, f) -> Just (Right a, f) -- a is our normal monadic value+-- > -- f is still our filter function+--+type UnWebT m a = m (Maybe (Either Response a, FilterFun Response))++instance Monad m => Monad (WebT m) where+ m >>= f = WebT $ unWebT m >>= unWebT . f+ {-# INLINE (>>=) #-}+ return a = WebT $ return a+ {-# INLINE return #-}+ fail s = outputTraceMessage s (mkFailMessage s)++-- | 'WebMonad' provides a means to end the current computation+-- and return a 'Response' immediately. This provides an+-- alternate escape route. In particular it has a monadic value+-- of any type. And unless you call @'setFilter' 'id'@ first your+-- response filters will be applied normally.+--+-- Extremely useful when you're deep inside a monad and decide+-- that you want to return a completely different content type,+-- since it doesn't force you to convert all your return types to+-- 'Response' early just to accommodate this.+--+-- see also: 'escape' and 'escape''+class Monad m => WebMonad a m | m->a where+ -- abort the current computation and return a value+ finishWith :: a -- ^ value to return (For 'ServerPart', 'a' will always be the type 'Response')+ -> m b ++-- | Used to ignore all your filters and immediately end the+-- computation. A combination of 'ignoreFilters' and 'finishWith'.+escape :: (WebMonad a m, FilterMonad a m) => m a -> m b+escape gen = ignoreFilters >> gen >>= finishWith++-- | An alternate form of 'escape' that can be easily used within a do+-- block.+escape' :: (WebMonad a m, FilterMonad a m) => a -> m b+escape' a = ignoreFilters >> finishWith a+++instance (Monad m) => WebMonad Response (WebT m) where+ finishWith r = WebT $ throwError r++instance MonadTrans WebT where+ lift = WebT . lift . lift . lift++instance (Monad m) => MonadPlus (WebT m) where+ -- | Aborts a computation.+ --+ -- This is primarily useful because 'msum' will take an array of+ -- 'MonadPlus' and return the first one that isn't 'mzero', which+ -- is exactly the semantics expected from objects that take lists+ -- of 'ServerPartT'.+ mzero = WebT $ lift $ lift $ mzero+ mplus x y = WebT $ ErrorT $ FilterT $ (lower x) `mplus` (lower y)+ where lower = (unFilterT . runErrorT . unWebT)++instance (Monad m) => FilterMonad Response (WebT m) where+ setFilter f = WebT $ lift $ setFilter $ f+ composeFilter f = WebT . lift . composeFilter $ f+ getFilter m = WebT $ ErrorT $ liftM lft $ getFilter (runErrorT $ unWebT m)+ where+ lft (Left r, _) = Left r+ lft (Right a, f) = Right (a, f)++instance (Monad m) => Monoid (WebT m a) where+ mempty = mzero+ mappend = mplus++-- | For when you really need to unpack a 'WebT' entirely (and not+-- just unwrap the first layer with 'unWebT').+ununWebT :: WebT m a -> UnWebT m a+ununWebT = runMaybeT . runWriterT . unFilterT . runErrorT . unWebT++-- | For wrapping a 'WebT' back up. @'mkWebT' . 'ununWebT' = 'id'@+mkWebT :: UnWebT m a -> WebT m a+mkWebT = WebT . ErrorT . FilterT . WriterT . MaybeT++-- | See 'mapServerPartT' for a discussion of this function.+mapWebT :: (UnWebT m a -> UnWebT n b)+ -> ( WebT m a -> WebT n b)+mapWebT f ma = mkWebT $ f (ununWebT ma)++-- | This is kinda like a very oddly shaped 'mapServerPartT' or 'mapWebT'.+-- You probably want one or the other of those.+localContext :: Monad m => (WebT m a -> WebT m' a) -> ServerPartT m a -> ServerPartT m' a+localContext fn hs+ = withRequest $ \rq -> fn (runServerPartT hs rq)++instance (Monad m, Functor m) => Applicative (WebT m) where+ pure = return+ (<*>) = ap++instance (Functor m, MonadPlus m) => Alternative (WebT m) where+ empty = mzero+ (<|>) = mplus++instance MonadReader r m => MonadReader r (WebT m) where+ ask = lift ask+ local fn m = mkWebT $ local fn (ununWebT m)++instance MonadState st m => MonadState st (WebT m) where+ get = lift get+ put = lift . put++instance MonadError e m => MonadError e (WebT m) where+ throwError err = lift $ throwError err+ catchError action handler = mkWebT $ catchError (ununWebT action) (ununWebT . handler)++instance MonadWriter w m => MonadWriter w (WebT m) where+ tell = lift . tell+ listen m = mkWebT $ Writer.listen (ununWebT m) >>= (return . liftWebT)+ where liftWebT (Nothing, _) = Nothing+ liftWebT (Just (Left x,f), _) = Just (Left x,f)+ liftWebT (Just (Right x,f),w) = Just (Right (x,w),f)+ pass m = mkWebT $ ununWebT m >>= liftWebT+ where liftWebT Nothing = return Nothing+ liftWebT (Just (Left x,f)) = return $ Just (Left x, f)+ liftWebT (Just (Right x,f)) = pass (return x)>>= (\a -> return $ Just (Right a,f))++-- | Deprecated: use 'msum'.+multi :: Monad m => [ServerPartT m a] -> ServerPartT m a+multi = msum+{-# DEPRECATED multi "Use msum instead" #-}++-- | What is this for, exactly? I don't understand why @Show a@ is+-- even in the context Deprecated: This function appears to do nothing+-- at all. If it use it, let us know why.+debugFilter :: (MonadIO m, Show a) => ServerPartT m a -> ServerPartT m a+debugFilter handle =+ withRequest $ \rq -> do+ r <- runServerPartT handle rq+ return r+{-# DEPRECATED debugFilter "This function appears to do nothing." #-}+++-- "Pattern match failure in do expression at src\AppControl.hs:43:24"+-- is converted to:+-- "src\AppControl.hs:43:24: Pattern match failure in do expression"+-- Then we output this to stderr. Help debugging under Emacs console when using GHCi.+-- This is GHC specific, but you may add your favourite compiler here also.+outputTraceMessage :: String -> a -> a+outputTraceMessage s c | "Pattern match failure " `isPrefixOf` s = + let w = [(k,p) | (i,p) <- zip (tails s) (inits s), Just k <- [stripPrefix " at " i]]+ v = concatMap (\(k,p) -> k ++ ": " ++ p) w+ in trace v c+outputTraceMessage s c = trace s c+++mkFailMessage :: (FilterMonad Response m, WebMonad Response m) => String -> m b+mkFailMessage s = do+ ignoreFilters+ let res = setHeader "Content-Type" "text/html; charset=UTF-8" $+ resultBS 500 (LU.fromString (failHtml s))+ finishWith $ res++failHtml:: String->String+failHtml errString = + "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">"+ ++ "<html><head><title>Happstack "+ ++ ver ++ " Internal Server Error</title></head>"+ ++ "<body><h1>Happstack " ++ ver ++ "</h1>"+ ++ "<p>Something went wrong here<br>"+ ++ "Internal server error<br>"+ ++ "Everything has stopped</p>"+ ++ "<p>The error was \"" ++ (escapeString errString) ++ "\"</p></body></html>"+ where ver = DV.showVersion Cabal.version++escapeString :: String -> String+escapeString str = concatMap encodeEntity str+ where+ encodeEntity :: Char -> String+ encodeEntity '<' = "<"+ encodeEntity '>' = ">"+ encodeEntity '&' = "&"+ encodeEntity '"' = """+ encodeEntity c+ | ord c > 127 = "&#" ++ show (ord c) ++ ";"+ | otherwise = [c]
+ src/Happstack/Server/Internal/Multipart.hs view
@@ -0,0 +1,257 @@+module Happstack.Server.Internal.Multipart where++import Control.Monad (MonadPlus(mplus), foldM)+import qualified Data.ByteString.Lazy.Char8 as L+import qualified Data.ByteString.Internal as B+import Data.ByteString.Lazy.Internal (ByteString(Chunk, Empty))+import Data.ByteString.Lazy.Internal as L+import qualified Data.ByteString.Lazy.UTF8 as LU+import qualified Data.ByteString.Char8 as S+import Data.List (intercalate)+import Data.Maybe (fromMaybe)+import Data.Int (Int64)+import Text.ParserCombinators.Parsec (ParseError, parse)+import Happstack.Server.Internal.Types (Input(..))+import Happstack.Server.Internal.RFC822Headers ( ContentType(..), ContentDisposition(..), Header+ , getContentDisposition, getContentType, pHeaders)+import System.IO (Handle, hClose, openBinaryTempFile)++-- | similar to the normal 'span' function, except the predicate gets the whole rest of the lazy bytestring, not just one character.+--+-- TODO: this function has not been profiled.+spanS :: (L.ByteString -> Bool) -> L.ByteString -> (L.ByteString, L.ByteString)+spanS f cs0 = spanS' 0 cs0+ where spanS' n Empty = (Empty, Empty)+ spanS' n bs@(Chunk c cs)+ | n >= S.length c = + let (x, y) = spanS' 0 cs+ in (Chunk c x, y)+ | not (f (Chunk (S.drop n c) cs)) = L.splitAt (fromIntegral n) bs+ | otherwise = (spanS' (n + 1) bs)+{-# INLINE spanS #-}++takeWhileS :: (L.ByteString -> Bool) -> L.ByteString -> L.ByteString+takeWhileS f cs0 = takeWhile' 0 cs0+ where takeWhile' n Empty = Empty+ takeWhile' n bs@(Chunk c cs)+ | n >= S.length c = Chunk c (takeWhile' 0 cs)+ | not (f (Chunk (S.drop n c) cs)) = (Chunk (S.take n c) Empty)+ | otherwise = takeWhile' (n + 1) bs++crlf :: L.ByteString+crlf = L.pack "\r\n"++crlfcrlf :: L.ByteString+crlfcrlf = L.pack "\r\n\r\n"++blankLine :: L.ByteString+blankLine = L.pack "\r\n\r\n"++dropWhileS :: (L.ByteString -> Bool) -> L.ByteString -> L.ByteString+dropWhileS f cs0 = dropWhile' cs0+ where dropWhile' bs + | L.null bs = bs+ | f bs = dropWhile' (L.drop 1 bs)+ | otherwise = bs++data BodyPart = BodyPart L.ByteString L.ByteString -- ^ headers body+ deriving (Eq, Ord, Read, Show)++data Work + = BodyWork ContentType [(String, String)] L.ByteString+ | HeaderWork L.ByteString ++type InputWorker = Work -> IO InputIter++data InputIter + = Failed (Maybe (String, Input)) String+ | BodyResult (String, Input) InputWorker+ | HeaderResult [Header] InputWorker++type FileSaver = FilePath -- ^ tempdir+ -> Int64 -- ^ quota+ -> FilePath -- ^ filename of field+ -> L.ByteString -- ^ content to save+ -> IO (Bool, Int64 , FilePath) -- ^ truncated?, saved bytes, saved filename++defaultFileSaver tmpDir diskQuota filename b =+ do (fn, h) <- openBinaryTempFile tmpDir filename+ (trunc, len) <- hPutLimit diskQuota h b+ hClose h+ return (trunc, len, fn) ++defaultInputIter :: FileSaver -> FilePath -> Int64 -> Int64 -> Int64 -> Int64 -> Int64 -> Int64 -> Work -> IO InputIter+defaultInputIter fileSaver tmpDir diskCount ramCount headerCount maxDisk maxRAM maxHeader (BodyWork ctype ps b)+ | diskCount > maxDisk = return $ Failed Nothing ("diskCount (" ++ show diskCount ++ ") is greater than maxDisk (" ++ show maxDisk ++ ")")+ | ramCount > maxRAM = return $ Failed Nothing ("ramCount (" ++ show ramCount ++ ") is greater than maxRAM (" ++ show maxRAM ++ ")")+ | otherwise =+ case lookup "filename" ps of+ Nothing -> + let (b',rest) = L.splitAt (maxRAM - ramCount) b+ input = (fromMaybe "" $ lookup "name" ps+ , Input { inputValue = (Right b')+ , inputFilename = Nothing+ , inputContentType = ctype })+ in if L.null rest+ then return $ BodyResult input (defaultInputIter fileSaver tmpDir diskCount (ramCount + L.length b) headerCount maxDisk maxRAM maxHeader)+ else return $ Failed (Just input) ("Reached RAM quota of " ++ show maxRAM ++ " bytes.")++ (Just filename) ->+ do (trunc, len, fn) <- fileSaver tmpDir (maxDisk - diskCount) filename b+ let input = ( fromMaybe "" $ lookup "name" ps+ , Input { inputValue = Left fn+ , inputFilename = (Just filename)+ , inputContentType = ctype })+ if trunc+ then return $ Failed (Just input) ("Reached disk quota of " ++ show maxDisk ++ " bytes.")+ else return $ BodyResult input (defaultInputIter fileSaver tmpDir (diskCount + len) ramCount headerCount maxDisk maxRAM maxHeader)++defaultInputIter fileSaver tmpDir diskCount ramCount headerCount maxDisk maxRAM maxHeader (HeaderWork bs) =+ case L.splitAt (maxHeader - headerCount) bs of+ (_hs, rest)+ | not (L.null rest) -> return $ Failed Nothing ("Reached header quota of " ++ show maxHeader ++ " bytes.")+ | otherwise ->+ case parse pHeaders (LU.toString bs) (LU.toString bs) of+ (Left e) -> return $ Failed Nothing (show e)+ (Right hs) ->+ return $ HeaderResult hs+ (defaultInputIter fileSaver tmpDir diskCount ramCount (headerCount + (L.length bs)) maxDisk maxRAM maxHeader)+{-# INLINE defaultInputIter #-}++hPutLimit :: Int64 -> Handle -> L.ByteString -> IO (Bool, Int64)+hPutLimit maxCount h bs = hPutLimit' maxCount h 0 bs+{-# INLINE hPutLimit #-}++hPutLimit' :: Int64 -> Handle -> Int64 -> L.ByteString -> IO (Bool, Int64)+hPutLimit' _maxCount h count Empty = return (False, count)+hPutLimit' maxCount h count (Chunk c cs)+ | (count + fromIntegral (S.length c)) > maxCount =+ do S.hPut h (S.take (fromIntegral (maxCount - count)) c)+ return (True, maxCount)+ | otherwise =+ do S.hPut h c+ hPutLimit' maxCount h (count + fromIntegral (S.length c)) cs+{-# INLINE hPutLimit' #-}++-- FIXME: can we safely use L.unpack, or do we need to worry about encoding issues in the headers?+bodyPartToInput :: InputWorker -> BodyPart -> IO InputIter -- (Either String (String,Input))+bodyPartToInput inputWorker (BodyPart rawHS b) =+ do r <- inputWorker (HeaderWork rawHS)+ case r of+ (Failed i e) -> return $ Failed i e+ (HeaderResult hs cont) ->+ let ctype = fromMaybe defaultInputType (getContentType hs) in+ case getContentDisposition hs of+ Just (ContentDisposition "form-data" ps) ->+ cont (BodyWork ctype ps b)++ cd -> return $ Failed Nothing ("Expected content-disposition: form-data but got " ++ show cd)++bodyPartsToInputs :: InputWorker -> [BodyPart] -> IO ([(String,Input)], Maybe String)+bodyPartsToInputs _ [] = + return ([], Nothing)+bodyPartsToInputs inputWorker (b:bs) =+ do r <- bodyPartToInput inputWorker b+ case r of+ (Failed mInput e) ->+ case mInput of+ Nothing -> return ([], Just e)+ (Just i) -> return ([i], Just e)+ (BodyResult i cont) ->+ do (is, err) <- bodyPartsToInputs cont bs+ return (i:is, err)+ (HeaderResult _ _) ->+ return ([], Just "InputWorker is broken. Returned a HeaderResult when a BodyResult was required.")++multipartBody :: InputWorker -> L.ByteString -> L.ByteString -> IO ([(String, Input)], Maybe String)+multipartBody inputWorker boundary s =+ do let (bodyParts, mErr) = parseMultipartBody boundary s+ (inputs, mErr2) <- bodyPartsToInputs inputWorker bodyParts+ return (inputs, mErr2 `mplus` mErr)++-- | Packs a string into an Input of type "text/plain"+simpleInput :: String -> Input+simpleInput v+ = Input { inputValue = Right (L.pack v)+ , inputFilename = Nothing+ , inputContentType = defaultInputType+ }++-- | The default content-type for variables.+defaultInputType :: ContentType+defaultInputType = ContentType "text" "plain" [] -- FIXME: use some default encoding?++parseMultipartBody :: L.ByteString -> L.ByteString -> ([BodyPart], Maybe String)+parseMultipartBody boundary s =+ case dropPreamble boundary s of+ (partData, Just e) -> ([], Just e)+ (partData, Nothing) -> splitParts boundary partData++dropPreamble :: L.ByteString -> L.ByteString -> (L.ByteString, Maybe String)+dropPreamble b s | isBoundary b s = (dropLine s, Nothing)+ | L.null s = (s, Just $ "Boundary " ++ L.unpack b ++ " not found.")+ | otherwise = dropPreamble b (dropLine s)++dropLine :: L.ByteString -> L.ByteString+dropLine = L.drop 2 . dropWhileS (not . L.isPrefixOf crlf)++-- | Check whether a string starts with two dashes followed by+-- the given boundary string.+isBoundary :: L.ByteString -- ^ The boundary, without the initial dashes+ -> L.ByteString+ -> Bool+isBoundary b s = startsWithDashes s && b `L.isPrefixOf` L.drop 2 s++-- | Checks whether a string starts with two dashes.+startsWithDashes :: L.ByteString -> Bool+startsWithDashes s = L.pack "--" `L.isPrefixOf` s++splitParts :: L.ByteString -> L.ByteString -> ([BodyPart], Maybe String)+splitParts boundary s =+-- | not (isBoundary boundary s) = ([], Just $ "Missing boundary: " ++ L.unpack boundary ++ "\n" ++ L.unpack s)+ case L.null s of+ True -> ([], Nothing)+ False ->+ case splitPart boundary s of+ (p, s') ->+ let (ps,e) = splitParts boundary s'+ in (p:ps, e)+{-# INLINE splitParts #-}++splitPart :: L.ByteString -> L.ByteString -> (BodyPart, L.ByteString)+splitPart boundary s =+ case splitBlank s of+ (headers, rest) ->+ case splitBoundary boundary (L.drop 4 rest) of+ (body, rest') -> (BodyPart (L.append headers crlf) body, rest')+{-# INLINE splitPart #-}+++splitBlank :: L.ByteString -> (L.ByteString, L.ByteString)+splitBlank s = spanS (not . L.isPrefixOf crlfcrlf) s+{-# INLINE splitBlank #-}+++splitBoundary :: L.ByteString -> L.ByteString -> (L.ByteString, L.ByteString)+splitBoundary boundary s =+ case spanS (not . L.isPrefixOf (L.pack "\r\n--" `L.append` boundary)) s of+ (x,y) -> (x, dropLine (L.drop 2 y))+{-# INLINE splitBoundary #-}++splitAtEmptyLine :: L.ByteString -> Maybe (L.ByteString, L.ByteString)+splitAtEmptyLine s =+ case splitBlank s of+ (before, after) | L.null after -> Nothing+ | otherwise -> Just (L.append before crlf, L.drop 4 after)+{-# INLINE splitAtEmptyLine #-}+ +-- | Split a string at the first CRLF. The CRLF is not included+-- in any of the returned strings.+splitAtCRLF :: ByteString -- ^ String to split.+ -> Maybe (ByteString,ByteString)+ -- ^ Returns 'Nothing' if there is no CRLF.+splitAtCRLF s =+ case spanS (not . L.isPrefixOf crlf) s of+ (before, after) | L.null after -> Nothing+ | otherwise -> Just (before, L.drop 2 after)+{-# INLINE splitAtCRLF #-}
+ src/Happstack/Server/Internal/RFC822Headers.hs view
@@ -0,0 +1,266 @@+-- #hide++-----------------------------------------------------------------------------+-- |+-- Module : Network.CGI.RFC822Headers+-- Copyright : (c) Peter Thiemann 2001,2002+-- (c) Bjorn Bringert 2005-2006+-- (c) Lemmih 2007+-- License : BSD-style+--+-- Maintainer : lemmih@vo.com+-- Stability : experimental+-- Portability : portable+--+-- Parsing of RFC822-style headers (name, value pairs)+-- Partly based on code from WASHMail.+--+-----------------------------------------------------------------------------+module Happstack.Server.Internal.RFC822Headers+ ( -- * Headers+ Header, + pHeader,+ pHeaders,+ parseHeaders,++ -- * Content-type+ ContentType(..), + getContentType,+ parseContentType,+ showContentType,++ -- * Content-transfer-encoding+ ContentTransferEncoding(..),+ getContentTransferEncoding,+ parseContentTransferEncoding,++ -- * Content-disposition+ ContentDisposition(..),+ getContentDisposition, + parseContentDisposition,+ + -- * Utilities+ parseM+ ) where++import Data.Char+import Data.List+import Text.ParserCombinators.Parsec++type Header = (String, String)++pHeaders :: Parser [Header]+pHeaders = many pHeader++parseHeaders :: Monad m => SourceName -> String -> m [Header]+parseHeaders = parseM pHeaders++pHeader :: Parser Header+pHeader = + do name <- many1 headerNameChar+ char ':'+ many ws1+ line <- lineString+ crLf+ extraLines <- many extraFieldLine+ return (map toLower name, concat (line:extraLines))++extraFieldLine :: Parser String+extraFieldLine = + do sp <- ws1+ line <- lineString+ crLf+ return (sp:line)++--+-- * Parameters (for Content-type etc.)+--++showParameters :: [(String,String)] -> String+showParameters = concatMap f+ where f (n,v) = "; " ++ n ++ "=\"" ++ concatMap esc v ++ "\""+ esc '\\' = "\\\\"+ esc '"' = "\\\""+ esc c | c `elem` ['\\','"'] = '\\':[c]+ | otherwise = [c]++p_parameter :: Parser (String,String)+p_parameter =+ do lexeme $ char ';'+ p_name <- lexeme $ p_token+ lexeme $ char '='+ -- Workaround for seemingly standardized web browser bug+ -- where nothing is escaped in the filename parameter+ -- of the content-disposition header in multipart/form-data+ let litStr = if p_name == "filename" + then buggyLiteralString+ else literalString+ p_value <- litStr <|> p_token+ return (map toLower p_name, p_value)+++-- +-- * Content type+--++-- | A MIME media type value.+-- The 'Show' instance is derived automatically.+-- Use 'showContentType' to obtain the standard+-- string representation.+-- See <http://www.ietf.org/rfc/rfc2046.txt> for more+-- information about MIME media types.+data ContentType = + ContentType {+ -- | The top-level media type, the general type+ -- of the data. Common examples are+ -- \"text\", \"image\", \"audio\", \"video\",+ -- \"multipart\", and \"application\".+ ctType :: String,+ -- | The media subtype, the specific data format.+ -- Examples include \"plain\", \"html\",+ -- \"jpeg\", \"form-data\", etc.+ ctSubtype :: String,+ -- | Media type parameters. On common example is+ -- the charset parameter for the \"text\" + -- top-level type, e.g. @(\"charset\",\"ISO-8859-1\")@.+ ctParameters :: [(String, String)]+ }+ deriving (Show, Read, Eq, Ord)++-- | Produce the standard string representation of a content-type,+-- e.g. \"text\/html; charset=ISO-8859-1\".+showContentType :: ContentType -> String+showContentType (ContentType x y ps) = x ++ "/" ++ y ++ showParameters ps++pContentType :: Parser ContentType+pContentType = + do many ws1+ c_type <- p_token+ lexeme $ char '/'+ c_subtype <- lexeme $ p_token+ c_parameters <- many p_parameter+ return $ ContentType (map toLower c_type) (map toLower c_subtype) c_parameters++-- | Parse the standard representation of a content-type.+-- If the input cannot be parsed, this function calls+-- 'fail' with a (hopefully) informative error message.+parseContentType :: Monad m => String -> m ContentType+parseContentType = parseM pContentType "Content-type"++getContentType :: Monad m => [Header] -> m ContentType+getContentType hs = lookupM "content-type" hs >>= parseContentType++--+-- * Content transfer encoding+--++data ContentTransferEncoding =+ ContentTransferEncoding String+ deriving (Show, Read, Eq, Ord)++pContentTransferEncoding :: Parser ContentTransferEncoding+pContentTransferEncoding =+ do many ws1+ c_cte <- p_token+ return $ ContentTransferEncoding (map toLower c_cte)++parseContentTransferEncoding :: Monad m => String -> m ContentTransferEncoding+parseContentTransferEncoding = + parseM pContentTransferEncoding "Content-transfer-encoding"++getContentTransferEncoding :: Monad m => [Header] -> m ContentTransferEncoding+getContentTransferEncoding hs = + lookupM "content-transfer-encoding" hs >>= parseContentTransferEncoding++--+-- * Content disposition+--++data ContentDisposition =+ ContentDisposition String [(String, String)]+ deriving (Show, Read, Eq, Ord)++pContentDisposition :: Parser ContentDisposition+pContentDisposition =+ do many ws1+ c_cd <- p_token+ c_parameters <- many p_parameter+ return $ ContentDisposition (map toLower c_cd) c_parameters++parseContentDisposition :: Monad m => String -> m ContentDisposition+parseContentDisposition = parseM pContentDisposition "Content-disposition"++getContentDisposition :: Monad m => [Header] -> m ContentDisposition+getContentDisposition hs = + lookupM "content-disposition" hs >>= parseContentDisposition++--+-- * Utilities+--++parseM :: Monad m => Parser a -> SourceName -> String -> m a+parseM p n inp =+ case parse p n inp of+ Left e -> fail (show e)+ Right x -> return x++lookupM :: (Monad m, Eq a, Show a) => a -> [(a,b)] -> m b+lookupM n = maybe (fail ("No such field: " ++ show n)) return . lookup n++-- +-- * Parsing utilities+--++-- | RFC 822 LWSP-char+ws1 :: Parser Char+ws1 = oneOf " \t"++lexeme :: Parser a -> Parser a+lexeme p = do x <- p; many ws1; return x++-- | RFC 822 CRLF (but more permissive)+crLf :: Parser String+crLf = try (string "\n\r" <|> string "\r\n") <|> string "\n" <|> string "\r"++-- | One line+lineString :: Parser String+lineString = many (noneOf "\n\r")++literalString :: Parser String+literalString = do char '\"'+ str <- many (noneOf "\"\\" <|> quoted_pair)+ char '\"'+ return str++-- No web browsers seem to implement RFC 2046 correctly,+-- since they do not escape double quotes and backslashes+-- in the filename parameter in multipart/form-data.+--+-- Note that this eats everything until the last double quote on the line.+buggyLiteralString :: Parser String+buggyLiteralString = + do char '\"'+ str <- manyTill anyChar (try lastQuote)+ return str+ where lastQuote = do char '\"' + notFollowedBy (try (many (noneOf "\"") >> char '\"'))++headerNameChar :: Parser Char+headerNameChar = noneOf "\n\r:"++especials, tokenchar :: [Char]+especials = "()<>@,;:\\\"/[]?.="+tokenchar = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" \\ especials++p_token :: Parser String+p_token = many1 (oneOf tokenchar)++text_chars :: [Char]+text_chars = map chr ([1..9] ++ [11,12] ++ [14..127])++p_text :: Parser Char+p_text = oneOf text_chars++quoted_pair :: Parser Char+quoted_pair = do char '\\'+ p_text
+ src/Happstack/Server/Internal/Socket.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE TemplateHaskell #-}+module Happstack.Server.Internal.Socket(acceptLite) where++import Happstack.Server.Internal.SocketTH(supportsIPv6)+import Language.Haskell.TH.Syntax+import Happstack.Util.HostAddress+import qualified Network as N+ ( PortID(PortNumber)+ , socketPort+ )+import qualified Network.Socket as S+ ( Socket(..)+ , PortNumber()+ , SockAddr(..)+ , HostName+ , accept+ , socketToHandle+ )+import System.IO++-- | alternative implementation of accept to work around EAI_AGAIN errors+acceptLite :: S.Socket -> IO (S.Socket, S.HostName, S.PortNumber)+acceptLite sock = do+ (sock', addr) <- S.accept sock+ (N.PortNumber p) <- N.socketPort sock'+ + let peer = $(if supportsIPv6+ then+ return $ CaseE (VarE (mkName "addr")) + [Match + (ConP (mkName "S.SockAddrInet") + [WildP,VarP (mkName "ha")]) + (NormalB (AppE (VarE (mkName "showHostAddress")) + (VarE (mkName "ha")))) []+ ,Match (ConP (mkName "S.SockAddrInet6") [WildP,WildP,VarP (mkName "ha"),WildP])+ (NormalB (AppE (VarE (mkName "showHostAddress6")) (VarE (mkName "ha")))) []+ ,Match WildP (NormalB (AppE (VarE (mkName "error")) (LitE (StringL "Unsupported socket")))) []]+ -- the above mess is the equivalent of this: + {-[| case addr of+ (S.SockAddrInet _ ha) -> showHostAddress ha+ (S.SockAddrInet6 _ _ ha _) -> showHostAddress6 ha+ _ -> error "Unsupported socket"+ |]-}+ else+ [| case addr of+ (S.SockAddrInet _ ha) -> showHostAddress ha+ _ -> error "Unsupported socket"+ |])+ + return (sock', peer, p)+
+ src/Happstack/Server/Internal/SocketTH.hs view
@@ -0,0 +1,15 @@+{-# LANGUAGE TemplateHaskell #-}+module Happstack.Server.Internal.SocketTH(supportsIPv6) where+import Language.Haskell.TH++import Data.List+import Data.Maybe+import Network.Socket(SockAddr(..))++-- find out at compile time if the SockAddr6 / HostAddress6 constructors are available+supportsIPv6 :: Bool+supportsIPv6 = $(let c = ["Network.Socket.SockAddrInet6", "Network.Socket.Internal.SockAddrInet6"] ; d = ''SockAddr in+ do TyConI (DataD _ _ _ cs _) <- reify d+ if isJust (find (\(NormalC n _) -> show n `elem` c) cs)+ then [| True |]+ else [| False |] )
+ src/Happstack/Server/Internal/TimeoutManager.hs view
@@ -0,0 +1,59 @@+module Happstack.Server.Internal.TimeoutManager+ ( Manager+ , Handle+ , initialize+ , register+ , tickle+ , pause+ , resume+ , cancel+ ) where++import qualified Data.IORef as I+import Control.Concurrent (forkIO, threadDelay)+import Control.Monad (forever)+import qualified Control.Exception as E++-- FIXME implement stopManager++newtype Manager = Manager (I.IORef [Handle])+data Handle = Handle (IO ()) (I.IORef State)+data State = Active | Inactive | Paused | Canceled++initialize :: Int -> IO Manager+initialize timeout = do+ ref <- I.newIORef []+ _ <- forkIO $ forever $ do+ threadDelay timeout+ ms <- I.atomicModifyIORef ref (\x -> ([], x))+ ms' <- go ms id+ I.atomicModifyIORef ref (\x -> (ms' x, ()))+ return $ Manager ref+ where+ go [] front = return front+ go (m@(Handle onTimeout iactive):rest) front = do+ state <- I.atomicModifyIORef iactive (\x -> (go' x, x))+ case state of+ Inactive -> do+ onTimeout `E.catch` ignoreAll+ go rest front+ Canceled -> go rest front+ _ -> go rest (front . (:) m)+ go' Active = Inactive+ go' x = x++ignoreAll :: E.SomeException -> IO ()+ignoreAll _ = return ()++register :: Manager -> IO () -> IO Handle+register (Manager ref) onTimeout = do+ iactive <- I.newIORef Active+ let h = Handle onTimeout iactive+ I.atomicModifyIORef ref (\x -> (h : x, ()))+ return h++tickle, pause, resume, cancel :: Handle -> IO ()+tickle (Handle _ iactive) = I.writeIORef iactive Active+pause (Handle _ iactive) = I.writeIORef iactive Paused+resume = tickle+cancel (Handle _ iactive) = I.writeIORef iactive Canceled
+ src/Happstack/Server/Internal/TimeoutSocket.hs view
@@ -0,0 +1,66 @@+{-# LANGUAGE BangPatterns, ScopedTypeVariables #-}+{- | +-- borrowed from snap-server. Check there periodically for updates.+-}+module Happstack.Server.Internal.TimeoutSocket where++import Control.Concurrent (ThreadId, forkIO, killThread, threadDelay, threadWaitWrite)+import Control.Exception (SomeException, catch, throw)+import Control.Monad (liftM, when)+import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Lazy.Char8 as L+import qualified Data.ByteString.Lazy.Internal as L+import qualified Data.ByteString as S+import qualified Network.Socket.ByteString as N+import Data.Word+import Data.IORef+import Data.List (foldl')+import Data.Time.Clock.POSIX(POSIXTime, getPOSIXTime)+import Happstack.Server.Internal.Clock (getApproximatePOSIXTime)+import qualified Happstack.Server.Internal.TimeoutManager as TM+import Network.Socket (Socket, ShutdownCmd(..), shutdown)+import Network.Socket.SendFile (Iter(..), ByteCount, Offset, sendFileIterWith')+import Network.Socket.ByteString (sendAll)+import Prelude hiding (catch)+import System.IO (Handle, hClose, hIsEOF, hWaitForInput)+import System.IO.Error (isDoesNotExistError)+import System.IO.Unsafe (unsafeInterleaveIO)++sPutTickle :: TM.Handle -> Socket -> L.ByteString -> IO ()+sPutTickle thandle sock cs =+ do L.foldrChunks (\c rest -> sendAll sock c >> TM.tickle thandle >> rest) (return ()) cs+{-# INLINE sPutTickle #-}++sGetContents :: TM.Handle + -> Socket -- ^ Connected socket+ -> IO L.ByteString -- ^ Data received+sGetContents handle sock = loop where+ loop = unsafeInterleaveIO $ do+ s <- N.recv sock 65536+ TM.tickle handle+ if S.null s+ then do shutdown sock ShutdownReceive `catch` (\e -> when (not $ isDoesNotExistError e) (throw e))+ return L.Empty+ else L.Chunk s `liftM` loop+++sendFileTickle :: TM.Handle -> Socket -> FilePath -> Offset -> ByteCount -> IO ()+sendFileTickle thandle outs fp offset count =+ sendFileIterWith' (iterTickle thandle) outs fp 65536 offset count++iterTickle :: TM.Handle -> IO Iter -> IO ()+iterTickle thandle = + iterTickle' + where+ iterTickle' :: (IO Iter -> IO ())+ iterTickle' iter =+ do r <- iter+ TM.tickle thandle+ case r of+ (Done _) ->+ return ()+ (WouldBlock _ fd cont) ->+ do threadWaitWrite fd+ iterTickle' cont+ (Sent _ cont) ->+ do iterTickle' cont
+ src/Happstack/Server/Internal/Types.hs view
@@ -0,0 +1,393 @@+{-# LANGUAGE TypeSynonymInstances, DeriveDataTypeable, RankNTypes #-}++module Happstack.Server.Internal.Types+ (Request(..), Response(..), RqBody(..), Input(..), HeaderPair(..),+ takeRequestBody, readInputsBody,+ rqURL, mkHeaders,+ getHeader, getHeaderBS, getHeaderUnsafe,+ hasHeader, hasHeaderBS, hasHeaderUnsafe,+ setHeader, setHeaderBS, setHeaderUnsafe,+ addHeader, addHeaderBS, addHeaderUnsafe,+ setRsCode, -- setCookie, setCookies,+ Conf(..), nullConf, result, resultBS,+ redirect, -- redirect_, redirect', redirect'_,+ isHTTP1_0, isHTTP1_1,+ RsFlags(..), nullRsFlags, noContentLength,+ HttpVersion(..), Length(..), Method(..), Headers, continueHTTP,+ Host, ContentType(..)+ ) where+++import Control.Monad.Error (Error(strMsg))+import Control.Monad.Trans (MonadIO(liftIO))+import Control.Concurrent.MVar+import qualified Data.Map as M+import Data.Data (Data)+import Data.IORef (IORef, atomicModifyIORef, readIORef)+import Data.Time.Format (FormatTime(..))+import Data.Typeable(Typeable)+import qualified Data.ByteString.Char8 as P+import Data.ByteString.Char8 (ByteString,pack)+import qualified Data.ByteString.Lazy.Char8 as L+import qualified Data.ByteString.Lazy.UTF8 as LU (fromString)+import Data.Maybe+import Data.List+import Happstack.Server.SURI+import Data.Char (toLower)++import Happstack.Server.Internal.RFC822Headers ( ContentType(..) )+import Happstack.Server.Internal.Cookie+import Happstack.Util.LogFormat (formatRequestCombined)+import System.Log.Logger (Priority(..), logM)+import Text.Show.Functions ()++-- | HTTP version+data HttpVersion = HttpVersion Int Int+ deriving(Read,Eq)++instance Show HttpVersion where+ show (HttpVersion x y) = (show x) ++ "." ++ (show y)++-- | 'True' if 'Request' is HTTP version @1.1@+isHTTP1_1 :: Request -> Bool+isHTTP1_1 rq = case rqVersion rq of HttpVersion 1 1 -> True; _ -> False++-- | 'True' if 'Request' is HTTP version @1.0@+isHTTP1_0 :: Request -> Bool+isHTTP1_0 rq = case rqVersion rq of HttpVersion 1 0 -> True; _ -> False++-- | Should the connection be used for further messages after this.+-- | isHTTP1_0 && hasKeepAlive || isHTTP1_1 && hasNotConnectionClose+continueHTTP :: Request -> Response -> Bool+--continueHTTP rq res = isHTTP1_1 rq && getHeader' connectionC rq /= Just closeC && rsfContentLength (rsFlags res)+continueHTTP rq res = (isHTTP1_0 rq && checkHeaderBS connectionC keepaliveC rq) ||+ (isHTTP1_1 rq && not (checkHeaderBS connectionC closeC rq)) && (rsfLength (rsFlags res) /= NoContentLength)++-- | HTTP configuration+data Conf = Conf { port :: Int -- ^ Port for the server to listen on.+ , validator :: Maybe (Response -> IO Response) -- ^ a function to validate the output on-the-fly+ , logAccess :: forall t. FormatTime t => Maybe (String -> String -> t -> String -> Int -> Integer -> String -> String -> IO ()) -- ^ function to log access requests (see also: 'logMAccess')+ , timeout :: Int -- ^ number of seconds to wait before killing an inactive thread+ } ++-- | Default configuration contains no validator and the port is set to 8000+nullConf :: Conf+nullConf = Conf { port = 8000+ , validator = Nothing+ , logAccess = Just logMAccess+ , timeout = 30+ }++-- | log access requests using hslogger and apache-style log formatting+--+-- see also: 'Conf'+logMAccess host user time requestLine responseCode size referer userAgent =+ logM "Happstack.Server.AccessLog.Combined" INFO $ formatRequestCombined host user time requestLine responseCode size referer userAgent++-- | HTTP request method+data Method = GET | HEAD | POST | PUT | DELETE | TRACE | OPTIONS | CONNECT+ deriving(Show,Read,Eq,Ord,Typeable,Data)++-- | an HTTP header+data HeaderPair + = HeaderPair { hName :: ByteString -- ^ header name+ , hValue :: [ByteString] -- ^ header value (or values if multiple occurances of the header are present)+ } + deriving (Read,Show)+-- | Combined headers.++-- | a Map of HTTP headers+-- +-- the Map key is the header converted to lowercase+type Headers = M.Map ByteString HeaderPair -- ^ lowercased name -> (realname, value)++-- | A flag value set in the 'Response' which controls how the+-- @Content-Length@ header is set, and whether *chunked* output+-- encoding is used.+--+-- see also: 'nullRsFlags', 'notContentLength', and 'chunked'+data Length + = ContentLength -- ^ automatically add a @Content-Length@ header to the 'Response'+ | TransferEncodingChunked -- ^ do not add a @Content-Length@ header. Do use @chunked@ output encoding+ | NoContentLength -- ^ do not set @Content-Length@ or @chunked@ output encoding.+ deriving (Eq, Ord, Read, Show, Enum)++-- | Result flags+data RsFlags = RsFlags + { rsfLength :: Length+ } deriving (Show,Read,Typeable)++-- | Default RsFlags that will include the content-length header+nullRsFlags :: RsFlags+nullRsFlags = RsFlags { rsfLength = TransferEncodingChunked }++-- | Do not automatically add a Content-Length field to the 'Response'+noContentLength :: Response -> Response+noContentLength res = res { rsFlags = flags } where flags = (rsFlags res) { rsfLength = NoContentLength }++-- | Do not automatically add a Content-Length header. Do automatically use Transfer-Encoding: Chunked+chunked :: Response -> Response+chunked res = res { rsFlags = flags } where flags = (rsFlags res) { rsfLength = TransferEncodingChunked }++-- | a value extract from the @QUERY_STRING@ or 'Request' body+--+-- If the input value was a file, then it will be saved to a temporary file on disk and 'inputValue' will contain @Left pathToTempFile@.+data Input = Input+ { inputValue :: Either FilePath L.ByteString+ , inputFilename :: Maybe FilePath+ , inputContentType :: ContentType+ } deriving (Show,Read,Typeable)++-- | hostname & port+type Host = (String, Int) -- ^ (hostname, port)++-- | an HTTP Response+data Response = Response { rsCode :: Int,+ rsHeaders :: Headers,+ rsFlags :: RsFlags,+ rsBody :: L.ByteString,+ rsValidator :: Maybe (Response -> IO Response)+ }+ | SendFile { rsCode :: Int,+ rsHeaders :: Headers,+ rsFlags :: RsFlags,+ rsValidator :: Maybe (Response -> IO Response),+ sfFilePath :: FilePath, -- ^ file handle to send from+ sfOffset :: Integer, -- ^ offset to start at+ sfCount :: Integer -- ^ number of bytes to send+ }+ deriving (Typeable)++instance Show Response where+ showsPrec _ res@Response{} =+ showString "================== Response ================" .+ showString "\nrsCode = " . shows (rsCode res) .+ showString "\nrsHeaders = " . shows (rsHeaders res) .+ showString "\nrsFlags = " . shows (rsFlags res) .+ showString "\nrsBody = " . shows (rsBody res) .+ showString "\nrsValidator = " . shows (rsValidator res)+ showsPrec _ res@SendFile{} =+ showString "================== Response ================" .+ showString "\nrsCode = " . shows (rsCode res) .+ showString "\nrsHeaders = " . shows (rsHeaders res) .+ showString "\nrsFlags = " . shows (rsFlags res) .+ showString "\nrsValidator = " . shows (rsValidator res).+ showString "\nsfFilePath = " . shows (sfFilePath res) .+ showString "\nsfOffset = " . shows (sfOffset res) .+ showString "\nsfCount = " . shows (sfCount res)++-- what should the status code be ?+instance Error Response where+ strMsg str = + setHeader "Content-Type" "text/plain; charset=UTF-8" $ + result 500 str++-- | an HTTP request+data Request = Request { rqMethod :: Method,+ rqPaths :: [String],+ rqUri :: String,+ rqQuery :: String,+ rqInputsQuery :: [(String,Input)],+ rqInputsBody :: MVar [(String,Input)],+ rqCookies :: [(String,Cookie)],+ rqVersion :: HttpVersion,+ rqHeaders :: Headers,+ rqBody :: MVar RqBody,+ rqPeer :: Host+ } deriving(Typeable)++instance Show Request where+ showsPrec _ rq =+ showString "================== Request =================" .+ showString "\nrqMethod = " . shows (rqMethod rq) .+ showString "\nrqPaths = " . shows (rqPaths rq) .+ showString "\nrqUri = " . showString (rqUri rq) .+ showString "\nrqQuery = " . showString (rqQuery rq) .+ showString "\nrqInputsQuery = " . shows (rqInputsQuery rq) .+ showString "\nrqInputsBody = " . showString "<<mvar>>" .+ showString "\nrqCookies = " . shows (rqCookies rq) .+ showString "\nrqVersion = " . shows (rqVersion rq) .+ showString "\nrqHeaders = " . shows (rqHeaders rq) .+ showString "\nrqBody = " . showString "<<mvar>>" .+ showString "\nrqPeer = " . shows (rqPeer rq)++-- | get the request body from the Request and replace it with Nothing+--+-- IMPORTANT: You can really only call this function once. Subsequent+-- calls will return 'Nothing'.+takeRequestBody :: (MonadIO m) => Request -> m (Maybe RqBody)+takeRequestBody rq = liftIO $ tryTakeMVar (rqBody rq) +-- takeRequestBody rq = return (rqBody rq)+++-- | read the request body inputs+--+-- This will only work if the body inputs have already been decoded. Otherwise it will return Nothing.+readInputsBody :: Request -> IO (Maybe [(String, Input)])+readInputsBody req =+ do mbi <- tryTakeMVar (rqInputsBody req)+ case mbi of+ (Just bi) ->+ do putMVar (rqInputsBody req) bi+ return (Just bi)+ Nothing -> return Nothing++{-+takeRequestBody rq = + do body <- atomicModifyIORef (rqBody rq) (\bdy -> (Nothing, bdy))+ newBD <- readIORef (rqBody rq)+ print newBD+ newBD `seq` return body+-}+-- | Converts a Request into a String representing the corresponding URL+rqURL :: Request -> String+rqURL rq = '/':intercalate "/" (rqPaths rq) ++ (rqQuery rq)++-- | a class for working with types that contain HTTP headers+class HasHeaders a where + updateHeaders :: (Headers->Headers) -> a -> a -- ^ modify the headers+ headers :: a -> Headers -- ^ extract the headers++instance HasHeaders Response where updateHeaders f rs = rs{rsHeaders=f $ rsHeaders rs}+ headers = rsHeaders+instance HasHeaders Request where updateHeaders f rq = rq{rqHeaders = f $ rqHeaders rq} + headers = rqHeaders++instance HasHeaders Headers where updateHeaders f = f+ headers = id++-- | The body of an HTTP 'Request'+newtype RqBody = Body { unBody :: L.ByteString } deriving (Read,Show,Typeable)++-- | Sets the Response status code to the provided Int and lifts the computation+-- into a Monad.+setRsCode :: (Monad m) => Int -> Response -> m Response+setRsCode code rs = return rs {rsCode = code}++-- | Takes a list of (key,val) pairs and converts it into Headers. The+-- keys will be converted to lowercase+mkHeaders :: [(String,String)] -> Headers+mkHeaders hdrs+ = M.fromListWith join [ (P.pack (map toLower key), HeaderPair (P.pack key) [P.pack value]) | (key,value) <- hdrs ]+ where join (HeaderPair key vs1) (HeaderPair _ vs2) = HeaderPair key (vs1++vs2)++--------------------------------------------------------------+-- Retrieving header information+--------------------------------------------------------------++-- | Lookup header value. Key is case-insensitive.+getHeader :: HasHeaders r => String -> r -> Maybe ByteString+getHeader = getHeaderBS . pack++-- | Lookup header value. Key is a case-insensitive bytestring.+getHeaderBS :: HasHeaders r => ByteString -> r -> Maybe ByteString+getHeaderBS = getHeaderUnsafe . P.map toLower++-- | Lookup header value with a case-sensitive key. The key must be lowercase.+getHeaderUnsafe :: HasHeaders r => ByteString -> r -> Maybe ByteString+getHeaderUnsafe key var = listToMaybe =<< fmap hValue (getHeaderUnsafe' key var)++-- | Lookup header with a case-sensitive key. The key must be lowercase.+getHeaderUnsafe' :: HasHeaders r => ByteString -> r -> Maybe HeaderPair+getHeaderUnsafe' key = M.lookup key . headers++--------------------------------------------------------------+-- Querying header status+--------------------------------------------------------------++-- | Returns True if the associated key is found in the Headers. The lookup+-- is case insensitive.+hasHeader :: HasHeaders r => String -> r -> Bool+hasHeader key r = isJust (getHeader key r)++-- | Acts as 'hasHeader' with ByteStrings+hasHeaderBS :: HasHeaders r => ByteString -> r -> Bool+hasHeaderBS key r = isJust (getHeaderBS key r)++-- | Acts as 'hasHeaderBS' but the key is case sensitive. It should be+-- in lowercase.+hasHeaderUnsafe :: HasHeaders r => ByteString -> r -> Bool+hasHeaderUnsafe key r = isJust (getHeaderUnsafe' key r)++checkHeaderBS :: HasHeaders r => ByteString -> ByteString -> r -> Bool+checkHeaderBS key val = checkHeaderUnsafe (P.map toLower key) (P.map toLower val)++checkHeaderUnsafe :: HasHeaders r => ByteString -> ByteString -> r -> Bool+checkHeaderUnsafe key val r+ = case getHeaderUnsafe key r of+ Just val' | P.map toLower val' == val -> True+ _ -> False+++--------------------------------------------------------------+-- Setting header status+--------------------------------------------------------------++-- | Associates the key/value pair in the headers. Forces the key to be+-- lowercase.+setHeader :: HasHeaders r => String -> String -> r -> r+setHeader key val = setHeaderBS (pack key) (pack val)++-- | Acts as 'setHeader' but with ByteStrings.+setHeaderBS :: HasHeaders r => ByteString -> ByteString -> r -> r+setHeaderBS key val = setHeaderUnsafe (P.map toLower key) (HeaderPair key [val])++-- | Sets the key to the HeaderPair. This is the only way to associate a key+-- with multiple values via the setHeader* functions. Does not force the key+-- to be in lowercase or guarantee that the given key and the key in the HeaderPair will match. +setHeaderUnsafe :: HasHeaders r => ByteString -> HeaderPair -> r -> r+setHeaderUnsafe key val = updateHeaders (M.insert key val)++--------------------------------------------------------------+-- Adding headers+--------------------------------------------------------------++-- | Add a key/value pair to the header. If the key already has a value+-- associated with it, then the value will be appended. +-- Forces the key to be lowercase.+addHeader :: HasHeaders r => String -> String -> r -> r+addHeader key val = addHeaderBS (pack key) (pack val)++-- | Acts as addHeader except for ByteStrings+addHeaderBS :: HasHeaders r => ByteString -> ByteString -> r -> r+addHeaderBS key val = addHeaderUnsafe (P.map toLower key) (HeaderPair key [val])++-- | Add a key/value pair to the header using the underlying HeaderPair data+-- type. Does not force the key to be in lowercase or guarantee that the given key and the key in the HeaderPair will match. +addHeaderUnsafe :: HasHeaders r => ByteString -> HeaderPair -> r -> r+addHeaderUnsafe key val = updateHeaders (M.insertWith join key val)+ where join (HeaderPair k vs1) (HeaderPair _ vs2) = HeaderPair k (vs1++vs2)++-- | Creates a Response with the given Int as the status code and the provided+-- String as the body of the Response +result :: Int -> String -> Response+result code = resultBS code . LU.fromString++-- | Acts as 'result' but works with ByteStrings directly.+-- +-- By default, Transfer-Encoding: chunked will be used+resultBS :: Int -> L.ByteString -> Response+resultBS code s = Response code M.empty nullRsFlags s Nothing++-- | Sets the Response's status code to the given Int and redirects to the given URI+redirect :: (ToSURI s) => Int -> s -> Response -> Response+redirect c s resp = setHeaderBS locationC (pack (render (toSURI s))) resp{rsCode = c}++-- constants here++-- | @Location@+locationC :: ByteString+locationC = P.pack "Location"++-- | @close@+closeC :: ByteString+closeC = P.pack "close"++-- | @Connection@+connectionC :: ByteString+connectionC = P.pack "Connection"++-- | @Keep-Alive@+keepaliveC :: ByteString+keepaliveC = P.pack "Keep-Alive"+
− src/Happstack/Server/MessageWrap.hs
@@ -1,97 +0,0 @@-{-# LANGUAGE FlexibleInstances #-}--module Happstack.Server.MessageWrap where--import qualified Data.ByteString.Char8 as P-import qualified Data.ByteString.Lazy.Char8 as L-import Data.Maybe-import Happstack.Server.HTTP.Types as H-import Happstack.Server.HTTP.Multipart-import Happstack.Server.SURI as SURI-import Happstack.Util.Common--queryInput :: SURI -> [(String, Input)]-queryInput uri = formDecode (case SURI.query uri of- '?':r -> r- xs -> xs)--bodyInput :: Request -> [(String, Input)]-bodyInput req | (rqMethod req /= POST) && (rqMethod req /= PUT) = []-bodyInput req =- let ctype = getHeader "content-type" req >>= parseContentType . P.unpack- getBS (Body bs) = bs- in decodeBody ctype (getBS $ rqBody req)----- | Decodes application\/x-www-form-urlencoded inputs. -formDecode :: String -> [(String, Input)]-formDecode [] = []-formDecode qString = - if null pairString then rest else - (SURI.unEscape name,simpleInput $ SURI.unEscape val):rest- where (pairString,qString')= split (=='&') qString- (name,val)=split (=='=') pairString- rest=if null qString' then [] else formDecode qString'--decodeBody :: Maybe ContentType- -> L.ByteString- -> [(String,Input)]-decodeBody ctype inp- = case ctype of- Just (ContentType "application" "x-www-form-urlencoded" _)- -> formDecode (L.unpack inp)- Just (ContentType "multipart" "form-data" ps)- -> multipartDecode ps inp- Just _ -> [] -- unknown content-type, the user will have to- -- deal with it by looking at the raw content- -- No content-type given, assume x-www-form-urlencoded- Nothing -> formDecode (L.unpack inp)---- | Decodes multipart\/form-data input.-multipartDecode :: [(String,String)] -- ^ Content-type parameters- -> L.ByteString -- ^ Request body- -> [(String,Input)] -- ^ Input variables and values.-multipartDecode ps inp =- case lookup "boundary" ps of- Just b -> case parseMultipartBody b inp of- Just (MultiPart bs) -> map bodyPartToInput bs- Nothing -> [] -- FIXME: report parse error- Nothing -> [] -- FIXME: report that there was no boundary--bodyPartToInput :: BodyPart -> (String,Input)-bodyPartToInput (BodyPart hs b) =- case getContentDisposition hs of- Just (ContentDisposition "form-data" ps) ->- (fromMaybe "" $ lookup "name" ps,- Input { inputValue = b,- inputFilename = lookup "filename" ps,- inputContentType = ctype })- _ -> ("ERROR",simpleInput "ERROR") -- FIXME: report error- where ctype = fromMaybe defaultInputType (getContentType hs)---- | Packs a string into an Input of type "text/plain"-simpleInput :: String -> Input-simpleInput v- = Input { inputValue = L.pack v- , inputFilename = Nothing- , inputContentType = defaultInputType- }---- | The default content-type for variables.-defaultInputType :: ContentType-defaultInputType = ContentType "text" "plain" [] -- FIXME: use some default encoding?---- | Get the path components from a String.-pathEls :: String -> [String]-pathEls = (drop 1) . map SURI.unEscape . splitList '/' ---- | Like 'Read' except Strings and Chars not quoted.-class (Read a)=>ReadString a where readString::String->a; readString =read --instance ReadString Int -instance ReadString Double -instance ReadString Float -instance ReadString SURI.SURI where readString = read . show-instance ReadString [Char] where readString=id-instance ReadString Char where - readString s= if length t==1 then head t else read t where t=trim s
− src/Happstack/Server/MinHaXML.hs
@@ -1,214 +0,0 @@-{-# LANGUAGE TemplateHaskell, FlexibleInstances,- OverlappingInstances, UndecidableInstances, TypeSynonymInstances #-}--- -module Happstack.Server.MinHaXML where--- Copyright (c) Happstack.com 2009; (c) HAppS.org 2005. All Rights Reserved.--import Prelude hiding (elem, pi)--import Text.XML.HaXml.Types as Types-import Text.XML.HaXml.Escape-import Text.XML.HaXml.Pretty as Pretty-import Text.XML.HaXml.Verbatim as Verbatim-import Happstack.Util.Common-import System.Time--import Happstack.Data.Xml as Xml-import Happstack.Data.Xml.HaXml--type StyleURL=String-data StyleSheet = NoStyle- | CSS {styleURL::StyleURL} - | XSL {styleURL::StyleURL} deriving (Read,Show)-hasStyleURL :: StyleSheet -> Bool-hasStyleURL NoStyle = False-hasStyleURL _ = True -type Element = Types.Element-- --isCSS :: StyleSheet -> Bool-isCSS (CSS _)=True-isCSS _ = False-isXSL :: StyleSheet -> Bool-isXSL = not.isCSS--t :: Name -> [(Name, String)] -> CharData -> Types.Element-t=textElem-l :: Name -> [(Name, String)] -> [Types.Element] -> Types.Element-l=listElem-e :: Name -> [(Name, String)] -> Types.Element-e=emptyElem-(</<) :: Name- -> [(Name, String)]- -> [Types.Element]- -> Types.Element-(</<)=l-(<>) :: Name -> [(Name, String)] -> CharData -> Types.Element-(<>)=t----xmlElem :: (t -> [Content])- -> Name- -> [(Name, String)]- -> t- -> Types.Element-xmlElem f = \name attrs val -> xmlelem name attrs (f val)- where - xmlelem name = Types.Elem name . map (uncurry attr)- attr name val= (name,AttValue [Left val])--textElem :: Name -> [(Name, String)] -> CharData -> Types.Element-textElem = xmlElem (return.CString True)-emptyElem :: Name -> [(Name, String)] -> Types.Element-emptyElem = \n a->xmlElem id n a []-listElem :: Name- -> [(Name, String)]- -> [Types.Element]- -> Types.Element-listElem = xmlElem $ map CElem--cdataElem :: CharData -> Content-cdataElem = CString False---- Document (simpleProlog xsl) [] $ xmlEscape stdXmlEscaper root-simpleDocOld :: StyleSheet -> Types.Element -> String-simpleDocOld xsl = show . document . - flip (Document (simpleProlog xsl) []) [] . xmlStdEscape--simpleDoc :: StyleSheet -> Types.Element -> String-simpleDoc style elem = ("<?xml version='1.0' encoding='UTF-8' ?>\n"++- if hasStyleURL style then pi else "") ++- (verbatim $ xmlStdEscape elem)- where typeText=if isCSS style then "text/css" else "text/xsl"- pi= "<?xml-stylesheet type=\""++ typeText ++ - "\" href=\""++styleURL style++"\" ?>\n"---simpleDoc' :: StyleSheet -> Types.Element -> String-simpleDoc' style elem = (if hasStyleURL style then pi else "") ++- (verbatim $ xmlStdEscape elem)- where typeText=if isCSS style then "text/css" else "text/xsl"- pi= "<?xml-stylesheet type=\""++ typeText ++ - "\" href=\""++styleURL style++"\" ?>\n"----xmlEscaper :: XmlEscaper-xmlEscaper=stdXmlEscaper-xmlStdEscape :: Types.Element -> Types.Element-xmlStdEscape = xmlEscape stdXmlEscaper-verbim :: (Verbatim a) => a -> String-verbim = verbatim--simpleProlog :: StyleSheet -> Prolog-simpleProlog style = - Prolog - (Just (XMLDecl "1.0" - (Just $ EncodingDecl "UTF-8") - Nothing -- standalone declaration- ))- [] Nothing- (if url=="" then [] else [pi])- where- pi = PI ("xml-stylesheet", "type=\""++typeText++"\" href=\""++url++"\"")- typeText = if isCSS style then "text/css" else "text/xsl"- url=if hasStyleURL style then styleURL style else ""--nonEmpty :: Name -> String -> Maybe Types.Element-nonEmpty name val = if val=="" then Nothing- else Just $ textElem name [] val--getRoot :: Document -> Types.Element-getRoot (Document _ _ root _) = root----toXML .< "App" attrs ./>---toXML .< "App" attrs .> []-data XML a = XML StyleSheet a--class ToElement x where toElement::x->Types.Element- -instance (ToElement x) => ToElement (Maybe x) where - toElement = maybe (emptyElem "Nothing" []) toElement--instance ToElement String where toElement = textElem "String" []-instance ToElement Types.Element where toElement = id-instance ToElement CalendarTime where - toElement = recToEl "CalendarTime" - [attrFS "year" ctYear- ,attrFS "month" (fromEnum.ctMonth)- ,attrFS "day" ctDay- ,attrFS "hour" ctHour- ,attrFS "min" ctMin- ,attrFS "sec" ctSec- ,attrFS "time" time - ] []- where time = epochPico--instance ToElement Int where toElement = toElement . show-instance ToElement Integer where toElement = toElement . show-instance ToElement Float where toElement = toElement . show-instance ToElement Double where toElement = toElement . show---instance (Xml a) => ToElement a where- toElement = un . head . map toHaXml . toXml- where- un (CElem el) = el- un _ = error "Case not handled in Xml toElement instance"--wrapElem :: (ToElement x) => Name -> x -> Types.Element-wrapElem tag x= listElem tag [] [toElement x]-elF :: (ToElement b) => Name -> (a -> b) -> a -> Types.Element-elF tag f = wrapElem tag.f --- label !<=! field = wrapField label field-attrF :: t1 -> (t -> String) -> t -> (t1, String)-attrF name f rec = (name,quoteEsc $ f rec)-attrFS :: (Show a) => t1 -> (t -> a) -> t -> (t1, String)-attrFS name f rec = (name,quoteEsc $ show $ f rec)-attrFMb :: (a -> String)- -> String- -> (a1 -> Maybe a)- -> a1- -> (String, String)-attrFMb r name f = maybe ("","") (\x->(name,quoteEsc $ r x)) . f ----(\x->(name,quoteEsc $ r x)) . f ---(name,quoteEsc $ show $ f rec)--quoteEsc :: String -> String-quoteEsc [] = []-quoteEsc ('"':list) = """ ++ quoteEsc list-quoteEsc (x:xs) = x:quoteEsc xs----quotescape \\ and " \"--recToEl :: Name- -> [a -> (String, String)]- -> [a -> Types.Element]- -> a- -> Types.Element-recToEl name attrs els rec = listElem name attrs' (revmap rec els)- where- attrs' = filter (\ (x,_)->not $ null x) (revmap rec attrs)-listToEl :: (ToElement a) =>- Name -> [(Name, String)] -> [a] -> Types.Element-listToEl name attrs = listElem name attrs . map toElement --toAttrs :: t -> [(t1, t -> t2)] -> [(t1, t2)]-toAttrs x = map (\ (s,f)->(s, f x)) --{---toElement rules:-1. if the attr is an instance of toElement then it is a child.-2. if it named and is type string then it is shown that way.-3. if it named and has non-string type then use show on the value.-4. if the attributes are not named then use the type as the label and- make the text child be a show of the object.---}---newtype ElString = ElString {elString::String} deriving (Eq,Ord,Read,Show)
+ src/Happstack/Server/Monads.hs view
@@ -0,0 +1,86 @@+{-# LANGUAGE FlexibleContexts #-}+-- | This module provides four classes and some related functions+-- which provide 'ServerPartT' with much of its web-centric behavior.+--+-- 1. 'ServerMonad' provides access to the HTTP 'Request'+--+-- 2. 'FilterMonad' provides the ability to apply filters and transformations to a 'Response'+--+-- 3. 'WebMonad' provides a way to escape a computation early and return a 'Response'+--+-- 4. 'HasRqData' which provides access to the decoded QUERY_STRING and request body/form data+module Happstack.Server.Monads+ ( -- * ServerPartT+ ServerPartT+ , ServerPart+ -- * Happstack class+ , Happstack(..)+ -- * ServerMonad+ , ServerMonad(..)+ , mapServerPartT+ , mapServerPartT'+ , UnWebT+ , filterFun+ -- * FilterMonad+ , FilterMonad(..)+ , ignoreFilters+ , addHeaderM+ , getHeaderM+ , setHeaderM+ -- * WebMonad+ , WebMonad(..)+ , escape+ , escape' + -- * MonadPlus helpers+ , require+ , requireM+ ) where++import Control.Monad (MonadPlus(mzero))+import Control.Monad.Trans (MonadIO(..),MonadTrans(lift))+import qualified Data.ByteString.Char8 as B+import Happstack.Server.Internal.Monads+import Happstack.Server.Types (Response, addHeader, getHeader, setHeader)+import Happstack.Server.RqData (HasRqData)++-- | A class alias for all the classes a standard server monad (such as 'ServerPartT') is expected to have instances for. This allows you to keep your type signatures shorter and easier to understand.+class ( ServerMonad m, WebMonad Response m, FilterMonad Response m+ , MonadIO m, MonadPlus m, HasRqData m, Monad m, Functor m) => Happstack m+++instance (Functor m, Monad m, MonadPlus m, MonadIO m) => Happstack (ServerPartT m)++-- | Get a header out of the request.+getHeaderM :: (ServerMonad m) => String -> m (Maybe B.ByteString)+getHeaderM a = askRq >>= return . (getHeader a)++-- | Add headers into the response. This method does not overwrite+-- any existing header of the same name, hence the name 'addHeaderM'.+-- If you want to replace a header use 'setHeaderM'.+addHeaderM :: (FilterMonad Response m) => String -> String -> m ()+addHeaderM a v = composeFilter $ \res-> addHeader a v res++-- | Set a header into the response. This will replace an existing+-- header of the same name. Use 'addHeaderM' if you want to add more+-- than one header of the same name.+setHeaderM :: (FilterMonad Response m) => String -> String -> m ()+setHeaderM a v = composeFilter $ \res -> setHeader a v res++-- | Run an 'IO' action and, if it returns 'Just', pass it to the+-- second argument.+require :: (MonadIO m, MonadPlus m) => IO (Maybe a) -> (a -> m r) -> m r+require fn handle = do+ mbVal <- liftIO fn+ case mbVal of+ Nothing -> mzero+ Just a -> handle a++-- | A variant of require that can run in any monad, not just 'IO'.+requireM :: (MonadTrans t, Monad m, MonadPlus (t m)) => m (Maybe a) -> (a -> t m r) -> t m r+requireM fn handle = do+ mbVal <- lift fn+ case mbVal of+ Nothing -> mzero+ Just a -> handle a++
− src/Happstack/Server/Parts.hs
@@ -1,171 +0,0 @@-{-# LANGUAGE NoMonomorphismRestriction, FlexibleContexts #-}-module Happstack.Server.Parts(- compressedResponseFilter- ,gzipFilter- ,deflateFilter- ,encodings-) where-import Happstack.Server.SimpleHTTP-import Text.ParserCombinators.Parsec-import Control.Monad-import Data.Maybe-import Data.List-import qualified Data.ByteString.Char8 as BS-import qualified Codec.Compression.GZip as GZ-import qualified Codec.Compression.Zlib as Z---- | reads the \"Accept-Encoding\" header. Then, if possible--- will compress the response body with methods "gzip" or "deflate"------ Returns the name of the coding chosen-compressedResponseFilter::- (FilterMonad Response m, MonadPlus m, WebMonad Response m, ServerMonad m)- => m String-compressedResponseFilter = do- getHeaderM "Accept-Encoding" >>=- (maybe (return "identity") installHandler)-- where- badEncoding = "Encoding returned not in the list of known encodings"-- installHandler accept = do- let eEncoding = bestEncoding $ BS.unpack accept- (coding,action) <- case eEncoding of- Left _ -> do- setResponseCode 406- finishWith $ toResponse ""-- Right a -> return (a, fromMaybe (fail badEncoding)- (lookup a allEncodingHandlers))-- setHeaderM "Content-Encoding" coding- action- return coding----- | compresses the body of the response with gzip.--- does not set any headers.-gzipFilter::(FilterMonad Response m) => m()-gzipFilter = do- composeFilter (\r -> r{rsBody = GZ.compress $ rsBody r})---- | compresses the body of the response with zlib's--- deflate method--- does not set any headers.-deflateFilter::(FilterMonad Response m) => m()-deflateFilter = do- composeFilter (\r -> r{rsBody = Z.compress $ rsBody r})---- | based on the rules describe in rfc2616 sec. 14.3-bestEncoding :: String -> Either String String-bestEncoding encs = do- encList<-either (Left . show) (Right) $ parse encodings "" encs- case acceptable encList of- [] -> Left "no encoding found"- a -> Right $ head a- where- -- first intersect with the list of encodings we know how to deal with at all- knownEncodings:: [(String,Maybe Double)] -> [(String, Maybe Double)]- knownEncodings m = intersectBy (\x y->fst x == fst y) m (map (\x -> (x,Nothing)) allEncodings)- -- this expands the wildcard, by figuring out if we need to include "identity" in the list- -- Then it deletes the wildcard entry, drops all the "q=0" entries (which aren't allowed).- --- -- note this implementation is a little conservative. if someone were to specify "*"- -- without a "q" value, it would be this server is willing to accept any format at all.- -- We pretty much assume we can't send them /any/ format and that they really- -- meant just "identity" this seems safe to me.- knownEncodings':: [(String,Maybe Double)] -> [(String, Maybe Double)]- knownEncodings' m = filter dropZero $ deleteBy (\(a,_) (b,_)->a==b) ("*",Nothing) $- case lookup "*" (knownEncodings m) of- Nothing -> addIdent $ knownEncodings m- Just (Just a) | a>0 -> addIdent $ knownEncodings m- | otherwise -> knownEncodings m- Just (Nothing) -> addIdent $ knownEncodings m- dropZero (_, Just a) | a==0 = False- | otherwise = True- dropZero (_, Nothing) = True- addIdent:: [(String,Maybe Double)] -> [(String, Maybe Double)]- addIdent m = if isNothing $ lookup "identity" m- then m ++ [("identity",Nothing)]- else m- -- finally we sort the list of available encodings.- acceptable:: [(String,Maybe Double)] -> [String]- acceptable l = map fst $ sortBy (flip cmp) $ knownEncodings' l- -- let the client choose but break ties with gzip- encOrder = reverse $ zip (reverse allEncodings) [1..]- m0 = maybe (0.0::Double) id- cmp (s,mI) (t,mJ) | m0 mI == m0 mJ- = compare (m0 $ lookup s encOrder) (m0 $ lookup t encOrder)- | otherwise = compare (m0 mI) (m0 mJ)---allEncodingHandlers:: (FilterMonad Response m) => [(String, m ())]-allEncodingHandlers = zip allEncodings handlers--allEncodings :: [String]-allEncodings =- ["gzip"- ,"x-gzip"--- ,"compress" -- as far as I can tell there is no haskell library that supports this--- ,"x-compress" -- as far as I can tell, there is no haskell library that supports this- ,"deflate"- ,"identity"- ,"*"- ]--handlers::(FilterMonad Response m) => [m ()]-handlers =- [gzipFilter- ,gzipFilter--- ,compressFilter--- ,compressFilter- ,deflateFilter- ,return ()- ,fail "chose * as content encoding"- ]---- | unsupported: a parser for the Accept-Encoding header-encodings :: GenParser Char st [([Char], Maybe Double)]-encodings = ws >> (encoding1 `sepBy` try sep) >>= (\x -> ws >> eof >> return x)- where- ws :: GenParser Char st ()- ws = many space >> return ()-- sep :: GenParser Char st ()- sep = do- ws- _ <- char ','- ws- - encoding1 :: GenParser Char st ([Char], Maybe Double)- encoding1 = do- encoding <- many1 (alphaNum <|> char '-') <|> string "*"- ws- quality<-optionMaybe qual- return (encoding, fmap read quality)-- qual :: GenParser Char st String- qual = do- char ';' >> ws >> char 'q' >> ws >> char '=' >> ws- q<-float- return q-- int :: GenParser Char st String- int = many1 digit-- float :: GenParser Char st String- float = do- wholePart<-many1 digit- fractionalPart<-option "" fraction- return $ wholePart ++ fractionalPart- <|>- do- fractionalPart<-fraction- return fractionalPart- fraction :: GenParser Char st String- fraction = do- _ <- char '.'- fractionalPart<-option "" int- return $ '.':fractionalPart--
+ src/Happstack/Server/Proxy.hs view
@@ -0,0 +1,92 @@+{-# LANGUAGE FlexibleContexts #-}+-- |Support for creating a proxy or reverse-proxy server+module Happstack.Server.Proxy where++import Control.Monad (MonadPlus(mzero), liftM)+import Control.Monad.Trans (MonadIO(liftIO))+import qualified Data.ByteString.Char8 as B+import Data.List (isPrefixOf)+import Data.Maybe (fromJust, fromMaybe)+import Happstack.Server.Monads (ServerMonad(askRq), FilterMonad, WebMonad, escape')+import Happstack.Server.Response (badGateway, toResponse)+import Happstack.Server.Client (getResponse)+import Happstack.Server.Types (Request(rqPaths, rqHeaders, rqPeer), Response, setHeader, getHeader)++-- | 'proxyServe' is for creating a part that acts as a proxy. The+-- sole argument @['String']@ is a list of allowed domains for+-- proxying. This matches the domain part of the request and the+-- wildcard * can be used. E.g.+--+-- * \"*\" to match anything.+--+-- * \"*.example.com\" to match anything under example.com+--+-- * \"example.com\" to match just example.com+--+--+-- TODO: annoyingly enough, this method eventually calls 'escape', so+-- any headers you set won't be used, and the computation immediately+-- ends.+proxyServe :: (MonadIO m, WebMonad Response m, ServerMonad m, MonadPlus m, FilterMonad Response m) => [String] -> m Response+proxyServe allowed = do+ rq <- askRq+ if cond rq then proxyServe' rq else mzero+ where+ cond rq+ | "*" `elem` allowed = True+ | domain `elem` allowed = True+ | superdomain `elem` wildcards =True+ | otherwise = False+ where+ domain = head (rqPaths rq)+ superdomain = tail $ snd $ break (=='.') domain+ wildcards = (map (drop 2) $ filter ("*." `isPrefixOf`) allowed)++-- | Take a proxy 'Request' and create a 'Response'. Your basic proxy+-- building block. See 'unproxify'.+--+-- TODO: this would be more useful if it didn\'t call 'escape'+-- (e.g. it let you modify the response afterwards, or set additional+-- headers)+proxyServe' :: (MonadIO m, FilterMonad Response m, WebMonad Response m) => Request-> m Response+proxyServe' rq = liftIO (getResponse (unproxify rq)) >>=+ either (badGateway . toResponse . show) escape'++-- | This is a reverse proxy implementation. See 'unrproxify'.+--+-- TODO: this would be more useful if it didn\'t call 'escape', just+-- like 'proxyServe''.+rproxyServe :: (ServerMonad m, WebMonad Response m, FilterMonad Response m, MonadIO m) =>+ String -- ^ defaultHost+ -> [(String, String)] -- ^ map to look up hostname mappings. For the reverse proxy+ -> m Response -- ^ the result is a 'ServerPartT' that will reverse proxy for you.+rproxyServe defaultHost list = + do rq <- askRq+ r <- liftIO (getResponse (unrproxify defaultHost list rq))+ either (badGateway . toResponse . show) (escape') r+++unproxify :: Request -> Request+unproxify rq = rq {rqPaths = tail $ rqPaths rq,+ rqHeaders = + forwardedFor $ forwardedHost $ + setHeader "host" (head $ rqPaths rq) $+ rqHeaders rq}+ where+ appendInfo hdr val = setHeader hdr (csv val $+ maybe "" B.unpack $+ getHeader hdr rq)+ forwardedFor = appendInfo "X-Forwarded-For" (fst $ rqPeer rq)+ forwardedHost = appendInfo "X-Forwarded-Host" + (B.unpack $ fromJust $ getHeader "host" rq)+ csv v "" = v+ csv v x = x++", " ++ v++unrproxify :: String -> [(String, String)] -> Request -> Request+unrproxify defaultHost list rq = + let host::String+ host = fromMaybe defaultHost $ flip lookup list =<< B.unpack `liftM` getHeader "host" rq + newrq = rq {rqPaths = host: rqPaths rq}+ in unproxify newrq++
+ src/Happstack/Server/Response.hs view
@@ -0,0 +1,289 @@+{-# LANGUAGE FlexibleContexts, TypeSynonymInstances, ScopedTypeVariables #-}+-- | Functions and classes related to generating a 'Response' and setting the response code. For detailed instruction see the Happstack Crash Course: <http://happstack.com/docs/crashcourse/HelloWorld.html#response_code>+module Happstack.Server.Response + ( -- * Converting values to a 'Response'+ ToMessage(..)+ , flatten+ , toResponseBS+ -- * Setting the Response Code+ , ok+ , noContent+ , internalServerError+ , badGateway+ , badRequest+ , unauthorized+ , forbidden+ , notFound+ , seeOther+ , found+ , movedPermanently+ , tempRedirect+ , setResponseCode+ , resp+ -- * Handling if-modified-since+ , ifModifiedSince+ ) where++import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Lazy.Char8 as L+import qualified Data.ByteString.Lazy.UTF8 as LU (fromString)+import qualified Data.Map as M+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import qualified Data.Text.Lazy as LT+import qualified Data.Text.Lazy.Encoding as LT+import Happstack.Server.Monads (FilterMonad(composeFilter))+import Happstack.Server.Types (Response(..), Request(..), nullRsFlags, getHeader, noContentLength, redirect, result, setHeader, setHeaderBS)+import Happstack.Server.SURI (ToSURI)+import System.Locale (defaultTimeLocale)+import System.Time (CalendarTime, formatCalendarTime)+import qualified Text.Blaze as Blaze+import qualified Text.Blaze.Renderer.Utf8 as Blaze+import Text.Html (Html, renderHtml)+import qualified Text.XHtml as XHtml (Html, renderHtml)++-- | A low-level function to build a 'Response' from a content-type+-- and a 'ByteString'.+--+-- Creates a 'Response' in a manner similar to the 'ToMessage' class,+-- but without requiring an instance declaration.+-- +-- example:+-- +-- > import Data.ByteString.Char8 as C+-- > import Data.ByteString.Lazy.Char8 as L+-- > import Happstack.Server+-- >+-- > main = simpleHTTP nullConf $ ok $ toResponseBS (C.pack "text/plain") (L.pack "hello, world")+--+-- (note: 'C.pack' and 'L.pack' only work for ascii. For unicode strings you would need to use @utf8-string@, @text@, or something similar to create a valid 'ByteString').+toResponseBS :: B.ByteString -- ^ content-type+ -> L.ByteString -- ^ response body+ -> Response+toResponseBS contentType message =+ let res = Response 200 M.empty nullRsFlags message Nothing+ in setHeaderBS (B.pack "Content-Type") contentType res+++-- | 'toResponse' will convert a value into a 'Response' body,+-- set the @content-type@, and set the default response code for that type.+--+-- Example:+--+-- > main = simpleHTTP nullConf $ toResponse "hello, world!"+--+-- will generate a 'Response' with the content-type @text/plain@,+-- the response code @200 OK@, and the body: @hello, world!@.+--+-- 'simpleHTTP' will call 'toResponse' automatically, so the above can be shortened to:+--+-- > main = simpleHTTP nullConf $ "hello, world!"+--+-- Minimal definition: 'toMessage' (and usually 'toContentType'). +class ToMessage a where+ toContentType :: a -> B.ByteString+ toContentType _ = B.pack "text/plain"+ toMessage :: a -> L.ByteString+ toMessage = error "Happstack.Server.SimpleHTTP.ToMessage.toMessage: Not defined"+ toResponse:: a -> Response+ toResponse val =+ let bs = toMessage val+ res = Response 200 M.empty nullRsFlags bs Nothing+ in setHeaderBS (B.pack "Content-Type") (toContentType val)+ res+{-+instance ToMessage [Element] where+ toContentType _ = B.pack "application/xml; charset=UTF-8"+ toMessage [el] = LU.fromString $ H.simpleDoc H.NoStyle $ toHaXmlEl el -- !! OPTIMIZE+ toMessage x = error ("Happstack.Server.SimpleHTTP 'instance ToMessage [Element]' Can't handle " ++ show x)+-}++instance ToMessage () where+ toContentType _ = B.pack "text/plain"+ toMessage () = L.empty++instance ToMessage String where+ toContentType _ = B.pack "text/plain; charset=UTF-8"+ toMessage = LU.fromString++instance ToMessage T.Text where+ toContentType _ = B.pack "text/plain; charset=UTF-8"+ toMessage t = L.fromChunks [T.encodeUtf8 t]++instance ToMessage LT.Text where+ toContentType _ = B.pack "text/plain; charset=UTF-8"+ toMessage = LT.encodeUtf8++instance ToMessage Integer where+ toMessage = toMessage . show++instance ToMessage a => ToMessage (Maybe a) where+ toContentType _ = toContentType (undefined :: a)+ toMessage Nothing = toMessage "nothing"+ toMessage (Just x) = toMessage x++instance ToMessage Html where+ toContentType _ = B.pack "text/html; charset=UTF-8"+ toMessage = LU.fromString . renderHtml++instance ToMessage XHtml.Html where+ toContentType _ = B.pack "text/html; charset=UTF-8"+ toMessage = LU.fromString . XHtml.renderHtml++instance ToMessage Blaze.Html where+ toContentType _ = B.pack "text/html; charset=UTF-8"+ toMessage = Blaze.renderHtml++instance ToMessage Response where+ toResponse = id+{-++-- This instances causes awful error messages. I am removing it and+-- seeing if anyone complains. I doubt they will.++instance (Xml a)=>ToMessage a where+ toContentType = toContentType . toXml+ toMessage = toMessage . toPublicXml+-}++-- toMessageM = toMessageM . toPublicXml++-- | alias for: @fmap toResponse@+--+-- turns @m a@ into @m 'Response'@ using 'toResponse'.+--+-- > main = simpleHTTP nullConf $ flatten $ do return "flatten me."+flatten :: (ToMessage a, Functor f) => f a -> f Response+flatten = fmap toResponse+++-- |Honor an @if-modified-since@ header in a 'Request'.+-- If the 'Request' includes the @if-modified-since@ header and the+-- 'Response' has not been modified, then return 304 (Not Modified),+-- otherwise return the 'Response'.+ifModifiedSince :: CalendarTime -- ^ mod-time for the 'Response' (MUST NOT be later than server's time of message origination)+ -> Request -- ^ incoming request (used to check for if-modified-since)+ -> Response -- ^ Response to send if there are modifications+ -> Response+ifModifiedSince modTime request response =+ let repr = formatCalendarTime defaultTimeLocale "%a, %d %b %Y %X GMT" modTime+ notmodified = getHeader "if-modified-since" request == Just (B.pack $ repr)+ in if notmodified+ then noContentLength $ result 304 "" -- Not Modified+ else setHeader "Last-modified" repr response++-- | Deprecated: use 'composeFilter'.+modifyResponse :: (FilterMonad a m) => (a -> a) -> m()+modifyResponse = composeFilter+{-# DEPRECATED modifyResponse "Use composeFilter" #-}++-- | Set an arbitrary return code in your response.+--+-- A filter for setting the response code. Generally you will use a+-- helper function like 'ok' or 'seeOther'.+-- +-- > main = simpleHTTP nullConf $ do setResponseCode 200+-- > return "Everything is OK"+-- +-- see also: 'resp'+setResponseCode :: FilterMonad Response m => + Int -- ^ response code+ -> m ()+setResponseCode code+ = composeFilter $ \r -> r{rsCode = code}++-- | Same as @'setResponseCode' status >> return val@.+-- +-- Use this if you want to set a response code that does not already+-- have a helper function. +-- +-- > main = simpleHTTP nullConf $ resp 200 "Everything is OK"+resp :: (FilterMonad Response m) => + Int -- ^ response code+ -> b -- ^ value to return+ -> m b+resp status val = setResponseCode status >> return val++-- | Respond with @200 OK@.+-- +-- > main = simpleHTTP nullConf $ ok "Everything is OK"+ok :: (FilterMonad Response m) => a -> m a+ok = resp 200++-- | Respond with @204 No Content@+--+-- A @204 No Content@ response may not contain a message-body. If you try to supply one, it will be dutifully ignored.+--+-- > main = simpleHTTP nullConf $ noContent "This will be ignored."+noContent :: (FilterMonad Response m) => a -> m a+noContent val = composeFilter (\r -> noContentLength (r { rsCode = 204, rsBody = L.empty })) >> return val++-- | Respond with @500 Internal Server Error@.+--+-- > main = simpleHTTP nullConf $ internalServerError "Sorry, there was an internal server error."+internalServerError :: (FilterMonad Response m) => a -> m a+internalServerError = resp 500++-- | Responds with @502 Bad Gateway@.+--+-- > main = simpleHTTP nullConf $ badGateway "Bad Gateway."+badGateway :: (FilterMonad Response m) => a -> m a+badGateway = resp 502++-- | Respond with @400 Bad Request@.+--+-- > main = simpleHTTP nullConf $ badRequest "Bad Request."+badRequest :: (FilterMonad Response m) => a -> m a+badRequest = resp 400++-- | Respond with @401 Unauthorized@.+--+-- > main = simpleHTTP nullConf $ unauthorized "You are not authorized."+unauthorized :: (FilterMonad Response m) => a -> m a+unauthorized = resp 401++-- | Respond with @403 Forbidden@.+--+-- > main = simpleHTTP nullConf $ forbidden "Sorry, it is forbidden."+forbidden :: (FilterMonad Response m) => a -> m a+forbidden = resp 403++-- | Respond with @404 Not Found@.+-- +-- > main = simpleHTTP nullConf $ notFound "What you are looking for has not been found."+notFound :: (FilterMonad Response m) => a -> m a+notFound = resp 404++-- | Respond with @303 See Other@.+--+-- > main = simpleHTTP nullConf $ seeOther "http://example.org/" "What you are looking for is now at http://example.org/"+--+-- NOTE: The second argument of 'seeOther' is the message body which will sent to the browser. According to the HTTP 1.1 spec,+--+-- @the entity of the response SHOULD contain a short hypertext note with a hyperlink to the new URI(s).@+--+-- This is because pre-HTTP\/1.1 user agents do not support 303. However, in practice you can probably just use @\"\"@ as the second argument.+seeOther :: (FilterMonad Response m, ToSURI uri) => uri -> res -> m res+seeOther uri res = do modifyResponse $ redirect 303 uri+ return res++-- | Respond with @302 Found@.+-- +-- You probably want 'seeOther'. This method is not in popular use anymore, and is generally treated like 303 by most user-agents anyway.+found :: (FilterMonad Response m, ToSURI uri) => uri -> res -> m res+found uri res = do modifyResponse $ redirect 302 uri+ return res++-- | Respond with @301 Moved Permanently@.+--+-- > main = simpleHTTP nullConf $ movedPermanently "http://example.org/" "What you are looking for is now at http://example.org/"+movedPermanently :: (FilterMonad Response m, ToSURI a) => a -> res -> m res+movedPermanently uri res = do modifyResponse $ redirect 301 uri+ return res++-- | Respond with @307 Temporary Redirect@.+--+-- > main = simpleHTTP nullConf $ tempRedirect "http://example.org/" "What you are looking for is temporarily at http://example.org/"+tempRedirect :: (FilterMonad Response m, ToSURI a) => a -> res -> m res+tempRedirect val res = do modifyResponse $ redirect 307 val+ return res
+ src/Happstack/Server/Routing.hs view
@@ -0,0 +1,206 @@+{-# LANGUAGE FlexibleInstances, PatternGuards, ScopedTypeVariables, TypeSynonymInstances #-}+-- | Route an incoming 'Request' to a handler. For more in-depth documentation see this section of the Happstack Crash Course: <http://happstack.com/docs/crashcourse/RouteFilters.html>+module Happstack.Server.Routing + ( -- * Route by request method+ methodM+ , methodOnly+ , methodSP+ , method+ , MatchMethod(..)+ -- * Route by pathInfo+ , dir+ , dirs+ , nullDir+ , trailingSlash+ , anyPath+ , FromReqURI(..)+ , path+ , uriRest+ -- * Route by host+ , host+ , withHost+ -- * Route by (Request -> Bool)+ , guardRq+ ) where++import Control.Monad (MonadPlus(mzero,mplus), unless)+import qualified Data.ByteString.Char8 as B+import Happstack.Server.Monads (ServerPartT, ServerMonad(..))+import Happstack.Server.Internal.Monads (WebT, anyRequest)+import Happstack.Server.Types (Request(..), Method(..), getHeader, rqURL)+import Happstack.Util.Common (readM)+import System.FilePath (makeRelative, splitDirectories)++-- | instances of this class provide a variety of ways to match on the 'Request' method.+--+-- Examples+-- +-- > methodM GET -- match GET+-- > methodM [HEAD, GET] -- match HEAD or GET+-- > methodM (not . (==) DELETE) -- match any method except DELETE+-- > methodM () -- match any method+class MatchMethod m where matchMethod :: m -> Method -> Bool+instance MatchMethod Method where matchMethod m = (== m)+instance MatchMethod [Method] where matchMethod methods = (`elem` methods)+instance MatchMethod (Method -> Bool) where matchMethod f = f+instance MatchMethod () where matchMethod () _ = True++-- | This class is used by 'path' to parse a path component into a+-- value. +-- +-- The instances for number types ('Int', 'Float', etc) use 'readM' to+-- parse the path component.+--+-- The instance for 'String', on the other hand, returns the+-- unmodified path component.+--+-- See the following section of the Happstack Crash Course for+-- detailed instructions using and extending 'FromReqURI':+--+-- <http://www.happstack.com/docs/crashcourse/RouteFilters.html#FromReqURI>++class FromReqURI a where+ fromReqURI :: String -> Maybe a++instance FromReqURI String where fromReqURI = Just+instance FromReqURI Int where fromReqURI = readM+instance FromReqURI Integer where fromReqURI = readM+instance FromReqURI Float where fromReqURI = readM+instance FromReqURI Double where fromReqURI = readM+++-------------------------------------+-- guards++-- | Guard using an arbitrary function on the 'Request'.+guardRq :: (ServerMonad m, MonadPlus m) => (Request -> Bool) -> m ()+guardRq f = do+ rq <- askRq+ unless (f rq) mzero++-- | Guard against the method. This function also guards against+-- *any remaining path segments*. See 'methodOnly' for the version+-- that guards only by method.+--+-- Example:+--+-- > handler :: ServerPart Response+-- > handler =+-- > do methodM [GET, HEAD]+-- > ...+methodM :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m ()+methodM meth = methodOnly meth >> nullDir++-- | Guard against the method only (as opposed to 'methodM').+--+-- Example:+--+-- > handler :: ServerPart Response+-- > handler =+-- > do methodOnly [GET, HEAD]+-- > ...+methodOnly :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m ()+methodOnly meth = guardRq $ \rq -> matchMethod meth (rqMethod rq)++-- | Guard against the method. Note, this function also guards against+-- any remaining path segments. Similar to 'methodM' but with a different type signature.+--+-- Example:+--+-- > handler :: ServerPart Response+-- > handler = methodSP [GET, HEAD] $ subHandler+methodSP :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m b-> m b+methodSP m handle = methodM m >> handle++-- | Guard against the method. Note, this function also guards against any+-- remaining path segments.+--+-- DEPRECATED: Use 'methodSP', 'methodM', or 'methodOnly'+method :: (MatchMethod method, Monad m) => method -> WebT m a -> ServerPartT m a+method m handle = methodSP m (anyRequest handle)+{-# DEPRECATED method "you should be able to use methodSP" #-}++-- | guard which only succeeds if there are no remaining path segments+--+-- Often used if you want to explicitly assign a route for '/'+-- +nullDir :: (ServerMonad m, MonadPlus m) => m ()+nullDir = guardRq $ \rq -> null (rqPaths rq)++-- | Pop a path element and run the supplied handler if it matches the+-- given string.+-- +-- > handler :: ServerPart Response+-- > handler = dir "foo" $ dir "bar" $ subHandler+-- +-- The path element can not contain \'/\'. See also 'dirs'.+dir :: (ServerMonad m, MonadPlus m) => String -> m a -> m a+dir staticPath handle =+ do+ rq <- askRq+ case rqPaths rq of+ (p:xs) | p == staticPath -> localRq (\newRq -> newRq{rqPaths = xs}) handle+ _ -> mzero+ +-- | Guard against a 'FilePath'. Unlike 'dir' the 'FilePath' may+-- contain \'/\'. If the guard succeeds, the matched elements will be+-- popped from the directory stack.+--+-- > dirs "foo/bar" $ ...+-- +-- See also: 'dir'.+dirs :: (ServerMonad m, MonadPlus m) => FilePath -> m a -> m a +dirs fp m = + do let parts = splitDirectories (makeRelative "/" fp) + foldr dir m parts++-- | Guard against the host.+--+-- This matches against the @host@ header specified in the incoming 'Request'.+--+-- Can be used to support virtual hosting, <http://en.wikipedia.org/wiki/Virtual_hosting>+-- +-- see also: 'withHost'+host :: (ServerMonad m, MonadPlus m) => String -> m a -> m a+host desiredHost handle =+ do rq <- askRq+ case getHeader "host" rq of+ (Just hostBS) | desiredHost == B.unpack hostBS -> handle+ _ -> mzero++-- | Lookup the @host@ header in the incoming request and pass it to the handler.+--+-- see also: 'host'+withHost :: (ServerMonad m, MonadPlus m) => (String -> m a) -> m a+withHost handle =+ do rq <- askRq+ case getHeader "host" rq of+ (Just hostBS) -> handle (B.unpack hostBS)+ _ -> mzero+++-- | Pop a path element and parse it using the 'fromReqURI' in the+-- 'FromReqURI' class.+path :: (FromReqURI a, MonadPlus m, ServerMonad m) => (a -> m b) -> m b+path handle = do+ rq <- askRq+ case rqPaths rq of+ (p:xs) | Just a <- fromReqURI p+ -> localRq (\newRq -> newRq{rqPaths = xs}) (handle a)+ _ -> mzero++-- | Grab the rest of the URL (dirs + query) and passes it to your+-- handler.+uriRest :: (ServerMonad m) => (String -> m a) -> m a+uriRest handle = askRq >>= handle . rqURL++-- | Pop any path element and run the handler.+-- +-- Succeeds if a path component was popped. Fails is the remaining path was empty.+anyPath :: (ServerMonad m, MonadPlus m) => m r -> m r+anyPath x = path $ (\(_::String) -> x)++-- | Guard which checks that the Request URI ends in @\'\/\'@. Useful+-- for distinguishing between @foo@ and @foo/@+trailingSlash :: (ServerMonad m, MonadPlus m) => m ()+trailingSlash = guardRq $ \rq -> (last (rqUri rq)) == '/'
+ src/Happstack/Server/RqData.hs view
@@ -0,0 +1,568 @@+{-# LANGUAGE DeriveDataTypeable, GeneralizedNewtypeDeriving, FlexibleInstances, MultiParamTypeClasses #-}+-- | Functions for extracting values from the query string, form data, cookies, etc. +--+-- For in-depth documentation see the following section of the Happstack Crash Course:+--+-- <http://happstack.com/docs/crashcourse/RqData.html>+module Happstack.Server.RqData + ( -- * Looking up keys+ -- ** Form Values and Query Parameters+ look+ , looks+ , lookBS+ , lookBSs+ , lookRead+ , lookReads+ , lookFile+ , lookPairs+ , lookPairsBS+ -- ** Cookies+ , lookCookie+ , lookCookieValue+ , readCookieValue+ -- ** low-level+ , lookInput+ , lookInputs+ -- * Filters+ -- The look* functions normally search the QUERY_STRING and the Request+ -- body for matches keys. + , body+ , queryString+ -- * Validation and Parsing+ , checkRq+ , checkRqM + , readRq+ -- * Handling POST\/PUT Requests+ , decodeBody+ -- ** Body Policy+ , BodyPolicy(..)+ , defaultBodyPolicy+ -- * RqData Monad & Error Reporting+ , RqData+ , mapRqData+ , Errors(..) + -- ** Using RqData with ServerMonad+ , getDataFn+ , withDataFn+ , FromData(..)+ , getData+ , withData+ -- * HasRqData class+ , RqEnv+ , HasRqData(askRqEnv, localRqEnv,rqDataError)+ ) where++import Control.Applicative (Applicative((<*>), pure), Alternative((<|>), empty), WrappedMonad(WrapMonad, unwrapMonad), (<$>))+import Control.Concurrent.MVar (newMVar)+import Control.Monad (MonadPlus(mzero), liftM)+import Control.Monad.Reader (ReaderT(ReaderT, runReaderT), MonadReader(ask, local), mapReaderT)+import Control.Monad.Error (Error(noMsg, strMsg))+import Control.Monad.Trans (MonadIO(..))+import qualified Data.ByteString.Lazy.Char8 as L+import qualified Data.ByteString.Lazy.UTF8 as LU+import Data.Char (toLower)+import Data.Either (partitionEithers)+import Data.Generics (Data, Typeable)+import Data.Maybe (fromMaybe, fromJust)+import Data.Monoid (Monoid(mempty, mappend, mconcat))+import Data.Text.Lazy (Text)+import qualified Data.Text.Lazy.Encoding as Text+import Happstack.Server.Cookie (Cookie (cookieValue))+import Happstack.Server.Internal.Monads (ServerMonad(askRq, localRq), ServerPartT)+import Happstack.Server.Types (ContentType(..), Input(inputValue, inputFilename, inputContentType), Request(rqInputsQuery, rqInputsBody, rqCookies, rqMethod), Method(POST,PUT), readInputsBody)+import Happstack.Server.Internal.MessageWrap (BodyPolicy(..), bodyInput, defaultBodyPolicy)++newtype ReaderError r e a = ReaderError { unReaderError :: ReaderT r (Either e) a }+ deriving (Functor, Monad, MonadPlus)++instance (Error e) => MonadReader r (ReaderError r e) where+ ask = ReaderError ask+ local f m = ReaderError $ local f (unReaderError m)++instance (Monoid e, Error e) => Applicative (ReaderError r e) where+ pure = return+ (ReaderError (ReaderT f)) <*> (ReaderError (ReaderT a)) + = ReaderError $ ReaderT $ \env -> (f env) `apEither` (a env)++instance (Monoid e, Error e) => Alternative (ReaderError r e) where+ empty = unwrapMonad empty+ f <|> g = unwrapMonad $ (WrapMonad f) <|> (WrapMonad g)++apEither :: (Monoid e) => Either e (a -> b) -> Either e a -> Either e b+apEither (Left errs1) (Left errs2) = Left (errs1 `mappend` errs2)+apEither (Left errs) _ = Left errs+apEither _ (Left errs) = Left errs+apEither (Right f) (Right a) = Right (f a)++-- | a list of errors+newtype Errors a = Errors { unErrors :: [a] }+ deriving (Eq, Ord, Show, Read, Data, Typeable)++instance Monoid (Errors a) where+ mempty = Errors []+ (Errors x) `mappend` (Errors y) = Errors (x ++ y)+ mconcat errs = Errors $ concatMap unErrors errs++instance Error (Errors String) where+ noMsg = Errors []+ strMsg str = Errors [str]++mapReaderErrorT :: (Either e a -> Either e' b) -> (ReaderError r e a) -> (ReaderError r e' b)+mapReaderErrorT f m = ReaderError $ mapReaderT f (unReaderError m)++readerError :: (Monoid e, Error e) => e -> ReaderError r e b+readerError e = mapReaderErrorT ((Left e) `apEither`) (return ())++runReaderError :: ReaderError r e a -> r -> Either e a+runReaderError = runReaderT . unReaderError++-- | the environment used to lookup query parameters. It consists of+-- the triple: (query string inputs, body inputs, cookie inputs)+type RqEnv = ([(String, Input)], [(String, Input)], [(String, Cookie)])++-- | An applicative functor and monad for looking up key/value pairs+-- in the QUERY_STRING, Request body, and cookies.+newtype RqData a = RqData { unRqData :: ReaderError RqEnv (Errors String) a }+ deriving (Functor, Monad, MonadPlus, Applicative, Alternative, MonadReader RqEnv )++-- | A class for monads which contain a 'RqEnv'+class HasRqData m where+ askRqEnv :: m RqEnv+ localRqEnv :: (RqEnv -> RqEnv) -> m a -> m a+ -- | lift some 'Errors' into 'RqData'+ rqDataError :: Errors String -> m a ++instance HasRqData RqData where+ askRqEnv = RqData ask+ localRqEnv f (RqData re) = RqData $ local f re+ rqDataError e = mapRqData ((Left e) `apEither`) (return ())++-- instance (MonadPlus m, MonadIO m, ServerMonad m) => (HasRqData m) where+instance (MonadIO m) => HasRqData (ServerPartT m) where+ askRqEnv =+ do rq <- askRq+ mbi <- liftIO $ if ((rqMethod rq) == POST) || ((rqMethod rq) == PUT)+ then readInputsBody rq+ else return (Just [])+ case mbi of+ Nothing -> fail "askRqEnv failed because the request body has not been decoded yet. Try using 'decodeBody'."+ (Just bi) -> return (rqInputsQuery rq, bi, rqCookies rq)+ rqDataError e = mzero+ localRqEnv f m =+ do rq <- askRq+ b <- liftM (fromMaybe []) $ liftIO $ readInputsBody rq+ let (q', b', c') = f (rqInputsQuery rq, b, rqCookies rq)+ bv <- liftIO $ newMVar b'+ let rq' = rq { rqInputsQuery = q'+ , rqInputsBody = bv+ , rqCookies = c'+ }+ localRq (const rq') m++-- | apply 'RqData a' to a 'RqEnv'+--+-- see also: 'getData', 'getDataFn', 'withData', 'withDataFn', 'RqData', 'getDataFn'+runRqData :: RqData a -> RqEnv -> Either [String] a+runRqData rqData rqEnv =+ either (Left . unErrors) Right $ runReaderError (unRqData rqData) rqEnv++-- | transform the result of 'RqData a'.+--+-- This is similar to 'fmap' except it also allows you to modify the+-- 'Errors' not just 'a'.+mapRqData :: (Either (Errors String) a -> Either (Errors String) b) -> RqData a -> RqData b+mapRqData f m = RqData $ ReaderError $ mapReaderT f (unReaderError (unRqData m))++-- | use 'read' to convert a 'String' to a value of type 'a'+--+-- > look "key" `checkRq` (readRq "key")+-- +-- use with 'checkRq'+readRq :: (Read a) => + String -- ^ name of key (only used for error reporting)+ -> String -- ^ 'String' to 'read'+ -> Either String a -- ^ 'Left' on error, 'Right' on success+readRq key val =+ case reads val of+ [(a,[])] -> Right a+ _ -> Left $ "readRq failed while parsing key: " ++ key ++ " which has the value: " ++ val++-- | convert or validate a value+--+-- This is similar to 'fmap' except that the function can fail by+-- returning Left and an error message. The error will be propagated+-- by calling 'rqDataError'.+--+-- This function is useful for a number of things including:+-- +-- (1) Parsing a 'String' into another type+--+-- (2) Checking that a value meets some requirements (for example, that is an Int between 1 and 10).+--+-- Example usage at:+--+-- <http://happstack.com/docs/crashcourse/RqData.html#rqdatacheckrq>+checkRq :: (Monad m, HasRqData m) => m a -> (a -> Either String b) -> m b+checkRq rq f =+ do a <- rq+ case f a of+ (Left e) -> rqDataError (strMsg e)+ (Right b) -> return b++-- | like 'checkRq' but the check function can be monadic+checkRqM :: (Monad m, HasRqData m) => m a -> (a -> m (Either String b)) -> m b+checkRqM rq f =+ do a <- rq+ b <- f a+ case b of+ (Left e) -> rqDataError (strMsg e)+ (Right b) -> return b++-- | Used by 'withData' and 'getData'. Make your preferred data+-- type an instance of 'FromData' to use those functions.+class FromData a where+ fromData :: RqData a+{-+instance (Eq a,Show a,Xml a,G.Data a) => FromData a where+ fromData = do mbA <- lookPairs >>= return . normalize . fromPairs+ case mbA of+ Just a -> return a+ Nothing -> fail "FromData G.Data failure"+-- fromData = lookPairs >>= return . normalize . fromPairs+-}+instance (FromData a, FromData b) => FromData (a,b) where+ fromData = (,) <$> fromData <*> fromData++instance (FromData a, FromData b, FromData c) => FromData (a,b,c) where+ fromData = (,,) <$> fromData <*> fromData <*> fromData++instance (FromData a, FromData b, FromData c, FromData d) => FromData (a,b,c,d) where+ fromData = (,,,) <$> fromData <*> fromData <*> fromData <*> fromData++instance FromData a => FromData (Maybe a) where+ fromData = (Just <$> fromData) <|> (pure Nothing)++-- | similar to 'Data.List.lookup' but returns all matches not just the first+lookups :: (Eq a) => a -> [(a, b)] -> [b]+lookups a = map snd . filter ((a ==) . fst)++-- | Gets the first matching named input parameter+-- +-- Searches the QUERY_STRING followed by the Request body.+--+-- see also: 'lookInputs'+lookInput :: (Monad m, HasRqData m) => String -> m Input+lookInput name+ = do (query, body, _cookies) <- askRqEnv+ case lookup name (query ++ body) of+ Just i -> return $ i+ Nothing -> rqDataError (strMsg $ "Parameter not found: " ++ name)++-- | Gets all matches for the named input parameter+-- +-- Searches the QUERY_STRING followed by the Request body.+--+-- see also: 'lookInput'+lookInputs :: (Monad m, HasRqData m) => String -> m [Input]+lookInputs name+ = do (query, body, _cookies) <- askRqEnv+ return $ lookups name (query ++ body)++-- | Gets the first matching named input parameter as a lazy 'ByteString'+--+-- Searches the QUERY_STRING followed by the Request body.+--+-- see also: 'lookBSs'+lookBS :: (Functor m, Monad m, HasRqData m) => String -> m L.ByteString+lookBS n = + do i <- fmap inputValue (lookInput n)+ case i of+ (Left fp) -> rqDataError $ (strMsg $ "lookBS: " ++ n ++ " is a file.")+ (Right bs) -> return bs++-- | Gets all matches for the named input parameter as lazy 'ByteString's+--+-- Searches the QUERY_STRING followed by the Request body.+--+-- see also: 'lookBS'+lookBSs :: (Functor m, Monad m, HasRqData m) => String -> m [L.ByteString]+lookBSs n = + do is <- fmap (map inputValue) (lookInputs n)+ case partitionEithers is of+ ([], bs) -> return bs+ (fp, _) -> rqDataError (strMsg $ "lookBSs: " ++ n ++ " is a file.")++-- | Gets the first matching named input parameter as a 'String'+--+-- Searches the QUERY_STRING followed by the Request body.+--+-- This function assumes the underlying octets are UTF-8 encoded.+--+-- Example:+--+-- > handler :: ServerPart Response+-- > handler =+-- > do foo <- look "foo"+-- > ok $ toResponse $ "foo = " ++ foo+--+-- see also: 'looks', 'lookBS', and 'lookBSs'+look :: (Functor m, Monad m, HasRqData m) => String -> m String+look = fmap LU.toString . lookBS++-- | Gets all matches for the named input parameter as 'String's+--+-- Searches the QUERY_STRING followed by the Request body.+--+-- This function assumes the underlying octets are UTF-8 encoded.+--+-- see also: 'look' and 'lookBSs'+looks :: (Functor m, Monad m, HasRqData m) => String -> m [String]+looks = fmap (map LU.toString) . lookBSs++-- | Gets the first matching named input parameter as a lazy 'Text'+--+-- Searches the QUERY_STRING followed by the Request body.+--+-- This function assumes the underlying octets are UTF-8 encoded.+--+-- see also: 'lookTexts', 'look', 'looks', 'lookBS', and 'lookBSs'+lookText :: (Functor m, Monad m, HasRqData m) => String -> m Text+lookText = fmap Text.decodeUtf8 . lookBS++-- | Gets all matches for the named input parameter as lazy 'Text's+--+-- Searches the QUERY_STRING followed by the Request body.+--+-- This function assumes the underlying octets are UTF-8 encoded.+--+-- see also: 'lookText', 'looks' and 'lookBSs'+lookTexts :: (Functor m, Monad m, HasRqData m) => String -> m [Text]+lookTexts = fmap (map Text.decodeUtf8) . lookBSs++-- | Gets the named cookie+-- the cookie name is case insensitive+lookCookie :: (Monad m, HasRqData m) => String -> m Cookie+lookCookie name+ = do (_query,_body, cookies) <- askRqEnv+ case lookup (map toLower name) cookies of -- keys are lowercased+ Nothing -> rqDataError $ strMsg $ "lookCookie: cookie not found: " ++ name+ Just c -> return c++-- | gets the named cookie as a string+lookCookieValue :: (Functor m, Monad m, HasRqData m) => String -> m String+lookCookieValue = fmap cookieValue . lookCookie++-- | gets the named cookie as the requested Read type+readCookieValue :: (Functor m, Monad m, HasRqData m, Read a) => String -> m a+readCookieValue name = fmap cookieValue (lookCookie name) `checkRq` (readRq name)++-- | Gets the first matching named input parameter and decodes it using 'Read'+--+-- Searches the QUERY_STRING followed by the Request body.+--+-- This function assumes the underlying octets are UTF-8 encoded.+--+-- see also: 'lookReads'+lookRead :: (Functor m, Monad m, HasRqData m, Read a) => String -> m a+lookRead name = look name `checkRq` (readRq name)++-- | Gets all matches for the named input parameter and decodes them using 'Read'+--+-- Searches the QUERY_STRING followed by the Request body.+--+-- This function assumes the underlying octets are UTF-8 encoded.+--+-- see also: 'lookReads'+lookReads :: (Functor m, Monad m, HasRqData m, Read a) => String -> m [a]+lookReads name = + do vals <- looks name+ mapM (\v -> (return v) `checkRq` (readRq name)) vals++-- | Gets the first matching named file+--+-- Files can only appear in the request body. Additionally, the form+-- must set enctype=\"multipart\/form-data\".+--+-- This function returns a tuple consisting of:+-- +-- (1) The temporary location of the uploaded file+--+-- (2) The local filename supplied by the browser+--+-- (3) The content-type supplied by the browser+--+-- NOTE: You must move the file from the temporary location before the+-- 'Response' is sent. The temporary files are automatically removed+-- after the 'Response' is sent.+lookFile :: (Monad m, HasRqData m) =>+ String -- ^ name of input field to search for+ -> m (FilePath, FilePath, ContentType) -- ^ (temporary file location, uploaded file name, content-type)+lookFile n =+ do i <- lookInput n+ case inputValue i of+ (Right _) -> rqDataError $ (strMsg $ "lookFile: " ++ n ++ " was found but is not a file.")+ (Left fp) -> return (fp, fromJust $ inputFilename i, inputContentType i)++-- | gets all the input parameters, and converts them to a 'String'+--+-- The results will contain the QUERY_STRING followed by the Request+-- body.+--+-- This function assumes the underlying octets are UTF-8 encoded.+--+-- see also: 'lookPairsBS'+lookPairs :: (Monad m, HasRqData m) => m [(String, Either FilePath String)]+lookPairs = + do (query, body, _cookies) <- askRqEnv+ return $ map (\(n,vbs)->(n, (\e -> case e of Left fp -> Left fp ; Right bs -> Right (LU.toString bs)) $ inputValue vbs)) (query ++ body)++-- | gets all the input parameters+--+-- The results will contain the QUERY_STRING followed by the Request+-- body.+--+-- see also: 'lookPairs'+lookPairsBS :: (Monad m, HasRqData m) => m [(String, Either FilePath L.ByteString)]+lookPairsBS = + do (query, body, _cookies) <- askRqEnv+ return $ map (\(n,vbs) -> (n, inputValue vbs)) (query ++ body)++-- | The POST\/PUT body of a Request is not received or decoded unless+-- this function is invoked. +--+-- It is an error to try to use the look functions for a POST\/PUT+-- request with out first calling this function.+--+-- It is ok to call 'decodeBody' at the beginning of every request:+--+-- > main = simpleHTTP nullConf $ +-- > do decodeBody (defaultBodyPolicy "/tmp/" 4096 4096 4096)+-- > handlers+--+-- You can achieve finer granularity quotas by calling 'decodeBody'+-- with different values in different handlers.+--+-- Only the first call to 'decodeBody' will have any effect. Calling+-- it a second time, even with different quota values, will do+-- nothing.+decodeBody :: (ServerMonad m, MonadPlus m, MonadIO m) => BodyPolicy -> m ()+decodeBody bp =+ do rq <- askRq+ (_, me) <- bodyInput bp rq+ case me of+ Nothing -> return ()+ Just e -> fail e -- FIXME: is this the best way to report the error++-- | run 'RqData' in a 'ServerMonad'.+--+-- Example: a simple @GET@ or @POST@ variable based authentication+-- guard. It handles the request with 'errorHandler' if+-- authentication fails.+--+-- > data AuthCredentials = AuthCredentials { username :: String, password :: String }+-- >+-- > isValid :: AuthCredentials -> Bool+-- > isValid = const True+-- >+-- > myRqData :: RqData AuthCredentials+-- > myRqData = do+-- > username <- look "username"+-- > password <- look "password"+-- > return (AuthCredentials username password)+-- >+-- > checkAuth :: (String -> ServerPart Response) -> ServerPart Response+-- > checkAuth errorHandler = do+-- > d <- getDataFn myRqData+-- > case d of+-- > (Left e) -> errorHandler (unlines e)+-- > (Right a) | isValid a -> mzero+-- > (Right a) | otherwise -> errorHandler "invalid"+--+-- NOTE: you must call 'decodeBody' prior to calling this function if+-- the request method is POST or PUT.+getDataFn :: (HasRqData m, ServerMonad m, MonadIO m) => + RqData a -- ^ 'RqData' monad to evaluate+ -> m (Either [String] a) -- ^ return 'Left' errors or 'Right' a+getDataFn rqData =+ do rqEnv <- askRqEnv+ return (runRqData rqData rqEnv)++-- | similar to 'getDataFn', except it calls a sub-handler on success+-- or 'mzero' on failure.+-- +-- NOTE: you must call 'decodeBody' prior to calling this function if+-- the request method is POST or PUT.+withDataFn :: (HasRqData m, MonadIO m, MonadPlus m, ServerMonad m) => RqData a -> (a -> m r) -> m r+withDataFn fn handle = getDataFn fn >>= either (const mzero) handle++-- | A variant of 'getDataFn' that uses 'FromData' to chose your+-- 'RqData' for you. The example from 'getData' becomes:+-- +-- > data AuthCredentials = AuthCredentials { username :: String, password :: String }+-- >+-- > isValid :: AuthCredentials -> Bool+-- > isValid = const True+-- >+-- > myRqData :: RqData AuthCredentials+-- > myRqData = do+-- > username <- look "username"+-- > password <- look "password"+-- > return (AuthCredentials username password)+-- >+-- > instance FromData AuthCredentials where+-- > fromData = myRqData+-- >+-- > checkAuth :: (String -> ServerPart Response) -> ServerPart Response+-- > checkAuth errorHandler = do+-- > d <- getData+-- > case d of+-- > (Left e) -> errorHandler (unlines e)+-- > (Right a) | isValid a -> mzero+-- > (Right a) | otherwise -> errorHandler "invalid"+--+-- NOTE: you must call 'decodeBody' prior to calling this function if+-- the request method is POST or PUT.+getData :: (HasRqData m, MonadIO m, ServerMonad m, FromData a) => m (Either [String] a)+getData = getDataFn fromData++-- | similar to 'getData' except it calls a subhandler on success or 'mzero' on failure.+--+-- NOTE: you must call 'decodeBody' prior to calling this function if+-- the request method is POST or PUT.+withData :: (HasRqData m, MonadIO m, FromData a, MonadPlus m, ServerMonad m) => (a -> m r) -> m r+withData = withDataFn fromData++-- | limit the scope to the Request body+--+-- > handler :: ServerPart Response+-- > handler =+-- > do foo <- body $ look "foo"+-- > ok $ toResponse $ "foo = " ++ foo+body :: (HasRqData m) => m a -> m a+body rqData = localRqEnv f rqData+ where+ f (_query, body, _cookies) = ([], body, [])++-- | limit the scope to the QUERY_STRING+--+-- > handler :: ServerPart Response+-- > handler =+-- > do foo <- queryString $ look "foo"+-- > ok $ toResponse $ "foo = " ++ foo+queryString :: (HasRqData m) => m a -> m a+queryString rqData = localRqEnv f rqData+ where+ f (query, _body, _cookies) = (query, [], [])++right :: (MonadPlus m) => Either a b -> m b+right (Right a) = return a+right (Left e) = mzero++bytestring :: (HasRqData m) => m a -> m a+bytestring rqData = localRqEnv f rqData+ where+ f (query, body, cookies) = (filter bsf query, filter bsf body, cookies)+ bsf (_, i) =+ case inputValue i of+ (Left _fp) -> False+ (Right _bs) -> True
src/Happstack/Server/SURI.hs view
@@ -1,5 +1,5 @@ {-# LANGUAGE TypeSynonymInstances, DeriveDataTypeable #-}-+-- | A wrapper and type class so that functions like 'seeOther' can take a URI which is represented by a 'String', 'URI.URI', or other instance of 'ToSURI'. module Happstack.Server.SURI where import Data.Maybe import Data.Generics
src/Happstack/Server/SimpleHTTP.hs view
@@ -15,1491 +15,166 @@ ----------------------------------------------------------------------------- -- | -- Module : Happstack.Server.SimpleHTTP--- Copyright : (c) Happstack.com 2009; (c) HAppS Inc 2007--- License : BSD-like------ Maintainer : lemmih@vo.com--- Stability : provisional--- Portability : requires mtl------ SimpleHTTP provides a back-end independent API for handling HTTP requests.------ By default, the built-in HTTP server will be used. However, other back-ends--- like CGI\/FastCGI can be used if so desired.------ So the general nature of 'simpleHTTP' is just what you'd expect--- from a web application container. First you figure out which function is--- going to process your request, process the request to generate a response,--- then return that response to the client. The web application container is--- started with 'simpleHTTP', which takes a configuration and a--- response-building structure ('ServerPartT' which I'll return to in a--- moment), picks the first handler that is willing to accept the request, and--- passes the request in to the handler. A simple "hello world" style Happstack--- simpleHTTP server looks like:------ > main = simpleHTTP nullConf $ return "Hello World!"------ @simpleHTTP nullConf@ creates a HTTP server on port 8000.--- return \"Hello World!\" creates a 'ServerPartT' that just returns that text.------ 'ServerPartT' is the basic response builder. As you might expect, it's a--- container for a function that takes a Request and converts it to a response--- suitable for sending back to the server. Most of the time though you don't--- even need to worry about that as 'ServerPartT' hides almost all the machinery--- for building your response by exposing a few type classes.------ 'ServerPartT' is a pretty rich monad. You can interact with your request,--- your response, do IO, etc. Here is a do block that validates basic--- authentication. It takes a realm name as a string, a Map of username to--- password and a server part to run if authentication fails.------ 'basicAuth' acts like a guard, and only produces a response when--- authentication fails. So put it before any 'ServerPartT' for which you want to demand--- authentication, in any collection of 'ServerPartT's.------ > main = simpleHTTP nullConf $ myAuth, return "Hello World!"--- > where--- > myAuth = basicAuth' "Test"--- > (M.fromList [("hello", "world")]) (return "Login Failed")------ > basicAuth' realmName authMap unauthorizedPart =--- > do--- > let validLogin name pass = M.lookup name authMap == Just pass--- > let parseHeader = break (':'==) . Base64.decode . B.unpack . B.drop 6--- > authHeader <- getHeaderM "authorization"--- > case authHeader of--- > Nothing -> err--- > Just x -> case parseHeader x of--- > (name, ':':pass) | validLogin name pass -> mzero--- > | otherwise -> err--- > _ -> err--- > where--- > err = do--- > unauthorized ()--- > setHeaderM headerName headerValue--- > unauthorizedPart--- > headerValue = "Basic realm=\"" ++ realmName ++ "\""--- > headerName = "WWW-Authenticate"------ Here is another example that uses 'liftIO' to embed IO in a request process:------ > main = simpleHTTP nullConf $ myPart--- > myPart = do--- > line <- liftIO $ do -- IO--- > putStr "return? "--- > getLine--- > when (take 2 line /= "ok") $ (notfound () >> return "refused")--- > return "Hello World!"------ This example will ask in the console \"return? \" if you type \"ok\" it will--- show \"Hello World!\" and if you type anything else it will return a 404.----------------------------------------------------------------------------------module Happstack.Server.SimpleHTTP- ( module Happstack.Server.HTTP.Types- , module Happstack.Server.Cookie- -- * SimpleHTTP- , simpleHTTP- , simpleHTTP'- , simpleHTTP''- , simpleHTTPWithSocket- , simpleHTTPWithSocket'- , bindPort- , parseConfig- -- * ServerPartT- , ServerPartT(..)- , ServerPart- , runServerPartT- , mapServerPartT- , mapServerPartT'- , withRequest- , anyRequest- -- * WebT- , WebT(..)- , UnWebT- , FilterFun- , Web- , mkWebT- , ununWebT- , runWebT- , mapWebT- -- * Type Classes- , FromReqURI(..)- , ToMessage(..)- , toResponseBS- -- * Manipulating requests- , FromData(..)- , ServerMonad(..)- , RqData- , noHandle- , getHeaderM- , escape- , escape'- , multi- -- * Manipulating responses- , FilterMonad(..)- , ignoreFilters- , SetAppend(..)- , FilterT(..)- , WebMonad(..)- , addCookie- , addCookies- , expireCookie- , addHeaderM- , setHeaderM- , ifModifiedSince- , modifyResponse- , setResponseCode- , resp-- -- * Respond Codes- , ok- , badGateway- , internalServerError- , badRequest- , unauthorized- , forbidden- , notFound- , seeOther- , found- , movedPermanently- , tempRedirect-- -- * guards and building blocks- , guardRq- , dir- , dirs- , host- , withHost- , method- , methodSP- , methodM- , methodOnly- , nullDir- , path- , anyPath- , anyPath'- , trailingSlash- , withData- , withDataFn- , getDataFn- , getData- , require- , requireM- , basicAuth- , uriRest- , flatten- , localContext- -- * proxying- , proxyServe- , rproxyServe- -- * unknown- , debugFilter- , applyRequest- -- * Parsing input and cookies- , lookInput -- :: String -> Data Input- , lookBS -- :: String -> Data B.ByteString- , look -- :: String -> Data String- , lookCookie -- :: String -> Data Cookie- , lookCookieValue -- :: String -> Data String- , readCookieValue -- :: Read a => String -> Data a- , lookRead -- :: Read a => String -> Data a- , lookPairs- -- * XSLT- , xslt ,doXslt- -- * Error Handlng- , errorHandlerSP- , simpleErrorHandler- , spUnwrapErrorT- -- * Output Validation- , setValidator- , setValidatorSP- , validateConf- , runValidator- , wdgHTMLValidator- , noopValidator- , lazyProcValidator- ) where--import Happstack.Server.HTTP.Types hiding (Version(..))-import Happstack.Server.Cookie--import qualified Paths_happstack_server as Cabal--import qualified Data.Version as DV-import Happstack.Server.HTTP.Client (getResponse, unproxify, unrproxify)-import Happstack.Data.Xml.HaXml (toHaXmlEl)-import qualified Happstack.Server.MinHaXML as H-import qualified Happstack.Server.HTTP.Listen as Listen (listen, listen',listenOn) -- So that we can disambiguate 'Writer.listen'-import Happstack.Server.XSLT (XSLTCmd, XSLPath, procLBSIO)-import Happstack.Server.SURI (ToSURI)-import Happstack.Util.Common (Seconds, readM)-import Happstack.Data (Xml, normalize, fromPairs, Element, toXml, toPublicXml) -- used by default implementation of fromData-import Network (PortID(..), Socket)-import Control.Applicative (Applicative, pure, (<*>))-import Control.Concurrent (forkIO)-import Control.Exception (evaluate)-import Control.Monad ( MonadPlus, mzero, mplus- , msum, ap, unless- , liftM, liftM2, liftM3, liftM4- )-import Control.Monad.Trans ( MonadTrans, lift- , MonadIO, liftIO- )-import Control.Monad.Reader ( ReaderT(ReaderT), runReaderT- , MonadReader, ask, local- , asks- )-import Control.Monad.Writer ( WriterT(WriterT), runWriterT- , MonadWriter, tell, pass- , listens- )-import qualified Control.Monad.Writer as Writer (listen) -- So that we can disambiguate 'Listen.listen'-import Control.Monad.State (MonadState, get, put)-import Control.Monad.Error ( ErrorT(ErrorT), runErrorT- , Error, strMsg- , MonadError, throwError, catchError- , mapErrorT- )-import Control.Monad.Maybe (MaybeT(MaybeT), runMaybeT)-import Data.Char (ord)-import Data.Maybe (fromMaybe)-import Data.Monoid ( Monoid, mempty, mappend- , Dual(Dual), getDual- , Endo(Endo), appEndo- )--import qualified Data.ByteString.Char8 as B-import qualified Data.ByteString.Lazy.Char8 as L-import qualified Data.ByteString.Lazy.UTF8 as LU (toString, fromString)--import qualified Data.Generics as G-import qualified Data.Map as M--import Text.Html (Html, renderHtml)-import qualified Text.XHtml as XHtml (Html, renderHtml)--import qualified Happstack.Crypto.Base64 as Base64-import Data.Char (toLower)-import Data.List (isPrefixOf,stripPrefix,tails,inits)-import System.IO (hGetContents, hClose)-import System.Console.GetOpt ( OptDescr(Option)- , ArgDescr(ReqArg)- , ArgOrder(Permute)- , getOpt- )-import System.Locale (defaultTimeLocale)-import System.Process (runInteractiveProcess, waitForProcess)-import System.Time (CalendarTime, formatCalendarTime)-import System.Exit (ExitCode(ExitSuccess, ExitFailure))-import System.FilePath (makeRelative, splitDirectories)-import Debug.Trace (trace)---- | An alias for WebT when using IO.-type Web a = WebT IO a--- | An alias for using ServerPartT when using the IO.-type ServerPart a = ServerPartT IO a------------------------------------------- HERE BEGINS ServerPartT definitions---- | ServerPartT is a container for processing requests and returning results.-newtype ServerPartT m a = ServerPartT { unServerPartT :: ReaderT Request (WebT m) a }- deriving (Monad, MonadIO, MonadPlus, Functor)---- | particularly useful when combined with runWebT to produce--- a @m (Maybe Response)@ from a request.-runServerPartT :: ServerPartT m a -> Request -> WebT m a-runServerPartT = runReaderT . unServerPartT--withRequest :: (Request -> WebT m a) -> ServerPartT m a-withRequest = ServerPartT . ReaderT---- | Used to manipulate the containing monad. Very useful when embedding a--- monad into a 'ServerPartT', since 'simpleHTTP' requires a @ServerPartT IO a@.--- Refer to 'WebT' for an explanation of the structure of the monad.------ Here is an example. Suppose you want to embed an 'ErrorT' into your--- 'ServerPartT' to enable 'throwError' and 'catchError' in your 'Monad'.------ > type MyServerPartT e m a = ServerPartT (ErrorT e m) a------ Now suppose you want to pass @MyServerPartT@ into a function--- that demands a @ServerPartT IO a@ (e.g. 'simpleHTTP'). You--- can provide the function:------ > unpackErrorT :: (Monad m, Show e) => UnWebT (ErrorT e m) a -> UnWebT m a--- > unpackErrorT et = do--- > eitherV <- runErrorT et--- > return $ case eitherV of--- > Left err -> Just (Left $ toResponse $ "Catastrophic failure " ++ show err--- > , Set $ Dual $ Endo $ \r -> r{rsCode = 500})--- > Right x -> x------ With @unpackErrorT@ you can now call 'simpleHTTP'. Just wrap your @ServerPartT@ list.------ > simpleHTTP nullConf $ mapServerPartT unpackErrorT (myPart `catchError` myHandler)------ Or alternatively:------ > simpleHTTP' unpackErrorT nullConf (myPart `catchError` myHandler)------ Also see 'spUnwrapErrorT' for a more sophisticated version of this function.----mapServerPartT :: ( UnWebT m a -> UnWebT n b)- -> (ServerPartT m a -> ServerPartT n b)-mapServerPartT f ma = withRequest $ \rq -> mapWebT f (runServerPartT ma rq)---- | A variant of 'mapServerPartT' where the first argument also takes a request.--- Useful if you want to 'runServerPartT' on a different 'ServerPartT' inside your--- monad (see 'spUnwrapErrorT').-mapServerPartT' :: (Request -> UnWebT m a -> UnWebT n b)- -> ( ServerPartT m a -> ServerPartT n b)-mapServerPartT' f ma = withRequest $ \rq -> mapWebT (f rq) (runServerPartT ma rq)--instance MonadTrans (ServerPartT) where- lift m = withRequest (\_ -> lift m)--instance (Monad m) => Monoid (ServerPartT m a) where- mempty = mzero- mappend = mplus--instance (Monad m, Functor m) => Applicative (ServerPartT m) where- pure = return- (<*>) = ap--instance (Monad m, MonadWriter w m) => MonadWriter w (ServerPartT m) where- tell = lift . tell- listen m = withRequest $ \rq -> Writer.listen (runServerPartT m rq) >>= return- pass m = withRequest $ \rq -> pass (runServerPartT m rq) >>= return--instance (Monad m, MonadError e m) => MonadError e (ServerPartT m) where- throwError e = lift $ throwError e- catchError action handler = withRequest $ \rq -> (runServerPartT action rq) `catchError` ((flip runServerPartT $ rq) . handler)--instance (Monad m, MonadReader r m) => MonadReader r (ServerPartT m) where- ask = lift ask- local fn m = withRequest $ \rq-> local fn (runServerPartT m rq)--instance Monad m => FilterMonad Response (ServerPartT m) where- setFilter = anyRequest . setFilter- composeFilter = anyRequest . composeFilter- getFilter m = withRequest $ \rq -> getFilter (runServerPartT m rq)--instance Monad m => WebMonad Response (ServerPartT m) where- finishWith r = anyRequest $ finishWith r---- | Yes, this is exactly like 'ReaderT' with new names.--- Why you ask? Because 'ServerT' can lift up a 'ReaderT'.--- If you did that, it would shadow ServerT's behavior--- as a ReaderT, thus meaning if you lifted the ReaderT--- you could no longer modify the 'Request'. This way--- you can add a 'ReaderT' to your monad stack without--- any trouble.-class Monad m => ServerMonad m where- askRq :: m Request- localRq :: (Request -> Request) -> m a -> m a--instance (Monad m) => ServerMonad (ServerPartT m) where- askRq = ServerPartT $ ask- localRq f m = ServerPartT $ local f (unServerPartT m)--instance (Error e, ServerMonad m) => ServerMonad (ErrorT e m) where- askRq = lift askRq- localRq f = mapErrorT $ localRq f------------------------------------ HERE BEGINS WebT definitions---- | A monoid operation container.--- If a is a monoid, then 'SetAppend' is a monoid with the following behaviors:------ > Set x `mappend` Append y = Set (x `mappend` y)--- > Append x `mappend` Append y = Append (x `mappend` y)--- > _ `mappend` Set y = Set y------ A simple way of summarizing this is, if the right side is @Append@, then the--- right is appended to the left. If the right side is @Set@, then the left side--- is ignored.--data SetAppend a = Set a | Append a- deriving (Eq, Show)--instance Monoid a => Monoid (SetAppend a) where- mempty = Append mempty-- Set x `mappend` Append y = Set (x `mappend` y)- Append x `mappend` Append y = Append (x `mappend` y)- _ `mappend` Set y = Set y---- | Extract the value from a SetAppend.--- Note that a SetAppend is actually a CoPointed from:--- <http://hackage.haskell.org/packages/archive/category-extras/latest/doc/html/Control-Functor-Pointed.html>--- But lets not drag in that dependency. yet...-extract :: SetAppend t -> t-extract (Set x) = x-extract (Append x) = x--instance Functor (SetAppend) where- fmap f (Set x) = Set $ f x- fmap f (Append x) = Append $ f x---- | @FilterFun@ is a lot more fun to type than @SetAppend (Dual (Endo a))@.-type FilterFun a = SetAppend (Dual (Endo a))--unFilterFun :: FilterFun a -> (a -> a)-unFilterFun = appEndo . getDual . extract--newtype FilterT a m b = FilterT { unFilterT :: WriterT (FilterFun a) m b }- deriving (Monad, MonadTrans, Functor, MonadIO)---- | A set of functions for manipulating filters. A 'ServerPartT' implements--- 'FilterMonad' 'Response' so these methods are the fundamental ways of--- manipulating the response object, especially before you've converted your--- monadic value to a 'Response'.-class Monad m => FilterMonad a m | m->a where- -- | Ignores all previous alterations to your filter- --- -- As an example:- --- -- > do- -- > composeFilter f- -- > setFilter g- -- > return "Hello World"- --- -- setFilter g will cause the first composeFilter to be ignored.- setFilter :: (a->a) -> m ()- -- | Composes your filter function with the existing filter- -- function.- composeFilter :: (a->a) -> m ()- -- | Retrives the filter from the environment.- getFilter :: m b -> m (b, a->a)--instance (Monad m) => FilterMonad a (FilterT a m) where- setFilter = FilterT . tell . Set . Dual . Endo- composeFilter = FilterT . tell . Append . Dual . Endo- getFilter = FilterT . listens unFilterFun . unFilterT---- | The basic response building object.-newtype WebT m a = WebT { unWebT :: ErrorT Response (FilterT (Response) (MaybeT m)) a }- deriving (MonadIO, Functor)---- |--- It is worth discussing the unpacked structure of 'WebT' a bit as it's exposed--- in 'mapServerPartT' and 'mapWebT'.------ A fully unpacked 'WebT' has a structure that looks like:------ > ununWebT $ WebT m a :: m (Maybe (Either Response a, FilterFun Response))------ So, ignoring @m@, as it is just the containing 'Monad', the outermost layer is--- a 'Maybe'. This is 'Nothing' if 'mzero' was called or @'Just' ('Either' 'Response'--- a, 'SetAppend' ('Endo' 'Response'))@ if 'mzero' wasn't called. Inside the 'Maybe',--- there is a pair. The second element of the pair is our filter function--- @'FilterFun' 'Response'@. @'FilterFun' 'Response'@ is a type alias for @'SetAppend'--- ('Dual' ('Endo' 'Response'))@. This is just a wrapper for a @'Response' -> 'Response'@--- function with a particular 'Monoid' behavior. The value------ > Append (Dual (Endo f))------ Causes f to be composed with the previous filter.------ > Set (Dual (Endo f))------ Causes f to not be composed with the previous filter.------ Finally, the first element of the pair is either @Left Response@ or @Right a@.------ Another way of looking at all these pieces is from the behaviors--- they control. The 'Maybe' controls the 'mzero' behavior. @Set--- (Endo f)@ comes from the 'setFilter' behavior. Likewise, @Append--- (Endo f)@ is from 'composeFilter'. @Left Response@ is what you--- get when you call 'finishWith' and @Right a@ is the normal exit.------ An example case statement looks like:------ > ex1 webt = do--- > val <- ununWebT webt--- > case val of--- > Nothing -> Nothing -- this is the interior value when mzero was used--- > Just (Left r, f) -> Just (Left r, f) -- r is the value that was passed into "finishWith"--- > -- f is our filter function--- > Just (Right a, f) -> Just (Right a, f) -- a is our normal monadic value--- > -- f is still our filter function----type UnWebT m a = m (Maybe (Either Response a, FilterFun Response))--instance Monad m => Monad (WebT m) where- m >>= f = WebT $ unWebT m >>= unWebT . f- return a = WebT $ return a- fail s = outputTraceMessage s (mkFailMessage s)--instance Error Response where- strMsg = toResponse--class Monad m => WebMonad a m | m->a where- -- | A control structure. - -- It ends the computation and returns the 'Response' you passed into it- -- immediately. This provides an alternate escape route. In particular- -- it has a monadic value of any type. And unless you call @'setFilter' id@- -- first your response filters will be applied normally.- --- -- Extremely useful when you're deep inside a monad and decide that you- -- want to return a completely different content type, since it doesn't- -- force you to convert all your return types to 'Response' early just to- -- accomodate this.- finishWith :: a -> m b--instance (Monad m) => WebMonad Response (WebT m) where- finishWith r = WebT $ throwError r--instance MonadTrans WebT where- lift = WebT . lift . lift . lift--instance (Monad m) => MonadPlus (WebT m) where- -- | Aborts a computation.- --- -- This is primarily useful because 'msum' will take an array- -- of 'MonadPlus' and return the first one that isn't mzero,- -- which is exactly the semantics expected from objects- -- that take lists of 'ServerPartT'.- mzero = WebT $ lift $ lift $ mzero- mplus x y = WebT $ ErrorT $ FilterT $ (lower x) `mplus` (lower y)- where lower = (unFilterT . runErrorT . unWebT)---- | Deprecated: use 'mzero'.-noHandle :: (MonadPlus m) => m a-noHandle = mzero-{-# DEPRECATED noHandle "Use mzero" #-}--instance (Monad m) => FilterMonad Response (WebT m) where- setFilter f = WebT $ lift $ setFilter $ f- composeFilter f = WebT . lift . composeFilter $ f- getFilter m = WebT $ ErrorT $ liftM lft $ getFilter (runErrorT $ unWebT m)- where- lft (Left r, _) = Left r- lft (Right a, f) = Right (a, f)--instance (Monad m) => Monoid (WebT m a) where- mempty = mzero- mappend = mplus---- | Takes your 'WebT', if it is 'mempty' it returns 'Nothing' else it--- converts the value to a 'Response' and applies your filter to it.-runWebT :: forall m b. (Functor m, ToMessage b) => WebT m b -> m (Maybe Response)-runWebT = (fmap . fmap) appFilterToResp . ununWebT- where- appFilterToResp :: (Either Response b, FilterFun Response) -> Response- appFilterToResp (e, ff) = unFilterFun ff $ either id toResponse e---- | For when you really need to unpack a 'WebT' entirely (and not--- just unwrap the first layer with 'unWebT').-ununWebT :: WebT m a -> UnWebT m a-ununWebT = runMaybeT . runWriterT . unFilterT . runErrorT . unWebT---- | For wrapping a 'WebT' back up. @mkWebT . ununWebT = id@-mkWebT :: UnWebT m a -> WebT m a-mkWebT = WebT . ErrorT . FilterT . WriterT . MaybeT---- | See 'mapServerPartT' for a discussion of this function.-mapWebT :: (UnWebT m a -> UnWebT n b)- -> ( WebT m a -> WebT n b)-mapWebT f ma = mkWebT $ f (ununWebT ma)--instance (Monad m, Functor m) => Applicative (WebT m) where- pure = return- (<*>) = ap--instance MonadReader r m => MonadReader r (WebT m) where- ask = lift ask- local fn m = mkWebT $ local fn (ununWebT m)--instance MonadState st m => MonadState st (WebT m) where- get = lift get- put = lift . put--instance MonadError e m => MonadError e (WebT m) where- throwError err = lift $ throwError err- catchError action handler = mkWebT $ catchError (ununWebT action) (ununWebT . handler)--instance MonadWriter w m => MonadWriter w (WebT m) where- tell = lift . tell- listen m = mkWebT $ Writer.listen (ununWebT m) >>= (return . liftWebT)- where liftWebT (Nothing, _) = Nothing- liftWebT (Just (Left x,f), _) = Just (Left x,f)- liftWebT (Just (Right x,f),w) = Just (Right (x,w),f)- pass m = mkWebT $ ununWebT m >>= liftWebT- where liftWebT Nothing = return Nothing- liftWebT (Just (Left x,f)) = return $ Just (Left x, f)- liftWebT (Just (Right x,f)) = pass (return x)>>= (\a -> return $ Just (Right a,f))---- | An alias for @setFilter id@ It resets all your filters.-ignoreFilters :: (FilterMonad a m) => m ()-ignoreFilters = setFilter id---- | Used to ignore all your filters and immediately end the--- computation. A combination of 'ignoreFilters' and 'finishWith'.-escape :: (WebMonad a m, FilterMonad a m) => m a -> m b-escape gen = ignoreFilters >> gen >>= finishWith---- | An alternate form of 'escape' that can be easily used within a do--- block.-escape' :: (WebMonad a m, FilterMonad a m) => a -> m b-escape' a = ignoreFilters >> finishWith a--------------------------------------------------- additional types----- | An array of 'OptDescr', useful for processing--- command line options into an 'Conf' for 'simpleHTTP'.-ho :: [OptDescr (Conf -> Conf)]-ho = [Option [] ["http-port"] (ReqArg (\h c -> c { port = read h }) "port") "port to bind http server"]---- | Parse command line options into a 'Conf'.-parseConfig :: [String] -> Either [String] Conf-parseConfig args- = case getOpt Permute ho args of- (flags,_,[]) -> Right $ foldr ($) nullConf flags- (_,_,errs) -> Left errs---- | Use the built-in web-server to serve requests according to a--- 'ServerPartT'. Use 'msum' to pick the first handler from a list of--- handlers that doesn't call 'mzero'. This function always binds o--- IPv4 ports until Network module is fixed to support IPv6 in a--- portable way. Use 'simpleHTTPWithSocket' with custom socket if you--- want different behaviour.-simpleHTTP :: (ToMessage a) => Conf -> ServerPartT IO a -> IO ()-simpleHTTP = simpleHTTP' id---- | A combination of 'simpleHTTP''' and 'mapServerPartT'. See--- 'mapServerPartT' for a discussion of the first argument of this--- function. This function always binds to IPv4 ports until Network--- module is fixed to support IPv6 in a portable way. Use--- 'simpleHTTPWithSocket' with custom socket if you want different--- behaviour.-simpleHTTP' :: (ToMessage b, Monad m, Functor m) => (UnWebT m a -> UnWebT IO b)- -> Conf -> ServerPartT m a -> IO ()-simpleHTTP' toIO conf hs =- Listen.listen conf (\req -> runValidator (fromMaybe return (validator conf)) =<< (simpleHTTP'' (mapServerPartT toIO hs) req))----- | Generate a result from a 'ServerPartT' and a 'Request'. This is mainly used--- by CGI (and fast-cgi) wrappers.-simpleHTTP'' :: (ToMessage b, Monad m, Functor m) => ServerPartT m b -> Request -> m Response-simpleHTTP'' hs req = (runWebT $ runServerPartT hs req) >>= (return . (maybe standardNotFound id))- where- standardNotFound = setHeader "Content-Type" "text/html" $ (toResponse notFoundHtml){rsCode=404}---- | Run 'simpleHTTP' with a previously bound socket. Useful if you want to run--- happstack as user on port 80. Use something like this:------ > import System.Posix.User (setUserID, UserEntry(..), getUserEntryForName)--- >--- > main = do--- > let conf = nullConf { port = 80 }--- > socket <- bindPort conf--- > -- do other stuff as root here--- > getUserEntryForName "www" >>= setUserID . userID--- > -- finally start handling incoming requests--- > tid <- forkIO $ simpleHTTPWithSocket socket conf impl------ Note: It's important to use the same conf (or at least the same port) for--- 'bindPort' and 'simpleHTTPWithSocket'.-simpleHTTPWithSocket :: (ToMessage a) => Socket -> Conf -> ServerPartT IO a -> IO ()-simpleHTTPWithSocket = simpleHTTPWithSocket' id---- | Like 'simpleHTTP'' with a socket.-simpleHTTPWithSocket' :: (ToMessage b, Monad m, Functor m) => (UnWebT m a -> UnWebT IO b)- -> Socket -> Conf -> ServerPartT m a -> IO ()-simpleHTTPWithSocket' toIO socket conf hs =- Listen.listen' socket conf (\req -> runValidator (fromMaybe return (validator conf)) =<< (simpleHTTP'' (mapServerPartT toIO hs) req))---- | Bind port and return the socket for 'simpleHTTPWithSocket'. This--- function always binds to IPv4 ports until Network module is fixed to--- support IPv6 in a portable way.-bindPort :: Conf -> IO Socket-bindPort conf = Listen.listenOn (port conf)---- | This class is used by 'path' to parse a path component into a value.--- At present, the instances for number types (Int, Float, etc) just--- call 'readM'. The instance for 'String' however, just passes the--- path component straight through. This is so that you can read a--- path component which looks like this as a 'String':------ \/somestring\/------ instead of requiring the path component to look like:------ \/\"somestring\"\/-class FromReqURI a where- fromReqURI :: String -> Maybe a--instance FromReqURI String where fromReqURI = Just-instance FromReqURI Int where fromReqURI = readM-instance FromReqURI Integer where fromReqURI = readM-instance FromReqURI Float where fromReqURI = readM-instance FromReqURI Double where fromReqURI = readM--type RqData a = ReaderT ([(String,Input)], [(String,Cookie)]) Maybe a---- | Useful for 'withData' and 'getData'' implement this on your preferred type--- to use those functions.-class FromData a where- fromData :: RqData a--instance (Eq a,Show a,Xml a,G.Data a) => FromData a where- fromData = do mbA <- lookPairs >>= return . normalize . fromPairs- case mbA of- Just a -> return a- Nothing -> fail "FromData G.Data failure"--- fromData = lookPairs >>= return . normalize . fromPairs--instance (FromData a, FromData b) => FromData (a,b) where- fromData = liftM2 (,) fromData fromData-instance (FromData a, FromData b, FromData c) => FromData (a,b,c) where- fromData = liftM3 (,,) fromData fromData fromData-instance (FromData a, FromData b, FromData c, FromData d) => FromData (a,b,c,d) where- fromData = liftM4 (,,,) fromData fromData fromData fromData-instance FromData a => FromData (Maybe a) where- fromData = fmap Just fromData `mplus` return Nothing---- |low-level function to build a 'Response' from a content-type and a--- 'ByteString'------ Creates a 'Response' in a manner similar to the 'ToMessage' class,--- but with out requiring an instance declaration.-toResponseBS :: B.ByteString -- ^ content-type- -> L.ByteString -- ^ response body- -> Response-toResponseBS contentType message =- let res = Response 200 M.empty nullRsFlags message Nothing- in setHeaderBS (B.pack "Content-Type") contentType res----- |--- Used to convert arbitrary types into an HTTP response. You need to implement--- this if you want to pass @ServerPartT m@ containing your type into 'simpleHTTP'.------ Minimal definition: 'toMessage'.-class ToMessage a where- toContentType :: a -> B.ByteString- toContentType _ = B.pack "text/plain"- toMessage :: a -> L.ByteString- toMessage = error "Happstack.Server.SimpleHTTP.ToMessage.toMessage: Not defined"- toResponse:: a -> Response- toResponse val =- let bs = toMessage val- res = Response 200 M.empty nullRsFlags bs Nothing- in setHeaderBS (B.pack "Content-Type") (toContentType val)- res--instance ToMessage [Element] where- toContentType _ = B.pack "application/xml; charset=UTF-8"- toMessage [el] = LU.fromString $ H.simpleDoc H.NoStyle $ toHaXmlEl el -- !! OPTIMIZE- toMessage x = error ("Happstack.Server.SimpleHTTP 'instance ToMessage [Element]' Can't handle " ++ show x)-----instance ToMessage () where- toContentType _ = B.pack "text/plain"- toMessage () = L.empty-instance ToMessage String where- toContentType _ = B.pack "text/plain; charset=UTF-8"- toMessage = LU.fromString-instance ToMessage Integer where- toMessage = toMessage . show-instance ToMessage a => ToMessage (Maybe a) where- toContentType _ = toContentType (undefined :: a)- toMessage Nothing = toMessage "nothing"- toMessage (Just x) = toMessage x---instance ToMessage Html where- toContentType _ = B.pack "text/html; charset=UTF-8"- toMessage = LU.fromString . renderHtml--instance ToMessage XHtml.Html where- toContentType _ = B.pack "text/html; charset=UTF-8"- toMessage = LU.fromString . XHtml.renderHtml--instance ToMessage Response where- toResponse = id--instance (Xml a)=>ToMessage a where- toContentType = toContentType . toXml- toMessage = toMessage . toPublicXml---- toMessageM = toMessageM . toPublicXml---class MatchMethod m where matchMethod :: m -> Method -> Bool-instance MatchMethod Method where matchMethod m = (== m)-instance MatchMethod [Method] where matchMethod methods = (`elem` methods)-instance MatchMethod (Method -> Bool) where matchMethod f = f-instance MatchMethod () where matchMethod () _ = True---- | 'flatten' turns your arbitrary @m a@ and converts it too--- a @m 'Response'@ with 'toResponse'.-flatten :: (ToMessage a, Functor f) => f a -> f Response-flatten = fmap toResponse---- | This is kinda like a very oddly shaped 'mapServerPartT' or 'mapWebT'--- You probably want one or the other of those.-localContext :: Monad m => (WebT m a -> WebT m' a) -> ServerPartT m a -> ServerPartT m' a-localContext fn hs- = withRequest $ \rq -> fn (runServerPartT hs rq)----- | Get a header out of the request.-getHeaderM :: (ServerMonad m) => String -> m (Maybe B.ByteString)-getHeaderM a = askRq >>= return . (getHeader a)---- | Add headers into the response.--- This method does not overwrite any existing header of--- the same name, hence the name 'addHeaderM'. If you--- want to replace a header use 'setHeaderM'.-addHeaderM :: (FilterMonad Response m) => String -> String -> m ()-addHeaderM a v = composeFilter $ \res-> addHeader a v res---- | Set a header into the response. This will replace--- an existing header of the same name. Use 'addHeaderM' if you--- want to add more than one header of the same name.-setHeaderM :: (FilterMonad Response m) => String -> String -> m ()-setHeaderM a v = composeFilter $ \res -> setHeader a v res----------------------------------------- guards---- | Guard using an arbitrary function on the request.-guardRq :: (ServerMonad m, MonadPlus m) => (Request -> Bool) -> m ()-guardRq f = do- rq <- askRq- unless (f rq) mzero---- | Guard against the method. This function also guards against--- any remaining path segments. See 'methodOnly' for the version--- that guards only by method-methodM :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m ()-methodM meth = methodOnly meth >> nullDir---- | Guard against the method only. (as opposed to 'methodM')-methodOnly :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m ()-methodOnly meth = guardRq $ \rq -> matchMethod meth (rqMethod rq)---- | Guard against the method. Note, this function also guards against any--- remaining path segments.-methodSP :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m b-> m b-methodSP m handle = methodM m >> handle---- | Guard against the method. Note, this function also guards against any--- remaining path segments. This function is deprecated. You can probably--- just use methodSP (or methodM) now.-method :: (MatchMethod method, Monad m) => method -> WebT m a -> ServerPartT m a-method m handle = methodSP m (anyRequest handle)-{-# DEPRECATED method "you should be able to use methodSP" #-}---- | Guard against non-empty remaining path segments.-nullDir :: (ServerMonad m, MonadPlus m) => m ()-nullDir = guardRq $ \rq -> null (rqPaths rq)---- | Pop a path element and run the 'ServerPartT' if it matches the given string.--- --- The path element can not contain \'/\'. See also 'dirs'.-dir :: (ServerMonad m, MonadPlus m) => String -> m a -> m a-dir staticPath handle =- do- rq <- askRq- case rqPaths rq of- (p:xs) | p == staticPath -> localRq (\newRq -> newRq{rqPaths = xs}) handle- _ -> mzero- --- | Guard against a 'FilePath'. Unlike 'dir' the 'FilePath' may--- contain \'/\'. If the guard succeeds, the matched elements will be--- popped from the directory stack.------ > dirs "foo/bar" $ ...--- --- See also: 'dir'.-dirs :: (ServerMonad m, MonadPlus m) => FilePath -> m a -> m a -dirs fp m = - do let parts = splitDirectories (makeRelative "/" fp) - foldr dir m parts---- | Guard against the host.-host :: (ServerMonad m, MonadPlus m) => String -> m a -> m a-host desiredHost handle =- do rq <- askRq- case getHeader "host" rq of- (Just hostBS) | desiredHost == B.unpack hostBS -> handle- _ -> mzero---- | Lookup the host header and pass it to the handler.-withHost :: (ServerMonad m, MonadPlus m) => (String -> m a) -> m a-withHost handle =- do rq <- askRq- case getHeader "host" rq of- (Just hostBS) -> handle (B.unpack hostBS)- _ -> mzero----- | Pop a path element and parse it using the 'fromReqURI' in the 'FromReqURI' class.-path :: (FromReqURI a, MonadPlus m, ServerMonad m) => (a -> m b) -> m b-path handle = do- rq <- askRq- case rqPaths rq of- (p:xs) | Just a <- fromReqURI p- -> localRq (\newRq -> newRq{rqPaths = xs}) (handle a)- _ -> mzero---- | Grab the rest of the URL (dirs + query) and passes it to your handler.-uriRest :: (ServerMonad m) => (String -> m a) -> m a-uriRest handle = askRq >>= handle . rqURL---- | Pop any path element and ignore when chosing a 'ServerPartT' to--- handle the request.-anyPath :: (ServerMonad m, MonadPlus m) => m r -> m r-anyPath x = path $ (\(_::String) -> x)---- | Deprecated: use 'anyPath'.-anyPath' :: (ServerMonad m, MonadPlus m) => m r -> m r-anyPath' = anyPath-{-# DEPRECATED anyPath' "Use anyPath" #-}---- | Guard which checks that the Request URI ends in \'\/\'. --- Useful for distinguishing between @foo@ and @foo/@-trailingSlash :: (ServerMonad m, MonadPlus m) => m ()-trailingSlash = guardRq $ \rq -> (last (rqUri rq)) == '/'---- | Parse your request with a 'RqData' (a ReaderT, basically)--- For example here is a simple @GET@ or @POST@ variable based authentication--- guard. It handles the request with 'errorHandler' if authentication fails.------ > myRqData = do--- > username <- lookInput "username"--- > password <- lookInput "password"--- > return (username, password)--- > checkAuth errorHandler = do--- > d <- getData myRqDataA--- > case d of--- > Nothing -> errorHandler--- > Just a | isValid a -> mzero--- > Just a | otherwise -> errorHandler-getDataFn :: (ServerMonad m) => RqData a -> m (Maybe a)-getDataFn rqData = do- rq <- askRq- return $ runReaderT rqData (rqInputs rq, rqCookies rq)---- | An variant of 'getData' that uses 'FromData' to chose your--- 'RqData' for you. The example from 'getData' becomes:------ > myRqData = do--- > username <- lookInput "username"--- > password <- lookInput "password"--- > return (username, password)--- > instance FromData (String,String) where--- > fromData = myRqData--- > checkAuth errorHandler = do--- > d <- getData'--- > case d of--- > Nothing -> errorHandler--- > Just a | isValid a -> mzero--- > Just a | otherwise -> errorHandler----getData :: (ServerMonad m, FromData a) => m (Maybe a)-getData = getDataFn fromData---- | Retrieve data from the input query or the cookies.-withData :: (FromData a, MonadPlus m, ServerMonad m) => (a -> m r) -> m r-withData = withDataFn fromData---- | 'withDataFn' is like 'withData', but you pass in a 'RqData' monad--- for reading.-withDataFn :: (MonadPlus m, ServerMonad m) => RqData a -> (a -> m r) -> m r-withDataFn fn handle = getDataFn fn >>= maybe mzero handle---- | 'proxyServe' is for creating 'ServerPartT's that proxy.--- The sole argument @[String]@ is a list of allowed domains for--- proxying. This matches the domain part of the request--- and the wildcard * can be used. E.g.------ * \"*\" to match anything.------ * \"*.example.com\" to match anything under example.com------ * \"example.com\" to match just example.com--------- TODO: annoyingly enough, this method eventually calls escape, so--- any headers you set won't be used, and the computation immediatly ends.-proxyServe :: (MonadIO m, WebMonad Response m, ServerMonad m, MonadPlus m, FilterMonad Response m) => [String] -> m Response-proxyServe allowed = do- rq <- askRq- if cond rq then proxyServe' rq else mzero- where- cond rq- | "*" `elem` allowed = True- | domain `elem` allowed = True- | superdomain `elem` wildcards =True- | otherwise = False- where- domain = head (rqPaths rq)- superdomain = tail $ snd $ break (=='.') domain- wildcards = (map (drop 2) $ filter ("*." `isPrefixOf`) allowed)---- | Takes a proxy 'Request' and creates a 'Response'. Your basic proxy--- building block. See 'unproxify'.------ TODO: this would be more useful if it didn\'t call 'escape' (e.g. it let you--- modify the response afterwards, or set additional headers)-proxyServe' :: (MonadIO m, FilterMonad Response m, WebMonad Response m) => Request-> m Response-proxyServe' rq = liftIO (getResponse (unproxify rq)) >>=- either (badGateway . toResponse . show) escape'---- | This is a reverse proxy implementation.--- See 'unrproxify'.------ TODO: this would be more useful if it didn\'t call 'escape', just like--- proxyServe'.-rproxyServe :: (MonadIO m) =>- String -- ^ defaultHost- -> [(String, String)] -- ^ map to look up hostname mappings. For the reverse proxy- -> ServerPartT m Response -- ^ the result is a ServerPartT that will reverse proxy for you.-rproxyServe defaultHost list = withRequest $ \rq ->- liftIO (getResponse (unrproxify defaultHost list rq)) >>=- either (badGateway . toResponse . show) (escape')---- | Run an IO action and, if it returns @Just@, pass it to the second argument.-require :: (MonadIO m, MonadPlus m) => IO (Maybe a) -> (a -> m r) -> m r-require fn handle = do- mbVal <- liftIO fn- case mbVal of- Nothing -> mzero- Just a -> handle a---- | A variant of require that can run in any monad, not just IO.-requireM :: (MonadTrans t, Monad m, MonadPlus (t m)) => m (Maybe a) -> (a -> t m r) -> t m r-requireM fn handle = do- mbVal <- lift fn- case mbVal of- Nothing -> mzero- Just a -> handle a---- | Use @cmd@ to transform XML against @xslPath@.--- This function only acts if the content-type is @application\/xml@.-xslt :: (MonadIO m, MonadPlus m, ToMessage r) =>- XSLTCmd -- ^ XSLT preprocessor. Usually 'xsltproc' or 'saxon'.- -> XSLPath -- ^ Path to xslt stylesheet.- -> m r -- ^ Affected @ServerParts@.- -> m Response-xslt cmd xslPath parts = do- res <- parts- if toContentType res == B.pack "application/xml"- then liftM toResponse (doXslt cmd xslPath (toResponse res))- else return (toResponse res)--doXslt :: (MonadIO m) =>- XSLTCmd -> XSLPath -> Response -> m Response-doXslt cmd xslPath res =- do new <- liftIO $ procLBSIO cmd xslPath $ rsBody res- return $ setHeader "Content-Type" "text/html" $- setHeader "Content-Length" (show $ L.length new) $- res { rsBody = new }---- | Add the cookie with a timeout to the response.-addCookie :: (FilterMonad Response m) => Seconds -> Cookie -> m ()-addCookie sec = (addHeaderM "Set-Cookie") . mkCookieHeader sec---- | Add the list of cookie timeout pairs to the response.-addCookies :: (FilterMonad Response m) => [(Seconds, Cookie)] -> m ()-addCookies = mapM_ (uncurry addCookie)---- | Expire the cookie immediately.-expireCookie :: (FilterMonad Response m) => String -> m () -expireCookie cookieName = addCookie 0 (mkCookie cookieName "")---- |Honor an @if-modified-since@ header in a 'Request'.--- If the 'Request' includes the @if-modified-since@ header and the--- 'Response' has not been modified, then return 304 (Not Modified),--- otherwise return the 'Response'.-ifModifiedSince :: CalendarTime -- ^ mod-time for the Response (MUST NOT be later than server's time of message origination)- -> Request -- ^ incoming request (used to check for if-modified-since)- -> Response -- ^ Response to send if there are modifications- -> Response-ifModifiedSince modTime request response =- let repr = formatCalendarTime defaultTimeLocale "%a, %d %b %Y %X GMT" modTime- notmodified = getHeader "if-modified-since" request == Just (B.pack $ repr)- in if notmodified- then result 304 "" -- Not Modified- else setHeader "Last-modified" repr response---- | Deprecated: use 'composeFilter'.-modifyResponse :: (FilterMonad a m) => (a -> a) -> m()-modifyResponse = composeFilter-{-# DEPRECATED modifyResponse "Use composeFilter" #-}---- | Set the return code in your response.-setResponseCode :: FilterMonad Response m => Int -> m ()-setResponseCode code- = composeFilter $ \r -> r{rsCode = code}---- | Same as @'setResponseCode' status >> return val@.-resp :: (FilterMonad Response m) => Int -> b -> m b-resp status val = setResponseCode status >> return val---- | Respond with @200 OK@.-ok :: (FilterMonad Response m) => a -> m a-ok = resp 200---- | Respond with @500 Interal Server Error@.-internalServerError :: (FilterMonad Response m) => a -> m a-internalServerError = resp 500---- | Responds with @502 Bad Gateway@.-badGateway :: (FilterMonad Response m) => a -> m a-badGateway = resp 502---- | Respond with @400 Bad Request@.-badRequest :: (FilterMonad Response m) => a -> m a-badRequest = resp 400---- | Respond with @401 Unauthorized@.-unauthorized :: (FilterMonad Response m) => a -> m a-unauthorized = resp 401---- | Respond with @403 Forbidden@.-forbidden :: (FilterMonad Response m) => a -> m a-forbidden = resp 403---- | Respond with @404 Not Found@.-notFound :: (FilterMonad Response m) => a -> m a-notFound = resp 404---- | Respond with @303 See Other@.-seeOther :: (FilterMonad Response m, ToSURI uri) => uri -> res -> m res-seeOther uri res = do modifyResponse $ redirect 303 uri- return res---- | Respond with @302 Found@.-found :: (FilterMonad Response m, ToSURI uri) => uri -> res -> m res-found uri res = do modifyResponse $ redirect 302 uri- return res---- | Respond with @301 Moved Permanently@.-movedPermanently :: (FilterMonad Response m, ToSURI a) => a -> res -> m res-movedPermanently uri res = do modifyResponse $ redirect 301 uri- return res---- | Respond with @307 Temporary Redirect@.-tempRedirect :: (FilterMonad Response m, ToSURI a) => a -> res -> m res-tempRedirect val res = do modifyResponse $ redirect 307 val- return res---- | Deprecated: use 'msum'.-multi :: Monad m => [ServerPartT m a] -> ServerPartT m a-multi = msum-{-# DEPRECATED multi "Use msum instead" #-}---- | What is this for, exactly? I don't understand why @Show a@ is even in the context--- Deprecated: This function appears to do nothing at all. If it use it, let us know why.-debugFilter :: (MonadIO m, Show a) => ServerPartT m a -> ServerPartT m a-debugFilter handle =- withRequest $ \rq -> do- r <- runServerPartT handle rq- return r---- | A constructor for a 'ServerPartT' when you don't care about the request.-anyRequest :: Monad m => WebT m a -> ServerPartT m a-anyRequest x = withRequest $ \_ -> x---- | Again, why is this useful?--- Deprecated: No idea why this function would be useful. If you use it, please tell us.-applyRequest :: (ToMessage a, Monad m, Functor m) =>- ServerPartT m a -> Request -> Either (m Response) b-applyRequest hs = simpleHTTP'' hs >>= return . Left---- | A simple HTTP basic authentication guard.-basicAuth :: (WebMonad Response m, ServerMonad m, FilterMonad Response m, MonadPlus m) =>- String -- ^ the realm name- -> M.Map String String -- ^ the username password map- -> m a -- ^ the part to guard- -> m a-basicAuth realmName authMap xs = basicAuthImpl `mplus` xs- where- basicAuthImpl = do- aHeader <- getHeaderM "authorization"- case aHeader of- Nothing -> err- Just x -> case parseHeader x of- (name, ':':password) | validLogin name password -> mzero- | otherwise -> err- _ -> err- validLogin name password = M.lookup name authMap == Just password- parseHeader = break (':'==) . Base64.decode . B.unpack . B.drop 6- headerName = "WWW-Authenticate"- headerValue = "Basic realm=\"" ++ realmName ++ "\""- err = escape $ do- setHeaderM headerName headerValue- unauthorized $ toResponse "Not authorized"------------------------------------------------------------------- Query/Post data validating------------------------------------------------------------------- | Useful inside the 'RqData' monad. Gets the named input parameter--- (either from a @POST@ or a @GET@ request).-lookInput :: String -> RqData Input-lookInput name- = do inputs <- asks fst- case lookup name inputs of- Nothing -> fail "input not found"- Just i -> return i---- | Get the named input parameter as a 'L.ByteString'.-lookBS :: String -> RqData L.ByteString-lookBS = fmap inputValue . lookInput---- | Get the named input as a 'String'.-look :: String -> RqData String-look = fmap LU.toString . lookBS---- | Get the named cookie.--- The cookie name is case insensitive.-lookCookie :: String -> RqData Cookie-lookCookie name- = do cookies <- asks snd- case lookup (map toLower name) cookies of -- keys are lowercased- Nothing -> fail "cookie not found"- Just c -> return c---- | Get the named cookie as a 'String'.-lookCookieValue :: String -> RqData String-lookCookieValue = fmap cookieValue . lookCookie---- | Get the named cookie as the requested 'Read' type.-readCookieValue :: Read a => String -> RqData a-readCookieValue name = readM =<< fmap cookieValue (lookCookie name)---- | Like 'look', but 'Read's for you.-lookRead :: Read a => String -> RqData a-lookRead name = readM =<< look name---- | Get all the input parameters and convert them to a 'String'.-lookPairs :: RqData [(String,String)]-lookPairs = asks fst >>= return . map (\(n,vbs)->(n,LU.toString $ inputValue vbs))-------------------------------------------------------------------- Error Handling------------------------------------------------------------------- | This 'ServerPart' modifier enables the use of 'throwError' and 'catchError' inside the--- 'WebT' actions, by adding the 'ErrorT' monad transformer to the stack.------ You can wrap the complete second argument to 'simpleHTTP' in this function.----errorHandlerSP :: (Monad m, Error e) => (Request -> e -> WebT m a) -> ServerPartT (ErrorT e m) a -> ServerPartT m a-errorHandlerSP handler sps = withRequest $ \req -> mkWebT $ do- eer <- runErrorT $ ununWebT $ runServerPartT sps req- case eer of- Left err -> ununWebT (handler req err)- Right res -> return res-{-# DEPRECATED errorHandlerSP "Use spUnwrapErrorT" #-}---- | An example error Handler to be used with 'spUnWrapErrorT', which returns the--- error message as a plain text message to the browser.------ Another possibility is to store the error message, e.g. as a FlashMsg, and--- then redirect the user somewhere.-simpleErrorHandler :: (Monad m) => String -> ServerPartT m Response-simpleErrorHandler err = ok $ toResponse $ ("An error occured: " ++ err)---- | This is a for use with 'mapServerPartT\'' It it unwraps the--- interior monad for use with 'simpleHTTP'. If you have a--- @ServerPartT (ErrorT e m) a@, this will convert that monad into a--- @ServerPartT m a@. Used with 'mapServerPartT\'' to allow--- 'throwError' and 'catchError' inside your monad. Eg.------ > simpleHTTP conf $ mapServerPartT' (spUnWrapErrorT failurePart) $ myPart `catchError` errorPart------ Note that @failurePart@ will only be run if @errorPart@ threw an error--- so it doesn\'t have to be very complex.-spUnwrapErrorT:: Monad m => (e -> ServerPartT m a)- -> Request- -> UnWebT (ErrorT e m) a- -> UnWebT m a-spUnwrapErrorT handler rq = \x -> do- err <- runErrorT x- case err of- Left e -> ununWebT $ runServerPartT (handler e) rq- Right a -> return a------------------------------------------------------------------- * Output validation------------------------------------------------------------------- |Set the validator which should be used for this particular 'Response'--- when validation is enabled.------ Calling this function does not enable validation. That can only be--- done by enabling the validation in the 'Conf' that is passed to--- 'simpleHTTP'.------ You do not need to call this function if the validator set in--- 'Conf' does what you want already.------ Example: (use 'noopValidator' instead of the default supplied by 'validateConf')------ > simpleHTTP validateConf . anyRequest $ ok . setValidator noopValidator =<< htmlPage------ See also: 'validateConf', 'wdgHTMLValidator', 'noopValidator', 'lazyProcValidator'-setValidator :: (Response -> IO Response) -> Response -> Response-setValidator v r = r { rsValidator = Just v }---- |ServerPart version of 'setValidator'.------ Example: (Set validator to 'noopValidator')------ > simpleHTTP validateConf $ setValidatorSP noopValidator (dir "ajax" ... )----setValidatorSP :: (Monad m, ToMessage r) => (Response -> IO Response) -> m r -> m Response-setValidatorSP v sp = return . setValidator v . toResponse =<< sp---- |Extend 'nullConf' by enabling validation and setting--- 'wdgHTMLValidator' as the default validator for @text\/html@.------ Example:------ > simpleHTTP validateConf . anyRequest $ ok htmlPage----validateConf :: Conf-validateConf = nullConf { validator = Just wdgHTMLValidator }---- |Actually perform the validation on a 'Response'.------ Run the validator specified in the 'Response'. If none is provide--- use the supplied default instead.------ Note: This function will run validation unconditionally. You--- probably want 'setValidator' or 'validateConf'.-runValidator :: (Response -> IO Response) -> Response -> IO Response-runValidator defaultValidator r =- case rsValidator r of- Nothing -> defaultValidator r- (Just altValidator) -> altValidator r---- |Validate @text\/html@ content with @WDG HTML Validator@.------ This function expects the executable to be named @validate@--- and it must be in the default @PATH@.------ See also: 'setValidator', 'validateConf', 'lazyProcValidator'.-wdgHTMLValidator :: (MonadIO m, ToMessage r) => r -> m Response-wdgHTMLValidator = liftIO . lazyProcValidator "validate" ["-w","--verbose","--charset=utf-8"] Nothing Nothing handledContentTypes . toResponse- where- handledContentTypes (Just ct) = elem (takeWhile (\c -> c /= ';' && c /= ' ') (B.unpack ct)) [ "text/html", "application/xhtml+xml" ]- handledContentTypes Nothing = False---- |A validator which always succeeds.------ Useful for selectively disabling validation. For example, if you--- are sending down HTML fragments to an AJAX application and the--- default validator only understands complete documents.-noopValidator :: Response -> IO Response-noopValidator = return---- |Validate the 'Response' using an external application.------ If the external application returns 0, the original response is--- returned unmodified. If the external application returns non-zero, a 'Response'--- containing the error messages and original response body is--- returned instead.------ This function also takes a predicate filter which is applied to the--- content-type of the response. The filter will only be applied if--- the predicate returns true.------ NOTE: This function requirse the use of -threaded to avoid blocking.--- However, you probably need that for Happstack anyway.------ See also: 'wdgHTMLValidator'.-lazyProcValidator :: FilePath -- ^ name of executable- -> [String] -- ^ arguements to pass to the executable- -> Maybe FilePath -- ^ optional path to working directory- -> Maybe [(String, String)] -- ^ optional environment (otherwise inherit)- -> (Maybe B.ByteString -> Bool) -- ^ content-type filter- -> Response -- ^ Response to validate- -> IO Response-lazyProcValidator exec args wd env mimeTypePred response- | mimeTypePred (getHeader "content-type" response) =- do (inh, outh, errh, ph) <- runInteractiveProcess exec args wd env- out <- hGetContents outh- err <- hGetContents errh- forkIO $ do L.hPut inh (rsBody response)- hClose inh- forkIO $ evaluate (length out) >> return ()- forkIO $ evaluate (length err) >> return ()- ec <- waitForProcess ph- case ec of- ExitSuccess -> return response- (ExitFailure _) ->- return $ toResponse (unlines ([ "ExitCode: " ++ show ec- , "stdout:"- , out- , "stderr:"- , err- , "input:"- ] ++- showLines (rsBody response)))- | otherwise = return response- where- column = " " ++ (take 120 $ concatMap (\n -> " " ++ show n) (drop 1 $ cycle [0..9::Int]))- showLines :: L.ByteString -> [String]- showLines string = column : zipWith (\n -> \l -> show n ++ " " ++ (L.unpack l)) [1::Integer ..] (L.lines string)---- "Pattern match failure in do expression at src\AppControl.hs:43:24"--- is converted to:--- "src\AppControl.hs:43:24: Pattern match failure in do expression"--- Then we output this to stderr. Help debugging under Emacs console when using GHCi.--- This is GHC specific, but you may add your favourite compiler here also.-outputTraceMessage s c | "Pattern match failure " `isPrefixOf` s = - let w = [(k,p) | (i,p) <- zip (tails s) (inits s), Just k <- [stripPrefix " at " i]]- v = concatMap (\(k,p) -> k ++ ": " ++ p) w- in trace v c-outputTraceMessage _ c = trace "some error" c---mkFailMessage :: (FilterMonad Response m, WebMonad Response m) => String -> m b-mkFailMessage s = do- ignoreFilters- internalServerError ()- setHeaderM "Content-Type" "text/html"- res <- return $ toResponse $ failHtml s- finishWith $ res--failHtml:: String->String-failHtml errString = - "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">"- ++ "<html><head><title>Happstack "- ++ ver ++ " Internal Server Error</title></head>"- ++ "<body><h1>Happstack " ++ ver ++ "</h1>"- ++ "<p>Something went wrong here<br>"- ++ "Internal server error<br>"- ++ "Everything has stopped</p>"- ++ "<p>The error was \"" ++ (escapeString errString) ++ "\"</p></body></html>"- where ver = DV.showVersion Cabal.version--escapeString :: String -> String-escapeString str = concatMap encodeEntity str- where- encodeEntity :: Char -> String- encodeEntity '<' = "<"- encodeEntity '>' = ">"- encodeEntity '&' = "&"- encodeEntity '"' = """- encodeEntity c- | ord c > 127 = "&#" ++ show (ord c) ++ ";"- | otherwise = [c]+-- Copyright : (c) Happstack.com 2010; (c) HAppS Inc 2007+-- License : BSD-like+--+-- Maintainer : Happstack team <happs@googlegroups.com>+-- Stability : provisional+-- Portability : requires mtl+--+-- 'simpleHTTP' is a self-contained HTTP server which can be used to+-- run a 'ServerPart'.+--+-- A very simple, \"Hello World!\" web app looks like:+-- +-- > import Happstack.Server+-- > main = simpleHTTP nullConf $ ok "Hello World!"+--+-- By default the server will listen on port 8000. Run the app and point your browser at: <http://localhost:8000/>+--+-- For FastCGI support see: <http://hackage.haskell.org/package/happstack-fastcgi>+-----------------------------------------------------------------------------+module Happstack.Server.SimpleHTTP+ ( -- * SimpleHTTP+ simpleHTTP+ , simpleHTTP'+ , simpleHTTP''+ , simpleHTTPWithSocket+ , simpleHTTPWithSocket'+ , bindPort+ , parseConfig+ -- * Re-exported modules+ -- ** Basic ServerMonad functionality+ , module Happstack.Server.Monads+ -- ** HTTP Realm Authentication+ , module Happstack.Server.Auth+ -- ** Create and Set Cookies (see also "Happstack.Server.RqData")+ , module Happstack.Server.Cookie+ -- ** Error Handling+ , module Happstack.Server.Error+ -- ** Creating Responses+ , module Happstack.Server.Response+ -- ** Request Routing+ , module Happstack.Server.Routing+ -- ** Proxying+ , module Happstack.Server.Proxy+ -- ** Looking up values in Query String, Request Body, and Cookies+ , module Happstack.Server.RqData+ -- ** Output Validation+ , module Happstack.Server.Validation+ , module Happstack.Server.Types+-- , module Happstack.Server.Internal.Monads++ ) where++-- re-exports++import Happstack.Server.Auth+import Happstack.Server.Monads+import Happstack.Server.Cookie+import Happstack.Server.Error+import Happstack.Server.Types+import Happstack.Server.Proxy+import Happstack.Server.Routing+import Happstack.Server.RqData+import Happstack.Server.Response+import Happstack.Server.Validation+++import Data.Maybe (fromMaybe)+import qualified Data.Version as DV+import Happstack.Server.Internal.Monads (FilterFun, WebT(..), UnWebT, unFilterFun, mapServerPartT, runServerPartT, ununWebT)+import qualified Happstack.Server.Internal.Listen as Listen (listen, listen',listenOn) -- So that we can disambiguate 'Writer.listen'+import Happstack.Server.Types (Conf(port, validator), Request, Response(rsBody, rsCode), nullConf, setHeader)+import Network (Socket)+import qualified Paths_happstack_server as Cabal+import System.Console.GetOpt ( OptDescr(Option)+ , ArgDescr(ReqArg)+ , ArgOrder(Permute)+ , getOpt+ )++-- | An array of 'OptDescr', useful for processing command line+-- options into an 'Conf' for 'simpleHTTP'.+ho :: [OptDescr (Conf -> Conf)]+ho = [Option [] ["http-port"] (ReqArg (\h c -> c { port = read h }) "port") "port to bind http server"]++-- | Parse command line options into a 'Conf'.+parseConfig :: [String] -> Either [String] Conf+parseConfig args+ = case getOpt Permute ho args of+ (flags,_,[]) -> Right $ foldr ($) nullConf flags+ (_,_,errs) -> Left errs++-- |start the server, and handle requests using the supplied+-- 'ServerPart'.+--+-- This function will not return, though it may throw an exception.+--+-- NOTE: The server will only listen on IPv4 due to portability issues+-- in the "Network" module. For IPv6 support, use+-- 'simpleHTTPWithSocket' with custom socket.+simpleHTTP :: (ToMessage a) => Conf -> ServerPartT IO a -> IO ()+simpleHTTP = simpleHTTP' id++-- | A combination of 'simpleHTTP''' and 'mapServerPartT'. See+-- 'mapServerPartT' for a discussion of the first argument of this+-- function. +--+-- NOTE: This function always binds to IPv4 ports until Network+-- module is fixed to support IPv6 in a portable way. Use+-- 'simpleHTTPWithSocket' with custom socket if you want different+-- behaviour.+simpleHTTP' :: (ToMessage b, Monad m, Functor m) => (UnWebT m a -> UnWebT IO b)+ -> Conf -> ServerPartT m a -> IO ()+simpleHTTP' toIO conf hs =+ Listen.listen conf (\req -> runValidator (fromMaybe return (validator conf)) =<< (simpleHTTP'' (mapServerPartT toIO hs) req))+++-- | Generate a result from a 'ServerPartT' and a 'Request'. This is+-- mainly used by CGI (and fast-cgi) wrappers.+simpleHTTP'' :: (ToMessage b, Monad m, Functor m) => ServerPartT m b -> Request -> m Response+simpleHTTP'' hs req = (runWebT $ runServerPartT hs req) >>= (return . (maybe standardNotFound id))+ where+ standardNotFound = setHeader "Content-Type" "text/html" $ (toResponse notFoundHtml){rsCode=404}++-- | Run 'simpleHTTP' with a previously bound socket. Useful if you+-- want to run happstack as user on port 80. Use something like this:+--+-- > import System.Posix.User (setUserID, UserEntry(..), getUserEntryForName)+-- >+-- > main = do+-- > let conf = nullConf { port = 80 }+-- > socket <- bindPort conf+-- > -- do other stuff as root here+-- > getUserEntryForName "www" >>= setUserID . userID+-- > -- finally start handling incoming requests+-- > tid <- forkIO $ simpleHTTPWithSocket socket conf impl+--+-- Note: It's important to use the same conf (or at least the same+-- port) for 'bindPort' and 'simpleHTTPWithSocket'.+simpleHTTPWithSocket :: (ToMessage a) => Socket -> Conf -> ServerPartT IO a -> IO ()+simpleHTTPWithSocket = simpleHTTPWithSocket' id++-- | Like 'simpleHTTP'' with a socket.+simpleHTTPWithSocket' :: (ToMessage b, Monad m, Functor m) => (UnWebT m a -> UnWebT IO b)+ -> Socket -> Conf -> ServerPartT m a -> IO ()+simpleHTTPWithSocket' toIO socket conf hs =+ Listen.listen' socket conf (\req -> runValidator (fromMaybe return (validator conf)) =<< (simpleHTTP'' (mapServerPartT toIO hs) req))++-- | Bind port and return the socket for 'simpleHTTPWithSocket'. This+-- function always binds to IPv4 ports until Network module is fixed+-- to support IPv6 in a portable way.+bindPort :: Conf -> IO Socket+bindPort conf = Listen.listenOn (port conf)++-- | Takes your 'WebT', if it is 'mempty' it returns 'Nothing' else it+-- converts the value to a 'Response' and applies your filter to it.+runWebT :: forall m b. (Functor m, ToMessage b) => WebT m b -> m (Maybe Response)+runWebT = (fmap . fmap) appFilterToResp . ununWebT+ where+ appFilterToResp :: (Either Response b, FilterFun Response) -> Response+ appFilterToResp (e, ff) = unFilterFun ff $ either id toResponse e notFoundHtml :: String notFoundHtml =
− src/Happstack/Server/StdConfig.hs
@@ -1,22 +0,0 @@-{-# LANGUAGE FlexibleContexts, ScopedTypeVariables #-}-module Happstack.Server.StdConfig where--import Control.Monad.Trans-import Control.Monad-import Happstack.Server.SimpleHTTP-import Happstack.Server.HTTP.FileServe---- | Is equal to "haskell/Main"-binarylocation :: String-binarylocation = "haskell/Main"---- | Is equal to "public/log"-loglocation :: String-loglocation = "public/log"---- | Convenience function around 'errorwrapper'--- with the default binary location set to 'binarylocation' and the--- log location set to 'loglocation'. -errWrap :: (MonadPlus m, FilterMonad Response m, MonadIO m) => m Response-errWrap = errorwrapper binarylocation loglocation---stateFuns -- main actually has state so you can just import them
+ src/Happstack/Server/Types.hs view
@@ -0,0 +1,18 @@+module Happstack.Server.Types+ (Request(..), Response(..), RqBody(..), Input(..), HeaderPair(..),+ takeRequestBody, readInputsBody,+ rqURL, mkHeaders,+ getHeader, getHeaderBS, getHeaderUnsafe,+ hasHeader, hasHeaderBS, hasHeaderUnsafe,+ setHeader, setHeaderBS, setHeaderUnsafe,+ addHeader, addHeaderBS, addHeaderUnsafe,+ setRsCode, -- setCookie, setCookies,+ Conf(..), nullConf, result, resultBS,+ redirect, -- redirect_, redirect', redirect'_,+ isHTTP1_0, isHTTP1_1,+ RsFlags(..), nullRsFlags, noContentLength,+ HttpVersion(..), Length(..), Method(..), Headers, continueHTTP,+ Host, ContentType(..)+ ) where++import Happstack.Server.Internal.Types
+ src/Happstack/Server/Validation.hs view
@@ -0,0 +1,134 @@+-- | Support for validating server output on-the-fly. Validators can be configured on a per content-type basis.+module Happstack.Server.Validation where++import Control.Concurrent (forkIO)+import Control.Exception (evaluate)+import Control.Monad.Trans (MonadIO(liftIO))+import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Lazy.Char8 as L+import Happstack.Server.Types (Conf(..), Response(..), getHeader, nullConf)+import Happstack.Server.Response (ToMessage, toResponse)+import System.Exit (ExitCode(ExitSuccess, ExitFailure))+import System.IO (hGetContents, hClose)+import System.Process (runInteractiveProcess, waitForProcess)++-- | Set the validator which should be used for this particular+-- 'Response' when validation is enabled.+--+-- Calling this function does not enable validation. That can only be+-- done by enabling the validation in the 'Conf' that is passed to+-- 'simpleHTTP'.+--+-- You do not need to call this function if the validator set in+-- 'Conf' does what you want already.+--+-- Example: (use 'noopValidator' instead of the default supplied by+-- 'validateConf')+--+-- > simpleHTTP validateConf $ ok . setValidator noopValidator =<< htmlPage+--+-- See also: 'validateConf', 'wdgHTMLValidator', 'noopValidator',+-- 'lazyProcValidator'.+setValidator :: (Response -> IO Response) -> Response -> Response+setValidator v r = r { rsValidator = Just v }++-- | 'ServerPart' version of 'setValidator'.+--+-- Example: (Set validator to 'noopValidator')+--+-- > simpleHTTP validateConf $ setValidatorSP noopValidator (dir "ajax" ... )+--+setValidatorSP :: (Monad m, ToMessage r) => (Response -> IO Response) -> m r -> m Response+setValidatorSP v sp = return . setValidator v . toResponse =<< sp++-- | Extend 'nullConf' by enabling validation and setting+-- 'wdgHTMLValidator' as the default validator for @text\/html@.+--+-- Example:+--+-- > simpleHTTP validateConf . anyRequest $ ok htmlPage+--+validateConf :: Conf+validateConf = nullConf { validator = Just wdgHTMLValidator }++-- | Actually perform the validation on a 'Response'.+--+-- Run the validator specified in the 'Response'. If none is provide+-- use the supplied default instead.+--+-- Note: This function will run validation unconditionally. You+-- probably want 'setValidator' or 'validateConf'.+runValidator :: (Response -> IO Response) -> Response -> IO Response+runValidator defaultValidator r =+ case rsValidator r of+ Nothing -> defaultValidator r+ (Just altValidator) -> altValidator r++-- | Validate @text\/html@ content with @WDG HTML Validator@.+--+-- This function expects the executable to be named @validate@ and it+-- must be in the default @PATH@.+--+-- See also: 'setValidator', 'validateConf', 'lazyProcValidator'.+wdgHTMLValidator :: (MonadIO m, ToMessage r) => r -> m Response+wdgHTMLValidator = liftIO . lazyProcValidator "validate" ["-w","--verbose","--charset=utf-8"] Nothing Nothing handledContentTypes . toResponse+ where+ handledContentTypes (Just ct) = elem (takeWhile (\c -> c /= ';' && c /= ' ') (B.unpack ct)) [ "text/html", "application/xhtml+xml" ]+ handledContentTypes Nothing = False++-- | A validator which always succeeds.+--+-- Useful for selectively disabling validation. For example, if you+-- are sending down HTML fragments to an AJAX application and the+-- default validator only understands complete documents.+noopValidator :: Response -> IO Response+noopValidator = return++-- | Validate the 'Response' using an external application.+--+-- If the external application returns 0, the original response is+-- returned unmodified. If the external application returns non-zero,+-- a 'Response' containing the error messages and original response+-- body is returned instead.+--+-- This function also takes a predicate filter which is applied to the+-- content-type of the response. The filter will only be applied if+-- the predicate returns true.+--+-- NOTE: This function requires the use of -threaded to avoid+-- blocking. However, you probably need that for Happstack anyway.+--+-- See also: 'wdgHTMLValidator'.+lazyProcValidator :: FilePath -- ^ name of executable+ -> [String] -- ^ arguments to pass to the executable+ -> Maybe FilePath -- ^ optional path to working directory+ -> Maybe [(String, String)] -- ^ optional environment (otherwise inherit)+ -> (Maybe B.ByteString -> Bool) -- ^ content-type filter+ -> Response -- ^ Response to validate+ -> IO Response+lazyProcValidator exec args wd env mimeTypePred response+ | mimeTypePred (getHeader "content-type" response) =+ do (inh, outh, errh, ph) <- runInteractiveProcess exec args wd env+ out <- hGetContents outh+ err <- hGetContents errh+ forkIO $ do L.hPut inh (rsBody response)+ hClose inh+ forkIO $ evaluate (length out) >> return ()+ forkIO $ evaluate (length err) >> return ()+ ec <- waitForProcess ph+ case ec of+ ExitSuccess -> return response+ (ExitFailure _) ->+ return $ toResponse (unlines ([ "ExitCode: " ++ show ec+ , "stdout:"+ , out+ , "stderr:"+ , err+ , "input:"+ ] +++ showLines (rsBody response)))+ | otherwise = return response+ where+ column = " " ++ (take 120 $ concatMap (\n -> " " ++ show n) (drop 1 $ cycle [0..9::Int]))+ showLines :: L.ByteString -> [String]+ showLines string = column : zipWith (\n -> \l -> show n ++ " " ++ (L.unpack l)) [1::Integer ..] (L.lines string)
src/Happstack/Server/XSLT.hs view
@@ -1,15 +1,24 @@ {-# LANGUAGE TemplateHaskell, FlexibleInstances , UndecidableInstances, DeriveDataTypeable, MultiParamTypeClasses, CPP, ScopedTypeVariables, ScopedTypeVariables #-}+-- | Functions to allow you to use XSLT to transform your output. To use this, you would generally design your happstack application to output XML. The xslt filter will then run an external tool which performs the tranforms. The transformed result will then be sent to the http client as the Response.+--+-- NOTE: This module is currently looking for a maintainer. If you want to improve XSLT support in Happstack, please volunteer! module Happstack.Server.XSLT- (xsltFile, xsltString, xsltElem, xsltFPS, xsltFPSIO, XSLPath,- xsltproc,saxon,procFPSIO,procLBSIO,XSLTCommand,XSLTCmd+ (xsltFile, xsltString, {- xsltElem, -} xsltFPS, xsltFPSIO, XSLPath,+ xslt, doXslt, xsltproc,saxon,procFPSIO,procLBSIO,XSLTCommand,XSLTCmd ) where import System.Log.Logger -import Happstack.Server.MinHaXML+import Control.Monad+import Control.Monad.Trans+import qualified Data.ByteString.Char8 as B+import Happstack.Server.SimpleHTTP++import Happstack.Server.Types+-- import Happstack.Server.MinHaXML import Happstack.Util.Common(runCommand) import Control.Exception.Extensible(bracket,try,SomeException) import qualified Data.ByteString.Char8 as P@@ -18,7 +27,7 @@ import System.Environment(getEnv) import System.IO import System.IO.Unsafe(unsafePerformIO)-import Text.XML.HaXml.Verbatim(verbatim)+-- import Text.XML.HaXml.Verbatim(verbatim) import Happstack.Data hiding (Element) logMX :: Priority -> String -> IO ()@@ -39,12 +48,12 @@ -> (FilePath, [String]) xsltCmd XSLTProc = xsltproc' xsltCmd Saxon = saxon'-+{- -- | Uses 'xsltString' to transform the given XML 'Element' into a -- a 'String'. xsltElem :: XSLPath -> Element -> String xsltElem xsl = xsltString xsl . verbatim-+-} procLBSIO :: XSLTCmd -> XSLPath -> L.ByteString -> IO L.ByteString procLBSIO xsltp' xsl inp = @@ -155,3 +164,24 @@ tryAny :: [IO a] -> IO a -> IO a tryAny [] c = c tryAny (x:xs) c = either (\(_::SomeException) -> tryAny xs c) return =<< try x++-- | Use @cmd@ to transform XML against @xslPath@. This function only+-- acts if the content-type is @application\/xml@.+xslt :: (MonadIO m, MonadPlus m, ToMessage r) =>+ XSLTCmd -- ^ XSLT preprocessor. Usually 'xsltproc' or 'saxon'.+ -> XSLPath -- ^ Path to xslt stylesheet.+ -> m r -- ^ Affected 'ServerPart's.+ -> m Response+xslt cmd xslPath parts = do+ res <- parts+ if toContentType res == B.pack "application/xml"+ then doXslt cmd xslPath (toResponse res)+ else return (toResponse res)++doXslt :: (MonadIO m) =>+ XSLTCmd -> XSLPath -> Response -> m Response+doXslt cmd xslPath res =+ do new <- liftIO $ procLBSIO cmd xslPath $ rsBody res+ return $ setHeader "Content-Type" "text/html" $+ setHeader "Content-Length" (show $ L.length new) $+ res { rsBody = new }
tests/Happstack/Server/Tests.hs view
@@ -1,12 +1,28 @@ -- |HUnit tests and QuickQuick properties for Happstack.Server.* module Happstack.Server.Tests (allTests) where -import Test.HUnit as HU (Test(..),(~:),(~?),(@?=),(@=?))+import qualified Codec.Compression.GZip as GZ+import qualified Codec.Compression.Zlib as Z+import Control.Arrow ((&&&))+import Control.Applicative ((<$>))+import Control.Concurrent.MVar+import Control.Monad (msum)+import Control.Monad.Trans (liftIO)+import Data.ByteString.Lazy.Char8 (pack, unpack)+import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy as L+import qualified Data.Map as Map+import Happstack.Server ( Request(..), Method(..), Response(..), ServerPart(..), Headers(..), RqBody(Body), HttpVersion(..)+ , ToMessage(..), HeaderPair(..), ok, dir, simpleHTTP'', composeFilter, noContentLength)+import Happstack.Server.FileServe.BuildingBlocks (sendFileResponse) import Happstack.Server.Cookie-import Happstack.Server.Parts+import Happstack.Server.Internal.Compression+import Happstack.Server.Internal.Cookie+import Happstack.Server.Internal.Multipart+import Happstack.Server.Internal.MessageWrap+import Happstack.Server.SURI(ToSURI(..), SURI(..),path,query)+import Test.HUnit as HU (Test(..),(~:),(~?),(@?=),(@=?), assertEqual, assertFailure) import Text.ParserCombinators.Parsec-import Data.ByteString.Lazy.Char8 (pack)-import Happstack.Server.HTTP.Multipart -- |All of the tests for happstack-util should be listed here. allTests :: Test@@ -14,6 +30,7 @@ "happstack-server tests" ~: [ cookieParserTest , acceptEncodingParserTest , splitMultipart + , compressFilterResponseTest ] cookieParserTest :: Test@@ -55,7 +72,8 @@ splitMultipart :: Test splitMultipart =- "split multipart" ~: + "split multipart" ~: assertFailure "update splitMultipart test"+{- [ Just [pack "1"] @=? splitParts (pack "boundary") (pack "beg\r\n--boundary\r\n1\r\n--boundary--\r\nend")@@ -72,5 +90,94 @@ splitParts (pack "boundary") (pack "beg\r\n--boundary\r\n\r\n1\n\r\r\n--boundary--\r\nend") ]+-}+compressFilterResponseTest :: Test+compressFilterResponseTest =+ "compressFilterResponseTest" ~: + [ uncompressedResponse + , uncompressedSendFile+ , compressedResponseGZ+ , compressedResponseZ+ , compressedSendFile+ , compressedSendFileNoIdentity+ ] +mkRequest :: Method -> String -> [(String, Cookie)] -> Headers -> L.ByteString -> IO Request+mkRequest method uri cookies headers body = + do let u = toSURI uri+ ib <- newEmptyMVar + b <- newMVar (Body body)+ return $ Request { rqMethod = method+ , rqPaths = (pathEls (path u))+ , rqUri = (path u)+ , rqQuery = (query u)+ , rqInputsQuery = (queryInput u)+ , rqInputsBody = ib+ , rqCookies = cookies+ , rqVersion = HttpVersion 1 1+ , rqHeaders = headers+ , rqBody = b+ , rqPeer = ("",0)+ } +compressPart :: ServerPart Response+compressPart =+ do compressedResponseFilter+ composeFilter noContentLength+ msum [ dir "response" $ ok (toResponse "compress Response")+ , dir "sendfile" $ ok (sendFileResponse "text/plain" "/dev/null" Nothing 0 100)+ ]++uncompressedResponse :: Test+uncompressedResponse =+ "uncompressedResponse" ~:+ do req <- mkRequest GET "/response" [] Map.empty L.empty+ res <- simpleHTTP'' compressPart req+ assertEqual "respone code" (rsCode res) 200+ assertEqual "body" (unpack (rsBody res)) "compress Response"+ assertEqual "Content-Encoding" ((hName &&& hValue) <$> Map.lookup (B.pack "content-encoding") (rsHeaders res)) Nothing++uncompressedSendFile :: Test+uncompressedSendFile =+ "uncompressedSendFile" ~:+ do req <- mkRequest GET "/sendfile" [] Map.empty L.empty+ res <- simpleHTTP'' compressPart req+ assertEqual "respone code" (rsCode res) 200+ assertEqual "filepath" (sfFilePath res) "/dev/null"+ assertEqual "Content-Encoding" ((hName &&& hValue) <$> Map.lookup (B.pack "content-encoding") (rsHeaders res)) Nothing++compressedResponseGZ :: Test+compressedResponseGZ =+ "compressedResponseGZ" ~:+ do req <- mkRequest GET "/response" [] (Map.singleton (B.pack "accept-encoding") (HeaderPair (B.pack "Accept-Encoding") [B.pack " gzip;q=1"])) L.empty+ res <- simpleHTTP'' compressPart req+ assertEqual "respone code" (rsCode res) 200+ assertEqual "body" (unpack (GZ.decompress (rsBody res))) ("compress Response")+ assertEqual "Content-Encoding" ((hName &&& hValue) <$> Map.lookup (B.pack "content-encoding") (rsHeaders res)) (Just (B.pack "Content-Encoding", [B.pack "gzip"]))++compressedResponseZ :: Test+compressedResponseZ =+ "compressedResponseZ" ~:+ do req <- mkRequest GET "/response" [] (Map.singleton (B.pack "accept-encoding") (HeaderPair (B.pack "Accept-Encoding") [B.pack " deflate;q=1"])) L.empty+ res <- simpleHTTP'' compressPart req+ assertEqual "respone code" (rsCode res) 200+ assertEqual "body" (unpack (Z.decompress (rsBody res))) ("compress Response")+ assertEqual "Content-Encoding" ((hName &&& hValue) <$> Map.lookup (B.pack "content-encoding") (rsHeaders res)) (Just (B.pack "Content-Encoding", [B.pack "deflate"]))++compressedSendFile :: Test+compressedSendFile =+ "compressedSendfile" ~:+ do req <- mkRequest GET "/sendfile" [] (Map.singleton (B.pack "accept-encoding") (HeaderPair (B.pack "Accept-Encoding") [B.pack " gzip;q=1"])) L.empty+ res <- simpleHTTP'' compressPart req+ assertEqual "respone code" (rsCode res) 200+ assertEqual "filepath" (sfFilePath res) "/dev/null"+ assertEqual "Content-Encoding" ((hName &&& hValue) <$> Map.lookup (B.pack "content-encoding") (rsHeaders res)) Nothing++compressedSendFileNoIdentity :: Test+compressedSendFileNoIdentity =+ "compressedSendFileNoIdentity" ~:+ do req <- mkRequest GET "/sendfile" [] (Map.singleton (B.pack "accept-encoding") (HeaderPair (B.pack "Accept-Encoding") [B.pack " gzip;q=1, identity: q=0.0"])) L.empty+ res <- simpleHTTP'' compressPart req+ assertEqual "respone code" (rsCode res) 406+ assertEqual "body" (unpack (rsBody res)) ""+ assertEqual "Content-Encoding" ((hName &&& hValue) <$> Map.lookup (B.pack "content-encoding") (rsHeaders res)) Nothing