From 754e08796b83da4e682167914041cd03350cda4a Mon Sep 17 00:00:00 2001 From: nmittler Date: Thu, 27 Mar 2014 10:40:47 -0700 Subject: [PATCH] First cut of frame encoding/decoding and session management for HTTP2 Motivation: Needed a rough performance comparison between SPDY and HTTP 2.0 framing. Expected performance gains were seen in HTTP 2.0 due to header compression. Modifications: Added a new codec-http2 module containing all of the new source and unit tests. Updated the top-level pom.xml to add this as a child module. Result: Netty will have basic support for HTTP2. --- codec-http2/pom.xml | 61 ++ .../codec/http2/draft10/Http2Error.java | 45 ++ .../codec/http2/draft10/Http2Exception.java | 50 ++ .../codec/http2/draft10/Http2Headers.java | 193 +++++ .../http2/draft10/Http2StreamException.java | 36 + .../connection/DefaultHttp2Connection.java | 456 ++++++++++++ .../DefaultInboundFlowController.java | 210 ++++++ .../DefaultOutboundFlowController.java | 291 ++++++++ .../draft10/connection/Http2Connection.java | 174 +++++ .../connection/Http2ConnectionHandler.java | 560 +++++++++++++++ .../connection/Http2ConnectionUtil.java | 62 ++ .../http2/draft10/connection/Http2Stream.java | 95 +++ .../connection/InboundFlowController.java | 56 ++ .../connection/OutboundFlowController.java | 79 +++ .../draft10/connection/package-info.java | 20 + .../draft10/frame/DefaultHttp2DataFrame.java | 190 +++++ .../frame/DefaultHttp2GoAwayFrame.java | 163 +++++ .../frame/DefaultHttp2HeadersFrame.java | 146 ++++ .../draft10/frame/DefaultHttp2PingFrame.java | 138 ++++ .../frame/DefaultHttp2PriorityFrame.java | 109 +++ .../frame/DefaultHttp2PushPromiseFrame.java | 138 ++++ .../frame/DefaultHttp2RstStreamFrame.java | 110 +++ .../frame/DefaultHttp2SettingsFrame.java | 164 +++++ .../frame/DefaultHttp2WindowUpdateFrame.java | 103 +++ .../http2/draft10/frame/Http2DataFrame.java | 54 ++ .../codec/http2/draft10/frame/Http2Flags.java | 138 ++++ .../codec/http2/draft10/frame/Http2Frame.java | 22 + .../http2/draft10/frame/Http2FrameCodec.java | 37 + .../draft10/frame/Http2FrameCodecUtil.java | 125 ++++ .../http2/draft10/frame/Http2FrameHeader.java | 84 +++ .../http2/draft10/frame/Http2GoAwayFrame.java | 60 ++ .../draft10/frame/Http2HeadersFrame.java | 34 + .../http2/draft10/frame/Http2PingFrame.java | 53 ++ .../draft10/frame/Http2PriorityFrame.java | 26 + .../draft10/frame/Http2PushPromiseFrame.java | 34 + .../draft10/frame/Http2RstStreamFrame.java | 26 + .../draft10/frame/Http2SettingsFrame.java | 51 ++ .../http2/draft10/frame/Http2StreamFrame.java | 31 + .../draft10/frame/Http2WindowUpdateFrame.java | 27 + .../decoder/AbstractHeadersUnmarshaller.java | 125 ++++ .../AbstractHttp2FrameUnmarshaller.java | 66 ++ .../decoder/DefaultHttp2HeadersDecoder.java | 63 ++ .../decoder/Http2DataFrameUnmarshaller.java | 86 +++ .../frame/decoder/Http2FrameDecoder.java | 137 ++++ .../frame/decoder/Http2FrameUnmarshaller.java | 50 ++ .../decoder/Http2GoAwayFrameUnmarshaller.java | 68 ++ .../frame/decoder/Http2HeadersDecoder.java | 36 + .../Http2HeadersFrameUnmarshaller.java | 149 ++++ .../decoder/Http2PingFrameUnmarshaller.java | 60 ++ .../Http2PriorityFrameUnmarshaller.java | 61 ++ .../Http2PushPromiseFrameUnmarshaller.java | 116 +++ .../Http2RstStreamFrameUnmarshaller.java | 61 ++ .../Http2SettingsFrameUnmarshaller.java | 98 +++ .../Http2StandardFrameUnmarshaller.java | 115 +++ .../Http2WindowUpdateFrameUnmarshaller.java | 58 ++ .../draft10/frame/decoder/package-info.java | 20 + .../encoder/AbstractHttp2FrameMarshaller.java | 65 ++ .../encoder/DefaultHttp2HeadersEncoder.java | 63 ++ .../encoder/Http2DataFrameMarshaller.java | 67 ++ .../frame/encoder/Http2FrameEncoder.java | 50 ++ .../frame/encoder/Http2FrameMarshaller.java | 38 + .../encoder/Http2GoAwayFrameMarshaller.java | 49 ++ .../frame/encoder/Http2HeadersEncoder.java | 39 + .../encoder/Http2HeadersFrameMarshaller.java | 116 +++ .../encoder/Http2PingFrameMarshaller.java | 46 ++ .../encoder/Http2PriorityFrameMarshaller.java | 44 ++ .../Http2PushPromiseFrameMarshaller.java | 93 +++ .../Http2RstStreamFrameMarshaller.java | 45 ++ .../encoder/Http2SettingsFrameMarshaller.java | 69 ++ .../encoder/Http2StandardFrameMarshaller.java | 110 +++ .../Http2WindowUpdateFrameMarshaller.java | 44 ++ .../draft10/frame/encoder/package-info.java | 20 + .../http2/draft10/frame/package-info.java | 20 + .../codec/http2/draft10/package-info.java | 20 + .../DefaultHttp2ConnectionTest.java | 308 ++++++++ .../DefaultInboundFlowControllerTest.java | 158 +++++ .../DefaultOutboundFlowControllerTest.java | 244 +++++++ .../Http2ConnectionHandlerTest.java | 664 ++++++++++++++++++ .../frame/HeaderBlockRoundtripTest.java | 92 +++ .../frame/Http2FrameRoundtripTest.java | 317 +++++++++ pom.xml | 1 + 81 files changed, 8602 insertions(+) create mode 100644 codec-http2/pom.xml create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2Error.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2Exception.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2Headers.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2StreamException.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/DefaultHttp2Connection.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/DefaultInboundFlowController.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/DefaultOutboundFlowController.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2Connection.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2ConnectionHandler.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2ConnectionUtil.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2Stream.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/InboundFlowController.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/OutboundFlowController.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/package-info.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2DataFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2GoAwayFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2HeadersFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2PingFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2PriorityFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2PushPromiseFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2RstStreamFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2SettingsFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2WindowUpdateFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2DataFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2Flags.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2Frame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameCodec.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameCodecUtil.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameHeader.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2GoAwayFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2HeadersFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2PingFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2PriorityFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2PushPromiseFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2RstStreamFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2SettingsFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2StreamFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2WindowUpdateFrame.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/AbstractHeadersUnmarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/AbstractHttp2FrameUnmarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/DefaultHttp2HeadersDecoder.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2DataFrameUnmarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2FrameDecoder.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2FrameUnmarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2GoAwayFrameUnmarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2HeadersDecoder.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2HeadersFrameUnmarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2PingFrameUnmarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2PriorityFrameUnmarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2PushPromiseFrameUnmarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2RstStreamFrameUnmarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2SettingsFrameUnmarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2StandardFrameUnmarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2WindowUpdateFrameUnmarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/package-info.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/AbstractHttp2FrameMarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/DefaultHttp2HeadersEncoder.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2DataFrameMarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2FrameEncoder.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2FrameMarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2GoAwayFrameMarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2HeadersEncoder.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2HeadersFrameMarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2PingFrameMarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2PriorityFrameMarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2PushPromiseFrameMarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2RstStreamFrameMarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2SettingsFrameMarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2StandardFrameMarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2WindowUpdateFrameMarshaller.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/package-info.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/package-info.java create mode 100644 codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/package-info.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/DefaultHttp2ConnectionTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/DefaultInboundFlowControllerTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/DefaultOutboundFlowControllerTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/Http2ConnectionHandlerTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/frame/HeaderBlockRoundtripTest.java create mode 100644 codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameRoundtripTest.java diff --git a/codec-http2/pom.xml b/codec-http2/pom.xml new file mode 100644 index 0000000000..7eeb7fb54a --- /dev/null +++ b/codec-http2/pom.xml @@ -0,0 +1,61 @@ + + + + + 4.0.0 + + io.netty + netty-parent + 5.0.0.Alpha2-SNAPSHOT + + + netty-codec-http2 + jar + + Netty/Codec/HTTP2 + + + + ${project.groupId} + netty-codec-http + ${project.version} + + + ${project.groupId} + netty-handler + ${project.version} + + + com.twitter + hpack + 0.6.0 + + + com.google.guava + guava + 16.0.1 + + + org.mockito + mockito-all + 1.9.5 + test + + + + diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2Error.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2Error.java new file mode 100644 index 0000000000..a1cac42cc8 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2Error.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014 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.draft10; + +/** + * All error codes identified by the HTTP2 spec. + */ +public enum Http2Error { + NO_ERROR(0), + PROTOCOL_ERROR(1), + INTERNAL_ERROR(2), + FLOW_CONTROL_ERROR(3), + SETTINGS_TIMEOUT(4), + STREAM_CLOSED(5), + FRAME_SIZE_ERROR(6), + REFUSED_STREAM(7), + CANCEL(8), + COMPRESSION_ERROR(9), + CONNECT_ERROR(10), + ENHANCE_YOUR_CALM(11), + INADEQUATE_SECURITY(12); + + private final int code; + + private Http2Error(int code) { + this.code = code; + } + + public int getCode() { + return this.code; + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2Exception.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2Exception.java new file mode 100644 index 0000000000..5cdd174e32 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2Exception.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014 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.draft10; + +/** + * Exception thrown when an HTTP2 error was encountered. + */ +public class Http2Exception extends Exception { + private static final long serialVersionUID = -2292608019080068769L; + + private final Http2Error error; + + public Http2Exception(Http2Error error) { + this.error = error; + } + + public Http2Exception(Http2Error error, String message) { + super(message); + this.error = error; + } + + public Http2Error getError() { + return error; + } + + public static Http2Exception format(Http2Error error, String fmt, Object... args) { + return new Http2Exception(error, String.format(fmt, args)); + } + + public static Http2Exception protocolError(String fmt, Object... args) { + return format(Http2Error.PROTOCOL_ERROR, fmt, args); + } + + public static Http2Exception flowControlError(String fmt, Object... args) { + return format(Http2Error.FLOW_CONTROL_ERROR, fmt, args); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2Headers.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2Headers.java new file mode 100644 index 0000000000..65826c4b76 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2Headers.java @@ -0,0 +1,193 @@ +/* + * Copyright 2014 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.draft10; + +import java.util.Iterator; +import java.util.Map.Entry; + +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableMultimap; + +public final class Http2Headers implements Iterable> { + + /** + * HTTP2 header names. + */ + public enum HttpName { + /** + * {@code :method}. + */ + METHOD(":method"), + + /** + * {@code :scheme}. + */ + SCHEME(":scheme"), + + /** + * {@code :authority}. + */ + AUTHORITY(":authority"), + + /** + * {@code :path}. + */ + PATH(":path"), + + /** + * {@code :status}. + */ + STATUS(":status"); + + private final String value; + + private HttpName(String value) { + this.value = value; + } + + public String value() { + return this.value; + } + } + + private final ImmutableMultimap headers; + + private Http2Headers(Builder builder) { + this.headers = builder.map.build(); + } + + public String getHeader(String name) { + ImmutableCollection col = getHeaders(name); + return col.isEmpty() ? null : col.iterator().next(); + } + + public ImmutableCollection getHeaders(String name) { + return headers.get(name); + } + + public String getMethod() { + return getHeader(HttpName.METHOD.value()); + } + + public String getScheme() { + return getHeader(HttpName.SCHEME.value()); + } + + public String getAuthority() { + return getHeader(HttpName.AUTHORITY.value()); + } + + public String getPath() { + return getHeader(HttpName.PATH.value()); + } + + public String getStatus() { + return getHeader(HttpName.STATUS.value()); + } + + @Override + public Iterator> iterator() { + return headers.entries().iterator(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((headers == null) ? 0 : headers.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Http2Headers other = (Http2Headers) obj; + if (headers == null) { + if (other.headers != null) { + return false; + } + } else if (!headers.equals(other.headers)) { + return false; + } + return true; + } + + @Override + public String toString() { + return headers.toString(); + } + + public static class Builder { + private ImmutableMultimap.Builder map = ImmutableMultimap.builder(); + + public Builder clear() { + map = ImmutableMultimap.builder(); + return this; + } + + public Builder addHeaders(Http2Headers headers) { + if (headers == null) { + throw new IllegalArgumentException("headers must not be null."); + } + map.putAll(headers.headers); + return this; + } + + public Builder addHeader(String name, String value) { + // Use interning on the header name to save space. + map.put(name.intern(), value); + return this; + } + + public Builder addHeader(byte[] name, byte[] value) { + addHeader(new String(name, Charsets.UTF_8), new String(value, Charsets.UTF_8)); + return this; + } + + public Builder setMethod(String value) { + return addHeader(HttpName.METHOD.value(), value); + } + + public Builder setScheme(String value) { + return addHeader(HttpName.SCHEME.value(), value); + } + + public Builder setAuthority(String value) { + return addHeader(HttpName.AUTHORITY.value(), value); + } + + public Builder setPath(String value) { + return addHeader(HttpName.PATH.value(), value); + } + + public Builder setStatus(String value) { + return addHeader(HttpName.STATUS.value(), value); + } + + public Http2Headers build() { + return new Http2Headers(this); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2StreamException.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2StreamException.java new file mode 100644 index 0000000000..d5ce51f9d8 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/Http2StreamException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 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.draft10; + +public class Http2StreamException extends Http2Exception { + + private static final long serialVersionUID = -7658235659648480024L; + private final int streamId; + + public Http2StreamException(int streamId, Http2Error error, String message) { + super(error, message); + this.streamId = streamId; + } + + public Http2StreamException(int streamId, Http2Error error) { + super(error); + this.streamId = streamId; + } + + public int getStreamId() { + return streamId; + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/DefaultHttp2Connection.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/DefaultHttp2Connection.java new file mode 100644 index 0000000000..b1966207c7 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/DefaultHttp2Connection.java @@ -0,0 +1,456 @@ +/* + * Copyright 2014 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.draft10.connection; + +import static io.netty.handler.codec.http2.draft10.Http2Error.NO_ERROR; +import static io.netty.handler.codec.http2.draft10.Http2Exception.format; +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.connection.Http2ConnectionUtil.toByteBuf; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.DEFAULT_STREAM_PRIORITY; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http2.draft10.Http2Error; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.connection.Http2Stream.State; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2GoAwayFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2GoAwayFrame; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multiset; +import com.google.common.collect.TreeMultiset; + +public class DefaultHttp2Connection implements Http2Connection { + + private final List listeners = Lists.newArrayList(); + private final Map streamMap = Maps.newHashMap(); + private final Multiset activeStreams = TreeMultiset.create(); + private final DefaultEndpoint localEndpoint; + private final DefaultEndpoint remoteEndpoint; + private boolean goAwaySent; + private boolean goAwayReceived; + private ChannelFutureListener closeListener; + + public DefaultHttp2Connection(boolean server) { + this.localEndpoint = new DefaultEndpoint(server); + this.remoteEndpoint = new DefaultEndpoint(!server); + } + + @Override + public void addListener(Listener listener) { + listeners.add(listener); + } + + @Override + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + @Override + public Http2Stream getStreamOrFail(int streamId) throws Http2Exception { + Http2Stream stream = getStream(streamId); + if (stream == null) { + throw protocolError("Stream does not exist %d", streamId); + } + return stream; + } + + @Override + public Http2Stream getStream(int streamId) { + return streamMap.get(streamId); + } + + @Override + public List getActiveStreams() { + // Copy the list in case any operation on the returned streams causes the activeStreams set + // to change. + return ImmutableList.copyOf(activeStreams); + } + + @Override + public Endpoint local() { + return localEndpoint; + } + + @Override + public Endpoint remote() { + return remoteEndpoint; + } + + @Override + public void sendGoAway(ChannelHandlerContext ctx, ChannelPromise promise, Http2Exception cause) { + closeListener = getOrCreateCloseListener(ctx, promise); + ChannelFuture future; + if (!goAwaySent) { + goAwaySent = true; + + int errorCode = cause != null ? cause.getError().getCode() : NO_ERROR.getCode(); + ByteBuf debugData = toByteBuf(ctx, cause); + + Http2GoAwayFrame frame = new DefaultHttp2GoAwayFrame.Builder().setErrorCode(errorCode) + .setLastStreamId(remote().getLastStreamCreated()).setDebugData(debugData).build(); + future = ctx.writeAndFlush(frame); + } else { + future = ctx.newSucceededFuture(); + } + + // If there are no active streams, close immediately after the send is complete. + // Otherwise wait until all streams are inactive. + if (cause != null || activeStreams.isEmpty()) { + future.addListener(closeListener); + } + } + + @Override + public void goAwayReceived() { + goAwayReceived = true; + } + + @Override + public boolean isGoAwaySent() { + return goAwaySent; + } + + @Override + public boolean isGoAwayReceived() { + return goAwayReceived; + } + + @Override + public boolean isGoAway() { + return isGoAwaySent() || isGoAwayReceived(); + } + + private ChannelFutureListener getOrCreateCloseListener(final ChannelHandlerContext ctx, + final ChannelPromise promise) { + if (closeListener == null) { + closeListener = new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + ctx.close(promise); + } + }; + } + return closeListener; + } + + private void notifyStreamClosed(int id) { + for (Listener listener : listeners) { + listener.streamClosed(id); + } + } + + private void notifyStreamCreated(int id) { + for (Listener listener : listeners) { + listener.streamCreated(id); + } + } + + /** + * Simple stream implementation. Streams can be compared to each other by priority. + */ + private class DefaultStream implements Http2Stream { + private final int id; + private State state = State.IDLE; + private int priority; + + public DefaultStream(int id) { + this.id = id; + this.priority = DEFAULT_STREAM_PRIORITY; + } + + @Override + public int getId() { + return id; + } + + @Override + public State getState() { + return state; + } + + @Override + public int compareTo(Http2Stream other) { + // Sort streams with the same priority by their ID. + if (priority == other.getPriority()) { + return id - other.getId(); + } + return priority - other.getPriority(); + } + + @Override + public void verifyState(Http2Error error, State... allowedStates) throws Http2Exception { + Predicate predicate = Predicates.in(Arrays.asList(allowedStates)); + if (!predicate.apply(state)) { + throw format(error, "Stream %d in unexpected state: %s", id, state); + } + } + + @Override + public void setPriority(int priority) throws Http2Exception { + if (priority < 0) { + throw protocolError("Invalid priority: %d", priority); + } + + // If it was active, we must remove it from the set before changing the priority. + // Otherwise it won't be able to locate the stream in the set. + boolean wasActive = activeStreams.remove(this); + this.priority = priority; + + // If this stream was in the active set, re-add it so that it's properly sorted. + if (wasActive) { + activeStreams.add(this); + } + } + + @Override + public int getPriority() { + return priority; + } + + @Override + public void openForPush() throws Http2Exception { + switch (state) { + case RESERVED_LOCAL: + state = State.HALF_CLOSED_REMOTE; + break; + case RESERVED_REMOTE: + state = State.HALF_CLOSED_LOCAL; + break; + default: + throw protocolError("Attempting to open non-reserved stream for push"); + } + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelFuture future) { + if (state == State.CLOSED) { + return; + } + + state = State.CLOSED; + activeStreams.remove(this); + streamMap.remove(id); + notifyStreamClosed(id); + + // If this connection is closing and there are no longer any + // active streams, close after the current operation completes. + if (closeListener != null && activeStreams.isEmpty()) { + future.addListener(closeListener); + } + } + + @Override + public void closeLocalSide(ChannelHandlerContext ctx, ChannelFuture future) { + switch (state) { + case OPEN: + case HALF_CLOSED_LOCAL: + state = State.HALF_CLOSED_LOCAL; + break; + case HALF_CLOSED_REMOTE: + case RESERVED_LOCAL: + case RESERVED_REMOTE: + case IDLE: + case CLOSED: + default: + close(ctx, future); + break; + } + } + + @Override + public void closeRemoteSide(ChannelHandlerContext ctx, ChannelFuture future) { + switch (state) { + case OPEN: + case HALF_CLOSED_REMOTE: + state = State.HALF_CLOSED_REMOTE; + break; + case RESERVED_LOCAL: + case RESERVED_REMOTE: + case IDLE: + case HALF_CLOSED_LOCAL: + case CLOSED: + default: + close(ctx, future); + break; + } + } + + @Override + public boolean isRemoteSideOpen() { + switch (state) { + case HALF_CLOSED_LOCAL: + case OPEN: + case RESERVED_REMOTE: + return true; + case IDLE: + case RESERVED_LOCAL: + case HALF_CLOSED_REMOTE: + case CLOSED: + default: + return false; + } + } + + @Override + public boolean isLocalSideOpen() { + switch (state) { + case HALF_CLOSED_REMOTE: + case OPEN: + case RESERVED_LOCAL: + return true; + case IDLE: + case RESERVED_REMOTE: + case HALF_CLOSED_LOCAL: + case CLOSED: + default: + return false; + } + } + } + + /** + * Simple endpoint implementation. + */ + private class DefaultEndpoint implements Endpoint { + private int nextStreamId; + private int lastStreamCreated; + private int maxStreams = Integer.MAX_VALUE; + private boolean pushToAllowed = true; + + public DefaultEndpoint(boolean serverEndpoint) { + // Determine the starting stream ID for this endpoint. Zero is reserved for the + // connection and 1 is reserved for responding to an upgrade from HTTP 1.1. + // Client-initiated streams use odd identifiers and server-initiated streams use + // even. + nextStreamId = serverEndpoint ? 2 : 3; + } + + @Override + public DefaultStream createStream(int streamId, int priority, boolean halfClosed) + throws Http2Exception { + checkNewStreamAllowed(streamId); + + // Create and initialize the stream. + DefaultStream stream = new DefaultStream(streamId); + stream.setPriority(priority); + if (halfClosed) { + stream.state = isLocal() ? State.HALF_CLOSED_LOCAL : State.HALF_CLOSED_REMOTE; + } else { + stream.state = State.OPEN; + } + + // Update the next and last stream IDs. + nextStreamId += 2; + lastStreamCreated = streamId; + + // Register the stream and mark it as active. + streamMap.put(streamId, stream); + activeStreams.add(stream); + + notifyStreamCreated(streamId); + return stream; + } + + @Override + public DefaultStream reservePushStream(int streamId, Http2Stream parent) throws Http2Exception { + if (parent == null) { + throw protocolError("Parent stream missing"); + } + if (isLocal() ? !parent.isLocalSideOpen() : !parent.isRemoteSideOpen()) { + throw protocolError("Stream %d is not open for sending push promise", parent.getId()); + } + if (!opposite().isPushToAllowed()) { + throw protocolError("Server push not allowed to opposite endpoint."); + } + + // Create and initialize the stream. + DefaultStream stream = new DefaultStream(streamId); + stream.setPriority(parent.getPriority() + 1); + stream.state = isLocal() ? State.RESERVED_LOCAL : State.RESERVED_REMOTE; + + // Update the next and last stream IDs. + nextStreamId += 2; + lastStreamCreated = streamId; + + // Register the stream. + streamMap.put(streamId, stream); + + notifyStreamCreated(streamId); + return stream; + } + + @Override + public void setPushToAllowed(boolean allow) { + this.pushToAllowed = allow; + } + + @Override + public boolean isPushToAllowed() { + return pushToAllowed; + } + + @Override + public int getMaxStreams() { + return maxStreams; + } + + @Override + public void setMaxStreams(int maxStreams) { + this.maxStreams = maxStreams; + } + + @Override + public int getLastStreamCreated() { + return lastStreamCreated; + } + + @Override + public Endpoint opposite() { + return isLocal() ? remoteEndpoint : localEndpoint; + } + + private void checkNewStreamAllowed(int streamId) throws Http2Exception { + if (isGoAway()) { + throw protocolError("Cannot create a stream since the connection is going away"); + } + if (nextStreamId < 0) { + throw protocolError("No more streams can be created on this connection"); + } + if (streamId != nextStreamId) { + throw protocolError("Incorrect next stream ID requested: %d", streamId); + } + if (streamMap.size() + 1 > maxStreams) { + // TODO(nathanmittler): is this right? + throw protocolError("Maximum streams exceeded for this endpoint."); + } + } + + private boolean isLocal() { + return this == localEndpoint; + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/DefaultInboundFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/DefaultInboundFlowController.java new file mode 100644 index 0000000000..b00fbd19c0 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/DefaultInboundFlowController.java @@ -0,0 +1,210 @@ +/* + * Copyright 2014 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.draft10.connection; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.flowControlError; +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.connection.Http2ConnectionUtil.DEFAULT_FLOW_CONTROL_WINDOW_SIZE; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.CONNECTION_STREAM_ID; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2WindowUpdateFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2DataFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2WindowUpdateFrame; + +import java.util.Map; + +import com.google.common.collect.Maps; + +/** + * Basic implementation of {@link InboundFlowController}. + */ +public class DefaultInboundFlowController implements InboundFlowController { + + private int initialWindowSize = DEFAULT_FLOW_CONTROL_WINDOW_SIZE; + private StreamWindow connectionWindow = new StreamWindow(CONNECTION_STREAM_ID); + private final Map streamWindows = Maps.newHashMap(); + + public DefaultInboundFlowController(Http2Connection connection) { + connection.addListener(new Http2Connection.Listener() { + @Override + public void streamCreated(int streamId) { + streamWindows.put(streamId, new StreamWindow(streamId)); + } + + @Override + public void streamClosed(int streamId) { + streamWindows.remove(streamId); + } + }); + } + + @Override + public void setInitialInboundWindowSize(int newWindowSize) throws Http2Exception { + int deltaWindowSize = newWindowSize - initialWindowSize; + initialWindowSize = newWindowSize; + + // Apply the delta to all of the windows. + connectionWindow.addAndGet(deltaWindowSize); + for (StreamWindow window : streamWindows.values()) { + window.updatedInitialWindowSize(deltaWindowSize); + } + } + + @Override + public void applyInboundFlowControl(Http2DataFrame dataFrame, FrameWriter frameWriter) + throws Http2Exception { + applyConnectionFlowControl(dataFrame, frameWriter); + applyStreamFlowControl(dataFrame, frameWriter); + } + + /** + * Apply connection-wide flow control to the incoming data frame. + */ + private void applyConnectionFlowControl(Http2DataFrame dataFrame, FrameWriter frameWriter) + throws Http2Exception { + // Remove the data length from the available window size. Throw if the lower bound + // was exceeded. + connectionWindow.addAndGet(-dataFrame.content().readableBytes()); + + // If less than the window update threshold remains, restore the window size + // to the initial value and send a window update to the remote endpoint indicating + // the new window size. + if (connectionWindow.getSize() <= getWindowUpdateThreshold()) { + connectionWindow.updateWindow(frameWriter); + } + } + + /** + * Apply stream-based flow control to the incoming data frame. + */ + private void applyStreamFlowControl(Http2DataFrame dataFrame, FrameWriter frameWriter) + throws Http2Exception { + // Remove the data length from the available window size. Throw if the lower bound + // was exceeded. + StreamWindow window = getWindowOrFail(dataFrame.getStreamId()); + window.addAndGet(-dataFrame.content().readableBytes()); + + // If less than the window update threshold remains, restore the window size + // to the initial value and send a window update to the remote endpoint indicating + // the new window size. + if (window.getSize() <= getWindowUpdateThreshold() && !dataFrame.isEndOfStream()) { + window.updateWindow(frameWriter); + } + } + + /** + * Gets the threshold for a window size below which a window update should be issued. + */ + private int getWindowUpdateThreshold() { + return initialWindowSize / 2; + } + + /** + * Gets the window for the stream or raises a {@code PROTOCOL_ERROR} if not found. + */ + private StreamWindow getWindowOrFail(int streamId) throws Http2Exception { + StreamWindow window = streamWindows.get(streamId); + if (window == null) { + throw protocolError("Flow control window missing for stream: %d", streamId); + } + return window; + } + + /** + * Flow control window state for an individual stream. + */ + private final class StreamWindow { + private int windowSize; + private int lowerBound; + private final int streamId; + + public StreamWindow(int streamId) { + this.streamId = streamId; + this.windowSize = initialWindowSize; + } + + public int getSize() { + return windowSize; + } + + /** + * Adds the given delta to the window size and returns the new value. + * + * @param delta the delta in the initial window size. + * @throws Http2Exception thrown if the new window is less than the allowed lower bound. + */ + public int addAndGet(int delta) throws Http2Exception { + // Apply the delta. Even if we throw an exception we want to have taken this delta into + // account. + windowSize += delta; + if (delta > 0) { + lowerBound = 0; + } + + // 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 (windowSize < lowerBound) { + if (streamId == CONNECTION_STREAM_ID) { + throw protocolError("Connection flow control window exceeded"); + } else { + throw flowControlError("Flow control window exceeded for stream: %d", streamId); + } + } + + return windowSize; + } + + /** + * Called when sending a SETTINGS frame with a new initial window size. If the window has gotten + * smaller (i.e. deltaWindowSize < 0), the lower bound is set to that value. This will + * temporarily allow for receipt of data frames which were sent by the remote endpoint before + * receiving the SETTINGS frame. + * + * @param delta the delta in the initial window size. + * @throws Http2Exception thrown if integer overflow occurs on the window. + */ + public void updatedInitialWindowSize(int delta) throws Http2Exception { + windowSize += delta; + if (delta > 0 && windowSize < Integer.MIN_VALUE + delta) { + // Integer overflow. + throw flowControlError("Flow control window overflowed for stream: %d", streamId); + } + + if (delta < 0) { + lowerBound = delta; + } + } + + /** + * Called to perform a window update for this stream (or connection). Updates the window size + * back to the size of the initial window and sends a window update frame to the remote + * endpoint. + */ + public void updateWindow(FrameWriter frameWriter) throws Http2Exception { + // Expand the window for this stream back to the size of the initial window. + int deltaWindowSize = initialWindowSize - getSize(); + addAndGet(deltaWindowSize); + + // Send a window update for the stream/connection. + Http2WindowUpdateFrame updateFrame = new DefaultHttp2WindowUpdateFrame.Builder() + .setStreamId(streamId).setWindowSizeIncrement(deltaWindowSize).build(); + frameWriter.writeFrame(updateFrame); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/DefaultOutboundFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/DefaultOutboundFlowController.java new file mode 100644 index 0000000000..aec0cc98c3 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/DefaultOutboundFlowController.java @@ -0,0 +1,291 @@ +/* + * Copyright 2014 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.draft10.connection; + +import static io.netty.handler.codec.http2.draft10.Http2Error.FLOW_CONTROL_ERROR; +import static io.netty.handler.codec.http2.draft10.Http2Error.STREAM_CLOSED; +import static io.netty.handler.codec.http2.draft10.Http2Exception.format; +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.connection.Http2ConnectionUtil.DEFAULT_FLOW_CONTROL_WINDOW_SIZE; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.CONNECTION_STREAM_ID; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.Http2StreamException; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2DataFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2DataFrame; + +import java.util.Map; +import java.util.Queue; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +/** + * Basic implementation of {@link OutboundFlowController}. + */ +public class DefaultOutboundFlowController implements OutboundFlowController { + + private final Http2Connection connection; + private final Map streamStates = Maps.newHashMap(); + private int initialWindowSize = DEFAULT_FLOW_CONTROL_WINDOW_SIZE; + private int connectionWindowSize = DEFAULT_FLOW_CONTROL_WINDOW_SIZE; + + public DefaultOutboundFlowController(Http2Connection connection) { + this.connection = connection; + connection.addListener(new Http2Connection.Listener() { + @Override + public void streamCreated(int streamId) { + streamStates.put(streamId, new StreamState(streamId)); + } + + @Override + public void streamClosed(int streamId) { + StreamState state = streamStates.remove(streamId); + if (state != null) { + state.clearPendingWrites(); + } + } + }); + } + + @Override + public void setInitialOutboundWindowSize(int newWindowSize) throws Http2Exception { + int delta = newWindowSize - initialWindowSize; + initialWindowSize = newWindowSize; + addAndGetConnectionWindowSize(delta); + for (StreamState window : streamStates.values()) { + // Verify that the maximum value is not exceeded by this change. + window.addAndGetWindow(delta); + } + + if (delta > 0) { + // The window size increased, send any pending frames for all streams. + writePendingFrames(); + } + } + + @Override + public void updateOutboundWindowSize(int streamId, int delta) throws Http2Exception { + StreamState streamWindow = null; + if (streamId == CONNECTION_STREAM_ID) { + // Update the connection window and write any pending frames for all streams. + addAndGetConnectionWindowSize(delta); + writePendingFrames(); + } else { + // Update the stream window and write any pending frames for the stream. + streamWindow = getStateOrFail(streamId); + streamWindow.addAndGetWindow(delta); + streamWindow.writePendingFrames(Integer.MAX_VALUE); + } + } + + @Override + public void sendFlowControlled(Http2DataFrame frame, FrameWriter frameWriter) + throws Http2Exception { + + StreamState streamState = getStateOrFail(frame.getStreamId()); + + int dataLength = frame.content().readableBytes(); + if (streamState.writableWindow() >= dataLength) { + // Window size is large enough to send entire data frame + writeFrame(frame, streamState, frameWriter); + return; + } + + // Enqueue the frame to be written when the window size permits. + streamState.addPendingWrite(new PendingWrite(frame, frameWriter)); + + if (streamState.writableWindow() <= 0) { + // Stream is stalled, don't send anything now. + return; + } + + // Create and send a partial frame up to the window size. + Http2DataFrame partialFrame = readPartialFrame(frame, streamState.writableWindow()); + writeFrame(partialFrame, streamState, frameWriter); + } + + /** + * Attempts to get the {@link StreamState} for the given stream. If not available, raises a + * {@code PROTOCOL_ERROR}. + */ + private StreamState getStateOrFail(int streamId) throws Http2Exception { + StreamState streamState = streamStates.get(streamId); + if (streamState == null) { + throw protocolError("Missing flow control window for stream: %d", streamId); + } + return streamState; + } + + /** + * Writes the frame and decrements the stream and connection window sizes. + */ + private void writeFrame(Http2DataFrame frame, StreamState state, FrameWriter frameWriter) + throws Http2Exception { + int dataLength = frame.content().readableBytes(); + connectionWindowSize -= dataLength; + state.addAndGetWindow(-dataLength); + frameWriter.writeFrame(frame); + } + + /** + * Creates a view of the given frame starting at the current read index with the given number of + * bytes. The reader index on the input frame is then advanced by the number of bytes. The + * returned frame will not have end-of-stream set. + */ + private Http2DataFrame readPartialFrame(Http2DataFrame frame, int numBytes) { + return new DefaultHttp2DataFrame.Builder().setStreamId(frame.getStreamId()) + .setContent(frame.content().readSlice(numBytes).retain()).build(); + } + + /** + * Indicates whether applying the delta to the given value will cause an integer overflow. + */ + private boolean isIntegerOverflow(int previousValue, int delta) { + return delta > 0 && (Integer.MAX_VALUE - delta) < previousValue; + } + + /** + * Increments the connectionWindowSize and returns the new value. + */ + private int addAndGetConnectionWindowSize(int delta) throws Http2Exception { + if (isIntegerOverflow(connectionWindowSize, delta)) { + throw format(FLOW_CONTROL_ERROR, "Window update exceeds maximum for connection"); + } + return connectionWindowSize += delta; + } + + /** + * Writes any pending frames for the entire connection. + */ + private void writePendingFrames() throws Http2Exception { + // The request for for the entire connection, write any pending frames across + // all active streams. Active streams are already sorted by their priority. + for (Http2Stream stream : connection.getActiveStreams()) { + StreamState state = getStateOrFail(stream.getId()); + state.writePendingFrames(1); + } + } + + /** + * The outbound flow control state for a single stream. + */ + private class StreamState { + private final int streamId; + private final Queue pendingWriteQueue = Lists.newLinkedList(); + private int windowSize = initialWindowSize; + + public StreamState(int streamId) { + this.streamId = streamId; + } + + public int addAndGetWindow(int delta) throws Http2Exception { + if (isIntegerOverflow(windowSize, delta)) { + throw new Http2StreamException(streamId, FLOW_CONTROL_ERROR, + "Window size overflow for stream"); + } + windowSize += delta; + return windowSize; + } + + public int writableWindow() { + return Math.min(windowSize, connectionWindowSize); + } + + public void addPendingWrite(PendingWrite pendingWrite) { + pendingWriteQueue.offer(pendingWrite); + } + + public boolean hasPendingWrite() { + return !pendingWriteQueue.isEmpty(); + } + + public PendingWrite peekPendingWrite() { + if (windowSize > 0) { + return pendingWriteQueue.peek(); + } + return null; + } + + public void removePendingWrite() { + pendingWriteQueue.poll(); + } + + public void clearPendingWrites() { + while (true) { + PendingWrite pendingWrite = pendingWriteQueue.poll(); + if (pendingWrite == null) { + break; + } + pendingWrite.writeError( + format(STREAM_CLOSED, "Stream closed before write could take place")); + } + } + + /** + * Sends all pending writes for this stream so long as there is space the the stream and + * connection windows. + * + * @param maxFrames the maximum number of frames to send. + */ + public void writePendingFrames(int maxFrames) throws Http2Exception { + while (maxFrames > 0 && writableWindow() > 0 && hasPendingWrite()) { + maxFrames--; + PendingWrite pendingWrite = peekPendingWrite(); + + if (writableWindow() >= pendingWrite.size()) { + // Window size is large enough to send entire data frame + removePendingWrite(); + writeFrame(pendingWrite.frame(), this, pendingWrite.writer()); + } else { + // We can send a partial frame + Http2DataFrame partialDataFrame = + readPartialFrame(pendingWrite.frame(), writableWindow()); + writeFrame(partialDataFrame, this, pendingWrite.writer()); + } + } + } + } + + /** + * Pending write for a single data frame. + */ + private class PendingWrite { + private final Http2DataFrame frame; + private final FrameWriter writer; + + public PendingWrite(Http2DataFrame frame, FrameWriter writer) { + this.frame = frame; + this.writer = writer; + } + + public Http2DataFrame frame() { + return frame; + } + + public FrameWriter writer() { + return writer; + } + + public int size() { + return frame.content().readableBytes(); + } + + public void writeError(Http2Exception cause) { + frame.release(); + writer.setFailure(cause); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2Connection.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2Connection.java new file mode 100644 index 0000000000..fd9bc92351 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2Connection.java @@ -0,0 +1,174 @@ +/* + * Copyright 2014 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.draft10.connection; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http2.draft10.Http2Exception; + +import java.util.List; + +public interface Http2Connection { + + /** + * A view of the connection from one endpoint (local or remote). + */ + interface Endpoint { + + /** + * Creates a stream initiated by this endpoint and notifies all listeners. This could fail for + * the following reasons: + *

+ * - The requested stream ID is not the next sequential ID for this endpoint.
+ * - The stream already exists.
+ * - The number of concurrent streams is above the allowed threshold for this endpoint.
+ * - The connection is marked as going away}.
+ * - The provided priority is < 0. + * + * @param streamId The ID of the stream + * @param priority the priority of the stream + * @param halfClosed if true, the stream is created in the half-closed state with respect to + * this endpoint. Otherwise it's created in the open state. + */ + Http2Stream createStream(int streamId, int priority, boolean halfClosed) throws Http2Exception; + + /** + * Creates a push stream in the reserved state for this endpoint and notifies all listeners. + * This could fail for the following reasons: + *

+ * - Server push is not allowed to the opposite endpoint.
+ * - The requested stream ID is not the next sequential stream ID for this endpoint.
+ * - The number of concurrent streams is above the allowed threshold for this endpoint.
+ * - The connection is marked as going away.
+ * - The parent stream ID does not exist or is not open from the side sending the push promise. + *
+ * - Could not set a valid priority for the new stream. + * + * @param streamId the ID of the push stream + * @param parent the parent stream used to initiate the push stream. + */ + Http2Stream reservePushStream(int streamId, Http2Stream parent) throws Http2Exception; + + /** + * Sets whether server push is allowed to this endpoint. + */ + void setPushToAllowed(boolean allow); + + /** + * Gets whether or not server push is allowed to this endpoint. + */ + boolean isPushToAllowed(); + + /** + * Gets the maximum number of concurrent streams allowed by this endpoint. + */ + int getMaxStreams(); + + /** + * Sets the maximum number of concurrent streams allowed by this endpoint. + */ + void setMaxStreams(int maxStreams); + + /** + * Gets the ID of the stream last successfully created by this endpoint. + */ + int getLastStreamCreated(); + + /** + * Gets the {@link Endpoint} opposite this one. + */ + Endpoint opposite(); + } + + /** + * A listener of the connection for stream events. + */ + interface Listener { + /** + * Called when a new stream with the given ID is created. + */ + void streamCreated(int streamId); + + /** + * Called when the stream with the given ID is closed. + */ + void streamClosed(int streamId); + } + + /** + * Adds a listener of this connection. + */ + void addListener(Listener listener); + + /** + * Removes a listener of this connection. + */ + void removeListener(Listener listener); + + /** + * Attempts to get the stream for the given ID. If it doesn't exist, throws. + */ + Http2Stream getStreamOrFail(int streamId) throws Http2Exception; + + /** + * Gets the stream if it exists. If not, returns {@code null}. + */ + Http2Stream getStream(int streamId); + + /** + * Gets all streams that are currently either open or half-closed. The returned collection is + * sorted by priority. + */ + List getActiveStreams(); + + /** + * Gets a view of this connection from the local {@link Endpoint}. + */ + Endpoint local(); + + /** + * Gets a view of this connection from the remote {@link Endpoint}. + */ + Endpoint remote(); + + /** + * Marks that a GoAway frame has been sent on this connection. After calling this, both + * {@link #isGoAwaySent()} and {@link #isGoAway()} will be {@code true}. + */ + void sendGoAway(ChannelHandlerContext ctx, ChannelPromise promise, Http2Exception cause); + + /** + * Marks that a GoAway frame has been received on this connection. After calling this, both + * {@link #isGoAwayReceived()} and {@link #isGoAway()} will be {@code true}. + */ + void goAwayReceived(); + + /** + * Indicates that this connection received a GoAway message. + */ + boolean isGoAwaySent(); + + /** + * Indicates that this connection send a GoAway message. + */ + boolean isGoAwayReceived(); + + /** + * Indicates whether or not this endpoint is going away. This is a short form for + * {@link #isGoAwaySent()} || {@link #isGoAwayReceived()}. + */ + boolean isGoAway(); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2ConnectionHandler.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2ConnectionHandler.java new file mode 100644 index 0000000000..d7573bac1f --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2ConnectionHandler.java @@ -0,0 +1,560 @@ +/* + * Copyright 2014 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.draft10.connection; + +import static io.netty.handler.codec.http2.draft10.Http2Error.PROTOCOL_ERROR; +import static io.netty.handler.codec.http2.draft10.Http2Error.STREAM_CLOSED; +import static io.netty.handler.codec.http2.draft10.Http2Exception.format; +import static io.netty.handler.codec.http2.draft10.connection.Http2ConnectionUtil.toHttp2Exception; +import static io.netty.handler.codec.http2.draft10.connection.Http2Stream.State.HALF_CLOSED_LOCAL; +import static io.netty.handler.codec.http2.draft10.connection.Http2Stream.State.HALF_CLOSED_REMOTE; +import static io.netty.handler.codec.http2.draft10.connection.Http2Stream.State.OPEN; +import static io.netty.handler.codec.http2.draft10.connection.Http2Stream.State.RESERVED_LOCAL; +import static io.netty.handler.codec.http2.draft10.connection.Http2Stream.State.RESERVED_REMOTE; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.Http2StreamException; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2PingFrame; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2RstStreamFrame; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2SettingsFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2DataFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2GoAwayFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2HeadersFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2PingFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2PriorityFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2PushPromiseFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2RstStreamFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2SettingsFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2StreamFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2WindowUpdateFrame; +import io.netty.util.ReferenceCountUtil; + +public class Http2ConnectionHandler extends ChannelHandlerAdapter { + + private final Http2Connection connection; + private final InboundFlowController inboundFlow; + private final OutboundFlowController outboundFlow; + + public Http2ConnectionHandler(boolean server) { + this(new DefaultHttp2Connection(server)); + } + + public Http2ConnectionHandler(Http2Connection connection) { + this(connection, new DefaultInboundFlowController(connection), + new DefaultOutboundFlowController(connection)); + } + + public Http2ConnectionHandler(final Http2Connection connection, + final InboundFlowController inboundFlow, final OutboundFlowController outboundFlow) { + this.connection = connection; + this.inboundFlow = inboundFlow; + this.outboundFlow = outboundFlow; + } + + @Override + public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { + // Avoid NotYetConnectedException + if (!ctx.channel().isActive()) { + ctx.close(promise); + return; + } + + connection.sendGoAway(ctx, promise, null); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) throws Exception { + for (Http2Stream stream : connection.getActiveStreams()) { + stream.close(ctx, ctx.newSucceededFuture()); + } + ctx.fireChannelInactive(); + } + + /** + * Handles {@link Http2Exception} objects that were thrown from other handlers. Ignores all other + * exceptions. + */ + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + if (cause instanceof Http2Exception) { + processHttp2Exception(ctx, (Http2Exception) cause); + } + + ctx.fireExceptionCaught(cause); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object inMsg) throws Exception { + try { + if (inMsg instanceof Http2DataFrame) { + handleInboundData(ctx, (Http2DataFrame) inMsg); + } else if (inMsg instanceof Http2HeadersFrame) { + handleInboundHeaders(ctx, (Http2HeadersFrame) inMsg); + } else if (inMsg instanceof Http2PushPromiseFrame) { + handleInboundPushPromise(ctx, (Http2PushPromiseFrame) inMsg); + } else if (inMsg instanceof Http2PriorityFrame) { + handleInboundPriority(ctx, (Http2PriorityFrame) inMsg); + } else if (inMsg instanceof Http2RstStreamFrame) { + handleInboundRstStream(ctx, (Http2RstStreamFrame) inMsg); + } else if (inMsg instanceof Http2PingFrame) { + handleInboundPing(ctx, (Http2PingFrame) inMsg); + } else if (inMsg instanceof Http2GoAwayFrame) { + handleInboundGoAway(ctx, (Http2GoAwayFrame) inMsg); + } else if (inMsg instanceof Http2WindowUpdateFrame) { + handleInboundWindowUpdate(ctx, (Http2WindowUpdateFrame) inMsg); + } else if (inMsg instanceof Http2SettingsFrame) { + handleInboundSettings(ctx, (Http2SettingsFrame) inMsg); + } else { + ctx.fireChannelRead(inMsg); + } + + } catch (Http2Exception e) { + ReferenceCountUtil.release(inMsg); + processHttp2Exception(ctx, e); + } + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) + throws Exception { + try { + if (msg instanceof Http2DataFrame) { + handleOutboundData(ctx, (Http2DataFrame) msg, promise); + } else if (msg instanceof Http2HeadersFrame) { + handleOutboundHeaders(ctx, (Http2HeadersFrame) msg, promise); + } else if (msg instanceof Http2PushPromiseFrame) { + handleOutboundPushPromise(ctx, (Http2PushPromiseFrame) msg, promise); + } else if (msg instanceof Http2PriorityFrame) { + handleOutboundPriority(ctx, (Http2PriorityFrame) msg, promise); + } else if (msg instanceof Http2RstStreamFrame) { + handleOutboundRstStream(ctx, (Http2RstStreamFrame) msg, promise); + } else if (msg instanceof Http2PingFrame) { + handleOutboundPing(ctx, (Http2PingFrame) msg, promise); + } else if (msg instanceof Http2GoAwayFrame) { + handleOutboundGoAway(); + } else if (msg instanceof Http2WindowUpdateFrame) { + handleOutboundWindowUpdate(); + } else if (msg instanceof Http2SettingsFrame) { + handleOutboundSettings(ctx, (Http2SettingsFrame) msg, promise); + } else { + ctx.write(msg, promise); + return; + } + + } catch (Throwable e) { + ReferenceCountUtil.release(msg); + promise.setFailure(e); + } + } + + /** + * Processes the given exception. Depending on the type of exception, delegates to either + * {@link #processConnectionError} or {@link #processStreamError}. + */ + private void processHttp2Exception(ChannelHandlerContext ctx, Http2Exception e) { + if (e instanceof Http2StreamException) { + processStreamError(ctx, (Http2StreamException) e); + } else { + processConnectionError(ctx, e); + } + } + + private void processConnectionError(ChannelHandlerContext ctx, Http2Exception cause) { + connection.sendGoAway(ctx, ctx.newPromise(), cause); + } + + private void processStreamError(ChannelHandlerContext ctx, Http2StreamException cause) { + // Close the stream if it was open. + int streamId = cause.getStreamId(); + ChannelPromise promise = ctx.newPromise(); + Http2Stream stream = connection.getStream(streamId); + if (stream != null) { + stream.close(ctx, promise); + } + + // Send the Rst frame to the remote endpoint. + Http2RstStreamFrame frame = new DefaultHttp2RstStreamFrame.Builder().setStreamId(streamId) + .setErrorCode(cause.getError().getCode()).build(); + ctx.writeAndFlush(frame, promise); + } + + private void handleInboundData(final ChannelHandlerContext ctx, Http2DataFrame frame) + throws Http2Exception { + + // Check if we received a data frame for a stream which is half-closed + Http2Stream stream = connection.getStreamOrFail(frame.getStreamId()); + stream.verifyState(STREAM_CLOSED, OPEN, HALF_CLOSED_LOCAL); + + // Apply flow control. + inboundFlow.applyInboundFlowControl(frame, new InboundFlowController.FrameWriter() { + @Override + public void writeFrame(Http2WindowUpdateFrame frame) { + ctx.writeAndFlush(frame); + } + }); + + if (isInboundStreamAfterGoAway(frame)) { + // Ignore frames for any stream created after we sent a go-away. + frame.release(); + return; + } + + if (frame.isEndOfStream()) { + stream.closeRemoteSide(ctx, ctx.newSucceededFuture()); + } + + // Allow this frame to continue other handlers. + ctx.fireChannelRead(frame); + } + + private void handleInboundHeaders(ChannelHandlerContext ctx, Http2HeadersFrame frame) + throws Http2Exception { + if (isInboundStreamAfterGoAway(frame)) { + return; + } + + int streamId = frame.getStreamId(); + Http2Stream stream = connection.getStream(streamId); + if (stream == null) { + // Create the new stream. + connection.remote().createStream(frame.getStreamId(), frame.getPriority(), + frame.isEndOfStream()); + } else { + // If the stream already exists, it must be a reserved push stream. If so, open + // it for push to the local endpoint. + stream.verifyState(PROTOCOL_ERROR, RESERVED_REMOTE); + stream.openForPush(); + + // If the headers completes this stream, close it. + if (frame.isEndOfStream()) { + stream.closeRemoteSide(ctx, ctx.newSucceededFuture()); + } + } + + ctx.fireChannelRead(frame); + } + + private void handleInboundPushPromise(ChannelHandlerContext ctx, Http2PushPromiseFrame frame) + throws Http2Exception { + if (isInboundStreamAfterGoAway(frame)) { + // Ignore frames for any stream created after we sent a go-away. + return; + } + + // Reserve the push stream based with a priority based on the current stream's priority. + Http2Stream parentStream = connection.getStreamOrFail(frame.getStreamId()); + connection.remote().reservePushStream(frame.getPromisedStreamId(), parentStream); + + ctx.fireChannelRead(frame); + } + + private void handleInboundPriority(ChannelHandlerContext ctx, Http2PriorityFrame frame) + throws Http2Exception { + if (isInboundStreamAfterGoAway(frame)) { + // Ignore frames for any stream created after we sent a go-away. + return; + } + + Http2Stream stream = connection.getStream(frame.getStreamId()); + if (stream == null) { + // Priority frames must be ignored for closed streams. + return; + } + + stream.verifyState(PROTOCOL_ERROR, HALF_CLOSED_LOCAL, HALF_CLOSED_REMOTE, OPEN, RESERVED_LOCAL); + + // Set the priority on the frame. + stream.setPriority(frame.getPriority()); + + ctx.fireChannelRead(frame); + } + + private void handleInboundWindowUpdate(ChannelHandlerContext ctx, Http2WindowUpdateFrame frame) + throws Http2Exception { + if (isInboundStreamAfterGoAway(frame)) { + // Ignore frames for any stream created after we sent a go-away. + return; + } + + int streamId = frame.getStreamId(); + if (streamId > 0) { + Http2Stream stream = connection.getStream(streamId); + if (stream == null) { + // Window Update frames must be ignored for closed streams. + return; + } + stream.verifyState(PROTOCOL_ERROR, OPEN, HALF_CLOSED_REMOTE); + } + + // Update the outbound flow controller. + outboundFlow.updateOutboundWindowSize(streamId, frame.getWindowSizeIncrement()); + + ctx.fireChannelRead(frame); + } + + private void handleInboundRstStream(ChannelHandlerContext ctx, Http2RstStreamFrame frame) { + if (isInboundStreamAfterGoAway(frame)) { + // Ignore frames for any stream created after we sent a go-away. + return; + } + + Http2Stream stream = connection.getStream(frame.getStreamId()); + if (stream == null) { + // RstStream frames must be ignored for closed streams. + return; + } + + stream.close(ctx, ctx.newSucceededFuture()); + + ctx.fireChannelRead(frame); + } + + private void handleInboundPing(ChannelHandlerContext ctx, Http2PingFrame frame) { + if (frame.isAck()) { + // The remote enpoint is responding to an Ack that we sent. + ctx.fireChannelRead(frame); + return; + } + + // The remote endpoint is sending the ping. Acknowledge receipt. + DefaultHttp2PingFrame ack = new DefaultHttp2PingFrame.Builder().setAck(true) + .setData(frame.content().duplicate().retain()).build(); + ctx.writeAndFlush(ack); + } + + private void handleInboundSettings(ChannelHandlerContext ctx, Http2SettingsFrame frame) + throws Http2Exception { + if (frame.isAck()) { + // The remote endpoint is acknowledging the settings - fire this up to the next + // handler. + ctx.fireChannelRead(frame); + return; + } + + // It's not an ack, apply the settings. + if (frame.getHeaderTableSize() != null) { + // TODO(nathanmittler): what's the right thing handle this? + // headersEncoder.setHeaderTableSize(frame.getHeaderTableSize()); + } + + if (frame.getPushEnabled() != null) { + connection.remote().setPushToAllowed(frame.getPushEnabled()); + } + + if (frame.getMaxConcurrentStreams() != null) { + int value = Math.max(0, (int) Math.min(Integer.MAX_VALUE, frame.getMaxConcurrentStreams())); + connection.local().setMaxStreams(value); + } + + if (frame.getInitialWindowSize() != null) { + outboundFlow.setInitialOutboundWindowSize(frame.getInitialWindowSize()); + } + + // Acknowledge receipt of the settings. + Http2Frame ack = new DefaultHttp2SettingsFrame.Builder().setAck(true).build(); + ctx.writeAndFlush(ack); + } + + private void handleInboundGoAway(ChannelHandlerContext ctx, Http2GoAwayFrame frame) { + // Don't allow any more connections to be created. + connection.goAwayReceived(); + ctx.fireChannelRead(frame); + } + + /** + * Determines whether or not the stream was created after we sent a go-away frame. Frames from + * streams created after we sent a go-away should be ignored. Frames for the connection stream ID + * (i.e. 0) will always be allowed. + */ + private boolean isInboundStreamAfterGoAway(Http2StreamFrame frame) { + return connection.isGoAwaySent() + && connection.remote().getLastStreamCreated() <= frame.getStreamId(); + } + + private void handleOutboundData(final ChannelHandlerContext ctx, Http2DataFrame frame, + final ChannelPromise promise) throws Http2Exception { + if (connection.isGoAway()) { + throw format(PROTOCOL_ERROR, "Sending data after connection going away."); + } + + Http2Stream stream = connection.getStreamOrFail(frame.getStreamId()); + stream.verifyState(PROTOCOL_ERROR, OPEN, HALF_CLOSED_REMOTE); + + // Hand control of the frame to the flow controller. + outboundFlow.sendFlowControlled(frame, new OutboundFlowController.FrameWriter() { + @Override + public void writeFrame(Http2DataFrame frame) { + ChannelFuture future = ctx.writeAndFlush(frame, promise); + + // Close the connection on write failures that leave the outbound flow control + // window in a corrupt state. + future.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) throws Exception { + if (!future.isSuccess()) { + processHttp2Exception(ctx, toHttp2Exception(future.cause())); + } + } + }); + + // Close the local side of the stream if this is the last frame + if (frame.isEndOfStream()) { + Http2Stream stream = connection.getStream(frame.getStreamId()); + stream.closeLocalSide(ctx, promise); + } + } + + @Override + public void setFailure(Throwable cause) { + promise.setFailure(cause); + } + }); + } + + private void handleOutboundHeaders(ChannelHandlerContext ctx, Http2HeadersFrame frame, + ChannelPromise promise) throws Http2Exception { + if (connection.isGoAway()) { + throw format(PROTOCOL_ERROR, "Sending headers after connection going away."); + } + + Http2Stream stream = connection.getStream(frame.getStreamId()); + if (stream == null) { + // Creates a new locally-initiated stream. + stream = connection.local().createStream(frame.getStreamId(), frame.getPriority(), + frame.isEndOfStream()); + } else { + // If the stream already exists, it must be a reserved push stream. If so, open + // it for push to the remote endpoint. + stream.verifyState(PROTOCOL_ERROR, RESERVED_LOCAL); + stream.openForPush(); + + // If the headers are the end of the stream, close it now. + if (frame.isEndOfStream()) { + stream.closeLocalSide(ctx, promise); + } + } + + // Flush to send all of the frames. + ctx.writeAndFlush(frame, promise); + } + + private void handleOutboundPushPromise(ChannelHandlerContext ctx, Http2PushPromiseFrame frame, + ChannelPromise promise) throws Http2Exception { + if (connection.isGoAway()) { + throw format(PROTOCOL_ERROR, "Sending push promise after connection going away."); + } + + // Reserve the promised stream. + Http2Stream stream = connection.getStreamOrFail(frame.getStreamId()); + connection.local().reservePushStream(frame.getPromisedStreamId(), stream); + + // Write the frame. + ctx.writeAndFlush(frame, promise); + } + + private void handleOutboundPriority(ChannelHandlerContext ctx, Http2PriorityFrame frame, + ChannelPromise promise) throws Http2Exception { + if (connection.isGoAway()) { + throw format(PROTOCOL_ERROR, "Sending priority after connection going away."); + } + + // Set the priority on the stream and forward the frame. + Http2Stream stream = connection.getStreamOrFail(frame.getStreamId()); + stream.setPriority(frame.getPriority()); + ctx.writeAndFlush(frame, promise); + } + + private void handleOutboundRstStream(ChannelHandlerContext ctx, Http2RstStreamFrame frame, + ChannelPromise promise) { + Http2Stream stream = connection.getStream(frame.getStreamId()); + if (stream == null) { + // The stream may already have been closed ... ignore. + promise.setSuccess(); + return; + } + + stream.close(ctx, promise); + ctx.writeAndFlush(frame, promise); + } + + private void handleOutboundPing(ChannelHandlerContext ctx, Http2PingFrame frame, + ChannelPromise promise) throws Http2Exception { + if (connection.isGoAway()) { + throw format(PROTOCOL_ERROR, "Sending ping after connection going away."); + } + + if (frame.isAck()) { + throw format(PROTOCOL_ERROR, "Another handler attempting to send ping ack"); + } + + // Just pass the frame through. + ctx.writeAndFlush(frame, promise); + } + + private void handleOutboundGoAway() throws Http2Exception { + // Why is this being sent? Intercept it and fail the write. + // Should have sent a CLOSE ChannelStateEvent + throw format(PROTOCOL_ERROR, "Another handler attempted to send GoAway."); + } + + private void handleOutboundWindowUpdate() throws Http2Exception { + // Why is this being sent? Intercept it and fail the write. + throw format(PROTOCOL_ERROR, "Another handler attempted to send window update."); + } + + private void handleOutboundSettings(ChannelHandlerContext ctx, Http2SettingsFrame frame, + ChannelPromise promise) throws Http2Exception { + if (connection.isGoAway()) { + throw format(PROTOCOL_ERROR, "Sending settings after connection going away."); + } + + if (frame.isAck()) { + throw format(PROTOCOL_ERROR, "Another handler attempting to send settings ack"); + } + + if (frame.getPushEnabled() != null) { + // Enable/disable server push to this endpoint. + connection.local().setPushToAllowed(frame.getPushEnabled()); + } + if (frame.getHeaderTableSize() != null) { + // TODO(nathanmittler): what's the right way to handle this? + // headersDecoder.setHeaderTableSize(frame.getHeaderTableSize()); + } + if (frame.getMaxConcurrentStreams() != null) { + // Update maximum number of streams the remote endpoint can initiate. + if (frame.getMaxConcurrentStreams() < 0L + || frame.getMaxConcurrentStreams() > Integer.MAX_VALUE) { + throw format(PROTOCOL_ERROR, "Invalid value for max concurrent streams: %d", + frame.getMaxConcurrentStreams()); + } + connection.remote().setMaxStreams(frame.getMaxConcurrentStreams().intValue()); + } + if (frame.getInitialWindowSize() != null) { + // Update the initial window size for inbound traffic. + if (frame.getInitialWindowSize() < 0) { + throw format(PROTOCOL_ERROR, "Invalid value for initial window size: %d", + frame.getInitialWindowSize()); + } + inboundFlow.setInitialInboundWindowSize(frame.getInitialWindowSize()); + } + ctx.writeAndFlush(frame, promise); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2ConnectionUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2ConnectionUtil.java new file mode 100644 index 0000000000..7cf0a84969 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2ConnectionUtil.java @@ -0,0 +1,62 @@ +/* + * Copyright 2014 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.draft10.connection; + +import static io.netty.handler.codec.http2.draft10.Http2Error.INTERNAL_ERROR; +import static io.netty.handler.codec.http2.draft10.Http2Exception.format; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http2.draft10.Http2Exception; + +/** + * Constants and utility method used for encoding/decoding HTTP2 frames. + */ +public final class Http2ConnectionUtil { + + public static final int DEFAULT_FLOW_CONTROL_WINDOW_SIZE = 65535; + public static final int DEFAULT_HEADER_TABLE_SIZE = 4096; + public static final int DEFAULT_MAX_HEADER_SIZE = 4096; + + /** + * Converts the given cause to a {@link Http2Exception} if it isn't already. + */ + public static Http2Exception toHttp2Exception(Throwable cause) { + if (cause instanceof Http2Exception) { + return (Http2Exception) cause; + } + String msg = cause != null ? cause.getMessage() : "Failed writing the data frame."; + return format(INTERNAL_ERROR, msg); + } + + /** + * Creates a buffer containing the error message from the given exception. If the cause is + * {@code null} returns an empty buffer. + */ + public static ByteBuf toByteBuf(ChannelHandlerContext ctx, Throwable cause) { + ByteBuf debugData = Unpooled.EMPTY_BUFFER; + if (cause != null) { + // Create the debug message. + byte[] msg = cause.getMessage().getBytes(); + debugData = ctx.alloc().buffer(msg.length); + debugData.writeBytes(msg); + } + return debugData; + } + + private Http2ConnectionUtil() { + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2Stream.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2Stream.java new file mode 100644 index 0000000000..dc7a490cd3 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/Http2Stream.java @@ -0,0 +1,95 @@ +/* + * Copyright 2014 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.draft10.connection; + +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http2.draft10.Http2Error; +import io.netty.handler.codec.http2.draft10.Http2Exception; + +/** + * A single stream within an HTTP2 connection. Streams are compared to each other by priority. + */ +public interface Http2Stream extends Comparable { + + /** + * The allowed states of an HTTP2 stream. + */ + enum State { + IDLE, RESERVED_LOCAL, RESERVED_REMOTE, OPEN, HALF_CLOSED_LOCAL, HALF_CLOSED_REMOTE, CLOSED; + } + + /** + * Gets the unique identifier for this stream within the connection. + */ + int getId(); + + /** + * Gets the state of this stream. + */ + State getState(); + + /** + * Verifies that the stream is in one of the given allowed states. + */ + void verifyState(Http2Error error, State... allowedStates) throws Http2Exception; + + /** + * Sets the priority of this stream. A value of zero is the highest priority and a value of + * {@link Integer#MAX_VALUE} is the lowest. + */ + void setPriority(int priority) throws Http2Exception; + + /** + * Gets the priority of this stream. A value of zero is the highest priority and a value of + * {@link Integer#MAX_VALUE} is the lowest. + */ + int getPriority(); + + /** + * If this is a reserved push stream, opens the stream for push in one direction. + */ + void openForPush() throws Http2Exception; + + /** + * Closes the stream. + */ + void close(ChannelHandlerContext ctx, ChannelFuture future); + + /** + * Closes the local side of this stream. If this makes the stream closed, the child is closed as + * well. + */ + void closeLocalSide(ChannelHandlerContext ctx, ChannelFuture future); + + /** + * Closes the remote side of this stream. If this makes the stream closed, the child is closed as + * well. + */ + void closeRemoteSide(ChannelHandlerContext ctx, ChannelFuture future); + + /** + * Indicates whether the remote side of this stream is open (i.e. the state is either + * {@link State#OPEN} or {@link State#HALF_CLOSED_LOCAL}). + */ + boolean isRemoteSideOpen(); + + /** + * Indicates whether the local side of this stream is open (i.e. the state is either + * {@link State#OPEN} or {@link State#HALF_CLOSED_REMOTE}). + */ + boolean isLocalSideOpen(); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/InboundFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/InboundFlowController.java new file mode 100644 index 0000000000..3d48353ff8 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/InboundFlowController.java @@ -0,0 +1,56 @@ +/* + * Copyright 2014 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.draft10.connection; + +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.Http2DataFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2WindowUpdateFrame; + +/** + * Controls the inbound flow of data frames from the remote endpoint. + */ +public interface InboundFlowController { + + /** + * A writer of window update frames. + */ + interface FrameWriter { + + /** + * Writes a window update frame to the remote endpoint. + */ + void writeFrame(Http2WindowUpdateFrame frame); + } + + /** + * Sets the initial inbound flow control window size and updates all stream window sizes by the + * delta. This is called as part of the processing for an outbound SETTINGS frame. + * + * @param newWindowSize the new initial window size. + * @throws Http2Exception thrown if any protocol-related error occurred. + */ + void setInitialInboundWindowSize(int newWindowSize) throws Http2Exception; + + /** + * Applies flow control for the received data frame. + * + * @param dataFrame the flow controlled frame + * @param frameWriter allows this flow controller to send window updates to the remote endpoint. + * @throws Http2Exception thrown if any protocol-related error occurred. + */ + void applyInboundFlowControl(Http2DataFrame dataFrame, FrameWriter frameWriter) + throws Http2Exception; +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/OutboundFlowController.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/OutboundFlowController.java new file mode 100644 index 0000000000..78fbaf6e43 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/OutboundFlowController.java @@ -0,0 +1,79 @@ +/* + * Copyright 2014 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.draft10.connection; + +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.Http2DataFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; + +/** + * Controls the outbound flow of data frames to the remote endpoint. + */ +public interface OutboundFlowController { + + /** + * Interface that abstracts the writing of {@link Http2Frame} objects to the remote endpoint. + */ + interface FrameWriter { + + /** + * Writes a single data frame to the remote endpoint. + */ + void writeFrame(Http2DataFrame frame); + + /** + * Called if an error occurred before the write could take place. Sets the failure on the + * channel promise. + */ + void setFailure(Throwable cause); + } + + /** + * Sets the initial size of the connection's outbound flow control window. The outbound flow + * control windows for all streams are updated by the delta in the initial window size. This is + * called as part of the processing of a SETTINGS frame received from the remote endpoint. + * + * @param newWindowSize the new initial window size. + */ + void setInitialOutboundWindowSize(int newWindowSize) throws Http2Exception; + + /** + * Updates the size of the stream's outbound flow control window. This is called upon receiving a + * WINDOW_UPDATE frame from the remote endpoint. + * + * @param streamId the ID of the stream, or zero if the window is for the entire connection. + * @param deltaWindowSize the change in size of the outbound flow control window. + * @throws Http2Exception thrown if a protocol-related error occurred. + */ + void updateOutboundWindowSize(int streamId, int deltaWindowSize) throws Http2Exception; + + /** + * Sends the frame with outbound flow control applied. The frame may be written at a later time, + * depending on whether the remote endpoint can receive the frame now. + *

+ * 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 stream's transfer window size is decremented by the amount of data + * transmitted. When the window size becomes less than or equal to 0, the sender must pause + * transmitting data frames. + * + * @param frame the frame to send. + * @param frameWriter peforms to the write of the frame to the remote endpoint. + * @throws Http2Exception thrown if a protocol-related error occurred. + */ + void sendFlowControlled(Http2DataFrame frame, FrameWriter frameWriter) throws Http2Exception; +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/package-info.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/package-info.java new file mode 100644 index 0000000000..5e03678bb2 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/connection/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014 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. + */ + +/** + * Connection-level services (stream management, flow control) for HTTP2. + */ +package io.netty.handler.codec.http2.draft10.connection; + diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2DataFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2DataFrame.java new file mode 100644 index 0000000000..c9b56ac886 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2DataFrame.java @@ -0,0 +1,190 @@ +/* + * Copyright 2014 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.draft10.frame; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_FRAME_PAYLOAD_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_UNSIGNED_SHORT; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.DefaultByteBufHolder; +import io.netty.buffer.Unpooled; + +/** + * Default implementation of {@link Http2DataFrame}. + */ +public final class DefaultHttp2DataFrame extends DefaultByteBufHolder implements Http2DataFrame { + + private final int paddingLength; + private final int streamId; + private final boolean endOfStream; + + private DefaultHttp2DataFrame(Builder builder) { + super(builder.content); + this.streamId = builder.streamId; + this.endOfStream = builder.endOfStream; + this.paddingLength = builder.paddingLength; + } + + @Override + public int getStreamId() { + return streamId; + } + + @Override + public boolean isEndOfStream() { + return endOfStream; + } + + @Override + public int getPaddingLength() { + return paddingLength; + } + + @Override + public DefaultHttp2DataFrame copy() { + return copyBuilder().setContent(content().copy()).build(); + } + + @Override + public DefaultHttp2DataFrame duplicate() { + return copyBuilder().setContent(content().duplicate()).build(); + } + + @Override + public DefaultHttp2DataFrame retain() { + super.retain(); + return this; + } + + @Override + public DefaultHttp2DataFrame retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public DefaultHttp2DataFrame touch() { + super.touch(); + return this; + } + + @Override + public DefaultHttp2DataFrame touch(Object hint) { + super.touch(hint); + return this; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = content().hashCode(); + result = prime * result + (endOfStream ? 1231 : 1237); + result = prime * result + paddingLength; + result = prime * result + streamId; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + DefaultHttp2DataFrame other = (DefaultHttp2DataFrame) obj; + if (endOfStream != other.endOfStream) { + return false; + } + if (paddingLength != other.paddingLength) { + return false; + } + if (streamId != other.streamId) { + return false; + } + if (!content().equals(other.content())) { + return false; + } + return true; + } + + private Builder copyBuilder() { + return new Builder().setStreamId(streamId).setPaddingLength(paddingLength) + .setEndOfStream(endOfStream); + } + + /** + * Builds instances of {@link DefaultHttp2DataFrame}. + */ + public static class Builder { + private int streamId; + private boolean endOfStream; + private ByteBuf content = Unpooled.EMPTY_BUFFER; + private int paddingLength; + + public Builder setStreamId(int streamId) { + if (streamId <= 0) { + throw new IllegalArgumentException("StreamId must be > 0."); + } + this.streamId = streamId; + return this; + } + + public Builder setEndOfStream(boolean endOfStream) { + this.endOfStream = endOfStream; + return this; + } + + /** + * Sets the content for the data frame, excluding any padding. This buffer will be retained when + * the frame is built. + */ + public Builder setContent(ByteBuf content) { + if (content == null) { + throw new IllegalArgumentException("content must not be null"); + } + verifyLength(paddingLength, content); + this.content = content; + return this; + } + + public Builder setPaddingLength(int paddingLength) { + if (paddingLength < 0 || paddingLength > MAX_UNSIGNED_SHORT) { + throw new IllegalArgumentException("Padding length invalid."); + } + verifyLength(paddingLength, content); + this.paddingLength = paddingLength; + return this; + } + + public DefaultHttp2DataFrame build() { + if (streamId <= 0) { + throw new IllegalArgumentException("StreamId must be set."); + } + + verifyLength(paddingLength, content); + + return new DefaultHttp2DataFrame(this); + } + + private void verifyLength(int paddingLength, ByteBuf data) { + int maxLength = MAX_FRAME_PAYLOAD_LENGTH; + maxLength -= paddingLength; + if (data.readableBytes() > maxLength) { + throw new IllegalArgumentException("Header block fragment length too big."); + } + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2GoAwayFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2GoAwayFrame.java new file mode 100644 index 0000000000..8bfa5fea97 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2GoAwayFrame.java @@ -0,0 +1,163 @@ +/* + * Copyright 2014 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.draft10.frame; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_FRAME_PAYLOAD_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_UNSIGNED_INT; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.DefaultByteBufHolder; +import io.netty.buffer.Unpooled; + +/** + * Default implementation of {@link Http2GoAwayFrame}. + */ +public final class DefaultHttp2GoAwayFrame extends DefaultByteBufHolder implements + Http2GoAwayFrame { + private final int lastStreamId; + private final long errorCode; + + private DefaultHttp2GoAwayFrame(Builder builder) { + super(builder.debugData); + this.lastStreamId = builder.lastStreamId; + this.errorCode = builder.errorCode; + } + + @Override + public int getLastStreamId() { + return lastStreamId; + } + + @Override + public long getErrorCode() { + return errorCode; + } + + @Override + public DefaultHttp2GoAwayFrame copy() { + return copyBuilder().setDebugData(content().copy()).build(); + } + + @Override + public DefaultHttp2GoAwayFrame duplicate() { + return copyBuilder().setDebugData(content().duplicate()).build(); + } + + @Override + public DefaultHttp2GoAwayFrame retain() { + super.retain(); + return this; + } + + @Override + public DefaultHttp2GoAwayFrame retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public DefaultHttp2GoAwayFrame touch() { + super.touch(); + return this; + } + + @Override + public DefaultHttp2GoAwayFrame touch(Object hint) { + super.touch(hint); + return this; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = content().hashCode(); + result = prime * result + (int) (errorCode ^ (errorCode >>> 32)); + result = prime * result + lastStreamId; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (getClass() != obj.getClass()) { + return false; + } + DefaultHttp2GoAwayFrame other = (DefaultHttp2GoAwayFrame) obj; + if (errorCode != other.errorCode) { + return false; + } + if (lastStreamId != other.lastStreamId) { + return false; + } + if (!content().equals(other.content())) { + return false; + } + return true; + } + + private Builder copyBuilder() { + return new Builder().setErrorCode(errorCode).setLastStreamId(lastStreamId); + } + + /** + * Builds instances of {@link DefaultHttp2GoAwayFrame}. + */ + public static class Builder { + private int lastStreamId = -1; + private long errorCode = -1; + private ByteBuf debugData = Unpooled.EMPTY_BUFFER; + + public Builder setLastStreamId(int lastStreamId) { + if (lastStreamId < 0) { + throw new IllegalArgumentException("Invalid lastStreamId."); + } + this.lastStreamId = lastStreamId; + return this; + } + + public Builder setErrorCode(long errorCode) { + if (errorCode < 0 || errorCode > MAX_UNSIGNED_INT) { + throw new IllegalArgumentException("Invalid error code."); + } + this.errorCode = errorCode; + return this; + } + + public Builder setDebugData(ByteBuf debugData) { + if (debugData == null) { + throw new IllegalArgumentException("debugData must not be null"); + } + if (debugData.readableBytes() > MAX_FRAME_PAYLOAD_LENGTH - 8) { + throw new IllegalArgumentException("Invalid debug data size."); + } + + this.debugData = debugData; + return this; + } + + public DefaultHttp2GoAwayFrame build() { + if (lastStreamId < 0) { + throw new IllegalArgumentException("LastStreamId must be set"); + } + if (errorCode < 0) { + throw new IllegalArgumentException("ErrorCode must be set."); + } + + return new DefaultHttp2GoAwayFrame(this); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2HeadersFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2HeadersFrame.java new file mode 100644 index 0000000000..2083277884 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2HeadersFrame.java @@ -0,0 +1,146 @@ +/* + * Copyright 2014 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.draft10.frame; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.DEFAULT_STREAM_PRIORITY; +import io.netty.handler.codec.http2.draft10.Http2Headers; + +public final class DefaultHttp2HeadersFrame implements Http2HeadersFrame { + + private final int streamId; + private final int priority; + private final boolean endOfStream; + private final Http2Headers headers; + + private DefaultHttp2HeadersFrame(Builder builder) { + this.streamId = builder.streamId; + this.priority = builder.priority; + this.headers = builder.headersBuilder.build(); + this.endOfStream = builder.endOfStream; + } + + @Override + public int getStreamId() { + return streamId; + } + + @Override + public boolean isEndOfStream() { + return endOfStream; + } + + @Override + public int getPriority() { + return priority; + } + + @Override + public Http2Headers getHeaders() { + return headers; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (endOfStream ? 1231 : 1237); + result = prime * result + ((headers == null) ? 0 : headers.hashCode()); + result = prime * result + priority; + result = prime * result + streamId; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + DefaultHttp2HeadersFrame other = (DefaultHttp2HeadersFrame) obj; + if (endOfStream != other.endOfStream) { + return false; + } + if (headers == null) { + if (other.headers != null) { + return false; + } + } else if (!headers.equals(other.headers)) { + return false; + } + if (priority != other.priority) { + return false; + } + if (streamId != other.streamId) { + return false; + } + return true; + } + + @Override + public String toString() { + return "DefaultHttp2HeadersFrame [streamId=" + streamId + ", priority=" + priority + + ", endOfStream=" + endOfStream + ", headers=" + headers + "]"; + } + + public static class Builder { + private int streamId; + private int priority = DEFAULT_STREAM_PRIORITY; + private Http2Headers.Builder headersBuilder = new Http2Headers.Builder(); + private boolean endOfStream; + + public Builder setStreamId(int streamId) { + if (streamId <= 0) { + throw new IllegalArgumentException("StreamId must be > 0."); + } + this.streamId = streamId; + return this; + } + + public Builder setEndOfStream(boolean endOfStream) { + this.endOfStream = endOfStream; + return this; + } + + public Builder setPriority(int priority) { + if (priority < 0) { + throw new IllegalArgumentException("Priority must be >= 0"); + } + this.priority = priority; + return this; + } + + public Http2Headers.Builder headers() { + return headersBuilder; + } + + public Builder setHeaders(Http2Headers headers) { + this.headersBuilder.addHeaders(headers); + return this; + } + + public DefaultHttp2HeadersFrame build() { + if (streamId <= 0) { + throw new IllegalArgumentException("StreamId must be set."); + } + return new DefaultHttp2HeadersFrame(this); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2PingFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2PingFrame.java new file mode 100644 index 0000000000..29a542f9cb --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2PingFrame.java @@ -0,0 +1,138 @@ +/* + * Copyright 2014 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.draft10.frame; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.PING_FRAME_PAYLOAD_LENGTH; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.DefaultByteBufHolder; + +/** + * Default implementation of {@link Http2PingFrame}. + */ +public final class DefaultHttp2PingFrame extends DefaultByteBufHolder implements Http2PingFrame { + + private final boolean ack; + + private DefaultHttp2PingFrame(Builder builder) { + super(builder.data); + this.ack = builder.ack; + } + + @Override + public boolean isAck() { + return ack; + } + + @Override + public DefaultHttp2PingFrame copy() { + return new Builder().setAck(ack).setData(content().copy()).build(); + } + + @Override + public DefaultHttp2PingFrame duplicate() { + return new Builder().setAck(ack).setData(content().duplicate()).build(); + } + + @Override + public DefaultHttp2PingFrame retain() { + super.retain(); + return this; + } + + @Override + public DefaultHttp2PingFrame retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public DefaultHttp2PingFrame touch() { + super.touch(); + return this; + } + + @Override + public DefaultHttp2PingFrame touch(Object hint) { + super.touch(hint); + return this; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = content().hashCode(); + result = prime * result + (ack ? 1231 : 1237); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + DefaultHttp2PingFrame other = (DefaultHttp2PingFrame) obj; + if (ack != other.ack) { + return false; + } + if (!content().equals(other.content())) { + return false; + } + return true; + } + + /** + * Builds instances of {@link DefaultHttp2PingFrame}. + */ + public static class Builder { + private boolean ack; + private ByteBuf data; + + /** + * Sets the data for this ping. This buffer will be retained when the frame is built. + */ + public Builder setData(ByteBuf data) { + if (data == null) { + throw new IllegalArgumentException("data must not be null."); + } + if (data.readableBytes() != PING_FRAME_PAYLOAD_LENGTH) { + throw new IllegalArgumentException(String.format( + "Incorrect data length for ping. Expected %d, found %d", + PING_FRAME_PAYLOAD_LENGTH, data.readableBytes())); + } + this.data = data; + return this; + } + + public Builder setAck(boolean ack) { + this.ack = ack; + return this; + } + + public DefaultHttp2PingFrame build() { + if (data == null) { + throw new IllegalArgumentException("debug data must be provided"); + } + + return new DefaultHttp2PingFrame(this); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2PriorityFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2PriorityFrame.java new file mode 100644 index 0000000000..57536cfec2 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2PriorityFrame.java @@ -0,0 +1,109 @@ +/* + * Copyright 2014 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.draft10.frame; + +/** + * Default implementation of {@link Http2PriorityFrame}. + */ +public final class DefaultHttp2PriorityFrame implements Http2PriorityFrame { + + private final int streamId; + private final int priority; + + private DefaultHttp2PriorityFrame(Builder builder) { + this.streamId = builder.streamId; + this.priority = builder.priority; + } + + @Override + public int getStreamId() { + return streamId; + } + + @Override + public int getPriority() { + return priority; + } + + @Override + public boolean isEndOfStream() { + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + priority; + result = prime * result + streamId; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + DefaultHttp2PriorityFrame other = (DefaultHttp2PriorityFrame) obj; + if (priority != other.priority) { + return false; + } + if (streamId != other.streamId) { + return false; + } + return true; + } + + /** + * Builds instances of {@link DefaultHttp2PriorityFrame}. + */ + public static class Builder { + private int streamId; + private int priority = -1; + + public Builder setStreamId(int streamId) { + if (streamId <= 0) { + throw new IllegalArgumentException("StreamId must be > 0."); + } + this.streamId = streamId; + return this; + } + + public Builder setPriority(int priority) { + if (priority < 0) { + throw new IllegalArgumentException("Invalid priority."); + } + this.priority = priority; + return this; + } + + public DefaultHttp2PriorityFrame build() { + if (streamId <= 0) { + throw new IllegalArgumentException("StreamId must be set."); + } + if (priority < 0) { + throw new IllegalArgumentException("Priority must be set."); + } + return new DefaultHttp2PriorityFrame(this); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2PushPromiseFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2PushPromiseFrame.java new file mode 100644 index 0000000000..15ac028472 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2PushPromiseFrame.java @@ -0,0 +1,138 @@ +/* + * Copyright 2014 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.draft10.frame; + +import io.netty.handler.codec.http2.draft10.Http2Headers; + +public final class DefaultHttp2PushPromiseFrame implements Http2PushPromiseFrame { + + private final int streamId; + private final int promisedStreamId; + private final Http2Headers headers; + + private DefaultHttp2PushPromiseFrame(Builder builder) { + this.streamId = builder.streamId; + this.promisedStreamId = builder.promisedStreamId; + this.headers = builder.headers; + } + + @Override + public int getStreamId() { + return streamId; + } + + @Override + public boolean isEndOfStream() { + return false; + } + + @Override + public int getPromisedStreamId() { + return promisedStreamId; + } + + @Override + public Http2Headers getHeaders() { + return headers; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((headers == null) ? 0 : headers.hashCode()); + result = prime * result + promisedStreamId; + result = prime * result + streamId; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + DefaultHttp2PushPromiseFrame other = (DefaultHttp2PushPromiseFrame) obj; + if (headers == null) { + if (other.headers != null) { + return false; + } + } else if (!headers.equals(other.headers)) { + return false; + } + if (promisedStreamId != other.promisedStreamId) { + return false; + } + if (streamId != other.streamId) { + return false; + } + return true; + } + + @Override + public String toString() { + return "DefaultHttp2PushPromiseFrame [streamId=" + streamId + ", promisedStreamId=" + + promisedStreamId + ", headers=" + headers + "]"; + } + + public static class Builder { + private int streamId; + private int promisedStreamId; + private Http2Headers headers; + + public Builder setStreamId(int streamId) { + if (streamId <= 0) { + throw new IllegalArgumentException("StreamId must be > 0."); + } + this.streamId = streamId; + return this; + } + + public Builder setPromisedStreamId(int promisedStreamId) { + if (promisedStreamId <= 0) { + throw new IllegalArgumentException("promisedStreamId must be > 0."); + } + this.promisedStreamId = promisedStreamId; + return this; + } + + public Builder setHeaders(Http2Headers headers) { + if (headers == null) { + throw new IllegalArgumentException("headers must not be null."); + } + this.headers = headers; + return this; + } + + public DefaultHttp2PushPromiseFrame build() { + if (streamId <= 0) { + throw new IllegalArgumentException("StreamId must be set."); + } + if (promisedStreamId <= 0) { + throw new IllegalArgumentException("promisedStreamId must be set."); + } + if (headers == null) { + throw new IllegalArgumentException("headers must be set."); + } + return new DefaultHttp2PushPromiseFrame(this); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2RstStreamFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2RstStreamFrame.java new file mode 100644 index 0000000000..727d666a2c --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2RstStreamFrame.java @@ -0,0 +1,110 @@ +/* + * Copyright 2014 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.draft10.frame; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_UNSIGNED_INT; + +/** + * Default implementation of {@link Http2RstStreamFrame}. + */ +public final class DefaultHttp2RstStreamFrame implements Http2RstStreamFrame { + private final int streamId; + private final long errorCode; + + private DefaultHttp2RstStreamFrame(Builder builder) { + this.streamId = builder.streamId; + this.errorCode = builder.errorCode; + } + + @Override + public int getStreamId() { + return streamId; + } + + @Override + public long getErrorCode() { + return errorCode; + } + + @Override + public boolean isEndOfStream() { + return false; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (int) (errorCode ^ (errorCode >>> 32)); + result = prime * result + streamId; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + DefaultHttp2RstStreamFrame other = (DefaultHttp2RstStreamFrame) obj; + if (errorCode != other.errorCode) { + return false; + } + if (streamId != other.streamId) { + return false; + } + return true; + } + + /** + * Builds instances of {@link DefaultHttp2RstStreamFrame}. + */ + public static class Builder { + private int streamId; + private long errorCode = -1L; + + public Builder setStreamId(int streamId) { + if (streamId <= 0) { + throw new IllegalArgumentException("StreamId must be > 0."); + } + this.streamId = streamId; + return this; + } + + public Builder setErrorCode(long errorCode) { + if (errorCode < 0 || errorCode > MAX_UNSIGNED_INT) { + throw new IllegalArgumentException("Invalid errorCode value."); + } + this.errorCode = errorCode; + return this; + } + + public DefaultHttp2RstStreamFrame build() { + if (streamId <= 0) { + throw new IllegalArgumentException("StreamId must be set."); + } + if (errorCode < 0L) { + throw new IllegalArgumentException("ErrorCode must be set."); + } + return new DefaultHttp2RstStreamFrame(this); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2SettingsFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2SettingsFrame.java new file mode 100644 index 0000000000..5a40829eb3 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2SettingsFrame.java @@ -0,0 +1,164 @@ +/* + * Copyright 2014 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.draft10.frame; + +/** + * Default implementation of {@link Http2SettingsFrame}. + */ +public final class DefaultHttp2SettingsFrame implements Http2SettingsFrame { + + private final boolean ack; + private final Integer headerTableSize; + private final Boolean pushEnabled; + private final Long maxConcurrentStreams; + private final Integer initialWindowSize; + + private DefaultHttp2SettingsFrame(Builder builder) { + this.ack = builder.ack; + this.headerTableSize = builder.headerTableSize; + this.pushEnabled = builder.pushEnabled; + this.maxConcurrentStreams = builder.maxConcurrentStreams; + this.initialWindowSize = builder.initialWindowSize; + } + + @Override + public boolean isAck() { + return ack; + } + + @Override + public Integer getHeaderTableSize() { + return headerTableSize; + } + + @Override + public Boolean getPushEnabled() { + return pushEnabled; + } + + @Override + public Long getMaxConcurrentStreams() { + return maxConcurrentStreams; + } + + @Override + public Integer getInitialWindowSize() { + return initialWindowSize; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (ack ? 1231 : 1237); + result = prime * result + ((headerTableSize == null) ? 0 : headerTableSize.hashCode()); + result = prime * result + ((initialWindowSize == null) ? 0 : initialWindowSize.hashCode()); + result = + prime * result + ((maxConcurrentStreams == null) ? 0 : maxConcurrentStreams.hashCode()); + result = prime * result + ((pushEnabled == null) ? 0 : pushEnabled.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + DefaultHttp2SettingsFrame other = (DefaultHttp2SettingsFrame) obj; + if (ack != other.ack) { + return false; + } + if (headerTableSize == null) { + if (other.headerTableSize != null) { + return false; + } + } else if (!headerTableSize.equals(other.headerTableSize)) { + return false; + } + if (initialWindowSize == null) { + if (other.initialWindowSize != null) { + return false; + } + } else if (!initialWindowSize.equals(other.initialWindowSize)) { + return false; + } + if (maxConcurrentStreams == null) { + if (other.maxConcurrentStreams != null) { + return false; + } + } else if (!maxConcurrentStreams.equals(other.maxConcurrentStreams)) { + return false; + } + if (pushEnabled == null) { + if (other.pushEnabled != null) { + return false; + } + } else if (!pushEnabled.equals(other.pushEnabled)) { + return false; + } + return true; + } + + /** + * Builds instances of {@link DefaultHttp2SettingsFrame}. + */ + public static class Builder { + private boolean ack; + private Integer headerTableSize; + private Boolean pushEnabled; + private Long maxConcurrentStreams; + private Integer initialWindowSize; + + public Builder setAck(boolean ack) { + this.ack = ack; + return this; + } + + public Builder setHeaderTableSize(int headerTableSize) { + this.headerTableSize = headerTableSize; + return this; + } + + public Builder setPushEnabled(boolean pushEnabled) { + this.pushEnabled = pushEnabled; + return this; + } + + public Builder setMaxConcurrentStreams(long maxConcurrentStreams) { + this.maxConcurrentStreams = maxConcurrentStreams; + return this; + } + + public Builder setInitialWindowSize(int initialWindowSize) { + this.initialWindowSize = initialWindowSize; + return this; + } + + public DefaultHttp2SettingsFrame build() { + if (ack && (headerTableSize != null || pushEnabled != null || maxConcurrentStreams != null + || initialWindowSize != null)) { + throw new IllegalArgumentException("Ack frame must not contain settings"); + } + return new DefaultHttp2SettingsFrame(this); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2WindowUpdateFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2WindowUpdateFrame.java new file mode 100644 index 0000000000..9ffdd24046 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/DefaultHttp2WindowUpdateFrame.java @@ -0,0 +1,103 @@ +/* + * Copyright 2014 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.draft10.frame; + +/** + * Default implementation of {@link Http2WindowUpdateFrame}. + */ +public final class DefaultHttp2WindowUpdateFrame implements Http2WindowUpdateFrame { + + private final int streamId; + private final int windowSizeIncrement; + + private DefaultHttp2WindowUpdateFrame(Builder builder) { + this.streamId = builder.streamId; + this.windowSizeIncrement = builder.windowSizeIncrement; + } + + @Override + public int getStreamId() { + return streamId; + } + + @Override + public boolean isEndOfStream() { + return false; + } + + @Override + public int getWindowSizeIncrement() { + return windowSizeIncrement; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + streamId; + result = prime * result + windowSizeIncrement; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + DefaultHttp2WindowUpdateFrame other = (DefaultHttp2WindowUpdateFrame) obj; + if (streamId != other.streamId) { + return false; + } + if (windowSizeIncrement != other.windowSizeIncrement) { + return false; + } + return true; + } + + /** + * Builds instances of {@link DefaultHttp2WindowUpdateFrame}. + */ + public static class Builder { + private int streamId; + private int windowSizeIncrement; + + public Builder setStreamId(int streamId) { + this.streamId = streamId; + return this; + } + + public Builder setWindowSizeIncrement(int windowSizeIncrement) { + this.windowSizeIncrement = windowSizeIncrement; + return this; + } + + public DefaultHttp2WindowUpdateFrame build() { + if (streamId < 0) { + throw new IllegalArgumentException("StreamId must be >= 0."); + } + if (windowSizeIncrement < 0) { + throw new IllegalArgumentException("SindowSizeIncrement must be >= 0."); + } + return new DefaultHttp2WindowUpdateFrame(this); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2DataFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2DataFrame.java new file mode 100644 index 0000000000..fa04eefd63 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2DataFrame.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014 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.draft10.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; + +/** + * An HTTP2 data frame. + */ +public interface Http2DataFrame extends Http2StreamFrame, ByteBufHolder { + + /** + * The amount of padding to follow the header data in the frame. + */ + int getPaddingLength(); + + /** + * Returns the data payload of this frame. + */ + @Override + ByteBuf content(); + + @Override + Http2DataFrame copy(); + + @Override + Http2DataFrame duplicate(); + + @Override + Http2DataFrame retain(); + + @Override + Http2DataFrame retain(int increment); + + @Override + Http2DataFrame touch(); + + @Override + Http2DataFrame touch(Object hint); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2Flags.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2Flags.java new file mode 100644 index 0000000000..8ef0c37dec --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2Flags.java @@ -0,0 +1,138 @@ +/* + * Copyright 2014 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.draft10.frame; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FLAG_ACK; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FLAG_END_HEADERS; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FLAG_END_SEGMENT; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FLAG_END_STREAM; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FLAG_PAD_HIGH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FLAG_PAD_LOW; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FLAG_PRIORITY; + +/** + * Provides utility methods for accessing specific flags as defined by the HTTP2 spec. + */ +public class Http2Flags { + private final short value; + + public Http2Flags(short value) { + this.value = value; + } + + /** + * Gets the underlying flags value. + */ + public short getValue() { + return value; + } + + /** + * Determines whether the end-of-stream flag is set. + */ + public boolean isEndOfStream() { + return isSet(FLAG_END_STREAM); + } + + /** + * Determines whether the end-of-segment flag is set. + */ + public boolean isEndOfSegment() { + return isSet(FLAG_END_SEGMENT); + } + + /** + * Determines whether the end-of-headers flag is set. + */ + public boolean isEndOfHeaders() { + return isSet(FLAG_END_HEADERS); + } + + /** + * Determines whether the flag is set indicating the presence of the priority field in a HEADERS + * frame. + */ + public boolean isPriorityPresent() { + return isSet(FLAG_PRIORITY); + } + + /** + * Determines whether the flag is set indicating that this frame is an ACK. + */ + public boolean isAck() { + return isSet(FLAG_ACK); + } + + /** + * For frames that include padding, indicates if the pad low field is present. + */ + public boolean isPadLowPresent() { + return isSet(FLAG_PAD_LOW); + } + + /** + * For frames that include padding, indicates if the pad high field is present. + */ + public boolean isPadHighPresent() { + return isSet(FLAG_PAD_HIGH); + } + + /** + * Indicates whether the padding flags are set properly. If pad high is set, pad low must also be + * set. + */ + public boolean isPaddingLengthValid() { + return isPadHighPresent() ? isPadLowPresent() : true; + } + + /** + * Gets the number of bytes expected in the padding length field of the payload. This is + * determined by the {@link #isPadHighPresent()} and {@link #isPadLowPresent()} flags. + */ + public int getNumPaddingLengthBytes() { + return (isPadHighPresent() ? 1 : 0) + (isPadLowPresent() ? 1 : 0); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + value; + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + Http2Flags other = (Http2Flags) obj; + if (value != other.value) { + return false; + } + return true; + } + + private boolean isSet(short mask) { + return (value & mask) != 0; + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2Frame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2Frame.java new file mode 100644 index 0000000000..f54dc8febe --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2Frame.java @@ -0,0 +1,22 @@ +/* + * Copyright 2014 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.draft10.frame; + +/** + * Marker interface for all HTTP2 frame types. + */ +public interface Http2Frame { +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameCodec.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameCodec.java new file mode 100644 index 0000000000..c347ed5d49 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameCodec.java @@ -0,0 +1,37 @@ +/* + * Copyright 2014 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.draft10.frame; + +import io.netty.channel.ChannelHandlerAppender; +import io.netty.handler.codec.http2.draft10.frame.decoder.Http2FrameDecoder; +import io.netty.handler.codec.http2.draft10.frame.decoder.Http2FrameUnmarshaller; +import io.netty.handler.codec.http2.draft10.frame.encoder.Http2FrameEncoder; +import io.netty.handler.codec.http2.draft10.frame.encoder.Http2FrameMarshaller; + +/** + * A combination of {@link Http2FrameEncoder} and {@link Http2FrameDecoder}. + */ +public class Http2FrameCodec extends ChannelHandlerAppender { + + public Http2FrameCodec(Http2FrameMarshaller frameMarshaller, + Http2FrameUnmarshaller frameUnmarshaller) { + super(new Http2FrameEncoder(frameMarshaller), new Http2FrameDecoder(frameUnmarshaller)); + } + + public Http2FrameCodec() { + super(new Http2FrameEncoder(), new Http2FrameDecoder()); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameCodecUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameCodecUtil.java new file mode 100644 index 0000000000..e840d3ff52 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameCodecUtil.java @@ -0,0 +1,125 @@ +/* + * Copyright 2014 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.draft10.frame; + +import io.netty.buffer.ByteBuf; + +/** + * Constants and utility method used for encoding/decoding HTTP2 frames. + */ +public final class Http2FrameCodecUtil { + public static final int CONNECTION_STREAM_ID = 0; + + public static final int DEFAULT_STREAM_PRIORITY = 0x40000000; // 2^30 + + public static final int MAX_FRAME_PAYLOAD_LENGTH = 16383; + public static final int PING_FRAME_PAYLOAD_LENGTH = 8; + public static final short MAX_UNSIGNED_BYTE = 0xFF; + public static final int MAX_UNSIGNED_SHORT = 0xFFFF; + public static final long MAX_UNSIGNED_INT = 0xFFFFFFFFL; + public static final int FRAME_HEADER_LENGTH = 8; + public static final int FRAME_LENGTH_MASK = 0x3FFF; + + public static final short FRAME_TYPE_DATA = 0x0; + public static final short FRAME_TYPE_HEADERS = 0x1; + public static final short FRAME_TYPE_PRIORITY = 0x2; + public static final short FRAME_TYPE_RST_STREAM = 0x3; + public static final short FRAME_TYPE_SETTINGS = 0x4; + public static final short FRAME_TYPE_PUSH_PROMISE = 0x5; + public static final short FRAME_TYPE_PING = 0x6; + public static final short FRAME_TYPE_GO_AWAY = 0x7; + public static final short FRAME_TYPE_WINDOW_UPDATE = 0x8; + public static final short FRAME_TYPE_CONTINUATION = 0x9; + + public static final short SETTINGS_HEADER_TABLE_SIZE = 1; + public static final short SETTINGS_ENABLE_PUSH = 2; + public static final short SETTINGS_MAX_CONCURRENT_STREAMS = 3; + public static final short SETTINGS_INITIAL_WINDOW_SIZE = 4; + + public static final short FLAG_END_STREAM = 0x1; + public static final short FLAG_END_SEGMENT = 0x2; + public static final short FLAG_END_HEADERS = 0x4; + public static final short FLAG_PRIORITY = 0x8; + public static final short FLAG_ACK = 0x1; + public static final short FLAG_PAD_LOW = 0x10; + public static final short FLAG_PAD_HIGH = 0x20; + + /** + * Reads a big-endian (31-bit) integer from the buffer. + */ + public static int readUnsignedInt(ByteBuf buf) { + int offset = buf.readerIndex(); + int value = (buf.getByte(offset + 0) & 0x7F) << 24 | (buf.getByte(offset + 1) & 0xFF) << 16 + | (buf.getByte(offset + 2) & 0xFF) << 8 | buf.getByte(offset + 3) & 0xFF; + buf.skipBytes(4); + return value; + } + + /** + * Writes a big-endian (32-bit) unsigned integer to the buffer. + */ + public static void writeUnsignedInt(long value, ByteBuf out) { + out.writeByte((int) ((value >> 24) & 0xFF)); + out.writeByte((int) ((value >> 16) & 0xFF)); + out.writeByte((int) ((value >> 8) & 0xFF)); + out.writeByte((int) ((value & 0xFF))); + } + + /** + * Reads the variable-length padding length field from the payload. + */ + public static int readPaddingLength(Http2Flags flags, ByteBuf payload) { + int paddingLength = 0; + if (flags.isPadHighPresent()) { + paddingLength += payload.readUnsignedByte() * 256; + } + if (flags.isPadLowPresent()) { + paddingLength += payload.readUnsignedByte(); + } + return paddingLength; + } + + /** + * Sets the padding flags in the given flags value as appropriate based on the padding length. + * Returns the new flags value after any padding flags have been set. + */ + public static short setPaddingFlags(short flags, int paddingLength) { + if (paddingLength > 255) { + flags |= Http2FrameCodecUtil.FLAG_PAD_HIGH; + } + if (paddingLength > 0) { + flags |= Http2FrameCodecUtil.FLAG_PAD_LOW; + } + return flags; + } + + /** + * Writes the padding length field to the output buffer. + */ + public static void writePaddingLength(int paddingLength, ByteBuf out) { + if (paddingLength > 255) { + int padHigh = paddingLength / 256; + out.writeByte(padHigh); + } + if (paddingLength > 0) { + int padLow = paddingLength % 256; + out.writeByte(padLow); + } + } + + private Http2FrameCodecUtil() { + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameHeader.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameHeader.java new file mode 100644 index 0000000000..a3cab38111 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameHeader.java @@ -0,0 +1,84 @@ +/* + * Copyright 2014 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.draft10.frame; + + +/** + * Encapsulates the content of an HTTP2 frame header. + */ +public final class Http2FrameHeader { + private final int payloadLength; + private final int type; + private final Http2Flags flags; + private final int streamId; + + private Http2FrameHeader(Builder builder) { + this.payloadLength = builder.payloadLength; + this.type = builder.type; + this.flags = builder.flags; + this.streamId = builder.streamId; + } + + public int getPayloadLength() { + return payloadLength; + } + + public int getType() { + return type; + } + + public Http2Flags getFlags() { + return flags; + } + + public int getStreamId() { + return streamId; + } + + /** + * Builds instances of {@link Http2FrameHeader}. + */ + public static class Builder { + private int payloadLength; + private int type; + private Http2Flags flags = new Http2Flags((short) 0); + private int streamId; + + public Builder setPayloadLength(int payloadLength) { + this.payloadLength = payloadLength; + return this; + } + + public Builder setType(int type) { + this.type = type; + return this; + } + + public Builder setFlags(Http2Flags flags) { + this.flags = flags; + return this; + } + + public Builder setStreamId(int streamId) { + this.streamId = streamId; + return this; + } + + public Http2FrameHeader build() { + return new Http2FrameHeader(this); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2GoAwayFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2GoAwayFrame.java new file mode 100644 index 0000000000..ecf39387f0 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2GoAwayFrame.java @@ -0,0 +1,60 @@ +/* + * Copyright 2014 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.draft10.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; + +/** + * An HTTP2 GO_AWAY frame indicating that the remote peer should stop creating streams for the + * connection. + */ +public interface Http2GoAwayFrame extends Http2Frame, ByteBufHolder { + /** + * The highest numbered stream identifier for which the sender of the GOAWAY frame has received + * frames on and might have taken some action on. + */ + int getLastStreamId(); + + /** + * The error code containing the reason for closing the connection. + */ + long getErrorCode(); + + /** + * Returns the debug data. + */ + @Override + ByteBuf content(); + + @Override + Http2GoAwayFrame copy(); + + @Override + Http2GoAwayFrame duplicate(); + + @Override + Http2GoAwayFrame retain(); + + @Override + Http2GoAwayFrame retain(int increment); + + @Override + Http2GoAwayFrame touch(); + + @Override + Http2GoAwayFrame touch(Object hint); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2HeadersFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2HeadersFrame.java new file mode 100644 index 0000000000..f50bb9ab23 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2HeadersFrame.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014 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.draft10.frame; + +import io.netty.handler.codec.http2.draft10.Http2Headers; + +/** + * The decoded form of a complete headers block for a HEADERS frame. + */ +public interface Http2HeadersFrame extends Http2StreamFrame { + + /** + * Gets the priority of the stream being created. + */ + int getPriority(); + + /** + * Gets the decoded HTTP headers. + */ + Http2Headers getHeaders(); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2PingFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2PingFrame.java new file mode 100644 index 0000000000..6ca439f0ca --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2PingFrame.java @@ -0,0 +1,53 @@ +/* + * Copyright 2014 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.draft10.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; + +/** + * An HTTP2 connection PING frame. + */ +public interface Http2PingFrame extends Http2Frame, ByteBufHolder { + /** + * Indicates whether this frame is an acknowledgment of a PING sent by the peer. + */ + boolean isAck(); + + /** + * Returns the opaque data of this frame. + */ + @Override + ByteBuf content(); + + @Override + Http2PingFrame copy(); + + @Override + Http2PingFrame duplicate(); + + @Override + Http2PingFrame retain(); + + @Override + Http2PingFrame retain(int increment); + + @Override + Http2PingFrame touch(); + + @Override + Http2PingFrame touch(Object hint); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2PriorityFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2PriorityFrame.java new file mode 100644 index 0000000000..e71a2f908f --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2PriorityFrame.java @@ -0,0 +1,26 @@ +/* + * Copyright 2014 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.draft10.frame; + +/** + * An HTTP2 priority frame indicating the sender-advised priority for the stream. + */ +public interface Http2PriorityFrame extends Http2StreamFrame { + /** + * The advised priority for the stream. + */ + int getPriority(); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2PushPromiseFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2PushPromiseFrame.java new file mode 100644 index 0000000000..9899d0dfef --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2PushPromiseFrame.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014 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.draft10.frame; + +import io.netty.handler.codec.http2.draft10.Http2Headers; + +/** + * A decoded form of the completed headers block for a PUSH_PROMISE frame. + */ +public interface Http2PushPromiseFrame extends Http2StreamFrame { + + /** + * The ID of the stream that the endpoint intends to start sending frames for. + */ + int getPromisedStreamId(); + + /** + * Gets the decoded HTTP headers. + */ + Http2Headers getHeaders(); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2RstStreamFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2RstStreamFrame.java new file mode 100644 index 0000000000..ea3e360200 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2RstStreamFrame.java @@ -0,0 +1,26 @@ +/* + * Copyright 2014 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.draft10.frame; + +/** + * HTTP2 RST_STREAM frame that indicates abnormal termination of a stream. + */ +public interface Http2RstStreamFrame extends Http2StreamFrame { + /** + * The error code containing the reason for the stream being terminated. + */ + long getErrorCode(); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2SettingsFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2SettingsFrame.java new file mode 100644 index 0000000000..f08c205de9 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2SettingsFrame.java @@ -0,0 +1,51 @@ +/* + * Copyright 2014 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.draft10.frame; + +/** + * HTTP2 SETTINGS frame providing configuration parameters that affect how endpoints communicate. + * + */ +public interface Http2SettingsFrame extends Http2Frame { + + /** + * Indicates whether this is an acknowledgment of the settings sent by the peer. + */ + boolean isAck(); + + /** + * Gets the sender's header compression table size, or {@code null} if not set. + */ + Integer getHeaderTableSize(); + + /** + * Gets whether or not the sender allows server push, or {@code null} if not set. + */ + Boolean getPushEnabled(); + + /** + * Gets the maximum number of streams the receiver is allowed to create, or {@code null} if not + * set. + */ + Long getMaxConcurrentStreams(); + + /** + * Gets the sender's initial flow control window in bytes, or {@code null} if not set. + * + * @return + */ + Integer getInitialWindowSize(); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2StreamFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2StreamFrame.java new file mode 100644 index 0000000000..49a7e381ff --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2StreamFrame.java @@ -0,0 +1,31 @@ +/* + * Copyright 2014 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.draft10.frame; + +/** + * Base interface for all frames that are associated to a stream. + */ +public interface Http2StreamFrame extends Http2Frame { + /** + * Gets the identifier of the associated stream. + */ + int getStreamId(); + + /** + * Indicates whether this frame represents the last frame for the stream. + */ + boolean isEndOfStream(); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2WindowUpdateFrame.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2WindowUpdateFrame.java new file mode 100644 index 0000000000..c2bcca466c --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/Http2WindowUpdateFrame.java @@ -0,0 +1,27 @@ +/* + * Copyright 2014 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.draft10.frame; + +/** + * HTTP2 WINDOW_UPDATE frame used to implement flow control. + */ +public interface Http2WindowUpdateFrame extends Http2StreamFrame { + /** + * Gets the number of bytes that the sender can transmit in addition to the existing flow control + * window. + */ + int getWindowSizeIncrement(); +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/AbstractHeadersUnmarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/AbstractHeadersUnmarshaller.java new file mode 100644 index 0000000000..5091a50280 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/AbstractHeadersUnmarshaller.java @@ -0,0 +1,125 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_CONTINUATION; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_FRAME_PAYLOAD_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.readPaddingLength; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.Http2Flags; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +public abstract class AbstractHeadersUnmarshaller extends AbstractHttp2FrameUnmarshaller { + + /** + * A builder for a headers/push_promise frame. + */ + protected abstract class FrameBuilder { + protected ByteBuf headerBlock; + + abstract int getStreamId(); + + final void addHeaderFragment(ByteBuf fragment, ByteBufAllocator alloc) { + if (headerBlock == null) { + headerBlock = alloc.buffer(fragment.readableBytes()); + headerBlock.writeBytes(fragment); + } else { + ByteBuf buf = alloc.buffer(headerBlock.readableBytes() + fragment.readableBytes()); + buf.writeBytes(headerBlock); + buf.writeBytes(fragment); + headerBlock.release(); + headerBlock = buf; + } + } + + abstract Http2Frame buildFrame() throws Http2Exception; + } + + private FrameBuilder frameBuilder; + + @Override + protected final void validate(Http2FrameHeader frameHeader) throws Http2Exception { + if (frameBuilder == null) { + // This frame is the beginning of a headers/push_promise. + validateStartOfHeaderBlock(frameHeader); + return; + } + + // Validate the continuation of a headers block. + if (frameHeader.getType() != FRAME_TYPE_CONTINUATION) { + throw protocolError("Unsupported frame type: %d.", frameHeader.getType()); + } + if (frameBuilder.getStreamId() != frameHeader.getStreamId()) { + throw protocolError("Continuation received for wrong stream. Expected %d, found %d", + frameBuilder.getStreamId(), frameHeader.getStreamId()); + } + Http2Flags flags = frameHeader.getFlags(); + if (!flags.isPaddingLengthValid()) { + throw protocolError("Pad high is set but pad low is not"); + } + if (frameHeader.getPayloadLength() < flags.getNumPaddingLengthBytes()) { + throw protocolError("Frame length %d to small.", frameHeader.getPayloadLength()); + } + if (frameHeader.getPayloadLength() > MAX_FRAME_PAYLOAD_LENGTH) { + throw protocolError("Frame length %d too big.", frameHeader.getPayloadLength()); + } + } + + @Override + protected final Http2Frame doUnmarshall(Http2FrameHeader header, ByteBuf payload, + ByteBufAllocator alloc) throws Http2Exception { + Http2Flags flags = header.getFlags(); + if (frameBuilder == null) { + // This is the start of a headers/push_promise frame. Delegate to the subclass to create + // the appropriate builder for the frame. + frameBuilder = createFrameBuilder(header, payload, alloc); + } else { + // Processing a continuation frame for a headers/push_promise. Update the current frame + // builder with the new fragment. + + int paddingLength = readPaddingLength(flags, payload); + + // Determine how much data there is to read by removing the trailing + // padding. + int dataLength = payload.readableBytes() - paddingLength; + if (dataLength < 0) { + throw protocolError("Payload too small for padding."); + } + + // The remainder of this frame is the headers block. + frameBuilder.addHeaderFragment(payload, alloc); + } + + // If the headers are complete, build the frame. + Http2Frame frame = null; + if (flags.isEndOfHeaders()) { + frame = frameBuilder.buildFrame(); + frameBuilder = null; + } + + return frame; + } + + protected abstract void validateStartOfHeaderBlock(Http2FrameHeader frameHeader) + throws Http2Exception; + + protected abstract FrameBuilder createFrameBuilder(Http2FrameHeader header, ByteBuf payload, + ByteBufAllocator alloc) throws Http2Exception; +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/AbstractHttp2FrameUnmarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/AbstractHttp2FrameUnmarshaller.java new file mode 100644 index 0000000000..9280b3b14a --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/AbstractHttp2FrameUnmarshaller.java @@ -0,0 +1,66 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +/** + * Abstract base class for all {@link Http2FrameUnmarshaller} classes. + */ +public abstract class AbstractHttp2FrameUnmarshaller implements Http2FrameUnmarshaller { + private Http2FrameHeader header; + + @Override + public final Http2FrameUnmarshaller unmarshall(Http2FrameHeader header) throws Http2Exception { + if (header == null) { + throw new IllegalArgumentException("header must be non-null."); + } + + validate(header); + this.header = header; + return this; + } + + @Override + public final Http2Frame from(ByteBuf payload, ByteBufAllocator alloc) throws Http2Exception { + if (header == null) { + throw new IllegalStateException("header must be set before calling from()."); + } + + return doUnmarshall(header, payload, alloc); + } + + /** + * Verifies that the given frame header is valid for the frame type(s) supported by this decoder. + */ + protected abstract void validate(Http2FrameHeader frameHeader) throws Http2Exception; + + /** + * Unmarshalls the frame. + * + * @param header the frame header + * @param payload the payload of the frame. + * @param alloc an allocator for new buffers + * @return the frame + * @throws Http2Exception thrown if any protocol error was encountered. + */ + protected abstract Http2Frame doUnmarshall(Http2FrameHeader header, ByteBuf payload, + ByteBufAllocator alloc) throws Http2Exception; +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/DefaultHttp2HeadersDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/DefaultHttp2HeadersDecoder.java new file mode 100644 index 0000000000..386e524cd3 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/DefaultHttp2HeadersDecoder.java @@ -0,0 +1,63 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import static io.netty.handler.codec.http2.draft10.connection.Http2ConnectionUtil.DEFAULT_HEADER_TABLE_SIZE; +import static io.netty.handler.codec.http2.draft10.connection.Http2ConnectionUtil.DEFAULT_MAX_HEADER_SIZE; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufInputStream; +import io.netty.handler.codec.http2.draft10.Http2Error; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.Http2Headers; + +import java.io.IOException; + +import com.twitter.hpack.Decoder; +import com.twitter.hpack.HeaderListener; + +public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder { + + private final Decoder decoder; + + public DefaultHttp2HeadersDecoder() { + this.decoder = new Decoder(DEFAULT_MAX_HEADER_SIZE, DEFAULT_HEADER_TABLE_SIZE); + } + + @Override + public void setHeaderTableSize(int size) throws Http2Exception { + // TODO: can we throw away the decoder and create a new one? + } + + @Override + public Http2Headers decodeHeaders(ByteBuf headerBlock) throws Http2Exception { + try { + final Http2Headers.Builder headersBuilder = new Http2Headers.Builder(); + HeaderListener listener = new HeaderListener() { + @Override + public void emitHeader(byte[] key, byte[] value) { + headersBuilder.addHeader(key, value); + } + }; + + decoder.decode(new ByteBufInputStream(headerBlock), listener); + decoder.endHeaderBlock(listener); + + return headersBuilder.build(); + } catch (IOException e) { + throw new Http2Exception(Http2Error.COMPRESSION_ERROR, e.getMessage()); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2DataFrameUnmarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2DataFrameUnmarshaller.java new file mode 100644 index 0000000000..f72cc4f99c --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2DataFrameUnmarshaller.java @@ -0,0 +1,86 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_DATA; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_FRAME_PAYLOAD_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.readPaddingLength; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2DataFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Flags; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +/** + * An unmarshaller for {@link Http2DataFrame} instances. The buffer contained in the frames is a + * slice of the original input buffer. If the frame needs to be persisted it should be copied. + */ +public class Http2DataFrameUnmarshaller extends AbstractHttp2FrameUnmarshaller { + + @Override + protected void validate(Http2FrameHeader frameHeader) throws Http2Exception { + if (frameHeader.getType() != FRAME_TYPE_DATA) { + throw protocolError("Unsupported frame type: %d.", frameHeader.getType()); + } + if (frameHeader.getStreamId() <= 0) { + throw protocolError("A stream ID must be > 0."); + } + Http2Flags flags = frameHeader.getFlags(); + if (!flags.isPaddingLengthValid()) { + throw protocolError("Pad high is set but pad low is not"); + } + if (frameHeader.getPayloadLength() < flags.getNumPaddingLengthBytes()) { + throw protocolError("Frame length %d too small.", frameHeader.getPayloadLength()); + } + if (frameHeader.getPayloadLength() > MAX_FRAME_PAYLOAD_LENGTH) { + throw protocolError("Frame length %d too big.", frameHeader.getPayloadLength()); + } + } + + @Override + protected Http2Frame doUnmarshall(Http2FrameHeader header, ByteBuf payload, + ByteBufAllocator alloc) throws Http2Exception { + DefaultHttp2DataFrame.Builder builder = new DefaultHttp2DataFrame.Builder(); + builder.setStreamId(header.getStreamId()); + + Http2Flags flags = header.getFlags(); + builder.setEndOfStream(flags.isEndOfStream()); + + // Read the padding length. + int paddingLength = readPaddingLength(flags, payload); + builder.setPaddingLength(paddingLength); + + // Determine how much data there is to read by removing the trailing + // padding. + int dataLength = payload.readableBytes() - paddingLength; + if (dataLength < 0) { + throw protocolError("Frame payload too small for padding."); + } + + // Copy the remaining data into the frame. + ByteBuf data = payload.slice(payload.readerIndex(), dataLength).retain(); + builder.setContent(data); + + // Skip the rest of the bytes in the payload. + payload.skipBytes(payload.readableBytes()); + + return builder.build(); + } + +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2FrameDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2FrameDecoder.java new file mode 100644 index 0000000000..199c8b9bed --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2FrameDecoder.java @@ -0,0 +1,137 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_HEADER_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_LENGTH_MASK; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.readUnsignedInt; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.ByteToMessageDecoder; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.Http2Flags; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +import java.util.List; + +/** + * Decodes {@link Http2Frame} objects from an input {@link ByteBuf}. The frames that this handler + * emits can be configured by providing a {@link Http2FrameUnmarshaller}. By default, the + * {@link Http2StandardFrameUnmarshaller} is used to handle all frame types - see the documentation + * for details. + * + * @see Http2StandardFrameUnmarshaller + */ +public class Http2FrameDecoder extends ByteToMessageDecoder { + + private enum State { + FRAME_HEADER, + FRAME_PAYLOAD, + ERROR + } + + private State state; + private Http2FrameUnmarshaller frameUnmarshaller; + private int payloadLength; + + public Http2FrameDecoder() { + this(new Http2StandardFrameUnmarshaller()); + } + + public Http2FrameDecoder(Http2FrameUnmarshaller frameUnmarshaller) { + this.frameUnmarshaller = frameUnmarshaller; + this.state = State.FRAME_HEADER; + } + + @Override + protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { + try { + switch (state) { + case FRAME_HEADER: + processFrameHeader(in); + if (state == State.FRAME_HEADER) { + // Still haven't read the entire frame header yet. + break; + } + + // If we successfully read the entire frame header, drop down and start processing + // the payload now. + + case FRAME_PAYLOAD: + processFramePayload(ctx, in, out); + break; + case ERROR: + in.skipBytes(in.readableBytes()); + break; + default: + throw new IllegalStateException("Should never get here"); + } + } catch (Throwable t) { + ctx.fireExceptionCaught(t); + state = State.ERROR; + } + } + + private void processFrameHeader(ByteBuf in) throws Http2Exception { + if (in.readableBytes() < FRAME_HEADER_LENGTH) { + // Wait until the entire frame header has been read. + return; + } + + // Read the header and prepare the unmarshaller to read the frame. + Http2FrameHeader frameHeader = readFrameHeader(in); + payloadLength = frameHeader.getPayloadLength(); + frameUnmarshaller.unmarshall(frameHeader); + + // Start reading the payload for the frame. + state = State.FRAME_PAYLOAD; + } + + private void processFramePayload(ChannelHandlerContext ctx, ByteBuf in, List out) + throws Http2Exception { + if (in.readableBytes() < payloadLength) { + // Wait until the entire payload has been read. + return; + } + + // Get a view of the buffer for the size of the payload. + ByteBuf payload = in.readSlice(payloadLength); + + // Create the frame and add it to the output. + Http2Frame frame = frameUnmarshaller.from(payload, ctx.alloc()); + if (frame != null) { + out.add(frame); + } + + // Go back to reading the next frame header. + state = State.FRAME_HEADER; + } + + /** + * Reads the frame header from the input buffer and creates an envelope initialized with those + * values. + */ + private static Http2FrameHeader readFrameHeader(ByteBuf in) { + int payloadLength = in.readUnsignedShort() & FRAME_LENGTH_MASK; + short type = in.readUnsignedByte(); + short flags = in.readUnsignedByte(); + int streamId = readUnsignedInt(in); + + return new Http2FrameHeader.Builder().setPayloadLength(payloadLength).setType(type) + .setFlags(new Http2Flags(flags)).setStreamId(streamId).build(); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2FrameUnmarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2FrameUnmarshaller.java new file mode 100644 index 0000000000..898c69afd5 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2FrameUnmarshaller.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +/** + * Used by the {@link Http2FrameDecoder} to unmarshall {@link Http2Frame} objects from an input + * {@link ByteBuf}. + */ +public interface Http2FrameUnmarshaller { + + /** + * Prepares the unmarshaller for the next frame. + * + * @param header the header providing the detais of the frame to be unmarshalled. + * @return this unmarshaller + * @throws Http2Exception thrown if any of the information of the header violates the protocol. + */ + Http2FrameUnmarshaller unmarshall(Http2FrameHeader header) throws Http2Exception; + + /** + * Unmarshalls the frame from the payload. + * + * @param payload the payload from which the frame is to be unmarshalled. + * @param alloc the allocator for any new buffers required by the unmarshaller. + * @return the frame or {@code null} if the unmarshall operation is processing is incomplete and + * requires additional data. + * @throws Http2Exception thrown if any protocol error was encountered while unmarshalling the + * frame. + */ + Http2Frame from(ByteBuf payload, ByteBufAllocator alloc) throws Http2Exception; +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2GoAwayFrameUnmarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2GoAwayFrameUnmarshaller.java new file mode 100644 index 0000000000..1e4dda4037 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2GoAwayFrameUnmarshaller.java @@ -0,0 +1,68 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_GO_AWAY; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_FRAME_PAYLOAD_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.readUnsignedInt; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2GoAwayFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +/** + * An unmarshaller for {@link Http2GoAwayFrame} instances. The buffer contained in the frames is a + * slice of the original input buffer. If the frame needs to be persisted it should be copied. + */ +public class Http2GoAwayFrameUnmarshaller extends AbstractHttp2FrameUnmarshaller { + + @Override + protected void validate(Http2FrameHeader frameHeader) throws Http2Exception { + if (frameHeader.getType() != FRAME_TYPE_GO_AWAY) { + throw protocolError("Unsupported frame type: %d.", frameHeader.getType()); + } + if (frameHeader.getStreamId() != 0) { + throw protocolError("A stream ID must be zero."); + } + if (frameHeader.getPayloadLength() < 8) { + throw protocolError("Frame length %d too small.", frameHeader.getPayloadLength()); + } + if (frameHeader.getPayloadLength() > MAX_FRAME_PAYLOAD_LENGTH) { + throw protocolError("Frame length %d too big.", frameHeader.getPayloadLength()); + } + } + + @Override + protected Http2Frame doUnmarshall(Http2FrameHeader header, ByteBuf payload, + ByteBufAllocator alloc) throws Http2Exception { + DefaultHttp2GoAwayFrame.Builder builder = new DefaultHttp2GoAwayFrame.Builder(); + + int lastStreamId = readUnsignedInt(payload); + builder.setLastStreamId(lastStreamId); + + long errorCode = payload.readUnsignedInt(); + builder.setErrorCode(errorCode); + + // The remainder of this frame is the debug data. + ByteBuf data = payload.slice().retain(); + builder.setDebugData(data); + + return builder.build(); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2HeadersDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2HeadersDecoder.java new file mode 100644 index 0000000000..3618284a53 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2HeadersDecoder.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.Http2Headers; + +/** + * Decodes HPACK-encoded headers blocks into {@link Http2Headers}. + */ +public interface Http2HeadersDecoder { + + /** + * Decodes the given headers block and returns the headers. + */ + Http2Headers decodeHeaders(ByteBuf headerBlock) throws Http2Exception; + + /** + * Sets the new max header table size for this decoder. + */ + void setHeaderTableSize(int size) throws Http2Exception; +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2HeadersFrameUnmarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2HeadersFrameUnmarshaller.java new file mode 100644 index 0000000000..87898e280d --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2HeadersFrameUnmarshaller.java @@ -0,0 +1,149 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_HEADERS; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_FRAME_PAYLOAD_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.readPaddingLength; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.readUnsignedInt; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.Http2Headers; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Flags; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +import com.google.common.base.Preconditions; + +/** + * An unmarshaller for {@link Http2HeadersFrame} instances. + */ +public class Http2HeadersFrameUnmarshaller extends AbstractHeadersUnmarshaller { + + private final Http2HeadersDecoder headersDecoder; + + public Http2HeadersFrameUnmarshaller(Http2HeadersDecoder headersDecoder) { + this.headersDecoder = Preconditions.checkNotNull(headersDecoder, "headersDecoder"); + } + + @Override + protected void validateStartOfHeaderBlock(Http2FrameHeader frameHeader) throws Http2Exception { + if (frameHeader.getType() != FRAME_TYPE_HEADERS) { + throw protocolError("Unsupported frame type: %d.", frameHeader.getType()); + } + + if (frameHeader.getStreamId() <= 0) { + throw protocolError("A stream ID must > 0."); + } + + Http2Flags flags = frameHeader.getFlags(); + if (flags.isPriorityPresent() && frameHeader.getPayloadLength() < 4) { + throw protocolError("Frame length too small." + frameHeader.getPayloadLength()); + } + + if (!flags.isPaddingLengthValid()) { + throw protocolError("Pad high is set but pad low is not"); + } + + if (frameHeader.getPayloadLength() < flags.getNumPaddingLengthBytes()) { + throw protocolError("Frame length %d too small for padding.", frameHeader.getPayloadLength()); + } + + if (frameHeader.getPayloadLength() > MAX_FRAME_PAYLOAD_LENGTH) { + throw protocolError("Frame length %d too big.", frameHeader.getPayloadLength()); + } + } + + @Override + protected FrameBuilder createFrameBuilder(final Http2FrameHeader header, final ByteBuf payload, + ByteBufAllocator alloc) throws Http2Exception { + try { + final DefaultHttp2HeadersFrame.Builder builder = new DefaultHttp2HeadersFrame.Builder(); + builder.setStreamId(header.getStreamId()); + + Http2Flags flags = header.getFlags(); + builder.setEndOfStream(flags.isEndOfStream()); + + // Read the padding length. + int paddingLength = readPaddingLength(flags, payload); + + // Read the priority if it was included in the frame. + if (flags.isPriorityPresent()) { + int priority = readUnsignedInt(payload); + builder.setPriority(priority); + } + + // Determine how much data there is to read by removing the trailing + // padding. + int dataLength = payload.readableBytes() - paddingLength; + if (dataLength < 0) { + throw protocolError("Payload too small for padding"); + } + + // Get a view of the header block portion of the payload. + final ByteBuf headerSlice = payload.readSlice(dataLength); + + // The remainder of this frame is the headers block. + if (flags.isEndOfHeaders()) { + // Optimization: don't copy the buffer if we have the entire headers block. + return new FrameBuilder() { + @Override + int getStreamId() { + return header.getStreamId(); + } + + @Override + Http2Frame buildFrame() throws Http2Exception { + Http2Headers headers = headersDecoder.decodeHeaders(headerSlice); + builder.setHeaders(headers); + return builder.build(); + } + }; + } + + // The header block is not complete. Await one or more continuation frames + // to complete the block before decoding. + FrameBuilder frameBuilder = new FrameBuilder() { + @Override + int getStreamId() { + return header.getStreamId(); + } + + @Override + Http2Frame buildFrame() throws Http2Exception { + try { + Http2Headers headers = headersDecoder.decodeHeaders(headerBlock); + builder.setHeaders(headers); + return builder.build(); + } finally { + headerBlock.release(); + headerBlock = null; + } + } + }; + + // Copy and add the initial fragment of the header block. + frameBuilder.addHeaderFragment(headerSlice, alloc); + + return frameBuilder; + } finally { + payload.skipBytes(payload.readableBytes()); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2PingFrameUnmarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2PingFrameUnmarshaller.java new file mode 100644 index 0000000000..405d17e057 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2PingFrameUnmarshaller.java @@ -0,0 +1,60 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_PING; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.PING_FRAME_PAYLOAD_LENGTH; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2PingFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +/** + * An unmarshaller for {@link Http2PingFrame} instances. The buffer contained in the frames is a + * slice of the original input buffer. If the frame needs to be persisted it should be copied. + */ +public class Http2PingFrameUnmarshaller extends AbstractHttp2FrameUnmarshaller { + + @Override + protected void validate(Http2FrameHeader frameHeader) throws Http2Exception { + if (frameHeader.getType() != FRAME_TYPE_PING) { + throw protocolError("Unsupported frame type: %d.", frameHeader.getType()); + } + if (frameHeader.getStreamId() != 0) { + throw protocolError("A stream ID must be zero."); + } + if (frameHeader.getPayloadLength() != PING_FRAME_PAYLOAD_LENGTH) { + throw protocolError("Frame length %d incorrect size for ping.", + frameHeader.getPayloadLength()); + } + } + + @Override + protected Http2Frame doUnmarshall(Http2FrameHeader header, ByteBuf payload, + ByteBufAllocator alloc) throws Http2Exception { + DefaultHttp2PingFrame.Builder builder = new DefaultHttp2PingFrame.Builder(); + builder.setAck(header.getFlags().isAck()); + + // The remainder of this frame is the opaque data. + ByteBuf data = payload.slice().retain(); + builder.setData(data); + + return builder.build(); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2PriorityFrameUnmarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2PriorityFrameUnmarshaller.java new file mode 100644 index 0000000000..2b1c757f71 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2PriorityFrameUnmarshaller.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_PRIORITY; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_FRAME_PAYLOAD_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.readUnsignedInt; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2PriorityFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +/** + * An unmarshaller for {@link Http2PriorityFrame} instances. + */ +public class Http2PriorityFrameUnmarshaller extends AbstractHttp2FrameUnmarshaller { + + @Override + protected void validate(Http2FrameHeader frameHeader) throws Http2Exception { + if (frameHeader.getType() != FRAME_TYPE_PRIORITY) { + throw protocolError("Unsupported frame type: %d.", frameHeader.getType()); + } + if (frameHeader.getStreamId() <= 0) { + throw protocolError("A stream ID must be > 0."); + } + if (frameHeader.getPayloadLength() < 4) { + throw protocolError("Frame length %d too small.", frameHeader.getPayloadLength()); + } + if (frameHeader.getPayloadLength() > MAX_FRAME_PAYLOAD_LENGTH) { + throw protocolError("Frame length %d too big.", frameHeader.getPayloadLength()); + } + } + + @Override + protected Http2Frame doUnmarshall(Http2FrameHeader header, ByteBuf payload, + ByteBufAllocator alloc) throws Http2Exception { + DefaultHttp2PriorityFrame.Builder builder = new DefaultHttp2PriorityFrame.Builder(); + builder.setStreamId(header.getStreamId()); + + int priority = readUnsignedInt(payload); + builder.setPriority(priority); + + return builder.build(); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2PushPromiseFrameUnmarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2PushPromiseFrameUnmarshaller.java new file mode 100644 index 0000000000..63ccd993a4 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2PushPromiseFrameUnmarshaller.java @@ -0,0 +1,116 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_PUSH_PROMISE; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_FRAME_PAYLOAD_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.readUnsignedInt; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.Http2Headers; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2PushPromiseFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Flags; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +import com.google.common.base.Preconditions; + +/** + * An unmarshaller for {@link Http2PushPromiseFrame} instances. + */ +public class Http2PushPromiseFrameUnmarshaller extends AbstractHeadersUnmarshaller { + + private final Http2HeadersDecoder headersDecoder; + + public Http2PushPromiseFrameUnmarshaller(Http2HeadersDecoder headersDecoder) { + this.headersDecoder = Preconditions.checkNotNull(headersDecoder, "headersDecoder"); + } + + @Override + protected void validateStartOfHeaderBlock(Http2FrameHeader frameHeader) throws Http2Exception { + if (frameHeader.getType() != FRAME_TYPE_PUSH_PROMISE) { + throw protocolError("Unsupported frame type: %d.", frameHeader.getType()); + } + if (frameHeader.getStreamId() <= 0) { + throw protocolError("A stream ID must > 0."); + } + if (frameHeader.getPayloadLength() < 4) { + throw protocolError("Frame length too small." + frameHeader.getPayloadLength()); + } + if (frameHeader.getPayloadLength() > MAX_FRAME_PAYLOAD_LENGTH) { + throw protocolError("Frame length %d too big.", frameHeader.getPayloadLength()); + } + } + + @Override + protected FrameBuilder createFrameBuilder(final Http2FrameHeader header, ByteBuf payload, + ByteBufAllocator alloc) throws Http2Exception { + final DefaultHttp2PushPromiseFrame.Builder builder = new DefaultHttp2PushPromiseFrame.Builder(); + builder.setStreamId(header.getStreamId()); + + int promisedStreamId = readUnsignedInt(payload); + builder.setPromisedStreamId(promisedStreamId); + + final ByteBuf headerSlice = payload.readSlice(payload.readableBytes()); + + // The remainder of this frame is the headers block. + Http2Flags flags = header.getFlags(); + if (flags.isEndOfHeaders()) { + // Optimization: don't copy the buffer if we have the entire headers block. + return new FrameBuilder() { + @Override + int getStreamId() { + return header.getStreamId(); + } + + @Override + Http2Frame buildFrame() throws Http2Exception { + Http2Headers headers = headersDecoder.decodeHeaders(headerSlice); + builder.setHeaders(headers); + return builder.build(); + } + }; + } + + // The header block is not complete. Await one or more continuation frames + // to complete the block before decoding. + FrameBuilder frameBuilder = new FrameBuilder() { + @Override + int getStreamId() { + return header.getStreamId(); + } + + @Override + Http2Frame buildFrame() throws Http2Exception { + try { + Http2Headers headers = headersDecoder.decodeHeaders(headerBlock); + builder.setHeaders(headers); + return builder.build(); + } finally { + headerBlock.release(); + headerBlock = null; + } + } + }; + + // Copy and add the initial fragment of the header block. + frameBuilder.addHeaderFragment(headerSlice, alloc); + + return frameBuilder; + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2RstStreamFrameUnmarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2RstStreamFrameUnmarshaller.java new file mode 100644 index 0000000000..b0630a21cf --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2RstStreamFrameUnmarshaller.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_RST_STREAM; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_FRAME_PAYLOAD_LENGTH; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2RstStreamFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +/** + * An unmarshaller for {@link Http2RstStreamFrame} instances. + */ +public class Http2RstStreamFrameUnmarshaller extends AbstractHttp2FrameUnmarshaller { + + @Override + protected void validate(Http2FrameHeader frameHeader) throws Http2Exception { + if (frameHeader.getType() != FRAME_TYPE_RST_STREAM) { + throw protocolError("Unsupported frame type: %d.", frameHeader.getType()); + } + if (frameHeader.getStreamId() <= 0) { + throw protocolError("A stream ID must be > 0."); + } + if (frameHeader.getPayloadLength() < 4) { + throw protocolError("Frame length %d too small.", frameHeader.getPayloadLength()); + } + if (frameHeader.getPayloadLength() > MAX_FRAME_PAYLOAD_LENGTH) { + throw protocolError("Frame length %d too big.", frameHeader.getPayloadLength()); + } + } + + @Override + protected Http2Frame doUnmarshall(Http2FrameHeader header, ByteBuf payload, + ByteBufAllocator alloc) throws Http2Exception { + DefaultHttp2RstStreamFrame.Builder builder = new DefaultHttp2RstStreamFrame.Builder(); + builder.setStreamId(header.getStreamId()); + + long errorCode = payload.readUnsignedInt(); + builder.setErrorCode(errorCode); + + return builder.build(); + } + +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2SettingsFrameUnmarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2SettingsFrameUnmarshaller.java new file mode 100644 index 0000000000..f87db6dcb9 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2SettingsFrameUnmarshaller.java @@ -0,0 +1,98 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_SETTINGS; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_FRAME_PAYLOAD_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.SETTINGS_ENABLE_PUSH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.SETTINGS_HEADER_TABLE_SIZE; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.SETTINGS_INITIAL_WINDOW_SIZE; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.SETTINGS_MAX_CONCURRENT_STREAMS; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2SettingsFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +/** + * An unmarshaller for {@link Http2SettingsFrame} instances. + */ +public class Http2SettingsFrameUnmarshaller extends AbstractHttp2FrameUnmarshaller { + + @Override + protected void validate(Http2FrameHeader frameHeader) throws Http2Exception { + if (frameHeader.getType() != FRAME_TYPE_SETTINGS) { + throw protocolError("Unsupported frame type: %d.", frameHeader.getType()); + } + if (frameHeader.getStreamId() != 0) { + throw protocolError("A stream ID must be zero."); + } + if (frameHeader.getFlags().isAck() && frameHeader.getPayloadLength() > 0) { + throw protocolError("Ack settings frame must have an empty payload."); + } + if (frameHeader.getPayloadLength() % 5 > 0) { + throw protocolError("Frame length %d invalid.", frameHeader.getPayloadLength()); + } + if (frameHeader.getPayloadLength() > MAX_FRAME_PAYLOAD_LENGTH) { + throw protocolError("Frame length %d too big.", frameHeader.getPayloadLength()); + } + } + + @Override + protected Http2Frame doUnmarshall(Http2FrameHeader header, ByteBuf payload, + ByteBufAllocator alloc) throws Http2Exception { + DefaultHttp2SettingsFrame.Builder builder = new DefaultHttp2SettingsFrame.Builder(); + builder.setAck(header.getFlags().isAck()); + + int numSettings = header.getPayloadLength() / 5; + for (int index = 0; index < numSettings; ++index) { + short id = payload.readUnsignedByte(); + long value = payload.readUnsignedInt(); + switch (id) { + case SETTINGS_HEADER_TABLE_SIZE: + if (value <= 0L || value > Integer.MAX_VALUE) { + throw protocolError("Invalid header table size setting: %d", value); + } + builder.setHeaderTableSize((int) value); + break; + case SETTINGS_ENABLE_PUSH: + if (value != 0L && value != 1L) { + throw protocolError("Invalid enable push setting: %d", value); + } + builder.setPushEnabled(value == 1); + break; + case SETTINGS_MAX_CONCURRENT_STREAMS: + if (value < 0L) { + throw protocolError("Invalid max concurrent streams setting: %d", value); + } + builder.setMaxConcurrentStreams(value); + break; + case SETTINGS_INITIAL_WINDOW_SIZE: + if (value < 0L || value > Integer.MAX_VALUE) { + throw protocolError("Invalid initial window size setting: %d", value); + } + builder.setInitialWindowSize((int) value); + break; + default: + throw protocolError("Unsupported setting: %d", id); + } + } + + return builder.build(); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2StandardFrameUnmarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2StandardFrameUnmarshaller.java new file mode 100644 index 0000000000..a92846e8ae --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2StandardFrameUnmarshaller.java @@ -0,0 +1,115 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_CONTINUATION; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_DATA; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_GO_AWAY; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_HEADERS; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_PING; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_PRIORITY; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_PUSH_PROMISE; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_RST_STREAM; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_SETTINGS; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_WINDOW_UPDATE; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +/** + * A composite {@link Http2FrameUnmarshaller} that supports all frames identified by the HTTP2 spec. + * This unmarshalls the following frames: + *

+ * {@link Http2DataFrame} (buffer is a slice of input buffer - must be copied if persisted)
+ * {@link Http2GoAwayFrame} (buffer is a slice of input buffer - must be copied if persisted)
+ * {@link Http2HeadersFrame}
+ * {@link Http2PingFrame} (buffer is a slice of input buffer - must be copied if persisted)
+ * {@link Http2PriorityFrame}
+ * {@link Http2PushPromiseFrame}
+ * {@link Http2RstStreamFrame}
+ * {@link Http2SettingsFrame}
+ * {@link Http2WindowUpdateFrame}
+ */ +public class Http2StandardFrameUnmarshaller implements Http2FrameUnmarshaller { + + private final Http2FrameUnmarshaller[] unmarshallers; + private Http2FrameUnmarshaller activeUnmarshaller; + + public Http2StandardFrameUnmarshaller() { + this(new DefaultHttp2HeadersDecoder()); + } + + public Http2StandardFrameUnmarshaller(Http2HeadersDecoder headersDecoder) { + unmarshallers = new Http2FrameUnmarshaller[FRAME_TYPE_CONTINUATION + 1]; + unmarshallers[FRAME_TYPE_DATA] = new Http2DataFrameUnmarshaller(); + unmarshallers[FRAME_TYPE_HEADERS] = new Http2HeadersFrameUnmarshaller(headersDecoder); + unmarshallers[FRAME_TYPE_PRIORITY] = new Http2PriorityFrameUnmarshaller(); + unmarshallers[FRAME_TYPE_RST_STREAM] = new Http2RstStreamFrameUnmarshaller(); + unmarshallers[FRAME_TYPE_SETTINGS] = new Http2SettingsFrameUnmarshaller(); + unmarshallers[FRAME_TYPE_PUSH_PROMISE] = new Http2PushPromiseFrameUnmarshaller(headersDecoder); + unmarshallers[FRAME_TYPE_PING] = new Http2PingFrameUnmarshaller(); + unmarshallers[FRAME_TYPE_GO_AWAY] = new Http2GoAwayFrameUnmarshaller(); + unmarshallers[FRAME_TYPE_WINDOW_UPDATE] = new Http2WindowUpdateFrameUnmarshaller(); + unmarshallers[FRAME_TYPE_CONTINUATION] = new Http2FrameUnmarshaller() { + private String msg = "Received continuation without headers or push_promise"; + + @Override + public Http2FrameUnmarshaller unmarshall(Http2FrameHeader header) throws Http2Exception { + throw protocolError(msg); + } + + @Override + public Http2Frame from(ByteBuf payload, ByteBufAllocator alloc) throws Http2Exception { + throw protocolError(msg); + } + }; + } + + @Override + public Http2FrameUnmarshaller unmarshall(Http2FrameHeader header) throws Http2Exception { + // If we're not in the middle of unmarshalling a continued frame (e.g. headers, + // push_promise), select the appropriate marshaller for the frame type. + if (activeUnmarshaller == null) { + int type = header.getType(); + if (type < 0 || type >= unmarshallers.length || unmarshallers[type] == null) { + throw protocolError("Unsupported frame type: %d", type); + } + + activeUnmarshaller = unmarshallers[type]; + } + + // Prepare the unmarshaller. + activeUnmarshaller.unmarshall(header); + return this; + } + + @Override + public Http2Frame from(ByteBuf payload, ByteBufAllocator alloc) throws Http2Exception { + if (activeUnmarshaller == null) { + throw new IllegalStateException("Must call unmarshall() before calling from()."); + } + Http2Frame frame = activeUnmarshaller.from(payload, alloc); + if (frame != null) { + // The unmarshall is complete and does not require more frames. Clear the active + // marshaller so that we select a fresh marshaller next time. + activeUnmarshaller = null; + } + return frame; + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2WindowUpdateFrameUnmarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2WindowUpdateFrameUnmarshaller.java new file mode 100644 index 0000000000..11cea955a4 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/Http2WindowUpdateFrameUnmarshaller.java @@ -0,0 +1,58 @@ +/* + * Copyright 2014 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.draft10.frame.decoder; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_WINDOW_UPDATE; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.PING_FRAME_PAYLOAD_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.readUnsignedInt; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2WindowUpdateFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameHeader; + +public class Http2WindowUpdateFrameUnmarshaller extends AbstractHttp2FrameUnmarshaller { + + @Override + protected void validate(Http2FrameHeader frameHeader) throws Http2Exception { + if (frameHeader.getType() != FRAME_TYPE_WINDOW_UPDATE) { + throw protocolError("Unsupported frame type: %d.", frameHeader.getType()); + } + if (frameHeader.getStreamId() < 0) { + throw protocolError("Stream Id must be >=0: ", frameHeader.getStreamId()); + } + if (frameHeader.getPayloadLength() < 4) { + throw protocolError("Frame length %d too small.", frameHeader.getPayloadLength()); + } + if (frameHeader.getPayloadLength() > PING_FRAME_PAYLOAD_LENGTH) { + throw protocolError("Frame length %d too big.", frameHeader.getPayloadLength()); + } + } + + @Override + protected Http2Frame doUnmarshall(Http2FrameHeader header, ByteBuf payload, + ByteBufAllocator alloc) throws Http2Exception { + DefaultHttp2WindowUpdateFrame.Builder builder = new DefaultHttp2WindowUpdateFrame.Builder(); + builder.setStreamId(header.getStreamId()); + + int windowSizeIncrement = readUnsignedInt(payload); + builder.setWindowSizeIncrement(windowSizeIncrement); + + return builder.build(); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/package-info.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/package-info.java new file mode 100644 index 0000000000..e055bb4d92 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/decoder/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014 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. + */ + +/** + * Decoder and related classes for HTTP2 frames. + */ +package io.netty.handler.codec.http2.draft10.frame.decoder; + diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/AbstractHttp2FrameMarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/AbstractHttp2FrameMarshaller.java new file mode 100644 index 0000000000..3e2fb9ce84 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/AbstractHttp2FrameMarshaller.java @@ -0,0 +1,65 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; + +/** + * Abstract base class for all {@link Http2FrameMarshaller}s. + */ +public abstract class AbstractHttp2FrameMarshaller implements + Http2FrameMarshaller { + + private final Class frameType; + + protected AbstractHttp2FrameMarshaller(Class frameType) { + if (frameType == null) { + throw new IllegalArgumentException("frameType must be non-null."); + } + this.frameType = frameType; + } + + @Override + public final void marshall(Http2Frame frame, ByteBuf out, ByteBufAllocator alloc) + throws Http2Exception { + if (frame == null) { + throw new IllegalArgumentException("frame must be non-null."); + } + + if (!frameType.isAssignableFrom(frame.getClass())) { + throw protocolError("Unsupported frame type: %s", frame.getClass().getName()); + } + + @SuppressWarnings("unchecked") + T frameT = (T) frame; + doMarshall(frameT, out, alloc); + } + + /** + * Marshals the frame to the output buffer. + * + * @param frame the frame to be marshalled + * @param out the buffer to marshall the frame to. + * @param alloc an allocator that this marshaller may use for creating intermediate buffers as + * needed. + */ + protected abstract void doMarshall(T frame, ByteBuf out, ByteBufAllocator alloc) + throws Http2Exception; +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/DefaultHttp2HeadersEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/DefaultHttp2HeadersEncoder.java new file mode 100644 index 0000000000..9f9a55213a --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/DefaultHttp2HeadersEncoder.java @@ -0,0 +1,63 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import static io.netty.handler.codec.http2.draft10.connection.Http2ConnectionUtil.DEFAULT_HEADER_TABLE_SIZE; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufOutputStream; +import io.netty.handler.codec.http2.draft10.Http2Error; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.Http2Headers; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.Charset; +import java.util.Map.Entry; + +import com.google.common.base.Charsets; +import com.twitter.hpack.Encoder; + +public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder { + private static final Charset DEFAULT_CHARSET = Charsets.UTF_8; + + private final Encoder encoder; + + public DefaultHttp2HeadersEncoder() { + this.encoder = new Encoder(DEFAULT_HEADER_TABLE_SIZE); + } + + @Override + public void encodeHeaders(Http2Headers headers, ByteBuf buffer) throws Http2Exception { + try { + OutputStream stream = new ByteBufOutputStream(buffer); + for (Entry header : headers) { + byte[] key = header.getKey().getBytes(DEFAULT_CHARSET); + byte[] value = header.getValue().getBytes(DEFAULT_CHARSET); + encoder.encodeHeader(stream, key, value); + } + encoder.endHeaders(stream); + } catch (IOException e) { + throw Http2Exception.format(Http2Error.COMPRESSION_ERROR, "Failed encoding headers block: %s", + e.getMessage()); + } + } + + @Override + public void setHeaderTableSize(int size) throws Http2Exception { + // TODO: can we throw away the encoder and create a new one? + } + +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2DataFrameMarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2DataFrameMarshaller.java new file mode 100644 index 0000000000..ef3996f6e0 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2DataFrameMarshaller.java @@ -0,0 +1,67 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FLAG_END_STREAM; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_HEADER_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_DATA; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.setPaddingFlags; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.writePaddingLength; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.frame.Http2DataFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Flags; + +public class Http2DataFrameMarshaller extends AbstractHttp2FrameMarshaller { + + public Http2DataFrameMarshaller() { + super(Http2DataFrame.class); + } + + @Override + protected void doMarshall(Http2DataFrame frame, ByteBuf out, ByteBufAllocator alloc) { + ByteBuf data = frame.content(); + + Http2Flags flags = getFlags(frame); + + // Write the frame header. + int payloadLength = data.readableBytes() + frame.getPaddingLength() + + (flags.isPadHighPresent() ? 1 : 0) + (flags.isPadLowPresent() ? 1 : 0); + out.ensureWritable(FRAME_HEADER_LENGTH + payloadLength); + out.writeShort(payloadLength); + out.writeByte(FRAME_TYPE_DATA); + out.writeByte(flags.getValue()); + out.writeInt(frame.getStreamId()); + + writePaddingLength(frame.getPaddingLength(), out); + + // Write the data. + out.writeBytes(data, data.readerIndex(), data.readableBytes()); + + // Write the required padding. + out.writeZero(frame.getPaddingLength()); + } + + private Http2Flags getFlags(Http2DataFrame frame) { + short flags = 0; + if (frame.isEndOfStream()) { + flags |= FLAG_END_STREAM; + } + + flags = setPaddingFlags(flags, frame.getPaddingLength()); + return new Http2Flags(flags); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2FrameEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2FrameEncoder.java new file mode 100644 index 0000000000..2b2ee4c13b --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2FrameEncoder.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; + +/** + * Encodes {@link Http2Frame} objects and writes them to an output {@link ByteBuf}. The set of frame + * types that is handled by this encoder is given by the {@link Http2FrameMarshaller}. By default, + * the {@link Http2StandardFrameMarshaller} is used. + * + * @see Http2StandardFrameMarshaller + */ +public class Http2FrameEncoder extends MessageToByteEncoder { + + private final Http2FrameMarshaller frameMarshaller; + + public Http2FrameEncoder() { + this(new Http2StandardFrameMarshaller()); + } + + public Http2FrameEncoder(Http2FrameMarshaller frameMarshaller) { + this.frameMarshaller = frameMarshaller; + } + + @Override + protected void encode(ChannelHandlerContext ctx, Http2Frame frame, ByteBuf out) throws Exception { + try { + frameMarshaller.marshall(frame, out, ctx.alloc()); + } catch (Throwable t) { + ctx.fireExceptionCaught(t); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2FrameMarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2FrameMarshaller.java new file mode 100644 index 0000000000..f17b471b85 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2FrameMarshaller.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; + +/** + * Marshalls {@link Http2Frame} objects to a {@link ByteBuf}. + */ +public interface Http2FrameMarshaller { + + /** + * Marshalls the given frame to the output buffer. + * + * @param frame the frame to be marshalled. + * @param out the buffer to marshall the frame to. + * @param alloc an allocator that this marshaller may use for creating intermediate buffers as + * needed. + * @throws Http2Exception thrown if the given fram is not supported by this marshaller. + */ + void marshall(Http2Frame frame, ByteBuf out, ByteBufAllocator alloc) throws Http2Exception; +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2GoAwayFrameMarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2GoAwayFrameMarshaller.java new file mode 100644 index 0000000000..24a8166753 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2GoAwayFrameMarshaller.java @@ -0,0 +1,49 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_HEADER_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_GO_AWAY; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.writeUnsignedInt; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.frame.Http2GoAwayFrame; + +public class Http2GoAwayFrameMarshaller extends AbstractHttp2FrameMarshaller { + + public Http2GoAwayFrameMarshaller() { + super(Http2GoAwayFrame.class); + } + + @Override + protected void doMarshall(Http2GoAwayFrame frame, ByteBuf out, ByteBufAllocator alloc) { + ByteBuf data = frame.content(); + + // Write the frame header. + int payloadLength = data.readableBytes() + 8; + out.ensureWritable(FRAME_HEADER_LENGTH + payloadLength); + out.writeShort(payloadLength); + out.writeByte(FRAME_TYPE_GO_AWAY); + out.writeByte(0); + out.writeInt(0); + + out.writeInt(frame.getLastStreamId()); + writeUnsignedInt(frame.getErrorCode(), out); + + // Write the debug data. + out.writeBytes(data, data.readerIndex(), data.readableBytes()); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2HeadersEncoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2HeadersEncoder.java new file mode 100644 index 0000000000..7a617a4a60 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2HeadersEncoder.java @@ -0,0 +1,39 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.Http2Headers; + +/** + * Encodes {@link Http2Headers} into HPACK-encoded headers blocks. + */ +public interface Http2HeadersEncoder { + + /** + * Encodes the given headers and writes the output headers block to the given output buffer. + * + * @param headers the headers to be encoded. + * @param buffer the buffer to write the headers to. + */ + void encodeHeaders(Http2Headers headers, ByteBuf buffer) throws Http2Exception; + + /** + * Updates the maximum header table size for this encoder. + */ + void setHeaderTableSize(int size) throws Http2Exception; +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2HeadersFrameMarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2HeadersFrameMarshaller.java new file mode 100644 index 0000000000..3005df98a7 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2HeadersFrameMarshaller.java @@ -0,0 +1,116 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.DEFAULT_STREAM_PRIORITY; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FLAG_END_HEADERS; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FLAG_END_STREAM; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FLAG_PRIORITY; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_HEADER_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_CONTINUATION; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_HEADERS; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_FRAME_PAYLOAD_LENGTH; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.Http2HeadersFrame; + +import com.google.common.base.Preconditions; + +public class Http2HeadersFrameMarshaller extends AbstractHttp2FrameMarshaller { + + private final Http2HeadersEncoder headersEncoder; + + public Http2HeadersFrameMarshaller(Http2HeadersEncoder headersEncoder) { + super(Http2HeadersFrame.class); + this.headersEncoder = Preconditions.checkNotNull(headersEncoder, "headersEncoder"); + } + + @Override + protected void doMarshall(Http2HeadersFrame frame, ByteBuf out, ByteBufAllocator alloc) + throws Http2Exception { + // TODO(nathanmittler): include padding? + + int maxFragmentLength = MAX_FRAME_PAYLOAD_LENGTH; + boolean hasPriority = frame.getPriority() != DEFAULT_STREAM_PRIORITY; + if (hasPriority) { + // The first frame will include the priority. + maxFragmentLength -= 4; + } + + // Encode the entire header block into an intermediate buffer. + ByteBuf headerBlock = alloc.buffer(); + headersEncoder.encodeHeaders(frame.getHeaders(), headerBlock); + + ByteBuf fragment = + headerBlock.readSlice(Math.min(headerBlock.readableBytes(), maxFragmentLength)); + int payloadLength = fragment.readableBytes() + (hasPriority ? 4 : 0); + boolean endOfHeaders = headerBlock.readableBytes() == 0; + + // Get the flags for the frame. + short flags = 0; + if (endOfHeaders) { + flags |= FLAG_END_HEADERS; + } + if (frame.isEndOfStream()) { + flags |= FLAG_END_STREAM; + } + if (hasPriority) { + flags |= FLAG_PRIORITY; + } + + // Write the frame header. + out.ensureWritable(FRAME_HEADER_LENGTH + payloadLength); + out.writeShort(payloadLength); + out.writeByte(FRAME_TYPE_HEADERS); + out.writeByte(flags); + out.writeInt(frame.getStreamId()); + + // Write out the priority if it's present. + if (hasPriority) { + out.writeInt(frame.getPriority()); + } + + // Write the first fragment. + out.writeBytes(fragment); + + // Process any continuation frames there might be. + while (headerBlock.readableBytes() > 0) { + writeContinuationFrame(frame.getStreamId(), headerBlock, out); + } + + // Release the intermediate buffer. + headerBlock.release(); + } + + /** + * Writes a single continuation frame with a fragment of the header block to the output buffer. + */ + private void writeContinuationFrame(int streamId, ByteBuf headerBlock, ByteBuf out) { + ByteBuf fragment = + headerBlock.readSlice(Math.min(headerBlock.readableBytes(), MAX_FRAME_PAYLOAD_LENGTH)); + + // Write the frame header. + out.ensureWritable(FRAME_HEADER_LENGTH + fragment.readableBytes()); + out.writeShort(fragment.readableBytes()); + out.writeByte(FRAME_TYPE_CONTINUATION); + out.writeByte(headerBlock.readableBytes() == 0 ? FLAG_END_HEADERS : 0); + out.writeInt(streamId); + + // Write the headers block. + out.writeBytes(fragment); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2PingFrameMarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2PingFrameMarshaller.java new file mode 100644 index 0000000000..0bbcd9086e --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2PingFrameMarshaller.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FLAG_ACK; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_HEADER_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_PING; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.frame.Http2PingFrame; + +public class Http2PingFrameMarshaller extends AbstractHttp2FrameMarshaller { + + public Http2PingFrameMarshaller() { + super(Http2PingFrame.class); + } + + @Override + protected void doMarshall(Http2PingFrame frame, ByteBuf out, ByteBufAllocator alloc) { + ByteBuf data = frame.content(); + + // Write the frame header. + int payloadLength = data.readableBytes(); + out.ensureWritable(FRAME_HEADER_LENGTH + payloadLength); + out.writeShort(payloadLength); + out.writeByte(FRAME_TYPE_PING); + out.writeByte(frame.isAck() ? FLAG_ACK : 0); + out.writeInt(0); + + // Write the debug data. + out.writeBytes(data, data.readerIndex(), data.readableBytes()); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2PriorityFrameMarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2PriorityFrameMarshaller.java new file mode 100644 index 0000000000..19968d45a5 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2PriorityFrameMarshaller.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_HEADER_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_PRIORITY; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.frame.Http2PriorityFrame; + +public class Http2PriorityFrameMarshaller extends AbstractHttp2FrameMarshaller { + + public Http2PriorityFrameMarshaller() { + super(Http2PriorityFrame.class); + } + + @Override + protected void doMarshall(Http2PriorityFrame frame, ByteBuf out, ByteBufAllocator alloc) { + + // Write the frame header. + int payloadLength = 4; + out.ensureWritable(FRAME_HEADER_LENGTH + payloadLength); + out.writeShort(payloadLength); + out.writeByte(FRAME_TYPE_PRIORITY); + out.writeByte(0); + out.writeInt(frame.getStreamId()); + + // Write out the priority if it's present. + out.writeInt(frame.getPriority()); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2PushPromiseFrameMarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2PushPromiseFrameMarshaller.java new file mode 100644 index 0000000000..139b38192d --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2PushPromiseFrameMarshaller.java @@ -0,0 +1,93 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FLAG_END_HEADERS; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_HEADER_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_CONTINUATION; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_PUSH_PROMISE; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.MAX_FRAME_PAYLOAD_LENGTH; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.Http2PushPromiseFrame; + +import com.google.common.base.Preconditions; + +public class Http2PushPromiseFrameMarshaller extends + AbstractHttp2FrameMarshaller { + + private final Http2HeadersEncoder headersEncoder; + + public Http2PushPromiseFrameMarshaller(Http2HeadersEncoder headersEncoder) { + super(Http2PushPromiseFrame.class); + this.headersEncoder = Preconditions.checkNotNull(headersEncoder, "headersEncoder"); + } + + @Override + protected void doMarshall(Http2PushPromiseFrame frame, ByteBuf out, ByteBufAllocator alloc) + throws Http2Exception { + + // Max size minus the promised stream ID. + int maxFragmentLength = MAX_FRAME_PAYLOAD_LENGTH - 4; + + // Encode the entire header block into an intermediate buffer. + ByteBuf headerBlock = alloc.buffer(); + headersEncoder.encodeHeaders(frame.getHeaders(), headerBlock); + + ByteBuf fragment = + headerBlock.readSlice(Math.min(headerBlock.readableBytes(), maxFragmentLength)); + + // Write the frame header. + out.ensureWritable(FRAME_HEADER_LENGTH + fragment.readableBytes()); + out.writeShort(fragment.readableBytes() + 4); + out.writeByte(FRAME_TYPE_PUSH_PROMISE); + out.writeByte(headerBlock.readableBytes() == 0 ? FLAG_END_HEADERS : 0); + out.writeInt(frame.getStreamId()); + + // Write out the promised stream ID. + out.writeInt(frame.getPromisedStreamId()); + + // Write the first fragment. + out.writeBytes(fragment); + + // Process any continuation frames there might be. + while (headerBlock.readableBytes() > 0) { + writeContinuationFrame(frame.getStreamId(), headerBlock, out); + } + + // Release the intermediate buffer. + headerBlock.release(); + } + + /** + * Writes a single continuation frame with a fragment of the header block to the output buffer. + */ + private void writeContinuationFrame(int streamId, ByteBuf headerBlock, ByteBuf out) { + ByteBuf fragment = + headerBlock.readSlice(Math.min(headerBlock.readableBytes(), MAX_FRAME_PAYLOAD_LENGTH)); + + // Write the frame header. + out.ensureWritable(FRAME_HEADER_LENGTH + fragment.readableBytes()); + out.writeShort(fragment.readableBytes()); + out.writeByte(FRAME_TYPE_CONTINUATION); + out.writeByte(headerBlock.readableBytes() == 0 ? FLAG_END_HEADERS : 0); + out.writeInt(streamId); + + // Write the headers block. + out.writeBytes(fragment); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2RstStreamFrameMarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2RstStreamFrameMarshaller.java new file mode 100644 index 0000000000..febd43179e --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2RstStreamFrameMarshaller.java @@ -0,0 +1,45 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_HEADER_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_RST_STREAM; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.writeUnsignedInt; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.frame.Http2RstStreamFrame; + +public class Http2RstStreamFrameMarshaller extends + AbstractHttp2FrameMarshaller { + + public Http2RstStreamFrameMarshaller() { + super(Http2RstStreamFrame.class); + } + + @Override + protected void doMarshall(Http2RstStreamFrame frame, ByteBuf out, ByteBufAllocator alloc) { + + // Write the frame header. + int payloadLength = 4; + out.ensureWritable(FRAME_HEADER_LENGTH + payloadLength); + out.writeShort(payloadLength); + out.writeByte(FRAME_TYPE_RST_STREAM); + out.writeByte(0); + out.writeInt(frame.getStreamId()); + + writeUnsignedInt(frame.getErrorCode(), out); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2SettingsFrameMarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2SettingsFrameMarshaller.java new file mode 100644 index 0000000000..daa1c0fbca --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2SettingsFrameMarshaller.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_HEADER_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_SETTINGS; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.SETTINGS_ENABLE_PUSH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.SETTINGS_HEADER_TABLE_SIZE; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.SETTINGS_INITIAL_WINDOW_SIZE; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.SETTINGS_MAX_CONCURRENT_STREAMS; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.writeUnsignedInt; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil; +import io.netty.handler.codec.http2.draft10.frame.Http2SettingsFrame; + +public class Http2SettingsFrameMarshaller extends AbstractHttp2FrameMarshaller { + + public Http2SettingsFrameMarshaller() { + super(Http2SettingsFrame.class); + } + + @Override + protected void doMarshall(Http2SettingsFrame frame, ByteBuf out, ByteBufAllocator alloc) { + int numSettings = 0; + numSettings += frame.getPushEnabled() != null ? 1 : 0; + numSettings += frame.getHeaderTableSize() != null ? 1 : 0; + numSettings += frame.getInitialWindowSize() != null ? 1 : 0; + numSettings += frame.getMaxConcurrentStreams() != null ? 1 : 0; + + // Write the frame header. + int payloadLength = 5 * numSettings; + out.ensureWritable(FRAME_HEADER_LENGTH + payloadLength); + out.writeShort(payloadLength); + out.writeByte(FRAME_TYPE_SETTINGS); + out.writeByte(frame.isAck() ? Http2FrameCodecUtil.FLAG_ACK : 0); + out.writeInt(0); + + if (frame.getPushEnabled() != null) { + out.writeByte(SETTINGS_ENABLE_PUSH); + writeUnsignedInt(frame.getPushEnabled() ? 1L : 0L, out); + } + if (frame.getHeaderTableSize() != null) { + out.writeByte(SETTINGS_HEADER_TABLE_SIZE); + writeUnsignedInt(frame.getHeaderTableSize(), out); + } + if (frame.getInitialWindowSize() != null) { + out.writeByte(SETTINGS_INITIAL_WINDOW_SIZE); + writeUnsignedInt(frame.getInitialWindowSize(), out); + } + if (frame.getMaxConcurrentStreams() != null) { + out.writeByte(SETTINGS_MAX_CONCURRENT_STREAMS); + writeUnsignedInt(frame.getMaxConcurrentStreams(), out); + } + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2StandardFrameMarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2StandardFrameMarshaller.java new file mode 100644 index 0000000000..1dfca729ce --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2StandardFrameMarshaller.java @@ -0,0 +1,110 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import static io.netty.handler.codec.http2.draft10.Http2Exception.protocolError; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.frame.Http2DataFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2HeadersFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2PushPromiseFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2GoAwayFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2PingFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2PriorityFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2RstStreamFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2SettingsFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2WindowUpdateFrame; + +/** + * A composite {@link Http2FrameMarshaller} that supports all frames identified by the HTTP2 spec. + * This handles marshalling for the following frame types: + *

+ * {@link Http2DataFrame}
+ * {@link Http2GoAwayFrame}
+ * {@link Http2HeadersFrame}
+ * {@link Http2PingFrame}
+ * {@link Http2PriorityFrame}
+ * {@link Http2PushPromiseFrame}
+ * {@link Http2RstStreamFrame}
+ * {@link Http2SettingsFrame}
+ * {@link Http2WindowUpdateFrame}
+ */ +public class Http2StandardFrameMarshaller implements Http2FrameMarshaller { + + private final Http2FrameMarshaller dataMarshaller; + private final Http2FrameMarshaller headersMarshaller; + private final Http2FrameMarshaller goAwayMarshaller; + private final Http2FrameMarshaller pingMarshaller; + private final Http2FrameMarshaller priorityMarshaller; + private final Http2FrameMarshaller pushPromiseMarshaller; + private final Http2FrameMarshaller rstStreamMarshaller; + private final Http2FrameMarshaller settingsMarshaller; + private final Http2FrameMarshaller windowUpdateMarshaller; + + public Http2StandardFrameMarshaller() { + this(new DefaultHttp2HeadersEncoder()); + } + + public Http2StandardFrameMarshaller(Http2HeadersEncoder headersEncoder) { + dataMarshaller = new Http2DataFrameMarshaller(); + headersMarshaller = new Http2HeadersFrameMarshaller(headersEncoder); + goAwayMarshaller = new Http2GoAwayFrameMarshaller(); + pingMarshaller = new Http2PingFrameMarshaller(); + priorityMarshaller = new Http2PriorityFrameMarshaller(); + pushPromiseMarshaller = new Http2PushPromiseFrameMarshaller(headersEncoder); + rstStreamMarshaller = new Http2RstStreamFrameMarshaller(); + settingsMarshaller = new Http2SettingsFrameMarshaller(); + windowUpdateMarshaller = new Http2WindowUpdateFrameMarshaller(); + } + + @Override + public void marshall(Http2Frame frame, ByteBuf out, ByteBufAllocator alloc) + throws Http2Exception { + Http2FrameMarshaller marshaller = null; + + if (frame == null) { + throw new IllegalArgumentException("frame must be non-null"); + } + + if (frame instanceof Http2DataFrame) { + marshaller = dataMarshaller; + } else if (frame instanceof Http2HeadersFrame) { + marshaller = headersMarshaller; + } else if (frame instanceof Http2GoAwayFrame) { + marshaller = goAwayMarshaller; + } else if (frame instanceof Http2PingFrame) { + marshaller = pingMarshaller; + } else if (frame instanceof Http2PriorityFrame) { + marshaller = priorityMarshaller; + } else if (frame instanceof Http2PushPromiseFrame) { + marshaller = pushPromiseMarshaller; + } else if (frame instanceof Http2RstStreamFrame) { + marshaller = rstStreamMarshaller; + } else if (frame instanceof Http2SettingsFrame) { + marshaller = settingsMarshaller; + } else if (frame instanceof Http2WindowUpdateFrame) { + marshaller = windowUpdateMarshaller; + } + + if (marshaller == null) { + throw protocolError("Unsupported frame type: %s", frame.getClass().getName()); + } + + marshaller.marshall(frame, out, alloc); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2WindowUpdateFrameMarshaller.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2WindowUpdateFrameMarshaller.java new file mode 100644 index 0000000000..3b436b6514 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/Http2WindowUpdateFrameMarshaller.java @@ -0,0 +1,44 @@ +/* + * Copyright 2014 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.draft10.frame.encoder; + +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_HEADER_LENGTH; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.FRAME_TYPE_WINDOW_UPDATE; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http2.draft10.frame.Http2WindowUpdateFrame; + +public class Http2WindowUpdateFrameMarshaller extends + AbstractHttp2FrameMarshaller { + + public Http2WindowUpdateFrameMarshaller() { + super(Http2WindowUpdateFrame.class); + } + + @Override + protected void doMarshall(Http2WindowUpdateFrame frame, ByteBuf out, ByteBufAllocator alloc) { + + // Write the frame header. + int payloadLength = 4; + out.ensureWritable(FRAME_HEADER_LENGTH + payloadLength); + out.writeShort(payloadLength); + out.writeByte(FRAME_TYPE_WINDOW_UPDATE); + out.writeByte(0); + out.writeInt(frame.getStreamId()); + + out.writeInt(frame.getWindowSizeIncrement()); + } +} diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/package-info.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/package-info.java new file mode 100644 index 0000000000..23d6adeb69 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/encoder/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014 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. + */ + +/** + * Encoder and related classes for HTTP2. + */ +package io.netty.handler.codec.http2.draft10.frame.encoder; + diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/package-info.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/package-info.java new file mode 100644 index 0000000000..38041272fe --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/frame/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014 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. + */ + +/** + * Encoder, decoder and their related message types for HTTP2 frames. + */ +package io.netty.handler.codec.http2.draft10.frame; + diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/package-info.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/package-info.java new file mode 100644 index 0000000000..f46898a4e3 --- /dev/null +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/draft10/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2014 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. + */ + +/** + * Encoder, decoder and their related message types for HTTP2. + */ +package io.netty.handler.codec.http2.draft10; + diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/DefaultHttp2ConnectionTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/DefaultHttp2ConnectionTest.java new file mode 100644 index 0000000000..6787c374e5 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/DefaultHttp2ConnectionTest.java @@ -0,0 +1,308 @@ +/* + * Copyright 2014 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.draft10.connection; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.connection.Http2Connection.Listener; +import io.netty.handler.codec.http2.draft10.connection.Http2Stream.State; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Tests for {@link DefaultHttp2Connection}. + */ +public class DefaultHttp2ConnectionTest { + + @Mock + private Listener listener; + + @Mock + private ChannelHandlerContext ctx; + + @Mock + private ChannelFuture future; + + @Mock + private ChannelPromise promise; + + private DefaultHttp2Connection server; + private DefaultHttp2Connection client; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + when(ctx.writeAndFlush(any())).thenReturn(future); + when(ctx.newSucceededFuture()).thenReturn(future); + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + ChannelFutureListener listener = + (ChannelFutureListener) invocation.getArguments()[0]; + listener.operationComplete(future); + return null; + } + }).when(future).addListener(any(ChannelFutureListener.class)); + + server = new DefaultHttp2Connection(true); + client = new DefaultHttp2Connection(false); + + server.addListener(listener); + client.addListener(listener); + } + + @Test(expected = Http2Exception.class) + public void getStreamOrFailWithoutStreamShouldFail() throws Http2Exception { + server.getStreamOrFail(100); + } + + @Test + public void getStreamWithoutStreamShouldReturnNull() { + assertNull(server.getStream(100)); + } + + @Test + public void serverCreateStreamShouldSucceed() throws Http2Exception { + Http2Stream stream = server.local().createStream(2, 1, false); + assertEquals(2, stream.getId()); + assertEquals(1, stream.getPriority()); + assertEquals(State.OPEN, stream.getState()); + assertEquals(1, server.getActiveStreams().size()); + assertEquals(2, server.local().getLastStreamCreated()); + verify(listener).streamCreated(2); + + stream = server.local().createStream(4, 256, true); + assertEquals(4, stream.getId()); + assertEquals(256, stream.getPriority()); + assertEquals(State.HALF_CLOSED_LOCAL, stream.getState()); + assertEquals(2, server.getActiveStreams().size()); + assertEquals(4, server.local().getLastStreamCreated()); + verify(listener).streamCreated(4); + + stream = server.remote().createStream(3, Integer.MAX_VALUE, true); + assertEquals(3, stream.getId()); + assertEquals(Integer.MAX_VALUE, stream.getPriority()); + assertEquals(State.HALF_CLOSED_REMOTE, stream.getState()); + assertEquals(3, server.getActiveStreams().size()); + assertEquals(3, server.remote().getLastStreamCreated()); + verify(listener).streamCreated(3); + + stream = server.remote().createStream(5, 1, false); + assertEquals(5, stream.getId()); + assertEquals(1, stream.getPriority()); + assertEquals(State.OPEN, stream.getState()); + assertEquals(4, server.getActiveStreams().size()); + assertEquals(5, server.remote().getLastStreamCreated()); + verify(listener).streamCreated(5); + } + + @Test + public void clientCreateStreamShouldSucceed() throws Http2Exception { + Http2Stream stream = client.remote().createStream(2, 1, false); + assertEquals(2, stream.getId()); + assertEquals(1, stream.getPriority()); + assertEquals(State.OPEN, stream.getState()); + assertEquals(1, client.getActiveStreams().size()); + assertEquals(2, client.remote().getLastStreamCreated()); + verify(listener).streamCreated(2); + + stream = client.remote().createStream(4, 256, true); + assertEquals(4, stream.getId()); + assertEquals(256, stream.getPriority()); + assertEquals(State.HALF_CLOSED_REMOTE, stream.getState()); + assertEquals(2, client.getActiveStreams().size()); + assertEquals(4, client.remote().getLastStreamCreated()); + verify(listener).streamCreated(4); + + stream = client.local().createStream(3, Integer.MAX_VALUE, true); + assertEquals(3, stream.getId()); + assertEquals(Integer.MAX_VALUE, stream.getPriority()); + assertEquals(State.HALF_CLOSED_LOCAL, stream.getState()); + assertEquals(3, client.getActiveStreams().size()); + assertEquals(3, client.local().getLastStreamCreated()); + verify(listener).streamCreated(3); + + stream = client.local().createStream(5, 1, false); + assertEquals(5, stream.getId()); + assertEquals(1, stream.getPriority()); + assertEquals(State.OPEN, stream.getState()); + assertEquals(4, client.getActiveStreams().size()); + assertEquals(5, client.local().getLastStreamCreated()); + verify(listener).streamCreated(5); + } + + @Test + public void serverReservePushStreamShouldSucceed() throws Http2Exception { + Http2Stream stream = server.remote().createStream(3, 1, true); + Http2Stream pushStream = server.local().reservePushStream(2, stream); + assertEquals(2, pushStream.getId()); + assertEquals(2, pushStream.getPriority()); + assertEquals(State.RESERVED_LOCAL, pushStream.getState()); + assertEquals(1, server.getActiveStreams().size()); + assertEquals(2, server.local().getLastStreamCreated()); + verify(listener).streamCreated(3); + verify(listener).streamCreated(2); + } + + @Test + public void clientReservePushStreamShouldSucceed() throws Http2Exception { + Http2Stream stream = client.remote().createStream(2, 1, true); + Http2Stream pushStream = client.local().reservePushStream(3, stream); + assertEquals(3, pushStream.getId()); + assertEquals(2, pushStream.getPriority()); + assertEquals(State.RESERVED_LOCAL, pushStream.getState()); + assertEquals(1, client.getActiveStreams().size()); + assertEquals(3, client.local().getLastStreamCreated()); + verify(listener).streamCreated(2); + verify(listener).streamCreated(3); + } + + @Test(expected = Http2Exception.class) + public void createStreamWithInvalidIdShouldThrow() throws Http2Exception { + server.remote().createStream(1, 1, true); + } + + @Test(expected = Http2Exception.class) + public void maxAllowedStreamsExceededShouldThrow() throws Http2Exception { + server.local().setMaxStreams(0); + server.local().createStream(2, 1, true); + } + + @Test(expected = Http2Exception.class) + public void invalidPriorityShouldThrow() throws Http2Exception { + server.local().createStream(2, -1, true); + } + + @Test(expected = Http2Exception.class) + public void reserveWithPushDisallowedShouldThrow() throws Http2Exception { + Http2Stream stream = server.remote().createStream(3, 1, true); + server.remote().setPushToAllowed(false); + server.local().reservePushStream(2, stream); + } + + @Test(expected = Http2Exception.class) + public void goAwayReceivedShouldDisallowCreation() throws Http2Exception { + server.goAwayReceived(); + server.remote().createStream(3, 1, true); + } + + @Test + public void activeStreamsShouldBeSortedByPriority() throws Http2Exception { + server.local().createStream(2, 1, false); + server.local().createStream(4, 256, true); + server.remote().createStream(3, Integer.MAX_VALUE, true); + server.remote().createStream(5, 1, false); + List activeStreams = server.getActiveStreams(); + assertEquals(2, activeStreams.get(0).getId()); + assertEquals(5, activeStreams.get(1).getId()); + assertEquals(4, activeStreams.get(2).getId()); + assertEquals(3, activeStreams.get(3).getId()); + } + + @Test + public void priorityChangeShouldReorderActiveStreams() throws Http2Exception { + server.local().createStream(2, 1, false); + server.local().createStream(4, 256, true); + server.remote().createStream(3, Integer.MAX_VALUE, true); + server.remote().createStream(5, 1, false); + Http2Stream stream7 = server.remote().createStream(7, 1, false); + server.remote().createStream(9, 1, false); + + // Make this this highest priority. + stream7.setPriority(0); + + List activeStreams = server.getActiveStreams(); + assertEquals(7, activeStreams.get(0).getId()); + assertEquals(2, activeStreams.get(1).getId()); + assertEquals(5, activeStreams.get(2).getId()); + assertEquals(9, activeStreams.get(3).getId()); + assertEquals(4, activeStreams.get(4).getId()); + assertEquals(3, activeStreams.get(5).getId()); + } + + @Test + public void closeShouldSucceed() throws Http2Exception { + Http2Stream stream = server.remote().createStream(3, 1, true); + stream.close(ctx, future); + assertEquals(State.CLOSED, stream.getState()); + assertTrue(server.getActiveStreams().isEmpty()); + verify(listener).streamClosed(3); + } + + @Test + public void closeLocalWhenOpenShouldSucceed() throws Http2Exception { + Http2Stream stream = server.remote().createStream(3, 1, false); + stream.closeLocalSide(ctx, future); + assertEquals(State.HALF_CLOSED_LOCAL, stream.getState()); + assertEquals(1, server.getActiveStreams().size()); + verify(listener, never()).streamClosed(3); + } + + @Test + public void closeRemoteWhenOpenShouldSucceed() throws Http2Exception { + Http2Stream stream = server.remote().createStream(3, 1, false); + stream.closeRemoteSide(ctx, future); + assertEquals(State.HALF_CLOSED_REMOTE, stream.getState()); + assertEquals(1, server.getActiveStreams().size()); + verify(listener, never()).streamClosed(3); + } + + @Test + public void closeOnlyOpenSideShouldClose() throws Http2Exception { + Http2Stream stream = server.remote().createStream(3, 1, true); + stream.closeLocalSide(ctx, future); + assertEquals(State.CLOSED, stream.getState()); + assertTrue(server.getActiveStreams().isEmpty()); + verify(listener).streamClosed(3); + } + + @Test + public void sendGoAwayShouldCloseConnection() { + server.sendGoAway(ctx, promise, null); + verify(ctx).close(promise); + } + + @Test + public void sendGoAwayShouldCloseAfterConnectionInactive() throws Http2Exception { + Http2Stream stream = server.remote().createStream(3, 1, true); + server.sendGoAway(ctx, promise, null); + verify(ctx, never()).close(promise); + + // Now close the stream and verify that the context was closed too. + stream.close(ctx, future); + verify(ctx).close(promise); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/DefaultInboundFlowControllerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/DefaultInboundFlowControllerTest.java new file mode 100644 index 0000000000..4e1a1c762c --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/DefaultInboundFlowControllerTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2014 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.draft10.connection; + +import static io.netty.handler.codec.http2.draft10.connection.Http2ConnectionUtil.DEFAULT_FLOW_CONTROL_WINDOW_SIZE; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.CONNECTION_STREAM_ID; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.connection.Http2Connection.Listener; +import io.netty.handler.codec.http2.draft10.connection.InboundFlowController.FrameWriter; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2WindowUpdateFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2DataFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2WindowUpdateFrame; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * Tests for {@link DefaultInboundFlowController}. + */ +public class DefaultInboundFlowControllerTest { + private static final int STREAM_ID = 1; + + private DefaultInboundFlowController controller; + + @Mock + private Http2Connection connection; + + @Mock + private ByteBuf buffer; + + @Mock + private FrameWriter frameWriter; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + // Mock the creation of a single stream. + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Listener listener = (Listener) invocation.getArguments()[0]; + listener.streamCreated(STREAM_ID); + return null; + } + }).when(connection).addListener(any(Listener.class)); + + controller = new DefaultInboundFlowController(connection); + } + + @Test + public void dataFrameShouldBeAccepted() throws Http2Exception { + Http2DataFrame frame = mockDataFrame(10, false); + controller.applyInboundFlowControl(frame, frameWriter); + verifyWindowUpdateNotSent(); + } + + @Test(expected = Http2Exception.class) + public void connectionFlowControlExceededShouldThrow() throws Http2Exception { + Http2DataFrame frame = mockDataFrame(DEFAULT_FLOW_CONTROL_WINDOW_SIZE + 1, true); + controller.applyInboundFlowControl(frame, frameWriter); + } + + @Test + public void halfWindowRemainingShouldUpdateConnectionWindow() throws Http2Exception { + int dataSize = (DEFAULT_FLOW_CONTROL_WINDOW_SIZE / 2) + 1; + int newWindow = DEFAULT_FLOW_CONTROL_WINDOW_SIZE - dataSize; + int windowDelta = DEFAULT_FLOW_CONTROL_WINDOW_SIZE - newWindow; + + // Set end-of-stream on the frame, so no window update will be sent for the stream. + Http2DataFrame frame = mockDataFrame(dataSize, true); + controller.applyInboundFlowControl(frame, frameWriter); + verify(frameWriter).writeFrame(eq(windowUpdate(CONNECTION_STREAM_ID, windowDelta))); + } + + @Test + public void halfWindowRemainingShouldUpdateAllWindows() throws Http2Exception { + int dataSize = (DEFAULT_FLOW_CONTROL_WINDOW_SIZE / 2) + 1; + int initialWindowSize = DEFAULT_FLOW_CONTROL_WINDOW_SIZE; + int windowDelta = getWindowDelta(initialWindowSize, initialWindowSize, dataSize); + + // Don't set end-of-stream so we'll get a window update for the stream as well. + Http2DataFrame frame = mockDataFrame(dataSize, false); + controller.applyInboundFlowControl(frame, frameWriter); + verify(frameWriter).writeFrame(eq(windowUpdate(CONNECTION_STREAM_ID, windowDelta))); + verify(frameWriter).writeFrame(eq(windowUpdate(STREAM_ID, windowDelta))); + } + + @Test + public void initialWindowUpdateShouldAllowMoreFrames() throws Http2Exception { + // Send a frame that takes up the entire window. + int initialWindowSize = DEFAULT_FLOW_CONTROL_WINDOW_SIZE; + Http2DataFrame bigFrame = mockDataFrame(initialWindowSize, false); + controller.applyInboundFlowControl(bigFrame, frameWriter); + + // Update the initial window size to allow another frame. + int newInitialWindowSize = 2 * initialWindowSize; + controller.setInitialInboundWindowSize(newInitialWindowSize); + + // Clear any previous calls to the writer. + Mockito.reset(frameWriter); + + // Send the next frame and verify that the expected window updates were sent. + controller.applyInboundFlowControl(bigFrame, frameWriter); + int delta = newInitialWindowSize - initialWindowSize; + verify(frameWriter).writeFrame(eq(windowUpdate(CONNECTION_STREAM_ID, delta))); + verify(frameWriter).writeFrame(eq(windowUpdate(STREAM_ID, delta))); + } + + private int getWindowDelta(int initialSize, int windowSize, int dataSize) { + int newWindowSize = windowSize - dataSize; + return initialSize - newWindowSize; + } + + private Http2DataFrame mockDataFrame(int payloadLength, boolean endOfStream) { + Http2DataFrame frame = Mockito.mock(Http2DataFrame.class); + when(frame.getStreamId()).thenReturn(STREAM_ID); + when(frame.isEndOfStream()).thenReturn(endOfStream); + when(frame.content()).thenReturn(buffer); + when(buffer.readableBytes()).thenReturn(payloadLength); + return frame; + } + + private void verifyWindowUpdateNotSent() { + verify(frameWriter, never()).writeFrame(any(Http2WindowUpdateFrame.class)); + } + + private Http2WindowUpdateFrame windowUpdate(int streamId, int delta) { + return new DefaultHttp2WindowUpdateFrame.Builder().setStreamId(streamId) + .setWindowSizeIncrement(delta).build(); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/DefaultOutboundFlowControllerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/DefaultOutboundFlowControllerTest.java new file mode 100644 index 0000000000..b4879bfc45 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/DefaultOutboundFlowControllerTest.java @@ -0,0 +1,244 @@ +/* + * Copyright 2014 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.draft10.connection; + +import static io.netty.handler.codec.http2.draft10.connection.Http2ConnectionUtil.DEFAULT_FLOW_CONTROL_WINDOW_SIZE; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.CONNECTION_STREAM_ID; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.connection.Http2Connection.Listener; +import io.netty.handler.codec.http2.draft10.connection.OutboundFlowController.FrameWriter; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2DataFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2DataFrame; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import com.google.common.collect.ImmutableList; + +/** + * Tests for {@link DefaultOutboundFlowController}. + */ +public class DefaultOutboundFlowControllerTest { + private static final int STREAM_ID = 1; + + private DefaultOutboundFlowController controller; + + @Mock + private Http2Connection connection; + + @Mock + private ByteBuf buffer; + + @Mock + private FrameWriter frameWriter; + + @Mock + private Http2Stream stream; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + + // Mock the creation of a single stream. + doAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + Listener listener = (Listener) invocation.getArguments()[0]; + listener.streamCreated(STREAM_ID); + return null; + } + }).when(connection).addListener(any(Listener.class)); + when(connection.getActiveStreams()).thenReturn(ImmutableList.of(stream)); + when(stream.getId()).thenReturn(STREAM_ID); + + controller = new DefaultOutboundFlowController(connection); + } + + @Test + public void frameShouldBeSentImmediately() throws Http2Exception { + Http2DataFrame frame = frame(10); + controller.sendFlowControlled(frame, frameWriter); + verify(frameWriter).writeFrame(frame); + assertEquals(1, frame.refCnt()); + frame.release(); + } + + @Test + public void stalledStreamShouldQueueFrame() throws Http2Exception { + controller.setInitialOutboundWindowSize(0); + + Http2DataFrame frame = frame(10); + controller.sendFlowControlled(frame, frameWriter); + verify(frameWriter, never()).writeFrame(frame); + assertEquals(1, frame.refCnt()); + frame.release(); + } + + @Test + public void nonZeroWindowShouldSendPartialFrame() throws Http2Exception { + controller.setInitialOutboundWindowSize(5); + + Http2DataFrame frame = frame(10); + controller.sendFlowControlled(frame, frameWriter); + + // Verify that a partial frame of 5 was sent. + ArgumentCaptor argument = ArgumentCaptor.forClass(Http2DataFrame.class); + verify(frameWriter).writeFrame(argument.capture()); + Http2DataFrame writtenFrame = argument.getValue(); + assertEquals(STREAM_ID, writtenFrame.getStreamId()); + assertEquals(5, writtenFrame.content().readableBytes()); + assertEquals(2, writtenFrame.refCnt()); + assertEquals(2, frame.refCnt()); + frame.release(2); + } + + @Test + public void initialWindowUpdateShouldSendFrame() throws Http2Exception { + controller.setInitialOutboundWindowSize(0); + + Http2DataFrame frame = frame(10); + controller.sendFlowControlled(frame, frameWriter); + verify(frameWriter, never()).writeFrame(frame); + + // Verify that the entire frame was sent. + controller.setInitialOutboundWindowSize(10); + ArgumentCaptor argument = ArgumentCaptor.forClass(Http2DataFrame.class); + verify(frameWriter).writeFrame(argument.capture()); + Http2DataFrame writtenFrame = argument.getValue(); + assertEquals(frame, writtenFrame); + assertEquals(1, frame.refCnt()); + frame.release(); + } + + @Test + public void initialWindowUpdateShouldSendPartialFrame() throws Http2Exception { + controller.setInitialOutboundWindowSize(0); + + Http2DataFrame frame = frame(10); + controller.sendFlowControlled(frame, frameWriter); + verify(frameWriter, never()).writeFrame(frame); + + // Verify that a partial frame of 5 was sent. + controller.setInitialOutboundWindowSize(5); + ArgumentCaptor argument = ArgumentCaptor.forClass(Http2DataFrame.class); + verify(frameWriter).writeFrame(argument.capture()); + Http2DataFrame writtenFrame = argument.getValue(); + assertEquals(STREAM_ID, writtenFrame.getStreamId()); + assertEquals(5, writtenFrame.content().readableBytes()); + assertEquals(2, writtenFrame.refCnt()); + assertEquals(2, frame.refCnt()); + frame.release(2); + } + + @Test + public void connectionWindowUpdateShouldSendFrame() throws Http2Exception { + // Set the connection window size to zero. + controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, -DEFAULT_FLOW_CONTROL_WINDOW_SIZE); + + Http2DataFrame frame = frame(10); + controller.sendFlowControlled(frame, frameWriter); + verify(frameWriter, never()).writeFrame(frame); + + // Verify that the entire frame was sent. + controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 10); + ArgumentCaptor argument = ArgumentCaptor.forClass(Http2DataFrame.class); + verify(frameWriter).writeFrame(argument.capture()); + Http2DataFrame writtenFrame = argument.getValue(); + assertEquals(frame, writtenFrame); + assertEquals(1, frame.refCnt()); + frame.release(); + } + + @Test + public void connectionWindowUpdateShouldSendPartialFrame() throws Http2Exception { + // Set the connection window size to zero. + controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, -DEFAULT_FLOW_CONTROL_WINDOW_SIZE); + + Http2DataFrame frame = frame(10); + controller.sendFlowControlled(frame, frameWriter); + verify(frameWriter, never()).writeFrame(frame); + + // Verify that a partial frame of 5 was sent. + controller.updateOutboundWindowSize(CONNECTION_STREAM_ID, 5); + ArgumentCaptor argument = ArgumentCaptor.forClass(Http2DataFrame.class); + verify(frameWriter).writeFrame(argument.capture()); + Http2DataFrame writtenFrame = argument.getValue(); + assertEquals(STREAM_ID, writtenFrame.getStreamId()); + assertEquals(5, writtenFrame.content().readableBytes()); + assertEquals(2, writtenFrame.refCnt()); + assertEquals(2, frame.refCnt()); + frame.release(2); + } + + @Test + public void streamWindowUpdateShouldSendFrame() throws Http2Exception { + // Set the stream window size to zero. + controller.updateOutboundWindowSize(STREAM_ID, -DEFAULT_FLOW_CONTROL_WINDOW_SIZE); + + Http2DataFrame frame = frame(10); + controller.sendFlowControlled(frame, frameWriter); + verify(frameWriter, never()).writeFrame(frame); + + // Verify that the entire frame was sent. + controller.updateOutboundWindowSize(STREAM_ID, 10); + ArgumentCaptor argument = ArgumentCaptor.forClass(Http2DataFrame.class); + verify(frameWriter).writeFrame(argument.capture()); + Http2DataFrame writtenFrame = argument.getValue(); + assertEquals(frame, writtenFrame); + assertEquals(1, frame.refCnt()); + frame.release(); + } + + @Test + public void streamWindowUpdateShouldSendPartialFrame() throws Http2Exception { + // Set the stream window size to zero. + controller.updateOutboundWindowSize(STREAM_ID, -DEFAULT_FLOW_CONTROL_WINDOW_SIZE); + + Http2DataFrame frame = frame(10); + controller.sendFlowControlled(frame, frameWriter); + verify(frameWriter, never()).writeFrame(frame); + + // Verify that a partial frame of 5 was sent. + controller.updateOutboundWindowSize(STREAM_ID, 5); + ArgumentCaptor argument = ArgumentCaptor.forClass(Http2DataFrame.class); + verify(frameWriter).writeFrame(argument.capture()); + Http2DataFrame writtenFrame = argument.getValue(); + assertEquals(STREAM_ID, writtenFrame.getStreamId()); + assertEquals(5, writtenFrame.content().readableBytes()); + assertEquals(2, writtenFrame.refCnt()); + assertEquals(2, frame.refCnt()); + frame.release(2); + } + + private Http2DataFrame frame(int payloadLength) { + ByteBuf buffer = Unpooled.buffer(payloadLength); + buffer.writerIndex(payloadLength); + return new DefaultHttp2DataFrame.Builder().setStreamId(STREAM_ID).setContent(buffer).build(); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/Http2ConnectionHandlerTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/Http2ConnectionHandlerTest.java new file mode 100644 index 0000000000..4632381e33 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/connection/Http2ConnectionHandlerTest.java @@ -0,0 +1,664 @@ +/* + * Copyright 2014 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.draft10.connection; + +import static io.netty.handler.codec.http2.draft10.Http2Error.PROTOCOL_ERROR; +import static io.netty.handler.codec.http2.draft10.frame.Http2FrameCodecUtil.PING_FRAME_PAYLOAD_LENGTH; +import static org.junit.Assert.assertEquals; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyBoolean; +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isNull; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.http2.draft10.Http2Error; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.Http2Headers; +import io.netty.handler.codec.http2.draft10.Http2StreamException; +import io.netty.handler.codec.http2.draft10.connection.InboundFlowController.FrameWriter; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2DataFrame; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2GoAwayFrame; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2HeadersFrame; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2PingFrame; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2PriorityFrame; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2PushPromiseFrame; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2RstStreamFrame; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2SettingsFrame; +import io.netty.handler.codec.http2.draft10.frame.DefaultHttp2WindowUpdateFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2DataFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2Frame; +import io.netty.handler.codec.http2.draft10.frame.Http2PingFrame; +import io.netty.handler.codec.http2.draft10.frame.Http2SettingsFrame; + +import java.nio.charset.Charset; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import com.google.common.collect.ImmutableList; + +/** + * Tests for {@link Http2ConnectionHandler}. + */ +public class Http2ConnectionHandlerTest { + private static final int STREAM_ID = 1; + private static final int PUSH_STREAM_ID = 2; + + private Http2ConnectionHandler handler; + + @Mock + private Http2Connection connection; + + @Mock + private Http2Connection.Endpoint remote; + + @Mock + private Http2Connection.Endpoint local; + + @Mock + private InboundFlowController inboundFlow; + + @Mock + private OutboundFlowController outboundFlow; + + @Mock + private ChannelHandlerContext ctx; + + @Mock + private Channel channel; + + @Mock + private ChannelPromise promise; + + @Mock + private ChannelFuture future; + + @Mock + private Http2Stream stream; + + @Mock + private Http2Stream pushStream; + + @Before + public void setup() throws Exception { + MockitoAnnotations.initMocks(this); + when(ctx.channel()).thenReturn(channel); + when(ctx.newSucceededFuture()).thenReturn(future); + when(ctx.newPromise()).thenReturn(promise); + when(channel.isActive()).thenReturn(true); + when(stream.getId()).thenReturn(STREAM_ID); + when(pushStream.getId()).thenReturn(PUSH_STREAM_ID); + when(connection.getActiveStreams()).thenReturn(ImmutableList.of(stream)); + when(connection.getStream(STREAM_ID)).thenReturn(stream); + when(connection.getStreamOrFail(STREAM_ID)).thenReturn(stream); + when(connection.local()).thenReturn(local); + when(connection.remote()).thenReturn(remote); + when(local.createStream(eq(STREAM_ID), anyInt(), anyBoolean())).thenReturn(stream); + when(local.reservePushStream(eq(PUSH_STREAM_ID), eq(stream))).thenReturn(pushStream); + when(remote.createStream(eq(STREAM_ID), anyInt(), anyBoolean())).thenReturn(stream); + when(remote.reservePushStream(eq(PUSH_STREAM_ID), eq(stream))).thenReturn(pushStream); + + handler = new Http2ConnectionHandler(connection, inboundFlow, outboundFlow); + } + + @Test + public void closeShouldSendGoAway() throws Exception { + handler.close(ctx, promise); + verify(connection).sendGoAway(eq(ctx), eq(promise), isNull(Http2Exception.class)); + } + + @Test + public void channelInactiveShouldCloseStreams() throws Exception { + handler.channelInactive(ctx); + verify(stream).close(eq(ctx), eq(future)); + verify(ctx).fireChannelInactive(); + } + + @Test + public void streamErrorShouldCloseStream() throws Exception { + Http2Exception e = new Http2StreamException(STREAM_ID, PROTOCOL_ERROR); + handler.exceptionCaught(ctx, e); + verify(stream).close(eq(ctx), eq(promise)); + verify(ctx).writeAndFlush(eq(createRstStreamFrame(STREAM_ID, PROTOCOL_ERROR)), eq(promise)); + verify(ctx).fireExceptionCaught(e); + } + + @Test + public void connectionErrorShouldSendGoAway() throws Exception { + Http2Exception e = new Http2Exception(PROTOCOL_ERROR); + handler.exceptionCaught(ctx, e); + verify(connection).sendGoAway(eq(ctx), eq(promise), eq(e)); + verify(ctx).fireExceptionCaught(e); + } + + @Test + public void inboundDataAfterGoAwayShouldApplyFlowControl() throws Exception { + when(connection.isGoAwaySent()).thenReturn(true); + ByteBuf data = Unpooled.copiedBuffer("Hello", Charset.defaultCharset()); + Http2DataFrame frame = + new DefaultHttp2DataFrame.Builder().setStreamId(STREAM_ID).setContent(data).build(); + handler.channelRead(ctx, frame); + assertEquals(0, data.refCnt()); + verify(inboundFlow).applyInboundFlowControl(eq(frame), any(FrameWriter.class)); + verify(ctx, never()).fireChannelRead(frame); + } + + @Test + public void inboundDataWithEndOfStreamShouldCloseRemoteSide() throws Exception { + ByteBuf data = Unpooled.copiedBuffer("Hello", Charset.defaultCharset()); + Http2DataFrame frame = + new DefaultHttp2DataFrame.Builder().setStreamId(STREAM_ID).setEndOfStream(true) + .setContent(data).build(); + handler.channelRead(ctx, frame); + assertEquals(1, data.refCnt()); + verify(inboundFlow).applyInboundFlowControl(eq(frame), any(FrameWriter.class)); + verify(stream).closeRemoteSide(eq(ctx), eq(future)); + verify(ctx).fireChannelRead(frame); + data.release(); + } + + @Test + public void inboundDataShouldSucceed() throws Exception { + ByteBuf data = Unpooled.copiedBuffer("Hello", Charset.defaultCharset()); + Http2DataFrame frame = + new DefaultHttp2DataFrame.Builder().setStreamId(STREAM_ID).setContent(data).build(); + handler.channelRead(ctx, frame); + assertEquals(1, data.refCnt()); + verify(inboundFlow).applyInboundFlowControl(eq(frame), any(FrameWriter.class)); + verify(stream, never()).closeRemoteSide(eq(ctx), eq(future)); + verify(ctx).fireChannelRead(frame); + data.release(); + } + + @Test + public void inboundHeadersAfterGoAwayShouldBeIgnored() throws Exception { + when(connection.isGoAwaySent()).thenReturn(true); + Http2Frame frame = + new DefaultHttp2HeadersFrame.Builder().setStreamId(STREAM_ID).setPriority(1) + .setHeaders(new Http2Headers.Builder().build()).build(); + handler.channelRead(ctx, frame); + verify(remote, never()).createStream(eq(STREAM_ID), eq(1), eq(false)); + verify(ctx, never()).fireChannelRead(frame); + } + + @Test + public void inboundHeadersShouldCreateStream() throws Exception { + int newStreamId = 5; + Http2Frame frame = + new DefaultHttp2HeadersFrame.Builder().setStreamId(newStreamId).setPriority(1) + .setHeaders(new Http2Headers.Builder().build()).build(); + handler.channelRead(ctx, frame); + verify(remote).createStream(eq(newStreamId), eq(1), eq(false)); + verify(ctx).fireChannelRead(frame); + } + + @Test + public void inboundHeadersWithEndOfStreamShouldCreateHalfClosedStream() throws Exception { + int newStreamId = 5; + Http2Frame frame = + new DefaultHttp2HeadersFrame.Builder().setStreamId(newStreamId).setPriority(1) + .setEndOfStream(true).setHeaders(new Http2Headers.Builder().build()) + .build(); + handler.channelRead(ctx, frame); + verify(remote).createStream(eq(newStreamId), eq(1), eq(true)); + verify(ctx).fireChannelRead(frame); + } + + @Test + public void inboundHeadersWithForPromisedStreamShouldHalfOpenStream() throws Exception { + Http2Frame frame = + new DefaultHttp2HeadersFrame.Builder().setStreamId(STREAM_ID).setPriority(1) + .setHeaders(new Http2Headers.Builder().build()).build(); + handler.channelRead(ctx, frame); + verify(stream).openForPush(); + verify(ctx).fireChannelRead(frame); + } + + @Test + public void inboundHeadersWithForPromisedStreamShouldCloseStream() throws Exception { + Http2Frame frame = + new DefaultHttp2HeadersFrame.Builder().setStreamId(STREAM_ID).setPriority(1) + .setEndOfStream(true).setHeaders(new Http2Headers.Builder().build()) + .build(); + handler.channelRead(ctx, frame); + verify(stream).openForPush(); + verify(stream).closeRemoteSide(ctx, future); + verify(ctx).fireChannelRead(frame); + } + + @Test + public void inboundPushPromiseAfterGoAwayShouldBeIgnored() throws Exception { + when(connection.isGoAwaySent()).thenReturn(true); + Http2Frame frame = + new DefaultHttp2PushPromiseFrame.Builder().setStreamId(STREAM_ID) + .setPromisedStreamId(PUSH_STREAM_ID) + .setHeaders(new Http2Headers.Builder().build()).build(); + handler.channelRead(ctx, frame); + verify(remote, never()).reservePushStream(eq(PUSH_STREAM_ID), eq(stream)); + verify(ctx, never()).fireChannelRead(frame); + } + + @Test + public void inboundPushPromiseShouldSucceed() throws Exception { + Http2Frame frame = + new DefaultHttp2PushPromiseFrame.Builder().setStreamId(STREAM_ID) + .setPromisedStreamId(PUSH_STREAM_ID) + .setHeaders(new Http2Headers.Builder().build()).build(); + handler.channelRead(ctx, frame); + verify(remote).reservePushStream(eq(PUSH_STREAM_ID), eq(stream)); + verify(ctx).fireChannelRead(frame); + } + + @Test + public void inboundPriorityAfterGoAwayShouldBeIgnored() throws Exception { + when(connection.isGoAwaySent()).thenReturn(true); + Http2Frame frame = + new DefaultHttp2PriorityFrame.Builder().setStreamId(STREAM_ID).setPriority(100) + .build(); + handler.channelRead(ctx, frame); + verify(stream, never()).setPriority(eq(100)); + verify(ctx, never()).fireChannelRead(frame); + } + + @Test + public void inboundPriorityForUnknownStreamShouldBeIgnored() throws Exception { + Http2Frame frame = + new DefaultHttp2PriorityFrame.Builder().setStreamId(5).setPriority(100).build(); + handler.channelRead(ctx, frame); + verify(stream, never()).setPriority(eq(100)); + verify(ctx, never()).fireChannelRead(frame); + } + + @Test + public void inboundPriorityShouldSucceed() throws Exception { + Http2Frame frame = + new DefaultHttp2PriorityFrame.Builder().setStreamId(STREAM_ID).setPriority(100) + .build(); + handler.channelRead(ctx, frame); + verify(stream).setPriority(eq(100)); + verify(ctx).fireChannelRead(frame); + } + + @Test + public void inboundWindowUpdateAfterGoAwayShouldBeIgnored() throws Exception { + when(connection.isGoAwaySent()).thenReturn(true); + Http2Frame frame = + new DefaultHttp2WindowUpdateFrame.Builder().setStreamId(STREAM_ID) + .setWindowSizeIncrement(10).build(); + handler.channelRead(ctx, frame); + verify(outboundFlow, never()).updateOutboundWindowSize(eq(STREAM_ID), eq(10)); + verify(ctx, never()).fireChannelRead(frame); + } + + @Test + public void inboundWindowUpdateForUnknownStreamShouldBeIgnored() throws Exception { + Http2Frame frame = + new DefaultHttp2WindowUpdateFrame.Builder().setStreamId(5) + .setWindowSizeIncrement(10).build(); + handler.channelRead(ctx, frame); + verify(outboundFlow, never()).updateOutboundWindowSize(eq(5), eq(10)); + verify(ctx, never()).fireChannelRead(frame); + } + + @Test + public void inboundWindowUpdateShouldSucceed() throws Exception { + Http2Frame frame = + new DefaultHttp2WindowUpdateFrame.Builder().setStreamId(STREAM_ID) + .setWindowSizeIncrement(10).build(); + handler.channelRead(ctx, frame); + verify(outboundFlow).updateOutboundWindowSize(eq(STREAM_ID), eq(10)); + verify(ctx).fireChannelRead(frame); + } + + @Test + public void inboundRstStreamAfterGoAwayShouldBeIgnored() throws Exception { + when(connection.isGoAwaySent()).thenReturn(true); + Http2Frame frame = + new DefaultHttp2RstStreamFrame.Builder().setStreamId(STREAM_ID) + .setErrorCode(PROTOCOL_ERROR.getCode()).build(); + handler.channelRead(ctx, frame); + verify(stream, never()).close(eq(ctx), eq(future)); + verify(ctx, never()).fireChannelRead(frame); + } + + @Test + public void inboundRstStreamForUnknownStreamShouldBeIgnored() throws Exception { + Http2Frame frame = + new DefaultHttp2RstStreamFrame.Builder().setStreamId(5) + .setErrorCode(PROTOCOL_ERROR.getCode()).build(); + handler.channelRead(ctx, frame); + verify(stream, never()).close(eq(ctx), eq(future)); + verify(ctx, never()).fireChannelRead(frame); + } + + @Test + public void inboundRstStreamShouldCloseStream() throws Exception { + Http2Frame frame = + new DefaultHttp2RstStreamFrame.Builder().setStreamId(STREAM_ID) + .setErrorCode(PROTOCOL_ERROR.getCode()).build(); + handler.channelRead(ctx, frame); + verify(stream).close(eq(ctx), eq(future)); + verify(ctx).fireChannelRead(frame); + } + + @Test + public void inboundPingWithAckShouldFireRead() throws Exception { + ByteBuf data = Unpooled.wrappedBuffer(new byte[PING_FRAME_PAYLOAD_LENGTH]); + Http2Frame frame = new DefaultHttp2PingFrame.Builder().setData(data).setAck(true).build(); + handler.channelRead(ctx, frame); + verify(ctx, never()).writeAndFlush(any(Http2PingFrame.class)); + verify(ctx).fireChannelRead(frame); + } + + @Test + public void inboundPingWithoutAckShouldReplyWithAck() throws Exception { + ByteBuf data = Unpooled.wrappedBuffer(new byte[PING_FRAME_PAYLOAD_LENGTH]); + Http2Frame frame = new DefaultHttp2PingFrame.Builder().setData(data).build(); + Http2Frame ack = new DefaultHttp2PingFrame.Builder().setData(data).setAck(true).build(); + handler.channelRead(ctx, frame); + verify(ctx).writeAndFlush(eq(ack)); + verify(ctx, never()).fireChannelRead(frame); + } + + @Test + public void inboundSettingsWithAckShouldFireRead() throws Exception { + Http2Frame frame = new DefaultHttp2SettingsFrame.Builder().setAck(true).build(); + handler.channelRead(ctx, frame); + verify(remote, never()).setPushToAllowed(anyBoolean()); + verify(remote, never()).setMaxStreams(anyInt()); + verify(outboundFlow, never()).setInitialOutboundWindowSize(anyInt()); + verify(ctx).fireChannelRead(frame); + verify(ctx, never()).writeAndFlush(any(Http2SettingsFrame.class)); + } + + @Test + public void inboundSettingsShouldSetValues() throws Exception { + Http2Frame frame = + new DefaultHttp2SettingsFrame.Builder().setPushEnabled(true) + .setMaxConcurrentStreams(10).setInitialWindowSize(20).build(); + handler.channelRead(ctx, frame); + verify(remote).setPushToAllowed(true); + verify(local).setMaxStreams(10); + verify(outboundFlow).setInitialOutboundWindowSize(20); + verify(ctx, never()).fireChannelRead(frame); + verify(ctx).writeAndFlush(eq(new DefaultHttp2SettingsFrame.Builder().setAck(true).build())); + } + + @Test + public void inboundGoAwayShouldUpdateConnectionState() throws Exception { + Http2Frame frame = + new DefaultHttp2GoAwayFrame.Builder().setLastStreamId(1).setErrorCode(2).build(); + handler.channelRead(ctx, frame); + verify(connection).goAwayReceived(); + verify(ctx).fireChannelRead(frame); + } + + @Test + public void outboundDataAfterGoAwayShouldFail() throws Exception { + when(connection.isGoAway()).thenReturn(true); + ByteBuf data = Unpooled.copiedBuffer("Hello", Charset.defaultCharset()); + Http2DataFrame frame = + new DefaultHttp2DataFrame.Builder().setStreamId(STREAM_ID).setContent(data).build(); + handler.write(ctx, frame, promise); + verify(outboundFlow, never()).sendFlowControlled(eq(frame), + any(OutboundFlowController.FrameWriter.class)); + verify(promise).setFailure(any(Http2Exception.class)); + assertEquals(0, frame.refCnt()); + } + + @Test + public void outboundDataShouldApplyFlowControl() throws Exception { + ByteBuf data = Unpooled.copiedBuffer("Hello", Charset.defaultCharset()); + Http2DataFrame frame = + new DefaultHttp2DataFrame.Builder().setStreamId(STREAM_ID).setContent(data).build(); + handler.write(ctx, frame, promise); + verify(outboundFlow).sendFlowControlled(eq(frame), + any(OutboundFlowController.FrameWriter.class)); + verify(promise, never()).setFailure(any(Http2Exception.class)); + assertEquals(1, frame.refCnt()); + frame.release(); + } + + @Test + public void outboundHeadersAfterGoAwayShouldFail() throws Exception { + when(connection.isGoAway()).thenReturn(true); + Http2Frame frame = + new DefaultHttp2HeadersFrame.Builder().setStreamId(STREAM_ID).setPriority(1) + .setHeaders(new Http2Headers.Builder().build()).build(); + handler.write(ctx, frame, promise); + verify(promise).setFailure(any(Http2Exception.class)); + verify(ctx, never()).writeAndFlush(frame, promise); + } + + @Test + public void outboundHeadersShouldCreateOpenStream() throws Exception { + int newStreamId = 5; + Http2Frame frame = + new DefaultHttp2HeadersFrame.Builder().setStreamId(newStreamId).setPriority(1) + .setHeaders(new Http2Headers.Builder().build()).build(); + handler.write(ctx, frame, promise); + verify(local).createStream(eq(newStreamId), eq(1), eq(false)); + verify(promise, never()).setFailure(any(Http2Exception.class)); + verify(ctx).writeAndFlush(frame, promise); + } + + @Test + public void outboundHeadersShouldCreateHalfClosedStream() throws Exception { + int newStreamId = 5; + Http2Frame frame = + new DefaultHttp2HeadersFrame.Builder().setStreamId(newStreamId).setPriority(1) + .setEndOfStream(true).setHeaders(new Http2Headers.Builder().build()) + .build(); + handler.write(ctx, frame, promise); + verify(local).createStream(eq(newStreamId), eq(1), eq(true)); + verify(promise, never()).setFailure(any(Http2Exception.class)); + verify(ctx).writeAndFlush(frame, promise); + } + + @Test + public void outboundHeadersShouldOpenStreamForPush() throws Exception { + Http2Frame frame = + new DefaultHttp2HeadersFrame.Builder().setStreamId(STREAM_ID).setPriority(1) + .setHeaders(new Http2Headers.Builder().build()).build(); + handler.write(ctx, frame, promise); + verify(stream).openForPush(); + verify(stream, never()).closeLocalSide(eq(ctx), eq(future)); + verify(promise, never()).setFailure(any(Http2Exception.class)); + verify(ctx).writeAndFlush(frame, promise); + } + + @Test + public void outboundHeadersShouldClosePushStream() throws Exception { + Http2Frame frame = + new DefaultHttp2HeadersFrame.Builder().setStreamId(STREAM_ID).setPriority(1) + .setEndOfStream(true).setHeaders(new Http2Headers.Builder().build()) + .build(); + handler.write(ctx, frame, promise); + verify(stream).openForPush(); + verify(stream).closeLocalSide(eq(ctx), eq(promise)); + verify(promise, never()).setFailure(any(Http2Exception.class)); + verify(ctx).writeAndFlush(frame, promise); + } + + @Test + public void outboundPushPromiseAfterGoAwayShouldFail() throws Exception { + when(connection.isGoAway()).thenReturn(true); + Http2Frame frame = + new DefaultHttp2PushPromiseFrame.Builder().setStreamId(STREAM_ID) + .setPromisedStreamId(PUSH_STREAM_ID) + .setHeaders(new Http2Headers.Builder().build()).build(); + handler.write(ctx, frame, promise); + verify(promise).setFailure(any(Http2Exception.class)); + verify(local, never()).reservePushStream(eq(PUSH_STREAM_ID), eq(stream)); + verify(ctx, never()).writeAndFlush(frame, promise); + } + + @Test + public void outboundPushPromiseShouldReserveStream() throws Exception { + Http2Frame frame = + new DefaultHttp2PushPromiseFrame.Builder().setStreamId(STREAM_ID) + .setPromisedStreamId(PUSH_STREAM_ID) + .setHeaders(new Http2Headers.Builder().build()).build(); + handler.write(ctx, frame, promise); + verify(promise, never()).setFailure(any(Http2Exception.class)); + verify(local).reservePushStream(eq(PUSH_STREAM_ID), eq(stream)); + verify(ctx).writeAndFlush(frame, promise); + } + + @Test + public void outboundPriorityAfterGoAwayShouldFail() throws Exception { + when(connection.isGoAway()).thenReturn(true); + Http2Frame frame = + new DefaultHttp2PriorityFrame.Builder().setStreamId(STREAM_ID).setPriority(10) + .build(); + handler.write(ctx, frame, promise); + verify(promise).setFailure(any(Http2Exception.class)); + verify(stream, never()).setPriority(10); + verify(ctx, never()).writeAndFlush(frame, promise); + } + + @Test + public void outboundPriorityShouldSetPriorityOnStream() throws Exception { + Http2Frame frame = + new DefaultHttp2PriorityFrame.Builder().setStreamId(STREAM_ID).setPriority(10) + .build(); + handler.write(ctx, frame, promise); + verify(promise, never()).setFailure(any(Http2Exception.class)); + verify(stream).setPriority(10); + verify(ctx).writeAndFlush(frame, promise); + } + + @Test + public void outboundRstStreamForUnknownStreamShouldIgnore() throws Exception { + Http2Frame frame = + new DefaultHttp2RstStreamFrame.Builder().setStreamId(5).setErrorCode(1).build(); + handler.write(ctx, frame, promise); + verify(promise, never()).setFailure(any(Http2Exception.class)); + verify(promise).setSuccess(); + verify(ctx, never()).writeAndFlush(frame, promise); + } + + @Test + public void outboundRstStreamShouldCloseStream() throws Exception { + Http2Frame frame = + new DefaultHttp2RstStreamFrame.Builder().setStreamId(STREAM_ID).setErrorCode(1) + .build(); + handler.write(ctx, frame, promise); + verify(promise, never()).setFailure(any(Http2Exception.class)); + verify(promise, never()).setSuccess(); + verify(stream).close(ctx, promise); + verify(ctx).writeAndFlush(frame, promise); + } + + @Test + public void outboundPingAfterGoAwayShouldFail() throws Exception { + when(connection.isGoAway()).thenReturn(true); + ByteBuf data = Unpooled.wrappedBuffer(new byte[PING_FRAME_PAYLOAD_LENGTH]); + Http2Frame frame = new DefaultHttp2PingFrame.Builder().setData(data).build(); + handler.write(ctx, frame, promise); + verify(promise).setFailure(any(Http2Exception.class)); + verify(ctx, never()).writeAndFlush(frame, promise); + } + + @Test + public void outboundPingWithAckShouldFail() throws Exception { + ByteBuf data = Unpooled.wrappedBuffer(new byte[PING_FRAME_PAYLOAD_LENGTH]); + Http2Frame frame = new DefaultHttp2PingFrame.Builder().setAck(true).setData(data).build(); + handler.write(ctx, frame, promise); + verify(promise).setFailure(any(Http2Exception.class)); + verify(ctx, never()).writeAndFlush(frame, promise); + } + + @Test + public void outboundPingWithAckShouldSend() throws Exception { + ByteBuf data = Unpooled.wrappedBuffer(new byte[PING_FRAME_PAYLOAD_LENGTH]); + Http2Frame frame = new DefaultHttp2PingFrame.Builder().setData(data).build(); + handler.write(ctx, frame, promise); + verify(promise, never()).setFailure(any(Http2Exception.class)); + verify(ctx).writeAndFlush(frame, promise); + } + + @Test + public void outboundGoAwayShouldFail() throws Exception { + Http2Frame frame = + new DefaultHttp2GoAwayFrame.Builder().setLastStreamId(0).setErrorCode(1).build(); + handler.write(ctx, frame, promise); + verify(promise).setFailure(any(Http2Exception.class)); + } + + @Test + public void outboundWindowUpdateShouldFail() throws Exception { + Http2Frame frame = + new DefaultHttp2WindowUpdateFrame.Builder().setStreamId(STREAM_ID) + .setWindowSizeIncrement(1).build(); + handler.write(ctx, frame, promise); + verify(promise).setFailure(any(Http2Exception.class)); + } + + @Test + public void outboundSettingsAfterGoAwayShouldFail() throws Exception { + when(connection.isGoAway()).thenReturn(true); + Http2Frame frame = + new DefaultHttp2SettingsFrame.Builder().setInitialWindowSize(10) + .setMaxConcurrentStreams(20).setPushEnabled(true).build(); + handler.write(ctx, frame, promise); + verify(promise).setFailure(any(Http2Exception.class)); + verify(local, never()).setPushToAllowed(anyBoolean()); + verify(remote, never()).setMaxStreams(anyInt()); + verify(inboundFlow, never()).setInitialInboundWindowSize(anyInt()); + verify(ctx, never()).writeAndFlush(frame, promise); + } + + @Test + public void outboundSettingsWithAckShouldFail() throws Exception { + when(connection.isGoAway()).thenReturn(true); + Http2Frame frame = new DefaultHttp2SettingsFrame.Builder().setAck(true).build(); + handler.write(ctx, frame, promise); + verify(promise).setFailure(any(Http2Exception.class)); + verify(local, never()).setPushToAllowed(anyBoolean()); + verify(remote, never()).setMaxStreams(anyInt()); + verify(inboundFlow, never()).setInitialInboundWindowSize(anyInt()); + verify(ctx, never()).writeAndFlush(frame, promise); + } + + @Test + public void outboundSettingsShouldUpdateSettings() throws Exception { + Http2Frame frame = + new DefaultHttp2SettingsFrame.Builder().setInitialWindowSize(10) + .setMaxConcurrentStreams(20).setPushEnabled(true).build(); + handler.write(ctx, frame, promise); + verify(promise, never()).setFailure(any(Http2Exception.class)); + verify(local).setPushToAllowed(eq(true)); + verify(remote).setMaxStreams(eq(20)); + verify(inboundFlow).setInitialInboundWindowSize(eq(10)); + verify(ctx).writeAndFlush(frame, promise); + } + + private DefaultHttp2RstStreamFrame createRstStreamFrame(int streamId, Http2Error error) { + return new DefaultHttp2RstStreamFrame.Builder().setStreamId(streamId) + .setErrorCode(error.getCode()).build(); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/frame/HeaderBlockRoundtripTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/frame/HeaderBlockRoundtripTest.java new file mode 100644 index 0000000000..2e34a57ccc --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/frame/HeaderBlockRoundtripTest.java @@ -0,0 +1,92 @@ +/* + * Copyright 2014 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.draft10.frame; + +import static org.junit.Assert.assertEquals; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http2.draft10.Http2Exception; +import io.netty.handler.codec.http2.draft10.Http2Headers; +import io.netty.handler.codec.http2.draft10.frame.decoder.DefaultHttp2HeadersDecoder; +import io.netty.handler.codec.http2.draft10.frame.encoder.DefaultHttp2HeadersEncoder; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Tests for encoding/decoding HTTP2 header blocks. + */ +public class HeaderBlockRoundtripTest { + + private DefaultHttp2HeadersDecoder decoder; + private DefaultHttp2HeadersEncoder encoder; + private ByteBuf buffer; + + @Before + public void setup() { + encoder = new DefaultHttp2HeadersEncoder(); + decoder = new DefaultHttp2HeadersDecoder(); + buffer = Unpooled.buffer(); + } + + @After + public void teardown() { + buffer.release(); + } + + @Test + public void roundtripShouldBeSuccessful() throws Http2Exception { + Http2Headers in = + new Http2Headers.Builder().setMethod("GET").setScheme("https") + .setAuthority("example.org").setPath("/some/path/resource2") + .addHeader("accept", "image/png").addHeader("cache-control", "no-cache") + .addHeader("custom", "value1").addHeader("custom", "value2") + .addHeader("custom", "value3").addHeader("custom", "custom4").build(); + assertRoundtripSuccessful(in); + } + + @Test + public void successiveCallsShouldSucceed() throws Http2Exception { + Http2Headers in = + new Http2Headers.Builder().setMethod("GET").setScheme("https") + .setAuthority("example.org").setPath("/some/path") + .addHeader("accept", "*/*").build(); + assertRoundtripSuccessful(in); + + in = + new Http2Headers.Builder().setMethod("GET").setScheme("https") + .setAuthority("example.org").setPath("/some/path/resource1") + .addHeader("accept", "image/jpeg").addHeader("cache-control", "no-cache") + .build(); + assertRoundtripSuccessful(in); + + in = + new Http2Headers.Builder().setMethod("GET").setScheme("https") + .setAuthority("example.org").setPath("/some/path/resource2") + .addHeader("accept", "image/png").addHeader("cache-control", "no-cache") + .build(); + assertRoundtripSuccessful(in); + } + + private void assertRoundtripSuccessful(Http2Headers in) throws Http2Exception { + encoder.encodeHeaders(in, buffer); + + Http2Headers out = decoder.decodeHeaders(buffer); + assertEquals(in, out); + } +} diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameRoundtripTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameRoundtripTest.java new file mode 100644 index 0000000000..45c140af0e --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/draft10/frame/Http2FrameRoundtripTest.java @@ -0,0 +1,317 @@ +/* + * Copyright 2014 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.draft10.frame; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http2.draft10.Http2Headers; +import io.netty.util.NetUtil; +import io.netty.util.ReferenceCountUtil; + +import java.net.InetSocketAddress; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; + +import com.google.common.base.Charsets; + +/** + * Tests encoding/decoding each HTTP2 frame type. + */ +public class Http2FrameRoundtripTest { + + private static final EventLoopGroup group = new NioEventLoopGroup(); + + private CaptureHandler captureHandler; + private ServerBootstrap sb; + private Bootstrap cb; + private Channel serverChannel; + private Channel clientChannel; + + @Before + public void setup() throws Exception { + captureHandler = new CaptureHandler(); + sb = new ServerBootstrap(); + cb = new Bootstrap(); + + sb.group(new NioEventLoopGroup(), new NioEventLoopGroup()); + sb.channel(NioServerSocketChannel.class); + sb.childHandler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast("codec", new Http2FrameCodec()); + p.addLast("handler", captureHandler); + } + }); + + cb.group(new NioEventLoopGroup()); + cb.channel(NioSocketChannel.class); + cb.handler(new ChannelInitializer() { + @Override + protected void initChannel(Channel ch) throws Exception { + ChannelPipeline p = ch.pipeline(); + p.addLast("codec", new Http2FrameCodec()); + } + }); + + serverChannel = sb.bind(new InetSocketAddress(0)).sync().channel(); + int port = ((InetSocketAddress) serverChannel.localAddress()).getPort(); + + ChannelFuture ccf = cb.connect(new InetSocketAddress(NetUtil.LOCALHOST, port)); + assertTrue(ccf.awaitUninterruptibly().isSuccess()); + clientChannel = ccf.channel(); + } + + @After + public void teardown() throws Exception { + serverChannel.close().sync(); + sb.group().shutdownGracefully(); + cb.group().shutdownGracefully(); + } + + @AfterClass + public static void destroy() throws Exception { + group.shutdownGracefully().sync(); + } + + @Test + public void dataFrameShouldMatch() throws Exception { + String text = "hello world"; + Http2DataFrame in = + new DefaultHttp2DataFrame.Builder() + .setContent(Unpooled.copiedBuffer(text.getBytes())).setEndOfStream(true) + .setStreamId(0x7FFFFFFF).setPaddingLength(100).build().retain(); + + Http2Frame out = sendAndWaitForFrame(clientChannel, in, captureHandler); + assertAndReleaseFrames(in, out); + } + + @Test + public void headersFrameWithoutPriorityShouldMatch() throws Exception { + Http2Headers headers = + new Http2Headers.Builder().setMethod("GET").setScheme("https") + .setAuthority("example.org").setPath("/some/path/resource2").build(); + Http2HeadersFrame in = + new DefaultHttp2HeadersFrame.Builder().setHeaders(headers).setEndOfStream(true) + .setStreamId(0x7FFFFFFF).build(); + + Http2Frame out = sendAndWaitForFrame(clientChannel, in, captureHandler); + assertAndReleaseFrames(in, out); + } + + @Test + public void headersFrameWithPriorityShouldMatch() throws Exception { + Http2Headers headers = + new Http2Headers.Builder().setMethod("GET").setScheme("https") + .setAuthority("example.org").setPath("/some/path/resource2").build(); + Http2HeadersFrame in = + new DefaultHttp2HeadersFrame.Builder().setHeaders(headers).setEndOfStream(true) + .setStreamId(0x7FFFFFFF).setPriority(0x7FFFFFFF).build(); + + Http2Frame out = sendAndWaitForFrame(clientChannel, in, captureHandler); + assertAndReleaseFrames(in, out); + } + + @Test + public void goAwayFrameShouldMatch() throws Exception { + String text = "test"; + Http2GoAwayFrame in = + new DefaultHttp2GoAwayFrame.Builder() + .setDebugData(Unpooled.copiedBuffer(text.getBytes())) + .setLastStreamId(0x7FFFFFFF).setErrorCode(0xFFFFFFFFL).build().retain(); + + Http2Frame out = sendAndWaitForFrame(clientChannel, in, captureHandler); + assertAndReleaseFrames(in, out); + } + + @Test + public void pingFrameShouldMatch() throws Exception { + ByteBuf buf = Unpooled.copiedBuffer("01234567", Charsets.UTF_8); + + Http2PingFrame in = + new DefaultHttp2PingFrame.Builder().setAck(true).setData(buf).build().retain(); + + Http2Frame out = sendAndWaitForFrame(clientChannel, in, captureHandler); + assertAndReleaseFrames(in, out); + } + + @Test + public void priorityFrameShouldMatch() throws Exception { + Http2PriorityFrame in = + new DefaultHttp2PriorityFrame.Builder().setStreamId(0x7FFFFFFF) + .setPriority(0x7FFFFFFF).build(); + + Http2Frame out = sendAndWaitForFrame(clientChannel, in, captureHandler); + assertEquals(in, out); + } + + @Test + public void pushPromiseFrameShouldMatch() throws Exception { + Http2Headers headers = + new Http2Headers.Builder().setMethod("GET").setScheme("https") + .setAuthority("example.org").setPath("/some/path/resource2").build(); + Http2PushPromiseFrame in = + new DefaultHttp2PushPromiseFrame.Builder().setHeaders(headers) + .setStreamId(0x7FFFFFFF).setPromisedStreamId(0x7FFFFFFF).build(); + + Http2Frame out = sendAndWaitForFrame(clientChannel, in, captureHandler); + assertAndReleaseFrames(in, out); + } + + @Test + public void rstStreamFrameShouldMatch() throws Exception { + Http2RstStreamFrame in = + new DefaultHttp2RstStreamFrame.Builder().setStreamId(0x7FFFFFFF) + .setErrorCode(0xFFFFFFFFL).build(); + + Http2Frame out = sendAndWaitForFrame(clientChannel, in, captureHandler); + assertEquals(in, out); + } + + @Test + public void settingsFrameShouldMatch() throws Exception { + Http2SettingsFrame in = + new DefaultHttp2SettingsFrame.Builder().setAck(false).setHeaderTableSize(1) + .setInitialWindowSize(Integer.MAX_VALUE).setPushEnabled(true) + .setMaxConcurrentStreams(100L).build(); + + Http2Frame out = sendAndWaitForFrame(clientChannel, in, captureHandler); + assertEquals(in, out); + } + + @Test + public void windowUpdateFrameShouldMatch() throws Exception { + Http2WindowUpdateFrame in = + new DefaultHttp2WindowUpdateFrame.Builder().setStreamId(0x7FFFFFFF) + .setWindowSizeIncrement(0x7FFFFFFF).build(); + + Http2Frame out = sendAndWaitForFrame(clientChannel, in, captureHandler); + assertEquals(in, out); + } + + @Test + public void stressTest() throws Exception { + Http2Headers headers = + new Http2Headers.Builder().setMethod("GET").setScheme("https") + .setAuthority("example.org").setPath("/some/path/resource2").build(); + String text = "hello world"; + int numStreams = 1000; + for (int i = 1; i < numStreams + 1; ++i) { + Http2HeadersFrame headersFrame = + new DefaultHttp2HeadersFrame.Builder().setHeaders(headers).setStreamId(i) + .build(); + + Http2DataFrame dataFrame = + new DefaultHttp2DataFrame.Builder() + .setContent(Unpooled.copiedBuffer(text.getBytes())) + .setEndOfStream(true).setStreamId(i).setPaddingLength(100).build() + .retain(); + + clientChannel.writeAndFlush(headersFrame); + clientChannel.writeAndFlush(dataFrame); + } + + // Wait for all frames to be received. + long theFuture = System.currentTimeMillis() + 5000; + int expectedFrames = numStreams * 2; + while (captureHandler.count < expectedFrames && System.currentTimeMillis() < theFuture) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { // Ignore. + } + } + assertEquals(expectedFrames, captureHandler.count); + captureHandler.release(); + } + + private void assertAndReleaseFrames(Http2Frame in, Http2Frame out) { + assertEquals(in, out); + if (in instanceof ByteBufHolder) { + assertEquals(1, ((ByteBufHolder) in).refCnt()); + ((ByteBufHolder) in).release(); + } + if (out instanceof ByteBufHolder) { + assertEquals(1, ((ByteBufHolder) out).refCnt()); + ((ByteBufHolder) out).release(); + } + } + + private static Http2Frame sendAndWaitForFrame(Channel cc, Http2Frame frame, + CaptureHandler captureHandler) { + cc.writeAndFlush(frame); + long theFuture = System.currentTimeMillis() + 3000; + while (captureHandler.frame == null && System.currentTimeMillis() < theFuture) { + try { + Thread.sleep(1); + } catch (InterruptedException e) { // Ignore. + } + } + assertNotNull("not null frame", captureHandler.frame); + return captureHandler.frame; + } + + private static class CaptureHandler extends ChannelHandlerAdapter { + public volatile Http2Frame frame; + public volatile int count; + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + // Release any allocated data for the previous frame if there was one. + release(); + + // Copy the frame if it contains allocated data. + if (msg instanceof ByteBufHolder) { + ByteBufHolder holder = (ByteBufHolder) msg; + msg = holder.copy(); + holder.release(); + } + + this.frame = (Http2Frame) msg; + count++; + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { + cause.printStackTrace(); + } + + public void release() { + if (frame != null) { + ReferenceCountUtil.release(frame); + frame = null; + } + } + } +} diff --git a/pom.xml b/pom.xml index 5bf2d50c6b..99c0eb214e 100644 --- a/pom.xml +++ b/pom.xml @@ -157,6 +157,7 @@ buffer codec codec-http + codec-http2 codec-memcache codec-socks transport