Files
upstream-seaweedfs/weed/s3api/auth_signature_v4_test.go
upstream-mirror 89f5be005d
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 03:23:42 +05:00

680 lines
20 KiB
Go

package s3api
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"net/http"
"slices"
"testing"
"time"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
)
func TestExtractV4AuthInfoFromHeader_S3Tables(t *testing.T) {
now := time.Now().UTC()
dateStr := now.Format(iso8601Format)
tests := []struct {
name string
service string
body string
expectAutoHash bool
}{
{
name: "s3 service should not auto-hash",
service: "s3",
body: "hello",
expectAutoHash: false,
},
{
name: "s3tables service should auto-hash",
service: "s3tables",
body: "hello",
expectAutoHash: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body := bytes.NewReader([]byte(tt.body))
req, _ := http.NewRequest(http.MethodPost, "http://localhost/", body)
authHeader := fmt.Sprintf("AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/%s/us-east-1/%s/aws4_request, SignedHeaders=host, Signature=dummy",
now.Format(yyyymmdd), tt.service)
req.Header.Set("Authorization", authHeader)
req.Header.Set("x-amz-date", dateStr)
authInfo, errCode := extractV4AuthInfoFromHeader(req)
if errCode != s3err.ErrNone {
t.Fatalf("extractV4AuthInfoFromHeader failed: %v", errCode)
}
if tt.expectAutoHash {
expectedHash := sha256.Sum256([]byte(tt.body))
expectedHashStr := hex.EncodeToString(expectedHash[:])
if authInfo.HashedPayload != expectedHashStr {
t.Errorf("Expected auto-hashed payload %s, got %s", expectedHashStr, authInfo.HashedPayload)
}
} else {
if authInfo.HashedPayload != emptySHA256 {
t.Errorf("Expected non-auto-hashed payload %s (emptySHA256), got %s", emptySHA256, authInfo.HashedPayload)
}
}
})
}
}
func TestParseSignedHeaderRejectsEmptyHeaderNames(t *testing.T) {
cases := []string{
"SignedHeaders=host;",
"SignedHeaders=;host",
"SignedHeaders=host;;x-amz-date",
}
for _, tc := range cases {
if _, errCode := parseSignedHeader(tc); errCode != s3err.ErrMissingFields {
t.Fatalf("parseSignedHeader(%q) errCode = %v, want %v", tc, errCode, s3err.ErrMissingFields)
}
}
}
func TestExtractV4AuthInfoFromQueryRejectsEmptySignedHeaderNames(t *testing.T) {
now := time.Now().UTC()
cases := []string{
"host;",
";host",
"host;;x-amz-date",
}
for _, tc := range cases {
t.Run(tc, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "http://localhost/bucket/object", nil)
if err != nil {
t.Fatalf("NewRequest: %v", err)
}
query := req.URL.Query()
query.Set("X-Amz-Algorithm", signV4Algorithm)
query.Set("X-Amz-Credential", fmt.Sprintf("AKIAIOSFODNN7EXAMPLE/%s/us-east-1/s3/aws4_request", now.Format(yyyymmdd)))
query.Set("X-Amz-Date", now.Format(iso8601Format))
query.Set("X-Amz-Expires", "60")
query.Set("X-Amz-Signature", "dummy")
query.Set("X-Amz-SignedHeaders", tc)
req.URL.RawQuery = query.Encode()
if _, errCode := extractV4AuthInfoFromQuery(req); errCode != s3err.ErrMissingFields {
t.Fatalf("extractV4AuthInfoFromQuery(%q) errCode = %v, want %v", tc, errCode, s3err.ErrMissingFields)
}
})
}
}
func TestGetCanonicalQueryString(t *testing.T) {
tests := []struct {
name string
target string
isPresigned bool
want string
}{
{
name: "sorts repeated values",
target: "http://localhost/bucket/key?partNumber=2&partNumber=10&uploadId=z",
want: "partNumber=10&partNumber=2&uploadId=z",
},
{
name: "removes presigned signature",
target: "http://localhost/bucket/key?X-Amz-Date=20260618T000000Z&X-Amz-Signature=dummy&X-Amz-SignedHeaders=host",
isPresigned: true,
want: "X-Amz-Date=20260618T000000Z&X-Amz-SignedHeaders=host",
},
{
name: "sorts mixed single and repeated parameters",
target: "http://localhost/bucket/key?z=last&a=2&bucket=b&a=1",
want: "a=1&a=2&bucket=b&z=last",
},
{
name: "encodes query parameter values",
target: "http://localhost/bucket/key?prefix=photos/2026&marker=a%2Bb",
want: "marker=a%2Bb&prefix=photos%2F2026",
},
{
name: "empty query string",
target: "http://localhost/bucket/key",
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, tt.target, nil)
if err != nil {
t.Fatalf("NewRequest: %v", err)
}
got := getCanonicalQueryString(req, tt.isPresigned)
if got != tt.want {
t.Fatalf("canonical query = %q, want %q", got, tt.want)
}
})
}
}
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",
},
{
name: "empty urlPath with prefix",
forwardedPrefix: "/s3",
urlPath: "",
expected: "/s3",
},
}
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
externalHost 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 standard port already included (HTTPS 443)",
hostHeader: "backend:8333",
forwardedHost: "example.com:443",
forwardedPort: "443",
forwardedProto: "https",
expected: "example.com",
},
{
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 default port",
hostHeader: "backend:8333",
forwardedHost: "::1",
forwardedPort: "80",
forwardedProto: "http",
expected: "::1",
},
{
name: "IPv6 address without brackets and standard HTTPS port, should strip default port",
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 default port)",
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",
},
{
name: "Simple port 442",
hostHeader: "bucket.domain.com:442",
expected: "bucket.domain.com:442",
},
{
name: "Port 442 with X-Forwarded-Host",
hostHeader: "backend:8333",
forwardedHost: "bucket.domain.com:442",
expected: "bucket.domain.com:442",
},
{
name: "Port 442 with X-Forwarded-Port",
hostHeader: "backend:8333",
forwardedHost: "bucket.domain.com",
forwardedPort: "442",
expected: "bucket.domain.com:442",
},
{
name: "HTTPS with port 442 (should NOT strip)",
hostHeader: "bucket.domain.com:442",
forwardedProto: "https",
expected: "bucket.domain.com:442",
},
{
name: "X-Forwarded-Host with multiple hosts (including port)",
forwardedHost: "bucket.domain.com:442, internal.proxy",
expected: "bucket.domain.com:442",
},
{
name: "IPv6 with port",
hostHeader: "[2001:db8::1]:442",
expected: "[2001:db8::1]:442",
},
{
name: "X-Forwarded-Host with port 442, but X-Forwarded-Port is 80 (should PREFER 442)",
forwardedHost: "bucket.domain.com:442",
forwardedPort: "80",
forwardedProto: "http",
expected: "bucket.domain.com:442",
},
// externalHost override tests
{
name: "externalHost overrides everything",
hostHeader: "backend:8333",
externalHost: "api.example.com:9000",
expected: "api.example.com:9000",
},
{
name: "externalHost overrides X-Forwarded-Host",
hostHeader: "backend:8333",
forwardedHost: "proxy.example.com",
forwardedPort: "443",
forwardedProto: "https",
externalHost: "api.example.com",
expected: "api.example.com",
},
{
name: "externalHost with IPv6",
hostHeader: "backend:8333",
externalHost: "[::1]:9000",
expected: "[::1]:9000",
},
// Bug fix: X-Forwarded-Port should not override more specific ports in other headers
{
name: "User reported case: X-Forwarded-Port misreports 443 but Host has 30007",
hostHeader: "storage-stgops.mt.mtnet:30007",
forwardedHost: "storage-stgops.mt.mtnet",
forwardedPort: "443",
forwardedProto: "https",
expected: "storage-stgops.mt.mtnet:30007",
},
{
name: "X-Forwarded-Host already contains correct port, ignore misaligned X-Forwarded-Port",
hostHeader: "backend:8333",
forwardedHost: "storage-stgops.mt.mtnet:30007",
forwardedPort: "443",
forwardedProto: "https",
expected: "storage-stgops.mt.mtnet:30007",
},
{
name: "X-Forwarded-Host has no port, match r.Host hostname and take its port",
hostHeader: "example.com:8080",
forwardedHost: "example.com",
forwardedPort: "80",
forwardedProto: "http",
expected: "example.com: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, tt.externalHost)
if result != tt.expected {
t.Errorf("extractHostHeader() = %q, want %q", result, tt.expected)
}
})
}
}
// TestExtractHostHeaderCandidates tests the alternate host values tried during verification
// when the true client-facing host is ambiguous behind a reverse proxy.
func TestExtractHostHeaderCandidates(t *testing.T) {
tests := []struct {
name string
hostHeader string
forwardedHost string
forwardedPort string
forwardedProto string
externalHost string
expected []string
}{
{
name: "externalHost leads, request host still follows",
hostHeader: "backend:8333",
externalHost: "api.example.com:9000",
expected: []string{"api.example.com:9000", "backend:8333"},
},
{
name: "externalHost leads the forwarded candidates",
hostHeader: "backend:8333",
forwardedHost: "example.com",
forwardedPort: "9000",
externalHost: "api.example.com",
expected: []string{"api.example.com", "example.com:9000", "example.com"},
},
{
name: "externalHost equal to the request host is not repeated",
hostHeader: "api.example.com:9000",
externalHost: "api.example.com:9000",
expected: []string{"api.example.com:9000"},
},
{
name: "X-Forwarded-Host with port is trusted as-is",
hostHeader: "backend:8333",
forwardedHost: "example.com:9000",
forwardedPort: "443",
expected: []string{"example.com:9000"},
},
{
name: "plain Host with port is the only candidate",
hostHeader: "example.com:8080",
expected: []string{"example.com:8080"},
},
{
name: "portless X-Forwarded-Host, hostnames match: r.Host port first, then X-Forwarded-Port, then bare",
hostHeader: "example.com:8333",
forwardedHost: "example.com",
forwardedPort: "9000",
forwardedProto: "http",
expected: []string{"example.com:8333", "example.com:9000", "example.com"},
},
{
name: "portless X-Forwarded-Host, hostnames match, no X-Forwarded-Port: bare host as fallback",
hostHeader: "example.com:8333",
forwardedHost: "example.com",
expected: []string{"example.com:8333", "example.com"},
},
{
name: "portless X-Forwarded-Host, hostnames differ: X-Forwarded-Port, then bare",
hostHeader: "backend:8333",
forwardedHost: "example.com",
forwardedPort: "9000",
expected: []string{"example.com:9000", "example.com"},
},
{
name: "default X-Forwarded-Port collapses into the bare candidate",
hostHeader: "example.com:8333",
forwardedHost: "example.com",
forwardedPort: "443",
forwardedProto: "https",
expected: []string{"example.com:8333", "example.com"},
},
{
name: "portless Host with X-Forwarded-Port: forwarded port, then bare",
hostHeader: "example.com",
forwardedPort: "9000",
expected: []string{"example.com:9000", "example.com"},
},
{
name: "bracketed portless IPv6 X-Forwarded-Host matches the request host",
hostHeader: "[::1]:8333",
forwardedHost: "[::1]",
expected: []string{"[::1]:8333", "::1"},
},
{
name: "unbracketed portless IPv6 X-Forwarded-Host with X-Forwarded-Port",
hostHeader: "backend:8333",
forwardedHost: "::1",
forwardedPort: "8080",
expected: []string{"[::1]:8080", "::1"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest("GET", "http://"+tt.hostHeader+"/bucket/object", nil)
if err != nil {
t.Fatalf("Failed to create request: %v", err)
}
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)
}
result := extractHostHeaderCandidates(req, tt.externalHost)
if !slices.Equal(result, tt.expected) {
t.Errorf("extractHostHeaderCandidates() = %v, want %v", result, tt.expected)
}
})
}
}
func TestExtractSignedHeadersCase(t *testing.T) {
tests := []struct {
name string
host string
signedHeads []string
expected string
}{
{
name: "lowercase host",
host: "bucket.domain.com:442",
signedHeads: []string{"host"},
expected: "host:bucket.domain.com:442\n",
},
{
name: "uppercase Host",
host: "bucket.domain.com:442",
signedHeads: []string{"Host"},
expected: "host:bucket.domain.com:442\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r, _ := http.NewRequest("GET", "http://"+tt.host+"/", nil)
r.Host = tt.host
extracted, errCode := extractSignedHeaders(tt.signedHeads, r, "")
if errCode != s3err.ErrNone {
t.Fatalf("extractSignedHeaders failed: %v", errCode)
}
actual := getCanonicalHeaders(extracted)
if actual != tt.expected {
t.Errorf("%s: expected %q, got %q", tt.name, tt.expected, actual)
}
})
}
}