17 changed files with 3376 additions and 25 deletions
-
4.gitignore
-
337test/s3/cors/Makefile
-
362test/s3/cors/README.md
-
36test/s3/cors/go.mod
-
63test/s3/cors/go.sum
-
469test/s3/cors/s3_cors_http_test.go
-
595test/s3/cors/s3_cors_test.go
-
9test/s3/cors/test_config.json
-
516weed/s3api/cors/cors.go
-
523weed/s3api/cors/cors_test.go
-
171weed/s3api/cors/middleware.go
-
71weed/s3api/s3api_bucket_config.go
-
160weed/s3api/s3api_bucket_cors_handlers.go
-
18weed/s3api/s3api_bucket_skip_handlers.go
-
22weed/s3api/s3api_object_handlers.go
-
12weed/s3api/s3api_server.go
-
33weed/s3api/s3err/error_handler.go
@ -0,0 +1,337 @@ |
|||
# CORS Integration Tests Makefile
|
|||
# This Makefile provides comprehensive targets for running CORS integration tests
|
|||
|
|||
.PHONY: help build-weed setup-server start-server stop-server test-cors test-cors-quick test-cors-comprehensive test-all clean logs check-deps |
|||
|
|||
# Configuration
|
|||
WEED_BINARY := ../../../weed/weed_binary |
|||
S3_PORT := 8333 |
|||
MASTER_PORT := 9333 |
|||
VOLUME_PORT := 8080 |
|||
FILER_PORT := 8888 |
|||
TEST_TIMEOUT := 10m |
|||
TEST_PATTERN := TestCORS |
|||
|
|||
# Default target
|
|||
help: |
|||
@echo "CORS Integration Tests Makefile" |
|||
@echo "" |
|||
@echo "Available targets:" |
|||
@echo " help - Show this help message" |
|||
@echo " build-weed - Build the SeaweedFS binary" |
|||
@echo " check-deps - Check dependencies and build binary if needed" |
|||
@echo " start-server - Start SeaweedFS server for testing" |
|||
@echo " start-server-simple - Start server without process cleanup (for CI)" |
|||
@echo " stop-server - Stop SeaweedFS server" |
|||
@echo " test-cors - Run all CORS tests" |
|||
@echo " test-cors-quick - Run core CORS tests only" |
|||
@echo " test-cors-simple - Run tests without server management" |
|||
@echo " test-cors-comprehensive - Run comprehensive CORS tests" |
|||
@echo " test-with-server - Start server, run tests, stop server" |
|||
@echo " logs - Show server logs" |
|||
@echo " clean - Clean up test artifacts and stop server" |
|||
@echo " health-check - Check if server is accessible" |
|||
@echo "" |
|||
@echo "Configuration:" |
|||
@echo " S3_PORT=${S3_PORT}" |
|||
@echo " TEST_TIMEOUT=${TEST_TIMEOUT}" |
|||
|
|||
# Build the SeaweedFS binary
|
|||
build-weed: |
|||
@echo "Building SeaweedFS binary..." |
|||
@cd ../../../weed && go build -o weed_binary . |
|||
@chmod +x $(WEED_BINARY) |
|||
@echo "✅ SeaweedFS binary built at $(WEED_BINARY)" |
|||
|
|||
check-deps: build-weed |
|||
@echo "Checking dependencies..." |
|||
@echo "🔍 DEBUG: Checking Go installation..." |
|||
@command -v go >/dev/null 2>&1 || (echo "Go is required but not installed" && exit 1) |
|||
@echo "🔍 DEBUG: Go version: $$(go version)" |
|||
@echo "🔍 DEBUG: Checking binary at $(WEED_BINARY)..." |
|||
@test -f $(WEED_BINARY) || (echo "SeaweedFS binary not found at $(WEED_BINARY)" && exit 1) |
|||
@echo "🔍 DEBUG: Binary size: $$(ls -lh $(WEED_BINARY) | awk '{print $$5}')" |
|||
@echo "🔍 DEBUG: Binary permissions: $$(ls -la $(WEED_BINARY) | awk '{print $$1}')" |
|||
@echo "🔍 DEBUG: Checking Go module dependencies..." |
|||
@go list -m github.com/aws/aws-sdk-go-v2 >/dev/null 2>&1 || (echo "AWS SDK Go v2 not found. Run 'go mod tidy'." && exit 1) |
|||
@go list -m github.com/stretchr/testify >/dev/null 2>&1 || (echo "Testify not found. Run 'go mod tidy'." && exit 1) |
|||
@echo "✅ All dependencies are available" |
|||
|
|||
# Start SeaweedFS server for testing
|
|||
start-server: check-deps |
|||
@echo "Starting SeaweedFS server..." |
|||
@echo "🔍 DEBUG: Current working directory: $$(pwd)" |
|||
@echo "🔍 DEBUG: Checking for existing weed processes..." |
|||
@ps aux | grep weed | grep -v grep || echo "No existing weed processes found" |
|||
@echo "🔍 DEBUG: Cleaning up any existing PID file..." |
|||
@rm -f weed-server.pid |
|||
@echo "🔍 DEBUG: Checking for port conflicts..." |
|||
@if netstat -tlnp 2>/dev/null | grep $(S3_PORT) >/dev/null; then \
|
|||
echo "⚠️ Port $(S3_PORT) is already in use, trying to find the process..."; \
|
|||
netstat -tlnp 2>/dev/null | grep $(S3_PORT) || true; \
|
|||
else \
|
|||
echo "✅ Port $(S3_PORT) is available"; \
|
|||
fi |
|||
@echo "🔍 DEBUG: Checking binary at $(WEED_BINARY)" |
|||
@ls -la $(WEED_BINARY) || (echo "❌ Binary not found!" && exit 1) |
|||
@echo "🔍 DEBUG: Checking config file at ../../../docker/compose/s3.json" |
|||
@ls -la ../../../docker/compose/s3.json || echo "⚠️ Config file not found, continuing without it" |
|||
@echo "🔍 DEBUG: Creating volume directory..." |
|||
@mkdir -p ./test-volume-data |
|||
@echo "🔍 DEBUG: Launching SeaweedFS server in background..." |
|||
@echo "🔍 DEBUG: Command: $(WEED_BINARY) server -debug -s3 -s3.port=$(S3_PORT) -s3.allowEmptyFolder=false -s3.allowDeleteBucketNotEmpty=true -s3.config=../../../docker/compose/s3.json -filer -filer.maxMB=64 -master.volumeSizeLimitMB=50 -volume.max=100 -dir=./test-volume-data -volume.preStopSeconds=1 -metricsPort=9324" |
|||
@$(WEED_BINARY) server \
|
|||
-debug \
|
|||
-s3 \
|
|||
-s3.port=$(S3_PORT) \
|
|||
-s3.allowEmptyFolder=false \
|
|||
-s3.allowDeleteBucketNotEmpty=true \
|
|||
-s3.config=../../../docker/compose/s3.json \
|
|||
-filer \
|
|||
-filer.maxMB=64 \
|
|||
-master.volumeSizeLimitMB=50 \
|
|||
-volume.max=100 \
|
|||
-dir=./test-volume-data \
|
|||
-volume.preStopSeconds=1 \
|
|||
-metricsPort=9324 \
|
|||
> weed-test.log 2>&1 & echo $$! > weed-server.pid |
|||
@echo "🔍 DEBUG: Server PID: $$(cat weed-server.pid 2>/dev/null || echo 'PID file not found')" |
|||
@echo "🔍 DEBUG: Checking if PID is still running..." |
|||
@sleep 2 |
|||
@if [ -f weed-server.pid ]; then \
|
|||
SERVER_PID=$$(cat weed-server.pid); \
|
|||
ps -p $$SERVER_PID || echo "⚠️ Server PID $$SERVER_PID not found after 2 seconds"; \
|
|||
else \
|
|||
echo "⚠️ PID file not found"; \
|
|||
fi |
|||
@echo "🔍 DEBUG: Waiting for server to start (up to 90 seconds)..." |
|||
@for i in $$(seq 1 90); do \
|
|||
echo "🔍 DEBUG: Attempt $$i/90 - checking port $(S3_PORT)"; \
|
|||
if curl -s http://localhost:$(S3_PORT) >/dev/null 2>&1; then \
|
|||
echo "✅ SeaweedFS server started successfully on port $(S3_PORT) after $$i seconds"; \
|
|||
exit 0; \
|
|||
fi; \
|
|||
if [ $$i -eq 5 ]; then \
|
|||
echo "🔍 DEBUG: After 5 seconds, checking process and logs..."; \
|
|||
ps aux | grep weed | grep -v grep || echo "No weed processes found"; \
|
|||
if [ -f weed-test.log ]; then \
|
|||
echo "=== First server logs ==="; \
|
|||
head -20 weed-test.log; \
|
|||
fi; \
|
|||
fi; \
|
|||
if [ $$i -eq 15 ]; then \
|
|||
echo "🔍 DEBUG: After 15 seconds, checking port bindings..."; \
|
|||
netstat -tlnp 2>/dev/null | grep $(S3_PORT) || echo "Port $(S3_PORT) not bound"; \
|
|||
netstat -tlnp 2>/dev/null | grep 9333 || echo "Port 9333 not bound"; \
|
|||
netstat -tlnp 2>/dev/null | grep 8080 || echo "Port 8080 not bound"; \
|
|||
fi; \
|
|||
if [ $$i -eq 30 ]; then \
|
|||
echo "⚠️ Server taking longer than expected (30s), checking logs..."; \
|
|||
if [ -f weed-test.log ]; then \
|
|||
echo "=== Recent server logs ==="; \
|
|||
tail -20 weed-test.log; \
|
|||
fi; \
|
|||
fi; \
|
|||
sleep 1; \
|
|||
done; \
|
|||
echo "❌ Server failed to start within 90 seconds"; \
|
|||
echo "🔍 DEBUG: Final process check:"; \
|
|||
ps aux | grep weed | grep -v grep || echo "No weed processes found"; \
|
|||
echo "🔍 DEBUG: Final port check:"; \
|
|||
netstat -tlnp 2>/dev/null | grep -E "(8333|9333|8080)" || echo "No ports bound"; \
|
|||
echo "=== Full server logs ==="; \
|
|||
if [ -f weed-test.log ]; then \
|
|||
cat weed-test.log; \
|
|||
else \
|
|||
echo "No log file found"; \
|
|||
fi; \
|
|||
exit 1 |
|||
|
|||
# Stop SeaweedFS server
|
|||
stop-server: |
|||
@echo "Stopping SeaweedFS server..." |
|||
@if [ -f weed-server.pid ]; then \
|
|||
SERVER_PID=$$(cat weed-server.pid); \
|
|||
echo "Killing server PID $$SERVER_PID"; \
|
|||
if ps -p $$SERVER_PID >/dev/null 2>&1; then \
|
|||
kill -TERM $$SERVER_PID 2>/dev/null || true; \
|
|||
sleep 2; \
|
|||
if ps -p $$SERVER_PID >/dev/null 2>&1; then \
|
|||
echo "Process still running, sending KILL signal..."; \
|
|||
kill -KILL $$SERVER_PID 2>/dev/null || true; \
|
|||
sleep 1; \
|
|||
fi; \
|
|||
else \
|
|||
echo "Process $$SERVER_PID not found (already stopped)"; \
|
|||
fi; \
|
|||
rm -f weed-server.pid; \
|
|||
else \
|
|||
echo "No PID file found, checking for running processes..."; \
|
|||
echo "⚠️ Skipping automatic process cleanup to avoid CI issues"; \
|
|||
echo "Note: Any remaining weed processes should be cleaned up by the CI environment"; \
|
|||
fi |
|||
@echo "✅ SeaweedFS server stopped" |
|||
|
|||
# Show server logs
|
|||
logs: |
|||
@if test -f weed-test.log; then \
|
|||
echo "=== SeaweedFS Server Logs ==="; \
|
|||
tail -f weed-test.log; \
|
|||
else \
|
|||
echo "No log file found. Server may not be running."; \
|
|||
fi |
|||
|
|||
# Core CORS tests (basic functionality)
|
|||
test-cors-quick: check-deps |
|||
@echo "Running core CORS tests..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "TestCORSConfigurationManagement|TestCORSPreflightRequest|TestCORSActualRequest" . |
|||
@echo "✅ Core CORS tests completed" |
|||
|
|||
# All CORS tests (comprehensive)
|
|||
test-cors: check-deps |
|||
@echo "Running all CORS tests..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "$(TEST_PATTERN)" . |
|||
@echo "✅ All CORS tests completed" |
|||
|
|||
# Comprehensive CORS tests (all features)
|
|||
test-cors-comprehensive: check-deps |
|||
@echo "Running comprehensive CORS tests..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "TestCORS" . |
|||
@echo "✅ Comprehensive CORS tests completed" |
|||
|
|||
# All tests without server management
|
|||
test-cors-simple: check-deps |
|||
@echo "Running CORS tests (assuming server is already running)..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) . |
|||
@echo "✅ All CORS tests completed" |
|||
|
|||
# Start server, run tests, stop server
|
|||
test-with-server: start-server |
|||
@echo "Running CORS tests with managed server..." |
|||
@sleep 5 # Give server time to fully start |
|||
@make test-cors-comprehensive || (echo "Tests failed, stopping server..." && make stop-server && exit 1) |
|||
@make stop-server |
|||
@echo "✅ All tests completed with managed server" |
|||
|
|||
# Health check
|
|||
health-check: |
|||
@echo "Checking server health..." |
|||
@if curl -s http://localhost:$(S3_PORT) >/dev/null 2>&1; then \
|
|||
echo "✅ Server is accessible on port $(S3_PORT)"; \
|
|||
else \
|
|||
echo "❌ Server is not accessible on port $(S3_PORT)"; \
|
|||
exit 1; \
|
|||
fi |
|||
|
|||
# Clean up
|
|||
clean: |
|||
@echo "Cleaning up test artifacts..." |
|||
@make stop-server |
|||
@rm -f weed-test.log |
|||
@rm -f weed-server.pid |
|||
@rm -rf ./test-volume-data |
|||
@rm -f cors.test |
|||
@go clean -testcache |
|||
@echo "✅ Cleanup completed" |
|||
|
|||
# Individual test targets for specific functionality
|
|||
test-basic-cors: |
|||
@echo "Running basic CORS tests..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "TestCORSConfigurationManagement" . |
|||
|
|||
test-preflight-cors: |
|||
@echo "Running preflight CORS tests..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "TestCORSPreflightRequest" . |
|||
|
|||
test-actual-cors: |
|||
@echo "Running actual CORS request tests..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "TestCORSActualRequest" . |
|||
|
|||
test-origin-matching: |
|||
@echo "Running origin matching tests..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "TestCORSOriginMatching" . |
|||
|
|||
test-header-matching: |
|||
@echo "Running header matching tests..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "TestCORSHeaderMatching" . |
|||
|
|||
test-method-matching: |
|||
@echo "Running method matching tests..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "TestCORSMethodMatching" . |
|||
|
|||
test-multiple-rules: |
|||
@echo "Running multiple rules tests..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "TestCORSMultipleRulesMatching" . |
|||
|
|||
test-validation: |
|||
@echo "Running validation tests..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "TestCORSValidation" . |
|||
|
|||
test-caching: |
|||
@echo "Running caching tests..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "TestCORSCaching" . |
|||
|
|||
test-error-handling: |
|||
@echo "Running error handling tests..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "TestCORSErrorHandling" . |
|||
|
|||
# Development targets
|
|||
dev-start: start-server |
|||
@echo "Development server started. Access S3 API at http://localhost:$(S3_PORT)" |
|||
@echo "To stop: make stop-server" |
|||
|
|||
dev-test: check-deps |
|||
@echo "Running tests in development mode..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -run "TestCORSConfigurationManagement" . |
|||
|
|||
# CI targets
|
|||
ci-test: check-deps |
|||
@echo "Running tests in CI mode..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -race . |
|||
|
|||
# All targets
|
|||
test-all: test-cors test-cors-comprehensive |
|||
@echo "✅ All CORS tests completed" |
|||
|
|||
# Benchmark targets
|
|||
benchmark-cors: |
|||
@echo "Running CORS performance benchmarks..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -bench=. -benchmem . |
|||
|
|||
# Coverage targets
|
|||
coverage: |
|||
@echo "Running tests with coverage..." |
|||
@go test -v -timeout=$(TEST_TIMEOUT) -coverprofile=coverage.out . |
|||
@go tool cover -html=coverage.out -o coverage.html |
|||
@echo "Coverage report generated: coverage.html" |
|||
|
|||
# Format and lint
|
|||
fmt: |
|||
@echo "Formatting Go code..." |
|||
@go fmt . |
|||
|
|||
lint: |
|||
@echo "Running linter..." |
|||
@golint . || echo "golint not available, skipping..." |
|||
|
|||
# Install dependencies for development
|
|||
install-deps: |
|||
@echo "Installing Go dependencies..." |
|||
@go mod tidy |
|||
@go mod download |
|||
|
|||
# Show current configuration
|
|||
show-config: |
|||
@echo "Current configuration:" |
|||
@echo " WEED_BINARY: $(WEED_BINARY)" |
|||
@echo " S3_PORT: $(S3_PORT)" |
|||
@echo " TEST_TIMEOUT: $(TEST_TIMEOUT)" |
|||
@echo " TEST_PATTERN: $(TEST_PATTERN)" |
|||
|
|||
# Legacy targets for backward compatibility
|
|||
test: test-with-server |
|||
test-verbose: test-cors-comprehensive |
|||
test-single: test-basic-cors |
|||
test-clean: clean |
|||
build: check-deps |
|||
setup: check-deps |
@ -0,0 +1,362 @@ |
|||
# CORS Integration Tests for SeaweedFS S3 API |
|||
|
|||
This directory contains comprehensive integration tests for the CORS (Cross-Origin Resource Sharing) functionality in SeaweedFS S3 API. |
|||
|
|||
## Overview |
|||
|
|||
The CORS integration tests validate the complete CORS implementation including: |
|||
- CORS configuration management (PUT/GET/DELETE) |
|||
- CORS rule validation |
|||
- CORS middleware behavior |
|||
- Caching functionality |
|||
- Error handling |
|||
- Real-world CORS scenarios |
|||
|
|||
## Prerequisites |
|||
|
|||
1. **Go 1.19+**: For building SeaweedFS and running tests |
|||
2. **Network Access**: Tests use `localhost:8333` by default |
|||
3. **System Dependencies**: `curl` and `netstat` for health checks |
|||
|
|||
## Quick Start |
|||
|
|||
The tests now automatically start their own SeaweedFS server, so you don't need to manually start one. |
|||
|
|||
### 1. Run All Tests with Managed Server |
|||
|
|||
```bash |
|||
# Run all tests with automatic server management |
|||
make test-with-server |
|||
|
|||
# Run core CORS tests only |
|||
make test-cors-quick |
|||
|
|||
# Run comprehensive CORS tests |
|||
make test-cors-comprehensive |
|||
``` |
|||
|
|||
### 2. Manual Server Management |
|||
|
|||
If you prefer to manage the server manually: |
|||
|
|||
```bash |
|||
# Start server |
|||
make start-server |
|||
|
|||
# Run tests (assuming server is running) |
|||
make test-cors-simple |
|||
|
|||
# Stop server |
|||
make stop-server |
|||
``` |
|||
|
|||
### 3. Individual Test Categories |
|||
|
|||
```bash |
|||
# Run specific test types |
|||
make test-basic-cors # Basic CORS configuration |
|||
make test-preflight-cors # Preflight OPTIONS requests |
|||
make test-actual-cors # Actual CORS request handling |
|||
make test-origin-matching # Origin matching logic |
|||
make test-header-matching # Header matching logic |
|||
make test-method-matching # Method matching logic |
|||
make test-multiple-rules # Multiple CORS rules |
|||
make test-validation # CORS validation |
|||
make test-caching # CORS caching behavior |
|||
make test-error-handling # Error handling |
|||
``` |
|||
|
|||
## Test Server Management |
|||
|
|||
The tests use a comprehensive server management system similar to other SeaweedFS integration tests: |
|||
|
|||
### Server Configuration |
|||
|
|||
- **S3 Port**: 8333 (configurable via `S3_PORT`) |
|||
- **Master Port**: 9333 |
|||
- **Volume Port**: 8080 |
|||
- **Filer Port**: 8888 |
|||
- **Metrics Port**: 9324 |
|||
- **Data Directory**: `./test-volume-data` (auto-created) |
|||
- **Log File**: `weed-test.log` |
|||
|
|||
### Server Lifecycle |
|||
|
|||
1. **Build**: Automatically builds `../../../weed/weed_binary` |
|||
2. **Start**: Launches SeaweedFS with S3 API enabled |
|||
3. **Health Check**: Waits up to 90 seconds for server to be ready |
|||
4. **Test**: Runs the requested tests |
|||
5. **Stop**: Gracefully shuts down the server |
|||
6. **Cleanup**: Removes temporary files and data |
|||
|
|||
### Available Commands |
|||
|
|||
```bash |
|||
# Server management |
|||
make start-server # Start SeaweedFS server |
|||
make stop-server # Stop SeaweedFS server |
|||
make health-check # Check server health |
|||
make logs # View server logs |
|||
|
|||
# Test execution |
|||
make test-with-server # Full test cycle with server management |
|||
make test-cors-simple # Run tests without server management |
|||
make test-cors-quick # Run core tests only |
|||
make test-cors-comprehensive # Run all tests |
|||
|
|||
# Development |
|||
make dev-start # Start server for development |
|||
make dev-test # Run development tests |
|||
make build-weed # Build SeaweedFS binary |
|||
make check-deps # Check dependencies |
|||
|
|||
# Maintenance |
|||
make clean # Clean up all artifacts |
|||
make coverage # Generate coverage report |
|||
make fmt # Format code |
|||
make lint # Run linter |
|||
``` |
|||
|
|||
## Test Configuration |
|||
|
|||
### Default Configuration |
|||
|
|||
The tests use these default settings (configurable via environment variables): |
|||
|
|||
```bash |
|||
WEED_BINARY=../../../weed/weed_binary |
|||
S3_PORT=8333 |
|||
TEST_TIMEOUT=10m |
|||
TEST_PATTERN=TestCORS |
|||
``` |
|||
|
|||
### Configuration File |
|||
|
|||
The `test_config.json` file contains S3 client configuration: |
|||
|
|||
```json |
|||
{ |
|||
"endpoint": "http://localhost:8333", |
|||
"access_key": "some_access_key1", |
|||
"secret_key": "some_secret_key1", |
|||
"region": "us-east-1", |
|||
"bucket_prefix": "test-cors-", |
|||
"use_ssl": false, |
|||
"skip_verify_ssl": true |
|||
} |
|||
``` |
|||
|
|||
## Troubleshooting |
|||
|
|||
### Compilation Issues |
|||
|
|||
If you encounter compilation errors, the most common issues are: |
|||
|
|||
1. **AWS SDK v2 Type Mismatches**: The `MaxAgeSeconds` field in `types.CORSRule` expects `int32`, not `*int32`. Use direct values like `3600` instead of `aws.Int32(3600)`. |
|||
|
|||
2. **Field Name Issues**: The `GetBucketCorsOutput` type has a `CORSRules` field directly, not a `CORSConfiguration` field. |
|||
|
|||
Example fix: |
|||
```go |
|||
// ❌ Incorrect |
|||
MaxAgeSeconds: aws.Int32(3600), |
|||
assert.Len(t, getResp.CORSConfiguration.CORSRules, 1) |
|||
|
|||
// ✅ Correct |
|||
MaxAgeSeconds: 3600, |
|||
assert.Len(t, getResp.CORSRules, 1) |
|||
``` |
|||
|
|||
### Server Issues |
|||
|
|||
1. **Server Won't Start** |
|||
```bash |
|||
# Check for port conflicts |
|||
netstat -tlnp | grep 8333 |
|||
|
|||
# View server logs |
|||
make logs |
|||
|
|||
# Force cleanup |
|||
make clean |
|||
``` |
|||
|
|||
2. **Test Failures** |
|||
```bash |
|||
# Run with server management |
|||
make test-with-server |
|||
|
|||
# Run specific test |
|||
make test-basic-cors |
|||
|
|||
# Check server health |
|||
make health-check |
|||
``` |
|||
|
|||
3. **Connection Issues** |
|||
```bash |
|||
# Verify server is running |
|||
curl -s http://localhost:8333 |
|||
|
|||
# Check server logs |
|||
tail -f weed-test.log |
|||
``` |
|||
|
|||
### Performance Issues |
|||
|
|||
If tests are slow or timing out: |
|||
|
|||
```bash |
|||
# Increase timeout |
|||
export TEST_TIMEOUT=30m |
|||
make test-with-server |
|||
|
|||
# Run quick tests only |
|||
make test-cors-quick |
|||
|
|||
# Check server resources |
|||
make debug-status |
|||
``` |
|||
|
|||
## Test Coverage |
|||
|
|||
### Core Functionality Tests |
|||
|
|||
#### 1. CORS Configuration Management (`TestCORSConfigurationManagement`) |
|||
- PUT CORS configuration |
|||
- GET CORS configuration |
|||
- DELETE CORS configuration |
|||
- Configuration updates |
|||
- Error handling for non-existent configurations |
|||
|
|||
#### 2. Multiple CORS Rules (`TestCORSMultipleRules`) |
|||
- Multiple rules in single configuration |
|||
- Rule precedence and ordering |
|||
- Complex rule combinations |
|||
|
|||
#### 3. CORS Validation (`TestCORSValidation`) |
|||
- Invalid HTTP methods |
|||
- Empty origins validation |
|||
- Negative MaxAge validation |
|||
- Rule limit validation |
|||
|
|||
#### 4. Wildcard Support (`TestCORSWithWildcards`) |
|||
- Wildcard origins (`*`, `https://*.example.com`) |
|||
- Wildcard headers (`*`) |
|||
- Wildcard expose headers |
|||
|
|||
#### 5. Rule Limits (`TestCORSRuleLimit`) |
|||
- Maximum 100 rules per configuration |
|||
- Rule limit enforcement |
|||
- Large configuration handling |
|||
|
|||
#### 6. Error Handling (`TestCORSErrorHandling`) |
|||
- Non-existent bucket operations |
|||
- Invalid configurations |
|||
- Malformed requests |
|||
|
|||
### HTTP-Level Tests |
|||
|
|||
#### 1. Preflight Requests (`TestCORSPreflightRequest`) |
|||
- OPTIONS request handling |
|||
- CORS headers in preflight responses |
|||
- Access-Control-Request-Method validation |
|||
- Access-Control-Request-Headers validation |
|||
|
|||
#### 2. Actual Requests (`TestCORSActualRequest`) |
|||
- CORS headers in actual responses |
|||
- Origin validation for real requests |
|||
- Proper expose headers handling |
|||
|
|||
#### 3. Origin Matching (`TestCORSOriginMatching`) |
|||
- Exact origin matching |
|||
- Wildcard origin matching (`*`) |
|||
- Subdomain wildcard matching (`https://*.example.com`) |
|||
- Non-matching origins (should be rejected) |
|||
|
|||
#### 4. Header Matching (`TestCORSHeaderMatching`) |
|||
- Wildcard header matching (`*`) |
|||
- Specific header matching |
|||
- Case-insensitive matching |
|||
- Disallowed headers |
|||
|
|||
#### 5. Method Matching (`TestCORSMethodMatching`) |
|||
- Allowed methods verification |
|||
- Disallowed methods rejection |
|||
- Method-specific CORS behavior |
|||
|
|||
#### 6. Multiple Rules (`TestCORSMultipleRulesMatching`) |
|||
- Rule precedence and selection |
|||
- Multiple rules with different configurations |
|||
- Complex rule interactions |
|||
|
|||
### Integration Tests |
|||
|
|||
#### 1. Caching (`TestCORSCaching`) |
|||
- CORS configuration caching |
|||
- Cache invalidation |
|||
- Cache performance |
|||
|
|||
#### 2. Object Operations (`TestCORSObjectOperations`) |
|||
- CORS with actual S3 operations |
|||
- PUT/GET/DELETE objects with CORS |
|||
- CORS headers in object responses |
|||
|
|||
#### 3. Without Configuration (`TestCORSWithoutConfiguration`) |
|||
- Behavior when no CORS configuration exists |
|||
- Default CORS behavior |
|||
- Graceful degradation |
|||
|
|||
## Development |
|||
|
|||
### Running Tests During Development |
|||
|
|||
```bash |
|||
# Start server for development |
|||
make dev-start |
|||
|
|||
# Run quick test |
|||
make dev-test |
|||
|
|||
# View logs in real-time |
|||
make logs |
|||
``` |
|||
|
|||
### Adding New Tests |
|||
|
|||
1. Follow the existing naming convention (`TestCORSXxxYyy`) |
|||
2. Use the helper functions (`getS3Client`, `createTestBucket`, etc.) |
|||
3. Add cleanup with `defer cleanupTestBucket(t, client, bucketName)` |
|||
4. Include proper error checking with `require.NoError(t, err)` |
|||
5. Use assertions with `assert.Equal(t, expected, actual)` |
|||
6. Add the test to the appropriate Makefile target |
|||
|
|||
### Code Quality |
|||
|
|||
```bash |
|||
# Format code |
|||
make fmt |
|||
|
|||
# Run linter |
|||
make lint |
|||
|
|||
# Generate coverage report |
|||
make coverage |
|||
``` |
|||
|
|||
## Performance Notes |
|||
|
|||
- Tests create and destroy buckets for each test case |
|||
- Large configuration tests may take several minutes |
|||
- Server startup typically takes 15-30 seconds |
|||
- Tests run in parallel where possible for efficiency |
|||
|
|||
## Integration with SeaweedFS |
|||
|
|||
These tests validate the CORS implementation in: |
|||
- `weed/s3api/cors/` - Core CORS package |
|||
- `weed/s3api/s3api_bucket_cors_handlers.go` - HTTP handlers |
|||
- `weed/s3api/s3api_server.go` - Router integration |
|||
- `weed/s3api/s3api_bucket_config.go` - Configuration management |
|||
|
|||
The tests ensure AWS S3 API compatibility and proper CORS behavior across all supported scenarios. |
@ -0,0 +1,36 @@ |
|||
module github.com/seaweedfs/seaweedfs/test/s3/cors |
|||
|
|||
go 1.19 |
|||
|
|||
require ( |
|||
github.com/aws/aws-sdk-go-v2 v1.21.0 |
|||
github.com/aws/aws-sdk-go-v2/config v1.18.42 |
|||
github.com/aws/aws-sdk-go-v2/credentials v1.13.40 |
|||
github.com/aws/aws-sdk-go-v2/service/s3 v1.40.0 |
|||
github.com/k0kubun/pp v3.0.1+incompatible |
|||
github.com/stretchr/testify v1.8.4 |
|||
) |
|||
|
|||
require ( |
|||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13 // indirect |
|||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 // indirect |
|||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 // indirect |
|||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 // indirect |
|||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.43 // indirect |
|||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.4 // indirect |
|||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.14 // indirect |
|||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.36 // indirect |
|||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 // indirect |
|||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.4 // indirect |
|||
github.com/aws/aws-sdk-go-v2/service/sso v1.14.1 // indirect |
|||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.1 // indirect |
|||
github.com/aws/aws-sdk-go-v2/service/sts v1.22.0 // indirect |
|||
github.com/aws/smithy-go v1.14.2 // indirect |
|||
github.com/davecgh/go-spew v1.1.1 // indirect |
|||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 // indirect |
|||
github.com/mattn/go-colorable v0.1.13 // indirect |
|||
github.com/mattn/go-isatty v0.0.16 // indirect |
|||
github.com/pmezard/go-difflib v1.0.0 // indirect |
|||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab // indirect |
|||
gopkg.in/yaml.v3 v3.0.1 // indirect |
|||
) |
@ -0,0 +1,63 @@ |
|||
github.com/aws/aws-sdk-go-v2 v1.21.0 h1:gMT0IW+03wtYJhRqTVYn0wLzwdnK9sRMcxmtfGzRdJc= |
|||
github.com/aws/aws-sdk-go-v2 v1.21.0/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M= |
|||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13 h1:OPLEkmhXf6xFPiz0bLeDArZIDx1NNS4oJyG4nv3Gct0= |
|||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13/go.mod h1:gpAbvyDGQFozTEmlTFO8XcQKHzubdq0LzRyJpG6MiXM= |
|||
github.com/aws/aws-sdk-go-v2/config v1.18.42 h1:28jHROB27xZwU0CB88giDSjz7M1Sba3olb5JBGwina8= |
|||
github.com/aws/aws-sdk-go-v2/config v1.18.42/go.mod h1:4AZM3nMMxwlG+eZlxvBKqwVbkDLlnN2a4UGTL6HjaZI= |
|||
github.com/aws/aws-sdk-go-v2/credentials v1.13.40 h1:s8yOkDh+5b1jUDhMBtngF6zKWLDs84chUk2Vk0c38Og= |
|||
github.com/aws/aws-sdk-go-v2/credentials v1.13.40/go.mod h1:VtEHVAAqDWASwdOqj/1huyT6uHbs5s8FUHfDQdky/Rs= |
|||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11 h1:uDZJF1hu0EVT/4bogChk8DyjSF6fof6uL/0Y26Ma7Fg= |
|||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.11/go.mod h1:TEPP4tENqBGO99KwVpV9MlOX4NSrSLP8u3KRy2CDwA8= |
|||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41 h1:22dGT7PneFMx4+b3pz7lMTRyN8ZKH7M2cW4GP9yUS2g= |
|||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.41/go.mod h1:CrObHAuPneJBlfEJ5T3szXOUkLEThaGfvnhTf33buas= |
|||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35 h1:SijA0mgjV8E+8G45ltVHs0fvKpTj8xmZJ3VwhGKtUSI= |
|||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.35/go.mod h1:SJC1nEVVva1g3pHAIdCp7QsRIkMmLAgoDquQ9Rr8kYw= |
|||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.43 h1:g+qlObJH4Kn4n21g69DjspU0hKTjWtq7naZ9OLCv0ew= |
|||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.43/go.mod h1:rzfdUlfA+jdgLDmPKjd3Chq9V7LVLYo1Nz++Wb91aRo= |
|||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.4 h1:6lJvvkQ9HmbHZ4h/IEwclwv2mrTW8Uq1SOB/kXy0mfw= |
|||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.4/go.mod h1:1PrKYwxTM+zjpw9Y41KFtoJCQrJ34Z47Y4VgVbfndjo= |
|||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.14 h1:m0QTSI6pZYJTk5WSKx3fm5cNW/DCicVzULBgU/6IyD0= |
|||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.14/go.mod h1:dDilntgHy9WnHXsh7dDtUPgHKEfTJIBUTHM8OWm0f/0= |
|||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.36 h1:eev2yZX7esGRjqRbnVk1UxMLw4CyVZDpZXRCcy75oQk= |
|||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.36/go.mod h1:lGnOkH9NJATw0XEPcAknFBj3zzNTEGRHtSw+CwC1YTg= |
|||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35 h1:CdzPW9kKitgIiLV1+MHobfR5Xg25iYnyzWZhyQuSlDI= |
|||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.35/go.mod h1:QGF2Rs33W5MaN9gYdEQOBBFPLwTZkEhRwI33f7KIG0o= |
|||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.4 h1:v0jkRigbSD6uOdwcaUQmgEwG1BkPfAPDqaeNt/29ghg= |
|||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.15.4/go.mod h1:LhTyt8J04LL+9cIt7pYJ5lbS/U98ZmXovLOR/4LUsk8= |
|||
github.com/aws/aws-sdk-go-v2/service/s3 v1.40.0 h1:wl5dxN1NONhTDQD9uaEvNsDRX29cBmGED/nl0jkWlt4= |
|||
github.com/aws/aws-sdk-go-v2/service/s3 v1.40.0/go.mod h1:rDGMZA7f4pbmTtPOk5v5UM2lmX6UAbRnMDJeDvnH7AM= |
|||
github.com/aws/aws-sdk-go-v2/service/sso v1.14.1 h1:YkNzx1RLS0F5qdf9v1Q8Cuv9NXCL2TkosOxhzlUPV64= |
|||
github.com/aws/aws-sdk-go-v2/service/sso v1.14.1/go.mod h1:fIAwKQKBFu90pBxx07BFOMJLpRUGu8VOzLJakeY+0K4= |
|||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.1 h1:8lKOidPkmSmfUtiTgtdXWgaKItCZ/g75/jEk6Ql6GsA= |
|||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.17.1/go.mod h1:yygr8ACQRY2PrEcy3xsUI357stq2AxnFM6DIsR9lij4= |
|||
github.com/aws/aws-sdk-go-v2/service/sts v1.22.0 h1:s4bioTgjSFRwOoyEFzAVCmFmoowBgjTR8gkrF/sQ4wk= |
|||
github.com/aws/aws-sdk-go-v2/service/sts v1.22.0/go.mod h1:VC7JDqsqiwXukYEDjoHh9U0fOJtNWh04FPQz4ct4GGU= |
|||
github.com/aws/smithy-go v1.14.2 h1:MJU9hqBGbvWZdApzpvoF2WAIJDbtjK2NDJSiJP7HblQ= |
|||
github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= |
|||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= |
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= |
|||
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= |
|||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= |
|||
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= |
|||
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= |
|||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88 h1:uC1QfSlInpQF+M0ao65imhwqKnz3Q2z/d8PWZRMQvDM= |
|||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= |
|||
github.com/k0kubun/pp v3.0.1+incompatible h1:3tqvf7QgUnZ5tXO6pNAZlrvHgl6DvifjDrd9g2S9Z40= |
|||
github.com/k0kubun/pp v3.0.1+incompatible/go.mod h1:GWse8YhT0p8pT4ir3ZgBbfZild3tgzSScAn6HmfYukg= |
|||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= |
|||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= |
|||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= |
|||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= |
|||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= |
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= |
|||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= |
|||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= |
|||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= |
|||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab h1:2QkjZIsXupsJbJIdSjjUOgWK3aEtzyuh2mPt3l/CkeU= |
|||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= |
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= |
|||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= |
|||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= |
|||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= |
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= |
@ -0,0 +1,469 @@ |
|||
package cors |
|||
|
|||
import ( |
|||
"context" |
|||
"fmt" |
|||
"net/http" |
|||
"strings" |
|||
"testing" |
|||
"time" |
|||
|
|||
"github.com/aws/aws-sdk-go-v2/aws" |
|||
"github.com/aws/aws-sdk-go-v2/service/s3" |
|||
"github.com/aws/aws-sdk-go-v2/service/s3/types" |
|||
"github.com/stretchr/testify/assert" |
|||
"github.com/stretchr/testify/require" |
|||
) |
|||
|
|||
// TestCORSPreflightRequest tests CORS preflight OPTIONS requests
|
|||
func TestCORSPreflightRequest(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
// Set up CORS configuration
|
|||
corsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"Content-Type", "Authorization"}, |
|||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, |
|||
AllowedOrigins: []string{"https://example.com"}, |
|||
ExposeHeaders: []string{"ETag", "Content-Length"}, |
|||
MaxAgeSeconds: 3600, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err := client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig, |
|||
}) |
|||
require.NoError(t, err, "Should be able to put CORS configuration") |
|||
|
|||
// Test preflight request with raw HTTP
|
|||
httpClient := &http.Client{Timeout: 10 * time.Second} |
|||
|
|||
// Create OPTIONS request
|
|||
req, err := http.NewRequest("OPTIONS", fmt.Sprintf("%s/%s/test-object", defaultConfig.Endpoint, bucketName), nil) |
|||
require.NoError(t, err, "Should be able to create OPTIONS request") |
|||
|
|||
// Add CORS preflight headers
|
|||
req.Header.Set("Origin", "https://example.com") |
|||
req.Header.Set("Access-Control-Request-Method", "PUT") |
|||
req.Header.Set("Access-Control-Request-Headers", "Content-Type, Authorization") |
|||
|
|||
// Send the request
|
|||
resp, err := httpClient.Do(req) |
|||
require.NoError(t, err, "Should be able to send OPTIONS request") |
|||
defer resp.Body.Close() |
|||
|
|||
// Verify CORS headers in response
|
|||
assert.Equal(t, "https://example.com", resp.Header.Get("Access-Control-Allow-Origin"), "Should have correct Allow-Origin header") |
|||
assert.Contains(t, resp.Header.Get("Access-Control-Allow-Methods"), "PUT", "Should allow PUT method") |
|||
assert.Contains(t, resp.Header.Get("Access-Control-Allow-Headers"), "Content-Type", "Should allow Content-Type header") |
|||
assert.Contains(t, resp.Header.Get("Access-Control-Allow-Headers"), "Authorization", "Should allow Authorization header") |
|||
assert.Equal(t, "3600", resp.Header.Get("Access-Control-Max-Age"), "Should have correct Max-Age header") |
|||
assert.Contains(t, resp.Header.Get("Access-Control-Expose-Headers"), "ETag", "Should expose ETag header") |
|||
assert.Equal(t, http.StatusOK, resp.StatusCode, "OPTIONS request should return 200") |
|||
} |
|||
|
|||
// TestCORSActualRequest tests CORS behavior with actual requests
|
|||
func TestCORSActualRequest(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
// Set up CORS configuration
|
|||
corsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET", "PUT"}, |
|||
AllowedOrigins: []string{"https://example.com"}, |
|||
ExposeHeaders: []string{"ETag", "Content-Length"}, |
|||
MaxAgeSeconds: 3600, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err := client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig, |
|||
}) |
|||
require.NoError(t, err, "Should be able to put CORS configuration") |
|||
|
|||
// First, put an object using S3 client
|
|||
objectKey := "test-cors-object" |
|||
_, err = client.PutObject(context.TODO(), &s3.PutObjectInput{ |
|||
Bucket: aws.String(bucketName), |
|||
Key: aws.String(objectKey), |
|||
Body: strings.NewReader("Test CORS content"), |
|||
}) |
|||
require.NoError(t, err, "Should be able to put object") |
|||
|
|||
// Test GET request with CORS headers using raw HTTP
|
|||
httpClient := &http.Client{Timeout: 10 * time.Second} |
|||
|
|||
req, err := http.NewRequest("GET", fmt.Sprintf("%s/%s/%s", defaultConfig.Endpoint, bucketName, objectKey), nil) |
|||
require.NoError(t, err, "Should be able to create GET request") |
|||
|
|||
// Add Origin header to simulate CORS request
|
|||
req.Header.Set("Origin", "https://example.com") |
|||
|
|||
// Send the request
|
|||
resp, err := httpClient.Do(req) |
|||
require.NoError(t, err, "Should be able to send GET request") |
|||
defer resp.Body.Close() |
|||
|
|||
// Verify CORS headers in response
|
|||
assert.Equal(t, "https://example.com", resp.Header.Get("Access-Control-Allow-Origin"), "Should have correct Allow-Origin header") |
|||
assert.Contains(t, resp.Header.Get("Access-Control-Expose-Headers"), "ETag", "Should expose ETag header") |
|||
assert.Equal(t, http.StatusOK, resp.StatusCode, "GET request should return 200") |
|||
} |
|||
|
|||
// TestCORSOriginMatching tests origin matching with different patterns
|
|||
func TestCORSOriginMatching(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
testCases := []struct { |
|||
name string |
|||
allowedOrigins []string |
|||
requestOrigin string |
|||
shouldAllow bool |
|||
}{ |
|||
{ |
|||
name: "exact match", |
|||
allowedOrigins: []string{"https://example.com"}, |
|||
requestOrigin: "https://example.com", |
|||
shouldAllow: true, |
|||
}, |
|||
{ |
|||
name: "wildcard match", |
|||
allowedOrigins: []string{"*"}, |
|||
requestOrigin: "https://example.com", |
|||
shouldAllow: true, |
|||
}, |
|||
{ |
|||
name: "subdomain wildcard match", |
|||
allowedOrigins: []string{"https://*.example.com"}, |
|||
requestOrigin: "https://api.example.com", |
|||
shouldAllow: true, |
|||
}, |
|||
{ |
|||
name: "no match", |
|||
allowedOrigins: []string{"https://example.com"}, |
|||
requestOrigin: "https://malicious.com", |
|||
shouldAllow: false, |
|||
}, |
|||
{ |
|||
name: "subdomain wildcard no match", |
|||
allowedOrigins: []string{"https://*.example.com"}, |
|||
requestOrigin: "https://example.com", |
|||
shouldAllow: false, |
|||
}, |
|||
} |
|||
|
|||
for _, tc := range testCases { |
|||
t.Run(tc.name, func(t *testing.T) { |
|||
// Set up CORS configuration for this test case
|
|||
corsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET"}, |
|||
AllowedOrigins: tc.allowedOrigins, |
|||
ExposeHeaders: []string{"ETag"}, |
|||
MaxAgeSeconds: 3600, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err := client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig, |
|||
}) |
|||
require.NoError(t, err, "Should be able to put CORS configuration") |
|||
|
|||
// Test preflight request
|
|||
httpClient := &http.Client{Timeout: 10 * time.Second} |
|||
|
|||
req, err := http.NewRequest("OPTIONS", fmt.Sprintf("%s/%s/test-object", defaultConfig.Endpoint, bucketName), nil) |
|||
require.NoError(t, err, "Should be able to create OPTIONS request") |
|||
|
|||
req.Header.Set("Origin", tc.requestOrigin) |
|||
req.Header.Set("Access-Control-Request-Method", "GET") |
|||
|
|||
resp, err := httpClient.Do(req) |
|||
require.NoError(t, err, "Should be able to send OPTIONS request") |
|||
defer resp.Body.Close() |
|||
|
|||
if tc.shouldAllow { |
|||
assert.Equal(t, tc.requestOrigin, resp.Header.Get("Access-Control-Allow-Origin"), "Should have correct Allow-Origin header") |
|||
assert.Contains(t, resp.Header.Get("Access-Control-Allow-Methods"), "GET", "Should allow GET method") |
|||
} else { |
|||
assert.Empty(t, resp.Header.Get("Access-Control-Allow-Origin"), "Should not have Allow-Origin header for disallowed origin") |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestCORSHeaderMatching tests header matching with different patterns
|
|||
func TestCORSHeaderMatching(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
testCases := []struct { |
|||
name string |
|||
allowedHeaders []string |
|||
requestHeaders string |
|||
shouldAllow bool |
|||
expectedHeaders string |
|||
}{ |
|||
{ |
|||
name: "wildcard headers", |
|||
allowedHeaders: []string{"*"}, |
|||
requestHeaders: "Content-Type, Authorization", |
|||
shouldAllow: true, |
|||
expectedHeaders: "Content-Type, Authorization", |
|||
}, |
|||
{ |
|||
name: "specific headers match", |
|||
allowedHeaders: []string{"Content-Type", "Authorization"}, |
|||
requestHeaders: "Content-Type, Authorization", |
|||
shouldAllow: true, |
|||
expectedHeaders: "Content-Type, Authorization", |
|||
}, |
|||
{ |
|||
name: "partial header match", |
|||
allowedHeaders: []string{"Content-Type"}, |
|||
requestHeaders: "Content-Type", |
|||
shouldAllow: true, |
|||
expectedHeaders: "Content-Type", |
|||
}, |
|||
{ |
|||
name: "case insensitive match", |
|||
allowedHeaders: []string{"content-type"}, |
|||
requestHeaders: "Content-Type", |
|||
shouldAllow: true, |
|||
expectedHeaders: "Content-Type", |
|||
}, |
|||
{ |
|||
name: "disallowed header", |
|||
allowedHeaders: []string{"Content-Type"}, |
|||
requestHeaders: "Authorization", |
|||
shouldAllow: false, |
|||
}, |
|||
} |
|||
|
|||
for _, tc := range testCases { |
|||
t.Run(tc.name, func(t *testing.T) { |
|||
// Set up CORS configuration for this test case
|
|||
corsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: tc.allowedHeaders, |
|||
AllowedMethods: []string{"GET", "POST"}, |
|||
AllowedOrigins: []string{"https://example.com"}, |
|||
ExposeHeaders: []string{"ETag"}, |
|||
MaxAgeSeconds: 3600, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err := client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig, |
|||
}) |
|||
require.NoError(t, err, "Should be able to put CORS configuration") |
|||
|
|||
// Test preflight request
|
|||
httpClient := &http.Client{Timeout: 10 * time.Second} |
|||
|
|||
req, err := http.NewRequest("OPTIONS", fmt.Sprintf("%s/%s/test-object", defaultConfig.Endpoint, bucketName), nil) |
|||
require.NoError(t, err, "Should be able to create OPTIONS request") |
|||
|
|||
req.Header.Set("Origin", "https://example.com") |
|||
req.Header.Set("Access-Control-Request-Method", "POST") |
|||
req.Header.Set("Access-Control-Request-Headers", tc.requestHeaders) |
|||
|
|||
resp, err := httpClient.Do(req) |
|||
require.NoError(t, err, "Should be able to send OPTIONS request") |
|||
defer resp.Body.Close() |
|||
|
|||
if tc.shouldAllow { |
|||
assert.Equal(t, "https://example.com", resp.Header.Get("Access-Control-Allow-Origin"), "Should have correct Allow-Origin header") |
|||
allowedHeaders := resp.Header.Get("Access-Control-Allow-Headers") |
|||
for _, header := range strings.Split(tc.expectedHeaders, ", ") { |
|||
assert.Contains(t, allowedHeaders, header, "Should allow header: %s", header) |
|||
} |
|||
} else { |
|||
// Even if headers are not allowed, the origin should still be in the response
|
|||
// but the headers should not be echoed back
|
|||
assert.Equal(t, "https://example.com", resp.Header.Get("Access-Control-Allow-Origin"), "Should have correct Allow-Origin header") |
|||
allowedHeaders := resp.Header.Get("Access-Control-Allow-Headers") |
|||
assert.NotContains(t, allowedHeaders, "Authorization", "Should not allow Authorization header") |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestCORSWithoutConfiguration tests CORS behavior when no configuration is set
|
|||
func TestCORSWithoutConfiguration(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
// Test preflight request without CORS configuration
|
|||
httpClient := &http.Client{Timeout: 10 * time.Second} |
|||
|
|||
req, err := http.NewRequest("OPTIONS", fmt.Sprintf("%s/%s/test-object", defaultConfig.Endpoint, bucketName), nil) |
|||
require.NoError(t, err, "Should be able to create OPTIONS request") |
|||
|
|||
req.Header.Set("Origin", "https://example.com") |
|||
req.Header.Set("Access-Control-Request-Method", "GET") |
|||
|
|||
resp, err := httpClient.Do(req) |
|||
require.NoError(t, err, "Should be able to send OPTIONS request") |
|||
defer resp.Body.Close() |
|||
|
|||
// Without CORS configuration, CORS headers should not be present
|
|||
assert.Empty(t, resp.Header.Get("Access-Control-Allow-Origin"), "Should not have Allow-Origin header without CORS config") |
|||
assert.Empty(t, resp.Header.Get("Access-Control-Allow-Methods"), "Should not have Allow-Methods header without CORS config") |
|||
assert.Empty(t, resp.Header.Get("Access-Control-Allow-Headers"), "Should not have Allow-Headers header without CORS config") |
|||
} |
|||
|
|||
// TestCORSMethodMatching tests method matching
|
|||
func TestCORSMethodMatching(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
// Set up CORS configuration with limited methods
|
|||
corsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET", "POST"}, |
|||
AllowedOrigins: []string{"https://example.com"}, |
|||
ExposeHeaders: []string{"ETag"}, |
|||
MaxAgeSeconds: 3600, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err := client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig, |
|||
}) |
|||
require.NoError(t, err, "Should be able to put CORS configuration") |
|||
|
|||
testCases := []struct { |
|||
method string |
|||
shouldAllow bool |
|||
}{ |
|||
{"GET", true}, |
|||
{"POST", true}, |
|||
{"PUT", false}, |
|||
{"DELETE", false}, |
|||
{"HEAD", false}, |
|||
} |
|||
|
|||
for _, tc := range testCases { |
|||
t.Run(fmt.Sprintf("method_%s", tc.method), func(t *testing.T) { |
|||
httpClient := &http.Client{Timeout: 10 * time.Second} |
|||
|
|||
req, err := http.NewRequest("OPTIONS", fmt.Sprintf("%s/%s/test-object", defaultConfig.Endpoint, bucketName), nil) |
|||
require.NoError(t, err, "Should be able to create OPTIONS request") |
|||
|
|||
req.Header.Set("Origin", "https://example.com") |
|||
req.Header.Set("Access-Control-Request-Method", tc.method) |
|||
|
|||
resp, err := httpClient.Do(req) |
|||
require.NoError(t, err, "Should be able to send OPTIONS request") |
|||
defer resp.Body.Close() |
|||
|
|||
if tc.shouldAllow { |
|||
assert.Equal(t, "https://example.com", resp.Header.Get("Access-Control-Allow-Origin"), "Should have correct Allow-Origin header") |
|||
assert.Contains(t, resp.Header.Get("Access-Control-Allow-Methods"), tc.method, "Should allow method: %s", tc.method) |
|||
} else { |
|||
// Even if method is not allowed, the origin should still be in the response
|
|||
// but the method should not be in the allowed methods
|
|||
assert.Equal(t, "https://example.com", resp.Header.Get("Access-Control-Allow-Origin"), "Should have correct Allow-Origin header") |
|||
allowedMethods := resp.Header.Get("Access-Control-Allow-Methods") |
|||
assert.NotContains(t, allowedMethods, tc.method, "Should not allow method: %s", tc.method) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// TestCORSMultipleRulesMatching tests CORS with multiple rules
|
|||
func TestCORSMultipleRulesMatching(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
// Set up CORS configuration with multiple rules
|
|||
corsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"Content-Type"}, |
|||
AllowedMethods: []string{"GET"}, |
|||
AllowedOrigins: []string{"https://example.com"}, |
|||
ExposeHeaders: []string{"ETag"}, |
|||
MaxAgeSeconds: 3600, |
|||
}, |
|||
{ |
|||
AllowedHeaders: []string{"Authorization"}, |
|||
AllowedMethods: []string{"POST", "PUT"}, |
|||
AllowedOrigins: []string{"https://api.example.com"}, |
|||
ExposeHeaders: []string{"Content-Length"}, |
|||
MaxAgeSeconds: 7200, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err := client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig, |
|||
}) |
|||
require.NoError(t, err, "Should be able to put CORS configuration") |
|||
|
|||
// Test first rule
|
|||
httpClient := &http.Client{Timeout: 10 * time.Second} |
|||
|
|||
req, err := http.NewRequest("OPTIONS", fmt.Sprintf("%s/%s/test-object", defaultConfig.Endpoint, bucketName), nil) |
|||
require.NoError(t, err, "Should be able to create OPTIONS request") |
|||
|
|||
req.Header.Set("Origin", "https://example.com") |
|||
req.Header.Set("Access-Control-Request-Method", "GET") |
|||
req.Header.Set("Access-Control-Request-Headers", "Content-Type") |
|||
|
|||
resp, err := httpClient.Do(req) |
|||
require.NoError(t, err, "Should be able to send OPTIONS request") |
|||
defer resp.Body.Close() |
|||
|
|||
assert.Equal(t, "https://example.com", resp.Header.Get("Access-Control-Allow-Origin"), "Should match first rule") |
|||
assert.Contains(t, resp.Header.Get("Access-Control-Allow-Methods"), "GET", "Should allow GET method") |
|||
assert.Contains(t, resp.Header.Get("Access-Control-Allow-Headers"), "Content-Type", "Should allow Content-Type header") |
|||
assert.Equal(t, "3600", resp.Header.Get("Access-Control-Max-Age"), "Should have first rule's max age") |
|||
|
|||
// Test second rule
|
|||
req2, err := http.NewRequest("OPTIONS", fmt.Sprintf("%s/%s/test-object", defaultConfig.Endpoint, bucketName), nil) |
|||
require.NoError(t, err, "Should be able to create OPTIONS request") |
|||
|
|||
req2.Header.Set("Origin", "https://api.example.com") |
|||
req2.Header.Set("Access-Control-Request-Method", "POST") |
|||
req2.Header.Set("Access-Control-Request-Headers", "Authorization") |
|||
|
|||
resp2, err := httpClient.Do(req2) |
|||
require.NoError(t, err, "Should be able to send OPTIONS request") |
|||
defer resp2.Body.Close() |
|||
|
|||
assert.Equal(t, "https://api.example.com", resp2.Header.Get("Access-Control-Allow-Origin"), "Should match second rule") |
|||
assert.Contains(t, resp2.Header.Get("Access-Control-Allow-Methods"), "POST", "Should allow POST method") |
|||
assert.Contains(t, resp2.Header.Get("Access-Control-Allow-Headers"), "Authorization", "Should allow Authorization header") |
|||
assert.Equal(t, "7200", resp2.Header.Get("Access-Control-Max-Age"), "Should have second rule's max age") |
|||
} |
@ -0,0 +1,595 @@ |
|||
package cors |
|||
|
|||
import ( |
|||
"context" |
|||
"fmt" |
|||
"strings" |
|||
"testing" |
|||
"time" |
|||
|
|||
"github.com/aws/aws-sdk-go-v2/aws" |
|||
"github.com/aws/aws-sdk-go-v2/config" |
|||
"github.com/aws/aws-sdk-go-v2/credentials" |
|||
"github.com/aws/aws-sdk-go-v2/service/s3" |
|||
"github.com/aws/aws-sdk-go-v2/service/s3/types" |
|||
"github.com/k0kubun/pp" |
|||
"github.com/stretchr/testify/assert" |
|||
"github.com/stretchr/testify/require" |
|||
) |
|||
|
|||
// S3TestConfig holds configuration for S3 tests
|
|||
type S3TestConfig struct { |
|||
Endpoint string |
|||
AccessKey string |
|||
SecretKey string |
|||
Region string |
|||
BucketPrefix string |
|||
UseSSL bool |
|||
SkipVerifySSL bool |
|||
} |
|||
|
|||
// Default test configuration - should match test_config.json
|
|||
var defaultConfig = &S3TestConfig{ |
|||
Endpoint: "http://localhost:8333", // Default SeaweedFS S3 port
|
|||
AccessKey: "some_access_key1", |
|||
SecretKey: "some_secret_key1", |
|||
Region: "us-east-1", |
|||
BucketPrefix: "test-cors-", |
|||
UseSSL: false, |
|||
SkipVerifySSL: true, |
|||
} |
|||
|
|||
// getS3Client creates an AWS S3 client for testing
|
|||
func getS3Client(t *testing.T) *s3.Client { |
|||
cfg, err := config.LoadDefaultConfig(context.TODO(), |
|||
config.WithRegion(defaultConfig.Region), |
|||
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( |
|||
defaultConfig.AccessKey, |
|||
defaultConfig.SecretKey, |
|||
"", |
|||
)), |
|||
config.WithEndpointResolverWithOptions(aws.EndpointResolverWithOptionsFunc( |
|||
func(service, region string, options ...interface{}) (aws.Endpoint, error) { |
|||
return aws.Endpoint{ |
|||
URL: defaultConfig.Endpoint, |
|||
SigningRegion: defaultConfig.Region, |
|||
}, nil |
|||
})), |
|||
) |
|||
require.NoError(t, err) |
|||
|
|||
client := s3.NewFromConfig(cfg, func(o *s3.Options) { |
|||
o.UsePathStyle = true |
|||
}) |
|||
return client |
|||
} |
|||
|
|||
// createTestBucket creates a test bucket with a unique name
|
|||
func createTestBucket(t *testing.T, client *s3.Client) string { |
|||
bucketName := fmt.Sprintf("%s%d", defaultConfig.BucketPrefix, time.Now().UnixNano()) |
|||
|
|||
_, err := client.CreateBucket(context.TODO(), &s3.CreateBucketInput{ |
|||
Bucket: aws.String(bucketName), |
|||
}) |
|||
require.NoError(t, err) |
|||
|
|||
return bucketName |
|||
} |
|||
|
|||
// cleanupTestBucket removes the test bucket and all its contents
|
|||
func cleanupTestBucket(t *testing.T, client *s3.Client, bucketName string) { |
|||
// First, delete all objects in the bucket
|
|||
listResp, err := client.ListObjectsV2(context.TODO(), &s3.ListObjectsV2Input{ |
|||
Bucket: aws.String(bucketName), |
|||
}) |
|||
if err == nil { |
|||
for _, obj := range listResp.Contents { |
|||
_, err := client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ |
|||
Bucket: aws.String(bucketName), |
|||
Key: obj.Key, |
|||
}) |
|||
if err != nil { |
|||
t.Logf("Warning: failed to delete object %s: %v", *obj.Key, err) |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Then delete the bucket
|
|||
_, err = client.DeleteBucket(context.TODO(), &s3.DeleteBucketInput{ |
|||
Bucket: aws.String(bucketName), |
|||
}) |
|||
if err != nil { |
|||
t.Logf("Warning: failed to delete bucket %s: %v", bucketName, err) |
|||
} |
|||
} |
|||
|
|||
// TestCORSConfigurationManagement tests basic CORS configuration CRUD operations
|
|||
func TestCORSConfigurationManagement(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
// Test 1: Get CORS configuration when none exists (should return error)
|
|||
_, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
}) |
|||
assert.Error(t, err, "Should get error when no CORS configuration exists") |
|||
|
|||
// Test 2: Put CORS configuration
|
|||
corsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET", "POST", "PUT"}, |
|||
AllowedOrigins: []string{"https://example.com"}, |
|||
ExposeHeaders: []string{"ETag"}, |
|||
MaxAgeSeconds: 3600, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err = client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig, |
|||
}) |
|||
assert.NoError(t, err, "Should be able to put CORS configuration") |
|||
|
|||
// Test 3: Get CORS configuration
|
|||
getResp, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
}) |
|||
assert.NoError(t, err, "Should be able to get CORS configuration") |
|||
assert.NotNil(t, getResp.CORSRules, "CORS configuration should not be nil") |
|||
assert.Len(t, getResp.CORSRules, 1, "Should have one CORS rule") |
|||
|
|||
rule := getResp.CORSRules[0] |
|||
assert.Equal(t, []string{"*"}, rule.AllowedHeaders, "Allowed headers should match") |
|||
assert.Equal(t, []string{"GET", "POST", "PUT"}, rule.AllowedMethods, "Allowed methods should match") |
|||
assert.Equal(t, []string{"https://example.com"}, rule.AllowedOrigins, "Allowed origins should match") |
|||
assert.Equal(t, []string{"ETag"}, rule.ExposeHeaders, "Expose headers should match") |
|||
assert.Equal(t, int32(3600), rule.MaxAgeSeconds, "Max age should match") |
|||
|
|||
// Test 4: Update CORS configuration
|
|||
updatedCorsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"Content-Type"}, |
|||
AllowedMethods: []string{"GET", "POST"}, |
|||
AllowedOrigins: []string{"https://example.com", "https://another.com"}, |
|||
ExposeHeaders: []string{"ETag", "Content-Length"}, |
|||
MaxAgeSeconds: 7200, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err = client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: updatedCorsConfig, |
|||
}) |
|||
assert.NoError(t, err, "Should be able to update CORS configuration") |
|||
|
|||
// Verify the update
|
|||
getResp, err = client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
}) |
|||
assert.NoError(t, err, "Should be able to get updated CORS configuration") |
|||
rule = getResp.CORSRules[0] |
|||
assert.Equal(t, []string{"Content-Type"}, rule.AllowedHeaders, "Updated allowed headers should match") |
|||
assert.Equal(t, []string{"https://example.com", "https://another.com"}, rule.AllowedOrigins, "Updated allowed origins should match") |
|||
|
|||
// Test 5: Delete CORS configuration
|
|||
_, err = client.DeleteBucketCors(context.TODO(), &s3.DeleteBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
}) |
|||
assert.NoError(t, err, "Should be able to delete CORS configuration") |
|||
|
|||
// Verify deletion
|
|||
_, err = client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
}) |
|||
assert.Error(t, err, "Should get error after deleting CORS configuration") |
|||
} |
|||
|
|||
// TestCORSMultipleRules tests CORS configuration with multiple rules
|
|||
func TestCORSMultipleRules(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
// Create CORS configuration with multiple rules
|
|||
corsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET", "HEAD"}, |
|||
AllowedOrigins: []string{"https://example.com"}, |
|||
ExposeHeaders: []string{"ETag"}, |
|||
MaxAgeSeconds: 3600, |
|||
}, |
|||
{ |
|||
AllowedHeaders: []string{"Content-Type", "Authorization"}, |
|||
AllowedMethods: []string{"POST", "PUT", "DELETE"}, |
|||
AllowedOrigins: []string{"https://app.example.com"}, |
|||
ExposeHeaders: []string{"ETag", "Content-Length"}, |
|||
MaxAgeSeconds: 7200, |
|||
}, |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET"}, |
|||
AllowedOrigins: []string{"*"}, |
|||
ExposeHeaders: []string{"ETag"}, |
|||
MaxAgeSeconds: 1800, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err := client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig, |
|||
}) |
|||
assert.NoError(t, err, "Should be able to put CORS configuration with multiple rules") |
|||
|
|||
// Get and verify the configuration
|
|||
getResp, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
}) |
|||
assert.NoError(t, err, "Should be able to get CORS configuration") |
|||
assert.Len(t, getResp.CORSRules, 3, "Should have three CORS rules") |
|||
|
|||
// Verify first rule
|
|||
rule1 := getResp.CORSRules[0] |
|||
assert.Equal(t, []string{"*"}, rule1.AllowedHeaders) |
|||
assert.Equal(t, []string{"GET", "HEAD"}, rule1.AllowedMethods) |
|||
assert.Equal(t, []string{"https://example.com"}, rule1.AllowedOrigins) |
|||
|
|||
// Verify second rule
|
|||
rule2 := getResp.CORSRules[1] |
|||
assert.Equal(t, []string{"Content-Type", "Authorization"}, rule2.AllowedHeaders) |
|||
assert.Equal(t, []string{"POST", "PUT", "DELETE"}, rule2.AllowedMethods) |
|||
assert.Equal(t, []string{"https://app.example.com"}, rule2.AllowedOrigins) |
|||
|
|||
// Verify third rule
|
|||
rule3 := getResp.CORSRules[2] |
|||
assert.Equal(t, []string{"*"}, rule3.AllowedHeaders) |
|||
assert.Equal(t, []string{"GET"}, rule3.AllowedMethods) |
|||
assert.Equal(t, []string{"*"}, rule3.AllowedOrigins) |
|||
} |
|||
|
|||
// TestCORSValidation tests CORS configuration validation
|
|||
func TestCORSValidation(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
// Test invalid HTTP method
|
|||
invalidMethodConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"INVALID_METHOD"}, |
|||
AllowedOrigins: []string{"https://example.com"}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err := client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: invalidMethodConfig, |
|||
}) |
|||
assert.Error(t, err, "Should get error for invalid HTTP method") |
|||
|
|||
// Test empty origins
|
|||
emptyOriginsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET"}, |
|||
AllowedOrigins: []string{}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err = client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: emptyOriginsConfig, |
|||
}) |
|||
assert.Error(t, err, "Should get error for empty origins") |
|||
|
|||
// Test negative MaxAge
|
|||
negativeMaxAgeConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET"}, |
|||
AllowedOrigins: []string{"https://example.com"}, |
|||
MaxAgeSeconds: -1, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err = client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: negativeMaxAgeConfig, |
|||
}) |
|||
assert.Error(t, err, "Should get error for negative MaxAge") |
|||
} |
|||
|
|||
// TestCORSWithWildcards tests CORS configuration with wildcard patterns
|
|||
func TestCORSWithWildcards(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
// Create CORS configuration with wildcard patterns
|
|||
corsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET", "POST"}, |
|||
AllowedOrigins: []string{"https://*.example.com"}, |
|||
ExposeHeaders: []string{"*"}, |
|||
MaxAgeSeconds: 3600, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err := client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig, |
|||
}) |
|||
assert.NoError(t, err, "Should be able to put CORS configuration with wildcards") |
|||
|
|||
// Get and verify the configuration
|
|||
getResp, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
}) |
|||
assert.NoError(t, err, "Should be able to get CORS configuration") |
|||
assert.Len(t, getResp.CORSRules, 1, "Should have one CORS rule") |
|||
|
|||
rule := getResp.CORSRules[0] |
|||
assert.Equal(t, []string{"*"}, rule.AllowedHeaders, "Wildcard headers should be preserved") |
|||
assert.Equal(t, []string{"https://*.example.com"}, rule.AllowedOrigins, "Wildcard origins should be preserved") |
|||
assert.Equal(t, []string{"*"}, rule.ExposeHeaders, "Wildcard expose headers should be preserved") |
|||
} |
|||
|
|||
// TestCORSRuleLimit tests the maximum number of CORS rules
|
|||
func TestCORSRuleLimit(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
// Create CORS configuration with maximum allowed rules (100)
|
|||
rules := make([]types.CORSRule, 100) |
|||
for i := 0; i < 100; i++ { |
|||
rules[i] = types.CORSRule{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET"}, |
|||
AllowedOrigins: []string{fmt.Sprintf("https://example%d.com", i)}, |
|||
MaxAgeSeconds: 3600, |
|||
} |
|||
} |
|||
|
|||
corsConfig := &types.CORSConfiguration{ |
|||
CORSRules: rules, |
|||
} |
|||
|
|||
_, err := client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig, |
|||
}) |
|||
assert.NoError(t, err, "Should be able to put CORS configuration with 100 rules") |
|||
|
|||
// Try to add one more rule (should fail)
|
|||
rules = append(rules, types.CORSRule{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET"}, |
|||
AllowedOrigins: []string{"https://example101.com"}, |
|||
MaxAgeSeconds: 3600, |
|||
}) |
|||
|
|||
corsConfig.CORSRules = rules |
|||
|
|||
_, err = client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig, |
|||
}) |
|||
assert.Error(t, err, "Should get error when exceeding maximum number of rules") |
|||
} |
|||
|
|||
// TestCORSNonExistentBucket tests CORS operations on non-existent bucket
|
|||
func TestCORSNonExistentBucket(t *testing.T) { |
|||
client := getS3Client(t) |
|||
nonExistentBucket := "non-existent-bucket-cors-test" |
|||
|
|||
// Test Get CORS on non-existent bucket
|
|||
_, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{ |
|||
Bucket: aws.String(nonExistentBucket), |
|||
}) |
|||
assert.Error(t, err, "Should get error for non-existent bucket") |
|||
|
|||
// Test Put CORS on non-existent bucket
|
|||
corsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET"}, |
|||
AllowedOrigins: []string{"https://example.com"}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err = client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(nonExistentBucket), |
|||
CORSConfiguration: corsConfig, |
|||
}) |
|||
assert.Error(t, err, "Should get error for non-existent bucket") |
|||
|
|||
// Test Delete CORS on non-existent bucket
|
|||
_, err = client.DeleteBucketCors(context.TODO(), &s3.DeleteBucketCorsInput{ |
|||
Bucket: aws.String(nonExistentBucket), |
|||
}) |
|||
assert.Error(t, err, "Should get error for non-existent bucket") |
|||
} |
|||
|
|||
// TestCORSObjectOperations tests CORS behavior with object operations
|
|||
func TestCORSObjectOperations(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
// Set up CORS configuration
|
|||
corsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"}, |
|||
AllowedOrigins: []string{"https://example.com"}, |
|||
ExposeHeaders: []string{"ETag", "Content-Length"}, |
|||
MaxAgeSeconds: 3600, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err := client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig, |
|||
}) |
|||
assert.NoError(t, err, "Should be able to put CORS configuration") |
|||
|
|||
// Test putting an object (this should work normally)
|
|||
objectKey := "test-object.txt" |
|||
objectContent := "Hello, CORS World!" |
|||
|
|||
_, err = client.PutObject(context.TODO(), &s3.PutObjectInput{ |
|||
Bucket: aws.String(bucketName), |
|||
Key: aws.String(objectKey), |
|||
Body: strings.NewReader(objectContent), |
|||
}) |
|||
assert.NoError(t, err, "Should be able to put object in CORS-enabled bucket") |
|||
|
|||
// Test getting the object
|
|||
getResp, err := client.GetObject(context.TODO(), &s3.GetObjectInput{ |
|||
Bucket: aws.String(bucketName), |
|||
Key: aws.String(objectKey), |
|||
}) |
|||
assert.NoError(t, err, "Should be able to get object from CORS-enabled bucket") |
|||
assert.NotNil(t, getResp.Body, "Object body should not be nil") |
|||
|
|||
// Test deleting the object
|
|||
_, err = client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{ |
|||
Bucket: aws.String(bucketName), |
|||
Key: aws.String(objectKey), |
|||
}) |
|||
assert.NoError(t, err, "Should be able to delete object from CORS-enabled bucket") |
|||
} |
|||
|
|||
// TestCORSCaching tests CORS configuration caching behavior
|
|||
func TestCORSCaching(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
// Set up initial CORS configuration
|
|||
corsConfig1 := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{"GET"}, |
|||
AllowedOrigins: []string{"https://example.com"}, |
|||
MaxAgeSeconds: 3600, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err := client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig1, |
|||
}) |
|||
assert.NoError(t, err, "Should be able to put initial CORS configuration") |
|||
|
|||
// Get the configuration
|
|||
getResp1, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
}) |
|||
assert.NoError(t, err, "Should be able to get initial CORS configuration") |
|||
assert.Len(t, getResp1.CORSRules, 1, "Should have one CORS rule") |
|||
|
|||
// Update the configuration
|
|||
corsConfig2 := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"Content-Type"}, |
|||
AllowedMethods: []string{"GET", "POST"}, |
|||
AllowedOrigins: []string{"https://example.com", "https://another.com"}, |
|||
MaxAgeSeconds: 7200, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err = client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: corsConfig2, |
|||
}) |
|||
assert.NoError(t, err, "Should be able to update CORS configuration") |
|||
|
|||
// Get the updated configuration (should reflect the changes)
|
|||
getResp2, err := client.GetBucketCors(context.TODO(), &s3.GetBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
}) |
|||
assert.NoError(t, err, "Should be able to get updated CORS configuration") |
|||
assert.Len(t, getResp2.CORSRules, 1, "Should have one CORS rule") |
|||
|
|||
rule := getResp2.CORSRules[0] |
|||
assert.Equal(t, []string{"Content-Type"}, rule.AllowedHeaders, "Should have updated headers") |
|||
assert.Equal(t, []string{"GET", "POST"}, rule.AllowedMethods, "Should have updated methods") |
|||
assert.Equal(t, []string{"https://example.com", "https://another.com"}, rule.AllowedOrigins, "Should have updated origins") |
|||
assert.Equal(t, int32(7200), rule.MaxAgeSeconds, "Should have updated max age") |
|||
} |
|||
|
|||
// TestCORSErrorHandling tests various error conditions
|
|||
func TestCORSErrorHandling(t *testing.T) { |
|||
client := getS3Client(t) |
|||
bucketName := createTestBucket(t, client) |
|||
defer cleanupTestBucket(t, client, bucketName) |
|||
|
|||
// Test empty CORS configuration
|
|||
emptyCorsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{}, |
|||
} |
|||
|
|||
_, err := client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: emptyCorsConfig, |
|||
}) |
|||
assert.Error(t, err, "Should get error for empty CORS configuration") |
|||
|
|||
// Test nil CORS configuration
|
|||
_, err = client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: nil, |
|||
}) |
|||
assert.Error(t, err, "Should get error for nil CORS configuration") |
|||
|
|||
// Test CORS rule with empty methods
|
|||
emptyMethodsConfig := &types.CORSConfiguration{ |
|||
CORSRules: []types.CORSRule{ |
|||
{ |
|||
AllowedHeaders: []string{"*"}, |
|||
AllowedMethods: []string{}, |
|||
AllowedOrigins: []string{"https://example.com"}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
_, err = client.PutBucketCors(context.TODO(), &s3.PutBucketCorsInput{ |
|||
Bucket: aws.String(bucketName), |
|||
CORSConfiguration: emptyMethodsConfig, |
|||
}) |
|||
assert.Error(t, err, "Should get error for empty methods") |
|||
} |
|||
|
|||
// Debugging helper to pretty print responses
|
|||
func debugResponse(t *testing.T, title string, response interface{}) { |
|||
t.Logf("=== %s ===", title) |
|||
pp.Println(response) |
|||
} |
@ -0,0 +1,9 @@ |
|||
{ |
|||
"endpoint": "http://localhost:8333", |
|||
"access_key": "some_access_key1", |
|||
"secret_key": "some_secret_key1", |
|||
"region": "us-east-1", |
|||
"bucket_prefix": "test-cors-", |
|||
"use_ssl": false, |
|||
"skip_verify_ssl": true |
|||
} |
@ -0,0 +1,516 @@ |
|||
package cors |
|||
|
|||
import ( |
|||
"context" |
|||
"encoding/json" |
|||
"encoding/xml" |
|||
"fmt" |
|||
"net/http" |
|||
"regexp" |
|||
"strconv" |
|||
"strings" |
|||
"time" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
|||
) |
|||
|
|||
// CORSRule represents a single CORS rule
|
|||
type CORSRule struct { |
|||
ID string `xml:"ID,omitempty" json:"ID,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"` |
|||
} |
|||
|
|||
// CORSConfiguration represents the CORS configuration for a bucket
|
|||
type CORSConfiguration struct { |
|||
XMLName xml.Name `xml:"CORSConfiguration"` |
|||
CORSRules []CORSRule `xml:"CORSRule" json:"CORSRules"` |
|||
} |
|||
|
|||
// CORSRequest represents a CORS request
|
|||
type CORSRequest struct { |
|||
Origin string |
|||
Method string |
|||
RequestHeaders []string |
|||
IsPreflightRequest bool |
|||
AccessControlRequestMethod string |
|||
AccessControlRequestHeaders []string |
|||
} |
|||
|
|||
// CORSResponse represents CORS response headers
|
|||
type CORSResponse struct { |
|||
AllowOrigin string |
|||
AllowMethods string |
|||
AllowHeaders string |
|||
ExposeHeaders string |
|||
MaxAge string |
|||
AllowCredentials bool |
|||
} |
|||
|
|||
// ValidateConfiguration validates a CORS configuration
|
|||
func ValidateConfiguration(config *CORSConfiguration) error { |
|||
if config == nil { |
|||
return fmt.Errorf("CORS configuration cannot be nil") |
|||
} |
|||
|
|||
if len(config.CORSRules) == 0 { |
|||
return fmt.Errorf("CORS configuration must have at least one rule") |
|||
} |
|||
|
|||
if len(config.CORSRules) > 100 { |
|||
return fmt.Errorf("CORS configuration cannot have more than 100 rules") |
|||
} |
|||
|
|||
for i, rule := range config.CORSRules { |
|||
if err := validateRule(&rule); err != nil { |
|||
return fmt.Errorf("invalid CORS rule at index %d: %v", i, err) |
|||
} |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// validateRule validates a single CORS rule
|
|||
func validateRule(rule *CORSRule) error { |
|||
if len(rule.AllowedMethods) == 0 { |
|||
return fmt.Errorf("AllowedMethods cannot be empty") |
|||
} |
|||
|
|||
if len(rule.AllowedOrigins) == 0 { |
|||
return fmt.Errorf("AllowedOrigins cannot be empty") |
|||
} |
|||
|
|||
// Validate allowed methods
|
|||
validMethods := map[string]bool{ |
|||
"GET": true, |
|||
"PUT": true, |
|||
"POST": true, |
|||
"DELETE": true, |
|||
"HEAD": true, |
|||
} |
|||
|
|||
for _, method := range rule.AllowedMethods { |
|||
if !validMethods[method] { |
|||
return fmt.Errorf("invalid HTTP method: %s", method) |
|||
} |
|||
} |
|||
|
|||
// Validate origins
|
|||
for _, origin := range rule.AllowedOrigins { |
|||
if origin == "*" { |
|||
continue |
|||
} |
|||
if err := validateOrigin(origin); err != nil { |
|||
return fmt.Errorf("invalid origin %s: %v", origin, err) |
|||
} |
|||
} |
|||
|
|||
// Validate MaxAgeSeconds
|
|||
if rule.MaxAgeSeconds != nil && *rule.MaxAgeSeconds < 0 { |
|||
return fmt.Errorf("MaxAgeSeconds cannot be negative") |
|||
} |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// validateOrigin validates an origin string
|
|||
func validateOrigin(origin string) error { |
|||
if origin == "" { |
|||
return fmt.Errorf("origin cannot be empty") |
|||
} |
|||
|
|||
// Special case: "*" is always valid
|
|||
if origin == "*" { |
|||
return nil |
|||
} |
|||
|
|||
// Count wildcards
|
|||
wildcardCount := strings.Count(origin, "*") |
|||
if wildcardCount > 1 { |
|||
return fmt.Errorf("origin can contain at most one wildcard") |
|||
} |
|||
|
|||
// If there's a wildcard, it should be in a valid position
|
|||
if wildcardCount == 1 { |
|||
// Must be in the format: http://*.example.com or https://*.example.com
|
|||
if !strings.HasPrefix(origin, "http://") && !strings.HasPrefix(origin, "https://") { |
|||
return fmt.Errorf("origin with wildcard must start with http:// or https://") |
|||
} |
|||
} |
|||
|
|||
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 { |
|||
return nil, fmt.Errorf("config and corsReq cannot be nil") |
|||
} |
|||
|
|||
if corsReq.Origin == "" { |
|||
return nil, fmt.Errorf("origin header is required for CORS requests") |
|||
} |
|||
|
|||
// Find the first matching rule
|
|||
for _, rule := range config.CORSRules { |
|||
if matchesRule(&rule, corsReq) { |
|||
return buildResponse(&rule, corsReq), nil |
|||
} |
|||
} |
|||
|
|||
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 only check origin matching
|
|||
// The response building will handle filtering out disallowed methods and headers
|
|||
// This allows partial CORS responses to be sent
|
|||
if corsReq.IsPreflightRequest { |
|||
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
|
|||
func matchesWildcard(pattern, origin string) bool { |
|||
// Convert wildcard pattern to regex
|
|||
escapedPattern := regexp.QuoteMeta(pattern) |
|||
regexPattern := strings.Replace(escapedPattern, "\\*", ".*", -1) |
|||
regex, err := regexp.Compile("^" + regexPattern + "$") |
|||
if err != nil { |
|||
return false |
|||
} |
|||
return regex.MatchString(origin) |
|||
} |
|||
|
|||
// 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 |
|||
} |
|||
|
|||
// buildResponse builds a CORS response from a matching rule
|
|||
func buildResponse(rule *CORSRule, corsReq *CORSRequest) *CORSResponse { |
|||
response := &CORSResponse{ |
|||
AllowOrigin: corsReq.Origin, |
|||
} |
|||
|
|||
// Set allowed methods - for preflight requests, check if the requested method is allowed
|
|||
if corsReq.IsPreflightRequest && corsReq.AccessControlRequestMethod != "" { |
|||
if contains(rule.AllowedMethods, corsReq.AccessControlRequestMethod) { |
|||
response.AllowMethods = corsReq.AccessControlRequestMethod |
|||
} else { |
|||
// If the requested method is not allowed, return all allowed methods
|
|||
response.AllowMethods = strings.Join(rule.AllowedMethods, ", ") |
|||
} |
|||
} else { |
|||
// For non-preflight requests, return all allowed methods
|
|||
response.AllowMethods = strings.Join(rule.AllowedMethods, ", ") |
|||
} |
|||
|
|||
// Set allowed headers - for preflight requests, return the specific headers that were requested and are allowed
|
|||
if corsReq.IsPreflightRequest && len(corsReq.AccessControlRequestHeaders) > 0 { |
|||
allowedHeaders := make([]string, 0) |
|||
for _, requestedHeader := range corsReq.AccessControlRequestHeaders { |
|||
if matchesHeader(rule.AllowedHeaders, requestedHeader) { |
|||
allowedHeaders = append(allowedHeaders, requestedHeader) |
|||
} |
|||
} |
|||
if len(allowedHeaders) > 0 { |
|||
response.AllowHeaders = strings.Join(allowedHeaders, ", ") |
|||
} |
|||
} else if len(rule.AllowedHeaders) > 0 { |
|||
// For non-preflight requests, return the allowed headers from the rule
|
|||
response.AllowHeaders = strings.Join(rule.AllowedHeaders, ", ") |
|||
} |
|||
|
|||
// Set exposed headers
|
|||
if len(rule.ExposeHeaders) > 0 { |
|||
response.ExposeHeaders = strings.Join(rule.ExposeHeaders, ", ") |
|||
} |
|||
|
|||
// Set max age
|
|||
if rule.MaxAgeSeconds != nil { |
|||
response.MaxAge = strconv.Itoa(*rule.MaxAgeSeconds) |
|||
} |
|||
|
|||
return response |
|||
} |
|||
|
|||
// contains checks if a slice contains a string
|
|||
func contains(slice []string, item string) bool { |
|||
for _, s := range slice { |
|||
if s == item { |
|||
return true |
|||
} |
|||
} |
|||
return false |
|||
} |
|||
|
|||
// ApplyHeaders applies CORS headers to an HTTP response
|
|||
func ApplyHeaders(w http.ResponseWriter, corsResp *CORSResponse) { |
|||
if corsResp == nil { |
|||
return |
|||
} |
|||
|
|||
if corsResp.AllowOrigin != "" { |
|||
w.Header().Set("Access-Control-Allow-Origin", corsResp.AllowOrigin) |
|||
} |
|||
|
|||
if corsResp.AllowMethods != "" { |
|||
w.Header().Set("Access-Control-Allow-Methods", corsResp.AllowMethods) |
|||
} |
|||
|
|||
if corsResp.AllowHeaders != "" { |
|||
w.Header().Set("Access-Control-Allow-Headers", corsResp.AllowHeaders) |
|||
} |
|||
|
|||
if corsResp.ExposeHeaders != "" { |
|||
w.Header().Set("Access-Control-Expose-Headers", corsResp.ExposeHeaders) |
|||
} |
|||
|
|||
if corsResp.MaxAge != "" { |
|||
w.Header().Set("Access-Control-Max-Age", corsResp.MaxAge) |
|||
} |
|||
|
|||
if corsResp.AllowCredentials { |
|||
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 := fmt.Sprintf("%s/%s/.s3metadata", s.bucketsPath, bucket) |
|||
|
|||
// 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: %v", 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: ".s3metadata", |
|||
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 := fmt.Sprintf("%s/%s/.s3metadata", s.bucketsPath, bucket) |
|||
|
|||
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: %v", 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: %v", err) |
|||
} |
|||
|
|||
var config CORSConfiguration |
|||
if err := json.Unmarshal(corsBytes, &config); err != nil { |
|||
return nil, fmt.Errorf("failed to unmarshal CORS configuration: %v", err) |
|||
} |
|||
|
|||
return &config, nil |
|||
} |
|||
|
|||
// Delete deletes CORS configuration from the filer
|
|||
func (s *Storage) Delete(bucket string) error { |
|||
bucketMetadataPath := fmt.Sprintf("%s/%s/.s3metadata", s.bucketsPath, bucket) |
|||
|
|||
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: %v", 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: %v", 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: ".s3metadata", |
|||
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 |
|||
}) |
|||
} |
@ -0,0 +1,523 @@ |
|||
package cors |
|||
|
|||
import ( |
|||
"net/http" |
|||
"net/http/httptest" |
|||
"reflect" |
|||
"testing" |
|||
) |
|||
|
|||
func TestValidateConfiguration(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
config *CORSConfiguration |
|||
wantErr bool |
|||
}{ |
|||
{ |
|||
name: "nil config", |
|||
config: nil, |
|||
wantErr: true, |
|||
}, |
|||
{ |
|||
name: "empty rules", |
|||
config: &CORSConfiguration{ |
|||
CORSRules: []CORSRule{}, |
|||
}, |
|||
wantErr: true, |
|||
}, |
|||
{ |
|||
name: "valid single rule", |
|||
config: &CORSConfiguration{ |
|||
CORSRules: []CORSRule{ |
|||
{ |
|||
AllowedMethods: []string{"GET", "POST"}, |
|||
AllowedOrigins: []string{"*"}, |
|||
}, |
|||
}, |
|||
}, |
|||
wantErr: false, |
|||
}, |
|||
{ |
|||
name: "too many rules", |
|||
config: &CORSConfiguration{ |
|||
CORSRules: make([]CORSRule, 101), |
|||
}, |
|||
wantErr: true, |
|||
}, |
|||
{ |
|||
name: "invalid method", |
|||
config: &CORSConfiguration{ |
|||
CORSRules: []CORSRule{ |
|||
{ |
|||
AllowedMethods: []string{"INVALID"}, |
|||
AllowedOrigins: []string{"*"}, |
|||
}, |
|||
}, |
|||
}, |
|||
wantErr: true, |
|||
}, |
|||
{ |
|||
name: "empty origins", |
|||
config: &CORSConfiguration{ |
|||
CORSRules: []CORSRule{ |
|||
{ |
|||
AllowedMethods: []string{"GET"}, |
|||
AllowedOrigins: []string{}, |
|||
}, |
|||
}, |
|||
}, |
|||
wantErr: true, |
|||
}, |
|||
{ |
|||
name: "invalid origin with multiple wildcards", |
|||
config: &CORSConfiguration{ |
|||
CORSRules: []CORSRule{ |
|||
{ |
|||
AllowedMethods: []string{"GET"}, |
|||
AllowedOrigins: []string{"http://*.*.example.com"}, |
|||
}, |
|||
}, |
|||
}, |
|||
wantErr: true, |
|||
}, |
|||
{ |
|||
name: "negative MaxAgeSeconds", |
|||
config: &CORSConfiguration{ |
|||
CORSRules: []CORSRule{ |
|||
{ |
|||
AllowedMethods: []string{"GET"}, |
|||
AllowedOrigins: []string{"*"}, |
|||
MaxAgeSeconds: intPtr(-1), |
|||
}, |
|||
}, |
|||
}, |
|||
wantErr: true, |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
err := ValidateConfiguration(tt.config) |
|||
if (err != nil) != tt.wantErr { |
|||
t.Errorf("ValidateConfiguration() error = %v, wantErr %v", err, tt.wantErr) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
func TestValidateOrigin(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
origin string |
|||
wantErr bool |
|||
}{ |
|||
{ |
|||
name: "empty origin", |
|||
origin: "", |
|||
wantErr: true, |
|||
}, |
|||
{ |
|||
name: "valid origin", |
|||
origin: "http://example.com", |
|||
wantErr: false, |
|||
}, |
|||
{ |
|||
name: "wildcard origin", |
|||
origin: "*", |
|||
wantErr: false, |
|||
}, |
|||
{ |
|||
name: "valid wildcard origin", |
|||
origin: "http://*.example.com", |
|||
wantErr: false, |
|||
}, |
|||
{ |
|||
name: "https wildcard origin", |
|||
origin: "https://*.example.com", |
|||
wantErr: false, |
|||
}, |
|||
{ |
|||
name: "invalid wildcard origin", |
|||
origin: "*.example.com", |
|||
wantErr: true, |
|||
}, |
|||
{ |
|||
name: "multiple wildcards", |
|||
origin: "http://*.*.example.com", |
|||
wantErr: true, |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
err := validateOrigin(tt.origin) |
|||
if (err != nil) != tt.wantErr { |
|||
t.Errorf("validateOrigin() error = %v, wantErr %v", err, tt.wantErr) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
func TestParseRequest(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
req *http.Request |
|||
want *CORSRequest |
|||
}{ |
|||
{ |
|||
name: "simple GET request", |
|||
req: &http.Request{ |
|||
Method: "GET", |
|||
Header: http.Header{ |
|||
"Origin": []string{"http://example.com"}, |
|||
}, |
|||
}, |
|||
want: &CORSRequest{ |
|||
Origin: "http://example.com", |
|||
Method: "GET", |
|||
IsPreflightRequest: false, |
|||
}, |
|||
}, |
|||
{ |
|||
name: "OPTIONS preflight request", |
|||
req: &http.Request{ |
|||
Method: "OPTIONS", |
|||
Header: http.Header{ |
|||
"Origin": []string{"http://example.com"}, |
|||
"Access-Control-Request-Method": []string{"PUT"}, |
|||
"Access-Control-Request-Headers": []string{"Content-Type, Authorization"}, |
|||
}, |
|||
}, |
|||
want: &CORSRequest{ |
|||
Origin: "http://example.com", |
|||
Method: "OPTIONS", |
|||
IsPreflightRequest: true, |
|||
AccessControlRequestMethod: "PUT", |
|||
AccessControlRequestHeaders: []string{"Content-Type", "Authorization"}, |
|||
}, |
|||
}, |
|||
{ |
|||
name: "request without origin", |
|||
req: &http.Request{ |
|||
Method: "GET", |
|||
Header: http.Header{}, |
|||
}, |
|||
want: &CORSRequest{ |
|||
Origin: "", |
|||
Method: "GET", |
|||
IsPreflightRequest: false, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
got := ParseRequest(tt.req) |
|||
if !reflect.DeepEqual(got, tt.want) { |
|||
t.Errorf("ParseRequest() = %v, want %v", got, tt.want) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
func TestMatchesOrigin(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
allowedOrigins []string |
|||
origin string |
|||
want bool |
|||
}{ |
|||
{ |
|||
name: "wildcard match", |
|||
allowedOrigins: []string{"*"}, |
|||
origin: "http://example.com", |
|||
want: true, |
|||
}, |
|||
{ |
|||
name: "exact match", |
|||
allowedOrigins: []string{"http://example.com"}, |
|||
origin: "http://example.com", |
|||
want: true, |
|||
}, |
|||
{ |
|||
name: "no match", |
|||
allowedOrigins: []string{"http://example.com"}, |
|||
origin: "http://other.com", |
|||
want: false, |
|||
}, |
|||
{ |
|||
name: "wildcard subdomain match", |
|||
allowedOrigins: []string{"http://*.example.com"}, |
|||
origin: "http://api.example.com", |
|||
want: true, |
|||
}, |
|||
{ |
|||
name: "wildcard subdomain no match", |
|||
allowedOrigins: []string{"http://*.example.com"}, |
|||
origin: "http://example.com", |
|||
want: false, |
|||
}, |
|||
{ |
|||
name: "multiple origins with match", |
|||
allowedOrigins: []string{"http://example.com", "http://other.com"}, |
|||
origin: "http://other.com", |
|||
want: true, |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
got := matchesOrigin(tt.allowedOrigins, tt.origin) |
|||
if got != tt.want { |
|||
t.Errorf("matchesOrigin() = %v, want %v", got, tt.want) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
func TestMatchesHeader(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
allowedHeaders []string |
|||
header string |
|||
want bool |
|||
}{ |
|||
{ |
|||
name: "empty allowed headers", |
|||
allowedHeaders: []string{}, |
|||
header: "Content-Type", |
|||
want: true, |
|||
}, |
|||
{ |
|||
name: "wildcard match", |
|||
allowedHeaders: []string{"*"}, |
|||
header: "Content-Type", |
|||
want: true, |
|||
}, |
|||
{ |
|||
name: "exact match", |
|||
allowedHeaders: []string{"Content-Type"}, |
|||
header: "Content-Type", |
|||
want: true, |
|||
}, |
|||
{ |
|||
name: "case insensitive match", |
|||
allowedHeaders: []string{"content-type"}, |
|||
header: "Content-Type", |
|||
want: true, |
|||
}, |
|||
{ |
|||
name: "no match", |
|||
allowedHeaders: []string{"Authorization"}, |
|||
header: "Content-Type", |
|||
want: false, |
|||
}, |
|||
{ |
|||
name: "wildcard prefix match", |
|||
allowedHeaders: []string{"x-amz-*"}, |
|||
header: "x-amz-date", |
|||
want: true, |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
got := matchesHeader(tt.allowedHeaders, tt.header) |
|||
if got != tt.want { |
|||
t.Errorf("matchesHeader() = %v, want %v", got, tt.want) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
func TestEvaluateRequest(t *testing.T) { |
|||
config := &CORSConfiguration{ |
|||
CORSRules: []CORSRule{ |
|||
{ |
|||
AllowedMethods: []string{"GET", "POST"}, |
|||
AllowedOrigins: []string{"http://example.com"}, |
|||
AllowedHeaders: []string{"Content-Type"}, |
|||
ExposeHeaders: []string{"ETag"}, |
|||
MaxAgeSeconds: intPtr(3600), |
|||
}, |
|||
{ |
|||
AllowedMethods: []string{"PUT"}, |
|||
AllowedOrigins: []string{"*"}, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
tests := []struct { |
|||
name string |
|||
config *CORSConfiguration |
|||
corsReq *CORSRequest |
|||
want *CORSResponse |
|||
wantErr bool |
|||
}{ |
|||
{ |
|||
name: "matching first rule", |
|||
config: config, |
|||
corsReq: &CORSRequest{ |
|||
Origin: "http://example.com", |
|||
Method: "GET", |
|||
}, |
|||
want: &CORSResponse{ |
|||
AllowOrigin: "http://example.com", |
|||
AllowMethods: "GET, POST", |
|||
AllowHeaders: "Content-Type", |
|||
ExposeHeaders: "ETag", |
|||
MaxAge: "3600", |
|||
}, |
|||
wantErr: false, |
|||
}, |
|||
{ |
|||
name: "matching second rule", |
|||
config: config, |
|||
corsReq: &CORSRequest{ |
|||
Origin: "http://other.com", |
|||
Method: "PUT", |
|||
}, |
|||
want: &CORSResponse{ |
|||
AllowOrigin: "http://other.com", |
|||
AllowMethods: "PUT", |
|||
}, |
|||
wantErr: false, |
|||
}, |
|||
{ |
|||
name: "no matching rule", |
|||
config: config, |
|||
corsReq: &CORSRequest{ |
|||
Origin: "http://forbidden.com", |
|||
Method: "GET", |
|||
}, |
|||
want: nil, |
|||
wantErr: true, |
|||
}, |
|||
{ |
|||
name: "preflight request", |
|||
config: config, |
|||
corsReq: &CORSRequest{ |
|||
Origin: "http://example.com", |
|||
Method: "OPTIONS", |
|||
IsPreflightRequest: true, |
|||
AccessControlRequestMethod: "POST", |
|||
AccessControlRequestHeaders: []string{"Content-Type"}, |
|||
}, |
|||
want: &CORSResponse{ |
|||
AllowOrigin: "http://example.com", |
|||
AllowMethods: "GET, POST", |
|||
AllowHeaders: "Content-Type", |
|||
ExposeHeaders: "ETag", |
|||
MaxAge: "3600", |
|||
}, |
|||
wantErr: false, |
|||
}, |
|||
{ |
|||
name: "preflight request with forbidden header", |
|||
config: config, |
|||
corsReq: &CORSRequest{ |
|||
Origin: "http://example.com", |
|||
Method: "OPTIONS", |
|||
IsPreflightRequest: true, |
|||
AccessControlRequestMethod: "POST", |
|||
AccessControlRequestHeaders: []string{"Authorization"}, |
|||
}, |
|||
want: nil, |
|||
wantErr: true, |
|||
}, |
|||
{ |
|||
name: "request without origin", |
|||
config: config, |
|||
corsReq: &CORSRequest{ |
|||
Origin: "", |
|||
Method: "GET", |
|||
}, |
|||
want: nil, |
|||
wantErr: true, |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
got, err := EvaluateRequest(tt.config, tt.corsReq) |
|||
if (err != nil) != tt.wantErr { |
|||
t.Errorf("EvaluateRequest() error = %v, wantErr %v", err, tt.wantErr) |
|||
return |
|||
} |
|||
if !reflect.DeepEqual(got, tt.want) { |
|||
t.Errorf("EvaluateRequest() = %v, want %v", got, tt.want) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
func TestApplyHeaders(t *testing.T) { |
|||
tests := []struct { |
|||
name string |
|||
corsResp *CORSResponse |
|||
want map[string]string |
|||
}{ |
|||
{ |
|||
name: "nil response", |
|||
corsResp: nil, |
|||
want: map[string]string{}, |
|||
}, |
|||
{ |
|||
name: "complete response", |
|||
corsResp: &CORSResponse{ |
|||
AllowOrigin: "http://example.com", |
|||
AllowMethods: "GET, POST", |
|||
AllowHeaders: "Content-Type", |
|||
ExposeHeaders: "ETag", |
|||
MaxAge: "3600", |
|||
}, |
|||
want: map[string]string{ |
|||
"Access-Control-Allow-Origin": "http://example.com", |
|||
"Access-Control-Allow-Methods": "GET, POST", |
|||
"Access-Control-Allow-Headers": "Content-Type", |
|||
"Access-Control-Expose-Headers": "ETag", |
|||
"Access-Control-Max-Age": "3600", |
|||
}, |
|||
}, |
|||
{ |
|||
name: "with credentials", |
|||
corsResp: &CORSResponse{ |
|||
AllowOrigin: "http://example.com", |
|||
AllowMethods: "GET", |
|||
AllowCredentials: true, |
|||
}, |
|||
want: map[string]string{ |
|||
"Access-Control-Allow-Origin": "http://example.com", |
|||
"Access-Control-Allow-Methods": "GET", |
|||
"Access-Control-Allow-Credentials": "true", |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
for _, tt := range tests { |
|||
t.Run(tt.name, func(t *testing.T) { |
|||
// Create a proper response writer using httptest
|
|||
w := httptest.NewRecorder() |
|||
|
|||
ApplyHeaders(w, tt.corsResp) |
|||
|
|||
// Extract headers from the response
|
|||
headers := make(map[string]string) |
|||
for key, values := range w.Header() { |
|||
if len(values) > 0 { |
|||
headers[key] = values[0] |
|||
} |
|||
} |
|||
|
|||
if !reflect.DeepEqual(headers, tt.want) { |
|||
t.Errorf("ApplyHeaders() headers = %v, want %v", headers, tt.want) |
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
// Helper functions and types for testing
|
|||
|
|||
func intPtr(i int) *int { |
|||
return &i |
|||
} |
@ -0,0 +1,171 @@ |
|||
package cors |
|||
|
|||
import ( |
|||
"net/http" |
|||
"strings" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" |
|||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err" |
|||
) |
|||
|
|||
// BucketChecker interface for checking bucket existence
|
|||
type BucketChecker interface { |
|||
CheckBucket(r *http.Request, bucket string) s3err.ErrorCode |
|||
} |
|||
|
|||
// CORSConfigGetter interface for getting CORS configuration
|
|||
type CORSConfigGetter interface { |
|||
GetCORSConfiguration(bucket string) (*CORSConfiguration, s3err.ErrorCode) |
|||
} |
|||
|
|||
// 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 { |
|||
return &Middleware{ |
|||
storage: storage, |
|||
bucketChecker: bucketChecker, |
|||
corsConfigGetter: corsConfigGetter, |
|||
} |
|||
} |
|||
|
|||
// 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) { |
|||
// Check if this is a CORS request
|
|||
corsReq := ParseRequest(r) |
|||
if corsReq.Origin == "" { |
|||
// Not a CORS request, continue normally
|
|||
next.ServeHTTP(w, r) |
|||
return |
|||
} |
|||
|
|||
// Extract bucket from request
|
|||
bucket, _ := s3_constants.GetBucketAndObject(r) |
|||
if bucket == "" { |
|||
// No bucket in request, continue normally
|
|||
next.ServeHTTP(w, r) |
|||
return |
|||
} |
|||
|
|||
// Check if bucket exists
|
|||
if err := m.bucketChecker.CheckBucket(r, bucket); err != s3err.ErrNone { |
|||
// Bucket doesn't exist, 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 to response
|
|||
ApplyHeaders(w, corsResp) |
|||
|
|||
// Handle preflight requests
|
|||
if corsReq.IsPreflightRequest { |
|||
// Preflight request should return 200 OK with just CORS headers
|
|||
w.WriteHeader(http.StatusOK) |
|||
return |
|||
} |
|||
|
|||
// Continue with normal request processing
|
|||
next.ServeHTTP(w, r) |
|||
}) |
|||
} |
|||
|
|||
// HandleOptionsRequest handles OPTIONS requests for CORS preflight
|
|||
func (m *Middleware) HandleOptionsRequest(w http.ResponseWriter, r *http.Request) { |
|||
// This is handled by the CORS middleware, but we need a specific OPTIONS handler
|
|||
// for the router to recognize OPTIONS requests
|
|||
|
|||
// Parse CORS request
|
|||
corsReq := ParseRequest(r) |
|||
if corsReq.Origin == "" { |
|||
// Not a CORS request
|
|||
w.WriteHeader(http.StatusOK) |
|||
return |
|||
} |
|||
|
|||
// 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 { |
|||
s3err.WriteErrorResponse(w, r, err) |
|||
return |
|||
} |
|||
|
|||
// Load CORS configuration from cache
|
|||
config, errCode := m.corsConfigGetter.GetCORSConfiguration(bucket) |
|||
if errCode != s3err.ErrNone || config == nil { |
|||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied) |
|||
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) |
|||
s3err.WriteErrorResponse(w, r, s3err.ErrAccessDenied) |
|||
return |
|||
} |
|||
|
|||
// Apply CORS headers and return success
|
|||
ApplyHeaders(w, corsResp) |
|||
w.WriteHeader(http.StatusOK) |
|||
} |
|||
|
|||
// ShouldApplyCORS checks if CORS should be applied to the current request
|
|||
func ShouldApplyCORS(r *http.Request) bool { |
|||
// Apply CORS to all bucket and object operations
|
|||
path := r.URL.Path |
|||
|
|||
// Skip CORS for service-level operations
|
|||
if path == "/" || path == "" { |
|||
return false |
|||
} |
|||
|
|||
// Skip CORS for bucket listing
|
|||
if strings.HasPrefix(path, "/") && !strings.Contains(path[1:], "/") { |
|||
// This is a bucket-level operation
|
|||
return true |
|||
} |
|||
|
|||
// This is an object-level operation
|
|||
return true |
|||
} |
@ -0,0 +1,160 @@ |
|||
package s3api |
|||
|
|||
import ( |
|||
"encoding/xml" |
|||
"net/http" |
|||
|
|||
"github.com/seaweedfs/seaweedfs/weed/glog" |
|||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
|||
"github.com/seaweedfs/seaweedfs/weed/s3api/cors" |
|||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants" |
|||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err" |
|||
) |
|||
|
|||
// S3EntryGetter implements cors.EntryGetter interface
|
|||
type S3EntryGetter struct { |
|||
server *S3ApiServer |
|||
} |
|||
|
|||
func (g *S3EntryGetter) GetEntry(directory, name string) (*filer_pb.Entry, error) { |
|||
return g.server.getEntry(directory, name) |
|||
} |
|||
|
|||
// S3BucketChecker implements cors.BucketChecker interface
|
|||
type S3BucketChecker struct { |
|||
server *S3ApiServer |
|||
} |
|||
|
|||
func (c *S3BucketChecker) CheckBucket(r *http.Request, bucket string) s3err.ErrorCode { |
|||
return c.server.checkBucket(r, bucket) |
|||
} |
|||
|
|||
// S3CORSConfigGetter implements cors.CORSConfigGetter interface
|
|||
type S3CORSConfigGetter struct { |
|||
server *S3ApiServer |
|||
} |
|||
|
|||
func (g *S3CORSConfigGetter) GetCORSConfiguration(bucket string) (*cors.CORSConfiguration, s3err.ErrorCode) { |
|||
return g.server.getCORSConfiguration(bucket) |
|||
} |
|||
|
|||
// getCORSStorage returns a CORS storage instance
|
|||
func (s3a *S3ApiServer) getCORSStorage() *cors.Storage { |
|||
entryGetter := &S3EntryGetter{server: s3a} |
|||
return cors.NewStorage(s3a, entryGetter, s3a.option.BucketsPath) |
|||
} |
|||
|
|||
// getCORSMiddleware returns a CORS middleware instance with caching
|
|||
func (s3a *S3ApiServer) getCORSMiddleware() *cors.Middleware { |
|||
storage := s3a.getCORSStorage() |
|||
bucketChecker := &S3BucketChecker{server: s3a} |
|||
corsConfigGetter := &S3CORSConfigGetter{server: s3a} |
|||
|
|||
return cors.NewMiddleware(storage, bucketChecker, corsConfigGetter) |
|||
} |
|||
|
|||
// GetBucketCorsHandler handles Get bucket CORS configuration
|
|||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketCors.html
|
|||
func (s3a *S3ApiServer) GetBucketCorsHandler(w http.ResponseWriter, r *http.Request) { |
|||
bucket, _ := s3_constants.GetBucketAndObject(r) |
|||
glog.V(3).Infof("GetBucketCorsHandler %s", bucket) |
|||
|
|||
if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone { |
|||
s3err.WriteErrorResponse(w, r, err) |
|||
return |
|||
} |
|||
|
|||
// Load CORS configuration from cache
|
|||
config, errCode := s3a.getCORSConfiguration(bucket) |
|||
if errCode != s3err.ErrNone { |
|||
if errCode == s3err.ErrNoSuchBucket { |
|||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchBucket) |
|||
} else { |
|||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) |
|||
} |
|||
return |
|||
} |
|||
|
|||
if config == nil { |
|||
s3err.WriteErrorResponse(w, r, s3err.ErrNoSuchCORSConfiguration) |
|||
return |
|||
} |
|||
|
|||
// Return CORS configuration as XML
|
|||
writeSuccessResponseXML(w, r, config) |
|||
} |
|||
|
|||
// PutBucketCorsHandler handles Put bucket CORS configuration
|
|||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketCors.html
|
|||
func (s3a *S3ApiServer) PutBucketCorsHandler(w http.ResponseWriter, r *http.Request) { |
|||
bucket, _ := s3_constants.GetBucketAndObject(r) |
|||
glog.V(3).Infof("PutBucketCorsHandler %s", bucket) |
|||
|
|||
if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone { |
|||
s3err.WriteErrorResponse(w, r, err) |
|||
return |
|||
} |
|||
|
|||
// Parse CORS configuration from request body
|
|||
var config cors.CORSConfiguration |
|||
if err := xml.NewDecoder(r.Body).Decode(&config); err != nil { |
|||
glog.V(1).Infof("Failed to parse CORS configuration: %v", err) |
|||
s3err.WriteErrorResponse(w, r, s3err.ErrMalformedXML) |
|||
return |
|||
} |
|||
|
|||
// Validate CORS configuration
|
|||
if err := cors.ValidateConfiguration(&config); err != nil { |
|||
glog.V(1).Infof("Invalid CORS configuration: %v", err) |
|||
s3err.WriteErrorResponse(w, r, s3err.ErrInvalidRequest) |
|||
return |
|||
} |
|||
|
|||
// Store CORS configuration and update cache
|
|||
if err := s3a.updateCORSConfiguration(bucket, &config); err != s3err.ErrNone { |
|||
glog.Errorf("Failed to update CORS configuration: %v", err) |
|||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) |
|||
return |
|||
} |
|||
|
|||
// Also update the .s3metadata file directly
|
|||
storage := s3a.getCORSStorage() |
|||
if err := storage.Store(bucket, &config); err != nil { |
|||
glog.Errorf("Failed to store CORS configuration in metadata: %v", err) |
|||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) |
|||
return |
|||
} |
|||
|
|||
// Return success
|
|||
writeSuccessResponseEmpty(w, r) |
|||
} |
|||
|
|||
// DeleteBucketCorsHandler handles Delete bucket CORS configuration
|
|||
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteBucketCors.html
|
|||
func (s3a *S3ApiServer) DeleteBucketCorsHandler(w http.ResponseWriter, r *http.Request) { |
|||
bucket, _ := s3_constants.GetBucketAndObject(r) |
|||
glog.V(3).Infof("DeleteBucketCorsHandler %s", bucket) |
|||
|
|||
if err := s3a.checkBucket(r, bucket); err != s3err.ErrNone { |
|||
s3err.WriteErrorResponse(w, r, err) |
|||
return |
|||
} |
|||
|
|||
// Remove CORS configuration from cache
|
|||
if err := s3a.removeCORSConfiguration(bucket); err != s3err.ErrNone { |
|||
glog.Errorf("Failed to remove CORS configuration from cache: %v", err) |
|||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) |
|||
return |
|||
} |
|||
|
|||
// Also remove from .s3metadata file
|
|||
storage := s3a.getCORSStorage() |
|||
if err := storage.Delete(bucket); err != nil { |
|||
glog.Errorf("Failed to delete CORS configuration from metadata: %v", err) |
|||
s3err.WriteErrorResponse(w, r, s3err.ErrInternalError) |
|||
return |
|||
} |
|||
|
|||
// Return success (204 No Content)
|
|||
w.WriteHeader(http.StatusNoContent) |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue