large refactoring, new transport layer, update to Netty 4.1.22
This commit is contained in:
parent
2572b6cb7f
commit
c43c3b9f67
78 changed files with 3921 additions and 5060 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -9,4 +9,5 @@
|
|||
/.project
|
||||
/.gradle
|
||||
/build
|
||||
/out
|
||||
*~
|
53
build.gradle
53
build.gradle
|
@ -1,12 +1,15 @@
|
|||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
plugins {
|
||||
id "org.sonarqube" version "2.2"
|
||||
id "org.xbib.gradle.plugin.asciidoctor" version "1.5.4.1.0"
|
||||
id "io.codearte.nexus-staging" version "0.7.0"
|
||||
id "org.sonarqube" version "2.6.1"
|
||||
id "io.codearte.nexus-staging" version "0.11.0"
|
||||
id "org.xbib.gradle.plugin.asciidoctor" version "1.6.0.0"
|
||||
}
|
||||
|
||||
printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" +
|
||||
printf "Date: %s\nHost: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGradle: %s Groovy: %s Java: %s\n" +
|
||||
"Build: group: ${project.group} name: ${project.name} version: ${project.version}\n",
|
||||
ZonedDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME),
|
||||
InetAddress.getLocalHost(),
|
||||
System.getProperty("os.name"),
|
||||
System.getProperty("os.arch"),
|
||||
|
@ -15,24 +18,15 @@ printf "Host: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGroovy: %s\nGradle: %s\n" +
|
|||
System.getProperty("java.vm.version"),
|
||||
System.getProperty("java.vm.vendor"),
|
||||
System.getProperty("java.vm.name"),
|
||||
GroovySystem.getVersion(),
|
||||
gradle.gradleVersion
|
||||
gradle.gradleVersion, GroovySystem.getVersion(), JavaVersion.current()
|
||||
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'maven'
|
||||
apply plugin: 'signing'
|
||||
apply plugin: 'findbugs'
|
||||
apply plugin: 'pmd'
|
||||
apply plugin: 'checkstyle'
|
||||
apply plugin: "jacoco"
|
||||
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
|
||||
apply plugin: "io.codearte.nexus-staging"
|
||||
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
|
||||
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
configurations {
|
||||
alpnagent
|
||||
asciidoclet
|
||||
|
@ -43,9 +37,11 @@ dependencies {
|
|||
compile "io.netty:netty-codec-http2:${project.property('netty.version')}"
|
||||
compile "io.netty:netty-handler-proxy:${project.property('netty.version')}"
|
||||
compile "io.netty:netty-tcnative-boringssl-static:${project.property('tcnative.version')}"
|
||||
alpnagent "org.mortbay.jetty.alpn:jetty-alpn-agent:${project.property('alpnagent.version')}"
|
||||
compile "org.xbib:net-url:${project.property('xbib-net-url.version')}"
|
||||
testCompile "junit:junit:${project.property('junit.version')}"
|
||||
asciidoclet "org.asciidoctor:asciidoclet:${project.property('asciidoclet.version')}"
|
||||
testCompile "com.fasterxml.jackson.core:jackson-databind:${project.property('jackson.version')}"
|
||||
alpnagent "org.mortbay.jetty.alpn:jetty-alpn-agent:${project.property('alpnagent.version')}"
|
||||
asciidoclet "org.xbib:asciidoclet:${project.property('asciidoclet.version')}"
|
||||
wagon "org.apache.maven.wagon:wagon-ssh:${project.property('wagon.version')}"
|
||||
}
|
||||
|
||||
|
@ -54,7 +50,7 @@ targetCompatibility = JavaVersion.VERSION_1_8
|
|||
|
||||
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
|
||||
tasks.withType(JavaCompile) {
|
||||
options.compilerArgs << "-Xlint:all"
|
||||
options.compilerArgs << "-Xlint:all,-serial"
|
||||
}
|
||||
|
||||
jar {
|
||||
|
@ -64,7 +60,9 @@ jar {
|
|||
}
|
||||
|
||||
test {
|
||||
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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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/"
|
||||
|
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
|
||||
|
|
6
gradlew
vendored
6
gradlew
vendored
|
@ -33,11 +33,11 @@ DEFAULT_JVM_OPTS=""
|
|||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
|
@ -155,7 +155,7 @@ if $cygwin ; then
|
|||
fi
|
||||
|
||||
# Escape application args
|
||||
save ( ) {
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
= Netty HTTP client
|
||||
Jörg Prante
|
||||
Version 4.1.9.0
|
||||
Version 4.1.22.0
|
||||
|
||||
|
|
218
src/main/java/org/xbib/netty/http/client/Client.java
Normal file
218
src/main/java/org/xbib/netty/http/client/Client.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package org.xbib.netty.http.client;
|
||||
|
||||
/**
|
||||
* Client authentication modes, useful for SSL channels.
|
||||
*/
|
||||
public enum ClientAuthMode {
|
||||
NONE, WANT, NEED
|
||||
}
|
181
src/main/java/org/xbib/netty/http/client/ClientBuilder.java
Normal file
181
src/main/java/org/xbib/netty/http/client/ClientBuilder.java
Normal 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);
|
||||
}
|
||||
}
|
410
src/main/java/org/xbib/netty/http/client/ClientConfig.java
Normal file
410
src/main/java/org/xbib/netty/http/client/ClientConfig.java
Normal 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;
|
||||
}
|
||||
}
|
113
src/main/java/org/xbib/netty/http/client/HttpAddress.java
Normal file
113
src/main/java/org/xbib/netty/http/client/HttpAddress.java
Normal 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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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<>();
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
216
src/main/java/org/xbib/netty/http/client/Request.java
Normal file
216
src/main/java/org/xbib/netty/http/client/Request.java
Normal 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);
|
||||
}
|
||||
}
|
329
src/main/java/org/xbib/netty/http/client/RequestBuilder.java
Normal file
329
src/main/java/org/xbib/netty/http/client/RequestBuilder.java
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
/**
|
||||
* Internal classes for Netty HTTP client.
|
||||
*/
|
||||
package org.xbib.netty.http.client.internal;
|
|
@ -2,8 +2,6 @@ package org.xbib.netty.http.client.listener;
|
|||
|
||||
import io.netty.handler.codec.http.cookie.Cookie;
|
||||
|
||||
/**
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface CookieListener {
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* Classes for transports in the Netty client.
|
||||
*/
|
||||
package org.xbib.netty.http.client.transport;
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
184
src/test/java/org/xbib/netty/http/client/test/ClientTest.java
Normal file
184
src/test/java/org/xbib/netty/http/client/test/ClientTest.java
Normal 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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue