add Netty server
This commit is contained in:
parent
12a5f6418c
commit
f2c483fcfa
135 changed files with 7122 additions and 223 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -8,6 +8,6 @@
|
|||
/.classpath
|
||||
/.project
|
||||
/.gradle
|
||||
/build
|
||||
build
|
||||
out
|
||||
*~
|
63
build.gradle
63
build.gradle
|
@ -21,11 +21,13 @@ printf "Date: %s\nHost: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGradle: %s Groovy: %
|
|||
System.getProperty("java.vm.name"),
|
||||
gradle.gradleVersion, GroovySystem.getVersion(), JavaVersion.current()
|
||||
|
||||
apply plugin: "io.codearte.nexus-staging"
|
||||
|
||||
subprojects {
|
||||
apply plugin: 'java'
|
||||
apply plugin: 'maven'
|
||||
apply plugin: 'signing'
|
||||
apply plugin: "com.github.spotbugs"
|
||||
apply plugin: "io.codearte.nexus-staging"
|
||||
apply plugin: 'org.xbib.gradle.plugin.asciidoctor'
|
||||
|
||||
configurations {
|
||||
|
@ -35,14 +37,6 @@ configurations {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
compile "org.xbib:net-url:${project.property('xbib-net-url.version')}"
|
||||
compile "io.netty:netty-codec-http2:${project.property('netty.version')}"
|
||||
compile "io.netty:netty-handler-proxy:${project.property('netty.version')}"
|
||||
compile "io.netty:netty-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')}"
|
||||
|
@ -123,30 +117,6 @@ if (project.hasProperty('signing.keyId')) {
|
|||
}
|
||||
|
||||
|
||||
spotbugs {
|
||||
effort = "max"
|
||||
reportLevel = "low"
|
||||
//includeFilter = file("findbugs-exclude.xml")
|
||||
}
|
||||
|
||||
tasks.withType(com.github.spotbugs.SpotBugsTask) {
|
||||
ignoreFailures = true
|
||||
reports {
|
||||
xml.enabled = false
|
||||
html.enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
sonarqube {
|
||||
properties {
|
||||
property "sonar.projectName", "${project.group} ${project.name}"
|
||||
property "sonar.sourceEncoding", "UTF-8"
|
||||
property "sonar.tests", "src/test/java"
|
||||
property "sonar.scm.provider", "git"
|
||||
property "sonar.junit.reportsPath", "build/test-results/test/"
|
||||
}
|
||||
}
|
||||
|
||||
ext {
|
||||
user = 'jprante'
|
||||
name = 'netty-http-client'
|
||||
|
@ -225,6 +195,33 @@ task sonaTypeUpload(type: Upload) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
spotbugs {
|
||||
effort = "max"
|
||||
reportLevel = "low"
|
||||
//includeFilter = file("findbugs-exclude.xml")
|
||||
}
|
||||
|
||||
tasks.withType(com.github.spotbugs.SpotBugsTask) {
|
||||
ignoreFailures = true
|
||||
reports {
|
||||
xml.enabled = false
|
||||
html.enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
sonarqube {
|
||||
properties {
|
||||
property "sonar.projectName", "${project.group} ${project.name}"
|
||||
property "sonar.sourceEncoding", "UTF-8"
|
||||
property "sonar.tests", "src/test/java"
|
||||
property "sonar.scm.provider", "git"
|
||||
property "sonar.junit.reportsPath", "build/test-results/test/"
|
||||
}
|
||||
}
|
||||
|
||||
nexusStaging {
|
||||
packageGroup = "org.xbib"
|
||||
}
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
group = org.xbib
|
||||
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
|
||||
conscrypt.version = 1.0.1
|
||||
bouncycastle.version = 1.57
|
||||
xbib-net-url.version = 1.1.0
|
||||
alpnagent.version = 2.0.7
|
||||
junit.version = 4.12
|
||||
jackson.version = 2.8.11.1
|
||||
asciidoclet.version = 1.6.0.0
|
||||
wagon.version = 3.0.0
|
||||
|
||||
|
||||
|
|
10
netty-http-client/build.gradle
Normal file
10
netty-http-client/build.gradle
Normal 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')}"
|
||||
}
|
|
@ -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.pool.BoundedChannelPool;
|
||||
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.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.SNIServerName;
|
||||
|
@ -213,13 +214,13 @@ public final class Client {
|
|||
Transport transport = null;
|
||||
if (httpAddress != null) {
|
||||
if (httpAddress.getVersion().majorVersion() == 1) {
|
||||
transport = new HttpTransport(this, httpAddress);
|
||||
transport = new Http1Transport(this, httpAddress);
|
||||
} else {
|
||||
transport = new Http2Transport(this, httpAddress);
|
||||
}
|
||||
} else if (hasPooledConnections()) {
|
||||
if (pool.getVersion().majorVersion() == 1) {
|
||||
transport = new HttpTransport(this, null);
|
||||
transport = new Http1Transport(this, null);
|
||||
} else {
|
||||
transport = new Http2Transport(this, null);
|
||||
}
|
||||
|
@ -234,7 +235,7 @@ public final class Client {
|
|||
}
|
||||
|
||||
public Channel newChannel(HttpAddress httpAddress) throws IOException {
|
||||
Channel channel = null;
|
||||
Channel channel;
|
||||
if (httpAddress != null) {
|
||||
HttpVersion httpVersion = httpAddress.getVersion();
|
||||
ChannelInitializer<SocketChannel> initializer;
|
||||
|
@ -285,14 +286,14 @@ public final class Client {
|
|||
}
|
||||
|
||||
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);
|
||||
return transport;
|
||||
}
|
||||
|
||||
public <T> CompletableFuture<T> execute(Request request,
|
||||
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 {
|
||||
|
@ -307,7 +308,7 @@ public final class Client {
|
|||
* @param request the new request for continuing the request.
|
||||
*/
|
||||
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.execute(request);
|
||||
nextTransport.get();
|
||||
|
@ -328,7 +329,7 @@ public final class Client {
|
|||
}
|
||||
|
||||
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 {
|
|
@ -4,12 +4,12 @@ import io.netty.buffer.ByteBufAllocator;
|
|||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.WriteBufferWaterMark;
|
||||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.handler.codec.http2.Http2Settings;
|
||||
import io.netty.handler.proxy.HttpProxyHandler;
|
||||
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 javax.net.ssl.TrustManagerFactory;
|
||||
import java.io.InputStream;
|
|
@ -11,6 +11,7 @@ import io.netty.handler.ssl.CipherSuiteFilter;
|
|||
import io.netty.handler.ssl.SslProvider;
|
||||
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
|
||||
import org.xbib.netty.http.client.retry.BackOff;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
import java.io.InputStream;
|
|
@ -3,9 +3,6 @@ package org.xbib.netty.http.client;
|
|||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
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.HttpHeaderNames;
|
||||
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.URLSyntaxException;
|
||||
import org.xbib.netty.http.client.retry.BackOff;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
|
@ -9,7 +9,7 @@ import io.netty.handler.codec.http.HttpObjectAggregator;
|
|||
import io.netty.handler.logging.LogLevel;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
import org.xbib.netty.http.client.ClientConfig;
|
||||
import org.xbib.netty.http.client.HttpAddress;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* HTTP handlers for Netty HTTP client.
|
||||
*/
|
||||
package org.xbib.netty.http.client.handler.http1;
|
|
@ -9,11 +9,13 @@ import io.netty.handler.codec.http2.Http2ConnectionHandler;
|
|||
import io.netty.handler.codec.http2.Http2FrameLogger;
|
||||
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
|
||||
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
|
||||
import io.netty.handler.logging.LogLevel;
|
||||
import io.netty.handler.ssl.ApplicationProtocolNames;
|
||||
import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler;
|
||||
import io.netty.handler.ssl.SslHandler;
|
||||
import org.xbib.netty.http.client.ClientConfig;
|
||||
import org.xbib.netty.http.client.HttpAddress;
|
||||
import org.xbib.netty.http.client.handler.http1.TrafficLoggingHandler;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
@ -51,13 +53,16 @@ public class Http2ChannelInitializer extends ChannelInitializer<SocketChannel> {
|
|||
*/
|
||||
@Override
|
||||
public void initChannel(SocketChannel channel) {
|
||||
if (clientConfig.isDebug()) {
|
||||
channel.pipeline().addLast(new TrafficLoggingHandler(LogLevel.DEBUG));
|
||||
}
|
||||
if (httpAddress.isSecure()) {
|
||||
configureEncrypted(channel);
|
||||
} else {
|
||||
configureCleartext(channel);
|
||||
}
|
||||
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)
|
||||
.build()));
|
||||
if (clientConfig.isDebug()) {
|
||||
http2ConnectionHandlerBuilder.frameLogger(new Http2FrameLogger(clientConfig.getDebugLogLevel(), "client"));
|
||||
Http2FrameLogger http2FrameLogger = new Http2FrameLogger(clientConfig.getDebugLogLevel(), "client");
|
||||
http2ConnectionHandlerBuilder.frameLogger(http2FrameLogger);
|
||||
}
|
||||
return http2ConnectionHandlerBuilder.build();
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
/**
|
||||
* HTTP/2 handlers for Netty HTTP client.
|
||||
*/
|
||||
package org.xbib.netty.http.client.handler.http2;
|
|
@ -9,6 +9,7 @@ import io.netty.channel.pool.ChannelPoolHandler;
|
|||
import io.netty.channel.socket.SocketChannel;
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import io.netty.util.AttributeKey;
|
||||
import org.xbib.netty.http.common.PoolKey;
|
||||
|
||||
import java.net.ConnectException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -145,11 +146,12 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
|
|||
if (channel == null) {
|
||||
semaphore.release();
|
||||
throw new ConnectException();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (channelPoolhandler != null) {
|
||||
channelPoolhandler.channelAcquired(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
|
@ -165,7 +167,7 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
|
|||
}
|
||||
Channel channel;
|
||||
for (int i = 0; i < availableCount; i ++) {
|
||||
if (null == (channel = poll())) {
|
||||
if ((channel = poll()) == null) {
|
||||
channel = newConnection();
|
||||
}
|
||||
if (channel == null) {
|
||||
|
@ -193,6 +195,8 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
|
|||
}
|
||||
} else if (channel.isOpen()) {
|
||||
channel.close();
|
||||
} else {
|
||||
logger.log(Level.WARNING, "channel not active or open while release");
|
||||
}
|
||||
if (channelPoolhandler != null) {
|
||||
channelPoolhandler.channelReleased(channel);
|
||||
|
@ -319,12 +323,23 @@ public class BoundedChannelPool<K extends PoolKey> implements Pool<Channel> {
|
|||
Queue<Channel> channelQueue;
|
||||
Channel channel;
|
||||
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) {
|
||||
channel = channelQueue.poll();
|
||||
if (channel != null && channel.isActive()) {
|
||||
return channel;
|
||||
}
|
||||
} else {
|
||||
logger.log(Level.FINE, "channelqueue is null");
|
||||
}
|
||||
}
|
||||
return null;
|
|
@ -5,7 +5,7 @@ import io.netty.handler.codec.http.FullHttpResponse;
|
|||
import io.netty.handler.codec.http.HttpMethod;
|
||||
import org.xbib.net.URL;
|
||||
import org.xbib.netty.http.client.Client;
|
||||
import org.xbib.netty.http.client.HttpAddress;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
import org.xbib.netty.http.client.Request;
|
||||
import org.xbib.netty.http.client.RequestBuilder;
|
||||
import org.xbib.netty.http.client.transport.Transport;
|
|
@ -15,7 +15,7 @@ import org.xbib.net.PercentDecoder;
|
|||
import org.xbib.net.URL;
|
||||
import org.xbib.net.URLSyntaxException;
|
||||
import org.xbib.netty.http.client.Client;
|
||||
import org.xbib.netty.http.client.HttpAddress;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
import org.xbib.netty.http.client.Request;
|
||||
import org.xbib.netty.http.client.RequestBuilder;
|
||||
import org.xbib.netty.http.client.listener.CookieListener;
|
||||
|
@ -99,6 +99,7 @@ abstract class BaseTransport implements Transport {
|
|||
if (streamId != null) {
|
||||
requests.put(streamId, request);
|
||||
}
|
||||
// flush after putting request into requests map
|
||||
if (channel.isWritable()) {
|
||||
channel.writeAndFlush(fullHttpRequest);
|
||||
|
|
@ -6,7 +6,7 @@ import io.netty.handler.codec.http2.Http2Headers;
|
|||
import io.netty.handler.codec.http2.Http2Settings;
|
||||
import org.xbib.net.URLSyntaxException;
|
||||
import org.xbib.netty.http.client.Client;
|
||||
import org.xbib.netty.http.client.HttpAddress;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
import org.xbib.netty.http.client.Request;
|
||||
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.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 SortedMap<Integer, CompletableFuture<Boolean>> sequentialPromiseMap;
|
||||
|
||||
public HttpTransport(Client client, HttpAddress httpAddress) {
|
||||
public Http1Transport(Client client, HttpAddress httpAddress) {
|
||||
super(client, httpAddress);
|
||||
this.sequentialCounter = new AtomicInteger();
|
||||
this.sequentialPromiseMap = new ConcurrentSkipListMap<>();
|
|
@ -6,7 +6,7 @@ import io.netty.handler.codec.http2.Http2Headers;
|
|||
import io.netty.handler.codec.http2.Http2Settings;
|
||||
import org.xbib.net.URLSyntaxException;
|
||||
import org.xbib.netty.http.client.Client;
|
||||
import org.xbib.netty.http.client.HttpAddress;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
import org.xbib.netty.http.client.Request;
|
||||
import org.xbib.netty.http.client.listener.HttpResponseListener;
|
||||
|
||||
|
@ -35,7 +35,7 @@ public class Http2Transport extends BaseTransport {
|
|||
super(client, httpAddress);
|
||||
streamIdCounter = new AtomicInteger(3);
|
||||
streamidPromiseMap = new ConcurrentSkipListMap<>();
|
||||
settingsPromise = (httpAddress != null && httpAddress.isSecure()) ||
|
||||
settingsPromise = (httpAddress != null /*&& httpAddress.isSecure() */) ||
|
||||
(client.hasPooledConnections() && client.getPool().isSecure()) ?
|
||||
new CompletableFuture<>() : null;
|
||||
}
|
||||
|
@ -65,10 +65,16 @@ public class Http2Transport extends BaseTransport {
|
|||
public void awaitSettings() {
|
||||
if (settingsPromise != null) {
|
||||
try {
|
||||
logger.log(Level.FINE, "waiting for settings");
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
logger.log(Level.WARNING, "settings promise is null");
|
||||
}
|
||||
}
|
||||
|
|
@ -3,7 +3,7 @@ package org.xbib.netty.http.client.test;
|
|||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.xbib.netty.http.client.Client;
|
||||
import org.xbib.netty.http.client.HttpAddress;
|
||||
import org.xbib.netty.http.common.HttpAddress;
|
||||
import org.xbib.netty.http.client.Request;
|
||||
|
||||
import java.io.IOException;
|
|
@ -4,7 +4,7 @@ import io.netty.handler.codec.http.HttpVersion;
|
|||
import org.junit.Test;
|
||||
import org.xbib.net.URL;
|
||||
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 java.io.IOException;
|
||||
|
@ -24,8 +24,8 @@ public class PooledClientTest extends LoggingBase {
|
|||
public void testPooledClientWithSingleNode() throws IOException {
|
||||
int loop = 10;
|
||||
int threads = Runtime.getRuntime().availableProcessors();
|
||||
URL url = URL.from("http://xbib.org");
|
||||
HttpAddress httpAddress = HttpAddress.of(url, HttpVersion.HTTP_1_1);
|
||||
URL url = URL.from("https://fl-test.hbz-nrw.de/app/fl");
|
||||
HttpAddress httpAddress = HttpAddress.of(url, HttpVersion.valueOf("HTTP/2.0"));
|
||||
Client client = Client.builder()
|
||||
.addPoolNode(httpAddress)
|
||||
.setPoolNodeConnectionLimit(threads)
|
|
@ -18,7 +18,7 @@ import org.junit.After;
|
|||
import org.junit.Before;
|
||||
import org.junit.Ignore;
|
||||
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.BoundedChannelPool;
|
||||
|
|
@ -16,7 +16,7 @@ import io.netty.handler.codec.http.HttpVersion;
|
|||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
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.BoundedChannelPool;
|
||||
|
|
@ -7,7 +7,7 @@ import io.netty.util.AttributeKey;
|
|||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
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.Pool;
|
||||
|
5
netty-http-common/build.gradle
Normal file
5
netty-http-common/build.gradle
Normal 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')}"
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
package org.xbib.netty.http.client;
|
||||
package org.xbib.netty.http.common;
|
||||
|
||||
import io.netty.handler.codec.http.HttpVersion;
|
||||
import org.xbib.net.URL;
|
||||
import org.xbib.netty.http.client.pool.PoolKey;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
|
@ -55,14 +54,14 @@ public class HttpAddress implements PoolKey {
|
|||
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) {
|
||||
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) {
|
||||
this(url.getHost(), url.getPort(), version, "https".equals(url.getScheme()));
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package org.xbib.netty.http.client.util;
|
||||
package org.xbib.netty.http.common;
|
||||
|
||||
/**
|
||||
* The network classes.
|
|
@ -1,4 +1,4 @@
|
|||
package org.xbib.netty.http.client.util;
|
||||
package org.xbib.netty.http.common;
|
||||
|
||||
/**
|
||||
* The TCP/IP network protocol versions.
|
|
@ -1,4 +1,4 @@
|
|||
package org.xbib.netty.http.client.util;
|
||||
package org.xbib.netty.http.common;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.Inet4Address;
|
|
@ -1,4 +1,4 @@
|
|||
package org.xbib.netty.http.client.pool;
|
||||
package org.xbib.netty.http.common;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
/**
|
||||
* Utilities for Netty HTTP client.
|
||||
*/
|
||||
package org.xbib.netty.http.client.util;
|
||||
package org.xbib.netty.http.common;
|
9
netty-http-server/build.gradle
Normal file
9
netty-http-server/build.gradle
Normal 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')}"
|
||||
}
|
684
netty-http-server/src/docs/asciidoc/css/foundation.css
vendored
Normal file
684
netty-http-server/src/docs/asciidoc/css/foundation.css
vendored
Normal 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; }
|
10
netty-http-server/src/docs/asciidoc/index.adoc
Normal file
10
netty-http-server/src/docs/asciidoc/index.adoc
Normal 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
|
3
netty-http-server/src/docs/asciidoclet/overview.adoc
Normal file
3
netty-http-server/src/docs/asciidoclet/overview.adoc
Normal file
|
@ -0,0 +1,3 @@
|
|||
= Netty HTTP server
|
||||
Jörg Prante
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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))));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in a new issue