package org.webpieces.httpcommon.impl;
import static com.webpieces.http2parser.api.dto.lib.SettingsParameter.SETTINGS_ENABLE_PUSH;
import static com.webpieces.http2parser.api.dto.lib.SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS;
import java.net.InetSocketAddress;
import java.util.LinkedList;
import java.util.concurrent.CompletableFuture;
import org.webpieces.data.api.DataWrapper;
import org.webpieces.httpcommon.api.Http2ServerEngine;
import org.webpieces.httpcommon.api.Http2SettingsMap;
import org.webpieces.httpcommon.api.RequestId;
import org.webpieces.httpcommon.api.RequestListener;
import org.webpieces.httpcommon.api.ResponseId;
import org.webpieces.httpcommon.api.ResponseSender;
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;
public class Http2ServerEngineImpl extends Http2EngineImpl implements Http2ServerEngine {
private static final Logger log = LoggerFactory.getLogger(Http2ServerEngineImpl.class);
public Http2ServerEngineImpl(
HpackParser http2Parser,
Channel channel,
InetSocketAddress remoteAddress,
Http2SettingsMap http2SettingsMap) {
super(http2Parser, channel, remoteAddress, http2SettingsMap, HttpSide.SERVER);
}
private RequestListener requestListener;
private ResponseSender responseSender = new Http2ResponseSender(this);
@Override
public ResponseSender getResponseSender() {
return responseSender;
}
@Override
public void setRequestListener(RequestListener requestListener) {
this.requestListener = requestListener;
}
@Override
public CompletableFuture<ResponseId> sendResponse(HttpResponse response, HttpRequest request, RequestId requestId, boolean isComplete) {
Stream responseStream = activeStreams.get(requestId.getValue());
if(responseStream == null) {
if(requestId.getValue() == 0x1) {
// Create a new response stream for this stream, because this is the first response after an upgrade
responseStream = new Stream();
responseStream.setStreamId(0x1);
responseStream.setRequest(request);
initializeFlowControl(0x1);
responseStream.setStatus(Stream.StreamStatus.HALF_CLOSED_REMOTE);
activeStreams.put(0x1, responseStream);
}
else {
throw new RuntimeException("invalid request id " + requestId);
}
}
// If we already have a response stored in the responseStream then we've already sent a response for this
// stream and we need to send a push promise and create a new stream
if(responseStream.getResponse() != null) { // If push promise, do some stuff (send PUSH_PROMISE frames, set up the new stream id, etc)
log.info("creating a pushed response stream");
if(remoteSettings.get(SETTINGS_ENABLE_PUSH) == 0) {
// Enable push is not permitted, so ignore this response and return a -1 response id
log.info("push promise not permitted by client, ignoring pushed response");
return CompletableFuture.completedFuture(new ResponseId(0));
}
long openStreams = countOpenLocalOriginatedStreams();
log.info("{} streams are open originated locally", openStreams);
if(remoteSettings.get(SETTINGS_MAX_CONCURRENT_STREAMS) != null &&
openStreams >= remoteSettings.get(SETTINGS_MAX_CONCURRENT_STREAMS)) {
// Too many open streams already, so going to drop this push promise
log.info("max concurrent streams exceeded, ignoring pushed response");
return CompletableFuture.completedFuture(new ResponseId(0));
}
Stream newStream = new Stream();
newStream.setStreamId(getAndIncrementStreamId());
newStream.setResponse(response);
newStream.setRequest(request);
initializeFlowControl(newStream.getStreamId());
activeStreams.put(newStream.getStreamId(), newStream);
return sendPushPromiseFrames(requestToHeaders(request), responseStream, newStream)
.thenCompose(v -> actuallySendResponse(response, newStream, isComplete));
}
else {
responseStream.setResponse(response);
return actuallySendResponse(response, responseStream, isComplete);
}
}
private CompletableFuture<ResponseId> actuallySendResponse(HttpResponse response, Stream stream, boolean isComplete) {
return sendHeaderFrames(responseToHeaders(response), stream)
.thenAccept(v -> {
// Don't send an empty dataframe that is not completing.
if (response.getBodyNonNull().getReadableSize() != 0 || isComplete)
sendDataFrames(response.getBodyNonNull(), isComplete, stream, false);
})
.thenApply(v -> stream.getResponseId()).exceptionally(e -> {
log.error("can't send header frames", e);
return stream.getResponseId();
});
}
@Override
public CompletableFuture<Void> sendData(DataWrapper data, ResponseId responseId, boolean isComplete) {
// If the responseid is 0 then we had just rejected the sendresponse because push promise is not permitted
if(responseId.getValue() == 0) {
log.info("push promise will be rejected by client, ignoring pushed data");
return CompletableFuture.completedFuture(null);
}
Stream responseStream = activeStreams.get(responseId.getValue());
if(responseStream == null) {
// TODO: use the right exception here
throw new RuntimeException("invalid responseid: " + responseId);
}
return sendDataFrames(data, isComplete, responseStream, false);
}
@Override
void sideSpecificHandleData(DataFrame frame, int payloadLength, Stream stream) {
requestListener.incomingData(frame.getData(), stream.getRequestId(), frame.isEndOfStream(), responseSender);
}
@Override
void sideSpecificHandleHeaders(Http2Headers frame, boolean isTrailer, Stream stream) {
if(isTrailer) {
requestListener.incomingTrailer(frame.getHeaders(), stream.getRequestId(), frame.isEndOfStream(), responseSender);
} else {
HttpRequest request = requestFromHeaders(new LinkedList<>(frame.getHeaders()), stream);
checkHeaders(request.getHeaderLookupStruct(), stream);
stream.setRequest(request);
requestListener.incomingRequest(request, stream.getRequestId(), frame.isEndOfStream(), responseSender);
}
}
@Override
void sideSpecificHandleRstStream(RstStreamFrame frame, Stream stream) {
// TODO: change incomingError to failure and fix the exception types
responseSender.sendException(null);
}
@Override
public void setRemoteSettings(SettingsFrame frame, boolean sendAck) {
super.setRemoteSettings(frame, sendAck);
}
}