All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sirius.web.http.WebServerHandler Maven / Gradle / Ivy

There is a newer version: 22.2.3
Show newest version
/*
 * 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); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy