]> gitweb @ CieloNegro.org - Lucu.git/commitdiff
Merge branch 'attoparsec'
authorPHO <pho@cielonegro.org>
Mon, 31 Oct 2011 17:27:19 +0000 (02:27 +0900)
committerPHO <pho@cielonegro.org>
Mon, 31 Oct 2011 17:27:19 +0000 (02:27 +0900)
55 files changed:
.gitignore
GNUmakefile
ImplantFile.hs
Lucu.cabal
Network/HTTP/Lucu.hs
Network/HTTP/Lucu/Abortion.hs
Network/HTTP/Lucu/Abortion/Internal.hs [new file with mode: 0644]
Network/HTTP/Lucu/Authentication.hs [new file with mode: 0644]
Network/HTTP/Lucu/Authorization.hs [deleted file]
Network/HTTP/Lucu/Chunk.hs
Network/HTTP/Lucu/Config.hs
Network/HTTP/Lucu/ContentCoding.hs
Network/HTTP/Lucu/DefaultPage.hs
Network/HTTP/Lucu/ETag.hs
Network/HTTP/Lucu/Format.hs [deleted file]
Network/HTTP/Lucu/HandleLike.hs
Network/HTTP/Lucu/Headers.hs
Network/HTTP/Lucu/HttpVersion.hs
Network/HTTP/Lucu/Httpd.hs
Network/HTTP/Lucu/Interaction.hs
Network/HTTP/Lucu/MIMEType.hs
Network/HTTP/Lucu/MIMEType/DefaultExtensionMap.hs [deleted file]
Network/HTTP/Lucu/MIMEType/Guess.hs
Network/HTTP/Lucu/MultipartForm.hs
Network/HTTP/Lucu/Parser.hs
Network/HTTP/Lucu/Parser/Http.hs
Network/HTTP/Lucu/Postprocess.hs
Network/HTTP/Lucu/Preprocess.hs
Network/HTTP/Lucu/RFC2231.hs [new file with mode: 0644]
Network/HTTP/Lucu/Request.hs
Network/HTTP/Lucu/RequestReader.hs
Network/HTTP/Lucu/Resource.hs
Network/HTTP/Lucu/Resource/Internal.hs [new file with mode: 0644]
Network/HTTP/Lucu/Resource/Tree.hs
Network/HTTP/Lucu/Response.hs
Network/HTTP/Lucu/ResponseWriter.hs
Network/HTTP/Lucu/SocketLike.hs
Network/HTTP/Lucu/StaticFile.hs
Network/HTTP/Lucu/Utils.hs
bugs/issue-74e34d62deabaab386472d2949a46fea893f1ec1.yaml
bugs/issue-8959dadc07db1bd363283dee401073f6e48dc7fa.yaml
bugs/issue-a5e6a89da31d2ca0a69d89ad1d579fee8d0c131f.yaml [new file with mode: 0644]
bugs/issue-b3e2a5ee9307d4ba9b7a0346e6ca0d91ca287997.yaml
bugs/issue-ce2851ba49c154838b48e56ecf4c01840e4c1b7c.yaml [new file with mode: 0644]
bugs/issue-e0312227f40a0fa92d4c5d69a64dad473f54389a.yaml [new file with mode: 0644]
cabal-package.mk
data/CompileMimeTypes.hs
data/Makefile
data/mime.types
examples/HelloWorld.hs
examples/Implanted.hs
examples/ImplantedSmall.hs
examples/Makefile
examples/Multipart.hs
examples/SSL.hs

index 0b4ee080722a93df73c87687f7e4445f77c8cab3..00bc2862091b90931d98ae7362c60273678e8198 100644 (file)
@@ -8,6 +8,10 @@ Setup
 dist
 report.html
 
+Network/HTTP/Lucu/MIMEType/DefaultExtensionMap.hs
+
+data/CompileMimeTypes
+
 examples/HelloWorld
 examples/Implanted
 examples/ImplantedSmall
index 8b9ab3191225d324578dd39e119dc07643ba476e..3b5520eb5629ff02df6a7aa7fcf57c02403ddc57 100644 (file)
@@ -4,14 +4,5 @@ CONFIGURE_ARGS = -O
 
 include cabal-package.mk
 
-update-web: update-web-doc update-web-ditz
-
-update-web-doc: doc
-       rsync -av --delete \
-               dist/doc/html/Lucu/ \
-               www@nem.cielonegro.org:static.cielonegro.org/htdocs/doc/Lucu
-
-update-web-ditz: ditz
-       rsync -av --delete \
-               dist/ditz/ \
-               www@nem.cielonegro.org:static.cielonegro.org/htdocs/ditz/Lucu
+build-hook:
+       $(MAKE) -C data
index fd57fadc456023ef5e78eee0b018098b12cada39..c253c2abd05395b3311dba1fd9d3ed999d37d89b 100644 (file)
@@ -1,28 +1,35 @@
-import           Codec.Compression.GZip
-import           Control.Monad
-import           Data.Bits
-import qualified Data.ByteString as BS
+{-# LANGUAGE
+    OverloadedStrings
+  , UnicodeSyntax
+  #-}
+module Main where
+import Codec.Compression.GZip
+import Control.Applicative
+import Control.Monad
+import qualified Data.Ascii as A
 import qualified Data.ByteString.Base64 as B64
-import qualified Data.ByteString.Char8 as C8
-import qualified Data.ByteString.Lazy as Lazy (ByteString)
-import qualified Data.ByteString.Lazy as LS hiding (ByteString)
-import           Data.Char
-import           Data.Int
-import           Data.Maybe
-import           Data.Time
-import           Data.Time.Clock.POSIX
-import           Language.Haskell.Pretty
-import           Language.Haskell.Syntax
-import           Network.HTTP.Lucu.MIMEType
-import           Network.HTTP.Lucu.MIMEType.DefaultExtensionMap
-import           Network.HTTP.Lucu.MIMEType.Guess
-import           OpenSSL
-import           OpenSSL.EVP.Digest
-import           System.Console.GetOpt
-import           System.Environment
-import           System.Exit
-import           System.Posix.Files
-import           System.IO
+import qualified Data.ByteString.Char8 as Strict
+import qualified Data.ByteString.Lazy as Lazy
+import Data.Char
+import Data.Digest.Pure.SHA
+import Data.Int
+import Data.Maybe
+import Data.Time
+import Data.Time.Clock.POSIX
+import Language.Haskell.Exts.Build
+import Language.Haskell.Exts.Extension
+import Language.Haskell.Exts.Pretty
+import Language.Haskell.Exts.Syntax
+import Network.HTTP.Lucu.ETag
+import Network.HTTP.Lucu.MIMEType
+import Network.HTTP.Lucu.MIMEType.DefaultExtensionMap
+import Network.HTTP.Lucu.MIMEType.Guess
+import Prelude.Unicode
+import System.Console.GetOpt
+import System.Environment
+import System.Exit
+import System.Posix.Files
+import System.IO
 
 data CmdOpt
     = OptOutput FilePath
@@ -33,423 +40,412 @@ data CmdOpt
     | OptHelp
     deriving (Eq, Show)
 
-
-options :: [OptDescr CmdOpt]
-options = [ Option ['o'] ["output"]
+options ∷ [OptDescr CmdOpt]
+options = [ Option "o" ["output"]
                        (ReqArg OptOutput "FILE")
                        "Output to the FILE."
 
-          , Option ['m'] ["module"]
+          , Option "m" ["module"]
                        (ReqArg OptModName "MODULE")
                        "Specify the resulting module name. (required)"
 
-          , Option ['s'] ["symbol"]
+          , Option "s" ["symbol"]
                        (ReqArg OptSymName "SYMBOL")
                        "Specify the resulting symbol name."
 
-          , Option ['t'] ["mime-type"]
+          , Option "t" ["mime-type"]
                        (ReqArg OptMIMEType "TYPE")
                        "Specify the MIME Type of the file."
 
-          , Option ['e'] ["etag"]
+          , Option "e" ["etag"]
                        (ReqArg OptETag "TAG")
                        "Specify the ETag of the file."
 
-          , Option ['h'] ["help"]
+          , Option "h" ["help"]
                        (NoArg OptHelp)
                        "Print this message."
           ]
 
-
-printUsage :: IO ()
-printUsage = do putStrLn ""
-                putStrLn "Description:"
-                putStrLn ("  lucu-implant-file is an utility that generates " ++
-                          "Haskell code containing an arbitrary file to " ++
-                          "compile it directly into programs and serve it " ++
-                          "statically with the Lucu HTTP server.")
-                putStrLn ""
-                putStrLn "Usage:"
-                putStrLn "  lucu-implant-file [OPTIONS...] FILE"
-                putStrLn ""
+printUsage ∷ IO ()
+printUsage = do mapM_ putStrLn msg
                 putStr $ usageInfo "Options:" options
                 putStrLn ""
+    where
+      msg = [ ""
+            , "Description:"
+            , concat [ "  lucu-implant-file is an utility that generates " 
+                     , "Haskell code containing an arbitrary file to "
+                     , "compile it directly into programs and serve it "
+                     , "statically with the Lucu HTTP server."
+                     ]
+            , ""
+            , "Usage:"
+            , "  lucu-implant-file [OPTIONS...] FILE"
+            , ""
+            ]
 
-
-main :: IO ()
-main = withOpenSSL $
-       do (opts, sources, errors) <- return . getOpt Permute options =<< getArgs
+main ∷ IO ()
+main = do (opts, sources, errors) ← getOpt Permute options <$> getArgs
 
           unless (null errors)
-                   $ do mapM_ putStr errors
-                        exitWith $ ExitFailure 1
+              $ do mapM_ putStr errors
+                   exitWith $ ExitFailure 1
 
-          when (any (== OptHelp) opts)
-                   $ do printUsage
-                        exitWith ExitSuccess
+          when (any ( OptHelp) opts)
+              $ do printUsage
+                   exitWith ExitSuccess
 
           when (null sources)
-                   $ do printUsage
-                        exitWith $ ExitFailure 1
+               $ do printUsage
+                    exitWith $ ExitFailure 1
 
-          when (length sources >= 2)
-                   $ error "too many input files."
+          when (length sources  2)
+               $ fail "too many input files."
 
           generateHaskellSource opts (head sources)
 
-
-generateHaskellSource :: [CmdOpt] -> FilePath -> IO ()
+generateHaskellSource ∷ [CmdOpt] → FilePath → IO ()
 generateHaskellSource opts srcFile
-    = do modName  <- getModuleName opts
-         symName  <- getSymbolName opts modName
-         mimeType <- getMIMEType opts srcFile
-         lastMod  <- getLastModified srcFile
-         input    <- openInput srcFile
-         output   <- openOutput opts
-         eTag     <- getETag opts input
+    = do modName  getModuleName opts
+         symName  getSymbolName opts modName
+         mType   ← getMIMEType opts srcFile
+         lastMod  getLastModified srcFile
+         input    openInput srcFile
+         output   openOutput opts
+         tag     ← getETag opts input
 
          let compParams  = defaultCompressParams { compressLevel = bestCompression }
              gzippedData = compressWith compParams input
-             originalLen = LS.length input
-             gzippedLen  = LS.length gzippedData
+             originalLen = Lazy.length input
+             gzippedLen  = Lazy.length gzippedData
              useGZip     = originalLen > gzippedLen
-             rawB64      = B64.encode $ BS.concat $ LS.toChunks input
-             gzippedB64  = B64.encode $ BS.concat $ LS.toChunks gzippedData
-
-         header <- mkHeader srcFile originalLen gzippedLen useGZip mimeType eTag lastMod
-             
-         let hsModule = HsModule undefined (Module modName) (Just exports) imports decls
-             exports  = [HsEVar (UnQual (HsIdent symName))]
-             imports  = [ HsImportDecl undefined (Module "Data.ByteString.Base64")
-                                       True (Just (Module "B64")) Nothing
-                        , HsImportDecl undefined (Module "Data.ByteString.Char8")
-                                       True (Just (Module "C8")) Nothing
-                        , HsImportDecl undefined (Module "Data.ByteString.Lazy")
-                                       True (Just (Module "LS")) Nothing
-                        , HsImportDecl undefined (Module "Data.Time")
-                                       False Nothing Nothing
-                        , HsImportDecl undefined (Module "Network.HTTP.Lucu")
-                                       False Nothing Nothing
-                        ]
-                        ++
-                        (if useGZip then
-                             [ HsImportDecl undefined (Module "Control.Monad")
-                                            False Nothing Nothing
-                             , HsImportDecl undefined (Module "Codec.Compression.GZip")
-                                            False Nothing Nothing
-                             ]
-                         else
-                             [])
-             decls    = declResourceDef
-                        ++
-                        declEntityTag
-                        ++
-                        declLastModified
-                        ++
-                        declContentType
-                        ++
-                        (if useGZip
-                         then declGZippedData
-                         else declRawData)
-
-             declResourceDef :: [HsDecl]
-             declResourceDef
-                 = [ HsTypeSig undefined [HsIdent symName]
-                               (HsQualType []
-                                (HsTyCon (UnQual (HsIdent "ResourceDef"))))
-                   , HsFunBind [HsMatch undefined (HsIdent symName)
-                                [] (HsUnGuardedRhs defResourceDef) []]
-                   ]
-
-             defResourceDef :: HsExp
-             defResourceDef 
-                 = let defResGet = if useGZip
-                                   then defResGetGZipped
-                                   else defResGetRaw
-                   in 
-                     (HsRecConstr (UnQual (HsIdent "ResourceDef"))
-                      [ HsFieldUpdate (UnQual (HsIdent "resUsesNativeThread"))
-                                      (HsCon (UnQual (HsIdent "False")))
-                      , HsFieldUpdate (UnQual (HsIdent "resIsGreedy"))
-                                      (HsCon (UnQual (HsIdent "False")))
-                      , HsFieldUpdate (UnQual (HsIdent "resGet")) defResGet
-                      , HsFieldUpdate (UnQual (HsIdent "resHead"))
-                                      (HsCon (UnQual (HsIdent "Nothing")))
-                      , HsFieldUpdate (UnQual (HsIdent "resPost"))
-                                      (HsCon (UnQual (HsIdent "Nothing")))
-                      , HsFieldUpdate (UnQual (HsIdent "resPut"))
-                                      (HsCon (UnQual (HsIdent "Nothing")))
-                      , HsFieldUpdate (UnQual (HsIdent "resDelete"))
-                                      (HsCon (UnQual (HsIdent "Nothing")))
-                      ]
-                     )
-
-             defResGetGZipped :: HsExp
-             defResGetGZipped
-                 = let doExp = HsDo [ doFoundEntity
-                                    , doSetContentType
-                                    , bindMustGunzip
-                                    , doConditionalOutput
-                                    ]
-                       doFoundEntity
-                           = HsQualifier (HsApp (HsApp (HsVar (UnQual (HsIdent "foundEntity")))
-                                                 (HsVar (UnQual (HsIdent "entityTag"))))
-                                          (HsVar (UnQual (HsIdent "lastModified"))))
-                       doSetContentType
-                           = HsQualifier (HsApp (HsVar (UnQual (HsIdent "setContentType")))
-                                          (HsVar (UnQual (HsIdent "contentType"))))
-                       bindMustGunzip
-                           = HsGenerator undefined
-                             (HsPVar (HsIdent "mustGunzip"))
-                             (HsApp (HsApp (HsVar (UnQual (HsIdent "liftM")))
-                                     (HsVar (UnQual (HsIdent "not"))))
-                              (HsParen
-                               (HsApp (HsVar (UnQual (HsIdent "isEncodingAcceptable")))
-                                      (HsLit (HsString "gzip")))))
-                       doConditionalOutput
-                           = HsQualifier
-                             (HsIf (HsVar (UnQual (HsIdent "mustGunzip")))
-                                   expOutputGunzipped
-                                   expOutputGZipped)
-                       expOutputGunzipped
-                           = (HsApp (HsVar (UnQual (HsIdent "outputLBS")))
-                              (HsParen
-                               (HsApp (HsVar (UnQual (HsIdent "decompress")))
-                                      (HsVar (UnQual (HsIdent "gzippedData"))))))
-                       expOutputGZipped
-                           = HsDo [ doSetContentEncodingGZip
-                                  , doOutputGZipped
-                                  ]
-                       doSetContentEncodingGZip
-                           = HsQualifier (HsApp (HsVar (UnQual (HsIdent "setContentEncoding")))
-                                          (HsList [HsLit (HsString "gzip")]))
-                       doOutputGZipped
-                           = HsQualifier (HsApp (HsVar (UnQual (HsIdent "outputLBS")))
-                                          (HsVar (UnQual (HsIdent "gzippedData"))))
-                   in 
-                     HsApp (HsCon (UnQual (HsIdent "Just")))
-                           (HsParen doExp)
-
-             defResGetRaw :: HsExp
-             defResGetRaw
-                 = let doExp = HsDo [ doFoundEntity
-                                    , doSetContentType
-                                    , doOutputRawData
-                                    ]
-                       doFoundEntity
-                           = HsQualifier (HsApp (HsApp (HsVar (UnQual (HsIdent "foundEntity")))
-                                                 (HsVar (UnQual (HsIdent "entityTag"))))
-                                          (HsVar (UnQual (HsIdent "lastModified"))))
-                       doSetContentType
-                           = HsQualifier (HsApp (HsVar (UnQual (HsIdent "setContentType")))
-                                          (HsVar (UnQual (HsIdent "contentType"))))
-                       doOutputRawData
-                           = HsQualifier (HsApp (HsVar (UnQual (HsIdent "outputLBS")))
-                                          (HsVar (UnQual (HsIdent "rawData"))))
-                   in
-                     HsApp (HsCon (UnQual (HsIdent "Just")))
-                           (HsParen doExp)
-
-             declEntityTag :: [HsDecl]
-             declEntityTag
-                 = [ HsTypeSig undefined [HsIdent "entityTag"]
-                               (HsQualType []
-                                (HsTyCon (UnQual (HsIdent "ETag"))))
-                   , HsFunBind [HsMatch undefined (HsIdent "entityTag")
-                                [] (HsUnGuardedRhs defEntityTag) []]
-                   ]
-
-             defEntityTag :: HsExp
-             defEntityTag
-                 = HsApp (HsVar (UnQual (HsIdent "strongETag")))
-                   (HsLit (HsString eTag))
-
-             declLastModified :: [HsDecl]
-             declLastModified
-                 = [ HsTypeSig undefined [HsIdent "lastModified"]
-                               (HsQualType []
-                                (HsTyCon (UnQual (HsIdent "UTCTime"))))
-                   , HsFunBind [HsMatch undefined (HsIdent "lastModified")
-                                [] (HsUnGuardedRhs defLastModified) []]
-                   ]
-
-             defLastModified :: HsExp
-             defLastModified 
-                 = HsApp (HsVar (UnQual (HsIdent "read")))
-                   (HsLit (HsString $ show lastMod))
-                            
-
-             declContentType :: [HsDecl]
-             declContentType 
-                 = [ HsTypeSig undefined [HsIdent "contentType"]
-                               (HsQualType []
-                                (HsTyCon (UnQual (HsIdent "MIMEType"))))
-                   , HsFunBind [HsMatch undefined (HsIdent "contentType")
-                                [] (HsUnGuardedRhs defContentType) []]
-                   ]
-
-             defContentType :: HsExp
-             defContentType
-                 = HsApp (HsVar (UnQual (HsIdent "read")))
-                   (HsLit (HsString $ show mimeType))
-
-             declGZippedData :: [HsDecl]
-             declGZippedData 
-                 = [ HsTypeSig undefined [HsIdent "gzippedData"]
-                               (HsQualType []
-                                (HsTyCon (Qual (Module "LS") (HsIdent "ByteString"))))
-                   , HsFunBind [HsMatch undefined (HsIdent "gzippedData")
-                                [] (HsUnGuardedRhs defGZippedData) []]
-                   ]
-
-             defGZippedData :: HsExp
-             defGZippedData 
-                 = HsApp (HsVar (Qual (Module "LS") (HsIdent "fromChunks")))
-                   (HsList [HsApp (HsVar (Qual (Module "B64") (HsIdent "decodeLenient")))
-                            (HsParen
-                             (HsApp (HsVar (Qual (Module "C8") (HsIdent "pack")))
-                              (HsLit (HsString $ C8.unpack gzippedB64))))])
-
-             declRawData :: [HsDecl]
-             declRawData 
-                 = [ HsTypeSig undefined [HsIdent "rawData"]
-                               (HsQualType []
-                                (HsTyCon (Qual (Module "LS") (HsIdent "ByteString"))))
-                   , HsFunBind [HsMatch undefined (HsIdent "rawData")
-                                [] (HsUnGuardedRhs defRawData) []]
-                   ]
-
-             defRawData :: HsExp
-             defRawData
-                 = HsApp (HsVar (Qual (Module "LS") (HsIdent "fromChunks")))
-                   (HsList [HsApp (HsVar (Qual (Module "B64") (HsIdent "decodeLenient")))
-                            (HsParen
-                             (HsApp (HsVar (Qual (Module "C8") (HsIdent "pack")))
-                              (HsLit (HsString $ C8.unpack rawB64))))])
+             rawB64      = B64.encode <$> Lazy.toChunks input
+             gzippedB64  = B64.encode <$> Lazy.toChunks gzippedData
+
+         header ← mkHeader srcFile originalLen gzippedLen useGZip mType tag lastMod
+
+         let hsModule = mkModule modName symName imports decls
+             imports  = mkImports useGZip
+             decls    = concat [ resourceDecl symName useGZip
+                               , entityTagDecl tag
+                               , lastModifiedDecl lastMod
+                               , contentTypeDecl mType
+                               , if useGZip then
+                                     dataDecl (name "gzippedData") gzippedB64
+                                 else
+                                     dataDecl (name "rawData") rawB64
+                               ]
 
          hPutStrLn output header
          hPutStrLn output (prettyPrint hsModule)
          hClose output
 
-
-mkHeader :: FilePath -> Int64 -> Int64 -> Bool -> MIMEType -> String -> UTCTime -> IO String
-mkHeader srcFile originalLen gzippedLen useGZip mimeType eTag lastMod
-    = do localLastMod <- utcToLocalZonedTime lastMod
-         return ("{- DO NOT EDIT THIS FILE.\n" ++
-                 "   This file is automatically generated by the lucu-implant-file program.\n" ++
-                 "\n" ++
-                 "              Source: " ++ (if srcFile == "-"
-                                              then "(stdin)"
-                                              else srcFile) ++ "\n" ++
-                 "     Original Length: " ++ show originalLen ++ " bytes\n" ++
-                 (if useGZip
-                  then "   Compressed Length: " ++ show gzippedLen ++ " bytes\n" ++
-                       "         Compression: gzip\n"
-                  else "         Compression: disabled\n") ++
-                 "           MIME Type: " ++ show mimeType ++ "\n" ++
-                 "                ETag: " ++ eTag ++ "\n" ++
-                 "       Last Modified: " ++ show localLastMod ++ "\n" ++
-                 " -}")
-
-
-getModuleName :: [CmdOpt] -> IO String
-getModuleName opts
-    = let modNameOpts = filter (\ x -> case x of
-                                         OptModName _ -> True
-                                         _            -> False) opts
-      in
-        case modNameOpts of
-          []                      -> error "a module name must be given."
-          (OptModName modName):[] -> return modName
-          _                       -> error "too many --module options."
-
-
-getSymbolName :: [CmdOpt] -> String -> IO String
-getSymbolName opts modName
-    = let symNameOpts    = filter (\ x -> case x of
-                                            OptSymName _ -> True
-                                            _            -> False) opts
-          -- モジュール名をピリオドで分割した時の最後の項目の先頭文字を
-          -- 小文字にしたものを使ふ。
-          defaultSymName  = mkDefault modName
-          mkDefault       = headToLower . getLastComp
-          headToLower str = case str of
-                              []     -> error "module name must not be empty"
-                              (x:xs) -> toLower x : xs
-          getLastComp     = reverse . fst . break (== '.') . reverse
-      in
-        case symNameOpts of
-          []                      -> return defaultSymName
-          (OptSymName symName):[] -> return symName
-          _                       -> error "too many --symbol options."
-
-
-getMIMEType :: [CmdOpt] -> FilePath -> IO MIMEType
-getMIMEType opts srcFile
-    = let mimeTypeOpts = filter (\ x -> case x of
-                                          OptMIMEType _ -> True
-                                          _             -> False) opts
-          defaultType  = fromMaybe (read "application/octet-stream")
-                         $ guessTypeByFileName defaultExtensionMap srcFile
+mkModule ∷ ModuleName → Name → [ImportDecl] → [Decl] → Module
+mkModule modName symName imports decls
+    = let modPragma = [ LanguagePragma (⊥) [ name (show OverloadedStrings) ] ]
+          exports   = [ EVar (UnQual symName) ]
       in
-        case mimeTypeOpts of
-          []                        -> return defaultType
-          (OptMIMEType mimeType):[] -> return $ read mimeType
-          _                         -> error "too many --mime-type options."
-
+        Module (⊥) modName modPragma Nothing (Just exports) imports decls
+
+mkImports ∷ Bool → [ImportDecl]
+mkImports useGZip
+    = [ ImportDecl (⊥) (ModuleName "Data.ByteString.Base64")
+                   True False Nothing (Just (ModuleName "B64")) Nothing
+      , ImportDecl (⊥) (ModuleName "Data.ByteString.Lazy")
+                   True False Nothing (Just (ModuleName "Lazy")) Nothing
+      , ImportDecl (⊥) (ModuleName "Data.Time")
+                   False False Nothing Nothing Nothing
+      , ImportDecl (⊥) (ModuleName "Network.HTTP.Lucu")
+                   False False Nothing Nothing Nothing
+      ]
+      ⧺
+      [ ImportDecl (⊥) (ModuleName "Codec.Compression.GZip")
+                   False False Nothing Nothing Nothing
+        | useGZip ]
+
+resourceDecl ∷ Name → Bool → [Decl]
+resourceDecl symName useGZip
+    = [ TypeSig (⊥) [symName] (TyCon (UnQual (name "ResourceDef")))
+      , nameBind (⊥) symName valExp
+      ]
+    where
+      valExp ∷ Exp
+      valExp = RecUpdate (function "emptyResource")
+               [ FieldUpdate (UnQual (name "resGet" )) resGet
+               , FieldUpdate (UnQual (name "resHead")) resHead
+               ]
+
+      resGet ∷ Exp
+      resGet | useGZip   = resGetGZipped
+             | otherwise = resGetRaw
+
+resHead ∷ Exp
+resHead
+    = function "Just" `app`
+      paren (doE [ foundEntityStmt
+                 , setContentTypeStmt
+                 ])
+
+resGetGZipped ∷ Exp
+resGetGZipped
+    = function "Just" `app`
+      paren (doE [ foundEntityStmt
+                 , setContentTypeStmt
+                 , bindGZipStmt
+                 , conditionalOutputStmt
+                 ])
+    where
+      condVarName ∷ Name
+      condVarName = name "gzipAllowed"
+
+      dataVarName ∷ Name
+      dataVarName = name "gzippedData"
+
+      bindGZipStmt ∷ Stmt
+      bindGZipStmt
+          = genStmt (⊥)
+            (pvar condVarName)
+            (function "isEncodingAcceptable" `app` strE "gzip")
+
+      conditionalOutputStmt ∷ Stmt
+      conditionalOutputStmt
+          = qualStmt $
+            If (var condVarName)
+               (doE [ setContentEncodingGZipStmt
+                    , putChunksStmt (var dataVarName)
+                    ])
+               (putChunksExp
+                (paren
+                 (function "decompress" `app` var dataVarName)))
+
+resGetRaw ∷ Exp
+resGetRaw
+    = function "Just" `app`
+      paren (doE [ foundEntityStmt
+                 , setContentTypeStmt
+                 , putChunksStmt (function "rawData")
+                 ])
+
+setContentEncodingGZipStmt ∷ Stmt
+setContentEncodingGZipStmt
+    = qualStmt
+      ( function "setContentEncoding"
+        `app`
+        listE [ strE "gzip" ]
+      )
+
+foundEntityStmt ∷ Stmt
+foundEntityStmt
+    = qualStmt $
+      metaFunction "foundEntity"
+                       [ var (name "entityTag")
+                       , var (name "lastModified")
+                       ]
+
+setContentTypeStmt ∷ Stmt
+setContentTypeStmt
+    = qualStmt
+      ( function "setContentType"
+        `app`
+        function "contentType"
+      )
+
+putChunksExp ∷ Exp → Exp
+putChunksExp = app (function "putChunks")
+
+putChunksStmt ∷ Exp → Stmt
+putChunksStmt = qualStmt ∘ putChunksExp
+
+entityTagDecl ∷ ETag → [Decl]
+entityTagDecl tag
+    = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "ETag")))
+      , nameBind (⊥) varName valExp
+      ]
+    where
+      varName ∷ Name
+      varName = name "entityTag"
+
+      valExp ∷ Exp
+      valExp = function "parseETag" `app` strE (eTagToString tag)
+
+lastModifiedDecl ∷ UTCTime → [Decl]
+lastModifiedDecl lastMod
+    = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "UTCTime")))
+      , nameBind (⊥) varName valExp
+      , InlineSig (⊥) False AlwaysActive (UnQual varName)
+      ]
+    where
+      varName ∷ Name
+      varName = name "lastModified"
+
+      valExp ∷ Exp
+      valExp = function "read" `app` strE (show lastMod)
+
+contentTypeDecl ∷ MIMEType → [Decl]
+contentTypeDecl mime
+    = [ TypeSig (⊥) [varName] (TyCon (UnQual (name "MIMEType")))
+      , nameBind (⊥) varName valExp
+      , InlineSig (⊥) False AlwaysActive (UnQual varName)
+      ]
+    where
+      varName ∷ Name
+      varName = name "contentType"
 
-getLastModified :: FilePath -> IO UTCTime
-getLastModified "-"   = getCurrentTime
-getLastModified fpath = fmap (posixSecondsToUTCTime . fromRational . toRational . modificationTime)
-                        $ getFileStatus fpath
+      valExp ∷ Exp
+      valExp = function "parseMIMEType" `app` strE (mimeToString mime)
 
+      mimeToString ∷ MIMEType → String
+      mimeToString = A.toString ∘ A.fromAsciiBuilder ∘ printMIMEType
 
-getETag :: [CmdOpt] -> Lazy.ByteString -> IO String
-getETag opts input
-    = let eTagOpts = filter (\ x -> case x of
-                                      OptETag _ -> True
-                                      _         -> False) opts
-      in
-        case eTagOpts of
-          []               -> fmap (mkETagFromInput . fromJust) (getDigestByName "SHA1")
-          (OptETag str):[] -> return str
-          _                -> error "too many --etag options."
+dataDecl ∷ Name → [Strict.ByteString] → [Decl]
+dataDecl varName chunks
+    = [ TypeSig (⊥) [varName] (TyCon (Qual (ModuleName "Lazy") (name "ByteString")))
+      , nameBind (⊥) varName valExp
+      , InlineSig (⊥) False AlwaysActive (UnQual varName)
+      ]
+    where
+      valExp ∷ Exp
+      valExp = qvar (ModuleName "Lazy") (name "fromChunks")
+               `app`
+               listE (chunkToExp <$> chunks)
+
+      chunkToExp ∷ Strict.ByteString → Exp
+      chunkToExp chunk
+          = qvar (ModuleName "B64") (name "decodeLenient")
+            `app`
+            strE (Strict.unpack chunk)
+
+mkHeader ∷ FilePath → Int64 → Int64 → Bool → MIMEType → ETag → UTCTime → IO String
+mkHeader srcFile originalLen gzippedLen useGZip mType tag lastMod
+    = do localLastMod ← utcToLocalZonedTime lastMod
+         return $ concat
+                    [ "{- DO NOT EDIT THIS FILE.\n"
+                    , "   This file is automatically generated by the lucu-implant-file program.\n"
+                    , "\n"
+                    , "              Source: ", if srcFile ≡ "-" then
+                                                    "(stdin)"
+                                                else
+                                                    srcFile
+                    , "\n"
+                    , "     Original Length: ", show originalLen, " bytes\n"
+                    , if useGZip then
+                          "   Compressed Length: " ⧺ show gzippedLen ⧺ " bytes\n" ⧺
+                          "         Compression: gzip\n"
+                      else
+                          "         Compression: disabled\n"
+                    , "           MIME Type: ", mimeTypeToString mType, "\n"
+                    , "                ETag: ", eTagToString tag, "\n"
+                    , "       Last Modified: ", show localLastMod, "\n"
+                    , " -}"
+                    ]
+
+eTagToString ∷ ETag → String
+eTagToString = A.toString ∘ A.fromAsciiBuilder ∘ printETag
+
+mimeTypeToString ∷ MIMEType → String
+mimeTypeToString = A.toString ∘ A.fromAsciiBuilder ∘ printMIMEType
+
+getModuleName ∷ [CmdOpt] → IO ModuleName
+getModuleName opts
+    = case modNameOpts of
+        []                    → fail "a module name must be given."
+        OptModName modName:[] → return $ ModuleName modName
+        _                     → fail "too many --module options."
+    where
+      modNameOpts ∷ [CmdOpt]
+      modNameOpts = filter (\ x → case x of
+                                     OptModName _ → True
+                                     _            → False) opts
+
+getSymbolName ∷ [CmdOpt] → ModuleName → IO Name
+getSymbolName opts (ModuleName modName)
+    = case symNameOpts of
+        []                    → return defaultSymName
+        OptSymName symName:[] → return $ name symName
+        _                     → fail "too many --symbol options."
     where
-      mkETagFromInput :: Digest -> String
-      mkETagFromInput sha1 = "SHA-1:" ++ toHex (digestLBS sha1 input)
+      symNameOpts ∷ [CmdOpt]
+      symNameOpts = filter (\ x → case x of
+                                     OptSymName _ → True
+                                     _            → False) opts
 
-      toHex :: String -> String
-      toHex = foldr ((++) . hexByte . fromEnum) ""
+      defaultSymName ∷ Name
+      defaultSymName
+          = name $ headToLower $ getLastComp modName
 
-      hexByte :: Int -> String
-      hexByte n
-          = [ hex4bit ((n `shiftR` 4) .&. 0x0F)
-            , hex4bit ( n             .&. 0x0F)
-            ]
+      headToLower ∷ String → String
+      headToLower []     = error "module name must not be empty"
+      headToLower (x:xs) = toLower x : xs
 
-      hex4bit :: Int -> Char
-      hex4bit n
-          | n < 10    = chr $ ord '0' + n
-          | n < 16    = chr $ ord 'a' + n - 10
-          | otherwise = undefined
+      getLastComp ∷ String → String
+      getLastComp = reverse ∘ fst ∘ break (≡ '.') ∘ reverse
 
+getMIMEType ∷ [CmdOpt] → FilePath → IO MIMEType
+getMIMEType opts srcFile
+    = case mimeTypeOpts of
+        []  → return defaultType
+        OptMIMEType ty:[]
+            → case A.fromChars ty of
+                 Just a  → return $ parseMIMEType a
+                 Nothing → fail "MIME type must not contain any non-ASCII letters."
+        _   → fail "too many --mime-type options."
+    where
+      mimeTypeOpts ∷ [CmdOpt]
+      mimeTypeOpts
+          = filter (\ x → case x of
+                             OptMIMEType _ → True
+                             _             → False) opts
 
-openInput :: FilePath -> IO Lazy.ByteString
-openInput "-"   = LS.getContents
-openInput fpath = LS.readFile fpath
+      octetStream ∷ MIMEType
+      octetStream = parseMIMEType "application/octet-stream"
 
+      defaultType ∷ MIMEType
+      defaultType = fromMaybe octetStream
+                    $ guessTypeByFileName defaultExtensionMap srcFile
 
-openOutput :: [CmdOpt] -> IO Handle
-openOutput opts
-    = let outputOpts = filter (\ x -> case x of
-                                        OptOutput _ -> True
-                                        _           -> False) opts
-      in
-        case outputOpts of
-          []                   -> return stdout
-          (OptOutput fpath):[] -> openFile fpath WriteMode
-          _                    -> error "two many --output options."
+getLastModified ∷ FilePath → IO UTCTime
+getLastModified "-"   = getCurrentTime
+getLastModified fpath = (posixSecondsToUTCTime ∘ fromRational ∘ toRational ∘ modificationTime)
+                        <$>
+                        getFileStatus fpath
 
+getETag ∷ [CmdOpt] → Lazy.ByteString → IO ETag
+getETag opts input
+    = case eTagOpts of
+        []             → return mkETagFromInput
+        OptETag str:[] → return $ strToETag str
+        _              → fail "too many --etag options."
+    where
+      eTagOpts ∷ [CmdOpt]
+      eTagOpts = filter (\ x → case x of
+                                  OptETag _ → True
+                                  _         → False) opts
+
+      mkETagFromInput ∷ ETag
+      mkETagFromInput
+          = strToETag $ "SHA-1:" ⧺ showDigest (sha1 input)
+
+      strToETag ∷ String → ETag
+      strToETag str
+          = case A.fromChars str of
+              Just a  → strongETag a
+              Nothing → error "ETag must not contain any non-ASCII letters."
+
+openInput ∷ FilePath → IO Lazy.ByteString
+openInput "-"   = Lazy.getContents
+openInput fpath = Lazy.readFile fpath
+
+openOutput ∷ [CmdOpt] → IO Handle
+openOutput opts
+    = case outputOpts of
+        []                 → return stdout
+        OptOutput fpath:[] → openFile fpath WriteMode
+        _                  → fail "two many --output options."
+    where
+      outputOpts ∷ [CmdOpt]
+      outputOpts = filter (\ x → case x of
+                                    OptOutput _ → True
+                                    _           → False) opts
 
 {-
   作られるファイルの例 (壓縮されない場合):
@@ -462,72 +458,85 @@ openOutput opts
      Compressed Length: 453 bytes  -- これは Compression: disabled の時には無い
            Compression: disabled
              MIME Type: image/png
-                  ETag: d41d8cd98f00b204e9800998ecf8427e
+                  ETag: "d41d8cd98f00b204e9800998ecf8427e"
          Last Modified: 2007-11-05 13:53:42.231882 JST
    -}
+  {-# LANGUAGE OverloadedStrings #-}
   module Foo.Bar.Baz (baz) where
   import qualified Data.ByteString.Base64 as B64
-  import qualified Data.ByteString.Char8 as C8
-  import qualified Data.ByteString.Lazy as LS
+  import qualified Data.ByteString.Lazy as Lazy
   import Data.Time
   import Network.HTTP.Lucu
 
-  baz :: ResourceDef
+  baz  ResourceDef
   baz = ResourceDef {
           resUsesNativeThread = False
         , resIsGreedy         = False
         , resGet
-            = Just (do foundEntity entityTag lastModified
-                       setContentType contentType
-                       outputLBS rawData)
-        , resHead   = Nothing
+            = Just $ do foundEntity entityTag lastModified
+                        setContentType contentType
+                        putChunk rawData
+        , resHead
+            = Just $ do foundEntity entityTag lastModified
+                        setContentType contentType
         , resPost   = Nothing
         , resPut    = Nothing
         , resDelete = Nothing
         }
 
-  entityTag :: ETag
+  entityTag  ETag
   entityTag = strongETag "d41d8cd98f00b204e9800998ecf8427e"
 
-  lastModified :: UTCTime
+  lastModified ∷ UTCTime
+  {-# NOINLINE lastModified #-}
   lastModified = read "2007-11-05 04:47:56.008366 UTC"
 
-  contentType :: MIMEType
-  contentType = read "image/png"
+  contentType ∷ MIMEType
+  {-# NOINLINE contentType #-}
+  contentType = parseMIMEType "image/png"
 
-  rawData :: LS.ByteString
-  rawData = LS.fromChunks [B64.decodeLenient (C8.pack "IyEvdXNyL2Jpbi9lbnYgcnVuZ2hjCgppbXBvcnQgRGlzdHJ...")]
+  rawData ∷ Lazy.ByteString
+  {-# NOINLINE rawData #-}
+  rawData = Lazy.fromChunks
+            [ B64.decodeLenient "IyEvdXNyL2Jpbi9lbnYgcnVuZ2hjCgppbXBvcnQgRG..."
+            , B64.decodeLenient "Otb/+DniOlRgAAAAYAAAAGAAAAB/6QOmToAEIGAAAA..."
+            ]
   ------------------------------------------------------------------------------
 
   壓縮される場合は次のやうに變はる:
   ------------------------------------------------------------------------------
   -- import に追加
-  import Control.Monad
-  import Codec.Compression.GZip
+  import Codec.Compression.Zlib
 
   -- ResourceDef は次のやうに變化
-  baz :: ResourceDef
+  baz  ResourceDef
   baz = ResourceDef {
           resUsesNativeThread = False
         , resIsGreedy         = False
         , resGet
-            = Just (do foundEntity entityTag lastModified
-                       setContentType contentType
-
-                       mustGunzip <- liftM not (isEncodingAcceptable "gzip")
-                       if mustGunzip then
-                           outputLBS (decompress gzippedData)
-                         else
-                           do setContentEncoding ["gzip"]
-                              outputLBS gzippedData
-        , resHead   = Nothing
+            = Just $ do foundEntity entityTag lastModified
+                        setContentType contentType
+
+                        gzipAllowed ← isEncodingAcceptable "gzip"
+                        if gzipAllowed then
+                            do setContentEncoding ["gzip"]
+                               putChunks gzippedData
+                        else
+                            putChunks (decompress gzippedData)
+        , resHead
+            = Just $ do foundEntity entityTag lastModified
+                        setContentType contentType
         , resPost   = Nothing
         , resPut    = Nothing
         , resDelete = Nothing
         }
-  
+
   -- rawData の代はりに gzippedData
-  gzippedData :: LS.ByteString
-  gzippedData = LS.fromChunks [B64.decodeLenient (C8.pack "Otb/+DniOlRgAAAAYAAAAGAAAAB/6QOmToAEIGAAAAB...")]
+  gzippedData ∷ Lazy.ByteString
+  {-# NOINLINE gzippedData #-}
+  gzippedData = Lazy.fromChunks
+                [ B64.decodeLenient "IyEvdXNyL2Jpbi9lbnYgcnVuZ2hjCgppbXBvcnQ..."
+                , B64.decodeLenient "Otb/+DniOlRgAAAAYAAAAGAAAAB/6QOmToAEIGA..."
+                ]
   ------------------------------------------------------------------------------
  -}
index f9c03c76fdcc4e687b23ffb7f3e1f85c509bf1d2..46fabcf51b093126ccca1b320db0472037775d10 100644 (file)
@@ -1,14 +1,15 @@
 Name: Lucu
-Synopsis: HTTP Daemonic Library
+Synopsis: Embedded HTTP Server
 Description:
-        Lucu is an HTTP daemonic library. It can be embedded in any
-        Haskell program and runs in an independent thread.  Lucu is
-        not a replacement for Apache or lighttpd. It is intended to be
-        used to create an efficient web-based RESTful application
-        without messing around FastCGI. It is also intended to be run
-        behind a reverse-proxy so it doesn't have some facilities like
-        logging, client filtering or such like.
-Version: 0.7.0.3
+
+        Lucu is an embedded HTTP server library.
+
+        It's not a replacement for Apache nor lighttpd. It is intended
+        to be used to build an efficient web-based RESTful application
+        which runs behind a reverse-proxy so it doesn't have some
+        functionalities like logging, client filtering and such.
+
+Version: 1.0
 License: PublicDomain
 License-File: COPYING
 Author: PHO <pho at cielonegro dot org>
@@ -24,6 +25,7 @@ Extra-Source-Files:
     ImplantFile.hs
     NEWS
     data/CompileMimeTypes.hs
+    data/Makefile
     data/mime.types
     examples/HelloWorld.hs
     examples/Implanted.hs
@@ -44,28 +46,34 @@ Flag build-lucu-implant-file
 
 Library
     Build-Depends:
-        HsOpenSSL            == 0.10.*,
-        base                 == 4.3.*,
-        base-unicode-symbols == 0.2.*,
-        base64-bytestring    == 0.1.*,
-        bytestring           == 0.9.*,
-        containers           == 0.4.*,
-        filepath             == 1.2.*,
-        directory            == 1.1.*,
-        haskell-src          == 1.0.*,
-        hxt                  == 9.1.*,
-        mtl                  == 2.0.*,
-        network              == 2.3.*,
-        stm                  == 2.2.*,
-        time                 == 1.2.*,
-        time-http            == 0.1.*,
-        unix                 == 2.4.*,
-        zlib                 == 0.5.*
+        HsOpenSSL                  == 0.10.*,
+        ascii                      == 0.0.*,
+        attoparsec                 == 0.9.*,
+        base                       == 4.*,
+        base-unicode-symbols       == 0.2.*,
+        base64-bytestring          == 0.1.*,
+        blaze-builder              == 0.3.*,
+        blaze-textual              == 0.2.*,
+        bytestring                 == 0.9.*,
+        containers                 == 0.4.*,
+        containers-unicode-symbols == 0.3.*,
+        filepath                   == 1.2.*,
+        haskell-src-exts           == 1.11.*,
+        hxt                        == 9.1.*,
+        mtl                        == 2.0.*,
+        network                    == 2.3.*,
+        stm                        == 2.2.*,
+        stringsearch               == 0.3.*,
+        text                       == 0.11.*,
+        time                       == 1.2.*,
+        time-http                  == 0.2.*,
+        transformers               == 0.2.*,
+        unix                       == 2.4.*
 
     Exposed-Modules:
         Network.HTTP.Lucu
         Network.HTTP.Lucu.Abortion
-        Network.HTTP.Lucu.Authorization
+        Network.HTTP.Lucu.Authentication
         Network.HTTP.Lucu.Config
         Network.HTTP.Lucu.ETag
         Network.HTTP.Lucu.HttpVersion
@@ -73,8 +81,10 @@ Library
         Network.HTTP.Lucu.MIMEType
         Network.HTTP.Lucu.MIMEType.DefaultExtensionMap
         Network.HTTP.Lucu.MIMEType.Guess
-        Network.HTTP.Lucu.Parser
+        Network.HTTP.Lucu.MultipartForm
         Network.HTTP.Lucu.Parser.Http
+        Network.HTTP.Lucu.Parser
+        Network.HTTP.Lucu.RFC2231
         Network.HTTP.Lucu.Request
         Network.HTTP.Lucu.Resource
         Network.HTTP.Lucu.Resource.Tree
@@ -83,23 +93,22 @@ Library
         Network.HTTP.Lucu.Utils
 
     Other-Modules:
+        Network.HTTP.Lucu.Abortion.Internal
         Network.HTTP.Lucu.Chunk
         Network.HTTP.Lucu.ContentCoding
         Network.HTTP.Lucu.DefaultPage
-        Network.HTTP.Lucu.Format
         Network.HTTP.Lucu.HandleLike
         Network.HTTP.Lucu.Headers
         Network.HTTP.Lucu.Interaction
-        Network.HTTP.Lucu.MultipartForm
         Network.HTTP.Lucu.Postprocess
         Network.HTTP.Lucu.Preprocess
         Network.HTTP.Lucu.RequestReader
+        Network.HTTP.Lucu.Resource.Internal
         Network.HTTP.Lucu.ResponseWriter
         Network.HTTP.Lucu.SocketLike
 
     ghc-options:
         -Wall
-        -funbox-strict-fields
 
 Executable lucu-implant-file
     if flag(build-lucu-implant-file)
@@ -109,9 +118,12 @@ Executable lucu-implant-file
 
     Main-Is: ImplantFile.hs
 
+    Build-Depends:
+        SHA  == 1.5.*,
+        zlib == 0.5.*
+
     ghc-options:
         -Wall
-        -funbox-strict-fields
 
 --Executable HelloWorld
 --    Main-Is: HelloWorld.hs
index 52315d6952afdce4e2c0e07600627b8a03dfd8ab..901ae001ed2a6e28d813883fd14401e46a3a3112 100644 (file)
@@ -1,92 +1,81 @@
--- | Lucu is an HTTP daemonic library. It can be embedded in any
--- Haskell program and runs in an independent thread.
+-- | Lucu is an embedded HTTP server library.
 --
 -- Features:
 --
+--   [/Affinity for RESTafarians/] Lucu is specifically designed to be
+--   suitable for RESTful applications.
+--
 --   [/Full support of HTTP\/1.1/] Lucu supports request pipelining,
 --   chunked I\/O, ETag comparison and \"100 Continue\".
 --
---   [/Performance/] Lucu doesn't fork\/exec to handle requests like
---   CGI. It just spawns a new thread. Inter-process communication is
---   done with STM.
---
---   [/Affinity for RESTafarians/] Lucu is a carefully designed
---   web server for RESTful applications.
---
---   [/SSL connections/] Lucu can handle HTTP connections over SSL
---   layer.
+--   [/SSL connections/] Lucu can handle HTTP connections over Secure
+--   Socket Layer.
 --
 -- Lucu is not a replacement for Apache or lighttpd. It is intended to
--- be used to create an efficient web-based RESTful application
--- without messing around FastCGI. It is also intended to be run
--- behind a reverse-proxy so it doesn't have the following (otherwise
--- essential) facilities:
+-- be used to build an efficient web-based RESTful application which
+-- runs behind a reverse-proxy so it doesn't have the following
+-- (otherwise essential) functionalities:
 --
---   [/Logging/] Lucu doesn't log any requests from any clients.
+--   [/Logging/] Lucu doesn't write logs of any requests from any
+--   clients.
 --
 --   [/Client Filtering/] Lucu always accepts any clients. No IP
 --   filter is implemented.
 --
 --   [/Bandwidth Limitting/] Lucu doesn't limit bandwidth it consumes.
 --
---   [/Protection Against Wicked Clients/] Lucu is fragile against
---   wicked clients. No attacker should be able to cause a
+--   [/Protection Against Wicked Clients/] Lucu is somewhat fragile
+--   against wicked clients. No attacker should be able to cause a
 --   buffer-overflow but can possibly DoS it.
 --
-
-
 module Network.HTTP.Lucu
     ( -- * Entry Point
       runHttpd
 
-      -- * Configuration
+      -- * 'Config'uration
     , module Network.HTTP.Lucu.Config
 
       -- * Resource Tree
-    , ResourceDef(..)
-    , emptyResource
     , ResTree
     , mkResTree
 
-      -- * Resource Monad
+      -- * 'Resource' Monad
     , module Network.HTTP.Lucu.Resource
 
       -- ** Things to be used in the Resource monad
-
       -- *** Status Code
     , StatusCode(..)
 
-      -- *** Abortion
-    , abort
-    , abortPurely
-    , abortA
+      -- *** 'Abortion'
+    , module Network.HTTP.Lucu.Abortion
 
       -- *** ETag
     , ETag(..)
     , strongETag
     , weakETag
+    , parseETag
 
       -- *** MIME Type
     , MIMEType(..)
+    , mkMIMEType
+    , parseMIMEType
 
-      -- *** Authorization
+      -- *** Authentication
     , AuthChallenge(..)
     , AuthCredential(..)
     
-      -- * Utility
-
+      -- * Utilities
       -- ** Static file handling
     , module Network.HTTP.Lucu.StaticFile
     )
     where
-
 import Network.HTTP.Lucu.Abortion
-import Network.HTTP.Lucu.Authorization
+import Network.HTTP.Lucu.Authentication
 import Network.HTTP.Lucu.Config
 import Network.HTTP.Lucu.ETag
 import Network.HTTP.Lucu.Httpd
 import Network.HTTP.Lucu.MIMEType
-import Network.HTTP.Lucu.Resource hiding (driftTo)
+import Network.HTTP.Lucu.Resource
 import Network.HTTP.Lucu.Resource.Tree
 import Network.HTTP.Lucu.Response
 import Network.HTTP.Lucu.StaticFile
index 26ea8b01e9bc4c6f5da8735e2af12046493dd324..40a8cb5ab0b276103a5cf9e8f4231be7d0e2e20c 100644 (file)
 {-# LANGUAGE
-    DeriveDataTypeable
-  , UnicodeSyntax
+    UnicodeSyntax
   #-}
-{-# OPTIONS_HADDOCK prune #-}
-
 -- |Aborting the computation of 'Network.HTTP.Lucu.Resource.Resource'
 -- in any 'Prelude.IO' monads or arrows.
 module Network.HTTP.Lucu.Abortion
-    ( Abortion(..)
+    ( Abortion
+    , mkAbortion
+    , mkAbortion'
+
     , abort
-    , abortPurely
-    , abortSTM
-    , abortA
-    , abortPage
     )
     where
-
-import           Control.Arrow
-import           Control.Arrow.ArrowIO
-import           Control.Concurrent.STM
-import           Control.Exception
-import           Control.Monad.Trans
-import qualified Data.ByteString.Char8 as C8
-import           Data.Typeable
-import           GHC.Conc (unsafeIOToSTM)
-import           Network.HTTP.Lucu.Config
-import           Network.HTTP.Lucu.DefaultPage
-import           Network.HTTP.Lucu.Headers
-import           Network.HTTP.Lucu.Request
-import           Network.HTTP.Lucu.Response
-import           System.IO.Unsafe
-import           Text.XML.HXT.Arrow.WriteDocument
-import           Text.XML.HXT.Arrow.XmlArrow
-import           Text.XML.HXT.Arrow.XmlState
-
-
-data Abortion = Abortion {
-      aboStatus  :: !StatusCode
-    , aboHeaders :: !Headers
-    , aboMessage :: !(Maybe String)
-    } deriving (Show, Typeable)
-
-instance Exception Abortion
-
--- |Computation of @'abort' status headers msg@ aborts the
--- 'Network.HTTP.Lucu.Resource.Resource' monad with given status,
--- additional response headers, and optional message string.
---
--- What this really does is to throw a special
--- 'Control.Exception.Exception'. The exception will be caught by the
--- Lucu system.
---
--- 1. If the 'Network.HTTP.Lucu.Resource.Resource' is in the /Deciding
---    Header/ or any precedent states, it is possible to use the
---    @status@ and such like as a HTTP response to be sent to the
---    client.
---
--- 2. Otherwise the HTTP response can't be modified anymore so the
---    only possible thing the system can do is to dump it to the
---    stderr. See
---    'Network.HTTP.Lucu.Config.cnfDumpTooLateAbortionToStderr'.
---
--- Note that the status code doesn't have to be an error code so you
--- can use this action for redirection as well as error reporting e.g.
---
--- > abort MovedPermanently
--- >       [("Location", "http://example.net/")]
--- >       (Just "It has been moved to example.net")
-abort :: MonadIO m => StatusCode -> [ (String, String) ] -> Maybe String -> m a
-abort status headers msg
-    = status `seq` headers `seq` msg `seq`
-      let abo = Abortion status (toHeaders $ map pack headers) msg
-      in
-        liftIO $ throwIO abo
-    where
-      pack (x, y) = (C8.pack x, C8.pack y)
-
--- |This is similar to 'abort' but computes it with
--- 'System.IO.Unsafe.unsafePerformIO'.
-abortPurely :: StatusCode -> [ (String, String) ] -> Maybe String -> a
-abortPurely = ((unsafePerformIO .) .) . abort
-
--- |Computation of @'abortSTM' status headers msg@ just computes
--- 'abort' in a 'Control.Monad.STM.STM' monad.
-abortSTM :: StatusCode -> [ (String, String) ] -> Maybe String -> STM a
-abortSTM status headers msg
-    = status `seq` headers `seq` msg `seq`
-      unsafeIOToSTM $! abort status headers msg
-
--- | Computation of @'abortA' -< (status, (headers, msg))@ just
--- computes 'abort' in an 'Control.Arrow.ArrowIO.ArrowIO'.
-abortA :: ArrowIO a => a (StatusCode, ([ (String, String) ], Maybe String)) c
-abortA 
-    = arrIO3 abort
-
--- aboMessage が Just なら單に mkDefaultPage に渡すだけで良いので樂だが、
--- Nothing の場合は getDefaultPage を使ってデフォルトのメッセージを得な
--- ければならない。
-abortPage :: Config -> Maybe Request -> Response -> Abortion -> String
-abortPage conf reqM res abo
-    = conf `seq` reqM `seq` res `seq` abo `seq`
-      case aboMessage abo of
-        Just msg
-            -> let [html] = unsafePerformIO 
-                            $ runX ( mkDefaultPage conf (aboStatus abo) (txt msg)
-                                     >>>
-                                     writeDocumentToString [ withIndent True ]
-                                   )
-               in
-                 html
-        Nothing
-            -> let res'  = res { resStatus = aboStatus abo }
-                   res'' = foldl (.) id [setHeader name value
-                                             | (name, value) <- fromHeaders $ aboHeaders abo] res'
-               in
-                 getDefaultPage conf reqM res''
+import Control.Exception
+import Control.Monad.Trans
+import Data.Ascii (Ascii, CIAscii)
+import Data.Monoid.Unicode
+import Data.Text (Text)
+import Network.HTTP.Lucu.Abortion.Internal
+import Network.HTTP.Lucu.Headers
+import Network.HTTP.Lucu.Response
+import Prelude.Unicode
+
+-- |Construct an 'Abortion' with additional headers and an optional
+-- message text.
+mkAbortion ∷ StatusCode → [(CIAscii, Ascii)] → Maybe Text → Abortion
+{-# INLINE mkAbortion #-}
+mkAbortion sc hdr msg
+    = Abortion {
+        aboStatus  = sc
+      , aboHeaders = toHeaders hdr
+      , aboMessage = msg
+      }
+
+-- |Construct an 'Abortion' without any additional headers but with a
+-- message text.
+mkAbortion' ∷ StatusCode → Text → Abortion
+{-# INLINE mkAbortion' #-}
+mkAbortion' sc msg
+    = Abortion {
+        aboStatus  = sc
+      , aboHeaders = (∅)
+      , aboMessage = Just msg
+      }
+
+-- |Throw an 'Abortion' in a 'MonadIO', including the very
+-- 'Network.HTTP.Lucu.Resource.Resource' monad.
+abort ∷ MonadIO m ⇒ Abortion → m a
+{-# INLINE abort #-}
+abort = liftIO ∘ throwIO
diff --git a/Network/HTTP/Lucu/Abortion/Internal.hs b/Network/HTTP/Lucu/Abortion/Internal.hs
new file mode 100644 (file)
index 0000000..f71e045
--- /dev/null
@@ -0,0 +1,74 @@
+{-# LANGUAGE
+    DeriveDataTypeable
+  , UnicodeSyntax
+  #-}
+module Network.HTTP.Lucu.Abortion.Internal
+    ( Abortion(..)
+    , abortPage
+    )
+    where
+import Blaze.ByteString.Builder (Builder)
+import qualified Blaze.ByteString.Builder.Char.Utf8 as BB
+import Control.Arrow.ListArrow
+import Control.Arrow.Unicode
+import Control.Exception
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Typeable
+import Network.HTTP.Lucu.Config
+import Network.HTTP.Lucu.DefaultPage
+import Network.HTTP.Lucu.Headers
+import Network.HTTP.Lucu.Request
+import Network.HTTP.Lucu.Response
+import Prelude.Unicode
+import Text.XML.HXT.Arrow.WriteDocument
+import Text.XML.HXT.Arrow.XmlArrow
+import Text.XML.HXT.Arrow.XmlState
+
+-- |'Abortion' is an 'Exception' that aborts the execution of
+-- 'Network.HTTP.Lucu.Resource.Resource' monad with a 'StatusCode',
+-- additional response headers, and an optional message text.
+--
+-- 1. If the 'Network.HTTP.Lucu.Resource.Resource' is in the /Deciding
+--    Header/ or any precedent states, throwing an 'Abortion' affects
+--    the HTTP response to be sent to the client.
+--
+-- 2. Otherwise it's too late to overwrite the HTTP response so the
+--    only possible thing the system can do is to dump the exception
+--    to the stderr. See 'cnfDumpTooLateAbortionToStderr'.
+--
+-- Note that the status code doesn't necessarily have to satisfy
+-- 'isError' so you can abuse this exception for redirections as well
+-- as error reporting e.g.
+--
+-- > abort $ mkAbortion MovedPermanently
+-- >         [("Location", "http://example.net/")]
+-- >         "It has been moved to example.net"
+data Abortion = Abortion {
+      aboStatus  ∷ !StatusCode
+    , aboHeaders ∷ !Headers
+    , aboMessage ∷ !(Maybe Text)
+    } deriving (Eq, Show, Typeable)
+
+instance Exception Abortion
+
+instance HasHeaders Abortion where
+    getHeaders         = aboHeaders
+    setHeaders abo hdr = abo { aboHeaders = hdr }
+
+abortPage ∷ Config → Maybe Request → Response → Abortion → Builder
+abortPage conf reqM res abo
+    = case aboMessage abo of
+        Just msg
+            → let [html] = runLA ( mkDefaultPage conf (aboStatus abo) (txt $ T.unpack msg)
+                                   ⋙
+                                   writeDocumentToString [ withIndent True ]
+                                 ) ()
+              in
+                BB.fromString html
+        Nothing
+            → let res'  = res { resStatus = aboStatus abo }
+                  res'' = foldl (∘) id [setHeader name value
+                                            | (name, value) ← fromHeaders $ aboHeaders abo] res'
+               in
+                 getDefaultPage conf reqM res''
diff --git a/Network/HTTP/Lucu/Authentication.hs b/Network/HTTP/Lucu/Authentication.hs
new file mode 100644 (file)
index 0000000..753af6e
--- /dev/null
@@ -0,0 +1,78 @@
+{-# LANGUAGE
+    OverloadedStrings
+  , UnicodeSyntax
+  #-}
+-- |HTTP Authentication
+module Network.HTTP.Lucu.Authentication
+    ( AuthChallenge(..)
+    , AuthCredential(..)
+    , Realm
+    , UserID
+    , Password
+
+    , printAuthChallenge
+    , authCredential
+    )
+    where
+import Control.Monad
+import Data.Ascii (Ascii)
+import qualified Data.Ascii as A
+import Data.Attoparsec.Char8
+import qualified Data.ByteString.Base64 as B64
+import qualified Data.ByteString.Char8 as C8
+import Data.Monoid.Unicode
+import Network.HTTP.Lucu.Parser.Http
+import Network.HTTP.Lucu.Utils
+import Prelude.Unicode
+
+-- |Authentication challenge to be sent to clients with
+-- \"WWW-Authenticate\" header field. See
+-- 'Network.HTTP.Lucu.Resource.setWWWAuthenticate'.
+data AuthChallenge
+    = BasicAuthChallenge !Realm
+      deriving (Eq)
+
+-- |'Realm' is just an 'Ascii' string.
+type Realm = Ascii
+
+-- |Authorization credential to be sent by client with
+-- \"Authorization\" header. See
+-- 'Network.HTTP.Lucu.Resource.getAuthorization'.
+data AuthCredential
+    = BasicAuthCredential !UserID !Password
+      deriving (Show, Eq)
+
+-- |'UserID' is just an 'Ascii' string containing no colons (\':\').
+type UserID = Ascii
+
+-- |'Password' is just an 'Ascii' string.
+type Password = Ascii
+
+-- |Convert an 'AuthChallenge' to 'Ascii'.
+printAuthChallenge ∷ AuthChallenge → Ascii
+printAuthChallenge (BasicAuthChallenge realm)
+    = A.fromAsciiBuilder $
+      A.toAsciiBuilder "Basic realm=" ⊕ quoteStr realm
+
+-- |'Parser' for an 'AuthCredential'.
+authCredential ∷ Parser AuthCredential
+authCredential
+    = do void $ string "Basic"
+         skipMany1 lws
+         b64 ← takeWhile1 base64
+         case C8.break (≡ ':') (B64.decodeLenient b64) of
+           (user, cPassword)
+               | C8.null cPassword
+                   → fail "no colons in the basic auth credential"
+               | otherwise
+                   → do u ← asc user
+                        p ← asc (C8.tail cPassword)
+                        return (BasicAuthCredential u p)
+    where
+      base64 ∷ Char → Bool
+      base64 = inClass "a-zA-Z0-9+/="
+
+      asc ∷ C8.ByteString → Parser Ascii
+      asc bs = case A.fromByteString bs of
+                 Just as → return as
+                 Nothing → fail "Non-ascii character in auth credential"
diff --git a/Network/HTTP/Lucu/Authorization.hs b/Network/HTTP/Lucu/Authorization.hs
deleted file mode 100644 (file)
index 6b0e1c2..0000000
+++ /dev/null
@@ -1,71 +0,0 @@
-{-# LANGUAGE
-    UnicodeSyntax
-  #-}
-{-# OPTIONS_HADDOCK prune #-}
-
--- |Manipulation of WWW authorization.
-module Network.HTTP.Lucu.Authorization
-    ( AuthChallenge(..)
-    , AuthCredential(..)
-    , Realm
-    , UserID
-    , Password
-
-    , authCredentialP -- private
-    )
-    where
-import qualified Data.ByteString.Base64 as B64
-import qualified Data.ByteString.Char8 as C8
-import Network.HTTP.Lucu.Parser
-import Network.HTTP.Lucu.Parser.Http
-import Network.HTTP.Lucu.Utils
-import Prelude.Unicode
-
--- |Authorization challenge to be sent to client with
--- \"WWW-Authenticate\" header. See
--- 'Network.HTTP.Lucu.Resource.setWWWAuthenticate'.
-data AuthChallenge
-    = BasicAuthChallenge Realm
-      deriving (Eq)
-
--- |'Realm' is just a string which must not contain any non-ASCII letters.
-type Realm = String
-
--- |Authorization credential to be sent by client with
--- \"Authorization\" header. See
--- 'Network.HTTP.Lucu.Resource.getAuthorization'.
-data AuthCredential
-    = BasicAuthCredential UserID Password
-      deriving (Show, Eq)
-
--- |'UserID' is just a string which must not contain colon and any
--- non-ASCII letters.
-type UserID   = String
-
--- |'Password' is just a string which must not contain any non-ASCII
--- letters.
-type Password = String
-
-instance Show AuthChallenge where
-    show (BasicAuthChallenge realm)
-        = "Basic realm=" ⧺ quoteStr realm
-
-authCredentialP ∷ Parser AuthCredential
-authCredentialP
-    = allowEOF $!
-      do _   ← string "Basic"
-         _   ← many1 lws
-         b64 ← many1
-               $ satisfy (\c → (c ≥ 'a' ∧ c ≤ 'z') ∨
-                               (c ≥ 'A' ∧ c ≤ 'Z') ∨
-                               (c ≥ '0' ∧ c ≤ '9') ∨
-                                c ≡ '+' ∨
-                                c ≡ '/' ∨
-                                c ≡ '=')
-         case break (≡ ':') (decode b64) of
-           (uid, ':' : password)
-               → return (BasicAuthCredential uid password)
-           _   → failP
-    where
-      decode ∷ String → String
-      decode = C8.unpack ∘ B64.decodeLenient ∘ C8.pack
index 27deb740821f9c68bd5b3159c08469513c7c222d..e8c9de41286c4fb3240425843acc67c52e565db5 100644 (file)
@@ -1,38 +1,34 @@
+{-# LANGUAGE
+    UnicodeSyntax
+  #-}
 module Network.HTTP.Lucu.Chunk
-    ( chunkHeaderP  -- Num a => Parser a
-    , chunkFooterP  -- Parser ()
-    , chunkTrailerP -- Parser Headers
+    ( chunkHeader
+    , chunkFooter
+    , chunkTrailer
     )
     where
-
-import           Network.HTTP.Lucu.Headers
-import           Network.HTTP.Lucu.Parser
-import           Network.HTTP.Lucu.Parser.Http
-import           Numeric
-
-
-chunkHeaderP :: Num a => Parser a
-chunkHeaderP = do hexLen <- many1 hexDigit
-                  _      <- extension
-                  _      <- crlf
-
-                  let [(len, _)] = readHex hexLen
-                  return len
+import Control.Applicative
+import Data.Attoparsec.Char8
+import Data.Bits
+import Network.HTTP.Lucu.Headers
+import Network.HTTP.Lucu.Parser.Http
+
+chunkHeader ∷ (Integral a, Bits a) ⇒ Parser a
+{-# INLINEABLE chunkHeader #-}
+chunkHeader = do len ← hexadecimal
+                 extension
+                 crlf
+                 return len
     where
-      extension :: Parser ()
-      extension = many ( char ';' >>
-                         token    >>
-                         char '=' >>
-                         ( token <|> quotedStr )
-                       )
-                  >>
-                  return ()
-{-# SPECIALIZE chunkHeaderP :: Parser Int #-}
-
-
-chunkFooterP :: Parser ()
-chunkFooterP = crlf >> return ()
-
-
-chunkTrailerP :: Parser Headers
-chunkTrailerP = headersP
+      extension ∷ Parser ()
+      extension
+          = skipMany ( char ';' *>
+                       token    *>
+                       char '=' *>
+                       (token <|> quotedStr) )
+
+chunkFooter ∷ Parser ()
+chunkFooter = crlf
+
+chunkTrailer ∷ Parser Headers
+chunkTrailer = headers
index cb3f4a8b4b5e68f6e9e105ed9d0e4624ceb209ae..2ea2055de853c35faa89f4d2e21cf3d3c2d74d83 100644 (file)
--- |Configurations for the Lucu httpd like a port to listen.
+{-# LANGUAGE
+    OverloadedStrings
+  , UnicodeSyntax
+  #-}
+-- |Configurations for the Lucu httpd.
 module Network.HTTP.Lucu.Config
     ( Config(..)
     , SSLConfig(..)
     , defaultConfig
     )
     where
-
-import qualified Data.ByteString as Strict (ByteString)
-import qualified Data.ByteString.Char8 as C8 hiding (ByteString)
-import           Network
-import           Network.BSD
-import           Network.HTTP.Lucu.MIMEType.Guess
-import           Network.HTTP.Lucu.MIMEType.DefaultExtensionMap
-import           OpenSSL.Session
-import           System.IO.Unsafe
-
--- |Configuration record for the Lucu httpd. You need to use
--- 'defaultConfig' or setup your own configuration to run the httpd.
+import Data.Ascii (Ascii)
+import Data.Text (Text)
+import qualified Data.Text as T
+import Network
+import Network.BSD
+import Network.HTTP.Lucu.MIMEType.Guess
+import Network.HTTP.Lucu.MIMEType.DefaultExtensionMap
+import OpenSSL.Session
+import System.IO.Unsafe
+
+-- |Configuration record for to run the httpd.
 data Config = Config {
 
-    -- |A string which will be sent to clients as \"Server\" field.
-      cnfServerSoftware :: !Strict.ByteString
+    -- |A banner string to be sent to clients with \"Server\" response
+    -- header field.
+      cnfServerSoftware ∷ !Ascii
 
     -- |The host name of the server. This value will be used in
     -- built-in pages like \"404 Not Found\".
-    , cnfServerHost :: !Strict.ByteString
+    , cnfServerHost ∷ !Text
 
-    -- |A port number (or service name) to listen to HTTP clients.
-    , cnfServerPort :: !ServiceName
+    -- |A port number (or service name) to listen to HTTP clients.
+    , cnfServerPort  !ServiceName
 
     -- |Local IPv4 address to listen to both HTTP and HTTPS
     -- clients. Set this to @('Just' "0.0.0.0")@ if you want to accept
     -- any IPv4 connections. Set this to 'Nothing' to disable IPv4.
-    , cnfServerV4Addr :: !(Maybe HostName)
+    , cnfServerV4Addr  !(Maybe HostName)
 
     -- |Local IPv6 address to listen to both HTTP and HTTPS
     -- clients. Set this to @('Just' "::")@ if you want to accept any
     -- IPv6 connections. Set this to 'Nothing' to disable IPv6. Note
-    -- that there is currently no way to assign separate ports to IPv4
-    -- and IPv6 server sockets.
-    , cnfServerV6Addr :: !(Maybe HostName)
+    -- that there is currently no ways to assign separate ports to
+    -- IPv4 and IPv6 server sockets (but I don't think that will be a
+    -- problem.)
+    , cnfServerV6Addr ∷ !(Maybe HostName)
 
     -- |Configuration for HTTPS connections. Set this 'Nothing' to
     -- disable HTTPS.
-    , cnfSSLConfig :: !(Maybe SSLConfig)
+    , cnfSSLConfig  !(Maybe SSLConfig)
 
-    -- |The maximum number of requests to accept in one connection
-    -- simultaneously. If a client exceeds this limitation, its last
+    -- |The maximum number of requests to simultaneously accept in one
+    -- connection. If a client exceeds this limitation, its last
     -- request won't be processed until a response for its earliest
     -- pending request is sent back to the client.
-    , cnfMaxPipelineDepth :: !Int
+    , cnfMaxPipelineDepth  !Int
 
-    -- |The maximum length of request entity to accept in bytes. Note
-    -- that this is nothing but the default value which is used when
-    -- 'Network.HTTP.Lucu.Resource.input' and such like are applied to
-    -- 'Network.HTTP.Lucu.Resource.defaultLimit', so there is no
-    -- guarantee that this value always constrains all the requests.
-    , cnfMaxEntityLength :: !Int
+    -- |The maximum length of request entity to accept in octets. Note
+    -- that this is nothing but a default value used by
+    -- 'Network.HTTP.Lucu.Resource.getForm' and such when they are
+    -- applied to 'Nothing', so there is no guarantee that this value
+    -- always constrains all the requests.
+    , cnfMaxEntityLength  !Int
 
-    -- |The maximum length of chunk to output. This value is used by
-    -- 'Network.HTTP.Lucu.Resource.output' and such like to limit the
-    -- chunk length so you can safely output an infinite string (like
-    -- a lazy stream of \/dev\/random) using those actions.
-    , cnfMaxOutputChunkLength :: !Int
-
-    -- | Whether to dump too late abortion to the stderr or not. See
+    -- |Whether to dump too late abortions to the stderr or not. See
     -- 'Network.HTTP.Lucu.Abortion.abort'.
-    , cnfDumpTooLateAbortionToStderr :: !Bool
+    , cnfDumpTooLateAbortionToStderr  !Bool
 
-    -- |A mapping from extension to MIME Type. This value is used by
-    -- 'Network.HTTP.Lucu.StaticFile.staticFile' to guess the MIME
-    -- Type of static files. Note that MIME Types are currently
-    -- guessed only by file name. 
+    -- |A mapping table from file extensions to MIME Types. This value
+    -- is used by 'Network.HTTP.Lucu.StaticFile.staticFile' to guess
+    -- the MIME Type of static files. Note that MIME Types are
+    -- currently guessed only by file name.
     -- 
-    -- Guessing by file magic is indeed a wonderful idea but that is
-    -- not implemented (yet). But, don't you think it's better a file
-    -- system got a MIME Type as a part of inode? Or it might be a
-    -- good idea to use GnomeVFS
-    -- (<http://developer.gnome.org/doc/API/2.0/gnome-vfs-2.0/>)
-    -- instead of vanilla FS.
-    , cnfExtToMIMEType :: !ExtMap
+    -- Guessing by file magic might be a good idea but that's not
+    -- implemented (yet).
+    , cnfExtToMIMEType ∷ !ExtMap
     }
 
 -- |Configuration record for HTTPS connections.
 data SSLConfig
     = SSLConfig {
-        -- |A port ID to listen to HTTPS clients. Local addresses
-        -- (both for IPv4 and IPv6) will be derived from the parent
-        -- 'Config'.
-        sslServerPort :: !ServiceName
-
-        -- |An SSL context for accepting connections.
-      , sslContext    :: !SSLContext
+        -- |A port number (or a service name) to listen to HTTPS
+        -- clients. Local addresses (both for IPv4 and IPv6) will be
+        -- derived from the parent 'Config'.
+        sslServerPort ∷ !ServiceName
+
+        -- |An SSL context for accepting connections. You must set it
+        -- up yourself with at least a server certification.
+      , sslContext ∷ !SSLContext
       }
 
 -- |The default configuration. Generally you can use this value as-is,
 -- or possibly you just want to replace the 'cnfServerSoftware' and
 -- 'cnfServerPort'. SSL connections are disabled by default.
-defaultConfig :: Config
+defaultConfig  Config
 defaultConfig = Config {
-                  cnfServerSoftware              = C8.pack "Lucu/1.0"
-                , cnfServerHost                  = C8.pack (unsafePerformIO getHostName)
+                  cnfServerSoftware              = "Lucu/1.0"
+                , cnfServerHost                  = T.pack (unsafePerformIO getHostName)
                 , cnfServerPort                  = "http"
                 , cnfServerV4Addr                = Just "0.0.0.0"
                 , cnfServerV6Addr                = Just "::"
                 , cnfSSLConfig                   = Nothing
                 , cnfMaxPipelineDepth            = 100
                 , cnfMaxEntityLength             = 16 * 1024 * 1024 -- 16 MiB
-                , cnfMaxOutputChunkLength        = 5 * 1024 * 1024  -- 5 MiB
                 , cnfDumpTooLateAbortionToStderr = True
                 , cnfExtToMIMEType               = defaultExtensionMap
                 }
index 27a89415a0d9e1e420ba7f57cd00815c594abb4a..a5f02b13ecf69ac28f15e747016f4af124eb2191 100644 (file)
@@ -1,48 +1,58 @@
+{-# LANGUAGE
+    OverloadedStrings
+  , UnicodeSyntax
+  #-}
 module Network.HTTP.Lucu.ContentCoding
-    ( acceptEncodingListP
+    ( AcceptEncoding(..)
+
+    , acceptEncodingList
     , normalizeCoding
     , unnormalizeCoding
-    , orderAcceptEncodings
     )
     where
-
-import           Data.Char
-import           Data.Ord
-import           Data.Maybe
-import           Network.HTTP.Lucu.Parser
-import           Network.HTTP.Lucu.Parser.Http
-
-
-acceptEncodingListP :: Parser [(String, Maybe Double)]
-acceptEncodingListP = allowEOF $! listOf accEncP
-
-      
-accEncP :: Parser (String, Maybe Double)
-accEncP = do coding <- token
-             qVal   <- option Nothing
-                       $ do _ <- string ";q="
-                            q <- qvalue
-                            return $ Just q
-             return (normalizeCoding coding, qVal)
-
-
-normalizeCoding :: String -> String
+import Control.Applicative
+import Data.Ascii (CIAscii, toCIAscii)
+import Data.Attoparsec.Char8
+import Data.Ord
+import Data.Maybe
+import Network.HTTP.Lucu.Parser.Http
+import Prelude.Unicode
+
+data AcceptEncoding
+    = AcceptEncoding {
+        aeEncoding ∷ !CIAscii
+      , aeQValue   ∷ !(Maybe Double)
+      }
+      deriving (Eq, Show)
+
+instance Ord AcceptEncoding where
+    (AcceptEncoding c1 q1) `compare` (AcceptEncoding c2 q2)
+        | q1' > q1' = GT
+        | q1' < q2' = LT
+        | otherwise = compare c1 c2
+        where
+          q1' = fromMaybe 0 q1
+          q2' = fromMaybe 0 q2
+
+acceptEncodingList ∷ Parser [AcceptEncoding]
+acceptEncodingList = listOf accEnc
+
+accEnc ∷ Parser AcceptEncoding
+accEnc = do coding ← toCIAscii <$> token
+            qVal   ← option Nothing
+                     $ do _ ← string ";q="
+                          q ← qvalue
+                          return $ Just q
+            return $ AcceptEncoding (normalizeCoding coding) qVal
+
+normalizeCoding ∷ CIAscii → CIAscii
 normalizeCoding coding
-    = case map toLower coding of
-        "x-gzip"     -> "gzip"
-        "x-compress" -> "compress"
-        other        -> other
-
+    | coding ≡ "x-gzip"     = "gzip"
+    | coding ≡ "x-compress" = "compress"
+    | otherwise             = coding
 
-unnormalizeCoding :: String -> String
+unnormalizeCoding ∷ CIAscii → CIAscii
 unnormalizeCoding coding
-    = case map toLower coding of
-        "gzip"     -> "x-gzip"
-        "compress" -> "x-compress"
-        other        -> other
-
-
-orderAcceptEncodings :: (String, Maybe Double) -> (String, Maybe Double) -> Ordering
-orderAcceptEncodings (_, q1) (_, q2)
-    = comparing (fromMaybe 0) q1 q2
-
+    | coding ≡ "gzip"     = "x-gzip"
+    | coding ≡ "compress" = "x-compress"
+    | otherwise           = coding
index 12aba154480cef33dc525acd04fe290423dcb48b..19a72936e2718608034e22c15885fb9ea2fbe747 100644 (file)
@@ -1,93 +1,82 @@
 {-# LANGUAGE
-    BangPatterns
-  , UnboxedTuples
+    OverloadedStrings
   , UnicodeSyntax
   #-}
 module Network.HTTP.Lucu.DefaultPage
     ( getDefaultPage
-    , writeDefaultPage
+    , defaultPageContentType
     , mkDefaultPage
     )
     where
-
-import           Control.Arrow
-import           Control.Arrow.ArrowList
-import           Control.Concurrent.STM
-import           Control.Monad
-import qualified Data.ByteString.Char8 as C8
-import qualified Data.ByteString.Lazy.Char8 as L8
-import           Data.Maybe
-import           Network.HTTP.Lucu.Config
-import           Network.HTTP.Lucu.Format
-import           Network.HTTP.Lucu.Headers
-import           Network.HTTP.Lucu.Interaction
-import           Network.HTTP.Lucu.Request
-import           Network.HTTP.Lucu.Response
-import           Network.URI hiding (path)
-import           System.IO.Unsafe
-import           Text.XML.HXT.Arrow.WriteDocument
-import           Text.XML.HXT.Arrow.XmlArrow
-import           Text.XML.HXT.Arrow.XmlState
-import           Text.XML.HXT.DOM.TypeDefs
-
-
-getDefaultPage :: Config -> Maybe Request -> Response -> String
-getDefaultPage !conf !req !res
-    = let msgA = getMsg req res
+import Blaze.ByteString.Builder (Builder)
+import qualified Blaze.ByteString.Builder.Char.Utf8 as BB
+import Control.Arrow
+import Control.Arrow.ArrowList
+import Control.Arrow.ListArrow
+import Control.Arrow.Unicode
+import Data.Ascii (Ascii)
+import qualified Data.Ascii as A
+import Data.Maybe
+import qualified Data.Text as T
+import Network.HTTP.Lucu.Config
+import Network.HTTP.Lucu.Headers
+import Network.HTTP.Lucu.Request
+import Network.HTTP.Lucu.Response
+import Network.URI hiding (path)
+import Prelude.Unicode
+import Text.XML.HXT.Arrow.WriteDocument
+import Text.XML.HXT.Arrow.XmlArrow
+import Text.XML.HXT.Arrow.XmlState
+import Text.XML.HXT.DOM.TypeDefs
+
+getDefaultPage ∷ Config → Maybe Request → Response → Builder
+{-# INLINEABLE getDefaultPage #-}
+getDefaultPage conf req res
+    = let msgA     = getMsg req res
+          [xmlStr] = runLA ( mkDefaultPage conf (resStatus res) msgA
+                             ⋙ 
+                             writeDocumentToString [ withIndent True ]
+                           ) ()
       in
-        unsafePerformIO $
-        do [xmlStr] <- runX ( mkDefaultPage conf (resStatus res) msgA
-                              >>>
-                              writeDocumentToString [ withIndent True ]
-                            )
-           return xmlStr
-
-
-writeDefaultPage :: Interaction -> STM ()
-writeDefaultPage !itr
-    -- Content-Type が正しくなければ補完できない。
-    = do res <- readItr itr itrResponse id
-         when (getHeader (C8.pack "Content-Type") res == Just defaultPageContentType)
-                  $ do reqM <- readItr itr itrRequest id
-
-                       let conf = itrConfig itr
-                           page = L8.pack $ getDefaultPage conf reqM res
-
-                       writeTVar (itrBodyToSend itr)
-                                     $ page
-
-
-mkDefaultPage :: (ArrowXml a) => Config -> StatusCode -> a b XmlTree -> a b XmlTree
-mkDefaultPage !conf !status !msgA
-    = let (# sCode, sMsg #) = statusCode status
-          sig               = C8.unpack (cnfServerSoftware conf)
-                              ++ " at "
-                              ++ C8.unpack (cnfServerHost conf)
+        BB.fromString xmlStr
+
+defaultPageContentType ∷ Ascii
+{-# INLINE defaultPageContentType #-}
+defaultPageContentType = "application/xhtml+xml"
+
+mkDefaultPage ∷ (ArrowXml a) ⇒ Config → StatusCode → a b XmlTree → a b XmlTree
+{-# INLINEABLE mkDefaultPage #-}
+mkDefaultPage conf status msgA
+    = let sStr = A.toString $ A.fromAsciiBuilder $ printStatusCode status
+          sig  = concat [ A.toString (cnfServerSoftware conf)
+                        , " at "
+                        , T.unpack (cnfServerHost conf)
+                        ]
       in ( eelem "/"
            += ( eelem "html"
                 += sattr "xmlns" "http://www.w3.org/1999/xhtml"
                 += ( eelem "head"
                      += ( eelem "title"
-                          += txt (fmtDec 3 sCode ++ " " ++ C8.unpack sMsg)
+                          += txt sStr
                         ))
                 += ( eelem "body"
                      += ( eelem "h1"
-                          += txt (C8.unpack sMsg)
+                          += txt sStr
                         )
                      += ( eelem "p" += msgA )
                      += eelem "hr"
                      += ( eelem "address" += txt sig ))))
-{-# SPECIALIZE mkDefaultPage :: Config -> StatusCode -> IOSArrow b XmlTree -> IOSArrow b XmlTree #-}
 
-getMsg :: (ArrowXml a) => Maybe Request -> Response -> a b XmlTree
-getMsg !req !res
+getMsg ∷ (ArrowXml a) ⇒ Maybe Request → Response → a b XmlTree
+{-# INLINEABLE getMsg #-}
+getMsg req res
     = case resStatus res of
         -- 1xx は body を持たない
         -- 2xx の body は補完しない
 
         -- 3xx
         MovedPermanently
-            -> txt ("The resource at " ++ path ++ " has been moved to ")
+            → txt ("The resource at " ⧺ path ⧺ " has been moved to ")
                <+>
                eelem "a" += sattr "href" loc
                          += txt loc
@@ -95,7 +84,7 @@ getMsg !req !res
                txt " permanently."
 
         Found
-            -> txt ("The resource at " ++ path ++ " is currently located at ")
+            → txt ("The resource at " ⧺ path ⧺ " is currently located at ")
                <+>
                eelem "a" += sattr "href" loc
                          += txt loc
@@ -103,7 +92,7 @@ getMsg !req !res
                txt ". This is not a permanent relocation."
 
         SeeOther
-            -> txt ("The resource at " ++ path ++ " can be found at ")
+            → txt ("The resource at " ⧺ path ⧺ " can be found at ")
                <+>
                eelem "a" += sattr "href" loc
                          += txt loc
@@ -111,7 +100,7 @@ getMsg !req !res
                txt "."
 
         TemporaryRedirect
-            -> txt ("The resource at " ++ path ++ " is temporarily located at ")
+            → txt ("The resource at " ⧺ path ⧺ " is temporarily located at ")
                <+>
                eelem "a" += sattr "href" loc
                          += txt loc
@@ -120,43 +109,40 @@ getMsg !req !res
 
         -- 4xx
         BadRequest
-            -> txt "The server could not understand the request you sent."
+             txt "The server could not understand the request you sent."
 
         Unauthorized
-            -> txt ("You need a valid authentication to access " ++ path)
+            → txt ("You need a valid authentication to access " ⧺ path)
 
         Forbidden
-            -> txt ("You don't have permission to access " ++ path)
+            → txt ("You don't have permission to access " ⧺ path)
 
         NotFound
-            -> txt ("The requested URL " ++ path ++ " was not found on this server.")
+            → txt ("The requested URL " ⧺ path ⧺ " was not found on this server.")
 
         Gone
-            -> txt ("The resource at " ++ path ++ " was here in past times, but has gone permanently.")
+            → txt ("The resource at " ⧺ path ⧺ " was here in past times, but has gone permanently.")
 
         RequestEntityTooLarge
-            -> txt ("The request entity you sent for " ++ path ++ " was too big to accept.")
+            → txt ("The request entity you sent for " ⧺ path ⧺ " was too large to accept.")
 
         RequestURITooLarge
-            -> txt "The request URI you sent was too big to accept."
+            → txt "The request URI you sent was too large to accept."
 
         -- 5xx
         InternalServerError
-            -> txt ("An internal server error has occured during the process of your request to " ++ path)
+            → txt ("An internal server error has occured during the process of your request to " ⧺ path)
 
         ServiceUnavailable
-            -> txt "The service is temporarily unavailable. Try later."
+             txt "The service is temporarily unavailable. Try later."
 
-        _  -> none
+        _   none
 
-                            
     where
-      path :: String
-      path = let uri = reqURI $! fromJust req
+      path  String
+      path = let uri = reqURI $ fromJust req
              in
                uriPath uri
 
-      loc :: String
-      loc = C8.unpack $! fromJust $! getHeader (C8.pack "Location") res
-
-{-# SPECIALIZE getMsg :: Maybe Request -> Response -> IOSArrow b XmlTree #-}
\ No newline at end of file
+      loc ∷ String
+      loc = A.toString $ fromJust $ getHeader "Location" res
index d607ad12db4d2fa22ec529a2f0456f9c7e4644f7..76df18378bf3e48417dddd8c73dc6222b65d5136 100644 (file)
@@ -1,58 +1,90 @@
-{-# OPTIONS_HADDOCK prune #-}
-
--- |Manipulation of entity tags.
+{-# LANGUAGE
+    OverloadedStrings
+  , UnicodeSyntax
+  #-}
+-- |Entity tags
 module Network.HTTP.Lucu.ETag
     ( ETag(..)
+    , parseETag
+    , printETag
+
     , strongETag
     , weakETag
-    , eTagP
-    , eTagListP
+    , eTag
+    , eTagList
     )
     where
+import Control.Applicative
+import Control.Monad
+import Data.Ascii (Ascii, AsciiBuilder)
+import qualified Data.Ascii as A
+import Data.Attoparsec.Char8
+import Data.Monoid.Unicode
+import Network.HTTP.Lucu.Parser.Http hiding (token)
+import Network.HTTP.Lucu.Utils
+import Prelude.Unicode
 
-import           Control.Monad
-import           Network.HTTP.Lucu.Parser
-import           Network.HTTP.Lucu.Parser.Http hiding (token)
-import           Network.HTTP.Lucu.Utils
-
--- |An entity tag is made of a weakness flag and a opaque string.
+-- |An entity tag consists of a weakness flag and an opaque string.
 data ETag = ETag {
       -- |The weakness flag. Weak tags looks like W\/\"blahblah\" and
-      -- strong tags are like \"blahblah\".
-      etagIsWeak :: !Bool
+      -- strong tags are like \"blahblah\". See:
+      -- <http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.3.3>
+      etagIsWeak ∷ !Bool
       -- |An opaque string. Only characters from 0x20 (sp) to 0x7e (~)
       -- are allowed.
-    , etagToken  :: !String
-    } deriving (Eq)
-
-instance Show ETag where
-    show (ETag isWeak token) = (if isWeak then
-                                    "W/"
-                                else
-                                    "")
-                               ++
-                               quoteStr token
-
--- |This is equivalent to @'ETag' 'Prelude.False'@. If you want to
--- generate an ETag from a file, try using
+    , etagToken  ∷ !Ascii
+    } deriving (Eq, Show)
+
+-- |Convert an 'ETag' to an 'AsciiBuilder'.
+printETag ∷ ETag → AsciiBuilder
+{-# INLINEABLE printETag #-}
+printETag et
+    = ( if etagIsWeak et then
+            A.toAsciiBuilder "W/"
+        else
+            (∅)
+      )
+      ⊕
+      quoteStr (etagToken et)
+
+-- |Parse 'Etag' from an 'Ascii'. This functions throws an exception
+-- for parse error.
+parseETag ∷ Ascii → ETag
+{-# INLINEABLE parseETag #-}
+parseETag str
+    = case parseOnly p $ A.toByteString str of
+        Right et → et
+        Left err → error ("unparsable ETag: " ⧺ A.toString str ⧺ ": " ⧺ err)
+    where
+      p ∷ Parser ETag
+      {-# INLINE p #-}
+      p = do et ← eTag
+             endOfInput
+             return et
+
+-- |This is equivalent to @'ETag' 'False'@. If you want to generate an
+-- ETag from a file, try using
 -- 'Network.HTTP.Lucu.StaticFile.generateETagFromFile'.
-strongETag :: String -> ETag
+strongETag ∷ Ascii → ETag
+{-# INLINE strongETag #-}
 strongETag = ETag False
 
--- |This is equivalent to @'ETag' 'Prelude.True'@.
-weakETag :: String -> ETag
+-- |This is equivalent to @'ETag' 'True'@.
+weakETag ∷ Ascii → ETag
+{-# INLINE weakETag #-}
 weakETag = ETag True
 
+-- |'Parser' for an 'ETag'.
+eTag ∷ Parser ETag
+{-# INLINEABLE eTag #-}
+eTag = do isWeak ← option False (string "W/" *> return True)
+          str    ← quotedStr
+          return $ ETag isWeak str
 
-eTagP :: Parser ETag
-eTagP = do isWeak <- option False (string "W/" >> return True)
-           str    <- quotedStr
-           return $ ETag isWeak str
-
-
-eTagListP :: Parser [ETag]
-eTagListP = allowEOF
-            $! do xs <- listOf eTagP
-                  when (null xs)
-                           $ fail ""
-                  return xs
+-- |'Parser' for a list of 'ETag's.
+eTagList ∷ Parser [ETag]
+{-# INLINEABLE eTagList #-}
+eTagList = do xs ← listOf eTag
+              when (null xs) $
+                  fail "empty list of ETags"
+              return xs
diff --git a/Network/HTTP/Lucu/Format.hs b/Network/HTTP/Lucu/Format.hs
deleted file mode 100644 (file)
index 93c2cda..0000000
+++ /dev/null
@@ -1,131 +0,0 @@
--- 本當にこんなものを自分で書く必要があったのだらうか。Printf は重いの
--- で駄目だが、それ以外のモジュールを探しても見付からなかった。
-
-module Network.HTTP.Lucu.Format
-    ( fmtInt
-
-    , fmtDec
-    , fmtHex
-    )
-    where
-
-
-fmtInt :: Int -> Bool -> Int -> Char -> Bool -> Int -> String
-fmtInt base upperCase minWidth pad forceSign n
-    = base `seq` minWidth `seq` pad `seq` forceSign `seq` n `seq`
-      let raw     = reverse $! fmt' (abs n)
-          sign    = if forceSign || n < 0 then
-                        if n < 0 then "-" else "+"
-                    else
-                        ""
-          padded  = padStr (minWidth - length sign) pad raw
-      in
-        sign ++ padded
-    where
-      fmt' :: Int -> String
-      fmt' m
-          | m < base  = [intToChar upperCase m]
-          | otherwise = (intToChar upperCase $! m `mod` base) : fmt' (m `div` base)
-
-
-fmtDec :: Int -> Int -> String
-fmtDec minWidth n
-    | minWidth == 2 = fmtDec2 n -- optimization 
-    | minWidth == 3 = fmtDec3 n -- optimization
-    | minWidth == 4 = fmtDec4 n -- optimization
-    | otherwise     = fmtInt 10 undefined minWidth '0' False n
-{-# INLINE fmtDec #-}
-
-
-fmtDec2 :: Int -> String
-fmtDec2 n
-    | n < 0 || n >= 100 = fmtInt 10 undefined 2 '0' False n -- fallback
-    | n < 10            = [ '0'
-                          , intToChar undefined n
-                          ]
-    | otherwise         = [ intToChar undefined (n `div` 10)
-                          , intToChar undefined (n `mod` 10)
-                          ]
-
-
-fmtDec3 :: Int -> String
-fmtDec3 n
-    | n < 0 || n >= 1000 = fmtInt 10 undefined 3 '0' False n -- fallback
-    | n < 10             = [ '0'
-                           , '0'
-                           , intToChar undefined n
-                           ]
-    | n < 100            = [ '0'
-                           , intToChar undefined ((n `div` 10) `mod` 10)
-                           , intToChar undefined ( n           `mod` 10)
-                           ]
-    | otherwise          = [ intToChar undefined ((n `div` 100) `mod` 10)
-                           , intToChar undefined ((n `div`  10) `mod` 10)
-                           , intToChar undefined ( n            `mod` 10)
-                           ]
-
-
-fmtDec4 :: Int -> String
-fmtDec4 n
-    | n < 0 || n >= 10000 = fmtInt 10 undefined 4 '0' False n -- fallback
-    | n < 10              = [ '0'
-                            , '0'
-                            , '0'
-                            , intToChar undefined n
-                            ]
-    | n < 100             = [ '0'
-                            , '0'
-                            , intToChar undefined ((n `div` 10) `mod` 10)
-                            , intToChar undefined ( n           `mod` 10)
-                            ]
-    | n < 1000            = [ '0'
-                            , intToChar undefined ((n `div` 100) `mod` 10)
-                            , intToChar undefined ((n `div`  10) `mod` 10)
-                            , intToChar undefined ( n            `mod` 10)
-                            ]
-    | otherwise           = [ intToChar undefined ((n `div` 1000) `mod` 10)
-                            , intToChar undefined ((n `div`  100) `mod` 10)
-                            , intToChar undefined ((n `div`   10) `mod` 10)
-                            , intToChar undefined ( n             `mod` 10)
-                            ]
-
-
-fmtHex :: Bool -> Int -> Int -> String
-fmtHex upperCase minWidth
-    = fmtInt 16 upperCase minWidth '0' False
-
-
-padStr :: Int -> Char -> String -> String
-padStr minWidth pad str
-    = let delta = minWidth - length str
-      in
-        if delta > 0 then
-            replicate delta pad ++ str
-        else
-            str
-
-
-intToChar :: Bool -> Int -> Char
-intToChar _ 0  = '0'
-intToChar _ 1  = '1'
-intToChar _ 2  = '2'
-intToChar _ 3  = '3'
-intToChar _ 4  = '4'
-intToChar _ 5  = '5'
-intToChar _ 6  = '6'
-intToChar _ 7  = '7'
-intToChar _ 8  = '8'
-intToChar _ 9  = '9'
-intToChar False 10 = 'a'
-intToChar True  10 = 'A'
-intToChar False 11 = 'b'
-intToChar True  11 = 'B'
-intToChar False 12 = 'c'
-intToChar True  12 = 'C'
-intToChar False 13 = 'd'
-intToChar True  13 = 'D'
-intToChar False 14 = 'e'
-intToChar True  14 = 'E'
-intToChar False 15 = 'f'
-intToChar True  15 = 'F'
-intToChar _ _ = undefined
index aa4dacbee7c3e00983c2f61afda2931fe10edb57..c4a4c62fc23298558b6ee48a0cc906f8191232f4 100644 (file)
@@ -1,68 +1,58 @@
+{-# LANGUAGE
+    DoAndIfThenElse
+  , UnicodeSyntax
+  #-}
 module Network.HTTP.Lucu.HandleLike
     ( HandleLike(..)
+    , hPutBuilder
     )
     where
-
+import Blaze.ByteString.Builder (Builder)
+import qualified Blaze.ByteString.Builder as BB
 import qualified Data.ByteString as B
 import qualified Data.ByteString.Lazy.Char8 as L
 import qualified OpenSSL.Session as SSL
-import           OpenSSL.X509
+import OpenSSL.X509
+import Prelude.Unicode
 import qualified System.IO as I
 
-
 class HandleLike h where
-    hGetLBS :: h -> IO L.ByteString
-    hPutLBS :: h -> L.ByteString -> IO ()
-
-    hGetBS  :: h -> Int -> IO B.ByteString
-    hPutBS  :: h -> B.ByteString -> IO ()
+    hGetLBS ∷ h → IO L.ByteString
 
-    hPutChar  :: h -> Char -> IO ()
+    hGetBS  ∷ h → Int → IO B.ByteString
+    hPutBS  ∷ h → B.ByteString → IO ()
 
-    hPutStr   :: h -> String -> IO ()
-    hPutStrLn :: h -> String -> IO ()
-
-    hGetPeerCert :: h -> IO (Maybe X509)
+    hGetPeerCert ∷ h → IO (Maybe X509)
     hGetPeerCert = const $ return Nothing
 
-    hFlush  :: h -> IO ()
-    hClose  :: h -> IO ()
-
+    hFlush  ∷ h → IO ()
+    hClose  ∷ h → IO ()
 
 instance HandleLike I.Handle where
     hGetLBS = L.hGetContents
-    hPutLBS = L.hPut
 
     hGetBS  = B.hGet
     hPutBS  = B.hPut
 
-    hPutChar  = I.hPutChar
-
-    hPutStr   = I.hPutStr
-    hPutStrLn = I.hPutStrLn
-
     hFlush  = I.hFlush
     hClose  = I.hClose
 
-
 instance HandleLike SSL.SSL where
-    hGetLBS   = SSL.lazyRead
-    hPutLBS   = SSL.lazyWrite
-
-    hGetBS    = SSL.read
-    hPutBS    = SSL.write
+    hGetLBS = SSL.lazyRead
 
-    hPutChar  s = hPutLBS s . L.singleton
-
-    hPutStr   s = hPutLBS s . L.pack
-    hPutStrLn s = hPutLBS s . L.pack . (++ "\n")
+    hGetBS  = SSL.read
+    hPutBS  = SSL.write
 
     hGetPeerCert s
         = do isValid <- SSL.getVerifyResult s
              if isValid then
                  SSL.getPeerCertificate s
-               else
+             else
                  return Nothing
 
-    hFlush _  = return () -- unneeded
-    hClose s  = SSL.shutdown s SSL.Bidirectional
+    hFlush _ = return () -- No need to do anything.
+    hClose s = SSL.shutdown s SSL.Bidirectional
+
+hPutBuilder ∷ HandleLike h ⇒ h → Builder → IO ()
+{-# INLINE hPutBuilder #-}
+hPutBuilder = BB.toByteStringIO ∘ hPutBS
index 87d858c55ec023a07a263a3f6d2280adaf958eb6..a47f2ac9ea60c1869221c573238b2186f076618e 100644 (file)
+{-# LANGUAGE
+    GeneralizedNewtypeDeriving
+  , OverloadedStrings
+  , UnicodeSyntax
+  #-}
 module Network.HTTP.Lucu.Headers
     ( Headers
     , HasHeaders(..)
 
-    , noCaseCmp
-    , noCaseEq
+    , singleton
 
-    , emptyHeaders
     , toHeaders
     , fromHeaders
 
-    , headersP
-    , hPutHeaders
+    , headers
+    , printHeaders
     )
     where
-
-import qualified Data.ByteString as Strict (ByteString)
-import           Data.ByteString.Internal (toForeignPtr, w2c, inlinePerformIO)
-import qualified Data.ByteString.Char8 as C8 hiding (ByteString)
-import           Data.Char
-import           Data.List
-import           Data.Map (Map)
+import Control.Applicative
+import Control.Monad
+import Data.Ascii (Ascii, AsciiBuilder, CIAscii)
+import qualified Data.Ascii as A
+import Data.Attoparsec.Char8 as P
+import qualified Data.ByteString as BS
+import Data.List
+import Data.Map (Map)
 import qualified Data.Map as M
-import           Data.Ord
-import           Data.Word
-import           Foreign.ForeignPtr
-import           Foreign.Ptr
-import           Foreign.Storable
-import           Network.HTTP.Lucu.HandleLike
-import           Network.HTTP.Lucu.Parser
-import           Network.HTTP.Lucu.Parser.Http
-import           Network.HTTP.Lucu.Utils
-
-type Headers = Map NCBS Strict.ByteString
-newtype NCBS = NCBS Strict.ByteString
-
-toNCBS :: Strict.ByteString -> NCBS
-toNCBS = NCBS
-{-# INLINE toNCBS #-}
-
-fromNCBS :: NCBS -> Strict.ByteString
-fromNCBS (NCBS x) = x
-{-# INLINE fromNCBS #-}
-
-instance Eq NCBS where
-    (NCBS a) == (NCBS b) = a == b
-
-instance Ord NCBS where
-    (NCBS a) `compare` (NCBS b) = a `noCaseCmp` b
-
-instance Show NCBS where
-    show (NCBS x) = show x
-
-noCaseCmp :: Strict.ByteString -> Strict.ByteString -> Ordering
-noCaseCmp a b = a `seq` b `seq`
-                toForeignPtr a `cmp` toForeignPtr b
-    where
-      cmp :: (ForeignPtr Word8, Int, Int) -> (ForeignPtr Word8, Int, Int) -> Ordering
-      cmp (x1, s1, l1) (x2, s2, l2)
-          | x1 `seq` s1 `seq` l1 `seq` x2 `seq` s2 `seq` l2 `seq` False = undefined
-          | l1 == 0  && l2 == 0               = EQ
-          | x1 == x2 && s1 == s2 && l1 == l2  = EQ
-          | otherwise
-              = inlinePerformIO $
-                withForeignPtr x1 $ \ p1 ->
-                withForeignPtr x2 $ \ p2 ->
-                noCaseCmp' (p1 `plusPtr` s1) l1 (p2 `plusPtr` s2) l2
-
-
--- もし先頭の文字列が等しければ、短い方が小さい。
-noCaseCmp' :: Ptr Word8 -> Int -> Ptr Word8 -> Int -> IO Ordering
-noCaseCmp' p1 l1 p2 l2
-    | p1 `seq` l1 `seq` p2 `seq` l2 `seq` False = undefined
-    | l1 == 0 && l2 == 0 = return EQ
-    | l1 == 0            = return LT
-    |            l2 == 0 = return GT
-    | otherwise
-        = do c1 <- peek p1
-             c2 <- peek p2
-             case comparing (toLower . w2c) c1 c2 of
-               EQ -> noCaseCmp' (p1 `plusPtr` 1) (l1 - 1) (p2 `plusPtr` 1) (l2 - 1)
-               x  -> return x
-
-
-noCaseEq :: Strict.ByteString -> Strict.ByteString -> Bool
-noCaseEq a b = a `seq` b `seq`
-               toForeignPtr a `cmp` toForeignPtr b
-    where
-      cmp :: (ForeignPtr Word8, Int, Int) -> (ForeignPtr Word8, Int, Int) -> Bool
-      cmp (x1, s1, l1) (x2, s2, l2)
-          | x1 `seq` s1 `seq` l1 `seq` x2 `seq` s2 `seq` l2 `seq` False = undefined
-          | l1 /= l2                          = False
-          | l1 == 0  && l2 == 0               = True
-          | x1 == x2 && s1 == s2 && l1 == l2  = True
-          | otherwise
-              = inlinePerformIO $
-                withForeignPtr x1 $ \ p1 ->
-                withForeignPtr x2 $ \ p2 ->
-                noCaseEq' (p1 `plusPtr` s1) (p2 `plusPtr` s2) l1
-
-
-noCaseEq' :: Ptr Word8 -> Ptr Word8 -> Int -> IO Bool
-noCaseEq' p1 p2 l
-    | p1 `seq` p2 `seq` l `seq` False = undefined
-    | l == 0    = return True
-    | otherwise
-        = do c1 <- peek p1
-             c2 <- peek p2
-             if toLower (w2c c1) == toLower (w2c c2) then
-                 noCaseEq' (p1 `plusPtr` 1) (p2 `plusPtr` 1) (l - 1)
-               else
-                 return False
+import qualified Data.Map.Unicode as M
+import Data.Monoid
+import Data.Monoid.Unicode
+import Network.HTTP.Lucu.Parser.Http
+import Prelude.Unicode
 
+newtype Headers
+    = Headers (Map CIAscii Ascii)
+      deriving (Eq, Show, Monoid)
 
 class HasHeaders a where
-    getHeaders :: a -> Headers
-    setHeaders :: a -> Headers -> a
+    getHeaders ∷ a → Headers
+    setHeaders ∷ a → Headers → a
 
-    getHeader :: Strict.ByteString -> a -> Maybe Strict.ByteString
+    getHeader ∷ CIAscii → a → Maybe Ascii
     getHeader key a
-        = key `seq` a `seq`
-          M.lookup (toNCBS key) (getHeaders a)
-
-    deleteHeader :: Strict.ByteString -> a -> a
+        = case getHeaders a of
+            Headers m → M.lookup key m
+
+    hasHeader ∷ CIAscii → a → Bool
+    {-# INLINE hasHeader #-}
+    hasHeader key a
+        = case getHeaders a of
+            Headers m → key M.∈ m
+
+    getCIHeader ∷ CIAscii → a → Maybe CIAscii
+    {-# INLINE getCIHeader #-}
+    getCIHeader key a
+        = A.toCIAscii <$> getHeader key a
+
+    deleteHeader ∷ CIAscii → a → a
+    {-# INLINE deleteHeader #-}
     deleteHeader key a
-        = key `seq` a `seq`
-          setHeaders a $ M.delete (toNCBS key) (getHeaders a)
+        = case getHeaders a of
+            Headers m
+              → setHeaders a $ Headers $ M.delete key m
 
-    setHeader :: Strict.ByteString -> Strict.ByteString -> a -> a
+    setHeader ∷ CIAscii → Ascii → a → a
+    {-# INLINE setHeader #-}
     setHeader key val a
-        = key `seq` val `seq` a `seq`
-          setHeaders a $ M.insert (toNCBS key) val (getHeaders a)
-
-
-emptyHeaders :: Headers
-emptyHeaders = M.empty
-
-
-toHeaders :: [(Strict.ByteString, Strict.ByteString)] -> Headers
-toHeaders xs = mkHeaders xs M.empty
-
-
-mkHeaders :: [(Strict.ByteString, Strict.ByteString)] -> Headers -> Headers
-mkHeaders []              m = m
-mkHeaders ((key, val):xs) m = mkHeaders xs $
-                              case M.lookup (toNCBS key) m of
-                                Nothing  -> M.insert (toNCBS key) val m
-                                Just old -> M.insert (toNCBS key) (merge old val) m
+        = case getHeaders a of
+            Headers m
+              → setHeaders a $ Headers $ M.insert key val m
+
+instance HasHeaders Headers where
+    getHeaders   = id
+    setHeaders _ = id
+
+singleton ∷ CIAscii → Ascii → Headers
+{-# INLINE singleton #-}
+singleton key val
+    = Headers $ M.singleton key val
+
+toHeaders ∷ [(CIAscii, Ascii)] → Headers
+{-# INLINE toHeaders #-}
+toHeaders = flip mkHeaders (∅)
+
+mkHeaders ∷ [(CIAscii, Ascii)] → Headers → Headers
+mkHeaders []              (Headers m) = Headers m
+mkHeaders ((key, val):xs) (Headers m)
+    = mkHeaders xs $ Headers $
+      case M.lookup key m of
+        Nothing  → M.insert key val m
+        Just old → M.insert key (merge old val) m
     where
-      merge :: Strict.ByteString -> Strict.ByteString -> Strict.ByteString
-      -- カンマ區切りである事を假定する。RFC ではカンマ區切りに出來ない
-      -- ヘッダは複數個あってはならない事になってゐる。
+      merge ∷ Ascii → Ascii → Ascii
+      {-# INLINE merge #-}
       merge a b
-          | C8.null a && C8.null b = C8.empty
-          | C8.null a              = b
-          |              C8.null b = a
-          | otherwise              = C8.concat [a, C8.pack ", ", b]
-
+          | nullA a ∧ nullA b = (∅)
+          | nullA a           = b
+          |           nullA b = a
+          | otherwise         = a ⊕ ", " ⊕ b
 
-fromHeaders :: Headers -> [(Strict.ByteString, Strict.ByteString)]
-fromHeaders hs = [(fromNCBS a, b) | (a, b) <- M.toList hs]
+      nullA ∷ Ascii → Bool
+      {-# INLINE nullA #-}
+      nullA = BS.null ∘ A.toByteString
 
+fromHeaders ∷ Headers → [(CIAscii, Ascii)]
+fromHeaders (Headers m) = M.toList m
 
 {-
   message-header = field-name ":" [ field-value ]
@@ -172,49 +116,42 @@ fromHeaders hs = [(fromNCBS a, b) | (a, b) <- M.toList hs]
   field-value の先頭および末尾にある LWS は全て削除され、それ以外の
   LWS は單一の SP に變換される。
 -}
-headersP :: Parser Headers
-headersP = do xs <- many header
-              _  <- crlf
-              return $! toHeaders xs
+headers ∷ Parser Headers
+{-# INLINEABLE headers #-}
+headers = do xs ← P.many header
+             crlf
+             return $ toHeaders xs
     where
-      header :: Parser (Strict.ByteString, Strict.ByteString)
-      header = do name <- token
-                  _    <- char ':'
-                  -- FIXME: これは多少インチキだが、RFC 2616 のこの部分
-                  -- の記述はひどく曖昧であり、この動作が本當に間違って
-                  -- ゐるのかどうかも良く分からない。例へば
-                  -- quoted-string の内部にある空白は纏めていいのか惡い
-                  -- のか?直勸的には駄目さうに思へるが、そんな記述は見
-                  -- 付からない。
-                  contents <- many (lws <|> many1 text)
-                  _        <- crlf
-                  let value = foldr (++) "" contents
-                      norm  = normalize value
-                  return (C8.pack name, C8.pack norm)
-
-      normalize :: String -> String
-      normalize = trimBody . trim isWhiteSpace
-
-      trimBody = concat
-                 . map (\ s -> if head s == ' ' then
-                                   " "
-                               else
-                                   s)
-                 . group
-                 . map (\ c -> if isWhiteSpace c
-                               then ' '
-                               else c)
-
-
-hPutHeaders :: HandleLike h => h -> Headers -> IO ()
-hPutHeaders h hds
-    = h `seq` hds `seq`
-      mapM_ putH (M.toList hds) >> hPutBS h (C8.pack "\r\n")
+      header ∷ Parser (CIAscii, Ascii)
+      header = do name ← A.toCIAscii <$> token
+                  void $ char ':'
+                  skipMany lws
+                  values ← content `sepBy` try lws
+                  skipMany (try lws)
+                  crlf
+                  return (name, joinValues values)
+
+      content ∷ Parser Ascii
+      {-# INLINE content #-}
+      content = A.unsafeFromByteString
+                <$>
+                takeWhile1 (\c → isText c ∧ c ≢ '\x20')
+
+      joinValues ∷ [Ascii] → Ascii
+      {-# INLINE joinValues #-}
+      joinValues = A.fromAsciiBuilder
+                   ∘ mconcat
+                   ∘ intersperse (A.toAsciiBuilder "\x20")
+                   ∘ map A.toAsciiBuilder
+
+printHeaders ∷ Headers → AsciiBuilder
+printHeaders (Headers m)
+    = mconcat (map printHeader (M.toList m)) ⊕
+      A.toAsciiBuilder "\x0D\x0A"
     where
-      putH :: (NCBS, Strict.ByteString) -> IO ()
-      putH (name, value)
-          = name `seq` value `seq`
-            do hPutBS h (fromNCBS name)
-               hPutBS h (C8.pack ": ")
-               hPutBS h value
-               hPutBS h (C8.pack "\r\n")
+      printHeader ∷ (CIAscii, Ascii) → AsciiBuilder
+      printHeader (name, value)
+          = A.toAsciiBuilder (A.fromCIAscii name) ⊕
+            A.toAsciiBuilder ": "                 ⊕
+            A.toAsciiBuilder value                ⊕
+            A.toAsciiBuilder "\x0D\x0A"
index d48f6ec8c58f3d5009c3038ed500eb4e863e5003..36b6c499b1428e48f9ea4c0c1a383fbeab8f8026 100644 (file)
@@ -1,28 +1,26 @@
 {-# LANGUAGE
-    BangPatterns
+    OverloadedStrings
   , UnicodeSyntax
   #-}
-{-# OPTIONS_HADDOCK prune #-}
-
--- |Manipulation of HTTP version string.
+-- |HTTP version number
 module Network.HTTP.Lucu.HttpVersion
     ( HttpVersion(..)
-    , httpVersionP
-    , hPutHttpVersion
+    , printHttpVersion
+    , httpVersion
     )
     where
-
-import qualified Data.ByteString.Char8 as C8
-import           Network.HTTP.Lucu.HandleLike
-import           Network.HTTP.Lucu.Parser
-import           Prelude hiding (min)
-
--- |@'HttpVersion' major minor@ represents \"HTTP\/major.minor\".
-data HttpVersion = HttpVersion !Int !Int
-                   deriving (Eq)
-
-instance Show HttpVersion where
-    show (HttpVersion maj min) = "HTTP/" ++ show maj ++ "." ++ show min
+import Control.Applicative
+import Control.Applicative.Unicode
+import Data.Ascii (AsciiBuilder)
+import qualified Data.Ascii as A
+import Data.Attoparsec.Char8
+import Data.Monoid.Unicode
+import Prelude hiding (min)
+
+-- |An HTTP version consists of major and minor versions.
+data HttpVersion
+    = HttpVersion !Int !Int
+      deriving (Eq, Show)
 
 instance Ord HttpVersion where
     (HttpVersion majA minA) `compare` (HttpVersion majB minB)
@@ -32,30 +30,22 @@ instance Ord HttpVersion where
         | minA < minB = LT
         | otherwise   = EQ
 
-
-httpVersionP :: Parser HttpVersion
-httpVersionP = string "HTTP/"
-               >>
-               -- 頻出するので高速化
-               choice [ string "1.0" >> return (HttpVersion 1 0)
-                      , string "1.1" >> return (HttpVersion 1 1)
-                        -- 一般の場合
-                      , do major <- many1 digit
-                           _     <- char '.'
-                           minor <- many1 digit
-                           return $ HttpVersion (read major) (read minor)
-                      ]
-
-
-hPutHttpVersion :: HandleLike h => h -> HttpVersion -> IO ()
-hPutHttpVersion !h !v
+-- |Convert an 'HttpVersion' to 'AsciiBuilder'.
+printHttpVersion ∷ HttpVersion → AsciiBuilder
+printHttpVersion v
     = case v of
-        -- 頻出するので高速化
-        HttpVersion 1 0 -> hPutBS h (C8.pack "HTTP/1.0")
-        HttpVersion 1 1 -> hPutBS h (C8.pack "HTTP/1.1")
-        -- 一般の場合
-        HttpVersion !maj !min
-            -> do hPutBS   h (C8.pack "HTTP/")
-                  hPutStr  h (show maj)
-                  hPutChar h '.'
-                  hPutStr  h (show min)
+        -- Optimisation for special cases.
+        HttpVersion 1 0 → A.toAsciiBuilder "HTTP/1.0"
+        HttpVersion 1 1 → A.toAsciiBuilder "HTTP/1.1"
+        -- General (but almost never stumbling) cases.
+        HttpVersion maj min
+            → A.toAsciiBuilder "HTTP/" ⊕
+              A.toAsciiBuilder (A.unsafeFromString $ show maj) ⊕
+              A.toAsciiBuilder "." ⊕
+              A.toAsciiBuilder (A.unsafeFromString $ show min)
+
+-- |'Parser' for an 'HttpVersion'.
+httpVersion ∷ Parser HttpVersion
+httpVersion = string "HTTP/"
+              *>
+              (HttpVersion <$> decimal ⊛ (char '.' *> decimal))
index 0bb92b1635c74a1498b1ee2936f807d6153d356d..595403abd0364f1a2e70c79088d9138d38eeaf90 100644 (file)
@@ -1,36 +1,38 @@
+{-# LANGUAGE
+    UnicodeSyntax
+  #-}
 -- |The entry point of Lucu httpd.
 module Network.HTTP.Lucu.Httpd
     ( FallbackHandler
     , runHttpd
     )
     where
-
-import           Control.Concurrent
-import           Control.Exception
-import           Control.Monad
-import           Data.Maybe
-import           Network.BSD
-import           Network.Socket
-import           Network.HTTP.Lucu.Config
-import           Network.HTTP.Lucu.Interaction
-import           Network.HTTP.Lucu.RequestReader
-import           Network.HTTP.Lucu.Resource.Tree
-import           Network.HTTP.Lucu.ResponseWriter
-import           Network.HTTP.Lucu.SocketLike as SL
-import           System.Posix.Signals
+import Control.Concurrent
+import Control.Exception
+import Control.Monad
+import Control.Monad.Unicode
+import Data.Maybe
+import Network.BSD
+import Network.Socket
+import Network.HTTP.Lucu.Config
+import Network.HTTP.Lucu.Interaction
+import Network.HTTP.Lucu.RequestReader
+import Network.HTTP.Lucu.Resource.Tree
+import Network.HTTP.Lucu.ResponseWriter
+import Network.HTTP.Lucu.SocketLike as SL
+import System.Posix.Signals
 
 -- |This is the entry point of Lucu httpd. It listens to a socket and
--- waits for clients. Computation of 'runHttpd' never stops by itself
--- so the only way to stop it is to raise an exception in the thread
--- computing it.
+-- waits for clients. 'runHttpd' never stops by itself so the only way
+-- to stop it is to raise an exception in the thread running it.
 --
 -- Note that 'runHttpd' automatically makes SIGPIPE be ignored by
--- computing @'System.Posix.Signals.installHandler'
--- 'System.Posix.Signals.sigPIPE' 'System.Posix.Signals.Ignore'
--- 'Prelude.Nothing'@. This can hardly cause a problem but it may do.
+-- calling @'installHandler' 'sigPIPE' 'Ignore' 'Nothing'@. This can
+-- hardly cause a problem though.
 --
 -- Example:
 --
+-- > {-# LANGUAGE OverloadedStrings #-}
 -- > module Main where
 -- > import Network.HTTP.Lucu
 -- > 
@@ -41,81 +43,72 @@ import           System.Posix.Signals
 -- >          runHttpd config resourcees []
 -- >
 -- > helloWorld :: ResourceDef
--- > helloWorld = ResourceDef {
--- >                resUsesNativeThread = False
--- >              , resIsGreedy         = False
--- >              , resGet
--- >                  = Just $ do setContentType $ read "text/plain"
--- >                              output "Hello, world!"
--- >              , resHead   = Nothing
--- >              , resPost   = Nothing
--- >              , resPut    = Nothing
--- >              , resDelete = Nothing
+-- > helloWorld = emptyResource {
+-- >                resGet
+-- >                  = Just $ do setContentType $ parseMIMEType "text/plain"
+-- >                              putChunk "Hello, world!"
 -- >              }
-runHttpd :: Config -> ResTree -> [FallbackHandler] -> IO ()
+runHttpd ∷ Config → ResTree → [FallbackHandler] → IO ()
 runHttpd cnf tree fbs
     = withSocketsDo $
-      do _ <- installHandler sigPIPE Ignore Nothing
-
+      do void $ installHandler sigPIPE Ignore Nothing
          let launchers
                  = catMaybes
-                   [ do scnf <- cnfSSLConfig    cnf
-                        addr <- cnfServerV4Addr cnf
-                        return ( do so <- listenOn AF_INET addr (sslServerPort scnf)
+                   [ do scnf  cnfSSLConfig    cnf
+                        addr  cnfServerV4Addr cnf
+                        return ( do so  listenOn AF_INET addr (sslServerPort scnf)
                                     launchListener (sslContext scnf, so)
                                )
-                   , do scnf <- cnfSSLConfig    cnf
-                        addr <- cnfServerV6Addr cnf
-                        return ( do so <- listenOn AF_INET6 addr (sslServerPort scnf)
+                   , do scnf  cnfSSLConfig    cnf
+                        addr  cnfServerV6Addr cnf
+                        return ( do so  listenOn AF_INET6 addr (sslServerPort scnf)
                                     launchListener (sslContext scnf, so)
                                )
-                   , do addr <- cnfServerV4Addr cnf
-                        return ( launchListener =<< listenOn AF_INET addr (cnfServerPort cnf)
+                   , do addr  cnfServerV4Addr cnf
+                        return ( launchListener = listenOn AF_INET addr (cnfServerPort cnf)
                                )
-                   , do addr <- cnfServerV6Addr cnf
-                        return ( launchListener =<< listenOn AF_INET6 addr (cnfServerPort cnf)
+                   , do addr  cnfServerV6Addr cnf
+                        return ( launchListener = listenOn AF_INET6 addr (cnfServerPort cnf)
                                )
                    ]
-
          sequence_ launchers
          waitForever
     where
-      launchListener :: SocketLike s => s -> IO ()
+      launchListener ∷ SocketLike s ⇒ s → IO ()
       launchListener so
-          = do p <- SL.socketPort so
+          = do p  SL.socketPort so
                -- FIXME: Don't throw away the thread ID as we can't
                -- kill it later then. [1]
-               _ <- forkIO $ httpLoop p so
-               return ()
+               void $ forkIO $ httpLoop p so
 
-      listenOn :: Family -> HostName -> ServiceName -> IO Socket
+      listenOn ∷ Family → HostName → ServiceName → IO Socket
       listenOn fam host srv
-          = do proto <- getProtocolNumber "tcp"
+          = do proto  getProtocolNumber "tcp"
                let hints = defaultHints {
                              addrFlags      = [AI_PASSIVE]
                            , addrFamily     = fam
                            , addrSocketType = Stream
                            , addrProtocol   = proto
                            }
-               addrs <- getAddrInfo (Just hints) (Just host) (Just srv)
+               addrs  getAddrInfo (Just hints) (Just host) (Just srv)
                let addr = head addrs
                bracketOnError
                    (socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr))
-                   (sClose)
-                   (\ sock ->
+                   sClose
+                   (\ sock 
                         do setSocketOption sock ReuseAddr 1
                            bindSocket sock (addrAddress addr)
                            listen sock maxListenQueue
                            return sock
                    )
 
-      httpLoop :: SocketLike s => PortNumber -> s -> IO ()
+      httpLoop ∷ SocketLike s ⇒ PortNumber → s → IO ()
       httpLoop port so
-          = do (h, addr)  <- SL.accept so
-               tQueue     <- newInteractionQueue
-               readerTID  <- forkIO $ requestReader cnf tree fbs h port addr tQueue
-               _writerTID <- forkIO $ responseWriter cnf h tQueue readerTID
+          = do (h, addr)   SL.accept so
+               tQueue     ← mkInteractionQueue
+               readerTID   forkIO $ requestReader cnf tree fbs h port addr tQueue
+               _writerTID  forkIO $ responseWriter cnf h tQueue readerTID
                httpLoop port so
 
-      waitForever :: IO ()
+      waitForever  IO ()
       waitForever = forever (threadDelay 1000000)
index 638d1b05bafc472f364cfb7626930f6f00a86423..f1e7ab371734304bf18b00f57a70c608e3f4c16a 100644 (file)
 {-# LANGUAGE
-    BangPatterns
+    DeriveDataTypeable
+  , ExistentialQuantification
+  , OverloadedStrings
+  , RecordWildCards
   , UnicodeSyntax
   #-}
 module Network.HTTP.Lucu.Interaction
     ( Interaction(..)
-    , InteractionState(..)
-    , InteractionQueue
-    , newInteractionQueue
-    , newInteraction
-    , defaultPageContentType
-
-    , writeItr
-    , readItr
-    , readItrF
-    , updateItr
-    , updateItrF
-    )
-    where
-
-import           Control.Concurrent.STM
-import qualified Data.ByteString as Strict (ByteString)
-import qualified Data.ByteString.Lazy as Lazy (ByteString)
-import           Data.ByteString.Char8 as C8 hiding (ByteString)
-import qualified Data.ByteString.Lazy.Char8 as L8 hiding (ByteString)
-import qualified Data.Sequence as S
-import           Data.Sequence (Seq)
-import           Network.Socket
-import           Network.HTTP.Lucu.Config
-import           Network.HTTP.Lucu.Headers
-import           Network.HTTP.Lucu.HttpVersion
-import           Network.HTTP.Lucu.Request
-import           Network.HTTP.Lucu.Response
-import           OpenSSL.X509
-
-data Interaction = Interaction {
-      itrConfig       :: !Config
-    , itrLocalPort    :: !PortNumber
-    , itrRemoteAddr   :: !SockAddr
-    , itrRemoteCert   :: !(Maybe X509)
-    , itrResourcePath :: !(Maybe [String])
-    , itrRequest      :: !(TVar (Maybe Request)) -- FIXME: TVar である必要無し
-    , itrResponse     :: !(TVar Response)
-
-    , itrRequestHasBody    :: !(TVar Bool) -- FIXME: TVar である必要無し
-    , itrRequestIsChunked  :: !(TVar Bool) -- FIXME: TVar である必要無し
-    , itrExpectedContinue  :: !(TVar Bool) -- FIXME: TVar である必要無し
-
-    , itrReqChunkLength    :: !(TVar (Maybe Int))
-    , itrReqChunkRemaining :: !(TVar (Maybe Int))
-    , itrReqChunkIsOver    :: !(TVar Bool)
-    , itrReqBodyWanted     :: !(TVar (Maybe Int))
-    , itrReqBodyWasteAll   :: !(TVar Bool)
-    , itrReceivedBody      :: !(TVar Lazy.ByteString) -- Resource が受領した部分は削除される
-
-    , itrWillReceiveBody   :: !(TVar Bool)
-    , itrWillChunkBody     :: !(TVar Bool)
-    , itrWillDiscardBody   :: !(TVar Bool)
-    , itrWillClose         :: !(TVar Bool)
-
-    , itrBodyToSend :: !(TVar Lazy.ByteString)
-    , itrBodyIsNull :: !(TVar Bool)
-
-    , itrState :: !(TVar InteractionState)
-
-    , itrWroteContinue :: !(TVar Bool)
-    , itrWroteHeader   :: !(TVar Bool)
-    }
-
--- Resource の視點で見た時の状態。常に上から下へ行き、逆行しない。初期
--- 状態は ExaminingRequest。
-data InteractionState = ExaminingRequest
-                      | GettingBody
-                      | DecidingHeader
-                      | DecidingBody
-                      | Done
-                        deriving (Show, Eq, Ord, Enum)
-
-type InteractionQueue = TVar (Seq Interaction)
-
-
-newInteractionQueue :: IO InteractionQueue
-newInteractionQueue = newTVarIO S.empty
-
-
-defaultPageContentType :: Strict.ByteString
-defaultPageContentType = C8.pack "application/xhtml+xml"
-
-
-newInteraction :: Config -> PortNumber -> SockAddr -> Maybe X509 -> Maybe Request -> IO Interaction
-newInteraction !conf !port !addr !cert !req
-    = do request  <- newTVarIO req
-         responce <- newTVarIO Response {
-                       resVersion = HttpVersion 1 1
-                     , resStatus  = Ok
-                     , resHeaders = toHeaders [(C8.pack "Content-Type", defaultPageContentType)]
-                     }
-
-         requestHasBody     <- newTVarIO False
-         requestIsChunked   <- newTVarIO False
-         expectedContinue   <- newTVarIO False
-         
-         reqChunkLength     <- newTVarIO Nothing -- 現在のチャンク長
-         reqChunkRemaining  <- newTVarIO Nothing -- 現在のチャンクの殘り
-         reqChunkIsOver     <- newTVarIO False   -- 最後のチャンクを讀み終へた
-         reqBodyWanted      <- newTVarIO Nothing -- Resource が要求してゐるチャンク長
-         reqBodyWasteAll    <- newTVarIO False   -- 殘りの body を讀み捨てよと云ふ要求
-         receivedBody       <- newTVarIO L8.empty
-
-         willReceiveBody   <- newTVarIO False
-         willChunkBody     <- newTVarIO False
-         willDiscardBody   <- newTVarIO False
-         willClose         <- newTVarIO False
-
-         bodyToSend <- newTVarIO L8.empty
-         bodyIsNull <- newTVarIO True -- 一度でも bodyToSend が空でなくなったら False
-
-         state <- newTVarIO ExaminingRequest
-
-         wroteContinue <- newTVarIO False
-         wroteHeader   <- newTVarIO False
-
-         return Interaction {
-                      itrConfig       = conf
-                    , itrLocalPort    = port
-                    , itrRemoteAddr   = addr
-                    , itrRemoteCert   = cert
-                    , itrResourcePath = Nothing
-                    , itrRequest      = request
-                    , itrResponse     = responce
-
-                    , itrRequestHasBody   = requestHasBody
-                    , itrRequestIsChunked = requestIsChunked
-                    , itrExpectedContinue = expectedContinue
-
-                    , itrReqChunkLength    = reqChunkLength
-                    , itrReqChunkRemaining = reqChunkRemaining
-                    , itrReqChunkIsOver    = reqChunkIsOver
-                    , itrReqBodyWanted     = reqBodyWanted
-                    , itrReqBodyWasteAll   = reqBodyWasteAll
-                    , itrReceivedBody      = receivedBody
-
-                    , itrWillReceiveBody   = willReceiveBody
-                    , itrWillChunkBody     = willChunkBody
-                    , itrWillDiscardBody   = willDiscardBody
-                    , itrWillClose         = willClose
-
-                    , itrBodyToSend = bodyToSend
-                    , itrBodyIsNull = bodyIsNull
-                    
-                    , itrState = state
-                    
-                    , itrWroteContinue = wroteContinue
-                    , itrWroteHeader   = wroteHeader
-                    }
-
-
-writeItr :: Interaction -> (Interaction -> TVar a) -> a -> STM ()
-writeItr !itr !accessor !value
-    = writeTVar (accessor itr) value
+    , SomeInteraction(..)
 
+    , SyntacticallyInvalidInteraction(..)
+    , mkSyntacticallyInvalidInteraction
 
-readItr :: Interaction -> (Interaction -> TVar a) -> (a -> b) -> STM b
-readItr !itr !accessor !reader
-    = fmap reader $ readTVar (accessor itr)
-
-
-readItrF :: Functor f => Interaction -> (Interaction -> TVar (f a)) -> (a -> b) -> STM (f b)
-readItrF !itr !accessor !reader
-    = readItr itr accessor (fmap reader)
-{-# SPECIALIZE readItrF :: Interaction -> (Interaction -> TVar (Maybe a)) -> (a -> b) -> STM (Maybe b) #-}
+    , SemanticallyInvalidInteraction(..)
+    , mkSemanticallyInvalidInteraction
 
+    , NormalInteraction(..)
+    , InteractionState(..)
+    , ReceiveBodyRequest(..)
+    , mkNormalInteraction
 
-updateItr :: Interaction -> (Interaction -> TVar a) -> (a -> a) -> STM ()
-updateItr !itr !accessor !updator
-    = do old <- readItr itr accessor id
-         writeItr itr accessor (updator old)
+    , InteractionQueue
+    , mkInteractionQueue
 
+    , setResponseStatus
+    , getCurrentDate
+    )
+    where
+import Blaze.ByteString.Builder (Builder)
+import Control.Applicative
+import Control.Concurrent.STM
+import Data.Ascii (Ascii)
+import qualified Data.ByteString as Strict
+import Data.Monoid.Unicode
+import Data.Sequence (Seq)
+import Data.Time
+import qualified Data.Time.HTTP as HTTP
+import Data.Typeable
+import Network.Socket
+import Network.HTTP.Lucu.Config
+import Network.HTTP.Lucu.DefaultPage
+import Network.HTTP.Lucu.Headers
+import Network.HTTP.Lucu.Preprocess
+import Network.HTTP.Lucu.Request
+import Network.HTTP.Lucu.Response
+import OpenSSL.X509
+
+class Typeable i ⇒ Interaction i where
+    toInteraction ∷ i → SomeInteraction
+    toInteraction = SomeInteraction
+
+    fromInteraction ∷ SomeInteraction → Maybe i
+    fromInteraction (SomeInteraction i) = cast i
+
+data SomeInteraction
+    = ∀i. Interaction i ⇒ SomeInteraction !i
+    deriving Typeable
+
+instance Interaction SomeInteraction where
+    toInteraction   = id
+    fromInteraction = Just
+
+-- |'SyntacticallyInvalidInteraction' is an 'Interaction' without even
+-- a syntactically valid 'Request'. The response code will always be
+-- 'BadRequest'.
+data SyntacticallyInvalidInteraction
+    = SYI {
+        syiResponse   ∷ !Response
+      , syiBodyToSend ∷ !Builder
+      }
+    deriving Typeable
+instance Interaction SyntacticallyInvalidInteraction
+
+mkSyntacticallyInvalidInteraction ∷ Config
+                                  → IO SyntacticallyInvalidInteraction
+mkSyntacticallyInvalidInteraction config@(Config {..})
+    = do date ← getCurrentDate
+         let res  = setHeader "Server"       cnfServerSoftware      $
+                    setHeader "Date"         date                   $
+                    setHeader "Content-Type" defaultPageContentType $
+                    emptyResponse BadRequest
+             body = getDefaultPage config Nothing res
+         return SYI {
+                  syiResponse   = res
+                , syiBodyToSend = body
+                }
+
+-- |'SemanticallyInvalidInteraction' is an 'Interaction' without a
+-- semantically valid 'Request'. The response code will always satisfy
+-- 'isError'.
+data SemanticallyInvalidInteraction
+    = SEI {
+        seiRequest          ∷ !Request
+      , seiExpectedContinue ∷ !Bool
+      , seiReqBodyLength    ∷ !(Maybe RequestBodyLength)
+
+      , seiResponse         ∷ !Response
+      , seiWillChunkBody    ∷ !Bool
+      , seiWillDiscardBody  ∷ !Bool
+      , seiWillClose        ∷ !Bool
+      , seiBodyToSend       ∷ !Builder
+      }
+    deriving Typeable
+instance Interaction SemanticallyInvalidInteraction
+
+mkSemanticallyInvalidInteraction ∷ Config
+                                 → AugmentedRequest
+                                 → IO SemanticallyInvalidInteraction
+mkSemanticallyInvalidInteraction config@(Config {..}) (AugmentedRequest {..})
+    = do date ← getCurrentDate
+         let res  = setHeader "Server"       cnfServerSoftware      $
+                    setHeader "Date"         date                   $
+                    setHeader "Content-Type" defaultPageContentType $
+                    emptyResponse arInitialStatus
+             body = getDefaultPage config (Just arRequest) res
+         return SEI {
+                  seiRequest          = arRequest
+                , seiExpectedContinue = arExpectedContinue
+                , seiReqBodyLength    = arReqBodyLength
+
+                , seiResponse         = res
+                , seiWillChunkBody    = arWillChunkBody
+                , seiWillDiscardBody  = arWillDiscardBody
+                , seiWillClose        = arWillClose
+                , seiBodyToSend       = body
+                }
+
+-- |'NormalInteraction' is an 'Interaction' with a semantically
+-- correct 'Request'.
+data NormalInteraction
+    = NI {
+        niConfig           ∷ !Config
+      , niRemoteAddr       ∷ !SockAddr
+      , niRemoteCert       ∷ !(Maybe X509)
+      , niRequest          ∷ !Request
+      , niResourcePath     ∷ ![Strict.ByteString]
+      , niExpectedContinue ∷ !Bool
+      , niReqBodyLength    ∷ !(Maybe RequestBodyLength)
+
+      , niReceiveBodyReq   ∷ !(TMVar ReceiveBodyRequest)
+      , niReceivedBody     ∷ !(TMVar Strict.ByteString)
+
+      , niResponse         ∷ !(TVar Response)
+      , niSendContinue     ∷ !(TMVar Bool)
+      , niWillChunkBody    ∷ !Bool
+      , niWillDiscardBody  ∷ !(TVar Bool)
+      , niWillClose        ∷ !(TVar Bool)
+      , niResponseHasCType ∷ !(TVar Bool)
+      , niBodyToSend       ∷ !(TMVar Builder)
+
+      , niState            ∷ !(TVar InteractionState)
+      }
+    deriving Typeable
+instance Interaction NormalInteraction
+
+data ReceiveBodyRequest
+    = ReceiveBody !Int -- ^ Maximum number of octets to receive.
+    | WasteAll
+    deriving (Show, Eq)
+
+-- |The interaction state of Resource monad. 'ExaminingRequest' is the
+-- initial state.
+data InteractionState
+    = ExaminingRequest
+    | ReceivingBody
+    | DecidingHeader
+    | SendingBody
+    | Done
+    deriving (Show, Eq, Ord, Enum)
+
+mkNormalInteraction ∷ Config
+                    → SockAddr
+                    → Maybe X509
+                    → AugmentedRequest
+                    → [Strict.ByteString]
+                    → IO NormalInteraction
+mkNormalInteraction config remoteAddr remoteCert (AugmentedRequest {..}) rsrcPath
+    = do receiveBodyReq   ← newEmptyTMVarIO
+         receivedBody     ← newEmptyTMVarIO
+
+         response         ← newTVarIO $ emptyResponse arInitialStatus
+         sendContinue     ← newEmptyTMVarIO
+         willDiscardBody  ← newTVarIO arWillDiscardBody
+         willClose        ← newTVarIO arWillClose
+         responseHasCType ← newTVarIO False
+         bodyToSend       ← newEmptyTMVarIO
+
+         state            ← newTVarIO ExaminingRequest
+
+         return NI {
+                  niConfig           = config
+                , niRemoteAddr       = remoteAddr
+                , niRemoteCert       = remoteCert
+                , niRequest          = arRequest
+                , niResourcePath     = rsrcPath
+                , niExpectedContinue = arExpectedContinue
+                , niReqBodyLength    = arReqBodyLength
+
+                , niReceiveBodyReq   = receiveBodyReq
+                , niReceivedBody     = receivedBody
+
+                , niResponse         = response
+                , niSendContinue     = sendContinue
+                , niWillChunkBody    = arWillChunkBody
+                , niWillDiscardBody  = willDiscardBody
+                , niWillClose        = willClose
+                , niResponseHasCType = responseHasCType
+                , niBodyToSend       = bodyToSend
+
+                , niState            = state
+                }
+
+type InteractionQueue = TVar (Seq SomeInteraction)
+
+mkInteractionQueue ∷ IO InteractionQueue
+mkInteractionQueue = newTVarIO (∅)
+
+setResponseStatus ∷ NormalInteraction → StatusCode → STM ()
+setResponseStatus (NI {..}) sc
+    = do res ← readTVar niResponse
+         let res' = res {
+                      resStatus = sc
+                    }
+         writeTVar niResponse res'
 
-updateItrF :: Functor f => Interaction -> (Interaction -> TVar (f a)) -> (a -> a) -> STM ()
-updateItrF !itr !accessor !updator
-    = updateItr itr accessor (fmap updator)
-{-# SPECIALIZE updateItrF :: Interaction -> (Interaction -> TVar (Maybe a)) -> (a -> a) -> STM () #-}
\ No newline at end of file
+getCurrentDate ∷ IO Ascii
+getCurrentDate = HTTP.toAscii <$> getCurrentTime
index a3f3fc5453ff77ee9436b347da9ee94c46879296..ab0e06596320d343164211017a8725ada3b9f07b 100644 (file)
@@ -1,78 +1,84 @@
 {-# LANGUAGE
-    UnboxedTuples
+    OverloadedStrings
   , UnicodeSyntax
   #-}
-{-# OPTIONS_HADDOCK prune #-}
 
--- |Manipulation of MIME Types.
+-- |MIME Types
 module Network.HTTP.Lucu.MIMEType
     ( MIMEType(..)
+    , mkMIMEType
+
     , parseMIMEType
-    , mimeTypeP
-    , mimeTypeListP
+    , printMIMEType
+
+    , mimeType
+    , mimeTypeList
     )
     where
+import Control.Applicative
+import Data.Ascii (Ascii, AsciiBuilder, CIAscii)
+import qualified Data.Ascii as A
+import Data.Attoparsec.Char8 as P
+import Data.Map (Map)
+import Data.Monoid.Unicode
+import Data.Text (Text)
+import Network.HTTP.Lucu.Parser.Http
+import Network.HTTP.Lucu.RFC2231
+import Prelude hiding (min)
+import Prelude.Unicode
 
-import qualified Data.ByteString.Lazy as B
-import           Network.HTTP.Lucu.Parser
-import           Network.HTTP.Lucu.Parser.Http
-import           Network.HTTP.Lucu.Utils
-import           Prelude hiding (min)
-
--- |@'MIMEType' \"major\" \"minor\" [(\"name\", \"value\")]@
--- represents \"major\/minor; name=value\".
+-- |@'MIMEType' \"major\" \"minor\" [(\"name\", \"value\"), ...]@
+-- represents \"major\/minor; name=value; ...\".
 data MIMEType = MIMEType {
-      mtMajor  :: !String
-    , mtMinor  :: !String
-    , mtParams :: ![ (String, String) ]
+      mtMajor  ∷ !CIAscii
+    , mtMinor  ∷ !CIAscii
+    , mtParams ∷ !(Map CIAscii Text)
     } deriving (Eq)
 
-
 instance Show MIMEType where
-    show (MIMEType maj min params)
-        = maj ++ "/" ++ min ++
-          if null params then
-              ""
-          else
-              "; " ++ joinWith "; " (map showPair params)
-        where
-          showPair :: (String, String) -> String
-          showPair (name, value)
-              = name ++ "=" ++ if any (not . isToken) value then
-                                   quoteStr value
-                               else
-                                   value
+    show = A.toString ∘ A.fromAsciiBuilder ∘ printMIMEType
 
+-- |@'mkMIMEType' major minor@ returns a 'MIMEType' with the given
+-- @major@ and @minor@ types but without any parameters.
+mkMIMEType ∷ CIAscii → CIAscii → MIMEType
+{-# INLINE mkMIMEType #-}
+mkMIMEType maj min
+    = MIMEType maj min (∅)
 
-instance Read MIMEType where
-    readsPrec _ s = [(parseMIMEType s, "")]
+-- |Convert a 'MIMEType' to an 'AsciiBuilder'.
+printMIMEType ∷ MIMEType → AsciiBuilder
+{-# INLINEABLE printMIMEType #-}
+printMIMEType (MIMEType maj min params)
+    = A.toAsciiBuilder (A.fromCIAscii maj) ⊕
+      A.toAsciiBuilder "/" ⊕
+      A.toAsciiBuilder (A.fromCIAscii min) ⊕
+      printMIMEParams params
 
--- |Parse 'MIMEType' from a 'Prelude.String'. This function throws an
+-- |Parse 'MIMEType' from an 'Ascii'. This function throws an
 -- exception for parse error.
-parseMIMEType :: String -> MIMEType
-parseMIMEType str = case parseStr mimeTypeP str of
-                      (# Success t, r #) -> if B.null r
-                                            then t
-                                            else error ("unparsable MIME Type: " ++ str)
-                      (# _        , _ #) -> error ("unparsable MIME Type: " ++ str)
-
-
-mimeTypeP :: Parser MIMEType
-mimeTypeP = allowEOF $!
-            do maj    <- token
-               _      <- char '/'
-               min    <- token
-               params <- many paramP
-               return $ MIMEType maj min params
+parseMIMEType ∷ Ascii → MIMEType
+{-# INLINEABLE parseMIMEType #-}
+parseMIMEType str
+    = case parseOnly p $ A.toByteString str of
+        Right  t → t
+        Left err → error ("unparsable MIME Type: " ⧺ A.toString str ⧺ ": " ⧺ err)
     where
-      paramP :: Parser (String, String)
-      paramP = do _     <- many lws
-                  _     <- char ';'
-                  _     <- many lws
-                  name  <- token
-                  _     <- char '='
-                  value <- token <|> quotedStr
-                  return (name, value)
+      p ∷ Parser MIMEType
+      {-# INLINE p #-}
+      p = do t ← mimeType
+             endOfInput
+             return t
+
+-- |'Parser' for an 'MIMEType'.
+mimeType ∷ Parser MIMEType
+{-# INLINEABLE mimeType #-}
+mimeType = do maj    ← A.toCIAscii <$> token
+              _      ← char '/'
+              min    ← A.toCIAscii <$> token
+              params ← mimeParams
+              return $ MIMEType maj min params
 
-mimeTypeListP :: Parser [MIMEType]
-mimeTypeListP = allowEOF $! listOf mimeTypeP
+-- |'Parser' for a list of 'MIMEType's.
+mimeTypeList ∷ Parser [MIMEType]
+{-# INLINE mimeTypeList #-}
+mimeTypeList = listOf mimeType
diff --git a/Network/HTTP/Lucu/MIMEType/DefaultExtensionMap.hs b/Network/HTTP/Lucu/MIMEType/DefaultExtensionMap.hs
deleted file mode 100644 (file)
index d6add2b..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
--- |This module is automatically generated from data\/mime.types.
--- 'defaultExtensionMap' contains every possible pairs of an extension
--- and a MIME Type.
-
-{- !!! WARNING !!!
-   This file is automatically generated.
-   DO NOT EDIT BY HAND OR YOU WILL REGRET -}
-
-module Network.HTTP.Lucu.MIMEType.DefaultExtensionMap
-       (defaultExtensionMap) where
-import Network.HTTP.Lucu.MIMEType ()
-import Network.HTTP.Lucu.MIMEType.Guess
-import qualified Data.Map as M
-defaultExtensionMap :: ExtMap
-defaultExtensionMap
-  = M.fromList
-      [("3gp", read "application/x-3gp"), ("669", read "audio/x-mod"),
-       ("Z", read "application/x-compress"),
-       ("a", read "application/x-ar"), ("ac3", read "audio/x-ac3"),
-       ("ai", read "application/postscript"),
-       ("aif", read "audio/x-aiff"), ("aifc", read "audio/x-aiff"),
-       ("aiff", read "audio/x-aiff"), ("amf", read "audio/x-mod"),
-       ("anx", read "application/ogg"), ("ape", read "application/x-ape"),
-       ("asc", read "text/plain"), ("asf", read "video/x-ms-asf"),
-       ("atom", read "application/atom+xml"), ("au", read "audio/x-au"),
-       ("avi", read "video/x-msvideo"),
-       ("bcpio", read "application/x-bcpio"),
-       ("bin", read "application/octet-stream"),
-       ("bmp", read "image/bmp"), ("bz2", read "application/x-bzip"),
-       ("cabal", read "text/x-cabal"),
-       ("cdf", read "application/x-netcdf"), ("cgm", read "image/cgm"),
-       ("class", read "application/octet-stream"),
-       ("cpio", read "application/x-cpio"),
-       ("cpt", read "application/mac-compactpro"),
-       ("csh", read "application/x-csh"), ("css", read "text/css"),
-       ("dcr", read "application/x-director"), ("dif", read "video/x-dv"),
-       ("dir", read "application/x-director"),
-       ("djv", read "image/vnd.djvu"), ("djvu", read "image/vnd.djvu"),
-       ("dll", read "application/octet-stream"),
-       ("dmg", read "application/octet-stream"),
-       ("dms", read "application/octet-stream"),
-       ("doc", read "application/msword"), ("dsm", read "audio/x-mod"),
-       ("dtd", read "application/xml-dtd"), ("dv", read "video/x-dv"),
-       ("dvi", read "application/x-dvi"),
-       ("dxr", read "application/x-director"),
-       ("eps", read "application/postscript"),
-       ("etx", read "text/x-setext"),
-       ("exe", read "application/octet-stream"),
-       ("ez", read "application/andrew-inset"),
-       ("far", read "audio/x-mod"), ("flac", read "audio/x-flac"),
-       ("flc", read "video/x-fli"), ("fli", read "video/x-fli"),
-       ("flv", read "video/x-flv"), ("gdm", read "audio/x-mod"),
-       ("gif", read "image/gif"), ("gram", read "application/srgs"),
-       ("grxml", read "application/srgs+xml"),
-       ("gtar", read "application/x-gtar"),
-       ("gz", read "application/x-gzip"),
-       ("hdf", read "application/x-hdf"),
-       ("hi", read "application/octet-stream"),
-       ("hqx", read "application/mac-binhex40"),
-       ("hs", read "text/x-haskell"), ("htm", read "text/html"),
-       ("html", read "text/html"),
-       ("ice", read "x-conference/x-cooltalk"),
-       ("ico", read "image/x-icon"), ("ics", read "text/calendar"),
-       ("ief", read "image/ief"), ("ifb", read "text/calendar"),
-       ("iff", read "audio/x-svx"), ("iges", read "model/iges"),
-       ("igs", read "model/iges"), ("ilbc", read "audio/iLBC-sh"),
-       ("imf", read "audio/x-mod"), ("it", read "audio/x-mod"),
-       ("jng", read "image/x-jng"),
-       ("jnlp", read "application/x-java-jnlp-file"),
-       ("jp2", read "image/jp2"), ("jpe", read "image/jpeg"),
-       ("jpeg", read "image/jpeg"), ("jpg", read "image/jpeg"),
-       ("js", read "application/x-javascript"),
-       ("kar", read "audio/midi"), ("latex", read "application/x-latex"),
-       ("lha", read "application/octet-stream"),
-       ("lzh", read "application/octet-stream"),
-       ("m3u", read "audio/x-mpegurl"), ("m4a", read "audio/mp4a-latm"),
-       ("m4p", read "audio/mp4a-latm"), ("m4u", read "video/vnd.mpegurl"),
-       ("m4v", read "video/mpeg4"), ("mac", read "image/x-macpaint"),
-       ("man", read "application/x-troff-man"),
-       ("mathml", read "application/mathml+xml"),
-       ("me", read "application/x-troff-me"), ("med", read "audio/x-mod"),
-       ("mesh", read "model/mesh"), ("mid", read "audio/midi"),
-       ("midi", read "audio/midi"), ("mif", read "application/vnd.mif"),
-       ("mka", read "video/x-matroska"), ("mkv", read "video/x-matroska"),
-       ("mng", read "video/x-mng"), ("mod", read "audio/x-mod"),
-       ("mov", read "video/quicktime"),
-       ("movie", read "video/x-sgi-movie"), ("mp2", read "audio/mpeg"),
-       ("mp3", read "audio/mpeg"), ("mp4", read "video/mp4"),
-       ("mpc", read "audio/x-musepack"), ("mpe", read "video/mpeg"),
-       ("mpeg", read "video/mpeg"), ("mpg", read "video/mpeg"),
-       ("mpga", read "audio/mpeg"), ("ms", read "application/x-troff-ms"),
-       ("msh", read "model/mesh"), ("mtm", read "audio/x-mod"),
-       ("mve", read "video/x-mve"), ("mxu", read "video/vnd.mpegurl"),
-       ("nar", read "application/x-nar"),
-       ("nc", read "application/x-netcdf"), ("nist", read "audio/x-nist"),
-       ("nuv", read "video/x-nuv"),
-       ("o", read "application/octet-stream"),
-       ("oda", read "application/oda"), ("ogg", read "application/ogg"),
-       ("ogm", read "application/ogg"), ("okt", read "audio/x-mod"),
-       ("paf", read "audio/x-paris"),
-       ("pbm", read "image/x-portable-bitmap"),
-       ("pct", read "image/pict"), ("pdb", read "chemical/x-pdb"),
-       ("pdf", read "application/pdf"),
-       ("pgm", read "image/x-portable-graymap"),
-       ("pgn", read "application/x-chess-pgn"),
-       ("pic", read "image/pict"), ("pict", read "image/pict"),
-       ("png", read "image/png"), ("pnm", read "image/x-portable-anymap"),
-       ("pnt", read "image/x-macpaint"),
-       ("pntg", read "image/x-macpaint"),
-       ("ppm", read "image/x-portable-pixmap"),
-       ("ppt", read "application/vnd.ms-powerpoint"),
-       ("ps", read "application/postscript"),
-       ("qif", read "image/x-quicktime"), ("qt", read "video/quicktime"),
-       ("qti", read "image/x-quicktime"),
-       ("qtif", read "image/x-quicktime"),
-       ("ra", read "audio/x-pn-realaudio"), ("ram", read "text/uri-list"),
-       ("rar", read "application/x-rar"),
-       ("ras", read "image/x-sun-raster"),
-       ("rdf", read "application/rdf+xml"), ("rgb", read "image/x-rgb"),
-       ("rm", read "application/vnd.rn-realmedia"),
-       ("roff", read "application/x-troff"), ("rtf", read "text/rtf"),
-       ("rtx", read "text/richtext"), ("s3m", read "audio/x-mod"),
-       ("sam", read "audio/x-mod"), ("sds", read "audio/x-sds"),
-       ("sf", read "audio/x-ircam"), ("sgm", read "text/sgml"),
-       ("sgml", read "text/sgml"), ("sh", read "application/x-sh"),
-       ("shar", read "application/x-shar"),
-       ("shn", read "audio/x-shorten"), ("sid", read "audio/x-sid"),
-       ("silo", read "model/mesh"), ("sit", read "application/x-stuffit"),
-       ("skd", read "application/x-koan"),
-       ("skm", read "application/x-koan"),
-       ("skp", read "application/x-koan"),
-       ("skt", read "application/x-koan"),
-       ("smi", read "application/smil"),
-       ("smil", read "application/smil"), ("snd", read "audio/x-au"),
-       ("so", read "application/octet-stream"),
-       ("spc", read "application/x-spc"),
-       ("spl", read "application/x-futuresplash"),
-       ("src", read "application/x-wais-source"),
-       ("stm", read "audio/x-mod"), ("stx", read "audio/x-mod"),
-       ("sv4cpio", read "application/x-sv4cpio"),
-       ("sv4crc", read "application/x-sv4crc"),
-       ("svg", read "image/svg+xml"), ("svx", read "audio/x-svx"),
-       ("swf", read "application/x-shockwave-flash"),
-       ("swfl", read "application/x-shockwave-flash"),
-       ("t", read "application/x-troff"),
-       ("tar", read "application/x-tar"),
-       ("tbz", read "application/x-bzip"),
-       ("tcl", read "application/x-tcl"),
-       ("tex", read "application/x-tex"),
-       ("texi", read "application/x-texinfo"),
-       ("texinfo", read "application/x-texinfo"),
-       ("tgz", read "application/x-gzip"), ("tif", read "image/tiff"),
-       ("tiff", read "image/tiff"), ("tr", read "application/x-troff"),
-       ("ts", read "video/mpegts"),
-       ("tsv", read "text/tab-separated-values"),
-       ("tta", read "audio/x-ttafile"), ("txt", read "text/plain"),
-       ("ult", read "audio/x-mod"), ("ustar", read "application/x-ustar"),
-       ("vcd", read "application/x-cdlink"), ("voc", read "audio/x-voc"),
-       ("vrml", read "model/vrml"),
-       ("vxml", read "application/voicexml+xml"),
-       ("w64", read "audio/x-w64"), ("wav", read "audio/x-wav"),
-       ("wbmp", read "image/vnd.wap.wbmp"),
-       ("wbxml", read "application/vnd.wap.wbxml"),
-       ("wm", read "video/x-ms-asf"), ("wma", read "video/x-ms-asf"),
-       ("wml", read "text/vnd.wap.wml"),
-       ("wmlc", read "application/vnd.wap.wmlc"),
-       ("wmls", read "text/vnd.wap.wmlscript"),
-       ("wmlsc", read "application/vnd.wap.wmlscriptc"),
-       ("wmv", read "video/x-ms-asf"), ("wrl", read "model/vrml"),
-       ("wv", read "audio/x-wavpack"),
-       ("wvc", read "audio/x-wavpack-correction"),
-       ("wvp", read "audio/x-wavpack"), ("xbm", read "image/x-xbitmap"),
-       ("xcf", read "image/x-xcf"), ("xht", read "application/xhtml+xml"),
-       ("xhtml", read "application/xhtml+xml"),
-       ("xls", read "application/vnd.ms-excel"),
-       ("xm", read "audio/x-mod"), ("xml", read "application/xml"),
-       ("xpm", read "image/x-xpixmap"), ("xsl", read "application/xml"),
-       ("xslt", read "application/xslt+xml"),
-       ("xul", read "application/vnd.mozilla.xul+xml"),
-       ("xwd", read "image/x-xwindowdump"),
-       ("xyz", read "chemical/x-xyz"), ("zip", read "application/zip")]
index 39de37e07d68464b8029021f745e956fa236c036..d8bca8e785658efa5390862f8baa97c071a93f58 100644 (file)
@@ -1,9 +1,8 @@
 {-# LANGUAGE
-    UnboxedTuples
-  , UnicodeSyntax
+    UnicodeSyntax
   #-}
--- |MIME Type guessing by a file extension. This is a poor man's way
--- of guessing MIME Types. It is simple and fast.
+-- |Guessing MIME Types by file extensions. It's not always accurate
+-- but simple and fast.
 --
 -- In general you don't have to use this module directly.
 module Network.HTTP.Lucu.MIMEType.Guess
@@ -14,110 +13,144 @@ module Network.HTTP.Lucu.MIMEType.Guess
     , serializeExtMap
     )
     where
-
+import Control.Applicative
+import Control.Monad
+import qualified Data.Ascii as A
+import Data.Attoparsec.Char8 as P
+import qualified Data.Attoparsec.Lazy as LP
 import qualified Data.ByteString.Lazy.Char8 as B
 import qualified Data.Map as M
-import           Data.Map (Map)
-import           Data.Maybe
-import           Language.Haskell.Pretty
-import           Language.Haskell.Syntax
-import           Network.HTTP.Lucu.MIMEType
-import           Network.HTTP.Lucu.Parser
-import           Network.HTTP.Lucu.Parser.Http
-import           Network.HTTP.Lucu.Utils
+import Data.Map (Map)
+import Data.Maybe
+import Data.Monoid.Unicode
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Text.Encoding
+import Language.Haskell.Exts.Build
+import Language.Haskell.Exts.Extension
+import Language.Haskell.Exts.Pretty
+import Language.Haskell.Exts.Syntax
+import Network.HTTP.Lucu.MIMEType
+import Prelude.Unicode
+import System.FilePath
 
--- |'Data.Map.Map' from extension to MIME Type.
-type ExtMap = Map String MIMEType
+-- |A 'Map' from file extensions to 'MIMEType's.
+type ExtMap = Map Text MIMEType
 
--- |Guess the MIME Type of file.
-guessTypeByFileName :: ExtMap -> FilePath -> Maybe MIMEType
+-- |Guess the MIME Type of file.
+guessTypeByFileName ∷ ExtMap → FilePath → Maybe MIMEType
 guessTypeByFileName extMap fpath
-    = extMap `seq` fpath `seq`
-      let ext = last $ splitBy (== '.') fpath
-      in
-        M.lookup ext extMap >>= return
+    = case takeExtension fpath of
+        []      → Nothing
+        (_:ext) → M.lookup (T.pack ext) extMap
 
 -- |Read an Apache mime.types and parse it.
-parseExtMapFile :: FilePath -> IO ExtMap
+parseExtMapFile ∷ FilePath → IO ExtMap
 parseExtMapFile fpath
-    = fpath `seq`
-      do file <- B.readFile fpath
-         case parse (allowEOF extMapP) file of
-           (# Success xs, _ #)
-               -> return $ compile xs
-
-           (# _, input' #)
-               -> let near = B.unpack $ B.take 100 input'
-                  in 
-                    fail ("Failed to parse: " ++ fpath ++ " (near: " ++ near ++ ")")
-
+    = do file ← B.readFile fpath
+         case LP.parse extMapP file of
+           LP.Done _ xs
+               → case compile xs of
+                    Right m → return m
+                    Left  e → fail (concat [ "Duplicate extension \""
+                                           , show e
+                                           , "\" in: "
+                                           , fpath
+                                           ])
+           LP.Fail _ _ e
+               → fail ("Failed to parse: " ⧺ fpath ⧺ ": " ⧺ e)
 
-extMapP :: Parser [ (MIMEType, [String]) ]
-extMapP = do xs <- many (comment <|> validLine <|> emptyLine)
-             eof
+extMapP ∷ Parser [ (MIMEType, [Text]) ]
+extMapP = do xs ← P.many (try comment <|> try validLine <|> emptyLine)
+             endOfInput
              return $ catMaybes xs
     where
-      spc = oneOf " \t"
+      isSpc ∷ Char → Bool
+      isSpc c = c ≡ '\x20' ∨ c ≡ '\x09'
 
-      comment = many spc >>
-                char '#' >>
-                ( many $ satisfy (/= '\n') ) >>
-                return Nothing
+      comment ∷ Parser (Maybe (MIMEType, [Text]))
+      comment = do skipWhile isSpc
+                   void $ char '#'
+                   skipWhile (≢ '\x0A')
+                   return Nothing
 
-      validLine = do _    <- many spc
-                     mime <- mimeTypeP
-                     _    <- many spc
-                     exts <- sepBy token (many spc)
+      validLine ∷ Parser (Maybe (MIMEType, [Text]))
+      validLine = do skipWhile isSpc
+                     mime ← mimeType
+                     skipWhile isSpc
+                     exts ← sepBy extP (skipWhile isSpc)
                      return $ Just (mime, exts)
 
-      emptyLine = oneOf " \t\n" >> return Nothing
+      extP ∷ Parser Text
+      extP = decodeUtf8 <$> takeWhile1 (\c → (¬) (isSpc c ∨ c ≡ '\x0A'))
 
+      emptyLine ∷ Parser (Maybe (MIMEType, [Text]))
+      emptyLine = do skipWhile isSpc
+                     void $ char '\x0A'
+                     return Nothing
 
-compile :: [ (MIMEType, [String]) ] -> Map String MIMEType
-compile = M.fromList . foldr (++) [] . map tr
+compile ∷ Ord k ⇒ [(v, [k])] → Either (k, v, v) (Map k v)
+compile = go (∅) ∘ concat ∘ map tr
     where
-      tr :: (MIMEType, [String]) -> [ (String, MIMEType) ]
-      tr (mime, exts) = [ (ext, mime) | ext <- exts ]
+      tr ∷ (v, [k]) → [(k, v)]
+      tr (v, ks) = [(k, v) | k ← ks]
+
+      go ∷ Ord k ⇒ Map k v → [(k, v)] → Either (k, v, v) (Map k v)
+      go m []         = Right m
+      go m ((k, v):xs)
+          = case M.insertLookupWithKey' f k v m of
+              (Nothing, m') → go m' xs
+              (Just v0, _ ) → Left (k, v0, v)
+
+      f ∷ k → v → v → v
+      f _ _ = id
 
 -- |@'serializeExtMap' extMap moduleName variableName@ generates a
 -- Haskell source code which contains the following things:
 --
 -- * A definition of module named @moduleName@.
 --
--- * @variableName :: 'ExtMap'@ whose content is a serialization of
---   @extMap@.
+-- * @variableName :: 'ExtMap'@ whose content is the serialised
+-- @extMap@.
 --
 -- The module "Network.HTTP.Lucu.MIMEType.DefaultExtensionMap" is
 -- surely generated using this function.
-serializeExtMap :: ExtMap -> String -> String -> String
+serializeExtMap ∷ ExtMap → String → String → String
 serializeExtMap extMap moduleName variableName
-    = let hsModule = HsModule undefined modName (Just exports) imports decls
-          modName  = Module moduleName
-          exports  = [HsEVar (UnQual (HsIdent variableName))]
-          imports  = [ HsImportDecl undefined (Module "Network.HTTP.Lucu.MIMEType") False Nothing (Just (False, []))
-                     , HsImportDecl undefined (Module "Network.HTTP.Lucu.MIMEType.Guess") False Nothing Nothing
-                     , HsImportDecl undefined (Module "Data.Map") True (Just (Module "M")) Nothing
-                     ]
-          decls    = [ HsTypeSig undefined [HsIdent variableName]
-                                     (HsQualType []
-                                      (HsTyCon (UnQual (HsIdent "ExtMap"))))
-                     , HsFunBind [HsMatch undefined (HsIdent variableName)
-                                  [] (HsUnGuardedRhs extMapExp) []]
-                     ]
-          extMapExp = HsApp (HsVar (Qual (Module "M") (HsIdent "fromList"))) (HsList records)
-          comment   =    "{- !!! WARNING !!!\n"
-                      ++ "   This file is automatically generated.\n"
-                      ++ "   DO NOT EDIT BY HAND OR YOU WILL REGRET -}\n\n"
+    = let hsModule  = Module (⊥) (ModuleName moduleName) modPragma
+                      Nothing (Just exports) imports decls
+          modPragma = [ LanguagePragma (⊥) [ name (show OverloadedStrings) ] ]
+          exports   = [ EVar (UnQual (name variableName)) ]
+          imports   = [ ImportDecl (⊥) (ModuleName "Network.HTTP.Lucu.MIMEType")
+                                   False False Nothing Nothing Nothing
+                      , ImportDecl (⊥) (ModuleName "Network.HTTP.Lucu.MIMEType.Guess")
+                                   False False Nothing Nothing Nothing
+                      , ImportDecl (⊥) (ModuleName "Data.Ascii")
+                                   False False Nothing Nothing (Just (False, []))
+                      , ImportDecl (⊥) (ModuleName "Data.Map")
+                                   True False Nothing (Just (ModuleName "M")) Nothing
+                      ]
+          decls     = [ TypeSig (⊥) [name variableName]
+                                    (TyCon (UnQual (name "ExtMap")))
+                      , nameBind (⊥) (name variableName) extMapExp
+                      , InlineSig (⊥) False AlwaysActive (UnQual (name variableName))
+                      ]
+          comment   = concat [ "{- !!! WARNING !!!\n"
+                             , "   This file is automatically generated.\n"
+                             , "   DO NOT EDIT BY HAND OR YOU WILL REGRET -}\n\n"
+                             ]
+          extMapExp = qvar (ModuleName "M") (name "fromList") `app` listE records
       in
-        comment ++ prettyPrint hsModule ++ "\n"
+        comment ⧺ prettyPrint hsModule ⧺ "\n"
     where
-      records :: [HsExp]
+      records ∷ [Exp]
       records = map record $ M.assocs extMap
 
-      record :: (String, MIMEType) -> HsExp
+      record ∷ (Text, MIMEType) → Exp
       record (ext, mime)
-          = HsTuple [HsLit (HsString ext), mimeToExp mime]
-                    
-      mimeToExp :: MIMEType -> HsExp
-      mimeToExp mt
-          = HsApp (HsVar (UnQual (HsIdent "read"))) (HsLit (HsString $ show mt))
+          = tuple [ strE (T.unpack ext)
+                  , function "parseMIMEType" `app` strE (mimeToString mime)
+                  ]
+
+      mimeToString ∷ MIMEType → String
+      mimeToString = A.toString ∘ A.fromAsciiBuilder ∘ printMIMEType
index c4631300e9efae3b3d14ac57917597ef685032fd..a04b4a059f9a28c7e10b3ffc6b7f144b30df0252 100644 (file)
 {-# LANGUAGE
-    UnboxedTuples
+    DoAndIfThenElse
+  , FlexibleContexts
+  , OverloadedStrings
+  , RecordWildCards
+  , ScopedTypeVariables
   , UnicodeSyntax
+  , ViewPatterns
   #-}
+-- |Parse \"multipart/form-data\" based on RFC 2388:
+-- <http://www.faqs.org/rfcs/rfc2388.html>
+--
+-- You usually don't have to use this module directly.
 module Network.HTTP.Lucu.MultipartForm
     ( FormData(..)
-    , multipartFormP
+    , parseMultipartFormData
     )
     where
+import Control.Applicative hiding (many)
+import Control.Applicative.Unicode hiding ((∅))
+import Control.Monad.Error
+import Control.Monad.Unicode
+import Data.Ascii (Ascii, CIAscii)
+import qualified Data.Ascii as A
+import Data.Attoparsec
+import qualified Data.Attoparsec.Lazy as LP
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Lazy as LS
+import Data.ByteString.Lazy.Search
+import Data.Foldable
+import Data.List
+import Data.Map (Map)
+import qualified Data.Map as M
+import Data.Maybe
+import Data.Monoid.Unicode
+import Data.Sequence (Seq)
+import Data.Sequence.Unicode hiding ((∅))
+import Data.Text (Text)
+import qualified Data.Text as T
+import Network.HTTP.Lucu.Headers
+import Network.HTTP.Lucu.MIMEType
+import Network.HTTP.Lucu.Parser.Http
+import Network.HTTP.Lucu.RFC2231
+import Prelude.Unicode
 
-import qualified Data.ByteString.Char8 as C8
-import qualified Data.ByteString.Lazy.Char8 as L8
-import           Data.Char
-import           Data.List
-import           Network.HTTP.Lucu.Abortion
-import           Network.HTTP.Lucu.Headers
-import           Network.HTTP.Lucu.Parser
-import           Network.HTTP.Lucu.Parser.Http
-import           Network.HTTP.Lucu.Response
-import           Network.HTTP.Lucu.Utils
-
-
-data Part = Part Headers L8.ByteString
-
--- |This data type represents a form value and possibly an uploaded
--- file name.
+-- |'FormData' represents a form value and possibly an uploaded file
+-- name.
 data FormData
     = FormData {
-        fdFileName :: Maybe String
-      , fdContent  :: L8.ByteString
+        -- | @'Nothing'@ for non-file values.
+        fdFileName ∷ !(Maybe Text)
+        -- | MIME Type of this value, defaulted to \"text/plain\".
+      , fdMIMEType ∷ !MIMEType
+        -- | The form value.
+      , fdContent  ∷ !(LS.ByteString)
+      }
+
+data Part
+    = Part {
+        ptContDispo ∷ !ContDispo
+      , ptContType  ∷ !MIMEType
+      , ptBody      ∷ !LS.ByteString
       }
 
-instance HasHeaders Part where
-    getHeaders (Part hs _)    = hs
-    setHeaders (Part _  b) hs = Part hs b
-
-
-data ContDispo = ContDispo String [(String, String)]
-
-instance Show ContDispo where
-    show (ContDispo dType dParams)
-        = dType ++
-          if null dParams then
-              ""
-          else
-              "; " ++ joinWith "; " (map showPair dParams)
-        where
-          showPair :: (String, String) -> String
-          showPair (name, value)
-              = name ++ "=" ++ if any (not . isToken) value then
-                                   quoteStr value
-                               else
-                                   value
-
-
-multipartFormP :: String -> Parser [(String, FormData)]
-multipartFormP boundary
-    = do parts <- many (partP boundary)
-         _     <- string "--"
-         _     <- string boundary
-         _     <- string "--"
-         _     <- crlf
-         eof
-         return $ map partToFormPair parts
-
-
-partP :: String -> Parser Part
-partP boundary
-    = do _    <- string "--"
-         _    <- string boundary
-         _    <- crlf -- バウンダリの末尾に -- が付いてゐたらここで fail する。
-         hs   <- headersP
-         body <- bodyP boundary
-         return $ Part hs body
-
-
-bodyP :: String -> Parser L8.ByteString
-bodyP boundary
-    = do body <- manyChar $
-                 do notFollowedBy $ ( crlf         >>
-                                      string "--"  >>
-                                      string boundary )
-                    anyChar
-         _    <- crlf
-         return body
-
-
-partToFormPair :: Part -> (String, FormData)
-partToFormPair part@(Part _ body)
-    = let name  = partName part
-          fname = partFileName part
-          fd    = FormData {
-                    fdFileName = fname
-                  , fdContent  = body
-                  }
-      in (name, fd)
-
-partName :: Part -> String
-partName = getName' . getContDispoFormData
+data ContDispo
+    = ContDispo {
+        dType   ∷ !CIAscii
+      , dParams ∷ !(Map CIAscii Text)
+      }
+
+printContDispo ∷ ContDispo → Ascii
+printContDispo d
+    = A.fromAsciiBuilder
+      ( A.toAsciiBuilder (A.fromCIAscii $ dType d)
+        ⊕
+        printMIMEParams (dParams d) )
+
+-- |Parse \"multipart/form-data\" and return either @'Left' err@ or
+-- @'Right' result@. Note that there are currently the following
+-- limitations:
+--
+--   * Multiple files embedded as \"multipart/mixed\" within the
+--     \"multipart/form-data\" won't be decomposed.
+--
+--   * \"Content-Transfer-Encoding\" is always ignored.
+--
+--   * RFC 2388 says that non-ASCII field names are encoded according
+--     to the method in RFC 2047
+--     <http://www.faqs.org/rfcs/rfc2047.html>, but they won't be
+--     decoded.
+parseMultipartFormData ∷ Ascii -- ^boundary
+                       → LS.ByteString -- ^input
+                       → Either String [(Ascii, FormData)]
+parseMultipartFormData boundary = (mapM partToFormPair =≪) ∘ go
     where
-      getName' :: ContDispo -> String
-      getName' dispo@(ContDispo _ dParams)
-          = case find ((== "name") . map toLower . fst) dParams of
-              Just (_, name) -> name
-              Nothing   
-                  -> abortPurely BadRequest []
-                     (Just $ "form-data without name: " ++ show dispo)
+      go ∷ (Functor m, MonadError String m)
+         ⇒ LS.ByteString
+         → m [Part]
+      {-# INLINEABLE go #-}
+      go src
+          = case LP.parse (prologue boundary) src of
+              LP.Done src' _
+                  → go' src' (∅)
+              LP.Fail _ eCtx e
+                  → throwError $ "Unparsable multipart/form-data: "
+                               ⧺ intercalate ", " eCtx
+                               ⧺ ": "
+                               ⧺ e
+      go' ∷ (Functor m, MonadError String m)
+          ⇒ LS.ByteString
+          → Seq Part
+          → m [Part]
+      {-# INLINEABLE go' #-}
+      go' src xs
+          = case LP.parse epilogue src of
+              LP.Done _ _
+                  → return $ toList xs
+              LP.Fail _ _ _
+                  → do (src', x) ← parsePart boundary src
+                       go' src' $ xs ⊳ x
+
+prologue ∷ Ascii → Parser ()
+prologue boundary
+    = ( (string "--" <?> "prefix")
+        *>
+        (string (A.toByteString boundary) <?> "boundary")
+        *>
+        pure ()
+      )
+      <?>
+      "prologue"
 
+epilogue ∷ Parser ()
+epilogue = ( (string "--" <?> "suffix")
+             *>
+             crlf
+             *>
+             endOfInput
+           )
+           <?>
+           "epilogue"
 
-partFileName :: Part -> Maybe String
-partFileName = getFileName' . getContDispoFormData
+parsePart ∷ (Functor m, MonadError String m)
+          ⇒ Ascii
+          → LS.ByteString
+          → m (LS.ByteString, Part)
+{-# INLINEABLE parsePart #-}
+parsePart boundary src
+    = case LP.parse partHeader src of
+        LP.Done src' hdrs
+            → do dispo ← getContDispo hdrs
+                 cType ← fromMaybe defaultCType <$> getContType hdrs
+                 (body, src'')
+                       ← getBody boundary src'
+                 return (src'', Part dispo cType body)
+        LP.Fail _ eCtx e
+            → throwError $ "unparsable part: "
+                         ⧺ intercalate ", " eCtx
+                         ⧺ ": "
+                         ⧺ e
+      where
+        defaultCType ∷ MIMEType
+        defaultCType = parseMIMEType "text/plain"
+
+partHeader ∷ Parser Headers
+partHeader = crlf *> headers
+
+getContDispo ∷ MonadError String m ⇒ Headers → m ContDispo
+{-# INLINEABLE getContDispo #-}
+getContDispo hdrs
+    = case getHeader "Content-Disposition" hdrs of
+        Nothing
+            → throwError "Content-Disposition is missing"
+        Just str
+            → case parseOnly p $ A.toByteString str of
+                 Right  d → return d
+                 Left err → throwError $ "malformed Content-Disposition: "
+                                       ⧺ A.toString str
+                                       ⧺ ": "
+                                       ⧺ err
     where
-      getFileName' :: ContDispo -> Maybe String
-      getFileName' (ContDispo _ dParams)
-          = do (_, fileName) <- find ((== "filename") . map toLower . fst) dParams
-               return fileName
-
-getContDispoFormData :: Part -> ContDispo
-getContDispoFormData part
-    = let dispo@(ContDispo dType _) = getContDispo part
-      in
-        if map toLower dType == "form-data" then
-            dispo
-        else
-            abortPurely BadRequest []
-            (Just $ "Content-Disposition type is not form-data: " ++ dType)
-
-
-getContDispo :: Part -> ContDispo
-getContDispo part
-    = case getHeader (C8.pack "Content-Disposition") part of
-        Nothing  
-            -> abortPurely BadRequest []
-               (Just "There is a part without Content-Disposition in the multipart/form-data.")
-        Just dispoStr
-            -> case parse contDispoP (L8.fromChunks [dispoStr]) of
-                 (# Success dispo, _ #)
-                     -> dispo
-                 (# _, _ #)
-                     -> abortPurely BadRequest []
-                        (Just $ "Unparsable Content-Disposition: " ++ C8.unpack dispoStr)
-
-
-contDispoP :: Parser ContDispo
-contDispoP = do dispoType <- token
-                params    <- allowEOF $ many paramP
-                return $ ContDispo dispoType params
+      p = do dispo ← contentDisposition
+             endOfInput
+             return dispo
+
+contentDisposition ∷ Parser ContDispo
+contentDisposition
+    = (ContDispo <$> (A.toCIAscii <$> token) ⊛ mimeParams)
+      <?>
+      "contentDisposition"
+
+getContType ∷ MonadError String m ⇒ Headers → m (Maybe MIMEType)
+{-# INLINEABLE getContType #-}
+getContType hdrs
+    = case getHeader "Content-Type" hdrs of
+        Nothing
+            → return Nothing
+        Just str
+            → case parseOnly p $ A.toByteString str of
+                 Right  d → return $ Just d
+                 Left err → throwError $ "malformed Content-Type: "
+                                       ⧺ A.toString str
+                                       ⧺ ": "
+                                       ⧺ err
     where
-      paramP :: Parser (String, String)
-      paramP = do _     <- many lws
-                  _     <- char ';'
-                  _     <- many lws
-                  name  <- token
-                  _     <- char '='
-                  value <- token <|> quotedStr
-                  return (name, value)
+      p = do t ← mimeType
+             endOfInput
+             return t
+
+getBody ∷ MonadError String m
+        ⇒ Ascii
+        → LS.ByteString
+        → m (LS.ByteString, LS.ByteString)
+{-# INLINEABLE getBody #-}
+getBody (("\r\n--" ⊕) ∘ A.toByteString → boundary) src
+    = case breakOn boundary src of
+        (before, after)
+            | LS.null after
+                → throwError "missing boundary"
+            | otherwise
+                → let len    = fromIntegral $ BS.length boundary
+                      after' = LS.drop len after
+                  in
+                    return (before, after')
+
+partToFormPair ∷ MonadError String m ⇒ Part → m (Ascii, FormData)
+{-# INLINEABLE partToFormPair #-}
+partToFormPair pt@(Part {..})
+    | dType ptContDispo ≡ "form-data"
+        = do name ← partName pt
+             let fd = FormData {
+                        fdFileName = partFileName pt
+                      , fdMIMEType = ptContType
+                      , fdContent  = ptBody
+                      }
+             return (name, fd)
+    | otherwise
+        = throwError $ "disposition type is not \"form-data\": "
+                     ⧺ A.toString (A.fromCIAscii $ dType ptContDispo)
+
+partName ∷ MonadError String m ⇒ Part → m Ascii
+{-# INLINEABLE partName #-}
+partName (Part {..})
+    = case M.lookup "name" $ dParams ptContDispo of
+        Just name
+            → case A.fromText name of
+                 Just a  → return a
+                 Nothing → throwError $ "Non-ascii part name: "
+                                      ⧺ T.unpack name
+        Nothing
+            → throwError $ "form-data without name: "
+                         ⧺ A.toString (printContDispo ptContDispo)
+
+partFileName ∷ Part → Maybe Text
+partFileName (Part {..})
+    = M.lookup "filename" $ dParams ptContDispo
index 7809f534790d2d770e147dc3b362a7df9b5c1203..ce4371878890301eb152d3b9ef55c526c4127174 100644 (file)
 {-# LANGUAGE
-    BangPatterns
-  , ScopedTypeVariables
-  , UnboxedTuples
-  , UnicodeSyntax
+    UnicodeSyntax
   #-}
--- |Yet another parser combinator. This is mostly a subset of
--- "Text.ParserCombinators.Parsec" but there are some differences:
---
--- * This parser works on 'Data.ByteString.Base.LazyByteString'
---   instead of 'Prelude.String'.
---
--- * Backtracking is the only possible behavior so there is no \"try\"
---   action.
---
--- * On success, the remaining string is returned as well as the
---   parser result.
---
--- * You can choose whether to treat reaching EOF (trying to eat one
---   more letter at the end of string) a fatal error or to treat it a
---   normal failure. If a fatal error occurs, the entire parsing
---   process immediately fails without trying any backtracks. The
---   default behavior is to treat EOF fatal.
---
--- In general, you don't have to use this module directly.
+-- |This is an auxiliary parser utilities. You usually don't have to
+-- use this module directly.
 module Network.HTTP.Lucu.Parser
-    ( Parser
-    , ParserResult(..)
-
-    , failP
-
-    , parse
-    , parseStr
-
-    , anyChar
-    , eof
-    , allowEOF
-    , satisfy
-    , char
-    , string
-    , (<|>)
-    , choice
-    , oneOf
-    , digit
-    , hexDigit
-    , notFollowedBy
-    , many
-    , manyChar
-    , many1
-    , count
-    , option
-    , sepBy
-    , sepBy1
-
-    , sp
-    , ht
-    , crlf
+    ( atMost
     )
     where
-
-import           Control.Monad.State.Strict hiding (state)
-import qualified Data.ByteString.Lazy as Lazy (ByteString)
-import qualified Data.ByteString.Lazy.Char8 as B hiding (ByteString)
-import qualified Data.Foldable as Fold
-import           Data.Int
-import qualified Data.Sequence as Seq
-import           Data.Sequence (Seq, (|>))
-
--- |@'Parser' a@ is obviously a parser which parses and returns @a@.
-newtype Parser a = Parser {
-      runParser :: State ParserState (ParserResult a)
-    }
-
-
-data ParserState
-    = PST {
-        pstInput      :: Lazy.ByteString
-      , pstIsEOFFatal :: !Bool
-      }
-    deriving (Eq, Show)
-
-
-data ParserResult a = Success !a
-                    | IllegalInput -- 受理出來ない入力があった
-                    | ReachedEOF   -- 限界を越えて讀まうとした
-                      deriving (Eq, Show)
-
-
---  (>>=) :: Parser a -> (a -> Parser b) -> Parser b
-instance Monad Parser where
-    p >>= f = Parser $! do saved <- get -- 失敗した時の爲に状態を保存
-                           result <- runParser p
-                           case result of
-                             Success a    -> runParser (f a)
-                             IllegalInput -> do put saved -- 状態を復歸
-                                                return IllegalInput
-                             ReachedEOF   -> do put saved -- 状態を復歸
-                                                return ReachedEOF
-    return !x = Parser $! return $! Success x
-    fail _    = Parser $! return $! IllegalInput
-
-instance Functor Parser where
-    fmap f p = p >>= return . f
-
--- |@'failP'@ is just a synonym for @'Prelude.fail'
--- 'Prelude.undefined'@.
-failP :: Parser a
-failP = fail undefined
-
--- |@'parse' p bstr@ parses @bstr@ with @p@ and returns @(# result,
--- remaining #)@.
-parse :: Parser a -> Lazy.ByteString -> (# ParserResult a, Lazy.ByteString #)
-parse !p input -- input は lazy である必要有り。
-    = let (!result, state') = runState (runParser p) (PST input True)
-      in
-        (# result, pstInput state' #) -- pstInput state' も lazy である必要有り。
-
--- |@'parseStr' p str@ packs @str@ and parses it.
-parseStr :: Parser a -> String -> (# ParserResult a, Lazy.ByteString #)
-parseStr !p input -- input は lazy である必要有り。
-    = parse p (B.pack input)
-
-
-anyChar :: Parser Char
-anyChar = Parser $!
-          do state@(PST input _) <- get
-             if B.null input then
-                 return ReachedEOF
-               else
-                 do put $! state { pstInput = B.tail input }
-                    return (Success $! B.head input)
-
-
-eof :: Parser ()
-eof = Parser $!
-      do PST input _ <- get
-         if B.null input then
-             return $! Success ()
-           else
-             return IllegalInput
-
--- |@'allowEOF' p@ makes @p@ treat reaching EOF a normal failure.
-allowEOF :: Parser a -> Parser a
-allowEOF !f
-    = Parser $! do saved@(PST _ isEOFFatal) <- get
-                   put $! saved { pstIsEOFFatal = False }
-
-                   result <- runParser f
-                         
-                   state <- get
-                   put $! state { pstIsEOFFatal = isEOFFatal }
-
-                   return result
-
-
-satisfy :: (Char -> Bool) -> Parser Char
-satisfy !f
-    = do c <- anyChar
-         if f c then
-             return c
-           else
-             failP
-
-
-char :: Char -> Parser Char
-char !c = satisfy (== c)
-
-
-string :: String -> Parser String
-string !str
-    = let bs  = B.pack str
-          len = B.length bs
-      in
-        Parser $!
-        do st <- get
-           let (bs', rest) = B.splitAt len $ pstInput st
-               st'         = st { pstInput = rest }
-           if B.length bs' < len then
-               return ReachedEOF
-             else
-               if bs == bs' then
-                   do put st'
-                      return $ Success str
-               else
-                   return IllegalInput
-
-
-infixr 0 <|>
-
--- |This is the backtracking alternation. There is no non-backtracking
--- equivalent.
-(<|>) :: Parser a -> Parser a -> Parser a
-(!f) <|> (!g)
-    = Parser $! do saved  <- get -- 状態を保存
-                   result <- runParser f
-                   case result of
-                     Success a    -> return $! Success a
-                     IllegalInput -> do put saved -- 状態を復歸
-                                        runParser g
-                     ReachedEOF   -> if pstIsEOFFatal saved then
-                                         do put saved
-                                            return ReachedEOF
-                                     else
-                                         do put saved
-                                            runParser g
-
-
-choice :: [Parser a] -> Parser a
-choice = foldl (<|>) failP
-
-
-oneOf :: [Char] -> Parser Char
-oneOf = foldl (<|>) failP . map char
-
-
-notFollowedBy :: Parser a -> Parser ()
-notFollowedBy !p
-    = Parser $! do saved  <- get -- 状態を保存
-                   result <- runParser p
-                   case result of
-                     Success _    -> do put saved -- 状態を復歸
-                                        return IllegalInput
-                     IllegalInput -> do put saved -- 状態を復歸
-                                        return $! Success ()
-                     ReachedEOF   -> do put saved -- 状態を復歸
-                                        return $! Success ()
-
-
-digit :: Parser Char
-digit = do c <- anyChar
-           if c >= '0' && c <= '9' then
-               return c
-             else
-               failP
-
-
-hexDigit :: Parser Char
-hexDigit = do c <- anyChar
-              if (c >= '0' && c <= '9') ||
-                 (c >= 'a' && c <= 'f') ||
-                 (c >= 'A' && c <= 'F') then
-                  return c
-                else
-                  failP
-
-
-many :: forall a. Parser a -> Parser [a]
-many !p = Parser $!
-          do state <- get
-             let (# result, state' #) = many' state Seq.empty
-             put state'
-             return result
-    where
-      many' :: ParserState -> Seq a -> (# ParserResult [a], ParserState #)
-      many' !st !soFar
-          = case runState (runParser p) st of
-              (Success a,  st') -> many' st' (soFar |> a)
-              (IllegalInput, _) -> (# Success (Fold.toList soFar), st #)
-              (ReachedEOF  , _) -> if pstIsEOFFatal st then
-                                       (# ReachedEOF, st #)
-                                   else
-                                       (# Success (Fold.toList soFar), st #)
-
-manyChar :: Parser Char -> Parser Lazy.ByteString
-manyChar !p = Parser $!
-              do state <- get
-                 case scan' state 0 of
-                   Success len
-                       -> do let (bs, rest) = B.splitAt len (pstInput state)
-                                 state'     = state { pstInput = rest }
-                             put state'
-                             return $ Success bs
-                   ReachedEOF
-                       -> if pstIsEOFFatal state then
-                              return ReachedEOF
-                          else
-                              error "internal error"
-                   _   -> error "internal error"
-    where
-      scan' :: ParserState -> Int64 -> ParserResult Int64
-      scan' !st !soFar
-          = case runState (runParser p) st of
-              (Success _   , st') -> scan' st' (soFar + 1)
-              (IllegalInput, _  ) -> Success soFar
-              (ReachedEOF  , _  ) -> if pstIsEOFFatal st then
-                                         ReachedEOF
-                                     else
-                                         Success soFar
-
-
-many1 :: Parser a -> Parser [a]
-many1 !p = do x  <- p
-              xs <- many p
-              return (x:xs)
-
-
-count :: Int -> Parser a -> Parser [a]
-count !n !p = Parser $! count' n p Seq.empty
-
--- This implementation is rather ugly but we need to make it
--- tail-recursive to avoid stack overflow.
-count' :: Int -> Parser a -> Seq a -> State ParserState (ParserResult [a])
-count' 0  _  !soFar = return $! Success $! Fold.toList soFar
-count' !n !p !soFar = do saved  <- get
-                         result <- runParser p
-                         case result of
-                           Success a    -> count' (n-1) p (soFar |> a)
-                           IllegalInput -> do put saved
-                                              return IllegalInput
-                           ReachedEOF   -> do put saved
-                                              return ReachedEOF
-
-
--- def may be a _|_
-option :: a -> Parser a -> Parser a
-option def !p = p <|> return def
-
-
-sepBy :: Parser a -> Parser sep -> Parser [a]
-sepBy !p !sep = sepBy1 p sep <|> return []
-
-
-sepBy1 :: Parser a -> Parser sep -> Parser [a]
-sepBy1 !p !sep
-    = do x  <- p
-         xs <- many $! sep >> p
-         return (x:xs)
-
-
-sp :: Parser Char
-sp = char ' '
-
-
-ht :: Parser Char
-ht = char '\t'
-
-
-crlf :: Parser String
-crlf = string "\x0d\x0a"
+import Control.Applicative
+import Control.Applicative.Unicode
+
+-- |@'atMost' n v@ is like @'P.many' v@ but accumulates @v@ at most
+-- @n@ times.
+atMost ∷ Alternative f ⇒ Int → f a → f [a]
+{-# INLINE atMost #-}
+atMost 0 _ = pure []
+atMost n v = ( (:) <$> v ⊛ atMost (n-1) v )
+             <|>
+             pure []
index fe54bde4c5d9f08b10ce443dd029f6d5bd838aa2..72d8ca1721ae7908a94cd1b2445eff1aaa16e1c8 100644 (file)
@@ -1,5 +1,5 @@
 {-# LANGUAGE
-    BangPatterns
+    OverloadedStrings
   , UnicodeSyntax
   #-}
 -- |This is an auxiliary parser utilities for parsing things related
 -- In general you don't have to use this module directly.
 module Network.HTTP.Lucu.Parser.Http
     ( isCtl
+    , isText
     , isSeparator
     , isChar
     , isToken
+    , isSPHT
+
     , listOf
-    , token
+
+    , crlf
+    , sp
     , lws
-    , text
-    , separator
+
+    , token
+    , separators
     , quotedStr
     , qvalue
     )
     where
+import Control.Applicative
+import Control.Monad
+import Data.Ascii (Ascii)
+import qualified Data.Ascii as A
+import Data.Attoparsec.Char8 as P hiding (scan)
+import qualified Data.Attoparsec.FastSet as FS
+import qualified Data.ByteString.Char8 as BS
+import Network.HTTP.Lucu.Parser
+import Prelude.Unicode
 
-import           Network.HTTP.Lucu.Parser
-
--- |@'isCtl' c@ is 'Prelude.False' iff @0x20 <= @c@ < 0x7F@.
-isCtl :: Char -> Bool
+-- |@'isCtl' c@ returns 'False' iff @0x20 <= c < 0x7F@.
+isCtl ∷ Char → Bool
+{-# INLINE isCtl #-}
 isCtl c
-    | c <  '\x1f' = True
-    | c >= '\x7f' = True
-    | otherwise   = False
+    | c ≤ '\x1f' = True
+    | c > '\x7f' = True
+    | otherwise  = False
+
+-- |@'isText'@ is equivalent to @'not' '.' 'isCtl'@
+isText ∷ Char → Bool
+{-# INLINE isText #-}
+isText = (¬) ∘ isCtl
 
--- |@'isSeparator' c@ is 'Prelude.True' iff c is one of HTTP
+-- |@'isSeparator' c@ returns 'True' iff c is one of the HTTP
 -- separators.
-isSeparator :: Char -> Bool
-isSeparator '('  = True
-isSeparator ')'  = True
-isSeparator '<'  = True
-isSeparator '>'  = True
-isSeparator '@'  = True
-isSeparator ','  = True
-isSeparator ';'  = True
-isSeparator ':'  = True
-isSeparator '\\' = True
-isSeparator '"'  = True
-isSeparator '/'  = True
-isSeparator '['  = True
-isSeparator ']'  = True
-isSeparator '?'  = True
-isSeparator '='  = True
-isSeparator '{'  = True
-isSeparator '}'  = True
-isSeparator ' '  = True
-isSeparator '\t' = True
-isSeparator _    = False
-
--- |@'isChar' c@ is 'Prelude.True' iff @c <= 0x7f@.
-isChar :: Char -> Bool
-isChar c
-    | c <= '\x7f' = True
-    | otherwise   = False
-
--- |@'isToken' c@ is equivalent to @not ('isCtl' c || 'isSeparator'
+isSeparator ∷ Char → Bool
+{-# INLINE isSeparator #-}
+isSeparator = flip FS.memberChar set
+    where
+      {-# NOINLINE set #-}
+      set = FS.charClass "()<>@,;:\\\"/[]?={}\x20\x09"
+
+-- |@'isChar' c@ returns 'True' iff @c <= 0x7f@.
+isChar ∷ Char → Bool
+{-# INLINE isChar #-}
+isChar = (≤ '\x7F')
+
+-- |@'isToken' c@ is equivalent to @not ('isCtl' c '||' 'isSeparator'
 -- c)@
-isToken :: Char -> Bool
-isToken c = c `seq`
-            not (isCtl c || isSeparator c)
-
--- |@'listOf' p@ is similar to @'Network.HTTP.Lucu.Parser.sepBy' p
--- ('Network.HTTP.Lucu.Parser.char' \',\')@ but it allows any
--- occurrences of LWS before and after each tokens.
-listOf :: Parser a -> Parser [a]
-listOf !p = do _ <- many lws
-               sepBy p $! do _ <- many lws
-                             _ <- char ','
-                             many lws
-
--- |'token' is equivalent to @'Network.HTTP.Lucu.Parser.many1' $
--- 'Network.HTTP.Lucu.Parser.satisfy' 'isToken'@
-token :: Parser String
-token = many1 $! satisfy isToken
-
--- |'lws' is an HTTP LWS: @'Network.HTTP.Lucu.Parser.crlf'?
--- ('Network.HTTP.Lucu.Parser.sp' | 'Network.HTTP.Lucu.Parser.ht')+@
-lws :: Parser String
-lws = do s  <- option "" crlf
-         xs <- many1 (sp <|> ht)
-         return (s ++ xs)
-
--- |'text' accepts one character which doesn't satisfy 'isCtl'.
-text :: Parser Char
-text = satisfy (not . isCtl)
-
--- |'separator' accepts one character which satisfies 'isSeparator'.
-separator :: Parser Char
-separator = satisfy isSeparator
+isToken ∷ Char → Bool
+{-# INLINE isToken #-}
+isToken c = (¬) (isCtl c ∨ isSeparator c)
+
+-- |@'listOf' p@ is similar to @'sepBy' p ('char' \',\')@ but it
+-- allows any occurrences of 'lws' before and after each tokens.
+listOf ∷ Parser a → Parser [a]
+{-# INLINEABLE listOf #-}
+listOf p
+    = do skipMany lws
+         p `sepBy` do skipMany lws
+                      void $ char ','
+                      skipMany lws
+      <?>
+      "listOf"
+
+-- |'token' is almost the same as @'takeWhile1' 'isToken'@
+token ∷ Parser Ascii
+{-# INLINE token #-}
+token = (A.unsafeFromByteString <$> takeWhile1 isToken)
+        <?>
+        "token"
+
+-- |The CRLF: 0x0D 0x0A.
+crlf ∷ Parser ()
+{-# INLINE crlf #-}
+crlf = (string "\x0D\x0A" *> return ())
+       <?>
+       "crlf"
+
+-- |The SP: 0x20.
+sp ∷ Parser ()
+{-# INLINE sp #-}
+sp = char '\x20' *> return ()
+
+-- |HTTP LWS: crlf? (sp | ht)+
+lws ∷ Parser ()
+{-# INLINEABLE lws #-}
+lws = (option () crlf *> void (takeWhile1 isSPHT))
+      <?>
+      "lws"
+
+-- |Returns 'True' for SP and HT.
+isSPHT ∷ Char → Bool
+{-# INLINE isSPHT #-}
+isSPHT '\x20' = True
+isSPHT '\x09' = True
+isSPHT _      = False
+
+-- |@'separators'@ is almost the same as @'takeWhile1' 'isSeparator'@.
+separators ∷ Parser Ascii
+{-# INLINE separators #-}
+separators = (A.unsafeFromByteString <$> takeWhile1 isSeparator)
+             <?>
+             "separators"
 
 -- |'quotedStr' accepts a string surrounded by double quotation
 -- marks. Quotes can be escaped by backslashes.
-quotedStr :: Parser String
-quotedStr = do _  <- char '"'
-               xs <- many (qdtext <|> quotedPair)
-               _  <- char '"'
-               return $ foldr (++) "" xs
+quotedStr ∷ Parser Ascii
+{-# INLINEABLE quotedStr #-}
+quotedStr = do void $ char '"'
+               xs ← P.many (qdtext <|> quotedPair)
+               void $ char '"'
+               return $ A.unsafeFromByteString $ BS.pack xs
+            <?>
+            "quotedStr"
     where
-      qdtext = do c <- satisfy (/= '"')
-                  return [c]
+      qdtext ∷ Parser Char
+      {-# INLINE qdtext #-}
+      qdtext = satisfy (\c → c ≢ '"' ∧ (¬) (isCtl c))
+               <?>
+               "qdtext"
 
-      quotedPair = do _ <- char '\\'
-                      c <- satisfy isChar
-                      return [c]
+      quotedPair ∷ Parser Char
+      {-# INLINE quotedPair #-}
+      quotedPair = (char '\\' *> satisfy isChar)
+                   <?>
+                   "quotedPair"
 
 -- |'qvalue' accepts a so-called qvalue.
-qvalue :: Parser Double
-qvalue = do x  <- char '0'
-            xs <- option ""
-                  $ do y  <- char '.'
-                       ys <- many digit -- 本當は三文字までに制限
-                       return (y:ys)
-            return $ read (x:xs)
-         <|>
-         do x  <- char '1'
-            xs <- option ""
-                  $ do y  <- char '.'
-                       ys <- many (char '0') -- 本當は三文字までに制限
-                       return (y:ys)
-            return $ read (x:xs)
+qvalue ∷ Parser Double
+{-# INLINEABLE qvalue #-}
+qvalue = ( do x  ← char '0'
+              xs ← option "" $
+                   do y  ← char '.'
+                      ys ← atMost 3 digit
+                      return (y:ys)
+              return $ read (x:xs)
+           <|>
+           do x  ← char '1'
+              xs ← option "" $
+                   do y  ← char '.'
+                      ys ← atMost 3 (char '0')
+                      return (y:ys)
+              return $ read (x:xs)
+         )
+         <?>
+         "qvalue"
index 806ed1c1c9d07529ec3e84e65b367d69d1d881dd..6735652d6a5656410c6cc5ebfcdc922c11184761 100644 (file)
 {-# LANGUAGE
-    BangPatterns
+    DoAndIfThenElse
+  , OverloadedStrings
+  , RecordWildCards
   , UnicodeSyntax
   #-}
 module Network.HTTP.Lucu.Postprocess
     ( postprocess
-    , completeUnconditionalHeaders
     )
     where
-
-import           Control.Concurrent.STM
-import           Control.Monad
-import qualified Data.ByteString as Strict (ByteString)
-import qualified Data.ByteString.Char8 as C8 hiding (ByteString)
-import           Data.IORef
-import           Data.Maybe
-import           Data.Time
-import qualified Data.Time.HTTP as HTTP
-import           GHC.Conc (unsafeIOToSTM)
-import           Network.HTTP.Lucu.Abortion
-import           Network.HTTP.Lucu.Config
-import           Network.HTTP.Lucu.Headers
-import           Network.HTTP.Lucu.HttpVersion
-import           Network.HTTP.Lucu.Interaction
-import           Network.HTTP.Lucu.Request
-import           Network.HTTP.Lucu.Response
-import           System.IO.Unsafe
-
-{-
-  
-  * Response が未設定なら、200 OK にする。
-
-  * ステータスが 2xx, 3xx, 4xx, 5xx のいずれでもなければ 500 にする。
-
-  * 405 Method Not Allowed なのに Allow ヘッダが無ければ 500 にする。
-
-  * 304 Not Modified 以外の 3xx なのに Location ヘッダが無ければ 500 に
-    する。
-
-  * Content-Length があれば、それを削除する。Transfer-Encoding があって
-    も削除する。
-
-  * HTTP/1.1 であり、body を持つ事が出來る時、Transfer-Encoding を
-    chunked に設定する。
-
-  * body を持つ事が出來る時、Content-Type が無ければデフォルト値にする。
-    出來ない時、HEAD でなければContent-Type, Etag, Last-Modified を削除
-    する。
-
-  * body を持つ事が出來ない時、body 破棄フラグを立てる。
-
-  * Connection: close が設定されてゐる時、切斷フラグを立てる。
-
-  * 切斷フラグが立ってゐる時、Connection: close を設定する。
-
-  * Server が無ければ設定。
-
-  * Date が無ければ設定。
-
--}
-
-postprocess :: Interaction -> STM ()
-postprocess !itr
-    = do reqM <- readItr itr itrRequest id
-         res  <- readItr itr itrResponse id
-         let sc = resStatus res
-
-         unless (any (\ p -> p sc) [isSuccessful, isRedirection, isError])
-                  $ abortSTM InternalServerError []
-                        $ Just ("The status code is not good for a final status: "
-                                ++ show sc)
-
-         when (sc == MethodNotAllowed && getHeader (C8.pack "Allow") res == Nothing)
-                  $ abortSTM InternalServerError []
-                        $ Just ("The status was " ++ show sc ++ " but no Allow header.")
-
-         when (sc /= NotModified && isRedirection sc && getHeader (C8.pack "Location") res == Nothing)
-                  $ abortSTM InternalServerError []
-                        $ Just ("The status code was " ++ show sc ++ " but no Location header.")
-
-         when (reqM /= Nothing) relyOnRequest
-
-         -- itrResponse の内容は relyOnRequest によって變へられてゐる可
-         -- 能性が高い。
-         do oldRes <- readItr itr itrResponse id
-            newRes <- unsafeIOToSTM
-                      $ completeUnconditionalHeaders (itrConfig itr) oldRes
-            writeItr itr itrResponse newRes
-    where
-      relyOnRequest :: STM ()
-      relyOnRequest
-          = do status <- readItr itr itrResponse resStatus
-               req    <- readItr itr itrRequest fromJust
-
-               let reqVer      = reqVersion req
-                   canHaveBody = if reqMethod req == HEAD then
-                                     False
-                                 else
-                                     not (isInformational status ||
-                                          status == NoContent    ||
-                                          status == ResetContent ||
-                                          status == NotModified    )
-
-               updateRes $! deleteHeader (C8.pack "Content-Length")
-               updateRes $! deleteHeader (C8.pack "Transfer-Encoding")
-
-               cType <- readHeader (C8.pack "Content-Type")
-               when (cType == Nothing)
-                        $ updateRes $ setHeader (C8.pack "Content-Type") defaultPageContentType
-
-               if canHaveBody then
-                   when (reqVer == HttpVersion 1 1)
-                            $ do updateRes $! setHeader (C8.pack "Transfer-Encoding") (C8.pack "chunked")
-                                 writeItr itr itrWillChunkBody True
-                 else
-                   -- body 関連のヘッダを削除。但し HEAD なら Content-* は殘す
-                   when (reqMethod req /= HEAD)
-                            $ do updateRes $! deleteHeader (C8.pack "Content-Type")
-                                 updateRes $! deleteHeader (C8.pack "Etag")
-                                 updateRes $! deleteHeader (C8.pack "Last-Modified")
-
-               conn <- readHeader (C8.pack "Connection")
-               case conn of
-                 Nothing    -> return ()
-                 Just value -> when (value `noCaseEq` C8.pack "close")
-                                   $ writeItr itr itrWillClose True
-
-               willClose <- readItr itr itrWillClose id
-               when willClose
-                        $ updateRes $! setHeader (C8.pack "Connection") (C8.pack "close")
-
-               when (reqMethod req == HEAD || not canHaveBody)
-                        $ writeTVar (itrWillDiscardBody itr) True
-
-      readHeader :: Strict.ByteString -> STM (Maybe Strict.ByteString)
-      readHeader !name
-          = readItr itr itrResponse $ getHeader name
-
-      updateRes :: (Response -> Response) -> STM ()
-      updateRes !updator 
-          = updateItr itr itrResponse updator
-
-
-completeUnconditionalHeaders :: Config -> Response -> IO Response
-completeUnconditionalHeaders !conf !res
-    = compServer res >>= compDate
-      where
-        compServer res'
-            = case getHeader (C8.pack "Server") res' of
-                Nothing -> return $ setHeader (C8.pack "Server") (cnfServerSoftware conf) res'
-                Just _  -> return res'
-
-        compDate res'
-            = case getHeader (C8.pack "Date") res' of
-                Nothing -> do date <- getCurrentDate
-                              return $ setHeader (C8.pack "Date") date res'
-                Just _  -> return res'
-
-
-cache :: IORef (UTCTime, Strict.ByteString)
-cache = unsafePerformIO $
-        newIORef (UTCTime (ModifiedJulianDay 0) 0, undefined)
-{-# NOINLINE cache #-}
-
-getCurrentDate :: IO Strict.ByteString
-getCurrentDate = do now                     <- getCurrentTime
-                    (cachedTime, cachedStr) <- readIORef cache
-
-                    if now `mostlyEq` cachedTime then
-                        return cachedStr
-                      else
-                        do let dateStr = C8.pack $ HTTP.format now
-                           writeIORef cache (now, dateStr)
-                           return dateStr
+import Control.Applicative
+import Control.Concurrent.STM
+import Control.Monad
+import Control.Monad.Unicode
+import Data.Ascii (Ascii, CIAscii, AsciiBuilder)
+import qualified Data.Ascii as A
+import Data.Maybe
+import Data.Monoid.Unicode
+import GHC.Conc (unsafeIOToSTM)
+import Network.HTTP.Lucu.Abortion
+import Network.HTTP.Lucu.Config
+import Network.HTTP.Lucu.DefaultPage
+import Network.HTTP.Lucu.Headers
+import Network.HTTP.Lucu.Interaction
+import Network.HTTP.Lucu.Request
+import Network.HTTP.Lucu.Response
+import Prelude.Unicode
+
+postprocess ∷ NormalInteraction → STM ()
+postprocess ni@(NI {..})
+    = do void $ tryPutTMVar niSendContinue False
+         abortOnCertainConditions ni
+         postprocessWithRequest ni
+         completeUnconditionalHeaders ni
+
+abortOnCertainConditions ∷ NormalInteraction → STM ()
+abortOnCertainConditions (NI {..})
+    = readTVar niResponse ≫= go
     where
-      mostlyEq :: UTCTime -> UTCTime -> Bool
-      mostlyEq a b
-          = (utctDay a == utctDay b)
-            &&
-            (fromEnum (utctDayTime a) == fromEnum (utctDayTime b))
+      go ∷ Response → STM ()
+      go res@(Response {..})
+          = do unless (any (\ p → p resStatus) [ isSuccessful
+                                               , isRedirection
+                                               , isError
+                                               ])
+                   $ abort'
+                   $ A.toAsciiBuilder "Inappropriate status code for a response: "
+                   ⊕ printStatusCode resStatus
+
+               when ( resStatus ≡ MethodNotAllowed ∧
+                      hasHeader "Allow" res        )
+                   $ abort'
+                   $ A.toAsciiBuilder "The status was "
+                   ⊕ printStatusCode resStatus
+                   ⊕ A.toAsciiBuilder " but no \"Allow\" header."
+
+               when ( resStatus ≢ NotModified  ∧
+                      isRedirection resStatus ∧
+                      hasHeader "Location" res )
+                   $ abort'
+                   $ A.toAsciiBuilder "The status code was "
+                   ⊕ printStatusCode resStatus
+                   ⊕ A.toAsciiBuilder " but no Location header."
+
+      abort' ∷ AsciiBuilder → STM ()
+      abort' = throwSTM
+               ∘ mkAbortion' InternalServerError
+               ∘ A.toText
+               ∘ A.fromAsciiBuilder
+
+postprocessWithRequest ∷ NormalInteraction → STM ()
+postprocessWithRequest ni@(NI {..})
+    = do willDiscardBody ← readTVar niWillDiscardBody
+         canHaveBody     ← if willDiscardBody then
+                               return False
+                           else
+                               resCanHaveBody <$> readTVar niResponse
+
+         updateRes ni
+             $ deleteHeader "Content-Length"
+             ∘ deleteHeader "Transfer-Encoding"
+
+         if canHaveBody then
+             do when niWillChunkBody $
+                    writeHeader ni "Transfer-Encoding" (Just "chunked")
+                writeDefaultPageIfNeeded ni
+         else
+             do writeTVar niWillDiscardBody True
+                -- These headers make sense for HEAD requests even
+                -- when there won't be a response entity body.
+                when (reqMethod niRequest ≢ HEAD)
+                    $ updateRes ni
+                    $ deleteHeader "Content-Type"
+                    ∘ deleteHeader "Etag"
+                    ∘ deleteHeader "Last-Modified"
+
+         hasConnClose ← (≡ Just "close") <$> readCIHeader ni "Connection"
+         willClose    ← readTVar niWillClose
+         when (hasConnClose ∧ (¬) willClose)
+             $ writeTVar niWillClose True
+         when ((¬) hasConnClose ∧ willClose)
+             $ writeHeader ni "Connection" (Just "close")
+
+writeDefaultPageIfNeeded ∷ NormalInteraction → STM ()
+writeDefaultPageIfNeeded ni@(NI {..})
+    = do resHasCType ← readTVar niResponseHasCType
+         unless resHasCType
+             $ do writeHeader ni "Content-Type" $ Just defaultPageContentType
+                  writeHeader ni "Content-Encoding" Nothing
+                  res ← readTVar niResponse
+                  let body = getDefaultPage niConfig (Just niRequest) res
+                  putTMVar niBodyToSend body
+
+completeUnconditionalHeaders ∷ NormalInteraction → STM ()
+completeUnconditionalHeaders ni@(NI {..})
+    = do srv ← readHeader ni "Server"
+         when (isNothing srv) $
+             writeHeader ni "Server" $ Just $ cnfServerSoftware niConfig
+
+         date ← readHeader ni "Date"
+         when (isNothing date) $
+             do date' ← unsafeIOToSTM getCurrentDate
+                writeHeader ni "Date" $ Just date'
+
+writeHeader ∷ NormalInteraction → CIAscii → Maybe Ascii → STM ()
+{-# INLINE writeHeader #-}
+writeHeader ni k v
+    = case v of
+        Just v' → updateRes ni $ setHeader    k v'
+        Nothing → updateRes ni $ deleteHeader k
+
+readHeader ∷ NormalInteraction → CIAscii → STM (Maybe Ascii)
+{-# INLINE readHeader #-}
+readHeader (NI {..}) k
+    = getHeader k <$> readTVar niResponse
+
+readCIHeader ∷ NormalInteraction → CIAscii → STM (Maybe CIAscii)
+{-# INLINE readCIHeader #-}
+readCIHeader (NI {..}) k
+    = getCIHeader k <$> readTVar niResponse
+
+updateRes ∷ NormalInteraction → (Response → Response) → STM ()
+{-# INLINE updateRes #-}
+updateRes (NI {..}) f
+    = do old ← readTVar niResponse
+         writeTVar niResponse $ f old
index 9f9fa0d68c3b83f187c6316213cc100f39cdc5cf..26fbd53546a2412a90d40f5f30c234620ab6890d 100644 (file)
 {-# LANGUAGE
-    BangPatterns
+    DoAndIfThenElse
+  , OverloadedStrings
+  , RecordWildCards
+  , UnicodeSyntax
   #-}
 module Network.HTTP.Lucu.Preprocess
-    ( preprocess
+    ( AugmentedRequest(..)
+    , RequestBodyLength(..)
+    , preprocess
     )
     where
-
-import           Control.Concurrent.STM
-import           Control.Monad
-import qualified Data.ByteString as Strict (ByteString)
-import qualified Data.ByteString.Char8 as C8 hiding (ByteString)
-import           Data.Char
-import           Data.Maybe
-import           Network.HTTP.Lucu.Config
-import           Network.HTTP.Lucu.Headers
-import           Network.HTTP.Lucu.HttpVersion
-import           Network.HTTP.Lucu.Interaction
-import           Network.HTTP.Lucu.Request
-import           Network.HTTP.Lucu.Response
-import           Network.URI
-
-{-
-
-  * URI にホスト名が存在しない時、
-    [1] HTTP/1.0 ならば Config を使って補完
-    [2] HTTP/1.1 ならば Host ヘッダで補完。Host が無ければ 400。
-
-  * Expect: に問題があった場合は 417 Expectation Failed に設定。
-    100-continue 以外のものは全部 417 に。
-
-  * Transfer-Encoding: に問題があったら 501 Not Implemented にする。具
-    体的には、identity でも chunked でもなければ 501 Not Implemented に
-    する。
-
-  * メソッドが GET, HEAD, POST, PUT, DELETE の何れでもない場合は 501
-    Not Implemented にする。
-
-  * HTTP/1.0 でも HTTP/1.1 でもないリクエストに對しては 505 HTTP
-    Version Not Supported を返す。
-
-  * POST または PUT に Content-Length も Transfer-Encoding も無い時は、
-    411 Length Required にする。
-
-  * Content-Length の値が數値でなかったり負だったりしたら 400 Bad
-    Request にする。
-
-  * willDiscardBody その他の變數を設定する。
-
--}
-
-preprocess :: Interaction -> STM ()
-preprocess !itr
-    = do req <- readItr itr itrRequest fromJust
-
-         let reqVer = reqVersion req
-
-         if reqVer /= HttpVersion 1 0 &&
-            reqVer /= HttpVersion 1 1 then
-
-             do setStatus HttpVersionNotSupported
-                writeItr itr itrWillClose True
-
-           else
-             -- HTTP/1.0 では Keep-Alive できない
-             do when (reqVer == HttpVersion 1 0)
-                     $ writeItr itr itrWillClose True
-
-                -- ホスト部の補完
-                completeAuthority req
-
-                case reqMethod req of
-                  GET    -> return ()
-                  HEAD   -> writeItr itr itrWillDiscardBody True
-                  POST   -> writeItr itr itrRequestHasBody True
-                  PUT    -> writeItr itr itrRequestHasBody True
-                  DELETE -> return ()
-                  _      -> setStatus NotImplemented
-                  
-                preprocessHeader req
+import Control.Applicative
+import Control.Monad
+import Control.Monad.State.Strict
+import Data.Ascii (Ascii)
+import qualified Data.Ascii as A
+import qualified Data.ByteString.Char8 as C8
+import Data.Maybe
+import Data.Text (Text)
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import Network.HTTP.Lucu.Headers
+import Network.HTTP.Lucu.HttpVersion
+import Network.HTTP.Lucu.Request
+import Network.HTTP.Lucu.Response
+import Network.Socket
+import Network.URI
+import Prelude.Unicode
+
+data AugmentedRequest
+    = AugmentedRequest {
+        arRequest          ∷ !Request
+      , arInitialStatus    ∷ !StatusCode
+      , arWillChunkBody    ∷ !Bool
+      , arWillDiscardBody  ∷ !Bool
+      , arWillClose        ∷ !Bool
+      , arExpectedContinue ∷ !Bool
+      , arReqBodyLength    ∷ !(Maybe RequestBodyLength)
+      }
+
+data RequestBodyLength
+    = Fixed !Int
+    | Chunked
+    deriving (Eq, Show)
+
+preprocess ∷ Text → PortNumber → Request → AugmentedRequest
+preprocess localHost localPort req@(Request {..})
+    = execState go initialAR
     where
-      setStatus :: StatusCode -> STM ()
-      setStatus !status
-          = updateItr itr itrResponse
-            $! \ res -> res {
-                          resStatus = status
-                        }
-
-      completeAuthority :: Request -> STM ()
-      completeAuthority !req
-          = when (uriAuthority (reqURI req) == Nothing)
-            $ if reqVersion req == HttpVersion 1 0 then
-                  -- HTTP/1.0 なので Config から補完
-                  do let conf = itrConfig itr
-                         host = cnfServerHost conf
-                         port = itrLocalPort itr
-                         portStr
-                              = case port of
-                                  80 -> ""
-                                  n  -> ':' : show n
-                     updateAuthority host (C8.pack portStr)
-              else
-                  case getHeader (C8.pack "Host") req of
-                    Just str -> let (host, portStr) = parseHost str
-                                in updateAuthority host portStr
-                    Nothing  -> setStatus BadRequest
-
-
-      parseHost :: Strict.ByteString -> (Strict.ByteString, Strict.ByteString)
-      parseHost = C8.break (== ':')
-
-
-      updateAuthority :: Strict.ByteString -> Strict.ByteString -> STM ()
-      updateAuthority !host !portStr
-          = updateItr itr itrRequest
-            $! \ (Just req) -> Just req {
-                                 reqURI = let uri = reqURI req
-                                          in uri {
-                                               uriAuthority = Just URIAuth {
-                                                                   uriUserInfo = ""
-                                                                 , uriRegName  = C8.unpack host
-                                                                 , uriPort     = C8.unpack portStr
-                                                              }
-                                             }
-                               }
-                
-
-      preprocessHeader :: Request -> STM ()
-      preprocessHeader !req
-          = do case getHeader (C8.pack "Expect") req of
-                 Nothing    -> return ()
-                 Just value -> if value `noCaseEq` C8.pack "100-continue" then
-                                   writeItr itr itrExpectedContinue True
-                               else
-                                   setStatus ExpectationFailed
-
-               case getHeader (C8.pack "Transfer-Encoding") req of
-                 Nothing    -> return ()
-                 Just value -> unless (value `noCaseEq` C8.pack "identity")
-                                   $ if value `noCaseEq` C8.pack "chunked" then
-                                         writeItr itr itrRequestIsChunked True
-                                     else
-                                         setStatus NotImplemented
-
-               case getHeader (C8.pack "Content-Length") req of
-                 Nothing    -> return ()
-                 Just value -> if C8.all isDigit value then
-                                   do let Just (len, _) = C8.readInt value
-                                      writeItr itr itrReqChunkLength    $ Just len
-                                      writeItr itr itrReqChunkRemaining $ Just len
-                               else
-                                   setStatus BadRequest
-
-               case getHeader (C8.pack "Connection") req of
-                 Nothing    -> return ()
-                 Just value -> when (value `noCaseEq` C8.pack "close")
-                                   $ writeItr itr itrWillClose True
+      initialAR ∷ AugmentedRequest
+      initialAR = AugmentedRequest {
+                    arRequest          = req
+                  , arInitialStatus    = Ok
+                  , arWillChunkBody    = False
+                  , arWillDiscardBody  = False
+                  , arWillClose        = False
+                  , arExpectedContinue = False
+                  , arReqBodyLength    = Nothing
+                  }
+      go ∷ State AugmentedRequest ()
+      go = do examineHttpVersion
+              examineMethod
+              examineAuthority localHost localPort
+              examineHeaders
+              examineBodyLength
+
+setRequest ∷ Request → State AugmentedRequest ()
+setRequest req
+    = modify $ \ar → ar { arRequest = req }
+
+setStatus ∷ StatusCode → State AugmentedRequest ()
+setStatus sc
+    = modify $ \ar → ar { arInitialStatus = sc }
+
+setWillClose ∷ Bool → State AugmentedRequest ()
+setWillClose b
+    = modify $ \ar → ar { arWillClose = b }
+
+setBodyLength ∷ Maybe RequestBodyLength → State AugmentedRequest ()
+setBodyLength len
+    = modify $ \ar → ar { arReqBodyLength = len }
+
+examineHttpVersion ∷ State AugmentedRequest ()
+examineHttpVersion
+    = do req ← gets arRequest
+         case reqVersion req of
+           -- HTTP/1.0 requests can't Keep-Alive.
+           HttpVersion 1 0
+               → setWillClose True
+           HttpVersion 1 1
+               → modify $ \ar → ar { arWillChunkBody = True }
+           _   → do setStatus    HttpVersionNotSupported
+                    setWillClose True
+
+examineMethod ∷ State AugmentedRequest ()
+examineMethod
+    = do req ← gets arRequest
+         case reqMethod req of
+           GET    → return ()
+           HEAD   → modify $ \ar → ar { arWillDiscardBody = True }
+           POST   → return ()
+           PUT    → return ()
+           DELETE → return ()
+           _      → setStatus NotImplemented
+
+examineAuthority ∷ Text → PortNumber → State AugmentedRequest ()
+examineAuthority localHost localPort
+    = do req ← gets arRequest
+         when (isNothing $ uriAuthority $ reqURI req) $
+             case reqVersion req of
+               -- HTTP/1.0 requests have no Host header so complete it
+               -- with the configuration value.
+               HttpVersion 1 0
+                   → let host = localHost
+                         port = case localPort of
+                                  80 → ""
+                                  n  → A.unsafeFromString $ ':':show n
+                         req' = updateAuthority host port req
+                     in
+                       setRequest req'
+               -- HTTP/1.1 requests MUST have a Host header.
+               HttpVersion 1 1
+                   → case getHeader "Host" req of
+                        Just str
+                            → let (host, port)
+                                       = parseHost str
+                                  req' = updateAuthority host port req
+                              in
+                                setRequest req'
+                        Nothing
+                            → setStatus BadRequest
+               -- Should never reach here...
+               ver → fail ("internal error: unknown version: " ⧺ show ver)
+
+parseHost ∷ Ascii → (Text, Ascii)
+parseHost hp
+    = let (h, p) = C8.break (≡ ':') $ A.toByteString hp
+          -- FIXME: should decode punycode here.
+          hText  = T.decodeUtf8 h
+          pAscii = A.unsafeFromByteString p
+      in
+        (hText, pAscii)
+
+updateAuthority ∷ Text → Ascii → Request → Request
+updateAuthority host port req
+    = let uri  = reqURI req
+          uri' = uri {
+                   uriAuthority = Just URIAuth {
+                                    uriUserInfo = ""
+                                  , uriRegName  = T.unpack host
+                                  , uriPort     = A.toString port
+                                  }
+                 }
+      in
+        req { reqURI = uri' }
+
+examineHeaders ∷ State AugmentedRequest ()
+examineHeaders
+    = do req ← gets arRequest
+
+         case getCIHeader "Expect" req of
+           Nothing → return ()
+           Just v
+               | v ≡ "100-continue"
+                   → modify $ \ar → ar { arExpectedContinue = True }
+               | otherwise
+                   → setStatus ExpectationFailed
+
+         case getCIHeader "Transfer-Encoding" req of
+           Nothing → return ()
+           Just v
+               | v ≡ "identity"
+                   → return ()
+               | v ≡ "chunked"
+                   → setBodyLength $ Just Chunked
+               | otherwise
+                   → setStatus NotImplemented
+
+         case A.toByteString <$> getHeader "Content-Length" req of
+           Nothing    → return ()
+           Just value → case C8.readInt value of
+                           Just (len, garbage)
+                               | C8.null garbage ∧ len ≥ 0
+                                   → setBodyLength $ Just $ Fixed len
+                           _       → setStatus BadRequest
+
+         case getCIHeader "Connection" req of
+           Just v
+               | v ≡ "close"
+                   → setWillClose True
+           _       → return ()
+
+examineBodyLength ∷ State AugmentedRequest ()
+examineBodyLength
+    = do req ← gets arRequest
+         len ← gets arReqBodyLength
+         if reqMustHaveBody req then
+             -- POST and PUT requests must have an entity body.
+             when (isNothing len)
+                 $ setStatus LengthRequired
+         else
+             -- Other requests must NOT have an entity body.
+             when (isJust len)
+                 $ setStatus BadRequest
diff --git a/Network/HTTP/Lucu/RFC2231.hs b/Network/HTTP/Lucu/RFC2231.hs
new file mode 100644 (file)
index 0000000..1046c5d
--- /dev/null
@@ -0,0 +1,321 @@
+{-# LANGUAGE
+    DoAndIfThenElse
+  , OverloadedStrings
+  , RecordWildCards
+  , UnicodeSyntax
+  #-}
+-- |Provide functionalities to encode/decode MIME parameter values in
+-- character sets other than US-ASCII. See:
+-- <http://www.faqs.org/rfcs/rfc2231.html>
+--
+-- You usually don't have to use this module directly.
+module Network.HTTP.Lucu.RFC2231
+    ( printMIMEParams
+    , mimeParams
+    )
+    where
+import Control.Applicative
+import Control.Monad hiding (mapM)
+import Control.Monad.Unicode
+import Data.Ascii (Ascii, CIAscii, AsciiBuilder)
+import qualified Data.Ascii as A
+import Data.Attoparsec.Char8 as P
+import Data.Bits
+import qualified Data.ByteString.Char8 as BS
+import Data.Char
+import Data.Foldable
+import Data.Map (Map)
+import qualified Data.Map as M
+import Data.Monoid.Unicode
+import Data.Sequence (Seq, ViewL(..))
+import qualified Data.Sequence as S
+import Data.Sequence.Unicode hiding ((∅))
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Text.Encoding
+import Data.Text.Encoding.Error
+import Data.Traversable
+import Data.Word
+import Network.HTTP.Lucu.Parser.Http
+import Network.HTTP.Lucu.Utils
+import Prelude hiding (concat, mapM, takeWhile)
+import Prelude.Unicode
+
+-- |Convert MIME parameter values to an 'AsciiBuilder'.
+printMIMEParams ∷ Map CIAscii Text → AsciiBuilder
+{-# INLINEABLE printMIMEParams #-}
+printMIMEParams m = M.foldlWithKey f (∅) m
+    -- THINKME: Use foldlWithKey' for newer Data.Map
+    where
+      f ∷ AsciiBuilder → CIAscii → Text → AsciiBuilder
+      {-# INLINE f #-}
+      f ab k v = ab ⊕ A.toAsciiBuilder "; " ⊕ printPair k v
+
+printPair ∷ CIAscii → Text → AsciiBuilder
+{-# INLINEABLE printPair #-}
+printPair name value
+    | T.any (> '\xFF') value
+        = printPairInUTF8 name value
+    | otherwise
+        = printPairInAscii name (A.unsafeFromByteString $ encodeUtf8 value)
+
+printPairInUTF8 ∷ CIAscii → Text → AsciiBuilder
+{-# INLINEABLE printPairInUTF8 #-}
+printPairInUTF8 name value
+    = A.toAsciiBuilder (A.fromCIAscii name) ⊕
+      A.toAsciiBuilder "*=utf-8''" ⊕
+      escapeUnsafeChars (encodeUtf8 value) (∅)
+
+printPairInAscii ∷ CIAscii → Ascii → AsciiBuilder
+{-# INLINEABLE printPairInAscii #-}
+printPairInAscii name value
+    = A.toAsciiBuilder (A.fromCIAscii name) ⊕
+      A.toAsciiBuilder "=" ⊕
+      if BS.any ((¬) ∘ isToken) (A.toByteString value) then
+          quoteStr value
+      else
+          A.toAsciiBuilder value
+
+escapeUnsafeChars ∷ BS.ByteString → AsciiBuilder → AsciiBuilder
+{-# INLINEABLE escapeUnsafeChars #-}
+escapeUnsafeChars bs b
+    = case BS.uncons bs of
+        Nothing         → b
+        Just (c, bs')
+            | isToken c → escapeUnsafeChars bs' $
+                          b ⊕ A.toAsciiBuilder (A.unsafeFromString [c])
+            | otherwise → escapeUnsafeChars bs' $
+                          b ⊕ toHex (fromIntegral $ fromEnum c)
+
+toHex ∷ Word8 → AsciiBuilder
+{-# INLINEABLE toHex #-}
+toHex o = A.toAsciiBuilder "%" ⊕
+          A.toAsciiBuilder (A.unsafeFromString [ toHex' (o `shiftR` 8)
+                                               , toHex' (o .&.   0x0F) ])
+    where
+      toHex' ∷ Word8 → Char
+      {-# INLINEABLE toHex' #-}
+      toHex' h
+          | h ≤ 0x09  = toEnum $ fromIntegral
+                               $ fromEnum '0' + fromIntegral h
+          | otherwise = toEnum $ fromIntegral
+                               $ fromEnum 'A' + fromIntegral (h - 0x0A)
+
+data ExtendedParam
+    = InitialEncodedParam {
+        epName    ∷ !CIAscii
+      , epCharset ∷ !CIAscii
+      , epPayload ∷ !BS.ByteString
+      }
+    | ContinuedEncodedParam {
+        epName    ∷ !CIAscii
+      , epSection ∷ !Integer
+      , epPayload ∷ !BS.ByteString
+      }
+    | AsciiParam {
+        epName    ∷ !CIAscii
+      , epSection ∷ !Integer
+      , apPayload ∷ !Ascii
+      }
+
+section ∷ ExtendedParam → Integer
+{-# INLINE section #-}
+section (InitialEncodedParam {..}) = 0
+section ep                         = epSection ep
+
+-- |'Parser' for MIME parameter values.
+mimeParams ∷ Parser (Map CIAscii Text)
+{-# INLINEABLE mimeParams #-}
+mimeParams = decodeParams =≪ P.many (try paramP)
+
+paramP ∷ Parser ExtendedParam
+paramP = do skipMany lws
+            void $ char ';'
+            skipMany lws
+            epm ← nameP
+            void $ char '='
+            case epm of
+              (name, 0, True)
+                  → do (charset, payload) ← initialEncodedValue
+                       return $ InitialEncodedParam name charset payload
+              (name, sect, True)
+                  → do payload ← encodedPayload
+                       return $ ContinuedEncodedParam name sect payload
+              (name, sect, False)
+                  → do payload ← token <|> quotedStr
+                       return $ AsciiParam name sect payload
+
+nameP ∷ Parser (CIAscii, Integer, Bool)
+nameP = do name      ← (A.toCIAscii ∘ A.unsafeFromByteString) <$>
+                       takeWhile1 (\c → isToken c ∧ c ≢ '*')
+           sect      ← option 0     $ try (char '*' *> decimal  )
+           isEncoded ← option False $ try (char '*' *> pure True)
+           return (name, sect, isEncoded)
+
+initialEncodedValue ∷ Parser (CIAscii, BS.ByteString)
+initialEncodedValue
+    = do charset ← metadata
+         void $ char '\''
+         void $ metadata -- Ignore the language tag
+         void $ char '\''
+         payload ← encodedPayload
+         if charset ≡ "" then
+             -- NOTE: I'm not sure this is the right thing, but RFC
+             -- 2231 doesn't tell us what we should do when the
+             -- charset is omitted.
+             return ("US-ASCII", payload)
+             -- FIXME: Rethink about this behaviour.
+         else
+             return (charset, payload)
+    where
+      metadata ∷ Parser CIAscii
+      metadata = (A.toCIAscii ∘ A.unsafeFromByteString) <$>
+                 takeWhile (\c → c ≢ '\'' ∧ isToken c)
+
+encodedPayload ∷ Parser BS.ByteString
+{-# INLINE encodedPayload #-}
+encodedPayload = BS.concat <$> P.many (hexChar <|> rawChars)
+
+hexChar ∷ Parser BS.ByteString
+{-# INLINEABLE hexChar #-}
+hexChar = do void $ char '%'
+             h ← satisfy isHexChar
+             l ← satisfy isHexChar
+             return $ BS.singleton $ hexToChar h l
+
+isHexChar ∷ Char → Bool
+isHexChar = inClass "0-9a-fA-F"
+
+hexToChar ∷ Char → Char → Char
+{-# INLINE hexToChar #-}
+hexToChar h l
+    = chr $ (hexToInt h `shiftL` 8) .&. hexToInt l
+
+hexToInt ∷ Char → Int
+{-# INLINEABLE hexToInt #-}
+hexToInt c
+    | c ≤ '9'   = ord c - ord '0'
+    | c ≤ 'F'   = ord c - ord 'A' + 10
+    | otherwise = ord c - ord 'a' + 10
+
+rawChars ∷ Parser BS.ByteString
+{-# INLINE rawChars #-}
+rawChars = takeWhile1 (\c → isToken c ∧ c ≢ '%')
+
+decodeParams ∷ Monad m ⇒ [ExtendedParam] → m (Map CIAscii Text)
+{-# INLINE decodeParams #-}
+decodeParams = (mapM decodeSections =≪) ∘ sortBySection
+
+sortBySection ∷ Monad m
+              ⇒ [ExtendedParam]
+              → m (Map CIAscii (Map Integer ExtendedParam))
+sortBySection = flip go (∅)
+    where
+      go ∷ Monad m
+         ⇒ [ExtendedParam]
+         → Map CIAscii (Map Integer ExtendedParam)
+         → m (Map CIAscii (Map Integer ExtendedParam))
+      go []     m = return m
+      go (x:xs) m
+          = case M.lookup (epName x) m of
+              Nothing
+                  → let s  = M.singleton (section x) x
+                        m' = M.insert (epName x) s m
+                    in
+                      go xs m'
+              Just s
+                  → case M.lookup (section x) s of
+                       Nothing
+                           → let s' = M.insert (section x) x  s
+                                 m' = M.insert (epName  x) s' m
+                             in
+                               go xs m'
+                       Just _
+                           → fail (concat [ "Duplicate section "
+                                          , show $ section x
+                                          , " for parameter '"
+                                          , A.toString $ A.fromCIAscii $ epName x
+                                          , "'"
+                                          ])
+
+decodeSections ∷ Monad m ⇒ Map Integer ExtendedParam → m Text
+decodeSections = (decodeSeq =≪) ∘ flip (flip toSeq 0) (∅)
+    where
+      toSeq ∷ Monad m
+            ⇒ Map Integer ExtendedParam
+            → Integer
+            → Seq ExtendedParam
+            → m (Seq ExtendedParam)
+      toSeq m expectedSect sects
+          = case M.minViewWithKey m of
+              Nothing
+                  → return sects
+              Just ((sect, p), m')
+                  | sect ≡ expectedSect
+                        → toSeq m' (expectedSect + 1) (sects ⊳ p)
+                  | otherwise
+                        → fail (concat [ "Missing section "
+                                       , show $ section p
+                                       , " for parameter '"
+                                       , A.toString $ A.fromCIAscii $ epName p
+                                       , "'"
+                                       ])
+
+      decodeSeq ∷ Monad m ⇒ Seq ExtendedParam → m Text
+      decodeSeq sects
+          = case S.viewl sects of
+              EmptyL
+                  → fail "decodeSeq: internal error: empty seq"
+              InitialEncodedParam {..} :< xs
+                  → do d ← getDecoder epCharset
+                       t ← decodeStr d epPayload
+                       decodeSeq' (Just d) xs $ S.singleton t
+              ContinuedEncodedParam {..} :< _
+                  → fail "decodeSeq: internal error: CEP at section 0"
+              AsciiParam {..} :< xs
+                  → let t = A.toText apPayload
+                    in
+                      decodeSeq' Nothing xs $ S.singleton t
+
+      decodeSeq' ∷ Monad m
+                 ⇒ Maybe Decoder
+                 → Seq ExtendedParam
+                 → Seq Text
+                 → m Text
+      decodeSeq' decoder sects chunks
+          = case S.viewl sects of
+              EmptyL
+                  → return $ T.concat $ toList chunks
+              InitialEncodedParam {..} :< _
+                  → fail "decodeSeq': internal error: IEP at section > 0"
+              ContinuedEncodedParam {..} :< xs
+                  → case decoder of
+                       Just d
+                           → do t ← decodeStr d epPayload
+                                decodeSeq' decoder xs $ chunks ⊳ t
+                       Nothing
+                           → fail (concat [ "Section "
+                                          , show epSection
+                                          , " for parameter '"
+                                          , A.toString $ A.fromCIAscii epName
+                                          , "' is encoded but its first section is not"
+                                          ])
+              AsciiParam {..} :< xs
+                  → let t = A.toText apPayload
+                    in
+                      decodeSeq' decoder xs $ chunks ⊳ t
+
+type Decoder = BS.ByteString → Either UnicodeException Text
+
+decodeStr ∷ Monad m ⇒ Decoder → BS.ByteString → m Text
+decodeStr decoder str
+    = case decoder str of
+        Right t → return t
+        Left  e → fail $ show e
+
+getDecoder ∷ Monad m ⇒ CIAscii → m Decoder
+getDecoder charset
+    | charset ≡ "UTF-8"    = return decodeUtf8'
+    | charset ≡ "US-ASCII" = return decodeUtf8'
+    | otherwise            = fail $ "No decoders found for charset: "
+                                  ⧺ A.toString (A.fromCIAscii charset)
index 712a6107f2932f93d603e9e272013e65c2553578..58286dbe6130733fcdc4b19057a061410df43a29 100644 (file)
@@ -1,23 +1,31 @@
-{-# OPTIONS_HADDOCK prune #-}
-
+{-# LANGUAGE
+    OverloadedStrings
+  , UnicodeSyntax
+  , ViewPatterns
+  #-}
 -- |Definition of things related on HTTP request.
 --
 -- In general you don't have to use this module directly.
 module Network.HTTP.Lucu.Request
     ( Method(..)
     , Request(..)
-    , requestP
+    , reqMustHaveBody
+    , request
     )
     where
-
-import           Network.HTTP.Lucu.Headers
-import           Network.HTTP.Lucu.HttpVersion
-import           Network.HTTP.Lucu.Parser
-import           Network.HTTP.Lucu.Parser.Http
-import           Network.URI
+import Control.Applicative
+import Control.Monad.Unicode
+import Data.Ascii (Ascii)
+import Data.Attoparsec.Char8
+import qualified Data.ByteString.Char8 as C8
+import Network.HTTP.Lucu.Headers
+import Network.HTTP.Lucu.HttpVersion
+import Network.HTTP.Lucu.Parser.Http
+import Network.URI
+import Prelude.Unicode
 
 -- |This is the definition of HTTP request methods, which shouldn't
--- require any description.
+-- require any descriptions.
 data Method = OPTIONS
             | GET
             | HEAD
@@ -26,64 +34,70 @@ data Method = OPTIONS
             | DELETE
             | TRACE
             | CONNECT
-            | ExtensionMethod !String
+            | ExtensionMethod !Ascii
               deriving (Eq, Show)
 
--- |This is the definition of HTTP reqest.
+-- |This is the definition of an HTTP reqest.
 data Request
     = Request {
-        reqMethod  :: !Method
-      , reqURI     :: !URI
-      , reqVersion :: !HttpVersion
-      , reqHeaders :: !Headers
+        reqMethod   !Method
+      , reqURI      !URI
+      , reqVersion  !HttpVersion
+      , reqHeaders  !Headers
       }
-    deriving (Show, Eq)
+    deriving (Eq, Show)
 
 instance HasHeaders Request where
+    {-# INLINE getHeaders #-}
     getHeaders = reqHeaders
+    {-# INLINE setHeaders #-}
     setHeaders req hdr = req { reqHeaders = hdr }
 
+-- |Returns 'True' iff the 'Request' must have an entity body.
+reqMustHaveBody ∷ Request → Bool
+{-# INLINEABLE reqMustHaveBody #-}
+reqMustHaveBody (reqMethod → m)
+    | m ≡ POST  = True
+    | m ≡ PUT   = True
+    | otherwise = False
 
-requestP :: Parser Request
-requestP = do _                      <- many crlf
-              (method, uri, version) <- requestLineP
-              headers                <- headersP
-              return Request {
-                           reqMethod  = method
-                         , reqURI     = uri
-                         , reqVersion = version
-                         , reqHeaders = headers
-                         }
-
-
-requestLineP :: Parser (Method, URI, HttpVersion)
-requestLineP = do method <- methodP
-                  _      <- sp
-                  uri    <- uriP
-                  _      <- sp
-                  ver    <- httpVersionP
-                  _      <- crlf
-                  return (method, uri, ver)
-
+-- |'Parser' for a 'Request'.
+request ∷ Parser Request
+request = do skipMany crlf
+             (meth, u, ver) ← requestLine
+             hdrs           ← headers
+             return Request {
+                          reqMethod  = meth
+                        , reqURI     = u
+                        , reqVersion = ver
+                        , reqHeaders = hdrs
+                        }
 
-methodP :: Parser Method
-methodP = ( let methods = [ ("OPTIONS", OPTIONS)
-                          , ("GET"    , GET    )
-                          , ("HEAD"   , HEAD   )
-                          , ("POST"   , POST   )
-                          , ("PUT"    , PUT    )
-                          , ("DELETE" , DELETE )
-                          , ("TRACE"  , TRACE  )
-                          , ("CONNECT", CONNECT)
-                          ]
-            in choice $ map (\ (str, mth)
-                                 -> string str >> return mth) methods )
-          <|>
-          fmap ExtensionMethod token
+requestLine ∷ Parser (Method, URI, HttpVersion)
+requestLine = do meth ← method
+                 sp
+                 u ← uri
+                 sp
+                 ver ← httpVersion
+                 crlf
+                 return (meth, u, ver)
 
+method ∷ Parser Method
+method = choice
+         [ string "OPTIONS" ≫ return OPTIONS
+         , string "GET"     ≫ return GET
+         , string "HEAD"    ≫ return HEAD
+         , string "POST"    ≫ return POST
+         , string "PUT"     ≫ return PUT
+         , string "DELETE"  ≫ return DELETE
+         , string "TRACE"   ≫ return TRACE
+         , string "CONNECT" ≫ return CONNECT
+         , ExtensionMethod <$> token
+         ]
 
-uriP :: Parser URI
-uriP = do str <- many1 $ satisfy (\ c -> not (isCtl c || c == ' '))
-          case parseURIReference str of
-            Nothing  -> failP
-            Just uri -> return uri
\ No newline at end of file
+uri ∷ Parser URI
+uri = do bs ← takeWhile1 (\c → (¬) (isCtl c ∨ c ≡ '\x20'))
+         let str = C8.unpack bs
+         case parseURIReference str of
+           Nothing → fail ("Unparsable URI: " ⧺ str)
+           Just u  → return u
index d3b8daad721a88b8b28a700c28565a278101d20a..7f48c9b0f4774ff853286bda721420dceb2fc678 100644 (file)
 {-# LANGUAGE
-    BangPatterns
-  , UnboxedTuples
+    DoAndIfThenElse
+  , OverloadedStrings
+  , RecordWildCards
+  , ScopedTypeVariables
   , UnicodeSyntax
   #-}
 module Network.HTTP.Lucu.RequestReader
     ( requestReader
     )
     where
-
-import           Control.Concurrent.STM
-import           Control.Exception
-import           Control.Monad
-import qualified Data.ByteString.Lazy.Char8 as B
-import           Data.ByteString.Lazy.Char8 (ByteString)
-import           Data.Maybe
+import Control.Concurrent
+import Control.Concurrent.STM
+import Control.Exception hiding (block)
+import Control.Monad
+import qualified Data.Attoparsec.Lazy as LP
+import qualified Data.ByteString as Strict
+import qualified Data.ByteString.Lazy as Lazy
+import Data.List
+import Data.Maybe
+import Data.Monoid.Unicode
 import qualified Data.Sequence as S
-import           Data.Sequence ((<|))
-import           GHC.Conc (unsafeIOToSTM)
-import           Network.Socket
-import           Network.HTTP.Lucu.Config
-import           Network.HTTP.Lucu.Chunk
-import           Network.HTTP.Lucu.DefaultPage
-import           Network.HTTP.Lucu.HandleLike
-import           Network.HTTP.Lucu.Interaction
-import           Network.HTTP.Lucu.Parser
-import           Network.HTTP.Lucu.Postprocess
-import           Network.HTTP.Lucu.Preprocess
-import           Network.HTTP.Lucu.Request
-import           Network.HTTP.Lucu.Response
-import           Network.HTTP.Lucu.Resource.Tree
-import           Prelude hiding (catch)
-import           System.IO (stderr)
+import Data.Sequence.Unicode hiding ((∅))
+import qualified Data.Text as T
+import Network.HTTP.Lucu.Abortion
+import Network.HTTP.Lucu.Config
+import Network.HTTP.Lucu.Chunk
+import Network.HTTP.Lucu.HandleLike
+import Network.HTTP.Lucu.Interaction
+import Network.HTTP.Lucu.Preprocess
+import Network.HTTP.Lucu.Request
+import Network.HTTP.Lucu.Response
+import Network.HTTP.Lucu.Resource.Internal
+import Network.HTTP.Lucu.Resource.Tree
+import Network.Socket
+import Prelude.Unicode
+import System.IO (hPutStrLn, stderr)
+
+data Context h
+    = Context {
+        cConfig    ∷ !Config
+      , cResTree   ∷ !ResTree
+      , cFallbacks ∷ ![FallbackHandler]
+      , cHandle    ∷ !h
+      , cPort      ∷ !PortNumber
+      , cAddr      ∷ !SockAddr
+      , cQueue     ∷ !InteractionQueue
+      }
+
+data ChunkReceivingState
+    = Initial
+    | InChunk !Int -- ^Number of remaining octets in the current
+                   -- chunk. It's always positive.
 
-requestReader :: HandleLike h => Config -> ResTree -> [FallbackHandler] -> h -> PortNumber -> SockAddr -> InteractionQueue -> IO ()
-requestReader !cnf !tree !fbs !h !port !addr !tQueue
-    = do input <- hGetLBS h
-         acceptRequest input
+requestReader ∷ HandleLike h
+              ⇒ Config
+              → ResTree
+              → [FallbackHandler]
+              → h
+              → PortNumber
+              → SockAddr
+              → InteractionQueue
+              → IO ()
+requestReader cnf tree fbs h port addr tQueue
+    = do input ← hGetLBS h
+         acceptRequest (Context cnf tree fbs h port addr tQueue) input
       `catches`
-      [ Handler (( \ _                   -> return () ) :: IOException -> IO ())
-      , Handler  ( \ ThreadKilled        -> return () )
-      , Handler  ( \ BlockedIndefinitelyOnSTM -> hPutStrLn stderr "requestReader: blocked indefinitely" )
-      , Handler (( \ e                   -> hPutStrLn stderr (show e) ) :: SomeException -> IO ())
+      [ Handler handleAsyncE
+      , Handler handleBIOS
+      , Handler handleOthers
       ]
     where
-      acceptRequest :: ByteString -> IO ()
-      acceptRequest input
-          -- キューに最大パイプライン深度以上のリクエストが溜まってゐる
-          -- 時は、それが限度以下になるまで待つ。
-          = {-# SCC "acceptRequest" #-}
-            do atomically $ do queue    <- readTVar tQueue
-                               when (S.length queue >= cnfMaxPipelineDepth cnf)
-                                    retry
+      handleAsyncE ∷ AsyncException → IO ()
+      handleAsyncE ThreadKilled = return ()
+      handleAsyncE e            = dump e
+
+      handleBIOS ∷ BlockedIndefinitelyOnSTM → IO ()
+      handleBIOS = dump
+
+      handleOthers ∷ SomeException → IO ()
+      handleOthers = dump
 
-               -- リクエストを讀む。パースできない場合は直ちに 400 Bad
-               -- Request 應答を設定し、それを出力してから切斷するやう
-               -- に ResponseWriter に通知する。
-               case parse requestP input of
-                 (# Success req , input' #) -> acceptParsableRequest req input'
-                 (# IllegalInput, _      #) -> acceptNonparsableRequest BadRequest
-                 (# ReachedEOF  , _      #) -> acceptNonparsableRequest BadRequest
+      dump ∷ Exception e ⇒ e → IO ()
+      dump e
+          = do hPutStrLn stderr "requestReader caught an exception:"
+               hPutStrLn stderr (show $ toException e)
 
-      acceptNonparsableRequest :: StatusCode -> IO ()
-      acceptNonparsableRequest status
-          = {-# SCC "acceptNonparsableRequest" #-}
-            do itr <- newInteraction cnf port addr Nothing Nothing
-               atomically $ do updateItr itr itrResponse
-                                             $ \ res -> res {
-                                                          resStatus = status
-                                                        }
-                               writeItr itr itrWillClose True
-                               writeItr itr itrState     Done
-                               writeDefaultPage itr
-                               postprocess itr
-                               enqueue itr
+acceptRequest ∷ HandleLike h ⇒ Context h → Lazy.ByteString → IO ()
+acceptRequest ctx@(Context {..}) input
+    -- キューに最大パイプライン深度以上のリクエストが溜まってゐる時は、
+    -- それが限度以下になるまで待つ。
+    = do atomically $
+             do queue ← readTVar cQueue
+                when (S.length queue ≥ cnfMaxPipelineDepth cConfig) $
+                    retry
+         -- リクエストを讀む。パースできない場合は直ちに 400 Bad
+         -- Request 應答を設定し、それを出力してから切斷するやうに
+         -- ResponseWriter に通知する。
+         case LP.parse request input of
+           LP.Done input' req → acceptParsableRequest ctx req input'
+           LP.Fail _ _ _      → acceptNonparsableRequest ctx
 
-      acceptParsableRequest :: Request -> ByteString -> IO ()
-      acceptParsableRequest req input
-          = {-# SCC "acceptParsableRequest" #-}
-            do cert <- hGetPeerCert h
-               itr  <- newInteraction cnf port addr cert (Just req)
-               action
-                   <- atomically $
-                      do preprocess itr
-                         isErr <- readItr itr itrResponse (isError . resStatus)
-                         if isErr then
-                             acceptSemanticallyInvalidRequest itr input
-                           else
-                             do rsrcM <- unsafeIOToSTM $ findResource tree fbs $ reqURI req
-                                case rsrcM of
-                                  Nothing -- Resource が無かった
-                                      -> acceptRequestForNonexistentResource itr input
+acceptNonparsableRequest ∷ HandleLike h ⇒ Context h → IO ()
+acceptNonparsableRequest ctx@(Context {..})
+    = do syi ← mkSyntacticallyInvalidInteraction cConfig
+         enqueue ctx syi
 
-                                  Just (rsrcPath, rsrcDef) -- あった
-                                      -> acceptRequestForExistentResource itr input rsrcPath rsrcDef
-               action
+acceptParsableRequest ∷ HandleLike h
+                      ⇒ Context h
+                      → Request
+                      → Lazy.ByteString
+                      → IO ()
+acceptParsableRequest ctx@(Context {..}) req input
+    = do let ar = preprocess (cnfServerHost cConfig) cPort req
+         if isError $ arInitialStatus ar then
+             acceptSemanticallyInvalidRequest ctx ar input
+         else
+             do rsrc ← findResource cResTree cFallbacks $ reqURI $ arRequest ar
+                case rsrc of
+                  Nothing
+                      → do let ar' = ar { arInitialStatus = NotFound }
+                           acceptSemanticallyInvalidRequest ctx ar' input
+                  Just (path, def)
+                      → acceptRequestForResource ctx ar input path def
 
-      acceptSemanticallyInvalidRequest :: Interaction -> ByteString -> STM (IO ())
-      acceptSemanticallyInvalidRequest itr input
-          = {-# SCC "acceptSemanticallyInvalidRequest" #-}
-            do writeItr itr itrState Done
-               writeDefaultPage itr
-               postprocess itr
-               enqueue itr
-               return $ acceptRequest input
+acceptSemanticallyInvalidRequest ∷ HandleLike h
+                                 ⇒ Context h
+                                 → AugmentedRequest
+                                 → Lazy.ByteString
+                                 → IO ()
+acceptSemanticallyInvalidRequest ctx@(Context {..}) ar input
+    = do sei ← mkSemanticallyInvalidInteraction cConfig ar
+         enqueue ctx sei
+         acceptRequest ctx input
 
-      acceptRequestForNonexistentResource :: Interaction -> ByteString -> STM (IO ())
-      acceptRequestForNonexistentResource itr input
-          = {-# SCC "acceptRequestForNonexistentResource" #-}
-            do updateItr itr itrResponse 
-                             $ \res -> res {
-                                         resStatus = NotFound
-                                       }
-               writeItr itr itrState Done
-               writeDefaultPage itr
-               postprocess itr
-               enqueue itr
-               return $ acceptRequest input
+acceptRequestForResource ∷ HandleLike h
+                         ⇒ Context h
+                         → AugmentedRequest
+                         → Lazy.ByteString
+                         → [Strict.ByteString]
+                         → ResourceDef
+                         → IO ()
+acceptRequestForResource ctx@(Context {..}) ar@(AugmentedRequest {..}) input rsrcPath rsrcDef
+    = do cert ← hGetPeerCert cHandle
+         ni   ← mkNormalInteraction cConfig cAddr cert ar rsrcPath
+         tid  ← spawnResource rsrcDef ni
+         enqueue ctx ni
+         if reqMustHaveBody arRequest then
+             waitForReceiveBodyReq ctx ni tid input
+         else
+             acceptRequest ctx input
 
-      acceptRequestForExistentResource :: Interaction -> ByteString -> [String] -> ResourceDef -> STM (IO ())
-      acceptRequestForExistentResource oldItr input rsrcPath rsrcDef
-          = {-# SCC "acceptRequestForExistentResource" #-}
-            do let itr = oldItr { itrResourcePath = Just rsrcPath }
-               requestHasBody <- readItr itr itrRequestHasBody id
-               enqueue itr
-               return $ do _ <- runResource rsrcDef itr
-                           if requestHasBody then
-                               observeRequest itr input
-                             else
-                               acceptRequest input
+waitForReceiveBodyReq ∷ HandleLike h
+                      ⇒ Context h
+                      → NormalInteraction
+                      → ThreadId
+                      → Lazy.ByteString
+                      → IO ()
+waitForReceiveBodyReq ctx ni@(NI {..}) rsrcTid input
+    = case fromJust niReqBodyLength of
+        Chunked
+            → waitForReceiveChunkedBodyReqForTheFirstTime ctx ni rsrcTid input
+        Fixed len
+            → waitForReceiveNonChunkedBodyReqForTheFirstTime ctx ni input len
 
-      observeRequest :: Interaction -> ByteString -> IO ()
-      observeRequest itr input
-          = {-# SCC "observeRequest" #-}
-            do isChunked <- atomically $ readItr itr itrRequestIsChunked id
-               if isChunked then
-                   observeChunkedRequest itr input
-                 else
-                   observeNonChunkedRequest itr input
+-- Toooooo long name for a function...
+waitForReceiveChunkedBodyReqForTheFirstTime ∷ HandleLike h
+                                            ⇒ Context h
+                                            → NormalInteraction
+                                            → ThreadId
+                                            → Lazy.ByteString
+                                            → IO ()
+waitForReceiveChunkedBodyReqForTheFirstTime ctx ni@(NI {..}) rsrcTid input
+    = join $
+      atomically $
+      do req ← takeTMVar niReceiveBodyReq
+         case req of
+           ReceiveBody wanted
+               → do putTMVar niSendContinue niExpectedContinue
+                    return $ readCurrentChunk ctx ni rsrcTid wanted input Initial
+           WasteAll
+               → do putTMVar niSendContinue False
+                    return $ wasteAllChunks ctx rsrcTid input Initial
 
-      observeChunkedRequest :: Interaction -> ByteString -> IO ()
-      observeChunkedRequest itr input
-          = {-# SCC "observeChunkedRequest" #-}
-            do action
-                   <- atomically $
-                      do isOver <- readItr itr itrReqChunkIsOver id
-                         if isOver then
-                             return $ acceptRequest input
-                           else
-                             do wantedM <- readItr itr itrReqBodyWanted id
-                                if wantedM == Nothing then
-                                    do wasteAll <- readItr itr itrReqBodyWasteAll id
-                                       if wasteAll then
-                                           -- 破棄要求が來た
-                                           do remainingM <- readItr itr itrReqChunkRemaining id
-                                              if fmap (> 0) remainingM == Just True then
-                                                  -- 現在のチャンクをまだ
-                                                  -- 讀み終へてゐない
-                                                  do let (_, input') = B.splitAt (fromIntegral
-                                                                                  $ fromJust remainingM) input
-                                                         (# footerR, input'' #) = parse chunkFooterP input'
+waitForReceiveChunkedBodyReq ∷ HandleLike h
+                             ⇒ Context h
+                             → NormalInteraction
+                             → ThreadId
+                             → Lazy.ByteString
+                             → ChunkReceivingState
+                             → IO ()
+waitForReceiveChunkedBodyReq ctx ni@(NI {..}) rsrcTid input st
+    = do req ← atomically $ takeTMVar niReceiveBodyReq
+         case req of
+           ReceiveBody wanted
+               → readCurrentChunk ctx ni rsrcTid wanted input st
+           WasteAll
+               → wasteAllChunks ctx rsrcTid input st
 
-                                                     if footerR == Success () then
-                                                         -- チャンクフッタを正常に讀めた
-                                                         do writeItr itr itrReqChunkRemaining $ Just 0
-                                                         
-                                                            return $ observeChunkedRequest itr input''
-                                                       else
-                                                         return $ chunkWasMalformed itr
-                                                else
-                                                  -- 次のチャンクを讀み始める
-                                                  seekNextChunk itr input
-                                         else
-                                           -- 要求がまだ來ない
-                                           retry
-                                  else
-                                    -- 受信要求が來た
-                                    do remainingM <- readItr itr itrReqChunkRemaining id
-                                       if fmap (> 0) remainingM == Just True then
-                                           -- 現在のチャンクをまだ讀み
-                                           -- 終へてゐない
-                                           do let wanted             = fromJust wantedM
-                                                  remaining          = fromJust remainingM
-                                                  bytesToRead        = fromIntegral $ min wanted remaining
-                                                  (chunk, input')    = B.splitAt bytesToRead input
-                                                  actualReadBytes    = fromIntegral $ B.length chunk
-                                                  newWanted          = case wanted - actualReadBytes of
-                                                                         0 -> Nothing
-                                                                         n -> Just n
-                                                  newRemaining       = Just $ remaining - actualReadBytes
-                                                  updateStates
-                                                      = do writeItr itr itrReqChunkRemaining newRemaining
-                                                           writeItr itr itrReqBodyWanted newWanted
-                                                           updateItr itr itrReceivedBody $ flip B.append chunk
+wasteAllChunks ∷ HandleLike h
+               ⇒ Context h
+               → ThreadId
+               → Lazy.ByteString
+               → ChunkReceivingState
+               → IO ()
+wasteAllChunks ctx rsrcTid = go
+    where
+      go ∷ Lazy.ByteString → ChunkReceivingState → IO ()
+      go input Initial
+          = case LP.parse chunkHeader input of
+              LP.Done input' chunkLen
+                  | chunkLen ≡ 0 → gotFinalChunk input'
+                  | otherwise    → gotChunk input' chunkLen
+              LP.Fail _ eCtx e
+                  → chunkWasMalformed rsrcTid eCtx e
+                       "wasteAllChunks: chunkHeader"
+      go input (InChunk chunkLen)
+          = gotChunk input chunkLen
+
+      gotChunk ∷ Lazy.ByteString → Int → IO ()
+      gotChunk input chunkLen
+          = let input' = Lazy.drop (fromIntegral chunkLen) input
+            in
+              case LP.parse chunkFooter input' of
+                LP.Done input'' _
+                    → go input'' Initial
+                LP.Fail _ eCtx e
+                    → chunkWasMalformed rsrcTid eCtx e
+                          "wasteAllChunks: chunkFooter"
 
-                                              if newRemaining == Just 0 then
-                                                  -- チャンクフッタを讀む
-                                                  case parse chunkFooterP input' of
-                                                    (# Success _, input'' #)
-                                                        -> do updateStates
-                                                              return $ observeChunkedRequest itr input''
-                                                    (# _, _ #)
-                                                        -> return $ chunkWasMalformed itr
-                                                else
-                                                  -- まだチャンクの終はりに達してゐない
-                                                  do updateStates
-                                                     return $ observeChunkedRequest itr input'
-                                         else
-                                           -- 次のチャンクを讀み始める
-                                           seekNextChunk itr input
-               action
+      gotFinalChunk ∷ Lazy.ByteString → IO ()
+      gotFinalChunk input
+          = case LP.parse chunkTrailer input of
+              LP.Done input' _
+                  → acceptRequest ctx input'
+              LP.Fail _ eCtx e
+                  → chunkWasMalformed rsrcTid eCtx e
+                        "wasteAllChunks: chunkTrailer"
 
-      seekNextChunk :: Interaction -> ByteString -> STM (IO ())
-      seekNextChunk itr input
-          = {-# SCC "seekNextChunk" #-}
-            case parse chunkHeaderP input of
-              -- 最終チャンク (中身が空)
-              (# Success 0, input' #)
-                  -> case parse chunkTrailerP input' of
-                       (# Success _, input'' #)
-                           -> do writeItr itr itrReqChunkLength $ Nothing
-                                 writeItr itr itrReqChunkRemaining $ Nothing
-                                 writeItr itr itrReqChunkIsOver True
-                                 
-                                 return $ acceptRequest input''
-                       (# _, _ #)
-                           -> return $ chunkWasMalformed itr
-              -- 最終でないチャンク
-              (# Success len, input' #)
-                  -> do writeItr itr itrReqChunkLength $ Just len
-                        writeItr itr itrReqChunkRemaining $ Just len
-                        
-                        return $ observeChunkedRequest itr input'
-              -- チャンクヘッダがをかしい
-              (# _, _ #)
-                  -> return $ chunkWasMalformed itr
+readCurrentChunk ∷ HandleLike h
+                 ⇒ Context h
+                 → NormalInteraction
+                 → ThreadId
+                 → Int
+                 → Lazy.ByteString
+                 → ChunkReceivingState
+                 → IO ()
+readCurrentChunk ctx ni@(NI {..}) rsrcTid wanted = go
+    where
+      go ∷ Lazy.ByteString → ChunkReceivingState → IO ()
+      go input Initial
+          = case LP.parse chunkHeader input of
+              LP.Done input' chunkLen
+                  | chunkLen ≡ 0
+                      → gotFinalChunk input'
+                  | otherwise
+                      → gotChunk input' chunkLen
+              LP.Fail _ eCtx e
+                  → chunkWasMalformed rsrcTid eCtx e
+                        "readCurrentChunk: chunkHeader"
+      go input (InChunk chunkLen)
+          = gotChunk input chunkLen
 
-      chunkWasMalformed :: Interaction -> IO ()
-      chunkWasMalformed itr
-          = {-# SCC "chunkWasMalformed" #-}
-            atomically $ do updateItr itr itrResponse 
-                                          $ \ res -> res {
-                                                       resStatus = BadRequest
-                                                     }
-                            writeItr itr itrWillClose True
-                            writeItr itr itrState Done
-                            writeDefaultPage itr
-                            postprocess itr
+      gotChunk ∷ Lazy.ByteString → Int → IO ()
+      gotChunk input chunkLen
+          = do let bytesToRead     = min wanted chunkLen
+                   (block, input') = Lazy.splitAt (fromIntegral bytesToRead) input
+                   block'          = Strict.concat $ Lazy.toChunks block
+                   actualReadBytes = Strict.length block'
+                   chunkLen'       = chunkLen - actualReadBytes
+               atomically $ putTMVar niReceivedBody block'
+               if chunkLen' ≡ 0 then
+                   case LP.parse chunkFooter input' of
+                     LP.Done input'' _
+                         → waitForReceiveChunkedBodyReq ctx ni rsrcTid input'' Initial
+                     LP.Fail _ eCtx e
+                         → chunkWasMalformed rsrcTid eCtx e
+                               "readCurrentChunk: chunkFooter"
+               else
+                   waitForReceiveChunkedBodyReq ctx ni rsrcTid input' $ InChunk chunkLen'
 
-      observeNonChunkedRequest :: Interaction -> ByteString -> IO ()
-      observeNonChunkedRequest itr input
-          = {-# SCC "observeNonChunkedRequest" #-}
-            do action
-                   <- atomically $
-                      do wantedM <- readItr itr itrReqBodyWanted id
-                         if wantedM == Nothing then
-                             do wasteAll <- readItr itr itrReqBodyWasteAll id
-                                if wasteAll then
-                                    -- 破棄要求が來た
-                                    do remainingM <- readItr itr itrReqChunkRemaining id
-                                       
-                                       let (_, input') = if remainingM == Nothing then
-                                                             (B.takeWhile (\ _ -> True) input, B.empty)
-                                                         else
-                                                             B.splitAt (fromIntegral $ fromJust remainingM) input
+      gotFinalChunk ∷ Lazy.ByteString → IO ()
+      gotFinalChunk input
+          = do atomically $ putTMVar niReceivedBody (∅)
+               case LP.parse chunkTrailer input of
+                 LP.Done input' _
+                     → acceptRequest ctx input'
+                 LP.Fail _ eCtx e
+                     → chunkWasMalformed rsrcTid eCtx e
+                           "readCurrentChunk: chunkTrailer"
 
-                                       writeItr itr itrReqChunkRemaining $ Just 0
-                                       writeItr itr itrReqChunkIsOver True
+chunkWasMalformed ∷ ThreadId → [String] → String → String → IO ()
+chunkWasMalformed tid eCtx e msg
+    = let abo = mkAbortion BadRequest [("Connection", "close")]
+                $ Just
+                $ "chunkWasMalformed: "
+                ⊕ T.pack msg
+                ⊕ ": "
+                ⊕ T.pack (intercalate ", " eCtx)
+                ⊕ ": "
+                ⊕ T.pack e
+      in
+        throwTo tid abo
 
-                                       return $ acceptRequest input'
-                                  else
-                                    -- 要求がまだ来ない
-                                    retry
-                           else
-                               -- 受信要求が來た
-                               do remainingM <- readItr itr itrReqChunkRemaining id
+waitForReceiveNonChunkedBodyReqForTheFirstTime ∷ HandleLike h
+                                               ⇒ Context h
+                                               → NormalInteraction
+                                               → Lazy.ByteString
+                                               → Int
+                                               → IO ()
+waitForReceiveNonChunkedBodyReqForTheFirstTime ctx ni@(NI {..}) input bodyLen
+    = join $
+      atomically $
+      do req ← takeTMVar niReceiveBodyReq
+         case req of
+           ReceiveBody wanted
+               → do putTMVar niSendContinue niExpectedContinue
+                    return $ readNonChunkedRequestBody ctx ni input bodyLen wanted
+           WasteAll
+               → do putTMVar niSendContinue False
+                    return $ wasteNonChunkedRequestBody ctx input bodyLen
 
-                                  let wanted          = fromJust wantedM
-                                      bytesToRead     = fromIntegral $ maybe wanted (min wanted) remainingM
-                                      (chunk, input') = B.splitAt bytesToRead input
-                                      newRemaining    = fmap
-                                                        (\ x -> x - (fromIntegral $ B.length chunk))
-                                                        remainingM
-                                      isOver          = B.length chunk < bytesToRead || newRemaining == Just 0
+waitForReceiveNonChunkedBodyReq ∷ HandleLike h
+                                ⇒ Context h
+                                → NormalInteraction
+                                → Lazy.ByteString
+                                → Int
+                                → IO ()
+waitForReceiveNonChunkedBodyReq ctx ni@(NI {..}) input bodyLen
+    = do req ← atomically $ takeTMVar niReceiveBodyReq
+         case req of
+           ReceiveBody wanted
+               → readNonChunkedRequestBody ctx ni input bodyLen wanted
+           WasteAll
+               → wasteNonChunkedRequestBody ctx input bodyLen
 
-                                  writeItr itr itrReqChunkRemaining newRemaining
-                                  writeItr itr itrReqChunkIsOver isOver
-                                  writeItr itr itrReqBodyWanted Nothing
-                                  writeItr itr itrReceivedBody chunk
+wasteNonChunkedRequestBody ∷ HandleLike h
+                           ⇒ Context h
+                           → Lazy.ByteString
+                           → Int
+                           → IO ()
+wasteNonChunkedRequestBody ctx input bodyLen
+    = do let input' = Lazy.drop (fromIntegral bodyLen) input
+         acceptRequest ctx input'
+
+readNonChunkedRequestBody ∷ HandleLike h
+                          ⇒ Context h
+                          → NormalInteraction
+                          → Lazy.ByteString
+                          → Int
+                          → Int
+                          → IO ()
+readNonChunkedRequestBody ctx ni@(NI {..}) input bodyLen wanted
+    | bodyLen ≡ 0 = gotEndOfRequest
+    | otherwise   = gotBody
+    where
+      gotBody ∷ IO ()
+      gotBody
+          = do let bytesToRead     = min wanted bodyLen
+                   (block, input') = Lazy.splitAt (fromIntegral bytesToRead) input
+                   block'          = Strict.concat $ Lazy.toChunks block
+                   actualReadBytes = Strict.length block'
+                   bodyLen'        = bodyLen - actualReadBytes
+               atomically $ putTMVar niReceivedBody block'
+               waitForReceiveNonChunkedBodyReq ctx ni input' bodyLen'
 
-                                  if isOver then
-                                      return $ acceptRequest input'
-                                    else
-                                      return $ observeNonChunkedRequest itr input'
-               action
+      gotEndOfRequest ∷ IO ()
+      gotEndOfRequest
+          = do atomically $ putTMVar niReceivedBody (∅)
+               acceptRequest ctx input
 
-      enqueue :: Interaction -> STM ()
-      enqueue itr = {-# SCC "enqueue" #-}
-                    do queue <- readTVar tQueue
-                       writeTVar tQueue (itr <| queue)
\ No newline at end of file
+enqueue ∷ (HandleLike h, Interaction i) ⇒ Context h → i → IO ()
+{-# INLINEABLE enqueue #-}
+enqueue (Context {..}) itr
+    = atomically $
+      do queue ← readTVar cQueue
+         writeTVar cQueue (toInteraction itr ⊲ queue)
index fa08fa5c3450c28b2131c7aa3320da814afa4e21..aee29d56f95682c7550623176267f23e6230d23b 100644 (file)
@@ -1,13 +1,14 @@
 {-# LANGUAGE
-    UnboxedTuples
+    BangPatterns
+  , GeneralizedNewtypeDeriving
+  , DoAndIfThenElse
+  , OverloadedStrings
+  , RecordWildCards
   , UnicodeSyntax
   #-}
-{-# OPTIONS_HADDOCK prune #-}
-
--- |This is the Resource Monad; monadic actions to define the behavior
--- of each resources. The 'Resource' Monad is a kind of 'Prelude.IO'
--- Monad thus it implements 'Control.Monad.Trans.MonadIO' class. It is
--- also a state machine.
+-- |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:
 --
@@ -16,9 +17,9 @@
 --   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 the request header, find (or not
---      find) an entity, receive the request body (if any), decide the
---      response header, and decide the response body. This process
+--   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
 -- /Examining Request/ and the final state is /Done/.
 --
 --   [/Examining Request/] In this state, a 'Resource' looks at the
---   request header and thinks about an 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.
+--   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.
 --
---   [/Getting Body/] A 'Resource' asks the system to receive a
---   request body from client. Before actually reading from the
+--   [/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 request body, the system still reads it
---   and just throws it away.
+--   receiving all or part of a request body, the system automatically
+--   discards it.
 --
---   [/Deciding Header/] A 'Resource' makes a decision of status code
---   and response header. When it transits to the next state, the
---   system checks the validness of response header and then write
---   them to the socket.
+--   [/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.
 --
---   [/Deciding Body/] In this state, a 'Resource' asks the system to
+--   [/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
---   completes it depending on the status code.
+--   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.
 -- 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
     (
     -- * Types
       Resource
+    , ResourceDef(..)
+    , emptyResource
     , FormData(..)
-    , runRes -- private
-
-    -- * Actions
 
-    -- ** Getting request header
-
-    -- |These actions can be computed regardless of the current state,
-    -- and they don't change the state.
+    -- * 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'
@@ -94,899 +96,597 @@ module Network.HTTP.Lucu.Resource
     , getContentType
     , getAuthorization
 
-    -- ** Finding an entity
-
-    -- |These actions can be computed only in the /Examining Request/
-    -- state. After the computation, the 'Resource' transits to
-    -- /Getting Body/ state.
+    -- * 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
-
-    -- ** Getting a request body
-
-    -- |Computation of these actions changes the state to /Getting
-    -- Body/.
-    , input
-    , inputChunk
-    , inputLBS
-    , inputChunkLBS
-    , inputForm
-    , defaultLimit
-
-    -- ** Setting response headers
-    
-    -- |Computation of these actions changes the state to /Deciding
-    -- Header/.
+    , 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
-    , setHeader
     , redirect
     , setContentType
-    , setLocation
     , setContentEncoding
     , setWWWAuthenticate
 
-    -- ** Writing a response body
+    -- ** Less frequently used functions
+    , setLocation
+    , setHeader
+    , deleteHeader
 
-    -- |Computation of these actions changes the state to /Deciding
-    -- Body/.
-    , output
-    , outputChunk
-    , outputLBS
-    , outputChunkLBS
+    -- * Sending a response body
 
-    , driftTo
+    -- |These functions make the 'Resource' transit to the
+    -- /Sending Body/ state.
+    , putChunk
+    , putChunks
+    , putBuilder
     )
     where
-
-import           Control.Concurrent.STM
-import           Control.Monad.Reader
-import qualified Data.ByteString as Strict (ByteString)
-import qualified Data.ByteString.Lazy as Lazy (ByteString)
-import qualified Data.ByteString.Char8 as C8 hiding (ByteString)
-import qualified Data.ByteString.Lazy.Char8 as L8 hiding (ByteString)
-import           Data.Char
-import           Data.List
-import           Data.Maybe
-import           Data.Time
+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.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.Authorization
-import           Network.HTTP.Lucu.Config
-import           Network.HTTP.Lucu.ContentCoding
-import           Network.HTTP.Lucu.DefaultPage
-import           Network.HTTP.Lucu.ETag
+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.MultipartForm
-import           Network.HTTP.Lucu.Parser
-import           Network.HTTP.Lucu.Postprocess
-import           Network.HTTP.Lucu.Request
-import           Network.HTTP.Lucu.Response
-import           Network.HTTP.Lucu.MIMEType
-import           Network.HTTP.Lucu.Utils
-import           Network.Socket hiding (accept)
-import           Network.URI hiding (path)
-import           OpenSSL.X509
-
--- |The 'Resource' monad. This monad implements
--- 'Control.Monad.Trans.MonadIO' so it can do any 'Prelude.IO'
--- actions.
-newtype Resource a = Resource { unRes :: ReaderT Interaction IO a }
-
-instance Functor Resource where
-    fmap f c = Resource (fmap f (unRes c))
-
-instance Monad Resource where
-    c >>= f = Resource (unRes c >>= unRes . f)
-    return  = Resource . return
-    fail    = Resource . fail
-
-instance MonadIO Resource where
-    liftIO = Resource . liftIO
-
-
-runRes :: Resource a -> Interaction -> IO a
-runRes r itr
-    = runReaderT (unRes r) itr
-
-
-getInteraction :: Resource Interaction
-getInteraction = Resource ask
-
-
--- |Get the 'Network.HTTP.Lucu.Config.Config' value which is used for
--- the httpd.
-getConfig :: Resource Config
-getConfig = do itr <- getInteraction
-               return $! itrConfig itr
-
-
--- |Get the 'Network.Socket.SockAddr' of the remote host. If you want
--- a string representation instead of 'Network.Socket.SockAddr', use
--- 'getRemoteAddr''.
-getRemoteAddr :: Resource SockAddr
-getRemoteAddr = do itr <- getInteraction
-                   return $! itrRemoteAddr itr
-
+import Network.HTTP.Lucu.HttpVersion
+import Network.HTTP.Lucu.Interaction
+import Network.HTTP.Lucu.MultipartForm
+import Network.HTTP.Lucu.Request
+import Network.HTTP.Lucu.Resource.Internal
+import Network.HTTP.Lucu.Response
+import Network.HTTP.Lucu.MIMEType
+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 'Network.Socket.SockAddr' instead of 'Prelude.String',
--- use 'getRemoteAddr'.
-getRemoteAddr' :: Resource String
-getRemoteAddr' = do addr          <- getRemoteAddr
-                    (Just str, _) <- liftIO $! getNameInfo [NI_NUMERICHOST] True False addr
-                    return str
+-- you want a 'SockAddr' instead of 'HostName', use 'getRemoteAddr'.
+getRemoteAddr' ∷ Resource HostName
+getRemoteAddr' = liftIO ∘ toNM =≪ getRemoteAddr
+    where
+      toNM ∷ SockAddr → IO HostName
+      toNM = (fromJust ∘ fst <$>) ∘ getNameInfo [NI_NUMERICHOST] True False
 
 -- |Resolve an address to the remote host.
-getRemoteHost :: Resource String
-getRemoteHost = do addr          <- getRemoteAddr
-                   (Just str, _) <- liftIO $! getNameInfo [] True False addr
-                   return str
+getRemoteHost ∷ Resource (Maybe HostName)
+getRemoteHost = liftIO ∘ getHN =≪ getRemoteAddr
+    where
+      getHN ∷ SockAddr → IO (Maybe HostName)
+      getHN = (fst <$>) ∘ getNameInfo [] True False
 
--- | Return the X.509 certificate of the client, or 'Nothing' if:
---
---   * This request didn't came through an SSL stream.
---
---   * The client didn't send us its certificate.
---
---   * The 'OpenSSL.Session.VerificationMode' of
---     'OpenSSL.Session.SSLContext' in
---     'Network.HTTP.Lucu.Config.SSLConfig' has not been set to
---     'OpenSSL.Session.VerifyPeer'.
-getRemoteCertificate :: Resource (Maybe X509)
-getRemoteCertificate = do itr <- getInteraction
-                          return $! itrRemoteCert itr
-
--- |Get the 'Network.HTTP.Lucu.Request.Request' value which represents
--- the request header. In general you don't have to use this action.
-getRequest :: Resource Request
-getRequest = do itr <- getInteraction
-                req <- liftIO $! atomically $! readItr itr itrRequest fromJust
-                return req
-
--- |Get the 'Network.HTTP.Lucu.Request.Method' value of the request.
-getMethod :: Resource Method
-getMethod = do req <- getRequest
-               return $! reqMethod req
+-- |Get the 'Method' value of the request.
+getMethod ∷ Resource Method
+getMethod = reqMethod <$> getRequest
 
 -- |Get the URI of the request.
-getRequestURI :: Resource URI
-getRequestURI = do req <- getRequest
-                   return $! reqURI req
+getRequestURI ∷ Resource URI
+getRequestURI = reqURI <$> getRequest
 
 -- |Get the HTTP version of the request.
-getRequestVersion :: Resource HttpVersion
-getRequestVersion = do req <- getRequest
-                       return $! reqVersion req
-
--- |Get the path of this 'Resource' (to be exact,
--- 'Network.HTTP.Lucu.Resource.Tree.ResourceDef') in the
--- 'Network.HTTP.Lucu.Resource.Tree.ResTree'. The result of this
--- action is the exact path in the tree even if the
--- 'Network.HTTP.Lucu.Resource.Tree.ResourceDef' is greedy.
---
--- Example:
+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'
 --
--- > main = let tree = mkResTree [ (["foo"], resFoo) ]
--- >        in runHttpd defaultConfig tree
--- >
--- > resFoo = ResourceDef {
--- >     resIsGreedy = True
--- >   , resGet = Just $ do requestURI   <- getRequestURI
--- >                        resourcePath <- getResourcePath
--- >                        pathInfo     <- getPathInfo
--- >                        -- uriPath requestURI == "/foo/bar/baz"
--- >                        -- resourcePath       == ["foo"]
--- >                        -- pathInfo           == ["bar", "baz"]
--- >                        ...
--- >   , ...
--- >   }
-getResourcePath :: Resource [String]
-getResourcePath = do itr <- getInteraction
-                     return $! fromJust $! itrResourcePath itr
-
-
--- |This is an analogy of CGI PATH_INFO. The result is
--- URI-unescaped. It is always @[]@ if the
--- 'Network.HTTP.Lucu.Resource.Tree.ResourceDef' is not greedy. See
--- 'getResourcePath'.
-getPathInfo :: Resource [String]
-getPathInfo = do rsrcPath <- getResourcePath
-                 uri      <- getRequestURI
-                 let reqPathStr = uriPath uri
-                     reqPath    = [unEscapeString x | x <- splitBy (== '/') reqPathStr, x /= ""]
-                 -- rsrcPath と reqPath の共通する先頭部分を reqPath か
-                 -- ら全部取り除くと、それは PATH_INFO のやうなものにな
-                 -- る。rsrcPath は全部一致してゐるに決まってゐる(でな
-                 -- ければこの Resource が撰ばれた筈が無い)ので、
-                 -- rsrcPath の長さの分だけ削除すれば良い。
-                 return $! drop (length rsrcPath) reqPath
+-- 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 to pairs of
--- @(name, formData)@. This action doesn't parse the request body. See
--- 'inputForm'.
-getQueryForm :: Resource [(String, FormData)]
-getQueryForm = liftM parse' getRequestURI
+-- 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
-      parse' = map toPairWithFormData .
-               parseWWWFormURLEncoded .
-               snd .
-               splitAt 1 .
+      parse' = map toPairWithFormData ∘
+               parseWWWFormURLEncoded ∘
+               fromJust ∘
+               A.fromChars ∘
+               drop 1 ∘
                uriQuery
 
-toPairWithFormData :: (String, String) -> (String, FormData)
+toPairWithFormData ∷ (ByteString, ByteString) → (Strict.ByteString, FormData)
 toPairWithFormData (name, value)
     = let fd = FormData {
                  fdFileName = Nothing
-               , fdContent  = L8.pack value
+               , fdMIMEType = parseMIMEType "text/plain"
+               , fdContent  = Lazy.fromChunks [value]
                }
       in (name, fd)
 
--- |Get a value of given request header. Comparison of header name is
--- case-insensitive. Note that this action is not intended to be used
--- so frequently: there should be actions like 'getContentType' for
--- every common headers.
-getHeader :: Strict.ByteString -> Resource (Maybe Strict.ByteString)
-getHeader name = name `seq`
-                 do req <- getRequest
-                    return $! H.getHeader name req
-
--- |Get a list of 'Network.HTTP.Lucu.MIMEType.MIMEType' enumerated on
--- header \"Accept\".
-getAccept :: Resource [MIMEType]
-getAccept = do acceptM <- getHeader (C8.pack "Accept")
-               case acceptM of
-                 Nothing 
-                     -> return []
-                 Just accept
-                     -> case parse mimeTypeListP (L8.fromChunks [accept]) of
-                          (# Success xs, _ #) -> return xs
-                          (# _         , _ #) -> abort BadRequest []
-                                                 (Just $ "Unparsable Accept: " ++ C8.unpack accept)
-
--- |Get a list of @(contentCoding, qvalue)@ enumerated on header
--- \"Accept-Encoding\". The list is sorted in descending order by
--- qvalue.
-getAcceptEncoding :: Resource [(String, Maybe Double)]
+-- |@'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 p (A.toByteString accept) of
+                    Right xs → return xs
+                    Left  _  → abort $ mkAbortion' BadRequest
+                                     $ "Unparsable Accept: " ⊕ A.toText accept
+    where
+      p = do xs ← mimeTypeList
+             P.endOfInput
+             return xs
+
+-- |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 (C8.pack "Accept-Encoding")
+    = 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)]
-                       _               -> undefined
-           Just value
-               -> if C8.null value then
+               → 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
-                      case parse acceptEncodingListP (L8.fromChunks [value]) of
-                        (# Success x, _ #) -> return $ reverse $ sortBy orderAcceptEncodings x
-                        (# _        , _ #) -> abort BadRequest []
-                                              (Just $ "Unparsable Accept-Encoding: " ++ C8.unpack value)
-
--- |Check whether a given content-coding is acceptable.
-isEncodingAcceptable :: String -> Resource Bool
-isEncodingAcceptable coding
-    = do accList <- getAcceptEncoding
-         return (flip any accList $ \ (c, q) ->
-                     (c == "*" || C8.pack c `H.noCaseEq` C8.pack coding) && q /= Just 0)
-
-
--- |Get the header \"Content-Type\" as
--- 'Network.HTTP.Lucu.MIMEType.MIMEType'.
-getContentType :: Resource (Maybe MIMEType)
+                 else
+                     case P.parseOnly p (A.toByteString ae) of
+                       Right xs → return $ map toTuple $ reverse $ sort xs
+                       Left  _  → abort $ mkAbortion' BadRequest
+                                        $ "Unparsable Accept-Encoding: " ⊕ A.toText ae
+    where
+      p = do xs ← acceptEncodingList
+             P.endOfInput
+             return xs
+
+      toTuple (AcceptEncoding {..})
+          = (aeEncoding, aeQValue)
+
+-- |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 (C8.pack "Content-Type")
+    = do cTypeM ← getHeader "Content-Type"
          case cTypeM of
            Nothing
-               -> return Nothing
+                return Nothing
            Just cType
-               -> case parse mimeTypeP (L8.fromChunks [cType]) of
-                    (# Success t, _ #) -> return $ Just t
-                    (# _        , _ #) -> abort BadRequest []
-                                          (Just $ "Unparsable Content-Type: " ++ C8.unpack cType)
-
+               → case P.parseOnly p (A.toByteString cType) of
+                    Right t → return $ Just t
+                    Left  _ → abort $ mkAbortion' BadRequest
+                                    $ "Unparsable Content-Type: " ⊕ A.toText cType
+    where
+      p = do t ← mimeType
+             P.endOfInput
+             return t
 
--- |Get the header \"Authorization\" as
--- 'Network.HTTP.Lucu.Authorization.AuthCredential'.
-getAuthorization :: Resource (Maybe AuthCredential)
+-- |Return the value of request header \"Authorization\" as
+-- 'AuthCredential'.
+getAuthorization  Resource (Maybe AuthCredential)
 getAuthorization
-    = do authM <- getHeader (C8.pack "Authorization")
+    = do authM ← getHeader "Authorization"
          case authM of
            Nothing
-               -> return Nothing
+                return Nothing
            Just auth
-               -> case parse authCredentialP (L8.fromChunks [auth]) of
-                    (# Success a, _ #) -> return $ Just a
-                    (# _        , _ #) -> return Nothing
-
-
-{- ExaminingRequest 時に使用するアクション群 -}
+               → case P.parseOnly p (A.toByteString auth) of
+                    Right ac → return $ Just ac
+                    Left  _  → return Nothing
+    where
+      p = do ac ← authCredential
+             P.endOfInput
+             return ac
 
 -- |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. It is an error to
--- compute 'foundEntity' if this is a POST request.
+-- a datum which was stored for the URI until now. For POST requests
+-- it raises an error.
 --
--- Computation of 'foundEntity' performs \"If-Match\" test or
--- \"If-None-Match\" test if possible. When those tests fail, the
--- computation of 'Resource' immediately aborts with status \"412
--- Precondition Failed\" or \"304 Not Modified\" depending on the
--- situation.
+-- '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 this is a GET or HEAD request, 'foundEntity' automatically puts
--- \"ETag\" and \"Last-Modified\" headers into the response.
-foundEntity :: ETag -> UTCTime -> Resource ()
+-- 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
-    = tag `seq` timeStamp `seq`
-      do driftTo ExaminingRequest
-
-         method <- getMethod
-         when (method == GET || method == HEAD)
-                  $ setHeader' (C8.pack "Last-Modified") (C8.pack $ HTTP.format timeStamp)
-         when (method == POST)
-                  $ abort InternalServerError []
-                        (Just "Illegal computation of foundEntity for a POST request.")
+    = do driftTo ExaminingRequest
+
+         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 GettingBody
+         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 (and can't) put \"Last-Modified\" header into
+-- 'foundETag' doesn't (nor can't) put \"Last-Modified\" header into
 -- the response.
 --
--- This action is not preferred. You should use 'foundEntity' whenever
--- possible.
-foundETag :: ETag -> Resource ()
+-- Using this function is discouraged. You should use 'foundEntity'
+-- whenever possible.
+foundETag ∷ ETag → Resource ()
 foundETag tag
-    = tag `seq`
-      do driftTo ExaminingRequest
+    = do driftTo ExaminingRequest
       
-         method <- getMethod
-         when (method == GET || method == HEAD)
-                  $ setHeader' (C8.pack "ETag") (C8.pack $ show tag)
-         when (method == POST)
-                  $ abort InternalServerError []
-                        (Just "Illegal computation of foundETag for POST request.")
+         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 (C8.pack "If-Match")
+         ifMatch ← getHeader "If-Match"
          case ifMatch of
-           Nothing    -> return ()
-           Just value -> if value == C8.pack "*" then
-                             return ()
-                         else
-                             case parse eTagListP (L8.fromChunks [value]) of
-                               (# Success tags, _ #)
-                                 -- tags の中に一致するものが無ければ
-                                 -- PreconditionFailed で終了。
-                                 -> when (not $ any (== tag) tags)
-                                    $ abort PreconditionFailed []
-                                          $! Just ("The entity tag doesn't match: " ++ C8.unpack value)
-                               (# _, _ #)
-                                   -> abort BadRequest [] $! Just ("Unparsable If-Match: " ++ C8.unpack value)
-
-         let statusForNoneMatch = if method == GET || method == HEAD then
-                                      NotModified
-                                  else
-                                      PreconditionFailed
+           Nothing    → return ()
+           Just value → if value ≡ "*" then
+                            return ()
+                        else
+                            case P.parseOnly p (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 (C8.pack "If-None-Match")
+         ifNoneMatch ← getHeader "If-None-Match"
          case ifNoneMatch of
-           Nothing    -> return ()
-           Just value -> if value == C8.pack "*" then
-                             abort statusForNoneMatch [] $! Just ("The entity tag matches: *")
-                         else
-                             case parse eTagListP (L8.fromChunks [value]) of
-                               (# Success tags, _ #)
-                                   -> when (any (== tag) tags)
-                                      $ abort statusForNoneMatch [] $! Just ("The entity tag matches: " ++ C8.unpack value)
-                               (# _, _ #)
-                                   -> abort BadRequest [] $! Just ("Unparsable If-None-Match: " ++ C8.unpack value)
-
-         driftTo GettingBody
+           Nothing    → return ()
+           Just value → if value ≡ "*" then
+                            abort $ mkAbortion' statusForNoneMatch
+                                  $ "The entity tag matches: *"
+                        else
+                            case P.parseOnly p (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
+    where
+      p = do xs ← eTagList
+             P.endOfInput
+             return xs
 
 -- |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 last
+-- \"If-None-Match\" test. Be aware that any tests based on last
 -- modification time are unsafe because it is possible to mess up such
 -- tests by modifying the entity twice in a second.
 --
--- This action is not preferred. You should use 'foundEntity' whenever
--- possible.
-foundTimeStamp :: UTCTime -> Resource ()
+-- Using this function is discouraged. You should use 'foundEntity'
+-- whenever possible.
+foundTimeStamp ∷ UTCTime → Resource ()
 foundTimeStamp timeStamp
-    = timeStamp `seq`
-      do driftTo ExaminingRequest
-
-         method <- getMethod
-         when (method == GET || method == HEAD)
-                  $ setHeader' (C8.pack "Last-Modified") (C8.pack $ HTTP.format timeStamp)
-         when (method == POST)
-                  $ abort InternalServerError []
-                        (Just "Illegal computation of foundTimeStamp for POST request.")
-
-         let statusForIfModSince = if method == GET || method == HEAD then
-                                       NotModified
-                                   else
-                                       PreconditionFailed
+    = do driftTo ExaminingRequest
+
+         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
 
          -- If-Modified-Since があればそれを見る。
-         ifModSince <- getHeader (C8.pack "If-Modified-Since")
+         ifModSince ← getHeader "If-Modified-Since"
          case ifModSince of
-           Just str -> case HTTP.parse (C8.unpack str) of
-                         Just lastTime
-                             -> when (timeStamp <= lastTime)
-                                $ abort statusForIfModSince []
-                                      $! Just ("The entity has not been modified since " ++ C8.unpack 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 (C8.pack "If-Unmodified-Since")
+         ifUnmodSince ← getHeader "If-Unmodified-Since"
          case ifUnmodSince of
-           Just str -> case HTTP.parse (C8.unpack str) of
-                         Just lastTime
-                             -> when (timeStamp > lastTime)
-                                $ abort PreconditionFailed []
-                                      $! Just  ("The entity has not been modified since " ++ C8.unpack str)
-                         Nothing
-                             -> return () -- 不正な時刻は無視
-           Nothing  -> return ()
-
-         driftTo GettingBody
-
--- | Computation of @'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.
+           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 this is a PUT request, 'foundNoEntity' performs \"If-Match\"
--- test and aborts with status \"412 Precondition Failed\" when it
--- failed. If this is a GET, HEAD, POST or DELETE request,
+-- 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 String -> Resource ()
+foundNoEntity ∷ Maybe Text → Resource ()
 foundNoEntity msgM
-    = msgM `seq`
-      do driftTo ExaminingRequest
+    = do driftTo ExaminingRequest
 
-         method <- getMethod
-         when (method /= PUT)
-                  $ abort NotFound [] msgM
+         method ← getMethod
+         when (method ≢ PUT)
+             $ abort
+             $ mkAbortion NotFound [] msgM
 
          -- エンティティが存在しないと云ふ事は、"*" も含めたどのやうな
          -- If-Match: 條件も滿たさない。
-         ifMatch <- getHeader (C8.pack "If-Match")
-         when (ifMatch /= Nothing)
-                  $ abort PreconditionFailed [] msgM
-
-         driftTo GettingBody
-
-
-{- GettingBody 時に使用するアクション群 -}
-
--- | Computation of @'input' limit@ attempts to read the request body
--- up to @limit@ bytes, and then make the 'Resource' transit to
--- /Deciding Header/ state. When the actual size of body is larger
--- than @limit@ bytes, computation of 'Resource' immediately aborts
--- with status \"413 Request Entity Too Large\". When the request has
--- no body, 'input' returns an empty string.
+         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.
 --
--- @limit@ may be less than or equal to zero. In this case, the
--- default limitation value
--- ('Network.HTTP.Lucu.Config.cnfMaxEntityLength') is used. See
--- 'defaultLimit'.
+-- When the @limit@ is 'Nothing', 'getChunks' uses the default
+-- limitation value ('cnfMaxEntityLength') instead.
 --
--- Note that 'inputLBS' is more efficient than 'input' so you should
--- use it whenever possible.
-input :: Int -> Resource String
-input limit = limit `seq`
-              inputLBS limit >>= return . L8.unpack
-
-
--- | This is mostly the same as 'input' but is more
--- efficient. 'inputLBS' returns a 'Data.ByteString.Lazy.ByteString'
--- but it's not really lazy: reading from the socket just happens at
--- the computation of 'inputLBS', not at the evaluation of the
--- 'Data.ByteString.Lazy.ByteString'. The same goes for
--- 'inputChunkLBS'.
-inputLBS :: Int -> Resource Lazy.ByteString
-inputLBS limit
-    = limit `seq`
-      do driftTo GettingBody
-         itr     <- getInteraction
-         hasBody <- liftIO $! atomically $! readItr itr itrRequestHasBody id
-         chunk   <- if hasBody then
-                        askForInput itr
-                    else
-                        do driftTo DecidingHeader
-                           return L8.empty
-         return chunk
+-- '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 Lazy.ByteString
-      askForInput itr
-          = itr `seq`
-            do let confLimit   = cnfMaxEntityLength $ itrConfig itr
-                   actualLimit = if limit <= 0 then
-                                     confLimit
-                                 else
-                                     limit
-               when (actualLimit <= 0)
-                        $ fail ("inputLBS: 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 L8.length chunk < fromIntegral actualLimit then
-                                  -- 要求された量に滿たなくて、まだ殘り
-                                  -- があるなら再試行。
-                                  unless chunkIsOver
-                                             $ retry
-                                else
-                                  -- 制限値一杯まで讀むやうに指示したの
-                                  -- にまだ殘ってゐるなら、それは多過ぎ
-                                  -- る。
-                                  unless chunkIsOver
-                                             $ tooLarge actualLimit
-                              -- 成功。itr 内にチャンクを置いたままにす
-                              -- るとメモリの無駄になるので除去。
-                              writeItr itr itrReceivedBody L8.empty
-                              return chunk
-               driftTo DecidingHeader
-               return chunk
-
-      tooLarge :: Int -> STM ()
-      tooLarge lim = lim `seq`
-                     abortSTM RequestEntityTooLarge []
-                     $! Just ("Request body must be smaller than "
-                              ++ show lim ++ " bytes.")
-         
--- | Computation of @'inputChunk' limit@ attempts to read a part of
--- request body up to @limit@ bytes. You can read any large request by
--- repeating computation of this action. When you've read all the
--- request body, 'inputChunk' returns an empty string and then make
--- the 'Resource' transit to /Deciding Header/ state.
---
--- @limit@ may be less than or equal to zero. In this case, the
--- default limitation value
--- ('Network.HTTP.Lucu.Config.cnfMaxEntityLength') is used. See
--- 'defaultLimit'.
---
--- Note that 'inputChunkLBS' is more efficient than 'inputChunk' so you
--- should use it whenever possible.
-inputChunk :: Int -> Resource String
-inputChunk limit = limit `seq`
-                   inputChunkLBS limit >>= return . L8.unpack
-
-
--- | This is mostly the same as 'inputChunk' but is more
--- efficient. See 'inputLBS'.
-inputChunkLBS :: Int -> Resource Lazy.ByteString
-inputChunkLBS limit
-    = limit `seq`
-      do driftTo GettingBody
-         itr     <- getInteraction
-         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
-                        do driftTo DecidingHeader
-                           return L8.empty
-         return chunk
-    where
-      askForInput :: Interaction -> Resource Lazy.ByteString
-      askForInput itr
-          = itr `seq`
-            do let confLimit   = cnfMaxEntityLength $! itrConfig itr
-                   actualLimit = if limit < 0 then
-                                      confLimit
-                                  else
-                                      limit
-               when (actualLimit <= 0)
-                        $ fail ("inputChunkLBS: 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 (L8.length chunk < fromIntegral actualLimit)
-                                      $ do chunkIsOver <- readItr itr itrReqChunkIsOver id
-                                           unless chunkIsOver
-                                                      $ retry
-                             -- 成功
-                             writeItr itr itrReceivedBody L8.empty
-                             return chunk
-               when (L8.null chunk)
-                        $ driftTo DecidingHeader
-               return chunk
-
--- | Computation of @'inputForm' limit@ attempts to read the request
--- body with 'input' and parse it as
--- application\/x-www-form-urlencoded or multipart\/form-data. If the
--- request header \"Content-Type\" is neither of them, 'inputForm'
--- makes 'Resource' abort with status \"415 Unsupported Media
--- Type\". If the request has no \"Content-Type\", it aborts with
--- \"400 Bad Request\".
-inputForm :: Int -> Resource [(String, FormData)]
-inputForm limit
-    = limit `seq` 
-      do cTypeM <- getContentType
+                        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 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 BadRequest [] (Just "Missing Content-Type")
+               → abort $ mkAbortion' BadRequest "Missing Content-Type"
            Just (MIMEType "application" "x-www-form-urlencoded" _)
-               -> readWWWFormURLEncoded
+                readWWWFormURLEncoded
            Just (MIMEType "multipart" "form-data" params)
-               -> readMultipartFormData params
+                readMultipartFormData params
            Just cType
-               -> abort UnsupportedMediaType [] (Just $! "Unsupported media type: "
-                                                          ++ show cType)
+               → abort $ mkAbortion' UnsupportedMediaType
+                       $ A.toText
+                       $ A.fromAsciiBuilder
+                       $ A.toAsciiBuilder "Unsupported media type: "
+                       ⊕ printMIMEType cType
     where
       readWWWFormURLEncoded
-          = liftM (map toPairWithFormData . parseWWWFormURLEncoded) (input limit)
+          = (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 params
-          = do case find ((== "boundary") . map toLower . fst) params of
-                 Nothing
-                     -> abort BadRequest [] (Just "Missing boundary of multipart/form-data")
-                 Just (_, boundary)
-                     -> do src <- inputLBS limit
-                           case parse (multipartFormP boundary) src of
-                             (# Success formList, _ #)
-                                 -> return formList
-                             (# _, _ #)
-                                 -> abort BadRequest [] (Just "Unparsable multipart/form-data")
-
--- | This is just a constant @-1@. It's better to say @'input'
--- 'defaultLimit'@ than to say @'input' (-1)@ but these are exactly
--- the same.
-defaultLimit :: Int
-defaultLimit = (-1)
-
-
-
-{- DecidingHeader 時に使用するアクション群 -}
-
--- | Set the response status code. If you omit to compute this action,
--- the status code will be defaulted to \"200 OK\".
-setStatus :: StatusCode -> Resource ()
-setStatus code
-    = code `seq`
-      do driftTo DecidingHeader
-         itr <- getInteraction
-         liftIO $! atomically $! updateItr itr itrResponse
-                    $! \ res -> res {
-                                  resStatus = code
-                                }
-
--- | Set a value of given resource header. Comparison of header name
--- is case-insensitive. Note that this action is not intended to be
--- used so frequently: there should be actions like 'setContentType'
--- for every common headers.
---
--- Some important headers (especially \"Content-Length\" and
--- \"Transfer-Encoding\") may be silently dropped or overwritten by
--- the system not to corrupt the interaction with client at the
--- viewpoint of HTTP protocol layer. For instance, if we are keeping
--- the connection alive, without this process it causes a catastrophe
--- to send a header \"Content-Length: 10\" and actually send a body of
--- 20 bytes long. In this case the client shall only accept the first
--- 10 bytes of response body and thinks that the residual 10 bytes is
--- a part of header of the next response.
-setHeader :: Strict.ByteString -> Strict.ByteString -> Resource ()
-setHeader name value
-    = name `seq` value `seq`
-      driftTo DecidingHeader >> setHeader' name value
-         
-
-setHeader' :: Strict.ByteString -> Strict.ByteString -> Resource ()
-setHeader' name value
-    = name `seq` value `seq`
-      do itr <- getInteraction
-         liftIO $ atomically
-                    $ updateItr itr itrResponse
-                          $ H.setHeader name value
-
--- | Computation of @'redirect' code uri@ sets the response status to
--- @code@ and \"Location\" header to @uri@. The @code@ must satisfy
--- 'Network.HTTP.Lucu.Response.isRedirection' or it causes an error.
-redirect :: StatusCode -> URI -> Resource ()
+          = case M.lookup "boundary" params 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
-    = code `seq` uri `seq`
-      do when (code == NotModified || not (isRedirection code))
-                  $ abort InternalServerError []
-                        $! Just ("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
          setLocation uri
-{-# INLINE redirect #-}
-
 
--- | Computation of @'setContentType' mType@ sets the response header
--- \"Content-Type\" to @mType@.
-setContentType :: MIMEType -> Resource ()
-setContentType mType
-    = setHeader (C8.pack "Content-Type") (C8.pack $ show mType)
+-- |@'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 ∘ printMIMEType
 
--- | Computation of @'setLocation' uri@ sets the response header
--- \"Location\" to @uri@.
-setLocation :: URI -> Resource ()
+-- |@'setLocation' uri@ declares the response header \"Location\" as
+-- @uri@. You usually don't need to call this function directly.
+setLocation ∷ URI → Resource ()
 setLocation uri
-    = setHeader (C8.pack "Location") (C8.pack $ uriToString id uri $ "")
+    = case A.fromChars uriStr of
+        Just a  → setHeader "Location" a
+        Nothing → abort $ mkAbortion' InternalServerError
+                        $ "Malformed URI: " ⊕ T.pack uriStr
+    where
+      uriStr = uriToString id uri ""
 
--- |Computation of @'setContentEncoding' codings@ sets the response
--- header \"Content-Encoding\" to @codings@.
-setContentEncoding :: [String] -> Resource ()
+-- |@'setContentEncoding' codings@ declares the response header
+-- \"Content-Encoding\" as @codings@.
+setContentEncoding ∷ [CIAscii] → Resource ()
 setContentEncoding codings
-    = do ver <- getRequestVersion
-         let tr = case ver of
-                    HttpVersion 1 0 -> unnormalizeCoding
-                    HttpVersion 1 1 -> id
-                    _               -> undefined
-         setHeader (C8.pack "Content-Encoding") (C8.pack $ joinWith ", " $ map tr codings)
-
--- |Computation of @'setWWWAuthenticate' challenge@ sets the response
--- header \"WWW-Authenticate\" to @challenge@.
-setWWWAuthenticate :: AuthChallenge -> Resource ()
-setWWWAuthenticate challenge
-    = setHeader (C8.pack "WWW-Authenticate") (C8.pack $ show challenge)
-
-
-{- DecidingBody 時に使用するアクション群 -}
-
--- | Computation of @'output' str@ writes @str@ as a response body,
--- and then make the 'Resource' transit to /Done/ state. It is safe to
--- apply 'output' to an infinite string, such as a lazy stream of
--- \/dev\/random.
---
--- Note that 'outputLBS' is more efficient than 'output' so you should
--- use it whenever possible.
-output :: String -> Resource ()
-output str = outputLBS $! L8.pack str
-{-# INLINE output #-}
-
--- | This is mostly the same as 'output' but is more efficient.
-outputLBS :: Lazy.ByteString -> Resource ()
-outputLBS str = do outputChunkLBS str
-                   driftTo Done
-{-# INLINE outputLBS #-}
-
--- | Computation of @'outputChunk' str@ writes @str@ as a part of
--- response body. You can compute this action multiple times to write
--- a body little at a time. It is safe to apply 'outputChunk' to an
--- infinite string.
---
--- Note that 'outputChunkLBS' is more efficient than 'outputChunk' so
--- you should use it whenever possible.
-outputChunk :: String -> Resource ()
-outputChunk str = outputChunkLBS $! L8.pack str
-{-# INLINE outputChunk #-}
-
--- | This is mostly the same as 'outputChunk' but is more efficient.
-outputChunkLBS :: Lazy.ByteString -> Resource ()
-outputChunkLBS wholeChunk
-    = wholeChunk `seq`
-      do driftTo DecidingBody
-         itr <- getInteraction
-         
-         let limit = cnfMaxOutputChunkLength $ itrConfig itr
-         when (limit <= 0)
-                  $ fail ("cnfMaxOutputChunkLength must be positive: "
-                          ++ show limit)
-
-         discardBody <- liftIO $ atomically $
-                        readItr itr itrWillDiscardBody id
-
-         unless (discardBody)
-                    $ sendChunks wholeChunk limit
-
-         unless (L8.null wholeChunk)
-                    $ liftIO $ atomically $
-                      writeItr itr itrBodyIsNull False
-    where
-      -- チャンクの大きさは Config で制限されてゐる。もし例へば
-      -- "/dev/zero" を L8.readFile して作った Lazy.ByteString をそのまま
-      -- ResponseWriter に渡したりすると大變な事が起こる。何故なら
-      -- ResponseWriter は Transfer-Encoding: chunked の時、ヘッダを書
-      -- く爲にチャンクの大きさを測る。
-      sendChunks :: Lazy.ByteString -> Int -> Resource ()
-      sendChunks str limit
-          | L8.null str = return ()
-          | otherwise   = do let (chunk, remaining) = L8.splitAt (fromIntegral limit) str
-                             itr <- getInteraction
-                             liftIO $ atomically $ 
-                                    do buf <- readItr itr itrBodyToSend id
-                                       if L8.null buf then
-                                           -- バッファが消化された
-                                           writeItr itr itrBodyToSend chunk
-                                         else
-                                           -- 消化されるのを待つ
-                                           retry
-                             -- 殘りのチャンクについて繰り返す
-                             sendChunks remaining limit
-
-{-
-
-  [GettingBody からそれ以降の状態に遷移する時]
-  
-  body を讀み終へてゐなければ、殘りの body を讀み捨てる。
-
-
-  [DecidingHeader からそれ以降の状態に遷移する時]
-
-  postprocess する。
-
-
-  [Done に遷移する時]
-
-  bodyIsNull が False ならば何もしない。True だった場合は出力補完す
-  る。
-
--}
-
-driftTo :: InteractionState -> Resource ()
-driftTo newState
-    = newState `seq`
-      do itr <- getInteraction
-         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
+    = 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
-      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)
+      toAB = A.toAsciiBuilder ∘ A.fromCIAscii
 
+-- |@'setWWWAuthenticate' challenge@ declares the response header
+-- \"WWW-Authenticate\" as @challenge@.
+setWWWAuthenticate ∷ AuthChallenge → Resource ()
+setWWWAuthenticate = setHeader "WWW-Authenticate" ∘ printAuthChallenge
 
-      drift :: Interaction -> InteractionState -> InteractionState -> STM ()
+-- |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
 
-      drift itr GettingBody _
-          = writeItr itr itrReqBodyWasteAll True
-
-      drift itr DecidingHeader _
-          = postprocess itr
-
-      drift itr _ Done
-          = do bodyIsNull <- readItr itr itrBodyIsNull id
-               when bodyIsNull
-                        $ writeDefaultPage itr
-
-      drift _ _ _
-          = return ()
+-- |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
diff --git a/Network/HTTP/Lucu/Resource/Internal.hs b/Network/HTTP/Lucu/Resource/Internal.hs
new file mode 100644 (file)
index 0000000..f43ec6c
--- /dev/null
@@ -0,0 +1,410 @@
+{-# LANGUAGE
+    DoAndIfThenElse
+  , GeneralizedNewtypeDeriving
+  , OverloadedStrings
+  , RecordWildCards
+  , UnicodeSyntax
+  #-}
+module Network.HTTP.Lucu.Resource.Internal
+    ( Resource
+    , ResourceDef(..)
+    , emptyResource
+    , spawnResource
+
+    , getConfig
+    , getRemoteAddr
+    , getRemoteCertificate
+    , getRequest
+    , getResourcePath
+
+    , getChunk
+
+    , setStatus
+    , setHeader
+    , deleteHeader
+
+    , putBuilder
+
+    , driftTo
+    )
+    where
+import Blaze.ByteString.Builder (Builder)
+import Control.Applicative
+import Control.Concurrent
+import Control.Concurrent.STM
+import Control.Exception
+import Control.Monad.IO.Class
+import Control.Monad.Reader
+import Control.Monad.Unicode
+import Data.Ascii (Ascii, CIAscii)
+import qualified Data.Ascii as A
+import qualified Data.ByteString as Strict
+import Data.List
+import Data.Maybe
+import Data.Monoid
+import Data.Monoid.Unicode
+import qualified Data.Text as T
+import Network.HTTP.Lucu.Abortion
+import Network.HTTP.Lucu.Abortion.Internal
+import Network.HTTP.Lucu.Config
+import Network.HTTP.Lucu.DefaultPage
+import qualified Network.HTTP.Lucu.Headers as H
+import Network.HTTP.Lucu.Interaction
+import Network.HTTP.Lucu.Postprocess
+import Network.HTTP.Lucu.Request
+import Network.HTTP.Lucu.Response
+import Network.Socket
+import OpenSSL.X509
+import Prelude hiding (catch)
+import Prelude.Unicode
+import System.IO
+
+-- |The 'Resource' monad. This monad implements 'MonadIO' so it can do
+-- any 'IO' actions.
+newtype Resource a
+    = Resource {
+        unResource ∷ ReaderT NormalInteraction IO a
+      }
+    deriving (Applicative, Functor, Monad, MonadIO)
+
+runResource ∷ Resource a → NormalInteraction → IO a
+runResource = runReaderT ∘ unResource
+
+-- |'ResourceDef' is basically a set of 'Resource' monads for each
+-- HTTP methods.
+data ResourceDef = ResourceDef {
+    -- |Whether to run a 'Resource' on a native thread (spawned by
+    -- 'forkOS') or to run it on a user thread (spanwed by
+    -- 'forkIO'). Generally you don't need to set this field to
+    -- 'True'.
+      resUsesNativeThread ∷ !Bool
+    -- | Whether to be greedy or not.
+    --
+    -- Say a client is trying to access \/aaa\/bbb\/ccc. If there is a
+    -- greedy resource at \/aaa\/bbb, it is always chosen even if
+    -- there is another resource at \/aaa\/bbb\/ccc. If the resource
+    -- at \/aaa\/bbb is not greedy, it is just ignored. Greedy
+    -- resources are like CGI scripts.
+    , resIsGreedy         ∷ !Bool
+    -- |A 'Resource' to be run when a GET request comes for the
+    -- resource path. If 'resGet' is Nothing, the system responds
+    -- \"405 Method Not Allowed\" for GET requests.
+    --
+    -- It also runs for HEAD request if the 'resHead' is 'Nothing'. In
+    -- that case 'putChunk' and such don't actually write a response
+    -- body.
+    , resGet              ∷ !(Maybe (Resource ()))
+    -- |A 'Resource' to be run when a HEAD request comes for the
+    -- resource path. If 'resHead' is Nothing, the system runs
+    -- 'resGet' instead. If 'resGet' is also Nothing, the system
+    -- responds \"405 Method Not Allowed\" for HEAD requests.
+    , resHead             ∷ !(Maybe (Resource ()))
+    -- |A 'Resource' to be run when a POST request comes for the
+    -- resource path. If 'resPost' is Nothing, the system responds
+    -- \"405 Method Not Allowed\" for POST requests.
+    , resPost             ∷ !(Maybe (Resource ()))
+    -- |A 'Resource' to be run when a PUT request comes for the
+    -- resource path. If 'resPut' is Nothing, the system responds
+    -- \"405 Method Not Allowed\" for PUT requests.
+    , resPut              ∷ !(Maybe (Resource ()))
+    -- |A 'Resource' to be run when a DELETE request comes for the
+    -- resource path. If 'resDelete' is Nothing, the system responds
+    -- \"405 Method Not Allowed\" for DELETE requests.
+    , resDelete           ∷ !(Maybe (Resource ()))
+    }
+
+-- |'emptyResource' is a resource definition with no actual
+-- handlers. You can construct a 'ResourceDef' by selectively
+-- overriding 'emptyResource'. It is defined as follows:
+--
+-- @
+--   emptyResource = ResourceDef {
+--                     resUsesNativeThread = False
+--                   , resIsGreedy         = False
+--                   , resGet              = Nothing
+--                   , resHead             = Nothing
+--                   , resPost             = Nothing
+--                   , resPut              = Nothing
+--                   , resDelete           = Nothing
+--                   }
+-- @
+emptyResource ∷ ResourceDef
+emptyResource = ResourceDef {
+                  resUsesNativeThread = False
+                , resIsGreedy         = False
+                , resGet              = Nothing
+                , resHead             = Nothing
+                , resPost             = Nothing
+                , resPut              = Nothing
+                , resDelete           = Nothing
+                }
+
+spawnResource ∷ ResourceDef → NormalInteraction → IO ThreadId
+spawnResource (ResourceDef {..}) ni@(NI {..})
+    = fork $ run `catch` processException
+    where
+      fork ∷ IO () → IO ThreadId
+      fork | resUsesNativeThread = forkOS
+           | otherwise           = forkIO
+
+      run ∷ IO ()
+      run = flip runResource ni $
+            do req ← getRequest
+               fromMaybe notAllowed $ rsrc req
+               driftTo Done
+
+      rsrc ∷ Request → Maybe (Resource ())
+      rsrc req
+          = case reqMethod req of
+              GET    → resGet
+              HEAD   → case resHead of
+                          Just r  → Just r
+                          Nothing → resGet
+              POST   → resPost
+              PUT    → resPut
+              DELETE → resDelete
+              _      → error $ "Unknown request method: " ⧺ show (reqMethod req)
+
+      notAllowed ∷ Resource ()
+      notAllowed = do setStatus MethodNotAllowed
+                      setHeader "Allow"
+                          $ A.fromAsciiBuilder
+                          $ mconcat
+                          $ intersperse (A.toAsciiBuilder ", ")
+                          $ map A.toAsciiBuilder allowedMethods
+
+      allowedMethods ∷ [Ascii]
+      allowedMethods = nub $ concat [ methods resGet    ["GET"]
+                                    , methods resHead   ["GET", "HEAD"]
+                                    , methods resPost   ["POST"]
+                                    , methods resPut    ["PUT"]
+                                    , methods resDelete ["DELETE"]
+                                    ]
+
+      methods ∷ Maybe a → [Ascii] → [Ascii]
+      methods m xs
+          | isJust m  = xs
+          | otherwise = []
+
+      toAbortion ∷ SomeException → Abortion
+      toAbortion e
+          = case fromException e of
+              Just abortion → abortion
+              Nothing       → mkAbortion' InternalServerError $ T.pack $ show e
+
+      processException ∷ SomeException → IO ()
+      processException exc
+          = do let abo = toAbortion exc
+               state ← atomically $ readTVar niState
+               res   ← atomically $ readTVar niResponse
+               if state ≤ DecidingHeader then
+                   -- We still have a chance to reflect this abortion
+                   -- in the response. Hooray!
+                   flip runResource ni $
+                       do setStatus $ aboStatus abo
+                          mapM_ (uncurry setHeader) $ H.fromHeaders $ aboHeaders abo
+                          setHeader "Content-Type" defaultPageContentType
+                          deleteHeader "Content-Encoding"
+                          putBuilder $ abortPage niConfig (Just niRequest) res abo
+               else
+                   when (cnfDumpTooLateAbortionToStderr niConfig)
+                       $ dumpAbortion abo
+               runResource (driftTo Done) ni
+
+dumpAbortion ∷ Abortion → IO ()
+dumpAbortion abo
+    = hPutStr stderr
+      $ concat [ "Lucu: an exception occured after "
+               , "sending the response header to the client:\n"
+               , "  ", show abo, "\n"
+               ]
+
+getInteraction ∷ Resource NormalInteraction
+getInteraction = Resource ask
+
+-- |Get the 'Config' value for this httpd.
+getConfig ∷ Resource Config
+getConfig = niConfig <$> getInteraction
+
+-- |Get the 'SockAddr' of the remote host.
+getRemoteAddr ∷ Resource SockAddr
+getRemoteAddr = niRemoteAddr <$> getInteraction
+
+-- | Return the X.509 certificate of the client, or 'Nothing' if:
+--
+--   * This request didn't came through an SSL stream.
+--
+--   * The client didn't send us its certificate.
+--
+--   * The 'OpenSSL.Session.VerificationMode' of
+--   'OpenSSL.Session.SSLContext' in 'SSLConfig' has not been set to
+--   'OpenSSL.Session.VerifyPeer'.
+getRemoteCertificate ∷ Resource (Maybe X509)
+getRemoteCertificate = niRemoteCert <$> getInteraction
+
+-- |Return the 'Request' value representing the request header. You
+-- usually don't need to call this function directly.
+getRequest ∷ Resource Request
+getRequest = niRequest <$> getInteraction
+
+-- |Get the path of this 'Resource' (to be exact, 'ResourceDef') in
+-- the 'Network.HTTP.Lucu.Resource.Tree.ResTree'. The result of this
+-- action is the exact path in the tree even when the 'ResourceDef' is
+-- greedy.
+--
+-- Example:
+--
+-- > main = let tree = mkResTree [ (["foo"], resFoo) ]
+-- >        in runHttpd defaultConfig tree []
+-- >
+-- > resFoo = emptyResource {
+-- >     resIsGreedy = True
+-- >   , resGet = Just $ do requestURI   <- getRequestURI
+-- >                        resourcePath <- getResourcePath
+-- >                        pathInfo     <- getPathInfo
+-- >                        -- uriPath requestURI == "/foo/bar/baz"
+-- >                        -- resourcePath       == ["foo"]
+-- >                        -- pathInfo           == ["bar", "baz"]
+-- >                        ...
+-- >   }
+getResourcePath ∷ Resource [Strict.ByteString]
+getResourcePath = niResourcePath <$> getInteraction
+
+-- |@'getChunk' n@ attempts to read a chunk of request body up to @n@
+-- bytes. You can incrementally read the request body by repeatedly
+-- calling this function. If there is nothing to be read anymore,
+-- 'getChunk' returns 'Strict.empty' and makes 'Resource' transit to
+-- the /Deciding Header/ state.
+getChunk ∷ Int → Resource Strict.ByteString
+getChunk = (driftTo ReceivingBody *>) ∘ getChunk'
+
+getChunk' ∷ Int → Resource Strict.ByteString
+getChunk' n
+    | n < 0     = fail ("getChunk: n must not be negative: " ⧺ show n)
+    | n ≡ 0     = return (∅)
+    | otherwise = do req ← getRequest
+                     if reqMustHaveBody req then
+                         askForInput =≪ getInteraction
+                     else
+                         driftTo DecidingHeader *> return (∅)
+    where
+      askForInput ∷ NormalInteraction → Resource Strict.ByteString
+      askForInput (NI {..})
+          = do -- Ask the RequestReader to get a chunk.
+               liftIO $ atomically
+                      $ putTMVar niReceiveBodyReq (ReceiveBody n)
+               -- Then wait for a reply.
+               chunk ← liftIO
+                       $ atomically
+                       $ takeTMVar niReceivedBody
+               -- Have we got an EOF?
+               when (Strict.null chunk)
+                   $ driftTo DecidingHeader
+               return chunk
+
+-- |Declare the response status code. If you don't call this function,
+-- the status code will be defaulted to \"200 OK\".
+setStatus ∷ StatusCode → Resource ()
+setStatus sc
+    = do ni ← getInteraction
+         liftIO $ atomically
+                $ do state ← readTVar $ niState ni
+                     when (state > DecidingHeader)
+                         $ fail "Too late to declare the response status."
+                     setResponseStatus ni sc
+
+-- |@'setHeader' name value@ declares the value of the response header
+-- @name@ as @value@. Note that this function is not intended to be
+-- used so frequently: there should be specialised functions like
+-- 'setContentType' for every common headers.
+--
+-- Some important headers (especially \"Content-Length\" and
+-- \"Transfer-Encoding\") may be silently dropped or overwritten by
+-- the system not to corrupt the interaction with client at the
+-- viewpoint of HTTP protocol layer. For instance, if we are keeping
+-- the connection alive, without this manipulation it will be a
+-- catastrophe when we send a header \"Content-Length: 10\" and
+-- actually send a body of 20 bytes long to the remote peer. In this
+-- case the client shall only accept the first 10 bytes of response
+-- body and thinks that the residual 10 bytes is a part of the header
+-- of the next response.
+setHeader ∷ CIAscii → Ascii → Resource ()
+setHeader name value = liftIO ∘ atomically ∘ go =≪ getInteraction
+    where
+      go ∷ NormalInteraction → STM ()
+      go (NI {..})
+          = do state ← readTVar niState
+               when (state > DecidingHeader) $
+                   fail "Too late to declare a response header field."
+               res ← readTVar niResponse
+               writeTVar niResponse $ H.setHeader name value res
+               when (name ≡ "Content-Type") $
+                   writeTVar niResponseHasCType True
+
+-- |@'deleteHeader' name@ deletes a response header @name@ if
+-- any. This function is not intended to be used so frequently.
+deleteHeader ∷ CIAscii → Resource ()
+deleteHeader name = liftIO ∘ atomically ∘ go =≪ getInteraction
+    where
+      go ∷ NormalInteraction → STM ()
+      go (NI {..})
+          = do state ← readTVar niState
+               when (state > DecidingHeader) $
+                   fail "Too late to delete a response header field."
+               res ← readTVar niResponse
+               writeTVar niResponse $ H.deleteHeader name res
+               when (name ≡ "Content-Type") $
+                   writeTVar niResponseHasCType False
+
+-- |Run a 'Builder' to construct a chunk, and write it to the response
+-- body. It can be safely applied to a 'Builder' producing an
+-- infinitely long stream of octets.
+--
+-- Note that you must first declare the response header
+-- \"Content-Type\" before applying this function. See:
+-- 'setContentType'
+putBuilder ∷ Builder → Resource ()
+putBuilder b = liftIO ∘ atomically ∘ go =≪ getInteraction
+    where
+      go ∷ NormalInteraction → STM ()
+      go ni@(NI {..})
+          = do driftTo' ni SendingBody
+               hasCType ← readTVar niResponseHasCType
+               unless hasCType
+                   $ throwSTM
+                   $ mkAbortion' InternalServerError
+                     "putBuilder: Content-Type has not been set."
+               putTMVar niBodyToSend b
+
+driftTo ∷ InteractionState → Resource ()
+driftTo = (getInteraction ≫=) ∘ ((liftIO ∘ atomically) ∘) ∘ flip driftTo'
+
+driftTo' ∷ NormalInteraction → InteractionState → STM ()
+driftTo' ni@(NI {..}) newState
+    = do oldState ← readTVar niState
+         driftFrom oldState
+    where
+      driftFrom ∷ InteractionState → STM ()
+      driftFrom oldState
+          | newState < oldState = throwStateError oldState newState
+          | newState ≡ oldState = return ()
+          | otherwise
+              = do let a = [oldState .. newState]
+                       b = tail a
+                       c = zip a b
+                   mapM_ (uncurry driftFromTo) c
+                   writeTVar niState newState
+
+      throwStateError ∷ Monad m ⇒ InteractionState → InteractionState → m a
+      throwStateError Done SendingBody
+          = fail "It makes no sense to output something after finishing outputs."
+      throwStateError old new
+          = fail ("State error: " ⧺ show old ⧺ " ==> " ⧺ show new)
+
+      driftFromTo ∷ InteractionState → InteractionState → STM ()
+      driftFromTo ReceivingBody _
+          = putTMVar niReceiveBodyReq WasteAll
+      driftFromTo DecidingHeader _
+          = postprocess ni
+      driftFromTo _ _
+          = return ()
index 4cb493274859cd7aa4fb56ca4664123dff0d5867..f3fca16b50520ff154775e1bcc3db58918a09ba9 100644 (file)
-{-# OPTIONS_HADDOCK prune #-}
+{-# LANGUAGE
+    DoAndIfThenElse
+  , OverloadedStrings
+  , RecordWildCards
+  , UnicodeSyntax
+  #-}
 
 -- | Repository of the resources in httpd.
 module Network.HTTP.Lucu.Resource.Tree
-    ( ResourceDef(..)
-    , emptyResource
-
-    , ResTree
+    ( ResTree
     , FallbackHandler
 
-    , mkResTree    -- [ ([String], ResourceDef) ] -> ResTree
-
-    , findResource -- ResTree -> URI -> Maybe ([String], ResourceDef)
-    , runResource  -- ResourceDef -> Interaction -> IO ThreadId
+    , mkResTree
+    , findResource
     )
     where
-
-import           Control.Arrow
-import           Control.Concurrent
-import           Control.Concurrent.STM
-import           Control.Exception
-import           Control.Monad
-import qualified Data.ByteString.Char8 as C8
-import           Data.List
+import Control.Arrow
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import Control.Monad
+import Data.Foldable
+import Data.List
 import qualified Data.Map as M
-import           Data.Map (Map)
-import           Data.Maybe
-import           Network.HTTP.Lucu.Abortion
-import           Network.HTTP.Lucu.Config
-import           Network.HTTP.Lucu.Headers (emptyHeaders, fromHeaders)
-import           Network.HTTP.Lucu.Request
-import           Network.HTTP.Lucu.Resource
-import           Network.HTTP.Lucu.Response
-import           Network.HTTP.Lucu.Interaction
-import           Network.HTTP.Lucu.Utils
-import           Network.URI hiding (path)
-import           System.IO
-import           Prelude hiding (catch)
-
+import Data.Map (Map)
+import Data.Maybe
+import Data.Monoid.Unicode
+import Data.Sequence (Seq)
+import Data.Sequence.Unicode hiding ((∅))
+import Network.HTTP.Lucu.Resource.Internal
+import Network.HTTP.Lucu.Utils
+import Network.URI hiding (path)
+import System.IO
+import Prelude hiding (catch)
+import Prelude.Unicode
 
 -- |'FallbackHandler' is an extra resource handler for resources which
--- can't be statically located somewhere in the resource tree. The
--- Lucu httpd first search for a resource in the tree, and then call
+-- can't be statically located anywhere in the resource tree. The Lucu
+-- httpd first searches for a resource in the tree, and then calls
 -- fallback handlers to ask them for a resource. If all of the
--- handlers returned 'Prelude.Nothing', the httpd responds with 404
--- Not Found.
-type FallbackHandler = [String] -> IO (Maybe ResourceDef)
-
-
--- "/aaa/bbb/ccc" にアクセスされた時、もし "/aaa/bbb" に貪欲なリソース
--- があれば、假に "/aaa/bbb/ccc" に何らかのリソースがあったとしても必ず
--- "/aaa/bbb" が撰ばれる。"/aaa/bbb" のリソースが貪欲でなければ、それは
--- 無視される。
-
--- | 'ResourceDef' is basically a set of
--- 'Network.HTTP.Lucu.Resource.Resource' monads for each HTTP methods.
-data ResourceDef = ResourceDef {
-    -- |Whether to run a 'Network.HTTP.Lucu.Resource.Resource' on a
-    -- native thread (spawned by 'Control.Concurrent.forkOS') or to
-    -- run it on a user thread (spanwed by
-    -- 'Control.Concurrent.forkIO'). Generally you don't need to set
-    -- this field to 'Prelude.True'.
-      resUsesNativeThread :: !Bool
-    -- | Whether to be greedy or not.
-    -- 
-    -- Say a client is trying to access \/aaa\/bbb\/ccc. If there is a
-    -- greedy resource at \/aaa\/bbb, it is always chosen even if
-    -- there is another resource at \/aaa\/bbb\/ccc. If the resource
-    -- at \/aaa\/bbb is not greedy, it is just ignored. Greedy
-    -- resources are like CGI scripts.
-    , resIsGreedy         :: !Bool
-    -- | A 'Network.HTTP.Lucu.Resource.Resource' to be run when a GET
-    -- request comes for the resource path. If 'resGet' is Nothing,
-    -- the system responds \"405 Method Not Allowed\" for GET
-    -- requests.
-    -- 
-    -- It also runs for HEAD request if the 'resHead' is Nothing. In
-    -- this case 'Network.HTTP.Lucu.Resource.output' and such like
-    -- don't actually write a response body.
-    , resGet              :: !(Maybe (Resource ()))
-    -- | A 'Network.HTTP.Lucu.Resource.Resource' to be run when a HEAD
-    -- request comes for the resource path. If 'resHead' is Nothing,
-    -- the system runs 'resGet' instead. If 'resGet' is also Nothing,
-    -- the system responds \"405 Method Not Allowed\" for HEAD
-    -- requests.
-    , resHead             :: !(Maybe (Resource ()))
-    -- | A 'Network.HTTP.Lucu.Resource.Resource' to be run when a POST
-    -- request comes for the resource path. If 'resPost' is Nothing,
-    -- the system responds \"405 Method Not Allowed\" for POST
-    -- requests.
-    , resPost             :: !(Maybe (Resource ()))
-    -- | A 'Network.HTTP.Lucu.Resource.Resource' to be run when a PUT
-    -- request comes for the resource path. If 'resPut' is Nothing,
-    -- the system responds \"405 Method Not Allowed\" for PUT
-    -- requests.
-    , resPut              :: !(Maybe (Resource ()))
-    -- | A 'Network.HTTP.Lucu.Resource.Resource' to be run when a
-    -- DELETE request comes for the resource path. If 'resDelete' is
-    -- Nothing, the system responds \"405 Method Not Allowed\" for
-    -- DELETE requests.
-    , resDelete           :: !(Maybe (Resource ()))
-    }
-
--- |'emptyResource' is a resource definition with no actual
--- handlers. You can construct a 'ResourceDef' by selectively
--- overriding 'emptyResource'. It is defined as follows:
---
--- @
---   emptyResource = ResourceDef {
---                     resUsesNativeThread = False
---                   , resIsGreedy         = False
---                   , resGet              = Nothing
---                   , resHead             = Nothing
---                   , resPost             = Nothing
---                   , resPut              = Nothing
---                   , resDelete           = Nothing
---                   }
--- @
-emptyResource :: ResourceDef
-emptyResource = ResourceDef {
-                  resUsesNativeThread = False
-                , resIsGreedy         = False
-                , resGet              = Nothing
-                , resHead             = Nothing
-                , resPost             = Nothing
-                , resPut              = Nothing
-                , resDelete           = Nothing
-                }
+-- handlers returned 'Nothing', the httpd responds with 404 Not Found.
+type FallbackHandler = [ByteString] → IO (Maybe ResourceDef)
 
 -- |'ResTree' is an opaque structure which is a map from resource path
 -- to 'ResourceDef'.
 newtype ResTree = ResTree ResNode -- root だから Map ではない
-type ResSubtree = Map String ResNode
+type ResSubtree = Map ByteString ResNode
 data ResNode    = ResNode (Maybe ResourceDef) ResSubtree
 
 -- |'mkResTree' converts a list of @(path, def)@ to a 'ResTree' e.g.
@@ -140,15 +53,21 @@ data ResNode    = ResNode (Maybe ResourceDef) ResSubtree
 --             , ([\"unistd\"], 'Network.HTTP.Lucu.StaticFile.staticFile' \"\/usr\/include\/unistd.h\") -- \/unistd
 --             ]
 -- @
-mkResTree :: [ ([String], ResourceDef) ] -> ResTree
-mkResTree = processRoot . map (first canonicalisePath)
+--
+-- Note that path components are always represented as octet streams
+-- in this system. Lucu automatically decodes percent-encoded URIs but
+-- has no involvement in character encodings such as UTF-8, since RFC
+-- 2616 (HTTP/1.1) says nothing about character encodings to be used
+-- in \"http\" and \"https\" URI schemas.
+mkResTree ∷ [ ([ByteString], ResourceDef) ] → ResTree
+mkResTree = processRoot ∘ map (first canonicalisePath)
     where
-      canonicalisePath :: [String] -> [String]
-      canonicalisePath = filter (/= "")
+      canonicalisePath ∷ [ByteString] → [ByteString]
+      canonicalisePath = filter ((¬) ∘ BS.null)
 
-      processRoot :: [ ([String], ResourceDef) ] -> ResTree
+      processRoot ∷ [ ([ByteString], ResourceDef) ] → ResTree
       processRoot list
-          = let (roots, nonRoots) = partition (\ (path, _) -> path == []) list
+          = let (roots, nonRoots) = partition (\(path, _) → null path) list
                 children = processNonRoot nonRoots
             in
               if null roots then
@@ -161,12 +80,12 @@ mkResTree = processRoot . map (first canonicalisePath)
                   in 
                     ResTree (ResNode (Just def) children)
 
-      processNonRoot :: [ ([String], ResourceDef) ] -> ResSubtree
+      processNonRoot ∷ [ ([ByteString], 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]]
+                                             | name  childNames]
+                childNames = [name | (name:_, _)  list]
+                node name  = let defs = [def | (path, def) ← list, path ≡ [name]]
                              in
                                if null defs then
                                    -- No resources are defined
@@ -177,123 +96,53 @@ mkResTree = processRoot . map (first canonicalisePath)
                                    -- There is a resource here.
                                    ResNode (Just $ last defs) children
                 children   = processNonRoot [(path, def)
-                                                 | (_:path, def) <- list]
+                                                 | (_:path, def)  list]
             in
               subtree
 
-
-findResource :: ResTree -> [FallbackHandler] -> URI -> IO (Maybe ([String], ResourceDef))
+findResource ∷ ResTree
+             → [FallbackHandler]
+             → URI
+             → IO (Maybe ([ByteString], ResourceDef))
 findResource (ResTree (ResNode rootDefM subtree)) fbs uri
-    = do let pathStr        = uriPath uri
-             path           = [unEscapeString x | x <- splitBy (== '/') pathStr, x /= ""]
-             haveGreedyRoot = case rootDefM of
-                                Just def -> resIsGreedy def
-                                Nothing  -> False
-             foundInTree    = if haveGreedyRoot || null path then
-                                  do def <- rootDefM
+    = do let path          = splitPathInfo uri
+             hasGreedyRoot = maybe False resIsGreedy rootDefM
+             foundInTree    = if hasGreedyRoot ∨ null path then
+                                  do def ← rootDefM
                                      return ([], def)
                               else
-                                  walkTree subtree path []
+                                  walkTree subtree path (∅)
          if isJust foundInTree then
              return foundInTree
-           else
+         else
              fallback path fbs
     where
-      walkTree :: ResSubtree -> [String] -> [String] -> Maybe ([String], ResourceDef)
+      walkTree ∷ ResSubtree
+               → [ByteString]
+               → Seq ByteString
+               → Maybe ([ByteString], ResourceDef)
 
       walkTree _ [] _
           = error "Internal error: should not reach here."
 
       walkTree tree (name:[]) soFar
-          = case M.lookup name tree of
-              Nothing               -> Nothing
-              Just (ResNode defM _) -> do def <- defM
-                                          return (soFar ++ [name], def)
+          = do ResNode defM _ ← M.lookup name tree
+               def            ← defM
+               return (toList $ soFar ⊳ name, def)
 
       walkTree tree (x:xs) soFar
-          = case M.lookup x tree of
-              Nothing                      -> Nothing
-              Just (ResNode defM children) -> case defM of
-                                                Just (ResourceDef { resIsGreedy = True })
-                                                    -> do def <- defM
-                                                          return (soFar ++ [x], def)
-                                                _   -> walkTree children xs (soFar ++ [x])
-
-      fallback :: [String] -> [FallbackHandler] -> IO (Maybe ([String], ResourceDef))
+          = do ResNode defM sub ← M.lookup x tree
+               case defM of
+                 Just (ResourceDef { resIsGreedy = True })
+                     → do def ← defM
+                          return (toList $ soFar ⊳ x, def)
+                 _   → walkTree sub xs (soFar ⊳ x)
+
+      fallback ∷ [ByteString]
+               → [FallbackHandler]
+               → IO (Maybe ([ByteString], ResourceDef))
       fallback _    []     = return Nothing
-      fallback path (x:xs) = do m <- x path
+      fallback path (x:xs) = do m  x path
                                 case m of
-                                  Just def -> return $! Just ([], def)
-                                  Nothing  -> fallback path xs
-
-
-runResource :: ResourceDef -> Interaction -> IO ThreadId
-runResource def itr
-    = def `seq` itr `seq`
-      fork
-      $! catch ( runRes ( do req <- getRequest
-                             fromMaybe notAllowed $ rsrc req
-                             driftTo Done
-                        ) itr
-               )
-               processException
-    where
-      fork :: IO () -> IO ThreadId
-      fork = if resUsesNativeThread def
-             then forkOS
-             else forkIO
-      
-      rsrc :: Request -> Maybe (Resource ())
-      rsrc req
-          = case reqMethod req 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
-              _      -> undefined
-
-      notAllowed :: Resource ()
-      notAllowed = do setStatus MethodNotAllowed
-                      setHeader (C8.pack "Allow") (C8.pack $ joinWith ", " allowedMethods)
-
-      allowedMethods :: [String]
-      allowedMethods = nub $ concat [ 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 -> []
-
-      toAbortion :: SomeException -> Abortion
-      toAbortion e = case fromException e of
-                       Just abortion -> abortion
-                       Nothing       -> Abortion InternalServerError emptyHeaders (Just (show e))
-
-      processException :: SomeException -> IO ()
-      processException exc
-          = do let abo = toAbortion exc
-                   conf = itrConfig itr
-               -- まだ DecidingHeader 以前の状態だったら、この途中終了
-               -- を應答に反映させる餘地がある。さうでなければ stderr
-               -- にでも吐くしか無い。
-               state <- atomically $ readItr itr itrState id
-               reqM  <- atomically $ readItr itr itrRequest id
-               res   <- atomically $ readItr itr itrResponse id
-               if state <= DecidingHeader then
-                   flip runRes itr
-                      $ do setStatus $ aboStatus abo
-                           mapM_ (uncurry setHeader) $ fromHeaders $ aboHeaders abo
-                           output $ abortPage conf reqM res abo
-                 else
-                   when (cnfDumpTooLateAbortionToStderr $ itrConfig itr)
-                            $ hPutStrLn stderr $ show abo
-
-               flip runRes itr $ driftTo Done
+                                  Just def → return $ Just ([], def)
+                                  Nothing  → fallback path xs
index adf8505defd683f03a7292fc74a3f85ba20dc49c..35c168fb38cde77ea2227cb2b808d00c9da79322 100644 (file)
@@ -1,36 +1,42 @@
 {-# LANGUAGE
     DeriveDataTypeable
+  , OverloadedStrings
+  , RecordWildCards
   , UnboxedTuples
   , UnicodeSyntax
+  , ViewPatterns
   #-}
-{-# OPTIONS_HADDOCK prune #-}
 
 -- |Definition of things related on HTTP response.
 module Network.HTTP.Lucu.Response
     ( StatusCode(..)
+    , printStatusCode
+
     , Response(..)
-    , hPutResponse
+    , emptyResponse
+    , resCanHaveBody
+    , printResponse
+
     , isInformational
     , isSuccessful
     , isRedirection
     , isError
     , isClientError
     , isServerError
-    , statusCode
     )
     where
-
-import qualified Data.ByteString as Strict (ByteString)
-import qualified Data.ByteString.Char8 as C8 hiding (ByteString)
-import           Data.Typeable
-import           Network.HTTP.Lucu.Format
-import           Network.HTTP.Lucu.HandleLike
-import           Network.HTTP.Lucu.Headers
-import           Network.HTTP.Lucu.HttpVersion
+import Data.Ascii (Ascii, AsciiBuilder)
+import qualified Data.Ascii as A
+import Data.Monoid.Unicode
+import Data.Typeable
+import Network.HTTP.Lucu.Headers
+import Network.HTTP.Lucu.HttpVersion
+import Network.HTTP.Lucu.Utils
+import Prelude.Unicode
 
 -- |This is the definition of HTTP status code.
--- 'Network.HTTP.Lucu.Resource.setStatus' accepts these named statuses
--- so you don't have to memorize, for instance, that \"Gateway
+-- 'Network.HTTP.Lucu.Resource.setStatus' accepts these named status
+-- codes so you don't have to memorize, for instance, that \"Gateway
 -- Timeout\" is 504.
 data StatusCode = Continue
                 | SwitchingProtocols
@@ -82,126 +88,144 @@ data StatusCode = Continue
                 | GatewayTimeout
                 | HttpVersionNotSupported
                 | InsufficientStorage
-                  deriving (Typeable, Eq)
-
-instance Show StatusCode where
-    show sc = case statusCode sc of
-                (# num, msg #)
-                    -> (fmtDec 3 num) ++ " " ++ C8.unpack msg
-
-
+                  deriving (Eq, Show, Typeable)
+
+-- |Convert a 'StatusCode' to an 'AsciiBuilder'.
+printStatusCode ∷ StatusCode → AsciiBuilder
+{-# INLINEABLE printStatusCode #-}
+printStatusCode (statusCode → (# num, msg #))
+    = ( show3 num            ⊕
+        A.toAsciiBuilder " " ⊕
+        A.toAsciiBuilder msg
+      )
+
+-- |This is the definition of an HTTP response.
 data Response = Response {
-      resVersion :: !HttpVersion
-    , resStatus  :: !StatusCode
-    , resHeaders :: !Headers
+      resVersion  !HttpVersion
+    , resStatus   !StatusCode
+    , resHeaders  !Headers
     } deriving (Show, Eq)
 
-
 instance HasHeaders Response where
-    getHeaders = resHeaders
+    getHeaders         = resHeaders
     setHeaders res hdr = res { resHeaders = hdr }
 
-
-hPutResponse :: HandleLike h => h -> Response -> IO ()
-hPutResponse h res
-    = h `seq` res `seq`
-      do hPutHttpVersion h (resVersion res)
-         hPutChar        h ' '
-         hPutStatus      h (resStatus  res)
-         hPutBS          h (C8.pack "\r\n")
-         hPutHeaders     h (resHeaders res)
-
-hPutStatus :: HandleLike h => h -> StatusCode -> IO ()
-hPutStatus h sc
-    = h `seq` sc `seq`
-      case statusCode sc of
-        (# num, msg #)
-            -> do hPutStr  h (fmtDec 3 num)
-                  hPutChar h ' '
-                  hPutBS   h msg
-
-
--- |@'isInformational' sc@ is 'Prelude.True' iff @sc < 200@.
-isInformational :: StatusCode -> Bool
-isInformational = doesMeet (< 200)
-
--- |@'isSuccessful' sc@ is 'Prelude.True' iff @200 <= sc < 300@.
-isSuccessful :: StatusCode -> Bool
-isSuccessful = doesMeet (\ n -> n >= 200 && n < 300)
-
--- |@'isRedirection' sc@ is 'Prelude.True' iff @300 <= sc < 400@.
-isRedirection :: StatusCode -> Bool
-isRedirection = doesMeet (\ n -> n >= 300 && n < 400)
-
--- |@'isError' sc@ is 'Prelude.True' iff @400 <= sc@
-isError :: StatusCode -> Bool
-isError = doesMeet (>= 400)
-
--- |@'isClientError' sc@ is 'Prelude.True' iff @400 <= sc < 500@.
-isClientError :: StatusCode -> Bool
-isClientError = doesMeet (\ n -> n >= 400 && n < 500)
-
--- |@'isServerError' sc@ is 'Prelude.True' iff @500 <= sc@.
-isServerError :: StatusCode -> Bool
-isServerError = doesMeet (>= 500)
-
-
-doesMeet :: (Int -> Bool) -> StatusCode -> Bool
-doesMeet p sc = case statusCode sc of
-                  (# num, _ #) -> p num
-
-
--- |@'statusCode' sc@ returns an unboxed tuple of numeric and textual
--- representation of @sc@.
-statusCode :: StatusCode -> (# Int, Strict.ByteString #)
-
-statusCode Continue                    = (# 100, C8.pack "Continue"                      #)
-statusCode SwitchingProtocols          = (# 101, C8.pack "Switching Protocols"           #)
-statusCode Processing                  = (# 102, C8.pack "Processing"                    #)
-
-statusCode Ok                          = (# 200, C8.pack "OK"                            #)
-statusCode Created                     = (# 201, C8.pack "Created"                       #)
-statusCode Accepted                    = (# 202, C8.pack "Accepted"                      #)
-statusCode NonAuthoritativeInformation = (# 203, C8.pack "Non Authoritative Information" #)
-statusCode NoContent                   = (# 204, C8.pack "No Content"                    #)
-statusCode ResetContent                = (# 205, C8.pack "Reset Content"                 #)
-statusCode PartialContent              = (# 206, C8.pack "Partial Content"               #)
-statusCode MultiStatus                 = (# 207, C8.pack "Multi Status"                  #)
-
-statusCode MultipleChoices             = (# 300, C8.pack "Multiple Choices"              #)
-statusCode MovedPermanently            = (# 301, C8.pack "Moved Permanently"             #)
-statusCode Found                       = (# 302, C8.pack "Found"                         #)
-statusCode SeeOther                    = (# 303, C8.pack "See Other"                     #)
-statusCode NotModified                 = (# 304, C8.pack "Not Modified"                  #)
-statusCode UseProxy                    = (# 305, C8.pack "Use Proxy"                     #)
-statusCode TemporaryRedirect           = (# 306, C8.pack "Temporary Redirect"            #)
-
-statusCode BadRequest                  = (# 400, C8.pack "Bad Request"                   #)
-statusCode Unauthorized                = (# 401, C8.pack "Unauthorized"                  #)
-statusCode PaymentRequired             = (# 402, C8.pack "Payment Required"              #)
-statusCode Forbidden                   = (# 403, C8.pack "Forbidden"                     #)
-statusCode NotFound                    = (# 404, C8.pack "Not Found"                     #)
-statusCode MethodNotAllowed            = (# 405, C8.pack "Method Not Allowed"            #)
-statusCode NotAcceptable               = (# 406, C8.pack "Not Acceptable"                #)
-statusCode ProxyAuthenticationRequired = (# 407, C8.pack "Proxy Authentication Required" #)
-statusCode RequestTimeout              = (# 408, C8.pack "Request Timeout"               #)
-statusCode Conflict                    = (# 409, C8.pack "Conflict"                      #)
-statusCode Gone                        = (# 410, C8.pack "Gone"                          #)
-statusCode LengthRequired              = (# 411, C8.pack "Length Required"               #)
-statusCode PreconditionFailed          = (# 412, C8.pack "Precondition Failed"           #)
-statusCode RequestEntityTooLarge       = (# 413, C8.pack "Request Entity Too Large"      #)
-statusCode RequestURITooLarge          = (# 414, C8.pack "Request URI Too Large"         #)
-statusCode UnsupportedMediaType        = (# 415, C8.pack "Unsupported Media Type"        #)
-statusCode RequestRangeNotSatisfiable  = (# 416, C8.pack "Request Range Not Satisfiable" #)
-statusCode ExpectationFailed           = (# 417, C8.pack "Expectation Failed"            #)
-statusCode UnprocessableEntitiy        = (# 422, C8.pack "Unprocessable Entity"          #)
-statusCode Locked                      = (# 423, C8.pack "Locked"                        #)
-statusCode FailedDependency            = (# 424, C8.pack "Failed Dependency"             #)
-
-statusCode InternalServerError         = (# 500, C8.pack "Internal Server Error"         #)
-statusCode NotImplemented              = (# 501, C8.pack "Not Implemented"               #)
-statusCode BadGateway                  = (# 502, C8.pack "Bad Gateway"                   #)
-statusCode ServiceUnavailable          = (# 503, C8.pack "Service Unavailable"           #)
-statusCode GatewayTimeout              = (# 504, C8.pack "Gateway Timeout"               #)
-statusCode HttpVersionNotSupported     = (# 505, C8.pack "HTTP Version Not Supported"    #)
-statusCode InsufficientStorage         = (# 507, C8.pack "Insufficient Storage"          #)
\ No newline at end of file
+-- |Returns an HTTP\/1.1 'Response' with no header fields.
+emptyResponse ∷ StatusCode → Response
+emptyResponse sc
+    = Response {
+        resVersion = HttpVersion 1 1
+      , resStatus  = sc
+      , resHeaders = (∅)
+      }
+
+-- |Returns 'True' iff a given 'Response' allows the existence of
+-- response entity body.
+resCanHaveBody ∷ Response → Bool
+{-# INLINEABLE resCanHaveBody #-}
+resCanHaveBody (Response {..})
+    | isInformational resStatus = False
+    | resStatus ≡ NoContent     = False
+    | resStatus ≡ ResetContent  = False
+    | resStatus ≡ NotModified   = False
+    | otherwise                 = True
+
+-- |Convert a 'Response' to 'AsciiBuilder'.
+printResponse ∷ Response → AsciiBuilder
+{-# INLINEABLE printResponse #-}
+printResponse (Response {..})
+    = printHttpVersion resVersion ⊕
+      A.toAsciiBuilder " "        ⊕
+      printStatusCode  resStatus  ⊕
+      A.toAsciiBuilder "\x0D\x0A" ⊕
+      printHeaders     resHeaders
+
+-- |@'isInformational' sc@ returns 'True' iff @sc < 200@.
+isInformational ∷ StatusCode → Bool
+{-# INLINE isInformational #-}
+isInformational = satisfy (< 200)
+
+-- |@'isSuccessful' sc@ returns 'True' iff @200 <= sc < 300@.
+isSuccessful ∷ StatusCode → Bool
+{-# INLINE isSuccessful #-}
+isSuccessful = satisfy (\ n → n ≥ 200 ∧ n < 300)
+
+-- |@'isRedirection' sc@ returns 'True' iff @300 <= sc < 400@.
+isRedirection ∷ StatusCode → Bool
+{-# INLINE isRedirection #-}
+isRedirection = satisfy (\ n → n ≥ 300 ∧ n < 400)
+
+-- |@'isError' sc@ returns 'True' iff @400 <= sc@
+isError ∷ StatusCode → Bool
+{-# INLINE isError #-}
+isError = satisfy (≥ 400)
+
+-- |@'isClientError' sc@ returns 'True' iff @400 <= sc < 500@.
+isClientError ∷ StatusCode → Bool
+{-# INLINE isClientError #-}
+isClientError = satisfy (\ n → n ≥ 400 ∧ n < 500)
+
+-- |@'isServerError' sc@ returns 'True' iff @500 <= sc@.
+isServerError ∷ StatusCode → Bool
+{-# INLINE isServerError #-}
+isServerError = satisfy (≥ 500)
+
+satisfy ∷ (Int → Bool) → StatusCode → Bool
+{-# INLINE satisfy #-}
+satisfy p (statusCode → (# num, _ #)) = p num
+
+statusCode ∷ StatusCode → (# Int, Ascii #)
+{-# INLINEABLE statusCode #-}
+
+statusCode Continue                    = (# 100, "Continue"                      #)
+statusCode SwitchingProtocols          = (# 101, "Switching Protocols"           #)
+statusCode Processing                  = (# 102, "Processing"                    #)
+
+statusCode Ok                          = (# 200, "OK"                            #)
+statusCode Created                     = (# 201, "Created"                       #)
+statusCode Accepted                    = (# 202, "Accepted"                      #)
+statusCode NonAuthoritativeInformation = (# 203, "Non Authoritative Information" #)
+statusCode NoContent                   = (# 204, "No Content"                    #)
+statusCode ResetContent                = (# 205, "Reset Content"                 #)
+statusCode PartialContent              = (# 206, "Partial Content"               #)
+statusCode MultiStatus                 = (# 207, "Multi Status"                  #)
+
+statusCode MultipleChoices             = (# 300, "Multiple Choices"              #)
+statusCode MovedPermanently            = (# 301, "Moved Permanently"             #)
+statusCode Found                       = (# 302, "Found"                         #)
+statusCode SeeOther                    = (# 303, "See Other"                     #)
+statusCode NotModified                 = (# 304, "Not Modified"                  #)
+statusCode UseProxy                    = (# 305, "Use Proxy"                     #)
+statusCode TemporaryRedirect           = (# 306, "Temporary Redirect"            #)
+
+statusCode BadRequest                  = (# 400, "Bad Request"                   #)
+statusCode Unauthorized                = (# 401, "Unauthorized"                  #)
+statusCode PaymentRequired             = (# 402, "Payment Required"              #)
+statusCode Forbidden                   = (# 403, "Forbidden"                     #)
+statusCode NotFound                    = (# 404, "Not Found"                     #)
+statusCode MethodNotAllowed            = (# 405, "Method Not Allowed"            #)
+statusCode NotAcceptable               = (# 406, "Not Acceptable"                #)
+statusCode ProxyAuthenticationRequired = (# 407, "Proxy Authentication Required" #)
+statusCode RequestTimeout              = (# 408, "Request Timeout"               #)
+statusCode Conflict                    = (# 409, "Conflict"                      #)
+statusCode Gone                        = (# 410, "Gone"                          #)
+statusCode LengthRequired              = (# 411, "Length Required"               #)
+statusCode PreconditionFailed          = (# 412, "Precondition Failed"           #)
+statusCode RequestEntityTooLarge       = (# 413, "Request Entity Too Large"      #)
+statusCode RequestURITooLarge          = (# 414, "Request URI Too Large"         #)
+statusCode UnsupportedMediaType        = (# 415, "Unsupported Media Type"        #)
+statusCode RequestRangeNotSatisfiable  = (# 416, "Request Range Not Satisfiable" #)
+statusCode ExpectationFailed           = (# 417, "Expectation Failed"            #)
+statusCode UnprocessableEntitiy        = (# 422, "Unprocessable Entity"          #)
+statusCode Locked                      = (# 423, "Locked"                        #)
+statusCode FailedDependency            = (# 424, "Failed Dependency"             #)
+
+statusCode InternalServerError         = (# 500, "Internal Server Error"         #)
+statusCode NotImplemented              = (# 501, "Not Implemented"               #)
+statusCode BadGateway                  = (# 502, "Bad Gateway"                   #)
+statusCode ServiceUnavailable          = (# 503, "Service Unavailable"           #)
+statusCode GatewayTimeout              = (# 504, "Gateway Timeout"               #)
+statusCode HttpVersionNotSupported     = (# 505, "HTTP Version Not Supported"    #)
+statusCode InsufficientStorage         = (# 507, "Insufficient Storage"          #)
+-- FIXME: Textual representations should also include numbers.
+-- FIXME: StatusCode should be a type class rather than a type.
index 9751a7699c7b175ba062ae750d4c5f710fffeac0..d89ee9e885aa114429489cdef1fb7c59466fb65b 100644 (file)
 {-# LANGUAGE
-    BangPatterns
+    DoAndIfThenElse
+  , RecordWildCards
+  , ScopedTypeVariables
   , UnicodeSyntax
   #-}
 module Network.HTTP.Lucu.ResponseWriter
     ( responseWriter
     )
     where
-
-import qualified Data.ByteString.Lazy.Char8 as C8
-import           Control.Concurrent
-import           Control.Concurrent.STM
-import           Control.Exception
-import           Control.Monad
+import qualified Blaze.ByteString.Builder.HTTP as BB
+import Control.Applicative
+import Control.Concurrent
+import Control.Concurrent.STM
+import Control.Exception
+import Control.Monad
+import qualified Data.Ascii as A
+import Data.Monoid.Unicode
 import qualified Data.Sequence as S
-import           Data.Sequence (ViewR(..))
-import           Network.HTTP.Lucu.Config
-import           Network.HTTP.Lucu.Format
-import           Network.HTTP.Lucu.HandleLike
-import           Network.HTTP.Lucu.Headers
-import           Network.HTTP.Lucu.HttpVersion
-import           Network.HTTP.Lucu.Interaction
-import           Network.HTTP.Lucu.Postprocess
-import           Network.HTTP.Lucu.Response
-import           Prelude hiding (catch)
-import           System.IO (stderr)
-
-
-responseWriter :: HandleLike h => Config -> h -> InteractionQueue -> ThreadId -> IO ()
-responseWriter !cnf !h !tQueue !readerTID
-    = awaitSomethingToWrite
+import Data.Sequence (ViewR(..))
+import Network.HTTP.Lucu.Config
+import Network.HTTP.Lucu.HandleLike
+import Network.HTTP.Lucu.HttpVersion
+import Network.HTTP.Lucu.Interaction
+import Network.HTTP.Lucu.Response
+import Prelude.Unicode
+import System.IO (hPutStrLn, stderr)
+import System.IO.Error
+
+data Context h
+    = Context {
+        cConfig ∷ !Config
+      , cHandle ∷ !h
+      , cQueue  ∷ !InteractionQueue
+      , cReader ∷ !ThreadId
+      }
+
+responseWriter ∷ HandleLike h ⇒ Config → h → InteractionQueue → ThreadId → IO ()
+responseWriter cnf h tQueue readerTID
+    = awaitSomethingToWrite (Context cnf h tQueue readerTID)
       `catches`
-      [ Handler (( \ _                   -> return () ) :: IOException -> IO ())
-      , Handler  ( \ ThreadKilled        -> return () )
-      , Handler  ( \ BlockedIndefinitelyOnSTM -> hPutStrLn stderr "requestWriter: blocked indefinitely" )
-      , Handler (( \ e                   -> hPutStrLn stderr (show e) ) :: SomeException -> IO ())
+      [ Handler handleIOE
+      , Handler handleAsyncE
+      , Handler handleBIOS
+      , Handler handleOthers
       ]
     where
-      awaitSomethingToWrite :: IO ()
-      awaitSomethingToWrite 
-          = {-# SCC "awaitSomethingToWrite" #-}
-            join $!
-                 atomically $!
-                 -- キューが空でなくなるまで待つ
-                 do queue <- readTVar tQueue
-                    -- GettingBody 状態にあり、Continue が期待されてゐ
-                    -- て、それがまだ送信前なのであれば、Continue を送
-                    -- 信する。
-                    case S.viewr queue of
-                      EmptyR   -> retry
-                      _ :> itr -> do state <- readItr itr itrState id
-
-                                     if state == GettingBody then
-                                         writeContinueIfNecessary itr
-                                       else
-                                         if state >= DecidingBody then
-                                             writeHeaderOrBodyIfNecessary itr
-                                         else
-                                             retry
-
-      writeContinueIfNecessary :: Interaction -> STM (IO ())
-      writeContinueIfNecessary !itr
-          = {-# SCC "writeContinueIfNecessary" #-}
-            do expectedContinue <- readItr itr itrExpectedContinue id
-               if expectedContinue then
-                   do wroteContinue <- readItr itr itrWroteContinue id
-                      if wroteContinue then
-                          -- 既に Continue を書込み濟
-                          retry
+      handleIOE ∷ IOException → IO ()
+      handleIOE e
+          | isIllegalOperation e
+              = return () -- EPIPE: should be ignored at all.
+          | otherwise
+              = terminate e
+
+      handleAsyncE ∷ AsyncException → IO ()
+      handleAsyncE ThreadKilled = terminate'
+      handleAsyncE e            = terminate e
+
+      handleBIOS ∷ BlockedIndefinitelyOnSTM → IO ()
+      handleBIOS = terminate
+
+      handleOthers ∷ SomeException → IO ()
+      handleOthers = terminate
+
+      terminate ∷ Exception e ⇒ e → IO ()
+      terminate e
+          = do hPutStrLn stderr "requestWriter caught an exception:"
+               hPutStrLn stderr (show $ toException e)
+               terminate'
+
+      terminate' ∷ IO ()
+      terminate' = hClose h
+
+awaitSomethingToWrite ∷ HandleLike h ⇒ Context h → IO ()
+awaitSomethingToWrite ctx@(Context {..})
+    = join $
+      atomically $
+      do queue ← readTVar cQueue
+         case S.viewr queue of
+           EmptyR        → retry
+           queue' :> itr → do writeTVar cQueue queue'
+                              return $ writeSomething ctx itr
+
+writeSomething ∷ HandleLike h ⇒ Context h → SomeInteraction → IO ()
+writeSomething ctx itr
+    = let writer = writeResponseForNI  ctx <$> fromInteraction itr <|>
+                   writeResponseForSEI ctx <$> fromInteraction itr <|>
+                   writeResponseForSYI ctx <$> fromInteraction itr
+      in
+        case writer of
+          Just f  → f
+          Nothing → fail "Internal error: unknown interaction type"
+
+writeResponseForNI ∷ HandleLike h
+                   ⇒ Context h
+                   → NormalInteraction
+                   → IO ()
+writeResponseForNI = writeContinueIfNeeded
+
+writeContinueIfNeeded ∷ HandleLike h
+                      ⇒ Context h
+                      → NormalInteraction
+                      → IO ()
+writeContinueIfNeeded ctx@(Context {..}) ni@(NI {..})
+    = do isNeeded ← atomically $ readTMVar niSendContinue
+         when isNeeded
+             $ do let cont = Response {
+                               resVersion = HttpVersion 1 1
+                             , resStatus  = Continue
+                             , resHeaders = (∅)
+                             }
+                  hPutBuilder cHandle $ A.toBuilder $ printResponse cont
+                  hFlush cHandle
+         writeHeader ctx ni
+
+writeHeader ∷ HandleLike h
+            ⇒ Context h
+            → NormalInteraction
+            → IO ()
+writeHeader ctx@(Context {..}) ni@(NI {..})
+    = do res ← atomically $
+               do state ← readTVar niState
+                  if state ≥ SendingBody then
+                      readTVar niResponse
+                  else
+                      retry -- Too early to write header fields.
+         hPutBuilder cHandle $ A.toBuilder $ printResponse res
+         hFlush cHandle
+         writeBodyIfNeeded ctx ni
+
+writeBodyIfNeeded ∷ HandleLike h
+                  ⇒ Context h
+                  → NormalInteraction
+                  → IO ()
+writeBodyIfNeeded ctx ni@(NI {..})
+    = join $
+      atomically $
+      do willDiscardBody ← readTVar niWillDiscardBody
+         if willDiscardBody then
+             return $ discardBody ctx ni
+         else
+             if niWillChunkBody then
+                 return $ writeChunkedBody    ctx ni
+             else
+                 return $ writeNonChunkedBody ctx ni
+
+discardBody ∷ HandleLike h
+            ⇒ Context h
+            → NormalInteraction
+            → IO ()
+discardBody ctx ni@(NI {..})
+    = join $
+      atomically $
+      do chunk ← tryTakeTMVar niBodyToSend
+         case chunk of
+           Just _  → return $ discardBody ctx ni
+           Nothing → do state ← readTVar niState
+                        if state ≡ Done then
+                            return $ finalize ctx ni
                         else
-                          do reqBodyWanted <- readItr itr itrReqBodyWanted id
-                             if reqBodyWanted /= Nothing then
-                                 return $ writeContinue itr
-                               else
-                                 retry
-                 else
-                   retry
-
-      writeHeaderOrBodyIfNecessary :: Interaction -> STM (IO ())
-      writeHeaderOrBodyIfNecessary !itr
-          -- DecidingBody 以降の状態にあり、まだヘッダを出力する前であ
-          -- れば、ヘッダを出力する。ヘッダ出力後であり、bodyToSend が
-          -- 空でなければ、それを出力する。空である時は、もし状態が
-          -- Done であれば後処理をする。
-          = {-# SCC "writeHeaderOrBodyIfNecessary" #-}
-            do wroteHeader <- readItr itr itrWroteHeader id
-               
-               if not wroteHeader then
-                   return $! writeHeader itr
-                 else
-                   do bodyToSend <- readItr itr itrBodyToSend id
-
-                      if C8.null bodyToSend then
-                          do state <- readItr itr itrState id
-
-                             if state == Done then
-                                 return $! finalize itr
-                               else
-                                 retry
+                            retry
+
+writeChunkedBody ∷ HandleLike h
+                 ⇒ Context h
+                 → NormalInteraction
+                 → IO ()
+writeChunkedBody ctx@(Context {..}) ni@(NI {..})
+    = join $
+      atomically $
+      do chunk ← tryTakeTMVar niBodyToSend
+         case chunk of
+           Just b  → return $
+                     do hPutBuilder cHandle $ BB.chunkedTransferEncoding b
+                        hFlush cHandle
+                        writeChunkedBody ctx ni
+           Nothing → do state ← readTVar niState
+                        if state ≡ Done then
+                            return $
+                            do hPutBuilder cHandle BB.chunkedTransferTerminator
+                               hFlush cHandle
+                               finalize ctx ni
                         else
-                          return $! writeBodyChunk itr
-
-      writeContinue :: Interaction -> IO ()
-      writeContinue !itr
-          = {-# SCC "writeContinue" #-}
-            do let cont = Response {
-                            resVersion = HttpVersion 1 1
-                          , resStatus  = Continue
-                          , resHeaders = emptyHeaders
-                          }
-               cont' <- completeUnconditionalHeaders cnf cont
-               hPutResponse h cont'
-               hFlush h
-               atomically $! writeItr itr itrWroteContinue True
-               awaitSomethingToWrite
-
-      writeHeader :: Interaction -> IO ()
-      writeHeader !itr
-          = {-# SCC "writeHeader" #-}
-            do res <- atomically $! do writeItr itr itrWroteHeader True
-                                       readItr itr itrResponse id
-               hPutResponse h res
-               hFlush h
-               awaitSomethingToWrite
-      
-      writeBodyChunk :: Interaction -> IO ()
-      writeBodyChunk !itr
-          = {-# SCC "writeBodyChunk" #-}
-            do willDiscardBody <- atomically $! readItr itr itrWillDiscardBody id
-               willChunkBody   <- atomically $! readItr itr itrWillChunkBody   id
-               chunk           <- atomically $! do chunk <- readItr itr itrBodyToSend id
-                                                   writeItr itr itrBodyToSend C8.empty
-                                                   return chunk
-               unless willDiscardBody
-                          $ do if willChunkBody then
-                                   do hPutStr h (fmtHex False 0 $! fromIntegral $! C8.length chunk)
-                                      hPutLBS h (C8.pack "\r\n")
-                                      hPutLBS h chunk
-                                      hPutLBS h (C8.pack "\r\n")
-                                 else
-                                   hPutLBS h chunk
-                               hFlush h
-               awaitSomethingToWrite
-
-      finishBodyChunk :: Interaction -> IO ()
-      finishBodyChunk !itr
-          = {-# SCC "finishBodyChunk" #-}
-            do willDiscardBody <- atomically $! readItr itr itrWillDiscardBody id
-               willChunkBody   <- atomically $! readItr itr itrWillChunkBody   id
-               when (not willDiscardBody && willChunkBody)
-                        $ hPutLBS h (C8.pack "0\r\n\r\n") >> hFlush h
-
-      finalize :: Interaction -> IO ()
-      finalize !itr
-          = {-# SCC "finalize" #-}
-            do finishBodyChunk itr
-               willClose <- atomically $!
-                            do queue <- readTVar tQueue
-
-                               case S.viewr queue of
-                                 EmptyR         -> return () -- this should never happen
-                                 remaining :> _ -> writeTVar tQueue remaining
-
-                               readItr itr itrWillClose id
-               if willClose then
-                   -- reader は恐らく hWaitForInput してゐる最中なので、
-                   -- スレッドを豫め殺して置かないとをかしくなる。
-                   do killThread readerTID
-                      hClose h
-                 else
-                   awaitSomethingToWrite
+                            retry
+
+writeNonChunkedBody ∷ HandleLike h
+                    ⇒ Context h
+                    → NormalInteraction
+                    → IO ()
+writeNonChunkedBody ctx@(Context {..}) ni@(NI {..})
+    = join $
+      atomically $
+      do chunk ← tryTakeTMVar niBodyToSend
+         case chunk of
+           Just b  → return $
+                     do hPutBuilder cHandle b
+                        hFlush cHandle
+                        writeNonChunkedBody ctx ni
+           Nothing → do state ← readTVar niState
+                        if state ≡ Done then
+                            return $ finalize ctx ni
+                        else
+                            retry
+
+finalize ∷ HandleLike h ⇒ Context h → NormalInteraction → IO ()
+finalize ctx@(Context {..}) (NI {..})
+    = join $
+      atomically $
+      do willClose    ← readTVar  niWillClose
+         sentContinue ← takeTMVar niSendContinue
+         return $
+             if needToClose willClose sentContinue then
+                 -- The RequestReader is probably blocking on
+                 -- hWaitForInput so we have to kill it before closing
+                 -- the socket.  THINKME: Couldn't that somehow be
+                 -- avoided?
+                 do killThread cReader
+                    hClose cHandle
+             else
+                 awaitSomethingToWrite ctx
+    where
+      needToClose ∷ Bool → Bool → Bool
+      needToClose willClose sentContinue
+          -- Explicitly instructed to close the connection.
+          | willClose = True
+          -- We've sent both "HTTP/1.1 100 Continue" and a final
+          -- response, so nothing prevents our connection from keeping
+          -- alive.
+          | sentContinue = False
+          -- We've got "Expect: 100-continue" but have sent a final
+          -- response without sending "HTTP/1.1 100
+          -- Continue". According to the RFC 2616 (HTTP/1.1), it is
+          -- undecidable whether the client will send us its
+          -- (rejected) request body OR start a completely new request
+          -- in this situation. So the only possible thing to do is to
+          -- brutally shutdown the connection.
+          | niExpectedContinue = True
+          -- The client didn't expect 100-continue so we haven't sent
+          -- one. No need to do anything special.
+          | otherwise = False
+
+writeResponseForSEI ∷ HandleLike h
+                    ⇒ Context h
+                    → SemanticallyInvalidInteraction
+                    → IO ()
+writeResponseForSEI ctx@(Context {..}) (SEI {..})
+    = do hPutBuilder cHandle $ A.toBuilder $ printResponse seiResponse
+         unless seiWillDiscardBody $
+             if seiWillChunkBody then
+                 do hPutBuilder cHandle $ BB.chunkedTransferEncoding seiBodyToSend
+                    hPutBuilder cHandle BB.chunkedTransferTerminator
+             else
+                 hPutBuilder cHandle seiBodyToSend
+         hFlush cHandle
+         if seiWillClose ∨ seiExpectedContinue then
+             do killThread cReader
+                hClose cHandle
+         else
+             awaitSomethingToWrite ctx
+
+writeResponseForSYI ∷ HandleLike h
+                    ⇒ Context h
+                    → SyntacticallyInvalidInteraction
+                    → IO ()
+writeResponseForSYI (Context {..}) (SYI {..})
+    = do hPutBuilder cHandle $ A.toBuilder $ printResponse syiResponse
+         hPutBuilder cHandle syiBodyToSend
+         hFlush cHandle
+         killThread cReader
+         hClose cHandle
index 915f32376eb17a3ae6a57eef443648698bc4d63b..dd9c34b00fc54935c051e4dd560cc123a7c28402 100644 (file)
@@ -8,37 +8,34 @@ module Network.HTTP.Lucu.SocketLike
     ( SocketLike(..)
     )
     where
-
 import qualified Network.Socket as So
-import           Network.HTTP.Lucu.HandleLike
+import Network.HTTP.Lucu.HandleLike
 import qualified OpenSSL.Session as SSL
+import Prelude.Unicode
 import qualified System.IO as I
 
-
-class (HandleLike (Handle s)) => SocketLike s where
-    type Handle s :: *
-    accept        :: s -> IO (Handle s, So.SockAddr)
-    socketPort    :: s -> IO So.PortNumber
-
+class (HandleLike (Handle s)) ⇒ SocketLike s where
+    type Handle s ∷ ★
+    accept        ∷ s → IO (Handle s, So.SockAddr)
+    socketPort    ∷ s → IO So.PortNumber
 
 instance SocketLike So.Socket where
     type Handle So.Socket = I.Handle
 
     accept soSelf
-        = do (soPeer, addr) <- So.accept soSelf
-             hPeer          <- So.socketToHandle soPeer I.ReadWriteMode
+        = do (soPeer, addr)  So.accept soSelf
+             hPeer           So.socketToHandle soPeer I.ReadWriteMode
              return (hPeer, addr)
 
     socketPort = So.socketPort
 
-
 instance SocketLike (SSL.SSLContext, So.Socket) where
     type Handle (SSL.SSLContext, So.Socket) = SSL.SSL
 
     accept (ctx, soSelf)
-        = do (soPeer, addr) <- So.accept soSelf
-             ssl            <- SSL.connection ctx soPeer
+        = do (soPeer, addr)  So.accept soSelf
+             ssl             SSL.connection ctx soPeer
              SSL.accept ssl
              return (ssl, addr)
 
-    socketPort = So.socketPort . snd
\ No newline at end of file
+    socketPort = So.socketPort ∘ snd
index 9175ce9289c816cdb4f8375195b9adf53efb2214..4f669314aee7e599703e999433a63713fe1b4a6f 100644 (file)
 {-# LANGUAGE
-    BangPatterns
+    DoAndIfThenElse
+  , OverloadedStrings
   , UnicodeSyntax
   #-}
 -- | Handling static files on the filesystem.
 module Network.HTTP.Lucu.StaticFile
     ( staticFile
-    , handleStaticFile
-
     , staticDir
-    , handleStaticDir
 
     , generateETagFromFile
     )
     where
+import qualified Blaze.ByteString.Builder.ByteString as BB
+import qualified Blaze.Text.Int as BT
+import Control.Monad
+import Control.Monad.Unicode
+import Control.Monad.Trans
+import qualified Data.Ascii as A
+import Data.ByteString (ByteString)
+import qualified Data.ByteString.Lazy.Char8 as LBS
+import Data.Monoid.Unicode
+import Data.String
+import qualified Data.Text as T
+import qualified Data.Text.Encoding as T
+import Data.Time.Clock.POSIX
+import Network.HTTP.Lucu.Abortion
+import Network.HTTP.Lucu.Config
+import Network.HTTP.Lucu.ETag
+import Network.HTTP.Lucu.MIMEType
+import Network.HTTP.Lucu.MIMEType.Guess
+import Network.HTTP.Lucu.Resource
+import Network.HTTP.Lucu.Resource.Internal
+import Network.HTTP.Lucu.Response
+import Prelude.Unicode
+import System.FilePath
+import System.Posix.Files
 
-import           Control.Monad
-import           Control.Monad.Trans
-import qualified Data.ByteString.Lazy.Char8 as B
-import           Data.Time.Clock.POSIX
-import           Network.HTTP.Lucu.Abortion
-import           Network.HTTP.Lucu.Config
-import           Network.HTTP.Lucu.ETag
-import           Network.HTTP.Lucu.Format
-import           Network.HTTP.Lucu.MIMEType.Guess
-import           Network.HTTP.Lucu.Resource
-import           Network.HTTP.Lucu.Resource.Tree
-import           Network.HTTP.Lucu.Response
-import           Network.HTTP.Lucu.Utils
-import           System.FilePath.Posix
-import           System.Posix.Files
-
-
--- | @'staticFile' fpath@ is a
--- 'Network.HTTP.Lucu.Resource.Tree.ResourceDef' which serves the file
--- at @fpath@ on the filesystem.
-staticFile :: FilePath -> ResourceDef
+-- | @'staticFile' fpath@ is a 'ResourceDef' which serves the file at
+-- @fpath@ on the filesystem.
+staticFile ∷ FilePath → ResourceDef
 staticFile path
-    = ResourceDef {
-        resUsesNativeThread = False
-      , resIsGreedy         = False
-      , resGet              = Just $! handleStaticFile path
-      , resHead             = Nothing
-      , resPost             = Nothing
-      , resPut              = Nothing
-      , resDelete           = Nothing
+    = emptyResource {
+        resGet  = Just $ handleStaticFile True  path
+      , resHead = Just $ handleStaticFile False path
       }
 
--- | Computation of @'handleStaticFile' fpath@ serves the file at
--- @fpath@ on the filesystem. The
--- 'Network.HTTP.Lucu.Resource.Resource' must be in the /Examining
--- Request/ state before the computation. It will be in the /Done/
--- state after the computation.
---
--- If you just want to place a static file on the
--- 'Network.HTTP.Lucu.Resource.Tree.ResTree', you had better use
--- 'staticFile' instead of this.
-handleStaticFile :: FilePath -> Resource ()
-handleStaticFile path
-    = path `seq`
-      do exists <- liftIO $ fileExist path
-         if exists then
-             -- 存在はした。讀めるかどうかは知らない。
-             do stat <- liftIO $ getFileStatus path
-                if isRegularFile stat then
-                    do readable <- liftIO $ fileAccess path True False False
-                       unless readable
-                           -- 讀めない
-                           $ abort Forbidden [] Nothing
-                       -- 讀める
-                       tag     <- liftIO $ generateETagFromFile path
-                       let lastMod = posixSecondsToUTCTime $ fromRational $ toRational $ modificationTime stat
-                       foundEntity tag lastMod
+octetStream ∷ MIMEType
+{-# NOINLINE octetStream #-}
+octetStream = parseMIMEType "application/octet-stream"
+
+handleStaticFile ∷ Bool → FilePath → Resource ()
+handleStaticFile sendContent path
+    = do exists ← liftIO $ fileExist path
+         unless exists
+             foundNoEntity'
+
+         readable ← liftIO $ fileAccess path True False False
+         unless readable
+             $ abort
+             $ mkAbortion Forbidden [] Nothing
+
+         stat ← liftIO $ getFileStatus path
+         when (isDirectory stat)
+             $ abort
+             $ mkAbortion Forbidden [] Nothing
 
-                       -- MIME Type を推定
-                       conf <- getConfig
-                       case guessTypeByFileName (cnfExtToMIMEType conf) path of
-                         Nothing   -> return ()
-                         Just mime -> setContentType mime
+         tag  ← liftIO $ generateETagFromFile path
+         let lastMod = posixSecondsToUTCTime
+                       $ fromRational
+                       $ toRational
+                       $ modificationTime stat
+         foundEntity tag lastMod
 
-                       -- 實際にファイルを讀んで送る
-                       liftIO (B.readFile path) >>= outputLBS
-                  else
-                    abort Forbidden [] Nothing
-           else
-             foundNoEntity Nothing
+         conf ← getConfig
+         case guessTypeByFileName (cnfExtToMIMEType conf) path of
+           Nothing   → setContentType octetStream
+           Just mime → setContentType mime
 
+         when sendContent
+             $ liftIO (LBS.readFile path) ≫= putChunks
 
--- |Computation of @'generateETagFromFile' fpath@ generates a strong
--- entity tag from a file. The file doesn't necessarily have to be a
--- regular file; it may be a FIFO or a device file. The tag is made of
--- inode ID, size and modification time.
+-- |@'generateETagFromFile' fpath@ generates a strong entity tag from
+-- a file. The file doesn't necessarily have to be a regular file; it
+-- may be a FIFO or a device file. The tag is made of inode ID, size
+-- and modification time.
 --
 -- Note that the tag is not strictly strong because the file could be
 -- modified twice at a second without changing inode ID or size, but
--- it's not really possible to generate a strict strong ETag from a
--- file since we don't want to simply grab the entire file and use it
--- as an ETag. It is indeed possible to hash it with SHA-1 or MD5 to
+-- it's not really possible to generate a strictly strong ETag from a
+-- file as we don't want to simply grab the entire file and use it as
+-- an ETag. It is indeed possible to hash it with SHA-1 or MD5 to
 -- increase strictness, but it's too inefficient if the file is really
 -- large (say, 1 TiB).
-generateETagFromFile :: FilePath -> IO ETag
+generateETagFromFile ∷ FilePath → IO ETag
 generateETagFromFile path
-    = path `seq`
-      do stat <- getFileStatus path
-         let inode   = fromEnum $! fileID   stat
-             size    = fromEnum $! fileSize stat
-             lastMod = fromEnum $! modificationTime stat
-             tag     = fmtHex False 0 inode
-                       ++ "-" ++
-                       fmtHex False 0 size
-                       ++ "-" ++
-                       fmtHex False 0 lastMod
-         return $! strongETag tag
+    = do stat ← getFileStatus path
+         let inode   = fileID   stat
+             size    = fileSize stat
+             lastMod = fromEnum $ modificationTime stat
+             tag     = A.fromAsciiBuilder
+                       $ A.unsafeFromBuilder
+                       $ BT.integral inode
+                       ⊕ BB.fromByteString "-"
+                       ⊕ BT.integral size
+                       ⊕ BB.fromByteString "-"
+                       ⊕ BT.integral lastMod
+         return $ strongETag tag
 
--- | @'staticDir' dir@ is a
--- 'Network.HTTP.Lucu.Resource.Tree.ResourceDef' which maps all files
--- in @dir@ and its subdirectories on the filesystem to the
+-- | @'staticDir' dir@ is a 'ResourceDef' which maps all files in
+-- @dir@ and its subdirectories on the filesystem to the
 -- 'Network.HTTP.Lucu.Resource.Tree.ResTree'.
-staticDir :: FilePath -> ResourceDef
+--
+-- Note that 'staticDir' currently doesn't have a directory-listing
+-- capability. Requesting the content of a directory will end up being
+-- replied with /403 Forbidden/.
+staticDir ∷ FilePath → ResourceDef
 staticDir path
-    = ResourceDef {
-        resUsesNativeThread = False
-      , resIsGreedy         = True
-      , resGet              = Just $! handleStaticDir path
-      , resHead             = Nothing
-      , resPost             = Nothing
-      , resPut              = Nothing
-      , resDelete           = Nothing
+    = emptyResource {
+        resIsGreedy = True
+      , resGet      = Just $ handleStaticDir True  path
+      , resHead     = Just $ handleStaticDir False path
       }
 
--- | Computation of @'handleStaticDir' dir@ maps all files in @dir@
--- and its subdirectories on the filesystem to the
--- 'Network.HTTP.Lucu.Resource.Tree.ResTree'. The
--- 'Network.HTTP.Lucu.Resource.Resource' must be in the /Examining
--- Request/ state before the computation. It will be in the /Done/
--- state after the computation.
---
--- If you just want to place a static directory tree on the
--- 'Network.HTTP.Lucu.Resource.Tree.ResTree', you had better use
--- 'staticDir' instead of this.
-handleStaticDir :: FilePath -> Resource ()
-handleStaticDir !basePath
-    = do extraPath <- getPathInfo
+-- TODO: implement directory listing.
+handleStaticDir ∷ Bool → FilePath → Resource ()
+handleStaticDir sendContent basePath
+    = do extraPath ← getPathInfo
          securityCheck extraPath
-         let path = basePath </> joinPath extraPath
-
-         handleStaticFile path
+         let path = basePath </> joinPath (map dec8 extraPath)
+         handleStaticFile sendContent path
     where
-      securityCheck :: Monad m => [String] -> m ()
-      securityCheck !pathElems
-          = when (any (== "..") pathElems) $ fail ("security error: "
-                                                   ++ joinWith "/" pathElems)
--- TODO: implement directory listing.
+      dec8 ∷ ByteString → String
+      dec8 = T.unpack ∘ T.decodeUtf8
+
+securityCheck ∷ (Eq s, Show s, IsString s, Monad m) ⇒ [s] → m ()
+securityCheck pathElems
+    = when (any (≡ "..") pathElems)
+          $ fail ("security error: " ⧺ show pathElems)
index dbc65ac17ad8a52f553af26e5506a33df9aab137..3d38b8b3aec36c9dff990cb4c7e66d05995ad27e 100644 (file)
@@ -1,74 +1,68 @@
 {-# LANGUAGE
-    BangPatterns
+    OverloadedStrings
   , UnicodeSyntax
   #-}
 -- |Utility functions used internally in the Lucu httpd. These
 -- functions may be useful too for something else.
 module Network.HTTP.Lucu.Utils
     ( splitBy
-    , joinWith
-    , trim
-    , isWhiteSpace
     , quoteStr
     , parseWWWFormURLEncoded
+    , splitPathInfo
+    , show3
     )
     where
+import Blaze.ByteString.Builder.ByteString as B
+import Blaze.Text.Int as BT
 import Control.Monad
-import Data.List     hiding (last)
+import Data.Ascii (Ascii, AsciiBuilder)
+import qualified Data.Ascii as A
+import Data.ByteString (ByteString)
+import qualified Data.ByteString.Char8 as BS
+import Data.List hiding (last)
+import Data.Monoid.Unicode
 import Network.URI
-import Prelude       hiding (last)
+import Prelude hiding (last)
 import Prelude.Unicode
 
 -- |> splitBy (== ':') "ab:c:def"
 --  > ==> ["ab", "c", "def"]
-splitBy :: (a -> Bool) -> [a] -> [[a]]
+splitBy ∷ (a → Bool) → [a] → [[a]]
+{-# INLINEABLE splitBy #-}
 splitBy isSep src
-    = case break isSep src
-      of (last , []       ) -> [last]
-         (first, _sep:rest) -> first : splitBy isSep rest
-
--- |> joinWith ":" ["ab", "c", "def"]
---  > ==> "ab:c:def"
-joinWith :: [a] -> [[a]] -> [a]
-joinWith = (join .) . intersperse
-
--- |> trim (== '_') "__ab_c__def___"
---  > ==> "ab_c__def"
-trim :: (a -> Bool) -> [a] -> [a]
-trim !p = trimTail . trimHead
-    where
-      trimHead = dropWhile p
-      trimTail = reverse . trimHead . reverse
-
--- |@'isWhiteSpace' c@ is 'Prelude.True' iff c is one of SP, HT, CR
--- and LF.
-isWhiteSpace :: Char -> Bool
-isWhiteSpace ' '  = True
-isWhiteSpace '\t' = True
-isWhiteSpace '\r' = True
-isWhiteSpace '\n' = True
-isWhiteSpace _    = False
-{-# INLINE isWhiteSpace #-}
+    = case break isSep src of
+        (last , []       ) → [last]
+        (first, _sep:rest) → first : splitBy isSep rest
 
 -- |> quoteStr "abc"
 --  > ==> "\"abc\""
 --
 --  > quoteStr "ab\"c"
 --  > ==> "\"ab\\\"c\""
-quoteStr :: String -> String
-quoteStr !str = concat (["\""] ++ map quote str ++ ["\""])
+quoteStr ∷ Ascii → AsciiBuilder
+quoteStr str = A.toAsciiBuilder "\"" ⊕
+               go (A.toByteString str) (∅) ⊕
+               A.toAsciiBuilder "\""
     where
-      quote :: Char -> String
-      quote '"' = "\\\""
-      quote c   = [c]
+      go ∷ BS.ByteString → AsciiBuilder → AsciiBuilder
+      go bs ab
+          = case BS.break (≡ '"') bs of
+              (x, y)
+                  | BS.null y → ab ⊕ b2ab x
+                  | otherwise → go (BS.tail y) (ab ⊕ b2ab x
+                                                   ⊕ A.toAsciiBuilder "\\\"")
 
+      b2ab ∷ BS.ByteString → AsciiBuilder
+      b2ab = A.toAsciiBuilder ∘ A.unsafeFromByteString
 
 -- |> parseWWWFormURLEncoded "aaa=bbb&ccc=ddd"
 --  > ==> [("aaa", "bbb"), ("ccc", "ddd")]
-parseWWWFormURLEncoded ∷ String → [(String, String)]
+parseWWWFormURLEncoded ∷ Ascii → [(ByteString, ByteString)]
 parseWWWFormURLEncoded src
-    | null src  = []
-    | otherwise = do pairStr ← splitBy (\ c → c ≡ ';' ∨ c ≡ '&') src
+    -- THINKME: We could gain some performance by using attoparsec
+    -- here.
+    | src ≡ ""  = []
+    | otherwise = do pairStr ← splitBy (\ c → c ≡ ';' ∨ c ≡ '&') (A.toString src)
                      let (key, value) = break (≡ '=') pairStr
                      return ( unescape key
                             , unescape $ case value of
@@ -76,9 +70,31 @@ parseWWWFormURLEncoded src
                                            val       → val
                             )
     where
-      unescape ∷ String → String
-      unescape = unEscapeString ∘ map plusToSpace
+      unescape ∷ String → ByteString
+      unescape = BS.pack ∘ unEscapeString ∘ map plusToSpace
 
       plusToSpace ∷ Char → Char
       plusToSpace '+' = ' '
       plusToSpace c   = c
+
+-- |> splitPathInfo "http://example.com/foo/bar"
+--  > ==> ["foo", "bar"]
+splitPathInfo ∷ URI → [ByteString]
+splitPathInfo uri
+    = let reqPathStr = uriPath uri
+          reqPath    = [unEscapeString x | x ← splitBy (≡ '/') reqPathStr, (¬) (null x)]
+      in
+        map BS.pack reqPath
+
+-- |> show3 5
+--  > ==> "005"
+show3 ∷ Integral n ⇒ n → AsciiBuilder
+{-# INLINEABLE show3 #-}
+show3 = A.unsafeFromBuilder ∘ go
+    where
+      go i | i ≥ 0 ∧ i < 10   = B.fromByteString "00" ⊕ BT.digit    i
+           | i ≥ 0 ∧ i < 100  = B.fromByteString "0"  ⊕ BT.integral i
+           | i ≥ 0 ∧ i < 1000 =                         BT.integral i
+           | otherwise        = error ("show3: the integer i must satisfy 0 <= i < 1000: " ⧺ show i)
+-- FIXME: Drop this function as soon as possible, to eliminate the
+-- dependency on blaze-textual.
index 717a9306079bb5beabbbb7e653846c149d26880f..d113d8227d0d2aa7a506345a35338945dd0308bb 100644 (file)
@@ -5,7 +5,7 @@ type: :task
 component: Lucu
 release: Lucu-1.0
 reporter: PHO <pho@cielonegro.org>
-status: :unstarted
+status: :in_progress
 disposition: 
 creation_time: 2011-07-29 16:01:14.666629 Z
 references: []
@@ -20,4 +20,8 @@ log_events:
   - PHO <pho@cielonegro.org>
   - assigned to release Lucu-1.0 from unassigned
   - ""
-git_branch: 
+- - 2011-07-30 11:17:25.622897 Z
+  - PHO <pho@cielonegro.org>
+  - changed status from unstarted to in_progress
+  - ""
+git_branch: attoparsec
index 76f9120050df7c278294e7abaf5c8d863fbfb361..3e454e7b620c75acf297d3d6d0aa643f5a214857 100644 (file)
@@ -5,7 +5,7 @@ type: :task
 component: Lucu
 release: Lucu-1.0
 reporter: PHO <pho@cielonegro.org>
-status: :unstarted
+status: :in_progress
 disposition: 
 creation_time: 2010-03-12 06:56:06.939283 Z
 references: []
@@ -20,4 +20,8 @@ log_events:
   - PHO <pho@cielonegro.org>
   - assigned to release Lucu-1.0 from unassigned
   - ""
-git_branch: 
+- - 2011-07-30 11:17:19.173203 Z
+  - PHO <pho@cielonegro.org>
+  - changed status from unstarted to in_progress
+  - ""
+git_branch: attoparsec
diff --git a/bugs/issue-a5e6a89da31d2ca0a69d89ad1d579fee8d0c131f.yaml b/bugs/issue-a5e6a89da31d2ca0a69d89ad1d579fee8d0c131f.yaml
new file mode 100644 (file)
index 0000000..43cf56d
--- /dev/null
@@ -0,0 +1,25 @@
+--- !ditz.rubyforge.org,2008-03-06/issue 
+title: "Add a configuration flag -fSSL to enable SSL support (default: off)"
+desc: |-
+  Reason #1: SSL support isn't essential for Lucu.
+  Reason #2: We have toooo many dependencies now, want to drop at least HsOpenSSL.
+type: :task
+component: Lucu
+release: Lucu-1.0
+reporter: PHO <pho@cielonegro.org>
+status: :unstarted
+disposition: 
+creation_time: 2011-10-26 23:04:33.719311 Z
+references: []
+
+id: a5e6a89da31d2ca0a69d89ad1d579fee8d0c131f
+log_events: 
+- - 2011-10-26 23:04:35.031007 Z
+  - PHO <pho@cielonegro.org>
+  - created
+  - ""
+- - 2011-10-27 17:33:31.904875 Z
+  - PHO <pho@cielonegro.org>
+  - edited title
+  - Should be defaulted to off!
+git_branch: 
index 661d6123d9053ce23be2581e15ef12f3a185896a..8469a0aead09958ca3f5f278b17be6f42fc5c5ce 100644 (file)
@@ -5,7 +5,7 @@ type: :task
 component: Lucu
 release: Lucu-1.0
 reporter: PHO <pho@cielonegro.org>
-status: :unstarted
+status: :in_progress
 disposition: 
 creation_time: 2011-07-30 04:39:53.073102 Z
 references: []
@@ -16,4 +16,8 @@ log_events:
   - PHO <pho@cielonegro.org>
   - created
   - ""
-git_branch: 
+- - 2011-07-30 11:17:28.677836 Z
+  - PHO <pho@cielonegro.org>
+  - changed status from unstarted to in_progress
+  - ""
+git_branch: attoparsec
diff --git a/bugs/issue-ce2851ba49c154838b48e56ecf4c01840e4c1b7c.yaml b/bugs/issue-ce2851ba49c154838b48e56ecf4c01840e4c1b7c.yaml
new file mode 100644 (file)
index 0000000..ea7b4e9
--- /dev/null
@@ -0,0 +1,21 @@
+--- !ditz.rubyforge.org,2008-03-06/issue 
+title: "Consider adding a configuration flag -funix (default: on)"
+desc: |-
+  Disabling it makes generateETagFromFile unavailable but drops
+  dependency to the unix package instead.
+type: :task
+component: Lucu
+release: Lucu-1.0
+reporter: PHO <pho@cielonegro.org>
+status: :unstarted
+disposition: 
+creation_time: 2011-10-26 23:18:42.168974 Z
+references: []
+
+id: ce2851ba49c154838b48e56ecf4c01840e4c1b7c
+log_events: 
+- - 2011-10-26 23:18:43.753104 Z
+  - PHO <pho@cielonegro.org>
+  - created
+  - ""
+git_branch: 
diff --git a/bugs/issue-e0312227f40a0fa92d4c5d69a64dad473f54389a.yaml b/bugs/issue-e0312227f40a0fa92d4c5d69a64dad473f54389a.yaml
new file mode 100644 (file)
index 0000000..1dfd5b4
--- /dev/null
@@ -0,0 +1,27 @@
+--- !ditz.rubyforge.org,2008-03-06/issue 
+title: Introduce a type class 'Dispatcher' and make ResTree/FallbackHandler instances of it.
+desc: And the dispatcher data type should form a Monoid.
+type: :task
+component: Lucu
+release: Lucu-1.0
+reporter: PHO <pho@cielonegro.org>
+status: :unstarted
+disposition: 
+creation_time: 2011-10-17 02:46:21.854704 Z
+references: []
+
+id: e0312227f40a0fa92d4c5d69a64dad473f54389a
+log_events: 
+- - 2011-10-17 02:46:22.826524 Z
+  - PHO <pho@cielonegro.org>
+  - created
+  - ""
+- - 2011-10-17 02:48:13.741801 Z
+  - PHO <pho@cielonegro.org>
+  - commented
+  - We should implement name-based virtualhosts at the same time.
+- - 2011-10-17 02:51:52.045280 Z
+  - PHO <pho@cielonegro.org>
+  - commented
+  - FallbackHandler should be either a non-pure function (MonadIO) or a pure function returning Maybe ResourceDef.
+git_branch: 
index 2363b98c6d4f6d38725bc0352c8cf6de2a08b9cf..cc534f4648d16242d8d4f33147d30acee9b61f7d 100644 (file)
@@ -18,6 +18,7 @@ HPC      ?= hpc
 DITZ     ?= ditz
 
 CONFIGURE_ARGS ?= --disable-optimization
+HLINT_OPTS     ?= --cross --report=dist/report.html
 
 SETUP_FILE := $(wildcard Setup.*hs)
 CABAL_FILE := $(wildcard *.cabal)
@@ -99,6 +100,13 @@ test: build
 ditz:
        $(DITZ) html dist/ditz
 
+ChangeLog:
+       rm -f $@
+       $(DITZ) releases | awk '{print $$1}' | sort --reverse | while read i; do \
+               $(DITZ) changelog $$i >> $@; \
+       done
+       head $@
+
 fixme:
        @$(FIND) . \
                \( -name 'dist' -or -name '.git' -or -name '_darcs' \) -prune \
@@ -106,25 +114,33 @@ fixme:
                \( -name '*.c'   -or -name '*.h'   -or \
                   -name '*.hs'  -or -name '*.lhs' -or \
                   -name '*.hsc' -or -name '*.cabal' \) \
-               -exec egrep -i '(fixme|thinkme)' {} \+ \
+               -exec egrep 'FIXME|THINKME|TODO' {} \+ \
                || echo 'No FIXME or THINKME found.'
 
 lint:
-       $(HLINT) . --report
+       $(HLINT) . $(HLINT_OPTS)
+
+push: push-repo push-ditz push-doc
 
-push: doc ditz
+push-repo:
        if [ -d "_darcs" ]; then \
                darcs push; \
        elif [ -d ".git" ]; then \
                git push --all && git push --tags; \
        fi
+
+push-ditz: ditz
+       rsync -av --delete \
+               dist/ditz/ \
+               www@nem.cielonegro.org:static.cielonegro.org/htdocs/ditz/$(PKG_NAME)
+
+push-doc: doc
        if [ -d "dist/doc" ]; then \
                rsync -av --delete \
                        dist/doc/html/$(PKG_NAME)/ \
                        www@nem.cielonegro.org:static.cielonegro.org/htdocs/doc/$(PKG_NAME); \
        fi
-       rsync -av --delete \
-               dist/ditz/ \
-               www@nem.cielonegro.org:static.cielonegro.org/htdocs/ditz/$(PKG_NAME)
 
-.PHONY: build build-hook setup-config setup-config-hook run clean clean-hook install doc sdist test lint push
+.PHONY: build build-hook setup-config setup-config-hook run clean clean-hook \
+               install doc sdist test lint push push-repo push-ditz push-doc \
+               ChangeLog
index 811840670178be97fa220b74fa082ae5d526c082..9ba5b1ed0ed228b5a1ca83d5aac33f8fae1d21ba 100755 (executable)
@@ -1,10 +1,13 @@
 #!/usr/bin/env runghc
-
+{-# LANGUAGE
+    UnicodeSyntax
+  #-}
 import Network.HTTP.Lucu.MIMEType.Guess
 import System
 
-main = do [inFile, outFile] <- getArgs
-          extMap <- parseExtMapFile inFile
+main ∷ IO ()
+main = do [inFile, outFile] ← getArgs
+          extMap ← parseExtMapFile inFile
 
           let src = serializeExtMap
                     extMap
index 584c8d6c0053d87334ba3870a5604eb764c80534..c73c1f3c2e5416eea76156db425503848ad718d8 100644 (file)
@@ -1,5 +1,18 @@
-../Network/HTTP/Lucu/MIMEType/DefaultExtensionMap.hs: mime.types CompileMimeTypes
-       ./CompileMimeTypes $< $@
+../Network/HTTP/Lucu/MIMEType/DefaultExtensionMap.hs: dist/DefaultExtensionMap.hs
+       cp -f $< $@
 
-CompileMimeTypes: CompileMimeTypes.hs
-       ghc --make $@
+dist/DefaultExtensionMap.hs: mime.types compiler
+       ./CompileMimeTypes $< $@.tmp
+       if diff $@ $@.tmp >/dev/null; then \
+               rm -f $@.tmp; \
+       else \
+               mv -f $@.tmp $@; \
+       fi
+
+compiler:
+       ghc -Wall --make CompileMimeTypes -i.. -odir dist -hidir dist
+
+clean:
+       rm -rf dist DefaultExtensionMap.hs CompileMimeTypes
+
+.PHONY: clean compiler
index f65dd3246cb1ced4c40420aa283af982a4810431..7b7601b3af1af1c52dd8fa2c82e8945843cde2dc 100644 (file)
@@ -75,7 +75,6 @@ audio/mp4a-latm                       m4a m4p
 audio/mpeg                     mpga mp2 mp3
 audio/x-ac3         ac3
 audio/x-aiff                   aif aiff aifc
-audio/x-au              au snd
 audio/x-ircam           sf
 audio/x-flac            flac
 audio/x-mod             669 amf dsm gdm far imf it med mod mtm okt sam s3m stm stx ult xm
@@ -133,11 +132,13 @@ text/richtext                     rtx
 text/rtf                       rtf
 text/sgml                      sgml sgm
 text/tab-separated-values      tsv
-text/uri-list               ram
+text/uri-list               uni unis uri uris
 text/vnd.wap.wml               wml
 text/vnd.wap.wmlscript         wmls
+text/x-c                    c h
+text/x-c++                  cc cpp cxx hpp hxx
 text/x-cabal                cabal
-text/x-haskell          hs
+text/x-haskell          hs hsc lhs
 text/x-setext                  etx
 video/mp4                      mp4
 video/mpeg                     mpeg mpg mpe
index dacd4c363604b4694430c6dcf68e91e0ea91d7b9..d7e0071c8767f72caf9e9bc72c0ef08e748c3b66 100644 (file)
@@ -1,40 +1,39 @@
+{-# LANGUAGE
+    OverloadedStrings
+  , UnicodeSyntax
+  #-}
+import Control.Applicative
+import Control.Monad.Unicode
+import qualified Data.ByteString.Lazy.Char8 as Lazy
 import Network.HTTP.Lucu
 
-main :: IO ()
+main  IO ()
 main = let config    = defaultConfig { cnfServerPort = "9999" }
-           resources = mkResTree [ ( []
-                                   , helloWorld )
-
-                                 , ( ["urandom"]
-                                   , staticFile "/dev/urandom" )
-
-                                 , ( ["inc"]
-                                   , staticDir "/usr/include" )
-                                 ]
-           fallbacks = [ \ path -> case path of
-                                     ["hello"] -> return $ Just helloWorld
-                                     _         -> return Nothing
+           resources = mkResTree
+                       [ ([]         , helloWorld               )
+                       , (["urandom"], staticFile "/dev/urandom")
+                       , (["inc"    ], staticDir "/usr/include" )
+                       ]
+           fallbacks = [ \ path → case path of
+                                     ["hello"] → return $ Just helloWorld
+                                     _         → return Nothing
                        ]
        in
          do putStrLn "Access http://localhost:9999/ with your browser."
             runHttpd config resources fallbacks
 
-
-helloWorld :: ResourceDef
+helloWorld ∷ ResourceDef
 helloWorld
     = emptyResource {
         resGet
-          = Just $ do --time <- liftIO $ getClockTime
-                      --foundEntity (strongETag "abcde") time
-                      setContentType $ read "text/hello"
-                      outputChunk "Hello, "
-                      outputChunk "World!\n"
-                      outputChunk =<< getRemoteAddr'
-                      
+          = Just $ do setContentType $ parseMIMEType "text/hello"
+                      putChunk "Hello, "
+                      putChunk "World!\n"
+                      putChunks =≪ Lazy.pack <$> getRemoteAddr'
       , resPost
-          = Just $ do str1 <- inputChunk 3
-                      str2 <- inputChunk 3
-                      str3 <- inputChunk 3
-                      setContentType $ read "text/hello"
-                      output ("[" ++ str1 ++ " - " ++ str2 ++ "#" ++ str3 ++ "]")
-      }
\ No newline at end of file
+          = Just $ do str1 ← getChunk 3
+                      str2 ← getChunk 3
+                      str3 ← getChunk 3
+                      setContentType $ parseMIMEType "text/hello"
+                      putChunks $ Lazy.fromChunks ["[", str1, " - ", str2, "#", str3, "]"]
+      }
index 68423086a2472ac51fa1d777ecf9ac412fbe2a97..82d98e74d4c7a683f255fca5b0e2565eb5945ec6 100644 (file)
@@ -1,10 +1,12 @@
+{-# LANGUAGE
+    UnicodeSyntax
+  #-}
 import MiseRafturai
 import Network.HTTP.Lucu
 
-main :: IO ()
+main  IO ()
 main = let config    = defaultConfig { cnfServerPort = "9999" }
            resources = mkResTree [ ([], miseRafturai) ]
        in
          do putStrLn "Access http://localhost:9999/ with your browser."
             runHttpd config resources []
-                                   
\ No newline at end of file
index af35b6320979db7d247649b90e3041e66ee1e4bb..a985cae482d20037d125cda916bd3dbef756f33b 100644 (file)
@@ -1,7 +1,10 @@
+{-# LANGUAGE
+    UnicodeSyntax
+  #-}
 import Network.HTTP.Lucu
 import SmallFile
 
-main :: IO ()
+main  IO ()
 main = let config    = defaultConfig { cnfServerPort = "9999" }
            resources = mkResTree [ ([], smallFile) ]
        in
index abd928eb482b505dc60b8a194b7d65d2e29192ba..0902512ce6eba5e77918566db661895ee728266a 100644 (file)
@@ -1,29 +1,32 @@
 TARGETS = \
        HelloWorld \
-       MiseRafturai \
        Implanted \
        ImplantedSmall \
        Multipart \
        SSL \
        $(NULL)
 
+IMPLANT ?= ../dist/build/lucu-implant-file/lucu-implant-file
+
 build: $(TARGETS)
 
 %: %.hs
-       ghc --make $@ -threaded -O3 -fwarn-unused-imports
+       ghc -Wall --make $@ -threaded -O3 -idist -odir dist -hidir dist
 
 run: build
        ./HelloWorld
 
 clean:
-       rm -f $(TARGETS) *.hi *.o MiseRafturai.hs SmallFile.hs
-
-MiseRafturai.hs: mise-rafturai.html
-       lucu-implant-file -m MiseRafturai -o $@ $<
+       rm -rf $(TARGETS) dist
 
-ImplantedSmall.hs: SmallFile.hs
+Implanted.hs: dist/MiseRafturai.hs
+dist/MiseRafturai.hs: mise-rafturai.html $(IMPLANT)
+       mkdir -p dist
+       $(IMPLANT) -m MiseRafturai -o $@ $<
 
-SmallFile.hs: small-file.txt
-       lucu-implant-file -m SmallFile -o $@ $<
+ImplantedSmall.hs: dist/SmallFile.hs
+dist/SmallFile.hs: small-file.txt $(IMPLANT)
+       mkdir -p dist
+       $(IMPLANT) -m SmallFile -o $@ $<
 
 .PHONY: build run clean
index 69c4125accd5a97c54f909dea9141fd5866062c8..8ddc6189be39a8ad942d372671819fd6f066e53f 100644 (file)
@@ -1,8 +1,15 @@
-import qualified Data.ByteString.Lazy.Char8 as L8
+{-# LANGUAGE
+    OverloadedStrings
+  , UnicodeSyntax
+  #-}
+import qualified Data.ByteString.Lazy.Char8 as Lazy
+import Control.Applicative
+import Control.Monad.Unicode
 import Data.Maybe
+import Data.Monoid.Unicode
 import Network.HTTP.Lucu
 
-main :: IO ()
+main  IO ()
 main = let config    = defaultConfig { cnfServerPort = "9999" }
            resources = mkResTree [ ([], resMain) ]
        in
@@ -10,30 +17,25 @@ main = let config    = defaultConfig { cnfServerPort = "9999" }
             runHttpd config resources []
 
 
-resMain :: ResourceDef
+resMain  ResourceDef
 resMain 
-    = ResourceDef {
-        resUsesNativeThread = False
-      , resIsGreedy         = False
-      , resGet
-          = Just $ do setContentType $ read "text/html"
-                      output ("<title>Multipart Form Test</title>" ++
-                              "<form action=\"/\" method=\"post\" enctype=\"multipart/form-data\">" ++
-                              "  Upload some file:" ++
-                              "  <input type=\"text\" name=\"text\">" ++
-                              "  <input type=\"file\" name=\"file\">" ++
-                              "  <input type=\"submit\" value=\"Submit\">" ++
-                              "</form>")
-      , resHead   = Nothing
+    = emptyResource {
+        resGet
+          = Just $ do setContentType $ parseMIMEType "text/html"
+                      putChunks $ "<title>Multipart Form Test</title>\n"
+                                ⊕ "<form action=\"/\" method=\"post\" enctype=\"multipart/form-data\">\n"
+                                ⊕ "  Upload some file:\n"
+                                ⊕ "  <input type=\"text\" name=\"text\">\n"
+                                ⊕ "  <input type=\"file\" name=\"file\">\n"
+                                ⊕ "  <input type=\"submit\" value=\"Submit\">\n"
+                                ⊕ "</form>\n"
       , resPost
-          = Just $ do form <- inputForm defaultLimit
-                      let text     = fromMaybe L8.empty $ fmap fdContent $ lookup "text" form
-                          file     = fromMaybe L8.empty $ fmap fdContent $ lookup "file" form
-                          fileName = fdFileName =<< lookup "file" form
-                      setContentType $ read "text/plain"
-                      outputChunk ("You entered \"" ++ L8.unpack text ++ "\".\n")
-                      outputChunk ("You uploaded a " ++ show (L8.length file) ++ " bytes long file.\n")
-                      output ("The file name is " ++ show fileName ++ ".\n")
-      , resPut    = Nothing
-      , resDelete = Nothing
-      }
\ No newline at end of file
+          = Just $ do form ← getForm Nothing
+                      let text     = fromMaybe (∅) $ fdContent <$> lookup "text" form
+                          file     = fromMaybe (∅) $ fdContent <$> lookup "file" form
+                          fileName = fdFileName =≪ lookup "file" form
+                      setContentType $ parseMIMEType "text/plain"
+                      putChunks $ "You entered \"" ⊕ text ⊕ "\".\n"
+                      putChunks $ "You uploaded a " ⊕ Lazy.pack (show $ Lazy.length file) ⊕ " bytes long file.\n"
+                      putChunks $ "The file name is " ⊕ Lazy.pack (show fileName) ⊕ ".\n"
+      }
index 436749fdc01fe7a2a081fec88831414d815c5fe5..6df2ab714e37c5ce4c265c8d10c1f176b5632894 100644 (file)
@@ -1,20 +1,26 @@
-{-# LANGUAGE PackageImports #-}
-import           Control.Monad
-import "mtl"     Control.Monad.Trans
-import           Data.Time.Clock
-import           Network.HTTP.Lucu
-import           OpenSSL
-import           OpenSSL.EVP.PKey
-import           OpenSSL.RSA
+{-# LANGUAGE
+    OverloadedStrings
+  , PackageImports
+  , UnicodeSyntax
+  #-}
+import Control.Applicative
+import "mtl" Control.Monad.Trans
+import Control.Monad.Unicode
+import qualified Data.ByteString.Lazy.Char8 as Lazy
+import Data.Time.Clock
+import Network.HTTP.Lucu
+import OpenSSL
+import OpenSSL.EVP.PKey
+import OpenSSL.RSA
 import qualified OpenSSL.Session as SSL
-import           OpenSSL.X509
+import OpenSSL.X509
 
-main :: IO ()
+main  IO ()
 main = withOpenSSL $
-       do ctx  <- SSL.context
+       do ctx   SSL.context
 
-          key  <- generateRSAKey 1024 3 Nothing
-          cert <- genCert key
+          key   generateRSAKey 1024 3 Nothing
+          cert  genCert key
           SSL.contextSetPrivateKey     ctx key
           SSL.contextSetCertificate    ctx cert
           SSL.contextSetDefaultCiphers ctx
@@ -26,42 +32,32 @@ main = withOpenSSL $
                                             , sslContext    = ctx
                                             }
                           }
-              resources = mkResTree [ ( []
-                                      , helloWorld )
-                                    ]
+              resources = mkResTree [ ([], helloWorld) ]
           putStrLn "Access https://localhost:9001/ with your browser."
           runHttpd config resources []
 
-
-helloWorld :: ResourceDef
+helloWorld ∷ ResourceDef
 helloWorld 
-    = ResourceDef {
-        resUsesNativeThread = False
-      , resIsGreedy         = False
-      , resGet
-          = Just $ do setContentType $ read "text/plain"
-                      outputChunk "getRemoteCertificate = "
-                      cert <- do c <- getRemoteCertificate
-                                 case c of
-                                   Just c  -> liftIO $ printX509 c
-                                   Nothing -> return "Nothing"
-                      outputChunk cert
-      , resHead   = Nothing
-      , resPost   = Nothing
-      , resPut    = Nothing
-      , resDelete = Nothing
+    = emptyResource {
+        resGet
+          = Just $ do setContentType $ parseMIMEType "text/plain"
+                      putChunk "getRemoteCertificate = "
+                      cert ← do cert ← getRemoteCertificate
+                                case cert of
+                                  Just c  → liftIO $ Lazy.pack <$> printX509 c
+                                  Nothing → return "Nothing"
+                      putChunks cert
       }
 
-
-genCert :: KeyPair k => k -> IO X509
+genCert ∷ KeyPair k ⇒ k → IO X509
 genCert pkey
-    = do cert <- newX509
+    = do cert  newX509
          setVersion      cert 2
          setSerialNumber cert 1
          setIssuerName   cert [("CN", "localhost")]
          setSubjectName  cert [("CN", "localhost")]
-         setNotBefore    cert =<< liftM (addUTCTime (-1)) getCurrentTime
-         setNotAfter     cert =<< liftM (addUTCTime (365 * 24 * 60 * 60)) getCurrentTime
+         setNotBefore    cert =≪ addUTCTime (-1)                 <$> getCurrentTime
+         setNotAfter     cert =≪ addUTCTime (365 * 24 * 60 * 60) <$> getCurrentTime
          setPublicKey    cert pkey
          signX509        cert pkey Nothing
          return cert
\ No newline at end of file