Verify() was being called by caller after the data has been successfully read after io.EOF. This disconnection opens a race under concurrent access to such an object. Verification is not necessary outside of Read() call, we can simply just do checksum verification right inside Read() call at io.EOF. This approach simplifies the usage.
951 lines
28 KiB
Go
951 lines
28 KiB
Go
/*
|
|
* Minio Cloud Storage, (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.
|
|
*/
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"io"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
|
|
router "github.com/gorilla/mux"
|
|
"github.com/minio/minio-go/pkg/policy"
|
|
"github.com/minio/minio/pkg/hash"
|
|
)
|
|
|
|
// GetObjectHandler - GET Object
|
|
// ----------
|
|
// This implementation of the GET operation retrieves object. To use GET,
|
|
// you must have READ access to the object.
|
|
func (api gatewayAPIHandlers) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|
var object, bucket string
|
|
vars := router.Vars(r)
|
|
bucket = vars["bucket"]
|
|
object = vars["object"]
|
|
|
|
// Fetch object stat info.
|
|
objectAPI := api.ObjectAPI()
|
|
if objectAPI == nil {
|
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
|
return
|
|
}
|
|
|
|
reqAuthType := getRequestAuthType(r)
|
|
|
|
switch reqAuthType {
|
|
case authTypePresignedV2, authTypeSignedV2:
|
|
// Signature V2 validation.
|
|
s3Error := isReqAuthenticatedV2(r)
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypeSigned, authTypePresigned:
|
|
s3Error := isReqAuthenticated(r, serverConfig.GetRegion())
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypeAnonymous:
|
|
// No verification needed for anonymous requests.
|
|
default:
|
|
// For all unknown auth types return error.
|
|
writeErrorResponse(w, ErrAccessDenied, r.URL)
|
|
return
|
|
}
|
|
|
|
getObjectInfo := objectAPI.GetObjectInfo
|
|
if reqAuthType == authTypeAnonymous {
|
|
getObjectInfo = objectAPI.AnonGetObjectInfo
|
|
}
|
|
objInfo, err := getObjectInfo(bucket, object)
|
|
if err != nil {
|
|
errorIf(err, "Unable to fetch object info.")
|
|
apiErr := toAPIErrorCode(err)
|
|
if apiErr == ErrNoSuchKey {
|
|
apiErr = errAllowableObjectNotFound(bucket, r)
|
|
}
|
|
writeErrorResponse(w, apiErr, r.URL)
|
|
return
|
|
}
|
|
|
|
// Get request range.
|
|
var hrange *httpRange
|
|
rangeHeader := r.Header.Get("Range")
|
|
if rangeHeader != "" {
|
|
if hrange, err = parseRequestRange(rangeHeader, objInfo.Size); err != nil {
|
|
// Handle only errInvalidRange
|
|
// Ignore other parse error and treat it as regular Get request like Amazon S3.
|
|
if err == errInvalidRange {
|
|
writeErrorResponse(w, ErrInvalidRange, r.URL)
|
|
return
|
|
}
|
|
|
|
// log the error.
|
|
errorIf(err, "Invalid request range")
|
|
}
|
|
}
|
|
|
|
// Validate pre-conditions if any.
|
|
if checkPreconditions(w, r, objInfo) {
|
|
return
|
|
}
|
|
|
|
// Get the object.
|
|
var startOffset int64
|
|
length := objInfo.Size
|
|
if hrange != nil {
|
|
startOffset = hrange.offsetBegin
|
|
length = hrange.getLength()
|
|
}
|
|
// Indicates if any data was written to the http.ResponseWriter
|
|
dataWritten := false
|
|
// io.Writer type which keeps track if any data was written.
|
|
writer := funcToWriter(func(p []byte) (int, error) {
|
|
if !dataWritten {
|
|
// Set headers on the first write.
|
|
// Set standard object headers.
|
|
setObjectHeaders(w, objInfo, hrange)
|
|
|
|
// Set any additional requested response headers.
|
|
setHeadGetRespHeaders(w, r.URL.Query())
|
|
|
|
dataWritten = true
|
|
}
|
|
return w.Write(p)
|
|
})
|
|
|
|
getObject := objectAPI.GetObject
|
|
if reqAuthType == authTypeAnonymous {
|
|
getObject = objectAPI.AnonGetObject
|
|
}
|
|
|
|
// Reads the object at startOffset and writes to mw.
|
|
if err = getObject(bucket, object, startOffset, length, writer); err != nil {
|
|
errorIf(err, "Unable to write to client.")
|
|
if !dataWritten {
|
|
// Error response only if no data has been written to client yet. i.e if
|
|
// partial data has already been written before an error
|
|
// occurred then no point in setting StatusCode and
|
|
// sending error XML.
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
}
|
|
return
|
|
}
|
|
if !dataWritten {
|
|
// If ObjectAPI.GetObject did not return error and no data has
|
|
// been written it would mean that it is a 0-byte object.
|
|
// call wrter.Write(nil) to set appropriate headers.
|
|
writer.Write(nil)
|
|
}
|
|
|
|
// Get host and port from Request.RemoteAddr.
|
|
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
host, port = "", ""
|
|
}
|
|
|
|
// Notify object accessed via a GET request.
|
|
eventNotify(eventData{
|
|
Type: ObjectAccessedGet,
|
|
Bucket: bucket,
|
|
ObjInfo: objInfo,
|
|
ReqParams: extractReqParams(r),
|
|
UserAgent: r.UserAgent(),
|
|
Host: host,
|
|
Port: port,
|
|
})
|
|
}
|
|
|
|
// PutObjectHandler - PUT Object
|
|
// ----------
|
|
// This implementation of the PUT operation adds an object to a bucket.
|
|
func (api gatewayAPIHandlers) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|
objectAPI := api.ObjectAPI()
|
|
if objectAPI == nil {
|
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
|
return
|
|
}
|
|
|
|
// X-Amz-Copy-Source shouldn't be set for this call.
|
|
if _, ok := r.Header["X-Amz-Copy-Source"]; ok {
|
|
writeErrorResponse(w, ErrInvalidCopySource, r.URL)
|
|
return
|
|
}
|
|
|
|
var object, bucket string
|
|
vars := router.Vars(r)
|
|
bucket = vars["bucket"]
|
|
object = vars["object"]
|
|
|
|
// TODO: we should validate the object name here
|
|
|
|
// Get Content-Md5 sent by client and verify if valid
|
|
md5Bytes, err := checkValidMD5(r.Header.Get("Content-Md5"))
|
|
if err != nil {
|
|
errorIf(err, "Unable to validate content-md5 format.")
|
|
writeErrorResponse(w, ErrInvalidDigest, r.URL)
|
|
return
|
|
}
|
|
|
|
/// if Content-Length is unknown/missing, deny the request
|
|
size := r.ContentLength
|
|
reqAuthType := getRequestAuthType(r)
|
|
if reqAuthType == authTypeStreamingSigned {
|
|
sizeStr := r.Header.Get("x-amz-decoded-content-length")
|
|
size, err = strconv.ParseInt(sizeStr, 10, 64)
|
|
if err != nil {
|
|
errorIf(err, "Unable to parse `x-amz-decoded-content-length` into its integer value", sizeStr)
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
}
|
|
if size == -1 {
|
|
writeErrorResponse(w, ErrMissingContentLength, r.URL)
|
|
return
|
|
}
|
|
|
|
/// maximum Upload size for objects in a single operation
|
|
if isMaxObjectSize(size) {
|
|
writeErrorResponse(w, ErrEntityTooLarge, r.URL)
|
|
return
|
|
}
|
|
|
|
// Extract metadata to be saved from incoming HTTP header.
|
|
metadata, err := extractMetadataFromHeader(r.Header)
|
|
if err != nil {
|
|
errorIf(err, "found invalid http request header")
|
|
writeErrorResponse(w, ErrInternalError, r.URL)
|
|
return
|
|
}
|
|
if reqAuthType == authTypeStreamingSigned {
|
|
if contentEncoding, ok := metadata["content-encoding"]; ok {
|
|
contentEncoding = trimAwsChunkedContentEncoding(contentEncoding)
|
|
if contentEncoding != "" {
|
|
// Make sure to trim and save the content-encoding
|
|
// parameter for a streaming signature which is set
|
|
// to a custom value for example: "aws-chunked,gzip".
|
|
metadata["content-encoding"] = contentEncoding
|
|
} else {
|
|
// Trimmed content encoding is empty when the header
|
|
// value is set to "aws-chunked" only.
|
|
|
|
// Make sure to delete the content-encoding parameter
|
|
// for a streaming signature which is set to value
|
|
// for example: "aws-chunked"
|
|
delete(metadata, "content-encoding")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lock the object.
|
|
objectLock := globalNSMutex.NewNSLock(bucket, object)
|
|
if objectLock.GetLock(globalOperationTimeout) != nil {
|
|
writeErrorResponse(w, ErrOperationTimedOut, r.URL)
|
|
return
|
|
}
|
|
defer objectLock.Unlock()
|
|
|
|
var (
|
|
// Make sure we hex encode md5sum here.
|
|
md5hex = hex.EncodeToString(md5Bytes)
|
|
sha256hex = ""
|
|
putObject = objectAPI.PutObject
|
|
reader = r.Body
|
|
)
|
|
|
|
switch reqAuthType {
|
|
default:
|
|
// For all unknown auth types return error.
|
|
writeErrorResponse(w, ErrAccessDenied, r.URL)
|
|
return
|
|
case authTypeAnonymous:
|
|
putObject = objectAPI.AnonPutObject
|
|
case authTypeStreamingSigned:
|
|
// Initialize stream signature verifier.
|
|
var s3Error APIErrorCode
|
|
reader, s3Error = newSignV4ChunkedReader(r)
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypeSignedV2, authTypePresignedV2:
|
|
s3Error := isReqAuthenticatedV2(r)
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypePresigned, authTypeSigned:
|
|
if s3Error := reqSignatureV4Verify(r, serverConfig.GetRegion()); s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
if !skipContentSha256Cksum(r) {
|
|
sha256hex = getContentSha256Cksum(r)
|
|
}
|
|
}
|
|
|
|
hashReader, err := hash.NewReader(reader, size, md5hex, sha256hex)
|
|
if err != nil {
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
|
|
objInfo, err := putObject(bucket, object, hashReader, metadata)
|
|
if err != nil {
|
|
errorIf(err, "Unable to save an object %s", r.URL.Path)
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
|
|
w.Header().Set("ETag", "\""+objInfo.ETag+"\"")
|
|
writeSuccessResponseHeadersOnly(w)
|
|
|
|
// Get host and port from Request.RemoteAddr.
|
|
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
host, port = "", ""
|
|
}
|
|
|
|
// Notify object created event.
|
|
eventNotify(eventData{
|
|
Type: ObjectCreatedPut,
|
|
Bucket: bucket,
|
|
ObjInfo: objInfo,
|
|
ReqParams: extractReqParams(r),
|
|
UserAgent: r.UserAgent(),
|
|
Host: host,
|
|
Port: port,
|
|
})
|
|
}
|
|
|
|
// HeadObjectHandler - HEAD Object
|
|
// -----------
|
|
// The HEAD operation retrieves metadata from an object without returning the object itself.
|
|
func (api gatewayAPIHandlers) HeadObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|
var object, bucket string
|
|
vars := router.Vars(r)
|
|
bucket = vars["bucket"]
|
|
object = vars["object"]
|
|
|
|
objectAPI := api.ObjectAPI()
|
|
if objectAPI == nil {
|
|
writeErrorResponseHeadersOnly(w, ErrServerNotInitialized)
|
|
return
|
|
}
|
|
|
|
reqAuthType := getRequestAuthType(r)
|
|
|
|
switch reqAuthType {
|
|
case authTypePresignedV2, authTypeSignedV2:
|
|
// Signature V2 validation.
|
|
s3Error := isReqAuthenticatedV2(r)
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypeSigned, authTypePresigned:
|
|
s3Error := isReqAuthenticated(r, serverConfig.GetRegion())
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypeAnonymous:
|
|
// No verification needed for anonymous requests.
|
|
default:
|
|
// For all unknown auth types return error.
|
|
writeErrorResponse(w, ErrAccessDenied, r.URL)
|
|
return
|
|
}
|
|
|
|
getObjectInfo := objectAPI.GetObjectInfo
|
|
if reqAuthType == authTypeAnonymous {
|
|
getObjectInfo = objectAPI.AnonGetObjectInfo
|
|
}
|
|
objInfo, err := getObjectInfo(bucket, object)
|
|
if err != nil {
|
|
errorIf(err, "Unable to fetch object info.")
|
|
apiErr := toAPIErrorCode(err)
|
|
if apiErr == ErrNoSuchKey {
|
|
apiErr = errAllowableObjectNotFound(bucket, r)
|
|
}
|
|
writeErrorResponse(w, apiErr, r.URL)
|
|
return
|
|
}
|
|
|
|
// Validate pre-conditions if any.
|
|
if checkPreconditions(w, r, objInfo) {
|
|
return
|
|
}
|
|
|
|
// Set standard object headers.
|
|
setObjectHeaders(w, objInfo, nil)
|
|
|
|
// Set any additional requested response headers.
|
|
setHeadGetRespHeaders(w, r.URL.Query())
|
|
|
|
// Successful response.
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
// Get host and port from Request.RemoteAddr.
|
|
host, port, err := net.SplitHostPort(r.RemoteAddr)
|
|
if err != nil {
|
|
host, port = "", ""
|
|
}
|
|
|
|
// Notify object accessed via a HEAD request.
|
|
eventNotify(eventData{
|
|
Type: ObjectAccessedHead,
|
|
Bucket: bucket,
|
|
ObjInfo: objInfo,
|
|
ReqParams: extractReqParams(r),
|
|
UserAgent: r.UserAgent(),
|
|
Host: host,
|
|
Port: port,
|
|
})
|
|
}
|
|
|
|
// PutBucketPolicyHandler - PUT Bucket policy
|
|
// -----------------
|
|
// This implementation of the PUT operation uses the policy
|
|
// subresource to add to or replace a policy on a bucket
|
|
func (api gatewayAPIHandlers) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
|
objAPI := api.ObjectAPI()
|
|
if objAPI == nil {
|
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
|
return
|
|
}
|
|
|
|
if s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion()); s3Error != ErrNone {
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
|
|
vars := router.Vars(r)
|
|
bucket := vars["bucket"]
|
|
|
|
// Before proceeding validate if bucket exists.
|
|
_, err := objAPI.GetBucketInfo(bucket)
|
|
if err != nil {
|
|
errorIf(err, "Unable to find bucket info.")
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
|
|
// If Content-Length is unknown or zero, deny the
|
|
// request. PutBucketPolicy always needs a Content-Length.
|
|
if r.ContentLength == -1 || r.ContentLength == 0 {
|
|
writeErrorResponse(w, ErrMissingContentLength, r.URL)
|
|
return
|
|
}
|
|
// If Content-Length is greater than maximum allowed policy size.
|
|
if r.ContentLength > maxAccessPolicySize {
|
|
writeErrorResponse(w, ErrEntityTooLarge, r.URL)
|
|
return
|
|
}
|
|
|
|
// Read access policy up to maxAccessPolicySize.
|
|
// http://docs.aws.amazon.com/AmazonS3/latest/dev/access-policy-language-overview.html
|
|
// bucket policies are limited to 20KB in size, using a limit reader.
|
|
policyBytes, err := ioutil.ReadAll(io.LimitReader(r.Body, maxAccessPolicySize))
|
|
if err != nil {
|
|
errorIf(err, "Unable to read from client.")
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
|
|
policyInfo := policy.BucketAccessPolicy{}
|
|
if err = json.Unmarshal(policyBytes, &policyInfo); err != nil {
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
|
|
if err = objAPI.SetBucketPolicies(bucket, policyInfo); err != nil {
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
// Success.
|
|
writeSuccessNoContent(w)
|
|
}
|
|
|
|
// DeleteBucketPolicyHandler - DELETE Bucket policy
|
|
// -----------------
|
|
// This implementation of the DELETE operation uses the policy
|
|
// subresource to add to remove a policy on a bucket.
|
|
func (api gatewayAPIHandlers) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
|
objAPI := api.ObjectAPI()
|
|
if objAPI == nil {
|
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
|
return
|
|
}
|
|
|
|
if s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion()); s3Error != ErrNone {
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
|
|
vars := router.Vars(r)
|
|
bucket := vars["bucket"]
|
|
|
|
// Before proceeding validate if bucket exists.
|
|
_, err := objAPI.GetBucketInfo(bucket)
|
|
if err != nil {
|
|
errorIf(err, "Unable to find bucket info.")
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Delete bucket access policy, by passing an empty policy
|
|
// struct.
|
|
objAPI.DeleteBucketPolicies(bucket)
|
|
// Success.
|
|
writeSuccessNoContent(w)
|
|
}
|
|
|
|
// GetBucketPolicyHandler - GET Bucket policy
|
|
// -----------------
|
|
// This operation uses the policy
|
|
// subresource to return the policy of a specified bucket.
|
|
func (api gatewayAPIHandlers) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
|
|
objAPI := api.ObjectAPI()
|
|
if objAPI == nil {
|
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
|
return
|
|
}
|
|
|
|
if s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion()); s3Error != ErrNone {
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
|
|
vars := router.Vars(r)
|
|
bucket := vars["bucket"]
|
|
|
|
// Before proceeding validate if bucket exists.
|
|
_, err := objAPI.GetBucketInfo(bucket)
|
|
if err != nil {
|
|
errorIf(err, "Unable to find bucket info.")
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
|
|
bp, err := objAPI.GetBucketPolicies(bucket)
|
|
if err != nil {
|
|
errorIf(err, "Unable to read bucket policy.")
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
|
|
policyBytes, err := json.Marshal(bp)
|
|
if err != nil {
|
|
errorIf(err, "Unable to read bucket policy.")
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
// Write to client.
|
|
w.Write(policyBytes)
|
|
}
|
|
|
|
// GetBucketNotificationHandler - This implementation of the GET
|
|
// operation uses the notification subresource to return the
|
|
// notification configuration of a bucket. If notifications are
|
|
// not enabled on the bucket, the operation returns an empty
|
|
// NotificationConfiguration element.
|
|
func (api gatewayAPIHandlers) GetBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
|
writeErrorResponse(w, ErrNotImplemented, r.URL)
|
|
}
|
|
|
|
// PutBucketNotificationHandler - Minio notification feature enables
|
|
// you to receive notifications when certain events happen in your bucket.
|
|
// Using this API, you can replace an existing notification configuration.
|
|
// The configuration is an XML file that defines the event types that you
|
|
// want Minio to publish and the destination where you want Minio to publish
|
|
// an event notification when it detects an event of the specified type.
|
|
// By default, your bucket has no event notifications configured. That is,
|
|
// the notification configuration will be an empty NotificationConfiguration.
|
|
func (api gatewayAPIHandlers) PutBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
|
writeErrorResponse(w, ErrNotImplemented, r.URL)
|
|
}
|
|
|
|
// ListenBucketNotificationHandler - list bucket notifications.
|
|
func (api gatewayAPIHandlers) ListenBucketNotificationHandler(w http.ResponseWriter, r *http.Request) {
|
|
writeErrorResponse(w, ErrNotImplemented, r.URL)
|
|
}
|
|
|
|
// PutBucketHandler - PUT Bucket
|
|
// ----------
|
|
// This implementation of the PUT operation creates a new bucket for authenticated request
|
|
func (api gatewayAPIHandlers) PutBucketHandler(w http.ResponseWriter, r *http.Request) {
|
|
objectAPI := api.ObjectAPI()
|
|
if objectAPI == nil {
|
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
|
return
|
|
}
|
|
|
|
// PutBucket does not have any bucket action.
|
|
s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion())
|
|
if s3Error != ErrNone {
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
|
|
vars := router.Vars(r)
|
|
bucket := vars["bucket"]
|
|
|
|
// Validate if incoming location constraint is valid, reject
|
|
// requests which do not follow valid region requirements.
|
|
location, s3Error := parseLocationConstraint(r)
|
|
if s3Error != ErrNone {
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
|
|
bucketLock := globalNSMutex.NewNSLock(bucket, "")
|
|
if bucketLock.GetLock(globalOperationTimeout) != nil {
|
|
writeErrorResponse(w, ErrOperationTimedOut, r.URL)
|
|
return
|
|
}
|
|
defer bucketLock.Unlock()
|
|
|
|
// Proceed to creating a bucket.
|
|
err := objectAPI.MakeBucketWithLocation(bucket, location)
|
|
if err != nil {
|
|
errorIf(err, "Unable to create a bucket.")
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Make sure to add Location information here only for bucket
|
|
w.Header().Set("Location", getLocation(r))
|
|
|
|
writeSuccessResponseHeadersOnly(w)
|
|
}
|
|
|
|
// DeleteBucketHandler - Delete bucket
|
|
func (api gatewayAPIHandlers) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
|
objectAPI := api.ObjectAPI()
|
|
if objectAPI == nil {
|
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
|
return
|
|
}
|
|
|
|
// DeleteBucket does not have any bucket action.
|
|
if s3Error := checkRequestAuthType(r, "", "", serverConfig.GetRegion()); s3Error != ErrNone {
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
|
|
vars := router.Vars(r)
|
|
bucket := vars["bucket"]
|
|
|
|
// Attempt to delete bucket.
|
|
if err := objectAPI.DeleteBucket(bucket); err != nil {
|
|
errorIf(err, "Unable to delete a bucket.")
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Write success response.
|
|
writeSuccessNoContent(w)
|
|
}
|
|
|
|
// ListObjectsV1Handler - GET Bucket (List Objects) Version 1.
|
|
// --------------------------
|
|
// This implementation of the GET operation returns some or all (up to 1000)
|
|
// of the objects in a bucket. You can use the request parameters as selection
|
|
// criteria to return a subset of the objects in a bucket.
|
|
//
|
|
func (api gatewayAPIHandlers) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
|
|
vars := router.Vars(r)
|
|
bucket := vars["bucket"]
|
|
|
|
objectAPI := api.ObjectAPI()
|
|
if objectAPI == nil {
|
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
|
return
|
|
}
|
|
|
|
reqAuthType := getRequestAuthType(r)
|
|
|
|
switch reqAuthType {
|
|
case authTypePresignedV2, authTypeSignedV2:
|
|
// Signature V2 validation.
|
|
s3Error := isReqAuthenticatedV2(r)
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypeSigned, authTypePresigned:
|
|
s3Error := isReqAuthenticated(r, serverConfig.GetRegion())
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypeAnonymous:
|
|
// No verification needed for anonymous requests.
|
|
default:
|
|
// For all unknown auth types return error.
|
|
writeErrorResponse(w, ErrAccessDenied, r.URL)
|
|
return
|
|
}
|
|
|
|
// Extract all the listObjectsV1 query params to their native
|
|
// values. N B We delegate validation of params to respective
|
|
// gateway backends.
|
|
prefix, marker, delimiter, maxKeys, _ := getListObjectsV1Args(r.URL.Query())
|
|
|
|
// Validate the maxKeys lowerbound. When maxKeys > 1000, S3 returns 1000 but
|
|
// does not throw an error.
|
|
if maxKeys < 0 {
|
|
writeErrorResponse(w, ErrInvalidMaxKeys, r.URL)
|
|
return
|
|
}
|
|
|
|
listObjects := objectAPI.ListObjects
|
|
if reqAuthType == authTypeAnonymous {
|
|
listObjects = objectAPI.AnonListObjects
|
|
}
|
|
// Inititate a list objects operation based on the input params.
|
|
// On success would return back ListObjectsInfo object to be
|
|
// marshalled into S3 compatible XML header.
|
|
listObjectsInfo, err := listObjects(bucket, prefix, marker, delimiter, maxKeys)
|
|
if err != nil {
|
|
errorIf(err, "Unable to list objects.")
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
response := generateListObjectsV1Response(bucket, prefix, marker, delimiter, maxKeys, listObjectsInfo)
|
|
// Write success response.
|
|
writeSuccessResponseXML(w, encodeResponse(response))
|
|
}
|
|
|
|
// ListObjectsV2Handler - GET Bucket (List Objects) Version 2.
|
|
// --------------------------
|
|
// This implementation of the GET operation returns some or all (up to 1000)
|
|
// of the objects in a bucket. You can use the request parameters as selection
|
|
// criteria to return a subset of the objects in a bucket.
|
|
//
|
|
// NOTE: It is recommended that this API to be used for application development.
|
|
// Minio continues to support ListObjectsV1 for supporting legacy tools.
|
|
func (api gatewayAPIHandlers) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
|
|
vars := router.Vars(r)
|
|
bucket := vars["bucket"]
|
|
|
|
objectAPI := api.ObjectAPI()
|
|
if objectAPI == nil {
|
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
|
return
|
|
}
|
|
|
|
reqAuthType := getRequestAuthType(r)
|
|
|
|
switch reqAuthType {
|
|
case authTypePresignedV2, authTypeSignedV2:
|
|
// Signature V2 validation.
|
|
s3Error := isReqAuthenticatedV2(r)
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypeSigned, authTypePresigned:
|
|
s3Error := isReqAuthenticated(r, serverConfig.GetRegion())
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypeAnonymous:
|
|
// No verification needed for anonymous requests.
|
|
default:
|
|
// For all unknown auth types return error.
|
|
writeErrorResponse(w, ErrAccessDenied, r.URL)
|
|
return
|
|
}
|
|
|
|
// Extract all the listObjectsV2 query params to their native values.
|
|
prefix, token, startAfter, delimiter, fetchOwner, maxKeys, _ := getListObjectsV2Args(r.URL.Query())
|
|
|
|
// In ListObjectsV2 'continuation-token' is the marker.
|
|
marker := token
|
|
// Check if 'continuation-token' is empty.
|
|
if token == "" {
|
|
// Then we need to use 'start-after' as marker instead.
|
|
marker = startAfter
|
|
}
|
|
|
|
listObjectsV2 := objectAPI.ListObjectsV2
|
|
if reqAuthType == authTypeAnonymous {
|
|
listObjectsV2 = objectAPI.AnonListObjectsV2
|
|
}
|
|
|
|
// Validate the query params before beginning to serve the request.
|
|
// fetch-owner is not validated since it is a boolean
|
|
if s3Error := validateGatewayListObjectsV2Args(prefix, marker, delimiter, maxKeys); s3Error != ErrNone {
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
// Inititate a list objects operation based on the input params.
|
|
// On success would return back ListObjectsV2Info object to be
|
|
// serialized as XML and sent as S3 compatible response body.
|
|
listObjectsV2Info, err := listObjectsV2(bucket, prefix, token, delimiter, maxKeys, fetchOwner, startAfter)
|
|
if err != nil {
|
|
errorIf(err, "Unable to list objects. Args to listObjectsV2 are bucket=%s, prefix=%s, token=%s, delimiter=%s", bucket, prefix, token, delimiter)
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
|
|
response := generateListObjectsV2Response(bucket, prefix, token, listObjectsV2Info.NextContinuationToken, startAfter, delimiter, fetchOwner, listObjectsV2Info.IsTruncated, maxKeys, listObjectsV2Info.Objects, listObjectsV2Info.Prefixes)
|
|
// Write success response.
|
|
writeSuccessResponseXML(w, encodeResponse(response))
|
|
}
|
|
|
|
// HeadBucketHandler - HEAD Bucket
|
|
// ----------
|
|
// This operation is useful to determine if a bucket exists.
|
|
// The operation returns a 200 OK if the bucket exists and you
|
|
// have permission to access it. Otherwise, the operation might
|
|
// return responses such as 404 Not Found and 403 Forbidden.
|
|
func (api gatewayAPIHandlers) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
|
|
vars := router.Vars(r)
|
|
bucket := vars["bucket"]
|
|
|
|
objectAPI := api.ObjectAPI()
|
|
if objectAPI == nil {
|
|
writeErrorResponseHeadersOnly(w, ErrServerNotInitialized)
|
|
return
|
|
}
|
|
|
|
reqAuthType := getRequestAuthType(r)
|
|
|
|
switch reqAuthType {
|
|
case authTypePresignedV2, authTypeSignedV2:
|
|
// Signature V2 validation.
|
|
s3Error := isReqAuthenticatedV2(r)
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypeSigned, authTypePresigned:
|
|
s3Error := isReqAuthenticated(r, serverConfig.GetRegion())
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypeAnonymous:
|
|
// No verification needed for anonymous requests.
|
|
default:
|
|
// For all unknown auth types return error.
|
|
writeErrorResponse(w, ErrAccessDenied, r.URL)
|
|
return
|
|
}
|
|
|
|
getBucketInfo := objectAPI.GetBucketInfo
|
|
if reqAuthType == authTypeAnonymous {
|
|
getBucketInfo = objectAPI.AnonGetBucketInfo
|
|
}
|
|
|
|
if _, err := getBucketInfo(bucket); err != nil {
|
|
errorIf(err, "Unable to fetch bucket info.")
|
|
writeErrorResponseHeadersOnly(w, toAPIErrorCode(err))
|
|
return
|
|
}
|
|
|
|
writeSuccessResponseHeadersOnly(w)
|
|
}
|
|
|
|
// GetBucketLocationHandler - GET Bucket location.
|
|
// -------------------------
|
|
// This operation returns bucket location.
|
|
func (api gatewayAPIHandlers) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
|
|
vars := router.Vars(r)
|
|
bucket := vars["bucket"]
|
|
|
|
objectAPI := api.ObjectAPI()
|
|
if objectAPI == nil {
|
|
writeErrorResponse(w, ErrServerNotInitialized, r.URL)
|
|
return
|
|
}
|
|
reqAuthType := getRequestAuthType(r)
|
|
|
|
switch reqAuthType {
|
|
case authTypePresignedV2, authTypeSignedV2:
|
|
// Signature V2 validation.
|
|
s3Error := isReqAuthenticatedV2(r)
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypeSigned, authTypePresigned:
|
|
s3Error := isReqAuthenticated(r, globalMinioDefaultRegion)
|
|
if s3Error == ErrInvalidRegion {
|
|
// Clients like boto3 send getBucketLocation() call signed with region that is configured.
|
|
s3Error = isReqAuthenticated(r, serverConfig.GetRegion())
|
|
}
|
|
if s3Error != ErrNone {
|
|
errorIf(errSignatureMismatch, "%s", dumpRequest(r))
|
|
writeErrorResponse(w, s3Error, r.URL)
|
|
return
|
|
}
|
|
case authTypeAnonymous:
|
|
// No verification needed for anonymous requests.
|
|
default:
|
|
// For all unknown auth types return error.
|
|
writeErrorResponse(w, ErrAccessDenied, r.URL)
|
|
return
|
|
}
|
|
|
|
getBucketInfo := objectAPI.GetBucketInfo
|
|
if reqAuthType == authTypeAnonymous {
|
|
getBucketInfo = objectAPI.AnonGetBucketInfo
|
|
}
|
|
|
|
if _, err := getBucketInfo(bucket); err != nil {
|
|
errorIf(err, "Unable to fetch bucket info.")
|
|
writeErrorResponse(w, toAPIErrorCode(err), r.URL)
|
|
return
|
|
}
|
|
|
|
// Generate response.
|
|
encodedSuccessResponse := encodeResponse(LocationResponse{})
|
|
// Get current region.
|
|
region := serverConfig.GetRegion()
|
|
if region != globalMinioDefaultRegion {
|
|
encodedSuccessResponse = encodeResponse(LocationResponse{
|
|
Location: region,
|
|
})
|
|
}
|
|
|
|
// Write success response.
|
|
writeSuccessResponseXML(w, encodedSuccessResponse)
|
|
}
|