1 module Network.HTTP.Lucu.Resource
4 , getMethod -- Resource Method
5 , getHeader -- String -> Resource (Maybe String)
6 , getAccept -- Resource [MIMEType]
7 , getContentType -- Resource (Maybe MIMEType)
9 , foundEntity -- ETag -> ClockTime -> Resource ()
10 , foundETag -- ETag -> Resource ()
11 , foundTimeStamp -- ClockTime -> Resource ()
12 , foundNoEntity -- Maybe String -> Resource ()
14 , input -- Int -> Resource String
15 , inputChunk -- Int -> Resource String
16 , inputBS -- Int -> Resource ByteString
17 , inputChunkBS -- Int -> Resource ByteString
20 , setStatus -- StatusCode -> Resource ()
21 , setHeader -- String -> String -> Resource ()
22 , redirect -- StatusCode -> URI -> Resource ()
23 , setETag -- ETag -> Resource ()
24 , setLastModified -- ClockTime -> Resource ()
25 , setContentType -- MIMEType -> Resource ()
27 , output -- String -> Resource ()
28 , outputChunk -- String -> Resource ()
29 , outputBS -- ByteString -> Resource ()
30 , outputChunkBS -- ByteString -> Resource ()
32 , driftTo -- InteractionState -> Resource ()
36 import Control.Concurrent.STM
37 import Control.Monad.Reader
38 import qualified Data.ByteString.Lazy.Char8 as B
39 import Data.ByteString.Lazy.Char8 (ByteString)
42 import GHC.Conc (unsafeIOToSTM)
43 import Network.HTTP.Lucu.Abortion
44 import Network.HTTP.Lucu.Config
45 import Network.HTTP.Lucu.DefaultPage
46 import Network.HTTP.Lucu.ETag
47 import qualified Network.HTTP.Lucu.Headers as H
48 import Network.HTTP.Lucu.HttpVersion
49 import Network.HTTP.Lucu.Interaction
50 import Network.HTTP.Lucu.Parser
51 import Network.HTTP.Lucu.Postprocess
52 import Network.HTTP.Lucu.RFC1123DateTime
53 import Network.HTTP.Lucu.Request
54 import Network.HTTP.Lucu.Response
55 import Network.HTTP.Lucu.MIMEType
56 import Network.HTTP.Lucu.Utils
61 type Resource a = ReaderT Interaction IO a
64 getMethod :: Resource Method
65 getMethod = do itr <- ask
66 return $ reqMethod $ fromJust $ itrRequest itr
69 getHeader :: String -> Resource (Maybe String)
70 getHeader name = do itr <- ask
71 return $ H.getHeader name $ fromJust $ itrRequest itr
74 getAccept :: Resource [MIMEType]
75 getAccept = do accept <- getHeader "Accept"
76 if accept == Nothing then
79 case parseStr mimeTypeListP $ fromJust accept of
80 (Success xs, _) -> return xs
84 getContentType :: Resource (Maybe MIMEType)
85 getContentType = do cType <- getHeader "Content-Type"
86 if cType == Nothing then
89 case parseStr mimeTypeP $ fromJust cType of
90 (Success t, _) -> return $ Just t
95 {- ExaminingRequest 時に使用するアクション群 -}
97 foundEntity :: ETag -> ClockTime -> Resource ()
98 foundEntity tag timeStamp
99 = do driftTo ExaminingRequest
102 when (method == GET || method == HEAD)
103 $ setHeader' "Last-Modified" $ formatHTTPDateTime timeStamp
109 foundETag :: ETag -> Resource ()
111 = do driftTo ExaminingRequest
114 when (method == GET || method == HEAD)
115 $ setHeader' "ETag" $ show tag
117 -- If-Match があればそれを見る。
118 ifMatch <- getHeader "If-Match"
121 Just "*" -> return ()
122 Just list -> case parseStr eTagListP list of
124 -- tags の中に一致するものが無ければ
125 -- PreconditionFailed で終了。
126 -> when (not $ any (== tag) tags)
127 $ abort PreconditionFailed [] ("The entity tag doesn't match: " ++ list)
128 _ -> abort BadRequest [] ("Unparsable If-Match: " ++ fromJust ifMatch)
130 let statusForNoneMatch = if method == GET || method == HEAD then
135 -- If-None-Match があればそれを見る。
136 ifNoneMatch <- getHeader "If-None-Match"
139 Just "*" -> abort statusForNoneMatch [] ("The entity tag matches: *")
140 Just list -> case parseStr eTagListP list of
142 -> when (any (== tag) tags)
143 $ abort statusForNoneMatch [] ("The entity tag matches: " ++ list)
144 _ -> abort BadRequest [] ("Unparsable If-None-Match: " ++ list)
149 foundTimeStamp :: ClockTime -> Resource ()
150 foundTimeStamp timeStamp
151 = do driftTo ExaminingRequest
154 when (method == GET || method == HEAD)
155 $ setHeader' "Last-Modified" $ formatHTTPDateTime timeStamp
157 let statusForIfModSince = if method == GET || method == HEAD then
162 -- If-Modified-Since があればそれを見る。
163 ifModSince <- getHeader "If-Modified-Since"
165 Just str -> case parseHTTPDateTime str of
167 -> when (timeStamp <= lastTime)
168 $ abort statusForIfModSince [] ("The entity has not been modified since " ++ str)
170 -> return () -- 不正な時刻は無視
173 -- If-Unmodified-Since があればそれを見る。
174 ifUnmodSince <- getHeader "If-Unmodified-Since"
176 Just str -> case parseHTTPDateTime str of
178 -> when (timeStamp > lastTime)
179 $ abort PreconditionFailed [] ("The entity has not been modified since " ++ str)
181 -> return () -- 不正な時刻は無視
187 foundNoEntity :: Maybe String -> Resource ()
189 = do driftTo ExaminingRequest
191 let msg = fromMaybe "The requested entity was not found in this server." msgM
195 $ abort NotFound [] msg
197 -- エンティティが存在しないと云ふ事は、"*" も含めたどのやうな
198 -- If-Match: 條件も滿たさない。
199 ifMatch <- getHeader "If-Match"
200 when (ifMatch /= Nothing)
201 $ abort PreconditionFailed [] msg
206 {- GettingBody 時に使用するアクション群 -}
208 input :: Int -> Resource String
209 input limit = inputBS limit >>= return . B.unpack
212 -- 多くとも limit バイトまでのリクエストボディ全體を受信する。limit が
213 -- 零以下なら Config で設定されたデフォルトのボディ長により制限される。
214 inputBS :: Int -> Resource ByteString
216 = do driftTo GettingBody
218 hasBody <- liftIO $ atomically $ readItr itr itrRequestHasBody id
219 chunk <- if hasBody then
222 do driftTo DecidingHeader
226 askForInput :: Interaction -> Resource ByteString
228 = do let defaultLimit = cnfMaxEntityLength $ itrConfig itr
229 actualLimit = if limit <= 0 then
233 when (actualLimit <= 0)
234 $ fail ("inputBS: limit must be positive: " ++ show actualLimit)
237 $ do chunkLen <- readItr itr itrReqChunkLength id
238 writeItr itr itrWillReceiveBody True
239 if fmap (> actualLimit) chunkLen == Just True then
243 writeItr itr itrReqBodyWanted $ Just actualLimit
244 -- 應答を待つ。トランザクションを分けなければ當然デッドロック。
245 chunk <- liftIO $ atomically
246 $ do chunk <- readItr itr itrReceivedBody id
247 chunkIsOver <- readItr itr itrReqChunkIsOver id
248 if B.length chunk < fromIntegral actualLimit then
258 $ tooLarge actualLimit
259 -- 成功。itr 内にチャンクを置いたままにす
261 writeItr itr itrReceivedBody B.empty
263 driftTo DecidingHeader
266 tooLarge :: Int -> STM ()
267 tooLarge lim = abortSTM RequestEntityTooLarge []
268 ("Request body must be smaller than "
269 ++ show lim ++ " bytes.")
272 inputChunk :: Int -> Resource String
273 inputChunk limit = inputChunkBS limit >>= return . B.unpack
276 -- 多くとも limit バイトまでのリクエストボディの一部を受信する。limit
277 -- が 0 以下なら Config で設定されたデフォルトのボディ長により制限され
278 -- る。これ以上ボディが殘ってゐなければ空文字列を返す。
279 inputChunkBS :: Int -> Resource ByteString
281 = do driftTo GettingBody
283 hasBody <- liftIO $ atomically $ readItr itr itrRequestHasBody id
284 chunk <- if hasBody then
287 do driftTo DecidingHeader
291 askForInput :: Interaction -> Resource ByteString
293 = do let defaultLimit = cnfMaxEntityLength $ itrConfig itr
294 actualLimit = if limit < 0 then
298 when (actualLimit <= 0)
299 $ fail ("inputChunkBS: limit must be positive: " ++ show actualLimit)
302 $ do writeItr itr itrReqBodyWanted $ Just actualLimit
303 writeItr itr itrWillReceiveBody True
304 -- 應答を待つ。トランザクションを分けなければ當然デッドロック。
305 chunk <- liftIO $ atomically
306 $ do chunk <- readItr itr itrReceivedBody id
307 -- 要求された量に滿たなくて、まだ殘りがあ
309 when (B.length chunk < fromIntegral actualLimit)
310 $ do chunkIsOver <- readItr itr itrReqChunkIsOver id
314 writeItr itr itrReceivedBody B.empty
317 $ driftTo DecidingHeader
326 {- DecidingHeader 時に使用するアクション群 -}
328 setStatus :: StatusCode -> Resource ()
330 = do driftTo DecidingHeader
332 liftIO $ atomically $ updateItr itr itrResponse
333 $ \ resM -> case resM of
334 Nothing -> Just $ Response {
335 resVersion = HttpVersion 1 1
339 Just res -> Just $ res {
344 setHeader :: String -> String -> Resource ()
346 = driftTo DecidingHeader >> setHeader' name value
349 setHeader' :: String -> String -> Resource()
350 setHeader' name value
352 liftIO $ atomically $ updateItr itr itrResponse
353 $ \ resM -> case resM of
354 Nothing -> Just $ Response {
355 resVersion = HttpVersion 1 1
357 , resHeaders = [ (name, value) ]
359 Just res -> Just $ H.setHeader name value res
362 redirect :: StatusCode -> URI -> Resource ()
364 = do when (code == NotModified || not (isRedirection code))
365 $ abort InternalServerError []
366 $ "Attempted to redirect with status " ++ show code
368 setHeader "Location" (uriToString id uri $ "")
371 setETag :: ETag -> Resource ()
373 = setHeader "ETag" $ show tag
376 setLastModified :: ClockTime -> Resource ()
377 setLastModified lastmod
378 = setHeader "Last-Modified" $ formatHTTPDateTime lastmod
381 setContentType :: MIMEType -> Resource ()
383 = setHeader "Content-Type" $ show mType
386 {- DecidingBody 時に使用するアクション群 -}
388 output :: String -> Resource ()
389 output = outputBS . B.pack
392 outputBS :: ByteString -> Resource ()
393 outputBS str = do outputChunkBS str
397 outputChunk :: String -> Resource ()
398 outputChunk = outputChunkBS . B.pack
401 outputChunkBS :: ByteString -> Resource ()
402 outputChunkBS str = do driftTo DecidingBody
404 liftIO $ atomically $
405 do updateItr itr itrBodyToSend (flip B.append str)
407 $ writeItr itr itrBodyIsNull False
412 [GettingBody からそれ以降の状態に遷移する時]
414 body を讀み終へてゐなければ、殘りの body を讀み捨てる。
417 [DecidingHeader からそれ以降の状態に遷移する時]
424 bodyIsNull が False ならば何もしない。True だった場合は出力補完す
425 る。Content-Type も變はる。但し(デフォルトのまま)Status が 200 OK
426 だった場合は、補完の代はりに 204 No Content に變へる。
430 driftTo :: InteractionState -> Resource ()
433 liftIO $ atomically $ do oldState <- readItr itr itrState id
434 if newState < oldState then
435 throwStateError oldState newState
437 do let a = [oldState .. newState]
440 mapM_ (uncurry $ drift itr) c
441 writeItr itr itrState newState
443 throwStateError :: Monad m => InteractionState -> InteractionState -> m a
445 throwStateError Done DecidingBody
446 = fail "It makes no sense to output something after finishing to output."
448 throwStateError old new
449 = fail ("state error: " ++ show old ++ " ==> " ++ show new)
452 drift :: Interaction -> InteractionState -> InteractionState -> STM ()
454 drift itr GettingBody _
455 = writeItr itr itrReqBodyWasteAll True
457 drift itr DecidingHeader _
461 = do bodyIsNull <- readItr itr itrBodyIsNull id
463 $ do status <- readStatus itr
465 do updateItrF itr itrResponse
466 $ \ res -> res { resStatus = NoContent }
467 updateItrF itr itrResponse
468 $ H.deleteHeader "Content-Type"
469 updateItrF itr itrResponse
470 $ H.deleteHeader "ETag"
471 updateItrF itr itrResponse
472 $ H.deleteHeader "Last-Modified"
481 readStatus :: Interaction -> STM StatusCode
482 readStatus itr = readItr itr itrResponse (resStatus . fromJust)