Repository: twitter/netty-http2 Branch: master Commit: e8df896db610 Files: 54 Total size: 303.3 KB Directory structure: gitextract_lusmlr8n/ ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml └── src/ ├── main/ │ └── java/ │ └── com/ │ └── twitter/ │ └── http2/ │ ├── DefaultHttp2Headers.java │ ├── DefaultHttpDataFrame.java │ ├── DefaultHttpGoAwayFrame.java │ ├── DefaultHttpHeaderBlockFrame.java │ ├── DefaultHttpHeadersFrame.java │ ├── DefaultHttpPingFrame.java │ ├── DefaultHttpPriorityFrame.java │ ├── DefaultHttpPushPromiseFrame.java │ ├── DefaultHttpRstStreamFrame.java │ ├── DefaultHttpSettingsFrame.java │ ├── DefaultHttpStreamFrame.java │ ├── DefaultHttpWindowUpdateFrame.java │ ├── HttpCodecUtil.java │ ├── HttpConnection.java │ ├── HttpConnectionHandler.java │ ├── HttpDataFrame.java │ ├── HttpErrorCode.java │ ├── HttpFrame.java │ ├── HttpFrameDecoder.java │ ├── HttpFrameDecoderDelegate.java │ ├── HttpFrameEncoder.java │ ├── HttpGoAwayFrame.java │ ├── HttpHeaderBlockDecoder.java │ ├── HttpHeaderBlockEncoder.java │ ├── HttpHeaderBlockFrame.java │ ├── HttpHeadersFrame.java │ ├── HttpMessageProxy.java │ ├── HttpPingFrame.java │ ├── HttpPriorityFrame.java │ ├── HttpProtocolException.java │ ├── HttpPushPromiseFrame.java │ ├── HttpRequestProxy.java │ ├── HttpResponseProxy.java │ ├── HttpRstStreamFrame.java │ ├── HttpSettingsFrame.java │ ├── HttpStreamDecoder.java │ ├── HttpStreamEncoder.java │ ├── HttpStreamFrame.java │ ├── HttpWindowUpdateFrame.java │ ├── Pipe.java │ ├── StreamedHttpMessage.java │ ├── StreamedHttpRequest.java │ └── StreamedHttpResponse.java └── test/ └── java/ └── com/ └── twitter/ └── http2/ ├── HttpFrameDecoderTest.java ├── HttpFrameEncoderTest.java ├── HttpHeaderCompressionTest.java ├── HttpRequestProxyTest.java ├── HttpResponseProxyTest.java └── PipeTest.java ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ target .idea *.iml ================================================ FILE: .travis.yml ================================================ language: java sudo: false jdk: - openjdk7 after_success: - mvn clean cobertura:cobertura coveralls:cobertura ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # netty-http2 [![Build Status](https://travis-ci.org/twitter/netty-http2.svg)](https://travis-ci.org/twitter/netty-http2) Legacy HTTP/2 codec for Netty. Use the codec shipped with Netty 4.1 instead https://github.com/netty/netty/tree/4.1/codec-http2 ================================================ FILE: pom.xml ================================================ 4.0.0 com.twitter netty-http2 0.17.12-SNAPSHOT HTTP/2 http://github.com/twitter/netty-http2 HTTP/2 for Netty scm:git:git@github.com:twitter/netty-http2.git scm:git:git@github.com:twitter/netty-http2.git scm:git:git@github.com:twitter/netty-http2.git The Apache Software License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0.txt Jeff Pinner jpinner@twitter.com 1.6 1.6 UTF-8 UTF-8 sonatype-nexus-snapshots Sonatype OSS https://oss.sonatype.org/content/repositories/snapshots sonatype-nexus-staging Nexus Release Repository https://oss.sonatype.org/service/local/staging/deploy/maven2/ com.twitter hpack 1.0.2 io.netty netty-codec-http 4.1.0.CR4 junit junit 4.12 test org.mockito mockito-core 1.9.5 test sonatype-nexus-snapshots https://oss.sonatype.org/content/repositories/snapshots false true org.apache.maven.plugins maven-release-plugin 2.1 forked-path false -Psonatype-oss-release org.apache.maven.plugins maven-compiler-plugin 2.5.1 1.6 1.6 org.apache.maven.plugins maven-surefire-plugin 2.12 -Xmx1024m false **/Test*.java **/*Test.java **/*Spec.java org.codehaus.mojo cobertura-maven-plugin 2.5.2 xml 256m true org.eluder.coveralls coveralls-maven-plugin 2.2.0 sonatype-oss-release org.apache.maven.plugins maven-source-plugin 2.1.2 attach-sources jar-no-fork org.apache.maven.plugins maven-javadoc-plugin 2.7 attach-javadocs jar org.apache.maven.plugins maven-gpg-plugin 1.1 sign-artifacts verify sign ================================================ FILE: src/main/java/com/twitter/http2/DefaultHttp2Headers.java ================================================ package com.twitter.http2; import io.netty.handler.codec.DefaultHeaders.NameValidator; import io.netty.handler.codec.http.DefaultHttpHeaders; import io.netty.util.AsciiString; import io.netty.util.ByteProcessor; import io.netty.util.internal.PlatformDependent; import static io.netty.util.internal.ObjectUtil.checkNotNull; public class DefaultHttp2Headers extends DefaultHttpHeaders { private static final ByteProcessor HEADER_NAME_VALIDATOR = new ByteProcessor() { @Override public boolean process(byte value) throws Exception { validateChar((char) (value & 0xFF)); return true; } }; private static void validateChar(char character) { switch (character) { case '\t': case '\n': case 0x0b: case '\f': case '\r': case ' ': case ',': case ';': case '=': throw new IllegalArgumentException( "a header name cannot contain the following prohibited characters: =,; \\t\\r\\n\\v\\f: " + character); default: // Check to see if the character is not an ASCII character, or invalid if (character > 127) { throw new IllegalArgumentException("a header name cannot contain non-ASCII character: " + character); } } } static final NameValidator Http2NameValidator = new NameValidator() { @Override public void validateName(CharSequence name) { if (name instanceof AsciiString) { try { ((AsciiString) name).forEachByte(HEADER_NAME_VALIDATOR); } catch (Exception e) { PlatformDependent.throwException(e); } } else { checkNotNull(name, "name"); // Go through each character in the name for (int index = 0; index < name.length(); ++index) { validateChar(name.charAt(index)); } } } }; public DefaultHttp2Headers() { super(true, Http2NameValidator); } } ================================================ FILE: src/main/java/com/twitter/http2/DefaultHttpDataFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; import io.netty.buffer.Unpooled; import io.netty.util.IllegalReferenceCountException; import io.netty.util.internal.StringUtil; /** * The default {@link HttpDataFrame} implementation. */ public class DefaultHttpDataFrame extends DefaultHttpStreamFrame implements HttpDataFrame { private final ByteBuf data; private boolean last; /** * Creates a new instance. * * @param streamId the stream identifier of this frame */ public DefaultHttpDataFrame(int streamId) { this(streamId, Unpooled.buffer(0)); } /** * Creates a new instance. * * @param streamId the stream identifier of this frame * @param data the payload of the frame. Can not exceed {@link HttpCodecUtil#HTTP_MAX_LENGTH} */ public DefaultHttpDataFrame(int streamId, ByteBuf data) { super(streamId); if (data == null) { throw new NullPointerException("data"); } this.data = validate(data); } private static ByteBuf validate(ByteBuf data) { if (data.readableBytes() > HttpCodecUtil.HTTP_MAX_LENGTH) { throw new IllegalArgumentException("data payload cannot exceed " + HttpCodecUtil.HTTP_MAX_LENGTH + " bytes"); } return data; } @Override public boolean isLast() { return last; } @Override public HttpDataFrame setLast(boolean last) { this.last = last; return this; } @Override public HttpDataFrame setStreamId(int streamId) { super.setStreamId(streamId); return this; } @Override public ByteBuf content() { if (data.refCnt() <= 0) { throw new IllegalReferenceCountException(data.refCnt()); } return data; } @Override public HttpDataFrame copy() { HttpDataFrame frame = new DefaultHttpDataFrame(getStreamId(), content().copy()); frame.setLast(isLast()); return frame; } @Override public HttpDataFrame duplicate() { HttpDataFrame frame = new DefaultHttpDataFrame(getStreamId(), content().duplicate()); frame.setLast(isLast()); return frame; } @Override public int refCnt() { return data.refCnt(); } @Override public HttpDataFrame retain() { data.retain(); return this; } @Override public HttpDataFrame retain(int increment) { data.retain(increment); return this; } @Override public boolean release() { return data.release(); } @Override public boolean release(int decrement) { return data.release(decrement); } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(StringUtil.simpleClassName(this)); buf.append("(last: "); buf.append(isLast()); buf.append(')'); buf.append(StringUtil.NEWLINE); buf.append("--> Stream-ID = "); buf.append(getStreamId()); buf.append(StringUtil.NEWLINE); buf.append("--> Size = "); if (refCnt() == 0) { buf.append("(freed)"); } else { buf.append(content().readableBytes()); } return buf.toString(); } @Override public ByteBufHolder touch() { data.touch(); return this; } @Override public ByteBufHolder touch(Object o) { data.touch(o); return this; } } ================================================ FILE: src/main/java/com/twitter/http2/DefaultHttpGoAwayFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.util.internal.StringUtil; /** * The default {@link HttpGoAwayFrame} implementation. */ public class DefaultHttpGoAwayFrame implements HttpGoAwayFrame { private int lastStreamId; private HttpErrorCode errorCode; /** * Creates a new instance. * * @param lastStreamId the Last-Stream-ID of this frame * @param code the error code of this frame */ public DefaultHttpGoAwayFrame(int lastStreamId, int code) { this(lastStreamId, HttpErrorCode.valueOf(code)); } /** * Creates a new instance. * * @param lastStreamId the Last-Stream-ID of this frame * @param errorCode the error code of this frame */ public DefaultHttpGoAwayFrame(int lastStreamId, HttpErrorCode errorCode) { setLastStreamId(lastStreamId); setErrorCode(errorCode); } @Override public int getLastStreamId() { return lastStreamId; } @Override public HttpGoAwayFrame setLastStreamId(int lastStreamId) { if (lastStreamId < 0) { throw new IllegalArgumentException( "Last-Stream-ID cannot be negative: " + lastStreamId); } this.lastStreamId = lastStreamId; return this; } @Override public HttpErrorCode getErrorCode() { return errorCode; } @Override public HttpGoAwayFrame setErrorCode(HttpErrorCode errorCode) { this.errorCode = errorCode; return this; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(StringUtil.simpleClassName(this)); buf.append(StringUtil.NEWLINE); buf.append("--> Last-Stream-ID = "); buf.append(getLastStreamId()); buf.append(StringUtil.NEWLINE); buf.append("--> Error Code: "); buf.append(getErrorCode().toString()); return buf.toString(); } } ================================================ FILE: src/main/java/com/twitter/http2/DefaultHttpHeaderBlockFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.handler.codec.http.HttpHeaders; import io.netty.util.internal.StringUtil; import java.util.Map; /** * The default {@link HttpHeaderBlockFrame} implementation. */ public abstract class DefaultHttpHeaderBlockFrame extends DefaultHttpStreamFrame implements HttpHeaderBlockFrame { private boolean invalid; private boolean truncated; private final HttpHeaders headers = new DefaultHttp2Headers(); /** * Creates a new instance. * * @param streamId the stream identifier of this frame */ protected DefaultHttpHeaderBlockFrame(int streamId) { super(streamId); } @Override public boolean isInvalid() { return invalid; } @Override public HttpHeaderBlockFrame setInvalid() { invalid = true; return this; } @Override public boolean isTruncated() { return truncated; } @Override public HttpHeaderBlockFrame setTruncated() { truncated = true; return this; } @Override public HttpHeaders headers() { return headers; } protected void appendHeaders(StringBuilder buf) { for (Map.Entry e : headers()) { buf.append(" "); buf.append(e.getKey()); buf.append(": "); buf.append(e.getValue()); buf.append(StringUtil.NEWLINE); } } } ================================================ FILE: src/main/java/com/twitter/http2/DefaultHttpHeadersFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.util.internal.StringUtil; import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_DEPENDENCY; import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_WEIGHT; /** * The default {@link HttpHeadersFrame} implementation. */ public class DefaultHttpHeadersFrame extends DefaultHttpHeaderBlockFrame implements HttpHeadersFrame { private boolean last; private boolean exclusive = false; private int dependency = HTTP_DEFAULT_DEPENDENCY; private int weight = HTTP_DEFAULT_WEIGHT; /** * Creates a new instance. * * @param streamId the stream identifier of this frame */ public DefaultHttpHeadersFrame(int streamId) { super(streamId); } @Override public boolean isLast() { return last; } @Override public HttpHeadersFrame setLast(boolean last) { this.last = last; return this; } @Override public boolean isExclusive() { return exclusive; } @Override public HttpHeadersFrame setExclusive(boolean exclusive) { this.exclusive = exclusive; return this; } @Override public int getDependency() { return dependency; } @Override public HttpHeadersFrame setDependency(int dependency) { if (dependency < 0) { throw new IllegalArgumentException( "Dependency cannot be negative: " + dependency); } this.dependency = dependency; return this; } @Override public int getWeight() { return weight; } @Override public HttpHeadersFrame setWeight(int weight) { if (weight <= 0 || weight > 256) { throw new IllegalArgumentException( "Illegal weight: " + weight); } this.weight = weight; return this; } @Override public HttpHeadersFrame setStreamId(int streamId) { super.setStreamId(streamId); return this; } @Override public HttpHeadersFrame setInvalid() { super.setInvalid(); return this; } @Override public HttpHeadersFrame setTruncated() { super.setTruncated(); return this; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(StringUtil.simpleClassName(this)); buf.append("(last: "); buf.append(isLast()); buf.append(')'); buf.append(StringUtil.NEWLINE); buf.append("--> Stream-ID = "); buf.append(getStreamId()); buf.append(StringUtil.NEWLINE); buf.append("--> Dependency = "); buf.append(getDependency()); buf.append(" (exclusive: "); buf.append(isExclusive()); buf.append(", weight: "); buf.append(getWeight()); buf.append(')'); buf.append(StringUtil.NEWLINE); buf.append("--> Headers:"); buf.append(StringUtil.NEWLINE); appendHeaders(buf); // Remove the last newline. buf.setLength(buf.length() - StringUtil.NEWLINE.length()); return buf.toString(); } } ================================================ FILE: src/main/java/com/twitter/http2/DefaultHttpPingFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.util.internal.StringUtil; /** * The default {@link HttpPingFrame} implementation. */ public class DefaultHttpPingFrame implements HttpPingFrame { private long data; private boolean pong; /** * Creates a new instance. * * @param data the data payload of this frame */ public DefaultHttpPingFrame(long data) { setData(data); } @Override public long getData() { return data; } @Override public HttpPingFrame setData(long data) { this.data = data; return this; } @Override public boolean isPong() { return pong; } @Override public HttpPingFrame setPong(boolean pong) { this.pong = pong; return this; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(StringUtil.simpleClassName(this)); buf.append("(pong: "); buf.append(isPong()); buf.append(')'); buf.append(StringUtil.NEWLINE); buf.append("--> Data = "); buf.append(getData()); return buf.toString(); } } ================================================ FILE: src/main/java/com/twitter/http2/DefaultHttpPriorityFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.util.internal.StringUtil; /** * The default {@link HttpPriorityFrame} implementation. */ public class DefaultHttpPriorityFrame implements HttpPriorityFrame { private int streamId; private boolean exclusive; private int dependency; private int weight; /** * Creates a new instance. * * @param streamId the stream identifier of this frame * @param exclusive if the dependency of the stream is exclusive * @param dependency the dependency of the stream * @param weight the weight of the dependency of the stream */ public DefaultHttpPriorityFrame(int streamId, boolean exclusive, int dependency, int weight) { setStreamId(streamId); setExclusive(exclusive); setDependency(dependency); setWeight(weight); } @Override public int getStreamId() { return streamId; } @Override public HttpPriorityFrame setStreamId(int streamId) { if (streamId <= 0) { throw new IllegalArgumentException( "Stream identifier must be positive: " + streamId); } this.streamId = streamId; return this; } @Override public boolean isExclusive() { return exclusive; } @Override public HttpPriorityFrame setExclusive(boolean exclusive) { this.exclusive = exclusive; return this; } @Override public int getDependency() { return dependency; } @Override public HttpPriorityFrame setDependency(int dependency) { if (dependency < 0) { throw new IllegalArgumentException( "Dependency cannot be negative: " + dependency); } this.dependency = dependency; return this; } @Override public int getWeight() { return weight; } @Override public HttpPriorityFrame setWeight(int weight) { if (weight <= 0 || weight > 256) { throw new IllegalArgumentException( "Illegal weight: " + weight); } this.weight = weight; return this; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(StringUtil.simpleClassName(this)); buf.append(StringUtil.NEWLINE); buf.append("--> Stream-ID = "); buf.append(getStreamId()); buf.append(StringUtil.NEWLINE); buf.append("--> Dependency = "); buf.append(getDependency()); buf.append(" (exclusive: "); buf.append(isExclusive()); buf.append(", weight: "); buf.append(getWeight()); buf.append(')'); return buf.toString(); } } ================================================ FILE: src/main/java/com/twitter/http2/DefaultHttpPushPromiseFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.util.internal.StringUtil; /** * The default {@link HttpPushPromiseFrame} implementation. */ public class DefaultHttpPushPromiseFrame extends DefaultHttpHeaderBlockFrame implements HttpPushPromiseFrame { private int promisedStreamId; /** * Creates a new instance. * * @param streamId the stream identifier of this frame */ public DefaultHttpPushPromiseFrame(int streamId, int promisedStreamId) { super(streamId); setPromisedStreamId(promisedStreamId); } @Override public int getPromisedStreamId() { return promisedStreamId; } @Override public HttpPushPromiseFrame setPromisedStreamId(int promisedStreamId) { if (promisedStreamId <= 0) { throw new IllegalArgumentException( "Promised-Stream-ID must be positive: " + promisedStreamId); } this.promisedStreamId = promisedStreamId; return this; } @Override public HttpPushPromiseFrame setStreamId(int streamId) { super.setStreamId(streamId); return this; } @Override public HttpPushPromiseFrame setInvalid() { super.setInvalid(); return this; } @Override public HttpPushPromiseFrame setTruncated() { super.setTruncated(); return this; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(StringUtil.simpleClassName(this)); buf.append(StringUtil.NEWLINE); buf.append("--> Stream-ID = "); buf.append(getStreamId()); buf.append(StringUtil.NEWLINE); buf.append("--> Promised-Stream-ID = "); buf.append(getPromisedStreamId()); buf.append(StringUtil.NEWLINE); buf.append("--> Headers:"); buf.append(StringUtil.NEWLINE); appendHeaders(buf); // Remove the last newline. buf.setLength(buf.length() - StringUtil.NEWLINE.length()); return buf.toString(); } } ================================================ FILE: src/main/java/com/twitter/http2/DefaultHttpRstStreamFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.util.internal.StringUtil; /** * The default {@link HttpRstStreamFrame} implementation. */ public class DefaultHttpRstStreamFrame implements HttpRstStreamFrame { private int streamId; private HttpErrorCode errorCode; /** * Creates a new instance. * * @param streamId the stream identifier of this frame * @param code the error code of this frame */ public DefaultHttpRstStreamFrame(int streamId, int code) { this(streamId, HttpErrorCode.valueOf(code)); } /** * Creates a new instance. * * @param streamId the stream identifier of this frame * @param errorCode the error code of this frame */ public DefaultHttpRstStreamFrame(int streamId, HttpErrorCode errorCode) { setStreamId(streamId); setErrorCode(errorCode); } @Override public int getStreamId() { return streamId; } @Override public HttpRstStreamFrame setStreamId(int streamId) { if (streamId <= 0) { throw new IllegalArgumentException( "Stream identifier must be positive: " + streamId); } this.streamId = streamId; return this; } @Override public HttpErrorCode getErrorCode() { return errorCode; } @Override public HttpRstStreamFrame setErrorCode(HttpErrorCode errorCode) { this.errorCode = errorCode; return this; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(StringUtil.simpleClassName(this)); buf.append(StringUtil.NEWLINE); buf.append("--> Stream-ID = "); buf.append(getStreamId()); buf.append(StringUtil.NEWLINE); buf.append("--> Error Code: "); buf.append(getErrorCode().toString()); return buf.toString(); } } ================================================ FILE: src/main/java/com/twitter/http2/DefaultHttpSettingsFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.util.Map; import java.util.Set; import java.util.TreeMap; import io.netty.util.internal.StringUtil; /** * The default {@link HttpSettingsFrame} implementation. */ public class DefaultHttpSettingsFrame implements HttpSettingsFrame { private final Map settingsMap = new TreeMap(); private boolean ack; @Override public Set getIds() { return settingsMap.keySet(); } @Override public boolean isSet(int id) { return settingsMap.containsKey(id); } @Override public int getValue(int id) { if (settingsMap.containsKey(id)) { return settingsMap.get(id); } else { return -1; } } @Override public HttpSettingsFrame setValue(int id, int value) { if (id < 0 || id > HttpCodecUtil.HTTP_SETTINGS_MAX_ID) { throw new IllegalArgumentException("Setting ID is not valid: " + id); } settingsMap.put(id, value); return this; } @Override public HttpSettingsFrame removeValue(int id) { settingsMap.remove(id); return this; } @Override public boolean isAck() { return ack; } @Override public HttpSettingsFrame setAck(boolean ack) { this.ack = ack; return this; } private Set> getSettings() { return settingsMap.entrySet(); } private void appendSettings(StringBuilder buf) { for (Map.Entry e : getSettings()) { buf.append("--> "); buf.append(e.getKey().toString()); buf.append(':'); buf.append(e.getValue().toString()); buf.append(StringUtil.NEWLINE); } } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(StringUtil.simpleClassName(this)); buf.append("(ack: "); buf.append(isAck()); buf.append(')'); buf.append(StringUtil.NEWLINE); appendSettings(buf); buf.setLength(buf.length() - StringUtil.NEWLINE.length()); return buf.toString(); } } ================================================ FILE: src/main/java/com/twitter/http2/DefaultHttpStreamFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; /** * The default {@link HttpStreamFrame} implementation. */ public abstract class DefaultHttpStreamFrame implements HttpStreamFrame { private int streamId; /** * Creates a new instance. * * @param streamId the stream identifier of this frame */ protected DefaultHttpStreamFrame(int streamId) { setStreamId(streamId); } @Override public int getStreamId() { return streamId; } @Override public HttpStreamFrame setStreamId(int streamId) { if (streamId <= 0) { throw new IllegalArgumentException( "Stream identifier must be positive: " + streamId); } this.streamId = streamId; return this; } } ================================================ FILE: src/main/java/com/twitter/http2/DefaultHttpWindowUpdateFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.util.internal.StringUtil; /** * The default {@link HttpWindowUpdateFrame} implementation. */ public class DefaultHttpWindowUpdateFrame implements HttpWindowUpdateFrame { private int streamId; private int windowSizeIncrement; /** * Creates a new instance. * * @param streamId the stream identifier of this frame * @param windowSizeIncrement the Window-Size-Increment of this frame */ public DefaultHttpWindowUpdateFrame(int streamId, int windowSizeIncrement) { setStreamId(streamId); setWindowSizeIncrement(windowSizeIncrement); } @Override public int getStreamId() { return streamId; } @Override public DefaultHttpWindowUpdateFrame setStreamId(int streamId) { if (streamId < 0) { throw new IllegalArgumentException( "Stream identifier cannot be negative: " + streamId); } this.streamId = streamId; return this; } @Override public int getWindowSizeIncrement() { return windowSizeIncrement; } @Override public DefaultHttpWindowUpdateFrame setWindowSizeIncrement(int windowSizeIncrement) { if (windowSizeIncrement <= 0) { throw new IllegalArgumentException( "Window-Size-Increment must be positive: " + windowSizeIncrement); } this.windowSizeIncrement = windowSizeIncrement; return this; } @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append(StringUtil.simpleClassName(this)); buf.append(StringUtil.NEWLINE); buf.append("--> Stream-ID = "); buf.append(getStreamId()); buf.append(StringUtil.NEWLINE); buf.append("--> Window-Size-Increment = "); buf.append(getWindowSizeIncrement()); return buf.toString(); } } ================================================ FILE: src/main/java/com/twitter/http2/HttpCodecUtil.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.buffer.ByteBuf; final class HttpCodecUtil { static final int HTTP_FRAME_HEADER_SIZE = 9; static final int HTTP_MAX_LENGTH = 0x4000; // Initial MAX_FRAME_SIZE value is 2^14 static final int HTTP_DATA_FRAME = 0x00; static final int HTTP_HEADERS_FRAME = 0x01; static final int HTTP_PRIORITY_FRAME = 0x02; static final int HTTP_RST_STREAM_FRAME = 0x03; static final int HTTP_SETTINGS_FRAME = 0x04; static final int HTTP_PUSH_PROMISE_FRAME = 0x05; static final int HTTP_PING_FRAME = 0x06; static final int HTTP_GOAWAY_FRAME = 0x07; static final int HTTP_WINDOW_UPDATE_FRAME = 0x08; static final int HTTP_CONTINUATION_FRAME = 0x09; static final byte HTTP_FLAG_ACK = 0x01; static final byte HTTP_FLAG_END_STREAM = 0x01; static final byte HTTP_FLAG_END_SEGMENT = 0x02; static final byte HTTP_FLAG_END_HEADERS = 0x04; static final byte HTTP_FLAG_PADDED = 0x08; static final byte HTTP_FLAG_PRIORITY = 0x20; static final int HTTP_DEFAULT_WEIGHT = 16; static final int HTTP_DEFAULT_DEPENDENCY = 0; static final int HTTP_SETTINGS_MAX_ID = 0xFFFF; // Identifier is a 16-bit field static final int HTTP_CONNECTION_STREAM_ID = 0; /** * Reads a big-endian unsigned short integer from the buffer. */ static int getUnsignedShort(ByteBuf buf, int offset) { return (buf.getByte(offset) & 0xFF) << 8 | buf.getByte(offset + 1) & 0xFF; } /** * Reads a big-endian unsigned medium integer from the buffer. */ static int getUnsignedMedium(ByteBuf buf, int offset) { return (buf.getByte(offset) & 0xFF) << 16 | (buf.getByte(offset + 1) & 0xFF) << 8 | buf.getByte(offset + 2) & 0xFF; } /** * Reads a big-endian (31-bit) integer from the buffer. */ static int getUnsignedInt(ByteBuf buf, int offset) { return (buf.getByte(offset) & 0x7F) << 24 | (buf.getByte(offset + 1) & 0xFF) << 16 | (buf.getByte(offset + 2) & 0xFF) << 8 | buf.getByte(offset + 3) & 0xFF; } /** * Reads a big-endian signed integer from the buffer. */ static int getSignedInt(ByteBuf buf, int offset) { return (buf.getByte(offset) & 0xFF) << 24 | (buf.getByte(offset + 1) & 0xFF) << 16 | (buf.getByte(offset + 2) & 0xFF) << 8 | buf.getByte(offset + 3) & 0xFF; } /** * Reads a big-endian signed long from the buffer. */ static long getSignedLong(ByteBuf buf, int offset) { return ((long) buf.getByte(offset) & 0xFF) << 56 | ((long) buf.getByte(offset + 1) & 0xFF) << 48 | ((long) buf.getByte(offset + 2) & 0xFF) << 40 | ((long) buf.getByte(offset + 3) & 0xFF) << 32 | ((long) buf.getByte(offset + 4) & 0xFF) << 24 | ((long) buf.getByte(offset + 5) & 0xFF) << 16 | ((long) buf.getByte(offset + 6) & 0xFF) << 8 | (long) buf.getByte(offset + 7) & 0xFF; } /** * Returns {@code true} if the stream identifier is for a server initiated stream. */ static boolean isServerId(int streamId) { // Server initiated streams have even stream identifiers return streamId % 2 == 0; } private HttpCodecUtil() { } } ================================================ FILE: src/main/java/com/twitter/http2/HttpConnection.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.util.Comparator; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; import io.netty.channel.ChannelPromise; import io.netty.util.internal.EmptyArrays; import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_WEIGHT; import static com.twitter.http2.HttpCodecUtil.HTTP_CONNECTION_STREAM_ID; final class HttpConnection { private static final HttpProtocolException STREAM_CLOSED = new HttpProtocolException("Stream closed"); static { STREAM_CLOSED.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); } private final AtomicInteger activeLocalStreams = new AtomicInteger(); private final AtomicInteger activeRemoteStreams = new AtomicInteger(); private final Map streams = new ConcurrentHashMap(); private final AtomicInteger sendWindowSize; private final AtomicInteger receiveWindowSize; public HttpConnection(int sendWindowSize, int receiveWindowSize) { streams.put(HTTP_CONNECTION_STREAM_ID, new Node(null)); this.sendWindowSize = new AtomicInteger(sendWindowSize); this.receiveWindowSize = new AtomicInteger(receiveWindowSize); } int numActiveStreams(boolean remote) { if (remote) { return activeRemoteStreams.get(); } else { return activeLocalStreams.get(); } } boolean noActiveStreams() { return activeRemoteStreams.get() + activeLocalStreams.get() == 0; } void acceptStream( int streamId, boolean remoteSideClosed, boolean localSideClosed, int streamSendWindowSize, int streamReceiveWindowSize, boolean remote) { StreamState state = null; if (!remoteSideClosed || !localSideClosed) { state = new StreamState( remoteSideClosed, localSideClosed, streamSendWindowSize, streamReceiveWindowSize); } Node node = new Node(state); node.parent = streams.get(HTTP_CONNECTION_STREAM_ID); streams.put(streamId, node); if (state != null) { if (remote) { activeRemoteStreams.incrementAndGet(); } else { activeLocalStreams.incrementAndGet(); } } } private StreamState removeActiveStream(int streamId, boolean remote) { Node stream = streams.remove(streamId); if (stream != null && stream.state != null) { StreamState state = stream.state; stream.close(); if (remote) { activeRemoteStreams.decrementAndGet(); } else { activeLocalStreams.decrementAndGet(); } return state; } return null; } void removeStream(int streamId, boolean remote) { StreamState state = removeActiveStream(streamId, remote); if (state != null) { state.clearPendingWrites(STREAM_CLOSED); } } boolean isRemoteSideClosed(int streamId) { Node stream = streams.get(streamId); StreamState state = stream == null ? null : stream.state; return state == null || state.isRemoteSideClosed(); } void closeRemoteSide(int streamId, boolean remote) { Node stream = streams.get(streamId); StreamState state = stream == null ? null : stream.state; if (state != null) { state.closeRemoteSide(); if (state.isLocalSideClosed()) { removeActiveStream(streamId, remote); } } } boolean isLocalSideClosed(int streamId) { Node stream = streams.get(streamId); StreamState state = stream == null ? null : stream.state; return state == null || state.isLocalSideClosed(); } void closeLocalSide(int streamId, boolean remote) { Node stream = streams.get(streamId); StreamState state = stream == null ? null : stream.state; if (state != null) { state.closeLocalSide(); if (state.isRemoteSideClosed()) { removeActiveStream(streamId, remote); } } } int getSendWindowSize(int streamId) { if (streamId == HTTP_CONNECTION_STREAM_ID) { return sendWindowSize.get(); } Node stream = streams.get(streamId); StreamState state = stream == null ? null : stream.state; return state != null ? state.getSendWindowSize() : -1; } int updateSendWindowSize(int streamId, int deltaWindowSize) { if (streamId == HTTP_CONNECTION_STREAM_ID) { return sendWindowSize.addAndGet(deltaWindowSize); } Node stream = streams.get(streamId); StreamState state = stream == null ? null : stream.state; return state != null ? state.updateSendWindowSize(deltaWindowSize) : -1; } int updateReceiveWindowSize(int streamId, int deltaWindowSize) { if (streamId == HTTP_CONNECTION_STREAM_ID) { return receiveWindowSize.addAndGet(deltaWindowSize); } Node stream = streams.get(streamId); StreamState state = stream == null ? null : stream.state; if (state == null) { return -1; } if (deltaWindowSize > 0) { state.setReceiveWindowSizeLowerBound(0); } return state.updateReceiveWindowSize(deltaWindowSize); } int getReceiveWindowSizeLowerBound(int streamId) { if (streamId == HTTP_CONNECTION_STREAM_ID) { return 0; } Node stream = streams.get(streamId); StreamState state = stream == null ? null : stream.state; return state != null ? state.getReceiveWindowSizeLowerBound() : 0; } void updateAllSendWindowSizes(int deltaWindowSize) { for (Node stream : streams.values()) { StreamState state = stream.state; if (state != null) { state.updateSendWindowSize(deltaWindowSize); } } } void updateAllReceiveWindowSizes(int deltaWindowSize) { for (Node stream : streams.values()) { StreamState state = stream.state; if (state != null) { state.updateReceiveWindowSize(deltaWindowSize); if (deltaWindowSize < 0) { state.setReceiveWindowSizeLowerBound(deltaWindowSize); } } } } boolean putPendingWrite(int streamId, PendingWrite evt) { Node stream = streams.get(streamId); StreamState state = stream == null ? null : stream.state; return state != null && state.putPendingWrite(evt); } PendingWrite getPendingWrite(int streamId) { if (streamId == HTTP_CONNECTION_STREAM_ID) { Node connection = streams.get(HTTP_CONNECTION_STREAM_ID); return getPendingWrite(connection); } Node stream = streams.get(streamId); StreamState state = stream == null ? null : stream.state; return state != null ? state.getPendingWrite() : null; } PendingWrite getPendingWrite(Node node) { PendingWrite e = null; if (node.state != null && node.state.getSendWindowSize() > 0) { e = node.state.getPendingWrite(); } if (e == null) { for (Node child : node.children) { e = getPendingWrite(child); if (e != null) { break; } } } return e; } PendingWrite removePendingWrite(int streamId) { Node stream = streams.get(streamId); StreamState state = stream == null ? null : stream.state; return state != null ? state.removePendingWrite() : null; } /** * Set the priority of the stream. */ boolean setPriority(int streamId, boolean exclusive, int dependency, int weight) { Node stream = streams.get(streamId); if (stream == null) { // stream closed? return false; } Node parent = streams.get(dependency); if (parent == null) { // garbage collected? stream.parent.removeDependent(stream); // set to default priority Node root = streams.get(HTTP_CONNECTION_STREAM_ID); root.addDependent(false, stream); stream.setWeight(HTTP_DEFAULT_WEIGHT); return false; } // check if we need to restructure the tree if (parent == stream.parent) { if (exclusive) { // move dependents to stream parent.addDependent(true, stream); } } else { stream.parent.removeDependent(stream); parent.addDependent(exclusive, stream); } stream.setWeight(weight); return true; } private static final class StreamState { private boolean remoteSideClosed; private boolean localSideClosed; private final AtomicInteger sendWindowSize; private final AtomicInteger receiveWindowSize; private int receiveWindowSizeLowerBound; private final ConcurrentLinkedQueue pendingWriteQueue = new ConcurrentLinkedQueue(); StreamState( boolean remoteSideClosed, boolean localSideClosed, int sendWindowSize, int receiveWindowSize) { this.remoteSideClosed = remoteSideClosed; this.localSideClosed = localSideClosed; this.sendWindowSize = new AtomicInteger(sendWindowSize); this.receiveWindowSize = new AtomicInteger(receiveWindowSize); } boolean isRemoteSideClosed() { return remoteSideClosed; } void closeRemoteSide() { remoteSideClosed = true; } boolean isLocalSideClosed() { return localSideClosed; } void closeLocalSide() { localSideClosed = true; } int getSendWindowSize() { return sendWindowSize.get(); } int updateSendWindowSize(int deltaWindowSize) { return sendWindowSize.addAndGet(deltaWindowSize); } int updateReceiveWindowSize(int deltaWindowSize) { return receiveWindowSize.addAndGet(deltaWindowSize); } int getReceiveWindowSizeLowerBound() { return receiveWindowSizeLowerBound; } void setReceiveWindowSizeLowerBound(int receiveWindowSizeLowerBound) { this.receiveWindowSizeLowerBound = receiveWindowSizeLowerBound; } boolean putPendingWrite(PendingWrite msg) { return pendingWriteQueue.offer(msg); } PendingWrite getPendingWrite() { return pendingWriteQueue.peek(); } PendingWrite removePendingWrite() { return pendingWriteQueue.poll(); } void clearPendingWrites(Throwable cause) { for (; ; ) { PendingWrite pendingWrite = pendingWriteQueue.poll(); if (pendingWrite == null) { break; } pendingWrite.fail(cause); } } } private static final class Node { private static final Comparator COMPARATOR = new WeightComparator(); public Node parent; // the dependency of the stream public int weight; // the weight of the dependency // Children should be iterator in weighted order public Set children = new TreeSet(COMPARATOR); // the dependents of the stream public int dependentWeights; // the total weight of all the dependents of the stream public StreamState state; public Node(StreamState state) { this.state = state; } public void close() { this.state = null; } public void setWeight(int weight) { // Remove and re-add parent to maintain comparator order parent.removeDependent(this); this.weight = weight; parent.addDependent(false, this); } public void addDependent(boolean exclusive, Node node) { removeDependent(node); if (exclusive) { for (Node child : children) { node.addDependent(false, child); } children.clear(); dependentWeights = 0; } children.add(node); dependentWeights += node.weight; } public void removeDependent(Node node) { if (children.remove(node)) { dependentWeights -= node.weight; } } } private static final class WeightComparator implements Comparator { @Override public int compare(Node n1, Node n2) { return n2.weight - n1.weight; } } public static final class PendingWrite { public final HttpDataFrame httpDataFrame; public final ChannelPromise promise; PendingWrite(HttpDataFrame httpDataFrame, ChannelPromise promise) { this.httpDataFrame = httpDataFrame; this.promise = promise; } void fail(Throwable cause) { // httpDataFrame.release(); promise.setFailure(cause); } } } ================================================ FILE: src/main/java/com/twitter/http2/HttpConnectionHandler.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.io.IOException; import java.net.SocketAddress; import java.nio.channels.ClosedChannelException; import java.util.List; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandler; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.ByteToMessageDecoder; import io.netty.util.internal.EmptyArrays; import static com.twitter.http2.HttpCodecUtil.HTTP_CONNECTION_STREAM_ID; import static com.twitter.http2.HttpCodecUtil.isServerId; /** * Manages streams within an HTTP/2 connection. */ public class HttpConnectionHandler extends ByteToMessageDecoder implements HttpFrameDecoderDelegate, ChannelOutboundHandler { private static final HttpProtocolException PROTOCOL_EXCEPTION = new HttpProtocolException(); static { PROTOCOL_EXCEPTION.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); } private static final HttpSettingsFrame SETTINGS_ACK_FRAME = new DefaultHttpSettingsFrame().setAck(true); private static final int DEFAULT_HEADER_TABLE_SIZE = 4096; private static final int DEFAULT_WINDOW_SIZE = 65535; private int initialSendWindowSize = DEFAULT_WINDOW_SIZE; private int initialReceiveWindowSize = DEFAULT_WINDOW_SIZE; private volatile int initialConnectionReceiveWindowSize = DEFAULT_WINDOW_SIZE; private final HttpConnection httpConnection = new HttpConnection(initialSendWindowSize, initialReceiveWindowSize); private int lastStreamId; private static final int DEFAULT_MAX_CONCURRENT_STREAMS = Integer.MAX_VALUE; private int remoteConcurrentStreams = DEFAULT_MAX_CONCURRENT_STREAMS; private int localConcurrentStreams = DEFAULT_MAX_CONCURRENT_STREAMS; private boolean sentGoAwayFrame; private boolean receivedGoAwayFrame; private final ChannelFutureListener connectionErrorListener = new ConnectionErrorFutureListener(); private ChannelFutureListener closingChannelFutureListener; private final boolean server; private final boolean handleStreamWindowUpdates; private final HttpFrameDecoder httpFrameDecoder; private final HttpFrameEncoder httpFrameEncoder; private final HttpHeaderBlockDecoder httpHeaderBlockDecoder; private final HttpHeaderBlockEncoder httpHeaderBlockEncoder; private HttpHeaderBlockFrame httpHeaderBlockFrame; private HttpSettingsFrame httpSettingsFrame; private boolean needSettingsAck; private boolean changeDecoderHeaderTableSize; private int headerTableSize; private boolean changeEncoderHeaderTableSize; private int lastHeaderTableSize = Integer.MAX_VALUE; private int minHeaderTableSize = Integer.MAX_VALUE; private boolean pushEnabled = true; private ChannelHandlerContext context; /** * Creates a new connection handler. * * @param server {@code true} if and only if this connection handler should * handle the server endpoint of the connection. * {@code false} if and only if this connection handler should * handle the client endpoint of the connection. */ public HttpConnectionHandler(boolean server) { this(server, true); } /** * Creates a new connection handler with the specified options. */ public HttpConnectionHandler(boolean server, boolean handleStreamWindowUpdates) { this(server, handleStreamWindowUpdates, 8192, 16384); } /** * Creates a new connection handler with the specified options. */ public HttpConnectionHandler(boolean server, int maxChunkSize, int maxHeaderSize) { this(server, true, maxChunkSize, maxHeaderSize); } /** * Creates a new connection handler with the specified options. */ public HttpConnectionHandler( boolean server, boolean handleStreamWindowUpdates, int maxChunkSize, int maxHeaderSize) { this.server = server; this.handleStreamWindowUpdates = handleStreamWindowUpdates; httpFrameDecoder = new HttpFrameDecoder(server, this, maxChunkSize); httpFrameEncoder = new HttpFrameEncoder(); httpHeaderBlockDecoder = new HttpHeaderBlockDecoder(maxHeaderSize, DEFAULT_HEADER_TABLE_SIZE); httpHeaderBlockEncoder = new HttpHeaderBlockEncoder(DEFAULT_HEADER_TABLE_SIZE); } public void setConnectionReceiveWindowSize(int connectionReceiveWindowSize) { if (connectionReceiveWindowSize < 0) { throw new IllegalArgumentException("connectionReceiveWindowSize"); } // This will not send a window update frame immediately. // If this value increases the allowed receive window size, // a WINDOW_UPDATE frame will be sent when only half of the // session window size remains during data frame processing. // If this value decreases the allowed receive window size, // the window will be reduced as data frames are processed. initialConnectionReceiveWindowSize = connectionReceiveWindowSize; } @Override public void handlerAdded(ChannelHandlerContext ctx) throws Exception { super.handlerAdded(ctx); context = ctx; } @Override protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { httpFrameDecoder.decode(in); } /** * {@inheritDoc} */ @Override public void readDataFramePadding(int streamId, boolean endStream, int padding) { int deltaWindowSize = -1 * padding; int newConnectionWindowSize = httpConnection.updateReceiveWindowSize( HTTP_CONNECTION_STREAM_ID, deltaWindowSize); // Check if connection window size is reduced beyond allowable lower bound if (newConnectionWindowSize < 0) { issueConnectionError(HttpErrorCode.PROTOCOL_ERROR); return; } // Send a WINDOW_UPDATE frame if less than half the connection window size remains if (newConnectionWindowSize <= initialConnectionReceiveWindowSize / 2) { int windowSizeIncrement = initialConnectionReceiveWindowSize - newConnectionWindowSize; httpConnection.updateReceiveWindowSize(HTTP_CONNECTION_STREAM_ID, windowSizeIncrement); ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame( HTTP_CONNECTION_STREAM_ID, windowSizeIncrement); context.writeAndFlush(frame); } // Check if we received a DATA frame for a stream which is half-closed (remote) or closed if (httpConnection.isRemoteSideClosed(streamId)) { if (streamId <= lastStreamId) { issueStreamError(streamId, HttpErrorCode.STREAM_CLOSED); } else if (!sentGoAwayFrame) { issueStreamError(streamId, HttpErrorCode.PROTOCOL_ERROR); } return; } // Update receive window size int newWindowSize = httpConnection.updateReceiveWindowSize(streamId, deltaWindowSize); // Window size can become negative if we sent a SETTINGS frame that reduces the // size of the transfer window after the peer has written data frames. // The value is bounded by the length that SETTINGS frame decrease the window. // This difference is stored for the connection when writing the SETTINGS frame // and is cleared once we send a WINDOW_UPDATE frame. if (newWindowSize < httpConnection.getReceiveWindowSizeLowerBound(streamId)) { issueStreamError(streamId, HttpErrorCode.FLOW_CONTROL_ERROR); return; } // Send a WINDOW_UPDATE frame if less than half the stream window size remains // Recipient should not send a WINDOW_UPDATE frame as it consumes the last data frame. if (handleStreamWindowUpdates && newWindowSize <= initialReceiveWindowSize / 2 && !endStream) { int windowSizeIncrement = initialReceiveWindowSize - newWindowSize; httpConnection.updateReceiveWindowSize(streamId, windowSizeIncrement); ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame(streamId, windowSizeIncrement); context.writeAndFlush(frame); } } /** * {@inheritDoc} */ @Override public void readDataFrame(int streamId, boolean endStream, boolean endSegment, ByteBuf data) { // HTTP/2 DATA frame processing requirements: // // If an endpoint receives a data frame for a Stream-ID which is not open // and the endpoint has not sent a GOAWAY frame, it must issue a stream error // with the error code INVALID_STREAM for the Stream-ID. // // If an endpoint receives multiple data frames for invalid Stream-IDs, // it may close the connection. // // If an endpoint refuses a stream it must ignore any data frames for that stream. // // If an endpoint receives a data frame after the stream is half-closed (remote) // or closed, it must respond with a stream error of type STREAM_CLOSED. int deltaWindowSize = -1 * data.readableBytes(); int newConnectionWindowSize = httpConnection.updateReceiveWindowSize( HTTP_CONNECTION_STREAM_ID, deltaWindowSize); // Check if connection window size is reduced beyond allowable lower bound if (newConnectionWindowSize < 0) { issueConnectionError(HttpErrorCode.PROTOCOL_ERROR); return; } // Send a WINDOW_UPDATE frame if less than half the connection window size remains if (newConnectionWindowSize <= initialConnectionReceiveWindowSize / 2) { int windowSizeIncrement = initialConnectionReceiveWindowSize - newConnectionWindowSize; httpConnection.updateReceiveWindowSize(HTTP_CONNECTION_STREAM_ID, windowSizeIncrement); ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame( HTTP_CONNECTION_STREAM_ID, windowSizeIncrement); context.writeAndFlush(frame); } // Check if we received a DATA frame for a stream which is half-closed (remote) or closed if (httpConnection.isRemoteSideClosed(streamId)) { if (streamId <= lastStreamId) { issueStreamError(streamId, HttpErrorCode.STREAM_CLOSED); } else if (!sentGoAwayFrame) { issueStreamError(streamId, HttpErrorCode.PROTOCOL_ERROR); } return; } // Update receive window size int newWindowSize = httpConnection.updateReceiveWindowSize(streamId, deltaWindowSize); // Window size can become negative if we sent a SETTINGS frame that reduces the // size of the transfer window after the peer has written data frames. // The value is bounded by the length that SETTINGS frame decrease the window. // This difference is stored for the connection when writing the SETTINGS frame // and is cleared once we send a WINDOW_UPDATE frame. if (newWindowSize < httpConnection.getReceiveWindowSizeLowerBound(streamId)) { issueStreamError(streamId, HttpErrorCode.FLOW_CONTROL_ERROR); return; } // Window size became negative due to sender writing frame before receiving SETTINGS // Send data frames upstream in initialReceiveWindowSize chunks if (newWindowSize < 0) { while (data.readableBytes() > initialReceiveWindowSize) { ByteBuf partialData = data.readBytes(initialReceiveWindowSize); HttpDataFrame partialDataFrame = new DefaultHttpDataFrame(streamId, partialData); context.fireChannelRead(partialDataFrame); } } // Send a WINDOW_UPDATE frame if less than half the stream window size remains // Recipient should not send a WINDOW_UPDATE frame as it consumes the last data frame. if (handleStreamWindowUpdates && newWindowSize <= initialReceiveWindowSize / 2 && !endStream) { int windowSizeIncrement = initialReceiveWindowSize - newWindowSize; httpConnection.updateReceiveWindowSize(streamId, windowSizeIncrement); ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame(streamId, windowSizeIncrement); context.writeAndFlush(frame); } // Close the remote side of the stream if this is the last frame if (endStream) { halfCloseStream(streamId, true, context.channel().newSucceededFuture()); } HttpDataFrame httpDataFrame = new DefaultHttpDataFrame(streamId, data); httpDataFrame.setLast(endStream); context.fireChannelRead(httpDataFrame); } /** * {@inheritDoc} */ @Override public void readHeadersFrame( int streamId, boolean endStream, boolean endSegment, boolean exclusive, int dependency, int weight ) { // HTTP/2 HEADERS frame processing requirements: // // If an endpoint receives a HEADERS frame with a Stream-ID that is less than // any previously received HEADERS, it must issue a connection error of type // PROTOCOL_ERROR. // // If an endpoint receives multiple SYN_STREAM frames with the same active // Stream-ID, it must issue a stream error with the status code PROTOCOL_ERROR. // // The recipient can reject a stream by sending a stream error with the // status code REFUSED_STREAM. if (isRemoteInitiatedId(streamId)) { if (streamId <= lastStreamId) { // Check if we received a HEADERS frame for a stream which is half-closed (remote) or closed if (httpConnection.isRemoteSideClosed(streamId)) { issueStreamError(streamId, HttpErrorCode.STREAM_CLOSED); return; } } else { // Try to accept the stream if (!acceptStream(streamId, exclusive, dependency, weight)) { issueStreamError(streamId, HttpErrorCode.REFUSED_STREAM); return; } } } else { // Check if we received a HEADERS frame for a stream which is half-closed (remote) or closed if (httpConnection.isRemoteSideClosed(streamId)) { issueStreamError(streamId, HttpErrorCode.STREAM_CLOSED); return; } } // Close the remote side of the stream if this is the last frame if (endStream) { halfCloseStream(streamId, true, context.channel().newSucceededFuture()); } HttpHeadersFrame httpHeadersFrame = new DefaultHttpHeadersFrame(streamId); httpHeadersFrame.setLast(endStream); httpHeadersFrame.setExclusive(exclusive); httpHeadersFrame.setDependency(dependency); httpHeadersFrame.setWeight(weight); httpHeaderBlockFrame = httpHeadersFrame; } /** * {@inheritDoc} */ @Override public void readPriorityFrame(int streamId, boolean exclusive, int dependency, int weight) { if (streamId == dependency) { issueStreamError(streamId, HttpErrorCode.PROTOCOL_ERROR); } else { setPriority(streamId, exclusive, dependency, weight); } } /** * {@inheritDoc} */ @Override public void readRstStreamFrame(int streamId, int errorCode) { // If a RST_STREAM frame identifying an idle stream is received, // the recipient MUST treat this as a connection error of type // PROTOCOL_ERROR. removeStream(streamId, context.channel().newSucceededFuture()); HttpRstStreamFrame httpRstStreamFrame = new DefaultHttpRstStreamFrame(streamId, errorCode); context.fireChannelRead(httpRstStreamFrame); } /** * {@inheritDoc} */ @Override public void readSettingsFrame(boolean ack) { needSettingsAck = !ack; httpSettingsFrame = new DefaultHttpSettingsFrame(); httpSettingsFrame.setAck(ack); if (ack && changeDecoderHeaderTableSize) { httpHeaderBlockDecoder.setMaxHeaderTableSize(headerTableSize); changeDecoderHeaderTableSize = false; } } /** * {@inheritDoc} */ @Override public void readSetting(int id, int value) { httpSettingsFrame.setValue(id, value); switch (id) { case HttpSettingsFrame.SETTINGS_HEADER_TABLE_SIZE: // Ignore 'negative' values -- they are too large for java if (value >= 0) { changeEncoderHeaderTableSize = true; lastHeaderTableSize = value; if (lastHeaderTableSize < minHeaderTableSize) { minHeaderTableSize = lastHeaderTableSize; } } break; case HttpSettingsFrame.SETTINGS_ENABLE_PUSH: if (value == 0) { pushEnabled = false; } else if (value == 1) { pushEnabled = true; } else { issueConnectionError(HttpErrorCode.PROTOCOL_ERROR); } break; case HttpSettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS: if (value >= 0) { remoteConcurrentStreams = value; } break; case HttpSettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE: if (value >= 0) { updateInitialSendWindowSize(value); } else { issueConnectionError(HttpErrorCode.FLOW_CONTROL_ERROR); } break; case HttpSettingsFrame.SETTINGS_MAX_FRAME_SIZE: if (value != HttpCodecUtil.HTTP_MAX_LENGTH) { issueConnectionError(HttpErrorCode.PROTOCOL_ERROR); } break; default: // Ignore Unknown Settings } } /** * {@inheritDoc} */ @Override public void readSettingsEnd() { if (changeEncoderHeaderTableSize) { synchronized (httpHeaderBlockEncoder) { httpHeaderBlockEncoder.setDecoderMaxHeaderTableSize(minHeaderTableSize); httpHeaderBlockEncoder.setDecoderMaxHeaderTableSize(lastHeaderTableSize); // Writes of settings ack must occur in order ByteBuf frame = httpFrameEncoder.encodeSettingsFrame(SETTINGS_ACK_FRAME); context.writeAndFlush(frame); } changeEncoderHeaderTableSize = false; lastHeaderTableSize = Integer.MAX_VALUE; minHeaderTableSize = Integer.MAX_VALUE; } else if (needSettingsAck) { ByteBuf frame = httpFrameEncoder.encodeSettingsFrame(SETTINGS_ACK_FRAME); context.writeAndFlush(frame); } Object frame = httpSettingsFrame; httpSettingsFrame = null; context.fireChannelRead(frame); } /** * {@inheritDoc} */ @Override public void readPushPromiseFrame(int streamId, int promisedStreamId) { // TODO(jpinner) handle push promise frames // Any we receive must be associated with a "peer-initiated" stream. // Since we don't have a way currently to initiate streams, any // frame that we receive must be treated as a protocol error. issueConnectionError(HttpErrorCode.PROTOCOL_ERROR); } /** * {@inheritDoc} */ @Override public void readPingFrame(long data, boolean ack) { // HTTP/2 PING frame processing requirements: // // Receivers of a PING frame should send an identical frame to the sender // as soon as possible. // // Receivers of a PING frame must ignore frames that it did not initiate HttpPingFrame httpPingFrame = new DefaultHttpPingFrame(data); httpPingFrame.setPong(true); if (ack) { context.fireChannelRead(httpPingFrame); } else { ByteBuf frame = httpFrameEncoder.encodePingFrame(data, false); context.writeAndFlush(frame); } } /** * {@inheritDoc} */ @Override public void readGoAwayFrame(int lastStreamId, int errorCode) { receivedGoAwayFrame = true; HttpGoAwayFrame httpGoAwayFrame = new DefaultHttpGoAwayFrame(lastStreamId, errorCode); context.fireChannelRead(httpGoAwayFrame); } /** * {@inheritDoc} */ @Override public void readWindowUpdateFrame(int streamId, int windowSizeIncrement) { // HTTP/2 WINDOW_UPDATE frame processing requirements: // // Receivers of a WINDOW_UPDATE that cause the window size to exceed 2^31 // must send a RST_STREAM with the status code FLOW_CONTROL_ERROR. // // Sender should ignore all WINDOW_UPDATE frames associated with a stream // after sending the last frame for the stream. // Ignore frames for half-closed streams if (streamId != HTTP_CONNECTION_STREAM_ID && httpConnection.isLocalSideClosed(streamId)) { return; } // Check for numerical overflow if (httpConnection.getSendWindowSize(streamId) > Integer.MAX_VALUE - windowSizeIncrement) { if (streamId == HTTP_CONNECTION_STREAM_ID) { issueConnectionError(HttpErrorCode.PROTOCOL_ERROR); } else { issueStreamError(streamId, HttpErrorCode.FLOW_CONTROL_ERROR); } return; } updateSendWindowSize(context, streamId, windowSizeIncrement); } /** * {@inheritDoc} */ @Override public void readHeaderBlock(ByteBuf headerBlockFragment) { try { httpHeaderBlockDecoder.decode(headerBlockFragment, httpHeaderBlockFrame); } catch (IOException e) { httpHeaderBlockFrame = null; issueConnectionError(HttpErrorCode.PROTOCOL_ERROR); } } /** * {@inheritDoc} */ @Override public void readHeaderBlockEnd() { httpHeaderBlockDecoder.endHeaderBlock(httpHeaderBlockFrame); if (httpHeaderBlockFrame == null) { return; } // Check if we received a valid Header Block if (httpHeaderBlockFrame.isInvalid()) { issueStreamError(httpHeaderBlockFrame.getStreamId(), HttpErrorCode.PROTOCOL_ERROR); return; } Object frame = httpHeaderBlockFrame; httpHeaderBlockFrame = null; context.fireChannelRead(frame); } /** * {@inheritDoc} */ @Override public void readFrameError(String message) { issueConnectionError(HttpErrorCode.PROTOCOL_ERROR); } @Override public void bind( ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception { ctx.bind(localAddress, promise); } @Override public void connect( ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise ) throws Exception { ctx.connect(remoteAddress, localAddress, promise); } @Override public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { // HTTP/2 connection requirements: // // When either endpoint closes the transport-level connection, // it must first send a GOAWAY frame. // // Avoid NotYetConnectedException if (!ctx.channel().isActive()) { ctx.disconnect(promise); } else { sendGoAwayFrame(ctx, promise); } } @Override public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { // HTTP/2 connection requirements: // // When either endpoint closes the transport-level connection, // it must first send a GOAWAY frame. // // Avoid NotYetConnectedException if (!ctx.channel().isActive()) { ctx.close(promise); } else { sendGoAwayFrame(ctx, promise); } } @Override public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception { ctx.deregister(promise); } @Override public void read(ChannelHandlerContext ctx) throws Exception { ctx.read(); } @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (msg instanceof HttpDataFrame) { HttpDataFrame httpDataFrame = (HttpDataFrame) msg; int streamId = httpDataFrame.getStreamId(); // Frames must not be sent on half-closed streams if (httpConnection.isLocalSideClosed(streamId)) { promise.setFailure(PROTOCOL_EXCEPTION); return; } // HTTP/2 DATA frame flow control processing requirements: // // Sender must not send a data frame with data length greater // than the transfer window size. // // After sending each data frame, the sender decrements its // transfer window size by the amount of data transmitted. // // When the window size becomes less than or equal to 0, the // sender must pause transmitting data frames. int dataLength = httpDataFrame.content().readableBytes(); int sendWindowSize = httpConnection.getSendWindowSize(streamId); int connectionSendWindowSize = httpConnection.getSendWindowSize( HTTP_CONNECTION_STREAM_ID); sendWindowSize = Math.min(sendWindowSize, connectionSendWindowSize); if (sendWindowSize <= 0) { // Stream is stalled -- enqueue Data frame and return httpConnection.putPendingWrite( streamId, new HttpConnection.PendingWrite(httpDataFrame, promise)); return; } else if (sendWindowSize < dataLength) { // Stream is not stalled but we cannot send the entire frame httpConnection.updateSendWindowSize(streamId, -1 * sendWindowSize); httpConnection.updateSendWindowSize(HTTP_CONNECTION_STREAM_ID, -1 * sendWindowSize); // Create a partial data frame whose length is the current window size ByteBuf data = httpDataFrame.content().readSlice(sendWindowSize).retain(); ByteBuf partialDataFrame = httpFrameEncoder.encodeDataFrame(streamId, false, data); // Enqueue the remaining data (will be the first frame queued) httpConnection.putPendingWrite( streamId, new HttpConnection.PendingWrite(httpDataFrame, promise)); ChannelPromise writeFuture = ctx.channel().newPromise(); // The transfer window size is pre-decremented when sending a data frame downstream. // Close the connection on write failures that leaves the transfer window in a corrupt state. writeFuture.addListener(connectionErrorListener); ctx.write(partialDataFrame, writeFuture); return; } else { // Window size is large enough to send entire data frame httpConnection.updateSendWindowSize(streamId, -1 * dataLength); httpConnection.updateSendWindowSize(HTTP_CONNECTION_STREAM_ID, -1 * dataLength); // The transfer window size is pre-decremented when sending a data frame downstream. // Close the connection on write failures that leaves the transfer window in a corrupt state. promise.addListener(connectionErrorListener); } // Close the local side of the stream if this is the last frame if (httpDataFrame.isLast()) { halfCloseStream(streamId, false, promise); } ByteBuf frame = httpFrameEncoder.encodeDataFrame( streamId, httpDataFrame.isLast(), httpDataFrame.content() ); ctx.write(frame, promise); } else if (msg instanceof HttpHeadersFrame) { HttpHeadersFrame httpHeadersFrame = (HttpHeadersFrame) msg; int streamId = httpHeadersFrame.getStreamId(); if (isRemoteInitiatedId(streamId)) { if (streamId <= lastStreamId) { // Attempting to send headers for an older stream // (older than the latest accepted remote initiated stream) // Ensure that the frames are not sent on a half-closed (local) or closed streams if (httpConnection.isLocalSideClosed(streamId)) { promise.setFailure(PROTOCOL_EXCEPTION); return; } } else { // If we are attempting to write to a remote initiated stream id which is greater than the latest // accepted stream Id then we must throw a protocol exception! i.e we cannot write on a remote // initiated stream which we have not accepted before promise.setFailure(PROTOCOL_EXCEPTION); return; } } else { // This is a locally initiated stream (Push) boolean exclusive = httpHeadersFrame.isExclusive(); int dependency = httpHeadersFrame.getDependency(); int weight = httpHeadersFrame.getWeight(); if (!acceptStream(streamId, exclusive, dependency, weight)) { promise.setFailure(PROTOCOL_EXCEPTION); return; } } // Close the local side of the stream if this is the last frame if (httpHeadersFrame.isLast()) { halfCloseStream(streamId, false, promise); } synchronized (httpHeaderBlockEncoder) { ByteBuf frame = httpFrameEncoder.encodeHeadersFrame( httpHeadersFrame.getStreamId(), httpHeadersFrame.isLast(), httpHeadersFrame.isExclusive(), httpHeadersFrame.getDependency(), httpHeadersFrame.getWeight(), httpHeaderBlockEncoder.encode(ctx, httpHeadersFrame) ); // Writes of compressed data must occur in order ctx.write(frame, promise); } } else if (msg instanceof HttpPriorityFrame) { HttpPriorityFrame httpPriorityFrame = (HttpPriorityFrame) msg; int streamId = httpPriorityFrame.getStreamId(); boolean exclusive = httpPriorityFrame.isExclusive(); int dependency = httpPriorityFrame.getDependency(); int weight = httpPriorityFrame.getWeight(); setPriority(streamId, exclusive, dependency, weight); ByteBuf frame = httpFrameEncoder.encodePriorityFrame( streamId, exclusive, dependency, weight ); ctx.write(frame, promise); } else if (msg instanceof HttpRstStreamFrame) { HttpRstStreamFrame httpRstStreamFrame = (HttpRstStreamFrame) msg; removeStream(httpRstStreamFrame.getStreamId(), promise); ByteBuf frame = httpFrameEncoder.encodeRstStreamFrame( httpRstStreamFrame.getStreamId(), httpRstStreamFrame.getErrorCode().getCode()); ctx.write(frame, promise); } else if (msg instanceof HttpSettingsFrame) { // TODO(jpinner) currently cannot have more than one settings frame outstanding at a time HttpSettingsFrame httpSettingsFrame = (HttpSettingsFrame) msg; if (httpSettingsFrame.isAck()) { // Cannot send an acknowledgement frame promise.setFailure(PROTOCOL_EXCEPTION); return; } int newHeaderTableSize = httpSettingsFrame.getValue(HttpSettingsFrame.SETTINGS_HEADER_TABLE_SIZE); if (newHeaderTableSize >= 0) { headerTableSize = newHeaderTableSize; changeDecoderHeaderTableSize = true; } int newConcurrentStreams = httpSettingsFrame.getValue(HttpSettingsFrame.SETTINGS_MAX_CONCURRENT_STREAMS); if (newConcurrentStreams >= 0) { localConcurrentStreams = newConcurrentStreams; } int newInitialWindowSize = httpSettingsFrame.getValue(HttpSettingsFrame.SETTINGS_INITIAL_WINDOW_SIZE); if (newInitialWindowSize >= 0) { updateInitialReceiveWindowSize(newInitialWindowSize); } ByteBuf frame = httpFrameEncoder.encodeSettingsFrame(httpSettingsFrame); ctx.write(frame, promise); } else if (msg instanceof HttpPushPromiseFrame) { if (!pushEnabled) { promise.setFailure(PROTOCOL_EXCEPTION); return; } synchronized (httpHeaderBlockEncoder) { HttpPushPromiseFrame httpPushPromiseFrame = (HttpPushPromiseFrame) msg; ByteBuf frame = httpFrameEncoder.encodePushPromiseFrame( httpPushPromiseFrame.getStreamId(), httpPushPromiseFrame.getPromisedStreamId(), httpHeaderBlockEncoder.encode(ctx, httpPushPromiseFrame) ); // Writes of compressed data must occur in order ctx.write(frame, promise); } } else if (msg instanceof HttpPingFrame) { HttpPingFrame httpPingFrame = (HttpPingFrame) msg; if (httpPingFrame.isPong()) { // Cannot send a PONG frame promise.setFailure(PROTOCOL_EXCEPTION); } else { ByteBuf frame = httpFrameEncoder.encodePingFrame(httpPingFrame.getData(), false); ctx.write(frame, promise); } } else if (msg instanceof HttpGoAwayFrame) { // Why is this being sent? Intercept it and fail the write. // Should have sent a CLOSE ChannelStateEvent promise.setFailure(PROTOCOL_EXCEPTION); } else if (msg instanceof HttpWindowUpdateFrame) { HttpWindowUpdateFrame httpWindowUpdateFrame = (HttpWindowUpdateFrame) msg; int streamId = httpWindowUpdateFrame.getStreamId(); if (handleStreamWindowUpdates || streamId == HTTP_CONNECTION_STREAM_ID) { // Why is this being sent? Intercept it and fail the write. promise.setFailure(PROTOCOL_EXCEPTION); } else { int windowSizeIncrement = httpWindowUpdateFrame.getWindowSizeIncrement(); httpConnection.updateReceiveWindowSize(streamId, windowSizeIncrement); ByteBuf frame = httpFrameEncoder.encodeWindowUpdateFrame(streamId, windowSizeIncrement); ctx.write(frame, promise); } } else { ctx.write(msg, promise); } } @Override public void flush(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } // HTTP/2 Connection Error Handling: // // When a connection error occurs, the endpoint encountering the error must first // send a GOAWAY frame with the stream identifier of the most recently received stream // from the remote endpoint, and the error code for why the connection is terminating. // // After sending the GOAWAY frame, the endpoint must close the TCP connection. private void issueConnectionError(HttpErrorCode status) { ChannelFuture future = sendGoAwayFrame(status); future.addListener(ChannelFutureListener.CLOSE); } // Http/2 Stream Error Handling: // // Upon a stream error, the endpoint must send a RST_STREAM frame which contains // the Stream-ID for the stream where the error occurred and the error status which // caused the error. // // After sending the RST_STREAM, the stream is closed to the sending endpoint. // // Note: this is only called by the worker thread private void issueStreamError(int streamId, HttpErrorCode errorCode) { boolean fireMessageReceived = !httpConnection.isRemoteSideClosed(streamId); ChannelPromise promise = context.channel().newPromise(); removeStream(streamId, promise); ByteBuf frame = httpFrameEncoder.encodeRstStreamFrame(streamId, errorCode.getCode()); context.writeAndFlush(frame, promise); if (fireMessageReceived) { HttpRstStreamFrame httpRstStreamFrame = new DefaultHttpRstStreamFrame(streamId, errorCode); context.fireChannelRead(httpRstStreamFrame); } } // Helper functions private boolean isRemoteInitiatedId(int id) { boolean serverId = isServerId(id); return server && !serverId || !server && serverId; } // need to synchronize to prevent new streams from being created while updating active streams private synchronized void updateInitialSendWindowSize(int newInitialWindowSize) { int deltaWindowSize = newInitialWindowSize - initialSendWindowSize; initialSendWindowSize = newInitialWindowSize; httpConnection.updateAllSendWindowSizes(deltaWindowSize); } // need to synchronize to prevent new streams from being created while updating active streams private synchronized void updateInitialReceiveWindowSize(int newInitialWindowSize) { int deltaWindowSize = newInitialWindowSize - initialReceiveWindowSize; initialReceiveWindowSize = newInitialWindowSize; httpConnection.updateAllReceiveWindowSizes(deltaWindowSize); } // need to synchronize accesses to sentGoAwayFrame, lastStreamId, and initial window sizes private synchronized boolean acceptStream( int streamId, boolean exclusive, int dependency, int weight) { // Cannot initiate any new streams after receiving or sending GOAWAY if (receivedGoAwayFrame || sentGoAwayFrame) { return false; } boolean remote = isRemoteInitiatedId(streamId); int maxConcurrentStreams = remote ? localConcurrentStreams : remoteConcurrentStreams; if (httpConnection.numActiveStreams(remote) >= maxConcurrentStreams) { return false; } httpConnection.acceptStream( streamId, false, false, initialSendWindowSize, initialReceiveWindowSize, remote); if (remote) { lastStreamId = streamId; } setPriority(streamId, exclusive, dependency, weight); return true; } private synchronized boolean setPriority( int streamId, boolean exclusive, int dependency, int weight) { return httpConnection.setPriority(streamId, exclusive, dependency, weight); } private void halfCloseStream(int streamId, boolean remote, ChannelFuture future) { if (remote) { httpConnection.closeRemoteSide(streamId, isRemoteInitiatedId(streamId)); } else { httpConnection.closeLocalSide(streamId, isRemoteInitiatedId(streamId)); } if (closingChannelFutureListener != null && httpConnection.noActiveStreams()) { future.addListener(closingChannelFutureListener); } } private void removeStream(int streamId, ChannelFuture future) { httpConnection.removeStream(streamId, isRemoteInitiatedId(streamId)); if (closingChannelFutureListener != null && httpConnection.noActiveStreams()) { future.addListener(closingChannelFutureListener); } } private void updateSendWindowSize( ChannelHandlerContext ctx, int streamId, int windowSizeIncrement) { httpConnection.updateSendWindowSize(streamId, windowSizeIncrement); while (true) { // Check if we have unblocked a stalled stream HttpConnection.PendingWrite e = httpConnection.getPendingWrite(streamId); if (e == null) { break; } HttpDataFrame httpDataFrame = e.httpDataFrame; int dataFrameSize = httpDataFrame.content().readableBytes(); int writeStreamId = httpDataFrame.getStreamId(); int sendWindowSize = httpConnection.getSendWindowSize(writeStreamId); int connectionSendWindowSize = httpConnection.getSendWindowSize( HTTP_CONNECTION_STREAM_ID); sendWindowSize = Math.min(sendWindowSize, connectionSendWindowSize); if (sendWindowSize <= 0) { return; } else if (sendWindowSize < dataFrameSize) { // We can send a partial frame httpConnection.updateSendWindowSize(writeStreamId, -1 * sendWindowSize); httpConnection.updateSendWindowSize(HTTP_CONNECTION_STREAM_ID, -1 * sendWindowSize); // Create a partial data frame whose length is the current window size ByteBuf data = httpDataFrame.content().readSlice(sendWindowSize).retain(); ByteBuf partialDataFrame = httpFrameEncoder.encodeDataFrame(writeStreamId, false, data); ChannelPromise writeFuture = ctx.channel().newPromise(); // The transfer window size is pre-decremented when sending a data frame downstream. // Close the connection on write failures that leaves the transfer window in a corrupt state. writeFuture.addListener(connectionErrorListener); ctx.writeAndFlush(partialDataFrame, writeFuture); } else { // Window size is large enough to send entire data frame httpConnection.removePendingWrite(writeStreamId); httpConnection.updateSendWindowSize(writeStreamId, -1 * dataFrameSize); httpConnection.updateSendWindowSize(HTTP_CONNECTION_STREAM_ID, -1 * dataFrameSize); // The transfer window size is pre-decremented when sending a data frame downstream. // Close the connection on write failures that leaves the transfer window in a corrupt state. e.promise.addListener(connectionErrorListener); // Close the local side of the stream if this is the last frame if (httpDataFrame.isLast()) { halfCloseStream(writeStreamId, false, e.promise); } ByteBuf frame = httpFrameEncoder.encodeDataFrame( writeStreamId, httpDataFrame.isLast(), httpDataFrame.content() ); ctx.writeAndFlush(frame, e.promise); } } } private void sendGoAwayFrame(ChannelHandlerContext ctx, ChannelPromise promise) { ChannelFuture future = sendGoAwayFrame(HttpErrorCode.NO_ERROR); if (httpConnection.noActiveStreams()) { future.addListener(new ClosingChannelFutureListener(ctx, promise)); } else { closingChannelFutureListener = new ClosingChannelFutureListener(ctx, promise); } } private synchronized ChannelFuture sendGoAwayFrame(HttpErrorCode httpErrorCode) { if (!sentGoAwayFrame) { sentGoAwayFrame = true; ChannelPromise promise = context.channel().newPromise(); ByteBuf frame = httpFrameEncoder.encodeGoAwayFrame(lastStreamId, httpErrorCode.getCode()); context.writeAndFlush(frame, promise); return promise; } return context.channel().newSucceededFuture(); } private final class ConnectionErrorFutureListener implements ChannelFutureListener { @Override public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { issueConnectionError(HttpErrorCode.INTERNAL_ERROR); } } } private static final class ClosingChannelFutureListener implements ChannelFutureListener { private final ChannelHandlerContext ctx; private final ChannelPromise promise; ClosingChannelFutureListener(ChannelHandlerContext ctx, ChannelPromise promise) { this.ctx = ctx; this.promise = promise; } public void operationComplete(ChannelFuture sentGoAwayFuture) throws Exception { if (!(sentGoAwayFuture.cause() instanceof ClosedChannelException)) { ctx.close(promise); } else { promise.setSuccess(); } } } } ================================================ FILE: src/main/java/com/twitter/http2/HttpDataFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; /** * An HTTP/2 DATA Frame */ public interface HttpDataFrame extends ByteBufHolder, HttpStreamFrame { /** * Returns {@code true} if this frame is the last frame to be transmitted on the stream. */ boolean isLast(); /** * Sets if this frame is the last frame to be transmitted on the stream. */ HttpDataFrame setLast(boolean last); @Override HttpDataFrame setStreamId(int streamId); /** * Returns the data payload of this frame. If there is no data payload * {@link io.netty.buffer.Unpooled#EMPTY_BUFFER} is returned. *

* The data payload cannot exceed 16384 bytes. */ @Override ByteBuf content(); @Override HttpDataFrame copy(); @Override HttpDataFrame duplicate(); @Override HttpDataFrame retain(); @Override HttpDataFrame retain(int increment); } ================================================ FILE: src/main/java/com/twitter/http2/HttpErrorCode.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; public class HttpErrorCode implements Comparable { /** * 0 No Error */ public static final HttpErrorCode NO_ERROR = new HttpErrorCode(0, "NO_ERROR"); /** * 1 Protocol Error */ public static final HttpErrorCode PROTOCOL_ERROR = new HttpErrorCode(1, "PROTOCOL_ERROR"); /** * 2 Internal Error */ public static final HttpErrorCode INTERNAL_ERROR = new HttpErrorCode(2, "INTERNAL_ERROR"); /** * 3 Flow Control Error */ public static final HttpErrorCode FLOW_CONTROL_ERROR = new HttpErrorCode(3, "FLOW_CONTROL_ERROR"); /** * 4 Settings Timeout */ public static final HttpErrorCode SETTINGS_TIMEOUT = new HttpErrorCode(4, "SETTINGS_TIMEOUT"); /** * 5 Stream Closed */ public static final HttpErrorCode STREAM_CLOSED = new HttpErrorCode(5, "STREAM_CLOSED"); /** * 6 Frame Size Error */ public static final HttpErrorCode FRAME_SIZE_ERROR = new HttpErrorCode(6, "FRAME_SIZE_ERROR"); /** * 7 Refused Stream */ public static final HttpErrorCode REFUSED_STREAM = new HttpErrorCode(7, "REFUSED_STREAM"); /** * 8 Cancel */ public static final HttpErrorCode CANCEL = new HttpErrorCode(8, "CANCEL"); /** * 9 Compression Error */ public static final HttpErrorCode COMPRESSION_ERROR = new HttpErrorCode(9, "COMPRESSION_ERROR"); /** * 10 Connect Error */ public static final HttpErrorCode CONNECT_ERROR = new HttpErrorCode(10, "CONNECT_ERROR"); /** * 11 Enhance Your Calm (420) */ public static final HttpErrorCode ENHANCE_YOUR_CALM = new HttpErrorCode(420, "ENHANCE_YOUR_CALM"); /** * 12 Inadequate Security */ public static final HttpErrorCode INADEQUATE_SECURITY = new HttpErrorCode(12, "INADEQUATE_SECURITY"); /** * Returns the {@link HttpErrorCode} represented by the specified code. * If the specified code is a defined HTTP error code, a cached instance * will be returned. Otherwise, a new instance will be returned. */ public static HttpErrorCode valueOf(int code) { switch (code) { case 0: return NO_ERROR; case 1: return PROTOCOL_ERROR; case 2: return INTERNAL_ERROR; case 3: return FLOW_CONTROL_ERROR; case 4: return SETTINGS_TIMEOUT; case 5: return STREAM_CLOSED; case 6: return FRAME_SIZE_ERROR; case 7: return REFUSED_STREAM; case 8: return CANCEL; case 9: return COMPRESSION_ERROR; case 10: return CONNECT_ERROR; case 11: return ENHANCE_YOUR_CALM; case 12: return INADEQUATE_SECURITY; case 420: return ENHANCE_YOUR_CALM; default: return new HttpErrorCode(code, "UNKNOWN (" + code + ')'); } } private final int code; private final String statusPhrase; /** * Creates a new instance with the specified {@code code} and its * {@code statusPhrase}. */ public HttpErrorCode(int code, String statusPhrase) { if (statusPhrase == null) { throw new NullPointerException("statusPhrase"); } this.code = code; this.statusPhrase = statusPhrase; } /** * Returns the code of this status. */ public int getCode() { return code; } /** * Returns the status phrase of this status. */ public String getStatusPhrase() { return statusPhrase; } @Override public int hashCode() { return getCode(); } @Override public boolean equals(Object o) { if (!(o instanceof HttpErrorCode)) { return false; } return getCode() == ((HttpErrorCode) o).getCode(); } @Override public String toString() { return getStatusPhrase(); } @Override public int compareTo(HttpErrorCode o) { return getCode() - o.getCode(); } } ================================================ FILE: src/main/java/com/twitter/http2/HttpFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; /** * An HTTP/2 Frame */ public interface HttpFrame { // Tag interface } ================================================ FILE: src/main/java/com/twitter/http2/HttpFrameDecoder.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.nio.charset.StandardCharsets; import io.netty.buffer.ByteBuf; import static com.twitter.http2.HttpCodecUtil.HTTP_CONTINUATION_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_DATA_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_ACK; import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_END_HEADERS; import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_END_SEGMENT; import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_END_STREAM; import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_PADDED; import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_PRIORITY; import static com.twitter.http2.HttpCodecUtil.HTTP_FRAME_HEADER_SIZE; import static com.twitter.http2.HttpCodecUtil.HTTP_GOAWAY_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_HEADERS_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_MAX_LENGTH; import static com.twitter.http2.HttpCodecUtil.HTTP_PING_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_PRIORITY_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_PUSH_PROMISE_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_RST_STREAM_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_SETTINGS_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_WINDOW_UPDATE_FRAME; import static com.twitter.http2.HttpCodecUtil.getSignedInt; import static com.twitter.http2.HttpCodecUtil.getSignedLong; import static com.twitter.http2.HttpCodecUtil.getUnsignedInt; import static com.twitter.http2.HttpCodecUtil.getUnsignedMedium; import static com.twitter.http2.HttpCodecUtil.getUnsignedShort; /** * Decodes {@link ByteBuf}s into HTTP/2 Frames. */ public class HttpFrameDecoder { private static final byte[] CLIENT_CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(StandardCharsets.US_ASCII); private final int maxChunkSize; private final HttpFrameDecoderDelegate delegate; private State state; // HTTP/2 frame header fields private int length; private short type; private byte flags; private int streamId; // HTTP/2 frame padding length private int paddingLength; private enum State { READ_CONNECTION_HEADER, READ_FRAME_HEADER, READ_PADDING_LENGTH, READ_DATA_FRAME, READ_DATA, READ_HEADERS_FRAME, READ_PRIORITY_FRAME, READ_RST_STREAM_FRAME, READ_SETTINGS_FRAME, READ_SETTING, READ_PUSH_PROMISE_FRAME, READ_PING_FRAME, READ_GOAWAY_FRAME, READ_WINDOW_UPDATE_FRAME, READ_CONTINUATION_FRAME_HEADER, READ_HEADER_BLOCK, SKIP_FRAME_PADDING, SKIP_FRAME_PADDING_CONTINUATION, FRAME_ERROR } /** * Creates a new instance with the specified @{code HttpFrameDecoderDelegate} * and the default {@code maxChunkSize (8192)}. */ public HttpFrameDecoder(boolean server, HttpFrameDecoderDelegate delegate) { this(server, delegate, 8192); } /** * Creates a new instance with the specified parameters. */ public HttpFrameDecoder(boolean server, HttpFrameDecoderDelegate delegate, int maxChunkSize) { if (delegate == null) { throw new NullPointerException("delegate"); } if (maxChunkSize <= 0) { throw new IllegalArgumentException( "maxChunkSize must be a positive integer: " + maxChunkSize); } this.delegate = delegate; this.maxChunkSize = maxChunkSize; if (server) { state = State.READ_CONNECTION_HEADER; } else { state = State.READ_FRAME_HEADER; } } /** * Decode the byte buffer. */ public void decode(ByteBuf buffer) { boolean endStream; boolean endSegment; int minLength; int dependency; int weight; boolean exclusive; int errorCode; while (true) { switch (state) { case READ_CONNECTION_HEADER: while (buffer.isReadable()) { byte b = buffer.readByte(); if (b != CLIENT_CONNECTION_PREFACE[length++]) { state = State.FRAME_ERROR; delegate.readFrameError("Invalid Connection Header"); return; } if (length == CLIENT_CONNECTION_PREFACE.length) { state = State.READ_FRAME_HEADER; break; } } if (buffer.isReadable()) { break; } else { return; } case READ_FRAME_HEADER: // Wait until entire header is readable if (buffer.readableBytes() < HTTP_FRAME_HEADER_SIZE) { return; } // Read frame header fields readFrameHeader(buffer); // TODO(jpinner) Sec 4.2 FRAME_SIZE_ERROR if (!isValidFrameHeader(length, type, flags, streamId)) { state = State.FRAME_ERROR; delegate.readFrameError("Invalid Frame Header"); } else if (frameHasPadding(type, flags)) { state = State.READ_PADDING_LENGTH; } else { paddingLength = 0; state = getNextState(length, type); } break; case READ_PADDING_LENGTH: if (buffer.readableBytes() < 1) { return; } paddingLength = buffer.readUnsignedByte(); --length; if (!isValidPaddingLength(length, type, flags, paddingLength)) { state = State.FRAME_ERROR; delegate.readFrameError("Invalid Frame Padding Length"); } else { state = getNextState(length, type); } break; case READ_DATA_FRAME: endStream = hasFlag(flags, HTTP_FLAG_END_STREAM); state = State.READ_DATA; if (hasFlag(flags, HTTP_FLAG_PADDED)) { delegate.readDataFramePadding(streamId, endStream, paddingLength + 1); } break; case READ_DATA: // Generate data frames that do not exceed maxChunkSize // maxChunkSize must be > 0 so we cannot infinitely loop int dataLength = Math.min(maxChunkSize, length - paddingLength); // Wait until entire frame is readable if (buffer.readableBytes() < dataLength) { return; } ByteBuf data = buffer.readBytes(dataLength); length -= dataLength; if (length == paddingLength) { if (paddingLength == 0) { state = State.READ_FRAME_HEADER; } else { state = State.SKIP_FRAME_PADDING; } } endStream = length == paddingLength && hasFlag(flags, HTTP_FLAG_END_STREAM); endSegment = length == paddingLength && hasFlag(flags, HTTP_FLAG_END_SEGMENT); delegate.readDataFrame(streamId, endStream, endSegment, data); break; case READ_HEADERS_FRAME: minLength = 0; if (hasFlag(flags, HTTP_FLAG_PRIORITY)) { minLength = 5; } if (buffer.readableBytes() < minLength) { return; } endStream = hasFlag(flags, HTTP_FLAG_END_STREAM); endSegment = hasFlag(flags, HTTP_FLAG_END_SEGMENT); exclusive = false; dependency = 0; weight = 16; if (hasFlag(flags, HTTP_FLAG_PRIORITY)) { dependency = getSignedInt(buffer, buffer.readerIndex()); buffer.skipBytes(4); weight = buffer.readUnsignedByte() + 1; if (dependency < 0) { dependency = dependency & 0x7FFFFFFF; exclusive = true; } length -= 5; } state = State.READ_HEADER_BLOCK; delegate.readHeadersFrame(streamId, endStream, endSegment, exclusive, dependency, weight); break; case READ_PRIORITY_FRAME: if (buffer.readableBytes() < length) { return; } exclusive = false; dependency = getSignedInt(buffer, buffer.readerIndex()); buffer.skipBytes(4); weight = buffer.readUnsignedByte() + 1; if (dependency < 0) { dependency = dependency & 0x7FFFFFFF; exclusive = true; } state = State.READ_FRAME_HEADER; delegate.readPriorityFrame(streamId, exclusive, dependency, weight); break; case READ_RST_STREAM_FRAME: if (buffer.readableBytes() < length) { return; } errorCode = getSignedInt(buffer, buffer.readerIndex()); buffer.skipBytes(length); state = State.READ_FRAME_HEADER; delegate.readRstStreamFrame(streamId, errorCode); break; case READ_SETTINGS_FRAME: boolean ack = hasFlag(flags, HTTP_FLAG_ACK); state = State.READ_SETTING; delegate.readSettingsFrame(ack); break; case READ_SETTING: if (length == 0) { state = State.READ_FRAME_HEADER; delegate.readSettingsEnd(); break; } if (buffer.readableBytes() < 6) { return; } int id = getUnsignedShort(buffer, buffer.readerIndex()); int value = getSignedInt(buffer, buffer.readerIndex() + 2); buffer.skipBytes(6); length -= 6; delegate.readSetting(id, value); break; case READ_PUSH_PROMISE_FRAME: if (buffer.readableBytes() < 4) { return; } int promisedStreamId = getUnsignedInt(buffer, buffer.readerIndex()); buffer.skipBytes(4); length -= 4; if (promisedStreamId == 0) { state = State.FRAME_ERROR; delegate.readFrameError("Invalid Promised-Stream-ID"); } else { state = State.READ_HEADER_BLOCK; delegate.readPushPromiseFrame(streamId, promisedStreamId); } break; case READ_PING_FRAME: if (buffer.readableBytes() < length) { return; } long ping = getSignedLong(buffer, buffer.readerIndex()); buffer.skipBytes(length); boolean pong = hasFlag(flags, HTTP_FLAG_ACK); state = State.READ_FRAME_HEADER; delegate.readPingFrame(ping, pong); break; case READ_GOAWAY_FRAME: if (buffer.readableBytes() < 8) { return; } int lastStreamId = getUnsignedInt(buffer, buffer.readerIndex()); errorCode = getSignedInt(buffer, buffer.readerIndex() + 4); buffer.skipBytes(8); length -= 8; if (length == 0) { state = State.READ_FRAME_HEADER; } else { paddingLength = length; state = State.SKIP_FRAME_PADDING; } delegate.readGoAwayFrame(lastStreamId, errorCode); break; case READ_WINDOW_UPDATE_FRAME: // Wait until entire frame is readable if (buffer.readableBytes() < length) { return; } int windowSizeIncrement = getUnsignedInt(buffer, buffer.readerIndex()); buffer.skipBytes(length); if (windowSizeIncrement == 0) { state = State.FRAME_ERROR; delegate.readFrameError("Invalid Window Size Increment"); } else { state = State.READ_FRAME_HEADER; delegate.readWindowUpdateFrame(streamId, windowSizeIncrement); } break; case READ_CONTINUATION_FRAME_HEADER: // Wait until entire frame header is readable if (buffer.readableBytes() < HTTP_FRAME_HEADER_SIZE) { return; } // Read and validate continuation frame header fields int prevStreamId = streamId; readFrameHeader(buffer); // TODO(jpinner) Sec 4.2 FRAME_SIZE_ERROR // TODO(jpinner) invalid flags if (type != HTTP_CONTINUATION_FRAME || streamId != prevStreamId) { state = State.FRAME_ERROR; delegate.readFrameError("Invalid Continuation Frame"); } else { paddingLength = 0; state = State.READ_HEADER_BLOCK; } break; case READ_HEADER_BLOCK: if (length == paddingLength) { boolean endHeaders = hasFlag(flags, HTTP_FLAG_END_HEADERS); if (endHeaders) { state = State.SKIP_FRAME_PADDING; delegate.readHeaderBlockEnd(); } else { state = State.SKIP_FRAME_PADDING_CONTINUATION; } break; } if (!buffer.isReadable()) { return; } int readableBytes = Math.min(buffer.readableBytes(), length - paddingLength); ByteBuf headerBlockFragment = buffer.readBytes(readableBytes); length -= readableBytes; delegate.readHeaderBlock(headerBlockFragment); break; case SKIP_FRAME_PADDING: int numBytes = Math.min(buffer.readableBytes(), length); buffer.skipBytes(numBytes); length -= numBytes; if (length == 0) { state = State.READ_FRAME_HEADER; break; } return; case SKIP_FRAME_PADDING_CONTINUATION: int numPaddingBytes = Math.min(buffer.readableBytes(), length); buffer.skipBytes(numPaddingBytes); length -= numPaddingBytes; if (length == 0) { state = State.READ_CONTINUATION_FRAME_HEADER; break; } return; case FRAME_ERROR: buffer.skipBytes(buffer.readableBytes()); return; default: throw new Error("Shouldn't reach here."); } } } /** * Reads the HTTP/2 Frame Header and sets the length, type, flags, and streamId member variables. * * @param buffer input buffer containing the entire 9-octet header */ private void readFrameHeader(ByteBuf buffer) { int frameOffset = buffer.readerIndex(); length = getUnsignedMedium(buffer, frameOffset); type = buffer.getUnsignedByte(frameOffset + 3); flags = buffer.getByte(frameOffset + 4); streamId = getUnsignedInt(buffer, frameOffset + 5); buffer.skipBytes(HTTP_FRAME_HEADER_SIZE); } private static boolean hasFlag(byte flags, byte flag) { return (flags & flag) != 0; } private static boolean frameHasPadding(int type, byte flags) { switch (type) { case HTTP_DATA_FRAME: case HTTP_HEADERS_FRAME: case HTTP_PUSH_PROMISE_FRAME: return hasFlag(flags, HTTP_FLAG_PADDED); default: return false; } } private static State getNextState(int length, int type) { switch (type) { case HTTP_DATA_FRAME: return State.READ_DATA_FRAME; case HTTP_HEADERS_FRAME: return State.READ_HEADERS_FRAME; case HTTP_PRIORITY_FRAME: return State.READ_PRIORITY_FRAME; case HTTP_RST_STREAM_FRAME: return State.READ_RST_STREAM_FRAME; case HTTP_SETTINGS_FRAME: return State.READ_SETTINGS_FRAME; case HTTP_PUSH_PROMISE_FRAME: return State.READ_PUSH_PROMISE_FRAME; case HTTP_PING_FRAME: return State.READ_PING_FRAME; case HTTP_GOAWAY_FRAME: return State.READ_GOAWAY_FRAME; case HTTP_WINDOW_UPDATE_FRAME: return State.READ_WINDOW_UPDATE_FRAME; case HTTP_CONTINUATION_FRAME: throw new Error("Shouldn't reach here."); default: if (length != 0) { return State.SKIP_FRAME_PADDING; } else { return State.READ_FRAME_HEADER; } } } private static boolean isValidFrameHeader(int length, short type, byte flags, int streamId) { if (length > HTTP_MAX_LENGTH) { return false; } int minLength; switch (type) { case HTTP_DATA_FRAME: if (hasFlag(flags, HTTP_FLAG_PADDED)) { minLength = 1; } else { minLength = 0; } return length >= minLength && streamId != 0; case HTTP_HEADERS_FRAME: if (hasFlag(flags, HTTP_FLAG_PADDED)) { minLength = 1; } else { minLength = 0; } if (hasFlag(flags, HTTP_FLAG_PRIORITY)) { minLength += 5; } return length >= minLength && streamId != 0; case HTTP_PRIORITY_FRAME: return length == 5 && streamId != 0; case HTTP_RST_STREAM_FRAME: return length == 4 && streamId != 0; case HTTP_SETTINGS_FRAME: boolean lengthValid = hasFlag(flags, HTTP_FLAG_ACK) ? length == 0 : (length % 6) == 0; return lengthValid && streamId == 0; case HTTP_PUSH_PROMISE_FRAME: if (hasFlag(flags, HTTP_FLAG_PADDED)) { minLength = 5; } else { minLength = 4; } return length >= minLength && streamId != 0; case HTTP_PING_FRAME: return length == 8 && streamId == 0; case HTTP_GOAWAY_FRAME: return length >= 8 && streamId == 0; case HTTP_WINDOW_UPDATE_FRAME: return length == 4; case HTTP_CONTINUATION_FRAME: return false; default: return true; } } private static boolean isValidPaddingLength( int length, short type, byte flags, int paddingLength) { switch (type) { case HTTP_DATA_FRAME: return length >= paddingLength; case HTTP_HEADERS_FRAME: if (hasFlag(flags, HTTP_FLAG_PRIORITY)) { return length >= paddingLength + 5; } else { return length >= paddingLength; } case HTTP_PUSH_PROMISE_FRAME: return length >= paddingLength + 4; default: throw new Error("Shouldn't reach here."); } } } ================================================ FILE: src/main/java/com/twitter/http2/HttpFrameDecoderDelegate.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.buffer.ByteBuf; /** * Callback interface for {@link HttpFrameDecoder}. */ public interface HttpFrameDecoderDelegate { /** * Called when a DATA frame is received. */ void readDataFramePadding(int streamId, boolean endStream, int padding); /** * Called when a DATA frame is received. */ void readDataFrame(int streamId, boolean endStream, boolean endSegment, ByteBuf data); /** * Called when a HEADERS frame is received. * The Header Block Fragment is not included. See readHeaderBlock(). */ void readHeadersFrame( int streamId, boolean endStream, boolean endSegment, boolean exclusive, int dependency, int weight ); /** * Called when a PRIORITY frame is received. */ void readPriorityFrame(int streamId, boolean exclusive, int dependency, int weight); /** * Called when a RST_STREAM frame is received. */ void readRstStreamFrame(int streamId, int errorCode); /** * Called when a SETTINGS frame is received. * Settings are not included. See readSetting(). */ void readSettingsFrame(boolean ack); /** * Called when an individual setting within a SETTINGS frame is received. */ void readSetting(int id, int value); /** * Called when the entire SETTINGS frame has been received. */ void readSettingsEnd(); /** * Called when a PUSH_PROMISE frame is received. * The Header Block Fragment is not included. See readHeaderBlock(). */ void readPushPromiseFrame(int streamId, int promisedStreamId); /** * Called when a PING frame is received. */ void readPingFrame(long data, boolean ack); /** * Called when a GOAWAY frame is received. */ void readGoAwayFrame(int lastStreamId, int errorCode); /** * Called when a WINDOW_UPDATE frame is received. */ void readWindowUpdateFrame(int streamId, int windowSizeIncrement); /** * Called when the header block fragment within a HEADERS, * PUSH_PROMISE, or CONTINUATION frame is received. */ void readHeaderBlock(ByteBuf headerBlockFragment); /** * Called when an entire header block has been received. */ void readHeaderBlockEnd(); /** * Called when an unrecoverable connection error has occurred. */ void readFrameError(String message); } ================================================ FILE: src/main/java/com/twitter/http2/HttpFrameEncoder.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.util.Set; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import static com.twitter.http2.HttpCodecUtil.HTTP_CONTINUATION_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_DATA_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_DEPENDENCY; import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_WEIGHT; import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_ACK; import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_END_HEADERS; import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_END_STREAM; import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_PRIORITY; import static com.twitter.http2.HttpCodecUtil.HTTP_FRAME_HEADER_SIZE; import static com.twitter.http2.HttpCodecUtil.HTTP_GOAWAY_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_HEADERS_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_MAX_LENGTH; import static com.twitter.http2.HttpCodecUtil.HTTP_PING_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_PRIORITY_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_PUSH_PROMISE_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_RST_STREAM_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_SETTINGS_FRAME; import static com.twitter.http2.HttpCodecUtil.HTTP_WINDOW_UPDATE_FRAME; /** * Encodes an HTTP/2 Frame into a {@link ByteBuf}. */ public class HttpFrameEncoder { /** * Encode an HTTP/2 DATA Frame */ public ByteBuf encodeDataFrame(int streamId, boolean endStream, ByteBuf data) { byte flags = endStream ? HTTP_FLAG_END_STREAM : 0; ByteBuf header = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE); writeFrameHeader(header, data.readableBytes(), HTTP_DATA_FRAME, flags, streamId); return Unpooled.wrappedBuffer(header, data); } /** * Encode an HTTP/2 HEADERS Frame */ public ByteBuf encodeHeadersFrame( int streamId, boolean endStream, boolean exclusive, int dependency, int weight, ByteBuf headerBlock ) { byte flags = endStream ? HTTP_FLAG_END_STREAM : 0; boolean hasPriority = exclusive || dependency != HTTP_DEFAULT_DEPENDENCY || weight != HTTP_DEFAULT_WEIGHT; if (hasPriority) { flags |= HTTP_FLAG_PRIORITY; } int maxLength = hasPriority ? HTTP_MAX_LENGTH - 5 : HTTP_MAX_LENGTH; boolean needsContinuations = headerBlock.readableBytes() > maxLength; if (!needsContinuations) { flags |= HTTP_FLAG_END_HEADERS; } int length = needsContinuations ? maxLength : headerBlock.readableBytes(); if (hasPriority) { length += 5; } int frameLength = hasPriority ? HTTP_FRAME_HEADER_SIZE + 5 : HTTP_FRAME_HEADER_SIZE; ByteBuf header = Unpooled.buffer(frameLength); writeFrameHeader(header, length, HTTP_HEADERS_FRAME, flags, streamId); if (hasPriority) { if (exclusive) { header.writeInt(dependency | 0x80000000); } else { header.writeInt(dependency); } header.writeByte(weight - 1); length -= 5; } ByteBuf frame = Unpooled.wrappedBuffer(header, headerBlock.readSlice(length)); if (needsContinuations) { while (headerBlock.readableBytes() > HTTP_MAX_LENGTH) { header = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE); writeFrameHeader(header, HTTP_MAX_LENGTH, HTTP_CONTINUATION_FRAME, (byte) 0, streamId); frame = Unpooled.wrappedBuffer(frame, header, headerBlock.readSlice(HTTP_MAX_LENGTH)); } header = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE); writeFrameHeader( header, headerBlock.readableBytes(), HTTP_CONTINUATION_FRAME, HTTP_FLAG_END_HEADERS, streamId ); frame = Unpooled.wrappedBuffer(frame, header, headerBlock); } return frame; } /** * Encode an HTTP/2 PRIORITY Frame */ public ByteBuf encodePriorityFrame(int streamId, boolean exclusive, int dependency, int weight) { int length = 5; byte flags = 0; ByteBuf frame = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + length); writeFrameHeader(frame, length, HTTP_PRIORITY_FRAME, flags, streamId); if (exclusive) { frame.writeInt(dependency | 0x80000000); } else { frame.writeInt(dependency); } frame.writeByte(weight - 1); return frame; } /** * Encode an HTTP/2 RST_STREAM Frame */ public ByteBuf encodeRstStreamFrame(int streamId, int errorCode) { int length = 4; byte flags = 0; ByteBuf frame = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + length); writeFrameHeader(frame, length, HTTP_RST_STREAM_FRAME, flags, streamId); frame.writeInt(errorCode); return frame; } /** * Encode an HTTP/2 SETTINGS Frame */ public ByteBuf encodeSettingsFrame(HttpSettingsFrame httpSettingsFrame) { Set ids = httpSettingsFrame.getIds(); int length = ids.size() * 6; byte flags = httpSettingsFrame.isAck() ? HTTP_FLAG_ACK : 0; int streamId = 0; ByteBuf frame = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + length); writeFrameHeader(frame, length, HTTP_SETTINGS_FRAME, flags, streamId); for (int id : ids) { frame.writeShort(id); frame.writeInt(httpSettingsFrame.getValue(id)); } return frame; } /** * Encode an HTTP/2 PUSH_PROMISE Frame */ public ByteBuf encodePushPromiseFrame(int streamId, int promisedStreamId, ByteBuf headerBlock) { boolean needsContinuations = headerBlock.readableBytes() > HTTP_MAX_LENGTH - 4; int length = needsContinuations ? HTTP_MAX_LENGTH - 4 : headerBlock.readableBytes(); byte flags = needsContinuations ? 0 : HTTP_FLAG_END_HEADERS; ByteBuf header = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + 4); writeFrameHeader(header, length + 4, HTTP_PUSH_PROMISE_FRAME, flags, streamId); header.writeInt(promisedStreamId); ByteBuf frame = Unpooled.wrappedBuffer(header, headerBlock.readSlice(length)); if (needsContinuations) { while (headerBlock.readableBytes() > HTTP_MAX_LENGTH) { header = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE); writeFrameHeader(header, HTTP_MAX_LENGTH, HTTP_CONTINUATION_FRAME, (byte) 0, streamId); frame = Unpooled.wrappedBuffer(frame, header, headerBlock.readSlice(HTTP_MAX_LENGTH)); } header = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE); writeFrameHeader( header, headerBlock.readableBytes(), HTTP_CONTINUATION_FRAME, HTTP_FLAG_END_HEADERS, streamId ); frame = Unpooled.wrappedBuffer(frame, header, headerBlock); } return frame; } /** * Encode an HTTP/2 PING Frame */ public ByteBuf encodePingFrame(long data, boolean ack) { int length = 8; byte flags = ack ? HTTP_FLAG_ACK : 0; int streamId = 0; ByteBuf frame = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + length); writeFrameHeader(frame, length, HTTP_PING_FRAME, flags, streamId); frame.writeLong(data); return frame; } /** * Encode an HTTP/2 GOAWAY Frame */ public ByteBuf encodeGoAwayFrame(int lastStreamId, int errorCode) { int length = 8; byte flags = 0; int streamId = 0; ByteBuf frame = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + length); writeFrameHeader(frame, length, HTTP_GOAWAY_FRAME, flags, streamId); frame.writeInt(lastStreamId); frame.writeInt(errorCode); return frame; } /** * Encode an HTTP/2 WINDOW_UPDATE Frame */ public ByteBuf encodeWindowUpdateFrame(int streamId, int windowSizeIncrement) { int length = 4; byte flags = 0; ByteBuf frame = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + length); writeFrameHeader(frame, length, HTTP_WINDOW_UPDATE_FRAME, flags, streamId); frame.writeInt(windowSizeIncrement); return frame; } private void writeFrameHeader(ByteBuf buffer, int length, int type, byte flags, int streamId) { buffer.writeMedium(length); buffer.writeByte(type); buffer.writeByte(flags); buffer.writeInt(streamId); } } ================================================ FILE: src/main/java/com/twitter/http2/HttpGoAwayFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; /** * An HTTP/2 GOAWAY Frame */ public interface HttpGoAwayFrame extends HttpFrame { /** * Returns the Last-Stream-ID of this frame. */ int getLastStreamId(); /** * Sets the Last-Stream-ID of this frame. The Last-Stream-ID cannot be negative. */ HttpGoAwayFrame setLastStreamId(int lastStreamId); /** * Returns the error code of this frame. */ HttpErrorCode getErrorCode(); /** * Sets the error code of this frame. */ HttpGoAwayFrame setErrorCode(HttpErrorCode errorCode); } ================================================ FILE: src/main/java/com/twitter/http2/HttpHeaderBlockDecoder.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.io.IOException; import java.nio.charset.StandardCharsets; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufInputStream; import io.netty.buffer.Unpooled; import io.netty.handler.codec.http.HttpHeaders; import com.twitter.hpack.Decoder; import com.twitter.hpack.HeaderListener; final class HttpHeaderBlockDecoder { private static final HeaderListener NULL_HEADER_LISTENER = new NullHeaderListener(); private Decoder decoder; private ByteBuf cumulation; public HttpHeaderBlockDecoder(int maxHeaderSize, int maxHeaderTableSize) { decoder = new Decoder(maxHeaderSize, maxHeaderTableSize); } /** * Set the maximum header table size allowed by the decoder. * This is the value of SETTINGS_HEADER_TABLE_SIZE sent to the peer. * * @param maxHeaderTableSize the maximum header table size allowed by the decoder */ public void setMaxHeaderTableSize(int maxHeaderTableSize) { decoder.setMaxHeaderTableSize(maxHeaderTableSize); } public void decode(ByteBuf headerBlock, final HttpHeaderBlockFrame frame) throws IOException { HeaderListener headerListener = NULL_HEADER_LISTENER; if (frame != null) { headerListener = new HeaderListenerImpl(frame.headers()); } if (cumulation == null) { decoder.decode(new ByteBufInputStream(headerBlock), headerListener); if (headerBlock.isReadable()) { cumulation = Unpooled.buffer(headerBlock.readableBytes()); cumulation.writeBytes(headerBlock); } } else { cumulation.writeBytes(headerBlock); decoder.decode(new ByteBufInputStream(cumulation), headerListener); if (cumulation.isReadable()) { cumulation.discardReadBytes(); } else { cumulation.release(); cumulation = null; } } } public void endHeaderBlock(final HttpHeaderBlockFrame frame) { if (cumulation != null) { if (cumulation.isReadable() && frame != null) { frame.setInvalid(); } cumulation.release(); cumulation = null; } boolean truncated = decoder.endHeaderBlock(); if (truncated && frame != null) { frame.setTruncated(); } } private static final class NullHeaderListener implements HeaderListener { @Override public void addHeader(byte[] name, byte[] value, boolean sensitive) { // No Op } } private static final class HeaderListenerImpl implements HeaderListener { private final HttpHeaders headers; HeaderListenerImpl(HttpHeaders headers) { this.headers = headers; } @Override public void addHeader(byte[] name, byte[] value, boolean sensitive) { String nameStr = new String(name, StandardCharsets.UTF_8); // check for empty value if (value.length == 0) { addHeader(nameStr, ""); return; } // Sec. 8.1.3.3. Header Field Ordering int index = 0; int offset = 0; while (index < value.length) { while (index < value.length && value[index] != (byte) 0) { index++; } if (index - offset == 0) { addHeader(nameStr, ""); } else { String valueStr = new String(value, offset, index - offset, StandardCharsets.UTF_8); addHeader(nameStr, valueStr); } index++; offset = index; } } private void addHeader(String name, String value) { boolean crumb = "cookie".equalsIgnoreCase(name); if (value.length() == 0) { if (crumb || headers.contains(name)) { return; } } if (crumb) { // Sec. 8.1.3.4. Cookie Header Field String cookie = headers.get(name); if (cookie == null) { headers.set(name, value); } else { headers.set(name, cookie + "; " + value); } } else { headers.add(name, value); } } } } ================================================ FILE: src/main/java/com/twitter/http2/HttpHeaderBlockEncoder.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Locale; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufOutputStream; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelHandlerContext; import com.twitter.hpack.Encoder; public class HttpHeaderBlockEncoder { private static final byte[] COOKIE = {'c', 'o', 'o', 'k', 'i', 'e'}; private static final byte[] EMPTY = {}; private int encoderMaxHeaderTableSize; private int decoderMaxHeaderTableSize; private int maxHeaderTableSize; private Encoder encoder; /** * Create a new instance. */ public HttpHeaderBlockEncoder(int maxHeaderTableSize) { encoderMaxHeaderTableSize = maxHeaderTableSize; decoderMaxHeaderTableSize = maxHeaderTableSize; this.maxHeaderTableSize = maxHeaderTableSize; encoder = new Encoder(maxHeaderTableSize); } /** * Set the maximum header table size allowed by the encoder. * * @param encoderMaxHeaderTableSize the maximum header table size allowed by the encoder */ public void setEncoderMaxHeaderTableSize(int encoderMaxHeaderTableSize) { this.encoderMaxHeaderTableSize = encoderMaxHeaderTableSize; if (encoderMaxHeaderTableSize < maxHeaderTableSize) { maxHeaderTableSize = encoderMaxHeaderTableSize; } } /** * Set the maximum header table size allowed by the peer's encoder. * This is the value of SETTINGS_HEADER_TABLE_SIZE received from the peer. * * @param decoderMaxHeaderTableSize the maximum header table size allowed by the decoder */ public void setDecoderMaxHeaderTableSize(int decoderMaxHeaderTableSize) { this.decoderMaxHeaderTableSize = decoderMaxHeaderTableSize; if (decoderMaxHeaderTableSize < maxHeaderTableSize) { maxHeaderTableSize = decoderMaxHeaderTableSize; } } /** * Encode the header block frame. */ public ByteBuf encode(ChannelHandlerContext ctx, HttpHeaderBlockFrame frame) throws IOException { ByteBuf buf = Unpooled.buffer(); ByteBufOutputStream out = new ByteBufOutputStream(buf); // The current allowable max header table size is the // minimum of the encoder and decoder allowable sizes int allowableHeaderTableSize = Math.min(encoderMaxHeaderTableSize, decoderMaxHeaderTableSize); // maxHeaderTableSize will hold the smallest size seen the // last call to encode. This might be smaller than the // current allowable max header table size if (maxHeaderTableSize < allowableHeaderTableSize) { encoder.setMaxHeaderTableSize(out, maxHeaderTableSize); } // Check if the current allowable size is equal to the encoder's // capacity and set the new size if necessary if (allowableHeaderTableSize != encoder.getMaxHeaderTableSize()) { encoder.setMaxHeaderTableSize(out, allowableHeaderTableSize); } // Store the current allowable size for the next call maxHeaderTableSize = allowableHeaderTableSize; // Now we can encode headers for (String name : frame.headers().names()) { if ("cookie".equalsIgnoreCase(name)) { // Sec. 8.1.3.4. Cookie Header Field for (String value : frame.headers().getAll(name)) { for (String crumb : value.split(";")) { byte[] valueBytes = crumb.trim().getBytes(StandardCharsets.UTF_8); encoder.encodeHeader(out, COOKIE, valueBytes, true); } } } else { byte[] nameBytes = name.toLowerCase(Locale.ENGLISH).getBytes(StandardCharsets.UTF_8); // Sec. 8.1.3.3. Header Field Ordering List values = frame.headers().getAll(name); if (values.size() == 0) { encoder.encodeHeader(out, nameBytes, EMPTY, false); } else { for (String value : values) { byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8); encoder.encodeHeader(out, nameBytes, valueBytes, false); } } } } return buf; } } ================================================ FILE: src/main/java/com/twitter/http2/HttpHeaderBlockFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.handler.codec.http.HttpHeaders; public interface HttpHeaderBlockFrame extends HttpStreamFrame { /** * Returns {@code true} if this header block is invalid. * A RST_STREAM frame with code PROTOCOL_ERROR should be sent. */ boolean isInvalid(); /** * Marks this header block as invalid. */ HttpHeaderBlockFrame setInvalid(); /** * Returns {@code true} if this header block has been truncated due to length restrictions. */ boolean isTruncated(); /** * Mark this header block as truncated. */ HttpHeaderBlockFrame setTruncated(); /** * Returns the {@link HttpHeaders} of this frame. */ HttpHeaders headers(); } ================================================ FILE: src/main/java/com/twitter/http2/HttpHeadersFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; /** * An HTTP/2 HEADERS Frame */ public interface HttpHeadersFrame extends HttpHeaderBlockFrame { /** * Returns {@code true} if this frame is the last frame to be transmitted on the stream. */ boolean isLast(); /** * Sets if this frame is the last frame to be transmitted on the stream. */ HttpHeadersFrame setLast(boolean last); /** * Returns {@code true} if the dependency of the stream is exclusive. */ boolean isExclusive(); /** * Sets if the dependency of the stream is exclusive. */ HttpHeadersFrame setExclusive(boolean exclusive); /** * Returns the dependency of the stream. */ int getDependency(); /** * Sets the dependency of the stream. The dependency cannot be negative. */ HttpHeadersFrame setDependency(int dependency); /** * Returns the weight of the dependency of the stream. */ int getWeight(); /** * Sets the weight of the dependency of the stream. * The weight must be positive and cannot exceed 256 bytes. */ HttpHeadersFrame setWeight(int weight); @Override HttpHeadersFrame setStreamId(int streamId); @Override HttpHeadersFrame setInvalid(); @Override HttpHeadersFrame setTruncated(); } ================================================ FILE: src/main/java/com/twitter/http2/HttpMessageProxy.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpVersion; /** * An {@link HttpMessage} decorator. */ class HttpMessageProxy implements HttpMessage { private final HttpMessage message; protected HttpMessageProxy(HttpMessage message) { this.message = message; } @Override public HttpVersion getProtocolVersion() { return message.getProtocolVersion(); } @Override public HttpMessage setProtocolVersion(HttpVersion version) { message.setProtocolVersion(version); return this; } @Override public HttpHeaders headers() { return message.headers(); } @Override @Deprecated public DecoderResult getDecoderResult() { return message.getDecoderResult(); } @Override public void setDecoderResult(DecoderResult result) { message.setDecoderResult(result); } @Override public HttpVersion protocolVersion() { return message.protocolVersion(); } @Override public DecoderResult decoderResult() { return message.decoderResult(); } } ================================================ FILE: src/main/java/com/twitter/http2/HttpPingFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; /** * An HTTP/2 PING Frame */ public interface HttpPingFrame extends HttpFrame { /** * Returns the data payload of this frame. */ long getData(); /** * Sets the data payload of this frame. */ HttpPingFrame setData(long data); /** * Returns {@code true} if this frame is in response to a PING. */ boolean isPong(); /** * Sets if this frame is in response to a PING. */ HttpPingFrame setPong(boolean pong); } ================================================ FILE: src/main/java/com/twitter/http2/HttpPriorityFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; /** * An HTTP/2 PRIORITY Frame */ public interface HttpPriorityFrame extends HttpFrame { /** * Returns the stream identifier of this frame. */ int getStreamId(); /** * Sets the stream identifier of this frame. The stream identifier must be positive. */ HttpPriorityFrame setStreamId(int streamId); /** * Returns {@code true} if the dependency of the stream is exclusive. */ boolean isExclusive(); /** * Sets if the dependency of the stream is exclusive. */ HttpPriorityFrame setExclusive(boolean exclusive); /** * Returns the dependency of the stream. */ int getDependency(); /** * Sets the dependency of the stream. The dependency cannot be negative. */ HttpPriorityFrame setDependency(int dependency); /** * Returns the weight of the dependency of the stream. */ int getWeight(); /** * Sets the weight of the dependency of the stream. * The weight must be positive and cannot exceed 256 bytes. */ HttpPriorityFrame setWeight(int weight); } ================================================ FILE: src/main/java/com/twitter/http2/HttpProtocolException.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; public class HttpProtocolException extends Exception { /** * Creates a new instance. */ public HttpProtocolException() { super(); } /** * Creates a new instance. */ public HttpProtocolException(String message, Throwable cause) { super(message, cause); } /** * Creates a new instance. */ public HttpProtocolException(String message) { super(message); } /** * Creates a new instance. */ public HttpProtocolException(Throwable cause) { super(cause); } } ================================================ FILE: src/main/java/com/twitter/http2/HttpPushPromiseFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; /** * An HTTP/2 PUSH_PROMISE Frame */ public interface HttpPushPromiseFrame extends HttpHeaderBlockFrame { /** * Returns the Promised-Stream-ID of this frame. */ int getPromisedStreamId(); /** * Sets the Promised-Stream-ID of this frame. The Promised-Stream-ID must be positive. */ HttpPushPromiseFrame setPromisedStreamId(int promisedStreamId); @Override HttpPushPromiseFrame setStreamId(int streamId); @Override HttpPushPromiseFrame setInvalid(); @Override HttpPushPromiseFrame setTruncated(); } ================================================ FILE: src/main/java/com/twitter/http2/HttpRequestProxy.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; /** * An {@link HttpRequest} decorator. */ class HttpRequestProxy extends HttpMessageProxy implements HttpRequest { private final HttpRequest request; public HttpRequestProxy(HttpRequest request) { super(request); this.request = request; } public HttpRequest httpRequest() { return request; } @Override @Deprecated public HttpMethod getMethod() { return request.getMethod(); } @Override public HttpRequest setMethod(HttpMethod method) { request.setMethod(method); return this; } @Override @Deprecated public String getUri() { return request.getUri(); } @Override public HttpRequest setUri(String uri) { request.setUri(uri); return this; } @Override public HttpRequest setProtocolVersion(HttpVersion version) { request.setProtocolVersion(version); return this; } @Override public String toString() { return request.toString(); } @Override public HttpMethod method() { return request.method(); } @Override public String uri() { return request.uri(); } } ================================================ FILE: src/main/java/com/twitter/http2/HttpResponseProxy.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; /** * An {@link HttpResponse} decorator. */ public class HttpResponseProxy extends HttpMessageProxy implements HttpResponse { private final HttpResponse response; public HttpResponseProxy(HttpResponse response) { super(response); this.response = response; } @Override @Deprecated public HttpResponseStatus getStatus() { return response.getStatus(); } @Override public HttpResponse setStatus(HttpResponseStatus status) { response.setStatus(status); return this; } @Override public HttpResponse setProtocolVersion(HttpVersion version) { response.setProtocolVersion(version); return this; } @Override public String toString() { return response.toString(); } @Override public HttpResponseStatus status() { return response.status(); } } ================================================ FILE: src/main/java/com/twitter/http2/HttpRstStreamFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; /** * An HTTP/2 RST_STREAM Frame */ public interface HttpRstStreamFrame extends HttpFrame { /** * Returns the stream identifier of this frame. */ int getStreamId(); /** * Sets the stream identifier of this frame. The stream identifier must be positive. */ HttpRstStreamFrame setStreamId(int streamId); /** * Returns the error code of this frame. */ HttpErrorCode getErrorCode(); /** * Sets the error code of this frame. */ HttpRstStreamFrame setErrorCode(HttpErrorCode errorCode); } ================================================ FILE: src/main/java/com/twitter/http2/HttpSettingsFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.util.Set; /** * An HTTP/2 SETTINGS Frame */ public interface HttpSettingsFrame extends HttpFrame { int SETTINGS_HEADER_TABLE_SIZE = 1; int SETTINGS_ENABLE_PUSH = 2; int SETTINGS_MAX_CONCURRENT_STREAMS = 3; int SETTINGS_INITIAL_WINDOW_SIZE = 4; int SETTINGS_MAX_FRAME_SIZE = 5; int SETTINGS_MAX_HEADER_LIST_SIZE = 6; /** * Returns a {@code Set} of the setting IDs. * The set's iterator will return the IDs in ascending order. */ Set getIds(); /** * Returns {@code true} if the setting ID has a value. */ boolean isSet(int id); /** * Returns the value of the setting ID. * Returns -1 if the setting ID is not set. */ int getValue(int id); /** * Sets the value of the setting ID. * The ID cannot be negative and cannot exceed 65535. */ HttpSettingsFrame setValue(int id, int value); /** * Removes the value of the setting ID. */ HttpSettingsFrame removeValue(int id); /** * Returns {@code true} if this frame is an acknowledgement frame. */ boolean isAck(); /** * Sets if this frame is acknowledgement frame. */ HttpSettingsFrame setAck(boolean ack); } ================================================ FILE: src/main/java/com/twitter/http2/HttpStreamDecoder.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.util.List; import java.util.Map; import java.util.concurrent.CancellationException; import java.util.concurrent.ConcurrentHashMap; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.DecoderResult; import io.netty.handler.codec.MessageToMessageDecoder; import io.netty.handler.codec.http.DefaultHttpContent; import io.netty.handler.codec.http.DefaultLastHttpContent; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; /** * Decodes {@link HttpFrame}s into {@link StreamedHttpRequest}s. */ public class HttpStreamDecoder extends MessageToMessageDecoder { private static final CancellationException CANCELLATION_EXCEPTION = new CancellationException("HTTP/2 RST_STREAM Frame Received"); private final Map messageMap = new ConcurrentHashMap(); private final Map trailerMap = new ConcurrentHashMap(); @Override protected void decode(ChannelHandlerContext ctx, Object msg, List out) throws Exception { if (msg instanceof HttpHeadersFrame) { HttpHeadersFrame httpHeadersFrame = (HttpHeadersFrame) msg; int streamId = httpHeadersFrame.getStreamId(); StreamedHttpMessage message = messageMap.get(streamId); if (message == null) { if (httpHeadersFrame.headers().contains(":status")) { // If a client receives a reply with a truncated header block, // reply with a RST_STREAM frame with error code INTERNAL_ERROR. if (httpHeadersFrame.isTruncated()) { HttpRstStreamFrame httpRstStreamFrame = new DefaultHttpRstStreamFrame(streamId, HttpErrorCode.INTERNAL_ERROR); out.add(httpRstStreamFrame); return; } try { StreamedHttpResponse response = createHttpResponse(httpHeadersFrame); if (httpHeadersFrame.isLast()) { HttpHeaders.setContentLength(response, 0); response.getContent().close(); } else { // Response body will follow in a series of Data Frames if (!HttpHeaders.isContentLengthSet(response)) { HttpHeaders.setTransferEncodingChunked(response); } messageMap.put(streamId, response); } out.add(response); } catch (Exception e) { // If a client receives a SYN_REPLY without valid getStatus and version headers // the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR HttpRstStreamFrame httpRstStreamFrame = new DefaultHttpRstStreamFrame(streamId, HttpErrorCode.PROTOCOL_ERROR); ctx.writeAndFlush(httpRstStreamFrame); out.add(httpRstStreamFrame); } } else { // If a client sends a request with a truncated header block, the server must // reply with a HTTP 431 REQUEST HEADER FIELDS TOO LARGE reply. if (httpHeadersFrame.isTruncated()) { httpHeadersFrame = new DefaultHttpHeadersFrame(streamId); httpHeadersFrame.setLast(true); httpHeadersFrame.headers().set( ":status", HttpResponseStatus.REQUEST_HEADER_FIELDS_TOO_LARGE.code()); ctx.writeAndFlush(httpHeadersFrame); return; } try { message = createHttpRequest(httpHeadersFrame); if (httpHeadersFrame.isLast()) { message.setDecoderResult(DecoderResult.SUCCESS); message.getContent().close(); } else { // Request body will follow in a series of Data Frames messageMap.put(streamId, message); } out.add(message); } catch (Exception e) { // If a client sends a SYN_STREAM without all of the method, url (host and path), // scheme, and version headers the server must reply with a HTTP 400 BAD REQUEST reply. // Also sends HTTP 400 BAD REQUEST reply if header name/value pairs are invalid httpHeadersFrame = new DefaultHttpHeadersFrame(streamId); httpHeadersFrame.setLast(true); httpHeadersFrame.headers().set(":status", HttpResponseStatus.BAD_REQUEST.code()); ctx.writeAndFlush(httpHeadersFrame); } } } else { LastHttpContent trailer = trailerMap.remove(streamId); if (trailer == null) { trailer = new DefaultLastHttpContent(); } // Ignore trailers in a truncated HEADERS frame. if (!httpHeadersFrame.isTruncated()) { for (Map.Entry e: httpHeadersFrame.headers()) { trailer.trailingHeaders().add(e.getKey(), e.getValue()); } } if (httpHeadersFrame.isLast()) { messageMap.remove(streamId); message.addContent(trailer); } else { trailerMap.put(streamId, trailer); } } } else if (msg instanceof HttpDataFrame) { HttpDataFrame httpDataFrame = (HttpDataFrame) msg; int streamId = httpDataFrame.getStreamId(); StreamedHttpMessage message = messageMap.get(streamId); // If message is not in map discard Data Frame. if (message == null) { return; } ByteBuf content = httpDataFrame.content(); if (content.isReadable()) { content.retain(); message.addContent(new DefaultHttpContent(content)); } if (httpDataFrame.isLast()) { messageMap.remove(streamId); message.addContent(LastHttpContent.EMPTY_LAST_CONTENT); message.setDecoderResult(DecoderResult.SUCCESS); } } else if (msg instanceof HttpRstStreamFrame) { HttpRstStreamFrame httpRstStreamFrame = (HttpRstStreamFrame) msg; int streamId = httpRstStreamFrame.getStreamId(); StreamedHttpMessage message = messageMap.remove(streamId); if (message != null) { message.getContent().close(); message.setDecoderResult(DecoderResult.failure(CANCELLATION_EXCEPTION)); } } else { // HttpGoAwayFrame out.add(msg); } } private StreamedHttpRequest createHttpRequest(HttpHeadersFrame httpHeadersFrame) throws Exception { // Create the first line of the request from the name/value pairs HttpMethod method = HttpMethod.valueOf(httpHeadersFrame.headers().get(":method")); String url = httpHeadersFrame.headers().get(":path"); httpHeadersFrame.headers().remove(":method"); httpHeadersFrame.headers().remove(":path"); StreamedHttpRequest request = new StreamedHttpRequest(HttpVersion.HTTP_1_1, method, url); // Remove the scheme header httpHeadersFrame.headers().remove(":scheme"); // Replace the SPDY host header with the HTTP host header String host = httpHeadersFrame.headers().get(":authority"); httpHeadersFrame.headers().remove(":authority"); httpHeadersFrame.headers().set("host", host); for (Map.Entry e : httpHeadersFrame.headers()) { String name = e.getKey(); String value = e.getValue(); if (name.charAt(0) != ':') { request.headers().add(name, value); } } // Set the Stream-ID as a header request.headers().set("X-SPDY-Stream-ID", httpHeadersFrame.getStreamId()); // The Connection and Keep-Alive headers are no longer valid HttpHeaders.setKeepAlive(request, true); // Transfer-Encoding header is not valid request.headers().remove(HttpHeaders.Names.TRANSFER_ENCODING); if (httpHeadersFrame.isLast()) { request.getContent().close(); request.setDecoderResult(DecoderResult.SUCCESS); } else { request.setDecoderResult(DecoderResult.UNFINISHED); } return request; } private StreamedHttpResponse createHttpResponse(HttpHeadersFrame httpHeadersFrame) throws Exception { // Create the first line of the request from the name/value pairs HttpResponseStatus status = HttpResponseStatus.valueOf(Integer.parseInt( httpHeadersFrame.headers().get(":status"))); httpHeadersFrame.headers().remove(":status"); StreamedHttpResponse response = new StreamedHttpResponse(HttpVersion.HTTP_1_1, status); for (Map.Entry e : httpHeadersFrame.headers()) { String name = e.getKey(); String value = e.getValue(); if (name.charAt(0) != ':') { response.headers().add(name, value); } } // Set the Stream-ID as a header response.headers().set("X-SPDY-Stream-ID", httpHeadersFrame.getStreamId()); // The Connection and Keep-Alive headers are no longer valid HttpHeaders.setKeepAlive(response, true); // Transfer-Encoding header is not valid response.headers().remove(HttpHeaders.Names.TRANSFER_ENCODING); response.headers().remove(HttpHeaders.Names.TRAILER); if (httpHeadersFrame.isLast()) { response.getContent().close(); response.setDecoderResult(DecoderResult.SUCCESS); } else { response.setDecoderResult(DecoderResult.UNFINISHED); } return response; } } ================================================ FILE: src/main/java/com/twitter/http2/HttpStreamEncoder.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.util.Map; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelOutboundHandlerAdapter; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.FutureListener; /** * Encodes {@link StreamedHttpResponse}s into {@link HttpFrame}s. */ public class HttpStreamEncoder extends ChannelOutboundHandlerAdapter { private static final int MAX_DATA_LENGTH = 0x2000; // Limit Data Frames to 8k private int currentStreamId; @Override public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { if (msg instanceof HttpRequest) { HttpRequest httpRequest = (HttpRequest) msg; HttpHeadersFrame httpHeadersFrame = createHttpHeadersFrame(httpRequest); currentStreamId = httpHeadersFrame.getStreamId(); ChannelPromise writeFuture = getMessageFuture(ctx, promise, currentStreamId, httpRequest); if (promise == writeFuture) { httpHeadersFrame.setLast(true); } else { promise = writeFuture; } ctx.write(httpHeadersFrame, promise); } else if (msg instanceof HttpResponse) { HttpResponse httpResponse = (HttpResponse) msg; HttpHeadersFrame httpHeadersFrame = createHttpHeadersFrame(httpResponse); currentStreamId = httpHeadersFrame.getStreamId(); ChannelPromise writeFuture = getMessageFuture(ctx, promise, currentStreamId, httpResponse); if (promise == writeFuture) { httpHeadersFrame.setLast(true); } else { promise = writeFuture; } ctx.write(httpHeadersFrame, promise); } else if (msg instanceof HttpContent) { HttpContent chunk = (HttpContent) msg; writeChunk(ctx, promise, currentStreamId, chunk); } else { // Unknown message type ctx.write(msg, promise); } } private ChannelPromise getMessageFuture( final ChannelHandlerContext ctx, final ChannelPromise promise, final int streamId, HttpMessage message ) { if (message instanceof StreamedHttpMessage && !((StreamedHttpMessage) message).getContent().isClosed()) { final Pipe pipe = ((StreamedHttpMessage) message).getContent(); ChannelPromise writeFuture = ctx.channel().newPromise(); writeFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { // Channel's thread // First frame has been written if (future.isSuccess()) { pipe.receive().addListener( new ChunkListener(ctx, streamId, pipe, promise)); } else if (future.isCancelled()) { pipe.close(); promise.cancel(true); } else { pipe.close(); promise.setFailure(future.cause()); } } }); return writeFuture; } else { return promise; } } /** * Listens to chunks being ready on a pipe. */ private class ChunkListener implements FutureListener { private final ChannelHandlerContext ctx; private final int streamId; private final Pipe pipe; private final ChannelPromise completionFuture; ChunkListener( ChannelHandlerContext ctx, int streamId, Pipe pipe, ChannelPromise completionFuture ) { this.ctx = ctx; this.streamId = streamId; this.pipe = pipe; this.completionFuture = completionFuture; } @Override public void operationComplete(final Future future) throws Exception { final FutureListener chunkListener = this; ctx.executor().execute(new Runnable() { @Override public void run() { if (future.isSuccess()) { HttpContent content = future.getNow(); ChannelPromise writeFuture; if (content instanceof LastHttpContent) { writeFuture = completionFuture; } else { writeFuture = ctx.channel().newPromise(); writeFuture.addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { pipe.receive().addListener(chunkListener); } else if (future.isCancelled()) { pipe.close(); completionFuture.cancel(true); } else { pipe.close(); completionFuture.setFailure(future.cause()); } } }); } writeChunk(ctx, writeFuture, streamId, content); } else { // Somebody closed the pipe // Send a reset frame to the channel and complete the completion future ctx.writeAndFlush( new DefaultHttpRstStreamFrame(streamId, HttpErrorCode.INTERNAL_ERROR)); if (future.isCancelled()) { completionFuture.cancel(true); } else { completionFuture.setFailure(future.cause()); } } } }); } } /** * Writes an HTTP chunk downstream as one or more HTTP/2 frames. */ protected void writeChunk( ChannelHandlerContext ctx, ChannelPromise future, int streamId, HttpContent content) { HttpFrame[] httpFrames = createHttpDataFrames(streamId, content.content()); if (content instanceof LastHttpContent) { LastHttpContent trailer = (LastHttpContent) content; HttpHeaders trailers = trailer.trailingHeaders(); if (trailers.isEmpty()) { if (httpFrames.length == 0) { HttpDataFrame httpDataFrame = new DefaultHttpDataFrame(streamId); httpDataFrame.setLast(true); httpFrames = new HttpFrame[1]; httpFrames[0] = httpDataFrame; } else { HttpDataFrame httpDataFrame = (HttpDataFrame) httpFrames[httpFrames.length - 1]; httpDataFrame.setLast(true); } } else { // Create HTTP HEADERS frame out of trailers HttpHeadersFrame httpHeadersFrame = new DefaultHttpHeadersFrame(streamId); httpHeadersFrame.setLast(true); for (Map.Entry entry : trailer.trailingHeaders()) { httpHeadersFrame.headers().add(entry.getKey(), entry.getValue()); } if (httpFrames.length == 0) { httpFrames = new HttpFrame[1]; httpFrames[0] = httpHeadersFrame; } else { HttpFrame[] copy = new HttpFrame[httpFrames.length + 1]; for (int i = 0; i < httpFrames.length; i++) { copy[i] = httpFrames[i]; } copy[httpFrames.length] = httpHeadersFrame; httpFrames = copy; } } } ChannelPromise frameFuture = getFrameFuture(ctx, future, httpFrames); // Trigger a write frameFuture.setSuccess(); } private static ChannelPromise getFrameFuture( ChannelHandlerContext ctx, ChannelPromise future, HttpFrame[] httpFrames) { ChannelPromise frameFuture = future; for (int i = httpFrames.length; --i >= 0; ) { future = ctx.channel().newPromise(); future.addListener(new HttpFrameWriter(ctx, frameFuture, httpFrames[i])); frameFuture = future; } return frameFuture; } private static class HttpFrameWriter implements ChannelFutureListener { private final ChannelHandlerContext ctx; private final ChannelPromise promise; private final Object msg; HttpFrameWriter(ChannelHandlerContext ctx, ChannelPromise promise, Object msg) { this.ctx = ctx; this.promise = promise; this.msg = msg; } public void operationComplete(ChannelFuture future) throws Exception { if (future.isSuccess()) { ctx.writeAndFlush(msg, promise); } else if (future.isCancelled()) { ReferenceCountUtil.release(msg); promise.cancel(true); } else { ReferenceCountUtil.release(msg); promise.setFailure(future.cause()); } } } private HttpHeadersFrame createHttpHeadersFrame(HttpRequest httpRequest) throws Exception { // Get the Stream-ID from the headers int streamId = HttpHeaders.getIntHeader(httpRequest, "X-SPDY-Stream-ID"); httpRequest.headers().remove("X-SPDY-Stream-ID"); // The Connection, Keep-Alive, Proxy-Connection, and Transfer-Encoding // headers are not valid and MUST not be sent. httpRequest.headers().remove(HttpHeaders.Names.CONNECTION); httpRequest.headers().remove("Keep-Alive"); httpRequest.headers().remove("Proxy-Connection"); httpRequest.headers().remove(HttpHeaders.Names.TRANSFER_ENCODING); HttpHeadersFrame httpHeadersFrame = new DefaultHttpHeadersFrame(streamId); // Unfold the first line of the request into name/value pairs httpHeadersFrame.headers().add(":method", httpRequest.getMethod().name()); httpHeadersFrame.headers().set(":scheme", "https"); httpHeadersFrame.headers().add(":path", httpRequest.getUri()); // Replace the HTTP host header with the SPDY host header String host = httpRequest.headers().get(HttpHeaders.Names.HOST); httpRequest.headers().remove(HttpHeaders.Names.HOST); httpHeadersFrame.headers().add(":authority", host); // Transfer the remaining HTTP headers for (Map.Entry entry : httpRequest.headers()) { httpHeadersFrame.headers().add(entry.getKey(), entry.getValue()); } return httpHeadersFrame; } private HttpHeadersFrame createHttpHeadersFrame(HttpResponse httpResponse) throws Exception { // Get the Stream-ID from the headers int streamId = HttpHeaders.getIntHeader(httpResponse, "X-SPDY-Stream-ID"); httpResponse.headers().remove("X-SPDY-Stream-ID"); // The Connection, Keep-Alive, Proxy-Connection, and Transfer-Encoding // headers are not valid and MUST not be sent. httpResponse.headers().remove(HttpHeaders.Names.CONNECTION); httpResponse.headers().remove("Keep-Alive"); httpResponse.headers().remove("Proxy-Connection"); httpResponse.headers().remove(HttpHeaders.Names.TRANSFER_ENCODING); HttpHeadersFrame httpHeadersFrame = new DefaultHttpHeadersFrame(streamId); // Unfold the first line of the response into name/value pairs httpHeadersFrame.headers().set(":status", httpResponse.getStatus().code()); // Transfer the remaining HTTP headers for (Map.Entry entry : httpResponse.headers()) { httpHeadersFrame.headers().add(entry.getKey(), entry.getValue()); } return httpHeadersFrame; } private HttpDataFrame[] createHttpDataFrames(int streamId, ByteBuf content) { int readableBytes = content.readableBytes(); int count = readableBytes / MAX_DATA_LENGTH; if (readableBytes % MAX_DATA_LENGTH > 0) { count++; } HttpDataFrame[] httpDataFrames = new HttpDataFrame[count]; for (int i = 0; i < count; i++) { int dataSize = Math.min(content.readableBytes(), MAX_DATA_LENGTH); HttpDataFrame httpDataFrame = new DefaultHttpDataFrame(streamId, content.readSlice(dataSize)); httpDataFrames[i] = httpDataFrame; } return httpDataFrames; } } ================================================ FILE: src/main/java/com/twitter/http2/HttpStreamFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; /** * An HTTP/2 Frame that is associated with an individual stream */ public interface HttpStreamFrame extends HttpFrame { /** * Returns the stream identifier of this frame. */ int getStreamId(); /** * Sets the stream identifier of this frame. The stream identifier must be positive. */ HttpStreamFrame setStreamId(int streamId); } ================================================ FILE: src/main/java/com/twitter/http2/HttpWindowUpdateFrame.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; /** * An HTTP/2 WINDOW_UPDATE Frame */ public interface HttpWindowUpdateFrame extends HttpFrame { /** * Returns the stream identifier of this frame. */ int getStreamId(); /** * Sets the stream identifier of this frame. The stream identifier cannot be negative. */ HttpWindowUpdateFrame setStreamId(int streamId); /** * Returns the Window-Size-Increment of this frame. */ int getWindowSizeIncrement(); /** * Sets the Window-Size-Increment of this frame. * The Window-Size-Increment must be positive. */ HttpWindowUpdateFrame setWindowSizeIncrement(int deltaWindowSize); } ================================================ FILE: src/main/java/com/twitter/http2/Pipe.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.util.LinkedList; import java.util.Objects; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import io.netty.channel.ChannelException; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.ImmediateEventExecutor; import io.netty.util.concurrent.Promise; /** * Implements a stream that pipes objects between its two ends. * Futures are used to communicate when messages are sent and received. * * @param the type of objects to send along the pipe */ public class Pipe { private static final ChannelException PIPE_CLOSED = new ChannelException("pipe closed"); private static final Future SENT_FUTURE = ImmediateEventExecutor.INSTANCE.newSucceededFuture(null); private static final Future CLOSED_FUTURE = ImmediateEventExecutor.INSTANCE.newFailedFuture(PIPE_CLOSED); private Queue sendQueue = new LinkedList(); private Queue> receiveQueue = new ConcurrentLinkedQueue>(); private boolean closed; /** * Holds a message and an associated future. */ private final class Node { public T message; public Promise promise; Node(T message, Promise promise) { this.message = message; this.promise = promise; } } /** * Creates a new pipe and uses instances of {@link ImmediateEventExecutor} * for the send and receive executors. * * @see ImmediateEventExecutor#INSTANCE */ public Pipe() { super(); } /** * Sends a message to this pipe. Returns a {@link Future} that is completed * when the message is received. *

* If the pipe is closed then this will return a failed future.

* * @param message the message to send to the pipe * @return a {@link Future} that is satisfied when the message is received, * or a failed future if the pipe is closed. * @throws NullPointerException if the message is {@code null}. * @throws IllegalStateException if the message could not be added to the queue for some reason. * @see #receive() */ public Future send(T message) { Objects.requireNonNull(message, "msg"); Promise receivePromise; synchronized (this) { if (closed) { return CLOSED_FUTURE; } receivePromise = receiveQueue.poll(); if (receivePromise == null) { Promise sendPromise = ImmediateEventExecutor.INSTANCE.newPromise(); sendQueue.add(new Node(message, sendPromise)); return sendPromise; } } receivePromise.setSuccess(message); return SENT_FUTURE; } /** * Receives a message from this pipe. *

* If the pipe is closed then this will return a failed future.

*/ public Future receive() { Node node; synchronized (this) { node = sendQueue.poll(); if (node == null) { if (closed) { return ImmediateEventExecutor.INSTANCE.newFailedFuture(PIPE_CLOSED); } Promise promise = ImmediateEventExecutor.INSTANCE.newPromise(); receiveQueue.add(promise); return promise; } } node.promise.setSuccess(null); return ImmediateEventExecutor.INSTANCE.newSucceededFuture(node.message); } /** * Closes this pipe. This fails all outstanding receive futures. * This does nothing if the pipe is already closed. */ public void close() { synchronized (this) { if (closed) { return; } closed = true; } while (!receiveQueue.isEmpty()) { receiveQueue.poll().setFailure(PIPE_CLOSED); } } /** * Checks if this pipe is closed. * * @return whether this pipe is closed. */ public synchronized boolean isClosed() { return closed; } } ================================================ FILE: src/main/java/com/twitter/http2/StreamedHttpMessage.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpMessage; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.concurrent.Future; /** * An {@link HttpMessage} that adds support for streaming content using {@linkplain Pipe pipes}. * @see Pipe */ public interface StreamedHttpMessage extends HttpMessage { /** * Gets the pipe for sending the message body as {@linkplain HttpContent} objects. * A {@linkplain LastHttpContent last http content} will indicate the end of the body. * * @return the message body pipe. * @see RFC 2616, 3.6.1 Chunked Transfer Coding * @see RFC 2616, 4.3 Message Body */ Pipe getContent(); /** * Adds content to the pipe. This returns a future for determining whether the message was received. * If the chunk is the last chunk then the pipe will be closed. *

* An {@link LastHttpContent} can be used for the trailer part of a chunked-encoded request.

*

* This method is preferable to {@code getContent().send()} because it does additional checks.

* * @param content the content to send * @return a future indicating when the message was received. */ Future addContent(HttpContent content); } ================================================ FILE: src/main/java/com/twitter/http2/StreamedHttpRequest.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.concurrent.Future; /** * An {@link HttpRequest} that adds content streaming. */ public class StreamedHttpRequest extends HttpRequestProxy implements StreamedHttpMessage { private Pipe pipe = new Pipe(); public StreamedHttpRequest(HttpVersion version, HttpMethod method, String uri) { this(new DefaultHttpRequest(version, method, uri)); } public StreamedHttpRequest(HttpRequest request) { super(request); } @Override public Pipe getContent() { return pipe; } @Override public Future addContent(HttpContent content) { Future future = pipe.send(content); if (content instanceof LastHttpContent) { pipe.close(); } return future; } } ================================================ FILE: src/main/java/com/twitter/http2/StreamedHttpResponse.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.HttpContent; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import io.netty.handler.codec.http.LastHttpContent; import io.netty.util.concurrent.Future; /** * An {@link HttpResponse} that adds content streaming. */ public class StreamedHttpResponse extends HttpResponseProxy implements StreamedHttpMessage { private Pipe pipe = new Pipe(); public StreamedHttpResponse(HttpVersion version, HttpResponseStatus status) { this(new DefaultHttpResponse(version, status)); } public StreamedHttpResponse(HttpResponse response) { super(response); } @Override public Pipe getContent() { return pipe; } @Override public Future addContent(HttpContent content) { Future future = pipe.send(content); if (content instanceof LastHttpContent) { pipe.close(); } return future; } } ================================================ FILE: src/test/java/com/twitter/http2/HttpFrameDecoderTest.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.util.Random; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import org.junit.Before; import org.junit.Test; import org.mockito.InOrder; import org.mockito.Mockito; import static io.netty.util.ReferenceCountUtil.releaseLater; import static org.mockito.Matchers.anyString; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.verifyZeroInteractions; import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_DEPENDENCY; import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_WEIGHT; import static com.twitter.http2.HttpCodecUtil.HTTP_FRAME_HEADER_SIZE; public class HttpFrameDecoderTest { private static final Random RANDOM = new Random(); private final HttpFrameDecoderDelegate delegate = Mockito.mock(HttpFrameDecoderDelegate.class); private HttpFrameDecoder decoder; @Before public void createHandler() { // Set server to false to ignore the Connection Header decoder = new HttpFrameDecoder(false, delegate); } @Test public void testClientConnectionPreface() throws Exception { decoder = new HttpFrameDecoder(true, delegate); ByteBuf connectionPreface = releaseLater(Unpooled.wrappedBuffer(new byte[]{ 0x50, 0x52, 0x49, 0x20, 0x2a, 0x20, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x32, 0x2e, 0x30, 0x0d, 0x0a, 0x0d, 0x0a, 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a })); int length = 0; byte flags = 0; int streamId = 0; // connection identifier ByteBuf frame = settingsFrame(length, flags, streamId); decoder.decode(releaseLater(Unpooled.wrappedBuffer(connectionPreface, frame))); verify(delegate).readSettingsFrame(false); verify(delegate).readSettingsEnd(); verifyNoMoreInteractions(delegate); } @Test public void testInvalidClientConnectionPreface() throws Exception { decoder = new HttpFrameDecoder(true, delegate); // Only write SETTINGS frame int length = 0; byte flags = 0; int streamId = 0; // connection identifier ByteBuf frame = settingsFrame(length, flags, streamId); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testHttpDataFrame() throws Exception { int length = RANDOM.nextInt() & 0x3FFF; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = dataFrame(length, flags, streamId); writeRandomData(frame, length); decoder.decode(frame); InOrder inOrder = inOrder(delegate); for (int i = 0; i < length; i += 8192) { // data frames do not exceed maxChunkSize int off = HTTP_FRAME_HEADER_SIZE + i; int len = Math.min(length - i, 8192); inOrder.verify(delegate).readDataFrame(streamId, false, false, frame.slice(off, len)); } verifyNoMoreInteractions(delegate); } @Test public void testEmptyHttpDataFrame() throws Exception { int length = 0; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = dataFrame(length, flags, streamId); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readDataFrame(streamId, false, false, Unpooled.EMPTY_BUFFER); verifyNoMoreInteractions(delegate); } @Test public void testLastHttpDataFrame() throws Exception { int length = 0; byte flags = 0x01; // END_STREAM int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = dataFrame(length, flags, streamId); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readDataFrame(streamId, true, false, Unpooled.EMPTY_BUFFER); verifyNoMoreInteractions(delegate); } @Test public void testLastSegmentHttpDataFrame() throws Exception { int length = 0; byte flags = 0x02; // END_SEGMENT int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = dataFrame(length, flags, streamId); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readDataFrame(streamId, false, true, Unpooled.EMPTY_BUFFER); verifyNoMoreInteractions(delegate); } @Test public void testPaddedHttpDataFrame() throws Exception { int length = RANDOM.nextInt() & 0x3FFF | 0x01; byte flags = 0x08; // PADDED int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int padding = Math.min(RANDOM.nextInt() & 0xFF, length - 1); ByteBuf frame = dataFrame(length, flags, streamId); frame.writeByte(padding); writeRandomData(frame, length - 1); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readDataFramePadding(streamId, false, padding + 1); int dataLength = length - 1 - padding; if (dataLength == 0) { inOrder.verify(delegate).readDataFrame(streamId, false, false, Unpooled.EMPTY_BUFFER); } else { for (int i = 0; i < dataLength; i += 8192) { // data frames do not exceed maxChunkSize int off = HTTP_FRAME_HEADER_SIZE + 1 + i; int len = Math.min(dataLength - i, 8192); inOrder.verify(delegate).readDataFrame(streamId, false, false, frame.slice(off, len)); } } verifyNoMoreInteractions(delegate); } @Test public void testHttpDataFrameReservedBits() throws Exception { int length = 0; byte flags = (byte) 0xC4; // should ignore any unknown flags int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = dataFrame(length, flags, streamId); setReservedBits(frame); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readDataFrame(streamId, false, false, Unpooled.EMPTY_BUFFER); verifyNoMoreInteractions(delegate); } @Test public void testIllegalHttpDataFrameStreamId() throws Exception { int length = 0; byte flags = 0; int streamId = 0; // illegal stream identifier ByteBuf frame = dataFrame(length, flags, streamId); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testIllegalHttpDataFrameLength() throws Exception { int length = 0; // illegal length byte flags = 0x08; // PADDED int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = dataFrame(length, flags, streamId); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testIllegalHttpDataFramePaddingLength() throws Exception { int length = 1; byte flags = 0x08; // PADDED int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = dataFrame(length, flags, streamId); frame.writeByte(1); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testHttpHeadersFrame() throws Exception { int headerBlockLength = 16; int length = 5 + headerBlockLength; byte flags = 0x04 | 0x20; // END_HEADERS | PRIORITY int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; boolean exclusive = RANDOM.nextBoolean(); int dependency = RANDOM.nextInt() & 0x7FFFFFFF; int weight = RANDOM.nextInt() & 0xFF; ByteBuf frame = headersFrame(length, flags, streamId); writePriorityFields(frame, exclusive, dependency, weight); writeRandomData(frame, headerBlockLength); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readHeadersFrame( streamId, false, false, exclusive, dependency, weight + 1); inOrder.verify(delegate).readHeaderBlock( frame.slice(HTTP_FRAME_HEADER_SIZE + 5, headerBlockLength)); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testLastHttpHeadersFrame() throws Exception { int length = 0; byte flags = 0x01 | 0x04; // END_STREAM | END_HEADERS int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = headersFrame(length, flags, streamId); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readHeadersFrame( streamId, true, false, false, HTTP_DEFAULT_DEPENDENCY, HTTP_DEFAULT_WEIGHT); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testLastSegmentHttpHeadersFrame() throws Exception { int length = 0; byte flags = 0x02 | 0x04; // END_SEGMENT | END_HEADERS int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = headersFrame(length, flags, streamId); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readHeadersFrame( streamId, false, true, false, HTTP_DEFAULT_DEPENDENCY, HTTP_DEFAULT_WEIGHT); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testHttpHeadersFrameReservedBits() throws Exception { int length = 0; byte flags = (byte) 0xC4; // END_HEADERS -- should ignore any unknown flags int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = headersFrame(length, flags, streamId); setReservedBits(frame); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readHeadersFrame( streamId, false, false, false, HTTP_DEFAULT_DEPENDENCY, HTTP_DEFAULT_WEIGHT); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testInvalidHttpHeadersFrame() throws Exception { int length = 0; byte flags = 0x04 | 0x20; // END_HEADERS | PRIORITY int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = headersFrame(length, flags, streamId); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testIllegalHttpHeadersFrame() throws Exception { int length = 0; byte flags = 0x04; // END_HEADERS int streamId = 0; // illegal stream identifier ByteBuf frame = headersFrame(length, flags, streamId); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testContinuedHttpHeadersFrame() throws Exception { int headerBlockLength = 16; int length = 5; byte flags = 0x20; // PRIORITY int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; boolean exclusive = RANDOM.nextBoolean(); int dependency = RANDOM.nextInt() & 0x7FFFFFFF; int weight = RANDOM.nextInt() & 0xFF; ByteBuf headersFrame = headersFrame(length, flags, streamId); writePriorityFields(headersFrame, exclusive, dependency, weight); ByteBuf continuationFrame = continuationFrame(headerBlockLength, (byte) 0x04, streamId); // END_HEADERS writeRandomData(continuationFrame, headerBlockLength); decoder.decode(releaseLater(Unpooled.wrappedBuffer(headersFrame, continuationFrame))); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readHeadersFrame( streamId, false, false, exclusive, dependency, weight + 1); inOrder.verify(delegate).readHeaderBlock( continuationFrame.slice(HTTP_FRAME_HEADER_SIZE, headerBlockLength)); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testHttpHeadersFrameEmptyContinuation() throws Exception { int length = 16; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf headersFrame = headersFrame(length, flags, streamId); writeRandomData(headersFrame, length); ByteBuf continuationFrame = continuationFrame(0, (byte) 0x04, streamId); // END_HEADERS decoder.decode(releaseLater(Unpooled.wrappedBuffer(headersFrame, continuationFrame))); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readHeadersFrame( streamId, false, false, false, HTTP_DEFAULT_DEPENDENCY, HTTP_DEFAULT_WEIGHT); inOrder.verify(delegate).readHeaderBlock( headersFrame.slice(HTTP_FRAME_HEADER_SIZE, length)); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testHttpHeadersFrameMultipleContinuations() throws Exception { int headerBlockLength = 16; int length = 5 + headerBlockLength; byte flags = 0x01 | 0x20; // END_STREAM | PRIORITY int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; boolean exclusive = RANDOM.nextBoolean(); int dependency = RANDOM.nextInt() & 0x7FFFFFFF; int weight = RANDOM.nextInt() & 0xFF; ByteBuf headersFrame = headersFrame(length, flags, streamId); writePriorityFields(headersFrame, exclusive, dependency, weight); writeRandomData(headersFrame, headerBlockLength); ByteBuf continuationFrame1 = continuationFrame(0, (byte) 0x00, streamId); ByteBuf continuationFrame2 = continuationFrame(0, (byte) 0x04, streamId); // END_HEADERS decoder.decode(releaseLater( Unpooled.wrappedBuffer(headersFrame, continuationFrame1, continuationFrame2))); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readHeadersFrame( streamId, true, false, exclusive, dependency, weight + 1); inOrder.verify(delegate).readHeaderBlock( headersFrame.slice(HTTP_FRAME_HEADER_SIZE + 5, headerBlockLength)); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testHttpHeadersFrameContinuationReservedFlags() throws Exception { int length = 16; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf headersFrame = headersFrame(length, flags, streamId); writeRandomData(headersFrame, length); ByteBuf continuationFrame = continuationFrame(0, (byte) 0xE7, streamId); // END_HEADERS decoder.decode(releaseLater(Unpooled.wrappedBuffer(headersFrame, continuationFrame))); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readHeadersFrame( streamId, false, false, false, HTTP_DEFAULT_DEPENDENCY, HTTP_DEFAULT_WEIGHT); inOrder.verify(delegate).readHeaderBlock(headersFrame.slice(HTTP_FRAME_HEADER_SIZE, 16)); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testHttpHeadersFrameContinuationIllegalStreamId() throws Exception { int length = 16; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf headersFrame = headersFrame(length, flags, streamId); writeRandomData(headersFrame, length); // different stream identifier ByteBuf continuationFrame = continuationFrame(0, (byte) 0x04, streamId + 1); // END_HEADERS decoder.decode(releaseLater(Unpooled.wrappedBuffer(headersFrame, continuationFrame))); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readHeadersFrame( streamId, false, false, false, HTTP_DEFAULT_DEPENDENCY, HTTP_DEFAULT_WEIGHT); inOrder.verify(delegate).readHeaderBlock( headersFrame.slice(HTTP_FRAME_HEADER_SIZE, 16)); inOrder.verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testHttpHeadersFrameMissingContinuation() throws Exception { int length = 5; byte flags = 0x20; // PRIORITY int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; boolean exclusive = RANDOM.nextBoolean(); int dependency = RANDOM.nextInt() & 0x7FFFFFFF; int weight = RANDOM.nextInt() & 0xFF; ByteBuf headersFrame = headersFrame(length, flags, streamId); writePriorityFields(headersFrame, exclusive, dependency, weight); ByteBuf dataFrame = dataFrame(0, (byte) 0, streamId); decoder.decode(releaseLater(Unpooled.wrappedBuffer(headersFrame, dataFrame))); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readHeadersFrame( streamId, false, false, exclusive, dependency, weight + 1); inOrder.verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testHttpPriorityFrame() throws Exception { int length = 5; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; boolean exclusive = RANDOM.nextBoolean(); int dependency = RANDOM.nextInt() & 0x7FFFFFFF; int weight = RANDOM.nextInt() & 0xFF; ByteBuf frame = priorityFrame(length, flags, streamId); writePriorityFields(frame, exclusive, dependency, weight); decoder.decode(frame); verify(delegate).readPriorityFrame(streamId, exclusive, dependency, weight + 1); verifyNoMoreInteractions(delegate); } @Test public void testHttpPriorityFrameReservedBits() throws Exception { int length = 5; byte flags = (byte) 0xFF; // should ignore any flags int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; boolean exclusive = RANDOM.nextBoolean(); int dependency = RANDOM.nextInt() & 0x7FFFFFFF; int weight = RANDOM.nextInt() & 0xFF; ByteBuf frame = priorityFrame(length, flags, streamId); setReservedBits(frame); writePriorityFields(frame, exclusive, dependency, weight); decoder.decode(frame); verify(delegate).readPriorityFrame(streamId, exclusive, dependency, weight + 1); verifyNoMoreInteractions(delegate); } @Test public void testInvalidHttpPriorityFrame() throws Exception { int length = 8; // invalid length byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; boolean exclusive = RANDOM.nextBoolean(); int dependency = RANDOM.nextInt() & 0x7FFFFFFF; int weight = RANDOM.nextInt() & 0xFF; ByteBuf frame = priorityFrame(length, flags, streamId); writePriorityFields(frame, exclusive, dependency, weight); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testIllegalHttpPriorityFrame() throws Exception { int length = 5; byte flags = 0; int streamId = 0; // illegal stream identifier boolean exclusive = RANDOM.nextBoolean(); int dependency = RANDOM.nextInt() & 0x7FFFFFFF; int weight = RANDOM.nextInt() & 0xFF; ByteBuf frame = priorityFrame(length, flags, streamId); writePriorityFields(frame, exclusive, dependency, weight); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testHttpRstStreamFrame() throws Exception { int length = 4; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int errorCode = RANDOM.nextInt(); ByteBuf frame = rstStreamFrame(length, flags, streamId); frame.writeInt(errorCode); decoder.decode(frame); verify(delegate).readRstStreamFrame(streamId, errorCode); verifyNoMoreInteractions(delegate); } @Test public void testHttpRstStreamFrameReservedBits() throws Exception { int length = 4; byte flags = (byte) 0xFF; // should ignore any flags int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int errorCode = RANDOM.nextInt(); ByteBuf frame = rstStreamFrame(length, flags, streamId); setReservedBits(frame); frame.writeInt(errorCode); decoder.decode(frame); verify(delegate).readRstStreamFrame(streamId, errorCode); verifyNoMoreInteractions(delegate); } @Test public void testInvalidHttpRstStreamFrame() throws Exception { int length = 8; // invalid length byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int errorCode = RANDOM.nextInt(); ByteBuf frame = rstStreamFrame(length, flags, streamId); frame.writeInt(errorCode); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testIllegalHttpRstStreamFrame() throws Exception { int length = 4; byte flags = 0; int streamId = 0; // illegal stream identifier int errorCode = RANDOM.nextInt(); ByteBuf frame = rstStreamFrame(length, flags, streamId); frame.writeInt(errorCode); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testHttpSettingsFrame() throws Exception { int length = 6; byte flags = 0; int streamId = 0; // connection identifier int id = RANDOM.nextInt() & 0xFFFF; int value = RANDOM.nextInt(); ByteBuf frame = settingsFrame(length, flags, streamId); frame.writeShort(id); frame.writeInt(value); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readSettingsFrame(false); inOrder.verify(delegate).readSetting(id, value); inOrder.verify(delegate).readSettingsEnd(); verifyNoMoreInteractions(delegate); } @Test public void testHttpSettingsAckFrame() throws Exception { int length = 0; byte flags = 0x01; // ACK int streamId = 0; // connection identifier ByteBuf frame = settingsFrame(length, flags, streamId); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readSettingsFrame(true); inOrder.verify(delegate).readSettingsEnd(); verifyNoMoreInteractions(delegate); } @Test public void testHttpSettingsFrameWithMultiples() throws Exception { int length = 12; byte flags = 0; int streamId = 0; // connection identifier int id = RANDOM.nextInt() & 0xFFFF; int value1 = RANDOM.nextInt(); int value2 = RANDOM.nextInt(); ByteBuf frame = settingsFrame(length, flags, streamId); frame.writeShort(id); frame.writeInt(value1); frame.writeShort(id); frame.writeInt(value2); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readSettingsFrame(false); inOrder.verify(delegate).readSetting(id, value1); inOrder.verify(delegate).readSetting(id, value2); inOrder.verify(delegate).readSettingsEnd(); verifyNoMoreInteractions(delegate); } @Test public void testHttpSettingsFrameReservedBits() throws Exception { int length = 6; byte flags = (byte) 0xFE; // should ignore any unknown flags int streamId = 0; // connection identifier int id = RANDOM.nextInt() & 0xFFFF; int value = RANDOM.nextInt(); ByteBuf frame = settingsFrame(length, flags, streamId); setReservedBits(frame); frame.writeShort(id); frame.writeInt(value); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readSettingsFrame(false); inOrder.verify(delegate).readSetting(id, value); inOrder.verify(delegate).readSettingsEnd(); verifyNoMoreInteractions(delegate); } @Test public void testInvalidHttpSettingsFrame() throws Exception { int length = 8; // invalid length byte flags = 0; int streamId = 0; // connection identifier int id = RANDOM.nextInt() & 0xFFFF; int value = RANDOM.nextInt(); ByteBuf frame = settingsFrame(length, flags, streamId); frame.writeShort(id); frame.writeInt(value); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testInvalidHttpSettingsAckFrame() throws Exception { int length = 6; // invalid length byte flags = 0x01; // ACK int streamId = 0; // connection identifier int id = RANDOM.nextInt() & 0xFFFF; int value = RANDOM.nextInt(); ByteBuf frame = settingsFrame(length, flags, streamId); frame.writeShort(id); frame.writeInt(value); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testIllegalHttpSettingsFrame() throws Exception { int length = 6; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; // illegal stream identifier int id = RANDOM.nextInt() & 0xFFFF; int value = RANDOM.nextInt(); ByteBuf frame = settingsFrame(length, flags, streamId); frame.writeShort(id); frame.writeInt(value); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testIllegalHttpSettingsAckFrame() throws Exception { int length = 0; byte flags = 0x01; // ACK int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; // illegal stream identifier ByteBuf frame = settingsFrame(length, flags, streamId); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testHttpPushPromiseFrame() throws Exception { int headerBlockLength = 16; int length = 4 + headerBlockLength; byte flags = 0x04; // END_HEADERS int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = pushPromiseFrame(length, flags, streamId); frame.writeInt(promisedStreamId); writeRandomData(frame, headerBlockLength); decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readPushPromiseFrame(streamId, promisedStreamId); inOrder.verify(delegate).readHeaderBlock( frame.slice(HTTP_FRAME_HEADER_SIZE + 4, headerBlockLength)); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testHttpPushPromiseFrameReservedBits() throws Exception { int length = 4; byte flags = (byte) 0xE7; // END_HEADERS -- should ignore any unknown flags int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = pushPromiseFrame(length, flags, streamId); setReservedBits(frame); frame.writeInt(promisedStreamId | 0x80000000); // should ignore reserved bit decoder.decode(frame); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readPushPromiseFrame(streamId, promisedStreamId); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testIllegalHttpPushPromiseFrame() throws Exception { int length = 4; byte flags = 0x04; // END_HEADERS int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int promisedStreamId = 0; // illegal stream identifier ByteBuf frame = pushPromiseFrame(length, flags, streamId); frame.writeInt(promisedStreamId); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testContinuedHttpPushPromiseFrame() throws Exception { int headerBlockLength = 16; int length = 4; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf pushPromiseFrame = pushPromiseFrame(length, flags, streamId); pushPromiseFrame.writeInt(promisedStreamId); ByteBuf continuationFrame = continuationFrame(headerBlockLength, (byte) 0x04, streamId); // END_HEADERS writeRandomData(continuationFrame, headerBlockLength); decoder.decode(releaseLater(Unpooled.wrappedBuffer(pushPromiseFrame, continuationFrame))); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readPushPromiseFrame(streamId, promisedStreamId); inOrder.verify(delegate).readHeaderBlock( continuationFrame.slice(HTTP_FRAME_HEADER_SIZE, headerBlockLength)); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testHttpPushPromiseFrameEmptyContinuation() throws Exception { int headerBlockLength = 16; int length = 4 + headerBlockLength; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf pushPromiseFrame = pushPromiseFrame(length, flags, streamId); pushPromiseFrame.writeInt(promisedStreamId); writeRandomData(pushPromiseFrame, headerBlockLength); ByteBuf continuationFrame = continuationFrame(0, (byte) 0x04, streamId); // END_HEADERS decoder.decode(releaseLater(Unpooled.wrappedBuffer(pushPromiseFrame, continuationFrame))); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readPushPromiseFrame(streamId, promisedStreamId); inOrder.verify(delegate).readHeaderBlock( pushPromiseFrame.slice(HTTP_FRAME_HEADER_SIZE + 4, headerBlockLength)); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testHttpPushPromiseFrameMultipleContinuations() throws Exception { int headerBlockLength = 16; int length = 4 + headerBlockLength; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf pushPromiseFrame = pushPromiseFrame(length, flags, streamId); pushPromiseFrame.writeInt(promisedStreamId); writeRandomData(pushPromiseFrame, headerBlockLength); ByteBuf continuationFrame1 = continuationFrame(0, (byte) 0x00, streamId); ByteBuf continuationFrame2 = continuationFrame(0, (byte) 0x04, streamId); // END_HEADERS decoder.decode(releaseLater( Unpooled.wrappedBuffer(pushPromiseFrame, continuationFrame1, continuationFrame2))); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readPushPromiseFrame(streamId, promisedStreamId); inOrder.verify(delegate).readHeaderBlock( pushPromiseFrame.slice(HTTP_FRAME_HEADER_SIZE + 4, headerBlockLength)); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testHttpPushPromiseFrameContinuationReservedFlags() throws Exception { int headerBlockLength = 16; int length = 4 + headerBlockLength; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf pushPromiseFrame = pushPromiseFrame(length, flags, streamId); pushPromiseFrame.writeInt(promisedStreamId); writeRandomData(pushPromiseFrame, headerBlockLength); ByteBuf continuationFrame = continuationFrame(0, (byte) 0xE7, streamId); // END_HEADERS decoder.decode(releaseLater(Unpooled.wrappedBuffer(pushPromiseFrame, continuationFrame))); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readPushPromiseFrame(streamId, promisedStreamId); inOrder.verify(delegate).readHeaderBlock( pushPromiseFrame.slice(HTTP_FRAME_HEADER_SIZE + 4, headerBlockLength)); inOrder.verify(delegate).readHeaderBlockEnd(); verifyNoMoreInteractions(delegate); } @Test public void testHttpPushPromiseFrameContinuationIllegalStreamId() throws Exception { int headerBlockLength = 16; int length = 4 + headerBlockLength; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf pushPromiseFrame = pushPromiseFrame(length, flags, streamId); pushPromiseFrame.writeInt(promisedStreamId); writeRandomData(pushPromiseFrame, headerBlockLength); // different stream identifier ByteBuf continuationFrame = continuationFrame(0, (byte) 0x04, streamId + 1); // END_HEADERS decoder.decode(releaseLater(Unpooled.wrappedBuffer(pushPromiseFrame, continuationFrame))); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readPushPromiseFrame(streamId, promisedStreamId); inOrder.verify(delegate).readHeaderBlock( pushPromiseFrame.slice(HTTP_FRAME_HEADER_SIZE + 4, headerBlockLength)); inOrder.verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testHttpPushPromiseFrameMissingContinuation() throws Exception { int length = 4; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf pushPromiseFrame = pushPromiseFrame(length, flags, streamId); pushPromiseFrame.writeInt(promisedStreamId); ByteBuf dataFrame = dataFrame(0, (byte) 0, streamId); decoder.decode(releaseLater(Unpooled.wrappedBuffer(pushPromiseFrame, dataFrame))); InOrder inOrder = inOrder(delegate); inOrder.verify(delegate).readPushPromiseFrame(streamId, promisedStreamId); inOrder.verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testHttpPingFrame() throws Exception { int length = 8; byte flags = 0; int streamId = 0; // connection identifier long data = RANDOM.nextLong(); ByteBuf frame = pingFrame(length, flags, streamId); frame.writeLong(data); decoder.decode(frame); verify(delegate).readPingFrame(data, false); verifyNoMoreInteractions(delegate); } @Test public void testHttpPongFrame() throws Exception { int length = 8; byte flags = 0x01; // PONG int streamId = 0; // connection identifier long data = RANDOM.nextLong(); ByteBuf frame = pingFrame(length, flags, streamId); frame.writeLong(data); decoder.decode(frame); verify(delegate).readPingFrame(data, true); verifyNoMoreInteractions(delegate); } @Test public void testHttpPingFrameReservedBits() throws Exception { int length = 8; byte flags = (byte) 0xFE; // should ignore any unknown flags int streamId = 0; // connection identifier long data = RANDOM.nextLong(); ByteBuf frame = pingFrame(length, flags, streamId); frame.writeLong(data); decoder.decode(frame); verify(delegate).readPingFrame(data, false); verifyNoMoreInteractions(delegate); } @Test public void testInvalidHttpPingFrame() throws Exception { int length = 12; // invalid length byte flags = 0; int streamId = 0; // connection identifier long data = RANDOM.nextLong(); ByteBuf frame = pingFrame(length, flags, streamId); frame.writeLong(data); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testIllegalHttpPingFrame() throws Exception { int length = 8; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; // illegal stream identifier long data = RANDOM.nextLong(); ByteBuf frame = pingFrame(length, flags, streamId); frame.writeLong(data); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testHttpGoAwayFrame() throws Exception { int length = 8; byte flags = 0; int streamId = 0; // connection identifier int lastStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int errorCode = RANDOM.nextInt(); ByteBuf frame = goAwayFrame(length, flags, streamId); frame.writeInt(lastStreamId); frame.writeInt(errorCode); decoder.decode(frame); verify(delegate).readGoAwayFrame(lastStreamId, errorCode); verifyNoMoreInteractions(delegate); } @Test public void testHttpGoAwayFrameReservedBits() throws Exception { int length = 8; byte flags = (byte) 0xFF; // should ignore any flags int streamId = 0; // connection identifier int lastStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int errorCode = RANDOM.nextInt(); ByteBuf frame = goAwayFrame(length, flags, streamId); setReservedBits(frame); frame.writeInt(lastStreamId | 0x80000000); // should ignore reserved bit frame.writeInt(errorCode); decoder.decode(frame); verify(delegate).readGoAwayFrame(lastStreamId, errorCode); verifyNoMoreInteractions(delegate); } @Test public void testHttpGoAwayFrameWithDebugData() throws Exception { int debugDataLength = 1024; int length = 8 + debugDataLength; byte flags = 0; int streamId = 0; // connection identifier int lastStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int errorCode = RANDOM.nextInt(); ByteBuf frame = goAwayFrame(length, flags, streamId); frame.writeInt(lastStreamId); frame.writeInt(errorCode); writeRandomData(frame, debugDataLength); decoder.decode(frame); verify(delegate).readGoAwayFrame(lastStreamId, errorCode); verifyNoMoreInteractions(delegate); } @Test public void testInvalidHttpGoAwayFrame() throws Exception { int length = 4; // invalid length byte flags = 0; int streamId = 0; // connection identifier int lastStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = goAwayFrame(length, flags, streamId); frame.writeInt(lastStreamId); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testIllegalHttpGoAwayFrame() throws Exception { int length = 8; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; // illegal stream identifier int lastStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int errorCode = RANDOM.nextInt(); ByteBuf frame = goAwayFrame(length, flags, streamId); frame.writeInt(lastStreamId); frame.writeInt(errorCode); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testHttpWindowUpdateFrame() throws Exception { int length = 4; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF; // connection identifier allowed int windowSizeIncrement = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = windowUpdateFrame(length, flags, streamId); frame.writeInt(windowSizeIncrement); decoder.decode(frame); verify(delegate).readWindowUpdateFrame(streamId, windowSizeIncrement); verifyNoMoreInteractions(delegate); } @Test public void testHttpWindowUpdateFrameReservedBits() throws Exception { int length = 4; byte flags = (byte) 0xFF; // should ignore any unknown flags int streamId = RANDOM.nextInt() & 0x7FFFFFFF; // connection identifier allowed int windowSizeIncrement = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = windowUpdateFrame(length, flags, streamId); setReservedBits(frame); frame.writeInt(windowSizeIncrement | 0x80000000); // should ignore reserved bit decoder.decode(frame); verify(delegate).readWindowUpdateFrame(streamId, windowSizeIncrement); verifyNoMoreInteractions(delegate); } @Test public void testInvalidHttpWindowUpdateFrame() throws Exception { int length = 8; // invalid length byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF; // connection identifier allowed int windowSizeIncrement = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = windowUpdateFrame(length, flags, streamId); frame.writeInt(windowSizeIncrement); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testIllegalHttpWindowUpdateFrame() throws Exception { int length = 4; byte flags = 0; int streamId = RANDOM.nextInt() & 0x7FFFFFFF; // connection identifier allowed int windowSizeIncrement = 0; // illegal delta window size ByteBuf frame = windowUpdateFrame(length, flags, streamId); frame.writeInt(windowSizeIncrement); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testIllegalContinuationFrame() throws Exception { int length = 0; byte flags = 0x01 | 0x04; // END_STREAM | END_HEADERS int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = continuationFrame(length, flags, streamId); decoder.decode(frame); verify(delegate).readFrameError(anyString()); verifyNoMoreInteractions(delegate); } @Test public void testUnknownFrame() throws Exception { int length = 0; int type = 0xFF; // unknown frame byte flags = 0; int streamId = 0; // connection identifier ByteBuf frame = frame(length, type, flags, streamId); decoder.decode(frame); verifyZeroInteractions(delegate); } private ByteBuf frame(int length, int type, byte flags, int streamId) { ByteBuf buffer = releaseLater( Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + length) ); buffer.writeMedium(length); buffer.writeByte(type); buffer.writeByte(flags); buffer.writeInt(streamId); return buffer; } private ByteBuf dataFrame(int length, byte flags, int streamId) { int type = 0x0; // DATA frame return frame(length, type, flags, streamId); } private ByteBuf headersFrame(int length, byte flags, int streamId) { int type = 0x1; // HEADERS frame return frame(length, type, flags, streamId); } private ByteBuf priorityFrame(int length, byte flags, int streamId) { int type = 0x2; // PRIORITY frame return frame(length, type, flags, streamId); } private ByteBuf rstStreamFrame(int length, byte flags, int streamId) { int type = 0x3; // RST_STREAM frame return frame(length, type, flags, streamId); } private ByteBuf settingsFrame(int length, byte flags, int streamId) { int type = 0x4; // SETTINGS frame return frame(length, type, flags, streamId); } private ByteBuf pushPromiseFrame(int length, byte flags, int streamId) { int type = 0x5; // PUSH_PROMISE frame return frame(length, type, flags, streamId); } private ByteBuf pingFrame(int length, byte flags, int streamId) { int type = 0x6; // PUSH frame return frame(length, type, flags, streamId); } private ByteBuf goAwayFrame(int length, byte flags, int streamId) { int type = 0x7; // GOAWAY frame return frame(length, type, flags, streamId); } private ByteBuf windowUpdateFrame(int length, byte flags, int streamId) { int type = 0x8; // WINDOW_UPDATE frame return frame(length, type, flags, streamId); } private ByteBuf continuationFrame(int length, byte flags, int streamId) { int type = 0x9; // CONTINUATION frame return frame(length, type, flags, streamId); } private void setReservedBits(ByteBuf frame) { frame.setInt(5, frame.getInt(5) | 0x80000000); } private void writeRandomData(ByteBuf frame, int length) { for (int i = 0; i < length; i++) { frame.writeByte(RANDOM.nextInt()); } } private void writePriorityFields(ByteBuf frame, boolean exclusive, int dependency, int weight) { int dependencyWithFlag = exclusive ? dependency | 0x80000000 : dependency; frame.writeInt(dependencyWithFlag); frame.writeByte(weight); } } ================================================ FILE: src/test/java/com/twitter/http2/HttpFrameEncoderTest.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.util.Random; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import org.junit.Test; import static io.netty.util.ReferenceCountUtil.releaseLater; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_DEPENDENCY; import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_WEIGHT; import static com.twitter.http2.HttpCodecUtil.HTTP_MAX_LENGTH; public class HttpFrameEncoderTest { private static final Random RANDOM = new Random(); private static final HttpFrameEncoder ENCODER = new HttpFrameEncoder(); @Test public void testHttpDataFrame() throws Exception { int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf data = Unpooled.buffer(1024); for (int i = 0; i < 256; i++) { data.writeInt(RANDOM.nextInt()); } ByteBuf frame = releaseLater( ENCODER.encodeDataFrame(streamId, false, data.duplicate()) ); assertDataFrame(frame, streamId, false, data); } @Test public void testEmptyHttpDataFrame() throws Exception { int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = releaseLater( ENCODER.encodeDataFrame(streamId, false, Unpooled.EMPTY_BUFFER) ); assertDataFrame(frame, streamId, false, Unpooled.EMPTY_BUFFER); } @Test public void testLastHttpDataFrame() throws Exception { int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = releaseLater( ENCODER.encodeDataFrame(streamId, true, Unpooled.EMPTY_BUFFER) ); assertDataFrame(frame, streamId, true, Unpooled.EMPTY_BUFFER); } @Test public void testHttpHeadersFrame() throws Exception { int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; boolean exclusive = RANDOM.nextBoolean(); int dependency = RANDOM.nextInt() & 0x7FFFFFFF; int weight = (RANDOM.nextInt() & 0xFF) + 1; ByteBuf headerBlock = Unpooled.buffer(1024); for (int i = 0; i < 256; i++) { headerBlock.writeInt(RANDOM.nextInt()); } ByteBuf frame = releaseLater( ENCODER.encodeHeadersFrame( streamId, false, exclusive, dependency, weight, headerBlock.duplicate()) ); assertHeadersFrame(frame, streamId, exclusive, dependency, weight, false, headerBlock); } @Test public void testEmptyHttpHeadersFrame() throws Exception { int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; boolean exclusive = RANDOM.nextBoolean(); int dependency = RANDOM.nextInt() & 0x7FFFFFFF; int weight = (RANDOM.nextInt() & 0xFF) + 1; ByteBuf frame = releaseLater( ENCODER.encodeHeadersFrame( streamId, false, exclusive, dependency, weight, Unpooled.EMPTY_BUFFER) ); assertHeadersFrame( frame, streamId, exclusive, dependency, weight, false, Unpooled.EMPTY_BUFFER); } @Test public void testLastHttpHeadersFrame() throws Exception { int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; boolean exclusive = false; int dependency = HTTP_DEFAULT_DEPENDENCY; int weight = HTTP_DEFAULT_WEIGHT; ByteBuf frame = releaseLater( ENCODER.encodeHeadersFrame( streamId, true, exclusive, dependency, weight, Unpooled.EMPTY_BUFFER) ); assertHeadersFrame( frame, streamId, exclusive, dependency, weight, true, Unpooled.EMPTY_BUFFER); } @Test public void testContinuedHttpHeadersFrame() throws Exception { int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; boolean exclusive = RANDOM.nextBoolean(); int dependency = RANDOM.nextInt() & 0x7FFFFFFF; int weight = (RANDOM.nextInt() & 0xFF) + 1; ByteBuf headerBlock = Unpooled.buffer(2 * HTTP_MAX_LENGTH); for (int i = 0; i < 2 * HTTP_MAX_LENGTH; i++) { headerBlock.writeByte(RANDOM.nextInt()); } ByteBuf frame = releaseLater( ENCODER.encodeHeadersFrame( streamId, false, exclusive, dependency, weight, headerBlock.duplicate()) ); assertHeadersFrame( frame, streamId, exclusive, dependency, weight, false, headerBlock); } @Test public void testHttpPriorityFrame() throws Exception { int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; boolean exclusive = RANDOM.nextBoolean(); int dependency = RANDOM.nextInt() & 0x7FFFFFFF; int weight = (RANDOM.nextInt() & 0xFF) + 1; ByteBuf frame = releaseLater( ENCODER.encodePriorityFrame(streamId, exclusive, dependency, weight) ); assertPriorityFrame(frame, streamId, exclusive, dependency, weight); } @Test public void testHttpRstStreamFrame() throws Exception { int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int errorCode = RANDOM.nextInt(); ByteBuf frame = releaseLater( ENCODER.encodeRstStreamFrame(streamId, errorCode) ); assertRstStreamFrame(frame, streamId, errorCode); } @Test public void testHttpSettingsFrame() throws Exception { int id = RANDOM.nextInt() & 0xFFFF; int value = RANDOM.nextInt(); HttpSettingsFrame httpSettingsFrame = new DefaultHttpSettingsFrame(); httpSettingsFrame.setValue(id, value); ByteBuf frame = releaseLater( ENCODER.encodeSettingsFrame(httpSettingsFrame) ); assertSettingsFrame(frame, false, id, value); } @Test public void testHttpSettingsAckFrame() throws Exception { HttpSettingsFrame httpSettingsFrame = new DefaultHttpSettingsFrame(); httpSettingsFrame.setAck(true); ByteBuf frame = releaseLater( ENCODER.encodeSettingsFrame(httpSettingsFrame) ); assertSettingsFrame(frame, true, 0, 0); } @Test public void testHttpPushPromiseFrame() throws Exception { int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf headerBlock = Unpooled.buffer(1024); for (int i = 0; i < 256; i++) { headerBlock.writeInt(RANDOM.nextInt()); } ByteBuf frame = releaseLater( ENCODER.encodePushPromiseFrame(streamId, promisedStreamId, headerBlock.duplicate()) ); assertPushPromiseFrame(frame, streamId, promisedStreamId, headerBlock); } @Test public void testEmptyHttpPushPromiseFrame() throws Exception { int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = releaseLater( ENCODER.encodePushPromiseFrame(streamId, promisedStreamId, Unpooled.EMPTY_BUFFER) ); assertPushPromiseFrame(frame, streamId, promisedStreamId, Unpooled.EMPTY_BUFFER); } @Test public void testContinuedHttpPushPromiseFrame() throws Exception { int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf headerBlock = Unpooled.buffer(2 * HTTP_MAX_LENGTH); for (int i = 0; i < 2 * HTTP_MAX_LENGTH; i++) { headerBlock.writeByte(RANDOM.nextInt()); } ByteBuf frame = releaseLater( ENCODER.encodePushPromiseFrame(streamId, promisedStreamId, headerBlock.duplicate()) ); assertPushPromiseFrame(frame, streamId, promisedStreamId, headerBlock); } @Test public void testHttpPingFrame() throws Exception { long data = RANDOM.nextLong(); ByteBuf frame = releaseLater( ENCODER.encodePingFrame(data, false) ); assertPingFrame(frame, false, data); } @Test public void testHttpPongFrame() throws Exception { long data = RANDOM.nextLong(); ByteBuf frame = releaseLater( ENCODER.encodePingFrame(data, true) ); assertPingFrame(frame, true, data); } @Test public void testHttpGoAwayFrame() throws Exception { int lastStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; int errorCode = RANDOM.nextInt(); ByteBuf frame = releaseLater( ENCODER.encodeGoAwayFrame(lastStreamId, errorCode) ); assertGoAwayFrame(frame, lastStreamId, errorCode); } @Test public void testHttpWindowUpdateFrame() throws Exception { int streamId = RANDOM.nextInt() & 0x7FFFFFFF; // connection identifier allowed int windowSizeIncrement = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; ByteBuf frame = releaseLater( ENCODER.encodeWindowUpdateFrame(streamId, windowSizeIncrement) ); assertWindowUpdateFrame(frame, streamId, windowSizeIncrement); } private static void assertDataFrame(ByteBuf frame, int streamId, boolean last, ByteBuf data) { byte type = 0x00; byte flags = 0x00; if (last) { flags |= 0x01; } int length = assertFrameHeader(frame, type, flags, streamId); assertEquals(data.readableBytes(), length); for (int i = 0; i < length; i++) { assertEquals(data.getByte(i), frame.readByte()); } assertFalse(frame.isReadable()); } private static void assertHeadersFrame( ByteBuf frame, int streamId, boolean exclusive, int dependency, int weight, boolean last, ByteBuf headerBlock ) { boolean hasPriority = exclusive || dependency != HTTP_DEFAULT_DEPENDENCY || weight != HTTP_DEFAULT_WEIGHT; int maxLength = hasPriority ? HTTP_MAX_LENGTH - 5 : HTTP_MAX_LENGTH; byte type = 0x01; byte flags = 0x00; if (last) { flags |= 0x01; } if (headerBlock.readableBytes() <= maxLength) { flags |= 0x04; } if (hasPriority) { flags |= 0x20; } int length = assertFrameHeader(frame, type, flags, streamId); if (hasPriority) { assertTrue(length >= 5); if (exclusive) { assertEquals(dependency | 0x80000000, frame.readInt()); } else { assertEquals(dependency, frame.readInt()); } assertEquals(weight - 1, frame.readUnsignedByte()); length -= 5; } assertTrue(length <= headerBlock.readableBytes()); for (int i = 0; i < length; i++) { assertEquals(headerBlock.readByte(), frame.readByte()); } while (headerBlock.isReadable()) { type = 0x09; flags = 0x00; if (headerBlock.readableBytes() <= HTTP_MAX_LENGTH) { flags |= 0x04; } length = assertFrameHeader(frame, type, flags, streamId); assertTrue(length <= headerBlock.readableBytes()); for (int i = 0; i < length; i++) { assertEquals(headerBlock.readByte(), frame.readByte()); } } assertFalse(frame.isReadable()); } private static void assertPriorityFrame( ByteBuf frame, int streamId, boolean exclusive, int dependency, int weight ) { byte type = 0x02; byte flags = 0x00; assertEquals(5, assertFrameHeader(frame, type, flags, streamId)); if (exclusive) { assertEquals(dependency | 0x80000000, frame.readInt()); } else { assertEquals(dependency, frame.readInt()); } assertEquals(weight - 1, frame.readUnsignedByte()); assertFalse(frame.isReadable()); } private static void assertRstStreamFrame(ByteBuf frame, int streamId, int errorCode) { byte type = 0x03; byte flags = 0x00; assertEquals(4, assertFrameHeader(frame, type, flags, streamId)); assertEquals(errorCode, frame.readInt()); assertFalse(frame.isReadable()); } private static void assertSettingsFrame(ByteBuf frame, boolean ack, int id, int value) { byte type = 0x04; byte flags = ack ? (byte) 0x01 : 0x00; int length = assertFrameHeader(frame, type, flags, 0); if (ack) { assertEquals(0, length); } else { assertEquals(6, length); assertEquals(id, frame.readUnsignedShort()); assertEquals(value, frame.readInt()); } assertFalse(frame.isReadable()); } private static void assertPushPromiseFrame( ByteBuf frame, int streamId, int promisedStreamId, ByteBuf headerBlock) { int maxLength = HTTP_MAX_LENGTH - 4; byte type = 0x05; byte flags = 0x00; if (headerBlock.readableBytes() <= maxLength) { flags |= 0x04; } int length = assertFrameHeader(frame, type, flags, streamId); assertTrue(length >= 4); assertEquals(promisedStreamId, frame.readInt()); length -= 4; assertTrue(length <= headerBlock.readableBytes()); for (int i = 0; i < length; i++) { assertEquals(headerBlock.readByte(), frame.readByte()); } while (headerBlock.isReadable()) { type = 0x09; flags = 0x00; if (headerBlock.readableBytes() <= HTTP_MAX_LENGTH) { flags |= 0x04; } length = assertFrameHeader(frame, type, flags, streamId); assertTrue(length <= headerBlock.readableBytes()); for (int i = 0; i < length; i++) { assertEquals(headerBlock.readByte(), frame.readByte()); } } assertFalse(frame.isReadable()); } private static void assertPingFrame(ByteBuf frame, boolean pong, long data) { byte type = 0x06; byte flags = 0x00; if (pong) { flags |= 0x01; } assertEquals(8, assertFrameHeader(frame, type, flags, 0)); assertEquals(data, frame.readLong()); assertFalse(frame.isReadable()); } private static void assertGoAwayFrame(ByteBuf frame, int lastStreamId, int errorCode) { byte type = 0x07; byte flags = 0x00; assertEquals(8, assertFrameHeader(frame, type, flags, 0)); assertEquals(lastStreamId, frame.readInt()); assertEquals(errorCode, frame.readInt()); assertFalse(frame.isReadable()); } private static void assertWindowUpdateFrame( ByteBuf frame, int streamId, int windowSizeIncrement ) { byte type = 0x08; byte flags = 0x00; assertEquals(4, assertFrameHeader(frame, type, flags, streamId)); assertEquals(windowSizeIncrement, frame.readInt()); assertFalse(frame.isReadable()); } // Verifies the type, flag, and streamId in the frame header and returns the length. private static int assertFrameHeader(ByteBuf frame, byte type, byte flags, int streamId) { int length = frame.readUnsignedMedium(); assertEquals(type, frame.readByte()); assertEquals(flags, frame.readByte()); assertEquals(streamId, frame.readInt()); return length; } } ================================================ FILE: src/test/java/com/twitter/http2/HttpHeaderCompressionTest.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import io.netty.bootstrap.Bootstrap; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.Unpooled; import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelInboundHandlerAdapter; import io.netty.channel.ChannelInitializer; import io.netty.channel.local.LocalAddress; import io.netty.channel.local.LocalChannel; import io.netty.channel.local.LocalEventLoopGroup; import io.netty.channel.local.LocalServerChannel; import io.netty.handler.codec.http.HttpHeaders; import org.junit.Test; import static org.junit.Assert.assertTrue; public class HttpHeaderCompressionTest { @Test public void testHttpHeadersFrame() throws Throwable { HttpHeadersFrame httpHeadersFrame = new DefaultHttpHeadersFrame(1); httpHeadersFrame.headers().add("name", "value"); testHeaderEcho(httpHeadersFrame); } private void testHeaderEcho(HttpHeaderBlockFrame frame) throws Throwable { final EchoHandler sh = new EchoHandler(); final TestHandler ch = new TestHandler(frame); ServerBootstrap sb = new ServerBootstrap() .group(new LocalEventLoopGroup()) .channel(LocalServerChannel.class) .childHandler(new ChannelInitializer() { @Override public void initChannel(LocalChannel channel) throws Exception { channel.pipeline().addLast(new HttpConnectionHandler(true), sh); } }); Bootstrap cb = new Bootstrap() .group(new LocalEventLoopGroup()) .channel(LocalChannel.class) .handler(new ChannelInitializer() { @Override public void initChannel(LocalChannel channel) throws Exception { channel.pipeline().addLast(new HttpConnectionHandler(false), ch); } }); LocalAddress localAddress = new LocalAddress("HttpHeaderCompressionTest"); Channel sc = sb.bind(localAddress).sync().channel(); ChannelFuture ccf = cb.connect(localAddress); assertTrue(ccf.awaitUninterruptibly().isSuccess()); while (!ch.success) { if (sh.exception.get() != null) { break; } if (ch.exception.get() != null) { break; } try { Thread.sleep(1); } catch (InterruptedException e) { // Ignore. } } sc.close().awaitUninterruptibly(); cb.group().shutdownGracefully(); sb.group().shutdownGracefully(); if (sh.exception.get() != null && !(sh.exception.get() instanceof IOException)) { throw sh.exception.get(); } if (ch.exception.get() != null && !(ch.exception.get() instanceof IOException)) { throw ch.exception.get(); } if (sh.exception.get() != null) { throw sh.exception.get(); } if (ch.exception.get() != null) { throw ch.exception.get(); } } private static class EchoHandler extends ChannelInboundHandlerAdapter { public final AtomicReference exception = new AtomicReference(); @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ctx.writeAndFlush(msg); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (exception.compareAndSet(null, cause)) { ctx.close(); } } } private static class TestHandler extends ChannelInboundHandlerAdapter { private static final byte[] CONNECTION_HEADER = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(StandardCharsets.US_ASCII); public final AtomicReference exception = new AtomicReference(); public final HttpHeaderBlockFrame frame; public volatile boolean success; public TestHandler(HttpHeaderBlockFrame frame) { this.frame = frame; } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.write(Unpooled.wrappedBuffer(CONNECTION_HEADER)); ctx.writeAndFlush(frame); } @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { assertTrue(msg instanceof HttpHeaderBlockFrame); HttpHeaders actual = ((HttpHeaderBlockFrame) msg).headers(); HttpHeaders expected = frame.headers(); for (String name : expected.names()) { List expectedValues = new ArrayList(expected.getAll(name)); List actualValues = new ArrayList(actual.getAll(name)); assertTrue(actualValues.containsAll(expectedValues)); actualValues.removeAll(expectedValues); assertTrue(actualValues.isEmpty()); actual.remove(name); } assertTrue(actual.isEmpty()); success = true; } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { if (exception.compareAndSet(null, cause)) { ctx.close(); } } } } ================================================ FILE: src/test/java/com/twitter/http2/HttpRequestProxyTest.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.HttpMethod; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpVersion; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; public class HttpRequestProxyTest { private HttpRequest httpRequest = new DefaultHttpRequest( HttpVersion.HTTP_1_0, HttpMethod.GET, "/"); private HttpRequestProxy httpRequestProxy = new HttpRequestProxy(httpRequest); @Test public void testHttpRequest() { assertSame(httpRequest, httpRequestProxy.httpRequest()); } @Test public void testGetMethod() { assertEquals(httpRequest.getMethod(), httpRequestProxy.getMethod()); } @Test public void testSetMethod() { assertSame(httpRequestProxy, httpRequestProxy.setMethod(HttpMethod.POST)); assertEquals(httpRequest.getMethod(), httpRequestProxy.getMethod()); } @Test public void testGetUri() { assertEquals(httpRequest.getUri(), httpRequestProxy.getUri()); } @Test public void testSetUri() { assertSame(httpRequestProxy, httpRequestProxy.setUri("/path")); assertEquals(httpRequest.getUri(), httpRequestProxy.getUri()); } @Test public void testToString() { assertEquals(httpRequest.toString(), httpRequestProxy.toString()); } @Test public void testGetProtocolVersion() { assertEquals(httpRequest.getProtocolVersion(), httpRequestProxy.getProtocolVersion()); } @Test public void testSetProtocolVersion() { assertSame(httpRequestProxy, httpRequestProxy.setProtocolVersion(HttpVersion.HTTP_1_1)); assertEquals(httpRequest.getProtocolVersion(), httpRequestProxy.getProtocolVersion()); } @Test public void testHeaders() { assertSame(httpRequest.headers(), httpRequestProxy.headers()); } } ================================================ FILE: src/test/java/com/twitter/http2/HttpResponseProxyTest.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.handler.codec.http.HttpVersion; import org.junit.Test; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; public class HttpResponseProxyTest { private HttpResponse httpResponse = new DefaultHttpResponse( HttpVersion.HTTP_1_0, HttpResponseStatus.OK); private HttpResponseProxy httpResponseProxy = new HttpResponseProxy(httpResponse); @Test public void testSetStatus() { assertSame(httpResponseProxy, httpResponseProxy.setStatus(HttpResponseStatus.NOT_FOUND)); assertEquals(httpResponse.getStatus(), httpResponseProxy.getStatus()); } @Test public void testGetStatus() { assertEquals(httpResponse.getStatus(), httpResponseProxy.getStatus()); } @Test public void testToString() { assertEquals(httpResponse.toString(), httpResponseProxy.toString()); } @Test public void testGetProtocolVersion() { assertEquals(httpResponse.getProtocolVersion(), httpResponseProxy.getProtocolVersion()); } @Test public void testSetProtocolVersion() { assertSame(httpResponseProxy, httpResponseProxy.setProtocolVersion(HttpVersion.HTTP_1_1)); assertEquals(httpResponse.getProtocolVersion(), httpResponseProxy.getProtocolVersion()); } @Test public void testHeaders() { assertSame(httpResponse.headers(), httpResponseProxy.headers()); } } ================================================ FILE: src/test/java/com/twitter/http2/PipeTest.java ================================================ /* * Copyright 2015 Twitter, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.twitter.http2; import org.junit.Before; import org.junit.Test; import io.netty.util.concurrent.Future; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; public class PipeTest { private static final Object MESSAGE = new Object(); private static final Object MESSAGE1 = new Object(); private static final Object MESSAGE2 = new Object(); // // Test Plan // // o Choose from an alphabet, { S, C, R }, where the letters stand for Send, Close, and Receive. // o The tests are named using the alphabet. // For example, testSR tests a Send then Receive. // At each point in the test, all the state in the current // and previous futures and values are tested as well. // // Some assumptions: // o If a future is not done, then it will not be succeeded, // cancelled, or failed, as per the future spec. // o If a future is done, then it will be only one of the three other future states. // private Pipe pipe; @Before public void createPipe() { pipe = new Pipe(); } @Test public void testS() { Future sendFuture = pipe.send(MESSAGE); assertFalse(sendFuture.isDone()); } @Test public void testR() { Future recvFuture = pipe.receive(); assertFalse(recvFuture.isDone()); } @Test public void testSR() { Future sendFuture = pipe.send(MESSAGE); assertFalse(sendFuture.isDone()); Future recvFuture = pipe.receive(); assertSame(MESSAGE, recvFuture.getNow()); assertTrue(sendFuture.isSuccess()); } @Test public void testRS() { Future recvFuture = pipe.receive(); assertFalse(recvFuture.isDone()); Future sendFuture = pipe.send(MESSAGE); assertTrue(sendFuture.isSuccess()); assertSame(MESSAGE, recvFuture.getNow()); } @Test public void testSS() { Future sendFuture1 = pipe.send(MESSAGE1); assertFalse(sendFuture1.isDone()); Future sendFuture2 = pipe.send(MESSAGE2); assertFalse(sendFuture2.isDone()); } @Test public void testRR() { Future recvFuture1 = pipe.receive(); assertFalse(recvFuture1.isDone()); Future recvFuture2 = pipe.receive(); assertFalse(recvFuture2.isDone()); } @Test public void testSRR() { Future sendFuture1 = pipe.send(MESSAGE); assertFalse(sendFuture1.isDone()); Future recvFuture1 = pipe.receive(); assertSame(MESSAGE, recvFuture1.getNow()); assertTrue(sendFuture1.isSuccess()); Future recvFuture2 = pipe.receive(); assertFalse(recvFuture2.isDone()); assertTrue(recvFuture1.isSuccess()); assertTrue(sendFuture1.isSuccess()); } @Test public void testSSR() { Future sendFuture1 = pipe.send(MESSAGE1); assertFalse(sendFuture1.isDone()); Future sendFuture2 = pipe.send(MESSAGE2); assertFalse(sendFuture2.isDone()); assertFalse(sendFuture1.isDone()); Future recvFuture = pipe.receive(); assertSame(MESSAGE1, recvFuture.getNow()); assertFalse(sendFuture2.isDone()); assertTrue(sendFuture1.isSuccess()); } @Test public void testSRS() { Future sendFuture1 = pipe.send(MESSAGE1); assertFalse(sendFuture1.isDone()); Future recvFuture = pipe.receive(); assertSame(MESSAGE1, recvFuture.getNow()); assertTrue(sendFuture1.isSuccess()); Future sendFuture2 = pipe.send(MESSAGE2); assertFalse(sendFuture2.isDone()); assertTrue(recvFuture.isSuccess()); assertTrue(sendFuture1.isSuccess()); } @Test public void testRSR() { Future recvFuture1 = pipe.receive(); assertFalse(recvFuture1.isDone()); Future sendFuture = pipe.send(MESSAGE); assertTrue(sendFuture.isSuccess()); assertSame(MESSAGE, recvFuture1.getNow()); Future recvFuture2 = pipe.receive(); assertFalse(recvFuture2.isDone()); assertTrue(sendFuture.isSuccess()); assertTrue(recvFuture1.isSuccess()); } @Test public void testRSS() { Future recvFuture = pipe.receive(); assertFalse(recvFuture.isDone()); Future sendFuture1 = pipe.send(MESSAGE1); assertTrue(sendFuture1.isSuccess()); assertSame(MESSAGE1, recvFuture.getNow()); Future sendFuture2 = pipe.send(MESSAGE2); assertFalse(sendFuture2.isDone()); assertTrue(sendFuture1.isSuccess()); assertTrue(recvFuture.isSuccess()); } @Test public void testRRS() { Future recvFuture1 = pipe.receive(); assertFalse(recvFuture1.isDone()); Future recvFuture2 = pipe.receive(); assertFalse(recvFuture2.isDone()); assertFalse(recvFuture1.isDone()); Future sendFuture = pipe.send(MESSAGE); assertTrue(sendFuture.isSuccess()); assertFalse(recvFuture2.isDone()); assertSame(MESSAGE, recvFuture1.getNow()); } @Test public void testSSRR() { Future sendFuture1 = pipe.send(MESSAGE1); assertFalse(sendFuture1.isDone()); Future sendFuture2 = pipe.send(MESSAGE2); assertFalse(sendFuture2.isDone()); assertFalse(sendFuture1.isDone()); Future recvFuture1 = pipe.receive(); assertSame(MESSAGE1, recvFuture1.getNow()); assertFalse(sendFuture2.isDone()); assertTrue(sendFuture1.isSuccess()); Future recvFuture2 = pipe.receive(); assertSame(MESSAGE2, recvFuture2.getNow()); assertTrue(recvFuture1.isSuccess()); assertTrue(sendFuture2.isSuccess()); assertTrue(sendFuture1.isSuccess()); } @Test public void testRRSS() { Future recvFuture1 = pipe.receive(); assertFalse(recvFuture1.isDone()); Future recvFuture2 = pipe.receive(); assertFalse(recvFuture2.isDone()); assertFalse(recvFuture1.isDone()); Future sendFuture1 = pipe.send(MESSAGE1); assertTrue(sendFuture1.isSuccess()); assertFalse(recvFuture2.isDone()); assertSame(MESSAGE1, recvFuture1.getNow()); Future sendFuture2 = pipe.send(MESSAGE2); assertTrue(sendFuture2.isSuccess()); assertTrue(sendFuture1.isSuccess()); assertSame(MESSAGE2, recvFuture2.getNow()); assertSame(MESSAGE1, recvFuture1.getNow()); } @Test public void testRSSR() { Future recvFuture1 = pipe.receive(); assertFalse(recvFuture1.isDone()); Future sendFuture1 = pipe.send(MESSAGE1); assertTrue(sendFuture1.isSuccess()); assertSame(MESSAGE1, recvFuture1.getNow()); Future sendFuture2 = pipe.send(MESSAGE2); assertFalse(sendFuture2.isDone()); assertTrue(sendFuture1.isSuccess()); assertTrue(recvFuture1.isSuccess()); Future recvFuture2 = pipe.receive(); assertSame(MESSAGE2, recvFuture2.getNow()); assertTrue(sendFuture2.isSuccess()); assertTrue(sendFuture1.isSuccess()); assertTrue(recvFuture1.isSuccess()); } @Test public void testSRRS() { Future sendFuture1 = pipe.send(MESSAGE1); assertFalse(sendFuture1.isDone()); Future recvFuture1 = pipe.receive(); assertSame(MESSAGE1, recvFuture1.getNow()); assertTrue(sendFuture1.isSuccess()); Future recvFuture2 = pipe.receive(); assertFalse(recvFuture2.isDone()); assertTrue(recvFuture1.isSuccess()); assertTrue(sendFuture1.isSuccess()); Future sendFuture2 = pipe.send(MESSAGE2); assertTrue(sendFuture2.isSuccess()); assertSame(MESSAGE2, recvFuture2.getNow()); assertTrue(recvFuture1.isSuccess()); assertTrue(sendFuture1.isSuccess()); } @Test public void testSSRRR() { Future sendFuture1 = pipe.send(MESSAGE1); assertFalse(sendFuture1.isDone()); Future sendFuture2 = pipe.send(MESSAGE2); assertFalse(sendFuture2.isDone()); assertFalse(sendFuture1.isDone()); Future recvFuture1 = pipe.receive(); assertTrue(recvFuture1.isSuccess()); assertSame(MESSAGE1, recvFuture1.getNow()); assertFalse(sendFuture2.isDone()); assertTrue(sendFuture1.isSuccess()); Future recvFuture2 = pipe.receive(); assertTrue(recvFuture2.isSuccess()); assertSame(MESSAGE2, recvFuture2.getNow()); assertTrue(sendFuture2.isSuccess()); assertTrue(recvFuture1.isSuccess()); assertTrue(sendFuture2.isSuccess()); assertTrue(sendFuture1.isSuccess()); Future recvFuture3 = pipe.receive(); assertFalse(recvFuture3.isDone()); } @Test public void testCS() { pipe.close(); Future sendFuture = pipe.send(MESSAGE); assertNotNull(sendFuture.cause()); } @Test public void testSC() { Future sendFuture = pipe.send(MESSAGE); assertFalse(sendFuture.isDone()); pipe.close(); assertFalse(sendFuture.isDone()); } @Test public void testCR() { pipe.close(); Future recvFuture = pipe.receive(); assertNotNull(recvFuture.cause()); } @Test public void testRC() { Future recvFuture = pipe.receive(); assertFalse(recvFuture.isDone()); pipe.close(); assertNotNull(recvFuture.isDone()); } @Test public void testCSR() { pipe.close(); Future sendFuture = pipe.send(MESSAGE); assertNotNull(sendFuture.cause()); Future recvFuture = pipe.receive(); assertNotNull(recvFuture.cause()); assertNotNull(sendFuture.cause()); } @Test public void testSCR() { Future sendFuture = pipe.send(MESSAGE); assertFalse(sendFuture.isDone()); pipe.close(); assertFalse(sendFuture.isDone()); Future recvFuture = pipe.receive(); assertSame(MESSAGE, recvFuture.getNow()); assertTrue(sendFuture.isSuccess()); } @Test public void testSRCS() { Future sendFuture1 = pipe.send(MESSAGE1); assertFalse(sendFuture1.isDone()); Future recvFuture = pipe.receive(); assertSame(MESSAGE1, recvFuture.getNow()); assertTrue(sendFuture1.isSuccess()); pipe.close(); Future sendFuture2 = pipe.send(MESSAGE2); assertNotNull(sendFuture2.cause()); } @Test public void testSRCR() { Future sendFuture = pipe.send(MESSAGE); assertFalse(sendFuture.isDone()); Future recvFuture1 = pipe.receive(); assertSame(MESSAGE, recvFuture1.getNow()); assertTrue(sendFuture.isSuccess()); pipe.close(); Future recvFuture2 = pipe.receive(); assertNotNull(recvFuture2.cause()); assertTrue(sendFuture.isSuccess()); } @Test public void testCRS() { pipe.close(); Future recvFuture = pipe.receive(); assertNotNull(recvFuture.cause()); Future sendFuture = pipe.send(MESSAGE); assertNotNull(sendFuture.cause()); assertNotNull(recvFuture.cause()); } @Test public void testRCS() { Future recvFuture = pipe.receive(); assertFalse(recvFuture.isDone()); pipe.close(); assertNotNull(recvFuture.cause()); Future sendFuture = pipe.send(MESSAGE); assertNotNull(sendFuture.cause()); assertNotNull(recvFuture.cause()); } @Test public void testRSCR() { Future recvFuture1 = pipe.receive(); assertFalse(recvFuture1.isDone()); Future sendFuture1 = pipe.send(MESSAGE); assertTrue(sendFuture1.isSuccess()); assertSame(MESSAGE, recvFuture1.getNow()); pipe.close(); Future recvFuture2 = pipe.receive(); assertNotNull(recvFuture2.cause()); assertTrue(sendFuture1.isSuccess()); assertTrue(recvFuture1.isSuccess()); } @Test public void testRSCS() { Future recvFuture = pipe.receive(); assertFalse(recvFuture.isDone()); Future sendFuture1 = pipe.send(MESSAGE1); assertTrue(sendFuture1.isSuccess()); assertSame(MESSAGE1, recvFuture.getNow()); pipe.close(); Future sendFuture2 = pipe.send(MESSAGE2); assertNotNull(sendFuture2.cause()); assertTrue(sendFuture1.isSuccess()); assertTrue(recvFuture.isSuccess()); } }