netty5/codec-http2/src/main/java/io/netty/handler/codec/http2/Http2FrameCodec.java

371 lines
17 KiB
Java
Raw Normal View History

/*
* Copyright 2016 The Netty Project
*
* The Netty 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 io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.UnsupportedMessageTypeException;
import io.netty.handler.codec.http.HttpServerUpgradeHandler.UpgradeEvent;
import io.netty.handler.codec.http2.Http2Connection.Endpoint;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.internal.UnstableApi;
import static io.netty.handler.codec.http2.Http2CodecUtil.isOutboundStream;
import static io.netty.handler.codec.http2.Http2CodecUtil.isStreamIdValid;
import static io.netty.handler.logging.LogLevel.INFO;
/**
* An HTTP/2 handler that maps HTTP/2 frames to {@link Http2Frame} objects and vice versa. For every incoming HTTP/2
* frame a {@link Http2Frame} object is created and propagated via {@link #channelRead}. Outbound {@link Http2Frame}
* objects received via {@link #write} are converted to the HTTP/2 wire format.
*
* <p>A change in stream state is propagated through the channel pipeline as a user event via
* {@link Http2StreamStateEvent} objects. When a HTTP/2 stream first becomes active a {@link Http2StreamActiveEvent}
* and when it gets closed a {@link Http2StreamClosedEvent} is emitted.
*
* <p>Server-side HTTP to HTTP/2 upgrade is supported in conjunction with {@link Http2ServerUpgradeCodec}; the necessary
* HTTP-to-HTTP/2 conversion is performed automatically.
*
* <p><em>This API is very immature.</em> The Http2Connection-based API is currently preferred over
* this API. This API is targeted to eventually replace or reduce the need for the Http2Connection-based API.
*
* <h3>Opening and Closing Streams</h3>
*
* <p>When the remote side opens a new stream, the frame codec first emits a {@link Http2StreamActiveEvent} with the
* stream identifier set.
* <pre>
* {@link Http2FrameCodec} {@link Http2MultiplexCodec}
* + +
* | Http2StreamActiveEvent(streamId=3, headers=null) |
* +------------------------------------------------------------->
* | |
* | Http2HeadersFrame(streamId=3) |
* +------------------------------------------------------------->
* | |
* + +
* </pre>
*
* <p>When a stream is closed either due to a reset frame by the remote side, or due to both sides having sent frames
* with the END_STREAM flag, then the frame codec emits a {@link Http2StreamClosedEvent}.
* <pre>
* {@link Http2FrameCodec} {@link Http2MultiplexCodec}
* + +
* | Http2StreamClosedEvent(streamId=3) |
* +--------------------------------------------------------->
* | |
* + +
* </pre>
*
* <p>When the local side wants to close a stream, it has to write a {@link Http2ResetFrame} to which the frame codec
* will respond to with a {@link Http2StreamClosedEvent}.
* <pre>
* {@link Http2FrameCodec} {@link Http2MultiplexCodec}
* + +
* | Http2ResetFrame(streamId=3) |
* <---------------------------------------------------------+
* | |
* | Http2StreamClosedEvent(streamId=3) |
* +--------------------------------------------------------->
* | |
* + +
* </pre>
*
* <p>Opening an outbound/local stream works by first sending the frame codec a {@link Http2HeadersFrame} with no
* stream identifier set (such that {@link Http2CodecUtil#isStreamIdValid} returns {@code false}). If opening the stream
* was successful, the frame codec responds with a {@link Http2StreamActiveEvent} that contains the stream's new
* identifier as well as the <em>same</em> {@link Http2HeadersFrame} object that opened the stream.
* <pre>
* {@link Http2FrameCodec} {@link Http2MultiplexCodec}
* + +
* | Http2HeadersFrame(streamId=-1) |
* <-----------------------------------------------------------------------------------------------+
* | |
* | Http2StreamActiveEvent(streamId=2, headers=Http2HeadersFrame(streamId=-1)) |
* +----------------------------------------------------------------------------------------------->
* | |
* + +
* </pre>
*/
@UnstableApi
public class Http2FrameCodec extends ChannelDuplexHandler {
private static final Http2FrameLogger HTTP2_FRAME_LOGGER = new Http2FrameLogger(INFO, Http2FrameCodec.class);
private final Http2ConnectionHandler http2Handler;
private final boolean server;
private ChannelHandlerContext ctx;
private ChannelHandlerContext http2HandlerCtx;
/**
* Construct a new handler.
*
* @param server {@code true} this is a server
*/
public Http2FrameCodec(boolean server) {
this(server, HTTP2_FRAME_LOGGER);
}
/**
* Construct a new handler.
*
* @param server {@code true} this is a server
*/
public Http2FrameCodec(boolean server, Http2FrameLogger frameLogger) {
this(server, new DefaultHttp2FrameWriter(), frameLogger, new Http2Settings());
}
// Visible for testing
Http2FrameCodec(boolean server, Http2FrameWriter frameWriter, Http2FrameLogger frameLogger,
Http2Settings initialSettings) {
HTTP/2 Non Active Stream RFC Corrections Motivation: codec-http2 couples the dependency tree state with the remainder of the stream state (Http2Stream). This makes implementing constraints where stream state and dependency tree state diverge in the RFC challenging. For example the RFC recommends retaining dependency tree state after a stream transitions to closed [1]. Dependency tree state can be exchanged on streams in IDLE. In practice clients may use stream IDs for the purpose of establishing QoS classes and therefore retaining this dependency tree state can be important to client perceived performance. It is difficult to limit the total amount of state we retain when stream state and dependency tree state is combined. Modifications: - Remove dependency tree, priority, and weight related items from public facing Http2Connection and Http2Stream APIs. This information is optional to track and depends on the flow controller implementation. - Move all dependency tree, priority, and weight related code from DefaultHttp2Connection to WeightedFairQueueByteDistributor. This is currently the only place which cares about priority. We can pull out the dependency tree related code in the future if it is generally useful to expose for other implementations. - DefaultHttp2Connection should explicitly limit the number of reserved streams now that IDLE streams are no longer created. Result: More compliant with the HTTP/2 RFC. Fixes https://github.com/netty/netty/issues/6206. [1] https://tools.ietf.org/html/rfc7540#section-5.3.4
2017-01-24 21:50:39 +01:00
// TODO(scott): configure maxReservedStreams when API is more finalized.
Http2Connection connection = new DefaultHttp2Connection(server);
frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger);
Http2ConnectionEncoder encoder = new DefaultHttp2ConnectionEncoder(connection, frameWriter);
HTTP/2 Max Header List Size Bug Motivation: If the HPACK Decoder detects that SETTINGS_MAX_HEADER_LIST_SIZE has been violated it aborts immediately and sends a RST_STREAM frame for what ever stream caused the issue. Because HPACK is stateful this means that the HPACK state may become out of sync between peers, and the issue won't be detected until the next headers frame. We should make a best effort to keep processing to keep the HPACK state in sync with our peer, or completely close the connection. If the HPACK Encoder is configured to verify SETTINGS_MAX_HEADER_LIST_SIZE it checks the limit and encodes at the same time. This may result in modifying the HPACK local state but not sending the headers to the peer if SETTINGS_MAX_HEADER_LIST_SIZE is violated. This will also lead to an inconsistency in HPACK state that will be flagged at some later time. Modifications: - HPACK Decoder now has 2 levels of limits related to SETTINGS_MAX_HEADER_LIST_SIZE. The first will attempt to keep processing data and send a RST_STREAM after all data is processed. The second will send a GO_AWAY and close the entire connection. - When the HPACK Encoder enforces SETTINGS_MAX_HEADER_LIST_SIZE it should not modify the HPACK state until the size has been checked. - https://tools.ietf.org/html/rfc7540#section-6.5.2 states that the initial value of SETTINGS_MAX_HEADER_LIST_SIZE is "unlimited". We currently use 8k as a limit. We should honor the specifications default value so we don't unintentionally close a connection before the remote peer is aware of the local settings. - Remove unnecessary object allocation in DefaultHttp2HeadersDecoder and DefaultHttp2HeadersEncoder. Result: Fixes https://github.com/netty/netty/issues/6209.
2017-01-14 02:09:44 +01:00
Long maxHeaderListSize = initialSettings.maxHeaderListSize();
Http2FrameReader frameReader = new DefaultHttp2FrameReader(maxHeaderListSize == null ?
new DefaultHttp2HeadersDecoder(true) :
new DefaultHttp2HeadersDecoder(true, maxHeaderListSize));
Http2FrameReader reader = new Http2InboundFrameLogger(frameReader, frameLogger);
Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder, reader);
decoder.frameListener(new FrameListener());
http2Handler = new InternalHttp2ConnectionHandler(decoder, encoder, initialSettings);
http2Handler.connection().addListener(new ConnectionListener());
this.server = server;
}
Http2ConnectionHandler connectionHandler() {
return http2Handler;
}
/**
* Load any dependencies.
*/
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
this.ctx = ctx;
ctx.pipeline().addBefore(ctx.executor(), ctx.name(), null, http2Handler);
http2HandlerCtx = ctx.pipeline().context(http2Handler);
}
/**
* Clean up any dependencies.
*/
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
ctx.pipeline().remove(http2Handler);
}
/**
* Handles the cleartext HTTP upgrade event. If an upgrade occurred, sends a simple response via
* HTTP/2 on stream 1 (the stream specifically reserved for cleartext HTTP upgrade).
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if (!(evt instanceof UpgradeEvent)) {
super.userEventTriggered(ctx, evt);
return;
}
UpgradeEvent upgrade = (UpgradeEvent) evt;
ctx.fireUserEventTriggered(upgrade.retain());
try {
Http2Stream stream = http2Handler.connection().stream(Http2CodecUtil.HTTP_UPGRADE_STREAM_ID);
// TODO: improve handler/stream lifecycle so that stream isn't active before handler added.
// The stream was already made active, but ctx may have been null so it wasn't initialized.
// https://github.com/netty/netty/issues/4942
new ConnectionListener().onStreamActive(stream);
upgrade.upgradeRequest().headers().setInt(
HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), Http2CodecUtil.HTTP_UPGRADE_STREAM_ID);
new InboundHttpToHttp2Adapter(http2Handler.connection(), http2Handler.decoder().frameListener())
.channelRead(ctx, upgrade.upgradeRequest().retain());
} finally {
upgrade.release();
}
}
// Override this to signal it will never throw an exception.
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ctx.fireExceptionCaught(cause);
}
/**
* Processes all {@link Http2Frame}s. {@link Http2StreamFrame}s may only originate in child
* streams.
*/
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
try {
if (msg instanceof Http2WindowUpdateFrame) {
Http2WindowUpdateFrame frame = (Http2WindowUpdateFrame) msg;
consumeBytes(frame.streamId(), frame.windowSizeIncrement(), promise);
} else if (msg instanceof Http2StreamFrame) {
writeStreamFrame((Http2StreamFrame) msg, promise);
} else if (msg instanceof Http2GoAwayFrame) {
writeGoAwayFrame((Http2GoAwayFrame) msg, promise);
} else {
throw new UnsupportedMessageTypeException(msg);
}
} finally {
ReferenceCountUtil.release(msg);
}
}
private void consumeBytes(int streamId, int bytes, ChannelPromise promise) {
try {
Http2Stream stream = http2Handler.connection().stream(streamId);
http2Handler.connection().local().flowController()
.consumeBytes(stream, bytes);
promise.setSuccess();
} catch (Throwable t) {
promise.setFailure(t);
}
}
private void writeGoAwayFrame(Http2GoAwayFrame frame, ChannelPromise promise) {
if (frame.lastStreamId() > -1) {
throw new IllegalArgumentException("Last stream id must not be set on GOAWAY frame");
}
int lastStreamCreated = http2Handler.connection().remote().lastStreamCreated();
int lastStreamId = lastStreamCreated + frame.extraStreamIds() * 2;
// Check if the computation overflowed.
if (lastStreamId < lastStreamCreated) {
lastStreamId = Integer.MAX_VALUE;
}
http2Handler.goAway(
http2HandlerCtx, lastStreamId, frame.errorCode(), frame.content().retain(), promise);
}
private void writeStreamFrame(Http2StreamFrame frame, ChannelPromise promise) {
if (frame instanceof Http2DataFrame) {
Http2DataFrame dataFrame = (Http2DataFrame) frame;
http2Handler.encoder().writeData(http2HandlerCtx, frame.streamId(), dataFrame.content().retain(),
dataFrame.padding(), dataFrame.isEndStream(), promise);
} else if (frame instanceof Http2HeadersFrame) {
writeHeadersFrame((Http2HeadersFrame) frame, promise);
} else if (frame instanceof Http2ResetFrame) {
Http2ResetFrame rstFrame = (Http2ResetFrame) frame;
http2Handler.resetStream(http2HandlerCtx, frame.streamId(), rstFrame.errorCode(), promise);
} else {
throw new UnsupportedMessageTypeException(frame);
}
}
private void writeHeadersFrame(Http2HeadersFrame headersFrame, ChannelPromise promise) {
int streamId = headersFrame.streamId();
if (!isStreamIdValid(streamId)) {
final Endpoint<Http2LocalFlowController> localEndpoint = http2Handler.connection().local();
streamId = localEndpoint.incrementAndGetNextStreamId();
try {
// Try to create a stream in OPEN state before writing headers, to catch errors on stream creation
// early on i.e. max concurrent streams limit reached, stream id exhaustion, etc.
localEndpoint.createStream(streamId, false);
} catch (Http2Exception e) {
promise.setFailure(e);
return;
}
ctx.fireUserEventTriggered(new Http2StreamActiveEvent(streamId, headersFrame));
}
http2Handler.encoder().writeHeaders(http2HandlerCtx, streamId, headersFrame.headers(),
headersFrame.padding(), headersFrame.isEndStream(), promise);
}
private final class ConnectionListener extends Http2ConnectionAdapter {
@Override
public void onStreamActive(Http2Stream stream) {
if (ctx == null) {
// UPGRADE stream is active before handlerAdded().
return;
}
if (isOutboundStream(server, stream.id())) {
// Creation of outbound streams is notified in writeHeadersFrame().
return;
}
ctx.fireUserEventTriggered(new Http2StreamActiveEvent(stream.id()));
}
@Override
public void onStreamClosed(Http2Stream stream) {
ctx.fireUserEventTriggered(new Http2StreamClosedEvent(stream.id()));
}
@Override
public void onGoAwayReceived(final int lastStreamId, long errorCode, ByteBuf debugData) {
ctx.fireChannelRead(new DefaultHttp2GoAwayFrame(lastStreamId, errorCode, debugData.retain()));
}
}
private static final class InternalHttp2ConnectionHandler extends Http2ConnectionHandler {
InternalHttp2ConnectionHandler(Http2ConnectionDecoder decoder, Http2ConnectionEncoder encoder,
Http2Settings initialSettings) {
super(decoder, encoder, initialSettings);
}
@Override
protected void onStreamError(ChannelHandlerContext ctx, Throwable cause,
Http2Exception.StreamException http2Ex) {
try {
Http2Stream stream = connection().stream(http2Ex.streamId());
if (stream == null) {
return;
}
ctx.fireExceptionCaught(http2Ex);
} finally {
super.onStreamError(ctx, cause, http2Ex);
}
}
}
private static final class FrameListener extends Http2FrameAdapter {
@Override
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) {
Http2ResetFrame rstFrame = new DefaultHttp2ResetFrame(errorCode);
rstFrame.streamId(streamId);
ctx.fireChannelRead(rstFrame);
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId,
Http2Headers headers, int streamDependency, short weight, boolean
exclusive, int padding, boolean endStream) {
onHeadersRead(ctx, streamId, headers, padding, endStream);
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
int padding, boolean endOfStream) {
Http2HeadersFrame headersFrame = new DefaultHttp2HeadersFrame(headers, endOfStream, padding);
headersFrame.streamId(streamId);
ctx.fireChannelRead(headersFrame);
}
@Override
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
boolean endOfStream) {
Http2DataFrame dataFrame = new DefaultHttp2DataFrame(data.retain(), endOfStream, padding);
dataFrame.streamId(streamId);
ctx.fireChannelRead(dataFrame);
// We return the bytes in bytesConsumed() once the stream channel consumed the bytes.
return 0;
}
}
}