X-Git-Url: http://git.cielonegro.org/gitweb.cgi?p=Lucu.git;a=blobdiff_plain;f=Network%2FHTTP%2FLucu%2FRequestReader.hs;h=4c59b3e9f8b1ac5a1524d634d2595a339c80c853;hp=58183787a3942b81993e3ba00ca22e67b3b8fa90;hb=1ead053df6a792edafa9d714c4c038a8a9c3ad16;hpb=ca338174155913a969808d7b20193973394e474e diff --git a/Network/HTTP/Lucu/RequestReader.hs b/Network/HTTP/Lucu/RequestReader.hs index 5818378..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,29 +10,32 @@ module Network.HTTP.Lucu.RequestReader ( requestReader ) where -import Control.Applicative -import Control.Concurrent.STM -import Control.Exception -import Control.Monad +import Control.Concurrent +import Control.Concurrent.STM +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.Maybe +import Data.List +import Data.Maybe +import Data.Monoid.Unicode import qualified Data.Sequence as S -import Data.Sequence.Unicode -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.Tree +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.HandleLike +import Network.HTTP.Lucu.Interaction +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) +import System.IO (hPutStrLn, stderr) data Context h = Context { @@ -43,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 @@ -56,227 +66,319 @@ 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 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 (Context {..}) status - = do itr ← newInteraction cConfig cPort cAddr Nothing Nothing - atomically $ - do setResponseStatus itr status - writeTVar (itrWillClose itr) True - writeTVar (itrState itr) Done - writeDefaultPage itr - postprocess itr - enqueue itr +acceptNonparsableRequest ∷ HandleLike h ⇒ Context h → IO () +acceptNonparsableRequest ctx@(Context {..}) + = do syi ← mkSyntacticallyInvalidInteraction cConfig + enqueue ctx syi acceptParsableRequest ∷ HandleLike h ⇒ Context h → Request → Lazy.ByteString → IO () -acceptParsableRequest (Context {..}) req input - = do cert ← hGetPeerCert cHandle - itr ← newInteraction cConfig cPort cAddr cert (Right req) - join $ atomically - $ do preprocess itr - isErr ← (isError ∘ resStatus) <$> readTVar (itrResponse itr) - if isErr then - acceptSemanticallyInvalidRequest itr input - else - acceptSemanticallyValidRequest itr (reqURI req) input - -acceptSemanticallyInvalidRequest ∷ Interaction → Lazy.ByteString → STM (IO ()) -acceptSemanticallyInvalidRequest itr input - = do writeTVar (itr itrState) Done - writeDefaultPage itr - postprocess itr - enqueue itr - return $ acceptRequest input - -acceptSemanticallyValidRequest ∷ HandleLike h - ⇒ Context h - → Interaction - → URI - → Lazy.ByteString - → IO () -acceptSemanticallyValidRequest (Context {..}) itr uri input - = do rsrcM ← findResource cResTree cFallbacks uri - case rsrcM of - Nothing - → acceptRequestForNonexistentResource itr input - Just (rsrcPath, rsrcDef) - → acceptRequestForExistentResource itr input rsrcPath rsrcDef +acceptParsableRequest ctx@(Context {..}) 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 -acceptRequestForNonexistentResource ∷ Interaction → Lazy.ByteString → STM (IO ()) -acceptRequestForNonexistentResource itr input - = do setResponseStatus itr NotFound - writeTVar (itrState itr) Done - writeDefaultPage itr - postprocess itr - enqueue itr - return $ acceptRequest input +acceptSemanticallyInvalidRequest ∷ HandleLike h + ⇒ Context h + → AugmentedRequest + → Lazy.ByteString + → IO () +acceptSemanticallyInvalidRequest ctx@(Context {..}) ar input + = do sei ← mkSemanticallyInvalidInteraction cConfig ar + enqueue ctx sei + acceptRequest ctx input -acceptRequestForExistentResource ∷ Interaction → Lazy.ByteString → [String] → ResourceDef → STM (IO ()) -acceptRequestForExistentResource oldItr input rsrcPath rsrcDef - = do let itr = oldItr { itrResourcePath = Just rsrcPath } - enqueue itr - return $ do _ ← runResource rsrcDef itr - if reqHasBody $ fromJust $ itrRequest itr then - observeRequest itr input - else - acceptRequest input +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 -observeRequest ∷ Interaction → Lazy.ByteString → IO () -observeRequest itr input - | itrReqBodyLength itr ≡ Just Chunked - = observeChunkedRequest itr input - | otherwise - = observeNonChunkedRequest itr input +waitForReceiveBodyReq ∷ HandleLike h + ⇒ Context h + → NormalInteraction + → ThreadId + → Lazy.ByteString + → IO () +waitForReceiveBodyReq ctx ni@(NI {..}) rsrcTid input + = case fromJust niReqBodyLength of + Chunked + → waitForReceiveChunkedBodyReqForTheFirstTime ctx ni rsrcTid input + Fixed len + → waitForReceiveNonChunkedBodyReqForTheFirstTime ctx ni input len -observeChunkedRequest ∷ Interaction → Lazy.ByteString → IO () -observeChunkedRequest itr input +-- 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 input - else - do wantedM ← readTVar $ itrReqBodyWanted itr - if isNothing wantedM then - do wasteAll ← readTVar $ itrReqBodyWasteAll itr - if wasteAll then - wasteCurrentChunk input - else - retry - else - readCurrentChunk (fromJust wantedM) + 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 + +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 -wasteCurrentChunk ∷ Interaction → Lazy.ByteString → Int → IO () -wasteCurrentChunk itr input len - | len > 0 - = let input' = Lazy.drop (fromIntegral len) input - in - case LP.parse chunkFooterP input' of - LP.Done input'' _ - → observeChunkedRequest itr input'' - LP.Fail _ _ _ - → chunkWasMalformed itr - | otherwise - = seekNextChunk itr input +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" -readCurrentChunk ∷ Interaction → Lazy.ByteString → Int → Int → IO () -readCurrentChunk 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 = case wanted - actualReadBytes of - 0 → Nothing - n → Just n - newRemaining = Just $ remaining - actualReadBytes - updateStates = 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 ≡ Just 0 then - case LP.parse chunkFooterP input' of - LP.Done input'' _ - → do updateStates - observeChunkedRequest itr input'' - LP.Fail _ _ _ - → chunkWasMalformed itr - else - do updateStates - observeChunkedRequest itr input' - | otherwise - = seekNextChunk itr input + 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 + → NormalInteraction + → ThreadId + → Int + → Lazy.ByteString + → ChunkReceivingState + → IO () +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 ∷ Interaction → Lazy.ByteString → IO () -seekNextChunk 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 writeTVar (itrReqChunkIsOver itr) True - acceptRequest input'' - LP.Fail _ _ _ - → chunkWasMalformed itr - | otherwise -- Non-final chunk - → do observeChunkedRequest itr input' - 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 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" + +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 -observeNonChunkedRequest ∷ Interaction → Lazy.ByteString → IO () -observeNonChunkedRequest itr input +waitForReceiveNonChunkedBodyReqForTheFirstTime ∷ HandleLike h + ⇒ Context h + → NormalInteraction + → Lazy.ByteString + → Int + → IO () +waitForReceiveNonChunkedBodyReqForTheFirstTime ctx ni@(NI {..}) input bodyLen = join $ atomically $ - do wantedM ← readTVar $ itrReqBodyWanted itr - if isNothing wantedM then - do wasteAll ← readTVar itr itrReqBodyWasteAll id - if wasteAll then - wasteNonChunkedRequestBody itr input - else - retry - else - readNonChunkedRequestBody itr input + 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 -wasteNonChunkedRequestBody ∷ Interaction → Lazy.ByteString → Maybe Int → IO () -wasteNonChunkedRequestBody itr input remaining - = do let input' = case remaining of - Just len → Lazy.drop len input - Nothing → (∅) - writeTVar (itrReqChunkIsOver itr) True - acceptRequest input' +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 -readNonChunkedRequestBody ∷ Interaction → Lazy.ByteString → Int → Maybe Int → IO () -readNonChunkedRequestBody itr input wanted remaining - = do let bytesToRead = fromIntegral $ maybe wanted (min wanted) remaining - (chunk, input') = Lazy.splitAt bytesToRead input - actualReadBytes = fromIntegral $ Lazy.length chunk - newRemaining = (- actualReadBytes) <$> remaining - isOver = actualReadBytes < bytesToRead ∨ newRemaining ≡ Just 0 - writeTVar (itrReqChunkIsOver itr) isOver - writeTVar (itrReqBodyWanted itr) Nothing - writeTVar (itrReceivedBody itr) chunk - writeTVar (itrReceivedBodyLen itr) actualReadBytes - if isOver then - acceptRequest input' - else - observeNonChunkedRequest itr input' +wasteNonChunkedRequestBody ∷ HandleLike h + ⇒ Context h + → Lazy.ByteString + → Int + → IO () +wasteNonChunkedRequestBody ctx input bodyLen + = do let input' = Lazy.drop (fromIntegral bodyLen) input + acceptRequest ctx input' + +readNonChunkedRequestBody ∷ HandleLike h + ⇒ Context h + → NormalInteraction + → Lazy.ByteString + → Int + → Int + → IO () +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)