Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.twitter.http2.HttpConnectionHandler Maven / Gradle / Ivy
/*
* Copyright 2015 Twitter, Inc.
*
* Licensed 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 com.twitter.http2;
import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.List;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandler;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.util.internal.EmptyArrays;
import static com.twitter.http2.HttpCodecUtil.HTTP_CONNECTION_STREAM_ID;
import static com.twitter.http2.HttpCodecUtil.isServerId;
/**
* Manages streams within an HTTP/2 connection.
*/
public class HttpConnectionHandler extends ByteToMessageDecoder
implements HttpFrameDecoderDelegate, ChannelOutboundHandler {
private static final HttpProtocolException PROTOCOL_EXCEPTION =
new HttpProtocolException();
static {
PROTOCOL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE);
}
private static final HttpSettingsFrame SETTINGS_ACK_FRAME =
new DefaultHttpSettingsFrame().setAck(true);
private static final int DEFAULT_HEADER_TABLE_SIZE = 4096;
private static final int DEFAULT_WINDOW_SIZE = 65535;
private int initialSendWindowSize = DEFAULT_WINDOW_SIZE;
private int initialReceiveWindowSize = DEFAULT_WINDOW_SIZE;
private volatile int initialConnectionReceiveWindowSize = DEFAULT_WINDOW_SIZE;
private final HttpConnection httpConnection =
new HttpConnection(initialSendWindowSize, initialReceiveWindowSize);
private int lastStreamId;
private static final int DEFAULT_MAX_CONCURRENT_STREAMS = Integer.MAX_VALUE;
private int remoteConcurrentStreams = DEFAULT_MAX_CONCURRENT_STREAMS;
private int localConcurrentStreams = DEFAULT_MAX_CONCURRENT_STREAMS;
private boolean sentGoAwayFrame;
private boolean receivedGoAwayFrame;
private final ChannelFutureListener connectionErrorListener =
new ConnectionErrorFutureListener();
private ChannelFutureListener closingChannelFutureListener;
private final boolean server;
private final boolean handleStreamWindowUpdates;
private final HttpFrameDecoder httpFrameDecoder;
private final HttpFrameEncoder httpFrameEncoder;
private final HttpHeaderBlockDecoder httpHeaderBlockDecoder;
private final HttpHeaderBlockEncoder httpHeaderBlockEncoder;
private HttpHeaderBlockFrame httpHeaderBlockFrame;
private HttpSettingsFrame httpSettingsFrame;
private boolean needSettingsAck;
private boolean changeDecoderHeaderTableSize;
private int headerTableSize;
private boolean changeEncoderHeaderTableSize;
private int lastHeaderTableSize = Integer.MAX_VALUE;
private int minHeaderTableSize = Integer.MAX_VALUE;
private boolean pushEnabled = true;
private ChannelHandlerContext context;
/**
* Creates a new connection handler.
*
* @param server {@code true} if and only if this connection handler should
* handle the server endpoint of the connection.
* {@code false} if and only if this connection handler should
* handle the client endpoint of the connection.
*/
public HttpConnectionHandler(boolean server) {
this(server, true);
}
/**
* Creates a new connection handler with the specified options.
*/
public HttpConnectionHandler(boolean server, boolean handleStreamWindowUpdates) {
this(server, handleStreamWindowUpdates, 8192, 16384);
}
/**
* Creates a new connection handler with the specified options.
*/
public HttpConnectionHandler(boolean server, int maxChunkSize, int maxHeaderSize) {
this(server, true, maxChunkSize, maxHeaderSize);
}
/**
* Creates a new connection handler with the specified options.
*/
public HttpConnectionHandler(
boolean server, boolean handleStreamWindowUpdates, int maxChunkSize, int maxHeaderSize) {
this.server = server;
this.handleStreamWindowUpdates = handleStreamWindowUpdates;
httpFrameDecoder = new HttpFrameDecoder(server, this, maxChunkSize);
httpFrameEncoder = new HttpFrameEncoder();
httpHeaderBlockDecoder = new HttpHeaderBlockDecoder(maxHeaderSize, DEFAULT_HEADER_TABLE_SIZE);
httpHeaderBlockEncoder = new HttpHeaderBlockEncoder(DEFAULT_HEADER_TABLE_SIZE);
}
public void setConnectionReceiveWindowSize(int connectionReceiveWindowSize) {
if (connectionReceiveWindowSize < 0) {
throw new IllegalArgumentException("connectionReceiveWindowSize");
}
// This will not send a window update frame immediately.
// If this value increases the allowed receive window size,
// a WINDOW_UPDATE frame will be sent when only half of the
// session window size remains during data frame processing.
// If this value decreases the allowed receive window size,
// the window will be reduced as data frames are processed.
initialConnectionReceiveWindowSize = connectionReceiveWindowSize;
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
super.handlerAdded(ctx);
context = ctx;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception {
httpFrameDecoder.decode(in);
}
/**
* {@inheritDoc}
*/
@Override
public void readDataFramePadding(int streamId, boolean endStream, int padding) {
int deltaWindowSize = -1 * padding;
int newConnectionWindowSize = httpConnection.updateReceiveWindowSize(
HTTP_CONNECTION_STREAM_ID, deltaWindowSize);
// Check if connection window size is reduced beyond allowable lower bound
if (newConnectionWindowSize < 0) {
issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
return;
}
// Send a WINDOW_UPDATE frame if less than half the connection window size remains
if (newConnectionWindowSize <= initialConnectionReceiveWindowSize / 2) {
int windowSizeIncrement = initialConnectionReceiveWindowSize - newConnectionWindowSize;
httpConnection.updateReceiveWindowSize(HTTP_CONNECTION_STREAM_ID, windowSizeIncrement);
ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame(
HTTP_CONNECTION_STREAM_ID, windowSizeIncrement);
context.writeAndFlush(frame);
}
// Check if we received a DATA frame for a stream which is half-closed (remote) or closed
if (httpConnection.isRemoteSideClosed(streamId)) {
if (streamId <= lastStreamId) {
issueStreamError(streamId, HttpErrorCode.STREAM_CLOSED);
} else if (!sentGoAwayFrame) {
issueStreamError(streamId, HttpErrorCode.PROTOCOL_ERROR);
}
return;
}
// Update receive window size
int newWindowSize = httpConnection.updateReceiveWindowSize(streamId, deltaWindowSize);
// Window size can become negative if we sent a SETTINGS frame that reduces the
// size of the transfer window after the peer has written data frames.
// The value is bounded by the length that SETTINGS frame decrease the window.
// This difference is stored for the connection when writing the SETTINGS frame
// and is cleared once we send a WINDOW_UPDATE frame.
if (newWindowSize < httpConnection.getReceiveWindowSizeLowerBound(streamId)) {
issueStreamError(streamId, HttpErrorCode.FLOW_CONTROL_ERROR);
return;
}
// Send a WINDOW_UPDATE frame if less than half the stream window size remains
// Recipient should not send a WINDOW_UPDATE frame as it consumes the last data frame.
if (handleStreamWindowUpdates && newWindowSize <= initialReceiveWindowSize / 2 && !endStream) {
int windowSizeIncrement = initialReceiveWindowSize - newWindowSize;
httpConnection.updateReceiveWindowSize(streamId, windowSizeIncrement);
ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame(streamId, windowSizeIncrement);
context.writeAndFlush(frame);
}
}
/**
* {@inheritDoc}
*/
@Override
public void readDataFrame(int streamId, boolean endStream, boolean endSegment, ByteBuf data) {
// HTTP/2 DATA frame processing requirements:
//
// If an endpoint receives a data frame for a Stream-ID which is not open
// and the endpoint has not sent a GOAWAY frame, it must issue a stream error
// with the error code INVALID_STREAM for the Stream-ID.
//
// If an endpoint receives multiple data frames for invalid Stream-IDs,
// it may close the connection.
//
// If an endpoint refuses a stream it must ignore any data frames for that stream.
//
// If an endpoint receives a data frame after the stream is half-closed (remote)
// or closed, it must respond with a stream error of type STREAM_CLOSED.
int deltaWindowSize = -1 * data.readableBytes();
int newConnectionWindowSize = httpConnection.updateReceiveWindowSize(
HTTP_CONNECTION_STREAM_ID, deltaWindowSize);
// Check if connection window size is reduced beyond allowable lower bound
if (newConnectionWindowSize < 0) {
issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
return;
}
// Send a WINDOW_UPDATE frame if less than half the connection window size remains
if (newConnectionWindowSize <= initialConnectionReceiveWindowSize / 2) {
int windowSizeIncrement = initialConnectionReceiveWindowSize - newConnectionWindowSize;
httpConnection.updateReceiveWindowSize(HTTP_CONNECTION_STREAM_ID, windowSizeIncrement);
ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame(
HTTP_CONNECTION_STREAM_ID, windowSizeIncrement);
context.writeAndFlush(frame);
}
// Check if we received a DATA frame for a stream which is half-closed (remote) or closed
if (httpConnection.isRemoteSideClosed(streamId)) {
if (streamId <= lastStreamId) {
issueStreamError(streamId, HttpErrorCode.STREAM_CLOSED);
} else if (!sentGoAwayFrame) {
issueStreamError(streamId, HttpErrorCode.PROTOCOL_ERROR);
}
return;
}
// Update receive window size
int newWindowSize = httpConnection.updateReceiveWindowSize(streamId, deltaWindowSize);
// Window size can become negative if we sent a SETTINGS frame that reduces the
// size of the transfer window after the peer has written data frames.
// The value is bounded by the length that SETTINGS frame decrease the window.
// This difference is stored for the connection when writing the SETTINGS frame
// and is cleared once we send a WINDOW_UPDATE frame.
if (newWindowSize < httpConnection.getReceiveWindowSizeLowerBound(streamId)) {
issueStreamError(streamId, HttpErrorCode.FLOW_CONTROL_ERROR);
return;
}
// Window size became negative due to sender writing frame before receiving SETTINGS
// Send data frames upstream in initialReceiveWindowSize chunks
if (newWindowSize < 0) {
while (data.readableBytes() > initialReceiveWindowSize) {
ByteBuf partialData = data.readBytes(initialReceiveWindowSize);
HttpDataFrame partialDataFrame = new DefaultHttpDataFrame(streamId, partialData);
context.fireChannelRead(partialDataFrame);
}
}
// Send a WINDOW_UPDATE frame if less than half the stream window size remains
// Recipient should not send a WINDOW_UPDATE frame as it consumes the last data frame.
if (handleStreamWindowUpdates && newWindowSize <= initialReceiveWindowSize / 2 && !endStream) {
int windowSizeIncrement = initialReceiveWindowSize - newWindowSize;
httpConnection.updateReceiveWindowSize(streamId, windowSizeIncrement);
ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame(streamId, windowSizeIncrement);
context.writeAndFlush(frame);
}
// Close the remote side of the stream if this is the last frame
if (endStream) {
halfCloseStream(streamId, true, context.channel().newSucceededFuture());
}
HttpDataFrame httpDataFrame = new DefaultHttpDataFrame(streamId, data);
httpDataFrame.setLast(endStream);
context.fireChannelRead(httpDataFrame);
}
/**
* {@inheritDoc}
*/
@Override
public void readHeadersFrame(
int streamId,
boolean endStream,
boolean endSegment,
boolean exclusive,
int dependency,
int weight
) {
// HTTP/2 HEADERS frame processing requirements:
//
// If an endpoint receives a HEADERS frame with a Stream-ID that is less than
// any previously received HEADERS, it must issue a connection error of type
// PROTOCOL_ERROR.
//
// If an endpoint receives multiple SYN_STREAM frames with the same active
// Stream-ID, it must issue a stream error with the status code PROTOCOL_ERROR.
//
// The recipient can reject a stream by sending a stream error with the
// status code REFUSED_STREAM.
if (isRemoteInitiatedId(streamId)) {
if (streamId <= lastStreamId) {
// Check if we received a HEADERS frame for a stream which is half-closed (remote) or closed
if (httpConnection.isRemoteSideClosed(streamId)) {
issueStreamError(streamId, HttpErrorCode.STREAM_CLOSED);
return;
}
} else {
// Try to accept the stream
if (!acceptStream(streamId, exclusive, dependency, weight)) {
issueStreamError(streamId, HttpErrorCode.REFUSED_STREAM);
return;
}
}
} else {
// Check if we received a HEADERS frame for a stream which is half-closed (remote) or closed
if (httpConnection.isRemoteSideClosed(streamId)) {
issueStreamError(streamId, HttpErrorCode.STREAM_CLOSED);
return;
}
}
// Close the remote side of the stream if this is the last frame
if (endStream) {
halfCloseStream(streamId, true, context.channel().newSucceededFuture());
}
HttpHeadersFrame httpHeadersFrame = new DefaultHttpHeadersFrame(streamId);
httpHeadersFrame.setLast(endStream);
httpHeadersFrame.setExclusive(exclusive);
httpHeadersFrame.setDependency(dependency);
httpHeadersFrame.setWeight(weight);
httpHeaderBlockFrame = httpHeadersFrame;
}
/**
* {@inheritDoc}
*/
@Override
public void readPriorityFrame(int streamId, boolean exclusive, int dependency, int weight) {
if (streamId == dependency) {
issueStreamError(streamId, HttpErrorCode.PROTOCOL_ERROR);
} else {
setPriority(streamId, exclusive, dependency, weight);
}
}
/**
* {@inheritDoc}
*/
@Override
public void readRstStreamFrame(int streamId, int errorCode) {
// If a RST_STREAM frame identifying an idle stream is received,
// the recipient MUST treat this as a connection error of type
// PROTOCOL_ERROR.
removeStream(streamId, context.channel().newSucceededFuture());
HttpRstStreamFrame httpRstStreamFrame = new DefaultHttpRstStreamFrame(streamId, errorCode);
context.fireChannelRead(httpRstStreamFrame);
}
/**
* {@inheritDoc}
*/
@Override
public void readSettingsFrame(boolean ack) {
needSettingsAck = !ack;
httpSettingsFrame = new DefaultHttpSettingsFrame();
httpSettingsFrame.setAck(ack);
if (ack && changeDecoderHeaderTableSize) {
httpHeaderBlockDecoder.setMaxHeaderTableSize(headerTableSize);
changeDecoderHeaderTableSize = false;
}
}
/**
* {@inheritDoc}
*/
@Override
public void readSetting(int id, int value) {
httpSettingsFrame.setValue(id, value);
switch (id) {
case HttpSettingsFrame.SETTINGS_HEADER_TABLE_SIZE:
// Ignore 'negative' values -- they are too large for java
if (value >= 0) {
changeEncoderHeaderTableSize = true;
lastHeaderTableSize = value;
if (lastHeaderTableSize < minHeaderTableSize) {
minHeaderTableSize = lastHeaderTableSize;
}
}
break;
case HttpSettingsFrame.SETTINGS_ENABLE_PUSH:
if (value == 0) {
pushEnabled = false;
} else if (value == 1) {
pushEnabled = true;
} else {
issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
}
break;
case HttpSettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS:
if (value >= 0) {
remoteConcurrentStreams = value;
}
break;
case HttpSettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE:
if (value >= 0) {
updateInitialSendWindowSize(value);
} else {
issueConnectionError(HttpErrorCode.FLOW_CONTROL_ERROR);
}
break;
case HttpSettingsFrame.SETTINGS_MAX_FRAME_SIZE:
if (value != HttpCodecUtil.HTTP_MAX_LENGTH) {
issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
}
break;
default:
// Ignore Unknown Settings
}
}
/**
* {@inheritDoc}
*/
@Override
public void readSettingsEnd() {
if (changeEncoderHeaderTableSize) {
synchronized (httpHeaderBlockEncoder) {
httpHeaderBlockEncoder.setDecoderMaxHeaderTableSize(minHeaderTableSize);
httpHeaderBlockEncoder.setDecoderMaxHeaderTableSize(lastHeaderTableSize);
// Writes of settings ack must occur in order
ByteBuf frame = httpFrameEncoder.encodeSettingsFrame(SETTINGS_ACK_FRAME);
context.writeAndFlush(frame);
}
changeEncoderHeaderTableSize = false;
lastHeaderTableSize = Integer.MAX_VALUE;
minHeaderTableSize = Integer.MAX_VALUE;
} else if (needSettingsAck) {
ByteBuf frame = httpFrameEncoder.encodeSettingsFrame(SETTINGS_ACK_FRAME);
context.writeAndFlush(frame);
}
Object frame = httpSettingsFrame;
httpSettingsFrame = null;
context.fireChannelRead(frame);
}
/**
* {@inheritDoc}
*/
@Override
public void readPushPromiseFrame(int streamId, int promisedStreamId) {
// TODO(jpinner) handle push promise frames
// Any we receive must be associated with a "peer-initiated" stream.
// Since we don't have a way currently to initiate streams, any
// frame that we receive must be treated as a protocol error.
issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
}
/**
* {@inheritDoc}
*/
@Override
public void readPingFrame(long data, boolean ack) {
// HTTP/2 PING frame processing requirements:
//
// Receivers of a PING frame should send an identical frame to the sender
// as soon as possible.
//
// Receivers of a PING frame must ignore frames that it did not initiate
HttpPingFrame httpPingFrame = new DefaultHttpPingFrame(data);
httpPingFrame.setPong(true);
if (ack) {
context.fireChannelRead(httpPingFrame);
} else {
ByteBuf frame = httpFrameEncoder.encodePingFrame(data, false);
context.writeAndFlush(frame);
}
}
/**
* {@inheritDoc}
*/
@Override
public void readGoAwayFrame(int lastStreamId, int errorCode) {
receivedGoAwayFrame = true;
HttpGoAwayFrame httpGoAwayFrame = new DefaultHttpGoAwayFrame(lastStreamId, errorCode);
context.fireChannelRead(httpGoAwayFrame);
}
/**
* {@inheritDoc}
*/
@Override
public void readWindowUpdateFrame(int streamId, int windowSizeIncrement) {
// HTTP/2 WINDOW_UPDATE frame processing requirements:
//
// Receivers of a WINDOW_UPDATE that cause the window size to exceed 2^31
// must send a RST_STREAM with the status code FLOW_CONTROL_ERROR.
//
// Sender should ignore all WINDOW_UPDATE frames associated with a stream
// after sending the last frame for the stream.
// Ignore frames for half-closed streams
if (streamId != HTTP_CONNECTION_STREAM_ID && httpConnection.isLocalSideClosed(streamId)) {
return;
}
// Check for numerical overflow
if (httpConnection.getSendWindowSize(streamId) > Integer.MAX_VALUE - windowSizeIncrement) {
if (streamId == HTTP_CONNECTION_STREAM_ID) {
issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
} else {
issueStreamError(streamId, HttpErrorCode.FLOW_CONTROL_ERROR);
}
return;
}
updateSendWindowSize(context, streamId, windowSizeIncrement);
}
/**
* {@inheritDoc}
*/
@Override
public void readHeaderBlock(ByteBuf headerBlockFragment) {
try {
httpHeaderBlockDecoder.decode(headerBlockFragment, httpHeaderBlockFrame);
} catch (IOException e) {
httpHeaderBlockFrame = null;
issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
}
}
/**
* {@inheritDoc}
*/
@Override
public void readHeaderBlockEnd() {
httpHeaderBlockDecoder.endHeaderBlock(httpHeaderBlockFrame);
if (httpHeaderBlockFrame == null) {
return;
}
// Check if we received a valid Header Block
if (httpHeaderBlockFrame.isInvalid()) {
issueStreamError(httpHeaderBlockFrame.getStreamId(), HttpErrorCode.PROTOCOL_ERROR);
return;
}
Object frame = httpHeaderBlockFrame;
httpHeaderBlockFrame = null;
context.fireChannelRead(frame);
}
/**
* {@inheritDoc}
*/
@Override
public void readFrameError(String message) {
issueConnectionError(HttpErrorCode.PROTOCOL_ERROR);
}
@Override
public void bind(
ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
throws Exception {
ctx.bind(localAddress, promise);
}
@Override
public void connect(
ChannelHandlerContext ctx,
SocketAddress remoteAddress,
SocketAddress localAddress,
ChannelPromise promise
) throws Exception {
ctx.connect(remoteAddress, localAddress, promise);
}
@Override
public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
// HTTP/2 connection requirements:
//
// When either endpoint closes the transport-level connection,
// it must first send a GOAWAY frame.
//
// Avoid NotYetConnectedException
if (!ctx.channel().isActive()) {
ctx.disconnect(promise);
} else {
sendGoAwayFrame(ctx, promise);
}
}
@Override
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
// HTTP/2 connection requirements:
//
// When either endpoint closes the transport-level connection,
// it must first send a GOAWAY frame.
//
// Avoid NotYetConnectedException
if (!ctx.channel().isActive()) {
ctx.close(promise);
} else {
sendGoAwayFrame(ctx, promise);
}
}
@Override
public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.deregister(promise);
}
@Override
public void read(ChannelHandlerContext ctx) throws Exception {
ctx.read();
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
throws Exception {
if (msg instanceof HttpDataFrame) {
HttpDataFrame httpDataFrame = (HttpDataFrame) msg;
int streamId = httpDataFrame.getStreamId();
// Frames must not be sent on half-closed streams
if (httpConnection.isLocalSideClosed(streamId)) {
promise.setFailure(PROTOCOL_EXCEPTION);
return;
}
// HTTP/2 DATA frame flow control processing requirements:
//
// Sender must not send a data frame with data length greater
// than the transfer window size.
//
// After sending each data frame, the sender decrements its
// transfer window size by the amount of data transmitted.
//
// When the window size becomes less than or equal to 0, the
// sender must pause transmitting data frames.
int dataLength = httpDataFrame.content().readableBytes();
int sendWindowSize = httpConnection.getSendWindowSize(streamId);
int connectionSendWindowSize = httpConnection.getSendWindowSize(
HTTP_CONNECTION_STREAM_ID);
sendWindowSize = Math.min(sendWindowSize, connectionSendWindowSize);
if (sendWindowSize <= 0) {
// Stream is stalled -- enqueue Data frame and return
httpConnection.putPendingWrite(
streamId, new HttpConnection.PendingWrite(httpDataFrame, promise));
return;
} else if (sendWindowSize < dataLength) {
// Stream is not stalled but we cannot send the entire frame
httpConnection.updateSendWindowSize(streamId, -1 * sendWindowSize);
httpConnection.updateSendWindowSize(HTTP_CONNECTION_STREAM_ID, -1 * sendWindowSize);
// Create a partial data frame whose length is the current window size
ByteBuf data = httpDataFrame.content().readSlice(sendWindowSize).retain();
ByteBuf partialDataFrame = httpFrameEncoder.encodeDataFrame(streamId, false, data);
// Enqueue the remaining data (will be the first frame queued)
httpConnection.putPendingWrite(
streamId, new HttpConnection.PendingWrite(httpDataFrame, promise));
ChannelPromise writeFuture = ctx.channel().newPromise();
// The transfer window size is pre-decremented when sending a data frame downstream.
// Close the connection on write failures that leaves the transfer window in a corrupt state.
writeFuture.addListener(connectionErrorListener);
ctx.write(partialDataFrame, writeFuture);
return;
} else {
// Window size is large enough to send entire data frame
httpConnection.updateSendWindowSize(streamId, -1 * dataLength);
httpConnection.updateSendWindowSize(HTTP_CONNECTION_STREAM_ID, -1 * dataLength);
// The transfer window size is pre-decremented when sending a data frame downstream.
// Close the connection on write failures that leaves the transfer window in a corrupt state.
promise.addListener(connectionErrorListener);
}
// Close the local side of the stream if this is the last frame
if (httpDataFrame.isLast()) {
halfCloseStream(streamId, false, promise);
}
ByteBuf frame = httpFrameEncoder.encodeDataFrame(
streamId,
httpDataFrame.isLast(),
httpDataFrame.content()
);
ctx.write(frame, promise);
} else if (msg instanceof HttpHeadersFrame) {
HttpHeadersFrame httpHeadersFrame = (HttpHeadersFrame) msg;
int streamId = httpHeadersFrame.getStreamId();
if (isRemoteInitiatedId(streamId)) {
if (streamId <= lastStreamId) {
// Attempting to send headers for an older stream
// (older than the latest accepted remote initiated stream)
// Ensure that the frames are not sent on a half-closed (local) or closed streams
if (httpConnection.isLocalSideClosed(streamId)) {
promise.setFailure(PROTOCOL_EXCEPTION);
return;
}
} else {
// If we are attempting to write to a remote initiated stream id which is greater than the latest
// accepted stream Id then we must throw a protocol exception! i.e we cannot write on a remote
// initiated stream which we have not accepted before
promise.setFailure(PROTOCOL_EXCEPTION);
return;
}
} else {
// This is a locally initiated stream (Push)
boolean exclusive = httpHeadersFrame.isExclusive();
int dependency = httpHeadersFrame.getDependency();
int weight = httpHeadersFrame.getWeight();
if (!acceptStream(streamId, exclusive, dependency, weight)) {
promise.setFailure(PROTOCOL_EXCEPTION);
return;
}
}
// Close the local side of the stream if this is the last frame
if (httpHeadersFrame.isLast()) {
halfCloseStream(streamId, false, promise);
}
synchronized (httpHeaderBlockEncoder) {
ByteBuf frame = httpFrameEncoder.encodeHeadersFrame(
httpHeadersFrame.getStreamId(),
httpHeadersFrame.isLast(),
httpHeadersFrame.isExclusive(),
httpHeadersFrame.getDependency(),
httpHeadersFrame.getWeight(),
httpHeaderBlockEncoder.encode(ctx, httpHeadersFrame)
);
// Writes of compressed data must occur in order
ctx.write(frame, promise);
}
} else if (msg instanceof HttpPriorityFrame) {
HttpPriorityFrame httpPriorityFrame = (HttpPriorityFrame) msg;
int streamId = httpPriorityFrame.getStreamId();
boolean exclusive = httpPriorityFrame.isExclusive();
int dependency = httpPriorityFrame.getDependency();
int weight = httpPriorityFrame.getWeight();
setPriority(streamId, exclusive, dependency, weight);
ByteBuf frame = httpFrameEncoder.encodePriorityFrame(
streamId,
exclusive,
dependency,
weight
);
ctx.write(frame, promise);
} else if (msg instanceof HttpRstStreamFrame) {
HttpRstStreamFrame httpRstStreamFrame = (HttpRstStreamFrame) msg;
removeStream(httpRstStreamFrame.getStreamId(), promise);
ByteBuf frame = httpFrameEncoder.encodeRstStreamFrame(
httpRstStreamFrame.getStreamId(),
httpRstStreamFrame.getErrorCode().getCode());
ctx.write(frame, promise);
} else if (msg instanceof HttpSettingsFrame) {
// TODO(jpinner) currently cannot have more than one settings frame outstanding at a time
HttpSettingsFrame httpSettingsFrame = (HttpSettingsFrame) msg;
if (httpSettingsFrame.isAck()) {
// Cannot send an acknowledgement frame
promise.setFailure(PROTOCOL_EXCEPTION);
return;
}
int newHeaderTableSize =
httpSettingsFrame.getValue(HttpSettingsFrame.SETTINGS_HEADER_TABLE_SIZE);
if (newHeaderTableSize >= 0) {
headerTableSize = newHeaderTableSize;
changeDecoderHeaderTableSize = true;
}
int newConcurrentStreams =
httpSettingsFrame.getValue(HttpSettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS);
if (newConcurrentStreams >= 0) {
localConcurrentStreams = newConcurrentStreams;
}
int newInitialWindowSize =
httpSettingsFrame.getValue(HttpSettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE);
if (newInitialWindowSize >= 0) {
updateInitialReceiveWindowSize(newInitialWindowSize);
}
ByteBuf frame = httpFrameEncoder.encodeSettingsFrame(httpSettingsFrame);
ctx.write(frame, promise);
} else if (msg instanceof HttpPushPromiseFrame) {
if (!pushEnabled) {
promise.setFailure(PROTOCOL_EXCEPTION);
return;
}
synchronized (httpHeaderBlockEncoder) {
HttpPushPromiseFrame httpPushPromiseFrame = (HttpPushPromiseFrame) msg;
ByteBuf frame = httpFrameEncoder.encodePushPromiseFrame(
httpPushPromiseFrame.getStreamId(),
httpPushPromiseFrame.getPromisedStreamId(),
httpHeaderBlockEncoder.encode(ctx, httpPushPromiseFrame)
);
// Writes of compressed data must occur in order
ctx.write(frame, promise);
}
} else if (msg instanceof HttpPingFrame) {
HttpPingFrame httpPingFrame = (HttpPingFrame) msg;
if (httpPingFrame.isPong()) {
// Cannot send a PONG frame
promise.setFailure(PROTOCOL_EXCEPTION);
} else {
ByteBuf frame = httpFrameEncoder.encodePingFrame(httpPingFrame.getData(), false);
ctx.write(frame, promise);
}
} else if (msg instanceof HttpGoAwayFrame) {
// Why is this being sent? Intercept it and fail the write.
// Should have sent a CLOSE ChannelStateEvent
promise.setFailure(PROTOCOL_EXCEPTION);
} else if (msg instanceof HttpWindowUpdateFrame) {
HttpWindowUpdateFrame httpWindowUpdateFrame = (HttpWindowUpdateFrame) msg;
int streamId = httpWindowUpdateFrame.getStreamId();
if (handleStreamWindowUpdates || streamId == HTTP_CONNECTION_STREAM_ID) {
// Why is this being sent? Intercept it and fail the write.
promise.setFailure(PROTOCOL_EXCEPTION);
} else {
int windowSizeIncrement = httpWindowUpdateFrame.getWindowSizeIncrement();
httpConnection.updateReceiveWindowSize(streamId, windowSizeIncrement);
ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame(streamId, windowSizeIncrement);
ctx.write(frame, promise);
}
} else {
ctx.write(msg, promise);
}
}
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
// HTTP/2 Connection Error Handling:
//
// When a connection error occurs, the endpoint encountering the error must first
// send a GOAWAY frame with the stream identifier of the most recently received stream
// from the remote endpoint, and the error code for why the connection is terminating.
//
// After sending the GOAWAY frame, the endpoint must close the TCP connection.
private void issueConnectionError(HttpErrorCode status) {
ChannelFuture future = sendGoAwayFrame(status);
future.addListener(ChannelFutureListener.CLOSE);
}
// Http/2 Stream Error Handling:
//
// Upon a stream error, the endpoint must send a RST_STREAM frame which contains
// the Stream-ID for the stream where the error occurred and the error status which
// caused the error.
//
// After sending the RST_STREAM, the stream is closed to the sending endpoint.
//
// Note: this is only called by the worker thread
private void issueStreamError(int streamId, HttpErrorCode errorCode) {
boolean fireMessageReceived = !httpConnection.isRemoteSideClosed(streamId);
ChannelPromise promise = context.channel().newPromise();
removeStream(streamId, promise);
ByteBuf frame = httpFrameEncoder.encodeRstStreamFrame(streamId, errorCode.getCode());
context.writeAndFlush(frame, promise);
if (fireMessageReceived) {
HttpRstStreamFrame httpRstStreamFrame = new DefaultHttpRstStreamFrame(streamId, errorCode);
context.fireChannelRead(httpRstStreamFrame);
}
}
// Helper functions
private boolean isRemoteInitiatedId(int id) {
boolean serverId = isServerId(id);
return server && !serverId || !server && serverId;
}
// need to synchronize to prevent new streams from being created while updating active streams
private synchronized void updateInitialSendWindowSize(int newInitialWindowSize) {
int deltaWindowSize = newInitialWindowSize - initialSendWindowSize;
initialSendWindowSize = newInitialWindowSize;
httpConnection.updateAllSendWindowSizes(deltaWindowSize);
}
// need to synchronize to prevent new streams from being created while updating active streams
private synchronized void updateInitialReceiveWindowSize(int newInitialWindowSize) {
int deltaWindowSize = newInitialWindowSize - initialReceiveWindowSize;
initialReceiveWindowSize = newInitialWindowSize;
httpConnection.updateAllReceiveWindowSizes(deltaWindowSize);
}
// need to synchronize accesses to sentGoAwayFrame, lastStreamId, and initial window sizes
private synchronized boolean acceptStream(
int streamId, boolean exclusive, int dependency, int weight) {
// Cannot initiate any new streams after receiving or sending GOAWAY
if (receivedGoAwayFrame || sentGoAwayFrame) {
return false;
}
boolean remote = isRemoteInitiatedId(streamId);
int maxConcurrentStreams = remote ? localConcurrentStreams : remoteConcurrentStreams;
if (httpConnection.numActiveStreams(remote) >= maxConcurrentStreams) {
return false;
}
httpConnection.acceptStream(
streamId, false, false, initialSendWindowSize, initialReceiveWindowSize, remote);
if (remote) {
lastStreamId = streamId;
}
setPriority(streamId, exclusive, dependency, weight);
return true;
}
private synchronized boolean setPriority(
int streamId, boolean exclusive, int dependency, int weight) {
return httpConnection.setPriority(streamId, exclusive, dependency, weight);
}
private void halfCloseStream(int streamId, boolean remote, ChannelFuture future) {
if (remote) {
httpConnection.closeRemoteSide(streamId, isRemoteInitiatedId(streamId));
} else {
httpConnection.closeLocalSide(streamId, isRemoteInitiatedId(streamId));
}
if (closingChannelFutureListener != null && httpConnection.noActiveStreams()) {
future.addListener(closingChannelFutureListener);
}
}
private void removeStream(int streamId, ChannelFuture future) {
httpConnection.removeStream(streamId, isRemoteInitiatedId(streamId));
if (closingChannelFutureListener != null && httpConnection.noActiveStreams()) {
future.addListener(closingChannelFutureListener);
}
}
private void updateSendWindowSize(
ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) {
httpConnection.updateSendWindowSize(streamId, windowSizeIncrement);
while (true) {
// Check if we have unblocked a stalled stream
HttpConnection.PendingWrite e = httpConnection.getPendingWrite(streamId);
if (e == null) {
break;
}
HttpDataFrame httpDataFrame = e.httpDataFrame;
int dataFrameSize = httpDataFrame.content().readableBytes();
int writeStreamId = httpDataFrame.getStreamId();
int sendWindowSize = httpConnection.getSendWindowSize(writeStreamId);
int connectionSendWindowSize = httpConnection.getSendWindowSize(
HTTP_CONNECTION_STREAM_ID);
sendWindowSize = Math.min(sendWindowSize, connectionSendWindowSize);
if (sendWindowSize <= 0) {
return;
} else if (sendWindowSize < dataFrameSize) {
// We can send a partial frame
httpConnection.updateSendWindowSize(writeStreamId, -1 * sendWindowSize);
httpConnection.updateSendWindowSize(HTTP_CONNECTION_STREAM_ID, -1 * sendWindowSize);
// Create a partial data frame whose length is the current window size
ByteBuf data = httpDataFrame.content().readSlice(sendWindowSize).retain();
ByteBuf partialDataFrame = httpFrameEncoder.encodeDataFrame(writeStreamId, false, data);
ChannelPromise writeFuture = ctx.channel().newPromise();
// The transfer window size is pre-decremented when sending a data frame downstream.
// Close the connection on write failures that leaves the transfer window in a corrupt state.
writeFuture.addListener(connectionErrorListener);
ctx.writeAndFlush(partialDataFrame, writeFuture);
} else {
// Window size is large enough to send entire data frame
httpConnection.removePendingWrite(writeStreamId);
httpConnection.updateSendWindowSize(writeStreamId, -1 * dataFrameSize);
httpConnection.updateSendWindowSize(HTTP_CONNECTION_STREAM_ID, -1 * dataFrameSize);
// The transfer window size is pre-decremented when sending a data frame downstream.
// Close the connection on write failures that leaves the transfer window in a corrupt state.
e.promise.addListener(connectionErrorListener);
// Close the local side of the stream if this is the last frame
if (httpDataFrame.isLast()) {
halfCloseStream(writeStreamId, false, e.promise);
}
ByteBuf frame = httpFrameEncoder.encodeDataFrame(
writeStreamId,
httpDataFrame.isLast(),
httpDataFrame.content()
);
ctx.writeAndFlush(frame, e.promise);
}
}
}
private void sendGoAwayFrame(ChannelHandlerContext ctx, ChannelPromise promise) {
ChannelFuture future = sendGoAwayFrame(HttpErrorCode.NO_ERROR);
if (httpConnection.noActiveStreams()) {
future.addListener(new ClosingChannelFutureListener(ctx, promise));
} else {
closingChannelFutureListener = new ClosingChannelFutureListener(ctx, promise);
}
}
private synchronized ChannelFuture sendGoAwayFrame(HttpErrorCode httpErrorCode) {
if (!sentGoAwayFrame) {
sentGoAwayFrame = true;
ChannelPromise promise = context.channel().newPromise();
ByteBuf frame = httpFrameEncoder.encodeGoAwayFrame(lastStreamId, httpErrorCode.getCode());
context.writeAndFlush(frame, promise);
return promise;
}
return context.channel().newSucceededFuture();
}
private final class ConnectionErrorFutureListener implements ChannelFutureListener {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
issueConnectionError(HttpErrorCode.INTERNAL_ERROR);
}
}
}
private static final class ClosingChannelFutureListener implements ChannelFutureListener {
private final ChannelHandlerContext ctx;
private final ChannelPromise promise;
ClosingChannelFutureListener(ChannelHandlerContext ctx, ChannelPromise promise) {
this.ctx = ctx;
this.promise = promise;
}
public void operationComplete(ChannelFuture sentGoAwayFuture) throws Exception {
if (!(sentGoAwayFuture.cause() instanceof ClosedChannelException)) {
ctx.close(promise);
} else {
promise.setSuccess();
}
}
}
}