diff --git a/wai-extra/.gitignore b/wai-extra/.gitignore new file mode 100644 index 000000000..019dac95d --- /dev/null +++ b/wai-extra/.gitignore @@ -0,0 +1,2 @@ +*.swp +dist diff --git a/wai-extra/LICENSE b/wai-extra/LICENSE new file mode 100644 index 000000000..8643e5d8b --- /dev/null +++ b/wai-extra/LICENSE @@ -0,0 +1,25 @@ +The following license covers this documentation, and the source code, except +where otherwise indicated. + +Copyright 2010, Michael Snoyman. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/wai-extra/Network/Wai/Handler/CGI.hs b/wai-extra/Network/Wai/Handler/CGI.hs new file mode 100755 index 000000000..ae8061057 --- /dev/null +++ b/wai-extra/Network/Wai/Handler/CGI.hs @@ -0,0 +1,178 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +-- | Backend for Common Gateway Interface. Almost all users should use the +-- 'run' function. +module Network.Wai.Handler.CGI + ( run + , runSendfile + , runGeneric + , requestBodyFunc + ) where + +import Network.Wai +import Network.Socket (getAddrInfo, addrAddress) +import System.Environment (getEnvironment) +import Data.Maybe (fromMaybe) +import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy as L +import Control.Arrow ((***)) +import Data.Char (toLower) +import qualified System.IO +import qualified Data.String as String +import Data.Enumerator + ( Enumerator, Step (..), Stream (..), continue, yield + , enumList, ($$), joinI, returnI, (>>==), run_ + ) +import Data.Monoid (mconcat) +import Blaze.ByteString.Builder (fromByteString, toLazyByteString) +import Blaze.ByteString.Builder.Char8 (fromChar, fromString) +import Blaze.ByteString.Builder.Enumerator (builderToByteString) +import Control.Monad.IO.Class (liftIO) +import Data.ByteString.Lazy.Internal (defaultChunkSize) +import System.IO (Handle) +import Network.HTTP.Types (Status (..)) +import qualified Network.HTTP.Types as H +import qualified Data.CaseInsensitive as CI + +safeRead :: Read a => a -> String -> a +safeRead d s = + case reads s of + ((x, _):_) -> x + [] -> d + +lookup' :: String -> [(String, String)] -> String +lookup' key pairs = fromMaybe "" $ lookup key pairs + +-- | Run an application using CGI. +run :: Application -> IO () +run app = do + vars <- getEnvironment + let input = requestBodyHandle System.IO.stdin + output = B.hPut System.IO.stdout + runGeneric vars input output Nothing app + +-- | Some web servers provide an optimization for sending files via a sendfile +-- system call via a special header. To use this feature, provide that header +-- name here. +runSendfile :: B.ByteString -- ^ sendfile header + -> Application -> IO () +runSendfile sf app = do + vars <- getEnvironment + let input = requestBodyHandle System.IO.stdin + output = B.hPut System.IO.stdout + runGeneric vars input output (Just sf) app + +-- | A generic CGI helper, which allows other backends (FastCGI and SCGI) to +-- use the same code as CGI. Most users will not need this function, and can +-- stick with 'run' or 'runSendfile'. +runGeneric + :: [(String, String)] -- ^ all variables + -> (forall a. Int -> Enumerator B.ByteString IO a) -- ^ responseBody of input + -> (B.ByteString -> IO ()) -- ^ destination for output + -> Maybe B.ByteString -- ^ does the server support the X-Sendfile header? + -> Application + -> IO () +runGeneric vars inputH outputH xsendfile app = do + let rmethod = B.pack $ lookup' "REQUEST_METHOD" vars + pinfo = lookup' "PATH_INFO" vars + qstring = lookup' "QUERY_STRING" vars + servername = lookup' "SERVER_NAME" vars + serverport = safeRead 80 $ lookup' "SERVER_PORT" vars + contentLength = safeRead 0 $ lookup' "CONTENT_LENGTH" vars + remoteHost' = + case lookup "REMOTE_ADDR" vars of + Just x -> x + Nothing -> + case lookup "REMOTE_HOST" vars of + Just x -> x + Nothing -> "" + isSecure' = + case map toLower $ lookup' "SERVER_PROTOCOL" vars of + "https" -> True + _ -> False + addrs <- getAddrInfo Nothing (Just remoteHost') Nothing + let addr = + case addrs of + a:_ -> addrAddress a + [] -> error $ "Invalid REMOTE_ADDR or REMOTE_HOST: " ++ remoteHost' + let env = Request + { requestMethod = rmethod + , rawPathInfo = B.pack pinfo + , pathInfo = H.decodePathSegments $ B.pack pinfo + , rawQueryString = B.pack qstring + , queryString = H.parseQuery $ B.pack qstring + , serverName = B.pack servername + , serverPort = serverport + , requestHeaders = map (cleanupVarName *** B.pack) vars + , isSecure = isSecure' + , remoteHost = addr + , httpVersion = H.http11 -- FIXME + } + -- FIXME worry about exception? + res <- run_ $ inputH contentLength $$ app env + case (xsendfile, res) of + (Just sf, ResponseFile s hs fp Nothing) -> + mapM_ outputH $ L.toChunks $ toLazyByteString $ sfBuilder s hs sf fp + _ -> responseEnumerator res $ \s hs -> + joinI $ enumList 1 [headers s hs, fromChar '\n'] $$ builderIter + where + headers s hs = mconcat (map header $ status s : map header' (fixHeaders hs)) + status (Status i m) = (fromByteString "Status", mconcat + [ fromString $ show i + , fromChar ' ' + , fromByteString m + ]) + header' (x, y) = (fromByteString $ CI.original x, fromByteString y) + header (x, y) = mconcat + [ x + , fromByteString ": " + , y + , fromChar '\n' + ] + sfBuilder s hs sf fp = mconcat + [ headers s hs + , header $ (fromByteString sf, fromString fp) + , fromChar '\n' + , fromByteString sf + , fromByteString " not supported" + ] + bsStep = Continue bsStep' + bsStep' EOF = yield () EOF + bsStep' (Chunks []) = continue bsStep' + bsStep' (Chunks bss) = liftIO (mapM_ outputH bss) >> continue bsStep' + builderIter = builderToByteString bsStep + fixHeaders h = + case lookup "content-type" h of + Nothing -> ("Content-Type", "text/html; charset=utf-8") : h + Just _ -> h + +cleanupVarName :: String -> CI.CI B.ByteString +cleanupVarName ('H':'T':'T':'P':'_':a:as) = + String.fromString $ a : helper' as + where + helper' ('_':x:rest) = '-' : x : helper' rest + helper' (x:rest) = toLower x : helper' rest + helper' [] = [] +cleanupVarName "CONTENT_TYPE" = "Content-Type" +cleanupVarName "CONTENT_LENGTH" = "Content-Length" +cleanupVarName "SCRIPT_NAME" = "CGI-Script-Name" +cleanupVarName x = String.fromString x -- FIXME remove? + +requestBodyHandle :: Handle -> Int -> Enumerator B.ByteString IO a +requestBodyHandle h = + requestBodyFunc go + where + go i = Just `fmap` B.hGet h (min i defaultChunkSize) + +requestBodyFunc :: (Int -> IO (Maybe B.ByteString)) + -> Int + -> Enumerator B.ByteString IO a +requestBodyFunc _ 0 step = returnI step +requestBodyFunc h len (Continue k) = do + mbs <- liftIO $ h len + case mbs of + Nothing -> continue k + Just bs -> do + let newLen = len - B.length bs + k (Chunks [bs]) >>== requestBodyFunc h newLen +requestBodyFunc _ _ step = returnI step diff --git a/wai-extra/Network/Wai/Middleware/AcceptOverride.hs b/wai-extra/Network/Wai/Middleware/AcceptOverride.hs new file mode 100644 index 000000000..7b2e6a4d6 --- /dev/null +++ b/wai-extra/Network/Wai/Middleware/AcceptOverride.hs @@ -0,0 +1,26 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Network.Wai.Middleware.AcceptOverride + ( acceptOverride + ) where + +import Network.Wai +import Control.Monad (join) +import Data.ByteString (ByteString) + +acceptOverride :: Middleware +acceptOverride app req = + app req' + where + req' = + case join $ lookup "_accept" $ queryString req of + Nothing -> req + Just a -> req { requestHeaders = changeVal "Accept" a $ requestHeaders req} + +changeVal :: Eq a + => a + -> ByteString + -> [(a, ByteString)] + -> [(a, ByteString)] +changeVal key val old = (key, val) + : filter (\(k, _) -> k /= key) old diff --git a/wai-extra/Network/Wai/Middleware/Autohead.hs b/wai-extra/Network/Wai/Middleware/Autohead.hs new file mode 100644 index 000000000..d28cf5906 --- /dev/null +++ b/wai-extra/Network/Wai/Middleware/Autohead.hs @@ -0,0 +1,23 @@ +{-# LANGUAGE OverloadedStrings #-} +-- | Automatically produce responses to HEAD requests based on the underlying +-- applications GET response. +module Network.Wai.Middleware.Autohead (autohead) where + +import Network.Wai +import Data.Monoid (mempty) +import Data.Enumerator (enumEOF, ($$)) + +autohead :: Middleware +autohead app req + | requestMethod req == "HEAD" = do + res <- app req { requestMethod = "GET" } + case res of + ResponseFile s hs _ _ -> return $ ResponseBuilder s hs mempty + ResponseBuilder s hs _ -> return $ ResponseBuilder s hs mempty + ResponseEnumerator e -> do + let helper f = + let helper' s hs = enumEOF $$ f s hs + in e helper' + return $ ResponseEnumerator helper + | otherwise = app req + diff --git a/wai-extra/Network/Wai/Middleware/CleanPath.hs b/wai-extra/Network/Wai/Middleware/CleanPath.hs new file mode 100644 index 000000000..00d907502 --- /dev/null +++ b/wai-extra/Network/Wai/Middleware/CleanPath.hs @@ -0,0 +1,30 @@ +{-# LANGUAGE OverloadedStrings #-} +module Network.Wai.Middleware.CleanPath + ( cleanPath + ) where + +import Network.Wai +import qualified Data.ByteString.Char8 as B +import qualified Data.ByteString.Lazy as L +import Network.HTTP.Types (status301) +import Data.Text (Text) +import Data.Monoid (mconcat) + +cleanPath :: ([Text] -> Either B.ByteString [Text]) + -> B.ByteString + -> ([Text] -> Application) + -> Application +cleanPath splitter prefix app env = + case splitter $ pathInfo env of + Right pieces -> app pieces env + Left p -> return + $ responseLBS status301 + [("Location", mconcat [prefix, p, suffix])] + $ L.empty + where + -- include the query string if present + suffix = + case B.uncons $ rawQueryString env of + Nothing -> B.empty + Just ('?', _) -> rawQueryString env + _ -> B.cons '?' $ rawQueryString env diff --git a/wai-extra/Network/Wai/Middleware/Debug.hs b/wai-extra/Network/Wai/Middleware/Debug.hs new file mode 100644 index 000000000..6fc569597 --- /dev/null +++ b/wai-extra/Network/Wai/Middleware/Debug.hs @@ -0,0 +1,36 @@ +{-# LANGUAGE OverloadedStrings #-} +module Network.Wai.Middleware.Debug + ( debug + , debugDest + ) where + +import Network.Wai (Middleware, requestMethod, requestHeaders, rawPathInfo, rawQueryString) +import Network.Wai.Parse (parseRequestBody, lbsSink, fileName) +import Data.ByteString.Char8 (unpack) +import System.IO (hPutStrLn, stderr) +import Control.Monad.IO.Class (liftIO) +import qualified Data.Text.Lazy as T +import Data.Enumerator (run_, ($$), enumList) +import Data.Enumerator.List (consume) + +-- | Prints a message to 'stderr' for each request. +debug :: Middleware +debug = debugDest $ hPutStrLn stderr . T.unpack + +-- | Prints a message using the given callback function for each request. +debugDest :: (T.Text -> IO ()) -> Middleware +debugDest cb app req = do + body <- consume + (params, files) <- liftIO $ run_ $ enumList 1 body $$ parseRequestBody lbsSink req + liftIO $ cb $ T.pack $ concat + [ unpack $ requestMethod req + , " " + , unpack $ rawPathInfo req + , unpack $ rawQueryString req + , "\n" + , (++) "Accept: " $ maybe "" unpack $ lookup "Accept" $ requestHeaders req + , "\n" + , if null params then "" else "Post parameters: " ++ show params ++ "\n" + , if null files then "" else "Post file names: " ++ show (map (fileName . snd) files) ++ "\n" + ] + liftIO $ run_ $ enumList 1 body $$ app req diff --git a/wai-extra/Network/Wai/Middleware/Gzip.hs b/wai-extra/Network/Wai/Middleware/Gzip.hs new file mode 100644 index 000000000..8531f6b41 --- /dev/null +++ b/wai-extra/Network/Wai/Middleware/Gzip.hs @@ -0,0 +1,70 @@ +{-# LANGUAGE Rank2Types #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE OverloadedStrings #-} +--------------------------------------------------------- +-- | +-- Module : Network.Wai.Middleware.Gzip +-- Copyright : Michael Snoyman +-- License : BSD3 +-- +-- Maintainer : Michael Snoyman +-- Stability : Unstable +-- Portability : portable +-- +-- Automatic gzip compression of responses. +-- +--------------------------------------------------------- +module Network.Wai.Middleware.Gzip (gzip) where + +import Network.Wai +import Network.Wai.Zlib +import Data.Maybe (fromMaybe) +import Data.Enumerator (($$), joinI) +import qualified Data.ByteString.Char8 as S8 +import qualified Data.ByteString as S + +-- | Use gzip to compress the body of the response. +-- +-- Analyzes the \"Accept-Encoding\" header from the client to determine +-- if gzip is supported. +-- +-- Possible future enhancements: +-- +-- * Only compress if the response is above a certain size. +-- +-- * I've read that IE can\'t support compression for Javascript files. +gzip :: Bool -- ^ should we gzip files? + -> Middleware +gzip files app env = do + res <- app env + return $ + case res of + ResponseFile{} | not files -> res + _ -> if "gzip" `elem` enc && not isMSIE6 + then ResponseEnumerator $ compressE $ responseEnumerator res + else res + where + enc = fromMaybe [] $ (splitCommas . S8.unpack) + `fmap` lookup "Accept-Encoding" (requestHeaders env) + ua = fromMaybe "" $ lookup "user-agent" $ requestHeaders env + isMSIE6 = "MSIE 6" `S.isInfixOf` ua + +compressE :: (forall a. ResponseEnumerator a) + -> (forall a. ResponseEnumerator a) +compressE re f = + re f' + --e s hs' + where + f' s hs = + joinI $ compress $$ f s hs' + where + -- Remove Content-Length header, since we will certainly have a + -- different length after gzip compression. + hs' = ("Content-Encoding", "gzip") : filter notLength hs + notLength (x, _) = x /= "content-length" + +splitCommas :: String -> [String] +splitCommas [] = [] +splitCommas x = + let (y, z) = break (== ',') x + in y : splitCommas (dropWhile (== ' ') $ drop 1 z) diff --git a/wai-extra/Network/Wai/Middleware/Jsonp.hs b/wai-extra/Network/Wai/Middleware/Jsonp.hs new file mode 100644 index 000000000..757545165 --- /dev/null +++ b/wai-extra/Network/Wai/Middleware/Jsonp.hs @@ -0,0 +1,106 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RankNTypes #-} +--------------------------------------------------------- +-- | +-- Module : Network.Wai.Middleware.Jsonp +-- Copyright : Michael Snoyman +-- License : BSD3 +-- +-- Maintainer : Michael Snoyman +-- Stability : Unstable +-- Portability : portable +-- +-- Automatic wrapping of JSON responses to convert into JSONP. +-- +--------------------------------------------------------- +module Network.Wai.Middleware.Jsonp (jsonp) where + +import Network.Wai +import Data.ByteString (ByteString) +import qualified Data.ByteString.Char8 as B8 +import Data.Enumerator (($$), enumList, Step (..), Enumerator, Iteratee, Enumeratee, joinI, checkDone, continue, Stream (..), (>>==)) +import Blaze.ByteString.Builder (copyByteString, Builder) +import Blaze.ByteString.Builder.Char8 (fromChar) +import Data.Monoid (mappend) +import Control.Monad (join) +import Data.Maybe (fromMaybe) +import qualified Data.ByteString as S + +-- | Wrap json responses in a jsonp callback. +-- +-- Basically, if the user requested a \"text\/javascript\" and supplied a +-- \"callback\" GET parameter, ask the application for an +-- \"application/json\" response, then convern that into a JSONP response, +-- having a content type of \"text\/javascript\" and calling the specified +-- callback function. +jsonp :: Middleware +jsonp app env = do + let accept = fromMaybe B8.empty $ lookup "Accept" $ requestHeaders env + let callback :: Maybe B8.ByteString + callback = + if B8.pack "text/javascript" `B8.isInfixOf` accept + then join $ lookup "callback" $ queryString env + else Nothing + let env' = + case callback of + Nothing -> env + Just _ -> env + { requestHeaders = changeVal "Accept" + "application/json" + $ requestHeaders env + } + res <- app env' + case callback of + Nothing -> return res + Just c -> go c res + where + go c r@(ResponseFile _ hs _ _) = go' c r hs + go c r@(ResponseBuilder s hs b) = + case checkJSON hs of + Nothing -> return r + Just hs' -> return $ ResponseBuilder s hs' $ + copyByteString c + `mappend` fromChar '(' + `mappend` b + `mappend` fromChar ')' + go c (ResponseEnumerator e) = addCallback c e + go' c r hs = + case checkJSON hs of + Just _ -> addCallback c $ responseEnumerator r + Nothing -> return r + checkJSON hs = + case lookup "Content-Type" hs of + Just x + | B8.pack "application/json" `S.isPrefixOf` x -> Just $ fixHeaders hs + _ -> Nothing + fixHeaders = changeVal "Content-Type" "text/javascript" + addCallback :: B8.ByteString -> (forall a. ResponseEnumerator a) + -> Iteratee B8.ByteString IO Response + addCallback cb e = + return $ ResponseEnumerator $ helper + where + helper f = + e helper' + where + helper' s hs = + case checkJSON hs of + Just hs' -> wrap $$ f s hs' + Nothing -> f s hs + wrap :: Step Builder IO b -> Iteratee Builder IO b + wrap step = joinI $ after (enumList 1 [fromChar ')']) + $$ enumList 1 [copyByteString cb, fromChar '('] step + after :: Enumerator Builder IO b -> Enumeratee Builder Builder IO b + after enum = + loop + where + loop = checkDone $ continue . step + step k EOF = enum (Continue k) >>== return + step k s = k s >>== loop + +changeVal :: Eq a + => a + -> ByteString + -> [(a, ByteString)] + -> [(a, ByteString)] +changeVal key val old = (key, val) + : filter (\(k, _) -> k /= key) old diff --git a/wai-extra/Network/Wai/Middleware/MethodOverride.hs b/wai-extra/Network/Wai/Middleware/MethodOverride.hs new file mode 100644 index 000000000..f1bb948dc --- /dev/null +++ b/wai-extra/Network/Wai/Middleware/MethodOverride.hs @@ -0,0 +1,16 @@ +{-# LANGUAGE OverloadedStrings #-} +module Network.Wai.Middleware.MethodOverride + ( methodOverride + ) where + +import Network.Wai +import Control.Monad (join) + +methodOverride :: Middleware +methodOverride app req = + app req' + where + req' = + case join $ lookup "_method" $ queryString req of + Nothing -> req + Just m -> req { requestMethod = m } diff --git a/wai-extra/Network/Wai/Middleware/Rewrite.hs b/wai-extra/Network/Wai/Middleware/Rewrite.hs new file mode 100644 index 000000000..069b827e9 --- /dev/null +++ b/wai-extra/Network/Wai/Middleware/Rewrite.hs @@ -0,0 +1,33 @@ +{-# LANGUAGE OverloadedStrings #-} +module Network.Wai.Middleware.Rewrite + ( rewrite, autoHtmlRewrite + ) where + +import Network.Wai +import System.Directory (doesFileExist) +import Control.Monad.IO.Class (liftIO) +import Data.Text (Text, unpack, pack) + +-- | rewrite based on your own conversion rules +-- Example usage: rewrite (autoHtmlRewrite "static") +rewrite :: ([Text] -> IO [Text]) -> Middleware +rewrite convert app req = do + newPathInfo <- liftIO $ convert $ pathInfo req + app req { pathInfo = newPathInfo } + +-- | example rewriter +-- implements 2 rules for static html re-writes +-- 1) for a directory foo/, check for foo/index.html +-- 2) for a non-directory bar, check for bar.html +-- if the file exists, do the rewrite +autoHtmlRewrite :: String -> [Text] -> IO [Text] +autoHtmlRewrite staticDir pieces' = do + fe <- doesFileExist $ staticDir ++ "/" ++ reWritePath + return $ if fe then map pack reWritePieces else pieces' + where + pieces = map unpack pieces' + reWritePath = concat $ map ((:) '/') reWritePieces + reWritePieces = + if (null pieces) || (null $ last pieces) + then pieces ++ ["index.html"] + else (init pieces) ++ [(last pieces) ++ ".html"] diff --git a/wai-extra/Network/Wai/Middleware/Vhost.hs b/wai-extra/Network/Wai/Middleware/Vhost.hs new file mode 100644 index 000000000..5e3db93cf --- /dev/null +++ b/wai-extra/Network/Wai/Middleware/Vhost.hs @@ -0,0 +1,9 @@ +module Network.Wai.Middleware.Vhost (vhost) where + +import Network.Wai + +vhost :: [(Request -> Bool, Application)] -> Application -> Application +vhost vhosts def req = + case filter (\(b, _) -> b req) vhosts of + [] -> def req + (_, app):_ -> app req diff --git a/wai-extra/Network/Wai/Parse.hs b/wai-extra/Network/Wai/Parse.hs new file mode 100644 index 000000000..0bc2c50be --- /dev/null +++ b/wai-extra/Network/Wai/Parse.hs @@ -0,0 +1,342 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE OverloadedStrings #-} +-- | Some helpers for parsing data out of a raw WAI 'Request'. + +module Network.Wai.Parse + ( parseHttpAccept + , parseRequestBody + , Sink (..) + , lbsSink + , tempFileSink + , FileInfo (..) +#if TEST + , Bound (..) + , findBound + , sinkTillBound + , killCR + , killCRLF + , takeLine +#endif + ) where + +import qualified Data.ByteString as S +import qualified Data.ByteString.Lazy as L +import qualified Data.ByteString.Char8 as S8 +import Data.Word (Word8) +import Data.Bits +import Data.Maybe (fromMaybe) +import Data.List (sortBy) +import Data.Function (on) +import System.Directory (removeFile, getTemporaryDirectory) +import System.IO (hClose, openBinaryTempFile, Handle) +import Network.Wai +import Data.Enumerator (Iteratee, yield) +import qualified Data.Enumerator as E +import qualified Data.Enumerator.List as EL +import Control.Monad.IO.Class (liftIO) +import qualified Network.HTTP.Types as H + +uncons :: S.ByteString -> Maybe (Word8, S.ByteString) +uncons s + | S.null s = Nothing + | otherwise = Just (S.head s, S.tail s) + +breakDiscard :: Word8 -> S.ByteString -> (S.ByteString, S.ByteString) +breakDiscard w s = + let (x, y) = S.break (== w) s + in (x, S.drop 1 y) + +qsDecode :: S.ByteString -> S.ByteString +qsDecode z = fst $ S.unfoldrN (S.length z) go z + where + go bs = + case uncons bs of + Nothing -> Nothing + Just (43, ws) -> Just (32, ws) -- plus to space + Just (37, ws) -> Just $ fromMaybe (37, ws) $ do -- percent + (x, xs) <- uncons ws + x' <- hexVal x + (y, ys) <- uncons xs + y' <- hexVal y + Just $ (combine x' y', ys) + Just (w, ws) -> Just (w, ws) + hexVal w + | 48 <= w && w <= 57 = Just $ w - 48 -- 0 - 9 + | 65 <= w && w <= 70 = Just $ w - 55 -- A - F + | 97 <= w && w <= 102 = Just $ w - 87 -- a - f + | otherwise = Nothing + combine :: Word8 -> Word8 -> Word8 + combine a b = shiftL a 4 .|. b + +-- | Parse the HTTP accept string to determine supported content types. +parseHttpAccept :: S.ByteString -> [S.ByteString] +parseHttpAccept = map fst + . sortBy (rcompare `on` snd) + . map grabQ + . S.split 44 -- comma + where + rcompare :: Double -> Double -> Ordering + rcompare = flip compare + grabQ s = + let (s', q) = breakDiscard 59 s -- semicolon + (_, q') = breakDiscard 61 q -- equals sign + in (trimWhite s', readQ $ trimWhite q') + readQ s = case reads $ S8.unpack s of + (x, _):_ -> x + _ -> 1.0 + trimWhite = S.dropWhile (== 32) -- space + +-- | A destination for data, the opposite of a 'Source'. +data Sink x y = Sink + { sinkInit :: IO x + , sinkAppend :: x -> S.ByteString -> IO x + , sinkClose :: x -> IO y + , sinkFinalize :: y -> IO () + } + +lbsSink :: Sink ([S.ByteString] -> [S.ByteString]) L.ByteString +lbsSink = Sink + { sinkInit = return id + , sinkAppend = \front bs -> return $ front . (:) bs + , sinkClose = \front -> return $ L.fromChunks $ front [] + , sinkFinalize = \_ -> return () + } + +tempFileSink :: Sink (FilePath, Handle) FilePath +tempFileSink = Sink + { sinkInit = do + tempDir <- getTemporaryDirectory + openBinaryTempFile tempDir "webenc.buf" + , sinkAppend = \(fp, h) bs -> S.hPut h bs >> return (fp, h) + , sinkClose = \(fp, h) -> do + hClose h + return fp + , sinkFinalize = \fp -> removeFile fp + } + +-- | Information on an uploaded file. +data FileInfo c = FileInfo + { fileName :: S.ByteString + , fileContentType :: S.ByteString + , fileContent :: c + } + deriving (Eq, Show) + +type Param = (S.ByteString, S.ByteString) +type File y = (S.ByteString, FileInfo y) + +parseRequestBody :: Sink x y + -> Request + -> Iteratee S.ByteString IO ([Param], [File y]) +parseRequestBody sink req = do + case ctype of + Nothing -> return ([], []) + Just Nothing -> do -- url-encoded + -- NOTE: in general, url-encoded data will be in a single chunk. + -- Therefore, I'm optimizing for the usual case by sticking with + -- strict byte strings here. + bs <- EL.consume + return (H.parseSimpleQuery $ S.concat bs, []) + Just (Just bound) -> -- multi-part + let bound'' = S8.pack "--" `S.append` bound + in parsePieces sink bound'' + where + urlenc = S8.pack "application/x-www-form-urlencoded" + formBound = S8.pack "multipart/form-data;" + bound' = "boundary=" + boundary s = + if "multipart/form-data;" `S.isPrefixOf` s + then + let s' = S.dropWhile (== 32) $ S.drop (S.length formBound) s + in if bound' `S.isPrefixOf` s' + then Just $ S.drop (S.length bound') s' + else Nothing + else Nothing + ctype = do + ctype' <- lookup "Content-Type" $ requestHeaders req + if urlenc `S.isPrefixOf` ctype' + then Just Nothing + else case boundary ctype' of + Just x -> Just $ Just x + Nothing -> Nothing + +takeLine :: Iteratee S.ByteString IO (Maybe S.ByteString) +takeLine = do + mbs <- EL.head + case mbs of + Nothing -> return Nothing + Just bs -> + let (x, y) = S.break (== 10) bs -- LF + in if S.null y + then do + x' <- takeLine + case x' of + Nothing -> return $ Just $ killCR x + Just x'' -> return $ Just $ killCR $ S.append x x'' + else do + E.yield () $ E.Chunks [S.drop 1 y] + return $ Just $ killCR x + +takeLines :: Iteratee S.ByteString IO [S.ByteString] +takeLines = do + res <- takeLine + case res of + Nothing -> return [] + Just l + | S.null l -> return [] + | otherwise -> do + ls <- takeLines + return $ l : ls + +parsePieces :: Sink x y -> S.ByteString + -> Iteratee S.ByteString IO ([Param], [File y]) +parsePieces sink bound = do + _boundLine <- takeLine + res' <- takeLines + case res' of + [] -> return ([], []) + _ -> do + let ls' = map parsePair res' + let x = do + cd <- lookup contDisp ls' + let ct = lookup contType ls' + let attrs = parseAttrs cd + name <- lookup "name" attrs + return (ct, name, lookup "filename" attrs) + case x of + Just (mct, name, Just filename) -> do + let ct = fromMaybe "application/octet-stream" mct + seed <- liftIO $ sinkInit sink + (seed', wasFound) <- + sinkTillBound bound (sinkAppend sink) seed + y <- liftIO $ sinkClose sink seed' + let fi = FileInfo filename ct y + let y' = (name, fi) + (xs, ys) <- + if wasFound + then parsePieces sink bound + else return ([], []) + return (xs, y' : ys) + Just (_ct, name, Nothing) -> do + let seed = id + let iter front bs = return $ front . (:) bs + (front, wasFound) <- + sinkTillBound bound iter seed + let bs = S.concat $ front [] + let x' = (name, qsDecode bs) + (xs, ys) <- + if wasFound + then parsePieces sink bound + else return ([], []) + return (x' : xs, ys) + _ -> do + -- ignore this part + let seed = () + iter () _ = return () + ((), wasFound) <- sinkTillBound bound iter seed + if wasFound + then parsePieces sink bound + else return ([], []) + where + contDisp = S8.pack "Content-Disposition" + contType = S8.pack "Content-Type" + parsePair s = + let (x, y) = breakDiscard 58 s -- colon + in (x, S.dropWhile (== 32) y) -- space + +data Bound = FoundBound S.ByteString S.ByteString + | NoBound + | PartialBound + deriving (Eq, Show) + +findBound :: S.ByteString -> S.ByteString -> Bound +findBound b bs = go [0..S.length bs - 1] + where + go [] = NoBound + go (i:is) + | mismatch [0..S.length b - 1] [i..S.length bs - 1] = go is + | otherwise = + let endI = i + S.length b + in if endI > S.length bs + then PartialBound + else FoundBound (S.take i bs) (S.drop endI bs) + mismatch [] _ = False + mismatch _ [] = False + mismatch (x:xs) (y:ys) + | S.index b x == S.index bs y = mismatch xs ys + | otherwise = True + +sinkTillBound :: S.ByteString + -> (x -> S.ByteString -> IO x) + -> x + -> Iteratee S.ByteString IO (x, Bool) +sinkTillBound bound iter seed = do + mbs <- EL.head + case mbs of + Nothing -> return (seed, False) + Just bs -> go bs + where + go bs = + case findBound bound bs of + FoundBound before after -> do + let before' = killCRLF before + seed' <- liftIO $ iter seed before' + yield () $ E.Chunks [after] + return (seed', True) + PartialBound -> do + mbs <- EL.head + case mbs of + Nothing -> do + seed' <- liftIO $ iter seed bs + return (seed', False) + Just bs2 -> do + let bs' = bs `S.append` bs2 + yield () $ E.Chunks [bs'] + sinkTillBound bound iter seed + NoBound -> do + mbs <- EL.head + case mbs of + Nothing -> do + seed' <- liftIO $ iter seed bs + sinkTillBound bound iter seed' + Just bs' -> do + -- this funny bit is to catch when there's a + -- newline at the end of the previous chunk + (seed', bs'') <- + if not (S8.null bs) && S8.last bs `elem` "\n\r" + then do + let (front, back) = + S.splitAt (S.length bs - 2) bs + seed' <- liftIO $ iter seed front + return (seed', back `S.append` bs') + else do + seed' <- liftIO $ iter seed bs + return (seed', bs') + yield () $ E.Chunks [bs''] + sinkTillBound bound iter seed' + {- + NoBound -> do + case msrc of + Just (bs', src') -> do + -} + +parseAttrs :: S.ByteString -> [(S.ByteString, S.ByteString)] +parseAttrs = map go . S.split 59 -- semicolon + where + tw = S.dropWhile (== 32) -- space + dq s = if S.length s > 2 && S.head s == 34 && S.last s == 34 -- quote + then S.tail $ S.init s + else s + go s = + let (x, y) = breakDiscard 61 s -- equals sign + in (tw x, dq $ tw y) + +killCRLF :: S.ByteString -> S.ByteString +killCRLF bs + | S.null bs || S.last bs /= 10 = bs -- line feed + | otherwise = killCR $ S.init bs + +killCR :: S.ByteString -> S.ByteString +killCR bs + | S.null bs || S.last bs /= 13 = bs -- carriage return + | otherwise = S.init bs diff --git a/wai-extra/Network/Wai/Zlib.hs b/wai-extra/Network/Wai/Zlib.hs new file mode 100644 index 000000000..f0ecbdf99 --- /dev/null +++ b/wai-extra/Network/Wai/Zlib.hs @@ -0,0 +1,41 @@ +module Network.Wai.Zlib (compress) where + +import Prelude hiding (head) +import Data.Enumerator + ( Enumeratee, checkDone, Stream (..) + , (>>==), ($$), joinI + ) +import Data.Enumerator.List (head) +import Blaze.ByteString.Builder (Builder, fromByteString) +import Blaze.ByteString.Builder.Enumerator (builderToByteString) +import Control.Monad.IO.Class (liftIO) + +import Codec.Zlib + +-- Note: this function really should return a stream of ByteStrings, but the +-- WAI protocol needs Builders anyway. +compress :: Enumeratee Builder Builder IO a +compress step0 = joinI $ builderToByteString $$ do + def <- liftIO $ initDeflate 7 $ WindowBits 31 + loop def step0 + where + loop def = checkDone $ step def + step def k = do + minput <- head + case minput of + Nothing -> do + bss <- liftIO $ finishDeflate def drain + k (Chunks bss) >>== return + Just input -> do + bss <- liftIO $ withDeflateInput def input drain + case bss of + [] -> step def k + _ -> k (Chunks bss) >>== loop def + drain = + go id + where + go front mbs' = do + mbs <- mbs' + case mbs of + Nothing -> return $ map fromByteString $ front [] + Just bs -> go (front . (:) bs) mbs' diff --git a/wai-extra/README b/wai-extra/README new file mode 100644 index 000000000..59ce147cf --- /dev/null +++ b/wai-extra/README @@ -0,0 +1 @@ +Provides some basic WAI handlers and middleware. diff --git a/wai-extra/Setup.lhs b/wai-extra/Setup.lhs new file mode 100755 index 000000000..06e2708f2 --- /dev/null +++ b/wai-extra/Setup.lhs @@ -0,0 +1,7 @@ +#!/usr/bin/env runhaskell + +> module Main where +> import Distribution.Simple + +> main :: IO () +> main = defaultMain diff --git a/wai-extra/proxy.hs b/wai-extra/proxy.hs new file mode 100644 index 000000000..923151cec --- /dev/null +++ b/wai-extra/proxy.hs @@ -0,0 +1,25 @@ +{-# LANGUAGE OverloadedStrings #-} +import qualified Network.HTTP.Enumerator as H +import qualified Network.Wai as W +import Network.Wai.Middleware.Gzip (gzip) +import Network.Wai.Handler.SimpleServer (run) +import qualified Data.ByteString.Char8 as S8 +import qualified Data.ByteString.Lazy.Char8 () +import Data.Enumerator (($$), joinI, run_) +import Blaze.ByteString.Builder (fromByteString) +import qualified Data.Enumerator as E + +main :: IO () +main = run 3000 $ gzip False app + +app :: W.Application +app W.Request { W.pathInfo = path } = + case H.parseUrl $ "http://wiki.yesodweb.com" ++ S8.unpack path of + Nothing -> return notFound + Just hreq -> return $ W.ResponseEnumerator $ run_ . H.http hreq . go + where + go f s h = joinI $ E.map fromByteString $$ f s $ filter safe h + safe (x, _) = not $ x `elem` ["Content-Encoding", "Transfer-Encoding"] + +notFound :: W.Response +notFound = W.ResponseLBS W.status404 [("Content-Type", "text/plain")] "Not found" diff --git a/wai-extra/runtests.hs b/wai-extra/runtests.hs new file mode 100644 index 000000000..215d17845 --- /dev/null +++ b/wai-extra/runtests.hs @@ -0,0 +1,396 @@ +{-# LANGUAGE OverloadedStrings #-} +import Test.Framework (defaultMain, testGroup, Test) +import Test.Framework.Providers.HUnit +import Test.HUnit hiding (Test) + +import Network.Wai +import Network.Wai.Test +import Network.Wai.Parse +import qualified Data.ByteString as S +import qualified Data.ByteString.Char8 as S8 +import qualified Data.ByteString.Lazy.Char8 as L8 +import qualified Data.ByteString.Lazy as L +import Control.Arrow + +import Network.Wai.Middleware.Jsonp +import Network.Wai.Middleware.Gzip +import Network.Wai.Middleware.Vhost +import Network.Wai.Middleware.Autohead +import Network.Wai.Middleware.MethodOverride +import Network.Wai.Middleware.AcceptOverride +import Network.Wai.Middleware.Debug (debug) +import Codec.Compression.GZip (decompress) + +import Data.Enumerator (run_, enumList, ($$), Iteratee) +import Data.Enumerator.Binary (enumFile) +import Control.Monad.IO.Class (liftIO) +import Data.Maybe (fromMaybe) +import Network.HTTP.Types (parseSimpleQuery, status200) + +main :: IO () +main = defaultMain [testSuite] + +testSuite :: Test +testSuite = testGroup "Network.Wai.Parse" + [ testCase "parseQueryString" caseParseQueryString + , testCase "parseQueryString with question mark" caseParseQueryStringQM + , testCase "parseHttpAccept" caseParseHttpAccept + , testCase "parseRequestBody" caseParseRequestBody + , testCase "findBound" caseFindBound + , testCase "sinkTillBound" caseSinkTillBound + , testCase "killCR" caseKillCR + , testCase "killCRLF" caseKillCRLF + , testCase "takeLine" caseTakeLine + , testCase "jsonp" caseJsonp + , testCase "gzip" caseGzip + , testCase "gzip not for MSIE" caseGzipMSIE + , testCase "vhost" caseVhost + , testCase "autohead" caseAutohead + , testCase "method override" caseMethodOverride + , testCase "accept override" caseAcceptOverride + , testCase "dalvik multipart" caseDalvikMultipart + , testCase "debug request body" caseDebugRequestBody + ] + +caseParseQueryString :: Assertion +caseParseQueryString = do + let go l r = + map (S8.pack *** S8.pack) l @=? parseSimpleQuery (S8.pack r) + + go [] "" + go [("foo", "")] "foo" + go [("foo", "bar")] "foo=bar" + go [("foo", "bar"), ("baz", "bin")] "foo=bar&baz=bin" + go [("%Q", "")] "%Q" + go [("%1Q", "")] "%1Q" + go [("%1", "")] "%1" + go [("/", "")] "%2F" + go [("/", "")] "%2f" + go [("foo bar", "")] "foo+bar" + +caseParseQueryStringQM :: Assertion +caseParseQueryStringQM = do + let go l r = + map (S8.pack *** S8.pack) l + @=? parseSimpleQuery (S8.pack $ '?' : r) + + go [] "" + go [("foo", "")] "foo" + go [("foo", "bar")] "foo=bar" + go [("foo", "bar"), ("baz", "bin")] "foo=bar&baz=bin" + go [("%Q", "")] "%Q" + go [("%1Q", "")] "%1Q" + go [("%1", "")] "%1" + go [("/", "")] "%2F" + go [("/", "")] "%2f" + go [("foo bar", "")] "foo+bar" + +caseParseHttpAccept :: Assertion +caseParseHttpAccept = do + let input = "text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c" + expected = ["text/html", "text/x-c", "text/x-dvi", "text/plain"] + expected @=? parseHttpAccept input + +parseRequestBody' :: Sink ([S8.ByteString] -> [S8.ByteString]) L.ByteString + -> SRequest + -> Iteratee S.ByteString IO ([(S.ByteString, S.ByteString)], [(S.ByteString, FileInfo L.ByteString)]) +parseRequestBody' sink (SRequest req bod) = + enumList 1 (L.toChunks bod) $$ parseRequestBody sink req + +caseParseRequestBody :: Assertion +caseParseRequestBody = run_ t where + content2 = S8.pack $ + "--AaB03x\n" ++ + "Content-Disposition: form-data; name=\"document\"; filename=\"b.txt\"\n" ++ + "Content-Type: text/plain; charset=iso-8859-1\n\n" ++ + "This is a file.\n" ++ + "It has two lines.\n" ++ + "--AaB03x\n" ++ + "Content-Disposition: form-data; name=\"title\"\n" ++ + "Content-Type: text/plain; charset=iso-8859-1\n\n" ++ + "A File\n" ++ + "--AaB03x\n" ++ + "Content-Disposition: form-data; name=\"summary\"\n" ++ + "Content-Type: text/plain; charset=iso-8859-1\n\n" ++ + "This is my file\n" ++ + "file test\n" ++ + "--AaB03x--" + content3 = S8.pack "------WebKitFormBoundaryB1pWXPZ6lNr8RiLh\r\nContent-Disposition: form-data; name=\"yaml\"; filename=\"README\"\r\nContent-Type: application/octet-stream\r\n\r\nPhoto blog using Hack.\n\r\n------WebKitFormBoundaryB1pWXPZ6lNr8RiLh--\r\n" + t = do + let content1 = "foo=bar&baz=bin" + let ctype1 = "application/x-www-form-urlencoded" + result1 <- parseRequestBody' lbsSink $ toRequest ctype1 content1 + liftIO $ assertEqual "parsing post x-www-form-urlencoded" + (map (S8.pack *** S8.pack) [("foo", "bar"), ("baz", "bin")], []) + result1 + + let ctype2 = "multipart/form-data; boundary=AaB03x" + result2 <- parseRequestBody' lbsSink $ toRequest ctype2 content2 + let expectedsmap2 = + [ ("title", "A File") + , ("summary", "This is my file\nfile test") + ] + let textPlain = S8.pack $ "text/plain; charset=iso-8859-1" + let expectedfile2 = + [(S8.pack "document", FileInfo (S8.pack "b.txt") textPlain $ L8.pack + "This is a file.\nIt has two lines.")] + let expected2 = (map (S8.pack *** S8.pack) expectedsmap2, expectedfile2) + liftIO $ assertEqual "parsing post multipart/form-data" + expected2 + result2 + + let ctype3 = "multipart/form-data; boundary=----WebKitFormBoundaryB1pWXPZ6lNr8RiLh" + result3 <- parseRequestBody' lbsSink $ toRequest ctype3 content3 + let expectedsmap3 = [] + let expectedfile3 = [(S8.pack "yaml", FileInfo (S8.pack "README") (S8.pack "application/octet-stream") $ + L8.pack "Photo blog using Hack.\n")] + let expected3 = (expectedsmap3, expectedfile3) + liftIO $ assertEqual "parsing actual post multipart/form-data" + expected3 + result3 + + result2' <- parseRequestBody' lbsSink $ toRequest' ctype2 content2 + liftIO $ assertEqual "parsing post multipart/form-data 2" + expected2 + result2' + + result3' <- parseRequestBody' lbsSink $ toRequest' ctype3 content3 + liftIO $ assertEqual "parsing actual post multipart/form-data 2" + expected3 + result3' + +toRequest :: S8.ByteString -> S8.ByteString -> SRequest +toRequest ctype content = SRequest (Request + { requestHeaders = [("Content-Type", ctype)] + , requestMethod = "POST" + , rawPathInfo = "" + , rawQueryString = "" + }) (L.fromChunks [content]) + +toRequest' :: S8.ByteString -> S8.ByteString -> SRequest +toRequest' ctype content = SRequest (Request + { requestHeaders = [("Content-Type", ctype)] + }) (L.fromChunks $ map S.singleton $ S.unpack content) + +caseFindBound :: Assertion +caseFindBound = do + findBound (S8.pack "def") (S8.pack "abcdefghi") @?= + FoundBound (S8.pack "abc") (S8.pack "ghi") + findBound (S8.pack "def") (S8.pack "ABC") @?= NoBound + findBound (S8.pack "def") (S8.pack "abcd") @?= PartialBound + findBound (S8.pack "def") (S8.pack "abcdE") @?= NoBound + findBound (S8.pack "def") (S8.pack "abcdEdef") @?= + FoundBound (S8.pack "abcdE") (S8.pack "") + +caseSinkTillBound :: Assertion +caseSinkTillBound = do + let iter () _ = return () + let src = S8.pack "this is some text" + bound1 = S8.pack "some" + bound2 = S8.pack "some!" + let enum = enumList 1 [src] + let helper _ _ = return () + (_, res1) <- run_ $ enum $$ sinkTillBound bound1 helper () + res1 @?= True + (_, res2) <- run_ $ enum $$ sinkTillBound bound2 helper () + res2 @?= False + +caseKillCR :: Assertion +caseKillCR = do + "foo" @=? killCR "foo" + "foo" @=? killCR "foo\r" + "foo\r\n" @=? killCR "foo\r\n" + "foo\r'" @=? killCR "foo\r'" + +caseKillCRLF :: Assertion +caseKillCRLF = do + "foo" @=? killCRLF "foo" + "foo\r" @=? killCRLF "foo\r" + "foo" @=? killCRLF "foo\r\n" + "foo\r'" @=? killCRLF "foo\r'" + "foo" @=? killCRLF "foo\n" + +caseTakeLine :: Assertion +caseTakeLine = do + helper "foo\nbar\nbaz" "foo" + helper "foo\r\nbar\nbaz" "foo" + helper "foo\nbar\r\nbaz" "foo" + helper "foo\rbar\r\nbaz" "foo\rbar" + where + helper haystack needle = do + x <- run_ $ enumList 1 [haystack] $$ takeLine + Just needle @=? x + +jsonpApp = jsonp $ const $ return $ responseLBS + status200 + [("Content-Type", "application/json")] + "{\"foo\":\"bar\"}" + +caseJsonp :: Assertion +caseJsonp = flip runSession jsonpApp $ do + sres1 <- request Request + { queryString = [("callback", Just "test")] + , requestHeaders = [("Accept", "text/javascript")] + } + assertContentType "text/javascript" sres1 + assertBody "test({\"foo\":\"bar\"})" sres1 + + sres2 <- request Request + { queryString = [("call_back", Just "test")] + , requestHeaders = [("Accept", "text/javascript")] + } + assertContentType "application/json" sres2 + assertBody "{\"foo\":\"bar\"}" sres2 + + sres3 <- request Request + { queryString = [("callback", Just "test")] + , requestHeaders = [("Accept", "text/html")] + } + assertContentType "application/json" sres3 + assertBody "{\"foo\":\"bar\"}" sres3 + +gzipApp = gzip True $ const $ return $ responseLBS status200 [] "test" + +caseGzip :: Assertion +caseGzip = flip runSession gzipApp $ do + sres1 <- request Request + { requestHeaders = [("Accept-Encoding", "gzip")] + } + assertHeader "Content-Encoding" "gzip" sres1 + liftIO $ decompress (simpleBody sres1) @?= "test" + + sres2 <- request Request + { requestHeaders = [] + } + assertNoHeader "Content-Encoding" sres2 + assertBody "test" sres2 + +caseGzipMSIE :: Assertion +caseGzipMSIE = flip runSession gzipApp $ do + sres1 <- request Request + { requestHeaders = + [ ("Accept-Encoding", "gzip") + , ("User-Agent", "Mozilla/4.0 (Windows; MSIE 6.0; Windows NT 6.0)") + ] + } + assertNoHeader "Content-Encoding" sres1 + liftIO $ simpleBody sres1 @?= "test" + +vhostApp1 = const $ return $ responseLBS status200 [] "app1" +vhostApp2 = const $ return $ responseLBS status200 [] "app2" +vhostApp = vhost + [ ((== "foo.com") . serverName, vhostApp1) + ] + vhostApp2 + +caseVhost = flip runSession vhostApp $ do + sres1 <- request Request + { serverName = "foo.com" + } + assertBody "app1" sres1 + + sres2 <- request Request + { serverName = "bar.com" + } + assertBody "app2" sres2 + +autoheadApp = autohead $ const $ return $ responseLBS status200 + [("Foo", "Bar")] "body" + +caseAutohead = flip runSession autoheadApp $ do + sres1 <- request Request + { requestMethod = "GET" + } + assertHeader "Foo" "Bar" sres1 + assertBody "body" sres1 + + sres1 <- request Request + { requestMethod = "HEAD" + } + assertHeader "Foo" "Bar" sres1 + assertBody "" sres1 + +moApp = methodOverride $ \req -> return $ responseLBS status200 + [("Method", requestMethod req)] "" + +caseMethodOverride = flip runSession moApp $ do + sres1 <- request Request + { requestMethod = "GET" + , queryString = [] + } + assertHeader "Method" "GET" sres1 + + sres2 <- request Request + { requestMethod = "POST" + , queryString = [] + } + assertHeader "Method" "POST" sres2 + + sres3 <- request Request + { requestMethod = "POST" + , queryString = [("_method", Just "PUT")] + } + assertHeader "Method" "PUT" sres3 + +aoApp = acceptOverride $ \req -> return $ responseLBS status200 + [("Accept", fromMaybe "" $ lookup "Accept" $ requestHeaders req)] "" + +caseAcceptOverride = flip runSession aoApp $ do + sres1 <- request Request + { queryString = [] + , requestHeaders = [("Accept", "foo")] + } + assertHeader "Accept" "foo" sres1 + + sres2 <- request Request + { queryString = [] + , requestHeaders = [("Accept", "bar")] + } + assertHeader "Accept" "bar" sres2 + + sres3 <- request Request + { queryString = [("_accept", Just "baz")] + , requestHeaders = [("Accept", "bar")] + } + assertHeader "Accept" "baz" sres3 + +caseDalvikMultipart = do + let headers = + [ ("content-length", "12098") + , ("content-type", "multipart/form-data;boundary=*****") + , ("GATEWAY_INTERFACE", "CGI/1.1") + , ("PATH_INFO", "/") + , ("QUERY_STRING", "") + , ("REMOTE_ADDR", "192.168.1.115") + , ("REMOTE_HOST", "ganjizza") + , ("REQUEST_URI", "http://192.168.1.115:3000/") + , ("REQUEST_METHOD", "POST") + , ("HTTP_CONNECTION", "Keep-Alive") + , ("HTTP_COOKIE", "_SESSION=fgUGM5J/k6mGAAW+MMXIJZCJHobw/oEbb6T17KQN0p9yNqiXn/m/ACrsnRjiCEgqtG4fogMUDI+jikoFGcwmPjvuD5d+MDz32iXvDdDJsFdsFMfivuey2H+n6IF6yFGD") + , ("HTTP_USER_AGENT", "Dalvik/1.1.0 (Linux; U; Android 2.1-update1; sdk Build/ECLAIR)") + , ("HTTP_HOST", "192.168.1.115:3000") + , ("HTTP_ACCEPT", "*, */*") + , ("HTTP_VERSION", "HTTP/1.1") + , ("REQUEST_PATH", "/") + ] + let request = Request + { requestHeaders = headers + } + (params, files) <- run_ $ enumFile "test/dalvik-request" $$ parseRequestBody lbsSink request + lookup "scannedTime" params @?= Just "1.298590056748E9" + lookup "geoLong" params @?= Just "0" + lookup "geoLat" params @?= Just "0" + length files @?= 1 + +caseDebugRequestBody :: Assertion +caseDebugRequestBody = flip runSession debugApp $ do + let req = toRequest "application/x-www-form-urlencoded" "foo=bar&baz=bin" + res <- srequest req + assertStatus 200 res + assertHeader "Parsed" (S8.pack $ show ([("foo", "bar"), ("baz", "bin")], [] :: [Int])) res + +debugApp = debug $ \req -> do + x <- parseRequestBody lbsSink req + return $ responseLBS status200 + [ ("Parsed", S8.pack $ show x) + ] "" diff --git a/wai-extra/test/dalvik-request b/wai-extra/test/dalvik-request new file mode 100644 index 000000000..752bf972a Binary files /dev/null and b/wai-extra/test/dalvik-request differ diff --git a/wai-extra/test/json b/wai-extra/test/json new file mode 100644 index 000000000..5f19bf705 --- /dev/null +++ b/wai-extra/test/json @@ -0,0 +1 @@ +{"data":"this is some data"} diff --git a/wai-extra/test/sample.hs b/wai-extra/test/sample.hs new file mode 100644 index 000000000..d45a24872 --- /dev/null +++ b/wai-extra/test/sample.hs @@ -0,0 +1,25 @@ +import Data.ByteString.Char8 (unpack, pack) +import Data.ByteString.Lazy (fromChunks) +import Network.Wai +import Network.Wai.Enumerator +import Network.Wai.Middleware.Gzip +import Network.Wai.Middleware.Jsonp +import Network.Wai.Middleware.CleanPath +import Network.Wai.Handler.SimpleServer + +app :: [String] -> Application +app [] _ = return $ Response Status200 [] $ Right $ fromLBS + $ fromChunks $ flip map [1..10000] $ \i -> pack $ + concat + [ "

Just this same paragraph again. " + , show i + , "

" + ] +app ["test.html"] _ = return $ Response Status200 [] $ Left "test.html" +app ["json"] _ =return $ Response Status200 + [(ContentType, pack "application/json")] + $ Left "json" +app _ _ = return $ Response Status404 [] $ Left "../LICENSE" + +main :: IO () +main = run 3000 $ jsonp $ gzip $ cleanPath app diff --git a/wai-extra/test/test.html b/wai-extra/test/test.html new file mode 100644 index 000000000..bb2170cf0 --- /dev/null +++ b/wai-extra/test/test.html @@ -0,0 +1,17 @@ + + + + + + + + There should be some content loaded below: +
+ + diff --git a/wai-extra/wai-extra.cabal b/wai-extra/wai-extra.cabal new file mode 100644 index 000000000..168774deb --- /dev/null +++ b/wai-extra/wai-extra.cabal @@ -0,0 +1,47 @@ +Name: wai-extra +Version: 0.4.0.3 +Synopsis: Provides some basic WAI handlers and middleware. +Description: The goal here is to provide common features without many dependencies. +License: BSD3 +License-file: LICENSE +Author: Michael Snoyman +Maintainer: michael@snoyman.com +Homepage: http://github.com/snoyberg/wai-extra +Category: Web +Build-Type: Simple +Cabal-Version: >=1.6 +Stability: Stable + +Library + Build-Depends: base >= 3 && < 5, + bytestring >= 0.9 && < 0.10, + wai >= 0.4 && < 0.5, + old-locale >= 1.0 && < 1.1, + time >= 1.1.4 && < 1.3, + network >= 2.2.1.5 && < 2.4, + directory >= 1.0.1 && < 1.2, + zlib-bindings >= 0.0 && < 0.1, + blaze-builder-enumerator >= 0.2 && < 0.3, + transformers >= 0.2 && < 0.3, + enumerator >= 0.4.7 && < 0.5, + blaze-builder >= 0.2.1.3 && < 0.4, + http-types >= 0.6 && < 0.7, + text >= 0.5 && < 1.0, + case-insensitive >= 0.2 && < 0.4 + Exposed-modules: Network.Wai.Handler.CGI + Network.Wai.Middleware.AcceptOverride + Network.Wai.Middleware.Autohead + Network.Wai.Middleware.CleanPath + Network.Wai.Middleware.Debug + Network.Wai.Middleware.Gzip + Network.Wai.Middleware.Jsonp + Network.Wai.Middleware.MethodOverride + Network.Wai.Middleware.Rewrite + Network.Wai.Middleware.Vhost + Network.Wai.Zlib + Network.Wai.Parse + ghc-options: -Wall + +source-repository head + type: git + location: git://github.com/snoyberg/wai-extra.git