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.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.Strings;
import sirius.kernel.commons.Watch;
import sirius.kernel.di.std.ConfigValue;
import sirius.kernel.di.std.Part;
import sirius.kernel.health.Average;
import sirius.kernel.health.Exceptions;
import sirius.kernel.nls.NLS;
import sirius.web.security.UserContext;

import javax.net.ssl.SSLHandshakeException;
import java.io.File;
import java.io.IOException;
import java.net.InetSocketAddress;
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 = maxKeepalive; 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; private DispatcherPipeline pipeline; @Part private static Firewall firewall; @ConfigValue("http.maxKeepalive") private static int maxKeepalive; /** * 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 SSLHandshakeException || e.getCause() instanceof SSLHandshakeException) { SSLWebServerInitializer.LOG.FINE(e); } else if (e instanceof ClosedChannelException || e instanceof IOException || 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) .handle(); } try { if (ctx.channel().isOpen()) { ctx.channel().close(); } } catch (Exception 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 we know we're an SSL endpoint, tell the WebContext, otherwise let the null value remain // so that the automatic detection (headers set by an upstream proxy like X-Forwarded-Proto) // is performend when needed... if (this.ssl) { wc.ssl = true; } wc.setCtx(ctx); wc.setRequest(req); currentCall.get(TaskContext.class).setSystem("HTTP").setJob(wc.getRequestedURI()); // Adds a deferred handler to determine the language to i18n stuff. // If a user is present, the system will sooner or later detect it and set the appropriate // language. If not, this handler will be evaluated, check for a user in the session or // if everything else fails, parse the lang header. currentCall.deferredSetLang(callContext -> { if (!callContext.get(UserContext.class).bindUserIfPresent(wc).isPresent()) { callContext.setLangIfEmpty(NLS.makeLang(wc.getLang())); } }); 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.isLongCall() && !wc.responseCompleted) { if (WebServer.LOG.isFINE()) { WebServer.LOG.FINE("IDLE: " + wc.getRequestedURI()); } if (WebServer.idleTimeouts.incrementAndGet() < 0) { WebServer.idleTimeouts.set(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. *

* For proxies however, we don't apply any limit to permit best resource utilization. * * @return true if keepalive is still supported, false otherwise. */ public boolean shouldKeepAlive() { if (!WebServer.getProxyIPs().isEmpty()) { if (WebServer.getProxyIPs().accepts(((InetSocketAddress) this.remoteAddress).getAddress())) { return true; } } 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(msg); } } catch (Exception 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(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) { if (WebServer.chunks.incrementAndGet() < 0) { WebServer.chunks.set(0); } } if (currentContext.contentHandler != null) { CallContext.setCurrent(currentCall); currentContext.contentHandler.handle(((HttpContent) msg).content(), last); } else { processContent((HttpContent) msg); } } finally { ((HttpContent) msg).release(); } } private void channelReadLastHttpContent(ChannelHandlerContext ctx, Object msg) throws Exception { channelReadHttpContent(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.equals(currentRequest.method())) { return false; } return currentRequest.headers().contains(HttpHeaderNames.ACCESS_CONTROL_REQUEST_METHOD); } private void channelReadRequest(ChannelHandlerContext ctx, HttpRequest msg) { // Reset stats bytesIn = 0; bytesOut = 0; inboundLatency.getAndClear(); processLatency.getAndClear(); if (WebServer.requests.incrementAndGet() < 0) { WebServer.requests.set(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) { if (WebServer.clientErrors.incrementAndGet() < 0) { WebServer.clientErrors.set(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); processRequest(ctx, req); } catch (Exception t) { Exceptions.handle(WebServer.LOG, t); try { ctx.channel().close(); } catch (Exception ex) { Exceptions.ignore(ex); } cleanup(); currentRequest = null; } } private void processRequest(ChannelHandlerContext ctx, HttpRequest req) { try { if (checkIfBlockedByIPFilter(ctx, req)) { return; } handle100Continue(ctx, req); processRequestMethod(req); } catch (Exception t) { currentContext.respondWith() .error(HttpResponseStatus.INTERNAL_SERVER_ERROR, Exceptions.handle(WebServer.LOG, t)); currentRequest = null; } } private void processRequestMethod(HttpRequest req) throws Exception { if (HttpMethod.POST.equals(req.method()) || HttpMethod.PUT.equals(req.method())) { preDispatched = preDispatch(); if (!preDispatched) { setupContentReceiver(req); } } else if (!HttpMethod.GET.equals(currentRequest.method()) && !HttpMethod.HEAD.equals(currentRequest.method()) && !HttpMethod.DELETE.equals(currentRequest.method()) && !HttpMethod.OPTIONS.equals(currentRequest.method())) { 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); } } /** * Although the {@link LowLevelHandler} already checked the effective TCP remote IP, * we now check again, as the parsed request might contain a X-Forwarded-For header, * which contains the effective remote IP to verify. * * @param ctx the current channel * @param req the current request * @return true if the request was blocked, false otherwise */ private boolean checkIfBlockedByIPFilter(ChannelHandlerContext ctx, HttpRequest req) { if (isBlocked(currentContext)) { if (WebServer.blocks.incrementAndGet() < 0) { WebServer.blocks.set(0); } if (WebServer.LOG.isFINE()) { WebServer.LOG.FINE("BLOCK: " + req.uri()); } ctx.channel().close(); return true; } return false; } private boolean isBlocked(WebContext ctx) { if (!WebServer.getIPFilter().isEmpty() && WebServer.getIPFilter().accepts(ctx.getRemoteIP())) { return true; } return firewall != null && firewall.isIPBlacklisted(ctx); } /* * 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(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 (!HttpMethod.POST.equals(currentRequest.method()) && !HttpMethod.PUT.equals(currentRequest.method())) { currentContext.respondWith() .error(HttpResponseStatus.BAD_REQUEST, "Only POST or PUT may sent chunked data"); currentRequest = null; } } } catch (Exception 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; } } private DispatcherPipeline getPipeline() { if (pipeline == null) { pipeline = DispatcherPipeline.create(); } return pipeline; } /* * 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() { if (WebServer.LOG.isFINE() && currentContext != null) { WebServer.LOG.FINE("DISPATCHING: " + currentContext.getRequestedURI()); } return getPipeline().preDispatch(currentContext); } private void logPredispatched(String msg) { if (WebServer.LOG.isFINE()) { WebServer.LOG.FINE(msg); } } /* * Dispatches the completely read request. */ private void dispatch() { if (WebServer.LOG.isFINE() && currentContext != null) { WebServer.LOG.FINE("DISPATCHING: " + currentContext.getRequestedURI()); } dispatched = true; getPipeline().dispatch(currentContext); currentRequest = null; } @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() { if (currentContext != null && currentContext.isValid()) { return String.valueOf(currentContext.getRemoteIP()); } return String.valueOf(remoteAddress); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy