mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2025-11-24 16:53:14 +08:00
* Add nginx reverse proxy documentation for S3 API Fixes #7407 Add comprehensive documentation and example configuration for using nginx as a reverse proxy with SeaweedFS S3 API while maintaining AWS Signature V4 authentication compatibility. Changes: - Add docker/nginx/README.md with detailed setup guide - Add docker/nginx/s3-example.conf with working configuration - Update docker/nginx/proxy.conf with important S3 notes The documentation covers: - Critical requirements for AWS Signature V4 authentication - Common mistakes and why they break S3 authentication - Complete working nginx configurations - Debugging tips and troubleshooting - Performance tuning recommendations * Fix IPv6 host header formatting to match AWS SDK behavior Follow-up to PR #7403 When a default port (80 for HTTP, 443 for HTTPS) is stripped from an IPv6 address, the square brackets should also be removed to match AWS SDK behavior for S3 signature calculation. Reference: https://github.com/aws/aws-sdk-go-v2/blob/main/aws/signer/internal/v4/host.go The AWS SDK's stripPort function explicitly removes brackets when returning an IPv6 address without a port. Changes: - Update extractHostHeader to strip brackets from IPv6 addresses when no port or default port is used - Update test expectations to match AWS SDK behavior - Add detailed comments explaining the AWS SDK compatibility requirement This ensures S3 signature validation works correctly with IPv6 addresses behind reverse proxies, matching AWS S3 canonical request format. Fixes the issue raised in PR #7403 comment: https://github.com/seaweedfs/seaweedfs/pull/7403#issuecomment-3471105438 * Update docker/nginx/README.md Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> * Add nginx reverse proxy documentation for S3 API Fixes #7407 Add comprehensive documentation and example configuration for using nginx as a reverse proxy with SeaweedFS S3 API while maintaining AWS Signature V4 authentication compatibility. Changes: - Add docker/nginx/README.md with detailed setup guide - Add docker/nginx/s3-example.conf with working configuration - Update docker/nginx/proxy.conf with important S3 notes The documentation covers: - Critical requirements for AWS Signature V4 authentication - Common mistakes and why they break S3 authentication - Complete working nginx configurations - Debugging tips and troubleshooting - Performance tuning recommendations Fix IPv6 host header formatting to match AWS SDK behavior Follow-up to PR #7403 When a default port (80 for HTTP, 443 for HTTPS) is stripped from an IPv6 address, the square brackets should also be removed to match AWS SDK behavior for S3 signature calculation. Reference: https://github.com/aws/aws-sdk-go-v2/blob/main/aws/signer/internal/v4/host.go The AWS SDK's stripPort function explicitly removes brackets when returning an IPv6 address without a port. Changes: - Update extractHostHeader to strip brackets from IPv6 addresses when no port or default port is used - Update test expectations to match AWS SDK behavior - Add detailed comments explaining the AWS SDK compatibility requirement This ensures S3 signature validation works correctly with IPv6 addresses behind reverse proxies, matching AWS S3 canonical request format. Fixes the issue raised in PR #7403 comment: https://github.com/seaweedfs/seaweedfs/pull/7403#issuecomment-3471105438 * Revert "Merge branch 'fix-ipv6-brackets-default-port' of https://github.com/seaweedfs/seaweedfs into fix-ipv6-brackets-default-port" This reverts commitcca3f3985f, reversing changes made to2b8f9de78e. --------- Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
264 lines
7.5 KiB
Go
264 lines
7.5 KiB
Go
package s3api
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
)
|
|
|
|
func TestBuildPathWithForwardedPrefix(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
forwardedPrefix string
|
|
urlPath string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "empty prefix returns urlPath",
|
|
forwardedPrefix: "",
|
|
urlPath: "/bucket/obj",
|
|
expected: "/bucket/obj",
|
|
},
|
|
{
|
|
name: "prefix without trailing slash",
|
|
forwardedPrefix: "/storage",
|
|
urlPath: "/bucket/obj",
|
|
expected: "/storage/bucket/obj",
|
|
},
|
|
{
|
|
name: "prefix with trailing slash",
|
|
forwardedPrefix: "/storage/",
|
|
urlPath: "/bucket/obj",
|
|
expected: "/storage/bucket/obj",
|
|
},
|
|
{
|
|
name: "prefix without leading slash",
|
|
forwardedPrefix: "storage",
|
|
urlPath: "/bucket/obj",
|
|
expected: "/storage/bucket/obj",
|
|
},
|
|
{
|
|
name: "prefix without leading slash and with trailing slash",
|
|
forwardedPrefix: "storage/",
|
|
urlPath: "/bucket/obj",
|
|
expected: "/storage/bucket/obj",
|
|
},
|
|
{
|
|
name: "preserve double slashes in key",
|
|
forwardedPrefix: "/storage",
|
|
urlPath: "/bucket//obj",
|
|
expected: "/storage/bucket//obj",
|
|
},
|
|
{
|
|
name: "preserve trailing slash in urlPath",
|
|
forwardedPrefix: "/storage",
|
|
urlPath: "/bucket/folder/",
|
|
expected: "/storage/bucket/folder/",
|
|
},
|
|
{
|
|
name: "preserve trailing slash with prefix having trailing slash",
|
|
forwardedPrefix: "/storage/",
|
|
urlPath: "/bucket/folder/",
|
|
expected: "/storage/bucket/folder/",
|
|
},
|
|
{
|
|
name: "root path",
|
|
forwardedPrefix: "/storage",
|
|
urlPath: "/",
|
|
expected: "/storage/",
|
|
},
|
|
{
|
|
name: "complex key with multiple slashes",
|
|
forwardedPrefix: "/api/v1",
|
|
urlPath: "/bucket/path//with///slashes",
|
|
expected: "/api/v1/bucket/path//with///slashes",
|
|
},
|
|
{
|
|
name: "urlPath without leading slash",
|
|
forwardedPrefix: "/storage",
|
|
urlPath: "bucket/obj",
|
|
expected: "/storage/bucket/obj",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := buildPathWithForwardedPrefix(tt.forwardedPrefix, tt.urlPath)
|
|
if result != tt.expected {
|
|
t.Errorf("buildPathWithForwardedPrefix(%q, %q) = %q, want %q",
|
|
tt.forwardedPrefix, tt.urlPath, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestExtractHostHeader tests the extractHostHeader function with various scenarios
|
|
func TestExtractHostHeader(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
hostHeader string
|
|
forwardedHost string
|
|
forwardedPort string
|
|
forwardedProto string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "basic host without forwarding",
|
|
hostHeader: "example.com",
|
|
forwardedHost: "",
|
|
forwardedPort: "",
|
|
forwardedProto: "",
|
|
expected: "example.com",
|
|
},
|
|
{
|
|
name: "host with port without forwarding",
|
|
hostHeader: "example.com:8080",
|
|
forwardedHost: "",
|
|
forwardedPort: "",
|
|
forwardedProto: "",
|
|
expected: "example.com:8080",
|
|
},
|
|
{
|
|
name: "X-Forwarded-Host without port",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "example.com",
|
|
forwardedPort: "",
|
|
forwardedProto: "",
|
|
expected: "example.com",
|
|
},
|
|
{
|
|
name: "X-Forwarded-Host with X-Forwarded-Port (HTTP non-standard)",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "example.com",
|
|
forwardedPort: "8080",
|
|
forwardedProto: "http",
|
|
expected: "example.com:8080",
|
|
},
|
|
{
|
|
name: "X-Forwarded-Host with X-Forwarded-Port (HTTPS non-standard)",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "example.com",
|
|
forwardedPort: "8443",
|
|
forwardedProto: "https",
|
|
expected: "example.com:8443",
|
|
},
|
|
{
|
|
name: "X-Forwarded-Host with X-Forwarded-Port (HTTP standard port 80)",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "example.com",
|
|
forwardedPort: "80",
|
|
forwardedProto: "http",
|
|
expected: "example.com",
|
|
},
|
|
{
|
|
name: "X-Forwarded-Host with X-Forwarded-Port (HTTPS standard port 443)",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "example.com",
|
|
forwardedPort: "443",
|
|
forwardedProto: "https",
|
|
expected: "example.com",
|
|
},
|
|
// Issue #6649: X-Forwarded-Host already contains port (Traefik/HAProxy style)
|
|
{
|
|
name: "X-Forwarded-Host with port already included (should not add port again)",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "127.0.0.1:8433",
|
|
forwardedPort: "8433",
|
|
forwardedProto: "https",
|
|
expected: "127.0.0.1:8433",
|
|
},
|
|
{
|
|
name: "X-Forwarded-Host with port, no X-Forwarded-Port header",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "example.com:9000",
|
|
forwardedPort: "",
|
|
forwardedProto: "http",
|
|
expected: "example.com:9000",
|
|
},
|
|
// IPv6 test cases
|
|
{
|
|
name: "IPv6 address with brackets and port in X-Forwarded-Host",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "[::1]:8080",
|
|
forwardedPort: "8080",
|
|
forwardedProto: "http",
|
|
expected: "[::1]:8080",
|
|
},
|
|
{
|
|
name: "IPv6 address without brackets, should add brackets with port",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "::1",
|
|
forwardedPort: "8080",
|
|
forwardedProto: "http",
|
|
expected: "[::1]:8080",
|
|
},
|
|
{
|
|
name: "IPv6 address without brackets and standard port, should strip brackets per AWS SDK",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "::1",
|
|
forwardedPort: "80",
|
|
forwardedProto: "http",
|
|
expected: "::1",
|
|
},
|
|
{
|
|
name: "IPv6 address without brackets and standard HTTPS port, should strip brackets per AWS SDK",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "2001:db8::1",
|
|
forwardedPort: "443",
|
|
forwardedProto: "https",
|
|
expected: "2001:db8::1",
|
|
},
|
|
{
|
|
name: "IPv6 address with brackets but no port, should add port",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "[2001:db8::1]",
|
|
forwardedPort: "8080",
|
|
forwardedProto: "http",
|
|
expected: "[2001:db8::1]:8080",
|
|
},
|
|
{
|
|
name: "IPv6 full address with brackets and default port (should strip port and brackets)",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "[2001:db8:85a3::8a2e:370:7334]:443",
|
|
forwardedPort: "443",
|
|
forwardedProto: "https",
|
|
expected: "2001:db8:85a3::8a2e:370:7334",
|
|
},
|
|
{
|
|
name: "IPv4-mapped IPv6 address without brackets, should add brackets with port",
|
|
hostHeader: "backend:8333",
|
|
forwardedHost: "::ffff:127.0.0.1",
|
|
forwardedPort: "8080",
|
|
forwardedProto: "http",
|
|
expected: "[::ffff:127.0.0.1]:8080",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create a mock request
|
|
req, err := http.NewRequest("GET", "http://"+tt.hostHeader+"/bucket/object", nil)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create request: %v", err)
|
|
}
|
|
|
|
// Set headers
|
|
req.Host = tt.hostHeader
|
|
if tt.forwardedHost != "" {
|
|
req.Header.Set("X-Forwarded-Host", tt.forwardedHost)
|
|
}
|
|
if tt.forwardedPort != "" {
|
|
req.Header.Set("X-Forwarded-Port", tt.forwardedPort)
|
|
}
|
|
if tt.forwardedProto != "" {
|
|
req.Header.Set("X-Forwarded-Proto", tt.forwardedProto)
|
|
}
|
|
|
|
// Test the function
|
|
result := extractHostHeader(req)
|
|
if result != tt.expected {
|
|
t.Errorf("extractHostHeader() = %q, want %q", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|