X-Git-Url: http://git.cielonegro.org/gitweb.cgi?a=blobdiff_plain;f=Network%2FHTTP%2FLucu%2FResource%2FTree.hs;h=06fed17dd13c7d52af6d49e7f65ca3adcc164fb8;hb=f62b6f07bbf1eefcf552163d8f7daa6e0862ed5d;hp=062ffdca9217b5dd5462cf238fdb97e033b01ca7;hpb=c6b11025d1f81c668e9995e856b7bb34175230d3;p=Lucu.git diff --git a/Network/HTTP/Lucu/Resource/Tree.hs b/Network/HTTP/Lucu/Resource/Tree.hs index 062ffdc..06fed17 100644 --- a/Network/HTTP/Lucu/Resource/Tree.hs +++ b/Network/HTTP/Lucu/Resource/Tree.hs @@ -1,10 +1,14 @@ +{-# OPTIONS_HADDOCK prune #-} + +-- | Repository of the resources in httpd. module Network.HTTP.Lucu.Resource.Tree ( ResourceDef(..) - , Resource , ResTree + , FallbackHandler + , mkResTree -- [ ([String], ResourceDef) ] -> ResTree - , findResource -- ResTree -> URI -> Maybe ResourceDef + , findResource -- ResTree -> URI -> Maybe ([String], ResourceDef) , runResource -- ResourceDef -> Interaction -> IO ThreadId ) where @@ -12,45 +16,103 @@ module Network.HTTP.Lucu.Resource.Tree import Control.Concurrent import Control.Concurrent.STM import Control.Exception -import Control.Monad.Reader -import Data.Dynamic +import Control.Monad +import qualified Data.ByteString.Char8 as C8 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 +import Network.URI hiding (path) import System.IO -import System.IO.Error hiding (catch) import Prelude hiding (catch) -{- /aaa/bbb/ccc にアクセスされた時、もし /aaa/bbb に貪欲なリソースがあ - れば、假に /aaa/bbb/ccc に何らかのリソースがあったとしても必ず - /aaa/bbb が撰ばれる。/aaa/bbb のリソースが貪欲でなければ、それは無視 - される。 -} +-- |'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 +-- 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 { - resUsesNativeThread :: Bool - , resIsGreedy :: Bool - , resGet :: Maybe (Resource ()) - , resHead :: Maybe (Resource ()) - , resPost :: Maybe (Resource ()) - , resPut :: Maybe (Resource ()) - , resDelete :: Maybe (Resource ()) + -- |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 ())) } -type ResTree = ResNode -- root だから Map ではない -type ResSubtree = Map String ResNode -data ResNode = ResNode (Maybe ResourceDef) ResSubtree +-- |'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 +data ResNode = ResNode !(Maybe ResourceDef) !ResSubtree +-- |'mkResTree' converts a list of @(path, def)@ to a 'ResTree' e.g. +-- +-- @ +-- mkResTree [ ([] , 'Network.HTTP.Lucu.StaticFile.staticFile' \"\/usr\/include\/stdio.h\" ) -- \/ +-- , ([\"unistd\"], 'Network.HTTP.Lucu.StaticFile.staticFile' \"\/usr\/include\/unistd.h\") -- \/unistd +-- ] +-- @ mkResTree :: [ ([String], ResourceDef) ] -> ResTree -mkResTree list = processRoot list +mkResTree xs = xs `seq` processRoot xs where processRoot :: [ ([String], ResourceDef) ] -> ResTree processRoot list @@ -58,13 +120,13 @@ mkResTree list = processRoot list children = processNonRoot nonRoots in if null roots then - -- / にリソースが定義されない。/foo とかにはあるかも。 - ResNode Nothing children + -- "/" にリソースが定義されない。"/foo" とかにはあるかも。 + ResTree (ResNode Nothing children) else - -- / がある。 + -- "/" がある。 let (_, def) = last roots in - ResNode (Just def) children + ResTree (ResNode (Just def) children) processNonRoot :: [ ([String], ResourceDef) ] -> ResSubtree processNonRoot list @@ -86,103 +148,115 @@ mkResTree list = processRoot list subtree -findResource :: ResTree -> URI -> Maybe ResourceDef -findResource (ResNode rootDefM subtree) uri - = let pathStr = uriPath uri - path = [x | x <- splitBy (== '/') pathStr, x /= ""] - in - if null path then - rootDefM - else - walkTree subtree path +findResource :: ResTree -> [FallbackHandler] -> URI -> IO (Maybe ([String], ResourceDef)) +findResource (ResTree (ResNode rootDefM subtree)) fbs uri + = do let pathStr = uriPath uri + path = [x | x <- splitBy (== '/') pathStr, x /= ""] + foundInTree = if null path then + do def <- rootDefM + return (path, def) + else + walkTree subtree path [] + if isJust foundInTree then + return foundInTree + else + fallback path fbs where - walkTree :: ResSubtree -> [String] -> Maybe ResourceDef + walkTree :: ResSubtree -> [String] -> [String] -> Maybe ([String], ResourceDef) + + walkTree _ [] _ + = error "Internal error: should not reach here." - walkTree subtree (name:[]) - = case M.lookup name subtree of + walkTree tree (name:[]) soFar + = case M.lookup name tree of Nothing -> Nothing - Just (ResNode defM _) -> defM + Just (ResNode defM _) -> do def <- defM + return (soFar ++ [name], def) - walkTree subtree (x:xs) - = case M.lookup x subtree of + walkTree tree (x:xs) soFar + = case M.lookup x tree of Nothing -> Nothing Just (ResNode defM children) -> case defM of Just (ResourceDef { resIsGreedy = True }) - -> defM - _ -> walkTree children xs + -> do def <- defM + return (soFar ++ [x], def) + _ -> walkTree children xs (soFar ++ [x]) + + fallback :: [String] -> [FallbackHandler] -> IO (Maybe ([String], ResourceDef)) + fallback _ [] = return Nothing + 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 - = fork - $ catch ( runReaderT ( do fromMaybe notAllowed rsrc - driftTo Done - ) itr - ) - $ \ exc -> processException (itrConfig itr) exc + = 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) + fork = if resUsesNativeThread def then forkOS else forkIO - rsrc :: Maybe (Resource ()) - rsrc = case reqMethod $ fromJust $ itrRequest itr of - GET -> resGet def - HEAD -> case resHead def of - Just r -> Just r - Nothing -> resGet def - POST -> resPost def - PUT -> resPut def - DELETE -> resDelete def + 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 "Allow" $ joinWith ", " allowedMethods + setHeader (C8.pack "Allow") (C8.pack $ joinWith ", " allowedMethods) allowedMethods :: [String] - allowedMethods = nub $ foldr (++) [] [ methods resGet ["GET"] - , methods resHead ["GET", "HEAD"] - , methods resPost ["POST"] - , methods resPut ["PUT"] - , methods resDelete ["DELETE"] - ] + 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 -> [] - processException :: Config -> Exception -> IO () - processException conf exc - = do let abo = case exc of - ErrorCall msg -> Abortion InternalServerError [] msg - IOException ioE -> Abortion InternalServerError [] $ formatIOE ioE - DynException dynE -> case fromDynamic dynE of - Just (abo :: Abortion) -> abo - Nothing - -> Abortion InternalServerError [] - $ show exc - _ -> Abortion InternalServerError [] $ show exc + 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 runReaderT itr + flip runRes itr $ do setStatus $ aboStatus abo - -- FIXME: 同じ名前で複數の値があった時は、こ - -- れではまずいと思ふ。 - mapM_ (\ (name, value) -> setHeader name value) $ aboHeaders abo - setHeader "Content-Type" "application/xhtml+xml" - output $ aboPage conf abo + mapM_ (uncurry setHeader) $ fromHeaders $ aboHeaders abo + output $ abortPage conf reqM res abo else - hPutStrLn stderr $ show abo - - flip runReaderT itr $ driftTo Done + when (cnfDumpTooLateAbortionToStderr $ itrConfig itr) + $ hPutStrLn stderr $ show abo - formatIOE :: IOError -> String - formatIOE ioE = if isUserError ioE then - ioeGetErrorString ioE - else - show ioE + flip runRes itr $ driftTo Done