bugsnag-wai-1.0.0.2: src/Network/Bugsnag/Wai.hs
module Network.Bugsnag.Wai
( bugsnagOnException
, bugsnagOnExceptionWith
, updateEventFromWaiRequest
, updateEventFromWaiRequestUnredacted
, bugsnagRequestFromWaiRequest
, bugsnagDeviceFromWaiRequest
-- * Exported for testing
, redactRequestHeaders
, readForwardedFor
) where
import Prelude
import Control.Applicative ((<|>))
import Control.Arrow ((***))
import Control.Concurrent (forkIO)
import Control.Exception (SomeException)
import Control.Monad (void, when)
import Data.Bugsnag
import Data.Bugsnag.Settings
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as C8
import qualified Data.CaseInsensitive as CI
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.IP
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding (decodeUtf8)
import qualified Data.Text.Encoding as TE
import Network.Bugsnag
import Network.Bugsnag.Device
import Network.HTTP.Types
import Network.Socket
import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp
bugsnagOnException :: Settings -> Maybe Wai.Request -> SomeException -> IO ()
bugsnagOnException =
bugsnagOnExceptionWith (maybe mempty updateEventFromWaiRequest)
bugsnagOnExceptionWith
:: (Maybe Wai.Request -> BeforeNotify)
-> Settings
-> Maybe Wai.Request
-> SomeException
-> IO ()
bugsnagOnExceptionWith mkBeforeNotify settings mRequest ex =
when (Warp.defaultShouldDisplayException ex) $ do
void $ forkIO $ notifyBugsnagWith (mkBeforeNotify mRequest) settings ex
-- | Constructs a 'Request' from a 'Wai.Request'
bugsnagRequestFromWaiRequest :: Wai.Request -> Request
bugsnagRequestFromWaiRequest request =
defaultRequest
{ request_clientIp = decodeUtf8 <$> clientIp
, request_headers = Just $ fromRequestHeaders $ Wai.requestHeaders request
, request_httpMethod = Just $ decodeUtf8 $ Wai.requestMethod request
, request_url = Just $ decodeUtf8 $ requestUrl request
, request_referer = decodeUtf8 <$> Wai.requestHeaderReferer request
}
where
clientIp =
requestRealIp request <|> Just (sockAddrToIp $ Wai.remoteHost request)
fromRequestHeaders :: [(HeaderName, ByteString)] -> HashMap Text Text
fromRequestHeaders =
HashMap.fromList . map (decodeUtf8 . CI.original *** decodeUtf8)
requestRealIp :: Wai.Request -> Maybe ByteString
requestRealIp request =
requestForwardedFor request
<|> lookup "X-Real-IP" (Wai.requestHeaders request)
requestForwardedFor :: Wai.Request -> Maybe ByteString
requestForwardedFor request =
readForwardedFor =<< lookup "X-Forwarded-For" (Wai.requestHeaders request)
readForwardedFor :: ByteString -> Maybe ByteString
readForwardedFor bs
| C8.null bs = Nothing
| otherwise = Just $ fst $ C8.break (== ',') bs
requestUrl :: Wai.Request -> ByteString
requestUrl request =
requestProtocol
<> "://"
<> requestHost request
<> prependIfNecessary "/" (Wai.rawPathInfo request)
<> Wai.rawQueryString request
where
clientProtocol :: ByteString
clientProtocol = if Wai.isSecure request then "https" else "http"
requestHost :: Wai.Request -> ByteString
requestHost = fromMaybe "<unknown>" . Wai.requestHeaderHost
requestProtocol :: ByteString
requestProtocol =
fromMaybe clientProtocol $
lookup "X-Forwarded-Proto" $
Wai.requestHeaders request
prependIfNecessary c x
| c `C8.isPrefixOf` x = x
| otherwise = c <> x
sockAddrToIp :: SockAddr -> ByteString
sockAddrToIp (SockAddrInet _ h) = C8.pack $ show $ fromHostAddress h
sockAddrToIp (SockAddrInet6 _ _ h _) = C8.pack $ show $ fromHostAddress6 h
sockAddrToIp _ = "<socket>"
-- | /Attempt/ to divine a 'Device' from a request's User Agent
bugsnagDeviceFromWaiRequest :: Wai.Request -> Maybe Device
bugsnagDeviceFromWaiRequest request = do
userAgent <- lookup "User-Agent" $ Wai.requestHeaders request
pure $ bugsnagDeviceFromUserAgent userAgent
-- | Set the events 'Event' and 'Device'
--
-- This function redacts the following Request headers:
--
-- - Authorization
-- - Cookie
-- - X-XSRF-TOKEN (CSRF token header used by Yesod)
--
-- To avoid this, use 'updateEventFromWaiRequestUnredacted'.
updateEventFromWaiRequest :: Wai.Request -> BeforeNotify
updateEventFromWaiRequest wrequest =
redactRequestHeaders ["Authorization", "Cookie", "X-XSRF-TOKEN"]
<> updateEventFromWaiRequestUnredacted wrequest
updateEventFromWaiRequestUnredacted :: Wai.Request -> BeforeNotify
updateEventFromWaiRequestUnredacted wrequest =
maybe mempty setDevice mdevice <> setRequest request <> setContext context
where
mdevice = bugsnagDeviceFromWaiRequest wrequest
request = bugsnagRequestFromWaiRequest wrequest
context = "/" <> T.intercalate "/" (Wai.pathInfo wrequest)
-- | Redact the given request headers
--
-- Headers like @Authorization@ may contain information you don't want to report
-- to Bugsnag.
--
-- > redactRequestHeaders ["Authorization", "Cookie"]
redactRequestHeaders :: [HeaderName] -> BeforeNotify
redactRequestHeaders headers = updateEvent $ \event ->
event {event_request = redactHeaders headers <$> event_request event}
redactHeaders :: [HeaderName] -> Request -> Request
redactHeaders headers request =
request
{ request_headers =
redactBugsnagRequestHeaders headers
<$> request_headers request
}
redactBugsnagRequestHeaders
:: [HeaderName] -> HashMap Text Text -> HashMap Text Text
redactBugsnagRequestHeaders redactList = HashMap.mapWithKey go
where
go :: Text -> Text -> Text
go k _ | any (`matchesHeaderName` k) redactList = "<redacted>"
go _ v = v
matchesHeaderName :: HeaderName -> Text -> Bool
matchesHeaderName h = (h ==) . CI.mk . TE.encodeUtf8