diff --git a/.github/workflows/builds.yml b/.github/workflows/builds.yml
index 6a59bde6c..2586cf3c6 100644
--- a/.github/workflows/builds.yml
+++ b/.github/workflows/builds.yml
@@ -5,34 +5,50 @@ on:
- cron: '0 12 * * *'
jobs:
+ Verify:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Grant Permission
+ run: chmod +x ./mvnw
+ - uses: actions/setup-java@v4
+ with:
+ distribution: 'corretto'
+ java-version: '11'
+ - name: Verify
+ run: ./mvnw -B -ntp clean verify -DskipTests -Dgpg.skip=true
+
RunOnLinux:
runs-on: ubuntu-latest
+ needs: Verify
steps:
- uses: actions/checkout@v4
- name: Grant Permission
- run: sudo chmod +x ./mvnw
+ run: chmod +x ./mvnw
- uses: actions/setup-java@v4
with:
distribution: 'corretto'
java-version: '11'
- name: Run Tests
- run: ./mvnw -B -ntp clean test
+ run: ./mvnw -B -ntp test
RunOnMacOs:
runs-on: macos-latest
+ needs: Verify
steps:
- uses: actions/checkout@v4
- name: Grant Permission
- run: sudo chmod +x ./mvnw
+ run: chmod +x ./mvnw
- uses: actions/setup-java@v4
with:
distribution: 'corretto'
java-version: '11'
- name: Run Tests
- run: ./mvnw -B -ntp clean test
+ run: ./mvnw -B -ntp test
RunOnWindows:
runs-on: windows-latest
+ needs: Verify
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
@@ -40,4 +56,4 @@ jobs:
distribution: 'corretto'
java-version: '11'
- name: Run Tests
- run: ./mvnw.cmd -B -ntp clean test
+ run: ./mvnw.cmd -B -ntp test
diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java
index 12dc93d7d..954628b3d 100644
--- a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java
+++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java
@@ -375,6 +375,13 @@ public interface AsyncHttpClientConfig {
int getIoThreadsCount();
+ /**
+ * Indicates whether the Authorization header should be stripped during redirects to a different domain.
+ *
+ * @return true if the Authorization header should be stripped, false otherwise.
+ */
+ boolean isStripAuthorizationOnRedirect();
+
enum ResponseBodyPartFactory {
EAGER {
diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java
index e72235c17..1c7dbf37f 100644
--- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java
+++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java
@@ -127,6 +127,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig {
private final boolean keepEncodingHeader;
private final ProxyServerSelector proxyServerSelector;
private final boolean validateResponseHeaders;
+ private final boolean stripAuthorizationOnRedirect;
// websockets
private final boolean aggregateWebSocketFrameFragments;
@@ -219,6 +220,7 @@ private DefaultAsyncHttpClientConfig(// http
boolean validateResponseHeaders,
boolean aggregateWebSocketFrameFragments,
boolean enablewebSocketCompression,
+ boolean stripAuthorizationOnRedirect,
// timeouts
Duration connectTimeout,
@@ -307,6 +309,7 @@ private DefaultAsyncHttpClientConfig(// http
this.keepEncodingHeader = keepEncodingHeader;
this.proxyServerSelector = proxyServerSelector;
this.validateResponseHeaders = validateResponseHeaders;
+ this.stripAuthorizationOnRedirect = stripAuthorizationOnRedirect;
// websocket
this.aggregateWebSocketFrameFragments = aggregateWebSocketFrameFragments;
@@ -564,6 +567,11 @@ public boolean isValidateResponseHeaders() {
return validateResponseHeaders;
}
+ @Override
+ public boolean isStripAuthorizationOnRedirect() {
+ return stripAuthorizationOnRedirect;
+ }
+
// ssl
@Override
public boolean isUseOpenSsl() {
@@ -800,6 +808,7 @@ public static class Builder {
private boolean useProxySelector = defaultUseProxySelector();
private boolean useProxyProperties = defaultUseProxyProperties();
private boolean validateResponseHeaders = defaultValidateResponseHeaders();
+ private boolean stripAuthorizationOnRedirect = false; // default value
// websocket
private boolean aggregateWebSocketFrameFragments = defaultAggregateWebSocketFrameFragments();
@@ -891,6 +900,7 @@ public Builder(AsyncHttpClientConfig config) {
keepEncodingHeader = config.isKeepEncodingHeader();
proxyServerSelector = config.getProxyServerSelector();
validateResponseHeaders = config.isValidateResponseHeaders();
+ stripAuthorizationOnRedirect = config.isStripAuthorizationOnRedirect();
// websocket
aggregateWebSocketFrameFragments = config.isAggregateWebSocketFrameFragments();
@@ -1079,6 +1089,11 @@ public Builder setUseProxyProperties(boolean useProxyProperties) {
return this;
}
+ public Builder setStripAuthorizationOnRedirect(boolean value) {
+ stripAuthorizationOnRedirect = value;
+ return this;
+ }
+
// websocket
public Builder setAggregateWebSocketFrameFragments(boolean aggregateWebSocketFrameFragments) {
this.aggregateWebSocketFrameFragments = aggregateWebSocketFrameFragments;
@@ -1444,6 +1459,7 @@ public DefaultAsyncHttpClientConfig build() {
validateResponseHeaders,
aggregateWebSocketFrameFragments,
enablewebSocketCompression,
+ stripAuthorizationOnRedirect,
connectTimeout,
requestTimeout,
readTimeout,
diff --git a/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java b/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java
index c5e4a97d0..c29c0f33d 100755
--- a/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java
+++ b/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java
@@ -187,10 +187,10 @@ public boolean cancel(boolean force) {
return false;
}
- // cancel could happen before channel was attached
- if (channel != null) {
- Channels.setDiscard(channel);
- Channels.silentlyCloseChannel(channel);
+ final Channel ch = channel; //atomic read, so that it won't end up in TOCTOU
+ if (ch != null) {
+ Channels.setDiscard(ch);
+ Channels.silentlyCloseChannel(ch);
}
if (ON_THROWABLE_CALLED_FIELD.getAndSet(this, 1) == 0) {
diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java
index e60495f80..40628a7e5 100644
--- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java
+++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Redirect30xInterceptor.java
@@ -35,7 +35,6 @@
import org.slf4j.LoggerFactory;
import java.util.HashSet;
-import java.util.List;
import java.util.Set;
import static io.netty.handler.codec.http.HttpHeaderNames.AUTHORIZATION;
@@ -73,11 +72,13 @@ public class Redirect30xInterceptor {
private final AsyncHttpClientConfig config;
private final NettyRequestSender requestSender;
private final MaxRedirectException maxRedirectException;
+ private final boolean stripAuthorizationOnRedirect;
Redirect30xInterceptor(ChannelManager channelManager, AsyncHttpClientConfig config, NettyRequestSender requestSender) {
this.channelManager = channelManager;
this.config = config;
this.requestSender = requestSender;
+ stripAuthorizationOnRedirect = config.isStripAuthorizationOnRedirect(); // New flag
maxRedirectException = unknownStackTrace(new MaxRedirectException("Maximum redirect reached: " + config.getMaxRedirects()),
Redirect30xInterceptor.class, "exitAfterHandlingRedirect");
}
@@ -127,7 +128,7 @@ public boolean exitAfterHandlingRedirect(Channel channel, NettyResponseFuture>
}
}
- requestBuilder.setHeaders(propagatedHeaders(request, realm, keepBody));
+ requestBuilder.setHeaders(propagatedHeaders(request, realm, keepBody, stripAuthorizationOnRedirect));
// in case of a redirect from HTTP to HTTPS, future
// attributes might change
@@ -180,7 +181,7 @@ public boolean exitAfterHandlingRedirect(Channel channel, NettyResponseFuture>
return false;
}
- private static HttpHeaders propagatedHeaders(Request request, Realm realm, boolean keepBody) {
+ private static HttpHeaders propagatedHeaders(Request request, Realm realm, boolean keepBody, boolean stripAuthorization) {
HttpHeaders headers = request.getHeaders()
.remove(HOST)
.remove(CONTENT_LENGTH);
@@ -189,7 +190,7 @@ private static HttpHeaders propagatedHeaders(Request request, Realm realm, boole
headers.remove(CONTENT_TYPE);
}
- if (realm != null && realm.getScheme() == AuthScheme.NTLM) {
+ if (stripAuthorization || (realm != null && realm.getScheme() == AuthScheme.NTLM)) {
headers.remove(AUTHORIZATION)
.remove(PROXY_AUTHORIZATION);
}
diff --git a/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientConfigTest.java b/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientConfigTest.java
new file mode 100644
index 000000000..1548d6812
--- /dev/null
+++ b/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientConfigTest.java
@@ -0,0 +1,30 @@
+package org.asynchttpclient;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class DefaultAsyncHttpClientConfigTest {
+ @Test
+ void testStripAuthorizationOnRedirect_DefaultIsFalse() {
+ DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder().build();
+ assertFalse(config.isStripAuthorizationOnRedirect(), "Default should be false");
+ }
+
+ @Test
+ void testStripAuthorizationOnRedirect_SetTrue() {
+ DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder()
+ .setStripAuthorizationOnRedirect(true)
+ .build();
+ assertTrue(config.isStripAuthorizationOnRedirect(), "Should be true when set");
+ }
+
+ @Test
+ void testStripAuthorizationOnRedirect_SetFalse() {
+ DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder()
+ .setStripAuthorizationOnRedirect(false)
+ .build();
+ assertFalse(config.isStripAuthorizationOnRedirect(), "Should be false when set to false");
+ }
+}
diff --git a/client/src/test/java/org/asynchttpclient/StripAuthorizationOnRedirectHttpTest.java b/client/src/test/java/org/asynchttpclient/StripAuthorizationOnRedirectHttpTest.java
new file mode 100644
index 000000000..08c150c08
--- /dev/null
+++ b/client/src/test/java/org/asynchttpclient/StripAuthorizationOnRedirectHttpTest.java
@@ -0,0 +1,95 @@
+package org.asynchttpclient;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+import com.sun.net.httpserver.HttpServer;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import java.net.InetSocketAddress;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+public class StripAuthorizationOnRedirectHttpTest {
+ private static HttpServer server;
+ private static int port;
+ private static volatile String lastAuthHeader;
+
+ @BeforeAll
+ public static void startServer() throws Exception {
+ server = HttpServer.create(new InetSocketAddress(0), 0);
+ port = server.getAddress().getPort();
+ server.createContext("/redirect", new RedirectHandler());
+ server.createContext("/final", new FinalHandler());
+ server.start();
+ }
+
+ @AfterAll
+ public static void stopServer() {
+ server.stop(0);
+ }
+
+ static class RedirectHandler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange exchange) {
+ String auth = exchange.getRequestHeaders().getFirst("Authorization");
+ lastAuthHeader = auth;
+ exchange.getResponseHeaders().add("Location", "http://localhost/" + port + "/final");
+ try {
+ exchange.sendResponseHeaders(302, -1);
+ } catch (Exception ignored) {
+ }
+ exchange.close();
+ }
+ }
+
+ static class FinalHandler implements HttpHandler {
+ @Override
+ public void handle(HttpExchange exchange) {
+ String auth = exchange.getRequestHeaders().getFirst("Authorization");
+ lastAuthHeader = auth;
+ try {
+ exchange.sendResponseHeaders(200, 0);
+ exchange.getResponseBody().close();
+ } catch (Exception ignored) {
+ }
+ exchange.close();
+ }
+ }
+
+ @Test
+ void testAuthHeaderPropagatedByDefault() throws Exception {
+ DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder()
+ .setFollowRedirect(true)
+ .build();
+ try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) {
+ lastAuthHeader = null;
+ client.prepareGet("http://localhost/" + port + "/redirect")
+ .setHeader("Authorization", "Bearer testtoken")
+ .execute()
+ .get(5, TimeUnit.SECONDS);
+ // By default, Authorization header is propagated to /final
+ assertEquals("Bearer testtoken", lastAuthHeader, "Authorization header should be present on redirect by default");
+ }
+ }
+
+ @Test
+ void testAuthHeaderStrippedWhenEnabled() throws Exception {
+ DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder()
+ .setFollowRedirect(true)
+ .setStripAuthorizationOnRedirect(true)
+ .build();
+ try (DefaultAsyncHttpClient client = new DefaultAsyncHttpClient(config)) {
+ lastAuthHeader = null;
+ client.prepareGet("http://localhost/" + port + "/redirect")
+ .setHeader("Authorization", "Bearer testtoken")
+ .execute()
+ .get(5, TimeUnit.SECONDS);
+ // When enabled, Authorization header should be stripped on /final
+ assertNull(lastAuthHeader, "Authorization header should be stripped on redirect when enabled");
+ }
+ }
+}
diff --git a/pom.xml b/pom.xml
index 70d09ac53..ee1c2308c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -112,7 +112,7 @@
io.github.nettyplus
netty-leak-detector-junit-extension
- 0.0.6
+ 0.0.8
@@ -422,10 +422,38 @@
--pinentry-mode
loopback
+ false
+
+
+ com.github.siom79.japicmp
+ japicmp-maven-plugin
+ 0.23.1
+
+
+ RELEASE
+ ${project.version}
+
+
+ true
+ true
+ true
+ false
+ public
+
+
+
+
+
+ cmp
+
+ verify
+
+
+