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

io.helidon.webserver.http2.Http2ServerStream Maven / Gradle / Ivy

There is a newer version: 4.1.1
Show newest version
/*
 * Copyright (c) 2022, 2023 Oracle and/or its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.helidon.webserver.http2;

import java.io.UncheckedIOException;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Semaphore;

import io.helidon.common.buffers.BufferData;
import io.helidon.common.socket.SocketWriterException;
import io.helidon.http.DirectHandler;
import io.helidon.http.Header;
import io.helidon.http.HeaderNames;
import io.helidon.http.HeaderValues;
import io.helidon.http.Headers;
import io.helidon.http.HttpPrologue;
import io.helidon.http.RequestException;
import io.helidon.http.ServerResponseHeaders;
import io.helidon.http.Status;
import io.helidon.http.WritableHeaders;
import io.helidon.http.encoding.ContentDecoder;
import io.helidon.http.encoding.ContentEncodingContext;
import io.helidon.http.http2.ConnectionFlowControl;
import io.helidon.http.http2.Http2ErrorCode;
import io.helidon.http.http2.Http2Exception;
import io.helidon.http.http2.Http2Flag;
import io.helidon.http.http2.Http2FrameData;
import io.helidon.http.http2.Http2FrameHeader;
import io.helidon.http.http2.Http2FrameTypes;
import io.helidon.http.http2.Http2GoAway;
import io.helidon.http.http2.Http2Headers;
import io.helidon.http.http2.Http2Priority;
import io.helidon.http.http2.Http2RstStream;
import io.helidon.http.http2.Http2Settings;
import io.helidon.http.http2.Http2Stream;
import io.helidon.http.http2.Http2StreamState;
import io.helidon.http.http2.Http2StreamWriter;
import io.helidon.http.http2.Http2WindowUpdate;
import io.helidon.http.http2.StreamFlowControl;
import io.helidon.http.http2.WindowSize;
import io.helidon.webserver.CloseConnectionException;
import io.helidon.webserver.ConnectionContext;
import io.helidon.webserver.Router;
import io.helidon.webserver.http.HttpRouting;
import io.helidon.webserver.http2.spi.Http2SubProtocolSelector;
import io.helidon.webserver.http2.spi.SubProtocolResult;

import static java.lang.System.Logger.Level.TRACE;

/**
 * Server HTTP/2 stream implementation.
 */
class Http2ServerStream implements Runnable, Http2Stream {
    private static final DataFrame TERMINATING_FRAME =
            new DataFrame(Http2FrameHeader.create(0,
                                                  Http2FrameTypes.DATA,
                                                  Http2Flag.DataFlags.create(Http2Flag.DataFlags.END_OF_STREAM),
                                                  0), BufferData.empty());
    private static final System.Logger LOGGER = System.getLogger(Http2Stream.class.getName());
    private static final Set DATA_RECEIVABLE_STATES =
            Set.of(Http2StreamState.OPEN, Http2StreamState.HALF_CLOSED_LOCAL);

    private final ConnectionContext ctx;
    private final Http2Config http2Config;
    private final List subProviders;
    private final int streamId;
    private final Http2Settings serverSettings;
    private final Http2Settings clientSettings;
    private final Http2StreamWriter writer;
    private final Router router;
    private final ArrayBlockingQueue inboundData = new ArrayBlockingQueue<>(32);
    private final StreamFlowControl flowControl;
    private final Http2ConcurrentConnectionStreams streams;
    private final HttpRouting routing;

    private boolean wasLastDataFrame = false;
    private volatile Http2Headers headers;
    private volatile Http2Priority priority;
    // used from this instance and from connection
    private volatile Http2StreamState state = Http2StreamState.IDLE;
    private WriteState writeState = WriteState.INIT;
    private Http2SubProtocolSelector.SubProtocolHandler subProtocolHandler;
    private long expectedLength = -1;
    private HttpPrologue prologue;
    // create a semaphore if accessed before we get the one from connection
    // must be volatile, as it is accessed both from connection thread and from stream thread
    private volatile Semaphore requestSemaphore = new Semaphore(1);
    private boolean semaphoreAcquired;

    /**
     * A new HTTP/2 server stream.
     *
     * @param ctx                   connection context
     * @param streams
     * @param routing               HTTP routing
     * @param http2Config           HTTP/2 configuration
     * @param subProviders          HTTP/2 sub protocol selectors
     * @param streamId              stream id
     * @param serverSettings        server settings
     * @param clientSettings        client settings
     * @param writer                writer
     * @param connectionFlowControl connection flow control
     */
    Http2ServerStream(ConnectionContext ctx,
                      Http2ConcurrentConnectionStreams streams,
                      HttpRouting routing,
                      Http2Config http2Config,
                      List subProviders,
                      int streamId,
                      Http2Settings serverSettings,
                      Http2Settings clientSettings,
                      Http2StreamWriter writer,
                      ConnectionFlowControl connectionFlowControl) {
        this.ctx = ctx;
        this.streams = streams;
        this.routing = routing;
        this.http2Config = http2Config;
        this.subProviders = subProviders;
        this.streamId = streamId;
        this.serverSettings = serverSettings;
        this.clientSettings = clientSettings;
        this.writer = writer;
        this.router = ctx.router();
        this.flowControl = connectionFlowControl.createStreamFlowControl(
                streamId,
                http2Config.initialWindowSize(),
                http2Config.maxFrameSize()
        );
    }

    /**
     * Check if data can be received on this stream.
     * This method is called from connection thread.
     *
     * @throws Http2Exception in case data cannot be received
     */
    public void checkDataReceivable() throws Http2Exception {
        if (!DATA_RECEIVABLE_STATES.contains(state)) {
            throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Received data for stream "
                    + streamId + " in state " + state);
        }
    }

    /**
     * Check if headers can be received on this stream.
     * This method is called from connection thread.
     *
     * @throws Http2Exception in case headers cannot be received.
     */
    public void checkHeadersReceivable() throws Http2Exception {
        switch (state) {
        case IDLE:
            // this is OK
            break;
        case HALF_CLOSED_LOCAL:
        case HALF_CLOSED_REMOTE:
        case CLOSED:
            throw new Http2Exception(Http2ErrorCode.STREAM_CLOSED,
                                     "Stream " + streamId + " received headers when stream is " + state);
        case OPEN:
            throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Received headers for open stream " + streamId);
        default:
            throw new Http2Exception(Http2ErrorCode.INTERNAL, "Unknown stream state: " + streamId + ", state: " + state);
        }
    }

    @Override
    public boolean rstStream(Http2RstStream rstStream) {
        if (state == Http2StreamState.IDLE) {
            throw new Http2Exception(Http2ErrorCode.PROTOCOL,
                                     "Received RST_STREAM for stream "
                                             + streamId + " in IDLE state");
        }
        // TODO interrupt
        boolean rapidReset = writeState == WriteState.INIT;
        this.state = Http2StreamState.CLOSED;
        return rapidReset;
    }

    @Override
    public void windowUpdate(Http2WindowUpdate windowUpdate) {
        //5.1/3
        if (state == Http2StreamState.IDLE) {
            String msg = "Received WINDOW_UPDATE for stream " + streamId + " in state IDLE";
            Http2GoAway frame = new Http2GoAway(0, Http2ErrorCode.PROTOCOL, msg);
            writer.write(frame.toFrameData(clientSettings, 0, Http2Flag.NoFlags.create()));
            throw new Http2Exception(Http2ErrorCode.PROTOCOL, msg);
        }
        //6.9/2
        if (windowUpdate.windowSizeIncrement() == 0) {
            Http2RstStream frame = new Http2RstStream(Http2ErrorCode.PROTOCOL);
            writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()));
        }
        //6.9.1/3
        long size = flowControl.outbound().incrementStreamWindowSize(windowUpdate.windowSizeIncrement());
        if (size > WindowSize.MAX_WIN_SIZE || size < 0L) {
            Http2RstStream frame = new Http2RstStream(Http2ErrorCode.FLOW_CONTROL);
            writer.write(frame.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()));
        }
    }

    // this method is called from connection thread and start the
    // thread o this stream
    @Override
    public void headers(Http2Headers headers, boolean endOfStream) {
        this.headers = headers;
        this.state = endOfStream ? Http2StreamState.HALF_CLOSED_REMOTE : Http2StreamState.OPEN;
        if (state == Http2StreamState.HALF_CLOSED_REMOTE) {
            try {
                // we need to notify that there is no data coming
                inboundData.put(TERMINATING_FRAME);
            } catch (InterruptedException e) {
                throw new Http2Exception(Http2ErrorCode.INTERNAL, "Interrupted", e);
            }
        }
    }

    @Override
    public void data(Http2FrameHeader header, BufferData data, boolean endOfStream) {
        if (expectedLength != -1 && expectedLength < header.length()) {
            state = Http2StreamState.CLOSED;
            Http2RstStream rst = new Http2RstStream(Http2ErrorCode.PROTOCOL);
            writer.write(rst.toFrameData(clientSettings, streamId, Http2Flag.NoFlags.create()));
            return;
        }
        if (expectedLength != -1) {
            expectedLength -= header.length();
        }
        try {
            inboundData.put(new DataFrame(header, data));
        } catch (InterruptedException e) {
            throw new Http2Exception(Http2ErrorCode.INTERNAL, "Interrupted", e);
        }
    }

    @Override
    public void priority(Http2Priority http2Priority) {
        int i = http2Priority.streamId();
        if (i == this.streamId) {
            throw new Http2Exception(Http2ErrorCode.PROTOCOL, "Stream depends on itself");
        }
        this.priority = http2Priority;
    }

    @Override
    public int streamId() {
        return streamId;
    }

    @Override
    public Http2StreamState streamState() {
        return state;
    }

    @Override
    public StreamFlowControl flowControl() {
        return this.flowControl;
    }

    @Override
    public void run() {
        Thread.currentThread()
                .setName("[" + ctx.socketId() + " "
                                 + ctx.childSocketId() + " ] - " + streamId);
        try {
            handle();
        } catch (SocketWriterException | CloseConnectionException | UncheckedIOException e) {
            Http2RstStream rst = new Http2RstStream(Http2ErrorCode.STREAM_CLOSED);
            writer.write(rst.toFrameData(serverSettings, streamId, Http2Flag.NoFlags.create()));
            // no sense in throwing an exception, as this is invoked from an executor service directly
        } catch (RequestException e) {
            DirectHandler handler = ctx.listenerContext()
                    .directHandlers()
                    .handler(e.eventType());
            DirectHandler.TransportResponse response = handler.handle(e.request(),
                                                                      e.eventType(),
                                                                      e.status(),
                                                                      e.responseHeaders(),
                                                                      e);

            ServerResponseHeaders headers = response.headers();
            byte[] message = response.entity().orElse(BufferData.EMPTY_BYTES);
            if (message.length != 0) {
                headers.set(HeaderValues.create(HeaderNames.CONTENT_LENGTH, String.valueOf(message.length)));
            }
            Http2Headers http2Headers = Http2Headers.create(headers);
            if (message.length == 0) {
                writer.writeHeaders(http2Headers,
                                    streamId,
                                    Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS | Http2Flag.END_OF_STREAM),
                                    flowControl.outbound());
            } else {
                Http2FrameHeader dataHeader = Http2FrameHeader.create(message.length,
                                                                      Http2FrameTypes.DATA,
                                                                      Http2Flag.DataFlags.create(Http2Flag.END_OF_STREAM),
                                                                      streamId);
                writer.writeHeaders(http2Headers,
                                    streamId,
                                    Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS),
                                    new Http2FrameData(dataHeader, BufferData.create(message)),
                                    flowControl.outbound());
            }
        } finally {
            headers = null;
            subProtocolHandler = null;
            if (semaphoreAcquired) {
                requestSemaphore.release();
            }
        }
    }

    int writeHeaders(Http2Headers http2Headers, boolean endOfStream) {
        writeState = writeState.checkAndMove(WriteState.HEADERS_SENT);
        Http2Flag.HeaderFlags flags;
        if (endOfStream) {
            writeState = writeState.checkAndMove(WriteState.END);
            streams.remove(this.streamId);
            flags = Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS | Http2Flag.END_OF_STREAM);
        } else {
            flags = Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS);
        }

        return writer.writeHeaders(http2Headers, streamId, flags, flowControl.outbound());
    }

    int writeHeadersWithData(Http2Headers http2Headers, int contentLength, BufferData bufferData, boolean endOfStream) {
        writeState = writeState.checkAndMove(WriteState.HEADERS_SENT);
        writeState = writeState.checkAndMove(WriteState.DATA_SENT);
        if (endOfStream) {
            writeState = writeState.checkAndMove(WriteState.END);
            streams.remove(this.streamId);
        }

        Http2FrameData frameData =
                new Http2FrameData(Http2FrameHeader.create(contentLength,
                                                           Http2FrameTypes.DATA,
                                                           Http2Flag.DataFlags.create(endOfStream ? Http2Flag.END_OF_STREAM : 0),
                                                           streamId),
                                   bufferData);
        return writer.writeHeaders(http2Headers, streamId,
                                   Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS),
                                   frameData,
                                   flowControl.outbound());
    }

    int writeData(BufferData bufferData, boolean endOfStream) {
        writeState = writeState.checkAndMove(WriteState.DATA_SENT);
        if (endOfStream) {
            writeState = writeState.checkAndMove(WriteState.END);
            streams.remove(this.streamId);
        }

        Http2FrameData frameData =
                new Http2FrameData(Http2FrameHeader.create(bufferData.available(),
                                                           Http2FrameTypes.DATA,
                                                           Http2Flag.DataFlags.create(endOfStream ? Http2Flag.END_OF_STREAM : 0),
                                                           streamId),
                                   bufferData);

        writer.writeData(frameData, flowControl.outbound());
        return frameData.header().length() + Http2FrameHeader.LENGTH;
    }

    int writeTrailers(Http2Headers http2trailers) {
        writeState = writeState.checkAndMove(WriteState.TRAILERS_SENT);
        streams.remove(this.streamId);

        return writer.writeHeaders(http2trailers,
                                          streamId,
                                          Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS | Http2Flag.END_OF_STREAM),
                                          flowControl.outbound());
    }

    void write100Continue() {
        if (writeState == WriteState.EXPECTED_100) {
            writeState = writeState.checkAndMove(WriteState.CONTINUE_100_SENT);

            Header status = HeaderValues.createCached(Http2Headers.STATUS_NAME, 100);
            Http2Headers http2Headers = Http2Headers.create(WritableHeaders.create().add(status));
            writer.writeHeaders(http2Headers,
                                streamId,
                                Http2Flag.HeaderFlags.create(Http2Flag.END_OF_HEADERS),
                                flowControl.outbound());
        }
    }

    void requestSemaphore(Semaphore requestSemaphore) {
        this.requestSemaphore = requestSemaphore;
    }

    void prologue(HttpPrologue prologue) {
        this.prologue = prologue;
    }

    ConnectionContext connectionContext() {
        return this.ctx;
    }

    private BufferData readEntityFromPipeline() {
        write100Continue();
        if (wasLastDataFrame) {
            return BufferData.empty();
        }

        DataFrame frame;
        try {
            frame = inboundData.take();
            flowControl.inbound().incrementWindowSize(frame.header().length());
        } catch (InterruptedException e) {
            // this stream was interrupted, does not make sense to do anything else
            return BufferData.empty();
        }

        if (frame.header().flags(Http2FrameTypes.DATA).endOfStream()) {
            wasLastDataFrame = true;
        }
        return frame.data();
    }

    private void handle() {
        Headers httpHeaders = headers.httpHeaders();
        if (httpHeaders.contains(HeaderNames.CONTENT_LENGTH)) {
            this.expectedLength = httpHeaders.get(HeaderNames.CONTENT_LENGTH).get(long.class);
        }
        if (headers.httpHeaders().contains(HeaderValues.EXPECT_100)) {
            writeState = writeState.checkAndMove(WriteState.EXPECTED_100);
        }

        subProtocolHandler = null;

        for (Http2SubProtocolSelector provider : subProviders) {
            SubProtocolResult subProtocolResult = provider.subProtocol(ctx,
                                                                       prologue,
                                                                       headers,
                                                                       writer,
                                                                       streamId,
                                                                       serverSettings,
                                                                       clientSettings,
                                                                       flowControl,
                                                                       state,
                                                                       router);
            if (subProtocolResult.supported()) {
                subProtocolHandler = subProtocolResult.subProtocol();
                break;
            }
        }

        if (subProtocolHandler == null) {
            ContentEncodingContext contentEncodingContext = ctx.listenerContext().contentEncodingContext();
            ContentDecoder decoder;
            if (contentEncodingContext.contentDecodingEnabled()) {
                if (httpHeaders.contains(HeaderNames.CONTENT_ENCODING)) {
                    String contentEncoding = httpHeaders.get(HeaderNames.CONTENT_ENCODING).get();
                    if (contentEncodingContext.contentDecodingSupported(contentEncoding)) {
                        decoder = contentEncodingContext.decoder(contentEncoding);
                    } else {
                        throw RequestException.builder()
                                .type(DirectHandler.EventType.OTHER)
                                .status(Status.UNSUPPORTED_MEDIA_TYPE_415)
                                .message("Unsupported content encoding")
                                .build();
                    }
                } else {
                    decoder = ContentDecoder.NO_OP;
                }
            } else {
                decoder = ContentDecoder.NO_OP;
            }

            Http2ServerRequest request = Http2ServerRequest.create(ctx,
                                                                   routing.security(),
                                                                   prologue,
                                                                   headers,
                                                                   decoder,
                                                                   streamId,
                                                                   this::readEntityFromPipeline);
            Http2ServerResponse response = new Http2ServerResponse(this, request);
            semaphoreAcquired = requestSemaphore.tryAcquire();
            try {
                if (semaphoreAcquired) {
                    routing.route(ctx, request, response);
                } else {
                    ctx.log(LOGGER, TRACE, "Too many concurrent requests, rejecting request.");
                    response.status(Status.SERVICE_UNAVAILABLE_503)
                            .send("Too Many Concurrent Requests");
                    response.commit();
                }
            } finally {
                request.content().consume();
                if (this.state == Http2StreamState.HALF_CLOSED_REMOTE) {
                    this.state = Http2StreamState.CLOSED;
                } else {
                    this.state = Http2StreamState.HALF_CLOSED_LOCAL;
                }
            }
        } else {
            subProtocolHandler.init();
            while (subProtocolHandler.streamState() != Http2StreamState.CLOSED
                    && subProtocolHandler.streamState() != Http2StreamState.HALF_CLOSED_LOCAL) {
                DataFrame frame;
                try {
                    frame = inboundData.take();
                    flowControl.inbound().incrementWindowSize(frame.header().length());
                } catch (InterruptedException e) {
                    // this stream was interrupted, does not make sense to do anything else
                    String handlerName = subProtocolHandler.getClass().getSimpleName();
                    ctx.log(LOGGER, System.Logger.Level.DEBUG, "%s interrupted stream %d", handlerName, streamId);
                    return;
                }
                subProtocolHandler.data(frame.header, frame.data);
                this.state = subProtocolHandler.streamState();
            }
        }
    }

    private record DataFrame(Http2FrameHeader header, BufferData data) { }

    private enum WriteState {
        END,
        TRAILERS_SENT(END),
        DATA_SENT(TRAILERS_SENT, END),
        HEADERS_SENT(DATA_SENT, TRAILERS_SENT, END),
        CONTINUE_100_SENT(HEADERS_SENT),
        EXPECTED_100(CONTINUE_100_SENT, HEADERS_SENT),
        INIT(EXPECTED_100, HEADERS_SENT);

        private final Set allowedTransitions;

        WriteState(WriteState... allowedTransitions){
            this.allowedTransitions = Set.of(allowedTransitions);
        }

        WriteState checkAndMove(WriteState newState) {
            if (this == newState || allowedTransitions.contains(newState)) {
                return newState;
            }
            throw new IllegalStateException("Transition from " + this + " to " + newState + " is not allowed!");
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy