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

io.helidon.webclient.http1.Http1CallChainBase Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (c) 2023, 2024 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.webclient.http1;

import java.io.InputStream;
import java.io.UncheckedIOException;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;

import io.helidon.common.ParserHelper;
import io.helidon.common.buffers.BufferData;
import io.helidon.common.buffers.Bytes;
import io.helidon.common.buffers.DataReader;
import io.helidon.common.buffers.DataWriter;
import io.helidon.common.socket.HelidonSocket;
import io.helidon.common.tls.Tls;
import io.helidon.http.ClientRequestHeaders;
import io.helidon.http.ClientResponseHeaders;
import io.helidon.http.Header;
import io.helidon.http.HeaderNames;
import io.helidon.http.HeaderValues;
import io.helidon.http.Headers;
import io.helidon.http.Http1HeadersParser;
import io.helidon.http.Method;
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.webclient.api.ClientConnection;
import io.helidon.webclient.api.ClientUri;
import io.helidon.webclient.api.HttpClientConfig;
import io.helidon.webclient.api.Proxy;
import io.helidon.webclient.api.WebClientServiceRequest;
import io.helidon.webclient.api.WebClientServiceResponse;
import io.helidon.webclient.spi.WebClientService;

import static java.lang.System.Logger.Level.TRACE;
import static java.nio.charset.StandardCharsets.US_ASCII;

abstract class Http1CallChainBase implements WebClientService.Chain {
    private static final System.Logger LOGGER = System.getLogger(Http1CallChainBase.class.getName());
    private static final Supplier INVALID_SIZE_EXCEPTION_SUPPLIER =
            () -> new IllegalArgumentException("Chunk size is invalid");

    private final BufferData writeBuffer = BufferData.growing(128);
    private final HttpClientConfig clientConfig;
    private final Http1ClientProtocolConfig protocolConfig;
    private final ClientConnection connection;
    private final Http1ClientRequestImpl originalRequest;
    private final Tls tls;
    private final Proxy proxy;
    private final boolean keepAlive;
    private final CompletableFuture whenComplete;
    private final Duration timeout;
    private final Http1ClientImpl http1Client;
    private ClientConnection effectiveConnection;

    Http1CallChainBase(Http1ClientImpl http1Client,
                       Http1ClientRequestImpl clientRequest,
                       CompletableFuture whenComplete) {
        this.clientConfig = http1Client.clientConfig();
        this.protocolConfig = http1Client.protocolConfig();
        this.originalRequest = clientRequest;
        this.timeout = clientRequest.readTimeout();
        this.connection = clientRequest.connection().orElse(null);
        this.tls = clientRequest.tls();
        this.proxy = clientRequest.proxy();
        this.keepAlive = clientRequest.keepAlive();
        this.http1Client = clientRequest.http1Client();
        this.whenComplete = whenComplete;
    }

    static void writeHeaders(Headers headers, BufferData bufferData, boolean validate) {
        for (Header header : headers) {
            if (validate) {
                header.validate();
            }
            header.writeHttp1Header(bufferData);
        }
        bufferData.write(Bytes.CR_BYTE);
        bufferData.write(Bytes.LF_BYTE);
    }

    static WebClientServiceResponse createServiceResponse(HttpClientConfig clientConfig,
                                                          WebClientServiceRequest serviceRequest,
                                                          ClientConnection connection,
                                                          DataReader reader,
                                                          Status responseStatus,
                                                          ClientResponseHeaders responseHeaders,
                                                          CompletableFuture whenComplete) {
        WebClientServiceResponse.Builder builder = WebClientServiceResponse.builder();
        AtomicReference response = new AtomicReference<>();

        if (mayHaveEntity(responseStatus, responseHeaders)) {
            // this may be an entity (if content length is set to zero, we know there is no entity)
            builder.inputStream(inputStream(clientConfig,
                                            connection.helidonSocket(),
                                            response,
                                            responseHeaders,
                                            reader,
                                            whenComplete));
        }

        WebClientServiceResponse serviceResponse = builder
                .connection(connection)
                .headers(responseHeaders)
                .status(responseStatus)
                .whenComplete(whenComplete)
                .serviceRequest(serviceRequest)
                .build();

        response.set(serviceResponse);
        return serviceResponse;
    }

    @Override
    public WebClientServiceResponse proceed(WebClientServiceRequest serviceRequest) {
        // either use the explicit connection, or obtain one (keep alive or one-off)
        effectiveConnection = connection == null ? obtainConnection(serviceRequest, timeout) : connection;
        effectiveConnection.readTimeout(this.timeout);

        DataWriter writer = effectiveConnection.writer();
        DataReader reader = effectiveConnection.reader();
        ClientUri uri = serviceRequest.uri();
        ClientRequestHeaders headers = serviceRequest.headers();

        writeBuffer.clear();
        prologue(writeBuffer, serviceRequest, uri);
        headers.setIfAbsent(HeaderValues.create(HeaderNames.HOST, uri.authority()));

        return doProceed(effectiveConnection, serviceRequest, headers, writer, reader, writeBuffer);
    }

    abstract WebClientServiceResponse doProceed(ClientConnection connection,
                                                WebClientServiceRequest request,
                                                ClientRequestHeaders headers,
                                                DataWriter writer,
                                                DataReader reader,
                                                BufferData writeBuffer);

    void prologue(BufferData nonEntityData, WebClientServiceRequest request, ClientUri uri) {
        if (request.method() == Method.CONNECT) {
            // When CONNECT, the first line contains the remote host:port, in the same way as the HOST header.
            nonEntityData.writeAscii(request.method().text()
                    + " "
                    + request.headers().get(HeaderNames.HOST).get()
                    + " HTTP/1.1\r\n");
        } else {
            // When proxy is set, ensure that the request uses absolute URI because of Section 5.1.2 Request-URI in
            // https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html which states: "The absoluteURI form is REQUIRED when the
            // request is being made to a proxy."
            String absoluteUri = uri.scheme() + "://" + uri.host() + ":" + uri.port();
            InetSocketAddress uriAddress = new InetSocketAddress(uri.host(), uri.port());
            String requestUri = proxy == Proxy.noProxy()
                    || (proxy.type() == Proxy.ProxyType.HTTP && proxy.isNoHosts(uriAddress))
                    || (proxy.type() == Proxy.ProxyType.SYSTEM && !proxy.isUsingSystemProxy(absoluteUri))
                    || clientConfig.relativeUris()
                    ? "" // don't set host details, so it becomes relative URI
                    : absoluteUri;
            nonEntityData.writeAscii(request.method().text()
                    + " "
                    + requestUri
                    + uri.pathWithQueryAndFragment()
                    + " HTTP/1.1\r\n");
        }
    }

    ClientResponseHeaders readHeaders(DataReader reader) {
        WritableHeaders writable = Http1HeadersParser.readHeaders(reader,
                                                                     protocolConfig.maxHeaderSize(),
                                                                     protocolConfig.validateResponseHeaders());
        return ClientResponseHeaders.create(writable, clientConfig.mediaTypeParserMode());
    }

    HttpClientConfig clientConfig() {
        return clientConfig;
    }

    Http1ClientProtocolConfig protocolConfig() {
        return protocolConfig;
    }

    ClientConnection connection() {
        return effectiveConnection;
    }

    Http1ClientRequestImpl originalRequest() {
        return originalRequest;
    }

    CompletableFuture whenComplete() {
        return whenComplete;
    }

    protected WebClientServiceResponse readResponse(WebClientServiceRequest serviceRequest,
                                                    ClientConnection connection,
                                                    DataReader reader) {
        Status responseStatus;
        try {
            responseStatus = Http1StatusParser.readStatus(reader, protocolConfig.maxStatusLineLength());
        } catch (UncheckedIOException e) {
            // if we get a timeout or connection close, we must close the resource (as otherwise we may receive
            // data of this request on the next use of this connection
            try {
                connection.closeResource();
            } catch (Exception ex) {
                e.addSuppressed(ex);
            }
            throw e;
        }
        connection.helidonSocket().log(LOGGER, TRACE, "client received status %n%s", responseStatus);
        ClientResponseHeaders responseHeaders = readHeaders(reader);
        connection.helidonSocket().log(LOGGER, TRACE, "client received headers %n%s", responseHeaders);

        return createServiceResponse(clientConfig,
                                     serviceRequest,
                                     connection,
                                     reader,
                                     responseStatus,
                                     responseHeaders,
                                     whenComplete);
    }

    private static InputStream inputStream(HttpClientConfig clientConfig,
                                           HelidonSocket helidonSocket,
                                           AtomicReference response,
                                           ClientResponseHeaders responseHeaders,
                                           DataReader reader,
                                           CompletableFuture whenComplete) {
        ContentEncodingContext encodingSupport = clientConfig.contentEncoding();

        ContentDecoder decoder;

        if (encodingSupport.contentDecodingEnabled() && responseHeaders.contains(HeaderNames.CONTENT_ENCODING)) {
            String contentEncoding = responseHeaders.get(HeaderNames.CONTENT_ENCODING).get();
            if (encodingSupport.contentDecodingSupported(contentEncoding)) {
                decoder = encodingSupport.decoder(contentEncoding);
            } else {
                throw new IllegalStateException("Unsupported content encoding: \n"
                                                        + BufferData.create(contentEncoding.getBytes(StandardCharsets.UTF_8))
                        .debugDataHex());
            }
        } else {
            decoder = ContentDecoder.NO_OP;
        }
        if (responseHeaders.contains(HeaderNames.CONTENT_LENGTH)) {
            long length = responseHeaders.contentLength().getAsLong();
            return decoder.apply(new ContentLengthInputStream(helidonSocket, reader, whenComplete, response, length));
        } else if (responseHeaders.contains(HeaderValues.TRANSFER_ENCODING_CHUNKED)) {
            return new ChunkedInputStream(helidonSocket, reader, whenComplete, response);
        } else {
            // we assume the rest of the connection is entity (valid for HTTP/1.0, HTTP CONNECT method etc.
            return new EverythingInputStream(helidonSocket, reader, whenComplete, response);
        }
    }

    private static boolean mayHaveEntity(Status responseStatus, ClientResponseHeaders responseHeaders) {
        if (responseHeaders.contains(HeaderValues.CONTENT_LENGTH_ZERO)) {
            return false;
        }
        // Why is NOT_MODIFIED_304 not added here too?
        if (responseStatus == Status.NO_CONTENT_204) {
            return false;
        }
        if ((
                responseHeaders.contains(HeaderNames.UPGRADE)
                        && !responseHeaders.contains(HeaderValues.TRANSFER_ENCODING_CHUNKED))) {
            // this is an upgrade response and there is no entity
            return false;
        }
        // if we decide to support HTTP/1.0, we may have an entity without any headers
        // in HTTP/1.1, we should have a content encoding
        return true;
    }

    private ClientConnection obtainConnection(WebClientServiceRequest request, Duration requestReadTimeout) {
        return http1Client.connectionCache()
                .connection(http1Client,
                            requestReadTimeout,
                            tls,
                            proxy,
                            request.uri(),
                            request.headers(),
                            keepAlive);
    }

    static class ContentLengthInputStream extends InputStream {
        private final DataReader reader;
        private final Runnable entityProcessedRunnable;
        private final HelidonSocket socket;

        private BufferData currentBuffer;
        private boolean finished;
        private long remainingLength;

        ContentLengthInputStream(HelidonSocket socket,
                                 DataReader reader,
                                 CompletableFuture whenComplete,
                                 AtomicReference response,
                                 long length) {
            this.socket = socket;
            this.reader = reader;
            this.remainingLength = length;
            // we can only get the response at the time of completion, as the instance is created after this constructor
            // returns
            this.entityProcessedRunnable = () -> whenComplete.complete(response.get());
        }

        @Override
        public int read() {
            if (finished) {
                return -1;
            }
            int maxRemaining = maxRemaining(512);
            ensureBuffer(maxRemaining);
            if (finished || currentBuffer == null) {
                return -1;
            }
            int read = currentBuffer.read();
            if (read != -1) {
                remainingLength--;
            }
            return read;
        }

        @Override
        public int read(byte[] b, int off, int len) {
            if (finished) {
                return -1;
            }
            int maxRemaining = maxRemaining(len);
            ensureBuffer(maxRemaining);
            if (finished || currentBuffer == null) {
                return -1;
            }
            int read = currentBuffer.read(b, off, len);
            remainingLength -= read;
            return read;
        }

        private int maxRemaining(int estimate) {
            return Integer.min(estimate, (int) Long.min(Integer.MAX_VALUE, remainingLength));
        }

        private void ensureBuffer(int estimate) {
            if (remainingLength == 0) {
                entityProcessedRunnable.run();
                // we have fully read the entity
                finished = true;
                currentBuffer = null;
                return;
            }

            if (currentBuffer != null && !currentBuffer.consumed()) {
                return;
            }

            reader.ensureAvailable();
            int toRead = Math.min(reader.available(), estimate);

            // read between 0 and available bytes (or estimate, which is the number of requested bytes)
            currentBuffer = reader.readBuffer(toRead);
            if (currentBuffer == null || currentBuffer == BufferData.empty()) {
                entityProcessedRunnable.run();
                finished = true;
            } else {
                socket.log(LOGGER, TRACE, "client read entity buffer %n%s", currentBuffer.debugDataHex(true));
            }
        }
    }

    static class EverythingInputStream extends InputStream {
        private final HelidonSocket helidonSocket;
        private final DataReader reader;
        private final Runnable entityProcessedRunnable;

        private BufferData currentBuffer;
        private boolean finished;

        EverythingInputStream(HelidonSocket helidonSocket,
                              DataReader reader,
                              CompletableFuture whenComplete,
                              AtomicReference response) {
            this.helidonSocket = helidonSocket;
            this.reader = reader;
            // we can only get the response at the time of completion, as the instance is created after this constructor
            // returns
            this.entityProcessedRunnable = () -> whenComplete.complete(response.get());
        }

        @Override
        public int read() {
            if (finished) {
                return -1;
            }
            ensureBuffer(512);
            if (finished || currentBuffer == null) {
                return -1;
            }
            return currentBuffer.read();
        }

        @Override
        public int read(byte[] b, int off, int len) {
            if (finished) {
                return -1;
            }
            ensureBuffer(len);
            if (finished || currentBuffer == null) {
                return -1;
            }
            return currentBuffer.read(b, off, len);
        }

        private void ensureBuffer(int estimate) {
            if (currentBuffer != null && currentBuffer.available() > 0) {
                // we did not read the previous buffer fully
                return;
            }

            reader.ensureAvailable();
            int toRead = Math.min(reader.available(), estimate);

            // read between 0 and available bytes (or estimate, which is the number of requested bytes)
            currentBuffer = reader.readBuffer(toRead);
            if (currentBuffer == null || currentBuffer == BufferData.empty()) {
                entityProcessedRunnable.run();
                finished = true;
            } else {
                helidonSocket.log(LOGGER, TRACE, "client read entity buffer %n%s", currentBuffer.debugDataHex(true));
            }
        }
    }

    static class ChunkedInputStream extends InputStream {
        private final HelidonSocket helidonSocket;
        private final DataReader reader;
        private final Runnable entityProcessedRunnable;

        private BufferData currentBuffer;
        private boolean finished;

        ChunkedInputStream(HelidonSocket helidonSocket,
                           DataReader reader,
                           CompletableFuture whenComplete,
                           AtomicReference response) {
            this.helidonSocket = helidonSocket;
            this.reader = reader;
            // we can only get the response at the time of completion, as the instance is created after this constructor
            // returns
            this.entityProcessedRunnable = () -> whenComplete.complete(response.get());
        }

        @Override
        public int read() {
            if (finished) {
                return -1;
            }
            ensureBuffer();
            if (finished || currentBuffer == null) {
                return -1;
            }
            return currentBuffer.read();
        }

        @Override
        public int read(byte[] b, int off, int len) {
            if (finished) {
                return -1;
            }
            ensureBuffer();
            if (finished || currentBuffer == null) {
                return -1;
            }
            return currentBuffer.read(b, off, len);
        }

        private void ensureBuffer() {
            if (currentBuffer != null && currentBuffer.available() > 0) {
                // we did not read the previous buffer fully
                return;
            }
            // chunked encoding - I will just read each chunk fully into memory, as that is how the protocol is designed
            int endOfChunkSize = reader.findNewLine(256);
            if (endOfChunkSize == 256) {
                entityProcessedRunnable.run();
                throw new IllegalStateException("Cannot read chunked entity, end of line not found within 256 bytes:\n"
                                                        + reader.readBuffer(Math.min(reader.available(), 256)));
            }
            String hex = reader.readAsciiString(endOfChunkSize);
            reader.skip(2); // CRLF
            int length;
            try {
                length = ParserHelper.parseNonNegative(hex, 16, INVALID_SIZE_EXCEPTION_SUPPLIER);
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException("Chunk size is not a number:\n"
                                                           + BufferData.create(hex.getBytes(US_ASCII)).debugDataHex());
            }
            if (length == 0) {
                if (reader.startsWithNewLine()) {
                    // No trailers, skip second CRLF
                    reader.skip(2);
                }
                helidonSocket.log(LOGGER, TRACE, "read last (empty) chunk");
                finished = true;
                currentBuffer = null;
                entityProcessedRunnable.run();
                return;
            }

            BufferData chunk = reader.readBuffer(length);

            if (LOGGER.isLoggable(TRACE)) {
                helidonSocket.log(LOGGER, TRACE, "client read chunk\n%s", chunk.debugDataHex(true));
            }

            reader.skip(2); // trailing CRLF after each chunk
            this.currentBuffer = chunk;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy