freckle-app-1.2.0.0: library/Freckle/App/Wai.hs
-- | Integration of "Freckle.App" tooling with "Network.Wai"
module Freckle.App.Wai
( makeRequestMetricsMiddleware
, noCacheMiddleware
, corsMiddleware
, denyFrameEmbeddingMiddleware
) where
import Freckle.App.Prelude hiding (decodeUtf8)
import Control.Monad.Reader (runReaderT)
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.CaseInsensitive as CI
import Data.Text.Encoding (decodeUtf8With)
import Data.Text.Encoding.Error (lenientDecode)
import Freckle.App.Datadog (HasDogStatsClient, HasDogStatsTags)
import qualified Freckle.App.Datadog as Datadog
import Network.HTTP.Types (ResponseHeaders)
import Network.HTTP.Types.Status (status200, statusCode)
import Network.Wai
import Network.Wai.Middleware.AddHeaders (addHeaders)
makeRequestMetricsMiddleware
:: (HasDogStatsClient env, HasDogStatsTags env)
=> env
-> (Request -> [(Text, Text)])
-> Middleware
makeRequestMetricsMiddleware env getTags app req sendResponse' = do
start <- getCurrentTime
app req $ \res -> do
flip runReaderT env $ do
Datadog.increment "requests" $ tags res
Datadog.histogramSinceMs "response_time_ms" (tags res) start
sendResponse' res
where
tags res =
getTags req
<> [ ("method", decodeUtf8 $ requestMethod req)
, ("status", pack $ show $ statusCode $ responseStatus res)
]
noCacheMiddleware :: Middleware
noCacheMiddleware = addHeaders [cacheControlHeader]
where
cacheControlHeader =
("Cache-Control", "no-cache, no-store, max-age=0, private")
corsMiddleware
:: (ByteString -> Bool)
-- ^ Predicate that returns 'True' for valid @Origin@ values
-> [ByteString]
-- ^ Extra headers to add to @Expose-Headers@
-> Middleware
corsMiddleware validateOrigin extraExposedHeaders =
handleOptions validateOrigin extraExposedHeaders
. addCORSHeaders validateOrigin extraExposedHeaders
-- | Middleware that adds header to deny all frame embedding
denyFrameEmbeddingMiddleware :: Middleware
denyFrameEmbeddingMiddleware = addHeaders [("X-Frame-Options", "DENY")]
handleOptions :: (ByteString -> Bool) -> [ByteString] -> Middleware
handleOptions validateOrigin extraExposedHeaders app req sendResponse =
case (requestMethod req, lookup "Origin" (requestHeaders req)) of
("OPTIONS", Just origin) -> sendResponse $ responseLBS
status200
(toHeaders $ corsResponseHeaders validateOrigin extraExposedHeaders origin
)
mempty
_ -> app req sendResponse
where
toHeaders :: [(ByteString, ByteString)] -> ResponseHeaders
toHeaders = map (first CI.mk)
addCORSHeaders :: (ByteString -> Bool) -> [ByteString] -> Middleware
addCORSHeaders validateOrigin extraExposedHeaders app req sendResponse =
case lookup "Origin" (requestHeaders req) of
Nothing -> app req sendResponse
Just origin -> addHeaders
(corsResponseHeaders validateOrigin extraExposedHeaders origin)
app
req
sendResponse
corsResponseHeaders
:: (ByteString -> Bool)
-> [ByteString]
-> ByteString
-> [(ByteString, ByteString)]
corsResponseHeaders validateOrigin extraExposedHeaders origin =
[ ("Access-Control-Allow-Origin", validatedOrigin)
, ("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE, PATCH")
, ("Access-Control-Allow-Credentials", "true")
, ("Access-Control-Allow-Headers", "Content-Type, *")
, ("Access-Control-Expose-Headers", BS.intercalate ", " exposedHeaders)
]
where
validatedOrigin = if validateOrigin origin then origin else "BADORIGIN"
exposedHeaders =
["Set-Cookie", "Content-Disposition", "Link"] <> extraExposedHeaders
decodeUtf8 :: ByteString -> Text
decodeUtf8 = decodeUtf8With lenientDecode