{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE CPP #-}
module Network.Mail.Mime.SES.Internal where
import Crypto.Hash (SHA256, hmac, hmacGetDigest, hash)
import Data.Bifunctor (bimap)
import Data.Byteable (toBytes)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.ByteString.Base16 as Base16
import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Lazy as L
import Data.Char (toLower)
import Data.CaseInsensitive (CI)
import qualified Data.CaseInsensitive as CI
import Data.List (sort)
#if MIN_VERSION_base(4, 11, 0)
#else
import Data.Monoid ((<>))
#endif
import Data.Time (UTCTime)
import Data.Time.Format (formatTime)
import Network.HTTP.Client (Request, RequestBody(RequestBodyLBS, RequestBodyBS),
#if MIN_VERSION_http_client(0, 5, 0)
parseRequest,
#else
checkStatus,
parseUrl,
#endif
method, host, path, requestHeaders, queryString, requestBody
)
#if MIN_VERSION_time(1,5,0)
import Data.Time (defaultTimeLocale)
#else
import System.Locale (defaultTimeLocale)
#endif
makeCanonicalRequest :: ByteString -> ByteString -> ByteString -> [(CI ByteString, ByteString)] -> ByteString -> ByteString
makeCanonicalRequest requesMethod requestPath requestQueryString headers payload = S8.intercalate "\n"
[ requesMethod
, requestPath
, requestQueryString
, S8.concat . fmap (\ (name, value) -> name <> ":" <> value <> "\n")
. sort . fmap (bimap (bytesToLowerCase . CI.original) id)
$ headers
, makeListOfHeaders $ headers
, unaryHashBase16 $ payload
]
canonicalizeRequest :: Request -> ByteString
canonicalizeRequest request
= makeCanonicalRequest
(method request)
(path request)
(queryString request)
(patchedRequestHeaders request)
(requestBodyAsByteString request)
makeStringToSign :: ByteString -> UTCTime -> ByteString -> ByteString -> ByteString
makeStringToSign service time region canonicalRequest = S8.intercalate "\n"
[ "AWS4-HMAC-SHA256"
, formatAmazonTime time
, makeCredentialScope service time region
, unaryHashBase16 canonicalRequest
]
makeSig :: ByteString -> UTCTime -> ByteString -> ByteString -> ByteString -> ByteString
makeSig service time region secret stringToSign =
let f = flip keyedHash
in Base16.encode
. f stringToSign
. f "aws4_request"
. f service
. f region
. f (formatAmazonDate time)
$ ("AWS4" <> secret)
makeAuthorizationString :: ByteString -> UTCTime -> ByteString -> [(CI ByteString, ByteString)] -> ByteString -> ByteString -> ByteString
makeAuthorizationString service time region headers keyId sig = S8.concat
[ "AWS4-HMAC-SHA256 Credential="
<> keyId
<> "/"
<> makeCredentialScope service time region
, ", SignedHeaders=" <> makeListOfHeaders headers
, ", Signature=" <> sig
]
formatAmazonTime :: UTCTime -> ByteString
formatAmazonTime = S8.pack . formatTime defaultTimeLocale "%Y%m%dT%H%M%SZ"
formatAmazonDate :: UTCTime -> ByteString
formatAmazonDate = S8.pack . formatTime defaultTimeLocale "%Y%m%d"
buildRequest :: String -> IO Request
buildRequest url = do
#if MIN_VERSION_http_client(0, 5, 0)
requestBase <- (parseRequest url)
#else
requestBase <- parseUrl url {checkStatus = \_ _ _ -> Nothing}
#endif
return requestBase
requestBodyAsByteString :: Request -> ByteString
requestBodyAsByteString request = case requestBody request of
RequestBodyBS x -> x
RequestBodyLBS x -> L.toStrict x
_ -> error "Not implemented."
requestBodyLength :: Request -> Int
requestBodyLength = B.length . requestBodyAsByteString
makeListOfHeaders :: [(CI ByteString, ByteString)] -> ByteString
makeListOfHeaders = S8.intercalate ";" . sort . fmap (bytesToLowerCase . CI.original . fst)
patchedRequestHeaders :: Request -> [(CI ByteString, ByteString)]
patchedRequestHeaders request = requestHeaders request ++
[ (CI.mk "Host", host request)
, (CI.mk "Content-Length", S8.pack . show $ requestBodyLength request)
]
makeCredentialScope :: ByteString -> UTCTime -> ByteString -> ByteString
makeCredentialScope service time region = S8.intercalate "/" [formatAmazonDate time, region, service, "aws4_request"]
bytesToLowerCase :: ByteString -> ByteString
bytesToLowerCase = S8.pack . fmap toLower . S8.unpack
unaryHashBase16 :: ByteString -> ByteString
unaryHashBase16 = Base16.encode . toBytes . hash @SHA256
keyedHash :: ByteString -> ByteString -> ByteString
keyedHash key payload = toBytes . hmacGetDigest $ hmac @SHA256 key payload