module OpenID.Connect.Client.HTTP
( HTTPS
, uriToText
, forceHTTPS
, requestFromURI
, addRequestHeader
, jsonPostRequest
, cacheUntil
, parseResponse
) where
import Control.Applicative
import Data.Aeson (ToJSON, FromJSON, eitherDecode)
import qualified Data.Aeson as Aeson
import Data.Bifunctor (bimap)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as Char8
import qualified Data.ByteString.Lazy as LByteString
import qualified Data.ByteString.Lazy.Char8 as LChar8
import Data.CaseInsensitive (CI)
import Data.Char (isDigit)
import Data.Function ((&))
import Data.Functor ((<&>))
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Time.Clock (UTCTime, addUTCTime)
import Data.Time.Format (parseTimeM, defaultTimeLocale)
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Types.Header as HTTP
import qualified Network.HTTP.Types.Status as HTTP
import Network.URI (URI(..), parseURI, uriToString)
import OpenID.Connect.JSON (ErrorResponse(..))
type HTTPS m = HTTP.Request -> m (HTTP.Response LByteString.ByteString)
uriToText :: URI -> Text
uriToText uri = Text.pack (uriToString id uri [])
forceHTTPS :: URI -> URI
forceHTTPS uri = uri { uriScheme = "https:" }
requestFromURI :: Either Text URI -> Maybe HTTP.Request
requestFromURI (Left t) = parseURI (Text.unpack t) >>= requestFromURI . Right
requestFromURI (Right uri) =
HTTP.requestFromURI (forceHTTPS uri)
<&> addRequestHeader ("Accept", "application/json")
jsonPostRequest :: ToJSON a => a -> HTTP.Request -> HTTP.Request
jsonPostRequest json req = addRequestHeader ("Content-Type", "application/json") $
req { HTTP.method = "POST"
, HTTP.requestBody = HTTP.RequestBodyLBS (Aeson.encode json)
}
addRequestHeader :: (CI ByteString, ByteString) -> HTTP.Request -> HTTP.Request
addRequestHeader header req =
req { HTTP.requestHeaders =
header : filter ((/= fst header) . fst) (HTTP.requestHeaders req)
}
cacheUntil :: HTTP.Response a -> Maybe UTCTime
cacheUntil res = maxAge <|> expires
where
parseTime :: ByteString -> Maybe UTCTime
parseTime = parseTimeM True defaultTimeLocale rfc1123 . Char8.unpack
rfc1123 :: String
rfc1123 = "%a, %d %b %Y %X %Z"
date :: Maybe UTCTime
date = lookup HTTP.hDate (HTTP.responseHeaders res) >>= parseTime
expires :: Maybe UTCTime
expires = lookup HTTP.hExpires (HTTP.responseHeaders res) >>= parseTime
maxAge :: Maybe UTCTime
maxAge = do
dt <- date
bs <- lookup HTTP.hCacheControl (HTTP.responseHeaders res)
ma <- nullM (snd (Char8.breakSubstring "max-age" bs))
bn <- nullM (snd (Char8.break isDigit ma))
addUTCTime . fromIntegral . fst
<$> Char8.readInt (Char8.take 6 bn)
<*> pure dt
nullM :: ByteString -> Maybe ByteString
nullM bs = if Char8.null bs then Nothing else Just bs
parseResponse
:: FromJSON a
=> HTTP.Response LByteString.ByteString
-> Either ErrorResponse (a, Maybe UTCTime)
parseResponse response =
if HTTP.statusIsSuccessful (HTTP.responseStatus response)
then eitherDecode (HTTP.responseBody response) &
bimap asError (,cacheUntil response)
else Left (asError "invalid response from server")
where
asError :: String -> ErrorResponse
asError s = case eitherDecode (HTTP.responseBody response) of
Left _ -> ErrorResponse (Text.pack s) (Just bodyError)
Right e -> e
bodyError :: Text
bodyError = response
& HTTP.responseBody
& LChar8.take 1024
& LChar8.toStrict
& Text.decodeUtf8