-- | Infer the remote IP address using headers
module Network.Wai.Middleware.RealIp (
    realIp,
    realIpHeader,
    realIpTrusted,
    defaultTrusted,
    ipInRange,
) where

import qualified Data.ByteString.Char8 as B8 (split, unpack)
import qualified Data.IP as IP
import Data.Maybe (fromMaybe, listToMaybe, mapMaybe)
import Network.HTTP.Types (HeaderName, RequestHeaders)
import Network.Wai (Middleware, remoteHost, requestHeaders)
import Text.Read (readMaybe)

-- | Infer the remote IP address from the @X-Forwarded-For@ header,
-- trusting requests from any private IP address. See 'realIpHeader' and
-- 'realIpTrusted' for more information and options.
--
-- @since 3.1.5
realIp :: Middleware
realIp = realIpHeader "X-Forwarded-For"

-- | Infer the remote IP address using the given header, trusting
-- requests from any private IP address. See 'realIpTrusted' for more
-- information and options.
--
-- @since 3.1.5
realIpHeader :: HeaderName -> Middleware
realIpHeader header =
    realIpTrusted header $ \ip -> any (ipInRange ip) defaultTrusted

-- | Infer the remote IP address using the given header, but only if the
-- request came from an IP that is trusted by the provided predicate.
--
-- The last non-trusted address is used to replace the 'remoteHost' in
-- the 'Request', unless all present IP addresses are trusted, in which
-- case the first address is used. Invalid IP addresses are ignored, and
-- the remoteHost value remains unaltered if no valid IP addresses are
-- found.
--
-- Examples:
--
-- @ realIpTrusted "X-Forwarded-For" $ flip ipInRange "10.0.0.0/8" @
--
-- @ realIpTrusted "X-Real-Ip" $ \\ip -> any (ipInRange ip) defaultTrusted @
--
-- @since 3.1.5
realIpTrusted :: HeaderName -> (IP.IP -> Bool) -> Middleware
realIpTrusted header isTrusted app req = app req'
  where
    req' = fromMaybe req $ do
        (ip, port) <- IP.fromSockAddr (remoteHost req)
        ip' <-
            if isTrusted ip
                then findRealIp (requestHeaders req) header isTrusted
                else Nothing
        Just $ req{remoteHost = IP.toSockAddr (ip', port)}

-- | Standard private IP ranges.
--
-- @since 3.1.5
defaultTrusted :: [IP.IPRange]
defaultTrusted =
    [ "127.0.0.0/8"
    , "10.0.0.0/8"
    , "172.16.0.0/12"
    , "192.168.0.0/16"
    , "::1/128"
    , "fc00::/7"
    ]

-- | Check if the given IP address is in the given range.
--
-- IPv4 addresses can be checked against IPv6 ranges, but testing an
-- IPv6 address against an IPv4 range is always 'False'.
--
-- @since 3.1.5
ipInRange :: IP.IP -> IP.IPRange -> Bool
ipInRange (IP.IPv4 ip) (IP.IPv4Range r) = ip `IP.isMatchedTo` r
ipInRange (IP.IPv6 ip) (IP.IPv6Range r) = ip `IP.isMatchedTo` r
ipInRange (IP.IPv4 ip) (IP.IPv6Range r) = IP.ipv4ToIPv6 ip `IP.isMatchedTo` r
ipInRange _ _ = False

findRealIp :: RequestHeaders -> HeaderName -> (IP.IP -> Bool) -> Maybe IP.IP
findRealIp reqHeaders header isTrusted =
    case (nonTrusted, ips) of
        ([], xs) -> listToMaybe xs
        (xs, _) -> listToMaybe $ reverse xs
  where
    -- account for repeated headers
    headerVals = [v | (k, v) <- reqHeaders, k == header]
    ips = concatMap (mapMaybe (readMaybe . B8.unpack) . B8.split ',') headerVals
    nonTrusted = filter (not . isTrusted) ips
