package handler
import (
"encoding/xml"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"aws-sts-mock/internal/service"
"aws-sts-mock/internal/storage"
"aws-sts-mock/pkg/checksum"
"aws-sts-mock/pkg/s3"
"aws-sts-mock/pkg/sigv4"
)
// S3Handler handles S3 HTTP requests with user isolation
type S3Handler struct {
service *service.S3Service
}
// NewS3Handler creates a new S3 handler
func NewS3Handler(service *service.S3Service) *S3Handler {
return &S3Handler{
service: service,
}
}
// Handle routes S3 requests to appropriate handlers
func (h *S3Handler) Handle(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/")
parts := strings.SplitN(path, "/", 2)
// Root path - list buckets
if path == "" {
if r.Method == "GET" {
h.handleListBuckets(w, r)
} else {
h.writeError(w, "MethodNotAllowed", "Method not allowed", "", http.StatusMethodNotAllowed)
}
return
}
bucketName := parts[0]
objectKey := ""
if len(parts) > 1 {
objectKey = parts[1]
}
// Bucket operations (no object key)
if objectKey == "" {
h.handleBucketOperation(w, r, bucketName)
return
}
// Object operations
h.handleObjectOperation(w, r, bucketName, objectKey)
}
// getUserFromContext extracts user credentials from request context
func (h *S3Handler) getUserFromContext(r *http.Request) (sigv4.AWSCredentials, error) {
creds, ok := r.Context().Value(sigv4.CredentialsContextKey).(sigv4.AWSCredentials)
if !ok {
return sigv4.AWSCredentials{}, fmt.Errorf("failed to retrieve credentials")
}
return creds, nil
}
func (h *S3Handler) handleBucketOperation(w http.ResponseWriter, r *http.Request, bucketName string) {
query := r.URL.Query()
// Check for list multipart uploads
if query.Has("uploads") && r.Method == "GET" {
h.handleListMultipartUploads(w, r, bucketName)
return
}
// Check for delete query parameter (for DeleteObjects)
if r.Method == "POST" && query.Get("delete") != "" {
h.handleDeleteObjects(w, r, bucketName)
return
}
switch r.Method {
case "PUT":
h.handleCreateBucket(w, r, bucketName)
case "GET":
h.handleListObjects(w, r, bucketName)
case "DELETE":
h.handleDeleteBucket(w, r, bucketName)
case "HEAD":
h.handleHeadBucket(w, r, bucketName)
default:
h.writeError(w, "MethodNotAllowed", "Method not allowed", bucketName, http.StatusMethodNotAllowed)
}
}
func (h *S3Handler) handleObjectOperation(w http.ResponseWriter, r *http.Request, bucketName, objectKey string) {
query := r.URL.Query()
// Check for multipart operations
if query.Has("uploads") && r.Method == "POST" {
h.handleCreateMultipartUpload(w, r, bucketName, objectKey)
return
}
uploadID := query.Get("uploadId")
if uploadID != "" {
if r.Method == "PUT" && query.Has("partNumber") {
h.handleUploadPart(w, r, bucketName, objectKey)
return
}
if r.Method == "POST" {
h.handleCompleteMultipartUpload(w, r, bucketName, objectKey)
return
}
if r.Method == "DELETE" {
h.handleAbortMultipartUpload(w, r, bucketName, objectKey)
return
}
if r.Method == "GET" {
h.handleListParts(w, r, bucketName, objectKey)
return
}
}
// Regular object operations
switch r.Method {
case "PUT":
h.handlePutObject(w, r, bucketName, objectKey)
case "GET":
h.handleGetObject(w, r, bucketName, objectKey)
case "DELETE":
h.handleDeleteObject(w, r, bucketName, objectKey)
case "HEAD":
h.handleHeadObject(w, r, bucketName, objectKey)
default:
h.writeError(w, "MethodNotAllowed", "Method not allowed", objectKey, http.StatusMethodNotAllowed)
}
}
func (h *S3Handler) handleListBuckets(w http.ResponseWriter, r *http.Request) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
result, err := h.service.ListBuckets(creds.AccountID, creds.AccessKeyID)
if err != nil {
log.Printf("Error listing buckets for user %s: %v", creds.AccountID, err)
h.writeError(w, "InternalError", "Failed to list buckets", "", http.StatusInternalServerError)
return
}
result.XMLName = xml.Name{Space: "http://s3.amazonaws.com/doc/2006-03-01/", Local: "ListAllMyBucketsResult"}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusOK)
encoder := xml.NewEncoder(w)
encoder.Indent("", " ")
if err := encoder.Encode(result); err != nil {
log.Printf("Error encoding response: %v", err)
}
}
func (h *S3Handler) handleCreateBucket(w http.ResponseWriter, r *http.Request, bucketName string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
err = h.service.CreateBucket(creds.AccountID, bucketName)
if err != nil {
if err == storage.ErrBucketAlreadyExists {
h.writeError(w, "BucketAlreadyExists", "The requested bucket name is not available", bucketName, http.StatusConflict)
return
}
log.Printf("Failed to create bucket %s for user %s: %v", bucketName, creds.AccountID, err)
h.writeError(w, "InternalError", "Failed to create bucket", bucketName, http.StatusInternalServerError)
return
}
log.Printf("Created bucket: %s for user: %s", bucketName, creds.AccountID)
w.Header().Set("Location", "/"+bucketName)
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusOK)
}
func (h *S3Handler) handleListObjects(w http.ResponseWriter, r *http.Request, bucketName string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
log.Printf("ListObjects request for bucket: %s, user: %s", bucketName, creds.AccountID)
objects, err := h.service.ListObjects(creds.AccountID, bucketName)
if err != nil {
if err == storage.ErrBucketNotFound {
h.writeError(w, "NoSuchBucket", "The specified bucket does not exist", bucketName, http.StatusNotFound)
return
}
log.Printf("Failed to list objects in bucket %s for user %s: %v", bucketName, creds.AccountID, err)
h.writeError(w, "InternalError", "Failed to list objects", bucketName, http.StatusInternalServerError)
return
}
// Build object list XML
var objectsXML []string
for _, obj := range objects {
objectsXML = append(objectsXML, fmt.Sprintf(`
%s
%s
"%s"
%d
STANDARD
`, obj.Key, obj.LastModified.UTC().Format(time.RFC3339), obj.ETag, obj.Size))
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusOK)
response := fmt.Sprintf(`
%s
1000
false
%s
`, bucketName, strings.Join(objectsXML, "\n"))
w.Write([]byte(response))
}
func (h *S3Handler) handleDeleteBucket(w http.ResponseWriter, r *http.Request, bucketName string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
// Check if bucket exists
exists, err := h.service.BucketExists(creds.AccountID, bucketName)
if err != nil {
log.Printf("Error checking bucket existence %s for user %s: %v", bucketName, creds.AccountID, err)
h.writeError(w, "InternalError", "Failed to check bucket", bucketName, http.StatusInternalServerError)
return
}
if !exists {
h.writeError(w, "NoSuchBucket", "The specified bucket does not exist", bucketName, http.StatusNotFound)
return
}
// Check if bucket is empty
isEmpty, err := h.service.BucketIsEmpty(creds.AccountID, bucketName)
if err != nil {
log.Printf("Error checking if bucket %s is empty for user %s: %v", bucketName, creds.AccountID, err)
h.writeError(w, "InternalError", "Failed to check bucket contents", bucketName, http.StatusInternalServerError)
return
}
if !isEmpty {
h.writeError(w, "BucketNotEmpty", "The bucket you tried to delete is not empty", bucketName, http.StatusConflict)
return
}
// Delete the bucket
if err := h.service.DeleteBucket(creds.AccountID, bucketName); err != nil {
log.Printf("Failed to delete bucket %s for user %s: %v", bucketName, creds.AccountID, err)
if err == storage.ErrBucketNotFound {
h.writeError(w, "NoSuchBucket", "The specified bucket does not exist", bucketName, http.StatusNotFound)
return
}
h.writeError(w, "InternalError", "Failed to delete bucket", bucketName, http.StatusInternalServerError)
return
}
log.Printf("Deleted bucket: %s for user: %s", bucketName, creds.AccountID)
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusNoContent)
}
func (h *S3Handler) handleHeadBucket(w http.ResponseWriter, r *http.Request, bucketName string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
exists, err := h.service.BucketExists(creds.AccountID, bucketName)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
if !exists {
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusOK)
}
func (h *S3Handler) handlePutObject(w http.ResponseWriter, r *http.Request, bucketName, objectKey string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
// Extract checksum headers
checksumHeaders := extractChecksumHeaders(r)
validator := checksum.NewValidator(checksumHeaders)
// Validate and store the object
result, data, err := validator.ValidateStream(r.Body)
if err != nil {
log.Printf("Failed to read object data %s/%s for user %s: %v", bucketName, objectKey, creds.AccountID, err)
h.writeError(w, "InternalError", "Failed to read object data", objectKey, http.StatusInternalServerError)
return
}
// Check if validation failed
if !result.Valid {
log.Printf("Checksum validation failed for %s/%s: %s", bucketName, objectKey, result.ErrorMessage)
h.writeError(w, "InvalidDigest", result.ErrorMessage, objectKey, http.StatusBadRequest)
return
}
// Store the object
reader := strings.NewReader(string(data))
err = h.service.PutObject(creds.AccountID, bucketName, objectKey, reader)
if err != nil {
if err == storage.ErrBucketNotFound {
h.writeError(w, "NoSuchBucket", "The specified bucket does not exist", bucketName, http.StatusNotFound)
return
}
log.Printf("Failed to put object %s/%s for user %s: %v", bucketName, objectKey, creds.AccountID, err)
h.writeError(w, "InternalError", "Failed to create object", objectKey, http.StatusInternalServerError)
return
}
log.Printf("Created object: %s/%s for user: %s (validated with %s)",
bucketName, objectKey, creds.AccountID, result.ValidatedWith)
// Set response headers including checksums
responseHeaders := result.GetResponseHeaders()
for key, value := range responseHeaders {
w.Header().Set(key, value)
}
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusOK)
}
func (h *S3Handler) handleGetObject(w http.ResponseWriter, r *http.Request, bucketName, objectKey string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
file, info, err := h.service.GetObject(creds.AccountID, bucketName, objectKey)
if err != nil {
if err == storage.ErrObjectNotFound {
h.writeError(w, "NoSuchKey", "The specified key does not exist", objectKey, http.StatusNotFound)
return
}
log.Printf("Failed to get object %s/%s for user %s: %v", bucketName, objectKey, creds.AccountID, err)
h.writeError(w, "InternalError", "Failed to retrieve object", objectKey, http.StatusInternalServerError)
return
}
defer file.Close()
// Read file data to compute checksums
data, err := io.ReadAll(file)
if err != nil {
log.Printf("Failed to read object data %s/%s: %v", bucketName, objectKey, err)
h.writeError(w, "InternalError", "Failed to read object data", objectKey, http.StatusInternalServerError)
return
}
// Compute checksums for the response
validator := checksum.NewValidator(map[string]string{})
result := validator.ComputeChecksums(data)
log.Printf("Retrieved object: %s/%s for user: %s", bucketName, objectKey, creds.AccountID)
// Set standard headers
w.Header().Set("Content-Type", getContentType(objectKey))
w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size))
w.Header().Set("Last-Modified", info.LastModified.UTC().Format(http.TimeFormat))
w.Header().Set("x-amz-request-id", generateRequestId())
// Set checksum headers
w.Header().Set("ETag", fmt.Sprintf(`"%s"`, result.MD5))
w.Header().Set("x-amz-checksum-crc32", result.CRC32)
w.Header().Set("x-amz-checksum-crc32c", result.CRC32C)
w.Header().Set("x-amz-checksum-sha1", result.SHA1)
w.Header().Set("x-amz-checksum-sha256", result.SHA256)
w.WriteHeader(http.StatusOK)
w.Write(data)
}
func (h *S3Handler) handleDeleteObject(w http.ResponseWriter, r *http.Request, bucketName, objectKey string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
err = h.service.DeleteObject(creds.AccountID, bucketName, objectKey)
if err != nil {
log.Printf("Failed to delete object %s/%s for user %s: %v", bucketName, objectKey, creds.AccountID, err)
}
log.Printf("Deleted object: %s/%s for user: %s", bucketName, objectKey, creds.AccountID)
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusNoContent)
}
func (h *S3Handler) handleHeadObject(w http.ResponseWriter, r *http.Request, bucketName, objectKey string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
info, err := h.service.GetObjectInfo(creds.AccountID, bucketName, objectKey)
if err != nil {
if err == storage.ErrObjectNotFound {
w.WriteHeader(http.StatusNotFound)
return
}
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", getContentType(objectKey))
w.Header().Set("Content-Length", fmt.Sprintf("%d", info.Size))
w.Header().Set("Last-Modified", info.LastModified.UTC().Format(http.TimeFormat))
w.Header().Set("ETag", fmt.Sprintf("\"%s\"", info.ETag))
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusOK)
}
func (h *S3Handler) writeError(w http.ResponseWriter, code, message, resource string, statusCode int) {
errorResp := s3.S3Error{
XMLName: xml.Name{Space: "", Local: "Error"},
Code: code,
Message: message,
Resource: resource,
RequestId: generateRequestId(),
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("x-amz-request-id", errorResp.RequestId)
w.WriteHeader(statusCode)
encoder := xml.NewEncoder(w)
encoder.Indent("", " ")
if err := encoder.Encode(errorResp); err != nil {
log.Printf("Error encoding S3 error response: %v", err)
}
}
// 123
// Update handleDeleteBucket in handler/s3_handler.go
// Update handleDeleteObjects to use the batch service method
func (h *S3Handler) handleDeleteObjects(w http.ResponseWriter, r *http.Request, bucketName string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
// Check if bucket exists
exists, err := h.service.BucketExists(creds.AccountID, bucketName)
if err != nil {
log.Printf("Error checking bucket existence %s for user %s: %v", bucketName, creds.AccountID, err)
h.writeError(w, "InternalError", "Failed to check bucket", bucketName, http.StatusInternalServerError)
return
}
if !exists {
h.writeError(w, "NoSuchBucket", "The specified bucket does not exist", bucketName, http.StatusNotFound)
return
}
// Parse the delete request
var deleteReq s3.DeleteObjectsRequest
if err := xml.NewDecoder(r.Body).Decode(&deleteReq); err != nil {
log.Printf("Error decoding delete objects request: %v", err)
h.writeError(w, "MalformedXML", "The XML you provided was not well-formed", "", http.StatusBadRequest)
return
}
// Validate request
if len(deleteReq.Objects) == 0 {
h.writeError(w, "MalformedXML", "No objects specified for deletion", "", http.StatusBadRequest)
return
}
if len(deleteReq.Objects) > 1000 {
h.writeError(w, "InvalidRequest", "You cannot delete more than 1000 objects in a single request", "", http.StatusBadRequest)
return
}
// Extract keys
keys := make([]string, len(deleteReq.Objects))
for i, obj := range deleteReq.Objects {
keys[i] = obj.Key
}
// Process deletions using service
deletionErrors := h.service.DeleteObjects(creds.AccountID, bucketName, keys)
// Build response
response := s3.DeleteObjectsResponse{
XMLName: xml.Name{Space: "http://s3.amazonaws.com/doc/2006-03-01/", Local: "DeleteResult"},
Deleted: []s3.DeletedObject{},
Errors: []s3.DeleteError{},
}
for _, obj := range deleteReq.Objects {
if err, hasError := deletionErrors[obj.Key]; hasError {
log.Printf("Failed to delete object %s/%s for user %s: %v", bucketName, obj.Key, creds.AccountID, err)
// Determine error code based on error type
errorCode := "InternalError"
errorMessage := "We encountered an internal error. Please try again."
if err == storage.ErrObjectNotFound {
// S3 doesn't return an error for missing objects in batch delete
// Just add to deleted list
if !deleteReq.Quiet {
response.Deleted = append(response.Deleted, s3.DeletedObject{
Key: obj.Key,
})
}
continue
}
response.Errors = append(response.Errors, s3.DeleteError{
Key: obj.Key,
VersionId: obj.VersionId,
Code: errorCode,
Message: errorMessage,
})
} else {
// Add to deleted list if not in quiet mode
if !deleteReq.Quiet {
response.Deleted = append(response.Deleted, s3.DeletedObject{
Key: obj.Key,
VersionId: obj.VersionId,
})
}
}
}
log.Printf("Batch deleted %d objects (attempted: %d) from bucket %s for user: %s",
len(deleteReq.Objects)-len(response.Errors), len(deleteReq.Objects), bucketName, creds.AccountID)
// Write response
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusOK)
encoder := xml.NewEncoder(w)
encoder.Indent("", " ")
if err := encoder.Encode(response); err != nil {
log.Printf("Error encoding delete objects response: %v", err)
}
}
// Add these methods to S3Handler
func (h *S3Handler) handleCreateMultipartUpload(w http.ResponseWriter, r *http.Request, bucketName, objectKey string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
uploadID, err := h.service.CreateMultipartUpload(creds.AccountID, bucketName, objectKey)
if err != nil {
if err == storage.ErrBucketNotFound {
h.writeError(w, "NoSuchBucket", "The specified bucket does not exist", bucketName, http.StatusNotFound)
return
}
log.Printf("Failed to create multipart upload for %s/%s: %v", bucketName, objectKey, err)
h.writeError(w, "InternalError", "Failed to initiate multipart upload", objectKey, http.StatusInternalServerError)
return
}
log.Printf("Created multipart upload: %s for %s/%s, user: %s", uploadID, bucketName, objectKey, creds.AccountID)
result := s3.InitiateMultipartUploadResult{
XMLName: xml.Name{Space: "http://s3.amazonaws.com/doc/2006-03-01/", Local: "InitiateMultipartUploadResult"},
Bucket: bucketName,
Key: objectKey,
UploadId: uploadID,
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusOK)
encoder := xml.NewEncoder(w)
encoder.Indent("", " ")
if err := encoder.Encode(result); err != nil {
log.Printf("Error encoding response: %v", err)
}
}
func (h *S3Handler) handleUploadPart(w http.ResponseWriter, r *http.Request, bucketName, objectKey string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
// Get query parameters
uploadID := r.URL.Query().Get("uploadId")
partNumberStr := r.URL.Query().Get("partNumber")
if uploadID == "" || partNumberStr == "" {
h.writeError(w, "InvalidRequest", "Missing uploadId or partNumber", "", http.StatusBadRequest)
return
}
partNumber := 0
fmt.Sscanf(partNumberStr, "%d", &partNumber)
// Verify upload belongs to user
upload, err := h.service.GetMultipartUpload(uploadID)
if err != nil {
h.writeError(w, "NoSuchUpload", "The specified upload does not exist", uploadID, http.StatusNotFound)
return
}
if upload.AccountID != creds.AccountID || upload.Bucket != bucketName || upload.Key != objectKey {
h.writeError(w, "AccessDenied", "Access denied to this upload", uploadID, http.StatusForbidden)
return
}
// Extract checksum headers and validate
checksumHeaders := extractChecksumHeaders(r)
validator := checksum.NewValidator(checksumHeaders)
// Validate the part data
result, data, err := validator.ValidateStream(r.Body)
if err != nil {
log.Printf("Failed to read part data for upload %s: %v", uploadID, err)
h.writeError(w, "InternalError", "Failed to read part data", "", http.StatusInternalServerError)
return
}
// Check if validation failed
if !result.Valid {
log.Printf("Checksum validation failed for part %d of upload %s: %s",
partNumber, uploadID, result.ErrorMessage)
h.writeError(w, "InvalidDigest", result.ErrorMessage, "", http.StatusBadRequest)
return
}
// Upload the part
reader := strings.NewReader(string(data))
etag, err := h.service.UploadPart(uploadID, partNumber, reader)
if err != nil {
log.Printf("Failed to upload part %d for upload %s: %v", partNumber, uploadID, err)
h.writeError(w, "InternalError", "Failed to upload part", "", http.StatusInternalServerError)
return
}
log.Printf("Uploaded part %d for upload %s, user: %s (validated with %s)",
partNumber, uploadID, creds.AccountID, result.ValidatedWith)
// Set response headers including checksums
w.Header().Set("ETag", etag)
responseHeaders := result.GetResponseHeaders()
for key, value := range responseHeaders {
if key != "ETag" { // Don't override the ETag
w.Header().Set(key, value)
}
}
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusOK)
}
func (h *S3Handler) handleCompleteMultipartUpload(w http.ResponseWriter, r *http.Request, bucketName, objectKey string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
uploadID := r.URL.Query().Get("uploadId")
if uploadID == "" {
h.writeError(w, "InvalidRequest", "Missing uploadId", "", http.StatusBadRequest)
return
}
// Verify upload belongs to user
upload, err := h.service.GetMultipartUpload(uploadID)
if err != nil {
h.writeError(w, "NoSuchUpload", "The specified upload does not exist", uploadID, http.StatusNotFound)
return
}
if upload.AccountID != creds.AccountID || upload.Bucket != bucketName || upload.Key != objectKey {
h.writeError(w, "AccessDenied", "Access denied to this upload", uploadID, http.StatusForbidden)
return
}
// Parse request
var req s3.CompleteMultipartUploadRequest
if err := xml.NewDecoder(r.Body).Decode(&req); err != nil {
log.Printf("Error decoding complete multipart upload request: %v", err)
h.writeError(w, "MalformedXML", "The XML you provided was not well-formed", "", http.StatusBadRequest)
return
}
// Extract part numbers
parts := make([]int, len(req.Parts))
for i, part := range req.Parts {
parts[i] = part.PartNumber
}
// Complete the upload
if err := h.service.CompleteMultipartUpload(uploadID, parts); err != nil {
log.Printf("Failed to complete multipart upload %s: %v", uploadID, err)
h.writeError(w, "InternalError", "Failed to complete multipart upload", "", http.StatusInternalServerError)
return
}
log.Printf("Completed multipart upload: %s for %s/%s, user: %s", uploadID, bucketName, objectKey, creds.AccountID)
result := s3.CompleteMultipartUploadResult{
XMLName: xml.Name{Space: "http://s3.amazonaws.com/doc/2006-03-01/", Local: "CompleteMultipartUploadResult"},
Location: fmt.Sprintf("/%s/%s", bucketName, objectKey),
Bucket: bucketName,
Key: objectKey,
ETag: fmt.Sprintf("\"%d\"", time.Now().Unix()),
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusOK)
encoder := xml.NewEncoder(w)
encoder.Indent("", " ")
if err := encoder.Encode(result); err != nil {
log.Printf("Error encoding response: %v", err)
}
}
func (h *S3Handler) handleAbortMultipartUpload(w http.ResponseWriter, r *http.Request, bucketName, objectKey string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
uploadID := r.URL.Query().Get("uploadId")
if uploadID == "" {
h.writeError(w, "InvalidRequest", "Missing uploadId", "", http.StatusBadRequest)
return
}
// Verify upload belongs to user
upload, err := h.service.GetMultipartUpload(uploadID)
if err != nil {
h.writeError(w, "NoSuchUpload", "The specified upload does not exist", uploadID, http.StatusNotFound)
return
}
if upload.AccountID != creds.AccountID || upload.Bucket != bucketName || upload.Key != objectKey {
h.writeError(w, "AccessDenied", "Access denied to this upload", uploadID, http.StatusForbidden)
return
}
// Abort the upload
if err := h.service.AbortMultipartUpload(uploadID); err != nil {
log.Printf("Failed to abort multipart upload %s: %v", uploadID, err)
h.writeError(w, "InternalError", "Failed to abort multipart upload", "", http.StatusInternalServerError)
return
}
log.Printf("Aborted multipart upload: %s for %s/%s, user: %s", uploadID, bucketName, objectKey, creds.AccountID)
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusNoContent)
}
func (h *S3Handler) handleListParts(w http.ResponseWriter, r *http.Request, bucketName, objectKey string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
uploadID := r.URL.Query().Get("uploadId")
if uploadID == "" {
h.writeError(w, "InvalidRequest", "Missing uploadId", "", http.StatusBadRequest)
return
}
// Verify upload belongs to user
upload, err := h.service.GetMultipartUpload(uploadID)
if err != nil {
h.writeError(w, "NoSuchUpload", "The specified upload does not exist", uploadID, http.StatusNotFound)
return
}
if upload.AccountID != creds.AccountID || upload.Bucket != bucketName || upload.Key != objectKey {
h.writeError(w, "AccessDenied", "Access denied to this upload", uploadID, http.StatusForbidden)
return
}
// Parse query parameters
maxParts := 1000
partNumberMarker := 0
if maxPartsStr := r.URL.Query().Get("max-parts"); maxPartsStr != "" {
fmt.Sscanf(maxPartsStr, "%d", &maxParts)
}
if markerStr := r.URL.Query().Get("part-number-marker"); markerStr != "" {
fmt.Sscanf(markerStr, "%d", &partNumberMarker)
}
// List parts
parts, nextMarker, isTruncated, err := h.service.ListParts(uploadID, maxParts, partNumberMarker)
if err != nil {
log.Printf("Failed to list parts for upload %s: %v", uploadID, err)
h.writeError(w, "InternalError", "Failed to list parts", "", http.StatusInternalServerError)
return
}
// Build response
s3Parts := make([]s3.Part, len(parts))
for i, part := range parts {
s3Parts[i] = s3.Part{
PartNumber: part.PartNumber,
LastModified: part.LastModified.UTC().Format(time.RFC3339),
ETag: part.ETag,
Size: part.Size,
}
}
result := s3.ListPartsResult{
XMLName: xml.Name{Space: "http://s3.amazonaws.com/doc/2006-03-01/", Local: "ListPartsResult"},
Bucket: bucketName,
Key: objectKey,
UploadId: uploadID,
Initiator: s3.Initiator{ID: creds.AccountID, DisplayName: creds.AccessKeyID},
Owner: s3.Owner{ID: creds.AccountID, DisplayName: creds.AccessKeyID},
StorageClass: "STANDARD",
PartNumberMarker: partNumberMarker,
NextPartNumberMarker: nextMarker,
MaxParts: maxParts,
IsTruncated: isTruncated,
Parts: s3Parts,
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusOK)
encoder := xml.NewEncoder(w)
encoder.Indent("", " ")
if err := encoder.Encode(result); err != nil {
log.Printf("Error encoding response: %v", err)
}
}
func (h *S3Handler) handleListMultipartUploads(w http.ResponseWriter, r *http.Request, bucketName string) {
creds, err := h.getUserFromContext(r)
if err != nil {
h.writeError(w, "AccessDenied", "Failed to retrieve credentials", "", http.StatusForbidden)
return
}
// Parse query parameters
maxUploads := 1000
keyMarker := r.URL.Query().Get("key-marker")
uploadIDMarker := r.URL.Query().Get("upload-id-marker")
prefix := r.URL.Query().Get("prefix")
if maxUploadsStr := r.URL.Query().Get("max-uploads"); maxUploadsStr != "" {
fmt.Sscanf(maxUploadsStr, "%d", &maxUploads)
}
// List uploads
uploads, nextKeyMarker, nextUploadIDMarker, isTruncated, err := h.service.ListMultipartUploads(
creds.AccountID, bucketName, maxUploads, keyMarker, uploadIDMarker,
)
if err != nil {
if err == storage.ErrBucketNotFound {
h.writeError(w, "NoSuchBucket", "The specified bucket does not exist", bucketName, http.StatusNotFound)
return
}
log.Printf("Failed to list multipart uploads for bucket %s: %v", bucketName, err)
h.writeError(w, "InternalError", "Failed to list uploads", "", http.StatusInternalServerError)
return
}
// Filter by prefix if provided
if prefix != "" {
filtered := []storage.MultipartUploadInfo{}
for _, upload := range uploads {
if len(upload.Key) >= len(prefix) && upload.Key[:len(prefix)] == prefix {
filtered = append(filtered, upload)
}
}
uploads = filtered
}
// Build response
s3Uploads := make([]s3.MultipartUpload, len(uploads))
for i, upload := range uploads {
s3Uploads[i] = s3.MultipartUpload{
Key: upload.Key,
UploadId: upload.UploadID,
Initiator: s3.Initiator{ID: creds.AccountID, DisplayName: creds.AccessKeyID},
Owner: s3.Owner{ID: creds.AccountID, DisplayName: creds.AccessKeyID},
StorageClass: "STANDARD",
Initiated: upload.Initiated.UTC().Format(time.RFC3339),
}
}
result := s3.ListMultipartUploadsResult{
XMLName: xml.Name{Space: "http://s3.amazonaws.com/doc/2006-03-01/", Local: "ListMultipartUploadsResult"},
Bucket: bucketName,
KeyMarker: keyMarker,
UploadIdMarker: uploadIDMarker,
NextKeyMarker: nextKeyMarker,
NextUploadIdMarker: nextUploadIDMarker,
MaxUploads: maxUploads,
IsTruncated: isTruncated,
Uploads: s3Uploads,
Prefix: prefix,
}
w.Header().Set("Content-Type", "application/xml")
w.Header().Set("x-amz-request-id", generateRequestId())
w.WriteHeader(http.StatusOK)
encoder := xml.NewEncoder(w)
encoder.Indent("", " ")
if err := encoder.Encode(result); err != nil {
log.Printf("Error encoding response: %v", err)
}
}
func extractChecksumHeaders(r *http.Request) map[string]string {
headers := make(map[string]string)
// Extract all checksum-related headers
if val := r.Header.Get("Content-MD5"); val != "" {
headers["Content-MD5"] = val
}
if val := r.Header.Get("x-amz-sdk-checksum-algorithm"); val != "" {
headers["x-amz-sdk-checksum-algorithm"] = val
}
if val := r.Header.Get("x-amz-checksum-crc32"); val != "" {
headers["x-amz-checksum-crc32"] = val
}
if val := r.Header.Get("x-amz-checksum-crc32c"); val != "" {
headers["x-amz-checksum-crc32c"] = val
}
if val := r.Header.Get("x-amz-checksum-crc64nvme"); val != "" {
headers["x-amz-checksum-crc64nvme"] = val
}
if val := r.Header.Get("x-amz-checksum-sha1"); val != "" {
headers["x-amz-checksum-sha1"] = val
}
if val := r.Header.Get("x-amz-checksum-sha256"); val != "" {
headers["x-amz-checksum-sha256"] = val
}
return headers
}