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 // Helper functions - add these at package level (before S3Handler struct) func isVirtualHostedStyle(host string) bool { // Virtual-hosted-style: bucketname.s3.domain.com // Path-style: s3.domain.com parts := strings.Split(host, ".") // If host starts with a bucket name (has more than 2 parts before domain) // and contains "s3", it's virtual-hosted-style return len(parts) > 2 && strings.Contains(host, "s3") } func extractBucketFromHost(host string) string { // Extract bucket name from virtual-hosted-style host // photosbucket.s3.alanyeung.co -> photosbucket // Remove port if present if idx := strings.Index(host, ":"); idx != -1 { host = host[:idx] } parts := strings.Split(host, ".") if len(parts) > 0 { return parts[0] } return "" } // Handle routes S3 requests to appropriate handlers func (h *S3Handler) Handle(w http.ResponseWriter, r *http.Request) { log.Printf("S3 Request: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) log.Printf("Host header: %s", r.Host) log.Printf("RequestURI: %s", r.RequestURI) var bucketName, objectKey string // Determine if this is virtual-hosted-style or path-style if isVirtualHostedStyle(r.Host) { // Virtual-hosted-style: bucket from Host header, object from path bucketName = extractBucketFromHost(r.Host) objectKey = strings.TrimPrefix(r.URL.Path, "/") log.Printf("Virtual-hosted-style request: bucket=%s, key=%s", bucketName, objectKey) } else { // Path-style: bucket and object from path 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] if len(parts) > 1 { objectKey = parts[1] } log.Printf("Path-style request: bucket=%s, key=%s", bucketName, objectKey) } // 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 }