add static file server capability

This commit is contained in:
Jörg Prante 2019-04-22 23:09:47 +02:00
parent f4cfd6313d
commit ad460a5111
37 changed files with 452 additions and 129 deletions

View file

@ -64,14 +64,14 @@ subprojects {
}
test {
if (JavaVersion.current() == JavaVersion.VERSION_1_8) {
jvmArgs "-javaagent:" + configurations.alpnagent.asPath
}
systemProperty 'java.util.logging.config.file', 'src/test/resources/logging.properties'
testLogging {
// set this to 'true' to show lot of debug output
showStandardStreams = false
exceptionFormat = 'full'
}
if (JavaVersion.current() == JavaVersion.VERSION_1_8) {
jvmArgs "-javaagent:" + configurations.alpnagent.asPath
}
}
clean {

View file

@ -1,17 +1,16 @@
group = org.xbib
name = netty-http
version = 4.1.34.0
version = 4.1.35.0
# main packages
netty.version = 4.1.34.Final
netty.version = 4.1.35.Final
tcnative.version = 2.0.22.Final
bouncycastle.version = 1.61
alpnagent.version = 2.0.9
xbib-net-url.version = 1.2.1
xbib-net-url.version = 1.2.2
# test packages
junit.version = 4.12
# 1.0.1
conscrypt.version = 2.0.0
jackson.version = 2.8.11.1
wagon.version = 3.0.0

Binary file not shown.

View file

@ -1,6 +1,6 @@
#Thu Mar 14 13:01:02 CET 2019
#Mon Apr 22 17:45:04 CEST 2019
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-5.3-all.zip

18
gradlew vendored
View file

@ -1,5 +1,21 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
@ -28,7 +44,7 @@ APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m"'
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"

18
gradlew.bat vendored
View file

@ -1,3 +1,19 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem http://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m"
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

View file

@ -80,6 +80,10 @@ public class HttpTransport extends BaseTransport {
logger.log(Level.WARNING, "throwable not null for response " + fullHttpResponse, throwable);
return;
}
if (requests.isEmpty()) {
logger.log(Level.WARNING, "no request present, can not handle response " + fullHttpResponse);
return;
}
// streamID is expected to be null, last request on memory is expected to be current, remove request from memory
Request request = requests.remove(requests.lastKey());
if (request != null) {

View file

@ -0,0 +1,5 @@
handlers = java.util.logging.ConsoleHandler
.level = FINE
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format = %1$tFT%1$tT.%1$tL%1$tz [%4$-11s] [%3$s] %5$s %6$s%n

View file

@ -138,7 +138,7 @@ public final class Server {
DomainNameMappingBuilder<SslContext> mappingBuilder = new DomainNameMappingBuilder<>(sslContext);
for (VirtualServer virtualServer : serverConfig.getVirtualServers()) {
String name = virtualServer.getName();
mappingBuilder.add( name == null ? "*" : name, sslContext);
mappingBuilder.add(name == null ? "*" : name, sslContext);
}
domainNameMapping = mappingBuilder.build();
}

View file

@ -224,7 +224,7 @@ public class ServerBuilder {
return this;
}
public ServerBuilder addVirtualHost(VirtualServer virtualServer) {
public ServerBuilder addVirtualServer(VirtualServer virtualServer) {
this.serverConfig.addVirtualServer(virtualServer);
return this;
}

View file

@ -0,0 +1,53 @@
package org.xbib.netty.http.server.context;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.xbib.netty.http.server.transport.ServerRequest;
import org.xbib.netty.http.server.transport.ServerResponse;
import org.xbib.netty.http.server.util.MimeTypeUtils;
import java.io.IOException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class ClasspathContextHandler implements ContextHandler {
private final ClassLoader classLoader;
private final String prefix;
public ClasspathContextHandler(ClassLoader classLoader, String prefix) {
this.classLoader = classLoader;
this.prefix = prefix;
}
@Override
public void serve(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException {
String contextPath = serverRequest.getContextPath();
URL url = classLoader.getResource(prefix + contextPath);
if (url != null) {
try {
Path path = Paths.get(url.toURI());
FileChannel fileChannel = (FileChannel) Files.newByteChannel(path);
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
ByteBuf byteBuf = Unpooled.wrappedBuffer(mappedByteBuffer);
try {
String contentType = MimeTypeUtils.guessFromPath(contextPath, false);
serverResponse.write(HttpResponseStatus.OK, contentType, byteBuf);
} finally {
byteBuf.release();
}
} catch (URISyntaxException e) {
serverResponse.write(HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
} else {
serverResponse.write(HttpResponseStatus.NOT_FOUND);
}
}
}

View file

@ -1,6 +1,6 @@
package org.xbib.netty.http.server.context;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
/**
@ -8,12 +8,13 @@ import java.util.Map;
*/
public class ContextInfo {
private final Map<String, ContextHandler> handlers = new HashMap<>(2);
private final VirtualServer virtualServer;
private final Map<String, ContextHandler> methodHandlerMap;
public ContextInfo(VirtualServer virtualServer) {
this.virtualServer = virtualServer;
this.methodHandlerMap = new LinkedHashMap<>();
}
/**
@ -21,8 +22,8 @@ public class ContextInfo {
*
* @return the map of supported HTTP methods and their corresponding handlers
*/
public Map<String, ContextHandler> getHandlers() {
return handlers;
public Map<String, ContextHandler> getMethodHandlerMap() {
return methodHandlerMap;
}
/**
@ -33,11 +34,13 @@ public class ContextInfo {
*/
public void addHandler(ContextHandler handler, String... methods) {
if (methods.length == 0) {
methods = new String[]{"GET"};
}
for (String method : methods) {
handlers.put(method, handler);
virtualServer.getMethods().add(method);
methodHandlerMap.put("GET", handler);
virtualServer.getMethods().add("GET");
} else {
for (String method : methods) {
methodHandlerMap.put(method, handler);
virtualServer.getMethods().add(method);
}
}
}
}

View file

@ -2,6 +2,7 @@ package org.xbib.netty.http.server.context;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.xbib.netty.http.server.transport.ServerRequest;
import org.xbib.netty.http.server.transport.ServerResponse;
@ -28,11 +29,10 @@ public class DirectoryContextHandler implements ContextHandler {
String uri = serverRequest.getRequest().uri();
Path p = path.resolve(uri);
ByteBuf byteBuf = read(allocator, p);
serverResponse.write(200, "application/octet-stream", byteBuf);
serverResponse.write(HttpResponseStatus.OK, "application/octet-stream", byteBuf);
byteBuf.release();
}
public static ByteBuf read(ByteBufAllocator allocator, Path path)
throws IOException {
try (SeekableByteChannel sbc = Files.newByteChannel(path);

View file

@ -0,0 +1,42 @@
package org.xbib.netty.http.server.context;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.xbib.netty.http.server.transport.ServerRequest;
import org.xbib.netty.http.server.transport.ServerResponse;
import org.xbib.netty.http.server.util.MimeTypeUtils;
import java.io.IOException;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
public class NioContextHandler implements ContextHandler {
private final Path prefix;
public NioContextHandler(Path prefix) {
this.prefix = prefix;
if (!Files.exists(prefix) || !Files.isDirectory(prefix)) {
throw new IllegalArgumentException("prefix: " + prefix + " (not a directory");
}
}
@Override
public void serve(ServerRequest serverRequest, ServerResponse serverResponse) throws IOException {
String requestPath = serverRequest.getRequestPath();
Path path = prefix.resolve(requestPath.substring(1)); // starts always with '/'
if (Files.exists(path) && Files.isReadable(path)) {
try (FileChannel fileChannel = (FileChannel) Files.newByteChannel(path)) {
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
ByteBuf byteBuf = Unpooled.wrappedBuffer(mappedByteBuffer);
String contentType = MimeTypeUtils.guessFromPath(requestPath, false);
serverResponse.write(HttpResponseStatus.OK, contentType, byteBuf);
}
} else {
serverResponse.write(HttpResponseStatus.NOT_FOUND);
}
}
}

View file

@ -24,6 +24,10 @@ public class VirtualServer {
private volatile boolean allowGeneratedIndex;
public VirtualServer() {
this(null);
}
/**
* Constructs a VirtualServer with the given name.
*
@ -94,25 +98,6 @@ public class VirtualServer {
return methods;
}
/**
* Returns the context handler for the given path.
* If a context is not found for the given path, the search is repeated for
* its parent path, and so on until a base context is found. If neither the
* given path nor any of its parents has a context, an empty context is returned.
*
* @param path the context's path
* @return the context info for the given path, or an empty context if none exists
*/
public ContextInfo getContext(String path) {
path = trimRight(path, '/'); // remove trailing slash
ContextInfo info = null;
while (info == null && path != null) {
info = contexts.get(path);
path = getParentPath(path);
}
return info != null ? info : emptyContext;
}
/**
* Adds a context and its corresponding context handler to this server.
* Paths are normalized by removing trailing slashes (except the root).
@ -122,15 +107,16 @@ public class VirtualServer {
* @param methods the HTTP methods supported by the context handler (default is "GET")
* @throws IllegalArgumentException if path is malformed
*/
public void addContext(String path, ContextHandler handler, String... methods) {
public VirtualServer addContext(String path, ContextHandler handler, String... methods) {
if (path == null || !path.startsWith("/") && !path.equals("*")) {
throw new IllegalArgumentException("invalid path: " + path);
}
path = trimRight(path, '/');
String s = trimRight(path, '/');
ContextInfo info = new ContextInfo(this);
ContextInfo existing = contexts.putIfAbsent(path, info);
ContextInfo existing = contexts.putIfAbsent(s, info);
info = existing != null ? existing : info;
info.addHandler(handler, methods);
return this;
}
/**
@ -141,17 +127,37 @@ public class VirtualServer {
* @throws IllegalArgumentException if a Context-annotated
* method has an {@link Context invalid signature}
*/
public void addContexts(Object o) throws IllegalArgumentException {
public VirtualServer addContexts(Object o) throws IllegalArgumentException {
for (Class<?> c = o.getClass(); c != null; c = c.getSuperclass()) {
// add to contexts those with @Context annotation
for (Method m : c.getDeclaredMethods()) {
Context context = m.getAnnotation(Context.class);
if (context != null) {
//m.setAccessible(true); // allow access to private method
addContext(context.value(), new MethodContextHandler(m, o), context.methods());
}
}
}
return this;
}
/**
* Returns the context handler for the given path.
* If a context is not found for the given path, the search is repeated for
* its parent path, and so on until a base context is found. If neither the
* given path nor any of its parents has a context, an empty context is returned.
*
* @param path the context's path
* @return the context info for the given path, or an empty context if none exists
*/
public ContextPath getContextPath(String path) {
String s = trimRight(path, '/');
ContextInfo info = null;
String hook = null;
while (info == null && s != null) {
hook = s;
info = contexts.get(s);
s = getParentPath(s);
}
return new ContextPath(hook, info != null ? info : emptyContext);
}
/**
@ -179,9 +185,29 @@ public class VirtualServer {
* or null if given path is the root path
*/
private static String getParentPath(String path) {
path = trimRight(path, '/'); // remove trailing slash
int slash = path.lastIndexOf('/');
return slash == -1 ? null : path.substring(0, slash);
String s = trimRight(path, '/'); // remove trailing slash
int slash = s.lastIndexOf('/');
return slash == -1 ? null : s.substring(0, slash);
}
public class ContextPath {
private final String hook;
private final ContextInfo contextInfo;
ContextPath(String hook, ContextInfo contextInfo) {
this.hook = hook;
this.contextInfo = contextInfo;
}
public String getHook() {
return hook;
}
public ContextInfo getContextInfo() {
return contextInfo;
}
}
}

View file

@ -3,6 +3,7 @@ package org.xbib.netty.http.server.transport;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.context.ContextHandler;
@ -33,7 +34,7 @@ abstract class BaseServerTransport implements ServerTransport {
}
@Override
public void exceptionReceived(ChannelHandlerContext ctx, Throwable throwable) throws IOException {
public void exceptionReceived(ChannelHandlerContext ctx, Throwable throwable) {
logger.log(Level.WARNING, throwable.getMessage(), throwable);
}
@ -54,7 +55,7 @@ abstract class BaseServerTransport implements ServerTransport {
case 2:
if (!reqHeaders.contains(HttpHeaderNames.HOST)) {
// RFC2616#14.23: missing Host header gets 400
serverResponse.writeError(400, "missing 'Host' header");
serverResponse.writeError(HttpResponseStatus.BAD_REQUEST, "missing 'Host' header");
return false;
}
// return a continue response before reading body
@ -65,13 +66,13 @@ abstract class BaseServerTransport implements ServerTransport {
//tempResp.sendHeaders(100);
} else {
// RFC2616#14.20: if unknown expect, send 417
serverResponse.writeError(417);
serverResponse.writeError(HttpResponseStatus.EXPECTATION_FAILED);
return false;
}
}
break;
default:
serverResponse.writeError(400, "Unknown version: " + version);
serverResponse.writeError(HttpResponseStatus.BAD_REQUEST, "Unknown version: " + version);
return false;
}
return true;
@ -88,12 +89,14 @@ abstract class BaseServerTransport implements ServerTransport {
String method = serverRequest.getRequest().method().name();
String path = serverRequest.getRequest().uri();
VirtualServer virtualServer = serverRequest.getVirtualServer();
Map<String, ContextHandler> handlers = virtualServer.getContext(path).getHandlers();
VirtualServer.ContextPath contextPath = virtualServer.getContextPath(path);
serverRequest.setContextPath(contextPath.getHook());
Map<String, ContextHandler> methodHandlerMap = contextPath.getContextInfo().getMethodHandlerMap();
// RFC 2616#5.1.1 - GET and HEAD must be supported
if (method.equals("GET") || method.equals("HEAD") || handlers.containsKey(method)) {
ContextHandler handler = virtualServer.getContext(path).getHandlers().get(method);
if (method.equals("GET") || method.equals("HEAD") || methodHandlerMap.containsKey(method)) {
ContextHandler handler = methodHandlerMap.get(method);
if (handler == null) {
serverResponse.writeError(404);
serverResponse.writeError(HttpResponseStatus.NOT_FOUND);
} else {
handler.serve(serverRequest, serverResponse);
}
@ -101,15 +104,15 @@ abstract class BaseServerTransport implements ServerTransport {
Set<String> methods = new LinkedHashSet<>(METHODS);
// "*" is a special server-wide (no-context) request supported by OPTIONS
boolean isServerOptions = path.equals("*") && method.equals("OPTIONS");
methods.addAll(isServerOptions ? virtualServer.getMethods() : handlers.keySet());
methods.addAll(isServerOptions ? virtualServer.getMethods() : methodHandlerMap.keySet());
serverResponse.setHeader(HttpHeaderNames.ALLOW, String.join(", ", methods));
if (method.equals("OPTIONS")) { // default OPTIONS handler
serverResponse.setHeader(HttpHeaderNames.CONTENT_LENGTH, "0"); // RFC2616#9.2
serverResponse.write(200);
serverResponse.write(HttpResponseStatus.OK);
} else if (virtualServer.getMethods().contains(method)) {
serverResponse.write(405); // supported by server, but not this context (nor built-in)
serverResponse.write(HttpResponseStatus.METHOD_NOT_ALLOWED); // supported by server, but not this context (nor built-in)
} else {
serverResponse.writeError(501); // unsupported method
serverResponse.writeError(HttpResponseStatus.NOT_IMPLEMENTED); // unsupported method
}
}
}

View file

@ -41,7 +41,7 @@ public class Http2ServerResponse implements ServerResponse {
@Override
public void write(String text) {
write(200, "text/plain; charset=utf-8", text);
write(HttpResponseStatus.OK, "text/plain; charset=utf-8", text);
}
/**
@ -50,8 +50,8 @@ public class Http2ServerResponse implements ServerResponse {
* @param status the response status
*/
@Override
public void writeError(int status) {
writeError(status, status < 400 ? ":)" : "sorry it didn't work out :(");
public void writeError(HttpResponseStatus status) {
writeError(status, status.code() < 400 ? ":)" : "sorry it didn't work out :(");
}
/**
@ -64,32 +64,32 @@ public class Http2ServerResponse implements ServerResponse {
* @param text the text body (sent as text/html)
*/
@Override
public void writeError(int status, String text) {
public void writeError(HttpResponseStatus status, String text) {
write(status, "text/html; charset=utf-8",
String.format("<!DOCTYPE html>%n<html>%n<head><title>%d %s</title></head>%n" +
"<body><h1>%d %s</h1>%n<p>%s</p>%n</body></html>",
status, HttpResponseStatus.valueOf(status).reasonPhrase(),
status, HttpResponseStatus.valueOf(status).reasonPhrase(),
status.code(), status.reasonPhrase(),
status.code(), status.reasonPhrase(),
escapeHTML(text)));
}
@Override
public void write(int status) {
public void write(HttpResponseStatus status) {
write(status, null, (ByteBuf) null);
}
@Override
public void write(int status, String contentType, String text) {
public void write(HttpResponseStatus status, String contentType, String text) {
write(status, contentType, ByteBufUtil.writeUtf8(ctx.alloc(), text));
}
@Override
public void write(int status, String contentType, String text, Charset charset) {
public void write(HttpResponseStatus status, String contentType, String text, Charset charset) {
write(status, contentType, ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.allocate(text.length()).append(text), charset));
}
@Override
public void write(int status, String contentType, ByteBuf byteBuf) {
public void write(HttpResponseStatus status, String contentType, ByteBuf byteBuf) {
if (byteBuf != null) {
CharSequence s = headers.get(HttpHeaderNames.CONTENT_TYPE);
if (s == null) {
@ -120,7 +120,7 @@ public class Http2ServerResponse implements ServerResponse {
}
}
Http2Headers http2Headers = new DefaultHttp2Headers()
.status(HttpResponseStatus.valueOf(status).codeAsText())
.status(status.codeAsText())
.add(headers);
ctx.channel().write(new DefaultHttp2HeadersFrame(http2Headers,byteBuf == null));
if (byteBuf != null) {

View file

@ -3,6 +3,7 @@ package org.xbib.netty.http.server.transport;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http2.Http2Settings;
import io.netty.handler.codec.http2.HttpConversionUtil;
import org.xbib.netty.http.common.HttpAddress;
@ -36,6 +37,8 @@ public class Http2ServerTransport extends BaseServerTransport {
ServerResponse serverResponse = new Http2ServerResponse(serverRequest, ctx);
if (acceptRequest(serverRequest, serverResponse)) {
handle(serverRequest, serverResponse);
} else {
serverResponse.write(HttpResponseStatus.NOT_ACCEPTABLE);
}
}

View file

@ -21,12 +21,9 @@ import java.nio.charset.Charset;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.logging.Logger;
public class HttpServerResponse implements ServerResponse {
private static final Logger logger = Logger.getLogger(HttpServerResponse.class.getName());
private final ServerRequest serverRequest;
private final ChannelHandlerContext ctx;
@ -49,7 +46,7 @@ public class HttpServerResponse implements ServerResponse {
@Override
public void write(String text) {
write(200, "text/plain; charset=utf-8", text);
write(HttpResponseStatus.OK, "text/plain; charset=utf-8", text);
}
/**
@ -58,8 +55,8 @@ public class HttpServerResponse implements ServerResponse {
* @param status the response status
*/
@Override
public void writeError(int status) {
writeError(status, status < 400 ? ":)" : "sorry it didn't work out :(");
public void writeError(HttpResponseStatus status) {
writeError(status, status.code() < 400 ? ":)" : "sorry it didn't work out :(");
}
/**
@ -72,32 +69,32 @@ public class HttpServerResponse implements ServerResponse {
* @param text the text body (sent as text/html)
*/
@Override
public void writeError(int status, String text) {
public void writeError(HttpResponseStatus status, String text) {
write(status, "text/html; charset=utf-8",
String.format("<!DOCTYPE html>%n<html>%n<head><title>%d %s</title></head>%n" +
"<body><h1>%d %s</h1>%n<p>%s</p>%n</body></html>",
status, HttpResponseStatus.valueOf(status).reasonPhrase(),
status, HttpResponseStatus.valueOf(status).reasonPhrase(),
status.code(), status.reasonPhrase(),
status.code(), status.reasonPhrase(),
escapeHTML(text)));
}
@Override
public void write(int status) {
public void write(HttpResponseStatus status) {
write(status, null, (ByteBuf) null);
}
@Override
public void write(int status, String contentType, String text) {
public void write(HttpResponseStatus status, String contentType, String text) {
write(status, contentType, ByteBufUtil.writeUtf8(ctx.alloc(), text));
}
@Override
public void write(int status, String contentType, String text, Charset charset) {
public void write(HttpResponseStatus status, String contentType, String text, Charset charset) {
write(status, contentType, ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.allocate(text.length()).append(text), charset));
}
@Override
public void write(int status, String contentType, ByteBuf byteBuf) {
public void write(HttpResponseStatus status, String contentType, ByteBuf byteBuf) {
if (byteBuf != null) {
CharSequence s = headers.get(HttpHeaderNames.CONTENT_TYPE);
if (s == null) {
@ -123,9 +120,9 @@ public class HttpServerResponse implements ServerResponse {
}
FullHttpResponse fullHttpResponse = byteBuf != null ?
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.valueOf(status), byteBuf, headers, trailingHeaders) :
status, byteBuf, headers, trailingHeaders) :
new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
HttpResponseStatus.valueOf(status), Unpooled.EMPTY_BUFFER, headers, trailingHeaders);
status, Unpooled.EMPTY_BUFFER, headers, trailingHeaders);
if (serverRequest != null && serverRequest.getSequenceId() != null) {
HttpPipelinedResponse httpPipelinedResponse = new HttpPipelinedResponse(fullHttpResponse,
ctx.channel().newPromise(), serverRequest.getSequenceId());
@ -176,7 +173,7 @@ public class HttpServerResponse implements ServerResponse {
break;
}
if (ref != null) {
es.append(s.substring(start, i)).append(ref);
es.append(s, start, i).append(ref);
start = i + 1;
}
}

View file

@ -3,6 +3,7 @@ package org.xbib.netty.http.server.transport;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http2.Http2Settings;
import org.xbib.netty.http.common.HttpAddress;
import org.xbib.netty.http.server.Server;
@ -35,6 +36,8 @@ public class HttpServerTransport extends BaseServerTransport {
ServerResponse serverResponse = new HttpServerResponse(serverRequest, ctx);
if (acceptRequest(serverRequest, serverResponse)) {
handle(serverRequest, serverResponse);
} else {
serverResponse.write(HttpResponseStatus.NOT_ACCEPTABLE);
}
}

View file

@ -21,6 +21,8 @@ public class ServerRequest {
private final Integer requestId;
private String contextPath;
public ServerRequest(VirtualServer virtualServer, HttpAddress httpAddress,
FullHttpRequest httpRequest, Integer sequenceId, Integer streamId, Integer requestId) {
this.virtualServer = virtualServer;
@ -35,6 +37,18 @@ public class ServerRequest {
return virtualServer;
}
public void setContextPath(String contextPath) {
this.contextPath = contextPath;
}
public String getContextPath() {
return contextPath;
}
public String getRequestPath() {
return contextPath != null ? httpRequest.uri().substring(contextPath.length()) : httpRequest.uri();
}
public HttpAddress getHttpAddress() {
return httpAddress;
}

View file

@ -1,6 +1,7 @@
package org.xbib.netty.http.server.transport;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.util.AsciiString;
import java.nio.charset.Charset;
@ -14,16 +15,16 @@ public interface ServerResponse {
void write(String text);
void writeError(int status);
void writeError(HttpResponseStatus status);
void writeError(int status, String text);
void writeError(HttpResponseStatus status, String text);
void write(int status);
void write(HttpResponseStatus status);
void write(int status, String contentType, String text);
void write(HttpResponseStatus status, String contentType, String text);
void write(int status, String contentType, String text, Charset charset);
void write(HttpResponseStatus status, String contentType, String text, Charset charset);
void write(int status, String contentType, ByteBuf byteBuf);
void write(HttpResponseStatus status, String contentType, ByteBuf byteBuf);
}

View file

@ -0,0 +1,81 @@
package org.xbib.netty.http.server.util;
import java.net.URLConnection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import static java.util.Objects.requireNonNull;
public class MimeTypeUtils {
/**
* A map from extension to MIME types, which is queried before
* {@link URLConnection#guessContentTypeFromName(String)}, so that
* important extensions are always mapped to the right MIME types.
*/
private static final Map<String, String> EXTENSION_TO_MEDIA_TYPE;
static {
Map<String, String> map = new HashMap<>();
// Text files
add(map, "text/css", "css");
add(map, "text/html", "html", "htm");
add(map, "text/plain", "txt");
// Image files
add(map, "image/gif", "gif");
add(map, "image/jpeg", "jpeg", "jpg");
add(map, "image/png", "png");
add(map, "image/svg+xml", "svg", "svgz");
add(map, "image/x-icon", "ico");
// Font files
add(map, "application/x-font-ttf", "ttc", "ttf");
add(map, "application/font-woff", "woff");
add(map, "application/font-woff2", "woff2");
add(map, "application/vnd.ms-fontobject", "eot");
add(map, "font/opentype", "otf");
// JavaScript, XML, etc
add(map, "application/javascript", "js", "map");
add(map, "application/json", "json");
add(map, "application/pdf", "pdf");
add(map, "application/xhtml+xml", "xhtml", "xhtm");
add(map, "application/xml", "xml", "xsd");
add(map, "application/xml-dtd", "dtd");
EXTENSION_TO_MEDIA_TYPE = Collections.unmodifiableMap(map);
}
private static void add(Map<String, String> extensionToMediaType,
String mediaType, String... extensions) {
for (String s : extensions) {
extensionToMediaType.put(s, mediaType);
}
}
public static String guessFromPath(String path, boolean preCompressed) {
requireNonNull(path, "path");
String s = path;
// If the path is for a precompressed file, it will have an additional extension indicating the
// encoding, which we don't want to use when determining content type.
if (preCompressed) {
s = s.substring(0, s.lastIndexOf('.'));
}
int dotIdx = s.lastIndexOf('.');
int slashIdx = s.lastIndexOf('/');
if (dotIdx < 0 || slashIdx > dotIdx) {
// No extension
return null;
}
String extension = s.substring(dotIdx + 1).toLowerCase(Locale.ROOT);
String mediaType = EXTENSION_TO_MEDIA_TYPE.get(extension);
if (mediaType != null) {
return mediaType;
}
String guessedContentType = URLConnection.guessContentTypeFromName(path);
return guessedContentType != null ? guessedContentType : "application/octet-stream";
}
}

View file

@ -1,8 +1,8 @@
package org.xbib.netty.http.server.test;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.client.listener.ResponseListener;
@ -30,7 +30,7 @@ public class CleartextHttp1Test extends TestBase {
Server server = Server.builder()
.bind(httpAddress).build();
server.getDefaultVirtualServer().addContext("/", (request, response) ->
response.write(200, "text/plain", request.getRequest().content().retain()));
response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()));
server.accept();
Client client = Client.builder()
.build();
@ -63,7 +63,7 @@ public class CleartextHttp1Test extends TestBase {
//.enableDebug()
.bind(httpAddress).build();
server.getDefaultVirtualServer().addContext("/", (request, response) -> {
response.write(200, "text/plain", request.getRequest().content().retain());
response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain());
});
server.accept();
Client client = Client.builder()
@ -109,7 +109,7 @@ public class CleartextHttp1Test extends TestBase {
//.enableDebug()
.bind(httpAddress).build();
server.getDefaultVirtualServer().addContext("/", (request, response) -> {
response.write(200, "text/plain", request.getRequest().content().retain());
response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain());
});
server.accept();
Client client = Client.builder()

View file

@ -1,7 +1,7 @@
package org.xbib.netty.http.server.test;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.client.listener.ResponseListener;
@ -31,7 +31,7 @@ public class CleartextHttp2Test extends TestBase {
.bind(httpAddress)
.build();
server.getDefaultVirtualServer().addContext("/", (request, response) ->
response.write(200, "text/plain", request.getRequest().content().retain()));
response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()));
server.accept();
Client client = Client.builder()
.build();
@ -71,7 +71,7 @@ public class CleartextHttp2Test extends TestBase {
Server server = Server.builder()
.bind(httpAddress).build();
server.getDefaultVirtualServer().addContext("/", (request, response) ->
response.write(200, "text/plain", request.getRequest().content().retain()));
response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()));
//server.getDefaultVirtualServer().addContext("/", (request, response) ->
// response.write(request.getRequest().content().toString(StandardCharsets.UTF_8)));
server.accept();

View file

@ -1,10 +1,9 @@
package org.xbib.netty.http.server.test;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Ignore;
import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.client.listener.ResponseListener;
@ -53,7 +52,7 @@ public class SecureHttp1Test extends TestBase {
};
try {
server.getDefaultVirtualServer().addContext("/", (request, response) ->
response.write(200, "text/plain", request.getRequest().content().retain()));
response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()));
server.accept();
Request request = Request.get().setVersion(HttpVersion.HTTP_1_1)
.url(server.getServerConfig().getAddress().base())
@ -77,7 +76,7 @@ public class SecureHttp1Test extends TestBase {
.setSelfCert()
.bind(httpAddress).build();
server.getDefaultVirtualServer().addContext("/", (request, response) ->
response.write(200, "text/plain", request.getRequest().content().retain()));
response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()));
server.accept();
Client client = Client.builder()
.setJdkSslProvider()
@ -125,7 +124,7 @@ public class SecureHttp1Test extends TestBase {
.bind(httpAddress)
.build();
server.getDefaultVirtualServer().addContext("/", (request, response) ->
response.write(200, "text/plain", request.getRequest().content().retain())
response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())
);
server.accept();
Client client = Client.builder()

View file

@ -1,9 +1,8 @@
package org.xbib.netty.http.server.test;
import io.netty.handler.codec.http.HttpResponseStatus;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.Ignore;
import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.client.listener.ResponseListener;
@ -42,7 +41,7 @@ public class SecureHttp2Test extends TestBase {
.bind(httpAddress)
.build();
server.getDefaultVirtualServer().addContext("/", (request, response) ->
response.write(200, "text/plain", request.getRequest().content().retain()));
response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()));
server.accept();
Client client = Client.builder()
.setJdkSslProvider()
@ -86,7 +85,7 @@ public class SecureHttp2Test extends TestBase {
.bind(httpAddress)
.build();
server.getDefaultVirtualServer().addContext("/", (request, response) ->
response.write(200, "text/plain", request.getRequest().content().retain()));
response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain()));
server.accept();
Client client = Client.builder()
.setJdkSslProvider()
@ -138,7 +137,7 @@ public class SecureHttp2Test extends TestBase {
.bind(httpAddress)
.build();
server.getDefaultVirtualServer().addContext("/", (request, response) ->
response.write(200, "text/plain", request.getRequest().content().retain())
response.write(HttpResponseStatus.OK, "text/plain", request.getRequest().content().retain())
);
server.accept();
Client client = Client.builder()

View file

@ -0,0 +1,55 @@
package org.xbib.netty.http.server.test;
import io.netty.handler.codec.http.HttpVersion;
import org.junit.Test;
import org.xbib.netty.http.client.Client;
import org.xbib.netty.http.client.Request;
import org.xbib.netty.http.server.Server;
import org.xbib.netty.http.server.context.NioContextHandler;
import org.xbib.netty.http.server.context.VirtualServer;
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.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
public class StaticFileServerTest {
private static final Logger logger = Logger.getLogger(StaticFileServerTest.class.getName());
@Test
public void testStaticFileServer() throws Exception {
Path vartmp = Paths.get("/var/tmp/");
Server server = Server.builder()
.addVirtualServer(new VirtualServer().addContext("/static", new NioContextHandler(vartmp)))
.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"));
}
assertTrue(success.get());
}
}

View file

@ -1,4 +1,4 @@
package org.xbib;
package org.xbib.netty.http.server.test;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;

View file

@ -3,7 +3,6 @@ package org.xbib.netty.http.server.test;
import io.netty.buffer.UnpooledByteBufAllocator;
import org.junit.After;
import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.server.Server;
import java.io.IOException;

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.hacks;
package org.xbib.netty.http.server.test.hacks;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
@ -31,7 +31,7 @@ import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import org.junit.Ignore;
import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.server.test.TestBase;
import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture;

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.hacks;
package org.xbib.netty.http.server.test.hacks;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
@ -24,7 +24,7 @@ import org.junit.Test;
import org.xbib.netty.http.server.handler.http.HttpPipelinedRequest;
import org.xbib.netty.http.server.handler.http.HttpPipelinedResponse;
import org.xbib.netty.http.server.handler.http.HttpPipeliningHandler;
import org.xbib.TestBase;
import org.xbib.netty.http.server.test.TestBase;
import java.nio.channels.ClosedChannelException;
import java.nio.charset.StandardCharsets;

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.hacks;
package org.xbib.netty.http.server.test.hacks;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
@ -43,7 +43,7 @@ import io.netty.handler.logging.LoggingHandler;
import io.netty.util.AsciiString;
import org.junit.Ignore;
import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.server.test.TestBase;
import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture;

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.hacks;
package org.xbib.netty.http.server.test.hacks;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
@ -29,7 +29,7 @@ import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandlerBuilder;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapterBuilder;
import org.junit.Ignore;
import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.server.test.TestBase;
import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture;

View file

@ -1,4 +1,4 @@
package org.xbib.netty.http.hacks;
package org.xbib.netty.http.server.test.hacks;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
@ -37,7 +37,7 @@ import io.netty.handler.codec.http2.Http2StreamFrameToHttpObjectCodec;
import io.netty.util.AsciiString;
import org.junit.Ignore;
import org.junit.Test;
import org.xbib.TestBase;
import org.xbib.netty.http.server.test.TestBase;
import java.net.InetSocketAddress;
import java.util.concurrent.CompletableFuture;

View file

@ -1,4 +1,4 @@
/**
* Hacking Netty for showing server functions.
*/
package org.xbib.netty.http.hacks;
package org.xbib.netty.http.server.test.hacks;

View file

@ -0,0 +1,5 @@
handlers = java.util.logging.ConsoleHandler
.level = FINE
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format = %1$tFT%1$tT.%1$tL%1$tz [%4$-11s] [%3$s] %5$s %6$s%n