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
|
/.project
|
||||||
/.gradle
|
/.gradle
|
||||||
/build
|
/build
|
||||||
|
/out
|
||||||
*~
|
*~
|
53
build.gradle
53
build.gradle
|
@ -1,12 +1,15 @@
|
||||||
|
import java.time.ZonedDateTime
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id "org.sonarqube" version "2.2"
|
id "org.sonarqube" version "2.6.1"
|
||||||
id "org.xbib.gradle.plugin.asciidoctor" version "1.5.4.1.0"
|
id "io.codearte.nexus-staging" version "0.11.0"
|
||||||
id "io.codearte.nexus-staging" version "0.7.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",
|
"Build: group: ${project.group} name: ${project.name} version: ${project.version}\n",
|
||||||
|
ZonedDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME),
|
||||||
InetAddress.getLocalHost(),
|
InetAddress.getLocalHost(),
|
||||||
System.getProperty("os.name"),
|
System.getProperty("os.name"),
|
||||||
System.getProperty("os.arch"),
|
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.version"),
|
||||||
System.getProperty("java.vm.vendor"),
|
System.getProperty("java.vm.vendor"),
|
||||||
System.getProperty("java.vm.name"),
|
System.getProperty("java.vm.name"),
|
||||||
GroovySystem.getVersion(),
|
gradle.gradleVersion, GroovySystem.getVersion(), JavaVersion.current()
|
||||||
gradle.gradleVersion
|
|
||||||
|
|
||||||
apply plugin: 'java'
|
apply plugin: 'java'
|
||||||
apply plugin: 'maven'
|
apply plugin: 'maven'
|
||||||
apply plugin: 'signing'
|
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: "io.codearte.nexus-staging"
|
||||||
|
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
|
||||||
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
alpnagent
|
alpnagent
|
||||||
asciidoclet
|
asciidoclet
|
||||||
|
@ -43,9 +37,11 @@ dependencies {
|
||||||
compile "io.netty:netty-codec-http2:${project.property('netty.version')}"
|
compile "io.netty:netty-codec-http2:${project.property('netty.version')}"
|
||||||
compile "io.netty:netty-handler-proxy:${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')}"
|
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')}"
|
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')}"
|
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'
|
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
|
||||||
tasks.withType(JavaCompile) {
|
tasks.withType(JavaCompile) {
|
||||||
options.compilerArgs << "-Xlint:all"
|
options.compilerArgs << "-Xlint:all,-serial"
|
||||||
}
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
|
@ -64,7 +60,9 @@ jar {
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
|
if (JavaVersion.current() == JavaVersion.VERSION_1_8) {
|
||||||
jvmArgs "-javaagent:" + configurations.alpnagent.asPath
|
jvmArgs "-javaagent:" + configurations.alpnagent.asPath
|
||||||
|
}
|
||||||
testLogging {
|
testLogging {
|
||||||
showStandardStreams = false
|
showStandardStreams = false
|
||||||
exceptionFormat = 'full'
|
exceptionFormat = 'full'
|
||||||
|
@ -72,18 +70,20 @@ test {
|
||||||
}
|
}
|
||||||
|
|
||||||
asciidoctor {
|
asciidoctor {
|
||||||
backends 'html5'
|
attributes toc: 'left',
|
||||||
separateOutputDirs = false
|
doctype: 'book',
|
||||||
attributes 'source-highlighter': 'coderay',
|
icons: 'font',
|
||||||
toc : '',
|
encoding: 'utf-8',
|
||||||
idprefix : '',
|
sectlink: true,
|
||||||
idseparator : '-',
|
sectanchors: true,
|
||||||
stylesheet: "${projectDir}/src/docs/asciidoc/css/foundation.css"
|
linkattrs: true,
|
||||||
|
imagesdir: 'img',
|
||||||
|
'source-highlighter': 'coderay'
|
||||||
}
|
}
|
||||||
|
|
||||||
javadoc {
|
javadoc {
|
||||||
options.docletpath = configurations.asciidoclet.files.asType(List)
|
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.overview = "src/docs/asciidoclet/overview.adoc"
|
||||||
options.addStringOption "-base-dir", "${projectDir}"
|
options.addStringOption "-base-dir", "${projectDir}"
|
||||||
options.addStringOption "-attribute",
|
options.addStringOption "-attribute",
|
||||||
|
@ -117,4 +117,3 @@ if (project.hasProperty('signing.keyId')) {
|
||||||
|
|
||||||
apply from: 'gradle/ext.gradle'
|
apply from: 'gradle/ext.gradle'
|
||||||
apply from: 'gradle/publish.gradle'
|
apply from: 'gradle/publish.gradle'
|
||||||
apply from: 'gradle/sonarqube.gradle'
|
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
group = org.xbib
|
group = org.xbib
|
||||||
name = netty-http-client
|
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
|
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
|
junit.version = 4.12
|
||||||
asciidoclet.version = 1.5.4
|
jackson.version = 2.8.11.1
|
||||||
wagon.version = 2.12
|
asciidoclet.version = 1.6.0.0
|
||||||
|
wagon.version = 3.0.0
|
||||||
|
|
|
@ -22,10 +22,8 @@ tasks.withType(Checkstyle) {
|
||||||
|
|
||||||
jacocoTestReport {
|
jacocoTestReport {
|
||||||
reports {
|
reports {
|
||||||
xml.enabled true
|
xml.enabled = true
|
||||||
csv.enabled false
|
csv.enabled = false
|
||||||
xml.destination "${buildDir}/reports/jacoco-xml"
|
|
||||||
html.destination "${buildDir}/reports/jacoco-html"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +31,7 @@ sonarqube {
|
||||||
properties {
|
properties {
|
||||||
property "sonar.projectName", "${project.group} ${project.name}"
|
property "sonar.projectName", "${project.group} ${project.name}"
|
||||||
property "sonar.sourceEncoding", "UTF-8"
|
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.scm.provider", "git"
|
||||||
property "sonar.java.coveragePlugin", "jacoco"
|
property "sonar.java.coveragePlugin", "jacoco"
|
||||||
property "sonar.junit.reportsPath", "build/test-results/test/"
|
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
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD="maximum"
|
MAX_FD="maximum"
|
||||||
|
|
||||||
warn ( ) {
|
warn () {
|
||||||
echo "$*"
|
echo "$*"
|
||||||
}
|
}
|
||||||
|
|
||||||
die ( ) {
|
die () {
|
||||||
echo
|
echo
|
||||||
echo "$*"
|
echo "$*"
|
||||||
echo
|
echo
|
||||||
|
@ -155,7 +155,7 @@ if $cygwin ; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Escape application args
|
# Escape application args
|
||||||
save ( ) {
|
save () {
|
||||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||||
echo " "
|
echo " "
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
= Netty HTTP client
|
= Netty HTTP client
|
||||||
Jörg Prante
|
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 @@
|
||||||
/*
|
package org.xbib.netty.http.client;
|
||||||
* 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.bootstrap.Bootstrap;
|
||||||
import org.xbib.netty.http.client.HttpClient;
|
|
||||||
|
|
||||||
import java.util.Optional;
|
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)",
|
private static final String USER_AGENT = String.format("XbibHttpClient/%s (Java/%s/%s) (Netty/%s)",
|
||||||
httpClientVersion(), javaVendor(), javaVersion(), nettyVersion());
|
httpClientVersion(), javaVendor(), javaVersion(), nettyVersion());
|
||||||
|
|
||||||
private HttpClientUserAgent() {
|
private UserAgent() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getUserAgent() {
|
public static String getUserAgent() {
|
||||||
|
@ -38,7 +23,7 @@ public final class HttpClientUserAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String httpClientVersion() {
|
private static String httpClientVersion() {
|
||||||
return Optional.ofNullable(HttpClient.class.getPackage().getImplementationVersion())
|
return Optional.ofNullable(UserAgent.class.getPackage().getImplementationVersion())
|
||||||
.orElse("unknown");
|
.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;
|
package org.xbib.netty.http.client.handler;
|
||||||
|
|
||||||
import io.netty.buffer.ByteBuf;
|
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.
|
* A Netty handler that logs the I/O traffic of a connection.
|
||||||
*/
|
*/
|
||||||
@ChannelHandler.Sharable
|
@ChannelHandler.Sharable
|
||||||
class TrafficLoggingHandler extends LoggingHandler {
|
public class TrafficLoggingHandler extends LoggingHandler {
|
||||||
|
|
||||||
TrafficLoggingHandler() {
|
public TrafficLoggingHandler() {
|
||||||
super("client", LogLevel.TRACE);
|
super("client", LogLevel.TRACE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
|
public void channelRegistered(ChannelHandlerContext ctx) {
|
||||||
ctx.fireChannelRegistered();
|
ctx.fireChannelRegistered();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
|
public void channelUnregistered(ChannelHandlerContext ctx) {
|
||||||
ctx.fireChannelUnregistered();
|
ctx.fireChannelUnregistered();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void flush(ChannelHandlerContext ctx) throws Exception {
|
public void flush(ChannelHandlerContext ctx) {
|
||||||
ctx.flush();
|
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;
|
package org.xbib.netty.http.client.handler;
|
||||||
|
|
||||||
import io.netty.channel.ChannelHandler;
|
import io.netty.channel.ChannelHandler;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
import io.netty.channel.ChannelInboundHandlerAdapter;
|
import io.netty.channel.ChannelInboundHandlerAdapter;
|
||||||
import io.netty.channel.socket.ChannelInputShutdownReadComplete;
|
import io.netty.channel.socket.ChannelInputShutdownReadComplete;
|
||||||
import io.netty.handler.codec.http2.Http2ConnectionPrefaceWrittenEvent;
|
|
||||||
import io.netty.handler.ssl.SslCloseCompletionEvent;
|
import io.netty.handler.ssl.SslCloseCompletionEvent;
|
||||||
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
|
@ -36,10 +20,8 @@ class UserEventLogger extends ChannelInboundHandlerAdapter {
|
||||||
@Override
|
@Override
|
||||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||||
logger.log(Level.FINE, () -> "got user event " + evt);
|
logger.log(Level.FINE, () -> "got user event " + evt);
|
||||||
if (evt instanceof Http2ConnectionPrefaceWrittenEvent ||
|
if (evt instanceof SslCloseCompletionEvent ||
|
||||||
evt instanceof SslCloseCompletionEvent ||
|
|
||||||
evt instanceof ChannelInputShutdownReadComplete) {
|
evt instanceof ChannelInputShutdownReadComplete) {
|
||||||
// log expected events
|
|
||||||
logger.log(Level.FINE, () -> "user event is expected: " + evt);
|
logger.log(Level.FINE, () -> "user event is expected: " + evt);
|
||||||
return;
|
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;
|
import io.netty.handler.codec.http.cookie.Cookie;
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface CookieListener {
|
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;
|
package org.xbib.netty.http.client.listener;
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface ExceptionListener {
|
public interface ExceptionListener {
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when an exception is transported to a listener.
|
|
||||||
* @param throwable the exception
|
|
||||||
*/
|
|
||||||
void onException(Throwable throwable);
|
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;
|
package org.xbib.netty.http.client.listener;
|
||||||
|
|
||||||
import io.netty.handler.codec.http.HttpHeaders;
|
import io.netty.handler.codec.http.HttpHeaders;
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface HttpHeadersListener {
|
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;
|
package org.xbib.netty.http.client.listener;
|
||||||
|
|
||||||
import io.netty.handler.codec.http.FullHttpResponse;
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
import io.netty.handler.codec.http2.Http2Headers;
|
import io.netty.handler.codec.http2.Http2Headers;
|
||||||
|
|
||||||
/**
|
|
||||||
* This listener can forward HTTP push.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface HttpPushListener {
|
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;
|
package org.xbib.netty.http.client.listener;
|
||||||
|
|
||||||
import io.netty.handler.codec.http.FullHttpResponse;
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
public interface HttpResponseListener {
|
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;
|
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;
|
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;
|
package org.xbib.netty.http.client.util;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
|
@ -1,58 +1,54 @@
|
||||||
package org.xbib.netty.http.client.test;
|
package org.xbib.netty.http.client.test;
|
||||||
|
|
||||||
|
import org.junit.Ignore;
|
||||||
import org.junit.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 org.xbib.netty.http.client.test.LoggingBase;
|
||||||
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.logging.ConsoleHandler;
|
|
||||||
import java.util.logging.Handler;
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.LogManager;
|
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.logging.SimpleFormatter;
|
|
||||||
|
|
||||||
/**
|
@Ignore
|
||||||
*/
|
public class AkamaiTest extends LoggingBase {
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger("");
|
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
|
@Test
|
||||||
public void testAkamaiHttps() throws Exception {
|
public void testAkamaiHttps() {
|
||||||
HttpClient httpClient = HttpClient.getInstance();
|
Client client = new Client();
|
||||||
httpClient.prepareGet("https://http2.akamai.com/demo/h2_demo_frame.html")
|
try {
|
||||||
.setHttp2()
|
Request request = Request.get()
|
||||||
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
//.setURL("https://http2.akamai.com/demo/h2_demo_frame.html")
|
||||||
.onResponse(fullHttpResponse -> {
|
.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);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status()
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status()
|
||||||
+ " response body = " + response);
|
+ " response body = " + response);
|
||||||
})
|
})
|
||||||
.onPushReceived((requestHeaders, fullHttpResponse) -> {
|
.setPushListener((requestHeaders, fullHttpResponse) -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "received push: request headers = " + requestHeaders
|
logger.log(Level.INFO, "received push: request headers = " + requestHeaders
|
||||||
+ " status = " + fullHttpResponse.status()
|
+ " status = " + fullHttpResponse.status()
|
||||||
+ " response headers = " + fullHttpResponse.headers().entries()
|
+ " response headers = " + fullHttpResponse.headers().entries()
|
||||||
+ " response body = " + response
|
+ " response body = " + response
|
||||||
);
|
);
|
||||||
})
|
});
|
||||||
.execute()
|
client.execute(request).get();
|
||||||
.get();
|
} finally {
|
||||||
httpClient.close();
|
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;
|
package org.xbib.netty.http.client.test;
|
||||||
|
|
||||||
|
import org.junit.Ignore;
|
||||||
import org.junit.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.HttpRequestBuilder;
|
import org.xbib.netty.http.client.Request;
|
||||||
import org.xbib.netty.http.client.HttpRequestContext;
|
import org.xbib.netty.http.client.transport.Transport;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.ConnectException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.logging.ConsoleHandler;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.logging.Handler;
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.LogManager;
|
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.logging.SimpleFormatter;
|
|
||||||
|
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertEquals;
|
||||||
|
|
||||||
/**
|
@Ignore
|
||||||
*/
|
public class ElasticsearchTest extends LoggingBase {
|
||||||
public class ElasticsearchTest {
|
|
||||||
|
|
||||||
static {
|
private static final Logger logger = Logger.getLogger(ElasticsearchTest.class.getName());
|
||||||
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("");
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testElasticsearchCreateDocument() throws Exception {
|
public void testElasticsearchCreateDocument() {
|
||||||
HttpClient httpClient = HttpClient.builder()
|
Client client = new Client();
|
||||||
.build();
|
|
||||||
try {
|
try {
|
||||||
HttpRequestContext requestContext = httpClient.preparePut()
|
Request request = Request.put().setURL("http://localhost:9200/test/test/1")
|
||||||
.setURL("http://localhost:9200/test/test/1")
|
|
||||||
.json("{\"text\":\"Hello World\"}")
|
.json("{\"text\":\"Hello World\"}")
|
||||||
.onResponse(fullHttpResponse -> {
|
.build()
|
||||||
|
.setResponseListener(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
})
|
})
|
||||||
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
.setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e));
|
||||||
.execute()
|
client.execute(request);
|
||||||
.get();
|
} finally {
|
||||||
logger.log(Level.FINE, "took = " + requestContext.took());
|
client.shutdownGracefully();
|
||||||
} catch (Exception exception) {
|
|
||||||
assertTrue(exception.getCause() instanceof ConnectException);
|
|
||||||
logger.log(Level.INFO, "got expected exception");
|
|
||||||
}
|
}
|
||||||
httpClient.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testElasticsearchMatchQuery() throws Exception {
|
public void testElasticsearchMatchQuery() {
|
||||||
HttpClient httpClient = HttpClient.builder()
|
Client client = new Client();
|
||||||
.build();
|
|
||||||
try {
|
try {
|
||||||
HttpRequestContext requestContext = httpClient.preparePost()
|
Request request = Request.post().setURL("http://localhost:9200/test/_search")
|
||||||
.setURL("http://localhost:9200/test/_search")
|
.json("{\"query\":{\"match\":{\"text\":\"Hello World\"}}}")
|
||||||
.json("{\"query\":{\"match\":{\"_all\":\"Hello World\"}}}")
|
.build()
|
||||||
.onResponse(fullHttpResponse -> {
|
.setResponseListener(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
})
|
})
|
||||||
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
.setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e));
|
||||||
.execute()
|
client.execute(request).get();
|
||||||
.get();
|
} finally {
|
||||||
logger.log(Level.FINE, "took = " + requestContext.took());
|
client.shutdownGracefully();
|
||||||
} catch (Exception exception) {
|
|
||||||
assertTrue(exception.getCause() instanceof ConnectException);
|
|
||||||
logger.log(Level.INFO, "got expected exception");
|
|
||||||
}
|
}
|
||||||
httpClient.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testElasticsearchConcurrent() throws Exception {
|
public void testElasticsearchConcurrent() {
|
||||||
int max = 100;
|
Client client = Client.builder().setReadTimeoutMillis(20000).build();
|
||||||
HttpClient httpClient = HttpClient.builder()
|
int max = 1000;
|
||||||
.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++) {
|
|
||||||
try {
|
try {
|
||||||
responses.add(contexts.get(i).get());
|
List<Request> queries = new ArrayList<>();
|
||||||
} catch (Exception exception) {
|
for (int i = 0; i < max; i++) {
|
||||||
assertTrue(exception.getCause() instanceof ConnectException);
|
queries.add(newRequest());
|
||||||
logger.log(Level.INFO, "got expected exception");
|
|
||||||
}
|
}
|
||||||
|
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++) {
|
} finally {
|
||||||
logger.log(Level.FINE, "took = " + responses.get(i).took());
|
client.shutdownGracefully();
|
||||||
|
logger.log(Level.INFO, "count=" + count);
|
||||||
}
|
}
|
||||||
httpClient.close();
|
assertEquals(max, count.get());
|
||||||
logger.log(Level.INFO, "pool peak = " + httpClient.poolMap().getHttpClientChannelPoolHandler().getPeak());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpRequestBuilder createQuery(HttpClient httpClient) throws IOException {
|
private Request newRequest() {
|
||||||
return httpClient.preparePost()
|
return Request.post()
|
||||||
.setURL("http://localhost:9200/test/_search")
|
.setURL("http://localhost:9200/test/_search")
|
||||||
.json("{\"query\":{\"match\":{\"_all\":\"Hello World\"}}}")
|
.json("{\"query\":{\"match\":{\"text\":\"Hello World\"}}}")
|
||||||
.addHeader("connection", "keep-alive")
|
.addHeader("connection", "keep-alive")
|
||||||
.onResponse(fullHttpResponse -> {
|
.build()
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
.setResponseListener(fullHttpResponse ->
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.FINE, "status = " + fullHttpResponse.status() +
|
||||||
})
|
" counter = " + count.incrementAndGet() +
|
||||||
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e));
|
" 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;
|
package org.xbib.netty.http.client.test;
|
||||||
|
|
||||||
import org.junit.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.nio.charset.StandardCharsets;
|
||||||
import java.util.logging.ConsoleHandler;
|
|
||||||
import java.util.logging.Handler;
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.LogManager;
|
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.logging.SimpleFormatter;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*/
|
*/
|
||||||
public class HttpBinTest {
|
public class HttpBinTest extends LoggingBase {
|
||||||
|
|
||||||
static {
|
private static final Logger logger = Logger.getLogger(HttpBinTest.class.getName());
|
||||||
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 httpbin.org cookie setter with HTTP/1.1.
|
* Test httpbin.org "Set-Cookie:" header after redirection of URL.
|
||||||
*
|
*
|
||||||
* The reponse body should be
|
* The reponse body should be
|
||||||
* <pre>
|
* <pre>
|
||||||
|
@ -46,21 +28,22 @@ public class HttpBinTest {
|
||||||
* @throws Exception
|
* @throws Exception
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public void testHttpBinCookies() throws Exception {
|
public void testHttpBinCookies() {
|
||||||
HttpClient httpClient = HttpClient.builder()
|
Client client = new Client();
|
||||||
.build();
|
try {
|
||||||
httpClient.prepareGet()
|
Request request = Request.get()
|
||||||
.setURL("http://httpbin.org/cookies/set?name=value")
|
.setURL("http://httpbin.org/cookies/set?name=value")
|
||||||
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
.build()
|
||||||
.onCookie(cookie -> logger.log(Level.INFO, cookie.toString()))
|
.setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
||||||
.onHeaders(headers -> logger.log(Level.INFO, headers.toString()))
|
.setCookieListener(cookie -> logger.log(Level.INFO, "this is the cookie " + cookie.toString()))
|
||||||
.onResponse(fullHttpResponse -> {
|
.setHeadersListener(headers -> logger.log(Level.INFO, headers.toString()))
|
||||||
|
.setResponseListener(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
})
|
});
|
||||||
.execute()
|
client.execute(request).get();
|
||||||
.get();
|
} finally {
|
||||||
httpClient.close();
|
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;
|
package org.xbib.netty.http.client.test;
|
||||||
|
|
||||||
import io.netty.handler.codec.http.HttpMethod;
|
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.xbib.netty.http.client.HttpClientRequestBuilder;
|
import org.xbib.netty.http.client.Request;
|
||||||
import org.xbib.netty.http.client.HttpRequestBuilder;
|
import org.xbib.netty.http.client.RequestBuilder;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
|
||||||
|
@ -24,15 +23,15 @@ public class URITest {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testClientRequestURIs() {
|
public void testRequestURIs() {
|
||||||
HttpRequestBuilder httpRequestBuilder = HttpClientRequestBuilder.builder(HttpMethod.GET);
|
RequestBuilder httpRequestBuilder = Request.get();
|
||||||
httpRequestBuilder.setURL("https://localhost").path("/path");
|
httpRequestBuilder.setURL("https://localhost").path("/path");
|
||||||
assertEquals("/path", httpRequestBuilder.build().uri());
|
assertEquals("/path", httpRequestBuilder.build().relativeUri());
|
||||||
httpRequestBuilder.path("/foobar");
|
httpRequestBuilder.path("/foobar");
|
||||||
assertEquals("/foobar", httpRequestBuilder.build().uri());
|
assertEquals("/foobar", httpRequestBuilder.build().relativeUri());
|
||||||
httpRequestBuilder.path("/path1?a=b");
|
httpRequestBuilder.path("/path1?a=b");
|
||||||
assertEquals("/path1?a=b", httpRequestBuilder.build().uri());
|
assertEquals("/path1?a=b", httpRequestBuilder.build().relativeUri());
|
||||||
httpRequestBuilder.path("/path2?c=d");
|
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;
|
package org.xbib.netty.http.client.test;
|
||||||
|
|
||||||
import io.netty.handler.codec.http.FullHttpResponse;
|
import io.netty.handler.codec.http.FullHttpResponse;
|
||||||
import org.junit.Ignore;
|
import io.netty.handler.proxy.HttpProxyHandler;
|
||||||
import org.junit.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 org.xbib.netty.http.client.test.LoggingBase;
|
||||||
|
|
||||||
import java.net.InetSocketAddress;
|
import java.net.InetSocketAddress;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.logging.ConsoleHandler;
|
|
||||||
import java.util.logging.Handler;
|
|
||||||
import java.util.logging.Level;
|
import java.util.logging.Level;
|
||||||
import java.util.logging.LogManager;
|
|
||||||
import java.util.logging.Logger;
|
import java.util.logging.Logger;
|
||||||
import java.util.logging.SimpleFormatter;
|
|
||||||
|
|
||||||
/**
|
public class XbibTest extends LoggingBase {
|
||||||
*/
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger("");
|
private static final Logger logger = Logger.getLogger("");
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testXbibOrgWithDefaults() throws Exception {
|
public void testXbibOrgWithDefaults() {
|
||||||
HttpClient httpClient = HttpClient.builder()
|
Client client = new Client();
|
||||||
.build();
|
try {
|
||||||
httpClient.prepareGet()
|
Request request = Request.get().setURL("http://xbib.org")
|
||||||
.setURL("http://xbib.org")
|
.build()
|
||||||
.onResponse(fullHttpResponse -> {
|
.setResponseListener(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
})
|
});
|
||||||
.execute()
|
client.execute(request);
|
||||||
.get();
|
} finally {
|
||||||
httpClient.close();
|
client.shutdownGracefully();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testXbibOrgWithCompletableFuture() throws Exception {
|
public void testXbibOrgWithCompletableFuture() {
|
||||||
HttpClient httpClient = HttpClient.builder()
|
Client httpClient = Client.builder()
|
||||||
.setTcpNodelay(true)
|
.setTcpNodelay(true)
|
||||||
.build();
|
.build();
|
||||||
|
try {
|
||||||
final Function<FullHttpResponse, String> httpResponseStringFunction =
|
final Function<FullHttpResponse, String> httpResponseStringFunction =
|
||||||
response -> response.content().toString(StandardCharsets.UTF_8);
|
response -> response.content().toString(StandardCharsets.UTF_8);
|
||||||
|
Request request = Request.get().setURL("http://xbib.org")
|
||||||
final CompletableFuture<String> completableFuture = httpClient.prepareGet()
|
.build();
|
||||||
.setURL("http://index.hbz-nrw.de")
|
final CompletableFuture<String> completableFuture = httpClient.execute(request, httpResponseStringFunction)
|
||||||
.execute(httpResponseStringFunction)
|
|
||||||
.exceptionally(Throwable::getMessage)
|
.exceptionally(Throwable::getMessage)
|
||||||
.thenCompose(content -> httpClient.prepareGet()
|
.thenCompose(content -> httpClient.execute(Request.post()
|
||||||
.setURL("http://google.de/?query=" + content)
|
.setURL("http://google.de")
|
||||||
.execute(httpResponseStringFunction));
|
.addParam("query", content)
|
||||||
|
.build(), httpResponseStringFunction));
|
||||||
String result = completableFuture.join();
|
String result = completableFuture.join();
|
||||||
|
logger.info("result = " + result);
|
||||||
logger.log(Level.FINE, "completablefuture result = " + result);
|
} finally {
|
||||||
|
httpClient.shutdownGracefully();
|
||||||
httpClient.close();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Ignore
|
public void testXbibOrgWithProxy() {
|
||||||
public void testXbibOrgWithProxy() throws Exception {
|
Client httpClient = Client.builder()
|
||||||
HttpClient httpClient = HttpClient.builder()
|
.setHttpProxyHandler(new HttpProxyHandler(new InetSocketAddress("80.241.223.251", 8080)))
|
||||||
.setHttpProxyHandler(new InetSocketAddress("80.241.223.251", 8080))
|
|
||||||
.setConnectTimeoutMillis(30000)
|
.setConnectTimeoutMillis(30000)
|
||||||
.setReadTimeoutMillis(30000)
|
.setReadTimeoutMillis(30000)
|
||||||
.build();
|
.build();
|
||||||
httpClient.prepareGet()
|
try {
|
||||||
|
httpClient.execute(Request.get()
|
||||||
.setURL("http://xbib.org")
|
.setURL("http://xbib.org")
|
||||||
.onResponse(fullHttpResponse -> {
|
.build()
|
||||||
|
.setResponseListener(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
})
|
})
|
||||||
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
.setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e)))
|
||||||
.execute()
|
|
||||||
.get();
|
.get();
|
||||||
httpClient.close();
|
} finally {
|
||||||
|
httpClient.shutdownGracefully();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testXbibOrgWithVeryShortReadTimeout() throws Exception {
|
public void testXbibOrgWithVeryShortReadTimeout() {
|
||||||
logger.log(Level.FINE, "start");
|
Client httpClient = Client.builder()
|
||||||
HttpClient httpClient = HttpClient.builder()
|
|
||||||
.setReadTimeoutMillis(50)
|
.setReadTimeoutMillis(50)
|
||||||
.build();
|
.build();
|
||||||
httpClient.prepareGet()
|
try {
|
||||||
|
httpClient.execute(Request.get()
|
||||||
.setURL("http://xbib.org")
|
.setURL("http://xbib.org")
|
||||||
.onResponse(fullHttpResponse -> {
|
.build()
|
||||||
|
.setResponseListener(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
})
|
})
|
||||||
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
.setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e)))
|
||||||
.execute()
|
|
||||||
.get();
|
.get();
|
||||||
httpClient.close();
|
} finally {
|
||||||
logger.log(Level.FINE, "end");
|
httpClient.shutdownGracefully();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testXbibTwoSequentialRequests() throws Exception {
|
public void testXbibTwoSequentialRequests() {
|
||||||
HttpClient httpClient = HttpClient.builder()
|
Client httpClient = new Client();
|
||||||
.build();
|
try {
|
||||||
|
httpClient.execute(Request.get()
|
||||||
httpClient.prepareGet()
|
|
||||||
.setVersion("HTTP/1.1")
|
.setVersion("HTTP/1.1")
|
||||||
.setURL("http://xbib.org")
|
.setURL("http://xbib.org")
|
||||||
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
.build()
|
||||||
.onResponse(fullHttpResponse -> {
|
.setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
||||||
|
.setResponseListener(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
})
|
}))
|
||||||
.execute()
|
|
||||||
.get();
|
.get();
|
||||||
|
|
||||||
httpClient.prepareGet()
|
httpClient.execute(Request.get()
|
||||||
.setVersion("HTTP/1.1")
|
.setVersion("HTTP/1.1")
|
||||||
.setURL("http://xbib.org")
|
.setURL("http://xbib.org")
|
||||||
.onException(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
.build()
|
||||||
.onResponse(fullHttpResponse -> {
|
.setExceptionListener(e -> logger.log(Level.SEVERE, e.getMessage(), e))
|
||||||
|
.setResponseListener(fullHttpResponse -> {
|
||||||
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8);
|
||||||
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response);
|
||||||
})
|
}))
|
||||||
.execute()
|
|
||||||
.get();
|
.get();
|
||||||
|
} finally {
|
||||||
httpClient.close();
|
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