sirius.web.http.WebServerHandler Maven / Gradle / Ivy
Show all versions of sirius-web Show documentation
/*
* Made with all the love in the world
* by scireum in Remshalden, Germany
*
* Copyright by scireum GmbH
* http://www.scireum.de - [email protected]
*/
package sirius.web.http;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpContent;
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.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.multipart.Attribute;
import io.netty.handler.codec.http.multipart.HttpPostRequestDecoder;
import io.netty.handler.timeout.IdleStateEvent;
import sirius.kernel.async.CallContext;
import sirius.kernel.async.TaskContext;
import sirius.kernel.commons.PriorityCollector;
import sirius.kernel.commons.Strings;
import sirius.kernel.commons.Watch;
import sirius.kernel.di.PartCollection;
import sirius.kernel.di.std.Parts;
import sirius.kernel.health.Average;
import sirius.kernel.health.Exceptions;
import sirius.kernel.nls.NLS;
import javax.net.ssl.SSLHandshakeException;
import java.io.File;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.TimeUnit;
/**
* Handles incoming HTTP requests.
*
* Takes care of gluing together chunks, handling file uploads etc. In order to participate in handling HTTP requests,
* one has to provide a {@link WebDispatcher} rather than modifying this class.
*/
class WebServerHandler extends ChannelDuplexHandler implements ActiveHTTPConnection {
private int numKeepAlive = 15;
private HttpRequest currentRequest;
private WebContext currentContext;
private CallContext currentCall;
private volatile long connected;
private volatile long bytesIn;
private volatile long bytesOut;
private volatile long currentBytesIn;
private volatile long currentBytesOut;
private volatile long uplink;
private volatile long downlink;
private volatile long lastBandwidthUpdate;
private SocketAddress remoteAddress;
private boolean preDispatched = false;
private boolean dispatched = false;
private Average inboundLatency = new Average();
private Average processLatency = new Average();
private Watch latencyWatch;
private boolean ssl;
@Parts(WebDispatcher.class)
private static PartCollection dispatchers;
protected static WebDispatcher[] sortedDispatchers;
/*
* Sorts all available dispatchers by their priority ascending
*/
private static WebDispatcher[] computeSortedDispatchers() {
PriorityCollector collector = PriorityCollector.create();
for (WebDispatcher wd : dispatchers.getParts()) {
collector.add(wd.getPriority(), wd);
}
return collector.getData().toArray(new WebDispatcher[collector.getData().size()]);
}
protected static WebDispatcher[] getSortedDispatchers() {
if (sortedDispatchers == null) {
sortedDispatchers = computeSortedDispatchers();
}
return sortedDispatchers;
}
/**
* Creates a new instance and initializes some statistics.
*/
WebServerHandler(boolean ssl) {
this.ssl = ssl;
this.connected = System.currentTimeMillis();
}
/**
* Periodically called by {@link sirius.web.http.WebServer.BandwidthUpdater#runTimer()} to recompute the current
* bandwidth.
*/
protected void updateBandwidth() {
long now = System.currentTimeMillis();
long lastUpdate = lastBandwidthUpdate;
long deltaInSeconds = TimeUnit.SECONDS.convert(now - lastUpdate, TimeUnit.MILLISECONDS);
if (lastUpdate > 0 && deltaInSeconds > 0) {
uplink = currentBytesIn / deltaInSeconds;
downlink = currentBytesOut / deltaInSeconds;
}
currentBytesIn = 0;
currentBytesOut = 0;
lastBandwidthUpdate = now;
}
/*
* Used when this handler is bound to an incoming connection
*/
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
WebServer.addOpenConnection(this);
this.remoteAddress = ctx.channel().remoteAddress();
super.channelRegistered(ctx);
}
/*
* Get notified about each exception which occurs while processing channel events
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) throws Exception {
if (currentCall != null) {
CallContext.setCurrent(currentCall);
}
String uri = "unknown";
if (currentContext != null && currentContext.getRequest() != null) {
uri = currentContext.getRequest().uri();
}
if (e instanceof ClosedChannelException
|| e instanceof IOException
|| e instanceof SSLHandshakeException
|| e instanceof DecoderException) {
WebServer.LOG.FINE("Received an error for url: %s - %s", uri, NLS.toUserString(e));
} else {
Exceptions.handle()
.to(WebServer.LOG)
.error(e)
.withSystemErrorMessage("Received an error for %s - %s (%s)", uri);
}
try {
if (ctx.channel().isOpen()) {
ctx.channel().close();
}
} catch (Throwable t) {
Exceptions.ignore(t);
}
currentRequest = null;
}
/*
* Binds the request to the CallContext
*/
private WebContext setupContext(ChannelHandlerContext ctx, HttpRequest req) {
currentCall = CallContext.initialize();
currentCall.addToMDC("uri", req.uri());
WebContext wc = currentCall.get(WebContext.class);
if (ssl) {
wc.ssl = true;
}
wc.setCtx(ctx);
wc.setRequest(req);
currentCall.get(TaskContext.class).setSystem("HTTP").setJob(req.uri());
return wc;
}
/*
* Will be notified by the IdleStateHandler if a channel is completely idle for a certain amount of time
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (evt instanceof IdleStateEvent) {
if (currentCall != null) {
CallContext.setCurrent(currentCall);
} else {
ctx.channel().close();
return;
}
WebContext wc = currentCall.get(WebContext.class);
if (wc == null) {
ctx.channel().close();
return;
}
if (!wc.isLongCall() && !wc.responseCompleted) {
if (WebServer.LOG.isFINE()) {
WebServer.LOG.FINE("IDLE: " + wc.getRequestedURI());
}
WebServer.idleTimeouts++;
if (WebServer.idleTimeouts < 0) {
WebServer.idleTimeouts = 0;
}
ctx.channel().close();
}
}
}
/**
* Used by responses to determine if keepalive is supported.
*
* Internally we used a countdown, to limit the max number of keepalives for a connection. Calling this method
* decrements the internal counter, therefore this must not be called several times per request.
*
* @return true if keepalive is still supported, false otherwise.
*/
public boolean shouldKeepAlive() {
return numKeepAlive-- > 0;
}
/*
* Called once a connection is closed. Note that due to keep-alive approaches specified by HTTP 1.1, several
* independent requests can be handled via one connection
*/
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
cleanup();
WebServer.removeOpenConnection(this);
// Detach the CallContext we created
if (currentCall != null) {
CallContext.setCurrent(currentCall);
CallContext.detach();
}
super.channelUnregistered(ctx);
}
/*
* Notified if a new message is available
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
try {
if (latencyWatch != null) {
inboundLatency.addValue(latencyWatch.elapsed(TimeUnit.MILLISECONDS, true));
} else {
latencyWatch = Watch.start();
}
if (msg instanceof HttpRequest) {
channelReadRequest(ctx, (HttpRequest) msg);
} else if (msg instanceof LastHttpContent) {
channelReadLastHttpContent(ctx, msg);
} else if (msg instanceof HttpContent) {
channelReadHttpContent(ctx, msg);
}
} catch (Throwable t) {
String errorMessage = Exceptions.handle(WebServer.LOG, t).getMessage();
if (currentRequest != null && currentContext != null) {
try {
if (!currentContext.responseCompleted) {
currentContext.respondWith().error(HttpResponseStatus.INTERNAL_SERVER_ERROR, errorMessage);
}
} catch (Exception e) {
Exceptions.ignore(e);
}
currentRequest = null;
}
ctx.channel().close();
}
processLatency.addValue(latencyWatch.elapsed(TimeUnit.MILLISECONDS, true));
}
private void channelReadHttpContent(ChannelHandlerContext ctx, Object msg) throws IOException {
try {
if (currentRequest == null || currentCall == null) {
WebServer.LOG.FINE("Ignoring CHUNK without request: " + msg);
return;
}
boolean last = msg instanceof LastHttpContent;
if (!last) {
WebServer.chunks++;
if (WebServer.chunks < 0) {
WebServer.chunks = 0;
}
}
if (currentContext.contentHandler != null) {
CallContext.setCurrent(currentCall);
currentContext.contentHandler.handle(((HttpContent) msg).content(), last);
} else {
processContent(ctx, (HttpContent) msg);
}
} finally {
((HttpContent) msg).release();
}
}
private void channelReadLastHttpContent(ChannelHandlerContext ctx, Object msg) throws Exception {
channelReadHttpContent(ctx, msg);
if (currentRequest != null && currentCall != null) {
CallContext.setCurrent(currentCall);
if (!preDispatched) {
if (WebContext.corsAllowAll && isPreflightRequest()) {
handlePreflightRequest();
} else {
dispatch();
}
}
} else if (!preDispatched) {
WebServer.LOG.FINE("Terminating a channel for a last http content without a request: " + msg);
ctx.channel().close();
}
}
private void handlePreflightRequest() {
String requestHeaders = currentRequest.headers().get(HttpHeaderNames.ACCESS_CONTROL_REQUEST_HEADERS);
currentContext.respondWith()
.setHeader(HttpHeaderNames.ACCESS_CONTROL_ALLOW_METHODS, "GET,PUT,POST,DELETE")
.setHeader(HttpHeaderNames.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true")
.setHeader(HttpHeaderNames.ACCESS_CONTROL_ALLOW_HEADERS,
requestHeaders == null ? "" : requestHeaders)
.status(HttpResponseStatus.OK);
}
private boolean isPreflightRequest() {
if (currentRequest == null || HttpMethod.OPTIONS != currentRequest.method()) {
return false;
}
HttpHeaders headers = currentRequest.headers();
if (!headers.contains(HttpHeaderNames.ORIGIN)) {
return false;
}
return headers.contains(HttpHeaderNames.ACCESS_CONTROL_REQUEST_METHOD);
}
private void channelReadRequest(ChannelHandlerContext ctx, HttpRequest msg) {
// Reset stats
bytesIn = 0;
bytesOut = 0;
inboundLatency.getAndClearAverage();
processLatency.getAndClearAverage();
WebServer.requests++;
if (WebServer.requests < 0) {
WebServer.requests = 0;
}
// Do some housekeeping...
cleanup();
preDispatched = false;
dispatched = false;
handleRequest(ctx, msg);
}
/*
* Signals that a bad or incomplete request was received
*/
private void signalBadRequest(ChannelHandlerContext ctx) {
WebServer.clientErrors++;
if (WebServer.clientErrors < 0) {
WebServer.clientErrors = 0;
}
ctx.writeAndFlush(new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_REQUEST))
.addListener(ChannelFutureListener.CLOSE);
currentRequest = null;
}
/*
* Handles a new request - called once the first chunk of data of a request is available.
*/
private void handleRequest(ChannelHandlerContext ctx, HttpRequest req) {
try {
if (WebServer.LOG.isFINE()) {
WebServer.LOG.FINE("REQUEST: " + req.uri());
}
// Handle a bad request.
if (!req.decoderResult().isSuccess()) {
signalBadRequest(ctx);
return;
}
currentRequest = req;
currentContext = setupContext(ctx, req);
try {
if (checkIPFilter(ctx, req)) {
return;
}
handle100Continue(ctx, req);
processRequestMethod(req);
} catch (Throwable t) {
currentContext.respondWith()
.error(HttpResponseStatus.INTERNAL_SERVER_ERROR, Exceptions.handle(WebServer.LOG, t));
currentRequest = null;
}
} catch (Throwable t) {
Exceptions.handle(WebServer.LOG, t);
try {
ctx.channel().close();
} catch (Exception ex) {
Exceptions.ignore(ex);
}
cleanup();
currentRequest = null;
}
}
private void processRequestMethod(HttpRequest req) throws Exception {
if (req.method() == HttpMethod.POST || req.method() == HttpMethod.PUT) {
preDispatched = preDispatch();
if (!preDispatched) {
setupContentReceiver(req);
}
} else if (currentRequest.method() != HttpMethod.GET
&& currentRequest.method() != HttpMethod.HEAD
&& currentRequest.method() != HttpMethod.DELETE
&& currentRequest.method() != HttpMethod.OPTIONS) {
currentContext.respondWith()
.error(HttpResponseStatus.BAD_REQUEST,
Strings.apply("Cannot %s as method. Use GET, POST, PUT, HEAD, DELETE, OPTIONS",
req.method().name()));
currentRequest = null;
}
}
private void setupContentReceiver(HttpRequest req) throws IOException {
String contentType = req.headers().get(HttpHeaderNames.CONTENT_TYPE);
if (isDecodeableContent(contentType)) {
if (WebServer.LOG.isFINE()) {
WebServer.LOG.FINE("POST/PUT-FORM: " + req.uri());
}
HttpPostRequestDecoder postDecoder = new HttpPostRequestDecoder(WebServer.getHttpDataFactory(), req);
currentContext.setPostDecoder(postDecoder);
} else {
if (WebServer.LOG.isFINE()) {
WebServer.LOG.FINE("POST/PUT-DATA: " + req.uri());
}
Attribute body = WebServer.getHttpDataFactory().createAttribute(req, "body");
if (req instanceof FullHttpRequest) {
body.setContent(((FullHttpRequest) req).content().retain());
}
currentContext.content = body;
}
}
private boolean isDecodeableContent(String contentType) {
return Strings.isFilled(contentType) && (contentType.startsWith("multipart/form-data")
|| contentType.startsWith("application/x-www-form-urlencoded"));
}
private void handle100Continue(ChannelHandlerContext ctx, HttpRequest req) {
if (HttpUtil.is100ContinueExpected(req)) {
if (WebServer.LOG.isFINE()) {
WebServer.LOG.FINE("CONTINUE: " + req.uri());
}
send100Continue(ctx);
}
}
private boolean checkIPFilter(ChannelHandlerContext ctx, HttpRequest req) {
if (!WebServer.getIPFilter().accepts(currentContext.getRemoteIP())) {
WebServer.blocks++;
if (WebServer.blocks < 0) {
WebServer.blocks = 0;
}
if (WebServer.LOG.isFINE()) {
WebServer.LOG.FINE("BLOCK: " + req.uri());
}
ctx.channel().close();
return true;
}
return false;
}
/*
* Sends an 100 CONTINUE response to conform to the keepalive protocol
*/
private void send100Continue(ChannelHandlerContext e) {
if (WebServer.LOG.isFINE()) {
WebServer.LOG.FINE("100 - CONTINUE: " + currentContext.getRequestedURI());
}
HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
e.writeAndFlush(response);
}
/*
* Releases the last context (request) which was processed by this handler.
*/
private void cleanup() {
if (currentContext != null) {
currentContext.release();
currentContext = null;
}
}
/*
* Reads another chunk of data for a previously started request
*/
private void processContent(ChannelHandlerContext ctx, HttpContent chunk) {
try {
if (currentContext.getPostDecoder() != null) {
if (WebServer.LOG.isFINE()) {
WebServer.LOG.FINE("POST-CHUNK: "
+ currentContext.getRequestedURI()
+ " - "
+ chunk.content()
.readableBytes()
+ " bytes");
}
currentContext.getPostDecoder().offer(chunk);
} else if (currentContext.content != null) {
if (WebServer.LOG.isFINE()) {
WebServer.LOG.FINE("DATA-CHUNK: "
+ currentContext.getRequestedURI()
+ " - "
+ chunk.content()
.readableBytes()
+ " bytes");
}
currentContext.content.addContent(chunk.content().retain(), chunk instanceof LastHttpContent);
if (!currentContext.content.isInMemory()) {
File file = currentContext.content.getFile();
checkUploadFileLimits(file);
}
} else if (!(chunk instanceof LastHttpContent)) {
if (currentRequest.method() != HttpMethod.POST && currentRequest.method() != HttpMethod.PUT) {
currentContext.respondWith()
.error(HttpResponseStatus.BAD_REQUEST, "Only POST or PUT may sent chunked data");
currentRequest = null;
}
}
} catch (Throwable ex) {
currentContext.respondWith()
.error(HttpResponseStatus.INTERNAL_SERVER_ERROR, Exceptions.handle(WebServer.LOG, ex));
currentRequest = null;
}
}
/*
* Checks if the can still upload more date
*/
private void checkUploadFileLimits(File file) {
if (file.getFreeSpace() < WebServer.getMinUploadFreespace() && WebServer.getMinUploadFreespace() > 0) {
if (WebServer.LOG.isFINE()) {
WebServer.LOG.FINE("Not enough space to handle: " + currentContext.getRequestedURI());
}
currentContext.respondWith()
.error(HttpResponseStatus.INSUFFICIENT_STORAGE,
Exceptions.handle()
.withSystemErrorMessage(
"The web server is running out of temporary space to store the upload")
.to(WebServer.LOG)
.handle());
currentRequest = null;
}
if (file.length() > WebServer.getMaxUploadSize() && WebServer.getMaxUploadSize() > 0) {
if (WebServer.LOG.isFINE()) {
WebServer.LOG.FINE("Body is too large: " + currentContext.getRequestedURI());
}
currentContext.respondWith()
.error(HttpResponseStatus.INSUFFICIENT_STORAGE,
Exceptions.handle()
.withSystemErrorMessage(
"The uploaded file exceeds the maximal upload size of %d bytes",
WebServer.getMaxUploadSize())
.to(WebServer.LOG)
.handle());
currentRequest = null;
}
}
/*
* Tries to dispatch a POST or PUT request before it is completely received so that the handler can install
* a ContentHandler to process all incoming data.
*/
private boolean preDispatch() throws Exception {
if (WebServer.LOG.isFINE() && currentContext != null) {
WebServer.LOG.FINE("DISPATCHING: " + currentContext.getRequestedURI());
}
for (WebDispatcher wd : getSortedDispatchers()) {
try {
currentCall.get(TaskContext.class).setSubSystem(wd.getClass().getSimpleName());
if (wd.preDispatch(currentContext)) {
if (WebServer.LOG.isFINE()) {
WebServer.LOG.FINE("PRE-DISPATCHED: " + currentContext.getRequestedURI() + " to " + wd);
}
return true;
}
} catch (Exception e) {
Exceptions.handle(WebServer.LOG, e);
}
}
return false;
}
/*
* Dispatches the completely read request.
*/
private void dispatch() throws Exception {
if (WebServer.LOG.isFINE() && currentContext != null) {
WebServer.LOG.FINE("DISPATCHING: " + currentContext.getRequestedURI());
}
currentContext.started = System.currentTimeMillis();
dispatched = true;
for (WebDispatcher wd : getSortedDispatchers()) {
try {
currentCall.get(TaskContext.class).setSubSystem(wd.getClass().getSimpleName());
if (wd.dispatch(currentContext)) {
if (WebServer.LOG.isFINE()) {
WebServer.LOG.FINE("DISPATCHED: " + currentContext.getRequestedURI() + " to " + wd);
}
currentRequest = null;
return;
}
} catch (Exception e) {
Exceptions.handle(WebServer.LOG, e);
}
}
}
@Override
public int getNumKeepAlive() {
return numKeepAlive;
}
@Override
public String getURL() {
if (currentContext == null) {
return "";
}
if (currentContext.responseCompleted) {
return currentContext.getRequestedURI() + " (completed)";
} else if (currentContext.responseCommitted) {
return currentContext.getRequestedURI() + " (committed)";
} else if (preDispatched) {
return currentContext.getRequestedURI() + " (pre-dispatched)";
} else if (dispatched) {
return currentContext.getRequestedURI() + " (dispatched)";
} else {
return currentContext.getRequestedURI();
}
}
/*
* Updates inbound traffic (called via LowLevelHandler)
*/
protected void inbound(long bytes) {
bytesIn += bytes;
currentBytesIn += bytes;
}
/*
* Updates outbound traffic (called via LowLevelHandler)
*/
protected void outbound(long bytes) {
bytesOut += bytes;
currentBytesOut += bytes;
}
@Override
public String getConnectedSince() {
return TimeUnit.SECONDS.convert(System.currentTimeMillis() - connected, TimeUnit.MILLISECONDS) + "s";
}
@Override
public String getBytesIn() {
if (bytesIn == 0) {
return "-";
}
return NLS.formatSize(bytesIn);
}
@Override
public String getBytesOut() {
if (bytesOut == 0) {
return "-";
}
return NLS.formatSize(bytesOut);
}
@Override
public String getUplink() {
if (uplink == 0) {
return "-";
}
return NLS.formatSize(uplink) + "/s";
}
@Override
public String getDownlink() {
if (downlink == 0) {
return "-";
}
return NLS.formatSize(downlink) + "/s";
}
@Override
public String getLatency() {
return Strings.apply("%1.2f ms / %1.2f ms", inboundLatency.getAvg(), processLatency.getAvg());
}
@Override
public String getRemoteAddress() {
return String.valueOf(remoteAddress);
}
}