Add streaming signature for PutObject (#123)

Use streaming signature to avoid reading the body twice in PutObject
requests, where the body can be upto 5GIB.

Note that the body is signed only used when the connection is not
using TLS.
This commit is contained in:
Aditya Manthramurthy 2019-05-16 20:49:38 -07:00 committed by Harshavardhana
parent 909f1c482d
commit 4a807fde56
9 changed files with 342 additions and 149 deletions

View File

@ -116,15 +116,18 @@ test-suite minio-hs-live-server-test
, TypeFamilies , TypeFamilies
other-modules: Lib.Prelude other-modules: Lib.Prelude
, Network.Minio , Network.Minio
, Network.Minio.AdminAPI
, Network.Minio.API , Network.Minio.API
, Network.Minio.API.Test
, Network.Minio.APICommon , Network.Minio.APICommon
, Network.Minio.AdminAPI
, Network.Minio.CopyObject , Network.Minio.CopyObject
, Network.Minio.Data , Network.Minio.Data
, Network.Minio.Data.ByteString , Network.Minio.Data.ByteString
, Network.Minio.Data.Crypto , Network.Minio.Data.Crypto
, Network.Minio.Data.Time , Network.Minio.Data.Time
, Network.Minio.Errors , Network.Minio.Errors
, Network.Minio.JsonParser
, Network.Minio.JsonParser.Test
, Network.Minio.ListOps , Network.Minio.ListOps
, Network.Minio.PresignedOperations , Network.Minio.PresignedOperations
, Network.Minio.PutObject , Network.Minio.PutObject
@ -134,13 +137,10 @@ test-suite minio-hs-live-server-test
, Network.Minio.TestHelpers , Network.Minio.TestHelpers
, Network.Minio.Utils , Network.Minio.Utils
, Network.Minio.Utils.Test , Network.Minio.Utils.Test
, Network.Minio.API.Test
, Network.Minio.XmlGenerator , Network.Minio.XmlGenerator
, Network.Minio.XmlGenerator.Test , Network.Minio.XmlGenerator.Test
, Network.Minio.XmlParser , Network.Minio.XmlParser
, Network.Minio.XmlParser.Test , Network.Minio.XmlParser.Test
, Network.Minio.JsonParser
, Network.Minio.JsonParser.Test
build-depends: base >= 4.7 && < 5 build-depends: base >= 4.7 && < 5
, minio-hs , minio-hs
, protolude >= 0.1.6 , protolude >= 0.1.6
@ -243,15 +243,18 @@ test-suite minio-hs-test
, TypeFamilies , TypeFamilies
other-modules: Lib.Prelude other-modules: Lib.Prelude
, Network.Minio , Network.Minio
, Network.Minio.AdminAPI
, Network.Minio.API , Network.Minio.API
, Network.Minio.API.Test
, Network.Minio.APICommon , Network.Minio.APICommon
, Network.Minio.AdminAPI
, Network.Minio.CopyObject
, Network.Minio.Data , Network.Minio.Data
, Network.Minio.Data.ByteString , Network.Minio.Data.ByteString
, Network.Minio.Data.Crypto , Network.Minio.Data.Crypto
, Network.Minio.Data.Time , Network.Minio.Data.Time
, Network.Minio.CopyObject
, Network.Minio.Errors , Network.Minio.Errors
, Network.Minio.JsonParser
, Network.Minio.JsonParser.Test
, Network.Minio.ListOps , Network.Minio.ListOps
, Network.Minio.PresignedOperations , Network.Minio.PresignedOperations
, Network.Minio.PutObject , Network.Minio.PutObject
@ -261,13 +264,10 @@ test-suite minio-hs-test
, Network.Minio.TestHelpers , Network.Minio.TestHelpers
, Network.Minio.Utils , Network.Minio.Utils
, Network.Minio.Utils.Test , Network.Minio.Utils.Test
, Network.Minio.API.Test
, Network.Minio.XmlGenerator , Network.Minio.XmlGenerator
, Network.Minio.XmlGenerator.Test , Network.Minio.XmlGenerator.Test
, Network.Minio.XmlParser , Network.Minio.XmlParser
, Network.Minio.XmlParser.Test , Network.Minio.XmlParser.Test
, Network.Minio.JsonParser
, Network.Minio.JsonParser.Test
source-repository head source-repository head
type: git type: git

View File

@ -37,7 +37,6 @@ import qualified Data.Conduit as C
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Time.Clock as Time import qualified Data.Time.Clock as Time
import Network.HTTP.Conduit (Response) import Network.HTTP.Conduit (Response)
import qualified Network.HTTP.Conduit as NC import qualified Network.HTTP.Conduit as NC
import qualified Network.HTTP.Types as HT import qualified Network.HTTP.Types as HT
@ -75,73 +74,97 @@ discoverRegion ri = runMaybeT $ do
return l return l
) return regionMay ) return regionMay
getRegion :: S3ReqInfo -> Minio (Maybe Region)
getRegion ri = do
ci <- asks mcConnInfo
-- getService/makeBucket/getLocation -- don't need location
if | not $ riNeedsLocation ri ->
return $ Just $ connectRegion ci
-- if autodiscovery of location is disabled by user
| not $ connectAutoDiscoverRegion ci ->
return $ Just $ connectRegion ci
-- discover the region for the request
| otherwise -> discoverRegion ri
getRegionHost :: Region -> Minio Text
getRegionHost r = do
ci <- asks mcConnInfo
if "amazonaws.com" `T.isSuffixOf` connectHost ci
then maybe (throwIO $ MErrVRegionNotSupported r)
return (Map.lookup r awsRegionMap)
else return $ connectHost ci
buildRequest :: S3ReqInfo -> Minio NC.Request buildRequest :: S3ReqInfo -> Minio NC.Request
buildRequest ri = do buildRequest ri = do
maybe (return ()) checkBucketNameValidity $ riBucket ri maybe (return ()) checkBucketNameValidity $ riBucket ri
maybe (return ()) checkObjectNameValidity $ riObject ri maybe (return ()) checkObjectNameValidity $ riObject ri
ci <- asks mcConnInfo ci <- asks mcConnInfo
-- getService/makeBucket/getLocation -- don't need regionMay <- getRegion ri
-- location
region <- if | not $ riNeedsLocation ri ->
return $ Just $ connectRegion ci
-- if autodiscovery of location is disabled by user regionHost <- maybe (return $ connectHost ci) getRegionHost regionMay
| not $ connectAutoDiscoverRegion ci ->
return $ Just $ connectRegion ci
-- discover the region for the request let ri' = ri { riHeaders = hostHeader : riHeaders ri
| otherwise -> discoverRegion ri , riRegion = regionMay
regionHost <- case region of
Nothing -> return $ connectHost ci
Just r -> if "amazonaws.com" `T.isSuffixOf` connectHost ci
then maybe
(throwIO $ MErrVRegionNotSupported r)
return
(Map.lookup r awsRegionMap)
else return $ connectHost ci
sha256Hash <- if | connectIsSecure ci ->
-- if secure connection
return "UNSIGNED-PAYLOAD"
-- otherwise compute sha256
| otherwise -> getPayloadSHA256Hash (riPayload ri)
timeStamp <- liftIO Time.getCurrentTime
let hostHeader = (hHost, getHostAddr ci)
newRi = ri { riPayloadHash = Just sha256Hash
, riHeaders = hostHeader
: sha256Header sha256Hash
: riHeaders ri
, riRegion = region
} }
newCi = ci { connectHost = regionHost } ci' = ci { connectHost = regionHost }
signReq = toRequest newCi newRi hostHeader = (hHost, getHostAddr ci')
sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
timeStamp (riRegion newRi) Nothing (riPayloadHash newRi)
let signHeaders = signV4 sp signReq
-- Update signReq with Authorization header containing v4 signature -- Does not contain body and auth info.
return signReq { baseRequest = NC.defaultRequest
NC.requestHeaders = riHeaders newRi ++ mkHeaderFromPairs signHeaders { NC.method = riMethod ri'
} , NC.secure = connectIsSecure ci'
where , NC.host = encodeUtf8 $ connectHost ci'
toRequest :: ConnectInfo -> S3ReqInfo -> NC.Request , NC.port = connectPort ci'
toRequest ci s3Req = NC.defaultRequest { , NC.path = getS3Path (riBucket ri') (riObject ri')
NC.method = riMethod s3Req , NC.requestHeaders = riHeaders ri'
, NC.secure = connectIsSecure ci , NC.queryString = HT.renderQuery False $ riQueryParams ri'
, NC.host = encodeUtf8 $ connectHost ci }
, NC.port = connectPort ci
, NC.path = getS3Path (riBucket s3Req) (riObject s3Req) timeStamp <- liftIO Time.getCurrentTime
, NC.requestHeaders = riHeaders s3Req
, NC.queryString = HT.renderQuery False $ riQueryParams s3Req let sp = SignParams (connectAccessKey ci') (connectSecretKey ci')
, NC.requestBody = getRequestBody (riPayload s3Req) timeStamp (riRegion ri') Nothing Nothing
}
-- Cases to handle:
--
-- 1. Connection is secure: use unsigned payload
--
-- 2. Insecure connection, streaming signature is enabled via use of
-- conduit payload: use streaming signature for request.
--
-- 3. Insecure connection, non-conduit payload: compute payload
-- sha256hash, buffer request in memory and perform request.
-- case 2 from above.
if | isStreamingPayload (riPayload ri') &&
(not $ connectIsSecure ci') -> do
(pLen, pSrc) <- case riPayload ri of
PayloadC l src -> return (l, src)
_ -> throwIO MErrVUnexpectedPayload
let reqFn = signV4Stream pLen sp baseRequest
return $ reqFn pSrc
| otherwise -> do
-- case 1 described above.
sp' <- if | connectIsSecure ci' -> return sp
-- case 3 described above.
| otherwise -> do
pHash <- getPayloadSHA256Hash $ riPayload ri'
return $ sp { spPayloadHash = Just pHash }
let signHeaders = signV4 sp' baseRequest
return $ baseRequest
{ NC.requestHeaders =
NC.requestHeaders baseRequest ++
mkHeaderFromPairs signHeaders
, NC.requestBody = getRequestBody (riPayload ri')
}
retryAPIRequest :: Minio a -> Minio a retryAPIRequest :: Minio a -> Minio a

View File

@ -16,6 +16,9 @@
module Network.Minio.APICommon where module Network.Minio.APICommon where
import qualified Conduit as C
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LB
import Data.Conduit.Binary (sourceHandleRange) import Data.Conduit.Binary (sourceHandleRange)
import qualified Network.HTTP.Conduit as NC import qualified Network.HTTP.Conduit as NC
import qualified Network.HTTP.Types as HT import qualified Network.HTTP.Types as HT
@ -24,16 +27,20 @@ import Lib.Prelude
import Network.Minio.Data import Network.Minio.Data
import Network.Minio.Data.Crypto import Network.Minio.Data.Crypto
import Network.Minio.Errors
sha256Header :: ByteString -> HT.Header sha256Header :: ByteString -> HT.Header
sha256Header = ("x-amz-content-sha256", ) sha256Header = ("x-amz-content-sha256", )
getPayloadSHA256Hash :: (MonadIO m) => Payload -> m ByteString -- | This function throws an error if the payload is a conduit (as it
-- will not be possible to re-read the conduit after it is consumed).
getPayloadSHA256Hash :: Payload -> Minio ByteString
getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
getPayloadSHA256Hash (PayloadH h off size) = hashSHA256FromSource $ getPayloadSHA256Hash (PayloadH h off size) = hashSHA256FromSource $
sourceHandleRange h sourceHandleRange h
(return . fromIntegral $ off) (return . fromIntegral $ off)
(return . fromIntegral $ size) (return . fromIntegral $ size)
getPayloadSHA256Hash (PayloadC _ _) = throwIO MErrVUnexpectedPayload
getRequestBody :: Payload -> NC.RequestBody getRequestBody :: Payload -> NC.RequestBody
getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
@ -42,3 +49,20 @@ getRequestBody (PayloadH h off size) =
sourceHandleRange h sourceHandleRange h
(return . fromIntegral $ off) (return . fromIntegral $ off)
(return . fromIntegral $ size) (return . fromIntegral $ size)
getRequestBody (PayloadC n src) = NC.requestBodySource n src
mkStreamingPayload :: Payload -> Payload
mkStreamingPayload payload =
case payload of
PayloadBS bs ->
PayloadC (fromIntegral $ BS.length bs)
(C.sourceLazy $ LB.fromStrict bs)
PayloadH h off len ->
PayloadC len $ sourceHandleRange h
(return . fromIntegral $ off)
(return . fromIntegral $ len)
_ -> payload
isStreamingPayload :: Payload -> Bool
isStreamingPayload (PayloadC _ _) = True
isStreamingPayload _ = False

View File

@ -20,6 +20,7 @@
{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeFamilies #-}
module Network.Minio.Data where module Network.Minio.Data where
import qualified Conduit as C
import Control.Concurrent.MVar (MVar) import Control.Concurrent.MVar (MVar)
import qualified Control.Concurrent.MVar as M import qualified Control.Concurrent.MVar as M
import Control.Monad.IO.Unlift (MonadUnliftIO, UnliftIO (..), import Control.Monad.IO.Unlift (MonadUnliftIO, UnliftIO (..),
@ -883,10 +884,10 @@ type Stats = Progress
-- | Represents different kinds of payload that are used with S3 API -- | Represents different kinds of payload that are used with S3 API
-- requests. -- requests.
data Payload = PayloadBS ByteString data Payload
| PayloadH Handle = PayloadBS ByteString
Int64 -- offset | PayloadH Handle Int64 Int64 -- file handle, offset and length
Int64 -- size | PayloadC Int64 (C.ConduitT () ByteString (ResourceT IO) ()) -- length and byte source
defaultPayload :: Payload defaultPayload :: Payload
defaultPayload = PayloadBS "" defaultPayload = PayloadBS ""

View File

@ -1,5 +1,5 @@
-- --
-- MinIO Haskell SDK, (C) 2017, 2018 MinIO, Inc. -- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
-- --
-- Licensed under the Apache License, Version 2.0 (the "License"); -- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License. -- you may not use this file except in compliance with the License.
@ -42,6 +42,8 @@ data MErrV = MErrVSinglePUTSizeExceeded Int64
| MErrVInvalidHealPath | MErrVInvalidHealPath
| MErrVMissingCredentials | MErrVMissingCredentials
| MErrVInvalidEncryptionKeyLength | MErrVInvalidEncryptionKeyLength
| MErrVStreamingBodyUnexpectedEOF
| MErrVUnexpectedPayload
deriving (Show, Eq) deriving (Show, Eq)
instance Exception MErrV instance Exception MErrV

View File

@ -1,5 +1,5 @@
-- --
-- MinIO Haskell SDK, (C) 2017 MinIO, Inc. -- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
-- --
-- Licensed under the Apache License, Version 2.0 (the "License"); -- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License. -- you may not use this file except in compliance with the License.
@ -23,13 +23,14 @@ module Network.Minio.PutObject
import Conduit (takeC) import Conduit (takeC)
import qualified Conduit as C
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import qualified Data.Conduit as C
import qualified Data.Conduit.Binary as CB import qualified Data.Conduit.Binary as CB
import qualified Data.Conduit.Combinators as CC import qualified Data.Conduit.Combinators as CC
import qualified Data.Conduit.List as CL import qualified Data.Conduit.List as CL
import qualified Data.List as List import qualified Data.List as List
import Lib.Prelude import Lib.Prelude
import Network.Minio.Data import Network.Minio.Data
@ -63,8 +64,8 @@ putObjectInternal :: Bucket -> Object -> PutObjectOptions
-> ObjectData Minio -> Minio ETag -> ObjectData Minio -> Minio ETag
putObjectInternal b o opts (ODStream src sizeMay) = do putObjectInternal b o opts (ODStream src sizeMay) = do
case sizeMay of case sizeMay of
-- unable to get size, so assume non-seekable file and max-object size -- unable to get size, so assume non-seekable file
Nothing -> sequentialMultipartUpload b o opts (Just maxObjectSize) src Nothing -> sequentialMultipartUpload b o opts Nothing src
-- got file size, so check for single/multipart upload -- got file size, so check for single/multipart upload
Just size -> Just size ->
@ -85,9 +86,8 @@ putObjectInternal b o opts (ODFile fp sizeMay) = do
let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay] let finalSizeMay = listToMaybe $ catMaybes [sizeMay, handleSizeMay]
case finalSizeMay of case finalSizeMay of
-- unable to get size, so assume non-seekable file and max-object size -- unable to get size, so assume non-seekable file
Nothing -> sequentialMultipartUpload b o opts (Just maxObjectSize) $ Nothing -> sequentialMultipartUpload b o opts Nothing $ CB.sourceFile fp
CB.sourceFile fp
-- got file size, so check for single/multipart upload -- got file size, so check for single/multipart upload
Just size -> Just size ->
@ -138,7 +138,7 @@ sequentialMultipartUpload b o opts sizeMay src = do
(pnums, _, sizes) = List.unzip3 partSizes (pnums, _, sizes) = List.unzip3 partSizes
uploadedParts <- C.runConduit uploadedParts <- C.runConduit
$ src $ src
C..| chunkBSConduit sizes C..| chunkBSConduit (map fromIntegral sizes)
C..| CL.map PayloadBS C..| CL.map PayloadBS
C..| uploadPart' uploadId pnums C..| uploadPart' uploadId pnums
C..| CC.sinkList C..| CC.sinkList

View File

@ -42,6 +42,7 @@ module Network.Minio.S3API
--------------------------------- ---------------------------------
, putBucket , putBucket
, ETag , ETag
, maxSinglePutObjectSizeBytes
, putObjectSingle' , putObjectSingle'
, putObjectSingle , putObjectSingle
, copyObjectSingle , copyObjectSingle
@ -90,8 +91,8 @@ module Network.Minio.S3API
, removeAllBucketNotification , removeAllBucketNotification
) where ) where
import qualified Conduit as C
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.Conduit as C
import qualified Data.Text as T import qualified Data.Text as T
import qualified Network.HTTP.Conduit as NC import qualified Network.HTTP.Conduit as NC
import qualified Network.HTTP.Types as HT import qualified Network.HTTP.Types as HT
@ -101,6 +102,7 @@ import UnliftIO (Handler (Handler))
import Lib.Prelude import Lib.Prelude
import Network.Minio.API import Network.Minio.API
import Network.Minio.APICommon
import Network.Minio.Data import Network.Minio.Data
import Network.Minio.Errors import Network.Minio.Errors
import Network.Minio.PresignedOperations import Network.Minio.PresignedOperations
@ -156,13 +158,13 @@ putObjectSingle' bucket object headers bs = do
when (size > maxSinglePutObjectSizeBytes) $ when (size > maxSinglePutObjectSizeBytes) $
throwIO $ MErrVSinglePUTSizeExceeded size throwIO $ MErrVSinglePUTSizeExceeded size
-- content-length header is automatically set by library. let payload = mkStreamingPayload $ PayloadBS bs
resp <- executeRequest $ resp <- executeRequest $
defaultS3ReqInfo { riMethod = HT.methodPut defaultS3ReqInfo { riMethod = HT.methodPut
, riBucket = Just bucket , riBucket = Just bucket
, riObject = Just object , riObject = Just object
, riHeaders = headers , riHeaders = headers
, riPayload = PayloadBS bs , riPayload = payload
} }
let rheaders = NC.responseHeaders resp let rheaders = NC.responseHeaders resp
@ -181,13 +183,14 @@ putObjectSingle bucket object headers h offset size = do
throwIO $ MErrVSinglePUTSizeExceeded size throwIO $ MErrVSinglePUTSizeExceeded size
-- content-length header is automatically set by library. -- content-length header is automatically set by library.
let payload = mkStreamingPayload $ PayloadH h offset size
resp <- executeRequest $ resp <- executeRequest $
defaultS3ReqInfo { riMethod = HT.methodPut defaultS3ReqInfo { riMethod = HT.methodPut
, riBucket = Just bucket , riBucket = Just bucket
, riObject = Just object , riObject = Just object
, riHeaders = headers , riHeaders = headers
, riPayload = PayloadH h offset size , riPayload = payload
} }
let rheaders = NC.responseHeaders resp let rheaders = NC.responseHeaders resp
etag = getETagHeader rheaders etag = getETagHeader rheaders
@ -264,14 +267,16 @@ newMultipartUpload bucket object headers = do
putObjectPart :: Bucket -> Object -> UploadId -> PartNumber -> [HT.Header] putObjectPart :: Bucket -> Object -> UploadId -> PartNumber -> [HT.Header]
-> Payload -> Minio PartTuple -> Payload -> Minio PartTuple
putObjectPart bucket object uploadId partNumber headers payload = do putObjectPart bucket object uploadId partNumber headers payload = do
-- transform payload to conduit to enable streaming signature
let payload' = mkStreamingPayload payload
resp <- executeRequest $ resp <- executeRequest $
defaultS3ReqInfo { riMethod = HT.methodPut defaultS3ReqInfo { riMethod = HT.methodPut
, riBucket = Just bucket , riBucket = Just bucket
, riObject = Just object , riObject = Just object
, riQueryParams = mkOptionalParams params , riQueryParams = mkOptionalParams params
, riHeaders = headers , riHeaders = headers
, riPayload = payload , riPayload = payload'
} }
let rheaders = NC.responseHeaders resp let rheaders = NC.responseHeaders resp
etag = getETagHeader rheaders etag = getETagHeader rheaders
maybe maybe

View File

@ -1,5 +1,5 @@
-- --
-- MinIO Haskell SDK, (C) 2017 MinIO, Inc. -- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
-- --
-- Licensed under the Apache License, Version 2.0 (the "License"); -- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License. -- you may not use this file except in compliance with the License.
@ -14,21 +14,9 @@
-- limitations under the License. -- limitations under the License.
-- --
module Network.Minio.Sign.V4 module Network.Minio.Sign.V4 where
(
signV4
, signV4PostPolicy
, mkScope
, getHeadersToSign
, mkCanonicalRequest
, mkStringToSign
, mkSigningKey
, computeSignature
, SignV4Data(..)
, SignParams(..)
, debugPrintSignV4Data
) where
import qualified Conduit as C
import qualified Data.ByteString as B import qualified Data.ByteString as B
import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Base64 as Base64
import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Char8 as B8
@ -39,13 +27,15 @@ import qualified Data.Set as Set
import qualified Data.Time as Time import qualified Data.Time as Time
import qualified Network.HTTP.Conduit as NC import qualified Network.HTTP.Conduit as NC
import Network.HTTP.Types (Header, parseQuery) import Network.HTTP.Types (Header, parseQuery)
import qualified Network.HTTP.Types.Header as H import qualified Network.HTTP.Types as H
import Text.Printf (printf)
import Lib.Prelude import Lib.Prelude
import Network.Minio.Data.ByteString import Network.Minio.Data.ByteString
import Network.Minio.Data.Crypto import Network.Minio.Data.Crypto
import Network.Minio.Data.Time import Network.Minio.Data.Time
import Network.Minio.Errors
-- these headers are not included in the string to sign when signing a -- these headers are not included in the string to sign when signing a
-- request -- request
@ -53,7 +43,6 @@ ignoredHeaders :: Set ByteString
ignoredHeaders = Set.fromList $ map CI.foldedCase ignoredHeaders = Set.fromList $ map CI.foldedCase
[ H.hAuthorization [ H.hAuthorization
, H.hContentType , H.hContentType
, H.hContentLength
, H.hUserAgent , H.hUserAgent
] ]
@ -93,6 +82,20 @@ debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
mapM_ (\x -> B.putStr $ B.concat [show x, " "]) $ B.unpack b mapM_ (\x -> B.putStr $ B.concat [show x, " "]) $ B.unpack b
B8.putStrLn "" B8.putStrLn ""
mkAuthHeader :: Text -> ByteString -> ByteString -> ByteString -> H.Header
mkAuthHeader accessKey scope signedHeaderKeys sign =
let authValue = B.concat
[ "AWS4-HMAC-SHA256 Credential="
, toS accessKey
, "/"
, scope
, ", SignedHeaders="
, signedHeaderKeys
, ", Signature="
, sign
]
in (H.hAuthorization, authValue)
-- | Given SignParams and request details, including request method, -- | Given SignParams and request details, including request method,
-- request path, headers, query params and payload hash, generates an -- request path, headers, query params and payload hash, generates an
-- updated set of headers, including the x-amz-date header and the -- updated set of headers, including the x-amz-date header and the
@ -105,7 +108,6 @@ debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
-- is being created. The expiry is interpreted as an integer number of -- is being created. The expiry is interpreted as an integer number of
-- seconds. The output will be the list of query-parameters to add to -- seconds. The output will be the list of query-parameters to add to
-- the request. -- the request.
signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)] signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)]
signV4 !sp !req = signV4 !sp !req =
let let
@ -139,7 +141,8 @@ signV4 !sp !req =
else [] else []
-- 1. compute canonical request -- 1. compute canonical request
canonicalRequest = mkCanonicalRequest sp (NC.setQueryString finalQP req) canonicalRequest = mkCanonicalRequest False sp
(NC.setQueryString finalQP req)
headersToSign headersToSign
-- 2. compute string to sign -- 2. compute string to sign
@ -152,23 +155,15 @@ signV4 !sp !req =
signature = computeSignature stringToSign signingKey signature = computeSignature stringToSign signingKey
-- 4. compute auth header -- 4. compute auth header
authValue = B.concat authHeader = mkAuthHeader (spAccessKey sp) scope signedHeaderKeys signature
[ "AWS4-HMAC-SHA256 Credential="
, accessKey
, "/"
, scope
, ", SignedHeaders="
, signedHeaderKeys
, ", Signature="
, signature
]
authHeader = (H.hAuthorization, authValue)
-- finally compute output pairs -- finally compute output pairs
sha256Hdr = ("x-amz-content-sha256",
fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp)
output = if isJust expiry output = if isJust expiry
then ("X-Amz-Signature", signature) : authQP then ("X-Amz-Signature", signature) : authQP
else [(\(x, y) -> (CI.foldedCase x, y)) authHeader, else [(\(x, y) -> (CI.foldedCase x, y)) authHeader,
datePair] datePair, sha256Hdr]
in output in output
@ -186,9 +181,9 @@ getHeadersToSign !h =
filter (flip Set.notMember ignoredHeaders . fst) $ filter (flip Set.notMember ignoredHeaders . fst) $
map (\(x, y) -> (CI.foldedCase x, stripBS y)) h map (\(x, y) -> (CI.foldedCase x, stripBS y)) h
mkCanonicalRequest :: SignParams -> NC.Request -> [(ByteString, ByteString)] mkCanonicalRequest :: Bool -> SignParams -> NC.Request -> [(ByteString, ByteString)]
-> ByteString -> ByteString
mkCanonicalRequest !sp !req !headersForSign = mkCanonicalRequest !isStreaming !sp !req !headersForSign =
let let
canonicalQueryString = B.intercalate "&" $ canonicalQueryString = B.intercalate "&" $
map (\(x, y) -> B.concat [x, "=", y]) $ map (\(x, y) -> B.concat [x, "=", y]) $
@ -203,6 +198,10 @@ mkCanonicalRequest !sp !req !headersForSign =
signedHeaders = B.intercalate ";" $ map fst sortedHeaders signedHeaders = B.intercalate ";" $ map fst sortedHeaders
payloadHashStr =
if isStreaming
then "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
else fromMaybe "UNSIGNED-PAYLOAD" $ spPayloadHash sp
in in
B.intercalate "\n" B.intercalate "\n"
[ NC.method req [ NC.method req
@ -210,7 +209,7 @@ mkCanonicalRequest !sp !req !headersForSign =
, canonicalQueryString , canonicalQueryString
, canonicalHeaders , canonicalHeaders
, signedHeaders , signedHeaders
, maybe "UNSIGNED-PAYLOAD" identity $ spPayloadHash sp , payloadHashStr
] ]
mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
@ -246,3 +245,147 @@ signV4PostPolicy !postPolicyJSON !sp =
Map.fromList [ ("x-amz-signature", signature) Map.fromList [ ("x-amz-signature", signature)
, ("policy", stringToSign) , ("policy", stringToSign)
] ]
chunkSizeConstant :: Int
chunkSizeConstant = 64 * 1024
-- base16Len computes the number of bytes required to represent @n (> 0)@ in
-- hexadecimal.
base16Len :: Integral a => a -> Int
base16Len n | n == 0 = 0
| otherwise = 1 + base16Len (n `div` 16)
signedStreamLength :: Int64 -> Int64
signedStreamLength dataLen =
let
chunkSzInt = fromIntegral chunkSizeConstant
(numChunks, lastChunkLen) = quotRem dataLen chunkSzInt
-- Structure of a chunk:
-- string(IntHexBase(chunk-size)) + ";chunk-signature=" + signature + \r\n + chunk-data + \r\n
encodedChunkLen csz = fromIntegral (base16Len csz) + 17 + 64 + 2 + csz + 2
fullChunkSize = encodedChunkLen chunkSzInt
lastChunkSize = bool 0 (encodedChunkLen lastChunkLen) $ lastChunkLen > 0
finalChunkSize = 1 + 17 + 64 + 2 + 2
in
numChunks * fullChunkSize + lastChunkSize + finalChunkSize
signV4Stream :: Int64 -> SignParams -> NC.Request
-> (C.ConduitT () ByteString (C.ResourceT IO) () -> NC.Request)
-- -> ([Header], C.ConduitT () ByteString (C.ResourceT IO) () -> NC.RequestBody)
signV4Stream !payloadLength !sp !req =
let
ts = spTimeStamp sp
addContentEncoding hs =
let ceMay = headMay $ filter (\(x, _) -> x == "content-encoding") hs
in case ceMay of
Nothing -> ("content-encoding", "aws-chunked") : hs
Just (_, ce) -> ("content-encoding", ce <> ",aws-chunked") :
filter (\(x, _) -> x /= "content-encoding") hs
-- headers to be added to the request
datePair = ("X-Amz-Date", awsTimeFormatBS ts)
computedHeaders = addContentEncoding $
datePair : NC.requestHeaders req
-- headers specific to streaming signature
signedContentLength = signedStreamLength payloadLength
streamingHeaders :: [Header]
streamingHeaders =
[ ("x-amz-decoded-content-length", show payloadLength)
, ("content-length", show signedContentLength )
, ("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
]
headersToSign = getHeadersToSign $ computedHeaders ++ streamingHeaders
signedHeaderKeys = B.intercalate ";" $ sort $ map fst headersToSign
finalQP = parseQuery (NC.queryString req)
-- 1. Compute Seed Signature
-- 1.1 Canonical Request
canonicalReq = mkCanonicalRequest True sp
(NC.setQueryString finalQP req)
headersToSign
region = fromMaybe "" $ spRegion sp
scope = mkScope ts region
accessKey = spAccessKey sp
secretKey = spSecretKey sp
-- 1.2 String toSign
stringToSign = mkStringToSign ts scope canonicalReq
-- 1.3 Compute signature
-- 1.3.1 compute signing key
signingKey = mkSigningKey ts region $ toS secretKey
-- 1.3.2 Compute signature
seedSignature = computeSignature stringToSign signingKey
-- 1.3.3 Compute Auth Header
authHeader = mkAuthHeader accessKey scope signedHeaderKeys seedSignature
-- 1.4 Updated headers for the request
finalReqHeaders = authHeader : (computedHeaders ++ streamingHeaders)
-- headersToAdd = authHeader : datePair : streamingHeaders
toHexStr n = B8.pack $ printf "%x" n
(numParts, lastPSize) = payloadLength `quotRem` fromIntegral chunkSizeConstant
-- Function to compute string to sign for each chunk.
chunkStrToSign prevSign currChunkHash =
B.intercalate "\n"
[ "AWS4-HMAC-SHA256-PAYLOAD"
, awsTimeFormatBS ts
, scope
, prevSign
, hashSHA256 ""
, currChunkHash
]
-- Read n byte from upstream and return a strict bytestring.
mustTakeN n = do
bs <- toS <$> (C.takeCE n C..| C.sinkLazy)
when (B.length bs /= n) $
throwIO MErrVStreamingBodyUnexpectedEOF
return bs
signerConduit n lps prevSign =
-- First case encodes a full chunk of length
-- 'chunkSizeConstant'.
if | n > 0 -> do
bs <- mustTakeN chunkSizeConstant
let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
nextSign = computeSignature strToSign signingKey
chunkBS = toHexStr chunkSizeConstant
<> ";chunk-signature="
<> nextSign <> "\r\n" <> bs <> "\r\n"
C.yield chunkBS
signerConduit (n-1) lps nextSign
-- Second case encodes the last chunk which is smaller than
-- 'chunkSizeConstant'
| lps > 0 -> do
bs <- mustTakeN $ fromIntegral lps
let strToSign = chunkStrToSign prevSign (hashSHA256 bs)
nextSign = computeSignature strToSign signingKey
chunkBS = toHexStr lps <> ";chunk-signature="
<> nextSign <> "\r\n" <> bs <> "\r\n"
C.yield chunkBS
signerConduit 0 0 nextSign
-- Last case encodes the final signature chunk that has no
-- data.
| otherwise -> do
let strToSign = chunkStrToSign prevSign (hashSHA256 "")
nextSign = computeSignature strToSign signingKey
lastChunkBS = "0;chunk-signature=" <> nextSign <> "\r\n\r\n"
C.yield lastChunkBS
in
\src -> req { NC.requestHeaders = finalReqHeaders
, NC.requestBody =
NC.requestBodySource signedContentLength $
src C..| signerConduit numParts lastPSize seedSignature
}

View File

@ -1,5 +1,5 @@
-- --
-- MinIO Haskell SDK, (C) 2017 MinIO, Inc. -- MinIO Haskell SDK, (C) 2017-2019 MinIO, Inc.
-- --
-- Licensed under the Apache License, Version 2.0 (the "License"); -- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License. -- you may not use this file except in compliance with the License.
@ -16,12 +16,12 @@
module Network.Minio.Utils where module Network.Minio.Utils where
import qualified Conduit as C
import Control.Monad.IO.Unlift (MonadUnliftIO) import Control.Monad.IO.Unlift (MonadUnliftIO)
import qualified Control.Monad.Trans.Resource as R import qualified Control.Monad.Trans.Resource as R
import qualified Data.ByteString as B import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as LB import qualified Data.ByteString.Lazy as LB
import Data.CaseInsensitive (mk, original) import Data.CaseInsensitive (mk, original)
import qualified Data.Conduit as C
import qualified Data.Conduit.Binary as CB import qualified Data.Conduit.Binary as CB
import qualified Data.List as List import qualified Data.List as List
import qualified Data.Map as Map import qualified Data.Map as Map
@ -212,22 +212,17 @@ mkQuery k mv = (k,) <$> mv
mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query mkOptionalParams :: [(Text, Maybe Text)] -> HT.Query
mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params mkOptionalParams params = HT.toQuery $ uncurry mkQuery <$> params
chunkBSConduit :: (Monad m, Integral a) -- | Conduit that rechunks bytestrings into the given chunk
=> [a] -> C.ConduitM ByteString ByteString m () -- lengths. Stops after given chunk lengths are yielded. Stops if
chunkBSConduit s = loop 0 [] s -- there are no more chunks to yield or if a shorter chunk is
where -- received. Does not throw any errors.
loop _ _ [] = return () chunkBSConduit :: (Monad m) => [Int] -> C.ConduitM ByteString ByteString m ()
loop n readChunks (size:sizes) = do chunkBSConduit [] = return ()
bsMay <- C.await chunkBSConduit (s:ss) = do
case bsMay of bs <- fmap LB.toStrict $ C.takeCE s C..| C.sinkLazy
Nothing -> when (n > 0) $ C.yield $ B.concat readChunks if | B.length bs == s -> C.yield bs >> chunkBSConduit ss
Just bs -> if n + fromIntegral (B.length bs) >= size | B.length bs > 0 -> C.yield bs
then do let (a, b) = B.splitAt (fromIntegral $ size - n) bs | otherwise -> return ()
chunkBS = B.concat $ readChunks ++ [a]
C.yield chunkBS
loop (fromIntegral $ B.length b) [b] sizes
else loop (n + fromIntegral (B.length bs))
(readChunks ++ [bs]) (size:sizes)
-- | Select part sizes - the logic is that the minimum part-size will -- | Select part sizes - the logic is that the minimum part-size will
-- be 64MiB. -- be 64MiB.