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

org.jgrapes.http.HttpServer Maven / Gradle / Ivy

The newest version!
/*
 * JGrapes Event Driven Framework
 * Copyright (C) 2016-2018 Michael N. Lipp
 * 
 * This program is free software; you can redistribute it and/or modify it 
 * under the terms of the GNU Affero General Public License as published by 
 * the Free Software Foundation; either version 3 of the License, or 
 * (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
 * for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License along 
 * with this program; if not, see .
 */

package org.jgrapes.http;

import java.lang.ref.WeakReference;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;
import org.jdrupes.httpcodec.Codec;
import org.jdrupes.httpcodec.Decoder;
import org.jdrupes.httpcodec.MessageHeader;
import org.jdrupes.httpcodec.ProtocolException;
import org.jdrupes.httpcodec.ServerEngine;
import org.jdrupes.httpcodec.protocols.http.HttpConstants.HttpStatus;
import org.jdrupes.httpcodec.protocols.http.HttpField;
import org.jdrupes.httpcodec.protocols.http.HttpRequest;
import org.jdrupes.httpcodec.protocols.http.HttpResponse;
import org.jdrupes.httpcodec.protocols.http.server.HttpRequestDecoder;
import org.jdrupes.httpcodec.protocols.http.server.HttpResponseEncoder;
import org.jdrupes.httpcodec.protocols.websocket.WsCloseFrame;
import org.jdrupes.httpcodec.protocols.websocket.WsMessageHeader;
import org.jdrupes.httpcodec.types.Converters;
import org.jdrupes.httpcodec.types.StringList;
import org.jgrapes.core.Channel;
import org.jgrapes.core.ClassChannel;
import org.jgrapes.core.Component;
import org.jgrapes.core.Components;
import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.annotation.HandlerDefinition.ChannelReplacements;
import org.jgrapes.core.internal.EventProcessor;
import org.jgrapes.http.events.ProtocolSwitchAccepted;
import org.jgrapes.http.events.Request;
import org.jgrapes.http.events.Response;
import org.jgrapes.http.events.Upgraded;
import org.jgrapes.http.events.WebSocketClose;
import org.jgrapes.io.IOSubchannel;
import org.jgrapes.io.events.Close;
import org.jgrapes.io.events.Closed;
import org.jgrapes.io.events.Input;
import org.jgrapes.io.events.Output;
import org.jgrapes.io.events.Purge;
import org.jgrapes.io.util.LinkedIOSubchannel;
import org.jgrapes.io.util.ManagedBuffer;
import org.jgrapes.io.util.ManagedBufferPool;
import org.jgrapes.net.SocketServer;
import org.jgrapes.net.events.Accepted;

/**
 * A converter component that receives and sends byte buffers on a 
 * network channel and web application layer messages on
 * {@link IOSubchannel}s of its channel. 
 * 
 * Each {@link IOSubchannel} represents a connection established by 
 * the browser. The {@link HttpServer} fires {@link Request} events 
 * (and {@link Input} events, if there is associated data) on the 
 * subchannels. Web application components (short "weblets") handle 
 * these events and use 
 * {@link LinkedIOSubchannel#respond(org.jgrapes.core.Event)}
 * to send {@link Response} events and, if applicable, {@link Output}
 * events with data belonging to the response.
 * 
 * Events must be fired by weblets while handling the {@link Request}
 * or {@link Input} events only (to be precise: while handling events 
 * processed by the associated {@link EventProcessor}) to ensure
 * that responses and their associated data do not interleave. 
 */
@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.CouplingBetweenObjects" })
public class HttpServer extends Component {

    @SuppressWarnings("PMD.SingularField")
    private WeakReference networkChannelPassBack;
    private List> providedFallbacks;
    private int matchLevels = 1;
    private boolean acceptNoSni;
    private int applicationBufferSize = -1;

    /**
     * Denotes the network channel in handler annotations.
     */
    private static final class NetworkChannel extends ClassChannel {
    }

    /**
     * Create a new server that uses the {@code networkChannel} for network
     * level I/O.
     * 

* As a convenience the server can provide fall back handlers for the * specified types of requests. The fall back handler simply returns 404 ( * "Not found"). * * @param appChannel * this component's channel * @param networkChannel * the channel for network level I/O * @param fallbacks * the requests for which a fall back handler is provided */ @SafeVarargs public HttpServer(Channel appChannel, Channel networkChannel, Class... fallbacks) { super(appChannel, ChannelReplacements.create() .add(NetworkChannel.class, networkChannel)); networkChannelPassBack = new WeakReference<>(networkChannel); this.providedFallbacks = Arrays.asList(fallbacks); } /** * Create a new server that creates its own {@link SocketServer} with * the given address and uses it for network level I/O. * * @param appChannel * this component's channel * @param serverAddress the address to listen on * @param fallbacks fall backs */ @SafeVarargs public HttpServer(Channel appChannel, InetSocketAddress serverAddress, Class... fallbacks) { this(appChannel, new SocketServer().setServerAddress(serverAddress), fallbacks); attach((SocketServer) networkChannelPassBack.get()); } /** * @return the matchLevels */ public int matchLevels() { return matchLevels; } /** * Sets the number of elements from the request path used in the match value * of the generated events (see {@link Request#defaultCriterion()}), defaults * to 1. * * @param matchLevels the matchLevels to set * @return the http server for easy chaining */ public HttpServer setMatchLevels(int matchLevels) { this.matchLevels = matchLevels; return this; } /** * Sets the size of the buffers used for {@link Output} events * on the application channel. Defaults to the upstream buffer size * minus 512 (estimate for added protocol overhead). * * @param applicationBufferSize the size to set * @return the http server for easy chaining */ public HttpServer setApplicationBufferSize(int applicationBufferSize) { this.applicationBufferSize = applicationBufferSize; return this; } /** * Returns the size of the application side (receive) buffers. * * @return the value or -1 if not set */ public int applicationBufferSize() { return applicationBufferSize; } /** * Determines if request from secure (TLS) connections without * SNI are accepted. * * Secure (TLS) requests usually transfer the name of the server that * they want to connect to during handshake. The HTTP server checks * that the `Host` header field of decoded requests matches the * name used to establish the connection. If, however, the connection * is made using the IP-address, the client does not have a host name. * If such connections are to be accepted, this flag, which * defaults to `false`, must be set. * * Note that in request accepted without SNI, the `Host` header field * will be modified to contain the IP-address of the indicated host * to prevent accidental matching with virtual host names. * * @param acceptNoSni the value to set * @return the http server for easy chaining */ public HttpServer setAcceptNoSni(boolean acceptNoSni) { this.acceptNoSni = acceptNoSni; return this; } /** * Returns if secure (TLS) requests without SNI are allowed. * * @return the result */ public boolean acceptNoSni() { return acceptNoSni; } /** * Creates a new downstream connection as {@link LinkedIOSubchannel} * of the network connection, a {@link HttpRequestDecoder} and a * {@link HttpResponseEncoder}. * * @param event * the accepted event */ @Handler(channels = NetworkChannel.class) public void onAccepted(Accepted event, IOSubchannel netChannel) { new WebAppMsgChannel(event, netChannel); } /** * Handles data from the client (from upstream). The data is send through * the {@link HttpRequestDecoder} and events are sent downstream according * to the decoding results. * * @param event the event * @throws ProtocolException if a protocol exception occurs * @throws InterruptedException */ @Handler(channels = NetworkChannel.class) public void onInput( Input event, IOSubchannel netChannel) throws ProtocolException, InterruptedException { @SuppressWarnings("unchecked") final Optional appChannel = (Optional) LinkedIOSubchannel .downstreamChannel(this, netChannel); if (appChannel.isPresent()) { appChannel.get().handleNetInput(event); } } /** * Forwards a {@link Closed} event to the application channel. * * @param event the event * @param netChannel the net channel */ @Handler(channels = NetworkChannel.class) public void onClosed(Closed event, IOSubchannel netChannel) { LinkedIOSubchannel.downstreamChannel(this, netChannel, WebAppMsgChannel.class).ifPresent(appChannel -> { appChannel.handleClosed(event); }); } /** * Forwards a {@link Purge} event to the application channel. * * @param event the event * @param netChannel the net channel */ @Handler(channels = NetworkChannel.class) public void onPurge(Purge event, IOSubchannel netChannel) { LinkedIOSubchannel.downstreamChannel(this, netChannel, WebAppMsgChannel.class).ifPresent(appChannel -> { appChannel.handlePurge(event); }); } /** * Handles a response event from downstream by sending it through an * {@link HttpResponseEncoder} that generates the data (encoded information) * and sends it upstream with {@link Output} events. Depending on whether * the response has a body, subsequent {@link Output} events can * follow. * * @param event * the response event * @throws InterruptedException if the execution was interrupted */ @Handler public void onResponse(Response event, WebAppMsgChannel appChannel) throws InterruptedException { appChannel.handleResponse(event); } /** * Receives the message body of a response. A {@link Response} event that * has a message body can be followed by one or more {@link Output} events * from downstream that contain the data. An {@code Output} event * with the end of record flag set signals the end of the message body. * * @param event * the event with the data * @throws InterruptedException if the execution was interrupted */ @Handler public void onOutput(Output event, WebAppMsgChannel appChannel) throws InterruptedException { appChannel.handleAppOutput(event); } /** * Handles a close event from downstream by closing the upstream * connections. * * @param event * the close event * @throws InterruptedException if the execution was interrupted */ @Handler public void onClose(Close event, WebAppMsgChannel appChannel) throws InterruptedException { appChannel.handleClose(event); } /** * Checks whether the request has been handled (value of {@link Request} * event set to `true`) or the status code in the prepared response * is no longer "Not Implemented". If not, but a fall back has been set, * send a "Not Found" response. If this isn't the case either, send * the default response ("Not implemented") to the client. * * @param event * the request completed event * @param appChannel the application channel * @throws InterruptedException if the execution was interrupted */ @Handler public void onRequestCompleted( Request.In.Completed event, IOSubchannel appChannel) throws InterruptedException { final Request.In requestEvent = event.event(); // A check that also works with null. if (Boolean.TRUE.equals(requestEvent.get()) || requestEvent.httpRequest().response().map( response -> response.statusCode() != HttpStatus.NOT_IMPLEMENTED .statusCode()) .orElse(false)) { // Some other component has taken care return; } // Check if "Not Found" should be sent if (providedFallbacks != null && providedFallbacks.contains(requestEvent.getClass())) { ResponseCreationSupport.sendResponse( requestEvent.httpRequest(), appChannel, HttpStatus.NOT_FOUND); return; } // Last resort ResponseCreationSupport.sendResponse(requestEvent.httpRequest(), appChannel, HttpStatus.NOT_IMPLEMENTED); } /** * Provides a fallback handler for an OPTIONS request with asterisk. Simply * responds with "OK". * * @param event the event * @param appChannel the application channel */ @Handler(priority = Integer.MIN_VALUE) public void onOptions(Request.In.Options event, IOSubchannel appChannel) { if (event.requestUri() == HttpRequest.ASTERISK_REQUEST) { HttpResponse response = event.httpRequest().response().get(); response.setStatus(HttpStatus.OK); appChannel.respond(new Response(response)); event.setResult(true); event.stop(); } } /** * Send the response indicating that the protocol switch was accepted * and causes subsequent data to be handled as {@link Input} and * {@link Output} events on the channel. * * As a convenience, the channel is associates with the URI that * was used to request the protocol switch using {@link URI} as key. * * @param event the event * @param appChannel the channel */ @Handler public void onProtocolSwitchAccepted( ProtocolSwitchAccepted event, WebAppMsgChannel appChannel) { appChannel.handleProtocolSwitchAccepted(event, appChannel); } /** * An application layer channel. */ @SuppressWarnings("PMD.DataflowAnomalyAnalysis") private class WebAppMsgChannel extends LinkedIOSubchannel { // Starts as ServerEngine but may change private final ServerEngine engine; private ManagedBuffer outBuffer; private final boolean secure; private List snis = Collections.emptyList(); private final ManagedBufferPool, ByteBuffer> byteBufferPool; private final ManagedBufferPool, CharBuffer> charBufferPool; private ManagedBufferPool currentPool; private final EventPipeline downPipeline; private Upgraded pendingUpgraded; private WsMessageHeader currentWsMessage; /** * Instantiates a new channel. * * @param event the event * @param netChannel the net channel */ @SuppressWarnings("PMD.AvoidLiteralsInIfCondition") public WebAppMsgChannel(Accepted event, IOSubchannel netChannel) { super(HttpServer.this, channel(), netChannel, newEventPipeline()); engine = new ServerEngine<>( new HttpRequestDecoder(), new HttpResponseEncoder()); secure = event.isSecure(); if (secure) { snis = new ArrayList<>(); for (SNIServerName sni : event.requestedServerNames()) { if (sni instanceof SNIHostName) { snis.add(((SNIHostName) sni).getAsciiName()); } } } // Calculate "good" application buffer size int bufferSize = applicationBufferSize; if (bufferSize <= 0) { bufferSize = netChannel.byteBufferPool().bufferSize() - 512; if (bufferSize < 4096) { bufferSize = 4096; } } String channelName = Components.objectName(HttpServer.this) + "." + Components.objectName(this); byteBufferPool().setName(channelName + ".upstream.byteBuffers"); charBufferPool().setName(channelName + ".upstream.charBuffers"); // Allocate downstream buffer pools. Note that decoding WebSocket // network packets may result in several WS frames that are each // delivered in independent events. Therefore provide some // additional buffers. final int bufSize = bufferSize; byteBufferPool = new ManagedBufferPool<>(ManagedBuffer::new, () -> { return ByteBuffer.allocate(bufSize); }, 2, 100) .setName(channelName + ".downstream.byteBuffers"); charBufferPool = new ManagedBufferPool<>(ManagedBuffer::new, () -> { return CharBuffer.allocate(bufSize); }, 2, 100) .setName(channelName + ".downstream.charBuffers"); // Downstream pipeline downPipeline = newEventPipeline(); } /** * Handle {@link Input} events from the network. * * @param event the event * @throws ProtocolException the protocol exception * @throws InterruptedException the interrupted exception */ @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidDeeplyNestedIfStmts", "PMD.CollapsibleIfStatements", "PMD.CognitiveComplexity", "PMD.AvoidDuplicateLiterals" }) public void handleNetInput(Input event) throws ProtocolException, InterruptedException { // Send the data from the event through the decoder. ByteBuffer inData = event.data(); // Don't unnecessary allocate a buffer, may be header only message ManagedBuffer bodyData = null; boolean wasOverflow = false; while (inData.hasRemaining()) { if (wasOverflow) { // Message has (more) body bodyData = currentPool.acquire(); } Decoder.Result result = engine.decode(inData, bodyData == null ? null : bodyData.backingBuffer(), event.isEndOfRecord()); if (result.response().isPresent()) { // Feedback required, send it "in sync", even if // event source is not the regular one. responsePipeline().overrideRestriction().fire( new Response(result.response().get()), this); if (result.isResponseOnly()) { maybeCloseConnection(result); continue; } } if (result.isHeaderCompleted()) { if (!handleRequestHeader(engine.currentRequest().get())) { maybeCloseConnection(result); break; } } if (bodyData != null) { if (bodyData.position() > 0) { downPipeline.fire(Input.fromSink( bodyData, !result.isOverflow() && !result.isUnderflow()), this); } else { bodyData.unlockBuffer(); } bodyData = null; } maybeCloseConnection(result); wasOverflow = result.isOverflow(); } } private void maybeCloseConnection(Decoder.Result result) { if (result.closeConnection()) { // Send close "in sync", even if event source is unexpected. responsePipeline().overrideRestriction() .fire(new Close(), this); } } @SuppressWarnings({ "PMD.CollapsibleIfStatements", "PMD.CognitiveComplexity" }) private boolean handleRequestHeader(MessageHeader request) { if (request instanceof HttpRequest) { HttpRequest httpRequest = (HttpRequest) request; if (httpRequest.hasPayload()) { if (httpRequest.findValue( HttpField.CONTENT_TYPE, Converters.MEDIA_TYPE) .map(type -> "text" .equalsIgnoreCase(type.value().topLevelType())) .orElse(false)) { currentPool = charBufferPool; } else { currentPool = byteBufferPool; } } if (secure) { if (!snis.contains(httpRequest.host())) { if (acceptNoSni && snis.isEmpty()) { convertHostToNumerical(httpRequest); } else { ResponseCreationSupport.sendResponse(httpRequest, this, 421, "Misdirected Request"); return false; } } } try { downPipeline.fire(Request.In.fromHttpRequest(httpRequest, secure, matchLevels), this); } catch (URISyntaxException e) { ResponseCreationSupport.sendResponse(httpRequest, this, 400, "Bad Request"); return false; } } else if (request instanceof WsMessageHeader) { WsMessageHeader wsMessage = (WsMessageHeader) request; if (wsMessage.hasPayload()) { if (wsMessage.isTextMode()) { currentPool = charBufferPool; } else { currentPool = byteBufferPool; } } } else if (request instanceof WsCloseFrame) { downPipeline.fire( new WebSocketClose((WsCloseFrame) request, this)); } return true; } @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.UseStringBufferForStringAppends" }) private void convertHostToNumerical(HttpRequest request) { int port = request.port(); String host; try { InetAddress addr = InetAddress.getByName( request.host()); host = addr.getHostAddress(); if (!(addr instanceof Inet4Address)) { host = "[" + host + "]"; } } catch (UnknownHostException e) { host = InetAddress.getLoopbackAddress().getHostAddress(); } request.setHostAndPort(host, port); } /** * Handle a response event from the application layer. * * @param event the event * @throws InterruptedException the interrupted exception */ @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops", "PMD.AvoidBranchingStatementAsLastInLoop", "PMD.CognitiveComplexity" }) public void handleResponse(Response event) throws InterruptedException { if (!engine.encoding() .isAssignableFrom(event.response().getClass())) { return; } final MessageHeader response = event.response(); // Start sending the response @SuppressWarnings("unchecked") ServerEngine msgEngine = (ServerEngine) engine; msgEngine.encode(response); if (pendingUpgraded != null) { if (response instanceof HttpResponse && ((HttpResponse) response).statusCode() % 100 == 1) { downPipeline.fire(pendingUpgraded, this); } pendingUpgraded = null; } boolean hasBody = response.hasPayload(); while (true) { outBuffer = upstreamChannel().byteBufferPool().acquire(); final ManagedBuffer buffer = outBuffer; Codec.Result result = engine.encode( Codec.EMPTY_IN, buffer.backingBuffer(), !hasBody); if (result.isOverflow()) { upstreamChannel().respond(Output.fromSink(buffer, false)); continue; } if (hasBody) { // Keep buffer with incomplete response to be further // filled by subsequent Output events break; } // Response is complete if (buffer.position() > 0) { upstreamChannel().respond(Output.fromSink(buffer, true)); } else { buffer.unlockBuffer(); } outBuffer = null; if (result.closeConnection()) { upstreamChannel().respond(new Close()); } break; } } /** * Handle a {@link ProtocolSwitchAccepted} event from the * application layer. * * @param event the event * @param appChannel the app channel */ public void handleProtocolSwitchAccepted( ProtocolSwitchAccepted event, WebAppMsgChannel appChannel) { appChannel.setAssociated(URI.class, event.requestEvent().requestUri()); final HttpResponse response = event.requestEvent() .httpRequest().response().get() .setStatus(HttpStatus.SWITCHING_PROTOCOLS) .setField(HttpField.UPGRADE, new StringList(event.protocol())); // We send the Upgraded event only after the response has // successfully been encoded (and thus checked). pendingUpgraded = new Upgraded(event.resourceName(), event.protocol()); respond(new Response(response)); } /** * Handle output from the application layer. * * @param event the event * @throws InterruptedException the interrupted exception */ @SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.NcssCount", "PMD.NPathComplexity", "PMD.AvoidInstantiatingObjectsInLoops", "PMD.CognitiveComplexity" }) public void handleAppOutput(Output event) throws InterruptedException { Buffer eventData = event.data(); Buffer input; if (eventData instanceof ByteBuffer) { input = ((ByteBuffer) eventData).duplicate(); } else if (eventData instanceof CharBuffer) { input = ((CharBuffer) eventData).duplicate(); } else { return; } if (engine.switchedTo().equals(Optional.of("websocket")) && currentWsMessage == null) { // When switched to WebSockets, we only have Input and Output // events. Add header automatically. @SuppressWarnings("unchecked") ServerEngine wsEngine = (ServerEngine) engine; currentWsMessage = new WsMessageHeader( event.buffer().backingBuffer() instanceof CharBuffer, true); wsEngine.encode(currentWsMessage); } while (input.hasRemaining() || event.isEndOfRecord()) { if (outBuffer == null) { outBuffer = upstreamChannel().byteBufferPool().acquire(); } Codec.Result result = engine.encode(input, outBuffer.backingBuffer(), event.isEndOfRecord()); if (result.isOverflow()) { upstreamChannel() .respond(Output.fromSink(outBuffer, false)); outBuffer = upstreamChannel().byteBufferPool().acquire(); continue; } if (event.isEndOfRecord() || result.closeConnection()) { if (outBuffer.position() > 0) { upstreamChannel() .respond(Output.fromSink(outBuffer, true)); } else { outBuffer.unlockBuffer(); } outBuffer = null; if (result.closeConnection()) { upstreamChannel().respond(new Close()); } break; } } if (engine.switchedTo().equals(Optional.of("websocket")) && event.isEndOfRecord()) { currentWsMessage = null; } } /** * Handle a {@link Close} event from the application layer. * * @param event the event * @throws InterruptedException the interrupted exception */ public void handleClose(Close event) throws InterruptedException { if (engine.switchedTo().equals(Optional.of("websocket"))) { fire(new Response(new WsCloseFrame(null, null)), this); return; } upstreamChannel().respond(new Close()); } /** * Handle a {@link Closed} event from the network by forwarding * it to the application layer. * * @param event the event */ public void handleClosed(Closed event) { downPipeline.fire(new Closed(), this); } /** * Handle a {@link Purge} event by forwarding it to the * application layer. * * @param event the event */ public void handlePurge(Purge event) { downPipeline.fire(new Purge(), this); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy