SSL session, gzip compression/decompression, static resource services for file/claspath urls, etags/caching/range responses

This commit is contained in:
Jörg Prante 2019-06-25 23:54:52 +02:00
parent 71a912d7cd
commit 509b8073eb
35 changed files with 1005 additions and 520 deletions

View file

@ -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')}"
}

View file

@ -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<Channel> initializer;
if (httpVersion.majorVersion() == 1) {
initializer = new HttpChannelInitializer(clientConfig, httpAddress, sslHandlerFactory,
@ -330,10 +331,8 @@ 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());
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();
@ -361,9 +360,6 @@ public final class Client {
break;
}
return sslHandler;
} catch (SSLException e) {
throw new IllegalArgumentException(e);
}
}
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);
}
}

View file

@ -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<Channel> {
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) {

View file

@ -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<Request, Channel> channels;
private SSLSession sslSession;
final Map<String, Flow> channelFlowMap;
final SortedMap<String, Request> requests;
@ -70,8 +75,15 @@ abstract class BaseTransport implements Transport {
@Override
public <T> CompletableFuture<T> execute(Request request,
Function<FullHttpResponse, T> supplier) throws IOException {
Objects.requireNonNull(supplier);
final CompletableFuture<T> 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;
}

View file

@ -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;
}

View file

@ -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");

View file

@ -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;
}

View file

@ -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) {

View file

@ -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<ByteBuf> 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 = "";
}

View file

@ -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;
}
}

View file

@ -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();
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}*/
}

View file

@ -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);

View file

@ -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();
}

View file

@ -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<Range> 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;
}
}
}

View file

@ -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);
}*/
}
}

View file

@ -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<SocketChannel> {
private final HttpHandler httpHandler;
private final DomainNameMapping<SslContext> domainNameMapping;
private final SniHandler sniHandler;
public HttpChannelInitializer(Server server,
HttpAddress httpAddress,
@ -48,8 +49,8 @@ public class HttpChannelInitializer extends ChannelInitializer<SocketChannel> {
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<SocketChannel> {
}
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<SocketChannel> {
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(),

View file

@ -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<Channel> {
private final HttpAddress httpAddress;
private final DomainNameMapping<SslContext> domainNameMapping;
private final SniHandler sniHandler;
public Http2ChannelInitializer(Server server,
HttpAddress httpAddress,
@ -52,7 +54,7 @@ public class Http2ChannelInitializer extends ChannelInitializer<Channel> {
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<Channel> {
}
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<Channel> {
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<Channel> {
p.addLast("server-messages", new ServerMessages());
}
public SslContext getSessionContext() {
if (httpAddress.isSecure()) {
return sniHandler.sslContext();
}
return null;
}
class ServerRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {
@Override

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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,35 +49,60 @@ 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);
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)) {
int length = byteBuf.readableBytes();
if (length < 0) {
headers.add(HttpHeaderNames.TRANSFER_ENCODING, "chunked");
} else {
headers.add(HttpHeaderNames.CONTENT_LENGTH, Long.toString(length));
}
headers.add(HttpHeaderNames.CONTENT_LENGTH, Long.toString(byteBuf.readableBytes()));
}
if (serverRequest != null && "close".equalsIgnoreCase(serverRequest.getRequest().headers().get(HttpHeaderNames.CONNECTION)) &&
!headers.contains(HttpHeaderNames.CONNECTION)) {
@ -89,7 +112,7 @@ public class Http2ServerResponse implements ServerResponse {
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);
}
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<ByteBuf> 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<ByteBuf> 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");
}

View file

@ -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

View file

@ -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<ByteBuf> 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<ByteBuf> 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");
}

View file

@ -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()

View file

@ -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();

View file

@ -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();

View file

@ -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())

View file

@ -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());
}
}

View file

@ -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");

View file

@ -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()

View file

@ -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();

View file

@ -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();

View file

@ -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());
}
}