org.reaktivity.nukleus.http2.internal.Http2Connection Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of nukleus-http2 Show documentation
Show all versions of nukleus-http2 Show documentation
HTTP/2 Nukleus Implementation
/**
* Copyright 2016-2017 The Reaktivity Project
*
* The Reaktivity Project licenses this file to you 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 org.reaktivity.nukleus.http2.internal;
import org.agrona.DirectBuffer;
import org.agrona.MutableDirectBuffer;
import org.agrona.collections.Int2ObjectHashMap;
import org.agrona.concurrent.UnsafeBuffer;
import org.reaktivity.nukleus.function.MessageConsumer;
import org.reaktivity.nukleus.function.MessageFunction;
import org.reaktivity.nukleus.function.MessagePredicate;
import org.reaktivity.nukleus.http2.internal.types.HttpHeaderFW;
import org.reaktivity.nukleus.http2.internal.types.ListFW;
import org.reaktivity.nukleus.http2.internal.types.OctetsFW;
import org.reaktivity.nukleus.http2.internal.types.String16FW;
import org.reaktivity.nukleus.http2.internal.types.StringFW;
import org.reaktivity.nukleus.http2.internal.types.control.HttpRouteExFW;
import org.reaktivity.nukleus.http2.internal.types.control.RouteFW;
import org.reaktivity.nukleus.http2.internal.types.stream.AbortFW;
import org.reaktivity.nukleus.http2.internal.types.stream.BeginFW;
import org.reaktivity.nukleus.http2.internal.types.stream.DataFW;
import org.reaktivity.nukleus.http2.internal.types.stream.EndFW;
import org.reaktivity.nukleus.http2.internal.types.stream.HpackContext;
import org.reaktivity.nukleus.http2.internal.types.stream.HpackHeaderBlockFW;
import org.reaktivity.nukleus.http2.internal.types.stream.HpackHeaderFieldFW;
import org.reaktivity.nukleus.http2.internal.types.stream.HpackHuffman;
import org.reaktivity.nukleus.http2.internal.types.stream.HpackLiteralHeaderFieldFW;
import org.reaktivity.nukleus.http2.internal.types.stream.HpackStringFW;
import org.reaktivity.nukleus.http2.internal.types.stream.Http2DataExFW;
import org.reaktivity.nukleus.http2.internal.types.stream.Http2DataFW;
import org.reaktivity.nukleus.http2.internal.types.stream.Http2ErrorCode;
import org.reaktivity.nukleus.http2.internal.types.stream.Http2Flags;
import org.reaktivity.nukleus.http2.internal.types.stream.Http2FrameType;
import org.reaktivity.nukleus.http2.internal.types.stream.Http2SettingsId;
import org.reaktivity.nukleus.http2.internal.types.stream.HttpBeginExFW;
import org.reaktivity.nukleus.http2.internal.types.stream.ResetFW;
import org.reaktivity.nukleus.http2.internal.types.stream.WindowFW;
import org.reaktivity.nukleus.route.RouteManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import static java.nio.ByteOrder.BIG_ENDIAN;
import static org.reaktivity.nukleus.buffer.BufferPool.NO_SLOT;
import static org.reaktivity.nukleus.http2.internal.Http2Connection.State.HALF_CLOSED_REMOTE;
import static org.reaktivity.nukleus.http2.internal.Http2Connection.State.OPEN;
import static org.reaktivity.nukleus.http2.internal.types.stream.HpackContext.CONNECTION;
import static org.reaktivity.nukleus.http2.internal.types.stream.HpackContext.KEEP_ALIVE;
import static org.reaktivity.nukleus.http2.internal.types.stream.HpackContext.PROXY_CONNECTION;
import static org.reaktivity.nukleus.http2.internal.types.stream.HpackContext.TE;
import static org.reaktivity.nukleus.http2.internal.types.stream.HpackContext.TRAILERS;
import static org.reaktivity.nukleus.http2.internal.types.stream.HpackContext.UPGRADE;
import static org.reaktivity.nukleus.http2.internal.types.stream.HpackHeaderFieldFW.HeaderFieldType.UNKNOWN;
import static org.reaktivity.nukleus.http2.internal.types.stream.HpackLiteralHeaderFieldFW.LiteralType.INCREMENTAL_INDEXING;
import static org.reaktivity.nukleus.http2.internal.types.stream.HpackLiteralHeaderFieldFW.LiteralType.WITHOUT_INDEXING;
import static org.reaktivity.nukleus.http2.internal.types.stream.Http2PrefaceFW.PRI_REQUEST;
final class Http2Connection
{
private static final Map EMPTY_HEADERS = Collections.emptyMap();
ServerStreamFactory factory;
private DecoderState decoderState;
// slab to assemble a complete HTTP2 frame
// no need for separate slab per HTTP2 stream as the frames are not fragmented
private int frameSlotIndex = NO_SLOT;
private int frameSlotPosition;
// slab to assemble a complete HTTP2 headers frame(including its continuation frames)
// no need for separate slab per HTTP2 stream as no interleaved frames of any other type
// or from any other stream
private int headersSlotIndex = NO_SLOT;
private int headersSlotPosition;
long sourceId;
int lastStreamId;
long sourceRef;
int outWindow;
int outWindowThreshold = -1;
final WriteScheduler writeScheduler;
final long sourceOutputEstId;
private final HpackContext decodeContext;
private final HpackContext encodeContext;
private final MessageFunction wrapRoute;
final Int2ObjectHashMap http2Streams; // HTTP2 stream-id --> Http2Stream
private int noClientStreams;
private int noPromisedStreams;
private int maxClientStreamId;
private int maxPushPromiseStreamId;
private boolean goaway;
Settings initialSettings;
Settings localSettings;
Settings remoteSettings;
private boolean expectContinuation;
private int expectContinuationStreamId;
private boolean expectDynamicTableSizeUpdate = true;
long http2OutWindow;
long http2InWindow;
private boolean prefaceAvailable;
private boolean http2FrameAvailable;
private final Consumer headerFieldConsumer;
private final HeadersContext headersContext = new HeadersContext();
private final EncodeHeadersContext encodeHeadersContext = new EncodeHeadersContext();
final Http2Writer http2Writer;
MessageConsumer networkConsumer;
RouteManager router;
String sourceName;
Http2Connection(ServerStreamFactory factory, RouteManager router, long networkReplyId, MessageConsumer networkConsumer,
MessageFunction wrapRoute)
{
this.factory = factory;
this.router = router;
this.wrapRoute = wrapRoute;
sourceOutputEstId = networkReplyId;
http2Streams = new Int2ObjectHashMap<>();
localSettings = new Settings();
remoteSettings = new Settings();
decodeContext = new HpackContext(localSettings.headerTableSize, false);
encodeContext = new HpackContext(remoteSettings.headerTableSize, true);
http2Writer = factory.http2Writer;
writeScheduler = new Http2WriteScheduler(this, networkConsumer, http2Writer, sourceOutputEstId);
http2InWindow = localSettings.initialWindowSize;
http2OutWindow = remoteSettings.initialWindowSize;
this.networkConsumer = networkConsumer;
BiConsumer nameValue =
((BiConsumer)this::collectHeaders)
.andThen(this::mapToHttp)
.andThen(this::validatePseudoHeaders)
.andThen(this::uppercaseHeaders)
.andThen(this::connectionHeaders)
.andThen(this::contentLengthHeader)
.andThen(this::teHeader);
Consumer consumer = this::validateHeaderFieldType;
consumer = consumer.andThen(this::dynamicTableSizeUpdate);
this.headerFieldConsumer = consumer.andThen(h -> decodeHeaderField(h, nameValue));
}
void processUnexpected(
long streamId)
{
http2Writer.doReset(networkConsumer, streamId);
cleanConnection();
}
void cleanConnection()
{
releaseSlot();
releaseHeadersSlot();
for(Http2Stream http2Stream : http2Streams.values())
{
closeStream(http2Stream);
}
http2Streams.clear();
}
void handleBegin(BeginFW beginRO)
{
this.sourceId = beginRO.streamId();
this.sourceRef = beginRO.sourceRef();
this.sourceName = beginRO.source().asString();
this.decoderState = this::decodePreface;
initialSettings = new Settings(100, 0);
writeScheduler.settings(initialSettings.maxConcurrentStreams, initialSettings.initialWindowSize);
}
void handleData(DataFW dataRO)
{
OctetsFW payload = dataRO.payload();
int limit = payload.limit();
int offset = payload.offset();
while (offset < limit)
{
offset += decoderState.decode(dataRO.buffer(), offset, limit);
}
}
void handleAbort()
{
http2Streams.forEach((i, s) -> s.onAbort());
cleanConnection();
}
void handleReset(ResetFW reset)
{
http2Streams.forEach((i, s) -> s.onReset());
cleanConnection();
}
void handleEnd(EndFW end)
{
decoderState = (b, o, l) -> o;
http2Streams.forEach((i, s) -> s.onEnd());
writeScheduler.doEnd();
cleanConnection();
}
// Decodes client preface
private int decodePreface(final DirectBuffer buffer, final int offset, final int limit)
{
int length = prefaceAvailable(buffer, offset, limit);
if (!prefaceAvailable)
{
return length;
}
if (factory.prefaceRO.error())
{
processUnexpected(sourceId);
return limit-offset;
}
this.decoderState = this::decodeHttp2Frame;
return length;
}
private int http2FrameLength(DirectBuffer buffer, final int offset, int limit)
{
assert limit - offset >= 3;
int length = (buffer.getByte(offset) & 0xFF) << 16;
length += (buffer.getShort(offset + 1, BIG_ENDIAN) & 0xFF_FF);
return length + 9; // +3 for length, +1 type, +1 flags, +4 stream-id
}
/*
* Assembles a complete HTTP2 client preface and the flyweight is wrapped with the
* buffer (it could be given buffer or slab)
*
* @return no of bytes consumed
*/
private int prefaceAvailable(DirectBuffer buffer, int offset, int limit)
{
int available = limit - offset;
if (frameSlotPosition > 0 && frameSlotPosition + available >= PRI_REQUEST.length)
{
MutableDirectBuffer prefaceBuffer = factory.framePool.buffer(frameSlotIndex);
int remainingLength = PRI_REQUEST.length - frameSlotPosition;
prefaceBuffer.putBytes(frameSlotPosition, buffer, offset, remainingLength);
factory.prefaceRO.wrap(prefaceBuffer, 0, PRI_REQUEST.length);
releaseSlot();
prefaceAvailable = true;
return remainingLength;
}
else if (available >= PRI_REQUEST.length)
{
factory.prefaceRO.wrap(buffer, offset, offset + PRI_REQUEST.length);
prefaceAvailable = true;
return PRI_REQUEST.length;
}
assert frameSlotIndex == NO_SLOT;
if (!acquireSlot())
{
prefaceAvailable = false;
return available; // assume everything is consumed
}
MutableDirectBuffer prefaceBuffer = factory.framePool.buffer(frameSlotIndex);
prefaceBuffer.putBytes(frameSlotPosition, buffer, offset, available);
frameSlotPosition += available;
prefaceAvailable = false;
return available;
}
/*
* Assembles a complete HTTP2 frame and the flyweight is wrapped with the
* buffer (it could be given buffer or slab)
*
* @return consumed octets
* -1 if the frame size is more than the max frame size
*/
// TODO check slab capacity
private int http2FrameAvailable(DirectBuffer buffer, int offset, int limit)
{
int available = limit - offset;
if (frameSlotPosition > 0 && frameSlotPosition + available >= 3)
{
MutableDirectBuffer frameBuffer = factory.framePool.buffer(frameSlotIndex);
if (frameSlotPosition < 3)
{
frameBuffer.putBytes(frameSlotPosition, buffer, offset, 3 - frameSlotPosition);
}
int frameLength = http2FrameLength(frameBuffer, 0, 3);
if (frameLength > localSettings.maxFrameSize + 9)
{
return -1;
}
if (frameSlotPosition + available >= frameLength)
{
int remainingFrameLength = frameLength - frameSlotPosition;
frameBuffer.putBytes(frameSlotPosition, buffer, offset, remainingFrameLength);
factory.http2RO.wrap(frameBuffer, 0, frameLength);
releaseSlot();
http2FrameAvailable = true;
return remainingFrameLength;
}
}
else if (available >= 3)
{
int frameLength = http2FrameLength(buffer, offset, limit);
if (frameLength > localSettings.maxFrameSize + 9)
{
return -1;
}
if (available >= frameLength)
{
factory.http2RO.wrap(buffer, offset, offset + frameLength);
http2FrameAvailable = true;
return frameLength;
}
}
if (!acquireSlot())
{
http2FrameAvailable = false;
return available;
}
MutableDirectBuffer frameBuffer = factory.framePool.buffer(frameSlotIndex);
frameBuffer.putBytes(frameSlotPosition, buffer, offset, available);
frameSlotPosition += available;
http2FrameAvailable = false;
return available;
}
private boolean acquireSlot()
{
if (frameSlotIndex == NO_SLOT)
{
assert frameSlotPosition == 0;
frameSlotIndex = factory.framePool.acquire(sourceId);
if (frameSlotIndex == NO_SLOT)
{
// all slots are in use, just reset the connection
http2Writer.doReset(networkConsumer, sourceId);
handleAbort();
http2FrameAvailable = false;
return false;
}
}
return true;
}
private void releaseSlot()
{
if (frameSlotIndex != NO_SLOT)
{
factory.framePool.release(frameSlotIndex);
frameSlotIndex = NO_SLOT;
frameSlotPosition = 0;
}
}
private boolean acquireHeadersSlot()
{
if (headersSlotIndex == NO_SLOT)
{
assert headersSlotPosition == 0;
headersSlotIndex = factory.headersPool.acquire(sourceId);
if (headersSlotIndex == NO_SLOT)
{
// all slots are in use, just reset the connection
http2Writer.doReset(networkConsumer, sourceId);
handleAbort();
return false;
}
}
return true;
}
private void releaseHeadersSlot()
{
if (headersSlotIndex != NO_SLOT)
{
factory.headersPool.release(headersSlotIndex);
headersSlotIndex = NO_SLOT;
headersSlotPosition = 0;
}
}
/*
* Assembles a complete HTTP2 headers (including any continuations) if any.
*
* @return true if a complete HTTP2 headers is assembled or any other frame
* false otherwise
*/
private boolean http2HeadersAvailable()
{
if (expectContinuation)
{
if (factory.http2RO.type() != Http2FrameType.CONTINUATION || factory.http2RO.streamId() != expectContinuationStreamId)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return false;
}
}
else if (factory.http2RO.type() == Http2FrameType.CONTINUATION)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return false;
}
switch (factory.http2RO.type())
{
case HEADERS:
int streamId = factory.http2RO.streamId();
if (streamId == 0 || streamId % 2 != 1 || streamId <= maxClientStreamId)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return false;
}
factory.headersRO.wrap(factory.http2RO.buffer(), factory.http2RO.offset(), factory.http2RO.limit());
int parentStreamId = factory.headersRO.parentStream();
if (parentStreamId == streamId)
{
// 5.3.1 A stream cannot depend on itself
streamError(streamId, Http2ErrorCode.PROTOCOL_ERROR);
return false;
}
if (factory.headersRO.dataLength() < 0)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return false;
}
return http2HeadersAvailable(factory.headersRO.buffer(), factory.headersRO.dataOffset(),
factory.headersRO.dataLength(), factory.headersRO.endHeaders());
case CONTINUATION:
factory.continationRO.wrap(factory.http2RO.buffer(), factory.http2RO.offset(), factory.http2RO.limit());
DirectBuffer payload = factory.continationRO.payload();
boolean endHeaders = factory.continationRO.endHeaders();
return http2HeadersAvailable(payload, 0, payload.capacity(), endHeaders);
}
return true;
}
/*
* Assembles a complete HTTP2 headers (including any continuations) and the
* flyweight is wrapped with the buffer (it could be given buffer or slab)
*
* @return true if a complete HTTP2 headers is assembled
* false otherwise
*/
private boolean http2HeadersAvailable(DirectBuffer buffer, int offset, int length, boolean endHeaders)
{
if (endHeaders)
{
if (headersSlotPosition > 0)
{
MutableDirectBuffer headersBuffer = factory.headersPool.buffer(headersSlotIndex);
headersBuffer.putBytes(headersSlotPosition, buffer, offset, length);
headersSlotPosition += length;
buffer = headersBuffer;
offset = 0;
length = headersSlotPosition;
}
int maxLimit = offset + length;
expectContinuation = false;
releaseHeadersSlot(); // early release, but fine
factory.blockRO.wrap(buffer, offset, maxLimit);
return true;
}
else
{
if (!acquireHeadersSlot())
{
return false;
}
MutableDirectBuffer headersBuffer = factory.headersPool.buffer(headersSlotIndex);
headersBuffer.putBytes(headersSlotPosition, buffer, offset, length);
headersSlotPosition += length;
expectContinuation = true;
expectContinuationStreamId = factory.headersRO.streamId();
}
return false;
}
private int decodeHttp2Frame(final DirectBuffer buffer, final int offset, final int limit)
{
int length = http2FrameAvailable(buffer, offset, limit);
if (length == -1)
{
error(Http2ErrorCode.FRAME_SIZE_ERROR);
return limit - offset;
}
if (!http2FrameAvailable)
{
return length;
}
Http2FrameType http2FrameType = factory.http2RO.type();
// Assembles HTTP2 HEADERS and its CONTINUATIONS frames, if any
if (!http2HeadersAvailable())
{
return length;
}
switch (http2FrameType)
{
case DATA:
doData();
break;
case HEADERS: // fall-through
case CONTINUATION:
doHeaders();
break;
case PRIORITY:
doPriority();
break;
case RST_STREAM:
doRst();
break;
case SETTINGS:
doSettings();
break;
case PUSH_PROMISE:
doPushPromise();
break;
case PING:
doPing();
break;
case GO_AWAY:
doGoAway();
break;
case WINDOW_UPDATE:
doWindow();
break;
default:
// Ignore and discard unknown frame
}
return length;
}
private void doGoAway()
{
int streamId = factory.http2RO.streamId();
if (goaway)
{
if (streamId != 0)
{
processUnexpected(sourceId);
}
}
else
{
goaway = true;
Http2ErrorCode errorCode = (streamId != 0) ? Http2ErrorCode.PROTOCOL_ERROR : Http2ErrorCode.NO_ERROR;
remoteSettings.enablePush = false; // no new streams
error(errorCode);
}
}
private void doPushPromise()
{
error(Http2ErrorCode.PROTOCOL_ERROR);
}
private void doPriority()
{
int streamId = factory.http2RO.streamId();
if (streamId == 0)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return;
}
int payloadLength = factory.http2RO.payloadLength();
if (payloadLength != 5)
{
streamError(streamId, Http2ErrorCode.FRAME_SIZE_ERROR);
return;
}
factory.priorityRO.wrap(factory.http2RO.buffer(), factory.http2RO.offset(), factory.http2RO.limit());
int parentStreamId = factory.priorityRO.parentStream();
if (parentStreamId == streamId)
{
// 5.3.1 A stream cannot depend on itself
streamError(streamId, Http2ErrorCode.PROTOCOL_ERROR);
return;
}
}
private void doHeaders()
{
int streamId = factory.http2RO.streamId();
Http2Stream stream = http2Streams.get(streamId);
if (stream != null)
{
// TODO trailers
}
if (streamId <= maxClientStreamId)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return;
}
maxClientStreamId = streamId;
if (noClientStreams + 1 > localSettings.maxConcurrentStreams)
{
streamError(streamId, Http2ErrorCode.REFUSED_STREAM);
return;
}
State state = factory.http2RO.endStream() ? HALF_CLOSED_REMOTE : OPEN;
headersContext.reset();
factory.httpBeginExRW.wrap(factory.scratch, 0, factory.scratch.capacity());
factory.blockRO.forEach(headerFieldConsumer);
// All HTTP/2 requests MUST include exactly one valid value for the
// ":method", ":scheme", and ":path" pseudo-header fields, unless it is
// a CONNECT request (Section 8.3). An HTTP request that omits
// mandatory pseudo-header fields is malformed
if (!headersContext.error() && (headersContext.method != 1 || headersContext.scheme != 1 || headersContext.path != 1))
{
headersContext.streamError = Http2ErrorCode.PROTOCOL_ERROR;
}
if (headersContext.error())
{
if (headersContext.streamError != null)
{
streamError(streamId, headersContext.streamError);
return;
}
if (headersContext.connectionError != null)
{
error(headersContext.connectionError);
return;
}
}
RouteFW route = resolveTarget(sourceRef, sourceName, headersContext.headers);
if (route == null)
{
noRoute(streamId);
}
else
{
followRoute(streamId, state, route);
}
}
private void followRoute(int streamId, State state, RouteFW route)
{
final String applicationName = route.target().asString();
final MessageConsumer applicationTarget = router.supplyTarget(applicationName);
HttpWriter httpWriter = factory.httpWriter;
Http2Stream stream = newStream(streamId, state, applicationTarget, httpWriter);
final long targetRef = route.targetRef();
stream.contentLength = headersContext.contentLength;
HttpBeginExFW beginEx = factory.httpBeginExRW.build();
httpWriter.doHttpBegin(applicationTarget, stream.targetId, targetRef, stream.correlationId,
beginEx.buffer(), beginEx.offset(), beginEx.sizeof());
router.setThrottle(applicationName, stream.targetId, stream::onThrottle);
if (factory.headersRO.endStream())
{
httpWriter.doHttpEnd(applicationTarget, stream.targetId); // TODO use HttpWriteScheduler
}
}
// No route for the HTTP2 request, send 404 on the corresponding HTTP2 stream
private void noRoute(int streamId)
{
ListFW headers =
factory.headersRW.wrap(factory.errorBuf, 0, factory.errorBuf.capacity())
.item(b -> b.name(":status").value("404"))
.build();
writeScheduler.headers(streamId, Http2Flags.END_STREAM, headers);
}
private void doRst()
{
int streamId = factory.http2RO.streamId();
if (streamId == 0)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return;
}
int payloadLength = factory.http2RO.payloadLength();
if (payloadLength != 4)
{
error(Http2ErrorCode.FRAME_SIZE_ERROR);
return;
}
Http2Stream stream = http2Streams.get(streamId);
if (stream == null || stream.state == State.IDLE)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
}
else
{
stream.onReset();
closeStream(stream);
}
}
void closeStream(Http2Stream stream)
{
if (stream.isClientInitiated())
{
noClientStreams--;
}
else
{
noPromisedStreams--;
}
factory.correlations.remove(stream.targetId); // remove from Correlations map
http2Streams.remove(stream.http2StreamId);
stream.close();
}
private void doWindow()
{
int streamId = factory.http2RO.streamId();
if (factory.http2RO.payloadLength() != 4)
{
if (streamId == 0)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return;
}
else
{
streamError(streamId, Http2ErrorCode.PROTOCOL_ERROR);
return;
}
}
if (streamId != 0)
{
State state = state(streamId);
if (state == State.IDLE)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return;
}
Http2Stream stream = http2Streams.get(streamId);
if (stream == null)
{
// A receiver could receive a WINDOW_UPDATE frame on a "half-closed (remote)" or "closed" stream.
// A receiver MUST NOT treat this as an error
return;
}
}
factory.http2WindowRO.wrap(factory.http2RO.buffer(), factory.http2RO.offset(), factory.http2RO.limit());
// 6.9 WINDOW_UPDATE - legal range for flow-control window increment is 1 to 2^31-1 octets.
if (factory.http2WindowRO.size() < 1)
{
if (streamId == 0)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return;
}
else
{
streamError(streamId, Http2ErrorCode.PROTOCOL_ERROR);
return;
}
}
// 6.9.1 A sender MUST NOT allow a flow-control window to exceed 2^31-1 octets.
if (streamId == 0)
{
http2OutWindow += factory.http2WindowRO.size();
if (http2OutWindow > Integer.MAX_VALUE)
{
error(Http2ErrorCode.FLOW_CONTROL_ERROR);
return;
}
writeScheduler.onHttp2Window();
}
else
{
Http2Stream stream = http2Streams.get(streamId);
stream.http2OutWindow += factory.http2WindowRO.size();
if (stream.http2OutWindow > Integer.MAX_VALUE)
{
streamError(streamId, Http2ErrorCode.FLOW_CONTROL_ERROR);
return;
}
writeScheduler.onHttp2Window(streamId);
}
}
private void doData()
{
int streamId = factory.http2RO.streamId();
Http2Stream stream = http2Streams.get(streamId);
if (streamId == 0 || stream == null || stream.state == State.IDLE)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return;
}
if (stream.state == HALF_CLOSED_REMOTE)
{
error(Http2ErrorCode.STREAM_CLOSED);
closeStream(stream);
return;
}
Http2DataFW dataRO = factory.http2DataRO.wrap(factory.http2RO.buffer(), factory.http2RO.offset(),
factory.http2RO.limit());
if (dataRO.dataLength() < 0) // because of invalid padding length
{
error(Http2ErrorCode.PROTOCOL_ERROR);
closeStream(stream);
return;
}
//
if (stream.http2InWindow < factory.http2RO.payloadLength() || http2InWindow < factory.http2RO.payloadLength())
{
streamError(streamId, Http2ErrorCode.FLOW_CONTROL_ERROR);
return;
}
http2InWindow -= factory.http2RO.payloadLength();
stream.http2InWindow -= factory.http2RO.payloadLength();
stream.totalData += factory.http2RO.payloadLength();
if (dataRO.endStream())
{
// 8.1.2.6 A request is malformed if the value of a content-length header field does
// not equal the sum of the DATA frame payload lengths
if (stream.contentLength != -1 && stream.totalData != stream.contentLength)
{
streamError(streamId, Http2ErrorCode.PROTOCOL_ERROR);
//stream.httpWriteScheduler.doEnd(stream.targetId);
return;
}
stream.state = State.HALF_CLOSED_REMOTE;
}
stream.onData();
}
private void doSettings()
{
if (factory.http2RO.streamId() != 0)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return;
}
if (factory.http2RO.payloadLength()%6 != 0)
{
error(Http2ErrorCode.FRAME_SIZE_ERROR);
return;
}
factory.settingsRO.wrap(factory.http2RO.buffer(), factory.http2RO.offset(), factory.http2RO.limit());
if (factory.settingsRO.ack() && factory.http2RO.payloadLength() != 0)
{
error(Http2ErrorCode.FRAME_SIZE_ERROR);
return;
}
if (!factory.settingsRO.ack())
{
factory.settingsRO.accept(this::doSetting);
writeScheduler.settingsAck();
}
else
{
int update = initialSettings.initialWindowSize - localSettings.initialWindowSize;
for(Http2Stream http2Stream: http2Streams.values())
{
http2Stream.http2InWindow += update; // http2InWindow can become negative
}
// now that peer acked our initial settings, can use them as our local settings
localSettings = initialSettings;
}
}
private void doSetting(Http2SettingsId id, Long value)
{
switch (id)
{
case HEADER_TABLE_SIZE:
remoteSettings.headerTableSize = value.intValue();
break;
case ENABLE_PUSH:
if (!(value == 0L || value == 1L))
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return;
}
remoteSettings.enablePush = (value == 1L);
break;
case MAX_CONCURRENT_STREAMS:
remoteSettings.maxConcurrentStreams = value.intValue();
break;
case INITIAL_WINDOW_SIZE:
if (value > Integer.MAX_VALUE)
{
error(Http2ErrorCode.FLOW_CONTROL_ERROR);
return;
}
int old = remoteSettings.initialWindowSize;
remoteSettings.initialWindowSize = value.intValue();
int update = value.intValue() - old;
// 6.9.2. Initial Flow-Control Window Size
// SETTINGS frame can alter the initial flow-control
// window size for streams with active flow-control windows
for(Http2Stream http2Stream: http2Streams.values())
{
http2Stream.http2OutWindow += update; // http2OutWindow can become negative
if (http2Stream.http2OutWindow > Integer.MAX_VALUE)
{
// 6.9.2. Initial Flow-Control Window Size
// An endpoint MUST treat a change to SETTINGS_INITIAL_WINDOW_SIZE that
// causes any flow-control window to exceed the maximum size as a
// connection error of type FLOW_CONTROL_ERROR.
error(Http2ErrorCode.FLOW_CONTROL_ERROR);
return;
}
}
break;
case MAX_FRAME_SIZE:
if (value < Math.pow(2, 14) || value > Math.pow(2, 24) -1)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return;
}
remoteSettings.maxFrameSize = value.intValue();
break;
case MAX_HEADER_LIST_SIZE:
remoteSettings.maxHeaderListSize = value.intValue();
break;
default:
// Ignore the unkonwn setting
break;
}
}
private void doPing()
{
if (factory.http2RO.streamId() != 0)
{
error(Http2ErrorCode.PROTOCOL_ERROR);
return;
}
if (factory.http2RO.payloadLength() != 8)
{
error(Http2ErrorCode.FRAME_SIZE_ERROR);
return;
}
factory.pingRO.wrap(factory.http2RO.buffer(), factory.http2RO.offset(), factory.http2RO.limit());
if (!factory.pingRO.ack())
{
writeScheduler.pingAck(factory.pingRO.payload(), 0, factory.pingRO.payload().capacity());
}
}
private State state(int streamId)
{
Http2Stream stream = http2Streams.get(streamId);
if (stream != null)
{
return stream.state;
}
if (streamId%2 == 1)
{
if (streamId <= maxClientStreamId)
{
return State.CLOSED;
}
}
else
{
if (streamId <= maxPushPromiseStreamId)
{
return State.CLOSED;
}
}
return State.IDLE;
}
RouteFW resolveTarget(
long sourceRef,
String sourceName,
Map headers)
{
MessagePredicate filter = (t, b, o, l) ->
{
RouteFW route = factory.routeRO.wrap(b, o, l);
OctetsFW extension = route.extension();
if (sourceRef == route.sourceRef() && sourceName.equals(route.source().asString()))
{
Map routeHeaders;
if (extension.sizeof() == 0)
{
routeHeaders = EMPTY_HEADERS;
}
else
{
final HttpRouteExFW routeEx = extension.get(factory.httpRouteExRO::wrap);
routeHeaders = new LinkedHashMap<>();
routeEx.headers().forEach(h -> routeHeaders.put(h.name().asString(), h.value().asString()));
}
return headers.entrySet().containsAll(routeHeaders.entrySet());
}
return false;
};
return router.resolve(filter, wrapRoute);
}
void handleWindow(WindowFW windowRO)
{
writeScheduler.onWindow();
}
void error(Http2ErrorCode errorCode)
{
writeScheduler.goaway(lastStreamId, errorCode);
writeScheduler.doEnd();
}
void streamError(int streamId, Http2ErrorCode errorCode)
{
writeScheduler.rst(streamId, errorCode);
}
private int nextPromisedId()
{
maxPushPromiseStreamId += 2;
return maxPushPromiseStreamId;
}
/*
* @param streamId corresponding http2 stream-id on which service response
* will be sent
* @return a stream id on which PUSH_PROMISE can be sent
* -1 otherwise
*/
private int findPushId(int streamId)
{
if (remoteSettings.enablePush && noPromisedStreams+1 < remoteSettings.maxConcurrentStreams)
{
// PUSH_PROMISE frames MUST only be sent on a peer-initiated stream
if (streamId%2 == 0)
{
// Find a stream on which PUSH_PROMISE can be sent
return http2Streams.entrySet()
.stream()
.map(Map.Entry::getValue)
.filter(s -> (s.http2StreamId & 0x01) == 1) // client-initiated stream
.filter(s -> s.state == OPEN || s.state == HALF_CLOSED_REMOTE)
.mapToInt(s -> s.http2StreamId)
.findAny()
.orElse(-1);
}
else
{
return streamId; // client-initiated stream
}
}
return -1;
}
private void doPromisedRequest(int http2StreamId, ListFW headers)
{
Map headersMap = new HashMap<>();
headers.forEach(
httpHeader -> headersMap.put(httpHeader.name().asString(), httpHeader.value().asString()));
RouteFW route = resolveTarget(sourceRef, sourceName, headersMap);
final String applicationName = route.target().asString();
final MessageConsumer applicationTarget = router.supplyTarget(applicationName);
HttpWriter httpWriter = factory.httpWriter;
Http2Stream http2Stream = newStream(http2StreamId, HALF_CLOSED_REMOTE, applicationTarget, httpWriter);
long targetId = http2Stream.targetId;
long targetRef = route.targetRef();
httpWriter.doHttpBegin(applicationTarget, targetId, targetRef, http2Stream.correlationId,
hs -> headers.forEach(h -> hs.item(b -> b.name(h.name())
.value(h.value()))));
httpWriter.doHttpEnd(applicationTarget, targetId);
router.setThrottle(applicationName, targetId, http2Stream::onThrottle);
}
private Http2Stream newStream(int http2StreamId, State state, MessageConsumer applicationTarget, HttpWriter httpWriter)
{
assert http2StreamId != 0;
Http2Stream http2Stream = new Http2Stream(factory, this, http2StreamId, state, applicationTarget, httpWriter);
http2Streams.put(http2StreamId, http2Stream);
Correlation correlation = new Correlation(http2Stream.correlationId, sourceOutputEstId, writeScheduler,
this::doPromisedRequest, this, http2StreamId, encodeContext, this::nextPromisedId, this::findPushId);
factory.correlations.put(http2Stream.correlationId, correlation);
if (http2Stream.isClientInitiated())
{
noClientStreams++;
}
else
{
noPromisedStreams++;
}
return http2Stream;
}
private void validateHeaderFieldType(HpackHeaderFieldFW hf)
{
if (!headersContext.error() && hf.type() == UNKNOWN)
{
headersContext.connectionError = Http2ErrorCode.COMPRESSION_ERROR;
}
}
private void dynamicTableSizeUpdate(HpackHeaderFieldFW hf)
{
if (!headersContext.error())
{
switch (hf.type())
{
case INDEXED:
case LITERAL:
expectDynamicTableSizeUpdate = false;
break;
case UPDATE:
if (!expectDynamicTableSizeUpdate)
{
// dynamic table size update MUST occur at the beginning of the first header block
headersContext.connectionError = Http2ErrorCode.COMPRESSION_ERROR;
return;
}
int maxTableSize = hf.tableSize();
if (maxTableSize > localSettings.headerTableSize)
{
headersContext.connectionError = Http2ErrorCode.COMPRESSION_ERROR;
return;
}
decodeContext.updateSize(hf.tableSize());
break;
}
}
}
private void validatePseudoHeaders(DirectBuffer name, DirectBuffer value)
{
if (!headersContext.error())
{
if (name.capacity() > 0 && name.getByte(0) == ':')
{
// All pseudo-header fields MUST appear in the header block before regular header fields
if (headersContext.regularHeader)
{
headersContext.streamError = Http2ErrorCode.PROTOCOL_ERROR;
return;
}
// request pseudo-header fields MUST be one of :authority, :method, :path, :scheme,
int index = decodeContext.index(name);
switch (index)
{
case 1: // :authority
break;
case 2: // :method
headersContext.method++;
break;
case 4: // :path
if (value.capacity() > 0) // :path MUST not be empty
{
headersContext.path++;
}
break;
case 6: // :scheme
headersContext.scheme++;
break;
default:
headersContext.streamError = Http2ErrorCode.PROTOCOL_ERROR;
return;
}
}
else
{
headersContext.regularHeader = true;
}
}
}
private void connectionHeaders(DirectBuffer name, DirectBuffer value)
{
if (!headersContext.error() && name.equals(HpackContext.CONNECTION))
{
headersContext.streamError = Http2ErrorCode.PROTOCOL_ERROR;
}
}
private void contentLengthHeader(DirectBuffer name, DirectBuffer value)
{
if (!headersContext.error() && name.equals(decodeContext.nameBuffer(28)))
{
String contentLength = value.getStringWithoutLengthUtf8(0, value.capacity());
headersContext.contentLength = Long.parseLong(contentLength);
}
}
// 8.1.2.2 TE header MUST NOT contain any value other than "trailers".
private void teHeader(DirectBuffer name, DirectBuffer value)
{
if (!headersContext.error() && name.equals(TE) && !value.equals(TRAILERS))
{
headersContext.streamError = Http2ErrorCode.PROTOCOL_ERROR;
}
}
private void uppercaseHeaders(DirectBuffer name, DirectBuffer value)
{
if (!headersContext.error())
{
for(int i=0; i < name.capacity(); i++)
{
if (name.getByte(i) >= 'A' && name.getByte(i) <= 'Z')
{
headersContext.streamError = Http2ErrorCode.PROTOCOL_ERROR;
}
}
}
}
// Collect headers into map to resolve target
// TODO avoid this
private void collectHeaders(DirectBuffer name, DirectBuffer value)
{
if (!headersContext.error())
{
String nameStr = name.getStringWithoutLengthUtf8(0, name.capacity());
String valueStr = value.getStringWithoutLengthUtf8(0, value.capacity());
headersContext.headers.put(nameStr, valueStr);
}
}
// Writes HPACK header field to http representation in a buffer
private void mapToHttp(DirectBuffer name, DirectBuffer value)
{
if (!headersContext.error())
{
factory.httpBeginExRW.headersItem(item -> item.name(name, 0, name.capacity())
.value(value, 0, value.capacity()));
}
}
private void decodeHeaderField(HpackHeaderFieldFW hf,
BiConsumer nameValue)
{
int index;
DirectBuffer name = null;
DirectBuffer value = null;
switch (hf.type())
{
case INDEXED :
index = hf.index();
if (!decodeContext.valid(index))
{
headersContext.connectionError = Http2ErrorCode.COMPRESSION_ERROR;
return;
}
name = decodeContext.nameBuffer(index);
value = decodeContext.valueBuffer(index);
nameValue.accept(name, value);
break;
case LITERAL :
HpackLiteralHeaderFieldFW literalRO = hf.literal();
if (literalRO.error())
{
headersContext.connectionError = Http2ErrorCode.COMPRESSION_ERROR;
return;
}
switch (literalRO.nameType())
{
case INDEXED:
{
index = literalRO.nameIndex();
name = decodeContext.nameBuffer(index);
HpackStringFW valueRO = literalRO.valueLiteral();
value = valueRO.payload();
if (valueRO.huffman())
{
MutableDirectBuffer dst = new UnsafeBuffer(new byte[4096]); // TODO
int length = HpackHuffman.decode(value, dst);
if (length == -1)
{
headersContext.connectionError = Http2ErrorCode.COMPRESSION_ERROR;
return;
}
value = new UnsafeBuffer(dst, 0, length);
}
nameValue.accept(name, value);
}
break;
case NEW:
{
HpackStringFW nameRO = literalRO.nameLiteral();
name = nameRO.payload();
if (nameRO.huffman())
{
MutableDirectBuffer dst = new UnsafeBuffer(new byte[4096]); // TODO
int length = HpackHuffman.decode(name, dst);
if (length == -1)
{
headersContext.connectionError = Http2ErrorCode.COMPRESSION_ERROR;
return;
}
name = new UnsafeBuffer(dst, 0, length);
}
HpackStringFW valueRO = literalRO.valueLiteral();
value = valueRO.payload();
if (valueRO.huffman())
{
MutableDirectBuffer dst = new UnsafeBuffer(new byte[4096]); // TODO
int length = HpackHuffman.decode(value, dst);
if (length == -1)
{
headersContext.connectionError = Http2ErrorCode.COMPRESSION_ERROR;
return;
}
value = new UnsafeBuffer(dst, 0, length);
}
nameValue.accept(name, value);
}
break;
}
if (literalRO.literalType() == INCREMENTAL_INDEXING)
{
// make a copy for name and value as they go into dynamic table (outlives current frame)
MutableDirectBuffer nameCopy = new UnsafeBuffer(new byte[name.capacity()]);
nameCopy.putBytes(0, name, 0, name.capacity());
MutableDirectBuffer valueCopy = new UnsafeBuffer(new byte[value.capacity()]);
valueCopy.putBytes(0, value, 0, value.capacity());
decodeContext.add(nameCopy, valueCopy);
}
break;
}
}
void mapPushPromise(ListFW httpHeaders, HpackHeaderBlockFW.Builder builder)
{
httpHeaders.forEach(h -> builder.header(b -> mapHeader(h, b)));
}
void mapHeaders(ListFW httpHeaders, HpackHeaderBlockFW.Builder builder)
{
encodeHeadersContext.reset();
httpHeaders.forEach(this::status) // notes if there is :status
.forEach(this::connectionHeaders); // collects all connection headers
if (!encodeHeadersContext.status)
{
builder.header(b -> b.indexed(8)); // no mandatory :status header, add :status: 200
}
httpHeaders.forEach(h ->
{
if (validHeader(h))
{
builder.header(b -> mapHeader(h, b));
}
});
}
void status(HttpHeaderFW httpHeader)
{
if (!encodeHeadersContext.status)
{
StringFW name = httpHeader.name();
String16FW value = httpHeader.value();
factory.nameRO.wrap(name.buffer(), name.offset() + 1, name.sizeof() - 1); // +1, -1 for length-prefixed buffer
factory.valueRO.wrap(value.buffer(), value.offset() + 2, value.sizeof() - 2);
if (factory.nameRO.equals(encodeContext.nameBuffer(8)))
{
encodeHeadersContext.status = true;
}
}
}
void connectionHeaders(HttpHeaderFW httpHeader)
{
StringFW name = httpHeader.name();
String16FW value = httpHeader.value();
factory.nameRO.wrap(name.buffer(), name.offset() + 1, name.sizeof() - 1); // +1, -1 for length-prefixed buffer
if (factory.nameRO.equals(CONNECTION))
{
String[] headers = value.asString().split(",");
for (String header : headers)
{
encodeHeadersContext.connectionHeaders.add(header.trim());
}
}
}
boolean validHeader(HttpHeaderFW httpHeader)
{
StringFW name = httpHeader.name();
String16FW value = httpHeader.value();
factory.nameRO.wrap(name.buffer(), name.offset() + 1, name.sizeof() - 1); // +1, -1 for length-prefixed buffer
factory.valueRO.wrap(value.buffer(), value.offset() + 2, value.sizeof() - 2);
// Removing 8.1.2.1 Pseudo-Header Fields
// Not sending error as it will allow requests to loop back
if (factory.nameRO.equals(encodeContext.nameBuffer(1)) || // :authority
factory.nameRO.equals(encodeContext.nameBuffer(2)) || // :method
factory.nameRO.equals(encodeContext.nameBuffer(4)) || // :path
factory.nameRO.equals(encodeContext.nameBuffer(6))) // :scheme
{
return false;
}
// Removing 8.1.2.2 connection-specific header fields from response
if (factory.nameRO.equals(encodeContext.nameBuffer(57)) || // transfer-encoding
factory.nameRO.equals(CONNECTION) ||
factory.nameRO.equals(KEEP_ALIVE) ||
factory.nameRO.equals(PROXY_CONNECTION) ||
factory.nameRO.equals(UPGRADE))
{
return false;
}
// Removing any header that is nominated by Connection header field
for(String connectionHeader: encodeHeadersContext.connectionHeaders)
{
if (name.asString().equals(connectionHeader))
{
return false;
}
}
return true;
}
// Map http1.1 header to http2 header field in HEADERS, PUSH_PROMISE request
private void mapHeader(HttpHeaderFW httpHeader, HpackHeaderFieldFW.Builder builder)
{
StringFW name = httpHeader.name();
String16FW value = httpHeader.value();
factory.nameRO.wrap(name.buffer(), name.offset() + 1, name.sizeof() - 1); // +1, -1 for length-prefixed buffer
factory.valueRO.wrap(value.buffer(), value.offset() + 2, value.sizeof() - 2);
int index = encodeContext.index(factory.nameRO, factory.valueRO);
if (index != -1)
{
// Indexed
builder.indexed(index);
}
else
{
// Literal
builder.literal(literalBuilder -> buildLiteral(literalBuilder, encodeContext));
}
}
// Building Literal representation of header field
// TODO dynamic table, huffman, never indexed
private void buildLiteral(
HpackLiteralHeaderFieldFW.Builder builder,
HpackContext hpackContext)
{
int nameIndex = hpackContext.index(factory.nameRO);
builder.type(WITHOUT_INDEXING);
if (nameIndex != -1)
{
builder.name(nameIndex);
}
else
{
builder.name(factory.nameRO, 0, factory.nameRO.capacity());
}
builder.value(factory.valueRO, 0, factory.valueRO.capacity());
}
void handleHttpBegin(BeginFW begin, MessageConsumer applicationReplyThrottle, long applicationReplyId,
Correlation correlation)
{
OctetsFW extension = begin.extension();
Http2Stream stream = http2Streams.get(correlation.http2StreamId);
if (stream == null)
{
factory.doReset(applicationReplyThrottle, applicationReplyId);
}
else
{
stream.applicationReplyThrottle = applicationReplyThrottle;
stream.applicationReplyId = applicationReplyId;
stream.sendHttpWindow();
if (extension.sizeof() > 0)
{
HttpBeginExFW beginEx = extension.get(factory.beginExRO::wrap);
writeScheduler.headers(correlation.http2StreamId, Http2Flags.NONE, beginEx.headers());
}
}
}
void handleHttpData(DataFW dataRO, Correlation correlation)
{
OctetsFW extension = dataRO.extension();
OctetsFW payload = dataRO.payload();
if (extension.sizeof() > 0)
{
int pushStreamId = correlation.pushStreamIds.applyAsInt(correlation.http2StreamId);
if (pushStreamId != -1)
{
int promisedStreamId = correlation.promisedStreamIds.getAsInt();
Http2DataExFW dataEx = extension.get(factory.dataExRO::wrap);
writeScheduler.pushPromise(pushStreamId, promisedStreamId, dataEx.headers());
correlation.pushHandler.accept(promisedStreamId, dataEx.headers());
}
}
if (payload.sizeof() > 0)
{
writeScheduler.data(correlation.http2StreamId, payload.buffer(), payload.offset(), payload.sizeof());
}
}
void handleHttpEnd(EndFW data, Correlation correlation)
{
Http2Stream stream = http2Streams.get(correlation.http2StreamId);
if (stream != null)
{
stream.onHttpEnd();
}
}
void handleHttpAbort(AbortFW abort, Correlation correlation)
{
Http2Stream stream = http2Streams.get(correlation.http2StreamId);
if (stream != null)
{
stream.onHttpAbort();
}
}
void doRstByUs(Http2Stream stream)
{
stream.onReset();
writeScheduler.rst(stream.http2StreamId, Http2ErrorCode.INTERNAL_ERROR);
closeStream(stream);
}
enum State
{
IDLE,
RESERVED_LOCAL,
RESERVED_REMOTE,
OPEN,
HALF_CLOSED_LOCAL,
HALF_CLOSED_REMOTE,
CLOSED
}
private static final class HeadersContext
{
Http2ErrorCode connectionError;
Map headers = new HashMap<>();
int method;
int scheme;
int path;
boolean regularHeader;
Http2ErrorCode streamError;
long contentLength = -1;
void reset()
{
connectionError = null;
headers.clear();
method = 0;
scheme = 0;
path = 0;
regularHeader = false;
streamError = null;
contentLength = -1;
}
boolean error()
{
return streamError != null || connectionError != null;
}
}
private static final class EncodeHeadersContext
{
boolean status;
final List connectionHeaders = new ArrayList<>();
void reset()
{
status = false;
connectionHeaders.clear();
}
}
@FunctionalInterface
private interface DecoderState
{
int decode(DirectBuffer buffer, int offset, int length);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy