199 lines
6.3 KiB
Haskell
199 lines
6.3 KiB
Haskell
--
|
|
-- Minio Haskell SDK, (C) 2017 Minio, Inc.
|
|
--
|
|
-- Licensed under the Apache License, Version 2.0 (the "License");
|
|
-- you may not use this file except in compliance with the License.
|
|
-- You may obtain a copy of the License at
|
|
--
|
|
-- http://www.apache.org/licenses/LICENSE-2.0
|
|
--
|
|
-- Unless required by applicable law or agreed to in writing, software
|
|
-- distributed under the License is distributed on an "AS IS" BASIS,
|
|
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
-- See the License for the specific language governing permissions and
|
|
-- limitations under the License.
|
|
--
|
|
|
|
module Network.Minio.API
|
|
(
|
|
connect
|
|
, RequestInfo(..)
|
|
, runMinio
|
|
, executeRequest
|
|
, mkStreamRequest
|
|
, getLocation
|
|
|
|
, isValidBucketName
|
|
, checkBucketNameValidity
|
|
, isValidObjectName
|
|
, checkObjectNameValidity
|
|
) where
|
|
|
|
import qualified Data.Conduit as C
|
|
import Data.Conduit.Binary (sourceHandleRange)
|
|
import Data.Default (def)
|
|
import qualified Data.Map as Map
|
|
import qualified Data.Char as C
|
|
import qualified Data.Text as T
|
|
import qualified Data.ByteString as B
|
|
import Network.HTTP.Conduit (Response)
|
|
import qualified Network.HTTP.Conduit as NC
|
|
import qualified Network.HTTP.Types as HT
|
|
|
|
import Lib.Prelude
|
|
|
|
import Network.Minio.Data
|
|
import Network.Minio.Data.Crypto
|
|
import Network.Minio.Errors
|
|
import Network.Minio.Sign.V4
|
|
import Network.Minio.Utils
|
|
import Network.Minio.XmlParser
|
|
|
|
sha256Header :: ByteString -> HT.Header
|
|
sha256Header = ("x-amz-content-sha256", )
|
|
|
|
getPayloadSHA256Hash :: (MonadIO m) => Payload -> m ByteString
|
|
getPayloadSHA256Hash (PayloadBS bs) = return $ hashSHA256 bs
|
|
getPayloadSHA256Hash (PayloadH h off size) = hashSHA256FromSource $
|
|
sourceHandleRange h
|
|
(return . fromIntegral $ off)
|
|
(return . fromIntegral $ size)
|
|
|
|
getRequestBody :: Payload -> NC.RequestBody
|
|
getRequestBody (PayloadBS bs) = NC.RequestBodyBS bs
|
|
getRequestBody (PayloadH h off size) =
|
|
NC.requestBodySource (fromIntegral size) $
|
|
sourceHandleRange h
|
|
(return . fromIntegral $ off)
|
|
(return . fromIntegral $ size)
|
|
|
|
|
|
-- | Fetch bucket location (region)
|
|
getLocation :: Bucket -> Minio Region
|
|
getLocation bucket = do
|
|
resp <- executeRequest $ def {
|
|
riBucket = Just bucket
|
|
, riQueryParams = [("location", Nothing)]
|
|
, riNeedsLocation = False
|
|
}
|
|
parseLocation $ NC.responseBody resp
|
|
|
|
|
|
-- | Looks for region in RegionMap and updates it using getLocation if
|
|
-- absent.
|
|
discoverRegion :: RequestInfo -> Minio (Maybe Region)
|
|
discoverRegion ri = runMaybeT $ do
|
|
bucket <- MaybeT $ return $ riBucket ri
|
|
regionMay <- gets (Map.lookup bucket)
|
|
maybe (do
|
|
l <- lift $ getLocation bucket
|
|
modify $ Map.insert bucket l
|
|
return l
|
|
) return regionMay
|
|
|
|
|
|
buildRequest :: RequestInfo -> Minio NC.Request
|
|
buildRequest ri = do
|
|
maybe (return ()) checkBucketNameValidity $ riBucket ri
|
|
maybe (return ()) checkObjectNameValidity $ riObject ri
|
|
|
|
ci <- asks mcConnInfo
|
|
|
|
-- getService/makeBucket/getLocation -- don't need
|
|
-- location
|
|
region <- 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
|
|
|
|
regionHost <- case region of
|
|
Nothing -> return $ connectHost ci
|
|
Just r -> if "amazonaws.com" `T.isSuffixOf` connectHost ci
|
|
then maybe
|
|
(throwM $ MErrVRegionNotSupported r)
|
|
return
|
|
(Map.lookup r awsRegionMap)
|
|
else return $ connectHost ci
|
|
|
|
|
|
sha256Hash <- getPayloadSHA256Hash (riPayload ri)
|
|
let newRi = ri { riPayloadHash = sha256Hash
|
|
, riHeaders = sha256Header sha256Hash : riHeaders ri
|
|
, riRegion = region
|
|
}
|
|
newCi = ci { connectHost = regionHost }
|
|
|
|
reqHeaders <- liftIO $ signV4 newCi newRi
|
|
|
|
return NC.defaultRequest {
|
|
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 = reqHeaders
|
|
, NC.requestBody = getRequestBody (riPayload newRi)
|
|
}
|
|
|
|
executeRequest :: RequestInfo -> Minio (Response LByteString)
|
|
executeRequest ri = do
|
|
req <- buildRequest ri
|
|
mgr <- asks mcConnManager
|
|
httpLbs req mgr
|
|
|
|
|
|
mkStreamRequest :: RequestInfo
|
|
-> Minio (Response (C.ResumableSource Minio ByteString))
|
|
mkStreamRequest ri = do
|
|
req <- buildRequest ri
|
|
mgr <- asks mcConnManager
|
|
http req mgr
|
|
|
|
-- Bucket name validity check according to AWS rules.
|
|
isValidBucketName :: Bucket -> Bool
|
|
isValidBucketName bucket =
|
|
not (or [ len < 3 || len > 63
|
|
, or (map labelCheck labels)
|
|
, or (map labelCharsCheck labels)
|
|
, isIPCheck
|
|
])
|
|
where
|
|
len = T.length bucket
|
|
labels = T.splitOn "." bucket
|
|
|
|
-- does label `l` fail basic checks of length and start/end?
|
|
labelCheck l = T.length l == 0 || T.head l == '-' || T.last l == '-'
|
|
|
|
-- does label `l` have non-allowed characters?
|
|
labelCharsCheck l = isJust $ T.find (\x -> not (C.isAsciiLower x ||
|
|
x == '-' ||
|
|
C.isDigit x)) l
|
|
|
|
-- does label `l` have non-digit characters?
|
|
labelNonDigits l = isJust $ T.find (not . C.isDigit) l
|
|
labelAsNums = map (not . labelNonDigits) labels
|
|
|
|
-- check if bucket name looks like an IP
|
|
isIPCheck = and labelAsNums && length labelAsNums == 4
|
|
|
|
-- Throws exception iff bucket name is invalid according to AWS rules.
|
|
checkBucketNameValidity :: MonadThrow m => Bucket -> m ()
|
|
checkBucketNameValidity bucket =
|
|
when (not $ isValidBucketName bucket) $
|
|
throwM $ MErrVInvalidBucketName bucket
|
|
|
|
isValidObjectName :: Object -> Bool
|
|
isValidObjectName object =
|
|
T.length object > 0 && B.length (encodeUtf8 object) <= 1024
|
|
|
|
checkObjectNameValidity :: MonadThrow m => Object -> m ()
|
|
checkObjectNameValidity object =
|
|
when (not $ isValidObjectName object) $
|
|
throwM $ MErrVInvalidObjectName object
|