From 09d82f576eb456553db4cba0a4a249fcecee12c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Mon, 20 May 2019 15:02:02 +0200 Subject: [PATCH] Netty 4.1.36, introducing endpoints --- build.gradle | 32 +- gradle.properties | 9 +- .../org/xbib/netty/http/client/Client.java | 259 +++++++- .../xbib/netty/http/client/ClientBuilder.java | 233 ------- .../xbib/netty/http/client/ClientConfig.java | 59 +- .../org/xbib/netty/http/client/Request.java | 37 +- .../netty/http/client/RequestBuilder.java | 50 +- .../org/xbib/netty/http/client/UserAgent.java | 2 +- .../handler/http/HttpChannelInitializer.java | 1 + .../http/client/listener/StatusListener.java | 9 + .../http/client/transport/Http2Transport.java | 16 +- .../http/client/transport/HttpTransport.java | 14 +- .../http/client/test/ElasticsearchTest.java | 140 ----- .../netty/http/client/test/Http2Test.java | 116 ---- .../http/client/test/RequestBuilderTest.java | 75 ++- .../xbib/netty/http/client/test/URITest.java | 35 -- .../http/client/test/akamai/AkamaiTest.java | 52 ++ .../client/test/htt2push/Http2PushTest.java | 39 ++ .../http/client/test/webtide/WebtideTest.java | 57 ++ .../xbib/netty/http/common/HttpAddress.java | 5 + .../netty/http/common/HttpParameters.java | 319 ++++++++++ .../xbib/netty/http/common/HttpRequest.java | 28 + .../xbib/netty/http/common/HttpResponse.java | 13 + .../xbib/netty/http/common/SecurityUtil.java | 52 ++ .../common/util/LimitedSortedStringSet.java | 29 + .../http/common/util/LimitedStringMap.java | 22 + .../org/xbib/netty/http/server/Server.java | 308 ++++++--- .../xbib/netty/http/server/ServerBuilder.java | 244 -------- .../xbib/netty/http/server/ServerConfig.java | 143 +---- .../xbib/netty/http/server/ServerRequest.java | 35 ++ .../{transport => }/ServerResponse.java | 4 +- .../netty/http/server/endpoint/Context.java | 10 +- .../netty/http/server/endpoint/Endpoint.java | 227 ++++++- .../server/endpoint/EndpointDispatcher.java | 12 + .../server/endpoint/EndpointResolver.java | 194 ++++++ .../netty/http/server/endpoint/Handler.java | 24 - .../http/server/endpoint/NamedEndpoint.java | 21 - .../http/server/endpoint/NamedServer.java | 380 +++++++----- .../ClasspathService.java} | 20 +- .../server/endpoint/service/EmptyService.java | 14 + .../MethodService.java} | 22 +- .../NioService.java} | 14 +- .../PathReaderService.java} | 24 +- .../http/server/endpoint/service/Service.java | 22 + .../server/transport/BaseServerTransport.java | 52 +- .../server/transport/Http2ServerResponse.java | 56 +- .../transport/Http2ServerTransport.java | 18 +- .../server/transport/HttpServerRequest.java | 124 ++++ .../server/transport/HttpServerResponse.java | 141 ++--- .../server/transport/HttpServerTransport.java | 16 +- .../http/server/transport/ServerRequest.java | 71 --- .../netty/http/server/util/NetworkUtils.java | 586 ------------------ .../http/server/test/CleartextHttp1Test.java | 56 +- .../http/server/test/CleartextHttp2Test.java | 64 +- .../netty/http/server/test/EndpointTest.java | 149 +++++ .../http/server/test/SecureHttp1Test.java | 35 +- .../http/server/test/SecureHttp2Test.java | 33 +- .../test/SecureStaticFileServerTest.java | 105 ++++ .../netty/http/server/test/ServerTest.java | 8 +- .../server/test/StaticFileServerTest.java | 52 +- .../http/server/test/ThreadLeakTest.java | 9 +- 61 files changed, 2686 insertions(+), 2310 deletions(-) delete mode 100644 netty-http-client/src/main/java/org/xbib/netty/http/client/ClientBuilder.java create mode 100644 netty-http-client/src/main/java/org/xbib/netty/http/client/listener/StatusListener.java delete mode 100644 netty-http-client/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java delete mode 100644 netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http2Test.java delete mode 100644 netty-http-client/src/test/java/org/xbib/netty/http/client/test/URITest.java create mode 100644 netty-http-client/src/test/java/org/xbib/netty/http/client/test/akamai/AkamaiTest.java create mode 100644 netty-http-client/src/test/java/org/xbib/netty/http/client/test/htt2push/Http2PushTest.java create mode 100644 netty-http-client/src/test/java/org/xbib/netty/http/client/test/webtide/WebtideTest.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/HttpParameters.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/HttpRequest.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/HttpResponse.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/SecurityUtil.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedSortedStringSet.java create mode 100644 netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedStringMap.java delete 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/ServerRequest.java rename netty-http-server/src/main/java/org/xbib/netty/http/server/{transport => }/ServerResponse.java (90%) create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDispatcher.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointResolver.java delete mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Handler.java delete mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NamedEndpoint.java rename netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/{ClasspathHandler.java => service/ClasspathService.java} (73%) create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/EmptyService.java rename netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/{MethodHandler.java => service/MethodService.java} (60%) rename netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/{NioHandler.java => service/NioService.java} (76%) rename netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/{DirectoryHandler.java => service/PathReaderService.java} (58%) create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Service.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerRequest.java delete mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerRequest.java delete mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkUtils.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/EndpointTest.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureStaticFileServerTest.java diff --git a/build.gradle b/build.gradle index 84f740d..3005b88 100644 --- a/build.gradle +++ b/build.gradle @@ -2,7 +2,7 @@ import java.time.ZonedDateTime import java.time.format.DateTimeFormatter plugins { - id "com.github.spotbugs" version "1.7.1" + id "com.github.spotbugs" version "2.0.0" id "org.sonarqube" version "2.6.1" id "io.codearte.nexus-staging" version "0.11.0" id "org.xbib.gradle.plugin.asciidoctor" version "1.5.6.0.1" @@ -29,11 +29,10 @@ subprojects { apply plugin: 'maven' apply plugin: 'signing' apply plugin: "com.github.spotbugs" - + configurations { alpnagent asciidoclet - wagon } dependencies { @@ -42,7 +41,6 @@ subprojects { testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${project.property('junit.version')}" alpnagent "org.mortbay.jetty.alpn:jetty-alpn-agent:${project.property('alpnagent.version')}" asciidoclet "org.asciidoctor:asciidoclet:${project.property('asciidoclet.version')}" - wagon "org.apache.maven.wagon:wagon-ssh:${project.property('wagon.version')}" } compileJava { @@ -147,23 +145,7 @@ subprojects { 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 @@ -225,14 +207,6 @@ subprojects { // includeFilter = file("config/findbugs/findbugs-include.xml") // excludeFilter = file("config/findbugs/findbugs-excludes.xml") } - spotbugsMain.reports { - xml.enabled = false - html.enabled = true - } - spotbugsTest.reports { - xml.enabled = false - html.enabled = true - } } sonarqube { diff --git a/gradle.properties b/gradle.properties index 69a3ec0..8cec5da 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,14 +1,14 @@ group = org.xbib name = netty-http -version = 4.1.35.2 +version = 4.1.36.2 # main packages -netty.version = 4.1.35.Final -tcnative.version = 2.0.22.Final +netty.version = 4.1.36.Final +tcnative.version = 2.0.25.Final alpnagent.version = 2.0.9 # common -xbib-net-url.version = 1.2.2 +xbib-net-url.version = 1.3.1 # server bouncycastle.version = 1.61 @@ -21,7 +21,6 @@ xbib-guice.version = 4.0.4 junit.version = 5.4.2 conscrypt.version = 2.0.0 jackson.version = 2.8.11.1 -wagon.version = 3.0.0 asciidoclet.version = 1.5.4 org.gradle.warning.mode = all diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java index aa6a386..58ed160 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/Client.java @@ -6,6 +6,7 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; +import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollSocketChannel; import io.netty.channel.nio.NioEventLoopGroup; @@ -14,13 +15,17 @@ import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.codec.http2.Http2Settings; +import io.netty.handler.proxy.HttpProxyHandler; import io.netty.handler.ssl.ApplicationProtocolConfig; import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.CipherSuiteFilter; import io.netty.handler.ssl.OpenSsl; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslHandler; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; import org.xbib.netty.http.client.handler.http.HttpChannelInitializer; import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer; import org.xbib.netty.http.client.pool.BoundedChannelPool; @@ -29,6 +34,7 @@ import org.xbib.netty.http.client.transport.HttpTransport; import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.common.NetworkUtils; +import org.xbib.netty.http.common.SecurityUtil; import javax.net.ssl.SNIHostName; import javax.net.ssl.SNIServerName; @@ -37,14 +43,17 @@ import javax.net.ssl.SSLException; import javax.net.ssl.SSLParameters; import javax.net.ssl.TrustManagerFactory; import java.io.IOException; +import java.io.InputStream; import java.net.InetSocketAddress; import java.security.KeyStoreException; +import java.security.Provider; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Queue; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Semaphore; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; @@ -81,7 +90,7 @@ public final class Client { private final Bootstrap bootstrap; - private final List transports; + private final Queue transports; private BoundedChannelPool pool; @@ -116,7 +125,7 @@ public final class Client { .option(ChannelOption.SO_RCVBUF, clientConfig.getTcpReceiveBufferSize()) .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, clientConfig.getConnectTimeoutMillis()) .option(ChannelOption.WRITE_BUFFER_WATER_MARK, clientConfig.getWriteBufferWaterMark()); - this.transports = new CopyOnWriteArrayList<>(); + this.transports = new ConcurrentLinkedQueue<>(); if (!clientConfig.getPoolNodes().isEmpty()) { List nodes = clientConfig.getPoolNodes(); Integer limit = clientConfig.getPoolNodeConnectionLimit(); @@ -161,12 +170,15 @@ public final class Client { } public void logDiagnostics(Level level) { - logger.log(level, () -> "OpenSSL available: " + OpenSsl.isAvailable() + - " OpenSSL ALPN support: " + OpenSsl.isAlpnSupported() + - " Local host name: " + NetworkUtils.getLocalHostName("localhost") + - " event loop group: " + eventLoopGroup + - " socket: " + socketChannelClass.getName() + - " allocator: " + byteBufAllocator.getClass().getName()); + logger.log(level, () -> "JDK ciphers: " + SecurityUtil.Defaults.JDK_CIPHERS); + logger.log(level, () -> "OpenSSL ciphers: " + SecurityUtil.Defaults.OPENSSL_CIPHERS); + logger.log(level, () -> "OpenSSL available: " + OpenSsl.isAvailable()); + logger.log(level, () -> "OpenSSL ALPN support: " + OpenSsl.isAlpnSupported()); + logger.log(level, () -> "Candidate ciphers on client: " + clientConfig.getCiphers()); + logger.log(level, () -> "Local host name: " + NetworkUtils.getLocalHostName("localhost")); + logger.log(level, () -> "Event loop group: " + eventLoopGroup + " threads=" + clientConfig.getThreadCount()); + logger.log(level, () -> "Socket: " + socketChannelClass.getName()); + logger.log(level, () -> "Allocator: " + byteBufAllocator.getClass().getName()); logger.log(level, NetworkUtils::displayNetworkInterfaces); } @@ -243,9 +255,8 @@ public final class Client { } public Transport execute(Request request) throws IOException { - Transport transport = newTransport(HttpAddress.of(request.url(), request.httpVersion())); - transport.execute(request); - return transport; + return newTransport(HttpAddress.of(request.url(), request.httpVersion())) + .execute(request); } public CompletableFuture execute(Request request, @@ -328,6 +339,7 @@ public final class Client { private static SslHandler newSslHandler(ClientConfig clientConfig, ByteBufAllocator allocator, HttpAddress httpAddress) { try { SslContext sslContext = newSslContext(clientConfig, httpAddress.getVersion()); + logger.log(Level.FINE, () -> "installed ciphers: " + sslContext.cipherSuites()); InetSocketAddress peer = httpAddress.getInetSocketAddress(); SslHandler sslHandler = sslContext.newHandler(allocator, peer.getHostName(), peer.getPort()); SSLEngine engine = sslHandler.engine(); @@ -363,7 +375,7 @@ public final class Client { private static SslContext newSslContext(ClientConfig clientConfig, HttpVersion httpVersion) throws SSLException { SslContextBuilder sslContextBuilder = SslContextBuilder.forClient() .sslProvider(clientConfig.getSslProvider()) - .ciphers(Http2SecurityUtil.CIPHERS, clientConfig.getCipherSuiteFilter()) + .ciphers(clientConfig.getCiphers(), clientConfig.getCipherSuiteFilter()) .applicationProtocolConfig(newApplicationProtocolConfig(httpVersion)); if (clientConfig.getSslContextProvider() != null) { sslContextBuilder.sslContextProvider(clientConfig.getSslContextProvider()); @@ -442,4 +454,223 @@ public final class Client { return newSslHandler(clientConfig, allocator, httpAddress); } } + + public static class ClientBuilder { + + private ByteBufAllocator byteBufAllocator; + + private EventLoopGroup eventLoopGroup; + + private Class socketChannelClass; + + private ClientConfig clientConfig; + + private ClientBuilder() { + this.clientConfig = new ClientConfig(); + } + + public ClientBuilder enableDebug() { + clientConfig.enableDebug(); + return this; + } + + public ClientBuilder disableDebug() { + clientConfig.disableDebug(); + return this; + } + + /** + * Set byte buf allocator for payload in HTTP requests. + * @param byteBufAllocator the byte buf allocator + * @return this builder + */ + public ClientBuilder setByteBufAllocator(ByteBufAllocator byteBufAllocator) { + this.byteBufAllocator = byteBufAllocator; + return this; + } + + public ClientBuilder setEventLoop(EventLoopGroup eventLoopGroup) { + this.eventLoopGroup = eventLoopGroup; + return this; + } + + public ClientBuilder setChannelClass(Class socketChannelClass) { + this.socketChannelClass = socketChannelClass; + return this; + } + + public ClientBuilder setThreadCount(int threadCount) { + clientConfig.setThreadCount(threadCount); + return this; + } + + public ClientBuilder setConnectTimeoutMillis(int connectTimeoutMillis) { + clientConfig.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + public ClientBuilder setTcpSendBufferSize(int tcpSendBufferSize) { + clientConfig.setTcpSendBufferSize(tcpSendBufferSize); + return this; + } + + public ClientBuilder setTcpReceiveBufferSize(int tcpReceiveBufferSize) { + clientConfig.setTcpReceiveBufferSize(tcpReceiveBufferSize); + return this; + } + + public ClientBuilder setTcpNodelay(boolean tcpNodelay) { + clientConfig.setTcpNodelay(tcpNodelay); + return this; + } + + public ClientBuilder setKeepAlive(boolean keepAlive) { + clientConfig.setKeepAlive(keepAlive); + return this; + } + + public ClientBuilder setReuseAddr(boolean reuseAddr) { + clientConfig.setReuseAddr(reuseAddr); + return this; + } + + public ClientBuilder setMaxChunkSize(int maxChunkSize) { + clientConfig.setMaxChunkSize(maxChunkSize); + return this; + } + + public ClientBuilder setMaxInitialLineLength(int maxInitialLineLength) { + clientConfig.setMaxInitialLineLength(maxInitialLineLength); + return this; + } + + public ClientBuilder setMaxHeadersSize(int maxHeadersSize) { + clientConfig.setMaxHeadersSize(maxHeadersSize); + return this; + } + + public ClientBuilder setMaxContentLength(int maxContentLength) { + clientConfig.setMaxContentLength(maxContentLength); + return this; + } + + public ClientBuilder setMaxCompositeBufferComponents(int maxCompositeBufferComponents) { + clientConfig.setMaxCompositeBufferComponents(maxCompositeBufferComponents); + return this; + } + + public ClientBuilder setReadTimeoutMillis(int readTimeoutMillis) { + clientConfig.setReadTimeoutMillis(readTimeoutMillis); + return this; + } + + public ClientBuilder setEnableGzip(boolean enableGzip) { + clientConfig.setEnableGzip(enableGzip); + return this; + } + + public ClientBuilder setSslProvider(SslProvider sslProvider) { + clientConfig.setSslProvider(sslProvider); + return this; + } + + public ClientBuilder setJdkSslProvider() { + clientConfig.setJdkSslProvider(); + clientConfig.setCiphers(SecurityUtil.Defaults.JDK_CIPHERS); + return this; + } + + public ClientBuilder setOpenSSLSslProvider() { + clientConfig.setOpenSSLSslProvider(); + clientConfig.setCiphers(SecurityUtil.Defaults.OPENSSL_CIPHERS); + return this; + } + + public ClientBuilder setSslContextProvider(Provider provider) { + clientConfig.setSslContextProvider(provider); + return this; + } + + public ClientBuilder setCiphers(Iterable ciphers) { + clientConfig.setCiphers(ciphers); + return this; + } + + public ClientBuilder setCipherSuiteFilter(CipherSuiteFilter cipherSuiteFilter) { + clientConfig.setCipherSuiteFilter(cipherSuiteFilter); + return this; + } + + public ClientBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream) { + clientConfig.setKeyCert(keyCertChainInputStream, keyInputStream); + return this; + } + + public ClientBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream, + String keyPassword) { + clientConfig.setKeyCert(keyCertChainInputStream, keyInputStream, keyPassword); + return this; + } + + public ClientBuilder setTrustManagerFactory(TrustManagerFactory trustManagerFactory) { + clientConfig.setTrustManagerFactory(trustManagerFactory); + return this; + } + + public ClientBuilder trustInsecure() { + clientConfig.setTrustManagerFactory(InsecureTrustManagerFactory.INSTANCE); + return this; + } + + public ClientBuilder setClientAuthMode(ClientAuthMode clientAuthMode) { + clientConfig.setClientAuthMode(clientAuthMode); + return this; + } + + public ClientBuilder setHttpProxyHandler(HttpProxyHandler httpProxyHandler) { + clientConfig.setHttpProxyHandler(httpProxyHandler); + return this; + } + + public ClientBuilder addPoolNode(HttpAddress httpAddress) { + clientConfig.addPoolNode(httpAddress); + clientConfig.setPoolVersion(httpAddress.getVersion()); + clientConfig.setPoolSecure(httpAddress.isSecure()); + return this; + } + + public ClientBuilder setPoolNodeConnectionLimit(int nodeConnectionLimit) { + clientConfig.setPoolNodeConnectionLimit(nodeConnectionLimit); + return this; + } + + public ClientBuilder setRetriesPerPoolNode(int retriesPerNode) { + clientConfig.setRetriesPerPoolNode(retriesPerNode); + return this; + } + + public ClientBuilder addServerNameForIdentification(String serverName) { + clientConfig.addServerNameForIdentification(serverName); + return this; + } + + public ClientBuilder setHttp2Settings(Http2Settings http2Settings) { + clientConfig.setHttp2Settings(http2Settings); + return this; + } + + public ClientBuilder setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + clientConfig.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + public ClientBuilder enableNegotiation(boolean enableNegotiation) { + clientConfig.setEnableNegotiation(enableNegotiation); + return this; + } + + public Client build() { + return new Client(clientConfig, byteBufAllocator, eventLoopGroup, socketChannelClass); + } + } } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientBuilder.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientBuilder.java deleted file mode 100644 index 3782cd9..0000000 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientBuilder.java +++ /dev/null @@ -1,233 +0,0 @@ -package org.xbib.netty.http.client; - -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.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; -import java.security.Provider; - -public class ClientBuilder { - - private ByteBufAllocator byteBufAllocator; - - private EventLoopGroup eventLoopGroup; - - private Class socketChannelClass; - - private ClientConfig clientConfig; - - public ClientBuilder() { - this.clientConfig = new ClientConfig(); - } - - public ClientBuilder enableDebug() { - clientConfig.enableDebug(); - return this; - } - - public ClientBuilder disableDebug() { - clientConfig.disableDebug(); - return this; - } - - /** - * Set byte buf allocator for payload in HTTP requests. - * @param byteBufAllocator the byte buf allocator - * @return this builder - */ - public ClientBuilder setByteBufAllocator(ByteBufAllocator byteBufAllocator) { - this.byteBufAllocator = byteBufAllocator; - return this; - } - - public ClientBuilder setEventLoop(EventLoopGroup eventLoopGroup) { - this.eventLoopGroup = eventLoopGroup; - return this; - } - - public ClientBuilder setChannelClass(Class socketChannelClass) { - this.socketChannelClass = socketChannelClass; - return this; - } - - public ClientBuilder setThreadCount(int threadCount) { - clientConfig.setThreadCount(threadCount); - return this; - } - - public ClientBuilder setConnectTimeoutMillis(int connectTimeoutMillis) { - clientConfig.setConnectTimeoutMillis(connectTimeoutMillis); - return this; - } - - public ClientBuilder setTcpSendBufferSize(int tcpSendBufferSize) { - clientConfig.setTcpSendBufferSize(tcpSendBufferSize); - return this; - } - - public ClientBuilder setTcpReceiveBufferSize(int tcpReceiveBufferSize) { - clientConfig.setTcpReceiveBufferSize(tcpReceiveBufferSize); - return this; - } - - public ClientBuilder setTcpNodelay(boolean tcpNodelay) { - clientConfig.setTcpNodelay(tcpNodelay); - return this; - } - - public ClientBuilder setKeepAlive(boolean keepAlive) { - clientConfig.setKeepAlive(keepAlive); - return this; - } - - public ClientBuilder setReuseAddr(boolean reuseAddr) { - clientConfig.setReuseAddr(reuseAddr); - return this; - } - - public ClientBuilder setMaxChunkSize(int maxChunkSize) { - clientConfig.setMaxChunkSize(maxChunkSize); - return this; - } - - public ClientBuilder setMaxInitialLineLength(int maxInitialLineLength) { - clientConfig.setMaxInitialLineLength(maxInitialLineLength); - return this; - } - - public ClientBuilder setMaxHeadersSize(int maxHeadersSize) { - clientConfig.setMaxHeadersSize(maxHeadersSize); - return this; - } - - public ClientBuilder setMaxContentLength(int maxContentLength) { - clientConfig.setMaxContentLength(maxContentLength); - return this; - } - - public ClientBuilder setMaxCompositeBufferComponents(int maxCompositeBufferComponents) { - clientConfig.setMaxCompositeBufferComponents(maxCompositeBufferComponents); - return this; - } - - public ClientBuilder setReadTimeoutMillis(int readTimeoutMillis) { - clientConfig.setReadTimeoutMillis(readTimeoutMillis); - return this; - } - - public ClientBuilder setEnableGzip(boolean enableGzip) { - clientConfig.setEnableGzip(enableGzip); - return this; - } - - public ClientBuilder setSslProvider(SslProvider sslProvider) { - clientConfig.setSslProvider(sslProvider); - return this; - } - - public ClientBuilder setJdkSslProvider() { - clientConfig.setJdkSslProvider(); - return this; - } - - public ClientBuilder setOpenSSLSslProvider() { - clientConfig.setOpenSSLSslProvider(); - return this; - } - - public ClientBuilder setSslContextProvider(Provider provider) { - clientConfig.setSslContextProvider(provider); - return this; - } - - public ClientBuilder setCiphers(Iterable ciphers) { - clientConfig.setCiphers(ciphers); - return this; - } - - public ClientBuilder setCipherSuiteFilter(CipherSuiteFilter cipherSuiteFilter) { - clientConfig.setCipherSuiteFilter(cipherSuiteFilter); - return this; - } - - public ClientBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream) { - clientConfig.setKeyCert(keyCertChainInputStream, keyInputStream); - return this; - } - - public ClientBuilder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream, - String keyPassword) { - clientConfig.setKeyCert(keyCertChainInputStream, keyInputStream, keyPassword); - return this; - } - - public ClientBuilder setTrustManagerFactory(TrustManagerFactory trustManagerFactory) { - clientConfig.setTrustManagerFactory(trustManagerFactory); - return this; - } - - public ClientBuilder trustInsecure() { - clientConfig.setTrustManagerFactory(InsecureTrustManagerFactory.INSTANCE); - return this; - } - - public ClientBuilder setClientAuthMode(ClientAuthMode clientAuthMode) { - clientConfig.setClientAuthMode(clientAuthMode); - return this; - } - - public ClientBuilder setHttpProxyHandler(HttpProxyHandler httpProxyHandler) { - clientConfig.setHttpProxyHandler(httpProxyHandler); - return this; - } - - public ClientBuilder addPoolNode(HttpAddress httpAddress) { - clientConfig.addPoolNode(httpAddress); - clientConfig.setPoolVersion(httpAddress.getVersion()); - clientConfig.setPoolSecure(httpAddress.isSecure()); - return this; - } - - public ClientBuilder setPoolNodeConnectionLimit(int nodeConnectionLimit) { - clientConfig.setPoolNodeConnectionLimit(nodeConnectionLimit); - return this; - } - - public ClientBuilder setRetriesPerPoolNode(int retriesPerNode) { - clientConfig.setRetriesPerPoolNode(retriesPerNode); - return this; - } - - public ClientBuilder addServerNameForIdentification(String serverName) { - clientConfig.addServerNameForIdentification(serverName); - return this; - } - - public ClientBuilder setHttp2Settings(Http2Settings http2Settings) { - clientConfig.setHttp2Settings(http2Settings); - return this; - } - - public ClientBuilder setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { - clientConfig.setWriteBufferWaterMark(writeBufferWaterMark); - return this; - } - - public ClientBuilder enableNegotiation(boolean enableNegotiation) { - clientConfig.setEnableNegotiation(enableNegotiation); - return this; - } - - public Client build() { - return new Client(clientConfig, byteBufAllocator, eventLoopGroup, socketChannelClass); - } -} diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java index 43bfe9b..4be2491 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/ClientConfig.java @@ -3,15 +3,14 @@ package org.xbib.netty.http.client; import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.epoll.Epoll; import io.netty.handler.codec.http.HttpVersion; -import io.netty.handler.codec.http2.Http2SecurityUtil; import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.logging.LogLevel; import io.netty.handler.proxy.HttpProxyHandler; 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 org.xbib.netty.http.common.SecurityUtil; import javax.net.ssl.TrustManagerFactory; import java.io.InputStream; @@ -95,7 +94,7 @@ public class ClientConfig { /** * This is Netty's default. - * See {@link io.netty.handler.codec.MessageAggregator#DEFAULT_MAX_COMPOSITEBUFFER_COMPONENTS}. + * See {@link io.netty.handler.codec.MessageAggregator}. */ int MAX_COMPOSITE_BUFFER_COMPONENTS = 1024; @@ -112,7 +111,7 @@ public class ClientConfig { /** * Default SSL provider. */ - SslProvider SSL_PROVIDER = SslProvider.JDK; + SslProvider SSL_PROVIDER = SecurityUtil.Defaults.DEFAULT_SSL_PROVIDER; /** * Default SSL context provider (for JDK SSL only). @@ -122,12 +121,12 @@ public class ClientConfig { /** * Default ciphers. We care about HTTP/2. */ - Iterable CIPHERS = Http2SecurityUtil.CIPHERS; + Iterable CIPHERS = SecurityUtil.Defaults.DEFAULT_CIPHERS; /** * Default cipher suite filter. */ - CipherSuiteFilter CIPHER_SUITE_FILTER = SupportedCipherSuiteFilter.INSTANCE; + CipherSuiteFilter CIPHER_SUITE_FILTER = SecurityUtil.Defaults.DEFAULT_CIPHER_SUITE_FILTER; /** * Default for SSL client authentication. @@ -167,16 +166,6 @@ public class ClientConfig { Boolean ENABLE_NEGOTIATION = false; } - private static TrustManagerFactory TRUST_MANAGER_FACTORY; - - static { - try { - TRUST_MANAGER_FACTORY = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - } catch (Exception e) { - TRUST_MANAGER_FACTORY = null; - } - } - private boolean debug = Defaults.DEBUG; private LogLevel debugLogLevel = Defaults.DEFAULT_DEBUG_LOG_LEVEL; @@ -219,7 +208,7 @@ public class ClientConfig { private CipherSuiteFilter cipherSuiteFilter = Defaults.CIPHER_SUITE_FILTER; - private TrustManagerFactory trustManagerFactory = TRUST_MANAGER_FACTORY; + private TrustManagerFactory trustManagerFactory = SecurityUtil.Defaults.DEFAULT_TRUST_MANAGER_FACTORY; private KeyStore trustManagerKeyStore = null; @@ -430,6 +419,24 @@ public class ClientConfig { return http2Settings; } + public ClientConfig setTrustManagerFactory(TrustManagerFactory trustManagerFactory) { + this.trustManagerFactory = trustManagerFactory; + return this; + } + + public TrustManagerFactory getTrustManagerFactory() { + return trustManagerFactory; + } + + public ClientConfig setTrustManagerKeyStore(KeyStore trustManagerKeyStore) { + this.trustManagerKeyStore = trustManagerKeyStore; + return this; + } + + public KeyStore getTrustManagerKeyStore() { + return trustManagerKeyStore; + } + public ClientConfig setSslProvider(SslProvider sslProvider) { this.sslProvider = sslProvider; return this; @@ -511,24 +518,6 @@ public class ClientConfig { return clientAuthMode; } - public ClientConfig setTrustManagerFactory(TrustManagerFactory trustManagerFactory) { - this.trustManagerFactory = trustManagerFactory; - return this; - } - - public TrustManagerFactory getTrustManagerFactory() { - return trustManagerFactory; - } - - public ClientConfig setTrustManagerKeyStore(KeyStore trustManagerKeyStore) { - this.trustManagerKeyStore = trustManagerKeyStore; - return this; - } - - public KeyStore getTrustManagerKeyStore() { - return trustManagerKeyStore; - } - public ClientConfig setHttpProxyHandler(HttpProxyHandler httpProxyHandler) { this.httpProxyHandler = httpProxyHandler; return this; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java index 9ea25e5..94b1a6d 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/Request.java @@ -11,6 +11,7 @@ import io.netty.handler.codec.http.cookie.Cookie; import org.xbib.net.URL; import org.xbib.netty.http.client.listener.CookieListener; import org.xbib.netty.http.client.listener.ResponseListener; +import org.xbib.netty.http.client.listener.StatusListener; import org.xbib.netty.http.client.retry.BackOff; import java.nio.charset.StandardCharsets; @@ -24,6 +25,8 @@ public class Request { private final URL url; + private final String uri; + private final HttpVersion httpVersion; private final HttpMethod httpMethod; @@ -32,8 +35,6 @@ public class Request { private final Collection cookies; - private final String uri; - private final ByteBuf content; private final long timeoutInMillis; @@ -54,17 +55,18 @@ public class Request { private CookieListener cookieListener; - Request(URL url, HttpVersion httpVersion, HttpMethod httpMethod, - HttpHeaders headers, Collection cookies, - String uri, ByteBuf content, + private StatusListener statusListener; + + Request(URL url, String uri, HttpVersion httpVersion, HttpMethod httpMethod, + HttpHeaders headers, Collection cookies, ByteBuf content, long timeoutInMillis, boolean followRedirect, int maxRedirect, int redirectCount, boolean isBackOff, BackOff backOff) { this.url = url; + this.uri = uri; this.httpVersion = httpVersion; this.httpMethod = httpMethod; this.headers = headers; this.cookies = cookies; - this.uri = uri; this.content = content; this.timeoutInMillis = timeoutInMillis; this.followRedirect = followRedirect; @@ -78,6 +80,15 @@ public class Request { return url; } + public String absolute() { + return url.toExternalForm(); + } + + public String relative() { + // is already in external form + return uri; + } + public HttpVersion httpVersion() { return httpVersion; } @@ -86,10 +97,6 @@ public class Request { return httpMethod; } - public String relativeUri() { - return uri; - } - public HttpHeaders headers() { return headers; } @@ -145,7 +152,6 @@ public class Request { sb.append("Request[url='").append(url) .append("',version=").append(httpVersion) .append(",method=").append(httpMethod) - .append(",uri=").append(uri) .append(",headers=").append(headers.entries()) .append(",content=").append(content != null && content.readableBytes() >= 16 ? content.copy(0,16).toString(StandardCharsets.UTF_8) + "..." : @@ -173,6 +179,15 @@ public class Request { return cookieListener; } + public Request setStatusListener(StatusListener statusListener) { + this.statusListener = statusListener; + return this; + } + + public StatusListener getStatusListener() { + return statusListener; + } + public Request setResponseListener(ResponseListener responseListener) { this.responseListener = responseListener; return this; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/RequestBuilder.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/RequestBuilder.java index f0e8a9f..f561b9d 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/RequestBuilder.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/RequestBuilder.java @@ -16,13 +16,11 @@ import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.util.AsciiString; import org.xbib.net.PercentEncoder; import org.xbib.net.PercentEncoders; -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 org.xbib.netty.http.common.HttpParameters; -import java.net.URI; import java.nio.charset.MalformedInputException; import java.nio.charset.StandardCharsets; import java.nio.charset.UnmappableCharacterException; @@ -79,9 +77,7 @@ public class RequestBuilder { private URL url; - private String uri; - - private QueryParameters queryParameters; + private HttpParameters queryParameters; private ByteBuf content; @@ -110,7 +106,7 @@ public class RequestBuilder { removeHeaders = new ArrayList<>(); cookies = new HashSet<>(); encoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8); - queryParameters = new QueryParameters(); + queryParameters = new HttpParameters(); } public RequestBuilder setMethod(HttpMethod httpMethod) { @@ -163,7 +159,7 @@ public class RequestBuilder { } public RequestBuilder uri(String uri) { - this.uri = uri; + this.url = url.resolve(uri); return this; } @@ -285,41 +281,31 @@ public class RequestBuilder { if (url.getHost() == null) { throw new IllegalStateException("host in URL not defined: " + url); } - // add path from uri() - if (uri != null) { - try { - url = URL.base(url).resolve(uri); - } catch (URLSyntaxException | MalformedInputException | UnmappableCharacterException e) { - throw new IllegalArgumentException(e); - } - } + // attach user query parameters to URL + URL.Builder builder = url.newBuilder(); + queryParameters.forEach((k, v) -> v.forEach(value -> builder.queryParam(k, value))); + url = builder.build(); // let Netty's query string decoder/encoder work over the URL to add parameters given implicitly in url() - QueryStringDecoder queryStringDecoder = new QueryStringDecoder(URI.create(url.toString()), StandardCharsets.UTF_8); + String path = url.getPath(); + String query = url.getQuery(); + QueryStringDecoder queryStringDecoder = new QueryStringDecoder(query != null ? path + "?" + query : path, StandardCharsets.UTF_8); QueryStringEncoder queryStringEncoder = new QueryStringEncoder(queryStringDecoder.path()); for (Map.Entry> entry : queryStringDecoder.parameters().entrySet()) { for (String value : entry.getValue()) { queryStringEncoder.addParam(entry.getKey(), value); } } - // attach user query parameters - queryParameters.forEach(param -> queryStringEncoder.addParam(param.getFirst(), param.getSecond())); // build uri from QueryStringDecoder String pathAndQuery = queryStringEncoder.toString(); StringBuilder sb = new StringBuilder(); - sb.append(pathAndQuery.isEmpty() ? "/" : pathAndQuery); - String ref = url.getFragment(); - if (ref != null && !ref.isEmpty()) { - sb.append('#').append(ref); + if (!pathAndQuery.isEmpty()) { + sb.append(pathAndQuery); } - String uri = sb.toString(); - // resolve again - if (!uri.equals("/")) { - try { - url = uri.startsWith("/") ? URL.base(url).resolve(uri) : URL.base(url).resolve("/" + uri) ; - } catch (URLSyntaxException | MalformedInputException | UnmappableCharacterException e) { - throw new IllegalArgumentException(e); - } + String fragment = url.getFragment(); + if (fragment != null && !fragment.isEmpty()) { + sb.append('#').append(fragment); } + String uri = sb.toString(); // the encoded form of path/query/fragment DefaultHttpHeaders validatedHeaders = new DefaultHttpHeaders(true); validatedHeaders.set(headers); String scheme = url.getScheme(); @@ -355,7 +341,7 @@ public class RequestBuilder { for (String headerName : removeHeaders) { validatedHeaders.remove(headerName); } - return new Request(url, httpVersion, httpMethod, validatedHeaders, cookies, uri, content, + return new Request(url, uri, httpVersion, httpMethod, validatedHeaders, cookies, content, timeoutInMillis, followRedirect, maxRedirects, 0, enableBackOff, backOff); } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/UserAgent.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/UserAgent.java index f9833de..f76e58a 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/UserAgent.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/UserAgent.java @@ -12,7 +12,7 @@ public final class UserAgent { /** * The default value for {@code User-Agent}. */ - private static final String USER_AGENT = String.format("XbibHttpClient/%s (Java/%s/%s) (Netty/%s)", + private static final String USER_AGENT = String.format("NettyHttpClient/%s (Java/%s/%s) (Netty/%s)", httpClientVersion(), javaVendor(), javaVersion(), nettyVersion()); private UserAgent() { diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpChannelInitializer.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpChannelInitializer.java index 7ef4248..27b0cd6 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpChannelInitializer.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/handler/http/HttpChannelInitializer.java @@ -10,6 +10,7 @@ import io.netty.handler.codec.http.HttpObjectAggregator; 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.Client; import org.xbib.netty.http.client.ClientConfig; import org.xbib.netty.http.client.handler.http2.Http2ChannelInitializer; diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/StatusListener.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/StatusListener.java new file mode 100644 index 0000000..afe5ef6 --- /dev/null +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/listener/StatusListener.java @@ -0,0 +1,9 @@ +package org.xbib.netty.http.client.listener; + +import io.netty.handler.codec.http.HttpResponseStatus; + +@FunctionalInterface +public interface StatusListener { + + void onStatus(HttpResponseStatus httpResponseStatus); +} diff --git a/netty-http-client/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 index 33505b1..eed0ed5 100644 --- a/netty-http-client/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 @@ -17,11 +17,13 @@ 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.HttpConversionUtil; +import io.netty.util.AsciiString; import org.xbib.net.URLSyntaxException; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.handler.http2.Http2ResponseHandler; import org.xbib.netty.http.client.handler.http2.Http2StreamFrameToHttpObjectCodec; import org.xbib.netty.http.client.listener.CookieListener; +import org.xbib.netty.http.client.listener.StatusListener; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.listener.ResponseListener; @@ -73,14 +75,12 @@ public class Http2Transport extends BaseTransport { channelFlowMap.putIfAbsent(channelId, new Flow()); Http2StreamChannel childChannel = new Http2StreamChannelBootstrap(channel) .handler(initializer).open().syncUninterruptibly().getNow(); + AsciiString method = request.httpMethod().asciiName(); + String scheme = request.url().getScheme(); String authority = request.url().getHost() + (request.url().getPort() != null ? ":" + request.url().getPort() : ""); - String path = request.url().getPath() != null && !request.url().getPath().isEmpty() ? - request.url().getPath() : "/"; + String path = request.relative().isEmpty() ? "/" : request.relative(); Http2Headers http2Headers = new DefaultHttp2Headers() - .method(request.httpMethod().asciiName()) - .scheme(request.url().getScheme()) - .authority(authority) - .path(path); + .method(method).scheme(scheme).authority(authority).path(path); final Integer streamId = channelFlowMap.get(channelId).nextStreamId(); if (streamId == null) { throw new IllegalStateException(); @@ -158,6 +158,10 @@ public class Http2Transport extends BaseTransport { if (request == null) { promise.completeExceptionally(new IllegalStateException()); } else { + StatusListener statusListener = request.getStatusListener(); + if (statusListener != null) { + statusListener.onStatus(fullHttpResponse.status()); + } for (String cookieString : fullHttpResponse.headers().getAll(HttpHeaderNames.SET_COOKIE)) { Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); addCookie(cookie); diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java index a1cbb8c..c535a54 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/HttpTransport.java @@ -14,6 +14,7 @@ import io.netty.handler.codec.http2.HttpConversionUtil; import org.xbib.net.URLSyntaxException; import org.xbib.netty.http.client.Client; import org.xbib.netty.http.client.listener.CookieListener; +import org.xbib.netty.http.client.listener.StatusListener; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.listener.ResponseListener; @@ -45,8 +46,7 @@ public class HttpTransport extends BaseTransport { // The "origin form" requires a "Host" header. // Our algorithm is: use always "origin form" for HTTP 1, use absolute form for HTTP 2. // The reason is that Netty derives the HTTP/2 scheme header from the absolute form. - String uri = request.httpVersion().majorVersion() == 1 ? - request.url().relativeReference() : request.url().toString(); + String uri = request.httpVersion().majorVersion() == 1 ? request.relative() : request.absolute(); FullHttpRequest fullHttpRequest = request.content() == null ? new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri) : new DefaultFullHttpRequest(request.httpVersion(), request.httpMethod(), uri, request.content()); @@ -80,9 +80,17 @@ public class HttpTransport extends BaseTransport { logger.log(Level.WARNING, "throwable not null for response " + fullHttpResponse, throwable); return; } + if (requests.isEmpty()) { + logger.log(Level.WARNING, "no request present for responding"); + return; + } // streamID is expected to be null, last request on memory is expected to be current, remove request from memory - Request request = requests.remove(requests.isEmpty() ? null : requests.lastKey()); + Request request = requests.remove(requests.lastKey()); if (request != null) { + StatusListener statusListener = request.getStatusListener(); + if (statusListener != null) { + statusListener.onStatus(fullHttpResponse.status()); + } for (String cookieString : fullHttpResponse.headers().getAll(HttpHeaderNames.SET_COOKIE)) { Cookie cookie = ClientCookieDecoder.STRICT.decode(cookieString); addCookie(cookie); diff --git a/netty-http-client/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 deleted file mode 100644 index c32c04e..0000000 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/ElasticsearchTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package org.xbib.netty.http.client.test; - -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.common.HttpAddress; -import org.xbib.netty.http.client.Request; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -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; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -@Disabled -@ExtendWith(NettyHttpExtension.class) -class ElasticsearchTest { - - private static final Logger logger = Logger.getLogger(ElasticsearchTest.class.getName()); - - @Test - void testElasticsearch() throws IOException { - Client client = Client.builder() - .build(); - try { - Request request = Request.get().url("http://localhost:9200") - .build() - .setResponseListener(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }); - logger.info("request = " + request.toString()); - client.execute(request); - } finally { - client.shutdownGracefully(); - } - } - - @Test - void testElasticsearchCreateDocument() throws IOException { - Client client = Client.builder() - .build(); - try { - Request request = Request.put().url("http://localhost:9200/test/test/1") - .json("{\"text\":\"Hello World\"}") - .build() - .setResponseListener(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }); - logger.info("request = " + request.toString()); - client.execute(request); - } finally { - client.shutdownGracefully(); - } - } - - @Test - void testElasticsearchMatchQuery() throws IOException { - Client client = new Client(); - try { - Request request = Request.post().url("http://localhost:9200/test/_search") - .json("{\"query\":{\"match\":{\"text\":\"Hello World\"}}}") - .build() - .setResponseListener(fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - }); - client.execute(request).get(); - } finally { - client.shutdownGracefully(); - } - } - - /** - * This shows the usage of 4 concurrent pooled connections on 4 threads, querying Elasticsearch. - * @throws IOException if test fails - */ - @Test - void testElasticsearchPooled() throws IOException { - HttpAddress httpAddress = HttpAddress.http1("localhost", 9200); - int limit = 4; - Client client = Client.builder() - .addPoolNode(httpAddress) - .setPoolNodeConnectionLimit(limit) - .build(); - int max = 1000; - int threads = 4; - try { - ExecutorService executorService = Executors.newFixedThreadPool(threads); - for (int n = 0; n < threads; n++) { - executorService.submit(() -> { - List queries = new ArrayList<>(); - for (int i = 0; i < max; i++) { - queries.add(newRequest()); - } - try { - for (int i = 0; i < max; i++) { - client.newTransport().execute(queries.get(i)).get(); - } - } catch (IOException e) { - logger.log(Level.WARNING, e.getMessage(), e); - } - }); - } - executorService.shutdown(); - executorService.awaitTermination(60, TimeUnit.SECONDS); - } catch (InterruptedException e) { - logger.log(Level.WARNING, e.getMessage(), e); - } finally { - client.shutdownGracefully(); - } - logger.log(Level.INFO, "count=" + count); - assertEquals(max * threads, count.get()); - } - - private Request newRequest() { - return Request.post() - .url("http://localhost:9200/test/_search") - .json("{\"query\":{\"match\":{\"text\":\"Hello World\"}}}") - .addHeader("connection", "keep-alive") - .build() - .setResponseListener(fullHttpResponse -> { - count.getAndIncrement(); - if (fullHttpResponse.status().code() != 200) { - logger.log(Level.WARNING,"error: " + fullHttpResponse.toString()); - } - }); - } - - private final AtomicInteger count = new AtomicInteger(); -} diff --git a/netty-http-client/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 deleted file mode 100644 index 7b3e67c..0000000 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/Http2Test.java +++ /dev/null @@ -1,116 +0,0 @@ -package org.xbib.netty.http.client.test; - -import io.netty.handler.codec.http.HttpMethod; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.xbib.netty.http.client.Client; -import org.xbib.netty.http.client.Request; - -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.logging.Level; -import java.util.logging.Logger; - -@ExtendWith(NettyHttpExtension.class) -class Http2Test { - - private static final Logger logger = Logger.getLogger(Http2Test.class.getName()); - - /** - * Problems with akamai: - * - * 2018-03-07 16:02:52.385 FEIN [client] io.netty.handler.codec.http2.Http2FrameLogger logRstStream - * [id: 0x57cc65bb, L:/10.1.1.94:52834 - R:http2.akamai.com/104.94.191.203:443] INBOUND RST_STREAM: streamId=2 errorCode=8 - * 2018-03-07 16:02:52.385 FEIN [client] io.netty.handler.codec.http2.Http2FrameLogger logGoAway - * [id: 0x57cc65bb, L:/10.1.1.94:52834 - R:http2.akamai.com/104.94.191.203:443] OUTBOUND GO_AWAY: lastStreamId=2 errorCode=0 length=0 bytes= - * - * demo/h2_demo_frame.html sends no content, only a push promise, and does not continue - * - * @throws IOException if test fails - */ - @Test - void testAkamai() throws IOException { - Client client = Client.builder() - .addServerNameForIdentification("http2.akamai.com") - .build(); - try { - Request request = Request.get() - .url("https://http2.akamai.com/demo/h2_demo_frame.html") - //.url("https://http2.akamai.com/") - .setVersion("HTTP/2.0") - .build() - .setResponseListener(msg -> { - String response = msg.content().toString(StandardCharsets.UTF_8); - logger.log(Level.INFO, "status = " + msg.status() + - msg.headers().entries() + " " + response); - }); - client.execute(request).get(); - } finally { - client.shutdownGracefully(); - } - } - - @Test - void testWebtide() throws Exception { - Client client = Client.builder() - .build(); - client.logDiagnostics(Level.INFO); - try { - Request request = Request.get().url("https://webtide.com").setVersion("HTTP/2.0").build() - .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + - msg.headers().entries() + - //msg.content().toString(StandardCharsets.UTF_8) + - " status=" + msg.status().code())); - client.execute(request).get(); - } finally { - client.shutdown(); - } - } - - @Test - void testHttp2PushIO() throws IOException { - String url = "https://http2-push.io"; - Client client = Client.builder() - .addServerNameForIdentification("http2-push.io") - .build(); - try { - Request request = Request.builder(HttpMethod.GET) - .url(url).setVersion("HTTP/2.0") - .build() - .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + - msg.headers().entries() + - //msg.content().toString(StandardCharsets.UTF_8) + - " status=" + msg.status().code())); - client.execute(request).get(); - - } finally { - client.shutdownGracefully(); - } - } - - @Test - void testWebtideTwoRequestsOnSameConnection() throws IOException { - Client client = new Client(); - try { - Request request1 = Request.builder(HttpMethod.GET) - .url("https://webtide.com").setVersion("HTTP/2.0") - .build() - .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + - msg.headers().entries() + - //msg.content().toString(StandardCharsets.UTF_8) + - " status=" + msg.status().code())); - - Request request2 = Request.builder(HttpMethod.GET) - .url("https://webtide.com/why-choose-jetty/").setVersion("HTTP/2.0") - .build() - .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + - msg.headers().entries() + - //msg.content().toString(StandardCharsets.UTF_8) + - " status=" + msg.status().code())); - - client.execute(request1).execute(request2); - } finally { - client.shutdownGracefully(); - } - } -} diff --git a/netty-http-client/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 index caf234e..5d65213 100644 --- a/netty-http-client/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 @@ -2,8 +2,11 @@ package org.xbib.netty.http.client.test; import io.netty.handler.codec.http.HttpMethod; import org.junit.jupiter.api.Test; +import org.xbib.net.URL; import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.RequestBuilder; +import java.net.URI; import java.nio.charset.StandardCharsets; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -11,6 +14,38 @@ import static org.junit.jupiter.api.Assertions.assertNull; class RequestBuilderTest { + @Test + void testResolve() { + URI uri = URI.create("http://localhost"); + URI uri2 = uri.resolve("/path"); + assertEquals("http://localhost/path", uri2.toString()); + + uri = URI.create("http://localhost/path1?a=b"); + uri2 = uri.resolve("path2?c=d"); + assertEquals("http://localhost/path2?c=d", uri2.toString()); + + URL url = URL.from("http://localhost"); + URL url2 = url.resolve("/path"); + assertEquals("http://localhost/path", url2.toString()); + + url = URL.from("http://localhost/path1?a=b"); + url2 = url.resolve("path2?c=d"); + assertEquals("http://localhost/path2?c=d", url2.toString()); + } + + @Test + void testRelativeUri() { + RequestBuilder httpRequestBuilder = Request.get(); + httpRequestBuilder.url("https://localhost").uri("/path"); + assertEquals("/path", httpRequestBuilder.build().relative()); + httpRequestBuilder.uri("/foobar"); + assertEquals("/foobar", httpRequestBuilder.build().relative()); + httpRequestBuilder.uri("/path1?a=b"); + assertEquals("/path1?a=b", httpRequestBuilder.build().relative()); + httpRequestBuilder.uri("/path2?c=d"); + assertEquals("/path2?c=d", httpRequestBuilder.build().relative()); + } + @Test void testSimpleRequest() { Request request = Request.builder(HttpMethod.GET) @@ -28,8 +63,8 @@ class RequestBuilderTest { .addParameter("param1", "value1") .addParameter("param2", "value2") .build(); - assertEquals("?param1=value1¶m2=value2", request.relativeUri()); - assertEquals("http://xbib.org/?param1=value1¶m2=value2", request.url().toString()); + assertEquals("?param1=value1¶m2=value2", request.relative()); + assertEquals("http://xbib.org?param1=value1¶m2=value2", request.url().toString()); } @Test @@ -40,8 +75,9 @@ class RequestBuilderTest { .addParameter("param2", "value2") .content("Hello", "text/plain") .build(); - assertEquals("?param1=value1¶m2=value2", request.relativeUri()); - assertEquals("http://xbib.org/?param1=value1¶m2=value2", request.url().toString()); + assertEquals("xbib.org", request.url().getHost()); + assertEquals("?param1=value1¶m2=value2", request.relative()); + assertEquals("http://xbib.org?param1=value1¶m2=value2", request.url().toExternalForm()); assertEquals("Hello", request.content().toString(StandardCharsets.UTF_8)); } @@ -49,9 +85,38 @@ class RequestBuilderTest { void testRequest() { Request request = Request.get() .url("https://google.com") - .setVersion("HTTP/1.1") .build(); assertEquals("google.com", request.url().getHost()); + } + @Test + void testRequestWithSpaceInParameters() { + Request request = Request.get() + .url("https://google.com? a = b") + .build(); + assertEquals("google.com", request.url().getHost()); + assertEquals("https://google.com?%20a%20=%20b", request.absolute()); + assertEquals("?%20a%20=%20b", request.relative()); + assertEquals("https://google.com? a = b", request.url().toString()); + assertEquals("https://google.com?%20a%20=%20b", request.url().toExternalForm()); + + request = Request.get() + .url("https://google.com?%20a%20=%20b") + .build(); + assertEquals("google.com", request.url().getHost()); + assertEquals("https://google.com?%20a%20=%20b", request.absolute()); + assertEquals("?%20a%20=%20b", request.relative()); + assertEquals("https://google.com? a = b", request.url().toString()); + assertEquals("https://google.com?%20a%20=%20b", request.url().toExternalForm()); + } + + @Test + void testMassiveQueryParameters() { + RequestBuilder requestBuilder = Request.builder(HttpMethod.GET); + for (int i = 0; i < 2000; i++) { + requestBuilder.addParameter("param" + i, "value" + i); + } + Request request = requestBuilder.build(); + assertEquals(18276, request.absolute().length()); } } diff --git a/netty-http-client/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 deleted file mode 100644 index d1fc5c2..0000000 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/URITest.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.xbib.netty.http.client.test; - -import org.junit.jupiter.api.Test; -import org.xbib.netty.http.client.Request; -import org.xbib.netty.http.client.RequestBuilder; - -import java.net.URI; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -class URITest { - - @Test - void testURIResolve() { - URI uri = URI.create("http://localhost"); - URI uri2 = uri.resolve("/path"); - assertEquals("http://localhost/path", uri2.toString()); - uri = URI.create("http://localhost/path1?a=b"); - uri2 = uri.resolve("path2?c=d"); - assertEquals("http://localhost/path2?c=d", uri2.toString()); - } - - @Test - void testRelativeUri() { - RequestBuilder httpRequestBuilder = Request.get(); - httpRequestBuilder.url("https://localhost").uri("/path"); - assertEquals("/path", httpRequestBuilder.build().relativeUri()); - httpRequestBuilder.uri("/foobar"); - assertEquals("/foobar", httpRequestBuilder.build().relativeUri()); - httpRequestBuilder.uri("/path1?a=b"); - assertEquals("/path1?a=b", httpRequestBuilder.build().relativeUri()); - httpRequestBuilder.uri("/path2?c=d"); - assertEquals("/path2?c=d", httpRequestBuilder.build().relativeUri()); - } -} diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/akamai/AkamaiTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/akamai/AkamaiTest.java new file mode 100644 index 0000000..c8c0ad1 --- /dev/null +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/akamai/AkamaiTest.java @@ -0,0 +1,52 @@ +package org.xbib.netty.http.client.test.akamai; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.test.NettyHttpExtension; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; + +@ExtendWith(NettyHttpExtension.class) +public class AkamaiTest { + + private static Logger logger = Logger.getLogger(AkamaiTest.class.getName()); + + /** + * Problems with akamai: + * + * 2018-03-07 16:02:52.385 FEIN [client] io.netty.handler.codec.http2.Http2FrameLogger logRstStream + * [id: 0x57cc65bb, L:/10.1.1.94:52834 - R:http2.akamai.com/104.94.191.203:443] INBOUND RST_STREAM: streamId=2 errorCode=8 + * 2018-03-07 16:02:52.385 FEIN [client] io.netty.handler.codec.http2.Http2FrameLogger logGoAway + * [id: 0x57cc65bb, L:/10.1.1.94:52834 - R:http2.akamai.com/104.94.191.203:443] OUTBOUND GO_AWAY: lastStreamId=2 errorCode=0 length=0 bytes= + * + * demo/h2_demo_frame.html sends no content, only a push promise, and does not continue + * + * @throws IOException if test fails + */ + @Test + void testAkamai() throws IOException { + Client client = Client.builder() + .addServerNameForIdentification("http2.akamai.com") + .build(); + try { + Request request = Request.get() + .url("https://http2.akamai.com/demo/h2_demo_frame.html") + //.url("https://http2.akamai.com/") + .setVersion("HTTP/2.0") + .build() + .setResponseListener(msg -> { + String response = msg.content().toString(StandardCharsets.UTF_8); + logger.log(Level.INFO, "status = " + msg.status() + + msg.headers().entries() + " " + response); + }); + client.execute(request).get(); + } finally { + client.shutdownGracefully(); + } + } +} diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/htt2push/Http2PushTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/htt2push/Http2PushTest.java new file mode 100644 index 0000000..0b15a72 --- /dev/null +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/htt2push/Http2PushTest.java @@ -0,0 +1,39 @@ +package org.xbib.netty.http.client.test.htt2push; + +import io.netty.handler.codec.http.HttpMethod; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.test.NettyHttpExtension; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +@ExtendWith(NettyHttpExtension.class) +class Http2PushTest { + + private static final Logger logger = Logger.getLogger(Http2PushTest.class.getName()); + + @Test + void testHttp2PushIO() throws IOException { + String url = "https://http2-push.io"; + Client client = Client.builder() + .addServerNameForIdentification("http2-push.io") + .build(); + try { + Request request = Request.builder(HttpMethod.GET) + .url(url).setVersion("HTTP/2.0") + .build() + .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + + msg.headers().entries() + + //msg.content().toString(StandardCharsets.UTF_8) + + " status=" + msg.status().code())); + client.execute(request).get(); + + } finally { + client.shutdownGracefully(); + } + } +} diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/webtide/WebtideTest.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/webtide/WebtideTest.java new file mode 100644 index 0000000..17a743a --- /dev/null +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/webtide/WebtideTest.java @@ -0,0 +1,57 @@ +package org.xbib.netty.http.client.test.webtide; + +import io.netty.handler.codec.http.HttpMethod; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.xbib.netty.http.client.Client; +import org.xbib.netty.http.client.Request; +import org.xbib.netty.http.client.test.NettyHttpExtension; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +@ExtendWith(NettyHttpExtension.class) +class WebtideTest { + + private static final Logger logger = Logger.getLogger(WebtideTest.class.getName()); + + @Test + void testWebtide() throws Exception { + Client client = Client.builder() + .build(); + try { + Request request = Request.get().url("https://webtide.com").setVersion("HTTP/2.0").build() + .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + msg)); + client.execute(request).get(); + } finally { + client.shutdown(); + } + } + + @Test + void testWebtideTwoRequestsOnSameConnection() throws IOException { + Client client = new Client(); + try { + Request request1 = Request.builder(HttpMethod.GET) + .url("https://webtide.com").setVersion("HTTP/2.0") + .build() + .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + + msg.headers().entries() + + //msg.content().toString(StandardCharsets.UTF_8) + + " status=" + msg.status().code())); + + Request request2 = Request.builder(HttpMethod.GET) + .url("https://webtide.com/why-choose-jetty/").setVersion("HTTP/2.0") + .build() + .setResponseListener(msg -> logger.log(Level.INFO, "got response: " + + msg.headers().entries() + + //msg.content().toString(StandardCharsets.UTF_8) + + " status=" + msg.status().code())); + + client.execute(request1).execute(request2); + } finally { + client.shutdownGracefully(); + } + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java index ca3212f..3e01410 100644 --- a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpAddress.java @@ -22,6 +22,7 @@ public class HttpAddress implements PoolKey { private InetSocketAddress inetSocketAddress; + public static HttpAddress http1(String host) { return new HttpAddress(host, 80, HttpVersion.HTTP_1_1, false); } @@ -62,6 +63,10 @@ public class HttpAddress implements PoolKey { return new HttpAddress(url, HTTP_2_0); } + public static HttpAddress of(URL url) { + return new HttpAddress(url, HttpVersion.HTTP_1_1); + } + public static HttpAddress of(URL url, HttpVersion httpVersion) { return new HttpAddress(url, httpVersion); } diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpParameters.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpParameters.java new file mode 100644 index 0000000..75516de --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpParameters.java @@ -0,0 +1,319 @@ +package org.xbib.netty.http.common; + +import org.xbib.net.PercentDecoder; +import org.xbib.net.PercentEncoder; +import org.xbib.net.PercentEncoders; +import org.xbib.netty.http.common.util.LimitedSortedStringSet; +import org.xbib.netty.http.common.util.LimitedStringMap; + +import java.nio.charset.MalformedInputException; +import java.nio.charset.StandardCharsets; +import java.nio.charset.UnmappableCharacterException; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.SortedSet; + +/** + * A limited multi-map of HTTP request parameters. Each key references a + * limited set of parameters collected from the request during message + * signing. Parameter values are sorted as per + * OAuth specification. + * Every key/value pair will be percent-encoded upon insertion. + * This class has special semantics tailored to + * being useful for message signing; it's not a general purpose collection class + * to handle request parameters. + */ +public class HttpParameters implements Map> { + + private final int maxParam; + + private final int sizeLimit; + + private final int elementSizeLimit; + + private final LimitedStringMap map; + + private final PercentEncoder percentEncoder; + + private final PercentDecoder percentDecoder; + + public HttpParameters() { + this(1024, 1024, 65536); + } + + public HttpParameters(int maxParam, int sizeLimit, int elementSizeLimit) { + this.maxParam = maxParam; + this.sizeLimit = sizeLimit; + this.elementSizeLimit = elementSizeLimit; + this.map = new LimitedStringMap(maxParam); + this.percentEncoder = PercentEncoders.getQueryEncoder(StandardCharsets.UTF_8); + this.percentDecoder = new PercentDecoder(); + } + + @Override + public SortedSet put(String key, SortedSet value) { + return map.put(key, value); + } + + @Override + public SortedSet get(Object key) { + return map.get(key); + } + + @Override + public void putAll(Map> m) { + map.putAll(m); + } + + @Override + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + if (value instanceof String) { + for (Set values : map.values()) { + if (values.contains(value)) { + return true; + } + } + } + return false; + } + + @Override + public int size() { + int count = 0; + for (String key : map.keySet()) { + count += map.get(key).size(); + } + return count; + } + + @Override + public boolean isEmpty() { + return map.isEmpty(); + } + + @Override + public void clear() { + map.clear(); + } + + @Override + public SortedSet remove(Object key) { + return map.remove(key); + } + + @Override + public Set keySet() { + return map.keySet(); + } + + @Override + public Collection> values() { + return map.values(); + } + + @Override + public Set>> entrySet() { + return map.entrySet(); + } + + public SortedSet put(String key, SortedSet values, boolean percentEncode) + throws MalformedInputException, UnmappableCharacterException { + if (percentEncode) { + remove(key); + for (String v : values) { + add(key, v, true); + } + return get(key); + } else { + return map.put(key, values); + } + } + + /** + * Convenience method to add a single value for the parameter specified by 'key'. + * + * @param key the parameter name + * @param value the parameter value + * @return the value + * @throws MalformedInputException if input is malformed + * @throws UnmappableCharacterException if characters are unmappable + */ + public String add(String key, String value) + throws MalformedInputException, UnmappableCharacterException { + return add(key, value, false); + } + + /** + * Convenience method to add a single value for the parameter specified by + * 'key'. + * + * @param key the parameter name + * @param value the parameter value + * @param percentEncode whether key and value should be percent encoded before being + * inserted into the map + * @return the value + * @throws MalformedInputException if input is malformed + * @throws UnmappableCharacterException if characters are unmappable + */ + public String add(String key, String value, boolean percentEncode) + throws MalformedInputException, UnmappableCharacterException { + String k = percentEncode ? percentEncoder.encode(key) : key; + SortedSet values = map.get(k); + if (values == null) { + values = new LimitedSortedStringSet(sizeLimit, elementSizeLimit); + map.put(k, values); + } + String v = null; + if (value != null) { + v = percentEncode ? percentEncoder.encode(value) : value; + values.add(v); + } + return v; + } + + /** + * Convenience method to allow for storing null values. {@link #put} doesn't + * allow null values, because that would be ambiguous. + * + * @param key the parameter name + * @param nullString can be anything, but probably... null? + * @return null + * @throws MalformedInputException if input is malformed + * @throws UnmappableCharacterException if characters are unmappable + */ + public String addNull(String key, String nullString) + throws MalformedInputException, UnmappableCharacterException { + return add(key, nullString); + } + + public void addAll(Map> m, boolean percentEncode) + throws MalformedInputException, UnmappableCharacterException { + if (percentEncode) { + for (String key : m.keySet()) { + put(key, m.get(key), true); + } + } else { + map.putAll(m); + } + } + + public void addAll(String[] keyValuePairs, boolean percentEncode) + throws MalformedInputException, UnmappableCharacterException { + for (int i = 0; i < keyValuePairs.length - 1; i += 2) { + add(keyValuePairs[i], keyValuePairs[i + 1], percentEncode); + } + } + + /** + * Convenience method to merge a {@code Map>}. + * + * @param m the map + */ + public void addMap(Map> m) { + for (String key : m.keySet()) { + SortedSet vals = get(key); + if (vals == null) { + vals = new LimitedSortedStringSet(sizeLimit, elementSizeLimit); + put(key, vals); + } + vals.addAll(m.get(key)); + } + } + + public String getFirst(String key) { + SortedSet values = map.get(key); + if (values == null || values.isEmpty()) { + return null; + } + return values.first(); + } + + /** + * Returns the first value from the set of all values for the given + * parameter name. If the key passed to this method contains special + * characters, you must first percent encode it, otherwise the lookup will fail + * (that's because upon storing values in this map, keys get + * percent-encoded). + * + * @param key the parameter name (must be percent encoded if it contains unsafe + * characters!) + * @return the first value found for this parameter + * @throws MalformedInputException if input is malformed + * @throws UnmappableCharacterException if characters are unmappable + */ + public String getFirstDecoded(String key) + throws MalformedInputException, UnmappableCharacterException { + SortedSet values = map.get(key); + if (values == null || values.isEmpty()) { + return null; + } + String value = values.first(); + return percentDecoder.decode(value); + } + + /** + * Concatenates all values for the given key to a list of key/value pairs + * suitable for use in a URL query string. + * + * @param key the parameter name + * @return the query string + * @throws MalformedInputException if input is malformed + * @throws UnmappableCharacterException if characters are unmappable + */ + public String getAsQueryString(String key) + throws MalformedInputException, UnmappableCharacterException { + return getAsQueryString(key, true); + } + + /** + * Concatenates all values for the given key to a list of key/value pairs + * suitable for use in a URL query string. + * + * @param key the parameter name + * @param percentEncode whether key should be percent encoded before being + * used with the map + * @return the query string + * @throws MalformedInputException if input is malformed + * @throws UnmappableCharacterException if characters are unmappable + */ + public String getAsQueryString(String key, boolean percentEncode) + throws MalformedInputException, UnmappableCharacterException { + String k = percentEncode ? percentEncoder.encode(key) : key; + SortedSet values = map.get(k); + if (values == null) { + return k + "="; + } + Iterator it = values.iterator(); + StringBuilder sb = new StringBuilder(); + while (it.hasNext()) { + sb.append(k).append("=").append(it.next()); + if (it.hasNext()) { + sb.append("&"); + } + } + return sb.toString(); + } + + public String getAsHeaderElement(String key) { + String value = getFirst(key); + if (value == null) { + return null; + } + return key + "=\"" + value + "\""; + } + + public HttpParameters getOAuthParameters() { + HttpParameters oauthParams = new HttpParameters(maxParam, sizeLimit, elementSizeLimit); + entrySet().stream().filter(entry -> entry.getKey().startsWith("oauth_") || entry.getKey().startsWith("x_oauth_")) + .forEach(entry -> oauthParams.put(entry.getKey(), entry.getValue())); + return oauthParams; + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpRequest.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpRequest.java new file mode 100644 index 0000000..2180663 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpRequest.java @@ -0,0 +1,28 @@ +package org.xbib.netty.http.common; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; + +/** + * A representation of an HTTP request. Contains methods to access all those parts of an HTTP request. + */ +public interface HttpRequest { + + String getMethod(); + + String getRequestUrl(); + + void setRequestUrl(String url); + + void setHeader(String name, String value); + + String getHeader(String name); + + Map getHeaders(); + + InputStream getContent() throws IOException; + + String getContentType(); + +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpResponse.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpResponse.java new file mode 100644 index 0000000..01778c7 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/HttpResponse.java @@ -0,0 +1,13 @@ +package org.xbib.netty.http.common; + +import java.io.IOException; +import java.io.InputStream; + +public interface HttpResponse { + + int getStatusCode() throws IOException; + + String getReason() throws Exception; + + InputStream getContent() throws IOException; +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/SecurityUtil.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/SecurityUtil.java new file mode 100644 index 0000000..a90edb2 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/SecurityUtil.java @@ -0,0 +1,52 @@ +package org.xbib.netty.http.common; + +import io.netty.handler.codec.http2.Http2SecurityUtil; +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.OpenSsl; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.SupportedCipherSuiteFilter; + +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManagerFactory; +import java.util.Arrays; +import java.util.List; + +public class SecurityUtil { + + private static TrustManagerFactory TRUST_MANAGER_FACTORY; + + static { + try { + TRUST_MANAGER_FACTORY = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + } catch (Exception e) { + TRUST_MANAGER_FACTORY = null; + } + } + + public interface Defaults { + + List OPENSSL_CIPHERS = Http2SecurityUtil.CIPHERS; + + List JDK_CIPHERS = + Arrays.asList(((SSLSocketFactory) SSLSocketFactory.getDefault()).getDefaultCipherSuites()); + + + TrustManagerFactory DEFAULT_TRUST_MANAGER_FACTORY = TRUST_MANAGER_FACTORY; + /** + * Default SSL provider. + */ + SslProvider DEFAULT_SSL_PROVIDER = OpenSsl.isAvailable() ? SslProvider.OPENSSL : SslProvider.JDK; + + /** + * Default ciphers. + */ + Iterable DEFAULT_CIPHERS = OpenSsl.isAvailable() ? OPENSSL_CIPHERS : JDK_CIPHERS; + + /** + * Default cipher suite filter. + */ + CipherSuiteFilter DEFAULT_CIPHER_SUITE_FILTER = SupportedCipherSuiteFilter.INSTANCE; + + } + +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedSortedStringSet.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedSortedStringSet.java new file mode 100644 index 0000000..e06d98a --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedSortedStringSet.java @@ -0,0 +1,29 @@ +package org.xbib.netty.http.common.util; + +import java.util.SortedSet; +import java.util.TreeSet; + +@SuppressWarnings("serial") +public class LimitedSortedStringSet extends TreeSet implements SortedSet { + + private final int sizeLimit; + + private final int elementSizeLimit; + + public LimitedSortedStringSet() { + this(1024, 65536); + } + + public LimitedSortedStringSet(int sizeLimit, int elementSizeLimit) { + this.sizeLimit = sizeLimit; + this.elementSizeLimit = elementSizeLimit; + } + + @Override + public boolean add(String string) { + if (size() < sizeLimit && string.length() <= elementSizeLimit ) { + return super.add(string); + } + return false; + } +} diff --git a/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedStringMap.java b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedStringMap.java new file mode 100644 index 0000000..5b0d471 --- /dev/null +++ b/netty-http-common/src/main/java/org/xbib/netty/http/common/util/LimitedStringMap.java @@ -0,0 +1,22 @@ +package org.xbib.netty.http.common.util; + +import java.util.SortedSet; +import java.util.TreeMap; + +@SuppressWarnings("serial") +public class LimitedStringMap extends TreeMap> { + + private final int limit; + + public LimitedStringMap(int limit) { + this.limit = limit; + } + + @Override + public SortedSet put(String key, SortedSet value) { + if (size() < limit) { + return super.put(key, value); + } + return null; + } +} 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 index cf4c101..4cb561f 100644 --- 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 @@ -5,6 +5,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelOption; import io.netty.channel.EventLoopGroup; +import io.netty.channel.WriteBufferWaterMark; import io.netty.channel.epoll.Epoll; import io.netty.channel.epoll.EpollEventLoopGroup; import io.netty.channel.epoll.EpollServerSocketChannel; @@ -13,28 +14,24 @@ 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.common.NetworkUtils; import org.xbib.netty.http.server.endpoint.NamedServer; import org.xbib.netty.http.server.handler.http.HttpChannelInitializer; import org.xbib.netty.http.server.handler.http2.Http2ChannelInitializer; +import org.xbib.netty.http.common.SecurityUtil; +import org.xbib.netty.http.server.transport.Http2ServerResponse; +import org.xbib.netty.http.server.transport.HttpServerRequest; +import org.xbib.netty.http.server.transport.HttpServerResponse; import org.xbib.netty.http.server.transport.HttpServerTransport; 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; @@ -73,27 +70,23 @@ public final class Server { private final ServerBootstrap bootstrap; - private final Map virtualServerMap; - private ChannelFuture channelFuture; /** - * Create a new HTTP server. Use {@link #builder()} to build HTTP client instance. + * Create a new HTTP server. Use {@link #builder(HttpAddress)} to build HTTP client instance. * @param serverConfig server configuration * @param byteBufAllocator byte buf allocator * @param parentEventLoopGroup parent event loop group * @param childEventLoopGroup child event loop group * @param socketChannelClass socket channel class - * @throws SSLException if SSL can not be configured */ - public Server(ServerConfig serverConfig, + private Server(ServerConfig serverConfig, ByteBufAllocator byteBufAllocator, EventLoopGroup parentEventLoopGroup, EventLoopGroup childEventLoopGroup, - Class socketChannelClass) throws SSLException { + Class socketChannelClass) { Objects.requireNonNull(serverConfig); this.serverConfig = serverConfig; - initializeTrustManagerFactory(serverConfig); this.byteBufAllocator = byteBufAllocator != null ? byteBufAllocator : ByteBufAllocator.DEFAULT; this.parentEventLoopGroup = createParentEventLoopGroup(serverConfig, parentEventLoopGroup); @@ -117,45 +110,28 @@ public final class Server { if (serverConfig.isDebug()) { bootstrap.handler(new LoggingHandler("bootstrap-server", serverConfig.getDebugLogLevel())); } - this.virtualServerMap = new HashMap<>(); - for (NamedServer namedServer : serverConfig.getNamedServers()) { - String name = namedServer.getName(); - virtualServerMap.put(name, namedServer); - for (String alias : namedServer.getAliases()) { - virtualServerMap.put(alias, namedServer); - } - } - 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 (NamedServer namedServer : serverConfig.getNamedServers()) { - String name = namedServer.getName(); - mappingBuilder.add(name == null ? "*" : name, sslContext); - } - domainNameMapping = mappingBuilder.build(); - } - HttpAddress httpAddress = serverConfig.getAddress(); - if (httpAddress.getVersion().majorVersion() == 1) { + DomainNameMapping domainNameMapping = createDomainNameMapping(); + if (serverConfig.getAddress().getVersion().majorVersion() == 1) { HttpChannelInitializer httpChannelInitializer = new HttpChannelInitializer(this, - httpAddress, domainNameMapping); + serverConfig.getAddress(), domainNameMapping); bootstrap.childHandler(httpChannelInitializer); } else { - Http2ChannelInitializer initializer = new Http2ChannelInitializer(this, - httpAddress, domainNameMapping); - bootstrap.childHandler(initializer); + Http2ChannelInitializer http2ChannelInitializer = new Http2ChannelInitializer(this, + serverConfig.getAddress(), domainNameMapping); + bootstrap.childHandler(http2ChannelInitializer); } } - public static ServerBuilder builder() { - return new ServerBuilder(); + public static Builder builder() { + return new Builder(HttpAddress.http1("localhost", 8008)); + } + + public static Builder builder(HttpAddress httpAddress) { + return new Builder(httpAddress); + } + + public static Builder builder(NamedServer namedServer) { + return new Builder(namedServer.getHttpAddress(), namedServer); } public ServerConfig getServerConfig() { @@ -163,18 +139,18 @@ public final class Server { } /** - * Returns the virtual host with the given name. + * Returns the named server 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 NamedServer getVirtualServer(String name) { - return virtualServerMap.get(name); + public NamedServer getNamedServer(String name) { + return serverConfig.getNamedServers().get(name); } - public NamedServer getDefaultVirtualServer() { - return virtualServerMap.get(null); + public NamedServer getDefaultNamedServer() { + return serverConfig.getDefaultNamedServer(); } /** @@ -189,16 +165,29 @@ public final class Server { } 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, () -> "JDK ciphers: " + SecurityUtil.Defaults.JDK_CIPHERS); + logger.log(level, () -> "OpenSSL ciphers: " + SecurityUtil.Defaults.OPENSSL_CIPHERS); + logger.log(level, () -> "OpenSSL available: " + OpenSsl.isAvailable()); + logger.log(level, () -> "OpenSSL ALPN support: " + OpenSsl.isAlpnSupported()); + logger.log(level, () -> "Installed ciphers on default server: " + + (serverConfig.getAddress().isSecure() ? getDefaultNamedServer().getSslContext().cipherSuites() : "")); + logger.log(level, () -> "Local host name: " + NetworkUtils.getLocalHostName("localhost")); + logger.log(level, () -> "Parent event loop group: " + parentEventLoopGroup + " threads=" + serverConfig.getParentThreadCount()); + logger.log(level, () -> "Child event loop group: " + childEventLoopGroup + " threads=" +serverConfig.getChildThreadCount()); + logger.log(level, () -> "Socket: " + socketChannelClass.getName()); + logger.log(level, () -> "Allocator: " + byteBufAllocator.getClass().getName()); logger.log(level, NetworkUtils::displayNetworkInterfaces); } + public ServerRequest newRequest() { + return new HttpServerRequest(); + } + + public ServerResponse newResponse(ServerRequest serverRequest) { + return serverRequest.getNamedServer().getHttpAddress().getVersion().majorVersion() == 1 ? + new HttpServerResponse(serverRequest) : new Http2ServerResponse(serverRequest); + } + public ServerTransport newTransport(HttpVersion httpVersion) { return httpVersion.majorVersion() == 1 ? new HttpServerTransport(this) : new Http2ServerTransport(this); } @@ -249,27 +238,23 @@ public final class Server { 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 DomainNameMapping createDomainNameMapping() { + if (serverConfig.getDefaultNamedServer() == null) { + throw new IllegalStateException("no default named server (with name '*') configured, unable to continue"); } - } - - private static ApplicationProtocolConfig newApplicationProtocolConfig() { - return new ApplicationProtocolConfig(ApplicationProtocolConfig.Protocol.ALPN, - ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, - ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, - ApplicationProtocolNames.HTTP_2, - ApplicationProtocolNames.HTTP_1_1); + DomainNameMapping domainNameMapping = null; + if (serverConfig.getAddress().isSecure() && serverConfig.getDefaultNamedServer().getSslContext() != null) { + DomainNameMappingBuilder mappingBuilder = + new DomainNameMappingBuilder<>(serverConfig.getDefaultNamedServer().getSslContext()); + for (NamedServer namedServer : serverConfig.getNamedServers().values()) { + String name = namedServer.getName(); + if (!"*".equals(name)) { + mappingBuilder.add(name, namedServer.getSslContext()); + } + } + domainNameMapping = mappingBuilder.build(); + } + return domainNameMapping; } static class HttpServerParentThreadFactory implements ThreadFactory { @@ -295,4 +280,165 @@ public final class Server { return thread; } } + + /** + * HTTP server builder. + */ + public static class Builder { + + private ByteBufAllocator byteBufAllocator; + + private EventLoopGroup parentEventLoopGroup; + + private EventLoopGroup childEventLoopGroup; + + private Class socketChannelClass; + + private ServerConfig serverConfig; + + Builder(HttpAddress httpAddress) { + this(httpAddress, NamedServer.builder(httpAddress, "*").build()); + } + + Builder(HttpAddress httpAddress, NamedServer defaultNamedServer) { + this.serverConfig = new ServerConfig(); + this.serverConfig.setAddress(httpAddress); + this.serverConfig.add(defaultNamedServer); + } + + public Builder enableDebug() { + this.serverConfig.enableDebug(); + return this; + } + + public Builder setByteBufAllocator(ByteBufAllocator byteBufAllocator) { + this.byteBufAllocator = byteBufAllocator; + return this; + } + + public Builder setParentEventLoopGroup(EventLoopGroup parentEventLoopGroup) { + this.parentEventLoopGroup = parentEventLoopGroup; + return this; + } + + public Builder setChildEventLoopGroup(EventLoopGroup childEventLoopGroup) { + this.childEventLoopGroup = childEventLoopGroup; + return this; + } + + public Builder setChannelClass(Class socketChannelClass) { + this.socketChannelClass = socketChannelClass; + return this; + } + + public Builder setUseEpoll(boolean useEpoll) { + this.serverConfig.setEpoll(useEpoll); + return this; + } + + public Builder setConnectTimeoutMillis(int connectTimeoutMillis) { + this.serverConfig.setConnectTimeoutMillis(connectTimeoutMillis); + return this; + } + + public Builder setParentThreadCount(int parentThreadCount) { + this.serverConfig.setParentThreadCount(parentThreadCount); + return this; + } + + public Builder setChildThreadCount(int childThreadCount) { + this.serverConfig.setChildThreadCount(childThreadCount); + return this; + } + + public Builder setTcpSendBufferSize(int tcpSendBufferSize) { + this.serverConfig.setTcpSendBufferSize(tcpSendBufferSize); + return this; + } + + public Builder setTcpReceiveBufferSize(int tcpReceiveBufferSize) { + this.serverConfig.setTcpReceiveBufferSize(tcpReceiveBufferSize); + return this; + } + + public Builder setTcpNoDelay(boolean tcpNoDelay) { + this.serverConfig.setTcpNodelay(tcpNoDelay); + return this; + } + + public Builder setReuseAddr(boolean reuseAddr) { + this.serverConfig.setReuseAddr(reuseAddr); + return this; + } + + public Builder setBacklogSize(int backlogSize) { + this.serverConfig.setBackLogSize(backlogSize); + return this; + } + + public Builder setMaxChunkSize(int maxChunkSize) { + this.serverConfig.setMaxChunkSize(maxChunkSize); + return this; + } + + public Builder setMaxInitialLineLength(int maxInitialLineLength) { + this.serverConfig.setMaxInitialLineLength(maxInitialLineLength); + return this; + } + + public Builder setMaxHeadersSize(int maxHeadersSize) { + this.serverConfig.setMaxHeadersSize(maxHeadersSize); + return this; + } + + public Builder setMaxContentLength(int maxContentLength) { + this.serverConfig.setMaxContentLength(maxContentLength); + return this; + } + + public Builder setMaxCompositeBufferComponents(int maxCompositeBufferComponents) { + this.serverConfig.setMaxCompositeBufferComponents(maxCompositeBufferComponents); + return this; + } + + public Builder setReadTimeoutMillis(int readTimeoutMillis) { + this.serverConfig.setReadTimeoutMillis(readTimeoutMillis); + return this; + } + + public Builder setConnectionTimeoutMillis(int connectionTimeoutMillis) { + this.serverConfig.setConnectTimeoutMillis(connectionTimeoutMillis); + return this; + } + + public Builder setIdleTimeoutMillis(int idleTimeoutMillis) { + this.serverConfig.setIdleTimeoutMillis(idleTimeoutMillis); + return this; + } + + public Builder setWriteBufferWaterMark(WriteBufferWaterMark writeBufferWaterMark) { + this.serverConfig.setWriteBufferWaterMark(writeBufferWaterMark); + return this; + } + + public Builder setEnableGzip(boolean enableGzip) { + this.serverConfig.setEnableGzip(enableGzip); + return this; + } + + public Builder setInstallHttp2Upgrade(boolean installHttp2Upgrade) { + this.serverConfig.setInstallHttp2Upgrade(installHttp2Upgrade); + return this; + } + + public Builder addServer(NamedServer namedServer) { + this.serverConfig.add(namedServer); + return this; + } + + public Server build() { + return new Server(serverConfig, byteBufAllocator, parentEventLoopGroup, childEventLoopGroup, socketChannelClass); + } + } + } 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 deleted file mode 100644 index 46ddf2f..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerBuilder.java +++ /dev/null @@ -1,244 +0,0 @@ -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.endpoint.Handler; -import org.xbib.netty.http.server.endpoint.NamedServer; -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 addServer(NamedServer namedServer) { - this.serverConfig.add(namedServer); - return this; - } - - public ServerBuilder addHandler(String path, Handler handler, String... methods) { - if (serverConfig.getNamedServers().isEmpty()) { - serverConfig.add(new NamedServer()); - } - serverConfig.getNamedServers().getLast().addHandler(path, handler, methods); - 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 index b796a6c..85d2247 100644 --- 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 @@ -2,23 +2,13 @@ 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.endpoint.NamedServer; -import javax.net.ssl.TrustManagerFactory; -import java.io.InputStream; -import java.security.KeyStore; -import java.util.ArrayList; -import java.util.Deque; -import java.util.LinkedList; -import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; public class ServerConfig { @@ -143,30 +133,6 @@ public class ServerConfig { */ 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; @@ -215,27 +181,10 @@ public class ServerConfig { 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 Deque namedServers; - - private TrustManagerFactory trustManagerFactory = TRUST_MANAGER_FACTORY; - - private KeyStore trustManagerKeyStore = null; + private Map namedServers; public ServerConfig() { - this.namedServers = new LinkedList<>(); - add(new NamedServer(null)); + this.namedServers = new LinkedHashMap<>(); } public ServerConfig enableDebug() { @@ -460,84 +409,20 @@ public class ServerConfig { 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 add(NamedServer namedServer) { - this.namedServers.add(namedServer); + this.namedServers.put(namedServer.getName(), namedServer); + for (String alias : namedServer.getAliases()) { + this.namedServers.put(alias, namedServer); + } return this; } - public Deque getNamedServers() { + public NamedServer getDefaultNamedServer() { + return namedServers.get("*"); + } + + public Map getNamedServers() { return namedServers; } - 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; - } -} +} \ No newline at end of file diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java new file mode 100644 index 0000000..acb7911 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerRequest.java @@ -0,0 +1,35 @@ +package org.xbib.netty.http.server; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import org.xbib.netty.http.server.endpoint.NamedServer; + +import java.util.List; +import java.util.Map; + +public interface ServerRequest { + + NamedServer getNamedServer(); + + ChannelHandlerContext getChannelHandlerContext(); + + FullHttpRequest getRequest(); + + void setContext(List context); + + List getContext(); + + void setRawParameters(Map rawParameters); + + Map getRawParameters(); + + String getContextPath(); + + String getEffectiveRequestPath(); + + Integer getSequenceId(); + + Integer streamId(); + + Integer 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/ServerResponse.java similarity index 90% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerResponse.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java index c4dd0ee..89df500 100644 --- 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/ServerResponse.java @@ -1,4 +1,4 @@ -package org.xbib.netty.http.server.transport; +package org.xbib.netty.http.server; import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.HttpResponseStatus; @@ -13,6 +13,8 @@ public interface ServerResponse { void setHeader(AsciiString name, String value); + HttpResponseStatus getLastStatus(); + void write(String text); void writeError(HttpResponseStatus status); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Context.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Context.java index 0f7d9cd..9e9fc6b 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Context.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Context.java @@ -1,5 +1,7 @@ package org.xbib.netty.http.server.endpoint; +import org.xbib.netty.http.server.endpoint.service.Service; + import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -9,9 +11,7 @@ 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 Handler#handle}, but can have arbitrary names. - * - * @see NamedServer#addHandlers(Object) + * as {@link Service#handle}, but can have arbitrary names. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @@ -25,9 +25,9 @@ public @interface Context { String value(); /** - * The HTTP methods supported by this context handler (default is "GET"). + * The HTTP methods supported by this context handler (default is "GET" and "HEAD"). * * @return the HTTP methods supported by this context handler */ - String[] methods() default "GET"; + String[] methods() default {"GET", "HEAD"}; } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Endpoint.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Endpoint.java index 3929cda..b887c23 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Endpoint.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Endpoint.java @@ -1,46 +1,215 @@ package org.xbib.netty.http.server.endpoint; +import org.xbib.net.QueryParameters; +import org.xbib.net.path.PathMatcher; +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.endpoint.service.Service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; -/** - * The {@code Endpoint} class holds an endpoint information. - */ +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE; + public class Endpoint { - private final NamedServer namedServer; + private static final PathMatcher pathMatcher = new PathMatcher(); - private final Map handlerMap; + public static final List DEFAULT_METHODS = Arrays.asList("GET", "HEAD"); - public Endpoint(NamedServer namedServer) { - this.namedServer = namedServer; - this.handlerMap = new LinkedHashMap<>(); + private final String prefix; + + private final String path; + + private final List methods; + + private final List contentTypes; + + private final List filters; + + private Endpoint(String prefix, String path, + List methods, List contentTypes, List filters) { + this.prefix = prefix; + this.path = path == null || path.isEmpty() ? + prefix + "/**" : path.startsWith("/") ? prefix + path : prefix + "/" + path; + this.methods = methods; + this.contentTypes = contentTypes; + this.filters = filters; } - /** - * Returns the map of supported HTTP methods and their corresponding handlers. - * - * @return the map of supported HTTP methods and their corresponding handlers - */ - public Map getHandlerMap() { - return handlerMap; + public static Builder builder() { + return new Builder(); } - /** - * Adds (or replaces) a handler for the given HTTP methods. - * - * @param handler the handler - * @param methods the HTTP methods supported by the handler (default is "GET") - */ - public void addHandler(Handler handler, String... methods) { - if (methods.length == 0) { - handlerMap.put("GET", handler); - namedServer.getMethods().add("GET"); - } else { - for (String method : methods) { - handlerMap.put(method, handler); - namedServer.getMethods().add(method); + public static Builder builder(Endpoint endpoint) { + return new Builder() + .setPrefix(endpoint.prefix) + .setPath(endpoint.path) + .setMethods(endpoint.methods) + .setContentTypes(endpoint.contentTypes) + .setFilters(endpoint.filters); + } + + public String getPrefix() { + return prefix; + } + + public String getPath() { + return path; + } + + public boolean matches(EndpointInfo info) { + return pathMatcher.match(path, info.path) && + (methods == null || methods.isEmpty() || (methods.contains(info.method))) && + (contentTypes == null || contentTypes.isEmpty() || info.contentType == null || + contentTypes.stream().anyMatch(info.contentType::startsWith)); + } + + public void resolveUriTemplate(ServerRequest serverRequest) { + if (pathMatcher.match(path, serverRequest.getEffectiveRequestPath())) { + QueryParameters queryParameters = pathMatcher.extractUriTemplateVariables(path, serverRequest.getEffectiveRequestPath()); + Map map = new LinkedHashMap<>(); + for (QueryParameters.Pair pair : queryParameters) { + map.put(pair.getFirst(), pair.getSecond()); + } + serverRequest.setRawParameters(map); + } + } + + public void executeFilters(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + serverRequest.setContext(pathMatcher.tokenizePath(getPrefix())); + for (Service service : filters) { + service.handle(serverRequest, serverResponse); + if (serverResponse.getLastStatus() != null) { + break; } } } + + @Override + public String toString() { + return path + "_" + methods + "_" + contentTypes + " --> " + filters; + } + + public static class EndpointInfo implements Comparable { + + private final String path; + + private final String method; + + private final String contentType; + + public EndpointInfo(ServerRequest serverRequest) { + this.path = serverRequest.getEffectiveRequestPath(); + this.method = serverRequest.getRequest().method().name(); + this.contentType = serverRequest.getRequest().headers().get(CONTENT_TYPE); + } + + @Override + public String toString() { + return path + "_" + method + "_" + contentType; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public boolean equals(Object o) { + return o instanceof EndpointInfo && toString().equals(o.toString()); + } + + @Override + public int compareTo(EndpointInfo o) { + return toString().compareTo(o.toString()); + } + } + + static class EndpointPathComparator implements Comparator { + + private final Comparator pathComparator; + + EndpointPathComparator(String path) { + this.pathComparator = pathMatcher.getPatternComparator(path); + } + + @Override + public int compare(Endpoint endpoint1, Endpoint endpoint2) { + return pathComparator.compare(endpoint1.path, endpoint2.path); + } + } + + public static class Builder { + + private String prefix; + + private String path; + + private List methods; + + private List contentTypes; + + private List filters; + + Builder() { + this.prefix = "/"; + this.path = "/**"; + this.methods = new ArrayList<>(); + this.contentTypes = new ArrayList<>(); + this.filters = new ArrayList<>(); + } + + public Builder setPrefix(String prefix) { + this.prefix = prefix; + return this; + } + + public Builder setPath(String path) { + this.path = path; + return this; + } + + public Builder setMethods(List methods) { + this.methods = methods; + return this; + } + + public Builder addMethod(String method) { + methods.add(method); + return this; + } + + public Builder setContentTypes(List contentTypes) { + this.contentTypes = contentTypes; + return this; + } + + public Builder addContentType(String contentType) { + this.contentTypes.add(contentType); + return this; + } + + public Builder setFilters(List filters) { + this.filters = filters; + return this; + } + + public Builder addFilter(Service filter) { + this.filters.add(filter); + return this; + } + + public Endpoint build() { + if (methods.isEmpty()) { + methods = DEFAULT_METHODS; + } + return new Endpoint(prefix, path, methods, contentTypes, filters); + } + } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDispatcher.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDispatcher.java new file mode 100644 index 0000000..697755f --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointDispatcher.java @@ -0,0 +1,12 @@ +package org.xbib.netty.http.server.endpoint; + +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; + +import java.io.IOException; + +@FunctionalInterface +public interface EndpointDispatcher { + + void dispatch(Endpoint endpoint, ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointResolver.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointResolver.java new file mode 100644 index 0000000..2e3bd6b --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/EndpointResolver.java @@ -0,0 +1,194 @@ +package org.xbib.netty.http.server.endpoint; + +import io.netty.handler.codec.http.HttpResponseStatus; +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.endpoint.service.MethodService; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +public class EndpointResolver { + + private static final Logger logger = Logger.getLogger(EndpointResolver.class.getName()); + + private final Endpoint defaultEndpoint; + + private final List endpoints; + + private final EndpointDispatcher endpointDispatcher; + + private final LRUCache> cache; + + private EndpointResolver(Endpoint defaultEndpoint, + List endpoints, + EndpointDispatcher endpointDispatcher, + int cacheSize) { + this.defaultEndpoint = defaultEndpoint == null ? createDefaultEndpoint() : defaultEndpoint; + this.endpoints = endpoints; + this.endpointDispatcher = endpointDispatcher; + this.cache = new LRUCache<>(cacheSize); + } + + public void resolve(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + Endpoint.EndpointInfo endpointInfo = new Endpoint.EndpointInfo(serverRequest); + cache.putIfAbsent(endpointInfo, endpoints.stream() + .filter(endpoint -> endpoint.matches(endpointInfo)) + .sorted(new Endpoint.EndpointPathComparator(serverRequest.getEffectiveRequestPath())).collect(Collectors.toList())); + List matchingEndpoints = cache.get(endpointInfo); + if (logger.isLoggable(Level.FINER)) { + logger.log(Level.FINER, "matching endpoints = " + matchingEndpoints); + } + if (matchingEndpoints.isEmpty()) { + if (defaultEndpoint != null) { + defaultEndpoint.resolveUriTemplate(serverRequest); + defaultEndpoint.executeFilters(serverRequest, serverResponse); + if (endpointDispatcher != null) { + endpointDispatcher.dispatch(defaultEndpoint, serverRequest, serverResponse); + } + } else { + serverResponse.write(HttpResponseStatus.NOT_IMPLEMENTED); + } + } else { + for (Endpoint endpoint : matchingEndpoints) { + endpoint.resolveUriTemplate(serverRequest); + endpoint.executeFilters(serverRequest, serverResponse); + if (serverResponse.getLastStatus() != null) { + break; + } + } + if (endpointDispatcher != null) { + for (Endpoint endpoint : matchingEndpoints) { + endpointDispatcher.dispatch(endpoint, serverRequest, serverResponse); + if (serverResponse.getLastStatus() != null) { + break; + } + } + } + } + } + + public LRUCache> getCache() { + return cache; + } + + protected Endpoint createDefaultEndpoint() { + return Endpoint.builder() + .setPath("/**") + .addMethod("GET") + .addMethod("HEAD") + .addFilter((req, resp) -> { + resp.writeError(HttpResponseStatus.NOT_FOUND,"No endpoint configured"); + }).build(); + } + + /** + * A simple LRU cache, based on a {@link LinkedHashMap}. + * + * @param the key type parameter + * @param the vale type parameter + */ + @SuppressWarnings("serial") + private static class LRUCache extends LinkedHashMap { + + private final int cacheSize; + + LRUCache(int cacheSize) { + super(16, 0.75f, true); + this.cacheSize = cacheSize; + } + + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() >= cacheSize; + } + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + + private int cacheSize; + + private String prefix; + + private Endpoint defaultEndpoint; + + private List endpoints; + + private EndpointDispatcher endpointDispatcher; + + Builder() { + this.cacheSize = 1024; + this.endpoints = new ArrayList<>(); + } + + public Builder setCacheSize(int cacheSize) { + this.cacheSize = cacheSize; + return this; + } + + public Builder setPrefix(String prefix) { + this.prefix = prefix; + return this; + } + + public Builder setDefaultEndpoint(Endpoint endpoint) { + this.defaultEndpoint = endpoint; + return this; + } + + /** + * + * @param endpoint + * @return this builder + */ + public Builder addEndpoint(Endpoint endpoint) { + if (endpoint.getPrefix().equals("/") && prefix != null && !prefix.isEmpty()) { + endpoints.add(Endpoint.builder(endpoint).setPrefix(prefix).build()); + } else { + endpoints.add(endpoint); + } + return this; + } + + /** + * Adds a service for the methods of the given object that + * are annotated with the {@link Context} annotation. + */ + public Builder addEndpoint(Object classWithAnnotatedMethods) { + for (Class clazz = classWithAnnotatedMethods.getClass(); clazz != null; clazz = clazz.getSuperclass()) { + for (Method method : clazz.getDeclaredMethods()) { + Context context = method.getAnnotation(Context.class); + if (context != null) { + addEndpoint(Endpoint.builder() + .setPrefix(prefix) + .setPath(context.value()) + .setMethods(Arrays.asList(context.methods())) + .addFilter(new MethodService(method, classWithAnnotatedMethods)) + .build()); + } + } + } + return this; + } + + public Builder setDispatcher(EndpointDispatcher endpointDispatcher) { + this.endpointDispatcher = endpointDispatcher; + return this; + } + + public EndpointResolver build() { + return new EndpointResolver(defaultEndpoint, endpoints, endpointDispatcher, cacheSize); + } + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Handler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Handler.java deleted file mode 100644 index 56c4bb8..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/Handler.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.xbib.netty.http.server.endpoint; - -import org.xbib.netty.http.server.transport.ServerRequest; -import org.xbib.netty.http.server.transport.ServerResponse; - -import java.io.IOException; - -/** - * A {@code Handler} is capable of serving content for resources within its context. - * - * @see NamedServer#addHandler - */ -@FunctionalInterface -public interface Handler { - - /** - * Handles the given request by using the given response. - * - * @param serverRequest the request to be served - * @param serverResponse the response to be generated - * @throws IOException if an IO error occurs - */ - void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NamedEndpoint.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NamedEndpoint.java deleted file mode 100644 index 0172937..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NamedEndpoint.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.xbib.netty.http.server.endpoint; - -public class NamedEndpoint { - - private final String name; - - private final Endpoint endpoint; - - NamedEndpoint(String name, Endpoint endpoint) { - this.name = name; - this.endpoint = endpoint; - } - - public String getName() { - return name; - } - - public Endpoint getEndpoint() { - return endpoint; - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NamedServer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NamedServer.java index 62d966b..7d80791 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NamedServer.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NamedServer.java @@ -1,45 +1,83 @@ package org.xbib.netty.http.server.endpoint; -import java.lang.reflect.Method; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.ApplicationProtocolNames; +import io.netty.handler.ssl.CipherSuiteFilter; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import org.xbib.netty.http.common.HttpAddress; +import org.xbib.netty.http.common.SecurityUtil; +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.endpoint.service.Service; +import org.xbib.netty.http.server.security.tls.SelfSignedCertificate; + +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.Provider; +import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Set; /** - * The {@code VirtualServer} class represents a virtual server. + * The {@code NamedServer} class represents a virtual server, with or without SSL. */ public class NamedServer { + private final HttpAddress httpAddress; + private final String name; + private final SslContext sslContext; + private final Set aliases; - private final Set methods; + private final List endpointResolvers; - private final Endpoint defaultEndpoint; - - private final Map endpointMap; - - private volatile boolean allowGeneratedIndex; - - public NamedServer() { - this(null); + protected NamedServer(HttpAddress httpAddress, String name, Set aliases, + List endpointResolvers) { + this(httpAddress, name, aliases, endpointResolvers, null); } /** * Constructs a {@code NamedServer} with the given name. * + * @param httpAddress HTTP address, used for determining if named server is secure or not * @param name the name, or null if it is the default server + * @param aliases alias names for the named server + * @param endpointResolvers the endpoint resolvers + * @param sslContext SSL context or null */ - public NamedServer(String name) { + protected NamedServer(HttpAddress httpAddress, String name, Set aliases, + List endpointResolvers, + SslContext sslContext) { + this.httpAddress = httpAddress; this.name = name; - this.aliases = new HashSet<>(); - this.methods = new HashSet<>(); - this.endpointMap = new HashMap<>(); - this.defaultEndpoint = new Endpoint(this); - endpointMap.put("*", new Endpoint(this)); // for "OPTIONS *" + this.sslContext = sslContext; + this.aliases = aliases; + this.endpointResolvers = endpointResolvers; + } + + public static Builder builder() { + return new Builder(HttpAddress.http1("localhost", 8008), "*"); + } + + public static Builder builder(HttpAddress httpAddress) { + return new Builder(httpAddress, "*"); + } + + public static Builder builder(HttpAddress httpAddress, String serverName) { + return new Builder(httpAddress, serverName); + } + + public HttpAddress getHttpAddress() { + return httpAddress; } /** @@ -51,13 +89,8 @@ public class NamedServer { return name; } - /** - * Adds an alias for this virtual server. - * - * @param alias the alias - */ - public void addAlias(String alias) { - aliases.add(alias); + public SslContext getSslContext() { + return sslContext; } /** @@ -69,127 +102,192 @@ public class NamedServer { 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; - } - - /** - * 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") - * @return this virtual server - * @throws IllegalArgumentException if path is malformed - */ - public NamedServer addHandler(String path, Handler handler, String... methods) { - if (path == null || !path.startsWith("/") && !path.equals("*")) { - throw new IllegalArgumentException("invalid path: " + path); + public void execute(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + if (endpointResolvers != null && !endpointResolvers.isEmpty()) { + for (EndpointResolver endpointResolver : endpointResolvers) { + endpointResolver.resolve(serverRequest, serverResponse); + } + } else { + serverResponse.writeError(HttpResponseStatus.NOT_IMPLEMENTED); } - String s = trimRight(path, '/'); - Endpoint info = new Endpoint(this); - Endpoint existing = endpointMap.putIfAbsent(s, info); - info = existing != null ? existing : info; - info.addHandler(handler, methods); - return this; } - /** - * Adds handler for all methods of the given object that - * are annotated with the {@link Context} annotation. - * - * @param o the object whose annotated methods are added - * @return this virtual server - * @throws IllegalArgumentException if a Context-annotated - * method has an {@link Context invalid signature} - */ - public NamedServer addHandlers(Object o) throws IllegalArgumentException { - for (Class c = o.getClass(); c != null; c = c.getSuperclass()) { - for (Method m : c.getDeclaredMethods()) { - Context context = m.getAnnotation(Context.class); - if (context != null) { - addHandler(context.value(), new MethodHandler(m, o), context.methods()); + public static class Builder { + + private HttpAddress httpAddress; + + private String serverName; + + private Set aliases; + + private List endpointResolvers; + + private TrustManagerFactory trustManagerFactory; + + private KeyStore trustManagerKeyStore; + + private Provider sslContextProvider; + + private SslProvider sslProvider; + + private Iterable ciphers; + + private CipherSuiteFilter cipherSuiteFilter; + + private InputStream keyCertChainInputStream; + + private InputStream keyInputStream; + + private String keyPassword; + + Builder(HttpAddress httpAddress, String serverName) { + this.httpAddress = httpAddress; + this.serverName = serverName; + this.aliases = new LinkedHashSet<>(); + this.endpointResolvers = new ArrayList<>(); + this.trustManagerFactory = SecurityUtil.Defaults.DEFAULT_TRUST_MANAGER_FACTORY; // InsecureTrustManagerFactory.INSTANCE; + this.sslProvider = SecurityUtil.Defaults.DEFAULT_SSL_PROVIDER; + this.ciphers = SecurityUtil.Defaults.DEFAULT_CIPHERS; + this.cipherSuiteFilter = SecurityUtil.Defaults.DEFAULT_CIPHER_SUITE_FILTER; + } + + public Builder setTrustManagerFactory(TrustManagerFactory trustManagerFactory) { + this.trustManagerFactory = trustManagerFactory; + return this; + } + + public Builder setTrustManagerKeyStore(KeyStore trustManagerKeyStore) { + this.trustManagerKeyStore = trustManagerKeyStore; + return this; + } + + public Builder setSslContextProvider(Provider sslContextProvider) { + this.sslContextProvider = sslContextProvider; + return this; + } + + public Builder setSslProvider(SslProvider sslProvider) { + this.sslProvider = sslProvider; + return this; + } + + public Builder setCiphers(Iterable ciphers) { + this.ciphers = ciphers; + return this; + } + + public Builder setCipherSuiteFilter(CipherSuiteFilter cipherSuiteFilter) { + this.cipherSuiteFilter = cipherSuiteFilter; + return this; + } + + public Builder setJdkSslProvider() { + setSslProvider(SslProvider.JDK); + setCiphers(SecurityUtil.Defaults.JDK_CIPHERS); + return this; + } + + public Builder setOpenSSLSslProvider() { + setSslProvider(SslProvider.OPENSSL); + setCiphers(SecurityUtil.Defaults.OPENSSL_CIPHERS); + return this; + } + + public Builder setKeyCertChainInputStream(InputStream keyCertChainInputStream) { + this.keyCertChainInputStream = keyCertChainInputStream; + return this; + } + + public Builder setKeyInputStream(InputStream keyInputStream) { + this.keyInputStream = keyInputStream; + return this; + } + + public Builder setKeyPassword(String keyPassword) { + this.keyPassword = keyPassword; + return this; + } + + public Builder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream) { + setKeyCertChainInputStream(keyCertChainInputStream); + setKeyInputStream(keyInputStream); + return this; + } + + public Builder setKeyCert(InputStream keyCertChainInputStream, InputStream keyInputStream, + String keyPassword) { + setKeyCertChainInputStream(keyCertChainInputStream); + setKeyInputStream(keyInputStream); + setKeyPassword(keyPassword); + return this; + } + + public Builder setSelfCert() throws Exception { + SelfSignedCertificate selfSignedCertificate = new SelfSignedCertificate(serverName); + setKeyCertChainInputStream(selfSignedCertificate.certificate()); + setKeyInputStream(selfSignedCertificate.privateKey()); + setKeyPassword(null); + return this; + } + + /** + * Adds an alias for this virtual server. + * + * @param alias the alias + * @return this builder + */ + public Builder addAlias(String alias) { + aliases.add(alias); + return this; + } + + public Builder addEndpointResolver(EndpointResolver endpointResolver) { + this.endpointResolvers.add(endpointResolver); + return this; + } + + public Builder singleEndpoint(String path, Service service) { + addEndpointResolver(EndpointResolver.builder() + .addEndpoint(Endpoint.builder().setPath(path).addFilter(service).build()).build()); + return this; + } + + public Builder singleEndpoint(String prefix, String path, Service service) { + addEndpointResolver(EndpointResolver.builder() + .addEndpoint(Endpoint.builder().setPrefix(prefix).setPath(path).addFilter(service).build()).build()); + return this; + } + + public NamedServer build() { + if (httpAddress.isSecure()) { + try { + trustManagerFactory.init(trustManagerKeyStore); + SslContextBuilder sslContextBuilder = SslContextBuilder + .forServer(keyCertChainInputStream, keyInputStream, keyPassword) + .trustManager(trustManagerFactory) + .sslProvider(sslProvider) + .ciphers(ciphers, cipherSuiteFilter); + if (sslContextProvider != null) { + sslContextBuilder.sslContextProvider(sslContextProvider); + } + if (httpAddress.getVersion().majorVersion() == 2) { + sslContextBuilder.applicationProtocolConfig(newApplicationProtocolConfig()); + } + return new NamedServer(httpAddress, serverName, aliases, endpointResolvers, sslContextBuilder.build()); + } catch (Throwable t) { + throw new RuntimeException(t); } + } else { + return new NamedServer(httpAddress, serverName, aliases, endpointResolvers); } } - return this; - } - /** - * Returns the endpoint for the given path. - * If an endpoint 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 NamedEndpoint getNamedEndpoint(String path) { - String s = trimRight(path, '/'); - Endpoint info = null; - String hook = null; - while (info == null && s != null) { - hook = s; - info = endpointMap.get(s); - s = getParentPath(s); + private static ApplicationProtocolConfig newApplicationProtocolConfig() { + return new ApplicationProtocolConfig(ApplicationProtocolConfig.Protocol.ALPN, + ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, + ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, + ApplicationProtocolNames.HTTP_2, + ApplicationProtocolNames.HTTP_1_1); } - return new NamedEndpoint(hook, info != null ? info : defaultEndpoint); } - - /** - * 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) { - String s = trimRight(path, '/'); // remove trailing slash - int slash = s.lastIndexOf('/'); - return slash == -1 ? null : s.substring(0, slash); - } - } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/ClasspathHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ClasspathService.java similarity index 73% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/ClasspathHandler.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ClasspathService.java index 5679375..a82de2c 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/ClasspathHandler.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ClasspathService.java @@ -1,10 +1,10 @@ -package org.xbib.netty.http.server.endpoint; +package org.xbib.netty.http.server.endpoint.service; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.HttpResponseStatus; -import org.xbib.netty.http.server.transport.ServerRequest; -import org.xbib.netty.http.server.transport.ServerResponse; +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; import org.xbib.netty.http.server.util.MimeTypeUtils; import java.io.IOException; @@ -13,32 +13,30 @@ import java.net.URL; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.nio.file.Files; -import java.nio.file.Path; import java.nio.file.Paths; -public class ClasspathHandler implements Handler { +public class ClasspathService implements Service { private final ClassLoader classLoader; private final String prefix; - public ClasspathHandler(ClassLoader classLoader, String prefix) { + public ClasspathService(ClassLoader classLoader, String prefix) { this.classLoader = classLoader; this.prefix = prefix; } @Override public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { - String contextPath = serverRequest.getContextPath(); - URL url = classLoader.getResource(prefix + contextPath); + String requestPath = serverRequest.getEffectiveRequestPath(); + URL url = classLoader.getResource(prefix + requestPath); if (url != null) { try { - Path path = Paths.get(url.toURI()); - FileChannel fileChannel = (FileChannel) Files.newByteChannel(path); + FileChannel fileChannel = (FileChannel) Files.newByteChannel(Paths.get(url.toURI())); MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); ByteBuf byteBuf = Unpooled.wrappedBuffer(mappedByteBuffer); try { - String contentType = MimeTypeUtils.guessFromPath(contextPath, false); + String contentType = MimeTypeUtils.guessFromPath(requestPath, false); serverResponse.write(HttpResponseStatus.OK, contentType, byteBuf); } finally { byteBuf.release(); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/EmptyService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/EmptyService.java new file mode 100644 index 0000000..2f6c03e --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/EmptyService.java @@ -0,0 +1,14 @@ +package org.xbib.netty.http.server.endpoint.service; + +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; +import org.xbib.netty.http.server.endpoint.service.Service; + +import java.io.IOException; + +public class EmptyService implements Service { + @Override + public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + // do nothing + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/MethodHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MethodService.java similarity index 60% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/MethodHandler.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MethodService.java index c34bde1..e83c4d3 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/MethodHandler.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MethodService.java @@ -1,7 +1,7 @@ -package org.xbib.netty.http.server.endpoint; +package org.xbib.netty.http.server.endpoint.service; -import org.xbib.netty.http.server.transport.ServerRequest; -import org.xbib.netty.http.server.transport.ServerResponse; +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -10,24 +10,22 @@ import java.lang.reflect.Method; /** * The {@code MethodHandler} invokes g a handler method on a specified object. * The method must have the same signature and contract as - * {@link Handler#handle}, but can have an arbitrary name. - * - * @see NamedServer#addHandlers(Object) + * {@link Service#handle}, but can have an arbitrary name. */ -public class MethodHandler implements Handler { +public class MethodService implements Service { private final Method m; private final Object obj; - public MethodHandler(Method m, Object obj) throws IllegalArgumentException { + public MethodService(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())) { + 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); } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NioHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/NioService.java similarity index 76% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NioHandler.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/NioService.java index 3365560..4e903f0 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/NioHandler.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/NioService.java @@ -1,10 +1,10 @@ -package org.xbib.netty.http.server.endpoint; +package org.xbib.netty.http.server.endpoint.service; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.HttpResponseStatus; -import org.xbib.netty.http.server.transport.ServerRequest; -import org.xbib.netty.http.server.transport.ServerResponse; +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; import org.xbib.netty.http.server.util.MimeTypeUtils; import java.io.IOException; @@ -13,11 +13,11 @@ import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; -public class NioHandler implements Handler { +public class NioService implements Service { private final Path prefix; - public NioHandler(Path prefix) { + public NioService(Path prefix) { this.prefix = prefix; if (!Files.exists(prefix) || !Files.isDirectory(prefix)) { throw new IllegalArgumentException("prefix: " + prefix + " (not a directory"); @@ -26,8 +26,8 @@ public class NioHandler implements Handler { @Override public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { - String requestPath = serverRequest.getRequestPath(); - Path path = prefix.resolve(requestPath.substring(1)); // starts always with '/' + String requestPath = serverRequest.getEffectiveRequestPath(); + Path path = prefix.resolve(requestPath); if (Files.exists(path) && Files.isReadable(path)) { try (FileChannel fileChannel = (FileChannel) Files.newByteChannel(path)) { MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/DirectoryHandler.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/PathReaderService.java similarity index 58% rename from netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/DirectoryHandler.java rename to netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/PathReaderService.java index a8b23d4..dca9d77 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/DirectoryHandler.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/PathReaderService.java @@ -1,10 +1,10 @@ -package org.xbib.netty.http.server.endpoint; +package org.xbib.netty.http.server.endpoint.service; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.handler.codec.http.HttpResponseStatus; -import org.xbib.netty.http.server.transport.ServerRequest; -import org.xbib.netty.http.server.transport.ServerResponse; +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; import java.io.IOException; import java.io.InputStream; @@ -13,28 +13,28 @@ import java.nio.channels.SeekableByteChannel; import java.nio.file.Files; import java.nio.file.Path; -public class DirectoryHandler implements Handler { +public class PathReaderService implements Service { private Path path; private ByteBufAllocator allocator; - public DirectoryHandler(Path path, ByteBufAllocator allocator) { + public PathReaderService(Path path, ByteBufAllocator allocator) { this.path = path; this.allocator = allocator; } @Override public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { - String uri = serverRequest.getRequest().uri(); - Path p = path.resolve(uri); - ByteBuf byteBuf = read(allocator, p); - serverResponse.write(HttpResponseStatus.OK, "application/octet-stream", byteBuf); - byteBuf.release(); + ByteBuf byteBuf = read(allocator, path.resolve(serverRequest.getEffectiveRequestPath())); + try { + serverResponse.write(HttpResponseStatus.OK, "application/octet-stream", byteBuf); + } finally { + byteBuf.release(); + } } - public static ByteBuf read(ByteBufAllocator allocator, Path path) - throws IOException { + private static ByteBuf read(ByteBufAllocator allocator, Path path) throws IOException { try (SeekableByteChannel sbc = Files.newByteChannel(path); InputStream in = Channels.newInputStream(sbc)) { int size = Math.toIntExact(sbc.size()); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Service.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Service.java new file mode 100644 index 0000000..f2d0847 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Service.java @@ -0,0 +1,22 @@ +package org.xbib.netty.http.server.endpoint.service; + +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; + +import java.io.IOException; + +/** + * A {@code Service} is capable of serving requests for resources within its context. + */ +@FunctionalInterface +public interface Service { + + /** + * Handles the given request by building and returning a response. + * + * @param serverRequest the request to be served + * @param serverResponse the response to be written + * @throws IOException if an IO error occurs + */ + void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; +} 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 index 06dfbf2..6d0a2e1 100644 --- 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 @@ -6,16 +6,10 @@ 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.Server; -import org.xbib.netty.http.server.endpoint.Handler; -import org.xbib.netty.http.server.endpoint.NamedEndpoint; -import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; 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; import java.util.logging.Level; import java.util.logging.Logger; @@ -24,13 +18,11 @@ abstract class BaseServerTransport implements ServerTransport { private static final Logger logger = Logger.getLogger(BaseServerTransport.class.getName()); - protected static final AtomicInteger requestCounter = new AtomicInteger(); - - private static final List METHODS = Arrays.asList("GET", "HEAD", "OPTIONS"); + static final AtomicInteger requestCounter = new AtomicInteger(); protected final Server server; - protected BaseServerTransport(Server server) { + BaseServerTransport(Server server) { this.server = server; } @@ -48,9 +40,9 @@ abstract class BaseServerTransport implements ServerTransport { * @param serverResponse the response * @return whether further processing should be performed */ - protected static boolean acceptRequest(ServerRequest serverRequest, ServerResponse serverResponse) { + static boolean acceptRequest(ServerRequest serverRequest, ServerResponse serverResponse) { HttpHeaders reqHeaders = serverRequest.getRequest().headers(); - HttpVersion version = serverRequest.getHttpAddress().getVersion(); + HttpVersion version = serverRequest.getNamedServer().getHttpAddress().getVersion(); switch (version.majorVersion()) { case 1: case 2: @@ -86,35 +78,7 @@ abstract class BaseServerTransport implements ServerTransport { * @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(); - NamedServer namedServer = serverRequest.getNamedServer(); - NamedEndpoint namedEndpoint = namedServer.getNamedEndpoint(path); - serverRequest.setContextPath(namedEndpoint.getName()); - Map methodHandlerMap = namedEndpoint.getEndpoint().getHandlerMap(); - // RFC 2616#5.1.1 - GET and HEAD must be supported - if (method.equals("GET") || method.equals("HEAD") || methodHandlerMap.containsKey(method)) { - Handler handler = methodHandlerMap.get(method); - if (handler == null) { - serverResponse.writeError(HttpResponseStatus.NOT_FOUND); - } else { - handler.handle(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 ? namedServer.getMethods() : methodHandlerMap.keySet()); - serverResponse.setHeader(HttpHeaderNames.ALLOW, String.join(", ", methods)); - if (method.equals("OPTIONS")) { // default OPTIONS handler - serverResponse.setHeader(HttpHeaderNames.CONTENT_LENGTH, "0"); // RFC2616#9.2 - serverResponse.write(HttpResponseStatus.OK); - } else if (namedServer.getMethods().contains(method)) { - serverResponse.write(HttpResponseStatus.METHOD_NOT_ALLOWED); // supported by server, but not this context (nor built-in) - } else { - serverResponse.writeError(HttpResponseStatus.NOT_IMPLEMENTED); // unsupported method - } - } + static void handle(HttpServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + serverRequest.getNamedServer().execute(serverRequest, serverResponse); } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java index 182f98b..547b4ec 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/Http2ServerResponse.java @@ -9,28 +9,41 @@ import io.netty.handler.codec.http.HttpResponseStatus; 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.handler.codec.http2.HttpConversionUtil; import io.netty.util.AsciiString; import org.xbib.netty.http.server.ServerName; +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; public class Http2ServerResponse implements ServerResponse { + private static final Logger logger = Logger.getLogger(Http2ServerResponse.class.getName()); + private final ServerRequest serverRequest; private final ChannelHandlerContext ctx; private Http2Headers headers; - public Http2ServerResponse(ServerRequest serverRequest, ChannelHandlerContext ctx) { + private HttpResponseStatus httpResponseStatus; + + public Http2ServerResponse(ServerRequest serverRequest) { + Objects.requireNonNull(serverRequest); + Objects.requireNonNull(serverRequest.getChannelHandlerContext()); this.serverRequest = serverRequest; - this.ctx = ctx; + this.ctx = serverRequest.getChannelHandlerContext(); this.headers = new DefaultHttp2Headers(); } @@ -39,38 +52,30 @@ public class Http2ServerResponse implements ServerResponse { headers.set(name, value); } + @Override + public HttpResponseStatus getLastStatus() { + return httpResponseStatus; + } + @Override public void write(String text) { write(HttpResponseStatus.OK, "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(HttpResponseStatus status) { - writeError(status, status.code() < 400 ? ":)" : "sorry it didn't work out :("); + writeError(status, status.reasonPhrase()); } /** * 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) + * @param text the text body */ @Override public void writeError(HttpResponseStatus status, String text) { - write(status, "text/html; charset=utf-8", - String.format("%n%n%d %s%n" + - "

%d %s

%n

%s

%n", - status.code(), status.reasonPhrase(), - status.code(), status.reasonPhrase(), - escapeHTML(text))); + write(status, "text/plain; charset=utf-8", status.code() + " " + text); } @Override @@ -119,12 +124,15 @@ public class Http2ServerResponse implements ServerResponse { headers.setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), streamId); } } - Http2Headers http2Headers = new DefaultHttp2Headers() - .status(status.codeAsText()) - .add(headers); - ctx.channel().write(new DefaultHttp2HeadersFrame(http2Headers,byteBuf == null)); + Http2Headers http2Headers = new DefaultHttp2Headers().status(status.codeAsText()).add(headers); + Http2HeadersFrame http2HeadersFrame = new DefaultHttp2HeadersFrame(http2Headers,byteBuf == null); + logger.log(Level.FINEST, http2HeadersFrame::toString); + ctx.channel().write(http2HeadersFrame); + this.httpResponseStatus = status; if (byteBuf != null) { - ctx.channel().write(new DefaultHttp2DataFrame(byteBuf, true)); + Http2DataFrame http2DataFrame = new DefaultHttp2DataFrame(byteBuf, true); + logger.log(Level.FINEST, http2DataFrame::toString); + ctx.channel().write(http2DataFrame); } ctx.channel().flush(); } @@ -166,7 +174,7 @@ public class Http2ServerResponse implements ServerResponse { break; } if (ref != null) { - es.append(s.substring(start, i)).append(ref); + es.append(s, start, i).append(ref); start = i + 1; } } 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 index b3f42f6..96113e9 100644 --- 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 @@ -6,8 +6,8 @@ import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http2.Http2Settings; import io.netty.handler.codec.http2.HttpConversionUtil; -import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.ServerResponse; import org.xbib.netty.http.server.endpoint.NamedServer; import java.io.IOException; @@ -26,15 +26,19 @@ public class Http2ServerTransport extends BaseServerTransport { @Override public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, Integer sequenceId) throws IOException { int requestId = requestCounter.incrementAndGet(); - NamedServer namedServer = server.getVirtualServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); + NamedServer namedServer = server.getNamedServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); if (namedServer == null) { - namedServer = server.getDefaultVirtualServer(); + namedServer = server.getDefaultNamedServer(); } - HttpAddress httpAddress = server.getServerConfig().getAddress(); Integer streamId = fullHttpRequest.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); - ServerRequest serverRequest = new ServerRequest(namedServer, httpAddress, fullHttpRequest, - sequenceId, streamId, requestId); - ServerResponse serverResponse = new Http2ServerResponse(serverRequest, ctx); + HttpServerRequest serverRequest = new HttpServerRequest(); + serverRequest.setNamedServer(namedServer); + serverRequest.setChannelHandlerContext(ctx); + serverRequest.setRequest(fullHttpRequest); + serverRequest.setSequenceId(sequenceId); + serverRequest.setRequestId(requestId); + serverRequest.setStreamId(streamId); + ServerResponse serverResponse = new Http2ServerResponse(serverRequest); if (acceptRequest(serverRequest, serverResponse)) { handle(serverRequest, serverResponse); } else { diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerRequest.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerRequest.java new file mode 100644 index 0000000..96eb061 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerRequest.java @@ -0,0 +1,124 @@ +package org.xbib.netty.http.server.transport; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.endpoint.NamedServer; + +import java.util.List; +import java.util.Map; + +/** + * The {@code HttpServerRequest} class encapsulates a single request. There must be a default constructor. + */ +public class HttpServerRequest implements ServerRequest { + + private static final String PATH_SEPARATOR = "/"; + + private NamedServer namedServer; + + private ChannelHandlerContext ctx; + + private List context; + + private Map rawParameters; + + private FullHttpRequest httpRequest; + + private Integer sequenceId; + + private Integer streamId; + + private Integer requestId; + + public void setNamedServer(NamedServer namedServer) { + this.namedServer = namedServer; + } + + @Override + public NamedServer getNamedServer() { + return namedServer; + } + + public void setChannelHandlerContext(ChannelHandlerContext ctx) { + this.ctx = ctx; + } + + @Override + public ChannelHandlerContext getChannelHandlerContext() { + return ctx; + } + + public void setRequest(FullHttpRequest fullHttpRequest) { + this.httpRequest = fullHttpRequest; + } + + @Override + public FullHttpRequest getRequest() { + return httpRequest; + } + + public void setContext(List context) { + this.context = context; + } + + @Override + public List getContext() { + return context; + } + + @Override + public String getContextPath() { + return String.join(PATH_SEPARATOR, context); + } + + @Override + public String getEffectiveRequestPath() { + String uri = httpRequest.uri(); + return context != null && !context.isEmpty() && uri.length() > 1 ? + uri.substring(getContextPath().length() + 2) : uri; + } + + public void setRawParameters(Map rawParameters) { + this.rawParameters = rawParameters; + } + + @Override + public Map getRawParameters() { + return rawParameters; + } + + public void setSequenceId(Integer sequenceId) { + this.sequenceId = sequenceId; + } + + @Override + public Integer getSequenceId() { + return sequenceId; + } + + public void setStreamId(Integer streamId) { + this.streamId = streamId; + } + + @Override + public Integer streamId() { + return streamId; + } + + public void setRequestId(Integer requestId) { + this.requestId = requestId; + } + + @Override + public Integer requestId() { + return requestId; + } + + public String toString() { + return "ServerRequest[namedServer=" + namedServer + + ",context=" + context + + ",request=" + httpRequest + + "]"; + } +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java index df9821a..7d1ffd7 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerResponse.java @@ -2,7 +2,6 @@ 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; @@ -14,6 +13,8 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.util.AsciiString; import org.xbib.netty.http.server.ServerName; +import org.xbib.netty.http.server.ServerRequest; +import org.xbib.netty.http.server.ServerResponse; import org.xbib.netty.http.server.handler.http.HttpPipelinedResponse; import java.nio.CharBuffer; @@ -21,9 +22,16 @@ import java.nio.charset.Charset; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; +import java.util.Objects; +import java.util.logging.Level; +import java.util.logging.Logger; public class HttpServerResponse implements ServerResponse { + private static final Logger logger = Logger.getLogger(HttpServerResponse.class.getName()); + + private static final String EMPTY_STRING = ""; + private final ServerRequest serverRequest; private final ChannelHandlerContext ctx; @@ -32,9 +40,13 @@ public class HttpServerResponse implements ServerResponse { private HttpHeaders trailingHeaders; - public HttpServerResponse(ServerRequest serverRequest, ChannelHandlerContext ctx) { + private HttpResponseStatus httpResponseStatus; + + public HttpServerResponse(ServerRequest serverRequest) { + Objects.requireNonNull(serverRequest, "serverRequest"); + Objects.requireNonNull(serverRequest.getChannelHandlerContext(), "serverRequest channelHandlerContext"); this.serverRequest = serverRequest; - this.ctx = ctx; + this.ctx = serverRequest.getChannelHandlerContext(); this.headers = new DefaultHttpHeaders(); this.trailingHeaders = new DefaultHttpHeaders(); } @@ -44,6 +56,11 @@ public class HttpServerResponse implements ServerResponse { headers.set(name, value); } + @Override + public HttpResponseStatus getLastStatus() { + return httpResponseStatus; + } + @Override public void write(String text) { write(HttpResponseStatus.OK, "text/plain; charset=utf-8", text); @@ -56,31 +73,23 @@ public class HttpServerResponse implements ServerResponse { */ @Override public void writeError(HttpResponseStatus status) { - writeError(status, status.code() < 400 ? ":)" : "sorry it didn't work out :("); + writeError(status, status.reasonPhrase()); } /** * 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) + * @param text the text body */ @Override public void writeError(HttpResponseStatus status, String text) { - write(status, "text/html; charset=utf-8", - String.format("%n%n%d %s%n" + - "

%d %s

%n

%s

%n", - status.code(), status.reasonPhrase(), - status.code(), status.reasonPhrase(), - escapeHTML(text))); + write(status, "text/plain; charset=utf-8", status.code() + " " + text); } @Override public void write(HttpResponseStatus status) { - write(status, null, (ByteBuf) null); + write(status, "application/octet-stream", EMPTY_STRING); } @Override @@ -95,88 +104,48 @@ public class HttpServerResponse implements ServerResponse { @Override public void write(HttpResponseStatus 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()); + Objects.requireNonNull(byteBuf); + CharSequence s = headers.get(HttpHeaderNames.CONTENT_TYPE); + if (s == null) { + s = contentType != null ? contentType : HttpHeaderValues.APPLICATION_OCTET_STREAM; + headers.add(HttpHeaderNames.CONTENT_TYPE, s); } - FullHttpResponse fullHttpResponse = byteBuf != null ? - new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, - status, byteBuf, headers, trailingHeaders) : - new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, - status, Unpooled.EMPTY_BUFFER, headers, trailingHeaders); + 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 = + new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, byteBuf, headers, trailingHeaders); if (serverRequest != null && serverRequest.getSequenceId() != null) { HttpPipelinedResponse httpPipelinedResponse = new HttpPipelinedResponse(fullHttpResponse, ctx.channel().newPromise(), serverRequest.getSequenceId()); if (ctx.channel().isWritable()) { + logger.log(Level.FINEST, fullHttpResponse::toString); ctx.channel().writeAndFlush(httpPipelinedResponse); + httpResponseStatus = status; + } else { + logger.log(Level.WARNING, "channel not writeable"); } } else { if (ctx.channel().isWritable()) { + logger.log(Level.FINEST, fullHttpResponse::toString); ctx.channel().writeAndFlush(fullHttpResponse); + httpResponseStatus = status; + } else { + logger.log(Level.WARNING, "channel not writeable"); } } } - - /** - * 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, 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/HttpServerTransport.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerTransport.java index 9238c41..49f0acf 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerTransport.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/HttpServerTransport.java @@ -5,7 +5,6 @@ import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpResponseStatus; 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.endpoint.NamedServer; @@ -26,14 +25,17 @@ public class HttpServerTransport extends BaseServerTransport { public void requestReceived(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest, Integer sequenceId) throws IOException { int requestId = requestCounter.incrementAndGet(); - NamedServer namedServer = server.getVirtualServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); + NamedServer namedServer = server.getNamedServer(fullHttpRequest.headers().get(HttpHeaderNames.HOST)); if (namedServer == null) { - namedServer = server.getDefaultVirtualServer(); + namedServer = server.getDefaultNamedServer(); } - HttpAddress httpAddress = server.getServerConfig().getAddress(); - ServerRequest serverRequest = new ServerRequest(namedServer, httpAddress, fullHttpRequest, - sequenceId, null, requestId); - ServerResponse serverResponse = new HttpServerResponse(serverRequest, ctx); + HttpServerRequest serverRequest = new HttpServerRequest(); + serverRequest.setNamedServer(namedServer); + serverRequest.setChannelHandlerContext(ctx); + serverRequest.setRequest(fullHttpRequest); + serverRequest.setSequenceId(sequenceId); + serverRequest.setRequestId(requestId); + HttpServerResponse serverResponse = new HttpServerResponse(serverRequest); if (acceptRequest(serverRequest, serverResponse)) { handle(serverRequest, serverResponse); } else { 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 deleted file mode 100644 index 1903e93..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/transport/ServerRequest.java +++ /dev/null @@ -1,71 +0,0 @@ -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.endpoint.NamedServer; - -/** - * The {@code ServerRequest} class encapsulates a single request. - */ -public class ServerRequest { - - private final NamedServer namedServer; - - private final HttpAddress httpAddress; - - private final FullHttpRequest httpRequest; - - private final Integer sequenceId; - - private final Integer streamId; - - private final Integer requestId; - - private String contextPath; - - public ServerRequest(NamedServer namedServer, HttpAddress httpAddress, - FullHttpRequest httpRequest, Integer sequenceId, Integer streamId, Integer requestId) { - this.namedServer = namedServer; - this.httpAddress = httpAddress; - this.httpRequest = httpRequest; - this.sequenceId = sequenceId; - this.streamId = streamId; - this.requestId = requestId; - } - - public NamedServer getNamedServer() { - return namedServer; - } - - public void setContextPath(String contextPath) { - this.contextPath = contextPath; - } - - public String getContextPath() { - return contextPath; - } - - public String getRequestPath() { - return contextPath != null ? httpRequest.uri().substring(contextPath.length()) : httpRequest.uri(); - } - - public HttpAddress getHttpAddress() { - return httpAddress; - } - - public FullHttpRequest getRequest() { - return httpRequest; - } - - public Integer getSequenceId() { - return sequenceId; - } - - public Integer streamId() { - return streamId; - } - - public Integer requestId() { - return requestId; - } -} 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 deleted file mode 100644 index 6f06163..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/util/NetworkUtils.java +++ /dev/null @@ -1,586 +0,0 @@ -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/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 index 6f5d43b..745653e 100644 --- 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 @@ -10,6 +10,7 @@ import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.endpoint.NamedServer; import java.nio.charset.StandardCharsets; import java.util.concurrent.ExecutorService; @@ -29,18 +30,21 @@ class CleartextHttp1Test { @Test void testSimpleClearTextHttp1() throws Exception { HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - Server server = Server.builder() - .bind(httpAddress).build(); - server.getDefaultVirtualServer().addHandler("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())); + NamedServer namedServer = NamedServer.builder(httpAddress) + .singleEndpoint("/**", (request, response) -> + response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + .build(); + Server server = Server.builder(namedServer).build(); server.accept(); Client client = Client.builder() .build(); AtomicInteger counter = new AtomicInteger(); final ResponseListener responseListener = fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - counter.incrementAndGet(); + if (fullHttpResponse.status().equals(HttpResponseStatus.OK)) { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); + counter.incrementAndGet(); + } }; try { Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) @@ -60,11 +64,11 @@ class CleartextHttp1Test { void testPooledClearTextHttp1() throws Exception { int loop = 4096; HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - Server server = Server.builder() - .bind(httpAddress).build(); - server.getDefaultVirtualServer().addHandler("/", (request, response) -> { - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()); - }); + NamedServer namedServer = NamedServer.builder(httpAddress) + .singleEndpoint("/**", (request, response) -> + response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + .build(); + Server server = Server.builder(namedServer).build(); server.accept(); Client client = Client.builder() .addPoolNode(httpAddress) @@ -72,9 +76,11 @@ class CleartextHttp1Test { .build(); AtomicInteger counter = new AtomicInteger(); final ResponseListener responseListener = fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); - counter.incrementAndGet(); + if (fullHttpResponse.status().equals(HttpResponseStatus.OK)) { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + " response body = " + response); + counter.incrementAndGet(); + } }; try { for (int i = 0; i < loop; i++) { @@ -103,11 +109,11 @@ class CleartextHttp1Test { int threads = 4; int loop = 4 * 1024; HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - Server server = Server.builder() - .bind(httpAddress).build(); - server.getDefaultVirtualServer().addHandler("/", (request, response) -> { - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()); - }); + NamedServer namedServer = NamedServer.builder(httpAddress) + .singleEndpoint("/**", (request, response) -> + response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + .build(); + Server server = Server.builder(namedServer).build(); server.accept(); Client client = Client.builder() .addPoolNode(httpAddress) @@ -115,10 +121,12 @@ class CleartextHttp1Test { .build(); AtomicInteger counter = new AtomicInteger(); final ResponseListener responseListener = fullHttpResponse -> { - String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); - //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + - // " response=" + response + " payload=" + payload); - counter.incrementAndGet(); + if (fullHttpResponse.status().equals(HttpResponseStatus.OK)) { + String response = fullHttpResponse.content().toString(StandardCharsets.UTF_8); + //logger.log(Level.INFO, "status = " + fullHttpResponse.status() + + // " response=" + response + " payload=" + payload); + counter.incrementAndGet(); + } }; try { ExecutorService executorService = Executors.newFixedThreadPool(threads); 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 index 66843db..aad9789 100644 --- 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 @@ -9,6 +9,7 @@ import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.endpoint.NamedServer; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -29,11 +30,11 @@ class CleartextHttp2Test { @Test void testSimpleCleartextHttp2() throws Exception { HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); - Server server = Server.builder() - .bind(httpAddress) + NamedServer namedServer = NamedServer.builder(httpAddress) + .singleEndpoint("/", (request, response) -> + response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) .build(); - server.getDefaultVirtualServer().addHandler("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())); + Server server = Server.builder(namedServer).build(); server.accept(); Client client = Client.builder() .build(); @@ -46,7 +47,7 @@ class CleartextHttp2Test { counter.incrementAndGet(); }; try { - String payload = Integer.toString(0) + "/" + Integer.toString(0); + String payload = 0 + "/" + 0; Request request = Request.get().setVersion("HTTP/2.0") .url(server.getServerConfig().getAddress().base()) .content(payload, "text/plain") @@ -69,10 +70,11 @@ class CleartextHttp2Test { void testPooledClearTextHttp2() throws Exception { int loop = 4096; HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); - Server server = Server.builder() - .bind(httpAddress).build(); - server.getDefaultVirtualServer().addHandler("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())); + NamedServer namedServer = NamedServer.builder(httpAddress) + .singleEndpoint("/", (request, response) -> + response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + .build(); + Server server = Server.builder(namedServer).build(); server.accept(); Client client = Client.builder() .addPoolNode(httpAddress) @@ -116,12 +118,12 @@ class CleartextHttp2Test { int threads = 2; int loop = 4 * 1024; HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); - Server server = Server.builder() - .bind(httpAddress) + NamedServer namedServer = NamedServer.builder(httpAddress) + .singleEndpoint("/", (request, response) -> + response.write(HttpResponseStatus.OK, "text/plain", + request.getRequest().content().toString(StandardCharsets.UTF_8))) .build(); - server.getDefaultVirtualServer().addHandler("/", (request, response) -> - response.write(request.getRequest().content().toString(StandardCharsets.UTF_8)) - ); + Server server = Server.builder(namedServer).build(); server.accept(); Client client = Client.builder() .addPoolNode(httpAddress) @@ -144,7 +146,7 @@ class CleartextHttp2Test { executorService.submit(() -> { try { for (int i = 0; i < loop; i++) { - String payload = Integer.toString(t) + "/" + Integer.toString(i); + String payload = t + "/" + i; Request request = Request.get().setVersion("HTTP/2.0") .url(server.getServerConfig().getAddress().base()) .content(payload, "text/plain") @@ -180,24 +182,26 @@ class CleartextHttp2Test { HttpAddress httpAddress1 = HttpAddress.http2("localhost", 8008); AtomicInteger counter1 = new AtomicInteger(); - Server server1 = Server.builder() - .bind(httpAddress1).build(); - server1.getDefaultVirtualServer().addHandler("/", (request, response) -> { - response.write(request.getRequest().content().toString(StandardCharsets.UTF_8)); - counter1.incrementAndGet(); - }); + NamedServer namedServer1 = NamedServer.builder(httpAddress1) + .singleEndpoint("/", (request, response) -> { + response.write(HttpResponseStatus.OK, "text/plain", + request.getRequest().content().toString(StandardCharsets.UTF_8)); + counter1.incrementAndGet(); + }) + .build(); + Server server1 = Server.builder(namedServer1).build(); server1.accept(); - HttpAddress httpAddress2 = HttpAddress.http2("localhost", 8009); AtomicInteger counter2 = new AtomicInteger(); - Server server2 = Server.builder() - .bind(httpAddress2).build(); - server2.getDefaultVirtualServer().addHandler("/", (request, response) -> { - response.write(request.getRequest().content().toString(StandardCharsets.UTF_8)); - counter2.incrementAndGet(); - }); + NamedServer namedServer2 = NamedServer.builder(httpAddress2) + .singleEndpoint("/", (request, response) -> { + response.write(HttpResponseStatus.OK, "text/plain", + request.getRequest().content().toString(StandardCharsets.UTF_8)); + counter2.incrementAndGet(); + }) + .build(); + Server server2 = Server.builder(namedServer2).build(); server2.accept(); - Client client = Client.builder() .addPoolNode(httpAddress1) .addPoolNode(httpAddress2) @@ -220,7 +224,7 @@ class CleartextHttp2Test { executorService.submit(() -> { try { for (int i = 0; i < loop; i++) { - String payload = Integer.toString(t) + "/" + Integer.toString(i); + String payload = t + "/" + i; Request request = Request.get().setVersion("HTTP/2.0") .uri("/") .content(payload, "text/plain") diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/EndpointTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/EndpointTest.java new file mode 100644 index 0000000..f98820f --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/EndpointTest.java @@ -0,0 +1,149 @@ +package org.xbib.netty.http.server.test; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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 org.xbib.netty.http.server.endpoint.Endpoint; +import org.xbib.netty.http.server.endpoint.EndpointResolver; +import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.endpoint.service.NioService; +import org.xbib.netty.http.server.endpoint.service.Service; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(NettyHttpExtension.class) +class EndpointTest { + + private static final Logger logger = Logger.getLogger(EndpointTest.class.getName()); + + @Test + void testEndpoints() throws Exception { + Path vartmp = Paths.get("/var/tmp/"); + Service service = new NioService(vartmp); + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + EndpointResolver endpointResolver = EndpointResolver.builder() + .addEndpoint(Endpoint.builder().setPrefix("/static").setPath("/**").build()) + .addEndpoint(Endpoint.builder().setPrefix("/static1").setPath("/**").build()) + .addEndpoint(Endpoint.builder().setPrefix("/static2").setPath("/**").build()) + .setDispatcher((endpoint, req, resp) -> { + logger.log(Level.FINE, "endpoint=" + endpoint + " req=" + req); + service.handle(req, resp); + }) + .build(); + NamedServer namedServer = NamedServer.builder(httpAddress) + .addEndpointResolver(endpointResolver) + .build(); + Server server = Server.builder(namedServer) + .build(); + server.logDiagnostics(Level.INFO); + Client client = Client.builder() + .build(); + final AtomicBoolean success = new AtomicBoolean(false); + final AtomicBoolean success1 = new AtomicBoolean(false); + final AtomicBoolean success2 = new AtomicBoolean(false); + try { + Files.write(vartmp.resolve("test.txt"), "Hello Jörg".getBytes(StandardCharsets.UTF_8)); + Files.write(vartmp.resolve("test1.txt"), "Hello Jörg 1".getBytes(StandardCharsets.UTF_8)); + Files.write(vartmp.resolve("test2.txt"), "Hello Jörg 2".getBytes(StandardCharsets.UTF_8)); + server.accept(); + Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base().resolve("/static/test.txt")) + .build() + .setResponseListener(r -> { + assertEquals("Hello Jörg", r.content().toString(StandardCharsets.UTF_8)); + success.set(true); + }); + client.execute(request).get(); + Request request1 = Request.get().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base().resolve("/static1/test1.txt")) + .build() + .setResponseListener(r -> { + assertEquals("Hello Jörg 1", r.content().toString(StandardCharsets.UTF_8)); + success1.set(true); + }); + client.execute(request1).get(); + Request request2 = Request.get().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base().resolve("/static2/test2.txt")) + .build() + .setResponseListener(r -> { + assertEquals("Hello Jörg 2", r.content().toString(StandardCharsets.UTF_8)); + success2.set(true); + }); + client.execute(request2).get(); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + Files.delete(vartmp.resolve("test.txt")); + Files.delete(vartmp.resolve("test1.txt")); + Files.delete(vartmp.resolve("test2.txt")); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success.get()); + assertTrue(success1.get()); + assertTrue(success2.get()); + } + + @Test + void testMassiveEndpoints() throws IOException { + int max = 1000; + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + EndpointResolver.Builder endpointResolverBuilder = EndpointResolver.builder() + .setPrefix("/static"); + for (int i = 0; i < max; i++) { + endpointResolverBuilder.addEndpoint(Endpoint.builder() + .setPath(i + "/**") + .addFilter((req, resp) -> resp.write(HttpResponseStatus.OK)) + .build()); + } + endpointResolverBuilder.setDispatcher((endpoint, req, resp) -> { + logger.log(Level.FINEST, "endpoint=" + endpoint + " req=" + req + " resp=" + resp); + }); + NamedServer namedServer = NamedServer.builder(httpAddress) + .addEndpointResolver(endpointResolverBuilder.build()) + .build(); + Server server = Server.builder(namedServer) + .build(); + server.logDiagnostics(Level.INFO); + Client client = Client.builder() + .build(); + final AtomicInteger count = new AtomicInteger(0); + try { + server.accept(); + for (int i = 0; i < max; i++) { + Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base().resolve("/static/" + i + "/test.txt")) + .build() + .setResponseListener(r -> { + if (r.status().equals(HttpResponseStatus.OK)) { + count.incrementAndGet(); + logger.log(Level.INFO, r.status().reasonPhrase()); + } else { + logger.log(Level.WARNING, r.status().reasonPhrase()); + } + }); + client.execute(request).get(); + } + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + logger.log(Level.INFO, "server and client shut down"); + } + assertEquals(max, count.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 index 3859cc4..085783f 100644 --- 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 @@ -10,6 +10,7 @@ import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.endpoint.NamedServer; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -29,13 +30,14 @@ class SecureHttp1Test { @Test void testSimpleSecureHttp1() throws Exception { - Server server = Server.builder() - .setJdkSslProvider() + HttpAddress httpAddress = HttpAddress.secureHttp1("localhost", 8143); + Server server = Server.builder(NamedServer.builder(httpAddress) .setSelfCert() - .bind(HttpAddress.secureHttp1("localhost", 8143)) + .singleEndpoint("/", (request, response) -> + response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + .build()) .build(); Client client = Client.builder() - .setJdkSslProvider() .trustInsecure() .build(); AtomicInteger counter = new AtomicInteger(); @@ -45,8 +47,6 @@ class SecureHttp1Test { counter.getAndIncrement(); }; try { - server.getDefaultVirtualServer().addHandler("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())); server.accept(); Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) .url(server.getServerConfig().getAddress().base()) @@ -64,15 +64,14 @@ class SecureHttp1Test { void testPooledSecureHttp1() throws Exception { int loop = 4096; HttpAddress httpAddress = HttpAddress.secureHttp1("localhost", 8143); - Server server = Server.builder() - .setJdkSslProvider() + Server server = Server.builder(NamedServer.builder(httpAddress) .setSelfCert() - .bind(httpAddress).build(); - server.getDefaultVirtualServer().addHandler("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())); + .singleEndpoint("/", (request, response) -> + response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + .build()) + .build(); server.accept(); Client client = Client.builder() - .setJdkSslProvider() .trustInsecure() .addPoolNode(httpAddress) .setPoolNodeConnectionLimit(2) @@ -111,17 +110,15 @@ class SecureHttp1Test { int threads = 4; int loop = 4 * 1024; HttpAddress httpAddress = HttpAddress.secureHttp1("localhost", 8143); - Server server = Server.builder() - .setJdkSslProvider() + Server server = Server.builder(NamedServer.builder(httpAddress) .setSelfCert() - .bind(httpAddress) + .singleEndpoint("/", (request, response) -> + response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()) + ) + .build()) .build(); - server.getDefaultVirtualServer().addHandler("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()) - ); server.accept(); Client client = Client.builder() - .setJdkSslProvider() .trustInsecure() .addPoolNode(httpAddress) .setPoolNodeConnectionLimit(threads) 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 index 05ef17b..c49bbf2 100644 --- 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 @@ -9,6 +9,7 @@ import org.xbib.netty.http.client.listener.ResponseListener; import org.xbib.netty.http.client.transport.Transport; import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.endpoint.NamedServer; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -29,16 +30,14 @@ class SecureHttp2Test { @Test void testSimpleSecureHttp2() throws Exception { HttpAddress httpAddress = HttpAddress.secureHttp2("localhost", 8143); - Server server = Server.builder() - .setJdkSslProvider() + Server server = Server.builder(NamedServer.builder(httpAddress) .setSelfCert() - .bind(httpAddress) + .singleEndpoint("/", (request, response) -> + response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + .build()) .build(); - server.getDefaultVirtualServer().addHandler("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())); server.accept(); Client client = Client.builder() - .setJdkSslProvider() .trustInsecure() .build(); AtomicInteger counter = new AtomicInteger(); @@ -72,16 +71,14 @@ class SecureHttp2Test { void testPooledSecureHttp2() throws Exception { int loop = 4096; HttpAddress httpAddress = HttpAddress.secureHttp2("localhost", 8143); - Server server = Server.builder() - .setJdkSslProvider() + Server server = Server.builder(NamedServer.builder(httpAddress) .setSelfCert() - .bind(httpAddress) + .singleEndpoint("/", (request, response) -> + response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + .build()) .build(); - server.getDefaultVirtualServer().addHandler("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())); server.accept(); Client client = Client.builder() - .setJdkSslProvider() .trustInsecure() .addPoolNode(httpAddress) .setPoolNodeConnectionLimit(2) @@ -123,17 +120,15 @@ class SecureHttp2Test { int threads = 4; int loop = 4 * 1024; HttpAddress httpAddress = HttpAddress.secureHttp2("localhost", 8143); - Server server = Server.builder() - .setJdkSslProvider() + Server server = Server.builder(NamedServer.builder(httpAddress) .setSelfCert() - .bind(httpAddress) + .singleEndpoint("/", (request, response) -> + response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()) + ) + .build()) .build(); - server.getDefaultVirtualServer().addHandler("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()) - ); server.accept(); Client client = Client.builder() - .setJdkSslProvider() .trustInsecure() .addPoolNode(httpAddress) .setPoolNodeConnectionLimit(threads) diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureStaticFileServerTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureStaticFileServerTest.java new file mode 100644 index 0000000..f847e8e --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureStaticFileServerTest.java @@ -0,0 +1,105 @@ +package org.xbib.netty.http.server.test; + +import io.netty.handler.codec.http.HttpVersion; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +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 org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.endpoint.service.NioService; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@ExtendWith(NettyHttpExtension.class) +class SecureStaticFileServerTest { + + private static final Logger logger = Logger.getLogger(SecureStaticFileServerTest.class.getName()); + + @Test + void testSecureStaticFileServerHttp1() throws Exception { + Path vartmp = Paths.get("/var/tmp/"); + HttpAddress httpAddress = HttpAddress.secureHttp1("localhost", 8143); + Server server = Server.builder(NamedServer.builder(httpAddress, "*") + .setJdkSslProvider() + .setSelfCert() + .singleEndpoint("/static", "/**", new NioService(vartmp)) + .build()) + .setChildThreadCount(8) + .build(); + server.logDiagnostics(Level.INFO); + Client client = Client.builder() + .setJdkSslProvider() + .trustInsecure() + .build(); + client.logDiagnostics(Level.INFO); + final AtomicBoolean success = new AtomicBoolean(false); + try { + Files.write(vartmp.resolve("test.txt"), "Hello Jörg".getBytes(StandardCharsets.UTF_8)); + server.accept(); + Request request = Request.get().setVersion(HttpVersion.HTTP_1_1) + .url(server.getServerConfig().getAddress().base().resolve("/static/test.txt")) + .build() + .setResponseListener(r -> { + assertEquals("Hello Jörg", r.content().toString(StandardCharsets.UTF_8)); + success.set(true); + }); + logger.log(Level.INFO, request.toString()); + client.execute(request).get(); + logger.log(Level.INFO, "request complete"); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + Files.delete(vartmp.resolve("test.txt")); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success.get()); + } + + @Test + void testSecureStaticFileServerHttp2() throws Exception { + Path vartmp = Paths.get("/var/tmp/"); + HttpAddress httpAddress = HttpAddress.secureHttp2("localhost", 8143); + Server server = Server.builder(NamedServer.builder(httpAddress, "*") + .setOpenSSLSslProvider() + .setSelfCert() + .singleEndpoint("/static", "/**", new NioService(vartmp)) + .build()) + .build(); + Client client = Client.builder() + .setOpenSSLSslProvider() + .trustInsecure() + .build(); + final AtomicBoolean success = new AtomicBoolean(false); + try { + Files.write(vartmp.resolve("test.txt"), "Hello Jörg".getBytes(StandardCharsets.UTF_8)); + server.accept(); + Request request = Request.get().setVersion(HttpVersion.valueOf("HTTP/2.0")) + .url(server.getServerConfig().getAddress().base().resolve("/static/test.txt")) + .build() + .setResponseListener(r -> { + assertEquals("Hello Jörg", r.content().toString(StandardCharsets.UTF_8)); + success.set(true); + }); + logger.log(Level.INFO, request.toString()); + client.execute(request).get(); + logger.log(Level.INFO, "request complete"); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + Files.delete(vartmp.resolve("test.txt")); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success.get()); + } +} 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 index b25766c..5ff735f 100644 --- 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 @@ -2,17 +2,19 @@ package org.xbib.netty.http.server.test; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.endpoint.NamedServer; @Disabled class ServerTest { @Test void testServer() throws Exception { - Server server = Server.builder() + NamedServer namedServer = NamedServer.builder(HttpAddress.http1("localhost", 8008), "*") + .singleEndpoint("/", (request, response) -> response.write("Hello World")) .build(); - server.getDefaultVirtualServer().addHandler("/", (request, response) -> - response.write("Hello World")); + Server server = Server.builder(namedServer).build(); try { server.accept().channel().closeFuture().sync(); } finally { diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/StaticFileServerTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/StaticFileServerTest.java index df09000..536626a 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/StaticFileServerTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/StaticFileServerTest.java @@ -2,11 +2,13 @@ package org.xbib.netty.http.server.test; import io.netty.handler.codec.http.HttpVersion; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; 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 org.xbib.netty.http.server.endpoint.NioHandler; +import org.xbib.netty.http.server.endpoint.NamedServer; +import org.xbib.netty.http.server.endpoint.service.NioService; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -19,17 +21,21 @@ import java.util.logging.Logger; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +@ExtendWith(NettyHttpExtension.class) class StaticFileServerTest { private static final Logger logger = Logger.getLogger(StaticFileServerTest.class.getName()); @Test - void testStaticFileServer() throws Exception { + void testStaticFileServerHttp1() throws Exception { Path vartmp = Paths.get("/var/tmp/"); - Server server = Server.builder() - .bind(HttpAddress.http1("localhost", 8008)) - .addHandler("/static", new NioHandler(vartmp)) + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + NamedServer namedServer = NamedServer.builder(httpAddress) + .singleEndpoint("/static", "/**", new NioService(vartmp)) .build(); + Server server = Server.builder(namedServer) + .build(); + server.logDiagnostics(Level.INFO); Client client = Client.builder() .build(); final AtomicBoolean success = new AtomicBoolean(false); @@ -50,6 +56,42 @@ class StaticFileServerTest { server.shutdownGracefully(); client.shutdownGracefully(); Files.delete(vartmp.resolve("test.txt")); + logger.log(Level.INFO, "server and client shut down"); + } + assertTrue(success.get()); + } + + @Test + void testStaticFileServerHttp2() throws Exception { + Path vartmp = Paths.get("/var/tmp/"); + HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); + NamedServer namedServer = NamedServer.builder(httpAddress) + .singleEndpoint("/static", "/**", new NioService(vartmp)) + .build(); + Server server = Server.builder(namedServer) + .build(); + server.logDiagnostics(Level.INFO); + Client client = Client.builder() + .build(); + final AtomicBoolean success = new AtomicBoolean(false); + try { + Files.write(vartmp.resolve("test.txt"), "Hello Jörg".getBytes(StandardCharsets.UTF_8)); + server.accept(); + Request request = Request.get().setVersion(HttpVersion.valueOf("HTTP/2.0")) + .url(server.getServerConfig().getAddress().base().resolve("/static/test.txt")) + .build() + .setResponseListener(r -> { + assertEquals("Hello Jörg", r.content().toString(StandardCharsets.UTF_8)); + success.set(true); + }); + logger.log(Level.INFO, request.toString()); + client.execute(request).get(); + logger.log(Level.INFO, "request complete"); + } finally { + server.shutdownGracefully(); + client.shutdownGracefully(); + Files.delete(vartmp.resolve("test.txt")); + logger.log(Level.INFO, "server and client shut down"); } assertTrue(success.get()); } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java index 04037e9..655b7c4 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ThreadLeakTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; import org.xbib.netty.http.server.Server; +import org.xbib.netty.http.server.endpoint.NamedServer; import java.io.IOException; import java.util.Set; @@ -20,11 +21,13 @@ class ThreadLeakTest { @Test void testForLeaks() throws IOException { - Server server = Server.builder() + NamedServer namedServer = NamedServer.builder() + .singleEndpoint("/", (request, response) -> + response.write("Hello World")) + .build(); + Server server = Server.builder(namedServer) .setByteBufAllocator(UnpooledByteBufAllocator.DEFAULT) .build(); - server.getDefaultVirtualServer().addHandler("/", (request, response) -> - response.write("Hello World")); try { server.accept(); } finally {