happstack-server 0.2.1 → 7.9.3
raw patch · 61 files changed
Files
- README.md +13/−0
- Setup.hs +1/−1
- happstack-server.cabal +110/−71
- src/Happstack/Server.hs +90/−14
- src/Happstack/Server/Auth.hs +103/−0
- src/Happstack/Server/Compression.hs +23/−0
- src/Happstack/Server/Cookie.hs +38/−128
- src/Happstack/Server/Error.hs +57/−0
- src/Happstack/Server/FileServe.hs +20/−0
- src/Happstack/Server/FileServe/BuildingBlocks.hs +716/−0
- src/Happstack/Server/HTTP/Client.hs +0/−50
- src/Happstack/Server/HTTP/Clock.hs +0/−30
- src/Happstack/Server/HTTP/FileServe.hs +0/−215
- src/Happstack/Server/HTTP/Handler.hs +0/−322
- src/Happstack/Server/HTTP/LazyLiner.hs +0/−41
- src/Happstack/Server/HTTP/Listen.hs +0/−58
- src/Happstack/Server/HTTP/LowLevel.hs +0/−28
- src/Happstack/Server/HTTP/Multipart.hs +0/−216
- src/Happstack/Server/HTTP/RFC822Headers.hs +0/−266
- src/Happstack/Server/HTTP/Socket.hs +0/−52
- src/Happstack/Server/HTTP/SocketTH.hs +0/−16
- src/Happstack/Server/HTTP/Types.hs +0/−252
- src/Happstack/Server/HTTPClient/HTTP.hs +0/−1019
- src/Happstack/Server/HTTPClient/Stream.hs +0/−173
- src/Happstack/Server/HTTPClient/TCP.hs +0/−193
- src/Happstack/Server/I18N.hs +52/−0
- src/Happstack/Server/Internal/Clock.hs +58/−0
- src/Happstack/Server/Internal/Compression.hs +276/−0
- src/Happstack/Server/Internal/Cookie.hs +231/−0
- src/Happstack/Server/Internal/Handler.hs +404/−0
- src/Happstack/Server/Internal/LazyLiner.hs +41/−0
- src/Happstack/Server/Internal/Listen.hs +140/−0
- src/Happstack/Server/Internal/LogFormat.hs +57/−0
- src/Happstack/Server/Internal/LowLevel.hs +28/−0
- src/Happstack/Server/Internal/MessageWrap.hs +153/−0
- src/Happstack/Server/Internal/Monads.hs +800/−0
- src/Happstack/Server/Internal/Multipart.hs +281/−0
- src/Happstack/Server/Internal/RFC822Headers.hs +272/−0
- src/Happstack/Server/Internal/Socket.hs +55/−0
- src/Happstack/Server/Internal/TimeoutIO.hs +23/−0
- src/Happstack/Server/Internal/TimeoutManager.hs +84/−0
- src/Happstack/Server/Internal/TimeoutSocket.hs +98/−0
- src/Happstack/Server/Internal/Types.hs +536/−0
- src/Happstack/Server/JSON.hs +0/−29
- src/Happstack/Server/MessageWrap.hs +0/−99
- src/Happstack/Server/MinHaXML.hs +0/−215
- src/Happstack/Server/Monads.hs +118/−0
- src/Happstack/Server/Parts.hs +0/−153
- src/Happstack/Server/Response.hs +334/−0
- src/Happstack/Server/Routing.hs +257/−0
- src/Happstack/Server/RqData.hs +720/−0
- src/Happstack/Server/S3.hs +0/−240
- src/Happstack/Server/SURI.hs +51/−15
- src/Happstack/Server/SURI/ParseURI.hs +21/−1
- src/Happstack/Server/SimpleHTTP.hs +235/−1355
- src/Happstack/Server/StdConfig.hs +0/−22
- src/Happstack/Server/Types.hs +19/−0
- src/Happstack/Server/Validation.hs +135/−0
- src/Happstack/Server/XSLT.hs +0/−157
- tests/Happstack/Server/Tests.hs +234/−13
- tests/Test.hs +1/−1
+ README.md view
@@ -0,0 +1,13 @@+# happstack-server [][hackage]++[hackage]: https://hackage.haskell.org/package/happstack-server++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.++## Install++There are packages available on [hackage][] and [stack](https://www.stackage.org/lts-3.12/package/happstack-server-7.4.5).++## Documentation ++Please refer to the [Documentation on Hackage][hackage].
Setup.hs view
@@ -1,3 +1,3 @@ #!/usr/bin/env runhaskell import Distribution.Simple-main = defaultMainWithHooks defaultUserHooks+main = defaultMainWithHooks simpleUserHooks
happstack-server.cabal view
@@ -1,108 +1,147 @@ Name: happstack-server-Version: 0.2.1+Version: 7.9.3 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 Maintainer: Happstack team <happs@googlegroups.com> homepage: http://happstack.com-Category: Web, Distributed Computing+Category: Web, Happstack Build-Type: Simple-Cabal-Version: >= 1.4+Cabal-Version: >= 1.10+Extra-Source-Files: tests/Happstack/Server/Tests.hs README.md -Flag base4- Description: Choose the even newer, even smaller, split-up base package.+tested-with:+ GHC == 9.14.1+ GHC == 9.12.2+ GHC == 9.10.2+ GHC == 9.8.2+ GHC == 9.6.7+ GHC == 9.4.8+ GHC == 9.2.8+ GHC == 9.0.2+ GHC == 8.10.7+ GHC == 8.8.4+ GHC == 8.6.5+ GHC == 8.4.4+ GHC == 8.2.2+ GHC == 8.0.2 -Flag tests- Description: Build the testsuite, and include the tests in the library- Default: False+source-repository head+ type: git+ location: https://github.com/Happstack/happstack-server.git -Library+flag network-uri+ description: Get Network.URI from the network-uri package+ default: True +Library+ Default-language: Haskell2010 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.JSON- Happstack.Server.MessageWrap- Happstack.Server.MinHaXML+ Happstack.Server.Compression+ Happstack.Server.Error+ Happstack.Server.FileServe+ Happstack.Server.FileServe.BuildingBlocks+ Happstack.Server.I18N+ Happstack.Server.Internal.Compression+ Happstack.Server.Internal.Cookie+ Happstack.Server.Internal.Handler+ Happstack.Server.Internal.Types+ Happstack.Server.Internal.Listen+ Happstack.Server.Internal.LowLevel+ Happstack.Server.Internal.LogFormat+ Happstack.Server.Internal.MessageWrap+ Happstack.Server.Internal.Multipart+ Happstack.Server.Internal.RFC822Headers+ Happstack.Server.Internal.Socket+ Happstack.Server.Internal.TimeoutIO+ Happstack.Server.Internal.TimeoutManager+ Happstack.Server.Internal.TimeoutSocket+ Happstack.Server.Internal.Monads+ Happstack.Server.Monads+ Happstack.Server.Response+ Happstack.Server.Routing+ Happstack.Server.RqData Happstack.Server.SURI- Happstack.Server.XSLT- Happstack.Server.StdConfig- Happstack.Server.Parts- if flag(tests)- Exposed-modules: - Happstack.Server.Tests- Other-modules: - Happstack.Server.S3- 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.SimpleHTTP+ Happstack.Server.Types+ Happstack.Server.Validation+ Other-modules:+ Happstack.Server.Internal.Clock+ Happstack.Server.Internal.LazyLiner Happstack.Server.SURI.ParseURI Paths_happstack_server - Build-Depends: base,+ if flag(network-uri)+ build-depends: network >= 3.0.0 && < 3.3,+ network-uri >= 2.6 && < 2.7+ else+ build-depends: network < 2.6+ Build-Depends: base >= 4 && < 5,+ base64-bytestring >= 1.0 && < 1.3,+ blaze-html >= 0.5 && < 0.10, bytestring, containers,- directory,+ directory >=1.2,+ exceptions, extensible-exceptions,- HaXml >= 1.13 && < 1.14,- hslogger >= 1.0.2,- happstack-data >= 0.2.1 && < 0.3,- happstack-util >= 0.2.1 && < 0.3,+ filepath,+ hslogger >= 1.0.2, html,- MaybeT,- mtl,- network,- old-locale,- old-time,- parsec < 3,+ monad-control >= 1.0 && < 1.1,+ mtl >= 2.2 && < 2.4,+ parsec < 4, process,- template-haskell,- time,- xhtml,+ sendfile >= 0.7.1 && < 0.8,+ system-filepath >= 0.3.1,+ syb,+ text >= 0.10 && < 2.2,+ time >= 1.5,+ threads >= 0.5,+ transformers >= 0.1.3 && < 0.7,+ transformers-base >= 0.4 && < 0.5,+ utf8-string >= 0.3.4 && < 1.1,+ xhtml < 3000.4, zlib hs-source-dirs: src- if flag(tests)- hs-source-dirs: tests if !os(windows) Build-Depends: unix cpp-options: -DUNIX- if flag(base4)- Build-Depends: base >= 4 && < 5, syb - if flag(tests)- Build-Depends: HUnit+ if impl(ghc < 8.6)+ Default-Extensions: MonadFailDesugaring - Extensions: TemplateHaskell, DeriveDataTypeable, MultiParamTypeClasses,- TypeFamilies, FlexibleContexts, OverlappingInstances,- FlexibleInstances, UndecidableInstances, ScopedTypeVariables,- TypeSynonymInstances, PatternGuards+ Other-Extensions: DeriveDataTypeable, MultiParamTypeClasses,+ TypeFamilies, FlexibleContexts,+ FlexibleInstances, UndecidableInstances,+ ScopedTypeVariables, TypeSynonymInstances, PatternGuards CPP, ForeignFunctionInterface- ghc-options: -Wall- GHC-Prof-Options: -auto-all+ ghc-options: -Wall -fwarn-tabs+ -- The policy is to support GHC versions no older than the GHC stable+ -- branch that was used by the latest Haskell Platform release+ -- available 18 months ago. In order to avoid people spending time+ -- keeping the build working for older versions, we tell Cabal that+ -- it shouldn't allow builds with them.+ if impl(ghc < 8.0)+ buildable: False -Executable happstack-server-tests+Test-Suite happstack-server-tests+ Default-language: Haskell2010+ Type: exitcode-stdio-1.0 Main-Is: Test.hs+ Other-Modules: Happstack.Server.Tests GHC-Options: -threaded- Build-depends: HUnit- hs-source-dirs: tests, src- if flag(tests)- Buildable: True- else- Buildable: False+ hs-source-dirs: tests+ Build-depends: HUnit,+ base >= 4 && < 5,+ bytestring,+ containers,+ happstack-server,+ parsec < 4,+ zlib
src/Happstack/Server.hs view
@@ -1,15 +1,91 @@-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+{- |+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+ ( -- * 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+ -- * I18N+ , module Happstack.Server.I18N+ -- * Web-related Monads+ , module Happstack.Server.Monads+ -- * Output Validation+ , module Happstack.Server.Validation+ -- * HTTP Types+ , module Happstack.Server.Types+-- , module Happstack.Server.Internal.Monads+ )+ where++import Happstack.Server.SimpleHTTP (simpleHTTP+ , simpleHTTP'+ , simpleHTTP''+ , simpleHTTPWithSocket+ , simpleHTTPWithSocket'+ , bindPort+ , bindIPv4+ , parseConfig+ , waitForTermination+ )+import Happstack.Server.FileServe+import Happstack.Server.Monads+import Happstack.Server.Auth+import Happstack.Server.Cookie+import Happstack.Server.Error+import Happstack.Server.I18N+import Happstack.Server.Response+import Happstack.Server.Routing+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,103 @@+{-# LANGUAGE FlexibleContexts #-}+-- | Support for basic access authentication <http://en.wikipedia.org/wiki/Basic_access_authentication>+module Happstack.Server.Auth where++import Data.Foldable (foldl')+import Data.Bits (xor, (.|.))+import Data.Maybe (fromMaybe)+import Control.Monad (MonadPlus(mzero, mplus))+import Data.ByteString.Base64 as Base64+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as B+import qualified Data.Map as M+import Happstack.Server.Monads (Happstack, escape, getHeaderM, setHeaderM)+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 :: (Happstack m) =>+ String -- ^ the realm name+ -> M.Map String String -- ^ the username password map+ -> m a -- ^ the part to guard+ -> m a+basicAuth realmName authMap = basicAuthBy (validLoginPlaintext authMap) realmName+++-- | Generalized version of 'basicAuth'.+--+-- The function that checks the username password combination must be+-- supplied as first argument.+--+-- example:+--+-- > main = simpleHTTP nullConf $+-- > msum [ basicAuth' (validLoginPlaintext (fromList [("happstack","rocks")])) "127.0.0.1" $ ok "You are in the secret club"+-- > , ok "You are not in the secret club."+-- > ]+--+basicAuthBy :: (Happstack m) =>+ (B.ByteString -> B.ByteString -> Bool) -- ^ function that returns true if the name password combination is valid+ -> String -- ^ the realm name+ -> m a -- ^ the part to guard+ -> m a+basicAuthBy validLogin realmName xs = basicAuthImpl `mplus` xs+ where+ basicAuthImpl = do+ aHeader <- getHeaderM "authorization"+ case aHeader of+ Nothing -> err+ Just x ->+ do (name, password) <- parseHeader x+ if B.length password > 0+ && B.head password == ':'+ && validLogin name (B.tail password)+ then mzero+ else err+ parseHeader h =+ case Base64.decode . B.drop 6 $ h of+ (Left _) -> err+ (Right bs) -> return (B.break (':'==) bs)+ headerName = "WWW-Authenticate"+ headerValue = "Basic realm=\"" ++ realmName ++ "\""+ err :: (Happstack m) => m a+ err = escape $ do+ setHeaderM headerName headerValue+ unauthorized $ toResponse "Not authorized"+++-- | Function that looks up the plain text password for username in a+-- Map and returns True if it matches with the given password.+--+-- Note: The implementation is hardened against timing attacks but not+-- completely safe. Ideally you should build your own predicate, using+-- a robust constant-time equality comparison from a cryptographic+-- library like sodium.+validLoginPlaintext ::+ M.Map String String -- ^ the username password map+ -> B.ByteString -- ^ the username+ -> B.ByteString -- ^ the password+ -> Bool+validLoginPlaintext authMap name password = fromMaybe False $ do+ r <- M.lookup (B.unpack name) authMap+ pure (constTimeEq (B.pack r) password)+ where+ -- (Mostly) constant time equality of bytestrings to prevent timing attacks by testing out passwords. This still+ -- allows to extract the length of the configured password via timing attacks. This implementation is still brittle+ -- in the sense that it relies on GHC not unrolling or vectorizing the loop.+ {-# NOINLINE constTimeEq #-}+ constTimeEq :: BS.ByteString -> BS.ByteString -> Bool+ constTimeEq x y+ | BS.length x /= BS.length y+ = False++ | otherwise+ = foldl' (.|.) 0 (BS.zipWith xor x y) == 0
+ src/Happstack/Server/Compression.hs view
@@ -0,0 +1,23 @@+{-# LANGUAGE NoMonomorphismRestriction, FlexibleContexts #-}+-- | Filter for compressing the 'Response' body.+module Happstack.Server.Compression+ ( compressedResponseFilter+ , compressedResponseFilter'+ , compressWithFilter+ , gzipFilter+ , deflateFilter+ , identityFilter+ , starFilter+ , standardEncodingHandlers+ ) where++import Happstack.Server.Internal.Compression ( compressedResponseFilter+ , compressedResponseFilter'+ , compressWithFilter+ , gzipFilter+ , deflateFilter+ , identityFilter+ , starFilter+ , standardEncodingHandlers+ )+
src/Happstack/Server/Cookie.hs view
@@ -1,138 +1,48 @@-{-# LANGUAGE DeriveDataTypeable #-}---- http://tools.ietf.org/html/rfc2109+{-# LANGUAGE FlexibleContexts #-}+-- | Functions for creating, adding, and expiring cookies. To lookup cookie values see "Happstack.Server.RqData". module Happstack.Server.Cookie ( Cookie(..)+ , CookieLife(..)+ , SameSite(..) , mkCookie- , mkCookieHeader- , getCookies- , getCookie- , getCookies'- , getCookie'- , parseCookies- , cookiesParser+ , addCookie+ , addCookies+ , expireCookie ) where -import qualified Data.ByteString.Char8 as C-import Data.Either-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- } 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---- | 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 @\' \'@.-mkCookieHeader :: Seconds -> Cookie -> String-mkCookieHeader sec cookie =- let l = [("Domain=",s cookieDomain)- ,("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 ])--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<-attr- cookieEq- val<-value- path<-option "" $ try (cookieSep >> cookie_path)- domain<-option "" $ try (cookieSep >> cookie_domain)- return $ Cookie ver path domain (low name) val- 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- attr = token- value = word- word = try (quoted_string) <|> incomp_token-- -- Parsers based on RFC 2068- token = many1 $ oneOf ((chars \\ ctl) \\ tspecials)- quoted_string = do- char '"'- r <-many (oneOf qdtext)- char '"'- return r-- -- Custom parser, incompatible with RFC 2068, but very forgiving ;)- incomp_token = many1 $ oneOf ((chars \\ ctl) \\ " \t\";")-- -- Primitives from RFC 2068- tspecials = "()<>@,;:\\\"/[]?={} \t"- 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+import Control.Monad.Trans (MonadIO(..))+import Happstack.Server.Internal.Monads (FilterMonad, composeFilter)+import Happstack.Server.Internal.Cookie (Cookie(..), CookieLife(..), SameSite(..), calcLife, mkCookie, mkCookieHeader)+import Happstack.Server.Types (Response, addHeader) -getCookies' :: Monad m => C.ByteString -> m (Either String [Cookie])-getCookies' header | C.null header = return $ Right []- | otherwise = return $ parseCookies (C.unpack header)+-- | Add the 'Cookie' to 'Response'.+--+-- 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 name = addCookie Expired (mkCookie name "")
+ 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.Trans.Except (ExceptT, runExceptT)+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 (ExceptT e m) a+ -> UnWebT m a+spUnwrapErrorT handler rq = \x -> do+ err <- runExceptT 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) => (Request -> e -> WebT m a) -> ServerPartT (ExceptT e m) a -> ServerPartT m a+errorHandlerSP handler sps = withRequest $ \req -> mkWebT $ do+ eer <- runExceptT $ 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,20 @@+-- | functions for serving static files from the disk+module Happstack.Server.FileServe + ( -- * Serving Functions + Browsing(..)+ , serveDirectory+ , serveFile+ , serveFileFrom+ -- * 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,716 @@+{-# LANGUAGE CPP, 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,+ serveDirectory',+ -- ** Serving a single file+ serveFile,+ serveFileFrom,+ serveFileUsing,+ -- * Low-Level+ sendFileResponse,+ lazyByteStringResponse,+ strictByteStringResponse,+ filePathSendFile,+ filePathLazy,+ filePathStrict,+ -- * Content-Type \/ Mime-Type+ MimeMap,+ mimeTypes,+ asContentType,+ guessContentType,+ guessContentTypeM,+ -- * Directory Browsing+ EntryKind(..),+ browseIndex,+ renderDirectoryContents,+ renderDirectoryContentsTable,+ -- * Other+ blockDotFiles,+ defaultIxFiles,+ combineSafe,+ isSafePath,+ tryIndex,+ doIndex,+ doIndex',+ doIndexLazy,+ doIndexStrict,+ fileNotFound,+ isDot+ ) where++import Control.Exception.Extensible as E (IOException, bracket, catch)+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.Char (toLower)+import Data.Data (Data)+import Data.List (sort)+import Data.Maybe (fromMaybe)+import Data.Map (Map)+import qualified Data.Map as Map+import Data.Time (UTCTime, formatTime, defaultTimeLocale)+import Filesystem.Path.CurrentOS (commonPrefix, encodeString, decodeString, collapse, append)+import Happstack.Server.Monads (ServerMonad(askRq), FilterMonad, WebMonad)+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, hasDrive, isPathSeparator, joinPath, takeExtension, isValid)+import System.IO (IOMode(ReadMode), hFileSize, hClose, openBinaryFile, withBinaryFile)+import System.Log.Logger (Priority(DEBUG), logM)+import Text.Blaze.Html ((!))+import qualified Text.Blaze.Html5 as H+import qualified Text.Blaze.Html5.Attributes as A++-- * 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 (map toLower ext) mimeMap -- FIXME? @foldCase@ would be more proper than @map toLower@ but would add a dependency. But are those edge cases ever going to be relevant here?++-- | 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 argumebnt 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+--+-- NOTE: this does not perform case folding. So @example.jpg@ will return @.jpg@ and @example.JPG@ will return @.JPG@.+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 (UTCTime, 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 offset 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 (UTCTime, 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 (UTCTime, 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 count <- liftIO $ withBinaryFile fp ReadMode hFileSize -- garbage collection should close this+ modtime <- liftIO $ getModificationTime fp+ rq <- askRq+ return $ sendFileResponse contentType fp (Just (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 (modtime, rq)) 0 count++-- | Send the specified file with the specified mime-type using strict 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 $ withBinaryFile fp ReadMode hFileSize+ rq <- askRq+ return $ strictByteStringResponse contentType contents (Just (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++-- | Like 'serveFile', but uses 'combineSafe' to prevent directory+-- traversal attacks when the path to the file is supplied by the user.+serveFileFrom :: (ServerMonad m, FilterMonad Response m, MonadIO m, MonadPlus m) =>+ FilePath -- ^ directory wherein served files must be contained+ -> (FilePath -> m String) -- ^ function for determining content-type of file. Typically 'asContentType' or 'guessContentTypeM'+ -> FilePath -- ^ path to the file to serve+ -> m Response+serveFileFrom root mimeFn fp =+ maybe no yes $ combineSafe root fp+ where+ no = forbidden $ toResponse "Directory traversal forbidden"+ yes = serveFile mimeFn++-- ** 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+ if (not $ isSafePath (rqPaths rq))+ then do liftIO $ logM "Happstack.Server.FileServe" DEBUG ("fileServe: unsafe filepath " ++ show (rqPaths rq))+ mzero+ else do let fp = joinPath (localPath : rqPaths rq)+ 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 fp+ else do let path' = addTrailingPathSeparator (rqUri rq)+ seeOther path' (toResponse path')+ else if fe+ then serveFileUsing serveFn mimeFn fp+ else mzero++-- | Combine two 'FilePath's, ensuring that the resulting path leads to+-- a file within the first 'FilePath'.+--+-- >>> combineSafe "/var/uploads/" "etc/passwd"+-- Just "/var/uploads/etc/passwd"+-- >>> combineSafe "/var/uploads/" "/etc/passwd"+-- Nothing+-- >>> combineSafe "/var/uploads/" "../../etc/passwd"+-- Nothing+-- >>> combineSafe "/var/uploads/" "../uploads/home/../etc/passwd"+-- Just "/var/uploads/etc/passwd"+combineSafe :: FilePath -> FilePath -> Maybe FilePath+combineSafe root path =+ if commonPrefix [root', joined] == root'+ then Just $ encodeString joined+ else Nothing+ where+ root' = decodeString root+ path' = decodeString path+ joined = collapse $ append root' path'++isSafePath :: [FilePath] -> Bool+isSafePath [] = True+isSafePath (s:ss) =+ isValid s+ && (all (not . isPathSeparator) s)+ && not (hasDrive s)+ && not (isParent s)+ && isSafePath ss++-- note: could be different on other OSs+isParent :: FilePath -> Bool+isParent ".." = True+isParent _ = False++-- | 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 (/= ".") (sort c)+ ok $ toResponse $ listing++data EntryKind = File | Directory | UnknownKind deriving (Eq, Ord, Read, Show, Data, 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.toHtml "Directory Listing"+ H.meta ! A.httpEquiv (H.toValue "Content-Type") ! A.content (H.toValue "text/html;charset=utf-8")+ H.style $ H.toHtml $ 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.body $ do+ H.h1 $ H.toHtml "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 UTCTime, Maybe Integer, EntryKind)] -- ^ list of files+meta data, see 'getMetaData'+ -> H.Html+renderDirectoryContentsTable fps =+ H.table $ do H.thead $ do H.th $ H.toHtml ""+ H.th $ H.toHtml "Name"+ H.th $ H.toHtml "Last modified"+ H.th $ H.toHtml "Size"+ H.tbody $ mapM_ mkRow (zip fps $ cycle [False, True])+ where+ mkRow :: ((FilePath, Maybe UTCTime, Maybe Integer, EntryKind), Bool) -> H.Html+ mkRow ((fp, modTime, count, kind), alt) =+ (if alt then (! A.class_ (H.toValue "alt")) else id) $+ H.tr $ do+ H.td (mkKind kind)+ H.td (H.a ! A.href (H.toValue fp) $ H.toHtml fp)+ H.td ! A.class_ (H.toValue "date") $ (H.toHtml $ maybe "-" (formatTime defaultTimeLocale "%d-%b-%Y %X %Z") modTime)+ (maybe id (\c -> (! A.title (H.toValue (show c)))) count) (H.td ! A.class_ (H.toValue "size") $ (H.toHtml $ maybe "-" prettyShow count))+ mkKind :: EntryKind -> H.Html+ mkKind File = return ()+ mkKind Directory = H.toHtml "➦"+ 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 UTCTime, Maybe Integer, EntryKind)+getMetaData localPath fp =+ do let localFp = localPath </> fp+ modTime <- (Just <$> getModificationTime localFp) `E.catch`+ (\(_ :: IOException) -> return Nothing)+ count <- do de <- doesDirectoryExist localFp+ if de+ then do return Nothing+ else do bracket (openBinaryFile localFp ReadMode) hClose (fmap Just . hFileSize)+ `E.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 (if kind == Directory then (fp ++ "/") else fp, modTime, count, kind)++-- | see 'serveDirectory'+data Browsing+ = EnableBrowsing | DisableBrowsing+ deriving (Eq, Enum, Ord, Read, Show, Data)++-- | 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 =+ serveDirectory' browsing ixFiles mimeFn localPath+ where+ mimeFn = guessContentTypeM mimeTypes+++-- | like 'serveDirectory' but with custom mimeTypes+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 -> m String) -- ^ function which returns the mime-type for FilePath+ -> FilePath -- ^ file/directory to serve+ -> m Response+serveDirectory' browsing ixFiles mimeFn localPath =+ fileServe' serveFn mimeFn indexFn localPath+ where+ serveFn = filePathSendFile+ 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 http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/docs/conf/mime.types?view=co+mimeTypes :: MimeMap+mimeTypes = Map.fromList [("gz","application/x-gzip"),("cabal","text/x-cabal"),("ez","application/andrew-inset"),("aw","application/applixware"),("atom","application/atom+xml"),("atomcat","application/atomcat+xml"),("atomsvc","application/atomsvc+xml"),("ccxml","application/ccxml+xml"),("cdmia","application/cdmi-capability"),("cdmic","application/cdmi-container"),("cdmid","application/cdmi-domain"),("cdmio","application/cdmi-object"),("cdmiq","application/cdmi-queue"),("cu","application/cu-seeme"),("davmount","application/davmount+xml"),("dbk","application/docbook+xml"),("dssc","application/dssc+der"),("xdssc","application/dssc+xml"),("ecma","application/ecmascript"),("emma","application/emma+xml"),("epub","application/epub+zip"),("exi","application/exi"),("pfr","application/font-tdpfr"),("gml","application/gml+xml"),("gpx","application/gpx+xml"),("gxf","application/gxf"),("stk","application/hyperstudio"),("ink","application/inkml+xml"),("inkml","application/inkml+xml"),("ipfix","application/ipfix"),("jar","application/java-archive"),("ser","application/java-serialized-object"),("class","application/java-vm"),("js","application/javascript"),("json","application/json"),("jsonml","application/jsonml+json"),("lostxml","application/lost+xml"),("hqx","application/mac-binhex40"),("cpt","application/mac-compactpro"),("mads","application/mads+xml"),("mrc","application/marc"),("mrcx","application/marcxml+xml"),("ma","application/mathematica"),("nb","application/mathematica"),("mb","application/mathematica"),("mathml","application/mathml+xml"),("mbox","application/mbox"),("mscml","application/mediaservercontrol+xml"),("metalink","application/metalink+xml"),("meta4","application/metalink4+xml"),("mets","application/mets+xml"),("mods","application/mods+xml"),("m21","application/mp21"),("mp21","application/mp21"),("mp4s","application/mp4"),("doc","application/msword"),("dot","application/msword"),("mxf","application/mxf"),("bin","application/octet-stream"),("dms","application/octet-stream"),("lrf","application/octet-stream"),("mar","application/octet-stream"),("so","application/octet-stream"),("dist","application/octet-stream"),("distz","application/octet-stream"),("pkg","application/octet-stream"),("bpk","application/octet-stream"),("dump","application/octet-stream"),("elc","application/octet-stream"),("deploy","application/octet-stream"),("oda","application/oda"),("opf","application/oebps-package+xml"),("ogx","application/ogg"),("omdoc","application/omdoc+xml"),("onetoc","application/onenote"),("onetoc2","application/onenote"),("onetmp","application/onenote"),("onepkg","application/onenote"),("oxps","application/oxps"),("xer","application/patch-ops-error+xml"),("pdf","application/pdf"),("pgp","application/pgp-encrypted"),("asc","application/pgp-signature"),("sig","application/pgp-signature"),("prf","application/pics-rules"),("p10","application/pkcs10"),("p7m","application/pkcs7-mime"),("p7c","application/pkcs7-mime"),("p7s","application/pkcs7-signature"),("p8","application/pkcs8"),("ac","application/pkix-attr-cert"),("cer","application/pkix-cert"),("crl","application/pkix-crl"),("pkipath","application/pkix-pkipath"),("pki","application/pkixcmp"),("pls","application/pls+xml"),("ai","application/postscript"),("eps","application/postscript"),("ps","application/postscript"),("cww","application/prs.cww"),("pskcxml","application/pskc+xml"),("rdf","application/rdf+xml"),("rif","application/reginfo+xml"),("rnc","application/relax-ng-compact-syntax"),("rl","application/resource-lists+xml"),("rld","application/resource-lists-diff+xml"),("rs","application/rls-services+xml"),("gbr","application/rpki-ghostbusters"),("mft","application/rpki-manifest"),("roa","application/rpki-roa"),("rsd","application/rsd+xml"),("rss","application/rss+xml"),("rtf","application/rtf"),("sbml","application/sbml+xml"),("scq","application/scvp-cv-request"),("scs","application/scvp-cv-response"),("spq","application/scvp-vp-request"),("spp","application/scvp-vp-response"),("sdp","application/sdp"),("setpay","application/set-payment-initiation"),("setreg","application/set-registration-initiation"),("shf","application/shf+xml"),("smi","application/smil+xml"),("smil","application/smil+xml"),("rq","application/sparql-query"),("srx","application/sparql-results+xml"),("gram","application/srgs"),("grxml","application/srgs+xml"),("sru","application/sru+xml"),("ssdl","application/ssdl+xml"),("ssml","application/ssml+xml"),("tei","application/tei+xml"),("teicorpus","application/tei+xml"),("tfi","application/thraud+xml"),("tsd","application/timestamped-data"),("plb","application/vnd.3gpp.pic-bw-large"),("psb","application/vnd.3gpp.pic-bw-small"),("pvb","application/vnd.3gpp.pic-bw-var"),("tcap","application/vnd.3gpp2.tcap"),("pwn","application/vnd.3m.post-it-notes"),("aso","application/vnd.accpac.simply.aso"),("imp","application/vnd.accpac.simply.imp"),("acu","application/vnd.acucobol"),("atc","application/vnd.acucorp"),("acutc","application/vnd.acucorp"),("air","application/vnd.adobe.air-application-installer-package+zip"),("fcdt","application/vnd.adobe.formscentral.fcdt"),("fxp","application/vnd.adobe.fxp"),("fxpl","application/vnd.adobe.fxp"),("xdp","application/vnd.adobe.xdp+xml"),("xfdf","application/vnd.adobe.xfdf"),("ahead","application/vnd.ahead.space"),("azf","application/vnd.airzip.filesecure.azf"),("azs","application/vnd.airzip.filesecure.azs"),("azw","application/vnd.amazon.ebook"),("acc","application/vnd.americandynamics.acc"),("ami","application/vnd.amiga.ami"),("apk","application/vnd.android.package-archive"),("cii","application/vnd.anser-web-certificate-issue-initiation"),("fti","application/vnd.anser-web-funds-transfer-initiation"),("atx","application/vnd.antix.game-component"),("mpkg","application/vnd.apple.installer+xml"),("m3u8","application/vnd.apple.mpegurl"),("swi","application/vnd.aristanetworks.swi"),("iota","application/vnd.astraea-software.iota"),("aep","application/vnd.audiograph"),("mpm","application/vnd.blueice.multipass"),("bmi","application/vnd.bmi"),("rep","application/vnd.businessobjects"),("cdxml","application/vnd.chemdraw+xml"),("mmd","application/vnd.chipnuts.karaoke-mmd"),("cdy","application/vnd.cinderella"),("cla","application/vnd.claymore"),("rp9","application/vnd.cloanto.rp9"),("c4g","application/vnd.clonk.c4group"),("c4d","application/vnd.clonk.c4group"),("c4f","application/vnd.clonk.c4group"),("c4p","application/vnd.clonk.c4group"),("c4u","application/vnd.clonk.c4group"),("c11amc","application/vnd.cluetrust.cartomobile-config"),("c11amz","application/vnd.cluetrust.cartomobile-config-pkg"),("csp","application/vnd.commonspace"),("cdbcmsg","application/vnd.contact.cmsg"),("cmc","application/vnd.cosmocaller"),("clkx","application/vnd.crick.clicker"),("clkk","application/vnd.crick.clicker.keyboard"),("clkp","application/vnd.crick.clicker.palette"),("clkt","application/vnd.crick.clicker.template"),("clkw","application/vnd.crick.clicker.wordbank"),("wbs","application/vnd.criticaltools.wbs+xml"),("pml","application/vnd.ctc-posml"),("ppd","application/vnd.cups-ppd"),("car","application/vnd.curl.car"),("pcurl","application/vnd.curl.pcurl"),("dart","application/vnd.dart"),("rdz","application/vnd.data-vision.rdz"),("uvf","application/vnd.dece.data"),("uvvf","application/vnd.dece.data"),("uvd","application/vnd.dece.data"),("uvvd","application/vnd.dece.data"),("uvt","application/vnd.dece.ttml+xml"),("uvvt","application/vnd.dece.ttml+xml"),("uvx","application/vnd.dece.unspecified"),("uvvx","application/vnd.dece.unspecified"),("uvz","application/vnd.dece.zip"),("uvvz","application/vnd.dece.zip"),("fe_launch","application/vnd.denovo.fcselayout-link"),("dna","application/vnd.dna"),("mlp","application/vnd.dolby.mlp"),("dpg","application/vnd.dpgraph"),("dfac","application/vnd.dreamfactory"),("kpxx","application/vnd.ds-keypoint"),("ait","application/vnd.dvb.ait"),("svc","application/vnd.dvb.service"),("geo","application/vnd.dynageo"),("mag","application/vnd.ecowin.chart"),("nml","application/vnd.enliven"),("esf","application/vnd.epson.esf"),("msf","application/vnd.epson.msf"),("qam","application/vnd.epson.quickanime"),("slt","application/vnd.epson.salt"),("ssf","application/vnd.epson.ssf"),("es3","application/vnd.eszigno3+xml"),("et3","application/vnd.eszigno3+xml"),("ez2","application/vnd.ezpix-album"),("ez3","application/vnd.ezpix-package"),("fdf","application/vnd.fdf"),("mseed","application/vnd.fdsn.mseed"),("seed","application/vnd.fdsn.seed"),("dataless","application/vnd.fdsn.seed"),("gph","application/vnd.flographit"),("ftc","application/vnd.fluxtime.clip"),("fm","application/vnd.framemaker"),("frame","application/vnd.framemaker"),("maker","application/vnd.framemaker"),("book","application/vnd.framemaker"),("fnc","application/vnd.frogans.fnc"),("ltf","application/vnd.frogans.ltf"),("fsc","application/vnd.fsc.weblaunch"),("oas","application/vnd.fujitsu.oasys"),("oa2","application/vnd.fujitsu.oasys2"),("oa3","application/vnd.fujitsu.oasys3"),("fg5","application/vnd.fujitsu.oasysgp"),("bh2","application/vnd.fujitsu.oasysprs"),("ddd","application/vnd.fujixerox.ddd"),("xdw","application/vnd.fujixerox.docuworks"),("xbd","application/vnd.fujixerox.docuworks.binder"),("fzs","application/vnd.fuzzysheet"),("txd","application/vnd.genomatix.tuxedo"),("ggb","application/vnd.geogebra.file"),("ggt","application/vnd.geogebra.tool"),("gex","application/vnd.geometry-explorer"),("gre","application/vnd.geometry-explorer"),("gxt","application/vnd.geonext"),("g2w","application/vnd.geoplan"),("g3w","application/vnd.geospace"),("gmx","application/vnd.gmx"),("kml","application/vnd.google-earth.kml+xml"),("kmz","application/vnd.google-earth.kmz"),("gqf","application/vnd.grafeq"),("gqs","application/vnd.grafeq"),("gac","application/vnd.groove-account"),("ghf","application/vnd.groove-help"),("gim","application/vnd.groove-identity-message"),("grv","application/vnd.groove-injector"),("gtm","application/vnd.groove-tool-message"),("tpl","application/vnd.groove-tool-template"),("vcg","application/vnd.groove-vcard"),("hal","application/vnd.hal+xml"),("zmm","application/vnd.handheld-entertainment+xml"),("hbci","application/vnd.hbci"),("les","application/vnd.hhe.lesson-player"),("hpgl","application/vnd.hp-hpgl"),("hpid","application/vnd.hp-hpid"),("hps","application/vnd.hp-hps"),("jlt","application/vnd.hp-jlyt"),("pcl","application/vnd.hp-pcl"),("pclxl","application/vnd.hp-pclxl"),("sfd-hdstx","application/vnd.hydrostatix.sof-data"),("mpy","application/vnd.ibm.minipay"),("afp","application/vnd.ibm.modcap"),("listafp","application/vnd.ibm.modcap"),("list3820","application/vnd.ibm.modcap"),("irm","application/vnd.ibm.rights-management"),("sc","application/vnd.ibm.secure-container"),("icc","application/vnd.iccprofile"),("icm","application/vnd.iccprofile"),("igl","application/vnd.igloader"),("ivp","application/vnd.immervision-ivp"),("ivu","application/vnd.immervision-ivu"),("igm","application/vnd.insors.igm"),("xpw","application/vnd.intercon.formnet"),("xpx","application/vnd.intercon.formnet"),("i2g","application/vnd.intergeo"),("qbo","application/vnd.intu.qbo"),("qfx","application/vnd.intu.qfx"),("rcprofile","application/vnd.ipunplugged.rcprofile"),("irp","application/vnd.irepository.package+xml"),("xpr","application/vnd.is-xpr"),("fcs","application/vnd.isac.fcs"),("jam","application/vnd.jam"),("rms","application/vnd.jcp.javame.midlet-rms"),("jisp","application/vnd.jisp"),("joda","application/vnd.joost.joda-archive"),("ktz","application/vnd.kahootz"),("ktr","application/vnd.kahootz"),("karbon","application/vnd.kde.karbon"),("chrt","application/vnd.kde.kchart"),("kfo","application/vnd.kde.kformula"),("flw","application/vnd.kde.kivio"),("kon","application/vnd.kde.kontour"),("kpr","application/vnd.kde.kpresenter"),("kpt","application/vnd.kde.kpresenter"),("ksp","application/vnd.kde.kspread"),("kwd","application/vnd.kde.kword"),("kwt","application/vnd.kde.kword"),("htke","application/vnd.kenameaapp"),("kia","application/vnd.kidspiration"),("kne","application/vnd.kinar"),("knp","application/vnd.kinar"),("skp","application/vnd.koan"),("skd","application/vnd.koan"),("skt","application/vnd.koan"),("skm","application/vnd.koan"),("sse","application/vnd.kodak-descriptor"),("lasxml","application/vnd.las.las+xml"),("lbd","application/vnd.llamagraphics.life-balance.desktop"),("lbe","application/vnd.llamagraphics.life-balance.exchange+xml"),("123","application/vnd.lotus-1-2-3"),("apr","application/vnd.lotus-approach"),("pre","application/vnd.lotus-freelance"),("nsf","application/vnd.lotus-notes"),("org","application/vnd.lotus-organizer"),("scm","application/vnd.lotus-screencam"),("lwp","application/vnd.lotus-wordpro"),("portpkg","application/vnd.macports.portpkg"),("mcd","application/vnd.mcd"),("mc1","application/vnd.medcalcdata"),("cdkey","application/vnd.mediastation.cdkey"),("mwf","application/vnd.mfer"),("mfm","application/vnd.mfmp"),("flo","application/vnd.micrografx.flo"),("igx","application/vnd.micrografx.igx"),("mif","application/vnd.mif"),("daf","application/vnd.mobius.daf"),("dis","application/vnd.mobius.dis"),("mbk","application/vnd.mobius.mbk"),("mqy","application/vnd.mobius.mqy"),("msl","application/vnd.mobius.msl"),("plc","application/vnd.mobius.plc"),("txf","application/vnd.mobius.txf"),("mpn","application/vnd.mophun.application"),("mpc","application/vnd.mophun.certificate"),("xul","application/vnd.mozilla.xul+xml"),("cil","application/vnd.ms-artgalry"),("cab","application/vnd.ms-cab-compressed"),("xls","application/vnd.ms-excel"),("xlm","application/vnd.ms-excel"),("xla","application/vnd.ms-excel"),("xlc","application/vnd.ms-excel"),("xlt","application/vnd.ms-excel"),("xlw","application/vnd.ms-excel"),("xlam","application/vnd.ms-excel.addin.macroenabled.12"),("xlsb","application/vnd.ms-excel.sheet.binary.macroenabled.12"),("xlsm","application/vnd.ms-excel.sheet.macroenabled.12"),("xltm","application/vnd.ms-excel.template.macroenabled.12"),("eot","application/vnd.ms-fontobject"),("chm","application/vnd.ms-htmlhelp"),("ims","application/vnd.ms-ims"),("lrm","application/vnd.ms-lrm"),("thmx","application/vnd.ms-officetheme"),("cat","application/vnd.ms-pki.seccat"),("stl","application/vnd.ms-pki.stl"),("ppt","application/vnd.ms-powerpoint"),("pps","application/vnd.ms-powerpoint"),("pot","application/vnd.ms-powerpoint"),("ppam","application/vnd.ms-powerpoint.addin.macroenabled.12"),("pptm","application/vnd.ms-powerpoint.presentation.macroenabled.12"),("sldm","application/vnd.ms-powerpoint.slide.macroenabled.12"),("ppsm","application/vnd.ms-powerpoint.slideshow.macroenabled.12"),("potm","application/vnd.ms-powerpoint.template.macroenabled.12"),("mpp","application/vnd.ms-project"),("mpt","application/vnd.ms-project"),("docm","application/vnd.ms-word.document.macroenabled.12"),("dotm","application/vnd.ms-word.template.macroenabled.12"),("wps","application/vnd.ms-works"),("wks","application/vnd.ms-works"),("wcm","application/vnd.ms-works"),("wdb","application/vnd.ms-works"),("wpl","application/vnd.ms-wpl"),("xps","application/vnd.ms-xpsdocument"),("mseq","application/vnd.mseq"),("mus","application/vnd.musician"),("msty","application/vnd.muvee.style"),("taglet","application/vnd.mynfc"),("nlu","application/vnd.neurolanguage.nlu"),("ntf","application/vnd.nitf"),("nitf","application/vnd.nitf"),("nnd","application/vnd.noblenet-directory"),("nns","application/vnd.noblenet-sealer"),("nnw","application/vnd.noblenet-web"),("ngdat","application/vnd.nokia.n-gage.data"),("n-gage","application/vnd.nokia.n-gage.symbian.install"),("rpst","application/vnd.nokia.radio-preset"),("rpss","application/vnd.nokia.radio-presets"),("edm","application/vnd.novadigm.edm"),("edx","application/vnd.novadigm.edx"),("ext","application/vnd.novadigm.ext"),("odc","application/vnd.oasis.opendocument.chart"),("otc","application/vnd.oasis.opendocument.chart-template"),("odb","application/vnd.oasis.opendocument.database"),("odf","application/vnd.oasis.opendocument.formula"),("odft","application/vnd.oasis.opendocument.formula-template"),("odg","application/vnd.oasis.opendocument.graphics"),("otg","application/vnd.oasis.opendocument.graphics-template"),("odi","application/vnd.oasis.opendocument.image"),("oti","application/vnd.oasis.opendocument.image-template"),("odp","application/vnd.oasis.opendocument.presentation"),("otp","application/vnd.oasis.opendocument.presentation-template"),("ods","application/vnd.oasis.opendocument.spreadsheet"),("ots","application/vnd.oasis.opendocument.spreadsheet-template"),("odt","application/vnd.oasis.opendocument.text"),("odm","application/vnd.oasis.opendocument.text-master"),("ott","application/vnd.oasis.opendocument.text-template"),("oth","application/vnd.oasis.opendocument.text-web"),("xo","application/vnd.olpc-sugar"),("dd2","application/vnd.oma.dd2+xml"),("oxt","application/vnd.openofficeorg.extension"),("pptx","application/vnd.openxmlformats-officedocument.presentationml.presentation"),("sldx","application/vnd.openxmlformats-officedocument.presentationml.slide"),("ppsx","application/vnd.openxmlformats-officedocument.presentationml.slideshow"),("potx","application/vnd.openxmlformats-officedocument.presentationml.template"),("xlsx","application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),("xltx","application/vnd.openxmlformats-officedocument.spreadsheetml.template"),("docx","application/vnd.openxmlformats-officedocument.wordprocessingml.document"),("dotx","application/vnd.openxmlformats-officedocument.wordprocessingml.template"),("mgp","application/vnd.osgeo.mapguide.package"),("dp","application/vnd.osgi.dp"),("esa","application/vnd.osgi.subsystem"),("pdb","application/vnd.palm"),("pqa","application/vnd.palm"),("oprc","application/vnd.palm"),("paw","application/vnd.pawaafile"),("str","application/vnd.pg.format"),("ei6","application/vnd.pg.osasli"),("efif","application/vnd.picsel"),("wg","application/vnd.pmi.widget"),("plf","application/vnd.pocketlearn"),("pbd","application/vnd.powerbuilder6"),("box","application/vnd.previewsystems.box"),("mgz","application/vnd.proteus.magazine"),("qps","application/vnd.publishare-delta-tree"),("ptid","application/vnd.pvi.ptid1"),("qxd","application/vnd.quark.quarkxpress"),("qxt","application/vnd.quark.quarkxpress"),("qwd","application/vnd.quark.quarkxpress"),("qwt","application/vnd.quark.quarkxpress"),("qxl","application/vnd.quark.quarkxpress"),("qxb","application/vnd.quark.quarkxpress"),("bed","application/vnd.realvnc.bed"),("mxl","application/vnd.recordare.musicxml"),("musicxml","application/vnd.recordare.musicxml+xml"),("cryptonote","application/vnd.rig.cryptonote"),("cod","application/vnd.rim.cod"),("rm","application/vnd.rn-realmedia"),("rmvb","application/vnd.rn-realmedia-vbr"),("link66","application/vnd.route66.link66+xml"),("st","application/vnd.sailingtracker.track"),("see","application/vnd.seemail"),("sema","application/vnd.sema"),("semd","application/vnd.semd"),("semf","application/vnd.semf"),("ifm","application/vnd.shana.informed.formdata"),("itp","application/vnd.shana.informed.formtemplate"),("iif","application/vnd.shana.informed.interchange"),("ipk","application/vnd.shana.informed.package"),("twd","application/vnd.simtech-mindmapper"),("twds","application/vnd.simtech-mindmapper"),("mmf","application/vnd.smaf"),("teacher","application/vnd.smart.teacher"),("sdkm","application/vnd.solent.sdkm+xml"),("sdkd","application/vnd.solent.sdkm+xml"),("dxp","application/vnd.spotfire.dxp"),("sfs","application/vnd.spotfire.sfs"),("sdc","application/vnd.stardivision.calc"),("sda","application/vnd.stardivision.draw"),("sdd","application/vnd.stardivision.impress"),("smf","application/vnd.stardivision.math"),("sdw","application/vnd.stardivision.writer"),("vor","application/vnd.stardivision.writer"),("sgl","application/vnd.stardivision.writer-global"),("smzip","application/vnd.stepmania.package"),("sm","application/vnd.stepmania.stepchart"),("sxc","application/vnd.sun.xml.calc"),("stc","application/vnd.sun.xml.calc.template"),("sxd","application/vnd.sun.xml.draw"),("std","application/vnd.sun.xml.draw.template"),("sxi","application/vnd.sun.xml.impress"),("sti","application/vnd.sun.xml.impress.template"),("sxm","application/vnd.sun.xml.math"),("sxw","application/vnd.sun.xml.writer"),("sxg","application/vnd.sun.xml.writer.global"),("stw","application/vnd.sun.xml.writer.template"),("sus","application/vnd.sus-calendar"),("susp","application/vnd.sus-calendar"),("svd","application/vnd.svd"),("sis","application/vnd.symbian.install"),("sisx","application/vnd.symbian.install"),("xsm","application/vnd.syncml+xml"),("bdm","application/vnd.syncml.dm+wbxml"),("xdm","application/vnd.syncml.dm+xml"),("tao","application/vnd.tao.intent-module-archive"),("pcap","application/vnd.tcpdump.pcap"),("cap","application/vnd.tcpdump.pcap"),("dmp","application/vnd.tcpdump.pcap"),("tmo","application/vnd.tmobile-livetv"),("tpt","application/vnd.trid.tpt"),("mxs","application/vnd.triscape.mxs"),("tra","application/vnd.trueapp"),("ufd","application/vnd.ufdl"),("ufdl","application/vnd.ufdl"),("utz","application/vnd.uiq.theme"),("umj","application/vnd.umajin"),("unityweb","application/vnd.unity"),("uoml","application/vnd.uoml+xml"),("vcx","application/vnd.vcx"),("vsd","application/vnd.visio"),("vst","application/vnd.visio"),("vss","application/vnd.visio"),("vsw","application/vnd.visio"),("vis","application/vnd.visionary"),("vsf","application/vnd.vsf"),("wbxml","application/vnd.wap.wbxml"),("wmlc","application/vnd.wap.wmlc"),("wmlsc","application/vnd.wap.wmlscriptc"),("wtb","application/vnd.webturbo"),("nbp","application/vnd.wolfram.player"),("wpd","application/vnd.wordperfect"),("wqd","application/vnd.wqd"),("stf","application/vnd.wt.stf"),("xar","application/vnd.xara"),("xfdl","application/vnd.xfdl"),("hvd","application/vnd.yamaha.hv-dic"),("hvs","application/vnd.yamaha.hv-script"),("hvp","application/vnd.yamaha.hv-voice"),("osf","application/vnd.yamaha.openscoreformat"),("osfpvg","application/vnd.yamaha.openscoreformat.osfpvg+xml"),("saf","application/vnd.yamaha.smaf-audio"),("spf","application/vnd.yamaha.smaf-phrase"),("cmp","application/vnd.yellowriver-custom-menu"),("zir","application/vnd.zul"),("zirz","application/vnd.zul"),("zaz","application/vnd.zzazz.deck+xml"),("vxml","application/voicexml+xml"),("wgt","application/widget"),("hlp","application/winhlp"),("wsdl","application/wsdl+xml"),("wspolicy","application/wspolicy+xml"),("7z","application/x-7z-compressed"),("abw","application/x-abiword"),("ace","application/x-ace-compressed"),("dmg","application/x-apple-diskimage"),("aab","application/x-authorware-bin"),("x32","application/x-authorware-bin"),("u32","application/x-authorware-bin"),("vox","application/x-authorware-bin"),("aam","application/x-authorware-map"),("aas","application/x-authorware-seg"),("bcpio","application/x-bcpio"),("torrent","application/x-bittorrent"),("blb","application/x-blorb"),("blorb","application/x-blorb"),("bz","application/x-bzip"),("bz2","application/x-bzip2"),("boz","application/x-bzip2"),("cbr","application/x-cbr"),("cba","application/x-cbr"),("cbt","application/x-cbr"),("cbz","application/x-cbr"),("cb7","application/x-cbr"),("vcd","application/x-cdlink"),("cfs","application/x-cfs-compressed"),("chat","application/x-chat"),("pgn","application/x-chess-pgn"),("nsc","application/x-conference"),("cpio","application/x-cpio"),("csh","application/x-csh"),("deb","application/x-debian-package"),("udeb","application/x-debian-package"),("dgc","application/x-dgc-compressed"),("dir","application/x-director"),("dcr","application/x-director"),("dxr","application/x-director"),("cst","application/x-director"),("cct","application/x-director"),("cxt","application/x-director"),("w3d","application/x-director"),("fgd","application/x-director"),("swa","application/x-director"),("wad","application/x-doom"),("ncx","application/x-dtbncx+xml"),("dtb","application/x-dtbook+xml"),("res","application/x-dtbresource+xml"),("dvi","application/x-dvi"),("evy","application/x-envoy"),("eva","application/x-eva"),("bdf","application/x-font-bdf"),("gsf","application/x-font-ghostscript"),("psf","application/x-font-linux-psf"),("pcf","application/x-font-pcf"),("snf","application/x-font-snf"),("pfa","application/x-font-type1"),("pfb","application/x-font-type1"),("pfm","application/x-font-type1"),("afm","application/x-font-type1"),("arc","application/x-freearc"),("spl","application/x-futuresplash"),("gca","application/x-gca-compressed"),("ulx","application/x-glulx"),("gnumeric","application/x-gnumeric"),("gramps","application/x-gramps-xml"),("gtar","application/x-gtar"),("hdf","application/x-hdf"),("install","application/x-install-instructions"),("iso","application/x-iso9660-image"),("jnlp","application/x-java-jnlp-file"),("latex","application/x-latex"),("lzh","application/x-lzh-compressed"),("lha","application/x-lzh-compressed"),("mie","application/x-mie"),("prc","application/x-mobipocket-ebook"),("mobi","application/x-mobipocket-ebook"),("application","application/x-ms-application"),("lnk","application/x-ms-shortcut"),("wmd","application/x-ms-wmd"),("wmz","application/x-ms-wmz"),("xbap","application/x-ms-xbap"),("mdb","application/x-msaccess"),("obd","application/x-msbinder"),("crd","application/x-mscardfile"),("clp","application/x-msclip"),("exe","application/x-msdownload"),("dll","application/x-msdownload"),("com","application/x-msdownload"),("bat","application/x-msdownload"),("msi","application/x-msdownload"),("mvb","application/x-msmediaview"),("m13","application/x-msmediaview"),("m14","application/x-msmediaview"),("wmf","application/x-msmetafile"),("wmz","application/x-msmetafile"),("emf","application/x-msmetafile"),("emz","application/x-msmetafile"),("mny","application/x-msmoney"),("pub","application/x-mspublisher"),("scd","application/x-msschedule"),("trm","application/x-msterminal"),("wri","application/x-mswrite"),("nc","application/x-netcdf"),("cdf","application/x-netcdf"),("nzb","application/x-nzb"),("p12","application/x-pkcs12"),("pfx","application/x-pkcs12"),("p7b","application/x-pkcs7-certificates"),("spc","application/x-pkcs7-certificates"),("p7r","application/x-pkcs7-certreqresp"),("rar","application/x-rar-compressed"),("ris","application/x-research-info-systems"),("sh","application/x-sh"),("shar","application/x-shar"),("swf","application/x-shockwave-flash"),("xap","application/x-silverlight-app"),("sql","application/x-sql"),("sit","application/x-stuffit"),("sitx","application/x-stuffitx"),("srt","application/x-subrip"),("sv4cpio","application/x-sv4cpio"),("sv4crc","application/x-sv4crc"),("t3","application/x-t3vm-image"),("gam","application/x-tads"),("tar","application/x-tar"),("tcl","application/x-tcl"),("tex","application/x-tex"),("tfm","application/x-tex-tfm"),("texinfo","application/x-texinfo"),("texi","application/x-texinfo"),("obj","application/x-tgif"),("ustar","application/x-ustar"),("src","application/x-wais-source"),("der","application/x-x509-ca-cert"),("crt","application/x-x509-ca-cert"),("fig","application/x-xfig"),("xlf","application/x-xliff+xml"),("xpi","application/x-xpinstall"),("xz","application/x-xz"),("z1","application/x-zmachine"),("z2","application/x-zmachine"),("z3","application/x-zmachine"),("z4","application/x-zmachine"),("z5","application/x-zmachine"),("z6","application/x-zmachine"),("z7","application/x-zmachine"),("z8","application/x-zmachine"),("xaml","application/xaml+xml"),("xdf","application/xcap-diff+xml"),("xenc","application/xenc+xml"),("xhtml","application/xhtml+xml"),("xht","application/xhtml+xml"),("xml","application/xml"),("xsl","application/xml"),("dtd","application/xml-dtd"),("xop","application/xop+xml"),("xpl","application/xproc+xml"),("xslt","application/xslt+xml"),("xspf","application/xspf+xml"),("mxml","application/xv+xml"),("xhvml","application/xv+xml"),("xvml","application/xv+xml"),("xvm","application/xv+xml"),("yang","application/yang"),("yin","application/yin+xml"),("zip","application/zip"),("adp","audio/adpcm"),("au","audio/basic"),("snd","audio/basic"),("mid","audio/midi"),("midi","audio/midi"),("kar","audio/midi"),("rmi","audio/midi"),("m4a","audio/mp4"),("mp4a","audio/mp4"),("mpga","audio/mpeg"),("mp2","audio/mpeg"),("mp2a","audio/mpeg"),("mp3","audio/mpeg"),("m2a","audio/mpeg"),("m3a","audio/mpeg"),("oga","audio/ogg"),("ogg","audio/ogg"),("spx","audio/ogg"),("s3m","audio/s3m"),("sil","audio/silk"),("uva","audio/vnd.dece.audio"),("uvva","audio/vnd.dece.audio"),("eol","audio/vnd.digital-winds"),("dra","audio/vnd.dra"),("dts","audio/vnd.dts"),("dtshd","audio/vnd.dts.hd"),("lvp","audio/vnd.lucent.voice"),("pya","audio/vnd.ms-playready.media.pya"),("ecelp4800","audio/vnd.nuera.ecelp4800"),("ecelp7470","audio/vnd.nuera.ecelp7470"),("ecelp9600","audio/vnd.nuera.ecelp9600"),("rip","audio/vnd.rip"),("weba","audio/webm"),("aac","audio/x-aac"),("aif","audio/x-aiff"),("aiff","audio/x-aiff"),("aifc","audio/x-aiff"),("caf","audio/x-caf"),("flac","audio/x-flac"),("mka","audio/x-matroska"),("m3u","audio/x-mpegurl"),("wax","audio/x-ms-wax"),("wma","audio/x-ms-wma"),("ram","audio/x-pn-realaudio"),("ra","audio/x-pn-realaudio"),("rmp","audio/x-pn-realaudio-plugin"),("wav","audio/x-wav"),("xm","audio/xm"),("cdx","chemical/x-cdx"),("cif","chemical/x-cif"),("cmdf","chemical/x-cmdf"),("cml","chemical/x-cml"),("csml","chemical/x-csml"),("xyz","chemical/x-xyz"),("ttc","font/collection"),("otf","font/otf"),("ttf","font/ttf"),("woff","font/woff"),("woff2","font/woff2"),("bmp","image/bmp"),("cgm","image/cgm"),("g3","image/g3fax"),("gif","image/gif"),("ief","image/ief"),("jpeg","image/jpeg"),("jpg","image/jpeg"),("jpe","image/jpeg"),("ktx","image/ktx"),("png","image/png"),("btif","image/prs.btif"),("sgi","image/sgi"),("svg","image/svg+xml"),("svgz","image/svg+xml"),("tiff","image/tiff"),("tif","image/tiff"),("psd","image/vnd.adobe.photoshop"),("uvi","image/vnd.dece.graphic"),("uvvi","image/vnd.dece.graphic"),("uvg","image/vnd.dece.graphic"),("uvvg","image/vnd.dece.graphic"),("djvu","image/vnd.djvu"),("djv","image/vnd.djvu"),("sub","image/vnd.dvb.subtitle"),("dwg","image/vnd.dwg"),("dxf","image/vnd.dxf"),("fbs","image/vnd.fastbidsheet"),("fpx","image/vnd.fpx"),("fst","image/vnd.fst"),("mmr","image/vnd.fujixerox.edmics-mmr"),("rlc","image/vnd.fujixerox.edmics-rlc"),("mdi","image/vnd.ms-modi"),("wdp","image/vnd.ms-photo"),("npx","image/vnd.net-fpx"),("wbmp","image/vnd.wap.wbmp"),("xif","image/vnd.xiff"),("webp","image/webp"),("3ds","image/x-3ds"),("ras","image/x-cmu-raster"),("cmx","image/x-cmx"),("fh","image/x-freehand"),("fhc","image/x-freehand"),("fh4","image/x-freehand"),("fh5","image/x-freehand"),("fh7","image/x-freehand"),("ico","image/x-icon"),("sid","image/x-mrsid-image"),("pcx","image/x-pcx"),("pic","image/x-pict"),("pct","image/x-pict"),("pnm","image/x-portable-anymap"),("pbm","image/x-portable-bitmap"),("pgm","image/x-portable-graymap"),("ppm","image/x-portable-pixmap"),("rgb","image/x-rgb"),("tga","image/x-tga"),("xbm","image/x-xbitmap"),("xpm","image/x-xpixmap"),("xwd","image/x-xwindowdump"),("eml","message/rfc822"),("mime","message/rfc822"),("igs","model/iges"),("iges","model/iges"),("msh","model/mesh"),("mesh","model/mesh"),("silo","model/mesh"),("dae","model/vnd.collada+xml"),("dwf","model/vnd.dwf"),("gdl","model/vnd.gdl"),("gtw","model/vnd.gtw"),("mts","model/vnd.mts"),("vtu","model/vnd.vtu"),("wrl","model/vrml"),("vrml","model/vrml"),("x3db","model/x3d+binary"),("x3dbz","model/x3d+binary"),("x3dv","model/x3d+vrml"),("x3dvz","model/x3d+vrml"),("x3d","model/x3d+xml"),("x3dz","model/x3d+xml"),("appcache","text/cache-manifest"),("ics","text/calendar"),("ifb","text/calendar"),("css","text/css"),("csv","text/csv"),("html","text/html"),("htm","text/html"),("n3","text/n3"),("txt","text/plain"),("text","text/plain"),("conf","text/plain"),("def","text/plain"),("list","text/plain"),("log","text/plain"),("in","text/plain"),("dsc","text/prs.lines.tag"),("rtx","text/richtext"),("sgml","text/sgml"),("sgm","text/sgml"),("tsv","text/tab-separated-values"),("t","text/troff"),("tr","text/troff"),("roff","text/troff"),("man","text/troff"),("me","text/troff"),("ms","text/troff"),("ttl","text/turtle"),("uri","text/uri-list"),("uris","text/uri-list"),("urls","text/uri-list"),("vcard","text/vcard"),("curl","text/vnd.curl"),("dcurl","text/vnd.curl.dcurl"),("mcurl","text/vnd.curl.mcurl"),("scurl","text/vnd.curl.scurl"),("sub","text/vnd.dvb.subtitle"),("fly","text/vnd.fly"),("flx","text/vnd.fmi.flexstor"),("gv","text/vnd.graphviz"),("3dml","text/vnd.in3d.3dml"),("spot","text/vnd.in3d.spot"),("jad","text/vnd.sun.j2me.app-descriptor"),("wml","text/vnd.wap.wml"),("wmls","text/vnd.wap.wmlscript"),("s","text/x-asm"),("asm","text/x-asm"),("c","text/x-c"),("cc","text/x-c"),("cxx","text/x-c"),("cpp","text/x-c"),("h","text/x-c"),("hh","text/x-c"),("dic","text/x-c"),("f","text/x-fortran"),("for","text/x-fortran"),("f77","text/x-fortran"),("f90","text/x-fortran"),("java","text/x-java-source"),("nfo","text/x-nfo"),("opml","text/x-opml"),("p","text/x-pascal"),("pas","text/x-pascal"),("etx","text/x-setext"),("sfv","text/x-sfv"),("uu","text/x-uuencode"),("vcs","text/x-vcalendar"),("vcf","text/x-vcard"),("3gp","video/3gpp"),("3g2","video/3gpp2"),("h261","video/h261"),("h263","video/h263"),("h264","video/h264"),("jpgv","video/jpeg"),("jpm","video/jpm"),("jpgm","video/jpm"),("mj2","video/mj2"),("mjp2","video/mj2"),("mp4","video/mp4"),("mp4v","video/mp4"),("mpg4","video/mp4"),("mpeg","video/mpeg"),("mpg","video/mpeg"),("mpe","video/mpeg"),("m1v","video/mpeg"),("m2v","video/mpeg"),("ogv","video/ogg"),("qt","video/quicktime"),("mov","video/quicktime"),("uvh","video/vnd.dece.hd"),("uvvh","video/vnd.dece.hd"),("uvm","video/vnd.dece.mobile"),("uvvm","video/vnd.dece.mobile"),("uvp","video/vnd.dece.pd"),("uvvp","video/vnd.dece.pd"),("uvs","video/vnd.dece.sd"),("uvvs","video/vnd.dece.sd"),("uvv","video/vnd.dece.video"),("uvvv","video/vnd.dece.video"),("dvb","video/vnd.dvb.file"),("fvt","video/vnd.fvt"),("mxu","video/vnd.mpegurl"),("m4u","video/vnd.mpegurl"),("pyv","video/vnd.ms-playready.media.pyv"),("uvu","video/vnd.uvvu.mp4"),("uvvu","video/vnd.uvvu.mp4"),("viv","video/vnd.vivo"),("webm","video/webm"),("f4v","video/x-f4v"),("fli","video/x-fli"),("flv","video/x-flv"),("m4v","video/x-m4v"),("mkv","video/x-matroska"),("mk3d","video/x-matroska"),("mks","video/x-matroska"),("mng","video/x-mng"),("asf","video/x-ms-asf"),("asx","video/x-ms-asf"),("vob","video/x-ms-vob"),("wm","video/x-ms-wm"),("wmv","video/x-ms-wmv"),("wmx","video/x-ms-wmx"),("wvx","video/x-ms-wvx"),("avi","video/x-msvideo"),("movie","video/x-sgi-movie"),("smv","video/x-smv"),("ice","x-conference/x-cooltalk")]
− src/Happstack/Server/HTTP/Client.hs
@@ -1,50 +0,0 @@-module Happstack.Server.HTTP.Client where---import Happstack.Server.HTTP.Handler-import Happstack.Server.HTTP.Types-import Data.Maybe-import qualified Data.ByteString.Lazy.Char8 as L --import System.IO-import qualified Data.ByteString.Char8 as B -import Network--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 = unproxify rq {rqPaths = host: rqPaths rq}- where- host::String- host = maybe defaultHost (f .B.unpack) $- getHeader "host" rq- f = maybe defaultHost id . flip lookup list-
− 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) * 1) -- Every second- writeIORef ref =<< mkTime- updater ref--getApproximateTime :: IO B.ByteString-getApproximateTime = readIORef clock
− src/Happstack/Server/HTTP/FileServe.hs
@@ -1,215 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}-module Happstack.Server.HTTP.FileServe- (- MimeMap,fileServe, mimeTypes,isDot, blockDotFiles,doIndex,errorwrapper- ) where--import Control.Exception.Extensible--import Control.Monad.Reader-import Control.Monad.Trans-import Data.List-import Data.Maybe-import Data.Int-import Happstack.Server.SimpleHTTP hiding (path)-import System.Directory-import System.IO-import System.Locale(defaultTimeLocale)-import System.Log.Logger-import System.Time -- (formatCalendarTime, toUTCTime,TOD(..))-import qualified Data.ByteString.Char8 as P-import qualified Data.ByteString.Lazy.Char8 as L-import qualified Data.Map as Map-import qualified Happstack.Server.SimpleHTTP as SH--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 -- fileServe [loglocation] [] "./"- else return Nothing--type MimeMap = Map.Map String String--doIndex :: (ServerMonad m, FilterMonad Response m, MonadIO m) =>- [String] -> MimeMap -> String -> m Response-doIndex [] _mime _fp = do forbidden $ toResponse "Directory index forbidden"-doIndex (index:rest) mime fp =- do- let path = fp++'/':index- --print path- fe <- liftIO $ doesFileExist path- if fe then retFile path else doIndex rest mime fp- where retFile = returnFile mime-defaultIxFiles :: [String]-defaultIxFiles= ["index.html","index.xml","index.gif"]---fileServe :: (ServerMonad m, FilterMonad Response m, MonadIO m) => [FilePath] -> FilePath -> m Response-fileServe ixFiles localpath = - fileServe' localpath (doIndex (ixFiles++defaultIxFiles)) mimeTypes---- | Serve files with a mime type map under a directory.--- Uses the function to transform URIs to FilePaths.-fileServe' :: (ServerMonad m, FilterMonad Response m, MonadIO m) =>- String- -> (Map.Map String String -> String -> m Response)- -> Map.Map String String- -> m Response-fileServe' localpath fdir mime = do- rq <- askRq- let fp2 = takeWhile (/=',') fp- fp = filepath- safepath = filter (\x->not (null x) && head x /= '.') (rqPaths rq)- filepath = intercalate "/" (localpath:safepath)- fp' = if null safepath then "" else last safepath- if "TESTH" `isPrefixOf` fp'- then renderResponse mime $ fakeFile $ (read $ drop 5 $ fp' :: Integer)- else do- fe <- liftIO $ doesFileExist fp- fe2 <- liftIO $ doesFileExist fp2- de <- liftIO $ doesDirectoryExist fp- -- error $ "show ilepath: " ++show (fp,de)- let status | de = "DIR"- | fe = "file"- | fe2 = "group"- | True = "NOT FOUND"- liftIO $ logM "Happstack.Server.HTTP.FileServe" DEBUG ("fileServe: "++show fp++" \t"++status)- if de then fdir mime fp else do- getFile mime fp >>= flip either (renderResponse mime) - (const $ returnGroup localpath mime safepath)--returnFile :: (ServerMonad m, FilterMonad Response m, MonadIO m) =>- Map.Map String String -> String -> m Response-returnFile mime fp = - getFile mime fp >>= either fileNotFound (renderResponse mime)---- if fp has , separated then return concatenation with content-type of last--- and last modified of latest-tr :: (Eq a) => a -> a -> [a] -> [a]-tr a b = map (\x->if x==a then b else x)-ltrim :: String -> String-ltrim = dropWhile (flip elem " \t\r") --returnGroup :: (ServerMonad m, FilterMonad Response m, MonadIO m) =>- String -> Map.Map String String -> [String] -> m Response-returnGroup localPath mime fp = do- let fps0 = map ((:[]). ltrim) $ lines $ tr ',' '\n' $ last fp- fps = map (intercalate "/" . ((localPath:init fp) ++)) fps0-- -- if (head $ head fps0)=="TEST" then renderResponse mime rq fakeFile else do-- mbFiles <- mapM (getFile mime) $ fps- let notFounds = [x | Left x <- mbFiles]- files = [x | Right x <- mbFiles]- if not $ null notFounds - then fileNotFound $ drop (length localPath) $ head notFounds else do- let totSize = sum $ map (snd . fst) files- maxTime = maximum $ map (fst . fst) files :: ClockTime-- renderResponse mime ((maxTime,totSize),(fst $ snd $ head files,- L.concat $ map (snd . snd) files))----fileNotFound :: (Monad m, FilterMonad Response m) => String -> m Response-fileNotFound fp = do setResponseCode 404 - return $ toResponse $ "File not found "++ fp---fakeLen = 71* 1024-fakeFile :: (Integral a) =>- a -> ((ClockTime, Int64), (String, L.ByteString))-fakeFile fakeLen = ((TOD 0 0,L.length body),("text/javascript",body))- where- body = L.pack $ (("//"++(show len)++" ") ++ ) $ (replicate len '0') ++ "\n"- len = fromIntegral fakeLen--getFile :: (MonadIO m) =>- Map.Map String String- -> String- -> m (Either String ((ClockTime, Integer), (String, L.ByteString)))-getFile mime fp = do- let ct = Map.findWithDefault "text/plain" (getExt fp) mime- fe <- liftIO $ doesFileExist fp- if not fe then return $ Left fp else do- - time <- liftIO $ getModificationTime fp- h <- liftIO $ openBinaryFile fp ReadMode- size <- liftIO $ hFileSize h- lbs <- liftIO $ L.hGetContents h- return $ Right ((time,size),(ct,lbs))--renderResponse :: (Monad m,- ServerMonad m,- FilterMonad Response m,- Show t1) =>- t- -> ((ClockTime, t1), (String, L.ByteString))- -> m Response-renderResponse _ ((modtime,size),(ct,body)) = do- rq <- askRq- let notmodified = getHeader "if-modified-since" rq == Just (P.pack $ repr)- repr = formatCalendarTime defaultTimeLocale - "%a, %d %b %Y %X GMT" (toUTCTime modtime)- -- "Mon, 07 Jan 2008 19:51:02 GMT"- -- when (isJust $ getHeader "if-modified-since" rq) $ error $ show $ getHeader "if-modified-since" rq- if notmodified then do setResponseCode 304 ; return $ toResponse "" else do- -- modifyResponse (setHeader "HUH" $ show $ (fmap P.unpack mod == Just repr,mod,Just repr))- setHeaderM "Last-modified" repr- -- if %Z or UTC are in place of GMT below, wget complains that the last-modified header is invalid- setHeaderM "Content-Length" (show size)- setHeaderM "Content-Type" ct- return $ resultBS 200 body-- ---getExt :: String -> String-getExt = reverse . takeWhile (/='.') . reverse---- | Ready collection of common mime types.-mimeTypes :: MimeMap-mimeTypes = Map.fromList- [("xml","application/xml")- ,("xsl","application/xml")- ,("js","text/javascript")- ,("html","text/html")- ,("css","text/css")- ,("gif","image/gif")- ,("jpg","image/jpeg")- ,("png","image/png")- ,("txt","text/plain")- ,("doc","application/msword")- ,("exe","application/octet-stream")- ,("pdf","application/pdf")- ,("zip","application/zip")- ,("gz","application/x-gzip")- ,("ps","application/postscript")- ,("rtf","application/rtf")- ,("wav","application/x-wav")- ,("hs","text/plain")]----- | Prevents files of the form '.foo' or 'bar/.foo' from being served-blockDotFiles :: (Request -> IO Response) -> Request -> IO Response-blockDotFiles fn rq- | isDot (intercalate "/" (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
− src/Happstack/Server/HTTP/Handler.hs
@@ -1,322 +0,0 @@-{-# LANGUAGE ScopedTypeVariables, PatternSignatures #-}--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.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.LazyLiner-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 System.Log.Logger (Priority(..), logM)--log' = logM "Happstack.Server"--request :: Conf -> Handle -> Host -> (Request -> IO Response) -> IO ()-request conf h host handler = rloop conf h host handler =<< L.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 = "-"- requestLine = unwords [show $ rqMethod req, rqUri req, show $ rqVersion req]- responseCode = rsCode res- size = toInteger $ L.length $ rsBody res- referer = B.unpack $ fromMaybe (B.pack "") $ getHeader "Referer" req- userAgent = B.unpack $ fromMaybe (B.pack "") $ getHeader "User-Agent" req- log' NOTICE $ formatRequestCombined host' user time requestLine responseCode size referer userAgent- - putAugmentedResult h req res- when (continueHTTP req res) $ rloop conf h host handler rest--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) ]--putAugmentedResult :: Handle -> Request -> Response -> IO ()-putAugmentedResult h req res = do- let ph (HeaderPair k vs) = map (\v -> P.concat [k, fsepC, v, crlfC]) vs- raw <- getApproximateTime- let cl = L.length $ rsBody res- let put = P.hPut h- -- TODO: Hoist static headers to the toplevel.- 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 [] )- allHeaders = rsHeaders res `M.union` stdHeaders -- 'union' prefers 'headers res' when duplicate keys are encountered.-- mapM_ put $ concat- [ (pversion $ rqVersion req) -- Print HTTP version- , [responseMessage $ rsCode res] -- Print responseCode- , concatMap ph (M.elems allHeaders) -- Print all headers- , [crlfC]- ]- when (rqMethod req /= HEAD) $ L.hPut h $ rsBody res- hFlush h---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,58 +0,0 @@-{-# LANGUAGE CPP, ScopedTypeVariables, PatternSignatures #-}-module Happstack.Server.HTTP.Listen(listen) 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(PortID(..), listenOn, sClose)-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"--listen :: Conf -> (Request -> IO Response) -> IO ()-listen conf hand = do-{--#ifndef mingw32_HOST_OS--}- installHandler openEndedPipe Ignore Nothing-{--#endif--}- let port' = port conf- log' NOTICE ("Listening on port " ++ show port')- s <- listenOn $ PortNumber $ toEnum 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,216 +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 : xbnon-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- ) 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- ('\n','\r') -> Just (j,2)- ('\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 == '\n' && c1 == '\r' = BS.drop 2 s- | 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,16 +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"; d = ''SockAddr in- do TyConI (DataD _ _ _ cs _) <- reify d- if isJust (find (\(NormalC n _) -> show n == c) cs)- then [| True |]- else [| False |] )-
− src/Happstack/Server/HTTP/Types.hs
@@ -1,252 +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)- } 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/HTTPClient/HTTP.hs
@@ -1,1019 +0,0 @@-{-# LANGUAGE ScopedTypeVariables, PatternSignatures #-}--------------------------------------------------------------------------------- |--- Module : Happstack.Server.HTTPClient.HTTP--- Copyright : (c) Warrick Gray 2002, Bjorn Bringert 2003-2005--- License : BSD--- --- Maintainer : bjorn@bringert.net--- Stability : experimental--- Portability : non-portable (not tested)------ An easy HTTP interface enjoy.------ * Changes by Simon Foster:--- - Split module up into to sepearate Network.[Stream,TCP,HTTP] modules--- - Created functions receiveHTTP and responseHTTP to allow server side interactions--- (although 100-continue is unsupported and I haven't checked for standard compliancy).--- - Pulled the transfer functions from sendHTTP to global scope to allow access by--- above functions.------ * Changes by Graham Klyne:--- - export httpVersion--- - use new URI module (similar to old, but uses revised URI datatype)------ * Changes by Bjorn Bringert:------ - handle URIs with a port number--- - added debugging toggle--- - disabled 100-continue transfers to get HTTP\/1.0 compatibility--- - change 'ioError' to 'throw'--- - Added simpleHTTP_, which takes a stream argument.------ * Changes from 0.1--- - change 'openHTTP' to 'openTCP', removed 'closeTCP' - use 'close' from 'Stream' class.--- - added use of inet_addr to openHTTP, allowing use of IP "dot" notation addresses.--- - reworking of the use of Stream, including alterations to make 'sendHTTP' generic--- and the addition of a debugging stream.--- - simplified error handling.--- --- * TODO--- - request pipelining--- - https upgrade (includes full TLS, i.e. SSL, implementation)--- - use of Stream classes will pay off--- - consider C implementation of encryption\/decryption--- - comm timeouts--- - MIME & entity stuff (happening in separate module)--- - support \"*\" uri-request-string for OPTIONS request method--- --- --- * Header notes:------ [@Host@]--- Required by HTTP\/1.1, if not supplied as part--- of a request a default Host value is extracted--- from the request-uri.--- --- [@Connection@] --- If this header is present in any request or--- response, and it's value is "close", then--- the current request\/response is the last --- to be allowed on that connection.--- --- [@Expect@]--- Should a request contain a body, an Expect--- header will be added to the request. The added--- header has the value \"100-continue\". After--- a 417 \"Expectation Failed\" response the request--- is attempted again without this added Expect--- header.--- --- [@TransferEncoding,ContentLength,...@]--- if request is inconsistent with any of these--- header values then you may not receive any response--- or will generate an error response (probably 4xx).--------- * Response code notes--- Some response codes induce special behaviour:------ [@1xx@] \"100 Continue\" will cause any unsent request body to be sent.--- \"101 Upgrade\" will be returned.--- Other 1xx responses are ignored.--- --- [@417@] The reason for this code is \"Expectation failed\", indicating--- that the server did not like the Expect \"100-continue\" header--- added to a request. Receipt of 417 will induce another--- request attempt (without Expect header), unless no Expect header--- had been added (in which case 417 response is returned).----------------------------------------------------------------------------------module Happstack.Server.HTTPClient.HTTP (- module Happstack.Server.HTTPClient.Stream,- module Happstack.Server.HTTPClient.TCP,-- -- ** Constants- httpVersion,- - -- ** HTTP - Request(..),- Response(..),- RequestMethod(..),- simpleHTTP, simpleHTTP_,- sendHTTP,- sendHTTPPipelined,- receiveHTTP,- respondHTTP,-- -- ** Header Functions- HasHeaders,- Header(..),- HeaderName(..),- insertHeader,- insertHeaderIfMissing,- insertHeaders,- retrieveHeaders,- replaceHeader,- findHeader,-- -- ** URL Encoding- urlEncode,- urlDecode,- urlEncodeVars,--) where---------------------------------------------------------------------------------------- Imports ----------------------------------------------------------------------------------------------------------import Control.Exception.Extensible as Exception---- Networking-import Network.URI-import Happstack.Server.HTTPClient.Stream-import Happstack.Server.HTTPClient.TCP----- Util-import Data.Bits ((.&.))-import Data.Char-import Data.List (partition,elemIndex,intersperse)-import Data.Maybe-import Control.Monad (when,forM)-import Numeric (readHex)-import Text.ParserCombinators.ReadP-import System.IO------ Turn on to enable HTTP traffic logging-debug :: Bool-debug = True -- False---- File that HTTP traffic logs go to-httpLogFile :: String-httpLogFile = "http-debug.log"-------------------------------------------------------------------------------------- Misc --------------------------------------------------------------------------------------------------------------- remove leading and trailing whitespace.-trim :: String -> String-trim = let dropspace = dropWhile isSpace in- reverse . dropspace . reverse . dropspace----- Split a list into two parts, the delimiter occurs--- at the head of the second list. Nothing is returned--- when no occurance of the delimiter is found.-split :: Eq a => a -> [a] -> Maybe ([a],[a])-split delim list = case delim `elemIndex` list of- Nothing -> Nothing- Just x -> Just $ splitAt x list- ---crlf :: String-crlf = "\r\n"-sp :: String-sp = " "-------------------------------------------------------------------------------------- Header Data --------------------------------------------------------------------------------------------------------- | The Header data type pairs header names & values.-data Header = Header HeaderName String---instance Show Header where- show (Header key value) = show key ++ ": " ++ value ++ crlf----- | HTTP Header Name type:--- Why include this at all? I have some reasons--- 1) prevent spelling errors of header names,--- 2) remind everyone of what headers are available,--- 3) might speed up searches for specific headers.------ Arguments against:--- 1) makes customising header names laborious--- 2) increases code volume.----data HeaderName = - -- Generic Headers --- HdrCacheControl- | HdrConnection- | HdrDate- | HdrPragma- | HdrTransferEncoding - | HdrUpgrade - | HdrVia-- -- Request Headers --- | HdrAccept- | HdrAcceptCharset- | HdrAcceptEncoding- | HdrAcceptLanguage- | HdrAuthorization- | HdrCookie- | HdrExpect- | HdrFrom- | HdrHost- | HdrIfModifiedSince- | HdrIfMatch- | HdrIfNoneMatch- | HdrIfRange- | HdrIfUnmodifiedSince- | HdrMaxForwards- | HdrProxyAuthorization- | HdrRange- | HdrReferer- | HdrUserAgent-- -- Response Headers- | HdrAge- | HdrLocation- | HdrProxyAuthenticate- | HdrPublic- | HdrRetryAfter- | HdrServer- | HdrSetCookie- | HdrVary- | HdrWarning- | HdrWWWAuthenticate-- -- Entity Headers- | HdrAllow- | HdrContentBase- | HdrContentEncoding- | HdrContentLanguage- | HdrContentLength- | HdrContentLocation- | HdrContentMD5- | HdrContentRange- | HdrContentType- | HdrETag- | HdrExpires- | HdrLastModified-- -- Mime entity headers (for sub-parts)- | HdrContentTransferEncoding-- -- | Allows for unrecognised or experimental headers.- | HdrCustom String -- not in header map below.- deriving(Eq)----- Translation between header names and values,--- good candidate for improvement.-headerMap :: [ (String,HeaderName) ]-headerMap - = [ ("Cache-Control" ,HdrCacheControl )- , ("Connection" ,HdrConnection )- , ("Date" ,HdrDate ) - , ("Pragma" ,HdrPragma )- , ("Transfer-Encoding" ,HdrTransferEncoding ) - , ("Upgrade" ,HdrUpgrade ) - , ("Via" ,HdrVia )- , ("Accept" ,HdrAccept )- , ("Accept-Charset" ,HdrAcceptCharset )- , ("Accept-Encoding" ,HdrAcceptEncoding )- , ("Accept-Language" ,HdrAcceptLanguage )- , ("Authorization" ,HdrAuthorization )- , ("From" ,HdrFrom )- , ("Host" ,HdrHost )- , ("If-Modified-Since" ,HdrIfModifiedSince )- , ("If-Match" ,HdrIfMatch )- , ("If-None-Match" ,HdrIfNoneMatch )- , ("If-Range" ,HdrIfRange ) - , ("If-Unmodified-Since" ,HdrIfUnmodifiedSince )- , ("Max-Forwards" ,HdrMaxForwards )- , ("Proxy-Authorization" ,HdrProxyAuthorization)- , ("Range" ,HdrRange ) - , ("Referer" ,HdrReferer )- , ("User-Agent" ,HdrUserAgent )- , ("Age" ,HdrAge )- , ("Location" ,HdrLocation )- , ("Proxy-Authenticate" ,HdrProxyAuthenticate )- , ("Public" ,HdrPublic )- , ("Retry-After" ,HdrRetryAfter )- , ("Server" ,HdrServer )- , ("Vary" ,HdrVary )- , ("Warning" ,HdrWarning )- , ("WWW-Authenticate" ,HdrWWWAuthenticate )- , ("Allow" ,HdrAllow )- , ("Content-Base" ,HdrContentBase )- , ("Content-Encoding" ,HdrContentEncoding )- , ("Content-Language" ,HdrContentLanguage )- , ("Content-Length" ,HdrContentLength )- , ("Content-Location" ,HdrContentLocation )- , ("Content-MD5" ,HdrContentMD5 )- , ("Content-Range" ,HdrContentRange )- , ("Content-Type" ,HdrContentType )- , ("ETag" ,HdrETag )- , ("Expires" ,HdrExpires )- , ("Last-Modified" ,HdrLastModified )- , ("Set-Cookie" ,HdrSetCookie )- , ("Cookie" ,HdrCookie )- , ("Expect" ,HdrExpect ) ]---instance Show HeaderName where- show (HdrCustom s) = s- show x = case filter ((==x).snd) headerMap of- [] -> error "headerMap incomplete"- (h:_) -> fst h-------- | This class allows us to write generic header manipulation functions--- for both 'Request' and 'Response' data types.-class HasHeaders x where- getHeaders :: x -> [Header]- setHeaders :: x -> [Header] -> x------ Header manipulation functions-insertHeader, replaceHeader, insertHeaderIfMissing- :: HasHeaders a => HeaderName -> String -> a -> a----- | Inserts a header with the given name and value.--- Allows duplicate header names.-insertHeader name value x = setHeaders x newHeaders- where- newHeaders = (Header name value) : getHeaders x----- | Adds the new header only if no previous header shares--- the same name.-insertHeaderIfMissing name value x = setHeaders x (newHeaders $ getHeaders x)- where- newHeaders list@(h@(Header n _): rest)- | n == name = list- | otherwise = h : newHeaders rest- newHeaders [] = [Header name value]-- ---- | Removes old headers with duplicate name.-replaceHeader name value x = setHeaders x newHeaders- where- newHeaders = Header name value : [ h | h@(Header n _) <- getHeaders x, name /= n ]- ---- | Inserts multiple headers.-insertHeaders :: HasHeaders a => [Header] -> a -> a-insertHeaders hdrs x = setHeaders x (getHeaders x ++ hdrs)----- | Gets a list of headers with a particular 'HeaderName'.-retrieveHeaders :: HasHeaders a => HeaderName -> a -> [Header]-retrieveHeaders name x = filter matchname (getHeaders x)- where- matchname (Header n _) | n == name = True- matchname _ = False----- | Lookup presence of specific HeaderName in a list of Headers--- Returns the value from the first matching header.-findHeader :: HasHeaders a => HeaderName -> a -> Maybe String-findHeader n = lookupHeader n . getHeaders---- An anomally really:-lookupHeader :: HeaderName -> [Header] -> Maybe String-lookupHeader v (Header n s:t) | v == n = Just s- | otherwise = lookupHeader v t-lookupHeader _ _ = Nothing---------------------------------------------------------------------------------------- HTTP Messages ------------------------------------------------------------------------------------------------------- Protocol version-httpVersion :: String-httpVersion = "HTTP/1.1"----- | The HTTP request method, to be used in the 'Request' object.--- We are missing a few of the stranger methods, but these are--- not really necessary until we add full TLS.-data RequestMethod = HEAD | PUT | GET | POST | OPTIONS | TRACE | DELETE- deriving(Show,Eq)--rqMethodMap :: [(String, RequestMethod)]-rqMethodMap = [("HEAD", HEAD),- ("PUT", PUT),- ("GET", GET),- ("POST", POST),- ("OPTIONS", OPTIONS),- ("TRACE", TRACE),- ("DELETE", DELETE)]---- | An HTTP Request.--- The 'Show' instance of this type is used for message serialisation,--- which means no body data is output.-data Request =- Request { rqURI :: URI -- ^ might need changing in future- -- 1) to support '*' uri in OPTIONS request- -- 2) transparent support for both relative- -- & absolute uris, although this should- -- already work (leave scheme & host parts empty).- , rqMethod :: RequestMethod - , rqHeaders :: [Header]- , rqBody :: String- }------- Notice that request body is not included,--- this show function is used to serialise--- a request for the transport link, we send--- the body separately where possible.-instance Show Request where- show (Request u m h _) =- show m ++ sp ++ alt_uri ++ sp ++ httpVersion ++ crlf- ++ concatMap show h ++ crlf- where- alt_uri = show $ if null (uriPath u) || head (uriPath u) /= '/' - then u { uriPath = '/' : uriPath u } - else u---instance HasHeaders Request where- getHeaders = rqHeaders- setHeaders rq hdrs = rq { rqHeaders=hdrs }-------type ResponseCode = (Int,Int,Int)-type ResponseData = (ResponseCode,String,[Header])-type RequestData = (RequestMethod,URI,[Header])---- | An HTTP Response.--- The 'Show' instance of this type is used for message serialisation,--- which means no body data is output, additionally the output will--- show an HTTP version of 1.1 instead of the actual version returned--- by a server.-data Response =- Response { rspCode :: ResponseCode- , rspReason :: String- , rspHeaders :: [Header]- , rspBody :: String- }- ----- This is an invalid representation of a received response, --- since we have made the assumption that all responses are HTTP/1.1-instance Show Response where- show (Response (a,b,c) reason headers _) =- httpVersion ++ ' ' : map intToDigit [a,b,c] ++ ' ' : reason ++ crlf- ++ concatMap show headers ++ crlf----instance HasHeaders Response where- getHeaders = rspHeaders- setHeaders rsp hdrs = rsp { rspHeaders=hdrs }-------------------------------------------------------------------------------------- Parsing ----------------------------------------------------------------------------------------------------------parseHeader :: String -> Result Header-parseHeader str =- case split ':' str of- Nothing -> Left (ErrorParse $ "Unable to parse header: " ++ str)- Just (k,v) -> Right $ Header (fn k) (trim $ drop 1 v)- where- fn k = case map snd $ filter (match k . fst) headerMap of- [] -> (HdrCustom k)- (h:_) -> h-- match :: String -> String -> Bool- match s1 s2 = map toLower s1 == map toLower s2- --parseHeaders :: [String] -> Result [Header]-parseHeaders = catRslts [] . map (parseHeader . clean) . joinExtended ""- where- -- Joins consecutive lines where the second line- -- begins with ' ' or '\t'.- joinExtended old (h : t)- | not (null h) && (head h == ' ' || head h == '\t')- = joinExtended (old ++ ' ' : tail h) t- | otherwise = old : joinExtended h t- joinExtended old [] = [old]-- clean [] = []- clean (h:t) | h `elem` "\t\r\n" = ' ' : clean t- | otherwise = h : clean t-- -- tollerant of errors? should parse- -- errors here be reported or ignored?- -- currently ignored.- catRslts :: [a] -> [Result a] -> Result [a]- catRslts list (h:t) = - case h of- Left _ -> catRslts list t- Right v -> catRslts (v:list) t- catRslts list [] = Right $ reverse list - ---- Parsing a request-parseRequestHead :: [String] -> Result RequestData-parseRequestHead [] = Left ErrorClosed-parseRequestHead (com:hdrs) =- requestCommand com `bindE` \(_version,rqm,uri) ->- parseHeaders hdrs `bindE` \hdrs' ->- Right (rqm,uri,hdrs')- where- requestCommand line- = case words line of- _yes@(rqm:uri:version) -> case (parseURIReference uri, lookup rqm rqMethodMap) of- (Just u, Just r) -> Right (version,r,u)- _ -> Left (ErrorParse $ "Request command line parse failure: " ++ line)- _no -> if null line- then Left ErrorClosed- else Left (ErrorParse $ "Request command line parse failure: " ++ line) ---- Parsing a response-parseResponseHead :: [String] -> Result ResponseData-parseResponseHead [] = Left ErrorClosed-parseResponseHead (sts:hdrs) = - responseStatus sts `bindE` \(_version,code,reason) ->- parseHeaders hdrs `bindE` \hdrs' ->- Right (code,reason,hdrs')- where-- responseStatus line- = case words line of- _yes@(version:code:reason) -> Right (version,match code,concatMap (++" ") reason)- _no -> if null line - then Left ErrorClosed -- an assumption- else Left (ErrorParse $ "Response status line parse failure: " ++ line)--- match [a,b,c] = (digitToInt a,- digitToInt b,- digitToInt c)- match _ = (-1,-1,-1) -- will create appropriate behaviour--- -------------------------------------------------------------------------------------- HTTP Send / Recv ------------------------------------------------------------------------------------------------------data Behaviour = Continue- | Retry- | Done- | ExpectEntity- | DieHorribly String------matchResponse :: RequestMethod -> ResponseCode -> Behaviour-matchResponse rqst rsp =- case rsp of- (1,0,0) -> Continue- (1,0,1) -> Done -- upgrade to TLS- (1,_,_) -> Continue -- default- (2,0,4) -> Done- (2,0,5) -> Done- (2,_,_) -> ans- (3,0,4) -> Done- (3,0,5) -> Done- (3,_,_) -> ans- (4,1,7) -> Retry -- Expectation failed- (4,_,_) -> ans- (5,_,_) -> ans- (a,b,c) -> DieHorribly ("Response code " ++ map intToDigit [a,b,c] ++ " not recognised")- where- ans | rqst == HEAD = Done- | otherwise = ExpectEntity- ---- | Simple way to get a resource across a non-persistant connection.--- Headers that may be altered:--- Host Altered only if no Host header is supplied, HTTP\/1.1--- requires a Host header.--- Connection Where no allowance is made for persistant connections--- the Connection header will be set to "close"-simpleHTTP :: Request -> IO (Result Response)-simpleHTTP r = - do - auth <- getAuth r- c <- openTCPPort (uriRegName auth) (port auth)- simpleHTTP_ c r- where- port auth = if null (uriPort auth)- then 80- else read $ uriPort auth- ---- | Like 'simpleHTTP', but acting on an already opened stream.-simpleHTTP_ :: Stream s => s -> Request -> IO (Result Response)-simpleHTTP_ s r =- do - auth <- getAuth r- let r' = fixReq auth r - rsp <- if debug then do- s' <- debugStream httpLogFile s- sendHTTP s' r'- else- sendHTTP s r'- -- already done by sendHTTP because of "Connection: close" header- --; close s - return rsp- where- {- RFC 2616, section 5.1.2:- "The most common form of Request-URI is that used to identify a- resource on an origin server or gateway. In this case the absolute- path of the URI MUST be transmitted (see section 3.2.1, abs_path) as- the Request-URI, and the network location of the URI (authority) MUST- be transmitted in a Host header field." -}- -- we assume that this is the case, so we take the host name from- -- the Host header if there is one, otherwise from the request-URI.- -- Then we make the request-URI an abs_path and make sure that there- -- is a Host header.- fixReq :: URIAuth -> Request -> Request- fixReq URIAuth{uriRegName=h} req = - insertHeaderIfMissing HdrConnection "close" $- insertHeaderIfMissing HdrHost h $- req { rqURI = (rqURI req){ uriScheme = "", - uriAuthority = Nothing } } ---- | this is not the most graceful of implementations.--- The problem is that Network.URI.authority is--- deprecated. And we want to use Network.URI.URIAuth.------ So this method use to parse a "host" field as a URI--- auth, which is not stictly correct. We still --- fake that behavior here.-getAuth :: Monad m => Request -> m URIAuth-getAuth r = case auth of- Just x -> return x - Nothing -> fail $ "Error parsing URI authority '"- ++ show (rqURI r) ++ "'"- where- auth = case findHeader HdrHost r of- Just h -> Just $ fakeAuth h - Nothing -> uriAuthority (rqURI r)- fakeAuth h = fst . head $ (flip readP_to_S) h $ do- host<-many1 $ satisfy (\c -> c /= ':')- port<-option "" $ char ':' >> many1 get- return URIAuth{uriRegName=host, uriPort=port, uriUserInfo=""}---sendHTTP :: Stream s => s -> Request -> IO (Result Response)-sendHTTP conn rq- = do rst <- sendHTTPPipelined conn [rq]- case rst of- ([response],_) -> return (Right response)- (_,Just err) -> return (Left err)- (_,_) -> error "Case not supported in sendHTTP"--sendHTTPPipelined :: Stream s => s -> [Request] -> IO ([Response],Maybe ConnError)-sendHTTPPipelined conn rqs = - do { (ok,rsp) <- Exception.catch (main (map fixHostHeader rqs))- (\(e::SomeException) -> do { close conn; throw e })- ; let fn list = when (any findConnClose list)- (close conn)- ; fn (map rqHeaders rqs ++ map rspHeaders ok)- ; return (ok,rsp)- }- where --- From RFC 2616, section 8.2.3:--- 'Because of the presence of older implementations, the protocol allows--- ambiguous situations in which a client may send "Expect: 100---- continue" without receiving either a 417 (Expectation Failed) status--- or a 100 (Continue) status. Therefore, when a client sends this--- header field to an origin server (possibly via a proxy) from which it--- has never seen a 100 (Continue) status, the client SHOULD NOT wait--- for an indefinite period before sending the request body.'------ Since we would wait forever, I have disabled use of 100-continue for now.- main :: [Request] -> IO ([Response], Maybe ConnError)- main rqsts =- do - --let str = if null (rqBody rqst)- -- then show rqst- -- else show (insertHeader HdrExpect "100-continue" rqst)- -- write body immediately, don't wait for 100 CONTINUE- writeBlock conn $ concat $ intersperse "\r\n" [ show rqst ++ rqBody rqst | rqst <- rqsts ]- rets <- forM rqsts $ \rqst ->- do rsp <- getResponseHead- switchResponse True True rsp rqst- return (sequenceResponses rets)-- sequenceResponses :: [Result Response] -> ([Response], Maybe ConnError)- sequenceResponses = worker []- where worker acc [] = (reverse acc, Nothing)- worker acc (Right x:xs) = worker (x:acc) xs- worker acc (Left x:_) = (reverse acc,Just x)-- -- reads and parses headers- getResponseHead :: IO (Result ResponseData)- getResponseHead =- do { lor <- readTillEmpty1 conn- ; return $ lor `bindE` parseResponseHead- }-- -- Hmmm, this could go bad if we keep getting "100 Continue"- -- responses... Except this should never happen according- -- to the RFC.- switchResponse :: Bool {- allow retry? -}- -> Bool {- is body sent? -}- -> Result ResponseData- -> Request- -> IO (Result Response)- - switchResponse _ _ (Left e) _ = return (Left e)- -- retry on connreset?- -- if we attempt to use the same socket then there is an excellent- -- chance that the socket is not in a completely closed state.-- switchResponse allow_retry bdy_sent (Right (cd,rn,hdrs)) rqst =- case matchResponse (rqMethod rqst) cd of- Continue- | not bdy_sent -> {- Time to send the body -}- do { val <- writeBlock conn (rqBody rqst)- ; case val of- Left e -> return (Left e)- Right _ ->- do { rsp <- getResponseHead- ; switchResponse allow_retry True rsp rqst- }- }- | otherwise -> {- keep waiting -}- do { rsp <- getResponseHead- ; switchResponse allow_retry bdy_sent rsp rqst - }-- Retry -> {- Request with "Expect" header failed.- Trouble is the request contains Expects- other than "100-Continue" -}- do { writeBlock conn (show rqst ++ rqBody rqst)- ; rsp <- getResponseHead- ; switchResponse False bdy_sent rsp rqst- } - - Done ->- return (Right $ Response cd rn hdrs "")-- DieHorribly str ->- return $ Left $ ErrorParse ("Invalid response: " ++ str)-- ExpectEntity ->- let tc = lookupHeader HdrTransferEncoding hdrs- cl = lookupHeader HdrContentLength hdrs- in- do { rslt <- case tc of- Nothing -> - case cl of- Just x -> linearTransfer conn (read x :: Int)- Nothing -> hopefulTransfer conn ""- Just x -> - case map toLower (trim x) of- "chunked" -> chunkedTransfer conn- _ -> uglyDeathTransfer conn- ; return $ rslt `bindE` \(ftrs,bdy) -> Right (Response cd rn (hdrs++ftrs) bdy) - }-- - -- Adds a Host header if one is NOT ALREADY PRESENT- fixHostHeader :: Request -> Request- fixHostHeader rq =- let uri = rqURI rq- h = fmap uriRegName $ uriAuthority uri- in case h of- Just x -> insertHeaderIfMissing HdrHost x rq- _ -> rq- - -- Looks for a "Connection" header with the value "close".- -- Returns True when this is found.- findConnClose :: [Header] -> Bool- findConnClose hdrs =- case lookupHeader HdrConnection hdrs of- Nothing -> False- Just x -> map toLower (trim x) == "close"---- | Receive and parse a HTTP request from the given Stream. Should be used --- for server side interactions.-receiveHTTP :: Stream s => s -> IO (Result Request)-receiveHTTP conn = do rq <- getRequestHead- processRequest rq - where- -- reads and parses headers- getRequestHead :: IO (Result RequestData)- getRequestHead =- do { lor <- readTillEmpty1 conn- ; return $ lor `bindE` parseRequestHead- }- - processRequest (Left e) = return $ Left e- processRequest (Right (rm,uri,hdrs)) = - do -- FIXME : Also handle 100-continue.- let tc = lookupHeader HdrTransferEncoding hdrs- cl = lookupHeader HdrContentLength hdrs- rslt <- case tc of- Nothing ->- case cl of- Just x -> linearTransfer conn (read x :: Int)- Nothing -> return (Right ([], "")) -- hopefulTransfer ""- Just x ->- case map toLower (trim x) of- "chunked" -> chunkedTransfer conn- _ -> uglyDeathTransfer conn- - return $ rslt `bindE` \(ftrs,bdy) -> Right (Request uri rm (hdrs++ftrs) bdy)----- | Very simple function, send a HTTP response over the given stream. This --- could be improved on to use different transfer types.-respondHTTP :: Stream s => s -> Response -> IO ()-respondHTTP conn rsp = do writeBlock conn (show rsp)- -- write body immediately, don't wait for 100 CONTINUE- writeBlock conn (rspBody rsp)- return ()---- The following functions were in the where clause of sendHTTP, they have--- been moved to global scope so other functions can access them. ---- | Used when we know exactly how many bytes to expect.-linearTransfer :: Stream s => s -> Int -> IO (Result ([Header],String))-linearTransfer conn n- = do info <- readBlock conn n- return $ info `bindE` \str -> Right ([],str)---- | Used when nothing about data is known,--- Unfortunately waiting for a socket closure--- causes bad behaviour. Here we just--- take data once and give up the rest.-hopefulTransfer :: Stream s => s -> String -> IO (Result ([Header],String))-hopefulTransfer conn str- = readLine conn >>= - either (\v -> return $ Left v)- (\more -> if null more - then return (Right ([],str)) - else hopefulTransfer conn (str++more))--- | A necessary feature of HTTP\/1.1--- Also the only transfer variety likely to--- return any footers.-chunkedTransfer :: Stream s => s -> IO (Result ([Header],String))-chunkedTransfer conn- = chunkedTransferC conn 0 >>= \v ->- return $ v `bindE` \(ftrs,c,info) ->- let myftrs = Header HdrContentLength (show c) : ftrs - in Right (myftrs,info)--chunkedTransferC :: Stream s => s -> Int -> IO (Result ([Header],Int,String))-chunkedTransferC conn n- = readLine conn >>= \v -> case v of- Left e -> return (Left e)- Right line ->- let size = ( if null line || (head line) == '0'- then 0- else case readHex line of- (n',_):_ -> n'- _ -> 0- )- in if size == 0- then do { rs <- readTillEmpty2 conn []- ; return $- rs `bindE` \strs ->- parseHeaders strs `bindE` \ftrs ->- Right (ftrs,n,"")- }- else do { some <- readBlock conn size- ; readLine conn- ; more <- chunkedTransferC conn (n+size)- ; return $ - some `bindE` \cdata ->- more `bindE` \(ftrs,m,mdata) -> - Right (ftrs,m,cdata++mdata) - } ---- | Maybe in the future we will have a sensible thing--- to do here, at that time we might want to change--- the name.-uglyDeathTransfer :: Stream s => s -> IO (Result ([Header],String))-uglyDeathTransfer _- = return $ Left $ ErrorParse "Unknown Transfer-Encoding"---- | Remove leading crlfs then call readTillEmpty2 (not required by RFC)-readTillEmpty1 :: Stream s => s -> IO (Result [String])-readTillEmpty1 conn =- do { line <- readLine conn- ; case line of- Left e -> return $ Left e- Right s ->- if s == crlf- then readTillEmpty1 conn- else readTillEmpty2 conn [s]- }---- | Read lines until an empty line (CRLF),--- also accepts a connection close as end of--- input, which is not an HTTP\/1.1 compliant--- thing to do - so probably indicates an--- error condition.-readTillEmpty2 :: Stream s => s -> [String] -> IO (Result [String])-readTillEmpty2 conn list =- do { line <- readLine conn- ; case line of- Left e -> return $ Left e- Right s ->- if s == crlf || null s- then return (Right $ reverse (s:list))- else readTillEmpty2 conn (s:list)- }-- ------------------------------------------------------------------------------------- A little friendly funtionality ------------------------------------------------------------------------------------{-- I had a quick look around but couldn't find any RFC about- the encoding of data on the query string. I did find an- IETF memo, however, so this is how I justify the urlEncode- and urlDecode methods.-- Doc name: draft-tiwari-appl-wxxx-forms-01.txt (look on www.ietf.org)-- Reserved chars: ";", "/", "?", ":", "@", "&", "=", "+", ",", and "$" are reserved.- Unwise: "{" | "}" | "|" | "\" | "^" | "[" | "]" | "`"- URI delims: "<" | ">" | "#" | "%" | <">- Unallowed ASCII: <US-ASCII coded characters 00-1F and 7F hexadecimal>- <US-ASCII coded character 20 hexadecimal>- Also unallowed: any non-us-ascii character-- Escape method: char -> '%' a b where a, b :: Hex digits--}--urlEncode, urlDecode :: String -> String--urlDecode ('%':a:b:rest) = chr (16 * digitToInt a + digitToInt b)- : urlDecode rest-urlDecode (h:t) = h : urlDecode t-urlDecode [] = []--urlEncode (h:t) =- let str = if reserved_ (ord h) then escape h else [h]- in str ++ urlEncode t- where- reserved_ x- | x >= ord 'a' && x <= ord 'z' = False- | x >= ord 'A' && x <= ord 'Z' = False- | x >= ord '0' && x <= ord '9' = False- | x <= 0x20 || x >= 0x7F = True- | otherwise = x `elem` map ord [';','/','?',':','@','&'- ,'=','+',',','$','{','}'- ,'|','\\','^','[',']','`'- ,'<','>','#','%','"']- -- wouldn't it be nice if the compiler- -- optimised the above for us?-- escape x = - let y = ord x - in [ '%', intToDigit ((y `div` 16) .&. 0xf), intToDigit (y .&. 0xf) ]--urlEncode [] = []- ----- Encode form variables, useable in either the--- query part of a URI, or the body of a POST request.--- I have no source for this information except experience,--- this sort of encoding worked fine in CGI programming.-urlEncodeVars :: [(String,String)] -> String-urlEncodeVars ((n,v):t) =- let (same,diff) = partition ((==n) . fst) t- in urlEncode n ++ '=' : foldl (\x y -> x ++ ',' : urlEncode y) (urlEncode $ v) (map snd same)- ++ urlEncodeRest diff- where urlEncodeRest [] = []- urlEncodeRest diff = '&' : urlEncodeVars diff-urlEncodeVars [] = []
− src/Happstack/Server/HTTPClient/Stream.hs
@@ -1,173 +0,0 @@--------------------------------------------------------------------------------- |--- Module : Happstack.Server.HTTPClient.Stream--- Copyright : (c) Warrick Gray 2002, Bjorn Bringert 2003-2004, Simon Foster 2004--- License : BSD------ Maintainer : bjorn@bringert.net--- Stability : experimental--- Portability : non-portable (not tested)------ An library for creating abstract streams. Originally part of Gray's\/Bringert's--- HTTP module.------ * Changes by Simon Foster:--- - Split module up into to sepearate Network.[Stream,TCP,HTTP] modules--- -------------------------------------------------------------------------------module Happstack.Server.HTTPClient.Stream (- -- ** Streams- Debug,- Stream(..),- debugStream,- - -- ** Errors- ConnError(..),- Result,- handleSocketError,- bindE,- myrecv--) where--import Control.Exception.Extensible as Exception-import System.IO.Error---- Networking-import Network.Socket--import Control.Monad (liftM)-import System.IO--data ConnError = ErrorReset - | ErrorClosed- | ErrorParse String- | ErrorMisc String- deriving(Show,Eq)---- error propagating:--- we could've used a monad, but that would lead us--- into using the "-fglasgow-exts" compile flag.-bindE :: Either ConnError a -> (a -> Either ConnError b) -> Either ConnError b-bindE (Left e) _ = Left e-bindE (Right v) f = f v---- | This is the type returned by many exported network functions.-type Result a = Either ConnError {- error -}- a {- result -}-------------------------------------------------------------------------------------- Gentle Art of Socket Sucking --------------------------------------------------------------------------------------- | Streams should make layering of TLS protocol easier in future,--- they allow reading/writing to files etc for debugging,--- they allow use of protocols other than TCP/IP--- and they allow customisation.------ Instances of this class should not trim--- the input in any way, e.g. leave LF on line--- endings etc. Unless that is exactly the behaviour--- you want from your twisted instances ;)-class Stream x where - readLine :: x -> IO (Result String)- readBlock :: x -> Int -> IO (Result String)- writeBlock :: x -> String -> IO (Result ())- close :: x -> IO ()-------- Exception handler for socket operations-handleSocketError :: Socket -> Exception.SomeException -> IO (Result a)-handleSocketError sk e =- do { se <- getSocketOption sk SoError- ; if se == 0- then throw e- else return $ if se == 10054 -- reset- then Left ErrorReset- else Left $ ErrorMisc $ show se- }-----instance Stream Socket where- readBlock sk n = (liftM Right $ fn n) `Exception.catch` (handleSocketError sk)- where- fn x = do { str <- myrecv sk x- ; let len = length str- ; if len < x && len /= 0- then ( fn (x-len) >>= \more -> return (str++more) ) - else return str- }-- -- Use of the following function is discouraged.- -- The function reads in one character at a time, - -- which causes many calls to the kernel recv()- -- hence causes many context switches.- readLine sk = (liftM Right $ fn "") `Exception.catch` (handleSocketError sk)- where- fn str =- do { c <- myrecv sk 1 -- like eating through a straw.- ; if null c || c == "\n"- then return (reverse str++c)- else fn (head c:str)- }- - writeBlock sk str = (liftM Right $ fn str) `Exception.catch` (handleSocketError sk)- where- fn [] = return ()- fn x = send sk x >>= \i -> fn (drop i x)-- -- This slams closed the connection (which is considered rude for TCP\/IP)- close sk = shutdown sk ShutdownBoth >> sClose sk--myrecv :: Socket -> Int -> IO String-myrecv sock len =- let handler e = if isEOFError e then return [] else ioError e- in System.IO.Error.catch (recv sock len) handler---- | Allows stream logging.--- Refer to 'debugStream' below.-data Debug x = Dbg Handle x---instance (Stream x) => Stream (Debug x) where- readBlock (Dbg h c) n =- do { val <- readBlock c n- ; hPutStrLn h ("readBlock " ++ show n ++ ' ' : show val)- ; hFlush h- ; return val- }-- readLine (Dbg h c) =- do { val <- readLine c- ; hPutStrLn h ("readLine " ++ show val)- ; return val- }-- writeBlock (Dbg h c) str =- do { val <- writeBlock c str- ; hPutStrLn h ("writeBlock " ++ show val ++ ' ' : show str)- ; return val- }-- close (Dbg h c) =- do { hPutStrLn h "closing..."- ; hFlush h- ; close c- ; hPutStrLn h "...closed"- ; hClose h- }----- | Wraps a stream with logging I\/O, the first--- argument is a filename which is opened in AppendMode.-debugStream :: (Stream a) => String -> a -> IO (Debug a)-debugStream file stm = - do { h <- openFile file AppendMode- ; hPutStrLn h "File opened for appending."- ; return (Dbg h stm)- }
− src/Happstack/Server/HTTPClient/TCP.hs
@@ -1,193 +0,0 @@-{-# LANGUAGE ScopedTypeVariables, PatternSignatures #-}--------------------------------------------------------------------------------- |--- Module : Happstack.Server.HTTPClient.TCP--- Copyright : (c) Warrick Gray 2002, Bjorn Bringert 2003-2004, Simon Foster 2004--- License : BSD------ Maintainer : bjorn@bringert.net--- Stability : experimental--- Portability : non-portable (not tested)------ An easy access TCP library. Makes the use of TCP in Haskell much easier.--- This was originally part of Gray's\/Bringert's HTTP module.------ * Changes by Simon Foster:--- - Split module up into to sepearate Network.[Stream,TCP,HTTP] modules--- -------------------------------------------------------------------------------module Happstack.Server.HTTPClient.TCP (- -- ** Connections- Conn(..),- Connection(..),- openTCP,- openTCPPort,- isConnectedTo-) where--import Control.Exception.Extensible as Exception---- Networking-import Network.BSD-import Network.Socket-import Happstack.Server.HTTPClient.Stream--import Data.List (elemIndex)-import Data.Char-import Data.IORef-import System.IO-------------------------------------------------------------------------------------- TCP Connections ---------------------------------------------------------------------------------------------------- | The 'Connection' newtype is a wrapper that allows us to make--- connections an instance of the StreamIn\/Out classes, without ghc extensions.--- While this looks sort of like a generic reference to the transport--- layer it is actually TCP specific, which can be seen in the--- implementation of the 'Stream Connection' instance.-newtype Connection = ConnRef {getRef :: IORef Conn}----- | The 'Conn' object allows input buffering, and maintenance of --- some admin-type data.-data Conn = MkConn { connSock :: ! Socket- , connAddr :: ! SockAddr - , connBffr :: ! String - , connHost :: String- }- | ConnClosed- deriving(Eq)----- | Open a connection to port 80 on a remote host.-openTCP :: String -> IO Connection-openTCP host = openTCPPort host 80----- | This function establishes a connection to a remote--- host, it uses "getHostByName" which interrogates the--- DNS system, hence may trigger a network connection.------ Add a "persistant" option? Current persistant is default.--- Use "Result" type for synchronous exception reporting?-openTCPPort :: String -> Int -> IO Connection-openTCPPort uri port = - do { s <- socket AF_INET Stream 6- ; setSocketOption s KeepAlive 1- ; host <- Exception.catch (inet_addr uri) -- handles ascii IP numbers- (\(_::SomeException) -> getHostByName uri >>= \host -> -- _shrug_ this will catch _any_ exception FIXME- case hostAddresses host of- [] -> return (error "no addresses in host entry")- (h:_) -> return h)- ; let a = SockAddrInet (toEnum port) host- ; Exception.catch (connect s a) (\(e::SomeException) -> sClose s >> throw e)- ; v <- newIORef (MkConn s a [] uri)- ; return (ConnRef v)- }--instance Stream Connection where- readBlock ref n = - readIORef (getRef ref) >>= \conn -> case conn of- ConnClosed -> return (Left ErrorClosed)- (MkConn sk _addr bfr _hst)- | length bfr >= n ->- do { modifyIORef (getRef ref) (\c -> c { connBffr=(drop n bfr) })- ; return (Right $ take n bfr)- }- | otherwise ->- do { modifyIORef (getRef ref) (\c -> c { connBffr=[] })- ; more <- readBlock sk (n - length bfr)- ; return $ case more of- Left _ -> more- Right s -> (Right $ bfr ++ s)- }-- -- This function uses a buffer, at this time the buffer is just 1000 characters.- -- (however many bytes this is is left to the user to decypher)- readLine ref =- readIORef (getRef ref) >>= \conn -> case conn of- ConnClosed -> return (Left ErrorClosed)- (MkConn sk _addr bfr _)- | null bfr -> {- read in buffer -}- do { str <- myrecv sk 1000 -- DON'T use "readBlock sk 1000" !!- -- ... since that call will loop.- ; let len = length str- ; if len == 0 {- indicates a closed connection -}- then return (Right "")- else modifyIORef (getRef ref) (\c -> c { connBffr=str })- >> readLine ref -- recursion- }- | otherwise ->- case elemIndex '\n' bfr of- Nothing -> {- need recursion to finish line -}- do { modifyIORef (getRef ref) (\c -> c { connBffr=[] })- ; more <- readLine ref -- contains extra recursion - ; return $ more `bindE` \str -> Right (bfr++str)- }- Just i -> {- end of line found -}- let (bgn,end) = splitAt i bfr in- do { modifyIORef (getRef ref) (\c -> c { connBffr=(drop 1 end) })- ; return (Right (bgn++['\n']))- }---- -- The 'Connection' object allows no outward buffering, - -- since in general messages are serialised in their entirety.- writeBlock ref str =- readIORef (getRef ref) >>= \conn -> case conn of- ConnClosed -> return (Left ErrorClosed)- (MkConn sk addr _ _) -> fn sk addr str `Exception.catch` (handleSocketError sk)- where- fn sk addr s- | null s = return (Right ()) -- done- | otherwise =- getSocketOption sk SoError >>= \se ->- if se == 0- then sendTo sk s addr >>= \i -> fn sk addr (drop i s)- else writeIORef (getRef ref) ConnClosed >>- if se == 10054- then return (Left ErrorReset)- else return (Left $ ErrorMisc $ show se)--- -- Closes a Connection. Connection will no longer- -- allow any of the other Stream functions. Notice that a Connection may close- -- at any time before a call to this function. This function is idempotent.- -- (I think the behaviour here is TCP specific)- close ref = - do { c <- readIORef (getRef ref)- ; closeConn c `Exception.catch` (\(_::SomeException) -> return ()) -- FIXME see above- ; writeIORef (getRef ref) ConnClosed- }- where- -- Be kind to peer & close gracefully.- closeConn (ConnClosed) = return ()- closeConn (MkConn sk _addr [] _) =- do { shutdown sk ShutdownSend- ; suck ref- ; shutdown sk ShutdownReceive- ; sClose sk- }- closeConn (MkConn _ _ _ _) = error "Case in closeConn not supported"-- suck :: Connection -> IO ()- suck cn = readLine cn >>= - either (\_ -> return ()) -- catch errors & ignore- (\x -> if null x then return () else suck cn)---- | Checks both that the underlying Socket is connected--- and that the connection peer matches the given--- host name (which is recorded locally).-isConnectedTo :: Connection -> String -> IO Bool-isConnectedTo conn name =- do { v <- readIORef (getRef conn)- ; case v of- ConnClosed -> return False- (MkConn sk _ _ h) ->- if (map toLower h == map toLower name)- then sIsConnected sk- else return False- }-
+ src/Happstack/Server/I18N.hs view
@@ -0,0 +1,52 @@+module Happstack.Server.I18N+ ( acceptLanguage+ , bestLanguage+ ) where++import Control.Arrow ((>>>), first, second)+import Data.Function (on)+import qualified Data.ByteString.Char8 as C+import Data.List (sortBy)+import Data.Maybe (fromMaybe)+import Data.Text as Text (Text, breakOnAll, pack, singleton)+import Happstack.Server.Monads (Happstack, getHeaderM)+import Happstack.Server.Internal.Compression (encodings)+import Text.ParserCombinators.Parsec (parse)++-- TODO: proper Accept-Language parser++-- | parse the 'Accept-Language' header, or [] if not found.+acceptLanguage :: (Happstack m) => m [(Text, Maybe Double)]+acceptLanguage =+ do mAcceptLanguage <- (fmap C.unpack) <$> getHeaderM "Accept-Language"+ case mAcceptLanguage of+ Nothing -> return []+ (Just al) ->+ case parse encodings al al of+ (Left _) -> return []+ (Right encs) -> return (map (first Text.pack) encs)++-- | deconstruct the 'acceptLanguage' results a return a list of+-- languages sorted by preference in descending order.+--+-- Note: this implementation does not conform to RFC4647+--+-- Among other things, it does not handle wildcards. A proper+-- implementation needs to take a list of available languages.+bestLanguage :: [(Text, Maybe Double)] -> [Text]+bestLanguage range =+ -- is no 'q' param, set 'q' to 1.0+ map (second $ fromMaybe 1) >>>+ -- sort in descending order+ sortBy (flip compare `on` snd) >>>+ -- remove entries with '*' or q == 0. Removing '*' entries is not+ -- technically correct, but it is the best we can do with out a+ -- list of available languages.+ filter (\(lang, q) -> lang /= (Text.singleton '*') && q > 0) >>>+ -- lookup fallback (RFC 4647, Section 3.4)+ concatMap (explode . fst) $+ range+ where+ -- | example: "en-us-gb" -> ["en-us-gb","en-us","en"]+ explode :: Text -> [Text]+ explode lang = lang : (reverse $ map fst $ breakOnAll (singleton '-') lang)
+ src/Happstack/Server/Internal/Clock.hs view
@@ -0,0 +1,58 @@+{-# LANGUAGE CPP #-}+{-# OPTIONS -fno-cse #-}+module Happstack.Server.Internal.Clock+ ( getApproximateTime+ , getApproximatePOSIXTime+ , getApproximateUTCTime+ , formatHttpDate+ ) where++import Control.Concurrent+import Control.Monad+import qualified Data.ByteString.Char8 as B+import Data.IORef+import Data.Time.Clock (UTCTime)+import Data.Time.Clock.POSIX (POSIXTime, getPOSIXTime, posixSecondsToUTCTime)+import Data.Time.Format (formatTime, defaultTimeLocale)+import System.IO.Unsafe++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)+ void $ 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,276 @@+{-# LANGUAGE CPP,NoMonomorphismRestriction, FlexibleContexts #-}+-- | Filter for compressing the 'Response' body.+module Happstack.Server.Internal.Compression+ ( compressedResponseFilter+ , compressedResponseFilter'+ , compressWithFilter+ , gzipFilter+ , deflateFilter+ , identityFilter+ , starFilter+ , encodings+ , standardEncodingHandlers+ ) where+import Happstack.Server.SimpleHTTP+import Text.ParserCombinators.Parsec+import Control.Monad+#if !MIN_VERSION_base(4,13,0)+import Control.Monad.Fail (MonadFail)+#endif+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@.+--+-- This function uses 'standardEncodingHandlers'. If you want to+-- provide alternative handers (perhaps to change compression levels),+-- see 'compressedResponseFilter''+--+-- > main =+-- > simpleHTTP nullConf $+-- > do str <- compressedResponseFilter+-- > return $ toResponse ("This response compressed using: " ++ str)+compressedResponseFilter :: (FilterMonad Response m, MonadPlus m, WebMonad Response m, ServerMonad m, MonadFail m) =>+ m String -- ^ name of the encoding chosen+compressedResponseFilter = compressedResponseFilter' standardEncodingHandlers++-- | reads the @Accept-Encoding@ header. Then, if possible+-- will compress the response body using one of the supplied filters.+--+-- A filter function takes two arguments. The first is a 'String' with+-- the value to be used as the 'Content-Encoding' header. The second+-- is 'Bool' which indicates if the compression filter is allowed to+-- fallback to @identity@.+--+-- This is important if the resource being sent using sendfile, since+-- sendfile does not provide a compression option. If @identity@ is+-- allowed, then the file can be sent uncompressed using sendfile. But+-- if @identity@ is not allowed, then the filter will need to return+-- error 406.+--+-- You should probably always include the @identity@ and @*@ encodings+-- as acceptable.+--+-- > myFilters :: (FilterMonad Response m) => [(String, String -> Bool -> m ()]+-- > myFilters = [ ("gzip" , gzipFilter)+-- > , ("identity", identityFilter)+-- > , ("*" , starFilter)+-- > ]+-- >+-- > main =+-- > simpleHTTP nullConf $+-- > do let filters =+-- > str <- compressedResponseFilter'+-- > return $ toResponse ("This response compressed using: " ++ str)+compressedResponseFilter' ::+ (FilterMonad Response m, MonadPlus m, WebMonad Response m, ServerMonad m, MonadFail m)+ => [(String, String -> Bool -> m ())] -- ^ compression filter assoc list+ -> m String -- ^ name of the encoding chosen+compressedResponseFilter' encodingHandlers = 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 (map fst encodingHandlers) $ 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 encodingHandlers)+ )+ Right [] -> fail badEncoding+ 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++-- | compression filter for the identity encoding (aka, do nothing)+--+-- see also: 'compressedResponseFilter'+identityFilter :: (FilterMonad Response m) =>+ String -- ^ encoding to use for Content-Encoding header+ -> Bool -- ^ fallback to identity for SendFile (irrelavant for this filter)+ -> m ()+identityFilter = compressWithFilter id++-- | compression filter for the * encoding+--+-- This filter always fails.+starFilter :: (FilterMonad Response m, MonadFail m) =>+ String -- ^ encoding to use for Content-Encoding header+ -> Bool -- ^ fallback to identity for SendFile (irrelavant for this filter)+ -> m ()+starFilter _ _ = fail "chose * as content encoding"++-- | 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', 'deflateFilter', 'identityFilter', 'starFilter', 'compressedResponseFilter''+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 $+ setHeader "Vary" "Accept-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)+++-- | an assoc list of encodings and their corresponding compression+-- functions.+--+-- e.g.+--+-- > [("gzip", gzipFilter), ("identity", identityFilter), ("*",starFilter)]+standardEncodingHandlers :: (FilterMonad Response m, MonadFail m) =>+ [(String, String -> Bool -> m ())]+standardEncodingHandlers = zip standardEncodings handlers++standardEncodings :: [String]+standardEncodings =+ ["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, MonadFail m) => [String -> Bool -> m ()]+handlers =+ [ gzipFilter+ , gzipFilter+-- ,compressFilter+-- ,compressFilter+ , deflateFilter+ , identityFilter+ , starFilter+ ]++-- | a parser for the Accept-Encoding header+encodings :: GenParser Char st [(String, 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,231 @@+{-# LANGUAGE CPP, DeriveDataTypeable #-}++-- http://tools.ietf.org/html/rfc2109+module Happstack.Server.Internal.Cookie+ ( Cookie(..)+ , CookieLife(..)+ , SameSite(..)+ , calcLife+ , mkCookie+ , mkCookieHeader+ , getCookies+ , getCookie+ , getCookies'+ , getCookie'+ , parseCookies+ , cookiesParser+ )+ where++import Control.Monad+import Control.Monad.Fail (MonadFail)+import qualified Data.ByteString.Char8 as C+import Data.Char (chr, toLower)+import Data.Data (Data)+import Data.List ((\\), intersperse)+import Data.Time.Clock (UTCTime, addUTCTime, diffUTCTime)+import Data.Time.Clock.POSIX (posixSecondsToUTCTime)+import Data.Time.Format (formatTime, defaultTimeLocale)+import Happstack.Server.Internal.Clock (getApproximateUTCTime)+import Network.URI (escapeURIString)+import Text.ParserCombinators.Parsec hiding (token)+++++-- | a type for HTTP cookies. Usually created using 'mkCookie'.+data Cookie = Cookie+ { cookieVersion :: String+ , cookiePath :: String+ , cookieDomain :: String+ , cookieName :: String+ , cookieValue :: String+ , secure :: Bool+ , httpOnly :: Bool+ , sameSite :: SameSite+ , partitioned :: Bool+ } deriving (Show, Eq, Read, 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 Int -- ^ life time of cookie in seconds+ | Expires UTCTime -- ^ cookie expiration date+ | Expired -- ^ cookie already expired+ deriving (Eq, Ord, Read, Show)++-- | Options for specifying third party cookie behaviour.+--+-- Note that most or all web clients require the cookie to be secure if "none" is+-- specified.+data SameSite+ = SameSiteLax+ -- ^ The cookie is sent in first party contexts as well as linked requests initiated+ -- from other contexts.+ | SameSiteStrict+ -- ^ The cookie is sent in first party contexts only.+ | SameSiteNone+ -- ^ The cookie is sent in first as well as third party contexts if the cookie is+ -- secure.+ | SameSiteNoValue+ -- ^ The default; used if you do not wish a SameSite attribute present at all.+ deriving (Eq, Ord, Data, Show, Read)++displaySameSite :: SameSite -> String+displaySameSite ss =+ case ss of+ SameSiteLax -> "SameSite=Lax"+ SameSiteStrict -> "SameSite=Strict"+ SameSiteNone -> "SameSite=None"+ SameSiteNoValue -> ""++-- convert 'CookieLife' to the argument needed for calling 'mkCookieHeader'+calcLife :: CookieLife -> IO (Maybe (Int, 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 "/", secure == False, httpOnly == False and+-- sameSite == SameSiteNoValue and partitioned = False+--+-- see also: 'addCookie'+mkCookie :: String -- ^ cookie name+ -> String -- ^ cookie value+ -> Cookie+mkCookie key val = Cookie "1" "/" "" key val False False SameSiteNoValue 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 (Int, UTCTime) -> Cookie -> String+mkCookieHeader mLife cookie =+ let+ l =+ [ (,) "Domain=" (cookieDomain cookie)+ , (,) "Max-Age=" (maybe "" (show . max 0 . fst) mLife)+ , (,) "expires=" (maybe "" (formatTime' . snd) mLife)+ , (,) "Path=" (cookiePath cookie)+ , (,) "Version=" (s cookieVersion)+ ]+ formatTime' =+ formatTime defaultTimeLocale "%a, %d-%b-%Y %X GMT"+ encode =+ escapeURIString+ (\c -> c `elem` (['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "-_.~"))+ s f | f cookie == "" = ""+ | otherwise = '\"' : (encode $ f cookie) ++ "\""+ in+ concat $ intersperse ";" $+ (cookieName cookie++"="++s cookieValue):[ (k++v) | (k,v) <- l, "" /= v ]+ ++ (if secure cookie then ["Secure"] else [])+ ++ (if httpOnly cookie then ["HttpOnly"] else [])+ ++ (if sameSite cookie /= SameSiteNoValue+ then [displaySameSite . sameSite $ cookie] else [])+ ++ (if partitioned cookie then ["Partitioned"] else [])++-- | 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 False SameSiteNoValue False+ cookie_version = cookie_special "$Version"+ cookie_path = cookie_special "$Path"+ cookie_domain = cookie_special "$Domain"+ cookie_special s = do+ void $ string s+ cookieEq+ value+ cookieSep = ws >> oneOf ",;" >> ws+ cookieEq = ws >> char '=' >> ws+ ws = spaces+ value = word+ word = try quoted_string <|> try incomp_token <|> return ""++ -- Parsers based on RFC 2068+ quoted_string = do+ void $ char '"'+ r <-many ((try quotedPair) <|> (oneOf qdtext))+ void $ 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 \\ "\""+ quotedPair = char '\\' >> anyChar++-- | 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 :: MonadFail 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 :: MonadFail 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+ [] -> Left "No cookie found"+ f -> return $ head f++low :: String -> String+low = map toLower
+ src/Happstack/Server/Internal/Handler.hs view
@@ -0,0 +1,404 @@+{-# LANGUAGE ScopedTypeVariables, ScopedTypeVariables, TupleSections #-}++module Happstack.Server.Internal.Handler+ ( request+ , parseResponse+ , putRequest+ ) where++import qualified Paths_happstack_server as Paths+import qualified Data.Version as DV+import Control.Applicative (pure)+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 Data.Time (UTCTime)+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.SURI(SURI(..),path,query)+import Happstack.Server.SURI.ParseURI+import Happstack.Server.Internal.TimeoutIO (TimeoutIO(..))+import Happstack.Server.Internal.Monads (failResponse)+import qualified Happstack.Server.Internal.TimeoutManager as TM+import Numeric+import System.Directory (removeFile)+import System.IO+import System.IO.Error (isDoesNotExistError)++request :: TimeoutIO -> Maybe (LogAccess UTCTime) -> Host -> (Request -> IO Response) -> IO ()+request timeoutIO mlog host handler =+ rloop timeoutIO mlog host handler =<< toGetContents timeoutIO++required :: String -> Maybe a -> Either String a+required err Nothing = Left err+required _ (Just a) = Right a++rloop :: TimeoutIO+ -> Maybe (LogAccess UTCTime)+ -> Host+ -> (Request -> IO Response)+ -> L.ByteString+ -> IO ()+rloop timeoutIO mlog 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' <- case parseHeaders "host" (L.unpack headerStr) of+ Nothing -> Left "failed to parse host header"+ Just x -> Right x+ let headers = mkHeaders headers'+ let contentLen = fromMaybe 0 $ fmap fst (P.readInt =<< getHeaderUnsafe contentlengthC headers)+ (body, nextRequest) <- case () of+ () | contentLen < 0 -> Left "negative content-length"+ | isJust $ getHeaderBS transferEncodingC headers ->+ return $ consumeChunks restStr+ | otherwise -> return (L.splitAt (fromIntegral contentLen) restStr)+ let cookies = [ (cookieName c, c) | cl <- fromMaybe [] (fmap getCookies (getHeader "Cookie" headers)), c <- cl ] -- Ugle+ return (m, u, cookies, v, headers, body, nextRequest)++ case parseRequest of+ Left err -> error $ "failed to parse HTTP request: " ++ err+ Right (m, u, cookies, v, headers, body, nextRequest)+ -> pure $+ do bodyRef <- newMVar (Body body)+ bodyInputRef <- newEmptyMVar+ let req = Request (toSecure timeoutIO) 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, handlerKilled) <- ((, False) `liftM` ioseq (handler req))+ `E.catches` [ Handler $ \(e::EscapeHTTP) -> throwIO e -- need to handle this higher up+ , Handler $ \(e::E.SomeException) -> pure (failResponse (show e), fromException e == Just ThreadKilled)+ ]++ case mlog 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) (readDec' . 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 timeoutIO req res+ -- clean up tmp files+ cleanupTempFiles req+ -- do not continue if handler was killed+ when (not handlerKilled && continueHTTP req res) $+ rloop timeoutIO mlog host handler nextRequest) `E.catch` (escapeHttpHandler timeoutIO)++escapeHttpHandler :: TimeoutIO+ -> EscapeHTTP+ -> IO ()+escapeHttpHandler tio (EscapeHTTP f) = f tio++-- 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' <- case parseHeaders "host" (L.unpack headerStr) of+ Nothing -> Left "failed to parse host header"+ Just x -> Right x+ 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 = EXTENSION r+ 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)+ , (P.pack "PATCH", PATCH)+ ]++-- Result side++staticHeaders :: Headers+staticHeaders =+ foldr (uncurry setHeaderBS) (mkHeaders [])+ [ (serverC, happstackC) ]++-- 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 :: TimeoutIO -> Request -> Response -> IO ()+putAugmentedResult timeoutIO req res = do+ case res of+ -- standard bytestring response+ Response {} -> do+ let isChunked = rsfLength (rsFlags res) == TransferEncodingChunked && isHTTP1_1 req+ sendTop (if isChunked then Nothing else (Just (fromIntegral (L.length (rsBody res))))) isChunked+ when (rqMethod req /= HEAD)+ (let body = if isChunked+ then chunk (rsBody res)+ else rsBody res+ in toPutLazy timeoutIO 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 (toHandle timeoutIO)+ toSendFile timeoutIO infp off count++ where ph (HeaderPair k vs) = map (\v -> P.concat [k, fsepC, v, crlfC]) vs+ sendTop cl isChunked = do+ allHeaders <- augmentHeaders req res cl isChunked+ toPut timeoutIO $ 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 (toHandle timeoutIO)+ 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 isChunked = 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+ | isChunked -> [(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"++-- * ByteString 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"+happstackC :: B.ByteString+happstackC = 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, Show t, Eq 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,140 @@+{-# LANGUAGE BangPatterns, CPP, ScopedTypeVariables #-}+module Happstack.Server.Internal.Listen(listen, listen',listenOn,listenOnIPv4) where++import Data.Maybe (isNothing)+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, forceTimeoutAll)+import Happstack.Server.Internal.TimeoutSocket as TS+import qualified Control.Concurrent.Thread.Group as TG+import Control.Exception.Extensible as E+import Control.Concurrent (forkIO, killThread, myThreadId)+import Control.Monad+import qualified Data.Maybe as Maybe+import qualified Network.Socket as Socket+import System.IO.Error (isFullError)+import Foreign.C (CInt)+{-+#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"++-- Meant to be TCP in practise.+-- See https://www.gnu.org/software/libc/manual/html_node/Creating-a-Socket.html+-- which says "zero is usually right". It could theoretically be SCTP, but it+-- would be a bizarre system that defaults to SCTP over TCP.+proto :: CInt+proto = Socket.defaultProtocol++{-+ 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.Socket+listenOn portm = do+ E.bracketOnError+ (Socket.socket Socket.AF_INET Socket.Stream proto)+ (Socket.close)+ (\sock -> do+ Socket.setSocketOption sock Socket.ReuseAddr 1+ Socket.bind sock (Socket.SockAddrInet (fromIntegral portm) iNADDR_ANY)+ Socket.listen sock (max 1024 Socket.maxListenQueue)+ return sock+ )++listenOnIPv4 :: String -- ^ IP address to listen on (must be an IP address not a host name)+ -> Int -- ^ port number to listen on+ -> IO Socket.Socket+listenOnIPv4 ip portm = do+ hostAddr <- inet_addr ip+ E.bracketOnError+ (Socket.socket Socket.AF_INET Socket.Stream proto)+ (Socket.close)+ (\sock -> do+ Socket.setSocketOption sock Socket.ReuseAddr 1+ Socket.bind sock (Socket.SockAddrInet (fromIntegral portm) hostAddr)+ Socket.listen sock (max 1024 Socket.maxListenQueue)+ return sock+ )++inet_addr :: String -> IO Socket.HostAddress+inet_addr ip = do+ addrInfos <- Socket.getAddrInfo (Just Socket.defaultHints) (Just ip) Nothing+ let getHostAddress addrInfo = case Socket.addrAddress addrInfo of+ Socket.SockAddrInet _ hostAddress -> Just hostAddress+ _ -> Nothing+ maybe (fail "inet_addr: no HostAddress") pure+ . Maybe.listToMaybe+ $ Maybe.mapMaybe getHostAddress addrInfos++iNADDR_ANY :: Socket.HostAddress+iNADDR_ANY = 0++-- | Bind and listen port+listen :: Conf -> (Request -> IO Response) -> IO ()+listen conf hand = do+ let port' = port conf+ lsocket <- listenOn port'+ Socket.setSocketOption lsocket Socket.KeepAlive 1+ listen' lsocket conf hand++-- | Use a previously bind port and listen+listen' :: Socket.Socket -> Conf -> (Request -> IO Response) -> IO ()+listen' s conf hand = do+{-+#ifndef mingw32_HOST_OS+-}+ void $ installHandler openEndedPipe Ignore Nothing+{-+#endif+-}+ let port' = port conf+ fork = case threadGroup conf of+ Nothing -> forkIO+ Just tg -> \m -> fst `liftM` TG.forkIO tg m+ tm <- initialize ((timeout conf) * (10^(6 :: Int)))+ -- http:// loop+ log' NOTICE ("Listening for http:// on port " ++ show port')+ 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)+ let timeoutIO = TS.timeoutSocketIO thandle sock+ request timeoutIO (logAccess conf) (hn,fromIntegral p) hand `E.catch` eh+ -- remove thread from timeout table+ cancel thandle+ Socket.close sock+ loop = forever $ do w <- acceptLite s+ fork $ work w+ pe e = log' ERROR ("ERROR in http accept thread: " ++ show e)+ infi :: IO ()+ infi = loop `catchSome` pe >> infi++ infi `finally` (Socket.close s >> when (isNothing $ threadGroup conf) (forceTimeoutAll tm))++{--+#ifndef mingw32_HOST_OS+-}+ void $ installHandler openEndedPipe Ignore Nothing+{-+#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/LogFormat.hs view
@@ -0,0 +1,57 @@+{-# LANGUAGE CPP #-}+module Happstack.Server.Internal.LogFormat+ ( formatTimeCombined+ , formatRequestCombined+ ) where++import Data.Time.Format (FormatTime(..), formatTime, defaultTimeLocale)++-- | Format the time as describe in the Apache combined log format.+-- http://httpd.apache.org/docs/2.2/logs.html#combined+--+-- The format is:+-- [day/month/year:hour:minute:second zone]+-- day = 2*digit+-- month = 3*letter+-- year = 4*digit+-- hour = 2*digit+-- minute = 2*digit+-- second = 2*digit+-- zone = (`+' | `-') 4*digit+formatTimeCombined :: FormatTime t => t -> String+formatTimeCombined = formatTime defaultTimeLocale "%d/%b/%Y:%H:%M:%S %z"++-- | Format the request as describe in the Apache combined log format.+-- http://httpd.apache.org/docs/2.2/logs.html#combined+--+-- The format is: "%h - %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-agent}i\""+-- %h: This is the IP address of the client (remote host) which made the request to the server.+-- %u: This is the userid of the person requesting the document as determined by HTTP authentication.+-- %t: The time that the request was received.+-- %r: The request line from the client is given in double quotes.+-- %>s: This is the status code that the server sends back to the client.+-- %b: The last part indicates the size of the object returned to the client, not including the response headers.+-- %{Referer}: The "Referer" (sic) HTTP request header.+-- %{User-agent}: The User-Agent HTTP request header.+formatRequestCombined :: FormatTime t =>+ String+ -> String+ -> t+ -> String+ -> Int+ -> Integer+ -> String+ -> String+ -> String+formatRequestCombined host user time requestLine responseCode size referer userAgent =+ unwords+ [ host+ , user+ , "[" ++ formattedTime ++ "]"+ , show requestLine+ , show responseCode+ , show size+ , show referer+ , show userAgent+ ]+ where formattedTime = formatTimeCombined time
+ 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,153 @@+{-# LANGUAGE FlexibleInstances #-}++module Happstack.Server.Internal.MessageWrap (+ module Happstack.Server.Internal.MessageWrap+ ,defaultInputIter+ ) where++import Control.Concurrent.MVar (tryTakeMVar, tryPutMVar, putMVar)+import Control.Monad.Trans (MonadIO(liftIO))+import qualified Data.ByteString.Char8 as P+import qualified Data.ByteString.Lazy.Char8 as L+import qualified Data.ByteString.UTF8 as U (toString)+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++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 | (not (canHaveBody (rqMethod req))) || (not (isDecodable ctype)) =+ do _ <- liftIO $ tryPutMVar (rqInputsBody req) []+ return ([], Nothing)+ where+ ctype :: Maybe ContentType+ ctype = parseContentType . P.unpack =<< getHeader "content-type" req+ 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++bodyInput bodyPolicy req =+ liftIO $+ do let ctype = parseContentType . P.unpack =<< getHeader "content-type" req+ 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++-- | 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.unEscapeQS name,simpleInput $ SURI.unEscapeQS val):rest+ where (pairString,qString')= split (=='&') qString+ (name,val)=split (=='=') pairString+ rest=if null qString' then [] else formDecode qString'++-- | Decodes application\/x-www-form-urlencoded inputs.+-- TODO: should any of the [] be error conditions?+formDecodeBS :: L.ByteString -> [(String, Input)]+formDecodeBS qString | L.null qString = []+formDecodeBS qString =+ if L.null pairString+ then rest -- skip in case of consecutive ampersands "...&&..."+ else (SURI.unEscapeQS (L.unpack name), simpleInput $ SURI.unEscapeQS (L.unpack $ L.drop 1 val)) : rest+ where (pairString,qString') = L.break (== '&') qString+ (name,val) = L.break (== '=') pairString+ rest = formDecodeBS (L.drop 1 qString')++-- 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 decodedUrlEncodedForm+ 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 decodedUrlEncodedForm+ where+ (upToMaxRAM,overMaxRAM) = L.splitAt (maxRAM bp) inp+ decodedUrlEncodedForm = (formDecodeBS upToMaxRAM,+ if L.null overMaxRAM+ then Nothing+ else Just ("x-www-form-urlencoded content longer than BodyPolicy.maxRAM=" ++ show (maxRAM bp) ++ " bytes"))++-- | 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 worker ps inp =+ case lookup "boundary" ps of+ Just b -> multipartBody worker (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 (U.toString . P.pack . SURI.unEscape) . splitList '/'++-- | Repeadly splits a list by the provided separator and collects the results+splitList :: Eq a => a -> [a] -> [[a]]+splitList _ [] = []+splitList sep list = h:splitList sep t+ where (h,t)=split (==sep) list++-- | Repeatedly splits a list and collects the results+splitListBy :: (a -> Bool) -> [a] -> [[a]]+splitListBy _ [] = []+splitListBy f list = h:splitListBy f t+ where (h,t)=split f list++-- | Split is like break, but the matching element is dropped.+split :: (a -> Bool) -> [a] -> ([a], [a])+split f s = (left,right)+ where+ (left,right')=break f s+ right = if null right' then [] else tail right'+
+ src/Happstack/Server/Internal/Monads.hs view
@@ -0,0 +1,800 @@+{-# LANGUAGE CPP, FlexibleContexts, FlexibleInstances, FunctionalDependencies, GeneralizedNewtypeDeriving, MultiParamTypeClasses, RankNTypes, TypeFamilies, 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.Concurrent (newMVar)+import Control.Exception (throwIO)++import Control.Monad ( MonadPlus(mzero, mplus), ap, liftM, msum+ )+import Control.Monad.Base ( MonadBase, liftBase )+import Control.Monad.Catch ( MonadCatch(..), MonadThrow(..) )+#if !MIN_VERSION_transformers(0,6,0)+import Control.Monad.Trans.Error ( ErrorT, Error, mapErrorT )+#endif+import Control.Monad.Except ( MonadError, throwError+ , catchError+ )+#if MIN_VERSION_base(4,9,0)+import Control.Monad.Fail (MonadFail)+import qualified Control.Monad.Fail as Fail+#endif+import Control.Monad.Reader ( ReaderT(ReaderT), runReaderT+ , MonadReader, ask, local, mapReaderT+ )+import qualified Control.Monad.RWS.Lazy as Lazy ( RWST, mapRWST )+import qualified Control.Monad.RWS.Strict as Strict ( RWST, mapRWST )++import Control.Monad.Trans.Except ( ExceptT(ExceptT), mapExceptT, runExceptT )+import Control.Monad.State.Class ( MonadState, get, put )+import qualified Control.Monad.State.Lazy as Lazy ( StateT, mapStateT )+import qualified Control.Monad.State.Strict as Strict ( StateT, mapStateT )+import Control.Monad.Trans ( MonadTrans, lift+ , MonadIO, liftIO+ )+import Control.Monad.Trans.Control ( MonadTransControl(..)+ , MonadBaseControl(..)+ , ComposeSt, defaultLiftBaseWith, defaultRestoreM+ )+import Control.Monad.Writer.Class ( MonadWriter, tell, pass, listens )+import qualified Control.Monad.Writer.Lazy as Lazy ( WriterT(WriterT), runWriterT, mapWriterT )+import qualified Control.Monad.Writer.Strict as Strict ( WriterT, mapWriterT )+import qualified Control.Monad.Writer.Class as Writer ( listen )++import Control.Monad.Trans.Maybe (MaybeT(MaybeT), runMaybeT)+import qualified Data.ByteString.Char8 as P+import qualified Data.ByteString.Lazy.UTF8 as LU (fromString)+import Data.Char (ord)+import Data.List (inits, isPrefixOf, stripPrefix, tails)+import Data.Maybe (fromMaybe)+import Data.Monoid (Monoid(mempty, mappend), Dual(..), Endo(..))+import qualified Data.Semigroup as SG+import qualified Paths_happstack_server as Cabal+import qualified Data.Version as DV+import Debug.Trace (trace)++import Happstack.Server.Internal.Cookie (Cookie)+import Happstack.Server.Internal.RFC822Headers (parseContentType)+import Happstack.Server.Internal.Types (EscapeHTTP(..), canHaveBody)+import Happstack.Server.Internal.TimeoutIO (TimeoutIO)+import Happstack.Server.Types+import Prelude (Bool(..), Either(..), Eq(..), Functor(..), IO, Monad(..), Char, Maybe(..), String, Show(..), ($), (.), (>), (++), (&&), (||), (=<<), const, concatMap, flip, id, otherwise, zip)++-- | 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 }+#if MIN_VERSION_base(4,9,0)+ deriving (Monad, MonadFail, MonadPlus, Functor)+#else+ deriving (Monad, MonadPlus, Functor)+#endif++instance MonadCatch m => MonadCatch (ServerPartT m) where+ catch action handle = ServerPartT $ catch (unServerPartT action) (unServerPartT . handle)++instance MonadThrow m => MonadThrow (ServerPartT m) where+ throwM = ServerPartT . throwM++instance MonadBase b m => MonadBase b (ServerPartT m) where+ liftBase = lift . liftBase++instance (MonadIO m) => MonadIO (ServerPartT m) where+ liftIO = ServerPartT . liftIO+ {-# INLINE liftIO #-}++instance MonadTransControl ServerPartT where+ type StT ServerPartT a = StT WebT (StT (ReaderT Request) a)+ liftWith f = ServerPartT $ liftWith $ \runReader ->+ liftWith $ \runWeb ->+ f $ runWeb . runReader . unServerPartT+ restoreT = ServerPartT . restoreT . restoreT++instance MonadBaseControl b m => MonadBaseControl b (ServerPartT m) where+ type StM (ServerPartT m) a = ComposeSt ServerPartT m a+ liftBaseWith = defaultLiftBaseWith+ restoreM = defaultRestoreM++-- | 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, MonadPlus m) => SG.Semigroup (ServerPartT m a) where+ (<>) = mplus++instance (Monad m, MonadPlus m) => Monoid (ServerPartT m a) where+ mempty = mzero+ mappend = (SG.<>)++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)++-- | Implementation of 'askRqEnv' for arbitrary 'ServerMonad'.+smAskRqEnv :: (ServerMonad m, MonadIO m) => m ([(String, Input)], Maybe [(String, Input)], [(String, Cookie)])+smAskRqEnv = do+ rq <- askRq+ mbi <- liftIO $ if (canHaveBody (rqMethod rq)) && (isDecodable (ctype rq))+ then readInputsBody rq+ else return (Just [])+ return (rqInputsQuery rq, mbi, rqCookies rq)+ where+ ctype :: Request -> Maybe ContentType+ ctype req = parseContentType . P.unpack =<< getHeader "content-type" req++ 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++-- | Implementation of 'localRqEnv' for arbitrary 'ServerMonad'.+smLocalRqEnv :: (ServerMonad m, MonadIO m) => (([(String, Input)], Maybe [(String, Input)], [(String, Cookie)]) -> ([(String, Input)], Maybe [(String, Input)], [(String, Cookie)])) -> m b -> m b+smLocalRqEnv f m = do+ rq <- askRq+ b <- liftIO $ readInputsBody rq+ let (q', b', c') = f (rqInputsQuery rq, b, rqCookies rq)+ bv <- liftIO $ newMVar (fromMaybe [] b')+ let rq' = rq { rqInputsQuery = q'+ , rqInputsBody = bv+ , rqCookies = c'+ }+ localRq (const rq') m++-------------------------------+-- 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 => SG.Semigroup (SetAppend a) where+ Set x <> Append y = Set (x `mappend` y)+ Append x <> Append y = Append (x `mappend` y)+ _ <> Set y = Set y++instance Monoid a => Monoid (SetAppend a) where+ mempty = Append mempty+ mappend = (SG.<>)++-- | 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 :: Lazy.WriterT (FilterFun a) m b }+ deriving (Functor, Applicative, Monad, MonadTrans)++instance MonadCatch m => MonadCatch (FilterT a m) where+ catch action handle = FilterT $ catch (unFilterT action) (unFilterT . handle)++instance MonadThrow m => MonadThrow (FilterT a m) where+ throwM = FilterT . throwM++instance MonadBase b m => MonadBase b (FilterT a m) where+ liftBase = lift . liftBase++instance (MonadIO m) => MonadIO (FilterT a m) where+ liftIO = FilterT . liftIO+ {-# INLINE liftIO #-}++instance MonadTransControl (FilterT a) where+ type StT (FilterT a) b = StT (Lazy.WriterT (FilterFun a)) b+ liftWith f = FilterT $ liftWith $ \run -> f $ run . unFilterT+ restoreT = FilterT . restoreT++instance MonadBaseControl b m => MonadBaseControl b (FilterT a m) where+ type StM (FilterT a m) c = ComposeSt (FilterT a) m c+ liftBaseWith = defaultLiftBaseWith+ restoreM = defaultRestoreM++-- | 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 :: ExceptT Response (FilterT (Response) (MaybeT m)) a }+ deriving (Functor)++instance MonadCatch m => MonadCatch (WebT m) where+ catch action handle = WebT $ catch (unWebT action) (unWebT . handle)++instance MonadThrow m => MonadThrow (WebT m) where+ throwM = WebT . throwM++instance MonadBase b m => MonadBase b (WebT m) where+ liftBase = lift . liftBase++instance (MonadIO m) => MonadIO (WebT m) where+ liftIO = WebT . liftIO+ {-# INLINE liftIO #-}++instance MonadTransControl WebT where+ type StT WebT a = StT MaybeT+ (StT (FilterT Response)+ (StT (ExceptT Response) a))+ liftWith f = WebT $ liftWith $ \runError ->+ liftWith $ \runFilter ->+ liftWith $ \runMaybe ->+ f $ runMaybe .+ runFilter .+ runExceptT . unWebT+ restoreT = WebT . restoreT . restoreT . restoreT++instance MonadBaseControl b m => MonadBaseControl b (WebT m) where+ type StM (WebT m) a = ComposeSt WebT m a+ liftBaseWith = defaultLiftBaseWith+ restoreM = defaultRestoreM++-- | '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 #-}++#if MIN_VERSION_base(4,9,0)+instance MonadFail m => MonadFail (WebT m) where+#endif++ fail s = lift (Fail.fail 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 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 $ ExceptT $ FilterT $ (lower x) `mplus` (lower y)+ where lower = (unFilterT . runExceptT . unWebT)++instance (Monad m) => FilterMonad Response (WebT m) where+ setFilter f = WebT $ lift $ setFilter $ f+ composeFilter f = WebT . lift . composeFilter $ f+ getFilter m = WebT $ ExceptT $ liftM lft $ getFilter (runExceptT $ unWebT m)+ where+ lft (Left r, _) = Left r+ lft (Right a, f) = Right (a, f)++instance (Monad m, MonadPlus m) => SG.Semigroup (WebT m a) where+ (<>) = mplus++instance (Monad m, MonadPlus m) => Monoid (WebT m a) where+ mempty = mzero+ mappend = (SG.<>)++-- | 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 . Lazy.runWriterT . unFilterT . runExceptT . unWebT++-- | For wrapping a 'WebT' back up. @'mkWebT' . 'ununWebT' = 'id'@+mkWebT :: UnWebT m a -> WebT m a+mkWebT = WebT . ExceptT . FilterT . Lazy.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, MonadPlus 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+ finishWith (failResponse s)++failResponse :: String -> Response+failResponse s =+ setHeader "Content-Type" "text/html; charset=UTF-8" $+ resultBS 500 (LU.fromString (failHtml s))++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]++------------------------------------------------------------------------------+-- ServerMonad, FilterMonad, and WebMonad instances for ReaderT, StateT,+-- WriterT, RWST, and ErrorT+------------------------------------------------------------------------------++-- ReaderT++instance (ServerMonad m) => ServerMonad (ReaderT r m) where+ askRq = lift askRq+ localRq f = mapReaderT (localRq f)++instance (FilterMonad res m) => FilterMonad res (ReaderT r m) where+ setFilter f = lift $ setFilter f+ composeFilter = lift . composeFilter+ getFilter = mapReaderT getFilter++instance (WebMonad a m) => WebMonad a (ReaderT r m) where+ finishWith = lift . finishWith++-- StateT++instance (ServerMonad m) => ServerMonad (Lazy.StateT s m) where+ askRq = lift askRq+ localRq f = Lazy.mapStateT (localRq f)++instance (ServerMonad m) => ServerMonad (Strict.StateT s m) where+ askRq = lift askRq+ localRq f = Strict.mapStateT (localRq f)++instance (FilterMonad res m) => FilterMonad res (Lazy.StateT s m) where+ setFilter f = lift $ setFilter f+ composeFilter = lift . composeFilter+ getFilter m = Lazy.mapStateT (\m' ->+ do ((b,s), f) <- getFilter m'+ return ((b, f), s)) m++instance (FilterMonad res m) => FilterMonad res (Strict.StateT s m) where+ setFilter f = lift $ setFilter f+ composeFilter = lift . composeFilter+ getFilter m = Strict.mapStateT (\m' ->+ do ((b,s), f) <- getFilter m'+ return ((b, f), s)) m++instance (WebMonad a m) => WebMonad a (Lazy.StateT s m) where+ finishWith = lift . finishWith++instance (WebMonad a m) => WebMonad a (Strict.StateT s m) where+ finishWith = lift . finishWith++-- WriterT++instance (ServerMonad m, Monoid w) => ServerMonad (Lazy.WriterT w m) where+ askRq = lift askRq+ localRq f = Lazy.mapWriterT (localRq f)++instance (ServerMonad m, Monoid w) => ServerMonad (Strict.WriterT w m) where+ askRq = lift askRq+ localRq f = Strict.mapWriterT (localRq f)++instance (FilterMonad res m, Monoid w) => FilterMonad res (Lazy.WriterT w m) where+ setFilter f = lift $ setFilter f+ composeFilter = lift . composeFilter+ getFilter m = Lazy.mapWriterT (\m' ->+ do ((b,w), f) <- getFilter m'+ return ((b, f), w)) m++instance (FilterMonad res m, Monoid w) => FilterMonad res (Strict.WriterT w m) where+ setFilter f = lift $ setFilter f+ composeFilter = lift . composeFilter+ getFilter m = Strict.mapWriterT (\m' ->+ do ((b,w), f) <- getFilter m'+ return ((b, f), w)) m++instance (WebMonad a m, Monoid w) => WebMonad a (Lazy.WriterT w m) where+ finishWith = lift . finishWith++instance (WebMonad a m, Monoid w) => WebMonad a (Strict.WriterT w m) where+ finishWith = lift . finishWith++-- RWST++instance (ServerMonad m, Monoid w) => ServerMonad (Lazy.RWST r w s m) where+ askRq = lift askRq+ localRq f = Lazy.mapRWST (localRq f)++instance (ServerMonad m, Monoid w) => ServerMonad (Strict.RWST r w s m) where+ askRq = lift askRq+ localRq f = Strict.mapRWST (localRq f)++instance (FilterMonad res m, Monoid w) => FilterMonad res (Lazy.RWST r w s m) where+ setFilter f = lift $ setFilter f+ composeFilter = lift . composeFilter+ getFilter m = Lazy.mapRWST (\m' ->+ do ((b,s,w), f) <- getFilter m'+ return ((b, f), s, w)) m++instance (FilterMonad res m, Monoid w) => FilterMonad res (Strict.RWST r w s m) where+ setFilter f = lift $ setFilter f+ composeFilter = lift . composeFilter+ getFilter m = Strict.mapRWST (\m' ->+ do ((b,s,w), f) <- getFilter m'+ return ((b, f), s, w)) m++instance (WebMonad a m, Monoid w) => WebMonad a (Lazy.RWST r w s m) where+ finishWith = lift . finishWith++instance (WebMonad a m, Monoid w) => WebMonad a (Strict.RWST r w s m) where+ finishWith = lift . finishWith++-- ErrorT++#if !MIN_VERSION_transformers(0,6,0)+instance (Error e, ServerMonad m) => ServerMonad (ErrorT e m) where+ askRq = lift askRq+ localRq f = mapErrorT $ localRq f++instance (Error e, FilterMonad a m) => FilterMonad a (ErrorT e m) where+ setFilter f = lift $ setFilter f+ composeFilter = lift . composeFilter+ getFilter m = mapErrorT (\m' ->+ do (eb, f) <- getFilter m'+ case eb of+ (Left e) -> return (Left e)+ (Right b) -> return $ Right (b, f)+ ) m++instance (Error e, WebMonad a m) => WebMonad a (ErrorT e m) where+ finishWith = lift . finishWith+#endif++-- ExceptT++instance ServerMonad m => ServerMonad (ExceptT e m) where+ askRq = lift askRq+ localRq f = mapExceptT $ localRq f++instance (FilterMonad a m) => FilterMonad a (ExceptT e m) where+ setFilter f = lift $ setFilter f+ composeFilter = lift . composeFilter+ getFilter m = mapExceptT (\m' ->+ do (eb, f) <- getFilter m'+ case eb of+ (Left e) -> return (Left e)+ (Right b) -> return $ Right (b, f)+ ) m++instance WebMonad a m => WebMonad a (ExceptT e m) where+ finishWith = lift . finishWith++escapeHTTP :: (ServerMonad m, MonadIO m) =>+ (TimeoutIO -> IO ())+ -> m a+escapeHTTP h = liftIO (throwIO (EscapeHTTP h))
+ src/Happstack/Server/Internal/Multipart.hs view
@@ -0,0 +1,281 @@+module Happstack.Server.Internal.Multipart where++import Control.Monad (MonadPlus(mplus))+import Data.ByteString.Base64.Lazy+import qualified Data.ByteString.Lazy.Char8 as L+import Data.ByteString.Lazy.Internal (ByteString(Chunk, Empty))+import qualified Data.ByteString.Lazy.UTF8 as LU+import qualified Data.ByteString.Char8 as S+import Data.Maybe (fromMaybe)+import Data.Int (Int64)+import Text.ParserCombinators.Parsec (parse)+import Happstack.Server.Internal.Types (Input(..))+import Happstack.Server.Internal.RFC822Headers+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' _ 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' _ 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 :: FilePath -> Int64 -> FilePath -> ByteString -> IO (Bool, Int64, FilePath)+defaultFileSaver tmpDir diskQuota filename b+ | pathSeparator filename = error ("Filename contains path separators: " ++ show filename)+ | otherwise =+ do (fn, h) <- openBinaryTempFile tmpDir filename+ (trunc, len) <- hPutLimit diskQuota h b+ hClose h+ return (trunc, len, fn)+ where+ pathSeparator :: String -> Bool+ pathSeparator template = any (\x-> x == '/' || x == '\\') template++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) -> do+ let eb' = case getContentTransferEncoding hs of+ Nothing -> Right b+ Just (ContentTransferEncoding "7bit") ->+ -- We don't bother checking that the data+ -- really is 7bit-only+ Right b+ Just (ContentTransferEncoding "8bit") ->+ Right b+ Just (ContentTransferEncoding "binary") ->+ Right b+ Just (ContentTransferEncoding "base64") ->+ Right $ decodeLenient b+ -- TODO: Support quoted-printable+ Just cte ->+ Left ("Bad content-transfer-encoding: " ++ show cte)+ case eb' of+ Right b' ->+ cont (BodyWork ctype ps b')+ Left err ->+ return $ Failed Nothing err+ cd -> return $ Failed Nothing ("Expected content-disposition: form-data but got " ++ show cd)+ (BodyResult {}) -> return $ Failed Nothing "bodyPartToInput: Got unexpected BodyResult."++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) | (L.pack "\r\n--" `L.append` boundary `L.append` (L.pack "--"))+ `L.isPrefixOf` y -> (x, L.empty)+ | otherwise -> (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,272 @@+-- #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 Control.Monad+import Control.Monad.Fail (MonadFail)+import Data.Char+import Data.List+import Text.ParserCombinators.Parsec++type Header = (String, String)++pHeaders :: Parser [Header]+pHeaders = many pHeader++parseHeaders :: MonadFail m => SourceName -> String -> m [Header]+parseHeaders = parseM pHeaders++pHeader :: Parser Header+pHeader =+ do name <- many1 headerNameChar+ void $ char ':'+ void $ many ws1+ line <- lineString+ void crLf+ extraLines <- many extraFieldLine+ return (map toLower name, concat (line:extraLines))++extraFieldLine :: Parser String+extraFieldLine =+ do sp <- ws1+ line <- lineString+ void $ 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 void $ lexeme $ char ';'+ p_name <- lexeme $ p_token+ void $ 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 choice [ try ((lookAhead $ do+ void (literalString >>+ p_parameter))+ >> literalString)+ , 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 void $ many ws1+ c_type <- p_token+ void $ 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 :: MonadFail m => String -> m ContentType+parseContentType = parseM pContentType "Content-type"++getContentType :: MonadFail 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 void $ many ws1+ c_cte <- p_token+ return $ ContentTransferEncoding (map toLower c_cte)++parseContentTransferEncoding :: MonadFail m => String -> m ContentTransferEncoding+parseContentTransferEncoding =+ parseM pContentTransferEncoding "Content-transfer-encoding"++getContentTransferEncoding :: MonadFail 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 void $ many ws1+ c_cd <- p_token+ c_parameters <- many p_parameter+ return $ ContentDisposition (map toLower c_cd) c_parameters++parseContentDisposition :: MonadFail m => String -> m ContentDisposition+parseContentDisposition = parseM pContentDisposition "Content-disposition"++getContentDisposition :: MonadFail m => [Header] -> m ContentDisposition+getContentDisposition hs =+ lookupM "content-disposition" hs >>= parseContentDisposition++--+-- * Utilities+--++parseM :: MonadFail 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 :: (MonadFail 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; void $ 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 void $ char '\"'+ str <- many (noneOf "\"\\" <|> quoted_pair)+ void $ 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 void $ char '\"'+ str <- manyTill anyChar (try lastQuote)+ return str+ where lastQuote = do void $ char '\"'+ notFollowedBy (try (many (noneOf "\"") >> char '\"'))++headerNameChar :: Parser Char+headerNameChar = noneOf "\n\r:"++tspecials, tokenchar :: [Char]+tspecials = "()<>@,;:\\\"/[]?=" -- RFC2045+tokenchar = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" \\ tspecials++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 void $ char '\\'+ p_text
+ src/Happstack/Server/Internal/Socket.hs view
@@ -0,0 +1,55 @@+{-# LANGUAGE CPP #-}+module Happstack.Server.Internal.Socket+ ( acceptLite+ , sockAddrToPeer+ ) where++import Data.List (intersperse)+import Data.Word (Word32)+import qualified Network.Socket as S+ ( Socket+ , PortNumber()+ , SockAddr(..)+ , HostName+ , accept+ )+import Numeric (showHex)++type HostAddress = Word32+type HostAddress6 = (Word32, Word32, Word32, Word32)++-- | Converts a HostAddress to a String in dot-decimal notation+showHostAddress :: HostAddress -> String+showHostAddress num = concat [show q1, ".", show q2, ".", show q3, ".", show q4]+ where (num',q1) = num `quotRem` 256+ (num'',q2) = num' `quotRem` 256+ (num''',q3) = num'' `quotRem` 256+ (_,q4) = num''' `quotRem` 256++-- | Converts a IPv6 HostAddress6 to standard hex notation+showHostAddress6 :: HostAddress6 -> String+showHostAddress6 (a,b,c,d) =+ (concat . intersperse ":" . map (flip showHex ""))+ [p1,p2,p3,p4,p5,p6,p7,p8]+ where (a',p2) = a `quotRem` 65536+ (_,p1) = a' `quotRem` 65536+ (b',p4) = b `quotRem` 65536+ (_,p3) = b' `quotRem` 65536+ (c',p6) = c `quotRem` 65536+ (_,p5) = c' `quotRem` 65536+ (d',p8) = d `quotRem` 65536+ (_,p7) = d' `quotRem` 65536++-- | 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+ let (peer, port) = sockAddrToPeer addr+ return (sock', peer, port)++sockAddrToPeer :: S.SockAddr -> (S.HostName, S.PortNumber)+sockAddrToPeer addr =+ case addr of+ (S.SockAddrInet p ha) -> (showHostAddress ha, p)+ (S.SockAddrInet6 p _ ha _) -> (showHostAddress6 ha, p)+ _ -> error "sockAddrToPeer: Unsupported socket type"
+ src/Happstack/Server/Internal/TimeoutIO.hs view
@@ -0,0 +1,23 @@+module Happstack.Server.Internal.TimeoutIO+ ( TimeoutIO(..)+ ) where++import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Lazy.Char8 as L+import Happstack.Server.Internal.TimeoutManager (Handle)+import Network.Socket.SendFile (ByteCount, Offset)+++-- |TimeoutIO is a record which abstracts out all the network IO+-- functions needed by the request handling loop. This allows use to+-- use the same event loop for handle both http:// and https://.+data TimeoutIO = TimeoutIO+ { toHandle :: Handle+ , toPutLazy :: L.ByteString -> IO ()+ , toPut :: B.ByteString -> IO ()+ , toGet :: IO (Maybe B.ByteString)+ , toGetContents :: IO L.ByteString+ , toSendFile :: FilePath -> Offset -> ByteCount -> IO ()+ , toShutdown :: IO ()+ , toSecure :: Bool+ }
+ src/Happstack/Server/Internal/TimeoutManager.hs view
@@ -0,0 +1,84 @@+module Happstack.Server.Internal.TimeoutManager+ ( Manager+ , Handle+ , initialize+ , register+ , registerKillThread+ , tickle+ , pause+ , resume+ , cancel+ , forceTimeout+ , forceTimeoutAll+ ) where++import qualified Data.IORef as I+import Control.Concurrent (forkIO, threadDelay, myThreadId, killThread)+import Control.Monad (forever)+import qualified Control.Exception as E++-- FIXME implement stopManager++-- | A timeout manager+newtype Manager = Manager (I.IORef [Handle])+data Handle = Handle (I.IORef (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+ action <- I.readIORef onTimeout+ action `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+ action <- I.newIORef onTimeout+ let h = Handle action iactive+ I.atomicModifyIORef ref (\x -> (h : x, ()))+ return h++registerKillThread :: Manager -> IO Handle+registerKillThread m = do+ tid <- myThreadId+ register m $ killThread tid++tickle, pause, resume, cancel :: Handle -> IO ()+tickle (Handle _ iactive) = I.writeIORef iactive $! Active+pause (Handle _ iactive) = I.writeIORef iactive $! Paused+resume = tickle+cancel (Handle action iactive) =+ do I.writeIORef iactive $! Canceled+ I.writeIORef action $! (return ())++forceTimeout :: Handle -> IO ()+forceTimeout (Handle action iactive) =+ do I.writeIORef iactive $! Canceled+ io <- I.atomicModifyIORef action (\io -> (return (), io))+ io `E.catch` ignoreAll++-- | terminate all threads immediately+forceTimeoutAll :: Manager -> IO ()+forceTimeoutAll (Manager ref) =+ do hs <- I.atomicModifyIORef ref (\hs -> ([], hs))+ mapM_ forceTimeout hs
+ src/Happstack/Server/Internal/TimeoutSocket.hs view
@@ -0,0 +1,98 @@+{-# LANGUAGE BangPatterns, ScopedTypeVariables #-}+{- |+-- borrowed from snap-server. Check there periodically for updates.+-}+module Happstack.Server.Internal.TimeoutSocket where++import Control.Applicative (pure)+import Control.Concurrent (threadWaitWrite)+import Control.Exception as E (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 Network.Socket (close)+import qualified Network.Socket.ByteString as N+import qualified Happstack.Server.Internal.TimeoutManager as TM+import Happstack.Server.Internal.TimeoutIO (TimeoutIO(..))+import Network.Socket (Socket, ShutdownCmd(..), shutdown)+import Network.Socket.SendFile (Iter(..), ByteCount, Offset, sendFileIterWith')+import Network.Socket.ByteString (sendAll)+import System.IO.Error (isDoesNotExistError, ioeGetErrorType)+import System.IO.Unsafe (unsafeInterleaveIO)+import GHC.IO.Exception (IOErrorType(InvalidArgument))++sPutLazyTickle :: TM.Handle -> Socket -> L.ByteString -> IO ()+sPutLazyTickle thandle sock cs =+ do L.foldrChunks (\c rest -> sendAll sock c >> TM.tickle thandle >> rest) (return ()) cs+{-# INLINE sPutLazyTickle #-}++sPutTickle :: TM.Handle -> Socket -> B.ByteString -> IO ()+sPutTickle thandle sock cs =+ do sendAll sock cs+ TM.tickle thandle+ return ()+{-# INLINE sPutTickle #-}++sGet :: TM.Handle+ -> Socket+ -> IO (Maybe B.ByteString)+sGet handle socket =+ do s <- N.recv socket 65536+ TM.tickle handle+ if S.null s+ then pure Nothing+ else pure (Just s)++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+ -- 'InvalidArgument' is GHCs code for eNOTCONN (among other+ -- things). Sometimes the other end of socket is closed first+ -- and this end is already disconnected before we do+ -- 'shutdown'. Ignore this exception.+ shutdown sock ShutdownReceive `E.catch`+ (\e -> when (not (isDoesNotExistError e || ioeGetErrorType e == InvalidArgument)) (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++timeoutSocketIO :: TM.Handle -> Socket -> TimeoutIO+timeoutSocketIO handle socket =+ TimeoutIO { toHandle = handle+ , toShutdown = close socket+ , toPutLazy = sPutLazyTickle handle socket+ , toGet = sGet handle socket+ , toPut = sPutTickle handle socket+ , toGetContents = sGetContents handle socket+ , toSendFile = sendFileTickle handle socket+ , toSecure = False+ }
+ src/Happstack/Server/Internal/Types.hs view
@@ -0,0 +1,536 @@+{-# LANGUAGE TypeSynonymInstances, DeriveDataTypeable, FlexibleInstances, RankNTypes, CPP #-}++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,+ LogAccess, logMAccess, Conf(..), nullConf, result, resultBS,+ redirect, -- redirect_, redirect', redirect'_,+ isHTTP1_0, isHTTP1_1,+ RsFlags(..), nullRsFlags, contentLength, chunked, noContentLength,+ HttpVersion(..), Length(..), Method(..), canHaveBody, Headers, continueHTTP,+ Host, ContentType(..),+ readDec', fromReadS, readM, FromReqURI(..),+ showRsValidator, EscapeHTTP(..)+ ) where+++import Control.Exception (Exception, SomeException)+#if !MIN_VERSION_transformers(0,6,0)+import Control.Monad.Trans.Error (Error(strMsg))+#endif+import Control.Monad.Fail (MonadFail)+import Control.Monad.Trans (MonadIO(liftIO))+import qualified Control.Concurrent.Thread.Group as TG+import Control.Concurrent.MVar+import qualified Data.Map as M+import Data.Data (Data)+import Data.String (fromString)+import Data.Time.Format (FormatTime(..))+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.Int (Int8, Int16, Int32, Int64)+import Data.Maybe+import Data.List+import Data.Word (Word, Word8, Word16, Word32, Word64)+import qualified Data.Text as Text+import qualified Data.Text.Lazy as Lazy+import Happstack.Server.SURI+import Data.Char (toLower)+import Happstack.Server.Internal.RFC822Headers ( ContentType(..) )+import Happstack.Server.Internal.Cookie+import Happstack.Server.Internal.LogFormat (formatRequestCombined)+import Happstack.Server.Internal.TimeoutIO (TimeoutIO)+import Numeric (readDec, readSigned)+import System.Log.Logger (Priority(..), logM)++-- | 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+--+-- In addition to this rule All 1xx (informational), 204 (no content),+-- and 304 (not modified) responses MUST NOT include a message-body+-- and therefore are eligible for connection keep-alive.+continueHTTP :: Request -> Response -> Bool+continueHTTP rq rs =+ (isHTTP1_0 rq && checkHeaderBS connectionC keepaliveC rq &&+ (rsfLength (rsFlags rs) == ContentLength || isNoMessageBodyResponse rs)) ||+ (isHTTP1_1 rq && not (checkHeaderBS connectionC closeC rq) &&+ (rsfLength (rsFlags rs) /= NoContentLength || isNoMessageBodyResponse rs))+ where+ isNoMessageBodyCode code = (code >= 100 && code <= 199) || code == 204 || code == 304+ isNoMessageBodyResponse rs' = isNoMessageBodyCode (rsCode rs') && L.null (rsBody rs')++-- | function to log access requests (see also: 'logMAccess')+-- type LogAccess time =+-- ( String -- ^ host+-- -> String -- ^ user+-- -> time -- ^ time+-- -> String -- ^ requestLine+-- -> Int -- ^ responseCode+-- -> Integer -- ^ size+-- -> String -- ^ referer+-- -> String -- ^ userAgent+-- -> IO ())+type LogAccess time =+ ( String+ -> String+ -> time+ -> String+ -> Int+ -> Integer+ -> String+ -> String+ -> IO ())++-- | 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 (LogAccess t) -- ^ function to log access requests (see also: 'logMAccess')+ , timeout :: Int -- ^ number of seconds to wait before killing an inactive thread+ , threadGroup :: Maybe TG.ThreadGroup -- ^ ThreadGroup for registering spawned threads for handling requests.+ }++-- | 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+ , threadGroup = Nothing+ }++-- | log access requests using hslogger and apache-style log formatting+--+-- see also: 'Conf'+logMAccess :: forall t. FormatTime t => LogAccess t+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 | PATCH | EXTENSION ByteString+ deriving (Show, Read, Eq, Ord, Data)++-- | Does the method support a message body?+--+-- For extension methods, we assume yes.+canHaveBody :: Method+ -> Bool+canHaveBody POST = True+canHaveBody PUT = True+canHaveBody PATCH = True+canHaveBody DELETE = True+canHaveBody (EXTENSION _) = True+canHaveBody _ = False++-- | 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)++-- | 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)++-- | Default RsFlags: automatically use @Transfer-Encoding: Chunked@.+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 }++-- | Automatically add a Content-Length header. Do not use Transfer-Encoding: Chunked+contentLength :: Response -> Response+contentLength res = res { rsFlags = flags } where flags = (rsFlags res) { rsfLength = ContentLength }++-- | 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)++-- | 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+ }++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 (showRsValidator (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 (showRsValidator (rsValidator res)) .+ showString "\nsfFilePath = " . shows (sfFilePath res) .+ showString "\nsfOffset = " . shows (sfOffset res) .+ showString "\nsfCount = " . shows (sfCount res)++showRsValidator :: Maybe (Response -> IO Response) -> String+showRsValidator = maybe "Nothing" (const "Just <function>")++#if !MIN_VERSION_transformers(0,6,0)+-- what should the status code be ?+instance Error Response where+ strMsg str =+ setHeader "Content-Type" "text/plain; charset=UTF-8" $+ result 500 str+#endif++-- | an HTTP request+data Request = Request+ { rqSecure :: Bool -- ^ request uses https:\/\/+ , rqMethod :: Method -- ^ request method+ , rqPaths :: [String] -- ^ the uri, split on /, and then decoded+ , rqUri :: String -- ^ the raw rqUri+ , rqQuery :: String -- ^ the QUERY_STRING+ , rqInputsQuery :: [(String,Input)] -- ^ the QUERY_STRING decoded as key/value pairs+ , rqInputsBody :: MVar [(String,Input)] -- ^ the request body decoded as key/value pairs (when appropriate)+ , rqCookies :: [(String,Cookie)] -- ^ cookies+ , rqVersion :: HttpVersion -- ^ HTTP version+ , rqHeaders :: Headers -- ^ the HTTP request headers+ , rqBody :: MVar RqBody -- ^ the raw, undecoded request body+ , rqPeer :: Host -- ^ (hostname, port) of the client making the request+ }++instance Show Request where+ showsPrec _ rq =+ showString "================== Request =================" .+ showString "\nrqSecure = " . shows (rqSecure rq) .+ 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)++-- | 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++-- | 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)++-- | 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 (vs2++vs1)++--------------------------------------------------------------+-- 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 (vs2++vs1)++-- | 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"++readDec' :: (Num a, Eq a) => String -> a+readDec' s =+ case readDec s of+ [(n,[])] -> n+ _ -> error "readDec' failed."++-- | Read in any monad.+readM :: (MonadFail m, Read t) => String -> m t+readM s = case reads s of+ [(v,"")] -> return v+ _ -> fail "readM: parse error"++-- |convert a 'ReadS a' result to 'Maybe a'+fromReadS :: [(a, String)] -> Maybe a+fromReadS [(n,[])] = Just n+fromReadS _ = Nothing++-- | 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 Text.Text where fromReqURI = fmap fromString . fromReqURI+instance FromReqURI Lazy.Text where fromReqURI = fmap fromString . fromReqURI+instance FromReqURI Char where fromReqURI s = case s of [c] -> Just c ; _ -> Nothing+instance FromReqURI Int where fromReqURI = fromReadS . readSigned readDec+instance FromReqURI Int8 where fromReqURI = fromReadS . readSigned readDec+instance FromReqURI Int16 where fromReqURI = fromReadS . readSigned readDec+instance FromReqURI Int32 where fromReqURI = fromReadS . readSigned readDec+instance FromReqURI Int64 where fromReqURI = fromReadS . readSigned readDec+instance FromReqURI Integer where fromReqURI = fromReadS . readSigned readDec+instance FromReqURI Word where fromReqURI = fromReadS . readDec+instance FromReqURI Word8 where fromReqURI = fromReadS . readDec+instance FromReqURI Word16 where fromReqURI = fromReadS . readDec+instance FromReqURI Word32 where fromReqURI = fromReadS . readDec+instance FromReqURI Word64 where fromReqURI = fromReadS . readDec+instance FromReqURI Float where fromReqURI = readM+instance FromReqURI Double where fromReqURI = readM+instance FromReqURI Bool where+ fromReqURI s =+ let s' = map toLower s in+ case s' of+ "0" -> Just False+ "false" -> Just False+ "1" -> Just True+ "true" -> Just True+ _ -> Nothing++------------------------------------------------------------------------------+-- EscapeHTTP - escape hatched use by websockets+------------------------------------------------------------------------------++-- | Escape from the HTTP world and get direct access to the underlying 'TimeoutIO' functions+data EscapeHTTP+ = EscapeHTTP (TimeoutIO -> IO ())++instance Exception EscapeHTTP++instance Show EscapeHTTP where+ show (EscapeHTTP {}) = "<EscapeHTTP _>"
− src/Happstack/Server/JSON.hs
@@ -1,29 +0,0 @@-module Happstack.Server.JSON where-import Data.Char-import Data.List--data JSON = JBool Bool | JString String | JInt Int | JFloat Float | - JObj [(String,JSON)] | JList [JSON] | JNull---jInt :: Integral a => a -> JSON-jInt = JInt . fromIntegral--class ToJSON a where- toJSON::a->JSON-instance ToJSON JSON where toJSON=id--jsonToString :: JSON -> String-jsonToString (JBool bool) = map toLower $ show bool-jsonToString (JString string) = show string-jsonToString (JInt int) = show int-jsonToString (JFloat float) = show float-jsonToString (JObj pairs) = '{' : (concat $ (intersperse "," $ map impl pairs) )++"}"- where- impl (name,val) = concat [show name,":",jsonToString val]-jsonToString (JList list) = - '[':(concat $ (intersperse "," $ map jsonToString list)) ++"]"-jsonToString JNull = "null"-type CallBack=String--data JSONCall x = JCall CallBack x
− src/Happstack/Server/MessageWrap.hs
@@ -1,99 +0,0 @@-{-# LANGUAGE FlexibleInstances #-}--module Happstack.Server.MessageWrap where--import Control.Monad.Identity-import qualified Data.ByteString.Char8 as P-import qualified Data.ByteString.Lazy.Char8 as L-import qualified Data.List as List-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 = []-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)---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,215 +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 Data.Maybe-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,118 @@+{-# LANGUAGE FlexibleContexts, CPP #-}+-- | 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+ , neverExpires+ -- * WebMonad+ , WebMonad(..)+ , escape+ , escape'+ -- * MonadPlus helpers+ , require+ , requireM+ -- * escapeHTTP+ , escapeHTTP+ ) where++import Control.Applicative (Alternative, Applicative)+import Control.Monad (MonadPlus(mzero))+#if !MIN_VERSION_transformers(0,6,0)+import Control.Monad.Trans.Error (Error, ErrorT)+#endif+import Control.Monad.Trans (MonadIO(..),MonadTrans(lift))+import Control.Monad.Trans.Except (ExceptT)+import Control.Monad.Reader (ReaderT)+import qualified Control.Monad.Writer.Lazy as Lazy (WriterT)+import qualified Control.Monad.Writer.Strict as Strict (WriterT)+import qualified Control.Monad.State.Lazy as Lazy (StateT)+import qualified Control.Monad.State.Strict as Strict (StateT)+import qualified Control.Monad.RWS.Lazy as Lazy (RWST)+import qualified Control.Monad.RWS.Strict as Strict (RWST)+import qualified Data.ByteString.Char8 as B+import Data.Monoid (Monoid)+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+ , Applicative m, Alternative m) => Happstack m++instance (Functor m, Monad m, MonadPlus m+ , MonadIO m) => Happstack (ServerPartT m)+instance (Happstack m) => Happstack (Lazy.StateT s m)+instance (Happstack m) => Happstack (Strict.StateT s m)+instance (Happstack m) => Happstack (ReaderT r m)+instance (Happstack m, Monoid w) => Happstack (Lazy.WriterT w m)+instance (Happstack m, Monoid w) => Happstack (Strict.WriterT w m)+instance (Happstack m, Monoid w) => Happstack (Lazy.RWST r w s m)+instance (Happstack m, Monoid w) => Happstack (Strict.RWST r w s m)+#if !MIN_VERSION_transformers(0,6,0)+instance (Happstack m, Error e) => Happstack (ErrorT e m)+#endif+instance (Happstack m, Monoid e) => Happstack (ExceptT e 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++-- | Set a far-future Expires header. Useful for static resources. If the+-- browser has the resource cached, no extra request is spent.+neverExpires :: (FilterMonad Response m) => m ()+neverExpires = setHeaderM "Expires" "Mon, 31 Dec 2035 12:00:00 GMT"++-- | 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,153 +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- mbAccept<-getHeaderM "Accept-Encoding"- accept<- maybe mzero return mbAccept- let eEncoding = bestEncoding $ BS.unpack accept- (coding,action) <- case eEncoding of- Left _ -> do- setResponseCode 406- finishWith $ toResponse ""- Right a -> return (a, maybe (fail "Encoding returned not in the list of known encodings")- id (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 = many space- sep = do- ws- char ','- ws- encoding1 :: GenParser Char st ([Char], Maybe Double)- encoding1 = do- encoding <- many1 letter <|> string "*"- ws- quality<-optionMaybe qual- return (encoding, fmap read quality)- qual = do- char ';' >> ws >> char 'q' >> ws >> char '=' >> ws- q<-float- return $ q- int = many1 digit- 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/Response.hs view
@@ -0,0 +1,334 @@+{-# LANGUAGE CPP, FlexibleContexts, FlexibleInstances, TypeSynonymInstances, ScopedTypeVariables #-}+-- | Functions and classes related to generating a 'Response' and setting the response code. For detailed instruction see the Happstack Crash Course: <http://www.happstack.com/docs/crashcourse/index.html#creating-a-response>+module Happstack.Server.Response+ ( -- * Converting values to a 'Response'+ ToMessage(..)+ , flatten+ , toResponseBS+ -- * Setting the Response Code+ , ok+ , noContent+ , internalServerError+ , badGateway+ , badRequest+ , unauthorized+ , forbidden+ , notFound+ , prettyResponse+ , requestEntityTooLarge+ , seeOther+ , found+ , movedPermanently+ , tempRedirect+ , setResponseCode+ , resp+ -- * Handling if-modified-since+ , ifModifiedSince+ ) where++#if MIN_VERSION_xhtml(3000,3,0)+import qualified Data.ByteString.Builder as L+#endif+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 Data.Time (UTCTime, formatTime, defaultTimeLocale)+import Happstack.Server.Internal.Monads (FilterMonad(composeFilter))+import Happstack.Server.Internal.Types+import Happstack.Server.Types (Response(..), Request(..), nullRsFlags, getHeader, noContentLength, redirect, result, setHeader, setHeaderBS)+import Happstack.Server.SURI (ToSURI)+import qualified Text.Blaze.Html as Blaze+import qualified Text.Blaze.Html.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.+--+-- @happstack-server@ 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!"+--+-- @happstack-lite@ Example:+--+-- > main = serve Nothing $ toResponse "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"+#if MIN_VERSION_xhtml(3000,3,0)+ toMessage = L.toLazyByteString . XHtml.renderHtml+#else+ toMessage = LU.fromString . XHtml.renderHtml+#endif+instance ToMessage Blaze.Html where+ toContentType _ = B.pack "text/html; charset=UTF-8"+ toMessage = Blaze.renderHtml++instance ToMessage Response where+ toResponse = id++instance ToMessage L.ByteString where+ toResponse bs = Response 200 M.empty nullRsFlags bs Nothing++instance ToMessage B.ByteString where+ toResponse bs = toResponse (L.fromChunks [bs])++{-++-- 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 :: UTCTime -- ^ 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 = formatTime 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 @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 @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 @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 @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++-- | 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 @413 Request Entity Too Large@.+--+-- > main = simpleHTTP nullConf $ requestEntityTooLarge "That's too big for me to handle."+requestEntityTooLarge :: (FilterMonad Response m) => a -> m a+requestEntityTooLarge = resp 413++-- | 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++-- | A nicely formatted rendering of a 'Response'+prettyResponse :: Response -> String+prettyResponse 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 = " $ showRsValidator (rsValidator res)+prettyResponse res@SendFile{} =+ showString "================== Response ================" .+ showString "\nrsCode = " . shows (rsCode res) .+ showString "\nrsHeaders = " . shows (rsHeaders res) .+ showString "\nrsFlags = " . shows (rsFlags res) .+ showString "\nrsValidator = " . shows (showRsValidator (rsValidator res)) .+ showString "\nsfFilePath = " . shows (sfFilePath res) .+ showString "\nsfOffset = " . shows (sfOffset res) .+ showString "\nsfCount = " $ show (sfCount res)
+ src/Happstack/Server/Routing.hs view
@@ -0,0 +1,257 @@+{-# 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://www.happstack.com/docs/crashcourse/index.html#route-filters>+module Happstack.Server.Routing+ ( -- * Route by scheme+ http+ , https+ -- * Route by request method+ , methodM+ , methodOnly+ , methodSP+ , method+ , MatchMethod(..)+ -- * Route by pathInfo+ , dir+ , dirs+ , nullDir+ , trailingSlash+ , noTrailingSlash+ , anyPath+ , path+ , uriRest+ -- * Route by host+ , host+ , withHost+ -- * Route by (Request -> Bool)+ , guardRq+ ) where++import Control.Monad (MonadPlus(mzero), unless)+import qualified Data.ByteString.Char8 as B+import Happstack.Server.Monads (ServerMonad(..))+import Happstack.Server.Types (Request(..), Method(..), FromReqURI(..), getHeader, rqURL)+import System.FilePath (makeRelative, splitDirectories)++-- | instances of this class provide a variety of ways to match on the 'Request' method.+--+-- Examples:+--+-- > method GET -- match GET or HEAD+-- > method [GET, POST] -- match GET, HEAD or POST+-- > method HEAD -- match HEAD /but not/ GET+-- > method (== GET) -- match GET or HEAD+-- > method (not . (==) DELETE) -- match any method except DELETE+-- > method () -- match any method+--+-- As you can see, GET implies that HEAD should match as well. This is to+-- make it harder to write an application that uses HTTP incorrectly.+-- Happstack handles HEAD requests automatically, but we still need to make+-- sure our handlers don't mismatch or a HEAD will result in a 404.+--+-- If you must, you can still do something like this+-- to match GET without HEAD:+--+-- > guardRq ((== GET) . rqMethod)++class MatchMethod m where+ matchMethod :: m -> Method -> Bool++instance MatchMethod Method where+ matchMethod m = matchMethod (== m)++instance MatchMethod [Method] where+ matchMethod ms m = any (`matchMethod` m) ms++instance MatchMethod (Method -> Bool) where+ matchMethod f HEAD = f HEAD || f GET+ matchMethod f m = f m++instance MatchMethod () where+ matchMethod () _ = True++-------------------------------------+-- 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 which checks that an insecure connection was made via http:\/\/+--+-- Example:+--+-- > handler :: ServerPart Response+-- > handler =+-- > do http+-- > ...+http :: (ServerMonad m, MonadPlus m) => m ()+http = guardRq (not . rqSecure)+++-- | guard which checks that a secure connection was made via https:\/\/+--+-- Example:+--+-- > handler :: ServerPart Response+-- > handler =+-- > do https+-- > ...+https :: (ServerMonad m, MonadPlus m) => m ()+https = guardRq rqSecure+++-- | Guard against the method only (as opposed to 'methodM').+--+-- Example:+--+-- > handler :: ServerPart Response+-- > handler =+-- > do method [GET, HEAD]+-- > ...+method :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m ()+method meth = guardRq $ \rq -> matchMethod meth (rqMethod rq)++-- | Guard against the method. This function also guards against+-- *any remaining path segments*. See 'method' for the version+-- that guards only by method.+--+-- Example:+--+-- > handler :: ServerPart Response+-- > handler =+-- > do methodM [GET, HEAD]+-- > ...+--+-- NOTE: This function is largely retained for backwards+-- compatibility. The fact that implicitly calls 'nullDir' is often+-- forgotten and leads to confusion. It is probably better to just use+-- 'method' and call 'nullDir' explicitly.+--+-- This function will likely be deprecated in the future.+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 = method+{-# DEPRECATED methodOnly "this function is just an alias for method now" #-}++-- | 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+--+-- NOTE: This style of combinator is going to be deprecated in the+-- future. It is better to just use 'method'.+--+-- > handler :: ServerPart Response+-- > handler = method [GET, HEAD] >> nullDir >> subHandler+{-# DEPRECATED methodSP "use method instead." #-}+methodSP :: (ServerMonad m, MonadPlus m, MatchMethod method) => method -> m b-> m b+methodSP m handle = methodM m >> handle++-- | 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>+--+-- Note that this matches against the value of the @Host@ header which may include the port number.+--+-- <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.23>+--+-- 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)) == '/'++-- | The opposite of 'trailingSlash'.+noTrailingSlash :: (ServerMonad m, MonadPlus m) => m ()+noTrailingSlash = guardRq $ \rq -> (last (rqUri rq)) /= '/'
+ src/Happstack/Server/RqData.hs view
@@ -0,0 +1,720 @@+{-# LANGUAGE CPP, DeriveDataTypeable, GeneralizedNewtypeDeriving, FlexibleContexts, FlexibleInstances, MultiParamTypeClasses, StandaloneDeriving, UndecidableInstances #-}+-- | 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://www.happstack.com/docs/crashcourse/index.html#parsing-request-data-from-the-query_string-cookies-and-request-body>+module Happstack.Server.RqData+ ( -- * Looking up keys+ -- ** Form Values and Query Parameters+ look+ , looks+ , lookText+ , lookText'+ , lookTexts+ , lookTexts'+ , 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+ , bytestring+ -- * Validation and Parsing+ , checkRq+ , checkRqM+ , readRq+ , unsafeReadRq+ -- * 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.Monad (MonadPlus(mzero, mplus))+import Control.Monad.Reader (ReaderT(ReaderT, runReaderT), MonadReader(ask, local), mapReaderT)+import qualified Control.Monad.State.Lazy as Lazy (StateT, mapStateT)+import qualified Control.Monad.State.Strict as Strict (StateT, mapStateT)+import qualified Control.Monad.Writer.Lazy as Lazy (WriterT, mapWriterT)+import qualified Control.Monad.Writer.Strict as Strict (WriterT, mapWriterT)+import qualified Control.Monad.RWS.Lazy as Lazy (RWST, mapRWST)+import qualified Control.Monad.RWS.Strict as Strict (RWST, mapRWST)+#if !MIN_VERSION_transformers(0,6,0)+import qualified Control.Monad.Trans.Error as DeprecatedError+#endif+import Control.Monad.Except (throwError)+import Control.Monad.Trans (MonadIO(..), lift)+import Control.Monad.Trans.Except (ExceptT, mapExceptT)+import qualified Data.ByteString.Char8 as P+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)+import Data.Maybe (fromJust)+import Data.Monoid (Monoid(mempty, mappend, mconcat))+import qualified Data.Semigroup as SG+import Data.Text (Text)+import qualified Data.Text.Lazy as LazyText+import qualified Data.Text.Lazy.Encoding as LazyText+import Happstack.Server.Cookie (Cookie (cookieValue))+import Happstack.Server.Internal.Monads+import Happstack.Server.Types+import Happstack.Server.Internal.MessageWrap (BodyPolicy(..), bodyInput, defaultBodyPolicy)+import Happstack.Server.Response (requestEntityTooLarge, toResponse)+import Network.URI (unEscapeString)++newtype ReaderError r e a = ReaderError { unReaderError :: ReaderT r (Either e) a }+ deriving (Functor, Monad)++#if MIN_VERSION_transformers(0,6,0)+deriving instance (Monoid e, MonadPlus (Either e)) => MonadPlus (ReaderError r e)+#else+deriving instance (Monoid e, DeprecatedError.Error e, MonadPlus (Either e)) => MonadPlus (ReaderError r e)+#endif++#if MIN_VERSION_transformers(0,6,0)+instance (Monoid e) => MonadReader r (ReaderError r e) where+#else+instance (DeprecatedError.Error e, Monoid e) => MonadReader r (ReaderError r e) where+#endif+ ask = ReaderError ask+ local f m = ReaderError $ local f (unReaderError m)++#if MIN_VERSION_transformers(0,6,0)+instance (Monoid e) => Applicative (ReaderError r e) where+#else+instance (Monoid e, DeprecatedError.Error e) => Applicative (ReaderError r e) where+#endif+ pure = return+ (ReaderError (ReaderT f)) <*> (ReaderError (ReaderT a))+ = ReaderError $ ReaderT $ \env -> (f env) `apEither` (a env)++#if MIN_VERSION_transformers(0,6,0)+instance (MonadPlus (Either e), Monoid e) => Alternative (ReaderError r e) where+#else+instance (Monoid e, DeprecatedError.Error e) => Alternative (ReaderError r e) where+#endif+ 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)++instance SG.Semigroup (Errors a) where+ (Errors x) <> (Errors y) = Errors (x ++ y)++instance Monoid (Errors a) where+ mempty = Errors []+ mappend = (SG.<>)+ mconcat errs = Errors $ concatMap unErrors errs++#if MIN_VERSION_transformers(0,6,0)+instance (Alternative (Either (Errors a))) => MonadPlus (Either (Errors a)) where+ mzero = Left (Errors [])+ (Left _) `mplus` n = n+ m `mplus` _ = m++instance Alternative (Either (Errors a)) where+ empty = Left (Errors [])+ (Left _) <|> n = n+ m <|> _ = m+#endif++#if !MIN_VERSION_transformers(0,6,0)+instance DeprecatedError.Error (Errors String) where+ noMsg = Errors []+ strMsg str = Errors [str]+#endif++strMsg :: a -> Errors a+strMsg errMsg = Errors [errMsg]++{- commented out to avoid 'Defined but not used' warning.+readerError :: (Monoid e, Error e) => e -> ReaderError r e b+readerError e = mapReaderErrorT ((Left e) `apEither`) (return ())++mapReaderErrorT :: (Either e a -> Either e' b) -> (ReaderError r e a) -> (ReaderError r e' b)+mapReaderErrorT f m = ReaderError $ mapReaderT f (unReaderError m)+-}++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)], Maybe [(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, MonadPlus m) => HasRqData (ServerPartT m) where+ askRqEnv = smAskRqEnv+ rqDataError _e = mzero+ localRqEnv = smLocalRqEnv++------------------------------------------------------------------------------+-- HasRqData instances for ReaderT, StateT, WriterT, RWST, and ErrorT+------------------------------------------------------------------------------++instance (Monad m, HasRqData m) => HasRqData (ReaderT s m) where+ askRqEnv = lift askRqEnv+ localRqEnv f = mapReaderT (localRqEnv f)+ rqDataError e = lift (rqDataError e)++instance (Monad m, HasRqData m) => HasRqData (Lazy.StateT s m) where+ askRqEnv = lift askRqEnv+ localRqEnv f = Lazy.mapStateT (localRqEnv f)+ rqDataError e = lift (rqDataError e)++instance (Monad m, HasRqData m) => HasRqData (Strict.StateT s m) where+ askRqEnv = lift askRqEnv+ localRqEnv f = Strict.mapStateT (localRqEnv f)+ rqDataError e = lift (rqDataError e)++instance (Monad m, HasRqData m, Monoid w) => HasRqData (Lazy.WriterT w m) where+ askRqEnv = lift askRqEnv+ localRqEnv f = Lazy.mapWriterT (localRqEnv f)+ rqDataError e = lift (rqDataError e)++instance (Monad m, HasRqData m, Monoid w) => HasRqData (Strict.WriterT w m) where+ askRqEnv = lift askRqEnv+ localRqEnv f = Strict.mapWriterT (localRqEnv f)+ rqDataError e = lift (rqDataError e)++instance (Monad m, HasRqData m, Monoid w) => HasRqData (Lazy.RWST r w s m) where+ askRqEnv = lift askRqEnv+ localRqEnv f = Lazy.mapRWST (localRqEnv f)+ rqDataError e = lift (rqDataError e)++instance (Monad m, HasRqData m, Monoid w) => HasRqData (Strict.RWST r w s m) where+ askRqEnv = lift askRqEnv+ localRqEnv f = Strict.mapRWST (localRqEnv f)+ rqDataError e = lift (rqDataError e)++#if !MIN_VERSION_transformers(0,6,0)+instance (Monad m, DeprecatedError.Error e, HasRqData m) => HasRqData (DeprecatedError.ErrorT e m) where+ askRqEnv = lift askRqEnv+ localRqEnv f = DeprecatedError.mapErrorT (localRqEnv f)+ rqDataError e = lift (rqDataError e)+#endif++instance (Monad m, HasRqData m) => HasRqData (ExceptT e m) where+ askRqEnv = lift askRqEnv+ localRqEnv f = mapExceptT (localRqEnv f)+ rqDataError e = lift (rqDataError e)++-- | 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` (unsafeReadRq "key")+--+-- use with 'checkRq'+--+-- NOTE: This function is marked unsafe because some Read instances+-- are vulnerable to attacks that attempt to create an out of memory+-- condition. For example:+--+-- > read "1e10000000000000" :: Integer+--+-- see also: 'readRq'+unsafeReadRq :: (Read a) =>+ String -- ^ name of key (only used for error reporting)+ -> String -- ^ 'String' to 'read'+ -> Either String a -- ^ 'Left' on error, 'Right' on success+unsafeReadRq key val =+ case reads val of+ [(a,[])] -> Right a+ _ -> Left $ "readRq failed while parsing key: " ++ key ++ " which has the value: " ++ val++-- | use 'fromReqURI' to convert a 'String' to a value of type 'a'+--+-- > look "key" `checkRq` (readRq "key")+--+-- use with 'checkRq'+readRq :: (FromReqURI 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 fromReqURI val of+ (Just 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+ eb <- f a+ case eb 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)++fromMaybeBody :: String -> String -> Maybe [(String, Input)] -> [(String, Input)]+fromMaybeBody funName fieldName mBody =+ case mBody of+ Nothing -> error $ funName ++ " " ++ fieldName ++ " failed because the request body has not been decoded yet. Try using 'decodeBody' to decode the body. Or the 'queryString' filter to ignore the body."+ (Just bdy) -> bdy++-- | 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, mBody, _cookies) <- askRqEnv+ let bdy = fromMaybeBody "lookInput" name mBody+ case lookup name (query ++ bdy) 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, mBody, _cookies) <- askRqEnv+ let bdy = fromMaybeBody "lookInputs" name mBody+ return $ lookups name (query ++ bdy)++-- | 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 LazyText.Text+lookText = fmap LazyText.decodeUtf8 . lookBS++-- | Gets the first matching named input parameter as a strict '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 LazyText.toStrict . lookText++-- | 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 [LazyText.Text]+lookTexts = fmap (map LazyText.decodeUtf8) . lookBSs++-- | Gets all matches for the named input parameter as strict '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 LazyText.toStrict) . lookTexts++-- | 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{cookieValue = f c}+ where+ f = unEscapeString . cookieValue++-- | 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, FromReqURI 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, FromReqURI 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, FromReqURI 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+--+-- If the user does not supply a file in the html form input field,+-- the behaviour will depend upon the browser. Most browsers will send+-- a 0-length file with an empty file name, so checking that (2) is+-- not empty is usually sufficient to ensure the field has been+-- filled.+--+-- 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, mBody, _cookies) <- askRqEnv+ let bdy = fromMaybeBody "lookPairs" "" mBody+ return $ map (\(n,vbs)->(n, (\e -> case e of Left fp -> Left fp ; Right bs -> Right (LU.toString bs)) $ inputValue vbs)) (query ++ bdy)++-- | 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, mBody, _cookies) <- askRqEnv+ let bdy = fromMaybeBody "lookPairsBS" "" mBody+ return $ map (\(n,vbs) -> (n, inputValue vbs)) (query ++ bdy)++-- | The 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+-- '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, FilterMonad Response m, WebMonad Response m) => BodyPolicy -> m ()+decodeBody bp =+ do rq <- askRq+ (_, me) <- bodyInput bp rq+ case me of+ Nothing -> return ()+ Just e -> escape $ requestEntityTooLarge (toResponse 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, PUT, PATCH, etc.+getDataFn :: (HasRqData m, ServerMonad 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, PUT, PATCH, etc.+withDataFn :: (HasRqData 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, PUT, PATCH, etc.+getData :: (HasRqData 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, PUT, PATCH, etc.+withData :: (HasRqData 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, bdy, _cookies) = ([], bdy, [])++-- | 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, Just [], [])++-- | limit the scope to 'Input's which produce a 'ByteString' (aka, not a file)+bytestring :: (HasRqData m) => m a -> m a+bytestring rqData = localRqEnv f rqData+ where+ f (query, bdy, cookies) = (filter bsf query, filter bsf <$> bdy, cookies)+ bsf (_, i) =+ case inputValue i of+ (Left _fp) -> False+ (Right _bs) -> True
− src/Happstack/Server/S3.hs
@@ -1,240 +0,0 @@-{-# LANGUAGE PatternGuards #-}-module Happstack.Server.S3- ( newS3 -- :: AccessKey -> SecretKey -> URI -> IO S3- , closeS3 -- :: S3 -> IO ()- , createBucket -- :: S3 -> BucketId -> IO ()- , createObject -- :: S3 -> BucketId -> ObjectId -> String -> IO ()- , getObject -- :: S3 -> BucketId -> ObjectId -> IO String- , deleteBucket -- :: S3 -> BucketId -> IO ()- , deleteObject -- :: S3 -> BucketId -> ObjectId -> IO ()- , listObjects -- :: S3 -> BucketId -> IO [String]- , sendRequest -- :: S3 -> Request -> IO String- , sendRequest_ -- :: S3 -> Request -> IO ()--- , sendRequests -- :: S3 -> [Request] -> IO ()- , BucketId, ObjectId, AccessKey, SecretKey- , amazonURI- ) where--import Happstack.Crypto.HMAC ( hmacSHA1 )-import Happstack.Server.HTTPClient.HTTP-import qualified Happstack.Server.HTTPClient.Stream as Stream--import Network.URI hiding (path)-import Control.Concurrent ( newMVar, modifyMVar, swapMVar- , modifyMVar_, MVar )-import Data.Maybe ( fromJust, fromMaybe )-import Data.List ( intersperse )-import System.Time ( getClockTime, toCalendarTime- , formatCalendarTime )-import System.Locale ( defaultTimeLocale, rfc822DateFormat )--import Text.XML.HaXml ( xmlParse, Document(..), Content(..) )-import Text.XML.HaXml.Xtract.Parse ( xtract )--type BucketId = String-type ObjectId = String-type AccessKey = String-type SecretKey = String--data S3- = S3- { s3AccessKey :: AccessKey- , s3SecretKey :: SecretKey- , s3URI :: URI--- , s3KeepAliveTimeout :: Int- , s3Conn :: MVar (Maybe Connection)- }--{- |- Sign a request using the access key and secret key from the S3 data- type.--}-signRequest :: S3 -> Request -> IO Request-signRequest s3- = let akey = s3AccessKey s3- skey = s3SecretKey s3- in signRequest' akey skey--{- |- Fill in necessary information (such as a date header) and then sign- then request.--}-signRequest' :: AccessKey -> SecretKey -> Request -> IO Request-signRequest' akey skey request- = do now <- getClockTime- cal <- toCalendarTime now- let isoDate = formatCalendarTime defaultTimeLocale rfc822DateFormat cal- auth = fromJust (uriAuthority (rqURI request))--- authErr = error "S3.hs: internal error: failed to parse authority"- let dat = concat $ intersperse "\n"- [show (rqMethod request)- ,lookupHeader HdrContentMD5- ,lookupHeader HdrContentType- ,isoDate- ,uriPath (rqURI request)]- authorization = Header HdrAuthorization $ "AWS " ++ akey ++ ":" ++ signature- signature = hmacSHA1 skey dat- lookupHeader hn = fromMaybe "" (findHeader hn request)- dateHdr = Header HdrDate isoDate- lengthHdr = Header HdrContentLength (show $ length (rqBody request))- connHdr = Header HdrConnection "Keep-Alive"- hostHdr = Header HdrHost (uriRegName auth)- return $ request- { rqHeaders = hostHdr:connHdr:lengthHdr:dateHdr:- authorization:rqHeaders request- , rqURI = (rqURI request) { uriScheme = ""- , uriAuthority = Nothing}}--{- |- Return a connection to an S3 server. Will initiate a new- connection if no previous was found.--}-getConnection :: S3 -> IO Connection-getConnection s3- = modifyMVar (s3Conn s3) $ \mbConn ->- case mbConn of- Just conn -> return (mbConn,conn)- Nothing -> do print (uriRegName auth, uriPort auth)- c <- openTCPPort (uriRegName auth) (if null $ uriPort auth then 80 else read$ uriPort auth)- return (Just c,c)- where auth = fromJust (uriAuthority (s3URI s3))--createRequest :: S3 -> RequestMethod -> String -> String -> Request-createRequest _s3 method path body- = Request uri method [] body- where uri = localhost { uriPath = '/':escapeURIString isAllowedInURI path }--{- |- Send a single request to an S3 server returning the body- of the result.--}-sendRequest :: S3 -> Request -> IO String-sendRequest s3 request- = loop =<< signRequest s3 request- where loop request'- = do c <- getConnection s3- ret <- sendHTTP c request'- case ret of- Left ErrorClosed- -> do putStrLn "Connection closed."- swapMVar (s3Conn s3) Nothing- loop request'- Left err -> error ("Failed to connect: " ++ show err) -- FIXME- Right res- | (2,_,_) <- rspCode res -> return (rspBody res)- | otherwise -> error ("Server error: " ++ rspReason res)--{- |- Same as 'sendRequest' except that it ignored the result.--}-sendRequest_ :: S3 -> Request -> IO ()-sendRequest_ s3 request- = do sendRequest s3 request- return ()--{-- Sign and send requests pipelined over a keep-alive connection.--}-{-- S3 imposes a quite severe limitation on pipelined requests.- Sending too many requests or exceeding a size limit will- result in a disconnect. The precise borders of these limits- are hard-wired and unknown to the general public. At the time- of this writing, sending three requests at a time seems optimal.-- Quote from Amazons web-forum:- (http://developer.amazonwebservices.com/connect/thread.jspa?messageID=39883)- "OK, we have located the cause of the behavior you are seeing.- Your pipelined requests are being aborted because one of the- network devices handling the connection has certain limitations- on the amount of pipelined data it is willing to accept per- connection, and that limit is being exceeded. Unfortunately, this- limit is phrased in very low-level terms so it isn't possible to- say in a platform or network independent way whether any given- size, sequence or number of requests will exceed the limit and- cause a disconnection or not. We have engaged with the device- vendor and found out that this limit is hard-wired and that this- behavior is not likely to change any time soon.-- In light of these facts, here is some guidance on pipelining HTTP- requests to Amazon S3.- 1) Be optimistic and pipeline a modest number of GET or HEAD- requests, say two to four.- 2) Handle asynchronous disconnects by re-connecting and re-sending- unacknowledged requests left in your pipeline. (As Colin points- out, correct HTTP clients must do this anyway)- 3) If possible, try to minimize the number of TCP segments your- pipelined requests generate. In particular, leave the TCP socket- no delay option off and send as many requests per socket write call- as is practical."--}------------------------------------------------------------------- Initiate-----------------------------------------------------------------newS3 :: AccessKey -> SecretKey -> URI -> IO S3-newS3 akey skey uri- = do conn <- newMVar Nothing- return $ S3 { s3AccessKey = akey- , s3SecretKey = skey- , s3URI = uri--- , s3KeepAliveTimeout :: Int- , s3Conn = conn- }--closeS3 :: S3 -> IO ()-closeS3 s3- = modifyMVar_ (s3Conn s3) $ \mbConn ->- case mbConn of- Nothing -> return Nothing- Just conn -> do Stream.close conn- return Nothing-------------------------------------------------------------------- Requests------------------------------------------------------------------createBucket :: S3 -> BucketId -> Request-createBucket s3 bucket- = createRequest s3 PUT bucket ""--createObject :: S3 -> BucketId -> ObjectId -> String -> Request-createObject s3 bucket object- = createRequest s3 PUT (bucket ++ "/" ++ object)--getObject :: S3 -> BucketId -> ObjectId -> Request-getObject s3 bucket object- = createRequest s3 GET (bucket ++ "/" ++ object) ""--deleteBucket :: S3 -> BucketId -> Request-deleteBucket s3 bucket- = createRequest s3 DELETE bucket ""--deleteObject :: S3 -> BucketId -> ObjectId -> Request-deleteObject s3 bucket object- = createRequest s3 DELETE (bucket ++ "/" ++ object) ""------------------------------------------------------------------- Actions------------------------------------------------------------------listObjects :: S3 -> BucketId -> IO [String]-listObjects s3 bucket- = do lst <- sendRequest s3 (createRequest s3 GET bucket "")- return $ ppContent . auxFilter . getContent . xmlParse bucket $ lst- where auxFilter = xtract "*/Key/-"- getContent (Document _ _ e _) = CElem e-- ppContent xs = [ s | CString _ s <- xs ]----amazonURI :: URI-amazonURI = fromJust $ parseURI "http://s3.amazonaws.com/"-localhost :: URI-localhost = fromJust $ parseURI "http://localhost/"-
src/Happstack/Server/SURI.hs view
@@ -1,10 +1,32 @@-{-# LANGUAGE TypeSynonymInstances, DeriveDataTypeable #-}+{-# LANGUAGE DeriveDataTypeable, FlexibleInstances, TypeSynonymInstances #-}+-- | 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+ ( path+ , query+ , scheme+ , u_scheme+ , u_path+ , a_scheme+ , a_path+ , percentDecode+ , unEscape+ , unEscapeQS+ , isAbs+ , SURI(..)+ , render+ , parse+ , ToSURI(..)+ , FromPath(..)+ )+ where -module Happstack.Server.SURI where-import Data.Maybe-import Data.Generics-import Happstack.Util.Common(mapFst)-import qualified Network.URI as URI+import Control.Arrow (first)+import Data.Char (chr, digitToInt, isHexDigit)+import Data.Maybe (fromJust, isJust)+import Data.Generics (Data)+import qualified Data.Text as Text+import qualified Data.Text.Lazy as LazyText+import qualified Network.URI as URI -- | Retrieves the path component from the URI path :: SURI -> String@@ -34,19 +56,32 @@ a_path :: String -> SURI -> SURI a_path a (SURI u) = SURI $ u {URI.uriPath=a} -escape, unEscape :: String -> String-unEscape = URI.unEscapeString . map (\x->if x=='+' then ' ' else x)-escape = URI.escapeURIString URI.isAllowedInURI+-- | percent decode a String+--+-- e.g. @\"hello%2Fworld\"@ -> @\"hello/world\"@+percentDecode :: String -> String+percentDecode [] = ""+percentDecode ('%':x1:x2:s) | isHexDigit x1 && isHexDigit x2 =+ chr (digitToInt x1 * 16 + digitToInt x2) : percentDecode s+percentDecode (c:s) = c : percentDecode s +unEscape, unEscapeQS :: String -> String+unEscapeQS = percentDecode . map (\x->if x=='+' then ' ' else x)+unEscape = percentDecode+-- escape = URI.escapeURIString URI.isAllowedInURI++-- | Returns true if the URI is absolute isAbs :: SURI -> Bool isAbs = not . null . URI.uriScheme . suri---isAbs = maybe True ( mbParsed -newtype SURI = SURI {suri::URI.URI} deriving (Eq,Data,Typeable)+newtype SURI = SURI {suri::URI.URI} deriving (Eq,Data) instance Show SURI where showsPrec d (SURI uri) = showsPrec d $ show uri instance Read SURI where- readsPrec d = mapFst fromJust . filter (isJust . fst) . mapFst parse . readsPrec d + readsPrec d = mapFst fromJust . filter (isJust . fst) . mapFst parse . readsPrec d+ where+ mapFst :: (a -> b) -> [(a,x)] -> [(b,x)]+ mapFst = map . first instance Ord SURI where compare a b = show a `compare` show b@@ -57,16 +92,17 @@ -- | Parses a URI from a String. Returns Nothing on failure. parse :: String -> Maybe SURI-parse = fmap SURI . URI.parseURIReference +parse = fmap SURI . URI.parseURIReference -- | Convenience class for converting data types to URIs class ToSURI x where toSURI::x->SURI instance ToSURI SURI where toSURI=id instance ToSURI URI.URI where toSURI=SURI-instance ToSURI String where +instance ToSURI String where toSURI = maybe (SURI $ URI.URI "" Nothing "" "" "") id . parse-+instance ToSURI Text.Text where toSURI = toSURI . Text.unpack+instance ToSURI LazyText.Text where toSURI = toSURI . LazyText.unpack --handling obtaining things from URI paths class FromPath x where fromPath::String->x
src/Happstack/Server/SURI/ParseURI.hs view
@@ -1,12 +1,13 @@ module Happstack.Server.SURI.ParseURI(parseURIRef) where +import qualified Data.ByteString as BB import qualified Data.ByteString.Internal as BB import qualified Data.ByteString.Unsafe as BB import Data.ByteString.Char8 as BC import Prelude hiding(break,length,null,drop,splitAt) import Network.URI -import Happstack.Util.ByteStringCompat+-- import Happstack.Util.ByteStringCompat parseURIRef :: ByteString -> URI parseURIRef fs =@@ -79,3 +80,22 @@ unsafeIndex :: ByteString -> Int -> Char unsafeIndex s = BB.w2c . BB.unsafeIndex s +-- | Semantically equivalent to break on strings+{-# INLINE breakChar #-}+breakChar :: Char -> ByteString -> (ByteString, ByteString)+breakChar ch = BB.break ((==) x) where x = BB.c2w ch++-- | 'breakCharEnd' behaves like breakChar, but from the end of the+-- ByteString.+--+-- > breakCharEnd ('b') (pack "aabbcc") == ("aab","cc")+--+-- and the following are equivalent:+--+-- > breakCharEnd 'c' "abcdef"+-- > let (x,y) = break (=='c') (reverse "abcdef")+-- > in (reverse (drop 1 y), reverse x)+--+{-# INLINE breakCharEnd #-}+breakCharEnd :: Char -> ByteString -> (ByteString, ByteString)+breakCharEnd c p = BB.breakEnd ((==) x) p where x = BB.c2w c
src/Happstack/Server/SimpleHTTP.hs view
@@ -1,1355 +1,235 @@-{-# LANGUAGE UndecidableInstances, OverlappingInstances, ScopedTypeVariables, FlexibleInstances, TypeSynonymInstances,- MultiParamTypeClasses, PatternGuards, FlexibleContexts, FunctionalDependencies, GeneralizedNewtypeDeriving, PatternSignatures- #-}-{-# OPTIONS -fno-warn-orphans #-}--------------------------------------------------------------------------------- |--- 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 used if so desired.--- --- So the general nature of 'simpleHTTP' is no different than what you'd expect--- from a web application container. First you figure out when 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 too in a--- moment), and picks the first handler that is willing to accept the request,--- passes the request into the handler. A simple "hello world" style HAppS--- 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 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 you want to demand--- authentication for in any collection of ServerPartTs.------ @------ 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'- , parseConfig- -- * ServerPartT- , ServerPartT(..)- , ServerPart- , runServerPartT- , mapServerPartT- , mapServerPartT'- , withRequest- , anyRequest- -- * WebT- , WebT(..)- , FilterFun- , Web- , mkWebT- , ununWebT- , runWebT- , mapWebT- -- * Type Classes- , FromReqURI(..)- , ToMessage(..)-- -- * Manipulating requests- , FromData(..)- , ServerMonad(..)- , RqData- , noHandle- , getHeaderM- , escape- , escape'- , multi- -- * Manipulating responses- , FilterMonad(..)- , ignoreFilters- , SetAppend(..)- , FilterT(..)- , WebMonad(..)- , ok- , modifyResponse- , setResponseCode- , badGateway- , internalServerError- , badRequest- , unauthorized - , forbidden- , notFound- , seeOther- , found- , movedPermanently- , tempRedirect- , addCookie- , addCookies- , addHeaderM- , setHeaderM-- -- * guards and building blocks- , guardRq- , dir- , method- , methodSP- , methodM- , methodOnly- , nullDir- , path- , anyPath- , anyPath'- , 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 qualified Paths_happstack_server as Cabal-import qualified Data.Version as DV-import Happstack.Server.HTTP.Client-import Happstack.Data.Xml.HaXml-import qualified Happstack.Server.MinHaXML as H--import Happstack.Server.HTTP.Types hiding (Version(..))-import qualified Happstack.Server.HTTP.Types as Types-import Happstack.Server.HTTP.Listen as Listen-import Happstack.Server.XSLT-import Happstack.Server.SURI (ToSURI)-import Happstack.Util.Common-import Happstack.Server.Cookie-import Happstack.Data -- used by default implementation of fromData-import Control.Applicative-import Control.Concurrent (forkIO)-import Control.Exception (evaluate)-import Control.Monad.Reader-import Control.Monad.State-import Control.Monad.Error-import Control.Monad.Trans()-import Control.Monad.Maybe-import Control.Monad.Writer as Writer-import Data.Maybe-import Data.Monoid-import qualified Data.ByteString.Char8 as B-import qualified Data.ByteString.Lazy.Char8 as L-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-import Data.List-import System.IO-import System.Console.GetOpt-import System.Process (runInteractiveProcess, waitForProcess)-import System.Exit-import Text.Show.Functions ()---- | 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) =>--- -> (ErrorT e m) (Maybe ((Either Response a), FilterFun Response))--- -> m (Maybe ((Either Response a), FilterFun Response))--- unpackErrorT handler et = do--- eitherV <- runErrorT et--- case eitherV of--- Left err -> return $ Just (Left "Catastrophic failure " ++ show e, Set $ Endo \r -> r{rsCode = 500})--- Right x -> return 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 :: (m (Maybe (Either Response a, FilterFun Response)) -> n (Maybe (Either Response b, FilterFun Response))) -> ServerPartT m a -> ServerPartT n b-mapServerPartT f ma = withRequest $ \rq -> mapWebT f (runServerPartT ma rq)---- | A varient 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 -> m (Maybe (Either Response a, FilterFun Response)) -> n (Maybe (Either Response b, FilterFun Response))) -> 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)----------------------------------- 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 sumerizing this is, if the left side is Append, then the--- right is appended to the left. If the left side is Set, then the right 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--value :: SetAppend t -> t-value (Set x) = x-value (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))--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 f = FilterT $ Writer.tell $ Set $ Dual $ Endo f- composeFilter f = FilterT $ Writer.tell $ Append $ Dual $ Endo f- getFilter m = FilterT $ Writer.listens (appEndo . getDual . value) (unFilterT m) ---- | The basic response building object.--- 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--- @--- -newtype WebT m a = WebT { unWebT :: ErrorT Response (FilterT (Response) (MaybeT m)) a }- deriving (MonadIO, Functor)--instance Monad m => Monad (WebT m) where- m >>= f = WebT $ unWebT m >>= unWebT . f- return a = WebT $ return a- fail 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 $ getFilter (runErrorT $ unWebT m) >>= liftWebT- where liftWebT (Left r, _) = return $ Left r- liftWebT (Right a, f) = return $ Right (a, f)--instance (Monad m) => Monoid (WebT m a) where- mempty = mzero- mappend = mplus---- | takes your WebT, converts the monadic value to a Response,--- applys your filter, and returns it wrapped in a Maybe.-runWebT :: (ToMessage b, Monad m) => WebT m b -> m (Maybe Response)-runWebT m = runMaybeT $ do- (r,ed) <- runWriterT $ unFilterT $ runErrorT $ unWebT $ m- let f = appEndo $ getDual $ value ed- return $ either (f) (f . toResponse) r--- | for when you really need to unpack a WebT entirely (and not--- just unwrap the first layer with unWebT)-ununWebT :: WebT m a- -> m (Maybe- (Either Response a,- FilterFun Response))-ununWebT = runMaybeT . runWriterT . unFilterT . runErrorT . unWebT---- | for wrapping a WebT back up. @mkWebT . ununWebT = id@-mkWebT :: m (Maybe- (Either Response a,- FilterFun Response)) -> WebT m a-mkWebT = WebT . ErrorT . FilterT . WriterT . MaybeT---- | see 'mapServerPartT' for a discussion of this function-mapWebT :: (m (Maybe (Either Response a, FilterFun Response)) -> n (Maybe (Either Response b, FilterFun Response))) -> 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 . Writer.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"]---- | parseConfig tries to parse your 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--- noHandle.-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.-simpleHTTP' :: (Monad m, ToMessage b) =>- (m (Maybe (Either Response a, FilterFun Response))- -> IO (Maybe (Either Response b, FilterFun Response)))- -> Conf- -> ServerPartT m a- -> IO ()-simpleHTTP' toIO conf hs = do- Listen.listen conf (\req -> runValidator (fromMaybe return (validator conf)) =<< (simpleHTTP'' (mapServerPartT toIO hs) req))- ---- | Generate a result from a 'ServerPart' and a 'Request'. This is mainly used--- by CGI (and fast-cgi) wrappers.-simpleHTTP'' :: (ToMessage b, Monad 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----- | a wrapper for Read apparently. Pretty much only used for 'path' and probably--- unnecessarily, as it is exactly "readM"-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---- |--- Minimal definition: 'toMessage'------ 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-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"- toMessage [el] = L.pack $ 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"- toMessage = L.pack-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"- toMessage = L.pack . renderHtml--instance ToMessage XHtml.Html where- toContentType _ = B.pack "text/html"- toMessage = L.pack . 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)---- | adds 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---- | sets 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- when ( f rq /= True ) 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 id 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.-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- | otherwise -> mzero- _ -> mzero---- | Pop a path element and parse it. Annoyingly enough, rather than just using Read--- (or providing a parser argument), this method uses 'FromReqURI' which is just a wrapper--- for Read.-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)- | otherwise -> mzero- _ -> mzero---- | grabs 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---- | pops any path element and ignores 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" #-}---- | used to read 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 varient 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 with data, but you pass in a RqData monad--- for reading.-withDataFn :: (MonadPlus m, ServerMonad m) => RqData a -> (a -> m r) -> m r-withDataFn fn handle = do- d <- getDataFn fn- case d of- Nothing -> mzero- Just a -> handle a---- | 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, WebMonad Response 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 varient 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 }---- | deprecated. Same as 'composeFilter'-modifyResponse :: (FilterMonad a m) => (a -> a) -> m()-modifyResponse = composeFilter-{-# DEPRECATED modifyResponse "Use composeFilter" #-}---- | sets the return code in your response-setResponseCode :: FilterMonad Response m => Int -> m ()-setResponseCode code- = composeFilter $ \r -> r{rsCode = code}---- | adds the cookie with a timeout to the response-addCookie :: (FilterMonad Response m) => Seconds -> Cookie -> m ()-addCookie sec = (addHeaderM "Set-Cookie") . mkCookieHeader sec---- | adds the list of cookie timeout pairs to the response-addCookies :: (FilterMonad Response m) => [(Seconds, Cookie)] -> m ()-addCookies = mapM_ (uncurry addCookie)---- | 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--internalServerError :: (FilterMonad Response m) => a -> m a-internalServerError = resp 500--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. Just 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--- This appears to do nothing at all.-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?-applyRequest :: (ToMessage a, Monad 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 = do- unauthorized ()- setHeaderM headerName headerValue- escape' $ toResponse "Not authorized"------------------------------------------------------------------- Query/Post data validating------------------------------------------------------------------- | Useful inside the RqData monad. Gets the named input parameter (either--- from a POST or a GET)-lookInput :: String -> RqData Input-lookInput name- = do inputs <- asks fst- case lookup name inputs of- Nothing -> fail "input not found"- Just i -> return i---- | Gets the named input parameter as a lazy byte string-lookBS :: String -> RqData L.ByteString-lookBS = fmap inputValue . lookInput---- | Gets the named input as a String-look :: String -> RqData String-look = fmap L.unpack . lookBS---- | Gets 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---- | gets the named cookie as a string-lookCookieValue :: String -> RqData String-lookCookieValue = fmap cookieValue . lookCookie---- | gets the named cookie as the requested Read type-readCookieValue :: Read a => String -> RqData a-readCookieValue name = readM =<< fmap cookieValue (lookCookie name)---- | like look, but Reads for you.-lookRead :: Read a => String -> RqData a-lookRead name = readM =<< look name---- | gets all the input parameters, and converts them to a string-lookPairs :: RqData [(String,String)]-lookPairs = asks fst >>= return . map (\(n,vbs)->(n,L.unpack $ 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- -> ErrorT e m (Maybe (Either Response a, FilterFun Response))- -> m (Maybe (Either Response a, FilterFun Response))-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" ... )--- @------ See also: 'setValidator'-setValidatorSP :: (Monad m, ToMessage r) => (Response -> IO Response) -> m r -> m Response-setValidatorSP v sp = return . setValidator v . toResponse =<< sp---- |This extends '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)--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 = "<html><head><title>Happstack "- ++ ver ++ " Internal Server Error</title>"- ++ "<body><h1>Happstack " ++ ver ++ "</h1>"- ++ "<p>Something went wrong here<br />"- ++ "Internal server error<br />"- ++ "Everything has stopped</p>"- ++ "<p>The error was \"" ++ errString ++ "\"</p></body></html>"- where ver = DV.showVersion Cabal.version--notFoundHtml :: String-notFoundHtml = "<html><head><title>Happstack "- ++ ver ++ " File not found</title>"- ++ "<body><h1>Happstack " ++ ver ++ "</h1>"- ++ "<p>Your file is not found<br />"- ++ "To try again is useless<br />"- ++ "It is just not here</p>"- ++ "</body></html>"- where ver = DV.showVersion Cabal.version+{-# LANGUAGE CPP #-}+{-# LANGUAGE UndecidableInstances #-}+{-# LANGUAGE OverlappingInstances #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE TypeSynonymInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE PatternGuards #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FunctionalDependencies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE ScopedTypeVariables #-}++{-# OPTIONS -fno-warn-orphans #-}+-----------------------------------------------------------------------------+-- |+-- Module : Happstack.Server.SimpleHTTP+-- 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+ , bindIPv4+ , parseConfig+ , runWebT+ , waitForTermination+ -- * 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+ -- ** 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.Routing+import Happstack.Server.RqData+import Happstack.Server.Response+import Happstack.Server.Validation++import Control.Monad+import Data.Maybe (fromMaybe)+import qualified Data.Version as DV+import Happstack.Server.Internal.Monads (FilterFun, WebT(..), unFilterFun, runServerPartT, ununWebT)+import qualified Happstack.Server.Internal.Listen as Listen (listen, listen',listenOn, listenOnIPv4) -- So that we can disambiguate 'Writer.listen'+import Network.Socket (Socket)+import qualified Paths_happstack_server as Cabal+import System.Console.GetOpt ( OptDescr(Option)+ , ArgDescr(ReqArg)+ , ArgOrder(Permute)+ , getOpt+ )+#ifdef UNIX+import Control.Concurrent.MVar+import System.Posix.Signals hiding (Handler)+import System.Posix.IO ( stdInput )+import System.Posix.Terminal ( queryTerminal )+#endif++-- | 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 = readDec' 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 Nothing conf impl+--+-- Note: It's important to use the same conf (or at least the same+-- port) for 'bindPort' and 'simpleHTTPWithSocket'.+--+-- see also: 'bindPort', 'bindIPv4'+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 use with '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)++-- | Bind to ip and port and return the socket for use with 'simpleHTTPWithSocket'.+--+-- >+-- > import Happstack.Server+-- >+-- > main = do let conf = nullConf+-- > addr = "127.0.0.1"+-- > s <- bindIPv4 addr (port conf)+-- > simpleHTTPWithSocket s conf $ ok $ toResponse $+-- > "now listening on ip addr " ++ addr +++-- > " and port " ++ show (port conf)+--+bindIPv4 :: String -- ^ IP address to bind to (must be an IP address and not a host name)+ -> Int -- ^ port number to bind to+ -> IO Socket+bindIPv4 addr prt = Listen.listenOnIPv4 addr prt++-- | 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 =+ "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">"+ ++ "<html><head><title>Happstack "+ ++ ver ++ " File not found</title></head>"+ ++ "<body><h1>Happstack " ++ ver ++ "</h1>"+ ++ "<p>Your file is not found<br>"+ ++ "To try again is useless<br>"+ ++ "It is just not here</p>"+ ++ "</body></html>"+ where ver = DV.showVersion Cabal.version+++-- | Wait for a signal.+-- On unix, a signal is sigINT or sigTERM (aka Control-C).+--+-- On windows, the signal is entering: e <return>+waitForTermination :: IO ()+waitForTermination+ = do+#ifdef UNIX+ istty <- queryTerminal stdInput+ mv <- newEmptyMVar+ void $ installHandler softwareTermination (CatchOnce (putMVar mv ())) Nothing+ void $ installHandler lostConnection (CatchOnce (putMVar mv ())) Nothing+ case istty of+ True -> void $ installHandler keyboardSignal (CatchOnce (putMVar mv ())) Nothing+ False -> return ()+ takeMVar mv+#else+ let loop 'e' = return ()+ loop _ = getChar >>= loop+ loop 'c'+#endif
− src/Happstack/Server/StdConfig.hs
@@ -1,22 +0,0 @@-{-# LANGUAGE 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,19 @@+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,+ LogAccess, logMAccess, Conf(..), nullConf, result, resultBS,+ redirect, -- redirect_, redirect', redirect'_,+ isHTTP1_0, isHTTP1_1,+ RsFlags(..), nullRsFlags, contentLength, chunked, noContentLength,+ HttpVersion(..), Length(..), Method(..), Headers, continueHTTP,+ Host, ContentType(..),+ readDec', fromReadS, FromReqURI(..)+ ) where++import Happstack.Server.Internal.Types
+ src/Happstack/Server/Validation.hs view
@@ -0,0 +1,135 @@+-- | 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+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+ void $ forkIO $ do L.hPut inh (rsBody response)+ hClose inh+ void $ forkIO $ evaluate (length out) >> return ()+ void $ 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
@@ -1,157 +0,0 @@-{-# LANGUAGE TemplateHaskell, FlexibleInstances , UndecidableInstances,- DeriveDataTypeable, MultiParamTypeClasses, CPP, ScopedTypeVariables,- PatternSignatures #-}-module Happstack.Server.XSLT- (xsltFile, xsltString, xsltElem, xsltFPS, xsltFPSIO, XSLPath,- xsltproc,saxon,procFPSIO,procLBSIO,XSLTCommand,XSLTCmd- ) where---import System.Log.Logger--import Happstack.Server.MinHaXML-import Happstack.Util.Common(runCommand)-import Control.Exception.Extensible(bracket,try,SomeException)-import qualified Data.ByteString.Char8 as P-import qualified Data.ByteString.Lazy.Char8 as L-import System.Directory(removeFile)-import System.Environment(getEnv)-import System.IO-import System.IO.Unsafe(unsafePerformIO)-import Text.XML.HaXml.Verbatim(verbatim)-import Happstack.Data hiding (Element)--logMX :: Priority -> String -> IO ()-logMX = logM "Happstack.Server.XSLT"--type XSLPath = FilePath--$(deriveAll [''Show,''Read,''Default, ''Eq, ''Ord]- [d|- data XSLTCmd = XSLTProc | Saxon - |]- )--xsltCmd :: XSLTCmd- -> XSLPath- -> FilePath- -> FilePath- -> (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 = - withTempFile "happs-src.xml" $ \sfp sh -> do- withTempFile "happs-dst.xml" $ \dfp dh -> do- let xsltp = xsltCmd xsltp'- L.hPut sh inp- hClose sh- hClose dh- xsltFileEx xsltp xsl sfp dfp- s <- L.readFile dfp- logMX DEBUG (">>> XSLT: result: "++ show s)- return s---procFPSIO :: XSLTCommand- -> XSLPath- -> [P.ByteString]- -> IO [P.ByteString]-procFPSIO xsltp xsl inp = - withTempFile "happs-src.xml" $ \sfp sh -> do- withTempFile "happs-dst.xml" $ \dfp dh -> do- mapM_ (P.hPut sh) inp- hClose sh- hClose dh- xsltFileEx xsltp xsl sfp dfp- s <- P.readFile dfp- logMX DEBUG (">>> XSLT: result: "++ show s)- return [s]---- | Performs an XSL transformation with lists of ByteStrings instead of--- a String.-xsltFPS :: XSLPath -> [P.ByteString] -> [P.ByteString]-xsltFPS xsl = unsafePerformIO . xsltFPSIO xsl---- | Equivalent to 'xsltFPS' but does not hide the inherent IO of the low-level--- ByteString operations.-xsltFPSIO :: XSLPath -> [P.ByteString] -> IO [P.ByteString]-xsltFPSIO xsl inp = - withTempFile "happs-src.xml" $ \sfp sh -> do- withTempFile "happs-dst.xml" $ \dfp dh -> do- mapM_ (P.hPut sh) inp- hClose sh- hClose dh- xsltFile xsl sfp dfp- s <- P.readFile dfp- logMX DEBUG (">>> XSLT: result: "++ show s)- return [s]---- | Uses the provided xsl file to transform the given string.--- This function creates temporary files during its execution, but--- guarantees their cleanup.-xsltString :: XSLPath -> String -> String-xsltString xsl inp = unsafePerformIO $- withTempFile "happs-src.xml" $ \sfp sh -> do- withTempFile "happs-dst.xml" $ \dfp dh -> do- hPutStr sh inp- hClose sh- hClose dh- xsltFile xsl sfp dfp- s <- readFileStrict dfp- logMX DEBUG (">>> XSLT: result: "++ show s)- return s---- | Note that the xsl file must have .xsl suffix.-xsltFile :: XSLPath -> FilePath -> FilePath -> IO ()-xsltFile = xsltFileEx xsltproc'---- | Use @xsltproc@ to transform XML.-xsltproc :: XSLTCmd-xsltproc = XSLTProc-xsltproc' :: XSLTCommand-xsltproc' dst xsl src = ("xsltproc",["-o",dst,xsl,src])----- | Use @saxon@ to transform XML.-saxon :: XSLTCmd-saxon = Saxon-saxon' :: XSLTCommand-saxon' dst xsl src = ("java -classpath /usr/share/java/saxon.jar",- ["com.icl.saxon.StyleSheet"- ,"-o",dst,src,xsl])- -type XSLTCommand = XSLPath -> FilePath -> FilePath -> (FilePath,[String])-xsltFileEx :: XSLTCommand -> XSLPath -> FilePath -> FilePath -> IO ()-xsltFileEx xsltp xsl src dst = do- let msg = (">>> XSLT: Starting xsltproc " ++ unwords ["-o",dst,xsl,src])- logMX DEBUG msg- uncurry runCommand $ xsltp dst xsl src- logMX DEBUG (">>> XSLT: xsltproc done")---- Utilities--withTempFile :: String -> (FilePath -> Handle -> IO a) -> IO a-withTempFile str hand = bracket (openTempFile tempDir str) (removeFile . fst) (uncurry hand)--readFileStrict :: FilePath -> IO String-readFileStrict fp = do- let fseqM [] = return [] - fseqM xs = last xs `seq` return xs- fseqM =<< readFile fp--{-# NOINLINE tempDir #-}-tempDir :: FilePath-tempDir = unsafePerformIO $ tryAny [getEnv "TEMP",getEnv "TMP"] err- where err = return "/tmp"--tryAny :: [IO a] -> IO a -> IO a-tryAny [] c = c-tryAny (x:xs) c = either (\(_::SomeException) -> tryAny xs c) return =<< try x
tests/Happstack/Server/Tests.hs view
@@ -1,41 +1,262 @@ -- |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+import Data.ByteString.Lazy.Char8 (pack, unpack)+import qualified Data.ByteString.Char8 as B+import qualified Data.ByteString.Lazy as L+import Data.List (intercalate)+import qualified Data.Map as Map+import Happstack.Server ( Request(..), Method(..), Response(..), ServerPart, Headers, RqBody(Body), HttpVersion(..)+ , ToMessage(..), HeaderPair(..), ok, dir, simpleHTTP'', composeFilter, noContentLength, matchMethod+ , look, getDataFn)+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.Internal.RFC822Headers (ContentDisposition(..), parseContentDisposition)+import Happstack.Server.SURI(ToSURI(..), path, query)+import Test.HUnit as HU (Test(..), (~:), (@?=), (@=?), assertEqual) import Text.ParserCombinators.Parsec --- |All of the tests for happstack-util should be listed here. +-- |All of the tests for happstack-util should be listed here. allTests :: Test-allTests = - "happstack-server tests" ~: [cookieParserTest, acceptEncodingParserTest]+allTests =+ "happstack-server tests" ~: [ cookieParserTest+ , acceptEncodingParserTest+ , multipart+ , compressFilterResponseTest+ , matchMethodTest+ , cookieHeaderOrderTest+ , pContentDispositionFilename+ , applicativeTest+ ] cookieParserTest :: Test-cookieParserTest = +cookieParserTest = "cookieParserTest" ~: [parseCookies "$Version=1;Cookie1=value1;$Path=\"/testpath\";$Domain=example.com;cookie2=value2" @?= (Right [- Cookie "1" "/testpath" "example.com" "cookie1" "value1"- , Cookie "1" "" "" "cookie2" "value2"+ Cookie "1" "/testpath" "example.com" "cookie1" "value1" False False SameSiteNoValue False+ , Cookie "1" "" "" "cookie2" "value2" False False SameSiteNoValue False ]) ,parseCookies " \t $Version = \"1\" ; cookie1 = \"randomcrap!@#%^&*()-_+={}[]:;'<>,.?/\\|\" , $Path=/ " @?= (Right [- Cookie "1" "/" "" "cookie1" "randomcrap!@#%^&*()-_+={}[]:;'<>,.?/\\|"+ Cookie "1" "/" "" "cookie1" "randomcrap!@#%^&*()-_+={}[]:;'<>,.?/|" False False SameSiteNoValue False ]) ,parseCookies " cookie1 = value1 " @?= (Right [- Cookie "" "" "" "cookie1" "value1"+ Cookie "" "" "" "cookie1" "value1" False False SameSiteNoValue False ]) ,parseCookies " $Version=\"1\";buggygooglecookie = valuewith=whereitshouldnotbe " @?= (Right [- Cookie "1" "" "" "buggygooglecookie" "valuewith=whereitshouldnotbe"+ Cookie "1" "" "" "buggygooglecookie" "valuewith=whereitshouldnotbe" False False SameSiteNoValue False ])+ , parseCookies "foo=\"\\\"bar\\\"\""+ @?= (Right [+ Cookie "" "" "" "foo" "\"bar\"" False False SameSiteNoValue False+ ]) ] acceptEncodingParserTest :: Test acceptEncodingParserTest = "acceptEncodingParserTest" ~:- [either (Left . show) Right (parse encodings "" " gzip;q=1,*, compress ; q = 0.5 ")- @?= (Right [("gzip", Just 1),("*", Nothing),("compress", Just 0.5)])+ map (\(str, result) -> either (Left . show) Right (parse encodings "" str) @?= (Right result)) acceptEncodings+ where+ acceptEncodings =+ [ (" gzip;q=1,*, compress ; q = 0.5 ", [("gzip", Just 1),("*", Nothing),("compress", Just 0.5)])+ , (" compress , gzip", [ ("compress", Nothing), ("gzip", Nothing)])+ , (" ", [])+ , (" *", [("*", Nothing)])+ , (" compress;q=0.5, gzip;q=1.0", [("compress", Just 0.5), ("gzip", Just 1.0)])+ , (" gzip;q=1.0, identity; q=0.5, *;q=0", [("gzip", Just 1.0), ("identity",Just 0.5), ("*", Just 0)])+ , (" x-gzip",[("x-gzip", Nothing)])+ ]++multipart :: Test+multipart =+ "split multipart" ~:+ [ ([BodyPart (pack "content-type: text/plain\r\n") (pack "1")], Nothing) @=?+ parseMultipartBody (pack "boundary")+ (pack "--boundary\r\ncontent-type: text/plain\r\n\r\n1\r\n--boundary--\r\nend")++ , ([BodyPart (pack "content-type: text/plain\r\n") (pack "1")], Nothing) @=?+ parseMultipartBody (pack "boundary.with.dot")+ (pack "--boundary.with.dot\r\ncontent-type: text/plain\r\n\r\n1\r\n--boundary.with.dot--\r\nend")++ , ([BodyPart (pack "content-type: text/plain\r\n") (pack "1")], Nothing) @=?+ parseMultipartBody (pack "boundary")+ (pack "beg\r\n--boundary\r\ncontent-type: text/plain\r\n\r\n1\r\n--boundary--\r\nend")++ , ([BodyPart (pack "content-type: text/plain\r\n") (pack "1\n")], Nothing) @=?+ parseMultipartBody (pack "boundary")+ (pack "beg\r\n--boundary\r\ncontent-type: text/plain\r\n\r\n1\n\r\n--boundary--\r\nend")++ , ([BodyPart (pack "content-type: text/plain\r\n") (pack "1\r\n")], Nothing) @=?+ parseMultipartBody (pack "boundary")+ (pack "beg\r\n--boundary\r\ncontent-type: text/plain\r\n\r\n1\r\n\r\n--boundary--\r\nend")+ , ([BodyPart (pack "content-type: text/plain\r\n") (pack "1\n\r")], Nothing) @=?+ parseMultipartBody (pack "boundary")+ (pack "beg\r\n--boundary\r\ncontent-type: text/plain\r\n\r\n1\n\r\r\n--boundary--\r\nend")++ , ([BodyPart (pack "content-type: text/plain\r\n") (pack "\r\n1\n\r")], Nothing) @=?+ parseMultipartBody (pack "boundary")+ (pack "beg\r\n--boundary\r\ncontent-type: text/plain\r\n\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)+ , rqSecure = False+ }++compressPart :: ServerPart Response+compressPart =+ do void compressedResponseFilter+ composeFilter noContentLength+ msum [ dir "response" $ ok (toResponse "compress Response")+ , dir "sendfile" $ ok (sendFileResponse "text/plain" "/dev/null" Nothing 0 100)+ ]++cookieHeaderOrderTestHandler :: ServerPart Response+cookieHeaderOrderTestHandler = do+ expireCookie "thecookie"+ addCookie Session $ mkCookie "thecookie" "value"+ ok $ toResponse $ "works"++cookieHeaderOrderTest :: Test+cookieHeaderOrderTest =+ "cookie header order test" ~:+ do req <- mkRequest GET "/" [] Map.empty L.empty+ res <- simpleHTTP'' cookieHeaderOrderTestHandler req+ let Just pair = (Map.lookup (B.pack "set-cookie") (rsHeaders res))+ assertEqual "Add cookie wins" False $ B.isInfixOf (B.pack "Max-Age=0")+ (last $ hValue pair)++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++matchMethodTest :: Test+matchMethodTest =+ "matchMethodTest" ~:+ do forM_ gethead $ \m -> matchMethod GET m @?= True+ forM_ others $ \m -> matchMethod GET m @?= False+ forM_ gethead $ \m -> matchMethod [GET] m @?= True+ forM_ others $ \m -> matchMethod [GET] m @?= False+ forM_ gethead $ \m -> matchMethod [GET, HEAD] m @?= True+ forM_ others $ \m -> matchMethod [GET, HEAD] m @?= False+ matchMethod POST GET @?= False+ matchMethod POST HEAD @?= False+ matchMethod POST TRACE @?= False+ matchMethod POST POST @?= True+ matchMethod [POST, PUT] GET @?= False+ matchMethod [POST, PUT] HEAD @?= False+ matchMethod [POST, PUT] TRACE @?= False+ matchMethod [POST, PUT] POST @?= True+ matchMethod [POST, PUT] PUT @?= True+ forM_ (others) $ \m -> matchMethod (`notElem` gethead) m @?= True+ forM_ (gethead ++ others) $ \m -> matchMethod () m @?= True+ where+ gethead = [GET, HEAD]+ others = [POST, PUT, DELETE, TRACE, OPTIONS, CONNECT]+++-- | https://github.com/Happstack/happstack-server/pull/56+pContentDispositionFilename :: Test+pContentDispositionFilename =+ "pContentDispositionFilename" ~:+ do let doesNotWorkWithOldParserButWithNew = "form-data; filename=\"file.pdf\"; name=\"file\"" :: String+ c <- parseContentDisposition doesNotWorkWithOldParserButWithNew+ assertEqual "parseContentDisposition" c (ContentDisposition "form-data" [("filename","file.pdf"),("name","file")])++applicativeTest :: Test+applicativeTest =+ "applicativeTest" ~:+ do req <- mkRequest GET "/response" [] mempty L.empty+ res <- flip simpleHTTP'' req $ do+ Left errors <- getDataFn $ (++) <$> look "a" <*> look "b"+ pure $ intercalate "," errors+ let ref = "Parameter not found: a,Parameter not found: b"+ assertEqual "getDataFn/ReaderError doesn't short-circuit" ref (unpack (rsBody res))
tests/Test.hs view
@@ -16,4 +16,4 @@ return c case (failures c) + (errors c) of 0 -> return ()- n -> exitFailure+ _ -> exitFailure