1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045 |
- 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(` <Contents>
- <Key>%s</Key>
- <LastModified>%s</LastModified>
- <ETag>"%s"</ETag>
- <Size>%d</Size>
- <StorageClass>STANDARD</StorageClass>
- </Contents>`, 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(`<?xml version="1.0" encoding="UTF-8"?>
- <ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
- <Name>%s</Name>
- <Prefix></Prefix>
- <Marker></Marker>
- <MaxKeys>1000</MaxKeys>
- <IsTruncated>false</IsTruncated>
- %s
- </ListBucketResult>`, 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
- }
|