SSL session, gzip compression/decompression, static resource services for file/claspath urls, etags/caching/range responses
This commit is contained in:
parent
71a912d7cd
commit
509b8073eb
35 changed files with 1005 additions and 520 deletions
|
@ -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')}"
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 = "";
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}*/
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}*/
|
||||
}
|
||||
}
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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()
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue