Compiling but without signing working properly yet
This commit is contained in:
parent
d7ceaf272e
commit
5ce595bc6f
16
app/Main.hs
16
app/Main.hs
@ -1,7 +1,19 @@
|
|||||||
module Main where
|
module Main where
|
||||||
|
|
||||||
|
import qualified Network.HTTP.Types as HT
|
||||||
|
import qualified Network.HTTP.Conduit as NC
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
import Lib
|
|
||||||
|
import Network.Minio.API
|
||||||
|
import Network.Minio.Data
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = someFunc
|
main = do
|
||||||
|
resp <- minioExecute mc req
|
||||||
|
print $ NC.responseStatus resp
|
||||||
|
print $ NC.responseHeaders resp
|
||||||
|
print $ NC.responseBody resp
|
||||||
|
where
|
||||||
|
mc = MinioClient "localhost" 9000 "abcd1" "abcd1234" False "us-east-1"
|
||||||
|
req = RequestInfo HT.methodGet Nothing Nothing [] [] "" ""
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
name: minio-client
|
name: minio-hs
|
||||||
version: 0.1.0.0
|
version: 0.1.0.0
|
||||||
synopsis: Initial project template from stack
|
synopsis: Initial project template from stack
|
||||||
description: Please see README.md
|
description: Please see README.md
|
||||||
homepage: https://github.com/donatello/minio-client#readme
|
homepage: https://github.com/donatello/minio-hs#readme
|
||||||
license: BSD3
|
license: BSD3
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Aditya Manthramurthy
|
author: Aditya Manthramurthy
|
||||||
@ -16,32 +16,46 @@ cabal-version: >=1.10
|
|||||||
library
|
library
|
||||||
hs-source-dirs: src
|
hs-source-dirs: src
|
||||||
ghc-options: -Wall
|
ghc-options: -Wall
|
||||||
exposed-modules: Lib
|
exposed-modules: Network.Minio.Data
|
||||||
|
, Network.Minio.Data.ByteString
|
||||||
|
, Network.Minio.Data.Crypto
|
||||||
|
, Network.Minio.Data.Time
|
||||||
|
, Network.Minio.Sign.V4
|
||||||
|
, Network.Minio.API
|
||||||
other-modules: Lib.Prelude
|
other-modules: Lib.Prelude
|
||||||
build-depends: base >= 4.7 && < 5
|
build-depends: base >= 4.7 && < 5
|
||||||
, protolude >= 0.1.6 && < 0.2
|
, protolude >= 0.1.6 && < 0.2
|
||||||
, bytestring
|
, bytestring
|
||||||
|
, case-insensitive
|
||||||
|
, containers
|
||||||
, cryptonite
|
, cryptonite
|
||||||
|
, http-client
|
||||||
|
, http-conduit
|
||||||
|
, http-types
|
||||||
, text
|
, text
|
||||||
|
, time
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
default-extensions: OverloadedStrings, NoImplicitPrelude
|
default-extensions: OverloadedStrings
|
||||||
|
, NoImplicitPrelude
|
||||||
|
|
||||||
executable minio-client-exe
|
executable minio-hs-exe
|
||||||
hs-source-dirs: app
|
hs-source-dirs: app
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
|
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
|
||||||
build-depends: base
|
build-depends: base
|
||||||
, minio-client
|
, minio-hs
|
||||||
, protolude >= 0.1.6 && < 0.2
|
, protolude >= 0.1.6 && < 0.2
|
||||||
|
, http-conduit
|
||||||
|
, http-types
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
default-extensions: OverloadedStrings, NoImplicitPrelude
|
default-extensions: OverloadedStrings, NoImplicitPrelude
|
||||||
|
|
||||||
test-suite minio-client-test
|
test-suite minio-hs-test
|
||||||
type: exitcode-stdio-1.0
|
type: exitcode-stdio-1.0
|
||||||
hs-source-dirs: test
|
hs-source-dirs: test
|
||||||
main-is: Spec.hs
|
main-is: Spec.hs
|
||||||
build-depends: base
|
build-depends: base
|
||||||
, minio-client
|
, minio-hs
|
||||||
, protolude >= 0.1.6 && < 0.2
|
, protolude >= 0.1.6 && < 0.2
|
||||||
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
|
ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
@ -49,4 +63,4 @@ test-suite minio-client-test
|
|||||||
|
|
||||||
source-repository head
|
source-repository head
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/donatello/minio-client
|
location: https://github.com/donatello/minio-hs
|
||||||
55
src/Lib.hs
55
src/Lib.hs
@ -1,55 +0,0 @@
|
|||||||
{-|
|
|
||||||
Module : Lib
|
|
||||||
Description : Lib's main module
|
|
||||||
|
|
||||||
This is a haddock comment describing your library
|
|
||||||
For more information on how to write Haddock comments check the user guide:
|
|
||||||
<https://www.haskell.org/haddock/doc/html/index.html>
|
|
||||||
-}
|
|
||||||
module Lib
|
|
||||||
( someFunc
|
|
||||||
, hmacSha256
|
|
||||||
, sha256Hash
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Lib.Prelude
|
|
||||||
|
|
||||||
import Crypto.Hash (SHA256, Digest, hash)
|
|
||||||
import Crypto.MAC.HMAC (hmac, HMAC(hmacGetDigest))
|
|
||||||
|
|
||||||
-- | Prints someFunc
|
|
||||||
--
|
|
||||||
-- >>> someFunc 10
|
|
||||||
-- someFunc
|
|
||||||
someFunc :: IO ()
|
|
||||||
someFunc = putStrLn ("someFunc" :: Text)
|
|
||||||
|
|
||||||
hmacSha256 :: ByteString -> ByteString -> ByteString
|
|
||||||
hmacSha256 key message =
|
|
||||||
show (hmacGetDigest (hmac key message) :: Digest SHA256)
|
|
||||||
|
|
||||||
sha256Hash :: ByteString -> ByteString
|
|
||||||
sha256Hash payload = show (hash payload :: Digest SHA256)
|
|
||||||
|
|
||||||
{-
|
|
||||||
|
|
||||||
|
|
||||||
public static String UriEncode(CharSequence input, boolean encodeSlash) {
|
|
||||||
StringBuilder result = new StringBuilder();
|
|
||||||
for (int i = 0; i < input.length(); i++) {
|
|
||||||
char ch = input.charAt(i);
|
|
||||||
if ((ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') ||
|
|
||||||
(ch >= '0' && ch <= '9') || ch == '_' || ch == '-' || ch == '~' || ch == '.') {
|
|
||||||
result.append(ch);
|
|
||||||
} else if (ch == '/') {
|
|
||||||
result.append(encodeSlash ? "%2F" : ch);
|
|
||||||
} else {
|
|
||||||
result.append(toHexUTF8(ch));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result.toString();
|
|
||||||
}
|
|
||||||
ch 65536 -> %10000
|
|
||||||
-}
|
|
||||||
uriEncode :: Text -> Bool -> ByteString
|
|
||||||
uriEncode payload encodeSlash =
|
|
||||||
@ -9,3 +9,5 @@ module Lib.Prelude
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import Protolude as Exports
|
import Protolude as Exports
|
||||||
|
|
||||||
|
import Data.Time as Exports (UTCTime)
|
||||||
|
|||||||
38
src/Network/Minio/API.hs
Normal file
38
src/Network/Minio/API.hs
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
module Network.Minio.API
|
||||||
|
(
|
||||||
|
minioExecute
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Network.HTTP.Client (defaultManagerSettings)
|
||||||
|
import qualified Network.HTTP.Types as HT
|
||||||
|
import Network.HTTP.Conduit (Response)
|
||||||
|
import qualified Network.HTTP.Conduit as NC
|
||||||
|
|
||||||
|
import Lib.Prelude
|
||||||
|
|
||||||
|
import Network.Minio.Data
|
||||||
|
import Network.Minio.Data.Crypto
|
||||||
|
import Network.Minio.Sign.V4
|
||||||
|
|
||||||
|
minioExecute :: MinioClient -> RequestInfo -> IO (Response LByteString)
|
||||||
|
minioExecute mc ri = do
|
||||||
|
mgr <- NC.newManager defaultManagerSettings
|
||||||
|
finalHeaders <- signV4 mc updatedRI
|
||||||
|
NC.httpLbs (req finalHeaders) mgr
|
||||||
|
where
|
||||||
|
req h = NC.defaultRequest {
|
||||||
|
NC.method = method ri
|
||||||
|
, NC.secure = mcIsSecure mc
|
||||||
|
, NC.host = encodeUtf8 $ mcEndPointHost mc
|
||||||
|
, NC.port = mcEndPointPort mc
|
||||||
|
, NC.path = getPathFromRI ri
|
||||||
|
, NC.queryString = HT.renderQuery False $ queryParams ri
|
||||||
|
, NC.requestHeaders = h
|
||||||
|
, NC.requestBody = NC.RequestBodyBS (payload ri)
|
||||||
|
}
|
||||||
|
|
||||||
|
phash = hashSHA256 $ payload ri
|
||||||
|
updatedRI = ri {
|
||||||
|
payloadHash = phash
|
||||||
|
, headers = ("x-amz-content-sha256", phash) : (headers ri)
|
||||||
|
}
|
||||||
41
src/Network/Minio/Data.hs
Normal file
41
src/Network/Minio/Data.hs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
module Network.Minio.Data
|
||||||
|
(
|
||||||
|
MinioClient(..)
|
||||||
|
, RequestInfo(..)
|
||||||
|
, Bucket
|
||||||
|
, Object
|
||||||
|
, getPathFromRI
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString as B
|
||||||
|
import Network.HTTP.Types (Method, Header, Query)
|
||||||
|
|
||||||
|
import Lib.Prelude
|
||||||
|
|
||||||
|
data MinioClient = MinioClient {
|
||||||
|
mcEndPointHost :: Text
|
||||||
|
, mcEndPointPort :: Int
|
||||||
|
, mcAccessKey :: Text
|
||||||
|
, mcSecretKey :: Text
|
||||||
|
, mcIsSecure :: Bool
|
||||||
|
, mcRegion :: Text
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
type Bucket = ByteString
|
||||||
|
type Object = Text
|
||||||
|
|
||||||
|
data RequestInfo = RequestInfo {
|
||||||
|
method :: Method
|
||||||
|
, bucket :: Maybe Bucket
|
||||||
|
, object :: Maybe Object
|
||||||
|
, queryParams :: Query
|
||||||
|
, headers :: [Header]
|
||||||
|
, payload :: ByteString
|
||||||
|
, payloadHash :: ByteString
|
||||||
|
}
|
||||||
|
|
||||||
|
getPathFromRI :: RequestInfo -> ByteString
|
||||||
|
getPathFromRI ri = B.concat $ parts
|
||||||
|
where
|
||||||
|
objPart = maybe [] (\o -> ["/", encodeUtf8 o]) $ object ri
|
||||||
|
parts = maybe ["/"] (\b -> "/" : b : objPart) $ bucket ri
|
||||||
58
src/Network/Minio/Data/ByteString.hs
Normal file
58
src/Network/Minio/Data/ByteString.hs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
|
module Network.Minio.Data.ByteString
|
||||||
|
(
|
||||||
|
stripBS
|
||||||
|
, UriEncodable(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString as B
|
||||||
|
import qualified Data.ByteString.Builder as BB
|
||||||
|
import qualified Data.ByteString.Char8 as BC8
|
||||||
|
import qualified Data.ByteString.Lazy as LB
|
||||||
|
import Data.Char (isSpace, toUpper)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import Numeric (showHex)
|
||||||
|
|
||||||
|
import Lib.Prelude
|
||||||
|
|
||||||
|
stripBS :: ByteString -> ByteString
|
||||||
|
stripBS = BC8.dropWhile isSpace . fst . BC8.spanEnd isSpace
|
||||||
|
|
||||||
|
class UriEncodable s where
|
||||||
|
uriEncode :: Bool -> s -> ByteString
|
||||||
|
|
||||||
|
instance UriEncodable [Char] where
|
||||||
|
uriEncode encodeSlash payload =
|
||||||
|
LB.toStrict $ BB.toLazyByteString $ mconcat $
|
||||||
|
map (flip uriEncodeChar encodeSlash) payload
|
||||||
|
|
||||||
|
instance UriEncodable ByteString where
|
||||||
|
-- assumes that uriEncode is passed ASCII encoded strings.
|
||||||
|
uriEncode encodeSlash bs =
|
||||||
|
uriEncode encodeSlash $ BC8.unpack bs
|
||||||
|
|
||||||
|
instance UriEncodable Text where
|
||||||
|
uriEncode encodeSlash txt =
|
||||||
|
uriEncode encodeSlash $ T.unpack txt
|
||||||
|
|
||||||
|
-- | URI encode a char according to AWS S3 signing rules - see
|
||||||
|
-- UriEncode() at
|
||||||
|
-- https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
|
||||||
|
uriEncodeChar :: Char -> Bool -> BB.Builder
|
||||||
|
uriEncodeChar '/' True = BB.byteString "%2F"
|
||||||
|
uriEncodeChar '/' False = BB.char7 '/'
|
||||||
|
uriEncodeChar ch _
|
||||||
|
| (ch >= 'A' && ch <= 'Z')
|
||||||
|
|| (ch >= 'a' && ch <= 'z')
|
||||||
|
|| (ch >= '0' && ch <= '9')
|
||||||
|
|| (ch == '_')
|
||||||
|
|| (ch == '-')
|
||||||
|
|| (ch == '.')
|
||||||
|
|| (ch == '~') = BB.char7 ch
|
||||||
|
| otherwise = mconcat $ map f $ B.unpack $ encodeUtf8 $ T.singleton ch
|
||||||
|
where
|
||||||
|
f :: Word8 -> BB.Builder
|
||||||
|
f n = BB.char7 '%' <> BB.string7 hexStr
|
||||||
|
where
|
||||||
|
hexStr = map toUpper $ showHex q $ showHex r ""
|
||||||
|
(q, r) = divMod (fromIntegral n) (16::Word8)
|
||||||
17
src/Network/Minio/Data/Crypto.hs
Normal file
17
src/Network/Minio/Data/Crypto.hs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
module Network.Minio.Data.Crypto
|
||||||
|
(
|
||||||
|
hashSHA256
|
||||||
|
, hmacSHA256
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Crypto.Hash (SHA256(..), hashWith, Digest)
|
||||||
|
import Crypto.MAC.HMAC (hmac, HMAC(hmacGetDigest))
|
||||||
|
|
||||||
|
import Lib.Prelude
|
||||||
|
|
||||||
|
hashSHA256 :: ByteString -> ByteString
|
||||||
|
hashSHA256 = show . hashWith SHA256
|
||||||
|
|
||||||
|
hmacSHA256 :: ByteString -> ByteString -> ByteString
|
||||||
|
hmacSHA256 message key =
|
||||||
|
show (hmacGetDigest (hmac key message) :: Digest SHA256)
|
||||||
25
src/Network/Minio/Data/Time.hs
Normal file
25
src/Network/Minio/Data/Time.hs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
module Network.Minio.Data.Time
|
||||||
|
(
|
||||||
|
awsTimeFormat
|
||||||
|
, awsTimeFormatBS
|
||||||
|
, awsDateFormat
|
||||||
|
, awsDateFormatBS
|
||||||
|
) where
|
||||||
|
|
||||||
|
|
||||||
|
import Data.ByteString.Char8 (pack)
|
||||||
|
import qualified Data.Time as Time
|
||||||
|
|
||||||
|
import Lib.Prelude
|
||||||
|
|
||||||
|
awsTimeFormat :: UTCTime -> [Char]
|
||||||
|
awsTimeFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%dT%H%M%SZ"
|
||||||
|
|
||||||
|
awsTimeFormatBS :: UTCTime -> ByteString
|
||||||
|
awsTimeFormatBS = pack . awsTimeFormat
|
||||||
|
|
||||||
|
awsDateFormat :: UTCTime -> [Char]
|
||||||
|
awsDateFormat = Time.formatTime Time.defaultTimeLocale "%Y%m%d"
|
||||||
|
|
||||||
|
awsDateFormatBS :: UTCTime -> ByteString
|
||||||
|
awsDateFormatBS = pack . awsDateFormat
|
||||||
122
src/Network/Minio/Sign/V4.hs
Normal file
122
src/Network/Minio/Sign/V4.hs
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
module Network.Minio.Sign.V4
|
||||||
|
(
|
||||||
|
signV4
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString as B
|
||||||
|
import Data.ByteString.Char8 (pack)
|
||||||
|
import Data.CaseInsensitive (mk)
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
import qualified Data.Set as Set
|
||||||
|
import qualified Data.Time as Time
|
||||||
|
import Network.HTTP.Types (Header)
|
||||||
|
|
||||||
|
import Lib.Prelude
|
||||||
|
import Network.Minio.Data
|
||||||
|
import Network.Minio.Data.ByteString
|
||||||
|
import Network.Minio.Data.Crypto
|
||||||
|
import Network.Minio.Data.Time
|
||||||
|
|
||||||
|
ignoredHeaders :: Set ByteString
|
||||||
|
ignoredHeaders = Set.fromList $ map CI.foldedCase [
|
||||||
|
mk "Authorization",
|
||||||
|
mk "Content-Type",
|
||||||
|
mk "Content-Length",
|
||||||
|
mk "User-Agent"
|
||||||
|
]
|
||||||
|
|
||||||
|
-- | Given MinioClient and request details, including request method,
|
||||||
|
-- request path, headers, query params and payload hash, generates an
|
||||||
|
-- updated set of headers, including the x-amz-date header and the
|
||||||
|
-- Authorization header, which includes the signature.
|
||||||
|
signV4 :: MinioClient -> RequestInfo
|
||||||
|
-> IO [Header]
|
||||||
|
signV4 mc ri = do
|
||||||
|
timestamp <- Time.getCurrentTime
|
||||||
|
return $ signV4AtTime timestamp mc ri
|
||||||
|
|
||||||
|
-- | Takes a timestamp, server params and request params and generates
|
||||||
|
-- an updated list of headers.
|
||||||
|
signV4AtTime :: UTCTime -> MinioClient -> RequestInfo -> [Header]
|
||||||
|
signV4AtTime ts mc ri = authHeader : headersWithDate
|
||||||
|
where
|
||||||
|
timeBS = awsTimeFormatBS ts
|
||||||
|
dateHeader = (mk "X-Amz-Date", timeBS)
|
||||||
|
|
||||||
|
headersWithDate = dateHeader : (headers ri)
|
||||||
|
|
||||||
|
authHeader = (mk "Authorization", authHeaderValue)
|
||||||
|
|
||||||
|
scope = getScope ts mc
|
||||||
|
|
||||||
|
authHeaderValue = B.concat [
|
||||||
|
"AWS4-HMAC-SHA256 Credential=",
|
||||||
|
scope,
|
||||||
|
", SignedHeaders=", signedHeaders,
|
||||||
|
", Signature=", signature
|
||||||
|
]
|
||||||
|
|
||||||
|
headersToSign = getHeadersToSign headersWithDate
|
||||||
|
|
||||||
|
signedHeaders = B.intercalate ";" $ map fst headersToSign
|
||||||
|
|
||||||
|
signature = hmacSHA256 stringToSign signingKey
|
||||||
|
|
||||||
|
signingKey = hmacSHA256 "aws4_request"
|
||||||
|
. hmacSHA256 "s3"
|
||||||
|
. hmacSHA256 (encodeUtf8 $ mcRegion mc)
|
||||||
|
. hmacSHA256 timeBS
|
||||||
|
$ (B.concat ["AWS4", encodeUtf8 $ mcSecretKey mc])
|
||||||
|
|
||||||
|
stringToSign = B.intercalate "\n" $
|
||||||
|
["AWS4-HMAC-SHA256",
|
||||||
|
timeBS,
|
||||||
|
scope,
|
||||||
|
hashSHA256 $ canonicalRequest
|
||||||
|
]
|
||||||
|
|
||||||
|
canonicalRequest = getCanonicalRequest ri headersToSign
|
||||||
|
|
||||||
|
|
||||||
|
getScope :: UTCTime -> MinioClient -> ByteString
|
||||||
|
getScope ts mc = B.intercalate "/" $ [
|
||||||
|
encodeUtf8 (mcAccessKey mc),
|
||||||
|
pack $ Time.formatTime Time.defaultTimeLocale "%Y%m%d" ts,
|
||||||
|
"us-east-1", "s3", "aws4_request"
|
||||||
|
]
|
||||||
|
|
||||||
|
getHeadersToSign :: [Header] -> [(ByteString, ByteString)]
|
||||||
|
getHeadersToSign h =
|
||||||
|
sort $
|
||||||
|
filter (flip Set.notMember ignoredHeaders . fst) $
|
||||||
|
map (\(x, y) -> (CI.foldedCase x, stripBS y)) $
|
||||||
|
h
|
||||||
|
|
||||||
|
getCanonicalRequest :: RequestInfo -> [(ByteString, ByteString)] -> ByteString
|
||||||
|
getCanonicalRequest ri headersForSign = B.intercalate "\n" $ [
|
||||||
|
method ri,
|
||||||
|
uriEncode False path,
|
||||||
|
canonicalQueryString,
|
||||||
|
canonicalHeaders,
|
||||||
|
signedHeaders,
|
||||||
|
payloadHash ri,
|
||||||
|
""
|
||||||
|
]
|
||||||
|
where
|
||||||
|
path = B.concat $
|
||||||
|
maybe [] (\bkt -> bkt : (
|
||||||
|
maybe [] (\obj ->
|
||||||
|
["/", encodeUtf8 $ obj]) $ object ri)) $
|
||||||
|
bucket ri
|
||||||
|
|
||||||
|
canonicalQueryString = B.intercalate "&" $
|
||||||
|
map (\(x, y) -> B.concat [x, "=", y]) $
|
||||||
|
sort $ map (\(x, y) ->
|
||||||
|
(uriEncode True x, maybe "" (uriEncode True) y)) $
|
||||||
|
queryParams ri
|
||||||
|
|
||||||
|
canonicalHeaders = B.concat $
|
||||||
|
map (\(x, y) -> B.concat [x, ":", y, "\n"]) $
|
||||||
|
headersForSign
|
||||||
|
|
||||||
|
signedHeaders = B.intercalate ";" $ map fst headersForSign
|
||||||
Loading…
Reference in New Issue
Block a user