package handler import ( "fmt" "html" "io" "log" "net/http" "path/filepath" "strings" "aws-sts-mock/internal/kvdb" "aws-sts-mock/internal/storage" ) // PublicViewHandler handles public viewing of S3 bucket contents type PublicViewHandler struct { storage *storage.UserAwareStorage db kvdb.KVDB } // NewPublicViewHandler creates a new public view handler func NewPublicViewHandler(storage *storage.UserAwareStorage, db kvdb.KVDB) *PublicViewHandler { return &PublicViewHandler{ storage: storage, db: db, } } // Handle processes public view requests // Supports both: // - Virtual-hosted-style: bucketname.s3.domain.com/{key} // - Path-style: s3.domain.com/{bucketName}/{key} func (h *PublicViewHandler) Handle(w http.ResponseWriter, r *http.Request) { var bucketName, key string // Determine if this is virtual-hosted-style or path-style if isVirtualHostedStyle(r.Host) { // Virtual-hosted-style: bucket from Host header, key from path bucketName = extractBucketFromHost(r.Host) key = strings.TrimPrefix(r.URL.Path, "/") log.Printf("Public view (virtual-hosted): bucket=%s, key=%s", bucketName, key) } else { // Path-style: bucket and key from path path := strings.TrimPrefix(r.URL.Path, "/") if path == "" { http.Error(w, "Invalid path format. Expected: /{bucketName}/{key}", http.StatusBadRequest) return } parts := strings.SplitN(path, "/", 2) bucketName = parts[0] if len(parts) > 1 { key = parts[1] } log.Printf("Public view (path-style): bucket=%s, key=%s", bucketName, key) } // Resolve bucket name to accountID and bucketID accountID, bucketID, err := h.db.ResolveBucketName(bucketName) if err != nil { log.Printf("Failed to resolve bucket name %s: %v", bucketName, err) http.Error(w, "Bucket not found", http.StatusNotFound) return } // Check if bucket has public viewing enabled config, err := h.db.GetBucketConfig(accountID, bucketID) if err != nil { log.Printf("Bucket config not found for %s:%s", accountID, bucketID) http.Error(w, "Bucket not found or not configured for public access", http.StatusNotFound) return } if !config.PublicViewing { http.Error(w, "Public viewing not enabled for this bucket", http.StatusForbidden) return } // If no key specified, show bucket listing if key == "" { h.handleBucketListing(w, r, accountID, bucketID, config) return } // Serve the file h.handleFileDownload(w, r, accountID, bucketID, bucketName, key) } func (h *PublicViewHandler) handleBucketListing(w http.ResponseWriter, r *http.Request, accountID, bucketID string, config *kvdb.BucketConfig) { // List all objects in the bucket objects, err := h.storage.ListObjectsByBucketIDForUser(accountID, bucketID) if err != nil { log.Printf("Error listing objects: %v", err) http.Error(w, "Error listing bucket contents", http.StatusInternalServerError) return } // Generate HTML page htmlContent := h.generateBucketListingHTML(config.BucketName, objects, r) w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(http.StatusOK) w.Write([]byte(htmlContent)) } func (h *PublicViewHandler) handleFileDownload(w http.ResponseWriter, r *http.Request, accountID, bucketID, bucketName, key string) { // Get the file file, info, err := h.storage.GetObjectByBucketIDForUser(accountID, bucketID, key) if err != nil { if err == storage.ErrObjectNotFound { http.Error(w, "File not found", http.StatusNotFound) return } log.Printf("Error getting object: %v", err) http.Error(w, "Error retrieving file", http.StatusInternalServerError) return } defer file.Close() // Set headers w.Header().Set("Content-Type", getContentType(key)) 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)) // If it's an image, set inline disposition, otherwise attachment ext := strings.ToLower(filepath.Ext(key)) if isImage(ext) || isText(ext) || ext == ".pdf" { w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=\"%s\"", filepath.Base(key))) } else { w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(key))) } w.WriteHeader(http.StatusOK) // Stream the file http.ServeContent(w, r, filepath.Base(key), info.LastModified, file.(io.ReadSeeker)) } func (h *PublicViewHandler) generateBucketListingHTML(bucketName string, objects []storage.ObjectInfo, r *http.Request) string { var sb strings.Builder // Determine URL prefix based on request style var urlPrefix string if isVirtualHostedStyle(r.Host) { // Virtual-hosted-style: just use the path urlPrefix = "" } else { // Path-style: include bucket name in URL urlPrefix = "/" + bucketName } sb.WriteString(`
Public Bucket Contents
This bucket is empty