webserver 0.5.1.0 → 0.6.0.0
raw patch · 5 files changed
+205/−92 lines, 5 filesdep +directorydep +stmPVP ok
version bump matches the API change (PVP)
Dependencies added: directory, stm
API changes (from Hackage documentation)
+ Network.Web.Params: FkAccept :: FieldKey
+ Network.Web.Server.Basic: serveHTTP :: Maybe FilePath -> Int -> ByteString -> (Request -> Path) -> IO ()
+ Network.Web.Server.Params: Handler :: (IO Response) -> Path
+ Network.Web.Server.Params: defaultConfig :: BasicConfig
+ Network.Web.Server.Params: defaultInfo :: FilePath -> IO (Maybe (Integer, UTCTime))
+ Network.Web.Server.Params: defaultObtain :: FilePath -> Maybe (Integer, Integer) -> IO ByteString
- Network.Web.Server.Params: BasicConfig :: (URI -> Path) -> (FilePath -> Maybe (Integer, Integer) -> IO ByteString) -> (FilePath -> IO (Maybe (Integer, UTCTime))) -> ByteString -> TCPInfo -> BasicConfig
+ Network.Web.Server.Params: BasicConfig :: (Request -> Path) -> (FilePath -> Maybe (Integer, Integer) -> IO ByteString) -> (FilePath -> IO (Maybe (Integer, UTCTime))) -> ByteString -> TCPInfo -> BasicConfig
- Network.Web.Server.Params: mapper :: BasicConfig -> URI -> Path
+ Network.Web.Server.Params: mapper :: BasicConfig -> Request -> Path
Files
- Network/Web/Params.hs +29/−44
- Network/Web/Server.hs +1/−0
- Network/Web/Server/Basic.hs +116/−42
- Network/Web/Server/Params.hs +56/−4
- webserver.cabal +3/−2
Network/Web/Params.hs view
@@ -143,7 +143,8 @@ {-| Field key of HTTP header. -}-data FieldKey = FkAcceptLanguage+data FieldKey = FkAccept+ | FkAcceptLanguage | FkCacheControl | FkConnection | FkContentLength@@ -165,47 +166,29 @@ | FkOther S.ByteString deriving (Eq,Show,Ord) -fieldKeyList :: [FieldKey]-fieldKeyList = [ FkAcceptLanguage- , FkCacheControl- , FkConnection- , FkContentLength- , FkContentRange- , FkContentType- , FkCookie- , FkDate- , FkHost- , FkIfModifiedSince- , FkIfRange- , FkIfUnmodifiedSince- , FkLastModified- , FkLocation- , FkRange- , FkServer- , FkSetCookie2- , FkStatus- , FkTransferEncoding ]--fieldStringList :: [S.ByteString]-fieldStringList = [ "Accept-Language"- , "Cache-Control"- , "Connection"- , "Content-Length"- , "Content-Range"- , "Content-Type"- , "Cookie"- , "Date"- , "Host"- , "If-Modified-Since"- , "If-Range"- , "If-Unmodified-Since"- , "Last-Modified"- , "Location"- , "Range"- , "Server"- , "Set-Cookie2"- , "Status"- , "Transfer-Encoding" ]+fieldKeyStringList :: [(FieldKey, S.ByteString)]+fieldKeyStringList =+ [ (FkAccept , "Accept")+ , (FkAcceptLanguage , "Accept-Language")+ , (FkCacheControl , "Cache-Control")+ , (FkConnection , "Connection")+ , (FkContentLength , "Content-Length")+ , (FkContentRange , "Content-Range")+ , (FkContentType , "Content-Type")+ , (FkCookie , "Cookie")+ , (FkDate , "Date")+ , (FkHost , "Host")+ , (FkIfModifiedSince , "If-Modified-Since")+ , (FkIfRange , "If-Range")+ , (FkIfUnmodifiedSince , "If-Unmodified-Since")+ , (FkLastModified , "Last-Modified")+ , (FkLocation , "Location")+ , (FkRange , "Range")+ , (FkServer , "Server")+ , (FkSetCookie2 , "Set-Cookie2")+ , (FkStatus , "Status")+ , (FkTransferEncoding , "Transfer-Encoding" )+ ] {-| Field value of HTTP header.@@ -213,10 +196,12 @@ type FieldValue = S.ByteString stringFieldKey :: M.Map FieldValue FieldKey-stringFieldKey = M.fromList (zip fieldStringList fieldKeyList)+stringFieldKey = M.fromList (map swap fieldKeyStringList)+ where+ swap (a,b) = (b,a) fieldKeyString :: M.Map FieldKey FieldValue-fieldKeyString = M.fromList (zip fieldKeyList fieldStringList)+fieldKeyString = M.fromList fieldKeyStringList {-| Converting field key to 'FieldKey'.
Network/Web/Server.hs view
@@ -1,6 +1,7 @@ {-| HTTP server library. -}+{-# OPTIONS -Wall #-} module Network.Web.Server (connection, WebServer, WebConfig(..)) where import Control.Applicative
Network/Web/Server/Basic.hs view
@@ -8,16 +8,24 @@ CGI, chunked data for CGI output; -} -module Network.Web.Server.Basic (basicServer,+{-# OPTIONS -Wall #-}+module Network.Web.Server.Basic (serveHTTP,+ basicServer, module Network.Web.Server.Params) where import Control.Applicative+import Control.Concurrent (forkIO)+import Control.Concurrent.MVar+import Control.Concurrent.STM (atomically)+import Control.Concurrent.STM.TChan+import Control.Exception (bracket) import Control.Monad import qualified Data.ByteString.Char8 as S import qualified Data.ByteString.Lazy.Char8 as L import Data.List import Data.Maybe import Data.Time+import Network (withSocketsDo, PortID(..), sClose, listenOn) import Network.TCPInfo import Network.Web.Date import Network.Web.HTTP@@ -28,12 +36,89 @@ import Network.Web.Server.Range import Network.Web.URI import System.FilePath+import System.IO (openFile, IOMode(..), BufferMode(..), hPutStrLn, hSetBuffering, hClose, stderr)+import System.Directory (createDirectoryIfMissing) import Text.Printf + ---------------------------------------------------------------- {-|+ Run an HTTP server, using a default BasicConfig.+-}+serveHTTP :: Maybe FilePath -- ^ Directory to write logfiles, "access.log" and "error.log". Will be created if it doesn't exist.+ -- if Nothing, errors will be written to stderr+ -> Int -- ^ HTTP port+ -> S.ByteString -- ^ Server name+ -> (Request -> Path) -- ^ site mapping function+ -> IO ()+serveHTTP m'logPath httpPort servName sitemap = do+ (accHandler,errHandler,logwait) <- case m'logPath of+ Nothing -> return (const (return ()), hPutStrLn stderr, return ())+ Just logPath -> do+ createDirectoryIfMissing True logPath+ acclogchan <- newTChanIO+ accsync <- newEmptyMVar+ errlogchan <- newTChanIO+ errsync <- newEmptyMVar+ let errlog = logPath </> "error.log"+ acclog = logPath </> "access.log"+ logwait = do+ atomically $ writeTChan acclogchan Nothing+ atomically $ writeTChan errlogchan Nothing+ mapM_ takeMVar [accsync, errsync]+ doAcc = atomically . writeTChan acclogchan . Just+ doErr = atomically . writeTChan errlogchan . Just++ _ <- forkIO $ logger acclog acclogchan accsync+ _ <- forkIO $ logger errlog errlogchan errsync+ return (doAcc, doErr, logwait)++ let cfg = WebConfig {+ closedHook = const $ return ()+ , accessHook = accHandler+ , errorHook = errHandler+ , fatalErrorHook = errHandler+ , connectionTimer = 2+ }+ topHandler tcpi = basicServer $ defaultConfig {+ serverName = servName+ , tcpInfo = tcpi+ , mapper = sitemap+ }+ runserver = withSocketsDo $ do+ sock <- listenOn (PortNumber $ fromIntegral httpPort)+ void $ mainLoop sock+ sClose sock+ mainLoop sock = do+ conn <- accept sock+ void $ forkIO (runConn conn)+ mainLoop sock+ runConn (hndl,tcpi) = do+ connection hndl (topHandler tcpi) cfg+ runserver+ logwait++logger :: FilePath -> TChan (Maybe String) -> MVar () -> IO ()+logger path chan sync = bracket opener closer go+ where+ opener = do+ h <- openFile path AppendMode+ hSetBuffering h LineBuffering+ return h+ closer h = do+ hClose h+ putMVar sync ()+ go h = do+ val <- atomically $ readTChan chan+ case val of+ Just msg -> hPutStrLn h msg >> go h+ Nothing -> hClose h >> putMVar sync ()++----------------------------------------------------------------++{-| Creating 'WebServer' with 'BasicConfig'. The created 'WebServer' can handle GET \/ HEAD \/ POST; OK, Not Found, Not Modified, Moved Permanently, etc;@@ -86,18 +171,16 @@ processGET :: BasicConfig -> Request -> IO Response processGET cnf req = do- let uri = reqURI req- langs = map ('.':) (languages req) ++ ["",".en"]- runAnyIO [ tryGet cnf req uri langs- , tryRedirect cnf uri langs+ let langs = map ('.':) (languages req) ++ ["",".en"]+ runAnyIO [ tryGet cnf req langs+ , tryRedirect cnf req langs , notFound ] -- always Just processHEAD :: BasicConfig -> Request -> IO Response processHEAD cnf req = do- let uri = reqURI req- langs = map ('.':) (languages req) ++ ["",".en"]- runAnyIO [ tryHead cnf uri langs- , tryRedirect cnf uri langs+ let langs = map ('.':) (languages req) ++ ["",".en"]+ runAnyIO [ tryHead cnf req langs+ , tryRedirect cnf req langs , notFound ] -- always Just processPOST :: BasicConfig -> Request -> IO Response@@ -108,24 +191,6 @@ ---------------------------------------------------------------- -(>>|) :: Maybe a -> (a -> IO (Maybe b)) -> IO (Maybe b)-v >>| act =- case v of- Nothing -> return Nothing- Just x -> act x--(|>|) :: IO (Maybe a) -> (a -> IO (Maybe b)) -> IO (Maybe b)-a |>| act = do- v <- a- case v of- Nothing -> return Nothing- Just x -> act x--(|||) :: Maybe Status -> Maybe Status -> Maybe Status-(|||) = mplus------------------------------------------------------------------- ifModifiedSince :: Request -> Maybe UTCTime ifModifiedSince = lookupAndParseDate FkIfModifiedSince @@ -138,12 +203,13 @@ lookupAndParseDate :: FieldKey -> Request -> Maybe UTCTime lookupAndParseDate key req = lookupField key req >>= parseDate -tryGet :: BasicConfig -> Request -> URI -> [String] -> IO (Maybe Response)-tryGet cnf req uri langs = tryGet' $ mapper cnf uri+tryGet :: BasicConfig -> Request -> [String] -> IO (Maybe Response)+tryGet cnf req langs = tryGet' $ mapper cnf req where tryGet' None = return Nothing tryGet' (File file) = tryGetFile cnf req file langs tryGet' (PathCGI cgi) = tryGetCGI cnf req cgi+ tryGet' (Handler hlr) = Just <$> hlr tryGetFile :: BasicConfig -> Request -> FilePath -> [String] -> IO (Maybe Response) tryGetFile cnf req file langs@@ -151,16 +217,19 @@ | otherwise = tryGetFile' cnf req file "" tryGetFile' :: BasicConfig -> Request -> FilePath -> String -> IO (Maybe Response)-tryGetFile' cnf req file lang = do- let file' = file ++ lang- info cnf file' |>| \(size, mtime) -> do+tryGetFile' cnf req file lang = info cnf file' >>= maybe (return Nothing) get+ where+ file' = file ++ lang+ get (size, mtime) = do let ext = takeExtension file ct = selectContentType ext modified = utcToDate mtime- mst = ifmodified req size mtime- ||| ifunmodified req size mtime- ||| ifrange req size mtime- ||| unconditional req size mtime+ mst = msum+ [ ifmodified req size mtime+ , ifunmodified req size mtime+ , ifrange req size mtime+ , unconditional req size mtime+ ] case mst of Just OK -> do val <- obtain cnf file' Nothing@@ -206,11 +275,12 @@ ---------------------------------------------------------------- -tryHead :: BasicConfig -> URI -> [String] -> IO (Maybe Response)-tryHead cnf uri langs = tryHead' (mapper cnf uri)+tryHead :: BasicConfig -> Request -> [String] -> IO (Maybe Response)+tryHead cnf req langs = tryHead' (mapper cnf req) where tryHead' None = return Nothing tryHead' (PathCGI _) = return Nothing+ tryHead' (Handler _) = return Nothing tryHead' (File file) = tryHeadFile cnf file langs tryHeadFile :: BasicConfig -> FilePath -> [String] -> IO (Maybe Response)@@ -237,12 +307,16 @@ then Nothing else Just uri { uriPath = path `S.append` "/" } -tryRedirect :: BasicConfig -> URI -> [String] -> IO (Maybe Response)-tryRedirect cnf uri langs =- redirectURI uri >>| \ruri -> tryRedirect' (mapper cnf ruri) ruri+tryRedirect :: BasicConfig -> Request -> [String] -> IO (Maybe Response)+tryRedirect cnf req langs = maybe (return Nothing)+ (\ruri -> tryRedirect' (mapper cnf $ rreq ruri) ruri)+ (redirectURI uri) where+ uri = reqURI req+ rreq ruri = req {reqURI = ruri} tryRedirect' None _ = return Nothing tryRedirect' (PathCGI _) _ = return Nothing+ tryRedirect' (Handler _) _ = return Nothing tryRedirect' (File file) ruri = runAnyMaybeIO $ map (tryRedirectFile cnf ruri file) langs tryRedirectFile :: BasicConfig -> URI -> FilePath -> String -> IO (Maybe Response)@@ -256,7 +330,7 @@ ---------------------------------------------------------------- tryPost :: BasicConfig -> Request -> IO Response-tryPost cnf req = case mapper cnf (reqURI req) of+tryPost cnf req = case mapper cnf req of -- never reached to undefined PathCGI cgi -> fromMaybe undefined <$> tryGetCGI cnf req cgi _ -> return responseBadRequest
Network/Web/Server/Params.hs view
@@ -1,17 +1,23 @@+{-# LANGUAGE OverloadedStrings #-}++{-# OPTIONS -Wall #-} module Network.Web.Server.Params where +import Control.Applicative ((<$>)) import qualified Data.ByteString.Char8 as S import qualified Data.ByteString.Lazy.Char8 as L import Data.Time+import Data.Time.Clock.POSIX import Network.TCPInfo-import Network.Web.URI+import Network.Web.HTTP+import System.Posix.Files {-| The configuration for the basic web server. -} data BasicConfig = BasicConfig { -- | A mapper from 'URI' to 'Path'.- mapper :: URI -> Path+ mapper :: Request -> Path -- | Resource obtaining function. The second argument is -- (offset of the resource, and length from the offset). , obtain :: FilePath -> Maybe (Integer,Integer) -> IO L.ByteString@@ -25,6 +31,19 @@ } {-|+ Default 'BasicConfig', with 'obtain', 'info', and 'serverName' filled in.+ It is necessary to override the 'mapper' and 'tcpInfo' fields+-}+defaultConfig :: BasicConfig+defaultConfig = BasicConfig+ { mapper = error "BasicConfig: no mapper defined"+ , obtain = defaultObtain+ , info = defaultInfo+ , serverName = "BasicConfig: no server name"+ , tcpInfo = error "BasicConfig: no TCPInfo"+ }++{-| Control information of how to handle 'URI'. -} data Path =@@ -34,13 +53,26 @@ | File FilePath -- | 'URI' is converted into CGI. | PathCGI CGI- deriving (Eq,Show)+ -- | 'URI' is converted into a handler callback+ | Handler (IO Response) +instance Eq Path where+ (File fp1) == (File fp2) = fp1 == fp2+ (PathCGI cgi1) == (PathCGI cgi2) = cgi1 == cgi2+ None == None = True+ _ == _ = False++instance Show Path where+ show None = "None"+ show (File fp) = "File " ++ show fp+ show (PathCGI cgi) = "PathCGI (" ++ show cgi ++ ")"+ show (Handler _) = "Handler"+ {-| Internal information of CGI converted from 'URI'. -} data CGI = CGI {- -- | A porgram path to be executed.+ -- | A program path to be executed. progPath :: FilePath -- | A script name. , scriptName :: String@@ -49,3 +81,23 @@ -- | A query string. , queryString :: String } deriving (Eq,Show)++-- | Get the size and modification time of a file, if possible.+defaultInfo :: FilePath -> IO (Maybe (Integer, UTCTime))+defaultInfo fp = do+ exists <- fileExist fp+ if exists+ then do+ status <- getFileStatus fp+ let size = fromIntegral $ fileSize status+ mt = posixSecondsToUTCTime . realToFrac $ modificationTime status+ return $ Just (size,mt)+ else return Nothing++-- | Obtain a data slice from a file as a lazy bytestring.+defaultObtain :: FilePath -> Maybe (Integer,Integer) -> IO L.ByteString+defaultObtain fp Nothing = L.readFile fp+defaultObtain fp (Just (offset,numBytes)) = L.take nb . L.drop ofs <$> L.readFile fp+ where+ nb = fromIntegral numBytes+ ofs = fromIntegral offset
webserver.cabal view
@@ -1,5 +1,5 @@ Name: webserver-Version: 0.5.1.0+Version: 0.6.0.0 Author: Kazu Yamamoto <kazu@iij.ad.jp> Maintainer: John W. Lato <jwlato@gmail.com> License: BSD3@@ -25,7 +25,8 @@ Network.Web.Server.Params Network.Web.Server.Range Build-Depends: base >= 4 && < 5, parsec >= 3,- network, bytestring, containers, old-locale,+ network, directory, bytestring, containers, old-locale,+ stm, filepath, time, unix, process, c10k Source-Repository head Type: git