You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/09/03 16:10:35 UTC

(maven-wagon) branch wagon-3.x updated: Keep configured HTTP headers on the repository origin across redirects

This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch wagon-3.x
in repository https://gitbox.apache.org/repos/asf/maven-wagon.git


The following commit(s) were added to refs/heads/wagon-3.x by this push:
     new daeba579 Keep configured HTTP headers on the repository origin across redirects
daeba579 is described below

commit daeba579b3e764ea6156be729371badb98bc7d3a
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Wed Sep 2 01:30:13 2026 +0200

    Keep configured HTTP headers on the repository origin across redirects
    
    Fixes #956. HttpClient copies the headers of the original request onto
    every redirected request, so headers configured for a repository through
    setHttpHeaders or the method configuration followed redirects to other
    hosts. The wagon now records the repository origin and the configured
    header names in the request context, and an interceptor on the shared
    client removes those headers from any request whose target differs from
    the origin in scheme, host or effective port. User-Agent is exempt.
    Setting -Dmaven.wagon.http.originScopedHeaders=false restores the
    previous behaviour.
    
    (cherry picked from commit c345af7242c6f1fdb424c0cec1d5ed207b5f2e29)
---
 .../apache/maven/wagon/http/HttpWagonTestCase.java | 137 +++++++++++++++++++++
 .../providers/http/LightweightHttpWagonTest.java   |   9 ++
 .../wagon/shared/http/AbstractHttpClientWagon.java |  37 ++++++
 .../http/OriginScopedHeadersInterceptor.java       |  99 +++++++++++++++
 4 files changed, 282 insertions(+)

diff --git a/wagon-provider-test/src/main/java/org/apache/maven/wagon/http/HttpWagonTestCase.java b/wagon-provider-test/src/main/java/org/apache/maven/wagon/http/HttpWagonTestCase.java
index 2eef9b4e..b4185976 100644
--- a/wagon-provider-test/src/main/java/org/apache/maven/wagon/http/HttpWagonTestCase.java
+++ b/wagon-provider-test/src/main/java/org/apache/maven/wagon/http/HttpWagonTestCase.java
@@ -88,6 +88,7 @@
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
 
 /**
  *
@@ -630,6 +631,142 @@ public void testRedirectGet() throws Exception {
         }
     }
 
+    /**
+     * Whether the provider under test keeps headers configured for a repository on that repository's origin when
+     * following redirects. Providers that cannot do so return {@code false} and skip the corresponding tests.
+     */
+    protected boolean supportsOriginScopedHeaders() {
+        return true;
+    }
+
+    @Test
+    public void testRedirectToOtherOriginDropsConfiguredHeaders() throws Exception {
+        assumeTrue(supportsOriginScopedHeaders());
+        StreamingWagon wagon = (StreamingWagon) getWagon();
+
+        Properties headers = new Properties();
+        headers.setProperty("X-Wagon-Test", "secret");
+        Method setHttpHeaders = wagon.getClass().getMethod("setHttpHeaders", Properties.class);
+        setHttpHeaders.invoke(wagon, headers);
+
+        Server realServer = new Server();
+        TestHeaderHandler handler = new TestHeaderHandler();
+        realServer.setHandler(handler);
+        addConnector(realServer);
+        realServer.start();
+
+        Server redirectServer = new Server();
+        addConnector(redirectServer);
+        TestHeaderHandler redirectHandler = new TestHeaderHandler();
+        // the redirecting server sits on another port and therefore is another origin than the real server
+        RedirectRecordingHandler recordingRedirect = new RedirectRecordingHandler(
+                redirectHandler, getRedirectProtocol() + "://localhost:" + getLocalPort(realServer));
+        redirectServer.setHandler(recordingRedirect);
+        redirectServer.start();
+
+        wagon.connect(new Repository("id", getRepositoryUrl(redirectServer)));
+
+        File tmpResult = File.createTempFile("foo", "get");
+        try {
+            wagon.get("resource", tmpResult);
+            assertEquals("Hello, World!", new String(Files.readAllBytes(tmpResult.toPath())));
+
+            assertEquals("secret", redirectHandler.headers.get("X-Wagon-Test"), "configured header on origin");
+            assertFalse(
+                    handler.headers.containsKey("X-Wagon-Test"),
+                    "configured header must not be sent to the redirect target on another origin");
+        } finally {
+            wagon.disconnect();
+            redirectServer.stop();
+            realServer.stop();
+            tmpResult.delete();
+        }
+    }
+
+    @Test
+    public void testRedirectWithinOriginKeepsConfiguredHeaders() throws Exception {
+        assumeTrue(supportsOriginScopedHeaders());
+        StreamingWagon wagon = (StreamingWagon) getWagon();
+
+        Properties headers = new Properties();
+        headers.setProperty("X-Wagon-Test", "secret");
+        Method setHttpHeaders = wagon.getClass().getMethod("setHttpHeaders", Properties.class);
+        setHttpHeaders.invoke(wagon, headers);
+
+        Server server = new Server();
+        addConnector(server);
+        TestHeaderHandler handler = new TestHeaderHandler();
+        // redirect to a different path on the same server: same origin
+        RedirectRecordingHandler redirecting = new RedirectRecordingHandler(handler, null);
+        server.setHandler(redirecting);
+        server.start();
+
+        wagon.connect(new Repository("id", getRepositoryUrl(server)));
+
+        File tmpResult = File.createTempFile("foo", "get");
+        try {
+            wagon.get("resource", tmpResult);
+            assertEquals("Hello, World!", new String(Files.readAllBytes(tmpResult.toPath())));
+
+            assertEquals(
+                    "secret",
+                    handler.headers.get("X-Wagon-Test"),
+                    "configured header must be kept on a redirect within the same origin");
+        } finally {
+            wagon.disconnect();
+            server.stop();
+            tmpResult.delete();
+        }
+    }
+
+    private String getRedirectProtocol() {
+        String protocol = getProtocol();
+        // protocol is wagon protocol but in fact dav is http(s)
+        if (protocol.equals("dav")) {
+            return "http";
+        }
+        if (protocol.equals("davs")) {
+            return "https";
+        }
+        return protocol;
+    }
+
+    /**
+     * Answers a request for {@code /resource} with a {@code 303 See Other} to {@code /redirected/resource}, either on
+     * the given other server or, when no base URL is given, on the same server, and delegates every other request to
+     * the given handler so that the headers of the redirected request can be inspected.
+     */
+    private static class RedirectRecordingHandler extends AbstractHandler {
+        private final AbstractHandler delegate;
+
+        private final String redirectBaseUrl;
+
+        RedirectRecordingHandler(AbstractHandler delegate, String redirectBaseUrl) {
+            this.delegate = delegate;
+            this.redirectBaseUrl = redirectBaseUrl;
+        }
+
+        public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
+                throws IOException, ServletException {
+            if (!request.getRequestURI().startsWith("/redirected")) {
+                delegate.handle(target, baseRequest, request, response);
+                if (redirectBaseUrl == null) {
+                    response.reset();
+                    response.setStatus(HttpServletResponse.SC_SEE_OTHER);
+                    response.setHeader("Location", "/redirected" + request.getRequestURI());
+                    baseRequest.setHandled(true);
+                    return;
+                }
+                response.reset();
+                response.setStatus(HttpServletResponse.SC_SEE_OTHER);
+                response.setHeader("Location", redirectBaseUrl + "/redirected" + request.getRequestURI());
+                baseRequest.setHandled(true);
+                return;
+            }
+            delegate.handle(target, baseRequest, request, response);
+        }
+    }
+
     @Test
     public void testRedirectPutFromStreamWithFullUrl() throws Exception {
         Server realServer = new Server();
diff --git a/wagon-providers/wagon-http-lightweight/src/test/java/org/apache/maven/wagon/providers/http/LightweightHttpWagonTest.java b/wagon-providers/wagon-http-lightweight/src/test/java/org/apache/maven/wagon/providers/http/LightweightHttpWagonTest.java
index c34c509c..dd2a9d47 100644
--- a/wagon-providers/wagon-http-lightweight/src/test/java/org/apache/maven/wagon/providers/http/LightweightHttpWagonTest.java
+++ b/wagon-providers/wagon-http-lightweight/src/test/java/org/apache/maven/wagon/providers/http/LightweightHttpWagonTest.java
@@ -52,6 +52,15 @@ protected void setHttpConfiguration(StreamingWagon wagon, Properties headers, Pr
         ((LightweightHttpWagon) wagon).setHttpHeaders(headers);
     }
 
+    /**
+     * The lightweight provider relies on HttpURLConnection following redirects and cannot keep configured headers
+     * from being sent to another origin.
+     */
+    @Override
+    protected boolean supportsOriginScopedHeaders() {
+        return false;
+    }
+
     @Override
     protected boolean supportPreemptiveAuthenticationGet() {
         return false;
diff --git a/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java
index da1aa3ca..a93d8217 100644
--- a/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java
+++ b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/AbstractHttpClientWagon.java
@@ -37,10 +37,12 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Properties;
+import java.util.Set;
 import java.util.TimeZone;
 import java.util.concurrent.TimeUnit;
 
@@ -506,6 +508,7 @@ private static CloseableHttpClient createClient() {
                 .setServiceUnavailableRetryStrategy(createServiceUnavailableRetryStrategy())
                 .setDefaultAuthSchemeRegistry(createAuthSchemeRegistry())
                 .setRedirectStrategy(new WagonRedirectStrategy())
+                .addInterceptorLast(new OriginScopedHeadersInterceptor())
                 .build();
     }
 
@@ -869,6 +872,14 @@ protected CloseableHttpResponse execute(HttpUriRequest httpMethod) throws HttpEx
         localContext.setCredentialsProvider(credentialsProvider);
         localContext.setAuthCache(authCache);
         localContext.setRequestConfig(requestConfigBuilder.build());
+        if (ORIGIN_SCOPED_HEADERS) {
+            // configured headers stay with the repository they were configured for, see
+            // OriginScopedHeadersInterceptor: a redirect to another origin does not carry them
+            localContext.setAttribute(
+                    OriginScopedHeadersInterceptor.ORIGIN,
+                    new HttpHost(repo.getHost(), repo.getPort(), repo.getProtocol()));
+            localContext.setAttribute(OriginScopedHeadersInterceptor.HEADER_NAMES, configuredHeaderNames(config));
+        }
 
         if (config != null && config.isUsePreemptive()) {
             HttpHost targetHost = new HttpHost(repo.getHost(), repo.getPort(), repo.getProtocol());
@@ -899,6 +910,32 @@ protected CloseableHttpResponse execute(HttpUriRequest httpMethod) throws HttpEx
         return httpClient.execute(httpMethod, localContext);
     }
 
+    /**
+     * Whether headers configured for a repository are sent only to that repository's origin (the default). Set the
+     * system property {@code maven.wagon.http.originScopedHeaders} to {@code false} to send them on redirects to
+     * other hosts as well, as earlier versions did.
+     */
+    private static final boolean ORIGIN_SCOPED_HEADERS =
+            Boolean.parseBoolean(System.getProperty("maven.wagon.http.originScopedHeaders", "true"));
+
+    private Set<String> configuredHeaderNames(HttpMethodConfiguration config) {
+        Set<String> names = new HashSet<>();
+        if (httpHeaders != null) {
+            for (Object key : httpHeaders.keySet()) {
+                names.add(String.valueOf(key));
+            }
+        }
+        Header[] headers = config == null ? null : config.asRequestHeaders();
+        if (headers != null) {
+            for (Header header : headers) {
+                names.add(header.getName());
+            }
+        }
+        // the User-Agent identifies the client, not the repository, and is safe to keep on any host
+        names.remove(HTTP.USER_AGENT);
+        return names;
+    }
+
     public void setHeaders(HttpUriRequest method) {
         HttpMethodConfiguration config =
                 httpConfiguration == null ? null : httpConfiguration.getMethodConfiguration(method);
diff --git a/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/OriginScopedHeadersInterceptor.java b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/OriginScopedHeadersInterceptor.java
new file mode 100644
index 00000000..2596e893
--- /dev/null
+++ b/wagon-providers/wagon-http-shared/src/main/java/org/apache/maven/wagon/shared/http/OriginScopedHeadersInterceptor.java
@@ -0,0 +1,99 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.wagon.shared.http;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.http.HttpHost;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpCoreContext;
+
+/**
+ * Removes the headers configured for a repository from requests that are not sent to that repository's origin.
+ * <p>
+ * The HTTP client copies the headers of the original request onto every redirected request, so headers configured
+ * through {@code setHttpHeaders} or the method configuration (for example an {@code Authorization} header carrying a
+ * token) would otherwise be sent to whatever host a repository redirects to. The wagon records the repository origin
+ * and the configured header names in the request context; this interceptor drops those headers when the target host
+ * of the request being sent differs from the origin in scheme, host or effective port. When the target host cannot be
+ * determined the headers are dropped as well.
+ */
+public class OriginScopedHeadersInterceptor implements HttpRequestInterceptor {
+    /**
+     * Context attribute holding the {@link HttpHost} of the repository the request was made for.
+     */
+    public static final String ORIGIN = OriginScopedHeadersInterceptor.class.getName() + ".origin";
+
+    /**
+     * Context attribute holding the {@code Collection<String>} of header names that must stay on the origin.
+     */
+    public static final String HEADER_NAMES = OriginScopedHeadersInterceptor.class.getName() + ".headerNames";
+
+    @Override
+    public void process(HttpRequest request, HttpContext context) {
+        if (context == null) {
+            return;
+        }
+        Set<String> headerNames = headerNames(context.getAttribute(HEADER_NAMES));
+        if (headerNames.isEmpty()) {
+            return;
+        }
+        Object origin = context.getAttribute(ORIGIN);
+        Object target = context.getAttribute(HttpCoreContext.HTTP_TARGET_HOST);
+        if (origin instanceof HttpHost
+                && target instanceof HttpHost
+                && isSameOrigin((HttpHost) origin, (HttpHost) target)) {
+            return;
+        }
+        for (String headerName : headerNames) {
+            request.removeHeaders(headerName);
+        }
+    }
+
+    private static Set<String> headerNames(Object attribute) {
+        if (!(attribute instanceof Collection)) {
+            return Collections.emptySet();
+        }
+        Set<String> names = new HashSet<>();
+        for (Object name : (Collection<?>) attribute) {
+            if (name != null) {
+                names.add(String.valueOf(name));
+            }
+        }
+        return names;
+    }
+
+    static boolean isSameOrigin(HttpHost origin, HttpHost target) {
+        return origin.getSchemeName().equalsIgnoreCase(target.getSchemeName())
+                && origin.getHostName().equalsIgnoreCase(target.getHostName())
+                && effectivePort(origin) == effectivePort(target);
+    }
+
+    static int effectivePort(HttpHost host) {
+        if (host.getPort() >= 0) {
+            return host.getPort();
+        }
+        return "https".equalsIgnoreCase(host.getSchemeName()) ? 443 : 80;
+    }
+}