fix listing objects (#7008)
* fix listing objects * add more list testing * address comments * fix next marker * fix isTruncated in listing * fix tests * address tests * Update s3api_object_handlers_multipart.go * fixes * store json into bucket content, for tagging and cors * switch bucket metadata from json to proto * fix * Update s3api_bucket_config.go * fix test issue * fix test_bucket_listv2_delimiter_prefix * Update cors.go * skip special characters * passing listing * fix test_bucket_list_delimiter_prefix * ok. fix the xsd generated go code now * fix cors tests * fix test * fix test_bucket_list_unordered and test_bucket_listv2_unordered do not accept the allow-unordered and delimiter parameter combination * fix test_bucket_list_objects_anonymous and test_bucket_listv2_objects_anonymous The tests test_bucket_list_objects_anonymous and test_bucket_listv2_objects_anonymous were failing because they try to set bucket ACL to public-read, but SeaweedFS only supported private ACL. Updated PutBucketAclHandler to use the existing ExtractAcl function which already supports all standard S3 canned ACLs Replaced the hardcoded check for only private ACL with proper ACL parsing that handles public-read, public-read-write, authenticated-read, bucket-owner-read, bucket-owner-full-control, etc. Added unit tests to verify all standard canned ACLs are accepted * fix list unordered The test is expecting the error code to be InvalidArgument instead of InvalidRequest * allow anonymous listing( and head, get) * fix test_bucket_list_maxkeys_invalid Invalid values: max-keys=blah → Returns ErrInvalidMaxKeys (HTTP 400) * updating IsPublicRead when parsing acl * more logs * CORS Test Fix * fix test_bucket_list_return_data * default to private * fix test_bucket_list_delimiter_not_skip_special * default no acl * add debug logging * more logs * use basic http client remove logs also * fixes * debug * Update stats.go * debugging * fix anonymous test expectation anonymous user can read, as configured in s3 json.
This commit is contained in:
@@ -1,36 +1,24 @@
|
||||
package cors
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/glog"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
)
|
||||
|
||||
// S3 metadata file name constant to avoid typos and reduce duplication
|
||||
const S3MetadataFileName = ".s3metadata"
|
||||
|
||||
// CORSRule represents a single CORS rule
|
||||
type CORSRule struct {
|
||||
ID string `xml:"ID,omitempty" json:"ID,omitempty"`
|
||||
AllowedHeaders []string `xml:"AllowedHeader,omitempty" json:"AllowedHeaders,omitempty"`
|
||||
AllowedMethods []string `xml:"AllowedMethod" json:"AllowedMethods"`
|
||||
AllowedOrigins []string `xml:"AllowedOrigin" json:"AllowedOrigins"`
|
||||
AllowedHeaders []string `xml:"AllowedHeader,omitempty" json:"AllowedHeaders,omitempty"`
|
||||
ExposeHeaders []string `xml:"ExposeHeader,omitempty" json:"ExposeHeaders,omitempty"`
|
||||
MaxAgeSeconds *int `xml:"MaxAgeSeconds,omitempty" json:"MaxAgeSeconds,omitempty"`
|
||||
ID string `xml:"ID,omitempty" json:"ID,omitempty"`
|
||||
}
|
||||
|
||||
// CORSConfiguration represents the CORS configuration for a bucket
|
||||
type CORSConfiguration struct {
|
||||
XMLName xml.Name `xml:"CORSConfiguration"`
|
||||
CORSRules []CORSRule `xml:"CORSRule" json:"CORSRules"`
|
||||
}
|
||||
|
||||
@@ -44,7 +32,7 @@ type CORSRequest struct {
|
||||
AccessControlRequestHeaders []string
|
||||
}
|
||||
|
||||
// CORSResponse represents CORS response headers
|
||||
// CORSResponse represents the response for a CORS request
|
||||
type CORSResponse struct {
|
||||
AllowOrigin string
|
||||
AllowMethods string
|
||||
@@ -77,6 +65,29 @@ func ValidateConfiguration(config *CORSConfiguration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseRequest parses an HTTP request to extract CORS information
|
||||
func ParseRequest(r *http.Request) *CORSRequest {
|
||||
corsReq := &CORSRequest{
|
||||
Origin: r.Header.Get("Origin"),
|
||||
Method: r.Method,
|
||||
}
|
||||
|
||||
// Check if this is a preflight request
|
||||
if r.Method == "OPTIONS" {
|
||||
corsReq.IsPreflightRequest = true
|
||||
corsReq.AccessControlRequestMethod = r.Header.Get("Access-Control-Request-Method")
|
||||
|
||||
if headers := r.Header.Get("Access-Control-Request-Headers"); headers != "" {
|
||||
corsReq.AccessControlRequestHeaders = strings.Split(headers, ",")
|
||||
for i := range corsReq.AccessControlRequestHeaders {
|
||||
corsReq.AccessControlRequestHeaders[i] = strings.TrimSpace(corsReq.AccessControlRequestHeaders[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return corsReq
|
||||
}
|
||||
|
||||
// validateRule validates a single CORS rule
|
||||
func validateRule(rule *CORSRule) error {
|
||||
if len(rule.AllowedMethods) == 0 {
|
||||
@@ -148,29 +159,6 @@ func validateOrigin(origin string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseRequest parses an HTTP request to extract CORS information
|
||||
func ParseRequest(r *http.Request) *CORSRequest {
|
||||
corsReq := &CORSRequest{
|
||||
Origin: r.Header.Get("Origin"),
|
||||
Method: r.Method,
|
||||
}
|
||||
|
||||
// Check if this is a preflight request
|
||||
if r.Method == "OPTIONS" {
|
||||
corsReq.IsPreflightRequest = true
|
||||
corsReq.AccessControlRequestMethod = r.Header.Get("Access-Control-Request-Method")
|
||||
|
||||
if headers := r.Header.Get("Access-Control-Request-Headers"); headers != "" {
|
||||
corsReq.AccessControlRequestHeaders = strings.Split(headers, ",")
|
||||
for i := range corsReq.AccessControlRequestHeaders {
|
||||
corsReq.AccessControlRequestHeaders[i] = strings.TrimSpace(corsReq.AccessControlRequestHeaders[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return corsReq
|
||||
}
|
||||
|
||||
// EvaluateRequest evaluates a CORS request against a CORS configuration
|
||||
func EvaluateRequest(config *CORSConfiguration, corsReq *CORSRequest) (*CORSResponse, error) {
|
||||
if config == nil || corsReq == nil {
|
||||
@@ -189,7 +177,7 @@ func EvaluateRequest(config *CORSConfiguration, corsReq *CORSRequest) (*CORSResp
|
||||
return buildPreflightResponse(&rule, corsReq), nil
|
||||
} else {
|
||||
// For actual requests, check method
|
||||
if contains(rule.AllowedMethods, corsReq.Method) {
|
||||
if containsString(rule.AllowedMethods, corsReq.Method) {
|
||||
return buildResponse(&rule, corsReq), nil
|
||||
}
|
||||
}
|
||||
@@ -199,152 +187,14 @@ func EvaluateRequest(config *CORSConfiguration, corsReq *CORSRequest) (*CORSResp
|
||||
return nil, fmt.Errorf("no matching CORS rule found")
|
||||
}
|
||||
|
||||
// matchesRule checks if a CORS request matches a CORS rule
|
||||
func matchesRule(rule *CORSRule, corsReq *CORSRequest) bool {
|
||||
// Check origin - this is the primary matching criterion
|
||||
if !matchesOrigin(rule.AllowedOrigins, corsReq.Origin) {
|
||||
return false
|
||||
}
|
||||
|
||||
// For preflight requests, we need to validate both the requested method and headers
|
||||
if corsReq.IsPreflightRequest {
|
||||
// Check if the requested method is allowed
|
||||
if corsReq.AccessControlRequestMethod != "" {
|
||||
if !contains(rule.AllowedMethods, corsReq.AccessControlRequestMethod) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check if all requested headers are allowed
|
||||
if len(corsReq.AccessControlRequestHeaders) > 0 {
|
||||
for _, requestedHeader := range corsReq.AccessControlRequestHeaders {
|
||||
if !matchesHeader(rule.AllowedHeaders, requestedHeader) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// For non-preflight requests, check method matching
|
||||
method := corsReq.Method
|
||||
if !contains(rule.AllowedMethods, method) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// matchesOrigin checks if an origin matches any of the allowed origins
|
||||
func matchesOrigin(allowedOrigins []string, origin string) bool {
|
||||
for _, allowedOrigin := range allowedOrigins {
|
||||
if allowedOrigin == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
if allowedOrigin == origin {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check wildcard matching
|
||||
if strings.Contains(allowedOrigin, "*") {
|
||||
if matchesWildcard(allowedOrigin, origin) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchesWildcard checks if an origin matches a wildcard pattern
|
||||
// Uses string manipulation instead of regex for better performance
|
||||
func matchesWildcard(pattern, origin string) bool {
|
||||
// Handle simple cases first
|
||||
if pattern == "*" {
|
||||
return true
|
||||
}
|
||||
if pattern == origin {
|
||||
return true
|
||||
}
|
||||
|
||||
// For CORS, we typically only deal with * wildcards (not ? wildcards)
|
||||
// Use string manipulation for * wildcards only (more efficient than regex)
|
||||
|
||||
// Split pattern by wildcards
|
||||
parts := strings.Split(pattern, "*")
|
||||
if len(parts) == 1 {
|
||||
// No wildcards, exact match
|
||||
return pattern == origin
|
||||
}
|
||||
|
||||
// Check if string starts with first part
|
||||
if len(parts[0]) > 0 && !strings.HasPrefix(origin, parts[0]) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if string ends with last part
|
||||
if len(parts[len(parts)-1]) > 0 && !strings.HasSuffix(origin, parts[len(parts)-1]) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check middle parts
|
||||
searchStr := origin
|
||||
if len(parts[0]) > 0 {
|
||||
searchStr = searchStr[len(parts[0]):]
|
||||
}
|
||||
if len(parts[len(parts)-1]) > 0 {
|
||||
searchStr = searchStr[:len(searchStr)-len(parts[len(parts)-1])]
|
||||
}
|
||||
|
||||
for i := 1; i < len(parts)-1; i++ {
|
||||
if len(parts[i]) > 0 {
|
||||
index := strings.Index(searchStr, parts[i])
|
||||
if index == -1 {
|
||||
return false
|
||||
}
|
||||
searchStr = searchStr[index+len(parts[i]):]
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// matchesHeader checks if a header matches allowed headers
|
||||
func matchesHeader(allowedHeaders []string, header string) bool {
|
||||
if len(allowedHeaders) == 0 {
|
||||
return true // No restrictions
|
||||
}
|
||||
|
||||
for _, allowedHeader := range allowedHeaders {
|
||||
if allowedHeader == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.EqualFold(allowedHeader, header) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check wildcard matching for headers
|
||||
if strings.Contains(allowedHeader, "*") {
|
||||
if matchesWildcard(strings.ToLower(allowedHeader), strings.ToLower(header)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// buildPreflightResponse builds a CORS response for preflight requests
|
||||
// This function allows partial matches - origin can match while methods/headers may not
|
||||
func buildPreflightResponse(rule *CORSRule, corsReq *CORSRequest) *CORSResponse {
|
||||
response := &CORSResponse{
|
||||
AllowOrigin: corsReq.Origin,
|
||||
}
|
||||
|
||||
// Check if the requested method is allowed
|
||||
methodAllowed := corsReq.AccessControlRequestMethod == "" || contains(rule.AllowedMethods, corsReq.AccessControlRequestMethod)
|
||||
methodAllowed := corsReq.AccessControlRequestMethod == "" || containsString(rule.AllowedMethods, corsReq.AccessControlRequestMethod)
|
||||
|
||||
// Check requested headers
|
||||
var allowedRequestHeaders []string
|
||||
@@ -403,42 +253,15 @@ func buildResponse(rule *CORSRule, corsReq *CORSRequest) *CORSResponse {
|
||||
AllowOrigin: corsReq.Origin,
|
||||
}
|
||||
|
||||
// Set allowed methods - for preflight requests, return all allowed methods
|
||||
if corsReq.IsPreflightRequest {
|
||||
response.AllowMethods = strings.Join(rule.AllowedMethods, ", ")
|
||||
} else {
|
||||
// For non-preflight requests, return all allowed methods
|
||||
response.AllowMethods = strings.Join(rule.AllowedMethods, ", ")
|
||||
}
|
||||
// Set allowed methods
|
||||
response.AllowMethods = strings.Join(rule.AllowedMethods, ", ")
|
||||
|
||||
// Set allowed headers
|
||||
if corsReq.IsPreflightRequest && len(rule.AllowedHeaders) > 0 {
|
||||
// For preflight requests, check if wildcard is allowed
|
||||
hasWildcard := false
|
||||
for _, header := range rule.AllowedHeaders {
|
||||
if header == "*" {
|
||||
hasWildcard = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if hasWildcard && len(corsReq.AccessControlRequestHeaders) > 0 {
|
||||
// Return the specific headers that were requested when wildcard is allowed
|
||||
response.AllowHeaders = strings.Join(corsReq.AccessControlRequestHeaders, ", ")
|
||||
} else if len(corsReq.AccessControlRequestHeaders) > 0 {
|
||||
// For non-wildcard cases, return the requested headers (preserving case)
|
||||
// since we already validated they are allowed in matchesRule
|
||||
response.AllowHeaders = strings.Join(corsReq.AccessControlRequestHeaders, ", ")
|
||||
} else {
|
||||
// Fallback to configured headers if no specific headers were requested
|
||||
response.AllowHeaders = strings.Join(rule.AllowedHeaders, ", ")
|
||||
}
|
||||
} else if len(rule.AllowedHeaders) > 0 {
|
||||
// For non-preflight requests, return the allowed headers from the rule
|
||||
if len(rule.AllowedHeaders) > 0 {
|
||||
response.AllowHeaders = strings.Join(rule.AllowedHeaders, ", ")
|
||||
}
|
||||
|
||||
// Set exposed headers
|
||||
// Set expose headers
|
||||
if len(rule.ExposeHeaders) > 0 {
|
||||
response.ExposeHeaders = strings.Join(rule.ExposeHeaders, ", ")
|
||||
}
|
||||
@@ -451,8 +274,77 @@ func buildResponse(rule *CORSRule, corsReq *CORSRequest) *CORSResponse {
|
||||
return response
|
||||
}
|
||||
|
||||
// contains checks if a slice contains a string
|
||||
func contains(slice []string, item string) bool {
|
||||
// Helper functions
|
||||
|
||||
// matchesOrigin checks if the request origin matches any allowed origin
|
||||
func matchesOrigin(allowedOrigins []string, origin string) bool {
|
||||
for _, allowedOrigin := range allowedOrigins {
|
||||
if allowedOrigin == "*" {
|
||||
return true
|
||||
}
|
||||
if allowedOrigin == origin {
|
||||
return true
|
||||
}
|
||||
// Handle wildcard patterns like https://*.example.com
|
||||
if strings.Contains(allowedOrigin, "*") {
|
||||
if matchWildcard(allowedOrigin, origin) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchWildcard performs wildcard matching for origins
|
||||
func matchWildcard(pattern, text string) bool {
|
||||
// Simple wildcard matching - only supports single * at the beginning
|
||||
if strings.HasPrefix(pattern, "http://*") {
|
||||
suffix := pattern[8:] // Remove "http://*"
|
||||
return strings.HasPrefix(text, "http://") && strings.HasSuffix(text, suffix)
|
||||
}
|
||||
if strings.HasPrefix(pattern, "https://*") {
|
||||
suffix := pattern[9:] // Remove "https://*"
|
||||
return strings.HasPrefix(text, "https://") && strings.HasSuffix(text, suffix)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchesHeader checks if a header is allowed
|
||||
func matchesHeader(allowedHeaders []string, header string) bool {
|
||||
// If no headers are specified, all headers are allowed
|
||||
if len(allowedHeaders) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
// Header matching is case-insensitive
|
||||
header = strings.ToLower(header)
|
||||
|
||||
for _, allowedHeader := range allowedHeaders {
|
||||
allowedHeaderLower := strings.ToLower(allowedHeader)
|
||||
|
||||
// Wildcard match
|
||||
if allowedHeaderLower == "*" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Exact match
|
||||
if allowedHeaderLower == header {
|
||||
return true
|
||||
}
|
||||
|
||||
// Prefix wildcard match (e.g., "x-amz-*" matches "x-amz-date")
|
||||
if strings.HasSuffix(allowedHeaderLower, "*") {
|
||||
prefix := strings.TrimSuffix(allowedHeaderLower, "*")
|
||||
if strings.HasPrefix(header, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// containsString checks if a slice contains a specific string
|
||||
func containsString(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
@@ -491,159 +383,3 @@ func ApplyHeaders(w http.ResponseWriter, corsResp *CORSResponse) {
|
||||
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
}
|
||||
|
||||
// FilerClient interface for dependency injection
|
||||
type FilerClient interface {
|
||||
WithFilerClient(streamingMode bool, fn func(filer_pb.SeaweedFilerClient) error) error
|
||||
}
|
||||
|
||||
// EntryGetter interface for getting filer entries
|
||||
type EntryGetter interface {
|
||||
GetEntry(directory, name string) (*filer_pb.Entry, error)
|
||||
}
|
||||
|
||||
// Storage provides CORS configuration storage operations
|
||||
type Storage struct {
|
||||
filerClient FilerClient
|
||||
entryGetter EntryGetter
|
||||
bucketsPath string
|
||||
}
|
||||
|
||||
// NewStorage creates a new CORS storage instance
|
||||
func NewStorage(filerClient FilerClient, entryGetter EntryGetter, bucketsPath string) *Storage {
|
||||
return &Storage{
|
||||
filerClient: filerClient,
|
||||
entryGetter: entryGetter,
|
||||
bucketsPath: bucketsPath,
|
||||
}
|
||||
}
|
||||
|
||||
// Store stores CORS configuration in the filer
|
||||
func (s *Storage) Store(bucket string, config *CORSConfiguration) error {
|
||||
// Store in bucket metadata
|
||||
bucketMetadataPath := filepath.Join(s.bucketsPath, bucket, S3MetadataFileName)
|
||||
|
||||
// Get existing metadata
|
||||
existingEntry, err := s.entryGetter.GetEntry("", bucketMetadataPath)
|
||||
var metadata map[string]interface{}
|
||||
|
||||
if err == nil && existingEntry != nil && len(existingEntry.Content) > 0 {
|
||||
if err := json.Unmarshal(existingEntry.Content, &metadata); err != nil {
|
||||
glog.V(1).Infof("Failed to unmarshal existing metadata: %v", err)
|
||||
metadata = make(map[string]interface{})
|
||||
}
|
||||
} else {
|
||||
metadata = make(map[string]interface{})
|
||||
}
|
||||
|
||||
metadata["cors"] = config
|
||||
|
||||
metadataBytes, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal bucket metadata: %w", err)
|
||||
}
|
||||
|
||||
// Store metadata
|
||||
return s.filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
request := &filer_pb.CreateEntryRequest{
|
||||
Directory: s.bucketsPath + "/" + bucket,
|
||||
Entry: &filer_pb.Entry{
|
||||
Name: S3MetadataFileName,
|
||||
IsDirectory: false,
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: time.Now().Unix(),
|
||||
Mtime: time.Now().Unix(),
|
||||
FileMode: 0644,
|
||||
},
|
||||
Content: metadataBytes,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := client.CreateEntry(context.Background(), request)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// Load loads CORS configuration from the filer
|
||||
func (s *Storage) Load(bucket string) (*CORSConfiguration, error) {
|
||||
bucketMetadataPath := filepath.Join(s.bucketsPath, bucket, S3MetadataFileName)
|
||||
|
||||
entry, err := s.entryGetter.GetEntry("", bucketMetadataPath)
|
||||
if err != nil || entry == nil {
|
||||
return nil, fmt.Errorf("no CORS configuration found")
|
||||
}
|
||||
|
||||
if len(entry.Content) == 0 {
|
||||
return nil, fmt.Errorf("no CORS configuration found")
|
||||
}
|
||||
|
||||
var metadata map[string]interface{}
|
||||
if err := json.Unmarshal(entry.Content, &metadata); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal metadata: %w", err)
|
||||
}
|
||||
|
||||
corsData, exists := metadata["cors"]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("no CORS configuration found")
|
||||
}
|
||||
|
||||
// Convert back to CORSConfiguration
|
||||
corsBytes, err := json.Marshal(corsData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal CORS data: %w", err)
|
||||
}
|
||||
|
||||
var config CORSConfiguration
|
||||
if err := json.Unmarshal(corsBytes, &config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal CORS configuration: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// Delete deletes CORS configuration from the filer
|
||||
func (s *Storage) Delete(bucket string) error {
|
||||
bucketMetadataPath := filepath.Join(s.bucketsPath, bucket, S3MetadataFileName)
|
||||
|
||||
entry, err := s.entryGetter.GetEntry("", bucketMetadataPath)
|
||||
if err != nil || entry == nil {
|
||||
return nil // Already deleted or doesn't exist
|
||||
}
|
||||
|
||||
var metadata map[string]interface{}
|
||||
if len(entry.Content) > 0 {
|
||||
if err := json.Unmarshal(entry.Content, &metadata); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal metadata: %w", err)
|
||||
}
|
||||
} else {
|
||||
return nil // No metadata to delete
|
||||
}
|
||||
|
||||
// Remove CORS configuration
|
||||
delete(metadata, "cors")
|
||||
|
||||
metadataBytes, err := json.Marshal(metadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
// Update metadata
|
||||
return s.filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
|
||||
request := &filer_pb.CreateEntryRequest{
|
||||
Directory: s.bucketsPath + "/" + bucket,
|
||||
Entry: &filer_pb.Entry{
|
||||
Name: S3MetadataFileName,
|
||||
IsDirectory: false,
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
Crtime: time.Now().Unix(),
|
||||
Mtime: time.Now().Unix(),
|
||||
FileMode: 0644,
|
||||
},
|
||||
Content: metadataBytes,
|
||||
},
|
||||
}
|
||||
|
||||
_, err := client.CreateEntry(context.Background(), request)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,94 +20,73 @@ type CORSConfigGetter interface {
|
||||
|
||||
// Middleware handles CORS evaluation for all S3 API requests
|
||||
type Middleware struct {
|
||||
storage *Storage
|
||||
bucketChecker BucketChecker
|
||||
corsConfigGetter CORSConfigGetter
|
||||
}
|
||||
|
||||
// NewMiddleware creates a new CORS middleware instance
|
||||
func NewMiddleware(storage *Storage, bucketChecker BucketChecker, corsConfigGetter CORSConfigGetter) *Middleware {
|
||||
func NewMiddleware(bucketChecker BucketChecker, corsConfigGetter CORSConfigGetter) *Middleware {
|
||||
return &Middleware{
|
||||
storage: storage,
|
||||
bucketChecker: bucketChecker,
|
||||
corsConfigGetter: corsConfigGetter,
|
||||
}
|
||||
}
|
||||
|
||||
// evaluateCORSRequest performs the common CORS request evaluation logic
|
||||
// Returns: (corsResponse, responseWritten, shouldContinue)
|
||||
// - corsResponse: the CORS response if evaluation succeeded
|
||||
// - responseWritten: true if an error response was already written
|
||||
// - shouldContinue: true if the request should continue to the next handler
|
||||
func (m *Middleware) evaluateCORSRequest(w http.ResponseWriter, r *http.Request) (*CORSResponse, bool, bool) {
|
||||
// Parse CORS request
|
||||
corsReq := ParseRequest(r)
|
||||
if corsReq.Origin == "" {
|
||||
// Not a CORS request
|
||||
return nil, false, true
|
||||
}
|
||||
|
||||
// Extract bucket from request
|
||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
||||
if bucket == "" {
|
||||
return nil, false, true
|
||||
}
|
||||
|
||||
// Check if bucket exists
|
||||
if err := m.bucketChecker.CheckBucket(r, bucket); err != s3err.ErrNone {
|
||||
// For non-existent buckets, let the normal handler deal with it
|
||||
return nil, false, true
|
||||
}
|
||||
|
||||
// Load CORS configuration from cache
|
||||
config, errCode := m.corsConfigGetter.GetCORSConfiguration(bucket)
|
||||
if errCode != s3err.ErrNone || config == nil {
|
||||
// No CORS configuration, handle based on request type
|
||||
if corsReq.IsPreflightRequest {
|
||||
// Preflight request without CORS config should fail
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||
return nil, true, false // Response written, don't continue
|
||||
}
|
||||
// Non-preflight request, continue normally
|
||||
return nil, false, true
|
||||
}
|
||||
|
||||
// Evaluate CORS request
|
||||
corsResp, err := EvaluateRequest(config, corsReq)
|
||||
if err != nil {
|
||||
glog.V(3).Infof("CORS evaluation failed for bucket %s: %v", bucket, err)
|
||||
if corsReq.IsPreflightRequest {
|
||||
// Preflight request that doesn't match CORS rules should fail
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||
return nil, true, false // Response written, don't continue
|
||||
}
|
||||
// Non-preflight request, continue normally but without CORS headers
|
||||
return nil, false, true
|
||||
}
|
||||
|
||||
return corsResp, false, false
|
||||
}
|
||||
|
||||
// Handler returns the CORS middleware handler
|
||||
func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Use the common evaluation logic
|
||||
corsResp, responseWritten, shouldContinue := m.evaluateCORSRequest(w, r)
|
||||
if responseWritten {
|
||||
// Response was already written (error case)
|
||||
return
|
||||
}
|
||||
// Parse CORS request
|
||||
corsReq := ParseRequest(r)
|
||||
|
||||
if shouldContinue {
|
||||
// Continue with normal request processing
|
||||
// If not a CORS request, continue normally
|
||||
if corsReq.Origin == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request to check if it's a preflight request
|
||||
corsReq := ParseRequest(r)
|
||||
// Extract bucket from request
|
||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
||||
if bucket == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Apply CORS headers to response
|
||||
// Check if bucket exists
|
||||
if err := m.bucketChecker.CheckBucket(r, bucket); err != s3err.ErrNone {
|
||||
// For non-existent buckets, let the normal handler deal with it
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Load CORS configuration from cache
|
||||
config, errCode := m.corsConfigGetter.GetCORSConfiguration(bucket)
|
||||
if errCode != s3err.ErrNone || config == nil {
|
||||
// No CORS configuration, handle based on request type
|
||||
if corsReq.IsPreflightRequest {
|
||||
// Preflight request without CORS config should fail
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||
return
|
||||
}
|
||||
// Non-preflight request, continue normally
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Evaluate CORS request
|
||||
corsResp, err := EvaluateRequest(config, corsReq)
|
||||
if err != nil {
|
||||
glog.V(3).Infof("CORS evaluation failed for bucket %s: %v", bucket, err)
|
||||
if corsReq.IsPreflightRequest {
|
||||
// Preflight request that doesn't match CORS rules should fail
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||
return
|
||||
}
|
||||
// Non-preflight request, continue normally but without CORS headers
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Apply CORS headers
|
||||
ApplyHeaders(w, corsResp)
|
||||
|
||||
// Handle preflight requests
|
||||
@@ -117,22 +96,56 @@ func (m *Middleware) Handler(next http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
// Continue with normal request processing
|
||||
// For actual requests, continue with normal processing
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// HandleOptionsRequest handles OPTIONS requests for CORS preflight
|
||||
func (m *Middleware) HandleOptionsRequest(w http.ResponseWriter, r *http.Request) {
|
||||
// Use the common evaluation logic
|
||||
corsResp, responseWritten, shouldContinue := m.evaluateCORSRequest(w, r)
|
||||
if responseWritten {
|
||||
// Response was already written (error case)
|
||||
// Parse CORS request
|
||||
corsReq := ParseRequest(r)
|
||||
|
||||
// If not a CORS request, return OK
|
||||
if corsReq.Origin == "" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
if shouldContinue || corsResp == nil {
|
||||
// Not a CORS request or should continue normally
|
||||
// Extract bucket from request
|
||||
bucket, _ := s3_constants.GetBucketAndObject(r)
|
||||
if bucket == "" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if bucket exists
|
||||
if err := m.bucketChecker.CheckBucket(r, bucket); err != s3err.ErrNone {
|
||||
// For non-existent buckets, return OK (let other handlers deal with bucket existence)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Load CORS configuration from cache
|
||||
config, errCode := m.corsConfigGetter.GetCORSConfiguration(bucket)
|
||||
if errCode != s3err.ErrNone || config == nil {
|
||||
// No CORS configuration for OPTIONS request should return access denied
|
||||
if corsReq.IsPreflightRequest {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Evaluate CORS request
|
||||
corsResp, err := EvaluateRequest(config, corsReq)
|
||||
if err != nil {
|
||||
glog.V(3).Infof("CORS evaluation failed for bucket %s: %v", bucket, err)
|
||||
if corsReq.IsPreflightRequest {
|
||||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user