packages feed

bugsnag-haskell-0.0.4.4: src/Network/Bugsnag/Request.hs

{-# OPTIONS_GHC -fno-warn-overlapping-patterns #-}

module Network.Bugsnag.Request
    ( BugsnagRequest(..)
    , bugsnagRequest
    , bugsnagRequestFromWaiRequest
    ) where

import Prelude

import Control.Applicative ((<|>))
import Data.Aeson
import Data.Aeson.Ext
import Data.Aeson.Types
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as C8
import Data.IP
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8)
import GHC.Generics
import Network.Bugsnag.BugsnagRequestHeaders
import Network.HTTP.Types
import Network.Socket
import Network.Wai

-- | The web request being handled when the error was encountered
data BugsnagRequest = BugsnagRequest
    { brClientIp :: Maybe ByteString
    , brHeaders :: Maybe BugsnagRequestHeaders
    , brHttpMethod :: Maybe Method
    , brUrl :: Maybe ByteString
    , brReferer :: Maybe ByteString
    }
    deriving stock Generic

instance ToJSON BugsnagRequest where
    toJSON BugsnagRequest {..} = object
        (("clientIp" .=? (decodeUtf8 <$> brClientIp))
        <> ("headers" .=? brHeaders)
        <> ("httpMethod" .=? (decodeUtf8 <$> brHttpMethod))
        <> ("url" .=? (decodeUtf8 <$> brUrl))
        <> ("referer" .=? (decodeUtf8 <$> brReferer))
        )
      where
        -- For implementing "omit Nothing fields"
        (.=?) :: ToJSON v => Text -> Maybe v -> [Pair]
        (.=?) k = maybe [] (pure . (fromText k .=))
    toEncoding BugsnagRequest {..} = pairs
        (("clientIp" .=? (decodeUtf8 <$> brClientIp))
        <> ("headers" .=? brHeaders)
        <> ("httpMethod" .=? (decodeUtf8 <$> brHttpMethod))
        <> ("url" .=? (decodeUtf8 <$> brUrl))
        <> ("referer" .=? (decodeUtf8 <$> brReferer))
        )
      where
        -- For implementing "omit Nothing fields"
        (.=?) :: ToJSON v => Text -> Maybe v -> Series
        k .=? mv = maybe mempty (\v -> fromText k .= v) mv

-- | Constructs an empty @'BugsnagRequest'@
bugsnagRequest :: BugsnagRequest
bugsnagRequest = BugsnagRequest
    { brClientIp = Nothing
    , brHeaders = Nothing
    , brHttpMethod = Nothing
    , brUrl = Nothing
    , brReferer = Nothing
    }

-- | Constructs a @'BugsnagRequest'@ from a WAI @'Request'@
bugsnagRequestFromWaiRequest :: Request -> BugsnagRequest
bugsnagRequestFromWaiRequest request = bugsnagRequest
    { brClientIp = requestRealIp request
        <|> Just (sockAddrToIp $ remoteHost request)
    , brHeaders = Just $ bugsnagRequestHeaders $ requestHeaders request
    , brHttpMethod = Just $ requestMethod request
    , brUrl = Just $ requestUrl request
    , brReferer = requestHeaderReferer request
    }

requestRealIp :: Request -> Maybe ByteString
requestRealIp request =
    requestForwardedFor request <|> lookup "X-Real-IP" (requestHeaders request)

requestForwardedFor :: Request -> Maybe ByteString
requestForwardedFor request =
    readForwardedFor =<< lookup "X-Forwarded-For" (requestHeaders request)

-- |
--
-- >>> readForwardedFor ""
-- Nothing
--
-- >>> readForwardedFor "123.123.123"
-- Just "123.123.123"
--
-- >>> readForwardedFor "123.123.123, 45.45.45"
-- Just "123.123.123"
--
readForwardedFor :: ByteString -> Maybe ByteString
readForwardedFor bs
    | C8.null bs = Nothing
    | otherwise = Just $ fst $ C8.break (== ',') bs

requestUrl :: Request -> ByteString
requestUrl request =
    requestProtocol
        <> "://"
        <> requestHost request
        <> prependIfNecessary "/" (rawPathInfo request)
        <> rawQueryString request
  where
    clientProtocol :: ByteString
    clientProtocol = if isSecure request then "https" else "http"

    requestHost :: Request -> ByteString
    requestHost = fromMaybe "<unknown>" . requestHeaderHost

    requestProtocol :: ByteString
    requestProtocol =
        fromMaybe clientProtocol $ lookup "X-Forwarded-Proto" $ 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 (SockAddrUnix _) = "<socket>"

-- Matches deprecated and eventually removed SockAddrCan on older GHCs.
-- overlapping-patterns warning is disabled for this.
sockAddrToIp _ = "<invalid>"