diff --git a/.github/dependabot.yml b/.github/dependabot.yml
deleted file mode 100644
index f4538d3c7..000000000
--- a/.github/dependabot.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-# To get started with Dependabot version updates, you'll need to specify which
-# package ecosystems to update and where the package manifests are located.
-# Please see the documentation for all configuration options:
-# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
-
-version: 2
-updates:
- - package-ecosystem: "maven"
- directories:
- - "/"
- schedule:
- interval: "daily"
- - package-ecosystem: "github-actions"
- directories:
- - "/"
- schedule:
- interval: "daily"
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/.github/workflows/release.yml b/.github/workflows/release.yml
index 8014135c2..b175fa865 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -27,7 +27,7 @@ jobs:
run: rm -f /home/runner/.m2/settings.xml
- name: Maven Settings
- uses: s4u/maven-settings-action@v3.0.0
+ uses: s4u/maven-settings-action@v3.1.0
with:
servers: |
[{
@@ -37,7 +37,7 @@ jobs:
}]
- name: Import GPG
- uses: crazy-max/ghaction-import-gpg@v6.2.0
+ uses: crazy-max/ghaction-import-gpg@v6.3.0
with:
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
passphrase: ${{ secrets.GPG_PASSPHRASE }}
diff --git a/README.md b/README.md
index 21a20ebbe..0272134ed 100644
--- a/README.md
+++ b/README.md
@@ -20,7 +20,7 @@ Maven:
org.asynchttpclient
async-http-client
- 3.0.0
+ 3.0.2
```
@@ -28,7 +28,7 @@ Maven:
Gradle:
```groovy
dependencies {
- implementation 'org.asynchttpclient:async-http-client:3.0.0'
+ implementation 'org.asynchttpclient:async-http-client:3.0.2'
}
```
diff --git a/client/pom.xml b/client/pom.xml
index a31faffad..733f20b51 100644
--- a/client/pom.xml
+++ b/client/pom.xml
@@ -19,7 +19,7 @@
org.asynchttpclient
async-http-client-project
- 3.0.0
+ 3.0.2
4.0.0
@@ -31,8 +31,8 @@
org.asynchttpclient.client
11.0.24
- 10.1.30
- 2.16.1
+ 10.1.40
+ 2.18.0
4.11.0
3.0
2.1.0
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/DefaultAsyncHttpClient.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java
index 1f616c328..3b417a5a3 100644
--- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java
+++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java
@@ -235,7 +235,7 @@ public ListenableFuture executeRequest(Request request, AsyncHandler h
if (!cookies.isEmpty()) {
RequestBuilder requestBuilder = request.toBuilder();
for (Cookie cookie : cookies) {
- requestBuilder.addOrReplaceCookie(cookie);
+ requestBuilder.addCookieIfUnset(cookie);
}
request = requestBuilder.build();
}
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/RequestBuilderBase.java b/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java
index 9f5cf9e5e..dbc5e4144 100644
--- a/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java
+++ b/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java
@@ -323,6 +323,21 @@ public T addCookie(Cookie cookie) {
* @return this
*/
public T addOrReplaceCookie(Cookie cookie) {
+ return maybeAddOrReplaceCookie(cookie, true);
+ }
+
+ /**
+ * Add a cookie based on its name, if it does not exist yet. Cookies that
+ * are already set will be ignored.
+ *
+ * @param cookie the new cookie
+ * @return this
+ */
+ public T addCookieIfUnset(Cookie cookie) {
+ return maybeAddOrReplaceCookie(cookie, false);
+ }
+
+ private T maybeAddOrReplaceCookie(Cookie cookie, boolean allowReplace) {
String cookieKey = cookie.name();
boolean replace = false;
int index = 0;
@@ -335,10 +350,10 @@ public T addOrReplaceCookie(Cookie cookie) {
index++;
}
- if (replace) {
- cookies.set(index, cookie);
- } else {
+ if (!replace) {
cookies.add(cookie);
+ } else if (allowReplace) {
+ cookies.set(index, cookie);
}
return asDerivedType();
}
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/HttpHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java
index 06ec46a2b..99a23c7e9 100755
--- a/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java
+++ b/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java
@@ -21,6 +21,7 @@
import io.netty.handler.codec.DecoderResultProvider;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaders;
+import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.LastHttpContent;
@@ -32,6 +33,7 @@
import org.asynchttpclient.netty.NettyResponseStatus;
import org.asynchttpclient.netty.channel.ChannelManager;
import org.asynchttpclient.netty.request.NettyRequestSender;
+import org.asynchttpclient.util.HttpConstants.ResponseStatusCodes;
import java.io.IOException;
import java.net.InetSocketAddress;
@@ -43,8 +45,11 @@ public HttpHandler(AsyncHttpClientConfig config, ChannelManager channelManager,
super(config, channelManager, requestSender);
}
- private static boolean abortAfterHandlingStatus(AsyncHandler> handler, NettyResponseStatus status) throws Exception {
- return handler.onStatusReceived(status) == State.ABORT;
+ private static boolean abortAfterHandlingStatus(AsyncHandler> handler, HttpMethod httpMethod, NettyResponseStatus status) throws Exception {
+ // For non-200 response of a CONNECT request, it's still unconnected.
+ // We need to either close the connection or reuse it but send CONNECT request again.
+ // The former one is easier or we have to attach more state to Channel.
+ return handler.onStatusReceived(status) == State.ABORT || httpMethod == HttpMethod.CONNECT && status.getStatusCode() != ResponseStatusCodes.OK_200;
}
private static boolean abortAfterHandlingHeaders(AsyncHandler> handler, HttpHeaders responseHeaders) throws Exception {
@@ -61,7 +66,7 @@ private void handleHttpResponse(final HttpResponse response, final Channel chann
HttpHeaders responseHeaders = response.headers();
if (!interceptors.exitAfterIntercept(channel, future, handler, response, status, responseHeaders)) {
- boolean abort = abortAfterHandlingStatus(handler, status) || abortAfterHandlingHeaders(handler, responseHeaders);
+ boolean abort = abortAfterHandlingStatus(handler, httpRequest.method(), status) || abortAfterHandlingHeaders(handler, responseHeaders);
if (abort) {
finishUpdate(future, channel, true);
}
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 51e7c8a9b..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
@@ -142,11 +143,8 @@ public boolean exitAfterHandlingRedirect(Channel channel, NettyResponseFuture>
CookieStore cookieStore = config.getCookieStore();
if (cookieStore != null) {
// Update request's cookies assuming that cookie store is already updated by Interceptors
- List cookies = cookieStore.get(newUri);
- if (!cookies.isEmpty()) {
- for (Cookie cookie : cookies) {
- requestBuilder.addOrReplaceCookie(cookie);
- }
+ for (Cookie cookie : cookieStore.get(newUri)) {
+ requestBuilder.addCookieIfUnset(cookie);
}
}
@@ -183,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);
@@ -192,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/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java
index 9fff868b2..b66dd713d 100755
--- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java
+++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java
@@ -97,6 +97,13 @@ public NettyRequestSender(AsyncHttpClientConfig config, ChannelManager channelMa
requestFactory = new NettyRequestFactory(config);
}
+ // needConnect returns true if the request is secure/websocket and a HTTP proxy is set
+ private boolean needConnect(final Request request, final ProxyServer proxyServer) {
+ return proxyServer != null
+ && proxyServer.getProxyType().isHttp()
+ && (request.getUri().isSecured() || request.getUri().isWebSocket());
+ }
+
public ListenableFuture sendRequest(final Request request, final AsyncHandler asyncHandler, NettyResponseFuture future) {
if (isClosed()) {
throw new IllegalStateException("Closed");
@@ -106,9 +113,7 @@ public ListenableFuture sendRequest(final Request request, final AsyncHan
ProxyServer proxyServer = getProxyServer(config, request);
// WebSockets use connect tunneling to work with proxies
- if (proxyServer != null && proxyServer.getProxyType().isHttp() &&
- (request.getUri().isSecured() || request.getUri().isWebSocket()) &&
- !isConnectAlreadyDone(request, future)) {
+ if (needConnect(request, proxyServer) && !isConnectAlreadyDone(request, future)) {
// Proxy with HTTPS or WebSocket: CONNECT for sure
if (future != null && future.isConnectAllowed()) {
// Perform CONNECT
@@ -125,6 +130,8 @@ public ListenableFuture sendRequest(final Request request, final AsyncHan
private static boolean isConnectAlreadyDone(Request request, NettyResponseFuture> future) {
return future != null
+ // If the channel can't be reused or closed, a CONNECT is still required
+ && future.isReuseChannel() && Channels.isChannelActive(future.channel())
&& future.getNettyRequest() != null
&& future.getNettyRequest().getHttpRequest().method() == HttpMethod.CONNECT
&& !request.getMethod().equals(CONNECT);
@@ -137,11 +144,19 @@ private static boolean isConnectAlreadyDone(Request request, NettyResponseFuture
*/
private ListenableFuture sendRequestWithCertainForceConnect(Request request, AsyncHandler asyncHandler, NettyResponseFuture future,
ProxyServer proxyServer, boolean performConnectRequest) {
- NettyResponseFuture newFuture = newNettyRequestAndResponseFuture(request, asyncHandler, future, proxyServer, performConnectRequest);
Channel channel = getOpenChannel(future, request, proxyServer, asyncHandler);
- return Channels.isChannelActive(channel)
- ? sendRequestWithOpenChannel(newFuture, asyncHandler, channel)
- : sendRequestWithNewChannel(request, proxyServer, newFuture, asyncHandler);
+ if (Channels.isChannelActive(channel)) {
+ NettyResponseFuture newFuture = newNettyRequestAndResponseFuture(request, asyncHandler, future,
+ proxyServer, performConnectRequest);
+ return sendRequestWithOpenChannel(newFuture, asyncHandler, channel);
+ } else {
+ // A new channel is not expected when performConnectRequest is false. We need to
+ // revisit the condition of sending
+ // the CONNECT request to the new channel.
+ NettyResponseFuture newFuture = newNettyRequestAndResponseFuture(request, asyncHandler, future,
+ proxyServer, needConnect(request, proxyServer));
+ return sendRequestWithNewChannel(request, proxyServer, newFuture, asyncHandler);
+ }
}
/**
diff --git a/client/src/test/java/org/asynchttpclient/AutomaticDecompressionTest.java b/client/src/test/java/org/asynchttpclient/AutomaticDecompressionTest.java
index dfd0a9446..8f57ffb88 100644
--- a/client/src/test/java/org/asynchttpclient/AutomaticDecompressionTest.java
+++ b/client/src/test/java/org/asynchttpclient/AutomaticDecompressionTest.java
@@ -22,6 +22,7 @@
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
+import io.github.nettyplus.leakdetector.junit.NettyLeakDetectorExtension;
import io.netty.handler.codec.compression.Brotli;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
@@ -35,11 +36,13 @@
import java.util.List;
import java.util.stream.Collectors;
import java.util.zip.GZIPOutputStream;
+import org.junit.jupiter.api.extension.ExtendWith;
import static org.junit.jupiter.api.Assertions.assertEquals;
+@ExtendWith(NettyLeakDetectorExtension.class)
public class AutomaticDecompressionTest {
- private static final String UNCOMPRESSED_PAYLOAD = "a".repeat(500);
+ private static final String UNCOMPRESSED_PAYLOAD = "a".repeat(50_000);
private static HttpServer HTTP_SERVER;
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/RequestBuilderTest.java b/client/src/test/java/org/asynchttpclient/RequestBuilderTest.java
index 024fce5f1..34e79121d 100644
--- a/client/src/test/java/org/asynchttpclient/RequestBuilderTest.java
+++ b/client/src/test/java/org/asynchttpclient/RequestBuilderTest.java
@@ -166,6 +166,40 @@ public void testAddOrReplaceCookies() {
assertEquals(requestBuilder.cookies.size(), 2, "cookie size must be 2 after adding 1 more cookie i.e. cookie3");
}
+ @RepeatedIfExceptionsTest(repeats = 5)
+ public void testAddIfUnsetCookies() {
+ RequestBuilder requestBuilder = new RequestBuilder();
+ Cookie cookie = new DefaultCookie("name", "value");
+ cookie.setDomain("google.com");
+ cookie.setPath("/");
+ cookie.setMaxAge(1000);
+ cookie.setSecure(true);
+ cookie.setHttpOnly(true);
+ requestBuilder.addCookieIfUnset(cookie);
+ assertEquals(requestBuilder.cookies.size(), 1, "cookies size should be 1 after adding one cookie");
+ assertEquals(requestBuilder.cookies.get(0), cookie, "cookie does not match");
+
+ Cookie cookie2 = new DefaultCookie("name", "value");
+ cookie2.setDomain("google2.com");
+ cookie2.setPath("/path");
+ cookie2.setMaxAge(1001);
+ cookie2.setSecure(false);
+ cookie2.setHttpOnly(false);
+
+ requestBuilder.addCookieIfUnset(cookie2);
+ assertEquals(requestBuilder.cookies.size(), 1, "cookies size should remain 1 as we just ignored cookie2 because of a cookie with same name");
+ assertEquals(requestBuilder.cookies.get(0), cookie, "cookie does not match");
+
+ Cookie cookie3 = new DefaultCookie("name2", "value");
+ cookie3.setDomain("google.com");
+ cookie3.setPath("/");
+ cookie3.setMaxAge(1000);
+ cookie3.setSecure(true);
+ cookie3.setHttpOnly(true);
+ requestBuilder.addCookieIfUnset(cookie3);
+ assertEquals(requestBuilder.cookies.size(), 2, "cookie size must be 2 after adding 1 more cookie i.e. cookie3");
+ }
+
@RepeatedIfExceptionsTest(repeats = 5)
public void testSettingQueryParamsBeforeUrlShouldNotProduceNPE() {
RequestBuilder requestBuilder = new RequestBuilder();
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/client/src/test/java/org/asynchttpclient/proxy/HttpsProxyTest.java b/client/src/test/java/org/asynchttpclient/proxy/HttpsProxyTest.java
index 6c4109aec..9bd5ca911 100644
--- a/client/src/test/java/org/asynchttpclient/proxy/HttpsProxyTest.java
+++ b/client/src/test/java/org/asynchttpclient/proxy/HttpsProxyTest.java
@@ -13,14 +13,22 @@
package org.asynchttpclient.proxy;
import io.github.artsok.RepeatedIfExceptionsTest;
+import io.netty.handler.codec.http.DefaultHttpHeaders;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
import org.asynchttpclient.AbstractBasicTest;
import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.AsyncHttpClientConfig;
import org.asynchttpclient.RequestBuilder;
import org.asynchttpclient.Response;
+import org.asynchttpclient.proxy.ProxyServer.Builder;
import org.asynchttpclient.request.body.generator.ByteArrayBodyGenerator;
import org.asynchttpclient.test.EchoHandler;
+import org.asynchttpclient.util.HttpConstants;
import org.eclipse.jetty.proxy.ConnectHandler;
+import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.server.handler.AbstractHandler;
@@ -36,6 +44,10 @@
import static org.asynchttpclient.test.TestUtils.addHttpConnector;
import static org.asynchttpclient.test.TestUtils.addHttpsConnector;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
+
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
/**
* Proxy usage tests.
@@ -46,7 +58,7 @@ public class HttpsProxyTest extends AbstractBasicTest {
@Override
public AbstractHandler configureHandler() throws Exception {
- return new ConnectHandler();
+ return new ProxyHandler();
}
@Override
@@ -142,4 +154,61 @@ public void testPooledConnectionsWithProxy() throws Exception {
assertEquals(200, response2.getStatusCode());
}
}
+
+ @RepeatedIfExceptionsTest(repeats = 5)
+ public void testFailedConnectWithProxy() throws Exception {
+ try (AsyncHttpClient asyncHttpClient = asyncHttpClient(config().setFollowRedirect(true).setUseInsecureTrustManager(true).setKeepAlive(true))) {
+ Builder proxyServer = proxyServer("localhost", port1);
+ proxyServer.setCustomHeaders(r -> new DefaultHttpHeaders().set(ProxyHandler.HEADER_FORBIDDEN, "1"));
+ RequestBuilder rb = get(getTargetUrl2()).setProxyServer(proxyServer);
+
+ Response response1 = asyncHttpClient.executeRequest(rb.build()).get();
+ assertEquals(403, response1.getStatusCode());
+
+ Response response2 = asyncHttpClient.executeRequest(rb.build()).get();
+ assertEquals(403, response2.getStatusCode());
+
+ Response response3 = asyncHttpClient.executeRequest(rb.build()).get();
+ assertEquals(403, response3.getStatusCode());
+ }
+ }
+
+ @RepeatedIfExceptionsTest(repeats = 5)
+ public void testClosedConnectionWithProxy() throws Exception {
+ try (AsyncHttpClient asyncHttpClient = asyncHttpClient(
+ config().setFollowRedirect(true).setUseInsecureTrustManager(true).setKeepAlive(true))) {
+ Builder proxyServer = proxyServer("localhost", port1);
+ proxyServer.setCustomHeaders(r -> new DefaultHttpHeaders().set(ProxyHandler.HEADER_FORBIDDEN, "2"));
+ RequestBuilder rb = get(getTargetUrl2()).setProxyServer(proxyServer);
+
+ assertThrowsExactly(ExecutionException.class, () -> asyncHttpClient.executeRequest(rb.build()).get());
+ assertThrowsExactly(ExecutionException.class, () -> asyncHttpClient.executeRequest(rb.build()).get());
+ assertThrowsExactly(ExecutionException.class, () -> asyncHttpClient.executeRequest(rb.build()).get());
+ }
+ }
+
+ public static class ProxyHandler extends ConnectHandler {
+ final static String HEADER_FORBIDDEN = "X-REJECT-REQUEST";
+
+ @Override
+ public void handle(String s, Request r, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+ if (HttpConstants.Methods.CONNECT.equalsIgnoreCase(request.getMethod())) {
+ String headerValue = request.getHeader(HEADER_FORBIDDEN);
+ if (headerValue == null) {
+ headerValue = "";
+ }
+ switch (headerValue) {
+ case "1":
+ response.setStatus(HttpServletResponse.SC_FORBIDDEN);
+ r.setHandled(true);
+ return;
+ case "2":
+ r.getHttpChannel().getConnection().close();
+ r.setHandled(true);
+ return;
+ }
+ }
+ super.handle(s, r, request, response);
+ }
+ }
}
diff --git a/pom.xml b/pom.xml
index aeb65d5f9..e55fe8a26 100644
--- a/pom.xml
+++ b/pom.xml
@@ -20,7 +20,7 @@
org.asynchttpclient
async-http-client-project
- 3.0.0
+ 3.0.2
pom
AHC/Project
@@ -45,14 +45,14 @@
11
UTF-8
- 4.1.113.Final
- 0.0.25.Final
- 1.17.0
+ 4.1.119.Final
+ 0.0.26.Final
+ 1.18.0
2.0.16
- 1.5.6-7
+ 1.5.7-2
2.0.1
- 1.5.8
- 26.0.1
+ 1.5.18
+ 26.0.2
@@ -105,14 +105,14 @@
org.junit
junit-bom
- 5.11.0
+ 5.11.4
pom
import
io.github.nettyplus
netty-leak-detector-junit-extension
- 0.0.5
+ 0.2.0
@@ -293,7 +293,7 @@
org.apache.maven.plugins
maven-compiler-plugin
- 3.13.0
+ 3.14.0
11
11
@@ -327,7 +327,7 @@
com.uber.nullaway
nullaway
- 0.11.3
+ 0.12.6
@@ -337,7 +337,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 3.5.0
+ 3.5.2
@{argLine} --add-exports java.base/jdk.internal.misc=ALL-UNNAMED
@@ -368,10 +368,9 @@
org.apache.maven.plugins
maven-source-plugin
- 3.3.1
+ 3.2.1
- attach-sources
jar-no-fork
@@ -382,7 +381,7 @@
org.apache.maven.plugins
maven-javadoc-plugin
- 3.10.0
+ 3.11.1
attach-javadocs
@@ -423,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
+
+
+