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 0e96606..8252d7e 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 4dee9e0..454cfef 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue May 02 21:00:09 CEST 2017 +#Sun Feb 25 12:39:15 CET 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-rc-1-all.zip diff --git a/gradlew b/gradlew index 4453cce..cccdd3d 100755 --- a/gradlew +++ b/gradlew @@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS="" # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" -warn ( ) { +warn () { echo "$*" } -die ( ) { +die () { echo echo "$*" echo @@ -155,7 +155,7 @@ if $cygwin ; then fi # Escape application args -save ( ) { +save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } diff --git a/src/docs/asciidoclet/overview.adoc b/src/docs/asciidoclet/overview.adoc index 0cbb854..2bf05fc 100644 --- a/src/docs/asciidoclet/overview.adoc +++ b/src/docs/asciidoclet/overview.adoc @@ -1,4 +1,4 @@ = Netty HTTP client Jörg Prante -Version 4.1.9.0 +Version 4.1.22.0 diff --git a/src/main/java/org/xbib/netty/http/client/Client.java b/src/main/java/org/xbib/netty/http/client/Client.java new file mode 100644 index 0000000..593b289 --- /dev/null +++ b/src/main/java/org/xbib/netty/http/client/Client.java @@ -0,0 +1,218 @@ +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.Channel; +import io.netty.channel.ChannelInitializer; +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.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.ssl.OpenSsl; +import org.xbib.netty.http.client.handler.http1.HttpChannelInitializer; +import org.xbib.netty.http.client.handler.http1.HttpResponseHandler; +import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer; +import org.xbib.netty.http.client.handler.http2.Http2ResponseHandler; +import org.xbib.netty.http.client.handler.http2.Http2SettingsHandler; +import org.xbib.netty.http.client.transport.Http2Transport; +import org.xbib.netty.http.client.transport.HttpTransport; +import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.client.util.NetworkUtils; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ThreadFactory; +import java.util.function.Function; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class Client { + + private static final Logger logger = Logger.getLogger(Client.class.getName()); + + private static final ThreadFactory httpClientThreadFactory = new HttpClientThreadFactory(); + + static { + NetworkUtils.extendSystemProperties(); + } + + private final ClientConfig clientConfig; + + private final ByteBufAllocator byteBufAllocator; + + private final EventLoopGroup eventLoopGroup; + + private final Class 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();
+        }
+    }
+}