diff --git a/api/minio/api-errors.go b/api/minio/api-errors.go new file mode 100644 index 000000000..c0879ef7c --- /dev/null +++ b/api/minio/api-errors.go @@ -0,0 +1,2235 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "encoding/xml" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/Azure/azure-storage-blob-go/azblob" + "google.golang.org/api/googleapi" + + minio "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/tags" + "github.com/minio/minio/internal/auth" + "github.com/minio/minio/internal/bucket/lifecycle" + "github.com/minio/minio/internal/bucket/replication" + "github.com/minio/minio/internal/config/dns" + "github.com/minio/minio/internal/crypto" + "github.com/minio/minio/internal/logger" + + objectlock "github.com/minio/minio/internal/bucket/object/lock" + "github.com/minio/minio/internal/bucket/versioning" + "github.com/minio/minio/internal/event" + "github.com/minio/minio/internal/hash" + "github.com/minio/pkg/bucket/policy" +) + +// APIError structure +type APIError struct { + Code string + Description string + HTTPStatusCode int +} + +// APIErrorResponse - error response format +type APIErrorResponse struct { + XMLName xml.Name `xml:"Error" json:"-"` + Code string + Message string + Key string `xml:"Key,omitempty" json:"Key,omitempty"` + BucketName string `xml:"BucketName,omitempty" json:"BucketName,omitempty"` + Resource string + Region string `xml:"Region,omitempty" json:"Region,omitempty"` + RequestID string `xml:"RequestId" json:"RequestId"` + HostID string `xml:"HostId" json:"HostId"` +} + +// APIErrorCode type of error status. +type APIErrorCode int + +//go:generate stringer -type=APIErrorCode -trimprefix=Err $GOFILE + +// Error codes, non exhaustive list - http://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html +const ( + ErrNone APIErrorCode = iota + ErrAccessDenied + ErrBadDigest + ErrEntityTooSmall + ErrEntityTooLarge + ErrPolicyTooLarge + ErrIncompleteBody + ErrInternalError + ErrInvalidAccessKeyID + ErrInvalidBucketName + ErrInvalidDigest + ErrInvalidRange + ErrInvalidRangePartNumber + ErrInvalidCopyPartRange + ErrInvalidCopyPartRangeSource + ErrInvalidMaxKeys + ErrInvalidEncodingMethod + ErrInvalidMaxUploads + ErrInvalidMaxParts + ErrInvalidPartNumberMarker + ErrInvalidPartNumber + ErrInvalidRequestBody + ErrInvalidCopySource + ErrInvalidMetadataDirective + ErrInvalidCopyDest + ErrInvalidPolicyDocument + ErrInvalidObjectState + ErrMalformedXML + ErrMissingContentLength + ErrMissingContentMD5 + ErrMissingRequestBodyError + ErrMissingSecurityHeader + ErrNoSuchBucket + ErrNoSuchBucketPolicy + ErrNoSuchBucketLifecycle + ErrNoSuchLifecycleConfiguration + ErrNoSuchBucketSSEConfig + ErrNoSuchCORSConfiguration + ErrNoSuchWebsiteConfiguration + ErrReplicationConfigurationNotFoundError + ErrRemoteDestinationNotFoundError + ErrReplicationDestinationMissingLock + ErrRemoteTargetNotFoundError + ErrReplicationRemoteConnectionError + ErrReplicationBandwidthLimitError + ErrBucketRemoteIdenticalToSource + ErrBucketRemoteAlreadyExists + ErrBucketRemoteLabelInUse + ErrBucketRemoteArnTypeInvalid + ErrBucketRemoteArnInvalid + ErrBucketRemoteRemoveDisallowed + ErrRemoteTargetNotVersionedError + ErrReplicationSourceNotVersionedError + ErrReplicationNeedsVersioningError + ErrReplicationBucketNeedsVersioningError + ErrReplicationNoMatchingRuleError + ErrObjectRestoreAlreadyInProgress + ErrNoSuchKey + ErrNoSuchUpload + ErrInvalidVersionID + ErrNoSuchVersion + ErrNotImplemented + ErrPreconditionFailed + ErrRequestTimeTooSkewed + ErrSignatureDoesNotMatch + ErrMethodNotAllowed + ErrInvalidPart + ErrInvalidPartOrder + ErrAuthorizationHeaderMalformed + ErrMalformedPOSTRequest + ErrPOSTFileRequired + ErrSignatureVersionNotSupported + ErrBucketNotEmpty + ErrAllAccessDisabled + ErrMalformedPolicy + ErrMissingFields + ErrMissingCredTag + ErrCredMalformed + ErrInvalidRegion + ErrInvalidServiceS3 + ErrInvalidServiceSTS + ErrInvalidRequestVersion + ErrMissingSignTag + ErrMissingSignHeadersTag + ErrMalformedDate + ErrMalformedPresignedDate + ErrMalformedCredentialDate + ErrMalformedCredentialRegion + ErrMalformedExpires + ErrNegativeExpires + ErrAuthHeaderEmpty + ErrExpiredPresignRequest + ErrRequestNotReadyYet + ErrUnsignedHeaders + ErrMissingDateHeader + ErrInvalidQuerySignatureAlgo + ErrInvalidQueryParams + ErrBucketAlreadyOwnedByYou + ErrInvalidDuration + ErrBucketAlreadyExists + ErrMetadataTooLarge + ErrUnsupportedMetadata + ErrMaximumExpires + ErrSlowDown + ErrInvalidPrefixMarker + ErrBadRequest + ErrKeyTooLongError + ErrInvalidBucketObjectLockConfiguration + ErrObjectLockConfigurationNotFound + ErrObjectLockConfigurationNotAllowed + ErrNoSuchObjectLockConfiguration + ErrObjectLocked + ErrInvalidRetentionDate + ErrPastObjectLockRetainDate + ErrUnknownWORMModeDirective + ErrBucketTaggingNotFound + ErrObjectLockInvalidHeaders + ErrInvalidTagDirective + // Add new error codes here. + + // SSE-S3 related API errors + ErrInvalidEncryptionMethod + + // Server-Side-Encryption (with Customer provided key) related API errors. + ErrInsecureSSECustomerRequest + ErrSSEMultipartEncrypted + ErrSSEEncryptedObject + ErrInvalidEncryptionParameters + ErrInvalidSSECustomerAlgorithm + ErrInvalidSSECustomerKey + ErrMissingSSECustomerKey + ErrMissingSSECustomerKeyMD5 + ErrSSECustomerKeyMD5Mismatch + ErrInvalidSSECustomerParameters + ErrIncompatibleEncryptionMethod + ErrKMSNotConfigured + + ErrNoAccessKey + ErrInvalidToken + + // Bucket notification related errors. + ErrEventNotification + ErrARNNotification + ErrRegionNotification + ErrOverlappingFilterNotification + ErrFilterNameInvalid + ErrFilterNamePrefix + ErrFilterNameSuffix + ErrFilterValueInvalid + ErrOverlappingConfigs + ErrUnsupportedNotification + + // S3 extended errors. + ErrContentSHA256Mismatch + + // Add new extended error codes here. + + // MinIO extended errors. + ErrReadQuorum + ErrWriteQuorum + ErrStorageFull + ErrRequestBodyParse + ErrObjectExistsAsDirectory + ErrInvalidObjectName + ErrInvalidObjectNamePrefixSlash + ErrInvalidResourceName + ErrServerNotInitialized + ErrOperationTimedOut + ErrClientDisconnected + ErrOperationMaxedOut + ErrInvalidRequest + ErrTransitionStorageClassNotFoundError + // MinIO storage class error codes + ErrInvalidStorageClass + ErrBackendDown + // Add new extended error codes here. + // Please open a https://github.com/minio/minio/issues before adding + // new error codes here. + + ErrMalformedJSON + ErrAdminNoSuchUser + ErrAdminNoSuchGroup + ErrAdminGroupNotEmpty + ErrAdminNoSuchPolicy + ErrAdminInvalidArgument + ErrAdminInvalidAccessKey + ErrAdminInvalidSecretKey + ErrAdminConfigNoQuorum + ErrAdminConfigTooLarge + ErrAdminConfigBadJSON + ErrAdminConfigDuplicateKeys + ErrAdminCredentialsMismatch + ErrInsecureClientRequest + ErrObjectTampered + // Bucket Quota error codes + ErrAdminBucketQuotaExceeded + ErrAdminNoSuchQuotaConfiguration + + ErrHealNotImplemented + ErrHealNoSuchProcess + ErrHealInvalidClientToken + ErrHealMissingBucket + ErrHealAlreadyRunning + ErrHealOverlappingPaths + ErrIncorrectContinuationToken + + // S3 Select Errors + ErrEmptyRequestBody + ErrUnsupportedFunction + ErrInvalidExpressionType + ErrBusy + ErrUnauthorizedAccess + ErrExpressionTooLong + ErrIllegalSQLFunctionArgument + ErrInvalidKeyPath + ErrInvalidCompressionFormat + ErrInvalidFileHeaderInfo + ErrInvalidJSONType + ErrInvalidQuoteFields + ErrInvalidRequestParameter + ErrInvalidDataType + ErrInvalidTextEncoding + ErrInvalidDataSource + ErrInvalidTableAlias + ErrMissingRequiredParameter + ErrObjectSerializationConflict + ErrUnsupportedSQLOperation + ErrUnsupportedSQLStructure + ErrUnsupportedSyntax + ErrUnsupportedRangeHeader + ErrLexerInvalidChar + ErrLexerInvalidOperator + ErrLexerInvalidLiteral + ErrLexerInvalidIONLiteral + ErrParseExpectedDatePart + ErrParseExpectedKeyword + ErrParseExpectedTokenType + ErrParseExpected2TokenTypes + ErrParseExpectedNumber + ErrParseExpectedRightParenBuiltinFunctionCall + ErrParseExpectedTypeName + ErrParseExpectedWhenClause + ErrParseUnsupportedToken + ErrParseUnsupportedLiteralsGroupBy + ErrParseExpectedMember + ErrParseUnsupportedSelect + ErrParseUnsupportedCase + ErrParseUnsupportedCaseClause + ErrParseUnsupportedAlias + ErrParseUnsupportedSyntax + ErrParseUnknownOperator + ErrParseMissingIdentAfterAt + ErrParseUnexpectedOperator + ErrParseUnexpectedTerm + ErrParseUnexpectedToken + ErrParseUnexpectedKeyword + ErrParseExpectedExpression + ErrParseExpectedLeftParenAfterCast + ErrParseExpectedLeftParenValueConstructor + ErrParseExpectedLeftParenBuiltinFunctionCall + ErrParseExpectedArgumentDelimiter + ErrParseCastArity + ErrParseInvalidTypeParam + ErrParseEmptySelect + ErrParseSelectMissingFrom + ErrParseExpectedIdentForGroupName + ErrParseExpectedIdentForAlias + ErrParseUnsupportedCallWithStar + ErrParseNonUnaryAgregateFunctionCall + ErrParseMalformedJoin + ErrParseExpectedIdentForAt + ErrParseAsteriskIsNotAloneInSelectList + ErrParseCannotMixSqbAndWildcardInSelectList + ErrParseInvalidContextForWildcardInSelectList + ErrIncorrectSQLFunctionArgumentType + ErrValueParseFailure + ErrEvaluatorInvalidArguments + ErrIntegerOverflow + ErrLikeInvalidInputs + ErrCastFailed + ErrInvalidCast + ErrEvaluatorInvalidTimestampFormatPattern + ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing + ErrEvaluatorTimestampFormatPatternDuplicateFields + ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch + ErrEvaluatorUnterminatedTimestampFormatPatternToken + ErrEvaluatorInvalidTimestampFormatPatternToken + ErrEvaluatorInvalidTimestampFormatPatternSymbol + ErrEvaluatorBindingDoesNotExist + ErrMissingHeaders + ErrInvalidColumnIndex + + ErrAdminConfigNotificationTargetsFailed + ErrAdminProfilerNotEnabled + ErrInvalidDecompressedSize + ErrAddUserInvalidArgument + ErrAdminAccountNotEligible + ErrAccountNotEligible + ErrAdminServiceAccountNotFound + ErrPostPolicyConditionInvalidFormat +) + +type errorCodeMap map[APIErrorCode]APIError + +func (e errorCodeMap) ToAPIErrWithErr(errCode APIErrorCode, err error) APIError { + apiErr, ok := e[errCode] + if !ok { + apiErr = e[ErrInternalError] + } + if err != nil { + apiErr.Description = fmt.Sprintf("%s (%s)", apiErr.Description, err) + } + if globalServerRegion != "" { + switch errCode { + case ErrAuthorizationHeaderMalformed: + apiErr.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalServerRegion) + return apiErr + } + } + return apiErr +} + +func (e errorCodeMap) ToAPIErr(errCode APIErrorCode) APIError { + return e.ToAPIErrWithErr(errCode, nil) +} + +// error code to APIError structure, these fields carry respective +// descriptions for all the error responses. +var errorCodes = errorCodeMap{ + ErrInvalidCopyDest: { + Code: "InvalidRequest", + Description: "This copy request is illegal because it is trying to copy an object to itself without changing the object's metadata, storage class, website redirect location or encryption attributes.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidCopySource: { + Code: "InvalidArgument", + Description: "Copy Source must mention the source bucket and key: sourcebucket/sourcekey.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidMetadataDirective: { + Code: "InvalidArgument", + Description: "Unknown metadata directive.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidStorageClass: { + Code: "InvalidStorageClass", + Description: "Invalid storage class.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidRequestBody: { + Code: "InvalidArgument", + Description: "Body shouldn't be set for this request.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidMaxUploads: { + Code: "InvalidArgument", + Description: "Argument max-uploads must be an integer between 0 and 2147483647", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidMaxKeys: { + Code: "InvalidArgument", + Description: "Argument maxKeys must be an integer between 0 and 2147483647", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidEncodingMethod: { + Code: "InvalidArgument", + Description: "Invalid Encoding Method specified in Request", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidMaxParts: { + Code: "InvalidArgument", + Description: "Argument max-parts must be an integer between 0 and 2147483647", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidPartNumberMarker: { + Code: "InvalidArgument", + Description: "Argument partNumberMarker must be an integer.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidPartNumber: { + Code: "InvalidPartNumber", + Description: "The requested partnumber is not satisfiable", + HTTPStatusCode: http.StatusRequestedRangeNotSatisfiable, + }, + ErrInvalidPolicyDocument: { + Code: "InvalidPolicyDocument", + Description: "The content of the form does not meet the conditions specified in the policy document.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAccessDenied: { + Code: "AccessDenied", + Description: "Access Denied.", + HTTPStatusCode: http.StatusForbidden, + }, + ErrBadDigest: { + Code: "BadDigest", + Description: "The Content-Md5 you specified did not match what we received.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrEntityTooSmall: { + Code: "EntityTooSmall", + Description: "Your proposed upload is smaller than the minimum allowed object size.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrEntityTooLarge: { + Code: "EntityTooLarge", + Description: "Your proposed upload exceeds the maximum allowed object size.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrPolicyTooLarge: { + Code: "PolicyTooLarge", + Description: "Policy exceeds the maximum allowed document size.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrIncompleteBody: { + Code: "IncompleteBody", + Description: "You did not provide the number of bytes specified by the Content-Length HTTP header.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInternalError: { + Code: "InternalError", + Description: "We encountered an internal error, please try again.", + HTTPStatusCode: http.StatusInternalServerError, + }, + ErrInvalidAccessKeyID: { + Code: "InvalidAccessKeyId", + Description: "The Access Key Id you provided does not exist in our records.", + HTTPStatusCode: http.StatusForbidden, + }, + ErrInvalidBucketName: { + Code: "InvalidBucketName", + Description: "The specified bucket is not valid.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidDigest: { + Code: "InvalidDigest", + Description: "The Content-Md5 you specified is not valid.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidRange: { + Code: "InvalidRange", + Description: "The requested range is not satisfiable", + HTTPStatusCode: http.StatusRequestedRangeNotSatisfiable, + }, + ErrInvalidRangePartNumber: { + Code: "InvalidRequest", + Description: "Cannot specify both Range header and partNumber query parameter", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMalformedXML: { + Code: "MalformedXML", + Description: "The XML you provided was not well-formed or did not validate against our published schema.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMissingContentLength: { + Code: "MissingContentLength", + Description: "You must provide the Content-Length HTTP header.", + HTTPStatusCode: http.StatusLengthRequired, + }, + ErrMissingContentMD5: { + Code: "MissingContentMD5", + Description: "Missing required header for this request: Content-Md5.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMissingSecurityHeader: { + Code: "MissingSecurityHeader", + Description: "Your request was missing a required header", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMissingRequestBodyError: { + Code: "MissingRequestBodyError", + Description: "Request body is empty.", + HTTPStatusCode: http.StatusLengthRequired, + }, + ErrNoSuchBucket: { + Code: "NoSuchBucket", + Description: "The specified bucket does not exist", + HTTPStatusCode: http.StatusNotFound, + }, + ErrNoSuchBucketPolicy: { + Code: "NoSuchBucketPolicy", + Description: "The bucket policy does not exist", + HTTPStatusCode: http.StatusNotFound, + }, + ErrNoSuchBucketLifecycle: { + Code: "NoSuchBucketLifecycle", + Description: "The bucket lifecycle configuration does not exist", + HTTPStatusCode: http.StatusNotFound, + }, + ErrNoSuchLifecycleConfiguration: { + Code: "NoSuchLifecycleConfiguration", + Description: "The lifecycle configuration does not exist", + HTTPStatusCode: http.StatusNotFound, + }, + ErrNoSuchBucketSSEConfig: { + Code: "ServerSideEncryptionConfigurationNotFoundError", + Description: "The server side encryption configuration was not found", + HTTPStatusCode: http.StatusNotFound, + }, + ErrNoSuchKey: { + Code: "NoSuchKey", + Description: "The specified key does not exist.", + HTTPStatusCode: http.StatusNotFound, + }, + ErrNoSuchUpload: { + Code: "NoSuchUpload", + Description: "The specified multipart upload does not exist. The upload ID may be invalid, or the upload may have been aborted or completed.", + HTTPStatusCode: http.StatusNotFound, + }, + ErrInvalidVersionID: { + Code: "InvalidArgument", + Description: "Invalid version id specified", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrNoSuchVersion: { + Code: "NoSuchVersion", + Description: "The specified version does not exist.", + HTTPStatusCode: http.StatusNotFound, + }, + ErrNotImplemented: { + Code: "NotImplemented", + Description: "A header you provided implies functionality that is not implemented", + HTTPStatusCode: http.StatusNotImplemented, + }, + ErrPreconditionFailed: { + Code: "PreconditionFailed", + Description: "At least one of the pre-conditions you specified did not hold", + HTTPStatusCode: http.StatusPreconditionFailed, + }, + ErrRequestTimeTooSkewed: { + Code: "RequestTimeTooSkewed", + Description: "The difference between the request time and the server's time is too large.", + HTTPStatusCode: http.StatusForbidden, + }, + ErrSignatureDoesNotMatch: { + Code: "SignatureDoesNotMatch", + Description: "The request signature we calculated does not match the signature you provided. Check your key and signing method.", + HTTPStatusCode: http.StatusForbidden, + }, + ErrMethodNotAllowed: { + Code: "MethodNotAllowed", + Description: "The specified method is not allowed against this resource.", + HTTPStatusCode: http.StatusMethodNotAllowed, + }, + ErrInvalidPart: { + Code: "InvalidPart", + Description: "One or more of the specified parts could not be found. The part may not have been uploaded, or the specified entity tag may not match the part's entity tag.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidPartOrder: { + Code: "InvalidPartOrder", + Description: "The list of parts was not in ascending order. The parts list must be specified in order by part number.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidObjectState: { + Code: "InvalidObjectState", + Description: "The operation is not valid for the current state of the object.", + HTTPStatusCode: http.StatusForbidden, + }, + ErrAuthorizationHeaderMalformed: { + Code: "AuthorizationHeaderMalformed", + Description: "The authorization header is malformed; the region is wrong; expecting 'us-east-1'.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMalformedPOSTRequest: { + Code: "MalformedPOSTRequest", + Description: "The body of your POST request is not well-formed multipart/form-data.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrPOSTFileRequired: { + Code: "InvalidArgument", + Description: "POST requires exactly one file upload per request.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrSignatureVersionNotSupported: { + Code: "InvalidRequest", + Description: "The authorization mechanism you have provided is not supported. Please use AWS4-HMAC-SHA256.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrBucketNotEmpty: { + Code: "BucketNotEmpty", + Description: "The bucket you tried to delete is not empty", + HTTPStatusCode: http.StatusConflict, + }, + ErrBucketAlreadyExists: { + Code: "BucketAlreadyExists", + Description: "The requested bucket name is not available. The bucket namespace is shared by all users of the system. Please select a different name and try again.", + HTTPStatusCode: http.StatusConflict, + }, + ErrAllAccessDisabled: { + Code: "AllAccessDisabled", + Description: "All access to this bucket has been disabled.", + HTTPStatusCode: http.StatusForbidden, + }, + ErrMalformedPolicy: { + Code: "MalformedPolicy", + Description: "Policy has invalid resource.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMissingFields: { + Code: "MissingFields", + Description: "Missing fields in request.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMissingCredTag: { + Code: "InvalidRequest", + Description: "Missing Credential field for this request.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrCredMalformed: { + Code: "AuthorizationQueryParametersError", + Description: "Error parsing the X-Amz-Credential parameter; the Credential is mal-formed; expecting \"/YYYYMMDD/REGION/SERVICE/aws4_request\".", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMalformedDate: { + Code: "MalformedDate", + Description: "Invalid date format header, expected to be in ISO8601, RFC1123 or RFC1123Z time format.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMalformedPresignedDate: { + Code: "AuthorizationQueryParametersError", + Description: "X-Amz-Date must be in the ISO8601 Long Format \"yyyyMMdd'T'HHmmss'Z'\"", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMalformedCredentialDate: { + Code: "AuthorizationQueryParametersError", + Description: "Error parsing the X-Amz-Credential parameter; incorrect date format. This date in the credential must be in the format \"yyyyMMdd\".", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidRegion: { + Code: "InvalidRegion", + Description: "Region does not match.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidServiceS3: { + Code: "AuthorizationParametersError", + Description: "Error parsing the Credential/X-Amz-Credential parameter; incorrect service. This endpoint belongs to \"s3\".", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidServiceSTS: { + Code: "AuthorizationParametersError", + Description: "Error parsing the Credential parameter; incorrect service. This endpoint belongs to \"sts\".", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidRequestVersion: { + Code: "AuthorizationQueryParametersError", + Description: "Error parsing the X-Amz-Credential parameter; incorrect terminal. This endpoint uses \"aws4_request\".", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMissingSignTag: { + Code: "AccessDenied", + Description: "Signature header missing Signature field.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMissingSignHeadersTag: { + Code: "InvalidArgument", + Description: "Signature header missing SignedHeaders field.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMalformedExpires: { + Code: "AuthorizationQueryParametersError", + Description: "X-Amz-Expires should be a number", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrNegativeExpires: { + Code: "AuthorizationQueryParametersError", + Description: "X-Amz-Expires must be non-negative", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAuthHeaderEmpty: { + Code: "InvalidArgument", + Description: "Authorization header is invalid -- one and only one ' ' (space) required.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMissingDateHeader: { + Code: "AccessDenied", + Description: "AWS authentication requires a valid Date or x-amz-date header", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidQuerySignatureAlgo: { + Code: "AuthorizationQueryParametersError", + Description: "X-Amz-Algorithm only supports \"AWS4-HMAC-SHA256\".", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrExpiredPresignRequest: { + Code: "AccessDenied", + Description: "Request has expired", + HTTPStatusCode: http.StatusForbidden, + }, + ErrRequestNotReadyYet: { + Code: "AccessDenied", + Description: "Request is not valid yet", + HTTPStatusCode: http.StatusForbidden, + }, + ErrSlowDown: { + Code: "SlowDown", + Description: "Resource requested is unreadable, please reduce your request rate", + HTTPStatusCode: http.StatusServiceUnavailable, + }, + ErrInvalidPrefixMarker: { + Code: "InvalidPrefixMarker", + Description: "Invalid marker prefix combination", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrBadRequest: { + Code: "BadRequest", + Description: "400 BadRequest", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrKeyTooLongError: { + Code: "KeyTooLongError", + Description: "Your key is too long", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrUnsignedHeaders: { + Code: "AccessDenied", + Description: "There were headers present in the request which were not signed", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidQueryParams: { + Code: "AuthorizationQueryParametersError", + Description: "Query-string authentication version 4 requires the X-Amz-Algorithm, X-Amz-Credential, X-Amz-Signature, X-Amz-Date, X-Amz-SignedHeaders, and X-Amz-Expires parameters.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrBucketAlreadyOwnedByYou: { + Code: "BucketAlreadyOwnedByYou", + Description: "Your previous request to create the named bucket succeeded and you already own it.", + HTTPStatusCode: http.StatusConflict, + }, + ErrInvalidDuration: { + Code: "InvalidDuration", + Description: "Duration provided in the request is invalid.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidBucketObjectLockConfiguration: { + Code: "InvalidRequest", + Description: "Bucket is missing ObjectLockConfiguration", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrBucketTaggingNotFound: { + Code: "NoSuchTagSet", + Description: "The TagSet does not exist", + HTTPStatusCode: http.StatusNotFound, + }, + ErrObjectLockConfigurationNotFound: { + Code: "ObjectLockConfigurationNotFoundError", + Description: "Object Lock configuration does not exist for this bucket", + HTTPStatusCode: http.StatusNotFound, + }, + ErrObjectLockConfigurationNotAllowed: { + Code: "InvalidBucketState", + Description: "Object Lock configuration cannot be enabled on existing buckets", + HTTPStatusCode: http.StatusConflict, + }, + ErrNoSuchCORSConfiguration: { + Code: "NoSuchCORSConfiguration", + Description: "The CORS configuration does not exist", + HTTPStatusCode: http.StatusNotFound, + }, + ErrNoSuchWebsiteConfiguration: { + Code: "NoSuchWebsiteConfiguration", + Description: "The specified bucket does not have a website configuration", + HTTPStatusCode: http.StatusNotFound, + }, + ErrReplicationConfigurationNotFoundError: { + Code: "ReplicationConfigurationNotFoundError", + Description: "The replication configuration was not found", + HTTPStatusCode: http.StatusNotFound, + }, + ErrRemoteDestinationNotFoundError: { + Code: "RemoteDestinationNotFoundError", + Description: "The remote destination bucket does not exist", + HTTPStatusCode: http.StatusNotFound, + }, + ErrReplicationDestinationMissingLock: { + Code: "ReplicationDestinationMissingLockError", + Description: "The replication destination bucket does not have object locking enabled", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrRemoteTargetNotFoundError: { + Code: "XMinioAdminRemoteTargetNotFoundError", + Description: "The remote target does not exist", + HTTPStatusCode: http.StatusNotFound, + }, + ErrReplicationRemoteConnectionError: { + Code: "XMinioAdminReplicationRemoteConnectionError", + Description: "Remote service connection error - please check remote service credentials and target bucket", + HTTPStatusCode: http.StatusNotFound, + }, + ErrReplicationBandwidthLimitError: { + Code: "XMinioAdminReplicationBandwidthLimitError", + Description: "Bandwidth limit for remote target must be atleast 100MBps", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrReplicationNoMatchingRuleError: { + Code: "XMinioReplicationNoMatchingRule", + Description: "No matching replication rule found for this object prefix", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrBucketRemoteIdenticalToSource: { + Code: "XMinioAdminRemoteIdenticalToSource", + Description: "The remote target cannot be identical to source", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrBucketRemoteAlreadyExists: { + Code: "XMinioAdminBucketRemoteAlreadyExists", + Description: "The remote target already exists", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrBucketRemoteLabelInUse: { + Code: "XMinioAdminBucketRemoteLabelInUse", + Description: "The remote target with this label already exists", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrBucketRemoteRemoveDisallowed: { + Code: "XMinioAdminRemoteRemoveDisallowed", + Description: "This ARN is in use by an existing configuration", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrBucketRemoteArnTypeInvalid: { + Code: "XMinioAdminRemoteARNTypeInvalid", + Description: "The bucket remote ARN type is not valid", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrBucketRemoteArnInvalid: { + Code: "XMinioAdminRemoteArnInvalid", + Description: "The bucket remote ARN does not have correct format", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrRemoteTargetNotVersionedError: { + Code: "RemoteTargetNotVersionedError", + Description: "The remote target does not have versioning enabled", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrReplicationSourceNotVersionedError: { + Code: "ReplicationSourceNotVersionedError", + Description: "The replication source does not have versioning enabled", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrReplicationNeedsVersioningError: { + Code: "InvalidRequest", + Description: "Versioning must be 'Enabled' on the bucket to apply a replication configuration", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrReplicationBucketNeedsVersioningError: { + Code: "InvalidRequest", + Description: "Versioning must be 'Enabled' on the bucket to add a replication target", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrNoSuchObjectLockConfiguration: { + Code: "NoSuchObjectLockConfiguration", + Description: "The specified object does not have a ObjectLock configuration", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrObjectLocked: { + Code: "InvalidRequest", + Description: "Object is WORM protected and cannot be overwritten", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidRetentionDate: { + Code: "InvalidRequest", + Description: "Date must be provided in ISO 8601 format", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrPastObjectLockRetainDate: { + Code: "InvalidRequest", + Description: "the retain until date must be in the future", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrUnknownWORMModeDirective: { + Code: "InvalidRequest", + Description: "unknown wormMode directive", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrObjectLockInvalidHeaders: { + Code: "InvalidRequest", + Description: "x-amz-object-lock-retain-until-date and x-amz-object-lock-mode must both be supplied", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrObjectRestoreAlreadyInProgress: { + Code: "RestoreAlreadyInProgress", + Description: "Object restore is already in progress", + HTTPStatusCode: http.StatusConflict, + }, + ErrTransitionStorageClassNotFoundError: { + Code: "TransitionStorageClassNotFoundError", + Description: "The transition storage class was not found", + HTTPStatusCode: http.StatusNotFound, + }, + + /// Bucket notification related errors. + ErrEventNotification: { + Code: "InvalidArgument", + Description: "A specified event is not supported for notifications.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrARNNotification: { + Code: "InvalidArgument", + Description: "A specified destination ARN does not exist or is not well-formed. Verify the destination ARN.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrRegionNotification: { + Code: "InvalidArgument", + Description: "A specified destination is in a different region than the bucket. You must use a destination that resides in the same region as the bucket.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrOverlappingFilterNotification: { + Code: "InvalidArgument", + Description: "An object key name filtering rule defined with overlapping prefixes, overlapping suffixes, or overlapping combinations of prefixes and suffixes for the same event types.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrFilterNameInvalid: { + Code: "InvalidArgument", + Description: "filter rule name must be either prefix or suffix", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrFilterNamePrefix: { + Code: "InvalidArgument", + Description: "Cannot specify more than one prefix rule in a filter.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrFilterNameSuffix: { + Code: "InvalidArgument", + Description: "Cannot specify more than one suffix rule in a filter.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrFilterValueInvalid: { + Code: "InvalidArgument", + Description: "Size of filter rule value cannot exceed 1024 bytes in UTF-8 representation", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrOverlappingConfigs: { + Code: "InvalidArgument", + Description: "Configurations overlap. Configurations on the same bucket cannot share a common event type.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrUnsupportedNotification: { + Code: "UnsupportedNotification", + Description: "MinIO server does not support Topic or Cloud Function based notifications.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidCopyPartRange: { + Code: "InvalidArgument", + Description: "The x-amz-copy-source-range value must be of the form bytes=first-last where first and last are the zero-based offsets of the first and last bytes to copy", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidCopyPartRangeSource: { + Code: "InvalidArgument", + Description: "Range specified is not valid for source object", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMetadataTooLarge: { + Code: "MetadataTooLarge", + Description: "Your metadata headers exceed the maximum allowed metadata size.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidTagDirective: { + Code: "InvalidArgument", + Description: "Unknown tag directive.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidEncryptionMethod: { + Code: "InvalidRequest", + Description: "The encryption method specified is not supported", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInsecureSSECustomerRequest: { + Code: "InvalidRequest", + Description: "Requests specifying Server Side Encryption with Customer provided keys must be made over a secure connection.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrSSEMultipartEncrypted: { + Code: "InvalidRequest", + Description: "The multipart upload initiate requested encryption. Subsequent part requests must include the appropriate encryption parameters.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrSSEEncryptedObject: { + Code: "InvalidRequest", + Description: "The object was stored using a form of Server Side Encryption. The correct parameters must be provided to retrieve the object.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidEncryptionParameters: { + Code: "InvalidRequest", + Description: "The encryption parameters are not applicable to this object.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidSSECustomerAlgorithm: { + Code: "InvalidArgument", + Description: "Requests specifying Server Side Encryption with Customer provided keys must provide a valid encryption algorithm.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidSSECustomerKey: { + Code: "InvalidArgument", + Description: "The secret key was invalid for the specified algorithm.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMissingSSECustomerKey: { + Code: "InvalidArgument", + Description: "Requests specifying Server Side Encryption with Customer provided keys must provide an appropriate secret key.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMissingSSECustomerKeyMD5: { + Code: "InvalidArgument", + Description: "Requests specifying Server Side Encryption with Customer provided keys must provide the client calculated MD5 of the secret key.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrSSECustomerKeyMD5Mismatch: { + Code: "InvalidArgument", + Description: "The calculated MD5 hash of the key did not match the hash that was provided.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidSSECustomerParameters: { + Code: "InvalidArgument", + Description: "The provided encryption parameters did not match the ones used originally.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrIncompatibleEncryptionMethod: { + Code: "InvalidArgument", + Description: "Server side encryption specified with both SSE-C and SSE-S3 headers", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrKMSNotConfigured: { + Code: "NotImplemented", + Description: "Server side encryption specified but KMS is not configured", + HTTPStatusCode: http.StatusNotImplemented, + }, + ErrNoAccessKey: { + Code: "AccessDenied", + Description: "No AWSAccessKey was presented", + HTTPStatusCode: http.StatusForbidden, + }, + ErrInvalidToken: { + Code: "InvalidTokenId", + Description: "The security token included in the request is invalid", + HTTPStatusCode: http.StatusForbidden, + }, + + /// S3 extensions. + ErrContentSHA256Mismatch: { + Code: "XAmzContentSHA256Mismatch", + Description: "The provided 'x-amz-content-sha256' header does not match what was computed.", + HTTPStatusCode: http.StatusBadRequest, + }, + + /// MinIO extensions. + ErrStorageFull: { + Code: "XMinioStorageFull", + Description: "Storage backend has reached its minimum free disk threshold. Please delete a few objects to proceed.", + HTTPStatusCode: http.StatusInsufficientStorage, + }, + ErrRequestBodyParse: { + Code: "XMinioRequestBodyParse", + Description: "The request body failed to parse.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrObjectExistsAsDirectory: { + Code: "XMinioObjectExistsAsDirectory", + Description: "Object name already exists as a directory.", + HTTPStatusCode: http.StatusConflict, + }, + ErrInvalidObjectName: { + Code: "XMinioInvalidObjectName", + Description: "Object name contains unsupported characters.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidObjectNamePrefixSlash: { + Code: "XMinioInvalidObjectName", + Description: "Object name contains a leading slash.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidResourceName: { + Code: "XMinioInvalidResourceName", + Description: "Resource name contains bad components such as \"..\" or \".\".", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrServerNotInitialized: { + Code: "XMinioServerNotInitialized", + Description: "Server not initialized, please try again.", + HTTPStatusCode: http.StatusServiceUnavailable, + }, + ErrMalformedJSON: { + Code: "XMinioMalformedJSON", + Description: "The JSON you provided was not well-formed or did not validate against our published format.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAdminNoSuchUser: { + Code: "XMinioAdminNoSuchUser", + Description: "The specified user does not exist.", + HTTPStatusCode: http.StatusNotFound, + }, + ErrAdminNoSuchGroup: { + Code: "XMinioAdminNoSuchGroup", + Description: "The specified group does not exist.", + HTTPStatusCode: http.StatusNotFound, + }, + ErrAdminGroupNotEmpty: { + Code: "XMinioAdminGroupNotEmpty", + Description: "The specified group is not empty - cannot remove it.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAdminNoSuchPolicy: { + Code: "XMinioAdminNoSuchPolicy", + Description: "The canned policy does not exist.", + HTTPStatusCode: http.StatusNotFound, + }, + ErrAdminInvalidArgument: { + Code: "XMinioAdminInvalidArgument", + Description: "Invalid arguments specified.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAdminInvalidAccessKey: { + Code: "XMinioAdminInvalidAccessKey", + Description: "The access key is invalid.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAdminInvalidSecretKey: { + Code: "XMinioAdminInvalidSecretKey", + Description: "The secret key is invalid.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAdminConfigNoQuorum: { + Code: "XMinioAdminConfigNoQuorum", + Description: "Configuration update failed because server quorum was not met", + HTTPStatusCode: http.StatusServiceUnavailable, + }, + ErrAdminConfigTooLarge: { + Code: "XMinioAdminConfigTooLarge", + Description: fmt.Sprintf("Configuration data provided exceeds the allowed maximum of %d bytes", + maxEConfigJSONSize), + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAdminConfigBadJSON: { + Code: "XMinioAdminConfigBadJSON", + Description: "JSON configuration provided is of incorrect format", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAdminConfigDuplicateKeys: { + Code: "XMinioAdminConfigDuplicateKeys", + Description: "JSON configuration provided has objects with duplicate keys", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAdminConfigNotificationTargetsFailed: { + Code: "XMinioAdminNotificationTargetsTestFailed", + Description: "Configuration update failed due an unsuccessful attempt to connect to one or more notification servers", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAdminProfilerNotEnabled: { + Code: "XMinioAdminProfilerNotEnabled", + Description: "Unable to perform the requested operation because profiling is not enabled", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAdminCredentialsMismatch: { + Code: "XMinioAdminCredentialsMismatch", + Description: "Credentials in config mismatch with server environment variables", + HTTPStatusCode: http.StatusServiceUnavailable, + }, + ErrAdminBucketQuotaExceeded: { + Code: "XMinioAdminBucketQuotaExceeded", + Description: "Bucket quota exceeded", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAdminNoSuchQuotaConfiguration: { + Code: "XMinioAdminNoSuchQuotaConfiguration", + Description: "The quota configuration does not exist", + HTTPStatusCode: http.StatusNotFound, + }, + ErrInsecureClientRequest: { + Code: "XMinioInsecureClientRequest", + Description: "Cannot respond to plain-text request from TLS-encrypted server", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrOperationTimedOut: { + Code: "RequestTimeout", + Description: "A timeout occurred while trying to lock a resource, please reduce your request rate", + HTTPStatusCode: http.StatusServiceUnavailable, + }, + ErrClientDisconnected: { + Code: "ClientDisconnected", + Description: "Client disconnected before response was ready", + HTTPStatusCode: 499, // No official code, use nginx value. + }, + ErrOperationMaxedOut: { + Code: "SlowDown", + Description: "A timeout exceeded while waiting to proceed with the request, please reduce your request rate", + HTTPStatusCode: http.StatusServiceUnavailable, + }, + ErrUnsupportedMetadata: { + Code: "InvalidArgument", + Description: "Your metadata headers are not supported.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrObjectTampered: { + Code: "XMinioObjectTampered", + Description: errObjectTampered.Error(), + HTTPStatusCode: http.StatusPartialContent, + }, + ErrMaximumExpires: { + Code: "AuthorizationQueryParametersError", + Description: "X-Amz-Expires must be less than a week (in seconds); that is, the given X-Amz-Expires must be less than 604800 seconds", + HTTPStatusCode: http.StatusBadRequest, + }, + + // Generic Invalid-Request error. Should be used for response errors only for unlikely + // corner case errors for which introducing new APIErrorCode is not worth it. LogIf() + // should be used to log the error at the source of the error for debugging purposes. + ErrInvalidRequest: { + Code: "InvalidRequest", + Description: "Invalid Request", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrHealNotImplemented: { + Code: "XMinioHealNotImplemented", + Description: "This server does not implement heal functionality.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrHealNoSuchProcess: { + Code: "XMinioHealNoSuchProcess", + Description: "No such heal process is running on the server", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrHealInvalidClientToken: { + Code: "XMinioHealInvalidClientToken", + Description: "Client token mismatch", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrHealMissingBucket: { + Code: "XMinioHealMissingBucket", + Description: "A heal start request with a non-empty object-prefix parameter requires a bucket to be specified.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrHealAlreadyRunning: { + Code: "XMinioHealAlreadyRunning", + Description: "", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrHealOverlappingPaths: { + Code: "XMinioHealOverlappingPaths", + Description: "", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrBackendDown: { + Code: "XMinioBackendDown", + Description: "Object storage backend is unreachable", + HTTPStatusCode: http.StatusServiceUnavailable, + }, + ErrIncorrectContinuationToken: { + Code: "InvalidArgument", + Description: "The continuation token provided is incorrect", + HTTPStatusCode: http.StatusBadRequest, + }, + //S3 Select API Errors + ErrEmptyRequestBody: { + Code: "EmptyRequestBody", + Description: "Request body cannot be empty.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrUnsupportedFunction: { + Code: "UnsupportedFunction", + Description: "Encountered an unsupported SQL function.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidDataSource: { + Code: "InvalidDataSource", + Description: "Invalid data source type. Only CSV and JSON are supported at this time.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidExpressionType: { + Code: "InvalidExpressionType", + Description: "The ExpressionType is invalid. Only SQL expressions are supported at this time.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrBusy: { + Code: "Busy", + Description: "The service is unavailable. Please retry.", + HTTPStatusCode: http.StatusServiceUnavailable, + }, + ErrUnauthorizedAccess: { + Code: "UnauthorizedAccess", + Description: "You are not authorized to perform this operation", + HTTPStatusCode: http.StatusUnauthorized, + }, + ErrExpressionTooLong: { + Code: "ExpressionTooLong", + Description: "The SQL expression is too long: The maximum byte-length for the SQL expression is 256 KB.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrIllegalSQLFunctionArgument: { + Code: "IllegalSqlFunctionArgument", + Description: "Illegal argument was used in the SQL function.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidKeyPath: { + Code: "InvalidKeyPath", + Description: "Key path in the SQL expression is invalid.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidCompressionFormat: { + Code: "InvalidCompressionFormat", + Description: "The file is not in a supported compression format. Only GZIP is supported at this time.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidFileHeaderInfo: { + Code: "InvalidFileHeaderInfo", + Description: "The FileHeaderInfo is invalid. Only NONE, USE, and IGNORE are supported.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidJSONType: { + Code: "InvalidJsonType", + Description: "The JsonType is invalid. Only DOCUMENT and LINES are supported at this time.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidQuoteFields: { + Code: "InvalidQuoteFields", + Description: "The QuoteFields is invalid. Only ALWAYS and ASNEEDED are supported.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidRequestParameter: { + Code: "InvalidRequestParameter", + Description: "The value of a parameter in SelectRequest element is invalid. Check the service API documentation and try again.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidDataType: { + Code: "InvalidDataType", + Description: "The SQL expression contains an invalid data type.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidTextEncoding: { + Code: "InvalidTextEncoding", + Description: "Invalid encoding type. Only UTF-8 encoding is supported at this time.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidTableAlias: { + Code: "InvalidTableAlias", + Description: "The SQL expression contains an invalid table alias.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMissingRequiredParameter: { + Code: "MissingRequiredParameter", + Description: "The SelectRequest entity is missing a required parameter. Check the service documentation and try again.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrObjectSerializationConflict: { + Code: "ObjectSerializationConflict", + Description: "The SelectRequest entity can only contain one of CSV or JSON. Check the service documentation and try again.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrUnsupportedSQLOperation: { + Code: "UnsupportedSqlOperation", + Description: "Encountered an unsupported SQL operation.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrUnsupportedSQLStructure: { + Code: "UnsupportedSqlStructure", + Description: "Encountered an unsupported SQL structure. Check the SQL Reference.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrUnsupportedSyntax: { + Code: "UnsupportedSyntax", + Description: "Encountered invalid syntax.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrUnsupportedRangeHeader: { + Code: "UnsupportedRangeHeader", + Description: "Range header is not supported for this operation.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrLexerInvalidChar: { + Code: "LexerInvalidChar", + Description: "The SQL expression contains an invalid character.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrLexerInvalidOperator: { + Code: "LexerInvalidOperator", + Description: "The SQL expression contains an invalid literal.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrLexerInvalidLiteral: { + Code: "LexerInvalidLiteral", + Description: "The SQL expression contains an invalid operator.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrLexerInvalidIONLiteral: { + Code: "LexerInvalidIONLiteral", + Description: "The SQL expression contains an invalid operator.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedDatePart: { + Code: "ParseExpectedDatePart", + Description: "Did not find the expected date part in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedKeyword: { + Code: "ParseExpectedKeyword", + Description: "Did not find the expected keyword in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedTokenType: { + Code: "ParseExpectedTokenType", + Description: "Did not find the expected token in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpected2TokenTypes: { + Code: "ParseExpected2TokenTypes", + Description: "Did not find the expected token in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedNumber: { + Code: "ParseExpectedNumber", + Description: "Did not find the expected number in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedRightParenBuiltinFunctionCall: { + Code: "ParseExpectedRightParenBuiltinFunctionCall", + Description: "Did not find the expected right parenthesis character in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedTypeName: { + Code: "ParseExpectedTypeName", + Description: "Did not find the expected type name in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedWhenClause: { + Code: "ParseExpectedWhenClause", + Description: "Did not find the expected WHEN clause in the SQL expression. CASE is not supported.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseUnsupportedToken: { + Code: "ParseUnsupportedToken", + Description: "The SQL expression contains an unsupported token.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseUnsupportedLiteralsGroupBy: { + Code: "ParseUnsupportedLiteralsGroupBy", + Description: "The SQL expression contains an unsupported use of GROUP BY.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedMember: { + Code: "ParseExpectedMember", + Description: "The SQL expression contains an unsupported use of MEMBER.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseUnsupportedSelect: { + Code: "ParseUnsupportedSelect", + Description: "The SQL expression contains an unsupported use of SELECT.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseUnsupportedCase: { + Code: "ParseUnsupportedCase", + Description: "The SQL expression contains an unsupported use of CASE.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseUnsupportedCaseClause: { + Code: "ParseUnsupportedCaseClause", + Description: "The SQL expression contains an unsupported use of CASE.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseUnsupportedAlias: { + Code: "ParseUnsupportedAlias", + Description: "The SQL expression contains an unsupported use of ALIAS.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseUnsupportedSyntax: { + Code: "ParseUnsupportedSyntax", + Description: "The SQL expression contains unsupported syntax.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseUnknownOperator: { + Code: "ParseUnknownOperator", + Description: "The SQL expression contains an invalid operator.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseMissingIdentAfterAt: { + Code: "ParseMissingIdentAfterAt", + Description: "Did not find the expected identifier after the @ symbol in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseUnexpectedOperator: { + Code: "ParseUnexpectedOperator", + Description: "The SQL expression contains an unexpected operator.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseUnexpectedTerm: { + Code: "ParseUnexpectedTerm", + Description: "The SQL expression contains an unexpected term.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseUnexpectedToken: { + Code: "ParseUnexpectedToken", + Description: "The SQL expression contains an unexpected token.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseUnexpectedKeyword: { + Code: "ParseUnexpectedKeyword", + Description: "The SQL expression contains an unexpected keyword.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedExpression: { + Code: "ParseExpectedExpression", + Description: "Did not find the expected SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedLeftParenAfterCast: { + Code: "ParseExpectedLeftParenAfterCast", + Description: "Did not find expected the left parenthesis in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedLeftParenValueConstructor: { + Code: "ParseExpectedLeftParenValueConstructor", + Description: "Did not find expected the left parenthesis in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedLeftParenBuiltinFunctionCall: { + Code: "ParseExpectedLeftParenBuiltinFunctionCall", + Description: "Did not find the expected left parenthesis in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedArgumentDelimiter: { + Code: "ParseExpectedArgumentDelimiter", + Description: "Did not find the expected argument delimiter in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseCastArity: { + Code: "ParseCastArity", + Description: "The SQL expression CAST has incorrect arity.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseInvalidTypeParam: { + Code: "ParseInvalidTypeParam", + Description: "The SQL expression contains an invalid parameter value.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseEmptySelect: { + Code: "ParseEmptySelect", + Description: "The SQL expression contains an empty SELECT.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseSelectMissingFrom: { + Code: "ParseSelectMissingFrom", + Description: "GROUP is not supported in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedIdentForGroupName: { + Code: "ParseExpectedIdentForGroupName", + Description: "GROUP is not supported in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedIdentForAlias: { + Code: "ParseExpectedIdentForAlias", + Description: "Did not find the expected identifier for the alias in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseUnsupportedCallWithStar: { + Code: "ParseUnsupportedCallWithStar", + Description: "Only COUNT with (*) as a parameter is supported in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseNonUnaryAgregateFunctionCall: { + Code: "ParseNonUnaryAgregateFunctionCall", + Description: "Only one argument is supported for aggregate functions in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseMalformedJoin: { + Code: "ParseMalformedJoin", + Description: "JOIN is not supported in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseExpectedIdentForAt: { + Code: "ParseExpectedIdentForAt", + Description: "Did not find the expected identifier for AT name in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseAsteriskIsNotAloneInSelectList: { + Code: "ParseAsteriskIsNotAloneInSelectList", + Description: "Other expressions are not allowed in the SELECT list when '*' is used without dot notation in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseCannotMixSqbAndWildcardInSelectList: { + Code: "ParseCannotMixSqbAndWildcardInSelectList", + Description: "Cannot mix [] and * in the same expression in a SELECT list in SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrParseInvalidContextForWildcardInSelectList: { + Code: "ParseInvalidContextForWildcardInSelectList", + Description: "Invalid use of * in SELECT list in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrIncorrectSQLFunctionArgumentType: { + Code: "IncorrectSqlFunctionArgumentType", + Description: "Incorrect type of arguments in function call in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrValueParseFailure: { + Code: "ValueParseFailure", + Description: "Time stamp parse failure in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrEvaluatorInvalidArguments: { + Code: "EvaluatorInvalidArguments", + Description: "Incorrect number of arguments in the function call in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrIntegerOverflow: { + Code: "IntegerOverflow", + Description: "Int overflow or underflow in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrLikeInvalidInputs: { + Code: "LikeInvalidInputs", + Description: "Invalid argument given to the LIKE clause in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrCastFailed: { + Code: "CastFailed", + Description: "Attempt to convert from one data type to another using CAST failed in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidCast: { + Code: "InvalidCast", + Description: "Attempt to convert from one data type to another using CAST failed in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrEvaluatorInvalidTimestampFormatPattern: { + Code: "EvaluatorInvalidTimestampFormatPattern", + Description: "Time stamp format pattern requires additional fields in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrEvaluatorInvalidTimestampFormatPatternSymbolForParsing: { + Code: "EvaluatorInvalidTimestampFormatPatternSymbolForParsing", + Description: "Time stamp format pattern contains a valid format symbol that cannot be applied to time stamp parsing in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrEvaluatorTimestampFormatPatternDuplicateFields: { + Code: "EvaluatorTimestampFormatPatternDuplicateFields", + Description: "Time stamp format pattern contains multiple format specifiers representing the time stamp field in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrEvaluatorTimestampFormatPatternHourClockAmPmMismatch: { + Code: "EvaluatorUnterminatedTimestampFormatPatternToken", + Description: "Time stamp format pattern contains unterminated token in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrEvaluatorUnterminatedTimestampFormatPatternToken: { + Code: "EvaluatorInvalidTimestampFormatPatternToken", + Description: "Time stamp format pattern contains an invalid token in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrEvaluatorInvalidTimestampFormatPatternToken: { + Code: "EvaluatorInvalidTimestampFormatPatternToken", + Description: "Time stamp format pattern contains an invalid token in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrEvaluatorInvalidTimestampFormatPatternSymbol: { + Code: "EvaluatorInvalidTimestampFormatPatternSymbol", + Description: "Time stamp format pattern contains an invalid symbol in the SQL expression.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrEvaluatorBindingDoesNotExist: { + Code: "ErrEvaluatorBindingDoesNotExist", + Description: "A column name or a path provided does not exist in the SQL expression", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrMissingHeaders: { + Code: "MissingHeaders", + Description: "Some headers in the query are missing from the file. Check the file and try again.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidColumnIndex: { + Code: "InvalidColumnIndex", + Description: "The column index is invalid. Please check the service documentation and try again.", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrInvalidDecompressedSize: { + Code: "XMinioInvalidDecompressedSize", + Description: "The data provided is unfit for decompression", + HTTPStatusCode: http.StatusBadRequest, + }, + ErrAddUserInvalidArgument: { + Code: "XMinioInvalidIAMCredentials", + Description: "User is not allowed to be same as admin access key", + HTTPStatusCode: http.StatusForbidden, + }, + ErrAdminAccountNotEligible: { + Code: "XMinioInvalidIAMCredentials", + Description: "The administrator key is not eligible for this operation", + HTTPStatusCode: http.StatusForbidden, + }, + ErrAccountNotEligible: { + Code: "XMinioInvalidIAMCredentials", + Description: "The account key is not eligible for this operation", + HTTPStatusCode: http.StatusForbidden, + }, + ErrAdminServiceAccountNotFound: { + Code: "XMinioInvalidIAMCredentials", + Description: "The specified service account is not found", + HTTPStatusCode: http.StatusNotFound, + }, + ErrPostPolicyConditionInvalidFormat: { + Code: "PostPolicyInvalidKeyName", + Description: "Invalid according to Policy: Policy Condition failed", + HTTPStatusCode: http.StatusForbidden, + }, + // Add your error structure here. +} + +// toAPIErrorCode - Converts embedded errors. Convenience +// function written to handle all cases where we have known types of +// errors returned by underlying layers. +func toAPIErrorCode(ctx context.Context, err error) (apiErr APIErrorCode) { + if err == nil { + return ErrNone + } + + // Only return ErrClientDisconnected if the provided context is actually canceled. + // This way downstream context.Canceled will still report ErrOperationTimedOut + select { + case <-ctx.Done(): + if ctx.Err() == context.Canceled { + return ErrClientDisconnected + } + default: + } + + switch err { + case errInvalidArgument: + apiErr = ErrAdminInvalidArgument + case errNoSuchUser: + apiErr = ErrAdminNoSuchUser + case errNoSuchServiceAccount: + apiErr = ErrAdminServiceAccountNotFound + case errNoSuchGroup: + apiErr = ErrAdminNoSuchGroup + case errGroupNotEmpty: + apiErr = ErrAdminGroupNotEmpty + case errNoSuchPolicy: + apiErr = ErrAdminNoSuchPolicy + case errSignatureMismatch: + apiErr = ErrSignatureDoesNotMatch + case errInvalidRange: + apiErr = ErrInvalidRange + case errDataTooLarge: + apiErr = ErrEntityTooLarge + case errDataTooSmall: + apiErr = ErrEntityTooSmall + case errAuthentication: + apiErr = ErrAccessDenied + case auth.ErrInvalidAccessKeyLength: + apiErr = ErrAdminInvalidAccessKey + case auth.ErrInvalidSecretKeyLength: + apiErr = ErrAdminInvalidSecretKey + case errInvalidStorageClass: + apiErr = ErrInvalidStorageClass + // SSE errors + case errInvalidEncryptionParameters: + apiErr = ErrInvalidEncryptionParameters + case crypto.ErrInvalidEncryptionMethod: + apiErr = ErrInvalidEncryptionMethod + case crypto.ErrInvalidCustomerAlgorithm: + apiErr = ErrInvalidSSECustomerAlgorithm + case crypto.ErrMissingCustomerKey: + apiErr = ErrMissingSSECustomerKey + case crypto.ErrMissingCustomerKeyMD5: + apiErr = ErrMissingSSECustomerKeyMD5 + case crypto.ErrCustomerKeyMD5Mismatch: + apiErr = ErrSSECustomerKeyMD5Mismatch + case errObjectTampered: + apiErr = ErrObjectTampered + case errEncryptedObject: + apiErr = ErrSSEEncryptedObject + case errInvalidSSEParameters: + apiErr = ErrInvalidSSECustomerParameters + case crypto.ErrInvalidCustomerKey, crypto.ErrSecretKeyMismatch: + apiErr = ErrAccessDenied // no access without correct key + case crypto.ErrIncompatibleEncryptionMethod: + apiErr = ErrIncompatibleEncryptionMethod + case errKMSNotConfigured: + apiErr = ErrKMSNotConfigured + case context.Canceled, context.DeadlineExceeded: + apiErr = ErrOperationTimedOut + case errDiskNotFound: + apiErr = ErrSlowDown + case objectlock.ErrInvalidRetentionDate: + apiErr = ErrInvalidRetentionDate + case objectlock.ErrPastObjectLockRetainDate: + apiErr = ErrPastObjectLockRetainDate + case objectlock.ErrUnknownWORMModeDirective: + apiErr = ErrUnknownWORMModeDirective + case objectlock.ErrObjectLockInvalidHeaders: + apiErr = ErrObjectLockInvalidHeaders + case objectlock.ErrMalformedXML: + apiErr = ErrMalformedXML + } + + // Compression errors + switch err { + case errInvalidDecompressedSize: + apiErr = ErrInvalidDecompressedSize + } + + if apiErr != ErrNone { + // If there was a match in the above switch case. + return apiErr + } + + // etcd specific errors, a key is always a bucket for us return + // ErrNoSuchBucket in such a case. + if err == dns.ErrNoEntriesFound { + return ErrNoSuchBucket + } + + switch err.(type) { + case StorageFull: + apiErr = ErrStorageFull + case hash.BadDigest: + apiErr = ErrBadDigest + case AllAccessDisabled: + apiErr = ErrAllAccessDisabled + case IncompleteBody: + apiErr = ErrIncompleteBody + case ObjectExistsAsDirectory: + apiErr = ErrObjectExistsAsDirectory + case PrefixAccessDenied: + apiErr = ErrAccessDenied + case BucketNameInvalid: + apiErr = ErrInvalidBucketName + case BucketNotFound: + apiErr = ErrNoSuchBucket + case BucketAlreadyOwnedByYou: + apiErr = ErrBucketAlreadyOwnedByYou + case BucketNotEmpty: + apiErr = ErrBucketNotEmpty + case BucketAlreadyExists: + apiErr = ErrBucketAlreadyExists + case BucketExists: + apiErr = ErrBucketAlreadyOwnedByYou + case ObjectNotFound: + apiErr = ErrNoSuchKey + case MethodNotAllowed: + apiErr = ErrMethodNotAllowed + case InvalidVersionID: + apiErr = ErrInvalidVersionID + case VersionNotFound: + apiErr = ErrNoSuchVersion + case ObjectAlreadyExists: + apiErr = ErrMethodNotAllowed + case ObjectNameInvalid: + apiErr = ErrInvalidObjectName + case ObjectNamePrefixAsSlash: + apiErr = ErrInvalidObjectNamePrefixSlash + case InvalidUploadID: + apiErr = ErrNoSuchUpload + case InvalidPart: + apiErr = ErrInvalidPart + case InsufficientWriteQuorum: + apiErr = ErrSlowDown + case InsufficientReadQuorum: + apiErr = ErrSlowDown + case InvalidMarkerPrefixCombination: + apiErr = ErrNotImplemented + case InvalidUploadIDKeyCombination: + apiErr = ErrNotImplemented + case MalformedUploadID: + apiErr = ErrNoSuchUpload + case PartTooSmall: + apiErr = ErrEntityTooSmall + case SignatureDoesNotMatch: + apiErr = ErrSignatureDoesNotMatch + case hash.SHA256Mismatch: + apiErr = ErrContentSHA256Mismatch + case ObjectTooLarge: + apiErr = ErrEntityTooLarge + case ObjectTooSmall: + apiErr = ErrEntityTooSmall + case NotImplemented: + apiErr = ErrNotImplemented + case PartTooBig: + apiErr = ErrEntityTooLarge + case UnsupportedMetadata: + apiErr = ErrUnsupportedMetadata + case BucketPolicyNotFound: + apiErr = ErrNoSuchBucketPolicy + case BucketLifecycleNotFound: + apiErr = ErrNoSuchLifecycleConfiguration + case BucketSSEConfigNotFound: + apiErr = ErrNoSuchBucketSSEConfig + case BucketTaggingNotFound: + apiErr = ErrBucketTaggingNotFound + case BucketObjectLockConfigNotFound: + apiErr = ErrObjectLockConfigurationNotFound + case BucketQuotaConfigNotFound: + apiErr = ErrAdminNoSuchQuotaConfiguration + case BucketReplicationConfigNotFound: + apiErr = ErrReplicationConfigurationNotFoundError + case BucketRemoteDestinationNotFound: + apiErr = ErrRemoteDestinationNotFoundError + case BucketReplicationDestinationMissingLock: + apiErr = ErrReplicationDestinationMissingLock + case BucketRemoteTargetNotFound: + apiErr = ErrRemoteTargetNotFoundError + case BucketRemoteConnectionErr: + apiErr = ErrReplicationRemoteConnectionError + case BucketRemoteAlreadyExists: + apiErr = ErrBucketRemoteAlreadyExists + case BucketRemoteLabelInUse: + apiErr = ErrBucketRemoteLabelInUse + case BucketRemoteArnTypeInvalid: + apiErr = ErrBucketRemoteArnTypeInvalid + case BucketRemoteArnInvalid: + apiErr = ErrBucketRemoteArnInvalid + case BucketRemoteRemoveDisallowed: + apiErr = ErrBucketRemoteRemoveDisallowed + case BucketRemoteTargetNotVersioned: + apiErr = ErrRemoteTargetNotVersionedError + case BucketReplicationSourceNotVersioned: + apiErr = ErrReplicationSourceNotVersionedError + case TransitionStorageClassNotFound: + apiErr = ErrTransitionStorageClassNotFoundError + case InvalidObjectState: + apiErr = ErrInvalidObjectState + + case BucketQuotaExceeded: + apiErr = ErrAdminBucketQuotaExceeded + case *event.ErrInvalidEventName: + apiErr = ErrEventNotification + case *event.ErrInvalidARN: + apiErr = ErrARNNotification + case *event.ErrARNNotFound: + apiErr = ErrARNNotification + case *event.ErrUnknownRegion: + apiErr = ErrRegionNotification + case *event.ErrInvalidFilterName: + apiErr = ErrFilterNameInvalid + case *event.ErrFilterNamePrefix: + apiErr = ErrFilterNamePrefix + case *event.ErrFilterNameSuffix: + apiErr = ErrFilterNameSuffix + case *event.ErrInvalidFilterValue: + apiErr = ErrFilterValueInvalid + case *event.ErrDuplicateEventName: + apiErr = ErrOverlappingConfigs + case *event.ErrDuplicateQueueConfiguration: + apiErr = ErrOverlappingFilterNotification + case *event.ErrUnsupportedConfiguration: + apiErr = ErrUnsupportedNotification + case OperationTimedOut: + apiErr = ErrOperationTimedOut + case BackendDown: + apiErr = ErrBackendDown + case ObjectNameTooLong: + apiErr = ErrKeyTooLongError + case dns.ErrInvalidBucketName: + apiErr = ErrInvalidBucketName + case dns.ErrBucketConflict: + apiErr = ErrBucketAlreadyExists + default: + var ie, iw int + // This work-around is to handle the issue golang/go#30648 + if _, ferr := fmt.Fscanf(strings.NewReader(err.Error()), + "request declared a Content-Length of %d but only wrote %d bytes", + &ie, &iw); ferr != nil { + apiErr = ErrInternalError + // Make sure to log the errors which we cannot translate + // to a meaningful S3 API errors. This is added to aid in + // debugging unexpected/unhandled errors. + logger.LogIf(ctx, err) + } else if ie > iw { + apiErr = ErrIncompleteBody + } else { + apiErr = ErrInternalError + // Make sure to log the errors which we cannot translate + // to a meaningful S3 API errors. This is added to aid in + // debugging unexpected/unhandled errors. + logger.LogIf(ctx, err) + } + } + + return apiErr +} + +var noError = APIError{} + +// toAPIError - Converts embedded errors. Convenience +// function written to handle all cases where we have known types of +// errors returned by underlying layers. +func toAPIError(ctx context.Context, err error) APIError { + if err == nil { + return noError + } + + var apiErr = errorCodes.ToAPIErr(toAPIErrorCode(ctx, err)) + e, ok := err.(dns.ErrInvalidBucketName) + if ok { + code := toAPIErrorCode(ctx, e) + apiErr = errorCodes.ToAPIErrWithErr(code, e) + } + + if apiErr.Code == "NotImplemented" { + switch e := err.(type) { + case NotImplemented: + desc := e.Error() + if desc == "" { + desc = apiErr.Description + } + apiErr = APIError{ + Code: apiErr.Code, + Description: desc, + HTTPStatusCode: apiErr.HTTPStatusCode, + } + return apiErr + } + } + + if apiErr.Code == "InternalError" { + // If we see an internal error try to interpret + // any underlying errors if possible depending on + // their internal error types. This code is only + // useful with gateway implementations. + switch e := err.(type) { + case InvalidArgument: + apiErr = APIError{ + Code: "InvalidArgument", + Description: e.Error(), + HTTPStatusCode: errorCodes[ErrInvalidRequest].HTTPStatusCode, + } + case *xml.SyntaxError: + apiErr = APIError{ + Code: "MalformedXML", + Description: fmt.Sprintf("%s (%s)", errorCodes[ErrMalformedXML].Description, + e.Error()), + HTTPStatusCode: errorCodes[ErrMalformedXML].HTTPStatusCode, + } + case url.EscapeError: + apiErr = APIError{ + Code: "XMinioInvalidObjectName", + Description: fmt.Sprintf("%s (%s)", errorCodes[ErrInvalidObjectName].Description, + e.Error()), + HTTPStatusCode: http.StatusBadRequest, + } + case versioning.Error: + apiErr = APIError{ + Code: "IllegalVersioningConfigurationException", + Description: fmt.Sprintf("Versioning configuration specified in the request is invalid. (%s)", e.Error()), + HTTPStatusCode: http.StatusBadRequest, + } + case lifecycle.Error: + apiErr = APIError{ + Code: "InvalidRequest", + Description: e.Error(), + HTTPStatusCode: http.StatusBadRequest, + } + case replication.Error: + apiErr = APIError{ + Code: "MalformedXML", + Description: e.Error(), + HTTPStatusCode: http.StatusBadRequest, + } + case tags.Error: + apiErr = APIError{ + Code: e.Code(), + Description: e.Error(), + HTTPStatusCode: http.StatusBadRequest, + } + case policy.Error: + apiErr = APIError{ + Code: "MalformedPolicy", + Description: e.Error(), + HTTPStatusCode: http.StatusBadRequest, + } + case crypto.Error: + apiErr = APIError{ + Code: "XMinIOEncryptionError", + Description: e.Error(), + HTTPStatusCode: http.StatusBadRequest, + } + case minio.ErrorResponse: + apiErr = APIError{ + Code: e.Code, + Description: e.Message, + HTTPStatusCode: e.StatusCode, + } + if globalIsGateway && strings.Contains(e.Message, "KMS is not configured") { + apiErr = APIError{ + Code: "NotImplemented", + Description: e.Message, + HTTPStatusCode: http.StatusNotImplemented, + } + } + case *googleapi.Error: + apiErr = APIError{ + Code: "XGCSInternalError", + Description: e.Message, + HTTPStatusCode: e.Code, + } + // GCS may send multiple errors, just pick the first one + // since S3 only sends one Error XML response. + if len(e.Errors) >= 1 { + apiErr.Code = e.Errors[0].Reason + + } + case azblob.StorageError: + apiErr = APIError{ + Code: string(e.ServiceCode()), + Description: e.Error(), + HTTPStatusCode: e.Response().StatusCode, + } + // Add more Gateway SDKs here if any in future. + default: + if errors.Is(err, errMalformedEncoding) { + apiErr = APIError{ + Code: "BadRequest", + Description: err.Error(), + HTTPStatusCode: http.StatusBadRequest, + } + } else if errors.Is(err, errChunkTooBig) { + apiErr = APIError{ + Code: "BadRequest", + Description: err.Error(), + HTTPStatusCode: http.StatusBadRequest, + } + } else if errors.Is(err, strconv.ErrRange) { + apiErr = APIError{ + Code: "BadRequest", + Description: err.Error(), + HTTPStatusCode: http.StatusBadRequest, + } + } else { + apiErr = APIError{ + Code: apiErr.Code, + Description: fmt.Sprintf("%s: cause(%v)", apiErr.Description, err), + HTTPStatusCode: apiErr.HTTPStatusCode, + } + } + } + } + + return apiErr +} + +// getAPIError provides API Error for input API error code. +func getAPIError(code APIErrorCode) APIError { + if apiErr, ok := errorCodes[code]; ok { + return apiErr + } + return errorCodes.ToAPIErr(ErrInternalError) +} + +// getErrorResponse gets in standard error and resource value and +// provides a encodable populated response values +func getAPIErrorResponse(ctx context.Context, err APIError, resource, requestID, hostID string) APIErrorResponse { + reqInfo := logger.GetReqInfo(ctx) + return APIErrorResponse{ + Code: err.Code, + Message: err.Description, + BucketName: reqInfo.BucketName, + Key: reqInfo.ObjectName, + Resource: resource, + Region: globalServerRegion, + RequestID: requestID, + HostID: hostID, + } +} diff --git a/api/minio/api-response.go b/api/minio/api-response.go new file mode 100644 index 000000000..dd329a3b4 --- /dev/null +++ b/api/minio/api-response.go @@ -0,0 +1,842 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "context" + "encoding/base64" + "encoding/xml" + "fmt" + "net/http" + "net/url" + "path" + "strconv" + "strings" + "time" + + "github.com/minio/minio/internal/crypto" + "github.com/minio/minio/internal/handlers" + xhttp "github.com/minio/minio/internal/http" + "github.com/minio/minio/internal/logger" +) + +const ( + // RFC3339 a subset of the ISO8601 timestamp format. e.g 2014-04-29T18:30:38Z + iso8601TimeFormat = "2006-01-02T15:04:05.000Z" // Reply date format with nanosecond precision. + maxObjectList = 1000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse. + maxDeleteList = 10000 // Limit number of objects deleted in a delete call. + maxUploadsList = 10000 // Limit number of uploads in a listUploadsResponse. + maxPartsList = 10000 // Limit number of parts in a listPartsResponse. +) + +// LocationResponse - format for location response. +type LocationResponse struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ LocationConstraint" json:"-"` + Location string `xml:",chardata"` +} + +// PolicyStatus captures information returned by GetBucketPolicyStatusHandler +type PolicyStatus struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ PolicyStatus" json:"-"` + IsPublic string +} + +// ListVersionsResponse - format for list bucket versions response. +type ListVersionsResponse struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListVersionsResult" json:"-"` + + Name string + Prefix string + KeyMarker string + + // When response is truncated (the IsTruncated element value in the response + // is true), you can use the key name in this field as marker in the subsequent + // request to get next set of objects. Server lists objects in alphabetical + // order Note: This element is returned only if you have delimiter request parameter + // specified. If response does not include the NextMaker and it is truncated, + // you can use the value of the last Key in the response as the marker in the + // subsequent request to get the next set of object keys. + NextKeyMarker string `xml:"NextKeyMarker,omitempty"` + + // When the number of responses exceeds the value of MaxKeys, + // NextVersionIdMarker specifies the first object version not + // returned that satisfies the search criteria. Use this value + // for the version-id-marker request parameter in a subsequent request. + NextVersionIDMarker string `xml:"NextVersionIdMarker"` + + // Marks the last version of the Key returned in a truncated response. + VersionIDMarker string `xml:"VersionIdMarker"` + + MaxKeys int + Delimiter string + // A flag that indicates whether or not ListObjects returned all of the results + // that satisfied the search criteria. + IsTruncated bool + + CommonPrefixes []CommonPrefix + Versions []ObjectVersion + + // Encoding type used to encode object keys in the response. + EncodingType string `xml:"EncodingType,omitempty"` +} + +// ListObjectsResponse - format for list objects response. +type ListObjectsResponse struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListBucketResult" json:"-"` + + Name string + Prefix string + Marker string + + // When response is truncated (the IsTruncated element value in the response + // is true), you can use the key name in this field as marker in the subsequent + // request to get next set of objects. Server lists objects in alphabetical + // order Note: This element is returned only if you have delimiter request parameter + // specified. If response does not include the NextMaker and it is truncated, + // you can use the value of the last Key in the response as the marker in the + // subsequent request to get the next set of object keys. + NextMarker string `xml:"NextMarker,omitempty"` + + MaxKeys int + Delimiter string + // A flag that indicates whether or not ListObjects returned all of the results + // that satisfied the search criteria. + IsTruncated bool + + Contents []Object + CommonPrefixes []CommonPrefix + + // Encoding type used to encode object keys in the response. + EncodingType string `xml:"EncodingType,omitempty"` +} + +// ListObjectsV2Response - format for list objects response. +type ListObjectsV2Response struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListBucketResult" json:"-"` + + Name string + Prefix string + StartAfter string `xml:"StartAfter,omitempty"` + // When response is truncated (the IsTruncated element value in the response + // is true), you can use the key name in this field as marker in the subsequent + // request to get next set of objects. Server lists objects in alphabetical + // order Note: This element is returned only if you have delimiter request parameter + // specified. If response does not include the NextMaker and it is truncated, + // you can use the value of the last Key in the response as the marker in the + // subsequent request to get the next set of object keys. + ContinuationToken string `xml:"ContinuationToken,omitempty"` + NextContinuationToken string `xml:"NextContinuationToken,omitempty"` + + KeyCount int + MaxKeys int + Delimiter string + // A flag that indicates whether or not ListObjects returned all of the results + // that satisfied the search criteria. + IsTruncated bool + + Contents []Object + CommonPrefixes []CommonPrefix + + // Encoding type used to encode object keys in the response. + EncodingType string `xml:"EncodingType,omitempty"` +} + +// Part container for part metadata. +type Part struct { + PartNumber int + LastModified string + ETag string + Size int64 +} + +// ListPartsResponse - format for list parts response. +type ListPartsResponse struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListPartsResult" json:"-"` + + Bucket string + Key string + UploadID string `xml:"UploadId"` + + Initiator Initiator + Owner Owner + + // The class of storage used to store the object. + StorageClass string + + PartNumberMarker int + NextPartNumberMarker int + MaxParts int + IsTruncated bool + + // List of parts. + Parts []Part `xml:"Part"` +} + +// ListMultipartUploadsResponse - format for list multipart uploads response. +type ListMultipartUploadsResponse struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListMultipartUploadsResult" json:"-"` + + Bucket string + KeyMarker string + UploadIDMarker string `xml:"UploadIdMarker"` + NextKeyMarker string + NextUploadIDMarker string `xml:"NextUploadIdMarker"` + Delimiter string + Prefix string + EncodingType string `xml:"EncodingType,omitempty"` + MaxUploads int + IsTruncated bool + + // List of pending uploads. + Uploads []Upload `xml:"Upload"` + + // Delimed common prefixes. + CommonPrefixes []CommonPrefix +} + +// ListBucketsResponse - format for list buckets response +type ListBucketsResponse struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ ListAllMyBucketsResult" json:"-"` + + Owner Owner + + // Container for one or more buckets. + Buckets struct { + Buckets []Bucket `xml:"Bucket"` + } // Buckets are nested +} + +// Upload container for in progress multipart upload +type Upload struct { + Key string + UploadID string `xml:"UploadId"` + Initiator Initiator + Owner Owner + StorageClass string + Initiated string +} + +// CommonPrefix container for prefix response in ListObjectsResponse +type CommonPrefix struct { + Prefix string +} + +// Bucket container for bucket metadata +type Bucket struct { + Name string + CreationDate string // time string of format "2006-01-02T15:04:05.000Z" +} + +// ObjectVersion container for object version metadata +type ObjectVersion struct { + Object + IsLatest bool + VersionID string `xml:"VersionId"` + + isDeleteMarker bool +} + +// MarshalXML - marshal ObjectVersion +func (o ObjectVersion) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + if o.isDeleteMarker { + start.Name.Local = "DeleteMarker" + } else { + start.Name.Local = "Version" + } + + type objectVersionWrapper ObjectVersion + return e.EncodeElement(objectVersionWrapper(o), start) +} + +// StringMap is a map[string]string. +type StringMap map[string]string + +// MarshalXML - StringMap marshals into XML. +func (s StringMap) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + + tokens := []xml.Token{start} + + for key, value := range s { + t := xml.StartElement{} + t.Name = xml.Name{ + Space: "", + Local: key, + } + tokens = append(tokens, t, xml.CharData(value), xml.EndElement{Name: t.Name}) + } + + tokens = append(tokens, xml.EndElement{ + Name: start.Name, + }) + + for _, t := range tokens { + if err := e.EncodeToken(t); err != nil { + return err + } + } + + // flush to ensure tokens are written + return e.Flush() +} + +// Object container for object metadata +type Object struct { + Key string + LastModified string // time string of format "2006-01-02T15:04:05.000Z" + ETag string + Size int64 + + // Owner of the object. + Owner Owner + + // The class of storage used to store the object. + StorageClass string + + // UserMetadata user-defined metadata + UserMetadata StringMap `xml:"UserMetadata,omitempty"` +} + +// CopyObjectResponse container returns ETag and LastModified of the successfully copied object +type CopyObjectResponse struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CopyObjectResult" json:"-"` + LastModified string // time string of format "2006-01-02T15:04:05.000Z" + ETag string // md5sum of the copied object. +} + +// CopyObjectPartResponse container returns ETag and LastModified of the successfully copied object +type CopyObjectPartResponse struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CopyPartResult" json:"-"` + LastModified string // time string of format "2006-01-02T15:04:05.000Z" + ETag string // md5sum of the copied object part. +} + +// Initiator inherit from Owner struct, fields are same +type Initiator Owner + +// Owner - bucket owner/principal +type Owner struct { + ID string + DisplayName string +} + +// InitiateMultipartUploadResponse container for InitiateMultiPartUpload response, provides uploadID to start MultiPart upload +type InitiateMultipartUploadResponse struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ InitiateMultipartUploadResult" json:"-"` + + Bucket string + Key string + UploadID string `xml:"UploadId"` +} + +// CompleteMultipartUploadResponse container for completed multipart upload response +type CompleteMultipartUploadResponse struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ CompleteMultipartUploadResult" json:"-"` + + Location string + Bucket string + Key string + ETag string +} + +// DeleteError structure. +type DeleteError struct { + Code string + Message string + Key string + VersionID string `xml:"VersionId"` +} + +// DeleteObjectsResponse container for multiple object deletes. +type DeleteObjectsResponse struct { + XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ DeleteResult" json:"-"` + + // Collection of all deleted objects + DeletedObjects []DeletedObject `xml:"Deleted,omitempty"` + + // Collection of errors deleting certain objects. + Errors []DeleteError `xml:"Error,omitempty"` +} + +// PostResponse container for POST object request when success_action_status is set to 201 +type PostResponse struct { + Bucket string + Key string + ETag string + Location string +} + +// returns "https" if the tls boolean is true, "http" otherwise. +func getURLScheme(tls bool) string { + if tls { + return httpsScheme + } + return httpScheme +} + +// getObjectLocation gets the fully qualified URL of an object. +func getObjectLocation(r *http.Request, domains []string, bucket, object string) string { + // unit tests do not have host set. + if r.Host == "" { + return path.Clean(r.URL.Path) + } + proto := handlers.GetSourceScheme(r) + if proto == "" { + proto = getURLScheme(globalIsTLS) + } + u := &url.URL{ + Host: r.Host, + Path: path.Join(SlashSeparator, bucket, object), + Scheme: proto, + } + // If domain is set then we need to use bucket DNS style. + for _, domain := range domains { + if strings.HasPrefix(r.Host, bucket+"."+domain) { + u.Path = path.Join(SlashSeparator, object) + break + } + } + return u.String() +} + +// generates ListBucketsResponse from array of BucketInfo which can be +// serialized to match XML and JSON API spec output. +func generateListBucketsResponse(buckets []BucketInfo) ListBucketsResponse { + listbuckets := make([]Bucket, 0, len(buckets)) + var data = ListBucketsResponse{} + var owner = Owner{ + ID: globalMinioDefaultOwnerID, + DisplayName: "minio", + } + + for _, bucket := range buckets { + var listbucket = Bucket{} + listbucket.Name = bucket.Name + listbucket.CreationDate = bucket.Created.UTC().Format(iso8601TimeFormat) + listbuckets = append(listbuckets, listbucket) + } + + data.Owner = owner + data.Buckets.Buckets = listbuckets + + return data +} + +// generates an ListBucketVersions response for the said bucket with other enumerated options. +func generateListVersionsResponse(bucket, prefix, marker, versionIDMarker, delimiter, encodingType string, maxKeys int, resp ListObjectVersionsInfo) ListVersionsResponse { + versions := make([]ObjectVersion, 0, len(resp.Objects)) + var owner = Owner{ + ID: globalMinioDefaultOwnerID, + DisplayName: "minio", + } + var data = ListVersionsResponse{} + + for _, object := range resp.Objects { + var content = ObjectVersion{} + if object.Name == "" { + continue + } + content.Key = s3EncodeName(object.Name, encodingType) + content.LastModified = object.ModTime.UTC().Format(iso8601TimeFormat) + if object.ETag != "" { + content.ETag = "\"" + object.ETag + "\"" + } + content.Size = object.Size + if object.StorageClass != "" { + content.StorageClass = object.StorageClass + } else { + content.StorageClass = globalMinioDefaultStorageClass + } + content.Owner = owner + content.VersionID = object.VersionID + if content.VersionID == "" { + content.VersionID = nullVersionID + } + content.IsLatest = object.IsLatest + content.isDeleteMarker = object.DeleteMarker + versions = append(versions, content) + } + + data.Name = bucket + data.Versions = versions + data.EncodingType = encodingType + data.Prefix = s3EncodeName(prefix, encodingType) + data.KeyMarker = s3EncodeName(marker, encodingType) + data.Delimiter = s3EncodeName(delimiter, encodingType) + data.MaxKeys = maxKeys + + data.NextKeyMarker = s3EncodeName(resp.NextMarker, encodingType) + data.NextVersionIDMarker = resp.NextVersionIDMarker + data.VersionIDMarker = versionIDMarker + data.IsTruncated = resp.IsTruncated + + prefixes := make([]CommonPrefix, 0, len(resp.Prefixes)) + for _, prefix := range resp.Prefixes { + var prefixItem = CommonPrefix{} + prefixItem.Prefix = s3EncodeName(prefix, encodingType) + prefixes = append(prefixes, prefixItem) + } + data.CommonPrefixes = prefixes + return data +} + +// generates an ListObjectsV1 response for the said bucket with other enumerated options. +func generateListObjectsV1Response(bucket, prefix, marker, delimiter, encodingType string, maxKeys int, resp ListObjectsInfo) ListObjectsResponse { + contents := make([]Object, 0, len(resp.Objects)) + var owner = Owner{ + ID: globalMinioDefaultOwnerID, + DisplayName: "minio", + } + var data = ListObjectsResponse{} + + for _, object := range resp.Objects { + var content = Object{} + if object.Name == "" { + continue + } + content.Key = s3EncodeName(object.Name, encodingType) + content.LastModified = object.ModTime.UTC().Format(iso8601TimeFormat) + if object.ETag != "" { + content.ETag = "\"" + object.ETag + "\"" + } + content.Size = object.Size + if object.StorageClass != "" { + content.StorageClass = object.StorageClass + } else { + content.StorageClass = globalMinioDefaultStorageClass + } + content.Owner = owner + contents = append(contents, content) + } + data.Name = bucket + data.Contents = contents + + data.EncodingType = encodingType + data.Prefix = s3EncodeName(prefix, encodingType) + data.Marker = s3EncodeName(marker, encodingType) + data.Delimiter = s3EncodeName(delimiter, encodingType) + data.MaxKeys = maxKeys + data.NextMarker = s3EncodeName(resp.NextMarker, encodingType) + data.IsTruncated = resp.IsTruncated + + prefixes := make([]CommonPrefix, 0, len(resp.Prefixes)) + for _, prefix := range resp.Prefixes { + var prefixItem = CommonPrefix{} + prefixItem.Prefix = s3EncodeName(prefix, encodingType) + prefixes = append(prefixes, prefixItem) + } + data.CommonPrefixes = prefixes + return data +} + +// generates an ListObjectsV2 response for the said bucket with other enumerated options. +func generateListObjectsV2Response(bucket, prefix, token, nextToken, startAfter, delimiter, encodingType string, fetchOwner, isTruncated bool, maxKeys int, objects []ObjectInfo, prefixes []string, metadata bool) ListObjectsV2Response { + contents := make([]Object, 0, len(objects)) + var owner = Owner{ + ID: globalMinioDefaultOwnerID, + DisplayName: "minio", + } + var data = ListObjectsV2Response{} + + for _, object := range objects { + var content = Object{} + if object.Name == "" { + continue + } + content.Key = s3EncodeName(object.Name, encodingType) + content.LastModified = object.ModTime.UTC().Format(iso8601TimeFormat) + if object.ETag != "" { + content.ETag = "\"" + object.ETag + "\"" + } + content.Size = object.Size + if object.StorageClass != "" { + content.StorageClass = object.StorageClass + } else { + content.StorageClass = globalMinioDefaultStorageClass + } + content.Owner = owner + if metadata { + content.UserMetadata = make(StringMap) + switch kind, _ := crypto.IsEncrypted(object.UserDefined); kind { + case crypto.S3: + content.UserMetadata[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionAES + case crypto.S3KMS: + content.UserMetadata[xhttp.AmzServerSideEncryption] = xhttp.AmzEncryptionKMS + case crypto.SSEC: + content.UserMetadata[xhttp.AmzServerSideEncryptionCustomerAlgorithm] = xhttp.AmzEncryptionAES + } + for k, v := range CleanMinioInternalMetadataKeys(object.UserDefined) { + if strings.HasPrefix(strings.ToLower(k), ReservedMetadataPrefixLower) { + // Do not need to send any internal metadata + // values to client. + continue + } + // https://github.com/google/security-research/security/advisories/GHSA-76wf-9vgp-pj7w + if equals(k, xhttp.AmzMetaUnencryptedContentLength, xhttp.AmzMetaUnencryptedContentMD5) { + continue + } + content.UserMetadata[k] = v + } + } + contents = append(contents, content) + } + data.Name = bucket + data.Contents = contents + + data.EncodingType = encodingType + data.StartAfter = s3EncodeName(startAfter, encodingType) + data.Delimiter = s3EncodeName(delimiter, encodingType) + data.Prefix = s3EncodeName(prefix, encodingType) + data.MaxKeys = maxKeys + data.ContinuationToken = base64.StdEncoding.EncodeToString([]byte(token)) + data.NextContinuationToken = base64.StdEncoding.EncodeToString([]byte(nextToken)) + data.IsTruncated = isTruncated + + commonPrefixes := make([]CommonPrefix, 0, len(prefixes)) + for _, prefix := range prefixes { + var prefixItem = CommonPrefix{} + prefixItem.Prefix = s3EncodeName(prefix, encodingType) + commonPrefixes = append(commonPrefixes, prefixItem) + } + data.CommonPrefixes = commonPrefixes + data.KeyCount = len(data.Contents) + len(data.CommonPrefixes) + return data +} + +// generates CopyObjectResponse from etag and lastModified time. +func generateCopyObjectResponse(etag string, lastModified time.Time) CopyObjectResponse { + return CopyObjectResponse{ + ETag: "\"" + etag + "\"", + LastModified: lastModified.UTC().Format(iso8601TimeFormat), + } +} + +// generates CopyObjectPartResponse from etag and lastModified time. +func generateCopyObjectPartResponse(etag string, lastModified time.Time) CopyObjectPartResponse { + return CopyObjectPartResponse{ + ETag: "\"" + etag + "\"", + LastModified: lastModified.UTC().Format(iso8601TimeFormat), + } +} + +// generates InitiateMultipartUploadResponse for given bucket, key and uploadID. +func generateInitiateMultipartUploadResponse(bucket, key, uploadID string) InitiateMultipartUploadResponse { + return InitiateMultipartUploadResponse{ + Bucket: bucket, + Key: key, + UploadID: uploadID, + } +} + +// generates CompleteMultipartUploadResponse for given bucket, key, location and ETag. +func generateCompleteMultpartUploadResponse(bucket, key, location, etag string) CompleteMultipartUploadResponse { + return CompleteMultipartUploadResponse{ + Location: location, + Bucket: bucket, + Key: key, + // AWS S3 quotes the ETag in XML, make sure we are compatible here. + ETag: "\"" + etag + "\"", + } +} + +// generates ListPartsResponse from ListPartsInfo. +func generateListPartsResponse(partsInfo ListPartsInfo, encodingType string) ListPartsResponse { + listPartsResponse := ListPartsResponse{} + listPartsResponse.Bucket = partsInfo.Bucket + listPartsResponse.Key = s3EncodeName(partsInfo.Object, encodingType) + listPartsResponse.UploadID = partsInfo.UploadID + listPartsResponse.StorageClass = globalMinioDefaultStorageClass + + // Dumb values not meaningful + listPartsResponse.Initiator = Initiator{ + ID: globalMinioDefaultOwnerID, + DisplayName: globalMinioDefaultOwnerID, + } + listPartsResponse.Owner = Owner{ + ID: globalMinioDefaultOwnerID, + DisplayName: globalMinioDefaultOwnerID, + } + + listPartsResponse.MaxParts = partsInfo.MaxParts + listPartsResponse.PartNumberMarker = partsInfo.PartNumberMarker + listPartsResponse.IsTruncated = partsInfo.IsTruncated + listPartsResponse.NextPartNumberMarker = partsInfo.NextPartNumberMarker + + listPartsResponse.Parts = make([]Part, len(partsInfo.Parts)) + for index, part := range partsInfo.Parts { + newPart := Part{} + newPart.PartNumber = part.PartNumber + newPart.ETag = "\"" + part.ETag + "\"" + newPart.Size = part.Size + newPart.LastModified = part.LastModified.UTC().Format(iso8601TimeFormat) + listPartsResponse.Parts[index] = newPart + } + return listPartsResponse +} + +// generates ListMultipartUploadsResponse for given bucket and ListMultipartsInfo. +func generateListMultipartUploadsResponse(bucket string, multipartsInfo ListMultipartsInfo, encodingType string) ListMultipartUploadsResponse { + listMultipartUploadsResponse := ListMultipartUploadsResponse{} + listMultipartUploadsResponse.Bucket = bucket + listMultipartUploadsResponse.Delimiter = s3EncodeName(multipartsInfo.Delimiter, encodingType) + listMultipartUploadsResponse.IsTruncated = multipartsInfo.IsTruncated + listMultipartUploadsResponse.EncodingType = encodingType + listMultipartUploadsResponse.Prefix = s3EncodeName(multipartsInfo.Prefix, encodingType) + listMultipartUploadsResponse.KeyMarker = s3EncodeName(multipartsInfo.KeyMarker, encodingType) + listMultipartUploadsResponse.NextKeyMarker = s3EncodeName(multipartsInfo.NextKeyMarker, encodingType) + listMultipartUploadsResponse.MaxUploads = multipartsInfo.MaxUploads + listMultipartUploadsResponse.NextUploadIDMarker = multipartsInfo.NextUploadIDMarker + listMultipartUploadsResponse.UploadIDMarker = multipartsInfo.UploadIDMarker + listMultipartUploadsResponse.CommonPrefixes = make([]CommonPrefix, len(multipartsInfo.CommonPrefixes)) + for index, commonPrefix := range multipartsInfo.CommonPrefixes { + listMultipartUploadsResponse.CommonPrefixes[index] = CommonPrefix{ + Prefix: s3EncodeName(commonPrefix, encodingType), + } + } + listMultipartUploadsResponse.Uploads = make([]Upload, len(multipartsInfo.Uploads)) + for index, upload := range multipartsInfo.Uploads { + newUpload := Upload{} + newUpload.UploadID = upload.UploadID + newUpload.Key = s3EncodeName(upload.Object, encodingType) + newUpload.Initiated = upload.Initiated.UTC().Format(iso8601TimeFormat) + listMultipartUploadsResponse.Uploads[index] = newUpload + } + return listMultipartUploadsResponse +} + +// generate multi objects delete response. +func generateMultiDeleteResponse(quiet bool, deletedObjects []DeletedObject, errs []DeleteError) DeleteObjectsResponse { + deleteResp := DeleteObjectsResponse{} + if !quiet { + deleteResp.DeletedObjects = deletedObjects + } + if len(errs) == len(deletedObjects) { + deleteResp.DeletedObjects = nil + } + deleteResp.Errors = errs + return deleteResp +} + +func writeResponse(w http.ResponseWriter, statusCode int, response []byte, mType mimeType) { + setCommonHeaders(w) + if mType != mimeNone { + w.Header().Set(xhttp.ContentType, string(mType)) + } + w.Header().Set(xhttp.ContentLength, strconv.Itoa(len(response))) + w.WriteHeader(statusCode) + if response != nil { + w.Write(response) + w.(http.Flusher).Flush() + } +} + +// mimeType represents various MIME type used API responses. +type mimeType string + +const ( + // Means no response type. + mimeNone mimeType = "" + // Means response type is JSON. + mimeJSON mimeType = "application/json" + // Means response type is XML. + mimeXML mimeType = "application/xml" +) + +// writeSuccessResponseJSON writes success headers and response if any, +// with content-type set to `application/json`. +func writeSuccessResponseJSON(w http.ResponseWriter, response []byte) { + writeResponse(w, http.StatusOK, response, mimeJSON) +} + +// writeSuccessResponseXML writes success headers and response if any, +// with content-type set to `application/xml`. +func writeSuccessResponseXML(w http.ResponseWriter, response []byte) { + writeResponse(w, http.StatusOK, response, mimeXML) +} + +// writeSuccessNoContent writes success headers with http status 204 +func writeSuccessNoContent(w http.ResponseWriter) { + writeResponse(w, http.StatusNoContent, nil, mimeNone) +} + +// writeRedirectSeeOther writes Location header with http status 303 +func writeRedirectSeeOther(w http.ResponseWriter, location string) { + w.Header().Set(xhttp.Location, location) + writeResponse(w, http.StatusSeeOther, nil, mimeNone) +} + +func writeSuccessResponseHeadersOnly(w http.ResponseWriter) { + writeResponse(w, http.StatusOK, nil, mimeNone) +} + +// writeErrorRespone writes error headers +func writeErrorResponse(ctx context.Context, w http.ResponseWriter, err APIError, reqURL *url.URL) { + switch err.Code { + case "SlowDown", "XMinioServerNotInitialized", "XMinioReadQuorum", "XMinioWriteQuorum": + // Set retry-after header to indicate user-agents to retry request after 120secs. + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + w.Header().Set(xhttp.RetryAfter, "120") + case "InvalidRegion": + err.Description = fmt.Sprintf("Region does not match; expecting '%s'.", globalServerRegion) + case "AuthorizationHeaderMalformed": + err.Description = fmt.Sprintf("The authorization header is malformed; the region is wrong; expecting '%s'.", globalServerRegion) + } + + // Generate error response. + errorResponse := getAPIErrorResponse(ctx, err, reqURL.Path, + w.Header().Get(xhttp.AmzRequestID), globalDeploymentID) + encodedErrorResponse := encodeResponse(errorResponse) + writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeXML) +} + +func writeErrorResponseHeadersOnly(w http.ResponseWriter, err APIError) { + writeResponse(w, err.HTTPStatusCode, nil, mimeNone) +} + +func writeErrorResponseString(ctx context.Context, w http.ResponseWriter, err APIError, reqURL *url.URL) { + // Generate string error response. + writeResponse(w, err.HTTPStatusCode, []byte(err.Description), mimeNone) +} + +// writeErrorResponseJSON - writes error response in JSON format; +// useful for admin APIs. +func writeErrorResponseJSON(ctx context.Context, w http.ResponseWriter, err APIError, reqURL *url.URL) { + // Generate error response. + errorResponse := getAPIErrorResponse(ctx, err, reqURL.Path, w.Header().Get(xhttp.AmzRequestID), globalDeploymentID) + encodedErrorResponse := encodeResponseJSON(errorResponse) + writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeJSON) +} + +// writeCustomErrorResponseJSON - similar to writeErrorResponseJSON, +// but accepts the error message directly (this allows messages to be +// dynamically generated.) +func writeCustomErrorResponseJSON(ctx context.Context, w http.ResponseWriter, err APIError, + errBody string, reqURL *url.URL) { + + reqInfo := logger.GetReqInfo(ctx) + errorResponse := APIErrorResponse{ + Code: err.Code, + Message: errBody, + Resource: reqURL.Path, + BucketName: reqInfo.BucketName, + Key: reqInfo.ObjectName, + RequestID: w.Header().Get(xhttp.AmzRequestID), + HostID: globalDeploymentID, + } + encodedErrorResponse := encodeResponseJSON(errorResponse) + writeResponse(w, err.HTTPStatusCode, encodedErrorResponse, mimeJSON) +} diff --git a/api/minio/api-utils.go b/api/minio/api-utils.go new file mode 100644 index 000000000..fa277b9fc --- /dev/null +++ b/api/minio/api-utils.go @@ -0,0 +1,108 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package cmd + +import ( + "strings" +) + +func shouldEscape(c byte) bool { + if 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' { + return false + } + + switch c { + case '-', '_', '.', '/', '*': + return false + } + return true +} + +// s3URLEncode is based on Golang's url.QueryEscape() code, +// while considering some S3 exceptions: +// - Avoid encoding '/' and '*' +// - Force encoding of '~' +func s3URLEncode(s string) string { + spaceCount, hexCount := 0, 0 + for i := 0; i < len(s); i++ { + c := s[i] + if shouldEscape(c) { + if c == ' ' { + spaceCount++ + } else { + hexCount++ + } + } + } + + if spaceCount == 0 && hexCount == 0 { + return s + } + + var buf [64]byte + var t []byte + + required := len(s) + 2*hexCount + if required <= len(buf) { + t = buf[:required] + } else { + t = make([]byte, required) + } + + if hexCount == 0 { + copy(t, s) + for i := 0; i < len(s); i++ { + if s[i] == ' ' { + t[i] = '+' + } + } + return string(t) + } + + j := 0 + for i := 0; i < len(s); i++ { + switch c := s[i]; { + case c == ' ': + t[j] = '+' + j++ + case shouldEscape(c): + t[j] = '%' + t[j+1] = "0123456789ABCDEF"[c>>4] + t[j+2] = "0123456789ABCDEF"[c&15] + j += 3 + default: + t[j] = s[i] + j++ + } + } + return string(t) +} + +// s3EncodeName encodes string in response when encodingType is specified in AWS S3 requests. +func s3EncodeName(name string, encodingType string) (result string) { + // Quick path to exit + if encodingType == "" { + return name + } + encodingType = strings.ToLower(encodingType) + switch encodingType { + case "url": + return s3URLEncode(name) + } + return name +} diff --git a/api/minio/header.go b/api/minio/header.go new file mode 100644 index 000000000..58903c46a --- /dev/null +++ b/api/minio/header.go @@ -0,0 +1,86 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package crypto + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "net/http" + + xhttp "github.com/minio/minio/internal/http" +) + +// RemoveSensitiveHeaders removes confidential encryption +// information - e.g. the SSE-C key - from the HTTP headers. +// It has the same semantics as RemoveSensitiveEntires. +func RemoveSensitiveHeaders(h http.Header) { + h.Del(xhttp.AmzServerSideEncryptionCustomerKey) + h.Del(xhttp.AmzServerSideEncryptionCopyCustomerKey) + h.Del(xhttp.AmzMetaUnencryptedContentLength) + h.Del(xhttp.AmzMetaUnencryptedContentMD5) +} + +var ( + // SSECopy represents AWS SSE-C for copy requests. It provides + // functionality to handle SSE-C copy requests. + SSECopy = ssecCopy{} +) + +type ssecCopy struct{} + +// IsRequested returns true if the HTTP headers contains +// at least one SSE-C copy header. Regular SSE-C headers +// are ignored. +func (ssecCopy) IsRequested(h http.Header) bool { + if _, ok := h[xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm]; ok { + return true + } + if _, ok := h[xhttp.AmzServerSideEncryptionCopyCustomerKey]; ok { + return true + } + if _, ok := h[xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5]; ok { + return true + } + return false +} + +// ParseHTTP parses the SSE-C copy headers and returns the SSE-C client key +// on success. Regular SSE-C headers are ignored. +func (ssecCopy) ParseHTTP(h http.Header) (key [32]byte, err error) { + if h.Get(xhttp.AmzServerSideEncryptionCopyCustomerAlgorithm) != xhttp.AmzEncryptionAES { + return key, ErrInvalidCustomerAlgorithm + } + if h.Get(xhttp.AmzServerSideEncryptionCopyCustomerKey) == "" { + return key, ErrMissingCustomerKey + } + if h.Get(xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5) == "" { + return key, ErrMissingCustomerKeyMD5 + } + + clientKey, err := base64.StdEncoding.DecodeString(h.Get(xhttp.AmzServerSideEncryptionCopyCustomerKey)) + if err != nil || len(clientKey) != 32 { // The client key must be 256 bits long + return key, ErrInvalidCustomerKey + } + keyMD5, err := base64.StdEncoding.DecodeString(h.Get(xhttp.AmzServerSideEncryptionCopyCustomerKeyMD5)) + if md5Sum := md5.Sum(clientKey); err != nil || !bytes.Equal(md5Sum[:], keyMD5) { + return key, ErrCustomerKeyMD5Mismatch + } + copy(key[:], clientKey) + return key, nil +} diff --git a/api/minio/proxy.go b/api/minio/proxy.go new file mode 100644 index 000000000..f23d4fd73 --- /dev/null +++ b/api/minio/proxy.go @@ -0,0 +1,127 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// Originally from https://github.com/gorilla/handlers with following license +// https://raw.githubusercontent.com/gorilla/handlers/master/LICENSE, forked +// and heavily modified for MinIO's internal needs. + +package handlers + +import ( + "net" + "net/http" + "regexp" + "strings" +) + +var ( + // De-facto standard header keys. + xForwardedFor = http.CanonicalHeaderKey("X-Forwarded-For") + xForwardedHost = http.CanonicalHeaderKey("X-Forwarded-Host") + xForwardedPort = http.CanonicalHeaderKey("X-Forwarded-Port") + xForwardedProto = http.CanonicalHeaderKey("X-Forwarded-Proto") + xForwardedScheme = http.CanonicalHeaderKey("X-Forwarded-Scheme") + xRealIP = http.CanonicalHeaderKey("X-Real-IP") +) + +var ( + // RFC7239 defines a new "Forwarded: " header designed to replace the + // existing use of X-Forwarded-* headers. + // e.g. Forwarded: for=192.0.2.60;proto=https;by=203.0.113.43 + forwarded = http.CanonicalHeaderKey("Forwarded") + // Allows for a sub-match of the first value after 'for=' to the next + // comma, semi-colon or space. The match is case-insensitive. + forRegex = regexp.MustCompile(`(?i)(?:for=)([^(;|,| )]+)(.*)`) + // Allows for a sub-match for the first instance of scheme (http|https) + // prefixed by 'proto='. The match is case-insensitive. + protoRegex = regexp.MustCompile(`(?i)^(;|,| )+(?:proto=)(https|http)`) +) + +// GetSourceScheme retrieves the scheme from the X-Forwarded-Proto and RFC7239 +// Forwarded headers (in that order). +func GetSourceScheme(r *http.Request) string { + var scheme string + + // Retrieve the scheme from X-Forwarded-Proto. + if proto := r.Header.Get(xForwardedProto); proto != "" { + scheme = strings.ToLower(proto) + } else if proto = r.Header.Get(xForwardedScheme); proto != "" { + scheme = strings.ToLower(proto) + } else if proto := r.Header.Get(forwarded); proto != "" { + // match should contain at least two elements if the protocol was + // specified in the Forwarded header. The first element will always be + // the 'for=', which we ignore, subsequently we proceed to look for + // 'proto=' which should precede right after `for=` if not + // we simply ignore the values and return empty. This is in line + // with the approach we took for returning first ip from multiple + // params. + if match := forRegex.FindStringSubmatch(proto); len(match) > 1 { + if match = protoRegex.FindStringSubmatch(match[2]); len(match) > 1 { + scheme = strings.ToLower(match[2]) + } + } + } + + return scheme +} + +// GetSourceIPFromHeaders retrieves the IP from the X-Forwarded-For, X-Real-IP +// and RFC7239 Forwarded headers (in that order) +func GetSourceIPFromHeaders(r *http.Request) string { + var addr string + + if fwd := r.Header.Get(xForwardedFor); fwd != "" { + // Only grab the first (client) address. Note that '192.168.0.1, + // 10.1.1.1' is a valid key for X-Forwarded-For where addresses after + // the first may represent forwarding proxies earlier in the chain. + s := strings.Index(fwd, ", ") + if s == -1 { + s = len(fwd) + } + addr = fwd[:s] + } else if fwd := r.Header.Get(xRealIP); fwd != "" { + // X-Real-IP should only contain one IP address (the client making the + // request). + addr = fwd + } else if fwd := r.Header.Get(forwarded); fwd != "" { + // match should contain at least two elements if the protocol was + // specified in the Forwarded header. The first element will always be + // the 'for=' capture, which we ignore. In the case of multiple IP + // addresses (for=8.8.8.8, 8.8.4.4, 172.16.1.20 is valid) we only + // extract the first, which should be the client IP. + if match := forRegex.FindStringSubmatch(fwd); len(match) > 1 { + // IPv6 addresses in Forwarded headers are quoted-strings. We strip + // these quotes. + addr = strings.Trim(match[1], `"`) + } + } + + return addr +} + +// GetSourceIP retrieves the IP from the request headers +// and falls back to r.RemoteAddr when necessary. +func GetSourceIP(r *http.Request) string { + addr := GetSourceIPFromHeaders(r) + if addr != "" { + return addr + } + + // Default to remote address if headers not set. + addr, _, _ = net.SplitHostPort(r.RemoteAddr) + return addr +} diff --git a/api/minio/reqinfo.go b/api/minio/reqinfo.go new file mode 100644 index 000000000..56e8c3ea8 --- /dev/null +++ b/api/minio/reqinfo.go @@ -0,0 +1,144 @@ +// Copyright (c) 2015-2021 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package logger + +import ( + "context" + "fmt" + "sync" +) + +// Key used for Get/SetReqInfo +type contextKeyType string + +const contextLogKey = contextKeyType("miniolog") + +// KeyVal - appended to ReqInfo.Tags +type KeyVal struct { + Key string + Val interface{} +} + +// ReqInfo stores the request info. +type ReqInfo struct { + RemoteHost string // Client Host/IP + Host string // Node Host/IP + UserAgent string // User Agent + DeploymentID string // x-minio-deployment-id + RequestID string // x-amz-request-id + API string // API name - GetObject PutObject NewMultipartUpload etc. + BucketName string // Bucket name + ObjectName string // Object name + AccessKey string // Access Key + tags []KeyVal // Any additional info not accommodated by above fields + sync.RWMutex +} + +// NewReqInfo : +func NewReqInfo(remoteHost, userAgent, deploymentID, requestID, api, bucket, object string) *ReqInfo { + req := ReqInfo{} + req.RemoteHost = remoteHost + req.UserAgent = userAgent + req.API = api + req.DeploymentID = deploymentID + req.RequestID = requestID + req.BucketName = bucket + req.ObjectName = object + return &req +} + +// AppendTags - appends key/val to ReqInfo.tags +func (r *ReqInfo) AppendTags(key string, val interface{}) *ReqInfo { + if r == nil { + return nil + } + r.Lock() + defer r.Unlock() + r.tags = append(r.tags, KeyVal{key, val}) + return r +} + +// SetTags - sets key/val to ReqInfo.tags +func (r *ReqInfo) SetTags(key string, val interface{}) *ReqInfo { + if r == nil { + return nil + } + r.Lock() + defer r.Unlock() + // Search of tag key already exists in tags + var updated bool + for _, tag := range r.tags { + if tag.Key == key { + tag.Val = val + updated = true + break + } + } + if !updated { + // Append to the end of tags list + r.tags = append(r.tags, KeyVal{key, val}) + } + return r +} + +// GetTags - returns the user defined tags +func (r *ReqInfo) GetTags() []KeyVal { + if r == nil { + return nil + } + r.RLock() + defer r.RUnlock() + return append([]KeyVal(nil), r.tags...) +} + +// GetTagsMap - returns the user defined tags in a map structure +func (r *ReqInfo) GetTagsMap() map[string]interface{} { + if r == nil { + return nil + } + r.RLock() + defer r.RUnlock() + m := make(map[string]interface{}, len(r.tags)) + for _, t := range r.tags { + m[t.Key] = t.Val + } + return m +} + +// SetReqInfo sets ReqInfo in the context. +func SetReqInfo(ctx context.Context, req *ReqInfo) context.Context { + if ctx == nil { + LogIf(context.Background(), fmt.Errorf("context is nil")) + return nil + } + return context.WithValue(ctx, contextLogKey, req) +} + +// GetReqInfo returns ReqInfo if set. +func GetReqInfo(ctx context.Context) *ReqInfo { + if ctx != nil { + r, ok := ctx.Value(contextLogKey).(*ReqInfo) + if ok { + return r + } + r = &ReqInfo{} + SetReqInfo(ctx, r) + return r + } + return nil +}