amazonka-core-2.0: src/Amazonka/Sign/V4/Chunked.hs
-- |
-- Module : Amazonka.Sign.V4.Chunked
-- Copyright : (c) 2013-2023 Brendan Hay
-- License : Mozilla Public License, v. 2.0.
-- Maintainer : Brendan Hay <brendan.g.hay+amazonka@gmail.com>
-- Stability : provisional
-- Portability : non-portable (GHC extensions)
module Amazonka.Sign.V4.Chunked where
import qualified Amazonka.Bytes as Bytes
import Amazonka.Core.Lens.Internal ((^.))
import qualified Amazonka.Crypto as Crypto
import Amazonka.Data
import Amazonka.Prelude as Prelude
import Amazonka.Sign.V4.Base as V4 hiding (algorithm)
import Amazonka.Types
import qualified Data.ByteString as BS
import qualified Data.ByteString.Builder as Build
import qualified Data.ByteString.Char8 as BS8
import Data.Conduit (ConduitM)
import qualified Data.Conduit as Conduit
import qualified Network.HTTP.Types as HTTP
import qualified Numeric
chunked :: ChunkedBody -> Algorithm a
chunked
c@ChunkedBody {length = len}
rq@Request {service = service@Service {endpoint}}
a
region
ts =
signRequest meta (toRequestBody body) auth
where
(meta, auth) = V4.base (Tag digest) (prepare rq) a region ts
-- Although https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html says to include
-- `Content-Encoding: aws-chunked`, we don't. If it's the only header, S3 will remove
-- `aws-chunked` leaving a blank header, and store `"ContentEncoding": ""` in the object's metadata.
-- This breaks some CDNs and HTTP clients.
--
-- According to https://github.com/fog/fog-aws/pull/147 , AWS support have confirmed that the
-- header is not strictly necessary, and S3 will figure out that it's a chunked body.
prepare :: Request a -> Request a
prepare r@Request {headers} =
r
{ headers =
headers
<> [ (hAMZDecodedContentLength, toBS len),
(HTTP.hContentLength, toBS (metadataLength c))
]
}
body = Chunked (c `fuseChunks` sign (metaSignature meta))
sign :: Monad m => Signature -> ConduitM ByteString ByteString m ()
sign prev = do
mx <- Conduit.await
let next = chunkSignature prev (fromMaybe mempty mx)
case mx of
Nothing -> Conduit.yield (chunkData next mempty)
Just x -> Conduit.yield (chunkData next x) >> sign next
chunkData next x =
toBS $
Build.word64Hex (fromIntegral (BS.length x))
<> Build.byteString chunkSignatureHeader
<> Build.byteString (toBS next)
<> Build.byteString crlf
<> Build.byteString x
<> Build.byteString crlf
chunkSignature prev x =
signature (secretAccessKey a ^. _Sensitive) scope (chunkStringToSign prev x)
chunkStringToSign prev x =
Tag $
BS8.intercalate
"\n"
[ algorithm,
time,
toBS scope,
toBS prev,
sha256Empty,
sha256 x
]
time :: ByteString
time = toBS (Time ts :: AWSTime)
scope :: CredentialScope
scope = credentialScope service end ts
end :: Endpoint
end = endpoint region
metadataLength :: ChunkedBody -> Integer
metadataLength c =
-- Number of full sized chunks.
fullChunks c * chunkLength (size c)
-- Non-full chunk preceeding the final chunk.
+ maybe 0 chunkLength (remainderBytes c)
-- The final empty chunk.
+ chunkLength (0 :: Integer)
where
chunkLength :: Integral a => a -> Integer
chunkLength (toInteger -> n) =
fromIntegral (Prelude.length (Numeric.showHex n ""))
+ headerLength
+ signatureLength
+ crlfLength
+ n
+ crlfLength
headerLength = toInteger (BS.length chunkSignatureHeader)
crlfLength = toInteger (BS.length crlf)
signatureLength = 64
sha256 :: ByteString -> ByteString
sha256 = Bytes.encodeBase16 . Crypto.hashSHA256
sha256Empty :: ByteString
sha256Empty = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
algorithm :: ByteString
algorithm = "AWS4-HMAC-SHA256-PAYLOAD"
digest :: ByteString
digest = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
chunkSignatureHeader :: ByteString
chunkSignatureHeader = ";chunk-signature="
crlf :: ByteString
crlf = "\r\n"