)
where
import qualified Blaze.ByteString.Builder.HTTP as BB
+import Control.Applicative
import Control.Concurrent
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
-import qualified Data.Ascii as A
+import Data.Ascii (AsciiBuilder)
+import Data.Convertible.Utils
+import GHC.IO.Exception (IOException(..), IOErrorType(..))
import Data.Monoid.Unicode
import qualified Data.Sequence as S
import Data.Sequence (ViewR(..))
import Network.HTTP.Lucu.HandleLike
import Network.HTTP.Lucu.HttpVersion
import Network.HTTP.Lucu.Interaction
-import Network.HTTP.Lucu.Postprocess
+import Network.HTTP.Lucu.Request
import Network.HTTP.Lucu.Response
import Prelude.Unicode
import System.IO (hPutStrLn, stderr)
-import System.IO.Error
data Context h
= Context {
cConfig ∷ !Config
, cHandle ∷ !h
, cQueue ∷ !InteractionQueue
- , cReader ∷ !ThreadId
}
responseWriter ∷ HandleLike h ⇒ Config → h → InteractionQueue → ThreadId → IO ()
responseWriter cnf h tQueue readerTID
- = awaitSomethingToWrite (Context cnf h tQueue readerTID)
+ = awaitSomethingToWrite (Context cnf h tQueue)
`catches`
[ Handler handleIOE
, Handler handleAsyncE
- , Handler handleBIOS
, Handler handleOthers
]
+ `finally`
+ do killThread readerTID
+ hClose h
where
handleIOE ∷ IOException → IO ()
- handleIOE e
- | isIllegalOperation e
- = return () -- EPIPE: should be ignored at all.
- | otherwise
- = terminate e
+ handleIOE e@(IOError {..})
+ | ioe_type ≡ ResourceVanished = return ()
+ | otherwise = dump e
handleAsyncE ∷ AsyncException → IO ()
- handleAsyncE ThreadKilled = terminate'
- handleAsyncE e = terminate e
-
- handleBIOS ∷ BlockedIndefinitelyOnSTM → IO ()
- handleBIOS = terminate
+ handleAsyncE ThreadKilled = return ()
+ handleAsyncE e = dump e
handleOthers ∷ SomeException → IO ()
- handleOthers = terminate
-
- terminate ∷ Exception e ⇒ e → IO ()
- terminate e
- = do hPutStrLn stderr "requestWriter caught an exception:"
- hPutStrLn stderr (show $ toException e)
- terminate'
+ handleOthers = dump
- terminate' ∷ IO ()
- terminate' = hClose h
+ dump ∷ Exception e ⇒ e → IO ()
+ dump e
+ = do hPutStrLn stderr "Lucu: responseWriter caught an exception:"
+ hPutStrLn stderr $ show e
awaitSomethingToWrite ∷ HandleLike h ⇒ Context h → IO ()
awaitSomethingToWrite ctx@(Context {..})
case S.viewr queue of
EmptyR → retry
queue' :> itr → do writeTVar cQueue queue'
- return $ writeContinueIfNeeded ctx itr
+ return $ writeSomething ctx itr
+
+writeSomething ∷ HandleLike h ⇒ Context h → SomeInteraction → IO ()
+writeSomething ctx itr
+ = let writer = writeResponseForNI ctx <$> fromInteraction itr <|>
+ writeResponseForSEI ctx <$> fromInteraction itr <|>
+ writeResponseForSYI ctx <$> fromInteraction itr <|>
+ endOfResponses <$> fromInteraction itr
+ in
+ case writer of
+ Just f → f
+ Nothing → fail "Internal error: unknown interaction type"
+
+writeResponseForNI ∷ HandleLike h
+ ⇒ Context h
+ → NormalInteraction
+ → IO ()
+writeResponseForNI = writeContinueIfNeeded
writeContinueIfNeeded ∷ HandleLike h
⇒ Context h
- → Interaction
+ → NormalInteraction
→ IO ()
-writeContinueIfNeeded ctx@(Context {..}) itr@(Interaction {..})
- = do isNeeded ← atomically $ readTMVar itrSendContinue
+writeContinueIfNeeded ctx@(Context {..}) ni@(NI {..})
+ = do isNeeded ← atomically $ readTMVar niSendContinue
when isNeeded
$ do let cont = Response {
resVersion = HttpVersion 1 1
- , resStatus = Continue
+ , resStatus = fromStatusCode Continue
, resHeaders = (∅)
}
- cont' ← completeUnconditionalHeaders cConfig cont
- hPutBuilder cHandle $ A.toBuilder $ printResponse cont'
+ hPutBuilder cHandle $ convertSuccessVia ((⊥) ∷ AsciiBuilder) cont
hFlush cHandle
- writeHeader ctx itr
+ writeHeader ctx ni
writeHeader ∷ HandleLike h
⇒ Context h
- → Interaction
+ → NormalInteraction
→ IO ()
-writeHeader ctx@(Context {..}) itr@(Interaction {..})
+writeHeader ctx@(Context {..}) ni@(NI {..})
= do res ← atomically $
- do state ← readTVar itrState
+ do state ← readTVar niState
if state ≥ SendingBody then
- readTVar itrResponse
+ readTVar niResponse
else
retry -- Too early to write header fields.
- hPutBuilder cHandle $ A.toBuilder $ printResponse res
+ hPutBuilder cHandle $ convertSuccessVia ((⊥) ∷ AsciiBuilder) res
hFlush cHandle
- writeBodyIfNeeded ctx itr
+ writeBodyIfNeeded ctx ni
writeBodyIfNeeded ∷ HandleLike h
⇒ Context h
- → Interaction
+ → NormalInteraction
→ IO ()
-writeBodyIfNeeded ctx itr@(Interaction {..})
+writeBodyIfNeeded ctx ni@(NI {..})
= join $
atomically $
- do willDiscardBody ← readTVar itrWillDiscardBody
- if willDiscardBody then
- return $ discardBody ctx itr
+ do canHaveBody ← resCanHaveBody <$> readTVar niResponse
+ if canHaveBody ∧ reqMethod niRequest ≢ HEAD then
+ if niWillChunkBody then
+ return $ writeChunkedBody ctx ni
+ else
+ return $ writeNonChunkedBody ctx ni
else
- do willChunkBody ← readTVar itrWillChunkBody
- if willChunkBody then
- return $ writeChunkedBody ctx itr
- else
- return $ writeNonChunkedBody ctx itr
+ return $ discardBody ctx ni
discardBody ∷ HandleLike h
⇒ Context h
- → Interaction
+ → NormalInteraction
→ IO ()
-discardBody ctx itr@(Interaction {..})
+discardBody ctx ni@(NI {..})
= join $
atomically $
- do chunk ← tryTakeTMVar itrBodyToSend
+ do chunk ← tryTakeTMVar niBodyToSend
case chunk of
- Just _ → return $ discardBody ctx itr
- Nothing → do state ← readTVar itrState
+ Just _ → return $ discardBody ctx ni
+ Nothing → do state ← readTVar niState
if state ≡ Done then
- return $ finalize ctx itr
+ return $ finalize ctx ni
else
retry
writeChunkedBody ∷ HandleLike h
⇒ Context h
- → Interaction
+ → NormalInteraction
→ IO ()
-writeChunkedBody ctx@(Context {..}) itr@(Interaction {..})
+writeChunkedBody ctx@(Context {..}) ni@(NI {..})
= join $
atomically $
- do chunk ← tryTakeTMVar itrBodyToSend
+ do chunk ← tryTakeTMVar niBodyToSend
case chunk of
Just b → return $
do hPutBuilder cHandle $ BB.chunkedTransferEncoding b
hFlush cHandle
- writeChunkedBody ctx itr
- Nothing → do state ← readTVar itrState
+ writeChunkedBody ctx ni
+ Nothing → do state ← readTVar niState
if state ≡ Done then
- return $ finalize ctx itr
+ return $
+ do hPutBuilder cHandle BB.chunkedTransferTerminator
+ hFlush cHandle
+ finalize ctx ni
else
retry
writeNonChunkedBody ∷ HandleLike h
⇒ Context h
- → Interaction
+ → NormalInteraction
→ IO ()
-writeNonChunkedBody ctx@(Context {..}) itr@(Interaction {..})
+writeNonChunkedBody ctx@(Context {..}) ni@(NI {..})
= join $
atomically $
- do chunk ← tryTakeTMVar itrBodyToSend
+ do chunk ← tryTakeTMVar niBodyToSend
case chunk of
Just b → return $
do hPutBuilder cHandle b
hFlush cHandle
- writeNonChunkedBody ctx itr
- Nothing → do state ← readTVar itrState
+ writeNonChunkedBody ctx ni
+ Nothing → do state ← readTVar niState
if state ≡ Done then
- return $ finalize ctx itr
+ return $ finalize ctx ni
else
retry
-finalize ∷ HandleLike h ⇒ Context h → Interaction → IO ()
-finalize ctx@(Context {..}) (Interaction {..})
+finalize ∷ HandleLike h ⇒ Context h → NormalInteraction → IO ()
+finalize ctx@(Context {..}) (NI {..})
= join $
atomically $
- do sentContinue ← takeTMVar itrSendContinue
- willDiscardBody ← readTVar itrWillDiscardBody
- willChunkBody ← readTVar itrWillChunkBody
- willClose ← readTVar itrWillClose
- queue ← readTVar cQueue
- case S.viewr queue of
- queue' :> _
- → writeTVar cQueue queue'
- EmptyR
- → fail "finalize: cQueue is empty, which should never happen."
+ do willClose ← readTVar niWillClose
+ sentContinue ← takeTMVar niSendContinue
return $
- do when (((¬) willDiscardBody) ∧ willChunkBody)
- $ do hPutBuilder cHandle BB.chunkedTransferTerminator
- hFlush cHandle
- if willClose ∨ needToClose sentContinue then
- -- The RequestReader is probably blocking on
- -- hWaitForInput so we have to kill it before
- -- closing the socket.
- -- THINKME: Couldn't that somehow be avoided?
- do killThread cReader
- hClose cHandle
- else
- awaitSomethingToWrite ctx
+ if needToClose willClose sentContinue then
+ return ()
+ else
+ awaitSomethingToWrite ctx
where
- needToClose ∷ Bool → Bool
- needToClose sentContinue
+ needToClose ∷ Bool → Bool → Bool
+ needToClose willClose sentContinue
+ -- Explicitly instructed to close the connection.
+ | willClose = True
-- We've sent both "HTTP/1.1 100 Continue" and a final
-- response, so nothing prevents our connection from keeping
-- alive.
-- (rejected) request body OR start a completely new request
-- in this situation. So the only possible thing to do is to
-- brutally shutdown the connection.
- | itrExpectedContinue ≡ Just True = True
+ | niExpectedContinue = True
-- The client didn't expect 100-continue so we haven't sent
-- one. No need to do anything special.
| otherwise = False
+
+writeResponseForSEI ∷ HandleLike h
+ ⇒ Context h
+ → SemanticallyInvalidInteraction
+ → IO ()
+writeResponseForSEI ctx@(Context {..}) (SEI {..})
+ = do hPutBuilder cHandle $ convertSuccessVia ((⊥) ∷ AsciiBuilder) seiResponse
+ when (reqMethod seiRequest ≢ HEAD) $
+ if seiWillChunkBody then
+ do hPutBuilder cHandle $ BB.chunkedTransferEncoding seiBodyToSend
+ hPutBuilder cHandle BB.chunkedTransferTerminator
+ else
+ hPutBuilder cHandle seiBodyToSend
+ hFlush cHandle
+ if seiWillClose ∨ seiExpectedContinue then
+ return ()
+ else
+ awaitSomethingToWrite ctx
+
+writeResponseForSYI ∷ HandleLike h
+ ⇒ Context h
+ → SyntacticallyInvalidInteraction
+ → IO ()
+writeResponseForSYI (Context {..}) (SYI {..})
+ = do hPutBuilder cHandle $ convertSuccessVia ((⊥) ∷ AsciiBuilder) syiResponse
+ hPutBuilder cHandle syiBodyToSend
+ hFlush cHandle
+ return ()
+
+endOfResponses ∷ EndOfInteraction → IO ()
+endOfResponses _ = return ()