8 -- | Repository of the resources in httpd.
9 module Network.HTTP.Lucu.Resource.Tree
23 import Control.Applicative
24 import Data.Ascii (Ascii)
25 import qualified Data.Ascii as A
26 import Control.Concurrent
27 import Control.Concurrent.STM
28 import Control.Exception
30 import Data.Text (Text)
31 import qualified Data.Text as T
32 import qualified Data.Text.Lazy.Encoding as LT
34 import qualified Data.Map as M
37 import Data.Monoid.Unicode
38 import Network.HTTP.Lucu.Abortion
39 import Network.HTTP.Lucu.Config
40 import Network.HTTP.Lucu.DefaultPage
41 import Network.HTTP.Lucu.Headers (fromHeaders)
42 import Network.HTTP.Lucu.Request
43 import Network.HTTP.Lucu.Resource
44 import Network.HTTP.Lucu.Response
45 import Network.HTTP.Lucu.Interaction
46 import Network.HTTP.Lucu.Utils
47 import Network.URI hiding (path)
49 import Prelude hiding (catch)
50 import Prelude.Unicode
53 -- |'FallbackHandler' is an extra resource handler for resources which
54 -- can't be statically located anywhere in the resource tree. The Lucu
55 -- httpd first searches for a resource in the tree, and then calls
56 -- fallback handlers to ask them for a resource. If all of the
57 -- handlers returned 'Nothing', the httpd responds with 404 Not Found.
58 type FallbackHandler = [Text] → IO (Maybe ResourceDef)
61 -- "/aaa/bbb/ccc" にアクセスされた時、もし "/aaa/bbb" に貪欲なリソース
62 -- があれば、假に "/aaa/bbb/ccc" に何らかのリソースがあったとしても必ず
63 -- "/aaa/bbb" が撰ばれる。"/aaa/bbb" のリソースが貪欲でなければ、それは
66 -- | 'ResourceDef' is basically a set of 'Resource' monads for each
68 data ResourceDef = ResourceDef {
69 -- |Whether to run a 'Resource' on a native thread (spawned by
70 -- 'forkOS') or to run it on a user thread (spanwed by
71 -- 'forkIO'). Generally you don't need to set this field to
73 resUsesNativeThread ∷ !Bool
74 -- | Whether to be greedy or not.
76 -- Say a client is trying to access \/aaa\/bbb\/ccc. If there is a
77 -- greedy resource at \/aaa\/bbb, it is always chosen even if
78 -- there is another resource at \/aaa\/bbb\/ccc. If the resource
79 -- at \/aaa\/bbb is not greedy, it is just ignored. Greedy
80 -- resources are like CGI scripts.
82 -- | A 'Resource' to be run when a GET request comes for the
83 -- resource path. If 'resGet' is Nothing, the system responds
84 -- \"405 Method Not Allowed\" for GET requests.
86 -- It also runs for HEAD request if the 'resHead' is Nothing. In
87 -- this case 'output' and such like don't actually write a
89 , resGet ∷ !(Maybe (Resource ()))
90 -- | A 'Resource' to be run when a HEAD request comes for the
91 -- resource path. If 'resHead' is Nothing, the system runs
92 -- 'resGet' instead. If 'resGet' is also Nothing, the system
93 -- responds \"405 Method Not Allowed\" for HEAD requests.
94 , resHead ∷ !(Maybe (Resource ()))
95 -- | A 'Resource' to be run when a POST request comes for the
96 -- resource path. If 'resPost' is Nothing, the system responds
97 -- \"405 Method Not Allowed\" for POST requests.
98 , resPost ∷ !(Maybe (Resource ()))
99 -- | A 'Resource' to be run when a PUT request comes for the
100 -- resource path. If 'resPut' is Nothing, the system responds
101 -- \"405 Method Not Allowed\" for PUT requests.
102 , resPut ∷ !(Maybe (Resource ()))
103 -- | A 'Resource' to be run when a DELETE request comes for the
104 -- resource path. If 'resDelete' is Nothing, the system responds
105 -- \"405 Method Not Allowed\" for DELETE requests.
106 , resDelete ∷ !(Maybe (Resource ()))
109 -- |'emptyResource' is a resource definition with no actual
110 -- handlers. You can construct a 'ResourceDef' by selectively
111 -- overriding 'emptyResource'. It is defined as follows:
114 -- emptyResource = ResourceDef {
115 -- resUsesNativeThread = False
116 -- , resIsGreedy = False
117 -- , resGet = Nothing
118 -- , resHead = Nothing
119 -- , resPost = Nothing
120 -- , resPut = Nothing
121 -- , resDelete = Nothing
124 emptyResource ∷ ResourceDef
125 emptyResource = ResourceDef {
126 resUsesNativeThread = False
127 , resIsGreedy = False
132 , resDelete = Nothing
135 -- |'ResTree' is an opaque structure which is a map from resource path
137 newtype ResTree = ResTree ResNode -- root だから Map ではない
138 type ResSubtree = Map Text ResNode
139 data ResNode = ResNode (Maybe ResourceDef) ResSubtree
141 -- |'mkResTree' converts a list of @(path, def)@ to a 'ResTree' e.g.
144 -- mkResTree [ ([] , 'Network.HTTP.Lucu.StaticFile.staticFile' \"\/usr\/include\/stdio.h\" ) -- \/
145 -- , ([\"unistd\"], 'Network.HTTP.Lucu.StaticFile.staticFile' \"\/usr\/include\/unistd.h\") -- \/unistd
149 -- Note that the request path in an incoming HTTP request is always
150 -- treated as an URI-encoded UTF-8 string.
151 mkResTree ∷ [ ([Text], ResourceDef) ] → ResTree
152 mkResTree = processRoot ∘ map (first canonicalisePath)
154 canonicalisePath ∷ [Text] → [Text]
155 canonicalisePath = filter (≢ "")
157 processRoot ∷ [ ([Text], ResourceDef) ] → ResTree
159 = let (roots, nonRoots) = partition (\(path, _) → null path) list
160 children = processNonRoot nonRoots
163 -- The root has no resources. Maybe there's one at
164 -- somewhere like "/foo".
165 ResTree (ResNode Nothing children)
167 -- There is a root resource.
168 let (_, def) = last roots
170 ResTree (ResNode (Just def) children)
172 processNonRoot ∷ [ ([Text], ResourceDef) ] → ResSubtree
174 = let subtree = M.fromList [(name, node name)
176 childNames = [name | (name:_, _) ← list]
177 node name = let defs = [def | (path, def) ← list, path ≡ [name]]
180 -- No resources are defined
181 -- here. Maybe there's one at
182 -- somewhere below this node.
183 ResNode Nothing children
185 -- There is a resource here.
186 ResNode (Just $ last defs) children
187 children = processNonRoot [(path, def)
188 | (_:path, def) ← list]
192 findResource ∷ ResTree → [FallbackHandler] → URI → IO (Maybe ([Text], ResourceDef))
193 findResource (ResTree (ResNode rootDefM subtree)) fbs uri
194 = do let path = splitPathInfo uri
195 hasGreedyRoot = maybe False resIsGreedy rootDefM
196 foundInTree = if hasGreedyRoot ∨ null path then
200 walkTree subtree path []
201 if isJust foundInTree then
206 walkTree ∷ ResSubtree → [Text] → [Text] → Maybe ([Text], ResourceDef)
209 = error "Internal error: should not reach here."
211 walkTree tree (name:[]) soFar
212 = do ResNode defM _ ← M.lookup name tree
214 return (soFar ⧺ [name], def)
216 walkTree tree (x:xs) soFar
217 = do ResNode defM sub ← M.lookup x tree
219 Just (ResourceDef { resIsGreedy = True })
221 return (soFar ⧺ [x], def)
222 _ → walkTree sub xs (soFar ⧺ [x])
224 fallback ∷ [Text] → [FallbackHandler] → IO (Maybe ([Text], ResourceDef))
225 fallback _ [] = return Nothing
226 fallback path (x:xs) = do m ← x path
228 Just def → return $ Just ([], def)
229 Nothing → fallback path xs
232 runResource ∷ ResourceDef → Interaction → IO ThreadId
233 runResource (ResourceDef {..}) itr@(Interaction {..})
234 = fork $ run `catch` processException
236 fork ∷ IO () → IO ThreadId
237 fork | resUsesNativeThread = forkOS
241 run = flip runRes itr $
243 fromMaybe notAllowed $ rsrc req
246 rsrc ∷ Request → Maybe (Resource ())
248 = case reqMethod req of
250 HEAD → case resHead of
256 _ → error $ "Unknown request method: " ⧺ show (reqMethod req)
258 notAllowed ∷ Resource ()
260 = setStatus MethodNotAllowed
262 (setHeader "Allow" $ A.fromAsciiBuilder
264 $ map A.toAsciiBuilder allowedMethods)
266 allowedMethods ∷ [Ascii]
267 allowedMethods = nub $ concat [ methods resGet ["GET"]
268 , methods resHead ["GET", "HEAD"]
269 , methods resPost ["POST"]
270 , methods resPut ["PUT"]
271 , methods resDelete ["DELETE"]
274 methods ∷ Maybe a → [Ascii] → [Ascii]
279 toAbortion ∷ SomeException → Abortion
281 = case fromException e of
282 Just abortion → abortion
283 Nothing → Abortion InternalServerError (∅) (Just $ T.pack $ show e)
285 processException ∷ SomeException → IO ()
287 = do let abo = toAbortion exc
288 -- まだ DecidingHeader 以前の状態だったら、この途中終了
289 -- を應答に反映させる餘地がある。さうでなければ stderr
291 state ← atomically $ readTVar itrState
292 res ← atomically $ readTVar itrResponse
293 if state ≤ DecidingHeader then
295 do setStatus $ aboStatus abo
296 setHeader "Content-Type" defaultPageContentType
297 mapM_ (uncurry setHeader) $ fromHeaders $ aboHeaders abo
298 putChunk $ LT.encodeUtf8 $ abortPage itrConfig itrRequest res abo
300 when (cnfDumpTooLateAbortionToStderr itrConfig)
302 runRes (driftTo Done) itr
304 dumpAbortion ∷ Abortion → IO ()
307 $ concat [ "Lucu: an exception occured after "
308 , "sending response header to the client:\n"
309 , " ", show abo, "\n"