snap-core-0.9.8.0/0000755000000000000000000000000012565252520011747 5ustar0000000000000000snap-core-0.9.8.0/snap-core.cabal0000644000000000000000000001347312565252520014632 0ustar0000000000000000name: snap-core version: 0.9.8.0 synopsis: Snap: A Haskell Web Framework (core interfaces and types) description: Snap is a simple and fast web development framework and server written in Haskell. For more information or to download the latest version, you can visit the Snap project website at . . This library contains the core definitions and types for the Snap framework, including: . 1. Primitive types and functions for HTTP (requests, responses, cookies, post/query parameters, etc) . 2. Type aliases and helper functions for Iteratee I/O . 3. A monad for programming web handlers called \"Snap\", which allows: . * Stateful access to the HTTP request and response objects . * Monadic failure (i.e. MonadPlus/Alternative instances) for declining to handle requests and chaining handlers together . * Early termination of the computation if you know early what you want to return and want to prevent further monadic processing . /Quick start/: The 'Snap' monad and HTTP definitions are in "Snap.Core", some iteratee utilities are in "Snap.Iteratee". license: BSD3 license-file: LICENSE author: James Sanders, Shu-yu Guo, Gregory Collins, Doug Beardsley maintainer: snap@snapframework.com build-type: Simple cabal-version: >= 1.8 homepage: http://snapframework.com/ category: Web, Snap extra-source-files: test/suite/TestSuite.hs, cbits/timefuncs.c, CONTRIBUTORS, extra/haddock.css, extra/hscolour.css, extra/logo.gif, haddock.sh, LICENSE, README.md, README.SNAP.md, Setup.hs, test/data/fileServe/foo.bin, test/data/fileServe/foo.bin.bin.bin, test/data/fileServe/foo.html, test/data/fileServe/foo.txt, test/data/fileServe/mydir1/index.txt, test/data/fileServe/mydir2/foo.txt, test/data/fileServe/mydir3/altindex.html, test/runTestsAndCoverage.sh, test/snap-core-testsuite.cabal, test/suite/Snap/Core/Tests.hs, test/suite/Snap/Internal/Http/Types/Tests.hs, test/suite/Snap/Internal/Parsing/Tests.hs, test/suite/Snap/Internal/Routing/Tests.hs, test/suite/Snap/Iteratee/Tests.hs, test/suite/Snap/Test/Common.hs, test/suite/Snap/Util/FileServe/Tests.hs, test/suite/Snap/Util/FileUploads/Tests.hs, test/suite/Snap/Util/GZip/Tests.hs, test/suite/Snap/Util/Proxy/Tests.hs Flag portable Description: Compile in cross-platform mode. No platform-specific code or optimizations such as C routines will be used. Default: False Flag debug Description: Enable debug logging code. With this flag, Snap will test the DEBUG environment variable to decide whether to do logging, and this introduces a tiny amount of overhead (a call into a function pointer) because the calls to 'debug' cannot be inlined. Default: False Library hs-source-dirs: src if !flag(debug) cpp-options: -DNODEBUG if flag(portable) || os(windows) cpp-options: -DPORTABLE build-depends: time-locale-compat == 0.1.* else c-sources: cbits/timefuncs.c include-dirs: cbits build-depends: bytestring-mmap >= 0.2.2 && <0.3, old-locale >= 1 && <2, unix >= 2.4 && <3.0 cpp-options: -DUSE_UNIX exposed-modules: Snap.Core, Snap.Types, Snap.Iteratee, Snap.Internal.Debug, Snap.Internal.Exceptions, Snap.Internal.Http.Types, Snap.Internal.Iteratee.Debug, Snap.Internal.Parsing, Snap.Test, Snap.Types.Headers, Snap.Util.FileServe, Snap.Util.FileUploads, Snap.Util.GZip, Snap.Util.Proxy, Snap.Util.Readable other-modules: Snap.Internal.Instances, Snap.Internal.Iteratee.BoyerMooreHorspool, Snap.Internal.Parsing.FastSet, Snap.Internal.Routing, Snap.Internal.Types, Snap.Internal.Test.RequestBuilder, Snap.Internal.Test.Assertions build-depends: attoparsec >= 0.10 && < 0.14, attoparsec-enumerator >= 0.3 && < 0.4, base >= 4 && < 5, blaze-builder >= 0.2.1.4 && < 0.5, blaze-builder-enumerator >= 0.2 && < 0.3, bytestring >= 0.9 && < 0.11, case-insensitive >= 0.3 && < 1.3, containers >= 0.3 && < 1.0, deepseq >= 1.1 && < 1.5, directory >= 1 && < 2, enumerator >= 0.4.15 && < 0.5, filepath >= 1.1 && < 2.0, hashable (>= 1.1 && < 1.2) || (>= 1.2.1 && <1.3), HUnit >= 1.2 && < 2, MonadCatchIO-transformers >= 0.2.1 && < 0.4, mtl >= 2.0 && < 2.3, random >= 1 && < 2, regex-posix >= 0.95 && < 1, text >= 0.11 && < 1.3, time >= 1.0 && < 1.6, unix-compat >= 0.2 && < 0.5, unordered-containers >= 0.1.4.3 && < 0.3, vector >= 0.6 && < 0.12, zlib-enum >= 0.2.1 && < 0.3 extensions: BangPatterns, CPP, DeriveDataTypeable, EmptyDataDecls, ExistentialQuantification, FlexibleContexts, FlexibleInstances, ForeignFunctionInterface, GeneralizedNewtypeDeriving, MultiParamTypeClasses, OverloadedStrings, OverloadedStrings, PackageImports, Rank2Types, ScopedTypeVariables, TypeSynonymInstances ghc-prof-options: -prof -auto-all if impl(ghc >= 6.12.0) ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -O2 -fno-warn-unused-do-bind else ghc-options: -Wall -fwarn-tabs -funbox-strict-fields -O2 source-repository head type: git location: git://github.com/snapframework/snap-core.git snap-core-0.9.8.0/CONTRIBUTORS0000644000000000000000000000062712565252520013634 0ustar0000000000000000Doug Beardsley Gregory Collins Shu-yu Guo Carl Howells Shane O'Brien James Sanders Jacob Stanley Jonas Kramer Jurriën Stutterheim Jasper Van der Jeugt Bryan O'Sullivan snap-core-0.9.8.0/haddock.sh0000755000000000000000000000044612565252520013707 0ustar0000000000000000#!/bin/sh set -x rm -Rf dist/doc HADDOCK_OPTS='--html-location=http://hackage.haskell.org/packages/archive/$pkg/latest/doc/html' cabal haddock $HADDOCK_OPTS --hyperlink-source $@ cp extra/logo.gif dist/doc/html/snap-core/haskell_icon.gif cp extra/hscolour.css dist/doc/html/snap-core/src/ snap-core-0.9.8.0/LICENSE0000644000000000000000000000300612565252520012753 0ustar0000000000000000Copyright (c) 2009, Snap Framework authors (see CONTRIBUTORS) Copyright (c) 2010, Google, Inc. 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. Neither the name of the Snap Framework authors nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 HOLDER OR CONTRIBUTORS 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. snap-core-0.9.8.0/README.md0000644000000000000000000000377512565252520013242 0ustar0000000000000000Snap Framework Core =================== Snap is a web framework for Haskell, based on iteratee I/O (as [popularized by Oleg Kiselyov](http://okmij.org/ftp/Streams.html#iteratee)). For more information about Snap, read the `README.SNAP.md` or visit the Snap project website at http://www.snapframework.com/. ## Library contents This is the `snap-core` library, which contains: * primitive types and functions for HTTP (requests, responses, cookies, post/query parameters, etc). * type aliases and helper functions for Iteratee I/O. * a "Snap" monad interface, inspired by [happstack's](http://happstack.com/index.html), for programming web handlers, which allows: * stateful access to the HTTP request and response objects. * monadic failure (i.e. MonadPlus/Alternative instances) for declining to handle requests and chaining handlers together. * early termination of the computation if you know early what you want to return and want to prevent further monadic processing. * Some useful utilities for web handlers, including gzip/zlib compression. Building snap-core =================== The snap-core library is built using [Cabal](http://www.haskell.org/cabal/) and [Hackage](http://hackage.haskell.org/packages/hackage.html). Just run cabal install from the `snap-core` toplevel directory. ## Building the Haddock Documentation The haddock documentation can be built using the supplied `haddock.sh` shell script: ./haddock.sh The docs get put in `dist/doc/html/`. ## Building the testsuite Snap is still in its very early stages, so most of the "action" (and a big chunk of the code) right now is centred on the test suite. Snap aims for 100% test coverage, and we're trying hard to stick to that. To build the test suite, `cd` into the `test/` directory and run $ cabal configure $ cabal build From here you can invoke the testsuite by running: $ ./runTestsAndCoverage.sh The testsuite generates an `hpc` test coverage report in `test/dist/hpc`. snap-core-0.9.8.0/README.SNAP.md0000644000000000000000000000227312565252520013772 0ustar0000000000000000Snap Framework -------------- Snap is a simple and fast web development framework and server written in Haskell. For more information or to download the latest version, you can visit the Snap project website at http://snapframework.com/. Snap Status and Features ------------------------ The Snap core system consists of: * a high-speed HTTP server, with an optional high-concurrency backend using the [libev](http://software.schmorp.de/pkg/libev.html) library * a sensible and clean monad for web programming * an xml-based templating system for generating HTML that allows you to bind Haskell functionality to XML tags without getting PHP-style tag soup all over your pants * a "snaplet" system for building web sites from composable pieces. Snap is currently only officially supported on Unix platforms; it has been tested on Linux and Mac OSX Snow Leopard, and is reported to work on Windows. Snap Philosophy --------------- Snap aims to be the *de facto* web toolkit for Haskell, on the basis of: * High performance * High design standards * Simplicity and ease of use, even for Haskell beginners * Excellent documentation * Robustness and high test coverage snap-core-0.9.8.0/Setup.hs0000644000000000000000000000005712565252520013405 0ustar0000000000000000import Distribution.Simple main = defaultMain snap-core-0.9.8.0/src/0000755000000000000000000000000012565252520012536 5ustar0000000000000000snap-core-0.9.8.0/src/Snap/0000755000000000000000000000000012565252520013437 5ustar0000000000000000snap-core-0.9.8.0/src/Snap/Core.hs0000644000000000000000000000635412565252520014673 0ustar0000000000000000{-| This module contains the core type definitions, class instances, and functions for HTTP as well as the 'Snap' monad, which is used for web handlers. -} module Snap.Core ( -- * The Snap Monad Snap , runSnap , MonadSnap(..) , NoHandlerException(..) -- ** Functions for control flow and early termination , bracketSnap , finishWith , catchFinishWith , pass , terminateConnection -- *** Escaping HTTP , EscapeHttpHandler , escapeHttp -- ** Routing , method , methods , path , pathArg , dir , ifTop , route , routeLocal -- ** Access to state , getRequest , getsRequest , getResponse , getsResponse , putRequest , putResponse , modifyRequest , modifyResponse , localRequest , withRequest , withResponse -- ** Logging , logError -- ** Grabbing/transforming request bodies , runRequestBody , getRequestBody , readRequestBody , transformRequestBody -- * HTTP Datatypes and Functions -- $httpDoc -- , Request , Response , Headers , HasHeaders(..) , Params , Method(..) , Cookie(..) , HttpVersion -- ** Headers , addHeader , setHeader , getHeader , getHeaders , listHeaders , deleteHeader , ipHeaderFilter , ipHeaderFilter' -- ** Requests , rqServerName , rqServerPort , rqRemoteAddr , rqRemotePort , rqLocalAddr , rqLocalHostname , rqIsSecure , rqContentLength , rqMethod , rqVersion , rqCookies , rqPathInfo , rqContextPath , rqURI , rqQueryString , rqParams , rqQueryParams , rqPostParams , rqParam , rqPostParam , rqQueryParam , getParam , getPostParam , getQueryParam , getParams , getPostParams , getQueryParams , rqModifyParams , rqSetParam -- ** Responses , emptyResponse , setResponseCode , setResponseStatus , rspStatus , rspStatusReason , setContentType , addResponseCookie , getResponseCookie , getResponseCookies , deleteResponseCookie , modifyResponseCookie , expireCookie , getCookie , readCookie , setContentLength , clearContentLength , redirect , redirect' , setBufferingMode , getBufferingMode -- *** Response I/O , setResponseBody , modifyResponseBody , addToOutput , writeBuilder , writeBS , writeLazyText , writeText , writeLBS , sendFile , sendFilePartial -- ** Timeouts , setTimeout , extendTimeout , modifyTimeout , getTimeoutAction , getTimeoutModifier -- * Iteratee , Enumerator , SomeEnumerator(..) -- * HTTP utilities , formatHttpTime , parseHttpTime , parseUrlEncoded , buildUrlEncoded , printUrlEncoded , urlEncode , urlEncodeBuilder , urlDecode ) where ------------------------------------------------------------------------------ import Snap.Internal.Exceptions (EscapeHttpHandler) import Snap.Internal.Http.Types import Snap.Internal.Instances () import Snap.Internal.Parsing import Snap.Internal.Routing import Snap.Internal.Types import Snap.Iteratee (Enumerator) import Snap.Types.Headers (Headers) ------------------------------------------------------------------------------ -- $httpDoc -- HTTP-related datatypes: 'Request', 'Response', 'Cookie', etc. snap-core-0.9.8.0/src/Snap/Types.hs0000644000000000000000000000032612565252520015100 0ustar0000000000000000-- | As of Snap 0.6, the "Snap.Types" module is deprecated: please use -- "Snap.Core" instead. module Snap.Types {-# DEPRECATED "As of 0.6, use Snap.Core instead" #-} ( module Snap.Core ) where import Snap.Core snap-core-0.9.8.0/src/Snap/Iteratee.hs0000644000000000000000000006052212565252520015542 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PackageImports #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeSynonymInstances #-} {-# OPTIONS_GHC -fno-warn-orphans #-} ------------------------------------------------------------------------------ -- | Snap Framework type aliases and utilities for iteratees. Note that as a -- convenience, this module also exports everything from @Data.Enumerator@ in -- the @enumerator@ library. module Snap.Iteratee ( -- * Enumerators enumBS , enumLBS , enumBuilder , enumFile , enumFilePartial , InvalidRangeException -- * Iteratee utilities , joinI' , countBytes , drop' , mkIterateeBuffer , unsafeBufferIterateeWithBuffer , unsafeBufferIteratee , take , drop , takeExactly , takeNoMoreThan , skipToEof , mapEnum , mapIter , enumBuilderToByteString , unsafeEnumBuilderToByteString , enumByteStringToBuilder , killIfTooSlow , TooManyBytesReadException , ShortWriteException , RateTooSlowException -- * Re-export types and functions from @Data.Enumerator@ , Stream (..) , Step (..) , Iteratee (..) , Enumerator , Enumeratee -- ** Primitives -- *** Combinators -- | These are common patterns which occur whenever iteratees are -- being defined. , returnI , yield , continue , throwError , catchError , liftI , (>>==) , (==<<) , ($$) , (>==>) , (<==<) , ($=) , (=$) -- *** Iteratees , run , run_ , consume , Data.Enumerator.isEOF , liftTrans , liftFoldL , liftFoldL' , liftFoldM , printChunks , head , peek -- *** Enumerators , enumEOF , enumList , concatEnums -- *** Enumeratees , checkDone , Data.Enumerator.List.map , Data.Enumerator.sequence , joinI {- -- ** Iteratee utilities , drop' -} ) where ------------------------------------------------------------------------------ import Blaze.ByteString.Builder import Blaze.ByteString.Builder.Enumerator import Control.DeepSeq import Control.Exception (SomeException, assert) import Control.Monad import Control.Monad.CatchIO import Control.Monad.Trans (MonadIO, lift, liftIO) import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as S import qualified Data.ByteString.Unsafe as S import qualified Data.ByteString.Lazy.Char8 as L import Data.Enumerator hiding (consume, drop, head) import qualified Data.Enumerator as I import Data.Enumerator.Binary (enumHandle) import Data.Enumerator.List hiding (take, drop) import qualified Data.Enumerator.List as IL import qualified Data.List as List import Data.Monoid (mappend) import Data.Time.Clock.POSIX (getPOSIXTime) import Data.Typeable import Foreign hiding (peek) import Foreign.C.Types import GHC.ForeignPtr #if MIN_VERSION_base(4,6,0) import Prelude hiding (drop, head, take) #else import Prelude hiding (catch, drop, head, take) #endif import System.IO #ifndef PORTABLE import System.IO.Posix.MMap import System.PosixCompat.Files import System.PosixCompat.Types #endif ------------------------------------------------------------------------------ instance (Functor m, MonadCatchIO m) => MonadCatchIO (Iteratee s m) where --catch :: Exception e => m a -> (e -> m a) -> m a catch m handler = insideCatch (m `catchError` h) where insideCatch !mm = Iteratee $ do ee <- try $ runIteratee mm case ee of (Left e) -> runIteratee $ handler e (Right v) -> step v step (Continue !k) = do return $! Continue (\s -> insideCatch $ k s) -- don't worry about Error here because the error had to come from the -- handler (because of 'catchError' above) step y = return y -- we can only catch iteratee errors if "e" matches "SomeException" h e = maybe (throwError e) (handler) (fromException e) --block :: m a -> m a block m = Iteratee $ block $ (runIteratee m >>= step) where step (Continue k) = return $! Continue (\s -> block (k s)) step y = return y unblock m = Iteratee $ unblock $ (runIteratee m >>= step) where step (Continue k) = return $! Continue (\s -> unblock (k s)) step y = return y ------------------------------------------------------------------------------ -- | Get the length of a bytestring Stream streamLength :: Stream ByteString -> Int streamLength (Chunks xs) = List.foldl' (\c s -> c + S.length s) 0 xs streamLength EOF = 0 ------------------------------------------------------------------------------ -- | Enumerates a Builder. enumBuilder :: (Monad m) => Builder -> Enumerator Builder m a enumBuilder = enumList 1 . (:[]) {-# INLINE enumBuilder #-} ------------------------------------------------------------------------------ -- | Enumerates a strict bytestring. enumBS :: (Monad m) => ByteString -> Enumerator ByteString m a enumBS = enumList 1 . (:[]) {-# INLINE enumBS #-} ------------------------------------------------------------------------------ -- | Enumerates a lazy bytestring. enumLBS :: (Monad m) => L.ByteString -> Enumerator ByteString m a enumLBS bs = enumList 1 (L.toChunks bs) {-# INLINE enumLBS #-} ------------------------------------------------------------------------------ skipToEof :: (Monad m) => Iteratee a m () skipToEof = continue k where k EOF = yield () EOF k _ = skipToEof ------------------------------------------------------------------------------ -- | Wraps an 'Iteratee', counting the number of bytes consumed by it. countBytes :: (Monad m) => forall a . Iteratee ByteString m a -> Iteratee ByteString m (a, Int64) countBytes i = Iteratee $ do step <- runIteratee i case step of (Continue k) -> return (Continue $ go 0 k) (Yield x s) -> return $! Yield (x,0) s (Error e) -> return $! Error e where go !n k str = Iteratee $ do let len = toEnum $ streamLength str step <- runIteratee (k str) case step of (Continue k') -> return (Continue $ go (n + len) k') (Yield x s) -> let len' = n + len - (toEnum $ streamLength s) in return (Yield (x, len') s) (Error e) -> return (Error e) ------------------------------------------------------------------------------ bUFSIZ :: Int bUFSIZ = 8192 ------------------------------------------------------------------------------ -- | Creates a buffer to be passed into 'unsafeBufferIterateeWithBuffer'. mkIterateeBuffer :: IO (ForeignPtr CChar) mkIterateeBuffer = mallocPlainForeignPtrBytes bUFSIZ ------------------------------------------------------------------------------ -- | Buffers an iteratee, \"unsafely\". Here we use a fixed binary buffer -- which we'll re-use, meaning that if you hold on to any of the bytestring -- data passed into your iteratee (instead of, let's say, shoving it right out -- a socket) it'll get changed out from underneath you, breaking referential -- transparency. Use with caution! unsafeBufferIteratee :: Iteratee ByteString IO a -> IO (Iteratee ByteString IO a) unsafeBufferIteratee step = do buf <- mkIterateeBuffer return $! unsafeBufferIterateeWithBuffer buf step ------------------------------------------------------------------------------ -- | Buffers an iteratee, \"unsafely\". Here we use a fixed binary buffer -- which we'll re-use, meaning that if you hold on to any of the bytestring -- data passed into your iteratee (instead of, let's say, shoving it right out -- a socket) it'll get changed out from underneath you, breaking referential -- transparency. Use with caution! -- -- This version accepts a buffer created by 'mkIterateeBuffer'. -- unsafeBufferIterateeWithBuffer :: ForeignPtr CChar -> Iteratee ByteString IO a -> Iteratee ByteString IO a unsafeBufferIterateeWithBuffer buf iter = Iteratee $ do step <- runIteratee iter start step where -------------------------------------------------------------------------- start :: Step ByteString IO a -> IO (Step ByteString IO a) start (Continue k) = return $! Continue $ go 0 k start s@_ = return s -------------------------------------------------------------------------- sendBuf :: Int -> (Stream ByteString -> Iteratee ByteString IO a) -> IO (Step ByteString IO a) sendBuf n k = {-# SCC "unsafeBufferIteratee/sendBuf" #-} do assert (n > 0) (return ()) assert (n <= bUFSIZ) (return ()) withForeignPtr buf $ \ptr -> do !s <- S.unsafePackCStringLen (ptr, n) runIteratee $ k $ Chunks [s] -------------------------------------------------------------------------- copy EOF = EOF copy (Chunks xs) = zs `deepseq` Chunks ys where !ys = Prelude.map S.copy xs !zs = Prelude.map (`seq` ()) ys -------------------------------------------------------------------------- go :: Int -> (Stream ByteString -> Iteratee ByteString IO a) -> (Stream ByteString -> Iteratee ByteString IO a) go !n !k EOF = Iteratee $ do if n == 0 then runIteratee $ k EOF else do assert (n > 0) (return ()) assert (n <= bUFSIZ) (return ()) step <- sendBuf n k step2 <- runIteratee $ enumEOF step return $! copyStep step2 go !n !k (Chunks xs) = Iteratee $ do assert (n >= 0) (return ()) assert (n <= bUFSIZ) (return ()) let s = S.concat xs let m = S.length s if m+n >= bUFSIZ then overflow n k s m else copyAndCont n k s m -------------------------------------------------------------------------- copyStep (Yield x r) = let !z = copy r in Yield x z copyStep x = x -------------------------------------------------------------------------- copyAndCont :: Int -> (Stream ByteString -> Iteratee ByteString IO a) -> ByteString -> Int -> IO (Step ByteString IO a) copyAndCont !n k !s !m = {-# SCC "unsafeBufferIteratee/copyAndCont" #-} do assert (n >= 0) (return ()) assert (n+m < bUFSIZ) (return ()) S.unsafeUseAsCStringLen s $ \(p,sz) -> do assert (m == sz) (return ()) withForeignPtr buf $ \bufp -> do let b' = plusPtr bufp n copyBytes b' p sz return $! Continue $! go (n+m) k -------------------------------------------------------------------------- overflow :: Int -> (Stream ByteString -> Iteratee ByteString IO a) -> ByteString -> Int -> IO (Step ByteString IO a) overflow !n k !s !m = {-# SCC "unsafeBufferIteratee/overflow" #-} do assert (n+m >= bUFSIZ) (return ()) assert (n < bUFSIZ) (return ()) let rest = bUFSIZ - n let m2 = m - rest let (s1,s2) = S.splitAt rest s S.unsafeUseAsCStringLen s1 $ \(p,_) -> withForeignPtr buf $ \bufp -> do let b' = plusPtr bufp n copyBytes b' p rest iv <- sendBuf bUFSIZ k case iv of (Yield x r) -> let !z = copy r in return $! Yield x $! (z `mappend` Chunks [s2]) (Error e) -> return $! Error e (Continue k') -> do -- check the size of the remainder; if it's bigger than the -- buffer size then just send it if m2 >= bUFSIZ then do step <- runIteratee $ k' $ Chunks [s2] case step of (Yield x r) -> let !z = copy r in return $! Yield x z (Error e) -> return $! Error e (Continue k'') -> return $! Continue $! go 0 k'' else copyAndCont 0 k' s2 m2 ------------------------------------------------------------------------------ -- | Skip n elements of the stream, if there are that many drop :: (Monad m) => Int -> Iteratee ByteString m () drop k = drop' (toEnum k) ------------------------------------------------------------------------------ -- | Skip n elements of the stream, if there are that many drop' :: (Monad m) => Int64 -> Iteratee ByteString m () drop' 0 = return () drop' !n = continue k where k EOF = return () k (Chunks xs) = chunks n xs chunks !m [] = drop' m chunks !m (x:xs) = do let strlen = toEnum $ S.length x if strlen <= m then chunks (m-strlen) xs else yield () $ Chunks ((S.drop (fromEnum m) x):xs) ------------------------------------------------------------------------------ data ShortWriteException = ShortWriteException deriving (Typeable) data RateTooSlowException = RateTooSlowException deriving (Typeable) data TooManyBytesReadException = TooManyBytesReadException deriving (Typeable) instance Show ShortWriteException where show ShortWriteException = "Short write" instance Show RateTooSlowException where show RateTooSlowException = "Input rate too slow" instance Show TooManyBytesReadException where show TooManyBytesReadException = "Too many bytes read" instance Exception ShortWriteException instance Exception RateTooSlowException instance Exception TooManyBytesReadException ------------------------------------------------------------------------------ take :: (Monad m) => Int -> Enumeratee ByteString ByteString m a take k = take' (toEnum k) ------------------------------------------------------------------------------ take' :: (Monad m) => Int64 -> Enumeratee ByteString ByteString m a take' _ y@(Yield _ _ ) = return y take' _ (Error e ) = throwError e take' !n st@(Continue k) = do if n == 0 then lift $ runIteratee $ k EOF else do mbX <- head maybe (lift $ runIteratee $ k EOF) check mbX where check x | S.null x = take' n st | strlen <= n = do newStep <- lift $ runIteratee $ k $ Chunks [x] take' (n-strlen) newStep | otherwise = do step1 <- lift $ runIteratee $ k $ Chunks [s1] step2 <- lift $ runIteratee $ enumEOF step1 case step2 of (Yield v _) -> yield (Yield v EOF) (Chunks [s2]) (Error e) -> throwError e (Continue _) -> error "divergent iteratee" where strlen = toEnum $ S.length x (s1,s2) = S.splitAt (fromEnum n) x ------------------------------------------------------------------------------ -- | Reads n bytes from a stream and applies the given iteratee to the stream -- of the read elements. Reads exactly n bytes, and if the stream is short -- propagates an error. takeExactly :: (Monad m) => Int64 -> Enumeratee ByteString ByteString m a takeExactly !n y@(Yield _ _ ) = drop' n >> return y takeExactly _ (Error e ) = throwError e takeExactly !n st@(Continue !k) = do if n == 0 then lift $ runIteratee $ k EOF else do mbX <- head maybe (throwError ShortWriteException) check mbX where check !x | S.null x = takeExactly n st | strlen < n = do newStep <- lift $ runIteratee $ k $ Chunks [x] takeExactly (n-strlen) newStep | otherwise = do let (s1,s2) = S.splitAt (fromEnum n) x !step1 <- lift $ runIteratee $ k $ Chunks [s1] !step2 <- lift $ runIteratee $ enumEOF step1 case step2 of (Continue _) -> error "divergent iteratee" (Error e) -> throwError e (Yield v _) -> yield (Yield v EOF) (Chunks [s2]) where !strlen = toEnum $ S.length x ------------------------------------------------------------------------------ takeNoMoreThan :: (Monad m) => Int64 -> Enumeratee ByteString ByteString m a takeNoMoreThan _ y@(Yield _ _) = return y takeNoMoreThan _ (Error e ) = throwError e takeNoMoreThan !n st@(Continue k) = do mbX <- head maybe (lift $ runIteratee $ k EOF) check mbX where check x | S.null x = takeNoMoreThan n st | strlen <= n = do newStep <- lift $ runIteratee $ k $ Chunks [x] takeNoMoreThan (n-strlen) newStep | otherwise = do step1 <- lift $ runIteratee $ k $ Chunks [s1] case step1 of (Yield v rest) -> yield (Yield v EOF) (rest `mappend` Chunks [s2]) (Error e) -> throwError e (Continue _) -> throwError TooManyBytesReadException where strlen = toEnum $ S.length x (s1,s2) = S.splitAt (fromEnum n) x ------------------------------------------------------------------------------ {-# INLINE _enumFile #-} _enumFile :: FilePath -> Enumerator ByteString IO a _enumFile fp iter = do h <- liftIO $ openBinaryFile fp ReadMode enumHandle 32678 h iter `finally` (liftIO $ hClose h) ------------------------------------------------------------------------------ data InvalidRangeException = InvalidRangeException deriving (Typeable) ------------------------------------------------------------------------------ instance Show InvalidRangeException where show InvalidRangeException = "Invalid range" ------------------------------------------------------------------------------ instance Exception InvalidRangeException ------------------------------------------------------------------------------ {-# INLINE _enumFilePartial #-} _enumFilePartial :: FilePath -> (Int64,Int64) -> Enumerator ByteString IO a _enumFilePartial fp (start,end) iter = do let len = end - start bracket (liftIO $ openBinaryFile fp ReadMode) (liftIO . hClose) (\h -> do unless (start == 0) $ liftIO $ hSeek h AbsoluteSeek $ toInteger start step <- lift $ runIteratee $ joinI $ takeExactly len iter enumHandle 32678 h step) ------------------------------------------------------------------------------ enumFile :: FilePath -> Enumerator ByteString IO a enumFilePartial :: FilePath -> (Int64,Int64) -> Enumerator ByteString IO a #ifdef PORTABLE enumFile = _enumFile enumFilePartial fp rng@(start,end) iter = do when (end < start) $ throwError InvalidRangeException _enumFilePartial fp rng iter #else -- 40MB limit maxMMapFileSize :: FileOffset maxMMapFileSize = 41943040 ------------------------------------------------------------------------------ tooBigForMMap :: FilePath -> IO Bool tooBigForMMap fp = do stat <- getFileStatus fp return $! fileSize stat > maxMMapFileSize ------------------------------------------------------------------------------ enumFile _ (Error e) = throwError e enumFile _ (Yield x _) = yield x EOF enumFile fp st@(Continue k) = do -- for small files we'll use mmap to save ourselves a copy, otherwise -- we'll stream it tooBig <- lift $ tooBigForMMap fp if tooBig then _enumFile fp st else do es <- try $ lift $ unsafeMMapFile fp case es of (Left (e :: SomeException)) -> throwError e (Right s) -> k $ Chunks [s] ------------------------------------------------------------------------------ enumFilePartial _ _ (Error e) = throwError e enumFilePartial _ _ (Yield x _) = yield x EOF enumFilePartial fp rng@(start,end) st@(Continue k) = do when (end < start) $ throwError InvalidRangeException let len = end - start tooBig <- lift $ tooBigForMMap fp if tooBig then _enumFilePartial fp rng st else do es <- try $ lift $ unsafeMMapFile fp case es of (Left (e::SomeException)) -> throwError e (Right s) -> k $ Chunks [ S.take (fromEnum len) $ S.drop (fromEnum start) s ] #endif ------------------------------------------------------------------------------ mapEnum :: (Monad m) => (aOut -> aIn) -> (aIn -> aOut) -> Enumerator aIn m a -> Enumerator aOut m a mapEnum f g enum outStep = do let z = IL.map g outStep let p = joinI z let q = enum $$ p (I.joinI . IL.map f) $$ q ------------------------------------------------------------------------------ mapIter :: (Monad m) => (aOut -> aIn) -> (aIn -> aOut) -> Iteratee aIn m a -> Iteratee aOut m a mapIter f g iter = do step <- lift $ runIteratee iter mapStep step where -- mapStep :: Step aIn m a -> Iteratee aOut m a mapStep (Continue k) = continue $ wrapK k mapStep (Yield x rest) = yield x (fmap g rest) mapStep (Error e) = throwError e -- wrapK :: (Stream aIn -> Iteratee aIn m a) -- -> (Stream aOut -> Iteratee aOut m a) wrapK k streamOut = mapIter f g iterIn where streamIn = fmap f streamOut iterIn = k streamIn ------------------------------------------------------------------------------ enumBuilderToByteString :: MonadIO m => Enumeratee Builder ByteString m a enumBuilderToByteString = builderToByteString ------------------------------------------------------------------------------ unsafeEnumBuilderToByteString :: MonadIO m => Enumeratee Builder ByteString m a unsafeEnumBuilderToByteString = builderToByteStringWith (reuseBufferStrategy (allocBuffer bufsize)) where bufsize = (2::Int) ^ (14::Int) ------------------------------------------------------------------------------ enumByteStringToBuilder :: MonadIO m => Enumeratee ByteString Builder m a enumByteStringToBuilder = IL.map fromByteString ------------------------------------------------------------------------------ joinI' :: Monad m => Iteratee a m (Step a m b) -> Iteratee a m b joinI' outer = outer >>= check where check (Continue k) = k EOF >>== \s -> case s of Continue _ -> error "joinI: divergent iteratee" _ -> check s check (Yield x r) = yield x r check (Error e) = throwError e ------------------------------------------------------------------------------ killIfTooSlow :: (MonadIO m) => m () -- ^ action to bump timeout -> Double -- ^ minimum data rate, in bytes per -- second -> Int -- ^ minimum amount of time to let -- the iteratee run for -> Iteratee ByteString m a -- ^ iteratee consumer to wrap -> Iteratee ByteString m a killIfTooSlow !bump !minRate !minSeconds' !inputIter = do !_ <- lift bump startTime <- liftIO getTime step <- lift $ runIteratee inputIter wrap startTime (0::Int64) step where minSeconds = fromIntegral minSeconds' wrap !startTime = proc where proc !nb (Continue !k) = continue $ cont nb k proc _ !z = returnI z cont _ !k EOF = k EOF cont !nBytesRead !k !stream = do let !slen = toEnum $ streamLength stream now <- liftIO getTime let !delta = now - startTime let !newBytes = nBytesRead + slen when (delta > minSeconds+1 && fromIntegral newBytes / (delta-minSeconds) < minRate) $ throwError RateTooSlowException -- otherwise bump the timeout and continue running the iteratee !_ <- lift bump lift (runIteratee $! k stream) >>= proc newBytes ------------------------------------------------------------------------------ getTime :: IO Double getTime = realToFrac `fmap` getPOSIXTime snap-core-0.9.8.0/src/Snap/Test.hs0000644000000000000000000000217512565252520014717 0ustar0000000000000000-- | The Snap.Test module contains primitives and combinators for testing Snap -- applications. module Snap.Test ( -- * Combinators and types for testing Snap handlers. -- ** Types RequestBuilder , MultipartParams , MultipartParam(..) , FileData (..) , RequestType (..) -- ** Building Requests and testing handlers , buildRequest , runHandler , runHandlerM , evalHandler , evalHandlerM -- *** Convenience functions for generating common types of HTTP requests , get , postUrlEncoded , postMultipart , put , postRaw , delete -- *** Precise control over building Requests , addHeader , setContentType , setHeader , setHttpVersion , setQueryString , setQueryStringRaw , setRequestPath , setRequestType , setSecure -- * HUnit Assertions , assertSuccess , assert404 , assertRedirectTo , assertRedirect , assertBodyContains -- * Getting response bodies , getResponseBody -- * Dumping HTTP Responses , dumpResponse , responseToString ) where import Snap.Internal.Test.Assertions import Snap.Internal.Test.RequestBuilder snap-core-0.9.8.0/src/Snap/Internal/0000755000000000000000000000000012565252520015213 5ustar0000000000000000snap-core-0.9.8.0/src/Snap/Internal/Debug.hs0000644000000000000000000001035412565252520016600 0ustar0000000000000000-- | An internal Snap module for (optionally) printing debugging -- messages. Normally 'debug' does nothing, but if you set @DEBUG=1@ in the -- environment you'll get debugging messages. We use 'unsafePerformIO' to make -- sure that the call to 'getEnv' is only made once. -- -- /N.B./ this is an internal interface, please don't write external code that -- depends on it. {-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE PackageImports #-} {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-cse #-} module Snap.Internal.Debug where ------------------------------------------------------------------------------ import Control.Monad.Trans #ifndef NODEBUG import Control.Concurrent import Control.DeepSeq import Data.Either import Control.Exception import Data.Char import Data.List import Data.Maybe import Foreign.C.Error import System.Environment import System.IO import System.IO.Unsafe import Text.Printf #endif ------------------------------------------------------------------------------ debug, debugErrno :: MonadIO m => String -> m () #ifndef NODEBUG {-# NOINLINE debug #-} debug = let !x = unsafePerformIO $! do !e <- try $ getEnv "DEBUG" !f <- either (\(_::SomeException) -> return debugIgnore) (\y0 -> let y = map toLower y0 in if y == "1" || y == "on" then return debugOn else if y == "testsuite" then return debugSeq else return debugIgnore) e return $! f in x {-# NOINLINE debugErrno #-} debugErrno = let !x = unsafePerformIO $ do e <- try $ getEnv "DEBUG" !f <- either (\(_::SomeException) -> return debugErrnoIgnore) (\y0 -> let y = map toLower y0 in if y == "1" || y == "on" then return debugErrnoOn else if y == "testsuite" then return debugErrnoSeq else return debugErrnoIgnore) e return $! f in x ------------------------------------------------------------------------------ debugSeq :: (MonadIO m) => String -> m () debugSeq !s = let !s' = rnf s in return $! s' `deepseq` () {-# NOINLINE debugSeq #-} debugErrnoSeq :: (MonadIO m) => String -> m () debugErrnoSeq !s = let !s' = rnf s in return $! s' `deepseq` () {-# NOINLINE debugErrnoSeq #-} ------------------------------------------------------------------------------ _debugMVar :: MVar () _debugMVar = unsafePerformIO $ newMVar () {-# NOINLINE _debugMVar #-} ------------------------------------------------------------------------------ debugOn :: (MonadIO m) => String -> m () debugOn s = liftIO $ withMVar _debugMVar $ \_ -> do tid <- myThreadId hPutStrLn stderr $ s' tid hFlush stderr where chop x = let y = fromMaybe x $ stripPrefix "ThreadId " x in printf "%8s" y s' t = "[" ++ chop (show t) ++ "] " ++ s {-# NOINLINE debugOn #-} ------------------------------------------------------------------------------ debugErrnoOn :: (MonadIO m) => String -> m () debugErrnoOn loc = liftIO $ do err <- getErrno let ex = errnoToIOError loc err Nothing Nothing debug $ show ex ------------------------------------------------------------------------------ #else debug = debugIgnore {-# INLINE debug #-} debugErrno = debugErrnoIgnore {-# INLINE debugErrno #-} #endif ------------------------------------------------------------------------------ debugIgnore :: (MonadIO m) => String -> m () debugIgnore _ = return () {-# INLINE debugIgnore #-} debugErrnoIgnore :: (MonadIO m) => String -> m () debugErrnoIgnore _ = return () {-# INLINE debugErrnoIgnore #-} ------------------------------------------------------------------------------ snap-core-0.9.8.0/src/Snap/Internal/Exceptions.hs0000644000000000000000000000625612565252520017701 0ustar0000000000000000{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ExistentialQuantification #-} -- | An internal Snap module containing the exception that escapes HTTP types. -- -- /N.B./ this is an internal interface, please don't write user code that -- depends on it. Interfaces subject to change etc etc etc. -- module Snap.Internal.Exceptions where ------------------------------------------------------------------------------ import Control.Exception import Data.ByteString.Char8 (ByteString) import Data.Typeable import Snap.Iteratee ------------------------------------------------------------------------------ -- | An exception hierarchy for exceptions that cannot be caught by -- user-defined error handlers data UncatchableException = forall e. Exception e => UncatchableException e deriving (Typeable) ------------------------------------------------------------------------------ instance Show UncatchableException where show (UncatchableException e) = "Uncatchable exception: " ++ show e ------------------------------------------------------------------------------ instance Exception UncatchableException ------------------------------------------------------------------------------ uncatchableExceptionToException :: Exception e => e -> SomeException uncatchableExceptionToException = toException . UncatchableException ------------------------------------------------------------------------------ uncatchableExceptionFromException :: Exception e => SomeException -> Maybe e uncatchableExceptionFromException e = do UncatchableException ue <- fromException e cast ue ------------------------------------------------------------------------------ data ConnectionTerminatedException = ConnectionTerminatedException SomeException deriving (Typeable) ------------------------------------------------------------------------------ instance Show ConnectionTerminatedException where show (ConnectionTerminatedException e) = "Connection terminated with exception: " ++ show e ------------------------------------------------------------------------------ instance Exception ConnectionTerminatedException where toException = uncatchableExceptionToException fromException = uncatchableExceptionFromException ------------------------------------------------------------------------------ -- | This exception is thrown if the handler chooses to escape regular HTTP -- traffic. data EscapeHttpException = EscapeHttpException EscapeHttpHandler deriving (Typeable) ------------------------------------------------------------------------------ type EscapeHttpHandler = ((Int -> Int) -> IO ()) -- ^ timeout modifier -> Iteratee ByteString IO () -- ^ socket write end -> Iteratee ByteString IO () ------------------------------------------------------------------------------ instance Show EscapeHttpException where show = const "HTTP traffic was escaped" ------------------------------------------------------------------------------ instance Exception EscapeHttpException where toException = uncatchableExceptionToException fromException = uncatchableExceptionFromException snap-core-0.9.8.0/src/Snap/Internal/Parsing.hs0000644000000000000000000004205112565252520017154 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE MagicHash #-} {-# LANGUAGE OverloadedStrings #-} module Snap.Internal.Parsing where ------------------------------------------------------------------------------ import Blaze.ByteString.Builder import Control.Applicative import Control.Arrow (first, second) import Control.Monad import Data.Attoparsec.Char8 import Data.Bits import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as S import Data.ByteString.Internal (c2w, w2c) import qualified Data.ByteString.Lazy.Char8 as L import Data.CaseInsensitive (CI) import qualified Data.CaseInsensitive as CI import Data.Char hiding (isDigit, isSpace) import Data.Int import Data.List (intersperse) import Data.Map (Map) import qualified Data.Map as Map import Data.Maybe import Data.Monoid import Data.Word import GHC.Exts import GHC.Word (Word8 (..)) import Prelude hiding (head, take, takeWhile) ------------------------------------------------------------------------------ import Snap.Internal.Http.Types import Snap.Internal.Parsing.FastSet (FastSet) import qualified Snap.Internal.Parsing.FastSet as FS ------------------------------------------------------------------------------ {-# INLINE fullyParse #-} fullyParse :: ByteString -> Parser a -> Either String a fullyParse s p = case r' of (Fail _ _ e) -> Left e (Partial _) -> Left "parse failed" (Done _ x) -> Right x where r = parse p s r' = feed r "" ------------------------------------------------------------------------------ parseNum :: Parser Int64 parseNum = decimal ------------------------------------------------------------------------------ -- | Parsers for different tokens in an HTTP request. sp, digit, letter :: Parser Char sp = char ' ' digit = satisfy isDigit letter = satisfy isAlpha ------------------------------------------------------------------------------ untilEOL :: Parser ByteString untilEOL = takeWhile notend where notend c = not $ c == '\r' || c == '\n' ------------------------------------------------------------------------------ crlf :: Parser ByteString crlf = string "\r\n" ------------------------------------------------------------------------------ generateFS :: (Word8 -> Bool) -> FastSet generateFS f = FS.fromList $ filter f [0..255] ------------------------------------------------------------------------------ -- | Parser for zero or more spaces. spaces :: Parser [Char] spaces = many sp ------------------------------------------------------------------------------ pSpaces :: Parser ByteString pSpaces = takeWhile isSpace ------------------------------------------------------------------------------ fieldChars :: Parser ByteString fieldChars = takeWhile isFieldChar where isFieldChar = flip FS.memberChar fieldCharSet ------------------------------------------------------------------------------ fieldCharSet :: FastSet fieldCharSet = generateFS f where f d = let c = (toEnum $ fromEnum d) in (isDigit c) || (isAlpha c) || c == '-' || c == '_' ------------------------------------------------------------------------------ -- | Parser for request headers. pHeaders :: Parser [(ByteString, ByteString)] pHeaders = many header where -------------------------------------------------------------------------- header = {-# SCC "pHeaders/header" #-} liftA2 (,) fieldName (char ':' *> spaces *> contents) -------------------------------------------------------------------------- fieldName = {-# SCC "pHeaders/fieldName" #-} liftA2 S.cons letter fieldChars -------------------------------------------------------------------------- contents = {-# SCC "pHeaders/contents" #-} liftA2 S.append (untilEOL <* crlf) (continuation <|> pure S.empty) -------------------------------------------------------------------------- isLeadingWS w = {-# SCC "pHeaders/isLeadingWS" #-} w == ' ' || w == '\t' -------------------------------------------------------------------------- leadingWhiteSpace = {-# SCC "pHeaders/leadingWhiteSpace" #-} takeWhile1 isLeadingWS -------------------------------------------------------------------------- continuation = {-# SCC "pHeaders/continuation" #-} liftA2 S.cons (leadingWhiteSpace *> pure ' ') contents ------------------------------------------------------------------------------ -- unhelpfully, the spec mentions "old-style" cookies that don't have quotes -- around the value. wonderful. pWord :: Parser ByteString pWord = pQuotedString <|> (takeWhile (/= ';')) ------------------------------------------------------------------------------ pQuotedString :: Parser ByteString pQuotedString = q *> quotedText <* q where quotedText = (S.concat . reverse) <$> f [] f soFar = do t <- takeWhile qdtext let soFar' = t:soFar -- RFC says that backslash only escapes for <"> choice [ string "\\\"" *> f ("\"" : soFar') , pure soFar' ] q = char '\"' qdtext = matchAll [ isRFCText, (/= '\"'), (/= '\\') ] ------------------------------------------------------------------------------ {-# INLINE isRFCText #-} isRFCText :: Char -> Bool isRFCText = not . isControl ------------------------------------------------------------------------------ {-# INLINE matchAll #-} matchAll :: [ Char -> Bool ] -> Char -> Bool matchAll x c = and $ map ($ c) x ------------------------------------------------------------------------------ pAvPairs :: Parser [(ByteString, ByteString)] pAvPairs = do a <- pAvPair b <- many (pSpaces *> char ';' *> pSpaces *> pAvPair) return $! a:b ------------------------------------------------------------------------------ {-# INLINE pAvPair #-} pAvPair :: Parser (ByteString, ByteString) pAvPair = do key <- pToken <* pSpaces val <- liftM trim (option "" $ char '=' *> pSpaces *> pWord) return $! (key, val) ------------------------------------------------------------------------------ pParameter :: Parser (ByteString, ByteString) pParameter = do key <- pToken <* pSpaces val <- liftM trim (char '=' *> pSpaces *> pWord) return $! (trim key, val) ------------------------------------------------------------------------------ {-# INLINE trim #-} trim :: ByteString -> ByteString trim = snd . S.span isSpace . fst . S.spanEnd isSpace ------------------------------------------------------------------------------ pValueWithParameters :: Parser (ByteString, [(CI ByteString, ByteString)]) pValueWithParameters = do value <- liftM trim (pSpaces *> takeWhile (/= ';')) params <- many pParam return (value, map (first CI.mk) params) where pParam = pSpaces *> char ';' *> pSpaces *> pParameter ------------------------------------------------------------------------------ pContentTypeWithParameters :: Parser ( ByteString , [(CI ByteString, ByteString)] ) pContentTypeWithParameters = do value <- liftM trim (pSpaces *> takeWhile (not . isSep)) params <- many (pSpaces *> satisfy isSep *> pSpaces *> pParameter) return $! (value, map (first CI.mk) params) where isSep c = c == ';' || c == ',' ------------------------------------------------------------------------------ {-# INLINE pToken #-} pToken :: Parser ByteString pToken = takeWhile isToken ------------------------------------------------------------------------------ {-# INLINE isToken #-} isToken :: Char -> Bool isToken c = FS.memberChar c tokenTable ------------------------------------------------------------------------------ tokenTable :: FastSet tokenTable = generateFS (f . toEnum . fromEnum) where f = matchAll [ isAscii , not . isControl , not . isSpace , not . flip elem [ '(', ')', '<', '>', '@', ',', ';' , ':', '\\', '\"', '/', '[', ']' , '?', '=', '{', '}' ] ] ------------------ -- Url encoding -- ------------------ ------------------------------------------------------------------------------ {-# INLINE parseToCompletion #-} parseToCompletion :: Parser a -> ByteString -> Maybe a parseToCompletion p s = toResult $ finish r where r = parse p s toResult (Done _ c) = Just c toResult _ = Nothing ------------------------------------------------------------------------------ type DList a = [a] -> [a] pUrlEscaped :: Parser ByteString pUrlEscaped = do sq <- nextChunk id return $! S.concat $ sq [] where -------------------------------------------------------------------------- nextChunk :: DList ByteString -> Parser (DList ByteString) nextChunk !s = (endOfInput *> pure s) <|> do c <- anyChar case c of '+' -> plusSpace s '%' -> percentEncoded s _ -> unEncoded c s -------------------------------------------------------------------------- percentEncoded :: DList ByteString -> Parser (DList ByteString) percentEncoded !l = do hx <- take 2 when (S.length hx /= 2 || (not $ S.all isHexDigit hx)) $ fail "bad hex in url" let code = w2c ((unsafeFromHex hx) :: Word8) nextChunk $ l . ((S.singleton code) :) -------------------------------------------------------------------------- unEncoded :: Char -> DList ByteString -> Parser (DList ByteString) unEncoded !c !l' = do let l = l' . ((S.singleton c) :) bs <- takeTill (flip elem ['%', '+']) if S.null bs then nextChunk l else nextChunk $ l . (bs :) -------------------------------------------------------------------------- plusSpace :: DList ByteString -> Parser (DList ByteString) plusSpace l = nextChunk (l . ((S.singleton ' ') :)) ------------------------------------------------------------------------------ -- "...Only alphanumerics [0-9a-zA-Z], the special characters "$-_.+!*'()," -- [not including the quotes - ed], and reserved characters used for their -- reserved purposes may be used unencoded within a URL." ------------------------------------------------------------------------------ -- | Decodes an URL-escaped string (see -- ) urlDecode :: ByteString -> Maybe ByteString urlDecode = parseToCompletion pUrlEscaped {-# INLINE urlDecode #-} ------------------------------------------------------------------------------ -- | URL-escapes a string (see -- ) urlEncode :: ByteString -> ByteString urlEncode = toByteString . urlEncodeBuilder {-# INLINE urlEncode #-} ------------------------------------------------------------------------------ -- | URL-escapes a string (see -- ) into a 'Builder'. urlEncodeBuilder :: ByteString -> Builder urlEncodeBuilder = go mempty where go !b !s = maybe b' esc (S.uncons y) where (x,y) = S.span (flip FS.memberChar urlEncodeTable) s b' = b `mappend` fromByteString x esc (c,r) = let b'' = if c == ' ' then b' `mappend` fromWord8 (c2w '+') else b' `mappend` hexd c in go b'' r ------------------------------------------------------------------------------ urlEncodeTable :: FastSet urlEncodeTable = generateFS f where f c = any ($ (w2c c)) [\x -> isAscii x && isAlphaNum x, flip elem [ '$', '_', '-', '.', '!' , '*' , '\'', '(', ')', ',' ] ] ------------------------------------------------------------------------------ hexd :: Char -> Builder hexd c0 = fromWord8 (c2w '%') `mappend` fromWord8 hi `mappend` fromWord8 low where !c = c2w c0 toDigit = c2w . intToDigit !low = toDigit $ fromEnum $ c .&. 0xf !hi = toDigit $ (c .&. 0xf0) `shiftr` 4 shiftr (W8# a#) (I# b#) = I# (word2Int# (uncheckedShiftRL# a# b#)) ------------------------------------------------------------------------------ finish :: Result a -> Result a finish (Partial f) = flip feed "" $ f "" finish x = x --------------------------------------- -- application/x-www-form-urlencoded -- --------------------------------------- ------------------------------------------------------------------------------ -- | Parses a string encoded in @application/x-www-form-urlencoded@ format. parseUrlEncoded :: ByteString -> Map ByteString [ByteString] parseUrlEncoded s = foldr ins Map.empty decoded where -------------------------------------------------------------------------- ins (!k,v) !m = Map.insertWith' (++) k [v] m -------------------------------------------------------------------------- parts :: [(ByteString,ByteString)] parts = map breakApart $ S.splitWith (\c -> c == '&' || c == ';') s -------------------------------------------------------------------------- breakApart = (second (S.drop 1)) . S.break (== '=') -------------------------------------------------------------------------- urldecode = parseToCompletion pUrlEscaped -------------------------------------------------------------------------- decodeOne (a,b) = do !a' <- urldecode a !b' <- urldecode b return $! (a',b') -------------------------------------------------------------------------- decoded = go id parts where go !dl [] = dl [] go !dl (x:xs) = maybe (go dl xs) (\p -> go (dl . (p:)) xs) (decodeOne x) ------------------------------------------------------------------------------ buildUrlEncoded :: Map ByteString [ByteString] -> Builder buildUrlEncoded m = mconcat builders where builders = intersperse (fromWord8 $ c2w '&') $ concatMap encodeVS $ Map.toList m encodeVS (k,vs) = map (encodeOne k) vs encodeOne k v = mconcat [ urlEncodeBuilder k , fromWord8 $ c2w '=' , urlEncodeBuilder v ] ------------------------------------------------------------------------------ printUrlEncoded :: Map ByteString [ByteString] -> ByteString printUrlEncoded = toByteString . buildUrlEncoded -------------------- -- Cookie parsing -- -------------------- ------------------------------------------------------------------------------ -- these definitions try to mirror RFC-2068 (the HTTP/1.1 spec) and RFC-2109 -- (cookie spec): please point out any errors! ------------------------------------------------------------------------------ pCookies :: Parser [Cookie] pCookies = do -- grab kvps and turn to strict bytestrings kvps <- pAvPairs return $! map toCookie $ filter (not . S.isPrefixOf "$" . fst) kvps where toCookie (nm,val) = Cookie nm val Nothing Nothing Nothing False False ------------------------------------------------------------------------------ parseCookie :: ByteString -> Maybe [Cookie] parseCookie = parseToCompletion pCookies ----------------------- -- utility functions -- ----------------------- ------------------------------------------------------------------------------ strictize :: L.ByteString -> ByteString strictize = S.concat . L.toChunks ------------------------------------------------------------------------------ unsafeFromHex :: (Enum a, Num a, Bits a) => ByteString -> a unsafeFromHex = S.foldl' f 0 where #if MIN_VERSION_base(4,5,0) sl = unsafeShiftL #else sl = shiftL #endif f !cnt !i = sl cnt 4 .|. nybble i nybble c | c >= '0' && c <= '9' = toEnum $! fromEnum c - fromEnum '0' | c >= 'a' && c <= 'f' = toEnum $! 10 + fromEnum c - fromEnum 'a' | c >= 'A' && c <= 'F' = toEnum $! 10 + fromEnum c - fromEnum 'A' | otherwise = error $ "bad hex digit: " ++ show c {-# INLINE unsafeFromHex #-} ------------------------------------------------------------------------------ unsafeFromInt :: (Enum a, Num a, Bits a) => ByteString -> a unsafeFromInt = S.foldl' f 0 where f !cnt !i = cnt * 10 + toEnum (digitToInt i) {-# INLINE unsafeFromInt #-} snap-core-0.9.8.0/src/Snap/Internal/Instances.hs0000644000000000000000000000574412565252520017510 0ustar0000000000000000{-# OPTIONS_GHC -fno-warn-orphans #-} {-# LANGUAGE CPP #-} {-# LANGUAGE PackageImports #-} module Snap.Internal.Instances where import Control.Applicative import Control.Monad.CatchIO () import Control.Monad.Cont import Control.Monad.Error import Control.Monad.List import Control.Monad.RWS.Strict hiding (pass) import qualified Control.Monad.RWS.Lazy as LRWS import Control.Monad.Reader import Control.Monad.State.Strict import qualified Control.Monad.State.Lazy as LState import Control.Monad.Writer.Strict hiding (pass) import qualified Control.Monad.Writer.Lazy as LWriter #if !MIN_VERSION_base(4,6,0) import Prelude hiding (catch) #endif ------------------------------------------------------------------------------ import Snap.Internal.Types ------------------------------------------------------------------------------ instance MonadPlus m => MonadPlus (ContT c m) where mzero = lift mzero m `mplus` n = ContT $ \ f -> runContT m f `mplus` runContT n f ------------------------------------------------------------------------------ instance MonadPlus m => Alternative (ContT c m) where empty = mzero (<|>) = mplus ------------------------------------------------------------------------------ instance MonadSnap m => MonadSnap (ContT c m) where liftSnap = lift . liftSnap ------------------------------------------------------------------------------ instance (MonadSnap m, Error e) => MonadSnap (ErrorT e m) where liftSnap = lift . liftSnap ------------------------------------------------------------------------------ instance MonadSnap m => MonadSnap (ListT m) where liftSnap = lift . liftSnap ------------------------------------------------------------------------------ instance (MonadSnap m, Monoid w) => MonadSnap (RWST r w s m) where liftSnap = lift . liftSnap ------------------------------------------------------------------------------ instance (MonadSnap m, Monoid w) => MonadSnap (LRWS.RWST r w s m) where liftSnap = lift . liftSnap ------------------------------------------------------------------------------ instance MonadSnap m => MonadSnap (ReaderT r m) where liftSnap = lift . liftSnap ------------------------------------------------------------------------------ instance MonadSnap m => MonadSnap (StateT s m) where liftSnap = lift . liftSnap ------------------------------------------------------------------------------ instance MonadSnap m => MonadSnap (LState.StateT s m) where liftSnap = lift . liftSnap ------------------------------------------------------------------------------ instance (MonadSnap m, Monoid w) => MonadSnap (WriterT w m) where liftSnap = lift . liftSnap ------------------------------------------------------------------------------ instance (MonadSnap m, Monoid w) => MonadSnap (LWriter.WriterT w m) where liftSnap = lift . liftSnap snap-core-0.9.8.0/src/Snap/Internal/Routing.hs0000644000000000000000000002217512565252520017205 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE Rank2Types #-} module Snap.Internal.Routing where ------------------------------------------------------------------------------ import Control.Applicative ((<|>)) import Data.ByteString (ByteString) import Data.ByteString.Internal (c2w) import qualified Data.ByteString as B import Data.Monoid import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as H import qualified Data.Map as Map ------------------------------------------------------------------------------ import Snap.Internal.Http.Types import Snap.Internal.Parsing import Snap.Internal.Types ------------------------------------------------------------------------------ {-| The internal data type you use to build a routing tree. Matching is done unambiguously. 'Capture' and 'Dir' routes can have a "fallback" route: - For 'Capture', the fallback is routed when there is nothing to capture - For 'Dir', the fallback is routed when we can't find a route in its map Fallback routes are stacked: i.e. for a route like: > Dir [("foo", Capture "bar" (Action bar) NoRoute)] baz visiting the URI foo/ will result in the "bar" capture being empty and triggering its fallback. It's NoRoute, so we go to the nearest parent fallback and try that, which is the baz action. -} data Route a m = Action ((MonadSnap m) => m a) -- wraps a 'Snap' action -- captures the dir in a param | Capture ByteString (Route a m) (Route a m) -- match on a dir | Dir (HashMap ByteString (Route a m)) (Route a m) | NoRoute ------------------------------------------------------------------------------ instance Monoid (Route a m) where mempty = NoRoute mappend NoRoute r = r mappend l@(Action a) r = case r of (Action a') -> Action (a <|> a') (Capture p r' fb) -> Capture p r' (mappend fb l) (Dir _ _) -> mappend (Dir H.empty l) r NoRoute -> l -- Whenever we're unioning two Captures and their capture variables -- differ, we have an ambiguity. We resolve this in the following order: -- 1. Prefer whichever route is longer -- 2. Else, prefer whichever has the earliest non-capture -- 3. Else, prefer the right-hand side mappend l@(Capture p r' fb) r = case r of (Action _) -> Capture p r' (mappend fb r) (Capture p' r'' fb') | p == p' -> Capture p (mappend r' r'') (mappend fb fb') | rh' > rh'' -> Capture p r' (mappend fb r) | rh' < rh'' -> Capture p' r'' (mappend fb' l) | en' < en'' -> Capture p r' (mappend fb r) | otherwise -> Capture p' r'' (mappend fb' l) where rh' = routeHeight r' rh'' = routeHeight r'' en' = routeEarliestNC r' 1 en'' = routeEarliestNC r'' 1 (Dir rm fb') -> Dir rm (mappend fb' l) NoRoute -> l mappend l@(Dir rm fb) r = case r of (Action _) -> Dir rm (mappend fb r) (Capture _ _ _) -> Dir rm (mappend fb r) (Dir rm' fb') -> Dir (H.unionWith mappend rm rm') (mappend fb fb') NoRoute -> l ------------------------------------------------------------------------------ routeHeight :: Route a m -> Int routeHeight r = case r of NoRoute -> 1 (Action _) -> 1 (Capture _ r' _) -> 1 + routeHeight r' (Dir rm _) -> 1 + foldl max 1 (map routeHeight $ H.elems rm) {-# INLINE routeHeight #-} ------------------------------------------------------------------------------ routeEarliestNC :: Route a m -> Int -> Int routeEarliestNC r n = case r of NoRoute -> n (Action _) -> n (Capture _ r' _) -> routeEarliestNC r' n+1 (Dir _ _) -> n {-# INLINE routeEarliestNC #-} ------------------------------------------------------------------------------ -- | A web handler which, given a mapping from URL entry points to web -- handlers, efficiently routes requests to the correct handler. -- -- The URL entry points are given as relative paths, for example: -- -- > route [ ("foo/bar/quux", fooBarQuux) ] -- -- If the URI of the incoming request is -- -- > /foo/bar/quux -- -- or -- -- > /foo/bar/quux/...anything... -- -- then the request will be routed to \"@fooBarQuux@\", with 'rqContextPath' -- set to \"@\/foo\/bar\/quux\/@\" and 'rqPathInfo' set to -- \"@...anything...@\". -- -- A path component within an URL entry point beginning with a colon (\"@:@\") -- is treated as a /variable capture/; the corresponding path component within -- the request URI will be entered into the 'rqParams' parameters mapping with -- the given name. For instance, if the routes were: -- -- > route [ ("foo/:bar/baz", fooBazHandler) ] -- -- Then a request for \"@\/foo\/saskatchewan\/baz@\" would be routed to -- @fooBazHandler@ with a mapping for: -- -- > "bar" => "saskatchewan" -- -- in its parameters table. -- -- Longer paths are matched first, and specific routes are matched before -- captures. That is, if given routes: -- -- > [ ("a", h1), ("a/b", h2), ("a/:x", h3) ] -- -- a request for \"@\/a\/b@\" will go to @h2@, \"@\/a\/s@\" for any /s/ will -- go to @h3@, and \"@\/a@\" will go to @h1@. -- -- The following example matches \"@\/article@\" to an article index, -- \"@\/login@\" to a login, and \"@\/article\/...@\" to an article renderer. -- -- > route [ ("article", renderIndex) -- > , ("article/:id", renderArticle) -- > , ("login", method POST doLogin) ] -- -- -- /URL decoding/ -- -- A short note about URL decoding: path matching and variable capture are done -- on /decoded/ URLs, but the contents of 'rqContextPath' and 'rqPathInfo' will -- contain the original encoded URL, i.e. what the user entered. For example, -- in the following scenario: -- -- > route [ ("a b c d/", foo ) ] -- -- A request for \"@/a+b+c+d@\" will be sent to @foo@ with 'rqContextPath' set -- to \"/a+b+c+d/\". -- -- This behaviour changed as of Snap 0.6.1; previous versions had unspecified -- (and buggy!) semantics here. -- route :: MonadSnap m => [(ByteString, m a)] -> m a route rts = do p <- getsRequest rqPathInfo route' (return ()) [] (splitPath p) Map.empty rts' where rts' = mconcat (map pRoute rts) {-# INLINE route #-} ------------------------------------------------------------------------------ -- | The 'routeLocal' function is the same as 'route'', except it doesn't -- change the request's context path. This is useful if you want to route to a -- particular handler but you want that handler to receive the 'rqPathInfo' as -- it is. routeLocal :: MonadSnap m => [(ByteString, m a)] -> m a routeLocal rts = do req <- getRequest let ctx = rqContextPath req let p = rqPathInfo req let md = modifyRequest $ \r -> r {rqContextPath=ctx, rqPathInfo=p} (route' md [] (splitPath p) Map.empty rts') <|> (md >> pass) where rts' = mconcat (map pRoute rts) {-# INLINE routeLocal #-} ------------------------------------------------------------------------------ splitPath :: ByteString -> [ByteString] splitPath = B.splitWith (== (c2w '/')) {-# INLINE splitPath #-} ------------------------------------------------------------------------------ pRoute :: MonadSnap m => (ByteString, m a) -> Route a m pRoute (r, a) = foldr f (Action a) hier where hier = filter (not . B.null) $ B.splitWith (== (c2w '/')) r f s rt = if B.head s == c2w ':' then Capture (B.tail s) rt NoRoute else Dir (H.fromList [(s, rt)]) NoRoute {-# INLINE pRoute #-} ------------------------------------------------------------------------------ route' :: MonadSnap m => m () -- ^ action to run before we call the user handler -> [ByteString] -- ^ the \"context\"; the list of path segments we've -- already successfully matched, in reverse order -> [ByteString] -- ^ the list of path segments we haven't yet matched -> Params -> Route a m -> m a route' pre !ctx _ !params (Action action) = localRequest (updateContextPath (B.length ctx') . updateParams) (pre >> action) where ctx' = B.intercalate (B.pack [c2w '/']) (reverse ctx) updateParams req = req { rqParams = Map.unionWith (++) params (rqParams req) } route' pre !ctx [] !params (Capture _ _ fb) = route' pre ctx [] params fb route' pre !ctx paths@(cwd:rest) !params (Capture p rt fb) | B.null cwd = fallback | otherwise = m <|> fallback where fallback = route' pre ctx paths params fb m = maybe pass (\cwd' -> let params' = Map.insertWith (++) p [cwd'] params in route' pre (cwd:ctx) rest params' rt) (urlDecode cwd) route' pre !ctx [] !params (Dir _ fb) = route' pre ctx [] params fb route' pre !ctx (cwd:rest) !params (Dir rtm fb) = do cwd' <- maybe pass return $ urlDecode cwd case H.lookup cwd' rtm of Just rt -> (route' pre (cwd:ctx) rest params rt) <|> (route' pre ctx (cwd:rest) params fb) Nothing -> route' pre ctx (cwd:rest) params fb route' _ _ _ _ NoRoute = pass snap-core-0.9.8.0/src/Snap/Internal/Types.hs0000644000000000000000000012316612565252520016664 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE Rank2Types #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE StandaloneDeriving #-} module Snap.Internal.Types where ------------------------------------------------------------------------------ import Blaze.ByteString.Builder import Blaze.ByteString.Builder.Char.Utf8 import Control.Applicative import Control.Exception (SomeException, throwIO, ErrorCall(..)) import Control.Monad import Control.Monad.CatchIO import qualified Control.Monad.Error.Class as EC import Control.Monad.State import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as S import qualified Data.ByteString.Lazy.Char8 as L import Data.CaseInsensitive (CI) import Data.Int import Data.IORef import Data.Maybe import Data.Monoid import Data.Time import qualified Data.Text as T import qualified Data.Text.Lazy as LT import Data.Typeable #if MIN_VERSION_base(4,6,0) import Prelude hiding (take) #else import Prelude hiding (catch, take) #endif import System.PosixCompat.Files hiding (setFileSize) import System.Posix.Types (FileOffset) ------------------------------------------------------------------------------ import Snap.Internal.Exceptions import Snap.Internal.Http.Types import Snap.Internal.Iteratee.Debug import Snap.Iteratee hiding (map) import qualified Snap.Types.Headers as H import Snap.Util.Readable ------------------------------------------------------------------------------ -------------------- -- The Snap Monad -- -------------------- {-| 'Snap' is the 'Monad' that user web handlers run in. 'Snap' gives you: 1. stateful access to fetch or modify an HTTP 'Request' 2. stateful access to fetch or modify an HTTP 'Response' 3. failure \/ 'Alternative' \/ 'MonadPlus' semantics: a 'Snap' handler can choose not to handle a given request, using 'empty' or its synonym 'pass', and you can try alternative handlers with the '<|>' operator: > a :: Snap String > a = pass > > b :: Snap String > b = return "foo" > > c :: Snap String > c = a <|> b -- try running a, if it fails then try b 4. convenience functions ('writeBS', 'writeLBS', 'writeText', 'writeLazyText', 'addToOutput') for queueing output to be written to the 'Response': > a :: (forall a . Enumerator a) -> Snap () > a someEnumerator = do > writeBS "I'm a strict bytestring" > writeLBS "I'm a lazy bytestring" > writeText "I'm strict text" > addToOutput someEnumerator 5. early termination: if you call 'finishWith': > a :: Snap () > a = do > modifyResponse $ setResponseStatus 500 "Internal Server Error" > writeBS "500 error" > r <- getResponse > finishWith r then any subsequent processing will be skipped and supplied 'Response' value will be returned from 'runSnap' as-is. 6. access to the 'IO' monad through a 'MonadIO' instance: > a :: Snap () > a = liftIO fireTheMissiles 7. the ability to set or extend a timeout which will kill the handler thread after @N@ seconds of inactivity (the default is 20 seconds): > a :: Snap () > a = setTimeout 30 8. throw and catch exceptions using a 'MonadCatchIO' instance: > foo :: Snap () > foo = bar `catch` \(e::SomeException) -> baz > where > bar = throw FooException 9. log a message to the error log: > foo :: Snap () > foo = logError "grumble." You may notice that most of the type signatures in this module contain a @(MonadSnap m) => ...@ typeclass constraint. 'MonadSnap' is a typeclass which, in essence, says \"you can get back to the 'Snap' monad from here\". Using 'MonadSnap' you can extend the 'Snap' monad with additional functionality and still have access to most of the 'Snap' functions without writing 'lift' everywhere. Instances are already provided for most of the common monad transformers ('ReaderT', 'WriterT', 'StateT', etc.). -} ------------------------------------------------------------------------------ -- | 'MonadSnap' is a type class, analogous to 'MonadIO' for 'IO', that makes -- it easy to wrap 'Snap' inside monad transformers. class (Monad m, MonadIO m, MonadCatchIO m, MonadPlus m, Functor m, Applicative m, Alternative m) => MonadSnap m where liftSnap :: Snap a -> m a ------------------------------------------------------------------------------ data SnapResult a = SnapValue a | PassOnProcessing String | EarlyTermination Response ------------------------------------------------------------------------------ newtype Snap a = Snap { unSnap :: StateT SnapState (Iteratee ByteString IO) (SnapResult a) } ------------------------------------------------------------------------------ data SnapState = SnapState { _snapRequest :: Request , _snapResponse :: Response , _snapLogError :: ByteString -> IO () , _snapModifyTimeout :: (Int -> Int) -> IO () } ------------------------------------------------------------------------------ instance Monad Snap where (>>=) = snapBind return = snapReturn fail = snapFail ------------------------------------------------------------------------------ snapBind :: Snap a -> (a -> Snap b) -> Snap b snapBind (Snap m) f = Snap $ do res <- m case res of SnapValue a -> unSnap $! f a PassOnProcessing r -> return $! PassOnProcessing r EarlyTermination r -> return $! EarlyTermination r {-# INLINE snapBind #-} snapReturn :: a -> Snap a snapReturn = Snap . return . SnapValue {-# INLINE snapReturn #-} snapFail :: String -> Snap a snapFail !m = Snap $! return $! PassOnProcessing m {-# INLINE snapFail #-} ------------------------------------------------------------------------------ instance MonadIO Snap where liftIO m = Snap $! liftM SnapValue $! liftIO m ------------------------------------------------------------------------------ instance MonadCatchIO Snap where catch (Snap m) handler = Snap $! m `catch` h where h e = do rethrowIfUncatchable $ fromException e maybe (throw e) (\e' -> let (Snap z) = handler e' in z) (fromException e) block (Snap m) = Snap $ block m unblock (Snap m) = Snap $ unblock m ------------------------------------------------------------------------------ rethrowIfUncatchable :: (MonadCatchIO m) => Maybe UncatchableException -> m () rethrowIfUncatchable Nothing = return () rethrowIfUncatchable (Just e) = throw e ------------------------------------------------------------------------------ instance MonadPlus Snap where mzero = Snap $! return $! PassOnProcessing "" a `mplus` b = Snap $! do r <- unSnap a -- redundant just in case ordering by frequency helps here. case r of SnapValue _ -> return r PassOnProcessing _ -> unSnap b _ -> return r ------------------------------------------------------------------------------ instance (EC.MonadError String) Snap where throwError = fail catchError act hndl = Snap $ do r <- unSnap act -- redundant just in case ordering by frequency helps here. case r of SnapValue _ -> return r PassOnProcessing m -> unSnap $ hndl m _ -> return r ------------------------------------------------------------------------------ instance Functor Snap where fmap = liftM ------------------------------------------------------------------------------ instance Applicative Snap where pure = return (<*>) = ap ------------------------------------------------------------------------------ instance Alternative Snap where empty = mzero (<|>) = mplus ------------------------------------------------------------------------------ instance MonadSnap Snap where liftSnap = id ------------------------------------------------------------------------------ -- | The Typeable instance is here so Snap can be dynamically executed with -- Hint. snapTyCon :: TyCon #if MIN_VERSION_base(4,4,0) snapTyCon = mkTyCon3 "snap-core" "Snap.Core" "Snap" #else snapTyCon = mkTyCon "Snap.Core.Snap" #endif {-# NOINLINE snapTyCon #-} #if __GLASGOW_HASKELL__ < 708 instance Typeable1 Snap where typeOf1 _ = mkTyConApp snapTyCon [] #else deriving instance Typeable Snap #endif ------------------------------------------------------------------------------ liftIter :: MonadSnap m => Iteratee ByteString IO a -> m a liftIter i = liftSnap $ Snap (lift i >>= return . SnapValue) ------------------------------------------------------------------------------ -- | Sends the request body through an iteratee (data consumer) and -- returns the result. -- -- If the iteratee you pass in here throws an exception, Snap will attempt to -- clear the rest of the unread request body before rethrowing the exception. -- If your iteratee used 'terminateConnection', however, Snap will give up and -- immediately close the socket. runRequestBody :: MonadSnap m => Iteratee ByteString IO a -> m a runRequestBody iter = do bumpTimeout <- liftM ($ max 5) getTimeoutModifier req <- getRequest senum <- liftIO $ readIORef $ rqBody req let (SomeEnumerator enum) = senum -- make sure the iteratee consumes all of the output let iter' = handle bumpTimeout req (iter >>= \a -> skipToEnd bumpTimeout >> return a) -- run the iteratee step <- liftIO $ runIteratee iter' result <- liftIter $ enum step -- stuff a new dummy enumerator into the request, so you can only try to -- read the request body from the socket once resetEnum req return result where resetEnum req = liftIO $ writeIORef (rqBody req) $ SomeEnumerator $ joinI . take 0 skipToEnd bump = killIfTooSlow bump 500 5 skipToEof `catchError` \e -> throwError $ ConnectionTerminatedException e handle bump req = (`catches` [ Handler $ \(e :: ConnectionTerminatedException) -> do let en = SomeEnumerator $ const $ throwError e liftIO $ writeIORef (rqBody req) en throwError e , Handler $ \(e :: SomeException) -> do resetEnum req skipToEnd bump throwError e ]) ------------------------------------------------------------------------------ -- | Returns the request body as a lazy bytestring. -- -- This function is deprecated as of 0.6; it places no limits on the size of -- the request being read, and as such, if used, can result in a -- denial-of-service attack on your server. Please use 'readRequestBody' -- instead. getRequestBody :: MonadSnap m => m L.ByteString getRequestBody = liftM L.fromChunks $ runRequestBody consume {-# INLINE getRequestBody #-} {-# DEPRECATED getRequestBody "As of 0.6, please use 'readRequestBody' instead" #-} ------------------------------------------------------------------------------ -- | Returns the request body as a lazy bytestring. /New in 0.6./ readRequestBody :: MonadSnap m => Int64 -- ^ size of the largest request body we're willing -- to accept. If a request body longer than this is -- received, a 'TooManyBytesReadException' is -- thrown. See 'takeNoMoreThan'. -> m L.ByteString readRequestBody sz = liftM L.fromChunks $ runRequestBody $ joinI $ takeNoMoreThan sz $$ consume ------------------------------------------------------------------------------ -- | Normally Snap is careful to ensure that the request body is fully -- consumed after your web handler runs, but before the 'Response' enumerator -- is streamed out the socket. If you want to transform the request body into -- some output in O(1) space, you should use this function. -- -- Note that upon calling this function, response processing finishes early as -- if you called 'finishWith'. Make sure you set any content types, headers, -- cookies, etc. before you call this function. -- transformRequestBody :: (forall a . Enumerator Builder IO a) -- ^ the output 'Iteratee' is passed to this -- 'Enumerator', and then the resulting 'Iteratee' is -- fed the request body stream. Your 'Enumerator' is -- responsible for transforming the input. -> Snap () transformRequestBody trans = do req <- getRequest let ioref = rqBody req senum <- liftIO $ readIORef ioref let (SomeEnumerator enum') = senum let enum = mapEnum toByteString fromByteString enum' liftIO $ writeIORef ioref (SomeEnumerator enumEOF) origRsp <- getResponse let rsp = setResponseBody (\writeEnd -> do let i = iterateeDebugWrapperWith showBuilder "transformRequestBody" $ trans writeEnd st <- liftIO $ runIteratee i enum st) $ origRsp { rspTransformingRqBody = True } finishWith rsp ------------------------------------------------------------------------------ -- | Short-circuits a 'Snap' monad action early, storing the given -- 'Response' value in its state. finishWith :: MonadSnap m => Response -> m a finishWith = liftSnap . Snap . return . EarlyTermination {-# INLINE finishWith #-} ------------------------------------------------------------------------------ -- | Capture the flow of control in case a handler calls 'finishWith'. -- -- /WARNING/: in the event of a call to 'transformRequestBody' it is possible -- to violate HTTP protocol safety when using this function. If you call -- 'catchFinishWith' it is suggested that you do not modify the body of the -- 'Response' which was passed to the 'finishWith' call. catchFinishWith :: Snap a -> Snap (Either Response a) catchFinishWith (Snap m) = Snap $ do r <- m case r of SnapValue a -> return $! SnapValue $! Right a PassOnProcessing e -> return $! PassOnProcessing e EarlyTermination resp -> return $! SnapValue $! Left resp {-# INLINE catchFinishWith #-} ------------------------------------------------------------------------------ -- | Fails out of a 'Snap' monad action. This is used to indicate -- that you choose not to handle the given request within the given -- handler. pass :: MonadSnap m => m a pass = empty ------------------------------------------------------------------------------ -- | Runs a 'Snap' monad action only if the request's HTTP method matches -- the given method. method :: MonadSnap m => Method -> m a -> m a method m action = do req <- getRequest unless (rqMethod req == m) pass action {-# INLINE method #-} ------------------------------------------------------------------------------ -- | Runs a 'Snap' monad action only if the request's HTTP method matches -- one of the given methods. methods :: MonadSnap m => [Method] -> m a -> m a methods ms action = do req <- getRequest unless (rqMethod req `elem` ms) pass action {-# INLINE methods #-} ------------------------------------------------------------------------------ -- Appends n bytes of the path info to the context path with a -- trailing slash. updateContextPath :: Int -> Request -> Request updateContextPath n req | n > 0 = req { rqContextPath = ctx , rqPathInfo = pinfo } | otherwise = req where ctx' = S.take n (rqPathInfo req) ctx = S.concat [rqContextPath req, ctx', "/"] pinfo = S.drop (n+1) (rqPathInfo req) ------------------------------------------------------------------------------ -- Runs a 'Snap' monad action only if the 'rqPathInfo' matches the given -- predicate. pathWith :: MonadSnap m => (ByteString -> ByteString -> Bool) -> ByteString -> m a -> m a pathWith c p action = do req <- getRequest unless (c p (rqPathInfo req)) pass localRequest (updateContextPath $ S.length p) action ------------------------------------------------------------------------------ -- | Runs a 'Snap' monad action only when the 'rqPathInfo' of the request -- starts with the given path. For example, -- -- > dir "foo" handler -- -- Will fail if 'rqPathInfo' is not \"@\/foo@\" or \"@\/foo\/...@\", and will -- add @\"foo\/\"@ to the handler's local 'rqContextPath'. dir :: MonadSnap m => ByteString -- ^ path component to match -> m a -- ^ handler to run -> m a dir = pathWith f where f dr pinfo = dr == x where (x,_) = S.break (=='/') pinfo {-# INLINE dir #-} ------------------------------------------------------------------------------ -- | Runs a 'Snap' monad action only for requests where 'rqPathInfo' is -- exactly equal to the given string. If the path matches, locally sets -- 'rqContextPath' to the old value of 'rqPathInfo', sets 'rqPathInfo'=\"\", -- and runs the given handler. path :: MonadSnap m => ByteString -- ^ path to match against -> m a -- ^ handler to run -> m a path = pathWith (==) {-# INLINE path #-} ------------------------------------------------------------------------------ -- | Runs a 'Snap' monad action only when the first path component is -- successfully parsed as the argument to the supplied handler function. pathArg :: (Readable a, MonadSnap m) => (a -> m b) -> m b pathArg f = do req <- getRequest let (p,_) = S.break (=='/') (rqPathInfo req) a <- fromBS p localRequest (updateContextPath $ S.length p) (f a) ------------------------------------------------------------------------------ -- | Runs a 'Snap' monad action only when 'rqPathInfo' is empty. ifTop :: MonadSnap m => m a -> m a ifTop = path "" {-# INLINE ifTop #-} ------------------------------------------------------------------------------ -- | Local Snap version of 'get'. sget :: Snap SnapState sget = Snap $ liftM SnapValue get {-# INLINE sget #-} ------------------------------------------------------------------------------ -- | Local Snap monad version of 'modify'. smodify :: (SnapState -> SnapState) -> Snap () smodify f = Snap $ modify f >> return (SnapValue ()) {-# INLINE smodify #-} ------------------------------------------------------------------------------ -- | Grabs the 'Request' object out of the 'Snap' monad. getRequest :: MonadSnap m => m Request getRequest = liftSnap $ liftM _snapRequest sget {-# INLINE getRequest #-} ------------------------------------------------------------------------------ -- | Grabs something out of the 'Request' object, using the given projection -- function. See 'gets'. getsRequest :: MonadSnap m => (Request -> a) -> m a getsRequest f = liftSnap $ liftM (f . _snapRequest) sget {-# INLINE getsRequest #-} ------------------------------------------------------------------------------ -- | Grabs the 'Response' object out of the 'Snap' monad. getResponse :: MonadSnap m => m Response getResponse = liftSnap $ liftM _snapResponse sget {-# INLINE getResponse #-} ------------------------------------------------------------------------------ -- | Grabs something out of the 'Response' object, using the given projection -- function. See 'gets'. getsResponse :: MonadSnap m => (Response -> a) -> m a getsResponse f = liftSnap $ liftM (f . _snapResponse) sget {-# INLINE getsResponse #-} ------------------------------------------------------------------------------ -- | Puts a new 'Response' object into the 'Snap' monad. putResponse :: MonadSnap m => Response -> m () putResponse r = liftSnap $ smodify $ \ss -> ss { _snapResponse = r } {-# INLINE putResponse #-} ------------------------------------------------------------------------------ -- | Puts a new 'Request' object into the 'Snap' monad. putRequest :: MonadSnap m => Request -> m () putRequest r = liftSnap $ smodify $ \ss -> ss { _snapRequest = r } {-# INLINE putRequest #-} ------------------------------------------------------------------------------ -- | Modifies the 'Request' object stored in a 'Snap' monad. modifyRequest :: MonadSnap m => (Request -> Request) -> m () modifyRequest f = liftSnap $ smodify $ \ss -> ss { _snapRequest = f $ _snapRequest ss } {-# INLINE modifyRequest #-} ------------------------------------------------------------------------------ -- | Modifes the 'Response' object stored in a 'Snap' monad. modifyResponse :: MonadSnap m => (Response -> Response) -> m () modifyResponse f = liftSnap $ smodify $ \ss -> ss { _snapResponse = f $ _snapResponse ss } {-# INLINE modifyResponse #-} ------------------------------------------------------------------------------ -- | Performs a redirect by setting the @Location@ header to the given target -- URL/path and the status code to 302 in the 'Response' object stored in a -- 'Snap' monad. Note that the target URL is not validated in any way. -- Consider using 'redirect\'' instead, which allows you to choose the correct -- status code. redirect :: MonadSnap m => ByteString -> m a redirect target = redirect' target 302 {-# INLINE redirect #-} ------------------------------------------------------------------------------ -- | Performs a redirect by setting the @Location@ header to the given target -- URL/path and the status code (should be one of 301, 302, 303 or 307) in the -- 'Response' object stored in a 'Snap' monad. Note that the target URL is not -- validated in any way. redirect' :: MonadSnap m => ByteString -> Int -> m a redirect' target status = do r <- getResponse finishWith $ setResponseCode status $ setContentLength 0 $ modifyResponseBody (const $ enumBuilder mempty) $ setHeader "Location" target r {-# INLINE redirect' #-} ------------------------------------------------------------------------------ -- | Log an error message in the 'Snap' monad logError :: MonadSnap m => ByteString -> m () logError s = liftSnap $ Snap $ gets _snapLogError >>= (\l -> liftIO $ l s) >> return (SnapValue ()) {-# INLINE logError #-} ------------------------------------------------------------------------------ -- | Adds the output from the given enumerator to the 'Response' -- stored in the 'Snap' monad state. addToOutput :: MonadSnap m => (forall a . Enumerator Builder IO a) -- ^ output to add -> m () addToOutput enum = modifyResponse $ modifyResponseBody (>==> enum) ------------------------------------------------------------------------------ -- | Adds the given 'Builder' to the body of the 'Response' stored in the -- | 'Snap' monad state. writeBuilder :: MonadSnap m => Builder -> m () writeBuilder b = addToOutput $ enumBuilder b {-# INLINE writeBuilder #-} ------------------------------------------------------------------------------ -- | Adds the given strict 'ByteString' to the body of the 'Response' stored -- in the 'Snap' monad state. -- -- Warning: This function is intentionally non-strict. If any pure -- exceptions are raised by the expression creating the 'ByteString', -- the exception won't actually be raised within the Snap handler. writeBS :: MonadSnap m => ByteString -> m () writeBS s = writeBuilder $ fromByteString s ------------------------------------------------------------------------------ -- | Adds the given lazy 'L.ByteString' to the body of the 'Response' stored -- in the 'Snap' monad state. -- -- Warning: This function is intentionally non-strict. If any pure -- exceptions are raised by the expression creating the 'ByteString', -- the exception won't actually be raised within the Snap handler. writeLBS :: MonadSnap m => L.ByteString -> m () writeLBS s = writeBuilder $ fromLazyByteString s ------------------------------------------------------------------------------ -- | Adds the given strict 'T.Text' to the body of the 'Response' stored in -- the 'Snap' monad state. -- -- Warning: This function is intentionally non-strict. If any pure -- exceptions are raised by the expression creating the 'ByteString', -- the exception won't actually be raised within the Snap handler. writeText :: MonadSnap m => T.Text -> m () writeText s = writeBuilder $ fromText s ------------------------------------------------------------------------------ -- | Adds the given lazy 'LT.Text' to the body of the 'Response' stored in the -- 'Snap' monad state. -- -- Warning: This function is intentionally non-strict. If any pure -- exceptions are raised by the expression creating the 'ByteString', -- the exception won't actually be raised within the Snap handler. writeLazyText :: MonadSnap m => LT.Text -> m () writeLazyText s = writeBuilder $ fromLazyText s ------------------------------------------------------------------------------ -- | Sets the output to be the contents of the specified file. -- -- Calling 'sendFile' will overwrite any output queued to be sent in the -- 'Response'. If the response body is not modified after the call to -- 'sendFile', Snap will use the efficient @sendfile()@ system call on -- platforms that support it. -- -- If the response body is modified (using 'modifyResponseBody'), the file -- will be read using @mmap()@. sendFile :: (MonadSnap m) => FilePath -> m () sendFile f = modifyResponse $ \r -> r { rspBody = SendFile f Nothing } ------------------------------------------------------------------------------ -- | Sets the output to be the contents of the specified file, within the -- given (start,end) range. -- -- Calling 'sendFilePartial' will overwrite any output queued to be sent in -- the 'Response'. If the response body is not modified after the call to -- 'sendFilePartial', Snap will use the efficient @sendfile()@ system call on -- platforms that support it. -- -- If the response body is modified (using 'modifyResponseBody'), the file -- will be read using @mmap()@. sendFilePartial :: (MonadSnap m) => FilePath -> (Int64,Int64) -> m () sendFilePartial f rng = modifyResponse $ \r -> r { rspBody = SendFile f (Just rng) } ------------------------------------------------------------------------------ -- | Runs a 'Snap' action with a locally-modified 'Request' state -- object. The 'Request' object in the Snap monad state after the call -- to localRequest will be unchanged. localRequest :: MonadSnap m => (Request -> Request) -> m a -> m a localRequest f m = do req <- getRequest runAct req <|> (putRequest req >> pass) where runAct req = do modifyRequest f result <- m putRequest req return result {-# INLINE localRequest #-} ------------------------------------------------------------------------------ -- | Fetches the 'Request' from state and hands it to the given action. withRequest :: MonadSnap m => (Request -> m a) -> m a withRequest = (getRequest >>=) {-# INLINE withRequest #-} ------------------------------------------------------------------------------ -- | Fetches the 'Response' from state and hands it to the given action. withResponse :: MonadSnap m => (Response -> m a) -> m a withResponse = (getResponse >>=) {-# INLINE withResponse #-} ------------------------------------------------------------------------------ -- | Modifies the 'Request' in the state to set the 'rqRemoteAddr' -- field to the value in the X-Forwarded-For header. If the header is -- not present, this action has no effect. -- -- This action should be used only when working behind a reverse http -- proxy that sets the X-Forwarded-For header. This is the only way to -- ensure the value in the X-Forwarded-For header can be trusted. -- -- This is provided as a filter so actions that require the remote -- address can get it in a uniform manner. It has specifically limited -- functionality to ensure that its transformation can be trusted, -- when used correctly. ipHeaderFilter :: MonadSnap m => m () ipHeaderFilter = ipHeaderFilter' "x-forwarded-for" ------------------------------------------------------------------------------ -- | Modifies the 'Request' in the state to set the 'rqRemoteAddr' -- field to the value from the header specified. If the header -- specified is not present, this action has no effect. -- -- This action should be used only when working behind a reverse http -- proxy that sets the header being looked at. This is the only way to -- ensure the value in the header can be trusted. -- -- This is provided as a filter so actions that require the remote -- address can get it in a uniform manner. It has specifically limited -- functionality to ensure that its transformation can be trusted, -- when used correctly. ipHeaderFilter' :: MonadSnap m => CI ByteString -> m () ipHeaderFilter' header = do headerContents <- getHeader header <$> getRequest let whitespace = [ ' ', '\t', '\r', '\n' ] ipChrs = [ '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' ] trim f s = f (`elem` s) clean = trim S.takeWhile ipChrs . trim S.dropWhile whitespace setIP ip = modifyRequest $ \rq -> rq { rqRemoteAddr = clean ip } maybe (return ()) setIP headerContents ------------------------------------------------------------------------------ -- | This function brackets a Snap action in resource acquisition and -- release. This is provided because MonadCatchIO's 'bracket' function -- doesn't work properly in the case of a short-circuit return from -- the action being bracketed. -- -- In order to prevent confusion regarding the effects of the -- aquisition and release actions on the Snap state, this function -- doesn't accept Snap actions for the acquire or release actions. -- -- This function will run the release action in all cases where the -- acquire action succeeded. This includes the following behaviors -- from the bracketed Snap action. -- -- 1. Normal completion -- -- 2. Short-circuit completion, either from calling 'fail' or 'finishWith' -- -- 3. An exception being thrown. bracketSnap :: IO a -> (a -> IO b) -> (a -> Snap c) -> Snap c bracketSnap before after thing = block . Snap $ do a <- liftIO before let after' = liftIO $ after a (Snap thing') = thing a r <- unblock thing' `onException` after' _ <- after' return r ------------------------------------------------------------------------------ -- | This exception is thrown if the handler you supply to 'runSnap' fails. data NoHandlerException = NoHandlerException String deriving (Eq, Typeable) ------------------------------------------------------------------------------ instance Show NoHandlerException where show (NoHandlerException e) = "No handler for request: failure was " ++ e ------------------------------------------------------------------------------ instance Exception NoHandlerException ------------------------------------------------------------------------------ -- | Terminate the HTTP session with the given exception. terminateConnection :: (Exception e, MonadCatchIO m) => e -> m a terminateConnection = throw . ConnectionTerminatedException . toException ------------------------------------------------------------------------------ -- | Terminate the HTTP session and hand control to some external handler, -- escaping all further HTTP traffic. -- -- The external handler takes two arguments: a function to modify the thread's -- timeout, and a write end to the socket. escapeHttp :: MonadCatchIO m => EscapeHttpHandler -> m () escapeHttp = throw . EscapeHttpException ------------------------------------------------------------------------------ -- | Runs a 'Snap' monad action in the 'Iteratee IO' monad. runSnap :: Snap a -> (ByteString -> IO ()) -> ((Int -> Int) -> IO ()) -> Request -> Iteratee ByteString IO (Request,Response) runSnap (Snap m) logerr timeoutAction req = do (r, ss') <- runStateT m ss let resp = case r of SnapValue _ -> _snapResponse ss' PassOnProcessing _ -> fourohfour EarlyTermination x -> x let req' = _snapRequest ss' resp' <- liftIO $ fixupResponse req' resp return (req', resp') where -------------------------------------------------------------------------- fourohfour = do clearContentLength $ setResponseStatus 404 "Not Found" $ setResponseBody enum404 $ emptyResponse -------------------------------------------------------------------------- enum404 = enumBuilder $ mconcat $ map fromByteString html -------------------------------------------------------------------------- html = [ S.concat [ "\n" , "\n" , "\n" , "Not found\n" , "\n" , "\n" , "No handler accepted \"" ] , rqURI req , "\"\n" ] -------------------------------------------------------------------------- dresp = emptyResponse { rspHttpVersion = rqVersion req } -------------------------------------------------------------------------- ss = SnapState req dresp logerr timeoutAction {-# INLINE runSnap #-} -------------------------------------------------------------------------- -- | Post-process a finalized HTTP response: -- -- * fixup content-length header -- * properly handle 204/304 responses -- * if request was HEAD, remove response body -- -- Note that we do NOT deal with transfer-encoding: chunked or "connection: -- close" here. fixupResponse :: Request -> Response -> IO Response fixupResponse req rsp = {-# SCC "fixupResponse" #-} do let code = rspStatus rsp let rsp' = if code == 204 || code == 304 then handle304 rsp else rsp rsp'' <- do z <- case rspBody rsp' of (Enum _) -> return rsp' (SendFile f Nothing) -> setFileSize f rsp' (SendFile _ (Just (s,e))) -> return $! setContentLength (e-s) rsp' return $! case rspContentLength z of Nothing -> deleteHeader "Content-Length" z (Just sz) -> setHeader "Content-Length" (toByteString $ fromShow sz) z -- HEAD requests cannot have bodies per RFC 2616 sec. 9.4 if rqMethod req == HEAD then return $! deleteHeader "Transfer-Encoding" $ rsp'' { rspBody = Enum $ enumBuilder mempty } else return $! rsp'' where -------------------------------------------------------------------------- setFileSize :: FilePath -> Response -> IO Response setFileSize fp r = {-# SCC "setFileSize" #-} do fs <- liftM fromIntegral $ getFileSize fp return $! r { rspContentLength = Just fs } ------------------------------------------------------------------------------ getFileSize :: FilePath -> IO FileOffset getFileSize fp = liftM fileSize $ getFileStatus fp -------------------------------------------------------------------------- handle304 :: Response -> Response handle304 r = setResponseBody (enumBuilder mempty) $ updateHeaders (H.delete "Transfer-Encoding") $ clearContentLength r {-# INLINE fixupResponse #-} ------------------------------------------------------------------------------ evalSnap :: Snap a -> (ByteString -> IO ()) -> ((Int -> Int) -> IO ()) -> Request -> Iteratee ByteString IO a evalSnap (Snap m) logerr timeoutAction req = do (r, _) <- runStateT m ss case r of SnapValue x -> return x PassOnProcessing e -> liftIO $ throwIO $ NoHandlerException e EarlyTermination _ -> liftIO $ throwIO $ ErrorCall "no value" where dresp = emptyResponse { rspHttpVersion = rqVersion req } ss = SnapState req dresp logerr timeoutAction {-# INLINE evalSnap #-} ------------------------------------------------------------------------------ getParamFrom :: MonadSnap m => (ByteString -> Request -> Maybe [ByteString]) -> ByteString -> m (Maybe ByteString) getParamFrom f k = do rq <- getRequest return $! liftM (S.intercalate " ") $ f k rq {-# INLINE getParamFrom #-} ------------------------------------------------------------------------------ -- | See 'rqParam'. Looks up a value for the given named parameter in the -- 'Request'. If more than one value was entered for the given parameter name, -- 'getParam' gloms the values together with: -- -- @ 'S.intercalate' \" \"@ -- getParam :: MonadSnap m => ByteString -- ^ parameter name to look up -> m (Maybe ByteString) getParam = getParamFrom rqParam {-# INLINE getParam #-} ------------------------------------------------------------------------------ -- | See 'rqPostParam'. Looks up a value for the given named parameter in the -- POST form parameters mapping in 'Request'. If more than one value was -- entered for the given parameter name, 'getPostParam' gloms the values -- together with: -- -- @ 'S.intercalate' \" \"@ -- getPostParam :: MonadSnap m => ByteString -- ^ parameter name to look up -> m (Maybe ByteString) getPostParam = getParamFrom rqPostParam {-# INLINE getPostParam #-} ------------------------------------------------------------------------------ -- | See 'rqQueryParam'. Looks up a value for the given named parameter in the -- query string parameters mapping in 'Request'. If more than one value was -- entered for the given parameter name, 'getQueryParam' gloms the values -- together with: -- -- @ 'S.intercalate' \" \"@ -- getQueryParam :: MonadSnap m => ByteString -- ^ parameter name to look up -> m (Maybe ByteString) getQueryParam = getParamFrom rqQueryParam {-# INLINE getQueryParam #-} ------------------------------------------------------------------------------ -- | See 'rqParams'. Convenience function to return 'Params' from the -- 'Request' inside of a 'MonadSnap' instance. getParams :: MonadSnap m => m Params getParams = getRequest >>= return . rqParams ------------------------------------------------------------------------------ -- | See 'rqParams'. Convenience function to return 'Params' from the -- 'Request' inside of a 'MonadSnap' instance. getPostParams :: MonadSnap m => m Params getPostParams = getRequest >>= return . rqPostParams ------------------------------------------------------------------------------ -- | See 'rqParams'. Convenience function to return 'Params' from the -- 'Request' inside of a 'MonadSnap' instance. getQueryParams :: MonadSnap m => m Params getQueryParams = getRequest >>= return . rqQueryParams ------------------------------------------------------------------------------ -- | Gets the HTTP 'Cookie' with the specified name. getCookie :: MonadSnap m => ByteString -> m (Maybe Cookie) getCookie name = withRequest $ return . listToMaybe . filter (\c -> cookieName c == name) . rqCookies ------------------------------------------------------------------------------ -- | Gets the HTTP 'Cookie' with the specified name and decodes it. If the -- decoding fails, the handler calls pass. readCookie :: (MonadSnap m, Readable a) => ByteString -> m a readCookie name = maybe pass (fromBS . cookieValue) =<< getCookie name ------------------------------------------------------------------------------ -- | Expire the given 'Cookie' in client's browser. expireCookie :: (MonadSnap m) => ByteString -- ^ Cookie name -> Maybe ByteString -- ^ Cookie domain -> m () expireCookie nm dm = do let old = UTCTime (ModifiedJulianDay 0) 0 modifyResponse $ addResponseCookie $ Cookie nm "" (Just old) Nothing dm False False ------------------------------------------------------------------------------ -- | Causes the handler thread to be killed @n@ seconds from now. setTimeout :: MonadSnap m => Int -> m () setTimeout = modifyTimeout . const ------------------------------------------------------------------------------ -- | Causes the handler thread to be killed at least @n@ seconds from now. extendTimeout :: MonadSnap m => Int -> m () extendTimeout = modifyTimeout . max ------------------------------------------------------------------------------ -- | Modifies the amount of time remaining before the request times out. modifyTimeout :: MonadSnap m => (Int -> Int) -> m () modifyTimeout f = do m <- getTimeoutModifier liftIO $ m f ------------------------------------------------------------------------------ -- | Returns an 'IO' action which you can use to set the handling thread's -- timeout value. getTimeoutAction :: MonadSnap m => m (Int -> IO ()) getTimeoutAction = do modifier <- liftSnap $ liftM _snapModifyTimeout sget return $! modifier . const {-# DEPRECATED getTimeoutAction "use getTimeoutModifier instead. Since 0.8." #-} ------------------------------------------------------------------------------ -- | Returns an 'IO' action which you can use to modify the timeout value. getTimeoutModifier :: MonadSnap m => m ((Int -> Int) -> IO ()) getTimeoutModifier = liftSnap $ liftM _snapModifyTimeout sget snap-core-0.9.8.0/src/Snap/Internal/Http/0000755000000000000000000000000012565252520016132 5ustar0000000000000000snap-core-0.9.8.0/src/Snap/Internal/Http/Types.hs0000644000000000000000000007473512565252520017612 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE EmptyDataDecls #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE ForeignFunctionInterface #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE Rank2Types #-} {-# LANGUAGE TypeSynonymInstances #-} ------------------------------------------------------------------------------ -- | An internal Snap module containing HTTP types. -- -- /N.B./ this is an internal interface, please don't write user code that -- depends on it. Most of these declarations (except for the -- unsafe/encapsulation-breaking ones) are re-exported from "Snap.Core". -- module Snap.Internal.Http.Types where ------------------------------------------------------------------------------ import Blaze.ByteString.Builder import Control.Monad (liftM) import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as B import Data.ByteString.Internal (c2w,w2c) import qualified Data.ByteString as S import Data.CaseInsensitive (CI) import qualified Data.CaseInsensitive as CI import Data.Int import qualified Data.IntMap as IM import Data.IORef import Data.List hiding (take) import Data.Map (Map) import qualified Data.Map as Map import Data.Maybe import Data.Monoid import Data.Time.Clock import Foreign.C.Types import Prelude hiding (take) ------------------------------------------------------------------------------ #ifdef PORTABLE import Data.Time.Format import Data.Time.LocalTime import Data.Time.Clock.POSIX import Data.Time.Locale.Compat (defaultTimeLocale) #else import Data.Time.Format () import Foreign import qualified Data.ByteString.Unsafe as S import Foreign.C.String #endif ------------------------------------------------------------------------------ import Snap.Iteratee (Enumerator) import qualified Snap.Iteratee as I import Snap.Types.Headers (Headers) import qualified Snap.Types.Headers as H #ifndef PORTABLE ------------------------------------------------------------------------------ -- foreign imports from cbits foreign import ccall unsafe "set_c_locale" set_c_locale :: IO () foreign import ccall unsafe "c_parse_http_time" c_parse_http_time :: CString -> IO CTime foreign import ccall unsafe "c_format_http_time" c_format_http_time :: CTime -> CString -> IO () foreign import ccall unsafe "c_format_log_time" c_format_log_time :: CTime -> CString -> IO () #endif ------------------------------------------------------------------------------ -- | A typeclass for datatypes which contain HTTP headers. class HasHeaders a where -- | Modify the datatype's headers. updateHeaders :: (Headers -> Headers) -> a -> a -- | Retrieve the headers from a datatype that has headers. headers :: a -> Headers ------------------------------------------------------------------------------ -- | Adds a header key-value-pair to the 'HasHeaders' datatype. If a header -- with the same name already exists, the new value is appended to the headers -- list. addHeader :: (HasHeaders a) => CI ByteString -> ByteString -> a -> a addHeader k v = updateHeaders $ H.insert k v ------------------------------------------------------------------------------ -- | Sets a header key-value-pair in a 'HasHeaders' datatype. If a header with -- the same name already exists, it is overwritten with the new value. setHeader :: (HasHeaders a) => CI ByteString -> ByteString -> a -> a setHeader k v = updateHeaders $ H.set k v ------------------------------------------------------------------------------ -- | Gets all of the values for a given header. getHeaders :: (HasHeaders a) => CI ByteString -> a -> Maybe [ByteString] getHeaders k a = H.lookup k $ headers a ------------------------------------------------------------------------------ -- | Gets a header value out of a 'HasHeaders' datatype. If many headers came -- in with the same name, they will be catenated together. getHeader :: (HasHeaders a) => CI ByteString -> a -> Maybe ByteString getHeader k a = liftM (S.intercalate ",") (H.lookup k $ headers a) ------------------------------------------------------------------------------ -- | Lists all the headers out of a 'HasHeaders' datatype. If many -- headers came in with the same name, they will be catenated together. listHeaders :: (HasHeaders a) => a -> [(CI ByteString, ByteString)] listHeaders = H.toList . headers ------------------------------------------------------------------------------ -- | Clears a header value from a 'HasHeaders' datatype. deleteHeader :: (HasHeaders a) => CI ByteString -> a -> a deleteHeader k = updateHeaders $ H.delete k ------------------------------------------------------------------------------ -- | Enumerates the HTTP method values (see -- ). data Method = GET | HEAD | POST | PUT | DELETE | TRACE | OPTIONS | CONNECT | PATCH | Method ByteString deriving(Show,Read,Ord) instance Eq Method where GET == GET = True GET == Method "GET" = True HEAD == HEAD = True HEAD == Method "HEAD" = True POST == POST = True POST == Method "POST" = True PUT == PUT = True PUT == Method "PUT" = True DELETE == DELETE = True DELETE == Method "DELETE" = True TRACE == TRACE = True TRACE == Method "TRACE" = True OPTIONS == OPTIONS = True OPTIONS == Method "OPTIONS" = True CONNECT == CONNECT = True CONNECT == Method "CONNECT" = True PATCH == PATCH = True PATCH == Method "PATCH" = True Method a == Method b = a == b m@(Method _) == other = other == m _ == _ = False ------------------------------------------------------------------------------ type HttpVersion = (Int,Int) ------------------------------------------------------------------------------ -- | A datatype representing an HTTP cookie. data Cookie = Cookie { -- | The name of the cookie. cookieName :: !ByteString -- | The cookie's string value. , cookieValue :: !ByteString -- | The cookie's expiration value, if it has one. , cookieExpires :: !(Maybe UTCTime) -- | The cookie's \"domain\" value, if it has one. , cookieDomain :: !(Maybe ByteString) -- | The cookie path. , cookiePath :: !(Maybe ByteString) -- | Tag as secure cookie? , cookieSecure :: !Bool -- | HttpOnly? , cookieHttpOnly :: !Bool } deriving (Eq, Show) ------------------------------------------------------------------------------ -- | A type alias for the HTTP parameters mapping. Each parameter -- key maps to a list of ByteString values; if a parameter is specified -- multiple times (e.g.: \"@GET /foo?param=bar1¶m=bar2@\"), looking up -- \"@param@\" in the mapping will give you @[\"bar1\", \"bar2\"]@. type Params = Map ByteString [ByteString] ------------------------------------------------------------------------------ -- request type ------------------------------------------------------------------------------ -- | An existential wrapper for the 'Enumerator ByteString IO a' type newtype SomeEnumerator = SomeEnumerator (forall a . Enumerator ByteString IO a) ------------------------------------------------------------------------------ -- | Contains all of the information about an incoming HTTP request. data Request = Request { -- | The server name of the request, as it came in from the request's -- @Host:@ header. rqServerName :: ByteString -- | Returns the port number the HTTP server is listening on. , rqServerPort :: !Int -- | The remote IP address. , rqRemoteAddr :: ByteString -- | The remote TCP port number. , rqRemotePort :: Int -- | The local IP address for this request. , rqLocalAddr :: ByteString -- | Returns the port number the HTTP server is listening on. , rqLocalPort :: Int -- | Returns the HTTP server's idea of its local hostname. , rqLocalHostname :: ByteString -- | Returns @True@ if this is an @HTTPS@ session. , rqIsSecure :: Bool , rqHeaders :: Headers , rqBody :: !(IORef SomeEnumerator) -- | Returns the @Content-Length@ of the HTTP request body. , rqContentLength :: !(Maybe Int) -- | Returns the HTTP request method. , rqMethod :: !Method -- | Returns the HTTP version used by the client. , rqVersion :: HttpVersion -- | Returns a list of the cookies that came in from the HTTP request -- headers. , rqCookies :: [Cookie] -- | Handlers can be hung on a @URI@ \"entry point\"; this is called the -- \"context path\". If a handler is hung on the context path -- @\"\/foo\/\"@, and you request @\"\/foo\/bar\"@, the value of -- 'rqPathInfo' will be @\"bar\"@. -- -- The following identity holds: -- -- > rqURI r == S.concat [ rqContextPath r -- > , rqPathInfo r -- > , let q = rqQueryString r -- > in if S.null q -- > then "" -- > else S.append "?" q -- > ] , rqPathInfo :: !ByteString -- | The \"context path\" of the request; catenating 'rqContextPath', -- and 'rqPathInfo' should get you back to the original 'rqURI' -- (ignoring query strings). The 'rqContextPath' always begins and ends -- with a slash (@\"\/\"@) character, and represents the path (relative -- to your component\/snaplet) you took to get to your handler. , rqContextPath :: !ByteString -- | Returns the @URI@ requested by the client. , rqURI :: !ByteString -- | Returns the HTTP query string for this 'Request'. , rqQueryString :: !ByteString -- | Returns the parameters mapping for this 'Request'. \"Parameters\" -- are automatically decoded from the URI's query string and @POST@ body -- and entered into this mapping. The 'rqParams' value is thus a union of -- 'rqQueryParams' and 'rqPostParams'. , rqParams :: Params -- | The parameter mapping decoded from the URI's query string. , rqQueryParams :: Params -- | The parameter mapping decoded from the POST body. Note that Snap -- only auto-decodes POST request bodies when the request's -- @Content-Type@ is @application/x-www-form-urlencoded@. -- For @multipart/form-data@ use 'Snap.Util.FileUploads.handleFileUploads' -- to decode the POST request and fill this mapping. , rqPostParams :: Params } ------------------------------------------------------------------------------ instance Show Request where show r = concat [ "Request <\n" , body , ">" ] where body = concat $ map ((" "++) . (++ "\n")) [ sname , remote , local , beginheaders , hdrs , endheaders , contentlength , method , version , cookies , pathinfo , contextpath , uri , params ] sname = concat [ "server-name: ", toStr $ rqServerName r ] remote = concat [ "remote: " , toStr $ rqRemoteAddr r , ":" , show (rqRemotePort r) ] local = concat [ "local: " , toStr $ rqLocalAddr r , ":" , show $ rqServerPort r ] beginheaders = "Headers:\n ========================================" endheaders = " ========================================" hdrs' (a,b) = (B.unpack $ CI.original a) ++ ": " ++ B.unpack b hdrs = " " ++ (concat $ intersperse "\n " $ map hdrs' (H.toList $ rqHeaders r)) contentlength = concat [ "content-length: " , show $ rqContentLength r ] method = concat [ "method: " , show $ rqMethod r ] version = concat [ "version: " , show $ rqVersion r ] cookies' = " " ++ (concat $ intersperse "\n " $ map show $ rqCookies r) cookies = concat [ "cookies:\n" , " ========================================\n" , cookies' , "\n ========================================" ] pathinfo = concat [ "pathinfo: ", toStr $ rqPathInfo r ] contextpath = concat [ "contextpath: ", toStr $ rqContextPath r ] uri = concat [ "URI: ", toStr $ rqURI r ] params' = " " ++ (concat $ intersperse "\n " $ map (\ (a,b) -> B.unpack a ++ ": " ++ show b) $ Map.toAscList $ rqParams r) params = concat [ "params:\n" , " ========================================\n" , params' , "\n ========================================" ] ------------------------------------------------------------------------------ instance HasHeaders Request where headers = rqHeaders updateHeaders f r = r { rqHeaders = f (rqHeaders r) } ------------------------------------------------------------------------------ instance HasHeaders Headers where headers = id updateHeaders = id ------------------------------------------------------------------------------ -- response type ------------------------------------------------------------------------------ data ResponseBody = Enum (forall a . Enumerator Builder IO a) -- ^ output body is a 'Builder' enumerator | SendFile FilePath (Maybe (Int64,Int64)) -- ^ output body is sendfile(), optional second argument -- is a byte range to send ------------------------------------------------------------------------------ rspBodyMap :: (forall a . Enumerator Builder IO a -> Enumerator Builder IO a) -> ResponseBody -> ResponseBody rspBodyMap f b = Enum $ f $ rspBodyToEnum b ------------------------------------------------------------------------------ rspBodyToEnum :: ResponseBody -> Enumerator Builder IO a rspBodyToEnum (Enum e) = e rspBodyToEnum (SendFile fp Nothing) = I.mapEnum toByteString fromByteString $ I.enumFile fp rspBodyToEnum (SendFile fp (Just s)) = I.mapEnum toByteString fromByteString $ I.enumFilePartial fp s ------------------------------------------------------------------------------ -- | Represents an HTTP response. data Response = Response { rspHeaders :: Headers , rspCookies :: Map ByteString Cookie , rspHttpVersion :: !HttpVersion -- | We will need to inspect the content length no matter what, and -- looking up \"content-length\" in the headers and parsing the number -- out of the text will be too expensive. , rspContentLength :: !(Maybe Int64) , rspBody :: ResponseBody -- | Returns the HTTP status code. , rspStatus :: !Int -- | Returns the HTTP status explanation string. , rspStatusReason :: !ByteString -- | If true, we are transforming the request body with -- 'transformRequestBody' , rspTransformingRqBody :: !Bool -- | Controls whether Snap will buffer the output or not. You may wish to -- disable buffering when using Comet-like techniques which rely on the -- immediate sending of output data in order to maintain interactive -- semantics. , rspOutputBuffering :: !Bool } ------------------------------------------------------------------------------ instance Show Response where show r = concat [ statusline , hdrs , "\r\n" ] where (v1,v2) = rspHttpVersion r statusline = concat [ "HTTP/" , show v1 , "." , show v2 , " " , show $ rspStatus r , " " , toStr $ rspStatusReason r , "\r\n" ] hdrs = concatMap showHdr $ H.toList $ rspHeaders r showHdr (k,v) = concat [ toStr (CI.original k), ": ", toStr v, "\r\n" ] ------------------------------------------------------------------------------ instance HasHeaders Response where headers = rspHeaders updateHeaders f r = r { rspHeaders = f (rspHeaders r) } ------------------------------------------------------------------------------ -- | Looks up the value(s) for the given named parameter. Parameters initially -- come from the request's query string and any decoded POST body (if the -- request's @Content-Type@ is @application\/x-www-form-urlencoded@). -- Parameter values can be modified within handlers using "rqModifyParams". rqParam :: ByteString -- ^ parameter name to look up -> Request -- ^ HTTP request -> Maybe [ByteString] rqParam k rq = Map.lookup k $ rqParams rq {-# INLINE rqParam #-} ------------------------------------------------------------------------------ -- | Looks up the value(s) for the given named parameter in the POST parameters -- mapping. rqPostParam :: ByteString -- ^ parameter name to look up -> Request -- ^ HTTP request -> Maybe [ByteString] rqPostParam k rq = Map.lookup k $ rqPostParams rq {-# INLINE rqPostParam #-} ------------------------------------------------------------------------------ -- | Looks up the value(s) for the given named parameter in the query -- parameters mapping. rqQueryParam :: ByteString -- ^ parameter name to look up -> Request -- ^ HTTP request -> Maybe [ByteString] rqQueryParam k rq = Map.lookup k $ rqQueryParams rq {-# INLINE rqQueryParam #-} ------------------------------------------------------------------------------ -- | Modifies the parameters mapping (which is a @Map ByteString ByteString@) -- in a 'Request' using the given function. rqModifyParams :: (Params -> Params) -> Request -> Request rqModifyParams f r = r { rqParams = p } where p = f $ rqParams r {-# INLINE rqModifyParams #-} ------------------------------------------------------------------------------ -- | Writes a key-value pair to the parameters mapping within the given -- request. rqSetParam :: ByteString -- ^ parameter name -> [ByteString] -- ^ parameter values -> Request -- ^ request -> Request rqSetParam k v = rqModifyParams $ Map.insert k v {-# INLINE rqSetParam #-} --------------- -- responses -- --------------- ------------------------------------------------------------------------------ -- | An empty 'Response'. emptyResponse :: Response emptyResponse = Response H.empty Map.empty (1,1) Nothing (Enum (I.enumBuilder mempty)) 200 "OK" False True ------------------------------------------------------------------------------ -- | Sets an HTTP response body to the given 'Enumerator' value. setResponseBody :: (forall a . Enumerator Builder IO a) -- ^ new response body enumerator -> Response -- ^ response to modify -> Response setResponseBody e r = r { rspBody = Enum e } {-# INLINE setResponseBody #-} ------------------------------------------------------------------------------ -- | Sets the HTTP response status. Note: normally you would use -- 'setResponseCode' unless you needed a custom response explanation. -- setResponseStatus :: Int -- ^ HTTP response integer code -> ByteString -- ^ HTTP response explanation -> Response -- ^ Response to be modified -> Response setResponseStatus s reason r = r { rspStatus=s, rspStatusReason=reason } {-# INLINE setResponseStatus #-} ------------------------------------------------------------------------------ -- | Sets the HTTP response code. setResponseCode :: Int -- ^ HTTP response integer code -> Response -- ^ Response to be modified -> Response setResponseCode s r = setResponseStatus s reason r where reason = fromMaybe "Unknown" (IM.lookup s statusReasonMap) {-# INLINE setResponseCode #-} ------------------------------------------------------------------------------ -- | Modifies a response body. modifyResponseBody :: (forall a . Enumerator Builder IO a -> Enumerator Builder IO a) -> Response -> Response modifyResponseBody f r = r { rspBody = rspBodyMap f (rspBody r) } {-# INLINE modifyResponseBody #-} ------------------------------------------------------------------------------ -- | Sets the @Content-Type@ in the 'Response' headers. setContentType :: ByteString -> Response -> Response setContentType = setHeader "Content-Type" {-# INLINE setContentType #-} ------------------------------------------------------------------------------ -- | Adds an HTTP 'Cookie' to 'Response' headers. addResponseCookie :: Cookie -- ^ cookie value -> Response -- ^ response to modify -> Response addResponseCookie ck@(Cookie k _ _ _ _ _ _) r = r { rspCookies = cks' } where cks'= Map.insert k ck $ rspCookies r {-# INLINE addResponseCookie #-} ------------------------------------------------------------------------------ -- | Gets an HTTP 'Cookie' with the given name from 'Response' headers. getResponseCookie :: ByteString -- ^ cookie name -> Response -- ^ response to query -> Maybe Cookie getResponseCookie cn r = Map.lookup cn $ rspCookies r {-# INLINE getResponseCookie #-} -- | Returns a list of 'Cookie's present in 'Response' getResponseCookies :: Response -- ^ response to query -> [Cookie] getResponseCookies = Map.elems . rspCookies {-# INLINE getResponseCookies #-} ------------------------------------------------------------------------------ -- | Deletes an HTTP 'Cookie' from the 'Response' headers. Please note -- this does not necessarily erase the cookie from the client browser. deleteResponseCookie :: ByteString -- ^ cookie name -> Response -- ^ response to modify -> Response deleteResponseCookie cn r = r { rspCookies = cks' } where cks'= Map.delete cn $ rspCookies r {-# INLINE deleteResponseCookie #-} ------------------------------------------------------------------------------ -- | Modifies an HTTP 'Cookie' with given name in 'Response' headers. -- Nothing will happen if a matching 'Cookie' can not be found in 'Response'. modifyResponseCookie :: ByteString -- ^ cookie name -> (Cookie -> Cookie) -- ^ modifier function -> Response -- ^ response to modify -> Response modifyResponseCookie cn f r = maybe r modify $ getResponseCookie cn r where modify ck = addResponseCookie (f ck) r {-# INLINE modifyResponseCookie #-} ------------------------------------------------------------------------------ -- | A note here: if you want to set the @Content-Length@ for the response, -- Snap forces you to do it with this function rather than by setting it in -- the headers; the @Content-Length@ in the headers will be ignored. -- -- The reason for this is that Snap needs to look up the value of -- @Content-Length@ for each request, and looking the string value up in the -- headers and parsing the number out of the text will be too expensive. -- -- If you don't set a content length in your response, HTTP keep-alive will be -- disabled for HTTP\/1.0 clients, forcing a @Connection: close@. For -- HTTP\/1.1 clients, Snap will switch to the chunked transfer encoding if -- @Content-Length@ is not specified. setContentLength :: Int64 -> Response -> Response setContentLength l r = r { rspContentLength = Just l } {-# INLINE setContentLength #-} ------------------------------------------------------------------------------ -- | Removes any @Content-Length@ set in the 'Response'. clearContentLength :: Response -> Response clearContentLength r = r { rspContentLength = Nothing } {-# INLINE clearContentLength #-} ------------------------------------------------------------------------------ -- | The buffering mode controls whether Snap will buffer the output or not. -- You may wish to disable buffering when using Comet-like techniques which -- rely on the immediate sending of output data in order to maintain -- interactive semantics. getBufferingMode :: Response -> Bool getBufferingMode = rspOutputBuffering {-# INLINE getBufferingMode #-} ------------------------------------------------------------------------------ -- | The buffering mode controls whether Snap will buffer the output or not. -- You may wish to disable buffering when using Comet-like techniques which -- rely on the immediate sending of output data in order to maintain -- interactive semantics. setBufferingMode :: Bool -- ^ if True, buffer the output, if False, send -- output immediately -> Response -> Response setBufferingMode b r = r { rspOutputBuffering = b } {-# INLINE setBufferingMode #-} ---------------- -- HTTP dates -- ---------------- ------------------------------------------------------------------------------ -- | Converts a 'CTime' into an HTTP timestamp. formatHttpTime :: CTime -> IO ByteString ------------------------------------------------------------------------------ -- | Converts a 'CTime' into common log entry format. formatLogTime :: CTime -> IO ByteString ------------------------------------------------------------------------------ -- | Converts an HTTP timestamp into a 'CTime'. parseHttpTime :: ByteString -> IO CTime #ifdef PORTABLE ------------------------------------------------------------------------------ formatHttpTime = return . format . toUTCTime where format :: UTCTime -> ByteString format = fromStr . formatTime defaultTimeLocale "%a, %d %b %Y %X GMT" toUTCTime :: CTime -> UTCTime toUTCTime = posixSecondsToUTCTime . realToFrac ------------------------------------------------------------------------------ formatLogTime ctime = do t <- utcToLocalZonedTime $ toUTCTime ctime return $! format t where format :: ZonedTime -> ByteString format = fromStr . formatTime defaultTimeLocale "%d/%b/%Y:%H:%M:%S %z" toUTCTime :: CTime -> UTCTime toUTCTime = posixSecondsToUTCTime . realToFrac ------------------------------------------------------------------------------ parseHttpTime = return . toCTime . prs . toStr where prs :: String -> Maybe UTCTime prs = parseTime defaultTimeLocale "%a, %d %b %Y %H:%M:%S GMT" toCTime :: Maybe UTCTime -> CTime toCTime (Just t) = fromInteger $ truncate $ utcTimeToPOSIXSeconds t toCTime Nothing = fromInteger 0 #else ------------------------------------------------------------------------------ formatLogTime t = do ptr <- mallocBytes 40 c_format_log_time t ptr S.unsafePackMallocCString ptr ------------------------------------------------------------------------------ formatHttpTime t = do ptr <- mallocBytes 40 c_format_http_time t ptr S.unsafePackMallocCString ptr ------------------------------------------------------------------------------ parseHttpTime s = S.unsafeUseAsCString s $ \ptr -> c_parse_http_time ptr #endif ------------------------------------------------------------------------------ -- local definitions fromStr :: String -> ByteString fromStr = S.pack . map c2w {-# INLINE fromStr #-} ------------------------------------------------------------------------------ -- private helper functions toStr :: ByteString -> String toStr = map w2c . S.unpack ------------------------------------------------------------------------------ statusReasonMap :: IM.IntMap ByteString statusReasonMap = IM.fromList [ (100, "Continue"), (101, "Switching Protocols"), (200, "OK"), (201, "Created"), (202, "Accepted"), (203, "Non-Authoritative Information"), (204, "No Content"), (205, "Reset Content"), (206, "Partial Content"), (300, "Multiple Choices"), (301, "Moved Permanently"), (302, "Found"), (303, "See Other"), (304, "Not Modified"), (305, "Use Proxy"), (307, "Temporary Redirect"), (400, "Bad Request"), (401, "Unauthorized"), (402, "Payment Required"), (403, "Forbidden"), (404, "Not Found"), (405, "Method Not Allowed"), (406, "Not Acceptable"), (407, "Proxy Authentication Required"), (408, "Request Time-out"), (409, "Conflict"), (410, "Gone"), (411, "Length Required"), (412, "Precondition Failed"), (413, "Request Entity Too Large"), (414, "Request-URI Too Large"), (415, "Unsupported Media Type"), (416, "Requested range not satisfiable"), (417, "Expectation Failed"), (500, "Internal Server Error"), (501, "Not Implemented"), (502, "Bad Gateway"), (503, "Service Unavailable"), (504, "Gateway Time-out"), (505, "HTTP Version not supported") ] snap-core-0.9.8.0/src/Snap/Internal/Iteratee/0000755000000000000000000000000012565252520016755 5ustar0000000000000000snap-core-0.9.8.0/src/Snap/Internal/Iteratee/Debug.hs0000644000000000000000000000620612565252520020343 0ustar0000000000000000-- | An internal Snap module for debugging iteratees. -- -- /N.B./ this is an internal interface, please don't write user code that -- depends on it. {-# LANGUAGE CPP #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE PackageImports #-} module Snap.Internal.Iteratee.Debug ( debugIteratee , iterateeDebugWrapper , iterateeDebugWrapperWith , showBuilder ) where ------------------------------------------------------------------------------ import Blaze.ByteString.Builder import Control.Monad.Trans import Data.ByteString (ByteString) import System.IO ------------------------------------------------------------------------------ #ifndef NODEBUG import Snap.Internal.Debug #endif import Snap.Iteratee hiding (map) ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ showBuilder :: Builder -> String showBuilder = show . toByteString ------------------------------------------------------------------------------ debugIteratee :: Iteratee ByteString IO () debugIteratee = continue f where f EOF = do liftIO $ putStrLn $ "got EOF" liftIO $ hFlush stdout yield () EOF f (Chunks xs) = do liftIO $ putStrLn $ "got chunk: " ++ show (xs) liftIO $ hFlush stdout continue f #ifndef NODEBUG iterateeDebugWrapperWith :: (MonadIO m) => (a -> String) -> String -> Iteratee a m b -> Iteratee a m b iterateeDebugWrapperWith showFunc name iter = do debug $ name ++ ": BEGIN" step <- lift $ runIteratee iter whatWasReturn step check step where whatWasReturn (Continue _) = debug $ name ++ ": continue" whatWasReturn (Yield _ z) = debug $ name ++ ": yield, with remainder " ++ showStream z whatWasReturn (Error e) = debug $ name ++ ": error, with " ++ show e check (Continue k) = continue $ f k check st = returnI st f k EOF = do debug $ name ++ ": got EOF" k EOF f k ch@(Chunks xs) = do debug $ name ++ ": got chunk: " ++ showL xs step <- lift $ runIteratee $ k ch whatWasReturn step check step showStream = show . fmap showFunc showL = show . map showFunc iterateeDebugWrapper :: (Show a, MonadIO m) => String -> Iteratee a m b -> Iteratee a m b iterateeDebugWrapper = iterateeDebugWrapperWith show #else iterateeDebugWrapperWith :: (MonadIO m) => (s -> String) -> String -> Iteratee s m a -> Iteratee s m a iterateeDebugWrapperWith _ _ = id {-# INLINE iterateeDebugWrapperWith #-} iterateeDebugWrapper :: (Show a, MonadIO m) => String -> Iteratee a m b -> Iteratee a m b iterateeDebugWrapper _ = id {-# INLINE iterateeDebugWrapper #-} #endif snap-core-0.9.8.0/src/Snap/Internal/Iteratee/BoyerMooreHorspool.hs0000644000000000000000000002154712565252520023132 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE Rank2Types #-} module Snap.Internal.Iteratee.BoyerMooreHorspool ( bmhEnumeratee , MatchInfo(..) ) where import Control.Monad.State import qualified Data.ByteString as S import Data.ByteString (ByteString) import Data.ByteString.Unsafe as S import Data.Enumerator hiding (head, filter, last, map) import qualified Data.Enumerator.List as EL import Data.Int import qualified Data.Vector.Unboxed as V import qualified Data.Vector.Unboxed.Mutable as MV import Prelude hiding (head, last) --{-# INLINE debug #-} --debug :: MonadIO m => String -> m () --debug s = liftIO $ putStrLn s --debug _ = return () ------------------------------------------------------------------------------ data MatchInfo = Match !ByteString | NoMatch !ByteString deriving (Show) -- We return strict bytestring because we always expect a chunk to be bigger -- than the needle lookahead :: (MonadIO m) => Int -> Iteratee ByteString m (Either ByteString ByteString) lookahead n = go id n where go !dlist !k = do EL.head >>= maybe (do let !ls = S.concat $ dlist [] -- debug $ "lookahead " ++ show n -- ++ " failing, returning " ++ show ls return $! Left ls) (\x -> do let !l = S.length x let !r = k - l let !d' = dlist . (x:) if r <= 0 then do let !ls = S.concat $ d' [] -- debug $ "lookahead " ++ show n -- ++ " successfully returning " -- ++ show ls return $! Right ls else go d' r) {-# INLINE lookahead #-} matches :: ByteString -- ^ needle -> Int -- ^ needle start -> Int -- ^ needle end (inclusive) -> ByteString -- ^ haystack -> Int -- ^ haystack start -> Int -- ^ haystack end (inclusive) -> Bool matches !needle !nstart !nend' !haystack !hstart !hend' = go nend' hend' where go !nend !hend = if nend < nstart || hend < hstart then True else let !nc = S.unsafeIndex needle nend !hc = S.unsafeIndex haystack hend in if nc /= hc then False else go (nend-1) (hend-1) {-# INLINE matches #-} bmhEnumeratee :: (MonadIO m) => ByteString -> Step MatchInfo m a -> Iteratee ByteString m (Step MatchInfo m a) bmhEnumeratee needle _step = do -- debug $ "boyermoore: needle=" ++ show needle cDone _step iter where {-# INLINE cDone #-} cDone (Continue k) f = f k cDone step _ = yield step (Chunks []) iter !k = {-# SCC "bmh/iter" #-} do lookahead nlen >>= either (finishAndEOF k . (:[])) (startSearch k) finishAndEOF k xs = {-# SCC "finishAndEOF" #-} do -- debug $ "finishAndEOF, returning NoMatch for " ++ show xs step <- lift $ runIteratee $ k $ Chunks (map NoMatch $ filter (not . S.null) xs) cDone step (\k' -> lift $ runIteratee $ k' EOF) startSearch !k !haystack = {-# SCC "startSearch" #-} do -- debug $ "startsearch: " ++ show haystack if S.null haystack then lookahead nlen >>= either (\s -> finishAndEOF k [s]) (startSearch k) else go 0 where !hlen = S.length haystack go !hidx | hend >= hlen = crossBound hidx | otherwise = {-# SCC "go" #-} do let match = matches needle 0 last haystack hidx hend -- debug $ "go " ++ show hidx ++ ", hend=" ++ show hend -- ++ ", match was " ++ show match if match then {-# SCC "go/match" #-} do let !nomatch = S.take hidx haystack let !aftermatch = S.drop (hend+1) haystack step <- if not $ S.null nomatch then lift $ runIteratee $ k $ Chunks [NoMatch nomatch] else return $! Continue k cDone step $ \k' -> do step' <- lift $ runIteratee $ k' $ Chunks [Match needle] cDone step' $ \k'' -> startSearch k'' aftermatch else {-# SCC "go/nomatch" #-} do -- skip ahead let c = S.unsafeIndex haystack hend let !skip = V.unsafeIndex table $ fromEnum c go (hidx + skip) where !hend = hidx + nlen - 1 mkCoeff hidx = let !ll = hlen - hidx !nm = nlen - ll in (ll,nm) crossBound !hidx0 = {-# SCC "crossBound" #-} do let (!leftLen, needMore) = mkCoeff hidx0 lookahead needMore >>= either (\s -> finishAndEOF k [haystack, s]) (runNext hidx0 leftLen needMore) where runNext !hidx !leftLen !needMore !nextHaystack = do let match1 = matches needle leftLen last nextHaystack 0 (needMore-1) let match2 = matches needle 0 (leftLen-1) haystack hidx (hlen-1) -- debug $ "crossbound match1=" ++ show match1 -- ++ " match2=" ++ show match2 if match1 && match2 then {-# SCC "crossBound/match" #-} do let !nomatch = S.take hidx haystack let !aftermatch = S.drop needMore nextHaystack -- FIXME: merge this code w/ above step <- if not $ S.null nomatch then lift $ runIteratee $ k $ Chunks [NoMatch nomatch] else return $! Continue k -- debug $ "matching" cDone step $ \k' -> do step' <- lift $ runIteratee $ k' $ Chunks [Match needle] cDone step' $ \k'' -> startSearch k'' aftermatch else {-# SCC "crossBound/nomatch" #-} do let c = S.unsafeIndex nextHaystack $ needMore-1 let p = V.unsafeIndex table (fromEnum c) -- debug $ "p was " ++ show p ++ ", ll=" ++ show leftLen if p < leftLen then do let !hidx' = hidx+p let (!leftLen', needMore') = mkCoeff hidx' let !nextlen = S.length nextHaystack if (nextlen < needMore') then do -- this should be impossibly rare lookahead (needMore' - nextlen) >>= either (\s -> finishAndEOF k [ haystack , nextHaystack , s ]) (\s -> runNext hidx' leftLen' needMore' $ S.append nextHaystack s) else runNext hidx' leftLen' needMore' nextHaystack else do let sidx = p - leftLen let (!crumb, !rest) = S.splitAt sidx nextHaystack step <- lift $ runIteratee $ k $ Chunks $ map NoMatch $ filter (not . S.null) [haystack, crumb] cDone step $ flip startSearch rest !nlen = S.length needle !last = nlen - 1 !table = V.create $ do t <- MV.replicate 256 nlen go t where go !t = go' 0 where go' !i | i >= last = return t | otherwise = do let c = fromEnum $ S.unsafeIndex needle i MV.unsafeWrite t c (last - i) go' $! i+1 {- testIt :: ByteString -> [ByteString] -> IO [MatchInfo] testIt needle haystack = do consumeStep <- runIteratee EL.consume eteeStep <- runIteratee $ etee consumeStep -- iter :: Iteratee ByteString m (Step MatchInfo m [MatchInfo]) let iter = enumList 1 haystack eteeStep finalInnerStep <- run_ iter run_ $ returnI finalInnerStep where etee = bmhEnumeratee needle -} snap-core-0.9.8.0/src/Snap/Internal/Parsing/0000755000000000000000000000000012565252520016616 5ustar0000000000000000snap-core-0.9.8.0/src/Snap/Internal/Parsing/FastSet.hs0000644000000000000000000000732612565252520020533 0ustar0000000000000000{-# LANGUAGE BangPatterns, MagicHash #-} ----------------------------------------------------------------------------- -- | -- Module : Snap.Internal.Parsing.FastSet -- Copyright : Bryan O'Sullivan 2008 -- License : BSD3 -- -- Maintainer : bos@serpentine.com -- Stability : experimental -- Portability : unknown -- -- Fast set membership tests for 'Word8' and 8-bit 'Char' values. The -- set representation is unboxed for efficiency. For small sets, we -- test for membership using a binary search. For larger sets, we use -- a lookup table. -- -- Note: this module copied here from the attoparsec source because it was made -- private in version 0.10. -- ----------------------------------------------------------------------------- module Snap.Internal.Parsing.FastSet ( -- * Data type FastSet -- * Construction , fromList , set -- * Lookup , memberChar , memberWord8 -- * Debugging , fromSet -- * Handy interface , charClass ) where import Data.Bits ((.&.), (.|.)) import Foreign.Storable (peekByteOff, pokeByteOff) import GHC.Base (Int(I#), iShiftRA#, narrow8Word#, shiftL#) import GHC.Word (Word8(W8#)) import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Internal as I import qualified Data.ByteString.Unsafe as U data FastSet = Sorted { fromSet :: !B.ByteString } | Table { fromSet :: !B.ByteString } deriving (Eq, Ord) instance Show FastSet where show (Sorted s) = "FastSet Sorted " ++ show (B8.unpack s) show (Table _) = "FastSet Table" -- | The lower bound on the size of a lookup table. We choose this to -- balance table density against performance. tableCutoff :: Int tableCutoff = 8 -- | Create a set. set :: B.ByteString -> FastSet set s | B.length s < tableCutoff = Sorted . B.sort $ s | otherwise = Table . mkTable $ s fromList :: [Word8] -> FastSet fromList = set . B.pack data I = I {-# UNPACK #-} !Int {-# UNPACK #-} !Word8 shiftR :: Int -> Int -> Int shiftR (I# x#) (I# i#) = I# (x# `iShiftRA#` i#) shiftL :: Word8 -> Int -> Word8 shiftL (W8# x#) (I# i#) = W8# (narrow8Word# (x# `shiftL#` i#)) index :: Int -> I index i = I (i `shiftR` 3) (1 `shiftL` (i .&. 7)) {-# INLINE index #-} -- | Check the set for membership. memberWord8 :: Word8 -> FastSet -> Bool memberWord8 w (Table t) = let I byte bit = index (fromIntegral w) in U.unsafeIndex t byte .&. bit /= 0 memberWord8 w (Sorted s) = search 0 (B.length s - 1) where search lo hi | hi < lo = False | otherwise = let mid = (lo + hi) `div` 2 in case compare w (U.unsafeIndex s mid) of GT -> search (mid + 1) hi LT -> search lo (mid - 1) _ -> True -- | Check the set for membership. Only works with 8-bit characters: -- characters above code point 255 will give wrong answers. memberChar :: Char -> FastSet -> Bool memberChar c = memberWord8 (I.c2w c) {-# INLINE memberChar #-} mkTable :: B.ByteString -> B.ByteString mkTable s = I.unsafeCreate 32 $ \t -> do _ <- I.memset t 0 32 U.unsafeUseAsCStringLen s $ \(p, l) -> let loop n | n == l = return () | otherwise = do c <- peekByteOff p n :: IO Word8 let I byte bit = index (fromIntegral c) prev <- peekByteOff t byte :: IO Word8 pokeByteOff t byte (prev .|. bit) loop (n + 1) in loop 0 charClass :: String -> FastSet charClass = set . B8.pack . go where go (a:'-':b:xs) = [a..b] ++ go xs go (x:xs) = x : go xs go _ = "" snap-core-0.9.8.0/src/Snap/Internal/Test/0000755000000000000000000000000012565252520016132 5ustar0000000000000000snap-core-0.9.8.0/src/Snap/Internal/Test/RequestBuilder.hs0000644000000000000000000005734412565252520021442 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE Rank2Types #-} module Snap.Internal.Test.RequestBuilder ( RequestBuilder , MultipartParams , MultipartParam(..) , FileData (..) , RequestType (..) , addHeader , buildRequest , delete , dumpResponse , evalHandler , evalHandlerM , get , postMultipart , postRaw , postUrlEncoded , put , responseToString , runHandler , runHandlerM , setContentType , setHeader , setHttpVersion , setQueryString , setQueryStringRaw , setRequestPath , setRequestType , setSecure ) where ------------------------------------------------------------------------------ import Blaze.ByteString.Builder import Blaze.ByteString.Builder.Char8 import Control.Applicative import Control.Monad.State hiding (get, put) import qualified Control.Monad.State as State import Data.Bits import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as S import qualified Data.ByteString as S8 import Data.CaseInsensitive (CI) import Data.IORef import qualified Data.Map as Map import Data.Monoid import Data.Word import qualified Data.Vector as V import System.PosixCompat.Time import System.Random ------------------------------------------------------------------------------ import Snap.Internal.Http.Types hiding (addHeader, setContentType, setHeader) import qualified Snap.Internal.Http.Types as H import Snap.Internal.Parsing import Snap.Internal.Types (evalSnap) import Snap.Iteratee hiding (map) import Snap.Core hiding ( addHeader , setContentType , setHeader ) import qualified Snap.Types.Headers as H ------------------------------------------------------------------------------ -- | RequestBuilder is a monad transformer that allows you to conveniently -- build a snap 'Request' for testing. newtype RequestBuilder m a = RequestBuilder (StateT Request m a) deriving (Functor, Applicative, Monad, MonadIO, MonadState Request, MonadTrans) ------------------------------------------------------------------------------ mkDefaultRequest :: IO Request mkDefaultRequest = do bodyRef <- newIORef $ SomeEnumerator enumEOF return $ Request "localhost" 8080 "127.0.0.1" 60000 "127.0.0.1" 8080 "localhost" False H.empty bodyRef Nothing GET (1,1) [] "" "/" "/" "" Map.empty Map.empty Map.empty ------------------------------------------------------------------------------ -- | Runs a 'RequestBuilder', producing the desired 'Request'. -- -- N.B. /please/ don't use the request you get here in a real Snap application; -- things will probably break. Don't say you weren't warned :-) -- buildRequest :: MonadIO m => RequestBuilder m () -> m Request buildRequest mm = do let (RequestBuilder m) = (mm >> fixup) rq0 <- liftIO mkDefaultRequest execStateT m rq0 where fixup = do fixupURI fixupMethod fixupCL fixupParams fixupMethod = do rq <- rGet if (rqMethod rq == GET || rqMethod rq == DELETE || rqMethod rq == HEAD) then do -- These requests are not permitted to have bodies let rq' = deleteHeader "Content-Type" rq liftIO $ writeIORef (rqBody rq') (SomeEnumerator enumEOF) rPut $ rq' { rqContentLength = Nothing } else return $! () fixupCL = do rq <- rGet maybe (rPut $ deleteHeader "Content-Length" rq) (\cl -> rPut $ H.setHeader "Content-Length" (S.pack (show cl)) rq) (rqContentLength rq) fixupParams = do rq <- rGet let query = rqQueryString rq let queryParams = parseUrlEncoded query let mbCT = getHeader "Content-Type" rq postParams <- if mbCT == Just "application/x-www-form-urlencoded" then do (SomeEnumerator e) <- liftIO $ readIORef $ rqBody rq s <- liftM S.concat (liftIO $ run_ $ e $$ consume) return $ parseUrlEncoded s else return Map.empty rPut $ rq { rqParams = Map.unionWith (++) queryParams postParams , rqQueryParams = queryParams } ------------------------------------------------------------------------------ -- | A request body of type \"@multipart/form-data@\" consists of a set of -- named form parameters, each of which can by either a list of regular form -- values or a set of file uploads. type MultipartParams = [(ByteString, MultipartParam)] ------------------------------------------------------------------------------ data MultipartParam = FormData [ByteString] -- ^ a form variable consisting of the given 'ByteString' values. | Files [FileData] -- ^ a file upload consisting of the given 'FileData' values. deriving (Show) ------------------------------------------------------------------------------ data FileData = FileData { fdFileName :: ByteString -- ^ the file's name , fdContentType :: ByteString -- ^ the file's content-type , fdContents :: ByteString -- ^ the file contents } deriving (Show) ------------------------------------------------------------------------------ -- | The 'RequestType' datatype enumerates the different kinds of HTTP -- requests you can generate using the testing interface. Most users will -- prefer to use the 'get', 'postUrlEncoded', 'postMultipart', 'put', and -- 'delete' convenience functions. data RequestType = GetRequest | RequestWithRawBody Method ByteString | MultipartPostRequest MultipartParams | UrlEncodedPostRequest Params | DeleteRequest deriving (Show) ------------------------------------------------------------------------------ -- | Sets the type of the 'Request' being built. setRequestType :: MonadIO m => RequestType -> RequestBuilder m () setRequestType GetRequest = do rq <- rGet liftIO $ writeIORef (rqBody rq) $ SomeEnumerator enumEOF rPut $ rq { rqMethod = GET , rqContentLength = Nothing } setRequestType DeleteRequest = do rq <- rGet liftIO $ writeIORef (rqBody rq) $ SomeEnumerator enumEOF rPut $ rq { rqMethod = DELETE , rqContentLength = Nothing } setRequestType (RequestWithRawBody m b) = do rq <- rGet liftIO $ writeIORef (rqBody rq) $ SomeEnumerator $ enumBS b rPut $ rq { rqMethod = m , rqContentLength = Just $ S.length b } setRequestType (MultipartPostRequest fp) = encodeMultipart fp setRequestType (UrlEncodedPostRequest fp) = do rq <- liftM (H.setHeader "Content-Type" "application/x-www-form-urlencoded") rGet let b = printUrlEncoded fp liftIO $ writeIORef (rqBody rq) $ SomeEnumerator $ enumBS b rPut $ rq { rqMethod = POST , rqContentLength = Just $ S.length b } ------------------------------------------------------------------------------ makeBoundary :: MonadIO m => m ByteString makeBoundary = do xs <- liftIO $ replicateM 16 randomWord8 let x = S.pack $ map (toEnum . fromEnum) xs return $ S.concat [ "snap-boundary-", encode x ] where randomWord8 :: IO Word8 randomWord8 = liftM (\c -> toEnum $ c .&. 0xff) randomIO table = V.fromList [ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' , 'a', 'b', 'c', 'd', 'e', 'f' ] encode = toByteString . S8.foldl' f mempty #if MIN_VERSION_base(4,5,0) shR = unsafeShiftR #else shR = shiftR #endif f m c = let low = c .&. 0xf hi = (c .&. 0xf0) `shR` 4 k = \i -> fromWord8 $! toEnum $! fromEnum $! V.unsafeIndex table (fromEnum i) in m `mappend` k hi `mappend` k low ------------------------------------------------------------------------------ multipartHeader :: ByteString -> ByteString -> Builder multipartHeader boundary name = mconcat [ fromByteString boundary , fromByteString "\r\ncontent-disposition: form-data" , fromByteString "; name=\"" , fromByteString name , fromByteString "\"\r\n" ] ------------------------------------------------------------------------------ -- Assume initial or preceding "--" just before this encodeFormData :: ByteString -> ByteString -> [ByteString] -> IO Builder encodeFormData boundary name vals = case vals of [] -> return mempty [v] -> return $ mconcat [ hdr , cr , fromByteString v , fromByteString "\r\n--" ] _ -> multi where hdr = multipartHeader boundary name cr = fromByteString "\r\n" oneVal b v = mconcat [ fromByteString b , cr , cr , fromByteString v , fromByteString "\r\n--" ] multi = do b <- makeBoundary return $ mconcat [ hdr , multipartMixed b , cr , fromByteString "--" , mconcat (map (oneVal b) vals) , fromByteString b , fromByteString "--\r\n--" ] ------------------------------------------------------------------------------ multipartMixed :: ByteString -> Builder multipartMixed b = mconcat [ fromByteString "Content-Type: multipart/mixed" , fromByteString "; boundary=" , fromByteString b , fromByteString "\r\n" ] ------------------------------------------------------------------------------ encodeFiles :: ByteString -> ByteString -> [FileData] -> IO Builder encodeFiles boundary name files = case files of [] -> return mempty _ -> do b <- makeBoundary return $ mconcat [ hdr , multipartMixed b , cr , fromByteString "--" , mconcat (map (oneVal b) files) , fromByteString b , fromByteString "--\r\n--" ] where -------------------------------------------------------------------------- contentDisposition fn = mconcat [ fromByteString "Content-Disposition: attachment" , fromByteString "; filename=\"" , fromByteString fn , fromByteString "\"\r\n" ] -------------------------------------------------------------------------- contentType ct = mconcat [ fromByteString "Content-Type: " , fromByteString ct , cr ] -------------------------------------------------------------------------- oneVal b (FileData fileName ct contents) = mconcat [ fromByteString b , cr , contentType ct , contentDisposition fileName , fromByteString "Content-Transfer-Encoding: binary\r\n" , cr , fromByteString contents , fromByteString "\r\n--" ] -------------------------------------------------------------------------- hdr = multipartHeader boundary name cr = fromByteString "\r\n" ------------------------------------------------------------------------------ encodeMultipart :: MonadIO m => MultipartParams -> RequestBuilder m () encodeMultipart kvps = do boundary <- liftIO $ makeBoundary builders <- liftIO $ mapM (handleOne boundary) kvps let b = toByteString $ mconcat (fromByteString "--" : builders) `mappend` finalBoundary boundary rq0 <- rGet liftIO $ writeIORef (rqBody rq0) $ SomeEnumerator $ enumBS b let rq = H.setHeader "Content-Type" (S.append "multipart/form-data; boundary=" boundary) rq0 rPut $ rq { rqMethod = POST , rqContentLength = Just $ S.length b } where finalBoundary b = mconcat [fromByteString b, fromByteString "--\r\n"] handleOne boundary (name, mp) = case mp of (FormData vals) -> encodeFormData boundary name vals (Files fs) -> encodeFiles boundary name fs ------------------------------------------------------------------------------ fixupURI :: Monad m => RequestBuilder m () fixupURI = do rq <- rGet let u = S.concat [ rqContextPath rq , rqPathInfo rq , let q = rqQueryString rq in if S.null q then "" else S.append "?" q ] rPut $ rq { rqURI = u } ------------------------------------------------------------------------------ -- | Sets the request's query string to be the raw bytestring provided, -- without any escaping or other interpretation. Most users should instead -- choose the 'setQueryString' function, which takes a parameter mapping. setQueryStringRaw :: Monad m => ByteString -> RequestBuilder m () setQueryStringRaw r = do rq <- rGet rPut $ rq { rqQueryString = r } fixupURI ------------------------------------------------------------------------------ -- | Escapes the given parameter mapping and sets it as the request's query -- string. setQueryString :: Monad m => Params -> RequestBuilder m () setQueryString p = setQueryStringRaw $ printUrlEncoded p ------------------------------------------------------------------------------ -- | Sets the given header in the request being built, overwriting any header -- with the same name already present. setHeader :: (Monad m) => CI ByteString -> ByteString -> RequestBuilder m () setHeader k v = rModify (H.setHeader k v) ------------------------------------------------------------------------------ -- | Adds the given header to the request being built. addHeader :: (Monad m) => CI ByteString -> ByteString -> RequestBuilder m () addHeader k v = rModify (H.addHeader k v) ------------------------------------------------------------------------------ -- | Sets the request's @content-type@ to the given MIME type. setContentType :: Monad m => ByteString -> RequestBuilder m () setContentType c = rModify (H.setHeader "Content-Type" c) ------------------------------------------------------------------------------ -- | Controls whether the test request being generated appears to be an https -- request or not. setSecure :: Monad m => Bool -> RequestBuilder m () setSecure b = rModify $ \rq -> rq { rqIsSecure = b } ------------------------------------------------------------------------------ -- | Sets the test request's http version setHttpVersion :: Monad m => (Int,Int) -> RequestBuilder m () setHttpVersion v = rModify $ \rq -> rq { rqVersion = v } ------------------------------------------------------------------------------ -- | Sets the request's path. The path provided must begin with a \"@/@\" and -- must /not/ contain a query string; if you want to provide a query string -- in your test request, you must use 'setQueryString' or 'setQueryStringRaw'. -- Note that 'rqContextPath' is never set by any 'RequestBuilder' function. setRequestPath :: Monad m => ByteString -> RequestBuilder m () setRequestPath p0 = do rModify $ \rq -> rq { rqContextPath = "/" , rqPathInfo = p } fixupURI where p = if S.isPrefixOf "/" p0 then S.drop 1 p0 else p0 ------------------------------------------------------------------------------ -- | Builds an HTTP \"GET\" request with the given query parameters. get :: MonadIO m => ByteString -- ^ request path -> Params -- ^ request's form parameters -> RequestBuilder m () get uri params = do setRequestType GetRequest setQueryString params setRequestPath uri ------------------------------------------------------------------------------ -- | Builds an HTTP \"DELETE\" request with the given query parameters. delete :: MonadIO m => ByteString -- ^ request path -> Params -- ^ request's form parameters -> RequestBuilder m () delete uri params = do setRequestType DeleteRequest setQueryString params setRequestPath uri ------------------------------------------------------------------------------ -- | Builds an HTTP \"POST\" request with the given form parameters, using the -- \"application/x-www-form-urlencoded\" MIME type. postUrlEncoded :: MonadIO m => ByteString -- ^ request path -> Params -- ^ request's form parameters -> RequestBuilder m () postUrlEncoded uri params = do setRequestType $ UrlEncodedPostRequest params setRequestPath uri ------------------------------------------------------------------------------ -- | Builds an HTTP \"POST\" request with the given form parameters, using the -- \"form-data/multipart\" MIME type. postMultipart :: MonadIO m => ByteString -- ^ request path -> MultipartParams -- ^ multipart form parameters -> RequestBuilder m () postMultipart uri params = do setRequestType $ MultipartPostRequest params setRequestPath uri ------------------------------------------------------------------------------ -- | Builds an HTTP \"PUT\" request. put :: MonadIO m => ByteString -- ^ request path -> ByteString -- ^ request body MIME content-type -> ByteString -- ^ request body contents -> RequestBuilder m () put uri contentType putData = do setRequestType $ RequestWithRawBody PUT putData setHeader "Content-Type" contentType setRequestPath uri ------------------------------------------------------------------------------ -- | Builds a \"raw\" HTTP \"POST\" request, with the given MIME type and body -- contents. postRaw :: MonadIO m => ByteString -- ^ request path -> ByteString -- ^ request body MIME content-type -> ByteString -- ^ request body contents -> RequestBuilder m () postRaw uri contentType postData = do setRequestType $ RequestWithRawBody POST postData setHeader "Content-Type" contentType setRequestPath uri ------------------------------------------------------------------------------ -- | Given a web handler in the 'Snap' monad, and a 'RequestBuilder' defining -- a test request, runs the handler, producing an HTTP 'Response'. -- -- This function will produce almost exactly the same output as running the -- handler in a real server, except that chunked transfer encoding is not -- applied, and the \"Transfer-Encoding\" header is not set (this makes it -- easier to test response output). runHandler :: MonadIO m => RequestBuilder m () -- ^ a request builder -> Snap a -- ^ a web handler -> m Response runHandler = runHandlerM rs where rs rq s = do (_,rsp) <- liftIO $ run_ $ runSnap s (const $ return $! ()) (const $ return $! ()) rq return rsp ------------------------------------------------------------------------------ -- | Given a web handler in some arbitrary 'MonadSnap' monad, a function -- specifying how to evaluate it within the context of the test monad, and a -- 'RequestBuilder' defining a test request, runs the handler, producing an -- HTTP 'Response'. runHandlerM :: (MonadIO m, MonadSnap n) => (forall a . Request -> n a -> m Response) -- ^ a function defining how the 'MonadSnap' monad should be run -> RequestBuilder m () -- ^ a request builder -> n b -- ^ a web handler -> m Response runHandlerM rSnap rBuilder snap = do rq <- buildRequest rBuilder rsp <- rSnap rq snap -- simulate server logic t1 <- liftIO (epochTime >>= formatHttpTime) return $ H.setHeader "Date" t1 $ H.setHeader "Server" "Snap/test" $ if rspContentLength rsp == Nothing && rspHttpVersion rsp < (1,1) then H.setHeader "Connection" "close" rsp else rsp ------------------------------------------------------------------------------ -- | Given a web handler in the 'Snap' monad, and a 'RequestBuilder' defining a -- test request, runs the handler and returns the monadic value it produces. -- -- Throws an exception if the 'Snap' handler early-terminates with 'finishWith' -- or 'mzero'. -- evalHandler :: MonadIO m => RequestBuilder m () -> Snap a -> m a evalHandler = evalHandlerM rs where rs rq s = liftIO $ run_ $ evalSnap s (const $ return $! ()) (const $ return $! ()) rq ------------------------------------------------------------------------------ -- | Given a web handler in some arbitrary 'MonadSnap' monad, a function -- specifying how to evaluate it within the context of the test monad, and a -- 'RequestBuilder' defining a test request, runs the handler, returning the -- monadic value it produces. -- -- Throws an exception if the 'Snap' handler early-terminates with 'finishWith' -- or 'mzero'. -- evalHandlerM :: (MonadIO m, MonadSnap n) => (forall a . Request -> n a -> m a) -- ^ a function defining -- how the 'MonadSnap' -- monad should be run -> RequestBuilder m () -- ^ a request builder -> n b -- ^ a web handler -> m b evalHandlerM rSnap rBuilder snap = do rq <- buildRequest rBuilder rSnap rq snap ------------------------------------------------------------------------------ -- | Dumps the given response to stdout. dumpResponse :: Response -> IO () dumpResponse resp = responseToString resp >>= S.putStrLn ------------------------------------------------------------------------------ -- | Converts the given response to a bytestring. responseToString :: Response -> IO ByteString responseToString resp = do b <- run_ (rspBodyToEnum (rspBody resp) $$ liftM mconcat consume) return $ toByteString $ fromShow resp `mappend` b ------------------------------------------------------------------------------ rGet :: Monad m => RequestBuilder m Request rGet = RequestBuilder State.get rPut :: Monad m => Request -> RequestBuilder m () rPut s = RequestBuilder $ State.put s rModify :: Monad m => (Request -> Request) -> RequestBuilder m () rModify f = RequestBuilder $ modify f snap-core-0.9.8.0/src/Snap/Internal/Test/Assertions.hs0000644000000000000000000000630412565252520020623 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} module Snap.Internal.Test.Assertions where ------------------------------------------------------------------------------ import Blaze.ByteString.Builder import Control.Monad (liftM) import Data.ByteString.Char8 (ByteString) import Data.Maybe (fromJust) import Data.Monoid (mconcat) import Test.HUnit (Assertion, assertBool, assertEqual) import Text.Regex.Posix ((=~)) ------------------------------------------------------------------------------ import Snap.Internal.Http.Types import Snap.Iteratee (run_, consume, ($$)) ------------------------------------------------------------------------------ getResponseBody :: Response -> IO ByteString getResponseBody rsp = run_ $ enum $$ liftM toBS consume where enum = rspBodyToEnum $ rspBody rsp toBS = toByteString . mconcat ------------------------------------------------------------------------------ -- | Given a Response, asserts that its HTTP status code is 200 (success). assertSuccess :: Response -> Assertion assertSuccess rsp = assertEqual message 200 status where message = "Expected success (200) but got (" ++ (show status) ++ ")" status = rspStatus rsp ------------------------------------------------------------------------------ -- | Given a Response, asserts that its HTTP status code is 404 (Not Found). assert404 :: Response -> Assertion assert404 rsp = assertEqual message 404 status where message = "Expected Not Found (404) but got (" ++ (show status) ++ ")" status = rspStatus rsp ------------------------------------------------------------------------------ -- | Given a Response, asserts that its HTTP status code is between 300 and -- 399 (a redirect), and that the Location header of the Response points to -- the specified URI. assertRedirectTo :: ByteString -- ^ The Response should redirect to this -- URI -> Response -> Assertion assertRedirectTo uri rsp = do assertRedirect rsp assertEqual message uri rspUri where rspUri = fromJust $ getHeader "Location" rsp message = "Expected redirect to " ++ show uri ++ " but got redirected to " ++ show rspUri ++ " instead" ------------------------------------------------------------------------------ -- | Given a Response, asserts that its HTTP status code is between 300 and -- 399 (a redirect). assertRedirect :: Response -> Assertion assertRedirect rsp = assertBool message (300 <= status && status <= 399) where message = "Expected redirect but got status code (" ++ show status ++ ")" status = rspStatus rsp ------------------------------------------------------------------------------ -- | Given a Response, asserts that its body matches the given regular -- expression. assertBodyContains :: ByteString -- ^ Regexp that will match the body content -> Response -> Assertion assertBodyContains match rsp = do body <- getResponseBody rsp assertBool message (body =~ match) where message = "Expected body to match regexp \"" ++ show match ++ "\", but didn't" snap-core-0.9.8.0/src/Snap/Types/0000755000000000000000000000000012565252520014543 5ustar0000000000000000snap-core-0.9.8.0/src/Snap/Types/Headers.hs0000644000000000000000000000611112565252520016451 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} -- | An opaque data type for HTTP headers. Intended to be imported qualified, -- i.e: -- -- > import Snap.Types.Headers (Headers) -- > import qualified Snap.Types.Headers as H -- > -- > foo :: Headers -- > foo = H.empty module Snap.Types.Headers ( -- * Headers type Headers -- * Headers creation , empty -- * Predicates , null , member -- * Lookup , lookup , lookupWithDefault -- * Adding/setting headers , insert , set -- * Deleting , delete -- * Traversal , fold -- * Lists , toList , fromList ) where import Data.ByteString.Char8 (ByteString) import Data.CaseInsensitive (CI) import Data.List (foldl') import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as Map import Data.Maybe (isJust) import Prelude hiding (null, lookup) ------------------------------------------------------------------------------ newtype Headers = H { unH :: HashMap (CI ByteString) [ByteString] } deriving (Show) ------------------------------------------------------------------------------ empty :: Headers empty = H (Map.empty) ------------------------------------------------------------------------------ null :: Headers -> Bool null = Map.null . unH {-# INLINE null #-} ------------------------------------------------------------------------------ member :: CI ByteString -> Headers -> Bool member k = f . unH where f m = isJust $ Map.lookup k m {-# INLINE member #-} ------------------------------------------------------------------------------ lookup :: CI ByteString -> Headers -> Maybe [ByteString] lookup k (H m) = Map.lookup k m {-# INLINE lookup #-} ------------------------------------------------------------------------------ lookupWithDefault :: ByteString -> CI ByteString -> Headers -> [ByteString] lookupWithDefault d k (H m) = Map.lookupDefault [d] k m ------------------------------------------------------------------------------ insert :: CI ByteString -> ByteString -> Headers -> Headers insert k v (H m) = H $ Map.insertWith (flip (++)) k [v] m ------------------------------------------------------------------------------ set :: CI ByteString -> ByteString -> Headers -> Headers set k v (H m) = H $ Map.insert k [v] m ------------------------------------------------------------------------------ delete :: CI ByteString -> Headers -> Headers delete k (H m) = H $ Map.delete k m ------------------------------------------------------------------------------ fold :: (a -> CI ByteString -> [ByteString] -> a) -> a -> Headers -> a fold f a (H m) = Map.foldlWithKey' f a m ------------------------------------------------------------------------------ toList :: Headers -> [(CI ByteString, ByteString)] toList (H m) = (Map.foldlWithKey' f id m) [] where f !dl k vs = dl . ((map (\v -> (k,v)) vs) ++) ------------------------------------------------------------------------------ fromList :: [(CI ByteString, ByteString)] -> Headers fromList = foldl' f empty where f m (k,v) = insert k v m snap-core-0.9.8.0/src/Snap/Util/0000755000000000000000000000000012565252520014354 5ustar0000000000000000snap-core-0.9.8.0/src/Snap/Util/FileServe.hs0000644000000000000000000007232012565252520016600 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -- | Contains web handlers to serve files from a directory. module Snap.Util.FileServe ( getSafePath -- * Configuration for directory serving , MimeMap , HandlerMap , DirectoryConfig(..) , simpleDirectoryConfig , defaultDirectoryConfig , fancyDirectoryConfig , defaultIndexGenerator , defaultMimeTypes , fileType -- * File servers , serveDirectory , serveDirectoryWith , serveFile , serveFileAs ) where ------------------------------------------------------------------------------ import Blaze.ByteString.Builder import Blaze.ByteString.Builder.Char8 import Control.Applicative import Control.Exception (SomeException, evaluate) import Control.Monad import Control.Monad.CatchIO import Control.Monad.Trans import Data.Attoparsec.Char8 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as S import Data.ByteString.Internal (c2w) import Data.HashMap.Strict (HashMap) import qualified Data.HashMap.Strict as Map import Data.Int import Data.List import Data.Maybe (fromMaybe, isNothing) import Data.Monoid import qualified Data.Text as T import qualified Data.Text.Encoding as T #if MIN_VERSION_base(4,6,0) import Prelude hiding (Show, show) #else import Prelude hiding (Show, catch, show) #endif import qualified Prelude import System.Directory import System.FilePath import System.PosixCompat.Files ------------------------------------------------------------------------------ import Snap.Core import Snap.Internal.Debug import Snap.Internal.Parsing import Snap.Iteratee hiding (drop) ------------------------------------------------------------------------------ -- | Gets a path from the 'Request' using 'rqPathInfo' and makes sure it is -- safe to use for opening files. A path is safe if it is a relative path -- and has no ".." elements to escape the intended directory structure. getSafePath :: MonadSnap m => m FilePath getSafePath = do req <- getRequest let mp = urlDecode $ rqPathInfo req p <- maybe pass (return . T.unpack . T.decodeUtf8) mp -- relative paths only! when (not $ isRelative p) pass -- check that we don't have any sneaky .. paths let dirs = splitDirectories p when (elem ".." dirs) pass return $! joinPath dirs ------------------------------------------------------------------------------ -- | A type alias for dynamic handlers type HandlerMap m = HashMap FilePath (FilePath -> m ()) ------------------------------------------------------------------------------ -- | A type alias for MIME type type MimeMap = HashMap FilePath ByteString ------------------------------------------------------------------------------ -- | The default set of mime type mappings we use when serving files. Its -- value: -- -- > Map.fromList [ -- > ( ".asc" , "text/plain" ), -- > ( ".asf" , "video/x-ms-asf" ), -- > ( ".asx" , "video/x-ms-asf" ), -- > ( ".avi" , "video/x-msvideo" ), -- > ( ".bz2" , "application/x-bzip" ), -- > ( ".c" , "text/plain" ), -- > ( ".class" , "application/octet-stream" ), -- > ( ".conf" , "text/plain" ), -- > ( ".cpp" , "text/plain" ), -- > ( ".css" , "text/css" ), -- > ( ".cxx" , "text/plain" ), -- > ( ".dtd" , "text/xml" ), -- > ( ".dvi" , "application/x-dvi" ), -- > ( ".gif" , "image/gif" ), -- > ( ".gz" , "application/x-gzip" ), -- > ( ".hs" , "text/plain" ), -- > ( ".htm" , "text/html" ), -- > ( ".html" , "text/html" ), -- > ( ".ico" , "image/x-icon" ), -- > ( ".jar" , "application/x-java-archive" ), -- > ( ".jpeg" , "image/jpeg" ), -- > ( ".jpg" , "image/jpeg" ), -- > ( ".js" , "text/javascript" ), -- > ( ".json" , "application/json" ), -- > ( ".log" , "text/plain" ), -- > ( ".m3u" , "audio/x-mpegurl" ), -- > ( ".mov" , "video/quicktime" ), -- > ( ".mp3" , "audio/mpeg" ), -- > ( ".mpeg" , "video/mpeg" ), -- > ( ".mpg" , "video/mpeg" ), -- > ( ".ogg" , "application/ogg" ), -- > ( ".pac" , "application/x-ns-proxy-autoconfig" ), -- > ( ".pdf" , "application/pdf" ), -- > ( ".png" , "image/png" ), -- > ( ".ps" , "application/postscript" ), -- > ( ".qt" , "video/quicktime" ), -- > ( ".sig" , "application/pgp-signature" ), -- > ( ".spl" , "application/futuresplash" ), -- > ( ".svg" , "image/svg+xml" ), -- > ( ".swf" , "application/x-shockwave-flash" ), -- > ( ".tar" , "application/x-tar" ), -- > ( ".tar.bz2" , "application/x-bzip-compressed-tar" ), -- > ( ".tar.gz" , "application/x-tgz" ), -- > ( ".tbz" , "application/x-bzip-compressed-tar" ), -- > ( ".text" , "text/plain" ), -- > ( ".tgz" , "application/x-tgz" ), -- > ( ".torrent" , "application/x-bittorrent" ), -- > ( ".txt" , "text/plain" ), -- > ( ".wav" , "audio/x-wav" ), -- > ( ".wax" , "audio/x-ms-wax" ), -- > ( ".wma" , "audio/x-ms-wma" ), -- > ( ".wmv" , "video/x-ms-wmv" ), -- > ( ".xbm" , "image/x-xbitmap" ), -- > ( ".xml" , "text/xml" ), -- > ( ".xpm" , "image/x-xpixmap" ), -- > ( ".xwd" , "image/x-xwindowdump" ), -- > ( ".zip" , "application/zip" ) ] -- defaultMimeTypes :: MimeMap defaultMimeTypes = Map.fromList [ ( ".asc" , "text/plain" ), ( ".asf" , "video/x-ms-asf" ), ( ".asx" , "video/x-ms-asf" ), ( ".avi" , "video/x-msvideo" ), ( ".bz2" , "application/x-bzip" ), ( ".c" , "text/plain" ), ( ".class" , "application/octet-stream" ), ( ".conf" , "text/plain" ), ( ".cpp" , "text/plain" ), ( ".css" , "text/css" ), ( ".cxx" , "text/plain" ), ( ".dtd" , "text/xml" ), ( ".dvi" , "application/x-dvi" ), ( ".gif" , "image/gif" ), ( ".gz" , "application/x-gzip" ), ( ".hs" , "text/plain" ), ( ".htm" , "text/html" ), ( ".html" , "text/html" ), ( ".ico" , "image/x-icon" ), ( ".jar" , "application/x-java-archive" ), ( ".jpeg" , "image/jpeg" ), ( ".jpg" , "image/jpeg" ), ( ".js" , "text/javascript" ), ( ".json" , "application/json" ), ( ".log" , "text/plain" ), ( ".m3u" , "audio/x-mpegurl" ), ( ".mov" , "video/quicktime" ), ( ".mp3" , "audio/mpeg" ), ( ".mpeg" , "video/mpeg" ), ( ".mpg" , "video/mpeg" ), ( ".ogg" , "application/ogg" ), ( ".pac" , "application/x-ns-proxy-autoconfig" ), ( ".pdf" , "application/pdf" ), ( ".png" , "image/png" ), ( ".ps" , "application/postscript" ), ( ".qt" , "video/quicktime" ), ( ".sig" , "application/pgp-signature" ), ( ".spl" , "application/futuresplash" ), ( ".svg" , "image/svg+xml" ), ( ".swf" , "application/x-shockwave-flash" ), ( ".tar" , "application/x-tar" ), ( ".tar.bz2" , "application/x-bzip-compressed-tar" ), ( ".tar.gz" , "application/x-tgz" ), ( ".tbz" , "application/x-bzip-compressed-tar" ), ( ".text" , "text/plain" ), ( ".tgz" , "application/x-tgz" ), ( ".torrent" , "application/x-bittorrent" ), ( ".ttf" , "application/x-font-truetype" ), ( ".txt" , "text/plain" ), ( ".wav" , "audio/x-wav" ), ( ".wax" , "audio/x-ms-wax" ), ( ".wma" , "audio/x-ms-wma" ), ( ".wmv" , "video/x-ms-wmv" ), ( ".xbm" , "image/x-xbitmap" ), ( ".xml" , "text/xml" ), ( ".xpm" , "image/x-xpixmap" ), ( ".xwd" , "image/x-xwindowdump" ), ( ".zip" , "application/zip" ) ] ------------------------------------------------------------------------------ -- | A collection of options for serving static files out of a directory. data DirectoryConfig m = DirectoryConfig { -- | Files to look for when a directory is requested (e.g., index.html) indexFiles :: [FilePath], -- | Handler to generate a directory listing if there is no index. indexGenerator :: FilePath -> m (), -- | Map of extensions to pass to dynamic file handlers. This could be -- used, for example, to implement CGI dispatch, pretty printing of source -- code, etc. dynamicHandlers :: HandlerMap m, -- | MIME type map to look up content types. mimeTypes :: MimeMap, -- | Handler that is called before a file is served. It will only be -- called when a file is actually found, not for generated index pages. preServeHook :: FilePath -> m () } ------------------------------------------------------------------------------ -- | Style information for the default directory index generator. snapIndexStyles :: ByteString snapIndexStyles = S.intercalate "\n" [ "body { margin: 0px 0px 0px 0px; font-family: sans-serif }" , "div.header {" , "padding: 40px 40px 0px 40px; height:35px;" , "background:rgb(25,50,87);" , "background-image:-webkit-gradient(" , "linear,left bottom,left top," , "color-stop(0.00, rgb(31,62,108))," , "color-stop(1.00, rgb(19,38,66)));" , "background-image:-moz-linear-gradient(" , "center bottom,rgb(31,62,108) 0%,rgb(19,38,66) 100%);" , "text-shadow:-1px 3px 1px rgb(16,33,57);" , "font-size:16pt; letter-spacing: 2pt; color:white;" , "border-bottom:10px solid rgb(46,93,156) }" , "div.content {" , "background:rgb(255,255,255);" , "background-image:-webkit-gradient(" , "linear,left bottom, left top," , "color-stop(0.50, rgb(255,255,255))," , "color-stop(1.00, rgb(224,234,247)));" , "background-image:-moz-linear-gradient(" , "center bottom, white 50%, rgb(224,234,247) 100%);" , "padding: 40px 40px 40px 40px }" , "div.footer {" , "padding: 16px 0px 10px 10px; height:31px;" , "border-top: 1px solid rgb(194,209,225);" , "color: rgb(160,172,186); font-size:10pt;" , "background: rgb(245,249,255) }" , "table { max-width:100%; margin: 0 auto;" `S.append` " border-collapse: collapse; }" , "tr:hover { background:rgb(256,256,224) }" , "td { border:0; font-family:monospace; padding: 2px 0; }" , "td.filename, td.type { padding-right: 2em; }" , "th { border:0; background:rgb(28,56,97);" , "text-shadow:-1px 3px 1px rgb(16,33,57); color: white}" ] ------------------------------------------------------------------------------ -- | An automatic index generator, which is fairly small and does not rely on -- any external files (which may not be there depending on external request -- routing). -- -- A 'MimeMap' is passed in to display the types of files in the directory -- listing based on their extension. Preferably, this is the same as the map -- in the 'DirectoryConfig' -- -- The styles parameter allows you to apply styles to the directory listing. -- The listing itself consists of a table, containing a header row using -- th elements, and one row per file using td elements, so styles for those -- pieces may be attached to the appropriate tags. defaultIndexGenerator :: MonadSnap m => MimeMap -- ^ MIME type mapping for reporting types -> ByteString -- ^ Style info to insert in header -> FilePath -- ^ Directory to generate index for -> m () defaultIndexGenerator mm styles d = do modifyResponse $ setContentType "text/html; charset=utf-8" rq <- getRequest let uri = uriWithoutQueryString rq let pInfo = rqPathInfo rq writeBS "\n\n" writeBS "Directory Listing: " writeBS uri writeBS "" writeBS "" writeBS "
Directory Listing: " writeBS uri writeBS "
" writeBS "" when (pInfo /= "") $ writeBS "" entries <- liftIO $ getDirectoryContents d dirs <- liftIO $ filterM (doesDirectoryExist . (d )) entries files <- liftIO $ filterM (doesFileExist . (d )) entries forM_ (sort $ filter (not . (`elem` ["..", "."])) dirs) $ \f0 -> do f <- liftIO $ liftM (\s -> T.encodeUtf8 s `mappend` "/") $ packFn f0 writeBS "" forM_ (sort files) $ \f0 -> do f <- liftIO $ liftM T.encodeUtf8 $ packFn f0 stat <- liftIO $ getFileStatus (d f0) tm <- liftIO $ formatHttpTime (modificationTime stat) writeBS "" writeBS "
File NameTypeLast Modified" writeBS "
..DIR
" writeBS f writeBS "DIR
" writeBS f writeBS "" writeBS (fileType mm f0) writeBS "" writeBS tm writeBS "
Powered by " writeBS "Snap
" writeBS "" where packFn fp = do tryFirst [ T.decodeUtf8 , T.decodeUtf16LE , T.decodeUtf16BE , T.decodeUtf32LE , T.decodeUtf32BE , const (T.pack fp) ] where tryFirst [] = error "No valid decoding" tryFirst (f:fs) = evaluate (f bs) `catch` \(_::SomeException) -> tryFirst fs bs = S.pack fp ------------------------------------------------------------------------------ -- | A very simple configuration for directory serving. This configuration -- uses built-in MIME types from 'defaultMimeTypes', and has no index files, -- index generator, dynamic file handlers, or 'preServeHook'. simpleDirectoryConfig :: MonadSnap m => DirectoryConfig m simpleDirectoryConfig = DirectoryConfig { indexFiles = [], indexGenerator = const pass, dynamicHandlers = Map.empty, mimeTypes = defaultMimeTypes, preServeHook = const $ return () } ------------------------------------------------------------------------------ -- | A reasonable default configuration for directory serving. This -- configuration uses built-in MIME types from 'defaultMimeTypes', serves -- common index files @index.html@ and @index.htm@, but does not autogenerate -- directory indexes, nor have any dynamic file handlers. The 'preServeHook' -- will not do anything. defaultDirectoryConfig :: MonadSnap m => DirectoryConfig m defaultDirectoryConfig = DirectoryConfig { indexFiles = ["index.html", "index.htm"], indexGenerator = const pass, dynamicHandlers = Map.empty, mimeTypes = defaultMimeTypes, preServeHook = const $ return () } ------------------------------------------------------------------------------ -- | A more elaborate configuration for file serving. This configuration -- uses built-in MIME types from 'defaultMimeTypes', serves common index files -- @index.html@ and @index.htm@, and autogenerates directory indexes with a -- Snap-like feel. It still has no dynamic file handlers, nor 'preServeHook', -- which should be added as needed. -- -- Files recognized as indexes include @index.html@, @index.htm@, -- @default.html@, @default.htm@, @home.html@ fancyDirectoryConfig :: MonadSnap m => DirectoryConfig m fancyDirectoryConfig = DirectoryConfig { indexFiles = ["index.html", "index.htm"], indexGenerator = defaultIndexGenerator defaultMimeTypes snapIndexStyles, dynamicHandlers = Map.empty, mimeTypes = defaultMimeTypes, preServeHook = const $ return () } ------------------------------------------------------------------------------ -- | Serves static files from a directory using the default configuration -- as given in 'defaultDirectoryConfig'. serveDirectory :: MonadSnap m => FilePath -- ^ Directory to serve from -> m () serveDirectory = serveDirectoryWith defaultDirectoryConfig {-# INLINE serveDirectory #-} ------------------------------------------------------------------------------ -- | Serves static files from a directory. Configuration options are -- passed in a 'DirectoryConfig' that captures various choices about desired -- behavior. The relative path given in 'rqPathInfo' is searched for a -- requested file, and the file is served with the appropriate mime type if it -- is found. Absolute paths and \"@..@\" are prohibited to prevent files from -- being served from outside the sandbox. serveDirectoryWith :: MonadSnap m => DirectoryConfig m -- ^ Configuration options -> FilePath -- ^ Directory to serve from -> m () serveDirectoryWith cfg base = do b <- directory <|> file <|> redir when (not b) pass where idxs = indexFiles cfg generate = indexGenerator cfg mimes = mimeTypes cfg dyns = dynamicHandlers cfg pshook = preServeHook cfg -- Serves a file if it exists; passes if not serve f = do liftIO (doesFileExist f) >>= flip unless pass let fname = takeFileName f let staticServe f' = pshook f >> serveFileAs (fileType mimes fname) f' lookupExt staticServe dyns fname f >> return True <|> return False -- Serves a directory via indices if available. Returns True on success, -- False on failure to find an index. Passes /only/ if the request was -- not for a directory (no trailing slash). directory = do rq <- getRequest let uri = uriWithoutQueryString rq unless ("/" `S.isSuffixOf` uri) pass rel <- (base ) <$> getSafePath b <- liftIO $ doesDirectoryExist rel if b then do let serveRel f = serve (rel f) foldl' (<|>) pass (Prelude.map serveRel idxs) <|> (generate rel >> return True) <|> return False else return False -- Serves a file requested by name. Passes if the file doesn't exist. file = serve =<< ((base ) <$> getSafePath) -- If the request is for a directory but lacks a trailing slash, redirects -- to the directory name with a trailing slash. redir = do rel <- (base ) <$> getSafePath liftIO (doesDirectoryExist rel) >>= flip unless pass rq <- getRequest let uri = uriWithoutQueryString rq let qss = queryStringSuffix rq let u = S.concat [uri, "/", qss] redirect u ------------------------------------------------------------------------------ -- | Serves a single file specified by a full or relative path. If the file -- does not exist, throws an exception (not that it does /not/ pass to the -- next handler). The path restrictions on 'serveDirectory' don't apply to -- this function since the path is not being supplied by the user. serveFile :: MonadSnap m => FilePath -- ^ path to file -> m () serveFile fp = serveFileAs (fileType defaultMimeTypes (takeFileName fp)) fp {-# INLINE serveFile #-} ------------------------------------------------------------------------------ -- | Same as 'serveFile', with control over the MIME mapping used. serveFileAs :: MonadSnap m => ByteString -- ^ MIME type -> FilePath -- ^ path to file -> m () serveFileAs mime fp = do reqOrig <- getRequest -- If-Range header must be ignored if there is no Range: header in the -- request (RFC 2616 section 14.27) let req = if isNothing $ getHeader "range" reqOrig then deleteHeader "if-range" reqOrig else reqOrig -- check "If-Modified-Since" and "If-Range" headers let mbH = getHeader "if-modified-since" req mbIfModified <- liftIO $ case mbH of Nothing -> return Nothing (Just s) -> liftM Just $ parseHttpTime s -- If-Range header could contain an entity, but then parseHttpTime will -- fail and return 0 which means a 200 response will be generated anyways mbIfRange <- liftIO $ case getHeader "if-range" req of Nothing -> return Nothing (Just s) -> liftM Just $ parseHttpTime s dbg $ "mbIfModified: " ++ Prelude.show mbIfModified dbg $ "mbIfRange: " ++ Prelude.show mbIfRange -- check modification time and bug out early if the file is not modified. -- -- TODO: a stat cache would be nice here, but it'd need the date thread -- stuff from snap-server to be folded into snap-core filestat <- liftIO $ getFileStatus fp let mt = modificationTime filestat maybe (return $! ()) (\lt -> when (mt <= lt) notModified) mbIfModified let sz = fromIntegral $ fileSize filestat lm <- liftIO $ formatHttpTime mt -- ok, at this point we know the last-modified time and the -- content-type. set those. modifyResponse $ setHeader "Last-Modified" lm . setHeader "Accept-Ranges" "bytes" . setContentType mime -- now check: is this a range request? If there is an 'If-Range' header -- with an old modification time we skip this check and send a 200 -- response let skipRangeCheck = maybe (False) (\lt -> mt > lt) mbIfRange -- checkRangeReq checks for a Range: header in the request and sends a -- partial response if it matches. wasRange <- if skipRangeCheck then return False else liftSnap $ checkRangeReq req fp sz dbg $ "was this a range request? " ++ Prelude.show wasRange -- if we didn't have a range request, we just do normal sendfile unless wasRange $ do modifyResponse $ setResponseCode 200 . setContentLength sz liftSnap $ sendFile fp where -------------------------------------------------------------------------- notModified = finishWith $ setResponseCode 304 emptyResponse ------------------------------------------------------------------------------ lookupExt :: a -> HashMap FilePath a -> FilePath -> a lookupExt def m f = if null ext then def else fromMaybe (lookupExt def m (drop 1 ext)) mbe where ext = takeExtensions f mbe = Map.lookup ext m ------------------------------------------------------------------------------ fileType :: MimeMap -> FilePath -> ByteString fileType = lookupExt defaultMimeType ------------------------------------------------------------------------------ defaultMimeType :: ByteString defaultMimeType = "application/octet-stream" ------------------------------------------------------------------------------ data RangeReq = RangeReq { _rangeFirst :: !Int64 , _rangeLast :: !(Maybe Int64) } | SuffixRangeReq { _suffixLength :: !Int64 } deriving (Eq, Prelude.Show) ------------------------------------------------------------------------------ rangeParser :: Parser RangeReq rangeParser = string "bytes=" *> (byteRangeSpec <|> suffixByteRangeSpec) <* endOfInput where byteRangeSpec = do start <- parseNum char '-' end <- option Nothing $ liftM Just parseNum return $! RangeReq start end suffixByteRangeSpec = liftM SuffixRangeReq $ char '-' *> parseNum ------------------------------------------------------------------------------ checkRangeReq :: (MonadSnap m) => Request -> FilePath -> Int64 -> m Bool checkRangeReq req fp sz = do -- TODO/FIXME: multiple ranges dbg $ "checkRangeReq, fp=" ++ fp ++ ", sz=" ++ Prelude.show sz maybe (return False) (\s -> either (const $ return False) withRange (fullyParse s rangeParser)) (getHeader "range" req) where withRange rng@(RangeReq start mend) = do dbg $ "withRange: got Range request: " ++ Prelude.show rng let end = fromMaybe (sz-1) mend dbg $ "withRange: start=" ++ Prelude.show start ++ ", end=" ++ Prelude.show end if start < 0 || end < start || start >= sz || end >= sz then send416 else send206 start end withRange rng@(SuffixRangeReq nbytes) = do dbg $ "withRange: got Range request: " ++ Prelude.show rng let end = sz-1 let start = sz - nbytes dbg $ "withRange: start=" ++ Prelude.show start ++ ", end=" ++ Prelude.show end if start < 0 || end < start || start >= sz || end >= sz then send416 else send206 start end -- note: start and end INCLUSIVE here send206 start end = do dbg "inside send206" let len = end-start+1 let crng = toByteString $ mconcat [ fromByteString "bytes " , fromShow start , fromWord8 (c2w '-') , fromShow end , fromWord8 (c2w '/') , fromShow sz ] modifyResponse $ setResponseCode 206 . setHeader "Content-Range" crng . setContentLength len dbg $ "send206: sending range (" ++ Prelude.show start ++ "," ++ Prelude.show (end+1) ++ ") to sendFilePartial" -- end here was inclusive, sendFilePartial is exclusive sendFilePartial fp (start,end+1) return True send416 = do dbg "inside send416" -- if there's an "If-Range" header in the request, then we just send -- back 200 if getHeader "If-Range" req /= Nothing then return False else do let crng = toByteString $ mconcat [ fromByteString "bytes */" , fromShow sz ] modifyResponse $ setResponseCode 416 . setHeader "Content-Range" crng . setContentLength 0 . deleteHeader "Content-Type" . deleteHeader "Content-Encoding" . deleteHeader "Transfer-Encoding" . setResponseBody (enumBuilder mempty) return True ------------------------------------------------------------------------------ dbg :: (MonadIO m) => String -> m () dbg s = debug $ "FileServe:" ++ s ------------------------------------------------------------------------------ uriWithoutQueryString :: Request -> ByteString uriWithoutQueryString rq = S.takeWhile (/= '?') uri where uri = rqURI rq ------------------------------------------------------------------------------ queryStringSuffix :: Request -> ByteString queryStringSuffix rq = S.concat [ s, qs ] where qs = rqQueryString rq s = if S.null qs then "" else "?" snap-core-0.9.8.0/src/Snap/Util/FileUploads.hs0000644000000000000000000010714012565252520017122 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} ------------------------------------------------------------------------------ -- | This module contains primitives and helper functions for handling -- requests with @Content-type: multipart/form-data@, i.e. HTML forms and file -- uploads. -- -- Typically most users will want to use 'handleFileUploads', which writes -- uploaded files to a temporary directory before sending them on to a handler -- specified by the user. -- -- Users who wish to handle their file uploads differently can use the -- lower-level streaming 'Iteratee' interface called 'handleMultipart'. That -- function takes uploaded files and streams them to an 'Iteratee' consumer of -- the user's choosing. -- -- Using these functions requires making \"policy\" decisions which Snap can't -- really make for users, such as \"what's the largest PDF file a user is -- allowed to upload?\" and \"should we read form inputs into the parameters -- mapping?\". Policy is specified on a \"global\" basis (using -- 'UploadPolicy'), and on a per-file basis (using 'PartUploadPolicy', which -- allows you to reject or limit the size of certain uploaded -- @Content-type@s). module Snap.Util.FileUploads ( -- * Functions handleFileUploads , handleMultipart -- * Uploaded parts , PartInfo(..) -- ** Policy -- *** General upload policy , UploadPolicy , defaultUploadPolicy , doProcessFormInputs , setProcessFormInputs , getMaximumFormInputSize , setMaximumFormInputSize , getMaximumNumberOfFormInputs , setMaximumNumberOfFormInputs , getMinimumUploadRate , setMinimumUploadRate , getMinimumUploadSeconds , setMinimumUploadSeconds , getUploadTimeout , setUploadTimeout -- *** Per-file upload policy , PartUploadPolicy , disallow , allowWithMaximumSize -- * Exceptions , FileUploadException , fileUploadExceptionReason , BadPartException , badPartExceptionReason , PolicyViolationException , policyViolationExceptionReason ) where ------------------------------------------------------------------------------ import Control.Arrow import Control.Applicative import Control.Concurrent.MVar import Control.Exception (SomeException(..)) import Control.Monad import Control.Monad.CatchIO import Control.Monad.Trans import qualified Data.Attoparsec.Char8 as Atto import Data.Attoparsec.Char8 import Data.Attoparsec.Enumerator import qualified Data.ByteString.Char8 as S import Data.ByteString.Char8 (ByteString) import Data.ByteString.Internal (c2w) import qualified Data.CaseInsensitive as CI import Data.Enumerator.Binary (iterHandle) import Data.IORef import Data.Int import Data.List hiding (takeWhile) import qualified Data.Map as Map import Data.Maybe import qualified Data.Text as T import Data.Text (Text) import qualified Data.Text.Encoding as TE import Data.Typeable #if MIN_VERSION_base(4,6,0) import Prelude hiding (getLine, takeWhile) #else import Prelude hiding (catch, getLine, takeWhile) #endif import System.Directory import System.IO hiding (isEOF) ------------------------------------------------------------------------------ import Snap.Core import Snap.Iteratee hiding (map) import qualified Snap.Iteratee as I import Snap.Internal.Debug import Snap.Internal.Iteratee.Debug import Snap.Internal.Iteratee.BoyerMooreHorspool import Snap.Internal.Parsing import qualified Snap.Types.Headers as H #ifdef USE_UNIX import System.FilePath (()) import System.Posix.Temp (mkstemp) #endif ------------------------------------------------------------------------------ -- | Reads uploaded files into a temporary directory and calls a user handler -- to process them. -- -- Given a temporary directory, global and file-specific upload policies, and -- a user handler, this function consumes a request body uploaded with -- @Content-type: multipart/form-data@. Each file is read into the temporary -- directory, and then a list of the uploaded files is passed to the user -- handler. After the user handler runs (but before the 'Response' body -- 'Enumerator' is streamed to the client), the files are deleted from disk; -- so if you want to retain or use the uploaded files in the generated -- response, you would need to move or otherwise process them. -- -- The argument passed to the user handler is a list of: -- -- > (PartInfo, Either PolicyViolationException FilePath) -- -- The first half of this tuple is a 'PartInfo', which contains the -- information the client browser sent about the given upload part (like -- filename, content-type, etc). The second half of this tuple is an 'Either' -- stipulating that either: -- -- 1. the file was rejected on a policy basis because of the provided -- 'PartUploadPolicy' handler -- -- 2. the file was accepted and exists at the given path. -- -- If the request's @Content-type@ was not \"@multipart/formdata@\", this -- function skips processing using 'pass'. -- -- If the client's upload rate passes below the configured minimum (see -- 'setMinimumUploadRate' and 'setMinimumUploadSeconds'), this function -- terminates the connection. This setting is there to protect the server -- against slowloris-style denial of service attacks. -- -- If the given 'UploadPolicy' stipulates that you wish form inputs to be -- placed in the 'rqParams' parameter map (using 'setProcessFormInputs'), and -- a form input exceeds the maximum allowable size, this function will throw a -- 'PolicyViolationException'. -- -- If an uploaded part contains MIME headers longer than a fixed internal -- threshold (currently 32KB), this function will throw a 'BadPartException'. handleFileUploads :: (MonadSnap m) => FilePath -- ^ temporary directory -> UploadPolicy -- ^ general upload policy -> (PartInfo -> PartUploadPolicy) -- ^ per-part upload policy -> ([(PartInfo, Either PolicyViolationException FilePath)] -> m a) -- ^ user handler (see function -- description) -> m a handleFileUploads tmpdir uploadPolicy partPolicy handler = do uploadedFiles <- newUploadedFiles (do xs <- handleMultipart uploadPolicy (iter uploadedFiles) handler xs ) `finally` (cleanupUploadedFiles uploadedFiles) where iter uploadedFiles partInfo = maybe disallowed takeIt mbFs where ctText = partContentType partInfo fnText = fromMaybe "" $ partFileName partInfo ct = TE.decodeUtf8 ctText fn = TE.decodeUtf8 fnText (PartUploadPolicy mbFs) = partPolicy partInfo retVal (_,x) = (partInfo, Right x) takeIt maxSize = do debug "handleFileUploads/takeIt: begin" let it = fmap retVal $ joinI' $ iterateeDebugWrapper "takeNoMoreThan" $ takeNoMoreThan maxSize $$ fileReader uploadedFiles tmpdir partInfo it `catches` [ Handler $ \(_ :: TooManyBytesReadException) -> do debug $ "handleFileUploads/iter: " ++ "caught TooManyBytesReadException" skipToEof tooMany maxSize , Handler $ \(e :: SomeException) -> do debug $ "handleFileUploads/iter: caught " ++ show e debug "handleFileUploads/iter: rethrowing" throw e ] tooMany maxSize = return ( partInfo , Left $ PolicyViolationException $ T.concat [ "File \"" , fn , "\" exceeded maximum allowable size " , T.pack $ show maxSize ] ) disallowed = return ( partInfo , Left $ PolicyViolationException $ T.concat [ "Policy disallowed upload of file \"" , fn , "\" with content-type \"" , ct , "\"" ] ) ------------------------------------------------------------------------------ -- | Given an upload policy and a function to consume uploaded \"parts\", -- consume a request body uploaded with @Content-type: multipart/form-data@. -- Normally most users will want to use 'handleFileUploads' (which writes -- uploaded files to a temporary directory and passes their names to a given -- handler) rather than this function; the lower-level 'handleMultipart' -- function should be used if you want to stream uploaded files to your own -- iteratee function. -- -- If the request's @Content-type@ was not \"@multipart/formdata@\", this -- function skips processing using 'pass'. -- -- If the client's upload rate passes below the configured minimum (see -- 'setMinimumUploadRate' and 'setMinimumUploadSeconds'), this function -- terminates the connection. This setting is there to protect the server -- against slowloris-style denial of service attacks. -- -- If the given 'UploadPolicy' stipulates that you wish form inputs to be -- placed in the 'rqParams' parameter map (using 'setProcessFormInputs'), and -- a form input exceeds the maximum allowable size, this function will throw a -- 'PolicyViolationException'. -- -- If an uploaded part contains MIME headers longer than a fixed internal -- threshold (currently 32KB), this function will throw a 'BadPartException'. -- handleMultipart :: (MonadSnap m) => UploadPolicy -- ^ global upload policy -> (PartInfo -> Iteratee ByteString IO a) -- ^ part processor -> m [a] handleMultipart uploadPolicy origPartHandler = do hdrs <- liftM headers getRequest let (ct, mbBoundary) = getContentType hdrs tickleTimeout <- liftM (. max) getTimeoutModifier let bumpTimeout = tickleTimeout $ uploadTimeout uploadPolicy let partHandler = if doProcessFormInputs uploadPolicy then captureVariableOrReadFile (getMaximumFormInputSize uploadPolicy) origPartHandler else (\p -> fmap File (origPartHandler p)) -- not well-formed multipart? bomb out. when (ct /= "multipart/form-data") $ do debug $ "handleMultipart called with content-type=" ++ S.unpack ct ++ ", passing" pass when (isNothing mbBoundary) $ throw $ BadPartException $ "got multipart/form-data without boundary" let boundary = fromJust mbBoundary captures <- runRequestBody (iter bumpTimeout boundary partHandler) xs <- procCaptures [] captures modifyRequest $ \req -> let pp = rqPostParams req in rqModifyParams (\p -> Map.unionWith (++) p pp) req return xs where rateLimit bump m = killIfTooSlow bump (minimumUploadRate uploadPolicy) (minimumUploadSeconds uploadPolicy) m `catchError` \e -> do debug $ "rateLimit: caught " ++ show e let (me::Maybe RateTooSlowException) = fromException e maybe (throwError e) terminateConnection me iter bump boundary ph = iterateeDebugWrapper "killIfTooSlow" $ rateLimit bump $ internalHandleMultipart boundary ph ins k v = Map.insertWith' (flip (++)) k [v] maxFormVars = maximumNumberOfFormInputs uploadPolicy modifyParams f r = r { rqPostParams = f $ rqPostParams r } procCaptures l [] = return $! reverse l procCaptures l ((File x):xs) = procCaptures (x:l) xs procCaptures l ((Capture k v):xs) = do rq <- getRequest let n = Map.size $ rqPostParams rq when (n >= maxFormVars) $ throw $ PolicyViolationException $ T.concat [ "number of form inputs exceeded maximum of " , T.pack $ show maxFormVars ] modifyRequest $ modifyParams (ins k v) procCaptures l xs ------------------------------------------------------------------------------ -- | 'PartInfo' contains information about a \"part\" in a request uploaded -- with @Content-type: multipart/form-data@. data PartInfo = PartInfo { partFieldName :: !ByteString , partFileName :: !(Maybe ByteString) , partContentType :: !ByteString } deriving (Show) ------------------------------------------------------------------------------ -- | All of the exceptions defined in this package inherit from -- 'FileUploadException', so if you write -- -- > foo `catch` \(e :: FileUploadException) -> ... -- -- you can catch a 'BadPartException', a 'PolicyViolationException', etc. data FileUploadException = GenericFileUploadException { _genericFileUploadExceptionReason :: Text } | forall e . (Exception e, Show e) => WrappedFileUploadException { _wrappedFileUploadException :: e , _wrappedFileUploadExceptionReason :: Text } deriving (Typeable) ------------------------------------------------------------------------------ instance Show FileUploadException where show (GenericFileUploadException r) = "File upload exception: " ++ T.unpack r show (WrappedFileUploadException e _) = show e ------------------------------------------------------------------------------ instance Exception FileUploadException ------------------------------------------------------------------------------ fileUploadExceptionReason :: FileUploadException -> Text fileUploadExceptionReason (GenericFileUploadException r) = r fileUploadExceptionReason (WrappedFileUploadException _ r) = r ------------------------------------------------------------------------------ uploadExceptionToException :: Exception e => e -> Text -> SomeException uploadExceptionToException e r = SomeException $ WrappedFileUploadException e r ------------------------------------------------------------------------------ uploadExceptionFromException :: Exception e => SomeException -> Maybe e uploadExceptionFromException x = do WrappedFileUploadException e _ <- fromException x cast e ------------------------------------------------------------------------------ data BadPartException = BadPartException { badPartExceptionReason :: Text } deriving (Typeable) instance Exception BadPartException where toException e@(BadPartException r) = uploadExceptionToException e r fromException = uploadExceptionFromException instance Show BadPartException where show (BadPartException s) = "Bad part: " ++ T.unpack s ------------------------------------------------------------------------------ data PolicyViolationException = PolicyViolationException { policyViolationExceptionReason :: Text } deriving (Typeable) instance Exception PolicyViolationException where toException e@(PolicyViolationException r) = uploadExceptionToException e r fromException = uploadExceptionFromException instance Show PolicyViolationException where show (PolicyViolationException s) = "File upload policy violation: " ++ T.unpack s ------------------------------------------------------------------------------ -- | 'UploadPolicy' controls overall policy decisions relating to -- @multipart/form-data@ uploads, specifically: -- -- * whether to treat parts without filenames as form input (reading them into -- the 'rqParams' map) -- -- * because form input is read into memory, the maximum size of a form input -- read in this manner, and the maximum number of form inputs -- -- * the minimum upload rate a client must maintain before we kill the -- connection; if very low-bitrate uploads were allowed then a Snap server -- would be vulnerable to a trivial denial-of-service using a -- \"slowloris\"-type attack -- -- * the minimum number of seconds which must elapse before we start killing -- uploads for having too low an upload rate. -- -- * the amount of time we should wait before timing out the connection -- whenever we receive input from the client. data UploadPolicy = UploadPolicy { processFormInputs :: Bool , maximumFormInputSize :: Int64 , maximumNumberOfFormInputs :: Int , minimumUploadRate :: Double , minimumUploadSeconds :: Int , uploadTimeout :: Int } deriving (Show, Eq) ------------------------------------------------------------------------------ -- | A reasonable set of defaults for upload policy. The default policy is: -- -- [@maximum form input size@] 128kB -- -- [@maximum number of form inputs@] 10 -- -- [@minimum upload rate@] 1kB/s -- -- [@seconds before rate limiting kicks in@] 10 -- -- [@inactivity timeout@] 20 seconds -- defaultUploadPolicy :: UploadPolicy defaultUploadPolicy = UploadPolicy True maxSize maxNum minRate minSeconds tout where maxSize = 2^(17::Int) maxNum = 10 minRate = 1000 minSeconds = 10 tout = 20 ------------------------------------------------------------------------------ -- | Does this upload policy stipulate that we want to treat parts without -- filenames as form input? doProcessFormInputs :: UploadPolicy -> Bool doProcessFormInputs = processFormInputs ------------------------------------------------------------------------------ -- | Set the upload policy for treating parts without filenames as form input. setProcessFormInputs :: Bool -> UploadPolicy -> UploadPolicy setProcessFormInputs b u = u { processFormInputs = b } ------------------------------------------------------------------------------ -- | Get the maximum size of a form input which will be read into our -- 'rqParams' map. getMaximumFormInputSize :: UploadPolicy -> Int64 getMaximumFormInputSize = maximumFormInputSize ------------------------------------------------------------------------------ -- | Set the maximum size of a form input which will be read into our -- 'rqParams' map. setMaximumFormInputSize :: Int64 -> UploadPolicy -> UploadPolicy setMaximumFormInputSize s u = u { maximumFormInputSize = s } ------------------------------------------------------------------------------ -- | Get the maximum size of a form input which will be read into our -- 'rqParams' map. getMaximumNumberOfFormInputs :: UploadPolicy -> Int getMaximumNumberOfFormInputs = maximumNumberOfFormInputs ------------------------------------------------------------------------------ -- | Set the maximum size of a form input which will be read into our -- 'rqParams' map. setMaximumNumberOfFormInputs :: Int -> UploadPolicy -> UploadPolicy setMaximumNumberOfFormInputs s u = u { maximumNumberOfFormInputs = s } ------------------------------------------------------------------------------ -- | Get the minimum rate (in /bytes\/second/) a client must maintain before -- we kill the connection. getMinimumUploadRate :: UploadPolicy -> Double getMinimumUploadRate = minimumUploadRate ------------------------------------------------------------------------------ -- | Set the minimum rate (in /bytes\/second/) a client must maintain before -- we kill the connection. setMinimumUploadRate :: Double -> UploadPolicy -> UploadPolicy setMinimumUploadRate s u = u { minimumUploadRate = s } ------------------------------------------------------------------------------ -- | Get the amount of time which must elapse before we begin enforcing the -- upload rate minimum getMinimumUploadSeconds :: UploadPolicy -> Int getMinimumUploadSeconds = minimumUploadSeconds ------------------------------------------------------------------------------ -- | Set the amount of time which must elapse before we begin enforcing the -- upload rate minimum setMinimumUploadSeconds :: Int -> UploadPolicy -> UploadPolicy setMinimumUploadSeconds s u = u { minimumUploadSeconds = s } ------------------------------------------------------------------------------ -- | Get the \"upload timeout\". Whenever input is received from the client, -- the connection timeout is set this many seconds in the future. getUploadTimeout :: UploadPolicy -> Int getUploadTimeout = uploadTimeout ------------------------------------------------------------------------------ -- | Set the upload timeout. setUploadTimeout :: Int -> UploadPolicy -> UploadPolicy setUploadTimeout s u = u { uploadTimeout = s } ------------------------------------------------------------------------------ -- | Upload policy can be set on an \"general\" basis (using 'UploadPolicy'), -- but handlers can also make policy decisions on individual files\/parts -- uploaded. For each part uploaded, handlers can decide: -- -- * whether to allow the file upload at all -- -- * the maximum size of uploaded files, if allowed data PartUploadPolicy = PartUploadPolicy { _maximumFileSize :: Maybe Int64 } deriving (Show, Eq) ------------------------------------------------------------------------------ -- | Disallows the file to be uploaded. disallow :: PartUploadPolicy disallow = PartUploadPolicy Nothing ------------------------------------------------------------------------------ -- | Allows the file to be uploaded, with maximum size /n/. allowWithMaximumSize :: Int64 -> PartUploadPolicy allowWithMaximumSize = PartUploadPolicy . Just ------------------------------------------------------------------------------ -- private exports follow. FIXME: organize ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ captureVariableOrReadFile :: Int64 -- ^ maximum size of form input -> (PartInfo -> Iteratee ByteString IO a) -- ^ file reading code -> (PartInfo -> Iteratee ByteString IO (Capture a)) captureVariableOrReadFile maxSize fileHandler partInfo = case partFileName partInfo of Nothing -> iter _ -> liftM File $ fileHandler partInfo where iter = varIter `catchError` handler fieldName = partFieldName partInfo varIter = do var <- liftM S.concat $ joinI' $ takeNoMoreThan maxSize $$ consume return $! Capture fieldName var handler e = do debug $ "captureVariableOrReadFile/handler: caught " ++ show e let m = fromException e :: Maybe TooManyBytesReadException case m of Nothing -> do debug "didn't expect this error, rethrowing" throwError e Just _ -> do debug "rethrowing as PolicyViolationException" throwError $ PolicyViolationException $ T.concat [ "form input '" , TE.decodeUtf8 fieldName , "' exceeded maximum permissible size (" , T.pack $ show maxSize , " bytes)" ] ------------------------------------------------------------------------------ data Capture a = Capture ByteString ByteString | File a deriving (Show) ------------------------------------------------------------------------------ fileReader :: UploadedFiles -> FilePath -> PartInfo -> Iteratee ByteString IO (PartInfo, FilePath) fileReader uploadedFiles tmpdir partInfo = do debug "fileReader: begin" (fn, h) <- openFileForUpload uploadedFiles tmpdir let i = iterateeDebugWrapper "fileReader" $ iter fn h i `catch` \(e::SomeException) -> throwError e where iter fileName h = do iterHandle h debug "fileReader: closing active file" closeActiveFile uploadedFiles return (partInfo, fileName) ------------------------------------------------------------------------------ internalHandleMultipart :: ByteString -- ^ boundary value -> (PartInfo -> Iteratee ByteString IO a) -- ^ part processor -> Iteratee ByteString IO [a] internalHandleMultipart boundary clientHandler = go `catch` errorHandler where -------------------------------------------------------------------------- errorHandler :: SomeException -> Iteratee ByteString IO a errorHandler e = do skipToEof throwError e -------------------------------------------------------------------------- go = do -- swallow the first boundary _ <- iterParser $ parseFirstBoundary boundary step <- iterateeDebugWrapper "boyer-moore" $ (bmhEnumeratee (fullBoundary boundary) $$ processParts iter) liftM concat $ lift $ run_ $ returnI step -------------------------------------------------------------------------- pBoundary b = Atto.try $ do _ <- string "--" string b -------------------------------------------------------------------------- fullBoundary b = S.concat ["\r\n", "--", b] pLine = takeWhile (not . isEndOfLine . c2w) <* eol takeLine = pLine *> pure () parseFirstBoundary b = pBoundary b <|> (takeLine *> parseFirstBoundary b) -------------------------------------------------------------------------- takeHeaders = hdrs `catchError` handler where hdrs = liftM toHeaders $ iterateeDebugWrapper "header parser" $ joinI' $ takeNoMoreThan mAX_HDRS_SIZE $$ iterParser pHeadersWithSeparator handler e = do debug $ "internalHandleMultipart/takeHeaders: caught " ++ show e let m = fromException e :: Maybe TooManyBytesReadException case m of Nothing -> throwError e Just _ -> throwError $ BadPartException $ "headers exceeded maximum size" -------------------------------------------------------------------------- iter = do hdrs <- takeHeaders debug $ "internalHandleMultipart/iter: got headers" -- are we using mixed? let (contentType, mboundary) = getContentType hdrs let (fieldName, fileName) = getFieldName hdrs if contentType == "multipart/mixed" then maybe (throwError $ BadPartException $ "got multipart/mixed without boundary") (processMixed fieldName) mboundary else do let info = PartInfo fieldName fileName contentType liftM (:[]) $ clientHandler info -------------------------------------------------------------------------- processMixed fieldName mixedBoundary = do -- swallow the first boundary _ <- iterParser $ parseFirstBoundary mixedBoundary step <- iterateeDebugWrapper "boyer-moore" $ (bmhEnumeratee (fullBoundary mixedBoundary) $$ processParts (mixedIter fieldName)) lift $ run_ $ returnI step -------------------------------------------------------------------------- mixedIter fieldName = do hdrs <- takeHeaders let (contentType, _) = getContentType hdrs let (_, fileName) = getFieldName hdrs let info = PartInfo fieldName fileName contentType clientHandler info ------------------------------------------------------------------------------ getContentType :: Headers -> (ByteString, Maybe ByteString) getContentType hdrs = (contentType, boundary) where contentTypeValue = fromMaybe "text/plain" $ getHeader "content-type" hdrs eCT = fullyParse contentTypeValue pContentTypeWithParameters (contentType, params) = either (const ("text/plain", [])) id eCT boundary = findParam "boundary" params ------------------------------------------------------------------------------ getFieldName :: Headers -> (ByteString, Maybe ByteString) getFieldName hdrs = (fieldName, fileName) where contentDispositionValue = fromMaybe "" $ getHeader "content-disposition" hdrs eDisposition = fullyParse contentDispositionValue pValueWithParameters (_, dispositionParameters) = either (const ("", [])) id eDisposition fieldName = fromMaybe "" $ findParam "name" dispositionParameters fileName = findParam "filename" dispositionParameters ------------------------------------------------------------------------------ findParam :: (Eq a) => a -> [(a, b)] -> Maybe b findParam p = fmap snd . find ((== p) . fst) ------------------------------------------------------------------------------ -- | Given a 'MatchInfo' stream which is partitioned by boundary values, read -- up until the next boundary and send all of the chunks into the wrapped -- iteratee processPart :: (Monad m) => Enumeratee MatchInfo ByteString m a processPart st = {-# SCC "pPart/outer" #-} case st of (Continue k) -> go k _ -> yield st (Chunks []) where go :: (Monad m) => (Stream ByteString -> Iteratee ByteString m a) -> Iteratee MatchInfo m (Step ByteString m a) go !k = {-# SCC "pPart/go" #-} I.head >>= maybe finished process where -- called when outer stream is EOF finished = {-# SCC "pPart/finish" #-} lift $ runIteratee $ k EOF -- no match ==> pass the stream chunk along process (NoMatch !s) = {-# SCC "pPart/noMatch" #-} do !step <- lift $ runIteratee $ k $ Chunks [s] case step of (Continue k') -> go k' _ -> yield step (Chunks []) process (Match _) = {-# SCC "pPart/match" #-} lift $ runIteratee $ k EOF ------------------------------------------------------------------------------ -- | Assuming we've already identified the boundary value and run -- 'bmhEnumeratee' to split the input up into parts which match and parts -- which don't, run the given 'ByteString' iteratee over each part and grab a -- list of the resulting values. processParts :: Iteratee ByteString IO a -> Iteratee MatchInfo IO [a] processParts partIter = iterateeDebugWrapper "processParts" $ go id where iter = {-# SCC "processParts/iter" #-} do isLast <- bParser if isLast then return Nothing else do !x <- partIter skipToEof return $! Just x go !soFar = {-# SCC "processParts/go" #-} do b <- isEOF if b then return $ soFar [] else do -- processPart $$ iter -- :: Iteratee MatchInfo m (Step ByteString m a) innerStep <- processPart $$ iter -- output :: Maybe a output <- lift $ run_ $ returnI innerStep case output of Just x -> go (soFar . (x:)) Nothing -> return $ soFar [] bParser = iterateeDebugWrapper "boundary debugger" $ iterParser $ pBoundaryEnd pBoundaryEnd = (eol *> pure False) <|> (string "--" *> pure True) ------------------------------------------------------------------------------ eol :: Parser ByteString eol = (string "\n") <|> (string "\r\n") ------------------------------------------------------------------------------ pHeadersWithSeparator :: Parser [(ByteString,ByteString)] pHeadersWithSeparator = pHeaders <* crlf ------------------------------------------------------------------------------ toHeaders :: [(ByteString,ByteString)] -> Headers toHeaders kvps = H.fromList kvps' where kvps' = map (first CI.mk) kvps ------------------------------------------------------------------------------ mAX_HDRS_SIZE :: Int64 mAX_HDRS_SIZE = 32768 ------------------------------------------------------------------------------ -- We need some code to keep track of the files we have already successfully -- created in case an exception is thrown by the request body enumerator or -- one of the client iteratees. data UploadedFilesState = UploadedFilesState { -- | This is the file which is currently being written to. If the -- calling function gets an exception here, it is responsible for -- closing and deleting this file. _currentFile :: Maybe (FilePath, Handle) -- | .. and these files have already been successfully read and closed. , _alreadyReadFiles :: [FilePath] } ------------------------------------------------------------------------------ emptyUploadedFilesState :: UploadedFilesState emptyUploadedFilesState = UploadedFilesState Nothing [] ------------------------------------------------------------------------------ data UploadedFiles = UploadedFiles (IORef UploadedFilesState) (MVar ()) ------------------------------------------------------------------------------ newUploadedFiles :: MonadIO m => m UploadedFiles newUploadedFiles = liftIO $ do r <- newIORef emptyUploadedFilesState m <- newMVar () let u = UploadedFiles r m addMVarFinalizer m $ cleanupUploadedFiles u return u ------------------------------------------------------------------------------ cleanupUploadedFiles :: (MonadIO m) => UploadedFiles -> m () cleanupUploadedFiles (UploadedFiles stateRef _) = liftIO $ do state <- readIORef stateRef killOpenFile state mapM_ killFile $ _alreadyReadFiles state writeIORef stateRef emptyUploadedFilesState where killFile = eatException . removeFile killOpenFile state = maybe (return ()) (\(fp,h) -> do eatException $ hClose h eatException $ removeFile fp) (_currentFile state) ------------------------------------------------------------------------------ openFileForUpload :: (MonadIO m) => UploadedFiles -> FilePath -> m (FilePath, Handle) openFileForUpload ufs@(UploadedFiles stateRef _) tmpdir = liftIO $ do state <- readIORef stateRef -- It should be an error to open a new file with this interface if there -- is already a file handle active. when (isJust $ _currentFile state) $ do cleanupUploadedFiles ufs throw $ GenericFileUploadException alreadyOpenMsg fph@(_,h) <- makeTempFile tmpdir "snap-" hSetBuffering h NoBuffering writeIORef stateRef $ state { _currentFile = Just fph } return fph where alreadyOpenMsg = T.concat [ "Internal error! UploadedFiles: " , "opened new file with pre-existing open handle" ] ------------------------------------------------------------------------------ closeActiveFile :: (MonadIO m) => UploadedFiles -> m () closeActiveFile (UploadedFiles stateRef _) = liftIO $ do state <- readIORef stateRef let m = _currentFile state maybe (return ()) (\(fp,h) -> do eatException $ hClose h writeIORef stateRef $ state { _currentFile = Nothing , _alreadyReadFiles = fp:(_alreadyReadFiles state) }) m ------------------------------------------------------------------------------ eatException :: (MonadCatchIO m) => m a -> m () eatException m = (m >> return ()) `catch` (\(_ :: SomeException) -> return ()) makeTempFile :: FilePath -> String -> IO (FilePath, Handle) #ifdef USE_UNIX makeTempFile fp temp = mkstemp $ fp (temp ++ "XXXXXXX") #else makeTempFile = openBinaryTempFile #endif snap-core-0.9.8.0/src/Snap/Util/GZip.hs0000644000000000000000000002130112565252520015556 0ustar0000000000000000{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE ExistentialQuantification #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PackageImports #-} {-# LANGUAGE ScopedTypeVariables #-} module Snap.Util.GZip ( withCompression , withCompression' , noCompression ) where import Blaze.ByteString.Builder import qualified Codec.Zlib.Enum as Z import Control.Applicative import Control.Exception import Control.Monad import Control.Monad.Trans import Data.Attoparsec.Char8 import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as S import qualified Data.Char as Char import Data.Maybe import Data.Monoid import qualified Data.Set as Set import Data.Set (Set) import Data.Typeable #if MIN_VERSION_base(4,6,0) import Prelude hiding (takeWhile) #else import Prelude hiding (catch, takeWhile) #endif ---------------------------------------------------------------------------- import Snap.Core import Snap.Internal.Debug import Snap.Internal.Parsing import Snap.Iteratee import qualified Snap.Iteratee as I ------------------------------------------------------------------------------ -- | Runs a 'Snap' web handler with compression if available. -- -- If the client has indicated support for @gzip@ or @deflate@ in its -- @Accept-Encoding@ header, and the @Content-Type@ in the response is one of -- the following types: -- -- * @application/x-javascript@ -- -- * @application/json@ -- -- * @text/css@ -- -- * @text/html@ -- -- * @text/javascript@ -- -- * @text/plain@ -- -- * @text/xml@ -- -- * @application/x-font-truetype@ -- -- Then the given handler's output stream will be compressed, -- @Content-Encoding@ will be set in the output headers, and the -- @Content-Length@ will be cleared if it was set. (We can't process the -- stream in O(1) space if the length is known beforehand.) -- -- The wrapped handler will be run to completion, and then the 'Response' -- that's contained within the 'Snap' monad state will be passed to -- 'finishWith' to prevent further processing. -- withCompression :: MonadSnap m => m a -- ^ the web handler to run -> m () withCompression = withCompression' compressibleMimeTypes ------------------------------------------------------------------------------ -- | The same as 'withCompression', with control over which MIME types to -- compress. withCompression' :: MonadSnap m => Set ByteString -- ^ set of compressible MIME types -> m a -- ^ the web handler to run -> m () withCompression' mimeTable action = do _ <- action resp <- getResponse -- If a content-encoding is already set, do nothing. This prevents -- "withCompression $ withCompression m" from ruining your day. when (not $ isJust $ getHeader "Content-Encoding" resp) $ do let mbCt = fmap chop $ getHeader "Content-Type" resp debug $ "withCompression', content-type is " ++ show mbCt case mbCt of (Just ct) -> when (Set.member ct mimeTable) chkAcceptEncoding _ -> return $! () getResponse >>= finishWith where chop = S.takeWhile (\c -> c /= ';' && not (Char.isSpace c)) chkAcceptEncoding = do req <- getRequest debug $ "checking accept-encoding" let mbAcc = getHeader "Accept-Encoding" req debug $ "accept-encoding is " ++ show mbAcc let s = fromMaybe "" mbAcc types <- liftIO $ parseAcceptEncoding s chooseType types chooseType [] = return $! () chooseType ("gzip":_) = gzipCompression "gzip" chooseType ("deflate":_) = compressCompression "deflate" chooseType ("x-gzip":_) = gzipCompression "x-gzip" chooseType ("x-deflate":_) = compressCompression "x-deflate" chooseType (_:xs) = chooseType xs ------------------------------------------------------------------------------ -- | Turn off compression by setting \"Content-Encoding: identity\" in the -- response headers. noCompression :: MonadSnap m => m () noCompression = modifyResponse $ setHeader "Content-Encoding" "identity" ------------------------------------------------------------------------------ -- private following ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ compressibleMimeTypes :: Set ByteString compressibleMimeTypes = Set.fromList [ "application/x-font-truetype" , "application/x-javascript" , "application/json" , "text/css" , "text/html" , "text/javascript" , "text/plain" , "text/xml" ] ------------------------------------------------------------------------------ gzipCompression :: MonadSnap m => ByteString -> m () gzipCompression ce = modifyResponse f where f r = setHeader "Content-Encoding" ce $ setHeader "Vary" "Accept-Encoding" $ clearContentLength $ modifyResponseBody (gcompress (getBufferingMode r)) r ------------------------------------------------------------------------------ compressCompression :: MonadSnap m => ByteString -> m () compressCompression ce = modifyResponse f where f r = setHeader "Content-Encoding" ce $ setHeader "Vary" "Accept-Encoding" $ clearContentLength $ modifyResponseBody (ccompress (getBufferingMode r)) r ------------------------------------------------------------------------------ gcompress :: Bool -- ^ buffer? -> forall a . Enumerator Builder IO a -> Enumerator Builder IO a gcompress buffer e st = e $$ iFinal where i0 = returnI st iNoB = mapFlush =$ i0 iZNoB = Z.gzip =$ iNoB iB = I.map fromByteString =$ i0 iZ = Z.gzip =$ iB iFinal = enumBuilderToByteString =$ if buffer then iZ else iZNoB mapFlush :: Monad m => Enumeratee ByteString Builder m b mapFlush = I.map ((`mappend` flush) . fromByteString) ------------------------------------------------------------------------------ ccompress :: Bool -- ^ buffer? -> forall a . Enumerator Builder IO a -> Enumerator Builder IO a ccompress buffer e st = e $$ iFinal where i0 = returnI st iNoB = mapFlush =$ i0 iZNoB = Z.compress 5 Z.defaultWindowBits =$ iNoB iB = I.map fromByteString =$ i0 iZ = Z.compress 5 Z.defaultWindowBits =$ iB iFinal = enumBuilderToByteString =$ if buffer then iZ else iZNoB mapFlush :: Monad m => Enumeratee ByteString Builder m b mapFlush = I.map ((`mappend` flush) . fromByteString) ------------------------------------------------------------------------------ -- We're not gonna bother with quality values; we'll do gzip or compress in -- that order. acceptParser :: Parser [ByteString] acceptParser = do xs <- option [] $ (:[]) <$> encoding ys <- many (char ',' *> encoding) endOfInput return $! xs ++ ys where encoding = skipSpace *> c <* skipSpace c = do x <- coding option () qvalue return x qvalue = do skipSpace char ';' skipSpace char 'q' skipSpace char '=' float return () coding = string "*" <|> takeWhile isCodingChar isCodingChar ch = isDigit ch || isAlpha_ascii ch || ch == '-' || ch == '_' float = takeWhile isDigit >> option () (char '.' >> takeWhile isDigit >> pure ()) ------------------------------------------------------------------------------ data BadAcceptEncodingException = BadAcceptEncodingException deriving (Typeable) ------------------------------------------------------------------------------ instance Show BadAcceptEncodingException where show BadAcceptEncodingException = "bad 'accept-encoding' header" ------------------------------------------------------------------------------ instance Exception BadAcceptEncodingException ------------------------------------------------------------------------------ parseAcceptEncoding :: ByteString -> IO [ByteString] parseAcceptEncoding s = case r of Left _ -> throwIO BadAcceptEncodingException Right x -> return x where r = fullyParse s acceptParser snap-core-0.9.8.0/src/Snap/Util/Proxy.hs0000644000000000000000000000470112565252520016033 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} -- | This module provides facilities for patching incoming 'Requests' to -- correct the value of 'rqRemoteAddr' if the snap server is running behind a -- proxy. -- -- Example usage: -- -- @ -- m :: Snap () -- m = undefined -- code goes here -- -- applicationHandler :: Snap () -- applicationHandler = behindProxy X_Forwarded_For m -- @ -- module Snap.Util.Proxy ( ProxyType(..) , behindProxy ) where ------------------------------------------------------------------------------ import Control.Applicative import Control.Arrow (second) import qualified Data.ByteString.Char8 as S import Data.Char (isSpace) import Data.Maybe (fromJust) ------------------------------------------------------------------------------ import Snap.Core ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ -- | What kind of proxy is this? Affects which headers 'behindProxy' pulls the -- original remote address from. -- -- Currently only proxy servers that send @X-Forwarded-For@ or @Forwarded-For@ -- are supported. data ProxyType = NoProxy -- ^ no proxy, leave the request alone | X_Forwarded_For -- ^ Use the @Forwarded-For@ or -- @X-Forwarded-For@ header deriving (Read, Show, Eq, Ord) ------------------------------------------------------------------------------ -- | Rewrite 'rqRemoteAddr' if we're behind a proxy. behindProxy :: MonadSnap m => ProxyType -> m a -> m a behindProxy NoProxy = id behindProxy X_Forwarded_For = ((modifyRequest xForwardedFor) >>) {-# INLINE behindProxy #-} ------------------------------------------------------------------------------ xForwardedFor :: Request -> Request xForwardedFor req = req { rqRemoteAddr = ip , rqRemotePort = port } where proxyString = getHeader "Forwarded-For" req <|> getHeader "X-Forwarded-For" req <|> Just (rqRemoteAddr req) proxyAddr = trim . snd . S.breakEnd (== ',') . fromJust $ proxyString trim = fst . S.spanEnd isSpace . S.dropWhile isSpace (ip,portStr) = second (S.drop 1) . S.break (== ':') $ proxyAddr port = fromJust (fst <$> S.readInt portStr <|> Just (rqRemotePort req)) {-# INLINE xForwardedFor #-} snap-core-0.9.8.0/src/Snap/Util/Readable.hs0000644000000000000000000000230212565252520016404 0ustar0000000000000000module Snap.Util.Readable ( Readable(..) ) where ------------------------------------------------------------------------------ import Data.ByteString.Char8 (ByteString) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Encoding import Data.Text.Read ------------------------------------------------------------------------------ -- | Monadic analog to Read that uses ByteString instead of String. class Readable a where fromBS :: Monad m => ByteString -> m a ------------------------------------------------------------------------------ -- | Fails if the input wasn't parsed completely. checkComplete :: Monad m => (t, Text) -> m t checkComplete (a,rest) | T.null rest = return a | otherwise = fail "Readable: could not parse completely" instance Readable ByteString where fromBS = return instance Readable Text where fromBS = return . decodeUtf8 instance Readable Int where fromBS = either fail checkComplete . decimal . decodeUtf8 instance Readable Integer where fromBS = either fail checkComplete . decimal . decodeUtf8 instance Readable Double where fromBS = either fail checkComplete . double . decodeUtf8 snap-core-0.9.8.0/cbits/0000755000000000000000000000000012565252520013053 5ustar0000000000000000snap-core-0.9.8.0/cbits/timefuncs.c0000644000000000000000000000107512565252520015217 0ustar0000000000000000#define _XOPEN_SOURCE #define _BSD_SOURCE #include #include void set_c_locale() { setlocale(LC_TIME, "C"); } time_t c_parse_http_time(char* s) { struct tm dest; strptime(s, "%a, %d %b %Y %H:%M:%S GMT", &dest); return timegm(&dest); } void c_format_http_time(time_t src, char* dest) { struct tm t; gmtime_r(&src, &t); strftime(dest, 40, "%a, %d %b %Y %H:%M:%S GMT", &t); } void c_format_log_time(time_t src, char* dest) { struct tm t; localtime_r(&src, &t); strftime(dest, 40, "%d/%b/%Y:%H:%M:%S %z", &t); } snap-core-0.9.8.0/test/0000755000000000000000000000000012565252520012726 5ustar0000000000000000snap-core-0.9.8.0/test/runTestsAndCoverage.sh0000755000000000000000000000204712565252520017216 0ustar0000000000000000#!/bin/sh set -e if [ -z "$DEBUG" ]; then export DEBUG="testsuite" fi SUITE=./dist/build/testsuite/testsuite export LC_ALL=C export LANG=C rm -f testsuite.tix if [ ! -f $SUITE ]; then cat </dev/null 2>&1 rm -f testsuite.tix cat <= 1.6 Flag portable Description: Compile in cross-platform mode. No platform-specific code or optimizations such as C routines will be used. Default: False Executable testsuite hs-source-dirs: ../src suite main-is: TestSuite.hs if flag(portable) || os(windows) cpp-options: -DPORTABLE else c-sources: ../cbits/timefuncs.c include-dirs: ../cbits build-depends: bytestring-mmap >= 0.2.2 && <0.3, unix >= 2.4 && <3.0 cpp-options: -DUSE_UNIX build-depends: QuickCheck >= 2.3.0.2 && <3, attoparsec >= 0.10 && <0.13, attoparsec-enumerator >= 0.3, base >= 4 && <5, blaze-builder >= 0.2.1.4 && <0.5, blaze-builder-enumerator >= 0.2 && <0.3, bytestring, case-insensitive >= 0.3 && <1.3, cereal >= 0.3 && <0.4, containers, deepseq >= 1.1 && <1.4, directory, filepath, HUnit >= 1.2 && <2, enumerator >= 0.4.13.1 && <0.5, MonadCatchIO-transformers >= 0.2 && <0.4, mtl >= 2 && <3, old-locale, parallel >= 3 && <4, pureMD5 == 2.1.*, random >= 1 && <2, regex-posix >= 0.94.4 && <0.96, test-framework >= 0.6 && <0.7, test-framework-hunit >= 0.2.7 && <0.3, test-framework-quickcheck2 >= 0.2.12.1 && <0.3, text >= 0.11 && <1.2, time, transformers, unix-compat >= 0.2 && <0.5, unordered-containers >= 0.1.4.3 && <0.3, vector >= 0.6 && <0.11, zlib, zlib-enum >= 0.2.1 && <0.3 extensions: BangPatterns, CPP, PackageImports, ScopedTypeVariables, EmptyDataDecls, ForeignFunctionInterface, OverloadedStrings, Rank2Types, TypeSynonymInstances, FlexibleInstances, ExistentialQuantification, OverloadedStrings, FlexibleContexts, GeneralizedNewtypeDeriving, DeriveDataTypeable, MultiParamTypeClasses ghc-options: -O2 -Wall -fhpc -fwarn-tabs -funbox-strict-fields -threaded -fno-warn-unused-do-bind snap-core-0.9.8.0/test/suite/0000755000000000000000000000000012565252520014057 5ustar0000000000000000snap-core-0.9.8.0/test/suite/TestSuite.hs0000644000000000000000000000327612565252520016354 0ustar0000000000000000module Main where import Test.Framework (defaultMain, testGroup) ------------------------------------------------------------------------------ import qualified Snap.Core.Tests import qualified Snap.Internal.Http.Types.Tests import qualified Snap.Internal.Parsing.Tests import qualified Snap.Internal.Routing.Tests import qualified Snap.Iteratee.Tests import qualified Snap.Util.FileServe.Tests import qualified Snap.Util.FileUploads.Tests import qualified Snap.Util.GZip.Tests import qualified Snap.Util.Proxy.Tests import qualified Snap.Test.Tests ------------------------------------------------------------------------------ main :: IO () main = defaultMain tests where tests = [ testGroup "Snap.Internal.Http.Types.Tests" Snap.Internal.Http.Types.Tests.tests , testGroup "Snap.Internal.Routing.Tests" Snap.Internal.Routing.Tests.tests , testGroup "Snap.Core.Tests" Snap.Core.Tests.tests , testGroup "Snap.Iteratee.Tests" Snap.Iteratee.Tests.tests , testGroup "Snap.Internal.Parsing.Tests" Snap.Internal.Parsing.Tests.tests , testGroup "Snap.Util.FileServe.Tests" Snap.Util.FileServe.Tests.tests , testGroup "Snap.Util.FileUploads.Tests" Snap.Util.FileUploads.Tests.tests , testGroup "Snap.Util.Proxy.Tests" Snap.Util.Proxy.Tests.tests , testGroup "Snap.Util.GZip.Tests" Snap.Util.GZip.Tests.tests , testGroup "Snap.Test.Tests" Snap.Test.Tests.tests ] snap-core-0.9.8.0/test/suite/Snap/0000755000000000000000000000000012565252520014760 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Core/0000755000000000000000000000000012565252520015650 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Core/Tests.hs0000644000000000000000000004743612565252520017324 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE Rank2Types #-} {-# LANGUAGE ScopedTypeVariables #-} module Snap.Core.Tests ( tests ) where import Blaze.ByteString.Builder import Control.Applicative import Control.Concurrent.MVar import Control.DeepSeq import Control.Exception (ErrorCall (..), SomeException, throwIO) import Control.Monad import Control.Monad.CatchIO import Control.Monad.Trans (liftIO) import Control.Parallel.Strategies import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as S import qualified Data.ByteString.Lazy.Char8 as L import qualified Data.IntMap as IM import Data.IORef import qualified Data.Map as Map import Data.Maybe (isJust) import Data.Monoid import Data.Text (Text) import qualified Data.Text.Encoding as T import Data.Text.Lazy () import Prelude hiding (catch) import Test.Framework import Test.Framework.Providers.HUnit import Test.Framework.Providers.QuickCheck2 import Test.HUnit hiding (Test, path) import Test.QuickCheck (Gen, arbitrary, elements, oneof, variant) import Snap.Internal.Exceptions import Snap.Internal.Http.Types import Snap.Internal.Parsing import Snap.Internal.Types import Snap.Iteratee import qualified Snap.Iteratee as I import Snap.Test.Common import qualified Snap.Types.Headers as H tests :: [Test] tests = [ testFail , testAlternative , testEarlyTermination , testEscapeHttp , testCatchFinishWith , testRqBody , testRqBodyTooLong , testRqBodyException , testRqBodyTermination , testTrivials , testMethod , testMethods , testMethodEq , testMethodNotEq , testDir , testCatchIO , testWrites , testParam , testURLEncode1 , testURLEncode2 , testDir2 , testIpHeaderFilter , testMZero404 , testEvalSnap , testLocalRequest , testRedirect , testBracketSnap , testPathArgs ] expectSpecificException :: Exception e => e -> IO a -> IO () expectSpecificException e0 m = do r <- try m let b = either (\se -> isJust $ forceSameType (Just e0) (fromException se)) (const False) r assertBool ("expected specific exception: " ++ show e0) b expect404 :: IO (Request,Response) -> IO () expect404 m = do (_,r) <- m assertBool "expected 404" (rspStatus r == 404) expectNo404 :: IO (Request,Response) -> IO () expectNo404 m = do (_,r) <- m assertBool ("expected 200, got " ++ show (rspStatus r)) (rspStatus r /= 404) mkRequest :: ByteString -> IO Request mkRequest uri = do enum <- newIORef $ SomeEnumerator returnI return $! Request "foo" 80 "127.0.0.1" 999 "foo" 1000 "foo" False H.empty enum Nothing GET (1,1) [] uri "/" (S.concat ["/",uri]) "" Map.empty Map.empty Map.empty mkRequestQuery :: ByteString -> ByteString -> [ByteString] -> IO Request mkRequestQuery uri k v = do enum <- newIORef $ SomeEnumerator returnI let mp = Map.fromList [(k,v)] let q = S.concat [k,"=", S.concat v] return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False H.empty enum Nothing GET (1,1) [] uri "/" (S.concat ["/",uri,"?",q]) q mp mp Map.empty mkZomgRq :: IO Request mkZomgRq = do enum <- newIORef $ SomeEnumerator returnI return $ Request "foo" 80 "127.0.0.1" 999 "foo" 1000 "foo" False H.empty enum Nothing GET (1,1) [] "/" "/" "/" "" Map.empty Map.empty Map.empty mkMethodRq :: Method -> IO Request mkMethodRq m = do enum <- newIORef $ SomeEnumerator returnI return $ Request "foo" 80 "127.0.0.1" 999 "foo" 1000 "foo" False H.empty enum Nothing m (1,1) [] "/" "/" "/" "" Map.empty Map.empty Map.empty mkIpHeaderRq :: IO Request mkIpHeaderRq = do rq <- mkZomgRq return $ setHeader "X-Forwarded-For" "1.2.3.4" $ deleteHeader "X-Forwarded-For" $ setHeader "X-Forwarded-For" "1.2.3.4" rq mkRqWithBody :: IO Request mkRqWithBody = mkRqWithEnum (enumBS "zazzle" >==> enumEOF) mkRqWithEnum :: (forall a . Enumerator ByteString IO a) -> IO Request mkRqWithEnum e = do enum <- newIORef $ SomeEnumerator e return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False H.empty enum Nothing GET (1,1) [] "/" "/" "/" "" Map.empty Map.empty Map.empty testCatchIO :: Test testCatchIO = testCase "types/catchIO" $ do (_,rsp) <- go f (_,rsp2) <- go g assertEqual "catchIO 1" (Just "bar") $ getHeader "foo" rsp assertEqual "catchIO 2" Nothing $ getHeader "foo" rsp2 where f :: Snap () f = (block $ unblock $ throw $ NoHandlerException "") `catch` h g :: Snap () g = return () `catch` h h :: SomeException -> Snap () h e = e `seq` modifyResponse $ addHeader "foo" "bar" go :: Snap a -> IO (Request,Response) go m = do zomgRq <- mkZomgRq run_ $ runSnap m dummy (const (return ())) zomgRq where dummy !x = return $! (show x `using` rdeepseq) `seq` () goMeth :: Method -> Snap a -> IO (Request,Response) goMeth m s = do methRq <- mkMethodRq m run_ $ runSnap s dummy (const (return ())) methRq where dummy !x = return $! (show x `using` rdeepseq) `seq` () goIP :: Snap a -> IO (Request,Response) goIP m = do rq <- mkIpHeaderRq run_ $ runSnap m dummy (const (return ())) rq where dummy = const $ return () goPath :: ByteString -> Snap a -> IO (Request,Response) goPath s m = do rq <- mkRequest s run_ $ runSnap m dummy (const (return ())) rq where dummy = const $ return () goPathQuery :: ByteString -> ByteString -> [ByteString] -> Snap a -> IO (Request,Response) goPathQuery s k v m = do rq <- mkRequestQuery s k v run_ $ runSnap m dummy (const (return ())) rq where dummy = const $ return () goBody :: Snap a -> IO (Request,Response) goBody m = do rq <- mkRqWithBody run_ $ runSnap m dummy (const (return ())) rq where dummy = const $ return () goEnum :: (forall a . Enumerator ByteString IO a) -> Snap b -> IO (Request,Response) goEnum enum m = do rq <- mkRqWithEnum enum run_ $ runSnap m dummy (const (return ())) rq where dummy = const $ return () testFail :: Test testFail = testCase "failure" $ expect404 (go pass) setFoo :: ByteString -> Snap () setFoo s = do modifyResponse (addHeader "Foo" s) fmap id $ pure () pure id <*> (liftIO $ return ()) testAlternative :: Test testAlternative = testCase "types/alternative" $ do (_,resp) <- go (pass <|> setFoo "Bar") assertEqual "foo present" (Just "Bar") $ getHeader "Foo" resp (_,resp2) <- go (fail "" <|> fail2 <|> setFoo "Bar" <|> setFoo "Baz") assertEqual "alternative chooses correct branch" (Just ["Bar"]) $ getHeaders "Foo" resp2 where fail2 :: Snap () fail2 = pass >>= \_ -> return () sampleResponse :: Response sampleResponse = addHeader "Foo" "Quux" $ emptyResponse testEarlyTermination :: Test testEarlyTermination = testCase "types/earlyTermination" $ do (_,resp) <- go (finishWith sampleResponse >>= \_ -> setFoo "Bar") assertEqual "foo" (Just ["Quux"]) $ getHeaders "Foo" resp testEscapeHttp :: Test testEscapeHttp = testCase "types/escapeHttp" $ flip catch catchEscape $ do (_, _) <- go (escapeHttp escaper) assertFailure "HTTP escape was ignored" where escaper _ _ = liftIO $ assert True tickle _ = return () catchEscape (EscapeHttpException iter) = run_ $ iter tickle (return ()) isLeft :: Either a b -> Bool isLeft (Left _) = True isLeft _ = False isRight :: Either a b -> Bool isRight (Right _) = True isRight _ = False testBracketSnap :: Test testBracketSnap = testCase "types/bracketSnap" $ do rq <- mkZomgRq ref <- newIORef 0 expectSpecificException (NoHandlerException "") $ run_ $ evalSnap (act ref) (const $ return ()) (const $ return ()) rq y <- readIORef ref assertEqual "bracketSnap/after1" (1::Int) y expectSpecificException (ErrorCall "no value") $ run_ $ evalSnap (act ref <|> finishWith emptyResponse) (const $ return ()) (const $ return ()) rq y' <- readIORef ref assertEqual "bracketSnap/after" 2 y' expectSpecificException (ErrorCall "foo") $ run_ $ evalSnap (act2 ref) (const $ return ()) (const $ return ()) rq y'' <- readIORef ref assertEqual "bracketSnap/after" 3 y'' where act ref = bracketSnap (liftIO $ readIORef ref) (\z -> liftIO $ writeIORef ref $! z+1) (\z -> z `seq` mzero) act2 ref = bracketSnap (liftIO $ readIORef ref) (\z -> liftIO $ writeIORef ref $! z+1) (\z -> z `seq` liftIO $ throwIO $ ErrorCall "foo") testCatchFinishWith :: Test testCatchFinishWith = testCase "types/catchFinishWith" $ do rq <- mkZomgRq x <- run_ $ evalSnap (catchFinishWith $ finishWith emptyResponse) (const $ return ()) (const $ return ()) rq assertBool "catchFinishWith" $ isLeft x y <- run_ $ evalSnap (catchFinishWith $ return ()) (const $ return ()) (const $ return ()) rq assertBool "catchFinishWith" $ isRight y testRqBody :: Test testRqBody = testCase "types/requestBodies" $ do mvar1 <- newEmptyMVar mvar2 <- newEmptyMVar _ <- goBody $ f mvar1 mvar2 v1 <- takeMVar mvar1 v2 <- takeMVar mvar2 assertEqual "rq body" "zazzle" v1 assertEqual "rq body 2" "" v2 (_,rsp) <- goBody g bd <- getBody rsp assertEqual "detached rq body" "zazzle" bd where f mvar1 mvar2 = do getRequestBody >>= liftIO . putMVar mvar1 getRequestBody >>= liftIO . putMVar mvar2 g = transformRequestBody returnI testRqBodyTooLong :: Test testRqBodyTooLong = testCase "types/requestBodyTooLong" $ do expectExceptionH $ goBody $ f 2 (_, rsp) <- goBody $ f 200000 bd <- getBody rsp assertEqual "detached rq body" "zazzle" bd where f sz = readRequestBody sz >>= writeLBS testRqBodyException :: Test testRqBodyException = testCase "types/requestBodyException" $ do (req,resp) <- goEnum (enumList 1 ["the", "quick", "brown", "fox"]) hndlr bd <- getBody resp (SomeEnumerator e) <- readIORef $ rqBody req b' <- liftM (S.concat) $ run_ $ e $$ consume assertEqual "request body was consumed" "" b' assertEqual "response body was produced" "OK" bd where h0 = runRequestBody $ do _ <- I.head throw $ ErrorCall "foo" hndlr = h0 `catch` \(_::SomeException) -> writeBS "OK" testRqBodyTermination :: Test testRqBodyTermination = testCase "types/requestBodyTermination" $ expectExceptionH $ goEnum (enumList 1 ["the", "quick", "brown", "fox"]) hndlr where h0 = runRequestBody $ do _ <- I.head terminateConnection $ ErrorCall "foo" hndlr = h0 `catch` \(_::SomeException) -> writeBS "OK" testTrivials :: Test testTrivials = testCase "types/trivials" $ do (rq,rsp) <- go $ do req <- getRequest putRequest $ req { rqIsSecure=True } putResponse $ setResponseStatus 333 "333" sampleResponse r <- getResponse liftIO $ assertEqual "rsp status" 333 $ rspStatus r !_ <- localRequest (\x -> x {rqIsSecure=False}) $ do q <- getRequest liftIO $ assertEqual "localrq" False $ rqIsSecure q return () logError "foo" writeText "zzz" writeLazyText "zzz" let req' = updateContextPath 0 req let cp1 = rqContextPath req let cp2 = rqContextPath req' liftIO $ assertEqual "updateContextPath 0" cp1 cp2 withRequest $ return . (`seq` ()) withResponse $ return . (`seq` ()) return () b <- getBody rsp coverShowInstance b coverShowInstance $ NoHandlerException "" coverShowInstance GET coverReadInstance GET coverEqInstance GET coverEqInstance $ NoHandlerException "" coverOrdInstance GET Prelude.map (\(x,y) -> (x,show y)) (IM.toList statusReasonMap) `deepseq` return () let cookie = Cookie "" "" Nothing Nothing Nothing False False coverEqInstance cookie coverShowInstance cookie assertEqual "rq secure" True $ rqIsSecure rq assertEqual "rsp status" 333 $ rspStatus rsp testMethod :: Test testMethod = testCase "types/method" $ do expect404 $ go (method POST $ return ()) expectNo404 $ go (method GET $ return ()) testMethods :: Test testMethods = testCase "types/methods" $ do expect404 $ go (methods [POST,PUT,PATCH,Method "MOVE"] $ return ()) expectNo404 $ go (methods [GET] $ return ()) expectNo404 $ go (methods [POST,GET] $ return ()) expectNo404 $ go (methods [PUT,GET] $ return ()) expectNo404 $ go (methods [GET,PUT,DELETE] $ return ()) expectNo404 $ go (methods [GET,PUT,DELETE,PATCH] $ return ()) expectNo404 $ go (methods [GET,Method "COPY"] $ return ()) expect404 $ goMeth PATCH (methods [POST,PUT,GET,Method "FOO"] $ return ()) expect404 $ goMeth (Method "Baz") (methods [GET,POST,Method "Foo"] $ return ()) expectNo404 $ goMeth (Method "Baz") (method (Method "Baz") $ return ()) expectNo404 $ goMeth (Method "Foo") (methods [Method "Baz",PATCH,GET,Method "Foo"] $ return ()) expectNo404 $ goMeth GET (method (Method "GET") $ return ()) expectNo404 $ goMeth (Method "GET") (method GET $ return ()) methodGen :: Int -> Gen Method methodGen n = variant n $ oneof [ elements [ GET, HEAD, POST, PUT, DELETE , TRACE, OPTIONS, CONNECT, PATCH ] , Method <$> arbitrary ] testMethodEq :: Test testMethodEq = testProperty "types/Method/eq" $ prop where prop n = do m <- methodGen n return $ m == m && toMeth m == m toMeth GET = Method "GET" toMeth HEAD = Method "HEAD" toMeth POST = Method "POST" toMeth PUT = Method "PUT" toMeth DELETE = Method "DELETE" toMeth TRACE = Method "TRACE" toMeth OPTIONS = Method "OPTIONS" toMeth CONNECT = Method "CONNECT" toMeth PATCH = Method "PATCH" toMeth (Method a) = Method a testMethodNotEq :: Test testMethodNotEq = testProperty "types/Method/noteq" $ prop where prop n = do m <- methodGen n m' <- methodGen (n + 1) return $ (m /= m') == not (m == m') testDir :: Test testDir = testCase "types/dir" $ do expect404 $ goPath "foo/bar" (dir "zzz" $ return ()) expectNo404 $ goPath "foo/bar" (dir "foo" $ return ()) expect404 $ goPath "fooz/bar" (dir "foo" $ return ()) expectNo404 $ goPath "foo/bar" (path "foo/bar" $ return ()) expect404 $ goPath "foo/bar/z" (path "foo/bar" $ return ()) expectNo404 $ goPath "" (ifTop $ return ()) expect404 $ goPath "a" (ifTop $ return ()) testParam :: Test testParam = testCase "types/getParam" $ do expect404 $ goPath "/foo" f expectNo404 $ goPathQuery "/foo" "param" ["foo"] f expectNo404 $ goPathQuery "/foo" "param" ["foo"] fQ expect404 $ goPathQuery "/foo" "param" ["foo"] fP where p gp = do mp <- gp "param" maybe pass (\s -> if s == "foo" then return () else pass) mp f = p getParam fQ = p getQueryParam fP = p getPostParam getBody :: Response -> IO L.ByteString getBody r = do let benum = rspBodyToEnum $ rspBody r liftM (toLazyByteString . mconcat) (runIteratee consume >>= run_ . benum) testWrites :: Test testWrites = testCase "types/writes" $ do (_,r) <- go h b <- getBody r assertEqual "output functions" "Foo1Foo2Foo3" b where h :: Snap () h = do addToOutput $ enumBuilder $ fromByteString "Foo1" writeBS "Foo2" writeLBS "Foo3" testURLEncode1 :: Test testURLEncode1 = testCase "types/urlEncoding1" $ do let b = urlEncode "the quick brown fox~#" assertEqual "url encoding 1" "the+quick+brown+fox%7e%23" b assertEqual "fail" Nothing $ urlDecode "%" testURLEncode2 :: Test testURLEncode2 = testProperty "types/urlEncoding2" prop where prop s = (urlDecode $ urlEncode s) == Just s testDir2 :: Test testDir2 = testCase "types/dir2" $ do (_,resp) <- goPath "foo/bar" f b <- getBody resp assertEqual "context path" "/foo/bar/" b where f = dir "foo" $ dir "bar" $ do p <- liftM rqContextPath getRequest addToOutput $ enumBuilder $ fromByteString p testIpHeaderFilter :: Test testIpHeaderFilter = testCase "types/ipHeaderFilter" $ do (_,r) <- goIP f b <- getBody r assertEqual "ipHeaderFilter" "1.2.3.4" b (_,r2) <- go f b2 <- getBody r2 assertEqual "ipHeaderFilter" "127.0.0.1" b2 where f = do ipHeaderFilter ip <- liftM rqRemoteAddr getRequest writeBS ip testMZero404 :: Test testMZero404 = testCase "types/mzero404" $ do (_,r) <- go mzero b <- getBody r assertBool "mzero 404" ("> return ()) where f = do logError "zzz" v <- withResponse (return . rspHttpVersion) liftIO $ assertEqual "evalSnap rsp version" (1,1) v finishWith emptyResponse testLocalRequest :: Test testLocalRequest = testCase "types/localRequest" $ do rq1 <- mkZomgRq rq2 <- mkRequest "zzz/zz/z" let h = localRequest (const rq2) mzero (rq',_) <- go (h <|> return ()) let u1 = rqURI rq1 let u2 = rqURI rq' assertEqual "localRequest backtrack" u1 u2 testRedirect :: Test testRedirect = testCase "types/redirect" $ do (_,rsp) <- go (redirect "/foo/bar") b <- getBody rsp assertEqual "no response body" "" b assertEqual "response content length" (Just 0) $ rspContentLength rsp assertEqual "redirect path" (Just "/foo/bar") $ getHeader "Location" rsp assertEqual "redirect status" 302 $ rspStatus rsp assertEqual "status description" "Found" $ rspStatusReason rsp (_,rsp2) <- go (redirect' "/bar/foo" 307) assertEqual "redirect path" (Just "/bar/foo") $ getHeader "Location" rsp2 assertEqual "redirect status" 307 $ rspStatus rsp2 assertEqual "status description" "Temporary Redirect" $ rspStatusReason rsp2 testPathArgs :: Test testPathArgs = testCase "types/pathArgs" $ do (_, rsp) <- goPath "%e4%b8%ad" m b <- getBody rsp assertEqual "pathargs url- and utf8-decodes" "ok" b where m = pathArg f f x = if x == ("\x4e2d" :: Text) then writeBS "ok" else writeBS $ "not ok: " `mappend` T.encodeUtf8 x snap-core-0.9.8.0/test/suite/Snap/Internal/0000755000000000000000000000000012565252520016534 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Internal/Http/0000755000000000000000000000000012565252520017453 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Internal/Http/Types/0000755000000000000000000000000012565252520020557 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Internal/Http/Types/Tests.hs0000644000000000000000000001177212565252520022225 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} module Snap.Internal.Http.Types.Tests ( tests ) where import Blaze.ByteString.Builder import Control.Monad import Control.Parallel.Strategies import Data.ByteString.Char8 (ByteString) import Data.ByteString.Lazy.Char8 () import Data.IORef import qualified Data.Map as Map import Data.Monoid import Data.Time.Calendar import Data.Time.Clock import Prelude hiding (take) import Test.Framework import Test.Framework.Providers.HUnit import Test.HUnit hiding (Test, path) import Text.Regex.Posix import Snap.Internal.Http.Types import Snap.Internal.Parsing import Snap.Iteratee import qualified Snap.Types.Headers as H tests :: [Test] tests = [ testTypes , testCookies , testUrlDecode , testFormatLogTime , testAddHeader ] mkRq :: IO Request mkRq = do enum <- newIORef (SomeEnumerator $ enumBS "") return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False H.empty enum Nothing GET (1,1) [] "/" "/" "/" "" Map.empty Map.empty Map.empty testFormatLogTime :: Test testFormatLogTime = testCase "formatLogTime" $ do b <- formatLogTime 3804938 let re = ("^[0-9]{1,2}/[A-Za-z]{3}/[0-9]{4}:[0-9]{2}:[0-9]{2}:[0-9]{2} (-|\\+)[0-9]{4}$" :: ByteString) assertBool "formatLogTime" $ b =~ re testAddHeader :: Test testAddHeader = testCase "addHeader" $ do defReq <- mkRq let req = addHeader "foo" "bar" $ addHeader "foo" "baz" defReq let x = getHeader "foo" req assertEqual "addHeader x 2" (Just "baz,bar") x testUrlDecode :: Test testUrlDecode = testCase "urlDecode" $ do assertEqual "bad hex" Nothing $ urlDecode "%qq" testTypes :: Test testTypes = testCase "show" $ do defReq <- mkRq let req = rqModifyParams (Map.insert "zzz" ["bbb"]) $ updateHeaders (H.set "zzz" "bbb") $ rqSetParam "foo" ["bar"] $ defReq let req2 = (addHeader "zomg" "1234" req) { rqCookies = [ cook, cook2 ] } let !a = show req `using` rdeepseq let !_ = show req2 `using` rdeepseq -- we don't care about the show instance really, we're just trying to shut -- up hpc assertBool "show" $ a /= b assertEqual "rqParam" (Just ["bar"]) (rqParam "foo" req) assertEqual "lookup" (Just ["bbb"]) (Map.lookup "zzz" $ rqParams req) assertEqual "lookup 2" (Just ["bbb"]) (H.lookup "zzz" $ headers req) assertEqual "response status" 555 $ rspStatus resp assertEqual "response status reason" "bogus" $ rspStatusReason resp assertEqual "content-length" (Just 4) $ rspContentLength resp -- run response body let benum = rspBodyToEnum $ rspBody resp bd <- liftM (toByteString . mconcat) (runIteratee consume >>= run_ . benum) assertEqual "response body" "PING" $ bd let !_ = show GET let !_ = GET == POST let !_ = headers $ headers defReq let !_ = show resp2 `using` rdeepseq return () where resp = addResponseCookie cook $ setContentLength 4 $ modifyResponseBody id $ setResponseBody (enumBuilder (fromByteString "PING")) $ setContentType "text/plain" $ setResponseStatus 555 "bogus" $ emptyResponse !b = show resp `using` rdeepseq resp2 = addResponseCookie cook2 resp utc = UTCTime (ModifiedJulianDay 55226) 0 cook = Cookie "foo" "bar" (Just utc) (Just ".foo.com") (Just "/") False False cook2 = Cookie "zoo" "baz" (Just utc) (Just ".foo.com") (Just "/") False False testCookies :: Test testCookies = testCase "cookies" $ do assertEqual "cookie" (Just cook) rCook assertEqual "cookie2" (Just cook2) rCook2 assertEqual "cookie3" (Just cook3) rCook3 assertEqual "empty response cookie3" (Just cook3) rCook3e assertEqual "removed cookie" Nothing nilCook assertEqual "multiple cookies" [cook, cook2] cks assertEqual "cookie modification" (Just cook3) rCook3Mod return () where resp = addResponseCookie cook $ setContentType "text/plain" $ emptyResponse f _ = cook3 resp' = deleteResponseCookie "foo" resp resp'' = modifyResponseCookie "foo" f resp resp2 = addResponseCookie cook2 resp resp3 = addResponseCookie cook3 resp2 resp4 = addResponseCookie cook3 emptyResponse utc = UTCTime (ModifiedJulianDay 55226) 0 cook = Cookie "foo" "bar" (Just utc) (Just ".foo.com") (Just "/") False True cook2 = Cookie "zoo" "baz" (Just utc) (Just ".foo.com") (Just "/") True False cook3 = Cookie "boo" "baz" Nothing Nothing Nothing False False rCook = getResponseCookie "foo" resp nilCook = getResponseCookie "foo" resp' rCook2 = getResponseCookie "zoo" resp2 rCook3 = getResponseCookie "boo" resp3 rCook3e = getResponseCookie "boo" resp4 rCook3Mod = getResponseCookie "boo" resp'' cks = getResponseCookies resp2 snap-core-0.9.8.0/test/suite/Snap/Internal/Parsing/0000755000000000000000000000000012565252520020137 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Internal/Parsing/Tests.hs0000644000000000000000000000130412565252520021573 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} module Snap.Internal.Parsing.Tests ( tests ) where import qualified Data.ByteString as S import Test.Framework import Test.Framework.Providers.HUnit import Test.HUnit hiding (Test, path) import Snap.Internal.Http.Types import Snap.Internal.Parsing tests :: [Test] tests = [ testCookie ] testCookie :: Test testCookie = testCase "parsing/parseCookie" $ do assertEqual "cookie parsing" (Just [cv]) cv2 where cv = Cookie nm v Nothing Nothing Nothing False False cv2 = parseCookie ct nm = "foo" v = "bar" ct = S.concat [ nm , "=" , v ] snap-core-0.9.8.0/test/suite/Snap/Internal/Routing/0000755000000000000000000000000012565252520020163 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Internal/Routing/Tests.hs0000644000000000000000000003126112565252520021624 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Snap.Internal.Routing.Tests ( tests ) where ------------------------------------------------------------------------------ import Control.Applicative ((<|>)) import Control.Monad import Data.ByteString (ByteString) import qualified Data.ByteString as S import qualified Data.Map as Map import Data.Maybe import Test.Framework import Test.Framework.Providers.HUnit import Test.HUnit hiding (Test, path) ------------------------------------------------------------------------------ import Snap.Internal.Http.Types import Snap.Internal.Routing import Snap.Internal.Types import Snap.Test import Snap.Test.Common ------------------------------------------------------------------------------ tests :: [Test] tests = [ testRouting1 , testRouting2 , testRouting3 , testRouting4 , testRouting5 , testRouting6 , testRouting7 , testRouting8 , testRouting9 , testRouting10 , testRouting11 , testRouting12 , testRouting13 , testRouting14 , testRouting15 , testRouting16 , testRouting17 , testRouting18 , testRouting19 , testRouting20 , testRouting21 , testRouting22 , testRouting23 , testRouting24 , testRouting25 , testRouting26 , testRouting27 , testRouting28 , testRouteLocal , testRouteUrlDecode , testRouteUrlEncodedPath , testRouteEmptyCapture ] ------------------------------------------------------------------------------ go :: Snap a -> ByteString -> IO a go m s0 = evalHandler (get s Map.empty) m where s = if S.isPrefixOf "/" s0 then s0 else S.append "/" s0 ------------------------------------------------------------------------------ routes :: Snap ByteString routes = route [ ("foo" , topFoo ) , ("foo/bar" , fooBar ) , ("foo/bar/baz" , getRqPathInfo ) , ("foo/:id" , fooCapture ) , ("bar/:id" , fooCapture ) , ("herp/:derp/" , getRqPathInfo ) , ("nerp/:derp/" , getRqContextPath) , ("a b c d" , return "OK" ) , ("bar/quux" , barQuux ) , ("bar" , bar ) , ("z/:a/:b/:c/d" , zabc ) ] ------------------------------------------------------------------------------ routesLocal :: Snap ByteString routesLocal = routeLocal [ ("foo/bar/baz" , getRqPathInfo ) , ("bar" , pass ) ] ------------------------------------------------------------------------------ routes2 :: Snap ByteString routes2 = route [ ("" , topTop ) , ("foo" , topFoo ) ] ------------------------------------------------------------------------------ routes3 :: Snap ByteString routes3 = route [ (":foo" , topCapture ) , ("" , topTop ) ] ------------------------------------------------------------------------------ routes4 :: Snap ByteString routes4 = route [ (":foo" , pass ) , (":foo" , topCapture ) , (":qqq/:id" , fooCapture ) , (":id2/baz" , fooCapture2 ) ] ------------------------------------------------------------------------------ routes5 :: Snap ByteString routes5 = route [ ("" , pass ) , ("" , topTop ) ] ------------------------------------------------------------------------------ routes6 :: Snap ByteString routes6 = route [ (":a/:a" , dblA ) ] ------------------------------------------------------------------------------ routes7 :: Snap ByteString routes7 = route [ ("foo/:id" , fooCapture ) , ("foo/:id/:id2" , fooCapture2) , ("fooo/:id/:id2" , fooCapture2) , ("foooo/bar/baz" , bar ) , ("" , topTop ) ] ------------------------------------------------------------------------------ routesEmptyCapture :: Snap ByteString routesEmptyCapture = route [ ("foo/:id", fooCapture) ] ------------------------------------------------------------------------------ topTop, topFoo, fooBar, fooCapture, getRqPathInfo, bar, getRqContextPath, barQuux, dblA, zabc, topCapture, fooCapture2 :: Snap ByteString dblA = do ma <- getParam "a" unless (ma == Just "a a") pass return "ok" zabc = do ma <- getParam "a" mb <- getParam "b" mc <- getParam "c" unless ( ma == Just "a" && mb == Just "b" && mc == Just "c" ) pass return "ok" topCapture = do mp <- getParam "foo" maybe pass return mp topTop = return "topTop" topFoo = return "topFoo" fooBar = return "fooBar" fooCapture = liftM (head . fromJust . rqParam "id") getRequest fooCapture2 = liftM (head . fromJust . rqParam "id2") getRequest getRqPathInfo = liftM rqPathInfo getRequest getRqContextPath = liftM rqContextPath getRequest barQuux = return "barQuux" bar = return "bar" ----------- -- Tests -- ----------- ------------------------------------------------------------------------------ -- TODO more useful test names testRouting1 :: Test testRouting1 = testCase "route/1" $ do r1 <- go routes "foo" assertEqual "/foo" "topFoo" r1 ------------------------------------------------------------------------------ testRouting2 :: Test testRouting2 = testCase "route/2" $ do r2 <- go routes "foo/baz" assertEqual "/foo/baz" "baz" r2 ------------------------------------------------------------------------------ testRouting3 :: Test testRouting3 = testCase "route/3" $ do expectExceptionH $ go routes "/xsaxsaxsax" ------------------------------------------------------------------------------ testRouting4 :: Test testRouting4 = testCase "route/4" $ do r3 <- go routes "foo/bar" assertEqual "/foo/bar" "fooBar" r3 ------------------------------------------------------------------------------ testRouting5 :: Test testRouting5 = testCase "route/5" $ do r4 <- go routes "foo/bar/baz/quux" assertEqual "/foo/bar/baz/quux" "quux" r4 ------------------------------------------------------------------------------ testRouting6 :: Test testRouting6 = testCase "route/6" $ do r5 <- go routes "foo/bar/sproing" assertEqual "/foo/bar/sproing" "fooBar" r5 ------------------------------------------------------------------------------ testRouting7 :: Test testRouting7 = testCase "route/7" $ do r <- go routes "bar" assertEqual "/bar" "bar" r ------------------------------------------------------------------------------ testRouting8 :: Test testRouting8 = testCase "route/8" $ do r2 <- go routes "bar/quux" assertEqual "/bar/quux" "barQuux" r2 ------------------------------------------------------------------------------ testRouting9 :: Test testRouting9 = testCase "route/9" $ do r3 <- go routes "bar/whatever" assertEqual "/bar/whatever" "whatever" r3 ------------------------------------------------------------------------------ testRouting10 :: Test testRouting10 = testCase "route/10" $ do r4 <- go routes "bar/quux/whatever" assertEqual "/bar/quux/whatever" "barQuux" r4 ------------------------------------------------------------------------------ testRouting11 :: Test testRouting11 = testCase "route/11" $ do r1 <- go routes2 "" assertEqual "/" "topTop" r1 ------------------------------------------------------------------------------ testRouting12 :: Test testRouting12 = testCase "route/12" $ do r1 <- go routes2 "foo" assertEqual "/foo" "topFoo" r1 ------------------------------------------------------------------------------ testRouting13 :: Test testRouting13 = testCase "route/13" $ do r1 <- go routes3 "zzzz" assertEqual "/zzzz" "zzzz" r1 ------------------------------------------------------------------------------ testRouting14 :: Test testRouting14 = testCase "route/14" $ do r1 <- go routes3 "" assertEqual "/" "topTop" r1 ------------------------------------------------------------------------------ testRouting15 :: Test testRouting15 = testCase "route/15" $ do r1 <- go routes4 "zzzz" assertEqual "/zzzz" "zzzz" r1 ------------------------------------------------------------------------------ testRouting16 :: Test testRouting16 = testCase "route/16" $ do r1 <- go routes5 "" assertEqual "/" "topTop" r1 ------------------------------------------------------------------------------ testRouting17 :: Test testRouting17 = testCase "route/17" $ do r1 <- go routes "z/a/b/c/d" assertEqual "/z/a/b/c/d" "ok" r1 ------------------------------------------------------------------------------ testRouting18 :: Test testRouting18 = testCase "route/18" $ do r1 <- go routes6 "a/a" assertEqual "/a/a" "ok" r1 ------------------------------------------------------------------------------ testRouting19 :: Test testRouting19 = testCase "route/19" $ do r1 <- go routes7 "foo" assertEqual "/foo" "topTop" r1 ------------------------------------------------------------------------------ testRouting20 :: Test testRouting20 = testCase "route/20" $ do r1 <- go routes7 "foo/baz" assertEqual "/foo/baz" "baz" r1 ------------------------------------------------------------------------------ testRouting21 :: Test testRouting21 = testCase "route/21" $ do r1 <- go routes7 "foo/baz/quux" assertEqual "/foo/baz/quux" "quux" r1 ------------------------------------------------------------------------------ testRouting22 :: Test testRouting22 = testCase "route/22" $ do r1 <- go routes7 "fooo/baz" assertEqual "/fooo/baz" "topTop" r1 ------------------------------------------------------------------------------ testRouting23 :: Test testRouting23 = testCase "route/23" $ do r1 <- go routes7 "fooo/baz/quux" assertEqual "/fooo/baz/quux" "quux" r1 ------------------------------------------------------------------------------ testRouting24 :: Test testRouting24 = testCase "route/24" $ do r1 <- go routes7 "foooo/bar/bax" assertEqual "/foooo/bar/bax" "topTop" r1 ------------------------------------------------------------------------------ testRouting25 :: Test testRouting25 = testCase "route/25" $ do r1 <- go routes7 "foooo/bar/baz" assertEqual "/foooo/bar/baz" "bar" r1 ------------------------------------------------------------------------------ testRouting26 :: Test testRouting26 = testCase "route/26" $ do r1 <- go routes4 "foo/bar" assertEqual "capture union" "bar" r1 ------------------------------------------------------------------------------ testRouting27 :: Test testRouting27 = testCase "route/27" $ do r1 <- go routes4 "foo" assertEqual "capture union" "foo" r1 ------------------------------------------------------------------------------ testRouting28 :: Test testRouting28 = testCase "route/28" $ do r1 <- go routes4 "quux/baz" assertEqual "capture union" "quux" r1 ------------------------------------------------------------------------------ testRouteUrlDecode :: Test testRouteUrlDecode = testCase "route/urlDecode" $ do r1 <- go routes "herp/%7Bderp%7D/" assertEqual "rqPathInfo on urldecode" "" r1 r2 <- go routes "foo/%7Bderp%7D/" assertEqual "urldecoded capture" "{derp}" r2 r3 <- go routes "nerp/%7Bderp%7D/" assertEqual "rqContextPath on urldecode" "/nerp/%7Bderp%7D/" r3 ------------------------------------------------------------------------------ testRouteUrlEncodedPath :: Test testRouteUrlEncodedPath = testCase "route/urlEncodedPath" $ do -- make sure path search urlDecodes. r1 <- go routes "a+b+c+d" assertEqual "urlEncoded search works" "OK" r1 ------------------------------------------------------------------------------ testRouteLocal :: Test testRouteLocal = testCase "route/routeLocal" $ do r4 <- go routesLocal "foo/bar/baz/quux" assertEqual "/foo/bar/baz/quux" "foo/bar/baz/quux" r4 expectExceptionH $ go routesLocal "bar" ------------------------------------------------------------------------------ testRouteEmptyCapture :: Test testRouteEmptyCapture = testCase "route/emptyCapture" $ do r <- go m "foo" assertEqual "empty capture must fail" expected r r2 <- go m "foo/" assertEqual "empty capture must fail" expected r2 where expected = "ZOMG_OK" m = routesEmptyCapture <|> return expected snap-core-0.9.8.0/test/suite/Snap/Iteratee/0000755000000000000000000000000012565252520016522 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Iteratee/Tests.hs0000644000000000000000000003731312565252520020167 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Snap.Iteratee.Tests ( tests ) where import Control.Concurrent (threadDelay) import qualified Control.Exception as E import Control.Exception hiding (try, assert, throw, catch) import Control.Monad import Control.Monad.CatchIO import Control.Monad.Identity import Control.Monad.Trans import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as S import qualified Data.ByteString.Lazy.Char8 as L import Data.Int import Data.Maybe import Prelude hiding (catch, head, drop, take) import System.Timeout import Test.Framework import Test.Framework.Providers.QuickCheck2 import Test.QuickCheck import Test.QuickCheck.Gen import qualified Test.QuickCheck.Monadic as QC import Test.QuickCheck.Monadic hiding (run) import Test.Framework.Providers.HUnit import qualified Test.HUnit as H import Snap.Iteratee import Snap.Internal.Iteratee.BoyerMooreHorspool import Snap.Test.Common import Snap.Internal.Iteratee.Debug throwErr :: String -> Iteratee a IO b throwErr = throwError . AssertionFailed tests :: [Test] tests = [ testEnumBS , testEnumLBS , testUnsafeBuffer , testUnsafeBuffer2 , testUnsafeBuffer3 , testUnsafeBuffer4 , testUnsafeBuffer5 , testTakeExactly1 , testTakeExactly2 , testTakeExactly3 , testTakeNoMoreThan1 , testTakeNoMoreThan2 , testTakeNoMoreThan3 , testCountBytes , testCountBytes2 , testKillIfTooSlow1 , testKillIfTooSlow2 , testBMH , testBMHTrivials , testCatchIO ] testEnumBS :: Test testEnumBS = testProperty "iteratee/enumBS" prop where prop :: S.ByteString -> Bool prop s = (S.concat $ runIdentity (run_ iter)) == s where iter = runIdentity $ liftM (enumBS s) $ runIteratee consume testEnumLBS :: Test testEnumLBS = testProperty "iteratee/enumLBS" prop where prop :: L.ByteString -> Bool prop s = L.fromChunks (runIdentity (run_ iter)) == s where iter = runIdentity $ liftM (enumLBS s) $ runIteratee consume copyingConsume :: Iteratee ByteString IO L.ByteString copyingConsume = f [] where f l = do mbX <- head maybe (return $ L.fromChunks $ reverse l) (\x -> let !z = S.copy x in z `seq` f (z : l)) mbX bufferAndRun :: Iteratee ByteString IO a -> L.ByteString -> IO a bufferAndRun ii s = do i <- unsafeBufferIteratee ii >>= runIteratee run_ $ enumLBS s i testUnsafeBuffer :: Test testUnsafeBuffer = testProperty "iteratee/testUnsafeBuffer" $ monadicIO $ forAllM arbitrary prop where prop s = do pre $ s /= L.empty x <- liftQ $ bufferAndRun copyingConsume s' assert $ x == s' where s' = L.take 20000 $ L.cycle s testUnsafeBuffer2 :: Test testUnsafeBuffer2 = testCase "iteratee/testUnsafeBuffer2" prop where prop = do i <- unsafeBufferIteratee (drop 4 >> copyingConsume) >>= runIteratee s <- run_ $ enumLBS "abcdefgh" i H.assertEqual "s == 'efgh'" "efgh" s testUnsafeBuffer3 :: Test testUnsafeBuffer3 = testProperty "iteratee/testUnsafeBuffer3" $ monadicIO $ forAllM arbitrary prop where prop s = do pre $ s /= L.empty ss <- liftQ $ runIteratee copyingConsume >>= return . joinI . take 19999 x <- liftQ $ bufferAndRun (ss >>= \x -> drop 1 >> return x) s' let y = L.take 19999 s' if x /= y then liftQ $ do putStrLn $ "FAILED!!!!!" putStrLn $ "length x = " ++ show (L.length x) putStrLn $ "length y = " ++ show (L.length y) diff x y putStrLn $ "input was " ++ show s else return () assert $ x == y where s' = L.take 20000 $ L.cycle s diff a b = d a b (0::Int) d a b n = do case ma of Nothing -> if mb /= Nothing then do let Just (y,_) = mb putStrLn $ "differ at byte " ++ show n putStrLn $ "a=Nothing, b=" ++ show y else return () Just (x,rest1) -> if isNothing mb then do putStrLn $ "differ at byte " ++ show n putStrLn $ "a=" ++ show x ++ ", b=Nothing" else do let Just (y,rest2) = mb if x /= y then do putStrLn $ "differ at byte " ++ show n putStrLn $ "a=" ++ show x ++ ", b=" ++ show y let r1 = L.take 6 rest1 let r2 = L.take 6 rest2 putStrLn $ "next few bytes a = " ++ show r1 putStrLn $ "next few bytes b = " ++ show r2 else d rest1 rest2 (n+1) where ma = L.uncons a mb = L.uncons b tub3Prop s = do ss <- runIteratee copyingConsume >>= return . joinI . take 19999 let it = (ss >>= \x -> drop 1 >> return x) x <- bufferAndRun (iterateeDebugWrapper "foo" it) s' let a = x let b = L.take 19999 s' let boo = (a == b) putStrLn $ "equal? " ++ show boo diff a b where diff a b = d a b (0::Int) d a b n = do case ma of Nothing -> if mb /= Nothing then do let Just (y,_) = mb putStrLn $ "differ at byte " ++ show n putStrLn $ "a=Nothing, b=" ++ show y else return () Just (x,rest1) -> if isNothing mb then do putStrLn $ "differ at byte " ++ show n putStrLn $ "a=" ++ show x ++ ", b=Nothing" else do let Just (y,rest2) = mb if x /= y then do putStrLn $ "differ at byte " ++ show n putStrLn $ "a=" ++ show x ++ ", b=" ++ show y else d rest1 rest2 (n+1) where ma = L.uncons a mb = L.uncons b s' = L.take 20000 $ L.cycle s testUnsafeBuffer4 :: Test testUnsafeBuffer4 = testProperty "iteratee/testUnsafeBuffer4" $ monadicIO $ forAllM arbitrary prop where prop s = do i <- liftQ (unsafeBufferIteratee (copyingConsume >> throwErr "foo") >>= runIteratee) expectException $ run_ $ enumLBS s i j <- liftQ $ unsafeBufferIteratee (throwErr "foo" >> copyingConsume) >>= runIteratee expectException $ run_ $ enumLBS s j testUnsafeBuffer5 :: Test testUnsafeBuffer5 = testProperty "iteratee/testUnsafeBuffer5" $ monadicIO $ forAllM arbitrary prop where prop s = do pre $ s /= L.empty x <- liftQ $ bufferAndRun copyingConsume s assert $ x == s testTakeExactly1 :: Test testTakeExactly1 = testProperty "iteratee/takeExactly1" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do expectException $ doIter >>= run_ where doIter = runIteratee consume >>= runIteratee . joinI . takeExactly (n+1) >>= return . enumLBS s n = fromIntegral $ L.length s testTakeExactly2 :: Test testTakeExactly2 = testProperty "iteratee/takeExactly2" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do e <- liftQ $ doIter >>= run_ assert $ L.fromChunks e == s where doIter = runIteratee consume >>= runIteratee . joinI . takeExactly n >>= return . enumLBS s n = fromIntegral $ L.length s testTakeExactly3 :: Test testTakeExactly3 = testProperty "iteratee/takeExactly3" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do e <- liftQ $ doIter >>= run_ assert $ L.fromChunks e == L.take (fromIntegral n) s where doIter = runIteratee consume >>= runIteratee . joinI . takeExactly n >>= return . enumLBS s n = fromIntegral $ L.length s testTakeNoMoreThan1 :: Test testTakeNoMoreThan1 = testProperty "iteratee/takeNoMoreThan1" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do s' <- liftQ $ doIter >>= run_ assert $ s == L.fromChunks s' where doIter = do step <- runIteratee consume >>= runIteratee . joinI . takeNoMoreThan (n+1) return $ enumLBS s step n = fromIntegral $ L.length s testTakeNoMoreThan2 :: Test testTakeNoMoreThan2 = testProperty "iteratee/takeNoMoreThan2" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do e <- liftQ $ doIter >>= run_ assert $ L.fromChunks e == s where doIter = do step <- runIteratee consume step' <- runIteratee (joinI (takeNoMoreThan n step)) return $ enumLBS (L.concat ["", s]) step' n = fromIntegral $ L.length s testTakeNoMoreThan3 :: Test testTakeNoMoreThan3 = testProperty "iteratee/takeNoMoreThan3" $ monadicIO $ forAllM arbitrary prop where prop :: (Int64,L.ByteString) -> PropertyM IO () prop (m,s) = do step1 <- liftQ $ runIteratee consume step2 <- liftQ $ runIteratee $ joinI (takeNoMoreThan 0 step1) v <- liftQ $ run_ $ enumLBS "" step2 assert $ S.concat v == "" if (L.null s || m == 0) then liftQ $ do !_ <- doIter >>= run_ return () else expectException $ doIter >>= run_ >>= return where doIter = do step <- runIteratee consume let i = joinI $ takeNoMoreThan (n-abs m) step step' <- runIteratee i return $ enumLBS s step' n = L.length s testCountBytes :: Test testCountBytes = testProperty "iteratee/countBytes" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do (!_,n1) <- liftQ (runIteratee (countBytes (return ())) >>= f) (!_,n2) <- liftQ (runIteratee (countBytes consume) >>= f) assert $ n1 == 0 assert $ n2 == n expectException $ (runIteratee erriter >>= f) expectException $ (runIteratee erriter >>= run_ . enumEOF) where erriter = countBytes $ throwError $ AssertionFailed "foo" f iter = run_ $ enumLBS s iter n = L.length s ------------------------------------------------------------------------------ testCountBytes2 :: Test testCountBytes2 = testProperty "iteratee/countBytes2" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do pre $ L.length s > 4 step <- liftQ $ runIteratee iter (n1,s') <- f step assert $ n1 == 4 assert $ s' == L.drop 4 s where f i = liftQ $ run_ $ enumLBS s i iter = do (!_,m) <- countBytes $ drop' 4 x <- liftM L.fromChunks consume return (m,x) ------------------------------------------------------------------------------ testBMHTrivials :: Test testBMHTrivials = testCase "iteratee/BoyerMooreHorspoolTrivial" prop where prop = do coverShowInstance $ Match "" coverShowInstance $ NoMatch "" ------------------------------------------------------------------------------ testBMH :: Test testBMH = testProperty "iteratee/BoyerMooreHorspool" $ monadicIO $ forAllM arbitrary prop where prop :: (ByteString, [ByteString]) -> PropertyM IO () prop (needle, haystack) = do let lneedle = L.fromChunks [needle] let lhaystack = L.fromChunks haystack pre ((not $ S.null needle) && (not $ L.null lhaystack) && (not $ S.isInfixOf needle (S.concat haystack))) -- put the needle at the beginning, at the end, and somewhere in the -- middle lhay <- insertNeedle lneedle lhaystack let stream = L.concat [lneedle, lhay] -- there should be exactly three Matches let iter = enumLBS stream $$ joinI (bmhEnumeratee needle $$ consume) outp <- QC.run $ run_ iter let nMatches = length $ filter isMatch outp when (nMatches /= 3) $ QC.run $ do putStrLn "got wrong number of matches!!" putStrLn "needle:\n" putStrLn $ show lneedle putStrLn "\nhaystack:\n" mapM_ (putStrLn . show) (L.toChunks stream) putStrLn "\noutput stream:" mapM_ (putStrLn . show) outp putStrLn "" assert $ nMatches == 3 isMatch (Match _) = True isMatch _ = False insertNeedle lneedle lhaystack = do idxL <- pick $ choose (0, lenL-1) idxN <- pick $ choose (0, lenN-1) idxN2 <- pick $ choose (0, lenN-1) let (l1, l2) = L.splitAt (toEnum idxL) lhaystack let (n1, n2) = L.splitAt (toEnum idxN) lneedle let (n3, n4) = L.splitAt (toEnum idxN2) lneedle return $ L.concat [ l1, n1, n2, l2, n3, n4 ] where lenN = fromEnum $ L.length lneedle lenL = fromEnum $ L.length lhaystack ------------------------------------------------------------------------------ testKillIfTooSlow1 :: Test testKillIfTooSlow1 = testCase "iteratee/killIfTooSlow1" $ do let iter = killIfTooSlow (return ()) 1000 4 consume m <- timeout (10*seconds) (expectExceptionH $ run_ $ tooSlowEnum 10 $$ iter) maybe (fail "timed out without dying") (const $ return ()) m ------------------------------------------------------------------------------ testKillIfTooSlow2 :: Test testKillIfTooSlow2 = testCase "iteratee/killIfTooSlow2" $ do -- 10 bytes per second, minimum run 2 seconds let iter = killIfTooSlow (return ()) 10 2 consume m <- liftM S.concat $ run_ $ tooSlowEnum 3 $$ iter H.assertEqual "testKillIfTooSlow2" (S.replicate 300 'f') m ------------------------------------------------------------------------------ testCatchIO :: Test testCatchIO = testCase "iteratee/monadCatchIO" $ do e <- run_ $ enumList 1 ["1", "2", "3", "4", "5"] $$ iter 0 H.assertBool "handled exception" $ isJust e where iter !i = (continue $ k (i::Int)) `catch` h k _ EOF = return Nothing k i _ = if i >= 2 then throw $ ErrorCall "should not escape!" else iter (i+1) h :: SomeException -> Iteratee ByteString IO (Maybe String) h e = return $ Just $ show e ------------------------------------------------------------------------------ tooSlowEnum :: Int -> Enumerator ByteString IO a tooSlowEnum ntimes (Continue k) = do if ntimes <= 0 then k EOF else do step <- lift $ runIteratee $ k $ Chunks [S.replicate 100 'f'] liftIO $ waitabit tooSlowEnum (ntimes-1) step tooSlowEnum _ z = returnI z ------------------------------------------------------------------------------ waitabit :: IO () waitabit = threadDelay $ 2*seconds ------------------------------------------------------------------------------ seconds :: Int seconds = (10::Int) ^ (6::Int) snap-core-0.9.8.0/test/suite/Snap/Test/0000755000000000000000000000000012565252520015677 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Test/Common.hs0000644000000000000000000000735212565252520017472 0ustar0000000000000000{-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE ScopedTypeVariables #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Snap.Test.Common ( coverEqInstance , coverOrdInstance , coverReadInstance , coverShowInstance , coverTypeableInstance , forceSameType , expectException , expectExceptionH , liftQ , eatException ) where ------------------------------------------------------------------------------ import Control.DeepSeq import Control.Exception (SomeException(..), evaluate) import Control.Monad import Control.Monad.CatchIO import Control.Monad.Trans import qualified Data.ByteString as S import qualified Data.ByteString.Lazy as L import Data.ByteString.Internal (c2w) import Data.Typeable import Prelude hiding (catch) import Test.QuickCheck import qualified Test.QuickCheck.Monadic as QC import Test.QuickCheck.Monadic ------------------------------------------------------------------------------ instance Arbitrary S.ByteString where arbitrary = liftM (S.pack . map c2w) arbitrary instance Arbitrary L.ByteString where arbitrary = do n <- choose(0,5) chunks <- replicateM n arbitrary return $ L.fromChunks chunks ------------------------------------------------------------------------------ eatException :: (MonadCatchIO m) => m a -> m () eatException a = (a >> return ()) `catch` handler where handler :: (MonadCatchIO m) => SomeException -> m () handler _ = return () ------------------------------------------------------------------------------ forceSameType :: a -> a -> a forceSameType _ a = a ------------------------------------------------------------------------------ -- | Kill the false negative on derived show instances. coverShowInstance :: (Monad m, Show a) => a -> m () coverShowInstance x = a `deepseq` b `deepseq` c `deepseq` return () where a = showsPrec 0 x "" b = show x c = showList [x] "" ------------------------------------------------------------------------------ coverReadInstance :: (MonadIO m, Read a) => a -> m () coverReadInstance x = do liftIO $ eatException $ evaluate $ forceSameType [(x,"")] $ readsPrec 0 "" liftIO $ eatException $ evaluate $ forceSameType [([x],"")] $ readList "" ------------------------------------------------------------------------------ coverEqInstance :: (Monad m, Eq a) => a -> m () coverEqInstance x = a `seq` b `seq` return () where a = x == x b = x /= x ------------------------------------------------------------------------------ coverOrdInstance :: (Monad m, Ord a) => a -> m () coverOrdInstance x = a `deepseq` b `deepseq` return () where a = [ x < x , x >= x , x > x , x <= x , compare x x == EQ ] b = min a $ max a a ------------------------------------------------------------------------------ coverTypeableInstance :: (Monad m, Typeable a) => a -> m () coverTypeableInstance a = typeOf a `seq` return () ------------------------------------------------------------------------------ expectException :: IO a -> PropertyM IO () expectException m = do e <- liftQ $ try m case e of Left (z::SomeException) -> (length $ show z) `seq` return () Right _ -> fail "expected exception, didn't get one" ------------------------------------------------------------------------------ expectExceptionH :: IO a -> IO () expectExceptionH act = do e <- try act case e of Left (z::SomeException) -> (length $ show z) `seq` return () Right _ -> fail "expected exception, didn't get one" ------------------------------------------------------------------------------ liftQ :: forall a m . (Monad m) => m a -> PropertyM m a liftQ = QC.run snap-core-0.9.8.0/test/suite/Snap/Util/0000755000000000000000000000000012565252520015675 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Util/FileServe/0000755000000000000000000000000012565252520017561 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Util/FileServe/Tests.hs0000644000000000000000000004573512565252520021235 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Snap.Util.FileServe.Tests ( tests ) where ------------------------------------------------------------------------------ import Blaze.ByteString.Builder import Control.Monad import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as S import qualified Data.ByteString.Lazy.Char8 as L import Data.IORef import qualified Data.HashMap.Strict as HashMap import qualified Data.Map as Map import Data.Maybe import Data.Monoid import Prelude hiding (take) import Test.Framework import Test.Framework.Providers.HUnit import Test.HUnit hiding (Test, path) ------------------------------------------------------------------------------ import Snap.Internal.Http.Types import Snap.Internal.Types import Snap.Util.FileServe import Snap.Iteratee import qualified Snap.Types.Headers as H ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ tests :: [Test] tests = [ testFooBin , testFooTxt , testFooHtml , testFooBinBinBin , test404s , testFsSingle , testFsCfgA , testFsCfgB , testFsCfgC , testFsCfgD , testFsCfgFancy , testRangeOK , testRangeBad , testMultiRange , testIfRange ] ------------------------------------------------------------------------------ expect404 :: IO Response -> IO () expect404 m = do r <- m assertBool "expected 404" (rspStatus r == 404) ------------------------------------------------------------------------------ expect302 :: ByteString -> IO Response -> IO () expect302 p m = do r <- m assertBool "expected 302" (rspStatus r == 302) assertEqual "redir location" (Just p) (getHeader "location" r) ------------------------------------------------------------------------------ getBody :: Response -> IO L.ByteString getBody r = do let benum = rspBodyToEnum $ rspBody r liftM (toLazyByteString . mconcat) (runIteratee consume >>= run_ . benum) ------------------------------------------------------------------------------ runIt :: Snap a -> Request -> Iteratee ByteString IO (Request, Response) runIt m rq = runSnap m d d rq where d = const $ return () ------------------------------------------------------------------------------ go :: Snap a -> ByteString -> IO Response go m s = do rq <- mkRequest s liftM snd (run_ $ runIt m rq) ------------------------------------------------------------------------------ goIfModifiedSince :: Snap a -> ByteString -> ByteString -> IO Response goIfModifiedSince m s lm = do rq <- mkRequest s let r = setHeader "if-modified-since" lm rq liftM snd (run_ $ runIt m r) ------------------------------------------------------------------------------ goIfRange :: Snap a -> ByteString -> (Int,Int) -> ByteString -> IO Response goIfRange m s (start,end) lm = do rq <- mkRequest s let r = setHeader "if-range" lm $ setHeader "Range" (S.pack $ "bytes=" ++ show start ++ "-" ++ show end) rq liftM snd (run_ $ runIt m r) ------------------------------------------------------------------------------ goRange :: Snap a -> ByteString -> (Int,Int) -> IO Response goRange m s (start,end) = do rq' <- mkRequest s let rq = setHeader "Range" (S.pack $ "bytes=" ++ show start ++ "-" ++ show end) rq' liftM snd (run_ $ runIt m rq) ------------------------------------------------------------------------------ goMultiRange :: Snap a -> ByteString -> (Int,Int) -> (Int,Int) -> IO Response goMultiRange m s (start,end) (start2,end2) = do rq' <- mkRequest s let rq = setHeader "Range" (S.pack $ "bytes=" ++ show start ++ "-" ++ show end ++ "," ++ show start2 ++ "-" ++ show end2) rq' liftM snd (run_ $ runIt m rq) ------------------------------------------------------------------------------ goRangePrefix :: Snap a -> ByteString -> Int -> IO Response goRangePrefix m s start = do rq' <- mkRequest s let rq = setHeader "Range" (S.pack $ "bytes=" ++ show start ++ "-") rq' liftM snd (run_ $ runIt m rq) ------------------------------------------------------------------------------ goRangeSuffix :: Snap a -> ByteString -> Int -> IO Response goRangeSuffix m s end = do rq' <- mkRequest s let rq = setHeader "Range" (S.pack $ "bytes=-" ++ show end) rq' liftM snd (run_ $ runIt m rq) ------------------------------------------------------------------------------ mkRequest :: ByteString -> IO Request mkRequest uri = do enum <- newIORef $ SomeEnumerator returnI return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False H.empty enum Nothing GET (1,1) [] pathPart "/" (S.concat ["/",uri]) queryPart Map.empty Map.empty Map.empty where (pathPart, queryPart) = breakQuery uri breakQuery s = (a, S.drop 1 b) where (a,b) = S.break (=='?') s ------------------------------------------------------------------------------ fs :: Snap () fs = do x <- serveDirectory "data/fileServe" return $! x `seq` () ------------------------------------------------------------------------------ fsSingle :: Snap () fsSingle = do x <- serveFile "data/fileServe/foo.html" return $! x `seq` () ------------------------------------------------------------------------------ fsCfg :: DirectoryConfig Snap -> Snap () fsCfg cfg = do x <- serveDirectoryWith cfg "data/fileServe" return $! x `seq` () ------------------------------------------------------------------------------ testFooBin :: Test testFooBin = testCase "fileServe/foo.bin" $ do r1 <- go fs "foo.bin" checkProps "fileServe/foo.bin/default" r1 let !lm = fromJust $ getHeader "last-modified" r1 go fs "foo.bin?blah=blah" >>= checkProps "fileServe/foo.bin/query-string" -- check last modified stuff r2 <- goIfModifiedSince fs "foo.bin" lm assertEqual "foo.bin 304" 304 $ rspStatus r2 r3 <- goIfModifiedSince fs "foo.bin" "Wed, 15 Nov 1995 04:58:08 GMT" checkProps "fileServe/foo.bin/ifModifiedSince" r3 where checkProps name r = do b <- getBody r assertEqual (name ++ "/contents") "FOO\n" b assertEqual (name ++ "/content-type") (Just "application/octet-stream") (getHeader "content-type" r) assertEqual (name ++ "/size") (Just 4) (rspContentLength r) assertBool (name ++ "/last-modified") (isJust $ getHeader "last-modified" r) assertEqual (name ++ "/accept-ranges") (Just "bytes") (getHeader "accept-ranges" r) ------------------------------------------------------------------------------ testFooTxt :: Test testFooTxt = testCase "fileServe/foo.txt" $ do go fs "foo.txt" >>= checkProps "fileServe/foo.txt/default" go fs "foo.txt?blah=blah" >>= checkProps "fileServe/foo.txt/query" where checkProps name r = do b <- getBody r assertEqual (name ++ "/contents") "FOO\n" b assertEqual (name ++ "/content-type") (Just "text/plain") (getHeader "content-type" r) assertEqual (name ++ "/size") (Just 4) (rspContentLength r) assertBool (name ++ "/last-modified") (isJust $ getHeader "last-modified" r) assertEqual (name ++ "/accept-ranges") (Just "bytes") (getHeader "accept-ranges" r) ------------------------------------------------------------------------------ testFooHtml :: Test testFooHtml = testCase "fileServe/foo.html" $ do go fs "foo.html" >>= checkProps "fileServe/foo.html/default" go fs "foo.html?bar=bar" >>= checkProps "fileServe/foo.html/query" where checkProps name r = do b <- getBody r assertEqual (name ++ "/contents") "FOO\n" b assertEqual (name ++ "/content-type") (Just "text/html") (getHeader "content-type" r) assertEqual (name ++ "/size") (Just 4) (rspContentLength r) assertBool (name ++ "/last-modified") (isJust $ getHeader "last-modified" r) assertEqual (name ++ "/accept-ranges") (Just "bytes") (getHeader "accept-ranges" r) ------------------------------------------------------------------------------ testFooBinBinBin :: Test testFooBinBinBin = testCase "fileServe/foo.bin.bin.bin" $ do go fs "foo.bin.bin.bin" >>= checkProps "fileServe/foo.bin.bin.bin" where checkProps name r = do b <- getBody r assertEqual (name ++ "/contents") "FOO\n" b assertEqual (name ++ "/content-type") (Just "application/octet-stream") (getHeader "content-type" r) assertEqual (name ++ "/size") (Just 4) (rspContentLength r) assertBool (name ++ "/last-modified") (isJust $ getHeader "last-modified" r) assertEqual (name ++ "/accept-ranges") (Just "bytes") (getHeader "accept-ranges" r) ------------------------------------------------------------------------------ test404s :: Test test404s = testCase "fileServe/404s" $ do expect404 $ go fs "jfldksjflksd" expect404 $ go fs "dummy/../foo.txt" expect404 $ go fs "/etc/password" coverMimeMap ------------------------------------------------------------------------------ printName :: FilePath -> Snap () printName c = writeBS $ snd $ S.breakEnd (=='/') $ S.pack c ------------------------------------------------------------------------------ cfgA, cfgB, cfgC, cfgD :: DirectoryConfig Snap cfgA = DirectoryConfig { indexFiles = [] , indexGenerator = const pass , dynamicHandlers = HashMap.empty , mimeTypes = defaultMimeTypes , preServeHook = const $ return () } cfgB = DirectoryConfig { indexFiles = ["index.txt", "altindex.html"] , indexGenerator = const pass , dynamicHandlers = HashMap.empty , mimeTypes = defaultMimeTypes , preServeHook = const $ return () } cfgC = DirectoryConfig { indexFiles = ["index.txt", "altindex.html"] , indexGenerator = printName , dynamicHandlers = HashMap.empty , mimeTypes = defaultMimeTypes , preServeHook = const $ return () } cfgD = DirectoryConfig { indexFiles = [] , indexGenerator = const pass , dynamicHandlers = HashMap.fromList [ (".txt", printName) ] , mimeTypes = defaultMimeTypes , preServeHook = const $ return () } ------------------------------------------------------------------------------ testFsCfgA :: Test testFsCfgA = testCase "fileServe/cfgA" $ do let gooo = go (fsCfg cfgA) -- Named file in the root directory gooo "foo.bin" >>= checkProps "cfgA1/1" "application/octet-stream" gooo "foo.bin?blah=blah" >>= checkProps "cfgA1/2" "application/octet-stream" -- Missing file in the root directory expect404 $ gooo "bar.bin" -- Named file in a subdirectory gooo "mydir2/foo.txt" >>= checkProps "cfgA1/subdir/1" "text/plain" gooo "mydir2/foo.txt?z=z" >>= checkProps "cfgA1/subdir/2" "text/plain" -- Missing file in a subdirectory expect404 $ gooo "mydir2/bar.txt" -- Request for directory with no trailing slash expect302 "/mydir1/" $ gooo "mydir1" -- Request for directory with no trailing slash, with query param expect302 "/mydir1/?z=z" $ gooo "mydir1?z=z" -- Request for directory with trailing slash, no index expect404 $ gooo "mydir1/" expect404 $ gooo "mydir2/" -- Request file with trailing slash expect404 $ gooo "foo.html/" expect404 $ gooo "mydir2/foo.txt/" where checkProps name ct r = do b <- getBody r assertEqual (name ++ "/contents") "FOO\n" b assertEqual (name ++ "/content-type") (Just ct) (getHeader "content-type" r) assertEqual (name ++ "/size") (Just 4) (rspContentLength r) assertBool (name ++ "/last-modified") (isJust $ getHeader "last-modified" r) assertEqual (name ++ "/accept-ranges") (Just "bytes") (getHeader "accept-ranges" r) ------------------------------------------------------------------------------ testFsCfgB :: Test testFsCfgB = testCase "fileServe/cfgB" $ do let gooo = go (fsCfg cfgB) -- Request for root directory with index rB1 <- gooo "mydir1/" bB1 <- getBody rB1 assertEqual "B1" "INDEX\n" bB1 assertEqual "B1 content-type" (Just "text/plain") (getHeader "content-type" rB1) -- Request for root directory with index, query rB2 <- gooo "mydir1/?z=z" bB2 <- getBody rB2 assertEqual "B2" "INDEX\n" bB2 assertEqual "B2 content-type" (Just "text/plain") (getHeader "content-type" rB2) -- Request for root directory with alternate index rB3 <- gooo "mydir3/" bB3 <- getBody rB3 assertEqual "B3" "ALTINDEX\n" bB3 assertEqual "B3 content-type" (Just "text/html") (getHeader "content-type" rB3) -- Request for root directory with no index expect404 $ gooo "mydir2/" ------------------------------------------------------------------------------ testFsCfgC :: Test testFsCfgC = testCase "fileServe/cfgC" $ do let gooo = go (fsCfg cfgC) -- Request for root directory with index rC1 <- gooo "mydir1/" bC1 <- getBody rC1 assertEqual "C1" "INDEX\n" bC1 assertEqual "C1 content-type" (Just "text/plain") (getHeader "content-type" rC1) -- Request for root directory with index, query rC2 <- gooo "mydir1/?z=z" bC2 <- getBody rC2 assertEqual "C2" "INDEX\n" bC2 assertEqual "C2 content-type" (Just "text/plain") (getHeader "content-type" rC2) -- Request for root directory with generated index rC3 <- gooo "mydir2/" bC3 <- getBody rC3 assertEqual "C3" "mydir2" bC3 ------------------------------------------------------------------------------ testFsCfgD :: Test testFsCfgD = testCase "fileServe/cfgD" $ do -- Request for file with dynamic handler rD1 <- go (fsCfg cfgD) "mydir2/foo.txt" bD1 <- getBody rD1 assertEqual "D1" "foo.txt" bD1 ------------------------------------------------------------------------------ testFsCfgFancy :: Test testFsCfgFancy = testCase "fileServe/cfgFancy" $ do -- Request for directory with autogen index rE1 <- go (fsCfg fancyDirectoryConfig) "mydir2/" bE1 <- S.concat `fmap` L.toChunks `fmap` getBody rE1 assertBool "autogen-sub-index" $ "Directory Listing: /mydir2/" `S.isInfixOf` bE1 assertBool "autogen-sub-parent" $ " m () coverMimeMap = Prelude.mapM_ f $ HashMap.toList defaultMimeTypes where f (!k,!v) = return $ case k `seq` v `seq` () of () -> () ------------------------------------------------------------------------------ testIfRange :: Test testIfRange = testCase "fileServe/range/if-range" $ do r <- goIfRange fs "foo.bin" (1,2) "Wed, 15 Nov 1995 04:58:08 GMT" assertEqual "foo.bin 200" 200 $ rspStatus r b <- getBody r assertEqual "foo.bin" "FOO\n" b r2 <- goIfRange fs "foo.bin" (1,2) "Tue, 01 Oct 2030 04:58:08 GMT" assertEqual "foo.bin 206" 206 $ rspStatus r2 b2 <- getBody r2 assertEqual "foo.bin partial" "OO" b2 r3 <- goIfRange fs "foo.bin" (1,24324) "Tue, 01 Oct 2030 04:58:08 GMT" assertEqual "foo.bin 200" 200 $ rspStatus r3 b3 <- getBody r3 assertEqual "foo.bin" "FOO\n" b3 snap-core-0.9.8.0/test/suite/Snap/Util/FileUploads/0000755000000000000000000000000012565252520020104 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Util/FileUploads/Tests.hs0000644000000000000000000004516512565252520021555 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Snap.Util.FileUploads.Tests ( tests ) where ------------------------------------------------------------------------------ import Control.Applicative import Control.Concurrent (threadDelay) import Control.DeepSeq import Control.Exception (Exception(..), SomeException(..)) import Control.Monad import Control.Monad.CatchIO import Control.Monad.Trans import Data.ByteString (ByteString) import qualified Data.ByteString.Char8 as S import Data.IORef import qualified Data.Map as Map import Data.Maybe import qualified Data.Text as T import Data.Typeable import Prelude hiding (catch) import System.Directory import System.Mem import System.Timeout import Test.Framework import Test.Framework.Providers.HUnit import Test.HUnit hiding (Test, path) ------------------------------------------------------------------------------ import Snap.Internal.Http.Types import Snap.Internal.Debug import Snap.Internal.Exceptions import Snap.Internal.Iteratee.Debug import Snap.Internal.Types import Snap.Iteratee hiding (map) import qualified Snap.Types.Headers as H import Snap.Test.Common import Snap.Util.FileUploads ------------------------------------------------------------------------------ data TestException = TestException deriving (Show, Typeable) instance Exception TestException ------------------------------------------------------------------------------ tests :: [Test] tests = [ testSuccess1 , testSuccess2 , testPerPartPolicyViolation1 , testPerPartPolicyViolation2 , testFormInputPolicyViolation , testTooManyHeaders , testNoBoundary , testNoMixedBoundary , testWrongContentType , testSlowEnumerator , testTrivials , testDisconnectionCleanup ] ------------------------------------------------------------------------------ testSuccess1 :: Test testSuccess1 = testCase "fileUploads/success1" $ harness tmpdir hndl mixedTestBody where tmpdir = "tempdir1" hndl = handleFileUploads tmpdir defaultUploadPolicy (const $ allowWithMaximumSize 300000) hndl' hndl' xs = do fileMap <- foldM f Map.empty xs p1 <- getParam "field1" p1P <- getPostParam "field1" p1Q <- getQueryParam "field1" p2 <- getParam "field2" liftIO $ do assertEqual "file1 contents" (Just ("text/plain", file1Contents)) (Map.lookup "file1.txt" fileMap) assertEqual "file2 contents" (Just ("image/gif", file2Contents)) (Map.lookup "file2.gif" fileMap) assertEqual "field1 contents" (Just formContents1) p1 assertEqual "field1 POST contents" (Just formContents1) p1P assertEqual "field1 query contents" Nothing p1Q assertEqual "field2 contents" (Just formContents2) p2 f mp (partInfo, e) = either throw (\fp -> do x <- liftIO $ S.readFile fp let fn = fromJust $ partFileName partInfo let ct = partContentType partInfo return $ Map.insert fn (ct,x) mp) e ------------------------------------------------------------------------------ testSuccess2 :: Test testSuccess2 = testCase "fileUploads/success2" $ harness tmpdir hndl mixedTestBody where tmpdir = "tempdir2" policy = setProcessFormInputs False defaultUploadPolicy hndl = handleFileUploads tmpdir policy (const $ allowWithMaximumSize 300000) hndl' hndl' xs = do liftIO $ assertEqual "num params" 4 (length xs) show xs `deepseq` return () ------------------------------------------------------------------------------ testPerPartPolicyViolation1 :: Test testPerPartPolicyViolation1 = testCase "fileUploads/perPartPolicyViolation1" $ harness tmpdir hndl mixedTestBody where tmpdir = "tempdir_pol1" hndl = handleFileUploads tmpdir defaultUploadPolicy (const disallow) hndl' hndl' xs = do p1 <- getParam "field1" p2 <- getParam "field2" mapM_ f xs liftIO $ do assertEqual "field1 contents" (Just formContents1) p1 assertEqual "field2 contents" (Just formContents2) p2 f (!_, e) = either (\i -> show i `deepseq` return ()) (const $ error "expected policy violation") e ------------------------------------------------------------------------------ testPerPartPolicyViolation2 :: Test testPerPartPolicyViolation2 = testCase "fileUploads/perPartPolicyViolation2" $ harness tmpdir hndl mixedTestBody where tmpdir = "tempdir_pol2" hndl = handleFileUploads tmpdir defaultUploadPolicy (const $ allowWithMaximumSize 4) hndl' hndl' xs = mapM_ f xs f (partInfo, e) = (if partFileName partInfo == Just "file1.txt" then ePass else eFail) e eFail = either (\i -> show i `deepseq` return ()) (const $ error "expected policy violation") ePass = either (throw) (\i -> show i `deepseq` return ()) ------------------------------------------------------------------------------ testFormInputPolicyViolation :: Test testFormInputPolicyViolation = testCase "fileUploads/formInputTooBig" $ (harness tmpdir hndl mixedTestBody `catch` h) where h !(e :: FileUploadException) = do let r = fileUploadExceptionReason e assertBool "correct exception" (T.isInfixOf "form input" r && T.isInfixOf "exceeded maximum" r) tmpdir = "tempdir_formpol" policy = setMaximumFormInputSize 2 defaultUploadPolicy hndl = handleFileUploads tmpdir policy (const $ allowWithMaximumSize 4) hndl' hndl' xs = show xs `deepseq` return () ------------------------------------------------------------------------------ testNoBoundary :: Test testNoBoundary = testCase "fileUploads/noBoundary" $ expectExceptionH $ harness' goBadContentType tmpdir hndl mixedTestBody where tmpdir = "tempdir_noboundary" hndl = handleFileUploads tmpdir defaultUploadPolicy (const $ allowWithMaximumSize 300000) hndl' hndl' xs = show xs `deepseq` return () ------------------------------------------------------------------------------ testNoMixedBoundary :: Test testNoMixedBoundary = testCase "fileUploads/noMixedBoundary" $ expectExceptionH $ harness' go tmpdir hndl badMixedBody where tmpdir = "tempdir_mixednoboundary" hndl = handleFileUploads tmpdir defaultUploadPolicy (const $ allowWithMaximumSize 300000) hndl' hndl' xs = show xs `deepseq` return () ------------------------------------------------------------------------------ testWrongContentType :: Test testWrongContentType = testCase "fileUploads/wrongContentType" $ expectExceptionH $ harness' goWrongContentType tmpdir hndl mixedTestBody where tmpdir = "tempdir_noboundary" hndl = handleFileUploads tmpdir defaultUploadPolicy (const $ allowWithMaximumSize 300000) hndl' <|> error "expect fail here" hndl' xs = show xs `deepseq` return () ------------------------------------------------------------------------------ testTooManyHeaders :: Test testTooManyHeaders = testCase "fileUploads/tooManyHeaders" $ (harness tmpdir hndl bigHeadersBody `catch` h) where h (e :: BadPartException) = show e `deepseq` return () tmpdir = "tempdir_tooManyHeaders" hndl = handleFileUploads tmpdir defaultUploadPolicy (const $ allowWithMaximumSize 4) hndl' hndl' xs = show xs `deepseq` return () ------------------------------------------------------------------------------ testSlowEnumerator :: Test testSlowEnumerator = testCase "fileUploads/tooSlow" $ (harness' goSlowEnumerator tmpdir hndl mixedTestBody `catch` h0) where h0 (e :: ConnectionTerminatedException) = let (ConnectionTerminatedException se) = e (me :: Maybe RateTooSlowException) = fromException se in maybe (throw e) h me h (e :: RateTooSlowException) = e `seq` return () tmpdir = "tempdir_tooslow" policy = setMinimumUploadRate 200000 $ setMinimumUploadSeconds 2 $ defaultUploadPolicy hndl = handleFileUploads tmpdir policy (const $ allowWithMaximumSize 400000) hndl' hndl' xs = show xs `deepseq` return () ------------------------------------------------------------------------------ testTrivials :: Test testTrivials = testCase "fileUploads/trivials" $ do assertEqual "" False $ doProcessFormInputs policy assertEqual "" 1000 $ getMinimumUploadRate policy assertEqual "" 1000 $ getMinimumUploadRate defaultUploadPolicy assertEqual "" 5 $ getMinimumUploadSeconds policy assertEqual "" 9 $ getUploadTimeout policy where policy = setProcessFormInputs False $ setMinimumUploadRate 1000 $ setMinimumUploadSeconds 5 $ setUploadTimeout 9 $ defaultUploadPolicy ------------------------------------------------------------------------------ testDisconnectionCleanup :: Test testDisconnectionCleanup = testCase "fileUploads/disconnectionCleanup" $ do runTest `finally` removeDirectoryRecursive tmpdir where runTest = do eatException $ removeDirectoryRecursive tmpdir createDirectoryIfMissing True tmpdir rq <- mkDamagedRequest mixedTestBody eatException $ liftM snd (run_ $ runIt hndl rq) performGC dirs <- liftM (filter (\x -> x /= "." && x /= "..")) $ getDirectoryContents tmpdir assertEqual "files should be cleaned up" [] dirs tmpdir = "tempdirC" hndl = handleFileUploads tmpdir defaultUploadPolicy (const $ allowWithMaximumSize 300000) hndl' hndl' _ = return () ------------------------------------------------------------------------------ harness :: FilePath -> Snap a -> ByteString -> IO () harness = harness' go ------------------------------------------------------------------------------ harness' :: (Snap a -> ByteString -> IO Response) -> FilePath -> Snap a -> ByteString -> IO () harness' g tmpdir hndl body = (do createDirectoryIfMissing True tmpdir g hndl body return ()) `finally` removeDirectoryRecursive tmpdir ------------------------------------------------------------------------------ mkRequest :: ByteString -> IO Request mkRequest body = do enum <- newIORef $ SomeEnumerator $ enumBS body let hdrs = H.fromList [ ("Content-type", S.append "multipart/form-data; boundary=" boundaryValue) ] return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False hdrs enum Nothing POST (1,1) [] "/" "/" "/" "" Map.empty Map.empty Map.empty ------------------------------------------------------------------------------ mkDamagedRequest :: ByteString -> IO Request mkDamagedRequest body = do enum <- newIORef $ SomeEnumerator $ enum let hdrs = H.fromList [ ("Content-type", S.append "multipart/form-data; boundary=" boundaryValue) ] return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False hdrs enum Nothing POST (1,1) [] "/" "/" "/" "" Map.empty Map.empty Map.empty where enum = enumBS (S.take (S.length body - 1) body) >==> dieNow dieNow _ = throw TestException ------------------------------------------------------------------------------ go :: Snap a -> ByteString -> IO Response go m s = do rq <- mkRequest s liftM snd (run_ $ runIt m rq) ------------------------------------------------------------------------------ goBadContentType :: Snap a -> ByteString -> IO Response goBadContentType m s = do rq <- mkRequest s let rq' = setHeader "Content-Type" "multipart/form-data" rq liftM snd (run_ $ runIt m rq') ------------------------------------------------------------------------------ goWrongContentType :: Snap a -> ByteString -> IO Response goWrongContentType m s = do rq <- mkRequest s let rq' = setHeader "Content-Type" "text/plain" rq liftM snd (run_ $ runIt m rq') ------------------------------------------------------------------------------ goSlowEnumerator :: Snap a -> ByteString -> IO Response goSlowEnumerator m s = do rq <- mkRequest s writeIORef (rqBody rq) $ SomeEnumerator slowEnum mx <- timeout (20*seconds) (liftM snd (run_ $ runIt m rq)) maybe (error "timeout") return mx where body = S.unpack s slowEnum x = goo x body goo (Continue k) [] = k EOF goo (Continue k) (x:xs) = do debug $ "goSlowEnumerator: sending " ++ show x step <- lift $ runIteratee $ k $ Chunks [ S.pack (x:[]) ] liftIO waitabit goo step xs goo (Error e) _ = throwError e goo _ _ = error "impossible" ------------------------------------------------------------------------------ waitabit :: IO () waitabit = threadDelay $ 2*seconds ------------------------------------------------------------------------------ seconds :: Int seconds = (10::Int) ^ (6::Int) ------------------------------------------------------------------------------ runIt :: Snap a -> Request -> Iteratee ByteString IO (Request, Response) runIt m rq = iterateeDebugWrapper "test" $ runSnap m d (const $ return ()) rq where d :: forall a . Show a => a -> IO () d = \x -> show x `deepseq` return () ------------------------------------------------------------------------------ -- TEST DATA formContents1 :: ByteString formContents1 = "form contents 1" formContents2 :: ByteString formContents2 = "form contents 2 zzzzzzzzzzzzzzzzzzzz" file1Contents :: ByteString file1Contents = "foo" file2Contents :: ByteString file2Contents = "... contents of file2.gif ..." boundaryValue :: ByteString boundaryValue = "fkjldsakjfdlsafldksjf" subBoundaryValue :: ByteString subBoundaryValue = "zjkzjjfjskzjzjkz" crlf :: ByteString crlf = "\r\n" ------------------------------------------------------------------------------ mixedTestBody :: ByteString mixedTestBody = S.concat [ "--" , boundaryValue , crlf , "content-disposition: form-data; name=\"field1\"\r\n" , crlf , formContents1 , crlf , "--" , boundaryValue , crlf , "content-disposition: form-data; name=\"field2\"\r\n" , crlf , formContents2 , crlf , "--" , boundaryValue , crlf , "content-disposition: form-data; name=\"files\"\r\n" , "Content-type: multipart/mixed; boundary=" , subBoundaryValue , crlf , crlf , "--" , subBoundaryValue , crlf , "Content-disposition: attachment; filename=\"file1.txt\"\r\n" , "Content-Type: text/plain\r\n" , crlf , file1Contents , crlf , "--" , subBoundaryValue , crlf , "Content-disposition: attachment; filename=\"file2.gif\"\r\n" , "Content-type: image/gif\r\n" , "Content-Transfer-Encoding: binary\r\n" , crlf , file2Contents , crlf , "--" , subBoundaryValue , "--\r\n" , "--" , boundaryValue , "--\r\n" ] ------------------------------------------------------------------------------ badMixedBody :: ByteString badMixedBody = S.concat [ "--" , boundaryValue , crlf , "content-disposition: form-data; name=\"field1\"\r\n" , crlf , formContents1 , crlf , "--" , boundaryValue , crlf , "content-disposition: form-data; name=\"field2\"\r\n" , crlf , formContents2 , crlf , "--" , boundaryValue , crlf , "content-disposition: form-data; name=\"files\"\r\n" , "Content-type: multipart/mixed" , crlf , crlf , "--" , subBoundaryValue , crlf , "Content-disposition: attachment; filename=\"file1.txt\"\r\n" , "Content-Type: text/plain\r\n" , crlf , file1Contents , crlf , "--" , subBoundaryValue , crlf , "Content-disposition: attachment; filename=\"file2.gif\"\r\n" , "Content-type: image/gif\r\n" , "Content-Transfer-Encoding: binary\r\n" , crlf , file2Contents , crlf , "--" , subBoundaryValue , "--\r\n" , "--" , boundaryValue , "--\r\n" ] ------------------------------------------------------------------------------ bigHeadersBody :: ByteString bigHeadersBody = S.concat ( [ "--" , boundaryValue , crlf , "content-disposition: form-data; name=\"field1\"\r\n" ] ++ map (\i -> S.pack ("field_" ++ show i ++ ": bar\r\n")) [1..40000::Int] ++ [ crlf , formContents1 , crlf , "--" , boundaryValue , "--\r\n" ]) snap-core-0.9.8.0/test/suite/Snap/Util/GZip/0000755000000000000000000000000012565252520016546 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Util/GZip/Tests.hs0000644000000000000000000003463612565252520020220 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE Rank2Types #-} {-# LANGUAGE ScopedTypeVariables #-} module Snap.Util.GZip.Tests ( tests ) where import Blaze.ByteString.Builder import qualified Codec.Compression.GZip as GZip import qualified Codec.Compression.Zlib as Zlib import Control.Exception hiding (assert) import Control.Monad (liftM) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Lazy.Char8 as L import Data.Digest.Pure.MD5 import Data.IORef import qualified Data.Map as Map import Data.Monoid import Data.Serialize (encode) import Test.Framework import Test.Framework.Providers.QuickCheck2 import Test.QuickCheck import qualified Test.QuickCheck.Monadic as QC import Test.QuickCheck.Monadic hiding (run) import Test.Framework.Providers.HUnit import qualified Test.HUnit as H import Snap.Core import Snap.Internal.Http.Types import Snap.Iteratee import Snap.Test.Common () import Snap.Util.GZip import qualified Snap.Types.Headers as H stream2stream :: Iteratee Builder IO L.ByteString stream2stream = liftM (toLazyByteString . mconcat) consume ------------------------------------------------------------------------------ tests :: [Test] tests = [ testIdentity1 , testIdentity1_charset , testIdentity2 , testIdentity3 , testIdentity4 , testIdentity5 , testNoHeaders , testNoAcceptEncoding , testNopWhenContentEncodingSet , testCompositionDoesn'tExplode , testGzipLotsaChunks , testNoCompression , testBadHeaders ] ------------------------------------------------------------------------------ expectException :: IO a -> PropertyM IO () expectException m = do e <- liftQ $ try m case e of Left (z::SomeException) -> (show z) `seq` return () Right _ -> fail "expected exception, didn't get one" ------------------------------------------------------------------------------ liftQ :: forall a m . (Monad m) => m a -> PropertyM m a liftQ = QC.run ------------------------------------------------------------------------------ gzipHdrs, xGzipHdrs, badHdrs, deflateHdrs, xDeflateHdrs, emptyHdrs :: Headers emptyHdrs = H.empty gzipHdrs = setHeader "Accept-Encoding" "froz,gzip, x-gzip" emptyHdrs xGzipHdrs = setHeader "Accept-Encoding" "x-gzip;q=1.0" emptyHdrs badHdrs = setHeader "Accept-Encoding" "*&%^&^$%&%&*^\023" emptyHdrs deflateHdrs = setHeader "Accept-Encoding" "deflate" emptyHdrs xDeflateHdrs = setHeader "Accept-Encoding" "x-deflate" emptyHdrs ------------------------------------------------------------------------------ mkNoHeaders :: IO Request mkNoHeaders = do enum <- newIORef $ SomeEnumerator returnI return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False emptyHdrs enum Nothing GET (1,1) [] "/" "/" "/" "" Map.empty Map.empty Map.empty mkGzipRq :: IO Request mkGzipRq = do enum <- newIORef $ SomeEnumerator returnI return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False gzipHdrs enum Nothing GET (1,1) [] "/" "/" "/" "" Map.empty Map.empty Map.empty mkXGzipRq :: IO Request mkXGzipRq = do enum <- newIORef $ SomeEnumerator returnI return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False xGzipHdrs enum Nothing GET (1,1) [] "/" "/" "/" "" Map.empty Map.empty Map.empty ------------------------------------------------------------------------------ mkDeflateRq :: IO Request mkDeflateRq = do enum <- newIORef $ SomeEnumerator returnI return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False deflateHdrs enum Nothing GET (1,1) [] "/" "/" "/" "" Map.empty Map.empty Map.empty mkXDeflateRq :: IO Request mkXDeflateRq = do enum <- newIORef $ SomeEnumerator returnI return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False xDeflateHdrs enum Nothing GET (1,1) [] "/" "/" "/" "" Map.empty Map.empty Map.empty ------------------------------------------------------------------------------ mkBadRq :: IO Request mkBadRq = do enum <- newIORef $ SomeEnumerator returnI return $ Request "foo" 80 "foo" 999 "foo" 1000 "foo" False badHdrs enum Nothing GET (1,1) [] "/" "/" "/" "" Map.empty Map.empty Map.empty ------------------------------------------------------------------------------ seqSnap :: Snap a -> Snap a seqSnap m = do !x <- m return $! x `seq` x ------------------------------------------------------------------------------ goGeneric :: IO Request -> Snap a -> IO (Request, Response) goGeneric mkRq m = do rq <- mkRq run_ $! runSnap (seqSnap m) d d rq where d = (const $ return ()) goGZip, goDeflate, goXGZip :: Snap a -> IO (Request,Response) goNoHeaders, goXDeflate, goBad :: Snap a -> IO (Request,Response) goGZip = goGeneric mkGzipRq goDeflate = goGeneric mkDeflateRq goXGZip = goGeneric mkXGzipRq goXDeflate = goGeneric mkXDeflateRq goBad = goGeneric mkBadRq goNoHeaders = goGeneric mkNoHeaders ------------------------------------------------------------------------------ noContentType :: L.ByteString -> Snap () noContentType s = modifyResponse $ setResponseBody $ enumBuilder $ fromLazyByteString s ------------------------------------------------------------------------------ withContentType :: ByteString -> L.ByteString -> Snap () withContentType ct body = modifyResponse $ setResponseBody (enumBuilder $ fromLazyByteString body) . setContentType ct ------------------------------------------------------------------------------ textPlain :: L.ByteString -> Snap () textPlain = withContentType "text/plain" ------------------------------------------------------------------------------ binary :: L.ByteString -> Snap () binary = withContentType "application/octet-stream" ------------------------------------------------------------------------------ testNoHeaders :: Test testNoHeaders = testProperty "gzip/noheaders" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do -- if there's no content-type, withCompression should be a no-op (!_,!rsp) <- liftQ $ goNoHeaders (seqSnap $ withCompression $ noContentType s) assert $ getHeader "Content-Encoding" rsp == Nothing assert $ getHeader "Vary" rsp == Nothing let body = rspBodyToEnum $ rspBody rsp c <- liftQ $ runIteratee stream2stream >>= run_ . body assert $ s == c ------------------------------------------------------------------------------ testNoAcceptEncoding :: Test testNoAcceptEncoding = testProperty "gzip/noAcceptEncoding" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do -- if there's no accept-encoding, withCompression should be a no-op (!_,!rsp) <- liftQ $ goNoHeaders (seqSnap $ withCompression $ textPlain s) assert $ getHeader "Content-Encoding" rsp == Nothing assert $ getHeader "Vary" rsp == Nothing let body = rspBodyToEnum $ rspBody rsp c <- liftQ $ runIteratee stream2stream >>= run_ . body assert $ s == c ------------------------------------------------------------------------------ testIdentity1 :: Test testIdentity1 = testProperty "gzip/identity1" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do (!_,!rsp) <- liftQ $ goGZip (seqSnap $ withCompression $ textPlain s) assert $ getHeader "Content-Encoding" rsp == Just "gzip" assert $ getHeader "Vary" rsp == Just "Accept-Encoding" let body = rspBodyToEnum $ rspBody rsp c <- liftQ $ runIteratee stream2stream >>= run_ . body let s1 = GZip.decompress c assert $ s == s1 ------------------------------------------------------------------------------ testIdentity1_charset :: Test testIdentity1_charset = testProperty "gzip/identity1_charset" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do (!_,!rsp) <- liftQ $ goGZip (seqSnap $ withCompression $ withContentType "text/plain; charset=utf-8" s) assert $ getHeader "Content-Encoding" rsp == Just "gzip" assert $ getHeader "Vary" rsp == Just "Accept-Encoding" let body = rspBodyToEnum $ rspBody rsp c <- liftQ $ runIteratee stream2stream >>= run_ . body let s1 = GZip.decompress c assert $ s == s1 ------------------------------------------------------------------------------ testIdentity2 :: Test testIdentity2 = testProperty "gzip/identity2" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do (!_,!rsp) <- liftQ $ goDeflate (seqSnap $ withCompression $ textPlain s) assert $ getHeader "Content-Encoding" rsp == Just "deflate" assert $ getHeader "Vary" rsp == Just "Accept-Encoding" let body = rspBodyToEnum $ rspBody rsp c <- liftQ $ runIteratee stream2stream >>= run_ . body let s' = Zlib.decompress c assert $ s == s' ------------------------------------------------------------------------------ testIdentity3 :: Test testIdentity3 = testProperty "gzip/identity3" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do (!_,!rsp3) <- liftQ $ goGZip (seqSnap $ withCompression $ binary s) let body3 = rspBodyToEnum $ rspBody rsp3 s3 <- liftQ $ runIteratee stream2stream >>= run_ . body3 assert $ s == s3 ------------------------------------------------------------------------------ testIdentity4 :: Test testIdentity4 = testProperty "gzip/identity4" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do (!_,!rsp) <- liftQ $ goXGZip (seqSnap $ withCompression $ textPlain s) assert $ getHeader "Content-Encoding" rsp == Just "x-gzip" let body = rspBodyToEnum $ rspBody rsp c <- liftQ $ runIteratee stream2stream >>= run_ . body let s1 = GZip.decompress c assert $ s == s1 ------------------------------------------------------------------------------ testIdentity5 :: Test testIdentity5 = testProperty "gzip/identity5" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do (!_,!rsp2) <- liftQ $ goXDeflate (seqSnap $ withCompression $ textPlain s) assert $ getHeader "Content-Encoding" rsp2 == Just "x-deflate" let body2 = rspBodyToEnum $ rspBody rsp2 c2 <- liftQ $ runIteratee stream2stream >>= run_ . body2 let s2 = Zlib.decompress c2 assert $ s == s2 ------------------------------------------------------------------------------ testBadHeaders :: Test testBadHeaders = testProperty "gzip/bad headers" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = expectException $ do (!_,!rsp) <- goBad (seqSnap $ withCompression $ textPlain s) let body = rspBodyToEnum $ rspBody rsp runIteratee stream2stream >>= run_ . body ------------------------------------------------------------------------------ testNopWhenContentEncodingSet :: Test testNopWhenContentEncodingSet = testProperty "gzip/testNopWhenContentEncodingSet" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do (!_,!rsp) <- liftQ $ goGZip $ f s assert $ getHeader "Content-Encoding" rsp == Just "identity" f !s = seqSnap $ withCompression $ do modifyResponse $ setHeader "Content-Encoding" "identity" textPlain s ------------------------------------------------------------------------------ testCompositionDoesn'tExplode :: Test testCompositionDoesn'tExplode = testProperty "gzip/testCompositionDoesn'tExplode" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do (!_,!rsp) <- liftQ $ goGZip (seqSnap $ withCompression $ withCompression $ withCompression $ textPlain s) assert $ getHeader "Content-Encoding" rsp == Just "gzip" let body = rspBodyToEnum $ rspBody rsp c <- liftQ $ runIteratee stream2stream >>= run_ . body let s1 = GZip.decompress c assert $ s == s1 ------------------------------------------------------------------------------ testGzipLotsaChunks :: Test testGzipLotsaChunks = testCase "gzip/lotsOfChunks" prop where prop = do let s = L.take 120000 $ L.fromChunks $ frobnicate "dshflkahdflkdhsaflk" (!_,!rsp) <- goGZip (seqSnap $ withCompression $ textPlain s) let body = rspBodyToEnum $ rspBody rsp c <- runIteratee stream2stream >>= run_ . body let s1 = GZip.decompress c H.assertBool "streams equal" $ s == s1 -- in order to get incompressible text (so that we can test whether the -- gzip thread is streaming properly!) we'll iteratively md5 the source -- string frobnicate s = let s' = encode $ md5 $ L.fromChunks [s] in (s:frobnicate s') ------------------------------------------------------------------------------ testNoCompression :: Test testNoCompression = testProperty "gzip/noCompression" $ monadicIO $ forAllM arbitrary prop where prop :: L.ByteString -> PropertyM IO () prop s = do (!_,!rsp) <- liftQ $ goGZip (seqSnap $ withCompression $ (noCompression >> textPlain s)) assert $ getHeader "Content-Encoding" rsp == Just "identity" let body = rspBodyToEnum $ rspBody rsp s1 <- liftQ $ runIteratee stream2stream >>= run_ . body assert $ s == s1 snap-core-0.9.8.0/test/suite/Snap/Util/Proxy/0000755000000000000000000000000012565252520017016 5ustar0000000000000000snap-core-0.9.8.0/test/suite/Snap/Util/Proxy/Tests.hs0000644000000000000000000001260012565252520020453 0ustar0000000000000000{-# LANGUAGE BangPatterns #-} {-# LANGUAGE OverloadedStrings #-} module Snap.Util.Proxy.Tests (tests) where ------------------------------------------------------------------------------ import Control.Monad.State hiding (get) import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as S import Data.CaseInsensitive (CI(..)) import qualified Data.Map as Map import Test.Framework import Test.Framework.Providers.HUnit import Test.HUnit hiding (Test, path) ------------------------------------------------------------------------------ import Snap.Core hiding (setHeader) import Snap.Test import Snap.Test.Common import Snap.Util.Proxy ------------------------------------------------------------------------------ ------------------------------------------------------------------------------ tests :: [Test] tests = [ testNoProxy , testForwardedFor , testTrivials ] --------------- -- Constants -- --------------- ------------------------------------------------------------------------------ initialPort :: Int initialPort = 9999 initialAddr :: ByteString initialAddr = "127.0.0.1" ----------- -- Tests -- ----------- ------------------------------------------------------------------------------ testNoProxy :: Test testNoProxy = testCase "proxy/no-proxy" $ do a <- evalHandler (mkReq $ forwardedFor [("4.3.2.1", Nothing)]) (behindProxy NoProxy reportRemoteAddr) p <- evalHandler (mkReq $ forwardedFor [("4.3.2.1", Nothing)]) (behindProxy NoProxy reportRemotePort) assertEqual "NoProxy leaves request alone" initialAddr a assertEqual "NoProxy leaves request alone" initialPort p -------------------------------------------------------------------------- b <- evalHandler (mkReq $ xForwardedFor [("4.3.2.1", Nothing)]) (behindProxy NoProxy reportRemoteAddr) assertEqual "NoProxy leaves request alone" initialAddr b -------------------------------------------------------------------------- c <- evalHandler (mkReq $ return ()) (behindProxy NoProxy reportRemoteAddr) assertEqual "NoProxy leaves request alone" initialAddr c ------------------------------------------------------------------------------ testForwardedFor :: Test testForwardedFor = testCase "proxy/forwarded-for" $ do (a,p) <- evalHandler (mkReq $ return ()) handler assertEqual "No Forwarded-For, no change" initialAddr a assertEqual "port" initialPort p -------------------------------------------------------------------------- (b,_) <- evalHandler (mkReq $ forwardedFor addr) handler assertEqual "Behind 5.6.7.8" ip b -------------------------------------------------------------------------- (c,q) <- evalHandler (mkReq $ xForwardedFor addrs2) handler assertEqual "Behind 5.6.7.8" ip c assertEqual "port change" port q where handler = behindProxy X_Forwarded_For $ do !a <- reportRemoteAddr !p <- reportRemotePort return $! (a,p) ip = "5.6.7.8" port = 10101 addr = [ (ip, Nothing) ] addr2 = [ (ip, Just port) ] addrs2 = [("4.3.2.1", Just 20202)] ++ addr2 ------------------------------------------------------------------------------ testTrivials :: Test testTrivials = testCase "proxy/trivials" $ do coverShowInstance NoProxy coverReadInstance NoProxy coverEqInstance NoProxy coverOrdInstance NoProxy --------------- -- Functions -- --------------- ------------------------------------------------------------------------------ mkReq :: RequestBuilder IO () -> RequestBuilder IO () mkReq m = do get "/" Map.empty modify $ \req -> req { rqRemoteAddr = initialAddr , rqRemotePort = initialPort } m ------------------------------------------------------------------------------ reportRemoteAddr :: Snap ByteString reportRemoteAddr = withRequest $ \req -> return $ rqRemoteAddr req ------------------------------------------------------------------------------ reportRemotePort :: Snap Int reportRemotePort = withRequest $ \req -> return $ rqRemotePort req ------------------------------------------------------------------------------ forwardedFor' :: CI ByteString -- ^ header name -> [(ByteString, Maybe Int)] -- ^ list of "forwarded-for" -> RequestBuilder IO () forwardedFor' hdr addrs = do setHeader hdr out where toStr (a, Nothing) = a toStr (a, Just p ) = S.concat [ a, ":", S.pack $ show p ] out = S.intercalate ", " $ map toStr addrs ------------------------------------------------------------------------------ forwardedFor :: [(ByteString, Maybe Int)] -> RequestBuilder IO () forwardedFor = forwardedFor' "Forwarded-For" ------------------------------------------------------------------------------ xForwardedFor :: [(ByteString, Maybe Int)] -> RequestBuilder IO () xForwardedFor = forwardedFor' "X-Forwarded-For" snap-core-0.9.8.0/test/data/0000755000000000000000000000000012565252520013637 5ustar0000000000000000snap-core-0.9.8.0/test/data/fileServe/0000755000000000000000000000000012565252520015563 5ustar0000000000000000snap-core-0.9.8.0/test/data/fileServe/foo.bin0000644000000000000000000000000412565252520017032 0ustar0000000000000000FOO snap-core-0.9.8.0/test/data/fileServe/foo.bin.bin.bin0000644000000000000000000000000412565252520020350 0ustar0000000000000000FOO snap-core-0.9.8.0/test/data/fileServe/foo.html0000644000000000000000000000000412565252520017226 0ustar0000000000000000FOO snap-core-0.9.8.0/test/data/fileServe/foo.txt0000644000000000000000000000000412565252520017101 0ustar0000000000000000FOO snap-core-0.9.8.0/test/data/fileServe/mydir1/0000755000000000000000000000000012565252520016770 5ustar0000000000000000snap-core-0.9.8.0/test/data/fileServe/mydir1/index.txt0000644000000000000000000000000612565252520020634 0ustar0000000000000000INDEX snap-core-0.9.8.0/test/data/fileServe/mydir2/0000755000000000000000000000000012565252520016771 5ustar0000000000000000snap-core-0.9.8.0/test/data/fileServe/mydir2/foo.txt0000644000000000000000000000000412565252520020307 0ustar0000000000000000FOO snap-core-0.9.8.0/test/data/fileServe/mydir3/0000755000000000000000000000000012565252520016772 5ustar0000000000000000snap-core-0.9.8.0/test/data/fileServe/mydir3/altindex.html0000644000000000000000000000001112565252520021460 0ustar0000000000000000ALTINDEX snap-core-0.9.8.0/extra/0000755000000000000000000000000012565252520013072 5ustar0000000000000000snap-core-0.9.8.0/extra/haddock.css0000644000000000000000000002016112565252520015201 0ustar0000000000000000HTML { background-color: #f0f3ff; width: 100%; } BODY { -moz-border-radius:5px; -webkit-border-radius:5px; width: 50em; margin: 2em auto; padding: 0; background-color: #ffffff; color: #000000; font-size: 110%; font-family: Georgia, serif; } A:link { color: #5200A3; text-decoration: none } A:visited { color: #5200A3; text-decoration: none } A:hover { color: #5200A3; text-decoration: none; border-bottom:#5200A3 dashed 1px; } TABLE.vanilla { width: 100%; border-width: 0px; /* I can't seem to specify cellspacing or cellpadding properly using CSS... */ } DL { font-family: "Gill Sans", "Helvetica Neue","Arial",sans-serif; letter-spacing: -0.01em; margin: 0; } .vanilla .vanilla dl { font-size: 80%; } .vanilla .vanilla dl dl { padding-left: 0; font-size: 95%; } TD.section1, TD.section2, TD.section3, TD.section4, TD.doc, DL { padding: 0 30px 0 34px; } TABLE.vanilla2 { font-family: "Gill Sans", "Helvetica Neue","Arial",sans-serif; border-width: 0px; } /* font is a little too small in MSIE */ TT, PRE, CODE { font-family: Monaco, "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Lucida Console", monospace; font-size: 90%; } LI P { margin: 0pt } P { margin-top: 0; margin-bottom: 0.75em; } TD { border-width: 0px; } TABLE.narrow { border-width: 0px; } TD.s8 { height: 0; margin:0; padding: 0 } TD.s15 { height: 20px; } SPAN.keyword { text-decoration: underline; } /* Resize the buttom image to match the text size */ IMG.coll { width : 0.75em; height: 0.75em; margin-bottom: 0; margin-right: 0.5em } /* --------- Contents page ---------- */ DIV.node { padding-left: 3em; } DIV.cnode { padding-left: 1.75em; } SPAN.pkg { position: absolute; left: 50em; } /* --------- Documentation elements ---------- */ TD FONT { font-weight: bold; letter-spacing: -0.02em; } TD.children { padding-left: 25px; } TD.synopsis { padding: 2px; background-color: #f0f0f0; font-size: 80%; font-family: Monaco, "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Lucida Console", monospace; } TD.decl { padding: 4px 8px; background-color: #FAFAFA; border-bottom: #F2F2F2 solid 1px; border-top: #FCFCFC solid 1px; font-size: 80%; font-family: Monaco, "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Lucida Console", monospace; vertical-align: top; } TD.decl TD.decl { font-size: 100%; padding: 4px 0; border: 0; } TD.topdecl { padding: 20px 30px 0.5ex 30px; font-size: 80%; font-family: Monaco, "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Lucida Console", monospace; ; vertical-align: top; } .vanilla .vanilla .vanilla .topdecl { padding-left: 0; padding-right: 0; } .vanilla .vanilla .vanilla { padding-left: 30px; } .decl .vanilla { padding-left: 0px !important; } .body .vanilla .body { padding-left: 0; padding-right: 0; } .body .vanilla .body .decl { padding-left: 12px; } .body .vanilla .body div .vanilla .decl { padding-left: 12px; } TABLE.declbar { background-color: #f0f0f0; border-spacing: 0px; border-bottom:1px solid #d7d7df; border-right:1px solid #d7d7df; border-top:1px solid #f4f4f9; border-left:1px solid #f4f4f9; padding: 4px; } TD.declname { width: 100%; padding-right: 4px; } TD.declbut { padding-left: 8px; padding-right: 5px; border-left-width: 1px; border-left-color: #000099; border-left-style: solid; white-space: nowrap; font-size: x-small; } /* arg is just like decl, except that wrapping is not allowed. It is used for function and constructor arguments which have a text box to the right, where if wrapping is allowed the text box squashes up the declaration by wrapping it. */ TD.arg { padding: 2px 12px; background-color: #f0f0f0; font-size: 80%; font-family: Monaco, "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Lucida Console", monospace; vertical-align: top; white-space: nowrap; } TD.recfield { padding-left: 20px } TD.doc { padding-left: 38px; font-size: 95%; line-height: 1.66; } TD.ndoc { font-size: 95%; line-height: 1.66; padding: 2px 4px 2px 8px; } TD.rdoc { padding: 2px; padding-left: 30px; width: 100%; font-size: 80%; font-style: italic; font-family: "Gill Sans", "Helvetica Neue","Arial",sans-serif; } TD.body { padding: 0 30px; } TD.pkg { width: 100%; padding-left: 30px } TABLE.indexsearch TR.indexrow { display: none; } TABLE.indexsearch TR.indexshow { display: table-row; } TD.indexentry { vertical-align: top; padding: 0 30px } TD.indexannot { vertical-align: top; padding-left: 20px; white-space: nowrap } TD.indexlinks { width: 100% } /* ------- Section Headings ------- */ TD.section1, TD.section2, TD.section3, TD.section4, TD.section5 { font-family: "Gill Sans", "Helvetica Neue","Arial",sans-serif; } TD.section1 { padding-top: 14px; font-weight: bold; letter-spacing: -0.02em; font-size: 140% } TD.section2 { padding-top: 4px; font-weight: bold; letter-spacing: -0.02em; font-size: 120% } TD.section3 { padding-top: 5px; font-weight: bold; letter-spacing: -0.02em; font-size: 105% } TD.section4 { font-weight: bold; padding-top: 12px; padding-bottom: 4px; letter-spacing: -0.02em; font-size: 90% } /* -------------- The title bar at the top of the page */ TD.infohead { font-family: "Gill Sans", "Helvetica Neue","Arial",sans-serif; color: #ffffff; font-weight: bold; padding: 0 30px; text-align: left; } TD.infoval { font-family: "Gill Sans", "Helvetica Neue","Arial",sans-serif; color: #ffffff; padding: 0 30px; text-align: left; } TD.topbar { font-family: "Gill Sans", "Helvetica Neue","Arial",sans-serif; background-color: #3465a4; padding: 5px; -moz-border-radius-topleft:5px; -moz-border-radius-topright:5px; -webkit-border-radius-topleft:5px; -webkit-border-radius-topright:5px; } TD.title { font-family: "Gill Sans", "Helvetica Neue","Arial",sans-serif; color: #ffffff; padding-left: 30px; letter-spacing: -0.02em; font-weight: bold; width: 100% } TD.topbut { font-family: "Gill Sans", "Helvetica Neue","Arial",sans-serif; padding-left: 5px; padding-right: 5px; border-left-width: 1px; border-left-color: #ffffff; border-left-style: solid; letter-spacing: -0.02em; font-weight: bold; white-space: nowrap; } TD.topbut A:link { color: #ffffff } TD.topbut A:visited { color: #ffff00 } TD.topbut A:hover { background-color: #C9D3DE; } TD.topbut:hover { background-color: #C9D3DE; } TD.modulebar { font-family: "Gill Sans", "Helvetica Neue","Arial",sans-serif; color: #141B24; background-color: #C9D3DE; padding: 5px; border-top-width: 1px; border-top-color: #ffffff; border-top-style: solid; -moz-border-radius-bottomleft:5px; -moz-border-radius-bottomright:5px; -webkit-border-radius-bottomleft:5px; -webkit-border-radius-bottomright:5px; } /* --------- The page footer --------- */ TD.botbar { font-family: "Gill Sans", "Helvetica Neue","Arial",sans-serif; -moz-border-radius:5px; -webkit-border-radius:5px; background-color: #3465a4; color: #ffffff; padding: 5px } TD.botbar A:link { color: #ffffff; text-decoration: underline } TD.botbar A:visited { color: #ffff00 } TD.botbar A:hover { background-color: #6060ff } /* --------- Mini Synopsis for Frame View --------- */ .outer { margin: 0 0; padding: 0 0; } .mini-synopsis { padding: 0.25em 0.25em; } .mini-synopsis H1 { font-size: 120%; } .mini-synopsis H2 { font-size: 107%; } .mini-synopsis H3 { font-size: 100%; } .mini-synopsis H1, .mini-synopsis H2, .mini-synopsis H3 { font-family: "Gill Sans", "Helvetica Neue","Arial",sans-serif; margin-top: 0.5em; margin-bottom: 0.25em; padding: 0 0; font-weight: bold; letter-spacing: -0.02em; } .mini-synopsis H1 { border-bottom: 1px solid #ccc; } .mini-topbar { font-size: 120%; background: #0077dd; padding: 0.25em; } snap-core-0.9.8.0/extra/hscolour.css0000644000000000000000000000073712565252520015451 0ustar0000000000000000body { font-size: 90%; } pre, code, body { font-family: Monaco, "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Lucida Console", monospace; } .hs-keyglyph, .hs-layout {color: #5200A3;} .hs-keyword {color: #3465a4; font-weight: bold;} .hs-comment, .hs-comment a {color: #579; } .hs-str, .hs-chr {color: #141B24;} .hs-keyword, .hs-conid, .hs-varid, .hs-conop, .hs-varop, .hs-num, .hs-cpp, .hs-sel, .hs-definition {} snap-core-0.9.8.0/extra/logo.gif0000644000000000000000000000113712565252520014523 0ustar0000000000000000GIF89a` J"K#M%N(P1W1X3Z6\?d4e=i9i:j