s3api: preserve Host header port in signature verification (#8434)
Avoid stripping default ports (80/443) from the Host header in extractHostHeader. This fixes SignatureDoesNotMatch errors when SeaweedFS is accessed via a proxy (like Kong Ingress) that explicitly includes the port in the Host header or X-Forwarded-Host, which S3 clients sign. Also cleaned up unused variables and logic after refactoring.
This commit is contained in:
@@ -788,25 +788,9 @@ func extractSignedHeaders(signedHeaders []string, r *http.Request) (http.Header,
|
|||||||
func extractHostHeader(r *http.Request) string {
|
func extractHostHeader(r *http.Request) string {
|
||||||
forwardedHost := r.Header.Get("X-Forwarded-Host")
|
forwardedHost := r.Header.Get("X-Forwarded-Host")
|
||||||
forwardedPort := r.Header.Get("X-Forwarded-Port")
|
forwardedPort := r.Header.Get("X-Forwarded-Port")
|
||||||
forwardedProto := r.Header.Get("X-Forwarded-Proto")
|
|
||||||
|
|
||||||
// Determine the effective scheme with correct order of precedence:
|
|
||||||
// 1. X-Forwarded-Proto (most authoritative, reflects client's original protocol)
|
|
||||||
// 2. r.TLS (authoritative for direct connection to server)
|
|
||||||
// 3. r.URL.Scheme (fallback, may not always be set correctly)
|
|
||||||
// 4. Default to "http"
|
|
||||||
scheme := "http"
|
|
||||||
if r.URL.Scheme != "" {
|
|
||||||
scheme = r.URL.Scheme
|
|
||||||
}
|
|
||||||
if r.TLS != nil {
|
|
||||||
scheme = "https"
|
|
||||||
}
|
|
||||||
if forwardedProto != "" {
|
|
||||||
scheme = forwardedProto
|
|
||||||
}
|
|
||||||
|
|
||||||
var host, port string
|
var host, port string
|
||||||
|
explicitPort := false
|
||||||
if forwardedHost != "" {
|
if forwardedHost != "" {
|
||||||
// X-Forwarded-Host can be a comma-separated list of hosts when there are multiple proxies.
|
// X-Forwarded-Host can be a comma-separated list of hosts when there are multiple proxies.
|
||||||
// Use only the first host in the list and trim spaces for robustness.
|
// Use only the first host in the list and trim spaces for robustness.
|
||||||
@@ -815,12 +799,16 @@ func extractHostHeader(r *http.Request) string {
|
|||||||
} else {
|
} else {
|
||||||
host = strings.TrimSpace(forwardedHost)
|
host = strings.TrimSpace(forwardedHost)
|
||||||
}
|
}
|
||||||
|
// Baseline port from forwarded port if available
|
||||||
|
if forwardedPort != "" {
|
||||||
|
port = forwardedPort
|
||||||
|
explicitPort = true
|
||||||
|
}
|
||||||
|
// If the host itself contains a port, it should take precedence
|
||||||
if h, p, err := net.SplitHostPort(host); err == nil {
|
if h, p, err := net.SplitHostPort(host); err == nil {
|
||||||
host = h
|
host = h
|
||||||
port = p
|
port = p
|
||||||
}
|
explicitPort = true
|
||||||
if forwardedPort != "" && isDefaultPort(scheme, port) {
|
|
||||||
port = forwardedPort
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
host = r.Host
|
host = r.Host
|
||||||
@@ -830,12 +818,13 @@ func extractHostHeader(r *http.Request) string {
|
|||||||
if h, p, err := net.SplitHostPort(host); err == nil {
|
if h, p, err := net.SplitHostPort(host); err == nil {
|
||||||
host = h
|
host = h
|
||||||
port = p
|
port = p
|
||||||
|
explicitPort = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have a non-default port, join it with the host.
|
// If a port was explicitly provided, join it with the host.
|
||||||
// net.JoinHostPort will handle bracketing for IPv6.
|
// net.JoinHostPort will handle bracketing for IPv6.
|
||||||
if port != "" && !isDefaultPort(scheme, port) {
|
if explicitPort && port != "" {
|
||||||
// Strip existing brackets before calling JoinHostPort, which automatically adds
|
// Strip existing brackets before calling JoinHostPort, which automatically adds
|
||||||
// brackets for IPv6 addresses. This prevents double-bracketing like [[::1]]:8080.
|
// brackets for IPv6 addresses. This prevents double-bracketing like [[::1]]:8080.
|
||||||
// Using Trim handles both well-formed and malformed bracketed hosts.
|
// Using Trim handles both well-formed and malformed bracketed hosts.
|
||||||
@@ -843,7 +832,7 @@ func extractHostHeader(r *http.Request) string {
|
|||||||
return net.JoinHostPort(host, port)
|
return net.JoinHostPort(host, port)
|
||||||
}
|
}
|
||||||
|
|
||||||
// No port or default port was stripped. According to AWS SDK behavior (aws-sdk-go-v2),
|
// No explicit port was provided (or port was empty). According to AWS SDK behavior (aws-sdk-go-v2),
|
||||||
// when a default port is removed from an IPv6 address, the brackets should also be removed.
|
// when a default port is removed from an IPv6 address, the brackets should also be removed.
|
||||||
// This matches AWS S3 signature calculation requirements.
|
// This matches AWS S3 signature calculation requirements.
|
||||||
// Reference: https://github.com/aws/aws-sdk-go-v2/blob/main/aws/signer/internal/v4/host.go
|
// Reference: https://github.com/aws/aws-sdk-go-v2/blob/main/aws/signer/internal/v4/host.go
|
||||||
@@ -855,21 +844,6 @@ func extractHostHeader(r *http.Request) string {
|
|||||||
return host
|
return host
|
||||||
}
|
}
|
||||||
|
|
||||||
func isDefaultPort(scheme, port string) bool {
|
|
||||||
if port == "" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
switch port {
|
|
||||||
case "80":
|
|
||||||
return strings.EqualFold(scheme, "http")
|
|
||||||
case "443":
|
|
||||||
return strings.EqualFold(scheme, "https")
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getScope generate a string of a specific date, an AWS region, and a service.
|
// getScope generate a string of a specific date, an AWS region, and a service.
|
||||||
func getScope(t time.Time, region string, service string) string {
|
func getScope(t time.Time, region string, service string) string {
|
||||||
scope := strings.Join([]string{
|
scope := strings.Join([]string{
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ func TestExtractHostHeader(t *testing.T) {
|
|||||||
forwardedHost: "example.com",
|
forwardedHost: "example.com",
|
||||||
forwardedPort: "80",
|
forwardedPort: "80",
|
||||||
forwardedProto: "http",
|
forwardedProto: "http",
|
||||||
expected: "example.com",
|
expected: "example.com:80",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "X-Forwarded-Host with X-Forwarded-Port (HTTPS standard port 443)",
|
name: "X-Forwarded-Host with X-Forwarded-Port (HTTPS standard port 443)",
|
||||||
@@ -216,7 +216,7 @@ func TestExtractHostHeader(t *testing.T) {
|
|||||||
forwardedHost: "example.com",
|
forwardedHost: "example.com",
|
||||||
forwardedPort: "443",
|
forwardedPort: "443",
|
||||||
forwardedProto: "https",
|
forwardedProto: "https",
|
||||||
expected: "example.com",
|
expected: "example.com:443",
|
||||||
},
|
},
|
||||||
// Issue #6649: X-Forwarded-Host already contains port (Traefik/HAProxy style)
|
// Issue #6649: X-Forwarded-Host already contains port (Traefik/HAProxy style)
|
||||||
{
|
{
|
||||||
@@ -227,6 +227,14 @@ func TestExtractHostHeader(t *testing.T) {
|
|||||||
forwardedProto: "https",
|
forwardedProto: "https",
|
||||||
expected: "127.0.0.1:8433",
|
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:443",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "X-Forwarded-Host with port, no X-Forwarded-Port header",
|
name: "X-Forwarded-Host with port, no X-Forwarded-Port header",
|
||||||
hostHeader: "backend:8333",
|
hostHeader: "backend:8333",
|
||||||
@@ -253,20 +261,20 @@ func TestExtractHostHeader(t *testing.T) {
|
|||||||
expected: "[::1]:8080",
|
expected: "[::1]:8080",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IPv6 address without brackets and standard port, should strip brackets per AWS SDK",
|
name: "IPv6 address without brackets and standard port, should include brackets and port when explicit",
|
||||||
hostHeader: "backend:8333",
|
hostHeader: "backend:8333",
|
||||||
forwardedHost: "::1",
|
forwardedHost: "::1",
|
||||||
forwardedPort: "80",
|
forwardedPort: "80",
|
||||||
forwardedProto: "http",
|
forwardedProto: "http",
|
||||||
expected: "::1",
|
expected: "[::1]:80",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IPv6 address without brackets and standard HTTPS port, should strip brackets per AWS SDK",
|
name: "IPv6 address without brackets and standard HTTPS port, should include brackets and port when explicit",
|
||||||
hostHeader: "backend:8333",
|
hostHeader: "backend:8333",
|
||||||
forwardedHost: "2001:db8::1",
|
forwardedHost: "2001:db8::1",
|
||||||
forwardedPort: "443",
|
forwardedPort: "443",
|
||||||
forwardedProto: "https",
|
forwardedProto: "https",
|
||||||
expected: "2001:db8::1",
|
expected: "[2001:db8::1]:443",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IPv6 address with brackets but no port, should add port",
|
name: "IPv6 address with brackets but no port, should add port",
|
||||||
@@ -277,12 +285,12 @@ func TestExtractHostHeader(t *testing.T) {
|
|||||||
expected: "[2001:db8::1]:8080",
|
expected: "[2001:db8::1]:8080",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IPv6 full address with brackets and default port (should strip port and brackets)",
|
name: "IPv6 full address with brackets and default port (should preserve port if explicit)",
|
||||||
hostHeader: "backend:8333",
|
hostHeader: "backend:8333",
|
||||||
forwardedHost: "[2001:db8:85a3::8a2e:370:7334]:443",
|
forwardedHost: "[2001:db8:85a3::8a2e:370:7334]:443",
|
||||||
forwardedPort: "443",
|
forwardedPort: "443",
|
||||||
forwardedProto: "https",
|
forwardedProto: "https",
|
||||||
expected: "2001:db8:85a3::8a2e:370:7334",
|
expected: "[2001:db8:85a3::8a2e:370:7334]:443",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IPv4-mapped IPv6 address without brackets, should add brackets with port",
|
name: "IPv4-mapped IPv6 address without brackets, should add brackets with port",
|
||||||
|
|||||||
@@ -415,13 +415,13 @@ func TestSignatureV4WithoutProxy(t *testing.T) {
|
|||||||
name: "HTTP with standard port",
|
name: "HTTP with standard port",
|
||||||
host: "backend:80",
|
host: "backend:80",
|
||||||
proto: "http",
|
proto: "http",
|
||||||
expectedHost: "backend",
|
expectedHost: "backend:80",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "HTTPS with standard port",
|
name: "HTTPS with standard port",
|
||||||
host: "backend:443",
|
host: "backend:443",
|
||||||
proto: "https",
|
proto: "https",
|
||||||
expectedHost: "backend",
|
expectedHost: "backend:443",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "HTTP without port",
|
name: "HTTP without port",
|
||||||
@@ -451,13 +451,13 @@ func TestSignatureV4WithoutProxy(t *testing.T) {
|
|||||||
name: "IPv6 HTTP with standard port",
|
name: "IPv6 HTTP with standard port",
|
||||||
host: "[::1]:80",
|
host: "[::1]:80",
|
||||||
proto: "http",
|
proto: "http",
|
||||||
expectedHost: "::1",
|
expectedHost: "[::1]:80",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IPv6 HTTPS with standard port",
|
name: "IPv6 HTTPS with standard port",
|
||||||
host: "[::1]:443",
|
host: "[::1]:443",
|
||||||
proto: "https",
|
proto: "https",
|
||||||
expectedHost: "::1",
|
expectedHost: "[::1]:443",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "IPv6 HTTP without port",
|
name: "IPv6 HTTP without port",
|
||||||
|
|||||||
Reference in New Issue
Block a user