diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml new file mode 100644 index 0000000000..5d16905627 --- /dev/null +++ b/.github/workflows/maven.yml @@ -0,0 +1,20 @@ +# This workflow is designed to build PRs for AHC. Note that it does not actually publish AHC, just builds and test it. +# Docs: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven + +name: Test PR + +on: + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v2 + - name: Set up JDK 1.8 + uses: actions/setup-java@v1 + with: + java-version: 1.8 + - name: Build and test with Maven + run: mvn test -Ptest-output diff --git a/.gitignore b/.gitignore index b023787595..d424b2597a 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ test-output MANIFEST.MF work atlassian-ide-plugin.xml +/bom/.flattened-pom.xml diff --git a/.travis.yml b/.travis.yml index bb8adf60b0..2760c26e6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,12 @@ language: java jdk: - - oraclejdk8 + - openjdk8 before_script: - travis/before_script.sh script: - - mvn test -Ptest-output + - mvn test javadoc:javadoc -Ptest-output - find $HOME/.m2 -name "_remote.repositories" | xargs rm - find $HOME/.m2 -name "resolver-status.properties" | xargs rm -f @@ -16,12 +16,6 @@ after_success: sudo: false -# https://github.com/travis-ci/travis-ci/issues/3259 -addons: - apt: - packages: - - oracle-java8-installer - # Cache settings cache: directories: diff --git a/README.md b/README.md index 487cf8ffc5..51e3339b19 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +# Looking for a new maintainer + +Due to lack of time on my end and this repo being dead for most of the last couple of years, I am bringing the repo back up for maintenance. Reach out to me on Twitter - @TomGranot - for more info. + # Async Http Client [![Build Status](https://travis-ci.org/AsyncHttpClient/async-http-client.svg?branch=master)](https://travis-ci.org/AsyncHttpClient/async-http-client) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.asynchttpclient/async-http-client/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.asynchttpclient/async-http-client/) Follow [@AsyncHttpClient](https://twitter.com/AsyncHttpClient) on Twitter. @@ -5,20 +9,45 @@ Follow [@AsyncHttpClient](https://twitter.com/AsyncHttpClient) on Twitter. The AsyncHttpClient (AHC) library allows Java applications to easily execute HTTP requests and asynchronously process HTTP responses. The library also supports the WebSocket Protocol. -It's built on top of [Netty](https://github.com/netty/netty). I's currently compiled on Java 8 but runs on Java 9 too. +It's built on top of [Netty](https://github.com/netty/netty). It's currently compiled on Java 8 but runs on Java 9 too. + +## New Roadmap RFCs! + +Well, not really RFCs, but as [I](https://github.com/TomGranot) am ramping up to release a new version, I would appreciate the comments from the community. Please add an issue and [label it RFC](https://github.com/AsyncHttpClient/async-http-client/labels/RFC) and I'll take a look! ## Installation -Binaries are deployed on Maven central: +Binaries are deployed on Maven Central. + +Import the AsyncHttpClient Bill of Materials (BOM) to add dependency management for AsyncHttpClient artifacts to your project: ```xml - - org.asynchttpclient - async-http-client - LATEST_VERSION - + + + + org.asynchttpclient + async-http-client-bom + LATEST_VERSION + pom + import + + + ``` +Add a dependency on the main AsyncHttpClient artifact: + +```xml + + + org.asynchttpclient + async-http-client + + +``` + +The `async-http-client-extras-*` and other modules can also be added without having to specify the version for each dependency, because they are all managed via the BOM. + ## Version AHC doesn't use SEMVER, and won't. @@ -73,7 +102,7 @@ AsyncHttpClient c = asyncHttpClient(config().setProxyServer(proxyServer("127.0.0 ### Basics AHC provides 2 APIs for defining requests: bound and unbound. -`AsyncHttpClient` and Dls` provide methods for standard HTTP methods (POST, PUT, etc) but you can also pass a custom one. +`AsyncHttpClient` and Dsl` provide methods for standard HTTP methods (POST, PUT, etc) but you can also pass a custom one. ```java import org.asynchttpclient.*; @@ -83,7 +112,7 @@ Future whenResponse = asyncHttpClient.prepareGet("http://www.example.c // unbound Request request = get("http://www.example.com/").build(); -Future whenResponse = asyncHttpClient.execute(request); +Future whenResponse = asyncHttpClient.executeRequest(request); ``` #### Setting Request Body @@ -110,6 +139,7 @@ Use the `addBodyPart` method to add a multipart part to the request. This part can be of type: * `ByteArrayPart` * `FilePart` +* `InputStreamPart` * `StringPart` ### Dealing with Responses @@ -158,7 +188,7 @@ See `AsyncCompletionHandler` implementation as an example. The below sample just capture the response status and skips processing the response body chunks. -Note that returning `ABORT` closed the underlying connection. +Note that returning `ABORT` closes the underlying connection. ```java import static org.asynchttpclient.Dsl.*; @@ -195,7 +225,7 @@ Integer statusCode = whenStatusCode.get(); #### Using Continuations -`ListenableFuture` has a `toCompletableFuture` that returns a `CompletableFuture`. +`ListenableFuture` has a `toCompletableFuture` method that returns a `CompletableFuture`. Beware that canceling this `CompletableFuture` won't properly cancel the ongoing request. There's a very good chance we'll return a `CompletionStage` instead in the next release. @@ -243,7 +273,7 @@ WebSocket websocket = c.prepareGet("ws://demos.kaazing.com/echo") ## Reactive Streams -AsyncHttpClient has build in support for reactive streams. +AsyncHttpClient has built-in support for reactive streams. You can pass a request body as a `Publisher` or a `ReactiveStreamsBodyGenerator`. @@ -274,9 +304,10 @@ Response response = c.executeRequest(propFindRequest, new AsyncHandler() { You can find more information on Jean-François Arcand's blog. Jean-François is the original author of this library. Code is sometimes not up-to-date but gives a pretty good idea of advanced features. -* https://jfarcand.wordpress.com/2010/12/21/going-asynchronous-using-asynchttpclient-the-basic/ -* https://jfarcand.wordpress.com/2011/01/04/going-asynchronous-using-asynchttpclient-the-complex/ -* https://jfarcand.wordpress.com/2011/12/21/writing-websocket-clients-using-asynchttpclient/ +* http://web.archive.org/web/20111224171448/http://jfarcand.wordpress.com/2011/01/12/going-asynchronous-using-asynchttpclient-for-dummies/ +* http://web.archive.org/web/20111224171241/http://jfarcand.wordpress.com/2010/12/21/going-asynchronous-using-asynchttpclient-the-basic/ +* http://web.archive.org/web/20111224162752/http://jfarcand.wordpress.com/2011/01/04/going-asynchronous-using-asynchttpclient-the-complex/ +* http://web.archive.org/web/20120218183108/http://jfarcand.wordpress.com/2011/12/21/writing-websocket-clients-using-asynchttpclient/ ## User Group @@ -288,7 +319,7 @@ Keep up to date on the library development by joining the Asynchronous HTTP Clie Of course, Pull Requests are welcome. -Here a the few rules we'd like you to respect if you do so: +Here are the few rules we'd like you to respect if you do so: * Only edit the code related to the suggested change, so DON'T automatically format the classes you've edited. * Use IntelliJ default formatting rules. diff --git a/bom/pom.xml b/bom/pom.xml new file mode 100644 index 0000000000..072db569ca --- /dev/null +++ b/bom/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + + org.asynchttpclient + async-http-client-project + 2.12.4-SNAPSHOT + + + async-http-client-bom + pom + Asynchronous Http Client Bill of Materials (BOM) + Importing this BOM will provide dependency management for all AsyncHttpClient artifacts. + http://github.com/AsyncHttpClient/async-http-client/bom + + + + + org.asynchttpclient + async-http-client + ${project.version} + + + org.asynchttpclient + async-http-client-example + ${project.version} + + + org.asynchttpclient + async-http-client-extras-guava + ${project.version} + + + org.asynchttpclient + async-http-client-extras-jdeferred + ${project.version} + + + org.asynchttpclient + async-http-client-extras-registry + ${project.version} + + + org.asynchttpclient + async-http-client-extras-retrofit2 + ${project.version} + + + org.asynchttpclient + async-http-client-extras-rxjava + ${project.version} + + + org.asynchttpclient + async-http-client-extras-rxjava2 + ${project.version} + + + org.asynchttpclient + async-http-client-extras-simple + ${project.version} + + + org.asynchttpclient + async-http-client-extras-typesafe-config + ${project.version} + + + org.asynchttpclient + async-http-client-netty-utils + ${project.version} + + + + + + + + + org.codehaus.mojo + flatten-maven-plugin + 1.1.0 + false + + + flatten + process-resources + + flatten + + + bom + + remove + remove + remove + + + + + + + + diff --git a/client/pom.xml b/client/pom.xml index da97920051..c9e13aea7e 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -2,13 +2,17 @@ org.asynchttpclient async-http-client-project - 2.5.3-SNAPSHOT + 2.12.4-SNAPSHOT 4.0.0 async-http-client Asynchronous Http Client The Async Http Client (AHC) classes. + + org.asynchttpclient.client + + @@ -53,7 +57,8 @@ io.netty - netty-resolver-dns + netty-transport-native-kqueue + osx-x86_64 org.reactivestreams @@ -73,5 +78,10 @@ reactive-streams-examples test + + org.apache.kerby + kerb-simplekdc + test + diff --git a/client/src/main/java/org/asynchttpclient/AsyncCompletionHandler.java b/client/src/main/java/org/asynchttpclient/AsyncCompletionHandler.java index c7b60e0b4c..d1f30c1ac3 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncCompletionHandler.java +++ b/client/src/main/java/org/asynchttpclient/AsyncCompletionHandler.java @@ -24,7 +24,7 @@ /** * An {@link AsyncHandler} augmented with an {@link #onCompleted(Response)} * convenience method which gets called when the {@link Response} processing is - * finished. This class also implement the {@link ProgressAsyncHandler} + * finished. This class also implements the {@link ProgressAsyncHandler} * callback, all doing nothing except returning * {@link org.asynchttpclient.AsyncHandler.State#CONTINUE} * diff --git a/client/src/main/java/org/asynchttpclient/AsyncHandler.java b/client/src/main/java/org/asynchttpclient/AsyncHandler.java index 090503cf14..6733c94711 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncHandler.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHandler.java @@ -19,6 +19,7 @@ import io.netty.handler.codec.http.HttpHeaders; import org.asynchttpclient.netty.request.NettyRequest; +import javax.net.ssl.SSLSession; import java.net.InetSocketAddress; import java.util.List; @@ -37,9 +38,9 @@ * *
* Returning a {@link AsyncHandler.State#ABORT} from any of those callback methods will interrupt asynchronous response - * processing, after that only {@link #onCompleted()} is going to be called. + * processing. After that, only {@link #onCompleted()} is going to be called. *
- * AsyncHandler aren't thread safe, hence you should avoid re-using the same instance when doing concurrent requests. + * AsyncHandlers aren't thread safe. Hence, you should avoid re-using the same instance when doing concurrent requests. * As an example, the following may produce unexpected results: *
  *   AsyncHandler ah = new AsyncHandler() {....};
@@ -49,9 +50,10 @@
  * 
* It is recommended to create a new instance instead. *

- * Do NOT perform any blocking operation in there, typically trying to send another request and call get() on its future. + * Do NOT perform any blocking operations in any of these methods. A typical example would be trying to send another + * request and calling get() on its future. * There's a chance you might end up in a dead lock. - * If you really to perform blocking operation, executed it in a different dedicated thread pool. + * If you really need to perform a blocking operation, execute it in a different dedicated thread pool. * * @param Type of object returned by the {@link java.util.concurrent.Future#get} */ @@ -142,6 +144,8 @@ default void onHostnameResolutionSuccess(String name, List ad default void onHostnameResolutionFailure(String name, Throwable cause) { } + // ////////////// TCP CONNECT //////// + /** * Notify the callback when trying to open a new connection. *

@@ -152,8 +156,6 @@ default void onHostnameResolutionFailure(String name, Throwable cause) { default void onTcpConnectAttempt(InetSocketAddress remoteAddress) { } - // ////////////// TCP CONNECT //////// - /** * Notify the callback after a successful connect * @@ -174,18 +176,18 @@ default void onTcpConnectSuccess(InetSocketAddress remoteAddress, Channel connec default void onTcpConnectFailure(InetSocketAddress remoteAddress, Throwable cause) { } + // ////////////// TLS /////////////// + /** * Notify the callback before TLS handshake */ default void onTlsHandshakeAttempt() { } - // ////////////// TLS /////////////// - /** * Notify the callback after the TLS was successful */ - default void onTlsHandshakeSuccess() { + default void onTlsHandshakeSuccess(SSLSession sslSession) { } /** @@ -196,14 +198,14 @@ default void onTlsHandshakeSuccess() { default void onTlsHandshakeFailure(Throwable cause) { } + // /////////// POOLING ///////////// + /** * Notify the callback when trying to fetch a connection from the pool. */ default void onConnectionPoolAttempt() { } - // /////////// POOLING ///////////// - /** * Notify the callback when a new connection was successfully fetched from the pool. * @@ -220,6 +222,8 @@ default void onConnectionPooled(Channel connection) { default void onConnectionOffer(Channel connection) { } + // //////////// SENDING ////////////// + /** * Notify the callback when a request is being written on the channel. If the original request causes multiple requests to be sent, for example, because of authorization or * retry, it will be notified multiple times. @@ -229,8 +233,6 @@ default void onConnectionOffer(Channel connection) { default void onRequestSend(NettyRequest request) { } - // //////////// SENDING ////////////// - /** * Notify the callback every time a request is being retried. */ diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClient.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClient.java index 1510de4513..2ab335f3f6 100755 --- a/client/src/main/java/org/asynchttpclient/AsyncHttpClient.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClient.java @@ -21,15 +21,15 @@ import java.util.function.Predicate; /** - * This class support asynchronous and synchronous HTTP request. + * This class support asynchronous and synchronous HTTP requests. *
- * To execute synchronous HTTP request, you just need to do + * To execute a synchronous HTTP request, you just need to do *

  *    AsyncHttpClient c = new AsyncHttpClient();
  *    Future<Response> f = c.prepareGet(TARGET_URL).execute();
  * 
*
- * The code above will block until the response is fully received. To execute asynchronous HTTP request, you + * The code above will block until the response is fully received. To execute an asynchronous HTTP request, you * create an {@link AsyncHandler} or its abstract implementation, {@link AsyncCompletionHandler} *
*
@@ -48,7 +48,7 @@
  *      });
  *      Response response = f.get();
  *
- *      // We are just interested to retrieve the status code.
+ *      // We are just interested in retrieving the status code.
  *     Future<Integer> f = c.prepareGet(TARGET_URL).execute(new AsyncCompletionHandler<Integer>() {
  *
  *          @Override
@@ -63,10 +63,10 @@
  *      });
  *      Integer statusCode = f.get();
  * 
- * The {@link AsyncCompletionHandler#onCompleted(Response)} will be invoked once the http response has been fully read, which include - * the http headers and the response body. Note that the entire response will be buffered in memory. + * The {@link AsyncCompletionHandler#onCompleted(Response)} method will be invoked once the http response has been fully read. + * The {@link Response} object includes the http headers and the response body. Note that the entire response will be buffered in memory. *
- * You can also have more control about the how the response is asynchronously processed by using a {@link AsyncHandler} + * You can also have more control about the how the response is asynchronously processed by using an {@link AsyncHandler} *
  *      AsyncHttpClient c = new AsyncHttpClient();
  *      Future<String> f = c.prepareGet(TARGET_URL).execute(new AsyncHandler<String>() {
@@ -106,8 +106,8 @@
  *
  *      String bodyResponse = f.get();
  * 
- * You can asynchronously process the response status,headers and body and decide when to - * stop the processing the response by returning a new {@link AsyncHandler.State#ABORT} at any moment. + * You can asynchronously process the response status, headers and body and decide when to + * stop processing the response by returning a new {@link AsyncHandler.State#ABORT} at any moment. * * This class can also be used without the need of {@link AsyncHandler}. *
@@ -125,8 +125,8 @@ * Response r = f.get(); * *
- * An instance of this class will cache every HTTP 1.1 connections and close them when the {@link DefaultAsyncHttpClientConfig#getReadTimeout()} - * expires. This object can hold many persistent connections to different host. + * An instance of this class will cache every HTTP 1.1 connection and close them when the {@link DefaultAsyncHttpClientConfig#getReadTimeout()} + * expires. This object can hold many persistent connections to different hosts. */ public interface AsyncHttpClient extends Closeable { @@ -138,7 +138,7 @@ public interface AsyncHttpClient extends Closeable { boolean isClosed(); /** - * Set default signature calculator to use for requests build by this client instance + * Set default signature calculator to use for requests built by this client instance * * @param signatureCalculator a signature calculator * @return {@link RequestBuilder} diff --git a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java index e0f8413662..a761322dc3 100644 --- a/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/AsyncHttpClientConfig.java @@ -65,6 +65,14 @@ public interface AsyncHttpClientConfig { */ int getMaxConnectionsPerHost(); + /** + * Return the maximum duration in milliseconds an {@link AsyncHttpClient} can wait to acquire a free channel + * + * @return Return the maximum duration in milliseconds an {@link AsyncHttpClient} can wait to acquire a free channel + */ + int getAcquireFreeChannelTimeout(); + + /** * Return the maximum time in millisecond an {@link AsyncHttpClient} can wait when connecting to a remote host * @@ -190,6 +198,13 @@ public interface AsyncHttpClientConfig { */ CookieStore getCookieStore(); + /** + * Return the delay in milliseconds to evict expired cookies from {@linkplain CookieStore} + * + * @return the delay in milliseconds to evict expired cookies from {@linkplain CookieStore} + */ + int expiredCookieEvictionDelay(); + /** * Return the number of time the library will retry when an {@link java.io.IOException} is throw by the remote server * @@ -240,7 +255,7 @@ public interface AsyncHttpClientConfig { String[] getEnabledCipherSuites(); /** - * @return if insecured cipher suites must be filtered out (only used when not explicitly passing enabled cipher suites) + * @return if insecure cipher suites must be filtered out (only used when not explicitly passing enabled cipher suites) */ boolean isFilterInsecureCipherSuites(); @@ -298,6 +313,16 @@ public interface AsyncHttpClientConfig { Timer getNettyTimer(); + /** + * @return the duration between tick of {@link io.netty.util.HashedWheelTimer} + */ + long getHashedWheelTimerTickDuration(); + + /** + * @return the size of the hashed wheel {@link io.netty.util.HashedWheelTimer} + */ + int getHashedWheelTimerSize(); + KeepAliveStrategy getKeepAliveStrategy(); boolean isValidateResponseHeaders(); @@ -310,6 +335,8 @@ public interface AsyncHttpClientConfig { boolean isSoReuseAddress(); + boolean isSoKeepAlive(); + int getSoLinger(); int getSoSndBuf(); diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java index 8d2c3f7ab1..7cc3e6e341 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java @@ -22,6 +22,8 @@ import io.netty.util.Timer; import io.netty.util.concurrent.DefaultThreadFactory; import org.asynchttpclient.channel.ChannelPool; +import org.asynchttpclient.cookie.CookieEvictionTask; +import org.asynchttpclient.cookie.CookieStore; import org.asynchttpclient.filter.FilterContext; import org.asynchttpclient.filter.FilterException; import org.asynchttpclient.filter.RequestFilter; @@ -33,6 +35,7 @@ import java.util.List; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Predicate; @@ -89,12 +92,23 @@ public DefaultAsyncHttpClient(AsyncHttpClientConfig config) { channelManager = new ChannelManager(config, nettyTimer); requestSender = new NettyRequestSender(config, channelManager, nettyTimer, new AsyncHttpClientState(closed)); channelManager.configureBootstraps(requestSender); + + CookieStore cookieStore = config.getCookieStore(); + if (cookieStore != null) { + int cookieStoreCount = config.getCookieStore().incrementAndGet(); + if ( + allowStopNettyTimer // timer is not shared + || cookieStoreCount == 1 // this is the first AHC instance for the shared (user-provided) timer + ) { + nettyTimer.newTimeout(new CookieEvictionTask(config.expiredCookieEvictionDelay(), cookieStore), + config.expiredCookieEvictionDelay(), TimeUnit.MILLISECONDS); + } + } } private Timer newNettyTimer(AsyncHttpClientConfig config) { ThreadFactory threadFactory = config.getThreadFactory() != null ? config.getThreadFactory() : new DefaultThreadFactory(config.getThreadPoolName() + "-timer"); - - HashedWheelTimer timer = new HashedWheelTimer(threadFactory); + HashedWheelTimer timer = new HashedWheelTimer(threadFactory, config.getHashedWheelTimerTickDuration(), TimeUnit.MILLISECONDS, config.getHashedWheelTimerSize()); timer.start(); return timer; } @@ -107,6 +121,10 @@ public void close() { } catch (Throwable t) { LOGGER.warn("Unexpected error on ChannelManager close", t); } + CookieStore cookieStore = config.getCookieStore(); + if (cookieStore != null) { + cookieStore.decrementAndGet(); + } if (allowStopNettyTimer) { try { nettyTimer.stop(); @@ -194,7 +212,7 @@ public ListenableFuture executeRequest(Request request, AsyncHandler h try { List cookies = config.getCookieStore().get(request.getUri()); if (!cookies.isEmpty()) { - RequestBuilder requestBuilder = new RequestBuilder(request); + RequestBuilder requestBuilder = request.toBuilder(); for (Cookie cookie : cookies) { requestBuilder.addOrReplaceCookie(cookie); } @@ -264,7 +282,7 @@ private FilterContext preProcessRequest(FilterContext fc) throws Filte } if (request.getRangeOffset() != 0) { - RequestBuilder builder = new RequestBuilder(request); + RequestBuilder builder = request.toBuilder(); builder.setHeader("Range", "bytes=" + request.getRangeOffset() + "-"); request = builder.build(); } diff --git a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java index 1827728e4c..0f4e62c560 100644 --- a/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java +++ b/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java @@ -84,6 +84,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { private final int connectionTtl; private final int maxConnections; private final int maxConnectionsPerHost; + private final int acquireFreeChannelTimeout; private final ChannelPool channelPool; private final ConnectionSemaphoreFactory connectionSemaphoreFactory; private final KeepAliveStrategy keepAliveStrategy; @@ -108,6 +109,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { // cookie store private final CookieStore cookieStore; + private final int expiredCookieEvictionDelay; // internals private final String threadPoolName; @@ -122,6 +124,7 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { private final ByteBufAllocator allocator; private final boolean tcpNoDelay; private final boolean soReuseAddress; + private final boolean soKeepAlive; private final int soLinger; private final int soSndBuf; private final int soRcvBuf; @@ -131,6 +134,8 @@ public class DefaultAsyncHttpClientConfig implements AsyncHttpClientConfig { private final Consumer wsAdditionalChannelInitializer; private final ResponseBodyPartFactory responseBodyPartFactory; private final int ioThreadsCount; + private final long hashedWheelTimerTickDuration; + private final int hashedWheelTimerSize; private DefaultAsyncHttpClientConfig(// http boolean followRedirect, @@ -163,6 +168,7 @@ private DefaultAsyncHttpClientConfig(// http int connectionTtl, int maxConnections, int maxConnectionsPerHost, + int acquireFreeChannelTimeout, ChannelPool channelPool, ConnectionSemaphoreFactory connectionSemaphoreFactory, KeepAliveStrategy keepAliveStrategy, @@ -187,10 +193,12 @@ private DefaultAsyncHttpClientConfig(// http // cookie store CookieStore cookieStore, + int expiredCookieEvictionDelay, // tuning boolean tcpNoDelay, boolean soReuseAddress, + boolean soKeepAlive, int soLinger, int soSndBuf, int soRcvBuf, @@ -213,7 +221,9 @@ private DefaultAsyncHttpClientConfig(// http Consumer httpAdditionalChannelInitializer, Consumer wsAdditionalChannelInitializer, ResponseBodyPartFactory responseBodyPartFactory, - int ioThreadsCount) { + int ioThreadsCount, + long hashedWheelTimerTickDuration, + int hashedWheelTimerSize) { // http this.followRedirect = followRedirect; @@ -250,6 +260,7 @@ private DefaultAsyncHttpClientConfig(// http this.connectionTtl = connectionTtl; this.maxConnections = maxConnections; this.maxConnectionsPerHost = maxConnectionsPerHost; + this.acquireFreeChannelTimeout = acquireFreeChannelTimeout; this.channelPool = channelPool; this.connectionSemaphoreFactory = connectionSemaphoreFactory; this.keepAliveStrategy = keepAliveStrategy; @@ -274,10 +285,12 @@ private DefaultAsyncHttpClientConfig(// http // cookie store this.cookieStore = cookieStore; + this.expiredCookieEvictionDelay = expiredCookieEvictionDelay; // tuning this.tcpNoDelay = tcpNoDelay; this.soReuseAddress = soReuseAddress; + this.soKeepAlive = soKeepAlive; this.soLinger = soLinger; this.soSndBuf = soSndBuf; this.soRcvBuf = soRcvBuf; @@ -299,6 +312,8 @@ private DefaultAsyncHttpClientConfig(// http this.wsAdditionalChannelInitializer = wsAdditionalChannelInitializer; this.responseBodyPartFactory = responseBodyPartFactory; this.ioThreadsCount = ioThreadsCount; + this.hashedWheelTimerTickDuration = hashedWheelTimerTickDuration; + this.hashedWheelTimerSize = hashedWheelTimerSize; } @Override @@ -445,6 +460,9 @@ public int getMaxConnectionsPerHost() { return maxConnectionsPerHost; } + @Override + public int getAcquireFreeChannelTimeout() { return acquireFreeChannelTimeout; } + @Override public ChannelPool getChannelPool() { return channelPool; @@ -543,6 +561,11 @@ public CookieStore getCookieStore() { return cookieStore; } + @Override + public int expiredCookieEvictionDelay() { + return expiredCookieEvictionDelay; + } + // tuning @Override public boolean isTcpNoDelay() { @@ -554,6 +577,11 @@ public boolean isSoReuseAddress() { return soReuseAddress; } + @Override + public boolean isSoKeepAlive() { + return soKeepAlive; + } + @Override public int getSoLinger() { return soLinger; @@ -625,6 +653,16 @@ public Timer getNettyTimer() { return nettyTimer; } + @Override + public long getHashedWheelTimerTickDuration() { + return hashedWheelTimerTickDuration; + } + + @Override + public int getHashedWheelTimerSize() { + return hashedWheelTimerSize; + } + @Override public ThreadFactory getThreadFactory() { return threadFactory; @@ -696,6 +734,7 @@ public static class Builder { private int connectionTtl = defaultConnectionTtl(); private int maxConnections = defaultMaxConnections(); private int maxConnectionsPerHost = defaultMaxConnectionsPerHost(); + private int acquireFreeChannelTimeout = defaultAcquireFreeChannelTimeout(); private ChannelPool channelPool; private ConnectionSemaphoreFactory connectionSemaphoreFactory; private KeepAliveStrategy keepAliveStrategy = new DefaultKeepAliveStrategy(); @@ -715,10 +754,12 @@ public static class Builder { // cookie store private CookieStore cookieStore = new ThreadSafeCookieStore(); + private int expiredCookieEvictionDelay = defaultExpiredCookieEvictionDelay(); // tuning private boolean tcpNoDelay = defaultTcpNoDelay(); private boolean soReuseAddress = defaultSoReuseAddress(); + private boolean soKeepAlive = defaultSoKeepAlive(); private int soLinger = defaultSoLinger(); private int soSndBuf = defaultSoSndBuf(); private int soRcvBuf = defaultSoRcvBuf(); @@ -740,6 +781,8 @@ public static class Builder { private Consumer wsAdditionalChannelInitializer; private ResponseBodyPartFactory responseBodyPartFactory = ResponseBodyPartFactory.EAGER; private int ioThreadsCount = defaultIoThreadsCount(); + private long hashedWheelTickDuration = defaultHashedWheelTimerTickDuration(); + private int hashedWheelSize = defaultHashedWheelTimerSize(); public Builder() { } @@ -754,6 +797,7 @@ public Builder(AsyncHttpClientConfig config) { realm = config.getRealm(); maxRequestRetry = config.getMaxRequestRetry(); disableUrlEncodingForBoundRequests = config.isDisableUrlEncodingForBoundRequests(); + useLaxCookieEncoder = config.isUseLaxCookieEncoder(); disableZeroCopy = config.isDisableZeroCopy(); keepEncodingHeader = config.isKeepEncodingHeader(); proxyServerSelector = config.getProxyServerSelector(); @@ -800,6 +844,7 @@ public Builder(AsyncHttpClientConfig config) { // tuning tcpNoDelay = config.isTcpNoDelay(); soReuseAddress = config.isSoReuseAddress(); + soKeepAlive = config.isSoKeepAlive(); soLinger = config.getSoLinger(); soSndBuf = config.getSoSndBuf(); soRcvBuf = config.getSoRcvBuf(); @@ -820,6 +865,8 @@ public Builder(AsyncHttpClientConfig config) { wsAdditionalChannelInitializer = config.getWsAdditionalChannelInitializer(); responseBodyPartFactory = config.getResponseBodyPartFactory(); ioThreadsCount = config.getIoThreadsCount(); + hashedWheelTickDuration = config.getHashedWheelTimerTickDuration(); + hashedWheelSize = config.getHashedWheelTimerSize(); } // http @@ -990,6 +1037,16 @@ public Builder setMaxConnectionsPerHost(int maxConnectionsPerHost) { return this; } + /** + * Sets the maximum duration in milliseconds to acquire a free channel to send a request + * @param acquireFreeChannelTimeout maximum duration in milliseconds to acquire a free channel to send a request + * @return the same builder instance + */ + public Builder setAcquireFreeChannelTimeout(int acquireFreeChannelTimeout) { + this.acquireFreeChannelTimeout = acquireFreeChannelTimeout; + return this; + } + public Builder setChannelPool(ChannelPool channelPool) { this.channelPool = channelPool; return this; @@ -1098,6 +1155,11 @@ public Builder setCookieStore(CookieStore cookieStore) { return this; } + public Builder setExpiredCookieEvictionDelay(int expiredCookieEvictionDelay) { + this.expiredCookieEvictionDelay = expiredCookieEvictionDelay; + return this; + } + // tuning public Builder setTcpNoDelay(boolean tcpNoDelay) { this.tcpNoDelay = tcpNoDelay; @@ -1109,6 +1171,11 @@ public Builder setSoReuseAddress(boolean soReuseAddress) { return this; } + public Builder setSoKeepAlive(boolean soKeepAlive) { + this.soKeepAlive = soKeepAlive; + return this; + } + public Builder setSoLinger(int soLinger) { this.soLinger = soLinger; return this; @@ -1155,6 +1222,16 @@ public Builder setChunkedFileChunkSize(int chunkedFileChunkSize) { return this; } + public Builder setHashedWheelTickDuration(long hashedWheelTickDuration) { + this.hashedWheelTickDuration = hashedWheelTickDuration; + return this; + } + + public Builder setHashedWheelSize(int hashedWheelSize) { + this.hashedWheelSize = hashedWheelSize; + return this; + } + @SuppressWarnings("unchecked") public Builder addChannelOption(ChannelOption name, T value) { channelOptions.put((ChannelOption) name, value); @@ -1248,6 +1325,7 @@ public DefaultAsyncHttpClientConfig build() { connectionTtl, maxConnections, maxConnectionsPerHost, + acquireFreeChannelTimeout, channelPool, connectionSemaphoreFactory, keepAliveStrategy, @@ -1266,8 +1344,10 @@ public DefaultAsyncHttpClientConfig build() { responseFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(responseFilters), ioExceptionFilters.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(ioExceptionFilters), cookieStore, + expiredCookieEvictionDelay, tcpNoDelay, soReuseAddress, + soKeepAlive, soLinger, soSndBuf, soRcvBuf, @@ -1288,7 +1368,9 @@ public DefaultAsyncHttpClientConfig build() { httpAdditionalChannelInitializer, wsAdditionalChannelInitializer, responseBodyPartFactory, - ioThreadsCount); + ioThreadsCount, + hashedWheelTickDuration, + hashedWheelSize); } } } diff --git a/client/src/main/java/org/asynchttpclient/Dsl.java b/client/src/main/java/org/asynchttpclient/Dsl.java index 914b734b77..cdb30ed165 100644 --- a/client/src/main/java/org/asynchttpclient/Dsl.java +++ b/client/src/main/java/org/asynchttpclient/Dsl.java @@ -99,7 +99,11 @@ public static Realm.Builder realm(Realm prototype) { .setNtlmDomain(prototype.getNtlmDomain()) .setNtlmHost(prototype.getNtlmHost()) .setUseAbsoluteURI(prototype.isUseAbsoluteURI()) - .setOmitQuery(prototype.isOmitQuery()); + .setOmitQuery(prototype.isOmitQuery()) + .setServicePrincipalName(prototype.getServicePrincipalName()) + .setUseCanonicalHostname(prototype.isUseCanonicalHostname()) + .setCustomLoginConfig(prototype.getCustomLoginConfig()) + .setLoginContextName(prototype.getLoginContextName()); } public static Realm.Builder realm(AuthScheme scheme, String principal, String password) { diff --git a/client/src/main/java/org/asynchttpclient/Realm.java b/client/src/main/java/org/asynchttpclient/Realm.java index 9b9bdf798e..c6324fd0b4 100644 --- a/client/src/main/java/org/asynchttpclient/Realm.java +++ b/client/src/main/java/org/asynchttpclient/Realm.java @@ -23,6 +23,7 @@ import java.nio.charset.Charset; import java.security.MessageDigest; +import java.util.Map; import java.util.concurrent.ThreadLocalRandom; import static java.nio.charset.StandardCharsets.*; @@ -60,6 +61,10 @@ public class Realm { private final String ntlmDomain; private final boolean useAbsoluteURI; private final boolean omitQuery; + private final Map customLoginConfig; + private final String servicePrincipalName; + private final boolean useCanonicalHostname; + private final String loginContextName; private Realm(AuthScheme scheme, String principal, @@ -78,11 +83,15 @@ private Realm(AuthScheme scheme, String ntlmDomain, String ntlmHost, boolean useAbsoluteURI, - boolean omitQuery) { + boolean omitQuery, + String servicePrincipalName, + boolean useCanonicalHostname, + Map customLoginConfig, + String loginContextName) { this.scheme = assertNotNull(scheme, "scheme"); - this.principal = assertNotNull(principal, "principal"); - this.password = assertNotNull(password, "password"); + this.principal = principal; + this.password = password; this.realmName = realmName; this.nonce = nonce; this.algorithm = algorithm; @@ -98,6 +107,10 @@ private Realm(AuthScheme scheme, this.ntlmHost = ntlmHost; this.useAbsoluteURI = useAbsoluteURI; this.omitQuery = omitQuery; + this.servicePrincipalName = servicePrincipalName; + this.useCanonicalHostname = useCanonicalHostname; + this.customLoginConfig = customLoginConfig; + this.loginContextName = loginContextName; } public String getPrincipal() { @@ -187,12 +200,48 @@ public boolean isOmitQuery() { return omitQuery; } + public Map getCustomLoginConfig() { + return customLoginConfig; + } + + public String getServicePrincipalName() { + return servicePrincipalName; + } + + public boolean isUseCanonicalHostname() { + return useCanonicalHostname; + } + + public String getLoginContextName() { + return loginContextName; + } + @Override public String toString() { - return "Realm{" + "principal='" + principal + '\'' + ", scheme=" + scheme + ", realmName='" + realmName + '\'' - + ", nonce='" + nonce + '\'' + ", algorithm='" + algorithm + '\'' + ", response='" + response + '\'' - + ", qop='" + qop + '\'' + ", nc='" + nc + '\'' + ", cnonce='" + cnonce + '\'' + ", uri='" + uri + '\'' - + ", useAbsoluteURI='" + useAbsoluteURI + '\'' + ", omitQuery='" + omitQuery + '\'' + '}'; + return "Realm{" + + "principal='" + principal + '\'' + + ", password='" + password + '\'' + + ", scheme=" + scheme + + ", realmName='" + realmName + '\'' + + ", nonce='" + nonce + '\'' + + ", algorithm='" + algorithm + '\'' + + ", response='" + response + '\'' + + ", opaque='" + opaque + '\'' + + ", qop='" + qop + '\'' + + ", nc='" + nc + '\'' + + ", cnonce='" + cnonce + '\'' + + ", uri=" + uri + + ", usePreemptiveAuth=" + usePreemptiveAuth + + ", charset=" + charset + + ", ntlmHost='" + ntlmHost + '\'' + + ", ntlmDomain='" + ntlmDomain + '\'' + + ", useAbsoluteURI=" + useAbsoluteURI + + ", omitQuery=" + omitQuery + + ", customLoginConfig=" + customLoginConfig + + ", servicePrincipalName='" + servicePrincipalName + '\'' + + ", useCanonicalHostname=" + useCanonicalHostname + + ", loginContextName='" + loginContextName + '\'' + + '}'; } public enum AuthScheme { @@ -223,6 +272,18 @@ public static class Builder { private String ntlmHost = "localhost"; private boolean useAbsoluteURI = false; private boolean omitQuery; + /** + * Kerberos/Spnego properties + */ + private Map customLoginConfig; + private String servicePrincipalName; + private boolean useCanonicalHostname; + private String loginContextName; + + public Builder() { + this.principal = null; + this.password = null; + } public Builder(String principal, String password) { this.principal = principal; @@ -311,6 +372,26 @@ public Builder setCharset(Charset charset) { return this; } + public Builder setCustomLoginConfig(Map customLoginConfig) { + this.customLoginConfig = customLoginConfig; + return this; + } + + public Builder setServicePrincipalName(String servicePrincipalName) { + this.servicePrincipalName = servicePrincipalName; + return this; + } + + public Builder setUseCanonicalHostname(boolean useCanonicalHostname) { + this.useCanonicalHostname = useCanonicalHostname; + return this; + } + + public Builder setLoginContextName(String loginContextName) { + this.loginContextName = loginContextName; + return this; + } + private String parseRawQop(String rawQop) { String[] rawServerSupportedQops = rawQop.split(","); String[] serverSupportedQops = new String[rawServerSupportedQops.length]; @@ -501,7 +582,11 @@ public Realm build() { ntlmDomain, ntlmHost, useAbsoluteURI, - omitQuery); + omitQuery, + servicePrincipalName, + useCanonicalHostname, + customLoginConfig, + loginContextName); } } } diff --git a/client/src/main/java/org/asynchttpclient/Request.java b/client/src/main/java/org/asynchttpclient/Request.java index 0bcf3ae710..cf6a82dee2 100644 --- a/client/src/main/java/org/asynchttpclient/Request.java +++ b/client/src/main/java/org/asynchttpclient/Request.java @@ -180,4 +180,12 @@ public interface Request { * @return the NameResolver to be used to resolve hostnams's IP */ NameResolver getNameResolver(); + + /** + * @return a new request builder using this request as a prototype + */ + @SuppressWarnings("deprecation") + default RequestBuilder toBuilder() { + return new RequestBuilder(this); + } } diff --git a/client/src/main/java/org/asynchttpclient/RequestBuilder.java b/client/src/main/java/org/asynchttpclient/RequestBuilder.java index ad0a141495..4761f0c2c4 100644 --- a/client/src/main/java/org/asynchttpclient/RequestBuilder.java +++ b/client/src/main/java/org/asynchttpclient/RequestBuilder.java @@ -18,7 +18,7 @@ import static org.asynchttpclient.util.HttpConstants.Methods.GET; /** - * Builder for a {@link Request}. Warning: mutable and not thread-safe! Beware that it holds a reference on the Request instance it builds, so modifying the builder will modify the + * Builder for a {@link Request}. Warning: mutable and not thread-safe! Beware that it holds a reference to the Request instance it builds, so modifying the builder will modify the * request even after it has been built. */ public class RequestBuilder extends RequestBuilderBase { @@ -39,10 +39,15 @@ public RequestBuilder(String method, boolean disableUrlEncoding, boolean validat super(method, disableUrlEncoding, validateHeaders); } + /** + * @deprecated Use request.toBuilder() instead + */ + @Deprecated public RequestBuilder(Request prototype) { super(prototype); } + @Deprecated public RequestBuilder(Request prototype, boolean disableUrlEncoding, boolean validateHeaders) { super(prototype, disableUrlEncoding, validateHeaders); } diff --git a/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java b/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java index 4a8a9e4474..35c8145776 100644 --- a/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java +++ b/client/src/main/java/org/asynchttpclient/RequestBuilderBase.java @@ -267,7 +267,7 @@ public T setHeaders(HttpHeaders headers) { * @param headers map of header names as the map keys and header values {@link Iterable} as the map values * @return {@code this} */ - public T setHeaders(Map> headers) { + public T setHeaders(Map> headers) { clearHeaders(); if (headers != null) { headers.forEach((name, values) -> this.headers.add(name, values)); @@ -282,7 +282,7 @@ public T setHeaders(Map> headers) { * @param headers map of header names as the map keys and header values as the map values * @return {@code this} */ - public T setSingleHeaders(Map headers) { + public T setSingleHeaders(Map headers) { clearHeaders(); if (headers != null) { headers.forEach((name, value) -> this.headers.add(name, value)); diff --git a/client/src/main/java/org/asynchttpclient/Response.java b/client/src/main/java/org/asynchttpclient/Response.java index f3bc6e3a91..99f033e995 100644 --- a/client/src/main/java/org/asynchttpclient/Response.java +++ b/client/src/main/java/org/asynchttpclient/Response.java @@ -147,24 +147,29 @@ public interface Response { boolean hasResponseHeaders(); /** - * Return true if the response's body has been computed by an {@link AsyncHandler}. It will return false if the either {@link AsyncHandler#onStatusReceived(HttpResponseStatus)} - * or {@link AsyncHandler#onHeadersReceived(HttpHeaders)} returned {@link AsyncHandler.State#ABORT} + * Return true if the response's body has been computed by an {@link AsyncHandler}. + * It will return false if: + *
    + *
  • either the {@link AsyncHandler#onStatusReceived(HttpResponseStatus)} returned {@link AsyncHandler.State#ABORT}
  • + *
  • or {@link AsyncHandler#onHeadersReceived(HttpHeaders)} returned {@link AsyncHandler.State#ABORT}
  • + *
  • response body was empty
  • + *
* - * @return true if the response's body has been computed by an {@link AsyncHandler} + * @return true if the response's body has been computed by an {@link AsyncHandler} to new empty bytes */ boolean hasResponseBody(); /** - * Get remote address client initiated request to. + * Get the remote address that the client initiated the request to. * - * @return remote address client initiated request to, may be {@code null} if asynchronous provider is unable to provide the remote address + * @return The remote address that the client initiated the request to. May be {@code null} if asynchronous provider is unable to provide the remote address */ SocketAddress getRemoteAddress(); /** - * Get local address client initiated request from. + * Get the local address that the client initiated the request from. * - * @return local address client initiated request from, may be {@code null} if asynchronous provider is unable to provide the local address + * @return The local address that the client initiated the request from. May be {@code null} if asynchronous provider is unable to provide the local address */ SocketAddress getLocalAddress(); diff --git a/client/src/main/java/org/asynchttpclient/SslEngineFactory.java b/client/src/main/java/org/asynchttpclient/SslEngineFactory.java index 1157e499f3..008f1c7ee8 100644 --- a/client/src/main/java/org/asynchttpclient/SslEngineFactory.java +++ b/client/src/main/java/org/asynchttpclient/SslEngineFactory.java @@ -19,7 +19,7 @@ public interface SslEngineFactory { /** - * Creates new {@link SSLEngine}. + * Creates a new {@link SSLEngine}. * * @param config the client config * @param peerHost the peer hostname @@ -39,4 +39,12 @@ public interface SslEngineFactory { default void init(AsyncHttpClientConfig config) throws SSLException { // no op } + + /** + * Perform any necessary cleanup. + */ + default void destroy() { + // no op + } + } diff --git a/client/src/main/java/org/asynchttpclient/channel/DefaultKeepAliveStrategy.java b/client/src/main/java/org/asynchttpclient/channel/DefaultKeepAliveStrategy.java index b9fb306cf3..f1c6a5f42f 100644 --- a/client/src/main/java/org/asynchttpclient/channel/DefaultKeepAliveStrategy.java +++ b/client/src/main/java/org/asynchttpclient/channel/DefaultKeepAliveStrategy.java @@ -5,6 +5,8 @@ import io.netty.handler.codec.http.HttpUtil; import org.asynchttpclient.Request; +import java.net.InetSocketAddress; + import static io.netty.handler.codec.http.HttpHeaderValues.CLOSE; /** @@ -16,7 +18,7 @@ public class DefaultKeepAliveStrategy implements KeepAliveStrategy { * Implemented in accordance with RFC 7230 section 6.1 https://tools.ietf.org/html/rfc7230#section-6.1 */ @Override - public boolean keepAlive(Request ahcRequest, HttpRequest request, HttpResponse response) { + public boolean keepAlive(InetSocketAddress remoteAddress, Request ahcRequest, HttpRequest request, HttpResponse response) { return HttpUtil.isKeepAlive(response) && HttpUtil.isKeepAlive(request) // support non standard Proxy-Connection diff --git a/client/src/main/java/org/asynchttpclient/channel/KeepAliveStrategy.java b/client/src/main/java/org/asynchttpclient/channel/KeepAliveStrategy.java index 4d619f222c..c748fe76ac 100644 --- a/client/src/main/java/org/asynchttpclient/channel/KeepAliveStrategy.java +++ b/client/src/main/java/org/asynchttpclient/channel/KeepAliveStrategy.java @@ -17,15 +17,18 @@ import io.netty.handler.codec.http.HttpResponse; import org.asynchttpclient.Request; +import java.net.InetSocketAddress; + public interface KeepAliveStrategy { /** * Determines whether the connection should be kept alive after this HTTP message exchange. * - * @param ahcRequest the Request, as built by AHC - * @param nettyRequest the HTTP request sent to Netty - * @param nettyResponse the HTTP response received from Netty + * @param remoteAddress the remote InetSocketAddress associated with the request + * @param ahcRequest the Request, as built by AHC + * @param nettyRequest the HTTP request sent to Netty + * @param nettyResponse the HTTP response received from Netty * @return true if the connection should be kept alive, false if it should be closed. */ - boolean keepAlive(Request ahcRequest, HttpRequest nettyRequest, HttpResponse nettyResponse); + boolean keepAlive(InetSocketAddress remoteAddress, Request ahcRequest, HttpRequest nettyRequest, HttpResponse nettyResponse); } diff --git a/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java b/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java index 274537a6ad..14dcec3bfd 100644 --- a/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java +++ b/client/src/main/java/org/asynchttpclient/config/AsyncHttpClientConfigDefaults.java @@ -22,6 +22,7 @@ public final class AsyncHttpClientConfigDefaults { public static final String THREAD_POOL_NAME_CONFIG = "threadPoolName"; public static final String MAX_CONNECTIONS_CONFIG = "maxConnections"; public static final String MAX_CONNECTIONS_PER_HOST_CONFIG = "maxConnectionsPerHost"; + public static final String ACQUIRE_FREE_CHANNEL_TIMEOUT = "acquireFreeChannelTimeout"; public static final String CONNECTION_TIMEOUT_CONFIG = "connectTimeout"; public static final String POOLED_CONNECTION_IDLE_TIMEOUT_CONFIG = "pooledConnectionIdleTimeout"; public static final String CONNECTION_POOL_CLEANER_PERIOD_CONFIG = "connectionPoolCleanerPeriod"; @@ -39,7 +40,7 @@ public final class AsyncHttpClientConfigDefaults { public static final String USE_PROXY_PROPERTIES_CONFIG = "useProxyProperties"; public static final String VALIDATE_RESPONSE_HEADERS_CONFIG = "validateResponseHeaders"; public static final String AGGREGATE_WEBSOCKET_FRAME_FRAGMENTS_CONFIG = "aggregateWebSocketFrameFragments"; - public static final String ENABLE_WEBSOCKET_COMPRESSION_CONFIG= "enableWebSocketCompression"; + public static final String ENABLE_WEBSOCKET_COMPRESSION_CONFIG = "enableWebSocketCompression"; public static final String STRICT_302_HANDLING_CONFIG = "strict302Handling"; public static final String KEEP_ALIVE_CONFIG = "keepAlive"; public static final String MAX_REQUEST_RETRY_CONFIG = "maxRequestRetry"; @@ -52,6 +53,7 @@ public final class AsyncHttpClientConfigDefaults { public static final String SSL_SESSION_TIMEOUT_CONFIG = "sslSessionTimeout"; public static final String TCP_NO_DELAY_CONFIG = "tcpNoDelay"; public static final String SO_REUSE_ADDRESS_CONFIG = "soReuseAddress"; + public static final String SO_KEEP_ALIVE_CONFIG = "soKeepAlive"; public static final String SO_LINGER_CONFIG = "soLinger"; public static final String SO_SND_BUF_CONFIG = "soSndBuf"; public static final String SO_RCV_BUF_CONFIG = "soRcvBuf"; @@ -69,6 +71,9 @@ public final class AsyncHttpClientConfigDefaults { public static final String SHUTDOWN_TIMEOUT_CONFIG = "shutdownTimeout"; public static final String USE_NATIVE_TRANSPORT_CONFIG = "useNativeTransport"; public static final String IO_THREADS_COUNT_CONFIG = "ioThreadsCount"; + public static final String HASHED_WHEEL_TIMER_TICK_DURATION = "hashedWheelTimerTickDuration"; + public static final String HASHED_WHEEL_TIMER_SIZE = "hashedWheelTimerSize"; + public static final String EXPIRED_COOKIE_EVICTION_DELAY = "expiredCookieEvictionDelay"; public static final String AHC_VERSION; @@ -97,6 +102,10 @@ public static int defaultMaxConnectionsPerHost() { return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + MAX_CONNECTIONS_PER_HOST_CONFIG); } + public static int defaultAcquireFreeChannelTimeout() { + return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + ACQUIRE_FREE_CHANNEL_TIMEOUT); + } + public static int defaultConnectTimeout() { return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + CONNECTION_TIMEOUT_CONFIG); } @@ -217,6 +226,10 @@ public static boolean defaultSoReuseAddress() { return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getBoolean(ASYNC_CLIENT_CONFIG_ROOT + SO_REUSE_ADDRESS_CONFIG); } + public static boolean defaultSoKeepAlive() { + return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getBoolean(ASYNC_CLIENT_CONFIG_ROOT + SO_KEEP_ALIVE_CONFIG); + } + public static int defaultSoLinger() { return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + SO_LINGER_CONFIG); } @@ -284,4 +297,16 @@ public static boolean defaultUseNativeTransport() { public static int defaultIoThreadsCount() { return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + IO_THREADS_COUNT_CONFIG); } + + public static int defaultHashedWheelTimerTickDuration() { + return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + HASHED_WHEEL_TIMER_TICK_DURATION); + } + + public static int defaultHashedWheelTimerSize() { + return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + HASHED_WHEEL_TIMER_SIZE); + } + + public static int defaultExpiredCookieEvictionDelay() { + return AsyncHttpClientConfigHelper.getAsyncHttpClientConfig().getInt(ASYNC_CLIENT_CONFIG_ROOT + EXPIRED_COOKIE_EVICTION_DELAY); + } } diff --git a/client/src/main/java/org/asynchttpclient/cookie/CookieEvictionTask.java b/client/src/main/java/org/asynchttpclient/cookie/CookieEvictionTask.java new file mode 100644 index 0000000000..b5ce4aed0a --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/cookie/CookieEvictionTask.java @@ -0,0 +1,30 @@ +package org.asynchttpclient.cookie; + +import java.util.concurrent.TimeUnit; + +import org.asynchttpclient.AsyncHttpClientConfig; + +import io.netty.util.Timeout; +import io.netty.util.TimerTask; + +/** + * Evicts expired cookies from the {@linkplain CookieStore} periodically. + * The default delay is 30 seconds. You may override the default using + * {@linkplain AsyncHttpClientConfig#expiredCookieEvictionDelay()}. + */ +public class CookieEvictionTask implements TimerTask { + + private final long evictDelayInMs; + private final CookieStore cookieStore; + + public CookieEvictionTask(long evictDelayInMs, CookieStore cookieStore) { + this.evictDelayInMs = evictDelayInMs; + this.cookieStore = cookieStore; + } + + @Override + public void run(Timeout timeout) throws Exception { + cookieStore.evictExpired(); + timeout.timer().newTimeout(this, evictDelayInMs, TimeUnit.MILLISECONDS); + } +} diff --git a/client/src/main/java/org/asynchttpclient/cookie/CookieStore.java b/client/src/main/java/org/asynchttpclient/cookie/CookieStore.java index 0c5ad544ed..6cd540226c 100644 --- a/client/src/main/java/org/asynchttpclient/cookie/CookieStore.java +++ b/client/src/main/java/org/asynchttpclient/cookie/CookieStore.java @@ -16,6 +16,7 @@ import io.netty.handler.codec.http.cookie.Cookie; import org.asynchttpclient.uri.Uri; +import org.asynchttpclient.util.Counted; import java.net.CookieManager; import java.util.List; @@ -31,10 +32,10 @@ * * @since 2.1 */ -public interface CookieStore { +public interface CookieStore extends Counted { /** * Adds one {@link Cookie} to the store. This is called for every incoming HTTP response. - * If the given cookie has already expired it will not be added, but existing values will still be removed. + * If the given cookie has already expired it will not be added. * *

A cookie to store may or may not be associated with an URI. If it * is not associated with an URI, the cookie's domain and path attribute @@ -82,4 +83,9 @@ public interface CookieStore { * @return true if any cookies were purged. */ boolean clear(); + + /** + * Evicts all the cookies that expired as of the time this method is run. + */ + void evictExpired(); } diff --git a/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java b/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java index 277db387ce..8cdc29f45e 100644 --- a/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java +++ b/client/src/main/java/org/asynchttpclient/cookie/ThreadSafeCookieStore.java @@ -21,12 +21,14 @@ import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; import java.util.stream.Collectors; public final class ThreadSafeCookieStore implements CookieStore { - private Map cookieJar = new ConcurrentHashMap<>(); + private final Map> cookieJar = new ConcurrentHashMap<>(); + private final AtomicInteger counter = new AtomicInteger(); @Override public void add(Uri uri, Cookie cookie) { @@ -43,28 +45,29 @@ public List get(Uri uri) { @Override public List getAll() { - final boolean[] removeExpired = {false}; List result = cookieJar - .entrySet() + .values() .stream() - .filter(pair -> { - boolean hasCookieExpired = hasCookieExpired(pair.getValue().cookie, pair.getValue().createdAt); - if (hasCookieExpired && !removeExpired[0]) - removeExpired[0] = true; - return !hasCookieExpired; - }) - .map(pair -> pair.getValue().cookie) + .flatMap(map -> map.values().stream()) + .filter(pair -> !hasCookieExpired(pair.cookie, pair.createdAt)) + .map(pair -> pair.cookie) .collect(Collectors.toList()); - if (removeExpired[0]) - removeExpired(); - return result; } @Override public boolean remove(Predicate predicate) { - return cookieJar.entrySet().removeIf(v -> predicate.test(v.getValue().cookie)); + final boolean[] removed = {false}; + cookieJar.forEach((key, value) -> { + if (!removed[0]) { + removed[0] = value.entrySet().removeIf(v -> predicate.test(v.getValue().cookie)); + } + }); + if (removed[0]) { + cookieJar.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue().isEmpty()); + } + return removed[0]; } @Override @@ -74,8 +77,33 @@ public boolean clear() { return result; } + @Override + public void evictExpired() { + removeExpired(); + } + + + @Override + public int incrementAndGet() { + return counter.incrementAndGet(); + } + + @Override + public int decrementAndGet() { + return counter.decrementAndGet(); + } + + @Override + public int count() { + return counter.get(); + } + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + public Map> getUnderlying() { + return new HashMap<>(cookieJar); + } + private String requestDomain(Uri requestUri) { return requestUri.getHost().toLowerCase(); } @@ -126,13 +154,6 @@ private boolean hasCookieExpired(Cookie cookie, long whenCreated) { return false; } - // rfc6265#section-5.1.3 - // check "The string is a host name (i.e., not an IP address)" ignored - private boolean domainsMatch(String cookieDomain, String requestDomain, boolean hostOnly) { - return (hostOnly && Objects.equals(requestDomain, cookieDomain)) || - (Objects.equals(requestDomain, cookieDomain) || requestDomain.endsWith("." + cookieDomain)); - } - // rfc6265#section-5.1.4 private boolean pathsMatch(String cookiePath, String requestPath) { return Objects.equals(cookiePath, requestPath) || @@ -140,50 +161,73 @@ private boolean pathsMatch(String cookiePath, String requestPath) { } private void add(String requestDomain, String requestPath, Cookie cookie) { - AbstractMap.SimpleEntry pair = cookieDomain(cookie.domain(), requestDomain); String keyDomain = pair.getKey(); boolean hostOnly = pair.getValue(); String keyPath = cookiePath(cookie.path(), requestPath); - CookieKey key = new CookieKey(cookie.name().toLowerCase(), keyDomain, keyPath); + CookieKey key = new CookieKey(cookie.name().toLowerCase(), keyPath); if (hasCookieExpired(cookie, 0)) - cookieJar.remove(key); - else - cookieJar.put(key, new StoredCookie(cookie, hostOnly, cookie.maxAge() != Cookie.UNDEFINED_MAX_AGE)); + cookieJar.getOrDefault(keyDomain, Collections.emptyMap()).remove(key); + else { + final Map innerMap = cookieJar.computeIfAbsent(keyDomain, domain -> new ConcurrentHashMap<>()); + innerMap.put(key, new StoredCookie(cookie, hostOnly, cookie.maxAge() != Cookie.UNDEFINED_MAX_AGE)); + } } private List get(String domain, String path, boolean secure) { + boolean exactDomainMatch = true; + String subDomain = domain; + List results = null; + + while (MiscUtils.isNonEmpty(subDomain)) { + final List storedCookies = getStoredCookies(subDomain, path, secure, exactDomainMatch); + subDomain = DomainUtils.getSubDomain(subDomain); + exactDomainMatch = false; + if (storedCookies.isEmpty()) { + continue; + } + if (results == null) { + results = new ArrayList<>(4); + } + results.addAll(storedCookies); + } - final boolean[] removeExpired = {false}; + return results == null ? Collections.emptyList() : results; + } - List result = cookieJar.entrySet().stream().filter(pair -> { + private List getStoredCookies(String domain, String path, boolean secure, boolean isExactMatch) { + final Map innerMap = cookieJar.get(domain); + if (innerMap == null) { + return Collections.emptyList(); + } + + return innerMap.entrySet().stream().filter(pair -> { CookieKey key = pair.getKey(); StoredCookie storedCookie = pair.getValue(); boolean hasCookieExpired = hasCookieExpired(storedCookie.cookie, storedCookie.createdAt); - if (hasCookieExpired && !removeExpired[0]) - removeExpired[0] = true; - return !hasCookieExpired && domainsMatch(key.domain, domain, storedCookie.hostOnly) && pathsMatch(key.path, path) && (secure || !storedCookie.cookie.isSecure()); + return !hasCookieExpired && + (isExactMatch || !storedCookie.hostOnly) && + pathsMatch(key.path, path) && + (secure || !storedCookie.cookie.isSecure()); }).map(v -> v.getValue().cookie).collect(Collectors.toList()); - - if (removeExpired[0]) - removeExpired(); - - return result; } private void removeExpired() { - cookieJar.entrySet().removeIf(v -> hasCookieExpired(v.getValue().cookie, v.getValue().createdAt)); + final boolean[] removed = {false}; + cookieJar.values().forEach(cookieMap -> removed[0] |= cookieMap.entrySet().removeIf( + v -> hasCookieExpired(v.getValue().cookie, v.getValue().createdAt))); + if (removed[0]) { + cookieJar.entrySet().removeIf(entry -> entry.getValue() == null || entry.getValue().isEmpty()); + } } private static class CookieKey implements Comparable { final String name; - final String domain; final String path; - CookieKey(String name, String domain, String path) { + CookieKey(String name, String path) { this.name = name; - this.domain = domain; this.path = path; } @@ -192,7 +236,6 @@ public int compareTo(CookieKey o) { Assertions.assertNotNull(o, "Parameter can't be null"); int result; if ((result = this.name.compareTo(o.name)) == 0) - if ((result = this.domain.compareTo(o.domain)) == 0) result = this.path.compareTo(o.path); return result; @@ -207,14 +250,13 @@ public boolean equals(Object obj) { public int hashCode() { int result = 17; result = 31 * result + name.hashCode(); - result = 31 * result + domain.hashCode(); result = 31 * result + path.hashCode(); return result; } @Override public String toString() { - return String.format("%s: %s; %s", name, domain, path); + return String.format("%s: %s", name, path); } } @@ -235,4 +277,20 @@ public String toString() { return String.format("%s; hostOnly %s; persistent %s", cookie.toString(), hostOnly, persistent); } } + + public static final class DomainUtils { + private static final char DOT = '.'; + public static String getSubDomain(String domain) { + if (domain == null || domain.isEmpty()) { + return null; + } + final int indexOfDot = domain.indexOf(DOT); + if (indexOfDot == -1) { + return null; + } + return domain.substring(indexOfDot + 1); + } + + private DomainUtils() {} + } } diff --git a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableAsyncHandler.java b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableAsyncHandler.java index 399638fbb2..d6b671a270 100644 --- a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableAsyncHandler.java +++ b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableAsyncHandler.java @@ -198,7 +198,7 @@ public Request adjustRequestRange(Request request) { byteTransferred.set(resumableListener.length()); } - RequestBuilder builder = new RequestBuilder(request); + RequestBuilder builder = request.toBuilder(); if (request.getHeaders().get(RANGE) == null && byteTransferred.get() != 0) { builder.setHeader(RANGE, "bytes=" + byteTransferred.get() + "-"); } diff --git a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableListener.java b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableListener.java index 4e36d74304..03472bd085 100644 --- a/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableListener.java +++ b/client/src/main/java/org/asynchttpclient/handler/resumable/ResumableListener.java @@ -29,7 +29,7 @@ public interface ResumableListener { void onBytesReceived(ByteBuffer byteBuffer) throws IOException; /** - * Invoked when all the bytes has been sucessfully transferred. + * Invoked when all the bytes has been successfully transferred. */ void onAllBytesReceived(); diff --git a/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java b/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java index cb3b05fbd8..dddebfff41 100755 --- a/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java +++ b/client/src/main/java/org/asynchttpclient/netty/NettyResponseFuture.java @@ -90,7 +90,6 @@ public final class NettyResponseFuture implements ListenableFuture { private volatile int isCancelled = 0; private volatile int inAuth = 0; private volatile int inProxyAuth = 0; - private volatile int statusReceived = 0; @SuppressWarnings("unused") private volatile int contentProcessed = 0; @SuppressWarnings("unused") @@ -367,7 +366,10 @@ public TimeoutsHolder getTimeoutsHolder() { } public void setTimeoutsHolder(TimeoutsHolder timeoutsHolder) { - TIMEOUTS_HOLDER_FIELD.set(this, timeoutsHolder); + TimeoutsHolder ref = TIMEOUTS_HOLDER_FIELD.getAndSet(this, timeoutsHolder); + if (ref != null) { + ref.cancel(); + } } public boolean isInAuth() { @@ -539,7 +541,6 @@ public String toString() { ",\n\tredirectCount=" + redirectCount + // ",\n\ttimeoutsHolder=" + TIMEOUTS_HOLDER_FIELD.get(this) + // ",\n\tinAuth=" + inAuth + // - ",\n\tstatusReceived=" + statusReceived + // ",\n\ttouch=" + touch + // '}'; } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java index 8deb22e53a..b93dfb380e 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/ChannelManager.java @@ -16,10 +16,12 @@ import io.netty.bootstrap.Bootstrap; import io.netty.buffer.ByteBufAllocator; import io.netty.channel.*; +import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.group.ChannelGroup; +import io.netty.channel.group.ChannelGroupFuture; import io.netty.channel.group.DefaultChannelGroup; +import io.netty.channel.kqueue.KQueueEventLoopGroup; import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.oio.OioEventLoopGroup; import io.netty.handler.codec.http.HttpClientCodec; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.websocketx.WebSocket08FrameDecoder; @@ -36,6 +38,7 @@ import io.netty.resolver.NameResolver; import io.netty.util.Timer; import io.netty.util.concurrent.*; +import io.netty.util.internal.PlatformDependent; import org.asynchttpclient.*; import org.asynchttpclient.channel.ChannelPool; import org.asynchttpclient.channel.ChannelPoolPartitioning; @@ -58,6 +61,7 @@ import java.net.InetSocketAddress; import java.util.Map; import java.util.Map.Entry; +import java.util.Objects; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; import java.util.function.Function; @@ -119,31 +123,31 @@ public ChannelManager(final AsyncHttpClientConfig config, Timer nettyTimer) { // check if external EventLoopGroup is defined ThreadFactory threadFactory = config.getThreadFactory() != null ? config.getThreadFactory() : new DefaultThreadFactory(config.getThreadPoolName()); allowReleaseEventLoopGroup = config.getEventLoopGroup() == null; - ChannelFactory channelFactory; + TransportFactory transportFactory; if (allowReleaseEventLoopGroup) { if (config.isUseNativeTransport()) { - eventLoopGroup = newEpollEventLoopGroup(config.getIoThreadsCount(), threadFactory); - channelFactory = getEpollSocketChannelFactory(); - + transportFactory = getNativeTransportFactory(); } else { - eventLoopGroup = new NioEventLoopGroup(config.getIoThreadsCount(), threadFactory); - channelFactory = NioSocketChannelFactory.INSTANCE; + transportFactory = NioTransportFactory.INSTANCE; } + eventLoopGroup = transportFactory.newEventLoopGroup(config.getIoThreadsCount(), threadFactory); } else { eventLoopGroup = config.getEventLoopGroup(); - if (eventLoopGroup instanceof OioEventLoopGroup) - throw new IllegalArgumentException("Oio is not supported"); if (eventLoopGroup instanceof NioEventLoopGroup) { - channelFactory = NioSocketChannelFactory.INSTANCE; + transportFactory = NioTransportFactory.INSTANCE; + } else if (eventLoopGroup instanceof EpollEventLoopGroup) { + transportFactory = new EpollTransportFactory(); + } else if (eventLoopGroup instanceof KQueueEventLoopGroup) { + transportFactory = new KQueueTransportFactory(); } else { - channelFactory = getEpollSocketChannelFactory(); + throw new IllegalArgumentException("Unknown event loop group " + eventLoopGroup.getClass().getSimpleName()); } } - httpBootstrap = newBootstrap(channelFactory, eventLoopGroup, config); - wsBootstrap = newBootstrap(channelFactory, eventLoopGroup, config); + httpBootstrap = newBootstrap(transportFactory, eventLoopGroup, config); + wsBootstrap = newBootstrap(transportFactory, eventLoopGroup, config); // for reactive streams httpBootstrap.option(ChannelOption.AUTO_READ, false); @@ -159,6 +163,7 @@ private Bootstrap newBootstrap(ChannelFactory channelFactory, .option(ChannelOption.ALLOCATOR, config.getAllocator() != null ? config.getAllocator() : ByteBufAllocator.DEFAULT) .option(ChannelOption.TCP_NODELAY, config.isTcpNoDelay()) .option(ChannelOption.SO_REUSEADDR, config.isSoReuseAddress()) + .option(ChannelOption.SO_KEEPALIVE, config.isSoKeepAlive()) .option(ChannelOption.AUTO_CLOSE, false); if (config.getConnectTimeout() > 0) { @@ -184,22 +189,22 @@ private Bootstrap newBootstrap(ChannelFactory channelFactory, return bootstrap; } - private EventLoopGroup newEpollEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { - try { - Class epollEventLoopGroupClass = Class.forName("io.netty.channel.epoll.EpollEventLoopGroup"); - return (EventLoopGroup) epollEventLoopGroupClass.getConstructor(int.class, ThreadFactory.class).newInstance(ioThreadsCount, threadFactory); - } catch (Exception e) { - throw new IllegalArgumentException(e); + @SuppressWarnings("unchecked") + private TransportFactory getNativeTransportFactory() { + String nativeTransportFactoryClassName = null; + if (PlatformDependent.isOsx()) { + nativeTransportFactoryClassName = "org.asynchttpclient.netty.channel.KQueueTransportFactory"; + } else if (!PlatformDependent.isWindows()) { + nativeTransportFactoryClassName = "org.asynchttpclient.netty.channel.EpollTransportFactory"; } - } - @SuppressWarnings("unchecked") - private ChannelFactory getEpollSocketChannelFactory() { try { - return (ChannelFactory) Class.forName("org.asynchttpclient.netty.channel.EpollSocketChannelFactory").newInstance(); + if (nativeTransportFactoryClassName != null) { + return (TransportFactory) Class.forName(nativeTransportFactoryClassName).newInstance(); + } } catch (Exception e) { - throw new IllegalArgumentException(e); } + throw new IllegalArgumentException("No suitable native transport (epoll or kqueue) available"); } public void configureBootstraps(NettyRequestSender requestSender) { @@ -291,8 +296,9 @@ public void removeAll(Channel connection) { } private void doClose() { - openChannels.close(); + ChannelGroupFuture groupFuture = openChannels.close(); channelPool.destroy(); + groupFuture.addListener(future -> sslEngineFactory.destroy()); } public void close() { @@ -336,7 +342,7 @@ private SslHandler createSslHandler(String peerHost, int peerPort) { public Future updatePipelineForHttpTunneling(ChannelPipeline pipeline, Uri requestUri) { - Future whenHanshaked = null; + Future whenHandshaked = null; if (pipeline.get(HTTP_CLIENT_CODEC) != null) pipeline.remove(HTTP_CLIENT_CODEC); @@ -344,8 +350,8 @@ public Future updatePipelineForHttpTunneling(ChannelPipeline pipeline, if (requestUri.isSecured()) { if (!isSslHandlerConfigured(pipeline)) { SslHandler sslHandler = createSslHandler(requestUri.getHost(), requestUri.getExplicitPort()); - whenHanshaked = sslHandler.handshakeFuture(); - pipeline.addBefore(AHC_HTTP_HANDLER, SSL_HANDLER, sslHandler); + whenHandshaked = sslHandler.handshakeFuture(); + pipeline.addBefore(INFLATER_HANDLER, SSL_HANDLER, sslHandler); } pipeline.addAfter(SSL_HANDLER, HTTP_CLIENT_CODEC, newHttpClientCodec()); @@ -355,9 +361,14 @@ public Future updatePipelineForHttpTunneling(ChannelPipeline pipeline, if (requestUri.isWebSocket()) { pipeline.addAfter(AHC_HTTP_HANDLER, AHC_WS_HANDLER, wsHandler); + + if (config.isEnableWebSocketCompression()) { + pipeline.addBefore(AHC_WS_HANDLER, WS_COMPRESSOR_HANDLER, WebSocketClientCompressionHandler.INSTANCE); + } + pipeline.remove(AHC_HTTP_HANDLER); } - return whenHanshaked; + return whenHandshaked; } public SslHandler addSslHandler(ChannelPipeline pipeline, Uri uri, String virtualHost, boolean hasSocksProxyHandler) { @@ -380,10 +391,11 @@ public SslHandler addSslHandler(ChannelPipeline pipeline, Uri uri, String virtua } SslHandler sslHandler = createSslHandler(peerHost, peerPort); - if (hasSocksProxyHandler) + if (hasSocksProxyHandler) { pipeline.addAfter(SOCKS_HANDLER, SSL_HANDLER, sslHandler); - else + } else { pipeline.addFirst(SSL_HANDLER, sslHandler); + } return sslHandler; } @@ -479,8 +491,8 @@ public EventLoopGroup getEventLoopGroup() { } public ClientStats getClientStats() { - Map totalConnectionsPerHost = openChannels.stream().map(Channel::remoteAddress).filter(a -> a.getClass() == InetSocketAddress.class) - .map(a -> (InetSocketAddress) a).map(InetSocketAddress::getHostName).collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + Map totalConnectionsPerHost = openChannels.stream().map(Channel::remoteAddress).filter(a -> a instanceof InetSocketAddress) + .map(a -> (InetSocketAddress) a).map(InetSocketAddress::getHostString).collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); Map idleConnectionsPerHost = channelPool.getIdleChannelCountPerHost(); Map statsPerHost = totalConnectionsPerHost.entrySet().stream().collect(Collectors.toMap(Entry::getKey, entry -> { final long totalConnectionCount = entry.getValue(); diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/CombinedConnectionSemaphore.java b/client/src/main/java/org/asynchttpclient/netty/channel/CombinedConnectionSemaphore.java new file mode 100644 index 0000000000..04549fd80d --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/netty/channel/CombinedConnectionSemaphore.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2018 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.channel; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +/** + * A combined {@link ConnectionSemaphore} with two limits - a global limit and a per-host limit + */ +public class CombinedConnectionSemaphore extends PerHostConnectionSemaphore { + protected final MaxConnectionSemaphore globalMaxConnectionSemaphore; + + CombinedConnectionSemaphore(int maxConnections, int maxConnectionsPerHost, int acquireTimeout) { + super(maxConnectionsPerHost, acquireTimeout); + this.globalMaxConnectionSemaphore = new MaxConnectionSemaphore(maxConnections, acquireTimeout); + } + + @Override + public void acquireChannelLock(Object partitionKey) throws IOException { + long remainingTime = super.acquireTimeout > 0 ? acquireGlobalTimed(partitionKey) : acquireGlobal(partitionKey); + + try { + if (remainingTime < 0 || !getFreeConnectionsForHost(partitionKey).tryAcquire(remainingTime, TimeUnit.MILLISECONDS)) { + releaseGlobal(partitionKey); + throw tooManyConnectionsPerHost; + } + } catch (InterruptedException e) { + releaseGlobal(partitionKey); + throw new RuntimeException(e); + } + } + + protected void releaseGlobal(Object partitionKey) { + this.globalMaxConnectionSemaphore.releaseChannelLock(partitionKey); + } + + protected long acquireGlobal(Object partitionKey) throws IOException { + this.globalMaxConnectionSemaphore.acquireChannelLock(partitionKey); + return 0; + } + + /* + * Acquires the global lock and returns the remaining time, in millis, to acquire the per-host lock + */ + protected long acquireGlobalTimed(Object partitionKey) throws IOException { + long beforeGlobalAcquire = System.currentTimeMillis(); + acquireGlobal(partitionKey); + long lockTime = System.currentTimeMillis() - beforeGlobalAcquire; + return this.acquireTimeout - lockTime; + } + + @Override + public void releaseChannelLock(Object partitionKey) { + this.globalMaxConnectionSemaphore.releaseChannelLock(partitionKey); + super.releaseChannelLock(partitionKey); + } +} diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/DefaultChannelPool.java b/client/src/main/java/org/asynchttpclient/netty/channel/DefaultChannelPool.java index 9988421595..f9c08b973b 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/DefaultChannelPool.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/DefaultChannelPool.java @@ -222,7 +222,7 @@ public Map getIdleChannelCountPerHost() { .map(idle -> idle.getChannel().remoteAddress()) .filter(a -> a.getClass() == InetSocketAddress.class) .map(a -> (InetSocketAddress) a) - .map(InetSocketAddress::getHostName) + .map(InetSocketAddress::getHostString) .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/DefaultConnectionSemaphoreFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/DefaultConnectionSemaphoreFactory.java index a102f1def8..eba42186ee 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/DefaultConnectionSemaphoreFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/DefaultConnectionSemaphoreFactory.java @@ -17,14 +17,21 @@ public class DefaultConnectionSemaphoreFactory implements ConnectionSemaphoreFactory { - public ConnectionSemaphore newConnectionSemaphore(AsyncHttpClientConfig config) { - ConnectionSemaphore semaphore = new NoopConnectionSemaphore(); - if (config.getMaxConnections() > 0) { - semaphore = new MaxConnectionSemaphore(config.getMaxConnections()); - } - if (config.getMaxConnectionsPerHost() > 0) { - semaphore = new PerHostConnectionSemaphore(config.getMaxConnectionsPerHost(), semaphore); - } - return semaphore; + public ConnectionSemaphore newConnectionSemaphore(AsyncHttpClientConfig config) { + int acquireFreeChannelTimeout = Math.max(0, config.getAcquireFreeChannelTimeout()); + int maxConnections = config.getMaxConnections(); + int maxConnectionsPerHost = config.getMaxConnectionsPerHost(); + + if (maxConnections > 0 && maxConnectionsPerHost > 0) { + return new CombinedConnectionSemaphore(maxConnections, maxConnectionsPerHost, acquireFreeChannelTimeout); + } + if (maxConnections > 0) { + return new MaxConnectionSemaphore(maxConnections, acquireFreeChannelTimeout); } + if (maxConnectionsPerHost > 0) { + return new CombinedConnectionSemaphore(maxConnections, maxConnectionsPerHost, acquireFreeChannelTimeout); + } + + return new NoopConnectionSemaphore(); + } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/EpollSocketChannelFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java similarity index 54% rename from client/src/main/java/org/asynchttpclient/netty/channel/EpollSocketChannelFactory.java rename to client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java index c6970b6d6c..8f84272916 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/EpollSocketChannelFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/EpollTransportFactory.java @@ -13,13 +13,32 @@ */ package org.asynchttpclient.netty.channel; -import io.netty.channel.ChannelFactory; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollSocketChannel; -class EpollSocketChannelFactory implements ChannelFactory { +import java.util.concurrent.ThreadFactory; + +class EpollTransportFactory implements TransportFactory { + + EpollTransportFactory() { + try { + Class.forName("io.netty.channel.epoll.Epoll"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("The epoll transport is not available"); + } + if (!Epoll.isAvailable()) { + throw new IllegalStateException("The epoll transport is not supported"); + } + } @Override public EpollSocketChannel newChannel() { return new EpollSocketChannel(); } + + @Override + public EpollEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { + return new EpollEventLoopGroup(ioThreadsCount, threadFactory); + } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/InfiniteSemaphore.java b/client/src/main/java/org/asynchttpclient/netty/channel/InfiniteSemaphore.java new file mode 100644 index 0000000000..97b8224739 --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/netty/channel/InfiniteSemaphore.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2018 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.channel; + +import java.util.Collection; +import java.util.Collections; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; + +/** + * A java.util.concurrent.Semaphore that always has Integer.Integer.MAX_VALUE free permits + * + * @author Alex Maltinsky + */ +public class InfiniteSemaphore extends Semaphore { + + public static final InfiniteSemaphore INSTANCE = new InfiniteSemaphore(); + private static final long serialVersionUID = 1L; + + private InfiniteSemaphore() { + super(Integer.MAX_VALUE); + } + + @Override + public void acquire() { + // NO-OP + } + + @Override + public void acquireUninterruptibly() { + // NO-OP + } + + @Override + public boolean tryAcquire() { + return true; + } + + @Override + public boolean tryAcquire(long timeout, TimeUnit unit) { + return true; + } + + @Override + public void release() { + // NO-OP + } + + @Override + public void acquire(int permits) { + // NO-OP + } + + @Override + public void acquireUninterruptibly(int permits) { + // NO-OP + } + + @Override + public boolean tryAcquire(int permits) { + return true; + } + + @Override + public boolean tryAcquire(int permits, long timeout, TimeUnit unit) { + return true; + } + + @Override + public void release(int permits) { + // NO-OP + } + + @Override + public int availablePermits() { + return Integer.MAX_VALUE; + } + + @Override + public int drainPermits() { + return Integer.MAX_VALUE; + } + + @Override + protected void reducePermits(int reduction) { + // NO-OP + } + + @Override + public boolean isFair() { + return true; + } + + @Override + protected Collection getQueuedThreads() { + return Collections.emptyList(); + } +} + diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java new file mode 100644 index 0000000000..f54fe46157 --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/netty/channel/KQueueTransportFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2019 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.channel; + +import io.netty.channel.kqueue.KQueue; +import io.netty.channel.kqueue.KQueueEventLoopGroup; +import io.netty.channel.kqueue.KQueueSocketChannel; + +import java.util.concurrent.ThreadFactory; + +class KQueueTransportFactory implements TransportFactory { + + KQueueTransportFactory() { + try { + Class.forName("io.netty.channel.kqueue.KQueue"); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("The kqueue transport is not available"); + } + if (!KQueue.isAvailable()) { + throw new IllegalStateException("The kqueue transport is not supported"); + } + } + + @Override + public KQueueSocketChannel newChannel() { + return new KQueueSocketChannel(); + } + + @Override + public KQueueEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { + return new KQueueEventLoopGroup(ioThreadsCount, threadFactory); + } +} diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/MaxConnectionSemaphore.java b/client/src/main/java/org/asynchttpclient/netty/channel/MaxConnectionSemaphore.java index 99bd6a4be4..99c318afac 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/MaxConnectionSemaphore.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/MaxConnectionSemaphore.java @@ -16,6 +16,8 @@ import org.asynchttpclient.exception.TooManyConnectionsException; import java.io.IOException; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; import static org.asynchttpclient.util.ThrowableUtil.unknownStackTrace; @@ -23,21 +25,29 @@ * Max connections limiter. * * @author Stepan Koltsov + * @author Alex Maltinsky */ public class MaxConnectionSemaphore implements ConnectionSemaphore { - private final NonBlockingSemaphoreLike freeChannels; - private final IOException tooManyConnections; + protected final Semaphore freeChannels; + protected final IOException tooManyConnections; + protected final int acquireTimeout; - MaxConnectionSemaphore(int maxConnections) { + MaxConnectionSemaphore(int maxConnections, int acquireTimeout) { tooManyConnections = unknownStackTrace(new TooManyConnectionsException(maxConnections), MaxConnectionSemaphore.class, "acquireChannelLock"); - freeChannels = maxConnections > 0 ? new NonBlockingSemaphore(maxConnections) : NonBlockingSemaphoreInfinite.INSTANCE; + freeChannels = maxConnections > 0 ? new Semaphore(maxConnections) : InfiniteSemaphore.INSTANCE; + this.acquireTimeout = Math.max(0, acquireTimeout); } @Override public void acquireChannelLock(Object partitionKey) throws IOException { - if (!freeChannels.tryAcquire()) - throw tooManyConnections; + try { + if (!freeChannels.tryAcquire(acquireTimeout, TimeUnit.MILLISECONDS)) { + throw tooManyConnections; + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } } @Override diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java b/client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java index 76bd652a44..4a6f4dce20 100755 --- a/client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/NettyConnectListener.java @@ -130,7 +130,7 @@ public void onSuccess(Channel channel, InetSocketAddress remoteAddress) { @Override protected void onSuccess(Channel value) { try { - asyncHandler.onTlsHandshakeSuccess(); + asyncHandler.onTlsHandshakeSuccess(sslHandler.engine().getSession()); } catch (Exception e) { LOGGER.error("onTlsHandshakeSuccess crashed", e); NettyConnectListener.this.onFailure(channel, e); diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreInfinite.java b/client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java similarity index 57% rename from client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreInfinite.java rename to client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java index 3d4fb91dbd..d691ff270a 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreInfinite.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/NioTransportFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved. + * Copyright (c) 2019 AsyncHttpClient Project. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. @@ -13,27 +13,22 @@ */ package org.asynchttpclient.netty.channel; -/** - * Non-blocking semaphore-like object with infinite permits. - *

- * So try-acquire always succeeds. - * - * @author Stepan Koltsov - */ -enum NonBlockingSemaphoreInfinite implements NonBlockingSemaphoreLike { - INSTANCE; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioSocketChannel; - @Override - public void release() { - } +import java.util.concurrent.ThreadFactory; + +enum NioTransportFactory implements TransportFactory { + + INSTANCE; @Override - public boolean tryAcquire() { - return true; + public NioSocketChannel newChannel() { + return new NioSocketChannel(); } @Override - public String toString() { - return NonBlockingSemaphore.class.getName(); + public NioEventLoopGroup newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory) { + return new NioEventLoopGroup(ioThreadsCount, threadFactory); } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphore.java b/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphore.java deleted file mode 100644 index a7bd2eacfe..0000000000 --- a/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphore.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ -package org.asynchttpclient.netty.channel; - -import java.util.concurrent.atomic.AtomicInteger; - -/** - * Semaphore-like API, but without blocking. - * - * @author Stepan Koltsov - */ -class NonBlockingSemaphore implements NonBlockingSemaphoreLike { - - private final AtomicInteger permits; - - NonBlockingSemaphore(int permits) { - this.permits = new AtomicInteger(permits); - } - - @Override - public void release() { - permits.incrementAndGet(); - } - - @Override - public boolean tryAcquire() { - for (; ; ) { - int count = permits.get(); - if (count <= 0) { - return false; - } - if (permits.compareAndSet(count, count - 1)) { - return true; - } - } - } - - @Override - public String toString() { - // mimic toString of Semaphore class - return super.toString() + "[Permits = " + permits + "]"; - } -} diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreLike.java b/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreLike.java deleted file mode 100644 index 44303c9dfc..0000000000 --- a/client/src/main/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreLike.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ -package org.asynchttpclient.netty.channel; - -/** - * Non-blocking semaphore API. - * - * @author Stepan Koltsov - */ -interface NonBlockingSemaphoreLike { - void release(); - - boolean tryAcquire(); -} diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/PerHostConnectionSemaphore.java b/client/src/main/java/org/asynchttpclient/netty/channel/PerHostConnectionSemaphore.java index 5ebb348abf..9ce1f20e93 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/PerHostConnectionSemaphore.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/PerHostConnectionSemaphore.java @@ -17,6 +17,8 @@ import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeUnit; import static org.asynchttpclient.util.ThrowableUtil.unknownStackTrace; @@ -25,37 +27,36 @@ */ public class PerHostConnectionSemaphore implements ConnectionSemaphore { - private final ConnectionSemaphore globalSemaphore; + protected final ConcurrentHashMap freeChannelsPerHost = new ConcurrentHashMap<>(); + protected final int maxConnectionsPerHost; + protected final IOException tooManyConnectionsPerHost; + protected final int acquireTimeout; - private final ConcurrentHashMap freeChannelsPerHost = new ConcurrentHashMap<>(); - private final int maxConnectionsPerHost; - private final IOException tooManyConnectionsPerHost; - - PerHostConnectionSemaphore(int maxConnectionsPerHost, ConnectionSemaphore globalSemaphore) { - this.globalSemaphore = globalSemaphore; + PerHostConnectionSemaphore(int maxConnectionsPerHost, int acquireTimeout) { tooManyConnectionsPerHost = unknownStackTrace(new TooManyConnectionsPerHostException(maxConnectionsPerHost), PerHostConnectionSemaphore.class, "acquireChannelLock"); this.maxConnectionsPerHost = maxConnectionsPerHost; + this.acquireTimeout = Math.max(0, acquireTimeout); } @Override public void acquireChannelLock(Object partitionKey) throws IOException { - globalSemaphore.acquireChannelLock(partitionKey); - - if (!getFreeConnectionsForHost(partitionKey).tryAcquire()) { - globalSemaphore.releaseChannelLock(partitionKey); - throw tooManyConnectionsPerHost; + try { + if (!getFreeConnectionsForHost(partitionKey).tryAcquire(acquireTimeout, TimeUnit.MILLISECONDS)) { + throw tooManyConnectionsPerHost; + } + } catch (InterruptedException e) { + throw new RuntimeException(e); } } @Override public void releaseChannelLock(Object partitionKey) { - globalSemaphore.releaseChannelLock(partitionKey); getFreeConnectionsForHost(partitionKey).release(); } - private NonBlockingSemaphoreLike getFreeConnectionsForHost(Object partitionKey) { + protected Semaphore getFreeConnectionsForHost(Object partitionKey) { return maxConnectionsPerHost > 0 ? - freeChannelsPerHost.computeIfAbsent(partitionKey, pk -> new NonBlockingSemaphore(maxConnectionsPerHost)) : - NonBlockingSemaphoreInfinite.INSTANCE; + freeChannelsPerHost.computeIfAbsent(partitionKey, pk -> new Semaphore(maxConnectionsPerHost)) : + InfiniteSemaphore.INSTANCE; } } diff --git a/client/src/main/java/org/asynchttpclient/netty/channel/NioSocketChannelFactory.java b/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java similarity index 67% rename from client/src/main/java/org/asynchttpclient/netty/channel/NioSocketChannelFactory.java rename to client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java index 907623bba6..76f45c2d28 100644 --- a/client/src/main/java/org/asynchttpclient/netty/channel/NioSocketChannelFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/channel/TransportFactory.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016 AsyncHttpClient Project. All rights reserved. + * Copyright (c) 2019 AsyncHttpClient Project. All rights reserved. * * This program is licensed to you under the Apache License Version 2.0, * and you may not use this file except in compliance with the Apache License Version 2.0. @@ -13,15 +13,14 @@ */ package org.asynchttpclient.netty.channel; +import io.netty.channel.Channel; import io.netty.channel.ChannelFactory; -import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.channel.EventLoopGroup; -enum NioSocketChannelFactory implements ChannelFactory { +import java.util.concurrent.ThreadFactory; - INSTANCE; +public interface TransportFactory extends ChannelFactory { + + L newEventLoopGroup(int ioThreadsCount, ThreadFactory threadFactory); - @Override - public NioSocketChannel newChannel() { - return new NioSocketChannel(); - } } diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java index de78537ac7..ec158673f0 100755 --- a/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/AsyncHttpClientHandler.java @@ -67,8 +67,10 @@ public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exce Object attribute = Channels.getAttribute(channel); try { - if (attribute instanceof OnLastHttpContentCallback && msg instanceof LastHttpContent) { - ((OnLastHttpContentCallback) attribute).call(); + if (attribute instanceof OnLastHttpContentCallback) { + if (msg instanceof LastHttpContent) { + ((OnLastHttpContentCallback) attribute).call(); + } } else if (attribute instanceof NettyResponseFuture) { NettyResponseFuture future = (NettyResponseFuture) attribute; 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 ad29808d96..dddaeb34cb 100755 --- a/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/HttpHandler.java @@ -30,6 +30,7 @@ import org.asynchttpclient.netty.request.NettyRequestSender; import java.io.IOException; +import java.net.InetSocketAddress; @Sharable public final class HttpHandler extends AsyncHttpClientHandler { @@ -38,8 +39,7 @@ public HttpHandler(AsyncHttpClientConfig config, ChannelManager channelManager, super(config, channelManager, requestSender); } - private boolean abortAfterHandlingStatus(// - AsyncHandler handler, + private boolean abortAfterHandlingStatus(AsyncHandler handler, NettyResponseStatus status) throws Exception { return handler.onStatusReceived(status) == State.ABORT; } @@ -69,7 +69,7 @@ private void handleHttpResponse(final HttpResponse response, final Channel chann HttpRequest httpRequest = future.getNettyRequest().getHttpRequest(); logger.debug("\n\nRequest {}\n\nResponse {}\n", httpRequest, response); - future.setKeepAlive(config.getKeepAliveStrategy().keepAlive(future.getTargetRequest(), httpRequest, response)); + future.setKeepAlive(config.getKeepAliveStrategy().keepAlive((InetSocketAddress) channel.remoteAddress(), future.getTargetRequest(), httpRequest, response)); NettyResponseStatus status = new NettyResponseStatus(future.getUri(), response, channel); HttpHeaders responseHeaders = response.headers(); diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/StreamedResponsePublisher.java b/client/src/main/java/org/asynchttpclient/netty/handler/StreamedResponsePublisher.java index f4565f91b6..4fb24dbd1a 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/StreamedResponsePublisher.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/StreamedResponsePublisher.java @@ -14,10 +14,13 @@ import com.typesafe.netty.HandlerPublisher; import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; import io.netty.util.concurrent.EventExecutor; import org.asynchttpclient.HttpResponseBodyPart; import org.asynchttpclient.netty.NettyResponseFuture; import org.asynchttpclient.netty.channel.ChannelManager; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -28,6 +31,8 @@ public class StreamedResponsePublisher extends HandlerPublisher future; private final Channel channel; + private volatile boolean hasOutstandingRequest = false; + private Throwable error; StreamedResponsePublisher(EventExecutor executor, ChannelManager channelManager, NettyResponseFuture future, Channel channel) { super(executor, HttpResponseBodyPart.class); @@ -51,7 +56,66 @@ protected void cancelled() { channelManager.closeChannel(channel); } + @Override + protected void requestDemand() { + hasOutstandingRequest = true; + super.requestDemand(); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { + hasOutstandingRequest = false; + super.channelReadComplete(ctx); + } + + @Override + public void subscribe(Subscriber subscriber) { + super.subscribe(new ErrorReplacingSubscriber(subscriber)); + } + + public boolean hasOutstandingRequest() { + return hasOutstandingRequest; + } + NettyResponseFuture future() { return future; } + + public void setError(Throwable t) { + this.error = t; + } + + private class ErrorReplacingSubscriber implements Subscriber { + + private final Subscriber subscriber; + + ErrorReplacingSubscriber(Subscriber subscriber) { + this.subscriber = subscriber; + } + + @Override + public void onSubscribe(Subscription s) { + subscriber.onSubscribe(s); + } + + @Override + public void onNext(HttpResponseBodyPart httpResponseBodyPart) { + subscriber.onNext(httpResponseBodyPart); + } + + @Override + public void onError(Throwable t) { + subscriber.onError(t); + } + + @Override + public void onComplete() { + Throwable replacementError = error; + if (replacementError == null) { + subscriber.onComplete(); + } else { + subscriber.onError(replacementError); + } + } + } } diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java index 753df0020d..eb2e98e36f 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ConnectSuccessInterceptor.java @@ -52,7 +52,7 @@ public boolean exitAfterHandlingConnect(Channel channel, future.setReuseChannel(true); future.setConnectAllowed(false); - Request targetRequest = new RequestBuilder(future.getTargetRequest()).build(); + Request targetRequest = future.getTargetRequest().toBuilder().build(); if (whenHandshaked == null) { requestSender.drainChannelAndExecuteNextRequest(channel, future, targetRequest); } else { diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java index 0812083ad5..57436e9ae5 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/ProxyUnauthorized407Interceptor.java @@ -79,7 +79,7 @@ public boolean exitAfterHandling407(Channel channel, // FIXME what's this??? future.setChannelState(ChannelState.NEW); - HttpHeaders requestHeaders = new DefaultHttpHeaders(false).add(request.getHeaders()); + HttpHeaders requestHeaders = new DefaultHttpHeaders().add(request.getHeaders()); switch (proxyRealm.getScheme()) { case BASIC: @@ -140,7 +140,7 @@ public boolean exitAfterHandling407(Channel channel, return false; } try { - kerberosProxyChallenge(proxyServer, requestHeaders); + kerberosProxyChallenge(proxyRealm, proxyServer, requestHeaders); } catch (SpnegoEngineException e) { // FIXME @@ -163,7 +163,7 @@ public boolean exitAfterHandling407(Channel channel, throw new IllegalStateException("Invalid Authentication scheme " + proxyRealm.getScheme()); } - RequestBuilder nextRequestBuilder = new RequestBuilder(future.getCurrentRequest()).setHeaders(requestHeaders); + RequestBuilder nextRequestBuilder = future.getCurrentRequest().toBuilder().setHeaders(requestHeaders); if (future.getCurrentRequest().getUri().isSecured()) { nextRequestBuilder.setMethod(CONNECT); } @@ -184,10 +184,17 @@ public boolean exitAfterHandling407(Channel channel, return true; } - private void kerberosProxyChallenge(ProxyServer proxyServer, + private void kerberosProxyChallenge(Realm proxyRealm, + ProxyServer proxyServer, HttpHeaders headers) throws SpnegoEngineException { - String challengeHeader = SpnegoEngine.instance().generateToken(proxyServer.getHost()); + String challengeHeader = SpnegoEngine.instance(proxyRealm.getPrincipal(), + proxyRealm.getPassword(), + proxyRealm.getServicePrincipalName(), + proxyRealm.getRealmName(), + proxyRealm.isUseCanonicalHostname(), + proxyRealm.getCustomLoginConfig(), + proxyRealm.getLoginContextName()).generateToken(proxyServer.getHost()); headers.set(PROXY_AUTHORIZATION, NEGOTIATE + " " + challengeHeader); } 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 121bb71658..a2ddbd9467 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 @@ -38,6 +38,8 @@ import static io.netty.handler.codec.http.HttpHeaderNames.*; import static org.asynchttpclient.util.HttpConstants.Methods.GET; +import static org.asynchttpclient.util.HttpConstants.Methods.HEAD; +import static org.asynchttpclient.util.HttpConstants.Methods.OPTIONS; import static org.asynchttpclient.util.HttpConstants.ResponseStatusCodes.*; import static org.asynchttpclient.util.HttpUtils.followRedirect; import static org.asynchttpclient.util.MiscUtils.isNonEmpty; @@ -87,7 +89,7 @@ public boolean exitAfterHandlingRedirect(Channel channel, String originalMethod = request.getMethod(); boolean switchToGet = !originalMethod.equals(GET) - && (statusCode == MOVED_PERMANENTLY_301 || statusCode == SEE_OTHER_303 || (statusCode == FOUND_302 && !config.isStrict302Handling())); + && !originalMethod.equals(OPTIONS) && !originalMethod.equals(HEAD) && (statusCode == MOVED_PERMANENTLY_301 || statusCode == SEE_OTHER_303 || (statusCode == FOUND_302 && !config.isStrict302Handling())); boolean keepBody = statusCode == TEMPORARY_REDIRECT_307 || statusCode == PERMANENT_REDIRECT_308 || (statusCode == FOUND_302 && config.isStrict302Handling()); final RequestBuilder requestBuilder = new RequestBuilder(switchToGet ? GET : originalMethod) @@ -111,6 +113,9 @@ else if (request.getByteBufferData() != null) requestBuilder.setBody(request.getByteBufferData()); else if (request.getBodyGenerator() != null) requestBuilder.setBody(request.getBodyGenerator()); + else if (isNonEmpty(request.getBodyParts())) { + requestBuilder.setBodyParts(request.getBodyParts()); + } } requestBuilder.setHeaders(propagatedHeaders(request, realm, keepBody)); @@ -123,7 +128,6 @@ else if (request.getBodyGenerator() != null) HttpHeaders responseHeaders = response.headers(); String location = responseHeaders.get(LOCATION); Uri newUri = Uri.create(future.getUri(), location); - LOGGER.debug("Redirecting to {}", newUri); CookieStore cookieStore = config.getCookieStore(); diff --git a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java index e63daece58..269042529b 100644 --- a/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java +++ b/client/src/main/java/org/asynchttpclient/netty/handler/intercept/Unauthorized401Interceptor.java @@ -77,7 +77,7 @@ public boolean exitAfterHandling401(final Channel channel, // FIXME what's this??? future.setChannelState(ChannelState.NEW); - HttpHeaders requestHeaders = new DefaultHttpHeaders(false).add(request.getHeaders()); + HttpHeaders requestHeaders = new DefaultHttpHeaders().add(request.getHeaders()); switch (realm.getScheme()) { case BASIC: @@ -139,7 +139,7 @@ public boolean exitAfterHandling401(final Channel channel, return false; } try { - kerberosChallenge(request, requestHeaders); + kerberosChallenge(realm, request, requestHeaders); } catch (SpnegoEngineException e) { // FIXME @@ -162,7 +162,7 @@ public boolean exitAfterHandling401(final Channel channel, throw new IllegalStateException("Invalid Authentication scheme " + realm.getScheme()); } - final Request nextRequest = new RequestBuilder(future.getCurrentRequest()).setHeaders(requestHeaders).build(); + final Request nextRequest = future.getCurrentRequest().toBuilder().setHeaders(requestHeaders).build(); LOGGER.debug("Sending authentication to {}", request.getUri()); if (future.isKeepAlive() @@ -200,12 +200,19 @@ private void ntlmChallenge(String authenticateHeader, } } - private void kerberosChallenge(Request request, + private void kerberosChallenge(Realm realm, + Request request, HttpHeaders headers) throws SpnegoEngineException { Uri uri = request.getUri(); String host = withDefault(request.getVirtualHost(), uri.getHost()); - String challengeHeader = SpnegoEngine.instance().generateToken(host); + String challengeHeader = SpnegoEngine.instance(realm.getPrincipal(), + realm.getPassword(), + realm.getServicePrincipalName(), + realm.getRealmName(), + realm.isUseCanonicalHostname(), + realm.getCustomLoginConfig(), + realm.getLoginContextName()).generateToken(host); headers.set(AUTHORIZATION, NEGOTIATE + " " + challengeHeader); } } diff --git a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java index 663ced6ce1..4cfee06cd6 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestFactory.java @@ -140,6 +140,7 @@ public NettyRequest newNettyRequest(Request request, boolean performConnectReque if (connect) { // assign proxy-auth as configured on request headers.set(PROXY_AUTHORIZATION, request.getHeaders().getAll(PROXY_AUTHORIZATION)); + headers.set(USER_AGENT, request.getHeaders().getAll(USER_AGENT)); } else { // assign headers as configured on request 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 03255731f7..aed08b7a70 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/NettyRequestSender.java @@ -35,6 +35,7 @@ import org.asynchttpclient.netty.OnLastHttpContentCallback; import org.asynchttpclient.netty.SimpleFutureListener; import org.asynchttpclient.netty.channel.*; +import org.asynchttpclient.netty.handler.StreamedResponsePublisher; import org.asynchttpclient.netty.timeout.TimeoutsHolder; import org.asynchttpclient.proxy.ProxyServer; import org.asynchttpclient.resolver.RequestHostnameResolver; @@ -274,6 +275,12 @@ private ListenableFuture sendRequestWithNewChannel(Request request, // some headers are only set when performing the first request HttpHeaders headers = future.getNettyRequest().getHttpRequest().headers(); + if(proxy != null && proxy.getCustomHeaders() != null ) { + HttpHeaders customHeaders = proxy.getCustomHeaders().apply(request); + if(customHeaders != null) { + headers.add(customHeaders); + } + } Realm realm = future.getRealm(); Realm proxyRealm = future.getProxyRealm(); requestFactory.addAuthorizationHeader(headers, perConnectionAuthorizationHeader(request, proxy, realm)); @@ -437,8 +444,8 @@ public void writeRequest(NettyResponseFuture future, Channel channel) { } private void configureTransferAdapter(AsyncHandler handler, HttpRequest httpRequest) { - HttpHeaders h = new DefaultHttpHeaders(false).set(httpRequest.headers()); - TransferCompletionHandler.class.cast(handler).headers(h); + HttpHeaders h = new DefaultHttpHeaders().set(httpRequest.headers()); + ((TransferCompletionHandler) handler).headers(h); } private void scheduleRequestTimeout(NettyResponseFuture nettyResponseFuture, @@ -462,8 +469,15 @@ private void scheduleReadTimeout(NettyResponseFuture nettyResponseFuture) { public void abort(Channel channel, NettyResponseFuture future, Throwable t) { - if (channel != null && channel.isActive()) { - channelManager.closeChannel(channel); + if (channel != null) { + Object attribute = Channels.getAttribute(channel); + if (attribute instanceof StreamedResponsePublisher) { + ((StreamedResponsePublisher) attribute).setError(t); + } + + if (channel.isActive()) { + channelManager.closeChannel(channel); + } } if (!future.isDone()) { diff --git a/client/src/main/java/org/asynchttpclient/netty/request/WriteListener.java b/client/src/main/java/org/asynchttpclient/netty/request/WriteListener.java index ab38a66f94..0a51e63e90 100644 --- a/client/src/main/java/org/asynchttpclient/netty/request/WriteListener.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/WriteListener.java @@ -16,11 +16,13 @@ import io.netty.channel.Channel; import org.asynchttpclient.handler.ProgressAsyncHandler; import org.asynchttpclient.netty.NettyResponseFuture; +import org.asynchttpclient.netty.channel.ChannelState; import org.asynchttpclient.netty.channel.Channels; import org.asynchttpclient.netty.future.StackTraceInspector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLException; import java.nio.channels.ClosedChannelException; public abstract class WriteListener { @@ -36,27 +38,27 @@ public abstract class WriteListener { this.notifyHeaders = notifyHeaders; } - private boolean abortOnThrowable(Channel channel, Throwable cause) { - if (cause != null) { - if (cause instanceof IllegalStateException || cause instanceof ClosedChannelException || StackTraceInspector.recoverOnReadOrWriteException(cause)) { - LOGGER.debug(cause.getMessage(), cause); - Channels.silentlyCloseChannel(channel); + private void abortOnThrowable(Channel channel, Throwable cause) { + if (future.getChannelState() == ChannelState.POOLED + && (cause instanceof IllegalStateException + || cause instanceof ClosedChannelException + || cause instanceof SSLException + || StackTraceInspector.recoverOnReadOrWriteException(cause))) { + LOGGER.debug("Write exception on pooled channel, letting retry trigger", cause); - } else { - future.abort(cause); - } - return true; + } else { + future.abort(cause); } - - return false; + Channels.silentlyCloseChannel(channel); } void operationComplete(Channel channel, Throwable cause) { future.touch(); - // The write operation failed. If the channel was cached, it means it got asynchronously closed. + // The write operation failed. If the channel was pooled, it means it got asynchronously closed. // Let's retry a second time. - if (abortOnThrowable(channel, cause)) { + if (cause != null) { + abortOnThrowable(channel, cause); return; } diff --git a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java index 728e2ec896..1a7d50b3fd 100755 --- a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyBodyBody.java @@ -53,7 +53,7 @@ public long getContentLength() { public void write(final Channel channel, NettyResponseFuture future) { Object msg; - if (body instanceof RandomAccessBody && !ChannelManager.isSslHandlerConfigured(channel.pipeline()) && !config.isDisableZeroCopy()) { + if (body instanceof RandomAccessBody && !ChannelManager.isSslHandlerConfigured(channel.pipeline()) && !config.isDisableZeroCopy() && getContentLength() > 0) { msg = new BodyFileRegion((RandomAccessBody) body); } else { diff --git a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyDirectBody.java b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyDirectBody.java index 0d25358713..9d4eacb165 100644 --- a/client/src/main/java/org/asynchttpclient/netty/request/body/NettyDirectBody.java +++ b/client/src/main/java/org/asynchttpclient/netty/request/body/NettyDirectBody.java @@ -23,6 +23,6 @@ public abstract class NettyDirectBody implements NettyBody { @Override public void write(Channel channel, NettyResponseFuture future) { - throw new UnsupportedOperationException("This kind of body is supposed to be writen directly"); + throw new UnsupportedOperationException("This kind of body is supposed to be written directly"); } } diff --git a/client/src/main/java/org/asynchttpclient/netty/ssl/DefaultSslEngineFactory.java b/client/src/main/java/org/asynchttpclient/netty/ssl/DefaultSslEngineFactory.java index 60b14b56e5..401c60a581 100644 --- a/client/src/main/java/org/asynchttpclient/netty/ssl/DefaultSslEngineFactory.java +++ b/client/src/main/java/org/asynchttpclient/netty/ssl/DefaultSslEngineFactory.java @@ -19,6 +19,7 @@ import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.util.ReferenceCountUtil; import org.asynchttpclient.AsyncHttpClientConfig; import javax.net.ssl.SSLEngine; @@ -73,6 +74,11 @@ public void init(AsyncHttpClientConfig config) throws SSLException { sslContext = buildSslContext(config); } + @Override + public void destroy() { + ReferenceCountUtil.release(sslContext); + } + /** * The last step of configuring the SslContextBuilder used to create an SslContext when no context is provided in the {@link AsyncHttpClientConfig}. This defaults to no-op and * is intended to be overridden as needed. diff --git a/client/src/main/java/org/asynchttpclient/netty/timeout/ReadTimeoutTimerTask.java b/client/src/main/java/org/asynchttpclient/netty/timeout/ReadTimeoutTimerTask.java index 5aebed9f80..0af2d153e0 100755 --- a/client/src/main/java/org/asynchttpclient/netty/timeout/ReadTimeoutTimerTask.java +++ b/client/src/main/java/org/asynchttpclient/netty/timeout/ReadTimeoutTimerTask.java @@ -15,6 +15,8 @@ import io.netty.util.Timeout; import org.asynchttpclient.netty.NettyResponseFuture; +import org.asynchttpclient.netty.channel.Channels; +import org.asynchttpclient.netty.handler.StreamedResponsePublisher; import org.asynchttpclient.netty.request.NettyRequestSender; import org.asynchttpclient.util.StringBuilderPool; @@ -47,7 +49,7 @@ public void run(Timeout timeout) { long currentReadTimeoutInstant = readTimeout + nettyResponseFuture.getLastTouch(); long durationBeforeCurrentReadTimeout = currentReadTimeoutInstant - now; - if (durationBeforeCurrentReadTimeout <= 0L) { + if (durationBeforeCurrentReadTimeout <= 0L && !isReactiveWithNoOutstandingRequest()) { // idleConnectTimeout reached StringBuilder sb = StringBuilderPool.DEFAULT.stringBuilder().append("Read timeout to "); appendRemoteAddress(sb); @@ -62,4 +64,10 @@ public void run(Timeout timeout) { timeoutsHolder.startReadTimeout(this); } } + + private boolean isReactiveWithNoOutstandingRequest() { + Object attribute = Channels.getAttribute(nettyResponseFuture.channel()); + return attribute instanceof StreamedResponsePublisher && + !((StreamedResponsePublisher) attribute).hasOutstandingRequest(); + } } diff --git a/client/src/main/java/org/asynchttpclient/netty/timeout/TimeoutTimerTask.java b/client/src/main/java/org/asynchttpclient/netty/timeout/TimeoutTimerTask.java index e746adfdb5..034502785c 100755 --- a/client/src/main/java/org/asynchttpclient/netty/timeout/TimeoutTimerTask.java +++ b/client/src/main/java/org/asynchttpclient/netty/timeout/TimeoutTimerTask.java @@ -55,7 +55,7 @@ public void clean() { void appendRemoteAddress(StringBuilder sb) { InetSocketAddress remoteAddress = timeoutsHolder.remoteAddress(); - sb.append(remoteAddress.getHostName()); + sb.append(remoteAddress.getHostString()); if (!remoteAddress.isUnresolved()) { sb.append('/').append(remoteAddress.getAddress().getHostAddress()); } diff --git a/client/src/main/java/org/asynchttpclient/netty/ws/NettyWebSocket.java b/client/src/main/java/org/asynchttpclient/netty/ws/NettyWebSocket.java index 531eaadd89..f6ab4ae2f3 100755 --- a/client/src/main/java/org/asynchttpclient/netty/ws/NettyWebSocket.java +++ b/client/src/main/java/org/asynchttpclient/netty/ws/NettyWebSocket.java @@ -155,7 +155,7 @@ public Future sendCloseFrame() { @Override public Future sendCloseFrame(int statusCode, String reasonText) { if (channel.isOpen()) { - return channel.writeAndFlush(new CloseWebSocketFrame(1000, "normal closure")); + return channel.writeAndFlush(new CloseWebSocketFrame(statusCode, reasonText)); } return ImmediateEventExecutor.INSTANCE.newSucceededFuture(null); } diff --git a/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorInstance.java b/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorInstance.java index acb9fce5b1..aa92c5aaa1 100644 --- a/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorInstance.java +++ b/client/src/main/java/org/asynchttpclient/oauth/OAuthSignatureCalculatorInstance.java @@ -199,7 +199,6 @@ private byte[] digest(ConsumerKey consumerAuth, RequestToken userAuth, ByteBuffe SecretKeySpec signingKey = new SecretKeySpec(keyBytes, HMAC_SHA1_ALGORITHM); mac.init(signingKey); - mac.reset(); mac.update(message); return mac.doFinal(); } diff --git a/client/src/main/java/org/asynchttpclient/proxy/ProxyServer.java b/client/src/main/java/org/asynchttpclient/proxy/ProxyServer.java index 13c33590be..bdbc76db8e 100644 --- a/client/src/main/java/org/asynchttpclient/proxy/ProxyServer.java +++ b/client/src/main/java/org/asynchttpclient/proxy/ProxyServer.java @@ -16,11 +16,15 @@ */ package org.asynchttpclient.proxy; +import io.netty.handler.codec.http.HttpHeaders; + import org.asynchttpclient.Realm; +import org.asynchttpclient.Request; import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.function.Function; import static org.asynchttpclient.util.Assertions.assertNotNull; import static org.asynchttpclient.util.MiscUtils.isNonEmpty; @@ -36,15 +40,22 @@ public class ProxyServer { private final Realm realm; private final List nonProxyHosts; private final ProxyType proxyType; + private final Function customHeaders; public ProxyServer(String host, int port, int securedPort, Realm realm, List nonProxyHosts, - ProxyType proxyType) { + ProxyType proxyType, Function customHeaders) { this.host = host; this.port = port; this.securedPort = securedPort; this.realm = realm; this.nonProxyHosts = nonProxyHosts; this.proxyType = proxyType; + this.customHeaders = customHeaders; + } + + public ProxyServer(String host, int port, int securedPort, Realm realm, List nonProxyHosts, + ProxyType proxyType) { + this(host, port, securedPort, realm, nonProxyHosts, proxyType, null); } public String getHost() { @@ -71,6 +82,10 @@ public ProxyType getProxyType() { return proxyType; } + public Function getCustomHeaders() { + return customHeaders; + } + /** * Checks whether proxy should be used according to nonProxyHosts settings of * it, or we want to go directly to target host. If null proxy is @@ -118,6 +133,7 @@ public static class Builder { private Realm realm; private List nonProxyHosts; private ProxyType proxyType; + private Function customHeaders; public Builder(String host, int port) { this.host = host; @@ -157,11 +173,16 @@ public Builder setProxyType(ProxyType proxyType) { return this; } + public Builder setCustomHeaders(Function customHeaders) { + this.customHeaders = customHeaders; + return this; + } + public ProxyServer build() { List nonProxyHosts = this.nonProxyHosts != null ? Collections.unmodifiableList(this.nonProxyHosts) : Collections.emptyList(); ProxyType proxyType = this.proxyType != null ? this.proxyType : ProxyType.HTTP; - return new ProxyServer(host, port, securedPort, realm, nonProxyHosts, proxyType); + return new ProxyServer(host, port, securedPort, realm, nonProxyHosts, proxyType, customHeaders); } } } diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/FileLikePart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/FileLikePart.java index ad3a702515..03de497867 100644 --- a/client/src/main/java/org/asynchttpclient/request/body/multipart/FileLikePart.java +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/FileLikePart.java @@ -47,14 +47,14 @@ public abstract class FileLikePart extends PartBase { * @param charset the charset encoding for this part * @param fileName the fileName * @param contentId the content id - * @param transfertEncoding the transfer encoding + * @param transferEncoding the transfer encoding */ - public FileLikePart(String name, String contentType, Charset charset, String fileName, String contentId, String transfertEncoding) { + public FileLikePart(String name, String contentType, Charset charset, String fileName, String contentId, String transferEncoding) { super(name, computeContentType(contentType, fileName), charset, contentId, - transfertEncoding); + transferEncoding); this.fileName = fileName; } diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java new file mode 100644 index 0000000000..ca7d0db367 --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/InputStreamPart.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2018 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.request.body.multipart; + +import java.io.InputStream; +import java.nio.charset.Charset; + +import static org.asynchttpclient.util.Assertions.assertNotNull; + +public class InputStreamPart extends FileLikePart { + + private final InputStream inputStream; + private final long contentLength; + + public InputStreamPart(String name, InputStream inputStream, String fileName) { + this(name, inputStream, fileName, -1); + } + + public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength) { + this(name, inputStream, fileName, contentLength, null); + } + + public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength, String contentType) { + this(name, inputStream, fileName, contentLength, contentType, null); + } + + public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength, String contentType, Charset charset) { + this(name, inputStream, fileName, contentLength, contentType, charset, null); + } + + public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength, String contentType, Charset charset, + String contentId) { + this(name, inputStream, fileName, contentLength, contentType, charset, contentId, null); + } + + public InputStreamPart(String name, InputStream inputStream, String fileName, long contentLength, String contentType, Charset charset, + String contentId, String transferEncoding) { + super(name, + contentType, + charset, + fileName, + contentId, + transferEncoding); + this.inputStream = assertNotNull(inputStream, "inputStream"); + this.contentLength = contentLength; + } + + public InputStream getInputStream() { + return inputStream; + } + + public long getContentLength() { + return contentLength; + } +} diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java index 94bcb295d5..78e2d130a4 100644 --- a/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/MultipartUtils.java @@ -75,6 +75,9 @@ public static List> generateMultipartParts(List { + + private long position = 0L; + private ByteBuffer buffer; + private ReadableByteChannel channel; + + public InputStreamMultipartPart(InputStreamPart part, byte[] boundary) { + super(part, boundary); + } + + private ByteBuffer getBuffer() { + if (buffer == null) { + buffer = ByteBuffer.allocateDirect(BodyChunkedInput.DEFAULT_CHUNK_SIZE); + } + return buffer; + } + + private ReadableByteChannel getChannel() { + if (channel == null) { + channel = Channels.newChannel(part.getInputStream()); + } + return channel; + } + + @Override + protected long getContentLength() { + return part.getContentLength(); + } + + @Override + protected long transferContentTo(ByteBuf target) throws IOException { + InputStream inputStream = part.getInputStream(); + int transferred = target.writeBytes(inputStream, target.writableBytes()); + if (transferred > 0) { + position += transferred; + } + if (position == getContentLength() || transferred < 0) { + state = MultipartState.POST_CONTENT; + inputStream.close(); + } + return transferred; + } + + @Override + protected long transferContentTo(WritableByteChannel target) throws IOException { + ReadableByteChannel channel = getChannel(); + ByteBuffer buffer = getBuffer(); + + int transferred = 0; + int read = channel.read(buffer); + + if (read > 0) { + buffer.flip(); + while (buffer.hasRemaining()) { + transferred += target.write(buffer); + } + buffer.compact(); + position += transferred; + } + if (position == getContentLength() || read < 0) { + state = MultipartState.POST_CONTENT; + if (channel.isOpen()) { + channel.close(); + } + } + + return transferred; + } + + @Override + public void close() { + super.close(); + closeSilently(part.getInputStream()); + closeSilently(channel); + } + +} diff --git a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartPart.java b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartPart.java index 38041338e8..b8c8622680 100644 --- a/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartPart.java +++ b/client/src/main/java/org/asynchttpclient/request/body/multipart/part/MultipartPart.java @@ -106,6 +106,10 @@ public abstract class MultipartPart implements Closeable { } public long length() { + long contentLength = getContentLength(); + if (contentLength < 0) { + return contentLength; + } return preContentLength + postContentLength + getContentLength(); } diff --git a/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java b/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java index 3edf13ff1d..da42fcf660 100644 --- a/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java +++ b/client/src/main/java/org/asynchttpclient/resolver/RequestHostnameResolver.java @@ -35,7 +35,7 @@ public enum RequestHostnameResolver { public Future> resolve(NameResolver nameResolver, InetSocketAddress unresolvedAddress, AsyncHandler asyncHandler) { - final String hostname = unresolvedAddress.getHostName(); + final String hostname = unresolvedAddress.getHostString(); final int port = unresolvedAddress.getPort(); final Promise> promise = ImmediateEventExecutor.INSTANCE.newPromise(); diff --git a/client/src/main/java/org/asynchttpclient/spnego/NamePasswordCallbackHandler.java b/client/src/main/java/org/asynchttpclient/spnego/NamePasswordCallbackHandler.java new file mode 100644 index 0000000000..680cdcac43 --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/spnego/NamePasswordCallbackHandler.java @@ -0,0 +1,82 @@ +package org.asynchttpclient.spnego; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import java.io.IOException; +import java.lang.reflect.Method; + +public class NamePasswordCallbackHandler implements CallbackHandler { + private final Logger log = LoggerFactory.getLogger(getClass()); + private static final String PASSWORD_CALLBACK_NAME = "setObject"; + private static final Class[] PASSWORD_CALLBACK_TYPES = + new Class[] {Object.class, char[].class, String.class}; + + private String username; + private String password; + + private String passwordCallbackName; + + public NamePasswordCallbackHandler(String username, String password) { + this(username, password, null); + } + + public NamePasswordCallbackHandler(String username, String password, String passwordCallbackName) { + this.username = username; + this.password = password; + this.passwordCallbackName = passwordCallbackName; + } + + public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException { + for (int i = 0; i < callbacks.length; i++) { + Callback callback = callbacks[i]; + if (handleCallback(callback)) { + continue; + } else if (callback instanceof NameCallback) { + ((NameCallback) callback).setName(username); + } else if (callback instanceof PasswordCallback) { + PasswordCallback pwCallback = (PasswordCallback) callback; + pwCallback.setPassword(password.toCharArray()); + } else if (!invokePasswordCallback(callback)) { + String errorMsg = "Unsupported callback type " + callbacks[i].getClass().getName(); + log.info(errorMsg); + throw new UnsupportedCallbackException(callbacks[i], errorMsg); + } + } + } + + protected boolean handleCallback(Callback callback) { + return false; + } + + /* + * This method is called from the handle(Callback[]) method when the specified callback + * did not match any of the known callback classes. It looks for the callback method + * having the specified method name with one of the supported parameter types. + * If found, it invokes the callback method on the object and returns true. + * If not, it returns false. + */ + private boolean invokePasswordCallback(Callback callback) { + String cbname = passwordCallbackName == null + ? PASSWORD_CALLBACK_NAME : passwordCallbackName; + for (Class arg : PASSWORD_CALLBACK_TYPES) { + try { + Method method = callback.getClass().getMethod(cbname, arg); + Object args[] = new Object[] { + arg == String.class ? password : password.toCharArray() + }; + method.invoke(callback, args); + return true; + } catch (Exception e) { + // ignore and continue + log.debug(e.toString()); + } + } + return false; + } +} \ No newline at end of file diff --git a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java index 3326dca931..515bf63184 100644 --- a/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java +++ b/client/src/main/java/org/asynchttpclient/spnego/SpnegoEngine.java @@ -38,6 +38,7 @@ package org.asynchttpclient.spnego; import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; import org.ietf.jgss.GSSException; import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; @@ -45,8 +46,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.security.auth.Subject; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.login.AppConfigurationEntry; +import javax.security.auth.login.Configuration; +import javax.security.auth.login.LoginContext; +import javax.security.auth.login.LoginException; import java.io.IOException; +import java.net.InetAddress; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; import java.util.Base64; +import java.util.HashMap; +import java.util.Map; /** * SPNEGO (Simple and Protected GSSAPI Negotiation Mechanism) authentication scheme. @@ -57,31 +69,87 @@ public class SpnegoEngine { private static final String SPNEGO_OID = "1.3.6.1.5.5.2"; private static final String KERBEROS_OID = "1.2.840.113554.1.2.2"; - private static SpnegoEngine instance; + private static Map instances = new HashMap<>(); private final Logger log = LoggerFactory.getLogger(getClass()); private final SpnegoTokenGenerator spnegoGenerator; + private final String username; + private final String password; + private final String servicePrincipalName; + private final String realmName; + private final boolean useCanonicalHostname; + private final String loginContextName; + private final Map customLoginConfig; - public SpnegoEngine(final SpnegoTokenGenerator spnegoGenerator) { + public SpnegoEngine(final String username, + final String password, + final String servicePrincipalName, + final String realmName, + final boolean useCanonicalHostname, + final Map customLoginConfig, + final String loginContextName, + final SpnegoTokenGenerator spnegoGenerator) { + this.username = username; + this.password = password; + this.servicePrincipalName = servicePrincipalName; + this.realmName = realmName; + this.useCanonicalHostname = useCanonicalHostname; + this.customLoginConfig = customLoginConfig; this.spnegoGenerator = spnegoGenerator; + this.loginContextName = loginContextName; } public SpnegoEngine() { - this(null); + this(null, + null, + null, + null, + true, + null, + null, + null); } - public static SpnegoEngine instance() { - if (instance == null) - instance = new SpnegoEngine(); - return instance; + public static SpnegoEngine instance(final String username, + final String password, + final String servicePrincipalName, + final String realmName, + final boolean useCanonicalHostname, + final Map customLoginConfig, + final String loginContextName) { + String key = ""; + if (customLoginConfig != null && !customLoginConfig.isEmpty()) { + StringBuilder customLoginConfigKeyValues = new StringBuilder(); + for (String loginConfigKey : customLoginConfig.keySet()) { + customLoginConfigKeyValues.append(loginConfigKey).append("=") + .append(customLoginConfig.get(loginConfigKey)); + } + key = customLoginConfigKeyValues.toString(); + } + if (username != null) { + key += username; + } + if (loginContextName != null) { + key += loginContextName; + } + if (!instances.containsKey(key)) { + instances.put(key, new SpnegoEngine(username, + password, + servicePrincipalName, + realmName, + useCanonicalHostname, + customLoginConfig, + loginContextName, + null)); + } + return instances.get(key); } - public String generateToken(String server) throws SpnegoEngineException { + public String generateToken(String host) throws SpnegoEngineException { GSSContext gssContext = null; byte[] token = null; // base64 decoded challenge Oid negotiationOid; try { - log.debug("init {}", server); /* * Using the SPNEGO OID is the correct method. Kerberos v5 works for IIS but not JBoss. Unwrapping the initial token when using SPNEGO OID looks like what is described * here... @@ -99,11 +167,30 @@ public String generateToken(String server) throws SpnegoEngineException { negotiationOid = new Oid(SPNEGO_OID); boolean tryKerberos = false; + String spn = getCompleteServicePrincipalName(host); try { GSSManager manager = GSSManager.getInstance(); - GSSName serverName = manager.createName("HTTP@" + server, GSSName.NT_HOSTBASED_SERVICE); - gssContext = manager.createContext(serverName.canonicalize(negotiationOid), negotiationOid, null, - GSSContext.DEFAULT_LIFETIME); + GSSName serverName = manager.createName(spn, GSSName.NT_HOSTBASED_SERVICE); + GSSCredential myCred = null; + if (username != null || loginContextName != null || (customLoginConfig != null && !customLoginConfig.isEmpty())) { + String contextName = loginContextName; + if (contextName == null) { + contextName = ""; + } + LoginContext loginContext = new LoginContext(contextName, + null, + getUsernamePasswordHandler(), + getLoginConfiguration()); + loginContext.login(); + final Oid negotiationOidFinal = negotiationOid; + final PrivilegedExceptionAction action = () -> manager.createCredential(null, + GSSCredential.INDEFINITE_LIFETIME, negotiationOidFinal, GSSCredential.INITIATE_AND_ACCEPT); + myCred = Subject.doAs(loginContext.getSubject(), action); + } + gssContext = manager.createContext(useCanonicalHostname ? serverName.canonicalize(negotiationOid) : serverName, + negotiationOid, + myCred, + GSSContext.DEFAULT_LIFETIME); gssContext.requestMutualAuth(true); gssContext.requestCredDeleg(true); } catch (GSSException ex) { @@ -123,7 +210,7 @@ public String generateToken(String server) throws SpnegoEngineException { log.debug("Using Kerberos MECH {}", KERBEROS_OID); negotiationOid = new Oid(KERBEROS_OID); GSSManager manager = GSSManager.getInstance(); - GSSName serverName = manager.createName("HTTP@" + server, GSSName.NT_HOSTBASED_SERVICE); + GSSName serverName = manager.createName(spn, GSSName.NT_HOSTBASED_SERVICE); gssContext = manager.createContext(serverName.canonicalize(negotiationOid), negotiationOid, null, GSSContext.DEFAULT_LIFETIME); gssContext.requestMutualAuth(true); @@ -164,8 +251,59 @@ public String generateToken(String server) throws SpnegoEngineException { throw new SpnegoEngineException(gsse.getMessage(), gsse); // other error throw new SpnegoEngineException(gsse.getMessage()); - } catch (IOException ex) { + } catch (IOException | LoginException | PrivilegedActionException ex) { throw new SpnegoEngineException(ex.getMessage()); } } + + String getCompleteServicePrincipalName(String host) { + String name; + if (servicePrincipalName == null) { + if (useCanonicalHostname) { + host = getCanonicalHostname(host); + } + name = "HTTP@" + host; + } else { + name = servicePrincipalName; + if (realmName != null && !name.contains("@")) { + name += "@" + realmName; + } + } + log.debug("Service Principal Name is {}", name); + return name; + } + + private String getCanonicalHostname(String hostname) { + String canonicalHostname = hostname; + try { + InetAddress in = InetAddress.getByName(hostname); + canonicalHostname = in.getCanonicalHostName(); + log.debug("Resolved hostname={} to canonicalHostname={}", hostname, canonicalHostname); + } catch (Exception e) { + log.warn("Unable to resolve canonical hostname", e); + } + return canonicalHostname; + } + + private CallbackHandler getUsernamePasswordHandler() { + if (username == null) { + return null; + } + return new NamePasswordCallbackHandler(username, password); + } + + public Configuration getLoginConfiguration() { + if (customLoginConfig != null && !customLoginConfig.isEmpty()) { + return new Configuration() { + @Override + public AppConfigurationEntry[] getAppConfigurationEntry(String name) { + return new AppConfigurationEntry[] { + new AppConfigurationEntry("com.sun.security.auth.module.Krb5LoginModule", + AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, + customLoginConfig)}; + } + }; + } + return null; + } } diff --git a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java index 59754e22a8..00d69af7d2 100644 --- a/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/AuthenticatorUtils.java @@ -175,7 +175,14 @@ else if (request.getVirtualHost() != null) host = request.getUri().getHost(); try { - authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance().generateToken(host); + authorizationHeader = NEGOTIATE + " " + SpnegoEngine.instance( + realm.getPrincipal(), + realm.getPassword(), + realm.getServicePrincipalName(), + realm.getRealmName(), + realm.isUseCanonicalHostname(), + realm.getCustomLoginConfig(), + realm.getLoginContextName()).generateToken(host); } catch (SpnegoEngineException e) { throw new RuntimeException(e); } diff --git a/client/src/main/java/org/asynchttpclient/util/Counted.java b/client/src/main/java/org/asynchttpclient/util/Counted.java new file mode 100644 index 0000000000..b8791e2fea --- /dev/null +++ b/client/src/main/java/org/asynchttpclient/util/Counted.java @@ -0,0 +1,23 @@ +package org.asynchttpclient.util; + +/** + * An interface that defines useful methods to check how many {@linkplain org.asynchttpclient.AsyncHttpClient} + * instances this particular implementation is shared with. + */ +public interface Counted { + + /** + * Increment counter and return the incremented value + */ + int incrementAndGet(); + + /** + * Decrement counter and return the decremented value + */ + int decrementAndGet(); + + /** + * Return the current counter + */ + int count(); +} diff --git a/client/src/main/java/org/asynchttpclient/util/ProxyUtils.java b/client/src/main/java/org/asynchttpclient/util/ProxyUtils.java index 5a22abc361..11d00c0572 100644 --- a/client/src/main/java/org/asynchttpclient/util/ProxyUtils.java +++ b/client/src/main/java/org/asynchttpclient/util/ProxyUtils.java @@ -156,7 +156,7 @@ private static ProxyServerSelector createProxyServerSelector(final ProxySelector return null; } else { InetSocketAddress address = (InetSocketAddress) proxy.address(); - return proxyServer(address.getHostName(), address.getPort()).build(); + return proxyServer(address.getHostString(), address.getPort()).build(); } case DIRECT: return null; diff --git a/client/src/main/java/org/asynchttpclient/util/StringBuilderPool.java b/client/src/main/java/org/asynchttpclient/util/StringBuilderPool.java index 2e89ad78d2..69ed426fed 100644 --- a/client/src/main/java/org/asynchttpclient/util/StringBuilderPool.java +++ b/client/src/main/java/org/asynchttpclient/util/StringBuilderPool.java @@ -19,7 +19,7 @@ public class StringBuilderPool { private final ThreadLocal pool = ThreadLocal.withInitial(() -> new StringBuilder(512)); /** - * BEWARE: MUSN'T APPEND TO ITSELF! + * BEWARE: MUSTN'T APPEND TO ITSELF! * * @return a pooled StringBuilder */ diff --git a/client/src/main/java/org/asynchttpclient/webdav/WebDavCompletionHandlerBase.java b/client/src/main/java/org/asynchttpclient/webdav/WebDavCompletionHandlerBase.java index a6df2fccf4..5a874af180 100644 --- a/client/src/main/java/org/asynchttpclient/webdav/WebDavCompletionHandlerBase.java +++ b/client/src/main/java/org/asynchttpclient/webdav/WebDavCompletionHandlerBase.java @@ -14,7 +14,6 @@ package org.asynchttpclient.webdav; import io.netty.handler.codec.http.HttpHeaders; -import org.asynchttpclient.AsyncCompletionHandlerBase; import org.asynchttpclient.AsyncHandler; import org.asynchttpclient.HttpResponseBodyPart; import org.asynchttpclient.HttpResponseStatus; @@ -42,11 +41,24 @@ * @param the result type */ public abstract class WebDavCompletionHandlerBase implements AsyncHandler { - private final Logger logger = LoggerFactory.getLogger(AsyncCompletionHandlerBase.class); + private static final Logger LOGGER = LoggerFactory.getLogger(WebDavCompletionHandlerBase.class); + private static final DocumentBuilderFactory DOCUMENT_BUILDER_FACTORY; private final List bodyParts = Collections.synchronizedList(new ArrayList<>()); private HttpResponseStatus status; private HttpHeaders headers; + static { + DOCUMENT_BUILDER_FACTORY = DocumentBuilderFactory.newInstance(); + if (Boolean.getBoolean("org.asynchttpclient.webdav.enableDtd")) { + try { + DOCUMENT_BUILDER_FACTORY.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + } catch (ParserConfigurationException e) { + LOGGER.error("Failed to disable doctype declaration"); + throw new ExceptionInInitializerError(e); + } + } + } + /** * {@inheritDoc} */ @@ -75,13 +87,12 @@ public final State onHeadersReceived(final HttpHeaders headers) { } private Document readXMLResponse(InputStream stream) { - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); Document document; try { - document = factory.newDocumentBuilder().parse(stream); + document = DOCUMENT_BUILDER_FACTORY.newDocumentBuilder().parse(stream); parse(document); } catch (SAXException | IOException | ParserConfigurationException e) { - logger.error(e.getMessage(), e); + LOGGER.error(e.getMessage(), e); throw new RuntimeException(e); } return document; @@ -94,7 +105,7 @@ private void parse(Document document) { Node node = statusNode.item(i); String value = node.getFirstChild().getNodeValue(); - int statusCode = Integer.valueOf(value.substring(value.indexOf(" "), value.lastIndexOf(" ")).trim()); + int statusCode = Integer.parseInt(value.substring(value.indexOf(" "), value.lastIndexOf(" ")).trim()); String statusText = value.substring(value.lastIndexOf(" ")); status = new HttpStatusWrapper(status, statusText, statusCode); } @@ -122,7 +133,7 @@ public final T onCompleted() throws Exception { */ @Override public void onThrowable(Throwable t) { - logger.debug(t.getMessage(), t); + LOGGER.debug(t.getMessage(), t); } /** @@ -134,7 +145,7 @@ public void onThrowable(Throwable t) { */ abstract public T onCompleted(WebDavResponse response) throws Exception; - private class HttpStatusWrapper extends HttpResponseStatus { + private static class HttpStatusWrapper extends HttpResponseStatus { private final HttpResponseStatus wrapped; diff --git a/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties b/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties index cdc632f701..62bc177726 100644 --- a/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties +++ b/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties @@ -1,6 +1,7 @@ org.asynchttpclient.threadPoolName=AsyncHttpClient org.asynchttpclient.maxConnections=-1 org.asynchttpclient.maxConnectionsPerHost=-1 +org.asynchttpclient.acquireFreeChannelTimeout=0 org.asynchttpclient.connectTimeout=5000 org.asynchttpclient.pooledConnectionIdleTimeout=60000 org.asynchttpclient.connectionPoolCleanerPeriod=1000 @@ -31,6 +32,7 @@ org.asynchttpclient.sslSessionCacheSize=0 org.asynchttpclient.sslSessionTimeout=0 org.asynchttpclient.tcpNoDelay=true org.asynchttpclient.soReuseAddress=false +org.asynchttpclient.soKeepAlive=true org.asynchttpclient.soLinger=-1 org.asynchttpclient.soSndBuf=-1 org.asynchttpclient.soRcvBuf=-1 @@ -48,3 +50,6 @@ org.asynchttpclient.shutdownQuietPeriod=2000 org.asynchttpclient.shutdownTimeout=15000 org.asynchttpclient.useNativeTransport=false org.asynchttpclient.ioThreadsCount=0 +org.asynchttpclient.hashedWheelTimerTickDuration=100 +org.asynchttpclient.hashedWheelTimerSize=512 +org.asynchttpclient.expiredCookieEvictionDelay=30000 diff --git a/client/src/test/java/org/asynchttpclient/AsyncHttpClientDefaultsTest.java b/client/src/test/java/org/asynchttpclient/AsyncHttpClientDefaultsTest.java index bbbb512a58..8b7d172a45 100644 --- a/client/src/test/java/org/asynchttpclient/AsyncHttpClientDefaultsTest.java +++ b/client/src/test/java/org/asynchttpclient/AsyncHttpClientDefaultsTest.java @@ -115,6 +115,16 @@ public void testDefaultUseInsecureTrustManager() { testBooleanSystemProperty("useInsecureTrustManager", "defaultUseInsecureTrustManager", "false"); } + public void testDefaultHashedWheelTimerTickDuration() { + Assert.assertEquals(AsyncHttpClientConfigDefaults.defaultHashedWheelTimerTickDuration(), 100); + testIntegerSystemProperty("hashedWheelTimerTickDuration", "defaultHashedWheelTimerTickDuration", "100"); + } + + public void testDefaultHashedWheelTimerSize() { + Assert.assertEquals(AsyncHttpClientConfigDefaults.defaultHashedWheelTimerSize(), 512); + testIntegerSystemProperty("hashedWheelTimerSize", "defaultHashedWheelTimerSize", "512"); + } + private void testIntegerSystemProperty(String propertyName, String methodName, String value) { String previous = System.getProperty(ASYNC_CLIENT_CONFIG_ROOT + propertyName); System.setProperty(ASYNC_CLIENT_CONFIG_ROOT + propertyName, value); diff --git a/client/src/test/java/org/asynchttpclient/AsyncStreamHandlerTest.java b/client/src/test/java/org/asynchttpclient/AsyncStreamHandlerTest.java index e5ec906576..17dc2213ba 100644 --- a/client/src/test/java/org/asynchttpclient/AsyncStreamHandlerTest.java +++ b/client/src/test/java/org/asynchttpclient/AsyncStreamHandlerTest.java @@ -25,6 +25,8 @@ import org.testng.annotations.Test; import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -40,7 +42,7 @@ public class AsyncStreamHandlerTest extends HttpTest { - private static final String RESPONSE = "param_1_"; + private static final String RESPONSE = "param_1=value_1"; private static HttpServer server; @@ -93,18 +95,25 @@ public void asyncStreamPOSTTest() throws Throwable { @Override public State onHeadersReceived(HttpHeaders headers) { assertContentTypesEquals(headers.get(CONTENT_TYPE), TEXT_HTML_CONTENT_TYPE_WITH_UTF_8_CHARSET); + for (Map.Entry header : headers) { + if (header.getKey().startsWith("X-param")) { + builder.append(header.getKey().substring(2)).append("=").append(header.getValue()).append("&"); + } + } return State.CONTINUE; } @Override public State onBodyPartReceived(HttpResponseBodyPart content) { - builder.append(new String(content.getBodyPartBytes(), US_ASCII)); return State.CONTINUE; } @Override public String onCompleted() { - return builder.toString().trim(); + if (builder.length() > 0) { + builder.setLength(builder.length() - 1); + } + return builder.toString(); } }).get(10, TimeUnit.SECONDS); @@ -174,17 +183,24 @@ public void asyncStreamFutureTest() throws Throwable { public State onHeadersReceived(HttpHeaders headers) { assertContentTypesEquals(headers.get(CONTENT_TYPE), TEXT_HTML_CONTENT_TYPE_WITH_UTF_8_CHARSET); onHeadersReceived.set(true); + for (Map.Entry header : headers) { + if (header.getKey().startsWith("X-param")) { + builder.append(header.getKey().substring(2)).append("=").append(header.getValue()).append("&"); + } + } return State.CONTINUE; } @Override public State onBodyPartReceived(HttpResponseBodyPart content) { - builder.append(new String(content.getBodyPartBytes())); return State.CONTINUE; } @Override public String onCompleted() { + if (builder.length() > 0) { + builder.setLength(builder.length() - 1); + } return builder.toString().trim(); } @@ -254,17 +270,24 @@ public void asyncStreamReusePOSTTest() throws Throwable { @Override public State onHeadersReceived(HttpHeaders headers) { responseHeaders.set(headers); + for (Map.Entry header : headers) { + if (header.getKey().startsWith("X-param")) { + builder.append(header.getKey().substring(2)).append("=").append(header.getValue()).append("&"); + } + } return State.CONTINUE; } @Override public State onBodyPartReceived(HttpResponseBodyPart content) { - builder.append(new String(content.getBodyPartBytes())); return State.CONTINUE; } @Override public String onCompleted() { + if (builder.length() > 0) { + builder.setLength(builder.length() - 1); + } return builder.toString(); } }); @@ -405,6 +428,8 @@ public Integer onCompleted() { })); } + // This test is flaky - see https://github.com/AsyncHttpClient/async-http-client/issues/1728#issuecomment-699962325 + // For now, just run again if fails @Test(groups = "online") public void asyncOptionsTest() throws Throwable { @@ -413,7 +438,10 @@ public void asyncOptionsTest() throws Throwable { final AtomicReference responseHeaders = new AtomicReference<>(); + // Some responses contain the TRACE method, some do not - account for both + // FIXME: Actually refactor this test to account for both cases final String[] expected = {"GET", "HEAD", "OPTIONS", "POST"}; + final String[] expectedWithTrace = {"GET", "HEAD", "OPTIONS", "POST", "TRACE"}; Future f = client.prepareOptions("http://www.apache.org/").execute(new AsyncHandlerAdapter() { @Override @@ -432,10 +460,16 @@ public String onCompleted() { HttpHeaders h = responseHeaders.get(); assertNotNull(h); String[] values = h.get(ALLOW).split(",|, "); - assertNotNull(values); - assertEquals(values.length, expected.length); + assertNotNull(values); + // Some responses contain the TRACE method, some do not - account for both + assert(values.length == expected.length || values.length == expectedWithTrace.length); Arrays.sort(values); - assertEquals(values, expected); + // Some responses contain the TRACE method, some do not - account for both + if(values.length == expected.length) { + assertEquals(values, expected); + } else { + assertEquals(values, expectedWithTrace); + } })); } diff --git a/client/src/test/java/org/asynchttpclient/BasicAuthTest.java b/client/src/test/java/org/asynchttpclient/BasicAuthTest.java index f36ef7d48a..1743140bc8 100644 --- a/client/src/test/java/org/asynchttpclient/BasicAuthTest.java +++ b/client/src/test/java/org/asynchttpclient/BasicAuthTest.java @@ -65,7 +65,7 @@ public void setUpGlobal() throws Exception { server2.start(); port2 = connector2.getLocalPort(); - // need noAuth server to verify the preemptive auth mode (see basicAuthTestPreemtiveTest) + // need noAuth server to verify the preemptive auth mode (see basicAuthTestPreemptiveTest) serverNoAuth = new Server(); ServerConnector connectorNoAuth = addHttpConnector(serverNoAuth); serverNoAuth.setHandler(new SimpleHandler()); @@ -170,7 +170,7 @@ public Integer onCompleted() { } @Test - public void basicAuthTestPreemtiveTest() throws IOException, ExecutionException, TimeoutException, InterruptedException { + public void basicAuthTestPreemptiveTest() throws IOException, ExecutionException, TimeoutException, InterruptedException { try (AsyncHttpClient client = asyncHttpClient()) { // send the request to the no-auth endpoint to be able to verify the // auth header is really sent preemptively for the initial call. diff --git a/client/src/test/java/org/asynchttpclient/BasicHttpProxyToHttpsTest.java b/client/src/test/java/org/asynchttpclient/BasicHttpProxyToHttpsTest.java index a1919f6f4a..260395674a 100644 --- a/client/src/test/java/org/asynchttpclient/BasicHttpProxyToHttpsTest.java +++ b/client/src/test/java/org/asynchttpclient/BasicHttpProxyToHttpsTest.java @@ -31,18 +31,20 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; -import static io.netty.handler.codec.http.HttpHeaderNames.PROXY_AUTHENTICATE; -import static io.netty.handler.codec.http.HttpHeaderNames.PROXY_AUTHORIZATION; +import static io.netty.handler.codec.http.HttpHeaderNames.*; import static org.asynchttpclient.Dsl.*; import static org.asynchttpclient.test.TestUtils.addHttpConnector; import static org.asynchttpclient.test.TestUtils.addHttpsConnector; +import static org.asynchttpclient.config.AsyncHttpClientConfigDefaults.*; /** - * Test that validates that when having an HTTP proxy and trying to access an HTTPS through the proxy the proxy credentials should be passed during the CONNECT request. + * Test that validates that when having an HTTP proxy and trying to access an HTTPS + * through the proxy the proxy credentials and a custom user-agent (if set) should be passed during the CONNECT request. */ public class BasicHttpProxyToHttpsTest { private static final Logger LOGGER = LoggerFactory.getLogger(BasicHttpProxyToHttpsTest.class); + private static final String CUSTOM_USER_AGENT = "custom-user-agent"; private int httpPort; private int proxyPort; @@ -66,13 +68,24 @@ public void setUpGlobal() throws Exception { ConnectHandler connectHandler = new ConnectHandler() { @Override + // This proxy receives a CONNECT request from the client before making the real request for the target host. protected boolean handleAuthentication(HttpServletRequest request, HttpServletResponse response, String address) { + + // If the userAgent of the CONNECT request is the same as the default userAgent, + // then the custom userAgent was not properly propagated and the test should fail. + String userAgent = request.getHeader(USER_AGENT.toString()); + if(userAgent.equals(defaultUserAgent())) { + return false; + } + + // If the authentication failed, the test should also fail. String authorization = request.getHeader(PROXY_AUTHORIZATION.toString()); if (authorization == null) { response.setStatus(HttpServletResponse.SC_PROXY_AUTHENTICATION_REQUIRED); response.setHeader(PROXY_AUTHENTICATE.toString(), "Basic realm=\"Fake Realm\""); return false; - } else if (authorization.equals("Basic am9obmRvZTpwYXNz")) { + } + else if (authorization.equals("Basic am9obmRvZTpwYXNz")) { return true; } response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); @@ -98,6 +111,7 @@ public void nonPreemptiveProxyAuthWithHttpsTarget() throws IOException, Interrup String targetUrl = "https://localhost:" + httpPort + "/foo/bar"; Request request = get(targetUrl) .setProxyServer(proxyServer("127.0.0.1", proxyPort).setRealm(realm(AuthScheme.BASIC, "johndoe", "pass"))) + .setHeader("user-agent", CUSTOM_USER_AGENT) // .setRealm(realm(AuthScheme.BASIC, "user", "passwd")) .build(); Future responseFuture = client.executeRequest(request); @@ -107,4 +121,4 @@ public void nonPreemptiveProxyAuthWithHttpsTarget() throws IOException, Interrup Assert.assertEquals("/foo/bar", response.getHeader("X-pathInfo")); } } -} \ No newline at end of file +} diff --git a/client/src/test/java/org/asynchttpclient/BasicHttpTest.java b/client/src/test/java/org/asynchttpclient/BasicHttpTest.java index 0a26310491..1b7cd1d564 100755 --- a/client/src/test/java/org/asynchttpclient/BasicHttpTest.java +++ b/client/src/test/java/org/asynchttpclient/BasicHttpTest.java @@ -38,7 +38,10 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; +import java.io.UnsupportedEncodingException; import java.net.ConnectException; +import java.net.URLDecoder; +import java.net.URLEncoder; import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.*; @@ -206,6 +209,40 @@ public Response onCompleted(Response response) { })); } + @Test + public void postChineseChar() throws Throwable { + withClient().run(client -> + withServer(server).run(server -> { + HttpHeaders h = new DefaultHttpHeaders(); + h.add(CONTENT_TYPE, HttpHeaderValues.APPLICATION_X_WWW_FORM_URLENCODED); + + String chineseChar = "是"; + + Map> m = new HashMap<>(); + m.put("param", Collections.singletonList(chineseChar)); + + Request request = post(getTargetUrl()).setHeaders(h).setFormParams(m).build(); + + server.enqueueEcho(); + + client.executeRequest(request, new AsyncCompletionHandlerAdapter() { + @Override + public Response onCompleted(Response response) { + assertEquals(response.getStatusCode(), 200); + String value; + try { + // headers must be encoded + value = URLDecoder.decode(response.getHeader("X-param"), StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + assertEquals(value, chineseChar); + return response; + } + }).get(TIMEOUT, SECONDS); + })); + } + @Test public void headHasEmptyBody() throws Throwable { withClient().run(client -> @@ -751,7 +788,7 @@ public void onThrowable(Throwable t) { } @Test - public void nonBlockingNestedRequetsFromIoThreadAreFine() throws Throwable { + public void nonBlockingNestedRequestsFromIoThreadAreFine() throws Throwable { withClient().run(client -> withServer(server).run(server -> { diff --git a/client/src/test/java/org/asynchttpclient/BasicHttpsTest.java b/client/src/test/java/org/asynchttpclient/BasicHttpsTest.java index 216e260439..4395f0f49b 100644 --- a/client/src/test/java/org/asynchttpclient/BasicHttpsTest.java +++ b/client/src/test/java/org/asynchttpclient/BasicHttpsTest.java @@ -107,7 +107,7 @@ public void multipleSequentialPostRequestsOverHttps() throws Throwable { public void multipleConcurrentPostRequestsOverHttpsWithDisabledKeepAliveStrategy() throws Throwable { logger.debug(">>> multipleConcurrentPostRequestsOverHttpsWithDisabledKeepAliveStrategy"); - KeepAliveStrategy keepAliveStrategy = (ahcRequest, nettyRequest, nettyResponse) -> !ahcRequest.getUri().isSecured(); + KeepAliveStrategy keepAliveStrategy = (remoteAddress, ahcRequest, nettyRequest, nettyResponse) -> !ahcRequest.getUri().isSecured(); withClient(config().setSslEngineFactory(createSslEngineFactory()).setKeepAliveStrategy(keepAliveStrategy)).run(client -> withServer(server).run(server -> { diff --git a/client/src/test/java/org/asynchttpclient/CookieStoreTest.java b/client/src/test/java/org/asynchttpclient/CookieStoreTest.java index e16a477c25..e248e9a0c4 100644 --- a/client/src/test/java/org/asynchttpclient/CookieStoreTest.java +++ b/client/src/test/java/org/asynchttpclient/CookieStoreTest.java @@ -17,6 +17,8 @@ import io.netty.handler.codec.http.cookie.ClientCookieDecoder; import io.netty.handler.codec.http.cookie.ClientCookieEncoder; import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http.cookie.DefaultCookie; + import org.asynchttpclient.cookie.CookieStore; import org.asynchttpclient.cookie.ThreadSafeCookieStore; import org.asynchttpclient.uri.Uri; @@ -26,10 +28,14 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; +import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; import static org.testng.Assert.assertTrue; +import com.google.common.collect.Sets; + public class CookieStoreTest { private final Logger logger = LoggerFactory.getLogger(getClass()); @@ -46,7 +52,7 @@ public void tearDownGlobal() { } @Test - public void runAllSequentiallyBecauseNotThreadSafe() { + public void runAllSequentiallyBecauseNotThreadSafe() throws Exception { addCookieWithEmptyPath(); dontReturnCookieForAnotherDomain(); returnCookieWhenItWasSetOnSamePath(); @@ -77,6 +83,7 @@ public void runAllSequentiallyBecauseNotThreadSafe() { shouldAlsoServeNonSecureCookiesBasedOnTheUriScheme(); shouldNotServeSecureCookiesForDefaultRetrievedHttpUriScheme(); shouldServeSecureCookiesForSpecificallyRetrievedHttpUriScheme(); + shouldCleanExpiredCookieFromUnderlyingDataStructure(); } private void addCookieWithEmptyPath() { @@ -284,8 +291,9 @@ private void returnMultipleCookiesEvenIfTheyHaveSameName() { assertTrue(cookies1.size() == 2); assertTrue(cookies1.stream().filter(c -> c.value().equals("FOO") || c.value().equals("BAR")).count() == 2); - String result = ClientCookieEncoder.LAX.encode(cookies1.get(0), cookies1.get(1)); - assertTrue(result.equals("JSESSIONID=FOO; JSESSIONID=BAR")); + List encodedCookieStrings = cookies1.stream().map(ClientCookieEncoder.LAX::encode).collect(Collectors.toList()); + assertTrue(encodedCookieStrings.contains("JSESSIONID=FOO")); + assertTrue(encodedCookieStrings.contains("JSESSIONID=BAR")); } // rfc6265#section-1 Cookies for a given host are shared across all the ports on that host @@ -337,4 +345,26 @@ private void shouldServeSecureCookiesForSpecificallyRetrievedHttpUriScheme() { assertTrue(store.get(uri).get(0).value().equals("VALUE3")); assertTrue(store.get(uri).get(0).isSecure()); } + + private void shouldCleanExpiredCookieFromUnderlyingDataStructure() throws Exception { + ThreadSafeCookieStore store = new ThreadSafeCookieStore(); + store.add(Uri.create("https://foo.org/moodle/"), getCookie("JSESSIONID", "FOO", 1)); + store.add(Uri.create("https://bar.org/moodle/"), getCookie("JSESSIONID", "BAR", 1)); + store.add(Uri.create("https://bar.org/moodle/"), new DefaultCookie("UNEXPIRED_BAR", "BAR")); + store.add(Uri.create("https://foobar.org/moodle/"), new DefaultCookie("UNEXPIRED_FOOBAR", "FOOBAR")); + + + assertTrue(store.getAll().size() == 4); + Thread.sleep(2000); + store.evictExpired(); + assertTrue(store.getUnderlying().size() == 2); + Collection unexpiredCookieNames = store.getAll().stream().map(Cookie::name).collect(Collectors.toList()); + assertTrue(unexpiredCookieNames.containsAll(Sets.newHashSet("UNEXPIRED_BAR", "UNEXPIRED_FOOBAR"))); + } + + private static Cookie getCookie(String key, String value, int maxAge) { + DefaultCookie cookie = new DefaultCookie(key, value); + cookie.setMaxAge(maxAge); + return cookie; + } } diff --git a/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientTest.java b/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientTest.java new file mode 100644 index 0000000000..82a58860a0 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/DefaultAsyncHttpClientTest.java @@ -0,0 +1,106 @@ +package org.asynchttpclient; + +import io.netty.util.Timer; +import org.asynchttpclient.cookie.CookieEvictionTask; +import org.asynchttpclient.cookie.CookieStore; +import org.asynchttpclient.cookie.ThreadSafeCookieStore; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +import static org.asynchttpclient.Dsl.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.*; +import static org.testng.Assert.assertEquals; + +public class DefaultAsyncHttpClientTest { + + @Test + public void testWithSharedNettyTimerShouldScheduleCookieEvictionOnlyOnce() throws IOException { + Timer nettyTimerMock = mock(Timer.class); + CookieStore cookieStore = new ThreadSafeCookieStore(); + AsyncHttpClientConfig config = config().setNettyTimer(nettyTimerMock).setCookieStore(cookieStore).build(); + + try (AsyncHttpClient client1 = asyncHttpClient(config)) { + try (AsyncHttpClient client2 = asyncHttpClient(config)) { + assertEquals(cookieStore.count(), 2); + verify(nettyTimerMock, times(1)).newTimeout(any(CookieEvictionTask.class), anyLong(), any(TimeUnit.class)); + } + } + } + + @Test + public void testWitDefaultConfigShouldScheduleCookieEvictionForEachAHC() throws IOException { + AsyncHttpClientConfig config1 = config().build(); + try (AsyncHttpClient client1 = asyncHttpClient(config1)) { + AsyncHttpClientConfig config2 = config().build(); + try (AsyncHttpClient client2 = asyncHttpClient(config2)) { + assertEquals(config1.getCookieStore().count(), 1); + assertEquals(config2.getCookieStore().count(), 1); + } + } + } + + @Test + public void testWithSharedCookieStoreButNonSharedTimerShouldScheduleCookieEvictionForFirstAHC() throws IOException { + CookieStore cookieStore = new ThreadSafeCookieStore(); + Timer nettyTimerMock1 = mock(Timer.class); + AsyncHttpClientConfig config1 = config() + .setCookieStore(cookieStore).setNettyTimer(nettyTimerMock1).build(); + + try (AsyncHttpClient client1 = asyncHttpClient(config1)) { + Timer nettyTimerMock2 = mock(Timer.class); + AsyncHttpClientConfig config2 = config() + .setCookieStore(cookieStore).setNettyTimer(nettyTimerMock2).build(); + try (AsyncHttpClient client2 = asyncHttpClient(config2)) { + assertEquals(config1.getCookieStore().count(), 2); + verify(nettyTimerMock1, times(1)).newTimeout(any(CookieEvictionTask.class), anyLong(), any(TimeUnit.class)); + verify(nettyTimerMock2, never()).newTimeout(any(CookieEvictionTask.class), anyLong(), any(TimeUnit.class)); + } + } + + Timer nettyTimerMock3 = mock(Timer.class); + AsyncHttpClientConfig config3 = config() + .setCookieStore(cookieStore).setNettyTimer(nettyTimerMock3).build(); + + try (AsyncHttpClient client2 = asyncHttpClient(config3)) { + assertEquals(config1.getCookieStore().count(), 1); + verify(nettyTimerMock3, times(1)).newTimeout(any(CookieEvictionTask.class), anyLong(), any(TimeUnit.class)); + } + } + + @Test + public void testWithSharedCookieStoreButNonSharedTimerShouldReScheduleCookieEvictionWhenFirstInstanceGetClosed() throws IOException { + CookieStore cookieStore = new ThreadSafeCookieStore(); + Timer nettyTimerMock1 = mock(Timer.class); + AsyncHttpClientConfig config1 = config() + .setCookieStore(cookieStore).setNettyTimer(nettyTimerMock1).build(); + + try (AsyncHttpClient client1 = asyncHttpClient(config1)) { + assertEquals(config1.getCookieStore().count(), 1); + verify(nettyTimerMock1, times(1)).newTimeout(any(CookieEvictionTask.class), anyLong(), any(TimeUnit.class)); + } + + assertEquals(config1.getCookieStore().count(), 0); + + Timer nettyTimerMock2 = mock(Timer.class); + AsyncHttpClientConfig config2 = config() + .setCookieStore(cookieStore).setNettyTimer(nettyTimerMock2).build(); + + try (AsyncHttpClient client2 = asyncHttpClient(config2)) { + assertEquals(config1.getCookieStore().count(), 1); + verify(nettyTimerMock2, times(1)).newTimeout(any(CookieEvictionTask.class), anyLong(), any(TimeUnit.class)); + } + } + + @Test + public void testDisablingCookieStore() throws IOException { + AsyncHttpClientConfig config = config() + .setCookieStore(null).build(); + try (AsyncHttpClient client = asyncHttpClient(config)) { + // + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/Head302Test.java b/client/src/test/java/org/asynchttpclient/Head302Test.java index 2072f3dbb3..2a3f5bf294 100644 --- a/client/src/test/java/org/asynchttpclient/Head302Test.java +++ b/client/src/test/java/org/asynchttpclient/Head302Test.java @@ -70,9 +70,17 @@ public Response onCompleted(Response response) throws Exception { private static class Head302handler extends AbstractHandler { public void handle(String s, org.eclipse.jetty.server.Request r, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { if ("HEAD".equalsIgnoreCase(request.getMethod())) { - response.setStatus(HttpServletResponse.SC_FOUND); // 302 - response.setHeader("Location", request.getPathInfo() + "_moved"); - } else if ("GET".equalsIgnoreCase(request.getMethod())) { + // See https://github.com/AsyncHttpClient/async-http-client/issues/1728#issuecomment-700007980 + // When setFollowRedirect == TRUE, a follow-up request to a HEAD request will also be a HEAD. + // This will cause an infinite loop, which will error out once the maximum amount of redirects is hit (default 5). + // Instead, we (arbitrarily) choose to allow for 3 redirects and then return a 200. + if(request.getRequestURI().endsWith("_moved_moved_moved")) { + response.setStatus(HttpServletResponse.SC_OK); + } else { + response.setStatus(HttpServletResponse.SC_FOUND); // 302 + response.setHeader("Location", request.getPathInfo() + "_moved"); + } + } else if ("GET".equalsIgnoreCase(request.getMethod()) ) { response.setStatus(HttpServletResponse.SC_OK); } else { response.setStatus(HttpServletResponse.SC_FORBIDDEN); diff --git a/client/src/test/java/org/asynchttpclient/MultipleHeaderTest.java b/client/src/test/java/org/asynchttpclient/MultipleHeaderTest.java index 0bad2af9b1..f46e622f97 100644 --- a/client/src/test/java/org/asynchttpclient/MultipleHeaderTest.java +++ b/client/src/test/java/org/asynchttpclient/MultipleHeaderTest.java @@ -53,8 +53,8 @@ public void setUpGlobal() throws Exception { socket.shutdownInput(); if (req.endsWith("MultiEnt")) { OutputStreamWriter outputStreamWriter = new OutputStreamWriter(socket.getOutputStream()); - outputStreamWriter.append("HTTP/1.0 200 OK\n" + "Connection: close\n" + "Content-Type: text/plain; charset=iso-8859-1\n" + "Content-Length: 2\n" - + "Content-Length: 1\n" + "\n0\n"); + outputStreamWriter.append("HTTP/1.0 200 OK\n" + "Connection: close\n" + "Content-Type: text/plain; charset=iso-8859-1\n" + "X-Duplicated-Header: 2\n" + + "X-Duplicated-Header: 1\n" + "\n0\n"); outputStreamWriter.flush(); socket.shutdownOutput(); } else if (req.endsWith("MultiOther")) { @@ -148,7 +148,7 @@ public State onStatusReceived(HttpResponseStatus objectHttpResponseStatus) { public State onHeadersReceived(HttpHeaders response) { try { int i = 0; - for (String header : response.getAll(CONTENT_LENGTH)) { + for (String header : response.getAll("X-Duplicated-Header")) { clHeaders[i++] = header; } } finally { diff --git a/client/src/test/java/org/asynchttpclient/RequestBuilderTest.java b/client/src/test/java/org/asynchttpclient/RequestBuilderTest.java index 8ffb9494f7..968c408fbc 100644 --- a/client/src/test/java/org/asynchttpclient/RequestBuilderTest.java +++ b/client/src/test/java/org/asynchttpclient/RequestBuilderTest.java @@ -20,10 +20,7 @@ import io.netty.handler.codec.http.cookie.DefaultCookie; import org.testng.annotations.Test; -import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.util.*; -import java.util.concurrent.ExecutionException; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Collections.singletonList; @@ -75,7 +72,7 @@ public void testEncodesQueryParameters() { public void testChaining() { Request request = get("http://foo.com").addQueryParam("x", "value").build(); - Request request2 = new RequestBuilder(request).build(); + Request request2 = request.toBuilder().build(); assertEquals(request2.getUri(), request.getUri()); } @@ -174,4 +171,16 @@ public void testSettingQueryParamsBeforeUrlShouldNotProduceNPE() { Request request = requestBuilder.build(); assertEquals(request.getUrl(), "http://localhost?key=value"); } + + @Test + public void testSettingHeadersUsingMapWithStringKeys() { + Map> headers = new HashMap<>(); + headers.put("X-Forwarded-For", singletonList("10.0.0.1")); + + RequestBuilder requestBuilder = new RequestBuilder(); + requestBuilder.setHeaders(headers); + requestBuilder.setUrl("http://localhost"); + Request request = requestBuilder.build(); + assertEquals(request.getHeaders().get("X-Forwarded-For"), "10.0.0.1"); + } } diff --git a/client/src/test/java/org/asynchttpclient/channel/MaxTotalConnectionTest.java b/client/src/test/java/org/asynchttpclient/channel/MaxTotalConnectionTest.java index 5992bf3ed5..492399e3af 100644 --- a/client/src/test/java/org/asynchttpclient/channel/MaxTotalConnectionTest.java +++ b/client/src/test/java/org/asynchttpclient/channel/MaxTotalConnectionTest.java @@ -33,7 +33,7 @@ public class MaxTotalConnectionTest extends AbstractBasicTest { @Test(groups = "online") public void testMaxTotalConnectionsExceedingException() throws IOException { - String[] urls = new String[]{"http://google.com", "http://github.com/"}; + String[] urls = new String[]{"https://google.com", "https://github.com"}; AsyncHttpClientConfig config = config() .setConnectTimeout(1000) @@ -69,7 +69,7 @@ public void testMaxTotalConnectionsExceedingException() throws IOException { @Test(groups = "online") public void testMaxTotalConnections() throws Exception { - String[] urls = new String[]{"http://google.com", "http://gatling.io"}; + String[] urls = new String[]{"https://www.google.com", "https://www.youtube.com"}; final CountDownLatch latch = new CountDownLatch(2); final AtomicReference ex = new AtomicReference<>(); diff --git a/client/src/test/java/org/asynchttpclient/filter/FilterTest.java b/client/src/test/java/org/asynchttpclient/filter/FilterTest.java index 10b36507a5..14997d6234 100644 --- a/client/src/test/java/org/asynchttpclient/filter/FilterTest.java +++ b/client/src/test/java/org/asynchttpclient/filter/FilterTest.java @@ -101,7 +101,7 @@ public void replayResponseFilterTest() throws Exception { ResponseFilter responseFilter = new ResponseFilter() { public FilterContext filter(FilterContext ctx) { if (replay.getAndSet(false)) { - Request request = new RequestBuilder(ctx.getRequest()).addHeader("X-Replay", "true").build(); + Request request = ctx.getRequest().toBuilder().addHeader("X-Replay", "true").build(); return new FilterContext.FilterContextBuilder().asyncHandler(ctx.getAsyncHandler()).request(request).replayRequest(true).build(); } return ctx; @@ -123,7 +123,7 @@ public void replayStatusCodeResponseFilterTest() throws Exception { ResponseFilter responseFilter = new ResponseFilter() { public FilterContext filter(FilterContext ctx) { if (ctx.getResponseStatus() != null && ctx.getResponseStatus().getStatusCode() == 200 && replay.getAndSet(false)) { - Request request = new RequestBuilder(ctx.getRequest()).addHeader("X-Replay", "true").build(); + Request request = ctx.getRequest().toBuilder().addHeader("X-Replay", "true").build(); return new FilterContext.FilterContextBuilder().asyncHandler(ctx.getAsyncHandler()).request(request).replayRequest(true).build(); } return ctx; @@ -145,7 +145,7 @@ public void replayHeaderResponseFilterTest() throws Exception { ResponseFilter responseFilter = new ResponseFilter() { public FilterContext filter(FilterContext ctx) { if (ctx.getResponseHeaders() != null && ctx.getResponseHeaders().get("Ping").equals("Pong") && replay.getAndSet(false)) { - Request request = new RequestBuilder(ctx.getRequest()).addHeader("Ping", "Pong").build(); + Request request = ctx.getRequest().toBuilder().addHeader("Ping", "Pong").build(); return new FilterContext.FilterContextBuilder().asyncHandler(ctx.getAsyncHandler()).request(request).replayRequest(true).build(); } return ctx; diff --git a/client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcesserTest.java b/client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorTest.java similarity index 97% rename from client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcesserTest.java rename to client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorTest.java index 883a2bb971..1cd0704bfd 100644 --- a/client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcesserTest.java +++ b/client/src/test/java/org/asynchttpclient/handler/resumable/PropertiesBasedResumableProcessorTest.java @@ -21,7 +21,7 @@ /** * @author Benjamin Hanzelmann */ -public class PropertiesBasedResumableProcesserTest { +public class PropertiesBasedResumableProcessorTest { @Test public void testSaveLoad() { diff --git a/client/src/test/java/org/asynchttpclient/netty/NettyConnectionResetByPeerTest.java b/client/src/test/java/org/asynchttpclient/netty/NettyConnectionResetByPeerTest.java new file mode 100644 index 0000000000..6a3dcc9ce1 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/NettyConnectionResetByPeerTest.java @@ -0,0 +1,108 @@ +package org.asynchttpclient.netty; + +import org.asynchttpclient.DefaultAsyncHttpClient; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; +import org.asynchttpclient.RequestBuilder; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.Arrays; +import java.util.concurrent.Exchanger; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; +import static org.testng.Assert.assertTrue; + +public class NettyConnectionResetByPeerTest { + + private String resettingServerAddress; + + @BeforeTest + public void setUp() { + resettingServerAddress = createResettingServer(); + } + + @Test + public void testAsyncHttpClientConnectionResetByPeer() throws InterruptedException { + try { + DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder() + .setRequestTimeout(1500) + .build(); + new DefaultAsyncHttpClient(config).executeRequest( + new RequestBuilder("GET").setUrl(resettingServerAddress) + ) + .get(); + } catch (ExecutionException e) { + Throwable ex = e.getCause(); + assertThat(ex, is(instanceOf(IOException.class))); + } + } + + private static String createResettingServer() { + return createServer(sock -> { + try (Socket socket = sock) { + socket.setSoLinger(true, 0); + InputStream inputStream = socket.getInputStream(); + //to not eliminate read + OutputStream os = new OutputStream() { + @Override + public void write(int b) { + // Do nothing + } + }; + os.write(startRead(inputStream)); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + + private static String createServer(Consumer handler) { + Exchanger portHolder = new Exchanger<>(); + Thread t = new Thread(() -> { + try (ServerSocket ss = new ServerSocket(0)) { + portHolder.exchange(ss.getLocalPort()); + while (true) { + handler.accept(ss.accept()); + } + } catch (Exception e) { + if (e instanceof InterruptedException) { + Thread.currentThread() + .interrupt(); + } + throw new RuntimeException(e); + } + }); + t.setDaemon(true); + t.start(); + return tryGetAddress(portHolder); + } + + private static String tryGetAddress(Exchanger portHolder) { + try { + return "http://localhost:" + portHolder.exchange(0); + } catch (InterruptedException e) { + Thread.currentThread() + .interrupt(); + throw new RuntimeException(e); + } + } + + private static byte[] startRead(InputStream inputStream) throws IOException { + byte[] buffer = new byte[4]; + int length = inputStream.read(buffer); + return Arrays.copyOf(buffer, length); + } + +} diff --git a/client/src/test/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreTest.java b/client/src/test/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreTest.java deleted file mode 100644 index a387ba408b..0000000000 --- a/client/src/test/java/org/asynchttpclient/netty/channel/NonBlockingSemaphoreTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright (c) 2017 AsyncHttpClient Project. All rights reserved. - * - * This program is licensed to you under the Apache License Version 2.0, - * and you may not use this file except in compliance with the Apache License Version 2.0. - * You may obtain a copy of the Apache License Version 2.0 at - * http://www.apache.org/licenses/LICENSE-2.0. - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the Apache License Version 2.0 is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. - */ -package org.asynchttpclient.netty.channel; - -import org.testng.annotations.Test; - -import java.util.concurrent.Semaphore; - -import static org.testng.Assert.*; - -/** - * @author Stepan Koltsov - */ -public class NonBlockingSemaphoreTest { - - @Test - public void test0() { - Mirror mirror = new Mirror(0); - assertFalse(mirror.tryAcquire()); - } - - @Test - public void three() { - Mirror mirror = new Mirror(3); - for (int i = 0; i < 3; ++i) { - assertTrue(mirror.tryAcquire()); - } - assertFalse(mirror.tryAcquire()); - mirror.release(); - assertTrue(mirror.tryAcquire()); - } - - @Test - public void negative() { - Mirror mirror = new Mirror(-1); - assertFalse(mirror.tryAcquire()); - mirror.release(); - assertFalse(mirror.tryAcquire()); - mirror.release(); - assertTrue(mirror.tryAcquire()); - } - - private static class Mirror { - private final Semaphore real; - private final NonBlockingSemaphore nonBlocking; - - Mirror(int permits) { - real = new Semaphore(permits); - nonBlocking = new NonBlockingSemaphore(permits); - } - - boolean tryAcquire() { - boolean a = real.tryAcquire(); - boolean b = nonBlocking.tryAcquire(); - assertEquals(a, b); - return a; - } - - void release() { - real.release(); - nonBlocking.release(); - } - } - -} diff --git a/client/src/test/java/org/asynchttpclient/netty/channel/SemaphoreRunner.java b/client/src/test/java/org/asynchttpclient/netty/channel/SemaphoreRunner.java new file mode 100644 index 0000000000..7bff799ceb --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/channel/SemaphoreRunner.java @@ -0,0 +1,52 @@ +package org.asynchttpclient.netty.channel; + +class SemaphoreRunner { + + final ConnectionSemaphore semaphore; + final Thread acquireThread; + + volatile long acquireTime; + volatile Exception acquireException; + + public SemaphoreRunner(ConnectionSemaphore semaphore, Object partitionKey) { + this.semaphore = semaphore; + this.acquireThread = new Thread(() -> { + long beforeAcquire = System.currentTimeMillis(); + try { + semaphore.acquireChannelLock(partitionKey); + } catch (Exception e) { + acquireException = e; + } finally { + acquireTime = System.currentTimeMillis() - beforeAcquire; + } + }); + } + + public void acquire() { + this.acquireThread.start(); + } + + public void interrupt() { + this.acquireThread.interrupt(); + } + + public void await() { + try { + this.acquireThread.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + public boolean finished() { + return !this.acquireThread.isAlive(); + } + + public long getAcquireTime() { + return acquireTime; + } + + public Exception getAcquireException() { + return acquireException; + } +} diff --git a/client/src/test/java/org/asynchttpclient/netty/channel/SemaphoreTest.java b/client/src/test/java/org/asynchttpclient/netty/channel/SemaphoreTest.java new file mode 100644 index 0000000000..125cd9b066 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/netty/channel/SemaphoreTest.java @@ -0,0 +1,143 @@ +package org.asynchttpclient.netty.channel; + +import org.asynchttpclient.exception.TooManyConnectionsException; +import org.asynchttpclient.exception.TooManyConnectionsPerHostException; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.testng.AssertJUnit.*; + +public class SemaphoreTest { + + static final int CHECK_ACQUIRE_TIME__PERMITS = 10; + static final int CHECK_ACQUIRE_TIME__TIMEOUT = 100; + + static final int NON_DETERMINISTIC__INVOCATION_COUNT = 10; + static final int NON_DETERMINISTIC__SUCCESS_PERCENT = 70; + + private final Object PK = new Object(); + + @DataProvider(name = "permitsAndRunnersCount") + public Object[][] permitsAndRunnersCount() { + Object[][] objects = new Object[100][]; + int row = 0; + for (int i = 0; i < 10; i++) { + for (int j = 0; j < 10; j++) { + objects[row++] = new Object[]{i, j}; + } + } + return objects; + } + + @Test(timeOut = 1000, dataProvider = "permitsAndRunnersCount") + public void maxConnectionCheckPermitCount(int permitCount, int runnerCount) { + allSemaphoresCheckPermitCount(new MaxConnectionSemaphore(permitCount, 0), permitCount, runnerCount); + } + + @Test(timeOut = 1000, dataProvider = "permitsAndRunnersCount") + public void perHostCheckPermitCount(int permitCount, int runnerCount) { + allSemaphoresCheckPermitCount(new PerHostConnectionSemaphore(permitCount, 0), permitCount, runnerCount); + } + + @Test(timeOut = 3000, dataProvider = "permitsAndRunnersCount") + public void combinedCheckPermitCount(int permitCount, int runnerCount) { + allSemaphoresCheckPermitCount(new CombinedConnectionSemaphore(permitCount, permitCount, 0), permitCount, runnerCount); + allSemaphoresCheckPermitCount(new CombinedConnectionSemaphore(0, permitCount, 0), permitCount, runnerCount); + allSemaphoresCheckPermitCount(new CombinedConnectionSemaphore(permitCount, 0, 0), permitCount, runnerCount); + } + + private void allSemaphoresCheckPermitCount(ConnectionSemaphore semaphore, int permitCount, int runnerCount) { + List runners = IntStream.range(0, runnerCount) + .mapToObj(i -> new SemaphoreRunner(semaphore, PK)) + .collect(Collectors.toList()); + runners.forEach(SemaphoreRunner::acquire); + runners.forEach(SemaphoreRunner::await); + + long tooManyConnectionsCount = runners.stream().map(SemaphoreRunner::getAcquireException) + .filter(Objects::nonNull) + .filter(e -> e instanceof IOException) + .count(); + + long acquired = runners.stream().map(SemaphoreRunner::getAcquireException) + .filter(Objects::isNull) + .count(); + + int expectedAcquired = permitCount > 0 ? Math.min(permitCount, runnerCount) : runnerCount; + + assertEquals(expectedAcquired, acquired); + assertEquals(runnerCount - acquired, tooManyConnectionsCount); + } + + @Test(timeOut = 1000, invocationCount = NON_DETERMINISTIC__INVOCATION_COUNT, successPercentage = NON_DETERMINISTIC__SUCCESS_PERCENT) + public void maxConnectionCheckAcquireTime() { + checkAcquireTime(new MaxConnectionSemaphore(CHECK_ACQUIRE_TIME__PERMITS, CHECK_ACQUIRE_TIME__TIMEOUT)); + } + + @Test(timeOut = 1000, invocationCount = NON_DETERMINISTIC__INVOCATION_COUNT, successPercentage = NON_DETERMINISTIC__SUCCESS_PERCENT) + public void perHostCheckAcquireTime() { + checkAcquireTime(new PerHostConnectionSemaphore(CHECK_ACQUIRE_TIME__PERMITS, CHECK_ACQUIRE_TIME__TIMEOUT)); + } + + @Test(timeOut = 1000, invocationCount = NON_DETERMINISTIC__INVOCATION_COUNT, successPercentage = NON_DETERMINISTIC__SUCCESS_PERCENT) + public void combinedCheckAcquireTime() { + checkAcquireTime(new CombinedConnectionSemaphore(CHECK_ACQUIRE_TIME__PERMITS, + CHECK_ACQUIRE_TIME__PERMITS, + CHECK_ACQUIRE_TIME__TIMEOUT)); + } + + private void checkAcquireTime(ConnectionSemaphore semaphore) { + List runners = IntStream.range(0, CHECK_ACQUIRE_TIME__PERMITS * 2) + .mapToObj(i -> new SemaphoreRunner(semaphore, PK)) + .collect(Collectors.toList()); + long acquireStartTime = System.currentTimeMillis(); + runners.forEach(SemaphoreRunner::acquire); + runners.forEach(SemaphoreRunner::await); + long timeToAcquire = System.currentTimeMillis() - acquireStartTime; + + assertTrue("Semaphore acquired too soon: " + timeToAcquire+" ms",timeToAcquire >= (CHECK_ACQUIRE_TIME__TIMEOUT - 50)); //Lower Bound + assertTrue("Semaphore acquired too late: " + timeToAcquire+" ms",timeToAcquire <= (CHECK_ACQUIRE_TIME__TIMEOUT + 300)); //Upper Bound + } + + @Test(timeOut = 1000) + public void maxConnectionCheckRelease() throws IOException { + checkRelease(new MaxConnectionSemaphore(1, 0)); + } + + @Test(timeOut = 1000) + public void perHostCheckRelease() throws IOException { + checkRelease(new PerHostConnectionSemaphore(1, 0)); + } + + @Test(timeOut = 1000) + public void combinedCheckRelease() throws IOException { + checkRelease(new CombinedConnectionSemaphore(1, 1, 0)); + } + + private void checkRelease(ConnectionSemaphore semaphore) throws IOException { + semaphore.acquireChannelLock(PK); + boolean tooManyCaught = false; + try { + semaphore.acquireChannelLock(PK); + } catch (TooManyConnectionsException | TooManyConnectionsPerHostException e) { + tooManyCaught = true; + } + assertTrue(tooManyCaught); + tooManyCaught = false; + semaphore.releaseChannelLock(PK); + try { + semaphore.acquireChannelLock(PK); + } catch (TooManyConnectionsException | TooManyConnectionsPerHostException e) { + tooManyCaught = true; + } + assertFalse(tooManyCaught); + } + + +} + diff --git a/client/src/test/java/org/asynchttpclient/proxy/CustomHeaderProxyTest.java b/client/src/test/java/org/asynchttpclient/proxy/CustomHeaderProxyTest.java new file mode 100644 index 0000000000..37b8c0edd8 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/proxy/CustomHeaderProxyTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2010-2012 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.proxy; + +import io.netty.handler.codec.http.DefaultHttpHeaders; +import org.asynchttpclient.AbstractBasicTest; +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.AsyncHttpClientConfig; +import org.asynchttpclient.Response; +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; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.asynchttpclient.Dsl.*; +import static org.asynchttpclient.test.TestUtils.*; +import static org.testng.Assert.assertEquals; + +/** + * Proxy usage tests. + */ +public class CustomHeaderProxyTest extends AbstractBasicTest { + + private Server server2; + + private final String customHeaderName = "Custom-Header"; + private final String customHeaderValue = "Custom-Value"; + + public AbstractHandler configureHandler() throws Exception { + return new ProxyHandler(customHeaderName, customHeaderValue); + } + + @BeforeClass(alwaysRun = true) + public void setUpGlobal() throws Exception { + server = new Server(); + ServerConnector connector = addHttpConnector(server); + server.setHandler(configureHandler()); + server.start(); + port1 = connector.getLocalPort(); + + server2 = new Server(); + ServerConnector connector2 = addHttpsConnector(server2); + server2.setHandler(new EchoHandler()); + server2.start(); + port2 = connector2.getLocalPort(); + + logger.info("Local HTTP server started successfully"); + } + + @AfterClass(alwaysRun = true) + public void tearDownGlobal() throws Exception { + server.stop(); + server2.stop(); + } + + @Test + public void testHttpProxy() throws Exception { + AsyncHttpClientConfig config = config() + .setFollowRedirect(true) + .setProxyServer( + proxyServer("localhost", port1) + .setCustomHeaders((req) -> new DefaultHttpHeaders().add(customHeaderName, customHeaderValue)) + .build() + ) + .setUseInsecureTrustManager(true) + .build(); + try (AsyncHttpClient asyncHttpClient = asyncHttpClient(config)) { + Response r = asyncHttpClient.executeRequest(post(getTargetUrl2()).setBody(new ByteArrayBodyGenerator(LARGE_IMAGE_BYTES))).get(); + assertEquals(r.getStatusCode(), 200); + } + } + + public static class ProxyHandler extends ConnectHandler { + String customHeaderName; + String customHeaderValue; + + public ProxyHandler(String customHeaderName, String customHeaderValue) { + this.customHeaderName = customHeaderName; + this.customHeaderValue = customHeaderValue; + } + + @Override + public void handle(String s, Request r, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + if (HttpConstants.Methods.CONNECT.equalsIgnoreCase(request.getMethod())) { + if (request.getHeader(customHeaderName).equals(customHeaderValue)) { + response.setStatus(HttpServletResponse.SC_OK); + super.handle(s, r, request, response); + } else { + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + r.setHandled(true); + } + } else { + super.handle(s, r, request, response); + } + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/proxy/HttpsProxyTest.java b/client/src/test/java/org/asynchttpclient/proxy/HttpsProxyTest.java index 481767022e..a8a1e8d3d3 100644 --- a/client/src/test/java/org/asynchttpclient/proxy/HttpsProxyTest.java +++ b/client/src/test/java/org/asynchttpclient/proxy/HttpsProxyTest.java @@ -13,6 +13,7 @@ package org.asynchttpclient.proxy; import org.asynchttpclient.*; +import org.asynchttpclient.request.body.generator.ByteArrayBodyGenerator; import org.asynchttpclient.test.EchoHandler; import org.eclipse.jetty.proxy.ConnectHandler; import org.eclipse.jetty.server.Server; @@ -23,6 +24,7 @@ import org.testng.annotations.Test; import static org.asynchttpclient.Dsl.*; +import static org.asynchttpclient.test.TestUtils.LARGE_IMAGE_BYTES; import static org.asynchttpclient.test.TestUtils.addHttpConnector; import static org.asynchttpclient.test.TestUtils.addHttpsConnector; import static org.testng.Assert.assertEquals; @@ -84,6 +86,36 @@ public void testConfigProxy() throws Exception { } } + @Test + public void testNoDirectRequestBodyWithProxy() throws Exception { + AsyncHttpClientConfig config = config() + .setFollowRedirect(true) + .setProxyServer(proxyServer("localhost", port1).build()) + .setUseInsecureTrustManager(true) + .build(); + try (AsyncHttpClient asyncHttpClient = asyncHttpClient(config)) { + Response r = asyncHttpClient.executeRequest(post(getTargetUrl2()).setBody(new ByteArrayBodyGenerator(LARGE_IMAGE_BYTES))).get(); + assertEquals(r.getStatusCode(), 200); + } + } + + @Test + public void testDecompressBodyWithProxy() throws Exception { + AsyncHttpClientConfig config = config() + .setFollowRedirect(true) + .setProxyServer(proxyServer("localhost", port1).build()) + .setUseInsecureTrustManager(true) + .build(); + try (AsyncHttpClient asyncHttpClient = asyncHttpClient(config)) { + String body = "hello world"; + Response r = asyncHttpClient.executeRequest(post(getTargetUrl2()) + .setHeader("X-COMPRESS", "true") + .setBody(body)).get(); + assertEquals(r.getStatusCode(), 200); + assertEquals(r.getResponseBody(), body); + } + } + @Test public void testPooledConnectionsWithProxy() throws Exception { diff --git a/client/src/test/java/org/asynchttpclient/reactivestreams/ReactiveStreamsDownLoadTest.java b/client/src/test/java/org/asynchttpclient/reactivestreams/ReactiveStreamsDownloadTest.java similarity index 92% rename from client/src/test/java/org/asynchttpclient/reactivestreams/ReactiveStreamsDownLoadTest.java rename to client/src/test/java/org/asynchttpclient/reactivestreams/ReactiveStreamsDownloadTest.java index 9a782bfcfb..536f9c1d82 100644 --- a/client/src/test/java/org/asynchttpclient/reactivestreams/ReactiveStreamsDownLoadTest.java +++ b/client/src/test/java/org/asynchttpclient/reactivestreams/ReactiveStreamsDownloadTest.java @@ -39,11 +39,11 @@ import static org.asynchttpclient.Dsl.asyncHttpClient; import static org.testng.Assert.assertEquals; -public class ReactiveStreamsDownLoadTest { +public class ReactiveStreamsDownloadTest { - private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveStreamsDownLoadTest.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveStreamsDownloadTest.class); - private int serverPort = 8080; + private final int serverPort = 8080; private File largeFile; private File smallFile; @@ -93,7 +93,7 @@ static protected class SimpleStreamedAsyncHandler implements StreamedAsyncHandle @Override public State onStream(Publisher publisher) { - LOGGER.debug("SimpleStreamedAsyncHandleronCompleted onStream"); + LOGGER.debug("SimpleStreamedAsyncHandlerOnCompleted onStream"); publisher.subscribe(subscriber); return State.CONTINUE; } @@ -104,8 +104,8 @@ public void onThrowable(Throwable t) { } @Override - public State onBodyPartReceived(HttpResponseBodyPart bodyPart) throws Exception { - LOGGER.debug("SimpleStreamedAsyncHandleronCompleted onBodyPartReceived"); + public State onBodyPartReceived(HttpResponseBodyPart bodyPart) { + LOGGER.debug("SimpleStreamedAsyncHandlerOnCompleted onBodyPartReceived"); throw new AssertionError("Should not have received body part"); } @@ -115,13 +115,13 @@ public State onStatusReceived(HttpResponseStatus responseStatus) { } @Override - public State onHeadersReceived(HttpHeaders headers) throws Exception { + public State onHeadersReceived(HttpHeaders headers) { return State.CONTINUE; } @Override - public SimpleStreamedAsyncHandler onCompleted() throws Exception { - LOGGER.debug("SimpleStreamedAsyncHandleronCompleted onSubscribe"); + public SimpleStreamedAsyncHandler onCompleted() { + LOGGER.debug("SimpleStreamedAsyncHandlerOnCompleted onSubscribe"); return this; } diff --git a/client/src/test/java/org/asynchttpclient/reactivestreams/ReactiveStreamsErrorTest.java b/client/src/test/java/org/asynchttpclient/reactivestreams/ReactiveStreamsErrorTest.java new file mode 100644 index 0000000000..d95973a0eb --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/reactivestreams/ReactiveStreamsErrorTest.java @@ -0,0 +1,378 @@ +package org.asynchttpclient.reactivestreams; + +import io.netty.handler.codec.http.HttpHeaders; +import org.asynchttpclient.AbstractBasicTest; +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.HttpResponseBodyPart; +import org.asynchttpclient.HttpResponseStatus; +import org.asynchttpclient.exception.RemotelyClosedException; +import org.asynchttpclient.handler.StreamedAsyncHandler; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; + +import static org.asynchttpclient.Dsl.asyncHttpClient; +import static org.asynchttpclient.Dsl.config; +import static org.testng.Assert.*; + +public class ReactiveStreamsErrorTest extends AbstractBasicTest { + + private static final Logger LOGGER = LoggerFactory.getLogger(ReactiveStreamsErrorTest.class); + + private static final byte[] BODY_CHUNK = "someBytes".getBytes(); + + private AsyncHttpClient client; + private ServletResponseHandler servletResponseHandler; + + @BeforeTest + public void initClient() { + client = asyncHttpClient(config() + .setMaxRequestRetry(0) + .setRequestTimeout(3_000) + .setReadTimeout(1_000)); + } + + @AfterTest + public void closeClient() throws Throwable { + client.close(); + } + + @Override + public AbstractHandler configureHandler() throws Exception { + return new AbstractHandler() { + @Override + public void handle(String target, Request r, HttpServletRequest request, HttpServletResponse response) { + try { + servletResponseHandler.handle(response); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + }; + } + + @Test + public void timeoutWithNoStatusLineSent() throws Throwable { + try { + execute(response -> Thread.sleep(5_000), bodyPublisher -> {}); + fail("Request should have timed out"); + } catch (ExecutionException e) { + expectReadTimeout(e.getCause()); + } + } + + @Test + public void neverSubscribingToResponseBodyHitsRequestTimeout() throws Throwable { + try { + execute(response -> { + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + Thread.sleep(500); + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + + response.getOutputStream().close(); + }, bodyPublisher -> {}); + + fail("Request should have timed out"); + } catch (ExecutionException e) { + expectRequestTimeout(e.getCause()); + } + } + + @Test + public void readTimeoutInMiddleOfBody() throws Throwable { + ServletResponseHandler responseHandler = response -> { + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + Thread.sleep(500); + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + Thread.sleep(5_000); + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + response.getOutputStream().close(); + }; + + try { + execute(responseHandler, bodyPublisher -> bodyPublisher.subscribe(new ManualRequestSubscriber() { + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + })); + fail("Request should have timed out"); + } catch (ExecutionException e) { + expectReadTimeout(e.getCause()); + } + } + + @Test + public void notRequestingForLongerThanReadTimeoutDoesNotCauseTimeout() throws Throwable { + ServletResponseHandler responseHandler = response -> { + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + Thread.sleep(100); + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + response.getOutputStream().close(); + }; + + ManualRequestSubscriber subscriber = new ManualRequestSubscriber() { + @Override + public void onSubscribe(Subscription s) { + super.onSubscribe(s); + new Thread(() -> { + try { + // chunk 1 + s.request(1); + + // there will be no read for longer than the read timeout + Thread.sleep(1_500); + + // read the rest + s.request(Long.MAX_VALUE); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }).start(); + } + }; + + execute(responseHandler, bodyPublisher -> bodyPublisher.subscribe(subscriber)); + + subscriber.await(); + + assertEquals(subscriber.elements.size(), 2); + } + + @Test + public void readTimeoutCancelsBodyStream() throws Throwable { + ServletResponseHandler responseHandler = response -> { + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + Thread.sleep(2_000); + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + response.getOutputStream().close(); + }; + + ManualRequestSubscriber subscriber = new ManualRequestSubscriber() { + @Override + public void onSubscribe(Subscription s) { + super.onSubscribe(s); + s.request(Long.MAX_VALUE); + } + }; + + try { + execute(responseHandler, bodyPublisher -> bodyPublisher.subscribe(subscriber)); + fail("Request should have timed out"); + } catch (ExecutionException e) { + expectReadTimeout(e.getCause()); + } + + subscriber.await(); + + assertEquals(subscriber.elements.size(), 1); + } + + @Test + public void requestTimeoutCancelsBodyStream() throws Throwable { + ServletResponseHandler responseHandler = response -> { + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + Thread.sleep(900); + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + Thread.sleep(900); + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + Thread.sleep(900); + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + Thread.sleep(900); + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + response.getOutputStream().close(); + }; + + ManualRequestSubscriber subscriber = new ManualRequestSubscriber() { + @Override + public void onSubscribe(Subscription subscription) { + super.onSubscribe(subscription); + subscription.request(Long.MAX_VALUE); + } + }; + + try { + execute(responseHandler, bodyPublisher -> bodyPublisher.subscribe(subscriber)); + fail("Request should have timed out"); + } catch (ExecutionException e) { + expectRequestTimeout(e.getCause()); + } + + subscriber.await(); + + expectRequestTimeout(subscriber.error); + assertEquals(subscriber.elements.size(), 4); + } + + @Test + public void ioErrorsArePropagatedToSubscriber() throws Throwable { + ServletResponseHandler responseHandler = response -> { + response.setContentLength(100); + + response.getOutputStream().write(BODY_CHUNK); + response.getOutputStream().flush(); + + response.getOutputStream().close(); + }; + + ManualRequestSubscriber subscriber = new ManualRequestSubscriber() { + @Override + public void onSubscribe(Subscription subscription) { + super.onSubscribe(subscription); + subscription.request(Long.MAX_VALUE); + } + }; + + Throwable error = null; + try { + execute(responseHandler, bodyPublisher -> bodyPublisher.subscribe(subscriber)); + fail("Request should have failed"); + } catch (ExecutionException e) { + error = e.getCause(); + assertTrue(error instanceof RemotelyClosedException, "Unexpected error: " + e); + } + + subscriber.await(); + + assertEquals(subscriber.error, error); + assertEquals(subscriber.elements.size(), 1); + } + + private void expectReadTimeout(Throwable e) { + assertTrue(e instanceof TimeoutException, + "Expected a read timeout, but got " + e); + assertTrue(e.getMessage().contains("Read timeout"), + "Expected read timeout, but was " + e); + } + + private void expectRequestTimeout(Throwable e) { + assertTrue(e instanceof TimeoutException, + "Expected a request timeout, but got " + e); + assertTrue(e.getMessage().contains("Request timeout"), + "Expected request timeout, but was " + e); + } + + private void execute(ServletResponseHandler responseHandler, + Consumer> bodyConsumer) throws Exception { + this.servletResponseHandler = responseHandler; + client.prepareGet(getTargetUrl()) + .execute(new SimpleStreamer(bodyConsumer)) + .get(3_500, TimeUnit.MILLISECONDS); + } + + private interface ServletResponseHandler { + void handle(HttpServletResponse response) throws Exception; + } + + private static class SimpleStreamer implements StreamedAsyncHandler { + + final Consumer> bodyStreamHandler; + + private SimpleStreamer(Consumer> bodyStreamHandler) { + this.bodyStreamHandler = bodyStreamHandler; + } + + @Override + public State onStream(Publisher publisher) { + LOGGER.debug("Got stream"); + bodyStreamHandler.accept(publisher); + return State.CONTINUE; + } + + @Override + public State onStatusReceived(HttpResponseStatus responseStatus) { + LOGGER.debug("Got status line"); + return State.CONTINUE; + } + + @Override + public State onHeadersReceived(HttpHeaders headers) { + LOGGER.debug("Got headers"); + return State.CONTINUE; + } + + @Override + public State onBodyPartReceived(HttpResponseBodyPart bodyPart) { + throw new IllegalStateException(); + } + + @Override + public void onThrowable(Throwable t) { + LOGGER.debug("Caught error", t); + } + + @Override + public Void onCompleted() { + LOGGER.debug("Completed request"); + return null; + } + } + + private static class ManualRequestSubscriber implements Subscriber { + private final List elements = Collections.synchronizedList(new ArrayList<>()); + private final CountDownLatch latch = new CountDownLatch(1); + private volatile Throwable error; + + @Override + public void onSubscribe(Subscription subscription) { + LOGGER.debug("SimpleSubscriber onSubscribe"); + } + + @Override + public void onNext(HttpResponseBodyPart t) { + LOGGER.debug("SimpleSubscriber onNext"); + elements.add(t); + } + + @Override + public void onError(Throwable error) { + LOGGER.debug("SimpleSubscriber onError"); + this.error = error; + latch.countDown(); + } + + @Override + public void onComplete() { + LOGGER.debug("SimpleSubscriber onComplete"); + latch.countDown(); + } + + void await() throws InterruptedException { + if (!latch.await(3_500, TimeUnit.MILLISECONDS)) { + fail("Request should have finished"); + } + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/reactivestreams/FailingReactiveStreamsTest.java b/client/src/test/java/org/asynchttpclient/reactivestreams/ReactiveStreamsRetryTest.java similarity index 98% rename from client/src/test/java/org/asynchttpclient/reactivestreams/FailingReactiveStreamsTest.java rename to client/src/test/java/org/asynchttpclient/reactivestreams/ReactiveStreamsRetryTest.java index 860678b35a..d09b16d037 100644 --- a/client/src/test/java/org/asynchttpclient/reactivestreams/FailingReactiveStreamsTest.java +++ b/client/src/test/java/org/asynchttpclient/reactivestreams/ReactiveStreamsRetryTest.java @@ -32,7 +32,7 @@ import static org.asynchttpclient.test.TestUtils.LARGE_IMAGE_BYTES; import static org.testng.Assert.assertTrue; -public class FailingReactiveStreamsTest extends AbstractBasicTest { +public class ReactiveStreamsRetryTest extends AbstractBasicTest { @Test public void testRetryingOnFailingStream() throws Exception { diff --git a/client/src/test/java/org/asynchttpclient/request/body/ChunkingTest.java b/client/src/test/java/org/asynchttpclient/request/body/ChunkingTest.java index 538488c2e5..55bf3248c2 100755 --- a/client/src/test/java/org/asynchttpclient/request/body/ChunkingTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/ChunkingTest.java @@ -33,7 +33,7 @@ public class ChunkingTest extends AbstractBasicTest { // So we can just test the returned data is the image, - // and doesn't contain the chunked delimeters. + // and doesn't contain the chunked delimiters. @Test public void testBufferLargerThanFileWithStreamBodyGenerator() throws Throwable { doTestWithInputStreamBodyGenerator(new BufferedInputStream(Files.newInputStream(LARGE_IMAGE_FILE.toPath()), 400000)); diff --git a/client/src/test/java/org/asynchttpclient/request/body/FilePartLargeFileTest.java b/client/src/test/java/org/asynchttpclient/request/body/FilePartLargeFileTest.java index 06e33c95a7..d0807b1adc 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/FilePartLargeFileTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/FilePartLargeFileTest.java @@ -51,7 +51,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest req, H total += count; } resp.setStatus(200); - resp.addHeader("X-TRANFERED", String.valueOf(total)); + resp.addHeader("X-TRANSFERRED", String.valueOf(total)); resp.getOutputStream().flush(); resp.getOutputStream().close(); diff --git a/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java new file mode 100644 index 0000000000..48d45341b5 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/request/body/InputStreamPartLargeFileTest.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2018 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.request.body; + +import org.asynchttpclient.AbstractBasicTest; +import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.Response; +import org.asynchttpclient.request.body.multipart.InputStreamPart; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.testng.annotations.Test; + +import javax.servlet.ServletInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.*; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.asynchttpclient.Dsl.asyncHttpClient; +import static org.asynchttpclient.Dsl.config; +import static org.asynchttpclient.test.TestUtils.LARGE_IMAGE_FILE; +import static org.asynchttpclient.test.TestUtils.createTempFile; +import static org.testng.Assert.assertEquals; + +public class InputStreamPartLargeFileTest extends AbstractBasicTest { + + @Override + public AbstractHandler configureHandler() throws Exception { + return new AbstractHandler() { + + public void handle(String target, Request baseRequest, HttpServletRequest req, HttpServletResponse resp) throws IOException { + + ServletInputStream in = req.getInputStream(); + byte[] b = new byte[8192]; + + int count; + int total = 0; + while ((count = in.read(b)) != -1) { + b = new byte[8192]; + total += count; + } + resp.setStatus(200); + resp.addHeader("X-TRANSFERRED", String.valueOf(total)); + resp.getOutputStream().flush(); + resp.getOutputStream().close(); + + baseRequest.setHandled(true); + } + }; + } + + @Test + public void testPutImageFile() throws Exception { + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE)); + Response response = client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), LARGE_IMAGE_FILE.length(), "application/octet-stream", UTF_8)).execute().get(); + assertEquals(response.getStatusCode(), 200); + } + } + + @Test + public void testPutImageFileUnknownSize() throws Exception { + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + InputStream inputStream = new BufferedInputStream(new FileInputStream(LARGE_IMAGE_FILE)); + Response response = client.preparePut(getTargetUrl()).addBodyPart(new InputStreamPart("test", inputStream, LARGE_IMAGE_FILE.getName(), -1, "application/octet-stream", UTF_8)).execute().get(); + assertEquals(response.getStatusCode(), 200); + } + } + + @Test + public void testPutLargeTextFile() throws Exception { + File file = createTempFile(1024 * 1024); + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + Response response = client.preparePut(getTargetUrl()) + .addBodyPart(new InputStreamPart("test", inputStream, file.getName(), file.length(), "application/octet-stream", UTF_8)).execute().get(); + assertEquals(response.getStatusCode(), 200); + } + } + + @Test + public void testPutLargeTextFileUnknownSize() throws Exception { + File file = createTempFile(1024 * 1024); + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + + try (AsyncHttpClient client = asyncHttpClient(config().setRequestTimeout(100 * 6000))) { + Response response = client.preparePut(getTargetUrl()) + .addBodyPart(new InputStreamPart("test", inputStream, file.getName(), -1, "application/octet-stream", UTF_8)).execute().get(); + assertEquals(response.getStatusCode(), 200); + } + } +} diff --git a/client/src/test/java/org/asynchttpclient/request/body/TransferListenerTest.java b/client/src/test/java/org/asynchttpclient/request/body/TransferListenerTest.java index b4df376ca4..3a55ccd6bc 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/TransferListenerTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/TransferListenerTest.java @@ -98,8 +98,8 @@ public void basicPutFileTest() throws Exception { final AtomicReference throwable = new AtomicReference<>(); final AtomicReference hSent = new AtomicReference<>(); final AtomicReference hRead = new AtomicReference<>(); - final AtomicInteger bbReceivedLenght = new AtomicInteger(0); - final AtomicLong bbSentLenght = new AtomicLong(0L); + final AtomicInteger bbReceivedLength = new AtomicInteger(0); + final AtomicLong bbSentLength = new AtomicLong(0L); final AtomicBoolean completed = new AtomicBoolean(false); @@ -120,11 +120,11 @@ public void onResponseHeadersReceived(HttpHeaders headers) { } public void onBytesReceived(byte[] b) { - bbReceivedLenght.addAndGet(b.length); + bbReceivedLength.addAndGet(b.length); } public void onBytesSent(long amount, long current, long total) { - bbSentLenght.addAndGet(amount); + bbSentLength.addAndGet(amount); } public void onRequestResponseCompleted() { @@ -142,8 +142,8 @@ public void onThrowable(Throwable t) { assertEquals(response.getStatusCode(), 200); assertNotNull(hRead.get()); assertNotNull(hSent.get()); - assertEquals(bbReceivedLenght.get(), file.length(), "Number of received bytes incorrect"); - assertEquals(bbSentLenght.get(), file.length(), "Number of sent bytes incorrect"); + assertEquals(bbReceivedLength.get(), file.length(), "Number of received bytes incorrect"); + assertEquals(bbSentLength.get(), file.length(), "Number of sent bytes incorrect"); } } @@ -153,8 +153,8 @@ public void basicPutFileBodyGeneratorTest() throws Exception { final AtomicReference throwable = new AtomicReference<>(); final AtomicReference hSent = new AtomicReference<>(); final AtomicReference hRead = new AtomicReference<>(); - final AtomicInteger bbReceivedLenght = new AtomicInteger(0); - final AtomicLong bbSentLenght = new AtomicLong(0L); + final AtomicInteger bbReceivedLength = new AtomicInteger(0); + final AtomicLong bbSentLength = new AtomicLong(0L); final AtomicBoolean completed = new AtomicBoolean(false); @@ -172,11 +172,11 @@ public void onResponseHeadersReceived(HttpHeaders headers) { } public void onBytesReceived(byte[] b) { - bbReceivedLenght.addAndGet(b.length); + bbReceivedLength.addAndGet(b.length); } public void onBytesSent(long amount, long current, long total) { - bbSentLenght.addAndGet(amount); + bbSentLength.addAndGet(amount); } public void onRequestResponseCompleted() { @@ -194,8 +194,8 @@ public void onThrowable(Throwable t) { assertEquals(response.getStatusCode(), 200); assertNotNull(hRead.get()); assertNotNull(hSent.get()); - assertEquals(bbReceivedLenght.get(), file.length(), "Number of received bytes incorrect"); - assertEquals(bbSentLenght.get(), file.length(), "Number of sent bytes incorrect"); + assertEquals(bbReceivedLength.get(), file.length(), "Number of received bytes incorrect"); + assertEquals(bbSentLength.get(), file.length(), "Number of sent bytes incorrect"); } } diff --git a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java index 0b6c5fe6f2..72d7300c3d 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartBodyTest.java @@ -19,8 +19,7 @@ import org.asynchttpclient.request.body.Body.BodyState; import org.testng.annotations.Test; -import java.io.File; -import java.io.IOException; +import java.io.*; import java.net.URISyntaxException; import java.net.URL; import java.nio.ByteBuffer; @@ -63,7 +62,15 @@ private static File getTestfile() throws URISyntaxException { } private static MultipartBody buildMultipart() { - return MultipartUtils.newMultipartBody(PARTS, EmptyHttpHeaders.INSTANCE); + List parts = new ArrayList<>(PARTS); + try { + File testFile = getTestfile(); + InputStream inputStream = new BufferedInputStream(new FileInputStream(testFile)); + parts.add(new InputStreamPart("isPart", inputStream, testFile.getName(), testFile.length())); + } catch (URISyntaxException | FileNotFoundException e) { + throw new ExceptionInInitializerError(e); + } + return MultipartUtils.newMultipartBody(parts, EmptyHttpHeaders.INSTANCE); } private static long transferWithCopy(MultipartBody multipartBody, int bufferSize) throws IOException { @@ -115,8 +122,8 @@ public int write(ByteBuffer src) { public void transferWithCopy() throws Exception { for (int bufferLength = 1; bufferLength < MAX_MULTIPART_CONTENT_LENGTH_ESTIMATE + 1; bufferLength++) { try (MultipartBody multipartBody = buildMultipart()) { - long tranferred = transferWithCopy(multipartBody, bufferLength); - assertEquals(tranferred, multipartBody.getContentLength()); + long transferred = transferWithCopy(multipartBody, bufferLength); + assertEquals(transferred, multipartBody.getContentLength()); } } } @@ -125,8 +132,8 @@ public void transferWithCopy() throws Exception { public void transferZeroCopy() throws Exception { for (int bufferLength = 1; bufferLength < MAX_MULTIPART_CONTENT_LENGTH_ESTIMATE + 1; bufferLength++) { try (MultipartBody multipartBody = buildMultipart()) { - long tranferred = transferZeroCopy(multipartBody, bufferLength); - assertEquals(tranferred, multipartBody.getContentLength()); + long transferred = transferZeroCopy(multipartBody, bufferLength); + assertEquals(transferred, multipartBody.getContentLength()); } } } diff --git a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java index 77584ecdf3..879a40a9d7 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/multipart/MultipartUploadTest.java @@ -77,21 +77,33 @@ public void testSendingSmallFilesAndByteArray() throws Exception { File testResource1File = getClasspathFile(testResource1); File testResource2File = getClasspathFile(testResource2); File testResource3File = getClasspathFile(testResource3); + InputStream inputStreamFile1 = new BufferedInputStream(new FileInputStream(testResource1File)); + InputStream inputStreamFile2 = new BufferedInputStream(new FileInputStream(testResource2File)); + InputStream inputStreamFile3 = new BufferedInputStream(new FileInputStream(testResource3File)); List testFiles = new ArrayList<>(); testFiles.add(testResource1File); testFiles.add(testResource2File); testFiles.add(testResource3File); + testFiles.add(testResource3File); + testFiles.add(testResource2File); + testFiles.add(testResource1File); List expected = new ArrayList<>(); expected.add(expectedContents); expected.add(expectedContents2); expected.add(expectedContents3); + expected.add(expectedContents3); + expected.add(expectedContents2); + expected.add(expectedContents); List gzipped = new ArrayList<>(); gzipped.add(false); gzipped.add(true); gzipped.add(false); + gzipped.add(false); + gzipped.add(true); + gzipped.add(false); File tmpFile = File.createTempFile("textbytearray", ".txt"); try (OutputStream os = Files.newOutputStream(tmpFile.toPath())) { @@ -109,8 +121,11 @@ public void testSendingSmallFilesAndByteArray() throws Exception { .addBodyPart(new StringPart("Name", "Dominic")) .addBodyPart(new FilePart("file3", testResource3File, "text/plain", UTF_8)) .addBodyPart(new StringPart("Age", "3")).addBodyPart(new StringPart("Height", "shrimplike")) + .addBodyPart(new InputStreamPart("inputStream3", inputStreamFile3, testResource3File.getName(), testResource3File.length(), "text/plain", UTF_8)) + .addBodyPart(new InputStreamPart("inputStream2", inputStreamFile2, testResource2File.getName(), testResource2File.length(), "application/x-gzip", null)) .addBodyPart(new StringPart("Hair", "ridiculous")).addBodyPart(new ByteArrayPart("file4", expectedContents.getBytes(UTF_8), "text/plain", UTF_8, "bytearray.txt")) + .addBodyPart(new InputStreamPart("inputStream1", inputStreamFile1, testResource1File.getName(), testResource1File.length(), "text/plain", UTF_8)) .build(); Response res = c.executeRequest(r).get(); @@ -142,6 +157,65 @@ public void sendEmptyFileZeroCopy() throws Exception { sendEmptyFile0(false); } + private void sendEmptyFileInputStream(boolean disableZeroCopy) throws Exception { + File file = getClasspathFile("empty.txt"); + try (AsyncHttpClient c = asyncHttpClient(config().setDisableZeroCopy(disableZeroCopy))) { + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + Request r = post("http://localhost" + ":" + port1 + "/upload") + .addBodyPart(new InputStreamPart("file", inputStream, file.getName(), file.length(), "text/plain", UTF_8)).build(); + + Response res = c.executeRequest(r).get(); + assertEquals(res.getStatusCode(), 200); + } + } + + @Test + public void testSendEmptyFileInputStream() throws Exception { + sendEmptyFileInputStream(true); + } + + @Test + public void testSendEmptyFileInputStreamZeroCopy() throws Exception { + sendEmptyFileInputStream(false); + } + + private void sendFileInputStream(boolean useContentLength, boolean disableZeroCopy) throws Exception { + File file = getClasspathFile("textfile.txt"); + try (AsyncHttpClient c = asyncHttpClient(config().setDisableZeroCopy(disableZeroCopy))) { + InputStream inputStream = new BufferedInputStream(new FileInputStream(file)); + InputStreamPart part; + if (useContentLength) { + part = new InputStreamPart("file", inputStream, file.getName(), file.length()); + } else { + part = new InputStreamPart("file", inputStream, file.getName()); + } + Request r = post("http://localhost" + ":" + port1 + "/upload").addBodyPart(part).build(); + + Response res = c.executeRequest(r).get(); + assertEquals(res.getStatusCode(), 200); + } + } + + @Test + public void testSendFileInputStreamUnknownContentLength() throws Exception { + sendFileInputStream(false, true); + } + + @Test + public void testSendFileInputStreamZeroCopyUnknownContentLength() throws Exception { + sendFileInputStream(false, false); + } + + @Test + public void testSendFileInputStreamKnownContentLength() throws Exception { + sendFileInputStream(true, true); + } + + @Test + public void testSendFileInputStreamZeroCopyKnownContentLength() throws Exception { + sendFileInputStream(true, false); + } + /** * Test that the files were sent, based on the response from the servlet */ diff --git a/client/src/test/java/org/asynchttpclient/request/body/multipart/part/MultipartPartTest.java b/client/src/test/java/org/asynchttpclient/request/body/multipart/part/MultipartPartTest.java index b66c7975ff..b96d14cd09 100644 --- a/client/src/test/java/org/asynchttpclient/request/body/multipart/part/MultipartPartTest.java +++ b/client/src/test/java/org/asynchttpclient/request/body/multipart/part/MultipartPartTest.java @@ -239,12 +239,12 @@ private class TestFileLikePart extends FileLikePart { this(name, contentType, charset, contentId, null); } - TestFileLikePart(String name, String contentType, Charset charset, String contentId, String transfertEncoding) { - this(name, contentType, charset, contentId, transfertEncoding, null); + TestFileLikePart(String name, String contentType, Charset charset, String contentId, String transferEncoding) { + this(name, contentType, charset, contentId, transferEncoding, null); } - TestFileLikePart(String name, String contentType, Charset charset, String contentId, String transfertEncoding, String fileName) { - super(name, contentType, charset, fileName, contentId, transfertEncoding); + TestFileLikePart(String name, String contentType, Charset charset, String contentId, String transferEncoding, String fileName) { + super(name, contentType, charset, fileName, contentId, transferEncoding); } } diff --git a/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java b/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java new file mode 100644 index 0000000000..92ff4a4d78 --- /dev/null +++ b/client/src/test/java/org/asynchttpclient/spnego/SpnegoEngineTest.java @@ -0,0 +1,163 @@ +package org.asynchttpclient.spnego; + +import org.apache.commons.io.FileUtils; +import org.apache.kerby.kerberos.kerb.server.SimpleKdcServer; +import org.asynchttpclient.AbstractBasicTest; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +public class SpnegoEngineTest extends AbstractBasicTest { + private static SimpleKdcServer kerbyServer; + + private static String basedir; + private static String alice; + private static String bob; + private static File aliceKeytab; + private static File bobKeytab; + private static File loginConfig; + + @BeforeClass + public static void startServers() throws Exception { + basedir = System.getProperty("basedir"); + if (basedir == null) { + basedir = new File(".").getCanonicalPath(); + } + + // System.setProperty("sun.security.krb5.debug", "true"); + System.setProperty("java.security.krb5.conf", + new File(basedir + File.separator + "target" + File.separator + "krb5.conf").getCanonicalPath()); + loginConfig = new File(basedir + File.separator + "target" + File.separator + "kerberos.jaas"); + System.setProperty("java.security.auth.login.config", loginConfig.getCanonicalPath()); + + kerbyServer = new SimpleKdcServer(); + + kerbyServer.setKdcRealm("service.ws.apache.org"); + kerbyServer.setAllowUdp(false); + kerbyServer.setWorkDir(new File(basedir, "target")); + + //kerbyServer.setInnerKdcImpl(new NettyKdcServerImpl(kerbyServer.getKdcSetting())); + + kerbyServer.init(); + + // Create principals + alice = "alice@service.ws.apache.org"; + bob = "bob/service.ws.apache.org@service.ws.apache.org"; + + kerbyServer.createPrincipal(alice, "alice"); + kerbyServer.createPrincipal(bob, "bob"); + + aliceKeytab = new File(basedir + File.separator + "target" + File.separator + "alice.keytab"); + bobKeytab = new File(basedir + File.separator + "target" + File.separator + "bob.keytab"); + kerbyServer.exportPrincipal(alice, aliceKeytab); + kerbyServer.exportPrincipal(bob, bobKeytab); + + kerbyServer.start(); + + FileUtils.copyInputStreamToFile(SpnegoEngine.class.getResourceAsStream("/kerberos.jaas"), loginConfig); + } + + @Test + public void testSpnegoGenerateTokenWithUsernamePassword() throws Exception { + SpnegoEngine spnegoEngine = new SpnegoEngine("alice", + "alice", + "bob", + "service.ws.apache.org", + false, + null, + "alice", + null); + String token = spnegoEngine.generateToken("localhost"); + Assert.assertNotNull(token); + Assert.assertTrue(token.startsWith("YII")); + } + + @Test(expectedExceptions = SpnegoEngineException.class) + public void testSpnegoGenerateTokenWithUsernamePasswordFail() throws Exception { + SpnegoEngine spnegoEngine = new SpnegoEngine("alice", + "wrong password", + "bob", + "service.ws.apache.org", + false, + null, + "alice", + null); + spnegoEngine.generateToken("localhost"); + } + + @Test + public void testSpnegoGenerateTokenWithCustomLoginConfig() throws Exception { + Map loginConfig = new HashMap<>(); + loginConfig.put("useKeyTab", "true"); + loginConfig.put("storeKey", "true"); + loginConfig.put("refreshKrb5Config", "true"); + loginConfig.put("keyTab", aliceKeytab.getCanonicalPath()); + loginConfig.put("principal", alice); + loginConfig.put("debug", String.valueOf(true)); + SpnegoEngine spnegoEngine = new SpnegoEngine(null, + null, + "bob", + "service.ws.apache.org", + false, + loginConfig, + null, + null); + + String token = spnegoEngine.generateToken("localhost"); + Assert.assertNotNull(token); + Assert.assertTrue(token.startsWith("YII")); + } + + @Test + public void testGetCompleteServicePrincipalName() throws Exception { + { + SpnegoEngine spnegoEngine = new SpnegoEngine(null, + null, + "bob", + "service.ws.apache.org", + false, + null, + null, + null); + Assert.assertEquals("bob@service.ws.apache.org", spnegoEngine.getCompleteServicePrincipalName("localhost")); + } + { + SpnegoEngine spnegoEngine = new SpnegoEngine(null, + null, + null, + "service.ws.apache.org", + true, + null, + null, + null); + Assert.assertNotEquals("HTTP@localhost", spnegoEngine.getCompleteServicePrincipalName("localhost")); + Assert.assertTrue(spnegoEngine.getCompleteServicePrincipalName("localhost").startsWith("HTTP@")); + } + { + SpnegoEngine spnegoEngine = new SpnegoEngine(null, + null, + null, + "service.ws.apache.org", + false, + null, + null, + null); + Assert.assertEquals("HTTP@localhost", spnegoEngine.getCompleteServicePrincipalName("localhost")); + } + } + + @AfterClass + public static void cleanup() throws Exception { + if (kerbyServer != null) { + kerbyServer.stop(); + } + FileUtils.deleteQuietly(aliceKeytab); + FileUtils.deleteQuietly(bobKeytab); + FileUtils.deleteQuietly(loginConfig); + } +} diff --git a/client/src/test/java/org/asynchttpclient/test/EchoHandler.java b/client/src/test/java/org/asynchttpclient/test/EchoHandler.java index ee19f2ee0a..6826155648 100644 --- a/client/src/test/java/org/asynchttpclient/test/EchoHandler.java +++ b/client/src/test/java/org/asynchttpclient/test/EchoHandler.java @@ -23,11 +23,15 @@ import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Enumeration; +import java.util.zip.Deflater; -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; -import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_MD5; +import static io.netty.handler.codec.http.HttpHeaderNames.*; +import static io.netty.handler.codec.http.HttpHeaderValues.CHUNKED; +import static io.netty.handler.codec.http.HttpHeaderValues.DEFLATE; public class EchoHandler extends AbstractHandler { @@ -69,6 +73,15 @@ public void handle(String pathInContext, Request request, HttpServletRequest htt httpResponse.sendRedirect(httpRequest.getHeader("X-redirect")); return; } + if (headerName.startsWith("X-fail")) { + byte[] body = "custom error message".getBytes(StandardCharsets.US_ASCII); + httpResponse.addHeader(CONTENT_LENGTH.toString(), String.valueOf(body.length)); + httpResponse.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED); + httpResponse.getOutputStream().write(body); + httpResponse.getOutputStream().flush(); + httpResponse.getOutputStream().close(); + return; + } httpResponse.addHeader("X-" + headerName, httpRequest.getHeader(headerName)); } @@ -105,18 +118,14 @@ public void handle(String pathInContext, Request request, HttpServletRequest htt } } - String requestBodyLength = httpRequest.getHeader("X-" + CONTENT_LENGTH); - - if (requestBodyLength != null) { - byte[] requestBodyBytes = IOUtils.toByteArray(httpRequest.getInputStream()); - int total = requestBodyBytes.length; + if (httpRequest.getHeader("X-COMPRESS") != null) { + byte[] compressed = deflate(IOUtils.toByteArray(httpRequest.getInputStream())); + httpResponse.addIntHeader(CONTENT_LENGTH.toString(), compressed.length); + httpResponse.addHeader(CONTENT_ENCODING.toString(), DEFLATE.toString()); + httpResponse.getOutputStream().write(compressed, 0, compressed.length); - httpResponse.addIntHeader("X-" + CONTENT_LENGTH, total); - String md5 = TestUtils.md5(requestBodyBytes, 0, total); - httpResponse.addHeader(CONTENT_MD5.toString(), md5); - - httpResponse.getOutputStream().write(requestBodyBytes, 0, total); } else { + httpResponse.addHeader(TRANSFER_ENCODING.toString(), CHUNKED.toString()); int size = 16384; if (httpRequest.getContentLength() > 0) { size = httpRequest.getContentLength(); @@ -138,4 +147,21 @@ public void handle(String pathInContext, Request request, HttpServletRequest htt // FIXME don't always close, depends on the test, cf ReactiveStreamsTest httpResponse.getOutputStream().close(); } + + private static byte[] deflate(byte[] input) throws IOException { + Deflater compressor = new Deflater(); + compressor.setLevel(Deflater.BEST_COMPRESSION); + + compressor.setInput(input); + compressor.finish(); + + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(input.length)) { + byte[] buf = new byte[1024]; + while (!compressor.finished()) { + int count = compressor.deflate(buf); + bos.write(buf, 0, count); + } + return bos.toByteArray(); + } + } } diff --git a/client/src/test/java/org/asynchttpclient/test/EventCollectingHandler.java b/client/src/test/java/org/asynchttpclient/test/EventCollectingHandler.java index 252de41913..8047c5f843 100644 --- a/client/src/test/java/org/asynchttpclient/test/EventCollectingHandler.java +++ b/client/src/test/java/org/asynchttpclient/test/EventCollectingHandler.java @@ -20,6 +20,7 @@ import org.asynchttpclient.netty.request.NettyRequest; import org.testng.Assert; +import javax.net.ssl.SSLSession; import java.net.InetSocketAddress; import java.util.List; import java.util.Queue; @@ -128,7 +129,8 @@ public void onTlsHandshakeAttempt() { } @Override - public void onTlsHandshakeSuccess() { + public void onTlsHandshakeSuccess(SSLSession sslSession) { + Assert.assertNotNull(sslSession); firedEvents.add(TLS_HANDSHAKE_SUCCESS_EVENT); } diff --git a/client/src/test/java/org/asynchttpclient/testserver/HttpServer.java b/client/src/test/java/org/asynchttpclient/testserver/HttpServer.java index ae387469a7..9b74656b5e 100644 --- a/client/src/test/java/org/asynchttpclient/testserver/HttpServer.java +++ b/client/src/test/java/org/asynchttpclient/testserver/HttpServer.java @@ -25,6 +25,8 @@ import javax.servlet.http.HttpServletResponse; import java.io.Closeable; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.Enumeration; import java.util.Map.Entry; import java.util.concurrent.ConcurrentLinkedQueue; @@ -208,8 +210,9 @@ protected void handle0(String target, Request baseRequest, HttpServletRequest re response.addHeader("X-" + headerName, request.getHeader(headerName)); } + StringBuilder requestBody = new StringBuilder(); for (Entry e : baseRequest.getParameterMap().entrySet()) { - response.addHeader("X-" + e.getKey(), e.getValue()[0]); + response.addHeader("X-" + e.getKey(), URLEncoder.encode(e.getValue()[0], StandardCharsets.UTF_8.name())); } Cookie[] cs = request.getCookies(); @@ -219,14 +222,6 @@ protected void handle0(String target, Request baseRequest, HttpServletRequest re } } - Enumeration parameterNames = request.getParameterNames(); - StringBuilder requestBody = new StringBuilder(); - while (parameterNames.hasMoreElements()) { - String param = parameterNames.nextElement(); - response.addHeader("X-" + param, request.getParameter(param)); - requestBody.append(param); - requestBody.append("_"); - } if (requestBody.length() > 0) { response.getOutputStream().write(requestBody.toString().getBytes()); } diff --git a/client/src/test/java/org/asynchttpclient/ws/CloseCodeReasonMessageTest.java b/client/src/test/java/org/asynchttpclient/ws/CloseCodeReasonMessageTest.java index 52aaefc3a5..ebbfb511fa 100644 --- a/client/src/test/java/org/asynchttpclient/ws/CloseCodeReasonMessageTest.java +++ b/client/src/test/java/org/asynchttpclient/ws/CloseCodeReasonMessageTest.java @@ -51,7 +51,8 @@ public void onCloseWithCodeServerClose() throws Exception { c.prepareGet(getTargetUrl()).execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new Listener(latch, text)).build()).get(); latch.await(); - assertEquals(text.get(), "1001-Idle Timeout"); + // used to be correct 001-Idle Timeout prior to Jetty 9.4.15... + assertEquals(text.get(), "1000-"); } } diff --git a/client/src/test/java/org/asynchttpclient/ws/TextMessageTest.java b/client/src/test/java/org/asynchttpclient/ws/TextMessageTest.java index d3249944d4..72c3e1d244 100644 --- a/client/src/test/java/org/asynchttpclient/ws/TextMessageTest.java +++ b/client/src/test/java/org/asynchttpclient/ws/TextMessageTest.java @@ -16,6 +16,7 @@ import org.testng.annotations.Test; import java.net.UnknownHostException; +import java.net.ConnectException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicReference; @@ -70,11 +71,14 @@ public void onEmptyListenerTest() throws Exception { } } - @Test(timeOut = 60000, expectedExceptions = UnknownHostException.class) + @Test(timeOut = 60000, expectedExceptions = {UnknownHostException.class, ConnectException.class}) public void onFailureTest() throws Throwable { try (AsyncHttpClient c = asyncHttpClient()) { c.prepareGet("ws://abcdefg").execute(new WebSocketUpgradeHandler.Builder().build()).get(); } catch (ExecutionException e) { + + String expectedMessage = "DNS name not found"; + assertTrue(e.getCause().toString().contains(expectedMessage)); throw e.getCause(); } } diff --git a/client/src/test/resources/kerberos.jaas b/client/src/test/resources/kerberos.jaas new file mode 100644 index 0000000000..cd5b316bf1 --- /dev/null +++ b/client/src/test/resources/kerberos.jaas @@ -0,0 +1,8 @@ + +alice { + com.sun.security.auth.module.Krb5LoginModule required refreshKrb5Config=true useKeyTab=false principal="alice"; +}; + +bob { + com.sun.security.auth.module.Krb5LoginModule required refreshKrb5Config=true useKeyTab=false storeKey=true principal="bob/service.ws.apache.org"; +}; diff --git a/docs/technical-overview.md b/docs/technical-overview.md new file mode 100644 index 0000000000..2d4e4b1f7d --- /dev/null +++ b/docs/technical-overview.md @@ -0,0 +1,290 @@ +# [WIP] AsyncHttpClient Technical Overview + +#### Disclaimer + +This document is a work in progress. + +## Motivation + +While heavily used (~2.3M downloads across the project in December 2020 alone), AsyncHttpClient (or AHC) does not - at this point in time - have a single guiding document that explains how it works internally. As a maintainer fresh on the scene it was unclear to me ([@TomGranot](https://github.com/TomGranot)) exactly how all the pieces fit together. + +As part of the attempt to educate myself, I figured it would be a good idea to write a technical overview of the project. This document provides an in-depth walkthtough of the library, allowing new potential contributors to "hop on" the coding train as fast as possible. + +Note that this library *is not small*. I expect that in addition to offering a better understanding as to how each piece *works*, writing this document will also allow me to understand which pieces *do not work* as well as expected, and direct me towards things that need a little bit of love. + +PRs are open for anyone who wants to help out. For now - let the fun begin. :) + +**Note: I wrote this guide while using AHC 2.12.2**. + +## The flow of a request + +### Introduction + +AHC is an *Asynchronous* HTTP Client. That means that it needs to have some underlying mechanism of dealing with response data that arrives **asynchronously**. To make that part easier, the creator of the library ([@jfarcand](https://github.com/jfarcand)) built it on top of [Netty](https://netty.io/), which is (by [their own definition](https://netty.io/#content:~:text=Netty%20is%20a%20NIO%20client%20server,as%20TCP%20and%20UDP%20socket%20server.)) "a framework that enables quick and easy development of network applications". + +This article is not a Netty user guide. If you're interested in all Netty has to offer, you should check out the [official user guide](https://netty.io/wiki/user-guide-for-4.x.html). This article is, instead, more of a discussion of using Netty *in the wild* - an overview of what a client library built on top of Netty actually looks like in practice. + +### The code in full + +The best way to explore what the client actually does is, of course, by following the path a request takes. + +Consider the following bit of code, [taken verbatim from one of the simplest tests](https://github.com/AsyncHttpClient/async-http-client/blob/2b12d0ba819e05153fa265b4da7ca900651fd5b3/client/src/test/java/org/asynchttpclient/BasicHttpTest.java#L81-L91) in the library: + +```java +@Test + public void getRootUrl() throws Throwable { + withClient().run(client -> + withServer(server).run(server -> { + String url = server.getHttpUrl(); + server.enqueueOk(); + + Response response = client.executeRequest(get(url), new AsyncCompletionHandlerAdapter()).get(TIMEOUT, SECONDS); + assertEquals(response.getUri().toUrl(), url); + })); + } +``` + +Let's take it bit by bit. + +First: +```java +withClient().run(client -> + withServer(server).run(server -> { + String url = server.getHttpUrl(); + server.enqueueOk(); +``` + +These lines take care of spinning up a server to run the test against, and create an instance of `AsyncHttpClient` called `client`. If you were to drill deeper into the code, you'd notice that the instantiation of `client` can be simplified to (converted from functional to procedural for the sake of the explanation): + +```java +DefaultAsyncHttpClientConfig config = new DefaultAsyncHttpClientConfig.Builder().build.()setMaxRedirects(0); +AsyncHttpClient client = new DefaultAsyncHttpClient(config); +``` + +Once the server and the client have been created, we can now run our test: + +```java +Response response = client.executeRequest(get(url), new AsyncCompletionHandlerAdapter()).get(TIMEOUT, SECONDS); +assertEquals(response.getUri().toUrl(), url); +``` + +The first line executes a `GET` request to the URL of the server that was previously spun up, while the second line is the assertion part of our test. Once the request is completed, the final `Response` object is returned. + +The intersting bits, of course, happen between the lines - and the best way to start the discussion is by considering what happens under the hood when a new client is instantiated. + +### Creating a new AsyncHTTPClient - Configuration + +AHC was designed to be *heavily configurable*. There are many, many different knobs you can turn and buttons you can press in order to get it to behave _just right_. [`DefaultAsyncHttpClientConfig`](https://github.com/AsyncHttpClient/async-http-client/blob/d4f1e5835b81a5e813033ba2a64a07b020c70007/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClientConfig.java) is a utility class that pulls a set of [hard-coded, sane defaults](https://github.com/AsyncHttpClient/async-http-client/blob/d4f1e5835b81a5e813033ba2a64a07b020c70007/client/src/main/resources/org/asynchttpclient/config/ahc-default.properties) to prevent you from having to deal with these mundane configurations - please check it out if you have the time. + +A keen observer will note that the construction of a `DefaultAsyncHttpClient` is done using the [Builder Pattern](https://dzone.com/articles/design-patterns-the-builder-pattern) - this is useful, since many times you want to change a few parameters in a request (`followRedirect`, `requestTimeout`, etc... ) but still rely on the rest of the default configuration properties. The reason I'm mentioning this here is to prevent a bit of busywork on your end the next time you want to create a client - it's much, much easier to work off of the default client and tweak properties than creating your own set of configuration properties. + + The `setMaxRedicrects(0)` from the initialization code above is an example of doign this in practice. Having no redirects following the `GET` requeset is useful in the context of the test, and so we turn a knob to ensure none do. + +### Creating a new AsyncHTTPClient - Client Instantiation + +Coming back to our example - once we've decided on a proper configuration, it's time to create a client. Let's look at the constructor of the [`DefaultAsyncHttpClient`](https://github.com/AsyncHttpClient/async-http-client/blob/a44aac86616f4e8ffe6977dfef0f0aa460e79d07/client/src/main/java/org/asynchttpclient/DefaultAsyncHttpClient.java): + +```java + public DefaultAsyncHttpClient(AsyncHttpClientConfig config) { + + this.config = config; + this.noRequestFilters = config.getRequestFilters().isEmpty(); + allowStopNettyTimer = config.getNettyTimer() == null; + nettyTimer = allowStopNettyTimer ? newNettyTimer(config) : config.getNettyTimer(); + + channelManager = new ChannelManager(config, nettyTimer); + requestSender = new NettyRequestSender(config, channelManager, nettyTimer, new AsyncHttpClientState(closed)); + channelManager.configureBootstraps(requestSender); + + CookieStore cookieStore = config.getCookieStore(); + if (cookieStore != null) { + int cookieStoreCount = config.getCookieStore().incrementAndGet(); + if ( + allowStopNettyTimer // timer is not shared + || cookieStoreCount == 1 // this is the first AHC instance for the shared (user-provided) timer + ) { + nettyTimer.newTimeout(new CookieEvictionTask(config.expiredCookieEvictionDelay(), cookieStore), + config.expiredCookieEvictionDelay(), TimeUnit.MILLISECONDS); + } + } + } +``` + + The constructor actually reveals a lot of the moving parts of AHC, and is worth a proper walkthrough: + +#### `RequestFilters` + + +```java + this.noRequestFilters = config.getRequestFilters().isEmpty(); +``` + +`RequestFilters` are a way to perform some form of computation **before sending a request to a server**. You can read more about request filters [here](#request-filters), but a simple example is the [ThrottleRequestFilter](https://github.com/AsyncHttpClient/async-http-client/blob/758dcf214bf0ec08142ba234a3967d98a3dc60ef/client/src/main/java/org/asynchttpclient/filter/ThrottleRequestFilter.java) that throttles requests by waiting for a response to arrive before executing the next request in line. + +Note that there is another set of filters, `ResponseFilters`, that can perform computations **before processing the first byte of the response**. You can read more about them [here](#response-filters). + +#### `NettyTimer` + +```java +allowStopNettyTimer = config.getNettyTimer() == null; +nettyTimer = allowStopNettyTimer ? newNettyTimer(config) : config.getNettyTimer(); +``` + +`NettyTimer` is actually not a timer, but a *task executor* that waits an arbitrary amount of time before performing the next task. In the case of the code above, it is used for evicting cookies after they expire - but it has many different use cases (request timeouts being a prime example). + +#### `ChannelManager` + +```java +channelManager = new ChannelManager(config, nettyTimer); +``` + +`ChannelManager` requires a [section of its own](#channelmanager), but the bottom line is that one has to do a lot of boilerplate work with Channels when building an HTTP client using Netty. For any given request there's a variable number of channel operations you would have to take, and there's a lot of value in re-using existing channels in clever ways instead of opening new ones. `ChannelManager` is AHC's way of encapsulating at least some of that functionality (for example, [connection pooling](https://en.wikipedia.org/wiki/Connection_pool#:~:text=In%20software%20engineering%2C%20a%20connection,executing%20commands%20on%20a%20database.)) into a single object, instead of having it spread out all over the place. + +There are two similiarly-named constructs in the project, so I'm mentioning them in this + +* `ChannelPool`, as it is [implemented in AHC](https://github.com/AsyncHttpClient/async-http-client/blob/758dcf214bf0ec08142ba234a3967d98a3dc60ef/client/src/main/java/org/asynchttpclient/channel/ChannelPool.java#L21), is an **AHC structure** designed to be a "container" of channels - a place you can add and remove channels from as the need arises. Note that the AHC implementation (that might go as far back as 2012) *predates* the [Netty implementation](https://netty.io/news/2015/05/07/4-0-28-Final.html) introduced in 2015 (see this [AHC user guide entry](https://asynchttpclient.github.io/async-http-client/configuring.html#contentBox:~:text=ConnectionsPoo,-%3C) from 2012 in which `ConnectionPool` is referenced as proof). + + As the [Netty release mentions](https://netty.io/news/2015/05/07/4-0-28-Final.html#main-content:~:text=Many%20of%20our%20users%20needed%20to,used%20Netty%20to%20writing%20a%20client.), connection pooling in the world of Netty-based clients is a valuable feature to have, one that [Jean-Francois](https://github.com/jfarcand) implemented himself instead of waiting for Netty to do so. This might confuse anyone coming to the code a at a later point in time - like me - and I have yet to explore the tradeoffs of stripping away the current implementation and in favor of the upstream one. See [this issue](https://github.com/AsyncHttpClient/async-http-client/issues/1766) for current progress. + +* [`ChannelGroup`](https://netty.io/4.0/api/io/netty/channel/group/ChannelGroup.html) (not to be confused with `ChannelPool`) is a **Netty structure** designed to work with Netty `Channel`s *in bulk*, to reduce the need to perform the same operation on multiple channels sequnetially. + +#### `NettyRequestSender` + +```java +requestSender = new NettyRequestSender(config, channelManager, nettyTimer, new AsyncHttpClientState(closed)); +channelManager.configureBootstraps(requestSender); +``` + +`NettyRequestSender` does the all the heavy lifting required for sending the HTTP request - creating the required `Request` and `Response` objects, making sure `CONNECT` requests are sent before the relevant requests, dealing with proxy servers (in the case of [HTTPS connections](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT)), dispatching DNS hostname resolution requests and more. + + A few extra comments before we move on: + +* When finished with all the work, `NettyRequestSender` will send back a [`ListenableFuture`](https://github.com/AsyncHttpClient/async-http-client/blob/d47c56e7ee80b76a4cffd4770237239cfea0ffd6/client/src/main/java/org/asynchttpclient/ListenableFuture.java#L40). AHC's `ListenableFuture` is an extension of a normal Java `Future` that allows for the addition of "Listeners" - pieces of code that get executed once the computation (the one blocking the `Future` from completing) is finished. It is an example of a *very* common abstraction that exists in many different Java projects - Google's [Guava](https://github.com/google/guava) [has one](https://github.com/google/guava/blob/master/futures/listenablefuture1/src/com/google/common/util/concurrent/ListenableFuture.java), for example, and so does [Spring](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/concurrent/ListenableFuture.html)). + +* Note the invocation of `configureBootstraps` in the second line here. `Bootstrap`s are a Netty concept that make it easy to set up `Channel`s - we'll talk about them a bit later. + +#### `CookieStore` + +```java +CookieStore cookieStore = config.getCookieStore(); + if (cookieStore != null) { + int cookieStoreCount = config.getCookieStore().incrementAndGet(); + if ( + allowStopNettyTimer // timer is not shared + || cookieStoreCount == 1 // this is the first AHC instance for the shared (user-provided) timer + ) { + nettyTimer.newTimeout(new CookieEvictionTask(config.expiredCookieEvictionDelay(), cookieStore), + config.expiredCookieEvictionDelay(), TimeUnit.MILLISECONDS); + } + } +``` + +`CookieStore` is, well, a container for cookies. In this context, it is used to handle the task of cookie eviction (removing cookies whose expiry date has passed). This is, by the way, an example of one of the *many, many features* AHC supports out of the box that might not be evident upon first observation. + +Once the client has been properly configured, it's time to actually execute the request. + +### Executing a request - Before execution + +Take a look at the execution line from the code above again: + +```java +Response response = client.executeRequest(get(url), new AsyncCompletionHandlerAdapter()).get(TIMEOUT, SECONDS); +``` + +Remember that what we have in front of us is an instance of `AsyncHttpClient` called `client` that is configured with an `AsyncHttpClientConfig`, and more specifically an instance of `DefaultAsyncHttpClient` that is configured with `DefaultAsyncHttpClientConfig`. + +The `executeRequest` method is passed two arguments, and returns a `ListenableFuture`. The `Response` created by executing the `get` method on the `ListenableFuture` is the end of the line in our case here, since this test is very simple - there's no response body to parse or any other computations to do in order to assert the test succeeded. The only thing that is required for the correct operation of the code is for the `Response` to come back with the correct URL. + +Let's turn our eyes to the two arguments passed to `executeRequest`, then, since they are the key parts here: + +1. `get(url)` is the functional equivalent of `new RequestBuilder("GET").setUrl(url)`. `RequestBuilder` is in charge of scaffolding an instance of [AHC's `Request` object](https://github.com/AsyncHttpClient/async-http-client/blob/c5eff423ebdd0cddd00bc6fcf17682651a151028/client/src/main/java/org/asynchttpclient/Request.java) and providing it with sane defaults - mostly regarding HTTP headers (`RequestBuilder` does for `Request` what `DefaultAsyncHttpClientConfig.Builder()` does for `DefaultAsyncHttpClient`). + 1. In our case, the `Request` contains no body (it's a simple `GET`). However, if that request was, for example, a `POST` - it could have a payload that would need to be sent to the server via HTTP as well. We'll be talking about `Request` in more detail [here](#working-with-request-bodies), including how to work with request bodies. +2. To fully understand what `AsyncCompletionHandlerAdapter` is, and why it's such a core piece of everything that goes on in AHC, a bit of Netty background is required. Let's take a sidestep for a moment: + +#### Netty `Channel`s and their associated entities ( `ChannelPipeline`s, `ChannelHandler`s and `ChannelAdapter`s) + +Recall that AHC is built on [Netty](https://netty.io/) and its networking abstractions. If you want to dive deeper into the framework you **should** read [Netty in Action](https://www.manning.com/books/netty-in-action) (great book, Norman!), but for the sake of our discussion it's enough to settle on clarifying a few basic terms: + +1. [`Channel`](https://netty.io/4.1/api/io/netty/channel/Channel.html) is Netty's version of a normal Java [`Socket`](https://docs.oracle.com/javase/8/docs/api/java/net/Socket.html), greatly simplified for easier usage. It encapsulates [all that you can know and do](https://netty.io/4.1/api/io/netty/channel/Channel.html#allclasses_navbar_top:~:text=the%20current%20state%20of%20the%20channel,and%20requests%20associated%20with%20the%20channel.) with a regular `Socket`: + + 1. **State** - Is the socket currently open? Is it currently closed? + 2. **I/O Options** - Can we read from it? Can we write to it? + 3. **Configuration** - What is the receive buffer size? What is the connect timeout? + 4. `ChannelPipeline` - A reference to this `Channel`'s `ChannelPipeline`. + +2. Note that operations on a channel, in and of themselves, are **blocking** - that is, any operation that is performed on a channel blocks any other operations from being performed on the channel at any given point in time. This is contrary to the Asynchronous nature Netty purports to support. + + To solve the issue, Netty adds a `ChannelPipeline` to every new `Channel` that is initialised. A `ChannelPipeline` is nothing but a container for `ChannelHandlers`. +3. [`ChannelHandler`](https://netty.io/4.1/api/io/netty/channel/ChannelHandler.html)s encapsulate the application logic of a Netty application. To be more precise, a *chain* of `ChannelHandler`s, each in charge of one or more small pieces of logic that - when taken together - describe the entire data processing that is supposed to take place during the lifetime of the application. + +4. [`ChannelHandlerContext`](https://netty.io/4.0/api/io/netty/channel/ChannelHandlerContext.html) is also worth mentioning here - it's the actual mechanism a `ChannelHandler` uses to talk to the `ChannelPipeline` that encapsulates it. + +5. `ChannelXHandlerAdapter`s are a set of *default* handler implementations - "sugar" that should make the development of application logic easier. `X` can be `Inbound ` (`ChannelInboundHandlerAdapter`), `Oubound` (`ChannelOutboundHandlerAdapter`) or one of many other options Netty provides out of the box. + +#### `ChannelXHandlerAdapter` VS. `AsyncXHandlerAdapter` + +This where it's important to note the difference between `ChannelXHandlerAdapter` (i.e. `ChannelInboundHandlerAdapater`) - which is a **Netty construct** and `AsyncXHandlerAdapter` (i.e. `AsyncCompletionHandlerAdapater`) - which is an **AHC construct**. + +Basically, `ChannelXHandlerAdapter` is a Netty construct that provides a default implementation of a `ChannelHandler`, while `AsyncXHandlerAdapter` is an AHC construct that provides a default implementation of an `AsyncHandler`. + +A `ChannelXHandlerAdapter` has methods that are called when *handler-related* and *channel-related* events occur. When the events "fire", a piece of business logic is carried out in the relevant method, and the operation is then **passed on to the** **next `ChannelHandler` in line.** *The methods return nothing.* + +An `AsyncXHandlerAdapter` works a bit differently. It has methods that are triggered when *some piece of data is available during an asynchronous response processing*. The methods are invoked in a pre-determined order, based on the expected arrival of each piece of data (when the status code arrives, when the headers arrive, etc.). When these pieces of information become availale, a piece of business logic is carried out in the relevant method, and *a [`STATE`](https://github.com/AsyncHttpClient/async-http-client/blob/f61f88e694850818950195379c5ba7efd1cd82ee/client/src/main/java/org/asynchttpclient/AsyncHandler.java#L242-L253) is returned*. This `STATE` enum instructs the current implementation of the `AsyncHandler` (in our case, `AsyncXHandlerAdapater`) whether it should `CONTINUE` or `ABORT` the current processing. + +This is **the core of AHC**: an asynchronous mechanism that encodes - and allows a developer to "hook" into - the various stages of the asynchronous processing of an HTTP response. + +### Executing a request - During execution + +TODO + +### Executing a request - After execution + +TODO + +## Working with Netty channels + +### ChannelManager + +TODO + +## Transforming requests and responses + +TODO + +### Working with Request Bodies + +TODO + +### Request Filters + +TODO + +### Working with Response Bodies + +TODO + +### Response Filters + +TODO + +### Handlers + +TODO + +## Resources + +### Netty + +* https://seeallhearall.blogspot.com/2012/05/netty-tutorial-part-1-introduction-to.html + +### AsyncHttpClient + +TODO + +### HTTP + +TODO + +## Footnotes + +[^1] Some Netty-related definitions borrow heavily from [here](https://seeallhearall.blogspot.com/2012/05/netty-tutorial-part-1-introduction-to.html). diff --git a/example/pom.xml b/example/pom.xml index 075bc60bea..6b96cdaefa 100644 --- a/example/pom.xml +++ b/example/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.5.3-SNAPSHOT + 2.12.4-SNAPSHOT 4.0.0 async-http-client-example @@ -11,6 +11,11 @@ The Async Http Client example. + + + org.asynchttpclient.example + + org.asynchttpclient diff --git a/extras/guava/pom.xml b/extras/guava/pom.xml index 151393daef..12fed4e86f 100644 --- a/extras/guava/pom.xml +++ b/extras/guava/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.5.3-SNAPSHOT + 2.12.4-SNAPSHOT 4.0.0 async-http-client-extras-guava @@ -11,11 +11,15 @@ The Async Http Client Guava Extras. + + org.asynchttpclient.extras.guava + + com.google.guava guava - 14.0.1 + 28.2-jre \ No newline at end of file diff --git a/extras/guava/src/main/java/org/asynchttpclient/extras/guava/RateLimitedThrottleRequestFilter.java b/extras/guava/src/main/java/org/asynchttpclient/extras/guava/RateLimitedThrottleRequestFilter.java index 102b03df86..6d74a08dbe 100644 --- a/extras/guava/src/main/java/org/asynchttpclient/extras/guava/RateLimitedThrottleRequestFilter.java +++ b/extras/guava/src/main/java/org/asynchttpclient/extras/guava/RateLimitedThrottleRequestFilter.java @@ -13,7 +13,7 @@ * {@link ThrottleRequestFilter} by allowing rate limiting per second in addition to the * number of concurrent connections. *

- * The maxWaitMs argument is respected accross both permit acquistions. For + * The maxWaitMs argument is respected across both permit acquisitions. For * example, if 1000 ms is given, and the filter spends 500 ms waiting for a connection, * it will only spend another 500 ms waiting for the rate limiter. */ @@ -44,9 +44,9 @@ public FilterContext filter(FilterContext ctx) throws FilterException } long startOfWait = System.currentTimeMillis(); - attemptConcurrencyPermitAcquistion(ctx); + attemptConcurrencyPermitAcquisition(ctx); - attemptRateLimitedPermitAcquistion(ctx, startOfWait); + attemptRateLimitedPermitAcquisition(ctx, startOfWait); } catch (InterruptedException e) { throw new FilterException(String.format("Interrupted Request %s with AsyncHandler %s", ctx.getRequest(), ctx.getAsyncHandler())); } @@ -56,7 +56,7 @@ public FilterContext filter(FilterContext ctx) throws FilterException .build(); } - private void attemptRateLimitedPermitAcquistion(FilterContext ctx, long startOfWait) throws FilterException { + private void attemptRateLimitedPermitAcquisition(FilterContext ctx, long startOfWait) throws FilterException { long wait = getMillisRemainingInMaxWait(startOfWait); if (!rateLimiter.tryAcquire(wait, TimeUnit.MILLISECONDS)) { @@ -65,7 +65,7 @@ private void attemptRateLimitedPermitAcquistion(FilterContext ctx, long s } } - private void attemptConcurrencyPermitAcquistion(FilterContext ctx) throws InterruptedException, FilterException { + private void attemptConcurrencyPermitAcquisition(FilterContext ctx) throws InterruptedException, FilterException { if (!available.tryAcquire(maxWaitMs, TimeUnit.MILLISECONDS)) { throw new FilterException(String.format("No slot available for processing Request %s with AsyncHandler %s", ctx.getRequest(), ctx.getAsyncHandler())); diff --git a/extras/jdeferred/pom.xml b/extras/jdeferred/pom.xml index aefd9cd662..0d57752e14 100644 --- a/extras/jdeferred/pom.xml +++ b/extras/jdeferred/pom.xml @@ -18,11 +18,16 @@ async-http-client-extras-parent org.asynchttpclient - 2.5.3-SNAPSHOT + 2.12.4-SNAPSHOT async-http-client-extras-jdeferred Asynchronous Http Client JDeferred Extras The Async Http Client jDeffered Extras. + + + org.asynchttpclient.extras.jdeferred + + org.jdeferred diff --git a/extras/pom.xml b/extras/pom.xml index 5443b9ffcc..e0e053f22e 100644 --- a/extras/pom.xml +++ b/extras/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-project - 2.5.3-SNAPSHOT + 2.12.4-SNAPSHOT 4.0.0 async-http-client-extras-parent diff --git a/extras/registry/pom.xml b/extras/registry/pom.xml index 995d6b21f5..1cd8b24e91 100644 --- a/extras/registry/pom.xml +++ b/extras/registry/pom.xml @@ -2,7 +2,7 @@ org.asynchttpclient async-http-client-extras-parent - 2.5.3-SNAPSHOT + 2.12.4-SNAPSHOT 4.0.0 async-http-client-extras-registry @@ -10,4 +10,9 @@ The Async Http Client Registry Extras. + + + org.asynchttpclient.extras.registry + + \ No newline at end of file diff --git a/extras/registry/src/main/java/org/asynchttpclient/extras/registry/AsyncHttpClientFactory.java b/extras/registry/src/main/java/org/asynchttpclient/extras/registry/AsyncHttpClientFactory.java index 1d56b3b96a..5580496976 100644 --- a/extras/registry/src/main/java/org/asynchttpclient/extras/registry/AsyncHttpClientFactory.java +++ b/extras/registry/src/main/java/org/asynchttpclient/extras/registry/AsyncHttpClientFactory.java @@ -33,7 +33,7 @@ * an instance of that class. If there is an exception while reading the * properties file or system property it throws a RuntimeException * AsyncHttpClientImplException. If any of the constructors of the instance - * throws an exception it thows a AsyncHttpClientImplException. By default if + * throws an exception it throws a AsyncHttpClientImplException. By default if * neither the system property or the property file exists then it will return * the default instance of {@link DefaultAsyncHttpClient} */ diff --git a/extras/retrofit2/pom.xml b/extras/retrofit2/pom.xml index 3fe62ce830..f82ceedd32 100644 --- a/extras/retrofit2/pom.xml +++ b/extras/retrofit2/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.5.3-SNAPSHOT + 2.12.4-SNAPSHOT async-http-client-extras-retrofit2 @@ -12,8 +12,9 @@ The Async Http Client Retrofit2 Extras. - 2.4.0 - 1.16.20 + 2.7.2 + 1.18.12 + org.asynchttpclient.extras.retrofit2 diff --git a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java index e8980fddab..d5534a9ce1 100644 --- a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java +++ b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCall.java @@ -17,6 +17,7 @@ import lombok.extern.slf4j.Slf4j; import okhttp3.*; import okio.Buffer; +import okio.Timeout; import org.asynchttpclient.AsyncCompletionHandler; import org.asynchttpclient.AsyncHttpClient; import org.asynchttpclient.RequestBuilder; @@ -29,6 +30,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import java.util.function.Supplier; /** * {@link AsyncHttpClient} Retrofit2 {@link okhttp3.Call} @@ -37,14 +39,9 @@ @Value @Builder(toBuilder = true) @Slf4j -class AsyncHttpClientCall implements Cloneable, okhttp3.Call { - /** - * Default {@link #execute()} timeout in milliseconds (value: {@value}) - * - * @see #execute() - * @see #executeTimeoutMillis - */ - public static final long DEFAULT_EXECUTE_TIMEOUT_MILLIS = 30_000; +public class AsyncHttpClientCall implements Cloneable, okhttp3.Call { + private static final ResponseBody EMPTY_BODY = ResponseBody.create(null, ""); + /** * Tells whether call has been executed. * @@ -52,37 +49,38 @@ class AsyncHttpClientCall implements Cloneable, okhttp3.Call { * @see #isCanceled() */ private final AtomicReference> futureRef = new AtomicReference<>(); + /** - * HttpClient instance. + * {@link AsyncHttpClient} supplier */ @NonNull - AsyncHttpClient httpClient; - /** - * {@link #execute()} response timeout in milliseconds. - */ - @Builder.Default - long executeTimeoutMillis = DEFAULT_EXECUTE_TIMEOUT_MILLIS; + Supplier httpClientSupplier; + /** * Retrofit request. */ @NonNull @Getter(AccessLevel.NONE) Request request; + /** * List of consumers that get called just before actual async-http-client request is being built. */ @Singular("requestCustomizer") List> requestCustomizers; + /** * List of consumers that get called just before actual HTTP request is being fired. */ @Singular("onRequestStart") List> onRequestStart; + /** * List of consumers that get called when HTTP request finishes with an exception. */ @Singular("onRequestFailure") List> onRequestFailure; + /** * List of consumers that get called when HTTP request finishes successfully. */ @@ -128,7 +126,7 @@ public Request request() { @Override public Response execute() throws IOException { try { - return executeHttpRequest().get(getExecuteTimeoutMillis(), TimeUnit.MILLISECONDS); + return executeHttpRequest().get(getRequestTimeoutMillis(), TimeUnit.MILLISECONDS); } catch (ExecutionException e) { throw toIOException(e.getCause()); } catch (Exception e) { @@ -146,7 +144,7 @@ public void enqueue(Callback responseCallback) { @Override public void cancel() { val future = futureRef.get(); - if (future != null) { + if (future != null && !future.isDone()) { if (!future.cancel(true)) { log.warn("Cannot cancel future: {}", future); } @@ -165,6 +163,20 @@ public boolean isCanceled() { return future != null && future.isCancelled(); } + @Override + public Timeout timeout() { + return new Timeout().timeout(getRequestTimeoutMillis(), TimeUnit.MILLISECONDS); + } + + /** + * Returns HTTP request timeout in milliseconds, retrieved from http client configuration. + * + * @return request timeout in milliseconds. + */ + protected long getRequestTimeoutMillis() { + return Math.abs(getHttpClient().getConfig().getRequestTimeout()); + } + @Override public Call clone() { return toBuilder().build(); @@ -228,6 +240,20 @@ public Response onCompleted(org.asynchttpclient.Response response) { return future; } + /** + * Returns HTTP client. + * + * @return http client + * @throws IllegalArgumentException if {@link #httpClientSupplier} returned {@code null}. + */ + protected AsyncHttpClient getHttpClient() { + val httpClient = httpClientSupplier.get(); + if (httpClient == null) { + throw new IllegalStateException("Async HTTP client instance supplier " + httpClientSupplier + " returned null."); + } + return httpClient; + } + /** * Converts async-http-client response to okhttp response. * @@ -254,6 +280,8 @@ private Response toOkhttpResponse(org.asynchttpclient.Response asyncHttpClientRe ? null : MediaType.parse(asyncHttpClientResponse.getContentType()); val okHttpBody = ResponseBody.create(contentType, asyncHttpClientResponse.getResponseBodyAsBytes()); rspBuilder.body(okHttpBody); + } else { + rspBuilder.body(EMPTY_BODY); } return rspBuilder.build(); diff --git a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java index b7c087fd35..0077cd32e3 100644 --- a/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java +++ b/extras/retrofit2/src/main/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactory.java @@ -19,31 +19,35 @@ import java.util.List; import java.util.function.Consumer; +import java.util.function.Supplier; import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCall.runConsumers; /** - * {@link AsyncHttpClient} implementation of Retrofit2 {@link Call.Factory} + * {@link AsyncHttpClient} implementation of Retrofit2 + * {@link Call.Factory}. */ @Value @Builder(toBuilder = true) public class AsyncHttpClientCallFactory implements Call.Factory { /** - * {@link AsyncHttpClient} in use. + * Supplier of {@link AsyncHttpClient}. */ @NonNull - AsyncHttpClient httpClient; + @Getter(AccessLevel.NONE) + Supplier httpClientSupplier; /** * List of {@link Call} builder customizers that are invoked just before creating it. */ @Singular("callCustomizer") + @Getter(AccessLevel.PACKAGE) List> callCustomizers; @Override public Call newCall(Request request) { val callBuilder = AsyncHttpClientCall.builder() - .httpClient(httpClient) + .httpClientSupplier(httpClientSupplier) .request(request); // customize builder before creating a call @@ -52,4 +56,35 @@ public Call newCall(Request request) { // create a call return callBuilder.build(); } -} + + /** + * Returns {@link AsyncHttpClient} from {@link #httpClientSupplier}. + * + * @return http client. + */ + AsyncHttpClient getHttpClient() { + return httpClientSupplier.get(); + } + + /** + * Builder for {@link AsyncHttpClientCallFactory}. + */ + public static class AsyncHttpClientCallFactoryBuilder { + /** + * {@link AsyncHttpClient} supplier that returns http client to be used to execute HTTP requests. + */ + private Supplier httpClientSupplier; + + /** + * Sets concrete http client to be used by the factory to execute HTTP requests. Invocation of this method + * overrides any previous http client supplier set by {@link #httpClientSupplier(Supplier)}! + * + * @param httpClient http client + * @return reference to itself. + * @see #httpClientSupplier(Supplier) + */ + public AsyncHttpClientCallFactoryBuilder httpClient(@NonNull AsyncHttpClient httpClient) { + return httpClientSupplier(() -> httpClient); + } + } +} \ No newline at end of file diff --git a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java index 730ab5d007..4b7605a813 100644 --- a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallFactoryTest.java @@ -14,23 +14,34 @@ import lombok.extern.slf4j.Slf4j; import lombok.val; +import okhttp3.MediaType; import okhttp3.Request; +import okhttp3.RequestBody; import okhttp3.Response; import org.asynchttpclient.AsyncHttpClient; import org.asynchttpclient.RequestBuilder; import org.testng.annotations.Test; +import java.util.Objects; import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; -import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCallTest.REQUEST; import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCallTest.createConsumer; import static org.mockito.Mockito.mock; import static org.testng.Assert.*; @Slf4j public class AsyncHttpClientCallFactoryTest { + private static final MediaType MEDIA_TYPE = MediaType.parse("application/json"); + private static final String JSON_BODY = "{\"foo\": \"bar\"}"; + private static final RequestBody BODY = RequestBody.create(MEDIA_TYPE, JSON_BODY); + private static final String URL = "http://localhost:11000/foo/bar?a=b&c=d"; + private static final Request REQUEST = new Request.Builder() + .post(BODY) + .addHeader("X-Foo", "Bar") + .url(URL) + .build(); @Test void newCallShouldProduceExpectedResult() { // given @@ -109,12 +120,12 @@ void shouldApplyAllConsumersToCallBeingConstructed() { }; Consumer callCustomizer = callBuilder -> - callBuilder - .requestCustomizer(requestCustomizer) - .requestCustomizer(rb -> log.warn("I'm customizing: {}", rb)) - .onRequestSuccess(createConsumer(numRequestSuccess)) - .onRequestFailure(createConsumer(numRequestFailure)) - .onRequestStart(createConsumer(numRequestStart)); + callBuilder + .requestCustomizer(requestCustomizer) + .requestCustomizer(rb -> log.warn("I'm customizing: {}", rb)) + .onRequestSuccess(createConsumer(numRequestSuccess)) + .onRequestFailure(createConsumer(numRequestFailure)) + .onRequestStart(createConsumer(numRequestStart)); // create factory val factory = AsyncHttpClientCallFactory.builder() @@ -151,4 +162,65 @@ void shouldApplyAllConsumersToCallBeingConstructed() { assertNotNull(call.getRequestCustomizers()); assertTrue(call.getRequestCustomizers().size() == 2); } + + @Test(expectedExceptions = NullPointerException.class, + expectedExceptionsMessageRegExp = "httpClientSupplier is marked non-null but is null") + void shouldThrowISEIfHttpClientIsNotDefined() { + // given + val factory = AsyncHttpClientCallFactory.builder() + .build(); + + // when + val httpClient = factory.getHttpClient(); + + // then + assertNull(httpClient); + } + + @Test + void shouldUseHttpClientInstanceIfSupplierIsNotAvailable() { + // given + val httpClient = mock(AsyncHttpClient.class); + + val factory = AsyncHttpClientCallFactory.builder() + .httpClient(httpClient) + .build(); + + // when + val usedHttpClient = factory.getHttpClient(); + + // then + assertTrue(usedHttpClient == httpClient); + + // when + val call = (AsyncHttpClientCall) factory.newCall(REQUEST); + + // then: call should contain correct http client + assertTrue(call.getHttpClient()== httpClient); + } + + @Test + void shouldPreferHttpClientSupplierOverHttpClient() { + // given + val httpClientA = mock(AsyncHttpClient.class); + val httpClientB = mock(AsyncHttpClient.class); + + val factory = AsyncHttpClientCallFactory.builder() + .httpClient(httpClientA) + .httpClientSupplier(() -> httpClientB) + .build(); + + // when + val usedHttpClient = factory.getHttpClient(); + + // then + assertTrue(usedHttpClient == httpClientB); + + // when: try to create new call + val call = (AsyncHttpClientCall) factory.newCall(REQUEST); + + // then: call should contain correct http client + assertNotNull(call); + assertTrue(call.getHttpClient() == httpClientB); + } } diff --git a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java index 90ab4b33e5..e655ed73fc 100644 --- a/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java +++ b/extras/retrofit2/src/test/java/org/asynchttpclient/extras/retrofit/AsyncHttpClientCallTest.java @@ -14,16 +14,18 @@ import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.EmptyHttpHeaders; -import lombok.val; -import okhttp3.MediaType; -import okhttp3.Request; -import okhttp3.RequestBody; +import lombok.*; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; import org.asynchttpclient.AsyncCompletionHandler; import org.asynchttpclient.AsyncHttpClient; +import org.asynchttpclient.AsyncHttpClientConfig; import org.asynchttpclient.BoundRequestBuilder; +import org.asynchttpclient.DefaultAsyncHttpClientConfig; import org.asynchttpclient.Response; import org.mockito.ArgumentCaptor; import org.testng.Assert; +import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -35,16 +37,32 @@ import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; +import java.util.function.Supplier; +import static java.util.concurrent.TimeUnit.SECONDS; import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCall.runConsumer; import static org.asynchttpclient.extras.retrofit.AsyncHttpClientCall.runConsumers; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotEquals; import static org.testng.Assert.assertTrue; +@Slf4j public class AsyncHttpClientCallTest { static final Request REQUEST = new Request.Builder().url("http://www.google.com/").build(); + static final DefaultAsyncHttpClientConfig DEFAULT_AHC_CONFIG = new DefaultAsyncHttpClientConfig.Builder() + .setRequestTimeout(1_000) + .build(); + + private AsyncHttpClient httpClient; + private Supplier httpClientSupplier = () -> httpClient; + + @BeforeMethod + void setup() { + httpClient = mock(AsyncHttpClient.class); + when(httpClient.getConfig()).thenReturn(DEFAULT_AHC_CONFIG); + } @Test(expectedExceptions = NullPointerException.class, dataProvider = "first") void builderShouldThrowInCaseOfMissingProperties(AsyncHttpClientCall.AsyncHttpClientCallBuilder builder) { @@ -53,12 +71,10 @@ void builderShouldThrowInCaseOfMissingProperties(AsyncHttpClientCall.AsyncHttpCl @DataProvider(name = "first") Object[][] dataProviderFirst() { - val httpClient = mock(AsyncHttpClient.class); - return new Object[][]{ {AsyncHttpClientCall.builder()}, {AsyncHttpClientCall.builder().request(REQUEST)}, - {AsyncHttpClientCall.builder().httpClient(httpClient)} + {AsyncHttpClientCall.builder().httpClientSupplier(httpClientSupplier)} }; } @@ -76,7 +92,7 @@ void shouldInvokeConsumersOnEachExecution(Consumer> ha val numRequestCustomizer = new AtomicInteger(); // prepare http client mock - val httpClient = mock(AsyncHttpClient.class); + this.httpClient = mock(AsyncHttpClient.class); val mockRequest = mock(org.asynchttpclient.Request.class); when(mockRequest.getHeaders()).thenReturn(EmptyHttpHeaders.INSTANCE); @@ -93,13 +109,12 @@ void shouldInvokeConsumersOnEachExecution(Consumer> ha // create call instance val call = AsyncHttpClientCall.builder() - .httpClient(httpClient) + .httpClientSupplier(httpClientSupplier) .request(REQUEST) .onRequestStart(e -> numStarted.incrementAndGet()) .onRequestFailure(t -> numFailed.incrementAndGet()) .onRequestSuccess(r -> numOk.incrementAndGet()) .requestCustomizer(rb -> numRequestCustomizer.incrementAndGet()) - .executeTimeoutMillis(1000) .build(); // when @@ -162,7 +177,7 @@ Object[][] dataProviderSecond() { void toIOExceptionShouldProduceExpectedResult(Throwable exception) { // given val call = AsyncHttpClientCall.builder() - .httpClient(mock(AsyncHttpClient.class)) + .httpClientSupplier(httpClientSupplier) .request(REQUEST) .build(); @@ -236,13 +251,12 @@ public void contentTypeHeaderIsPassedInRequest() throws Exception { Request request = requestWithBody(); ArgumentCaptor capture = ArgumentCaptor.forClass(org.asynchttpclient.Request.class); - AsyncHttpClient client = mock(AsyncHttpClient.class); - givenResponseIsProduced(client, aResponse()); + givenResponseIsProduced(httpClient, aResponse()); - whenRequestIsMade(client, request); + whenRequestIsMade(httpClient, request); - verify(client).executeRequest(capture.capture(), any()); + verify(httpClient).executeRequest(capture.capture(), any()); org.asynchttpclient.Request ahcRequest = capture.getValue(); @@ -254,11 +268,9 @@ public void contentTypeHeaderIsPassedInRequest() throws Exception { @Test public void contenTypeIsOptionalInResponse() throws Exception { - AsyncHttpClient client = mock(AsyncHttpClient.class); - - givenResponseIsProduced(client, responseWithBody(null, "test")); + givenResponseIsProduced(httpClient, responseWithBody(null, "test")); - okhttp3.Response response = whenRequestIsMade(client, REQUEST); + okhttp3.Response response = whenRequestIsMade(httpClient, REQUEST); assertEquals(response.code(), 200); assertEquals(response.header("Server"), "nginx"); @@ -268,11 +280,9 @@ public void contenTypeIsOptionalInResponse() throws Exception { @Test public void contentTypeIsProperlyParsedIfPresent() throws Exception { - AsyncHttpClient client = mock(AsyncHttpClient.class); + givenResponseIsProduced(httpClient, responseWithBody("text/plain", "test")); - givenResponseIsProduced(client, responseWithBody("text/plain", "test")); - - okhttp3.Response response = whenRequestIsMade(client, REQUEST); + okhttp3.Response response = whenRequestIsMade(httpClient, REQUEST); assertEquals(response.code(), 200); assertEquals(response.header("Server"), "nginx"); @@ -281,6 +291,73 @@ public void contentTypeIsProperlyParsedIfPresent() throws Exception { } + @Test + public void bodyIsNotNullInResponse() throws Exception { + givenResponseIsProduced(httpClient, responseWithNoBody()); + + okhttp3.Response response = whenRequestIsMade(httpClient, REQUEST); + + assertEquals(response.code(), 200); + assertEquals(response.header("Server"), "nginx"); + assertNotEquals(response.body(), null); + } + + @Test(expectedExceptions = IllegalStateException.class, expectedExceptionsMessageRegExp = ".*returned null.") + void getHttpClientShouldThrowISEIfSupplierReturnsNull() { + // given: + val call = AsyncHttpClientCall.builder() + .httpClientSupplier(() -> null) + .request(requestWithBody()) + .build(); + + // when: should throw ISE + call.getHttpClient(); + } + + @Test + void shouldReturnTimeoutSpecifiedInAHCInstanceConfig() { + // given: + val cfgBuilder = new DefaultAsyncHttpClientConfig.Builder(); + AsyncHttpClientConfig config = null; + + // and: setup call + val call = AsyncHttpClientCall.builder() + .httpClientSupplier(httpClientSupplier) + .request(requestWithBody()) + .build(); + + // when: set read timeout to 5s, req timeout to 6s + config = cfgBuilder.setReadTimeout((int) SECONDS.toMillis(5)) + .setRequestTimeout((int) SECONDS.toMillis(6)) + .build(); + when(httpClient.getConfig()).thenReturn(config); + + // then: expect request timeout + assertEquals(call.getRequestTimeoutMillis(), SECONDS.toMillis(6)); + assertEquals(call.timeout().timeoutNanos(), SECONDS.toNanos(6)); + + // when: set read timeout to 10 seconds, req timeout to 7s + config = cfgBuilder.setReadTimeout((int) SECONDS.toMillis(10)) + .setRequestTimeout((int) SECONDS.toMillis(7)) + .build(); + when(httpClient.getConfig()).thenReturn(config); + + // then: expect request timeout + assertEquals(call.getRequestTimeoutMillis(), SECONDS.toMillis(7)); + assertEquals(call.timeout().timeoutNanos(), SECONDS.toNanos(7)); + + // when: set request timeout to a negative value, just for fun. + config = cfgBuilder.setRequestTimeout(-1000) + .setReadTimeout(2000) + .build(); + + when(httpClient.getConfig()).thenReturn(config); + + // then: expect request timeout, but as positive value + assertEquals(call.getRequestTimeoutMillis(), SECONDS.toMillis(1)); + assertEquals(call.timeout().timeoutNanos(), SECONDS.toNanos(1)); + } + private void givenResponseIsProduced(AsyncHttpClient client, Response response) { when(client.executeRequest(any(org.asynchttpclient.Request.class), any())).thenAnswer(invocation -> { AsyncCompletionHandler handler = invocation.getArgument(1); @@ -290,9 +367,11 @@ private void givenResponseIsProduced(AsyncHttpClient client, Response response) } private okhttp3.Response whenRequestIsMade(AsyncHttpClient client, Request request) throws IOException { - AsyncHttpClientCall call = AsyncHttpClientCall.builder().httpClient(client).request(request).build(); - - return call.execute(); + return AsyncHttpClientCall.builder() + .httpClientSupplier(() -> client) + .request(request) + .build() + .execute(); } private Request requestWithBody() { @@ -323,6 +402,13 @@ private Response responseWithBody(String contentType, String content) { return response; } + private Response responseWithNoBody() { + Response response = aResponse(); + when(response.hasResponseBody()).thenReturn(false); + when(response.getContentType()).thenReturn(null); + return response; + } + private void doThrow(String message) { throw new RuntimeException(message); } diff --git a/extras/rxjava/pom.xml b/extras/rxjava/pom.xml index 9225a76d1d..8cdf60071a 100644 --- a/extras/rxjava/pom.xml +++ b/extras/rxjava/pom.xml @@ -3,11 +3,16 @@ async-http-client-extras-parent org.asynchttpclient - 2.5.3-SNAPSHOT + 2.12.4-SNAPSHOT async-http-client-extras-rxjava Asynchronous Http Client RxJava Extras The Async Http Client RxJava Extras. + + + org.asynchttpclient.extras.rxjava + + io.reactivex diff --git a/extras/rxjava2/pom.xml b/extras/rxjava2/pom.xml index f165dc1725..d0a551c756 100644 --- a/extras/rxjava2/pom.xml +++ b/extras/rxjava2/pom.xml @@ -3,11 +3,16 @@ async-http-client-extras-parent org.asynchttpclient - 2.5.3-SNAPSHOT + 2.12.4-SNAPSHOT async-http-client-extras-rxjava2 Asynchronous Http Client RxJava2 Extras The Async Http Client RxJava2 Extras. + + + org.asynchttpclient.extras.rxjava2 + + io.reactivex.rxjava2 diff --git a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/RxHttpClient.java b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/RxHttpClient.java index bf2fa39167..9b60aed759 100644 --- a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/RxHttpClient.java +++ b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/RxHttpClient.java @@ -57,7 +57,7 @@ default Maybe prepare(Request request) { * @param request the request that is to be executed * @param handlerSupplier supplies the desired {@code AsyncHandler} instances that are used to produce results * @return a {@code Maybe} that executes {@code request} upon subscription and that emits the results produced by - * the supplied handers + * the supplied handlers * @throws NullPointerException if at least one of the parameters is {@code null} */ Maybe prepare(Request request, Supplier> handlerSupplier); diff --git a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridge.java b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridge.java index bf366f8200..0d0fcdd91c 100644 --- a/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridge.java +++ b/extras/rxjava2/src/main/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridge.java @@ -13,6 +13,7 @@ */ package org.asynchttpclient.extras.rxjava2.maybe; +import io.netty.channel.Channel; import io.netty.handler.codec.http.HttpHeaders; import io.reactivex.MaybeEmitter; import io.reactivex.exceptions.CompositeException; @@ -21,10 +22,14 @@ import org.asynchttpclient.HttpResponseBodyPart; import org.asynchttpclient.HttpResponseStatus; import org.asynchttpclient.extras.rxjava2.DisposedException; +import org.asynchttpclient.netty.request.NettyRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.net.ssl.SSLSession; +import java.net.InetSocketAddress; import java.util.Arrays; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import static java.util.Objects.requireNonNull; @@ -89,7 +94,7 @@ public State onTrailingHeadersReceived(HttpHeaders headers) throws Exception { * {@inheritDoc} *

*

- * The value returned by the wrapped {@code AsyncHandler} won't be returned by this method, but emtited via RxJava. + * The value returned by the wrapped {@code AsyncHandler} won't be returned by this method, but emitted via RxJava. *

* * @return always {@code null} @@ -144,6 +149,76 @@ public final void onThrowable(Throwable t) { emitOnError(error); } + @Override + public void onHostnameResolutionAttempt(String name) { + executeUnlessEmitterDisposed(() -> delegate().onHostnameResolutionAttempt(name)); + } + + @Override + public void onHostnameResolutionSuccess(String name, List addresses) { + executeUnlessEmitterDisposed(() -> delegate().onHostnameResolutionSuccess(name, addresses)); + } + + @Override + public void onHostnameResolutionFailure(String name, Throwable cause) { + executeUnlessEmitterDisposed(() -> delegate().onHostnameResolutionFailure(name, cause)); + } + + @Override + public void onTcpConnectAttempt(InetSocketAddress remoteAddress) { + executeUnlessEmitterDisposed(() -> delegate().onTcpConnectAttempt(remoteAddress)); + } + + @Override + public void onTcpConnectSuccess(InetSocketAddress remoteAddress, Channel connection) { + executeUnlessEmitterDisposed(() -> delegate().onTcpConnectSuccess(remoteAddress, connection)); + } + + @Override + public void onTcpConnectFailure(InetSocketAddress remoteAddress, Throwable cause) { + executeUnlessEmitterDisposed(() -> delegate().onTcpConnectFailure(remoteAddress, cause)); + } + + @Override + public void onTlsHandshakeAttempt() { + executeUnlessEmitterDisposed(() -> delegate().onTlsHandshakeAttempt()); + } + + @Override + public void onTlsHandshakeSuccess(SSLSession sslSession) { + executeUnlessEmitterDisposed(() -> delegate().onTlsHandshakeSuccess(sslSession)); + } + + @Override + public void onTlsHandshakeFailure(Throwable cause) { + executeUnlessEmitterDisposed(() -> delegate().onTlsHandshakeFailure(cause)); + } + + @Override + public void onConnectionPoolAttempt() { + executeUnlessEmitterDisposed(() -> delegate().onConnectionPoolAttempt()); + } + + @Override + public void onConnectionPooled(Channel connection) { + executeUnlessEmitterDisposed(() -> delegate().onConnectionPooled(connection)); + } + + @Override + public void onConnectionOffer(Channel connection) { + executeUnlessEmitterDisposed(() -> delegate().onConnectionOffer(connection)); + } + + @Override + public void onRequestSend(NettyRequest request) { + executeUnlessEmitterDisposed(() -> delegate().onRequestSend(request)); + } + + @Override + public void onRetry() { + executeUnlessEmitterDisposed(() -> delegate().onRetry()); + } + /** * Called to indicate that request processing is to be aborted because the linked Rx stream has been disposed. If * the {@link #delegate() delegate} didn't already receive a terminal event, @@ -184,4 +259,12 @@ private void emitOnError(Throwable error) { LOGGER.debug("Not propagating onError after disposal: {}", error.getMessage(), error); } } + + private void executeUnlessEmitterDisposed(Runnable runnable) { + if (emitter.isDisposed()) { + disposed(); + } else { + runnable.run(); + } + } } diff --git a/extras/rxjava2/src/test/java/org/asynchttpclient/extras/rxjava2/DefaultRxHttpClientTest.java b/extras/rxjava2/src/test/java/org/asynchttpclient/extras/rxjava2/DefaultRxHttpClientTest.java index 198f4749f7..953037b8ad 100644 --- a/extras/rxjava2/src/test/java/org/asynchttpclient/extras/rxjava2/DefaultRxHttpClientTest.java +++ b/extras/rxjava2/src/test/java/org/asynchttpclient/extras/rxjava2/DefaultRxHttpClientTest.java @@ -61,7 +61,7 @@ public class DefaultRxHttpClientTest { private ArgumentCaptor> handlerCaptor; @Mock - private ListenableFuture resposeFuture; + private ListenableFuture responseFuture; @InjectMocks private DefaultRxHttpClient underTest; @@ -148,7 +148,7 @@ public void callsSupplierForEachSubscription() { @Test public void cancelsResponseFutureOnDispose() throws Exception { given(handlerSupplier.get()).willReturn(handler); - given(asyncHttpClient.executeRequest(eq(request), any())).willReturn(resposeFuture); + given(asyncHttpClient.executeRequest(eq(request), any())).willReturn(responseFuture); /* when */ underTest.prepare(request, handlerSupplier).subscribe().dispose(); @@ -156,7 +156,7 @@ public void cancelsResponseFutureOnDispose() throws Exception { // then then(asyncHttpClient).should().executeRequest(eq(request), handlerCaptor.capture()); final AsyncHandler bridge = handlerCaptor.getValue(); - then(resposeFuture).should().cancel(true); + then(responseFuture).should().cancel(true); verifyZeroInteractions(handler); assertThat(bridge.onStatusReceived(null), is(AsyncHandler.State.ABORT)); verify(handler).onThrowable(isA(DisposedException.class)); diff --git a/extras/rxjava2/src/test/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridgeTest.java b/extras/rxjava2/src/test/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridgeTest.java index b8a9b3b4e4..5c14778e1c 100644 --- a/extras/rxjava2/src/test/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridgeTest.java +++ b/extras/rxjava2/src/test/java/org/asynchttpclient/extras/rxjava2/maybe/AbstractMaybeAsyncHandlerBridgeTest.java @@ -13,6 +13,7 @@ */ package org.asynchttpclient.extras.rxjava2.maybe; +import io.netty.channel.Channel; import io.netty.handler.codec.http.HttpHeaders; import io.reactivex.MaybeEmitter; import io.reactivex.exceptions.CompositeException; @@ -26,7 +27,10 @@ import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import javax.net.ssl.SSLSession; +import java.net.InetSocketAddress; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.concurrent.Callable; @@ -35,10 +39,6 @@ import static org.mockito.BDDMockito.*; import static org.mockito.Matchers.any; import static org.mockito.Matchers.isA; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.only; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; public class AbstractMaybeAsyncHandlerBridgeTest { @@ -57,6 +57,20 @@ public class AbstractMaybeAsyncHandlerBridgeTest { @Mock private HttpResponseBodyPart bodyPart; + private final String hostname = "service:8080"; + + @Mock + private InetSocketAddress remoteAddress; + + @Mock + private Channel channel; + + @Mock + private SSLSession sslSession; + + @Mock + private Throwable error; + @Captor private ArgumentCaptor throwable; @@ -76,6 +90,20 @@ public T call() throws Exception { }; } + private static Runnable named(String name, Runnable runnable) { + return new Runnable() { + @Override + public String toString() { + return name; + } + + @Override + public void run() { + runnable.run(); + } + }; + } + @BeforeMethod public void initializeTest() { MockitoAnnotations.initMocks(this); @@ -104,10 +132,68 @@ public void forwardsEvents() throws Exception { underTest.onTrailingHeadersReceived(headers); then(delegate).should().onTrailingHeadersReceived(headers); + /* when */ + underTest.onHostnameResolutionAttempt(hostname); + then(delegate).should().onHostnameResolutionAttempt(hostname); + + /* when */ + List remoteAddresses = Collections.singletonList(remoteAddress); + underTest.onHostnameResolutionSuccess(hostname, remoteAddresses); + then(delegate).should().onHostnameResolutionSuccess(hostname, remoteAddresses); + + /* when */ + underTest.onHostnameResolutionFailure(hostname, error); + then(delegate).should().onHostnameResolutionFailure(hostname, error); + + /* when */ + underTest.onTcpConnectAttempt(remoteAddress); + then(delegate).should().onTcpConnectAttempt(remoteAddress); + + /* when */ + underTest.onTcpConnectSuccess(remoteAddress, channel); + then(delegate).should().onTcpConnectSuccess(remoteAddress, channel); + + /* when */ + underTest.onTcpConnectFailure(remoteAddress, error); + then(delegate).should().onTcpConnectFailure(remoteAddress, error); + + /* when */ + underTest.onTlsHandshakeAttempt(); + then(delegate).should().onTlsHandshakeAttempt(); + + /* when */ + underTest.onTlsHandshakeSuccess(sslSession); + then(delegate).should().onTlsHandshakeSuccess(sslSession); + + /* when */ + underTest.onTlsHandshakeFailure(error); + then(delegate).should().onTlsHandshakeFailure(error); + + /* when */ + underTest.onConnectionPoolAttempt(); + then(delegate).should().onConnectionPoolAttempt(); + + /* when */ + underTest.onConnectionPooled(channel); + then(delegate).should().onConnectionPooled(channel); + + /* when */ + underTest.onConnectionOffer(channel); + then(delegate).should().onConnectionOffer(channel); + + /* when */ + underTest.onRequestSend(null); + then(delegate).should().onRequestSend(null); + + /* when */ + underTest.onRetry(); + then(delegate).should().onRetry(); + /* when */ underTest.onCompleted(); then(delegate).should().onCompleted(); then(emitter).should().onSuccess(this); + /* then */ verifyNoMoreInteractions(delegate); } @@ -254,6 +340,42 @@ public void httpEventCallbacksCheckDisposal(Callable httpEve verifyNoMoreInteractions(delegate); } + @DataProvider + public Object[][] variousEvents() { + return new Object[][]{ + {named("onHostnameResolutionAttempt", () -> underTest.onHostnameResolutionAttempt("service:8080"))}, + {named("onHostnameResolutionSuccess", () -> underTest.onHostnameResolutionSuccess("service:8080", + Collections.singletonList(remoteAddress)))}, + {named("onHostnameResolutionFailure", () -> underTest.onHostnameResolutionFailure("service:8080", error))}, + {named("onTcpConnectAttempt", () -> underTest.onTcpConnectAttempt(remoteAddress))}, + {named("onTcpConnectSuccess", () -> underTest.onTcpConnectSuccess(remoteAddress, channel))}, + {named("onTcpConnectFailure", () -> underTest.onTcpConnectFailure(remoteAddress, error))}, + {named("onTlsHandshakeAttempt", () -> underTest.onTlsHandshakeAttempt())}, + {named("onTlsHandshakeSuccess", () -> underTest.onTlsHandshakeSuccess(sslSession))}, + {named("onTlsHandshakeFailure", () -> underTest.onTlsHandshakeFailure(error))}, + {named("onConnectionPoolAttempt", () -> underTest.onConnectionPoolAttempt())}, + {named("onConnectionPooled", () -> underTest.onConnectionPooled(channel))}, + {named("onConnectionOffer", () -> underTest.onConnectionOffer(channel))}, + {named("onRequestSend", () -> underTest.onRequestSend(null))}, + {named("onRetry", () -> underTest.onRetry())}, + }; + } + + @Test(dataProvider = "variousEvents") + public void variousEventCallbacksCheckDisposal(Runnable event) { + given(emitter.isDisposed()).willReturn(true); + + /* when */ + event.run(); + /* then */ + then(delegate).should(only()).onThrowable(isA(DisposedException.class)); + + /* when */ + event.run(); + /* then */ + verifyNoMoreInteractions(delegate); + } + private final class UnderTest extends AbstractMaybeAsyncHandlerBridge { UnderTest() { super(AbstractMaybeAsyncHandlerBridgeTest.this.emitter); diff --git a/extras/simple/pom.xml b/extras/simple/pom.xml index 05c1ac0d1f..94e5134865 100644 --- a/extras/simple/pom.xml +++ b/extras/simple/pom.xml @@ -3,9 +3,14 @@ async-http-client-extras-parent org.asynchttpclient - 2.5.3-SNAPSHOT + 2.12.4-SNAPSHOT async-http-client-extras-simple Asynchronous Http Simple Client The Async Http Simple Client. + + + org.asynchttpclient.extras.simple + + diff --git a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ResumableBodyConsumer.java b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ResumableBodyConsumer.java index 46048fca9e..0978e4f770 100644 --- a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ResumableBodyConsumer.java +++ b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/ResumableBodyConsumer.java @@ -30,7 +30,7 @@ public interface ResumableBodyConsumer extends BodyConsumer { /** * Get the previously transferred bytes, for example the current file size. * - * @return the number of tranferred bytes + * @return the number of transferred bytes * @throws IOException IO exception */ long getTransferredBytes() throws IOException; diff --git a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/SimpleAsyncHttpClient.java b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/SimpleAsyncHttpClient.java index b58658fb57..eb805eed1a 100644 --- a/extras/simple/src/main/java/org/asynchttpclient/extras/simple/SimpleAsyncHttpClient.java +++ b/extras/simple/src/main/java/org/asynchttpclient/extras/simple/SimpleAsyncHttpClient.java @@ -275,7 +275,7 @@ public Future options(BodyConsumer bodyConsumer, ThrowableHandler thro } private RequestBuilder rebuildRequest(Request rb) { - return new RequestBuilder(rb); + return rb.toBuilder(); } private Future execute(RequestBuilder rb, BodyConsumer bodyConsumer, ThrowableHandler throwableHandler) throws IOException { @@ -380,7 +380,7 @@ public interface DerivedBuilder { DerivedBuilder setFormParams(Map> params); - DerivedBuilder setHeaders(Map> headers); + DerivedBuilder setHeaders(Map> headers); DerivedBuilder setHeaders(HttpHeaders headers); @@ -422,7 +422,7 @@ public Builder() { } private Builder(SimpleAsyncHttpClient client) { - this.requestBuilder = new RequestBuilder(client.requestBuilder.build()); + this.requestBuilder = client.requestBuilder.build().toBuilder(); this.defaultThrowableHandler = client.defaultThrowableHandler; this.errorDocumentBehaviour = client.errorDocumentBehaviour; this.enableResumableDownload = client.resumeEnabled; @@ -465,7 +465,7 @@ public Builder setHeaders(HttpHeaders headers) { return this; } - public Builder setHeaders(Map> headers) { + public Builder setHeaders(Map> headers) { requestBuilder.setHeaders(headers); return this; } @@ -505,8 +505,8 @@ public Builder setMaxConnectionsPerHost(int defaultMaxConnectionsPerHost) { return this; } - public Builder setConnectTimeout(int connectTimeuot) { - configBuilder.setConnectTimeout(connectTimeuot); + public Builder setConnectTimeout(int connectTimeout) { + configBuilder.setConnectTimeout(connectTimeout); return this; } diff --git a/extras/typesafeconfig/README.md b/extras/typesafeconfig/README.md index 3078cac2d5..dcc29dc269 100644 --- a/extras/typesafeconfig/README.md +++ b/extras/typesafeconfig/README.md @@ -8,7 +8,7 @@ Download [the latest JAR][2] or grab via [Maven][3]: ```xml org.asynchttpclient - async-http-client-extras-typesafeconfig + async-http-client-extras-typesafe-config latest.version ``` @@ -16,12 +16,12 @@ Download [the latest JAR][2] or grab via [Maven][3]: or [Gradle][3]: ```groovy -compile "org.asynchttpclient:async-http-client-extras-typesafeconfig:latest.version" +compile "org.asynchttpclient:async-http-client-extras-typesafe-config:latest.version" ``` [1]: https://github.com/lightbend/config - [2]: https://search.maven.org/remote_content?g=org.asynchttpclient&a=async-http-client-extras-typesafeconfig&v=LATEST - [3]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.asynchttpclient%22%20a%3A%22async-http-client-extras-typesafeconfig%22 + [2]: https://search.maven.org/remote_content?g=org.asynchttpclient&a=async-http-client-extras-typesafe-config&v=LATEST + [3]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22org.asynchttpclient%22%20a%3A%22async-http-client-extras-typesafe-config%22 [snap]: https://oss.sonatype.org/content/repositories/snapshots/ ## Example usage @@ -31,4 +31,4 @@ compile "org.asynchttpclient:async-http-client-extras-typesafeconfig:latest.vers com.typesafe.config.Config config = ... AsyncHttpClientTypesafeConfig ahcConfig = new AsyncHttpClientTypesafeConfig(config); AsyncHttpClient client = new DefaultAsyncHttpClient(ahcConfig); -``` \ No newline at end of file +``` diff --git a/extras/typesafeconfig/pom.xml b/extras/typesafeconfig/pom.xml index d121e009c5..1908275ff3 100644 --- a/extras/typesafeconfig/pom.xml +++ b/extras/typesafeconfig/pom.xml @@ -4,7 +4,7 @@ async-http-client-extras-parent org.asynchttpclient - 2.5.3-SNAPSHOT + 2.12.4-SNAPSHOT async-http-client-extras-typesafe-config @@ -13,6 +13,7 @@ 1.3.3 + org.asynchttpclient.extras.typesafeconfig diff --git a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java index 8917052611..fa5d87bcf3 100644 --- a/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java +++ b/extras/typesafeconfig/src/main/java/org/asynchttpclient/extras/typesafeconfig/AsyncHttpClientTypesafeConfig.java @@ -69,6 +69,11 @@ public int getMaxConnectionsPerHost() { return getIntegerOpt(MAX_CONNECTIONS_PER_HOST_CONFIG).orElse(defaultMaxConnectionsPerHost()); } + @Override + public int getAcquireFreeChannelTimeout() { + return getIntegerOpt(ACQUIRE_FREE_CHANNEL_TIMEOUT).orElse(defaultAcquireFreeChannelTimeout()); + } + @Override public int getConnectTimeout() { return getIntegerOpt(CONNECTION_TIMEOUT_CONFIG).orElse(defaultConnectTimeout()); @@ -159,6 +164,11 @@ public CookieStore getCookieStore() { return new ThreadSafeCookieStore(); } + @Override + public int expiredCookieEvictionDelay() { + return getIntegerOpt(EXPIRED_COOKIE_EVICTION_DELAY).orElse(defaultExpiredCookieEvictionDelay()); + } + @Override public int getMaxRequestRetry() { return getIntegerOpt(MAX_REQUEST_RETRY_CONFIG).orElse(defaultMaxRequestRetry()); @@ -334,6 +344,16 @@ public Timer getNettyTimer() { return null; } + @Override + public long getHashedWheelTimerTickDuration() { + return getIntegerOpt(HASHED_WHEEL_TIMER_TICK_DURATION).orElse(defaultHashedWheelTimerTickDuration()); + } + + @Override + public int getHashedWheelTimerSize() { + return getIntegerOpt(HASHED_WHEEL_TIMER_SIZE).orElse(defaultHashedWheelTimerSize()); + } + @Override public KeepAliveStrategy getKeepAliveStrategy() { return new DefaultKeepAliveStrategy(); @@ -364,6 +384,11 @@ public boolean isSoReuseAddress() { return getBooleanOpt(SO_REUSE_ADDRESS_CONFIG).orElse(defaultSoReuseAddress()); } + @Override + public boolean isSoKeepAlive() { + return getBooleanOpt(SO_KEEP_ALIVE_CONFIG).orElse(defaultSoKeepAlive()); + } + @Override public int getSoLinger() { return getIntegerOpt(SO_LINGER_CONFIG).orElse(defaultSoLinger()); @@ -407,7 +432,7 @@ private Optional> getListOpt(String key) { private Optional getOpt(Function func, String key) { return config.hasPath(key) - ? Optional.ofNullable(func.apply(key)) - : Optional.empty(); + ? Optional.ofNullable(func.apply(key)) + : Optional.empty(); } } diff --git a/netty-utils/pom.xml b/netty-utils/pom.xml index 79aa56d555..a2e4fdb219 100644 --- a/netty-utils/pom.xml +++ b/netty-utils/pom.xml @@ -2,12 +2,16 @@ org.asynchttpclient async-http-client-project - 2.5.3-SNAPSHOT + 2.12.4-SNAPSHOT 4.0.0 async-http-client-netty-utils Asynchronous Http Client Netty Utils + + org.asynchttpclient.utils + + io.netty diff --git a/netty-utils/src/test/java/org/asynchttpclient/netty/util/ByteBufUtilsTests.java b/netty-utils/src/test/java/org/asynchttpclient/netty/util/ByteBufUtilsTests.java new file mode 100644 index 0000000000..4aaa61c8af --- /dev/null +++ b/netty-utils/src/test/java/org/asynchttpclient/netty/util/ByteBufUtilsTests.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 2019 AsyncHttpClient Project. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at + * http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package org.asynchttpclient.netty.util; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.nio.charset.Charset; +import org.testng.annotations.Test; +import org.testng.Assert; +import org.testng.internal.junit.ArrayAsserts; + +public class ByteBufUtilsTests { + + @Test + public void testByteBuf2BytesEmptyByteBuf() { + ByteBuf buf = Unpooled.buffer(); + + try { + ArrayAsserts.assertArrayEquals(new byte[]{}, + ByteBufUtils.byteBuf2Bytes(buf)); + } finally { + buf.release(); + } + } + + @Test + public void testByteBuf2BytesNotEmptyByteBuf() { + ByteBuf byteBuf = Unpooled.wrappedBuffer(new byte[]{'f', 'o', 'o'}); + + try { + ArrayAsserts.assertArrayEquals(new byte[]{'f', 'o', 'o'}, + ByteBufUtils.byteBuf2Bytes(byteBuf)); + } finally { + byteBuf.release(); + } + } + + @Test + public void testByteBuf2String() { + ByteBuf byteBuf = Unpooled.wrappedBuffer(new byte[]{'f', 'o', 'o'}); + Charset charset = Charset.forName("US-ASCII"); + + try { + Assert.assertEquals( + ByteBufUtils.byteBuf2String(charset, byteBuf), "foo"); + } finally { + byteBuf.release(); + } + } + + @Test + public void testByteBuf2StringWithByteBufArray() { + ByteBuf byteBuf1 = Unpooled.wrappedBuffer(new byte[]{'f'}); + ByteBuf byteBuf2 = Unpooled.wrappedBuffer(new byte[]{'o', 'o'}); + + try { + Assert.assertEquals(ByteBufUtils.byteBuf2String( + Charset.forName("ISO-8859-1"), byteBuf1, byteBuf2), "foo"); + } finally { + byteBuf1.release(); + byteBuf2.release(); + } + } + + @Test + public void testByteBuf2Chars() { + ByteBuf byteBuf1 = Unpooled.wrappedBuffer(new byte[]{}); + ByteBuf byteBuf2 = Unpooled.wrappedBuffer(new byte[]{'o'}); + + try { + ArrayAsserts.assertArrayEquals(new char[]{}, ByteBufUtils + .byteBuf2Chars(Charset.forName("US-ASCII"), byteBuf1)); + ArrayAsserts.assertArrayEquals(new char[]{}, ByteBufUtils + .byteBuf2Chars(Charset.forName("ISO-8859-1"), byteBuf1)); + ArrayAsserts.assertArrayEquals(new char[]{'o'}, ByteBufUtils + .byteBuf2Chars(Charset.forName("ISO-8859-1"), byteBuf2)); + } finally { + byteBuf1.release(); + byteBuf2.release(); + } + } + + @Test + public void testByteBuf2CharsWithByteBufArray() { + ByteBuf byteBuf1 = Unpooled.wrappedBuffer(new byte[]{'f', 'o'}); + ByteBuf byteBuf2 = Unpooled.wrappedBuffer(new byte[]{'%', '*'}); + + try { + ArrayAsserts.assertArrayEquals(new char[]{'f', 'o', '%', '*'}, + ByteBufUtils.byteBuf2Chars(Charset.forName("US-ASCII"), + byteBuf1, byteBuf2)); + ArrayAsserts.assertArrayEquals(new char[]{'f', 'o', '%', '*'}, + ByteBufUtils.byteBuf2Chars(Charset.forName("ISO-8859-1"), + byteBuf1, byteBuf2)); + } finally { + byteBuf1.release(); + byteBuf2.release(); + } + } + + @Test + public void testByteBuf2CharsWithEmptyByteBufArray() { + ByteBuf byteBuf1 = Unpooled.wrappedBuffer(new byte[]{}); + ByteBuf byteBuf2 = Unpooled.wrappedBuffer(new byte[]{'o'}); + + try { + ArrayAsserts.assertArrayEquals(new char[]{'o'}, ByteBufUtils + .byteBuf2Chars(Charset.forName("ISO-8859-1"), + byteBuf1, byteBuf2)); + } finally { + byteBuf1.release(); + byteBuf2.release(); + } + } +} diff --git a/pom.xml b/pom.xml index ce6249110e..ca2c7efb9a 100644 --- a/pom.xml +++ b/pom.xml @@ -1,31 +1,58 @@ - - org.sonatype.oss - oss-parent - 9 - 4.0.0 + org.asynchttpclient async-http-client-project - Asynchronous Http Client Project - 2.5.3-SNAPSHOT + 2.12.4-SNAPSHOT pom + + Asynchronous Http Client Project The Async Http Client (AHC) library's purpose is to allow Java applications to easily execute HTTP requests and asynchronously process the response. http://github.com/AsyncHttpClient/async-http-client + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + + slandelle + Stephane Landelle + slandelle@gatling.io + + + - https://github.com/AsyncHttpClient/async-http-client scm:git:git@github.com:AsyncHttpClient/async-http-client.git scm:git:git@github.com:AsyncHttpClient/async-http-client.git + https://github.com/AsyncHttpClient/async-http-client/tree/master + HEAD + + + + sonatype-nexus-staging + https://oss.sonatype.org/content/repositories/snapshots + + + sonatype-nexus-staging + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + - jira - https://issues.sonatype.org/browse/AHC + github + https://github.com/AsyncHttpClient/async-http-client/issues + asynchttpclient @@ -36,23 +63,6 @@ - - 3.0.0 - - - - slandelle - Stephane Landelle - slandelle@gatling.io - - - - - Apache License 2.0 - http://www.apache.org/licenses/LICENSE-2.0.html - repo - - @@ -65,20 +75,28 @@ org.apache.maven.wagon wagon-ssh-external - 1.0-beta-6 + 3.3.4 org.apache.maven.scm maven-scm-provider-gitexe - 1.6 + 1.11.2 org.apache.maven.scm maven-scm-manager-plexus - 1.6 + 1.11.2 install + + + + maven-release-plugin + 2.5.3 + + + maven-compiler-plugin @@ -136,11 +154,23 @@ maven-jar-plugin - 3.0.2 + 3.2.0 + + + default-jar + + + + ${javaModuleName} + + + + + maven-source-plugin - 3.0.1 + 3.2.1 attach-sources @@ -151,15 +181,46 @@ + + org.apache.felix + maven-bundle-plugin + 3.0.1 + true + + META-INF + + $(replace;$(project.version);-SNAPSHOT;.$(tstamp;yyyyMMdd-HHmm)) + The AsyncHttpClient Project + javax.activation;version="[1.1,2)", io.netty.channel.kqueue;resolution:=optional, io.netty.channel.epoll;resolution:=optional, * + + + + + osgi-bundle + package + + bundle + + + + + + org.apache.maven.plugins + maven-javadoc-plugin + 3.1.1 + + none + + + + attach-javadocs + + jar + + + + - - - - maven-javadoc-plugin - 2.10.4 - - - @@ -174,6 +235,7 @@ maven-gpg-plugin + 1.6 sign-artifacts @@ -194,20 +256,9 @@ - - - sonatype-nexus-staging - Sonatype Release - http://oss.sonatype.org/service/local/staging/deploy/maven2 - - - - sonatype-nexus-snapshots - sonatype-nexus-snapshots - ${distMgmtSnapshotsUrl} - - + + bom netty-utils client extras @@ -267,6 +318,13 @@ ${netty.version} true + + io.netty + netty-transport-native-kqueue + osx-x86_64 + ${netty.version} + true + org.reactivestreams reactive-streams @@ -292,6 +350,12 @@ rxjava ${rxjava2.version} + + org.apache.kerby + kerb-simplekdc + ${kerby.version} + test + @@ -302,7 +366,7 @@ com.sun.activation - javax.activation + jakarta.activation ${activation.version} @@ -392,31 +456,32 @@ org.hamcrest - java-hamcrest + hamcrest ${hamcrest.version} test - http://oss.sonatype.org/content/repositories/snapshots + UTF-8 true 1.8 1.8 - 4.1.27.Final - 1.7.25 - 1.0.2 - 1.2.0 - 2.0.0 + 4.1.60.Final + 1.7.30 + 1.0.3 + 1.2.2 + 2.0.4 1.3.8 - 2.1.16 + 2.2.19 1.2.3 - 6.13.1 - 9.4.11.v20180605 - 9.0.10 + 7.1.0 + 9.4.18.v20190429 + 9.0.31 2.6 1.3.3 1.2.2 - 2.19.0 - 2.0.0.0 + 3.4.6 + 2.2 + 2.0.0