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

org.webpieces.httpclient.impl.RequestSenderImpl Maven / Gradle / Ivy

package org.webpieces.httpclient.impl;

import static org.webpieces.httpcommon.api.Protocol.HTTP11;
import static org.webpieces.httpcommon.api.Protocol.HTTP2;

import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;

import org.webpieces.data.api.DataWrapper;
import org.webpieces.data.api.DataWrapperGenerator;
import org.webpieces.data.api.DataWrapperGeneratorFactory;
import org.webpieces.httpclient.api.HttpClientSocket;
import org.webpieces.httpcommon.api.CloseListener;
import org.webpieces.httpcommon.api.Http2ClientEngine;
import org.webpieces.httpcommon.api.Http2EngineFactory;
import org.webpieces.httpcommon.api.Protocol;
import org.webpieces.httpcommon.api.RequestId;
import org.webpieces.httpcommon.api.RequestSender;
import org.webpieces.httpcommon.api.ResponseId;
import org.webpieces.httpcommon.api.ResponseListener;
import org.webpieces.httpcommon.api.SwitchableDataListener;
import org.webpieces.httpcommon.api.SwitchableDataListenerFactory;
import org.webpieces.httpparser.api.HttpParser;
import org.webpieces.httpparser.api.Memento;
import org.webpieces.httpparser.api.common.Header;
import org.webpieces.httpparser.api.common.KnownHeaderName;
import org.webpieces.httpparser.api.dto.HttpChunk;
import org.webpieces.httpparser.api.dto.HttpPayload;
import org.webpieces.httpparser.api.dto.HttpRequest;
import org.webpieces.httpparser.api.dto.HttpResponse;
import org.webpieces.nio.api.channels.Channel;
import org.webpieces.nio.api.channels.TCPChannel;
import org.webpieces.nio.api.exceptions.NioClosedChannelException;
import org.webpieces.nio.api.handlers.DataListener;
import org.webpieces.util.logging.Logger;
import org.webpieces.util.logging.LoggerFactory;

import com.webpieces.http2parser.api.Http2Parser;
import com.webpieces.http2parser.api.Http2SettingsMap;
import com.webpieces.http2parser.api.dto.HasHeaderFragment;
import com.webpieces.http2parser.api.dto.Http2Settings;

public class RequestSenderImpl implements RequestSender {
    private static final Logger log = LoggerFactory.getLogger(RequestSenderImpl.class);
    private static DataWrapperGenerator wrapperGen = DataWrapperGeneratorFactory.createDataWrapperGenerator();

    private Protocol protocol = HTTP11;
    private SwitchableDataListener dataListener;
    private TCPChannel channel;
    private InetSocketAddress addr;

    private AtomicBoolean tryHttp2 = new AtomicBoolean(true);
    private AtomicBoolean negotiationDone = new AtomicBoolean(false);
    private AtomicBoolean negotiationStarted = new AtomicBoolean(false);
    private CompletableFuture negotiationDoneNotifier = new CompletableFuture<>();
    private Http2ClientEngine http2ClientEngine;
    private Http2Parser http2Parser;

    // HTTP 1.1
    private HttpParser httpParser;
    private class RequestAwaitingCompletion {
        ResponseListener listener;
        HttpRequest request;

        public RequestAwaitingCompletion(ResponseListener listener, HttpRequest request) {
            this.listener = listener;
            this.request = request;
        }
    }
    private ConcurrentLinkedQueue responsesToComplete = new ConcurrentLinkedQueue<>();
    private AtomicBoolean acceptingRequest = new AtomicBoolean(false);


    public RequestSenderImpl(
        HttpClientSocket socket,
        HttpParser httpParser,
        Http2Parser http2Parser,
        CloseListener closeListener,
        InetSocketAddress addr,
        TCPChannel channel,
        Http2SettingsMap http2SettingsMap
    ) {
        this.httpParser = httpParser;
        this.http2Parser = http2Parser;
        this.http2ClientEngine = Http2EngineFactory.createHttp2ClientEngine(http2Parser, channel, addr, http2SettingsMap);
        this.channel = channel;
        this.addr = addr;

        dataListener = SwitchableDataListenerFactory.createSwitchableDataListener(socket, closeListener);
        dataListener.put(HTTP2, this.http2ClientEngine.getDataListener());
        dataListener.put(HTTP11, new Http11DataListener());
    }

    public DataListener getDataListener() {
        return dataListener;
    }

    public TCPChannel getChannel() {
        return channel;
    }

    public InetSocketAddress getAddr() {
        return addr;
    }

    private void enableHttp2() {
        protocol = HTTP2;
        dataListener.setProtocol(HTTP2);
        http2ClientEngine.sendHttp2Preface();
        http2ClientEngine.sendLocalRequestedSettings();
        negotiationDone.set(true);

        // Initialize connection level flow control
        http2ClientEngine.startPing();
    }

    // These hosts can support http2, no need to try to upgrade.
    private boolean defaultToHttp2(InetSocketAddress addr, TCPChannel channel) {
        return Arrays.asList("nghttp2.org").contains(addr.getHostName());

        // TODO: check channel for ALPN 'h2'
    }

    private CompletableFuture negotiateHttpVersion(HttpRequest req, boolean isComplete, ResponseListener listener) {
        // First check if ALPN says HTTP2, in which case, set the protocol to HTTP2 and we're done
        // If we set this to true, then everyting is copacetic with http://nghttp2.org, but
        // if we set it to false and do upgrade negotation, then the first request succeeds
        // but subsequent requests give us RstStream responses from the server.
        if (defaultToHttp2(addr, channel)) { // We don't know how to check ALPN yet, but if we do, put that check here
            log.info("setting http2 because of defaultToHttp2");
            enableHttp2();
            negotiationDone.set(true);
            negotiationDoneNotifier.complete(channel);
            return actuallySendRequest(req, isComplete, listener);

        } else { // Try the HTTP1.1 upgrade technique
            log.info("attempting http11 upgrade");
            req.addHeader(new Header(KnownHeaderName.CONNECTION, "Upgrade, HTTP2-Settings"));
            req.addHeader(new Header(KnownHeaderName.UPGRADE, "h2c"));
            Http2Settings settingsFrame = this.http2ClientEngine.getLocalRequestedSettingsFrame();

            // For some reason we need to add a " " after the base64urlencoded settings to get this to work
            // against nghttp2.org ?
            // TODO: check if we still need this " " now that we are only shipping the payload and not the
            // whole frame
            byte[] settingsFrameBytes = http2Parser.marshal(settingsFrame).createByteArray();

            // strip the header
            byte[] settingsFramePayload = Arrays.copyOfRange(settingsFrameBytes, 9, settingsFrameBytes.length);
            req.addHeader(new Header(KnownHeaderName.HTTP2_SETTINGS,
                    Base64.getUrlEncoder().encodeToString(settingsFramePayload) + " "));

            CompletableFuture response = sendHttp11AndWaitForHeaders(req);

            return response.thenApply(r -> {
                if(r.getStatusLine().getStatus().getCode() != 101) {
                    log.info("upgrade failed");
                    // That didn't work, let's not try http2 and send what we have so far to the normal listener
                    tryHttp2.set(false);
                    negotiationDone.set(true);
                    negotiationDoneNotifier.complete(channel);

                    // If the response is chunked then it is probably not complete.
                    // TODO: make sure this is right. would be nicer to grab the isComplete
                    // out of the incomingResponse call to the CompletableListener in
                    // sendHttp11AndWaitForHeaders I think. We don't really support
                    // chunked requests yet anyway, so.
                    listener.incomingResponse(r, req, new ResponseId(0), !r.isHasChunkedTransferHeader());
                    // Request id is 0 for HTTP/1.1
                    return new RequestId(0);
                } else {
                    log.info("upgrade succeeded");
                    enableHttp2();

                    // Grab the leftover data out of the http11 parser and send that
                    // to the http2 engine
                    DataWrapper leftOverData = ((RequestSenderImpl.Http11DataListener) dataListener.getDataListener(HTTP11))
                            .getLeftOverData();
                    return http2ClientEngine.createInitialStream(r, req, listener, leftOverData);
                }
            });
        }
    }

    private CompletableFuture sendHttp11AndWaitForHeaders(HttpRequest request) {
        CompletableFuture future = new CompletableFuture<>();
        ResponseListener l = new CompletableListener(future, true);
        // This only works for complete requests
        sendHttp11Request(request, true, l);
        return future;
    }


    @Override
    public CompletableFuture send(HttpRequest request) {
        CompletableFuture future = new CompletableFuture<>();
        ResponseListener l = new CompletableListener(future);
        sendRequest(request, true, l);
        return future;
    }

    @Override
    public CompletableFuture sendRequest(HttpRequest request, boolean isComplete, ResponseListener listener) {
        if(acceptingRequest.get()) {
            throw new IllegalArgumentException("You can't call incoming request while in "
                    + "HTTP11 mode and a prior request is not complete");
        }

        if(!isComplete && protocol == HTTP11) {
            acceptingRequest.set(true);
        }

        return negotiateAndSendRequest(request, isComplete, listener);
    }

    @Override
    public CompletableFuture sendData(RequestId id, DataWrapper data, boolean isComplete) {
        if(protocol == HTTP11) {
            if(isComplete)
                acceptingRequest.set(false);

            // TODO: create a chunk out of the data
            throw new UnsupportedOperationException("sendData not implemented for HTTP/1.1");
        }
        else {
            return http2ClientEngine.sendData(id, data, isComplete);
        }
    }

    @Override
    public void sendTrailer(List headers, RequestId id, boolean isComplete) {
        if(protocol == HTTP11) {
            if(isComplete)
                acceptingRequest.set(false);
        }
        throw new UnsupportedOperationException("sendTrailer not implemented");
    }

    @Override
    public void failure(Throwable e) {
        // TODO: fill this in appropriately
        throw new UnsupportedOperationException();
    }

    private CompletableFuture sendHttp11Request(HttpRequest request, boolean isComplete, ResponseListener l) {
        ByteBuffer wrap = ByteBuffer.wrap(httpParser.marshalToBytes(request));

        // TODO: confirm that if isComplete is false that the transfer-encoding is chunked, otherwise throw
        if(!isComplete)
            throw new IllegalArgumentException("can only send complete requests for HTTP1.1 right now");

        //put this on the queue before the write to be completed from the listener below
        responsesToComplete.offer(new RequestAwaitingCompletion(l, request));

        log.info("sending request now. req=" + request);

        // HTTP/1.1 has request ids of 0
        return channel.write(wrap).thenApply(channel -> new RequestId(0));
    }

    private CompletableFuture negotiateAndSendRequest(HttpRequest request, boolean isComplete, ResponseListener listener) {
        ResponseListener l = new CatchResponseListener(listener);
        if (!negotiationDone.get()) {
            if (!negotiationStarted.get()) {
                negotiationStarted.set(true);
                return negotiateHttpVersion(request, isComplete, l);
            } else {
                log.info("waiting for negotiation to complete");
                return negotiationDoneNotifier.thenCompose(channel -> {
                    log.info("done waiting for negotiation to complete");
                    return actuallySendRequest(request, isComplete, l);
                });
            }
        } else {
            log.info("not waiting for negotiation at all");
            return actuallySendRequest(request, isComplete, l);
        }
    }

    private CompletableFuture actuallySendRequest(HttpRequest request, boolean isComplete, ResponseListener l) {
        if (protocol == HTTP11) {
            return sendHttp11Request(request, isComplete, l);
        } else { // HTTP2
            return http2ClientEngine.sendRequest(request, isComplete, l);
        }
    }

    public void cleanUpPendings(String msg) {
        //do we need an isClosing state and cache that future?  (I don't think so but time will tell)
        while(!responsesToComplete.isEmpty()) {
            ResponseListener listener = responsesToComplete.poll().listener;
            if(listener != null) {
                listener.failure(new NioClosedChannelException(msg+" before responses were received"));
            }
        }

        // TODO: Deal with open streams
        http2ClientEngine.cleanUpPendings(msg);
    }


    private class Http11DataListener implements DataListener {
        private boolean processingChunked = false;
        private Memento memento = httpParser.prepareToParse();

        /**
         * This is a special 'reach-in' method to let the http2 parser grab the data from the http11
         * parser that has not yet been parsed.
         *
         * @return
         */
        public DataWrapper getLeftOverData() {
            return memento.getLeftOverData();
        }

        @Override
        public void incomingData(Channel channel, ByteBuffer b) {
            log.info("http11 incomingData -> size="+b.remaining());
            DataWrapper wrapper = wrapperGen.wrapByteBuffer(b);
            memento = httpParser.parse(memento, wrapper);

            List parsedMessages = memento.getParsedMessages();
            for(HttpPayload msg : parsedMessages) {
                if(processingChunked) {
                    HttpChunk chunk = (HttpChunk) msg;
                    ResponseListener listener = responsesToComplete.peek().listener;
                    if(chunk.isLastChunk()) {
                        processingChunked = false;
                        responsesToComplete.poll();
                    }

                    listener.incomingData(chunk.getBodyNonNull(), new ResponseId(0), chunk.isLastChunk());
                } else if(!msg.isHasChunkedTransferHeader()) {
                    HttpResponse resp = (HttpResponse) msg;
                    RequestAwaitingCompletion requestAwaitingCompletion = responsesToComplete.poll();
                    ResponseListener listener = requestAwaitingCompletion.listener;
                    listener.incomingResponse(resp, requestAwaitingCompletion.request, new ResponseId(0), true);
                } else {
                    processingChunked = true;
                    HttpResponse resp = (HttpResponse) msg;
                    RequestAwaitingCompletion requestAwaitingCompletion = responsesToComplete.peek();
                    ResponseListener listener = requestAwaitingCompletion.listener;
                    listener.incomingResponse(resp, requestAwaitingCompletion.request, new ResponseId(0), false);
                }
            }
        }

        @Override
        public void farEndClosed(Channel channel) {
            cleanUpPendings("Remote end closed");
        }

        @Override
        public void failure(Channel channel, ByteBuffer data, Exception e) {
            while(!responsesToComplete.isEmpty()) {
                ResponseListener listener = responsesToComplete.poll().listener;
                if(listener != null) {
                    listener.failure(e);
                }
            }
        }

        @Override
        public void applyBackPressure(Channel channel) {

        }

        @Override
        public void releaseBackPressure(Channel channel) {
        }
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy