mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* s3: keep verifying the request host when externalUrl is set externalUrl was the only host candidate once set, so a client that dialed the gateway directly instead of through the proxy always got SignatureDoesNotMatch. Make it lead the candidate walk instead: every candidate still needs a valid signature, and the request-derived hosts are already trusted when the flag is unset, so a mixed proxy plus in-cluster topology can now advertise a public endpoint and verify both planes. * s3: cover virtual-hosted addressing behind externalUrl The old pin also rejected an external client that signed bucket.api.example.com, since only the bare externalUrl host was ever tried. The candidate walk covers it; pin the case down.
680 lines
20 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|