Repository: traefik/traefik (language: go)

Issue:
RedirectScheme middleware returns bad gateway for joined X-Forwarded-Proto header
### Welcome!

- [X] Yes, I've searched similar issues on [GitHub](https://github.com/traefik/traefik/issues) and didn't find any.
- [X] Yes, I've searched similar issues on the [Traefik community forum](https://community.traefik.io) and didn't find any.

### What did you do?

If Traefik is used behind a proxy server it might receive requests containing a `X-Forwarded-Proto` header. If there are multiple proxies involved or one of them is misbehaving it might happen that there are multiple X-Forwarded-Proto headers.
According to the [HTTP/1.1 RFC](https://www.rfc-editor.org/rfc/rfc2616#page-31)
>     Multiple message-header fields with the same field-name MAY be
>     present in a message if and only if the entire field-value for that
>     header field is defined as a comma-separated list [i.e., #(values)].
>     It MUST be possible to combine the multiple header fields into one
>     "field-name: field-value" pair, without changing the semantics of the
>     message, by appending each subsequent field-value to the first, each
>     separated by a comma. The order in which header fields with the same
>     field-name are received is therefore significant to the
>     interpretation of the combined field value, and thus a proxy MUST NOT
>     change the order of these field values when a message is forwarded.
So in that case the values of the X-Forwarded-Proto might be joined together by a proxy in front of Traefik which will result in the header `X-Forwarded-Proto: https, https`.

In #9121 the RedirectScheme middleware got the ability to interpret that header to decide whether it should return a redirect. In those cases it doesn't behave correctly, though.

Example request using curl:
```
❯ curl -i https://<router> -H 'X-Forwarded-Proto: https, https' 
HTTP/2 502
[...]
```

For requests **with** the space between the header values Traefik always returns status code 502 with this error in the debug logs:
```
'502 Bad Gateway' caused by: parse \"https://https, https://<router>/\": invalid character \" \" in host name
```

Note this is only possible with insecure forwarded headers enabled.

Relevant part in the code: https://github.com/traefik/traefik/blob/e54ee89330a800d509da7b11b46a6ecbb331e791/pkg/middlewares/redirect/redirect_scheme.go

### What did you see instead?

Traefik returns a response with status code 502

### What version of Traefik are you using?

2.9.5, problem was introduced in 2.7.2 by #9121

### What is your environment & configuration?

relevant CLI args
```
--entrypoints.http.address=:8000/tcp
--entrypoints.http.forwardedheaders.insecure=true
```

middleware configuration:
```
redirectScheme:
  permanent: true
  scheme: https
```

ingressroute (but also works with ingress or other kinds of configuration):
```
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
  name: httpbin-with-middleware
spec:
  entryPoints:
  - http
  - https
  routes:
  - kind: Rule
    match: Host(`<router>`) && PathPrefix(`/`)
    services:
    - port: 8080
      name: httpbin
    middlewares:
    - name: ingress-controller-redirect-to-https@kubernetescrd
```

### If applicable, please paste the log output in DEBUG level

_No response_


Discussion / hints:
Hello,

the RFC says the values should be separated by a comma, the space between the comma and the value is not allowed by the RFC.

Thanks for the fast response.
Without the space the behaviour is broken as well, though:
```
❯ curl -i https://<router> -H 'X-Forwarded-Proto: https,https' 
HTTP/2 301 
location: https://https,https://<router>
```
And 502 is quite misleading as this is a client and not a server problem if the header is not formatted correctly.
In the case of an illegal request header, wouldn't `400 Bad Request` be the correct response?
`X-Forwarded-Proto` is a mono-valued header.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto

But the 502 or the 301 are not expected.

Fix patch:
diff --git a/pkg/middlewares/redirect/redirect_scheme.go b/pkg/middlewares/redirect/redirect_scheme.go
index 063d6c4bc8..2828aa48ac 100644
--- a/pkg/middlewares/redirect/redirect_scheme.go
+++ b/pkg/middlewares/redirect/redirect_scheme.go
@@ -18,6 +18,12 @@ const (
 	xForwardedProto = "X-Forwarded-Proto"
 )
 
+type redirectScheme struct {
+	http.Handler
+
+	name string
+}
+
 // NewRedirectScheme creates a new RedirectScheme middleware.
 func NewRedirectScheme(ctx context.Context, next http.Handler, conf dynamic.RedirectScheme, name string) (http.Handler, error) {
 	logger := log.FromContext(middlewares.GetLoggerCtx(ctx, name, typeSchemeName))
@@ -33,10 +39,19 @@ func NewRedirectScheme(ctx context.Context, next http.Handler, conf dynamic.Redi
 		port = ":" + conf.Port
 	}
 
-	return newRedirect(next, uriPattern, conf.Scheme+"://${2}"+port+"${4}", conf.Permanent, clientRequestURL, name)
+	rs := &redirectScheme{name: name}
+
+	handler, err := newRedirect(next, uriPattern, conf.Scheme+"://${2}"+port+"${4}", conf.Permanent, rs.clientRequestURL, name)
+	if err != nil {
+		return nil, err
+	}
+
+	rs.Handler = handler
+
+	return rs, nil
 }
 
-func clientRequestURL(req *http.Request) string {
+func (r *redirectScheme) clientRequestURL(req *http.Request) string {
 	scheme := schemeHTTP
 	host, port, err := net.SplitHostPort(req.Host)
 	if err != nil {
@@ -71,12 +86,12 @@ func clientRequestURL(req *http.Request) string {
 		// Given that we're in a middleware that is only used in the context of HTTP(s) requests,
 		// the only possible valid schemes are one of "http" or "https", so we convert back to them.
 		switch {
-		case strings.EqualFold(xProto, "ws"):
+		case strings.EqualFold(xProto, schemeHTTP), strings.EqualFold(xProto, "ws"):
 			scheme = schemeHTTP
-		case strings.EqualFold(xProto, "wss"):
+		case strings.EqualFold(xProto, schemeHTTPS), strings.EqualFold(xProto, "wss"):
 			scheme = schemeHTTPS
 		default:
-			scheme = xProto
+			log.FromContext(middlewares.GetLoggerCtx(req.Context(), r.name, typeSchemeName)).Debugf("invalid X-Forwarded-Proto: %s", xProto)
 		}
 	}
 


Test patch:
diff --git a/pkg/middlewares/redirect/redirect_scheme_test.go b/pkg/middlewares/redirect/redirect_scheme_test.go
index dea1d6265e..07ae542468 100644
--- a/pkg/middlewares/redirect/redirect_scheme_test.go
+++ b/pkg/middlewares/redirect/redirect_scheme_test.go
@@ -72,7 +72,7 @@ func TestRedirectSchemeHandler(t *testing.T) {
 			headers: map[string]string{
 				"X-Forwarded-Proto": "bar",
 			},
-			expectedURL:    "https://bar://foo",
+			expectedURL:    "https://foo",
 			expectedStatus: http.StatusFound,
 		},
 		{