Make signature V4 independent of S3ReqInfo (#88)

- Rename RequestInfo to S3ReqInfo
This commit is contained in:
Krishnan Parthasarathi 2018-05-30 13:50:15 -07:00 committed by Aditya Manthramurthy
parent 56856e82e0
commit a946dfd305
4 changed files with 118 additions and 99 deletions

View File

@ -16,7 +16,7 @@
module Network.Minio.API module Network.Minio.API
( connect ( connect
, RequestInfo(..) , S3ReqInfo(..)
, runMinio , runMinio
, executeRequest , executeRequest
, mkStreamRequest , mkStreamRequest
@ -35,6 +35,7 @@ import Data.Conduit.Binary (sourceHandleRange)
import Data.Default (def) import Data.Default (def)
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 Network.HTTP.Conduit (Response) import Network.HTTP.Conduit (Response)
import qualified Network.HTTP.Conduit as NC import qualified Network.HTTP.Conduit as NC
@ -82,7 +83,7 @@ getLocation bucket = do
-- | Looks for region in RegionMap and updates it using getLocation if -- | Looks for region in RegionMap and updates it using getLocation if
-- absent. -- absent.
discoverRegion :: RequestInfo -> Minio (Maybe Region) discoverRegion :: S3ReqInfo -> Minio (Maybe Region)
discoverRegion ri = runMaybeT $ do discoverRegion ri = runMaybeT $ do
bucket <- MaybeT $ return $ riBucket ri bucket <- MaybeT $ return $ riBucket ri
regionMay <- lift $ lookupRegionCache bucket regionMay <- lift $ lookupRegionCache bucket
@ -93,7 +94,7 @@ discoverRegion ri = runMaybeT $ do
) return regionMay ) return regionMay
buildRequest :: RequestInfo -> 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
@ -128,6 +129,8 @@ buildRequest ri = do
-- otherwise compute sha256 -- otherwise compute sha256
| otherwise -> getPayloadSHA256Hash (riPayload ri) | otherwise -> getPayloadSHA256Hash (riPayload ri)
timeStamp <- liftIO Time.getCurrentTime
let hostHeader = (hHost, getHostAddr ci) let hostHeader = (hHost, getHostAddr ci)
newRi = ri { riPayloadHash = Just sha256Hash newRi = ri { riPayloadHash = Just sha256Hash
, riHeaders = hostHeader , riHeaders = hostHeader
@ -136,28 +139,36 @@ buildRequest ri = do
, riRegion = region , riRegion = region
} }
newCi = ci { connectHost = regionHost } newCi = ci { connectHost = regionHost }
signReq = toRequest newCi newRi
sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
timeStamp (riRegion newRi) Nothing (riPayloadHash newRi)
let signHeaders = signV4 sp signReq
signHeaders <- liftIO $ signV4 newCi newRi Nothing -- Update signReq with Authorization header containing v4 signature
return signReq {
NC.requestHeaders = riHeaders newRi ++ mkHeaderFromPairs signHeaders
}
where
toRequest :: ConnectInfo -> S3ReqInfo -> NC.Request
toRequest ci s3Req = NC.defaultRequest {
NC.method = riMethod s3Req
, NC.secure = connectIsSecure ci
, NC.host = encodeUtf8 $ connectHost ci
, NC.port = connectPort ci
, NC.path = getS3Path (riBucket s3Req) (riObject s3Req)
, NC.requestHeaders = riHeaders s3Req
, NC.queryString = HT.renderQuery False $ riQueryParams s3Req
, NC.requestBody = getRequestBody (riPayload s3Req)
}
return NC.defaultRequest { executeRequest :: S3ReqInfo -> Minio (Response LByteString)
NC.method = riMethod newRi
, NC.secure = connectIsSecure newCi
, NC.host = encodeUtf8 $ connectHost newCi
, NC.port = connectPort newCi
, NC.path = getPathFromRI newRi
, NC.queryString = HT.renderQuery False $ riQueryParams newRi
, NC.requestHeaders = riHeaders newRi ++ mkHeaderFromPairs signHeaders
, NC.requestBody = getRequestBody (riPayload newRi)
}
executeRequest :: RequestInfo -> Minio (Response LByteString)
executeRequest ri = do executeRequest ri = do
req <- buildRequest ri req <- buildRequest ri
mgr <- asks mcConnManager mgr <- asks mcConnManager
httpLbs req mgr httpLbs req mgr
mkStreamRequest :: RequestInfo mkStreamRequest :: S3ReqInfo
-> Minio (Response (C.ConduitM () ByteString Minio ())) -> Minio (Response (C.ConduitM () ByteString Minio ()))
mkStreamRequest ri = do mkStreamRequest ri = do
req <- buildRequest ri req <- buildRequest ri

View File

@ -472,7 +472,7 @@ data Payload = PayloadBS ByteString
instance Default Payload where instance Default Payload where
def = PayloadBS "" def = PayloadBS ""
data RequestInfo = RequestInfo { data S3ReqInfo = S3ReqInfo {
riMethod :: Method riMethod :: Method
, riBucket :: Maybe Bucket , riBucket :: Maybe Bucket
, riObject :: Maybe Object , riObject :: Maybe Object
@ -484,15 +484,12 @@ data RequestInfo = RequestInfo {
, riNeedsLocation :: Bool , riNeedsLocation :: Bool
} }
instance Default RequestInfo where instance Default S3ReqInfo where
def = RequestInfo HT.methodGet def def def def def Nothing def True def = S3ReqInfo HT.methodGet def def def def def Nothing def True
getPathFromRI :: RequestInfo -> ByteString getS3Path :: Maybe Bucket -> Maybe Object -> ByteString
getPathFromRI ri = getS3Path b o =
let let segments = map toS $ catMaybes $ b : bool [] [o] (isJust b)
b = riBucket ri
o = riObject ri
segments = map toS $ catMaybes $ b : bool [] [o] (isJust b)
in in
B.concat ["/", B.intercalate "/" segments] B.concat ["/", B.intercalate "/" segments]

View File

@ -39,10 +39,10 @@ module Network.Minio.PresignedOperations
import Data.Aeson ((.=)) import Data.Aeson ((.=))
import qualified Data.Aeson as Json import qualified Data.Aeson as Json
import Data.ByteString.Builder (byteString, toLazyByteString) import Data.ByteString.Builder (byteString, toLazyByteString)
import Data.Default (def)
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Time as Time import qualified Data.Time as Time
import qualified Network.HTTP.Conduit as NC
import qualified Network.HTTP.Types as HT import qualified Network.HTTP.Types as HT
import Network.HTTP.Types.Header (hHost) import Network.HTTP.Types.Header (hHost)
@ -72,24 +72,31 @@ makePresignedUrl expiry method bucket object region extraQuery extraHeaders = do
let let
hostHeader = (hHost, getHostAddr ci) hostHeader = (hHost, getHostAddr ci)
ri = def { riMethod = method req = NC.defaultRequest {
, riBucket = bucket NC.method = method
, riObject = object , NC.secure = connectIsSecure ci
, riQueryParams = extraQuery , NC.host = encodeUtf8 $ connectHost ci
, riHeaders = hostHeader : extraHeaders , NC.port = connectPort ci
, riRegion = Just $ maybe (connectRegion ci) identity region , NC.path = getS3Path bucket object
} , NC.requestHeaders = hostHeader : extraHeaders
, NC.queryString = HT.renderQuery True extraQuery
}
ts <- liftIO Time.getCurrentTime
signPairs <- liftIO $ signV4 ci ri (Just expiry) let sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
ts region (Just expiry) Nothing
let signPairs = signV4 sp req
qpToAdd = (fmap . fmap) Just signPairs
queryStr = HT.renderQueryBuilder True (riQueryParams ri ++ qpToAdd)
scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
return $ toS $ toLazyByteString $ qpToAdd = (fmap . fmap) Just signPairs
scheme <> byteString (getHostAddr ci) <> byteString (getPathFromRI ri) <> queryStr = HT.renderQueryBuilder True
queryStr ((HT.parseQuery $ NC.queryString req) ++ qpToAdd)
scheme = byteString $ bool "http://" "https://" $ connectIsSecure ci
return $ toS $ toLazyByteString $ scheme
<> byteString (getHostAddr ci)
<> byteString (getS3Path bucket object)
<> queryStr
-- | Generate a URL with authentication signature to PUT (upload) an -- | Generate a URL with authentication signature to PUT (upload) an
-- object. Any extra headers if passed, are signed, and so they are -- object. Any extra headers if passed, are signed, and so they are
@ -258,8 +265,10 @@ presignedPostPolicy p = do
ppWithCreds = p { ppWithCreds = p {
conditions = conditions p ++ extraConditions conditions = conditions p ++ extraConditions
} }
signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp = SignParams (connectAccessKey ci) (connectSecretKey ci)
signTime ci signTime (Just $ connectRegion ci) Nothing Nothing
signData = signV4PostPolicy (showPostPolicy ppWithCreds) sp
-- compute form-data -- compute form-data
mkPair (PPCStartsWith k v) = Just (k, v) mkPair (PPCStartsWith k v) = Just (k, v)

View File

@ -17,7 +17,6 @@
module Network.Minio.Sign.V4 module Network.Minio.Sign.V4
( (
signV4 signV4
, signV4AtTime
, signV4PostPolicy , signV4PostPolicy
, mkScope , mkScope
, getHeadersToSign , getHeadersToSign
@ -26,21 +25,24 @@ module Network.Minio.Sign.V4
, mkSigningKey , mkSigningKey
, computeSignature , computeSignature
, SignV4Data(..) , SignV4Data(..)
, SignParams(..)
, debugPrintSignV4Data , debugPrintSignV4Data
) where ) where
import qualified Data.ByteString as B import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Base64 as Base64
import Data.CaseInsensitive (mk) import qualified Data.ByteString.Char8 as B8
import qualified Data.CaseInsensitive as CI import Data.CaseInsensitive (mk)
import qualified Data.Set as Set import qualified Data.CaseInsensitive as CI
import qualified Data.Time as Time import qualified Data.Map.Strict as Map
import qualified Data.ByteString.Base64 as Base64 import qualified Data.Set as Set
import qualified Data.Map.Strict as Map import qualified Data.Time as Time
import Network.HTTP.Types (Header) import qualified Network.HTTP.Conduit as NC
import qualified Network.HTTP.Types.Header as H import Network.HTTP.Types (Header, parseQuery)
import qualified Network.HTTP.Types.Header as H
import Lib.Prelude import Lib.Prelude
import Network.Minio.Data import Network.Minio.Data
import Network.Minio.Data.ByteString import Network.Minio.Data.ByteString
import Network.Minio.Data.Crypto import Network.Minio.Data.Crypto
@ -57,14 +59,23 @@ ignoredHeaders = Set.fromList $ map CI.foldedCase
] ]
data SignV4Data = SignV4Data { data SignV4Data = SignV4Data {
sv4SignTime :: UTCTime sv4SignTime :: UTCTime
, sv4Scope :: ByteString , sv4Scope :: ByteString
, sv4CanonicalRequest :: ByteString , sv4CanonicalRequest :: ByteString
, sv4HeadersToSign :: [(ByteString, ByteString)] , sv4HeadersToSign :: [(ByteString, ByteString)]
, sv4Output :: [(ByteString, ByteString)] , sv4Output :: [(ByteString, ByteString)]
, sv4StringToSign :: ByteString , sv4StringToSign :: ByteString
, sv4SigningKey :: ByteString , sv4SigningKey :: ByteString
} deriving (Show) } deriving (Show)
data SignParams = SignParams {
spAccessKey :: Text
, spSecretKey :: Text
, spTimeStamp :: UTCTime
, spRegion :: Maybe Region
, spExpirySecs :: Maybe Int
, spPayloadHash :: Maybe ByteString
} deriving (Show)
debugPrintSignV4Data :: SignV4Data -> IO () debugPrintSignV4Data :: SignV4Data -> IO ()
debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do debugPrintSignV4Data (SignV4Data t s cr h2s o sts sk) = do
@ -83,40 +94,33 @@ 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 ""
-- | Given MinioClient 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
-- Authorization header, which includes the signature. -- Authorization header, which includes the signature.
signV4 :: ConnectInfo -> RequestInfo -> Maybe Int --
-> IO [(ByteString, ByteString)] -- For normal requests (i.e. without an expiry time), the output is
signV4 !ci !ri !expiry = do -- the list of headers to add to authenticate the request.
timestamp <- Time.getCurrentTime
let signData = signV4AtTime timestamp ci ri expiry
-- debugPrintSignV4Data signData
return $ sv4Output signData
-- | Takes a timestamp, server params and request params and generates
-- AWS Sign V4 data. For normal requests (i.e. without an expiry
-- time), the output is the list of headers to add to authenticate the
-- request.
-- --
-- If `expiry` is not Nothing, it is assumed that a presigned request -- If `expiry` is not Nothing, it is assumed that a presigned request
-- 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.
signV4AtTime :: UTCTime -> ConnectInfo -> RequestInfo -> Maybe Int
-> SignV4Data signV4 :: SignParams -> NC.Request -> [(ByteString, ByteString)]
signV4AtTime ts ci ri expiry = signV4 !sp !req =
let let
region = maybe (connectRegion ci) identity $ riRegion ri region = fromMaybe "" $ spRegion sp
ts = spTimeStamp sp
scope = mkScope ts region scope = mkScope ts region
accessKey = toS $ connectAccessKey ci accessKey = toS $ spAccessKey sp
secretKey = toS $ connectSecretKey ci secretKey = toS $ spSecretKey sp
expiry = spExpirySecs sp
-- headers to be added to the request -- headers to be added to the request
datePair = ("X-Amz-Date", awsTimeFormatBS ts) datePair = ("X-Amz-Date", awsTimeFormatBS ts)
computedHeaders = riHeaders ri ++ computedHeaders = NC.requestHeaders req ++
if isJust expiry if isJust $ expiry
then [] then []
else [(\(x, y) -> (mk x, y)) datePair] else [(\(x, y) -> (mk x, y)) datePair]
headersToSign = getHeadersToSign computedHeaders headersToSign = getHeadersToSign computedHeaders
@ -130,13 +134,13 @@ signV4AtTime ts ci ri expiry =
, ("X-Amz-Expires", maybe "" show expiry) , ("X-Amz-Expires", maybe "" show expiry)
, ("X-Amz-SignedHeaders", signedHeaderKeys) , ("X-Amz-SignedHeaders", signedHeaderKeys)
] ]
finalQP = riQueryParams ri ++ finalQP = parseQuery (NC.queryString req) ++
if isJust expiry if isJust expiry
then (fmap . fmap) Just authQP then (fmap . fmap) Just authQP
else [] else []
-- 1. compute canonical request -- 1. compute canonical request
canonicalRequest = mkCanonicalRequest (ri {riQueryParams = finalQP}) canonicalRequest = mkCanonicalRequest sp (NC.setQueryString finalQP req)
headersToSign headersToSign
-- 2. compute string to sign -- 2. compute string to sign
@ -167,9 +171,7 @@ signV4AtTime ts ci ri expiry =
else [(\(x, y) -> (CI.foldedCase x, y)) authHeader, else [(\(x, y) -> (CI.foldedCase x, y)) authHeader,
datePair] datePair]
in in output
SignV4Data ts scope canonicalRequest headersToSign output
stringToSign signingKey
mkScope :: UTCTime -> Region -> ByteString mkScope :: UTCTime -> Region -> ByteString
@ -185,15 +187,15 @@ 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 :: RequestInfo -> [(ByteString, ByteString)] mkCanonicalRequest :: SignParams -> NC.Request -> [(ByteString, ByteString)]
-> ByteString -> ByteString
mkCanonicalRequest !ri !headersForSign = mkCanonicalRequest !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]) $
sort $ map (\(x, y) -> sort $ map (\(x, y) ->
(uriEncode True x, maybe "" (uriEncode True) y)) $ (uriEncode True x, maybe "" (uriEncode True) y)) $
riQueryParams ri (parseQuery $ NC.queryString req)
sortedHeaders = sort headersForSign sortedHeaders = sort headersForSign
@ -204,12 +206,12 @@ mkCanonicalRequest !ri !headersForSign =
in in
B.intercalate "\n" B.intercalate "\n"
[ riMethod ri [ NC.method req
, uriEncode False $ getPathFromRI ri , uriEncode False $ NC.path req
, canonicalQueryString , canonicalQueryString
, canonicalHeaders , canonicalHeaders
, signedHeaders , signedHeaders
, maybe "UNSIGNED-PAYLOAD" identity $ riPayloadHash ri , maybe "UNSIGNED-PAYLOAD" identity $ spPayloadHash sp
] ]
mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString mkStringToSign :: UTCTime -> ByteString -> ByteString -> ByteString
@ -233,13 +235,13 @@ computeSignature !toSign !key = digestToBase16 $ hmacSHA256 toSign key
-- | Takes a validated Post Policy JSON bytestring, the signing time, -- | Takes a validated Post Policy JSON bytestring, the signing time,
-- and ConnInfo and returns form-data for the POST upload containing -- and ConnInfo and returns form-data for the POST upload containing
-- just the signature and the encoded post-policy. -- just the signature and the encoded post-policy.
signV4PostPolicy :: ByteString -> UTCTime -> ConnectInfo signV4PostPolicy :: ByteString -> SignParams
-> Map.Map Text ByteString -> Map.Map Text ByteString
signV4PostPolicy !postPolicyJSON !signTime !ci = signV4PostPolicy !postPolicyJSON !sp =
let let
stringToSign = Base64.encode postPolicyJSON stringToSign = Base64.encode postPolicyJSON
region = connectRegion ci region = fromMaybe "" $ spRegion sp
signingKey = mkSigningKey signTime region $ toS $ connectSecretKey ci signingKey = mkSigningKey (spTimeStamp sp) region $ toS $ spSecretKey sp
signature = computeSignature stringToSign signingKey signature = computeSignature stringToSign signingKey
in in
Map.fromList [ ("x-amz-signature", signature) Map.fromList [ ("x-amz-signature", signature)