Skip to content

Commit

Permalink
Clean up implementation to merge the URIResolver into specific subcla…
Browse files Browse the repository at this point in the history
…sses of FileRequestHandler.

Remove support for cascading file sources.  This may be added later.
Introduce HttpError to attach an HTTP response code to exceptions
  • Loading branch information
elandau committed Aug 25, 2014
1 parent aa718e7 commit 6065b35
Show file tree
Hide file tree
Showing 16 changed files with 251 additions and 259 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
import io.netty.buffer.ByteBuf;
import io.reactivex.netty.RxNetty;
import io.reactivex.netty.protocol.http.server.HttpServer;
import io.reactivex.netty.protocol.http.server.file.ClassPathURIResolver;
import io.reactivex.netty.protocol.http.server.file.FileRequestHandler;
import io.reactivex.netty.protocol.http.server.RequestHandlerWithErrorMapper;
import io.reactivex.netty.protocol.http.server.file.FileErrorResponseMapper;
import io.reactivex.netty.protocol.http.server.file.WebappFileRequestHandler;

public class HttpFileServer {
static final int DEFAULT_PORT = 8103;
Expand All @@ -17,9 +18,9 @@ public HttpFileServer(int port) {

public HttpServer<ByteBuf, ByteBuf> createServer() {
HttpServer<ByteBuf, ByteBuf> server = RxNetty.createHttpServer(port,
new FileRequestHandler(
new ClassPathURIResolver()
));
RequestHandlerWithErrorMapper.from(
new WebappFileRequestHandler(),
new FileErrorResponseMapper()));
System.out.println("HTTP file server started...");
return server;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,12 @@ class DefaultErrorResponseGenerator<O> implements ErrorResponseGenerator<O> {

@Override
public void updateResponse(HttpServerResponse<O> response, Throwable error) {
response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
if (error instanceof HttpError) {
response.setStatus(((HttpError)error).getStatus());
}
else {
response.setStatus(HttpResponseStatus.INTERNAL_SERVER_ERROR);
}
response.getHeaders().set(HttpHeaders.Names.CONTENT_TYPE, "text/html");
ByteBuf buffer = response.getChannelHandlerContext().alloc().buffer(1024);// 1KB initial length.
PrintStream printStream = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.reactivex.netty.protocol.http.server;

import io.netty.handler.codec.http.HttpResponseStatus;

/**
* Encapsulate an exception with a specific HTTP response code
* so a proper HTTP error response may be generated.
*
* @author elandau
*
*/
public class HttpError extends Exception {
private final HttpResponseStatus status;

public HttpError(HttpResponseStatus status, String message) {
super(message);
this.status = status;
}
public HttpError(HttpResponseStatus status, String message, Throwable t) {
super(message, t);
this.status = status;
}
public HttpError(HttpResponseStatus status, Throwable t) {
super(t);
this.status = status;
}
public HttpError(HttpResponseStatus status) {
this.status = status;
}
public HttpResponseStatus getStatus() {
return status;
}

public String toString() {
return "" + status.toString() + " : " + super.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.reactivex.netty.protocol.http.server;

import rx.Observable;
import rx.functions.Func1;

/**
* Decorator for a {@link RequestHandler} with an accompanying {@link ErrorResponseGenerator}.
*
* @author elandau
*
* @param <I>
* @param <O>
*/
public class RequestHandlerWithErrorMapper<I, O> implements RequestHandler<I, O> {
private Func1<Throwable, ErrorResponseGenerator<O>> errorMapper;
private RequestHandler<I, O> handler;

public static <I, O> RequestHandlerWithErrorMapper<I, O> from(RequestHandler<I, O> handler, Func1<Throwable, ErrorResponseGenerator<O>> errorMapper) {
return new RequestHandlerWithErrorMapper<I, O>(handler, errorMapper);
}

public RequestHandlerWithErrorMapper(
RequestHandler<I, O> handler,
Func1<Throwable, ErrorResponseGenerator<O>> errorMapper) {
this.handler = handler;
this.errorMapper = errorMapper;
}

@Override
public Observable<Void> handle(HttpServerRequest<I> request, final HttpServerResponse<O> response) {
return handler.handle(request, response)
.onErrorResumeNext(new Func1<Throwable, Observable<Void>>() {
@Override
public Observable<Void> call(Throwable exception) {
// Try to redner the error with the ErrorResponseGenerator
ErrorResponseGenerator<O> generator = errorMapper.call(exception);
if (generator != null) {
generator.updateResponse(response, exception);
return Observable.empty();
}
// Defer the to default error renderer
return Observable.error(exception);
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package io.reactivex.netty.protocol.http.server.file;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.util.CharsetUtil;
import io.reactivex.netty.protocol.http.server.HttpServerResponse;
import io.reactivex.netty.protocol.http.server.RequestHandler;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
Expand All @@ -17,29 +17,15 @@

import javax.activation.MimetypesFileTypeMap;

import rx.Observable;

import com.google.common.net.HttpHeaders;

public class FileResponses {
public abstract class AbstractFileRequestHandler implements RequestHandler<ByteBuf, ByteBuf> {
public static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");

This comment has been minimized.

Copy link
@jonashall

jonashall Oct 30, 2014

The INSECURE_URI pattern matches ampersand (&) which means that query parameters will not be allowed. In my case I am using the query parameters in a javascript on a static html page. Unfortunately santizeUri is static, otherwise I could have overridden it in a subclass, but now I simply can't use this feature.

public static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
public static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
public static final String HTTP_DATE_GMT_TIMEZONE = "GMT";
public static final int HTTP_CACHE_SECONDS = 60;

/**
* Send an error response with text describing the error
*
* @param response
* @param status
* @return
*/
public static Observable<Void> sendError(HttpServerResponse<ByteBuf> response, HttpResponseStatus status) {
response.setStatus(status);
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, "text/plain; charset=UTF-8");
return response.writeAndFlush(Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8));
}

/**
* Sets the Date header for the HTTP response
*
Expand Down Expand Up @@ -89,4 +75,32 @@ public static void setContentTypeHeader(HttpServerResponse<ByteBuf> response, Fi
MimetypesFileTypeMap mimeTypesMap = new MimetypesFileTypeMap();
response.getHeaders().set(HttpHeaders.CONTENT_TYPE, mimeTypesMap.getContentType(file.getPath()));
}

public static String sanitizeUri(String uri) {
// Decode the path.
try {
uri = URLDecoder.decode(uri, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(String.format("Unable to decode URI '%s'", uri), e);
}

if (!uri.startsWith("/")) {
return null;
}

// Convert file separators.
uri = uri.replace('/', File.separatorChar);

// Simplistic dumb security check.
// You will have to do something serious in the production environment.
if (uri.contains(File.separator + '.') ||
uri.contains('.' + File.separator) ||
uri.startsWith(".") || uri.endsWith(".") ||
INSECURE_URI.matcher(uri).matches()) {
return null;
}

// Convert to absolute path.
return uri;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,16 @@
import java.net.URL;

/**
* Resolve the URL for a request URI from the classpath
* FileRequestHandler that reads files from the class path
*
* @author elandau
*
*/
public class ClassPathURIResolver implements URIResolver {
private static final String DEFAULT_PATH_PREFIX = "WEB-INF";
public class ClassPathFileRequestHandler extends FileRequestHandler {

private final String prefix;

public ClassPathURIResolver() {
this(DEFAULT_PATH_PREFIX);
}

public ClassPathURIResolver(String prefix) {
public ClassPathFileRequestHandler(String prefix) {
this.prefix = prefix;

// Remove any trailing '/'s
Expand All @@ -30,7 +25,7 @@ public ClassPathURIResolver(String prefix) {
}

@Override
public URI getUri(String path) {
protected URI resolveUri(String path) {
try {
URL url = Thread.currentThread().getContextClassLoader().getResource(prefix + path);
if (url == null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package io.reactivex.netty.protocol.http.server.file;

import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.reactivex.netty.protocol.http.server.ErrorResponseGenerator;
import io.reactivex.netty.protocol.http.server.HttpError;
import io.reactivex.netty.protocol.http.server.HttpServerResponse;
import rx.functions.Func1;

/**
* Custom error mapper to render 404 errors when serving files. All other errors
* are defered to the default handler
*
* @author elandau
*
*/
public class FileErrorResponseMapper implements Func1<Throwable, ErrorResponseGenerator<ByteBuf>> {

public static final String _404_HTML_TEMPLATE =
"<!DOCTYPE html>\n" +
"<html>\n" +
"<head>\n" +
" <title>Http Error 404</title>\n" +
"</head>\n" +
"<body>\n" +
" <h1>File not found.</h1>\n" +
"</body>\n" +
"</html>";

private static class ConstantErrorResponseGenerator<O> implements ErrorResponseGenerator<O> {
public final String template;

public ConstantErrorResponseGenerator(String template) {
this.template = template;
}

@Override
public void updateResponse(HttpServerResponse<O> response, Throwable t) {
HttpError error = (HttpError)t;
response.setStatus(error.getStatus());
response.getHeaders().set(HttpHeaders.Names.CONTENT_TYPE, "text/html");
response.writeString(template);
}
}

@Override
public ErrorResponseGenerator<ByteBuf> call(Throwable t1) {
if (t1 instanceof HttpError) {
HttpError error = (HttpError)t1;
if (error.getStatus().equals(HttpResponseStatus.NOT_FOUND)) {
return new ConstantErrorResponseGenerator<ByteBuf>(_404_HTML_TEMPLATE);
}
}
return null;
}
}
Loading

0 comments on commit 6065b35

Please sign in to comment.