package org.webpieces.httpcommon.impl; import static com.webpieces.http2parser.api.dto.lib.SettingsParameter.SETTINGS_MAX_CONCURRENT_STREAMS; import static com.webpieces.http2parser.api.dto.lib.SettingsParameter.SETTINGS_MAX_FRAME_SIZE; import static org.webpieces.httpcommon.api.Http2Engine.HttpSide.SERVER; import static org.webpieces.httpcommon.impl.Stream.StreamStatus.CLOSED; import static org.webpieces.httpcommon.impl.Stream.StreamStatus.IDLE; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import javax.xml.bind.DatatypeConverter; import org.webpieces.data.api.DataWrapper; import org.webpieces.data.api.DataWrapperGenerator; import org.webpieces.data.api.DataWrapperGeneratorFactory; import org.webpieces.httpcommon.api.exceptions.GoAwayError; import org.webpieces.httpcommon.api.exceptions.Http2Error; import org.webpieces.httpcommon.api.exceptions.RstStreamError; import org.webpieces.httpcommon.impl.Http2EngineImpl.PendingData; import org.webpieces.httpparser.api.dto.HttpRequest; import org.webpieces.nio.api.channels.Channel; import org.webpieces.nio.api.handlers.DataListener; import com.webpieces.hpack.api.UnmarshalState; import com.webpieces.hpack.api.dto.Http2Headers; import com.webpieces.hpack.api.dto.Http2Push; import com.webpieces.http2parser.api.ErrorType; import com.webpieces.http2parser.api.Http2Exception; import com.webpieces.http2parser.api.dto.DataFrame; import com.webpieces.http2parser.api.dto.GoAwayFrame; import com.webpieces.http2parser.api.dto.PingFrame; import com.webpieces.http2parser.api.dto.PriorityFrame; import com.webpieces.http2parser.api.dto.RstStreamFrame; import com.webpieces.http2parser.api.dto.SettingsFrame; import com.webpieces.http2parser.api.dto.WindowUpdateFrame; import com.webpieces.http2parser.api.dto.lib.Http2ErrorCode; import com.webpieces.http2parser.api.dto.lib.Http2Header; import com.webpieces.http2parser.api.dto.lib.Http2Msg; import com.webpieces.http2parser.api.dto.lib.Http2MsgType; import com.webpieces.http2parser.api.dto.lib.SettingsParameter; class Http2DataListener implements DataListener { private static final DataWrapperGenerator dataGen = DataWrapperGeneratorFactory.createDataWrapperGenerator(); /** * */ private final Http2EngineImpl http2EngineImpl; private UnmarshalState unmarshalState; private AtomicBoolean gotPreface = new AtomicBoolean(false); private DataWrapper firstIncomingData = DataWrapperGeneratorFactory.EMPTY; /** * @param http2EngineImpl */ Http2DataListener(Http2EngineImpl http2EngineImpl) { this.http2EngineImpl = http2EngineImpl; Long localMaxFrameSize = http2EngineImpl.localSettings.get(SETTINGS_MAX_FRAME_SIZE); unmarshalState = this.http2EngineImpl.http2Parser.prepareToUnmarshal(4096, 4096, localMaxFrameSize); } private void handleData(DataFrame frame, Stream stream) { // Only allowable if stream is open or half closed local switch(stream.getStatus()) { case OPEN: case HALF_CLOSED_LOCAL: int dataSize = frame.getData().getReadableSize(); int paddingSize = frame.getPadding().getReadableSize(); if(paddingSize > 0) { paddingSize += 1; //add the length byte in since there is padding } int payloadLength = dataSize + paddingSize; this.http2EngineImpl.decrementIncomingWindow(frame.getStreamId(), payloadLength); stream.checkAgainstContentLength(frame.getData().getReadableSize(), frame.isEndOfStream()); this.http2EngineImpl.sideSpecificHandleData(frame, payloadLength, stream); if(frame.isEndOfStream()) this.http2EngineImpl.receivedEndStream(stream); break; case HALF_CLOSED_REMOTE: throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), stream.getStreamId(), Http2ErrorCode.STREAM_CLOSED, Http2EngineImpl.wrapperGen.emptyWrapper()); case CLOSED: throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), stream.getStreamId(), Http2ErrorCode.STREAM_CLOSED, Http2EngineImpl.wrapperGen.emptyWrapper()); case IDLE: throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), stream.getStreamId(), Http2ErrorCode.PROTOCOL_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); default: throw new RstStreamError(Http2ErrorCode.PROTOCOL_ERROR, stream.getStreamId()); } } private void handleHeaders(Http2Headers frame, Stream stream) { boolean isTrailer = false; switch (stream.getStatus()) { case IDLE: long currentlyOpenStreams = this.http2EngineImpl.countOpenRemoteOriginatedStreams(); Http2EngineImpl.log.info("got headers with currently open streams: " + currentlyOpenStreams); if(this.http2EngineImpl.localSettings.containsKey(SETTINGS_MAX_CONCURRENT_STREAMS) && currentlyOpenStreams >= this.http2EngineImpl.localSettings.get(SETTINGS_MAX_CONCURRENT_STREAMS)) { throw new RstStreamError(Http2ErrorCode.REFUSED_STREAM, stream.getStreamId()); } stream.setStatus(Stream.StreamStatus.OPEN); break; case HALF_CLOSED_LOCAL: // No status change in this case break; case RESERVED_REMOTE: stream.setStatus(Stream.StreamStatus.HALF_CLOSED_LOCAL); break; case OPEN: if(!stream.isTrailerEnabled()) { throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), stream.getStreamId(), Http2ErrorCode.STREAM_CLOSED, Http2EngineImpl.wrapperGen.emptyWrapper()); } else { isTrailer = true; } break; default: // HALF_CLOSED_REMOTE, or CLOSED throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), stream.getStreamId(), Http2ErrorCode.STREAM_CLOSED, Http2EngineImpl.wrapperGen.emptyWrapper()); } stream.setPriorityDetails(frame.getPriorityDetails()); boolean isComplete = frame.isEndOfStream(); if(isTrailer) { // Make sure that the headers match what we are expecting. List<String> allowedTrailerHeaders = stream.getTrailerHeaders(); for(Http2Header header: frame.getHeaders()) { if(!allowedTrailerHeaders.contains(header.getName())) throw new RstStreamError(Http2ErrorCode.PROTOCOL_ERROR, stream.getStreamId()); } } // if we have no headers must be a compression error? if(frame.getHeaders().isEmpty()) { throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), Http2ErrorCode.COMPRESSION_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); } this.http2EngineImpl.sideSpecificHandleHeaders(frame, isTrailer, stream); if (isComplete) this.http2EngineImpl.receivedEndStream(stream); } private void handlePriority(PriorityFrame frame, Stream stream) { // Can be received in any state. We aren't doing anything with this right now. stream.setPriorityDetails(frame.getPriorityDetails()); } private void handleRstStream(RstStreamFrame frame, Stream stream) { switch(stream.getStatus()) { case OPEN: case HALF_CLOSED_REMOTE: case HALF_CLOSED_LOCAL: case RESERVED_LOCAL: case RESERVED_REMOTE: case CLOSED: this.http2EngineImpl.sideSpecificHandleRstStream(frame, stream); stream.setStatus(CLOSED); break; default: throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), Http2ErrorCode.PROTOCOL_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); } } private void handlePushPromise(Http2Push frame, Stream stream) { if(this.http2EngineImpl.side == SERVER) { // Can't get pushpromise in the server throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), Http2ErrorCode.PROTOCOL_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); } long currentlyOpenStreams = this.http2EngineImpl.countOpenRemoteOriginatedStreams(); Http2EngineImpl.log.info("got push promise with currently open streams: " + currentlyOpenStreams); if(this.http2EngineImpl.localSettings.containsKey(SETTINGS_MAX_CONCURRENT_STREAMS) && currentlyOpenStreams >= this.http2EngineImpl.localSettings.get(SETTINGS_MAX_CONCURRENT_STREAMS)) { throw new RstStreamError(Http2ErrorCode.REFUSED_STREAM, frame.getPromisedStreamId()); } int newStreamId = frame.getPromisedStreamId(); if(newStreamId <= this.http2EngineImpl.lastIncomingStreamId.get()) { throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), Http2ErrorCode.PROTOCOL_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); } this.http2EngineImpl.lastIncomingStreamId.set(newStreamId); Stream promisedStream = new Stream(); this.http2EngineImpl.initializeFlowControl(newStreamId); promisedStream.setStreamId(newStreamId); // TODO: make sure streamid is valid // TODO: close all lower numbered even IDLE streams this.http2EngineImpl.activeStreams.put(newStreamId, promisedStream); // Uses the same listener as the stream it came in on promisedStream.setResponseListener(stream.getResponseListener()); HttpRequest request = this.http2EngineImpl.requestFromHeaders(new LinkedList<>(frame.getHeaders()), promisedStream); promisedStream.setRequest(request); promisedStream.setStatus(Stream.StreamStatus.RESERVED_REMOTE); } private void handleWindowUpdate(WindowUpdateFrame frame, Stream stream) { if(frame.getWindowSizeIncrement() == 0) { throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), Http2ErrorCode.PROTOCOL_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); } this.http2EngineImpl.incrementOutgoingWindow(frame.getStreamId(), frame.getWindowSizeIncrement()); // clear all queues if the connection-level stream if(frame.getStreamId() == 0x0) { for (Map.Entry<Integer, ConcurrentLinkedDeque<PendingData>> entry : this.http2EngineImpl.outgoingDataQueue.entrySet()) { if (!entry.getValue().isEmpty()) this.http2EngineImpl.clearQueue(entry.getKey()); } } else { if(stream.getStatus() == IDLE) { throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), Http2ErrorCode.PROTOCOL_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); } if(this.http2EngineImpl.outgoingDataQueue.containsKey(frame.getStreamId())) { this.http2EngineImpl.clearQueue(frame.getStreamId()); } } } private void handleSettings(SettingsFrame frame) { if(frame.isAck()) { // we received an ack, so the settings we sent have been accepted. for(Map.Entry<SettingsParameter, Long> entry: this.http2EngineImpl.localRequestedSettings.entrySet()) { this.http2EngineImpl.localSettings.put(entry.getKey(), entry.getValue()); } } else { this.http2EngineImpl.setRemoteSettings(frame, true); } } // TODO: actually deal with this goaway stuff where necessary private void handleGoAway(GoAwayFrame frame) { this.http2EngineImpl.remoteGoneAway = true; this.http2EngineImpl.goneAwayLastStreamId = frame.getLastStreamId(); this.http2EngineImpl.goneAwayErrorCode = frame.getKnownErrorCode(); this.http2EngineImpl.additionalDebugData = frame.getDebugData(); farEndClosed(this.http2EngineImpl.channel); } private void handlePing(PingFrame frame) { if(!frame.isPingResponse()) { // Send the same frame back, setting ping response frame.setIsPingResponse(true); Http2EngineImpl.log.info("sending ping response: " + frame); DataWrapper data = this.http2EngineImpl.marshal(frame); this.http2EngineImpl.channel.write(ByteBuffer.wrap(data.createByteArray())); } else { // measure latency from the ping that was sent. The opaqueData we sent is // System.nanoTime() so we just measure the difference long latency = System.nanoTime() - frame.getOpaqueData(); Http2EngineImpl.log.info("Ping: {} ms", latency * 1e-6); } } private void handleFrame(Http2Msg frame) { if(frame.getMessageType() != Http2MsgType.SETTINGS && !this.http2EngineImpl.gotSettings.get()) { preconditions(); } // Transition the stream state if(frame.getStreamId() != 0x0) { Stream stream = this.http2EngineImpl.activeStreams.get(frame.getStreamId()); // If the stream doesn't exist, create it, if server and if streamid is odd. if (stream == null) { if (this.http2EngineImpl.side == SERVER) { int streamId = frame.getStreamId(); if(streamId <= this.http2EngineImpl.lastIncomingStreamId.get() || frame.getStreamId() % 2 != 1) { throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), Http2ErrorCode.PROTOCOL_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); } this.http2EngineImpl.lastIncomingStreamId.set(streamId); stream = new Stream(); stream.setStreamId(streamId); this.http2EngineImpl.initializeFlowControl(stream.getStreamId()); this.http2EngineImpl.activeStreams.put(stream.getStreamId(), stream); } else { throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), Http2ErrorCode.PROTOCOL_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); } } switch (frame.getMessageType()) { case DATA: handleData((DataFrame) frame, stream); break; case HEADERS: handleHeaders((Http2Headers) frame, stream); break; case PRIORITY: handlePriority((PriorityFrame) frame, stream); break; case RST_STREAM: handleRstStream((RstStreamFrame) frame, stream); break; case PUSH_PROMISE: handlePushPromise((Http2Push) frame, stream); break; case WINDOW_UPDATE: handleWindowUpdate((WindowUpdateFrame) frame, stream); break; default: throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), frame.getStreamId(), Http2ErrorCode.PROTOCOL_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); } } else { switch (frame.getMessageType()) { case WINDOW_UPDATE: handleWindowUpdate((WindowUpdateFrame) frame, null); break; case SETTINGS: handleSettings((SettingsFrame) frame); break; case GOAWAY: handleGoAway((GoAwayFrame) frame); break; case PING: handlePing((PingFrame) frame); break; default: throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), Http2ErrorCode.PROTOCOL_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); } } } private void preconditions() { // If we haven't gotten the settings, let's wait a little bit because another thread // might have the settings frame and hasn't gotten around to processing it yet. try { Http2EngineImpl.log.info("Waiting for settings frame to arrive"); if (!this.http2EngineImpl.settingsLatch.await(500, TimeUnit.MILLISECONDS)) throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), Http2ErrorCode.PROTOCOL_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); } catch (InterruptedException e) { Http2EngineImpl.log.error("Caught exception while waiting for settings frame", e); throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), Http2ErrorCode.PROTOCOL_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); } } @Override public void incomingData(Channel channel, ByteBuffer b) { DataWrapper newData = Http2EngineImpl.wrapperGen.wrapByteBuffer(b); try { // TODO: turn the preface into a frame type. // First check to make sure we got our preface if(this.http2EngineImpl.side == SERVER && !gotPreface.get()) { // check to make sure we got the preface. DataWrapper combined = Http2EngineImpl.wrapperGen.chainDataWrappers(firstIncomingData, newData); int prefaceLength = Http2EngineImpl.prefaceHexString.length()/2; if(combined.getReadableSize() >= prefaceLength) { List<? extends DataWrapper> split = Http2EngineImpl.wrapperGen.split(combined, prefaceLength); if(Arrays.equals(split.get(0).createByteArray(), (DatatypeConverter.parseHexBinary(Http2EngineImpl.prefaceHexString)))) { gotPreface.set(true); DataWrapper afterPrefaceData = split.get(1); Http2EngineImpl.log.info("got http2 preface"); this.http2EngineImpl.sendLocalRequestedSettings(); firstIncomingData = null; //release as no longer needed parseStuff(afterPrefaceData); } else { throw new GoAwayError(0, Http2ErrorCode.PROTOCOL_ERROR, Http2EngineImpl.wrapperGen.emptyWrapper()); } } else { firstIncomingData = combined; } } else { // Either we got the preface or we don't need it parseStuff(newData); } } catch (Http2Error e) { Http2EngineImpl.log.error("sending error frames " + e.toFrames(), e); ByteBuffer dataPayload = translate(e.toFrames()); channel.write(dataPayload); if(RstStreamError.class.isInstance(e)) { // Mark the stream closed Stream stream = this.http2EngineImpl.activeStreams.get(((RstStreamError) e).getStreamId()); if(stream != null) stream.setStatus(CLOSED); } if(GoAwayError.class.isInstance(e)) { // TODO: Shut this connection down properly. channel.close(); } } } private void parseStuff(DataWrapper newData) { try { parseStuffTranslate(newData); for (Http2Msg frame : unmarshalState.getParsedFrames()) { Http2EngineImpl.log.info("got frame=" + frame); handleFrame(frame); } } catch (Http2Exception e) { if(e.getErrorType() == ErrorType.CONNECTION) { if(e.hasStream()) { throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), e.getStreamId(), e.getReason().getErrorCode(), Http2EngineImpl.wrapperGen.emptyWrapper()); } else { throw new GoAwayError(this.http2EngineImpl.lastClosedRemoteOriginatedStream().orElse(0), e.getReason().getErrorCode(), Http2EngineImpl.wrapperGen.emptyWrapper()); } } else { throw new RstStreamError(e.getReason().getErrorCode(), e.getStreamId(), e); } } } private void parseStuffTranslate(DataWrapper newData) { unmarshalState = this.http2EngineImpl.http2Parser.unmarshal(unmarshalState, newData); } private ByteBuffer translate(List<Http2Msg> frames) { DataWrapper allData = dataGen.emptyWrapper(); for(Http2Msg f : frames) { DataWrapper data = this.http2EngineImpl.marshal(f); allData = dataGen.chainDataWrappers(allData, data); } byte[] byteArray = allData.createByteArray(); return ByteBuffer.wrap(byteArray); } @Override public void farEndClosed(Channel channel) { // TODO: deal with this } @Override public void failure(Channel channel, ByteBuffer data, Exception e) { // TODO: deal with this } @Override public void applyBackPressure(Channel channel) { } @Override public void releaseBackPressure(Channel channel) { } }