]> gitweb @ CieloNegro.org - Lucu.git/blobdiff - Network/HTTP/Lucu/ResponseWriter.hs
Bugfix regarding HEAD requests
[Lucu.git] / Network / HTTP / Lucu / ResponseWriter.hs
index 587c01b695b1de62111a14ce9a70772cf467aa50..0af4a69df02f1a78604b0bc46d3b71077a545768 100644 (file)
@@ -9,11 +9,14 @@ module Network.HTTP.Lucu.ResponseWriter
     )
     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(..))
@@ -21,199 +24,237 @@ import Network.HTTP.Lucu.Config
 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
       }
 
-data Phase = Initial
-           | WroteContinue
-           | WroteHeader
-             deriving (Eq, Ord, Show)
-
 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 {..})
     = join $
       atomically $
-      -- キューが空でなくなるまで待つ
       do queue ← readTVar cQueue
          case S.viewr queue of
            EmptyR        → retry
            queue' :> itr → do writeTVar cQueue queue'
-                              return $ awaitSomethingToWriteOn ctx itr Initial
-
--- GettingBody 状態にあり、Continue が期待されてゐて、それがまだ送信前
--- なのであれば、Continue を送信する。
-awaitSomethingToWriteOn ∷ HandleLike h
-                        ⇒ Context h
-                        → Interaction
-                        → Phase
-                        → IO ()
-awaitSomethingToWriteOn ctx itr phase
-    = join $
-      atomically $
-      do state ← readTVar $ itrState itr
-         if state ≡ GettingBody then
-             writeContinueIfNeeded ctx itr phase
-         else
-             if state ≥ DecidingBody then
-                 writeHeaderOrBodyIfNeeded ctx itr phase
-             else
-                 retry
+                              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
-                      → Phase
-                      → STM (IO ())
-writeContinueIfNeeded ctx itr@(Interaction {..}) phase
-    | phase ≡ Initial ∧ itrExpectedContinue ≡ Just True
-        = do reqBodyWanted ← readTVar itrReqBodyWanted
-             if reqBodyWanted > 0 then
-                 return $ writeContinue ctx itr
-             else
-                 retry
-    | otherwise
-        = retry
-
--- DecidingBody 以降の状態にあり、まだヘッダを出力する前であれば、ヘッ
--- ダを出力する。ヘッダ出力後であり、bodyToSend が空でなければ、それを
--- 出力する。空である時は、もし状態がDone であれば後処理をする。
-writeHeaderOrBodyIfNeeded ∷ HandleLike h
-                          ⇒ Context h
-                          → Interaction
-                          → Phase
-                          → STM (IO ())
-writeHeaderOrBodyIfNeeded ctx itr@(Interaction {..}) phase
-    | phase < WroteHeader
-        = return $ writeHeader ctx itr
-    | otherwise
-        = do noBodyToWrite ← isEmptyTMVar itrBodyToSend
-             if noBodyToWrite then
-                 do state ← readTVar itrState
-                    if state ≡ Done then
-                        return $ finalize ctx itr
-                    else
-                        retry
-             else
-                 return $ writeBodyChunk ctx itr phase
-
-writeContinue ∷ HandleLike h ⇒ Context h → Interaction → IO ()
-writeContinue ctx@(Context {..}) itr@(Interaction {..})
-    = do let cont = Response {
-                      resVersion = HttpVersion 1 1
-                    , resStatus  = Continue
-                    , resHeaders = (∅)
-                    }
-         cont' ← completeUnconditionalHeaders cConfig cont
-         hPutBuilder cHandle $ A.toBuilder $ printResponse cont'
-         hFlush cHandle
-         awaitSomethingToWriteOn ctx itr WroteContinue
+                      → NormalInteraction
+                      → IO ()
+writeContinueIfNeeded ctx@(Context {..}) ni@(NI {..})
+    = do isNeeded ← atomically $ readTMVar niSendContinue
+         when isNeeded
+             $ do let cont = Response {
+                               resVersion = HttpVersion 1 1
+                             , resStatus  = fromStatusCode Continue
+                             , resHeaders = (∅)
+                             }
+                  hPutBuilder cHandle $ convertSuccessVia ((⊥) ∷ AsciiBuilder) cont
+                  hFlush cHandle
+         writeHeader ctx ni
 
 writeHeader ∷ HandleLike h
             ⇒ Context h
-            → Interaction
+            → NormalInteraction
             → IO ()
-writeHeader ctx@(Context {..}) itr@(Interaction {..})
-    = do res ← atomically $ readTVar itrResponse
-         hPutBuilder cHandle $ A.toBuilder $ printResponse res
+writeHeader ctx@(Context {..}) ni@(NI {..})
+    = do res ← atomically $
+               do state ← readTVar niState
+                  if state ≥ SendingBody then
+                      readTVar niResponse
+                  else
+                      retry -- Too early to write header fields.
+         hPutBuilder cHandle $ convertSuccessVia ((⊥) ∷ AsciiBuilder) res
          hFlush cHandle
-         awaitSomethingToWriteOn ctx itr WroteHeader
-
-writeBodyChunk ∷ HandleLike h
-               ⇒ Context h
-               → Interaction
-               → Phase
-               → IO ()
-writeBodyChunk ctx@(Context {..}) itr@(Interaction {..}) phase
+         writeBodyIfNeeded ctx ni
+
+writeBodyIfNeeded ∷ HandleLike h
+                  ⇒ Context h
+                  → NormalInteraction
+                  → IO ()
+writeBodyIfNeeded ctx ni@(NI {..})
     = join $
       atomically $
-      do willDiscardBody ← readTVar itrWillDiscardBody
-         if willDiscardBody then
-             do _ ← tryTakeTMVar itrBodyToSend
-                return $ awaitSomethingToWriteOn ctx itr phase
+      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
-                chunk         ← takeTMVar itrBodyToSend
-                return $
-                    do if willChunkBody then
-                           hPutBuilder cHandle $ BB.chunkedTransferEncoding chunk
-                       else
-                           hPutBuilder cHandle chunk
-                       hFlush cHandle
-                       awaitSomethingToWriteOn ctx itr phase
-
-finishBodyChunk ∷ HandleLike h ⇒ Context h → Interaction → IO ()
-finishBodyChunk (Context {..}) (Interaction {..})
+             return $ discardBody ctx ni
+
+discardBody ∷ HandleLike h
+            ⇒ Context h
+            → NormalInteraction
+            → IO ()
+discardBody ctx ni@(NI {..})
     = join $
       atomically $
-      do willDiscardBody ← readTVar itrWillDiscardBody
-         willChunkBody   ← readTVar itrWillChunkBody
-         if ((¬) willDiscardBody) ∧ willChunkBody then
-             return $
-                 do hPutBuilder cHandle BB.chunkedTransferTerminator
-                    hFlush cHandle
-         else
-             return $ return ()
-
-finalize ∷ HandleLike h ⇒ Context h → Interaction → IO ()
-finalize ctx@(Context {..}) itr@(Interaction {..})
-    = do finishBodyChunk ctx itr
-         willClose ← atomically $
-                     do queue ← readTVar cQueue
-                        case S.viewr queue of
-                          EmptyR         → return () -- this should never happen
-                          remaining :> _ → writeTVar cQueue remaining
-                        readTVar itrWillClose
-         if willClose then
-             -- reader は恐らく hWaitForInput してゐる最中なので、スレッ
-             -- ドを豫め殺して置かないとをかしくなる。
-             do killThread cReader
-                hClose cHandle
+      do chunk ← tryTakeTMVar niBodyToSend
+         case chunk of
+           Just _  → return $ discardBody ctx ni
+           Nothing → do state ← readTVar niState
+                        if state ≡ Done then
+                            return $ finalize ctx ni
+                        else
+                            retry
+
+writeChunkedBody ∷ HandleLike h
+                 ⇒ Context h
+                 → NormalInteraction
+                 → IO ()
+writeChunkedBody ctx@(Context {..}) ni@(NI {..})
+    = join $
+      atomically $
+      do chunk ← tryTakeTMVar niBodyToSend
+         case chunk of
+           Just b  → return $
+                     do hPutBuilder cHandle $ BB.chunkedTransferEncoding b
+                        hFlush cHandle
+                        writeChunkedBody ctx ni
+           Nothing → do state ← readTVar niState
+                        if state ≡ Done then
+                            return $
+                            do hPutBuilder cHandle BB.chunkedTransferTerminator
+                               hFlush cHandle
+                               finalize ctx ni
+                        else
+                            retry
+
+writeNonChunkedBody ∷ HandleLike h
+                    ⇒ Context h
+                    → NormalInteraction
+                    → IO ()
+writeNonChunkedBody ctx@(Context {..}) ni@(NI {..})
+    = join $
+      atomically $
+      do chunk ← tryTakeTMVar niBodyToSend
+         case chunk of
+           Just b  → return $
+                     do hPutBuilder cHandle b
+                        hFlush cHandle
+                        writeNonChunkedBody ctx ni
+           Nothing → do state ← readTVar niState
+                        if state ≡ Done then
+                            return $ finalize ctx ni
+                        else
+                            retry
+
+finalize ∷ HandleLike h ⇒ Context h → NormalInteraction → IO ()
+finalize ctx@(Context {..}) (NI {..})
+    = join $
+      atomically $
+      do willClose    ← readTVar  niWillClose
+         sentContinue ← takeTMVar niSendContinue
+         return $
+             if needToClose willClose sentContinue then
+                 return ()
+             else
+                 awaitSomethingToWrite ctx
+    where
+      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.
+          | sentContinue = False
+          -- We've got "Expect: 100-continue" but have sent a final
+          -- response without sending "HTTP/1.1 100
+          -- Continue". According to the RFC 2616 (HTTP/1.1), it is
+          -- undecidable whether the client will send us its
+          -- (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.
+          | 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 ()