1 -- |This is the Resource Monad; monadic actions to define the behavior
2 -- of each resources. The 'Resource' Monad is a kind of IO Monad thus
3 -- it implements MonadIO class. It is also a state machine.
5 -- Request Processing Flow:
7 -- 1. A client issues an HTTP request.
9 -- 2. If the URI of it matches to any resource, the corresponding
10 -- 'Resource' Monad starts running on a newly spawned thread.
12 -- 3. The 'Resource' Monad looks at the request header, find (or not
13 -- find) an entity, receive the request body (if any), decide the
14 -- response header, and decide the response body. This process
15 -- will be discussed later.
17 -- 4. The 'Resource' Monad and its thread stops running. The client
18 -- may or may not be sending us the next request at this point.
20 -- 'Resource' Monad takes the following states. The initial state is
21 -- /Examining Request/ and the final state is /Done/.
23 -- [/Examining Request/] In this state, a 'Resource' looks at the
24 -- request header and thinks about an entity for it. If there is a
25 -- suitable entity, the 'Resource' tells the system an entity tag
26 -- and its last modification time ('foundEntity'). If it found no
27 -- entity, it tells the system so ('foundNoEntity'). In case it is
28 -- impossible to decide the existence of entity, which is a typical
29 -- case for POST requests, 'Resource' does nothing in this state.
31 -- [/Getting Body/] A 'Resource' asks the system to receive a
32 -- request body from client. Before actually reading from the
33 -- socket, the system sends \"100 Continue\" to the client if need
34 -- be. When a 'Resource' transits to the next state without
35 -- receiving all or part of request body, the system still reads it
36 -- and just throws it away.
38 -- [/Deciding Header/] A 'Resource' makes a decision of status code
39 -- and response header. When it transits to the next state, the
40 -- system checks the validness of response header and then write
41 -- them to the socket.
43 -- [/Deciding Body/] In this state, a 'Resource' asks the system to
44 -- write some response body to the socket. When it transits to the
45 -- next state without writing any response body, the system
46 -- completes it depending on the status code.
48 -- [/Done/] Everything is over. A 'Resource' can do nothing for the
49 -- HTTP interaction anymore.
51 -- Note that the state transition is one-way: for instance, it is an
52 -- error to try to read a request body after writing some
53 -- response. This limitation is for efficiency. We don't want to read
54 -- the entire request before starting 'Resource', nor we don't want to
55 -- postpone writing the entire response till the end of 'Resource'
58 module Network.HTTP.Lucu.Resource
65 -- ** Getting request header
67 -- |These actions can be computed regardless of the current state,
68 -- and they don't change the state.
79 -- ** Finding an entity
81 -- |These actions can be computed only in the /Examining Request/
82 -- state. After the computation, the 'Resource' transits to
83 -- /Getting Body/ state.
89 -- ** Getting a request body
91 -- |Computation of these actions changes the state to /Getting
100 -- ** Setting response headers
102 -- |Computation of these actions changes the state to /Deciding
111 -- ** Writing a response body
113 -- |Computation of these actions changes the state to /Deciding
124 import Control.Concurrent.STM
125 import Control.Monad.Reader
126 import qualified Data.ByteString.Lazy.Char8 as B
127 import Data.ByteString.Lazy.Char8 (ByteString)
130 import GHC.Conc (unsafeIOToSTM)
131 import Network.HTTP.Lucu.Abortion
132 import Network.HTTP.Lucu.Config
133 import Network.HTTP.Lucu.DefaultPage
134 import Network.HTTP.Lucu.ETag
135 import qualified Network.HTTP.Lucu.Headers as H
136 import Network.HTTP.Lucu.HttpVersion
137 import Network.HTTP.Lucu.Interaction
138 import Network.HTTP.Lucu.Parser
139 import Network.HTTP.Lucu.Postprocess
140 import Network.HTTP.Lucu.RFC1123DateTime
141 import Network.HTTP.Lucu.Request
142 import Network.HTTP.Lucu.Response
143 import Network.HTTP.Lucu.MIMEType
144 import Network.HTTP.Lucu.Utils
148 -- |The 'Resource' monad. /Interaction/ is an internal state thus it
149 -- is not exposed to users. This monad implements 'MonadIO' so it can
150 -- do any IO actions.
151 type Resource a = ReaderT Interaction IO a
153 -- |Get the 'Network.HTTP.Lucu.Config.Config' value which is used for
155 getConfig :: Resource Config
156 getConfig = do itr <- ask
157 return $ itrConfig itr
159 -- |Get the 'Network.HTTP.Lucu.Request.Request' value which represents
160 -- the request header. In general you don't have to use this action.
161 getRequest :: Resource Request
162 getRequest = do itr <- ask
163 return $ fromJust $ itrRequest itr
165 -- |Get the 'Network.HTTP.Lucu.Request.Method' value of the request.
166 getMethod :: Resource Method
167 getMethod = do req <- getRequest
168 return $ reqMethod req
170 -- |Get the URI of the request.
171 getRequestURI :: Resource URI
172 getRequestURI = do req <- getRequest
175 -- |Get the path of this 'Resource' (to be exact,
176 -- 'Network.HTTP.Lucu.Resource.Tree.ResourceDef') in the
177 -- 'Network.HTTP.Lucu.Resource.Tree.ResTree'. The result of this
178 -- action is the exact path in the tree even if the
179 -- 'Network.HTTP.Lucu.Resource.Tree.ResourceDef' is greedy.
183 -- > main = let tree = mkResTree [ (["foo"], resFoo) ]
184 -- > in runHttpd defaultConfig tree
186 -- > resFoo = ResourceDef {
187 -- > resIsGreedy = True
188 -- > , resGet = Just $ do requestURI <- getRequestURI
189 -- > resourcePath <- getResourcePath
190 -- > pathInfo <- getPathInfo
191 -- > -- uriPath requestURI == "/foo/bar/baz"
192 -- > -- resourcePath == ["foo"]
193 -- > -- pathInfo == ["bar", "baz"]
197 getResourcePath :: Resource [String]
198 getResourcePath = do itr <- ask
199 return $ fromJust $ itrResourcePath itr
202 -- |This is an analogy of CGI PATH_INFO. Its result is always @[]@ if
203 -- the 'Network.HTTP.Lucu.Resource.Tree.ResourceDef' is not
204 -- greedy. See 'getResourcePath'.
205 getPathInfo :: Resource [String]
206 getPathInfo = do rsrcPath <- getResourcePath
207 reqURI <- getRequestURI
208 let reqPathStr = uriPath reqURI
209 reqPath = [x | x <- splitBy (== '/') reqPathStr, x /= ""]
210 -- rsrcPath と reqPath の共通する先頭部分を reqPath か
211 -- ら全部取り除くと、それは PATH_INFO のやうなものにな
212 -- る。rsrcPath は全部一致してゐるに決まってゐる(でな
213 -- ければこの Resource が撰ばれた筈が無い)ので、
214 -- rsrcPath の長さの分だけ削除すれば良い。
215 return $ drop (length rsrcPath) reqPath
217 -- |Get a value of given request header. Comparison of header name is
218 -- case-insensitive. Note that this action is not intended to be used
219 -- so frequently: there should be an action like 'getContentType' for
220 -- every common headers.
221 getHeader :: String -> Resource (Maybe String)
222 getHeader name = do itr <- ask
223 return $ H.getHeader name $ fromJust $ itrRequest itr
225 -- |Get a list of 'Network.HTTP.Lucu.MIMEType.MIMEType' enumerated on
226 -- header \"Accept\".
227 getAccept :: Resource [MIMEType]
228 getAccept = do accept <- getHeader "Accept"
229 if accept == Nothing then
232 case parseStr mimeTypeListP $ fromJust accept of
233 (Success xs, _) -> return xs
236 -- |Get the header \"Content-Type\" as
237 -- 'Network.HTTP.Lucu.MIMEType.MIMEType'.
238 getContentType :: Resource (Maybe MIMEType)
239 getContentType = do cType <- getHeader "Content-Type"
240 if cType == Nothing then
243 case parseStr mimeTypeP $ fromJust cType of
244 (Success t, _) -> return $ Just t
249 {- ExaminingRequest 時に使用するアクション群 -}
251 foundEntity :: ETag -> ClockTime -> Resource ()
252 foundEntity tag timeStamp
253 = do driftTo ExaminingRequest
256 when (method == GET || method == HEAD)
257 $ setHeader' "Last-Modified" $ formatHTTPDateTime timeStamp
263 foundETag :: ETag -> Resource ()
265 = do driftTo ExaminingRequest
268 when (method == GET || method == HEAD)
269 $ setHeader' "ETag" $ show tag
271 -- If-Match があればそれを見る。
272 ifMatch <- getHeader "If-Match"
275 Just "*" -> return ()
276 Just list -> case parseStr eTagListP list of
278 -- tags の中に一致するものが無ければ
279 -- PreconditionFailed で終了。
280 -> when (not $ any (== tag) tags)
281 $ abort PreconditionFailed []
282 $ Just ("The entity tag doesn't match: " ++ list)
283 _ -> abort BadRequest [] $ Just ("Unparsable If-Match: " ++ fromJust ifMatch)
285 let statusForNoneMatch = if method == GET || method == HEAD then
290 -- If-None-Match があればそれを見る。
291 ifNoneMatch <- getHeader "If-None-Match"
294 Just "*" -> abort statusForNoneMatch [] $ Just ("The entity tag matches: *")
295 Just list -> case parseStr eTagListP list of
297 -> when (any (== tag) tags)
298 $ abort statusForNoneMatch [] $ Just ("The entity tag matches: " ++ list)
299 _ -> abort BadRequest [] $ Just ("Unparsable If-None-Match: " ++ list)
304 foundTimeStamp :: ClockTime -> Resource ()
305 foundTimeStamp timeStamp
306 = do driftTo ExaminingRequest
309 when (method == GET || method == HEAD)
310 $ setHeader' "Last-Modified" $ formatHTTPDateTime timeStamp
312 let statusForIfModSince = if method == GET || method == HEAD then
317 -- If-Modified-Since があればそれを見る。
318 ifModSince <- getHeader "If-Modified-Since"
320 Just str -> case parseHTTPDateTime str of
322 -> when (timeStamp <= lastTime)
323 $ abort statusForIfModSince []
324 $ Just ("The entity has not been modified since " ++ str)
326 -> return () -- 不正な時刻は無視
329 -- If-Unmodified-Since があればそれを見る。
330 ifUnmodSince <- getHeader "If-Unmodified-Since"
332 Just str -> case parseHTTPDateTime str of
334 -> when (timeStamp > lastTime)
335 $ abort PreconditionFailed []
336 $ Just ("The entity has not been modified since " ++ str)
338 -> return () -- 不正な時刻は無視
344 foundNoEntity :: Maybe String -> Resource ()
346 = do driftTo ExaminingRequest
350 $ abort NotFound [] msgM
352 -- エンティティが存在しないと云ふ事は、"*" も含めたどのやうな
353 -- If-Match: 條件も滿たさない。
354 ifMatch <- getHeader "If-Match"
355 when (ifMatch /= Nothing)
356 $ abort PreconditionFailed [] msgM
361 {- GettingBody 時に使用するアクション群 -}
363 input :: Int -> Resource String
364 input limit = inputBS limit >>= return . B.unpack
367 -- 多くとも limit バイトまでのリクエストボディ全體を受信する。limit が
368 -- 零以下なら Config で設定されたデフォルトのボディ長により制限される。
369 inputBS :: Int -> Resource ByteString
371 = do driftTo GettingBody
373 hasBody <- liftIO $ atomically $ readItr itr itrRequestHasBody id
374 chunk <- if hasBody then
377 do driftTo DecidingHeader
381 askForInput :: Interaction -> Resource ByteString
383 = do let defaultLimit = cnfMaxEntityLength $ itrConfig itr
384 actualLimit = if limit <= 0 then
388 when (actualLimit <= 0)
389 $ fail ("inputBS: limit must be positive: " ++ show actualLimit)
392 $ do chunkLen <- readItr itr itrReqChunkLength id
393 writeItr itr itrWillReceiveBody True
394 if fmap (> actualLimit) chunkLen == Just True then
398 writeItr itr itrReqBodyWanted $ Just actualLimit
399 -- 應答を待つ。トランザクションを分けなければ當然デッドロック。
400 chunk <- liftIO $ atomically
401 $ do chunk <- readItr itr itrReceivedBody id
402 chunkIsOver <- readItr itr itrReqChunkIsOver id
403 if B.length chunk < fromIntegral actualLimit then
413 $ tooLarge actualLimit
414 -- 成功。itr 内にチャンクを置いたままにす
416 writeItr itr itrReceivedBody B.empty
418 driftTo DecidingHeader
421 tooLarge :: Int -> STM ()
422 tooLarge lim = abortSTM RequestEntityTooLarge []
423 $ Just ("Request body must be smaller than "
424 ++ show lim ++ " bytes.")
427 inputChunk :: Int -> Resource String
428 inputChunk limit = inputChunkBS limit >>= return . B.unpack
431 -- 多くとも limit バイトまでのリクエストボディの一部を受信する。limit
432 -- が 0 以下なら Config で設定されたデフォルトのボディ長により制限され
433 -- る。これ以上ボディが殘ってゐなければ空文字列を返す。
434 inputChunkBS :: Int -> Resource ByteString
436 = do driftTo GettingBody
438 hasBody <- liftIO $ atomically $ readItr itr itrRequestHasBody id
439 chunk <- if hasBody then
442 do driftTo DecidingHeader
446 askForInput :: Interaction -> Resource ByteString
448 = do let defaultLimit = cnfMaxEntityLength $ itrConfig itr
449 actualLimit = if limit < 0 then
453 when (actualLimit <= 0)
454 $ fail ("inputChunkBS: limit must be positive: " ++ show actualLimit)
457 $ do writeItr itr itrReqBodyWanted $ Just actualLimit
458 writeItr itr itrWillReceiveBody True
459 -- 應答を待つ。トランザクションを分けなければ當然デッドロック。
460 chunk <- liftIO $ atomically
461 $ do chunk <- readItr itr itrReceivedBody id
462 -- 要求された量に滿たなくて、まだ殘りがあ
464 when (B.length chunk < fromIntegral actualLimit)
465 $ do chunkIsOver <- readItr itr itrReqChunkIsOver id
469 writeItr itr itrReceivedBody B.empty
472 $ driftTo DecidingHeader
476 -- application/x-www-form-urlencoded または multipart/form-data をパー
477 -- スする。もし Content-Type が無かったら BadRequest で終了し、未對應の
478 -- タイプであったら UnsupportedMediaType で終了する。
479 inputForm :: Int -> Resource [(String, String)]
481 = do cTypeM <- getContentType
484 -> abort BadRequest [] (Just "Missing Content-Type")
485 Just (MIMEType "application" "x-www-form-urlencoded" _)
486 -> readWWWFormURLEncoded
487 Just (MIMEType "multipart" "form-data" _)
488 -> readMultipartFormData
490 -> abort UnsupportedMediaType [] (Just $ "Unsupported media type: "
493 readWWWFormURLEncoded
494 = do src <- input limit
495 return $ do pairStr <- splitBy (\ c -> c == ';' || c == '&') src
496 let pair = break (== '=') pairStr
497 return ( unEscapeString $ fst pair
498 , unEscapeString $ snd pair
500 readMultipartFormData -- FIXME: 未對應
501 = abort UnsupportedMediaType []
502 (Just $ "Sorry, inputForm does not currently support multipart/form-data.")
510 {- DecidingHeader 時に使用するアクション群 -}
512 setStatus :: StatusCode -> Resource ()
514 = do driftTo DecidingHeader
516 liftIO $ atomically $ updateItr itr itrResponse
522 setHeader :: String -> String -> Resource ()
524 = driftTo DecidingHeader >> setHeader' name value
527 setHeader' :: String -> String -> Resource()
528 setHeader' name value
531 $ updateItr itr itrResponse
532 $ H.setHeader name value
535 redirect :: StatusCode -> URI -> Resource ()
537 = do when (code == NotModified || not (isRedirection code))
538 $ abort InternalServerError []
539 $ Just ("Attempted to redirect with status " ++ show code)
541 setHeader "Location" (uriToString id uri $ "")
544 setETag :: ETag -> Resource ()
546 = setHeader "ETag" $ show tag
549 setLastModified :: ClockTime -> Resource ()
550 setLastModified lastmod
551 = setHeader "Last-Modified" $ formatHTTPDateTime lastmod
554 setContentType :: MIMEType -> Resource ()
556 = setHeader "Content-Type" $ show mType
559 {- DecidingBody 時に使用するアクション群 -}
561 output :: String -> Resource ()
562 output = outputBS . B.pack
565 outputBS :: ByteString -> Resource ()
566 outputBS str = do outputChunkBS str
570 outputChunk :: String -> Resource ()
571 outputChunk = outputChunkBS . B.pack
574 {- チャンクの大きさは Config で制限されてゐる。もし例へば /dev/zero を
575 B.readFile して作った ByteString をそのまま ResponseWriter に渡した
576 りすると大變な事が起こる。何故なら ResponseWriter は
577 Transfer-Encoding: chunked の時、ヘッダを書く爲にチャンクの大きさを
578 測るから、その時に起こるであらう事は言ふまでも無い。 -}
580 outputChunkBS :: ByteString -> Resource ()
582 = do driftTo DecidingBody
585 let limit = cnfMaxOutputChunkLength $ itrConfig itr
587 $ fail ("cnfMaxOutputChunkLength must be positive: "
590 discardBody <- liftIO $ atomically $
591 readItr itr itrWillDiscardBody id
594 $ sendChunks str limit
597 $ liftIO $ atomically $
598 writeItr itr itrBodyIsNull False
600 sendChunks :: ByteString -> Int -> Resource ()
602 | B.null str = return ()
603 | otherwise = do let (chunk, remaining) = B.splitAt (fromIntegral limit) str
605 liftIO $ atomically $
606 do buf <- readItr itr itrBodyToSend id
609 writeItr itr itrBodyToSend chunk
614 sendChunks remaining limit
618 [GettingBody からそれ以降の状態に遷移する時]
620 body を讀み終へてゐなければ、殘りの body を讀み捨てる。
623 [DecidingHeader からそれ以降の状態に遷移する時]
630 bodyIsNull が False ならば何もしない。True だった場合は出力補完す
635 driftTo :: InteractionState -> Resource ()
638 liftIO $ atomically $ do oldState <- readItr itr itrState id
639 if newState < oldState then
640 throwStateError oldState newState
642 do let a = [oldState .. newState]
645 mapM_ (uncurry $ drift itr) c
646 writeItr itr itrState newState
648 throwStateError :: Monad m => InteractionState -> InteractionState -> m a
650 throwStateError Done DecidingBody
651 = fail "It makes no sense to output something after finishing to output."
653 throwStateError old new
654 = fail ("state error: " ++ show old ++ " ==> " ++ show new)
657 drift :: Interaction -> InteractionState -> InteractionState -> STM ()
659 drift itr GettingBody _
660 = writeItr itr itrReqBodyWasteAll True
662 drift itr DecidingHeader _
666 = do bodyIsNull <- readItr itr itrBodyIsNull id
668 $ writeDefaultPage itr