1 module Network.HTTP.Lucu.Resource
5 , mkResTree -- [ ([String], ResourceDef) ] -> ResTree
6 , findResource -- ResTree -> URI -> Maybe ResourceDef
7 , runResource -- ResourceDef -> Interaction -> IO ThreadId
9 , input -- Int -> Resource String
10 , inputChunk -- Int -> Resource String
11 , inputBS -- Int -> Resource ByteString
12 , inputChunkBS -- Int -> Resource ByteString
14 , setStatus -- StatusCode -> Resource ()
15 , setHeader -- String -> String -> Resource ()
17 , redirect -- StatusCode -> URI -> Resource ()
19 , output -- String -> Resource ()
20 , outputChunk -- String -> Resource ()
21 , outputBS -- ByteString -> Resource ()
22 , outputChunkBS -- ByteString -> Resource ()
26 import Control.Concurrent
27 import Control.Concurrent.STM
28 import Control.Exception
29 import Control.Monad.Reader
30 import qualified Data.ByteString.Lazy.Char8 as B
31 import Data.ByteString.Lazy.Char8 (ByteString)
34 import qualified Data.Map as M
37 import GHC.Conc (unsafeIOToSTM)
38 import Network.HTTP.Lucu.Abortion
39 import Network.HTTP.Lucu.Config
40 import Network.HTTP.Lucu.DefaultPage
41 import qualified Network.HTTP.Lucu.Headers as H
42 import Network.HTTP.Lucu.HttpVersion
43 import Network.HTTP.Lucu.Interaction
44 import Network.HTTP.Lucu.Postprocess
45 import Network.HTTP.Lucu.Request
46 import Network.HTTP.Lucu.Response
47 import Network.HTTP.Lucu.Utils
49 import Prelude hiding (catch)
51 import System.IO.Error hiding (catch)
54 type Resource a = ReaderT Interaction IO a
57 {- /aaa/bbb/ccc にアクセスされた時、もし /aaa/bbb に貪欲なリソースがあ
58 れば、假に /aaa/bbb/ccc に何らかのリソースがあったとしても必ず
59 /aaa/bbb が撰ばれる。/aaa/bbb のリソースが貪欲でなければ、それは無視
61 data ResourceDef = ResourceDef {
62 resUsesNativeThread :: Bool
64 , resGet :: Maybe (Resource ())
65 , resHead :: Maybe (Resource ())
66 , resPost :: Maybe (Resource ())
67 , resPut :: Maybe (Resource ())
68 , resDelete :: Maybe (Resource ())
70 type ResTree = ResNode -- root だから Map ではない
71 type ResSubtree = Map String ResNode
72 data ResNode = ResNode (Maybe ResourceDef) ResSubtree
75 mkResTree :: [ ([String], ResourceDef) ] -> ResTree
76 mkResTree list = processRoot list
78 processRoot :: [ ([String], ResourceDef) ] -> ResTree
80 = let (roots, nonRoots) = partition (\ (path, _) -> path == []) list
81 children = processNonRoot nonRoots
84 -- / にリソースが定義されない。/foo とかにはあるかも。
85 ResNode Nothing children
88 let (_, def) = last roots
90 ResNode (Just def) children
92 processNonRoot :: [ ([String], ResourceDef) ] -> ResSubtree
94 = let subtree = M.fromList [(name, node name)
96 childNames = [name | (name:_, _) <- list]
97 node name = let defs = [def | (path, def) <- list, path == [name]]
102 ResNode Nothing children
105 ResNode (Just $ last defs) children
106 children = processNonRoot [(path, def)
107 | (_:path, def) <- list, not (null path)]
112 findResource :: ResTree -> URI -> Maybe ResourceDef
113 findResource (ResNode rootDefM subtree) uri
114 = let pathStr = uriPath uri
115 path = [x | x <- splitBy (== '/') pathStr, x /= ""]
120 walkTree subtree path
122 walkTree :: ResSubtree -> [String] -> Maybe ResourceDef
124 walkTree subtree (name:[])
125 = case M.lookup name subtree of
127 Just (ResNode defM _) -> defM
129 walkTree subtree (x:xs)
130 = case M.lookup x subtree of
132 Just (ResNode defM children) -> case defM of
133 Just (ResourceDef { resIsGreedy = True })
135 _ -> walkTree children xs
138 runResource :: ResourceDef -> Interaction -> IO ThreadId
141 $ catch ( runReaderT ( do fromMaybe notAllowed rsrc
145 $ \ exc -> processException (itrConfig itr) exc
147 fork :: IO () -> IO ThreadId
148 fork = if (resUsesNativeThread def)
152 rsrc :: Maybe (Resource ())
153 rsrc = case reqMethod $ fromJust $ itrRequest itr of
155 HEAD -> case resHead def of
157 Nothing -> resGet def
160 DELETE -> resDelete def
162 notAllowed :: Resource ()
163 notAllowed = do setStatus MethodNotAllowed
164 setHeader "Allow" $ joinWith ", " allowedMethods
166 allowedMethods :: [String]
167 allowedMethods = nub $ foldr (++) [] [ methods resGet ["GET"]
168 , methods resHead ["GET", "HEAD"]
169 , methods resPost ["POST"]
170 , methods resPut ["PUT"]
171 , methods resDelete ["DELETE"]
174 methods :: (ResourceDef -> Maybe a) -> [String] -> [String]
175 methods f xs = case f def of
179 processException :: Config -> Exception -> IO ()
180 processException conf exc
181 = do let abo = case exc of
182 ErrorCall msg -> Abortion InternalServerError [] msg
183 IOException ioE -> Abortion InternalServerError [] $ formatIOE ioE
184 DynException dynE -> case fromDynamic dynE of
185 Just (abo :: Abortion) -> abo
187 -> Abortion InternalServerError []
189 _ -> Abortion InternalServerError [] $ show exc
190 -- まだ DecidingHeader 以前の状態だったら、この途中終了
191 -- を應答に反映させる餘地がある。さうでなければ stderr
193 state <- atomically $ readItr itr itrState id
194 if state <= DecidingHeader then
196 $ do setStatus $ aboStatus abo
197 -- FIXME: 同じ名前で複數の値があった時は、こ
199 mapM_ (\ (name, value) -> setHeader name value) $ aboHeaders abo
200 setHeader "Content-Type" "application/xhtml+xml"
201 output $ aboPage conf abo
203 hPutStrLn stderr $ show abo
205 flip runReaderT itr $ driftTo Done
207 formatIOE :: IOError -> String
208 formatIOE ioE = if isUserError ioE then
209 ioeGetErrorString ioE
216 input :: Int -> Resource String
217 input limit = inputBS limit >>= return . B.unpack
220 -- 多くとも limit バイトまでのリクエストボディ全體を受信する。limit が
221 -- 零以下なら Config で設定されたデフォルトのボディ長により制限される。
222 inputBS :: Int -> Resource ByteString
224 = do driftTo GettingBody
226 let defaultLimit = cnfMaxEntityLength $ itrConfig itr
227 actualLimit = if limit <= 0 then
231 when (actualLimit <= 0)
232 $ fail ("inputBS: limit must be positive: " ++ show actualLimit)
235 $ do chunkLen <- readItr itr itrReqChunkLength id
236 writeItr itr itrWillReceiveBody True
237 if fmap (> actualLimit) chunkLen == Just True then
241 writeItr itr itrReqBodyWanted $ Just actualLimit
242 -- 應答を待つ。トランザクションを分けなければ當然デッドロック。
243 chunk <- liftIO $ atomically
244 $ do chunk <- readItr itr itrReceivedBody id
245 chunkIsOver <- readItr itr itrReqChunkIsOver id
246 if B.length chunk < fromIntegral actualLimit then
247 -- 要求された量に滿たなくて、まだ殘りがある
252 -- 制限値一杯まで讀むやうに指示したのにまだ殘っ
255 $ tooLarge actualLimit
256 -- 成功。itr 内にチャンクを置いたままにするとメ
258 writeItr itr itrReceivedBody B.empty
260 driftTo DecidingHeader
263 tooLarge :: Int -> STM ()
264 tooLarge lim = abortSTM RequestEntityTooLarge []
265 ("Request body must be smaller than "
266 ++ show lim ++ " bytes.")
269 inputChunk :: Int -> Resource String
270 inputChunk limit = inputChunkBS limit >>= return . B.unpack
273 -- 多くとも limit バイトまでのリクエストボディの一部を受信する。limit
274 -- が 0 以下なら Config で設定されたデフォルトのボディ長により制限され
275 -- る。これ以上ボディが殘ってゐなければ空文字列を返す。
276 inputChunkBS :: Int -> Resource ByteString
278 = do driftTo GettingBody
280 let defaultLimit = cnfMaxEntityLength $ itrConfig itr
281 actualLimit = if limit < 0 then
285 when (actualLimit <= 0)
286 $ fail ("inputChunkBS: limit must be positive: " ++ show actualLimit)
289 $ do writeItr itr itrReqBodyWanted $ Just actualLimit
290 writeItr itr itrWillReceiveBody True
291 -- 應答を待つ。トランザクションを分けなければ當然デッドロック。
292 chunk <- liftIO $ atomically
293 $ do chunk <- readItr itr itrReceivedBody id
294 -- 要求された量に滿たなくて、まだ殘りがあるなら
296 when (B.length chunk < fromIntegral actualLimit)
297 $ do chunkIsOver <- readItr itr itrReqChunkIsOver id
301 writeItr itr itrReceivedBody B.empty
304 $ driftTo DecidingHeader
308 setStatus :: StatusCode -> Resource ()
310 = do driftTo DecidingHeader
312 liftIO $ atomically $ updateItr itr itrResponse
313 $ \ resM -> case resM of
314 Nothing -> Just $ Response {
315 resVersion = HttpVersion 1 1
319 Just res -> Just $ res {
324 setHeader :: String -> String -> Resource ()
326 = do driftTo DecidingHeader
328 liftIO $ atomically $ updateItr itr itrResponse
329 $ \ resM -> case resM of
330 Nothing -> Just $ Response {
331 resVersion = HttpVersion 1 1
333 , resHeaders = [ (name, value) ]
335 Just res -> Just $ H.setHeader name value res
338 redirect :: StatusCode -> URI -> Resource ()
340 = do when (code == NotModified || not (isRedirection code))
341 $ abort InternalServerError []
342 $ "Attempted to redirect with status " ++ show code
344 setHeader "Location" (uriToString id uri $ "")
347 output :: String -> Resource ()
348 output = outputBS . B.pack
351 outputBS :: ByteString -> Resource ()
352 outputBS str = do outputChunkBS str
356 outputChunk :: String -> Resource ()
357 outputChunk = outputChunkBS . B.pack
360 outputChunkBS :: ByteString -> Resource ()
361 outputChunkBS str = do driftTo DecidingBody
363 liftIO $ atomically $
364 do updateItr itr itrBodyToSend (flip B.append str)
366 $ writeItr itr itrBodyIsNull False
371 [GettingBody からそれ以降の状態に遷移する時]
373 body を讀み終へてゐなければ、殘りの body を讀み捨てる。
376 [DecidingHeader からそれ以降の状態に遷移する時]
383 bodyIsNull が False ならば何もしない。True だった場合は出力補完す
384 る。Content-Type も變はる。但し(デフォルトのまま)Status が 200 OK
385 だった場合は、補完の代はりに 204 No Content に變へる。
389 driftTo :: InteractionState -> Resource ()
392 liftIO $ atomically $ do oldState <- readItr itr itrState id
393 if newState < oldState then
394 throwStateError oldState newState
396 do let a = [oldState .. newState]
399 mapM_ (uncurry $ drift itr) c
400 writeItr itr itrState newState
402 throwStateError :: Monad m => InteractionState -> InteractionState -> m a
404 throwStateError Done DecidingBody
405 = fail "It makes no sense to output something after finishing to output."
407 throwStateError old new
408 = fail ("state error: " ++ show old ++ " ==> " ++ show new)
411 drift :: Interaction -> InteractionState -> InteractionState -> STM ()
413 drift itr GettingBody _
414 = writeItr itr itrReqBodyWasteAll True
416 drift itr DecidingHeader _
420 = do bodyIsNull <- readItr itr itrBodyIsNull id
422 $ do status <- readStatus itr
424 do updateItrF itr itrResponse
425 $ \ res -> res { resStatus = NoContent }
426 updateItrF itr itrResponse
427 $ H.deleteHeader "Content-Type"
428 updateItrF itr itrResponse
429 $ H.deleteHeader "ETag"
430 updateItrF itr itrResponse
431 $ H.deleteHeader "Last-Modified"
440 readStatus :: Interaction -> STM StatusCode
441 readStatus itr = readItr itr itrResponse (resStatus . fromJust)