{-# LANGUAGE OverloadedStrings #-}
module Network.Wai.Auth.AppRoot
  ( smartAppRoot
  ) where

import           Data.ByteString          (ByteString)
import           Data.CaseInsensitive     (CI, mk)
import qualified Data.HashMap.Lazy        as HM
import           Data.Monoid              ((<>))
import qualified Data.Text                as T
import           Data.Text.Encoding       (decodeUtf8With)
import           Data.Text.Encoding.Error (lenientDecode)
import           Network.HTTP.Types       (Header)
import           Network.Wai              (Request, isSecure, requestHeaderHost,
                                           requestHeaders)


-- | Determine approot by:
--
-- * Respect the Host header and isSecure property, together with the following de facto standards: x-forwarded-protocol, x-forwarded-ssl, x-url-scheme, x-forwarded-proto, front-end-https. (Note: this list may be updated at will in the future without doc updates.)
--
-- Normally trusting headers in this way is insecure, however in the case of approot, the worst that can happen is that the client will get an incorrect URL. Note that this does not work for some situations, e.g.:
--
-- * Reverse proxies not setting one of the above mentioned headers
--
-- * Applications hosted somewhere besides the root of the domain name
--
-- * Reverse proxies that modify the host header
--
-- @since 0.1.0.0
smartAppRoot :: Request -> T.Text
smartAppRoot req =
  let secure = isSecure req || any isSecureHeader (requestHeaders req)
      host =
        maybe "localhost" (decodeUtf8With lenientDecode) (requestHeaderHost req)
  in (if secure
        then "https://"
        else "http://") <>
     host

-- |
--
-- See: http://stackoverflow.com/a/16042648/369198
httpsHeaders :: HM.HashMap (CI ByteString) (CI ByteString)
httpsHeaders =
  HM.fromList
    [ ("X-Forwarded-Protocol", "https")
    , ("X-Forwarded-Ssl", "on")
    , ("X-Url-Scheme", "https")
    , ("X-Forwarded-Proto", "https")
    , ("Front-End-Https", "on")
    ]

isSecureHeader :: Header -> Bool
isSecureHeader (key, value) =
  case HM.lookup key httpsHeaders of
    Nothing     -> False
    Just value' -> valueCI == value'
  where
    valueCI = mk value