package org.webpieces.httpcommon.impl;
import static com.webpieces.http2parser.api.dto.lib.SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.util.LinkedList;
import java.util.concurrent.CompletableFuture;
import javax.xml.bind.DatatypeConverter;
import org.webpieces.data.api.DataWrapper;
import org.webpieces.httpcommon.api.Http2ClientEngine;
import org.webpieces.httpcommon.api.Http2SettingsMap;
import org.webpieces.httpcommon.api.RequestId;
import org.webpieces.httpcommon.api.ResponseListener;
import org.webpieces.httpcommon.api.exceptions.ClientError;
import org.webpieces.httpcommon.api.exceptions.RstStreamError;
import org.webpieces.httpparser.api.dto.HttpRequest;
import org.webpieces.httpparser.api.dto.HttpResponse;
import org.webpieces.nio.api.channels.Channel;
import org.webpieces.util.logging.Logger;
import org.webpieces.util.logging.LoggerFactory;
import com.webpieces.hpack.api.HpackParser;
import com.webpieces.hpack.api.dto.Http2Headers;
import com.webpieces.http2parser.api.dto.DataFrame;
import com.webpieces.http2parser.api.dto.RstStreamFrame;
import com.webpieces.http2parser.api.dto.SettingsFrame;
import com.webpieces.http2parser.api.dto.lib.Http2Header;
public class Http2ClientEngineImpl extends Http2EngineImpl implements Http2ClientEngine {
private static final Logger log = LoggerFactory.getLogger(Http2ServerEngineImpl.class);
public Http2ClientEngineImpl(
HpackParser http2Parser,
Channel channel,
InetSocketAddress remoteAddress, Http2SettingsMap http2SettingsMap) {
super(http2Parser, channel, remoteAddress, http2SettingsMap, HttpSide.CLIENT);
}
@Override
public void cleanUpPendings(String msg) {
// TODO: deal with http2 streams to be cleaned up
}
@Override
public SettingsFrame getLocalRequestedSettingsFrame() {
return super.getLocalRequestedSettingsFrame();
}
@Override
void sideSpecificHandleData(DataFrame frame, int payloadLength, Stream stream) {
stream.getResponseListener().incomingData(frame.getData(), stream.getResponseId(), frame.isEndOfStream()).thenAccept(
length -> incrementIncomingWindow(frame.getStreamId(), payloadLength));
}
@Override
void sideSpecificHandleHeaders(Http2Headers frame, boolean isTrailer, Stream stream) {
if(isTrailer) {
stream.getResponseListener().incomingTrailer(frame.getHeaders(), stream.getResponseId(), frame.isEndOfStream());
} else {
HttpResponse response = responseFromHeaders(new LinkedList<>(frame.getHeaders()), stream);
checkHeaders(response.getHeaderLookupStruct(), stream);
stream.setResponse(response);
stream.getResponseListener().incomingResponse(response, stream.getRequest(), stream.getResponseId(), frame.isEndOfStream());
}
}
@Override
void sideSpecificHandleRstStream(RstStreamFrame frame, Stream stream) {
stream.getResponseListener().failure(new RstStreamError(frame.getKnownErrorCode(), stream.getStreamId()));
}
@Override
public void sendHttp2Preface() {
log.info("sending preface");
getUnderlyingChannel().write(ByteBuffer.wrap(DatatypeConverter.parseHexBinary(prefaceHexString)));
}
@Override
public RequestId createInitialStream(HttpResponse r, HttpRequest req, ResponseListener listener, DataWrapper leftOverData) {
int initialStreamId = getAndIncrementStreamId();
Stream initialStream = new Stream();
initialStream.setStreamId(initialStreamId);
initializeFlowControl(initialStreamId);
initialStream.setRequest(req);
initialStream.setResponseListener(listener);
initialStream.setResponse(r);
// Since we already sent the entire request as the upgrade, the stream basically starts in
// half closed local
initialStream.setStatus(Stream.StreamStatus.HALF_CLOSED_LOCAL);
activeStreams.put(initialStreamId, initialStream);
DataWrapper responseBody = r.getBodyNonNull();
// Send the content of the response to the datalistener, if any
// Not likely to happen but just in case
if(responseBody.getReadableSize() > 0)
{
log.info("got a responsebody that we're passing on to the http2 parser");
dataListener.incomingData(getUnderlyingChannel(), ByteBuffer.wrap(responseBody.createByteArray()));
}
if(leftOverData.getReadableSize() > 0)
{
log.info("got leftover data (size="+leftOverData.getReadableSize()+") that we're passing on to the http2 parser");
dataListener.incomingData(getUnderlyingChannel(), ByteBuffer.wrap(leftOverData.createByteArray()));
}
return new RequestId(initialStreamId);
}
@Override
public CompletableFuture<Void> sendData(RequestId id, DataWrapper data, boolean isComplete) {
Stream stream = activeStreams.get(id.getValue());
return sendDataFrames(data, isComplete, stream, false);
}
@Override
public CompletableFuture<RequestId> sendRequest(HttpRequest request, boolean isComplete, ResponseListener l) {
// Check if we are allowed to create a new stream
if (remoteSettings.containsKey(SETTINGS_MAX_CONCURRENT_STREAMS) &&
countOpenLocalOriginatedStreams() >= remoteSettings.get(SETTINGS_MAX_CONCURRENT_STREAMS)) {
throw new ClientError("Max concurrent streams exceeded, please wait and try again.");
// TODO: create a request queue that gets emptied when there are open streams
}
// Create a stream
Stream newStream = new Stream();
// Find a new Stream id
int thisStreamId = getAndIncrementStreamId();
newStream.setResponseListener(l);
newStream.setStreamId(thisStreamId);
newStream.setRequest(request);
initializeFlowControl(thisStreamId);
activeStreams.put(thisStreamId, newStream);
LinkedList<Http2Header> headers = requestToHeaders(request);
return sendHeaderFrames(headers, newStream)
.thenCompose(
channel -> sendDataFrames(request.getBodyNonNull(), isComplete, newStream, false))
.thenApply(channel -> new RequestId(thisStreamId));
}
}