add Netty server

This commit is contained in:
Jörg Prante 2018-03-12 11:22:40 +01:00
parent 12a5f6418c
commit f2c483fcfa
135 changed files with 7122 additions and 223 deletions

2
.gitignore vendored
View file

@ -8,6 +8,6 @@
/.classpath /.classpath
/.project /.project
/.gradle /.gradle
/build build
out out
*~ *~

View file

@ -21,107 +21,182 @@ printf "Date: %s\nHost: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGradle: %s Groovy: %
System.getProperty("java.vm.name"), System.getProperty("java.vm.name"),
gradle.gradleVersion, GroovySystem.getVersion(), JavaVersion.current() gradle.gradleVersion, GroovySystem.getVersion(), JavaVersion.current()
apply plugin: 'java'
apply plugin: 'maven'
apply plugin: 'signing'
apply plugin: "com.github.spotbugs"
apply plugin: "io.codearte.nexus-staging" apply plugin: "io.codearte.nexus-staging"
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
configurations { subprojects {
alpnagent apply plugin: 'java'
asciidoclet apply plugin: 'maven'
wagon apply plugin: 'signing'
} apply plugin: "com.github.spotbugs"
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
dependencies { configurations {
compile "org.xbib:net-url:${project.property('xbib-net-url.version')}" alpnagent
compile "io.netty:netty-codec-http2:${project.property('netty.version')}" asciidoclet
compile "io.netty:netty-handler-proxy:${project.property('netty.version')}" wagon
compile "io.netty:netty-transport-native-epoll:${project.property('netty.version')}"
testCompile "io.netty:netty-tcnative-boringssl-static:${project.property('tcnative.version')}"
testCompile "org.conscrypt:conscrypt-openjdk-uber:${project.property('conscrypt.version')}"
testCompile "junit:junit:${project.property('junit.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')}"
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:all,-serial"
}
jar {
manifest {
attributes('Implementation-Version': project.version)
} }
}
test { dependencies {
if (JavaVersion.current() == JavaVersion.VERSION_1_8) { alpnagent "org.mortbay.jetty.alpn:jetty-alpn-agent:${project.property('alpnagent.version')}"
jvmArgs "-javaagent:" + configurations.alpnagent.asPath asciidoclet "org.xbib:asciidoclet:${project.property('asciidoclet.version')}"
wagon "org.apache.maven.wagon:wagon-ssh:${project.property('wagon.version')}"
} }
testLogging {
showStandardStreams = false sourceCompatibility = JavaVersion.VERSION_1_8
exceptionFormat = 'full' targetCompatibility = JavaVersion.VERSION_1_8
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:all,-serial"
} }
}
clean { jar {
delete 'out' manifest {
} attributes('Implementation-Version': project.version)
}
asciidoctor {
attributes toc: 'left',
doctype: 'book',
icons: 'font',
encoding: 'utf-8',
sectlink: true,
sectanchors: true,
linkattrs: true,
imagesdir: 'img',
'source-highlighter': 'coderay'
}
javadoc {
options.docletpath = configurations.asciidoclet.files.asType(List)
options.doclet = "org.xbib.asciidoclet.Asciidoclet"
options.overview = "src/docs/asciidoclet/overview.adoc"
options.addStringOption "-base-dir", "${projectDir}"
options.addStringOption "-attribute",
"name=${project.name},version=${project.version},title-link=https://github.com/jprante/${project.name}"
configure(options) {
noTimestamp = true
} }
}
task javadocJar(type: Jar, dependsOn: classes) { test {
from javadoc if (JavaVersion.current() == JavaVersion.VERSION_1_8) {
into "build/tmp" jvmArgs "-javaagent:" + configurations.alpnagent.asPath
classifier 'javadoc' }
} testLogging {
showStandardStreams = false
task sourcesJar(type: Jar, dependsOn: classes) { exceptionFormat = 'full'
from sourceSets.main.allSource }
into "build/tmp"
classifier 'sources'
}
artifacts {
archives javadocJar, sourcesJar
}
if (project.hasProperty('signing.keyId')) {
signing {
sign configurations.archives
} }
}
clean {
delete 'out'
}
asciidoctor {
attributes toc: 'left',
doctype: 'book',
icons: 'font',
encoding: 'utf-8',
sectlink: true,
sectanchors: true,
linkattrs: true,
imagesdir: 'img',
'source-highlighter': 'coderay'
}
javadoc {
options.docletpath = configurations.asciidoclet.files.asType(List)
options.doclet = "org.xbib.asciidoclet.Asciidoclet"
options.overview = "src/docs/asciidoclet/overview.adoc"
options.addStringOption "-base-dir", "${projectDir}"
options.addStringOption "-attribute",
"name=${project.name},version=${project.version},title-link=https://github.com/jprante/${project.name}"
configure(options) {
noTimestamp = true
}
}
task javadocJar(type: Jar, dependsOn: classes) {
from javadoc
into "build/tmp"
classifier 'javadoc'
}
task sourcesJar(type: Jar, dependsOn: classes) {
from sourceSets.main.allSource
into "build/tmp"
classifier 'sources'
}
artifacts {
archives javadocJar, sourcesJar
}
if (project.hasProperty('signing.keyId')) {
signing {
sign configurations.archives
}
}
ext {
user = 'jprante'
name = 'netty-http-client'
description = 'A java client for Elasticsearch'
scmUrl = 'https://github.com/' + user + '/' + name
scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git'
scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git'
}
task xbibUpload(type: Upload) {
group = 'publish'
configuration = configurations.archives
uploadDescriptor = true
repositories {
if (project.hasProperty("xbibUsername")) {
mavenDeployer {
configuration = configurations.wagon
repository(url: 'sftp://xbib.org/repository') {
authentication(userName: xbibUsername, privateKey: xbibPrivateKey)
}
}
}
}
}
task sonaTypeUpload(type: Upload) {
group = 'publish'
configuration = configurations.archives
uploadDescriptor = true
repositories {
if (project.hasProperty('ossrhUsername')) {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots') {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
pom.project {
groupId project.group
artifactId project.name
version project.version
name project.name
description description
packaging 'jar'
inceptionYear '2012'
url scmUrl
organization {
name 'xbib'
url 'http://xbib.org'
}
developers {
developer {
id user
name 'Jörg Prante'
email 'joergprante@gmail.com'
url 'https://github.com/jprante'
}
}
scm {
url scmUrl
connection scmConnection
developerConnection scmDeveloperConnection
}
licenses {
license {
name 'The Apache License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
}
}
}
}
}
}
spotbugs { spotbugs {
effort = "max" effort = "max"
@ -147,84 +222,6 @@ sonarqube {
} }
} }
ext {
user = 'jprante'
name = 'netty-http-client'
description = 'A java client for Elasticsearch'
scmUrl = 'https://github.com/' + user + '/' + name
scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git'
scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git'
}
task xbibUpload(type: Upload) {
group = 'publish'
configuration = configurations.archives
uploadDescriptor = true
repositories {
if (project.hasProperty("xbibUsername")) {
mavenDeployer {
configuration = configurations.wagon
repository(url: 'sftp://xbib.org/repository') {
authentication(userName: xbibUsername, privateKey: xbibPrivateKey)
}
}
}
}
}
task sonaTypeUpload(type: Upload) {
group = 'publish'
configuration = configurations.archives
uploadDescriptor = true
repositories {
if (project.hasProperty('ossrhUsername')) {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots') {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
pom.project {
groupId project.group
artifactId project.name
version project.version
name project.name
description description
packaging 'jar'
inceptionYear '2012'
url scmUrl
organization {
name 'xbib'
url 'http://xbib.org'
}
developers {
developer {
id user
name 'Jörg Prante'
email 'joergprante@gmail.com'
url 'https://github.com/jprante'
}
}
scm {
url scmUrl
connection scmConnection
developerConnection scmDeveloperConnection
}
licenses {
license {
name 'The Apache License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
}
}
}
}
}
nexusStaging { nexusStaging {
packageGroup = "org.xbib" packageGroup = "org.xbib"
} }

View file

@ -1,13 +1,16 @@
group = org.xbib group = org.xbib
name = netty-http-client name = netty-http-client
version = 4.1.16.1 version = 4.1.22.2
netty.version = 4.1.16.Final netty.version = 4.1.22.Final
tcnative.version = 2.0.7.Final tcnative.version = 2.0.7.Final
conscrypt.version = 1.0.1 conscrypt.version = 1.0.1
bouncycastle.version = 1.57
xbib-net-url.version = 1.1.0 xbib-net-url.version = 1.1.0
alpnagent.version = 2.0.7 alpnagent.version = 2.0.7
junit.version = 4.12 junit.version = 4.12
jackson.version = 2.8.11.1 jackson.version = 2.8.11.1
asciidoclet.version = 1.6.0.0 asciidoclet.version = 1.6.0.0
wagon.version = 3.0.0 wagon.version = 3.0.0

View file

@ -0,0 +1,10 @@
dependencies {
compile project(":netty-http-common")
compile "io.netty:netty-handler-proxy:${project.property('netty.version')}"
compile "io.netty:netty-transport-native-epoll:${project.property('netty.version')}"
testCompile "io.netty:netty-tcnative-boringssl-static:${project.property('tcnative.version')}"
testCompile "org.conscrypt:conscrypt-openjdk-uber:${project.property('conscrypt.version')}"
testCompile "junit:junit:${project.property('junit.version')}"
testCompile "com.fasterxml.jackson.core:jackson-databind:${project.property('jackson.version')}"
}

View file

@ -29,9 +29,10 @@ import org.xbib.netty.http.client.handler.http2.Http2ResponseHandler;
import org.xbib.netty.http.client.handler.http2.Http2SettingsHandler; import org.xbib.netty.http.client.handler.http2.Http2SettingsHandler;
import org.xbib.netty.http.client.pool.BoundedChannelPool; import org.xbib.netty.http.client.pool.BoundedChannelPool;
import org.xbib.netty.http.client.transport.Http2Transport; import org.xbib.netty.http.client.transport.Http2Transport;
import org.xbib.netty.http.client.transport.HttpTransport; import org.xbib.netty.http.client.transport.Http1Transport;
import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.client.transport.Transport;
import org.xbib.netty.http.client.util.NetworkUtils; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.common.NetworkUtils;
import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName; import javax.net.ssl.SNIServerName;
@ -213,13 +214,13 @@ public final class Client {
Transport transport = null; Transport transport = null;
if (httpAddress != null) { if (httpAddress != null) {
if (httpAddress.getVersion().majorVersion() == 1) { if (httpAddress.getVersion().majorVersion() == 1) {
transport = new HttpTransport(this, httpAddress); transport = new Http1Transport(this, httpAddress);
} else { } else {
transport = new Http2Transport(this, httpAddress); transport = new Http2Transport(this, httpAddress);
} }
} else if (hasPooledConnections()) { } else if (hasPooledConnections()) {
if (pool.getVersion().majorVersion() == 1) { if (pool.getVersion().majorVersion() == 1) {
transport = new HttpTransport(this, null); transport = new Http1Transport(this, null);
} else { } else {
transport = new Http2Transport(this, null); transport = new Http2Transport(this, null);
} }
@ -234,7 +235,7 @@ public final class Client {
} }
public Channel newChannel(HttpAddress httpAddress) throws IOException { public Channel newChannel(HttpAddress httpAddress) throws IOException {
Channel channel = null; Channel channel;
if (httpAddress != null) { if (httpAddress != null) {
HttpVersion httpVersion = httpAddress.getVersion(); HttpVersion httpVersion = httpAddress.getVersion();
ChannelInitializer<SocketChannel> initializer; ChannelInitializer<SocketChannel> initializer;
@ -285,14 +286,14 @@ public final class Client {
} }
public Transport execute(Request request) throws IOException { public Transport execute(Request request) throws IOException {
Transport transport = newTransport(HttpAddress.of(request)); Transport transport = newTransport(HttpAddress.of(request.url(), request.httpVersion()));
transport.execute(request); transport.execute(request);
return transport; return transport;
} }
public <T> CompletableFuture<T> execute(Request request, public <T> CompletableFuture<T> execute(Request request,
Function<FullHttpResponse, T> supplier) throws IOException { Function<FullHttpResponse, T> supplier) throws IOException {
return newTransport(HttpAddress.of(request)).execute(request, supplier); return newTransport(HttpAddress.of(request.url(), request.httpVersion())).execute(request, supplier);
} }
public Transport pooledExecute(Request request) throws IOException { public Transport pooledExecute(Request request) throws IOException {
@ -307,7 +308,7 @@ public final class Client {
* @param request the new request for continuing the request. * @param request the new request for continuing the request.
*/ */
public void continuation(Transport transport, Request request) throws IOException { public void continuation(Transport transport, Request request) throws IOException {
Transport nextTransport = newTransport(HttpAddress.of(request)); Transport nextTransport = newTransport(HttpAddress.of(request.url(), request.httpVersion()));
nextTransport.setCookieBox(transport.getCookieBox()); nextTransport.setCookieBox(transport.getCookieBox());
nextTransport.execute(request); nextTransport.execute(request);
nextTransport.get(); nextTransport.get();
@ -328,7 +329,7 @@ public final class Client {
} }
public Transport prepareRequest(Request request) { public Transport prepareRequest(Request request) {
return newTransport(HttpAddress.of(request)); return newTransport(HttpAddress.of(request.url(), request.httpVersion()));
} }
public void close(Transport transport) throws IOException { public void close(Transport transport) throws IOException {

View file

@ -4,12 +4,12 @@ import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.EventLoopGroup; import io.netty.channel.EventLoopGroup;
import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.WriteBufferWaterMark;
import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.proxy.HttpProxyHandler; import io.netty.handler.proxy.HttpProxyHandler;
import io.netty.handler.ssl.CipherSuiteFilter; import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.xbib.netty.http.common.HttpAddress;
import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.TrustManagerFactory;
import java.io.InputStream; import java.io.InputStream;

View file

@ -11,6 +11,7 @@ import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.SslProvider; import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.SupportedCipherSuiteFilter; import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import org.xbib.netty.http.client.retry.BackOff; import org.xbib.netty.http.client.retry.BackOff;
import org.xbib.netty.http.common.HttpAddress;
import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.TrustManagerFactory;
import java.io.InputStream; import java.io.InputStream;

View file

@ -3,9 +3,6 @@ package org.xbib.netty.http.client;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues; import io.netty.handler.codec.http.HttpHeaderValues;
@ -21,6 +18,7 @@ import org.xbib.net.QueryParameters;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.net.URLSyntaxException; import org.xbib.net.URLSyntaxException;
import org.xbib.netty.http.client.retry.BackOff; import org.xbib.netty.http.client.retry.BackOff;
import org.xbib.netty.http.common.HttpAddress;
import java.net.URI; import java.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;

View file

@ -9,7 +9,7 @@ import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandler;
import org.xbib.netty.http.client.ClientConfig; import org.xbib.netty.http.client.ClientConfig;
import org.xbib.netty.http.client.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;

View file

@ -0,0 +1,4 @@
/**
* HTTP handlers for Netty HTTP client.
*/
package org.xbib.netty.http.client.handler.http1;

View file

@ -9,11 +9,13 @@ import io.netty.handler.codec.http2.Http2ConnectionHandler;
import io.netty.handler.codec.http2.Http2FrameLogger; import io.netty.handler.codec.http2.Http2FrameLogger;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.ApplicationProtocolNames; import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.SslHandler; import io.netty.handler.ssl.SslHandler;
import org.xbib.netty.http.client.ClientConfig; import org.xbib.netty.http.client.ClientConfig;
import org.xbib.netty.http.client.HttpAddress; import org.xbib.netty.http.client.handler.http1.TrafficLoggingHandler;
import org.xbib.netty.http.common.HttpAddress;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
@ -51,13 +53,16 @@ public class Http2ChannelInitializer extends ChannelInitializer<SocketChannel> {
*/ */
@Override @Override
public void initChannel(SocketChannel channel) { public void initChannel(SocketChannel channel) {
if (clientConfig.isDebug()) {
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
}
if (httpAddress.isSecure()) { if (httpAddress.isSecure()) {
configureEncrypted(channel); configureEncrypted(channel);
} else { } else {
configureCleartext(channel); configureCleartext(channel);
} }
if (clientConfig.isDebug()) { if (clientConfig.isDebug()) {
logger.log(Level.FINE, "HTTP/2 channel initialized: " + channel.pipeline().names()); logger.log(Level.FINE, "HTTP/2 client channel initialized: " + channel.pipeline().names());
} }
} }
@ -96,7 +101,8 @@ public class Http2ChannelInitializer extends ChannelInitializer<SocketChannel> {
.propagateSettings(true) .propagateSettings(true)
.build())); .build()));
if (clientConfig.isDebug()) { if (clientConfig.isDebug()) {
http2ConnectionHandlerBuilder.frameLogger(new Http2FrameLogger(clientConfig.getDebugLogLevel(), "client")); Http2FrameLogger http2FrameLogger = new Http2FrameLogger(clientConfig.getDebugLogLevel(), "client");
http2ConnectionHandlerBuilder.frameLogger(http2FrameLogger);
} }
return http2ConnectionHandlerBuilder.build(); return http2ConnectionHandlerBuilder.build();
} }

View file

@ -0,0 +1,4 @@
/**
* HTTP/2 handlers for Netty HTTP client.
*/
package org.xbib.netty.http.client.handler.http2;

View file

@ -9,6 +9,7 @@ import io.netty.channel.pool.ChannelPoolHandler;
import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import io.netty.util.AttributeKey; import io.netty.util.AttributeKey;
import org.xbib.netty.http.common.PoolKey;
import java.net.ConnectException; import java.net.ConnectException;
import java.util.ArrayList; import java.util.ArrayList;
@ -145,11 +146,12 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
if (channel == null) { if (channel == null) {
semaphore.release(); semaphore.release();
throw new ConnectException(); throw new ConnectException();
} else {
if (channelPoolhandler != null) {
channelPoolhandler.channelAcquired(channel);
}
} }
} }
if (channelPoolhandler != null) {
channelPoolhandler.channelAcquired(channel);
}
return channel; return channel;
} }
@ -165,7 +167,7 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
} }
Channel channel; Channel channel;
for (int i = 0; i < availableCount; i ++) { for (int i = 0; i < availableCount; i ++) {
if (null == (channel = poll())) { if ((channel = poll()) == null) {
channel = newConnection(); channel = newConnection();
} }
if (channel == null) { if (channel == null) {
@ -193,6 +195,8 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
} }
} else if (channel.isOpen()) { } else if (channel.isOpen()) {
channel.close(); channel.close();
} else {
logger.log(Level.WARNING, "channel not active or open while release");
} }
if (channelPoolhandler != null) { if (channelPoolhandler != null) {
channelPoolhandler.channelReleased(channel); channelPoolhandler.channelReleased(channel);
@ -319,12 +323,23 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
Queue<Channel> channelQueue; Queue<Channel> channelQueue;
Channel channel; Channel channel;
for(int j = i; j < i + numberOfNodes; j ++) { for(int j = i; j < i + numberOfNodes; j ++) {
channelQueue = availableChannels.get(nodes.get(j % numberOfNodes)); K key = nodes.get(j % numberOfNodes);
// for HTTP/2, use channel list
logger.log(Level.FINE, "pool version = " + httpVersion);
if (httpVersion.majorVersion() == 2) {
List<Channel> list = channels.get(key);
if (!list.isEmpty()) {
logger.log(Level.INFO, "we have a channel " + list);
}
}
channelQueue = availableChannels.get(key);
if (channelQueue != null) { if (channelQueue != null) {
channel = channelQueue.poll(); channel = channelQueue.poll();
if (channel != null && channel.isActive()) { if (channel != null && channel.isActive()) {
return channel; return channel;
} }
} else {
logger.log(Level.FINE, "channelqueue is null");
} }
} }
return null; return null;

View file

@ -5,7 +5,7 @@ import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpMethod;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.client.RequestBuilder; import org.xbib.netty.http.client.RequestBuilder;
import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.client.transport.Transport;

View file

@ -15,7 +15,7 @@ import org.xbib.net.PercentDecoder;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.net.URLSyntaxException; import org.xbib.net.URLSyntaxException;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.client.RequestBuilder; import org.xbib.netty.http.client.RequestBuilder;
import org.xbib.netty.http.client.listener.CookieListener; import org.xbib.netty.http.client.listener.CookieListener;
@ -99,6 +99,7 @@ abstract class BaseTransport implements Transport {
if (streamId != null) { if (streamId != null) {
requests.put(streamId, request); requests.put(streamId, request);
} }
// flush after putting request into requests map
if (channel.isWritable()) { if (channel.isWritable()) {
channel.writeAndFlush(fullHttpRequest); channel.writeAndFlush(fullHttpRequest);

View file

@ -6,7 +6,7 @@ import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Settings;
import org.xbib.net.URLSyntaxException; import org.xbib.net.URLSyntaxException;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.client.listener.HttpResponseListener; import org.xbib.netty.http.client.listener.HttpResponseListener;
@ -21,15 +21,15 @@ import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level; import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
public class HttpTransport extends BaseTransport { public class Http1Transport extends BaseTransport {
private static final Logger logger = Logger.getLogger(HttpTransport.class.getName()); private static final Logger logger = Logger.getLogger(Http1Transport.class.getName());
private final AtomicInteger sequentialCounter; private final AtomicInteger sequentialCounter;
private SortedMap<Integer, CompletableFuture<Boolean>> sequentialPromiseMap; private SortedMap<Integer, CompletableFuture<Boolean>> sequentialPromiseMap;
public HttpTransport(Client client, HttpAddress httpAddress) { public Http1Transport(Client client, HttpAddress httpAddress) {
super(client, httpAddress); super(client, httpAddress);
this.sequentialCounter = new AtomicInteger(); this.sequentialCounter = new AtomicInteger();
this.sequentialPromiseMap = new ConcurrentSkipListMap<>(); this.sequentialPromiseMap = new ConcurrentSkipListMap<>();

View file

@ -6,7 +6,7 @@ import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.Http2Settings;
import org.xbib.net.URLSyntaxException; import org.xbib.net.URLSyntaxException;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.client.listener.HttpResponseListener; import org.xbib.netty.http.client.listener.HttpResponseListener;
@ -35,7 +35,7 @@ public class Http2Transport extends BaseTransport {
super(client, httpAddress); super(client, httpAddress);
streamIdCounter = new AtomicInteger(3); streamIdCounter = new AtomicInteger(3);
streamidPromiseMap = new ConcurrentSkipListMap<>(); streamidPromiseMap = new ConcurrentSkipListMap<>();
settingsPromise = (httpAddress != null && httpAddress.isSecure()) || settingsPromise = (httpAddress != null /*&& httpAddress.isSecure() */) ||
(client.hasPooledConnections() && client.getPool().isSecure()) ? (client.hasPooledConnections() && client.getPool().isSecure()) ?
new CompletableFuture<>() : null; new CompletableFuture<>() : null;
} }
@ -65,10 +65,16 @@ public class Http2Transport extends BaseTransport {
public void awaitSettings() { public void awaitSettings() {
if (settingsPromise != null) { if (settingsPromise != null) {
try { try {
logger.log(Level.FINE, "waiting for settings");
settingsPromise.get(client.getClientConfig().getReadTimeoutMillis(), TimeUnit.MILLISECONDS); settingsPromise.get(client.getClientConfig().getReadTimeoutMillis(), TimeUnit.MILLISECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) { } catch (TimeoutException e) {
logger.log(Level.WARNING, "settings timeout");
settingsPromise.completeExceptionally(e);
} catch (InterruptedException | ExecutionException e) {
settingsPromise.completeExceptionally(e); settingsPromise.completeExceptionally(e);
} }
} else {
logger.log(Level.WARNING, "settings promise is null");
} }
} }

View file

@ -3,7 +3,7 @@ package org.xbib.netty.http.client.test;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.Request;
import java.io.IOException; import java.io.IOException;

View file

@ -4,7 +4,7 @@ import io.netty.handler.codec.http.HttpVersion;
import org.junit.Test; import org.junit.Test;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.Request;
import java.io.IOException; import java.io.IOException;
@ -24,8 +24,8 @@ public class PooledClientTest extends LoggingBase {
public void testPooledClientWithSingleNode() throws IOException { public void testPooledClientWithSingleNode() throws IOException {
int loop = 10; int loop = 10;
int threads = Runtime.getRuntime().availableProcessors(); int threads = Runtime.getRuntime().availableProcessors();
URL url = URL.from("http://xbib.org"); URL url = URL.from("https://fl-test.hbz-nrw.de/app/fl");
HttpAddress httpAddress = HttpAddress.of(url, HttpVersion.HTTP_1_1); HttpAddress httpAddress = HttpAddress.of(url, HttpVersion.valueOf("HTTP/2.0"));
Client client = Client.builder() Client client = Client.builder()
.addPoolNode(httpAddress) .addPoolNode(httpAddress)
.setPoolNodeConnectionLimit(threads) .setPoolNodeConnectionLimit(threads)

View file

@ -18,7 +18,7 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Ignore; import org.junit.Ignore;
import org.junit.Test; import org.junit.Test;
import org.xbib.netty.http.client.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.pool.Pool; import org.xbib.netty.http.client.pool.Pool;
import org.xbib.netty.http.client.pool.BoundedChannelPool; import org.xbib.netty.http.client.pool.BoundedChannelPool;

View file

@ -16,7 +16,7 @@ import io.netty.handler.codec.http.HttpVersion;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.xbib.netty.http.client.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.pool.Pool; import org.xbib.netty.http.client.pool.Pool;
import org.xbib.netty.http.client.pool.BoundedChannelPool; import org.xbib.netty.http.client.pool.BoundedChannelPool;

View file

@ -7,7 +7,7 @@ import io.netty.util.AttributeKey;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.Parameterized; import org.junit.runners.Parameterized;
import org.xbib.netty.http.client.HttpAddress; import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.client.pool.BoundedChannelPool; import org.xbib.netty.http.client.pool.BoundedChannelPool;
import org.xbib.netty.http.client.pool.Pool; import org.xbib.netty.http.client.pool.Pool;

View file

@ -0,0 +1,5 @@
dependencies {
compile "org.xbib:net-url:${project.property('xbib-net-url.version')}"
compile "io.netty:netty-codec-http2:${project.property('netty.version')}"
}

View file

@ -1,8 +1,7 @@
package org.xbib.netty.http.client; package org.xbib.netty.http.common;
import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.HttpVersion;
import org.xbib.net.URL; import org.xbib.net.URL;
import org.xbib.netty.http.client.pool.PoolKey;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
@ -55,14 +54,14 @@ public class HttpAddress implements PoolKey {
return new HttpAddress(url, HTTP_2_0); return new HttpAddress(url, HTTP_2_0);
} }
public static HttpAddress of(Request request) {
return new HttpAddress(request.url(), request.httpVersion());
}
public static HttpAddress of(URL url, HttpVersion httpVersion) { public static HttpAddress of(URL url, HttpVersion httpVersion) {
return new HttpAddress(url, httpVersion); return new HttpAddress(url, httpVersion);
} }
public static HttpAddress of(String host, Integer port, HttpVersion version, boolean secure) {
return new HttpAddress(host, port, version, secure);
}
public HttpAddress(URL url, HttpVersion version) { public HttpAddress(URL url, HttpVersion version) {
this(url.getHost(), url.getPort(), version, "https".equals(url.getScheme())); this(url.getHost(), url.getPort(), version, "https".equals(url.getScheme()));
} }

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.client.util; package org.xbib.netty.http.common;
/** /**
* The network classes. * The network classes.

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.client.util; package org.xbib.netty.http.common;
/** /**
* The TCP/IP network protocol versions. * The TCP/IP network protocol versions.

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.client.util; package org.xbib.netty.http.common;
import java.io.IOException; import java.io.IOException;
import java.net.Inet4Address; import java.net.Inet4Address;

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.client.pool; package org.xbib.netty.http.common;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;

View file

@ -1,4 +1,4 @@
/** /**
* Utilities for Netty HTTP client. * Utilities for Netty HTTP client.
*/ */
package org.xbib.netty.http.client.util; package org.xbib.netty.http.common;

View file

@ -0,0 +1,9 @@
dependencies {
compile project(":netty-http-common")
compile "io.netty:netty-handler:${project.property('netty.version')}"
compile "io.netty:netty-transport-native-epoll:${project.property('netty.version')}"
compile "io.netty:netty-tcnative-boringssl-static:${project.property('tcnative.version')}"
compile "org.bouncycastle:bcpkix-jdk15on:${project.property('bouncycastle.version')}"
testCompile project(":netty-http-client")
testCompile "junit:junit:${project.property('junit.version')}"
}

View file

@ -0,0 +1,684 @@
/*! normalize.css v2.1.2 | MIT License | git.io/normalize */
/* ========================================================================== HTML5 display definitions ========================================================================== */
/** Correct `block` display not defined in IE 8/9. */
article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary { display: block; }
/** Correct `inline-block` display not defined in IE 8/9. */
audio, canvas, video { display: inline-block; }
/** Prevent modern browsers from displaying `audio` without controls. Remove excess height in iOS 5 devices. */
audio:not([controls]) { display: none; height: 0; }
/** Address `[hidden]` styling not present in IE 8/9. Hide the `template` element in IE, Safari, and Firefox < 22. */
[hidden], template { display: none; }
script { display: none !important; }
/* ========================================================================== Base ========================================================================== */
/** 1. Set default font family to sans-serif. 2. Prevent iOS text size adjust after orientation change, without disabling user zoom. */
html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ }
/** Remove default margin. */
body { margin: 0; }
/* ========================================================================== Links ========================================================================== */
/** Remove the gray background color from active links in IE 10. */
a { background: transparent; }
/** Address `outline` inconsistency between Chrome and other browsers. */
a:focus { outline: thin dotted; }
/** Improve readability when focused and also mouse hovered in all browsers. */
a:active, a:hover { outline: 0; }
/* ========================================================================== Typography ========================================================================== */
/** Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari 5, and Chrome. */
h1 { font-size: 2em; margin: 0.67em 0; }
/** Address styling not present in IE 8/9, Safari 5, and Chrome. */
abbr[title] { border-bottom: 1px dotted; }
/** Address style set to `bolder` in Firefox 4+, Safari 5, and Chrome. */
b, strong { font-weight: bold; }
/** Address styling not present in Safari 5 and Chrome. */
dfn { font-style: italic; }
/** Address differences between Firefox and other browsers. */
hr { -moz-box-sizing: content-box; box-sizing: content-box; height: 0; }
/** Address styling not present in IE 8/9. */
mark { background: #ff0; color: #000; }
/** Correct font family set oddly in Safari 5 and Chrome. */
code, kbd, pre, samp { font-family: monospace, serif; font-size: 1em; }
/** Improve readability of pre-formatted text in all browsers. */
pre { white-space: pre-wrap; }
/** Set consistent quote types. */
q { quotes: "\201C" "\201D" "\2018" "\2019"; }
/** Address inconsistent and variable font size in all browsers. */
small { font-size: 80%; }
/** Prevent `sub` and `sup` affecting `line-height` in all browsers. */
sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; }
sup { top: -0.5em; }
sub { bottom: -0.25em; }
/* ========================================================================== Embedded content ========================================================================== */
/** Remove border when inside `a` element in IE 8/9. */
img { border: 0; }
/** Correct overflow displayed oddly in IE 9. */
svg:not(:root) { overflow: hidden; }
/* ========================================================================== Figures ========================================================================== */
/** Address margin not present in IE 8/9 and Safari 5. */
figure { margin: 0; }
/* ========================================================================== Forms ========================================================================== */
/** Define consistent border, margin, and padding. */
fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; }
/** 1. Correct `color` not being inherited in IE 8/9. 2. Remove padding so people aren't caught out if they zero out fieldsets. */
legend { border: 0; /* 1 */ padding: 0; /* 2 */ }
/** 1. Correct font family not being inherited in all browsers. 2. Correct font size not being inherited in all browsers. 3. Address margins set differently in Firefox 4+, Safari 5, and Chrome. */
button, input, select, textarea { font-family: inherit; /* 1 */ font-size: 100%; /* 2 */ margin: 0; /* 3 */ }
/** Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet. */
button, input { line-height: normal; }
/** Address inconsistent `text-transform` inheritance for `button` and `select`. All other form control elements do not inherit `text-transform` values. Correct `button` style inheritance in Chrome, Safari 5+, and IE 8+. Correct `select` style inheritance in Firefox 4+ and Opera. */
button, select { text-transform: none; }
/** 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls. 2. Correct inability to style clickable `input` types in iOS. 3. Improve usability and consistency of cursor style between image-type `input` and others. */
button, html input[type="button"], input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ }
/** Re-set default cursor for disabled elements. */
button[disabled], html input[disabled] { cursor: default; }
/** 1. Address box sizing set to `content-box` in IE 8/9. 2. Remove excess padding in IE 8/9. */
input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ }
/** 1. Address `appearance` set to `searchfield` in Safari 5 and Chrome. 2. Address `box-sizing` set to `border-box` in Safari 5 and Chrome (include `-moz` to future-proof). */
input[type="search"] { -webkit-appearance: textfield; /* 1 */ -moz-box-sizing: content-box; -webkit-box-sizing: content-box; /* 2 */ box-sizing: content-box; }
/** Remove inner padding and search cancel button in Safari 5 and Chrome on OS X. */
input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; }
/** Remove inner padding and border in Firefox 4+. */
button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; }
/** 1. Remove default vertical scrollbar in IE 8/9. 2. Improve readability and alignment in all browsers. */
textarea { overflow: auto; /* 1 */ vertical-align: top; /* 2 */ }
/* ========================================================================== Tables ========================================================================== */
/** Remove most spacing between table cells. */
table { border-collapse: collapse; border-spacing: 0; }
meta.foundation-mq-small { font-family: "only screen and (min-width: 768px)"; width: 768px; }
meta.foundation-mq-medium { font-family: "only screen and (min-width:1280px)"; width: 1280px; }
meta.foundation-mq-large { font-family: "only screen and (min-width:1440px)"; width: 1440px; }
*, *:before, *:after { -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; }
html, body { font-size: 100%; }
body { background: white; color: #222222; padding: 0; margin: 0; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: normal; font-style: normal; line-height: 1; position: relative; cursor: auto; }
a:hover { cursor: pointer; }
img, object, embed { max-width: 100%; height: auto; }
object, embed { height: 100%; }
img { -ms-interpolation-mode: bicubic; }
#map_canvas img, #map_canvas embed, #map_canvas object, .map_canvas img, .map_canvas embed, .map_canvas object { max-width: none !important; }
.left { float: left !important; }
.right { float: right !important; }
.text-left { text-align: left !important; }
.text-right { text-align: right !important; }
.text-center { text-align: center !important; }
.text-justify { text-align: justify !important; }
.hide { display: none; }
.antialiased { -webkit-font-smoothing: antialiased; }
img { display: inline-block; vertical-align: middle; }
textarea { height: auto; min-height: 50px; }
select { width: 100%; }
object, svg { display: inline-block; vertical-align: middle; }
.center { margin-left: auto; margin-right: auto; }
.spread { width: 100%; }
p.lead, .paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { font-size: 1.21875em; line-height: 1.6; }
.subheader, .admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { line-height: 1.4; color: #6f6f6f; font-weight: 300; margin-top: 0.2em; margin-bottom: 0.5em; }
/* Typography resets */
div, dl, dt, dd, ul, ol, li, h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6, pre, form, p, blockquote, th, td { margin: 0; padding: 0; direction: ltr; }
/* Default Link Styles */
a { color: #2ba6cb; text-decoration: none; line-height: inherit; }
a:hover, a:focus { color: #2795b6; }
a img { border: none; }
/* Default paragraph styles */
p { font-family: inherit; font-weight: normal; font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; text-rendering: optimizeLegibility; }
p aside { font-size: 0.875em; line-height: 1.35; font-style: italic; }
/* Default header styles */
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; font-weight: bold; font-style: normal; color: #222222; text-rendering: optimizeLegibility; margin-top: 1em; margin-bottom: 0.5em; line-height: 1.2125em; }
h1 small, h2 small, h3 small, #toctitle small, .sidebarblock > .content > .title small, h4 small, h5 small, h6 small { font-size: 60%; color: #6f6f6f; line-height: 0; }
h1 { font-size: 2.125em; }
h2 { font-size: 1.6875em; }
h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.375em; }
h4 { font-size: 1.125em; }
h5 { font-size: 1.125em; }
h6 { font-size: 1em; }
hr { border: solid #dddddd; border-width: 1px 0 0; clear: both; margin: 1.25em 0 1.1875em; height: 0; }
/* Helpful Typography Defaults */
em, i { font-style: italic; line-height: inherit; }
strong, b { font-weight: bold; line-height: inherit; }
small { font-size: 60%; line-height: inherit; }
code { font-family: Consolas, "Liberation Mono", Courier, monospace; font-weight: bold; color: #7f0a0c; }
/* Lists */
ul, ol, dl { font-size: 1em; line-height: 1.6; margin-bottom: 1.25em; list-style-position: outside; font-family: inherit; }
ul, ol { margin-left: 1.5em; }
ul.no-bullet, ol.no-bullet { margin-left: 1.5em; }
/* Unordered Lists */
ul li ul, ul li ol { margin-left: 1.25em; margin-bottom: 0; font-size: 1em; /* Override nested font-size change */ }
ul.square li ul, ul.circle li ul, ul.disc li ul { list-style: inherit; }
ul.square { list-style-type: square; }
ul.circle { list-style-type: circle; }
ul.disc { list-style-type: disc; }
ul.no-bullet { list-style: none; }
/* Ordered Lists */
ol li ul, ol li ol { margin-left: 1.25em; margin-bottom: 0; }
/* Definition Lists */
dl dt { margin-bottom: 0.3125em; font-weight: bold; }
dl dd { margin-bottom: 1.25em; }
/* Abbreviations */
abbr, acronym { text-transform: uppercase; font-size: 90%; color: #222222; border-bottom: 1px dotted #dddddd; cursor: help; }
abbr { text-transform: none; }
/* Blockquotes */
blockquote { margin: 0 0 1.25em; padding: 0.5625em 1.25em 0 1.1875em; border-left: 1px solid #dddddd; }
blockquote cite { display: block; font-size: 0.8125em; color: #555555; }
blockquote cite:before { content: "\2014 \0020"; }
blockquote cite a, blockquote cite a:visited { color: #555555; }
blockquote, blockquote p { line-height: 1.6; color: #6f6f6f; }
/* Microformats */
.vcard { display: inline-block; margin: 0 0 1.25em 0; border: 1px solid #dddddd; padding: 0.625em 0.75em; }
.vcard li { margin: 0; display: block; }
.vcard .fn { font-weight: bold; font-size: 0.9375em; }
.vevent .summary { font-weight: bold; }
.vevent abbr { cursor: auto; text-decoration: none; font-weight: bold; border: none; padding: 0 0.0625em; }
@media only screen and (min-width: 768px) { h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; }
h1 { font-size: 2.75em; }
h2 { font-size: 2.3125em; }
h3, #toctitle, .sidebarblock > .content > .title { font-size: 1.6875em; }
h4 { font-size: 1.4375em; } }
/* Tables */
table { background: white; margin-bottom: 1.25em; border: solid 1px #dddddd; }
table thead, table tfoot { background: whitesmoke; font-weight: bold; }
table thead tr th, table thead tr td, table tfoot tr th, table tfoot tr td { padding: 0.5em 0.625em 0.625em; font-size: inherit; color: #222222; text-align: left; }
table tr th, table tr td { padding: 0.5625em 0.625em; font-size: inherit; color: #222222; }
table tr.even, table tr.alt, table tr:nth-of-type(even) { background: #f9f9f9; }
table thead tr th, table tfoot tr th, table tbody tr td, table tr td, table tfoot tr td { display: table-cell; line-height: 1.4; }
body { -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; tab-size: 4; }
h1, h2, h3, #toctitle, .sidebarblock > .content > .title, h4, h5, h6 { line-height: 1.4; }
.clearfix:before, .clearfix:after, .float-group:before, .float-group:after { content: " "; display: table; }
.clearfix:after, .float-group:after { clear: both; }
*:not(pre) > code { font-size: inherit; font-style: normal !important; letter-spacing: 0; padding: 0; line-height: inherit; word-wrap: break-word; }
*:not(pre) > code.nobreak { word-wrap: normal; }
*:not(pre) > code.nowrap { white-space: nowrap; }
pre, pre > code { line-height: 1.4; color: black; font-family: monospace, serif; font-weight: normal; }
em em { font-style: normal; }
strong strong { font-weight: normal; }
.keyseq { color: #555555; }
kbd { font-family: Consolas, "Liberation Mono", Courier, monospace; display: inline-block; color: #222222; font-size: 0.65em; line-height: 1.45; background-color: #f7f7f7; border: 1px solid #ccc; -webkit-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 0.1em white inset; margin: 0 0.15em; padding: 0.2em 0.5em; vertical-align: middle; position: relative; top: -0.1em; white-space: nowrap; }
.keyseq kbd:first-child { margin-left: 0; }
.keyseq kbd:last-child { margin-right: 0; }
.menuseq, .menu { color: #090909; }
b.button:before, b.button:after { position: relative; top: -1px; font-weight: normal; }
b.button:before { content: "["; padding: 0 3px 0 2px; }
b.button:after { content: "]"; padding: 0 2px 0 3px; }
#header, #content, #footnotes, #footer { width: 100%; margin-left: auto; margin-right: auto; margin-top: 0; margin-bottom: 0; max-width: 62.5em; *zoom: 1; position: relative; padding-left: 0.9375em; padding-right: 0.9375em; }
#header:before, #header:after, #content:before, #content:after, #footnotes:before, #footnotes:after, #footer:before, #footer:after { content: " "; display: table; }
#header:after, #content:after, #footnotes:after, #footer:after { clear: both; }
#content { margin-top: 1.25em; }
#content:before { content: none; }
#header > h1:first-child { color: black; margin-top: 2.25rem; margin-bottom: 0; }
#header > h1:first-child + #toc { margin-top: 8px; border-top: 1px solid #dddddd; }
#header > h1:only-child, body.toc2 #header > h1:nth-last-child(2) { border-bottom: 1px solid #dddddd; padding-bottom: 8px; }
#header .details { border-bottom: 1px solid #dddddd; line-height: 1.45; padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.25em; color: #555555; display: -ms-flexbox; display: -webkit-flex; display: flex; -ms-flex-flow: row wrap; -webkit-flex-flow: row wrap; flex-flow: row wrap; }
#header .details span:first-child { margin-left: -0.125em; }
#header .details span.email a { color: #6f6f6f; }
#header .details br { display: none; }
#header .details br + span:before { content: "\00a0\2013\00a0"; }
#header .details br + span.author:before { content: "\00a0\22c5\00a0"; color: #6f6f6f; }
#header .details br + span#revremark:before { content: "\00a0|\00a0"; }
#header #revnumber { text-transform: capitalize; }
#header #revnumber:after { content: "\00a0"; }
#content > h1:first-child:not([class]) { color: black; border-bottom: 1px solid #dddddd; padding-bottom: 8px; margin-top: 0; padding-top: 1rem; margin-bottom: 1.25rem; }
#toc { border-bottom: 1px solid #dddddd; padding-bottom: 0.5em; }
#toc > ul { margin-left: 0.125em; }
#toc ul.sectlevel0 > li > a { font-style: italic; }
#toc ul.sectlevel0 ul.sectlevel1 { margin: 0.5em 0; }
#toc ul { font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; list-style-type: none; }
#toc li { line-height: 1.3334; margin-top: 0.3334em; }
#toc a { text-decoration: none; }
#toc a:active { text-decoration: underline; }
#toctitle { color: #6f6f6f; font-size: 1.2em; }
@media only screen and (min-width: 768px) { #toctitle { font-size: 1.375em; }
body.toc2 { padding-left: 15em; padding-right: 0; }
#toc.toc2 { margin-top: 0 !important; background-color: #f2f2f2; position: fixed; width: 15em; left: 0; top: 0; border-right: 1px solid #dddddd; border-top-width: 0 !important; border-bottom-width: 0 !important; z-index: 1000; padding: 1.25em 1em; height: 100%; overflow: auto; }
#toc.toc2 #toctitle { margin-top: 0; margin-bottom: 0.8rem; font-size: 1.2em; }
#toc.toc2 > ul { font-size: 0.9em; margin-bottom: 0; }
#toc.toc2 ul ul { margin-left: 0; padding-left: 1em; }
#toc.toc2 ul.sectlevel0 ul.sectlevel1 { padding-left: 0; margin-top: 0.5em; margin-bottom: 0.5em; }
body.toc2.toc-right { padding-left: 0; padding-right: 15em; }
body.toc2.toc-right #toc.toc2 { border-right-width: 0; border-left: 1px solid #dddddd; left: auto; right: 0; } }
@media only screen and (min-width: 1280px) { body.toc2 { padding-left: 20em; padding-right: 0; }
#toc.toc2 { width: 20em; }
#toc.toc2 #toctitle { font-size: 1.375em; }
#toc.toc2 > ul { font-size: 0.95em; }
#toc.toc2 ul ul { padding-left: 1.25em; }
body.toc2.toc-right { padding-left: 0; padding-right: 20em; } }
#content #toc { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; }
#content #toc > :first-child { margin-top: 0; }
#content #toc > :last-child { margin-bottom: 0; }
#footer { max-width: 100%; background-color: #222222; padding: 1.25em; }
#footer-text { color: #dddddd; line-height: 1.44; }
.sect1 { padding-bottom: 0.625em; }
@media only screen and (min-width: 768px) { .sect1 { padding-bottom: 1.25em; } }
.sect1 + .sect1 { border-top: 1px solid #dddddd; }
#content h1 > a.anchor, h2 > a.anchor, h3 > a.anchor, #toctitle > a.anchor, .sidebarblock > .content > .title > a.anchor, h4 > a.anchor, h5 > a.anchor, h6 > a.anchor { position: absolute; z-index: 1001; width: 1.5ex; margin-left: -1.5ex; display: block; text-decoration: none !important; visibility: hidden; text-align: center; font-weight: normal; }
#content h1 > a.anchor:before, h2 > a.anchor:before, h3 > a.anchor:before, #toctitle > a.anchor:before, .sidebarblock > .content > .title > a.anchor:before, h4 > a.anchor:before, h5 > a.anchor:before, h6 > a.anchor:before { content: "\00A7"; font-size: 0.85em; display: block; padding-top: 0.1em; }
#content h1:hover > a.anchor, #content h1 > a.anchor:hover, h2:hover > a.anchor, h2 > a.anchor:hover, h3:hover > a.anchor, #toctitle:hover > a.anchor, .sidebarblock > .content > .title:hover > a.anchor, h3 > a.anchor:hover, #toctitle > a.anchor:hover, .sidebarblock > .content > .title > a.anchor:hover, h4:hover > a.anchor, h4 > a.anchor:hover, h5:hover > a.anchor, h5 > a.anchor:hover, h6:hover > a.anchor, h6 > a.anchor:hover { visibility: visible; }
#content h1 > a.link, h2 > a.link, h3 > a.link, #toctitle > a.link, .sidebarblock > .content > .title > a.link, h4 > a.link, h5 > a.link, h6 > a.link { color: #222222; text-decoration: none; }
#content h1 > a.link:hover, h2 > a.link:hover, h3 > a.link:hover, #toctitle > a.link:hover, .sidebarblock > .content > .title > a.link:hover, h4 > a.link:hover, h5 > a.link:hover, h6 > a.link:hover { color: #151515; }
.audioblock, .imageblock, .literalblock, .listingblock, .stemblock, .videoblock { margin-bottom: 1.25em; }
.admonitionblock td.content > .title, .audioblock > .title, .exampleblock > .title, .imageblock > .title, .listingblock > .title, .literalblock > .title, .stemblock > .title, .openblock > .title, .paragraph > .title, .quoteblock > .title, table.tableblock > .title, .verseblock > .title, .videoblock > .title, .dlist > .title, .olist > .title, .ulist > .title, .qlist > .title, .hdlist > .title { text-rendering: optimizeLegibility; text-align: left; }
table.tableblock > caption.title { white-space: nowrap; overflow: visible; max-width: 0; }
.paragraph.lead > p, #preamble > .sectionbody > .paragraph:first-of-type p { color: black; }
table.tableblock #preamble > .sectionbody > .paragraph:first-of-type p { font-size: inherit; }
.admonitionblock > table { border-collapse: separate; border: 0; background: none; width: 100%; }
.admonitionblock > table td.icon { text-align: center; width: 80px; }
.admonitionblock > table td.icon img { max-width: initial; }
.admonitionblock > table td.icon .title { font-weight: bold; font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif; text-transform: uppercase; }
.admonitionblock > table td.content { padding-left: 1.125em; padding-right: 1.25em; border-left: 1px solid #dddddd; color: #555555; }
.admonitionblock > table td.content > :last-child > :last-child { margin-bottom: 0; }
.exampleblock > .content { border-style: solid; border-width: 1px; border-color: #e6e6e6; margin-bottom: 1.25em; padding: 1.25em; background: white; -webkit-border-radius: 0; border-radius: 0; }
.exampleblock > .content > :first-child { margin-top: 0; }
.exampleblock > .content > :last-child { margin-bottom: 0; }
.sidebarblock { border-style: solid; border-width: 1px; border-color: #d9d9d9; margin-bottom: 1.25em; padding: 1.25em; background: #f2f2f2; -webkit-border-radius: 0; border-radius: 0; }
.sidebarblock > :first-child { margin-top: 0; }
.sidebarblock > :last-child { margin-bottom: 0; }
.sidebarblock > .content > .title { color: #6f6f6f; margin-top: 0; }
.exampleblock > .content > :last-child > :last-child, .exampleblock > .content .olist > ol > li:last-child > :last-child, .exampleblock > .content .ulist > ul > li:last-child > :last-child, .exampleblock > .content .qlist > ol > li:last-child > :last-child, .sidebarblock > .content > :last-child > :last-child, .sidebarblock > .content .olist > ol > li:last-child > :last-child, .sidebarblock > .content .ulist > ul > li:last-child > :last-child, .sidebarblock > .content .qlist > ol > li:last-child > :last-child { margin-bottom: 0; }
.literalblock pre, .listingblock pre:not(.highlight), .listingblock pre[class="highlight"], .listingblock pre[class^="highlight "], .listingblock pre.CodeRay, .listingblock pre.prettyprint { background: #eeeeee; }
.sidebarblock .literalblock pre, .sidebarblock .listingblock pre:not(.highlight), .sidebarblock .listingblock pre[class="highlight"], .sidebarblock .listingblock pre[class^="highlight "], .sidebarblock .listingblock pre.CodeRay, .sidebarblock .listingblock pre.prettyprint { background: #f2f1f1; }
.literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { border: 1px solid #cccccc; -webkit-border-radius: 0; border-radius: 0; word-wrap: break-word; padding: 0.8em 0.8em 0.65em 0.8em; font-size: 0.8125em; }
.literalblock pre.nowrap, .literalblock pre[class].nowrap, .listingblock pre.nowrap, .listingblock pre[class].nowrap { overflow-x: auto; white-space: pre; word-wrap: normal; }
@media only screen and (min-width: 768px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 0.90625em; } }
@media only screen and (min-width: 1280px) { .literalblock pre, .literalblock pre[class], .listingblock pre, .listingblock pre[class] { font-size: 1em; } }
.literalblock.output pre { color: #eeeeee; background-color: black; }
.listingblock pre.highlightjs { padding: 0; }
.listingblock pre.highlightjs > code { padding: 0.8em 0.8em 0.65em 0.8em; -webkit-border-radius: 0; border-radius: 0; }
.listingblock > .content { position: relative; }
.listingblock code[data-lang]:before { display: none; content: attr(data-lang); position: absolute; font-size: 0.75em; top: 0.425rem; right: 0.5rem; line-height: 1; text-transform: uppercase; color: #999; }
.listingblock:hover code[data-lang]:before { display: block; }
.listingblock.terminal pre .command:before { content: attr(data-prompt); padding-right: 0.5em; color: #999; }
.listingblock.terminal pre .command:not([data-prompt]):before { content: "$"; }
table.pyhltable { border-collapse: separate; border: 0; margin-bottom: 0; background: none; }
table.pyhltable td { vertical-align: top; padding-top: 0; padding-bottom: 0; line-height: 1.4; }
table.pyhltable td.code { padding-left: .75em; padding-right: 0; }
pre.pygments .lineno, table.pyhltable td:not(.code) { color: #999; padding-left: 0; padding-right: .5em; border-right: 1px solid #dddddd; }
pre.pygments .lineno { display: inline-block; margin-right: .25em; }
table.pyhltable .linenodiv { background: none !important; padding-right: 0 !important; }
.quoteblock { margin: 0 1em 1.25em 1.5em; display: table; }
.quoteblock > .title { margin-left: -1.5em; margin-bottom: 0.75em; }
.quoteblock blockquote, .quoteblock blockquote p { color: #6f6f6f; font-size: 1.15rem; line-height: 1.75; word-spacing: 0.1em; letter-spacing: 0; font-style: italic; text-align: justify; }
.quoteblock blockquote { margin: 0; padding: 0; border: 0; }
.quoteblock blockquote:before { content: "\201c"; float: left; font-size: 2.75em; font-weight: bold; line-height: 0.6em; margin-left: -0.6em; color: #6f6f6f; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); }
.quoteblock blockquote > .paragraph:last-child p { margin-bottom: 0; }
.quoteblock .attribution { margin-top: 0.5em; margin-right: 0.5ex; text-align: right; }
.quoteblock .quoteblock { margin-left: 0; margin-right: 0; padding: 0.5em 0; border-left: 3px solid #555555; }
.quoteblock .quoteblock blockquote { padding: 0 0 0 0.75em; }
.quoteblock .quoteblock blockquote:before { display: none; }
.verseblock { margin: 0 1em 1.25em 1em; }
.verseblock pre { font-family: "Open Sans", "DejaVu Sans", sans; font-size: 1.15rem; color: #6f6f6f; font-weight: 300; text-rendering: optimizeLegibility; }
.verseblock pre strong { font-weight: 400; }
.verseblock .attribution { margin-top: 1.25rem; margin-left: 0.5ex; }
.quoteblock .attribution, .verseblock .attribution { font-size: 0.8125em; line-height: 1.45; font-style: italic; }
.quoteblock .attribution br, .verseblock .attribution br { display: none; }
.quoteblock .attribution cite, .verseblock .attribution cite { display: block; letter-spacing: -0.025em; color: #555555; }
.quoteblock.abstract { margin: 0 0 1.25em 0; display: block; }
.quoteblock.abstract blockquote, .quoteblock.abstract blockquote p { text-align: left; word-spacing: 0; }
.quoteblock.abstract blockquote:before, .quoteblock.abstract blockquote p:first-of-type:before { display: none; }
table.tableblock { max-width: 100%; border-collapse: separate; }
table.tableblock td > .paragraph:last-child p > p:last-child, table.tableblock th > p:last-child, table.tableblock td > p:last-child { margin-bottom: 0; }
table.tableblock, th.tableblock, td.tableblock { border: 0 solid #dddddd; }
table.grid-all th.tableblock, table.grid-all td.tableblock { border-width: 0 1px 1px 0; }
table.grid-all tfoot > tr > th.tableblock, table.grid-all tfoot > tr > td.tableblock { border-width: 1px 1px 0 0; }
table.grid-cols th.tableblock, table.grid-cols td.tableblock { border-width: 0 1px 0 0; }
table.grid-all * > tr > .tableblock:last-child, table.grid-cols * > tr > .tableblock:last-child { border-right-width: 0; }
table.grid-rows th.tableblock, table.grid-rows td.tableblock { border-width: 0 0 1px 0; }
table.grid-all tbody > tr:last-child > th.tableblock, table.grid-all tbody > tr:last-child > td.tableblock, table.grid-all thead:last-child > tr > th.tableblock, table.grid-rows tbody > tr:last-child > th.tableblock, table.grid-rows tbody > tr:last-child > td.tableblock, table.grid-rows thead:last-child > tr > th.tableblock { border-bottom-width: 0; }
table.grid-rows tfoot > tr > th.tableblock, table.grid-rows tfoot > tr > td.tableblock { border-width: 1px 0 0 0; }
table.frame-all { border-width: 1px; }
table.frame-sides { border-width: 0 1px; }
table.frame-topbot { border-width: 1px 0; }
th.halign-left, td.halign-left { text-align: left; }
th.halign-right, td.halign-right { text-align: right; }
th.halign-center, td.halign-center { text-align: center; }
th.valign-top, td.valign-top { vertical-align: top; }
th.valign-bottom, td.valign-bottom { vertical-align: bottom; }
th.valign-middle, td.valign-middle { vertical-align: middle; }
table thead th, table tfoot th { font-weight: bold; }
tbody tr th { display: table-cell; line-height: 1.4; background: whitesmoke; }
tbody tr th, tbody tr th p, tfoot tr th, tfoot tr th p { color: #222222; font-weight: bold; }
p.tableblock > code:only-child { background: none; padding: 0; }
p.tableblock { font-size: 1em; }
td > div.verse { white-space: pre; }
ol { margin-left: 1.75em; }
ul li ol { margin-left: 1.5em; }
dl dd { margin-left: 1.125em; }
dl dd:last-child, dl dd:last-child > :last-child { margin-bottom: 0; }
ol > li p, ul > li p, ul dd, ol dd, .olist .olist, .ulist .ulist, .ulist .olist, .olist .ulist { margin-bottom: 0.625em; }
ul.unstyled, ol.unnumbered, ul.checklist, ul.none { list-style-type: none; }
ul.unstyled, ol.unnumbered, ul.checklist { margin-left: 0.625em; }
ul.checklist li > p:first-child > .fa-square-o:first-child, ul.checklist li > p:first-child > .fa-check-square-o:first-child { width: 1em; font-size: 0.85em; }
ul.checklist li > p:first-child > input[type="checkbox"]:first-child { width: 1em; position: relative; top: 1px; }
ul.inline { margin: 0 auto 0.625em auto; margin-left: -1.375em; margin-right: 0; padding: 0; list-style: none; overflow: hidden; }
ul.inline > li { list-style: none; float: left; margin-left: 1.375em; display: block; }
ul.inline > li > * { display: block; }
.unstyled dl dt { font-weight: normal; font-style: normal; }
ol.arabic { list-style-type: decimal; }
ol.decimal { list-style-type: decimal-leading-zero; }
ol.loweralpha { list-style-type: lower-alpha; }
ol.upperalpha { list-style-type: upper-alpha; }
ol.lowerroman { list-style-type: lower-roman; }
ol.upperroman { list-style-type: upper-roman; }
ol.lowergreek { list-style-type: lower-greek; }
.hdlist > table, .colist > table { border: 0; background: none; }
.hdlist > table > tbody > tr, .colist > table > tbody > tr { background: none; }
td.hdlist1, td.hdlist2 { vertical-align: top; padding: 0 0.625em; }
td.hdlist1 { font-weight: bold; padding-bottom: 1.25em; }
.literalblock + .colist, .listingblock + .colist { margin-top: -0.5em; }
.colist > table tr > td:first-of-type { padding: 0 0.75em; line-height: 1; }
.colist > table tr > td:first-of-type img { max-width: initial; }
.colist > table tr > td:last-of-type { padding: 0.25em 0; }
.thumb, .th { line-height: 0; display: inline-block; border: solid 4px white; -webkit-box-shadow: 0 0 0 1px #dddddd; box-shadow: 0 0 0 1px #dddddd; }
.imageblock.left, .imageblock[style*="float: left"] { margin: 0.25em 0.625em 1.25em 0; }
.imageblock.right, .imageblock[style*="float: right"] { margin: 0.25em 0 1.25em 0.625em; }
.imageblock > .title { margin-bottom: 0; }
.imageblock.thumb, .imageblock.th { border-width: 6px; }
.imageblock.thumb > .title, .imageblock.th > .title { padding: 0 0.125em; }
.image.left, .image.right { margin-top: 0.25em; margin-bottom: 0.25em; display: inline-block; line-height: 0; }
.image.left { margin-right: 0.625em; }
.image.right { margin-left: 0.625em; }
a.image { text-decoration: none; display: inline-block; }
a.image object { pointer-events: none; }
sup.footnote, sup.footnoteref { font-size: 0.875em; position: static; vertical-align: super; }
sup.footnote a, sup.footnoteref a { text-decoration: none; }
sup.footnote a:active, sup.footnoteref a:active { text-decoration: underline; }
#footnotes { padding-top: 0.75em; padding-bottom: 0.75em; margin-bottom: 0.625em; }
#footnotes hr { width: 20%; min-width: 6.25em; margin: -0.25em 0 0.75em 0; border-width: 1px 0 0 0; }
#footnotes .footnote { padding: 0 0.375em 0 0.225em; line-height: 1.3334; font-size: 0.875em; margin-left: 1.2em; text-indent: -1.05em; margin-bottom: 0.2em; }
#footnotes .footnote a:first-of-type { font-weight: bold; text-decoration: none; }
#footnotes .footnote:last-of-type { margin-bottom: 0; }
#content #footnotes { margin-top: -0.625em; margin-bottom: 0; padding: 0.75em 0; }
.gist .file-data > table { border: 0; background: #fff; width: 100%; margin-bottom: 0; }
.gist .file-data > table td.line-data { width: 99%; }
div.unbreakable { page-break-inside: avoid; }
.big { font-size: larger; }
.small { font-size: smaller; }
.underline { text-decoration: underline; }
.overline { text-decoration: overline; }
.line-through { text-decoration: line-through; }
.aqua { color: #00bfbf; }
.aqua-background { background-color: #00fafa; }
.black { color: black; }
.black-background { background-color: black; }
.blue { color: #0000bf; }
.blue-background { background-color: #0000fa; }
.fuchsia { color: #bf00bf; }
.fuchsia-background { background-color: #fa00fa; }
.gray { color: #606060; }
.gray-background { background-color: #7d7d7d; }
.green { color: #006000; }
.green-background { background-color: #007d00; }
.lime { color: #00bf00; }
.lime-background { background-color: #00fa00; }
.maroon { color: #600000; }
.maroon-background { background-color: #7d0000; }
.navy { color: #000060; }
.navy-background { background-color: #00007d; }
.olive { color: #606000; }
.olive-background { background-color: #7d7d00; }
.purple { color: #600060; }
.purple-background { background-color: #7d007d; }
.red { color: #bf0000; }
.red-background { background-color: #fa0000; }
.silver { color: #909090; }
.silver-background { background-color: #bcbcbc; }
.teal { color: #006060; }
.teal-background { background-color: #007d7d; }
.white { color: #bfbfbf; }
.white-background { background-color: #fafafa; }
.yellow { color: #bfbf00; }
.yellow-background { background-color: #fafa00; }
span.icon > .fa { cursor: default; }
.admonitionblock td.icon [class^="fa icon-"] { font-size: 2.5em; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); cursor: default; }
.admonitionblock td.icon .icon-note:before { content: "\f05a"; color: #207c98; }
.admonitionblock td.icon .icon-tip:before { content: "\f0eb"; text-shadow: 1px 1px 2px rgba(155, 155, 0, 0.8); color: #111; }
.admonitionblock td.icon .icon-warning:before { content: "\f071"; color: #bf6900; }
.admonitionblock td.icon .icon-caution:before { content: "\f06d"; color: #bf3400; }
.admonitionblock td.icon .icon-important:before { content: "\f06a"; color: #bf0000; }
.conum[data-value] { display: inline-block; color: #fff !important; background-color: #222222; -webkit-border-radius: 100px; border-radius: 100px; text-align: center; font-size: 0.75em; width: 1.67em; height: 1.67em; line-height: 1.67em; font-family: "Open Sans", "DejaVu Sans", sans-serif; font-style: normal; font-weight: bold; }
.conum[data-value] * { color: #fff !important; }
.conum[data-value] + b { display: none; }
.conum[data-value]:after { content: attr(data-value); }
pre .conum[data-value] { position: relative; top: -0.125em; }
b.conum * { color: inherit !important; }
.conum:not([data-value]):empty { display: none; }
.literalblock pre, .listingblock pre { background: #eeeeee; }

View file

@ -0,0 +1,10 @@
= Netty HTTP server
Jörg Prante
:sectnums:
:toc: preamble
:toclevels: 4
:!toc-title: Content
:experimental:
:description: Netty HTTP server for Java
:keywords: Java, Netty, HTTP, server
:icons: font

View file

@ -0,0 +1,3 @@
= Netty HTTP server
Jörg Prante

View file

@ -0,0 +1,296 @@
package org.xbib.netty.http.server;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.util.DomainNameMapping;
import io.netty.util.DomainNameMappingBuilder;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.context.VirtualServer;
import org.xbib.netty.http.server.handler.http1.HttpChannelInitializer;
import org.xbib.netty.http.server.handler.http2.Http2ChannelInitializer;
import org.xbib.netty.http.server.transport.Http1ServerTransport;
import org.xbib.netty.http.server.transport.Http2ServerTransport;
import org.xbib.netty.http.server.transport.ServerTransport;
import org.xbib.netty.http.server.util.NetworkUtils;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManagerFactory;
import java.io.IOException;
import java.security.KeyStoreException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* HTTP server.
*/
public final class Server {
private static final Logger logger = Logger.getLogger(Server.class.getName());
static {
// extend Java system properties by detected network interfaces
//NetworkUtils.extendSystemProperties();
// change Netty defaults to safer ones, but still allow override from arg line
if (System.getProperty("io.netty.noUnsafe") == null) {
System.setProperty("io.netty.noUnsafe", Boolean.toString(true));
}
if (System.getProperty("io.netty.noKeySetOptimization") == null) {
System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true));
}
if (System.getProperty("io.netty.recycler.maxCapacity") == null) {
System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0));
}
//if (System.getProperty("io.netty.leakDetection.level") == null) {
// System.setProperty("io.netty.leakDetection.level", "paranoid");
//}
}
private final ServerConfig serverConfig;
private final ByteBufAllocator byteBufAllocator;
private final EventLoopGroup parentEventLoopGroup;
private final EventLoopGroup childEventLoopGroup;
private final Class<? extends ServerSocketChannel> socketChannelClass;
private final ServerBootstrap bootstrap;
private final Map<String, VirtualServer> virtualServerMap;
private ChannelFuture channelFuture;
/**
* Create a new HTTP server. Use {@link #builder()} to build HTTP client instance.
*/
public Server(ServerConfig serverConfig,
ByteBufAllocator byteBufAllocator,
EventLoopGroup parentEventLoopGroup,
EventLoopGroup childEventLoopGroup,
Class<? extends ServerSocketChannel> socketChannelClass) throws SSLException {
Objects.requireNonNull(serverConfig);
this.serverConfig = serverConfig;
initializeTrustManagerFactory(serverConfig);
this.byteBufAllocator = byteBufAllocator != null ?
byteBufAllocator : ByteBufAllocator.DEFAULT;
this.parentEventLoopGroup = createParentEventLoopGroup(serverConfig, parentEventLoopGroup);
this.childEventLoopGroup = createChildEventLoopGroup(serverConfig, childEventLoopGroup);
this.socketChannelClass = createSocketChannelClass(serverConfig, socketChannelClass);
this.bootstrap = new ServerBootstrap()
.group(this.parentEventLoopGroup, this.childEventLoopGroup)
.channel(this.socketChannelClass)
.option(ChannelOption.ALLOCATOR, this.byteBufAllocator)
.option(ChannelOption.SO_REUSEADDR, serverConfig.isReuseAddr())
.option(ChannelOption.SO_RCVBUF, serverConfig.getTcpReceiveBufferSize())
.option(ChannelOption.SO_BACKLOG, serverConfig.getBackLogSize())
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, serverConfig.getConnectTimeoutMillis())
.childOption(ChannelOption.ALLOCATOR, this.byteBufAllocator)
.childOption(ChannelOption.SO_REUSEADDR, serverConfig.isReuseAddr())
.childOption(ChannelOption.TCP_NODELAY, serverConfig.isTcpNodelay())
.childOption(ChannelOption.SO_SNDBUF, serverConfig.getTcpSendBufferSize())
.childOption(ChannelOption.SO_RCVBUF, serverConfig.getTcpReceiveBufferSize())
.childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, serverConfig.getConnectTimeoutMillis())
.childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, serverConfig.getWriteBufferWaterMark());
if (serverConfig.isDebug()) {
bootstrap.handler(new LoggingHandler("bootstrap-server", serverConfig.getDebugLogLevel()));
}
this.virtualServerMap = new HashMap<>();
for (VirtualServer virtualServer : serverConfig.getVirtualServers()) {
String name = virtualServer.getName();
virtualServerMap.put(name, virtualServer);
for (String alias : virtualServer.getAliases()) {
virtualServerMap.put(alias, virtualServer);
}
}
DomainNameMapping<SslContext> domainNameMapping = null;
if (serverConfig.getAddress().isSecure()) {
SslContextBuilder sslContextBuilder = SslContextBuilder.forServer(serverConfig.getKeyCertChainInputStream(),
serverConfig.getKeyInputStream(), serverConfig.getKeyPassword())
.sslProvider(serverConfig.getSslProvider())
.ciphers(serverConfig.getCiphers(), serverConfig.getCipherSuiteFilter());
if (serverConfig.getAddress().getVersion().majorVersion() == 2) {
sslContextBuilder.applicationProtocolConfig(newApplicationProtocolConfig());
}
SslContext sslContext = sslContextBuilder.build();
DomainNameMappingBuilder<SslContext> mappingBuilder = new DomainNameMappingBuilder<>(sslContext);
for (VirtualServer virtualServer : serverConfig.getVirtualServers()) {
String name = virtualServer.getName();
mappingBuilder.add( name == null ? "*" : name, sslContext);
}
domainNameMapping = mappingBuilder.build();
}
HttpAddress httpAddress = serverConfig.getAddress();
if (httpAddress.getVersion().majorVersion() == 1) {
HttpChannelInitializer httpChannelInitializer = new HttpChannelInitializer(this,
httpAddress, domainNameMapping);
bootstrap.childHandler(httpChannelInitializer);
} else {
Http2ChannelInitializer initializer = new Http2ChannelInitializer(this,
httpAddress, domainNameMapping);
bootstrap.childHandler(initializer);
}
}
public static ServerBuilder builder() {
return new ServerBuilder();
}
public ServerConfig getServerConfig() {
return serverConfig;
}
/**
* Returns the virtual host with the given name.
*
* @param name the name of the virtual host to return, or null for
* the default virtual host
* @return the virtual host with the given name, or null if it doesn't exist
*/
public VirtualServer getVirtualServer(String name) {
return virtualServerMap.get(name);
}
public VirtualServer getDefaultVirtualServer() {
return virtualServerMap.get(null);
}
/**
* Start accepting incoming connections.
*/
public ChannelFuture accept() {
logger.log(Level.INFO, () -> "trying to bind to " + serverConfig.getAddress());
this.channelFuture = bootstrap.bind(serverConfig.getAddress().getInetSocketAddress());
logger.log(Level.INFO, () -> ServerName.getServerName() + " ready, listening on " + serverConfig.getAddress());
return channelFuture;
}
public void logDiagnostics(Level level) {
logger.log(level, () -> "OpenSSL available: " + OpenSsl.isAvailable() +
" OpenSSL ALPN support: " + OpenSsl.isAlpnSupported() +
" Local host name: " + NetworkUtils.getLocalHostName("localhost") +
" parent event loop group: " + parentEventLoopGroup +
" child event loop group: " + childEventLoopGroup +
" socket: " + socketChannelClass.getName() +
" allocator: " + byteBufAllocator.getClass().getName());
logger.log(level, NetworkUtils::displayNetworkInterfaces);
}
public ServerTransport newTransport(HttpVersion httpVersion) {
return httpVersion.majorVersion() == 1 ? new Http1ServerTransport(this) : new Http2ServerTransport(this);
}
public synchronized void shutdownGracefully() throws IOException {
// first, shut down threads, then server socket
childEventLoopGroup.shutdownGracefully();
parentEventLoopGroup.shutdownGracefully();
try {
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
throw new IOException(e);
}
}
private static EventLoopGroup createParentEventLoopGroup(ServerConfig serverConfig,
EventLoopGroup parentEventLoopGroup ) {
EventLoopGroup eventLoopGroup = parentEventLoopGroup;
if (eventLoopGroup == null) {
eventLoopGroup = serverConfig.isEpoll() ?
new EpollEventLoopGroup(serverConfig.getParentThreadCount(), new HttpServerParentThreadFactory()) :
new NioEventLoopGroup(serverConfig.getParentThreadCount(), new HttpServerParentThreadFactory());
}
return eventLoopGroup;
}
private static EventLoopGroup createChildEventLoopGroup(ServerConfig serverConfig,
EventLoopGroup childEventLoopGroup ) {
EventLoopGroup eventLoopGroup = childEventLoopGroup;
if (eventLoopGroup == null) {
eventLoopGroup = serverConfig.isEpoll() ?
new EpollEventLoopGroup(serverConfig.getChildThreadCount(), new HttpServerChildThreadFactory()) :
new NioEventLoopGroup(serverConfig.getChildThreadCount(), new HttpServerChildThreadFactory());
}
return eventLoopGroup;
}
private static Class<? extends ServerSocketChannel> createSocketChannelClass(ServerConfig serverConfig,
Class<? extends ServerSocketChannel> socketChannelClass) {
Class<? extends ServerSocketChannel> channelClass = socketChannelClass;
if (channelClass == null) {
if (serverConfig.isEpoll() && Epoll.isAvailable()) {
channelClass = EpollServerSocketChannel.class;
} else {
channelClass = NioServerSocketChannel.class;
}
}
return channelClass;
}
/**
* Initialize trust manager factory once per server lifecycle.
* @param serverConfig the server config
*/
private static void initializeTrustManagerFactory(ServerConfig serverConfig) {
TrustManagerFactory trustManagerFactory = serverConfig.getTrustManagerFactory();
if (trustManagerFactory != null) {
try {
trustManagerFactory.init(serverConfig.getTrustManagerKeyStore());
} catch (KeyStoreException e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
}
}
private static ApplicationProtocolConfig newApplicationProtocolConfig() {
return new ApplicationProtocolConfig(ApplicationProtocolConfig.Protocol.ALPN,
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE,
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT,
ApplicationProtocolNames.HTTP_2,
ApplicationProtocolNames.HTTP_1_1);
}
static class HttpServerParentThreadFactory implements ThreadFactory {
private int number = 0;
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable, "org-xbib-netty-http-server-parent-" + (number++));
thread.setDaemon(true);
return thread;
}
}
static class HttpServerChildThreadFactory implements ThreadFactory {
private int number = 0;
@Override
public Thread newThread(Runnable runnable) {
Thread thread = new Thread(runnable, "org-xbib-netty-http-server-child-" + (number++));
thread.setDaemon(true);
return thread;
}
}
}

View file

@ -0,0 +1,235 @@
package org.xbib.netty.http.server;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.WriteBufferWaterMark;
import io.netty.channel.socket.ServerSocketChannel;
import io.netty.handler.ssl.CipherSuiteFilter;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.context.VirtualServer;
import org.xbib.netty.http.server.security.tls.SelfSignedCertificate;
import javax.net.ssl.SSLException;
import javax.net.ssl.TrustManagerFactory;
import java.io.InputStream;
/**
* HTTP server builder.
*/
public class ServerBuilder {
private ByteBufAllocator byteBufAllocator;
private EventLoopGroup parentEventLoopGroup;
private EventLoopGroup childEventLoopGroup;
private Class<? extends ServerSocketChannel> socketChannelClass;
private ServerConfig serverConfig;
public ServerBuilder() {
this.serverConfig = new ServerConfig();
}
public ServerBuilder enableDebug() {
this.serverConfig.enableDebug();
return this;
}
public ServerBuilder bind(HttpAddress httpAddress) {
this.serverConfig.setAddress(httpAddress);
return this;
}
public ServerBuilder host(String bindhost, int bindPort) {
this.serverConfig.setAddress(HttpAddress.http2(bindhost, bindPort));
return this;
}
public ServerBuilder port(int bindPort) {
this.serverConfig.setAddress(HttpAddress.http2(null, bindPort));
return this;
}
public ServerBuilder setByteBufAllocator(ByteBufAllocator byteBufAllocator) {
this.byteBufAllocator = byteBufAllocator;
return this;
}
public ServerBuilder setParentEventLoopGroup(EventLoopGroup parentEventLoopGroup) {
this.parentEventLoopGroup = parentEventLoopGroup;
return this;
}
public ServerBuilder setChildEventLoopGroup(EventLoopGroup childEventLoopGroup) {
this.childEventLoopGroup = childEventLoopGroup;
return this;
}
public ServerBuilder setChannelClass(Class<? extends ServerSocketChannel> socketChannelClass) {
this.socketChannelClass = socketChannelClass;
return this;
}
public ServerBuilder setUseEpoll(boolean useEpoll) {
this.serverConfig.setEpoll(useEpoll);
return this;
}
public ServerBuilder setConnectTimeoutMillis(int connectTimeoutMillis) {
this.serverConfig.setConnectTimeoutMillis(connectTimeoutMillis);
return this;
}
public ServerBuilder setParentThreadCount(int parentThreadCount) {
this.serverConfig.setParentThreadCount(parentThreadCount);
return this;
}
public ServerBuilder setChildThreadCount(int childThreadCount) {
this.serverConfig.setChildThreadCount(childThreadCount);
return this;
}
public ServerBuilder setTcpSendBufferSize(int tcpSendBufferSize) {
this.serverConfig.setTcpSendBufferSize(tcpSendBufferSize);
return this;
}
public ServerBuilder setTcpReceiveBufferSize(int tcpReceiveBufferSize) {
this.serverConfig.setTcpReceiveBufferSize(tcpReceiveBufferSize);
return this;
}
public ServerBuilder setTcpNoDelay(boolean tcpNoDelay) {
this.serverConfig.setTcpNodelay(tcpNoDelay);
return this;
}
public ServerBuilder setReuseAddr(boolean reuseAddr) {
this.serverConfig.setReuseAddr(reuseAddr);
return this;
}
public ServerBuilder setBacklogSize(int backlogSize) {
this.serverConfig.setBackLogSize(backlogSize);
return this;
}
public ServerBuilder setMaxChunkSize(int maxChunkSize) {
this.serverConfig.setMaxChunkSize(maxChunkSize);
return this;
}
public ServerBuilder setMaxInitialLineLength(int maxInitialLineLength) {
this.serverConfig.setMaxInitialLineLength(maxInitialLineLength);
return this;
}
public ServerBuilder setMaxHeadersSize(int maxHeadersSize) {
this.serverConfig.setMaxHeadersSize(maxHeadersSize);
return this;
}
public ServerBuilder setMaxContentLength(int maxContentLength) {
this.serverConfig.setMaxContentLength(maxContentLength);
return this;
}
public ServerBuilder setMaxCompositeBufferComponents(int maxCompositeBufferComponents) {
this.serverConfig.setMaxCompositeBufferComponents(maxCompositeBufferComponents);
return this;
}
public ServerBuilder setReadTimeoutMillis(int readTimeoutMillis) {
this.serverConfig.setReadTimeoutMillis(readTimeoutMillis);
return this;
}
public ServerBuilder setConnectionTimeoutMillis(int connectionTimeoutMillis) {
this.serverConfig.setConnectTimeoutMillis(connectionTimeoutMillis);
return this;
}
public ServerBuilder setIdleTimeoutMillis(int idleTimeoutMillis) {
this.serverConfig.setIdleTimeoutMillis(idleTimeoutMillis);
return this;
}
public ServerBuilder setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) {
this.serverConfig.setWriteBufferWaterMark(writeBufferWaterMark);
return this;
}
public ServerBuilder setEnableGzip(boolean enableGzip) {
this.serverConfig.setEnableGzip(enableGzip);
return this;
}
public ServerBuilder setInstallHttp2Upgrade(boolean installHttp2Upgrade) {
this.serverConfig.setInstallHttp2Upgrade(installHttp2Upgrade);
return this;
}
public ServerBuilder setSslProvider(SslProvider sslProvider) {
this.serverConfig.setSslProvider(sslProvider);
return this;
}
public ServerBuilder setJdkSslProvider() {
this.serverConfig.setSslProvider(SslProvider.JDK);
return this;
}
public ServerBuilder setOpenSSLSslProvider() {
this.serverConfig.setSslProvider(SslProvider.OPENSSL);
return this;
}
public ServerBuilder setCiphers(Iterable<String> ciphers) {
this.serverConfig.setCiphers(ciphers);
return this;
}
public ServerBuilder setCipherSuiteFilter(CipherSuiteFilter cipherSuiteFilter) {
this.serverConfig.setCipherSuiteFilter(cipherSuiteFilter);
return this;
}
public ServerBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream) {
this.serverConfig.setKeyCertChainInputStream(keyCertChainInputStream);
this.serverConfig.setKeyInputStream(keyInputStream);
return this;
}
public ServerBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream,
String keyPassword) {
this.serverConfig.setKeyCertChainInputStream(keyCertChainInputStream);
this.serverConfig.setKeyInputStream(keyInputStream);
this.serverConfig.setKeyPassword(keyPassword);
return this;
}
public ServerBuilder setSelfCert() throws Exception {
TrustManagerFactory trustManagerFactory = InsecureTrustManagerFactory.INSTANCE;
this.serverConfig.setTrustManagerFactory(trustManagerFactory);
String hostName = serverConfig.getAddress().getInetSocketAddress().getHostString();
SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate(hostName);
this.serverConfig.setKeyCertChainInputStream(selfSignedCertificate.certificate());
this.serverConfig.setKeyInputStream(selfSignedCertificate.privateKey());
this.serverConfig.setKeyPassword(null);
return this;
}
public ServerBuilder addVirtualHost(VirtualServer virtualServer) {
this.serverConfig.addVirtualServer(virtualServer);
return this;
}
public Server build() throws SSLException {
return new Server(serverConfig, byteBufAllocator, parentEventLoopGroup, childEventLoopGroup, socketChannelClass);
}
}

View file

@ -0,0 +1,541 @@
package org.xbib.netty.http.server;
import io.netty.channel.WriteBufferWaterMark;
import io.netty.channel.epoll.Epoll;
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.CipherSuiteFilter;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.context.VirtualServer;
import javax.net.ssl.TrustManagerFactory;
import java.io.InputStream;
import java.security.KeyStore;
import java.util.ArrayList;
import java.util.List;
public class ServerConfig {
interface Defaults {
/**
* Default bind address. We do not want to use port 80 or 8080.
*/
HttpAddress ADDRESS = HttpAddress.http1("localhost", 8008);
/**
* If frame logging/traffic logging is enabled or not.
*/
boolean DEBUG = false;
/**
* Default debug log level.
*/
LogLevel DEBUG_LOG_LEVEL = LogLevel.DEBUG;
/**
* The default for selecting epoll. If available, select epoll.
*/
boolean EPOLL = Epoll.isAvailable();
/**
* Let Netty decide about parent thread count.
*/
int PARENT_THREAD_COUNT = 0;
/**
* Child thread count. Let Netty decide.
*/
int CHILD_THREAD_COUNT = 0;
/**
* Default for SO_REUSEADDR.
*/
boolean SO_REUSEADDR = true;
/**
* Default for TCP_NODELAY.
*/
boolean TCP_NODELAY = 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;
/**
* Default for socket back log.
*/
int SO_BACKLOG = 10 * 1024;
/**
* Default connect timeout in milliseconds.
*/
int CONNECT_TIMEOUT_MILLIS = 5000;
/**
* Default connect timeout in milliseconds.
*/
int READ_TIMEOUT_MILLIS = 15000;
/**
* Default idle timeout in milliseconds.
*/
int IDLE_TIMEOUT_MILLIS = 30000;
/**
* 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;
/**
* Default write buffer water mark.
*/
WriteBufferWaterMark WRITE_BUFFER_WATER_MARK = WriteBufferWaterMark.DEFAULT;
/**
* Default for gzip codec.
*/
boolean ENABLE_GZIP = true;
/**
* Default HTTP/2 settings.
*/
Http2Settings HTTP_2_SETTINGS = Http2Settings.defaultSettings();
/**
* Default for HTTP/2 upgrade under HTTP 1.
*/
boolean INSTALL_HTTP_UPGRADE2 = false;
/**
* Default SSL provider.
*/
SslProvider DEFAULT_SSL_PROVIDER = OpenSsl.isAlpnSupported() ? SslProvider.OPENSSL : SslProvider.JDK;
/**
* Default ciphers.
*/
Iterable<String> DEFAULT_CIPHERS = Http2SecurityUtil.CIPHERS;
/**
* Default cipher suite filter.
*/
CipherSuiteFilter DEFAULT_CIPHER_SUITE_FILTER = SupportedCipherSuiteFilter.INSTANCE;
}
private static TrustManagerFactory TRUST_MANAGER_FACTORY;
static {
try {
TRUST_MANAGER_FACTORY = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
} catch (Exception e) {
TRUST_MANAGER_FACTORY = null;
}
}
private HttpAddress httpAddress = Defaults.ADDRESS;
private boolean debug = Defaults.DEBUG;
private LogLevel debugLogLevel = Defaults.DEBUG_LOG_LEVEL;
private boolean epoll = Defaults.EPOLL;
private int parentThreadCount = Defaults.PARENT_THREAD_COUNT;
private int childThreadCount = Defaults.CHILD_THREAD_COUNT;
private boolean reuseAddr = Defaults.SO_REUSEADDR;
private boolean tcpNodelay = Defaults.TCP_NODELAY;
private int tcpSendBufferSize = Defaults.TCP_SEND_BUFFER_SIZE;
private int tcpReceiveBufferSize = Defaults.TCP_RECEIVE_BUFFER_SIZE;
private int backLogSize = Defaults.SO_BACKLOG;
private int maxInitialLineLength = Defaults.MAX_INITIAL_LINE_LENGTH;
private int maxHeadersSize = Defaults.MAX_HEADERS_SIZE;
private int maxChunkSize = Defaults.MAX_CHUNK_SIZE;
private int maxContentLength = Defaults.MAX_CONTENT_LENGTH;
private int maxCompositeBufferComponents = Defaults.MAX_COMPOSITE_BUFFER_COMPONENTS;
private int connectTimeoutMillis = Defaults.CONNECT_TIMEOUT_MILLIS;
private int readTimeoutMillis = Defaults.READ_TIMEOUT_MILLIS;
private int idleTimeoutMillis = Defaults.IDLE_TIMEOUT_MILLIS;
private WriteBufferWaterMark writeBufferWaterMark = Defaults.WRITE_BUFFER_WATER_MARK;
private boolean enableGzip = Defaults.ENABLE_GZIP;
private Http2Settings http2Settings = Defaults.HTTP_2_SETTINGS;
private boolean installHttp2Upgrade = Defaults.INSTALL_HTTP_UPGRADE2;
private SslProvider sslProvider = Defaults.DEFAULT_SSL_PROVIDER;
private Iterable<String> ciphers = Defaults.DEFAULT_CIPHERS;
private CipherSuiteFilter cipherSuiteFilter = Defaults.DEFAULT_CIPHER_SUITE_FILTER;
private InputStream keyCertChainInputStream;
private InputStream keyInputStream;
private String keyPassword;
private List<VirtualServer> virtualServers;
private TrustManagerFactory trustManagerFactory = TRUST_MANAGER_FACTORY;
private KeyStore trustManagerKeyStore = null;
public ServerConfig() {
this.virtualServers = new ArrayList<>();
addVirtualServer(new VirtualServer(null));
}
public ServerConfig enableDebug() {
this.debug = true;
return this;
}
public ServerConfig disableDebug() {
this.debug = false;
return this;
}
public boolean isDebug() {
return debug;
}
public ServerConfig setDebugLogLevel(LogLevel debugLogLevel) {
this.debugLogLevel = debugLogLevel;
return this;
}
public LogLevel getDebugLogLevel() {
return debugLogLevel;
}
public ServerConfig enableEpoll() {
this.epoll = true;
return this;
}
public ServerConfig disableEpoll() {
this.epoll = false;
return this;
}
public ServerConfig setEpoll(boolean epoll) {
this.epoll = epoll;
return this;
}
public boolean isEpoll() {
return epoll;
}
public ServerConfig setParentThreadCount(int parentThreadCount) {
this.parentThreadCount = parentThreadCount;
return this;
}
public int getParentThreadCount() {
return parentThreadCount;
}
public ServerConfig setChildThreadCount(int childThreadCount) {
this.childThreadCount = childThreadCount;
return this;
}
public int getChildThreadCount() {
return childThreadCount;
}
public ServerConfig setReuseAddr(boolean reuseAddr) {
this.reuseAddr = reuseAddr;
return this;
}
public boolean isReuseAddr() {
return reuseAddr;
}
public ServerConfig setTcpNodelay(boolean tcpNodelay) {
this.tcpNodelay = tcpNodelay;
return this;
}
public boolean isTcpNodelay() {
return tcpNodelay;
}
public ServerConfig setTcpSendBufferSize(int tcpSendBufferSize) {
this.tcpSendBufferSize = tcpSendBufferSize;
return this;
}
public int getTcpSendBufferSize() {
return tcpSendBufferSize;
}
public ServerConfig setTcpReceiveBufferSize(int tcpReceiveBufferSize) {
this.tcpReceiveBufferSize = tcpReceiveBufferSize;
return this;
}
public int getTcpReceiveBufferSize() {
return tcpReceiveBufferSize;
}
public ServerConfig setBackLogSize(int backLogSize) {
this.backLogSize = backLogSize;
return this;
}
public int getBackLogSize() {
return backLogSize;
}
public ServerConfig setConnectTimeoutMillis(int connectTimeoutMillis) {
this.connectTimeoutMillis = connectTimeoutMillis;
return this;
}
public int getConnectTimeoutMillis() {
return connectTimeoutMillis;
}
public ServerConfig setReadTimeoutMillis(int readTimeoutMillis) {
this.readTimeoutMillis = readTimeoutMillis;
return this;
}
public int getReadTimeoutMillis() {
return readTimeoutMillis;
}
public ServerConfig setIdleTimeoutMillis(int idleTimeoutMillis) {
this.idleTimeoutMillis = idleTimeoutMillis;
return this;
}
public int getIdleTimeoutMillis() {
return idleTimeoutMillis;
}
public ServerConfig setAddress(HttpAddress httpAddress) {
this.httpAddress = httpAddress;
return this;
}
public HttpAddress getAddress() {
return httpAddress;
}
public ServerConfig setMaxInitialLineLength(int maxInitialLineLength) {
this.maxInitialLineLength = maxInitialLineLength;
return this;
}
public int getMaxInitialLineLength() {
return maxInitialLineLength;
}
public ServerConfig setMaxHeadersSize(int maxHeadersSize) {
this.maxHeadersSize = maxHeadersSize;
return this;
}
public int getMaxHeadersSize() {
return maxHeadersSize;
}
public ServerConfig setMaxChunkSize(int maxChunkSize) {
this.maxChunkSize = maxChunkSize;
return this;
}
public int getMaxChunkSize() {
return maxChunkSize;
}
public ServerConfig setMaxContentLength(int maxContentLength) {
this.maxContentLength = maxContentLength;
return this;
}
public int getMaxContentLength() {
return maxContentLength;
}
public ServerConfig setMaxCompositeBufferComponents(int maxCompositeBufferComponents) {
this.maxCompositeBufferComponents = maxCompositeBufferComponents;
return this;
}
public int getMaxCompositeBufferComponents() {
return maxCompositeBufferComponents;
}
public ServerConfig setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) {
this.writeBufferWaterMark = writeBufferWaterMark;
return this;
}
public WriteBufferWaterMark getWriteBufferWaterMark() {
return writeBufferWaterMark;
}
public ServerConfig setEnableGzip(boolean enableGzip) {
this.enableGzip = enableGzip;
return this;
}
public boolean isEnableGzip() {
return enableGzip;
}
public ServerConfig setInstallHttp2Upgrade(boolean http2Upgrade) {
this.installHttp2Upgrade = http2Upgrade;
return this;
}
public boolean isInstallHttp2Upgrade() {
return installHttp2Upgrade;
}
public ServerConfig setHttp2Settings(Http2Settings http2Settings) {
this.http2Settings = http2Settings;
return this;
}
public Http2Settings getHttp2Settings() {
return http2Settings;
}
public ServerConfig setSslProvider(SslProvider sslProvider) {
this.sslProvider = sslProvider;
return this;
}
public SslProvider getSslProvider() {
return sslProvider;
}
public ServerConfig setCiphers(Iterable<String> ciphers) {
this.ciphers = ciphers;
return this;
}
public Iterable<String> getCiphers() {
return ciphers;
}
public ServerConfig setCipherSuiteFilter(CipherSuiteFilter cipherSuiteFilter) {
this.cipherSuiteFilter = cipherSuiteFilter;
return this;
}
public CipherSuiteFilter getCipherSuiteFilter() {
return cipherSuiteFilter;
}
public ServerConfig setKeyCertChainInputStream(InputStream keyCertChainInputStream) {
this.keyCertChainInputStream = keyCertChainInputStream;
return this;
}
public InputStream getKeyCertChainInputStream() {
return keyCertChainInputStream;
}
public ServerConfig setKeyInputStream(InputStream keyInputStream) {
this.keyInputStream = keyInputStream;
return this;
}
public InputStream getKeyInputStream() {
return keyInputStream;
}
public ServerConfig setKeyPassword(String keyPassword) {
this.keyPassword = keyPassword;
return this;
}
public String getKeyPassword() {
return keyPassword;
}
public ServerConfig addVirtualServer(VirtualServer virtualServer) {
this.virtualServers.add(virtualServer);
return this;
}
public List<VirtualServer> getVirtualServers() {
return virtualServers;
}
public ServerConfig setTrustManagerFactory(TrustManagerFactory trustManagerFactory) {
this.trustManagerFactory = trustManagerFactory;
return this;
}
public TrustManagerFactory getTrustManagerFactory() {
return trustManagerFactory;
}
public ServerConfig setTrustManagerKeyStore(KeyStore trustManagerKeyStore) {
this.trustManagerKeyStore = trustManagerKeyStore;
return this;
}
public KeyStore getTrustManagerKeyStore() {
return trustManagerKeyStore;
}
}

View file

@ -0,0 +1,44 @@
package org.xbib.netty.http.server;
import io.netty.bootstrap.Bootstrap;
import java.util.Optional;
/**
* Server name.
*/
public final class ServerName {
/**
* The default value for {@code Server} header.
*/
private static final String SERVER_NAME = String.format("XbibHttpServer/%s (Java/%s/%s) (Netty/%s)",
httpServerVersion(), javaVendor(), javaVersion(), nettyVersion());
private ServerName() {
}
public static String getServerName() {
return SERVER_NAME;
}
private static String httpServerVersion() {
return Optional.ofNullable(Server.class.getPackage().getImplementationVersion())
.orElse("unknown");
}
private static String javaVendor() {
return Optional.ofNullable(System.getProperty("java.vendor"))
.orElse("unknown");
}
private static String javaVersion() {
return Optional.ofNullable(System.getProperty("java.version"))
.orElse("unknown");
}
private static String nettyVersion() {
return Optional.ofNullable(Bootstrap.class.getPackage().getImplementationVersion())
.orElse("unknown");
}
}

View file

@ -0,0 +1,33 @@
package org.xbib.netty.http.server.context;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* The {@code Context} annotation decorates methods which are mapped
* to a context (path) within the server, and provide its contents.
* The annotated methods must have the same signature and contract
* as {@link ContextHandler#serve}, but can have arbitrary names.
*
* @see VirtualServer#addContexts(Object)
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Context {
/**
* The context (path) that this field maps to (must begin with '/').
*
* @return the context (path) that this field maps to
*/
String value();
/**
* The HTTP methods supported by this context handler (default is "GET").
*
* @return the HTTP methods supported by this context handler
*/
String[] methods() default "GET";
}

View file

@ -0,0 +1,25 @@
package org.xbib.netty.http.server.context;
import org.xbib.netty.http.server.transport.ServerRequest;
import org.xbib.netty.http.server.transport.ServerResponse;
import java.io.IOException;
/**
* A {@code ContextHandler} is capable of serving content for
* resources within its context.
*
* @see VirtualServer#addContext
*/
@FunctionalInterface
public interface ContextHandler {
/**
* Serves the given request using the given response.
*
* @param serverRequest the request to be served
* @param serverResponse the response to be filled
* @throws IOException if an IO error occurs
*/
void serve(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException;
}

View file

@ -0,0 +1,43 @@
package org.xbib.netty.http.server.context;
import java.util.HashMap;
import java.util.Map;
/**
* The {@code ContextInfo} class holds a single context's information.
*/
public class ContextInfo {
private final Map<String, ContextHandler> handlers = new HashMap<>(2);
private final VirtualServer virtualServer;
public ContextInfo(VirtualServer virtualServer) {
this.virtualServer = virtualServer;
}
/**
* Returns the map of supported HTTP methods and their corresponding handlers.
*
* @return the map of supported HTTP methods and their corresponding handlers
*/
public Map<String, ContextHandler> getHandlers() {
return handlers;
}
/**
* Adds (or replaces) a context handler for the given HTTP methods.
*
* @param handler the context handler
* @param methods the HTTP methods supported by the handler (default is "GET")
*/
public void addHandler(ContextHandler handler, String... methods) {
if (methods.length == 0) {
methods = new String[]{"GET"};
}
for (String method : methods) {
handlers.put(method, handler);
virtualServer.getMethods().add(method);
}
}
}

View file

@ -0,0 +1,46 @@
package org.xbib.netty.http.server.context;
import org.xbib.netty.http.server.transport.ServerRequest;
import org.xbib.netty.http.server.transport.ServerResponse;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* The {@code MethodContextHandler} services a context
* by invoking a handler method on a specified object.
* The method must have the same signature and contract as
* {@link ContextHandler#serve}, but can have an arbitrary name.
*
* @see VirtualServer#addContexts(Object)
*/
public class MethodContextHandler implements ContextHandler {
private final Method m;
private final Object obj;
public MethodContextHandler(Method m, Object obj) throws IllegalArgumentException {
this.m = m;
this.obj = obj;
Class<?>[] params = m.getParameterTypes();
if (params.length != 2
|| !ServerRequest.class.isAssignableFrom(params[0])
|| !ServerResponse.class.isAssignableFrom(params[1])
|| !int.class.isAssignableFrom(m.getReturnType())) {
throw new IllegalArgumentException("invalid method signature: " + m);
}
}
@Override
public void serve(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException {
try {
m.invoke(obj, serverRequest, serverResponse);
} catch (InvocationTargetException ite) {
throw new IOException("error: " + ite.getCause().getMessage());
} catch (Exception e) {
throw new IOException("error: " + e);
}
}
}

View file

@ -0,0 +1,187 @@
package org.xbib.netty.http.server.context;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* The {@code VirtualServer} class represents a virtual server.
*/
public class VirtualServer {
private final String name;
private final Set<String> aliases;
private final Set<String> methods;
private final ContextInfo emptyContext;
private final Map<String, ContextInfo> contexts;
private volatile boolean allowGeneratedIndex;
/**
* Constructs a VirtualServer with the given name.
*
* @param name the name, or null if it is the default server
*/
public VirtualServer(String name) {
this.name = name;
this.aliases = new HashSet<>();
this.methods = new HashSet<>();
this.contexts = new HashMap<>();
this.emptyContext = new ContextInfo(this);
contexts.put("*", new ContextInfo(this)); // for "OPTIONS *"
}
/**
* Returns the name.
*
* @return the name, or null if it is the default server
*/
public String getName() {
return name;
}
/**
* Adds an alias for this virtual server.
*
* @param alias the alias
*/
public void addAlias(String alias) {
aliases.add(alias);
}
/**
* Returns the aliases.
*
* @return the (unmodifiable) set of aliases (which may be empty)
*/
public Set<String> getAliases() {
return Collections.unmodifiableSet(aliases);
}
/**
* Returns whether auto-generated indices are allowed.
*
* @return whether auto-generated indices are allowed
*/
public boolean isAllowGeneratedIndex() {
return allowGeneratedIndex;
}
/**
* Sets whether auto-generated indices are allowed. If false, and a
* directory resource is requested, an error will be returned instead.
*
* @param allowed specifies whether generated indices are allowed
*/
public void setAllowGeneratedIndex(boolean allowed) {
this.allowGeneratedIndex = allowed;
}
/**
* Returns all HTTP methods explicitly supported by at least one context
* (this may or may not include the methods with required or built-in support).
*
* @return all HTTP methods explicitly supported by at least one context
*/
public Set<String> getMethods() {
return methods;
}
/**
* Returns the context handler for the given path.
* If a context is not found for the given path, the search is repeated for
* its parent path, and so on until a base context is found. If neither the
* given path nor any of its parents has a context, an empty context is returned.
*
* @param path the context's path
* @return the context info for the given path, or an empty context if none exists
*/
public ContextInfo getContext(String path) {
path = trimRight(path, '/'); // remove trailing slash
ContextInfo info = null;
while (info == null && path != null) {
info = contexts.get(path);
path = getParentPath(path);
}
return info != null ? info : emptyContext;
}
/**
* Adds a context and its corresponding context handler to this server.
* Paths are normalized by removing trailing slashes (except the root).
*
* @param path the context's path (must start with '/')
* @param handler the context handler for the given path
* @param methods the HTTP methods supported by the context handler (default is "GET")
* @throws IllegalArgumentException if path is malformed
*/
public void addContext(String path, ContextHandler handler, String... methods) {
if (path == null || !path.startsWith("/") && !path.equals("*")) {
throw new IllegalArgumentException("invalid path: " + path);
}
path = trimRight(path, '/');
ContextInfo info = new ContextInfo(this);
ContextInfo existing = contexts.putIfAbsent(path, info);
info = existing != null ? existing : info;
info.addHandler(handler, methods);
}
/**
* Adds contexts for all methods of the given object that
* are annotated with the {@link Context} annotation.
*
* @param o the object whose annotated methods are added
* @throws IllegalArgumentException if a Context-annotated
* method has an {@link Context invalid signature}
*/
public void addContexts(Object o) throws IllegalArgumentException {
for (Class<?> c = o.getClass(); c != null; c = c.getSuperclass()) {
// add to contexts those with @Context annotation
for (Method m : c.getDeclaredMethods()) {
Context context = m.getAnnotation(Context.class);
if (context != null) {
//m.setAccessible(true); // allow access to private method
addContext(context.value(), new MethodContextHandler(m, o), context.methods());
}
}
}
}
/**
* Returns the given string with all occurrences of the given character
* removed from its right side.
*
* @param s the string to trim
* @param c the character to remove
* @return the trimmed string
*/
private static String trimRight(String s, char c) {
int len = s.length() - 1;
int end = len;
while (end >= 0 && s.charAt(end) == c) {
end--;
}
return end == len ? s : s.substring(0, end + 1);
}
/**
* Returns the parent of the given path.
*
* @param path the path whose parent is returned (must start with '/')
* @return the parent of the given path (excluding trailing slash),
* or null if given path is the root path
*/
private static String getParentPath(String path) {
path = trimRight(path, '/'); // remove trailing slash
int slash = path.lastIndexOf('/');
return slash == -1 ? null : path.substring(0, slash);
}
}

View file

@ -0,0 +1,220 @@
package org.xbib.netty.http.server.handler;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerUpgradeHandler;
import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler;
import io.netty.handler.codec.http2.Http2ConnectionHandler;
import io.netty.handler.codec.http2.Http2MultiplexCodec;
import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder;
import io.netty.handler.codec.http2.Http2ServerUpgradeCodec;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.ApplicationProtocolNames;
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
import io.netty.handler.ssl.SniHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.util.AsciiString;
import io.netty.util.DomainNameMapping;
import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.ServerConfig;
import org.xbib.netty.http.server.handler.http1.HttpHandler;
import org.xbib.netty.http.server.handler.http1.IdleTimeoutHandler;
import org.xbib.netty.http.server.handler.http1.TrafficLoggingHandler;
import org.xbib.netty.http.server.handler.http2.UserEventLogger;
import org.xbib.netty.http.server.internal.Http1ObjectEncoder;
import org.xbib.netty.http.server.internal.Http2ObjectEncoder;
import org.xbib.netty.http.server.internal.HttpObjectEncoder;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* HTTP server channel initializer.
*/
public class HttpServerChannelInitializer extends ChannelInitializer<SocketChannel> {
private static final Logger logger = Logger.getLogger(HttpServerChannelInitializer.class.getName());
private final Server server;
private final ServerConfig serverConfig;
private final Http2ConnectionHandler http2ConnectionHandler;
private final DomainNameMapping<SslContext> domainNameMapping;
public HttpServerChannelInitializer(Server server, ServerConfig serverConfig,
DomainNameMapping<SslContext> domainNameMapping) {
this.server = server;
this.serverConfig = serverConfig;
this.domainNameMapping = domainNameMapping;
this.http2ConnectionHandler = null;//createHttp2ConnectionHandler(serverConfig);
}
@Override
public void initChannel(SocketChannel ch) {
if (serverConfig.isDebug()) {
ch.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
}
if (serverConfig.getAddress().isSecure()) {
configureSecure(ch);
} else {
configureClearText(ch);
}
HttpObjectEncoder encoder = serverConfig.getAddress().getVersion().majorVersion() == 2 ?
new Http2ObjectEncoder(http2ConnectionHandler.encoder()) :
new Http1ObjectEncoder();
if (serverConfig.isDebug()) {
logger.log(Level.FINE, "server channel initialized: " + ch.pipeline().names());
}
}
private void configureClearText(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
if (serverConfig.getAddress().getVersion().majorVersion() == 1) {
if (serverConfig.isInstallHttp2Upgrade()) {
installHttp2Upgrade(pipeline);
} else {
pipeline.addFirst(new IdleTimeoutHandler());
pipeline.addLast(new UserEventLogger());
pipeline.addLast(createHttp1ConnectionHandler(serverConfig));
configureHttp1Pipeline(pipeline);
}
} else if (serverConfig.getAddress().getVersion().majorVersion() == 2) {
pipeline.addLast(http2ConnectionHandler);
configureHttp2Pipeline(pipeline);
}
}
private void installHttp2Upgrade(ChannelPipeline pipeline) {
HttpServerCodec httpServerCodec = new HttpServerCodec();
HttpServerUpgradeHandler httpServerUpgradeHandler = new HttpServerUpgradeHandler(httpServerCodec, protocol -> {
if (AsciiString.contentEquals("h2c", protocol)) {
return new Http2ServerUpgradeCodec(http2ConnectionHandler);
} else {
return null;
}
});
pipeline.addLast(new CleartextHttp2ServerUpgradeHandler(httpServerCodec, httpServerUpgradeHandler,
new HttpHandler(server)));
}
private void configureSecure(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new SniHandler(domainNameMapping));
pipeline.addLast(new Http2NegotiationHandler(ApplicationProtocolNames.HTTP_1_1));
}
private HttpServerCodec createHttp1ConnectionHandler(ServerConfig context) {
return new HttpServerCodec(context.getMaxInitialLineLength(),
context.getMaxHeadersSize(), context.getMaxChunkSize());
}
private void configureHttp1Pipeline(ChannelPipeline pipeline) {
if (serverConfig.isEnableGzip()) {
pipeline.addLast(new HttpContentDecompressor());
}
HttpObjectAggregator httpObjectAggregator =
new HttpObjectAggregator(serverConfig.getMaxContentLength(), false);
httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents());
pipeline.addLast(httpObjectAggregator);
pipeline.addLast(new HttpHandler(server));
}
private void configureHttp2Pipeline(ChannelPipeline pipeline) {
pipeline.addLast(new UserEventLogger());
pipeline.addLast(new HttpHandler(server));
}
/*private static Http2ConnectionHandler createHttp2ConnectionHandler(ServerConfig serverConfig) {
Http2Settings initialSettings = serverConfig.getHttp2Settings();
Http2Connection http2Connection = new DefaultHttp2Connection(true);
Long maxHeaderListSize = initialSettings.maxHeaderListSize();
Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ?
new DefaultHttp2HeadersDecoder(true) :
new DefaultHttp2HeadersDecoder(true, maxHeaderListSize));
Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter();
Http2FrameLogger frameLogger = null;
if (serverConfig.isDebug()) {
frameLogger = new Http2FrameLogger(serverConfig.getDebugLogLevel(), "server");
}
if (frameLogger != null) {
frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger);
frameReader = new Http2InboundFrameLogger(frameReader, frameLogger);
}
Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(http2Connection, frameWriter);
Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(http2Connection, encoder, frameReader);
Http2ConnectionHandler http2ConnectionHandler = new Http2ServerConnectionHandler(decoder, encoder, initialSettings);
Http2Handler http2Handler = new Http2Handler(serverConfig, http2Connection, true);
http2ConnectionHandler.connection().addListener(http2Handler);
http2ConnectionHandler.decoder().frameListener(new DelegatingDecompressorFrameListener(http2Connection, http2Handler));
if (serverConfig.getIdleTimeoutMillis() > 0) {
http2ConnectionHandler.gracefulShutdownTimeoutMillis(serverConfig.getIdleTimeoutMillis());
}
return http2ConnectionHandler;
}*/
private ChannelHandler createMultiplexInitializer() {
/*HttpObjectAggregator httpObjectAggregator =
new HttpObjectAggregator(serverConfig.getMaxContentLength(), false);
httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents());*/
return new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.pipeline().addLast(http2ConnectionHandler);
configureHttp2Pipeline(ch.pipeline());
//.addLast(new Http2StreamFrameToHttpObjectCodec(true))
//.addLast(httpObjectAggregator)
//.addLast(httpHandler);
}
};
}
private Http2MultiplexCodec createHttp2MultiplexCodec() {
Http2MultiplexCodecBuilder multiplexCodecBuilder = Http2MultiplexCodecBuilder.forServer(createMultiplexInitializer());
multiplexCodecBuilder.initialSettings(serverConfig.getHttp2Settings());
if (serverConfig.getIdleTimeoutMillis() > 0) {
multiplexCodecBuilder.gracefulShutdownTimeoutMillis(serverConfig.getIdleTimeoutMillis());
}
return multiplexCodecBuilder.build();
}
/**
* Negotiates with the browser if HTTP/2 or HTTP is going to be used. Once decided, the
* pipeline is setup with the correct handlers for the selected protocol.
*/
class Http2NegotiationHandler extends ApplicationProtocolNegotiationHandler {
Http2NegotiationHandler(String fallbackProtocol) {
super(fallbackProtocol);
}
@Override
protected void configurePipeline(ChannelHandlerContext ctx, String protocol) {
ChannelPipeline pipeline = ctx.pipeline();
if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) {
pipeline.addLast(createHttp1ConnectionHandler(serverConfig));
configureHttp1Pipeline(pipeline);
return;
}
if (ApplicationProtocolNames.HTTP_2.equals(protocol)) {
pipeline.addLast(http2ConnectionHandler);
configureHttp2Pipeline(pipeline);
if (serverConfig.isDebug()) {
logger.log(Level.INFO, "after successful HTTP/2 negotiation: " + pipeline.names());
}
return;
}
ctx.close();
throw new IllegalStateException("unknown protocol: " + protocol);
}
}
}

View file

@ -0,0 +1,75 @@
package org.xbib.netty.http.server.handler.http1;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpContentDecompressor;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.SniHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.util.DomainNameMapping;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.ServerConfig;
import java.util.logging.Level;
import java.util.logging.Logger;
public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
private static final Logger logger = Logger.getLogger(HttpChannelInitializer.class.getName());
private final ServerConfig serverConfig;
private final HttpAddress httpAddress;
private final HttpHandler httpHandler;
private final DomainNameMapping<SslContext> domainNameMapping;
public HttpChannelInitializer(Server server,
HttpAddress httpAddress,
DomainNameMapping<SslContext> domainNameMapping) {
this.serverConfig = server.getServerConfig();
this.httpAddress = httpAddress;
this.domainNameMapping = domainNameMapping;
this.httpHandler = new HttpHandler(server);
}
@Override
public void initChannel(SocketChannel channel) {
if (serverConfig.isDebug()) {
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
}
if (httpAddress.isSecure()) {
configureEncrypted(channel);
} else {
configureCleartext(channel);
}
if (serverConfig.isDebug()) {
logger.log(Level.FINE, "HTTP 1 channel initialized: " + channel.pipeline().names());
}
}
private void configureEncrypted(SocketChannel channel) {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new SniHandler(domainNameMapping));
configureCleartext(channel);
}
private void configureCleartext(SocketChannel channel) {
ChannelPipeline pipeline = channel.pipeline();
pipeline.addLast(new HttpServerCodec(serverConfig.getMaxInitialLineLength(),
serverConfig.getMaxHeadersSize(), serverConfig.getMaxChunkSize()));
if (serverConfig.isEnableGzip()) {
pipeline.addLast(new HttpContentDecompressor());
}
HttpObjectAggregator httpObjectAggregator = new HttpObjectAggregator(serverConfig.getMaxContentLength(),
false);
httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents());
pipeline.addLast(httpObjectAggregator);
pipeline.addLast(httpHandler);
}
}

View file

@ -0,0 +1,55 @@
package org.xbib.netty.http.server.handler.http1;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.transport.ServerTransport;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* HTTP handler.
*/
@ChannelHandler.Sharable
public class HttpHandler extends ChannelInboundHandlerAdapter {
private static final Logger logger = Logger.getLogger(HttpHandler.class.getName());
private final Server server;
public HttpHandler(Server server) {
this.server = server;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof FullHttpRequest) {
FullHttpRequest fullHttpRequest = (FullHttpRequest) msg;
ServerTransport serverTransport = server.newTransport(fullHttpRequest.protocolVersion());
serverTransport.requestReceived(ctx, fullHttpRequest);
} else {
super.channelRead(ctx, msg);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
logger.log(Level.WARNING, cause.getMessage(), cause);
ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.INTERNAL_SERVER_ERROR,
Unpooled.copiedBuffer(cause.getMessage().getBytes(StandardCharsets.UTF_8))));
}
}

View file

@ -0,0 +1,32 @@
package org.xbib.netty.http.server.handler.http1;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import java.text.MessageFormat;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Idle timeout handler.
*/
@ChannelHandler.Sharable
public class IdleTimeoutHandler extends IdleStateHandler {
private final Logger logger = Logger.getLogger(IdleTimeoutHandler.class.getName());
public IdleTimeoutHandler() {
super(30, 30, 30);
}
@Override
protected final void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) {
if (!evt.isFirst()) {
return;
}
logger.log(Level.WARNING, () -> MessageFormat.format("{0} closing an idle connection", ctx.channel()));
ctx.close();
}
}

View file

@ -0,0 +1,43 @@
package org.xbib.netty.http.server.handler.http1;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
/**
* A Netty handler that logs the I/O traffic of a connection.
*/
@ChannelHandler.Sharable
public class TrafficLoggingHandler extends LoggingHandler {
public TrafficLoggingHandler(LogLevel level) {
super("server", level);
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) {
ctx.fireChannelRegistered();
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) {
ctx.fireChannelUnregistered();
}
@Override
public void flush(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
if (msg instanceof ByteBuf && !((ByteBuf) msg).isReadable()) {
ctx.write(msg, promise);
} else {
super.write(ctx, msg, promise);
}
}
}

View file

@ -0,0 +1,17 @@
package org.xbib.netty.http.server.handler.http2;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import java.util.logging.Level;
import java.util.logging.Logger;
public class DummyHandler extends ChannelDuplexHandler {
private static final Logger logger = Logger.getLogger(DummyHandler.class.getName());
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
logger.log(Level.INFO, "msg = " + msg + " class = " + msg.getClass().getName());
}
}

View file

@ -0,0 +1,33 @@
package org.xbib.netty.http.server.handler.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http2.DefaultHttp2DataFrame;
import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame;
import io.netty.handler.codec.http2.Http2DataFrame;
import io.netty.handler.codec.http2.Http2EventAdapter;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.Http2HeadersFrame;
import java.util.logging.Level;
import java.util.logging.Logger;
public class FrameListener extends Http2EventAdapter {
private static final Logger logger = Logger.getLogger(FrameListener.class.getName());
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
short weight, boolean exclusive, int padding, boolean endStream) {
logger.log(Level.FINE, "onHeadersRead");
Http2HeadersFrame frame = new DefaultHttp2HeadersFrame(headers,endStream,padding);
ctx.fireChannelRead(frame);
}
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream) {
logger.log(Level.FINE, "onDataRead");
Http2DataFrame frame = new DefaultHttp2DataFrame(data, endOfStream, padding);
ctx.fireChannelRead(frame);
return data.readableBytes() + padding;
}
}

View file

@ -0,0 +1,71 @@
package org.xbib.netty.http.server.handler.http2;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpUtil;
import java.nio.charset.StandardCharsets;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
/**
* HTTP handler that responds with a "Hello World"
*/
public class HelloWorldHttp1Handler extends SimpleChannelInboundHandler<FullHttpRequest> {
static final ByteBuf RESPONSE_BYTES = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hello World", StandardCharsets.UTF_8));
private final String establishApproach;
public HelloWorldHttp1Handler(String establishApproach) {
this.establishApproach = checkNotNull(establishApproach, "establishApproach");
}
@Override
public void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req) {
if (HttpUtil.is100ContinueExpected(req)) {
ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
}
boolean keepAlive = HttpUtil.isKeepAlive(req);
ByteBuf content = ctx.alloc().buffer();
content.writeBytes(RESPONSE_BYTES.duplicate());
ByteBufUtil.writeAscii(content, " - via " + req.protocolVersion() + " (" + establishApproach + ")");
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
if (!keepAlive) {
ctx.write(response).addListener(ChannelFutureListener.CLOSE);
} else {
response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
ctx.write(response);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

View file

@ -0,0 +1,87 @@
package org.xbib.netty.http.server.handler.http2;
import static io.netty.buffer.Unpooled.copiedBuffer;
import static io.netty.buffer.Unpooled.unreleasableBuffer;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http2.DefaultHttp2DataFrame;
import io.netty.handler.codec.http2.DefaultHttp2Headers;
import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame;
import io.netty.handler.codec.http2.Http2DataFrame;
import io.netty.handler.codec.http2.Http2Headers;
import io.netty.handler.codec.http2.Http2HeadersFrame;
import io.netty.util.CharsetUtil;
/**
* A simple handler that responds with the message "Hello World!".
*
* <p>This example is making use of the "multiplexing" http2 API, where streams are mapped to child
* Channels. This API is very experimental and incomplete.
*/
@Sharable
public class HelloWorldHttp2Handler extends ChannelDuplexHandler {
private static final ByteBuf RESPONSE_BYTES = unreleasableBuffer(copiedBuffer("Hello World", CharsetUtil.UTF_8));
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
super.exceptionCaught(ctx, cause);
cause.printStackTrace();
ctx.close();
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof Http2HeadersFrame) {
onHeadersRead(ctx, (Http2HeadersFrame) msg);
} else if (msg instanceof Http2DataFrame) {
onDataRead(ctx, (Http2DataFrame) msg);
} else {
super.channelRead(ctx, msg);
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
/**
* If receive a frame with end-of-stream set, send a pre-canned response.
*/
private static void onDataRead(ChannelHandlerContext ctx, Http2DataFrame data) throws Exception {
if (data.isEndStream()) {
sendResponse(ctx, data.content());
} else {
// We do not send back the response to the remote-peer, so we need to release it.
data.release();
}
}
/**
* If receive a frame with end-of-stream set, send a pre-canned response.
*/
private static void onHeadersRead(ChannelHandlerContext ctx, Http2HeadersFrame headers)
throws Exception {
if (headers.isEndStream()) {
ByteBuf content = ctx.alloc().buffer();
content.writeBytes(RESPONSE_BYTES.duplicate());
ByteBufUtil.writeAscii(content, " - via HTTP/2");
sendResponse(ctx, content);
}
}
/**
* Sends a "Hello World" DATA frame to the client.
*/
private static void sendResponse(ChannelHandlerContext ctx, ByteBuf payload) {
// Send a frame for the response status
Http2Headers headers = new DefaultHttp2Headers().status(OK.codeAsText());
ctx.write(new DefaultHttp2HeadersFrame(headers));
ctx.write(new DefaultHttp2DataFrame(payload, true));
}
}

View file

@ -0,0 +1,251 @@
package org.xbib.netty.http.server.handler.http2;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpMessage;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerUpgradeHandler;
import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder;
import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder;
import io.netty.handler.codec.http2.DefaultHttp2FrameReader;
import io.netty.handler.codec.http2.DefaultHttp2FrameWriter;
import io.netty.handler.codec.http2.DefaultHttp2HeadersDecoder;
import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener;
import io.netty.handler.codec.http2.Http2CodecUtil;
import io.netty.handler.codec.http2.Http2Connection;
import io.netty.handler.codec.http2.Http2ConnectionDecoder;
import io.netty.handler.codec.http2.Http2ConnectionEncoder;
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.Http2FrameReader;
import io.netty.handler.codec.http2.Http2FrameWriter;
import io.netty.handler.codec.http2.Http2InboundFrameLogger;
import io.netty.handler.codec.http2.Http2MultiplexCodec;
import io.netty.handler.codec.http2.Http2MultiplexCodecBuilder;
import io.netty.handler.codec.http2.Http2OutboundFrameLogger;
import io.netty.handler.codec.http2.Http2ServerUpgradeCodec;
import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.ssl.SniHandler;
import io.netty.handler.ssl.SslContext;
import io.netty.util.AsciiString;
import io.netty.util.DomainNameMapping;
import io.netty.util.ReferenceCountUtil;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.ServerConfig;
import org.xbib.netty.http.server.handler.http1.HttpHandler;
import org.xbib.netty.http.server.handler.http1.TrafficLoggingHandler;
import java.util.logging.Level;
import java.util.logging.Logger;
public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
private static final Logger logger = Logger.getLogger(Http2ChannelInitializer.class.getName());
private final Server server;
private final ServerConfig serverConfig;
private final HttpAddress httpAddress;
private final DomainNameMapping<SslContext> domainNameMapping;
public Http2ChannelInitializer(Server server,
HttpAddress httpAddress,
DomainNameMapping<SslContext> domainNameMapping) {
this.server = server;
this.serverConfig = server.getServerConfig();
this.httpAddress = httpAddress;
this.domainNameMapping = domainNameMapping;
}
/**
* The channel initialization for HTTP/2.
*
* @param channel socket channel
*/
@Override
public void initChannel(Channel channel) {
if (serverConfig.isDebug()) {
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
}
if (httpAddress.isSecure()) {
configureEncrypted(channel);
} else {
configureCleartext(channel);
}
if (server.getServerConfig().isDebug()) {
logger.log(Level.FINE, "HTTP/2 server channel initialized: " + channel.pipeline().names());
}
}
private void configureEncrypted(Channel channel) {
channel.pipeline().addLast(new SniHandler(domainNameMapping));
configureCleartext(channel);
}
private void configureCleartext(Channel ch) {
Http2SettingsHandler http2SettingsHandler = new Http2SettingsHandler();
Http2RequestHandler http2RequestHandler = new Http2RequestHandler();
//HttpHandler httpHandler = new HttpHandler(server);
ch.pipeline()
//.addLast(newConnectionHandler())
.addLast(upgradeHandler());
//.addLast(http2SettingsHandler)
//.addLast(http2RequestHandler);
// .addLast(sourceCodec)
/*final Http2MultiplexCodec http2Codec = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
logger.log(Level.INFO, "initChannel multiplex ");
}
}).build();
HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol ->
new Http2ServerUpgradeCodec(http2Codec);
final HttpServerCodec serverCodec = new HttpServerCodec();
ch.pipeline().addLast(serverCodec)
.addLast(new HttpServerUpgradeHandler(serverCodec, upgradeCodecFactory))
.addLast(new SimpleChannelInboundHandler<HttpMessage>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpMessage msg) throws Exception {
// If this handler is hit then no upgrade has been attempted and the client is just talking HTTP.
System.err.println("Directly talking: " + msg.protocolVersion() + " (no upgrade was attempted)");
ChannelPipeline pipeline = ctx.pipeline();
ChannelHandlerContext thisCtx = pipeline.context(this);
pipeline.addAfter(thisCtx.name(), null, new HelloWorldHttp1Handler("Direct. No Upgrade Attempted."));
pipeline.replace(this, null, new HttpObjectAggregator(Integer.MAX_VALUE));
ctx.fireChannelRead(ReferenceCountUtil.retain(msg));
}
})
.addLast(new UserEventLogger())
.addLast(new HttpHandler(server));
*/
/*
Http2FrameCodec http2FrameCodec = Http2FrameCodecBuilder.forServer().build();
Http2StreamFrameToHttpObjectCodec http2StreamFrameToHttpObjectCodec =
new Http2StreamFrameToHttpObjectCodec(true, true);
HttpObjectAggregator httpObjectAggregator =
new HttpObjectAggregator(serverConfig.getMaxContentLength(), false);
httpObjectAggregator.setMaxCumulationBufferComponents(serverConfig.getMaxCompositeBufferComponents());
HttpHandler httpHandler = new HttpHandler(server);
Http2ConnectionHandler http2ConnectionHandler = newConnectionHandler(server.getServerConfig());
Http2Connection http2Connection = http2ConnectionHandler.connection();
Http2Handler http2Handler = new Http2Handler(serverConfig, http2Connection, true);
http2Connection.addListener(http2Handler);
http2ConnectionHandler.decoder().frameListener(new DelegatingDecompressorFrameListener(http2Connection, http2Handler));
channel.pipeline().addLast(http2ConnectionHandler)
.addLast(new UserEventLogger())
.addLast(new HttpHandler(server));
//.addLast(new Http2StreamFrameToHttpObjectCodec(true))
//.addLast(httpObjectAggregator)
//.addLast(httpHandler);
*/
}
private Http2ConnectionHandler newStandardConnectionHandler() {
Http2Connection http2Connection = new DefaultHttp2Connection(true);
InboundHttp2ToHttpAdapter inboundHttp2ToHttpAdapter =
new InboundHttp2ToHttpAdapterBuilder(http2Connection)
.maxContentLength(serverConfig.getMaxContentLength())
.propagateSettings(true)
.validateHttpHeaders(true)
.build();
Http2ConnectionHandlerBuilder builder = new Http2ConnectionHandlerBuilder()
.connection(http2Connection)
.initialSettings(serverConfig.getHttp2Settings())
.frameListener(new DelegatingDecompressorFrameListener(http2Connection, inboundHttp2ToHttpAdapter));
if (serverConfig.isDebug()) {
builder.frameLogger(new Http2FrameLogger(serverConfig.getDebugLogLevel(), "server"));
}
return builder.build();
}
private Http2ConnectionHandler newConnectionHandler() {
Http2Settings initialSettings = serverConfig.getHttp2Settings();
Http2Connection http2Connection = new DefaultHttp2Connection(true);
Long maxHeaderListSize = initialSettings.maxHeaderListSize();
Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ?
new DefaultHttp2HeadersDecoder(true) :
new DefaultHttp2HeadersDecoder(true, maxHeaderListSize));
Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter();
Http2FrameLogger frameLogger = null;
if (serverConfig.isDebug()) {
frameLogger = new Http2FrameLogger(serverConfig.getDebugLogLevel(), "server");
}
if (frameLogger != null) {
frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger);
frameReader = new Http2InboundFrameLogger(frameReader, frameLogger);
}
Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(http2Connection, frameWriter);
Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(http2Connection, encoder, frameReader);
Http2ConnectionHandlerBuilder builder = new Http2ConnectionHandlerBuilder()
.connection(http2Connection)
//.codec(decoder, encoder)
//.initialSettings(initialSettings)
.frameListener(new FrameListener())
.frameLogger(new Http2FrameLogger(serverConfig.getDebugLogLevel(), "server"));
if (serverConfig.getIdleTimeoutMillis() > 0) {
builder.gracefulShutdownTimeoutMillis(serverConfig.getIdleTimeoutMillis());
}
return builder.build();
//Http2Handler http2Handler = new Http2Handler(server, http2Connection, true);
//http2ConnectionHandler.connection().addListener(http2Handler);
//http2ConnectionHandler.decoder().frameListener();
//return http2ConnectionHandler;
}
static class Http2ServerConnectionHandler extends Http2ConnectionHandler {
Http2ServerConnectionHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder,
Http2Settings initialSettings) {
super(decoder, encoder, initialSettings);
}
}
private final HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> {
if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) {
return upgradeCodec();
} else {
return null;
}
};
private Http2ServerUpgradeCodec upgradeCodec() {
return new Http2ServerUpgradeCodec(Http2MultiplexCodecBuilder.forServer(http2MultiplexCodec()).build());
}
private HttpServerUpgradeHandler upgradeHandler() {
HttpServerCodec sourceCodec = new HttpServerCodec();
return new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory);
}
private Http2MultiplexCodec http2MultiplexCodec() {
Http2FrameLogger frameLogger = new Http2FrameLogger(serverConfig.getDebugLogLevel(), "server");
return Http2MultiplexCodecBuilder.forServer(new DummyHandler())
.frameLogger(frameLogger)
.initialSettings(serverConfig.getHttp2Settings())
.build();
}
}

View file

@ -0,0 +1,483 @@
package org.xbib.netty.http.server.handler.http2;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.CharSequenceMap;
import io.netty.handler.codec.http2.Http2CodecUtil;
import io.netty.handler.codec.http2.Http2Connection;
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 io.netty.util.AsciiString;
import io.netty.util.internal.ObjectUtil;
import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.ServerConfig;
import org.xbib.netty.http.server.transport.ServerTransport;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A HTTP/2 event adapter for a server.
*
* 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.
*/
@ChannelHandler.Sharable
public class Http2Handler extends Http2EventAdapter {
private static final Logger logger = Logger.getLogger(Http2Handler.class.getName());
private static final HttpVersion HTTP_2_0 = HttpVersion.valueOf("HTTP/2.0");
private final Server server;
private final ServerConfig serverConfig;
private final ServerTransport serverTransport;
private final Http2Connection connection;
private final Http2Connection.PropertyKey messageKey;
private final boolean validateHttpHeaders;
/**
* Constructor for {@link Http2Handler}.
* @param server the server
* @param connection the HTTP/2 connection
* @param validateHeaders true if headers should be validated
*/
public Http2Handler(Server server, Http2Connection connection, boolean validateHeaders) {
this.server = server;
this.serverConfig = server.getServerConfig();
this.connection = connection;
this.validateHttpHeaders = validateHeaders;
this.messageKey = connection.newKey();
this.serverTransport = server.newTransport(HTTP_2_0);
}
/**
* 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) {
if (serverConfig.isDebug()) {
logger.log(Level.FINE, () -> "settings received " + settings);
}
try {
serverTransport.settingsReceived(ctx, settings);
} catch (Exception e) {
logger.log(Level.WARNING, e.getMessage(), e);
}
}
/**
* 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 {
if (serverConfig.isDebug()) {
logger.log(Level.FINE, () -> "headers received " + headers + " endOfStream " + endOfStream);
}
Http2Stream stream = connection.stream(streamId);
FullHttpMessage msg = beginHeader(ctx, stream, headers);
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 {
if (serverConfig.isDebug()) {
logger.log(Level.FINE, () -> "headers received (weighted) " + headers + " endOfStream " + endOfStream);
}
Http2Stream stream = connection.stream(streamId);
FullHttpMessage msg = beginHeader(ctx, stream, headers);
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 {
if (serverConfig.isDebug()) {
logger.log(Level.FINE, () -> "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() > serverConfig.getMaxContentLength() - dataReadableBytes) {
throw Http2Exception.connectionError(Http2Error.INTERNAL_ERROR,
"content length exceeded maximum of %d for stream id %d",
serverConfig.getMaxContentLength(), 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) {
if (serverConfig.isDebug()) {
logger.log(Level.FINE, () -> "rst stream received: error code = " + errorCode);
}
Http2Stream stream = connection.stream(streamId);
FullHttpMessage msg = getMessage(stream);
if (msg != null) {
removeMessage(stream, true);
}
}
/**
* 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) {
if (serverConfig.isDebug()) {
logger.log(Level.FINE, () -> "push promise received: streamId " + streamId +
" promised stream ID = " + promisedStreamId + " headers =" + headers);
}
throw new IllegalStateException("server is not allowd to receive push promise");
}
/**
* 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) {
if (serverConfig.isDebug()) {
logger.log(Level.FINE, () -> "stream removed " + stream);
}
removeMessage(stream, true);
}
/**
* 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();
}
}
private FullHttpMessage beginHeader(ChannelHandlerContext ctx, Http2Stream stream, Http2Headers headers) throws Http2Exception {
FullHttpMessage msg = getMessage(stream);
if (msg == null) {
msg = newMessage(stream, headers, validateHttpHeaders, ctx.alloc());
} else {
addHttp2ToHttpHeaders(stream.id(), headers, msg.headers(), msg.protocolVersion(),
true, msg instanceof HttpRequest);
}
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);
}
}
/**
* 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);
}
/**
* 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 {
FullHttpMessage fullHttpMessage = toFullHttpRequest(stream.id(), headers, alloc, validateHttpHeaders);
if (serverConfig.isDebug()) {
logger.log(Level.FINE, headers.toString());
logger.log(Level.FINE, fullHttpMessage::toString);
}
return fullHttpMessage;
}
/**
* Create a new object to contain the request data
*
* @param streamId The stream associated with the request
* @param http2Headers The initial set of HTTP/2 headers to create the request with
* @param alloc The {@link ByteBufAllocator} to use to generate the content of the message
* @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>
* @return A new request object which represents headers/data
* @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x.
*/
public static FullHttpRequest toFullHttpRequest(int streamId, Http2Headers http2Headers,
ByteBufAllocator alloc,
boolean validateHttpHeaders)
throws Http2Exception {
final CharSequence method = ObjectUtil.checkNotNull(http2Headers.method(),"method header cannot be null");
final CharSequence path = ObjectUtil.checkNotNull(http2Headers.path(),"path header cannot be null ");
ByteBuf byteBuf = alloc.buffer();
FullHttpRequest msg = new DefaultFullHttpRequest(HTTP_2_0, HttpMethod.valueOf(method.toString()),
path.toString(), byteBuf, validateHttpHeaders);
try {
addHttp2ToHttpHeaders(streamId, http2Headers, msg.headers(), msg.protocolVersion(), false, true);
} catch (Http2Exception e) {
msg.release();
throw e;
} catch (Throwable t) {
msg.release();
throw Http2Exception.streamError(streamId, Http2Error.PROTOCOL_ERROR, t, "HTTP/2 full request conversion error");
}
return msg;
}
/**
* Translate and add HTTP/2 headers to HTTP/1.x headers.
*
* @param streamId The stream associated with {@code sourceHeaders}.
* @param inputHeaders The HTTP/2 headers to convert.
* @param outputHeaders The object which will contain the resulting HTTP/1.x headers..
* @param httpVersion What HTTP/1.x version {@code outputHeaders} should be treated as when doing the conversion.
* @param isTrailer {@code true} if {@code outputHeaders} should be treated as trailing headers.
* {@code false} otherwise.
* @param isRequest {@code true} if the {@code outputHeaders} will be used in a request message.
* {@code false} for response message.
* @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x.
*/
public static void addHttp2ToHttpHeaders(int streamId, Http2Headers inputHeaders,
HttpHeaders outputHeaders,
HttpVersion httpVersion,
boolean isTrailer,
boolean isRequest) throws Http2Exception {
final CharSequenceMap<AsciiString> translations = isRequest ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
try {
for (Map.Entry<CharSequence, CharSequence> entry : inputHeaders) {
final CharSequence name = entry.getKey();
final CharSequence value = entry.getValue();
AsciiString translatedName = translations.get(name);
if (translatedName != null) {
outputHeaders.add(translatedName, AsciiString.of(value));
} else if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
// https://tools.ietf.org/html/rfc7540#section-8.1.2.3
// All headers that start with ':' are only valid in HTTP/2 context
if (name.length() == 0 || name.charAt(0) == ':') {
throw Http2Exception.streamError(streamId, Http2Error.PROTOCOL_ERROR,
"Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", name);
}
if (HttpHeaderNames.COOKIE.equals(name)) {
// combine the cookie values into 1 header entry.
// https://tools.ietf.org/html/rfc7540#section-8.1.2.5
String existingCookie = outputHeaders.get(HttpHeaderNames.COOKIE);
outputHeaders.set(HttpHeaderNames.COOKIE,
(existingCookie != null) ? (existingCookie + "; " + value) : value);
} else {
outputHeaders.add(name, value);
}
}
}
} catch (Http2Exception ex) {
throw ex;
} catch (Throwable t) {
throw Http2Exception.streamError(streamId, Http2Error.PROTOCOL_ERROR, t, "HTTP/2 headers conversion error");
}
outputHeaders.remove(HttpHeaderNames.TRANSFER_ENCODING);
outputHeaders.remove(HttpHeaderNames.TRAILER);
if (!isTrailer) {
outputHeaders.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), streamId);
HttpUtil.setKeepAlive(outputHeaders, httpVersion, true);
}
}
/**
* Translations from HTTP/2 header name to the HTTP/1.x equivalent.
*/
private static final CharSequenceMap<AsciiString>
REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
private static final CharSequenceMap<AsciiString>
RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap<AsciiString>();
static {
RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
HttpHeaderNames.HOST);
RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.SCHEME.value(),
HttpConversionUtil.ExtensionHeaderNames.SCHEME.text());
REQUEST_HEADER_TRANSLATIONS.add(RESPONSE_HEADER_TRANSLATIONS);
RESPONSE_HEADER_TRANSLATIONS.add(Http2Headers.PseudoHeaderName.PATH.value(),
HttpConversionUtil.ExtensionHeaderNames.PATH.text());
}
}

View file

@ -0,0 +1,33 @@
package org.xbib.netty.http.server.handler.http2;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpRequest;
import org.xbib.netty.http.server.transport.ServerTransport;
import java.io.IOException;
@ChannelHandler.Sharable
public class Http2RequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest httpRequest) throws IOException {
ServerTransport transport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get();
transport.requestReceived(ctx, httpRequest);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) {
ctx.fireChannelInactive();
ServerTransport transport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get();
//transport.fail(new IOException("channel closed"));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ServerTransport transport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get();
//transport.fail(cause);
ctx.channel().close();
}
}

View file

@ -0,0 +1,18 @@
package org.xbib.netty.http.server.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.server.transport.ServerTransport;
@ChannelHandler.Sharable
public class Http2SettingsHandler extends SimpleChannelInboundHandler<Http2Settings> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, Http2Settings http2Settings) throws Exception {
ServerTransport transport = ctx.channel().attr(ServerTransport.TRANSPORT_ATTRIBUTE_KEY).get();
transport.settingsReceived(ctx, http2Settings);
ctx.pipeline().remove(this);
}
}

View file

@ -0,0 +1,23 @@
package org.xbib.netty.http.server.handler.http2;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A Netty handler that logs user events.
*/
@ChannelHandler.Sharable
public class UserEventLogger extends ChannelInboundHandlerAdapter {
private static final Logger logger = Logger.getLogger(UserEventLogger.class.getName());
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
logger.log(Level.FINE, () -> "got user event " + evt);
ctx.fireUserEventTriggered(evt);
}
}

Some files were not shown because too many files have changed in this diff Show more