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

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

The newest version!
/*
 * JGrapes Event Driven Framework
 * Copyright (C) 2016-2024 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.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.Optional;
import java.util.concurrent.Callable;
import org.jdrupes.httpcodec.ClientEngine;
import org.jdrupes.httpcodec.Codec;
import org.jdrupes.httpcodec.Decoder;
import org.jdrupes.httpcodec.MessageHeader;
import org.jdrupes.httpcodec.ProtocolException;
import org.jdrupes.httpcodec.protocols.http.HttpField;
import org.jdrupes.httpcodec.protocols.http.HttpResponse;
import org.jdrupes.httpcodec.protocols.http.client.HttpRequestEncoder;
import org.jdrupes.httpcodec.protocols.http.client.HttpResponseDecoder;
import org.jdrupes.httpcodec.protocols.websocket.WsCloseFrame;
import org.jdrupes.httpcodec.protocols.websocket.WsMessageHeader;
import org.jdrupes.httpcodec.types.Converters;
import org.jgrapes.core.Channel;
import org.jgrapes.core.ClassChannel;
import org.jgrapes.core.Component;
import org.jgrapes.core.Components;
import org.jgrapes.core.Components.PoolingIndex;
import org.jgrapes.core.EventPipeline;
import org.jgrapes.core.annotation.Handler;
import org.jgrapes.core.annotation.HandlerDefinition.ChannelReplacements;
import org.jgrapes.http.events.HostUnresolved;
import org.jgrapes.http.events.HttpConnected;
import org.jgrapes.http.events.Request;
import org.jgrapes.http.events.Response;
import org.jgrapes.http.events.WebSocketClose;
import org.jgrapes.io.IOSubchannel.DefaultIOSubchannel;
import org.jgrapes.io.events.Close;
import org.jgrapes.io.events.Closed;
import org.jgrapes.io.events.IOError;
import org.jgrapes.io.events.Input;
import org.jgrapes.io.events.OpenSocketConnection;
import org.jgrapes.io.events.Output;
import org.jgrapes.io.util.ManagedBuffer;
import org.jgrapes.io.util.ManagedBufferPool;
import org.jgrapes.net.SocketIOChannel;
import org.jgrapes.net.events.ClientConnected;

/**
 * A converter component that receives and sends web application
 * layer messages and byte buffers on associated network channels.
 */
@SuppressWarnings({ "PMD.ExcessiveImports", "PMD.CouplingBetweenObjects" })
public class HttpConnector extends Component {

    private int applicationBufferSize = -1;
    private final Channel netMainChannel;
    private final Channel netSecureChannel;
    private final PoolingIndex pooled
        = new PoolingIndex<>();

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

    /**
     * Create a new connector that uses the {@code networkChannel} for network
     * level I/O.
     * 
     * @param appChannel
     *            this component's channel
     * @param networkChannel
     *            the channel for network level I/O
     * @param secureChannel
     *            the channel for secure network level I/O
     */
    public HttpConnector(Channel appChannel, Channel networkChannel,
            Channel secureChannel) {
        super(appChannel, ChannelReplacements.create()
            .add(NetworkChannel.class, networkChannel, secureChannel));
        this.netMainChannel = networkChannel;
        this.netSecureChannel = secureChannel;
    }

    /**
     * Create a new connector that uses the {@code networkChannel} for network
     * level I/O.
     * 
     * @param appChannel
     *            this component's channel
     * @param networkChannel
     *            the channel for network level I/O
     */
    public HttpConnector(Channel appChannel, Channel networkChannel) {
        super(appChannel, ChannelReplacements.create()
            .add(NetworkChannel.class, networkChannel));
        this.netMainChannel = networkChannel;
        this.netSecureChannel = null;
    }

    /**
     * Sets the size of the buffers used for {@link Input} 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 HttpConnector 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;
    }

    /**
     * Starts the processing of a request from the application layer.
     * When a network connection has been established, the application
     * layer will be informed by a {@link HttpConnected} event, fired
     * on a subchannel that is created for the processing of this
     * request.
     *
     * @param event the request
     * @throws InterruptedException if processing is interrupted
     * @throws IOException Signals that an I/O exception has occurred.
     */
    @Handler
    public void onRequest(Request.Out event)
            throws InterruptedException, IOException {
        new WebAppMsgChannel(event);
    }

    /**
     * Handles output from the application. This may be the payload
     * of e.g. a POST or data to be transferes on a websocket connection.
     *
     * @param event the event
     * @param appChannel the application layer channel
     * @throws InterruptedException the interrupted exception
     */
    @Handler
    @SuppressWarnings({ "PMD.CompareObjectsWithEquals",
        "PMD.AvoidDuplicateLiterals" })
    public void onOutput(Output event, WebAppMsgChannel appChannel)
            throws InterruptedException {
        if (appChannel.httpConnector() == this) {
            appChannel.handleAppOutput(event);
        }
    }

    /**
     * Called when the network connection is established. Triggers the
     * further processing of the initial request.
     *
     * @param event the event
     * @param netConnChannel the network layer channel
     * @throws InterruptedException if the execution is interrupted
     * @throws IOException Signals that an I/O exception has occurred.
     */
    @Handler(channels = NetworkChannel.class)
    @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
    public void onConnected(ClientConnected event,
            SocketIOChannel netConnChannel)
            throws InterruptedException, IOException {
        // Check if this is a response to our request
        var appChannel = event.openEvent().associated(WebAppMsgChannel.class)
            .filter(c -> c.httpConnector() == this);
        if (appChannel.isPresent()) {
            appChannel.get().connected(netConnChannel);
        }
    }

    /**
     * Handles I/O error events from the network layer.
     *
     * @param event the event
     * @throws IOException Signals that an I/O exception has occurred.
     */
    @Handler(channels = NetworkChannel.class)
    @SuppressWarnings("PMD.CompareObjectsWithEquals")
    public void onIoError(IOError event) throws IOException {
        for (Channel channel : event.channels()) {
            if (channel instanceof SocketIOChannel netConnChannel) {
                // Error while using established network connection
                Optional appChannel
                    = netConnChannel.associated(WebAppMsgChannel.class)
                        .filter(c -> c.httpConnector() == this);
                if (appChannel.isPresent()) {
                    // Error while using a network connection
                    appChannel.get().handleIoError(event, netConnChannel);
                    continue;
                }
                // Just in case...
                pooled.remove(netConnChannel.remoteAddress(), netConnChannel);
                continue;
            }

            // Error while trying to establish the network connection
            if (event.event() instanceof OpenSocketConnection connEvent) {
                connEvent.associated(WebAppMsgChannel.class)
                    .filter(c -> c.httpConnector() == this).ifPresent(c -> {
                        c.openError(event);
                    });
            }
        }
    }

    /**
     * Processes any input from the network layer.
     *
     * @param event the event
     * @param netConnChannel the network layer channel
     * @throws InterruptedException if the thread is interrupted
     * @throws ProtocolException if the protocol is violated
     */
    @Handler(channels = NetworkChannel.class)
    @SuppressWarnings("PMD.CompareObjectsWithEquals")
    public void onInput(Input event, SocketIOChannel netConnChannel)
            throws InterruptedException, ProtocolException {
        Optional appChannel
            = netConnChannel.associated(WebAppMsgChannel.class)
                .filter(c -> c.httpConnector() == this);
        if (appChannel.isPresent()) {
            appChannel.get().handleNetInput(event, netConnChannel);
        }
    }

    /**
     * Called when the network connection is closed. 
     *
     * @param event the event
     * @param netConnChannel the net conn channel
     */
    @Handler(channels = NetworkChannel.class)
    public void onClosed(Closed event, SocketIOChannel netConnChannel) {
        netConnChannel.associated(WebAppMsgChannel.class)
            .filter(c -> c.httpConnector() == this).ifPresent(
                appChannel -> appChannel.handleClosed(event));
        pooled.remove(netConnChannel.remoteAddress(), netConnChannel);
    }

    /**
     * Handles a close event from the application channel. Such an
     * event may only be fired if the connection has been upgraded
     * to a websocket connection.
     *
     * @param event the event
     * @param appChannel the application channel
     */
    @Handler
    @SuppressWarnings("PMD.CompareObjectsWithEquals")
    public void onClose(Close event, WebAppMsgChannel appChannel) {
        if (appChannel.httpConnector() == this) {
            appChannel.handleClose(event);
        }
    }

    /**
     * An application layer channel.
     */
    private class WebAppMsgChannel extends DefaultIOSubchannel {
        // Starts as ClientEngine but may change
        private final ClientEngine engine
            = new ClientEngine<>(new HttpRequestEncoder(),
                new HttpResponseDecoder());
        private final InetSocketAddress serverAddress;
        private final Request.Out request;
        private ManagedBuffer outBuffer;
        private ManagedBufferPool,
                ByteBuffer> byteBufferPool;
        private ManagedBufferPool,
                CharBuffer> charBufferPool;
        private ManagedBufferPool currentPool;
        private SocketIOChannel netConnChannel;
        private final EventPipeline downPipeline;
        private WsMessageHeader currentWsMessage;

        /**
         * Instantiates a new channel.
         *
         * @param event the event
         * @param netChannel the net channel
         * @throws InterruptedException 
         * @throws IOException 
         */
        @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
        public WebAppMsgChannel(Request.Out event)
                throws InterruptedException, IOException {
            super(channel(), newEventPipeline());

            // Downstream pipeline, needed even if connection fails
            downPipeline = newEventPipeline();

            // Extract request data and check host
            request = event;
            var uri = request.requestUri();
            var port = uri.getPort();
            if (port == -1) {
                if ("https".equalsIgnoreCase(uri.getScheme())) {
                    port = 443;
                } else if ("http".equalsIgnoreCase(uri.getScheme())) {
                    port = 80;
                }
            }
            serverAddress = new InetSocketAddress(uri.getHost(), port);
            if (serverAddress.isUnresolved()) {
                downPipeline.fire(new HostUnresolved(event,
                    "Host cannot be resolved."), this);
                return;
            }

            // Re-use network connection, if possible
            SocketIOChannel recycled = pooled.poll(serverAddress);
            if (recycled != null) {
                connected(recycled);
                return;
            }

            // Fire on network channel (targeting the network connector)
            // as a follow up event (using the current pipeline).
            var useSecure = uri.getScheme().equalsIgnoreCase("https")
                && netSecureChannel != null;
            fire(new OpenSocketConnection(serverAddress)
                .setAssociated(WebAppMsgChannel.class, this),
                useSecure ? netSecureChannel : netMainChannel);
        }

        private HttpConnector httpConnector() {
            return HttpConnector.this;
        }

        /**
         * Error in response to trying to open a new TCP connection.
         *
         * @param event the event
         */
        public void openError(IOError event) {
            // Already removed from connecting by caller, simply forward.
            downPipeline.fire(IOError.duplicate(event), this);
        }

        /**
         * Error from established TCP connection.
         *
         * @param event the event
         * @param netConnChannel the network channel
         */
        public void handleIoError(IOError event,
                SocketIOChannel netConnChannel) {
            downPipeline.fire(IOError.duplicate(event), this);
        }

        /**
         * Sets the network connection channel for this application channel.
         *
         * @param netConnChannel the net conn channel
         * @throws InterruptedException the interrupted exception
         * @throws IOException Signals that an I/O exception has occurred.
         */
        @SuppressWarnings("PMD.AvoidLiteralsInIfCondition")
        public final void connected(SocketIOChannel netConnChannel)
                throws InterruptedException, IOException {
            // Associate the network channel with this application channel
            this.netConnChannel = netConnChannel;
            netConnChannel.setAssociated(WebAppMsgChannel.class, this);
            request.connectedCallback().ifPresent(
                consumer -> consumer.accept(request, netConnChannel));

            // Estimate "good" application buffer size
            int bufferSize = applicationBufferSize;
            if (bufferSize <= 0) {
                bufferSize = netConnChannel.byteBufferPool().bufferSize() - 512;
                if (bufferSize < 4096) {
                    bufferSize = 4096;
                }
            }
            String channelName = Components.objectName(HttpConnector.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");

            sendMessageUpstream(request.httpRequest(), netConnChannel);

            // Forward Connected event downstream to e.g. start preparation
            // of output events for payload data.
            downPipeline.fire(new HttpConnected(request,
                netConnChannel.localAddress(), netConnChannel.remoteAddress()),
                this);
        }

        @SuppressWarnings({ "PMD.DataflowAnomalyAnalysis",
            "PMD.CognitiveComplexity", "PMD.AvoidDuplicateLiterals" })
        private void sendMessageUpstream(MessageHeader message,
                SocketIOChannel netConnChannel) {
            // Now send request as if it came from downstream (to
            // avoid confusion with output events that may be
            // generated in parallel, see below).
            responsePipeline().submit("SynchronizedResponse",
                new Callable() {

                    @Override
                    @SuppressWarnings({ "PMD.CommentRequired",
                        "PMD.AvoidBranchingStatementAsLastInLoop",
                        "PMD.AvoidDuplicateLiterals",
                        "PMD.AvoidInstantiatingObjectsInLoops" })
                    public Void call() throws InterruptedException {
                        @SuppressWarnings("unchecked")
                        ClientEngine untypedEngine
                            = (ClientEngine) engine;
                        untypedEngine.encode(message);
                        boolean hasBody = message.hasPayload();
                        while (true) {
                            outBuffer
                                = netConnChannel.byteBufferPool().acquire();
                            Codec.Result result
                                = engine.encode(Codec.EMPTY_IN,
                                    outBuffer.backingBuffer(), !hasBody);
                            if (result.isOverflow()) {
                                netConnChannel
                                    .respond(Output.fromSink(outBuffer, false));
                                continue;
                            }
                            if (hasBody) {
                                // Keep buffer with incomplete request to be
                                // further
                                // filled by subsequent Output events
                                break;
                            }
                            // Request is completely encoded
                            if (outBuffer.position() > 0) {
                                netConnChannel
                                    .respond(Output.fromSink(outBuffer, true));
                            } else {
                                outBuffer.unlockBuffer();
                            }
                            outBuffer = null;
                            if (result.closeConnection()) {
                                netConnChannel.respond(new Close());
                            }
                            break;
                        }
                        return null;
                    }
                });
        }

        @SuppressWarnings({ "PMD.CommentRequired", "PMD.CyclomaticComplexity",
            "PMD.NPathComplexity", "PMD.AvoidInstantiatingObjectsInLoops",
            "PMD.AvoidDuplicateLiterals", "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")
                ClientEngine wsEngine
                    = (ClientEngine) engine;
                currentWsMessage = new WsMessageHeader(
                    event.buffer().backingBuffer() instanceof CharBuffer,
                    true);
                wsEngine.encode(currentWsMessage);
            }
            while (input.hasRemaining() || event.isEndOfRecord()) {
                if (outBuffer == null) {
                    outBuffer = netConnChannel.byteBufferPool().acquire();
                }
                Codec.Result result = engine.encode(input,
                    outBuffer.backingBuffer(), event.isEndOfRecord());
                if (result.isOverflow()) {
                    netConnChannel.respond(Output.fromSink(outBuffer, false));
                    outBuffer = netConnChannel.byteBufferPool().acquire();
                    continue;
                }
                if (event.isEndOfRecord() || result.closeConnection()) {
                    if (outBuffer.position() > 0) {
                        netConnChannel
                            .respond(Output.fromSink(outBuffer, true));
                    } else {
                        outBuffer.unlockBuffer();
                    }
                    outBuffer = null;
                    if (result.closeConnection()) {
                        netConnChannel.respond(new Close());
                    }
                    break;
                }
            }
            if (engine.switchedTo().equals(Optional.of("websocket"))
                && event.isEndOfRecord()) {
                currentWsMessage = null;
            }
        }

        @SuppressWarnings({ "PMD.CommentRequired",
            "PMD.DataflowAnomalyAnalysis", "PMD.CognitiveComplexity" })
        public void handleNetInput(Input event,
                SocketIOChannel netConnChannel)
                throws InterruptedException, ProtocolException {
            // 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;
            Decoder.Result result;
            while (inData.hasRemaining()) {
                if (wasOverflow) {
                    // Message has (more) body
                    bodyData = currentPool.acquire();
                }
                result = engine.decode(inData,
                    bodyData == null ? null : bodyData.backingBuffer(),
                    event.isEndOfRecord());
                if (result.response().isPresent()) {
                    sendMessageUpstream(result.response().get(),
                        netConnChannel);
                    if (result.isResponseOnly()) {
                        maybeReleaseConnection(result);
                        continue;
                    }
                }
                if (result.isHeaderCompleted()) {
                    MessageHeader header
                        = engine.responseDecoder().header().get();
                    if (!handleResponseHeader(header)) {
                        maybeReleaseConnection(result);
                        break;
                    }
                }
                if (bodyData != null) {
                    if (bodyData.position() > 0) {
                        boolean eor
                            = !result.isOverflow() && !result.isUnderflow();
                        downPipeline.fire(Input.fromSink(bodyData, eor), this);
                    } else {
                        bodyData.unlockBuffer();
                    }
                    bodyData = null;
                }
                maybeReleaseConnection(result);
                wasOverflow = result.isOverflow();
            }
        }

        @SuppressWarnings("PMD.CognitiveComplexity")
        private boolean handleResponseHeader(MessageHeader response) {
            if (response instanceof HttpResponse) {
                HttpResponse httpResponse = (HttpResponse) response;
                if (httpResponse.hasPayload()) {
                    if (httpResponse.findValue(
                        HttpField.CONTENT_TYPE, Converters.MEDIA_TYPE)
                        .map(type -> "text"
                            .equalsIgnoreCase(type.value().topLevelType()))
                        .orElse(false)) {
                        currentPool = charBufferPool;
                    } else {
                        currentPool = byteBufferPool;
                    }
                }
                downPipeline.fire(new Response(httpResponse), this);
            } else if (response instanceof WsMessageHeader) {
                WsMessageHeader wsMessage = (WsMessageHeader) response;
                if (wsMessage.hasPayload()) {
                    if (wsMessage.isTextMode()) {
                        currentPool = charBufferPool;
                    } else {
                        currentPool = byteBufferPool;
                    }
                }
            } else if (response instanceof WsCloseFrame) {
                downPipeline.fire(
                    new WebSocketClose((WsCloseFrame) response, this));
            }
            return true;
        }

        private void maybeReleaseConnection(Decoder.Result result) {
            if (result.isOverflow() || result.isUnderflow()) {
                // Data remains to be processed
                return;
            }
            MessageHeader header
                = engine.responseDecoder().header().get();
            // Don't release if something follows
            if (header instanceof HttpResponse
                && ((HttpResponse) header).statusCode() % 100 == 1) {
                return;
            }
            if (engine.switchedTo().equals(Optional.of("websocket"))) {
                if (!result.closeConnection()) {
                    return;
                }
                // Is web socket close, inform application layer
                downPipeline.fire(new Closed(), this);
            }
            netConnChannel.setAssociated(WebAppMsgChannel.class, null);
            if (!result.closeConnection()) {
                // May be reused
                pooled.add(serverAddress, netConnChannel);
            }
            netConnChannel = null;
        }

        @SuppressWarnings("PMD.CommentRequired")
        public void handleClose(Close event) {
            if (engine.switchedTo().equals(Optional.of("websocket"))) {
                sendMessageUpstream(new WsCloseFrame(null, null),
                    netConnChannel);
            }
        }

        @SuppressWarnings("PMD.CommentRequired")
        public void handleClosed(Closed event) {
            if (engine.switchedTo().equals(Optional.of("websocket"))) {
                downPipeline.fire(new Closed(), this);
            }
        }

    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy