/* * MinIO Cloud Storage, (C) 2019 MinIO, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package s3_constants import ( "context" "net/http" "strings" "sync/atomic" "github.com/gorilla/mux" ) // S3 XML namespace const ( S3Namespace = "http://s3.amazonaws.com/doc/2006-03-01/" ) // S3 object key limits const ( MaxS3ObjectKeyLength = 1024 ) // Standard S3 HTTP request constants const ( // S3 storage class AmzStorageClass = "X-Amz-Storage-Class" // S3 user-defined metadata AmzUserMetaPrefix = "X-Amz-Meta-" AmzUserMetaDirective = "X-Amz-Metadata-Directive" AmzUserMetaMtime = "X-Amz-Meta-Mtime" // S3 object tagging AmzObjectTagging = "X-Amz-Tagging" AmzObjectTaggingPrefix = "X-Amz-Tagging-" AmzObjectTaggingDirective = "X-Amz-Tagging-Directive" AmzTagCount = "x-amz-tagging-count" // GetObjectAttributes headers AmzObjectAttributes = "X-Amz-Object-Attributes" AmzMaxParts = "X-Amz-Max-Parts" AmzPartNumberMarker = "X-Amz-Part-Number-Marker" AmzDeleteMarker = "X-Amz-Delete-Marker" SeaweedFSUploadId = "X-Seaweedfs-Upload-Id" SeaweedFSMultipartPartsCount = "X-Seaweedfs-Multipart-Parts-Count" SeaweedFSMultipartPartBoundaries = "X-Seaweedfs-Multipart-Part-Boundaries" // JSON: [{part:1,start:0,end:2,etag:"abc"},{part:2,start:2,end:3,etag:"def"}] SeaweedFSExpiresS3 = "X-Seaweedfs-Expires-S3" AmzMpPartsCount = "x-amz-mp-parts-count" // S3 ACL headers AmzCannedAcl = "X-Amz-Acl" AmzAclFullControl = "X-Amz-Grant-Full-Control" AmzAclRead = "X-Amz-Grant-Read" AmzAclWrite = "X-Amz-Grant-Write" AmzAclReadAcp = "X-Amz-Grant-Read-Acp" AmzAclWriteAcp = "X-Amz-Grant-Write-Acp" // S3 Object Lock headers AmzBucketObjectLockEnabled = "X-Amz-Bucket-Object-Lock-Enabled" AmzObjectLockMode = "X-Amz-Object-Lock-Mode" AmzObjectLockRetainUntilDate = "X-Amz-Object-Lock-Retain-Until-Date" AmzObjectLockLegalHold = "X-Amz-Object-Lock-Legal-Hold" // S3 checksum headers AmzChecksumAlgorithm = "X-Amz-Checksum-Algorithm" AmzChecksumCRC32 = "X-Amz-Checksum-Crc32" AmzChecksumCRC32C = "X-Amz-Checksum-Crc32c" AmzChecksumCRC64NVME = "X-Amz-Checksum-Crc64nvme" AmzChecksumSHA1 = "X-Amz-Checksum-Sha1" AmzChecksumSHA256 = "X-Amz-Checksum-Sha256" AmzTrailer = "X-Amz-Trailer" AmzSdkChecksumAlgorithm = "X-Amz-Sdk-Checksum-Algorithm" AmzChecksumType = "X-Amz-Checksum-Type" // S3 checksum type values (x-amz-checksum-type). A COMPOSITE checksum is a // checksum-of-per-part-checksums ("base64-N"); a FULL_OBJECT checksum is the // checksum of the whole object as if uploaded in a single request ("base64"). ChecksumTypeComposite = "COMPOSITE" ChecksumTypeFullObject = "FULL_OBJECT" // S3 conditional headers IfMatch = "If-Match" IfNoneMatch = "If-None-Match" IfModifiedSince = "If-Modified-Since" IfUnmodifiedSince = "If-Unmodified-Since" // S3 conditional copy headers AmzCopySourceIfMatch = "X-Amz-Copy-Source-If-Match" AmzCopySourceIfNoneMatch = "X-Amz-Copy-Source-If-None-Match" AmzCopySourceIfModifiedSince = "X-Amz-Copy-Source-If-Modified-Since" AmzCopySourceIfUnmodifiedSince = "X-Amz-Copy-Source-If-Unmodified-Since" // RenameObject AmzRenameSource = "X-Amz-Rename-Source" AmzRenameSourceIfMatch = "X-Amz-Rename-Source-If-Match" AmzRenameSourceIfNoneMatch = "X-Amz-Rename-Source-If-None-Match" AmzRenameSourceIfModifiedSince = "X-Amz-Rename-Source-If-Modified-Since" AmzRenameSourceIfUnmodifiedSince = "X-Amz-Rename-Source-If-Unmodified-Since" // S3 Server-Side Encryption with Customer-provided Keys (SSE-C) AmzServerSideEncryptionCustomerAlgorithm = "X-Amz-Server-Side-Encryption-Customer-Algorithm" AmzServerSideEncryptionCustomerKey = "X-Amz-Server-Side-Encryption-Customer-Key" AmzServerSideEncryptionCustomerKeyMD5 = "X-Amz-Server-Side-Encryption-Customer-Key-MD5" AmzServerSideEncryptionContext = "X-Amz-Server-Side-Encryption-Context" // S3 Server-Side Encryption with KMS (SSE-KMS) AmzServerSideEncryption = "X-Amz-Server-Side-Encryption" AmzServerSideEncryptionAwsKmsKeyId = "X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id" AmzServerSideEncryptionBucketKeyEnabled = "X-Amz-Server-Side-Encryption-Bucket-Key-Enabled" // S3 SSE-C copy source headers AmzCopySourceServerSideEncryptionCustomerAlgorithm = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm" AmzCopySourceServerSideEncryptionCustomerKey = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key" AmzCopySourceServerSideEncryptionCustomerKeyMD5 = "X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-MD5" ) // Metadata keys for internal storage const ( // SSE-KMS metadata keys AmzEncryptedDataKey = "x-amz-encrypted-data-key" AmzEncryptionContextMeta = "x-amz-encryption-context" // SeaweedFS internal metadata prefix (used to filter internal headers from client responses) SeaweedFSInternalPrefix = "x-seaweedfs-" // SeaweedFSPrefixObject marks a directory entry that also holds the object named by // its path without a trailing slash. S3 keys are flat, so "a/b" and "a/b/c" are // independent keys, and the filer stores the first one on the directory the second // one lives under. SeaweedFSPrefixObject = "x-seaweedfs-prefix-object" // SeaweedFS internal metadata keys for encryption (prefixed to avoid automatic HTTP header conversion) SeaweedFSSSEKMSKey = "x-seaweedfs-sse-kms-key" // Key for storing serialized SSE-KMS metadata SeaweedFSSSES3Key = "x-seaweedfs-sse-s3-key" // Key for storing serialized SSE-S3 metadata SeaweedFSSSEIV = "x-seaweedfs-sse-c-iv" // Key for storing SSE-C IV // Multipart upload metadata keys for SSE-KMS (consistent with internal metadata key pattern) SeaweedFSSSEKMSKeyID = "x-seaweedfs-sse-kms-key-id" // Key ID for multipart upload SSE-KMS inheritance SeaweedFSSSEKMSEncryption = "x-seaweedfs-sse-kms-encryption" // Encryption type for multipart upload SSE-KMS inheritance SeaweedFSSSEKMSBucketKeyEnabled = "x-seaweedfs-sse-kms-bucket-key-enabled" // Bucket key setting for multipart upload SSE-KMS inheritance SeaweedFSSSEKMSEncryptionContext = "x-seaweedfs-sse-kms-encryption-context" // Encryption context for multipart upload SSE-KMS inheritance SeaweedFSSSEKMSBaseIV = "x-seaweedfs-sse-kms-base-iv" // Base IV for multipart upload SSE-KMS (for IV offset calculation) // Multipart upload metadata keys for SSE-S3 SeaweedFSSSES3Encryption = "x-seaweedfs-sse-s3-encryption" // Encryption type for multipart upload SSE-S3 inheritance SeaweedFSSSES3BaseIV = "x-seaweedfs-sse-s3-base-iv" // Base IV for multipart upload SSE-S3 (for IV offset calculation) SeaweedFSSSES3KeyData = "x-seaweedfs-sse-s3-key-data" // Encrypted key data for multipart upload SSE-S3 inheritance ) // SeaweedFS internal headers for filer communication const ( SeaweedFSSSEKMSKeyHeader = "X-SeaweedFS-SSE-KMS-Key" // Header for passing SSE-KMS metadata to filer SeaweedFSSSEIVHeader = "X-SeaweedFS-SSE-IV" // Header for passing SSE-C IV to filer (SSE-C only) SeaweedFSSSEKMSBaseIVHeader = "X-SeaweedFS-SSE-KMS-Base-IV" // Header for passing base IV for multipart SSE-KMS SeaweedFSSSES3BaseIVHeader = "X-SeaweedFS-SSE-S3-Base-IV" // Header for passing base IV for multipart SSE-S3 SeaweedFSSSES3KeyDataHeader = "X-SeaweedFS-SSE-S3-Key-Data" // Header for passing key data for multipart SSE-S3 // SeaweedFSPrincipalHeader and SeaweedFSSessionTokenHeader carry the // server-derived principal ARN and session token from the auth layer to // downstream authorization (IAM policy evaluation, S3Tables IAM checks). // These headers are trusted: callers of AuthSignatureOnly / // authenticateRequestInternal must scrub any client-supplied values at the // very start of authentication to prevent privilege escalation via header // injection. Prefer these constants over literals so that any future scrub // or consumer cannot drift by typo. SeaweedFSPrincipalHeader = "X-SeaweedFS-Principal" SeaweedFSSessionTokenHeader = "X-SeaweedFS-Session-Token" ) // Non-Standard S3 HTTP request constants const ( AmzIdentityId = "s3-identity-id" AmzAccountId = "s3-account-id" AmzAuthType = "s3-auth-type" ) func GetBucketAndObject(r *http.Request) (bucket, object string) { vars := mux.Vars(r) bucket = vars["bucket"] object = NormalizeObjectKey(vars["object"]) return } // IsValidObjectKey rejects S3 object keys that — after normalization // (backslash→slash, slash collapse) — contain a `.` or `..` path segment, or // embed a NUL byte. Such keys are collapsed by filepath.Join inside the filer // and would escape the bucket directory, so they must not reach the gRPC layer. // Gorilla mux URL-decodes captured vars before this runs, so `%2e%2e` is // already `..` here. func IsValidObjectKey(object string) bool { if object == "" { return true } if strings.ContainsRune(object, '\x00') { return false } object = strings.ReplaceAll(object, "\\", "/") for _, seg := range strings.Split(object, "/") { if seg == "." || seg == ".." { return false } } return true } // IsValidBucketName rejects bucket names captured from the URL path that are // unsafe to use in filer path construction (`.`, `..`, contain `/` or `\`, or // embed NUL). This is a path-safety check, not a full S3 naming-rule check. func IsValidBucketName(bucket string) bool { if bucket == "" { return true } if bucket == "." || bucket == ".." { return false } return !strings.ContainsAny(bucket, "/\\\x00") } // IsValidPathSegment reports whether value is safe to use as one filer path // component. It intentionally does not enforce any application-specific // format; callers can layer stricter checks on top. func IsValidPathSegment(value string) bool { if value == "" || value == "." || value == ".." { return false } return !strings.ContainsAny(value, "/\\\x00") } // NormalizeObjectKey normalizes object keys by removing duplicate slashes and converting backslashes. // This normalizes keys from various sources (URL path, form values, etc.) to a consistent format. // It also converts Windows-style backslashes to forward slashes for cross-platform compatibility. // Returns keys WITHOUT leading slash to match S3 API format (e.g., "foo/bar" not "/foo/bar"). // Preserves trailing slash if present (e.g., "foo/" stays "foo/"). func NormalizeObjectKey(object string) string { // Preserve trailing slash if present hasTrailingSlash := strings.HasSuffix(object, "/") // Convert Windows-style backslashes to forward slashes object = strings.ReplaceAll(object, "\\", "/") object = removeDuplicateSlashes(object) // Remove leading slash to match S3 API format object = strings.TrimPrefix(object, "/") // Restore trailing slash if it was present and result is not empty if hasTrailingSlash && object != "" && !strings.HasSuffix(object, "/") { object = object + "/" } return object } // removeDuplicateSlashes removes consecutive slashes from a path func removeDuplicateSlashes(s string) string { var result strings.Builder result.Grow(len(s)) lastWasSlash := false for _, r := range s { if r == '/' { if !lastWasSlash { result.WriteRune(r) } lastWasSlash = true } else { result.WriteRune(r) lastWasSlash = false } } return result.String() } func GetPrefix(r *http.Request) string { query := r.URL.Query() prefix := query.Get("prefix") prefix = removeDuplicateSlashes(prefix) return prefix } var PassThroughHeaders = map[string]string{ "response-cache-control": "Cache-Control", "response-content-disposition": "Content-Disposition", "response-content-encoding": "Content-Encoding", "response-content-language": "Content-Language", "response-content-type": "Content-Type", "response-expires": "Expires", } // IsSeaweedFSInternalHeader checks if a header key is a SeaweedFS internal header // that should be filtered from client responses. // Header names are case-insensitive in HTTP, so this function normalizes to lowercase. func IsSeaweedFSInternalHeader(headerKey string) bool { return strings.HasPrefix(strings.ToLower(headerKey), SeaweedFSInternalPrefix) } // Context keys for storing authenticated identity information type contextKey string const ( contextKeyIdentityName contextKey = "s3-identity-name" contextKeyIdentityObject contextKey = "s3-identity-object" contextKeyIdentityHolder contextKey = "s3-identity-holder" contextKeyPrincipalArn contextKey = "s3-principal-arn" ) // identityHolder is a mutable container for the authenticated identity name, // stored by pointer in the request context. Authentication runs as an inner // handler that records the identity with r.WithContext, which returns a request // copy; that copy's new context values are invisible to outer middleware (e.g. // the audit logger) holding an earlier copy of the request. A pointer to a // holder installed before authentication is shared across all copies, so the // name written by the inner handler is readable by the outer middleware. type identityHolder struct { name atomic.Pointer[string] principalArn atomic.Pointer[string] } // EnsureIdentityHolder attaches a mutable identity holder to the request context // if one is not already present, returning the (possibly new) request. Install // it before authentication so the authenticated requester remains recoverable // from the original request, e.g. when emitting an audit entry for a handler // that does not log the requester itself. func EnsureIdentityHolder(r *http.Request) *http.Request { if r == nil { return nil } if h, ok := r.Context().Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil { return r } return r.WithContext(context.WithValue(r.Context(), contextKeyIdentityHolder, &identityHolder{})) } // SetIdentityNameInContext stores the authenticated identity name in the request context // This is the secure way to propagate identity - headers can be spoofed, context cannot func SetIdentityNameInContext(ctx context.Context, identityName string) context.Context { if identityName == "" { return ctx } // Also record into the mutable holder (if installed) so the name survives the // request-context copy and stays visible to outer middleware such as the // audit logger. if h, ok := ctx.Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil { h.name.Store(&identityName) } return context.WithValue(ctx, contextKeyIdentityName, identityName) } // GetIdentityNameFromContext retrieves the authenticated identity name from the request context // Returns empty string if no identity is set (unauthenticated request) // This is the secure way to retrieve identity - never read from headers directly func GetIdentityNameFromContext(r *http.Request) string { // Prefer the per-request context value; fall back to the mutable holder for // callers (e.g. the audit middleware) that hold a copy of the request taken // before authentication set the identity. if name, ok := r.Context().Value(contextKeyIdentityName).(string); ok && name != "" { return name } if h, ok := r.Context().Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil { if name := h.name.Load(); name != nil { return *name } } return "" } // SetPrincipalArnInContext stores the authenticated principal ARN in the request // context. For an STS session the identity name is an opaque session subject, so // the ARN is the only place the assumed role and session name survive to the // audit log. func SetPrincipalArnInContext(ctx context.Context, principalArn string) context.Context { if principalArn == "" { return ctx } if h, ok := ctx.Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil { h.principalArn.Store(&principalArn) } return context.WithValue(ctx, contextKeyPrincipalArn, principalArn) } // GetPrincipalArnFromContext retrieves the authenticated principal ARN from the // request context, or "" when the request is unauthenticated. func GetPrincipalArnFromContext(r *http.Request) string { if arn, ok := r.Context().Value(contextKeyPrincipalArn).(string); ok && arn != "" { return arn } if h, ok := r.Context().Value(contextKeyIdentityHolder).(*identityHolder); ok && h != nil { if arn := h.principalArn.Load(); arn != nil { return *arn } } return "" } // SetIdentityInContext stores the full authenticated identity object in the request context // This is used to pass the full identity (including for JWT users) to handlers func SetIdentityInContext(ctx context.Context, identity interface{}) context.Context { if identity != nil { return context.WithValue(ctx, contextKeyIdentityObject, identity) } return ctx } // GetIdentityFromContext retrieves the full identity object from the request context // Returns nil if no identity is set (unauthenticated request) func GetIdentityFromContext(r *http.Request) interface{} { return r.Context().Value(contextKeyIdentityObject) }