X-Git-Url: http://git.cielonegro.org/gitweb.cgi?p=Lucu.git;a=blobdiff_plain;f=Network%2FHTTP%2FLucu%2FRequestReader.hs;h=4c59b3e9f8b1ac5a1524d634d2595a339c80c853;hp=49317a99ea8343270f222b7061c8bdd8c00cb322;hb=1ead053df6a792edafa9d714c4c038a8a9c3ad16;hpb=ea2b7838f1b3d9d4923a220a601be2e04cc559d7 diff --git a/Network/HTTP/Lucu/RequestReader.hs b/Network/HTTP/Lucu/RequestReader.hs index 49317a9..4c59b3e 100644 --- a/Network/HTTP/Lucu/RequestReader.hs +++ b/Network/HTTP/Lucu/RequestReader.hs @@ -1,5 +1,7 @@ {-# LANGUAGE - DoAndIfThenElse + CPP + , DoAndIfThenElse + , OverloadedStrings , RecordWildCards , ScopedTypeVariables , UnicodeSyntax @@ -8,28 +10,30 @@ module Network.HTTP.Lucu.RequestReader ( requestReader ) where -import Control.Applicative +import Control.Concurrent import Control.Concurrent.STM -import Control.Exception +import Control.Exception hiding (block) import Control.Monad import qualified Data.Attoparsec.Lazy as LP +import qualified Data.ByteString as Strict import qualified Data.ByteString.Lazy as Lazy +import Data.List import Data.Maybe +import Data.Monoid.Unicode import qualified Data.Sequence as S -import Data.Sequence.Unicode -import Data.Text (Text) +import qualified Data.Text as T +import Network.HTTP.Lucu.Abortion import Network.HTTP.Lucu.Config import Network.HTTP.Lucu.Chunk -import Network.HTTP.Lucu.DefaultPage import Network.HTTP.Lucu.HandleLike import Network.HTTP.Lucu.Interaction -import Network.HTTP.Lucu.Postprocess import Network.HTTP.Lucu.Preprocess import Network.HTTP.Lucu.Request import Network.HTTP.Lucu.Response +import Network.HTTP.Lucu.Resource.Internal import Network.HTTP.Lucu.Resource.Tree +import Network.HTTP.Lucu.Utils import Network.Socket -import Network.URI import Prelude.Unicode import System.IO (hPutStrLn, stderr) @@ -44,6 +48,11 @@ data Context h , cQueue ∷ !InteractionQueue } +data ChunkReceivingState + = Initial + | InChunk !Int -- ^Number of remaining octets in the current + -- chunk. It's always positive. + requestReader ∷ HandleLike h ⇒ Config → ResTree @@ -57,37 +66,42 @@ requestReader cnf tree fbs h port addr tQueue = do input ← hGetLBS h acceptRequest (Context cnf tree fbs h port addr tQueue) input `catches` - [ Handler $ \ (_ ∷ IOException) → return () - , Handler $ \ e → case e of - ThreadKilled → return () - _ → hPutStrLn stderr (show e) - , Handler $ \ BlockedIndefinitelyOnSTM → hPutStrLn stderr "requestReader: blocked indefinitely" - , Handler $ \ (e ∷ SomeException) → hPutStrLn stderr (show e) + [ Handler handleAsyncE + , Handler handleOthers ] + `finally` + enqueue' tQueue EndOfInteraction + where + handleAsyncE ∷ AsyncException → IO () + handleAsyncE ThreadKilled = return () + handleAsyncE e = dump e + + handleOthers ∷ SomeException → IO () + handleOthers = dump + + dump ∷ Exception e ⇒ e → IO () + dump e + = do hPutStrLn stderr "Lucu: requestReader caught an exception:" + hPutStrLn stderr $ show e acceptRequest ∷ HandleLike h ⇒ Context h → Lazy.ByteString → IO () acceptRequest ctx@(Context {..}) input - -- キューに最大パイプライン深度以上のリクエストが溜まってゐる時は、 - -- それが限度以下になるまで待つ。 = do atomically $ do queue ← readTVar cQueue - when (S.length queue ≥ cnfMaxPipelineDepth cConfig) $ + when (S.length queue ≥ cnfMaxPipelineDepth cConfig) + -- Too many requests in the pipeline... retry - -- リクエストを讀む。パースできない場合は直ちに 400 Bad - -- Request 應答を設定し、それを出力してから切斷するやうに - -- ResponseWriter に通知する。 - case LP.parse requestP input of - LP.Done input' req → acceptParsableRequest ctx req input' - LP.Fail _ _ _ → acceptNonparsableRequest ctx BadRequest + if Lazy.null input then + return () + else + case LP.parse request input of + LP.Done input' req → acceptParsableRequest ctx req input' + LP.Fail _ _ _ → acceptNonparsableRequest ctx -acceptNonparsableRequest ∷ HandleLike h ⇒ Context h → StatusCode → IO () -acceptNonparsableRequest ctx@(Context {..}) sc - = do itr ← newInteraction cConfig cPort cAddr Nothing (Left sc) - atomically $ - do writeTVar (itrState itr) Done - writeDefaultPage itr - postprocess itr - enqueue ctx itr +acceptNonparsableRequest ∷ HandleLike h ⇒ Context h → IO () +acceptNonparsableRequest ctx@(Context {..}) + = do syi ← mkSyntacticallyInvalidInteraction cConfig + enqueue ctx syi acceptParsableRequest ∷ HandleLike h ⇒ Context h @@ -95,242 +109,276 @@ acceptParsableRequest ∷ HandleLike h → Lazy.ByteString → IO () acceptParsableRequest ctx@(Context {..}) req input - = do cert ← hGetPeerCert cHandle - itr ← newInteraction cConfig cPort cAddr cert (Right req) - join $ atomically - $ do isErr ← (isError ∘ resStatus) <$> readTVar (itrResponse itr) - if isErr then - acceptSemanticallyInvalidRequest ctx itr input - else - return $ acceptSemanticallyValidRequest ctx itr (reqURI req) input + = do let ar = preprocess (cnfServerHost cConfig) cPort req + if isError $ arInitialStatus ar then + acceptSemanticallyInvalidRequest ctx ar input + else + do rsrc ← findResource cResTree cFallbacks $ reqURI $ arRequest ar + case rsrc of + Nothing + → do let ar' = ar { + arInitialStatus = fromStatusCode NotFound + } + acceptSemanticallyInvalidRequest ctx ar' input + Just (path, def) + → acceptRequestForResource ctx ar input path def acceptSemanticallyInvalidRequest ∷ HandleLike h ⇒ Context h - → Interaction - → Lazy.ByteString - → STM (IO ()) -acceptSemanticallyInvalidRequest ctx itr input - = do writeTVar (itrState itr) Done - writeDefaultPage itr - postprocess itr - enqueue ctx itr - return $ acceptRequest ctx input - -acceptSemanticallyValidRequest ∷ HandleLike h - ⇒ Context h - → Interaction - → URI - → Lazy.ByteString - → IO () -acceptSemanticallyValidRequest ctx@(Context {..}) itr uri input - = do rsrcM ← findResource cResTree cFallbacks uri - case rsrcM of - Nothing - → acceptRequestForNonexistentResource ctx itr input - Just (rsrcPath, rsrcDef) - → acceptRequestForExistentResource ctx itr input rsrcPath rsrcDef - -acceptRequestForNonexistentResource ∷ HandleLike h - ⇒ Context h - → Interaction - → Lazy.ByteString - → IO () -acceptRequestForNonexistentResource ctx itr input - = do atomically $ - do setResponseStatus itr NotFound - writeTVar (itrState itr) Done - writeDefaultPage itr - postprocess itr - enqueue ctx itr - acceptRequest ctx input - -acceptRequestForExistentResource ∷ HandleLike h - ⇒ Context h - → Interaction + → AugmentedRequest → Lazy.ByteString - → [Text] - → ResourceDef → IO () -acceptRequestForExistentResource ctx oldItr input rsrcPath rsrcDef - = do let itr = oldItr { itrResourcePath = Just rsrcPath } - atomically $ enqueue ctx itr - do _ ← runResource rsrcDef itr - if reqHasBody $ fromJust $ itrRequest itr then - observeRequest ctx itr input - else - acceptRequest ctx input +acceptSemanticallyInvalidRequest ctx@(Context {..}) ar input + = do sei ← mkSemanticallyInvalidInteraction cConfig ar + enqueue ctx sei + acceptRequest ctx input -observeRequest ∷ HandleLike h - ⇒ Context h - → Interaction - → Lazy.ByteString - → IO () -observeRequest ctx itr input - = case fromJust $ itrReqBodyLength itr of - Chunked - → observeChunkedRequest ctx itr input 0 - Fixed len - → observeNonChunkedRequest ctx itr input len +acceptRequestForResource ∷ HandleLike h + ⇒ Context h + → AugmentedRequest + → Lazy.ByteString + → [Strict.ByteString] + → Resource + → IO () +acceptRequestForResource ctx@(Context {..}) ar@(AugmentedRequest {..}) input rsrcPath rsrcDef + = do +#if defined(HAVE_SSL) + cert ← hGetPeerCert cHandle + ni ← mkNormalInteraction cConfig cAddr cert ar rsrcPath +#else + ni ← mkNormalInteraction cConfig cAddr ar rsrcPath +#endif + tid ← spawnRsrc rsrcDef ni + enqueue ctx ni + if reqMustHaveBody arRequest then + waitForReceiveBodyReq ctx ni tid input + else + acceptRequest ctx input -observeChunkedRequest ∷ HandleLike h +waitForReceiveBodyReq ∷ HandleLike h ⇒ Context h - → Interaction + → NormalInteraction + → ThreadId → Lazy.ByteString - → Int → IO () -observeChunkedRequest ctx itr input remaining +waitForReceiveBodyReq ctx ni@(NI {..}) rsrcTid input + = case fromJust niReqBodyLength of + Chunked + → waitForReceiveChunkedBodyReqForTheFirstTime ctx ni rsrcTid input + Fixed len + → waitForReceiveNonChunkedBodyReqForTheFirstTime ctx ni input len + +-- Toooooo long name for a function... +waitForReceiveChunkedBodyReqForTheFirstTime ∷ HandleLike h + ⇒ Context h + → NormalInteraction + → ThreadId + → Lazy.ByteString + → IO () +waitForReceiveChunkedBodyReqForTheFirstTime ctx ni@(NI {..}) rsrcTid input = join $ atomically $ - do isOver ← readTVar $ itrReqChunkIsOver itr - if isOver then - return $ acceptRequest ctx input - else - do wanted ← readTVar $ itrReqBodyWanted itr - case wanted of - 0 → do wasteAll ← readTVar $ itrReqBodyWasteAll itr - if wasteAll then - return $ wasteCurrentChunk ctx itr input remaining - else - retry - _ → return $ readCurrentChunk ctx itr input wanted remaining + do req ← takeTMVar niReceiveBodyReq + case req of + ReceiveBody wanted + → do putTMVar niSendContinue niExpectedContinue + return $ readCurrentChunk ctx ni rsrcTid wanted input Initial + WasteAll + → do putTMVar niSendContinue False + return $ wasteAllChunks ctx rsrcTid input Initial -wasteCurrentChunk ∷ HandleLike h - ⇒ Context h - → Interaction - → Lazy.ByteString - → Int - → IO () -wasteCurrentChunk ctx itr input len - | len > 0 - = let input' = Lazy.drop (fromIntegral len) input - in - case LP.parse chunkFooterP input' of - LP.Done input'' _ - → observeChunkedRequest ctx itr input'' 0 - LP.Fail _ _ _ - → chunkWasMalformed itr - | otherwise - = seekNextChunk ctx itr input +waitForReceiveChunkedBodyReq ∷ HandleLike h + ⇒ Context h + → NormalInteraction + → ThreadId + → Lazy.ByteString + → ChunkReceivingState + → IO () +waitForReceiveChunkedBodyReq ctx ni@(NI {..}) rsrcTid input st + = do req ← atomically $ takeTMVar niReceiveBodyReq + case req of + ReceiveBody wanted + → readCurrentChunk ctx ni rsrcTid wanted input st + WasteAll + → wasteAllChunks ctx rsrcTid input st + +wasteAllChunks ∷ HandleLike h + ⇒ Context h + → ThreadId + → Lazy.ByteString + → ChunkReceivingState + → IO () +wasteAllChunks ctx rsrcTid = go + where + go ∷ Lazy.ByteString → ChunkReceivingState → IO () + go input Initial + = case LP.parse chunkHeader input of + LP.Done input' chunkLen + | chunkLen ≡ 0 → gotFinalChunk input' + | otherwise → gotChunk input' chunkLen + LP.Fail _ eCtx e + → chunkWasMalformed rsrcTid eCtx e + "wasteAllChunks: chunkHeader" + go input (InChunk chunkLen) + = gotChunk input chunkLen + + gotChunk ∷ Lazy.ByteString → Int → IO () + gotChunk input chunkLen + = let input' = Lazy.drop (fromIntegral chunkLen) input + in + case LP.parse chunkFooter input' of + LP.Done input'' _ + → go input'' Initial + LP.Fail _ eCtx e + → chunkWasMalformed rsrcTid eCtx e + "wasteAllChunks: chunkFooter" + + gotFinalChunk ∷ Lazy.ByteString → IO () + gotFinalChunk input + = case LP.parse chunkTrailer input of + LP.Done input' _ + → acceptRequest ctx input' + LP.Fail _ eCtx e + → chunkWasMalformed rsrcTid eCtx e + "wasteAllChunks: chunkTrailer" readCurrentChunk ∷ HandleLike h ⇒ Context h - → Interaction - → Lazy.ByteString - → Int + → NormalInteraction + → ThreadId → Int + → Lazy.ByteString + → ChunkReceivingState → IO () -readCurrentChunk ctx itr input wanted remaining - | remaining > 0 - = do let bytesToRead = fromIntegral $ min wanted remaining - (chunk, input') = Lazy.splitAt bytesToRead input - actualReadBytes = fromIntegral $ Lazy.length chunk - newWanted = wanted - actualReadBytes - newRemaining = remaining - actualReadBytes - chunk' = S.fromList $ Lazy.toChunks chunk - updateStates = atomically $ - do writeTVar (itrReqBodyWanted itr) newWanted - oldBody ← readTVar $ itrReceivedBody itr - oldBodyLen ← readTVar $ itrReceivedBodyLen itr - writeTVar (itrReceivedBody itr) $ oldBody ⋈ chunk' - writeTVar (itrReceivedBodyLen itr) $ oldBodyLen + actualReadBytes - if newRemaining ≡ 0 then - case LP.parse chunkFooterP input' of - LP.Done input'' _ - → do updateStates - observeChunkedRequest ctx itr input'' 0 - LP.Fail _ _ _ - → chunkWasMalformed itr - else - do updateStates - observeChunkedRequest ctx itr input' newRemaining - | otherwise - = seekNextChunk ctx itr input +readCurrentChunk ctx ni@(NI {..}) rsrcTid wanted = go + where + go ∷ Lazy.ByteString → ChunkReceivingState → IO () + go input Initial + = case LP.parse chunkHeader input of + LP.Done input' chunkLen + | chunkLen ≡ 0 + → gotFinalChunk input' + | otherwise + → gotChunk input' chunkLen + LP.Fail _ eCtx e + → chunkWasMalformed rsrcTid eCtx e + "readCurrentChunk: chunkHeader" + go input (InChunk chunkLen) + = gotChunk input chunkLen -seekNextChunk ∷ HandleLike h - ⇒ Context h - → Interaction - → Lazy.ByteString - → IO () -seekNextChunk ctx itr input - = case LP.parse chunkHeaderP input of - LP.Done input' len - | len ≡ 0 -- Final chunk - → case LP.parse chunkTrailerP input' of + gotChunk ∷ Lazy.ByteString → Int → IO () + gotChunk input chunkLen + = do let bytesToRead = min wanted chunkLen + (block, input') = Lazy.splitAt (fromIntegral bytesToRead) input + block' = Strict.concat $ Lazy.toChunks block + actualReadBytes = Strict.length block' + chunkLen' = chunkLen - actualReadBytes + atomically $ putTMVar niReceivedBody block' + if chunkLen' ≡ 0 then + case LP.parse chunkFooter input' of LP.Done input'' _ - → do atomically $ - writeTVar (itrReqChunkIsOver itr) True - acceptRequest ctx input'' - LP.Fail _ _ _ - → chunkWasMalformed itr - | otherwise -- Non-final chunk - → observeChunkedRequest ctx itr input' len - LP.Fail _ _ _ - → chunkWasMalformed itr + → waitForReceiveChunkedBodyReq ctx ni rsrcTid input'' Initial + LP.Fail _ eCtx e + → chunkWasMalformed rsrcTid eCtx e + "readCurrentChunk: chunkFooter" + else + waitForReceiveChunkedBodyReq ctx ni rsrcTid input' $ InChunk chunkLen' -chunkWasMalformed ∷ Interaction → IO () -chunkWasMalformed itr - = atomically $ - do setResponseStatus itr BadRequest - writeTVar (itrWillClose itr) True - writeTVar (itrState itr) Done - writeDefaultPage itr - postprocess itr + gotFinalChunk ∷ Lazy.ByteString → IO () + gotFinalChunk input + = do atomically $ putTMVar niReceivedBody (∅) + case LP.parse chunkTrailer input of + LP.Done input' _ + → acceptRequest ctx input' + LP.Fail _ eCtx e + → chunkWasMalformed rsrcTid eCtx e + "readCurrentChunk: chunkTrailer" -observeNonChunkedRequest ∷ HandleLike h - ⇒ Context h - → Interaction - → Lazy.ByteString - → Int - → IO () -observeNonChunkedRequest ctx itr input remaining +chunkWasMalformed ∷ ThreadId → [String] → String → String → IO () +chunkWasMalformed tid eCtx e msg + = let abo = mkAbortion BadRequest [("Connection", "close")] + $ Just + $ "chunkWasMalformed: " + ⊕ T.pack msg + ⊕ ": " + ⊕ T.pack (intercalate ", " eCtx) + ⊕ ": " + ⊕ T.pack e + in + throwTo tid abo + +waitForReceiveNonChunkedBodyReqForTheFirstTime ∷ HandleLike h + ⇒ Context h + → NormalInteraction + → Lazy.ByteString + → Int + → IO () +waitForReceiveNonChunkedBodyReqForTheFirstTime ctx ni@(NI {..}) input bodyLen = join $ atomically $ - do wanted ← readTVar $ itrReqBodyWanted itr - case wanted of - 0 → do wasteAll ← readTVar $ itrReqBodyWasteAll itr - if wasteAll then - return $ wasteNonChunkedRequestBody ctx itr input remaining - else - retry - _ → return $ readNonChunkedRequestBody ctx itr input wanted remaining + do req ← takeTMVar niReceiveBodyReq + case req of + ReceiveBody wanted + → do putTMVar niSendContinue niExpectedContinue + return $ readNonChunkedRequestBody ctx ni input bodyLen wanted + WasteAll + → do putTMVar niSendContinue False + return $ wasteNonChunkedRequestBody ctx input bodyLen + +waitForReceiveNonChunkedBodyReq ∷ HandleLike h + ⇒ Context h + → NormalInteraction + → Lazy.ByteString + → Int + → IO () +waitForReceiveNonChunkedBodyReq ctx ni@(NI {..}) input bodyLen + = do req ← atomically $ takeTMVar niReceiveBodyReq + case req of + ReceiveBody wanted + → readNonChunkedRequestBody ctx ni input bodyLen wanted + WasteAll + → wasteNonChunkedRequestBody ctx input bodyLen wasteNonChunkedRequestBody ∷ HandleLike h ⇒ Context h - → Interaction → Lazy.ByteString → Int → IO () -wasteNonChunkedRequestBody ctx itr input remaining - = do let input' = Lazy.drop (fromIntegral remaining) input - atomically $ writeTVar (itrReqChunkIsOver itr) True +wasteNonChunkedRequestBody ctx input bodyLen + = do let input' = Lazy.drop (fromIntegral bodyLen) input acceptRequest ctx input' readNonChunkedRequestBody ∷ HandleLike h ⇒ Context h - → Interaction + → NormalInteraction → Lazy.ByteString → Int → Int → IO () -readNonChunkedRequestBody ctx itr input wanted remaining - = do let bytesToRead = min wanted remaining - (chunk, input') = Lazy.splitAt (fromIntegral bytesToRead) input - actualReadBytes = fromIntegral $ Lazy.length chunk - newWanted = wanted - actualReadBytes - newRemaining = remaining - actualReadBytes - isOver = actualReadBytes < bytesToRead ∨ newRemaining ≡ 0 - chunk' = S.fromList $ Lazy.toChunks chunk - atomically $ - do writeTVar (itrReqChunkIsOver itr) isOver - writeTVar (itrReqBodyWanted itr) newWanted - writeTVar (itrReceivedBody itr) chunk' - writeTVar (itrReceivedBodyLen itr) actualReadBytes - if isOver then - acceptRequest ctx input' - else - observeNonChunkedRequest ctx itr input' newRemaining +readNonChunkedRequestBody ctx ni@(NI {..}) input bodyLen wanted + | bodyLen ≡ 0 = gotEndOfRequest + | otherwise = gotBody + where + gotBody ∷ IO () + gotBody + = do let bytesToRead = min wanted bodyLen + (block, input') = Lazy.splitAt (fromIntegral bytesToRead) input + block' = Strict.concat $ Lazy.toChunks block + actualReadBytes = Strict.length block' + bodyLen' = bodyLen - actualReadBytes + atomically $ putTMVar niReceivedBody block' + waitForReceiveNonChunkedBodyReq ctx ni input' bodyLen' -enqueue ∷ HandleLike h ⇒ Context h → Interaction → STM () -enqueue (Context {..}) itr - = do queue ← readTVar cQueue - writeTVar cQueue (itr ⊲ queue) + gotEndOfRequest ∷ IO () + gotEndOfRequest + = do atomically $ putTMVar niReceivedBody (∅) + acceptRequest ctx input + +enqueue ∷ (HandleLike h, Interaction i) ⇒ Context h → i → IO () +enqueue (Context {..}) = enqueue' cQueue + +enqueue' ∷ Interaction i ⇒ InteractionQueue → i → IO () +enqueue' tQueue itr + = atomically $ + do queue ← readTVar tQueue + writeTVar tQueue (toInteraction itr ⊲ queue)