wai-app-file-cgi-3.2.2: Network/Wai/Application/Classic/RevProxy.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
module Network.Wai.Application.Classic.RevProxy (revProxyApp) where
#if __GLASGOW_HASKELL__ < 709
import Control.Applicative
#endif
import qualified Control.Exception as E
import Control.Monad
import Control.Monad.IO.Class (liftIO)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS (uncons)
import Data.ByteString.Builder (Builder)
import qualified Data.ByteString.Char8 as BS hiding (uncons)
import Data.CaseInsensitive (mk)
import Data.Conduit
import Data.IORef
import qualified Network.HTTP.Client as H
import Network.HTTP.Types
import Network.Wai.Application.Classic.Conduit
import Network.Wai.Application.Classic.EventSource
import Network.Wai.Application.Classic.Field
import Network.Wai.Application.Classic.Path
import Network.Wai.Application.Classic.Types
import Network.Wai.Conduit
-- $setup
-- >>> :set -XOverloadedStrings
----------------------------------------------------------------
-- | Relaying any requests as reverse proxy.
revProxyApp :: ClassicAppSpec -> RevProxyAppSpec -> RevProxyRoute -> Application
revProxyApp cspec spec route req respond = do
responded <- newIORef False
H.withResponse httpClientRequest mgr (proxy responded)
`E.catch` badGateway responded
where
proxy responded hrsp = do
let status = H.responseStatus hrsp
hdr = fixHeader $ H.responseHeaders hrsp
clientBody = H.responseBody hrsp
ct = lookup hContentType hdr
src = toSource ct clientBody
writeIORef responded True
respond $ responseSource status hdr src
-- The response cannot be replaced once it has been started.
badGateway responded e = do
started <- readIORef responded
if started
then E.throwIO (e :: H.HttpException)
else respond $ responseLBS badGateway502 textPlainHeader "Bad Gateway\r\n"
httpClientRequest = reqToHReq req route
mgr = revProxyManager spec
fixHeader = addVia cspec req . filterHeaders
-- | Removing hop-by-hop headers and ones handled by http-client.
--
-- >>> filterHeaders [("Connection","close, X-Foo"),("X-Foo","1"),("Keep-Alive","5"),("X-Bar","2")]
-- [("X-Bar","2")]
filterHeaders :: [Header] -> [Header]
filterHeaders hdr = filter (\(k, _) -> k `notElem` excluded) hdr
where
excluded = connectionTokens ++ hopByHop
connectionTokens =
[ mk $ BS.dropWhile (== ' ') $ BS.dropWhileEnd (== ' ') t
| (k, v) <- hdr
, k == hConnection
, t <- BS.split ',' v
]
hopByHop =
[ hConnection
, "keep-alive"
, "proxy-connection"
, hProxyAuthenticate
, hProxyAuthorization
, hTE
, "trailer"
, hTransferEncoding
, hUpgrade
, hAcceptEncoding
, hContentLength
, hContentEncoding -- See H.decompress.
]
----------------------------------------------------------------
reqToHReq :: Request -> RevProxyRoute -> H.Request
reqToHReq req route =
H.defaultRequest
{ H.host = revProxyDomain route
, H.port = revProxyPort route
, H.secure = False -- FIXME: upstream is HTTP only
, H.requestHeaders = addForwardedFor req $ filterHeaders hdr
, H.path = path'
, H.queryString = dropQuestion query
, H.requestBody = bodyToHBody len body
, H.method = requestMethod req
, H.proxy = Nothing
, -- , H.rawBody = False
H.decompress = const True
, H.checkResponse = \_ _ -> return ()
, H.redirectCount = 0
}
where
path = rawPathInfo req
src = revProxySrc route
dst = revProxyDst route
hdr = requestHeaders req
query = rawQueryString req
len = requestBodyLength req
body = getRequestBodyChunk req
path' = dst </> (path <\> src)
dropQuestion q = case BS.uncons q of
Just (63, q') -> q' -- '?' is 63
_ -> q
bodyToHBody :: RequestBodyLength -> IO ByteString -> H.RequestBody
bodyToHBody ChunkedBody src = H.RequestBodyStreamChunked ($ src)
bodyToHBody (KnownLength len) src = H.RequestBodyStream (fromIntegral len) ($ src)
----------------------------------------------------------------
#if MIN_VERSION_conduit(1,3,0)
toSource :: Maybe ByteString -> H.BodyReader -> ConduitT () (Flush Builder) IO ()
#else
toSource :: Maybe ByteString -> H.BodyReader -> Source IO (Flush Builder)
#endif
toSource (Just "text/event-stream") = bodyToEventSource
toSource _ = bodyToSource
#if MIN_VERSION_conduit(1,3,0)
bodyToSource :: H.BodyReader -> ConduitT () (Flush Builder) IO ()
#else
bodyToSource :: H.BodyReader -> Source IO (Flush Builder)
#endif
bodyToSource br = loop
where
loop = do
bs <- liftIO $ H.brRead br
unless (BS.null bs) $ do
yield $ Chunk $ byteStringToBuilder bs
loop