From f2c483fcfab9c2e17119491f1018d5de195acdbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Mon, 12 Mar 2018 11:22:40 +0100 Subject: [PATCH] add Netty server --- .gitignore | 2 +- build.gradle | 331 +++++---- gradle.properties | 7 +- netty-http-client/build.gradle | 10 + .../src}/docs/asciidoc/css/foundation.css | 0 .../src}/docs/asciidoc/index.adoc | 0 .../src}/docs/asciidoclet/overview.adoc | 0 .../org/xbib/netty/http/client/Client.java | 19 +- .../netty/http/client/ClientAuthMode.java | 0 .../xbib/netty/http/client/ClientBuilder.java | 2 +- .../xbib/netty/http/client/ClientConfig.java | 1 + .../org/xbib/netty/http/client/Request.java | 0 .../netty/http/client/RequestBuilder.java | 4 +- .../org/xbib/netty/http/client/UserAgent.java | 0 .../handler/http1/HttpChannelInitializer.java | 2 +- .../http1/HttpChunkContentCompressor.java | 0 .../handler/http1/HttpResponseHandler.java | 0 .../handler/http1/TrafficLoggingHandler.java | 0 .../client/handler/http1/package-info.java | 4 + .../http2/Http2ChannelInitializer.java | 12 +- .../http2/Http2PushPromiseHandler.java | 0 .../handler/http2/Http2ResponseHandler.java | 0 .../handler/http2/Http2SettingsHandler.java | 0 .../client/handler/http2/package-info.java | 4 + .../http/client/listener/CookieListener.java | 0 .../client/listener/ExceptionListener.java | 0 .../client/listener/HttpHeadersListener.java | 0 .../client/listener/HttpResponseListener.java | 0 .../http/client/listener/package-info.java | 0 .../xbib/netty/http/client/package-info.java | 0 .../http/client/pool/BoundedChannelPool.java | 25 +- .../org/xbib/netty/http/client/pool/Pool.java | 0 .../netty/http/client/rest/RestClient.java | 2 +- .../xbib/netty/http/client/retry/BackOff.java | 0 .../http/client/retry/ExponentialBackOff.java | 0 .../http/client/transport/BaseTransport.java | 3 +- .../http/client/transport/Http1Transport.java | 8 +- .../http/client/transport/Http2Transport.java | 12 +- .../http/client/transport/Transport.java | 0 .../http/client/transport/package-info.java | 0 .../client/test/CompletableFutureTest.java | 0 .../netty/http/client/test/ConscryptTest.java | 0 .../client/test/CookieSetterHttpBinTest.java | 0 .../http/client/test/ElasticsearchTest.java | 2 +- .../netty/http/client/test/Http1Test.java | 0 .../netty/http/client/test/Http2Test.java | 0 .../xbib/netty/http/client/test/LeakTest.java | 0 .../netty/http/client/test/LoggingBase.java | 0 .../http/client/test/PooledClientTest.java | 6 +- .../http/client/test/RequestBuilderTest.java | 0 .../http/client/test/SecureHttp1Test.java | 0 .../xbib/netty/http/client/test/URITest.java | 0 .../xbib/netty/http/client/test/XbibTest.java | 0 .../netty/http/client/test/package-info.java | 0 .../http/client/test/pool/EpollTest.java | 2 +- .../client/test/pool/MockEpollServer.java | 0 .../http/client/test/pool/MockNioServer.java | 0 .../netty/http/client/test/pool/NioTest.java | 2 +- .../netty/http/client/test/pool/PoolTest.java | 2 +- .../http/client/test/rest/RestClientTest.java | 0 .../test/retry/ExponentialBackOffTest.java | 0 .../http/client/test/retry/MockBackOff.java | 0 .../client/test/retry/MockBackOffTest.java | 0 .../client/test/simple/Http2FramesTest.java | 0 .../client/test/simple/SimpleHttp1Test.java | 0 .../client/test/simple/SimpleHttp2Test.java | 0 netty-http-common/build.gradle | 5 + .../xbib/netty/http/common}/HttpAddress.java | 11 +- .../xbib/netty/http/common}/NetworkClass.java | 2 +- .../http/common}/NetworkProtocolVersion.java | 2 +- .../xbib/netty/http/common}/NetworkUtils.java | 2 +- .../org/xbib/netty/http/common}/PoolKey.java | 2 +- .../xbib/netty/http/common}/package-info.java | 2 +- netty-http-server/build.gradle | 9 + .../src/docs/asciidoc/css/foundation.css | 684 ++++++++++++++++++ .../src/docs/asciidoc/index.adoc | 10 + .../src/docs/asciidoclet/overview.adoc | 3 + .../org/xbib/netty/http/server/Server.java | 296 ++++++++ .../xbib/netty/http/server/ServerBuilder.java | 235 ++++++ .../xbib/netty/http/server/ServerConfig.java | 541 ++++++++++++++ .../xbib/netty/http/server/ServerName.java | 44 ++ .../netty/http/server/context/Context.java | 33 + .../http/server/context/ContextHandler.java | 25 + .../http/server/context/ContextInfo.java | 43 ++ .../server/context/MethodContextHandler.java | 46 ++ .../http/server/context/VirtualServer.java | 187 +++++ .../handler/HttpServerChannelInitializer.java | 220 ++++++ .../handler/http1/HttpChannelInitializer.java | 75 ++ .../server/handler/http1/HttpHandler.java | 55 ++ .../handler/http1/IdleTimeoutHandler.java | 32 + .../handler/http1/TrafficLoggingHandler.java | 43 ++ .../server/handler/http2/DummyHandler.java | 17 + .../server/handler/http2/FrameListener.java | 33 + .../handler/http2/HelloWorldHttp1Handler.java | 71 ++ .../handler/http2/HelloWorldHttp2Handler.java | 87 +++ .../http2/Http2ChannelInitializer.java | 251 +++++++ .../server/handler/http2/Http2Handler.java | 483 +++++++++++++ .../handler/http2/Http2RequestHandler.java | 33 + .../handler/http2/Http2SettingsHandler.java | 18 + .../server/handler/http2/UserEventLogger.java | 23 + .../http/server/handler/package-info.java | 4 + .../internal/ClosedSessionException.java | 7 + .../server/internal/Http1ObjectEncoder.java | 192 +++++ .../server/internal/Http2ObjectEncoder.java | 167 +++++ .../server/internal/HttpObjectEncoder.java | 77 ++ .../http/server/internal/package-info.java | 4 + .../xbib/netty/http/server/package-info.java | 4 + .../security/tls/SelfSignedCertificate.java | 167 +++++ .../server/transport/BaseServerTransport.java | 106 +++ .../server/transport/Http1ServerResponse.java | 175 +++++ .../transport/Http1ServerTransport.java | 39 + .../transport/Http2ServerTransport.java | 44 ++ .../http/server/transport/ServerRequest.java | 50 ++ .../http/server/transport/ServerResponse.java | 29 + .../server/transport/ServerTransport.java | 18 + .../netty/http/server/util/NetworkClass.java | 9 + .../server/util/NetworkProtocolVersion.java | 9 + .../netty/http/server/util/NetworkUtils.java | 586 +++++++++++++++ .../netty/http/server/util/package-info.java | 4 + .../http/server/test/CleartextHttp1Test.java | 61 ++ .../http/server/test/CleartextHttp2Test.java | 73 ++ .../netty/http/server/test/LoggingBase.java | 26 + .../test/MultithreadedCleartextHttp2Test.java | 92 +++ .../server/test/PooledCleartextHttp1Test.java | 63 ++ .../http/server/test/SecureHttp1Test.java | 53 ++ .../http/server/test/SecureHttp2Test.java | 76 ++ .../test/SelfSignedCertificateTest.java | 20 + .../netty/http/server/test/ServerTest.java | 20 + .../MultithreadedCleartextHttp2Test.java | 241 ++++++ ...eadedMultiplexCodecCleartextHttp2Test.java | 276 +++++++ .../netty/http/server/test/package-info.java | 4 + .../test/simple/CleartextHttp2Test.java | 246 +++++++ .../MultiplexCodecCleartextHttp2Test.java | 311 ++++++++ settings.gradle | 3 + .../http/client/handler/package-info.java | 4 - 135 files changed, 7122 insertions(+), 223 deletions(-) create mode 100644 netty-http-client/build.gradle rename {src => netty-http-client/src}/docs/asciidoc/css/foundation.css (100%) rename {src => netty-http-client/src}/docs/asciidoc/index.adoc (100%) rename {src => netty-http-client/src}/docs/asciidoclet/overview.adoc (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/Client.java (97%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/ClientAuthMode.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/ClientBuilder.java (99%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/ClientConfig.java (99%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/Request.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/RequestBuilder.java (98%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/UserAgent.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java (98%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/handler/http1/HttpChunkContentCompressor.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/handler/http1/HttpResponseHandler.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/handler/http1/TrafficLoggingHandler.java (100%) create mode 100644 netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/package-info.java rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java (87%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/handler/http2/Http2PushPromiseHandler.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/handler/http2/Http2SettingsHandler.java (100%) create mode 100644 netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/package-info.java rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/listener/CookieListener.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/listener/package-info.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/package-info.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java (93%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/pool/Pool.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/rest/RestClient.java (98%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/retry/BackOff.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/retry/ExponentialBackOff.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/transport/BaseTransport.java (99%) rename src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java => netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http1Transport.java (95%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/transport/Http2Transport.java (93%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/transport/Transport.java (100%) rename {src => netty-http-client/src}/main/java/org/xbib/netty/http/client/transport/package-info.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/ConscryptTest.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/CookieSetterHttpBinTest.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java (99%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/Http1Test.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/Http2Test.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/LeakTest.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/LoggingBase.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/PooledClientTest.java (95%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/RequestBuilderTest.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/SecureHttp1Test.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/URITest.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/XbibTest.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/package-info.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java (99%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/pool/MockEpollServer.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/pool/MockNioServer.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/pool/NioTest.java (99%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java (99%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/rest/RestClientTest.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/retry/ExponentialBackOffTest.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/retry/MockBackOff.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/retry/MockBackOffTest.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/simple/Http2FramesTest.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp1Test.java (100%) rename {src => netty-http-client/src}/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp2Test.java (100%) create mode 100644 netty-http-common/build.gradle rename {src/main/java/org/xbib/netty/http/client => netty-http-common/src/main/java/org/xbib/netty/http/common}/HttpAddress.java (94%) rename {src/main/java/org/xbib/netty/http/client/util => netty-http-common/src/main/java/org/xbib/netty/http/common}/NetworkClass.java (70%) rename {src/main/java/org/xbib/netty/http/client/util => netty-http-common/src/main/java/org/xbib/netty/http/common}/NetworkProtocolVersion.java (74%) rename {src/main/java/org/xbib/netty/http/client/util => netty-http-common/src/main/java/org/xbib/netty/http/common}/NetworkUtils.java (99%) rename {src/main/java/org/xbib/netty/http/client/pool => netty-http-common/src/main/java/org/xbib/netty/http/common}/PoolKey.java (73%) rename {src/main/java/org/xbib/netty/http/client/util => netty-http-common/src/main/java/org/xbib/netty/http/common}/package-info.java (51%) create mode 100644 netty-http-server/build.gradle create mode 100644 netty-http-server/src/docs/asciidoc/css/foundation.css create mode 100644 netty-http-server/src/docs/asciidoc/index.adoc create mode 100644 netty-http-server/src/docs/asciidoclet/overview.adoc create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/ServerBuilder.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/ServerConfig.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/ServerName.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/context/Context.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/context/ContextHandler.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/context/ContextInfo.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/context/MethodContextHandler.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/context/VirtualServer.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/HttpServerChannelInitializer.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpChannelInitializer.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpHandler.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/IdleTimeoutHandler.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/TrafficLoggingHandler.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/DummyHandler.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/FrameListener.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp1Handler.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp2Handler.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2Handler.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2RequestHandler.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2SettingsHandler.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/UserEventLogger.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/package-info.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/internal/ClosedSessionException.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http1ObjectEncoder.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http2ObjectEncoder.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/internal/HttpObjectEncoder.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/internal/package-info.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/package-info.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/security/tls/SelfSignedCertificate.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseServerTransport.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerResponse.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerTransport.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerTransport.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerRequest.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerResponse.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerTransport.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkClass.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkProtocolVersion.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkUtils.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/util/package-info.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/LoggingBase.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/MultithreadedCleartextHttp2Test.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/PooledCleartextHttp1Test.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/SelfSignedCertificateTest.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedCleartextHttp2Test.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedMultiplexCodecCleartextHttp2Test.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/package-info.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/CleartextHttp2Test.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/MultiplexCodecCleartextHttp2Test.java create mode 100644 settings.gradle delete mode 100644 src/main/java/org/xbib/netty/http/client/handler/package-info.java diff --git a/.gitignore b/.gitignore index a8f6dba..3d0280a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,6 @@ /.classpath /.project /.gradle -/build +build out *~ \ No newline at end of file diff --git a/build.gradle b/build.gradle index 0f42c9e..48a49ed 100644 --- a/build.gradle +++ b/build.gradle @@ -21,107 +21,182 @@ printf "Date: %s\nHost: %s\nOS: %s %s %s\nJVM: %s %s %s %s\nGradle: %s Groovy: % System.getProperty("java.vm.name"), gradle.gradleVersion, GroovySystem.getVersion(), JavaVersion.current() -apply plugin: 'java' -apply plugin: 'maven' -apply plugin: 'signing' -apply plugin: "com.github.spotbugs" apply plugin: "io.codearte.nexus-staging" -apply plugin: 'org.xbib.gradle.plugin.asciidoctor' -configurations { - alpnagent - asciidoclet - wagon -} +subprojects { + apply plugin: 'java' + apply plugin: 'maven' + apply plugin: 'signing' + apply plugin: "com.github.spotbugs" + apply plugin: 'org.xbib.gradle.plugin.asciidoctor' -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')}" -} - -sourceCompatibility = JavaVersion.VERSION_1_8 -targetCompatibility = JavaVersion.VERSION_1_8 - -[compileJava, compileTestJava]*.options*.encoding = 'UTF-8' -tasks.withType(JavaCompile) { - options.compilerArgs << "-Xlint:all,-serial" -} - -jar { - manifest { - attributes('Implementation-Version': project.version) + configurations { + alpnagent + asciidoclet + wagon } -} -test { - if (JavaVersion.current() == JavaVersion.VERSION_1_8) { - jvmArgs "-javaagent:" + configurations.alpnagent.asPath + dependencies { + 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')}" } - testLogging { - showStandardStreams = false - exceptionFormat = 'full' + + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + + [compileJava, compileTestJava]*.options*.encoding = 'UTF-8' + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:all,-serial" } -} -clean { - delete 'out' -} - -asciidoctor { - attributes toc: 'left', - doctype: 'book', - icons: 'font', - encoding: 'utf-8', - sectlink: true, - sectanchors: true, - linkattrs: true, - imagesdir: 'img', - 'source-highlighter': 'coderay' -} - -javadoc { - options.docletpath = configurations.asciidoclet.files.asType(List) - options.doclet = "org.xbib.asciidoclet.Asciidoclet" - options.overview = "src/docs/asciidoclet/overview.adoc" - options.addStringOption "-base-dir", "${projectDir}" - options.addStringOption "-attribute", - "name=${project.name},version=${project.version},title-link=https://github.com/jprante/${project.name}" - configure(options) { - noTimestamp = true + jar { + manifest { + attributes('Implementation-Version': project.version) + } } -} -task javadocJar(type: Jar, dependsOn: classes) { - from javadoc - into "build/tmp" - classifier 'javadoc' -} - -task sourcesJar(type: Jar, dependsOn: classes) { - from sourceSets.main.allSource - into "build/tmp" - classifier 'sources' -} - -artifacts { - archives javadocJar, sourcesJar -} - -if (project.hasProperty('signing.keyId')) { - signing { - sign configurations.archives + test { + if (JavaVersion.current() == JavaVersion.VERSION_1_8) { + jvmArgs "-javaagent:" + configurations.alpnagent.asPath + } + testLogging { + showStandardStreams = false + exceptionFormat = 'full' + } } -} + clean { + delete 'out' + } + + asciidoctor { + attributes toc: 'left', + doctype: 'book', + icons: 'font', + encoding: 'utf-8', + sectlink: true, + sectanchors: true, + linkattrs: true, + imagesdir: 'img', + 'source-highlighter': 'coderay' + } + + javadoc { + options.docletpath = configurations.asciidoclet.files.asType(List) + options.doclet = "org.xbib.asciidoclet.Asciidoclet" + options.overview = "src/docs/asciidoclet/overview.adoc" + options.addStringOption "-base-dir", "${projectDir}" + options.addStringOption "-attribute", + "name=${project.name},version=${project.version},title-link=https://github.com/jprante/${project.name}" + configure(options) { + noTimestamp = true + } + } + + task javadocJar(type: Jar, dependsOn: classes) { + from javadoc + into "build/tmp" + classifier 'javadoc' + } + + task sourcesJar(type: Jar, dependsOn: classes) { + from sourceSets.main.allSource + into "build/tmp" + classifier 'sources' + } + + artifacts { + archives javadocJar, sourcesJar + } + + if (project.hasProperty('signing.keyId')) { + signing { + sign configurations.archives + } + } + + + ext { + user = 'jprante' + name = 'netty-http-client' + description = 'A java client for Elasticsearch' + scmUrl = 'https://github.com/' + user + '/' + name + scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' + scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' + } + + + task xbibUpload(type: Upload) { + group = 'publish' + configuration = configurations.archives + uploadDescriptor = true + repositories { + if (project.hasProperty("xbibUsername")) { + mavenDeployer { + configuration = configurations.wagon + repository(url: 'sftp://xbib.org/repository') { + authentication(userName: xbibUsername, privateKey: xbibPrivateKey) + } + } + } + } + } + + task sonaTypeUpload(type: Upload) { + group = 'publish' + configuration = configurations.archives + uploadDescriptor = true + repositories { + if (project.hasProperty('ossrhUsername')) { + mavenDeployer { + beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } + repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots') { + authentication(userName: ossrhUsername, password: ossrhPassword) + } + pom.project { + groupId project.group + artifactId project.name + version project.version + name project.name + description description + packaging 'jar' + inceptionYear '2012' + url scmUrl + organization { + name 'xbib' + url 'http://xbib.org' + } + developers { + developer { + id user + name 'Jörg Prante' + email 'joergprante@gmail.com' + url 'https://github.com/jprante' + } + } + scm { + url scmUrl + connection scmConnection + developerConnection scmDeveloperConnection + } + licenses { + license { + name 'The Apache License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + } + } + } + } + } + + +} spotbugs { effort = "max" @@ -147,84 +222,6 @@ sonarqube { } } -ext { - user = 'jprante' - name = 'netty-http-client' - description = 'A java client for Elasticsearch' - scmUrl = 'https://github.com/' + user + '/' + name - scmConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' - scmDeveloperConnection = 'scm:git:git://github.com/' + user + '/' + name + '.git' -} - - -task xbibUpload(type: Upload) { - group = 'publish' - configuration = configurations.archives - uploadDescriptor = true - repositories { - if (project.hasProperty("xbibUsername")) { - mavenDeployer { - configuration = configurations.wagon - repository(url: 'sftp://xbib.org/repository') { - authentication(userName: xbibUsername, privateKey: xbibPrivateKey) - } - } - } - } -} - -task sonaTypeUpload(type: Upload) { - group = 'publish' - configuration = configurations.archives - uploadDescriptor = true - repositories { - if (project.hasProperty('ossrhUsername')) { - mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2') { - authentication(userName: ossrhUsername, password: ossrhPassword) - } - snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots') { - authentication(userName: ossrhUsername, password: ossrhPassword) - } - pom.project { - groupId project.group - artifactId project.name - version project.version - name project.name - description description - packaging 'jar' - inceptionYear '2012' - url scmUrl - organization { - name 'xbib' - url 'http://xbib.org' - } - developers { - developer { - id user - name 'Jörg Prante' - email 'joergprante@gmail.com' - url 'https://github.com/jprante' - } - } - scm { - url scmUrl - connection scmConnection - developerConnection scmDeveloperConnection - } - licenses { - license { - name 'The Apache License, Version 2.0' - url 'http://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - } - } - } - } -} - nexusStaging { packageGroup = "org.xbib" } diff --git a/gradle.properties b/gradle.properties index 1094899..9a3a20d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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 + + diff --git a/netty-http-client/build.gradle b/netty-http-client/build.gradle new file mode 100644 index 0000000..58acf08 --- /dev/null +++ b/netty-http-client/build.gradle @@ -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')}" +} diff --git a/src/docs/asciidoc/css/foundation.css b/netty-http-client/src/docs/asciidoc/css/foundation.css similarity index 100% rename from src/docs/asciidoc/css/foundation.css rename to netty-http-client/src/docs/asciidoc/css/foundation.css diff --git a/src/docs/asciidoc/index.adoc b/netty-http-client/src/docs/asciidoc/index.adoc similarity index 100% rename from src/docs/asciidoc/index.adoc rename to netty-http-client/src/docs/asciidoc/index.adoc diff --git a/src/docs/asciidoclet/overview.adoc b/netty-http-client/src/docs/asciidoclet/overview.adoc similarity index 100% rename from src/docs/asciidoclet/overview.adoc rename to netty-http-client/src/docs/asciidoclet/overview.adoc diff --git a/src/main/java/org/xbib/netty/http/client/Client.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java similarity index 97% rename from src/main/java/org/xbib/netty/http/client/Client.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java index 76c9365..43d4d97 100644 --- a/src/main/java/org/xbib/netty/http/client/Client.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java @@ -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 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 CompletableFuture execute(Request request, Function 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 { diff --git a/src/main/java/org/xbib/netty/http/client/ClientAuthMode.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientAuthMode.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/ClientAuthMode.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/ClientAuthMode.java diff --git a/src/main/java/org/xbib/netty/http/client/ClientBuilder.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientBuilder.java similarity index 99% rename from src/main/java/org/xbib/netty/http/client/ClientBuilder.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/ClientBuilder.java index 7ddd3a3..ee73632 100644 --- a/src/main/java/org/xbib/netty/http/client/ClientBuilder.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientBuilder.java @@ -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; diff --git a/src/main/java/org/xbib/netty/http/client/ClientConfig.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java similarity index 99% rename from src/main/java/org/xbib/netty/http/client/ClientConfig.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java index e1677d3..855315a 100644 --- a/src/main/java/org/xbib/netty/http/client/ClientConfig.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java @@ -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; diff --git a/src/main/java/org/xbib/netty/http/client/Request.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/Request.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java diff --git a/src/main/java/org/xbib/netty/http/client/RequestBuilder.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/RequestBuilder.java similarity index 98% rename from src/main/java/org/xbib/netty/http/client/RequestBuilder.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/RequestBuilder.java index 71053f3..42624f0 100644 --- a/src/main/java/org/xbib/netty/http/client/RequestBuilder.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/RequestBuilder.java @@ -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; diff --git a/src/main/java/org/xbib/netty/http/client/UserAgent.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/UserAgent.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/UserAgent.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/UserAgent.java diff --git a/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java similarity index 98% rename from src/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java index 399083d..45ab3a8 100644 --- a/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChannelInitializer.java @@ -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; diff --git a/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChunkContentCompressor.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChunkContentCompressor.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/handler/http1/HttpChunkContentCompressor.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpChunkContentCompressor.java diff --git a/src/main/java/org/xbib/netty/http/client/handler/http1/HttpResponseHandler.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpResponseHandler.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/handler/http1/HttpResponseHandler.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/HttpResponseHandler.java diff --git a/src/main/java/org/xbib/netty/http/client/handler/http1/TrafficLoggingHandler.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/TrafficLoggingHandler.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/handler/http1/TrafficLoggingHandler.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/TrafficLoggingHandler.java diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/package-info.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/package-info.java new file mode 100644 index 0000000..4cb6b26 --- /dev/null +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http1/package-info.java @@ -0,0 +1,4 @@ +/** + * HTTP handlers for Netty HTTP client. + */ +package org.xbib.netty.http.client.handler.http1; diff --git a/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java similarity index 87% rename from src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java index c3c340b..4d3f904 100644 --- a/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ChannelInitializer.java @@ -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 { */ @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 { .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(); } diff --git a/src/main/java/org/xbib/netty/http/client/handler/http2/Http2PushPromiseHandler.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2PushPromiseHandler.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/handler/http2/Http2PushPromiseHandler.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2PushPromiseHandler.java diff --git a/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2ResponseHandler.java diff --git a/src/main/java/org/xbib/netty/http/client/handler/http2/Http2SettingsHandler.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2SettingsHandler.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/handler/http2/Http2SettingsHandler.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/Http2SettingsHandler.java diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/package-info.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/package-info.java new file mode 100644 index 0000000..92dc73c --- /dev/null +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http2/package-info.java @@ -0,0 +1,4 @@ +/** + * HTTP/2 handlers for Netty HTTP client. + */ +package org.xbib.netty.http.client.handler.http2; diff --git a/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/listener/CookieListener.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/listener/CookieListener.java diff --git a/src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/listener/ExceptionListener.java diff --git a/src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/listener/HttpHeadersListener.java diff --git a/src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/listener/HttpResponseListener.java diff --git a/src/main/java/org/xbib/netty/http/client/listener/package-info.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/package-info.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/listener/package-info.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/listener/package-info.java diff --git a/src/main/java/org/xbib/netty/http/client/package-info.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/package-info.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/package-info.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/package-info.java diff --git a/src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java similarity index 93% rename from src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java index 7fb87cb..3181322 100644 --- a/src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/BoundedChannelPool.java @@ -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 implements Pool { if (channel == null) { semaphore.release(); throw new ConnectException(); + } else { + if (channelPoolhandler != null) { + channelPoolhandler.channelAcquired(channel); + } } } - if (channelPoolhandler != null) { - channelPoolhandler.channelAcquired(channel); - } return channel; } @@ -165,7 +167,7 @@ public class BoundedChannelPool implements Pool { } 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 implements Pool { } } 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 implements Pool { Queue 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 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; diff --git a/src/main/java/org/xbib/netty/http/client/pool/Pool.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/pool/Pool.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/pool/Pool.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/pool/Pool.java diff --git a/src/main/java/org/xbib/netty/http/client/rest/RestClient.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/rest/RestClient.java similarity index 98% rename from src/main/java/org/xbib/netty/http/client/rest/RestClient.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/rest/RestClient.java index 6ba3742..e5375ad 100644 --- a/src/main/java/org/xbib/netty/http/client/rest/RestClient.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/rest/RestClient.java @@ -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; diff --git a/src/main/java/org/xbib/netty/http/client/retry/BackOff.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/BackOff.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/retry/BackOff.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/retry/BackOff.java diff --git a/src/main/java/org/xbib/netty/http/client/retry/ExponentialBackOff.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/retry/ExponentialBackOff.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/retry/ExponentialBackOff.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/retry/ExponentialBackOff.java diff --git a/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java similarity index 99% rename from src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java index 91d726a..fe9e9d9 100644 --- a/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java @@ -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); diff --git a/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http1Transport.java similarity index 95% rename from src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http1Transport.java index 6bbe8f1..1da4eb9 100644 --- a/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http1Transport.java @@ -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> sequentialPromiseMap; - public HttpTransport(Client client, HttpAddress httpAddress) { + public Http1Transport(Client client, HttpAddress httpAddress) { super(client, httpAddress); this.sequentialCounter = new AtomicInteger(); this.sequentialPromiseMap = new ConcurrentSkipListMap<>(); diff --git a/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java similarity index 93% rename from src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java index 69ccd5a..0fe1ae1 100644 --- a/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Http2Transport.java @@ -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"); } } diff --git a/src/main/java/org/xbib/netty/http/client/transport/Transport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/transport/Transport.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java diff --git a/src/main/java/org/xbib/netty/http/client/transport/package-info.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/package-info.java similarity index 100% rename from src/main/java/org/xbib/netty/http/client/transport/package-info.java rename to netty-http-client/src/main/java/org/xbib/netty/http/client/transport/package-info.java diff --git a/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/CompletableFutureTest.java diff --git a/src/test/java/org/xbib/netty/http/client/test/ConscryptTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ConscryptTest.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/ConscryptTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/ConscryptTest.java diff --git a/src/test/java/org/xbib/netty/http/client/test/CookieSetterHttpBinTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/CookieSetterHttpBinTest.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/CookieSetterHttpBinTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/CookieSetterHttpBinTest.java diff --git a/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java similarity index 99% rename from src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java index cc1c789..e4055d9 100644 --- a/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java @@ -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; diff --git a/src/test/java/org/xbib/netty/http/client/test/Http1Test.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http1Test.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/Http1Test.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http1Test.java diff --git a/src/test/java/org/xbib/netty/http/client/test/Http2Test.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http2Test.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/Http2Test.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http2Test.java diff --git a/src/test/java/org/xbib/netty/http/client/test/LeakTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/LeakTest.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/LeakTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/LeakTest.java diff --git a/src/test/java/org/xbib/netty/http/client/test/LoggingBase.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/LoggingBase.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/LoggingBase.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/LoggingBase.java diff --git a/src/test/java/org/xbib/netty/http/client/test/PooledClientTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/PooledClientTest.java similarity index 95% rename from src/test/java/org/xbib/netty/http/client/test/PooledClientTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/PooledClientTest.java index 6756d1f..a659f4f 100644 --- a/src/test/java/org/xbib/netty/http/client/test/PooledClientTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/PooledClientTest.java @@ -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) diff --git a/src/test/java/org/xbib/netty/http/client/test/RequestBuilderTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/RequestBuilderTest.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/RequestBuilderTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/RequestBuilderTest.java diff --git a/src/test/java/org/xbib/netty/http/client/test/SecureHttp1Test.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/SecureHttp1Test.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/SecureHttp1Test.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/SecureHttp1Test.java diff --git a/src/test/java/org/xbib/netty/http/client/test/URITest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/URITest.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/URITest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/URITest.java diff --git a/src/test/java/org/xbib/netty/http/client/test/XbibTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/XbibTest.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/XbibTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/XbibTest.java diff --git a/src/test/java/org/xbib/netty/http/client/test/package-info.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/package-info.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/package-info.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/package-info.java diff --git a/src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java similarity index 99% rename from src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java index 08b9ac3..73c19c4 100644 --- a/src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/EpollTest.java @@ -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; diff --git a/src/test/java/org/xbib/netty/http/client/test/pool/MockEpollServer.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/MockEpollServer.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/pool/MockEpollServer.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/MockEpollServer.java diff --git a/src/test/java/org/xbib/netty/http/client/test/pool/MockNioServer.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/MockNioServer.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/pool/MockNioServer.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/MockNioServer.java diff --git a/src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java similarity index 99% rename from src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java index d27217d..6fbc2ea 100644 --- a/src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/NioTest.java @@ -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; diff --git a/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java similarity index 99% rename from src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java index dcd6738..048f8ab 100644 --- a/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/pool/PoolTest.java @@ -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; diff --git a/src/test/java/org/xbib/netty/http/client/test/rest/RestClientTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/rest/RestClientTest.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/rest/RestClientTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/rest/RestClientTest.java diff --git a/src/test/java/org/xbib/netty/http/client/test/retry/ExponentialBackOffTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/ExponentialBackOffTest.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/retry/ExponentialBackOffTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/ExponentialBackOffTest.java diff --git a/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOff.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOff.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/retry/MockBackOff.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOff.java diff --git a/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOffTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOffTest.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/retry/MockBackOffTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/retry/MockBackOffTest.java diff --git a/src/test/java/org/xbib/netty/http/client/test/simple/Http2FramesTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/Http2FramesTest.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/simple/Http2FramesTest.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/Http2FramesTest.java diff --git a/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp1Test.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp1Test.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp1Test.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp1Test.java diff --git a/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp2Test.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp2Test.java similarity index 100% rename from src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp2Test.java rename to netty-http-client/src/test/java/org/xbib/netty/http/client/test/simple/SimpleHttp2Test.java diff --git a/netty-http-common/build.gradle b/netty-http-common/build.gradle new file mode 100644 index 0000000..9b90c6f --- /dev/null +++ b/netty-http-common/build.gradle @@ -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')}" +} \ No newline at end of file diff --git a/src/main/java/org/xbib/netty/http/client/HttpAddress.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java similarity index 94% rename from src/main/java/org/xbib/netty/http/client/HttpAddress.java rename to netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java index d82bc15..99add9a 100644 --- a/src/main/java/org/xbib/netty/http/client/HttpAddress.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java @@ -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())); } diff --git a/src/main/java/org/xbib/netty/http/client/util/NetworkClass.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/NetworkClass.java similarity index 70% rename from src/main/java/org/xbib/netty/http/client/util/NetworkClass.java rename to netty-http-common/src/main/java/org/xbib/netty/http/common/NetworkClass.java index 12e821a..0fed714 100644 --- a/src/main/java/org/xbib/netty/http/client/util/NetworkClass.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/NetworkClass.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.util; +package org.xbib.netty.http.common; /** * The network classes. diff --git a/src/main/java/org/xbib/netty/http/client/util/NetworkProtocolVersion.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/NetworkProtocolVersion.java similarity index 74% rename from src/main/java/org/xbib/netty/http/client/util/NetworkProtocolVersion.java rename to netty-http-common/src/main/java/org/xbib/netty/http/common/NetworkProtocolVersion.java index 1b7fc8f..85fec00 100644 --- a/src/main/java/org/xbib/netty/http/client/util/NetworkProtocolVersion.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/NetworkProtocolVersion.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.util; +package org.xbib.netty.http.common; /** * The TCP/IP network protocol versions. diff --git a/src/main/java/org/xbib/netty/http/client/util/NetworkUtils.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/NetworkUtils.java similarity index 99% rename from src/main/java/org/xbib/netty/http/client/util/NetworkUtils.java rename to netty-http-common/src/main/java/org/xbib/netty/http/common/NetworkUtils.java index 9744621..7860943 100644 --- a/src/main/java/org/xbib/netty/http/client/util/NetworkUtils.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/NetworkUtils.java @@ -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; diff --git a/src/main/java/org/xbib/netty/http/client/pool/PoolKey.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/PoolKey.java similarity index 73% rename from src/main/java/org/xbib/netty/http/client/pool/PoolKey.java rename to netty-http-common/src/main/java/org/xbib/netty/http/common/PoolKey.java index 7f4a18d..8b559ea 100644 --- a/src/main/java/org/xbib/netty/http/client/pool/PoolKey.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/PoolKey.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.client.pool; +package org.xbib.netty.http.common; import java.net.InetSocketAddress; diff --git a/src/main/java/org/xbib/netty/http/client/util/package-info.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/package-info.java similarity index 51% rename from src/main/java/org/xbib/netty/http/client/util/package-info.java rename to netty-http-common/src/main/java/org/xbib/netty/http/common/package-info.java index 403b658..8a69e9f 100644 --- a/src/main/java/org/xbib/netty/http/client/util/package-info.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/package-info.java @@ -1,4 +1,4 @@ /** * Utilities for Netty HTTP client. */ -package org.xbib.netty.http.client.util; +package org.xbib.netty.http.common; diff --git a/netty-http-server/build.gradle b/netty-http-server/build.gradle new file mode 100644 index 0000000..6b3ad15 --- /dev/null +++ b/netty-http-server/build.gradle @@ -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')}" +} diff --git a/netty-http-server/src/docs/asciidoc/css/foundation.css b/netty-http-server/src/docs/asciidoc/css/foundation.css new file mode 100644 index 0000000..27be611 --- /dev/null +++ b/netty-http-server/src/docs/asciidoc/css/foundation.css @@ -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; } diff --git a/netty-http-server/src/docs/asciidoc/index.adoc b/netty-http-server/src/docs/asciidoc/index.adoc new file mode 100644 index 0000000..62615bb --- /dev/null +++ b/netty-http-server/src/docs/asciidoc/index.adoc @@ -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 diff --git a/netty-http-server/src/docs/asciidoclet/overview.adoc b/netty-http-server/src/docs/asciidoclet/overview.adoc new file mode 100644 index 0000000..b61a9a7 --- /dev/null +++ b/netty-http-server/src/docs/asciidoclet/overview.adoc @@ -0,0 +1,3 @@ += Netty HTTP server +Jörg Prante + diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java new file mode 100644 index 0000000..4287023 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/Server.java @@ -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 socketChannelClass; + + private final ServerBootstrap bootstrap; + + private final Map 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 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 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 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 createSocketChannelClass(ServerConfig serverConfig, + Class socketChannelClass) { + Class 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; + } + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerBuilder.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerBuilder.java new file mode 100644 index 0000000..e3c704f --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerBuilder.java @@ -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 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 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 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); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerConfig.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerConfig.java new file mode 100644 index 0000000..d6fdbab --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerConfig.java @@ -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 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 ciphers = Defaults.DEFAULT_CIPHERS; + + private CipherSuiteFilter cipherSuiteFilter = Defaults.DEFAULT_CIPHER_SUITE_FILTER; + + private InputStream keyCertChainInputStream; + + private InputStream keyInputStream; + + private String keyPassword; + + private List 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 ciphers) { + this.ciphers = ciphers; + return this; + } + + public Iterable 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 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; + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerName.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerName.java new file mode 100644 index 0000000..623e6a4 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerName.java @@ -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"); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/context/Context.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/context/Context.java new file mode 100644 index 0000000..8112d27 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/context/Context.java @@ -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"; +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/context/ContextHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/context/ContextHandler.java new file mode 100644 index 0000000..7e2464c --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/context/ContextHandler.java @@ -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; +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/context/ContextInfo.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/context/ContextInfo.java new file mode 100644 index 0000000..470ff38 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/context/ContextInfo.java @@ -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 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 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); + } + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/context/MethodContextHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/context/MethodContextHandler.java new file mode 100644 index 0000000..24a83d5 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/context/MethodContextHandler.java @@ -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); + } + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/context/VirtualServer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/context/VirtualServer.java new file mode 100644 index 0000000..04d24b8 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/context/VirtualServer.java @@ -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 aliases; + + private final Set methods; + + private final ContextInfo emptyContext; + + private final Map 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 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 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); + } + +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/HttpServerChannelInitializer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/HttpServerChannelInitializer.java new file mode 100644 index 0000000..0ba5172 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/HttpServerChannelInitializer.java @@ -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 { + + 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 domainNameMapping; + + public HttpServerChannelInitializer(Server server, ServerConfig serverConfig, + DomainNameMapping 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() { + @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); + } + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpChannelInitializer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpChannelInitializer.java new file mode 100644 index 0000000..05a5813 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpChannelInitializer.java @@ -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 { + + 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 domainNameMapping; + + public HttpChannelInitializer(Server server, + HttpAddress httpAddress, + DomainNameMapping 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); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpHandler.java new file mode 100644 index 0000000..442a758 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/HttpHandler.java @@ -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)))); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/IdleTimeoutHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/IdleTimeoutHandler.java new file mode 100644 index 0000000..62f7a09 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/IdleTimeoutHandler.java @@ -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(); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/TrafficLoggingHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/TrafficLoggingHandler.java new file mode 100644 index 0000000..63a2c19 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http1/TrafficLoggingHandler.java @@ -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); + } + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/DummyHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/DummyHandler.java new file mode 100644 index 0000000..221bcaf --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/DummyHandler.java @@ -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()); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/FrameListener.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/FrameListener.java new file mode 100644 index 0000000..0b4094d --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/FrameListener.java @@ -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; + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp1Handler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp1Handler.java new file mode 100644 index 0000000..e6870b6 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp1Handler.java @@ -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 { + + 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(); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp2Handler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp2Handler.java new file mode 100644 index 0000000..e5e490b --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/HelloWorldHttp2Handler.java @@ -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!". + * + *

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)); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java new file mode 100644 index 0000000..41258e4 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java @@ -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 { + + 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 domainNameMapping; + + public Http2ChannelInitializer(Server server, + HttpAddress httpAddress, + DomainNameMapping 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() { + @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() { + @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(); + } + +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2Handler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2Handler.java new file mode 100644 index 0000000..4e22d61 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2Handler.java @@ -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. + *

+ * 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. + *

    + *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}
  • + *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}
  • + *
  • {@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}
  • + *
+ *

+ * 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. + *

+ * 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. + *

    + *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}
  • + *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}
  • + *
  • {@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}
  • + *
+ *

+ * 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. + *

+ * Promised requests MUST be authoritative, cacheable, and safe. + * See [RFC http2], Section 8.2. + *

+ * 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. + *

    + *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}
  • + *
  • {@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}
  • + *
  • {@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}
  • + *
+ *

+ * 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. + *

+ * If a {@link RuntimeException} is thrown it will be logged and not propagated. + * 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 + *

    + *
  • {@code true} to validate HTTP headers in the http-codec
  • + *
  • {@code false} not to validate HTTP headers in the http-codec
  • + *
+ * @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
    + *
  • {@code true} to validate HTTP headers in the http-codec
  • + *
  • {@code false} not to validate HTTP headers in the http-codec
  • + *
+ * @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 translations = isRequest ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS; + try { + for (Map.Entry 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 + REQUEST_HEADER_TRANSLATIONS = new CharSequenceMap(); + private static final CharSequenceMap + RESPONSE_HEADER_TRANSLATIONS = new CharSequenceMap(); + 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()); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2RequestHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2RequestHandler.java new file mode 100644 index 0000000..78734ed --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2RequestHandler.java @@ -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 { + + @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(); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2SettingsHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2SettingsHandler.java new file mode 100644 index 0000000..17f7fb6 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2SettingsHandler.java @@ -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 { + + @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); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/UserEventLogger.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/UserEventLogger.java new file mode 100644 index 0000000..b0341c7 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/UserEventLogger.java @@ -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); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/package-info.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/package-info.java new file mode 100644 index 0000000..6bb037f --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/package-info.java @@ -0,0 +1,4 @@ +/** + * Handlers for Netty HTTP server. + */ +package org.xbib.netty.http.server.handler; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/ClosedSessionException.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/ClosedSessionException.java new file mode 100644 index 0000000..9048ce1 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/ClosedSessionException.java @@ -0,0 +1,7 @@ +package org.xbib.netty.http.server.internal; + +/** + * A {@link RuntimeException} raised when the connection to the remote peer has been closed unexpectedly. + */ +public final class ClosedSessionException extends RuntimeException { +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http1ObjectEncoder.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http1ObjectEncoder.java new file mode 100644 index 0000000..4f42a46 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http1ObjectEncoder.java @@ -0,0 +1,192 @@ +package org.xbib.netty.http.server.internal; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http.DefaultHttpContent; +import io.netty.handler.codec.http.DefaultHttpResponse; +import io.netty.handler.codec.http.DefaultLastHttpContent; +import io.netty.handler.codec.http.HttpContent; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpObject; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http2.Http2Error; +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; + +import java.util.AbstractMap.SimpleImmutableEntry; +import java.util.ArrayDeque; +import java.util.Map.Entry; +import java.util.Queue; +import java.util.logging.Logger; + +/** + * HTTP 1 object encoder. + */ +public final class Http1ObjectEncoder extends HttpObjectEncoder { + + private static final Logger logger = Logger.getLogger(Http1ObjectEncoder.class.getName()); + + /** + * The map which maps a request ID to its related pending response. + */ + private final IntObjectMap pendingWrites = new IntObjectHashMap<>(); + /** + * The ID of the request which is at its turn to send a response. + */ + private int currentId = 1; + /** + * The minimum ID of the request whose stream has been closed/reset. + */ + private int minClosedId = Integer.MAX_VALUE; + /** + * The maximum known ID with pending writes. + */ + private int maxIdWithPendingWrites = Integer.MIN_VALUE; + + @Override + protected ChannelFuture doWriteHeaders(ChannelHandlerContext ctx, int id, int streamId, + HttpHeaders headers, HttpResponseStatus status, boolean endStream) { + if (id >= minClosedId) { + return ctx.newFailedFuture(new ClosedSessionException()); + } + try { + return write(ctx, id, new DefaultHttpResponse(HttpVersion.HTTP_1_1, status, headers), endStream); + } catch (Throwable t) { + return ctx.newFailedFuture(t); + } + } + + @Override + protected ChannelFuture doWriteData(ChannelHandlerContext ctx, int id, int streamId, ByteBuf buf, boolean endStream) { + if (id >= minClosedId) { + return ctx.newFailedFuture(new ClosedSessionException()); + } + try { + final HttpContent content; + if (endStream) { + content = new DefaultLastHttpContent(buf); + } else { + content = new DefaultHttpContent(buf); + } + return write(ctx, id, content, endStream); + } catch (Throwable t) { + return ctx.newFailedFuture(t); + } + } + + private ChannelFuture write(ChannelHandlerContext ctx, int id, HttpObject obj, boolean endStream) { + if (id < currentId) { + return ctx.newFailedFuture(new ClosedSessionException()); + } + final PendingWrites currentPendingWrites = pendingWrites.get(id); + if (id == currentId) { + if (currentPendingWrites != null) { + pendingWrites.remove(id); + flushPendingWrites(ctx, currentPendingWrites); + } + final ChannelFuture future = ctx.write(obj); + if (endStream) { + currentId++; + for (;;) { + final PendingWrites nextPendingWrites = pendingWrites.get(currentId); + if (nextPendingWrites == null) { + break; + } + flushPendingWrites(ctx, nextPendingWrites); + if (!nextPendingWrites.isEndOfStream()) { + break; + } + pendingWrites.remove(currentId); + currentId++; + } + } + ctx.flush(); + return future; + } else { + final ChannelPromise promise = ctx.newPromise(); + final Entry entry = new SimpleImmutableEntry<>(obj, promise); + if (currentPendingWrites == null) { + final PendingWrites newPendingWrites = new PendingWrites(); + maxIdWithPendingWrites = Math.max(maxIdWithPendingWrites, id); + newPendingWrites.add(entry); + pendingWrites.put(id, newPendingWrites); + } else { + currentPendingWrites.add(entry); + if (endStream) { + currentPendingWrites.setEndOfStream(); + } + } + return promise; + } + } + + private static void flushPendingWrites(ChannelHandlerContext ctx, PendingWrites pendingWrites) { + while (true) { + final Entry e = pendingWrites.poll(); + if (e == null) { + break; + } + ctx.write(e.getKey(), e.getValue()); + } + } + + @Override + protected ChannelFuture doWriteReset(ChannelHandlerContext ctx, int id, int streamId, Http2Error error) { + minClosedId = Math.min(minClosedId, id); + for (int i = minClosedId; i <= maxIdWithPendingWrites; i++) { + final PendingWrites pendingWrites = this.pendingWrites.remove(i); + while (true) { + final Entry e = pendingWrites.poll(); + if (e == null) { + break; + } + e.getValue().tryFailure(new ClosedSessionException()); + } + } + final ChannelFuture f = ctx.write(Unpooled.EMPTY_BUFFER); + if (currentId >= minClosedId) { + f.addListener(ChannelFutureListener.CLOSE); + } + return f; + } + + @Override + protected void doClose() { + if (pendingWrites.isEmpty()) { + return; + } + ClosedSessionException cause = new ClosedSessionException(); + for (Queue> queue : pendingWrites.values()) { + while (true) { + final Entry e = queue.poll(); + if (e == null) { + break; + } + e.getValue().tryFailure(cause); + } + } + pendingWrites.clear(); + } + + private static final class PendingWrites extends ArrayDeque> { + + private boolean endOfStream; + + PendingWrites() { + super(4); + } + + boolean isEndOfStream() { + return endOfStream; + } + + void setEndOfStream() { + endOfStream = true; + } + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http2ObjectEncoder.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http2ObjectEncoder.java new file mode 100644 index 0000000..dd299a9 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/Http2ObjectEncoder.java @@ -0,0 +1,167 @@ +package org.xbib.netty.http.server.internal; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http2.CharSequenceMap; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.Http2ConnectionEncoder; +import io.netty.handler.codec.http2.Http2Error; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.handler.codec.http2.Http2Stream; +import io.netty.handler.codec.http2.HttpConversionUtil; +import io.netty.util.AsciiString; + +import java.util.Iterator; +import java.util.Map; +import java.util.Objects; + +import static io.netty.util.AsciiString.EMPTY_STRING; +import static io.netty.util.ByteProcessor.FIND_SEMI_COLON; + +/** + * + */ +public final class Http2ObjectEncoder extends HttpObjectEncoder { + + /** + * The set of headers that should not be directly copied when converting headers from HTTP to HTTP/2. + */ + private static final CharSequenceMap HTTP_TO_HTTP2_HEADER_BLACKLIST = + new CharSequenceMap(); + static { + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.CONNECTION, EMPTY_STRING); + @SuppressWarnings("deprecation") + AsciiString keepAlive = HttpHeaderNames.KEEP_ALIVE; + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(keepAlive, EMPTY_STRING); + @SuppressWarnings("deprecation") + AsciiString proxyConnection = HttpHeaderNames.PROXY_CONNECTION; + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(proxyConnection, EMPTY_STRING); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.TRANSFER_ENCODING, EMPTY_STRING); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.HOST, EMPTY_STRING); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpHeaderNames.UPGRADE, EMPTY_STRING); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), EMPTY_STRING); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), EMPTY_STRING); + HTTP_TO_HTTP2_HEADER_BLACKLIST.add(HttpConversionUtil.ExtensionHeaderNames.PATH.text(), EMPTY_STRING); + } + + private final Http2ConnectionEncoder encoder; + + public Http2ObjectEncoder(Http2ConnectionEncoder encoder) { + super(); + this.encoder = Objects.requireNonNull(encoder, "encoder"); + } + + @Override + protected ChannelFuture doWriteHeaders(ChannelHandlerContext ctx, int id, int streamId, + HttpHeaders headers, HttpResponseStatus status, + boolean endStream) { + final ChannelFuture future = validateStream(ctx, streamId); + if (future != null) { + return future; + } + Http2Headers http2Headers = toHttp2Headers(headers, status, false); + return encoder.writeHeaders(ctx, streamId, http2Headers, 0, endStream, ctx.newPromise()); + } + + @Override + protected ChannelFuture doWriteData(ChannelHandlerContext ctx, int id, int streamId, ByteBuf data, + boolean endStream) { + final ChannelFuture future = validateStream(ctx, streamId); + if (future != null) { + return future; + } + return encoder.writeData(ctx, streamId, data, 0, endStream, ctx.newPromise()); + } + + @Override + protected ChannelFuture doWriteReset(ChannelHandlerContext ctx, int id, int streamId, Http2Error error) { + final ChannelFuture future = validateStream(ctx, streamId); + if (future != null) { + return future; + } + return encoder.writeRstStream(ctx, streamId, error.code(), ctx.newPromise()); + } + + + @Override + protected void doClose() { + } + + private ChannelFuture validateStream(ChannelHandlerContext ctx, int streamId) { + final Http2Stream stream = encoder.connection().stream(streamId); + if (stream != null) { + switch (stream.state()) { + case RESERVED_LOCAL: + case OPEN: + case HALF_CLOSED_REMOTE: + break; + default: + return ctx.newFailedFuture(new IllegalStateException("stream state = " + stream.state().name())); + } + } else if (encoder.connection().streamMayHaveExisted(streamId)) { + return ctx.newFailedFuture(new IllegalStateException("stream may have existed")); + } + return null; + } + + public static Http2Headers toHttp2Headers(HttpHeaders inHeaders, + HttpResponseStatus status, + boolean validateHeaders) { + final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size()); + out.status(status.codeAsText()); + toHttp2Headers(inHeaders, out); + return out; + } + + public static void toHttp2Headers(HttpHeaders inHeaders, Http2Headers outHeaders) { + Iterator> iter = inHeaders.iteratorCharSequence(); + while (iter.hasNext()) { + Map.Entry entry = iter.next(); + final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase(); + if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName)) { + // https://tools.ietf.org/html/rfc7540#section-8.1.2.2 makes a special exception for TE + if (aName.contentEqualsIgnoreCase(HttpHeaderNames.TE) && + !AsciiString.contentEqualsIgnoreCase(entry.getValue(), HttpHeaderValues.TRAILERS)) { + throw new IllegalArgumentException("Invalid value for " + HttpHeaderNames.TE + ": " + + entry.getValue()); + } + if (aName.contentEqualsIgnoreCase(HttpHeaderNames.COOKIE)) { + AsciiString value = AsciiString.of(entry.getValue()); + // split up cookies to allow for better compression + // https://tools.ietf.org/html/rfc7540#section-8.1.2.5 + try { + int index = value.forEachByte(FIND_SEMI_COLON); + if (index != -1) { + int start = 0; + do { + outHeaders.add(HttpHeaderNames.COOKIE, value.subSequence(start, index, false)); + // skip 2 characters "; " (see https://tools.ietf.org/html/rfc6265#section-4.2.1) + start = index + 2; + } while (start < value.length() && + (index = value.forEachByte(start, value.length() - start, FIND_SEMI_COLON)) != -1); + if (start >= value.length()) { + throw new IllegalArgumentException("cookie value is of unexpected format: " + value); + } + outHeaders.add(HttpHeaderNames.COOKIE, value.subSequence(start, value.length(), false)); + } else { + outHeaders.add(HttpHeaderNames.COOKIE, value); + } + } catch (Exception e) { + // This is not expect to happen because FIND_SEMI_COLON never throws but must be caught + // because of the ByteProcessor interface. + throw new IllegalStateException(e); + } + } else { + outHeaders.add(aName, entry.getValue()); + } + } + } + } +} + + diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/HttpObjectEncoder.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/HttpObjectEncoder.java new file mode 100644 index 0000000..d60fa23 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/HttpObjectEncoder.java @@ -0,0 +1,77 @@ +package org.xbib.netty.http.server.internal; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http2.Http2Error; + +/** + * HTTP object encoder. + */ +public abstract class HttpObjectEncoder { + + private volatile boolean closed; + + /** + * Writes an {@link HttpHeaders}. + */ + public final ChannelFuture writeHeaders(ChannelHandlerContext ctx, int id, int streamId, HttpHeaders headers, + HttpResponseStatus status, boolean endStream) { + if (!ctx.channel().eventLoop().inEventLoop()) { + throw new IllegalStateException(); + } + if (closed) { + return newFailedFuture(ctx); + } + return doWriteHeaders(ctx, id, streamId, headers, status, endStream); + } + + protected abstract ChannelFuture doWriteHeaders(ChannelHandlerContext ctx, int id, int streamId, + HttpHeaders headers, HttpResponseStatus status, boolean endStream); + + public final ChannelFuture writeData(ChannelHandlerContext ctx, int id, int streamId, ByteBuf data, boolean endStream) { + if (!ctx.channel().eventLoop().inEventLoop()) { + throw new IllegalStateException(); + } + if (closed) { + return newFailedFuture(ctx); + } + return doWriteData(ctx, id, streamId, data, endStream); + } + + protected abstract ChannelFuture doWriteData(ChannelHandlerContext ctx, int id, int streamId, ByteBuf data, + boolean endStream); + + /** + * Resets the specified stream. If the session protocol doesn't support multiplexing or the connection + * is in unrecoverable state, the connection will be closed. For example, in an HTTP/1 connection, this + * will lead the connection to be closed immediately or after the previous requests that are not reset. + */ + public final ChannelFuture writeReset(ChannelHandlerContext ctx, int id, int streamId, Http2Error error) { + if (closed) { + return newFailedFuture(ctx); + } + return doWriteReset(ctx, id, streamId, error); + } + + protected abstract ChannelFuture doWriteReset(ChannelHandlerContext ctx, int id, int streamId, Http2Error error); + + /** + * Releases the resources related with this encoder and fails any unfinished writes. + */ + public void close() { + if (closed) { + return; + } + closed = true; + doClose(); + } + + protected abstract void doClose(); + + private static ChannelFuture newFailedFuture(ChannelHandlerContext ctx) { + return ctx.newFailedFuture(new ClosedSessionException()); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/package-info.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/package-info.java new file mode 100644 index 0000000..cd28b37 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/internal/package-info.java @@ -0,0 +1,4 @@ +/** + * Internal classes for Netty HTTP server. + */ +package org.xbib.netty.http.server.internal; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/package-info.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/package-info.java new file mode 100644 index 0000000..85a5a34 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/package-info.java @@ -0,0 +1,4 @@ +/** + * Classes for Netty HTTP server. + */ +package org.xbib.netty.http.server; diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/security/tls/SelfSignedCertificate.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/security/tls/SelfSignedCertificate.java new file mode 100644 index 0000000..91f0104 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/security/tls/SelfSignedCertificate.java @@ -0,0 +1,167 @@ +package org.xbib.netty.http.server.security.tls; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x509.AlgorithmIdentifier; +import org.bouncycastle.asn1.x509.Certificate; +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.util.PrivateKeyFactory; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.DefaultDigestAlgorithmIdentifierFinder; +import org.bouncycastle.operator.DefaultSignatureAlgorithmIdentifierFinder; +import org.bouncycastle.operator.OperatorCreationException; +import org.bouncycastle.operator.bc.BcRSAContentSignerBuilder; +import org.bouncycastle.util.encoders.Base64; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.util.Date; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Generates a temporary self-signed certificate for testing purposes. + */ +public final class SelfSignedCertificate { + + /** Current time minus 1 year, just in case software clock goes back due to time synchronization */ + private static final Date DEFAULT_NOT_BEFORE = new Date(System.currentTimeMillis() - 86400000L * 365); + + /** The maximum possible value in X.509 specification: 9999-12-31 23:59:59 */ + private static final Date DEFAULT_NOT_AFTER = new Date(253402300799000L); + + private static final String BEGIN_KEY = "-----BEGIN PRIVATE KEY-----"; + + private static final String END_KEY = "-----END PRIVATE KEY-----"; + + private static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; + + private static final String END_CERT = "-----END CERTIFICATE-----"; + + private byte[] keyBytes; + + private byte[] certBytes; + + private Certificate cert; + + private PrivateKey key; + + public SelfSignedCertificate() + throws IOException, NoSuchProviderException, NoSuchAlgorithmException, OperatorCreationException { + this("localhost"); + } + + public SelfSignedCertificate(String fqdn) + throws IOException, NoSuchProviderException, NoSuchAlgorithmException, OperatorCreationException { + generate(fqdn, new SecureRandom(), 2048); + } + + /** + * Creates a new instance. + * + * @param fqdn a fully qualified domain name + * @param random the {@link SecureRandom} to use + * @param bits the number of bits of the generated private key + * @throws NoSuchAlgorithmException if algorithm does not exist + * @throws NoSuchProviderException if provider does not exist + * @throws OperatorCreationException if provider does not exist + * @throws IOException if generation fails + */ + public void generate(String fqdn, SecureRandom random, int bits) + throws IOException, NoSuchProviderException, NoSuchAlgorithmException, OperatorCreationException { + KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA", "BC"); + keyGen.initialize(bits, random); + KeyPair keypair = keyGen.generateKeyPair(); + this.key = keypair.getPrivate(); + X500Name name = new X500Name("CN=" + fqdn); + SubjectPublicKeyInfo subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(keypair.getPublic().getEncoded()); + X509v3CertificateBuilder certificateBuilder = + new X509v3CertificateBuilder(name, BigInteger.valueOf(System.currentTimeMillis()), + DEFAULT_NOT_BEFORE, DEFAULT_NOT_AFTER, name, subjectPublicKeyInfo); + AlgorithmIdentifier sigAlgId = + new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256WithRSAEncryption"); + AlgorithmIdentifier digestAlgId = + new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId); + AsymmetricKeyParameter caPrivateKeyParameters = PrivateKeyFactory.createKey(key.getEncoded()); + ContentSigner contentSigner = new BcRSAContentSignerBuilder(sigAlgId, digestAlgId) + .build(caPrivateKeyParameters); + this.cert = certificateBuilder.build(contentSigner).toASN1Structure(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + outputStream.write(BEGIN_KEY.getBytes(StandardCharsets.US_ASCII)); + outputStream.write('\n'); + writeEncoded(key.getEncoded(), outputStream); + outputStream.write(END_KEY.getBytes(StandardCharsets.US_ASCII)); + outputStream.write('\n'); + this.keyBytes = outputStream.toByteArray(); + outputStream = new ByteArrayOutputStream(); + outputStream.write(BEGIN_CERT.getBytes(StandardCharsets.US_ASCII)); + outputStream.write('\n'); + writeEncoded(cert.getEncoded(), outputStream); + outputStream.write(END_CERT.getBytes(StandardCharsets.US_ASCII)); + outputStream.write('\n'); + this.certBytes = outputStream.toByteArray(); + } + + /** + * Returns the generated X.509 certificate file in PEM format. + * @return input stream of certificate + */ + public InputStream certificate() { + return new ByteArrayInputStream(certBytes); + } + + /** + * Returns the generated RSA private key file in PEM format. + * @return input stream of private key + */ + public InputStream privateKey() { + return new ByteArrayInputStream(keyBytes); + } + + /** + * Returns the generated RSA private key. + * @return private key + */ + public PrivateKey key() { + return key; + } + + public void exportPEM(OutputStream outputStream) throws IOException { + outputStream.write(keyBytes); + outputStream.write(certBytes); + } + + public void exportPEM(Logger logger) { + logger.log(Level.INFO, new String(keyBytes, StandardCharsets.US_ASCII) + + new String(certBytes, StandardCharsets.US_ASCII)); + } + + private void writeEncoded(byte[] bytes, OutputStream outputStream) throws IOException { + byte[] buf = new byte[64]; + byte[] base64 = Base64.encode(bytes); + for (int i = 0; i < base64.length; i += buf.length) { + int index = 0; + while (index != buf.length) { + if ((i + index) >= base64.length) { + break; + } + buf[index] = base64[i + index]; + index++; + } + outputStream.write(buf, 0, index); + outputStream.write('\n'); + } + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseServerTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseServerTransport.java new file mode 100644 index 0000000..780eaab --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/BaseServerTransport.java @@ -0,0 +1,106 @@ +package org.xbib.netty.http.server.transport; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpVersion; +import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.context.ContextHandler; +import org.xbib.netty.http.server.context.VirtualServer; + +import java.io.IOException; +import java.util.Arrays; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +abstract class BaseServerTransport implements ServerTransport { + + protected static final AtomicInteger requestCounter = new AtomicInteger(); + + private static final List METHODS = Arrays.asList("GET", "HEAD", "OPTIONS"); + + protected final Server server; + + protected BaseServerTransport(Server server) { + this.server = server; + } + + /** + * Accepts a request, performing various validation checks + * and required special header handling, possibly returning an + * appropriate response. + * + * @param serverRequest the request + * @param serverResponse the response + * @return whether further processing should be performed + */ + protected static boolean acceptRequest(ServerRequest serverRequest, ServerResponse serverResponse) { + HttpHeaders reqHeaders = serverRequest.getRequest().headers(); + HttpVersion version = serverRequest.getHttpAddress().getVersion(); + switch (version.majorVersion()) { + case 1: + case 2: + if (!reqHeaders.contains(HttpHeaderNames.HOST)) { + // RFC2616#14.23: missing Host header gets 400 + serverResponse.writeError(400, "missing 'Host' header"); + return false; + } + // return a continue response before reading body + String expect = reqHeaders.get(HttpHeaderNames.EXPECT); + if (expect != null) { + if (expect.equalsIgnoreCase("100-continue")) { + //ServerResponse tempResp = new ServerResponse(serverResponse); + //tempResp.sendHeaders(100); + } else { + // RFC2616#14.20: if unknown expect, send 417 + serverResponse.writeError(417); + return false; + } + } + break; + default: + serverResponse.writeError(400, "Unknown version: " + version); + return false; + } + return true; + } + + /** + * Handles a request according to the request method. + * + * @param serverRequest the request + * @param serverResponse the response (into which the response is written) + * @throws IOException if and error occurs + */ + protected static void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + String method = serverRequest.getRequest().method().name(); + String path = serverRequest.getRequest().uri(); + VirtualServer virtualServer = serverRequest.getVirtualServer(); + Map handlers = virtualServer.getContext(path).getHandlers(); + // RFC 2616#5.1.1 - GET and HEAD must be supported + if (method.equals("GET") || method.equals("HEAD") || handlers.containsKey(method)) { + ContextHandler handler = virtualServer.getContext(path).getHandlers().get(method); + if (handler == null) { + serverResponse.writeError(404); + } else { + handler.serve(serverRequest, serverResponse); + } + } else { + Set methods = new LinkedHashSet<>(METHODS); + // "*" is a special server-wide (no-context) request supported by OPTIONS + boolean isServerOptions = path.equals("*") && method.equals("OPTIONS"); + methods.addAll(isServerOptions ? virtualServer.getMethods() : handlers.keySet()); + serverResponse.getHeaders().add(HttpHeaderNames.ALLOW, String.join(", ", methods)); + if (method.equals("OPTIONS")) { // default OPTIONS handler + serverResponse.getHeaders().add(HttpHeaderNames.CONTENT_LENGTH, "0"); // RFC2616#9.2 + serverResponse.write(200); + } else if (virtualServer.getMethods().contains(method)) { + serverResponse.write(405); // supported by server, but not this context (nor built-in) + } else { + serverResponse.writeError(501); // unsupported method + } + } + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerResponse.java new file mode 100644 index 0000000..6452e16 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerResponse.java @@ -0,0 +1,175 @@ +package org.xbib.netty.http.server.transport; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaderValues; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import org.xbib.netty.http.server.ServerName; + +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; + +public class Http1ServerResponse implements ServerResponse { + + private final HttpVersion httpVersion; + + private final ServerRequest serverRequest; + + private final ChannelHandlerContext ctx; + + private HttpHeaders headers; + + private HttpHeaders trailingHeaders; + + public Http1ServerResponse(HttpVersion httpVersion, ServerRequest serverRequest, ChannelHandlerContext ctx) { + this.httpVersion = httpVersion; + this.serverRequest = serverRequest; + this.ctx = ctx; + this.headers = new DefaultHttpHeaders(); + this.trailingHeaders = new DefaultHttpHeaders(); + } + + @Override + public HttpHeaders getHeaders() { + return headers; + } + + @Override + public void write(String text) { + write(200, "text/plain; charset=utf-8", text); + } + + /** + * Sends an error response with the given status and default body. + * + * @param status the response status + */ + @Override + public void writeError(int status) { + writeError(status, status < 400 ? ":)" : "sorry it didn't work out :("); + } + + /** + * Sends an error response with the given status and detailed message. + * An HTML body is created containing the status and its description, + * as well as the message, which is escaped using the + * {@link #escapeHTML escape} method. + * + * @param status the response status + * @param text the text body (sent as text/html) + */ + @Override + public void writeError(int status, String text) { + write(status, "text/html; charset=utf-8", + String.format("%n%n%d %s%n" + + "

%d %s

%n

%s

%n", + status, HttpResponseStatus.valueOf(status).reasonPhrase(), + status, HttpResponseStatus.valueOf(status).reasonPhrase(), + escapeHTML(text))); + } + + @Override + public void write(int status) { + write(status, null, (ByteBuf) null); + } + + @Override + public void write(int status, String contentType, String text) { + write(status, contentType, ByteBufUtil.writeUtf8(ctx.alloc(), text)); + } + + @Override + public void write(int status, String contentType, String text, Charset charset) { + write(status, contentType, ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.allocate(text.length()).append(text), charset)); + } + + @Override + public void write(int status, String contentType, ByteBuf byteBuf) { + if (byteBuf != null) { + CharSequence s = headers.get(HttpHeaderNames.CONTENT_TYPE); + if (s == null) { + s = contentType != null ? contentType : HttpHeaderValues.APPLICATION_OCTET_STREAM; + headers.add(HttpHeaderNames.CONTENT_TYPE, s); + } + if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH) && !headers.contains(HttpHeaderNames.TRANSFER_ENCODING)) { + int length = byteBuf.readableBytes(); + if (length < 0) { + headers.add(HttpHeaderNames.TRANSFER_ENCODING, "chunked"); + } else { + headers.add(HttpHeaderNames.CONTENT_LENGTH, Long.toString(length)); + } + } + if (serverRequest != null && "close".equalsIgnoreCase(serverRequest.getRequest().headers().get(HttpHeaderNames.CONNECTION)) && + !headers.contains(HttpHeaderNames.CONNECTION)) { + headers.add(HttpHeaderNames.CONNECTION, "close"); + } + if (!headers.contains(HttpHeaderNames.DATE)) { + headers.add(HttpHeaderNames.DATE, DateTimeFormatter.RFC_1123_DATE_TIME.format(ZonedDateTime.now(ZoneOffset.UTC))); + } + headers.add(HttpHeaderNames.SERVER, ServerName.getServerName()); + } + FullHttpResponse fullHttpResponse = byteBuf != null ? + new DefaultFullHttpResponse(httpVersion, + HttpResponseStatus.valueOf(status), byteBuf, headers, trailingHeaders) : + new DefaultFullHttpResponse(httpVersion, + HttpResponseStatus.valueOf(status), Unpooled.EMPTY_BUFFER, headers, trailingHeaders); + if (ctx.channel().isWritable()) { + ctx.channel().writeAndFlush(fullHttpResponse); + } + } + + /** + * Returns an HTML-escaped version of the given string for safe display + * within a web page. The characters '&', '>' and '<' must always + * be escaped, and single and double quotes must be escaped within + * attribute values; this method escapes them always. This method can + * be used for generating both HTML and XHTML valid content. + * + * @param s the string to escape + * @return the escaped string + * @see The W3C FAQ + */ + private static String escapeHTML(String s) { + int len = s.length(); + StringBuilder es = new StringBuilder(len + 30); + int start = 0; + for (int i = 0; i < len; i++) { + String ref = null; + switch (s.charAt(i)) { + case '&': + ref = "&"; + break; + case '>': + ref = ">"; + break; + case '<': + ref = "<"; + break; + case '"': + ref = """; + break; + case '\'': + ref = "'"; + break; + default: + break; + } + if (ref != null) { + es.append(s.substring(start, i)).append(ref); + start = i + 1; + } + } + return start == 0 ? s : es.append(s.substring(start)).toString(); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerTransport.java new file mode 100644 index 0000000..4d8af49 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http1ServerTransport.java @@ -0,0 +1,39 @@ +package org.xbib.netty.http.server.transport; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http2.Http2Settings; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.context.VirtualServer; + +import java.io.IOException; + +public class Http1ServerTransport extends BaseServerTransport { + + public Http1ServerTransport(Server server) { + super(server); + } + + @Override + public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException { + int requestId = requestCounter.incrementAndGet(); + VirtualServer virtualServer = server.getVirtualServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); + if (virtualServer == null) { + virtualServer = server.getDefaultVirtualServer(); + } + HttpAddress httpAddress = server.getServerConfig().getAddress(); + ServerRequest serverRequest = new ServerRequest(virtualServer, httpAddress, fullHttpRequest, + null, requestId); + ServerResponse serverResponse = new Http1ServerResponse(httpAddress.getVersion(), serverRequest, ctx); + if (acceptRequest(serverRequest, serverResponse)) { + handle(serverRequest, serverResponse); + } + } + + @Override + public void settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) throws Exception { + + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerTransport.java new file mode 100644 index 0000000..ea5aefe --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerTransport.java @@ -0,0 +1,44 @@ +package org.xbib.netty.http.server.transport; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http2.Http2Settings; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.context.VirtualServer; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class Http2ServerTransport extends BaseServerTransport { + + private static final Logger logger = Logger.getLogger(Http2ServerTransport.class.getName()); + + public Http2ServerTransport(Server server) { + super(server); + } + + @Override + public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException { + logger.log(Level.INFO, "requestReceived"); + int requestId = requestCounter.incrementAndGet(); + VirtualServer virtualServer = server.getVirtualServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); + if (virtualServer == null) { + virtualServer = server.getDefaultVirtualServer(); + } + HttpAddress httpAddress = server.getServerConfig().getAddress(); + ServerRequest serverRequest = new ServerRequest(virtualServer, httpAddress, fullHttpRequest, + null, requestId); + ServerResponse serverResponse = new Http1ServerResponse(httpAddress.getVersion(), serverRequest, ctx); + if (acceptRequest(serverRequest, serverResponse)) { + handle(serverRequest, serverResponse); + } + } + + @Override + public void settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) throws Exception { + logger.log(Level.INFO, "settings received"); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerRequest.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerRequest.java new file mode 100644 index 0000000..eaf506f --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerRequest.java @@ -0,0 +1,50 @@ +package org.xbib.netty.http.server.transport; + +import io.netty.handler.codec.http.FullHttpRequest; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.server.context.VirtualServer; + +/** + * The {@code ServerRequest} class encapsulates a single request. + */ +public class ServerRequest { + + private final VirtualServer virtualServer; + + private final HttpAddress httpAddress; + + private final FullHttpRequest httpRequest; + + private final Integer streamId; + + private final Integer requestId; + + public ServerRequest(VirtualServer virtualServer, HttpAddress httpAddress, FullHttpRequest httpRequest, + Integer streamId, Integer requestId) { + this.virtualServer = virtualServer; + this.httpAddress = httpAddress; + this.httpRequest = httpRequest; + this.streamId = streamId; + this.requestId = requestId; + } + + public VirtualServer getVirtualServer() { + return virtualServer; + } + + public HttpAddress getHttpAddress() { + return httpAddress; + } + + public FullHttpRequest getRequest() { + return httpRequest; + } + + public Integer streamId() { + return streamId; + } + + public Integer requestId() { + return requestId; + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerResponse.java new file mode 100644 index 0000000..2643be6 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerResponse.java @@ -0,0 +1,29 @@ +package org.xbib.netty.http.server.transport; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http.HttpHeaders; + +import java.nio.charset.Charset; + +/** + * HTTP server response. + */ +public interface ServerResponse { + + HttpHeaders getHeaders(); + + void write(String text); + + void writeError(int status); + + void writeError(int status, String text); + + void write(int status); + + void write(int status, String contentType, String text); + + void write(int status, String contentType, String text, Charset charset); + + void write(int status, String contentType, ByteBuf byteBuf); + +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerTransport.java new file mode 100644 index 0000000..a62fd0e --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerTransport.java @@ -0,0 +1,18 @@ +package org.xbib.netty.http.server.transport; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.util.AttributeKey; + +import java.io.IOException; + +public interface ServerTransport { + + AttributeKey TRANSPORT_ATTRIBUTE_KEY = AttributeKey.valueOf("transport"); + + void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) throws IOException; + + void settingsReceived(ChannelHandlerContext ctx, Http2Settings http2Settings) throws Exception; + +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkClass.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkClass.java new file mode 100644 index 0000000..16d9912 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkClass.java @@ -0,0 +1,9 @@ +package org.xbib.netty.http.server.util; + +/** + * The network classes. + */ +public enum NetworkClass { + + ANY, LOOPBACK, LOCAL, PUBLIC +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkProtocolVersion.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkProtocolVersion.java new file mode 100644 index 0000000..b4f997a --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkProtocolVersion.java @@ -0,0 +1,9 @@ +package org.xbib.netty.http.server.util; + +/** + * The TCP/IP network protocol versions. + */ +public enum NetworkProtocolVersion { + + IPV4, IPV6, IPV46, NONE +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkUtils.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkUtils.java new file mode 100644 index 0000000..6f06163 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkUtils.java @@ -0,0 +1,586 @@ +package org.xbib.netty.http.server.util; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.InterfaceAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Helper class for Java networking. + */ +public class NetworkUtils { + + private static final Logger logger = Logger.getLogger(NetworkUtils.class.getName()); + + private static final String lf = System.lineSeparator(); + + private static final char[] hexDigit = new char[]{ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + private static final String IPV4_SETTING = "java.net.preferIPv4Stack"; + + private static final String IPV6_SETTING = "java.net.preferIPv6Addresses"; + + private static InetAddress localAddress; + + public static void extendSystemProperties() { + InetAddress address; + try { + address = InetAddress.getLocalHost(); + } catch (Exception e) { + logger.log(Level.WARNING, e.getMessage(), e); + address = InetAddress.getLoopbackAddress(); + } + localAddress = address; + try { + Map map = new HashMap<>(); + map.put("net.localhost", address.getCanonicalHostName()); + String hostname = address.getHostName(); + map.put("net.hostname", hostname); + InetAddress[] hostnameAddresses = InetAddress.getAllByName(hostname); + int i = 0; + for (InetAddress hostnameAddress : hostnameAddresses) { + map.put("net.hostaddress." + (i++), hostnameAddress.getCanonicalHostName()); + } + for (NetworkInterface networkInterface : getAllRunningAndUpInterfaces()) { + InetAddress inetAddress = getFirstNonLoopbackAddress(networkInterface, NetworkProtocolVersion.IPV4); + if (inetAddress != null) { + map.put("net." + networkInterface.getDisplayName(), inetAddress.getCanonicalHostName()); + } + } + logger.log(Level.FINE, "found network properties for system properties: " + map); + for (Map.Entry entry : map.entrySet()) { + System.setProperty(entry.getKey(), entry.getValue()); + } + } catch (Throwable e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + } + + private NetworkUtils() { + } + + public static boolean isPreferIPv4() { + return Boolean.getBoolean(System.getProperty(IPV4_SETTING)); + } + + public static boolean isPreferIPv6() { + return Boolean.getBoolean(System.getProperty(IPV6_SETTING)); + } + + public static InetAddress getIPv4Localhost() throws UnknownHostException { + return getLocalhost(NetworkProtocolVersion.IPV4); + } + + public static InetAddress getIPv6Localhost() throws UnknownHostException { + return getLocalhost(NetworkProtocolVersion.IPV6); + } + + public static InetAddress getLocalhost(NetworkProtocolVersion ipversion) throws UnknownHostException { + return ipversion == NetworkProtocolVersion.IPV4 ? + InetAddress.getByName("127.0.0.1") : InetAddress.getByName("::1"); + } + + public static String getLocalHostName(String defaultHostName) { + if (localAddress == null) { + return defaultHostName; + } + String hostName = localAddress.getHostName(); + if (hostName == null) { + return defaultHostName; + } + return hostName; + } + + public static String getLocalHostAddress(String defaultHostAddress) { + if (localAddress == null) { + return defaultHostAddress; + } + String hostAddress = localAddress.getHostAddress(); + if (hostAddress == null) { + return defaultHostAddress; + } + return hostAddress; + } + + public static InetAddress getLocalAddress() { + return localAddress; + } + + public static NetworkClass getNetworkClass(InetAddress address) { + if (address == null || address.isAnyLocalAddress()) { + return NetworkClass.ANY; + } + if (address.isLoopbackAddress()) { + return NetworkClass.LOOPBACK; + } + if (address.isLinkLocalAddress() || address.isSiteLocalAddress()) { + return NetworkClass.LOCAL; + } + return NetworkClass.PUBLIC; + } + + public static String format(InetAddress address) { + return format(address, -1); + } + + public static String format(InetSocketAddress address) { + return format(address.getAddress(), address.getPort()); + } + + public static String format(InetAddress address, int port) { + Objects.requireNonNull(address); + StringBuilder sb = new StringBuilder(); + if (port != -1 && address instanceof Inet6Address) { + sb.append(toUriString(address)); + } else { + sb.append(toAddrString(address)); + } + if (port != -1) { + sb.append(':').append(port); + } + return sb.toString(); + } + + public static String toUriString(InetAddress ip) { + if (ip instanceof Inet6Address) { + return "[" + toAddrString(ip) + "]"; + } + return toAddrString(ip); + } + + public static String toAddrString(InetAddress ip) { + if (ip == null) { + throw new NullPointerException("ip"); + } + if (ip instanceof Inet4Address) { + byte[] bytes = ip.getAddress(); + return (bytes[0] & 0xff) + "." + (bytes[1] & 0xff) + "." + (bytes[2] & 0xff) + "." + (bytes[3] & 0xff); + } + if (!(ip instanceof Inet6Address)) { + throw new IllegalArgumentException("ip"); + } + byte[] bytes = ip.getAddress(); + int[] hextets = new int[8]; + for (int i = 0; i < hextets.length; i++) { + hextets[i] = (bytes[2 * i] & 255) << 8 | bytes[2 * i + 1] & 255; + } + compressLongestRunOfZeroes(hextets); + return hextetsToIPv6String(hextets); + } + + public static boolean matchesNetwork(NetworkClass given, NetworkClass expected) { + switch (expected) { + case ANY: + return EnumSet.of(NetworkClass.LOOPBACK, NetworkClass.LOCAL, NetworkClass.PUBLIC, NetworkClass.ANY) + .contains(given); + case PUBLIC: + return EnumSet.of(NetworkClass.LOOPBACK, NetworkClass.LOCAL, NetworkClass.PUBLIC) + .contains(given); + case LOCAL: + return EnumSet.of(NetworkClass.LOOPBACK, NetworkClass.LOCAL) + .contains(given); + case LOOPBACK: + return NetworkClass.LOOPBACK == given; + } + return false; + } + + public static InetAddress getFirstNonLoopbackAddress(NetworkProtocolVersion ipversion) { + InetAddress address; + for (NetworkInterface networkInterface : getAllNetworkInterfaces()) { + try { + if (!networkInterface.isUp() || networkInterface.isLoopback()) { + continue; + } + } catch (Exception e) { + logger.log(Level.WARNING, e.getMessage(), e); + continue; + } + address = getFirstNonLoopbackAddress(networkInterface, ipversion); + if (address != null) { + return address; + } + } + return null; + } + + public static InetAddress getFirstNonLoopbackAddress(NetworkInterface networkInterface, NetworkProtocolVersion ipVersion) { + if (networkInterface == null) { + throw new IllegalArgumentException("network interface is null"); + } + for (Enumeration addresses = networkInterface.getInetAddresses(); addresses.hasMoreElements(); ) { + InetAddress address = addresses.nextElement(); + if (!address.isLoopbackAddress() && (address instanceof Inet4Address && ipVersion == NetworkProtocolVersion.IPV4) || + (address instanceof Inet6Address && ipVersion == NetworkProtocolVersion.IPV6)) { + return address; + } + } + return null; + } + + public static InetAddress getFirstAddress(NetworkInterface networkInterface, NetworkProtocolVersion ipVersion) { + if (networkInterface == null) { + throw new IllegalArgumentException("network interface is null"); + } + for (Enumeration addresses = networkInterface.getInetAddresses(); addresses.hasMoreElements(); ) { + InetAddress address = addresses.nextElement(); + if ((address instanceof Inet4Address && ipVersion == NetworkProtocolVersion.IPV4) || + (address instanceof Inet6Address && ipVersion == NetworkProtocolVersion.IPV6)) { + return address; + } + } + return null; + } + + public static boolean interfaceSupports(NetworkInterface networkInterface, NetworkProtocolVersion ipVersion) { + boolean supportsVersion = false; + if (networkInterface != null) { + Enumeration addresses = networkInterface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + if ((address instanceof Inet4Address && (ipVersion == NetworkProtocolVersion.IPV4)) || + (address instanceof Inet6Address && (ipVersion == NetworkProtocolVersion.IPV6))) { + supportsVersion = true; + break; + } + } + } + return supportsVersion; + } + + public static NetworkProtocolVersion getProtocolVersion() { + switch (findAvailableProtocols()) { + case IPV4: + return NetworkProtocolVersion.IPV4; + case IPV6: + return NetworkProtocolVersion.IPV6; + case IPV46: + if (Boolean.getBoolean(System.getProperty(IPV4_SETTING))) { + return NetworkProtocolVersion.IPV4; + } + if (Boolean.getBoolean(System.getProperty(IPV6_SETTING))) { + return NetworkProtocolVersion.IPV6; + } + return NetworkProtocolVersion.IPV6; + default: + break; + } + return NetworkProtocolVersion.NONE; + } + + public static NetworkProtocolVersion findAvailableProtocols() { + boolean hasIPv4 = false; + boolean hasIPv6 = false; + for (InetAddress addr : getAllAvailableAddresses()) { + if (addr instanceof Inet4Address) { + hasIPv4 = true; + } + if (addr instanceof Inet6Address) { + hasIPv6 = true; + } + } + if (hasIPv4 && hasIPv6) { + return NetworkProtocolVersion.IPV46; + } + if (hasIPv4) { + return NetworkProtocolVersion.IPV4; + } + if (hasIPv6) { + return NetworkProtocolVersion.IPV6; + } + return NetworkProtocolVersion.NONE; + } + + public static InetAddress resolveInetAddress(String hostname, String defaultValue) throws IOException { + String host = hostname; + if (host == null) { + host = defaultValue; + } + String origHost = host; + int pos = host.indexOf(':'); + if (pos > 0) { + host = host.substring(0, pos - 1); + } + if ((host.startsWith("#") && host.endsWith("#")) || (host.startsWith("_") && host.endsWith("_"))) { + host = host.substring(1, host.length() - 1); + if ("local".equals(host)) { + return getLocalAddress(); + } else if (host.startsWith("non_loopback")) { + if (host.toLowerCase(Locale.ROOT).endsWith(":ipv4")) { + return getFirstNonLoopbackAddress(NetworkProtocolVersion.IPV4); + } else if (host.toLowerCase(Locale.ROOT).endsWith(":ipv6")) { + return getFirstNonLoopbackAddress(NetworkProtocolVersion.IPV6); + } else { + return getFirstNonLoopbackAddress(getProtocolVersion()); + } + } else { + NetworkProtocolVersion networkProtocolVersion = getProtocolVersion(); + if (host.toLowerCase(Locale.ROOT).endsWith(":ipv4")) { + networkProtocolVersion = NetworkProtocolVersion.IPV4; + host = host.substring(0, host.length() - 5); + } else if (host.toLowerCase(Locale.ROOT).endsWith(":ipv6")) { + networkProtocolVersion = NetworkProtocolVersion.IPV6; + host = host.substring(0, host.length() - 5); + } + for (NetworkInterface ni : getInterfaces(NetworkUtils::isUp)) { + if (host.equals(ni.getName()) || host.equals(ni.getDisplayName())) { + if (ni.isLoopback()) { + return getFirstAddress(ni, networkProtocolVersion); + } else { + return getFirstNonLoopbackAddress(ni, networkProtocolVersion); + } + } + } + } + throw new IOException("failed to find network interface for [" + origHost + "]"); + } + return InetAddress.getByName(host); + } + + public static InetAddress resolvePublicHostAddress(String host) throws IOException { + InetAddress address = resolveInetAddress(host, null); + if (address == null || address.isAnyLocalAddress()) { + address = getFirstNonLoopbackAddress(NetworkProtocolVersion.IPV4); + if (address == null) { + address = getFirstNonLoopbackAddress(getProtocolVersion()); + if (address == null) { + address = getLocalAddress(); + if (address == null) { + return getLocalhost(NetworkProtocolVersion.IPV4); + } + } + } + } + return address; + } + + private static List getAllNetworkInterfaces() { + return getInterfaces(n -> true); + } + + public static List getAllRunningAndUpInterfaces() { + return getInterfaces(NetworkUtils::isUp); + } + + public static List getInterfaces(Predicate predicate) { + List networkInterfaces = new ArrayList<>(); + Enumeration interfaces; + try { + interfaces = NetworkInterface.getNetworkInterfaces(); + } catch (Exception e) { + return networkInterfaces; + } + while (interfaces.hasMoreElements()) { + NetworkInterface networkInterface = interfaces.nextElement(); + if (predicate.test(networkInterface)) { + networkInterfaces.add(networkInterface); + Enumeration subInterfaces = networkInterface.getSubInterfaces(); + while (subInterfaces.hasMoreElements()) { + networkInterfaces.add(subInterfaces.nextElement()); + } + } + } + sortInterfaces(networkInterfaces); + return networkInterfaces; + } + + public static List getAllAvailableAddresses() { + List allAddresses = new ArrayList<>(); + for (NetworkInterface networkInterface : getAllNetworkInterfaces()) { + Enumeration addrs = networkInterface.getInetAddresses(); + while (addrs.hasMoreElements()) { + allAddresses.add(addrs.nextElement()); + } + } + sortAddresses(allAddresses); + return allAddresses; + } + + public static String displayNetworkInterfaces() { + StringBuilder sb = new StringBuilder(); + for (NetworkInterface nic : getAllNetworkInterfaces()) { + sb.append(displayNetworkInterface(nic)); + } + return sb.toString(); + } + + public static String displayNetworkInterface(NetworkInterface nic) { + StringBuilder sb = new StringBuilder(); + sb.append(lf).append(nic.getName()).append(lf); + if (!nic.getName().equals(nic.getDisplayName())) { + sb.append("\t").append(nic.getDisplayName()).append(lf); + } + sb.append("\t").append("flags "); + List flags = new ArrayList<>(); + try { + if (nic.isUp()) { + flags.add("UP"); + } + if (nic.supportsMulticast()) { + flags.add("MULTICAST"); + } + if (nic.isLoopback()) { + flags.add("LOOPBACK"); + } + if (nic.isPointToPoint()) { + flags.add("POINTTOPOINT"); + } + if (nic.isVirtual()) { + flags.add("VIRTUAL"); + } + } catch (Exception e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + sb.append(String.join(",", flags)); + try { + sb.append(" mtu ").append(nic.getMTU()).append(lf); + } catch (SocketException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + List addresses = nic.getInterfaceAddresses(); + for (InterfaceAddress address : addresses) { + sb.append("\t").append(formatAddress(address)).append(lf); + } + try { + byte[] b = nic.getHardwareAddress(); + if (b != null) { + sb.append("\t").append("ether "); + for (int i = 0; i < b.length; i++) { + if (i > 0) { + sb.append(":"); + } + sb.append(hexDigit[(b[i] >> 4) & 0x0f]).append(hexDigit[b[i] & 0x0f]); + } + sb.append(lf); + } + } catch (SocketException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + return sb.toString(); + } + + private static void sortInterfaces(List interfaces) { + interfaces.sort(Comparator.comparingInt(NetworkInterface::getIndex)); + } + + private static void sortAddresses(List addressList) { + addressList.sort((o1, o2) -> compareBytes(o1.getAddress(), o2.getAddress())); + } + + private static String formatAddress(InterfaceAddress interfaceAddress) { + StringBuilder sb = new StringBuilder(); + InetAddress address = interfaceAddress.getAddress(); + if (address instanceof Inet6Address) { + sb.append("inet6 ").append(format(address)) + .append(" prefixlen:").append(interfaceAddress.getNetworkPrefixLength()); + } else { + int netmask = 0xFFFFFFFF << (32 - interfaceAddress.getNetworkPrefixLength()); + byte[] b = new byte[] { + (byte) (netmask >>> 24), + (byte) (netmask >>> 16 & 0xFF), + (byte) (netmask >>> 8 & 0xFF), + (byte) (netmask & 0xFF) + }; + sb.append("inet ").append(format(address)); + try { + sb.append(" netmask:").append(format(InetAddress.getByAddress(b))); + } catch (UnknownHostException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + InetAddress broadcast = interfaceAddress.getBroadcast(); + if (broadcast != null) { + sb.append(" broadcast:").append(format(broadcast)); + } + } + if (address.isLoopbackAddress()) { + sb.append(" scope:host"); + } else if (address.isLinkLocalAddress()) { + sb.append(" scope:link"); + } else if (address.isSiteLocalAddress()) { + sb.append(" scope:site"); + } + return sb.toString(); + } + + private static boolean isUp(NetworkInterface networkInterface) { + try { + return networkInterface.isUp(); + } catch (SocketException e) { + return false; + } + } + + private static int compareBytes(byte[] left, byte[] right) { + for (int i = 0, j = 0; i < left.length && j < right.length; i++, j++) { + int a = left[i] & 0xff; + int b = right[j] & 0xff; + if (a != b) { + return a - b; + } + } + return left.length - right.length; + } + + private static void compressLongestRunOfZeroes(int[] hextets) { + int bestRunStart = -1; + int bestRunLength = -1; + int runStart = -1; + for (int i = 0; i < hextets.length + 1; i++) { + if (i < hextets.length && hextets[i] == 0) { + if (runStart < 0) { + runStart = i; + } + } else if (runStart >= 0) { + int runLength = i - runStart; + if (runLength > bestRunLength) { + bestRunStart = runStart; + bestRunLength = runLength; + } + runStart = -1; + } + } + if (bestRunLength >= 2) { + Arrays.fill(hextets, bestRunStart, bestRunStart + bestRunLength, -1); + } + } + + private static String hextetsToIPv6String(int[] hextets) { + StringBuilder sb = new StringBuilder(39); + boolean lastWasNumber = false; + for (int i = 0; i < hextets.length; i++) { + boolean b = hextets[i] >= 0; + if (b) { + if (lastWasNumber) { + sb.append(':'); + } + sb.append(Integer.toHexString(hextets[i])); + } else { + if (i == 0 || lastWasNumber) { + sb.append("::"); + } + } + lastWasNumber = b; + } + return sb.toString(); + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/util/package-info.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/util/package-info.java new file mode 100644 index 0000000..9b83f72 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/util/package-info.java @@ -0,0 +1,4 @@ +/** + * Utilities for Netty HTTP server. + */ +package org.xbib.netty.http.server.util; diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java new file mode 100644 index 0000000..6e2cf62 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp1Test.java @@ -0,0 +1,61 @@ +package org.xbib.netty.http.server.test; + +import org.junit.Test; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.server.Server; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class CleartextHttp1Test extends LoggingBase { + + private static final Logger logger = Logger.getLogger(""); + + @Test + public void testClearTextHttp1() throws Exception { + int loop = 1024; + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + Server server = Server.builder() + //.enableDebug() + .bind(httpAddress).build(); + server.getDefaultVirtualServer().addContext("/", (request, response) -> { + response.write("Hello World " + request.getRequest().content().toString(StandardCharsets.UTF_8)); + }); + server.accept(); + Client httpClient = Client.builder() + //.enableDebug() + .build(); + AtomicInteger counter = new AtomicInteger(); + try { + // will not work for several thousands channels - "java.net.SocketException: Too many open files in system" + for (int i = 0; i < loop; i++) { + Request request = Request.get().setVersion("HTTP/1.1") + .url(server.getServerConfig().getAddress().base()) + .addParameter("test", Integer.toString(i)) + .content(Integer.toString(i), "text/plain") + .build() + .setResponseListener(fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); + counter.incrementAndGet(); + }); + Transport transport = httpClient.execute(request); + if (transport.isFailed()) { + logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); + break; + } + // each execution needs to be synchronized + transport.get(); + } + } finally { + httpClient.shutdownGracefully(); + server.shutdownGracefully(); + } + logger.log(Level.INFO, "counter=" + counter.get()); + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java new file mode 100644 index 0000000..243e23a --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/CleartextHttp2Test.java @@ -0,0 +1,73 @@ +package org.xbib.netty.http.server.test; + +import io.netty.channel.WriteBufferWaterMark; +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.Request; +import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.server.Server; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class CleartextHttp2Test extends LoggingBase { + + private static final Logger logger = Logger.getLogger(CleartextHttp2Test.class.getName()); + + @Test + public void testCleartextHttp2() throws Exception { + int loop = 1; + // we assume slow server and reserve a large write buffer for the client, 32-64 bytes for each request, + // to avoid channel.isWritable() drop-outs + int low = 32 * loop; + int high = 64 * loop; + HttpAddress httpAddress = HttpAddress.of("localhost", 8008, HttpVersion.valueOf("HTTP/2.0"), false); + Server server = Server.builder() + .enableDebug() + .bind(httpAddress) + .build(); + //server.logDiagnostics(Level.INFO); + server.getDefaultVirtualServer().addContext("/", (request, response) -> + response.write("Hello World " + request.getRequest().content().toString(StandardCharsets.UTF_8))); + server.accept(); + Client httpClient = Client.builder() + .enableDebug() + .setWriteBufferWaterMark(new WriteBufferWaterMark(low, high)) + .build(); + AtomicInteger counter = new AtomicInteger(); + try { + URL serverURL = server.getServerConfig().getAddress().base(); + HttpVersion serverVersion = server.getServerConfig().getAddress().getVersion(); + // yes, HTTP/2 uses a single transport, and we can send many thousand requests per second asynchronously + Transport transport = httpClient.newTransport(serverURL, serverVersion); + for (int i = 0; i < loop; i++) { + Request request = Request.get().setVersion("HTTP/2.0") + .url(server.getServerConfig().getAddress().base()) + .content(Integer.toString(i), "text/plain") + .build() + .setResponseListener(fullHttpResponse -> { + String content = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + logger.log(Level.INFO, fullHttpResponse.toString() + " content=" + content); + counter.incrementAndGet(); + }); + // submit request + transport.execute(request); + if (transport.isFailed()) { + logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); + break; + } + } + // wait for transport to complete + transport.get(); + } finally { + httpClient.shutdownGracefully(); + server.shutdownGracefully(); + } + logger.log(Level.INFO, "counter = " + counter.get()); + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/LoggingBase.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/LoggingBase.java new file mode 100644 index 0000000..18f49d7 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/LoggingBase.java @@ -0,0 +1,26 @@ +package org.xbib.netty.http.server.test; + +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +public class LoggingBase { + + static { + System.setProperty("java.util.logging.SimpleFormatter.format", + "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); + LogManager.getLogManager().reset(); + Logger rootLogger = LogManager.getLogManager().getLogger(""); + Handler handler = new ConsoleHandler(); + handler.setFormatter(new SimpleFormatter()); + rootLogger.addHandler(handler); + rootLogger.setLevel(Level.ALL); + for (Handler h : rootLogger.getHandlers()) { + handler.setFormatter(new SimpleFormatter()); + h.setLevel(Level.ALL); + } + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/MultithreadedCleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/MultithreadedCleartextHttp2Test.java new file mode 100644 index 0000000..dab5ad2 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/MultithreadedCleartextHttp2Test.java @@ -0,0 +1,92 @@ +package org.xbib.netty.http.server.test; + +import io.netty.channel.WriteBufferWaterMark; +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.Request; +import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.server.Server; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class MultithreadedCleartextHttp2Test extends LoggingBase { + + private static final Logger logger = Logger.getLogger(""); + + /** + * 2018-03-09 18:27:08.975 WARNUNG [io.netty.channel.ChannelInitializer] + * io.netty.channel.ChannelInitializer exceptionCaught Failed to initialize a channel. + * Closing: [id: 0x4af3e71a, L:/127.0.0.1:8008 - R:/127.0.0.1:59996] + * io.netty.channel.ChannelPipelineException: org.xbib.netty.http.server.handler.Http2ServerConnectionHandler + * is not a @Sharable handler, so can't be added or removed multiple times. + * @throws Exception if test fails + */ + @Test + public void testmultithreadedCleartextHttp2() throws Exception { + int loop = 1000; + int threads = 4; + // we assume slow server and reserve a large write buffer for the client, 32-64 bytes for each request, + // to avoid channel.isWritable() drop-outs + int low = 32 * loop; + int high = 64 * loop; + HttpAddress httpAddress = HttpAddress.of("localhost", 8008, HttpVersion.valueOf("HTTP/2.0"), false); + Server server = Server.builder() + .bind(httpAddress) + .build(); + server.getDefaultVirtualServer().addContext("/", (request, response) -> + response.write("Hello World " + request.getRequest().content().toString(StandardCharsets.UTF_8))); + server.accept(); + Client httpClient = Client.builder() + .setWriteBufferWaterMark(new WriteBufferWaterMark(low, high)) + .build(); + AtomicInteger counter = new AtomicInteger(); + try { + URL serverURL = server.getServerConfig().getAddress().base(); + HttpVersion serverVersion = server.getServerConfig().getAddress().getVersion(); + ExecutorService executorService = Executors.newFixedThreadPool(threads); + for (int n = 0; n < threads; n++) { + executorService.submit(() -> { + try { + Transport transport = httpClient.newTransport(serverURL, serverVersion); + for (int i = 0; i < loop; i++) { + Request request = Request.get().setVersion("HTTP/2.0") + .content(Integer.toString(i), "text/plain") + .build() + .setResponseListener(fullHttpResponse -> { + String content = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, fullHttpResponse.toString() + " content=" + content); + counter.incrementAndGet(); + }); + // submit request + transport.execute(request); + if (transport.isFailed()) { + logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); + break; + } + } + // wait for transport to complete + transport.get(); + } catch (IOException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + }); + } + executorService.shutdown(); + executorService.awaitTermination(60, TimeUnit.SECONDS); + } finally { + httpClient.shutdownGracefully(); + server.shutdownGracefully(); + } + logger.log(Level.INFO, "counter = " + counter.get()); + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/PooledCleartextHttp1Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/PooledCleartextHttp1Test.java new file mode 100644 index 0000000..4fe6ada --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/PooledCleartextHttp1Test.java @@ -0,0 +1,63 @@ +package org.xbib.netty.http.server.test; + +import org.junit.Test; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.server.Server; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class PooledCleartextHttp1Test extends LoggingBase { + + private static final Logger logger = Logger.getLogger(""); + + @Test + public void testClearTextHttp1() throws Exception { + int loop = 10000; + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + Server server = Server.builder() + //.enableDebug() + .bind(httpAddress).build(); + server.getDefaultVirtualServer().addContext("/", (request, response) -> { + response.write("Hello World " + request.getRequest().content().toString(StandardCharsets.UTF_8)); + }); + server.accept(); + org.xbib.netty.http.common.HttpAddress poolNode = org.xbib.netty.http.common.HttpAddress.http1("localhost", 8008); + Client httpClient = Client.builder() + //.enableDebug() + .addPoolNode(poolNode) + .setPoolNodeConnectionLimit(8) + .build(); + AtomicInteger counter = new AtomicInteger(); + try { + for (int i = 0; i < loop; i++) { + Request request = Request.get().setVersion("HTTP/1.1") + .url(server.getServerConfig().getAddress().base()) + .addParameter("test", Integer.toString(i)) + .content(Integer.toString(i), "text/plain") + .build() + .setResponseListener(fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); + counter.incrementAndGet(); + }); + Transport transport = httpClient.pooledExecute(request); + if (transport.isFailed()) { + logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); + break; + } + // each execution needs to be synchronized + transport.get(); + } + } finally { + httpClient.shutdownGracefully(); + server.shutdownGracefully(); + } + logger.log(Level.INFO, "counter=" + counter.get()); + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java new file mode 100644 index 0000000..231afd9 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp1Test.java @@ -0,0 +1,53 @@ +package org.xbib.netty.http.server.test; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Test; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.server.Server; + +import java.nio.charset.StandardCharsets; +import java.security.Security; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class SecureHttp1Test extends LoggingBase { + + private static final Logger logger = Logger.getLogger(""); + + @Test + public void testSecureHttp1() throws Exception { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + Server server = Server.builder().bind(HttpAddress.secureHttp1("localhost", 8143)) + .setSelfCert() + .build(); + Client httpClient = Client.builder() + .trustInsecure() + .build(); + try { + server.getDefaultVirtualServer().addContext("/", (request, response) -> + response.write("Hello World")); + server.accept(); + httpClient.execute(Request.get().setVersion("HTTP/1.1") + .url(server.getServerConfig().getAddress().base()) + .build() + .setResponseListener(fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); + })).get(); + httpClient.execute(Request.get().setVersion("HTTP/1.1") + .url(server.getServerConfig().getAddress().base()) + .build() + .setResponseListener(fullHttpResponse -> { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); + })).get(); + } finally { + httpClient.shutdownGracefully(); + server.shutdownGracefully(); + } + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java new file mode 100644 index 0000000..22c381a --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureHttp2Test.java @@ -0,0 +1,76 @@ +package org.xbib.netty.http.server.test; + +import io.netty.channel.WriteBufferWaterMark; +import io.netty.handler.codec.http.HttpVersion; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Test; +import org.xbib.net.URL; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.transport.Transport; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.server.Server; + +import java.nio.charset.StandardCharsets; +import java.security.Security; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class SecureHttp2Test extends LoggingBase { + + private static final Logger logger = Logger.getLogger(""); + + @Test + public void testSecureHttp2() throws Exception { + // for self-signed certificate, we need Bouncycastle + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } + int threads = 4; + int loop = 100000; + // we assume slow server and reserve a large write buffer for the client, 32-64 bytes for each request, + // to avoid channel.isWritable() drop-outs + int low = 32 * loop; + int high = 64 * loop; + + Server server = Server.builder().bind(HttpAddress.http2("localhost", 8143)) + .setSelfCert() + .build(); + //server.logDiagnostics(Level.INFO); + server.getDefaultVirtualServer().addContext("/", (request, response) -> + response.write("Hello World " + request.getRequest().content().toString(StandardCharsets.UTF_8))); + server.accept(); + Client httpClient = Client.builder() + .trustInsecure() + .setWriteBufferWaterMark(new WriteBufferWaterMark(low, high)) + .build(); + AtomicInteger counter = new AtomicInteger(); + try { + URL serverURL = server.getServerConfig().getAddress().base(); + HttpVersion serverVersion = server.getServerConfig().getAddress().getVersion(); + Transport transport = httpClient.newTransport(serverURL, serverVersion); + for (int i = 0; i < loop; i++) { + Request request = Request.get().setVersion("HTTP/2.0") + .url(server.getServerConfig().getAddress().base()) + .content(Integer.toString(i), "text/plain") + .build() + .setResponseListener(fullHttpResponse -> { + String content = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, fullHttpResponse.toString() + " content=" + content); + counter.incrementAndGet(); + }); + transport.execute(request); + if (transport.isFailed()) { + logger.log(Level.WARNING, transport.getFailure().getMessage(), transport.getFailure()); + break; + } + } + transport.get(); + } finally { + httpClient.shutdownGracefully(); + server.shutdownGracefully(); + } + logger.log(Level.INFO, "counter = " + counter.get()); + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SelfSignedCertificateTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SelfSignedCertificateTest.java new file mode 100644 index 0000000..7c30472 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SelfSignedCertificateTest.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.server.test; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.Test; +import org.xbib.netty.http.server.security.tls.SelfSignedCertificate; + +import java.security.Security; +import java.util.logging.Logger; + +/** + */ +public class SelfSignedCertificateTest { + + @Test + public void testSelfSignedCertificate() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate("localhost"); + selfSignedCertificate.exportPEM(Logger.getLogger("test")); + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java new file mode 100644 index 0000000..0610458 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ServerTest.java @@ -0,0 +1,20 @@ +package org.xbib.netty.http.server.test; + +import org.junit.Test; +import org.xbib.netty.http.server.Server; + +public class ServerTest { + + @Test + public void testServer() throws Exception { + Server server = Server.builder() + .build(); + server.getDefaultVirtualServer().addContext("/", (request, response) -> + response.write("Hello World")); + try { + server.accept().channel().closeFuture().sync(); + } finally { + server.shutdownGracefully(); + } + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedCleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedCleartextHttp2Test.java new file mode 100644 index 0000000..e681b69 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedCleartextHttp2Test.java @@ -0,0 +1,241 @@ +package org.xbib.netty.http.server.test.multithread; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.WriteBufferWaterMark; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +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.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http2.DefaultHttp2Connection; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2ConnectionPrefaceAndSettingsFrameWrittenEvent; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; +import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +public class MultithreadedCleartextHttp2Test { + + private static final Logger clientLogger = Logger.getLogger("client"); + private static final Logger serverLogger = Logger.getLogger("server"); + + private static final Level level = Level.FINE; + + static { + System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); + System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); + //System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); + //System.setProperty("io.netty.leakDetection.level", "paranoid"); + + // expand Java logging to full level + System.setProperty("java.util.logging.SimpleFormatter.format", + "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); + LogManager.getLogManager().reset(); + Logger rootLogger = LogManager.getLogManager().getLogger(""); + Handler handler = new ConsoleHandler(); + handler.setFormatter(new SimpleFormatter()); + rootLogger.addHandler(handler); + rootLogger.setLevel(Level.OFF); + for (Handler h : rootLogger.getHandlers()) { + handler.setFormatter(new SimpleFormatter()); + h.setLevel(Level.ALL); + } + } + + private final InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8008); + + private final CompletableFuture settingsPrefaceFuture = new CompletableFuture<>(); + + private final CompletableFuture responseFuture = new CompletableFuture<>(); + + private final int threads = 10; + + private final int requestsPerThread = 100000; + + private final AtomicInteger responseCounter = new AtomicInteger(); + + @Test + public void testMultiThreadedHttp2() throws Exception { + + EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup(); + EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); + + try { + Http2Connection http2ServerConnection = new DefaultHttp2Connection(true); + ServerBootstrap serverBootstrap = new ServerBootstrap() + .group(serverEventLoopGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline() + .addLast("server-connection-handler", new HttpToHttp2ConnectionHandlerBuilder() + .initialSettings(Http2Settings.defaultSettings()) + .connection(http2ServerConnection) + .frameListener(new InboundHttp2ToHttpAdapterBuilder(http2ServerConnection) + .maxContentLength(10 * 1024 * 1024) + .propagateSettings(true) + .build()) + .build()) + .addLast("server-handler", new ServerHandler()); + } + }) + .option(ChannelOption.ALLOCATOR, ByteBufAllocator.DEFAULT) + .option(ChannelOption.SO_REUSEADDR, true) + .option(ChannelOption.SO_RCVBUF, 64 * 1024) + .option(ChannelOption.SO_BACKLOG, 8 * 1024) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5 * 1000) + .childOption(ChannelOption.ALLOCATOR, ByteBufAllocator.DEFAULT) + .childOption(ChannelOption.SO_REUSEADDR, true) + .childOption(ChannelOption.TCP_NODELAY, true) + .childOption(ChannelOption.SO_SNDBUF, 64 * 1024) + .childOption(ChannelOption.SO_RCVBUF, 64 * 1024) + .childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5 * 1000) + .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(32 * 1024, 64 * 1024)); + Channel serverChannel = serverBootstrap.bind(inetSocketAddress).sync().channel(); + serverLogger.log(level, "server up, channel = " + serverChannel); + + Http2Connection http2ClientConnection = new DefaultHttp2Connection(false); + Bootstrap clientBootstrap = new Bootstrap() + .group(clientEventLoopGroup) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline() + .addLast("client-connection-handler", new HttpToHttp2ConnectionHandlerBuilder() + .initialSettings(Http2Settings.defaultSettings()) + .connection(http2ClientConnection) + .frameListener(new InboundHttp2ToHttpAdapterBuilder(http2ClientConnection) + .maxContentLength(10 * 1024 * 1024) + .propagateSettings(true) + .build()) + .build()) + .addLast("client-handler", new ClientHandler()); + } + }) + .option(ChannelOption.TCP_NODELAY, true) + .option(ChannelOption.SO_KEEPALIVE, true) + .option(ChannelOption.SO_REUSEADDR, true) + .option(ChannelOption.SO_SNDBUF, 64 * 1024) + .option(ChannelOption.SO_RCVBUF, 64 * 1024) + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5 * 1000) + .option(ChannelOption.WRITE_BUFFER_WATER_MARK,new WriteBufferWaterMark(32 * 1024, 64 * 1024)); + Channel clientChannel = clientBootstrap.connect(inetSocketAddress).sync().channel(); + clientLogger.log(level, "client connected, channel = " + clientChannel); + + settingsPrefaceFuture.get(5L, TimeUnit.SECONDS); + if (!settingsPrefaceFuture.isDone()) { + throw new RuntimeException("no settings and preface written, unable to continue"); + } else { + clientLogger.log(level, "settings and preface written, let's start"); + } + + clientLogger.log(Level.INFO, "start"); + + ExecutorService executorService = Executors.newFixedThreadPool(threads); + for (int i = 0; i < threads; i++) { + final int t = i; + executorService.submit(() -> { + for (int j = 0; j < requestsPerThread; j++) { + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, + "http://localhost:8008/foobar/" + t + "/" + j); + request.headers().add(HttpHeaderNames.HOST, inetSocketAddress.getHostName() + ":" + inetSocketAddress.getPort()); + clientChannel.write(request); + } + clientChannel.flush(); + }); + } + executorService.shutdown(); + executorService.awaitTermination(60, TimeUnit.SECONDS); + + clientLogger.log(level, "waiting"); + responseFuture.get(60, TimeUnit.SECONDS); + if (responseFuture.isDone()) { + clientLogger.log(Level.INFO, "done"); + } + + } finally { + clientEventLoopGroup.shutdownGracefully(); + serverEventLoopGroup.shutdownGracefully(); + clientEventLoopGroup.awaitTermination(5, TimeUnit.SECONDS); + clientLogger.log(level, "client shutdown"); + serverEventLoopGroup.awaitTermination(5, TimeUnit.SECONDS); + serverLogger.log(level, "server shutdown"); + } + } + + class ClientHandler extends ChannelDuplexHandler { + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof Http2ConnectionPrefaceAndSettingsFrameWrittenEvent) { + settingsPrefaceFuture.complete(ctx); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof FullHttpResponse) { + responseCounter.getAndIncrement(); + } + if (responseCounter.get() == threads * requestsPerThread) { + responseFuture.complete(true); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientLogger.log(Level.WARNING, cause.getMessage(), cause); + } + } + + class ServerHandler extends ChannelDuplexHandler { + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof FullHttpRequest) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.OK); + ctx.writeAndFlush(response); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + serverLogger.log(Level.WARNING, cause.getMessage(), cause); + } + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedMultiplexCodecCleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedMultiplexCodecCleartextHttp2Test.java new file mode 100644 index 0000000..268d40a --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/multithread/MultithreadedMultiplexCodecCleartextHttp2Test.java @@ -0,0 +1,276 @@ +package org.xbib.netty.http.server.test.multithread; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +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.HttpMethod; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerUpgradeHandler; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2ConnectionPrefaceAndSettingsFrameWrittenEvent; +import io.netty.handler.codec.http2.Http2Headers; +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.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.Http2StreamChannel; +import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; +import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; +import io.netty.util.AsciiString; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +/** + * + * Multithreaded Http2MultiplexCodec demo for cleartext HTTP/2 between a server and a client. + * + */ +public class MultithreadedMultiplexCodecCleartextHttp2Test { + + private static final Logger clientLogger = Logger.getLogger("client"); + private static final Logger serverLogger = Logger.getLogger("server"); + + private Level level = Level.FINE; + + static { + System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); + System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); + //System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); + //System.setProperty("io.netty.leakDetection.level", "paranoid"); + + // expand Java logging to full level + System.setProperty("java.util.logging.SimpleFormatter.format", + "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); + LogManager.getLogManager().reset(); + Logger rootLogger = LogManager.getLogManager().getLogger(""); + Handler handler = new ConsoleHandler(); + handler.setFormatter(new SimpleFormatter()); + rootLogger.addHandler(handler); + rootLogger.setLevel(Level.INFO); + for (Handler h : rootLogger.getHandlers()) { + handler.setFormatter(new SimpleFormatter()); + h.setLevel(Level.ALL); + } + } + + private final InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8008); + + private final CompletableFuture settingsPrefaceFuture = new CompletableFuture<>(); + + private final CompletableFuture responseFuture = new CompletableFuture<>(); + + private final int threads = 10; + + private final int requestsPerThread = 100000; + + private final AtomicInteger responseCounter = new AtomicInteger(); + + @Test + public void testMultithreadedMultiplexHttp2() throws Exception { + + EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup(); + EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); + + try { + ServerBootstrap serverBootstrap = new ServerBootstrap() + .group(serverEventLoopGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ChannelPipeline p = ch.pipeline(); + Http2MultiplexCodec serverMultiplexCodec = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer() { + @Override + protected void initChannel(Channel channel) { + ChannelPipeline p = channel.pipeline(); + p.addLast("multiplex-server-frame-converter", new Http2StreamFrameToHttpObjectCodec(true)); + p.addLast("multiplex-server-chunk-aggregator", new HttpObjectAggregator(1048576)); + p.addLast("multiplex-server-request-handler", new ServerRequestHandler()); + } + }) + .initialSettings(Http2Settings.defaultSettings()) + .build(); + HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> { + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec("server-codec", serverMultiplexCodec); + } else { + return null; + } + }; + HttpServerCodec sourceCodec = new HttpServerCodec(); + HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory); + CleartextHttp2ServerUpgradeHandler cleartextHttp2ServerUpgradeHandler = + new CleartextHttp2ServerUpgradeHandler(sourceCodec, upgradeHandler, serverMultiplexCodec); + p.addLast("server-upgrade", cleartextHttp2ServerUpgradeHandler); + p.addLast("server-messages", new ServerMessages()); + } + }); + Channel serverChannel = serverBootstrap.bind(inetSocketAddress).sync().channel(); + serverLogger.log(level, "server up, channel = " + serverChannel); + + Bootstrap clientBootstrap = new Bootstrap() + .group(clientEventLoopGroup) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast("client-codec", Http2MultiplexCodecBuilder.forClient(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + // unused + throw new IllegalStateException(); + } + }) + .initialSettings(Http2Settings.defaultSettings()) + .build()); + p.addLast("client-messages", new ClientMessages()); + } + }); + Channel clientChannel = clientBootstrap.connect(inetSocketAddress).sync().channel(); + clientLogger.log(level, "client connected, channel = " + clientChannel); + + settingsPrefaceFuture.get(5L, TimeUnit.SECONDS); + if (!settingsPrefaceFuture.isDone()) { + throw new RuntimeException("no settings and preface written, unable to continue"); + } else { + clientLogger.log(level, "settings and preface written, let's start"); + } + + clientLogger.log(Level.INFO, "start"); + + ChannelInitializer streamChannelInitializer = new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast("child-client-frame-converter", new Http2StreamFrameToHttpObjectCodec(false)); + p.addLast("child-client-chunk-aggregator", new HttpObjectAggregator(1048576)); + p.addLast("child-client-response-handler", new ClientResponseHandler()); + } + }; + + ExecutorService executorService = Executors.newFixedThreadPool(threads); + for (int i = 0; i < threads; i++) { + final int t = i; + executorService.submit(() -> { + for (int j = 0; j < requestsPerThread; j++) { + Http2StreamChannel childChannel = new Http2StreamChannelBootstrap(clientChannel) + .handler(streamChannelInitializer).open().syncUninterruptibly().getNow(); + Http2Headers request = new DefaultHttp2Headers().method(HttpMethod.GET.asciiName()) + .path("/foobar/" + t + "/" + j) + .scheme("http") + .authority(inetSocketAddress.getHostName()); + childChannel.write(new DefaultHttp2HeadersFrame(request, true)); + //do not close child channel after write, a response is expected + } + }); + clientChannel.flush(); + } + executorService.shutdown(); + executorService.awaitTermination(60, TimeUnit.SECONDS); + + clientLogger.log(level, "waiting for response future"); + responseFuture.get(60, TimeUnit.SECONDS); + if (responseFuture.isDone()) { + clientLogger.log(Level.INFO, "stop"); + } + } finally { + clientEventLoopGroup.shutdownGracefully(); + serverEventLoopGroup.shutdownGracefully(); + clientEventLoopGroup.awaitTermination(5, TimeUnit.SECONDS); + clientLogger.log(level, "client shutdown"); + serverEventLoopGroup.awaitTermination(5, TimeUnit.SECONDS); + serverLogger.log(level, "server shutdown"); + } + } + + class ClientResponseHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) { + responseCounter.getAndIncrement(); + if (responseCounter.get() == threads * requestsPerThread) { + responseFuture.complete(true); + } + } + } + + class ClientMessages extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + // settings + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + if (evt instanceof Http2ConnectionPrefaceAndSettingsFrameWrittenEvent) { + settingsPrefaceFuture.complete(true); + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientLogger.log(Level.WARNING, cause.getMessage(), cause); + } + } + + class ServerRequestHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) { + DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.OK); + ctx.writeAndFlush(response); + } + } + + class ServerMessages extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + // settings + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + serverLogger.log(Level.WARNING, cause.getMessage(), cause); + } + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/package-info.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/package-info.java new file mode 100644 index 0000000..98f50c3 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/package-info.java @@ -0,0 +1,4 @@ +/** + * Tests for Netty HTTP server. + */ +package org.xbib.netty.http.server.test; diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/CleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/CleartextHttp2Test.java new file mode 100644 index 0000000..3fa08ba --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/CleartextHttp2Test.java @@ -0,0 +1,246 @@ +package org.xbib.netty.http.server.test.simple; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpRequest; +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.HttpHeaderNames; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http2.DefaultHttp2Connection; +import io.netty.handler.codec.http2.Http2Connection; +import io.netty.handler.codec.http2.Http2ConnectionPrefaceAndSettingsFrameWrittenEvent; +import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder; +import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +public class CleartextHttp2Test { + + private static final Logger clientLogger = Logger.getLogger("client"); + private static final Logger serverLogger = Logger.getLogger("server"); + + private static final LogLevel logLevel = LogLevel.DEBUG; + private static final Level level = Level.FINE; + + private static final Http2FrameLogger serverFrameLogger = new Http2FrameLogger(logLevel, "server"); + private static final Http2FrameLogger clientFrameLogger = new Http2FrameLogger(logLevel, "client"); + + static { + System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); + System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); + //System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); + //System.setProperty("io.netty.leakDetection.level", "paranoid"); + + // expand Java logging to full level + System.setProperty("java.util.logging.SimpleFormatter.format", + "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); + LogManager.getLogManager().reset(); + Logger rootLogger = LogManager.getLogManager().getLogger(""); + Handler handler = new ConsoleHandler(); + handler.setFormatter(new SimpleFormatter()); + rootLogger.addHandler(handler); + rootLogger.setLevel(Level.ALL); + for (Handler h : rootLogger.getHandlers()) { + handler.setFormatter(new SimpleFormatter()); + h.setLevel(Level.ALL); + } + } + + private final InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8008); + + private final CompletableFuture settingsPrefaceFuture = new CompletableFuture<>(); + + private final CompletableFuture completableFuture = new CompletableFuture<>(); + + @Test + public void testHttp2() throws Exception { + + EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup(); + EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); + + try { + Http2Connection http2ServerConnection = new DefaultHttp2Connection(true); + ServerBootstrap serverBootstrap = new ServerBootstrap() + .group(serverEventLoopGroup) + .channel(NioServerSocketChannel.class) + .handler(serverFrameLogger) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline() + .addLast("server-traffic", new TrafficLoggingHandler("server-traffic", logLevel)) + .addLast("server-connection-handler", new HttpToHttp2ConnectionHandlerBuilder() + .initialSettings(Http2Settings.defaultSettings()) + .connection(http2ServerConnection) + .frameListener(new InboundHttp2ToHttpAdapterBuilder(http2ServerConnection) + .maxContentLength(10 * 1024 * 1024) + .propagateSettings(true) + .build()) + .frameLogger(serverFrameLogger) + .build()) + .addLast("server-handler", new ServerHandler()); + } + }); + Channel serverChannel = serverBootstrap.bind(inetSocketAddress).sync().channel(); + serverLogger.log(level, "server up, channel = " + serverChannel); + + Http2Connection http2ClientConnection = new DefaultHttp2Connection(false); + Bootstrap clientBootstrap = new Bootstrap() + .group(clientEventLoopGroup) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ch.pipeline() + .addLast("client-traffic", new TrafficLoggingHandler("client-traffic", logLevel)) + .addLast("client-connection-handler", new HttpToHttp2ConnectionHandlerBuilder() + .initialSettings(Http2Settings.defaultSettings()) + .connection(http2ClientConnection) + .frameListener(new InboundHttp2ToHttpAdapterBuilder(http2ClientConnection) + .maxContentLength(10 * 1024 * 1024) + .propagateSettings(true) + .build()) + .frameLogger(clientFrameLogger) + .build()) + .addLast("client-handler", new ClientHandler()); + } + }); + Channel clientChannel = clientBootstrap.connect(inetSocketAddress).sync().channel(); + clientLogger.log(level, "client connected, channel = " + clientChannel); + + settingsPrefaceFuture.get(5L, TimeUnit.SECONDS); + if (!settingsPrefaceFuture.isDone()) { + throw new RuntimeException("no settings and preface written, unable to continue"); + } else { + clientLogger.log(level, "settings and preface written, let's start"); + } + + clientLogger.log(Level.INFO, "start"); + + FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, + "http://localhost:8008/foobar/0/0"); + request.headers().add(HttpHeaderNames.HOST, inetSocketAddress.getHostName() + ":" + inetSocketAddress.getPort()); + clientChannel.writeAndFlush(request); + + clientLogger.log(level, "waiting"); + completableFuture.get(10, TimeUnit.SECONDS); + if (completableFuture.isDone()) { + clientLogger.log(Level.INFO, "done"); + } + + } finally { + clientEventLoopGroup.shutdownGracefully(); + serverEventLoopGroup.shutdownGracefully(); + clientEventLoopGroup.awaitTermination(5, TimeUnit.SECONDS); + clientLogger.log(level, "client shutdown"); + serverEventLoopGroup.awaitTermination(5, TimeUnit.SECONDS); + serverLogger.log(level, "server shutdown"); + } + } + + class ClientHandler extends ChannelDuplexHandler { + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + clientLogger.log(level, "got event on client " + evt); + if (evt instanceof Http2ConnectionPrefaceAndSettingsFrameWrittenEvent) { + settingsPrefaceFuture.complete(ctx); + } + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + clientLogger.log(level, "msg received on client " + msg + " class=" + msg.getClass()); + if (msg instanceof FullHttpResponse) { + completableFuture.complete(true); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientLogger.log(Level.WARNING, cause.getMessage(), cause); + } + } + + class ServerHandler extends ChannelDuplexHandler { + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + serverLogger.log(level, "got event on server " + evt); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + serverLogger.log(level, "msg received on server " + msg + " class=" + msg.getClass()); + if (msg instanceof FullHttpRequest) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.OK); + serverLogger.log(Level.INFO, "writing server response: " + response); + ctx.writeAndFlush(response); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + serverLogger.log(Level.WARNING, cause.getMessage(), cause); + } + } + + class TrafficLoggingHandler extends LoggingHandler { + + TrafficLoggingHandler(String name, LogLevel level) { + super(name, 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); + } + } + } +} diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/MultiplexCodecCleartextHttp2Test.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/MultiplexCodecCleartextHttp2Test.java new file mode 100644 index 0000000..7a9b666 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/simple/MultiplexCodecCleartextHttp2Test.java @@ -0,0 +1,311 @@ +package org.xbib.netty.http.server.test.simple; + +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +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.HttpMethod; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerUpgradeHandler; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.codec.http2.CleartextHttp2ServerUpgradeHandler; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.Http2CodecUtil; +import io.netty.handler.codec.http2.Http2ConnectionPrefaceAndSettingsFrameWrittenEvent; +import io.netty.handler.codec.http2.Http2FrameLogger; +import io.netty.handler.codec.http2.Http2Headers; +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.codec.http2.Http2Settings; +import io.netty.handler.codec.http2.Http2StreamChannel; +import io.netty.handler.codec.http2.Http2StreamChannelBootstrap; +import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec; +import io.netty.handler.logging.LogLevel; +import io.netty.handler.logging.LoggingHandler; +import io.netty.util.AsciiString; +import org.junit.Test; + +import java.net.InetSocketAddress; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.logging.ConsoleHandler; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; +import java.util.logging.Logger; +import java.util.logging.SimpleFormatter; + +/** + * + * Http2MultiplexCodec demo for cleartext HTTP/2 between a server and a client. + * + * What is HTTP/2 multiplex codec? + * + * codec-http2 currently has two flavors of APIs: + * + *
    + *
  • Http2ConnectionHandler and FrameListener - the initial API, lots of hours/usage on this code, + * low object allocation, not canonical Netty design (extensibility via the channel pipeline is challenging)
  • + *
  • Http2FrameCodec and Http2MultiplexCodec - new API design which is more canonical Netty, + * more object allocation, not as much hours/usage. The FrameListener API exposure is minimized + * from the Http2MultiplexCodec and ideally its usage of the FrameListener is an implementation detail.
  • + *
+ * + * + */ +public class MultiplexCodecCleartextHttp2Test { + + private static final Logger clientLogger = Logger.getLogger("client"); + private static final Logger serverLogger = Logger.getLogger("server"); + + static { + System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); + System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); + System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); + System.setProperty("io.netty.leakDetection.level", "paranoid"); + + // expand Java logging to full level + System.setProperty("java.util.logging.SimpleFormatter.format", + "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %2$s %5$s %6$s%n"); + LogManager.getLogManager().reset(); + Logger rootLogger = LogManager.getLogManager().getLogger(""); + Handler handler = new ConsoleHandler(); + handler.setFormatter(new SimpleFormatter()); + rootLogger.addHandler(handler); + rootLogger.setLevel(Level.ALL); + for (Handler h : rootLogger.getHandlers()) { + handler.setFormatter(new SimpleFormatter()); + h.setLevel(Level.ALL); + } + } + + private final InetSocketAddress inetSocketAddress = new InetSocketAddress("localhost", 8443); + + private final CompletableFuture settingsPrefaceFuture = new CompletableFuture<>(); + + private final CompletableFuture responseFuture = new CompletableFuture<>(); + + @Test + public void testMultiplexHttp2() throws Exception { + + Http2FrameLogger serverFrameLogger = new Http2FrameLogger(LogLevel.INFO, "server"); + Http2FrameLogger clientFrameLogger = new Http2FrameLogger(LogLevel.INFO, "client"); + + EventLoopGroup serverEventLoopGroup = new NioEventLoopGroup(); + EventLoopGroup clientEventLoopGroup = new NioEventLoopGroup(); + + try { + ServerBootstrap serverBootstrap = new ServerBootstrap() + .group(serverEventLoopGroup) + .channel(NioServerSocketChannel.class) + .handler(serverFrameLogger) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ChannelPipeline p = ch.pipeline(); + Http2MultiplexCodec serverMultiplexCodec = Http2MultiplexCodecBuilder.forServer(new ChannelInitializer() { + @Override + protected void initChannel(Channel channel) { + ChannelPipeline p = channel.pipeline(); + p.addLast("multiplex-server-traffic", new TrafficLoggingHandler("multiplex-server-traffic", LogLevel.INFO)); + p.addLast("multiplex-server-frame-converter", new Http2StreamFrameToHttpObjectCodec(true)); + p.addLast("multiplex-server-chunk-aggregator", new HttpObjectAggregator(1048576)); + p.addLast("multiplex-server-request-handler", new ServerRequestHandler()); + } + }) + .initialSettings(Http2Settings.defaultSettings()) + .frameLogger(serverFrameLogger) + .build(); + HttpServerUpgradeHandler.UpgradeCodecFactory upgradeCodecFactory = protocol -> { + if (AsciiString.contentEquals(Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME, protocol)) { + return new Http2ServerUpgradeCodec("server-codec", serverMultiplexCodec); + } else { + return null; + } + }; + HttpServerCodec sourceCodec = new HttpServerCodec(); + HttpServerUpgradeHandler upgradeHandler = new HttpServerUpgradeHandler(sourceCodec, upgradeCodecFactory); + CleartextHttp2ServerUpgradeHandler cleartextHttp2ServerUpgradeHandler = + new CleartextHttp2ServerUpgradeHandler(sourceCodec, upgradeHandler, serverMultiplexCodec); + p.addLast("server-traffic", new TrafficLoggingHandler("server-traffic", LogLevel.INFO)); + p.addLast("server-upgrade", cleartextHttp2ServerUpgradeHandler); + p.addLast("server-messages", new ServerMessages()); + } + }); + Channel serverChannel = serverBootstrap.bind(inetSocketAddress).sync().channel(); + serverLogger.log(Level.INFO, "server up, channel = " + serverChannel); + + Http2MultiplexCodec clientMultiplexCodec = Http2MultiplexCodecBuilder.forClient(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + // unused + throw new IllegalStateException(); + } + }) + .initialSettings(Http2Settings.defaultSettings()) + .frameLogger(clientFrameLogger) + .build(); + Bootstrap clientBootstrap = new Bootstrap() + .group(clientEventLoopGroup) + .channel(NioSocketChannel.class) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast("client-traffic", new TrafficLoggingHandler("client-traffic", LogLevel.INFO)); + p.addLast("client-codec", clientMultiplexCodec); + p.addLast("client-messages", new ClientMessages()); + } + }); + Channel clientChannel = clientBootstrap.connect(inetSocketAddress).sync().channel(); + clientLogger.log(Level.INFO, "client connected, channel = " + clientChannel); + + settingsPrefaceFuture.get(5L, TimeUnit.SECONDS); + if (!settingsPrefaceFuture.isDone()) { + throw new RuntimeException("no settings and preface written, unable to continue"); + } else { + clientLogger.log(Level.INFO, "settings and preface written"); + } + // after settings/preface event, start child channel write + Http2StreamChannel childChannel = new Http2StreamChannelBootstrap(clientChannel) + .handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast("child-client-traffic", new TrafficLoggingHandler("child-client-traffic", LogLevel.INFO)); + p.addLast("child-client-frame-converter", new Http2StreamFrameToHttpObjectCodec(false)); + p.addLast("child-client-chunk-aggregator", new HttpObjectAggregator(1048576)); + p.addLast("child-client-response-handler", new ClientResponseHandler()); + } + }).open().syncUninterruptibly().getNow(); + Http2Headers request = new DefaultHttp2Headers().method(HttpMethod.GET.asciiName()) + .path("/foobar/0/0") + .scheme("http") + .authority(inetSocketAddress.getHostName()); + childChannel.writeAndFlush(new DefaultHttp2HeadersFrame(request, true)); + clientLogger.log(Level.INFO, "waiting max. 10 seconds"); + responseFuture.get(10, TimeUnit.SECONDS); + if (responseFuture.isDone()) { + clientLogger.log(Level.INFO, "done!"); + } + } finally { + clientEventLoopGroup.shutdownGracefully(); + serverEventLoopGroup.shutdownGracefully(); + clientEventLoopGroup.awaitTermination(5, TimeUnit.SECONDS); + clientLogger.log(Level.INFO, "client shutdown"); + serverEventLoopGroup.awaitTermination(5, TimeUnit.SECONDS); + serverLogger.log(Level.INFO, "server shutdown"); + } + } + + class ClientResponseHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) { + clientLogger.log(Level.INFO, "response received on client: " + msg); + responseFuture.complete(true); + } + } + + class ClientMessages extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + clientLogger.log(Level.FINE, "got client msg " + msg + " class = " + msg.getClass()); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + clientLogger.log(Level.FINE, "got client user event " + evt); + if (evt instanceof Http2ConnectionPrefaceAndSettingsFrameWrittenEvent) { + settingsPrefaceFuture.complete(true); + } + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + clientLogger.log(Level.WARNING, cause.getMessage(), cause); + } + } + + class ServerRequestHandler extends SimpleChannelInboundHandler { + + @Override + protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) { + serverLogger.log(Level.INFO, "request received on server: " + msg + + " path = " + msg); + DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.OK); + serverLogger.log(Level.INFO, "writing server response: " + response); + ctx.writeAndFlush(response); + } + } + + class ServerMessages extends ChannelInboundHandlerAdapter { + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + serverLogger.log(Level.FINE, "got server msg " + msg + " class = " + msg.getClass()); + } + + @Override + public void userEventTriggered(ChannelHandlerContext ctx, Object evt) { + serverLogger.log(Level.FINE, "got server user event " + evt); + ctx.fireUserEventTriggered(evt); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + serverLogger.log(Level.WARNING, cause.getMessage(), cause); + } + } + + class TrafficLoggingHandler extends LoggingHandler { + + TrafficLoggingHandler(String name, LogLevel level) { + super(name, 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); + } + } + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..586e261 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,3 @@ +include 'netty-http-common' +include 'netty-http-client' +include 'netty-http-server' diff --git a/src/main/java/org/xbib/netty/http/client/handler/package-info.java b/src/main/java/org/xbib/netty/http/client/handler/package-info.java deleted file mode 100644 index 4550e8e..0000000 --- a/src/main/java/org/xbib/netty/http/client/handler/package-info.java +++ /dev/null @@ -1,4 +0,0 @@ -/** - * Handlers for Netty HTTP client. - */ -package org.xbib.netty.http.client.handler;