1 module Network.HTTP.Lucu.Resource
5 , mkResTree -- [ ([String], ResourceDef) ] -> ResTree
6 , findResource -- ResTree -> URI -> Maybe ResourceDef
7 , runResource -- ResourceDef -> Interaction -> IO ThreadId
9 , getMethod -- Resource Method
10 , getHeader -- String -> Resource (Maybe String)
12 , foundEntity -- Bool -> String -> ClockTime -> Resource ()
13 , foundETag -- Bool -> String -> Resource ()
14 , foundTimeStamp -- ClockTime -> Resource ()
15 , foundNoEntity -- Maybe String -> Resource ()
17 , input -- Int -> Resource String
18 , inputChunk -- Int -> Resource String
19 , inputBS -- Int -> Resource ByteString
20 , inputChunkBS -- Int -> Resource ByteString
23 , setStatus -- StatusCode -> Resource ()
24 , setHeader -- String -> String -> Resource ()
25 , redirect -- StatusCode -> URI -> Resource ()
26 , setETag -- Bool -> String -> Resource ()
27 , setLastModified -- ClockTime -> Resource ()
29 , output -- String -> Resource ()
30 , outputChunk -- String -> Resource ()
31 , outputBS -- ByteString -> Resource ()
32 , outputChunkBS -- ByteString -> Resource ()
36 import Control.Concurrent
37 import Control.Concurrent.STM
38 import Control.Exception
39 import Control.Monad.Reader
40 import qualified Data.ByteString.Lazy.Char8 as B
41 import Data.ByteString.Lazy.Char8 (ByteString)
44 import qualified Data.Map as M
47 import GHC.Conc (unsafeIOToSTM)
48 import Network.HTTP.Lucu.Abortion
49 import Network.HTTP.Lucu.Config
50 import Network.HTTP.Lucu.DefaultPage
51 import Network.HTTP.Lucu.ETag
52 import qualified Network.HTTP.Lucu.Headers as H
53 import Network.HTTP.Lucu.HttpVersion
54 import Network.HTTP.Lucu.Interaction
55 import Network.HTTP.Lucu.Parser
56 import Network.HTTP.Lucu.Postprocess
57 import Network.HTTP.Lucu.RFC1123DateTime
58 import Network.HTTP.Lucu.Request
59 import Network.HTTP.Lucu.Response
60 import Network.HTTP.Lucu.Utils
62 import Prelude hiding (catch)
64 import System.IO.Error hiding (catch)
68 type Resource a = ReaderT Interaction IO a
71 {- /aaa/bbb/ccc にアクセスされた時、もし /aaa/bbb に貪欲なリソースがあ
72 れば、假に /aaa/bbb/ccc に何らかのリソースがあったとしても必ず
73 /aaa/bbb が撰ばれる。/aaa/bbb のリソースが貪欲でなければ、それは無視
75 data ResourceDef = ResourceDef {
76 resUsesNativeThread :: Bool
78 , resGet :: Maybe (Resource ())
79 , resHead :: Maybe (Resource ())
80 , resPost :: Maybe (Resource ())
81 , resPut :: Maybe (Resource ())
82 , resDelete :: Maybe (Resource ())
84 type ResTree = ResNode -- root だから Map ではない
85 type ResSubtree = Map String ResNode
86 data ResNode = ResNode (Maybe ResourceDef) ResSubtree
89 mkResTree :: [ ([String], ResourceDef) ] -> ResTree
90 mkResTree list = processRoot list
92 processRoot :: [ ([String], ResourceDef) ] -> ResTree
94 = let (roots, nonRoots) = partition (\ (path, _) -> path == []) list
95 children = processNonRoot nonRoots
98 -- / にリソースが定義されない。/foo とかにはあるかも。
99 ResNode Nothing children
102 let (_, def) = last roots
104 ResNode (Just def) children
106 processNonRoot :: [ ([String], ResourceDef) ] -> ResSubtree
108 = let subtree = M.fromList [(name, node name)
109 | name <- childNames]
110 childNames = [name | (name:_, _) <- list]
111 node name = let defs = [def | (path, def) <- list, path == [name]]
116 ResNode Nothing children
119 ResNode (Just $ last defs) children
120 children = processNonRoot [(path, def)
121 | (_:path, def) <- list, not (null path)]
126 findResource :: ResTree -> URI -> Maybe ResourceDef
127 findResource (ResNode rootDefM subtree) uri
128 = let pathStr = uriPath uri
129 path = [x | x <- splitBy (== '/') pathStr, x /= ""]
134 walkTree subtree path
136 walkTree :: ResSubtree -> [String] -> Maybe ResourceDef
138 walkTree subtree (name:[])
139 = case M.lookup name subtree of
141 Just (ResNode defM _) -> defM
143 walkTree subtree (x:xs)
144 = case M.lookup x subtree of
146 Just (ResNode defM children) -> case defM of
147 Just (ResourceDef { resIsGreedy = True })
149 _ -> walkTree children xs
152 runResource :: ResourceDef -> Interaction -> IO ThreadId
155 $ catch ( runReaderT ( do fromMaybe notAllowed rsrc
159 $ \ exc -> processException (itrConfig itr) exc
161 fork :: IO () -> IO ThreadId
162 fork = if (resUsesNativeThread def)
166 rsrc :: Maybe (Resource ())
167 rsrc = case reqMethod $ fromJust $ itrRequest itr of
169 HEAD -> case resHead def of
171 Nothing -> resGet def
174 DELETE -> resDelete def
176 notAllowed :: Resource ()
177 notAllowed = do setStatus MethodNotAllowed
178 setHeader "Allow" $ joinWith ", " allowedMethods
180 allowedMethods :: [String]
181 allowedMethods = nub $ foldr (++) [] [ methods resGet ["GET"]
182 , methods resHead ["GET", "HEAD"]
183 , methods resPost ["POST"]
184 , methods resPut ["PUT"]
185 , methods resDelete ["DELETE"]
188 methods :: (ResourceDef -> Maybe a) -> [String] -> [String]
189 methods f xs = case f def of
193 processException :: Config -> Exception -> IO ()
194 processException conf exc
195 = do let abo = case exc of
196 ErrorCall msg -> Abortion InternalServerError [] msg
197 IOException ioE -> Abortion InternalServerError [] $ formatIOE ioE
198 DynException dynE -> case fromDynamic dynE of
199 Just (abo :: Abortion) -> abo
201 -> Abortion InternalServerError []
203 _ -> Abortion InternalServerError [] $ show exc
204 -- まだ DecidingHeader 以前の状態だったら、この途中終了
205 -- を應答に反映させる餘地がある。さうでなければ stderr
207 state <- atomically $ readItr itr itrState id
208 if state <= DecidingHeader then
210 $ do setStatus $ aboStatus abo
211 -- FIXME: 同じ名前で複數の値があった時は、こ
213 mapM_ (\ (name, value) -> setHeader name value) $ aboHeaders abo
214 setHeader "Content-Type" "application/xhtml+xml"
215 output $ aboPage conf abo
217 hPutStrLn stderr $ show abo
219 flip runReaderT itr $ driftTo Done
221 formatIOE :: IOError -> String
222 formatIOE ioE = if isUserError ioE then
223 ioeGetErrorString ioE
228 getMethod :: Resource Method
229 getMethod = do itr <- ask
230 return $ reqMethod $ fromJust $ itrRequest itr
233 getHeader :: String -> Resource (Maybe String)
234 getHeader name = do itr <- ask
235 return $ H.getHeader name $ fromJust $ itrRequest itr
238 {- ExaminingRequest 時に使用するアクション群 -}
240 foundEntity :: Bool -> String -> ClockTime -> Resource ()
241 foundEntity isWeak token timeStamp
242 = do driftTo ExaminingRequest
245 when (method == GET || method == HEAD)
246 $ setHeader' "Last-Modified" $ formatHTTPDateTime timeStamp
247 foundETag isWeak token
252 foundETag :: Bool -> String -> Resource ()
253 foundETag isWeak token
254 = do driftTo ExaminingRequest
256 let tag = mkETag isWeak token
259 when (method == GET || method == HEAD)
260 $ setHeader' "ETag" $ show tag
262 -- If-Match があればそれを見る。
263 ifMatch <- getHeader "If-Match"
266 Just "*" -> return ()
267 Just list -> case parseStr eTagListP list of
269 -- tags の中に一致するものが無ければ
270 -- PreconditionFailed で終了。
271 -> when (not $ any (== tag) tags)
272 $ abort PreconditionFailed [] ("The entity tag doesn't match: " ++ list)
273 _ -> abort BadRequest [] ("Unparsable If-Match: " ++ fromJust ifMatch)
275 let statusForNoneMatch = if method == GET || method == HEAD then
280 -- If-None-Match があればそれを見る。
281 ifNoneMatch <- getHeader "If-None-Match"
284 Just "*" -> abort statusForNoneMatch [] ("The entity tag matches: *")
285 Just list -> case parseStr eTagListP list of
287 -> when (any (== tag) tags)
288 $ abort statusForNoneMatch [] ("The entity tag matches: " ++ list)
289 _ -> abort BadRequest [] ("Unparsable If-None-Match: " ++ list)
294 foundTimeStamp :: ClockTime -> Resource ()
295 foundTimeStamp timeStamp
296 = do driftTo ExaminingRequest
299 when (method == GET || method == HEAD)
300 $ setHeader' "Last-Modified" $ formatHTTPDateTime timeStamp
302 let statusForIfModSince = if method == GET || method == HEAD then
307 -- If-Modified-Since があればそれを見る。
308 ifModSince <- getHeader "If-Modified-Since"
310 Just str -> case parseHTTPDateTime str of
312 -> when (timeStamp <= lastTime)
313 $ abort statusForIfModSince [] ("The entity has not been modified since " ++ str)
315 -> return () -- 不正な時刻は無視
318 -- If-Unmodified-Since があればそれを見る。
319 ifUnmodSince <- getHeader "If-Unmodified-Since"
321 Just str -> case parseHTTPDateTime str of
323 -> when (timeStamp > lastTime)
324 $ abort PreconditionFailed [] ("The entity has not been modified since " ++ str)
326 -> return () -- 不正な時刻は無視
332 foundNoEntity :: Maybe String -> Resource ()
334 = do driftTo ExaminingRequest
336 let msg = fromMaybe "The requested entity was not found in this server." msgM
340 $ abort NotFound [] msg
342 -- エンティティが存在しないと云ふ事は、"*" も含めたどのやうな
343 -- If-Match: 條件も滿たさない。
344 ifMatch <- getHeader "If-Match"
345 when (ifMatch /= Nothing)
346 $ abort PreconditionFailed [] msg
351 {- GettingBody 時に使用するアクション群 -}
353 input :: Int -> Resource String
354 input limit = inputBS limit >>= return . B.unpack
357 -- 多くとも limit バイトまでのリクエストボディ全體を受信する。limit が
358 -- 零以下なら Config で設定されたデフォルトのボディ長により制限される。
359 inputBS :: Int -> Resource ByteString
361 = do driftTo GettingBody
363 hasBody <- liftIO $ atomically $ readItr itr itrRequestHasBody id
364 chunk <- if hasBody then
367 do driftTo DecidingHeader
371 askForInput :: Interaction -> Resource ByteString
373 = do let defaultLimit = cnfMaxEntityLength $ itrConfig itr
374 actualLimit = if limit <= 0 then
378 when (actualLimit <= 0)
379 $ fail ("inputBS: limit must be positive: " ++ show actualLimit)
382 $ do chunkLen <- readItr itr itrReqChunkLength id
383 writeItr itr itrWillReceiveBody True
384 if fmap (> actualLimit) chunkLen == Just True then
388 writeItr itr itrReqBodyWanted $ Just actualLimit
389 -- 應答を待つ。トランザクションを分けなければ當然デッドロック。
390 chunk <- liftIO $ atomically
391 $ do chunk <- readItr itr itrReceivedBody id
392 chunkIsOver <- readItr itr itrReqChunkIsOver id
393 if B.length chunk < fromIntegral actualLimit then
403 $ tooLarge actualLimit
404 -- 成功。itr 内にチャンクを置いたままにす
406 writeItr itr itrReceivedBody B.empty
408 driftTo DecidingHeader
411 tooLarge :: Int -> STM ()
412 tooLarge lim = abortSTM RequestEntityTooLarge []
413 ("Request body must be smaller than "
414 ++ show lim ++ " bytes.")
417 inputChunk :: Int -> Resource String
418 inputChunk limit = inputChunkBS limit >>= return . B.unpack
421 -- 多くとも limit バイトまでのリクエストボディの一部を受信する。limit
422 -- が 0 以下なら Config で設定されたデフォルトのボディ長により制限され
423 -- る。これ以上ボディが殘ってゐなければ空文字列を返す。
424 inputChunkBS :: Int -> Resource ByteString
426 = do driftTo GettingBody
428 hasBody <- liftIO $ atomically $ readItr itr itrRequestHasBody id
429 chunk <- if hasBody then
432 do driftTo DecidingHeader
436 askForInput :: Interaction -> Resource ByteString
438 = do let defaultLimit = cnfMaxEntityLength $ itrConfig itr
439 actualLimit = if limit < 0 then
443 when (actualLimit <= 0)
444 $ fail ("inputChunkBS: limit must be positive: " ++ show actualLimit)
447 $ do writeItr itr itrReqBodyWanted $ Just actualLimit
448 writeItr itr itrWillReceiveBody True
449 -- 應答を待つ。トランザクションを分けなければ當然デッドロック。
450 chunk <- liftIO $ atomically
451 $ do chunk <- readItr itr itrReceivedBody id
452 -- 要求された量に滿たなくて、まだ殘りがあ
454 when (B.length chunk < fromIntegral actualLimit)
455 $ do chunkIsOver <- readItr itr itrReqChunkIsOver id
459 writeItr itr itrReceivedBody B.empty
462 $ driftTo DecidingHeader
471 {- DecidingHeader 時に使用するアクション群 -}
473 setStatus :: StatusCode -> Resource ()
475 = do driftTo DecidingHeader
477 liftIO $ atomically $ updateItr itr itrResponse
478 $ \ resM -> case resM of
479 Nothing -> Just $ Response {
480 resVersion = HttpVersion 1 1
484 Just res -> Just $ res {
489 setHeader :: String -> String -> Resource ()
491 = driftTo DecidingHeader >> setHeader' name value
494 setHeader' :: String -> String -> Resource()
495 setHeader' name value
497 liftIO $ atomically $ updateItr itr itrResponse
498 $ \ resM -> case resM of
499 Nothing -> Just $ Response {
500 resVersion = HttpVersion 1 1
502 , resHeaders = [ (name, value) ]
504 Just res -> Just $ H.setHeader name value res
507 redirect :: StatusCode -> URI -> Resource ()
509 = do when (code == NotModified || not (isRedirection code))
510 $ abort InternalServerError []
511 $ "Attempted to redirect with status " ++ show code
513 setHeader "Location" (uriToString id uri $ "")
516 setETag :: Bool -> String -> Resource ()
518 = setHeader "ETag" $ show $ mkETag isWeak token
521 setLastModified :: ClockTime -> Resource ()
522 setLastModified lastmod
523 = setHeader "Last-Modified" $ formatHTTPDateTime lastmod
526 {- DecidingBody 時に使用するアクション群 -}
528 output :: String -> Resource ()
529 output = outputBS . B.pack
532 outputBS :: ByteString -> Resource ()
533 outputBS str = do outputChunkBS str
537 outputChunk :: String -> Resource ()
538 outputChunk = outputChunkBS . B.pack
541 outputChunkBS :: ByteString -> Resource ()
542 outputChunkBS str = do driftTo DecidingBody
544 liftIO $ atomically $
545 do updateItr itr itrBodyToSend (flip B.append str)
547 $ writeItr itr itrBodyIsNull False
552 [GettingBody からそれ以降の状態に遷移する時]
554 body を讀み終へてゐなければ、殘りの body を讀み捨てる。
557 [DecidingHeader からそれ以降の状態に遷移する時]
564 bodyIsNull が False ならば何もしない。True だった場合は出力補完す
565 る。Content-Type も變はる。但し(デフォルトのまま)Status が 200 OK
566 だった場合は、補完の代はりに 204 No Content に變へる。
570 driftTo :: InteractionState -> Resource ()
573 liftIO $ atomically $ do oldState <- readItr itr itrState id
574 if newState < oldState then
575 throwStateError oldState newState
577 do let a = [oldState .. newState]
580 mapM_ (uncurry $ drift itr) c
581 writeItr itr itrState newState
583 throwStateError :: Monad m => InteractionState -> InteractionState -> m a
585 throwStateError Done DecidingBody
586 = fail "It makes no sense to output something after finishing to output."
588 throwStateError old new
589 = fail ("state error: " ++ show old ++ " ==> " ++ show new)
592 drift :: Interaction -> InteractionState -> InteractionState -> STM ()
594 drift itr GettingBody _
595 = writeItr itr itrReqBodyWasteAll True
597 drift itr DecidingHeader _
601 = do bodyIsNull <- readItr itr itrBodyIsNull id
603 $ do status <- readStatus itr
605 do updateItrF itr itrResponse
606 $ \ res -> res { resStatus = NoContent }
607 updateItrF itr itrResponse
608 $ H.deleteHeader "Content-Type"
609 updateItrF itr itrResponse
610 $ H.deleteHeader "ETag"
611 updateItrF itr itrResponse
612 $ H.deleteHeader "Last-Modified"
621 readStatus :: Interaction -> STM StatusCode
622 readStatus itr = readItr itr itrResponse (resStatus . fromJust)