{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ViewPatterns #-} -- | -- Module : Network.AWS.Sign.V2Header.Base -- Stability : experimental -- Portability : non-portable (GHC extensions) -- -- This module provides auxiliary functions necessary for the AWS compliant V2 -- Header request signer. -- /See/: "Network.AWS.Sign.V2Header" module Network.AWS.Sign.V2Header.Base ( newSigner -- * Testing , 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) -- | Construct a full header signer following the V2 Header scheme 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 -- | The following function mostly follows the toBS in amazonka QueryString -- except for single QValue or single QPair keys not being suffixed with -- an equals. 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 -- .key | otherwise -> enc (Just k) x -- key Query.QValue (Just (urlEncode True -> v)) | Just n <- p -> [n <> vsep <> v] -- key=value | 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 -- | Filter for 'interesting' keys within a QueryString 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 -- | Filter for 'interesting' header fields isInterestingHeader :: HTTP.Header -> Bool isInterestingHeader (name, _) | name == HTTP.hDate = True | name == HTTP.hContentMD5 = True | name == HTTP.hContentType = True | hasAWSPrefix name = True | otherwise = False -- | Constructs a query string for signing 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 -- | Construct a header string for signing 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, "") ]