{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
module Network.AWS.Sign.V2Header.Base
( newSigner
, toSignerQueryBS
, constructSigningHeader
, constructSigningQuery
, constructFullPath
, unionNecessaryHeaders
) where
import Data.ByteString (ByteString)
import Data.ByteString.Builder (Builder)
import qualified Data.ByteString.Builder as Build
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as LBS
import qualified Data.CaseInsensitive as CI
import Data.Function (on)
import qualified Data.List as List
import Data.Monoid (mempty, (<>))
import qualified Network.AWS.Data.Query as Query
import qualified Network.HTTP.Types as HTTP
import Network.HTTP.Types.URI (urlEncode)
newSigner :: HTTP.RequestHeaders
-> ByteString
-> ByteString
-> Query.QueryString
-> ByteString
newSigner headers method path query = signer
where
signer =
BS8.intercalate "\n"
( method
: map constructSigningHeader (List.sort filteredHeaders)
++ [constructFullPath path (toSignerQueryBS filteredQuery)]
)
filteredHeaders = unionNecessaryHeaders (filter isInterestingHeader headers)
filteredQuery = constructSigningQuery query
toSignerQueryBS :: Query.QueryString -> ByteString
toSignerQueryBS =
LBS.toStrict . Build.toLazyByteString . cat . List.sort . enc Nothing
where
enc :: Maybe ByteString -> Query.QueryString -> [ByteString]
enc p = \case
Query.QList xs -> concatMap (enc p) xs
Query.QPair (urlEncode True -> k) x
| Just n <- p -> enc (Just (n <> kdelim <> k)) x
| otherwise -> enc (Just k) x
Query.QValue (Just (urlEncode True -> v))
| Just n <- p -> [n <> vsep <> v]
| otherwise -> [v]
_ | Just n <- p -> [n]
| otherwise -> []
cat :: [ByteString] -> Builder
cat [] = mempty
cat [x] = Build.byteString x
cat (x:xs) = Build.byteString x <> ksep <> cat xs
kdelim = "."
ksep = "&"
vsep = "="
hasAWSPrefix :: CI.CI ByteString -> Bool
hasAWSPrefix = BS8.isPrefixOf "aws-" . CI.foldedCase
isInterestingQueryKey :: ByteString -> Bool
isInterestingQueryKey = \case
"acl" -> True
"cors" -> True
"defaultObjectAcl" -> True
"location" -> True
"logging" -> True
"partNumber" -> True
"policy" -> True
"requestPayment" -> True
"torrent" -> True
"versioning" -> True
"versionId" -> True
"versions" -> True
"website" -> True
"uploads" -> True
"uploadId" -> True
"response-content-type" -> True
"response-content-language" -> True
"response-expires" -> True
"response-cache-control" -> True
"response-content-disposition" -> True
"response-content-encoding" -> True
"delete" -> True
"lifecycle" -> True
"tagging" -> True
"restore" -> True
"storageClass" -> True
"websiteConfig" -> True
"compose" -> True
_ -> False
isInterestingHeader :: HTTP.Header -> Bool
isInterestingHeader (name, _)
| name == HTTP.hDate = True
| name == HTTP.hContentMD5 = True
| name == HTTP.hContentType = True
| hasAWSPrefix name = True
| otherwise = False
constructSigningQuery :: Query.QueryString -> Query.QueryString
constructSigningQuery = \case
Query.QValue {} -> Query.QValue Nothing
Query.QList qs -> Query.QList (map constructSigningQuery qs)
Query.QPair k v
| isInterestingQueryKey k -> Query.QPair k v
| otherwise -> Query.QValue Nothing
constructSigningHeader :: HTTP.Header -> ByteString
constructSigningHeader (name, value)
| hasAWSPrefix name = CI.foldedCase name <> ":" <> value
| otherwise = value
constructFullPath :: ByteString -> ByteString -> ByteString
constructFullPath path q
| BS8.null q = path
| otherwise = path <> "?" <> q
unionNecessaryHeaders :: [HTTP.Header] -> [HTTP.Header]
unionNecessaryHeaders =
flip (List.unionBy (on (==) fst))
[ (HTTP.hContentMD5, "")
, (HTTP.hContentType, "")
]