From c43c3b9f67e6b46e9da7e221501ffc21f3f2362a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Wed, 28 Feb 2018 12:23:52 +0100 Subject: [PATCH] large refactoring, new transport layer, update to Netty 4.1.22 --- .gitignore | 1 + build.gradle | 55 +- gradle.properties | 12 +- gradle/sonarqube.gradle | 8 +- gradle/wrapper/gradle-wrapper.jar | Bin 54783 -> 54334 bytes gradle/wrapper/gradle-wrapper.properties | 4 +- gradlew | 6 +- src/docs/asciidoclet/overview.adoc | 2 +- .../org/xbib/netty/http/client/Client.java | 218 ++++++++ .../netty/http/client/ClientAuthMode.java | 8 + .../xbib/netty/http/client/ClientBuilder.java | 181 +++++++ .../xbib/netty/http/client/ClientConfig.java | 410 +++++++++++++++ .../xbib/netty/http/client/HttpAddress.java | 113 +++++ .../xbib/netty/http/client/HttpClient.java | 474 ------------------ .../netty/http/client/HttpClientBuilder.java | 333 ------------ .../http/client/HttpClientChannelContext.java | 193 ------- .../HttpClientChannelContextDefaults.java | 153 ------ .../http/client/HttpClientRequestBuilder.java | 450 ----------------- .../netty/http/client/HttpRequestBuilder.java | 97 ---- .../netty/http/client/HttpRequestContext.java | 310 ------------ .../http/client/HttpRequestDefaults.java | 42 -- .../netty/http/client/HttpRequestFuture.java | 20 - .../org/xbib/netty/http/client/Request.java | 216 ++++++++ .../netty/http/client/RequestBuilder.java | 329 ++++++++++++ ...ttpClientUserAgent.java => UserAgent.java} | 27 +- .../client/handler/Http2EventHandler.java | 410 --------------- .../handler/Http2NegotiationHandler.java | 65 --- .../client/handler/Http2ResponseHandler.java | 149 ------ .../handler/HttpClientChannelInitializer.java | 235 --------- .../http/client/handler/HttpHandler.java | 134 ----- .../client/handler/TrafficLoggingHandler.java | 25 +- .../client/handler/UpgradeRequestHandler.java | 71 --- .../http/client/handler/UserEventLogger.java | 20 +- .../handler/http1/HttpChannelInitializer.java | 92 ++++ .../handler/http1/HttpResponseHandler.java | 26 + .../http2/Http2ChannelInitializer.java | 112 +++++ .../handler/http2/Http2ResponseHandler.java | 41 ++ .../handler/http2/Http2SettingsHandler.java | 18 + .../HttpClientChannelPoolHandler.java | 76 --- .../internal/HttpClientChannelPoolMap.java | 71 --- .../internal/HttpClientThreadFactory.java | 33 -- .../http/client/internal/package-info.java | 4 - .../http/client/listener/CookieListener.java | 2 - .../client/listener/ExceptionListener.java | 21 - .../client/listener/HttpHeadersListener.java | 17 - .../client/listener/HttpPushListener.java | 19 - .../client/listener/HttpResponseListener.java | 17 - .../netty/http/client/rest/RestClient.java | 57 +++ .../http/client/transport/BaseTransport.java | 330 ++++++++++++ .../http/client/transport/Http2Transport.java | 166 ++++++ .../http/client/transport/HttpTransport.java | 135 +++++ .../http/client/transport/Transport.java | 78 +++ .../http/client/transport/package-info.java | 4 + .../http/client/util/AbstractFuture.java | 353 ------------- .../http/client/util/ClientAuthMode.java | 23 - .../http/client/util/InetAddressKey.java | 76 --- .../http/client/util/LimitedHashSet.java | 54 -- .../netty/http/client/util/NetworkClass.java | 15 - .../client/util/NetworkProtocolVersion.java | 15 - .../netty/http/client/util/NetworkUtils.java | 15 - .../netty/http/client/test/AkamaiTest.java | 84 ++-- .../netty/http/client/test/ClientTest.java | 184 +++++++ .../client/test/CompletableFutureTest.java | 46 ++ .../http/client/test/ElasticsearchTest.java | 173 +++---- .../netty/http/client/test/ExceptionTest.java | 76 --- .../netty/http/client/test/GoogleTest.java | 136 ----- .../client/test/Http2FrameAdapterTest.java | 171 ------- .../http/client/test/Http2PushioTest.java | 52 -- .../netty/http/client/test/HttpBinTest.java | 61 +-- .../netty/http/client/test/IndexHbzTest.java | 186 ------- .../netty/http/client/test/LoggingBase.java | 26 + .../xbib/netty/http/client/test/URITest.java | 17 +- .../netty/http/client/test/WebtideTest.java | 60 --- .../xbib/netty/http/client/test/XbibTest.java | 217 ++++---- .../http/client/test/rest/RestClientTest.java | 18 + .../client/test/simple/Http2FramesTest.java | 120 +++++ .../client/test/simple/SimpleHttp1Test.java | 324 ++++++++++++ .../client/test/simple/SimpleHttp2Test.java | 389 ++++++++++++++ 78 files changed, 3921 insertions(+), 5060 deletions(-) create mode 100644 src/main/java/org/xbib/netty/http/client/Client.java create mode 100644 src/main/java/org/xbib/netty/http/client/ClientAuthMode.java create mode 100644 src/main/java/org/xbib/netty/http/client/ClientBuilder.java create mode 100644 src/main/java/org/xbib/netty/http/client/ClientConfig.java create mode 100644 src/main/java/org/xbib/netty/http/client/HttpAddress.java delete mode 100755 src/main/java/org/xbib/netty/http/client/HttpClient.java delete mode 100644 src/main/java/org/xbib/netty/http/client/HttpClientBuilder.java delete mode 100644 src/main/java/org/xbib/netty/http/client/HttpClientChannelContext.java delete mode 100644 src/main/java/org/xbib/netty/http/client/HttpClientChannelContextDefaults.java delete mode 100644 src/main/java/org/xbib/netty/http/client/HttpClientRequestBuilder.java delete mode 100644 src/main/java/org/xbib/netty/http/client/HttpRequestBuilder.java delete mode 100755 src/main/java/org/xbib/netty/http/client/HttpRequestContext.java delete mode 100644 src/main/java/org/xbib/netty/http/client/HttpRequestDefaults.java delete mode 100644 src/main/java/org/xbib/netty/http/client/HttpRequestFuture.java create mode 100644 src/main/java/org/xbib/netty/http/client/Request.java create mode 100644 src/main/java/org/xbib/netty/http/client/RequestBuilder.java rename src/main/java/org/xbib/netty/http/client/{internal/HttpClientUserAgent.java => UserAgent.java} (50%) delete mode 100644 src/main/java/org/xbib/netty/http/client/handler/Http2EventHandler.java delete mode 100644 src/main/java/org/xbib/netty/http/client/handler/Http2NegotiationHandler.java delete mode 100644 src/main/java/org/xbib/netty/http/client/handler/Http2ResponseHandler.java delete mode 100644 src/main/java/org/xbib/netty/http/client/handler/HttpClientChannelInitializer.java delete mode 100755 src/main/java/org/xbib/netty/http/client/handler/HttpHandler.java delete mode 100644 src/main/java/org/xbib/netty/http/client/handler/UpgradeRequestHandler.java create mode 100644 src/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java create mode 100644 src/main/java/org/xbib/netty/http/client/handler/http1/HttpResponseHandler.java create mode 100644 src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java create mode 100644 src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java create mode 100644 src/main/java/org/xbib/netty/http/client/handler/http2/Http2SettingsHandler.java delete mode 100644 src/main/java/org/xbib/netty/http/client/internal/HttpClientChannelPoolHandler.java delete mode 100644 src/main/java/org/xbib/netty/http/client/internal/HttpClientChannelPoolMap.java delete mode 100644 src/main/java/org/xbib/netty/http/client/internal/HttpClientThreadFactory.java delete mode 100644 src/main/java/org/xbib/netty/http/client/internal/package-info.java create mode 100644 src/main/java/org/xbib/netty/http/client/rest/RestClient.java create mode 100644 src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java create mode 100644 src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java create mode 100644 src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java create mode 100644 src/main/java/org/xbib/netty/http/client/transport/Transport.java create mode 100644 src/main/java/org/xbib/netty/http/client/transport/package-info.java delete mode 100644 src/main/java/org/xbib/netty/http/client/util/AbstractFuture.java delete mode 100644 src/main/java/org/xbib/netty/http/client/util/ClientAuthMode.java delete mode 100644 src/main/java/org/xbib/netty/http/client/util/InetAddressKey.java delete mode 100644 src/main/java/org/xbib/netty/http/client/util/LimitedHashSet.java create mode 100644 src/test/java/org/xbib/netty/http/client/test/ClientTest.java create mode 100644 src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java delete mode 100644 src/test/java/org/xbib/netty/http/client/test/ExceptionTest.java delete mode 100644 src/test/java/org/xbib/netty/http/client/test/GoogleTest.java delete mode 100644 src/test/java/org/xbib/netty/http/client/test/Http2FrameAdapterTest.java delete mode 100644 src/test/java/org/xbib/netty/http/client/test/Http2PushioTest.java delete mode 100644 src/test/java/org/xbib/netty/http/client/test/IndexHbzTest.java create mode 100644 src/test/java/org/xbib/netty/http/client/test/LoggingBase.java delete mode 100644 src/test/java/org/xbib/netty/http/client/test/WebtideTest.java create mode 100644 src/test/java/org/xbib/netty/http/client/test/rest/RestClientTest.java create mode 100644 src/test/java/org/xbib/netty/http/client/test/simple/Http2FramesTest.java create mode 100644 src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp1Test.java create mode 100644 src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp2Test.java diff --git a/.gitignore b/.gitignore index 644a0f3..57262d8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ /.project /.gradle /build +/out *~ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 75e2208..b18f2a1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,15 @@ +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter plugins { - id "org.sonarqube" version "2.2" - id "org.xbib.gradle.plugin.asciidoctor" version "1.5.4.1.0" - id "io.codearte.nexus-staging" version "0.7.0" + id "org.sonarqube" version "2.6.1" + id "io.codearte.nexus-staging" version "0.11.0" + id "org.xbib.gradle.plugin.asciidoctor" version "1.6.0.0" } -printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" + +printf "Date: %s\nHost: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGradle: %s Groovy: %s Java: %s\n" + "Build: group: ${project.group} name: ${project.name} version: ${project.version}\n", + ZonedDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME), InetAddress.getLocalHost(), System.getProperty("os.name"), System.getProperty("os.arch"), @@ -15,24 +18,15 @@ printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" + System.getProperty("java.vm.version"), System.getProperty("java.vm.vendor"), System.getProperty("java.vm.name"), - GroovySystem.getVersion(), - gradle.gradleVersion + gradle.gradleVersion, GroovySystem.getVersion(), JavaVersion.current() apply plugin: 'java' apply plugin: 'maven' apply plugin: 'signing' -apply plugin: 'findbugs' -apply plugin: 'pmd' -apply plugin: 'checkstyle' -apply plugin: "jacoco" -apply plugin: 'org.xbib.gradle.plugin.asciidoctor' apply plugin: "io.codearte.nexus-staging" +apply plugin: 'org.xbib.gradle.plugin.asciidoctor' -repositories { - mavenCentral() -} - configurations { alpnagent asciidoclet @@ -43,9 +37,11 @@ dependencies { compile "io.netty:netty-codec-http2:${project.property('netty.version')}" compile "io.netty:netty-handler-proxy:${project.property('netty.version')}" compile "io.netty:netty-tcnative-boringssl-static:${project.property('tcnative.version')}" - alpnagent "org.mortbay.jetty.alpn:jetty-alpn-agent:${project.property('alpnagent.version')}" + compile "org.xbib:net-url:${project.property('xbib-net-url.version')}" testCompile "junit:junit:${project.property('junit.version')}" - asciidoclet "org.asciidoctor:asciidoclet:${project.property('asciidoclet.version')}" + testCompile "com.fasterxml.jackson.core:jackson-databind:${project.property('jackson.version')}" + alpnagent "org.mortbay.jetty.alpn:jetty-alpn-agent:${project.property('alpnagent.version')}" + asciidoclet "org.xbib:asciidoclet:${project.property('asciidoclet.version')}" wagon "org.apache.maven.wagon:wagon-ssh:${project.property('wagon.version')}" } @@ -54,7 +50,7 @@ targetCompatibility = JavaVersion.VERSION_1_8 [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:all" + options.compilerArgs << "-Xlint:all,-serial" } jar { @@ -64,7 +60,9 @@ jar { } test { - jvmArgs "-javaagent:" + configurations.alpnagent.asPath + if (JavaVersion.current() == JavaVersion.VERSION_1_8) { + jvmArgs "-javaagent:" + configurations.alpnagent.asPath + } testLogging { showStandardStreams = false exceptionFormat = 'full' @@ -72,18 +70,20 @@ test { } asciidoctor { - backends 'html5' - separateOutputDirs = false - attributes 'source-highlighter': 'coderay', - toc : '', - idprefix : '', - idseparator : '-', - stylesheet: "${projectDir}/src/docs/asciidoc/css/foundation.css" + attributes toc: 'left', + doctype: 'book', + icons: 'font', + encoding: 'utf-8', + sectlink: true, + sectanchors: true, + linkattrs: true, + imagesdir: 'img', + 'source-highlighter': 'coderay' } javadoc { options.docletpath = configurations.asciidoclet.files.asType(List) - options.doclet = 'org.asciidoctor.Asciidoclet' + options.doclet = "org.xbib.asciidoclet.Asciidoclet" options.overview = "src/docs/asciidoclet/overview.adoc" options.addStringOption "-base-dir", "${projectDir}" options.addStringOption "-attribute", @@ -117,4 +117,3 @@ if (project.hasProperty('signing.keyId')) { apply from: 'gradle/ext.gradle' apply from: 'gradle/publish.gradle' -apply from: 'gradle/sonarqube.gradle' diff --git a/gradle.properties b/gradle.properties index c5711b6..35a0bd4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,12 @@ group = org.xbib name = netty-http-client -version = 4.1.11.4 +version = 4.1.22.0 -netty.version = 4.1.11.Final +netty.version = 4.1.22.Final tcnative.version = 2.0.1.Final -alpnagent.version = 2.0.6 +xbib-net-url.version = 1.1.0 +alpnagent.version = 2.0.7 junit.version = 4.12 -asciidoclet.version = 1.5.4 -wagon.version = 2.12 +jackson.version = 2.8.11.1 +asciidoclet.version = 1.6.0.0 +wagon.version = 3.0.0 diff --git a/gradle/sonarqube.gradle b/gradle/sonarqube.gradle index ba85ed2..3985a4f 100644 --- a/gradle/sonarqube.gradle +++ b/gradle/sonarqube.gradle @@ -22,10 +22,8 @@ tasks.withType(Checkstyle) { jacocoTestReport { reports { - xml.enabled true - csv.enabled false - xml.destination "${buildDir}/reports/jacoco-xml" - html.destination "${buildDir}/reports/jacoco-html" + xml.enabled = true + csv.enabled = false } } @@ -33,7 +31,7 @@ sonarqube { properties { property "sonar.projectName", "${project.group} ${project.name}" property "sonar.sourceEncoding", "UTF-8" - property "sonar.tests", "src/integration-test/java" + property "sonar.tests", "src/test/java" property "sonar.scm.provider", "git" property "sonar.java.coveragePlugin", "jacoco" property "sonar.junit.reportsPath", "build/test-results/test/" diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 0e966062b19a6012c0751a2dc549a06dc8024dee..8252d7e25c5b548b131082159ee03249a5d36511 100644 GIT binary patch delta 30489 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 U`SI^%+Y6jppoJXi{io^w0A6AIZvX%Q delta 30930 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 socketChannelClass; + + private final Bootstrap bootstrap; + + private final HttpResponseHandler httpResponseHandler; + + private final Http2SettingsHandler http2SettingsHandler; + + private final Http2ResponseHandler http2ResponseHandler; + + private final List transports; + + private TransportListener transportListener; + + public Client() { + this(new ClientConfig()); + } + + public Client(ClientConfig clientConfig) { + this(clientConfig, null, null, null); + } + + public Client(ClientConfig clientConfig, ByteBufAllocator byteBufAllocator, + EventLoopGroup eventLoopGroup, Class socketChannelClass) { + Objects.requireNonNull(clientConfig); + this.clientConfig = clientConfig; + this.byteBufAllocator = byteBufAllocator != null ? + byteBufAllocator : PooledByteBufAllocator.DEFAULT; + this.eventLoopGroup = eventLoopGroup != null ? + eventLoopGroup : new NioEventLoopGroup(clientConfig.getThreadCount(), httpClientThreadFactory); + this.socketChannelClass = socketChannelClass != null ? + socketChannelClass : NioSocketChannel.class; + this.bootstrap = new Bootstrap() + .group(this.eventLoopGroup) + .channel(this.socketChannelClass) + .option(ChannelOption.TCP_NODELAY, clientConfig.isTcpNodelay()) + .option(ChannelOption.SO_KEEPALIVE, clientConfig.isKeepAlive()) + .option(ChannelOption.SO_REUSEADDR, clientConfig.isReuseAddr()) + .option(ChannelOption.SO_SNDBUF, clientConfig.getTcpSendBufferSize()) + .option(ChannelOption.SO_RCVBUF, clientConfig.getTcpReceiveBufferSize()) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, clientConfig.getConnectTimeoutMillis()) + .option(ChannelOption.ALLOCATOR, byteBufAllocator); + this.httpResponseHandler = new HttpResponseHandler(); + this.http2SettingsHandler = new Http2SettingsHandler(); + this.http2ResponseHandler = new Http2ResponseHandler(); + this.transports = new CopyOnWriteArrayList<>(); + } + + public static ClientBuilder builder() { + return new ClientBuilder(); + } + + public void setTransportListener(TransportListener transportListener) { + this.transportListener = transportListener; + } + + public void logDiagnostics(Level level) { + logger.log(level, () -> "OpenSSL available: " + OpenSsl.isAvailable() + + " OpenSSL ALPN support: " + OpenSsl.isAlpnSupported() + + " Local host name: " + NetworkUtils.getLocalHostName("localhost")); + logger.log(level, NetworkUtils::displayNetworkInterfaces); + } + + public int getTimeout() { + return clientConfig.getReadTimeoutMillis(); + } + + public Transport newTransport(HttpAddress httpAddress) { + Transport transport; + if (httpAddress.getVersion().majorVersion() < 2) { + transport = new HttpTransport(this, httpAddress); + } else { + transport = new Http2Transport(this, httpAddress); + } + if (transportListener != null) { + transportListener.onOpen(transport); + } + transports.add(transport); + return transport; + } + + public Channel newChannel(HttpAddress httpAddress) throws InterruptedException { + HttpVersion httpVersion = httpAddress.getVersion(); + ChannelInitializer initializer; + if (httpVersion.majorVersion() < 2) { + initializer = new HttpChannelInitializer(clientConfig, httpAddress, httpResponseHandler); + } else { + initializer = new Http2ChannelInitializer(clientConfig, httpAddress, http2SettingsHandler, http2ResponseHandler); + } + return bootstrap.handler(initializer) + .connect(httpAddress.getInetSocketAddress()).sync().await().channel(); + } + + /** + * For following redirects by a chain of transports. + * @param transport the previous transport + * @param request the new request for continuing the request. + */ + public void continuation(Transport transport, Request request) { + Transport nextTransport = newTransport(HttpAddress.of(request)); + nextTransport.setResponseListener(transport.getResponseListener()); + nextTransport.setExceptionListener(transport.getExceptionListener()); + nextTransport.setHeadersListener(transport.getHeadersListener()); + nextTransport.setCookieListener(transport.getCookieListener()); + nextTransport.setPushListener(transport.getPushListener()); + nextTransport.setCookieBox(transport.getCookieBox()); + nextTransport.execute(request); + nextTransport.get(); + close(nextTransport); + } + + public Transport execute(Request request) { + Transport nextTransport = newTransport(HttpAddress.of(request)); + nextTransport.execute(request); + return nextTransport; + } + + public CompletableFuture execute(Request request, + Function supplier) { + return newTransport(HttpAddress.of(request)).execute(request, supplier); + } + + public Transport prepareRequest(Request request) { + return newTransport(HttpAddress.of(request)); + } + + public void close(Transport transport) { + if (transportListener != null) { + transportListener.onClose(transport); + } + transport.close(); + transports.remove(transport); + } + + public void close() { + for (Transport transport : transports) { + close(transport); + } + } + + public void shutdown() { + eventLoopGroup.shutdownGracefully(); + } + + public void shutdownGracefully() { + close(); + shutdown(); + } + + public interface TransportListener { + + void onOpen(Transport transport); + + void onClose(Transport transport); + } + + static class HttpClientThreadFactory implements ThreadFactory { + + private int number = 0; + + @Override + public Thread newThread(Runnable runnable) { + Thread thread = new Thread(runnable, "org-xbib-netty-http-client-pool-" + (number++)); + thread.setDaemon(true); + return thread; + } + } +} diff --git a/src/main/java/org/xbib/netty/http/client/ClientAuthMode.java b/src/main/java/org/xbib/netty/http/client/ClientAuthMode.java new file mode 100644 index 0000000..6b54e64 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/ClientAuthMode.java @@ -0,0 +1,8 @@ +package org.xbib.netty.http.client; + +/** + * Client authentication modes, useful for SSL channels. + */ +public enum ClientAuthMode { + NONE, WANT, NEED +} diff --git a/src/main/java/org/xbib/netty/http/client/ClientBuilder.java b/src/main/java/org/xbib/netty/http/client/ClientBuilder.java new file mode 100644 index 0000000..d0e3bcf --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/ClientBuilder.java @@ -0,0 +1,181 @@ +package org.xbib.netty.http.client; + +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.proxy.HttpProxyHandler; +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.SslProvider; + +import javax.net.ssl.TrustManagerFactory; +import java.io.InputStream; + +public class ClientBuilder { + + private ByteBufAllocator byteBufAllocator; + + private EventLoopGroup eventLoopGroup; + + private Class socketChannelClass; + + private ClientConfig clientConfig; + + public ClientBuilder() { + this.clientConfig = new ClientConfig(); + } + + /** + * Set byte buf allocator for payload in HTTP requests. + * @param byteBufAllocator the byte buf allocator + * @return this builder + */ + public ClientBuilder setByteBufAllocator(ByteBufAllocator byteBufAllocator) { + this.byteBufAllocator = byteBufAllocator; + return this; + } + + public ClientBuilder setEventLoop(EventLoopGroup eventLoopGroup) { + this.eventLoopGroup = eventLoopGroup; + return this; + } + + public ClientBuilder setChannelClass(Class socketChannelClass) { + this.socketChannelClass = socketChannelClass; + return this; + } + + public ClientBuilder setThreadCount(int threadCount) { + clientConfig.setThreadCount(threadCount); + return this; + } + + public ClientBuilder setConnectTimeoutMillis(int connectTimeoutMillis) { + clientConfig.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + public ClientBuilder setTcpSendBufferSize(int tcpSendBufferSize) { + clientConfig.setTcpSendBufferSize(tcpSendBufferSize); + return this; + } + + public ClientBuilder setTcpReceiveBufferSize(int tcpReceiveBufferSize) { + clientConfig.setTcpReceiveBufferSize(tcpReceiveBufferSize); + return this; + } + + public ClientBuilder setTcpNodelay(boolean tcpNodelay) { + clientConfig.setTcpNodelay(tcpNodelay); + return this; + } + + public ClientBuilder setKeepAlive(boolean keepAlive) { + clientConfig.setKeepAlive(keepAlive); + return this; + } + + public ClientBuilder setReuseAddr(boolean reuseAddr) { + clientConfig.setReuseAddr(reuseAddr); + return this; + } + + public ClientBuilder setMaxChunkSize(int maxChunkSize) { + clientConfig.setMaxChunkSize(maxChunkSize); + return this; + } + + public ClientBuilder setMaxInitialLineLength(int maxInitialLineLength) { + clientConfig.setMaxInitialLineLength(maxInitialLineLength); + return this; + } + + public ClientBuilder setMaxHeadersSize(int maxHeadersSize) { + clientConfig.setMaxHeadersSize(maxHeadersSize); + return this; + } + + public ClientBuilder setMaxContentLength(int maxContentLength) { + clientConfig.setMaxContentLength(maxContentLength); + return this; + } + + public ClientBuilder setMaxCompositeBufferComponents(int maxCompositeBufferComponents) { + clientConfig.setMaxCompositeBufferComponents(maxCompositeBufferComponents); + return this; + } + + public ClientBuilder setMaxConnections(int maxConnections) { + clientConfig.setMaxConnections(maxConnections); + return this; + } + + public ClientBuilder setReadTimeoutMillis(int readTimeoutMillis) { + clientConfig.setReadTimeoutMillis(readTimeoutMillis); + return this; + } + + public ClientBuilder setEnableGzip(boolean enableGzip) { + clientConfig.setEnableGzip(enableGzip); + return this; + } + + public ClientBuilder setSslProvider(SslProvider sslProvider) { + clientConfig.setSslProvider(sslProvider); + return this; + } + + public ClientBuilder setJdkSslProvider() { + clientConfig.setJdkSslProvider(); + return this; + } + + public ClientBuilder setOpenSSLSslProvider() { + clientConfig.setOpenSSLSslProvider(); + return this; + } + + public ClientBuilder setCiphers(Iterable ciphers) { + clientConfig.setCiphers(ciphers); + return this; + } + + public ClientBuilder setCipherSuiteFilter(CipherSuiteFilter cipherSuiteFilter) { + clientConfig.setCipherSuiteFilter(cipherSuiteFilter); + return this; + } + + public ClientBuilder setTrustManagerFactory(TrustManagerFactory trustManagerFactory) { + clientConfig.setTrustManagerFactory(trustManagerFactory); + return this; + } + + public ClientBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream) { + clientConfig.setKeyCert(keyCertChainInputStream, keyInputStream); + return this; + } + + public ClientBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream, + String keyPassword) { + clientConfig.setKeyCert(keyCertChainInputStream, keyInputStream, keyPassword); + return this; + } + + public ClientBuilder setServerNameIdentification(boolean serverNameIdentification) { + clientConfig.setServerNameIdentification(serverNameIdentification); + return this; + } + + public ClientBuilder setClientAuthMode(ClientAuthMode clientAuthMode) { + clientConfig.setClientAuthMode(clientAuthMode); + return this; + } + + public ClientBuilder setHttpProxyHandler(HttpProxyHandler httpProxyHandler) { + clientConfig.setHttpProxyHandler(httpProxyHandler); + return this; + } + + public Client build() { + return new Client(clientConfig, byteBufAllocator, eventLoopGroup, socketChannelClass); + } +} diff --git a/src/main/java/org/xbib/netty/http/client/ClientConfig.java b/src/main/java/org/xbib/netty/http/client/ClientConfig.java new file mode 100644 index 0000000..dd168f9 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/ClientConfig.java @@ -0,0 +1,410 @@ +package org.xbib.netty.http.client; + +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.proxy.HttpProxyHandler; +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; + +import javax.net.ssl.TrustManagerFactory; +import java.io.InputStream; + +public class ClientConfig { + + interface Defaults { + + /** + * Default for thread count. + */ + int THREAD_COUNT = 0; + + /** + * Default for TCP_NODELAY. + */ + boolean TCP_NODELAY = true; + + /** + * Default for SO_KEEPALIVE. + */ + boolean SO_KEEPALIVE = true; + + /** + * Default for SO_REUSEADDR. + */ + boolean SO_REUSEADDR = true; + + /** + * Set TCP send buffer to 64k per socket. + */ + int TCP_SEND_BUFFER_SIZE = 64 * 1024; + + /** + * Set TCP receive buffer to 64k per socket. + */ + int TCP_RECEIVE_BUFFER_SIZE = 64 * 1024; + + /** + * Set HTTP chunk maximum size to 8k. + * See {@link io.netty.handler.codec.http.HttpClientCodec}. + */ + int MAX_CHUNK_SIZE = 8 * 1024; + + /** + * Set HTTP initial line length to 4k. + * See {@link io.netty.handler.codec.http.HttpClientCodec}. + */ + int MAX_INITIAL_LINE_LENGTH = 4 * 1024; + + /** + * Set HTTP maximum headers size to 8k. + * See {@link io.netty.handler.codec.http.HttpClientCodec}. + */ + int MAX_HEADERS_SIZE = 8 * 1024; + + /** + * Set maximum content length to 100 MB. + */ + int MAX_CONTENT_LENGTH = 100 * 1024 * 1024; + + /** + * This is Netty's default. + * See {@link io.netty.handler.codec.MessageAggregator#DEFAULT_MAX_COMPOSITEBUFFER_COMPONENTS}. + */ + int MAX_COMPOSITE_BUFFER_COMPONENTS = 1024; + + /** + * Allow maximum concurrent connections. + * Usually, browsers restrict concurrent connections to 8 for a single address. + */ + int MAX_CONNECTIONS = 8; + + /** + * Default read/write timeout in milliseconds. + */ + int TIMEOUT_MILLIS = 5000; + + /** + * Default for gzip codec. + */ + boolean ENABLE_GZIP = true; + + /** + * Default SSL provider. + */ + SslProvider SSL_PROVIDER = OpenSsl.isAvailable() && OpenSsl.isAlpnSupported() ? + SslProvider.OPENSSL : SslProvider.JDK; + + /** + * Default ciphers. + */ + Iterable CIPHERS = Http2SecurityUtil.CIPHERS; + + /** + * Default cipher suite filter. + */ + CipherSuiteFilter CIPHER_SUITE_FILTER = SupportedCipherSuiteFilter.INSTANCE; + + /** + * Default trust manager factory. + */ + TrustManagerFactory TRUST_MANAGER_FACTORY = InsecureTrustManagerFactory.INSTANCE; + + boolean USE_SERVER_NAME_IDENTIFICATION = true; + + /** + * Default for SSL client authentication. + */ + ClientAuthMode SSL_CLIENT_AUTH_MODE = ClientAuthMode.NONE; + } + + + /** + * If set to 0, then Netty will decide about thread count. + * Default is Runtime.getRuntime().availableProcessors() * 2 + */ + private int threadCount = Defaults.THREAD_COUNT; + + private boolean tcpNodelay = Defaults.TCP_NODELAY; + + private boolean keepAlive = Defaults.SO_KEEPALIVE; + + private boolean reuseAddr = Defaults.SO_REUSEADDR; + + private int tcpSendBufferSize = Defaults.TCP_SEND_BUFFER_SIZE; + + private int tcpReceiveBufferSize = Defaults.TCP_RECEIVE_BUFFER_SIZE; + + private int maxInitialLineLength = Defaults.MAX_INITIAL_LINE_LENGTH; + + private int maxHeadersSize = Defaults.MAX_HEADERS_SIZE; + + private int maxChunkSize = Defaults.MAX_CHUNK_SIZE; + + private int maxConnections = Defaults.MAX_CONNECTIONS; + + private int maxContentLength = Defaults.MAX_CONTENT_LENGTH; + + private int maxCompositeBufferComponents = Defaults.MAX_COMPOSITE_BUFFER_COMPONENTS; + + private int connectTimeoutMillis = Defaults.TIMEOUT_MILLIS; + + private int readTimeoutMillis = Defaults.TIMEOUT_MILLIS; + + private boolean enableGzip = Defaults.ENABLE_GZIP; + + private SslProvider sslProvider = Defaults.SSL_PROVIDER; + + private Iterable ciphers = Defaults.CIPHERS; + + private CipherSuiteFilter cipherSuiteFilter = Defaults.CIPHER_SUITE_FILTER; + + private TrustManagerFactory trustManagerFactory = Defaults.TRUST_MANAGER_FACTORY; + + private boolean serverNameIdentification = Defaults.USE_SERVER_NAME_IDENTIFICATION; + + private ClientAuthMode clientAuthMode = Defaults.SSL_CLIENT_AUTH_MODE; + + private InputStream keyCertChainInputStream; + + private InputStream keyInputStream; + + private String keyPassword; + + private HttpProxyHandler httpProxyHandler; + + public ClientConfig setThreadCount(int threadCount) { + this.threadCount = threadCount; + return this; + } + + public int getThreadCount() { + return threadCount; + } + + public ClientConfig setTcpNodelay(boolean tcpNodelay) { + this.tcpNodelay = tcpNodelay; + return this; + } + + public boolean isTcpNodelay() { + return tcpNodelay; + } + + public ClientConfig setKeepAlive(boolean keepAlive) { + this.keepAlive = keepAlive; + return this; + } + + public boolean isKeepAlive() { + return keepAlive; + } + + public ClientConfig setReuseAddr(boolean reuseAddr) { + this.reuseAddr = reuseAddr; + return this; + } + + public boolean isReuseAddr() { + return reuseAddr; + } + + public ClientConfig setTcpSendBufferSize(int tcpSendBufferSize) { + this.tcpSendBufferSize = tcpSendBufferSize; + return this; + } + + public int getTcpSendBufferSize() { + return tcpSendBufferSize; + } + + public ClientConfig setTcpReceiveBufferSize(int tcpReceiveBufferSize) { + this.tcpReceiveBufferSize = tcpReceiveBufferSize; + return this; + } + + public int getTcpReceiveBufferSize() { + return tcpReceiveBufferSize; + } + + public ClientConfig setMaxInitialLineLength(int maxInitialLineLength) { + this.maxInitialLineLength = maxInitialLineLength; + return this; + } + + public int getMaxInitialLineLength() { + return maxInitialLineLength; + } + + public ClientConfig setMaxHeadersSize(int maxHeadersSize) { + this.maxHeadersSize = maxHeadersSize; + return this; + } + + public int getMaxHeadersSize() { + return maxHeadersSize; + } + + public ClientConfig setMaxChunkSize(int maxChunkSize) { + this.maxChunkSize = maxChunkSize; + return this; + } + + public int getMaxChunkSize() { + return maxChunkSize; + } + + public ClientConfig setMaxConnections(int maxConnections) { + this.maxConnections = maxConnections; + return this; + } + + public int getMaxConnections() { + return maxConnections; + } + + public ClientConfig setMaxContentLength(int maxContentLength) { + this.maxContentLength = maxContentLength; + return this; + } + + public int getMaxContentLength() { + return maxContentLength; + } + + public ClientConfig setMaxCompositeBufferComponents(int maxCompositeBufferComponents) { + this.maxCompositeBufferComponents = maxCompositeBufferComponents; + return this; + } + + public int getMaxCompositeBufferComponents() { + return maxCompositeBufferComponents; + } + + public ClientConfig setConnectTimeoutMillis(int connectTimeoutMillis) { + this.connectTimeoutMillis = connectTimeoutMillis; + return this; + } + + public int getConnectTimeoutMillis() { + return connectTimeoutMillis; + } + + public ClientConfig setReadTimeoutMillis(int readTimeoutMillis) { + this.readTimeoutMillis = readTimeoutMillis; + return this; + } + + public int getReadTimeoutMillis() { + return readTimeoutMillis; + } + + public ClientConfig setEnableGzip(boolean enableGzip) { + this.enableGzip = enableGzip; + return this; + } + + public boolean isEnableGzip() { + return enableGzip; + } + + public ClientConfig setSslProvider(SslProvider sslProvider) { + this.sslProvider = sslProvider; + return this; + } + + public SslProvider getSslProvider() { + return sslProvider; + } + + public ClientConfig setJdkSslProvider() { + this.sslProvider = SslProvider.JDK; + return this; + } + + public ClientConfig setOpenSSLSslProvider() { + this.sslProvider = SslProvider.OPENSSL; + return this; + } + + public ClientConfig setCiphers(Iterable ciphers) { + this.ciphers = ciphers; + return this; + } + + public Iterable getCiphers() { + return ciphers; + } + + public ClientConfig setCipherSuiteFilter(CipherSuiteFilter cipherSuiteFilter) { + this.cipherSuiteFilter = cipherSuiteFilter; + return this; + } + + public CipherSuiteFilter getCipherSuiteFilter() { + return cipherSuiteFilter; + } + + public ClientConfig setTrustManagerFactory(TrustManagerFactory trustManagerFactory) { + this.trustManagerFactory = trustManagerFactory; + return this; + } + + public TrustManagerFactory getTrustManagerFactory() { + return trustManagerFactory; + } + + public ClientConfig setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream) { + this.keyCertChainInputStream = keyCertChainInputStream; + this.keyInputStream = keyInputStream; + return this; + } + + public InputStream getKeyCertChainInputStream() { + return keyCertChainInputStream; + } + + public InputStream getKeyInputStream() { + return keyInputStream; + } + + public ClientConfig setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream, + String keyPassword) { + this.keyCertChainInputStream = keyCertChainInputStream; + this.keyInputStream = keyInputStream; + this.keyPassword = keyPassword; + return this; + } + + public String getKeyPassword() { + return keyPassword; + } + + public ClientConfig setServerNameIdentification(boolean serverNameIdentification) { + this.serverNameIdentification = serverNameIdentification; + return this; + } + + public boolean isServerNameIdentification() { + return serverNameIdentification; + } + + public ClientConfig setClientAuthMode(ClientAuthMode clientAuthMode) { + this.clientAuthMode = clientAuthMode; + return this; + } + + public ClientAuthMode getClientAuthMode() { + return clientAuthMode; + } + + public ClientConfig setHttpProxyHandler(HttpProxyHandler httpProxyHandler) { + this.httpProxyHandler = httpProxyHandler; + return this; + } + + public HttpProxyHandler getHttpProxyHandler() { + return httpProxyHandler; + } +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpAddress.java b/src/main/java/org/xbib/netty/http/client/HttpAddress.java new file mode 100644 index 0000000..a3629d9 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/HttpAddress.java @@ -0,0 +1,113 @@ +package org.xbib.netty.http.client; + +import io.netty.handler.codec.http.HttpVersion; +import org.xbib.net.URL; + +import java.net.InetSocketAddress; + +/** + * A handle for host, port, HTTP version, secure transport flag of a channel for HTTP. + */ +public class HttpAddress { + + private static final HttpVersion HTTP_2_0 = HttpVersion.valueOf("HTTP/2.0"); + + private final String host; + + private final Integer port; + + private final HttpVersion version; + + private final Boolean secure; + + private InetSocketAddress inetSocketAddress; + + public static HttpAddress http1(String host) { + return new HttpAddress(host, 80, HttpVersion.HTTP_1_1, false); + } + + public static HttpAddress http1(String host, int port) { + return new HttpAddress(host, port, HttpVersion.HTTP_1_1, false); + } + + public static HttpAddress secureHttp1(String host) { + return new HttpAddress(host, 443, HttpVersion.HTTP_1_1, true); + } + + public static HttpAddress secureHttp1(String host, int port) { + return new HttpAddress(host, port, HttpVersion.HTTP_1_1, true); + } + + public static HttpAddress http2(String host) { + return new HttpAddress(host, 443, HTTP_2_0, true); + } + + public static HttpAddress http2(String host, int port) { + return new HttpAddress(host, port, HTTP_2_0, true); + } + + public static HttpAddress http1(URL url) { + return new HttpAddress(url, HttpVersion.HTTP_1_1); + } + + public static HttpAddress http2(URL url) { + return new HttpAddress(url, HTTP_2_0); + } + + public static HttpAddress of(Request request) { + return new HttpAddress(request.base(), request.httpVersion()); + } + + public static HttpAddress of(URL url, HttpVersion httpVersion) { + return new HttpAddress(url, httpVersion); + } + + public HttpAddress(URL url, HttpVersion version) { + this(url.getHost(), url.getPort(), version, "https".equals(url.getScheme())); + } + + public HttpAddress(String host, Integer port, HttpVersion version, boolean secure) { + this.host = host; + this.port = (port == null || port == -1) ? secure ? 443 : 80 : port; + this.version = version; + this.secure = secure; + } + + public InetSocketAddress getInetSocketAddress() { + if (inetSocketAddress == null) { + // this may execute DNS + this.inetSocketAddress = new InetSocketAddress(host, port); + } + return inetSocketAddress; + } + + public URL base() { + return isSecure() ? URL.https().host(host).port(port).build() : URL.http().host(host).port(port).build(); + } + + public HttpVersion getVersion() { + return version; + } + + public boolean isSecure() { + return secure; + } + + public String toString() { + return host + ":" + port + " (version:" + version + ",secure:" + secure + ")"; + } + + @Override + public boolean equals(Object object) { + return object instanceof HttpAddress && + host.equals(((HttpAddress) object).host) && + (port != null && port.equals(((HttpAddress) object).port)) && + version.equals(((HttpAddress) object).version) && + secure.equals(((HttpAddress) object).secure); + } + + @Override + public int hashCode() { + return host.hashCode() ^ port ^ version.hashCode() ^ secure.hashCode(); + } +} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClient.java b/src/main/java/org/xbib/netty/http/client/HttpClient.java deleted file mode 100755 index 2f3ef8b..0000000 --- a/src/main/java/org/xbib/netty/http/client/HttpClient.java +++ /dev/null @@ -1,474 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client; - -import io.netty.bootstrap.Bootstrap; -import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelPromise; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.pool.ChannelPool; -import io.netty.channel.pool.FixedChannelPool; -import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.DefaultHttpRequest; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpResponse; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http.cookie.ClientCookieEncoder; -import io.netty.handler.codec.http.cookie.Cookie; -import io.netty.handler.ssl.OpenSsl; -import io.netty.util.concurrent.Future; -import io.netty.util.concurrent.FutureListener; -import org.xbib.netty.http.client.internal.HttpClientChannelPoolMap; -import org.xbib.netty.http.client.listener.CookieListener; -import org.xbib.netty.http.client.listener.ExceptionListener; -import org.xbib.netty.http.client.listener.HttpHeadersListener; -import org.xbib.netty.http.client.listener.HttpPushListener; -import org.xbib.netty.http.client.listener.HttpResponseListener; -import org.xbib.netty.http.client.util.InetAddressKey; -import org.xbib.netty.http.client.util.NetworkUtils; - -import java.io.Closeable; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.ConnectException; -import java.net.URI; -import java.net.URLDecoder; -import java.util.Collection; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * A Netty HTTP client. - */ -public final class HttpClient implements Closeable { - - private static final Logger logger = Logger.getLogger(HttpClient.class.getName()); - - private static final AtomicInteger streamId = new AtomicInteger(3); - - private static final HttpClient INSTANCE = HttpClient.builder().build(); - - static { - NetworkUtils.extendSystemProperties(); - logger.log(Level.FINE, () -> "OpenSSL ALPN support: " + OpenSsl.isAlpnSupported()); - logger.log(Level.FINE, () -> "local host name = " + NetworkUtils.getLocalHostName("localhost")); - logger.log(Level.FINE, NetworkUtils::displayNetworkInterfaces); - } - - private final ByteBufAllocator byteBufAllocator; - - private final EventLoopGroup eventLoopGroup; - - private final HttpClientChannelPoolMap poolMap; - - /** - * Create a new HTTP client. Use {@link #builder()} to build HTTP client instance. - */ - HttpClient(ByteBufAllocator byteBufAllocator, - EventLoopGroup eventLoopGroup, - Bootstrap bootstrap, - int maxConnections, - HttpClientChannelContext httpClientChannelContext) { - this.byteBufAllocator = byteBufAllocator; - this.eventLoopGroup = eventLoopGroup; - this.poolMap = new HttpClientChannelPoolMap(this, httpClientChannelContext, bootstrap, maxConnections); - } - - public static HttpClient getInstance() { - return INSTANCE; - } - - /** - * Create a builder to configure connecting. - * - * @return A builder - */ - public static HttpClientBuilder builder() { - return new HttpClientBuilder(); - } - - public HttpClientRequestBuilder prepareRequest(HttpMethod method) { - return new HttpClientRequestBuilder(this, method, byteBufAllocator, streamId.getAndAdd(2)); - } - - /** - * Prepare a HTTP GET request. - * - * @return a request builder - */ - public HttpRequestBuilder prepareGet() { - return prepareRequest(HttpMethod.GET); - } - - public HttpRequestBuilder prepareGet(String url) { - return prepareRequest(HttpMethod.GET).setURL(url); - } - - /** - * Prepare a HTTP HEAD request. - * - * @return a request builder - */ - public HttpRequestBuilder prepareHead() { - return prepareRequest(HttpMethod.HEAD); - } - - public HttpRequestBuilder prepareHead(String url) { - return prepareRequest(HttpMethod.HEAD).setURL(url); - } - - /** - * Prepare a HTTP PUT request. - * - * @return a request builder - */ - public HttpRequestBuilder preparePut() { - return prepareRequest(HttpMethod.PUT); - } - - public HttpRequestBuilder preparePut(String url) { - return prepareRequest(HttpMethod.PUT).setURL(url); - } - - /** - * Prepare a HTTP POST request. - * - * @return a request builder - */ - public HttpRequestBuilder preparePost() { - return prepareRequest(HttpMethod.POST); - } - - public HttpRequestBuilder preparePost(String url) { - return prepareRequest(HttpMethod.POST).setURL(url); - } - - /** - * Prepare a HTTP DELETE request. - * - * @return a request builder - */ - public HttpRequestBuilder prepareDelete() { - return prepareRequest(HttpMethod.DELETE); - } - - public HttpRequestBuilder prepareDelete(String url) { - return prepareRequest(HttpMethod.DELETE).setURL(url); - } - - /** - * Prepare a HTTP OPTIONS request. - * - * @return a request builder - */ - public HttpRequestBuilder prepareOptions() { - return prepareRequest(HttpMethod.OPTIONS); - } - - public HttpRequestBuilder prepareOptions(String url) { - return prepareRequest(HttpMethod.OPTIONS).setURL(url); - } - - /** - * Prepare a HTTP PATCH request. - * - * @return a request builder - */ - public HttpRequestBuilder preparePatch() { - return prepareRequest(HttpMethod.PATCH); - } - - public HttpRequestBuilder preparePatch(String url) { - return prepareRequest(HttpMethod.PATCH).setURL(url); - } - - /** - * Prepare a HTTP TRACE request. - * - * @return a request builder - */ - public HttpRequestBuilder prepareTrace() { - return prepareRequest(HttpMethod.TRACE); - } - - public HttpRequestBuilder prepareTrace(String url) { - return prepareRequest(HttpMethod.TRACE).setURL(url); - } - - public HttpClientChannelPoolMap poolMap() { - return poolMap; - } - - /** - * Close client. - */ - public void close() { - logger.log(Level.FINE, () -> "closing pool map"); - poolMap.close(); - logger.log(Level.FINE, () -> "closing event loop group"); - if (!eventLoopGroup.isShuttingDown()) { - eventLoopGroup.shutdownGracefully(); - } - logger.log(Level.FINE, () -> "closed"); - } - - public void dispatch(final HttpRequestContext httpRequestContext) { - final URI uri = httpRequestContext.getURI(); - final HttpRequest httpRequest = httpRequestContext.getHttpRequest(); - if (!httpRequestContext.getCookies().isEmpty()) { - logger.log(Level.FINE, () -> "configured cookies: " + httpRequestContext.getCookies()); - Collection cookies = httpRequestContext.matchCookies(); - if (!cookies.isEmpty()) { - logger.log(Level.FINE, () -> "updating cookie header with matched cookies: " + cookies); - httpRequest.headers().set(HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode(cookies)); - } - } - logger.log(Level.FINE, () -> "trying URL " + uri); - if (httpRequestContext.isExpired()) { - httpRequestContext.fail("request expired"); - } - if (httpRequestContext.isFailed()) { - logger.log(Level.FINE, () -> "request is cancelled"); - return; - } - HttpVersion version = httpRequestContext.getHttpRequest().protocolVersion(); - boolean secure = "https".equals(uri.getScheme()); - InetAddressKey inetAddressKey = new InetAddressKey(uri.getHost(), uri.getPort(), version, secure); - final FixedChannelPool pool = poolMap.get(inetAddressKey); - logger.log(Level.FINE, () -> "connecting to " + inetAddressKey); - Future futureChannel = pool.acquire(); - futureChannel.addListener((FutureListener) future -> { - final ExceptionListener exceptionListener = httpRequestContext.getExceptionListener(); - if (future.isSuccess()) { - Channel channel = future.getNow(); - // set settings promise before adding httpRequestContext as a channel attribute - ChannelPromise settingsPromise = channel.newPromise(); - httpRequestContext.setSettingsPromise(settingsPromise); - channel.attr(HttpClientChannelContextDefaults.CHANNEL_POOL_ATTRIBUTE_KEY).set(pool); - channel.attr(HttpClientChannelContextDefaults.REQUEST_CONTEXT_ATTRIBUTE_KEY).set(httpRequestContext); - HttpResponseListener httpResponseListener = httpRequestContext.getHttpResponseListener(); - channel.attr(HttpClientChannelContextDefaults.RESPONSE_LISTENER_ATTRIBUTE_KEY).set(httpResponseListener); - HttpPushListener httpPushListener = httpRequestContext.getHttpPushListener(); - channel.attr(HttpClientChannelContextDefaults.PUSH_LISTENER_ATTRIBUTE_KEY).set(httpPushListener); - HttpHeadersListener httpHeadersListener = httpRequestContext.getHttpHeadersListener(); - channel.attr(HttpClientChannelContextDefaults.HEADER_LISTENER_ATTRIBUTE_KEY).set(httpHeadersListener); - CookieListener cookieListener = httpRequestContext.getCookieListener(); - channel.attr(HttpClientChannelContextDefaults.COOKIE_LISTENER_ATTRIBUTE_KEY).set(cookieListener); - channel.attr(HttpClientChannelContextDefaults.EXCEPTION_LISTENER_ATTRIBUTE_KEY).set(exceptionListener); - if (httpRequestContext.isFailed()) { - logger.log(Level.FINE, () -> "detected fail, close channel"); - future.cancel(true); - if (channel.isOpen()) { - channel.close(); - } - logger.log(Level.FINE, () -> "release channel to pool"); - pool.release(channel); - return; - } - if (httpRequest.protocolVersion().majorVersion() == 1) { - logger.log(Level.FINE, "HTTP1: write and flush " + httpRequest.toString()); - channel.writeAndFlush(httpRequest) - .addListener((ChannelFutureListener) future1 -> { - if (httpRequestContext.isFailed()) { - logger.log(Level.FINE, () -> "detected fail, close now"); - future1.cancel(true); - if (future1.channel().isOpen()) { - future1.channel().close(); - } - } - }); - } else if (httpRequest.protocolVersion().majorVersion() == 2) { - logger.log(Level.FINE, () -> "waiting for HTTP/2 settings"); - settingsPromise.await(httpRequestContext.getTimeout(), TimeUnit.MILLISECONDS); - logger.log(Level.FINE, () -> "waiting for HTTP/2 responses = " + - httpRequestContext.getStreamIdPromiseMap().size()); - int timeout = httpRequestContext.getTimeout(); - for (Map.Entry> entry : - httpRequestContext.getStreamIdPromiseMap().entrySet()) { - ChannelFuture channelFuture = entry.getValue().getKey(); - if (channelFuture != null) { - logger.log(Level.FINE, "waiting for channel, stream ID = " + entry.getKey()); - if (!channelFuture.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) { - IllegalStateException illegalStateException = - new IllegalStateException("time out while waiting to write for stream id " + - entry.getKey()); - if (exceptionListener != null) { - exceptionListener.onException(illegalStateException); - httpRequestContext.fail(illegalStateException.getMessage()); - final ChannelPool channelPool = channelFuture.channel() - .attr(HttpClientChannelContextDefaults.CHANNEL_POOL_ATTRIBUTE_KEY).get(); - channelPool.release(channelFuture.channel()); - } - throw illegalStateException; - } - if (!channelFuture.isSuccess()) { - throw new RuntimeException(channelFuture.cause()); - } - } - ChannelPromise promise = entry.getValue().getValue(); - logger.log(Level.FINE, "waiting for promise of stream ID = " + entry.getKey()); - if (!promise.awaitUninterruptibly(timeout, TimeUnit.MILLISECONDS)) { - IllegalStateException illegalStateException = - new IllegalStateException("time out while waiting for response on stream id " + - entry.getKey()); - if (exceptionListener != null) { - exceptionListener.onException(illegalStateException); - httpRequestContext.fail(illegalStateException.getMessage()); - if (channelFuture != null) { - final ChannelPool channelPool = channelFuture.channel() - .attr(HttpClientChannelContextDefaults.CHANNEL_POOL_ATTRIBUTE_KEY).get(); - channelPool.release(channelFuture.channel()); - } - } - throw illegalStateException; - } - if (!promise.isSuccess()) { - RuntimeException runtimeException = new RuntimeException(promise.cause()); - if (exceptionListener != null) { - exceptionListener.onException(runtimeException); - httpRequestContext.fail(runtimeException.getMessage()); - if (channelFuture != null) { - final ChannelPool channelPool = channelFuture.channel() - .attr(HttpClientChannelContextDefaults.CHANNEL_POOL_ATTRIBUTE_KEY).get(); - channelPool.release(channelFuture.channel()); - } - } - throw runtimeException; - } - } - } - } else { - if (exceptionListener != null) { - exceptionListener.onException(future.cause()); - } - httpRequestContext.fail(new ConnectException("unable to connect to " + inetAddressKey)); - } - }); - } - - public boolean tryRedirect(Channel channel, FullHttpResponse httpResponse, HttpRequestContext httpRequestContext) - throws IOException { - if (httpRequestContext.isFollowRedirect()) { - String redirUrl = findRedirect(httpRequestContext, httpResponse); - if (redirUrl != null) { - HttpMethod method = httpResponse.status().code() == 303 ? HttpMethod.GET : - httpRequestContext.getHttpRequest().method(); - if (httpRequestContext.getRedirectCount().getAndIncrement() < httpRequestContext.getMaxRedirects()) { - dispatchRedirect(method, URI.create(redirUrl), httpRequestContext); - } else { - httpRequestContext.fail("too many redirections"); - final ChannelPool channelPool = - channel.attr(HttpClientChannelContextDefaults.CHANNEL_POOL_ATTRIBUTE_KEY).get(); - channelPool.release(channel); - } - return true; - } - } - return false; - } - - private String findRedirect(HttpRequestContext httpRequestContext, HttpResponse httpResponse) - throws IOException { - if (httpResponse == null) { - return null; - } - switch (httpResponse.status().code()) { - case 300: - case 301: - case 302: - case 303: - case 305: - case 307: - case 308: - String location = URLDecoder.decode(httpResponse.headers().get(HttpHeaderNames.LOCATION), "UTF-8"); - if (location != null && (location.toLowerCase().startsWith("http://") || - location.toLowerCase().startsWith("https://"))) { - logger.log(Level.FINE, "(absolute) redirect to " + location); - return location; - } else { - logger.log(Level.FINE, "(relative->absolute) redirect to " + location); - return makeAbsolute(httpRequestContext.getURI(), location); - } - default: - break; - } - return null; - } - - private void dispatchRedirect(HttpMethod method, URI uri, - HttpRequestContext httpRequestContext) { - final String uriStr = httpRequestContext.getHttpRequest().protocolVersion().majorVersion() == 2 ? - uri.toASCIIString() : makeRelative(uri); - final HttpRequest httpRequest; - if (method.equals(httpRequestContext.getHttpRequest().method()) && - httpRequestContext.getHttpRequest() instanceof DefaultFullHttpRequest) { - DefaultFullHttpRequest defaultFullHttpRequest = (DefaultFullHttpRequest) httpRequestContext.getHttpRequest(); - FullHttpRequest fullHttpRequest = defaultFullHttpRequest.copy(); - fullHttpRequest.setUri(uriStr); - httpRequest = fullHttpRequest; - } else { - httpRequest = new DefaultHttpRequest(httpRequestContext.getHttpRequest().protocolVersion(), method, uriStr); - } - for (Map.Entry e : httpRequestContext.getHttpRequest().headers().entries()) { - httpRequest.headers().add(e.getKey(), e.getValue()); - } - httpRequest.headers().set(HttpHeaderNames.HOST, uri.getHost()); - HttpRequestContext redirectContext = new HttpRequestContext(uri, httpRequest, - httpRequestContext); - logger.log(Level.FINE, "dispatchRedirect url = " + uri + " with new request " + httpRequest.toString()); - dispatch(redirectContext); - } - - private String makeRelative(URI base) { - String uri = base.getPath(); - if (base.getQuery() != null) { - uri = uri + "?" + base.getQuery(); - } - return uri; - } - - private String makeAbsolute(URI base, String location) throws UnsupportedEncodingException { - String path = base.getPath() == null ? "/" : URLDecoder.decode(base.getPath(), "UTF-8"); - if (location.startsWith("/")) { - path = location; - } else if (path.endsWith("/")) { - path += location; - } else { - path += "/" + location; - } - String scheme = base.getScheme(); - StringBuilder sb = new StringBuilder(scheme).append("://").append(base.getHost()); - int defaultPort = "http".equals(scheme) ? 80 : "https".equals(scheme) ? 443 : -1; - if (defaultPort != -1 && base.getPort() != -1 && defaultPort != base.getPort()) { - sb.append(":").append(base.getPort()); - } - if (path.charAt(0) != '/') { - sb.append('/'); - } - sb.append(path); - return sb.toString(); - } -} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientBuilder.java b/src/main/java/org/xbib/netty/http/client/HttpClientBuilder.java deleted file mode 100644 index fb58bc1..0000000 --- a/src/main/java/org/xbib/netty/http/client/HttpClientBuilder.java +++ /dev/null @@ -1,333 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client; - -import io.netty.bootstrap.Bootstrap; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.PooledByteBufAllocator; -import io.netty.channel.ChannelOption; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.SocketChannel; -import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.proxy.HttpProxyHandler; -import io.netty.handler.proxy.Socks4ProxyHandler; -import io.netty.handler.proxy.Socks5ProxyHandler; -import io.netty.handler.ssl.CipherSuiteFilter; -import io.netty.handler.ssl.SslProvider; -import org.xbib.netty.http.client.internal.HttpClientThreadFactory; -import org.xbib.netty.http.client.util.ClientAuthMode; - -import javax.net.ssl.TrustManagerFactory; -import java.io.InputStream; -import java.net.InetSocketAddress; - -/** - * - */ -public class HttpClientBuilder implements HttpClientChannelContextDefaults { - - private ByteBufAllocator byteBufAllocator; - - private EventLoopGroup eventLoopGroup; - - private Class socketChannelClass; - - private Bootstrap bootstrap; - - // let Netty decide about thread number, default is Runtime.getRuntime().availableProcessors() * 2 - private int threads = 0; - - private boolean tcpNodelay = DEFAULT_TCP_NODELAY; - - private boolean keepAlive = DEFAULT_SO_KEEPALIVE; - - private boolean reuseAddr = DEFAULT_SO_REUSEADDR; - - private int tcpSendBufferSize = DEFAULT_TCP_SEND_BUFFER_SIZE; - - private int tcpReceiveBufferSize = DEFAULT_TCP_RECEIVE_BUFFER_SIZE; - - private int maxInitialLineLength = DEFAULT_MAX_INITIAL_LINE_LENGTH; - - private int maxHeadersSize = DEFAULT_MAX_HEADERS_SIZE; - - private int maxChunkSize = DEFAULT_MAX_CHUNK_SIZE; - - private int maxConnections = DEFAULT_MAX_CONNECTIONS; - - private int maxContentLength = DEFAULT_MAX_CONTENT_LENGTH; - - private int maxCompositeBufferComponents = DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS; - - private int connectTimeoutMillis = DEFAULT_TIMEOUT_MILLIS; - - private int readTimeoutMillis = DEFAULT_TIMEOUT_MILLIS; - - private boolean enableGzip = DEFAULT_ENABLE_GZIP; - - private boolean installHttp2Upgrade = DEFAULT_INSTALL_HTTP_UPGRADE2; - - private SslProvider sslProvider = DEFAULT_SSL_PROVIDER; - - private Iterable ciphers = DEFAULT_CIPHERS; - - private CipherSuiteFilter cipherSuiteFilter = DEFAULT_CIPHER_SUITE_FILTER; - - private TrustManagerFactory trustManagerFactory = DEFAULT_TRUST_MANAGER_FACTORY; - - private InputStream keyCertChainInputStream; - - private InputStream keyInputStream; - - private String keyPassword; - - private boolean useServerNameIdentification = DEFAULT_USE_SERVER_NAME_IDENTIFICATION; - - private ClientAuthMode clientAuthMode = DEFAULT_SSL_CLIENT_AUTH_MODE; - - private HttpProxyHandler httpProxyHandler; - - private Socks4ProxyHandler socks4ProxyHandler; - - private Socks5ProxyHandler socks5ProxyHandler; - - /** - * Set byte buf allocator for payload in HTTP requests. - * @param byteBufAllocator the byte buf allocator - * @return this builder - */ - public HttpClientBuilder withByteBufAllocator(ByteBufAllocator byteBufAllocator) { - this.byteBufAllocator = byteBufAllocator; - return this; - } - - public HttpClientBuilder withEventLoop(EventLoopGroup eventLoopGroup) { - this.eventLoopGroup = eventLoopGroup; - return this; - } - - public HttpClientBuilder withChannelClass(Class socketChannelClass) { - this.socketChannelClass = socketChannelClass; - return this; - } - - public HttpClientBuilder withBootstrap(Bootstrap bootstrap) { - this.bootstrap = bootstrap; - return this; - } - - public HttpClientBuilder setConnectTimeoutMillis(int connectTimeoutMillis) { - this.connectTimeoutMillis = connectTimeoutMillis; - return this; - } - - public HttpClientBuilder setThreadCount(int count) { - this.threads = count; - return this; - } - - public HttpClientBuilder setTcpSendBufferSize(int tcpSendBufferSize) { - this.tcpSendBufferSize = tcpSendBufferSize; - return this; - } - - public HttpClientBuilder setTcpReceiveBufferSize(int tcpReceiveBufferSize) { - this.tcpReceiveBufferSize = tcpReceiveBufferSize; - return this; - } - - public HttpClientBuilder setTcpNodelay(boolean tcpNodelay) { - this.tcpNodelay = tcpNodelay; - return this; - } - - public HttpClientBuilder setKeepAlive(boolean keepAlive) { - this.keepAlive = keepAlive; - return this; - } - - public HttpClientBuilder setReuseAddr(boolean reuseAddr) { - this.reuseAddr = reuseAddr; - return this; - } - - public HttpClientBuilder setMaxChunkSize(int maxChunkSize) { - this.maxChunkSize = maxChunkSize; - return this; - } - - public HttpClientBuilder setMaxInitialLineLength(int maxInitialLineLength) { - this.maxInitialLineLength = maxInitialLineLength; - return this; - } - - public HttpClientBuilder setMaxHeadersSize(int maxHeadersSize) { - this.maxHeadersSize = maxHeadersSize; - return this; - } - - public HttpClientBuilder setMaxContentLength(int maxContentLength) { - this.maxContentLength = maxContentLength; - return this; - } - - public HttpClientBuilder setMaxCompositeBufferComponents(int maxCompositeBufferComponents) { - this.maxCompositeBufferComponents = maxCompositeBufferComponents; - return this; - } - - public HttpClientBuilder setMaxConnections(int maxConnections) { - this.maxConnections = maxConnections; - return this; - } - - public HttpClientBuilder setReadTimeoutMillis(int readTimeoutMillis) { - this.readTimeoutMillis = readTimeoutMillis; - return this; - } - - public HttpClientBuilder setEnableGzip(boolean enableGzip) { - this.enableGzip = enableGzip; - return this; - } - - public HttpClientBuilder setInstallHttp2Upgrade(boolean installHttp2Upgrade) { - this.installHttp2Upgrade = installHttp2Upgrade; - return this; - } - - public HttpClientBuilder withSslProvider(SslProvider sslProvider) { - this.sslProvider = sslProvider; - return this; - } - - public HttpClientBuilder withJdkSslProvider() { - this.sslProvider = SslProvider.JDK; - return this; - } - - public HttpClientBuilder withOpenSSLSslProvider() { - this.sslProvider = SslProvider.OPENSSL; - return this; - } - - public HttpClientBuilder withCiphers(Iterable ciphers) { - this.ciphers = ciphers; - return this; - } - - public HttpClientBuilder withCipherSuiteFilter(CipherSuiteFilter cipherSuiteFilter) { - this.cipherSuiteFilter = cipherSuiteFilter; - return this; - } - - public HttpClientBuilder withTrustManagerFactory(TrustManagerFactory trustManagerFactory) { - this.trustManagerFactory = trustManagerFactory; - return this; - } - - public HttpClientBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream) { - this.keyCertChainInputStream = keyCertChainInputStream; - this.keyInputStream = keyInputStream; - return this; - } - - public HttpClientBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream, - String keyPassword) { - this.keyCertChainInputStream = keyCertChainInputStream; - this.keyInputStream = keyInputStream; - this.keyPassword = keyPassword; - return this; - } - - public HttpClientBuilder setUseServerNameIdentification(boolean useServerNameIdentification) { - this.useServerNameIdentification = useServerNameIdentification; - return this; - } - - public HttpClientBuilder setClientAuthMode(ClientAuthMode clientAuthMode) { - this.clientAuthMode = clientAuthMode; - return this; - } - - public HttpClientBuilder setHttpProxyHandler(InetSocketAddress proxyAddress) { - this.httpProxyHandler = new HttpProxyHandler(proxyAddress); - return this; - } - - public HttpClientBuilder setHttpProxyHandler(InetSocketAddress proxyAddress, String username, String password) { - this.httpProxyHandler = new HttpProxyHandler(proxyAddress, username, password); - return this; - } - - public HttpClientBuilder setSocks4Proxy(InetSocketAddress proxyAddress) { - this.socks4ProxyHandler = new Socks4ProxyHandler(proxyAddress); - return this; - } - - public HttpClientBuilder setSocks4Proxy(InetSocketAddress proxyAddress, String username) { - this.socks4ProxyHandler = new Socks4ProxyHandler(proxyAddress, username); - return this; - } - - public HttpClientBuilder setSocks5Proxy(InetSocketAddress proxyAddress) { - this.socks5ProxyHandler = new Socks5ProxyHandler(proxyAddress); - return this; - } - - public HttpClientBuilder setSocks5Proxy(InetSocketAddress proxyAddress, String username, String password) { - this.socks5ProxyHandler = new Socks5ProxyHandler(proxyAddress, username, password); - return this; - } - - /** - * Build a HTTP client. - * @return a http client - */ - public HttpClient build() { - if (byteBufAllocator == null) { - byteBufAllocator = PooledByteBufAllocator.DEFAULT; - } - if (eventLoopGroup == null) { - eventLoopGroup = new NioEventLoopGroup(threads, new HttpClientThreadFactory()); - } - if (socketChannelClass == null) { - socketChannelClass = NioSocketChannel.class; - } - if (bootstrap == null) { - bootstrap = new Bootstrap(); - } - bootstrap.option(ChannelOption.TCP_NODELAY, tcpNodelay); - bootstrap.option(ChannelOption.SO_KEEPALIVE, keepAlive); - bootstrap.option(ChannelOption.SO_REUSEADDR, reuseAddr); - bootstrap.option(ChannelOption.SO_SNDBUF, tcpSendBufferSize); - bootstrap.option(ChannelOption.SO_RCVBUF, tcpReceiveBufferSize); - bootstrap.option(ChannelOption.ALLOCATOR, byteBufAllocator); - bootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, connectTimeoutMillis); - bootstrap.group(eventLoopGroup); - bootstrap.channel(socketChannelClass); - final HttpClientChannelContext httpClientChannelContext = - new HttpClientChannelContext(maxInitialLineLength, maxHeadersSize, maxChunkSize, maxContentLength, - maxCompositeBufferComponents, - readTimeoutMillis, enableGzip, installHttp2Upgrade, - sslProvider, ciphers, cipherSuiteFilter, trustManagerFactory, - keyCertChainInputStream, keyInputStream, keyPassword, - useServerNameIdentification, clientAuthMode, - httpProxyHandler, socks4ProxyHandler, socks5ProxyHandler); - return new HttpClient(byteBufAllocator, eventLoopGroup, bootstrap, maxConnections, httpClientChannelContext); - } -} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientChannelContext.java b/src/main/java/org/xbib/netty/http/client/HttpClientChannelContext.java deleted file mode 100644 index eb38aba..0000000 --- a/src/main/java/org/xbib/netty/http/client/HttpClientChannelContext.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client; - -import io.netty.handler.proxy.HttpProxyHandler; -import io.netty.handler.proxy.Socks4ProxyHandler; -import io.netty.handler.proxy.Socks5ProxyHandler; -import io.netty.handler.ssl.CipherSuiteFilter; -import io.netty.handler.ssl.SslProvider; -import org.xbib.netty.http.client.util.ClientAuthMode; - -import javax.net.ssl.TrustManagerFactory; -import java.io.InputStream; - -/** - */ -public final class HttpClientChannelContext { - - private final int maxInitialLineLength; - - private final int maxHeaderSize; - - private final int maxChunkSize; - - private final int maxContentLength; - - private final int maxCompositeBufferComponents; - - private final int readTimeoutMillis; - - private final boolean enableGzip; - - private final boolean installHttp2Upgrade; - - private final SslProvider sslProvider; - - private final Iterable ciphers; - - private final CipherSuiteFilter cipherSuiteFilter; - - private final TrustManagerFactory trustManagerFactory; - - private final InputStream keyCertChainInputStream; - - private final InputStream keyInputStream; - - private final String keyPassword; - - private final boolean useServerNameIdentification; - - private final ClientAuthMode clientAuthMode; - - private final HttpProxyHandler httpProxyHandler; - - private final Socks4ProxyHandler socks4ProxyHandler; - - private final Socks5ProxyHandler socks5ProxyHandler; - - HttpClientChannelContext(int maxInitialLineLength, - int maxHeaderSize, - int maxChunkSize, - int maxContentLength, - int maxCompositeBufferComponents, - int readTimeoutMillis, - boolean enableGzip, - boolean installHttp2Upgrade, - SslProvider sslProvider, - Iterable ciphers, - CipherSuiteFilter cipherSuiteFilter, - TrustManagerFactory trustManagerFactory, - InputStream keyCertChainInputStream, - InputStream keyInputStream, - String keyPassword, - boolean useServerNameIdentification, - ClientAuthMode clientAuthMode, - HttpProxyHandler httpProxyHandler, - Socks4ProxyHandler socks4ProxyHandler, - Socks5ProxyHandler socks5ProxyHandler) { - this.maxInitialLineLength = maxInitialLineLength; - this.maxHeaderSize = maxHeaderSize; - this.maxChunkSize = maxChunkSize; - this.maxContentLength = maxContentLength; - this.maxCompositeBufferComponents = maxCompositeBufferComponents; - this.readTimeoutMillis = readTimeoutMillis; - this.enableGzip = enableGzip; - this.installHttp2Upgrade = installHttp2Upgrade; - this.sslProvider = sslProvider; - this.ciphers = ciphers; - this.cipherSuiteFilter = cipherSuiteFilter; - this.trustManagerFactory = trustManagerFactory; - this.keyCertChainInputStream = keyCertChainInputStream; - this.keyInputStream = keyInputStream; - this.keyPassword = keyPassword; - this.useServerNameIdentification = useServerNameIdentification; - this.clientAuthMode = clientAuthMode; - this.httpProxyHandler = httpProxyHandler; - this.socks4ProxyHandler = socks4ProxyHandler; - this.socks5ProxyHandler = socks5ProxyHandler; - } - - public int getMaxInitialLineLength() { - return maxInitialLineLength; - } - - public int getMaxHeaderSize() { - return maxHeaderSize; - } - - public int getMaxChunkSize() { - return maxChunkSize; - } - - public int getMaxContentLength() { - return maxContentLength; - } - - public int getMaxCompositeBufferComponents() { - return maxCompositeBufferComponents; - } - - public int getReadTimeoutMillis() { - return readTimeoutMillis; - } - - public boolean isGzipEnabled() { - return enableGzip; - } - - public boolean isInstallHttp2Upgrade() { - return installHttp2Upgrade; - } - - public SslProvider getSslProvider() { - return sslProvider; - } - - public Iterable getCiphers() { - return ciphers; - } - - public CipherSuiteFilter getCipherSuiteFilter() { - return cipherSuiteFilter; - } - - public TrustManagerFactory getTrustManagerFactory() { - return trustManagerFactory; - } - - public InputStream getKeyCertChainInputStream() { - return keyCertChainInputStream; - } - - public InputStream getKeyInputStream() { - return keyInputStream; - } - - public String getKeyPassword() { - return keyPassword; - } - - public boolean isUseServerNameIdentification() { - return useServerNameIdentification; - } - - public ClientAuthMode getClientAuthMode() { - return clientAuthMode; - } - - public HttpProxyHandler getHttpProxyHandler() { - return httpProxyHandler; - } - - public Socks4ProxyHandler getSocks4ProxyHandler() { - return socks4ProxyHandler; - } - - public Socks5ProxyHandler getSocks5ProxyHandler() { - return socks5ProxyHandler; - } -} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientChannelContextDefaults.java b/src/main/java/org/xbib/netty/http/client/HttpClientChannelContextDefaults.java deleted file mode 100644 index 0a2d23e..0000000 --- a/src/main/java/org/xbib/netty/http/client/HttpClientChannelContextDefaults.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client; - -import io.netty.channel.pool.ChannelPool; -import io.netty.handler.codec.http2.Http2SecurityUtil; -import io.netty.handler.ssl.CipherSuiteFilter; -import io.netty.handler.ssl.OpenSsl; -import io.netty.handler.ssl.SslProvider; -import io.netty.handler.ssl.SupportedCipherSuiteFilter; -import io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import io.netty.util.AttributeKey; -import org.xbib.netty.http.client.listener.CookieListener; -import org.xbib.netty.http.client.listener.ExceptionListener; -import org.xbib.netty.http.client.listener.HttpHeadersListener; -import org.xbib.netty.http.client.listener.HttpPushListener; -import org.xbib.netty.http.client.listener.HttpResponseListener; -import org.xbib.netty.http.client.util.ClientAuthMode; -import org.xbib.netty.http.client.util.InetAddressKey; - -import javax.net.ssl.TrustManagerFactory; - -/** - */ -public interface HttpClientChannelContextDefaults { - - AttributeKey CHANNEL_POOL_ATTRIBUTE_KEY = - AttributeKey.valueOf("httpClientChannelPool"); - - AttributeKey REQUEST_CONTEXT_ATTRIBUTE_KEY = - AttributeKey.valueOf("httpClientRequestContext"); - - AttributeKey RESPONSE_LISTENER_ATTRIBUTE_KEY = - AttributeKey.valueOf("httpClientResponseListener"); - - AttributeKey HEADER_LISTENER_ATTRIBUTE_KEY = - AttributeKey.valueOf("httpHeaderListener"); - - AttributeKey COOKIE_LISTENER_ATTRIBUTE_KEY = - AttributeKey.valueOf("cookieListener"); - - AttributeKey PUSH_LISTENER_ATTRIBUTE_KEY = - AttributeKey.valueOf("pushListener"); - - AttributeKey EXCEPTION_LISTENER_ATTRIBUTE_KEY = - AttributeKey.valueOf("httpClientExceptionListener"); - - /** - * Default for TCP_NODELAY. - */ - boolean DEFAULT_TCP_NODELAY = true; - - /** - * Default for SO_KEEPALIVE. - */ - boolean DEFAULT_SO_KEEPALIVE = true; - - /** - * Default for SO_REUSEADDR. - */ - boolean DEFAULT_SO_REUSEADDR = true; - - /** - * Set TCP send buffer to 64k per socket. - */ - int DEFAULT_TCP_SEND_BUFFER_SIZE = 64 * 1024; - - /** - * Set TCP receive buffer to 64k per socket. - */ - int DEFAULT_TCP_RECEIVE_BUFFER_SIZE = 64 * 1024; - - /** - * Set HTTP chunk maximum size to 8k. - * See {@link io.netty.handler.codec.http.HttpClientCodec}. - */ - int DEFAULT_MAX_CHUNK_SIZE = 8 * 1024; - - /** - * Set HTTP initial line length to 4k. - * See {@link io.netty.handler.codec.http.HttpClientCodec}. - */ - int DEFAULT_MAX_INITIAL_LINE_LENGTH = 4 * 1024; - - /** - * Set HTTP maximum headers size to 8k. - * See {@link io.netty.handler.codec.http.HttpClientCodec}. - */ - int DEFAULT_MAX_HEADERS_SIZE = 8 * 1024; - - /** - * Set maximum content length to 100 MB. - */ - int DEFAULT_MAX_CONTENT_LENGTH = 100 * 1024 * 1024; - - /** - * This is Netty's default. - * See {@link io.netty.handler.codec.MessageAggregator#DEFAULT_MAX_COMPOSITEBUFFER_COMPONENTS}. - */ - int DEFAULT_MAX_COMPOSITE_BUFFER_COMPONENTS = 1024; - - /** - * Allow maximum concurrent connections to an {@link InetAddressKey}. - * Usually, browsers restrict concurrent connections to 8 for a single address. - */ - int DEFAULT_MAX_CONNECTIONS = 8; - - /** - * Default read/write timeout in milliseconds. - */ - int DEFAULT_TIMEOUT_MILLIS = 5000; - - /** - * Default for gzip codec. - */ - boolean DEFAULT_ENABLE_GZIP = true; - - /** - * Default for HTTP/2 only. - */ - boolean DEFAULT_INSTALL_HTTP_UPGRADE2 = false; - - /** - * Default SSL provider. - */ - SslProvider DEFAULT_SSL_PROVIDER = OpenSsl.isAlpnSupported() ? SslProvider.OPENSSL : SslProvider.JDK; - - Iterable DEFAULT_CIPHERS = Http2SecurityUtil.CIPHERS; - - CipherSuiteFilter DEFAULT_CIPHER_SUITE_FILTER = SupportedCipherSuiteFilter.INSTANCE; - - TrustManagerFactory DEFAULT_TRUST_MANAGER_FACTORY = InsecureTrustManagerFactory.INSTANCE; - - boolean DEFAULT_USE_SERVER_NAME_IDENTIFICATION = true; - - /** - * Default for SSL client authentication. - */ - ClientAuthMode DEFAULT_SSL_CLIENT_AUTH_MODE = ClientAuthMode.NONE; -} diff --git a/src/main/java/org/xbib/netty/http/client/HttpClientRequestBuilder.java b/src/main/java/org/xbib/netty/http/client/HttpClientRequestBuilder.java deleted file mode 100644 index f7309d0..0000000 --- a/src/main/java/org/xbib/netty/http/client/HttpClientRequestBuilder.java +++ /dev/null @@ -1,450 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.UnpooledByteBufAllocator; -import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.DefaultHttpHeaders; -import io.netty.handler.codec.http.DefaultHttpRequest; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaderValues; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http.QueryStringDecoder; -import io.netty.handler.codec.http.QueryStringEncoder; -import io.netty.handler.codec.http.cookie.Cookie; -import io.netty.handler.codec.http2.HttpConversionUtil; -import io.netty.util.AsciiString; -import io.netty.util.CharsetUtil; -import org.xbib.netty.http.client.listener.CookieListener; -import org.xbib.netty.http.client.listener.ExceptionListener; -import org.xbib.netty.http.client.listener.HttpHeadersListener; -import org.xbib.netty.http.client.listener.HttpPushListener; -import org.xbib.netty.http.client.listener.HttpResponseListener; - -import java.io.IOException; -import java.net.URI; -import java.nio.charset.StandardCharsets; -import java.time.Instant; -import java.time.ZoneId; -import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Function; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * - */ -public class HttpClientRequestBuilder implements HttpRequestBuilder, HttpRequestDefaults { - - private static final Logger logger = Logger.getLogger(HttpClientRequestBuilder.class.getName()); - - private static final HttpVersion HTTP_2_0 = HttpVersion.valueOf("HTTP/2.0"); - - private final HttpClient httpClient; - - private final ByteBufAllocator byteBufAllocator; - - private final AtomicInteger streamId; - - private final DefaultHttpHeaders headers; - - private final List removeHeaders; - - private final Set cookies; - - private final HttpMethod httpMethod; - - private int timeout = DEFAULT_TIMEOUT_MILLIS; - - private HttpVersion httpVersion = DEFAULT_HTTP_VERSION; - - private String userAgent = DEFAULT_USER_AGENT; - - private boolean gzip = DEFAULT_GZIP; - - private boolean followRedirect = DEFAULT_FOLLOW_REDIRECT; - - private int maxRedirects = DEFAULT_MAX_REDIRECT; - - private URI uri = DEFAULT_URI; - - private QueryStringEncoder queryStringEncoder; - - private ByteBuf content; - - private HttpRequest httpRequest; - - private HttpRequestFuture httpRequestFuture = DEFAULT_FUTURE; - - private HttpRequestContext httpRequestContext; - - private HttpResponseListener httpResponseListener; - - private ExceptionListener exceptionListener; - - private HttpHeadersListener httpHeadersListener; - - private CookieListener cookieListener; - - private HttpPushListener httpPushListener; - - protected HttpClientRequestBuilder(HttpMethod httpMethod, - ByteBufAllocator byteBufAllocator, int streamId) { - this(null, httpMethod, byteBufAllocator, streamId); - } - - /** - * Construct HTTP client request builder. - * - * @param httpClient HTTP client - * @param httpMethod HTTP method - * @param byteBufAllocator byte buf allocator - */ - HttpClientRequestBuilder(HttpClient httpClient, HttpMethod httpMethod, - ByteBufAllocator byteBufAllocator, int streamId) { - this.httpClient = httpClient; - this.httpMethod = httpMethod; - this.byteBufAllocator = byteBufAllocator; - this.streamId = new AtomicInteger(streamId); - this.headers = new DefaultHttpHeaders(); - this.removeHeaders = new ArrayList<>(); - this.cookies = new HashSet<>(); - } - - public static HttpRequestBuilder builder(HttpMethod httpMethod) { - return new HttpClientRequestBuilder(httpMethod, UnpooledByteBufAllocator.DEFAULT, 3); - } - - public HttpRequestBuilder withFuture(HttpRequestFuture httpRequestFuture) { - this.httpRequestFuture = httpRequestFuture; - return this; - } - - @Override - public HttpRequestBuilder setHttp1() { - this.httpVersion = HttpVersion.HTTP_1_1; - return this; - } - - @Override - public HttpRequestBuilder setHttp2() { - this.httpVersion = HTTP_2_0; - return this; - } - - @Override - public HttpRequestBuilder setVersion(String httpVersion) { - this.httpVersion = HttpVersion.valueOf(httpVersion); - return this; - } - - @Override - public HttpRequestBuilder setTimeout(int timeout) { - this.timeout = timeout; - return this; - } - - @Override - public HttpRequestBuilder setURL(String url) { - this.uri = URI.create(url); - QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri, StandardCharsets.UTF_8); - this.queryStringEncoder = new QueryStringEncoder(queryStringDecoder.path()); - for (Map.Entry> entry : queryStringDecoder.parameters().entrySet()) { - for (String value : entry.getValue()) { - queryStringEncoder.addParam(entry.getKey(), value); - } - } - return this; - } - - @Override - public HttpRequestBuilder path(String path) { - if (this.uri != null) { - setURL(this.uri.resolve(path).toString()); - } else { - setURL(path); - } - return this; - } - - @Override - public HttpRequestBuilder addHeader(String name, Object value) { - headers.add(name, value); - return this; - } - - @Override - public HttpRequestBuilder setHeader(String name, Object value) { - headers.set(name, value); - return this; - } - - @Override - public HttpRequestBuilder removeHeader(String name) { - removeHeaders.add(name); - return this; - } - - @Override - public HttpRequestBuilder addParam(String name, String value) { - if (queryStringEncoder != null) { - queryStringEncoder.addParam(name, value); - } - return this; - } - - @Override - public HttpRequestBuilder addCookie(Cookie cookie) { - cookies.add(cookie); - return this; - } - - @Override - public HttpRequestBuilder contentType(String contentType) { - addHeader(HttpHeaderNames.CONTENT_TYPE, contentType); - return this; - } - - @Override - public HttpRequestBuilder acceptGzip(boolean gzip) { - this.gzip = gzip; - return this; - } - - @Override - public HttpRequestBuilder setFollowRedirect(boolean followRedirect) { - this.followRedirect = followRedirect; - return this; - } - - @Override - public HttpRequestBuilder setMaxRedirects(int maxRedirects) { - this.maxRedirects = maxRedirects; - return this; - } - - @Override - public HttpRequestBuilder setUserAgent(String userAgent) { - this.userAgent = userAgent; - return this; - } - - @Override - public HttpRequestBuilder text(String text) throws IOException { - content(text, HttpHeaderValues.TEXT_PLAIN); - return this; - } - - @Override - public HttpRequestBuilder json(String json) throws IOException { - content(json, HttpHeaderValues.APPLICATION_JSON); - return this; - } - - @Override - public HttpRequestBuilder xml(String xml) throws IOException { - content(xml, "application/xml"); - return this; - } - - @Override - public HttpRequestBuilder content(CharSequence charSequence, String contentType) throws IOException { - content(charSequence.toString().getBytes(CharsetUtil.UTF_8), AsciiString.of(contentType)); - return this; - } - - @Override - public HttpRequestBuilder content(byte[] buf, String contentType) throws IOException { - content(buf, AsciiString.of(contentType)); - return this; - } - - @Override - public HttpRequestBuilder content(ByteBuf body, String contentType) throws IOException { - content(body, AsciiString.of(contentType)); - return this; - } - - @Override - public HttpRequestBuilder onHeaders(HttpHeadersListener httpHeadersListener) { - this.httpHeadersListener = httpHeadersListener; - return this; - } - - @Override - public HttpRequestBuilder onCookie(CookieListener cookieListener) { - this.cookieListener = cookieListener; - return this; - } - - @Override - public HttpRequestBuilder onResponse(HttpResponseListener httpResponseListener) { - this.httpResponseListener = httpResponseListener; - return this; - } - - @Override - public HttpRequestBuilder onException(ExceptionListener exceptionListener) { - this.exceptionListener = exceptionListener; - return this; - } - - @Override - public HttpRequestBuilder onPushReceived(HttpPushListener httpPushListener) { - this.httpPushListener = httpPushListener; - return this; - } - - @Override - public HttpRequest build() { - if (uri == null) { - throw new IllegalStateException("URL not set"); - } - if (uri.getHost() == null) { - throw new IllegalStateException("URL host not set: " + uri); - } - DefaultHttpRequest httpRequest = createHttpRequest(); - String scheme = uri.getScheme(); - StringBuilder sb = new StringBuilder(uri.getHost()); - int defaultPort = "http".equals(scheme) ? 80 : "https".equals(scheme) ? 443 : -1; - if (defaultPort != -1 && uri.getPort() != -1 && defaultPort != uri.getPort()) { - sb.append(":").append(uri.getPort()); - } - if (httpVersion.majorVersion() == 2) { - httpRequest.headers().set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme); - } - String host = sb.toString(); - httpRequest.headers().add(HttpHeaderNames.HOST, host); - httpRequest.headers().add(HttpHeaderNames.DATE, - DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.ofInstant(Instant.now(), ZoneId.of("GMT")))); - if (userAgent != null) { - httpRequest.headers().add(HttpHeaderNames.USER_AGENT, userAgent); - } - if (gzip) { - httpRequest.headers().add(HttpHeaderNames.ACCEPT_ENCODING, "gzip"); - } - httpRequest.headers().setAll(headers); - if (!httpRequest.headers().contains(HttpHeaderNames.ACCEPT)) { - httpRequest.headers().add(HttpHeaderNames.ACCEPT, "*/*"); - } - // RFC 2616 Section 14.10 - // "An HTTP/1.1 client that does not support persistent connections MUST include the "close" connection - // option in every request message." - if (httpVersion.majorVersion() == 1 && !httpRequest.headers().contains(HttpHeaderNames.CONNECTION)) { - httpRequest.headers().add(HttpHeaderNames.CONNECTION, "close"); - } - // forced removal of headers, at last - for (String headerName : removeHeaders) { - httpRequest.headers().remove(headerName); - } - return httpRequest; - } - - @Override - public HttpRequestContext execute() { - return execute(httpClient); - } - - @Override - public HttpRequestContext execute(HttpClient httpClient) { - if (httpClient == null) { - return null; - } - if (httpRequest == null) { - httpRequest = build(); - } - if (httpResponseListener == null) { - httpResponseListener = httpRequestContext; - } - httpRequestContext = new HttpRequestContext(uri, httpRequest, - httpRequestFuture, - streamId, - timeout, System.currentTimeMillis(), - followRedirect, maxRedirects, new AtomicInteger(0), - httpResponseListener, - exceptionListener, - httpHeadersListener, - cookieListener, - httpPushListener); - // copy cookie(s) to context, will be added later to headers in dispatch (because of auto-cookie setting while redirect) - if (!cookies.isEmpty()) { - for (Cookie cookie : cookies) { - httpRequestContext.addCookie(cookie); - } - } - httpClient.dispatch(httpRequestContext); - return httpRequestContext; - } - - @Override - public CompletableFuture execute(Function supplier) { - final CompletableFuture completableFuture = new CompletableFuture<>(); - onResponse(response -> completableFuture.complete(supplier.apply(response))); - onException(completableFuture::completeExceptionally); - execute(); - return completableFuture; - } - - private DefaultHttpRequest createHttpRequest() { - String requestTarget = toOriginForm(); - logger.log(Level.FINE, () -> "origin form is " + requestTarget); - return content == null ? - new DefaultHttpRequest(httpVersion, httpMethod, requestTarget) : - new DefaultFullHttpRequest(httpVersion, httpMethod, requestTarget, content); - } - - private String toOriginForm() { - StringBuilder sb = new StringBuilder(); - String pathAndQuery = queryStringEncoder.toString(); - sb.append(pathAndQuery.isEmpty() ? "/" : pathAndQuery); - String ref = uri.getFragment(); - if (ref != null && !ref.isEmpty()) { - sb.append('#').append(ref); - } - return sb.toString(); - } - - private void addHeader(AsciiString name, Object value) { - headers.add(name, value); - } - - private void content(CharSequence charSequence, AsciiString contentType) throws IOException { - content(charSequence.toString().getBytes(CharsetUtil.UTF_8), contentType); - } - - private void content(byte[] buf, AsciiString contentType) throws IOException { - content(byteBufAllocator.buffer(buf.length).writeBytes(buf), contentType); - } - - private void content(ByteBuf body, AsciiString contentType) throws IOException { - this.content = body; - addHeader(HttpHeaderNames.CONTENT_LENGTH, (long) body.readableBytes()); - addHeader(HttpHeaderNames.CONTENT_TYPE, contentType); - } -} diff --git a/src/main/java/org/xbib/netty/http/client/HttpRequestBuilder.java b/src/main/java/org/xbib/netty/http/client/HttpRequestBuilder.java deleted file mode 100644 index 63d218c..0000000 --- a/src/main/java/org/xbib/netty/http/client/HttpRequestBuilder.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client; - -import io.netty.buffer.ByteBuf; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.cookie.Cookie; -import org.xbib.netty.http.client.listener.CookieListener; -import org.xbib.netty.http.client.listener.ExceptionListener; -import org.xbib.netty.http.client.listener.HttpHeadersListener; -import org.xbib.netty.http.client.listener.HttpPushListener; -import org.xbib.netty.http.client.listener.HttpResponseListener; - -import java.io.IOException; -import java.util.concurrent.CompletableFuture; -import java.util.function.Function; - -/** - */ -public interface HttpRequestBuilder { - - HttpRequestBuilder setHttp1(); - - HttpRequestBuilder setHttp2(); - - HttpRequestBuilder setVersion(String httpVersion); - - HttpRequestBuilder setURL(String url); - - HttpRequestBuilder path(String path); - - HttpRequestBuilder setHeader(String name, Object value); - - HttpRequestBuilder addHeader(String name, Object value); - - HttpRequestBuilder removeHeader(String name); - - HttpRequestBuilder addParam(String name, String value); - - HttpRequestBuilder addCookie(Cookie cookie); - - HttpRequestBuilder contentType(String contentType); - - HttpRequestBuilder acceptGzip(boolean gzip); - - HttpRequestBuilder setFollowRedirect(boolean followRedirect); - - HttpRequestBuilder setMaxRedirects(int maxRedirects); - - HttpRequestBuilder setUserAgent(String userAgent); - - HttpRequestBuilder content(CharSequence charSequence, String contentType) throws IOException; - - HttpRequestBuilder text(String text) throws IOException; - - HttpRequestBuilder json(String jsonText) throws IOException; - - HttpRequestBuilder xml(String xmlText) throws IOException; - - HttpRequestBuilder content(byte[] buf, String contentType) throws IOException; - - HttpRequestBuilder content(ByteBuf body, String contentType) throws IOException; - - HttpRequestBuilder onHeaders(HttpHeadersListener httpHeadersListener); - - HttpRequestBuilder onCookie(CookieListener cookieListener); - - HttpRequestBuilder onResponse(HttpResponseListener httpResponseListener); - - HttpRequestBuilder onException(ExceptionListener exceptionListener); - - HttpRequestBuilder onPushReceived(HttpPushListener httpPushListener); - - HttpRequestBuilder setTimeout(int timeout); - - HttpRequest build(); - - HttpRequestContext execute(); - - HttpRequestContext execute(HttpClient httpClient); - - CompletableFuture execute(Function supplier); -} diff --git a/src/main/java/org/xbib/netty/http/client/HttpRequestContext.java b/src/main/java/org/xbib/netty/http/client/HttpRequestContext.java deleted file mode 100755 index b588f42..0000000 --- a/src/main/java/org/xbib/netty/http/client/HttpRequestContext.java +++ /dev/null @@ -1,310 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client; - -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelPromise; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.cookie.Cookie; -import io.netty.handler.codec.http2.Http2Headers; -import io.netty.util.internal.PlatformDependent; -import org.xbib.netty.http.client.listener.CookieListener; -import org.xbib.netty.http.client.listener.ExceptionListener; -import org.xbib.netty.http.client.listener.HttpHeadersListener; -import org.xbib.netty.http.client.listener.HttpPushListener; -import org.xbib.netty.http.client.listener.HttpResponseListener; -import org.xbib.netty.http.client.util.LimitedHashSet; - -import java.net.URI; -import java.util.AbstractMap; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -/** - * - */ -public final class HttpRequestContext implements HttpResponseListener, HttpRequestDefaults { - - private static final Logger logger = Logger.getLogger(HttpRequestContext.class.getName()); - - private final URI uri; - - private final HttpRequest httpRequest; - - private final HttpRequestFuture httpRequestFuture; - - private final boolean followRedirect; - - private final int maxRedirects; - - private final AtomicInteger redirectCount; - - private final Integer timeout; - - private final Long startTime; - - private final AtomicInteger streamId; - - private final HttpResponseListener httpResponseListener; - - private final ExceptionListener exceptionListener; - - private final HttpHeadersListener httpHeadersListener; - - private final CookieListener cookieListener; - - private final HttpPushListener httpPushListener; - - private final Map> promiseMap; - - private final Map> pushMap; - - private ChannelPromise settingsPromise; - - private Collection cookies; - - private Map httpResponses; - - private Long stopTime; - - HttpRequestContext(URI uri, HttpRequest httpRequest, - HttpRequestFuture httpRequestFuture, - AtomicInteger streamId, - int timeout, Long startTime, - boolean followRedirect, int maxRedirects, AtomicInteger redirectCount, - HttpResponseListener httpResponseListener, - ExceptionListener exceptionListener, - HttpHeadersListener httpHeadersListener, - CookieListener cookieListener, - HttpPushListener httpPushListener) { - this.uri = uri; - this.httpRequest = httpRequest; - this.httpRequestFuture = httpRequestFuture; - this.streamId = streamId; - this.timeout = timeout; - this.startTime = startTime; - this.followRedirect = followRedirect; - this.maxRedirects = maxRedirects; - this.redirectCount = redirectCount; - this.httpResponseListener = httpResponseListener; - this.exceptionListener = exceptionListener; - this.httpHeadersListener = httpHeadersListener; - this.cookieListener = cookieListener; - this.httpPushListener = httpPushListener; - this.promiseMap = PlatformDependent.newConcurrentHashMap(); - this.pushMap = PlatformDependent.newConcurrentHashMap(); - this.cookies = new LimitedHashSet<>(10); - } - - /** - * A follow-up request to a given context with same stream ID (redirect). - * - */ - HttpRequestContext(URI uri, HttpRequest httpRequest, HttpRequestContext httpRequestContext) { - this.uri = uri; - this.httpRequest = httpRequest; - this.httpRequestFuture = httpRequestContext.httpRequestFuture; - this.streamId = httpRequestContext.streamId; - this.timeout = httpRequestContext.timeout; - this.startTime = httpRequestContext.startTime; - this.followRedirect = httpRequestContext.followRedirect; - this.maxRedirects = httpRequestContext.maxRedirects; - this.redirectCount = httpRequestContext.redirectCount; - this.httpResponseListener = httpRequestContext.httpResponseListener; - this.exceptionListener = httpRequestContext.exceptionListener; - this.httpHeadersListener = httpRequestContext.httpHeadersListener; - this.cookieListener = httpRequestContext.cookieListener; - this.httpPushListener = httpRequestContext.httpPushListener; - this.promiseMap = httpRequestContext.promiseMap; - this.pushMap = httpRequestContext.pushMap; - this.cookies = httpRequestContext.cookies; - } - - public URI getURI() { - return uri; - } - - public HttpRequest getHttpRequest() { - return httpRequest; - } - - public HttpResponseListener getHttpResponseListener() { - return httpResponseListener; - } - - public ExceptionListener getExceptionListener() { - return exceptionListener; - } - - public HttpHeadersListener getHttpHeadersListener() { - return httpHeadersListener; - } - - public CookieListener getCookieListener() { - return cookieListener; - } - - public HttpPushListener getHttpPushListener() { - return httpPushListener; - } - - public void setSettingsPromise(ChannelPromise settingsPromise) { - this.settingsPromise = settingsPromise; - } - - public ChannelPromise getSettingsPromise() { - return settingsPromise; - } - - public Map> getStreamIdPromiseMap() { - return promiseMap; - } - - public void putStreamID(Integer streamId, ChannelFuture channelFuture, ChannelPromise channelPromise) { - logger.log(Level.FINE, () -> "put stream ID " + streamId + " future = " + channelFuture); - promiseMap.put(streamId, new AbstractMap.SimpleEntry<>(channelFuture, channelPromise)); - } - - public Map> getPushMap() { - return pushMap; - } - - public void receiveStreamID(Integer streamId, Http2Headers headers, ChannelPromise channelPromise) { - logger.log(Level.FINE, () -> "receive stream ID " + streamId + " " + headers); - pushMap.put(streamId, new AbstractMap.SimpleEntry<>(headers, channelPromise)); - } - - public boolean isFinished() { - return promiseMap.isEmpty() && pushMap.isEmpty(); - } - - public void addCookie(Cookie cookie) { - cookies.add(cookie); - } - - public Collection getCookies() { - return cookies; - } - - public List matchCookies() { - return cookies.stream() - .filter(this::matchCookie) - .collect(Collectors.toList()); - } - - private boolean matchCookie(Cookie cookie) { - boolean domainMatch = cookie.domain() == null || uri.getHost().endsWith(cookie.domain()); - if (!domainMatch) { - return false; - } - boolean pathMatch = "/".equals(cookie.path()) || uri.getPath().startsWith(cookie.path()); - if (!pathMatch) { - return false; - } - boolean secure = "https".equals(uri.getScheme()); - return (secure && cookie.isSecure()) || (!secure && !cookie.isSecure()); - } - - public int getTimeout() { - return timeout; - } - - public long getStartTime() { - return startTime; - } - - public boolean isSucceeded() { - return httpRequestFuture.isSucceeded(); - } - - public boolean isFailed() { - return httpRequestFuture.isFailed(); - } - - public boolean isFollowRedirect() { - return followRedirect; - } - - public int getMaxRedirects() { - return maxRedirects; - } - - public AtomicInteger getRedirectCount() { - return redirectCount; - } - - public boolean isExpired() { - return timeout != null && System.currentTimeMillis() > startTime + timeout; - } - - public long took() { - return stopTime != null ? stopTime - startTime : -1L; - } - - public long remaining() { - return (startTime + timeout) - System.currentTimeMillis(); - } - - public AtomicInteger getStreamId() { - return streamId; - } - - public HttpRequestContext get() throws InterruptedException, TimeoutException, ExecutionException { - return get(DEFAULT_TIMEOUT_MILLIS, TimeUnit.SECONDS); - } - - public HttpRequestContext get(long timeout, TimeUnit timeUnit) - throws InterruptedException, TimeoutException, ExecutionException { - httpRequestFuture.get(timeout, timeUnit); - stopTime = System.currentTimeMillis(); - return this; - } - - public void success(String reason) { - logger.log(Level.FINE, () -> "success because of " + reason); - httpRequestFuture.success(reason); - } - - public void fail(String reason) { - fail(new IllegalStateException(reason)); - } - - public void fail(Exception exception) { - logger.log(Level.FINE, () -> "failed because of " + exception.getMessage()); - if (exceptionListener != null) { - exceptionListener.onException(exception); - } - httpRequestFuture.fail(exception); - } - - @Override - public void onResponse(FullHttpResponse fullHttpResponse) { - this.httpResponses.put(streamId.get(), fullHttpResponse); - } - - public Map getHttpResponses() { - return httpResponses; - } -} diff --git a/src/main/java/org/xbib/netty/http/client/HttpRequestDefaults.java b/src/main/java/org/xbib/netty/http/client/HttpRequestDefaults.java deleted file mode 100644 index 66a704e..0000000 --- a/src/main/java/org/xbib/netty/http/client/HttpRequestDefaults.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client; - -import io.netty.handler.codec.http.HttpVersion; -import org.xbib.netty.http.client.internal.HttpClientUserAgent; - -import java.net.URI; - -/** - */ -public interface HttpRequestDefaults { - - HttpVersion DEFAULT_HTTP_VERSION = HttpVersion.HTTP_1_1; - - String DEFAULT_USER_AGENT = HttpClientUserAgent.getUserAgent(); - - URI DEFAULT_URI = URI.create("http://localhost"); - - boolean DEFAULT_GZIP = true; - - boolean DEFAULT_FOLLOW_REDIRECT = true; - - int DEFAULT_TIMEOUT_MILLIS = 5000; - - int DEFAULT_MAX_REDIRECT = 10; - - HttpRequestFuture DEFAULT_FUTURE = new HttpRequestFuture<>(); -} diff --git a/src/main/java/org/xbib/netty/http/client/HttpRequestFuture.java b/src/main/java/org/xbib/netty/http/client/HttpRequestFuture.java deleted file mode 100644 index 43217eb..0000000 --- a/src/main/java/org/xbib/netty/http/client/HttpRequestFuture.java +++ /dev/null @@ -1,20 +0,0 @@ -package org.xbib.netty.http.client; - -import org.xbib.netty.http.client.util.AbstractFuture; - -/** - * A HTTP request future. - * - * @param the response type parameter. - */ -public class HttpRequestFuture extends AbstractFuture { - - public void success(V v) { - set(v); - } - - public void fail(Exception e) { - setException(e); - } - -} diff --git a/src/main/java/org/xbib/netty/http/client/Request.java b/src/main/java/org/xbib/netty/http/client/Request.java new file mode 100644 index 0000000..d4ab178 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/Request.java @@ -0,0 +1,216 @@ +package org.xbib.netty.http.client; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.cookie.Cookie; + +import org.xbib.net.URL; +import org.xbib.netty.http.client.listener.CookieListener; +import org.xbib.netty.http.client.listener.ExceptionListener; +import org.xbib.netty.http.client.listener.HttpHeadersListener; +import org.xbib.netty.http.client.listener.HttpPushListener; +import org.xbib.netty.http.client.listener.HttpResponseListener; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; + +/** + * + */ +public class Request { + + private final URL base; + + private final HttpVersion httpVersion; + + private final HttpMethod httpMethod; + + private final HttpHeaders headers; + + private final Collection cookies; + + private final String uri; + + private final ByteBuf content; + + private final int timeout; + + private final boolean followRedirect; + + private final int maxRedirects; + + private int redirectCount; + + private HttpResponseListener responseListener; + + private ExceptionListener exceptionListener; + + private HttpHeadersListener headersListener; + + private CookieListener cookieListener; + + private HttpPushListener pushListener; + + Request(URL url, HttpVersion httpVersion, HttpMethod httpMethod, + HttpHeaders headers, Collection cookies, + String uri, ByteBuf content, + int timeout, boolean followRedirect, int maxRedirect, int redirectCount) { + this.base = url; + this.httpVersion = httpVersion; + this.httpMethod = httpMethod; + this.headers = headers; + this.cookies = cookies; + this.uri = uri; + this.content = content; + this.timeout = timeout; + this.followRedirect = followRedirect; + this.maxRedirects = maxRedirect; + this.redirectCount = redirectCount; + } + + public URL base() { + return base; + } + + public HttpVersion httpVersion() { + return httpVersion; + } + + public HttpMethod httpMethod() { + return httpMethod; + } + + public String relativeUri() { + return uri; + } + + public HttpHeaders headers() { + return headers; + } + + public Collection cookies() { + return cookies; + } + + public ByteBuf content() { + return content; + } + + public int getTimeout() { + return timeout; + } + + public boolean isFollowRedirect() { + return followRedirect; + } + + public boolean checkRedirect() { + if (!followRedirect) { + return false; + } + if (redirectCount >= maxRedirects) { + return false; + } + redirectCount = redirectCount + 1; + return true; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("base=").append(base).append(',') + .append("version=").append(httpVersion).append(',') + .append("method=").append(httpMethod).append(',') + .append("relativeUri=").append(uri).append(',') + .append("headers=").append(headers).append(',') + .append("content=").append(content != null ? content.copy(0,16).toString(StandardCharsets.UTF_8) : ""); + return sb.toString(); + } + + public Request setHeadersListener(HttpHeadersListener httpHeadersListener) { + this.headersListener = httpHeadersListener; + return this; + } + + public HttpHeadersListener getHeadersListener() { + return headersListener; + } + + public Request setCookieListener(CookieListener cookieListener) { + this.cookieListener = cookieListener; + return this; + } + + public CookieListener getCookieListener() { + return cookieListener; + } + + public Request setResponseListener(HttpResponseListener httpResponseListener) { + this.responseListener = httpResponseListener; + return this; + } + + public HttpResponseListener getResponseListener() { + return responseListener; + } + + public Request setExceptionListener(ExceptionListener exceptionListener) { + this.exceptionListener = exceptionListener; + return this; + } + + public ExceptionListener getExceptionListener() { + return exceptionListener; + } + + public Request setPushListener(HttpPushListener httpPushListener) { + this.pushListener = httpPushListener; + return this; + } + + public HttpPushListener getPushListener() { + return pushListener; + } + + public static RequestBuilder get() { + return builder(HttpMethod.GET); + } + + public static RequestBuilder put() { + return builder(HttpMethod.PUT); + } + + public static RequestBuilder post() { + return builder(HttpMethod.POST); + } + + public static RequestBuilder delete() { + return builder(HttpMethod.DELETE); + } + + public static RequestBuilder head() { + return builder(HttpMethod.HEAD); + } + + public static RequestBuilder patch() { + return builder(HttpMethod.PATCH); + } + + public static RequestBuilder trace() { + return builder(HttpMethod.TRACE); + } + + public static RequestBuilder options() { + return builder(HttpMethod.OPTIONS); + } + + public static RequestBuilder connect() { + return builder(HttpMethod.CONNECT); + } + + public static RequestBuilder builder(HttpMethod httpMethod) { + return new RequestBuilder().setMethod(httpMethod); + } +} diff --git a/src/main/java/org/xbib/netty/http/client/RequestBuilder.java b/src/main/java/org/xbib/netty/http/client/RequestBuilder.java new file mode 100644 index 0000000..c4ec99b --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/RequestBuilder.java @@ -0,0 +1,329 @@ +package org.xbib.netty.http.client; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.PooledByteBufAllocator; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http.QueryStringDecoder; +import io.netty.handler.codec.http.QueryStringEncoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http2.HttpConversionUtil; +import io.netty.util.AsciiString; +import org.xbib.net.URL; +import org.xbib.net.URLSyntaxException; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +public class RequestBuilder { + + private static final HttpMethod DEFAULT_METHOD = HttpMethod.GET; + + private static final HttpVersion DEFAULT_HTTP_VERSION = HttpVersion.HTTP_1_1; + + private static final String DEFAULT_USER_AGENT = UserAgent.getUserAgent(); + + private static final URL DEFAULT_URL = URL.from("http://localhost"); + + private static final boolean DEFAULT_GZIP = true; + + private static final boolean DEFAULT_KEEPALIVE = true; + + private static final boolean DEFAULT_FOLLOW_REDIRECT = true; + + private static final int DEFAULT_TIMEOUT_MILLIS = 5000; + + private static final int DEFAULT_MAX_REDIRECT = 10; + + private static final HttpVersion HTTP_2_0 = HttpVersion.valueOf("HTTP/2.0"); + + private final List removeHeaders; + + private final Collection cookies; + + private HttpMethod httpMethod; + + private HttpHeaders headers; + + private HttpVersion httpVersion; + + private String userAgent; + + private boolean keepalive; + + private boolean gzip; + + private URL url; + + private QueryStringEncoder queryStringEncoder; + + private ByteBuf content; + + private int timeout; + + private boolean followRedirect; + + private int maxRedirects; + + RequestBuilder() { + httpMethod = DEFAULT_METHOD; + httpVersion = DEFAULT_HTTP_VERSION; + userAgent = DEFAULT_USER_AGENT; + gzip = DEFAULT_GZIP; + keepalive = DEFAULT_KEEPALIVE; + url = DEFAULT_URL; + timeout = DEFAULT_TIMEOUT_MILLIS; + followRedirect = DEFAULT_FOLLOW_REDIRECT; + maxRedirects = DEFAULT_MAX_REDIRECT; + headers = new DefaultHttpHeaders(); + removeHeaders = new ArrayList<>(); + cookies = new HashSet<>(); + } + + public RequestBuilder setMethod(HttpMethod httpMethod) { + this.httpMethod = httpMethod; + return this; + } + + public RequestBuilder setHttp1() { + this.httpVersion = HttpVersion.HTTP_1_1; + return this; + } + + public RequestBuilder setHttp2() { + this.httpVersion = HTTP_2_0; + return this; + } + + public RequestBuilder setVersion(HttpVersion httpVersion) { + this.httpVersion = httpVersion; + return this; + } + + public RequestBuilder setVersion(String httpVersion) { + this.httpVersion = HttpVersion.valueOf(httpVersion); + return this; + } + + public RequestBuilder setTimeout(int timeout) { + this.timeout = timeout; + return this; + } + + public RequestBuilder setURL(String url) { + return setURL(URL.from(url)); + } + + public RequestBuilder setURL(URL url) { + this.url = url; + QueryStringDecoder queryStringDecoder = new QueryStringDecoder(URI.create(url.toString()), StandardCharsets.UTF_8); + this.queryStringEncoder = new QueryStringEncoder(queryStringDecoder.path()); + for (Map.Entry> entry : queryStringDecoder.parameters().entrySet()) { + for (String value : entry.getValue()) { + queryStringEncoder.addParam(entry.getKey(), value); + } + } + return this; + } + + public RequestBuilder path(String path) { + if (this.url != null) { + try { + setURL(URL.base(url).resolve(path).toString()); + } catch (URLSyntaxException e) { + throw new IllegalArgumentException(e); + } + } else { + setURL(path); + } + return this; + } + + public RequestBuilder setHeaders(HttpHeaders headers) { + this.headers = headers; + return this; + } + + public RequestBuilder addHeader(String name, Object value) { + this.headers.add(name, value); + return this; + } + + public RequestBuilder setHeader(String name, Object value) { + this.headers.set(name, value); + return this; + } + + public RequestBuilder removeHeader(String name) { + removeHeaders.add(name); + return this; + } + + public RequestBuilder addParam(String name, String value) { + if (queryStringEncoder != null) { + queryStringEncoder.addParam(name, value); + } + return this; + } + + public RequestBuilder addCookie(Cookie cookie) { + cookies.add(cookie); + return this; + } + + public RequestBuilder contentType(String contentType) { + addHeader(HttpHeaderNames.CONTENT_TYPE, contentType); + return this; + } + + public RequestBuilder acceptGzip(boolean gzip) { + this.gzip = gzip; + return this; + } + + public RequestBuilder keepAlive(boolean keepalive) { + this.keepalive = keepalive; + return this; + } + + public RequestBuilder setFollowRedirect(boolean followRedirect) { + this.followRedirect = followRedirect; + return this; + } + + public RequestBuilder setMaxRedirects(int maxRedirects) { + this.maxRedirects = maxRedirects; + return this; + } + + public RequestBuilder setUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + public RequestBuilder setContent(ByteBuf byteBuf) { + this.content = byteBuf; + return this; + } + + public RequestBuilder text(String text) { + content(text, HttpHeaderValues.TEXT_PLAIN); + return this; + } + + public RequestBuilder json(String json) { + content(json, HttpHeaderValues.APPLICATION_JSON); + return this; + } + + public RequestBuilder xml(String xml) { + content(xml, "application/xml"); + return this; + } + + public RequestBuilder content(CharSequence charSequence, String contentType) { + content(charSequence.toString().getBytes(StandardCharsets.UTF_8), AsciiString.of(contentType)); + return this; + } + + public RequestBuilder content(byte[] buf, String contentType) { + content(buf, AsciiString.of(contentType)); + return this; + } + + public RequestBuilder content(ByteBuf body, String contentType) { + content(body, AsciiString.of(contentType)); + return this; + } + + public Request build() { + if (url == null) { + throw new IllegalStateException("URL not set"); + } + if (url.getHost() == null) { + throw new IllegalStateException("URL host not set: " + url); + } + DefaultHttpHeaders validatedHeaders = new DefaultHttpHeaders(true); + validatedHeaders.set(headers); + String scheme = url.getScheme(); + if (httpVersion.majorVersion() == 2) { + validatedHeaders.set(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme); + } + validatedHeaders.set(HttpHeaderNames.HOST, url.getHostInfo()); + validatedHeaders.set(HttpHeaderNames.DATE, DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC))); + if (userAgent != null) { + validatedHeaders.set(HttpHeaderNames.USER_AGENT, userAgent); + } + if (gzip) { + validatedHeaders.set(HttpHeaderNames.ACCEPT_ENCODING, "gzip"); + } + int length = content != null ? content.capacity() : 0; + if (!validatedHeaders.contains(HttpHeaderNames.CONTENT_LENGTH) && !validatedHeaders.contains(HttpHeaderNames.TRANSFER_ENCODING)) { + if (length < 0) { + validatedHeaders.set(HttpHeaderNames.TRANSFER_ENCODING, "chunked"); + } else { + validatedHeaders.set(HttpHeaderNames.CONTENT_LENGTH, Long.toString(length)); + } + } + if (!validatedHeaders.contains(HttpHeaderNames.ACCEPT)) { + validatedHeaders.set(HttpHeaderNames.ACCEPT, "*/*"); + } + // RFC 2616 Section 14.10 + // "An HTTP/1.1 client that does not support persistent connections MUST include the "close" connection + // option in every request message." + if (httpVersion.majorVersion() == 1 && !keepalive) { + validatedHeaders.set(HttpHeaderNames.CONNECTION, "close"); + } + // at last, forced removal of unwanted headers + for (String headerName : removeHeaders) { + validatedHeaders.remove(headerName); + } + // create origin form from query string encoder + String uri = toOriginForm(); + return new Request(url, httpVersion, httpMethod, validatedHeaders, cookies, uri, content, + timeout, followRedirect, maxRedirects, 0); + } + + private String toOriginForm() { + StringBuilder sb = new StringBuilder(); + String pathAndQuery = queryStringEncoder.toString(); + sb.append(pathAndQuery.isEmpty() ? "/" : pathAndQuery); + String ref = url.getFragment(); + if (ref != null && !ref.isEmpty()) { + sb.append('#').append(ref); + } + return sb.toString(); + } + + private void addHeader(AsciiString name, Object value) { + if (!headers.contains(name)) { + headers.add(name, value); + } + } + + private void content(CharSequence charSequence, AsciiString contentType) { + content(charSequence.toString().getBytes(StandardCharsets.UTF_8), contentType); + } + + private void content(byte[] buf, AsciiString contentType) { + content(PooledByteBufAllocator.DEFAULT.buffer(buf.length).writeBytes(buf), contentType); + } + + private void content(ByteBuf body, AsciiString contentType) { + this.content = body; + addHeader(HttpHeaderNames.CONTENT_LENGTH, (long) body.readableBytes()); + addHeader(HttpHeaderNames.CONTENT_TYPE, contentType); + } +} diff --git a/src/main/java/org/xbib/netty/http/client/internal/HttpClientUserAgent.java b/src/main/java/org/xbib/netty/http/client/UserAgent.java similarity index 50% rename from src/main/java/org/xbib/netty/http/client/internal/HttpClientUserAgent.java rename to src/main/java/org/xbib/netty/http/client/UserAgent.java index abf74d7..f9833de 100644 --- a/src/main/java/org/xbib/netty/http/client/internal/HttpClientUserAgent.java +++ b/src/main/java/org/xbib/netty/http/client/UserAgent.java @@ -1,36 +1,21 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.internal; +package org.xbib.netty.http.client; import io.netty.bootstrap.Bootstrap; -import org.xbib.netty.http.client.HttpClient; import java.util.Optional; /** + * HTTP client user agent. */ -public final class HttpClientUserAgent { +public final class UserAgent { /** - * The default valut for {@code User-Agent}. + * The default value for {@code User-Agent}. */ private static final String USER_AGENT = String.format("XbibHttpClient/%s (Java/%s/%s) (Netty/%s)", httpClientVersion(), javaVendor(), javaVersion(), nettyVersion()); - private HttpClientUserAgent() { + private UserAgent() { } public static String getUserAgent() { @@ -38,7 +23,7 @@ public final class HttpClientUserAgent { } private static String httpClientVersion() { - return Optional.ofNullable(HttpClient.class.getPackage().getImplementationVersion()) + return Optional.ofNullable(UserAgent.class.getPackage().getImplementationVersion()) .orElse("unknown"); } diff --git a/src/main/java/org/xbib/netty/http/client/handler/Http2EventHandler.java b/src/main/java/org/xbib/netty/http/client/handler/Http2EventHandler.java deleted file mode 100644 index 215f461..0000000 --- a/src/main/java/org/xbib/netty/http/client/handler/Http2EventHandler.java +++ /dev/null @@ -1,410 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.handler; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.Channel; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; -import io.netty.handler.codec.http.FullHttpMessage; -import io.netty.handler.codec.http.FullHttpRequest; -import io.netty.handler.codec.http.HttpRequest; -import io.netty.handler.codec.http.HttpUtil; -import io.netty.handler.codec.http2.DefaultHttp2Headers; -import io.netty.handler.codec.http2.Http2CodecUtil; -import io.netty.handler.codec.http2.Http2Connection; -import io.netty.handler.codec.http2.Http2ConnectionHandler; -import io.netty.handler.codec.http2.Http2Error; -import io.netty.handler.codec.http2.Http2EventAdapter; -import io.netty.handler.codec.http2.Http2Exception; -import io.netty.handler.codec.http2.Http2Headers; -import io.netty.handler.codec.http2.Http2LocalFlowController; -import io.netty.handler.codec.http2.Http2Settings; -import io.netty.handler.codec.http2.Http2Stream; -import io.netty.handler.codec.http2.HttpConversionUtil; -import org.xbib.netty.http.client.HttpClientChannelContextDefaults; -import org.xbib.netty.http.client.HttpRequestContext; - -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * A HTTP/2 event adapter for a client. - * This event adapter expects {@link Http2Settings} are sent from the server before the - * {@link HttpRequest} is submitted by sending a header frame, and, if a body exists, a - * data frame. - * The push promises of a server response are acknowledged and the headers of a push promise - * are stored in the {@link HttpRequestContext} for being received later. - */ -public class Http2EventHandler extends Http2EventAdapter { - - private static final Logger logger = Logger.getLogger(Http2EventHandler.class.getName()); - - private final Http2Connection connection; - - private final Http2Connection.PropertyKey messageKey; - - private final int maxContentLength; - - private final boolean validateHttpHeaders; - - /** - * Constructor for {@link Http2EventHandler}. - * @param connection the HTTP/2 connection - * @param maxContentLength the maximum content length - * @param validateHeaders true if headers should be validated - */ - public Http2EventHandler(Http2Connection connection, int maxContentLength, boolean validateHeaders) { - this.connection = connection; - this.maxContentLength = maxContentLength; - this.validateHttpHeaders = validateHeaders; - this.messageKey = connection.newKey(); - } - - /** - * Handles an inbound {@code SETTINGS} frame. - * After frame is received, the request is sent. - * - * @param ctx the context from the handler where the frame was read. - * @param settings the settings received from the remote endpoint. - */ - @Override - public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) - throws Http2Exception { - logger.log(Level.FINEST, () -> "settings received " + settings); - Channel channel = ctx.channel(); - final HttpRequestContext httpRequestContext = - channel.attr(HttpClientChannelContextDefaults.REQUEST_CONTEXT_ATTRIBUTE_KEY).get(); - final HttpRequest httpRequest = httpRequestContext.getHttpRequest(); - ChannelPromise channelPromise = channel.newPromise(); - Http2Headers headers = toHttp2Headers(httpRequestContext); - logger.log(Level.FINEST, () -> "write request " + httpRequest + " headers = " + headers); - boolean hasBody = httpRequestContext.getHttpRequest() instanceof FullHttpRequest; - Http2ConnectionHandler handler = ctx.pipeline().get(Http2ConnectionHandler.class); - Integer streamId = httpRequestContext.getStreamId().get(); - ChannelFuture channelFuture = handler.encoder().writeHeaders(ctx, streamId, - headers, 0, !hasBody, channelPromise); - httpRequestContext.putStreamID(streamId, channelFuture, channelPromise); - if (hasBody) { - FullHttpRequest fullHttpRequest = (FullHttpRequest) httpRequestContext.getHttpRequest(); - ChannelPromise contentChannelPromise = channel.newPromise(); - streamId = httpRequestContext.getStreamId().get(); - ChannelFuture contentChannelFuture = handler.encoder().writeData(ctx, streamId, - fullHttpRequest.content(), 0, true, contentChannelPromise); - httpRequestContext.putStreamID(streamId, contentChannelFuture, contentChannelPromise); - channel.flush(); - } - httpRequestContext.getSettingsPromise().setSuccess(); - } - - /** - * Handles an inbound {@code HEADERS} frame. - *

- * Only one of the following methods will be called for each {@code HEADERS} frame sequence. - * One will be called when the {@code END_HEADERS} flag has been received. - *

    - *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}
  • - *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}
  • - *
  • {@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}
  • - *
- *

- * To say it another way; the {@link Http2Headers} will contain all of the headers - * for the current message exchange step (additional queuing is not necessary). - * - * @param ctx the context from the handler where the frame was read. - * @param streamId the subject stream for the frame. - * @param headers the received headers. - * @param padding additional bytes that should be added to obscure the true content size. Must be between 0 and - * 256 (inclusive). - * @param endOfStream Indicates whether this is the last frame to be sent from the remote endpoint - * for this stream. - */ - @Override - public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding, - boolean endOfStream) throws Http2Exception { - logger.log(Level.FINEST, () -> "headers received " + headers); - Http2Stream stream = connection.stream(streamId); - FullHttpMessage msg = beginHeader(ctx, stream, headers, true, true); - if (msg != null) { - endHeader(ctx, stream, msg, endOfStream); - } - } - - /** - * Handles an inbound {@code HEADERS} frame with priority information specified. - * Only called if {@code END_HEADERS} encountered. - *

- * Only one of the following methods will be called for each {@code HEADERS} frame sequence. - * One will be called when the {@code END_HEADERS} flag has been received. - *

    - *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}
  • - *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}
  • - *
  • {@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}
  • - *
- *

- * To say it another way; the {@link Http2Headers} will contain all of the headers - * for the current message exchange step (additional queuing is not necessary). - * - * @param ctx the context from the handler where the frame was read. - * @param streamId the subject stream for the frame. - * @param headers the received headers. - * @param streamDependency the stream on which this stream depends, or 0 if dependent on the - * connection. - * @param weight the new weight for the stream. - * @param exclusive whether or not the stream should be the exclusive dependent of its parent. - * @param padding additional bytes that should be added to obscure the true content size. Must be between 0 and - * 256 (inclusive). - * @param endOfStream Indicates whether this is the last frame to be sent from the remote endpoint - * for this stream. - */ - @Override - public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency, - short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception { - logger.log(Level.FINEST, () -> "headers received " + headers); - Http2Stream stream = connection.stream(streamId); - FullHttpMessage msg = beginHeader(ctx, stream, headers, true, true); - if (msg != null) { - if (streamDependency != Http2CodecUtil.CONNECTION_STREAM_ID) { - msg.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), - streamDependency); - } - msg.headers().setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), weight); - endHeader(ctx, stream, msg, endOfStream); - } - } - - /** - * Handles an inbound {@code DATA} frame. - * - * @param ctx the context from the handler where the frame was read. - * @param streamId the subject stream for the frame. - * @param data payload buffer for the frame. This buffer will be released by the codec. - * @param padding additional bytes that should be added to obscure the true content size. Must be between 0 and - * 256 (inclusive). - * @param endOfStream Indicates whether this is the last frame to be sent from the remote endpoint for this stream. - * @return the number of bytes that have been processed by the application. The returned bytes are used by the - * inbound flow controller to determine the appropriate time to expand the inbound flow control window (i.e. send - * {@code WINDOW_UPDATE}). Returning a value equal to the length of {@code data} + {@code padding} will effectively - * opt-out of application-level flow control for this frame. Returning a value less than the length of {@code data} - * + {@code padding} will defer the returning of the processed bytes, which the application must later return via - * {@link Http2LocalFlowController#consumeBytes(Http2Stream, int)}. The returned value must - * be >= {@code 0} and <= {@code data.readableBytes()} + {@code padding}. - */ - @Override - public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) - throws Http2Exception { - logger.log(Level.FINEST, () -> "data received " + data); - Http2Stream stream = connection.stream(streamId); - FullHttpMessage msg = getMessage(stream); - if (msg == null) { - throw Http2Exception.connectionError(Http2Error.PROTOCOL_ERROR, - "data frame received for unknown stream id %d", streamId); - } - ByteBuf content = msg.content(); - final int dataReadableBytes = data.readableBytes(); - if (content.readableBytes() > maxContentLength - dataReadableBytes) { - throw Http2Exception.connectionError(Http2Error.INTERNAL_ERROR, - "content length exceeded maximum of %d for stream id %d", maxContentLength, streamId); - } - content.writeBytes(data, data.readerIndex(), dataReadableBytes); - if (endOfStream) { - fireChannelRead(ctx, msg, false, stream); - } - return dataReadableBytes + padding; - } - - /** - * Handles an inbound {@code RST_STREAM} frame. Deletes push stream id if present. - * - * @param ctx the context from the handler where the frame was read. - * @param streamId the stream that is terminating. - * @param errorCode the error code identifying the type of failure. - */ - @Override - public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception { - logger.log(Level.FINEST, () -> "rst stream received: error code = " + errorCode); - Http2Stream stream = connection.stream(streamId); - FullHttpMessage msg = getMessage(stream); - if (msg != null) { - removeMessage(stream, true); - } - final HttpRequestContext httpRequestContext = - ctx.channel().attr(HttpClientChannelContextDefaults.REQUEST_CONTEXT_ATTRIBUTE_KEY).get(); - httpRequestContext.getPushMap().remove(streamId); - } - - /** - * Handles an inbound {@code PUSH_PROMISE} frame. Only called if {@code END_HEADERS} encountered. - *

- * Promised requests MUST be authoritative, cacheable, and safe. - * See [RFC http2], Section 8.2. - *

- * Only one of the following methods will be called for each {@code HEADERS} frame sequence. - * One will be called when the {@code END_HEADERS} flag has been received. - *

    - *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}
  • - *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}
  • - *
  • {@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}
  • - *
- *

- * To say it another way; the {@link Http2Headers} will contain all of the headers - * for the current message exchange step (additional queuing is not necessary). - * - * @param ctx the context from the handler where the frame was read. - * @param streamId the stream the frame was sent on. - * @param promisedStreamId the ID of the promised stream. - * @param headers the received headers. - * @param padding additional bytes that should be added to obscure the true content size. Must be between 0 and - * 256 (inclusive). - */ - @Override - public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, - Http2Headers headers, int padding) throws Http2Exception { - logger.log(Level.FINEST, () -> "push promise received: streamId " + streamId + - " promised stream ID = " + promisedStreamId + " headers =" + headers); - Http2Stream promisedStream = connection.stream(promisedStreamId); - FullHttpMessage msg = beginHeader(ctx, promisedStream, headers, false, false); - if (msg != null) { - msg.headers().setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), streamId); - msg.headers().setShort(HttpConversionUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), - Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT); - endHeader(ctx, promisedStream, msg, false); - } - Channel channel = ctx.channel(); - final HttpRequestContext httpRequestContext = - channel.attr(HttpClientChannelContextDefaults.REQUEST_CONTEXT_ATTRIBUTE_KEY).get(); - httpRequestContext.receiveStreamID(promisedStreamId, headers, channel.newPromise()); - } - - /** - * Notifies the listener that the given stream has now been removed from the connection and - * will no longer be returned via {@link Http2Connection#stream(int)}. The connection may - * maintain inactive streams for some time before removing them. - *

- * If a {@link RuntimeException} is thrown it will be logged and not propagated. - * Throwing from this method is not supported and is considered a programming error. - */ - @Override - public void onStreamRemoved(Http2Stream stream) { - logger.log(Level.FINEST, () -> "stream removed " + stream); - removeMessage(stream, true); - } - - /** - * Create a new {@link FullHttpMessage} based upon the current connection parameters. - * - * @param stream The stream to create a message for - * @param headers The headers associated with {@code stream} - * @param validateHttpHeaders - *

    - *
  • {@code true} to validate HTTP headers in the http-codec
  • - *
  • {@code false} not to validate HTTP headers in the http-codec
  • - *
- * @param alloc The {@link ByteBufAllocator} to use to generate the content of the message - * @throws Http2Exception if message can not be created - */ - private FullHttpMessage newMessage(Http2Stream stream, Http2Headers headers, boolean validateHttpHeaders, - ByteBufAllocator alloc) throws Http2Exception { - if (headers.status() != null) { - return HttpConversionUtil.toHttpResponse(stream.id(), headers, alloc, validateHttpHeaders); - } else { - return null; - } - } - - /** - * Get the {@link FullHttpMessage} associated with {@code stream}. - * @param stream The stream to get the associated state from - * @return The {@link FullHttpMessage} associated with {@code stream}. - */ - private FullHttpMessage getMessage(Http2Stream stream) { - return (FullHttpMessage) stream.getProperty(messageKey); - } - - /** - * Make {@code message} be the state associated with {@code stream}. - * @param stream The stream which {@code message} is associated with. - * @param message The message which contains the HTTP semantics. - */ - private void putMessage(Http2Stream stream, FullHttpMessage message) { - FullHttpMessage previous = stream.setProperty(messageKey, message); - if (previous != message && previous != null) { - previous.release(); - } - } - /** - * The stream is out of scope for the HTTP message flow and will no longer be tracked. - * @param stream The stream to remove associated state with - * @param release {@code true} to call release on the value if it is present. {@code false} to not call release. - */ - private void removeMessage(Http2Stream stream, boolean release) { - FullHttpMessage msg = stream.removeProperty(messageKey); - if (release && msg != null) { - msg.release(); - } - } - - /** - * Set final headers and fire a channel read event. - * - * @param ctx The context to fire the event on - * @param msg The message to send - * @param release {@code true} to call release on the value if it is present. {@code false} to not call release. - * @param stream the stream of the message which is being fired - */ - private void fireChannelRead(ChannelHandlerContext ctx, FullHttpMessage msg, boolean release, - Http2Stream stream) { - removeMessage(stream, release); - HttpUtil.setContentLength(msg, msg.content().readableBytes()); - ctx.fireChannelRead(msg); - } - - private FullHttpMessage beginHeader(ChannelHandlerContext ctx, Http2Stream stream, Http2Headers headers, - boolean allowAppend, boolean appendToTrailer) throws Http2Exception { - FullHttpMessage msg = getMessage(stream); - if (msg == null) { - msg = newMessage(stream, headers, validateHttpHeaders, ctx.alloc()); - } else { - if (allowAppend) { - HttpConversionUtil.addHttp2ToHttpHeaders(stream.id(), headers, msg, appendToTrailer); - } else { - throw new Http2Exception(Http2Error.PROTOCOL_ERROR, "stream already exists"); - } - } - return msg; - } - - private void endHeader(ChannelHandlerContext ctx, Http2Stream stream, FullHttpMessage msg, boolean endOfStream) { - if (endOfStream) { - fireChannelRead(ctx, msg, getMessage(stream) != msg, stream); - } else { - putMessage(stream, msg); - } - } - - private static Http2Headers toHttp2Headers(HttpRequestContext httpRequestContext) { - HttpRequest httpRequest = httpRequestContext.getHttpRequest(); - Http2Headers headers = new DefaultHttp2Headers() - .method(httpRequest.method().asciiName()) - .path(httpRequest.uri()) - .scheme(httpRequestContext.getURI().getScheme()) - .authority(httpRequestContext.getURI().getHost()); - HttpConversionUtil.toHttp2Headers(httpRequest.headers(), headers); - return headers; - } -} diff --git a/src/main/java/org/xbib/netty/http/client/handler/Http2NegotiationHandler.java b/src/main/java/org/xbib/netty/http/client/handler/Http2NegotiationHandler.java deleted file mode 100644 index 2e2c446..0000000 --- a/src/main/java/org/xbib/netty/http/client/handler/Http2NegotiationHandler.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.handler; - -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPipeline; -import io.netty.handler.codec.http.HttpClientCodec; -import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler; -import io.netty.handler.ssl.ApplicationProtocolNames; -import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; - -import java.util.logging.Level; -import java.util.logging.Logger; - -import static org.xbib.netty.http.client.handler.HttpClientChannelInitializer.configureHttp1Pipeline; -import static org.xbib.netty.http.client.handler.HttpClientChannelInitializer.configureHttp2Pipeline; -import static org.xbib.netty.http.client.handler.HttpClientChannelInitializer.createHttp1ConnectionHandler; -import static org.xbib.netty.http.client.handler.HttpClientChannelInitializer.createHttp2ConnectionHandler; - -/** - * - */ -class Http2NegotiationHandler extends ApplicationProtocolNegotiationHandler { - - private static final Logger logger = Logger.getLogger(Http2NegotiationHandler.class.getName()); - - private final HttpClientChannelInitializer initializer; - - Http2NegotiationHandler(String fallbackProtocol, HttpClientChannelInitializer initializer) { - super(fallbackProtocol); - this.initializer = initializer; - } - - @Override - protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception { - ChannelPipeline pipeline = ctx.pipeline(); - if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { - pipeline.addLast(createHttp2ConnectionHandler(initializer.getContext())); - configureHttp2Pipeline(pipeline, initializer.getHttp2ResponseHandler()); - logger.log(Level.FINE, () -> "negotiated HTTP/2: handler = " + pipeline.names()); - return; - } - if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { - pipeline.addLast(createHttp1ConnectionHandler(initializer.getContext())); - configureHttp1Pipeline(pipeline, initializer.getContext(), initializer.getHttpHandler()); - logger.log(Level.FINE, () -> "negotiated HTTP/1.1: handler = " + pipeline.names()); - return; - } - ctx.close(); - throw new IllegalStateException("unexpected protocol: " + protocol); - } -} diff --git a/src/main/java/org/xbib/netty/http/client/handler/Http2ResponseHandler.java b/src/main/java/org/xbib/netty/http/client/handler/Http2ResponseHandler.java deleted file mode 100644 index aeae063..0000000 --- a/src/main/java/org/xbib/netty/http/client/handler/Http2ResponseHandler.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.handler; - -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPromise; -import io.netty.channel.SimpleChannelInboundHandler; -import io.netty.channel.pool.ChannelPool; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.cookie.ClientCookieDecoder; -import io.netty.handler.codec.http.cookie.Cookie; -import io.netty.handler.codec.http2.Http2Headers; -import io.netty.handler.codec.http2.HttpConversionUtil; -import org.xbib.netty.http.client.HttpClient; -import org.xbib.netty.http.client.HttpClientChannelContextDefaults; -import org.xbib.netty.http.client.HttpRequestContext; -import org.xbib.netty.http.client.listener.CookieListener; -import org.xbib.netty.http.client.listener.ExceptionListener; -import org.xbib.netty.http.client.listener.HttpHeadersListener; -import org.xbib.netty.http.client.listener.HttpPushListener; -import org.xbib.netty.http.client.listener.HttpResponseListener; - -import java.util.Map.Entry; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Netty channel handler for HTTP/2 responses. - */ -@ChannelHandler.Sharable -public class Http2ResponseHandler extends SimpleChannelInboundHandler { - - private static final Logger logger = Logger.getLogger(Http2ResponseHandler.class.getName()); - - private final HttpClient httpClient; - - public Http2ResponseHandler(HttpClient httpClient) { - this.httpClient = httpClient; - } - - @Override - protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse httpResponse) throws Exception { - logger.log(Level.FINE, () -> httpResponse.getClass().getName()); - Integer streamId = httpResponse.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); - if (streamId == null) { - logger.log(Level.WARNING, () -> "stream ID missing in headers"); - return; - } - final HttpRequestContext httpRequestContext = - ctx.channel().attr(HttpClientChannelContextDefaults.REQUEST_CONTEXT_ATTRIBUTE_KEY).get(); - HttpHeaders httpHeaders = httpResponse.headers(); - HttpHeadersListener httpHeadersListener = - ctx.channel().attr(HttpClientChannelContextDefaults.HEADER_LISTENER_ATTRIBUTE_KEY).get(); - if (httpHeadersListener != null) { - logger.log(Level.FINE, () -> "firing onHeaders"); - httpHeadersListener.onHeaders(httpHeaders); - } - CookieListener cookieListener = - ctx.channel().attr(HttpClientChannelContextDefaults.COOKIE_LISTENER_ATTRIBUTE_KEY).get(); - for (String cookieString : httpHeaders.getAll(HttpHeaderNames.SET_COOKIE)) { - Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); - httpRequestContext.addCookie(cookie); - if (cookieListener != null) { - logger.log(Level.FINE, () -> "firing onCookie"); - cookieListener.onCookie(cookie); - } - } - Entry pushEntry = httpRequestContext.getPushMap().get(streamId); - if (pushEntry != null) { - final HttpPushListener httpPushListener = - ctx.channel().attr(HttpClientChannelContextDefaults.PUSH_LISTENER_ATTRIBUTE_KEY).get(); - if (httpPushListener != null) { - httpPushListener.onPushReceived(pushEntry.getKey(), httpResponse); - } - if (!pushEntry.getValue().isSuccess()) { - pushEntry.getValue().setSuccess(); - } - httpRequestContext.getPushMap().remove(streamId); - if (httpRequestContext.isFinished()) { - httpRequestContext.success("response finished"); - } - return; - } - Entry promiseEntry = httpRequestContext.getStreamIdPromiseMap().get(streamId); - if (promiseEntry != null) { - final HttpResponseListener httpResponseListener = - ctx.channel().attr(HttpClientChannelContextDefaults.RESPONSE_LISTENER_ATTRIBUTE_KEY).get(); - if (httpResponseListener != null) { - httpResponseListener.onResponse(httpResponse); - } - if (!promiseEntry.getValue().isSuccess()) { - promiseEntry.getValue().setSuccess(); - } - if (httpClient.tryRedirect(ctx.channel(), httpResponse, httpRequestContext)) { - return; - } - httpRequestContext.getStreamIdPromiseMap().remove(streamId); - if (httpRequestContext.isFinished()) { - httpRequestContext.success("response finished"); - } - } - } - - /** - * The only method to release a HTTP/2 channel back to the pool is to wait for inactivity. - * @param ctx the channel handler context - * @throws Exception if the channel could not be released back to the pool - */ - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - logger.log(Level.FINE, ctx::toString); - final ChannelPool channelPool = - ctx.channel().attr(HttpClientChannelContextDefaults.CHANNEL_POOL_ATTRIBUTE_KEY).get(); - channelPool.release(ctx.channel()); - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - logger.log(Level.FINE, () -> "exception caught: " + cause); - ExceptionListener exceptionListener = - ctx.channel().attr(HttpClientChannelContextDefaults.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get(); - if (exceptionListener != null) { - exceptionListener.onException(cause); - } - final HttpRequestContext httpRequestContext = - ctx.channel().attr(HttpClientChannelContextDefaults.REQUEST_CONTEXT_ATTRIBUTE_KEY).get(); - httpRequestContext.fail(cause.getMessage()); - final ChannelPool channelPool = - ctx.channel().attr(HttpClientChannelContextDefaults.CHANNEL_POOL_ATTRIBUTE_KEY).get(); - channelPool.release(ctx.channel()); - } -} diff --git a/src/main/java/org/xbib/netty/http/client/handler/HttpClientChannelInitializer.java b/src/main/java/org/xbib/netty/http/client/handler/HttpClientChannelInitializer.java deleted file mode 100644 index e508bec..0000000 --- a/src/main/java/org/xbib/netty/http/client/handler/HttpClientChannelInitializer.java +++ /dev/null @@ -1,235 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.handler; - -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPipeline; -import io.netty.channel.socket.SocketChannel; -import io.netty.handler.codec.http.HttpClientCodec; -import io.netty.handler.codec.http.HttpClientUpgradeHandler; -import io.netty.handler.codec.http.HttpContentDecompressor; -import io.netty.handler.codec.http.HttpObjectAggregator; -import io.netty.handler.codec.http2.DefaultHttp2Connection; -import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener; -import io.netty.handler.codec.http2.Http2ClientUpgradeCodec; -import io.netty.handler.codec.http2.Http2Connection; -import io.netty.handler.codec.http2.Http2ConnectionHandler; -import io.netty.handler.codec.http2.Http2ConnectionHandlerBuilder; -import io.netty.handler.codec.http2.Http2FrameLogger; -import io.netty.handler.codec.http2.Http2Settings; -import io.netty.handler.logging.LogLevel; -import io.netty.handler.ssl.ApplicationProtocolConfig; -import io.netty.handler.ssl.ApplicationProtocolNames; -import io.netty.handler.ssl.SslContextBuilder; -import io.netty.handler.ssl.SslHandler; -import io.netty.handler.timeout.ReadTimeoutHandler; -import org.xbib.netty.http.client.HttpClientChannelContext; -import org.xbib.netty.http.client.util.InetAddressKey; - -import javax.net.ssl.SNIHostName; -import javax.net.ssl.SNIServerName; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLException; -import javax.net.ssl.SSLParameters; -import java.util.Arrays; -import java.util.concurrent.TimeUnit; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * Netty HTTP client channel initializer. - */ -public class HttpClientChannelInitializer extends ChannelInitializer { - - private static final Logger logger = Logger.getLogger(HttpClientChannelInitializer.class.getName()); - - private final HttpClientChannelContext context; - - private final HttpHandler httpHandler; - - private final Http2ResponseHandler http2ResponseHandler; - - private InetAddressKey key; - - /** - * Constructor for a new {@link HttpClientChannelInitializer}. - * @param context the HTTP client channel context - * @param httpHandler the HTTP 1.x handler - * @param http2ResponseHandler the HTTP 2 handler - */ - public HttpClientChannelInitializer(HttpClientChannelContext context, HttpHandler httpHandler, - Http2ResponseHandler http2ResponseHandler) { - this.context = context; - this.httpHandler = httpHandler; - this.http2ResponseHandler = http2ResponseHandler; - } - - HttpClientChannelContext getContext() { - return context; - } - - HttpHandler getHttpHandler() { - return httpHandler; - } - - Http2ResponseHandler getHttp2ResponseHandler() { - return http2ResponseHandler; - } - - /** - * Sets up a {@link InetAddressKey} for the channel initialization and initializes the channel. - * Using this method, the channel initializer can handle secure channels, the HTTP protocol version, - * and the host name for Server Name Identification (SNI). - * @param ch the channel - * @param key the key of the internet address - * @throws Exception if channel - */ - public void initChannel(SocketChannel ch, InetAddressKey key) throws Exception { - this.key = key; - initChannel(ch); - } - - @Override - protected void initChannel(SocketChannel ch) throws Exception { - logger.log(Level.FINE, () -> "initChannel with key = " + key); - if (key == null) { - throw new IllegalStateException("no key set for channel initialization"); - } - ChannelPipeline pipeline = ch.pipeline(); - pipeline.addLast(new TrafficLoggingHandler()); - if (context.getHttpProxyHandler() != null) { - pipeline.addLast(context.getHttpProxyHandler()); - } - if (context.getSocks4ProxyHandler() != null) { - pipeline.addLast(context.getSocks4ProxyHandler()); - } - if (context.getSocks5ProxyHandler() != null) { - pipeline.addLast(context.getSocks5ProxyHandler()); - } - pipeline.addLast(new ReadTimeoutHandler(context.getReadTimeoutMillis(), TimeUnit.MILLISECONDS)); - if (context.getSslProvider() != null && key.isSecure()) { - configureEncrypted(ch); - } else { - configureClearText(ch); - } - logger.log(Level.FINE, () -> "initChannel complete, pipeline handler names = " + ch.pipeline().names()); - } - - private void configureClearText(SocketChannel ch) { - ChannelPipeline pipeline = ch.pipeline(); - if (key.getVersion().majorVersion() == 1) { - HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler(context); - pipeline.addLast(http1connectionHandler); - configureHttp1Pipeline(pipeline, context, httpHandler); - } else if (key.getVersion().majorVersion() == 2) { - Http2ConnectionHandler http2connectionHandler = createHttp2ConnectionHandler(context); - // using the upgrade handler means mixed HTTP 1 and HTTP 2 on the same connection - if (context.isInstallHttp2Upgrade()) { - HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler(context); - Http2ClientUpgradeCodec upgradeCodec = - new Http2ClientUpgradeCodec(http2connectionHandler); - HttpClientUpgradeHandler upgradeHandler = - new HttpClientUpgradeHandler(http1connectionHandler, upgradeCodec, context.getMaxContentLength()); - pipeline.addLast(upgradeHandler); - UpgradeRequestHandler upgradeRequestHandler = - new UpgradeRequestHandler(); - pipeline.addLast(upgradeRequestHandler); - } else { - pipeline.addLast(http2connectionHandler); - } - configureHttp2Pipeline(pipeline, http2ResponseHandler); - configureHttp1Pipeline(pipeline, context, httpHandler); - } - } - - private void configureEncrypted(SocketChannel ch) throws SSLException { - ChannelPipeline pipeline = ch.pipeline(); - SslContextBuilder sslContextBuilder = SslContextBuilder.forClient() - .sslProvider(context.getSslProvider()) - .keyManager(context.getKeyCertChainInputStream(), context.getKeyInputStream(), context.getKeyPassword()) - .ciphers(context.getCiphers(), context.getCipherSuiteFilter()) - .trustManager(context.getTrustManagerFactory()); - if (key.getVersion().majorVersion() == 2) { - sslContextBuilder.applicationProtocolConfig(new ApplicationProtocolConfig( - ApplicationProtocolConfig.Protocol.ALPN, - ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, - ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, - ApplicationProtocolNames.HTTP_2, - ApplicationProtocolNames.HTTP_1_1)); - } - SslHandler sslHandler = sslContextBuilder.build().newHandler(ch.alloc()); - SSLEngine engine = sslHandler.engine(); - try { - if (context.isUseServerNameIdentification()) { - String fullQualifiedHostname = key.getInetSocketAddress().getHostName(); - SSLParameters params = engine.getSSLParameters(); - params.setServerNames(Arrays.asList(new SNIServerName[]{new SNIHostName(fullQualifiedHostname)})); - engine.setSSLParameters(params); - } - } finally { - pipeline.addLast(sslHandler); - } - switch (context.getClientAuthMode()) { - case NEED: - engine.setNeedClientAuth(true); - break; - case WANT: - engine.setWantClientAuth(true); - break; - default: - break; - } - if (key.getVersion().majorVersion() == 1) { - HttpClientCodec http1connectionHandler = createHttp1ConnectionHandler(context); - pipeline.addLast(http1connectionHandler); - configureHttp1Pipeline(pipeline, context, httpHandler); - } else if (key.getVersion().majorVersion() == 2) { - pipeline.addLast(new Http2NegotiationHandler(ApplicationProtocolNames.HTTP_1_1, this)); - } - } - - static void configureHttp1Pipeline(ChannelPipeline pipeline, HttpClientChannelContext context, HttpHandler httpHandler) { - if (context.isGzipEnabled()) { - pipeline.addLast(new HttpContentDecompressor()); - } - HttpObjectAggregator httpObjectAggregator = - new HttpObjectAggregator(context.getMaxContentLength(), false); - httpObjectAggregator.setMaxCumulationBufferComponents(context.getMaxCompositeBufferComponents()); - pipeline.addLast(httpObjectAggregator); - pipeline.addLast(httpHandler); - } - - static void configureHttp2Pipeline(ChannelPipeline pipeline, Http2ResponseHandler http2ResponseHandler) { - pipeline.addLast(new UserEventLogger()); - pipeline.addLast(http2ResponseHandler); - } - - static HttpClientCodec createHttp1ConnectionHandler(HttpClientChannelContext context) { - return new HttpClientCodec(context.getMaxInitialLineLength(), context.getMaxHeaderSize(), context.getMaxChunkSize()); - } - - static Http2ConnectionHandler createHttp2ConnectionHandler(HttpClientChannelContext context) { - final Http2Connection http2Connection = new DefaultHttp2Connection(false); - return new Http2ConnectionHandlerBuilder() - .connection(http2Connection) - .frameLogger(new Http2FrameLogger(LogLevel.TRACE, HttpClientChannelInitializer.class)) - .initialSettings(new Http2Settings()) - .encoderEnforceMaxConcurrentStreams(true) - .frameListener(new DelegatingDecompressorFrameListener(http2Connection, - new Http2EventHandler(http2Connection, context.getMaxContentLength(), false))) - .build(); - } -} diff --git a/src/main/java/org/xbib/netty/http/client/handler/HttpHandler.java b/src/main/java/org/xbib/netty/http/client/handler/HttpHandler.java deleted file mode 100755 index 70f116d..0000000 --- a/src/main/java/org/xbib/netty/http/client/handler/HttpHandler.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.handler; - -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.channel.pool.ChannelPool; -import io.netty.handler.codec.http.FullHttpResponse; -import io.netty.handler.codec.http.HttpHeaderNames; -import io.netty.handler.codec.http.HttpHeaders; -import io.netty.handler.codec.http.cookie.ClientCookieDecoder; -import io.netty.handler.codec.http.cookie.Cookie; -import org.xbib.netty.http.client.HttpClient; -import org.xbib.netty.http.client.HttpClientChannelContextDefaults; -import org.xbib.netty.http.client.HttpRequestContext; -import org.xbib.netty.http.client.listener.CookieListener; -import org.xbib.netty.http.client.listener.ExceptionListener; -import org.xbib.netty.http.client.listener.HttpHeadersListener; -import org.xbib.netty.http.client.listener.HttpResponseListener; - -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * HTTP 1.x Netty channel handler. - */ -@ChannelHandler.Sharable -public final class HttpHandler extends ChannelInboundHandlerAdapter { - - private static final Logger logger = Logger.getLogger(HttpHandler.class.getName()); - - private final HttpClient httpClient; - - public HttpHandler(HttpClient httpClient) { - this.httpClient = httpClient; - } - - /** - * - * Read channel message, hand over content to response handler, and redirect to next URL if possible. - * @param ctx the channel handler context - * @param msg the channel message - * @throws Exception if processing of channel message fails - */ - @Override - public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception { - logger.log(Level.FINE, () -> "channelRead msg " + msg.getClass().getName()); - final HttpRequestContext httpRequestContext = - ctx.channel().attr(HttpClientChannelContextDefaults.REQUEST_CONTEXT_ATTRIBUTE_KEY).get(); - if (msg instanceof FullHttpResponse) { - FullHttpResponse httpResponse = (FullHttpResponse) msg; - HttpHeaders httpHeaders = httpResponse.headers(); - HttpHeadersListener httpHeadersListener = - ctx.channel().attr(HttpClientChannelContextDefaults.HEADER_LISTENER_ATTRIBUTE_KEY).get(); - if (httpHeadersListener != null) { - logger.log(Level.FINE, () -> "firing onHeaders"); - httpHeadersListener.onHeaders(httpHeaders); - } - CookieListener cookieListener = - ctx.channel().attr(HttpClientChannelContextDefaults.COOKIE_LISTENER_ATTRIBUTE_KEY).get(); - for (String cookieString : httpHeaders.getAll(HttpHeaderNames.SET_COOKIE)) { - Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); - httpRequestContext.addCookie(cookie); - if (cookieListener != null) { - logger.log(Level.FINE, () -> "firing onCookie"); - cookieListener.onCookie(cookie); - } - } - HttpResponseListener httpResponseListener = - ctx.channel().attr(HttpClientChannelContextDefaults.RESPONSE_LISTENER_ATTRIBUTE_KEY).get(); - if (httpResponseListener != null) { - logger.log(Level.FINE, () -> "firing onResponse"); - httpResponseListener.onResponse(httpResponse); - } - logger.log(Level.FINE, () -> "trying redirect"); - if (httpClient.tryRedirect(ctx.channel(), httpResponse, httpRequestContext)) { - return; - } - httpRequestContext.success("response finished"); - final ChannelPool channelPool = - ctx.channel().attr(HttpClientChannelContextDefaults.CHANNEL_POOL_ATTRIBUTE_KEY).get(); - channelPool.release(ctx.channel()); - } - } - - @Override - public void channelInactive(ChannelHandlerContext ctx) throws Exception { - logger.log(Level.FINE, () -> "channelInactive " + ctx); - final HttpRequestContext httpRequestContext = - ctx.channel().attr(HttpClientChannelContextDefaults.REQUEST_CONTEXT_ATTRIBUTE_KEY).get(); - if (httpRequestContext.getRedirectCount().get() == 0 && !httpRequestContext.isSucceeded()) { - httpRequestContext.fail("channel inactive"); - } - final ChannelPool channelPool = - ctx.channel().attr(HttpClientChannelContextDefaults.CHANNEL_POOL_ATTRIBUTE_KEY).get(); - channelPool.release(ctx.channel()); - } - - /** - * Forward channel exceptions to the exception listener. - * @param ctx the channel handler context - * @param cause the cause of the exception - * @throws Exception if forwarding fails - */ - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - ExceptionListener exceptionListener = - ctx.channel().attr(HttpClientChannelContextDefaults.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get(); - logger.log(Level.FINE, () -> "exceptionCaught"); - if (exceptionListener != null) { - exceptionListener.onException(cause); - } - final HttpRequestContext httpRequestContext = - ctx.channel().attr(HttpClientChannelContextDefaults.REQUEST_CONTEXT_ATTRIBUTE_KEY).get(); - httpRequestContext.fail(cause.getMessage()); - final ChannelPool channelPool = - ctx.channel().attr(HttpClientChannelContextDefaults.CHANNEL_POOL_ATTRIBUTE_KEY).get(); - channelPool.release(ctx.channel()); - } -} diff --git a/src/main/java/org/xbib/netty/http/client/handler/TrafficLoggingHandler.java b/src/main/java/org/xbib/netty/http/client/handler/TrafficLoggingHandler.java index b35d8c5..bd62909 100644 --- a/src/main/java/org/xbib/netty/http/client/handler/TrafficLoggingHandler.java +++ b/src/main/java/org/xbib/netty/http/client/handler/TrafficLoggingHandler.java @@ -1,18 +1,3 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ package org.xbib.netty.http.client.handler; import io.netty.buffer.ByteBuf; @@ -26,24 +11,24 @@ import io.netty.handler.logging.LoggingHandler; * A Netty handler that logs the I/O traffic of a connection. */ @ChannelHandler.Sharable -class TrafficLoggingHandler extends LoggingHandler { +public class TrafficLoggingHandler extends LoggingHandler { - TrafficLoggingHandler() { + public TrafficLoggingHandler() { super("client", LogLevel.TRACE); } @Override - public void channelRegistered(ChannelHandlerContext ctx) throws Exception { + public void channelRegistered(ChannelHandlerContext ctx) { ctx.fireChannelRegistered(); } @Override - public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { + public void channelUnregistered(ChannelHandlerContext ctx) { ctx.fireChannelUnregistered(); } @Override - public void flush(ChannelHandlerContext ctx) throws Exception { + public void flush(ChannelHandlerContext ctx) { ctx.flush(); } diff --git a/src/main/java/org/xbib/netty/http/client/handler/UpgradeRequestHandler.java b/src/main/java/org/xbib/netty/http/client/handler/UpgradeRequestHandler.java deleted file mode 100644 index efa5dde..0000000 --- a/src/main/java/org/xbib/netty/http/client/handler/UpgradeRequestHandler.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.handler; - -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.handler.codec.http.DefaultFullHttpRequest; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http.HttpVersion; -import org.xbib.netty.http.client.HttpClientChannelContextDefaults; -import org.xbib.netty.http.client.HttpRequestContext; -import org.xbib.netty.http.client.listener.ExceptionListener; - -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * - */ -@ChannelHandler.Sharable -class UpgradeRequestHandler extends ChannelInboundHandlerAdapter { - - private static final Logger logger = Logger.getLogger(UpgradeRequestHandler.class.getName()); - - /** - * Send an upgrade request if channel becomes active. - * @param ctx the channel handler context - * @throws Exception if upgrade request sending fails - */ - @Override - public void channelActive(ChannelHandlerContext ctx) throws Exception { - DefaultFullHttpRequest upgradeRequest = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); - ctx.writeAndFlush(upgradeRequest); - super.channelActive(ctx); - ctx.pipeline().remove(this); - logger.log(Level.FINE, () -> "upgrade request handler removed, pipeline = " + ctx.pipeline().names()); - } - - /** - * Forward channel exceptions to the exception listener. - * @param ctx the channel handler context - * @param cause the cause of the exception - * @throws Exception if forwarding fails - */ - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - logger.log(Level.FINE, () -> "exceptionCaught " + cause.getMessage()); - ExceptionListener exceptionListener = - ctx.channel().attr(HttpClientChannelContextDefaults.EXCEPTION_LISTENER_ATTRIBUTE_KEY).get(); - if (exceptionListener != null) { - exceptionListener.onException(cause); - } - final HttpRequestContext httpRequestContext = - ctx.channel().attr(HttpClientChannelContextDefaults.REQUEST_CONTEXT_ATTRIBUTE_KEY).get(); - httpRequestContext.fail(cause.getMessage()); - } -} diff --git a/src/main/java/org/xbib/netty/http/client/handler/UserEventLogger.java b/src/main/java/org/xbib/netty/http/client/handler/UserEventLogger.java index 18e2721..fcb52db 100644 --- a/src/main/java/org/xbib/netty/http/client/handler/UserEventLogger.java +++ b/src/main/java/org/xbib/netty/http/client/handler/UserEventLogger.java @@ -1,25 +1,9 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ package org.xbib.netty.http.client.handler; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.socket.ChannelInputShutdownReadComplete; -import io.netty.handler.codec.http2.Http2ConnectionPrefaceWrittenEvent; import io.netty.handler.ssl.SslCloseCompletionEvent; import java.util.logging.Level; @@ -36,10 +20,8 @@ class UserEventLogger extends ChannelInboundHandlerAdapter { @Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception { logger.log(Level.FINE, () -> "got user event " + evt); - if (evt instanceof Http2ConnectionPrefaceWrittenEvent || - evt instanceof SslCloseCompletionEvent || + if (evt instanceof SslCloseCompletionEvent || evt instanceof ChannelInputShutdownReadComplete) { - // log expected events logger.log(Level.FINE, () -> "user event is expected: " + evt); return; } diff --git a/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java b/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java new file mode 100644 index 0000000..2301e43 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java @@ -0,0 +1,92 @@ +package org.xbib.netty.http.client.handler.http1; + +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpContentDecompressor; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslHandler; +import org.xbib.netty.http.client.ClientConfig; +import org.xbib.netty.http.client.HttpAddress; +import org.xbib.netty.http.client.handler.TrafficLoggingHandler; + +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; +import java.util.Collections; + +public class HttpChannelInitializer extends ChannelInitializer { + + private final ClientConfig clientConfig; + + private final HttpAddress httpAddress; + + private final HttpResponseHandler httpResponseHandler; + + public HttpChannelInitializer(ClientConfig clientConfig, HttpAddress httpAddress, HttpResponseHandler httpResponseHandler) { + this.clientConfig = clientConfig; + this.httpAddress = httpAddress; + this.httpResponseHandler = httpResponseHandler; + } + + @Override + protected void initChannel(SocketChannel ch) { + ch.pipeline().addLast(new TrafficLoggingHandler()); + if (httpAddress.isSecure()) { + configureEncryptedHttp1(ch); + } else { + configureCleartextHttp1(ch); + } + } + + private void configureEncryptedHttp1(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + try { + SslContextBuilder sslContextBuilder = SslContextBuilder.forClient() + .sslProvider(clientConfig.getSslProvider()) + .keyManager(clientConfig.getKeyCertChainInputStream(), clientConfig.getKeyInputStream(), + clientConfig.getKeyPassword()) + .ciphers(clientConfig.getCiphers(), clientConfig.getCipherSuiteFilter()) + .trustManager(clientConfig.getTrustManagerFactory()); + SslHandler sslHandler = sslContextBuilder.build().newHandler(ch.alloc()); + SSLEngine engine = sslHandler.engine(); + if (clientConfig.isServerNameIdentification()) { + String fullQualifiedHostname = httpAddress.getInetSocketAddress().getHostName(); + SSLParameters params = engine.getSSLParameters(); + params.setServerNames(Collections.singletonList(new SNIHostName(fullQualifiedHostname))); + engine.setSSLParameters(params); + } + pipeline.addLast(sslHandler); + switch (clientConfig.getClientAuthMode()) { + case NEED: + engine.setNeedClientAuth(true); + break; + case WANT: + engine.setWantClientAuth(true); + break; + default: + break; + } + } catch (SSLException e) { + throw new IllegalStateException("unable to configure SSL: " + e.getMessage(), e); + } + configureCleartextHttp1(ch); + } + + private void configureCleartextHttp1(SocketChannel ch) { + ChannelPipeline pipeline = ch.pipeline(); + pipeline.addLast(new HttpClientCodec(clientConfig.getMaxInitialLineLength(), + clientConfig.getMaxHeadersSize(), clientConfig.getMaxChunkSize())); + if (clientConfig.isEnableGzip()) { + pipeline.addLast(new HttpContentDecompressor()); + } + HttpObjectAggregator httpObjectAggregator = new HttpObjectAggregator(clientConfig.getMaxContentLength(), + false); + httpObjectAggregator.setMaxCumulationBufferComponents(clientConfig.getMaxCompositeBufferComponents()); + pipeline.addLast(httpObjectAggregator); + pipeline.addLast(httpResponseHandler); + } +} diff --git a/src/main/java/org/xbib/netty/http/client/handler/http1/HttpResponseHandler.java b/src/main/java/org/xbib/netty/http/client/handler/http1/HttpResponseHandler.java new file mode 100644 index 0000000..ead69a5 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/handler/http1/HttpResponseHandler.java @@ -0,0 +1,26 @@ +package org.xbib.netty.http.client.handler.http1; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.FullHttpResponse; +import org.xbib.netty.http.client.transport.Transport; + +@ChannelHandler.Sharable +public class HttpResponseHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse httpResponse) { + Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); + transport.headersReceived(null, httpResponse.headers()); + transport.responseReceived(null, httpResponse); + transport.success(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); + transport.fail(cause); + ctx.channel().close(); + } +} diff --git a/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java b/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java new file mode 100644 index 0000000..5bf5ca1 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java @@ -0,0 +1,112 @@ +package org.xbib.netty.http.client.handler.http2; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.http2.DefaultHttp2Connection; +import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener; +import io.netty.handler.codec.http2.Http2ConnectionHandler; +import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; +import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import org.xbib.netty.http.client.ClientConfig; +import org.xbib.netty.http.client.HttpAddress; + +import javax.net.ssl.SNIHostName; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; +import java.util.Collections; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class Http2ChannelInitializer extends ChannelInitializer { + + private static final Logger logger = Logger.getLogger(Http2ChannelInitializer.class.getName()); + + private final ClientConfig clientConfig; + + private final HttpAddress httpAddress; + + private final Http2SettingsHandler http2SettingsHandler; + + private final Http2ResponseHandler http2ResponseHandler; + + public Http2ChannelInitializer(ClientConfig clientConfig, + HttpAddress httpAddress, + Http2SettingsHandler http2SettingsHandler, + Http2ResponseHandler http2ResponseHandler) { + this.clientConfig = clientConfig; + this.httpAddress = httpAddress; + this.http2SettingsHandler = http2SettingsHandler; + this.http2ResponseHandler = http2ResponseHandler; + } + + /** + * The channel initialization for HTTP/2 is always encrypted. + * The reason is there is no known HTTP/2 server supporting cleartext. + * + * @param ch socket channel + */ + @Override + protected void initChannel(SocketChannel ch) { + DefaultHttp2Connection http2Connection = new DefaultHttp2Connection(false); + Http2FrameLogger frameLogger = new Http2FrameLogger(LogLevel.INFO, "client"); + Http2ConnectionHandler http2ConnectionHandler = new HttpToHttp2ConnectionHandlerBuilder() + .connection(http2Connection) + .frameLogger(frameLogger) + .frameListener(new DelegatingDecompressorFrameListener(http2Connection, + new InboundHttp2ToHttpAdapterBuilder(http2Connection) + .maxContentLength(clientConfig.getMaxContentLength()) + .propagateSettings(true) + .build())) + .build(); + + try { + SslContext sslContext = SslContextBuilder.forClient() + .sslProvider(SslProvider.JDK) + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) + .applicationProtocolConfig(new ApplicationProtocolConfig( + ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2)) + .build(); + SslHandler sslHandler = sslContext.newHandler(ch.alloc()); + SSLEngine engine = sslHandler.engine(); + if (clientConfig.isServerNameIdentification()) { + String fullQualifiedHostname = httpAddress.getInetSocketAddress().getHostName(); + SSLParameters params = engine.getSSLParameters(); + params.setServerNames(Collections.singletonList(new SNIHostName(fullQualifiedHostname))); + engine.setSSLParameters(params); + } + ch.pipeline().addLast(sslHandler); + ApplicationProtocolNegotiationHandler negotiationHandler = new ApplicationProtocolNegotiationHandler("") { + @Override + protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { + if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { + ctx.pipeline().addLast(http2ConnectionHandler, http2SettingsHandler, http2ResponseHandler); + return; + } + ctx.close(); + throw new IllegalStateException("unknown protocol: " + protocol); + } + }; + ch.pipeline().addLast(negotiationHandler); + } catch (SSLException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + } + } +} diff --git a/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java b/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java new file mode 100644 index 0000000..5f36c50 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java @@ -0,0 +1,41 @@ +package org.xbib.netty.http.client.handler.http2; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http2.HttpConversionUtil; +import org.xbib.netty.http.client.transport.Transport; + +import java.io.IOException; + +@ChannelHandler.Sharable +public class Http2ResponseHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse httpResponse) { + Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); + Integer streamId = httpResponse.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); + transport.headersReceived(streamId, httpResponse.headers()); + transport.responseReceived(streamId, httpResponse); + } + + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + // do nothing + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + ctx.fireChannelInactive(); + Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); + transport.fail(new IOException("channel closed")); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); + transport.fail(cause); + ctx.channel().close(); + } +} diff --git a/src/main/java/org/xbib/netty/http/client/handler/http2/Http2SettingsHandler.java b/src/main/java/org/xbib/netty/http/client/handler/http2/Http2SettingsHandler.java new file mode 100644 index 0000000..e9fb6ab --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/handler/http2/Http2SettingsHandler.java @@ -0,0 +1,18 @@ +package org.xbib.netty.http.client.handler.http2; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http2.Http2Settings; +import org.xbib.netty.http.client.transport.Transport; + +@ChannelHandler.Sharable +public class Http2SettingsHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, Http2Settings http2Settings) { + Transport transport = ctx.channel().attr(Transport.TRANSPORT_ATTRIBUTE_KEY).get(); + transport.settingsReceived(ctx.channel(), http2Settings); + ctx.pipeline().remove(this); + } +} diff --git a/src/main/java/org/xbib/netty/http/client/internal/HttpClientChannelPoolHandler.java b/src/main/java/org/xbib/netty/http/client/internal/HttpClientChannelPoolHandler.java deleted file mode 100644 index ca18dee..0000000 --- a/src/main/java/org/xbib/netty/http/client/internal/HttpClientChannelPoolHandler.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.internal; - -import io.netty.channel.Channel; -import io.netty.channel.pool.ChannelPoolHandler; -import io.netty.channel.socket.SocketChannel; -import org.xbib.netty.http.client.handler.HttpClientChannelInitializer; -import org.xbib.netty.http.client.util.InetAddressKey; - -import java.util.concurrent.atomic.AtomicInteger; -import java.util.logging.Level; -import java.util.logging.Logger; - -/** - * - */ -public class HttpClientChannelPoolHandler implements ChannelPoolHandler { - - private static final Logger logger = Logger.getLogger(HttpClientChannelPoolHandler.class.getName()); - - private final HttpClientChannelInitializer channelInitializer; - - private final InetAddressKey key; - - private final AtomicInteger active = new AtomicInteger(); - - private int peak; - - public HttpClientChannelPoolHandler(HttpClientChannelInitializer channelInitializer, InetAddressKey key) { - this.channelInitializer = channelInitializer; - this.key = key; - } - - @Override - public void channelCreated(Channel ch) throws Exception { - logger.log(Level.FINE, () -> "channel created " + ch + " key:" + key); - channelInitializer.initChannel((SocketChannel) ch, key); - int n = active.incrementAndGet(); - if (n > peak) { - peak = n; - } - } - - @Override - public void channelAcquired(Channel ch) throws Exception { - logger.log(Level.FINE, () -> "channel acquired from pool " + ch); - } - - @Override - public void channelReleased(Channel ch) throws Exception { - logger.log(Level.FINE, () -> "channel released to pool " + ch); - active.decrementAndGet(); - } - - public int getActive() { - return active.get(); - } - - public int getPeak() { - return peak; - } -} diff --git a/src/main/java/org/xbib/netty/http/client/internal/HttpClientChannelPoolMap.java b/src/main/java/org/xbib/netty/http/client/internal/HttpClientChannelPoolMap.java deleted file mode 100644 index 46fef1c..0000000 --- a/src/main/java/org/xbib/netty/http/client/internal/HttpClientChannelPoolMap.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.internal; - -import io.netty.bootstrap.Bootstrap; -import io.netty.channel.pool.AbstractChannelPoolMap; -import io.netty.channel.pool.FixedChannelPool; -import org.xbib.netty.http.client.HttpClient; -import org.xbib.netty.http.client.HttpClientChannelContext; -import org.xbib.netty.http.client.handler.Http2ResponseHandler; -import org.xbib.netty.http.client.handler.HttpClientChannelInitializer; -import org.xbib.netty.http.client.handler.HttpHandler; -import org.xbib.netty.http.client.util.InetAddressKey; - -/** - * - */ -public class HttpClientChannelPoolMap extends AbstractChannelPoolMap { - - private final HttpClient httpClient; - - private final HttpClientChannelContext httpClientChannelContext; - - private final Bootstrap bootstrap; - - private final int maxConnections; - - private HttpClientChannelInitializer httpClientChannelInitializer; - - private HttpClientChannelPoolHandler httpClientChannelPoolHandler; - - public HttpClientChannelPoolMap(HttpClient httpClient, - HttpClientChannelContext httpClientChannelContext, - Bootstrap bootstrap, - int maxConnections) { - this.httpClient = httpClient; - this.httpClientChannelContext = httpClientChannelContext; - this.bootstrap = bootstrap; - this.maxConnections = maxConnections; - } - - @Override - protected FixedChannelPool newPool(InetAddressKey key) { - this.httpClientChannelInitializer = new HttpClientChannelInitializer(httpClientChannelContext, - new HttpHandler(httpClient), new Http2ResponseHandler(httpClient)); - this.httpClientChannelPoolHandler = new HttpClientChannelPoolHandler(httpClientChannelInitializer, key); - return new FixedChannelPool(bootstrap.remoteAddress(key.getInetSocketAddress()), - httpClientChannelPoolHandler, maxConnections); - } - - public HttpClientChannelInitializer getHttpClientChannelInitializer() { - return httpClientChannelInitializer; - } - - public HttpClientChannelPoolHandler getHttpClientChannelPoolHandler() { - return httpClientChannelPoolHandler; - } -} diff --git a/src/main/java/org/xbib/netty/http/client/internal/HttpClientThreadFactory.java b/src/main/java/org/xbib/netty/http/client/internal/HttpClientThreadFactory.java deleted file mode 100644 index fd9fd52..0000000 --- a/src/main/java/org/xbib/netty/http/client/internal/HttpClientThreadFactory.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.internal; - -import java.util.concurrent.ThreadFactory; - -/** - * - */ -public class HttpClientThreadFactory implements ThreadFactory { - - private int number = 0; - - @Override - public Thread newThread(Runnable runnable) { - Thread thread = new Thread(runnable, "org-xbib-netty-http-client-pool-" + (number++)); - thread.setDaemon(true); - return thread; - } -} diff --git a/src/main/java/org/xbib/netty/http/client/internal/package-info.java b/src/main/java/org/xbib/netty/http/client/internal/package-info.java deleted file mode 100644 index 392fd50..0000000 --- a/src/main/java/org/xbib/netty/http/client/internal/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Internal classes for Netty HTTP client. - */ -package org.xbib.netty.http.client.internal; diff --git a/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java b/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java index 718efed..1552176 100644 --- a/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java +++ b/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java @@ -2,8 +2,6 @@ package org.xbib.netty.http.client.listener; import io.netty.handler.codec.http.cookie.Cookie; -/** - */ @FunctionalInterface public interface CookieListener { diff --git a/src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java b/src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java index ee011dc..eb93af6 100644 --- a/src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java +++ b/src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java @@ -1,28 +1,7 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ package org.xbib.netty.http.client.listener; -/** - */ @FunctionalInterface public interface ExceptionListener { - /** - * Called when an exception is transported to a listener. - * @param throwable the exception - */ void onException(Throwable throwable); } diff --git a/src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java b/src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java index 91c0cd1..311436c 100644 --- a/src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java +++ b/src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java @@ -1,24 +1,7 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ package org.xbib.netty.http.client.listener; import io.netty.handler.codec.http.HttpHeaders; -/** - */ @FunctionalInterface public interface HttpHeadersListener { diff --git a/src/main/java/org/xbib/netty/http/client/listener/HttpPushListener.java b/src/main/java/org/xbib/netty/http/client/listener/HttpPushListener.java index 22d3fb7..2eeb497 100644 --- a/src/main/java/org/xbib/netty/http/client/listener/HttpPushListener.java +++ b/src/main/java/org/xbib/netty/http/client/listener/HttpPushListener.java @@ -1,27 +1,8 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ package org.xbib.netty.http.client.listener; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http2.Http2Headers; -/** - * This listener can forward HTTP push. - * - */ @FunctionalInterface public interface HttpPushListener { diff --git a/src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java b/src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java index a71f3b6..f06b1ee 100644 --- a/src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java +++ b/src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java @@ -1,24 +1,7 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ package org.xbib.netty.http.client.listener; import io.netty.handler.codec.http.FullHttpResponse; -/** - */ @FunctionalInterface public interface HttpResponseListener { diff --git a/src/main/java/org/xbib/netty/http/client/rest/RestClient.java b/src/main/java/org/xbib/netty/http/client/rest/RestClient.java new file mode 100644 index 0000000..a8133e1 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/rest/RestClient.java @@ -0,0 +1,57 @@ +package org.xbib.netty.http.client.rest; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpMethod; +import org.xbib.net.URL; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.HttpAddress; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.transport.Transport; + +import java.io.IOException; +import java.net.ConnectException; +import java.nio.charset.StandardCharsets; +import java.util.logging.Logger; + +public class RestClient { + + private static final Logger logger = Logger.getLogger(RestClient.class.getName()); + + private Client client; + + private Transport transport; + + private FullHttpResponse response; + + private RestClient(Client client, Transport transport) { + this.client = client; + this.transport = transport; + } + + public void setResponse(FullHttpResponse response) { + this.response = response.copy(); + } + + public String asString() { + ByteBuf byteBuf = response != null ? response.content() : null; + return byteBuf != null && byteBuf.isReadable() ? response.content().toString(StandardCharsets.UTF_8) : null; + } + + public static RestClient get(String urlString) throws IOException { + URL url = URL.create(urlString); + Client client = new Client(); + Transport transport = client.newTransport(HttpAddress.http1(url)); + RestClient restClient = new RestClient(client, transport); + transport.setResponseListener(restClient::setResponse); + try { + transport.connect(); + } catch (InterruptedException e) { + throw new ConnectException("unable to connect to " + url); + } + transport.awaitSettings(); + transport.execute(Request.builder(HttpMethod.GET).setURL(url).build()); + transport.get(); + return restClient; + } +} diff --git a/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java b/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java new file mode 100644 index 0000000..3887355 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java @@ -0,0 +1,330 @@ +package org.xbib.netty.http.client.transport; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.cookie.ClientCookieEncoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http2.HttpConversionUtil; +import org.xbib.net.PercentDecoder; +import org.xbib.net.URL; +import org.xbib.net.URLSyntaxException; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.HttpAddress; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.RequestBuilder; +import org.xbib.netty.http.client.listener.CookieListener; +import org.xbib.netty.http.client.listener.ExceptionListener; +import org.xbib.netty.http.client.listener.HttpHeadersListener; +import org.xbib.netty.http.client.listener.HttpPushListener; +import org.xbib.netty.http.client.listener.HttpResponseListener; + +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.SortedMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +abstract class BaseTransport implements Transport { + + private static final Logger logger = Logger.getLogger(BaseTransport.class.getName()); + + protected final Client client; + + protected final HttpAddress httpAddress; + + protected Channel channel; + + protected SortedMap requests; + + protected HttpResponseListener responseListener; + + protected ExceptionListener exceptionListener; + + protected HttpHeadersListener httpHeadersListener; + + protected CookieListener cookieListener; + + protected HttpPushListener pushListener; + + private Map cookieBox; + + BaseTransport(Client client, HttpAddress httpAddress) { + this.client = client; + this.httpAddress = httpAddress; + this.requests = new ConcurrentSkipListMap<>(); + } + + @Override + public HttpAddress httpAddress() { + return httpAddress; + } + + @Override + public void connect() throws InterruptedException { + channel = client.newChannel(httpAddress); + channel.attr(TRANSPORT_ATTRIBUTE_KEY).set(this); + } + + @Override + public Channel channel() { + return channel; + } + + @Override + public Transport execute(Request request) { + if (channel == null) { + try { + connect(); + awaitSettings(); + } catch (InterruptedException e) { + return this; + } + } + setResponseListener(request.getResponseListener()); + setExceptionListener(request.getExceptionListener()); + setHeadersListener(request.getHeadersListener()); + setCookieListener(request.getCookieListener()); + setPushListener(request.getPushListener()); + // some HTTP 1.1 servers like Elasticsearch do not understand full URIs in HTTP command line + String uri = request.httpVersion().majorVersion() < 2 ? + request.base().relativeReference() : request.base().toString(); + FullHttpRequest fullHttpRequest = request.content() == null ? + new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri) : + new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri, + request.content()); + Integer streamId = nextStream(); + if (streamId != null) { + request.headers().add(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), Integer.toString(streamId)); + } + // add matching cookies from box (previous requests) and new cookies from request builder + Collection cookies = new ArrayList<>(); + cookies.addAll(matchCookiesFromBox(request)); + cookies.addAll(matchCookies(request)); + if (!cookies.isEmpty()) { + request.headers().set(HttpHeaderNames.COOKIE, ClientCookieEncoder.STRICT.encode(cookies)); + } + // add stream-id and cookie headers + fullHttpRequest.headers().set(request.headers()); + requests.put(streamId, request); + logger.log(Level.FINE, () -> "streamId = " + streamId + " writing request = " + fullHttpRequest); + channel.writeAndFlush(fullHttpRequest); + return this; + } + + /** + * Experimental. + * @param request request + * @param supplier supplier + * @param supplier result + * @return completable future + */ + @Override + public CompletableFuture execute(Request request, + Function supplier) { + final CompletableFuture completableFuture = new CompletableFuture<>(); + request.setExceptionListener(completableFuture::completeExceptionally); + request.setResponseListener(response -> completableFuture.complete(supplier.apply(response))); + execute(request); + return completableFuture; + } + + @Override + public void close() { + get(); + if (channel != null) { + channel.close(); + } + } + + @Override + public void setResponseListener(HttpResponseListener responseListener) { + if (responseListener != null) { + this.responseListener = responseListener; + } + } + + @Override + public HttpResponseListener getResponseListener() { + return responseListener; + } + + @Override + public void setHeadersListener(HttpHeadersListener httpHeadersListener) { + if (httpHeadersListener != null) { + this.httpHeadersListener = httpHeadersListener; + } + } + + @Override + public HttpHeadersListener getHeadersListener() { + return httpHeadersListener; + } + + @Override + public void setCookieListener(CookieListener cookieListener) { + if (cookieListener != null) { + this.cookieListener = cookieListener; + } + } + + @Override + public CookieListener getCookieListener() { + return cookieListener; + } + + @Override + public void setExceptionListener(ExceptionListener exceptionListener) { + if (exceptionListener != null) { + this.exceptionListener = exceptionListener; + } + } + + @Override + public ExceptionListener getExceptionListener() { + return exceptionListener; + } + + @Override + public void setPushListener(HttpPushListener pushListener) { + if (pushListener != null) { + this.pushListener = pushListener; + } + } + + @Override + public HttpPushListener getPushListener() { + return pushListener; + } + + protected Request continuation(Integer streamId, FullHttpResponse httpResponse) throws URLSyntaxException { + if (httpResponse == null) { + return null; + } + try { + if (streamId == null) { + streamId = requests.lastKey(); + } + Request request = requests.get(streamId); + if (request.checkRedirect()) { + int status = httpResponse.status().code(); + switch (status) { + case 300: + case 301: + case 302: + case 303: + case 305: + case 307: + case 308: + String location = httpResponse.headers().get(HttpHeaderNames.LOCATION); + location = new PercentDecoder(StandardCharsets.UTF_8.newDecoder()).decode(location); + if (location != null) { + logger.log(Level.INFO, "found redirect location: " + location); + URL redirUrl = URL.base(request.base()).resolve(location); + HttpMethod method = httpResponse.status().code() == 303 ? HttpMethod.GET : request.httpMethod(); + RequestBuilder newHttpRequestBuilder = Request.builder(method) + .setURL(redirUrl) + .setVersion(request.httpVersion()) + .setHeaders(request.headers()) + .setContent(request.content()); + request.base().getQueryParams().forEach(pair -> + newHttpRequestBuilder.addParam(pair.getFirst(), pair.getSecond()) + ); + request.cookies().forEach(newHttpRequestBuilder::addCookie); + Request newHttpRequest = newHttpRequestBuilder.build(); + newHttpRequest.setResponseListener(request.getResponseListener()); + newHttpRequest.setExceptionListener(request.getExceptionListener()); + newHttpRequest.setHeadersListener(request.getHeadersListener()); + newHttpRequest.setCookieListener(request.getCookieListener()); + newHttpRequest.setPushListener(request.getPushListener()); + StringBuilder hostAndPort = new StringBuilder(); + hostAndPort.append(redirUrl.getHost()); + if (redirUrl.getPort() != null) { + hostAndPort.append(':').append(redirUrl.getPort()); + } + newHttpRequest.headers().set(HttpHeaderNames.HOST, hostAndPort.toString()); + logger.log(Level.INFO, "redirect url: " + redirUrl + + " old request: " + request.toString() + + " new request: " + newHttpRequest.toString()); + return newHttpRequest; + } + break; + default: + logger.log(Level.FINE, "no redirect because of status code " + status); + break; + } + } + } catch (MalformedInputException | UnmappableCharacterException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + return null; + } + + public void setCookieBox(Map cookieBox) { + this.cookieBox = cookieBox; + } + + public Map getCookieBox() { + return cookieBox; + } + + public void addCookie(Cookie cookie) { + if (cookieBox == null) { + this.cookieBox = Collections.synchronizedMap(new LRUCache(32)); + } + cookieBox.put(cookie, true); + } + + private List matchCookiesFromBox(Request request) { + return cookieBox == null ? Collections.emptyList() : cookieBox.keySet().stream().filter(cookie -> + matchCookie(request.base(), cookie) + ).collect(Collectors.toList()); + } + + private List matchCookies(Request request) { + return request.cookies().stream().filter(cookie -> + matchCookie(request.base(), cookie) + ).collect(Collectors.toList()); + } + + private boolean matchCookie(URL url, Cookie cookie) { + boolean domainMatch = cookie.domain() == null || url.getHost().endsWith(cookie.domain()); + if (!domainMatch) { + return false; + } + boolean pathMatch = "/".equals(cookie.path()) || url.getPath().startsWith(cookie.path()); + if (!pathMatch) { + return false; + } + boolean secureScheme = "https".equals(url.getScheme()); + return (secureScheme && cookie.isSecure()) || (!secureScheme && !cookie.isSecure()); + } + + class LRUCache extends LinkedHashMap { + + private final int cacheSize; + + LRUCache(int cacheSize) { + super(16, 0.75f, true); + this.cacheSize = cacheSize; + } + + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() >= cacheSize; + } + } +} diff --git a/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java b/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java new file mode 100644 index 0000000..4d3bcbe --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java @@ -0,0 +1,166 @@ +package org.xbib.netty.http.client.transport; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.cookie.ClientCookieDecoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http2.Http2Settings; +import org.xbib.net.URLSyntaxException; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.HttpAddress; +import org.xbib.netty.http.client.Request; + +import java.util.SortedMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class Http2Transport extends BaseTransport implements Transport { + + private static final Logger logger = Logger.getLogger(Http2Transport.class.getName()); + + private CompletableFuture settingsPromise; + + private final AtomicInteger streamIdCounter; + + private SortedMap> streamidPromiseMap; + + public Http2Transport(Client client, HttpAddress httpAddress) { + super(client, httpAddress); + streamIdCounter = new AtomicInteger(3); + streamidPromiseMap = new ConcurrentSkipListMap<>(); + } + + @Override + public void connect() throws InterruptedException { + super.connect(); + settingsPromise = new CompletableFuture<>(); + } + + @Override + public Integer nextStream() { + Integer streamId = streamIdCounter.getAndAdd(2); + if (streamId == Integer.MIN_VALUE) { + // reset if overflow, Java wraps atomic integers to Integer.MIN_VALUE + streamIdCounter.set(3); + streamId = 3; + } + streamidPromiseMap.put(streamId, new CompletableFuture<>()); + return streamId; + } + + @Override + public void settingsReceived(Channel channel, Http2Settings http2Settings) { + if (settingsPromise != null) { + settingsPromise.complete(true); + } else { + logger.log(Level.WARNING, "settings received but no promise present"); + } + } + + @Override + public void awaitSettings() { + if (settingsPromise != null) { + try { + settingsPromise.get(client.getTimeout(), TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + settingsPromise.completeExceptionally(e); + } + } else { + logger.log(Level.WARNING, "waiting for settings but no promise present"); + } + } + + @Override + public void responseReceived(Integer streamId, FullHttpResponse fullHttpResponse) { + if (streamId == null) { + logger.log(Level.WARNING, "unexpected message received: " + fullHttpResponse); + return; + } + CompletableFuture promise = streamidPromiseMap.get(streamId); + if (promise == null) { + logger.log(Level.WARNING, "message received for unknown stream id " + streamId); + if (pushListener != null) { + pushListener.onPushReceived(null, fullHttpResponse); + } + } else { + if (responseListener != null) { + responseListener.onResponse(fullHttpResponse); + } + // forward? + try { + Request request = continuation(streamId, fullHttpResponse); + if (request != null) { + // synchronous call here + client.continuation(this, request); + } + } catch (URLSyntaxException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + // complete origin transport + promise.complete(true); + } + } + + @Override + public void headersReceived(Integer streamId, HttpHeaders httpHeaders) { + if (httpHeadersListener != null) { + httpHeadersListener.onHeaders(httpHeaders); + } + if (cookieListener != null) { + for (String cookieString : httpHeaders.getAll(HttpHeaderNames.SET_COOKIE)) { + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + cookieListener.onCookie(cookie); + } + } + } + + @Override + public void awaitResponse(Integer streamId) { + if (streamId == null) { + return; + } + CompletableFuture promise = streamidPromiseMap.get(streamId); + if (promise != null) { + try { + promise.get(client.getTimeout(), TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + logger.log(Level.WARNING, "streamId=" + streamId + " " + e.getMessage(), e); + } finally { + streamidPromiseMap.remove(streamId); + } + } + } + + @Override + public Transport get() { + for (Integer streamId : streamidPromiseMap.keySet()) { + awaitResponse(streamId); + } + return this; + } + + @Override + public void success() { + for (CompletableFuture promise : streamidPromiseMap.values()) { + promise.complete(true); + } + } + + @Override + public void fail(Throwable throwable) { + if (exceptionListener != null) { + exceptionListener.onException(throwable); + } + for (CompletableFuture promise : streamidPromiseMap.values()) { + promise.completeExceptionally(throwable); + } + } +} diff --git a/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java b/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java new file mode 100644 index 0000000..1ef81ee --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java @@ -0,0 +1,135 @@ +package org.xbib.netty.http.client.transport; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.cookie.ClientCookieDecoder; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http2.Http2Settings; +import org.xbib.net.URLSyntaxException; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.HttpAddress; +import org.xbib.netty.http.client.Request; + +import java.util.SortedMap; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentSkipListMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class HttpTransport extends BaseTransport implements Transport { + + private static final Logger logger = Logger.getLogger(HttpTransport.class.getName()); + + private final AtomicInteger sequentialCounter; + + private SortedMap> sequentialPromiseMap; + + public HttpTransport(Client client, HttpAddress httpAddress) { + super(client, httpAddress); + this.sequentialCounter = new AtomicInteger(); + this.sequentialPromiseMap = new ConcurrentSkipListMap<>(); + } + + @Override + public Integer nextStream() { + Integer streamId = sequentialCounter.getAndAdd(1); + if (streamId == Integer.MIN_VALUE) { + // reset if overflow, Java wraps atomic integers to Integer.MIN_VALUE + sequentialCounter.set(0); + streamId = 0; + } + sequentialPromiseMap.put(streamId, new CompletableFuture<>()); + return streamId; + } + + @Override + public void settingsReceived(Channel channel, Http2Settings http2Settings) { + } + + @Override + public void awaitSettings() { + } + + @Override + public void responseReceived(Integer streamId, FullHttpResponse fullHttpResponse) { + if (responseListener != null) { + responseListener.onResponse(fullHttpResponse); + } + try { + Request request = continuation(null, fullHttpResponse); + if (request != null) { + client.continuation(this, request); + } + } catch (URLSyntaxException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + if (!sequentialPromiseMap.isEmpty()) { + CompletableFuture promise = sequentialPromiseMap.get(sequentialPromiseMap.firstKey()); + if (promise != null) { + promise.complete(true); + } + } + } + + @Override + public void headersReceived(Integer streamId, HttpHeaders httpHeaders) { + if (httpHeadersListener != null) { + httpHeadersListener.onHeaders(httpHeaders); + } + for (String cookieString : httpHeaders.getAll(HttpHeaderNames.SET_COOKIE)) { + Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); + addCookie(cookie); + if (cookieListener != null) { + cookieListener.onCookie(cookie); + } + } + } + + @Override + public void awaitResponse(Integer streamId) { + if (streamId == null) { + return; + } + CompletableFuture promise = sequentialPromiseMap.get(streamId); + if (promise != null) { + try { + promise.get(client.getTimeout(), TimeUnit.MILLISECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + logger.log(Level.WARNING, "streamId=" + streamId + " " + e.getMessage(), e); + } finally { + sequentialPromiseMap.remove(streamId); + } + } + } + + @Override + public Transport get() { + for (Integer streamId : sequentialPromiseMap.keySet()) { + awaitResponse(streamId); + } + return this; + } + + @Override + public void success() { + for (CompletableFuture promise : sequentialPromiseMap.values()) { + promise.complete(true); + } + } + + @Override + public void fail(Throwable throwable) { + if (exceptionListener != null) { + exceptionListener.onException(throwable); + } + for (CompletableFuture promise : sequentialPromiseMap.values()) { + promise.completeExceptionally(throwable); + } + } +} diff --git a/src/main/java/org/xbib/netty/http/client/transport/Transport.java b/src/main/java/org/xbib/netty/http/client/transport/Transport.java new file mode 100644 index 0000000..85a266d --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/transport/Transport.java @@ -0,0 +1,78 @@ +package org.xbib.netty.http.client.transport; + +import io.netty.channel.Channel; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.util.AttributeKey; +import org.xbib.netty.http.client.HttpAddress; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.listener.CookieListener; +import org.xbib.netty.http.client.listener.ExceptionListener; +import org.xbib.netty.http.client.listener.HttpHeadersListener; +import org.xbib.netty.http.client.listener.HttpPushListener; +import org.xbib.netty.http.client.listener.HttpResponseListener; + +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +public interface Transport { + + AttributeKey TRANSPORT_ATTRIBUTE_KEY = AttributeKey.valueOf("transport"); + + HttpAddress httpAddress(); + + void connect() throws InterruptedException; + + Transport execute(Request request); + + CompletableFuture execute(Request request, Function supplier); + + Channel channel(); + + Integer nextStream(); + + void settingsReceived(Channel channel, Http2Settings http2Settings); + + void awaitSettings(); + + void setResponseListener(HttpResponseListener responseListener); + + HttpResponseListener getResponseListener(); + + void setExceptionListener(ExceptionListener exceptionListener); + + ExceptionListener getExceptionListener(); + + void setHeadersListener(HttpHeadersListener headersListener); + + HttpHeadersListener getHeadersListener(); + + void setPushListener(HttpPushListener pushListener); + + HttpPushListener getPushListener(); + + void setCookieListener(CookieListener cookieListener); + + CookieListener getCookieListener(); + + void setCookieBox(Map cookieBox); + + Map getCookieBox(); + + void responseReceived(Integer streamId, FullHttpResponse fullHttpResponse); + + void headersReceived(Integer streamId, HttpHeaders httpHeaders); + + void awaitResponse(Integer streamId); + + Transport get(); + + void success(); + + void fail(Throwable throwable); + + void close(); +} diff --git a/src/main/java/org/xbib/netty/http/client/transport/package-info.java b/src/main/java/org/xbib/netty/http/client/transport/package-info.java new file mode 100644 index 0000000..1327d44 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/transport/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for transports in the Netty client. + */ +package org.xbib.netty.http.client.transport; diff --git a/src/main/java/org/xbib/netty/http/client/util/AbstractFuture.java b/src/main/java/org/xbib/netty/http/client/util/AbstractFuture.java deleted file mode 100644 index 739c4bf..0000000 --- a/src/main/java/org/xbib/netty/http/client/util/AbstractFuture.java +++ /dev/null @@ -1,353 +0,0 @@ -package org.xbib.netty.http.client.util; - -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.locks.AbstractQueuedSynchronizer; - -/** - *

- * An abstract implementation of the {@link Future} interface. This class - * is an abstraction of {@link java.util.concurrent.FutureTask} to support use - * for tasks other than {@link Runnable}s. It uses an - * {@link AbstractQueuedSynchronizer} to deal with concurrency issues and - * guarantee thread safety. It could be used as a base class to - * {@code FutureTask}, or any other implementor of the {@code Future} interface. - *

- * - *

- * This class implements all methods in {@code Future}. Subclasses should - * provide a way to set the result of the computation through the protected - * methods {@link #set(Object)}, {@link #setException(Exception)}, or - * {@link #cancel()}. If subclasses want to implement cancellation they can - * override the {@link #cancel(boolean)} method with a real implementation, the - * default implementation doesn't support cancellation. - *

- * - *

- * The state changing methods all return a boolean indicating success or - * failure in changing the future's state. Valid states are running, - * completed, failed, or cancelled. Because this class does not implement - * cancellation it is left to the subclass to distinguish between created - * and running tasks. - *

- * - *

This class is taken from the Google Guava project.

- * - * @param the future value parameter type - */ -public abstract class AbstractFuture implements Future { - - /** - * Synchronization control. - */ - private final Sync sync = new Sync<>(); - - /** - * The default {@link AbstractFuture} implementation throws {@code - * InterruptedException} if the current thread is interrupted before or during - * the call, even if the value is already available. - * - * @throws InterruptedException if the current thread was interrupted before - * or during the call (optional but recommended). - * @throws TimeoutException if operation timed out - * @throws ExecutionException if execution fails - */ - @Override - public V get(long timeout, TimeUnit unit) throws InterruptedException, - TimeoutException, ExecutionException { - return sync.get(unit.toNanos(timeout)); - } - - /** - * The default {@link AbstractFuture} implementation throws {@code - * InterruptedException} if the current thread is interrupted before or during - * the call, even if the value is already available. - * - * @throws InterruptedException if the current thread was interrupted before - * or during the call (optional but recommended). - * @throws ExecutionException if execution fails - */ - @Override - public V get() throws InterruptedException, ExecutionException { - return sync.get(); - } - - /** - * Checks if the sync is not in the running state. - */ - @Override - public boolean isDone() { - return sync.isDone(); - } - - /** - * Checks if the sync is in the cancelled state. - */ - @Override - public boolean isCancelled() { - return sync.isCancelled(); - } - - public boolean isSucceeded() { - return sync.isSuccess(); - } - - public boolean isFailed() { - return sync.isFailed(); - } - - /** - * Default implementation of cancel that cancels the future. - */ - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - if (!sync.cancel()) { - return false; - } - done(); - if (mayInterruptIfRunning) { - interruptTask(); - } - return true; - } - - /** - * Subclasses should invoke this method to set the result of the computation - * to {@code value}. This will set the state of the future to - * {@link AbstractFuture.Sync#COMPLETED} and call {@link #done()} if the - * state was successfully changed. - * - * @param value the value that was the result of the task. - * @return true if the state was successfully changed. - */ - protected boolean set(V value) { - boolean result = sync.set(value); - if (result) { - done(); - } - return result; - } - - /** - * Subclasses should invoke this method to set the result of the computation - * to an error, {@code throwable}. This will set the state of the future to - * {@link AbstractFuture.Sync#COMPLETED} and call {@link #done()} if the - * state was successfully changed. - * - * @param exception the exception that the task failed with. - * @return true if the state was successfully changed. - */ - protected boolean setException(Exception exception) { - boolean result = sync.setException(exception); - if (result) { - done(); - } - return result; - } - - /** - * Subclasses should invoke this method to mark the future as cancelled. - * This will set the state of the future to {@link - * AbstractFuture.Sync#CANCELLED} and call {@link #done()} if the state was - * successfully changed. - * - * @return true if the state was successfully changed. - */ - protected final boolean cancel() { - boolean result = sync.cancel(); - if (result) { - done(); - } - return result; - } - - /** - * Called by the success, failed, or cancelled methods to indicate that the - * value is now available and the latch can be released. Subclasses can - * use this method to deal with any actions that should be undertaken when - * the task has completed. - */ - protected void done() { - } - - /** - * Subclasses can override this method to implement interruption of the - * future's computation. The method is invoked automatically by a successful - * call to {@link #cancel(boolean) cancel(true)}. - * The default implementation does nothing. - */ - protected void interruptTask() { - } - - /** - *

- * Following the contract of {@link AbstractQueuedSynchronizer} we create a - * private subclass to hold the synchronizer. This synchronizer is used to - * implement the blocking and waiting calls as well as to handle state changes - * in a thread-safe manner. The current state of the future is held in the - * Sync state, and the lock is released whenever the state changes to either - * {@link #COMPLETED} or {@link #CANCELLED}. - *

- *

- * To avoid races between threads doing release and acquire, we transition - * to the final state in two steps. One thread will successfully CAS from - * RUNNING to COMPLETING, that thread will then set the result of the - * computation, and only then transition to COMPLETED or CANCELLED. - *

- *

- * We don't use the integer argument passed between acquire methods so we - * pass around a -1 everywhere. - *

- */ - static final class Sync extends AbstractQueuedSynchronizer { - - private static final long serialVersionUID = -796072460488712821L; - - static final int RUNNING = 0; - static final int COMPLETING = 1; - static final int COMPLETED = 2; - static final int CANCELLED = 4; - - private V value; - private Exception exception; - - /* - * Acquisition succeeds if the future is done, otherwise it fails. - */ - @Override - protected int tryAcquireShared(int ignored) { - return isDone() ? 1 : -1; - } - - /* - * We always allow a release to go through, this means the state has been - * successfully changed and the result is available. - */ - @Override - protected boolean tryReleaseShared(int finalState) { - setState(finalState); - return true; - } - - /** - * Blocks until the task is complete or the timeout expires. Throws a - * {@link TimeoutException} if the timer expires, otherwise behaves like - * {@link #get()}. - */ - V get(long nanos) throws TimeoutException, CancellationException, - ExecutionException, InterruptedException { - // Attempt to acquire the shared lock with a timeout. - if (!tryAcquireSharedNanos(-1, nanos)) { - throw new TimeoutException("Timeout waiting for task."); - } - return getValue(); - } - - /** - * Blocks until {@link #complete(Object, Exception, int)} has been - * successfully called. Throws a {@link CancellationException} if the task - * was cancelled, or a {@link ExecutionException} if the task completed with - * an error. - */ - V get() throws CancellationException, ExecutionException, - InterruptedException { - // Acquire the shared lock allowing interruption. - acquireSharedInterruptibly(-1); - return getValue(); - } - - /** - * Implementation of the actual value retrieval. Will return the value - * on success, an exception on failure, a cancellation on cancellation, or - * an illegal state if the synchronizer is in an invalid state. - */ - private V getValue() throws CancellationException, ExecutionException { - int state = getState(); - switch (state) { - case COMPLETED: - if (exception != null) { - throw new ExecutionException(exception); - } else { - return value; - } - case CANCELLED: - throw new CancellationException("task was cancelled"); - default: - throw new IllegalStateException("error, synchronizer in invalid state: " + state); - } - } - - /** - * Checks if the state is {@link #COMPLETED} or {@link #CANCELLED}. - */ - boolean isDone() { - return (getState() & (COMPLETED | CANCELLED)) != 0; - } - - /** - * Checks if the state is {@link #CANCELLED}. - */ - boolean isCancelled() { - return getState() == CANCELLED; - } - - boolean isSuccess() { - return value != null && getState() == COMPLETED; - } - - boolean isFailed() { - return exception != null && getState() == COMPLETED; - } - - /** - * Transition to the COMPLETED state and set the value. - */ - boolean set(V v) { - return complete(v, null, COMPLETED); - } - - /** - * Transition to the COMPLETED state and set the exception. - */ - boolean setException(Exception exception) { - return complete(null, exception, COMPLETED); - } - - /** - * Transition to the CANCELLED state. - */ - boolean cancel() { - return complete(null, null, CANCELLED); - } - - /** - * Implementation of completing a task. Either {@code v} or {@code t} will - * be set but not both. The {@code finalState} is the state to change to - * from {@link #RUNNING}. If the state is not in the RUNNING state we - * return {@code false} after waiting for the state to be set to a valid - * final state ({@link #COMPLETED} or {@link #CANCELLED}). - * - * @param v the value to set as the result of the computation. - * @param exception the exception to set as the result of the computation. - * @param finalState the state to transition to. - */ - private boolean complete(V v, Exception exception, int finalState) { - boolean doCompletion = compareAndSetState(RUNNING, COMPLETING); - if (doCompletion) { - // If this thread successfully transitioned to COMPLETING, set the value - // and exception and then release to the final state. - this.value = v; - this.exception = exception; - releaseShared(finalState); - } else if (getState() == COMPLETING) { - // If some other thread is currently completing the future, block until - // they are done so we can guarantee completion. - acquireShared(-1); - } - return doCompletion; - } - } -} diff --git a/src/main/java/org/xbib/netty/http/client/util/ClientAuthMode.java b/src/main/java/org/xbib/netty/http/client/util/ClientAuthMode.java deleted file mode 100644 index 008fc2d..0000000 --- a/src/main/java/org/xbib/netty/http/client/util/ClientAuthMode.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.util; - -/** - * Client authentication modes, useful for SSL channels. - */ -public enum ClientAuthMode { - NONE, WANT, NEED -} diff --git a/src/main/java/org/xbib/netty/http/client/util/InetAddressKey.java b/src/main/java/org/xbib/netty/http/client/util/InetAddressKey.java deleted file mode 100644 index eb7cd7b..0000000 --- a/src/main/java/org/xbib/netty/http/client/util/InetAddressKey.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.util; - -import io.netty.handler.codec.http.HttpVersion; - -import java.net.InetSocketAddress; - -/** - * A key for host, port, HTTP version, and secure transport mode of a channel for HTTP. - */ -public class InetAddressKey { - - private final String host; - - private final int port; - - private final HttpVersion version; - - private final Boolean secure; - - private InetSocketAddress inetSocketAddress; - - public InetAddressKey(String host, int port, HttpVersion version, boolean secure) { - this.host = host; - this.port = port == -1 ? secure ? 443 : 80 : port; - this.version = version; - this.secure = secure; - } - - public InetSocketAddress getInetSocketAddress() { - if (inetSocketAddress == null) { - this.inetSocketAddress = new InetSocketAddress(host, port); - } - return inetSocketAddress; - } - - public HttpVersion getVersion() { - return version; - } - - public boolean isSecure() { - return secure; - } - - public String toString() { - return host + ":" + port + " (version:" + version + ",secure:" + secure + ")"; - } - - @Override - public boolean equals(Object object) { - return object instanceof InetAddressKey && - host.equals(((InetAddressKey) object).host) && - port == ((InetAddressKey) object).port && - version.equals(((InetAddressKey) object).version) && - secure.equals(((InetAddressKey) object).secure); - } - - @Override - public int hashCode() { - return host.hashCode() ^ port ^ version.hashCode() ^ secure.hashCode(); - } -} diff --git a/src/main/java/org/xbib/netty/http/client/util/LimitedHashSet.java b/src/main/java/org/xbib/netty/http/client/util/LimitedHashSet.java deleted file mode 100644 index 35aa242..0000000 --- a/src/main/java/org/xbib/netty/http/client/util/LimitedHashSet.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.util; - -import java.util.Collection; -import java.util.LinkedHashSet; - -/** - * A {@link java.util.Set} with limited size. If the size is exceeded, an exception is thrown. - * @param the element type - */ -public final class LimitedHashSet extends LinkedHashSet { - - private static final long serialVersionUID = 1838128758142912702L; - - private final int max; - - public LimitedHashSet(int max) { - this.max = max; - } - - @Override - public boolean add(E element) { - if (max < size()) { - throw new IllegalStateException("limit exceeded"); - } - return super.add(element); - } - - @Override - public boolean addAll(Collection elements) { - boolean b = false; - for (E element : elements) { - if (max < size()) { - throw new IllegalStateException("limit exceeded"); - } - b = b || super.add(element); - } - return b; - } -} diff --git a/src/main/java/org/xbib/netty/http/client/util/NetworkClass.java b/src/main/java/org/xbib/netty/http/client/util/NetworkClass.java index c571627..12e821a 100644 --- a/src/main/java/org/xbib/netty/http/client/util/NetworkClass.java +++ b/src/main/java/org/xbib/netty/http/client/util/NetworkClass.java @@ -1,18 +1,3 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ package org.xbib.netty.http.client.util; /** diff --git a/src/main/java/org/xbib/netty/http/client/util/NetworkProtocolVersion.java b/src/main/java/org/xbib/netty/http/client/util/NetworkProtocolVersion.java index 03baecc..1b7fc8f 100644 --- a/src/main/java/org/xbib/netty/http/client/util/NetworkProtocolVersion.java +++ b/src/main/java/org/xbib/netty/http/client/util/NetworkProtocolVersion.java @@ -1,18 +1,3 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ package org.xbib.netty.http.client.util; /** diff --git a/src/main/java/org/xbib/netty/http/client/util/NetworkUtils.java b/src/main/java/org/xbib/netty/http/client/util/NetworkUtils.java index d5df41a..5ffca22 100644 --- a/src/main/java/org/xbib/netty/http/client/util/NetworkUtils.java +++ b/src/main/java/org/xbib/netty/http/client/util/NetworkUtils.java @@ -1,18 +1,3 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ package org.xbib.netty.http.client.util; import java.io.IOException; diff --git a/src/test/java/org/xbib/netty/http/client/test/AkamaiTest.java b/src/test/java/org/xbib/netty/http/client/test/AkamaiTest.java index 1ce4651..bbc6466 100644 --- a/src/test/java/org/xbib/netty/http/client/test/AkamaiTest.java +++ b/src/test/java/org/xbib/netty/http/client/test/AkamaiTest.java @@ -1,58 +1,54 @@ package org.xbib.netty.http.client.test; +import org.junit.Ignore; import org.junit.Test; -import org.xbib.netty.http.client.HttpClient; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.test.LoggingBase; import java.nio.charset.StandardCharsets; -import java.util.logging.ConsoleHandler; -import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; -/** - */ -public class AkamaiTest { - - static { - System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); - LogManager.getLogManager().reset(); - Logger rootLogger = LogManager.getLogManager().getLogger(""); - Handler handler = new ConsoleHandler(); - handler.setFormatter(new SimpleFormatter()); - rootLogger.addHandler(handler); - rootLogger.setLevel(Level.ALL); - for (Handler h : rootLogger.getHandlers()) { - handler.setFormatter(new SimpleFormatter()); - h.setLevel(Level.ALL); - } - } +@Ignore +public class AkamaiTest extends LoggingBase { private static final Logger logger = Logger.getLogger(""); + /** + * 2018-02-27 23:43:32.048 INFORMATION [client] io.netty.handler.codec.http2.Http2FrameLogger + * logRstStream [id: 0x4fe29f1e, L:/192.168.178.23:49429 - R:http2.akamai.com/104.94.191.203:443] + * INBOUND RST_STREAM: streamId=2 errorCode=8 + * 2018-02-27 23:43:32.049 SCHWERWIEGEND [] org.xbib.netty.http.client.test.a.AkamaiTest lambda$testAkamaiHttps$0 + * HTTP/2 to HTTP layer caught stream reset + * io.netty.handler.codec.http2.Http2Exception$StreamException: HTTP/2 to HTTP layer caught stream reset + */ @Test - public void testAkamaiHttps() throws Exception { - HttpClient httpClient = HttpClient.getInstance(); - httpClient.prepareGet("https://http2.akamai.com/demo/h2_demo_frame.html") - .setHttp2() - .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e)) - .onResponse(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() - + " response body = " + response); - }) - .onPushReceived((requestHeaders, fullHttpResponse) -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "received push: request headers = " + requestHeaders - + " status = " + fullHttpResponse.status() - + " response headers = " + fullHttpResponse.headers().entries() - + " response body = " + response - ); - }) - .execute() - .get(); - httpClient.close(); + public void testAkamaiHttps() { + Client client = new Client(); + try { + Request request = Request.get() + //.setURL("https://http2.akamai.com/demo/h2_demo_frame.html") + .setURL("https://http2.akamai.com/") + .setVersion("HTTP/2.0") + .build() + .setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e)) + .setResponseListener(fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + logger.log(Level.INFO, "status = " + fullHttpResponse.status() + + " response body = " + response); + }) + .setPushListener((requestHeaders, fullHttpResponse) -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + logger.log(Level.INFO, "received push: request headers = " + requestHeaders + + " status = " + fullHttpResponse.status() + + " response headers = " + fullHttpResponse.headers().entries() + + " response body = " + response + ); + }); + client.execute(request).get(); + } finally { + client.shutdownGracefully(); + } } } diff --git a/src/test/java/org/xbib/netty/http/client/test/ClientTest.java b/src/test/java/org/xbib/netty/http/client/test/ClientTest.java new file mode 100644 index 0000000..8af9b65 --- /dev/null +++ b/src/test/java/org/xbib/netty/http/client/test/ClientTest.java @@ -0,0 +1,184 @@ +package org.xbib.netty.http.client.test; + +import io.netty.handler.codec.http.HttpMethod; +import org.junit.Ignore; +import org.junit.Test; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.HttpAddress; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.transport.Transport; + +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class ClientTest { + + private static final Logger logger = Logger.getLogger(ClientTest.class.getName()); + + @Test + @Ignore + public void testHttp1() throws Exception { + Client client = new Client(); + try { + Transport transport = client.newTransport(HttpAddress.http1("fl.hbz-nrw.de")); + transport.setResponseListener(msg -> logger.log(Level.INFO, "got response: " + + msg.headers().entries() + + msg.content().toString(StandardCharsets.UTF_8) + + " status=" + msg.status().code())); + transport.connect(); + transport.awaitSettings(); + simpleRequest(transport); + transport.get(); + transport.close(); + } finally { + client.shutdown(); + } + } + + @Test + @Ignore + public void testHttp1ParallelRequests() { + Client client = new Client(); + try { + Request request1 = Request.builder(HttpMethod.GET) + .setURL("http://fl.hbz-nrw.de").setVersion("HTTP/1.1") + .build() + .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + + msg.headers().entries() + + //msg.content().toString(StandardCharsets.UTF_8) + + " status=" + msg.status().code())); + Request request2 = Request.builder(HttpMethod.GET) + .setURL("http://fl.hbz-nrw.de/app/fl/").setVersion("HTTP/1.1") + .build() + .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + + msg.headers().entries() + + //msg.content().toString(StandardCharsets.UTF_8) + + " status=" + msg.status().code())); + + client.execute(request1); + client.execute(request2); + + } finally { + client.shutdownGracefully(); + } + } + + @Test + @Ignore + public void testHttp2() throws Exception { + String host = "webtide.com"; + Client client = new Client(); + client.logDiagnostics(Level.INFO); + try { + Transport transport = client.newTransport(HttpAddress.http2(host)); + transport.setResponseListener(msg -> logger.log(Level.INFO, "got response: " + + msg.headers().entries() + + msg.content().toString(StandardCharsets.UTF_8) + + " status=" + msg.status().code())); + transport.setPushListener((hdrs, msg) -> logger.log(Level.INFO, "got push: " + + msg.headers().entries() + + msg.content().toString(StandardCharsets.UTF_8))); + transport.connect(); + transport.awaitSettings(); + simpleRequest(transport); + transport.get(); + transport.close(); + } finally { + client.shutdown(); + } + } + + @Test + public void testHttp2Request() { + //String url = "https://webtide.com"; + String url = "https://http2-push.io"; + // TODO register push announces into promises in order to wait for them all. + Client client = new Client(); + try { + Request request = Request.builder(HttpMethod.GET) + .setURL(url).setVersion("HTTP/2.0") + .build() + .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + + msg.headers().entries() + + msg.content().toString(StandardCharsets.UTF_8) + + " status=" + msg.status().code())) + .setPushListener((hdrs, msg) -> logger.log(Level.INFO, "got push: " + + msg.headers().entries() + + msg.content().toString(StandardCharsets.UTF_8)) + ); + client.execute(request).get(); + + } finally { + client.shutdownGracefully(); + } + } + + @Test + @Ignore + public void testHttp2TwoRequestsOnSameConnection() { + Client client = new Client(); + try { + Request request1 = Request.builder(HttpMethod.GET) + .setURL("https://webtide.com").setVersion("HTTP/2.0") + .build() + .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + + msg.headers().entries() + + //msg.content().toString(StandardCharsets.UTF_8) + + " status=" + msg.status().code())) + .setPushListener((hdrs, msg) -> logger.log(Level.INFO, "got push: " + + msg.headers().entries() + //msg.content().toString(StandardCharsets.UTF_8)) + )); + + Request request2 = Request.builder(HttpMethod.GET) + .setURL("https://webtide.com/why-choose-jetty/").setVersion("HTTP/2.0") + .build() + .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + + msg.headers().entries() + + //msg.content().toString(StandardCharsets.UTF_8) + + " status=" + msg.status().code())) + .setPushListener((hdrs, msg) -> logger.log(Level.INFO, "got push: " + + msg.headers().entries() + + //msg.content().toString(StandardCharsets.UTF_8) + + " status=" + msg.status().code())); + + client.execute(request1).execute(request2); + + } finally { + client.shutdownGracefully(); + } + } + + @Test + @Ignore + public void testMixed() throws Exception { + Client client = new Client(); + try { + Transport transport = client.newTransport(HttpAddress.http1("xbib.org")); + transport.setResponseListener(msg -> logger.log(Level.INFO, "got response: " + + msg.content().toString(StandardCharsets.UTF_8))); + transport.connect(); + transport.awaitSettings(); + simpleRequest(transport); + transport.get(); + transport.close(); + + transport = client.newTransport(HttpAddress.http2("google.com")); + transport.setResponseListener(msg -> logger.log(Level.INFO, "got response: " + + msg.content().toString(StandardCharsets.UTF_8))); + transport.connect(); + transport.awaitSettings(); + simpleRequest(transport); + transport.get(); + transport.close(); + } finally { + client.shutdown(); + } + } + + private void simpleRequest(Transport transport) { + transport.execute(Request.builder(HttpMethod.GET).setURL(transport.httpAddress().base()).build()); + } + +} diff --git a/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java b/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java new file mode 100644 index 0000000..a9d42f9 --- /dev/null +++ b/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java @@ -0,0 +1,46 @@ +package org.xbib.netty.http.client.test; + +import io.netty.handler.codec.http.FullHttpResponse; +import org.junit.Test; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.Request; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class CompletableFutureTest { + + private static final Logger logger = Logger.getLogger(ElasticsearchTest.class.getName()); + + /** + * Get some weird content from one URL and post it to another URL, by composing completable futures. + */ + @Test + public void testComposeCompletableFutures() { + Client client = new Client(); + try { + final Function httpResponseStringFunction = response -> + response.content().toString(StandardCharsets.UTF_8); + Request request = Request.get() + .setURL("http://alkmene.hbz-nrw.de/repository/org/xbib/content/2.0.0-SNAPSHOT/maven-metadata-local.xml") + .build(); + CompletableFuture completableFuture = client.execute(request, httpResponseStringFunction) + .exceptionally(Throwable::getMessage) + .thenCompose(content -> { + logger.log(Level.INFO, content); + // POST is not allowed, we don't care + return client.execute(Request.post() + .setURL("http://google.com/") + .addParam("query", content) + .build(), httpResponseStringFunction); + }); + String result = completableFuture.join(); + logger.log(Level.INFO, "completablefuture result = " + result); + } finally { + client.shutdownGracefully(); + } + } +} diff --git a/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java b/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java index 49130a2..965843a 100644 --- a/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java +++ b/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java @@ -1,144 +1,93 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ package org.xbib.netty.http.client.test; +import org.junit.Ignore; import org.junit.Test; -import org.xbib.netty.http.client.HttpClient; -import org.xbib.netty.http.client.HttpRequestBuilder; -import org.xbib.netty.http.client.HttpRequestContext; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.transport.Transport; -import java.io.IOException; -import java.net.ConnectException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; -import java.util.logging.ConsoleHandler; -import java.util.logging.Handler; +import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; -import java.util.logging.LogManager; import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertEquals; -/** - */ -public class ElasticsearchTest { +@Ignore +public class ElasticsearchTest extends LoggingBase { - static { - System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); - LogManager.getLogManager().reset(); - Logger rootLogger = LogManager.getLogManager().getLogger(""); - Handler handler = new ConsoleHandler(); - handler.setFormatter(new SimpleFormatter()); - rootLogger.addHandler(handler); - rootLogger.setLevel(Level.INFO); - for (Handler h : rootLogger.getHandlers()) { - handler.setFormatter(new SimpleFormatter()); - h.setLevel(Level.INFO); - } - } - - private static final Logger logger = Logger.getLogger(""); + private static final Logger logger = Logger.getLogger(ElasticsearchTest.class.getName()); @Test - public void testElasticsearchCreateDocument() throws Exception { - HttpClient httpClient = HttpClient.builder() - .build(); + public void testElasticsearchCreateDocument() { + Client client = new Client(); try { - HttpRequestContext requestContext = httpClient.preparePut() - .setURL("http://localhost:9200/test/test/1") - .json("{\"text\":\"Hello World\"}") - .onResponse(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }) - .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e)) - .execute() - .get(); - logger.log(Level.FINE, "took = " + requestContext.took()); - } catch (Exception exception) { - assertTrue(exception.getCause() instanceof ConnectException); - logger.log(Level.INFO, "got expected exception"); + Request request = Request.put().setURL("http://localhost:9200/test/test/1") + .json("{\"text\":\"Hello World\"}") + .build() + .setResponseListener(fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); + }) + .setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e)); + client.execute(request); + } finally { + client.shutdownGracefully(); } - httpClient.close(); } @Test - public void testElasticsearchMatchQuery() throws Exception { - HttpClient httpClient = HttpClient.builder() - .build(); + public void testElasticsearchMatchQuery() { + Client client = new Client(); try { - HttpRequestContext requestContext = httpClient.preparePost() - .setURL("http://localhost:9200/test/_search") - .json("{\"query\":{\"match\":{\"_all\":\"Hello World\"}}}") - .onResponse(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }) - .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e)) - .execute() - .get(); - logger.log(Level.FINE, "took = " + requestContext.took()); - } catch (Exception exception) { - assertTrue(exception.getCause() instanceof ConnectException); - logger.log(Level.INFO, "got expected exception"); + Request request = Request.post().setURL("http://localhost:9200/test/_search") + .json("{\"query\":{\"match\":{\"text\":\"Hello World\"}}}") + .build() + .setResponseListener(fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); + }) + .setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e)); + client.execute(request).get(); + } finally { + client.shutdownGracefully(); } - httpClient.close(); } @Test - public void testElasticsearchConcurrent() throws Exception { - int max = 100; - HttpClient httpClient = HttpClient.builder() - .build(); - List queries = new ArrayList<>(); - for (int i = 0; i < max; i++) { - queries.add(createQuery(httpClient)); - } - List contexts = new ArrayList<>(); - for (int i = 0; i < max; i++) { - contexts.add(queries.get(i).execute()); - } - List responses = new ArrayList<>(); - for (int i = 0; i < max; i++) { - try { - responses.add(contexts.get(i).get()); - } catch (Exception exception) { - assertTrue(exception.getCause() instanceof ConnectException); - logger.log(Level.INFO, "got expected exception"); + public void testElasticsearchConcurrent() { + Client client = Client.builder().setReadTimeoutMillis(20000).build(); + int max = 1000; + try { + List queries = new ArrayList<>(); + for (int i = 0; i < max; i++) { + queries.add(newRequest()); } + Transport transport = client.execute(queries.get(0)).get(); + for (int i = 1; i < max; i++) { + transport.execute(queries.get(i)).get(); + } + } finally { + client.shutdownGracefully(); + logger.log(Level.INFO, "count=" + count); } - for (int i = 0; i < responses.size(); i++) { - logger.log(Level.FINE, "took = " + responses.get(i).took()); - } - httpClient.close(); - logger.log(Level.INFO, "pool peak = " + httpClient.poolMap().getHttpClientChannelPoolHandler().getPeak()); + assertEquals(max, count.get()); } - private HttpRequestBuilder createQuery(HttpClient httpClient) throws IOException { - return httpClient.preparePost() + private Request newRequest() { + return Request.post() .setURL("http://localhost:9200/test/_search") - .json("{\"query\":{\"match\":{\"_all\":\"Hello World\"}}}") + .json("{\"query\":{\"match\":{\"text\":\"Hello World\"}}}") .addHeader("connection", "keep-alive") - .onResponse(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }) - .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e)); + .build() + .setResponseListener(fullHttpResponse -> + logger.log(Level.FINE, "status = " + fullHttpResponse.status() + + " counter = " + count.incrementAndGet() + + " response body = " + fullHttpResponse.content().toString(StandardCharsets.UTF_8))) + .setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e)); } + + private final AtomicInteger count = new AtomicInteger(); } diff --git a/src/test/java/org/xbib/netty/http/client/test/ExceptionTest.java b/src/test/java/org/xbib/netty/http/client/test/ExceptionTest.java deleted file mode 100644 index 130aca4..0000000 --- a/src/test/java/org/xbib/netty/http/client/test/ExceptionTest.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2017 Jörg Prante - * - * Jörg Prante licenses this file to you under the Apache License, - * version 2.0 (the "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at: - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations - * under the License. - */ -package org.xbib.netty.http.client.test; - -import org.junit.Test; -import org.xbib.netty.http.client.HttpClient; - -import java.net.ConnectException; -import java.nio.charset.StandardCharsets; -import java.util.logging.ConsoleHandler; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; - -import static org.junit.Assert.assertTrue; - -/** - */ -public class ExceptionTest { - - static { - System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); - LogManager.getLogManager().reset(); - Logger rootLogger = LogManager.getLogManager().getLogger(""); - Handler handler = new ConsoleHandler(); - handler.setFormatter(new SimpleFormatter()); - rootLogger.addHandler(handler); - rootLogger.setLevel(Level.ALL); - for (Handler h : rootLogger.getHandlers()) { - handler.setFormatter(new SimpleFormatter()); - h.setLevel(Level.ALL); - } - } - - private static final Logger logger = Logger.getLogger(""); - - @Test - public void testConnectionRefused() throws Exception { - - // this basically tests if the connection refuse terminates. - - HttpClient httpClient = HttpClient.builder() - .build(); - try { - httpClient.prepareGet() - .setURL("http://localhost:1234") - .onResponse(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }) - .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e)) - .execute() - .get(); - } catch (Exception exception) { - assertTrue(exception.getCause() instanceof ConnectException); - logger.log(Level.INFO, "got expected exception"); - } - httpClient.close(); - } -} diff --git a/src/test/java/org/xbib/netty/http/client/test/GoogleTest.java b/src/test/java/org/xbib/netty/http/client/test/GoogleTest.java deleted file mode 100644 index 57fe567..0000000 --- a/src/test/java/org/xbib/netty/http/client/test/GoogleTest.java +++ /dev/null @@ -1,136 +0,0 @@ -package org.xbib.netty.http.client.test; - -import org.junit.Test; -import org.xbib.netty.http.client.HttpClient; -import org.xbib.netty.http.client.HttpRequestBuilder; -import org.xbib.netty.http.client.HttpRequestContext; - -import java.nio.charset.StandardCharsets; -import java.util.logging.ConsoleHandler; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; - -/** - */ -public class GoogleTest { - - static { - System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); - LogManager.getLogManager().reset(); - Logger rootLogger = LogManager.getLogManager().getLogger(""); - Handler handler = new ConsoleHandler(); - handler.setFormatter(new SimpleFormatter()); - rootLogger.addHandler(handler); - rootLogger.setLevel(Level.ALL); - for (Handler h : rootLogger.getHandlers()) { - handler.setFormatter(new SimpleFormatter()); - h.setLevel(Level.ALL); - } - } - - private static final Logger logger = Logger.getLogger(""); - - @Test - public void testGoogleHttp1() throws Exception { - HttpClient httpClient = HttpClient.builder() - .build(); - httpClient.prepareGet() - .setURL("http://www.google.com") - .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e)) - .onHeaders(headers -> logger.log(Level.INFO, headers.toString())) - .onResponse(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }) - .execute() - .get(); - httpClient.close(); - } - - - public void testGoogleWithoutFollowRedirects() throws Exception { - HttpClient httpClient = HttpClient.builder() - .build(); - httpClient.prepareGet() - .setURL("http://google.com") - .setFollowRedirect(false) - .onResponse(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }) - .execute() - .get(); - logger.log(Level.INFO, "pool size = " + httpClient.poolMap().size()); - httpClient.close(); - } - - - @Test - public void testGoogleHttps1() throws Exception { - HttpClient httpClient = HttpClient.builder() - .build(); - httpClient.prepareGet() - .setURL("https://www.google.com") - .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e)) - .onResponse(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }) - .execute() - .get(); - httpClient.close(); - } - - @Test - public void testGoogleHttp2() throws Exception { - HttpClient httpClient = HttpClient.builder() - .build(); - - httpClient.prepareGet() - .setVersion("HTTP/2.0") - .setURL("https://www.google.com") - .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e)) - .onResponse(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }) - .execute() - .get(); - httpClient.close(); - } - - @Test - public void testGoogleHttpTwo() throws Exception { - HttpClient httpClient = HttpClient.builder() - .build(); - - HttpRequestBuilder builder1 = httpClient.prepareGet() - .setVersion("HTTP/2.0") - .setURL("https://www.google.com") - .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e)) - .onResponse(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }); - - HttpRequestBuilder builder2 = httpClient.prepareGet() - .setVersion("HTTP/2.0") - .setURL("https://www.google.com") - .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e)) - .onResponse(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }); - - HttpRequestContext context1 = builder1.execute(); - HttpRequestContext context2 = builder2.execute(); - context1.get(); - context2.get(); - - httpClient.close(); - } -} diff --git a/src/test/java/org/xbib/netty/http/client/test/Http2FrameAdapterTest.java b/src/test/java/org/xbib/netty/http/client/test/Http2FrameAdapterTest.java deleted file mode 100644 index a4f5dd6..0000000 --- a/src/test/java/org/xbib/netty/http/client/test/Http2FrameAdapterTest.java +++ /dev/null @@ -1,171 +0,0 @@ -package org.xbib.netty.http.client.test; - -import io.netty.bootstrap.Bootstrap; -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInitializer; -import io.netty.channel.ChannelPromise; -import io.netty.channel.EventLoopGroup; -import io.netty.channel.nio.NioEventLoopGroup; -import io.netty.channel.socket.nio.NioSocketChannel; -import io.netty.handler.codec.http.HttpMethod; -import io.netty.handler.codec.http2.DefaultHttp2Headers; -import io.netty.handler.codec.http2.Http2ConnectionHandler; -import io.netty.handler.codec.http2.Http2ConnectionHandlerBuilder; -import io.netty.handler.codec.http2.Http2Exception; -import io.netty.handler.codec.http2.Http2FrameAdapter; -import io.netty.handler.codec.http2.Http2FrameLogger; -import io.netty.handler.codec.http2.Http2SecurityUtil; -import io.netty.handler.codec.http2.Http2Settings; -import io.netty.handler.logging.LogLevel; -import io.netty.handler.logging.LoggingHandler; -import io.netty.handler.ssl.ApplicationProtocolConfig; -import io.netty.handler.ssl.ApplicationProtocolNames; -import io.netty.handler.ssl.SslContext; -import io.netty.handler.ssl.SslContextBuilder; -import io.netty.handler.ssl.SslHandler; -import io.netty.handler.ssl.SslProvider; -import io.netty.handler.ssl.SupportedCipherSuiteFilter; -import io.netty.handler.ssl.util.InsecureTrustManagerFactory; -import org.junit.Test; - -import javax.net.ssl.SNIHostName; -import javax.net.ssl.SNIServerName; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLParameters; -import java.net.InetSocketAddress; -import java.util.Arrays; -import java.util.concurrent.CountDownLatch; -import java.util.logging.ConsoleHandler; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; - -/** - */ -public class Http2FrameAdapterTest { - - static { - System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); - LogManager.getLogManager().reset(); - Logger rootLogger = LogManager.getLogManager().getLogger(""); - Handler handler = new ConsoleHandler(); - handler.setFormatter(new SimpleFormatter()); - rootLogger.addHandler(handler); - rootLogger.setLevel(Level.ALL); - for (Handler h : rootLogger.getHandlers()) { - handler.setFormatter(new SimpleFormatter()); - h.setLevel(Level.ALL); - } - } - - private static final Logger logger = Logger.getLogger(""); - - @Test - public void testHttp2FrameAdapter() throws Exception { - final int serverExpectedDataFrames = 1; - //final InetSocketAddress inetSocketAddress = new InetSocketAddress("http2-push.io", 443); - final InetSocketAddress inetSocketAddress = new InetSocketAddress("webtide.com", 443); - final CountDownLatch dataLatch = new CountDownLatch(serverExpectedDataFrames); - EventLoopGroup group = new NioEventLoopGroup(); - Channel clientChannel = null; - try { - Bootstrap bs = new Bootstrap(); - bs.group(group); - bs.channel(NioSocketChannel.class); - bs.handler(new ChannelInitializer() { - @Override - protected void initChannel(Channel ch) throws Exception { - ch.pipeline().addLast(new TrafficLoggingHandler()); - SslContext sslContext = SslContextBuilder.forClient() - .sslProvider(SslProvider.OPENSSL) - .trustManager(InsecureTrustManagerFactory.INSTANCE) - .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) - .applicationProtocolConfig(new ApplicationProtocolConfig( - ApplicationProtocolConfig.Protocol.ALPN, - ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, - ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, - ApplicationProtocolNames.HTTP_2)) - .build(); - SslHandler sslHandler = sslContext.newHandler(ch.alloc()); - SSLEngine engine = sslHandler.engine(); - String fullQualifiedHostname = inetSocketAddress.getHostName(); - SSLParameters params = engine.getSSLParameters(); - params.setServerNames(Arrays.asList(new SNIServerName[]{new SNIHostName(fullQualifiedHostname)})); - engine.setSSLParameters(params); - ch.pipeline().addLast(sslHandler); - Http2ConnectionHandlerBuilder builder = new Http2ConnectionHandlerBuilder(); - builder.server(false); - builder.frameListener(new Http2FrameAdapter() { - @Override - public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) - throws Http2Exception { - logger.log(Level.FINE, "settings received, now writing headers"); - Http2ConnectionHandler handler = ctx.pipeline().get(Http2ConnectionHandler.class); - handler.encoder().writeHeaders(ctx, 3, - new DefaultHttp2Headers().method(HttpMethod.GET.asciiName()) - .path("/") - .scheme("https") - .authority(inetSocketAddress.getHostName()), - 0, true, ctx.newPromise()); - ctx.channel().flush(); - } - - @Override - public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, - boolean endOfStream) throws Http2Exception { - dataLatch.countDown(); - return super.onDataRead(ctx, streamId, data, padding, endOfStream); - } - }); - builder.frameLogger(new Http2FrameLogger(LogLevel.INFO, "client")); - ch.pipeline().addLast(builder.build()); - } - }); - clientChannel = bs.connect(inetSocketAddress).syncUninterruptibly().channel(); - logger.log(Level.INFO, () -> "waiting for HTTP/2 data"); - dataLatch.await(); - logger.log(Level.INFO, () -> "done, data arrived"); - } finally { - if (clientChannel != null) { - clientChannel.close(); - } - group.shutdownGracefully(); - } - } - - class TrafficLoggingHandler extends LoggingHandler { - - TrafficLoggingHandler() { - super("client", LogLevel.TRACE); - } - - @Override - public void channelRegistered(ChannelHandlerContext ctx) throws Exception { - ctx.fireChannelRegistered(); - } - - @Override - public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { - ctx.fireChannelUnregistered(); - } - - @Override - public void flush(ChannelHandlerContext ctx) throws Exception { - ctx.flush(); - } - - @Override - public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { - if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) { - ctx.write(msg, promise); - } else { - super.write(ctx, msg, promise); - } - } - } -} diff --git a/src/test/java/org/xbib/netty/http/client/test/Http2PushioTest.java b/src/test/java/org/xbib/netty/http/client/test/Http2PushioTest.java deleted file mode 100644 index fd8f1d2..0000000 --- a/src/test/java/org/xbib/netty/http/client/test/Http2PushioTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.xbib.netty.http.client.test; - -import org.junit.Test; -import org.xbib.netty.http.client.HttpClient; - -import java.nio.charset.StandardCharsets; -import java.util.logging.ConsoleHandler; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; - -/** - */ -public class Http2PushioTest { - - static { - System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); - LogManager.getLogManager().reset(); - Logger rootLogger = LogManager.getLogManager().getLogger(""); - Handler handler = new ConsoleHandler(); - handler.setFormatter(new SimpleFormatter()); - rootLogger.addHandler(handler); - rootLogger.setLevel(Level.ALL); - for (Handler h : rootLogger.getHandlers()) { - handler.setFormatter(new SimpleFormatter()); - h.setLevel(Level.ALL); - } - } - - private static final Logger logger = Logger.getLogger(""); - - @Test - public void testHttpPushIo() throws Exception { - HttpClient httpClient = HttpClient.builder() - .build(); - - httpClient.prepareGet() - .setVersion("HTTP/2.0") - .setURL("https://http2-push.io") - .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e)) - .onResponse(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }) - .execute() - .get(); - httpClient.close(); - } -} diff --git a/src/test/java/org/xbib/netty/http/client/test/HttpBinTest.java b/src/test/java/org/xbib/netty/http/client/test/HttpBinTest.java index 12b29a5..89a90d2 100644 --- a/src/test/java/org/xbib/netty/http/client/test/HttpBinTest.java +++ b/src/test/java/org/xbib/netty/http/client/test/HttpBinTest.java @@ -1,39 +1,21 @@ package org.xbib.netty.http.client.test; import org.junit.Test; -import org.xbib.netty.http.client.HttpClient; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.Request; import java.nio.charset.StandardCharsets; -import java.util.logging.ConsoleHandler; -import java.util.logging.Handler; import java.util.logging.Level; -import java.util.logging.LogManager; import java.util.logging.Logger; -import java.util.logging.SimpleFormatter; /** */ -public class HttpBinTest { +public class HttpBinTest extends LoggingBase { - static { - System.setProperty("java.util.logging.SimpleFormatter.format", - "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); - LogManager.getLogManager().reset(); - Logger rootLogger = LogManager.getLogManager().getLogger(""); - Handler handler = new ConsoleHandler(); - handler.setFormatter(new SimpleFormatter()); - rootLogger.addHandler(handler); - rootLogger.setLevel(Level.ALL); - for (Handler h : rootLogger.getHandlers()) { - handler.setFormatter(new SimpleFormatter()); - h.setLevel(Level.ALL); - } - } - - private static final Logger logger = Logger.getLogger(""); + private static final Logger logger = Logger.getLogger(HttpBinTest.class.getName()); /** - * Test httpbin.org cookie setter with HTTP/1.1. + * Test httpbin.org "Set-Cookie:" header after redirection of URL. * * The reponse body should be *
@@ -46,21 +28,22 @@ public class HttpBinTest {
      * @throws Exception
      */
     @Test
-    public void testHttpBinCookies() throws Exception {
-        HttpClient httpClient = HttpClient.builder()
-                .build();
-        httpClient.prepareGet()
-                .setURL("http://httpbin.org/cookies/set?name=value")
-                .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
-                .onCookie(cookie -> logger.log(Level.INFO, cookie.toString()))
-                .onHeaders(headers -> logger.log(Level.INFO, headers.toString()))
-                .onResponse(fullHttpResponse -> {
-                    String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
-                    logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
-                })
-                .execute()
-                .get();
-        httpClient.close();
+    public void testHttpBinCookies() {
+        Client client = new Client();
+        try {
+            Request request = Request.get()
+                    .setURL("http://httpbin.org/cookies/set?name=value")
+                    .build()
+                    .setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+                    .setCookieListener(cookie -> logger.log(Level.INFO, "this is the cookie " + cookie.toString()))
+                    .setHeadersListener(headers -> logger.log(Level.INFO, headers.toString()))
+                    .setResponseListener(fullHttpResponse -> {
+                        String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
+                        logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
+                    });
+            client.execute(request).get();
+        } finally {
+            client.shutdownGracefully();
+        }
     }
-
 }
diff --git a/src/test/java/org/xbib/netty/http/client/test/IndexHbzTest.java b/src/test/java/org/xbib/netty/http/client/test/IndexHbzTest.java
deleted file mode 100644
index d21da8c..0000000
--- a/src/test/java/org/xbib/netty/http/client/test/IndexHbzTest.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * Copyright 2017 Jörg Prante
- *
- * Jörg Prante licenses this file to you under the Apache License,
- * version 2.0 (the "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at:
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- */
-package org.xbib.netty.http.client.test;
-
-import io.netty.handler.codec.http.FullHttpResponse;
-import org.junit.Test;
-import org.xbib.netty.http.client.HttpClient;
-import org.xbib.netty.http.client.HttpRequestBuilder;
-import org.xbib.netty.http.client.HttpRequestContext;
-
-import java.nio.charset.StandardCharsets;
-import java.util.concurrent.CompletableFuture;
-import java.util.function.Function;
-import java.util.logging.ConsoleHandler;
-import java.util.logging.Handler;
-import java.util.logging.Level;
-import java.util.logging.LogManager;
-import java.util.logging.Logger;
-import java.util.logging.SimpleFormatter;
-
-/**
- */
-public class IndexHbzTest {
-
-    static {
-        System.setProperty("java.util.logging.SimpleFormatter.format",
-                "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
-        LogManager.getLogManager().reset();
-        Logger rootLogger = LogManager.getLogManager().getLogger("");
-        Handler handler = new ConsoleHandler();
-        handler.setFormatter(new SimpleFormatter());
-        rootLogger.addHandler(handler);
-        rootLogger.setLevel(Level.ALL);
-        for (Handler h : rootLogger.getHandlers()) {
-            handler.setFormatter(new SimpleFormatter());
-            h.setLevel(Level.ALL);
-        }
-    }
-
-    private static final Logger logger = Logger.getLogger("");
-
-    @Test
-    public void testIndexHbz() throws Exception {
-        HttpClient httpClient = HttpClient.builder()
-                .build();
-        httpClient.prepareGet()
-                .setVersion("HTTP/1.1")
-                .setURL("http://index.hbz-nrw.de")
-                .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
-                .onResponse(fullHttpResponse -> {
-                    String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
-                    logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
-                })
-                .execute()
-                .get();
-        httpClient.close();
-    }
-
-    @Test
-    public void testIndexHbzHttps() throws Exception {
-        HttpClient httpClient = HttpClient.builder()
-                .build();
-        httpClient.prepareGet()
-                .setVersion("HTTP/1.1")
-                .setURL("https://index.hbz-nrw.de")
-                .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
-                .onResponse(fullHttpResponse -> {
-                    String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
-                    logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
-                })
-                .execute()
-                .get();
-        httpClient.close();
-    }
-
-    @Test
-    public void testIndexHbzWithCompletableFuture() throws Exception {
-        // fetches "test" as content from index.hbz-nrw.de and continues with sending another URL to google.com
-
-        // tricky: google.com does not completely redirect because the first httpResponseStringFunction wins
-        // and generates the desired string result
-
-        HttpClient httpClient = HttpClient.builder()
-                .build();
-
-        final Function httpResponseStringFunction =
-                response -> response.content().toString(StandardCharsets.UTF_8);
-
-        final CompletableFuture completableFuture = httpClient.prepareGet()
-                .setURL("http://index.hbz-nrw.de")
-                .execute(httpResponseStringFunction)
-                .exceptionally(Throwable::getMessage)
-                .thenCompose(content -> httpClient.prepareGet()
-                        .setURL("http://google.com/?query=" + content)
-                        .execute(httpResponseStringFunction));
-
-        String result = completableFuture.join();
-
-        logger.log(Level.INFO, "completablefuture result = " + result);
-
-        httpClient.close();
-    }
-
-    @Test
-    public void testIndexHbzH2() throws Exception {
-        HttpClient httpClient = HttpClient.builder()
-                .build();
-        httpClient.prepareGet()
-                .setVersion("HTTP/2.0")
-                .setURL("https://index.hbz-nrw.de")
-                .setTimeout(5000)
-                .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
-                .onResponse(fullHttpResponse -> {
-                    String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
-                    logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
-                })
-                .execute()
-                .get();
-        httpClient.close();
-    }
-
-    public void testIndexHbzH2C() throws Exception {
-
-        // times out waiting for http2 settings frame
-
-        HttpClient httpClient = HttpClient.builder()
-                .setInstallHttp2Upgrade(true)
-                .build();
-        httpClient.prepareGet()
-                .setVersion("HTTP/2.0")
-                .setURL("http://index.hbz-nrw.de")
-                .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
-                .onResponse(fullHttpResponse -> {
-                    String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
-                    logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
-                })
-                .execute()
-                .get();
-        httpClient.close();
-    }
-
-    @Test
-    public void testIndexHbzConcurrentHttp1() throws Exception {
-
-        HttpClient httpClient = HttpClient.builder()
-                .build();
-
-        HttpRequestBuilder builder1 = httpClient.prepareGet()
-                .setVersion("HTTP/1.1")
-                .setURL("http://index.hbz-nrw.de")
-                .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
-                .onResponse(fullHttpResponse -> {
-                    String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
-                    logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
-                });
-
-        HttpRequestBuilder builder2 = httpClient.prepareGet()
-                .setVersion("HTTP/1.1")
-                .setURL("http://index.hbz-nrw.de")
-                .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
-                .onResponse(fullHttpResponse -> {
-                    String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
-                    logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
-                });
-
-        HttpRequestContext context1 = builder1.execute();
-        HttpRequestContext context2 = builder2.execute();
-        context1.get();
-        context2.get();
-
-        httpClient.close();
-    }
-}
diff --git a/src/test/java/org/xbib/netty/http/client/test/LoggingBase.java b/src/test/java/org/xbib/netty/http/client/test/LoggingBase.java
new file mode 100644
index 0000000..ad57a4e
--- /dev/null
+++ b/src/test/java/org/xbib/netty/http/client/test/LoggingBase.java
@@ -0,0 +1,26 @@
+package org.xbib.netty.http.client.test;
+
+import java.util.logging.ConsoleHandler;
+import java.util.logging.Handler;
+import java.util.logging.Level;
+import java.util.logging.LogManager;
+import java.util.logging.Logger;
+import java.util.logging.SimpleFormatter;
+
+public class LoggingBase {
+
+    static {
+        System.setProperty("java.util.logging.SimpleFormatter.format",
+                "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
+        LogManager.getLogManager().reset();
+        Logger rootLogger = LogManager.getLogManager().getLogger("");
+        Handler handler = new ConsoleHandler();
+        handler.setFormatter(new SimpleFormatter());
+        rootLogger.addHandler(handler);
+        rootLogger.setLevel(Level.INFO);
+        for (Handler h : rootLogger.getHandlers()) {
+            handler.setFormatter(new SimpleFormatter());
+            h.setLevel(Level.ALL);
+        }
+    }
+}
diff --git a/src/test/java/org/xbib/netty/http/client/test/URITest.java b/src/test/java/org/xbib/netty/http/client/test/URITest.java
index 76b8aa7..b68a66b 100644
--- a/src/test/java/org/xbib/netty/http/client/test/URITest.java
+++ b/src/test/java/org/xbib/netty/http/client/test/URITest.java
@@ -1,9 +1,8 @@
 package org.xbib.netty.http.client.test;
 
-import io.netty.handler.codec.http.HttpMethod;
 import org.junit.Test;
-import org.xbib.netty.http.client.HttpClientRequestBuilder;
-import org.xbib.netty.http.client.HttpRequestBuilder;
+import org.xbib.netty.http.client.Request;
+import org.xbib.netty.http.client.RequestBuilder;
 
 import java.net.URI;
 
@@ -24,15 +23,15 @@ public class URITest {
     }
 
     @Test
-    public void testClientRequestURIs() {
-        HttpRequestBuilder httpRequestBuilder = HttpClientRequestBuilder.builder(HttpMethod.GET);
+    public void testRequestURIs() {
+        RequestBuilder httpRequestBuilder = Request.get();
         httpRequestBuilder.setURL("https://localhost").path("/path");
-        assertEquals("/path", httpRequestBuilder.build().uri());
+        assertEquals("/path", httpRequestBuilder.build().relativeUri());
         httpRequestBuilder.path("/foobar");
-        assertEquals("/foobar", httpRequestBuilder.build().uri());
+        assertEquals("/foobar", httpRequestBuilder.build().relativeUri());
         httpRequestBuilder.path("/path1?a=b");
-        assertEquals("/path1?a=b", httpRequestBuilder.build().uri());
+        assertEquals("/path1?a=b", httpRequestBuilder.build().relativeUri());
         httpRequestBuilder.path("/path2?c=d");
-        assertEquals("/path2?c=d", httpRequestBuilder.build().uri());
+        assertEquals("/path2?c=d", httpRequestBuilder.build().relativeUri());
     }
 }
diff --git a/src/test/java/org/xbib/netty/http/client/test/WebtideTest.java b/src/test/java/org/xbib/netty/http/client/test/WebtideTest.java
deleted file mode 100644
index d84ab20..0000000
--- a/src/test/java/org/xbib/netty/http/client/test/WebtideTest.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package org.xbib.netty.http.client.test;
-
-import org.junit.Test;
-import org.xbib.netty.http.client.HttpClient;
-
-import java.nio.charset.StandardCharsets;
-import java.util.logging.ConsoleHandler;
-import java.util.logging.Handler;
-import java.util.logging.Level;
-import java.util.logging.LogManager;
-import java.util.logging.Logger;
-import java.util.logging.SimpleFormatter;
-
-/**
- */
-public class WebtideTest {
-
-    static {
-        System.setProperty("java.util.logging.SimpleFormatter.format",
-                "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
-        LogManager.getLogManager().reset();
-        Logger rootLogger = LogManager.getLogManager().getLogger("");
-        Handler handler = new ConsoleHandler();
-        handler.setFormatter(new SimpleFormatter());
-        rootLogger.addHandler(handler);
-        rootLogger.setLevel(Level.FINE);
-        for (Handler h : rootLogger.getHandlers()) {
-            handler.setFormatter(new SimpleFormatter());
-            h.setLevel(Level.FINE);
-        }
-    }
-
-    private static final Logger logger = Logger.getLogger("");
-
-    @Test
-    public void testWebtide() throws Exception {
-        HttpClient httpClient = HttpClient.builder()
-                .build();
-
-        httpClient.prepareGet()
-                .setVersion("HTTP/2.0")
-                .setURL("https://webtide.com")
-                .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
-                .onResponse(fullHttpResponse -> {
-                    logger.log(Level.INFO, "status = " + fullHttpResponse.status()
-                            + " response headers = " + fullHttpResponse.headers().entries()
-                            );
-                })
-                .onPushReceived((headers, fullHttpResponse) -> {
-                    logger.log(Level.INFO, "received push promise: request headers = " + headers
-                            + " status = " + fullHttpResponse.status()
-                            + " response headers = " + fullHttpResponse.headers().entries()
-                            );
-                })
-                .execute()
-                .get();
-
-        httpClient.close();
-    }
-}
diff --git a/src/test/java/org/xbib/netty/http/client/test/XbibTest.java b/src/test/java/org/xbib/netty/http/client/test/XbibTest.java
index 8c0b263..ba75b2d 100644
--- a/src/test/java/org/xbib/netty/http/client/test/XbibTest.java
+++ b/src/test/java/org/xbib/netty/http/client/test/XbibTest.java
@@ -1,162 +1,131 @@
-/*
- * Copyright 2017 Jörg Prante
- *
- * Jörg Prante licenses this file to you under the Apache License,
- * version 2.0 (the "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at:
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations
- * under the License.
- */
 package org.xbib.netty.http.client.test;
 
 import io.netty.handler.codec.http.FullHttpResponse;
-import org.junit.Ignore;
+import io.netty.handler.proxy.HttpProxyHandler;
 import org.junit.Test;
-import org.xbib.netty.http.client.HttpClient;
+import org.xbib.netty.http.client.Client;
+import org.xbib.netty.http.client.Request;
+import org.xbib.netty.http.client.test.LoggingBase;
 
 import java.net.InetSocketAddress;
 import java.nio.charset.StandardCharsets;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Function;
-import java.util.logging.ConsoleHandler;
-import java.util.logging.Handler;
 import java.util.logging.Level;
-import java.util.logging.LogManager;
 import java.util.logging.Logger;
-import java.util.logging.SimpleFormatter;
 
-/**
- */
-public class XbibTest {
-
-    static {
-        System.setProperty("java.util.logging.SimpleFormatter.format",
-                "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n");
-        LogManager.getLogManager().reset();
-        Logger rootLogger = LogManager.getLogManager().getLogger("");
-        Handler handler = new ConsoleHandler();
-        handler.setFormatter(new SimpleFormatter());
-        rootLogger.addHandler(handler);
-        rootLogger.setLevel(Level.ALL);
-        for (Handler h : rootLogger.getHandlers()) {
-            handler.setFormatter(new SimpleFormatter());
-            h.setLevel(Level.ALL);
-        }
-    }
+public class XbibTest extends LoggingBase {
 
     private static final Logger logger = Logger.getLogger("");
 
     @Test
-    public void testXbibOrgWithDefaults() throws Exception {
-        HttpClient httpClient = HttpClient.builder()
-                .build();
-        httpClient.prepareGet()
-                .setURL("http://xbib.org")
-                .onResponse(fullHttpResponse -> {
-                    String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
-                    logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
-                })
-                .execute()
-                .get();
-        httpClient.close();
+    public void testXbibOrgWithDefaults() {
+        Client client = new Client();
+        try {
+            Request request = Request.get().setURL("http://xbib.org")
+                    .build()
+                    .setResponseListener(fullHttpResponse -> {
+                        String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
+                        logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
+                    });
+            client.execute(request);
+        } finally {
+            client.shutdownGracefully();
+        }
     }
 
     @Test
-    public void testXbibOrgWithCompletableFuture() throws Exception {
-        HttpClient httpClient = HttpClient.builder()
+    public void testXbibOrgWithCompletableFuture() {
+        Client httpClient = Client.builder()
                 .setTcpNodelay(true)
                 .build();
-
-        final Function httpResponseStringFunction =
-                response -> response.content().toString(StandardCharsets.UTF_8);
-
-        final CompletableFuture completableFuture = httpClient.prepareGet()
-                .setURL("http://index.hbz-nrw.de")
-                .execute(httpResponseStringFunction)
-                .exceptionally(Throwable::getMessage)
-                .thenCompose(content -> httpClient.prepareGet()
-                        .setURL("http://google.de/?query=" + content)
-                        .execute(httpResponseStringFunction));
-
-        String result = completableFuture.join();
-
-        logger.log(Level.FINE, "completablefuture result = " + result);
-
-        httpClient.close();
+        try {
+            final Function httpResponseStringFunction =
+                    response -> response.content().toString(StandardCharsets.UTF_8);
+            Request request = Request.get().setURL("http://xbib.org")
+                    .build();
+            final CompletableFuture completableFuture = httpClient.execute(request, httpResponseStringFunction)
+                    .exceptionally(Throwable::getMessage)
+                    .thenCompose(content -> httpClient.execute(Request.post()
+                            .setURL("http://google.de")
+                            .addParam("query", content)
+                            .build(), httpResponseStringFunction));
+            String result = completableFuture.join();
+            logger.info("result = " + result);
+        } finally {
+            httpClient.shutdownGracefully();
+        }
     }
 
     @Test
-    @Ignore
-    public void testXbibOrgWithProxy() throws Exception {
-        HttpClient httpClient = HttpClient.builder()
-                .setHttpProxyHandler(new InetSocketAddress("80.241.223.251", 8080))
+    public void testXbibOrgWithProxy() {
+        Client httpClient = Client.builder()
+                .setHttpProxyHandler(new HttpProxyHandler(new InetSocketAddress("80.241.223.251", 8080)))
                 .setConnectTimeoutMillis(30000)
                 .setReadTimeoutMillis(30000)
                 .build();
-        httpClient.prepareGet()
-                .setURL("http://xbib.org")
-                .onResponse(fullHttpResponse -> {
-                    String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
-                    logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
-                })
-                .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
-                .execute()
-                .get();
-        httpClient.close();
+        try {
+            httpClient.execute(Request.get()
+                    .setURL("http://xbib.org")
+                    .build()
+                    .setResponseListener(fullHttpResponse -> {
+                        String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
+                        logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
+                    })
+                    .setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e)))
+                    .get();
+        } finally {
+            httpClient.shutdownGracefully();
+        }
     }
 
     @Test
-    public void testXbibOrgWithVeryShortReadTimeout() throws Exception {
-        logger.log(Level.FINE, "start");
-        HttpClient httpClient = HttpClient.builder()
+    public void testXbibOrgWithVeryShortReadTimeout() {
+        Client httpClient = Client.builder()
                 .setReadTimeoutMillis(50)
                 .build();
-        httpClient.prepareGet()
-                .setURL("http://xbib.org")
-                .onResponse(fullHttpResponse -> {
-                    String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
-                    logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
-                })
-                .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
-                .execute()
-                .get();
-        httpClient.close();
-        logger.log(Level.FINE, "end");
+        try {
+            httpClient.execute(Request.get()
+                    .setURL("http://xbib.org")
+                    .build()
+                    .setResponseListener(fullHttpResponse -> {
+                        String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
+                        logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
+                    })
+                    .setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e)))
+                    .get();
+        } finally {
+            httpClient.shutdownGracefully();
+        }
     }
 
     @Test
-    public void testXbibTwoSequentialRequests() throws Exception {
-        HttpClient httpClient = HttpClient.builder()
-                .build();
+    public void testXbibTwoSequentialRequests() {
+        Client httpClient = new Client();
+        try {
+            httpClient.execute(Request.get()
+                    .setVersion("HTTP/1.1")
+                    .setURL("http://xbib.org")
+                    .build()
+                    .setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+                    .setResponseListener(fullHttpResponse -> {
+                        String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
+                        logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
+                    }))
+                    .get();
 
-        httpClient.prepareGet()
-                .setVersion("HTTP/1.1")
-                .setURL("http://xbib.org")
-                .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
-                .onResponse(fullHttpResponse -> {
-                    String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
-                    logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
-                })
-                .execute()
-                .get();
-
-        httpClient.prepareGet()
-                .setVersion("HTTP/1.1")
-                .setURL("http://xbib.org")
-                .onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
-                .onResponse(fullHttpResponse -> {
-                    String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
-                    logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
-                })
-                .execute()
-                .get();
-
-        httpClient.close();
+            httpClient.execute(Request.get()
+                    .setVersion("HTTP/1.1")
+                    .setURL("http://xbib.org")
+                    .build()
+                    .setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e))
+                    .setResponseListener(fullHttpResponse -> {
+                        String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
+                        logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
+                    }))
+                    .get();
+        } finally {
+            httpClient.shutdownGracefully();
+        }
     }
 }
diff --git a/src/test/java/org/xbib/netty/http/client/test/rest/RestClientTest.java b/src/test/java/org/xbib/netty/http/client/test/rest/RestClientTest.java
new file mode 100644
index 0000000..e7fcbc2
--- /dev/null
+++ b/src/test/java/org/xbib/netty/http/client/test/rest/RestClientTest.java
@@ -0,0 +1,18 @@
+package org.xbib.netty.http.client.test.rest;
+
+import org.junit.Test;
+import org.xbib.netty.http.client.rest.RestClient;
+
+import java.io.IOException;
+import java.util.logging.Logger;
+
+public class RestClientTest {
+
+    private static final Logger logger = Logger.getLogger(RestClientTest.class.getName());
+
+    @Test
+    public void testSimpleGet() throws IOException {
+        String result = RestClient.get("http://xbib.org").asString();
+        logger.info(result);
+    }
+}
diff --git a/src/test/java/org/xbib/netty/http/client/test/simple/Http2FramesTest.java b/src/test/java/org/xbib/netty/http/client/test/simple/Http2FramesTest.java
new file mode 100644
index 0000000..386e006
--- /dev/null
+++ b/src/test/java/org/xbib/netty/http/client/test/simple/Http2FramesTest.java
@@ -0,0 +1,120 @@
+package org.xbib.netty.http.client.test.simple;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http2.DefaultHttp2Headers;
+import io.netty.handler.codec.http2.Http2ConnectionHandler;
+import io.netty.handler.codec.http2.Http2ConnectionHandlerBuilder;
+import io.netty.handler.codec.http2.Http2Exception;
+import io.netty.handler.codec.http2.Http2FrameAdapter;
+import io.netty.handler.codec.http2.Http2FrameLogger;
+import io.netty.handler.codec.http2.Http2SecurityUtil;
+import io.netty.handler.codec.http2.Http2Settings;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.ssl.ApplicationProtocolConfig;
+import io.netty.handler.ssl.ApplicationProtocolNames;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslHandler;
+import io.netty.handler.ssl.SslProvider;
+import io.netty.handler.ssl.SupportedCipherSuiteFilter;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import java.net.InetSocketAddress;
+import java.util.Arrays;
+import java.util.concurrent.CompletableFuture;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+public class Http2FramesTest {
+
+    private static final Logger logger = Logger.getLogger(Http2FramesTest.class.getName());
+
+    @Test
+    @Ignore
+    public void testHttp2Frames() throws Exception {
+        final InetSocketAddress inetSocketAddress = new InetSocketAddress("webtide.com", 443);
+        CompletableFuture completableFuture = new CompletableFuture<>();
+        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
+        Channel clientChannel = null;
+        try {
+            Bootstrap bootstrap = new Bootstrap()
+                    .group(eventLoopGroup)
+                    .channel(NioSocketChannel.class)
+                    .handler(new ChannelInitializer() {
+                        @Override
+                        protected void initChannel(Channel ch) throws Exception {
+                            SslContext sslContext = SslContextBuilder.forClient()
+                                    .sslProvider(SslProvider.JDK)
+                                    .trustManager(InsecureTrustManagerFactory.INSTANCE)
+                                    .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
+                                    .applicationProtocolConfig(new ApplicationProtocolConfig(
+                                            ApplicationProtocolConfig.Protocol.ALPN,
+                                            ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
+                                            ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
+                                            ApplicationProtocolNames.HTTP_2))
+                                    .build();
+                            SslHandler sslHandler = sslContext.newHandler(ch.alloc());
+                            SSLEngine engine = sslHandler.engine();
+                            String fullQualifiedHostname = inetSocketAddress.getHostName();
+                            SSLParameters params = engine.getSSLParameters();
+                            params.setServerNames(Arrays.asList(new SNIServerName[]{new SNIHostName(fullQualifiedHostname)}));
+                            engine.setSSLParameters(params);
+                            ch.pipeline().addLast(sslHandler);
+                            Http2FrameAdapter frameAdapter = new Http2FrameAdapter() {
+                                @Override
+                                public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) {
+                                    logger.log(Level.FINE, "settings received, now writing request");
+                                    Http2ConnectionHandler handler = ctx.pipeline().get(Http2ConnectionHandler.class);
+                                    handler.encoder().writeHeaders(ctx, 3,
+                                            new DefaultHttp2Headers().method(HttpMethod.GET.asciiName())
+                                                    .path("/")
+                                                    .scheme("https")
+                                                    .authority(inetSocketAddress.getHostName()),
+                                            0, true, ctx.newPromise());
+                                    ctx.channel().flush();
+                                }
+
+                                @Override
+                                public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
+                                                      boolean endOfStream) throws Http2Exception {
+                                    int i = super.onDataRead(ctx, streamId, data, padding, endOfStream);
+                                    if (endOfStream) {
+                                        completableFuture.complete(true);
+                                    }
+                                    return i;
+                                }
+                            };
+                            Http2ConnectionHandlerBuilder builder = new Http2ConnectionHandlerBuilder()
+                                    .server(false)
+                                    .frameListener(frameAdapter)
+                                    .frameLogger(new Http2FrameLogger(LogLevel.INFO, "client"));
+                            ch.pipeline().addLast(builder.build());
+                        }
+                    });
+            logger.log(Level.INFO, () -> "connecting");
+            clientChannel = bootstrap.connect(inetSocketAddress).sync().channel();
+            logger.log(Level.INFO, () -> "waiting for end of stream");
+            completableFuture.get();
+            logger.log(Level.INFO, () -> "done");
+        } finally {
+            if (clientChannel != null) {
+                clientChannel.close();
+            }
+            eventLoopGroup.shutdownGracefully();
+        }
+    }
+}
diff --git a/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp1Test.java b/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp1Test.java
new file mode 100644
index 0000000..5065847
--- /dev/null
+++ b/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp1Test.java
@@ -0,0 +1,324 @@
+package org.xbib.netty.http.client.test.simple;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.buffer.ByteBuf;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.ChannelPromise;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.http.DefaultFullHttpRequest;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpClientCodec;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpObjectAggregator;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http2.Http2Settings;
+import io.netty.handler.codec.http2.HttpConversionUtil;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.logging.LoggingHandler;
+import io.netty.util.AttributeKey;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ *
+ */
+public class SimpleHttp1Test {
+
+    private static final Logger logger = Logger.getLogger(SimpleHttp1Test.class.getName());
+
+    @Test
+    public void testHttp1() throws Exception {
+        Client client = new Client();
+        try {
+            HttpTransport transport = client.newTransport("fl.hbz-nrw.de", 80);
+            transport.onResponse(string -> logger.log(Level.INFO, "got messsage: " + string));
+            transport.connect();
+            transport.awaitSettings();
+            sendRequest(transport);
+            transport.awaitResponses();
+            transport.close();
+        } finally {
+            client.shutdown();
+        }
+    }
+
+    private void sendRequest(HttpTransport transport) {
+        Channel channel = transport.channel();
+        if (channel == null) {
+            return;
+        }
+        Integer streamId = transport.nextStream();
+        String host = transport.inetSocketAddress().getHostString();
+        int port = transport.inetSocketAddress().getPort();
+        String uri = "https://" + host + ":" + port;
+        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri);
+        request.headers().add(HttpHeaderNames.HOST, host + ":" + port);
+        request.headers().add(HttpHeaderNames.USER_AGENT, "Java");
+        request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
+        request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE);
+        if (streamId != null) {
+            request.headers().add(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), Integer.toString(streamId));
+        }
+        logger.log(Level.INFO, () -> "writing request = " + request);
+        channel.writeAndFlush(request);
+    }
+
+    private AttributeKey TRANSPORT_ATTRIBUTE_KEY = AttributeKey.valueOf("transport");
+
+    interface ResponseWriter {
+        void write(String string);
+    }
+
+    class Client {
+        private final EventLoopGroup eventLoopGroup;
+
+        private final Bootstrap bootstrap;
+
+        private final HttpResponseHandler httpResponseHandler;
+
+        private final Initializer initializer;
+
+        private final List transports;
+
+        Client() {
+            eventLoopGroup = new NioEventLoopGroup();
+            httpResponseHandler = new HttpResponseHandler();
+            initializer = new Initializer(httpResponseHandler);
+            bootstrap = new Bootstrap()
+                    .group(eventLoopGroup)
+                    .channel(NioSocketChannel.class)
+                    .handler(initializer);
+            transports = new ArrayList<>();
+        }
+
+        Bootstrap bootstrap() {
+            return bootstrap;
+        }
+
+        Initializer initializer() {
+            return initializer;
+        }
+
+        HttpResponseHandler responseHandler() {
+            return httpResponseHandler;
+        }
+
+        void shutdown() {
+            eventLoopGroup.shutdownGracefully();
+        }
+
+        HttpTransport newTransport(String host, int port) {
+            HttpTransport transport = new HttpTransport(this, new InetSocketAddress(host, port));
+            transports.add(transport);
+            return transport;
+        }
+
+        List transports() {
+            return transports;
+        }
+
+        void close(HttpTransport transport) {
+            transports.remove(transport);
+        }
+
+        void close() {
+            for (HttpTransport transport : transports) {
+                transport.close();
+            }
+        }
+    }
+
+    class HttpTransport {
+
+        private final Client client;
+
+        private final InetSocketAddress inetSocketAddress;
+
+        private Channel channel;
+
+        private CompletableFuture promise;
+
+        private ResponseWriter responseWriter;
+
+        HttpTransport(Client client, InetSocketAddress inetSocketAddress ) {
+            this.client = client;
+            this.inetSocketAddress = inetSocketAddress;
+        }
+
+        Client client() {
+            return client;
+        }
+
+        InetSocketAddress inetSocketAddress() {
+            return inetSocketAddress;
+        }
+
+        void connect() throws InterruptedException {
+            channel = client.bootstrap().connect(inetSocketAddress).sync().await().channel();
+            channel.attr(TRANSPORT_ATTRIBUTE_KEY).set(this);
+        }
+
+        Channel channel() {
+            return channel;
+        }
+
+        Integer nextStream() {
+            promise = new CompletableFuture<>();
+            return null;
+        }
+
+        void onResponse(ResponseWriter responseWriter) {
+            this.responseWriter = responseWriter;
+        }
+
+        void settingsReceived(Channel channel, Http2Settings http2Settings) {
+        }
+
+        void awaitSettings() {
+        }
+
+        void responseReceived(Integer streamId, String message) {
+            if (promise == null) {
+                logger.log(Level.WARNING, "message received for unknown stream id " + streamId);
+            } else {
+                if (responseWriter != null) {
+                    responseWriter.write(message);
+                }
+            }
+        }
+        void awaitResponse(Integer streamId) {
+            if (promise != null) {
+                try {
+                    logger.log(Level.INFO, "waiting for response");
+                    promise.get(5, TimeUnit.SECONDS);
+                    logger.log(Level.INFO, "response received");
+                } catch (InterruptedException | ExecutionException | TimeoutException e) {
+                    logger.log(Level.WARNING, e.getMessage(), e);
+                }
+            }
+        }
+
+        void awaitResponses() {
+            awaitResponse(null);
+        }
+
+        void complete() {
+            if (promise != null) {
+                promise.complete(true);
+            }
+        }
+
+        void fail(Throwable throwable) {
+            if (promise != null) {
+                promise.completeExceptionally(throwable);
+            }
+        }
+
+        void close() {
+            if (channel != null) {
+                channel.close();
+            }
+            client.close(this);
+        }
+    }
+
+    class Initializer extends ChannelInitializer {
+
+        private HttpResponseHandler httpResponseHandler;
+
+        Initializer(HttpResponseHandler httpResponseHandler) {
+            this.httpResponseHandler = httpResponseHandler;
+        }
+
+        @Override
+        protected void initChannel(SocketChannel ch) {
+            ch.pipeline().addLast(new TrafficLoggingHandler());
+            ch.pipeline().addLast(new HttpClientCodec());
+            ch.pipeline().addLast(new HttpObjectAggregator(1048576));
+            ch.pipeline().addLast(httpResponseHandler);
+        }
+    }
+
+    class HttpResponseHandler extends SimpleChannelInboundHandler {
+
+        @Override
+        protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) {
+            HttpTransport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get();
+            if (msg.content().isReadable()) {
+                transport.responseReceived(null, msg.content().toString(StandardCharsets.UTF_8));
+            }
+        }
+
+        @Override
+        public void channelReadComplete(ChannelHandlerContext ctx)  {
+            HttpTransport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get();
+            transport.complete();
+        }
+
+        @Override
+        public void channelInactive(ChannelHandlerContext ctx)  {
+            ctx.fireChannelInactive();
+            HttpTransport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get();
+            transport.fail(new IOException("channel closed"));
+        }
+
+        @Override
+        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+            logger.log(Level.SEVERE, cause.getMessage(), cause);
+            HttpTransport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get();
+            transport.fail(cause);
+            ctx.channel().close();
+        }
+    }
+
+    class TrafficLoggingHandler extends LoggingHandler {
+
+        TrafficLoggingHandler() {
+            super("client", LogLevel.INFO);
+        }
+
+        @Override
+        public void channelRegistered(ChannelHandlerContext ctx) {
+            ctx.fireChannelRegistered();
+        }
+
+        @Override
+        public void channelUnregistered(ChannelHandlerContext ctx) {
+            ctx.fireChannelUnregistered();
+        }
+
+        @Override
+        public void flush(ChannelHandlerContext ctx) {
+            ctx.flush();
+        }
+
+        @Override
+        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
+            if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) {
+                ctx.write(msg, promise);
+            } else {
+                super.write(ctx, msg, promise);
+            }
+        }
+    }
+}
diff --git a/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp2Test.java b/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp2Test.java
new file mode 100644
index 0000000..adac51c
--- /dev/null
+++ b/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp2Test.java
@@ -0,0 +1,389 @@
+package org.xbib.netty.http.client.test.simple;
+
+import io.netty.bootstrap.Bootstrap;
+import io.netty.channel.Channel;
+import io.netty.channel.ChannelHandlerContext;
+import io.netty.channel.ChannelInitializer;
+import io.netty.channel.EventLoopGroup;
+import io.netty.channel.SimpleChannelInboundHandler;
+import io.netty.channel.nio.NioEventLoopGroup;
+import io.netty.channel.socket.SocketChannel;
+import io.netty.channel.socket.nio.NioSocketChannel;
+import io.netty.handler.codec.http.DefaultFullHttpRequest;
+import io.netty.handler.codec.http.FullHttpRequest;
+import io.netty.handler.codec.http.FullHttpResponse;
+import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaderValues;
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpVersion;
+import io.netty.handler.codec.http2.DefaultHttp2Connection;
+import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener;
+import io.netty.handler.codec.http2.Http2ConnectionHandler;
+import io.netty.handler.codec.http2.Http2FrameLogger;
+import io.netty.handler.codec.http2.Http2SecurityUtil;
+import io.netty.handler.codec.http2.Http2Settings;
+import io.netty.handler.codec.http2.HttpConversionUtil;
+import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
+import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
+import io.netty.handler.logging.LogLevel;
+import io.netty.handler.ssl.ApplicationProtocolConfig;
+import io.netty.handler.ssl.ApplicationProtocolNames;
+import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslProvider;
+import io.netty.handler.ssl.SupportedCipherSuiteFilter;
+import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
+import io.netty.util.AttributeKey;
+import org.junit.Test;
+
+import javax.net.ssl.SSLException;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ */
+public class SimpleHttp2Test {
+
+    private static final Logger logger = Logger.getLogger(SimpleHttp2Test.class.getName());
+
+    @Test
+    public void testHttp2WithUpgrade() throws Exception {
+        Client client = new Client();
+        try {
+            Http2Transport transport = client.newTransport("webtide.com", 443);
+            transport.onResponse(string -> logger.log(Level.INFO, "got messsage: " + string));
+            transport.connect();
+            transport.awaitSettings();
+            sendRequest(transport);
+            transport.awaitResponses();
+            transport.close();
+        } finally {
+            client.shutdown();
+        }
+    }
+
+    private void sendRequest(Http2Transport transport) {
+        Channel channel = transport.channel();
+        if (channel == null) {
+            return;
+        }
+        Integer streamId = transport.nextStream();
+        String host = transport.inetSocketAddress().getHostString();
+        int port = transport.inetSocketAddress().getPort();
+        String uri = "https://" + host + ":" + port;
+        FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri);
+        request.headers().add(HttpHeaderNames.HOST, host + ":" + port);
+        request.headers().add(HttpHeaderNames.USER_AGENT, "Java");
+        request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
+        request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE);
+        if (streamId != null) {
+            request.headers().add(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), Integer.toString(streamId));
+        }
+        logger.log(Level.INFO, () -> "writing request = " + request);
+        channel.writeAndFlush(request);
+    }
+
+    private AttributeKey TRANSPORT_ATTRIBUTE_KEY = AttributeKey.valueOf("transport");
+
+    interface ResponseWriter {
+        void write(String string);
+    }
+
+    class Client {
+        private final EventLoopGroup eventLoopGroup;
+
+        private final Bootstrap bootstrap;
+
+        private final Http2SettingsHandler http2SettingsHandler;
+
+        private final Http2ResponseHandler http2ResponseHandler;
+
+        private final Initializer initializer;
+
+        private final List transports;
+
+        Client() {
+            eventLoopGroup = new NioEventLoopGroup();
+            http2SettingsHandler = new Http2SettingsHandler();
+            http2ResponseHandler = new Http2ResponseHandler();
+            initializer = new Initializer(http2SettingsHandler, http2ResponseHandler);
+            bootstrap = new Bootstrap()
+                    .group(eventLoopGroup)
+                    .channel(NioSocketChannel.class)
+                    .handler(initializer);
+            transports = new ArrayList<>();
+        }
+
+        Bootstrap bootstrap() {
+            return bootstrap;
+        }
+
+        void shutdown() {
+            eventLoopGroup.shutdownGracefully();
+        }
+
+        Http2Transport newTransport(String host, int port) {
+            Http2Transport transport = new Http2Transport(this, new InetSocketAddress(host, port));
+            transports.add(transport);
+            return transport;
+        }
+
+        List transports() {
+            return transports;
+        }
+
+        void close(Http2Transport transport) {
+            transports.remove(transport);
+        }
+
+        void close() {
+            for (Http2Transport transport : transports) {
+                transport.close();
+            }
+        }
+    }
+
+    class Http2Transport {
+
+        private final Client client;
+
+        private final InetSocketAddress inetSocketAddress;
+
+        private Channel channel;
+
+        CompletableFuture settingsPromise;
+
+        private SortedMap> streamidPromiseMap;
+
+        private AtomicInteger streamIdCounter;
+
+        private ResponseWriter responseWriter;
+
+        Http2Transport(Client client, InetSocketAddress inetSocketAddress) {
+            this.client = client;
+            this.inetSocketAddress = inetSocketAddress;
+            streamidPromiseMap = new TreeMap<>();
+            streamIdCounter = new AtomicInteger(3);
+        }
+
+        Client client() {
+            return client;
+        }
+
+        InetSocketAddress inetSocketAddress() {
+            return inetSocketAddress;
+        }
+
+        void connect() throws InterruptedException {
+            channel = client.bootstrap().connect(inetSocketAddress).sync().await().channel();
+            channel.attr(TRANSPORT_ATTRIBUTE_KEY).set(this);
+            settingsPromise = new CompletableFuture<>();
+        }
+
+        Channel channel() {
+            return channel;
+        }
+
+        Integer nextStream() {
+            Integer streamId = streamIdCounter.getAndAdd(2);
+            streamidPromiseMap.put(streamId, new CompletableFuture<>());
+            return streamId;
+        }
+
+        void onResponse(ResponseWriter responseWriter) {
+            this.responseWriter = responseWriter;
+        }
+
+        void settingsReceived(Channel channel, Http2Settings http2Settings) {
+            if (settingsPromise != null) {
+                settingsPromise.complete(true);
+            } else {
+                logger.log(Level.WARNING, "settings received but no promise present");
+            }
+        }
+
+        void awaitSettings() {
+            if (settingsPromise != null) {
+                try {
+                    logger.log(Level.INFO, "waiting for settings");
+                    settingsPromise.get(5, TimeUnit.SECONDS);
+                    logger.log(Level.INFO, "settings received");
+                } catch (InterruptedException | ExecutionException | TimeoutException e) {
+                    settingsPromise.completeExceptionally(e);
+                }
+            } else {
+                logger.log(Level.WARNING, "waiting for settings but no promise present");
+            }
+        }
+
+        void responseReceived(Integer streamId, String message) {
+            if (streamId == null) {
+                logger.log(Level.WARNING, "unexpected message received: " + message);
+                return;
+            }
+            CompletableFuture promise = streamidPromiseMap.get(streamId);
+            if (promise == null) {
+                logger.log(Level.WARNING, "message received for unknown stream id " + streamId);
+            } else {
+                if (responseWriter != null) {
+                    responseWriter.write(message);
+                }
+                promise.complete(true);
+            }
+        }
+
+        void awaitResponse(Integer streamId) {
+            if (streamId == null) {
+                return;
+            }
+            CompletableFuture promise = streamidPromiseMap.get(streamId);
+            if (promise != null) {
+                try {
+                    logger.log(Level.INFO, "waiting for response for stream id=" + streamId);
+                    promise.get(5, TimeUnit.SECONDS);
+                    logger.log(Level.INFO, "response for stream id=" + streamId + " received");
+                } catch (InterruptedException | ExecutionException | TimeoutException e) {
+                    logger.log(Level.WARNING, "streamId=" + streamId + " " + e.getMessage(), e);
+                } finally {
+                    streamidPromiseMap.remove(streamId);
+                }
+            }
+        }
+
+        void awaitResponses() {
+            logger.log(Level.INFO, "waiting for all stream ids " + streamidPromiseMap.keySet());
+            for (int streamId : streamidPromiseMap.keySet()) {
+                awaitResponse(streamId);
+            }
+        }
+
+        void complete() {
+            for (CompletableFuture promise : streamidPromiseMap.values()) {
+                promise.complete(true);
+            }
+        }
+
+        void fail(Throwable throwable) {
+            for (CompletableFuture promise : streamidPromiseMap.values()) {
+                promise.completeExceptionally(throwable);
+            }
+        }
+
+        void close() {
+            if (channel != null) {
+                channel.close();
+            }
+            client.close(this);
+        }
+    }
+
+    class Initializer extends ChannelInitializer {
+
+        private Http2SettingsHandler http2SettingsHandler;
+
+        private Http2ResponseHandler http2ResponseHandler;
+
+        Initializer(Http2SettingsHandler http2SettingsHandler, Http2ResponseHandler http2ResponseHandler) {
+            this.http2SettingsHandler = http2SettingsHandler;
+            this.http2ResponseHandler = http2ResponseHandler;
+        }
+
+        @Override
+        protected void initChannel(SocketChannel ch) {
+            DefaultHttp2Connection http2Connection = new DefaultHttp2Connection(false);
+            Http2FrameLogger frameLogger = new Http2FrameLogger(LogLevel.INFO, "client");
+            Http2ConnectionHandler http2ConnectionHandler = new HttpToHttp2ConnectionHandlerBuilder()
+                    .connection(http2Connection)
+                    .frameLogger(frameLogger)
+                    .frameListener(new DelegatingDecompressorFrameListener(http2Connection,
+                            new InboundHttp2ToHttpAdapterBuilder(http2Connection)
+                                    .maxContentLength(10 * 1024 * 1024)
+                                    .propagateSettings(true)
+                                    .build()))
+                    .build();
+
+            try {
+                SslContext sslContext = SslContextBuilder.forClient()
+                        .sslProvider(SslProvider.JDK)
+                        .trustManager(InsecureTrustManagerFactory.INSTANCE)
+                        .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE)
+                        .applicationProtocolConfig(new ApplicationProtocolConfig(
+                                ApplicationProtocolConfig.Protocol.ALPN,
+                                ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
+                                ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
+                                ApplicationProtocolNames.HTTP_2))
+                        .build();
+                ch.pipeline().addLast(sslContext.newHandler(ch.alloc()));
+                ApplicationProtocolNegotiationHandler negotiationHandler = new ApplicationProtocolNegotiationHandler("") {
+                    @Override
+                    protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
+                        if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
+                            ctx.pipeline().addLast(http2ConnectionHandler, http2SettingsHandler, http2ResponseHandler);
+                            return;
+                        }
+                        ctx.close();
+                        throw new IllegalStateException("unknown protocol: " + protocol);
+                    }
+                };
+                ch.pipeline().addLast(negotiationHandler);
+            } catch (SSLException e) {
+                logger.log(Level.SEVERE, e.getMessage(), e);
+            }
+        }
+    }
+
+    class Http2SettingsHandler extends SimpleChannelInboundHandler {
+
+        @Override
+        protected void channelRead0(ChannelHandlerContext ctx, Http2Settings http2Settings) {
+            Http2Transport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get();
+            transport.settingsReceived(ctx.channel(), http2Settings);
+            ctx.pipeline().remove(this);
+        }
+    }
+
+    class Http2ResponseHandler extends SimpleChannelInboundHandler {
+
+        @Override
+        protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) {
+            Http2Transport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get();
+            Integer streamId = msg.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
+            if (msg.content().isReadable()) {
+                transport.responseReceived(streamId, msg.content().toString(StandardCharsets.UTF_8));
+            }
+        }
+
+        @Override
+        public void channelReadComplete(ChannelHandlerContext ctx)  {
+            // do nothing
+        }
+
+        @Override
+        public void channelInactive(ChannelHandlerContext ctx)  {
+            ctx.fireChannelInactive();
+            Http2Transport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get();
+            transport.fail(new IOException("channel closed"));
+        }
+
+        @Override
+        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
+            logger.log(Level.SEVERE, cause.getMessage(), cause);
+            Http2Transport transport = ctx.channel().attr(TRANSPORT_ATTRIBUTE_KEY).get();
+            transport.fail(cause);
+            ctx.channel().close();
+        }
+    }
+}