large refactoring, new transport layer, update to Netty 4.1.22

This commit is contained in:
Jörg Prante 2018-02-28 12:23:52 +01:00
parent 2572b6cb7f
commit c43c3b9f67
78 changed files with 3921 additions and 5060 deletions

1
.gitignore vendored
View file

@ -9,4 +9,5 @@
/.project
/.gradle
/build
/out
*~

View file

@ -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 {
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'

View file

@ -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

View file

@ -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/"

Binary file not shown.

View file

@ -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

View file

@ -1,4 +1,4 @@
= Netty HTTP client
Jörg Prante
Version 4.1.9.0
Version 4.1.22.0

View file

@ -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<? extends SocketChannel> socketChannelClass;
private final Bootstrap bootstrap;
private final HttpResponseHandler httpResponseHandler;
private final Http2SettingsHandler http2SettingsHandler;
private final Http2ResponseHandler http2ResponseHandler;
private final List<Transport> 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<? extends SocketChannel> 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<SocketChannel> 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 <T> CompletableFuture<T> execute(Request request,
Function<FullHttpResponse, T> 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;
}
}
}

View file

@ -0,0 +1,8 @@
package org.xbib.netty.http.client;
/**
* Client authentication modes, useful for SSL channels.
*/
public enum ClientAuthMode {
NONE, WANT, NEED
}

View file

@ -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<? extends SocketChannel> 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<SocketChannel> 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<String> 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);
}
}

View file

@ -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<String> 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<String> 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<String> ciphers) {
this.ciphers = ciphers;
return this;
}
public Iterable<String> 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;
}
}

View file

@ -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();
}
}

View file

@ -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<Cookie> 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<Channel> futureChannel = pool.acquire();
futureChannel.addListener((FutureListener<Channel>) 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<Integer, Map.Entry<ChannelFuture, ChannelPromise>> 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<String, String> 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();
}
}

View file

@ -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<? extends SocketChannel> 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<String> 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<SocketChannel> 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<String> 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);
}
}

View file

@ -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<String> 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<String> 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<String> 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;
}
}

View file

@ -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<ChannelPool> CHANNEL_POOL_ATTRIBUTE_KEY =
AttributeKey.valueOf("httpClientChannelPool");
AttributeKey<HttpRequestContext> REQUEST_CONTEXT_ATTRIBUTE_KEY =
AttributeKey.valueOf("httpClientRequestContext");
AttributeKey<HttpResponseListener> RESPONSE_LISTENER_ATTRIBUTE_KEY =
AttributeKey.valueOf("httpClientResponseListener");
AttributeKey<HttpHeadersListener> HEADER_LISTENER_ATTRIBUTE_KEY =
AttributeKey.valueOf("httpHeaderListener");
AttributeKey<CookieListener> COOKIE_LISTENER_ATTRIBUTE_KEY =
AttributeKey.valueOf("cookieListener");
AttributeKey<HttpPushListener> PUSH_LISTENER_ATTRIBUTE_KEY =
AttributeKey.valueOf("pushListener");
AttributeKey<ExceptionListener> 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<String> 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;
}

View file

@ -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<String> removeHeaders;
private final Set<Cookie> 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<String> 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<String> 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<String, List<String>> 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 <T> CompletableFuture<T> execute(Function<FullHttpResponse, T> supplier) {
final CompletableFuture<T> 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);
}
}

View file

@ -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);
<T> CompletableFuture<T> execute(Function<FullHttpResponse, T> supplier);
}

View file

@ -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<String> 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<Integer, Map.Entry<ChannelFuture, ChannelPromise>> promiseMap;
private final Map<Integer, Map.Entry<Http2Headers, ChannelPromise>> pushMap;
private ChannelPromise settingsPromise;
private Collection<Cookie> cookies;
private Map<Integer, FullHttpResponse> httpResponses;
private Long stopTime;
HttpRequestContext(URI uri, HttpRequest httpRequest,
HttpRequestFuture<String> 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<Integer, Map.Entry<ChannelFuture, ChannelPromise>> 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<Integer, Map.Entry<Http2Headers, ChannelPromise>> 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<Cookie> getCookies() {
return cookies;
}
public List<Cookie> 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<Integer, FullHttpResponse> getHttpResponses() {
return httpResponses;
}
}

View file

@ -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<String> DEFAULT_FUTURE = new HttpRequestFuture<>();
}

View file

@ -1,20 +0,0 @@
package org.xbib.netty.http.client;
import org.xbib.netty.http.client.util.AbstractFuture;
/**
* A HTTP request future.
*
* @param <V> the response type parameter.
*/
public class HttpRequestFuture<V> extends AbstractFuture<V> {
public void success(V v) {
set(v);
}
public void fail(Exception e) {
setException(e);
}
}

View file

@ -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<Cookie> 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<Cookie> 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<Cookie> 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);
}
}

View file

@ -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<String> removeHeaders;
private final Collection<Cookie> 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<String, List<String>> 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);
}
}

View file

@ -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");
}

View file

@ -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.
* <p>
* 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.
* <ul>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
* </ul>
* <p>
* 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.
* <p>
* 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.
* <ul>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
* </ul>
* <p>
* 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.
* <p>
* Promised requests MUST be authoritative, cacheable, and safe.
* See <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-17#section-8.2">[RFC http2], Section 8.2</a>.
* <p>
* 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.
* <ul>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
* </ul>
* <p>
* 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.
* <p>
* If a {@link RuntimeException} is thrown it will be logged and <strong>not propagated</strong>.
* 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
* <ul>
* <li>{@code true} to validate HTTP headers in the http-codec</li>
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
* </ul>
* @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;
}
}

View file

@ -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);
}
}

View file

@ -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<FullHttpResponse> {
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<Http2Headers, ChannelPromise> 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<ChannelFuture, ChannelPromise> 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());
}
}

View file

@ -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<SocketChannel> {
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();
}
}

View file

@ -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());
}
}

View file

@ -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();
}

View file

@ -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());
}
}

View file

@ -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;
}

View file

@ -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<SocketChannel> {
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);
}
}

View file

@ -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<FullHttpResponse> {
@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();
}
}

View file

@ -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<SocketChannel> {
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);
}
}
}

View file

@ -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<FullHttpResponse> {
@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();
}
}

View file

@ -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<Http2Settings> {
@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);
}
}

View file

@ -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;
}
}

View file

@ -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<InetAddressKey, FixedChannelPool> {
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;
}
}

View file

@ -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;
}
}

View file

@ -1,4 +0,0 @@
/**
* Internal classes for Netty HTTP client.
*/
package org.xbib.netty.http.client.internal;

View file

@ -2,8 +2,6 @@ package org.xbib.netty.http.client.listener;
import io.netty.handler.codec.http.cookie.Cookie;
/**
*/
@FunctionalInterface
public interface CookieListener {

View file

@ -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);
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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 {

View file

@ -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;
}
}

View file

@ -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<Integer, Request> requests;
protected HttpResponseListener responseListener;
protected ExceptionListener exceptionListener;
protected HttpHeadersListener httpHeadersListener;
protected CookieListener cookieListener;
protected HttpPushListener pushListener;
private Map<Cookie, Boolean> 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<Cookie> 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 <T> supplier result
* @return completable future
*/
@Override
public <T> CompletableFuture<T> execute(Request request,
Function<FullHttpResponse, T> supplier) {
final CompletableFuture<T> 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<Cookie, Boolean> cookieBox) {
this.cookieBox = cookieBox;
}
public Map<Cookie, Boolean> getCookieBox() {
return cookieBox;
}
public void addCookie(Cookie cookie) {
if (cookieBox == null) {
this.cookieBox = Collections.synchronizedMap(new LRUCache<Cookie, Boolean>(32));
}
cookieBox.put(cookie, true);
}
private List<Cookie> matchCookiesFromBox(Request request) {
return cookieBox == null ? Collections.emptyList() : cookieBox.keySet().stream().filter(cookie ->
matchCookie(request.base(), cookie)
).collect(Collectors.toList());
}
private List<Cookie> 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<K, V> extends LinkedHashMap<K, V> {
private final int cacheSize;
LRUCache(int cacheSize) {
super(16, 0.75f, true);
this.cacheSize = cacheSize;
}
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() >= cacheSize;
}
}
}

View file

@ -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<Boolean> settingsPromise;
private final AtomicInteger streamIdCounter;
private SortedMap<Integer, CompletableFuture<Boolean>> 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<Boolean> 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<Boolean> 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<Boolean> promise : streamidPromiseMap.values()) {
promise.complete(true);
}
}
@Override
public void fail(Throwable throwable) {
if (exceptionListener != null) {
exceptionListener.onException(throwable);
}
for (CompletableFuture<Boolean> promise : streamidPromiseMap.values()) {
promise.completeExceptionally(throwable);
}
}
}

View file

@ -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<Integer, CompletableFuture<Boolean>> 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<Boolean> 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<Boolean> 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<Boolean> promise : sequentialPromiseMap.values()) {
promise.complete(true);
}
}
@Override
public void fail(Throwable throwable) {
if (exceptionListener != null) {
exceptionListener.onException(throwable);
}
for (CompletableFuture<Boolean> promise : sequentialPromiseMap.values()) {
promise.completeExceptionally(throwable);
}
}
}

View file

@ -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> TRANSPORT_ATTRIBUTE_KEY = AttributeKey.valueOf("transport");
HttpAddress httpAddress();
void connect() throws InterruptedException;
Transport execute(Request request);
<T> CompletableFuture<T> execute(Request request, Function<FullHttpResponse, T> 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<Cookie, Boolean> cookieBox);
Map<Cookie, Boolean> 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();
}

View file

@ -0,0 +1,4 @@
/**
* Classes for transports in the Netty client.
*/
package org.xbib.netty.http.client.transport;

View file

@ -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;
/**
* <p>
* 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.
* </p>
*
* <p>
* 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.
* </p>
*
* <p>
* 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.
* </p>
*
* <p>This class is taken from the Google Guava project.</p>
*
* @param <V> the future value parameter type
*/
public abstract class AbstractFuture<V> implements Future<V> {
/**
* Synchronization control.
*/
private final Sync<V> 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() {
}
/**
* <p>
* 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}.
* </p>
* <p>
* 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.
* </p>
* <p>
* We don't use the integer argument passed between acquire methods so we
* pass around a -1 everywhere.
* </p>
*/
static final class Sync<V> 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;
}
}
}

View file

@ -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
}

View file

@ -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();
}
}

View file

@ -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 <E> the element type
*/
public final class LimitedHashSet<E> extends LinkedHashSet<E> {
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<? extends E> elements) {
boolean b = false;
for (E element : elements) {
if (max < size()) {
throw new IllegalStateException("limit exceeded");
}
b = b || super.add(element);
}
return b;
}
}

View file

@ -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;
/**

View file

@ -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;
/**

View file

@ -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;

View file

@ -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 -> {
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);
})
.onPushReceived((requestHeaders, fullHttpResponse) -> {
.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
);
})
.execute()
.get();
httpClient.close();
});
client.execute(request).get();
} finally {
client.shutdownGracefully();
}
}
}

View file

@ -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());
}
}

View file

@ -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<FullHttpResponse, String> 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<String> 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();
}
}
}

View file

@ -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")
Request request = Request.put().setURL("http://localhost:9200/test/test/1")
.json("{\"text\":\"Hello World\"}")
.onResponse(fullHttpResponse -> {
.build()
.setResponseListener(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");
.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 -> {
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);
})
.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");
.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<HttpRequestBuilder> queries = new ArrayList<>();
for (int i = 0; i < max; i++) {
queries.add(createQuery(httpClient));
}
List<HttpRequestContext> contexts = new ArrayList<>();
for (int i = 0; i < max; i++) {
contexts.add(queries.get(i).execute());
}
List<HttpRequestContext> responses = new ArrayList<>();
for (int i = 0; i < max; i++) {
public void testElasticsearchConcurrent() {
Client client = Client.builder().setReadTimeoutMillis(20000).build();
int max = 1000;
try {
responses.add(contexts.get(i).get());
} catch (Exception exception) {
assertTrue(exception.getCause() instanceof ConnectException);
logger.log(Level.INFO, "got expected exception");
List<Request> 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();
}
for (int i = 0; i < responses.size(); i++) {
logger.log(Level.FINE, "took = " + responses.get(i).took());
} finally {
client.shutdownGracefully();
logger.log(Level.INFO, "count=" + count);
}
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();
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View file

@ -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<Channel>() {
@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);
}
}
}
}

View file

@ -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();
}
}

View file

@ -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
* <pre>
@ -46,21 +28,22 @@ public class HttpBinTest {
* @throws Exception
*/
@Test
public void testHttpBinCookies() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
public void testHttpBinCookies() {
Client client = new Client();
try {
Request request = Request.get()
.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 -> {
.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);
})
.execute()
.get();
httpClient.close();
});
client.execute(request).get();
} finally {
client.shutdownGracefully();
}
}
}

View file

@ -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<FullHttpResponse, String> httpResponseStringFunction =
response -> response.content().toString(StandardCharsets.UTF_8);
final CompletableFuture<String> 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();
}
}

View file

@ -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);
}
}
}

View file

@ -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());
}
}

View file

@ -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();
}
}

View file

@ -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 -> {
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);
})
.execute()
.get();
httpClient.close();
});
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();
try {
final Function<FullHttpResponse, String> httpResponseStringFunction =
response -> response.content().toString(StandardCharsets.UTF_8);
final CompletableFuture<String> completableFuture = httpClient.prepareGet()
.setURL("http://index.hbz-nrw.de")
.execute(httpResponseStringFunction)
Request request = Request.get().setURL("http://xbib.org")
.build();
final CompletableFuture<String> completableFuture = httpClient.execute(request, httpResponseStringFunction)
.exceptionally(Throwable::getMessage)
.thenCompose(content -> httpClient.prepareGet()
.setURL("http://google.de/?query=" + content)
.execute(httpResponseStringFunction));
.thenCompose(content -> httpClient.execute(Request.post()
.setURL("http://google.de")
.addParam("query", content)
.build(), httpResponseStringFunction));
String result = completableFuture.join();
logger.log(Level.FINE, "completablefuture result = " + result);
httpClient.close();
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()
try {
httpClient.execute(Request.get()
.setURL("http://xbib.org")
.onResponse(fullHttpResponse -> {
.build()
.setResponseListener(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()
.setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e)))
.get();
httpClient.close();
} 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()
try {
httpClient.execute(Request.get()
.setURL("http://xbib.org")
.onResponse(fullHttpResponse -> {
.build()
.setResponseListener(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()
.setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e)))
.get();
httpClient.close();
logger.log(Level.FINE, "end");
} finally {
httpClient.shutdownGracefully();
}
}
@Test
public void testXbibTwoSequentialRequests() throws Exception {
HttpClient httpClient = HttpClient.builder()
.build();
httpClient.prepareGet()
public void testXbibTwoSequentialRequests() {
Client httpClient = new Client();
try {
httpClient.execute(Request.get()
.setVersion("HTTP/1.1")
.setURL("http://xbib.org")
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
.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);
})
.execute()
}))
.get();
httpClient.prepareGet()
httpClient.execute(Request.get()
.setVersion("HTTP/1.1")
.setURL("http://xbib.org")
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
.onResponse(fullHttpResponse -> {
.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);
})
.execute()
}))
.get();
httpClient.close();
} finally {
httpClient.shutdownGracefully();
}
}
}

View file

@ -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);
}
}

View file

@ -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<Boolean> completableFuture = new CompletableFuture<>();
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
Channel clientChannel = null;
try {
Bootstrap bootstrap = new Bootstrap()
.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@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();
}
}
}

View file

@ -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<HttpTransport> 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<HttpTransport> 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<HttpTransport> 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<Boolean> 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<SocketChannel> {
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<FullHttpResponse> {
@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);
}
}
}
}

View file

@ -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<Http2Transport> 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<Http2Transport> 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<Http2Transport> 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<Boolean> settingsPromise;
private SortedMap<Integer, CompletableFuture<Boolean>> 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<Boolean> 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<Boolean> 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<Boolean> promise : streamidPromiseMap.values()) {
promise.complete(true);
}
}
void fail(Throwable throwable) {
for (CompletableFuture<Boolean> promise : streamidPromiseMap.values()) {
promise.completeExceptionally(throwable);
}
}
void close() {
if (channel != null) {
channel.close();
}
client.close(this);
}
}
class Initializer extends ChannelInitializer<SocketChannel> {
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<Http2Settings> {
@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<FullHttpResponse> {
@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();
}
}
}