From 509b8073eb95436c4cea021431bae0a61f5ea523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=CC=88rg=20Prante?= Date: Tue, 25 Jun 2019 23:54:52 +0200 Subject: [PATCH] SSL session, gzip compression/decompression, static resource services for file/claspath urls, etags/caching/range responses --- netty-http-client/build.gradle | 2 +- .../org/xbib/netty/http/client/Client.java | 83 ++--- .../handler/http/HttpChannelInitializer.java | 4 +- .../http/client/transport/BaseTransport.java | 20 +- .../http/client/transport/Transport.java | 4 +- .../http/client/test/NettyHttpExtension.java | 11 +- .../org/xbib/netty/http/server/Server.java | 21 +- .../xbib/netty/http/server/ServerConfig.java | 30 +- .../netty/http/server/ServerResponse.java | 60 ++- .../netty/http/server/endpoint/Endpoint.java | 2 +- .../server/endpoint/EndpointResolver.java | 7 +- .../endpoint/service/ChunkedFileService.java | 51 --- .../endpoint/service/ClassLoaderService.java | 103 +++--- .../server/endpoint/service/FileService.java | 103 ++++++ .../endpoint/service/MappedFileService.java | 2 +- .../server/endpoint/service/Resource.java | 15 + .../endpoint/service/ResourceService.java | 341 +++++++++++++++++- .../server/endpoint/service/URLService.java | 32 -- .../handler/http/HttpChannelInitializer.java | 15 +- .../http2/Http2ChannelInitializer.java | 25 +- .../stream/SeekableChunkedNioStream.java | 42 +++ .../server/transport/BaseServerTransport.java | 8 +- .../server/transport/Http2ServerResponse.java | 116 +++--- .../server/transport/HttpServerRequest.java | 4 +- .../server/transport/HttpServerResponse.java | 83 +++-- .../server/test/ClassloaderServiceTest.java | 1 + .../http/server/test/CleartextHttp1Test.java | 12 +- .../http/server/test/CleartextHttp2Test.java | 12 +- .../netty/http/server/test/EndpointTest.java | 10 +- .../http/server/test/FileServiceTest.java | 98 +++++ .../http/server/test/NettyHttpExtension.java | 4 - ...ceTest.java => SecureFileServiceTest.java} | 14 +- .../http/server/test/SecureHttp1Test.java | 13 +- .../http/server/test/SecureHttp2Test.java | 12 +- .../server/test/StaticFileServiceTest.java | 165 --------- 35 files changed, 1005 insertions(+), 520 deletions(-) delete mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ChunkedFileService.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/FileService.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Resource.java delete mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/URLService.java create mode 100644 netty-http-server/src/main/java/org/xbib/netty/http/server/handler/stream/SeekableChunkedNioStream.java create mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/FileServiceTest.java rename netty-http-server/src/test/java/org/xbib/netty/http/server/test/{SecureStaticFileServiceTest.java => SecureFileServiceTest.java} (90%) delete mode 100644 netty-http-server/src/test/java/org/xbib/netty/http/server/test/StaticFileServiceTest.java diff --git a/netty-http-client/build.gradle b/netty-http-client/build.gradle index 1e669ae..10ff3ac 100644 --- a/netty-http-client/build.gradle +++ b/netty-http-client/build.gradle @@ -5,8 +5,8 @@ dependencies { implementation "io.netty:netty-transport-native-epoll:${project.property('netty.version')}" implementation "io.netty:netty-codec-http2:${project.property('netty.version')}" implementation "org.xbib:net-url:${project.property('xbib-net-url.version')}" - testImplementation "io.netty:netty-tcnative-boringssl-static:${project.property('tcnative.version')}" + testImplementation "org.bouncycastle:bcpkix-jdk15on:${project.property('bouncycastle.version')}" testImplementation "org.conscrypt:conscrypt-openjdk-uber:${project.property('conscrypt.version')}" testImplementation "com.fasterxml.jackson.core:jackson-databind:${project.property('jackson.version')}" } 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 67d3dc8..abd2699 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 @@ -211,7 +211,8 @@ public final class Client { Channel channel; if (httpAddress != null) { HttpVersion httpVersion = httpAddress.getVersion(); - SslHandlerFactory sslHandlerFactory = new SslHandlerFactory(clientConfig, httpAddress, byteBufAllocator); + SslContext sslContext = newSslContext(clientConfig, httpAddress.getVersion()); + SslHandlerFactory sslHandlerFactory = new SslHandlerFactory(sslContext, clientConfig, httpAddress, byteBufAllocator); ChannelInitializer initializer; if (httpVersion.majorVersion() == 1) { initializer = new HttpChannelInitializer(clientConfig, httpAddress, sslHandlerFactory, @@ -330,40 +331,35 @@ 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(); - List serverNames = clientConfig.getServerNamesForIdentification(); - if (serverNames.isEmpty()) { - serverNames = Collections.singletonList(peer.getHostName()); - } - SSLParameters params = engine.getSSLParameters(); - // use sslContext.newHandler(allocator, peerHost, peerPort) when using params.setEndpointIdentificationAlgorithm - params.setEndpointIdentificationAlgorithm("HTTPS"); - List sniServerNames = new ArrayList<>(); - for (String serverName : serverNames) { - sniServerNames.add(new SNIHostName(serverName)); - } - params.setServerNames(sniServerNames); - engine.setSSLParameters(params); - switch (clientConfig.getClientAuthMode()) { - case NEED: - engine.setNeedClientAuth(true); - break; - case WANT: - engine.setWantClientAuth(true); - break; - default: - break; - } - return sslHandler; - } catch (SSLException e) { - throw new IllegalArgumentException(e); + private static SslHandler newSslHandler(SslContext sslContext, + ClientConfig clientConfig, ByteBufAllocator allocator, HttpAddress httpAddress) { + InetSocketAddress peer = httpAddress.getInetSocketAddress(); + SslHandler sslHandler = sslContext.newHandler(allocator, peer.getHostName(), peer.getPort()); + SSLEngine engine = sslHandler.engine(); + List serverNames = clientConfig.getServerNamesForIdentification(); + if (serverNames.isEmpty()) { + serverNames = Collections.singletonList(peer.getHostName()); } + SSLParameters params = engine.getSSLParameters(); + // use sslContext.newHandler(allocator, peerHost, peerPort) when using params.setEndpointIdentificationAlgorithm + params.setEndpointIdentificationAlgorithm("HTTPS"); + List sniServerNames = new ArrayList<>(); + for (String serverName : serverNames) { + sniServerNames.add(new SNIHostName(serverName)); + } + params.setServerNames(sniServerNames); + engine.setSSLParameters(params); + switch (clientConfig.getClientAuthMode()) { + case NEED: + engine.setNeedClientAuth(true); + break; + case WANT: + engine.setWantClientAuth(true); + break; + default: + break; + } + return sslHandler; } private static SslContext newSslContext(ClientConfig clientConfig, HttpVersion httpVersion) throws SSLException { @@ -415,14 +411,16 @@ public final class Client { } @Override - public void channelCreated(Channel channel) { + public void channelCreated(Channel channel) throws IOException { HttpAddress httpAddress = channel.attr(pool.getAttributeKey()).get(); HttpVersion httpVersion = httpAddress.getVersion(); - SslHandlerFactory sslHandlerFactory = new SslHandlerFactory(clientConfig, httpAddress, byteBufAllocator); - Http2ChannelInitializer http2ChannelInitializer = new Http2ChannelInitializer(clientConfig, httpAddress, sslHandlerFactory); + SslContext sslContext = newSslContext(clientConfig, httpAddress.getVersion()); + SslHandlerFactory sslHandlerFactory = new SslHandlerFactory(sslContext, clientConfig, httpAddress, byteBufAllocator); + Http2ChannelInitializer http2ChannelInitializer = + new Http2ChannelInitializer(clientConfig, httpAddress, sslHandlerFactory); if (httpVersion.majorVersion() == 1) { - HttpChannelInitializer initializer = new HttpChannelInitializer(clientConfig, httpAddress, sslHandlerFactory, - http2ChannelInitializer); + HttpChannelInitializer initializer = + new HttpChannelInitializer(clientConfig, httpAddress, sslHandlerFactory, http2ChannelInitializer); initializer.initChannel(channel); } else { http2ChannelInitializer.initChannel(channel); @@ -432,20 +430,23 @@ public final class Client { public class SslHandlerFactory { + private final SslContext sslContext; + private final ClientConfig clientConfig; private final HttpAddress httpAddress; private final ByteBufAllocator allocator; - SslHandlerFactory(ClientConfig clientConfig, HttpAddress httpAddress, ByteBufAllocator allocator) { + SslHandlerFactory(SslContext sslContext, ClientConfig clientConfig, HttpAddress httpAddress, ByteBufAllocator allocator) { + this.sslContext = sslContext; this.clientConfig = clientConfig; this.httpAddress = httpAddress; this.allocator = allocator; } public SslHandler create() { - return newSslHandler(clientConfig, allocator, httpAddress); + return newSslHandler(sslContext, clientConfig, allocator, httpAddress); } } 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..c13fcb1 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; @@ -60,7 +61,8 @@ public class HttpChannelInitializer extends ChannelInitializer { private void configureEncrypted(Channel channel) { ChannelPipeline pipeline = channel.pipeline(); - pipeline.addLast(sslHandlerFactory.create()); + SslHandler sslHandler = sslHandlerFactory.create(); + pipeline.addLast("ssl-handler", sslHandler); if (clientConfig.isEnableNegotiation()) { ApplicationProtocolNegotiationHandler negotiationHandler = new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_1_1) { diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java index f5ba92f..5de7434 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/BaseTransport.java @@ -5,6 +5,7 @@ import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.cookie.Cookie; +import io.netty.handler.ssl.SslHandler; import org.xbib.net.PercentDecoder; import org.xbib.net.URL; import org.xbib.net.URLSyntaxException; @@ -13,6 +14,7 @@ import org.xbib.netty.http.common.HttpAddress; import org.xbib.netty.http.client.Request; import org.xbib.netty.http.client.retry.BackOff; +import javax.net.ssl.SSLSession; import java.io.IOException; import java.net.ConnectException; import java.nio.charset.MalformedInputException; @@ -22,6 +24,7 @@ import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.SortedMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; @@ -46,6 +49,8 @@ abstract class BaseTransport implements Transport { private final Map channels; + private SSLSession sslSession; + final Map channelFlowMap; final SortedMap requests; @@ -70,8 +75,15 @@ abstract class BaseTransport implements Transport { @Override public CompletableFuture execute(Request request, Function supplier) throws IOException { + Objects.requireNonNull(supplier); final CompletableFuture completableFuture = new CompletableFuture<>(); - request.setResponseListener(response -> completableFuture.complete(supplier.apply(response))); + request.setResponseListener(response -> { + if (response != null) { + completableFuture.complete(supplier.apply(response)); + } else { + completableFuture.cancel(true); + } + }); execute(request); return completableFuture; } @@ -179,6 +191,10 @@ abstract class BaseTransport implements Transport { requests.clear(); } + public SSLSession getSession() { + return sslSession; + } + protected abstract String getRequestKey(String channelId, Integer streamId); Channel mapChannel(Request request) throws IOException { @@ -193,6 +209,8 @@ abstract class BaseTransport implements Transport { channel = switchNextChannel(); channels.put(request, channel); } + SslHandler sslHandler = channel.pipeline().get(SslHandler.class); + sslSession = sslHandler != null ? sslHandler.engine().getSession() : null; return channel; } diff --git a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java index 9de33ca..41186ab 100644 --- a/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java +++ b/netty-http-client/src/main/java/org/xbib/netty/http/client/transport/Transport.java @@ -3,12 +3,12 @@ package org.xbib.netty.http.client.transport; import io.netty.channel.Channel; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.cookie.Cookie; -import io.netty.handler.codec.http2.Http2Exception; import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2Settings; import io.netty.util.AttributeKey; import org.xbib.netty.http.client.Request; +import javax.net.ssl.SSLSession; import java.io.IOException; import java.util.Map; import java.util.concurrent.CompletableFuture; @@ -47,5 +47,7 @@ public interface Transport { Throwable getFailure(); + SSLSession getSession(); + void close() throws IOException; } diff --git a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/NettyHttpExtension.java b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/NettyHttpExtension.java index fed5bbf..161ba62 100644 --- a/netty-http-client/src/test/java/org/xbib/netty/http/client/test/NettyHttpExtension.java +++ b/netty-http-client/src/test/java/org/xbib/netty/http/client/test/NettyHttpExtension.java @@ -1,8 +1,10 @@ package org.xbib.netty.http.client.test; +import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.extension.BeforeAllCallback; import org.junit.jupiter.api.extension.ExtensionContext; +import java.security.Security; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; import java.util.logging.Level; @@ -13,12 +15,11 @@ import java.util.logging.SimpleFormatter; public class NettyHttpExtension implements BeforeAllCallback { @Override - public void beforeAll(ExtensionContext context) throws Exception { + public void beforeAll(ExtensionContext context) { + if (Security.getProvider("BC") == null) { + Security.addProvider(new BouncyCastleProvider()); + } System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); - System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); - //System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); - //System.setProperty("io.netty.leakDetection.level", "paranoid"); - Level level = Level.INFO; System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n"); 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 1363de2..40d7f7e 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 @@ -24,9 +24,6 @@ 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; @@ -184,15 +181,6 @@ public final class Server { 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); } @@ -429,8 +417,13 @@ public final class Server { return this; } - public Builder setEnableGzip(boolean enableGzip) { - this.serverConfig.setEnableGzip(enableGzip); + public Builder setEnablCcompression(boolean enablCcompression) { + this.serverConfig.setCompression(enablCcompression); + return this; + } + + public Builder setEnableDecompression(boolean enableDecompression) { + this.serverConfig.setDecompression(enableDecompression); return this; } 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 85d2247..64ecb01 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 @@ -119,9 +119,14 @@ public class ServerConfig { WriteBufferWaterMark WRITE_BUFFER_WATER_MARK = WriteBufferWaterMark.DEFAULT; /** - * Default for gzip codec. + * Default for compression. */ - boolean ENABLE_GZIP = true; + boolean ENABLE_COMPRESSION = true; + + /** + * Default for decompression. + */ + boolean ENABLE_DECOMPRESSION = true; /** * Default HTTP/2 settings. @@ -175,7 +180,9 @@ public class ServerConfig { private WriteBufferWaterMark writeBufferWaterMark = Defaults.WRITE_BUFFER_WATER_MARK; - private boolean enableGzip = Defaults.ENABLE_GZIP; + private boolean enableCompression = Defaults.ENABLE_COMPRESSION; + + private boolean enableDecompression = Defaults.ENABLE_DECOMPRESSION; private Http2Settings http2Settings = Defaults.HTTP_2_SETTINGS; @@ -382,13 +389,22 @@ public class ServerConfig { return writeBufferWaterMark; } - public ServerConfig setEnableGzip(boolean enableGzip) { - this.enableGzip = enableGzip; + public ServerConfig setCompression(boolean enabled) { + this.enableCompression = enabled; return this; } - public boolean isEnableGzip() { - return enableGzip; + public boolean isCompressionEnabled() { + return enableCompression; + } + + public ServerConfig setDecompression(boolean enabled) { + this.enableDecompression = enabled; + return this; + } + + public boolean isDecompressionEnabled() { + return enableDecompression; } public ServerConfig setInstallHttp2Upgrade(boolean http2Upgrade) { diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java index dab36ed..61a6063 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/ServerResponse.java @@ -4,50 +4,72 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.HttpResponseStatus; -import io.netty.util.AsciiString; +import io.netty.handler.stream.ChunkedInput; import java.nio.CharBuffer; -import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; /** * HTTP server response. */ public interface ServerResponse { - void setHeader(AsciiString name, String value); + void setHeader(CharSequence name, String value); + + CharSequence getHeader(CharSequence name); ChannelHandlerContext getChannelHandlerContext(); - HttpResponseStatus getLastStatus(); + HttpResponseStatus getStatus(); - void write(HttpResponseStatus status, String contentType, ByteBuf byteBuf); + ServerResponse withStatus(HttpResponseStatus httpResponseStatus); - void write(HttpResponseStatus status, String contentType, ReadableByteChannel byteChannel); + ServerResponse withContentType(String contentType); + + ServerResponse withCharset(Charset charset); + + void write(ByteBuf byteBuf); + + void write(ChunkedInput chunkedInput); static void write(ServerResponse serverResponse, HttpResponseStatus status) { - write(serverResponse, status, status.reasonPhrase()); + write(serverResponse, status, "application/octet-stream", status.reasonPhrase()); + } + + /** + * Responses to a HEAD request. + * @param serverResponse server response + * @param status status + * @param contentType content-type as if it were for a GET request (RFC 2616) + */ + static void write(ServerResponse serverResponse, HttpResponseStatus status, String contentType) { + write(serverResponse, status, contentType, EMPTY_STRING); } static void write(ServerResponse serverResponse, String text) { - write(serverResponse, HttpResponseStatus.OK, text); + write(serverResponse, HttpResponseStatus.OK, "text/plain; charset=utf-8", text); } - static void write(ServerResponse serverResponse, HttpResponseStatus status, String text) { - write(serverResponse, status, "text/plain; charset=utf-8", text); - } - - static void write(ServerResponse serverResponse, - HttpResponseStatus status, String contentType, String text) { - serverResponse.write(status, contentType, - ByteBufUtil.writeUtf8(serverResponse.getChannelHandlerContext().alloc(), text)); + static void write(ServerResponse serverResponse, HttpResponseStatus status, String contentType, String text) { + serverResponse.withStatus(status) + .withContentType(contentType) + .withCharset(StandardCharsets.UTF_8). + write(ByteBufUtil.writeUtf8(serverResponse.getChannelHandlerContext().alloc(), text)); } static void write(ServerResponse serverResponse, HttpResponseStatus status, String contentType, String text, Charset charset) { - serverResponse.write(status, contentType, - ByteBufUtil.encodeString(serverResponse.getChannelHandlerContext().alloc(), - CharBuffer.allocate(text.length()).append(text), charset)); + write(serverResponse, status, contentType, CharBuffer.allocate(text.length()).append(text), charset); } + static void write(ServerResponse serverResponse, HttpResponseStatus status, String contentType, + CharBuffer charBuffer, Charset charset) { + serverResponse.withStatus(status) + .withContentType(contentType) + .withCharset(charset) + .write(ByteBufUtil.encodeString(serverResponse.getChannelHandlerContext().alloc(), charBuffer, charset)); + } + + String EMPTY_STRING = ""; } 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 358c5ad..dfe4762 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 @@ -79,7 +79,7 @@ public class Endpoint { serverRequest.setContext(pathMatcher.tokenizePath(getPrefix())); for (Service service : filters) { service.handle(serverRequest, serverResponse); - if (serverResponse.getLastStatus() != null) { + if (serverResponse.getStatus() != null) { break; } } 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 index 73550ba..5e56a67 100644 --- 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 @@ -61,14 +61,14 @@ public class EndpointResolver { for (Endpoint endpoint : matchingEndpoints) { endpoint.resolveUriTemplate(serverRequest); endpoint.executeFilters(serverRequest, serverResponse); - if (serverResponse.getLastStatus() != null) { + if (serverResponse.getStatus() != null) { break; } } if (endpointDispatcher != null) { for (Endpoint endpoint : matchingEndpoints) { endpointDispatcher.dispatch(endpoint, serverRequest, serverResponse); - if (serverResponse.getLastStatus() != null) { + if (serverResponse.getStatus() != null) { break; } } @@ -86,7 +86,8 @@ public class EndpointResolver { .addMethod("GET") .addMethod("HEAD") .addFilter((req, resp) -> { - ServerResponse.write(resp, HttpResponseStatus.NOT_FOUND,"No endpoint configured"); + ServerResponse.write(resp, HttpResponseStatus.NOT_FOUND, + "application/octet-stream","no endpoint configured"); }).build(); } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ChunkedFileService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ChunkedFileService.java deleted file mode 100644 index 4274a83..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ChunkedFileService.java +++ /dev/null @@ -1,51 +0,0 @@ -package org.xbib.netty.http.server.endpoint.service; - -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.util.MimeTypeUtils; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.channels.Channels; -import java.nio.channels.ReadableByteChannel; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.logging.Level; -import java.util.logging.Logger; - -public class ChunkedFileService implements Service { - - private static final Logger logger = Logger.getLogger(ChunkedFileService.class.getName()); - - private final Path prefix; - - public ChunkedFileService(Path prefix) { - this.prefix = prefix; - if (!Files.exists(prefix)) { - throw new IllegalArgumentException("prefix: " + prefix + " (does not exist)"); - } - if (!Files.exists(prefix) || !Files.isDirectory(prefix)) { - throw new IllegalArgumentException("prefix: " + prefix + " (not a directory)"); - } - } - - @Override - public void handle(ServerRequest serverRequest, ServerResponse serverResponse) { - String requestPath = serverRequest.getEffectiveRequestPath().substring(1); // always starts with '/' - Path path = prefix.resolve(requestPath); - if (Files.isReadable(path)) { - try (InputStream inputStream = Files.newInputStream(path); - ReadableByteChannel byteChannel = Channels.newChannel(inputStream)) { - String contentType = MimeTypeUtils.guessFromPath(requestPath, false); - serverResponse.write(HttpResponseStatus.OK, contentType, byteChannel); - } catch (IOException e) { - logger.log(Level.SEVERE, e.getMessage(), e); - ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); - } - } else { - logger.log(Level.WARNING, "failed to access path " + path + " prefix = " + prefix + " requestPath=" + requestPath); - ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); - } - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ClassLoaderService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ClassLoaderService.java index c92a361..2ec141e 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ClassLoaderService.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ClassLoaderService.java @@ -1,30 +1,16 @@ 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.ServerRequest; import org.xbib.netty.http.server.ServerResponse; -import org.xbib.netty.http.server.util.MimeTypeUtils; import java.io.IOException; -import java.io.InputStream; -import java.net.URISyntaxException; import java.net.URL; -import java.nio.MappedByteBuffer; -import java.nio.channels.Channels; -import java.nio.channels.FileChannel; -import java.nio.channels.ReadableByteChannel; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.logging.Level; -import java.util.logging.Logger; +import java.net.URLConnection; +import java.time.Instant; -public class ClassLoaderService implements Service { +public class ClassLoaderService extends ResourceService { - private static final Logger logger = Logger.getLogger(ClassLoaderService.class.getName()); - - private Class clazz; + private final Class clazz; private final String prefix; @@ -34,40 +20,61 @@ public class ClassLoaderService implements Service { } @Override - public void handle(ServerRequest serverRequest, ServerResponse serverResponse) { - String requestPath = serverRequest.getEffectiveRequestPath().substring(1); - String contentType = MimeTypeUtils.guessFromPath(requestPath, false); - URL url = clazz.getResource(prefix + "/" + requestPath); - if (url != null) { - if ("file".equals(url.getProtocol())) { - doMappedResource(url, contentType, serverResponse); - } else { - doResource(url, contentType, serverResponse); - } - } else { - ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); - } + protected Resource createResource(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + return new ClassLoaderResource(serverRequest); } - private void doMappedResource(URL url, String contentType, ServerResponse serverResponse) { - try { - 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); - serverResponse.write(HttpResponseStatus.OK, contentType, byteBuf); - } catch (URISyntaxException | IOException e) { - logger.log(Level.SEVERE, e.getMessage(), e); - ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); - } + @Override + protected boolean isETagResponseEnabled() { + return true; } - private void doResource(URL url, String contentType, ServerResponse serverResponse) { - try (InputStream inputStream = url.openStream(); - ReadableByteChannel byteChannel = Channels.newChannel(inputStream)) { - serverResponse.write(HttpResponseStatus.OK, contentType, byteChannel); - } catch (IOException e) { - logger.log(Level.SEVERE, e.getMessage(), e); - ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); + @Override + protected boolean isCacheResponseEnabled() { + return true; + } + + @Override + protected boolean isRangeResponseEnabled() { + return true; + } + + class ClassLoaderResource implements Resource { + + private final String resourcePath; + + private final URL url; + + private final Instant lastModified; + + private final long length; + + ClassLoaderResource(ServerRequest serverRequest) throws IOException { + this.resourcePath = serverRequest.getEffectiveRequestPath().substring(1); + this.url = clazz.getResource(prefix + "/" + resourcePath); + URLConnection urlConnection = url.openConnection(); + this.lastModified = Instant.ofEpochMilli(urlConnection.getLastModified()); + this.length = urlConnection.getContentLength(); + } + + @Override + public String getResourcePath() { + return resourcePath; + } + + @Override + public URL getURL() { + return url; + } + + @Override + public Instant getLastModified() { + return lastModified; + } + + @Override + public long getLength() { + return length; } } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/FileService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/FileService.java new file mode 100644 index 0000000..d238b3e --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/FileService.java @@ -0,0 +1,103 @@ +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; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Instant; + +public class FileService extends ResourceService { + + private final Path prefix; + + public FileService(Path prefix) { + this.prefix = prefix; + if (!Files.exists(prefix)) { + throw new IllegalArgumentException("prefix: " + prefix + " (does not exist)"); + } + if (!Files.isDirectory(prefix)) { + throw new IllegalArgumentException("prefix: " + prefix + " (not a directory)"); + } + } + + @Override + protected Resource createResource(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { + return new ChunkedFileResource(serverRequest); + } + + @Override + protected boolean isETagResponseEnabled() { + return true; + } + + @Override + protected boolean isCacheResponseEnabled() { + return true; + } + + @Override + protected boolean isRangeResponseEnabled() { + return true; + } + + class ChunkedFileResource implements Resource { + + private final String resourcePath; + + private final URL url; + + private final Instant lastModified; + + private final long length; + + ChunkedFileResource(ServerRequest serverRequest) throws IOException { + this.resourcePath = serverRequest.getEffectiveRequestPath().substring(1); + Path path = prefix.resolve(resourcePath); + this.url = path.toUri().toURL(); + this.lastModified = Files.getLastModifiedTime(path).toInstant(); + this.length = Files.size(path); + } + + @Override + public String getResourcePath() { + return resourcePath; + } + + @Override + public URL getURL() { + return url; + } + + @Override + public Instant getLastModified() { + return lastModified; + } + + @Override + public long getLength() { + return length; + } + } + + /*@Override + public void handle(ServerRequest serverRequest, ServerResponse serverResponse) { + String requestPath = serverRequest.getEffectiveRequestPath().substring(1); // always starts with '/' + Path path = prefix.resolve(requestPath); + if (Files.isReadable(path)) { + try (InputStream inputStream = Files.newInputStream(path); + ReadableByteChannel byteChannel = Channels.newChannel(inputStream)) { + String contentType = MimeTypeUtils.guessFromPath(requestPath, false); + serverResponse.write(HttpResponseStatus.OK, contentType, new ChunkedNioStream(byteChannel)); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); + } + } else { + logger.log(Level.WARNING, "failed to access path " + path + " prefix = " + prefix + " requestPath=" + requestPath); + ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); + } + }*/ +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MappedFileService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MappedFileService.java index 30d1c04..2365d80 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MappedFileService.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/MappedFileService.java @@ -40,7 +40,7 @@ public class MappedFileService implements Service { MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size()); ByteBuf byteBuf = Unpooled.wrappedBuffer(mappedByteBuffer); String contentType = MimeTypeUtils.guessFromPath(requestPath, false); - serverResponse.write(HttpResponseStatus.OK, contentType, byteBuf); + serverResponse.withStatus(HttpResponseStatus.OK).withContentType(contentType).write(byteBuf); } } else { logger.log(Level.WARNING, "failed to access path " + path + " prefix = " + prefix + " requestPath=" + requestPath); diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Resource.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Resource.java new file mode 100644 index 0000000..c2c102e --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/Resource.java @@ -0,0 +1,15 @@ +package org.xbib.netty.http.server.endpoint.service; + +import java.net.URL; +import java.time.Instant; + +public interface Resource { + + String getResourcePath(); + + URL getURL(); + + Instant getLastModified(); + + long getLength(); +} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java index 111fa9d..d3da4f9 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/ResourceService.java @@ -1,21 +1,352 @@ package org.xbib.netty.http.server.endpoint.service; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http.HttpHeaders; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.stream.ChunkedNioStream; 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; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; public abstract class ResourceService implements Service { + private static final Logger logger = Logger.getLogger(ResourceService.class.getName()); + @Override public void handle(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { - String resourcePath = getResourcePath(serverRequest); - handleResource(resourcePath, serverRequest, serverResponse); + handleResource(serverRequest, serverResponse, createResource(serverRequest, serverResponse)); } - protected abstract void handleResource(String resourcePath, ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; + protected abstract Resource createResource(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException; - protected String getResourcePath(ServerRequest serverRequest) { - return serverRequest.getEffectiveRequestPath().substring(1); + protected abstract boolean isETagResponseEnabled(); + + protected abstract boolean isCacheResponseEnabled(); + + protected abstract boolean isRangeResponseEnabled(); + + protected void handleResource(ServerRequest serverRequest, ServerResponse serverResponse, Resource resource) { + HttpHeaders headers = serverRequest.getRequest().headers(); + String contentType = MimeTypeUtils.guessFromPath(resource.getResourcePath(), false); + long maxAgeSeconds = 24 * 3600; + long expirationMillis = System.currentTimeMillis() + 1000 * maxAgeSeconds; + if (isCacheResponseEnabled()) { + serverResponse.setHeader(HttpHeaderNames.EXPIRES, formatMillis(expirationMillis)); + serverResponse.setHeader(HttpHeaderNames.CACHE_CONTROL, "public, max-age=" + maxAgeSeconds); + } + boolean sent = false; + if (isETagResponseEnabled()) { + Instant lastModifiedInstant = resource.getLastModified(); + String eTag = resource.getResourcePath().hashCode() + "/" + lastModifiedInstant.toEpochMilli() + "/" + resource.getLength(); + Instant ifUnmodifiedSinceInstant = parseDate(headers.get(HttpHeaderNames.IF_UNMODIFIED_SINCE)); + if (ifUnmodifiedSinceInstant != null && + ifUnmodifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) { + ServerResponse.write(serverResponse, HttpResponseStatus.PRECONDITION_FAILED); + return; + } + String ifMatch = headers.get(HttpHeaderNames.IF_MATCH); + if (ifMatch != null && !matches(ifMatch, eTag)) { + ServerResponse.write(serverResponse, HttpResponseStatus.PRECONDITION_FAILED); + return; + } + String ifNoneMatch = headers.get(HttpHeaderNames.IF_NONE_MATCH); + if (ifNoneMatch != null && matches(ifNoneMatch, eTag)) { + serverResponse.setHeader(HttpHeaderNames.ETAG, eTag); + serverResponse.setHeader(HttpHeaderNames.EXPIRES, formatMillis(expirationMillis)); + ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED); + return; + } + Instant ifModifiedSinceInstant = parseDate(headers.get(HttpHeaderNames.IF_MODIFIED_SINCE)); + if (ifModifiedSinceInstant != null && + ifModifiedSinceInstant.plusMillis(1000L).isAfter(lastModifiedInstant)) { + serverResponse.setHeader(HttpHeaderNames.ETAG, eTag); + serverResponse.setHeader(HttpHeaderNames.EXPIRES, formatMillis(expirationMillis)); + ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED); + return; + } + serverResponse.setHeader(HttpHeaderNames.ETAG, eTag); + serverResponse.setHeader(HttpHeaderNames.LAST_MODIFIED, formatInstant(lastModifiedInstant)); + if (isRangeResponseEnabled()) { + performRangeResponse(serverRequest, serverResponse, resource, contentType, eTag, headers); + sent = true; + } + } + if (!sent) { + serverResponse.setHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(resource.getLength())); + send(resource.getURL(), HttpResponseStatus.OK, contentType, serverRequest, serverResponse); + } + } + + protected void performRangeResponse(ServerRequest serverRequest, ServerResponse serverResponse, + Resource resource, + String contentType, String eTag, + HttpHeaders headers) { + long length = resource.getLength(); + serverResponse.setHeader(HttpHeaderNames.ACCEPT_RANGES, "bytes"); + Range full = new Range(0, length - 1, length); + List ranges = new ArrayList<>(); + String range = headers.get(HttpHeaderNames.RANGE); + if (range != null) { + if (!range.matches("^bytes=\\d*-\\d*(,\\d*-\\d*)*$")) { + serverResponse.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes */" + length); + ServerResponse.write(serverResponse, HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE); + return; + } + String ifRange = headers.get(HttpHeaderNames.IF_RANGE); + if (ifRange != null && !ifRange.equals(eTag)) { + try { + Instant ifRangeTime = parseDate(ifRange); + if (ifRangeTime != null && ifRangeTime.plusMillis(1000).isBefore(resource.getLastModified())) { + ranges.add(full); + } + } catch (IllegalArgumentException ignore) { + ranges.add(full); + } + } + if (ranges.isEmpty()) { + for (String part : range.substring(6).split(",")) { + long start = sublong(part, 0, part.indexOf('-')); + long end = sublong(part, part.indexOf('-') + 1, part.length()); + if (start == -1L) { + start = length - end; + end = length - 1; + } else if (end == -1L || end > length - 1) { + end = length - 1; + } + if (start > end) { + serverResponse.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes */" + length); + ServerResponse.write(serverResponse, HttpResponseStatus.REQUESTED_RANGE_NOT_SATISFIABLE); + return; + } + ranges.add(new Range(start, end, length)); + } + } + } + if (ranges.isEmpty() || ranges.get(0) == full) { + serverResponse.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + full.start + '-' + full.end + '/' + full.total); + serverResponse.setHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(full.length)); + send(resource.getURL(), HttpResponseStatus.OK, contentType, serverRequest, serverResponse, full.start, full.length); + } else if (ranges.size() == 1) { + Range r = ranges.get(0); + serverResponse.setHeader(HttpHeaderNames.CONTENT_RANGE, "bytes " + r.start + '-' + r.end + '/' + r.total); + serverResponse.setHeader(HttpHeaderNames.CONTENT_LENGTH, Long.toString(r.length)); + send(resource.getURL(), HttpResponseStatus.PARTIAL_CONTENT, contentType, serverRequest, serverResponse, r.start, r.length); + } else { + serverResponse.setHeader(HttpHeaderNames.CONTENT_TYPE, "multipart/byteranges; boundary=MULTIPART_BOUNDARY"); + StringBuilder sb = new StringBuilder(); + for (Range r : ranges) { + try { + sb.append('\n') + .append("--MULTIPART_BOUNDARY").append('\n') + .append("content-type: ").append(contentType).append('\n') + .append("content-range: bytes ").append(r.start).append('-').append(r.end).append('/').append(r.total).append('\n') + .append(StandardCharsets.ISO_8859_1.decode(readBuffer(resource.getURL(), r.start, r.length))).append('\n') + .append("--MULTIPART_BOUNDARY--").append('\n'); + } catch (URISyntaxException | IOException e) { + logger.log(Level.FINEST, e.getMessage(), e); + } + } + ServerResponse.write(serverResponse, HttpResponseStatus.OK, contentType, CharBuffer.wrap(sb), StandardCharsets.ISO_8859_1); + } + } + + private static boolean matches(String matchHeader, String toMatch) { + String[] matchValues = matchHeader.split("\\s*,\\s*"); + Arrays.sort(matchValues); + return Arrays.binarySearch(matchValues, toMatch) > -1 || Arrays.binarySearch(matchValues, "*") > -1; + } + + private static String formatInstant(Instant instant) { + return DateTimeFormatter.RFC_1123_DATE_TIME + .format(ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)); + } + + private static String formatMillis(long millis) { + return formatInstant(Instant.ofEpochMilli(millis)); + } + + private static String formatSeconds(long seconds) { + return formatInstant(Instant.now().plusSeconds(seconds)); + } + + private static final String RFC1036_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss zzz"; + + private static final String ASCIITIME_PATTERN = "EEE MMM d HH:mm:ss yyyyy"; + + private static final DateTimeFormatter[] dateTimeFormatters = { + DateTimeFormatter.RFC_1123_DATE_TIME, + DateTimeFormatter.ofPattern(RFC1036_PATTERN), + DateTimeFormatter.ofPattern(ASCIITIME_PATTERN) + }; + + private static Instant parseDate(String date) { + if (date == null) { + return null; + } + int semicolonIndex = date.indexOf(';'); + String trimmedDate = semicolonIndex >= 0 ? date.substring(0, semicolonIndex) : date; + // RFC 2616 allows RFC 1123, RFC 1036, ASCII time + for (DateTimeFormatter formatter : dateTimeFormatters) { + try { + return Instant.from(formatter.withZone(ZoneId.of("UTC")).parse(trimmedDate)); + } catch (DateTimeParseException e) { + logger.log(Level.FINEST, e.getMessage()); + } + } + return null; + } + + private static long sublong(String value, int beginIndex, int endIndex) { + String substring = value.substring(beginIndex, endIndex); + return substring.length() > 0 ? Long.parseLong(substring) : -1; + } + + protected void send(URL url, HttpResponseStatus httpResponseStatus, String contentType, + ServerRequest serverRequest, ServerResponse serverResponse) { + if (serverRequest.getRequest().method() == HttpMethod.HEAD) { + ServerResponse.write(serverResponse, HttpResponseStatus.OK, contentType); + } else { + if ("file".equals(url.getProtocol())) { + try { + send((FileChannel) Files.newByteChannel(Paths.get(url.toURI())), + httpResponseStatus, contentType, serverResponse); + } catch (URISyntaxException | IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); + } + } else { + try (InputStream inputStream = url.openStream()) { + send(inputStream, httpResponseStatus, contentType, serverResponse); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); + } + } + } + } + + protected void send(URL url, HttpResponseStatus httpResponseStatus, String contentType, + ServerRequest serverRequest, ServerResponse serverResponse, long offset, long size) { + if (serverRequest.getRequest().method() == HttpMethod.HEAD) { + ServerResponse.write(serverResponse, HttpResponseStatus.OK, contentType); + } else { + if ("file".equals(url.getProtocol())) { + try { + send((FileChannel) Files.newByteChannel(Paths.get(url.toURI())), httpResponseStatus, + contentType, serverResponse, offset, size); + } catch (URISyntaxException | IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); + } + } else { + try (InputStream inputStream = url.openStream()) { + send(inputStream, httpResponseStatus, contentType, serverResponse, offset, size); + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + ServerResponse.write(serverResponse, HttpResponseStatus.NOT_FOUND); + } + } + } + } + + protected void send(FileChannel fileChannel, HttpResponseStatus httpResponseStatus, String contentType, + ServerResponse serverResponse) throws IOException { + send(fileChannel, httpResponseStatus, contentType, serverResponse, 0L, fileChannel.size()); + } + + protected void send(FileChannel fileChannel, HttpResponseStatus httpResponseStatus, String contentType, + ServerResponse serverResponse, long offset, long size) throws IOException { + MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, offset, size); + serverResponse.withStatus(httpResponseStatus) + .withContentType(contentType) + .write(Unpooled.wrappedBuffer(mappedByteBuffer)); + } + + protected void send(InputStream inputStream, HttpResponseStatus httpResponseStatus, String contentType, + ServerResponse serverResponse) throws IOException { + try (ReadableByteChannel channel = Channels.newChannel(inputStream)) { + serverResponse.withStatus(httpResponseStatus) + .withContentType(contentType) + .write(new ChunkedNioStream(channel)); + } + } + + protected void send(InputStream inputStream, HttpResponseStatus httpResponseStatus, String contentType, + ServerResponse serverResponse, long offset, long size) throws IOException { + serverResponse.withStatus(httpResponseStatus) + .withContentType(contentType) + .write(Unpooled.wrappedBuffer(readBuffer(inputStream, offset, size))); + } + + protected static ByteBuffer readBuffer(URL url, long offset, long size) throws IOException, URISyntaxException { + if ("file".equals(url.getProtocol())) { + try (SeekableByteChannel channel = Files.newByteChannel(Paths.get(url.toURI()))) { + return readBuffer(channel, offset, size); + } + } else { + try (InputStream inputStream = url.openStream()) { + return readBuffer(inputStream, offset, size); + } + } + } + + protected static ByteBuffer readBuffer(InputStream inputStream, long offset, long size) throws IOException { + long n = inputStream.skip(offset); + return readBuffer(Channels.newChannel(inputStream), size); + } + + protected static ByteBuffer readBuffer(SeekableByteChannel channel, long offset, long size) throws IOException { + channel.position(offset); + return readBuffer(channel, size); + } + + protected static ByteBuffer readBuffer(ReadableByteChannel channel, long size) throws IOException { + ByteBuffer buf = ByteBuffer.allocate((int) size); + buf.rewind(); + channel.read(buf); + buf.flip(); + return buf; + } + + class Range { + long start; + long end; + long length; + long total; + + Range(long start, long end, long total) { + this.start = start; + this.end = end; + this.length = end - start + 1; + this.total = total; + } } } diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/URLService.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/URLService.java deleted file mode 100644 index 2981878..0000000 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/endpoint/service/URLService.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.xbib.netty.http.server.endpoint.service; - -import io.netty.handler.codec.http.HttpResponseStatus; -import org.xbib.netty.http.server.ServerRequest; -import org.xbib.netty.http.server.ServerResponse; - -import java.io.IOException; -import java.net.URL; - -public abstract class URLService extends ResourceService { - - @Override - protected void handleResource(String resourcePath, ServerRequest serverRequest, ServerResponse serverResponse) throws IOException { - URL url = getResourceURL(resourcePath); - if (url != null) { - streamResource(url, serverRequest, serverResponse); - } - } - - protected abstract URL getResourceURL(String resourcePath); - - protected void streamResource(URL resourceUrl, ServerRequest serverRequest, - ServerResponse serverResponse) throws IOException { - /*long lastModified = resourceUrl.openConnection().getLastModified(); - serverResponse.addEtag(serverRequest, lastModified); - if (serverResponse.getLastStatus() == HttpResponseStatus.NOT_MODIFIED) { - ServerResponse.write(serverResponse, HttpResponseStatus.NOT_MODIFIED); - } else { - sendResource(resourceUrl, serverRequest, serverResponse); - }*/ - } -} diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java index e1137b3..5679aa3 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http/HttpChannelInitializer.java @@ -8,6 +8,7 @@ import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpContentCompressor; import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpResponseStatus; @@ -40,7 +41,7 @@ public class HttpChannelInitializer extends ChannelInitializer { private final HttpHandler httpHandler; - private final DomainNameMapping domainNameMapping; + private final SniHandler sniHandler; public HttpChannelInitializer(Server server, HttpAddress httpAddress, @@ -48,8 +49,8 @@ public class HttpChannelInitializer extends ChannelInitializer { this.server = server; this.serverConfig = server.getServerConfig(); this.httpAddress = httpAddress; - this.domainNameMapping = domainNameMapping; this.httpHandler = new HttpHandler(server); + this.sniHandler = domainNameMapping != null ? new SniHandler(domainNameMapping) : null; } @Override @@ -70,8 +71,9 @@ public class HttpChannelInitializer extends ChannelInitializer { } private void configureEncrypted(SocketChannel channel) { - ChannelPipeline pipeline = channel.pipeline(); - pipeline.addLast(new SniHandler(domainNameMapping)); + if (sniHandler != null) { + channel.pipeline().addLast("sni-handker", sniHandler); + } configureCleartext(channel); } @@ -80,7 +82,10 @@ public class HttpChannelInitializer extends ChannelInitializer { pipeline.addLast("http-server-codec", new HttpServerCodec(serverConfig.getMaxInitialLineLength(), serverConfig.getMaxHeadersSize(), serverConfig.getMaxChunkSize())); - if (serverConfig.isEnableGzip()) { + if (serverConfig.isCompressionEnabled()) { + pipeline.addLast("http-server-compressor", new HttpContentCompressor()); + } + if (serverConfig.isDecompressionEnabled()) { pipeline.addLast("http-server-decompressor", new HttpContentDecompressor()); } HttpObjectAggregator httpObjectAggregator = new HttpObjectAggregator(serverConfig.getMaxContentLength(), diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java index 0c0bf1d..b8a5f07 100644 --- a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/http2/Http2ChannelInitializer.java @@ -7,6 +7,8 @@ import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpContentCompressor; +import io.netty.handler.codec.http.HttpContentDecompressor; import io.netty.handler.codec.http.HttpObjectAggregator; import io.netty.handler.codec.http.HttpServerCodec; import io.netty.handler.codec.http.HttpServerUpgradeHandler; @@ -44,7 +46,7 @@ public class Http2ChannelInitializer extends ChannelInitializer { private final HttpAddress httpAddress; - private final DomainNameMapping domainNameMapping; + private final SniHandler sniHandler; public Http2ChannelInitializer(Server server, HttpAddress httpAddress, @@ -52,7 +54,7 @@ public class Http2ChannelInitializer extends ChannelInitializer { this.server = server; this.serverConfig = server.getServerConfig(); this.httpAddress = httpAddress; - this.domainNameMapping = domainNameMapping; + this.sniHandler = domainNameMapping != null ? new SniHandler(domainNameMapping) : null; } @Override @@ -73,7 +75,9 @@ public class Http2ChannelInitializer extends ChannelInitializer { } private void configureEncrypted(Channel channel) { - channel.pipeline().addLast(new SniHandler(domainNameMapping)); + if (sniHandler != null) { + channel.pipeline().addLast("sni-handler", sniHandler); + } configureCleartext(channel); } @@ -87,7 +91,13 @@ public class Http2ChannelInitializer extends ChannelInitializer { ChannelPipeline pipeline = channel.pipeline(); pipeline.addLast("multiplex-server-frame-converter", new Http2StreamFrameToHttpObjectCodec(true)); - pipeline.addLast("multiplex-server-chunk-aggregator", + if (serverConfig.isCompressionEnabled()) { + pipeline.addLast("multiplex-server-compressor", new HttpContentCompressor()); + } + if (serverConfig.isDecompressionEnabled()) { + pipeline.addLast("multiplex-server-decompressor", new HttpContentDecompressor()); + } + pipeline.addLast("multiplex-server-object-aggregator", new HttpObjectAggregator(serverConfig.getMaxContentLength())); pipeline.addLast("multiplex-server-chunked-write", new ChunkedWriteHandler()); @@ -115,6 +125,13 @@ public class Http2ChannelInitializer extends ChannelInitializer { p.addLast("server-messages", new ServerMessages()); } + public SslContext getSessionContext() { + if (httpAddress.isSecure()) { + return sniHandler.sslContext(); + } + return null; + } + class ServerRequestHandler extends SimpleChannelInboundHandler { @Override diff --git a/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/stream/SeekableChunkedNioStream.java b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/stream/SeekableChunkedNioStream.java new file mode 100644 index 0000000..d93c908 --- /dev/null +++ b/netty-http-server/src/main/java/org/xbib/netty/http/server/handler/stream/SeekableChunkedNioStream.java @@ -0,0 +1,42 @@ + +package org.xbib.netty.http.server.handler.stream; + +import io.netty.handler.stream.ChunkedNioStream; + +import java.io.IOException; +import java.nio.channels.SeekableByteChannel; + +/** + * A {@link ChunkedNioStream} that fetches data from a {@link SeekableByteChannel} + * chunk by chunk. Please note that the {@link SeekableByteChannel} must + * operate in blocking mode. Non-blocking mode channels are not supported. + */ +public class SeekableChunkedNioStream extends ChunkedNioStream { + + /** + * Creates a new instance that fetches data from the specified channel. + */ + public SeekableChunkedNioStream(SeekableByteChannel in) { + super(in); + } + + /** + * Creates a new instance that fetches data from the specified channel. + * + * @param chunkSize the number of bytes to fetch on each call + */ + public SeekableChunkedNioStream(SeekableByteChannel in, int chunkSize) { + super(in, chunkSize); + } + + /** + * Creates a new instance that fetches data from the specified channel. + * + * @param position the position in the byte channel + * @param chunkSize the number of bytes to fetch on each call + */ + public SeekableChunkedNioStream(SeekableByteChannel in, long position, int chunkSize) throws IOException { + super(in, chunkSize); + in.position(position); + } +} 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 3690069..5b97ca1 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 @@ -48,13 +48,14 @@ abstract class BaseServerTransport implements ServerTransport { if (version.majorVersion() == 1 || version.majorVersion() == 2) { if (!reqHeaders.contains(HttpHeaderNames.HOST)) { // RFC2616#14.23: missing Host header gets 400 - ServerResponse.write(serverResponse, HttpResponseStatus.BAD_REQUEST, "missing 'Host' header"); + ServerResponse.write(serverResponse, + HttpResponseStatus.BAD_REQUEST, "application/octet-stream", "missing 'Host' header"); return false; } // return a continue response before reading body String expect = reqHeaders.get(HttpHeaderNames.EXPECT); if (expect != null) { - if (expect.equalsIgnoreCase("100-continue")) { + if ("100-continue".equalsIgnoreCase(expect)) { //ServerResponse tempResp = new ServerResponse(serverResponse); //tempResp.sendHeaders(100); } else { @@ -64,7 +65,8 @@ abstract class BaseServerTransport implements ServerTransport { } } } else { - ServerResponse.write(serverResponse, HttpResponseStatus.BAD_REQUEST, "unsupported HTTP version: " + version); + ServerResponse.write(serverResponse, HttpResponseStatus.BAD_REQUEST, + "application/octet-stream", "unsupported HTTP version: " + version); return false; } return true; 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 0acf665..80eafc6 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 @@ -16,13 +16,11 @@ import io.netty.handler.codec.http2.Http2Headers; import io.netty.handler.codec.http2.Http2HeadersFrame; import io.netty.handler.codec.http2.HttpConversionUtil; import io.netty.handler.stream.ChunkedInput; -import io.netty.handler.stream.ChunkedNioStream; -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.channels.ReadableByteChannel; +import java.nio.charset.Charset; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -51,45 +49,70 @@ public class Http2ServerResponse implements ServerResponse { } @Override - public void setHeader(AsciiString name, String value) { + public void setHeader(CharSequence name, String value) { headers.set(name, value); } + @Override + public CharSequence getHeader(CharSequence name) { + return headers.get(name); + } + @Override public ChannelHandlerContext getChannelHandlerContext() { return ctx; } @Override - public HttpResponseStatus getLastStatus() { + public HttpResponseStatus getStatus() { return httpResponseStatus; } @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()); + public ServerResponse withStatus(HttpResponseStatus httpResponseStatus) { + this.httpResponseStatus = httpResponseStatus; + return this; + } + + @Override + public ServerResponse withContentType(String contentType) { + headers.remove(HttpHeaderNames.CONTENT_TYPE); + headers.add(HttpHeaderNames.CONTENT_TYPE, contentType); + return this; + } + + @Override + public ServerResponse withCharset(Charset charset) { + CharSequence contentType = headers.get(HttpHeaderNames.CONTENT_TYPE); + if (contentType != null) { + headers.remove(HttpHeaderNames.CONTENT_TYPE); + headers.add(HttpHeaderNames.CONTENT_TYPE, contentType + "; charset=" + charset.name()); } + return this; + } + + @Override + public void write(ByteBuf byteBuf) { + Objects.requireNonNull(byteBuf); + if (httpResponseStatus == null) { + httpResponseStatus = HttpResponseStatus.OK; + } + CharSequence contentType = headers.get(HttpHeaderNames.CONTENT_TYPE); + if (contentType == null) { + headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM); + } + if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH) && !headers.contains(HttpHeaderNames.TRANSFER_ENCODING)) { + headers.add(HttpHeaderNames.CONTENT_LENGTH, Long.toString(byteBuf.readableBytes())); + } + 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()); + if (serverRequest != null) { Integer streamId = serverRequest.streamId(); if (streamId != null) { @@ -97,16 +120,13 @@ public class Http2ServerResponse implements ServerResponse { } } if (ctx.channel().isWritable()) { - Http2Headers http2Headers = new DefaultHttp2Headers().status(status.codeAsText()).add(headers); - Http2HeadersFrame http2HeadersFrame = new DefaultHttp2HeadersFrame(http2Headers, byteBuf == null); + Http2Headers http2Headers = new DefaultHttp2Headers().status(httpResponseStatus.codeAsText()).add(headers); + Http2HeadersFrame http2HeadersFrame = new DefaultHttp2HeadersFrame(http2Headers, false); logger.log(Level.FINEST, http2HeadersFrame::toString); ctx.channel().write(http2HeadersFrame); - this.httpResponseStatus = status; - if (byteBuf != null) { - Http2DataFrame http2DataFrame = new DefaultHttp2DataFrame(byteBuf, true); - logger.log(Level.FINEST, http2DataFrame::toString); - ctx.channel().write(http2DataFrame); - } + Http2DataFrame http2DataFrame = new DefaultHttp2DataFrame(byteBuf, true); + logger.log(Level.FINEST, http2DataFrame::toString); + ctx.channel().write(http2DataFrame); ctx.channel().flush(); } } @@ -114,16 +134,17 @@ public class Http2ServerResponse implements ServerResponse { /** * Chunked response from a readable byte channel. * - * @param status status - * @param contentType content type - * @param byteChannel byte channel + * @param chunkedInput chunked input */ @Override - public void write(HttpResponseStatus status, String contentType, ReadableByteChannel byteChannel) { - CharSequence s = headers.get(HttpHeaderNames.CONTENT_TYPE); - if (s == null) { - s = contentType != null ? contentType : HttpHeaderValues.APPLICATION_OCTET_STREAM; - headers.add(HttpHeaderNames.CONTENT_TYPE, s); + public void write(ChunkedInput chunkedInput) { + Objects.requireNonNull(chunkedInput); + if (httpResponseStatus == null) { + httpResponseStatus = HttpResponseStatus.OK; + } + CharSequence contentType = headers.get(HttpHeaderNames.CONTENT_TYPE); + if (contentType == null) { + headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM); } headers.add(HttpHeaderNames.TRANSFER_ENCODING, "chunked"); if (!headers.contains(HttpHeaderNames.DATE)) { @@ -131,18 +152,15 @@ public class Http2ServerResponse implements ServerResponse { } headers.add(HttpHeaderNames.SERVER, ServerName.getServerName()); if (ctx.channel().isWritable()) { - Http2Headers http2Headers = new DefaultHttp2Headers().status(status.codeAsText()).add(headers); + Http2Headers http2Headers = new DefaultHttp2Headers().status(httpResponseStatus.codeAsText()).add(headers); Http2HeadersFrame http2HeadersFrame = new DefaultHttp2HeadersFrame(http2Headers,false); logger.log(Level.FINEST, http2HeadersFrame::toString); ctx.channel().write(http2HeadersFrame); - ChunkedInput input = new ChunkedNioStream(byteChannel); - HttpChunkedInput httpChunkedInput = new HttpChunkedInput(input); - ChannelFuture channelFuture = ctx.channel().writeAndFlush(httpChunkedInput); + ChannelFuture channelFuture = ctx.channel().writeAndFlush(new HttpChunkedInput(chunkedInput)); if ("close".equalsIgnoreCase(serverRequest.getRequest().headers().get(HttpHeaderNames.CONNECTION)) && !headers.contains(HttpHeaderNames.CONNECTION)) { channelFuture.addListener(ChannelFutureListener.CLOSE); } - httpResponseStatus = status; } else { logger.log(Level.WARNING, "channel not writeable"); } 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 index 43d0044..8dfa8b9 100644 --- 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 @@ -102,9 +102,7 @@ public class HttpServerRequest implements ServerRequest { String path = getEndpointInfo().getPath(); String effective = contextPath != null && !PATH_SEPARATOR.equals(contextPath) && path.startsWith(contextPath) ? path.substring(contextPath.length()) : path; - effective = effective.isEmpty() ? PATH_SEPARATOR : effective; - logger.log(Level.FINE, "path=" + path + " contextpath=" + contextPath + " effective=" + effective); - return effective; + return effective.isEmpty() ? PATH_SEPARATOR : effective; } @Override 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 bec2eed..2a6fc17 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 @@ -16,14 +16,12 @@ import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.stream.ChunkedInput; -import io.netty.handler.stream.ChunkedNioStream; -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.channels.ReadableByteChannel; +import java.nio.charset.Charset; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -31,8 +29,6 @@ import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; -import static io.netty.handler.codec.http.LastHttpContent.EMPTY_LAST_CONTENT; - public class HttpServerResponse implements ServerResponse { private static final Logger logger = Logger.getLogger(HttpServerResponse.class.getName()); @@ -57,27 +53,57 @@ public class HttpServerResponse implements ServerResponse { } @Override - public void setHeader(AsciiString name, String value) { + public void setHeader(CharSequence name, String value) { headers.set(name, value); } + @Override + public CharSequence getHeader(CharSequence name) { + return headers.get(name); + } + @Override public ChannelHandlerContext getChannelHandlerContext() { return ctx; } @Override - public HttpResponseStatus getLastStatus() { + public HttpResponseStatus getStatus() { return httpResponseStatus; } @Override - public void write(HttpResponseStatus status, String contentType, ByteBuf byteBuf) { + public ServerResponse withStatus(HttpResponseStatus httpResponseStatus) { + this.httpResponseStatus = httpResponseStatus; + return this; + } + + @Override + public ServerResponse withContentType(String contentType) { + headers.remove(HttpHeaderNames.CONTENT_TYPE); + headers.add(HttpHeaderNames.CONTENT_TYPE, contentType); + return this; + } + + @Override + public ServerResponse withCharset(Charset charset) { + CharSequence contentType = headers.get(HttpHeaderNames.CONTENT_TYPE); + if (contentType != null) { + headers.remove(HttpHeaderNames.CONTENT_TYPE); + headers.add(HttpHeaderNames.CONTENT_TYPE, contentType + "; charset=" + charset.name()); + } + return this; + } + + @Override + public void write(ByteBuf byteBuf) { 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); + if (httpResponseStatus == null) { + httpResponseStatus = HttpResponseStatus.OK; + } + CharSequence contentType = headers.get(HttpHeaderNames.CONTENT_TYPE); + if (contentType == null) { + headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM); } if (!headers.contains(HttpHeaderNames.CONTENT_LENGTH) && !headers.contains(HttpHeaderNames.TRANSFER_ENCODING)) { int length = byteBuf.readableBytes(); @@ -93,7 +119,8 @@ public class HttpServerResponse implements ServerResponse { headers.add(HttpHeaderNames.SERVER, ServerName.getServerName()); if (ctx.channel().isWritable()) { FullHttpResponse fullHttpResponse = - new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, byteBuf, headers, trailingHeaders); + new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, httpResponseStatus, byteBuf, headers, trailingHeaders); + logger.log(Level.FINEST, fullHttpResponse.headers()::toString); if (serverRequest != null && serverRequest.getSequenceId() != null) { HttpPipelinedResponse httpPipelinedResponse = new HttpPipelinedResponse(fullHttpResponse, ctx.channel().newPromise(), serverRequest.getSequenceId()); @@ -101,25 +128,25 @@ public class HttpServerResponse implements ServerResponse { } else { ctx.channel().writeAndFlush(fullHttpResponse); } - httpResponseStatus = status; } else { logger.log(Level.WARNING, "channel not writeable"); } } /** - * Chunked response from a readable byte channel. + * Chunked response. * - * @param status status - * @param contentType content type - * @param byteChannel byte channel + * @param chunkedInput chunked input */ @Override - public void write(HttpResponseStatus status, String contentType, ReadableByteChannel byteChannel) { - CharSequence s = headers.get(HttpHeaderNames.CONTENT_TYPE); - if (s == null) { - s = contentType != null ? contentType : HttpHeaderValues.APPLICATION_OCTET_STREAM; - headers.add(HttpHeaderNames.CONTENT_TYPE, s); + public void write(ChunkedInput chunkedInput) { + Objects.requireNonNull(chunkedInput); + if (httpResponseStatus == null) { + httpResponseStatus = HttpResponseStatus.OK; + } + CharSequence contentType = headers.get(HttpHeaderNames.CONTENT_TYPE); + if (contentType == null) { + headers.add(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.APPLICATION_OCTET_STREAM); } headers.add(HttpHeaderNames.TRANSFER_ENCODING, "chunked"); if (!headers.contains(HttpHeaderNames.DATE)) { @@ -127,19 +154,15 @@ public class HttpServerResponse implements ServerResponse { } headers.add(HttpHeaderNames.SERVER, ServerName.getServerName()); if (ctx.channel().isWritable()) { - HttpResponse httpResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, status); + HttpResponse httpResponse = new DefaultHttpResponse(HttpVersion.HTTP_1_1, httpResponseStatus); httpResponse.headers().add(headers); + logger.log(Level.FINEST, httpResponse.headers()::toString); ctx.channel().write(httpResponse); - logger.log(Level.FINE, "written response " + httpResponse); - ChunkedInput input = new ChunkedNioStream(byteChannel); - HttpChunkedInput httpChunkedInput = new HttpChunkedInput(input); - ctx.channel().writeAndFlush(httpChunkedInput); - ChannelFuture channelFuture = ctx.channel().writeAndFlush(EMPTY_LAST_CONTENT); + ChannelFuture channelFuture = ctx.channel().writeAndFlush(new HttpChunkedInput(chunkedInput)); if ("close".equalsIgnoreCase(serverRequest.getRequest().headers().get(HttpHeaderNames.CONNECTION)) && !headers.contains(HttpHeaderNames.CONNECTION)) { channelFuture.addListener(ChannelFutureListener.CLOSE); } - httpResponseStatus = status; } else { logger.log(Level.WARNING, "channel not writeable"); } diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ClassloaderServiceTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ClassloaderServiceTest.java index 91a9960..6aa6659 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ClassloaderServiceTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/ClassloaderServiceTest.java @@ -31,6 +31,7 @@ class ClassloaderServiceTest { new ClassLoaderService(ClassloaderServiceTest.class, "/cl")) .build(); Server server = Server.builder(namedServer) + .enableDebug() .build(); server.logDiagnostics(Level.INFO); Client client = Client.builder() 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 745653e..ce21963 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 @@ -32,7 +32,9 @@ class CleartextHttp1Test { HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); NamedServer namedServer = NamedServer.builder(httpAddress) .singleEndpoint("/**", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + response.withStatus(HttpResponseStatus.OK) + .withContentType("text/plain") + .write(request.getRequest().content().retain())) .build(); Server server = Server.builder(namedServer).build(); server.accept(); @@ -66,7 +68,9 @@ class CleartextHttp1Test { HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); NamedServer namedServer = NamedServer.builder(httpAddress) .singleEndpoint("/**", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + response.withStatus(HttpResponseStatus.OK) + .withContentType("text/plain") + .write(request.getRequest().content().retain())) .build(); Server server = Server.builder(namedServer).build(); server.accept(); @@ -111,7 +115,9 @@ class CleartextHttp1Test { HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); NamedServer namedServer = NamedServer.builder(httpAddress) .singleEndpoint("/**", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + response.withStatus(HttpResponseStatus.OK) + .withContentType("text/plain") + .write(request.getRequest().content().retain())) .build(); Server server = Server.builder(namedServer).build(); server.accept(); 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 13e04f7..a15a76d 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 @@ -33,7 +33,9 @@ class CleartextHttp2Test { HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); NamedServer namedServer = NamedServer.builder(httpAddress) .singleEndpoint("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + response.withStatus(HttpResponseStatus.OK) + .withContentType("text/plain") + .write(request.getRequest().content().retain())) .build(); Server server = Server.builder(namedServer).build(); server.accept(); @@ -73,7 +75,9 @@ class CleartextHttp2Test { HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); NamedServer namedServer = NamedServer.builder(httpAddress) .singleEndpoint("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + response.withStatus(HttpResponseStatus.OK) + .withContentType("text/plain") + .write(request.getRequest().content().retain())) .build(); Server server = Server.builder(namedServer).build(); server.accept(); @@ -165,9 +169,9 @@ class CleartextHttp2Test { }); } executorService.shutdown(); - boolean terminated = executorService.awaitTermination(30, TimeUnit.SECONDS); + boolean terminated = executorService.awaitTermination(60, TimeUnit.SECONDS); logger.log(Level.INFO, "terminated = " + terminated + ", now waiting for transport to complete"); - transport.get(30, TimeUnit.SECONDS); + transport.get(60, TimeUnit.SECONDS); } finally { client.shutdownGracefully(); server.shutdownGracefully(); 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 index cd57fe4..1860059 100644 --- 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 @@ -12,7 +12,7 @@ import org.xbib.netty.http.server.ServerResponse; 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.MappedFileService; +import org.xbib.netty.http.server.endpoint.service.FileService; import org.xbib.netty.http.server.endpoint.service.Service; import java.io.IOException; @@ -36,7 +36,7 @@ class EndpointTest { @Test void testEmptyPrefixEndpoint() throws Exception { Path vartmp = Paths.get("/var/tmp/"); - Service service = new MappedFileService(vartmp); + Service service = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); EndpointResolver endpointResolver = EndpointResolver.builder() .addEndpoint(Endpoint.builder().setPath("/**").build()) @@ -76,7 +76,7 @@ class EndpointTest { @Test void testPlainPrefixEndpoint() throws Exception { Path vartmp = Paths.get("/var/tmp/"); - Service service = new MappedFileService(vartmp); + Service service = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); EndpointResolver endpointResolver = EndpointResolver.builder() .addEndpoint(Endpoint.builder().setPrefix("/").setPath("/**").build()) @@ -117,7 +117,7 @@ class EndpointTest { @Test void testSimplePathEndpoints() throws Exception { Path vartmp = Paths.get("/var/tmp/"); - Service service = new MappedFileService(vartmp); + Service service = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); EndpointResolver endpointResolver = EndpointResolver.builder() .addEndpoint(Endpoint.builder().setPrefix("/static").setPath("/**").build()) @@ -183,7 +183,7 @@ class EndpointTest { @Test void testQueryAndFragmentEndpoints() throws Exception { Path vartmp = Paths.get("/var/tmp/"); - Service service = new MappedFileService(vartmp); + Service service = new FileService(vartmp); HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); EndpointResolver endpointResolver = EndpointResolver.builder() .addEndpoint(Endpoint.builder().setPrefix("/static").setPath("/**").build()) diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/FileServiceTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/FileServiceTest.java new file mode 100644 index 0000000..93535b5 --- /dev/null +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/FileServiceTest.java @@ -0,0 +1,98 @@ +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.FileService; + +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 FileServiceTest { + + private static final Logger logger = Logger.getLogger(FileServiceTest.class.getName()); + + @Test + void testFileServiceHttp1() throws Exception { + Path vartmp = Paths.get("/var/tmp/"); + HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); + NamedServer namedServer = NamedServer.builder(httpAddress) + .singleEndpoint("/static", "/**", new FileService(vartmp)) + .build(); + Server server = Server.builder(namedServer) + .enableDebug() + .build(); + 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.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 testFileServiceHttp2() throws Exception { + Path vartmp = Paths.get("/var/tmp/"); + HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); + NamedServer namedServer = NamedServer.builder(httpAddress) + .singleEndpoint("/static", "/**", new FileService(vartmp)) + .build(); + Server server = Server.builder(namedServer) + .enableDebug() + .build(); + 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/NettyHttpExtension.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/NettyHttpExtension.java index 5d9305d..508f567 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/NettyHttpExtension.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/NettyHttpExtension.java @@ -20,10 +20,6 @@ public class NettyHttpExtension implements BeforeAllCallback { Security.addProvider(new BouncyCastleProvider()); } System.setProperty("io.netty.noUnsafe", Boolean.toString(true)); - System.setProperty("io.netty.noKeySetOptimization", Boolean.toString(true)); - //System.setProperty("io.netty.recycler.maxCapacity", Integer.toString(0)); - //System.setProperty("io.netty.leakDetection.level", "paranoid"); - Level level = Level.INFO; System.setProperty("java.util.logging.SimpleFormatter.format", "%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n"); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureStaticFileServiceTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureFileServiceTest.java similarity index 90% rename from netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureStaticFileServiceTest.java rename to netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureFileServiceTest.java index c506e8c..cdc080b 100644 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureStaticFileServiceTest.java +++ b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/SecureFileServiceTest.java @@ -8,7 +8,7 @@ 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.MappedFileService; +import org.xbib.netty.http.server.endpoint.service.FileService; import java.nio.charset.StandardCharsets; import java.nio.file.Files; @@ -22,18 +22,18 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; @ExtendWith(NettyHttpExtension.class) -class SecureStaticFileServiceTest { +class SecureFileServiceTest { - private static final Logger logger = Logger.getLogger(SecureStaticFileServiceTest.class.getName()); + private static final Logger logger = Logger.getLogger(SecureFileServiceTest.class.getName()); @Test - void testSecureStaticFileServerHttp1() throws Exception { + void testSecureFileServerHttp1() 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 MappedFileService(vartmp)) + .singleEndpoint("/static", "/**", new FileService(vartmp)) .build()) .setChildThreadCount(8) .build(); @@ -67,13 +67,13 @@ class SecureStaticFileServiceTest { } @Test - void testSecureStaticFileServerHttp2() throws Exception { + void testSecureFileServerHttp2() 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 MappedFileService(vartmp)) + .singleEndpoint("/static", "/**", new FileService(vartmp)) .build()) .build(); Client client = Client.builder() 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 085783f..e00e32b 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 @@ -34,7 +34,9 @@ class SecureHttp1Test { Server server = Server.builder(NamedServer.builder(httpAddress) .setSelfCert() .singleEndpoint("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + response.withStatus(HttpResponseStatus.OK) + .withContentType("text/plain") + .write(request.getRequest().content().retain())) .build()) .build(); Client client = Client.builder() @@ -67,7 +69,9 @@ class SecureHttp1Test { Server server = Server.builder(NamedServer.builder(httpAddress) .setSelfCert() .singleEndpoint("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + response.withStatus(HttpResponseStatus.OK) + .withContentType("text/plain") + .write(request.getRequest().content().retain())) .build()) .build(); server.accept(); @@ -113,8 +117,9 @@ class SecureHttp1Test { Server server = Server.builder(NamedServer.builder(httpAddress) .setSelfCert() .singleEndpoint("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()) - ) + response.withStatus(HttpResponseStatus.OK) + .withContentType("text/plain") + .write(request.getRequest().content().retain())) .build()) .build(); server.accept(); 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 c49bbf2..aee23b2 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 @@ -33,7 +33,9 @@ class SecureHttp2Test { Server server = Server.builder(NamedServer.builder(httpAddress) .setSelfCert() .singleEndpoint("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + response.withStatus(HttpResponseStatus.OK) + .withContentType("text/plain") + .write(request.getRequest().content().retain())) .build()) .build(); server.accept(); @@ -74,7 +76,9 @@ class SecureHttp2Test { Server server = Server.builder(NamedServer.builder(httpAddress) .setSelfCert() .singleEndpoint("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())) + response.withStatus(HttpResponseStatus.OK) + .withContentType("text/plain") + .write(request.getRequest().content().retain())) .build()) .build(); server.accept(); @@ -123,7 +127,9 @@ class SecureHttp2Test { Server server = Server.builder(NamedServer.builder(httpAddress) .setSelfCert() .singleEndpoint("/", (request, response) -> - response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()) + response.withStatus(HttpResponseStatus.OK) + .withContentType("text/plain") + .write(request.getRequest().content().retain()) ) .build()) .build(); diff --git a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/StaticFileServiceTest.java b/netty-http-server/src/test/java/org/xbib/netty/http/server/test/StaticFileServiceTest.java deleted file mode 100644 index d2c9178..0000000 --- a/netty-http-server/src/test/java/org/xbib/netty/http/server/test/StaticFileServiceTest.java +++ /dev/null @@ -1,165 +0,0 @@ -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.ChunkedFileService; -import org.xbib.netty.http.server.endpoint.service.MappedFileService; - -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 StaticFileServiceTest { - - private static final Logger logger = Logger.getLogger(StaticFileServiceTest.class.getName()); - - @Test - void testStaticFileServerHttp1() throws Exception { - Path vartmp = Paths.get("/var/tmp/"); - HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) - .singleEndpoint("/static", "/**", new MappedFileService(vartmp)) - .build(); - Server server = Server.builder(namedServer) - .build(); - 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.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 testChunkedFileServerHttp1() throws Exception { - Path vartmp = Paths.get("/var/tmp/"); - HttpAddress httpAddress = HttpAddress.http1("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) - .singleEndpoint("/static", "/**", new ChunkedFileService(vartmp)) - .build(); - Server server = Server.builder(namedServer) - .build(); - 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.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 testStaticFileServerHttp2() throws Exception { - Path vartmp = Paths.get("/var/tmp/"); - HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) - .singleEndpoint("/static", "/**", new MappedFileService(vartmp)) - .build(); - Server server = Server.builder(namedServer) - .build(); - 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()); - } - - @Test - void testChunkedFileServerHttp2() throws Exception { - Path vartmp = Paths.get("/var/tmp/"); - HttpAddress httpAddress = HttpAddress.http2("localhost", 8008); - NamedServer namedServer = NamedServer.builder(httpAddress) - .singleEndpoint("/static", "/**", new ChunkedFileService(vartmp)) - .build(); - Server server = Server.builder(namedServer) - .build(); - 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()); - } -}