]> gitweb @ CieloNegro.org - Lucu.git/blobdiff - Network/HTTP/Lucu/Resource.hs
Make use of mimeType quasi-quoter.
[Lucu.git] / Network / HTTP / Lucu / Resource.hs
index 883cc149188a404f2007425cf88d6bcfc8a2b1d8..704feda9c79ca2e5ab4619b1550166bdd8023f4c 100644 (file)
+{-# LANGUAGE
+    BangPatterns
+  , GeneralizedNewtypeDeriving
+  , DoAndIfThenElse
+  , OverloadedStrings
+  , QuasiQuotes
+  , RecordWildCards
+  , UnicodeSyntax
+  #-}
+-- |This is the Resource Monad; monadic actions to define a behavior
+-- of resource. The 'Resource' Monad is a kind of 'IO' Monad thus it
+-- implements 'MonadIO' class, and it is a state machine as well.
+-- 
+-- Request Processing Flow:
+--
+--   1. A client issues an HTTP request.
+--
+--   2. If the URI of it matches to any resource, the corresponding
+--      'Resource' Monad starts running on a newly spawned thread.
+--
+--   3. The 'Resource' Monad looks at request headers, find (or not
+--      find) an entity, receive the request body (if any), send
+--      response headers, and then send a response body. This process
+--      will be discussed later.
+--
+--   4. The 'Resource' Monad and its thread stops running. The client
+--      may or may not be sending us the next request at this point.
+--
+-- 'Resource' Monad takes the following states. The initial state is
+-- /Examining Request/ and the final state is /Done/.
+--
+--   [/Examining Request/] In this state, a 'Resource' looks at the
+--   request header fields and thinks about the corresponding entity
+--   for it. If there is a suitable entity, the 'Resource' tells the
+--   system an entity tag and its last modification time
+--   ('foundEntity'). If it found no entity, it tells the system so
+--   ('foundNoEntity'). In case it is impossible to decide the
+--   existence of entity, which is a typical case for POST requests,
+--   'Resource' does nothing in this state.
+--
+--   [/Receiving Body/] A 'Resource' asks the system to receive a
+--   request body from the client. Before actually reading from the
+--   socket, the system sends \"100 Continue\" to the client if need
+--   be. When a 'Resource' transits to the next state without
+--   receiving all or part of a request body, the system automatically
+--   discards it.
+--
+--   [/Deciding Header/] A 'Resource' makes a decision of response
+--   status code and header fields. When it transits to the next
+--   state, the system validates and completes the header fields and
+--   then sends them to the client.
+--
+--   [/Sending Body/] In this state, a 'Resource' asks the system to
+--   write some response body to the socket. When it transits to the
+--   next state without writing any response body, the system
+--   automatically completes it depending on the status code. (To be
+--   exact, such completion only occurs when the 'Resource' transits
+--   to this state without even declaring the \"Content-Type\" header
+--   field. See: 'setContentType')
+--
+--   [/Done/] Everything is over. A 'Resource' can do nothing for the
+--   HTTP interaction anymore.
+--
+-- Note that the state transition is one-way: for instance, it is an
+-- error to try to read a request body after writing some
+-- response. This limitation is for efficiency. We don't want to read
+-- the entire request before starting 'Resource', nor we don't want to
+-- postpone writing the entire response till the end of 'Resource'
+-- computation.
 module Network.HTTP.Lucu.Resource
-    ( ResourceDef(..)
-    , Resource
-    , ResTree
-    , mkResTree    -- [ ([String], ResourceDef) ] -> ResTree
-    , findResource -- ResTree -> URI -> Maybe ResourceDef
-    , runResource  -- ResourceDef -> Interaction -> IO ThreadId
-
-    , getMethod -- Resource Method
-    , getHeader -- String -> Resource (Maybe String)
-
-    , foundEntity    -- Bool -> String -> ClockTime -> Resource ()
-    , foundETag      -- Bool -> String -> Resource ()
-    , foundTimeStamp -- ClockTime -> Resource ()
-    , foundNoEntity  -- Maybe String -> Resource ()
-
-    , input        -- Int -> Resource String
-    , inputChunk   -- Int -> Resource String
-    , inputBS      -- Int -> Resource ByteString
-    , inputChunkBS -- Int -> Resource ByteString
-    , defaultLimit -- Int
-
-    , setStatus -- StatusCode -> Resource ()
-    , setHeader -- String -> String -> Resource ()
-    , redirect  -- StatusCode -> URI -> Resource ()
-    , setETag   -- Bool -> String -> Resource ()
-    , setLastModified -- ClockTime -> Resource ()
-
-    , output        -- String -> Resource ()
-    , outputChunk   -- String -> Resource ()
-    , outputBS      -- ByteString -> Resource ()
-    , outputChunkBS -- ByteString -> Resource ()
+    (
+    -- * Types
+      Resource
+    , ResourceDef(..)
+    , emptyResource
+    , FormData(..)
+
+    -- * Getting request header
+    -- |These functions can be called regardless of the current state,
+    -- and they don't change the state of 'Resource'.
+    , getConfig
+    , getRemoteAddr
+    , getRemoteAddr'
+    , getRemoteHost
+    , getRemoteCertificate
+    , getRequest
+    , getMethod
+    , getRequestURI
+    , getRequestVersion
+    , getResourcePath
+    , getPathInfo
+    , getQueryForm
+    , getHeader
+    , getAccept
+    , getAcceptEncoding
+    , isEncodingAcceptable
+    , getContentType
+    , getAuthorization
+
+    -- * Finding an entity
+    -- |These functions can be called only in the /Examining Request/
+    -- state. They make the 'Resource' transit to the /Receiving Body/
+    -- state.
+    , foundEntity
+    , foundETag
+    , foundTimeStamp
+    , foundNoEntity
+    , foundNoEntity'
+
+    -- * Receiving a request body
+    -- |These functions make the 'Resource' transit to the /Receiving
+    -- Body/ state.
+    , getChunk
+    , getChunks
+    , getForm
+
+    -- * Declaring response status and header fields
+    -- |These functions can be called at any time before transiting to
+    -- the /Sending Body/ state, but they themselves never causes any
+    -- state transitions.
+    , setStatus
+    , redirect
+    , setContentType
+    , setContentEncoding
+    , setWWWAuthenticate
+
+    -- ** Less frequently used functions
+    , setLocation
+    , setHeader
+    , deleteHeader
+
+    -- * Sending a response body
+
+    -- |These functions make the 'Resource' transit to the
+    -- /Sending Body/ state.
+    , putChunk
+    , putChunks
+    , putBuilder
     )
     where
-
-import           Control.Concurrent
-import           Control.Concurrent.STM
-import           Control.Exception
-import           Control.Monad.Reader
-import qualified Data.ByteString.Lazy.Char8 as B
-import           Data.ByteString.Lazy.Char8 (ByteString)
-import           Data.Dynamic
-import           Data.List
+import Blaze.ByteString.Builder (Builder)
+import qualified Blaze.ByteString.Builder as BB
+import qualified Blaze.ByteString.Builder.Internal as BB
+import Control.Applicative
+import Control.Arrow
+import Control.Monad
+import Control.Monad.IO.Class
+import Control.Monad.Unicode
+import Data.Ascii (Ascii, CIAscii)
+import qualified Data.Ascii as A
+import qualified Data.Attoparsec.Char8 as P
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as Strict
+import qualified Data.ByteString.Lazy as Lazy
+import Data.List
 import qualified Data.Map as M
-import           Data.Map (Map)
-import           Data.Maybe
-import           GHC.Conc (unsafeIOToSTM)
-import           Network.HTTP.Lucu.Abortion
-import           Network.HTTP.Lucu.Config
-import           Network.HTTP.Lucu.DefaultPage
-import           Network.HTTP.Lucu.ETag
+import Data.Maybe
+import Data.Monoid
+import Data.Monoid.Unicode
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Time
+import qualified Data.Time.HTTP as HTTP
+import Network.HTTP.Lucu.Abortion
+import Network.HTTP.Lucu.Authentication
+import Network.HTTP.Lucu.Config
+import Network.HTTP.Lucu.ContentCoding
+import Network.HTTP.Lucu.ETag
 import qualified Network.HTTP.Lucu.Headers as H
-import           Network.HTTP.Lucu.HttpVersion
-import           Network.HTTP.Lucu.Interaction
-import           Network.HTTP.Lucu.Parser
-import           Network.HTTP.Lucu.Postprocess
-import           Network.HTTP.Lucu.RFC1123DateTime
-import           Network.HTTP.Lucu.Request
-import           Network.HTTP.Lucu.Response
-import           Network.HTTP.Lucu.Utils
-import           Network.URI
-import           Prelude hiding (catch)
-import           System.IO
-import           System.IO.Error hiding (catch)
-import           System.Time
-
-
-type Resource a = ReaderT Interaction IO a
-
-
-{- /aaa/bbb/ccc にアクセスされた時、もし /aaa/bbb に貪欲なリソースがあ
-   れば、假に /aaa/bbb/ccc に何らかのリソースがあったとしても必ず
-   /aaa/bbb が撰ばれる。/aaa/bbb のリソースが貪欲でなければ、それは無視
-   される。 -}
-data ResourceDef = ResourceDef {
-      resUsesNativeThread :: Bool
-    , resIsGreedy         :: Bool
-    , resGet              :: Maybe (Resource ())
-    , resHead             :: Maybe (Resource ())
-    , resPost             :: Maybe (Resource ())
-    , resPut              :: Maybe (Resource ())
-    , resDelete           :: Maybe (Resource ())
-    }
-type ResTree    = ResNode -- root だから Map ではない
-type ResSubtree = Map String ResNode
-data ResNode    = ResNode (Maybe ResourceDef) ResSubtree
-
-
-mkResTree :: [ ([String], ResourceDef) ] -> ResTree
-mkResTree list = processRoot list
+import Network.HTTP.Lucu.HttpVersion
+import Network.HTTP.Lucu.Interaction
+import Network.HTTP.Lucu.MultipartForm
+import Network.HTTP.Lucu.Parser
+import Network.HTTP.Lucu.Request
+import Network.HTTP.Lucu.Resource.Internal
+import Network.HTTP.Lucu.Response
+import Network.HTTP.Lucu.MIMEParams
+import Network.HTTP.Lucu.MIMEType (MIMEType(..))
+import qualified Network.HTTP.Lucu.MIMEType as MT
+import Network.HTTP.Lucu.MIMEType.TH
+import Network.HTTP.Lucu.Utils
+import Network.Socket hiding (accept)
+import Network.URI hiding (path)
+import Prelude.Unicode
+
+-- |Get the string representation of the address of remote host. If
+-- you want a 'SockAddr' instead of 'HostName', use 'getRemoteAddr'.
+getRemoteAddr' ∷ Resource HostName
+getRemoteAddr' = liftIO ∘ toNM =≪ getRemoteAddr
     where
-      processRoot :: [ ([String], ResourceDef) ] -> ResTree
-      processRoot list
-          = let (roots, nonRoots) = partition (\ (path, _) -> path == []) list
-                children = processNonRoot nonRoots
-            in
-              if null roots then
-                  -- / にリソースが定義されない。/foo とかにはあるかも。
-                  ResNode Nothing children
-              else
-                  -- / がある。
-                  let (_, def) = last roots
-                  in 
-                    ResNode (Just def) children
-
-      processNonRoot :: [ ([String], ResourceDef) ] -> ResSubtree
-      processNonRoot list
-          = let subtree    = M.fromList [(name, node name)
-                                             | name <- childNames]
-                childNames = [name | (name:_, _) <- list]
-                node name  = let defs = [def | (path, def) <- list, path == [name]]
-                             in
-                               if null defs then
-                                   -- この位置にリソースが定義されない。
-                                   -- もっと下にはあるかも。
-                                   ResNode Nothing children
-                               else
-                                   -- この位置にリソースがある。
-                                   ResNode (Just $ last defs) children
-                children   = processNonRoot [(path, def)
-                                                 | (_:path, def) <- list, not (null path)]
-            in
-              subtree
-
-
-findResource :: ResTree -> URI -> Maybe ResourceDef
-findResource (ResNode rootDefM subtree) uri
-    = let pathStr = uriPath uri
-          path    = [x | x <- splitBy (== '/') pathStr, x /= ""]
-      in
-        if null path then
-            rootDefM
-        else
-            walkTree subtree path
+      toNM ∷ SockAddr → IO HostName
+      toNM = (fromJust ∘ fst <$>) ∘ getNameInfo [NI_NUMERICHOST] True False
+
+-- |Resolve an address to the remote host.
+getRemoteHost ∷ Resource (Maybe HostName)
+getRemoteHost = liftIO ∘ getHN =≪ getRemoteAddr
     where
-      walkTree :: ResSubtree -> [String] -> Maybe ResourceDef
-
-      walkTree subtree (name:[])
-          = case M.lookup name subtree of
-              Nothing               -> Nothing
-              Just (ResNode defM _) -> defM
-
-      walkTree subtree (x:xs)
-          = case M.lookup x subtree of
-              Nothing                      -> Nothing
-              Just (ResNode defM children) -> case defM of
-                                                Just (ResourceDef { resIsGreedy = True })
-                                                    -> defM
-                                                _   -> walkTree children xs
-
-
-runResource :: ResourceDef -> Interaction -> IO ThreadId
-runResource def itr
-    = fork
-      $ catch ( runReaderT ( do fromMaybe notAllowed rsrc 
-                                driftTo Done
-                           ) itr
-              )
-      $ \ exc -> processException (itrConfig itr) exc
+      getHN ∷ SockAddr → IO (Maybe HostName)
+      getHN = (fst <$>) ∘ getNameInfo [] True False
+
+-- |Get the 'Method' value of the request.
+getMethod ∷ Resource Method
+getMethod = reqMethod <$> getRequest
+
+-- |Get the URI of the request.
+getRequestURI ∷ Resource URI
+getRequestURI = reqURI <$> getRequest
+
+-- |Get the HTTP version of the request.
+getRequestVersion ∷ Resource HttpVersion
+getRequestVersion = reqVersion <$> getRequest
+
+-- |This is an analogy of CGI PATH_INFO. 'getPathInfo' always returns
+-- @[]@ if the corresponding
+-- 'Network.HTTP.Lucu.Resource.Tree.ResourceDef' is not greedy. See:
+-- 'getResourcePath'
+--
+-- Note that the returned path components are URI-decoded.
+getPathInfo ∷ Resource [Strict.ByteString]
+getPathInfo = do rsrcPath ← getResourcePath
+                 reqPath  ← splitPathInfo <$> getRequestURI
+                 return $ drop (length rsrcPath) reqPath
+
+-- |Assume the query part of request URI as
+-- application\/x-www-form-urlencoded, and parse it into pairs of
+-- @(name, formData)@. This function doesn't read the request
+-- body.
+getQueryForm ∷ Resource [(Strict.ByteString, FormData)]
+getQueryForm = parse' <$> getRequestURI
     where
-      fork :: IO () -> IO ThreadId
-      fork = if (resUsesNativeThread def)
-             then forkOS
-             else forkIO
-      
-      rsrc :: Maybe (Resource ())
-      rsrc = case reqMethod $ fromJust $ itrRequest itr of
-               GET    -> resGet def
-               HEAD   -> case resHead def of
-                           Just r  -> Just r
-                           Nothing -> resGet def
-               POST   -> resPost def
-               PUT    -> resPut def
-               DELETE -> resDelete def
-
-      notAllowed :: Resource ()
-      notAllowed = do setStatus MethodNotAllowed
-                      setHeader "Allow" $ joinWith ", " allowedMethods
-
-      allowedMethods :: [String]
-      allowedMethods = nub $ foldr (++) [] [ methods resGet    ["GET"]
-                                           , methods resHead   ["GET", "HEAD"]
-                                           , methods resPost   ["POST"]
-                                           , methods resPut    ["PUT"]
-                                           , methods resDelete ["DELETE"]
-                                           ]
-
-      methods :: (ResourceDef -> Maybe a) -> [String] -> [String]
-      methods f xs = case f def of
-                       Just _  -> xs
-                       Nothing -> []
-
-      processException :: Config -> Exception -> IO ()
-      processException conf exc
-          = do let abo = case exc of
-                           ErrorCall    msg  -> Abortion InternalServerError [] msg
-                           IOException  ioE  -> Abortion InternalServerError [] $ formatIOE ioE
-                           DynException dynE -> case fromDynamic dynE of
-                                                  Just (abo :: Abortion) -> abo
-                                                  Nothing
-                                                      -> Abortion InternalServerError []
-                                                         $ show exc
-                           _                 -> Abortion InternalServerError [] $ show exc
-               -- まだ DecidingHeader 以前の状態だったら、この途中終了
-               -- を應答に反映させる餘地がある。さうでなければ stderr
-               -- にでも吐くしか無い。
-               state <- atomically $ readItr itr itrState id
-               if state <= DecidingHeader then
-                   flip runReaderT itr
-                      $ do setStatus $ aboStatus abo
-                           -- FIXME: 同じ名前で複數の値があった時は、こ
-                           -- れではまずいと思ふ。
-                           mapM_ (\ (name, value) -> setHeader name value) $ aboHeaders abo
-                           setHeader "Content-Type" "application/xhtml+xml"
-                           output $ aboPage conf abo
+      parse' = map toPairWithFormData ∘
+               parseWWWFormURLEncoded ∘
+               fromJust ∘
+               A.fromChars ∘
+               drop 1 ∘
+               uriQuery
+
+toPairWithFormData ∷ (ByteString, ByteString) → (Strict.ByteString, FormData)
+toPairWithFormData (name, value)
+    = let fd = FormData {
+                 fdFileName = Nothing
+               , fdMIMEType = [mimeType| text/plain |]
+               , fdContent  = Lazy.fromChunks [value]
+               }
+      in (name, fd)
+
+-- |@'getHeader' name@ returns the value of the request header field
+-- @name@. Comparison of header name is case-insensitive. Note that
+-- this function is not intended to be used so frequently: there
+-- should be functions like 'getContentType' for every common headers.
+getHeader ∷ CIAscii → Resource (Maybe Ascii)
+getHeader name
+    = H.getHeader name <$> getRequest
+
+-- |Return the list of 'MIMEType' enumerated on the value of request
+-- header \"Accept\", or @[]@ if absent.
+getAccept ∷ Resource [MIMEType]
+getAccept
+    = do acceptM ← getHeader "Accept"
+         case acceptM of
+           Nothing
+               → return []
+           Just accept
+               → case P.parseOnly (finishOff MT.mimeTypeList) (A.toByteString accept) of
+                    Right xs → return xs
+                    Left  _  → abort $ mkAbortion' BadRequest
+                                     $ "Unparsable Accept: " ⊕ A.toText accept
+
+-- |Return the list of @(contentCoding, qvalue)@ enumerated on the
+-- value of request header \"Accept-Encoding\". The list is sorted in
+-- descending order by qvalue.
+getAcceptEncoding ∷ Resource [(CIAscii, Maybe Double)]
+getAcceptEncoding
+    = do accEncM ← getHeader "Accept-Encoding"
+         case accEncM of
+           Nothing
+               -- HTTP/1.0 には Accept-Encoding が無い場合の規定が無い
+               -- ので安全の爲 identity が指定された事にする。HTTP/1.1
+               -- の場合は何でも受け入れて良い事になってゐるので "*" が
+               -- 指定された事にする。
+               → do ver ← getRequestVersion
+                    case ver of
+                      HttpVersion 1 0 → return [("identity", Nothing)]
+                      HttpVersion 1 1 → return [("*"       , Nothing)]
+                      _               → abort $ mkAbortion' InternalServerError
+                                                "getAcceptEncoding: unknown HTTP version"
+           Just ae
+               → if ae ≡ "" then
+                      -- identity のみが許される。
+                      return [("identity", Nothing)]
                  else
-                   hPutStrLn stderr $ show abo
-
-               flip runReaderT itr $ driftTo Done
-
-      formatIOE :: IOError -> String
-      formatIOE ioE = if isUserError ioE then
-                          ioeGetErrorString ioE
-                      else
-                          show ioE
-
-
-getMethod :: Resource Method
-getMethod = do itr <- ask
-               return $ reqMethod $ fromJust $ itrRequest itr
-
-
-getHeader :: String -> Resource (Maybe String)
-getHeader name = do itr <- ask
-                    return $ H.getHeader name $ fromJust $ itrRequest itr
-
-
-{- ExaminingRequest 時に使用するアクション群 -}
+                     case P.parseOnly (finishOff acceptEncodingList) (A.toByteString ae) of
+                       Right xs → return $ map toTuple $ reverse $ sort xs
+                       Left  _  → abort $ mkAbortion' BadRequest
+                                        $ "Unparsable Accept-Encoding: " ⊕ A.toText ae
+    where
+      toTuple (AcceptEncoding {..})
+          = (aeEncoding, aeQValue)
 
-foundEntity :: Bool -> String -> ClockTime -> Resource ()
-foundEntity isWeak token timeStamp
+-- |Return 'True' iff a given content-coding is acceptable by the
+-- client.
+isEncodingAcceptable ∷ CIAscii → Resource Bool
+isEncodingAcceptable encoding = any doesMatch <$> getAcceptEncoding
+    where
+      doesMatch ∷ (CIAscii, Maybe Double) → Bool
+      doesMatch (e, q) = (e ≡ "*" ∨ e ≡ encoding) ∧ q ≢ Just 0
+
+-- |Return the value of request header \"Content-Type\" as 'MIMEType'.
+getContentType ∷ Resource (Maybe MIMEType)
+getContentType
+    = do cTypeM ← getHeader "Content-Type"
+         case cTypeM of
+           Nothing
+               → return Nothing
+           Just cType
+               → case P.parseOnly (finishOff MT.mimeType) (A.toByteString cType) of
+                    Right t → return $ Just t
+                    Left  _ → abort $ mkAbortion' BadRequest
+                                    $ "Unparsable Content-Type: " ⊕ A.toText cType
+
+-- |Return the value of request header \"Authorization\" as
+-- 'AuthCredential'.
+getAuthorization ∷ Resource (Maybe AuthCredential)
+getAuthorization
+    = do authM ← getHeader "Authorization"
+         case authM of
+           Nothing
+               → return Nothing
+           Just auth
+               → case P.parseOnly (finishOff authCredential) (A.toByteString auth) of
+                    Right ac → return $ Just ac
+                    Left  _  → return Nothing
+
+-- |Tell the system that the 'Resource' found an entity for the
+-- request URI. If this is a GET or HEAD request, a found entity means
+-- a datum to be replied. If this is a PUT or DELETE request, it means
+-- a datum which was stored for the URI until now. For POST requests
+-- it raises an error.
+--
+-- 'foundEntity' performs \"If-Match\" test or \"If-None-Match\" test
+-- whenever possible, and if those tests fail, it immediately aborts
+-- with status \"412 Precondition Failed\" or \"304 Not Modified\"
+-- depending on the situation.
+--
+-- If the request method is either GET or HEAD, 'foundEntity'
+-- automatically puts \"ETag\" and \"Last-Modified\" headers into the
+-- response.
+foundEntity ∷ ETag → UTCTime → Resource ()
+foundEntity tag timeStamp
     = do driftTo ExaminingRequest
 
-         method <- getMethod
-         when (method == GET || method == HEAD)
-                  $ setHeader' "Last-Modified" $ formatHTTPDateTime timeStamp
-         foundETag isWeak token
-
-         driftTo GettingBody
-
-
-foundETag :: Bool -> String -> Resource ()
-foundETag isWeak token
+         method ← getMethod
+         when (method ≡ GET ∨ method ≡ HEAD)
+             $ setHeader "Last-Modified" (HTTP.toAscii timeStamp)
+         when (method ≡ POST)
+             $ abort
+             $ mkAbortion' InternalServerError
+               "foundEntity: this is a POST request."
+         foundETag tag
+
+         driftTo ReceivingBody
+
+-- |Tell the system that the 'Resource' found an entity for the
+-- request URI. The only difference from 'foundEntity' is that
+-- 'foundETag' doesn't (nor can't) put \"Last-Modified\" header into
+-- the response.
+--
+-- Using this function is discouraged. You should use 'foundEntity'
+-- whenever possible.
+foundETag ∷ ETag → Resource ()
+foundETag tag
     = do driftTo ExaminingRequest
-
-         let tag = mkETag isWeak token
       
-         method <- getMethod
-         when (method == GET || method == HEAD)
-                  $ setHeader' "ETag" $ show tag
+         method ← getMethod
+         when (method ≡ GET ∨ method ≡ HEAD)
+             $ setHeader "ETag"
+             $ A.fromAsciiBuilder
+             $ printETag tag
+         when (method ≡ POST)
+             $ abort
+             $ mkAbortion' InternalServerError
+               "Illegal computation of foundETag for POST request."
 
          -- If-Match があればそれを見る。
-         ifMatch <- getHeader "If-Match"
+         ifMatch  getHeader "If-Match"
          case ifMatch of
-           Nothing   -> return ()
-           Just "*"  -> return ()
-           Just list -> case parseStr eTagListP list of
-                          (Success tags, _)
-                              -- tags の中に一致するものが無ければ
-                              -- PreconditionFailed で終了。
-                              -> when (not $ any (== tag) tags)
-                                 $ abort PreconditionFailed [] ("The entity tag doesn't match: " ++ list)
-                          _   -> abort BadRequest [] ("Unparsable If-Match: " ++ fromJust ifMatch)
-
-         let statusForNoneMatch = if method == GET || method == HEAD then
-                                      NotModified
-                                  else
-                                      PreconditionFailed
+           Nothing
+               → return ()
+           Just value
+               → if value ≡ "*" then
+                      return ()
+                  else
+                      case P.parseOnly (finishOff eTagList) (A.toByteString value) of
+                        Right tags
+                            -- tags の中に一致するものが無ければ
+                            -- PreconditionFailed で終了。
+                            → when ((¬) (any (≡ tag) tags))
+                                  $ abort
+                                  $ mkAbortion' PreconditionFailed
+                                  $ "The entity tag doesn't match: " ⊕ A.toText value
+                        Left _
+                            → abort $ mkAbortion' BadRequest
+                                    $ "Unparsable If-Match: " ⊕ A.toText value
+
+         let statusForNoneMatch
+                 = if method ≡ GET ∨ method ≡ HEAD then
+                       NotModified
+                   else
+                       PreconditionFailed
 
          -- If-None-Match があればそれを見る。
-         ifNoneMatch <- getHeader "If-None-Match"
+         ifNoneMatch  getHeader "If-None-Match"
          case ifNoneMatch of
-           Nothing   -> return ()
-           Just "*"  -> abort statusForNoneMatch [] ("The entity tag matches: *")
-           Just list -> case parseStr eTagListP list of
-                          (Success tags, _)
-                              -> when (any (== tag) tags)
-                                 $ abort statusForNoneMatch [] ("The entity tag matches: " ++ list)
-                          _   -> abort BadRequest [] ("Unparsable If-None-Match: " ++ list)
-
-         driftTo GettingBody
-
-
-foundTimeStamp :: ClockTime -> Resource ()
+           Nothing
+               → return ()
+           Just value
+               → if value ≡ "*" then
+                      abort $ mkAbortion' statusForNoneMatch
+                            $ "The entity tag matches: *"
+                  else
+                      case P.parseOnly (finishOff eTagList) (A.toByteString value) of
+                        Right tags
+                            → when (any (≡ tag) tags)
+                                  $ abort
+                                  $ mkAbortion' statusForNoneMatch
+                                  $ "The entity tag matches: " ⊕ A.toText value
+                        Left _
+                            → abort $ mkAbortion' BadRequest
+                                    $ "Unparsable If-None-Match: " ⊕ A.toText value
+
+         driftTo ReceivingBody
+
+-- |Tell the system that the 'Resource' found an entity for the
+-- request URI. The only difference from 'foundEntity' is that
+-- 'foundTimeStamp' performs \"If-Modified-Since\" test or
+-- \"If-Unmodified-Since\" test instead of \"If-Match\" test or
+-- \"If-None-Match\" test. Be aware that any tests based on a last
+-- modification time are unsafe because it is possible to mess up such
+-- tests by modifying the entity twice in a second.
+--
+-- Using this function is discouraged. You should use 'foundEntity'
+-- whenever possible.
+foundTimeStamp ∷ UTCTime → Resource ()
 foundTimeStamp timeStamp
     = do driftTo ExaminingRequest
 
-         method <- getMethod
-         when (method == GET || method == HEAD)
-                  $ setHeader' "Last-Modified" $ formatHTTPDateTime timeStamp
+         method ← getMethod
+         when (method ≡ GET ∨ method ≡ HEAD)
+             $ setHeader "Last-Modified" (HTTP.toAscii timeStamp)
+         when (method ≡ POST)
+             $ abort
+             $ mkAbortion' InternalServerError
+               "Illegal computation of foundTimeStamp for POST request."
 
-         let statusForIfModSince = if method == GET || method == HEAD then
-                                       NotModified
-                                   else
-                                       PreconditionFailed
+         let statusForIfModSince
+                 = if method ≡ GET ∨ method ≡ HEAD then
+                       NotModified
+                   else
+                       PreconditionFailed
 
          -- If-Modified-Since があればそれを見る。
-         ifModSince <- getHeader "If-Modified-Since"
+         ifModSince  getHeader "If-Modified-Since"
          case ifModSince of
-           Just str -> case parseHTTPDateTime str of
-                         Just lastTime
-                             -> when (timeStamp <= lastTime)
-                                $ abort statusForIfModSince [] ("The entity has not been modified since " ++ str)
-                         Nothing
-                             -> return () -- 不正な時刻は無視
-           Nothing  -> return ()
+           Just str → case HTTP.fromAscii str of
+                         Right lastTime
+                             → when (timeStamp ≤ lastTime)
+                               $ abort
+                               $ mkAbortion' statusForIfModSince
+                               $ "The entity has not been modified since " ⊕ A.toText str
+                         Left _
+                             → return () -- 不正な時刻は無視
+           Nothing  → return ()
 
          -- If-Unmodified-Since があればそれを見る。
-         ifUnmodSince <- getHeader "If-Unmodified-Since"
+         ifUnmodSince  getHeader "If-Unmodified-Since"
          case ifUnmodSince of
-           Just str -> case parseHTTPDateTime str of
-                         Just lastTime
-                             -> when (timeStamp > lastTime)
-                                $ abort PreconditionFailed [] ("The entity has not been modified since " ++ str)
-                         Nothing
-                             -> return () -- 不正な時刻は無視
-           Nothing  -> return ()
-
-         driftTo GettingBody
-
-
-foundNoEntity :: Maybe String -> Resource ()
+           Just str → case HTTP.fromAscii str of
+                         Right lastTime
+                             → when (timeStamp > lastTime)
+                               $ abort
+                               $ mkAbortion' PreconditionFailed
+                               $ "The entity has not been modified since " ⊕ A.toText str
+                         Left _
+                             → return () -- 不正な時刻は無視
+           Nothing  → return ()
+
+         driftTo ReceivingBody
+
+-- |@'foundNoEntity' mStr@ tells the system that the 'Resource' found
+-- no entity for the request URI. @mStr@ is an optional error message
+-- to be replied to the client.
+--
+-- If the request method is PUT, 'foundNoEntity' performs \"If-Match\"
+-- test and when that fails it aborts with status \"412 Precondition
+-- Failed\". If the request method is GET, HEAD, POST or DELETE,
+-- 'foundNoEntity' always aborts with status \"404 Not Found\".
+foundNoEntity ∷ Maybe Text → Resource ()
 foundNoEntity msgM
     = do driftTo ExaminingRequest
 
-         let msg = fromMaybe "The requested entity was not found in this server." msgM
-
-         method <- getMethod
-         when (method /= PUT)
-              $ abort NotFound [] msg
+         method ← getMethod
+         when (method ≢ PUT)
+             $ abort
+             $ mkAbortion NotFound [] msgM
 
          -- エンティティが存在しないと云ふ事は、"*" も含めたどのやうな
          -- If-Match: 條件も滿たさない。
-         ifMatch <- getHeader "If-Match"
-         when (ifMatch /= Nothing)
-                  $ abort PreconditionFailed [] msg
-
-         driftTo GettingBody
-
-
-{- GettingBody 時に使用するアクション群 -}
-
-input :: Int -> Resource String
-input limit = inputBS limit >>= return . B.unpack
-
-
--- 多くとも limit バイトまでのリクエストボディ全體を受信する。limit が
--- 零以下なら Config で設定されたデフォルトのボディ長により制限される。
-inputBS :: Int -> Resource ByteString
-inputBS limit
-    = do driftTo GettingBody
-         itr     <- ask
-         hasBody <- liftIO $ atomically $ readItr itr itrRequestHasBody id
-         chunk   <- if hasBody then
-                        askForInput itr
-                    else
-                        do driftTo DecidingHeader
-                           return B.empty
-         return chunk
+         ifMatch ← getHeader "If-Match"
+         when (ifMatch ≢ Nothing)
+             $ abort
+             $ mkAbortion PreconditionFailed [] msgM
+
+         driftTo ReceivingBody
+
+-- |'foundNoEntity'' is the same as @'foundNoEntity' 'Nothing'@.
+foundNoEntity' ∷ Resource ()
+{-# INLINE foundNoEntity' #-}
+foundNoEntity' = foundNoEntity Nothing
+
+-- |@'getChunks' limit@ attemts to read the entire request body up to
+-- @limit@ bytes, and then make the 'Resource' transit to the
+-- /Deciding Header/ state. When the actual size of the body is larger
+-- than @limit@ bytes, 'getChunks' immediately aborts with status
+-- \"413 Request Entity Too Large\". When the request has no body, it
+-- returns an empty string.
+--
+-- When the @limit@ is 'Nothing', 'getChunks' uses the default
+-- limitation value ('cnfMaxEntityLength') instead.
+--
+-- 'getChunks' returns a lazy 'Lazy.ByteString' but it's not really
+-- lazy: reading from the socket just happens at the computation of
+-- 'getChunks', not at the evaluation of the 'Lazy.ByteString'.
+getChunks ∷ Maybe Int → Resource Lazy.ByteString
+getChunks (Just n)
+    | n < 0     = fail ("getChunks: limit must not be negative: " ⧺ show n)
+    | n ≡ 0     = return (∅)
+    | otherwise = getChunks' n
+getChunks Nothing
+    = getConfig ≫= getChunks ∘ Just ∘ cnfMaxEntityLength
+
+getChunks' ∷ Int → Resource Lazy.ByteString
+getChunks' limit = go limit (∅)
     where
-      askForInput :: Interaction -> Resource ByteString
-      askForInput itr
-          = do let defaultLimit = cnfMaxEntityLength $ itrConfig itr
-                   actualLimit  = if limit <= 0 then
-                                      defaultLimit
-                                  else
-                                      limit
-               when (actualLimit <= 0)
-                        $ fail ("inputBS: limit must be positive: " ++ show actualLimit)
-               -- Reader にリクエスト
-               liftIO $ atomically
-                          $ do chunkLen <- readItr itr itrReqChunkLength id
-                               writeItr itr itrWillReceiveBody True
-                               if fmap (> actualLimit) chunkLen == Just True then
-                                   -- 受信前から多過ぎる事が分かってゐる
-                                   tooLarge actualLimit
-                                 else
-                                   writeItr itr itrReqBodyWanted $ Just actualLimit
-               -- 應答を待つ。トランザクションを分けなければ當然デッドロック。
-               chunk <- liftIO $ atomically
-                        $ do chunk       <- readItr itr itrReceivedBody id
-                             chunkIsOver <- readItr itr itrReqChunkIsOver id
-                             if B.length chunk < fromIntegral actualLimit then
-                                 -- 要求された量に滿たなくて、まだ殘り
-                                 -- があるなら再試行。
-                                 unless chunkIsOver
-                                            $ retry
-                               else
-                                 -- 制限値一杯まで讀むやうに指示したの
-                                 -- にまだ殘ってゐるなら、それは多過ぎ
-                                 -- る。
-                                 unless chunkIsOver
-                                            $ tooLarge actualLimit
-                             -- 成功。itr 内にチャンクを置いたままにす
-                             -- るとメモリの無駄になるので除去。
-                             writeItr itr itrReceivedBody B.empty
-                             return chunk
-               driftTo DecidingHeader
-               return chunk
-
-      tooLarge :: Int -> STM ()
-      tooLarge lim = abortSTM RequestEntityTooLarge []
-                     ("Request body must be smaller than "
-                      ++ show lim ++ " bytes.")
-         
-
-inputChunk :: Int -> Resource String
-inputChunk limit = inputChunkBS limit >>= return . B.unpack
-
-
--- 多くとも limit バイトまでのリクエストボディの一部を受信する。limit
--- が 0 以下なら Config で設定されたデフォルトのボディ長により制限され
--- る。これ以上ボディが殘ってゐなければ空文字列を返す。
-inputChunkBS :: Int -> Resource ByteString
-inputChunkBS limit
-    = do driftTo GettingBody
-         itr <- ask
-         hasBody <- liftIO $ atomically $ readItr itr itrRequestHasBody id
-         chunk   <- if hasBody then
-                        askForInput itr
+      go ∷ Int → Builder → Resource Lazy.ByteString
+      go  0  _ = do chunk ← getChunk 1
+                    if Strict.null chunk then
+                        return (∅)
+                    else
+                        abort $ mkAbortion' RequestEntityTooLarge
+                              $ "Request body must be smaller than "
+                              ⊕ T.pack (show limit)
+                              ⊕ " bytes."
+      go !n !b = do c ← getChunk $ min n BB.defaultBufferSize
+                    if Strict.null c then
+                        -- Got EOF
+                        return $ BB.toLazyByteString b
                     else
-                        do driftTo DecidingHeader
-                           return B.empty
-         return chunk
+                        do let n'  = n - Strict.length c
+                               xs' = b ⊕ BB.fromByteString c
+                           go n' xs'
+
+-- |@'getForm' limit@ attempts to read the request body with
+-- 'getChunks' and parse it as @application\/x-www-form-urlencoded@ or
+-- @multipart\/form-data@. If the request header \"Content-Type\" is
+-- neither of them, 'getForm' aborts with status \"415 Unsupported
+-- Media Type\". If the request has no \"Content-Type\", it aborts
+-- with \"400 Bad Request\".
+--
+-- Note that there are currently a few limitations on parsing
+-- @multipart/form-data@. See: 'parseMultipartFormData'
+getForm ∷ Maybe Int → Resource [(Strict.ByteString, FormData)]
+getForm limit
+    = do cTypeM ← getContentType
+         case cTypeM of
+           Nothing
+               → abort $ mkAbortion' BadRequest "Missing Content-Type"
+           Just (MIMEType "application" "x-www-form-urlencoded" _)
+               → readWWWFormURLEncoded
+           Just (MIMEType "multipart" "form-data" params)
+               → readMultipartFormData params
+           Just cType
+               → abort $ mkAbortion' UnsupportedMediaType
+                       $ A.toText
+                       $ A.fromAsciiBuilder
+                       $ A.toAsciiBuilder "Unsupported media type: "
+                       ⊕ MT.printMIMEType cType
     where
-      askForInput :: Interaction -> Resource ByteString
-      askForInput itr
-          = do let defaultLimit = cnfMaxEntityLength $ itrConfig itr
-                   actualLimit  = if limit < 0 then
-                                      defaultLimit
-                                  else
-                                      limit
-               when (actualLimit <= 0)
-                        $ fail ("inputChunkBS: limit must be positive: " ++ show actualLimit)
-               -- Reader にリクエスト
-               liftIO $ atomically
-                          $ do writeItr itr itrReqBodyWanted $ Just actualLimit
-                               writeItr itr itrWillReceiveBody True
-               -- 應答を待つ。トランザクションを分けなければ當然デッドロック。
-               chunk <- liftIO $ atomically
-                        $ do chunk <- readItr itr itrReceivedBody id
-                             -- 要求された量に滿たなくて、まだ殘りがあ
-                             -- るなら再試行。
-                             when (B.length chunk < fromIntegral actualLimit)
-                                      $ do chunkIsOver <- readItr itr itrReqChunkIsOver id
-                                           unless chunkIsOver
-                                                      $ retry
-                             -- 成功
-                             writeItr itr itrReceivedBody B.empty
-                             return chunk
-               when (B.null chunk)
-                        $ driftTo DecidingHeader
-               return chunk
-
-
-defaultLimit :: Int
-defaultLimit = (-1)
-
-
-
-{- DecidingHeader 時に使用するアクション群 -}
-
-setStatus :: StatusCode -> Resource ()
-setStatus code
-    = do driftTo DecidingHeader
-         itr <- ask
-         liftIO $ atomically $ updateItr itr itrResponse
-                    $ \ resM -> case resM of
-                                  Nothing  -> Just $ Response {
-                                                resVersion = HttpVersion 1 1
-                                              , resStatus  = code
-                                              , resHeaders = []
-                                              }
-                                  Just res -> Just $ res {
-                                                resStatus = code
-                                              }
-
-
-setHeader :: String -> String -> Resource ()
-setHeader name value
-    = driftTo DecidingHeader >> setHeader' name value
-         
-
-setHeader' :: String -> String -> Resource()
-setHeader' name value
-    = do itr <- ask
-         liftIO $ atomically $ updateItr itr itrResponse
-                    $ \ resM -> case resM of
-                                  Nothing  -> Just $ Response {
-                                                resVersion = HttpVersion 1 1
-                                              , resStatus  = Ok
-                                              , resHeaders = [ (name, value) ]
-                                              }
-                                  Just res -> Just $ H.setHeader name value res
-
-
-redirect :: StatusCode -> URI -> Resource ()
+      readWWWFormURLEncoded
+          = (map toPairWithFormData ∘ parseWWWFormURLEncoded)
+            <$>
+            (bsToAscii =≪ getChunks limit)
+
+      bsToAscii bs
+          = case A.fromByteString (Strict.concat (Lazy.toChunks bs)) of
+              Just a  → return a
+              Nothing → abort $ mkAbortion' BadRequest "Malformed x-www-form-urlencoded"
+
+      readMultipartFormData (MIMEParams m)
+          = case M.lookup "boundary" m of
+              Nothing
+                  → abort $ mkAbortion' BadRequest "Missing boundary of multipart/form-data"
+              Just boundary
+                  → do src ← getChunks limit
+                       b   ← case A.fromText boundary of
+                                Just b  → return b
+                                Nothing → abort $ mkAbortion' BadRequest
+                                                $ "Malformed boundary: " ⊕ boundary
+                       case parseMultipartFormData b src of
+                         Right xs → return $ map (first A.toByteString) xs
+                         Left err → abort $ mkAbortion' BadRequest $ T.pack err
+
+-- |@'redirect' code uri@ declares the response status as @code@ and
+-- \"Location\" header field as @uri@. The @code@ must satisfy
+-- 'isRedirection' or it raises an error.
+redirect ∷ StatusCode → URI → Resource ()
 redirect code uri
-    = do when (code == NotModified || not (isRedirection code))
-                  $ abort InternalServerError []
-                        $ "Attempted to redirect with status " ++ show code
+    = do when (code ≡ NotModified ∨ not (isRedirection code))
+             $ abort
+             $ mkAbortion' InternalServerError
+             $ A.toText
+             $ A.fromAsciiBuilder
+             $ A.toAsciiBuilder "Attempted to redirect with status "
+             ⊕ printStatusCode code
          setStatus code
-         setHeader "Location" (uriToString id uri $ "")
-
-
-setETag :: Bool -> String -> Resource ()
-setETag isWeak token
-    = setHeader "ETag" $ show $ mkETag isWeak token
-
-
-setLastModified :: ClockTime -> Resource ()
-setLastModified lastmod
-    = setHeader "Last-Modified" $ formatHTTPDateTime lastmod
-
-
-{- DecidingBody 時に使用するアクション群 -}
-
-output :: String -> Resource ()
-output = outputBS . B.pack
-
-
-outputBS :: ByteString -> Resource ()
-outputBS str = do outputChunkBS str
-                  driftTo Done
-
-
-outputChunk :: String -> Resource ()
-outputChunk = outputChunkBS . B.pack
-
-
-outputChunkBS :: ByteString -> Resource ()
-outputChunkBS str = do driftTo DecidingBody
-                       itr <- ask
-                       liftIO $ atomically $
-                              do updateItr itr itrBodyToSend (flip B.append str)
-                                 unless (B.null str)
-                                            $ writeItr itr itrBodyIsNull False
-
-
-{-
-
-  [GettingBody からそれ以降の状態に遷移する時]
-  
-  body を讀み終へてゐなければ、殘りの body を讀み捨てる。
-
-
-  [DecidingHeader からそれ以降の状態に遷移する時]
-
-  postprocess する。
-
-
-  [Done に遷移する時]
-
-  bodyIsNull が False ならば何もしない。True だった場合は出力補完す
-  る。Content-Type も變はる。但し(デフォルトのまま)Status が 200 OK
-  だった場合は、補完の代はりに 204 No Content に變へる。
-
--}
-
-driftTo :: InteractionState -> Resource ()
-driftTo newState
-    = do itr <- ask
-         liftIO $ atomically $ do oldState <- readItr itr itrState id
-                                  if newState < oldState then
-                                      throwStateError oldState newState
-                                    else
-                                      do let a = [oldState .. newState]
-                                             b = tail a
-                                             c = zip a b
-                                         mapM_ (uncurry $ drift itr) c
-                                         writeItr itr itrState newState
+         setLocation uri
+
+-- |@'setContentType' mType@ declares the response header
+-- \"Content-Type\" as @mType@. Declaring \"Content-Type\" is
+-- mandatory for sending a response body.
+setContentType ∷ MIMEType → Resource ()
+setContentType
+    = setHeader "Content-Type" ∘ A.fromAsciiBuilder ∘ MT.printMIMEType
+
+-- |@'setLocation' uri@ declares the response header \"Location\" as
+-- @uri@. You usually don't need to call this function directly.
+setLocation ∷ URI → Resource ()
+setLocation uri
+    = case A.fromChars uriStr of
+        Just a  → setHeader "Location" a
+        Nothing → abort $ mkAbortion' InternalServerError
+                        $ "Malformed URI: " ⊕ T.pack uriStr
     where
-      throwStateError :: Monad m => InteractionState -> InteractionState -> m a
-
-      throwStateError Done DecidingBody
-          = fail "It makes no sense to output something after finishing to output."
-
-      throwStateError old new
-          = fail ("state error: " ++ show old ++ " ==> " ++ show new)
-
-
-      drift :: Interaction -> InteractionState -> InteractionState -> STM ()
-
-      drift itr GettingBody _
-          = writeItr itr itrReqBodyWasteAll True
-
-      drift itr DecidingHeader _
-          = postprocess itr
-
-      drift itr _ Done
-          = do bodyIsNull <- readItr itr itrBodyIsNull id
-               when bodyIsNull
-                        $ do status <- readStatus itr
-                             if status == Ok then
-                                 do updateItrF itr itrResponse
-                                                   $ \ res -> res { resStatus = NoContent }
-                                    updateItrF itr itrResponse
-                                                   $ H.deleteHeader "Content-Type"
-                                    updateItrF itr itrResponse
-                                                   $ H.deleteHeader "ETag"
-                                    updateItrF itr itrResponse
-                                                   $ H.deleteHeader "Last-Modified"
-                               else
-                                 writeDefaultPage itr
-                                       
-
-      drift _ _ _
-          = return ()
-
-
-      readStatus :: Interaction -> STM StatusCode
-      readStatus itr = readItr itr itrResponse (resStatus . fromJust)
\ No newline at end of file
+      uriStr = uriToString id uri ""
+
+-- |@'setContentEncoding' codings@ declares the response header
+-- \"Content-Encoding\" as @codings@.
+setContentEncoding ∷ [CIAscii] → Resource ()
+setContentEncoding codings
+    = do ver ← getRequestVersion
+         tr  ← case ver of
+                  HttpVersion 1 0 → return (toAB ∘ unnormalizeCoding)
+                  HttpVersion 1 1 → return toAB
+                  _               → abort $ mkAbortion' InternalServerError
+                                            "setContentEncoding: Unknown HTTP version"
+         setHeader "Content-Encoding"
+             $ A.fromAsciiBuilder
+             $ mconcat
+             $ intersperse (A.toAsciiBuilder ", ")
+             $ map tr codings
+    where
+      toAB = A.toAsciiBuilder ∘ A.fromCIAscii
+
+-- |@'setWWWAuthenticate' challenge@ declares the response header
+-- \"WWW-Authenticate\" as @challenge@.
+setWWWAuthenticate ∷ AuthChallenge → Resource ()
+setWWWAuthenticate = setHeader "WWW-Authenticate" ∘ printAuthChallenge
+
+-- |Write a chunk in 'Strict.ByteString' to the response body. You
+-- must first declare the response header \"Content-Type\" before
+-- applying this function. See: 'setContentType'
+putChunk ∷ Strict.ByteString → Resource ()
+putChunk = putBuilder ∘ BB.fromByteString
+
+-- |Write a chunk in lazy 'Lazy.ByteString' to the response body. It
+-- can be safely applied to an infinitely long 'Lazy.ByteString'.
+--
+-- Note that you must first declare the response header
+-- \"Content-Type\" before applying this function. See:
+-- 'setContentType'
+putChunks ∷ Lazy.ByteString → Resource ()
+putChunks = putBuilder ∘ BB.fromLazyByteString