-- | -- Module : Amazonka.Sign.V2Header -- Stability : provisional -- Portability : non-portable (GHC extensions) -- -- This module provides an AWS compliant V2 Header request signer. It is based -- heavily on , specifically boto's -- @HmacAuthV1Handler@ AWS capable signer. AWS documentation is available -- . -- -- Notice: Limitations include an inability to sign with a security token and -- inability to overwrite the @Date@ header with an expiry. module Amazonka.Sign.V2Header ( v2Header, newSigner, toSignerQueryBS, constructSigningHeader, constructSigningQuery, constructFullPath, unionNecessaryHeaders, ) where import qualified Amazonka.Bytes as Bytes import qualified Amazonka.Crypto as Crypto import Amazonka.Data import qualified Amazonka.Data.Query as Query import Amazonka.Prelude import Amazonka.Types hiding (presign, sign) 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 qualified Data.Function as Function import qualified Data.List as List import qualified Network.HTTP.Client as Client import qualified Network.HTTP.Types as HTTP import qualified Network.HTTP.Types.URI as URI data V2Header = V2Header { metaTime :: UTCTime, metaEndpoint :: Endpoint, metaSignature :: ByteString, headers :: HTTP.RequestHeaders, signer :: ByteString } instance ToLog V2Header where build V2Header {..} = buildLines [ "[Version 2 Header Metadata] {", " time = " <> build metaTime, " endpoint = " <> build (host metaEndpoint), " signature = " <> build metaSignature, " headers = " <> build headers, " signer = " <> build signer, "}" ] v2Header :: Signer v2Header = Signer sign (const sign) sign :: Algorithm a sign Request {..} AuthEnv {..} r t = Signed meta rq where meta = Meta (V2Header t end signature headers signer) signer = newSigner headers' meth path' query rq = (newClientRequest end timeout) { Client.method = meth, Client.path = path', Client.queryString = toBS query, Client.requestHeaders = headers', Client.requestBody = toRequestBody body } meth = toBS method path' = toBS (escapePath $ basePath <> path) end@Endpoint {basePath} = endpoint r Service {timeout, endpoint} = service signature = Bytes.encodeBase64 . Crypto.hmacSHA1 (toBS secretAccessKey) $ signer headers' = headers & hdr HTTP.hAuthorization ("AWS " <> toBS accessKeyId <> ":" <> signature) & hdr HTTP.hDate time time = toBS (Time t :: RFC822) -- | 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 (URI.urlEncode True -> k) x | Just n <- p -> enc (Just (n <> kdelim <> k)) x -- .key | otherwise -> enc (Just k) x -- key Query.QValue (Just (URI.urlEncode True -> v)) | Just n <- p -> [n <> vsep <> v] -- key=value | otherwise -> [v] _ | Just n <- p -> [n] | otherwise -> [] cat :: [ByteString] -> ByteStringBuilder 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 (Function.on (==) fst)) [ (HTTP.hContentMD5, ""), (HTTP.hContentType, "") ]