├── .gitignore ├── .travis.yml ├── README.md ├── src ├── main │ └── java │ │ └── com │ │ └── twitter │ │ └── http2 │ │ ├── HttpFrame.java │ │ ├── HttpStreamFrame.java │ │ ├── HttpPingFrame.java │ │ ├── HttpGoAwayFrame.java │ │ ├── HttpRstStreamFrame.java │ │ ├── HttpPushPromiseFrame.java │ │ ├── HttpProtocolException.java │ │ ├── HttpWindowUpdateFrame.java │ │ ├── HttpHeaderBlockFrame.java │ │ ├── DefaultHttpStreamFrame.java │ │ ├── HttpDataFrame.java │ │ ├── HttpResponseProxy.java │ │ ├── StreamedHttpRequest.java │ │ ├── StreamedHttpResponse.java │ │ ├── HttpPriorityFrame.java │ │ ├── DefaultHttpPingFrame.java │ │ ├── HttpMessageProxy.java │ │ ├── HttpSettingsFrame.java │ │ ├── HttpHeadersFrame.java │ │ ├── DefaultHttp2Headers.java │ │ ├── StreamedHttpMessage.java │ │ ├── HttpRequestProxy.java │ │ ├── DefaultHttpHeaderBlockFrame.java │ │ ├── DefaultHttpRstStreamFrame.java │ │ ├── DefaultHttpWindowUpdateFrame.java │ │ ├── DefaultHttpGoAwayFrame.java │ │ ├── DefaultHttpPushPromiseFrame.java │ │ ├── DefaultHttpSettingsFrame.java │ │ ├── HttpFrameDecoderDelegate.java │ │ ├── DefaultHttpPriorityFrame.java │ │ ├── DefaultHttpHeadersFrame.java │ │ ├── HttpCodecUtil.java │ │ ├── DefaultHttpDataFrame.java │ │ ├── Pipe.java │ │ ├── HttpHeaderBlockEncoder.java │ │ ├── HttpHeaderBlockDecoder.java │ │ ├── HttpErrorCode.java │ │ ├── HttpFrameEncoder.java │ │ ├── HttpStreamDecoder.java │ │ ├── HttpStreamEncoder.java │ │ └── HttpConnection.java └── test │ └── java │ └── com │ └── twitter │ └── http2 │ ├── HttpResponseProxyTest.java │ ├── HttpRequestProxyTest.java │ ├── HttpHeaderCompressionTest.java │ ├── PipeTest.java │ └── HttpFrameEncoderTest.java ├── pom.xml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .idea 3 | *.iml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | jdk: 4 | - openjdk7 5 | after_success: 6 | - mvn clean cobertura:cobertura coveralls:cobertura 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # netty-http2 [![Build Status](https://travis-ci.org/twitter/netty-http2.svg)](https://travis-ci.org/twitter/netty-http2) 2 | 3 | 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 4 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | /** 19 | * An HTTP/2 Frame 20 | */ 21 | public interface HttpFrame { 22 | // Tag interface 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpStreamFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | /** 19 | * An HTTP/2 Frame that is associated with an individual stream 20 | */ 21 | public interface HttpStreamFrame extends HttpFrame { 22 | 23 | /** 24 | * Returns the stream identifier of this frame. 25 | */ 26 | int getStreamId(); 27 | 28 | /** 29 | * Sets the stream identifier of this frame. The stream identifier must be positive. 30 | */ 31 | HttpStreamFrame setStreamId(int streamId); 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpPingFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | /** 19 | * An HTTP/2 PING Frame 20 | */ 21 | public interface HttpPingFrame extends HttpFrame { 22 | 23 | /** 24 | * Returns the data payload of this frame. 25 | */ 26 | long getData(); 27 | 28 | /** 29 | * Sets the data payload of this frame. 30 | */ 31 | HttpPingFrame setData(long data); 32 | 33 | /** 34 | * Returns {@code true} if this frame is in response to a PING. 35 | */ 36 | boolean isPong(); 37 | 38 | /** 39 | * Sets if this frame is in response to a PING. 40 | */ 41 | HttpPingFrame setPong(boolean pong); 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpGoAwayFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | /** 19 | * An HTTP/2 GOAWAY Frame 20 | */ 21 | public interface HttpGoAwayFrame extends HttpFrame { 22 | 23 | /** 24 | * Returns the Last-Stream-ID of this frame. 25 | */ 26 | int getLastStreamId(); 27 | 28 | /** 29 | * Sets the Last-Stream-ID of this frame. The Last-Stream-ID cannot be negative. 30 | */ 31 | HttpGoAwayFrame setLastStreamId(int lastStreamId); 32 | 33 | /** 34 | * Returns the error code of this frame. 35 | */ 36 | HttpErrorCode getErrorCode(); 37 | 38 | /** 39 | * Sets the error code of this frame. 40 | */ 41 | HttpGoAwayFrame setErrorCode(HttpErrorCode errorCode); 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpRstStreamFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | /** 19 | * An HTTP/2 RST_STREAM Frame 20 | */ 21 | public interface HttpRstStreamFrame extends HttpFrame { 22 | 23 | /** 24 | * Returns the stream identifier of this frame. 25 | */ 26 | int getStreamId(); 27 | 28 | /** 29 | * Sets the stream identifier of this frame. The stream identifier must be positive. 30 | */ 31 | HttpRstStreamFrame setStreamId(int streamId); 32 | 33 | /** 34 | * Returns the error code of this frame. 35 | */ 36 | HttpErrorCode getErrorCode(); 37 | 38 | /** 39 | * Sets the error code of this frame. 40 | */ 41 | HttpRstStreamFrame setErrorCode(HttpErrorCode errorCode); 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpPushPromiseFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | /** 19 | * An HTTP/2 PUSH_PROMISE Frame 20 | */ 21 | public interface HttpPushPromiseFrame extends HttpHeaderBlockFrame { 22 | 23 | /** 24 | * Returns the Promised-Stream-ID of this frame. 25 | */ 26 | int getPromisedStreamId(); 27 | 28 | /** 29 | * Sets the Promised-Stream-ID of this frame. The Promised-Stream-ID must be positive. 30 | */ 31 | HttpPushPromiseFrame setPromisedStreamId(int promisedStreamId); 32 | 33 | @Override 34 | HttpPushPromiseFrame setStreamId(int streamId); 35 | 36 | @Override 37 | HttpPushPromiseFrame setInvalid(); 38 | 39 | @Override 40 | HttpPushPromiseFrame setTruncated(); 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpProtocolException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | public class HttpProtocolException extends Exception { 19 | 20 | /** 21 | * Creates a new instance. 22 | */ 23 | public HttpProtocolException() { 24 | super(); 25 | } 26 | 27 | /** 28 | * Creates a new instance. 29 | */ 30 | public HttpProtocolException(String message, Throwable cause) { 31 | super(message, cause); 32 | } 33 | 34 | /** 35 | * Creates a new instance. 36 | */ 37 | public HttpProtocolException(String message) { 38 | super(message); 39 | } 40 | 41 | /** 42 | * Creates a new instance. 43 | */ 44 | public HttpProtocolException(Throwable cause) { 45 | super(cause); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpWindowUpdateFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | /** 19 | * An HTTP/2 WINDOW_UPDATE Frame 20 | */ 21 | public interface HttpWindowUpdateFrame extends HttpFrame { 22 | 23 | /** 24 | * Returns the stream identifier of this frame. 25 | */ 26 | int getStreamId(); 27 | 28 | /** 29 | * Sets the stream identifier of this frame. The stream identifier cannot be negative. 30 | */ 31 | HttpWindowUpdateFrame setStreamId(int streamId); 32 | 33 | /** 34 | * Returns the Window-Size-Increment of this frame. 35 | */ 36 | int getWindowSizeIncrement(); 37 | 38 | /** 39 | * Sets the Window-Size-Increment of this frame. 40 | * The Window-Size-Increment must be positive. 41 | */ 42 | HttpWindowUpdateFrame setWindowSizeIncrement(int deltaWindowSize); 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpHeaderBlockFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.handler.codec.http.HttpHeaders; 19 | 20 | public interface HttpHeaderBlockFrame extends HttpStreamFrame { 21 | 22 | /** 23 | * Returns {@code true} if this header block is invalid. 24 | * A RST_STREAM frame with code PROTOCOL_ERROR should be sent. 25 | */ 26 | boolean isInvalid(); 27 | 28 | /** 29 | * Marks this header block as invalid. 30 | */ 31 | HttpHeaderBlockFrame setInvalid(); 32 | 33 | /** 34 | * Returns {@code true} if this header block has been truncated due to length restrictions. 35 | */ 36 | boolean isTruncated(); 37 | 38 | /** 39 | * Mark this header block as truncated. 40 | */ 41 | HttpHeaderBlockFrame setTruncated(); 42 | 43 | /** 44 | * Returns the {@link HttpHeaders} of this frame. 45 | */ 46 | HttpHeaders headers(); 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/DefaultHttpStreamFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | /** 19 | * The default {@link HttpStreamFrame} implementation. 20 | */ 21 | public abstract class DefaultHttpStreamFrame implements HttpStreamFrame { 22 | 23 | private int streamId; 24 | 25 | /** 26 | * Creates a new instance. 27 | * 28 | * @param streamId the stream identifier of this frame 29 | */ 30 | protected DefaultHttpStreamFrame(int streamId) { 31 | setStreamId(streamId); 32 | } 33 | 34 | @Override 35 | public int getStreamId() { 36 | return streamId; 37 | } 38 | 39 | @Override 40 | public HttpStreamFrame setStreamId(int streamId) { 41 | if (streamId <= 0) { 42 | throw new IllegalArgumentException( 43 | "Stream identifier must be positive: " + streamId); 44 | } 45 | this.streamId = streamId; 46 | return this; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpDataFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.buffer.ByteBuf; 19 | import io.netty.buffer.ByteBufHolder; 20 | 21 | /** 22 | * An HTTP/2 DATA Frame 23 | */ 24 | public interface HttpDataFrame extends ByteBufHolder, HttpStreamFrame { 25 | 26 | /** 27 | * Returns {@code true} if this frame is the last frame to be transmitted on the stream. 28 | */ 29 | boolean isLast(); 30 | 31 | /** 32 | * Sets if this frame is the last frame to be transmitted on the stream. 33 | */ 34 | HttpDataFrame setLast(boolean last); 35 | 36 | @Override 37 | HttpDataFrame setStreamId(int streamId); 38 | 39 | /** 40 | * Returns the data payload of this frame. If there is no data payload 41 | * {@link io.netty.buffer.Unpooled#EMPTY_BUFFER} is returned. 42 | *

43 | * The data payload cannot exceed 16384 bytes. 44 | */ 45 | @Override 46 | ByteBuf content(); 47 | 48 | @Override 49 | HttpDataFrame copy(); 50 | 51 | @Override 52 | HttpDataFrame duplicate(); 53 | 54 | @Override 55 | HttpDataFrame retain(); 56 | 57 | @Override 58 | HttpDataFrame retain(int increment); 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpResponseProxy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.handler.codec.http.HttpResponse; 19 | import io.netty.handler.codec.http.HttpResponseStatus; 20 | import io.netty.handler.codec.http.HttpVersion; 21 | 22 | /** 23 | * An {@link HttpResponse} decorator. 24 | */ 25 | public class HttpResponseProxy extends HttpMessageProxy implements HttpResponse { 26 | 27 | private final HttpResponse response; 28 | 29 | public HttpResponseProxy(HttpResponse response) { 30 | super(response); 31 | this.response = response; 32 | } 33 | 34 | @Override 35 | @Deprecated 36 | public HttpResponseStatus getStatus() { 37 | return response.getStatus(); 38 | } 39 | 40 | @Override 41 | public HttpResponse setStatus(HttpResponseStatus status) { 42 | response.setStatus(status); 43 | return this; 44 | } 45 | 46 | @Override 47 | public HttpResponse setProtocolVersion(HttpVersion version) { 48 | response.setProtocolVersion(version); 49 | return this; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return response.toString(); 55 | } 56 | 57 | @Override 58 | public HttpResponseStatus status() { 59 | return response.status(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/StreamedHttpRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.handler.codec.http.DefaultHttpRequest; 19 | import io.netty.handler.codec.http.HttpContent; 20 | import io.netty.handler.codec.http.HttpMethod; 21 | import io.netty.handler.codec.http.HttpRequest; 22 | import io.netty.handler.codec.http.HttpVersion; 23 | import io.netty.handler.codec.http.LastHttpContent; 24 | import io.netty.util.concurrent.Future; 25 | 26 | /** 27 | * An {@link HttpRequest} that adds content streaming. 28 | */ 29 | public class StreamedHttpRequest extends HttpRequestProxy implements StreamedHttpMessage { 30 | 31 | private Pipe pipe = new Pipe(); 32 | 33 | public StreamedHttpRequest(HttpVersion version, HttpMethod method, String uri) { 34 | this(new DefaultHttpRequest(version, method, uri)); 35 | } 36 | 37 | public StreamedHttpRequest(HttpRequest request) { 38 | super(request); 39 | } 40 | 41 | @Override 42 | public Pipe getContent() { 43 | return pipe; 44 | } 45 | 46 | @Override 47 | public Future addContent(HttpContent content) { 48 | Future future = pipe.send(content); 49 | if (content instanceof LastHttpContent) { 50 | pipe.close(); 51 | } 52 | return future; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/StreamedHttpResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.handler.codec.http.DefaultHttpResponse; 19 | import io.netty.handler.codec.http.HttpContent; 20 | import io.netty.handler.codec.http.HttpResponse; 21 | import io.netty.handler.codec.http.HttpResponseStatus; 22 | import io.netty.handler.codec.http.HttpVersion; 23 | import io.netty.handler.codec.http.LastHttpContent; 24 | import io.netty.util.concurrent.Future; 25 | 26 | /** 27 | * An {@link HttpResponse} that adds content streaming. 28 | */ 29 | public class StreamedHttpResponse extends HttpResponseProxy implements StreamedHttpMessage { 30 | 31 | private Pipe pipe = new Pipe(); 32 | 33 | public StreamedHttpResponse(HttpVersion version, HttpResponseStatus status) { 34 | this(new DefaultHttpResponse(version, status)); 35 | } 36 | 37 | public StreamedHttpResponse(HttpResponse response) { 38 | super(response); 39 | } 40 | 41 | @Override 42 | public Pipe getContent() { 43 | return pipe; 44 | } 45 | 46 | @Override 47 | public Future addContent(HttpContent content) { 48 | Future future = pipe.send(content); 49 | if (content instanceof LastHttpContent) { 50 | pipe.close(); 51 | } 52 | return future; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpPriorityFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | /** 19 | * An HTTP/2 PRIORITY Frame 20 | */ 21 | public interface HttpPriorityFrame extends HttpFrame { 22 | 23 | /** 24 | * Returns the stream identifier of this frame. 25 | */ 26 | int getStreamId(); 27 | 28 | /** 29 | * Sets the stream identifier of this frame. The stream identifier must be positive. 30 | */ 31 | HttpPriorityFrame setStreamId(int streamId); 32 | 33 | /** 34 | * Returns {@code true} if the dependency of the stream is exclusive. 35 | */ 36 | boolean isExclusive(); 37 | 38 | /** 39 | * Sets if the dependency of the stream is exclusive. 40 | */ 41 | HttpPriorityFrame setExclusive(boolean exclusive); 42 | 43 | /** 44 | * Returns the dependency of the stream. 45 | */ 46 | int getDependency(); 47 | 48 | /** 49 | * Sets the dependency of the stream. The dependency cannot be negative. 50 | */ 51 | HttpPriorityFrame setDependency(int dependency); 52 | 53 | /** 54 | * Returns the weight of the dependency of the stream. 55 | */ 56 | int getWeight(); 57 | 58 | /** 59 | * Sets the weight of the dependency of the stream. 60 | * The weight must be positive and cannot exceed 256 bytes. 61 | */ 62 | HttpPriorityFrame setWeight(int weight); 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/DefaultHttpPingFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.util.internal.StringUtil; 19 | 20 | /** 21 | * The default {@link HttpPingFrame} implementation. 22 | */ 23 | public class DefaultHttpPingFrame implements HttpPingFrame { 24 | 25 | private long data; 26 | private boolean pong; 27 | 28 | /** 29 | * Creates a new instance. 30 | * 31 | * @param data the data payload of this frame 32 | */ 33 | public DefaultHttpPingFrame(long data) { 34 | setData(data); 35 | } 36 | 37 | @Override 38 | public long getData() { 39 | return data; 40 | } 41 | 42 | @Override 43 | public HttpPingFrame setData(long data) { 44 | this.data = data; 45 | return this; 46 | } 47 | 48 | @Override 49 | public boolean isPong() { 50 | return pong; 51 | } 52 | 53 | @Override 54 | public HttpPingFrame setPong(boolean pong) { 55 | this.pong = pong; 56 | return this; 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | StringBuilder buf = new StringBuilder(); 62 | buf.append(StringUtil.simpleClassName(this)); 63 | buf.append("(pong: "); 64 | buf.append(isPong()); 65 | buf.append(')'); 66 | buf.append(StringUtil.NEWLINE); 67 | buf.append("--> Data = "); 68 | buf.append(getData()); 69 | return buf.toString(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpMessageProxy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.handler.codec.DecoderResult; 19 | import io.netty.handler.codec.http.HttpHeaders; 20 | import io.netty.handler.codec.http.HttpMessage; 21 | import io.netty.handler.codec.http.HttpVersion; 22 | 23 | /** 24 | * An {@link HttpMessage} decorator. 25 | */ 26 | class HttpMessageProxy implements HttpMessage { 27 | 28 | private final HttpMessage message; 29 | 30 | protected HttpMessageProxy(HttpMessage message) { 31 | this.message = message; 32 | } 33 | 34 | @Override 35 | public HttpVersion getProtocolVersion() { 36 | return message.getProtocolVersion(); 37 | } 38 | 39 | @Override 40 | public HttpMessage setProtocolVersion(HttpVersion version) { 41 | message.setProtocolVersion(version); 42 | return this; 43 | } 44 | 45 | @Override 46 | public HttpHeaders headers() { 47 | return message.headers(); 48 | } 49 | 50 | @Override 51 | @Deprecated 52 | public DecoderResult getDecoderResult() { 53 | return message.getDecoderResult(); 54 | } 55 | 56 | @Override 57 | public void setDecoderResult(DecoderResult result) { 58 | message.setDecoderResult(result); 59 | } 60 | 61 | @Override 62 | public HttpVersion protocolVersion() { 63 | return message.protocolVersion(); 64 | } 65 | 66 | @Override 67 | public DecoderResult decoderResult() { 68 | return message.decoderResult(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpSettingsFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import java.util.Set; 19 | 20 | /** 21 | * An HTTP/2 SETTINGS Frame 22 | */ 23 | public interface HttpSettingsFrame extends HttpFrame { 24 | 25 | int SETTINGS_HEADER_TABLE_SIZE = 1; 26 | int SETTINGS_ENABLE_PUSH = 2; 27 | int SETTINGS_MAX_CONCURRENT_STREAMS = 3; 28 | int SETTINGS_INITIAL_WINDOW_SIZE = 4; 29 | int SETTINGS_MAX_FRAME_SIZE = 5; 30 | int SETTINGS_MAX_HEADER_LIST_SIZE = 6; 31 | 32 | /** 33 | * Returns a {@code Set} of the setting IDs. 34 | * The set's iterator will return the IDs in ascending order. 35 | */ 36 | Set getIds(); 37 | 38 | /** 39 | * Returns {@code true} if the setting ID has a value. 40 | */ 41 | boolean isSet(int id); 42 | 43 | /** 44 | * Returns the value of the setting ID. 45 | * Returns -1 if the setting ID is not set. 46 | */ 47 | int getValue(int id); 48 | 49 | /** 50 | * Sets the value of the setting ID. 51 | * The ID cannot be negative and cannot exceed 65535. 52 | */ 53 | HttpSettingsFrame setValue(int id, int value); 54 | 55 | /** 56 | * Removes the value of the setting ID. 57 | */ 58 | HttpSettingsFrame removeValue(int id); 59 | 60 | /** 61 | * Returns {@code true} if this frame is an acknowledgement frame. 62 | */ 63 | boolean isAck(); 64 | 65 | /** 66 | * Sets if this frame is acknowledgement frame. 67 | */ 68 | HttpSettingsFrame setAck(boolean ack); 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpHeadersFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | /** 19 | * An HTTP/2 HEADERS Frame 20 | */ 21 | public interface HttpHeadersFrame extends HttpHeaderBlockFrame { 22 | 23 | /** 24 | * Returns {@code true} if this frame is the last frame to be transmitted on the stream. 25 | */ 26 | boolean isLast(); 27 | 28 | /** 29 | * Sets if this frame is the last frame to be transmitted on the stream. 30 | */ 31 | HttpHeadersFrame setLast(boolean last); 32 | 33 | /** 34 | * Returns {@code true} if the dependency of the stream is exclusive. 35 | */ 36 | boolean isExclusive(); 37 | 38 | /** 39 | * Sets if the dependency of the stream is exclusive. 40 | */ 41 | HttpHeadersFrame setExclusive(boolean exclusive); 42 | 43 | /** 44 | * Returns the dependency of the stream. 45 | */ 46 | int getDependency(); 47 | 48 | /** 49 | * Sets the dependency of the stream. The dependency cannot be negative. 50 | */ 51 | HttpHeadersFrame setDependency(int dependency); 52 | 53 | /** 54 | * Returns the weight of the dependency of the stream. 55 | */ 56 | int getWeight(); 57 | 58 | /** 59 | * Sets the weight of the dependency of the stream. 60 | * The weight must be positive and cannot exceed 256 bytes. 61 | */ 62 | HttpHeadersFrame setWeight(int weight); 63 | 64 | @Override 65 | HttpHeadersFrame setStreamId(int streamId); 66 | 67 | @Override 68 | HttpHeadersFrame setInvalid(); 69 | 70 | @Override 71 | HttpHeadersFrame setTruncated(); 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/DefaultHttp2Headers.java: -------------------------------------------------------------------------------- 1 | package com.twitter.http2; 2 | 3 | import io.netty.handler.codec.DefaultHeaders.NameValidator; 4 | import io.netty.handler.codec.http.DefaultHttpHeaders; 5 | import io.netty.util.AsciiString; 6 | import io.netty.util.ByteProcessor; 7 | import io.netty.util.internal.PlatformDependent; 8 | 9 | import static io.netty.util.internal.ObjectUtil.checkNotNull; 10 | 11 | public class DefaultHttp2Headers extends DefaultHttpHeaders { 12 | private static final ByteProcessor HEADER_NAME_VALIDATOR = new ByteProcessor() { 13 | @Override 14 | public boolean process(byte value) throws Exception { 15 | validateChar((char) (value & 0xFF)); 16 | return true; 17 | } 18 | }; 19 | 20 | private static void validateChar(char character) { 21 | switch (character) { 22 | case '\t': 23 | case '\n': 24 | case 0x0b: 25 | case '\f': 26 | case '\r': 27 | case ' ': 28 | case ',': 29 | case ';': 30 | case '=': 31 | throw new IllegalArgumentException( 32 | "a header name cannot contain the following prohibited characters: =,; \\t\\r\\n\\v\\f: " + 33 | character); 34 | default: 35 | // Check to see if the character is not an ASCII character, or invalid 36 | if (character > 127) { 37 | throw new IllegalArgumentException("a header name cannot contain non-ASCII character: " + 38 | character); 39 | } 40 | } 41 | } 42 | 43 | static final NameValidator Http2NameValidator = new NameValidator() { 44 | @Override 45 | public void validateName(CharSequence name) { 46 | if (name instanceof AsciiString) { 47 | try { 48 | ((AsciiString) name).forEachByte(HEADER_NAME_VALIDATOR); 49 | } catch (Exception e) { 50 | PlatformDependent.throwException(e); 51 | } 52 | } else { 53 | checkNotNull(name, "name"); 54 | // Go through each character in the name 55 | for (int index = 0; index < name.length(); ++index) { 56 | validateChar(name.charAt(index)); 57 | } 58 | } 59 | } 60 | }; 61 | 62 | public DefaultHttp2Headers() { 63 | super(true, Http2NameValidator); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/StreamedHttpMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.handler.codec.http.HttpContent; 19 | import io.netty.handler.codec.http.HttpMessage; 20 | import io.netty.handler.codec.http.LastHttpContent; 21 | import io.netty.util.concurrent.Future; 22 | 23 | /** 24 | * An {@link HttpMessage} that adds support for streaming content using {@linkplain Pipe pipes}. 25 | * @see Pipe 26 | */ 27 | public interface StreamedHttpMessage extends HttpMessage { 28 | 29 | /** 30 | * Gets the pipe for sending the message body as {@linkplain HttpContent} objects. 31 | * A {@linkplain LastHttpContent last http content} will indicate the end of the body. 32 | * 33 | * @return the message body pipe. 34 | * @see RFC 2616, 3.6.1 Chunked Transfer Coding 35 | * @see RFC 2616, 4.3 Message Body 36 | */ 37 | Pipe getContent(); 38 | 39 | /** 40 | * Adds content to the pipe. This returns a future for determining whether the message was received. 41 | * If the chunk is the last chunk then the pipe will be closed. 42 | *

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

44 | *

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

46 | * 47 | * @param content the content to send 48 | * @return a future indicating when the message was received. 49 | */ 50 | Future addContent(HttpContent content); 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpRequestProxy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.handler.codec.http.HttpMethod; 19 | import io.netty.handler.codec.http.HttpRequest; 20 | import io.netty.handler.codec.http.HttpVersion; 21 | 22 | /** 23 | * An {@link HttpRequest} decorator. 24 | */ 25 | class HttpRequestProxy extends HttpMessageProxy implements HttpRequest { 26 | 27 | private final HttpRequest request; 28 | 29 | public HttpRequestProxy(HttpRequest request) { 30 | super(request); 31 | this.request = request; 32 | } 33 | 34 | public HttpRequest httpRequest() { 35 | return request; 36 | } 37 | 38 | @Override 39 | @Deprecated 40 | public HttpMethod getMethod() { 41 | return request.getMethod(); 42 | } 43 | 44 | @Override 45 | public HttpRequest setMethod(HttpMethod method) { 46 | request.setMethod(method); 47 | return this; 48 | } 49 | 50 | @Override 51 | @Deprecated 52 | public String getUri() { 53 | return request.getUri(); 54 | } 55 | 56 | @Override 57 | public HttpRequest setUri(String uri) { 58 | request.setUri(uri); 59 | return this; 60 | } 61 | 62 | @Override 63 | public HttpRequest setProtocolVersion(HttpVersion version) { 64 | request.setProtocolVersion(version); 65 | return this; 66 | } 67 | 68 | @Override 69 | public String toString() { 70 | return request.toString(); 71 | } 72 | 73 | @Override 74 | public HttpMethod method() { 75 | return request.method(); 76 | } 77 | 78 | @Override 79 | public String uri() { 80 | return request.uri(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/DefaultHttpHeaderBlockFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.handler.codec.http.HttpHeaders; 19 | import io.netty.util.internal.StringUtil; 20 | 21 | import java.util.Map; 22 | 23 | /** 24 | * The default {@link HttpHeaderBlockFrame} implementation. 25 | */ 26 | public abstract class DefaultHttpHeaderBlockFrame extends DefaultHttpStreamFrame 27 | implements HttpHeaderBlockFrame { 28 | 29 | private boolean invalid; 30 | private boolean truncated; 31 | private final HttpHeaders headers = new DefaultHttp2Headers(); 32 | 33 | /** 34 | * Creates a new instance. 35 | * 36 | * @param streamId the stream identifier of this frame 37 | */ 38 | protected DefaultHttpHeaderBlockFrame(int streamId) { 39 | super(streamId); 40 | } 41 | 42 | @Override 43 | public boolean isInvalid() { 44 | return invalid; 45 | } 46 | 47 | @Override 48 | public HttpHeaderBlockFrame setInvalid() { 49 | invalid = true; 50 | return this; 51 | } 52 | 53 | @Override 54 | public boolean isTruncated() { 55 | return truncated; 56 | } 57 | 58 | @Override 59 | public HttpHeaderBlockFrame setTruncated() { 60 | truncated = true; 61 | return this; 62 | } 63 | 64 | @Override 65 | public HttpHeaders headers() { 66 | return headers; 67 | } 68 | 69 | protected void appendHeaders(StringBuilder buf) { 70 | for (Map.Entry e : headers()) { 71 | buf.append(" "); 72 | buf.append(e.getKey()); 73 | buf.append(": "); 74 | buf.append(e.getValue()); 75 | buf.append(StringUtil.NEWLINE); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/twitter/http2/HttpResponseProxyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.handler.codec.http.DefaultHttpResponse; 19 | import io.netty.handler.codec.http.HttpResponse; 20 | import io.netty.handler.codec.http.HttpResponseStatus; 21 | import io.netty.handler.codec.http.HttpVersion; 22 | import org.junit.Test; 23 | 24 | import static org.junit.Assert.assertEquals; 25 | import static org.junit.Assert.assertSame; 26 | 27 | public class HttpResponseProxyTest { 28 | 29 | private HttpResponse httpResponse = new DefaultHttpResponse( 30 | HttpVersion.HTTP_1_0, HttpResponseStatus.OK); 31 | private HttpResponseProxy httpResponseProxy = new HttpResponseProxy(httpResponse); 32 | 33 | @Test 34 | public void testSetStatus() { 35 | assertSame(httpResponseProxy, httpResponseProxy.setStatus(HttpResponseStatus.NOT_FOUND)); 36 | assertEquals(httpResponse.getStatus(), httpResponseProxy.getStatus()); 37 | } 38 | 39 | @Test 40 | public void testGetStatus() { 41 | assertEquals(httpResponse.getStatus(), httpResponseProxy.getStatus()); 42 | } 43 | 44 | @Test 45 | public void testToString() { 46 | assertEquals(httpResponse.toString(), httpResponseProxy.toString()); 47 | } 48 | 49 | @Test 50 | public void testGetProtocolVersion() { 51 | assertEquals(httpResponse.getProtocolVersion(), httpResponseProxy.getProtocolVersion()); 52 | } 53 | 54 | @Test 55 | public void testSetProtocolVersion() { 56 | assertSame(httpResponseProxy, httpResponseProxy.setProtocolVersion(HttpVersion.HTTP_1_1)); 57 | assertEquals(httpResponse.getProtocolVersion(), httpResponseProxy.getProtocolVersion()); 58 | } 59 | 60 | @Test 61 | public void testHeaders() { 62 | assertSame(httpResponse.headers(), httpResponseProxy.headers()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/DefaultHttpRstStreamFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.util.internal.StringUtil; 19 | 20 | /** 21 | * The default {@link HttpRstStreamFrame} implementation. 22 | */ 23 | public class DefaultHttpRstStreamFrame implements HttpRstStreamFrame { 24 | 25 | private int streamId; 26 | private HttpErrorCode errorCode; 27 | 28 | /** 29 | * Creates a new instance. 30 | * 31 | * @param streamId the stream identifier of this frame 32 | * @param code the error code of this frame 33 | */ 34 | public DefaultHttpRstStreamFrame(int streamId, int code) { 35 | this(streamId, HttpErrorCode.valueOf(code)); 36 | } 37 | 38 | /** 39 | * Creates a new instance. 40 | * 41 | * @param streamId the stream identifier of this frame 42 | * @param errorCode the error code of this frame 43 | */ 44 | public DefaultHttpRstStreamFrame(int streamId, HttpErrorCode errorCode) { 45 | setStreamId(streamId); 46 | setErrorCode(errorCode); 47 | } 48 | 49 | @Override 50 | public int getStreamId() { 51 | return streamId; 52 | } 53 | 54 | @Override 55 | public HttpRstStreamFrame setStreamId(int streamId) { 56 | if (streamId <= 0) { 57 | throw new IllegalArgumentException( 58 | "Stream identifier must be positive: " + streamId); 59 | } 60 | this.streamId = streamId; 61 | return this; 62 | } 63 | 64 | @Override 65 | public HttpErrorCode getErrorCode() { 66 | return errorCode; 67 | } 68 | 69 | @Override 70 | public HttpRstStreamFrame setErrorCode(HttpErrorCode errorCode) { 71 | this.errorCode = errorCode; 72 | return this; 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | StringBuilder buf = new StringBuilder(); 78 | buf.append(StringUtil.simpleClassName(this)); 79 | buf.append(StringUtil.NEWLINE); 80 | buf.append("--> Stream-ID = "); 81 | buf.append(getStreamId()); 82 | buf.append(StringUtil.NEWLINE); 83 | buf.append("--> Error Code: "); 84 | buf.append(getErrorCode().toString()); 85 | return buf.toString(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/DefaultHttpWindowUpdateFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.util.internal.StringUtil; 19 | 20 | /** 21 | * The default {@link HttpWindowUpdateFrame} implementation. 22 | */ 23 | public class DefaultHttpWindowUpdateFrame implements HttpWindowUpdateFrame { 24 | 25 | private int streamId; 26 | private int windowSizeIncrement; 27 | 28 | /** 29 | * Creates a new instance. 30 | * 31 | * @param streamId the stream identifier of this frame 32 | * @param windowSizeIncrement the Window-Size-Increment of this frame 33 | */ 34 | public DefaultHttpWindowUpdateFrame(int streamId, int windowSizeIncrement) { 35 | setStreamId(streamId); 36 | setWindowSizeIncrement(windowSizeIncrement); 37 | } 38 | 39 | @Override 40 | public int getStreamId() { 41 | return streamId; 42 | } 43 | 44 | @Override 45 | public DefaultHttpWindowUpdateFrame setStreamId(int streamId) { 46 | if (streamId < 0) { 47 | throw new IllegalArgumentException( 48 | "Stream identifier cannot be negative: " + streamId); 49 | } 50 | this.streamId = streamId; 51 | return this; 52 | } 53 | 54 | @Override 55 | public int getWindowSizeIncrement() { 56 | return windowSizeIncrement; 57 | } 58 | 59 | @Override 60 | public DefaultHttpWindowUpdateFrame setWindowSizeIncrement(int windowSizeIncrement) { 61 | if (windowSizeIncrement <= 0) { 62 | throw new IllegalArgumentException( 63 | "Window-Size-Increment must be positive: " + windowSizeIncrement); 64 | } 65 | this.windowSizeIncrement = windowSizeIncrement; 66 | return this; 67 | } 68 | 69 | @Override 70 | public String toString() { 71 | StringBuilder buf = new StringBuilder(); 72 | buf.append(StringUtil.simpleClassName(this)); 73 | buf.append(StringUtil.NEWLINE); 74 | buf.append("--> Stream-ID = "); 75 | buf.append(getStreamId()); 76 | buf.append(StringUtil.NEWLINE); 77 | buf.append("--> Window-Size-Increment = "); 78 | buf.append(getWindowSizeIncrement()); 79 | return buf.toString(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/test/java/com/twitter/http2/HttpRequestProxyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.handler.codec.http.DefaultHttpRequest; 19 | import io.netty.handler.codec.http.HttpMethod; 20 | import io.netty.handler.codec.http.HttpRequest; 21 | import io.netty.handler.codec.http.HttpVersion; 22 | import org.junit.Test; 23 | 24 | import static org.junit.Assert.assertEquals; 25 | import static org.junit.Assert.assertSame; 26 | 27 | public class HttpRequestProxyTest { 28 | 29 | private HttpRequest httpRequest = new DefaultHttpRequest( 30 | HttpVersion.HTTP_1_0, HttpMethod.GET, "/"); 31 | private HttpRequestProxy httpRequestProxy = new HttpRequestProxy(httpRequest); 32 | 33 | @Test 34 | public void testHttpRequest() { 35 | assertSame(httpRequest, httpRequestProxy.httpRequest()); 36 | } 37 | 38 | @Test 39 | public void testGetMethod() { 40 | assertEquals(httpRequest.getMethod(), httpRequestProxy.getMethod()); 41 | } 42 | 43 | @Test 44 | public void testSetMethod() { 45 | assertSame(httpRequestProxy, httpRequestProxy.setMethod(HttpMethod.POST)); 46 | assertEquals(httpRequest.getMethod(), httpRequestProxy.getMethod()); 47 | } 48 | 49 | @Test 50 | public void testGetUri() { 51 | assertEquals(httpRequest.getUri(), httpRequestProxy.getUri()); 52 | } 53 | 54 | @Test 55 | public void testSetUri() { 56 | assertSame(httpRequestProxy, httpRequestProxy.setUri("/path")); 57 | assertEquals(httpRequest.getUri(), httpRequestProxy.getUri()); 58 | } 59 | 60 | @Test 61 | public void testToString() { 62 | assertEquals(httpRequest.toString(), httpRequestProxy.toString()); 63 | } 64 | 65 | @Test 66 | public void testGetProtocolVersion() { 67 | assertEquals(httpRequest.getProtocolVersion(), httpRequestProxy.getProtocolVersion()); 68 | } 69 | 70 | @Test 71 | public void testSetProtocolVersion() { 72 | assertSame(httpRequestProxy, httpRequestProxy.setProtocolVersion(HttpVersion.HTTP_1_1)); 73 | assertEquals(httpRequest.getProtocolVersion(), httpRequestProxy.getProtocolVersion()); 74 | } 75 | 76 | @Test 77 | public void testHeaders() { 78 | assertSame(httpRequest.headers(), httpRequestProxy.headers()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/DefaultHttpGoAwayFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.util.internal.StringUtil; 19 | 20 | /** 21 | * The default {@link HttpGoAwayFrame} implementation. 22 | */ 23 | public class DefaultHttpGoAwayFrame implements HttpGoAwayFrame { 24 | 25 | private int lastStreamId; 26 | private HttpErrorCode errorCode; 27 | 28 | /** 29 | * Creates a new instance. 30 | * 31 | * @param lastStreamId the Last-Stream-ID of this frame 32 | * @param code the error code of this frame 33 | */ 34 | public DefaultHttpGoAwayFrame(int lastStreamId, int code) { 35 | this(lastStreamId, HttpErrorCode.valueOf(code)); 36 | } 37 | 38 | /** 39 | * Creates a new instance. 40 | * 41 | * @param lastStreamId the Last-Stream-ID of this frame 42 | * @param errorCode the error code of this frame 43 | */ 44 | public DefaultHttpGoAwayFrame(int lastStreamId, HttpErrorCode errorCode) { 45 | setLastStreamId(lastStreamId); 46 | setErrorCode(errorCode); 47 | } 48 | 49 | @Override 50 | public int getLastStreamId() { 51 | return lastStreamId; 52 | } 53 | 54 | @Override 55 | public HttpGoAwayFrame setLastStreamId(int lastStreamId) { 56 | if (lastStreamId < 0) { 57 | throw new IllegalArgumentException( 58 | "Last-Stream-ID cannot be negative: " + lastStreamId); 59 | } 60 | this.lastStreamId = lastStreamId; 61 | return this; 62 | } 63 | 64 | @Override 65 | public HttpErrorCode getErrorCode() { 66 | return errorCode; 67 | } 68 | 69 | @Override 70 | public HttpGoAwayFrame setErrorCode(HttpErrorCode errorCode) { 71 | this.errorCode = errorCode; 72 | return this; 73 | } 74 | 75 | @Override 76 | public String toString() { 77 | StringBuilder buf = new StringBuilder(); 78 | buf.append(StringUtil.simpleClassName(this)); 79 | buf.append(StringUtil.NEWLINE); 80 | buf.append("--> Last-Stream-ID = "); 81 | buf.append(getLastStreamId()); 82 | buf.append(StringUtil.NEWLINE); 83 | buf.append("--> Error Code: "); 84 | buf.append(getErrorCode().toString()); 85 | return buf.toString(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/DefaultHttpPushPromiseFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.util.internal.StringUtil; 19 | 20 | /** 21 | * The default {@link HttpPushPromiseFrame} implementation. 22 | */ 23 | public class DefaultHttpPushPromiseFrame extends DefaultHttpHeaderBlockFrame 24 | implements HttpPushPromiseFrame { 25 | 26 | private int promisedStreamId; 27 | 28 | /** 29 | * Creates a new instance. 30 | * 31 | * @param streamId the stream identifier of this frame 32 | */ 33 | public DefaultHttpPushPromiseFrame(int streamId, int promisedStreamId) { 34 | super(streamId); 35 | setPromisedStreamId(promisedStreamId); 36 | } 37 | 38 | @Override 39 | public int getPromisedStreamId() { 40 | return promisedStreamId; 41 | } 42 | 43 | @Override 44 | public HttpPushPromiseFrame setPromisedStreamId(int promisedStreamId) { 45 | if (promisedStreamId <= 0) { 46 | throw new IllegalArgumentException( 47 | "Promised-Stream-ID must be positive: " + promisedStreamId); 48 | } 49 | this.promisedStreamId = promisedStreamId; 50 | return this; 51 | } 52 | 53 | @Override 54 | public HttpPushPromiseFrame setStreamId(int streamId) { 55 | super.setStreamId(streamId); 56 | return this; 57 | } 58 | 59 | @Override 60 | public HttpPushPromiseFrame setInvalid() { 61 | super.setInvalid(); 62 | return this; 63 | } 64 | 65 | @Override 66 | public HttpPushPromiseFrame setTruncated() { 67 | super.setTruncated(); 68 | return this; 69 | } 70 | 71 | @Override 72 | public String toString() { 73 | StringBuilder buf = new StringBuilder(); 74 | buf.append(StringUtil.simpleClassName(this)); 75 | buf.append(StringUtil.NEWLINE); 76 | buf.append("--> Stream-ID = "); 77 | buf.append(getStreamId()); 78 | buf.append(StringUtil.NEWLINE); 79 | buf.append("--> Promised-Stream-ID = "); 80 | buf.append(getPromisedStreamId()); 81 | buf.append(StringUtil.NEWLINE); 82 | buf.append("--> Headers:"); 83 | buf.append(StringUtil.NEWLINE); 84 | appendHeaders(buf); 85 | 86 | // Remove the last newline. 87 | buf.setLength(buf.length() - StringUtil.NEWLINE.length()); 88 | return buf.toString(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/DefaultHttpSettingsFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import java.util.Map; 19 | import java.util.Set; 20 | import java.util.TreeMap; 21 | 22 | import io.netty.util.internal.StringUtil; 23 | 24 | /** 25 | * The default {@link HttpSettingsFrame} implementation. 26 | */ 27 | public class DefaultHttpSettingsFrame implements HttpSettingsFrame { 28 | 29 | private final Map settingsMap = new TreeMap(); 30 | private boolean ack; 31 | 32 | @Override 33 | public Set getIds() { 34 | return settingsMap.keySet(); 35 | } 36 | 37 | @Override 38 | public boolean isSet(int id) { 39 | return settingsMap.containsKey(id); 40 | } 41 | 42 | @Override 43 | public int getValue(int id) { 44 | if (settingsMap.containsKey(id)) { 45 | return settingsMap.get(id); 46 | } else { 47 | return -1; 48 | } 49 | } 50 | 51 | @Override 52 | public HttpSettingsFrame setValue(int id, int value) { 53 | if (id < 0 || id > HttpCodecUtil.HTTP_SETTINGS_MAX_ID) { 54 | throw new IllegalArgumentException("Setting ID is not valid: " + id); 55 | } 56 | settingsMap.put(id, value); 57 | return this; 58 | } 59 | 60 | @Override 61 | public HttpSettingsFrame removeValue(int id) { 62 | settingsMap.remove(id); 63 | return this; 64 | } 65 | 66 | @Override 67 | public boolean isAck() { 68 | return ack; 69 | } 70 | 71 | @Override 72 | public HttpSettingsFrame setAck(boolean ack) { 73 | this.ack = ack; 74 | return this; 75 | } 76 | 77 | private Set> getSettings() { 78 | return settingsMap.entrySet(); 79 | } 80 | 81 | private void appendSettings(StringBuilder buf) { 82 | for (Map.Entry e : getSettings()) { 83 | buf.append("--> "); 84 | buf.append(e.getKey().toString()); 85 | buf.append(':'); 86 | buf.append(e.getValue().toString()); 87 | buf.append(StringUtil.NEWLINE); 88 | } 89 | } 90 | 91 | @Override 92 | public String toString() { 93 | StringBuilder buf = new StringBuilder(); 94 | buf.append(StringUtil.simpleClassName(this)); 95 | buf.append("(ack: "); 96 | buf.append(isAck()); 97 | buf.append(')'); 98 | buf.append(StringUtil.NEWLINE); 99 | appendSettings(buf); 100 | buf.setLength(buf.length() - StringUtil.NEWLINE.length()); 101 | return buf.toString(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpFrameDecoderDelegate.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.buffer.ByteBuf; 19 | 20 | /** 21 | * Callback interface for {@link HttpFrameDecoder}. 22 | */ 23 | public interface HttpFrameDecoderDelegate { 24 | 25 | /** 26 | * Called when a DATA frame is received. 27 | */ 28 | void readDataFramePadding(int streamId, boolean endStream, int padding); 29 | 30 | /** 31 | * Called when a DATA frame is received. 32 | */ 33 | void readDataFrame(int streamId, boolean endStream, boolean endSegment, ByteBuf data); 34 | 35 | /** 36 | * Called when a HEADERS frame is received. 37 | * The Header Block Fragment is not included. See readHeaderBlock(). 38 | */ 39 | void readHeadersFrame( 40 | int streamId, 41 | boolean endStream, 42 | boolean endSegment, 43 | boolean exclusive, 44 | int dependency, 45 | int weight 46 | ); 47 | 48 | /** 49 | * Called when a PRIORITY frame is received. 50 | */ 51 | void readPriorityFrame(int streamId, boolean exclusive, int dependency, int weight); 52 | 53 | /** 54 | * Called when a RST_STREAM frame is received. 55 | */ 56 | void readRstStreamFrame(int streamId, int errorCode); 57 | 58 | /** 59 | * Called when a SETTINGS frame is received. 60 | * Settings are not included. See readSetting(). 61 | */ 62 | void readSettingsFrame(boolean ack); 63 | 64 | /** 65 | * Called when an individual setting within a SETTINGS frame is received. 66 | */ 67 | void readSetting(int id, int value); 68 | 69 | /** 70 | * Called when the entire SETTINGS frame has been received. 71 | */ 72 | void readSettingsEnd(); 73 | 74 | /** 75 | * Called when a PUSH_PROMISE frame is received. 76 | * The Header Block Fragment is not included. See readHeaderBlock(). 77 | */ 78 | void readPushPromiseFrame(int streamId, int promisedStreamId); 79 | 80 | /** 81 | * Called when a PING frame is received. 82 | */ 83 | void readPingFrame(long data, boolean ack); 84 | 85 | /** 86 | * Called when a GOAWAY frame is received. 87 | */ 88 | void readGoAwayFrame(int lastStreamId, int errorCode); 89 | 90 | /** 91 | * Called when a WINDOW_UPDATE frame is received. 92 | */ 93 | void readWindowUpdateFrame(int streamId, int windowSizeIncrement); 94 | 95 | /** 96 | * Called when the header block fragment within a HEADERS, 97 | * PUSH_PROMISE, or CONTINUATION frame is received. 98 | */ 99 | void readHeaderBlock(ByteBuf headerBlockFragment); 100 | 101 | /** 102 | * Called when an entire header block has been received. 103 | */ 104 | void readHeaderBlockEnd(); 105 | 106 | /** 107 | * Called when an unrecoverable connection error has occurred. 108 | */ 109 | void readFrameError(String message); 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/DefaultHttpPriorityFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.util.internal.StringUtil; 19 | 20 | /** 21 | * The default {@link HttpPriorityFrame} implementation. 22 | */ 23 | public class DefaultHttpPriorityFrame implements HttpPriorityFrame { 24 | 25 | private int streamId; 26 | private boolean exclusive; 27 | private int dependency; 28 | private int weight; 29 | 30 | /** 31 | * Creates a new instance. 32 | * 33 | * @param streamId the stream identifier of this frame 34 | * @param exclusive if the dependency of the stream is exclusive 35 | * @param dependency the dependency of the stream 36 | * @param weight the weight of the dependency of the stream 37 | */ 38 | public DefaultHttpPriorityFrame(int streamId, boolean exclusive, int dependency, int weight) { 39 | setStreamId(streamId); 40 | setExclusive(exclusive); 41 | setDependency(dependency); 42 | setWeight(weight); 43 | } 44 | 45 | @Override 46 | public int getStreamId() { 47 | return streamId; 48 | } 49 | 50 | @Override 51 | public HttpPriorityFrame setStreamId(int streamId) { 52 | if (streamId <= 0) { 53 | throw new IllegalArgumentException( 54 | "Stream identifier must be positive: " + streamId); 55 | } 56 | this.streamId = streamId; 57 | return this; 58 | } 59 | 60 | @Override 61 | public boolean isExclusive() { 62 | return exclusive; 63 | } 64 | 65 | @Override 66 | public HttpPriorityFrame setExclusive(boolean exclusive) { 67 | this.exclusive = exclusive; 68 | return this; 69 | } 70 | 71 | @Override 72 | public int getDependency() { 73 | return dependency; 74 | } 75 | 76 | @Override 77 | public HttpPriorityFrame setDependency(int dependency) { 78 | if (dependency < 0) { 79 | throw new IllegalArgumentException( 80 | "Dependency cannot be negative: " + dependency); 81 | } 82 | this.dependency = dependency; 83 | return this; 84 | } 85 | 86 | @Override 87 | public int getWeight() { 88 | return weight; 89 | } 90 | 91 | @Override 92 | public HttpPriorityFrame setWeight(int weight) { 93 | if (weight <= 0 || weight > 256) { 94 | throw new IllegalArgumentException( 95 | "Illegal weight: " + weight); 96 | } 97 | this.weight = weight; 98 | return this; 99 | } 100 | 101 | @Override 102 | public String toString() { 103 | StringBuilder buf = new StringBuilder(); 104 | buf.append(StringUtil.simpleClassName(this)); 105 | buf.append(StringUtil.NEWLINE); 106 | buf.append("--> Stream-ID = "); 107 | buf.append(getStreamId()); 108 | buf.append(StringUtil.NEWLINE); 109 | buf.append("--> Dependency = "); 110 | buf.append(getDependency()); 111 | buf.append(" (exclusive: "); 112 | buf.append(isExclusive()); 113 | buf.append(", weight: "); 114 | buf.append(getWeight()); 115 | buf.append(')'); 116 | return buf.toString(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/DefaultHttpHeadersFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.util.internal.StringUtil; 19 | 20 | import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_DEPENDENCY; 21 | import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_WEIGHT; 22 | 23 | /** 24 | * The default {@link HttpHeadersFrame} implementation. 25 | */ 26 | public class DefaultHttpHeadersFrame extends DefaultHttpHeaderBlockFrame 27 | implements HttpHeadersFrame { 28 | 29 | private boolean last; 30 | private boolean exclusive = false; 31 | private int dependency = HTTP_DEFAULT_DEPENDENCY; 32 | private int weight = HTTP_DEFAULT_WEIGHT; 33 | 34 | /** 35 | * Creates a new instance. 36 | * 37 | * @param streamId the stream identifier of this frame 38 | */ 39 | public DefaultHttpHeadersFrame(int streamId) { 40 | super(streamId); 41 | } 42 | 43 | @Override 44 | public boolean isLast() { 45 | return last; 46 | } 47 | 48 | @Override 49 | public HttpHeadersFrame setLast(boolean last) { 50 | this.last = last; 51 | return this; 52 | } 53 | 54 | @Override 55 | public boolean isExclusive() { 56 | return exclusive; 57 | } 58 | 59 | @Override 60 | public HttpHeadersFrame setExclusive(boolean exclusive) { 61 | this.exclusive = exclusive; 62 | return this; 63 | } 64 | 65 | @Override 66 | public int getDependency() { 67 | return dependency; 68 | } 69 | 70 | @Override 71 | public HttpHeadersFrame setDependency(int dependency) { 72 | if (dependency < 0) { 73 | throw new IllegalArgumentException( 74 | "Dependency cannot be negative: " + dependency); 75 | } 76 | this.dependency = dependency; 77 | return this; 78 | } 79 | 80 | @Override 81 | public int getWeight() { 82 | return weight; 83 | } 84 | 85 | @Override 86 | public HttpHeadersFrame setWeight(int weight) { 87 | if (weight <= 0 || weight > 256) { 88 | throw new IllegalArgumentException( 89 | "Illegal weight: " + weight); 90 | } 91 | this.weight = weight; 92 | return this; 93 | } 94 | 95 | @Override 96 | public HttpHeadersFrame setStreamId(int streamId) { 97 | super.setStreamId(streamId); 98 | return this; 99 | } 100 | 101 | @Override 102 | public HttpHeadersFrame setInvalid() { 103 | super.setInvalid(); 104 | return this; 105 | } 106 | 107 | @Override 108 | public HttpHeadersFrame setTruncated() { 109 | super.setTruncated(); 110 | return this; 111 | } 112 | 113 | @Override 114 | public String toString() { 115 | StringBuilder buf = new StringBuilder(); 116 | buf.append(StringUtil.simpleClassName(this)); 117 | buf.append("(last: "); 118 | buf.append(isLast()); 119 | buf.append(')'); 120 | buf.append(StringUtil.NEWLINE); 121 | buf.append("--> Stream-ID = "); 122 | buf.append(getStreamId()); 123 | buf.append(StringUtil.NEWLINE); 124 | buf.append("--> Dependency = "); 125 | buf.append(getDependency()); 126 | buf.append(" (exclusive: "); 127 | buf.append(isExclusive()); 128 | buf.append(", weight: "); 129 | buf.append(getWeight()); 130 | buf.append(')'); 131 | buf.append(StringUtil.NEWLINE); 132 | buf.append("--> Headers:"); 133 | buf.append(StringUtil.NEWLINE); 134 | appendHeaders(buf); 135 | 136 | // Remove the last newline. 137 | buf.setLength(buf.length() - StringUtil.NEWLINE.length()); 138 | return buf.toString(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpCodecUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.buffer.ByteBuf; 19 | 20 | final class HttpCodecUtil { 21 | 22 | static final int HTTP_FRAME_HEADER_SIZE = 9; 23 | static final int HTTP_MAX_LENGTH = 0x4000; // Initial MAX_FRAME_SIZE value is 2^14 24 | 25 | static final int HTTP_DATA_FRAME = 0x00; 26 | static final int HTTP_HEADERS_FRAME = 0x01; 27 | static final int HTTP_PRIORITY_FRAME = 0x02; 28 | static final int HTTP_RST_STREAM_FRAME = 0x03; 29 | static final int HTTP_SETTINGS_FRAME = 0x04; 30 | static final int HTTP_PUSH_PROMISE_FRAME = 0x05; 31 | static final int HTTP_PING_FRAME = 0x06; 32 | static final int HTTP_GOAWAY_FRAME = 0x07; 33 | static final int HTTP_WINDOW_UPDATE_FRAME = 0x08; 34 | static final int HTTP_CONTINUATION_FRAME = 0x09; 35 | 36 | static final byte HTTP_FLAG_ACK = 0x01; 37 | static final byte HTTP_FLAG_END_STREAM = 0x01; 38 | static final byte HTTP_FLAG_END_SEGMENT = 0x02; 39 | static final byte HTTP_FLAG_END_HEADERS = 0x04; 40 | static final byte HTTP_FLAG_PADDED = 0x08; 41 | static final byte HTTP_FLAG_PRIORITY = 0x20; 42 | 43 | static final int HTTP_DEFAULT_WEIGHT = 16; 44 | static final int HTTP_DEFAULT_DEPENDENCY = 0; 45 | 46 | static final int HTTP_SETTINGS_MAX_ID = 0xFFFF; // Identifier is a 16-bit field 47 | 48 | static final int HTTP_CONNECTION_STREAM_ID = 0; 49 | 50 | /** 51 | * Reads a big-endian unsigned short integer from the buffer. 52 | */ 53 | static int getUnsignedShort(ByteBuf buf, int offset) { 54 | return (buf.getByte(offset) & 0xFF) << 8 55 | | buf.getByte(offset + 1) & 0xFF; 56 | } 57 | 58 | /** 59 | * Reads a big-endian unsigned medium integer from the buffer. 60 | */ 61 | static int getUnsignedMedium(ByteBuf buf, int offset) { 62 | return (buf.getByte(offset) & 0xFF) << 16 63 | | (buf.getByte(offset + 1) & 0xFF) << 8 64 | | buf.getByte(offset + 2) & 0xFF; 65 | } 66 | 67 | /** 68 | * Reads a big-endian (31-bit) integer from the buffer. 69 | */ 70 | static int getUnsignedInt(ByteBuf buf, int offset) { 71 | return (buf.getByte(offset) & 0x7F) << 24 72 | | (buf.getByte(offset + 1) & 0xFF) << 16 73 | | (buf.getByte(offset + 2) & 0xFF) << 8 74 | | buf.getByte(offset + 3) & 0xFF; 75 | } 76 | 77 | /** 78 | * Reads a big-endian signed integer from the buffer. 79 | */ 80 | static int getSignedInt(ByteBuf buf, int offset) { 81 | return (buf.getByte(offset) & 0xFF) << 24 82 | | (buf.getByte(offset + 1) & 0xFF) << 16 83 | | (buf.getByte(offset + 2) & 0xFF) << 8 84 | | buf.getByte(offset + 3) & 0xFF; 85 | } 86 | 87 | /** 88 | * Reads a big-endian signed long from the buffer. 89 | */ 90 | static long getSignedLong(ByteBuf buf, int offset) { 91 | return ((long) buf.getByte(offset) & 0xFF) << 56 92 | | ((long) buf.getByte(offset + 1) & 0xFF) << 48 93 | | ((long) buf.getByte(offset + 2) & 0xFF) << 40 94 | | ((long) buf.getByte(offset + 3) & 0xFF) << 32 95 | | ((long) buf.getByte(offset + 4) & 0xFF) << 24 96 | | ((long) buf.getByte(offset + 5) & 0xFF) << 16 97 | | ((long) buf.getByte(offset + 6) & 0xFF) << 8 98 | | (long) buf.getByte(offset + 7) & 0xFF; 99 | } 100 | 101 | /** 102 | * Returns {@code true} if the stream identifier is for a server initiated stream. 103 | */ 104 | static boolean isServerId(int streamId) { 105 | // Server initiated streams have even stream identifiers 106 | return streamId % 2 == 0; 107 | } 108 | 109 | private HttpCodecUtil() { 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/DefaultHttpDataFrame.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import io.netty.buffer.ByteBuf; 19 | import io.netty.buffer.ByteBufHolder; 20 | import io.netty.buffer.Unpooled; 21 | import io.netty.util.IllegalReferenceCountException; 22 | import io.netty.util.internal.StringUtil; 23 | 24 | /** 25 | * The default {@link HttpDataFrame} implementation. 26 | */ 27 | public class DefaultHttpDataFrame extends DefaultHttpStreamFrame implements HttpDataFrame { 28 | 29 | private final ByteBuf data; 30 | private boolean last; 31 | 32 | /** 33 | * Creates a new instance. 34 | * 35 | * @param streamId the stream identifier of this frame 36 | */ 37 | public DefaultHttpDataFrame(int streamId) { 38 | this(streamId, Unpooled.buffer(0)); 39 | } 40 | 41 | /** 42 | * Creates a new instance. 43 | * 44 | * @param streamId the stream identifier of this frame 45 | * @param data the payload of the frame. Can not exceed {@link HttpCodecUtil#HTTP_MAX_LENGTH} 46 | */ 47 | public DefaultHttpDataFrame(int streamId, ByteBuf data) { 48 | super(streamId); 49 | if (data == null) { 50 | throw new NullPointerException("data"); 51 | } 52 | this.data = validate(data); 53 | } 54 | 55 | private static ByteBuf validate(ByteBuf data) { 56 | if (data.readableBytes() > HttpCodecUtil.HTTP_MAX_LENGTH) { 57 | throw new IllegalArgumentException("data payload cannot exceed " 58 | + HttpCodecUtil.HTTP_MAX_LENGTH + " bytes"); 59 | } 60 | return data; 61 | } 62 | 63 | @Override 64 | public boolean isLast() { 65 | return last; 66 | } 67 | 68 | @Override 69 | public HttpDataFrame setLast(boolean last) { 70 | this.last = last; 71 | return this; 72 | } 73 | 74 | @Override 75 | public HttpDataFrame setStreamId(int streamId) { 76 | super.setStreamId(streamId); 77 | return this; 78 | } 79 | 80 | @Override 81 | public ByteBuf content() { 82 | if (data.refCnt() <= 0) { 83 | throw new IllegalReferenceCountException(data.refCnt()); 84 | } 85 | return data; 86 | } 87 | 88 | @Override 89 | public HttpDataFrame copy() { 90 | HttpDataFrame frame = new DefaultHttpDataFrame(getStreamId(), content().copy()); 91 | frame.setLast(isLast()); 92 | return frame; 93 | } 94 | 95 | @Override 96 | public HttpDataFrame duplicate() { 97 | HttpDataFrame frame = new DefaultHttpDataFrame(getStreamId(), content().duplicate()); 98 | frame.setLast(isLast()); 99 | return frame; 100 | } 101 | 102 | @Override 103 | public int refCnt() { 104 | return data.refCnt(); 105 | } 106 | 107 | @Override 108 | public HttpDataFrame retain() { 109 | data.retain(); 110 | return this; 111 | } 112 | 113 | @Override 114 | public HttpDataFrame retain(int increment) { 115 | data.retain(increment); 116 | return this; 117 | } 118 | 119 | @Override 120 | public boolean release() { 121 | return data.release(); 122 | } 123 | 124 | @Override 125 | public boolean release(int decrement) { 126 | return data.release(decrement); 127 | } 128 | 129 | @Override 130 | public String toString() { 131 | StringBuilder buf = new StringBuilder(); 132 | buf.append(StringUtil.simpleClassName(this)); 133 | buf.append("(last: "); 134 | buf.append(isLast()); 135 | buf.append(')'); 136 | buf.append(StringUtil.NEWLINE); 137 | buf.append("--> Stream-ID = "); 138 | buf.append(getStreamId()); 139 | buf.append(StringUtil.NEWLINE); 140 | buf.append("--> Size = "); 141 | if (refCnt() == 0) { 142 | buf.append("(freed)"); 143 | } else { 144 | buf.append(content().readableBytes()); 145 | } 146 | return buf.toString(); 147 | } 148 | 149 | @Override 150 | public ByteBufHolder touch() { 151 | data.touch(); 152 | return this; 153 | } 154 | 155 | @Override 156 | public ByteBufHolder touch(Object o) { 157 | data.touch(o); 158 | return this; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/Pipe.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import java.util.LinkedList; 19 | import java.util.Objects; 20 | import java.util.Queue; 21 | import java.util.concurrent.ConcurrentLinkedQueue; 22 | 23 | import io.netty.channel.ChannelException; 24 | import io.netty.util.concurrent.Future; 25 | import io.netty.util.concurrent.ImmediateEventExecutor; 26 | import io.netty.util.concurrent.Promise; 27 | 28 | /** 29 | * Implements a stream that pipes objects between its two ends. 30 | * Futures are used to communicate when messages are sent and received. 31 | * 32 | * @param the type of objects to send along the pipe 33 | */ 34 | public class Pipe { 35 | 36 | private static final ChannelException PIPE_CLOSED = new ChannelException("pipe closed"); 37 | 38 | private static final Future SENT_FUTURE = 39 | ImmediateEventExecutor.INSTANCE.newSucceededFuture(null); 40 | private static final Future CLOSED_FUTURE = 41 | ImmediateEventExecutor.INSTANCE.newFailedFuture(PIPE_CLOSED); 42 | 43 | private Queue sendQueue = new LinkedList(); 44 | private Queue> receiveQueue = new ConcurrentLinkedQueue>(); 45 | 46 | private boolean closed; 47 | 48 | /** 49 | * Holds a message and an associated future. 50 | */ 51 | private final class Node { 52 | public T message; 53 | public Promise promise; 54 | 55 | Node(T message, Promise promise) { 56 | this.message = message; 57 | this.promise = promise; 58 | } 59 | } 60 | 61 | /** 62 | * Creates a new pipe and uses instances of {@link ImmediateEventExecutor} 63 | * for the send and receive executors. 64 | * 65 | * @see ImmediateEventExecutor#INSTANCE 66 | */ 67 | public Pipe() { 68 | super(); 69 | } 70 | 71 | /** 72 | * Sends a message to this pipe. Returns a {@link Future} that is completed 73 | * when the message is received. 74 | *

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

76 | * 77 | * @param message the message to send to the pipe 78 | * @return a {@link Future} that is satisfied when the message is received, 79 | * or a failed future if the pipe is closed. 80 | * @throws NullPointerException if the message is {@code null}. 81 | * @throws IllegalStateException if the message could not be added to the queue for some reason. 82 | * @see #receive() 83 | */ 84 | public Future send(T message) { 85 | Objects.requireNonNull(message, "msg"); 86 | 87 | Promise receivePromise; 88 | 89 | synchronized (this) { 90 | if (closed) { 91 | return CLOSED_FUTURE; 92 | } 93 | 94 | receivePromise = receiveQueue.poll(); 95 | if (receivePromise == null) { 96 | Promise sendPromise = ImmediateEventExecutor.INSTANCE.newPromise(); 97 | sendQueue.add(new Node(message, sendPromise)); 98 | return sendPromise; 99 | } 100 | } 101 | 102 | receivePromise.setSuccess(message); 103 | return SENT_FUTURE; 104 | } 105 | 106 | /** 107 | * Receives a message from this pipe. 108 | *

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

110 | */ 111 | public Future receive() { 112 | Node node; 113 | 114 | synchronized (this) { 115 | node = sendQueue.poll(); 116 | if (node == null) { 117 | if (closed) { 118 | return ImmediateEventExecutor.INSTANCE.newFailedFuture(PIPE_CLOSED); 119 | } 120 | 121 | Promise promise = ImmediateEventExecutor.INSTANCE.newPromise(); 122 | receiveQueue.add(promise); 123 | return promise; 124 | } 125 | } 126 | 127 | node.promise.setSuccess(null); 128 | return ImmediateEventExecutor.INSTANCE.newSucceededFuture(node.message); 129 | } 130 | 131 | /** 132 | * Closes this pipe. This fails all outstanding receive futures. 133 | * This does nothing if the pipe is already closed. 134 | */ 135 | public void close() { 136 | synchronized (this) { 137 | if (closed) { 138 | return; 139 | } 140 | closed = true; 141 | } 142 | 143 | while (!receiveQueue.isEmpty()) { 144 | receiveQueue.poll().setFailure(PIPE_CLOSED); 145 | } 146 | } 147 | 148 | /** 149 | * Checks if this pipe is closed. 150 | * 151 | * @return whether this pipe is closed. 152 | */ 153 | public synchronized boolean isClosed() { 154 | return closed; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpHeaderBlockEncoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import java.io.IOException; 19 | import java.nio.charset.StandardCharsets; 20 | import java.util.List; 21 | import java.util.Locale; 22 | 23 | import io.netty.buffer.ByteBuf; 24 | import io.netty.buffer.ByteBufOutputStream; 25 | import io.netty.buffer.Unpooled; 26 | import io.netty.channel.ChannelHandlerContext; 27 | 28 | import com.twitter.hpack.Encoder; 29 | 30 | public class HttpHeaderBlockEncoder { 31 | 32 | private static final byte[] COOKIE = {'c', 'o', 'o', 'k', 'i', 'e'}; 33 | private static final byte[] EMPTY = {}; 34 | 35 | private int encoderMaxHeaderTableSize; 36 | private int decoderMaxHeaderTableSize; 37 | private int maxHeaderTableSize; 38 | private Encoder encoder; 39 | 40 | /** 41 | * Create a new instance. 42 | */ 43 | public HttpHeaderBlockEncoder(int maxHeaderTableSize) { 44 | encoderMaxHeaderTableSize = maxHeaderTableSize; 45 | decoderMaxHeaderTableSize = maxHeaderTableSize; 46 | this.maxHeaderTableSize = maxHeaderTableSize; 47 | encoder = new Encoder(maxHeaderTableSize); 48 | } 49 | 50 | /** 51 | * Set the maximum header table size allowed by the encoder. 52 | * 53 | * @param encoderMaxHeaderTableSize the maximum header table size allowed by the encoder 54 | */ 55 | public void setEncoderMaxHeaderTableSize(int encoderMaxHeaderTableSize) { 56 | this.encoderMaxHeaderTableSize = encoderMaxHeaderTableSize; 57 | if (encoderMaxHeaderTableSize < maxHeaderTableSize) { 58 | maxHeaderTableSize = encoderMaxHeaderTableSize; 59 | } 60 | } 61 | 62 | /** 63 | * Set the maximum header table size allowed by the peer's encoder. 64 | * This is the value of SETTINGS_HEADER_TABLE_SIZE received from the peer. 65 | * 66 | * @param decoderMaxHeaderTableSize the maximum header table size allowed by the decoder 67 | */ 68 | public void setDecoderMaxHeaderTableSize(int decoderMaxHeaderTableSize) { 69 | this.decoderMaxHeaderTableSize = decoderMaxHeaderTableSize; 70 | if (decoderMaxHeaderTableSize < maxHeaderTableSize) { 71 | maxHeaderTableSize = decoderMaxHeaderTableSize; 72 | } 73 | } 74 | 75 | /** 76 | * Encode the header block frame. 77 | */ 78 | public ByteBuf encode(ChannelHandlerContext ctx, HttpHeaderBlockFrame frame) throws IOException { 79 | ByteBuf buf = Unpooled.buffer(); 80 | ByteBufOutputStream out = new ByteBufOutputStream(buf); 81 | 82 | // The current allowable max header table size is the 83 | // minimum of the encoder and decoder allowable sizes 84 | int allowableHeaderTableSize = Math.min(encoderMaxHeaderTableSize, decoderMaxHeaderTableSize); 85 | 86 | // maxHeaderTableSize will hold the smallest size seen the 87 | // last call to encode. This might be smaller than the 88 | // current allowable max header table size 89 | if (maxHeaderTableSize < allowableHeaderTableSize) { 90 | encoder.setMaxHeaderTableSize(out, maxHeaderTableSize); 91 | } 92 | 93 | // Check if the current allowable size is equal to the encoder's 94 | // capacity and set the new size if necessary 95 | if (allowableHeaderTableSize != encoder.getMaxHeaderTableSize()) { 96 | encoder.setMaxHeaderTableSize(out, allowableHeaderTableSize); 97 | } 98 | 99 | // Store the current allowable size for the next call 100 | maxHeaderTableSize = allowableHeaderTableSize; 101 | 102 | // Now we can encode headers 103 | for (String name : frame.headers().names()) { 104 | if ("cookie".equalsIgnoreCase(name)) { 105 | // Sec. 8.1.3.4. Cookie Header Field 106 | for (String value : frame.headers().getAll(name)) { 107 | for (String crumb : value.split(";")) { 108 | byte[] valueBytes = crumb.trim().getBytes(StandardCharsets.UTF_8); 109 | encoder.encodeHeader(out, COOKIE, valueBytes, true); 110 | } 111 | } 112 | } else { 113 | byte[] nameBytes = name.toLowerCase(Locale.ENGLISH).getBytes(StandardCharsets.UTF_8); 114 | // Sec. 8.1.3.3. Header Field Ordering 115 | List values = frame.headers().getAll(name); 116 | if (values.size() == 0) { 117 | encoder.encodeHeader(out, nameBytes, EMPTY, false); 118 | } else { 119 | for (String value : values) { 120 | byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8); 121 | encoder.encodeHeader(out, nameBytes, valueBytes, false); 122 | } 123 | } 124 | } 125 | } 126 | 127 | return buf; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpHeaderBlockDecoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import java.io.IOException; 19 | import java.nio.charset.StandardCharsets; 20 | 21 | import io.netty.buffer.ByteBuf; 22 | import io.netty.buffer.ByteBufInputStream; 23 | import io.netty.buffer.Unpooled; 24 | import io.netty.handler.codec.http.HttpHeaders; 25 | 26 | import com.twitter.hpack.Decoder; 27 | import com.twitter.hpack.HeaderListener; 28 | 29 | final class HttpHeaderBlockDecoder { 30 | 31 | private static final HeaderListener NULL_HEADER_LISTENER = new NullHeaderListener(); 32 | 33 | private Decoder decoder; 34 | private ByteBuf cumulation; 35 | 36 | public HttpHeaderBlockDecoder(int maxHeaderSize, int maxHeaderTableSize) { 37 | decoder = new Decoder(maxHeaderSize, maxHeaderTableSize); 38 | } 39 | 40 | /** 41 | * Set the maximum header table size allowed by the decoder. 42 | * This is the value of SETTINGS_HEADER_TABLE_SIZE sent to the peer. 43 | * 44 | * @param maxHeaderTableSize the maximum header table size allowed by the decoder 45 | */ 46 | public void setMaxHeaderTableSize(int maxHeaderTableSize) { 47 | decoder.setMaxHeaderTableSize(maxHeaderTableSize); 48 | } 49 | 50 | public void decode(ByteBuf headerBlock, final HttpHeaderBlockFrame frame) throws IOException { 51 | HeaderListener headerListener = NULL_HEADER_LISTENER; 52 | if (frame != null) { 53 | headerListener = new HeaderListenerImpl(frame.headers()); 54 | } 55 | 56 | if (cumulation == null) { 57 | decoder.decode(new ByteBufInputStream(headerBlock), headerListener); 58 | if (headerBlock.isReadable()) { 59 | cumulation = Unpooled.buffer(headerBlock.readableBytes()); 60 | cumulation.writeBytes(headerBlock); 61 | } 62 | } else { 63 | cumulation.writeBytes(headerBlock); 64 | decoder.decode(new ByteBufInputStream(cumulation), headerListener); 65 | if (cumulation.isReadable()) { 66 | cumulation.discardReadBytes(); 67 | } else { 68 | cumulation.release(); 69 | cumulation = null; 70 | } 71 | } 72 | } 73 | 74 | public void endHeaderBlock(final HttpHeaderBlockFrame frame) { 75 | if (cumulation != null) { 76 | if (cumulation.isReadable() && frame != null) { 77 | frame.setInvalid(); 78 | } 79 | cumulation.release(); 80 | cumulation = null; 81 | } 82 | 83 | boolean truncated = decoder.endHeaderBlock(); 84 | 85 | if (truncated && frame != null) { 86 | frame.setTruncated(); 87 | } 88 | } 89 | 90 | private static final class NullHeaderListener implements HeaderListener { 91 | @Override 92 | public void addHeader(byte[] name, byte[] value, boolean sensitive) { 93 | // No Op 94 | } 95 | } 96 | 97 | private static final class HeaderListenerImpl implements HeaderListener { 98 | 99 | private final HttpHeaders headers; 100 | 101 | HeaderListenerImpl(HttpHeaders headers) { 102 | this.headers = headers; 103 | } 104 | 105 | @Override 106 | public void addHeader(byte[] name, byte[] value, boolean sensitive) { 107 | String nameStr = new String(name, StandardCharsets.UTF_8); 108 | 109 | // check for empty value 110 | if (value.length == 0) { 111 | addHeader(nameStr, ""); 112 | return; 113 | } 114 | 115 | // Sec. 8.1.3.3. Header Field Ordering 116 | int index = 0; 117 | int offset = 0; 118 | while (index < value.length) { 119 | while (index < value.length && value[index] != (byte) 0) { 120 | index++; 121 | } 122 | if (index - offset == 0) { 123 | addHeader(nameStr, ""); 124 | } else { 125 | String valueStr = new String(value, offset, index - offset, StandardCharsets.UTF_8); 126 | addHeader(nameStr, valueStr); 127 | } 128 | index++; 129 | offset = index; 130 | } 131 | } 132 | 133 | private void addHeader(String name, String value) { 134 | boolean crumb = "cookie".equalsIgnoreCase(name); 135 | if (value.length() == 0) { 136 | if (crumb || headers.contains(name)) { 137 | return; 138 | } 139 | } 140 | if (crumb) { 141 | // Sec. 8.1.3.4. Cookie Header Field 142 | String cookie = headers.get(name); 143 | if (cookie == null) { 144 | headers.set(name, value); 145 | } else { 146 | headers.set(name, cookie + "; " + value); 147 | } 148 | } else { 149 | headers.add(name, value); 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpErrorCode.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | public class HttpErrorCode implements Comparable { 19 | 20 | /** 21 | * 0 No Error 22 | */ 23 | public static final HttpErrorCode NO_ERROR = 24 | new HttpErrorCode(0, "NO_ERROR"); 25 | 26 | /** 27 | * 1 Protocol Error 28 | */ 29 | public static final HttpErrorCode PROTOCOL_ERROR = 30 | new HttpErrorCode(1, "PROTOCOL_ERROR"); 31 | 32 | /** 33 | * 2 Internal Error 34 | */ 35 | public static final HttpErrorCode INTERNAL_ERROR = 36 | new HttpErrorCode(2, "INTERNAL_ERROR"); 37 | 38 | /** 39 | * 3 Flow Control Error 40 | */ 41 | public static final HttpErrorCode FLOW_CONTROL_ERROR = 42 | new HttpErrorCode(3, "FLOW_CONTROL_ERROR"); 43 | 44 | /** 45 | * 4 Settings Timeout 46 | */ 47 | public static final HttpErrorCode SETTINGS_TIMEOUT = 48 | new HttpErrorCode(4, "SETTINGS_TIMEOUT"); 49 | 50 | /** 51 | * 5 Stream Closed 52 | */ 53 | public static final HttpErrorCode STREAM_CLOSED = 54 | new HttpErrorCode(5, "STREAM_CLOSED"); 55 | 56 | /** 57 | * 6 Frame Size Error 58 | */ 59 | public static final HttpErrorCode FRAME_SIZE_ERROR = 60 | new HttpErrorCode(6, "FRAME_SIZE_ERROR"); 61 | 62 | /** 63 | * 7 Refused Stream 64 | */ 65 | public static final HttpErrorCode REFUSED_STREAM = 66 | new HttpErrorCode(7, "REFUSED_STREAM"); 67 | 68 | /** 69 | * 8 Cancel 70 | */ 71 | public static final HttpErrorCode CANCEL = 72 | new HttpErrorCode(8, "CANCEL"); 73 | 74 | /** 75 | * 9 Compression Error 76 | */ 77 | public static final HttpErrorCode COMPRESSION_ERROR = 78 | new HttpErrorCode(9, "COMPRESSION_ERROR"); 79 | 80 | /** 81 | * 10 Connect Error 82 | */ 83 | public static final HttpErrorCode CONNECT_ERROR = 84 | new HttpErrorCode(10, "CONNECT_ERROR"); 85 | 86 | /** 87 | * 11 Enhance Your Calm (420) 88 | */ 89 | public static final HttpErrorCode ENHANCE_YOUR_CALM = 90 | new HttpErrorCode(420, "ENHANCE_YOUR_CALM"); 91 | 92 | /** 93 | * 12 Inadequate Security 94 | */ 95 | public static final HttpErrorCode INADEQUATE_SECURITY = 96 | new HttpErrorCode(12, "INADEQUATE_SECURITY"); 97 | 98 | /** 99 | * Returns the {@link HttpErrorCode} represented by the specified code. 100 | * If the specified code is a defined HTTP error code, a cached instance 101 | * will be returned. Otherwise, a new instance will be returned. 102 | */ 103 | public static HttpErrorCode valueOf(int code) { 104 | switch (code) { 105 | case 0: 106 | return NO_ERROR; 107 | case 1: 108 | return PROTOCOL_ERROR; 109 | case 2: 110 | return INTERNAL_ERROR; 111 | case 3: 112 | return FLOW_CONTROL_ERROR; 113 | case 4: 114 | return SETTINGS_TIMEOUT; 115 | case 5: 116 | return STREAM_CLOSED; 117 | case 6: 118 | return FRAME_SIZE_ERROR; 119 | case 7: 120 | return REFUSED_STREAM; 121 | case 8: 122 | return CANCEL; 123 | case 9: 124 | return COMPRESSION_ERROR; 125 | case 10: 126 | return CONNECT_ERROR; 127 | case 11: 128 | return ENHANCE_YOUR_CALM; 129 | case 12: 130 | return INADEQUATE_SECURITY; 131 | case 420: 132 | return ENHANCE_YOUR_CALM; 133 | default: 134 | return new HttpErrorCode(code, "UNKNOWN (" + code + ')'); 135 | } 136 | } 137 | 138 | private final int code; 139 | 140 | private final String statusPhrase; 141 | 142 | /** 143 | * Creates a new instance with the specified {@code code} and its 144 | * {@code statusPhrase}. 145 | */ 146 | public HttpErrorCode(int code, String statusPhrase) { 147 | if (statusPhrase == null) { 148 | throw new NullPointerException("statusPhrase"); 149 | } 150 | 151 | this.code = code; 152 | this.statusPhrase = statusPhrase; 153 | } 154 | 155 | /** 156 | * Returns the code of this status. 157 | */ 158 | public int getCode() { 159 | return code; 160 | } 161 | 162 | /** 163 | * Returns the status phrase of this status. 164 | */ 165 | public String getStatusPhrase() { 166 | return statusPhrase; 167 | } 168 | 169 | @Override 170 | public int hashCode() { 171 | return getCode(); 172 | } 173 | 174 | @Override 175 | public boolean equals(Object o) { 176 | if (!(o instanceof HttpErrorCode)) { 177 | return false; 178 | } 179 | 180 | return getCode() == ((HttpErrorCode) o).getCode(); 181 | } 182 | 183 | @Override 184 | public String toString() { 185 | return getStatusPhrase(); 186 | } 187 | 188 | @Override 189 | public int compareTo(HttpErrorCode o) { 190 | return getCode() - o.getCode(); 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/test/java/com/twitter/http2/HttpHeaderCompressionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import java.io.IOException; 19 | import java.nio.charset.StandardCharsets; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | import java.util.concurrent.atomic.AtomicReference; 23 | 24 | import io.netty.bootstrap.Bootstrap; 25 | import io.netty.bootstrap.ServerBootstrap; 26 | import io.netty.buffer.Unpooled; 27 | import io.netty.channel.Channel; 28 | import io.netty.channel.ChannelFuture; 29 | import io.netty.channel.ChannelHandlerContext; 30 | import io.netty.channel.ChannelInboundHandlerAdapter; 31 | import io.netty.channel.ChannelInitializer; 32 | import io.netty.channel.local.LocalAddress; 33 | import io.netty.channel.local.LocalChannel; 34 | import io.netty.channel.local.LocalEventLoopGroup; 35 | import io.netty.channel.local.LocalServerChannel; 36 | import io.netty.handler.codec.http.HttpHeaders; 37 | import org.junit.Test; 38 | 39 | import static org.junit.Assert.assertTrue; 40 | 41 | public class HttpHeaderCompressionTest { 42 | 43 | @Test 44 | public void testHttpHeadersFrame() throws Throwable { 45 | HttpHeadersFrame httpHeadersFrame = new DefaultHttpHeadersFrame(1); 46 | httpHeadersFrame.headers().add("name", "value"); 47 | testHeaderEcho(httpHeadersFrame); 48 | } 49 | 50 | private void testHeaderEcho(HttpHeaderBlockFrame frame) throws Throwable { 51 | final EchoHandler sh = new EchoHandler(); 52 | final TestHandler ch = new TestHandler(frame); 53 | 54 | ServerBootstrap sb = new ServerBootstrap() 55 | .group(new LocalEventLoopGroup()) 56 | .channel(LocalServerChannel.class) 57 | .childHandler(new ChannelInitializer() { 58 | @Override 59 | public void initChannel(LocalChannel channel) throws Exception { 60 | channel.pipeline().addLast(new HttpConnectionHandler(true), sh); 61 | } 62 | }); 63 | Bootstrap cb = new Bootstrap() 64 | .group(new LocalEventLoopGroup()) 65 | .channel(LocalChannel.class) 66 | .handler(new ChannelInitializer() { 67 | @Override 68 | public void initChannel(LocalChannel channel) throws Exception { 69 | channel.pipeline().addLast(new HttpConnectionHandler(false), ch); 70 | } 71 | }); 72 | 73 | LocalAddress localAddress = new LocalAddress("HttpHeaderCompressionTest"); 74 | Channel sc = sb.bind(localAddress).sync().channel(); 75 | ChannelFuture ccf = cb.connect(localAddress); 76 | assertTrue(ccf.awaitUninterruptibly().isSuccess()); 77 | 78 | while (!ch.success) { 79 | if (sh.exception.get() != null) { 80 | break; 81 | } 82 | if (ch.exception.get() != null) { 83 | break; 84 | } 85 | 86 | try { 87 | Thread.sleep(1); 88 | } catch (InterruptedException e) { 89 | // Ignore. 90 | } 91 | } 92 | 93 | sc.close().awaitUninterruptibly(); 94 | cb.group().shutdownGracefully(); 95 | sb.group().shutdownGracefully(); 96 | 97 | if (sh.exception.get() != null && !(sh.exception.get() instanceof IOException)) { 98 | throw sh.exception.get(); 99 | } 100 | if (ch.exception.get() != null && !(ch.exception.get() instanceof IOException)) { 101 | throw ch.exception.get(); 102 | } 103 | if (sh.exception.get() != null) { 104 | throw sh.exception.get(); 105 | } 106 | if (ch.exception.get() != null) { 107 | throw ch.exception.get(); 108 | } 109 | } 110 | 111 | private static class EchoHandler extends ChannelInboundHandlerAdapter { 112 | public final AtomicReference exception = new AtomicReference(); 113 | 114 | @Override 115 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 116 | ctx.writeAndFlush(msg); 117 | } 118 | 119 | @Override 120 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 121 | if (exception.compareAndSet(null, cause)) { 122 | ctx.close(); 123 | } 124 | } 125 | } 126 | 127 | private static class TestHandler extends ChannelInboundHandlerAdapter { 128 | private static final byte[] CONNECTION_HEADER = 129 | "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(StandardCharsets.US_ASCII); 130 | 131 | public final AtomicReference exception = new AtomicReference(); 132 | public final HttpHeaderBlockFrame frame; 133 | 134 | public volatile boolean success; 135 | 136 | public TestHandler(HttpHeaderBlockFrame frame) { 137 | this.frame = frame; 138 | } 139 | 140 | @Override 141 | public void channelActive(ChannelHandlerContext ctx) throws Exception { 142 | ctx.write(Unpooled.wrappedBuffer(CONNECTION_HEADER)); 143 | ctx.writeAndFlush(frame); 144 | } 145 | 146 | @Override 147 | public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 148 | assertTrue(msg instanceof HttpHeaderBlockFrame); 149 | HttpHeaders actual = ((HttpHeaderBlockFrame) msg).headers(); 150 | HttpHeaders expected = frame.headers(); 151 | for (String name : expected.names()) { 152 | List expectedValues = new ArrayList(expected.getAll(name)); 153 | List actualValues = new ArrayList(actual.getAll(name)); 154 | assertTrue(actualValues.containsAll(expectedValues)); 155 | actualValues.removeAll(expectedValues); 156 | assertTrue(actualValues.isEmpty()); 157 | actual.remove(name); 158 | } 159 | assertTrue(actual.isEmpty()); 160 | success = true; 161 | } 162 | 163 | @Override 164 | public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 165 | if (exception.compareAndSet(null, cause)) { 166 | ctx.close(); 167 | } 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | com.twitter 5 | netty-http2 6 | 0.17.12-SNAPSHOT 7 | HTTP/2 8 | http://github.com/twitter/netty-http2 9 | HTTP/2 for Netty 10 | 11 | 12 | scm:git:git@github.com:twitter/netty-http2.git 13 | scm:git:git@github.com:twitter/netty-http2.git 14 | scm:git:git@github.com:twitter/netty-http2.git 15 | 16 | 17 | 18 | 19 | The Apache Software License, Version 2.0 20 | http://www.apache.org/licenses/LICENSE-2.0.txt 21 | 22 | 23 | 24 | 25 | 26 | Jeff Pinner 27 | jpinner@twitter.com 28 | 29 | 30 | 31 | 32 | 1.6 33 | 1.6 34 | UTF-8 35 | UTF-8 36 | 37 | 38 | 39 | 40 | sonatype-nexus-snapshots 41 | Sonatype OSS 42 | https://oss.sonatype.org/content/repositories/snapshots 43 | 44 | 45 | sonatype-nexus-staging 46 | Nexus Release Repository 47 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 48 | 49 | 50 | 51 | 52 | 53 | com.twitter 54 | hpack 55 | 1.0.2 56 | 57 | 58 | io.netty 59 | netty-codec-http 60 | 4.1.0.CR4 61 | 62 | 63 | 64 | junit 65 | junit 66 | 4.12 67 | test 68 | 69 | 70 | org.mockito 71 | mockito-core 72 | 1.9.5 73 | test 74 | 75 | 76 | 77 | 78 | 79 | sonatype-nexus-snapshots 80 | https://oss.sonatype.org/content/repositories/snapshots 81 | 82 | false 83 | 84 | 85 | true 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | org.apache.maven.plugins 95 | maven-release-plugin 96 | 2.1 97 | 98 | forked-path 99 | false 100 | -Psonatype-oss-release 101 | 102 | 103 | 104 | 105 | 106 | 107 | org.apache.maven.plugins 108 | maven-compiler-plugin 109 | 2.5.1 110 | 111 | 1.6 112 | 1.6 113 | 114 | 115 | 116 | org.apache.maven.plugins 117 | maven-surefire-plugin 118 | 2.12 119 | 120 | -Xmx1024m 121 | false 122 | 123 | **/Test*.java 124 | **/*Test.java 125 | **/*Spec.java 126 | 127 | 128 | 129 | 130 | org.codehaus.mojo 131 | cobertura-maven-plugin 132 | 2.5.2 133 | 134 | xml 135 | 256m 136 | true 137 | 138 | 139 | 140 | org.eluder.coveralls 141 | coveralls-maven-plugin 142 | 2.2.0 143 | 144 | 145 | 146 | 147 | 148 | sonatype-oss-release 149 | 150 | 151 | 152 | org.apache.maven.plugins 153 | maven-source-plugin 154 | 2.1.2 155 | 156 | 157 | attach-sources 158 | 159 | jar-no-fork 160 | 161 | 162 | 163 | 164 | 165 | org.apache.maven.plugins 166 | maven-javadoc-plugin 167 | 2.7 168 | 169 | 170 | attach-javadocs 171 | 172 | jar 173 | 174 | 175 | 176 | 177 | 178 | org.apache.maven.plugins 179 | maven-gpg-plugin 180 | 1.1 181 | 182 | 183 | sign-artifacts 184 | verify 185 | 186 | sign 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpFrameEncoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import java.util.Set; 19 | 20 | import io.netty.buffer.ByteBuf; 21 | import io.netty.buffer.Unpooled; 22 | 23 | import static com.twitter.http2.HttpCodecUtil.HTTP_CONTINUATION_FRAME; 24 | import static com.twitter.http2.HttpCodecUtil.HTTP_DATA_FRAME; 25 | import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_DEPENDENCY; 26 | import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_WEIGHT; 27 | import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_ACK; 28 | import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_END_HEADERS; 29 | import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_END_STREAM; 30 | import static com.twitter.http2.HttpCodecUtil.HTTP_FLAG_PRIORITY; 31 | import static com.twitter.http2.HttpCodecUtil.HTTP_FRAME_HEADER_SIZE; 32 | import static com.twitter.http2.HttpCodecUtil.HTTP_GOAWAY_FRAME; 33 | import static com.twitter.http2.HttpCodecUtil.HTTP_HEADERS_FRAME; 34 | import static com.twitter.http2.HttpCodecUtil.HTTP_MAX_LENGTH; 35 | import static com.twitter.http2.HttpCodecUtil.HTTP_PING_FRAME; 36 | import static com.twitter.http2.HttpCodecUtil.HTTP_PRIORITY_FRAME; 37 | import static com.twitter.http2.HttpCodecUtil.HTTP_PUSH_PROMISE_FRAME; 38 | import static com.twitter.http2.HttpCodecUtil.HTTP_RST_STREAM_FRAME; 39 | import static com.twitter.http2.HttpCodecUtil.HTTP_SETTINGS_FRAME; 40 | import static com.twitter.http2.HttpCodecUtil.HTTP_WINDOW_UPDATE_FRAME; 41 | 42 | /** 43 | * Encodes an HTTP/2 Frame into a {@link ByteBuf}. 44 | */ 45 | public class HttpFrameEncoder { 46 | 47 | /** 48 | * Encode an HTTP/2 DATA Frame 49 | */ 50 | public ByteBuf encodeDataFrame(int streamId, boolean endStream, ByteBuf data) { 51 | byte flags = endStream ? HTTP_FLAG_END_STREAM : 0; 52 | ByteBuf header = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE); 53 | writeFrameHeader(header, data.readableBytes(), HTTP_DATA_FRAME, flags, streamId); 54 | return Unpooled.wrappedBuffer(header, data); 55 | } 56 | 57 | /** 58 | * Encode an HTTP/2 HEADERS Frame 59 | */ 60 | public ByteBuf encodeHeadersFrame( 61 | int streamId, 62 | boolean endStream, 63 | boolean exclusive, 64 | int dependency, 65 | int weight, 66 | ByteBuf headerBlock 67 | ) { 68 | byte flags = endStream ? HTTP_FLAG_END_STREAM : 0; 69 | boolean hasPriority = exclusive 70 | || dependency != HTTP_DEFAULT_DEPENDENCY || weight != HTTP_DEFAULT_WEIGHT; 71 | if (hasPriority) { 72 | flags |= HTTP_FLAG_PRIORITY; 73 | } 74 | int maxLength = hasPriority ? HTTP_MAX_LENGTH - 5 : HTTP_MAX_LENGTH; 75 | boolean needsContinuations = headerBlock.readableBytes() > maxLength; 76 | if (!needsContinuations) { 77 | flags |= HTTP_FLAG_END_HEADERS; 78 | } 79 | int length = needsContinuations ? maxLength : headerBlock.readableBytes(); 80 | if (hasPriority) { 81 | length += 5; 82 | } 83 | int frameLength = hasPriority ? HTTP_FRAME_HEADER_SIZE + 5 : HTTP_FRAME_HEADER_SIZE; 84 | ByteBuf header = Unpooled.buffer(frameLength); 85 | writeFrameHeader(header, length, HTTP_HEADERS_FRAME, flags, streamId); 86 | if (hasPriority) { 87 | if (exclusive) { 88 | header.writeInt(dependency | 0x80000000); 89 | } else { 90 | header.writeInt(dependency); 91 | } 92 | header.writeByte(weight - 1); 93 | length -= 5; 94 | } 95 | ByteBuf frame = Unpooled.wrappedBuffer(header, headerBlock.readSlice(length)); 96 | if (needsContinuations) { 97 | while (headerBlock.readableBytes() > HTTP_MAX_LENGTH) { 98 | header = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE); 99 | writeFrameHeader(header, HTTP_MAX_LENGTH, HTTP_CONTINUATION_FRAME, (byte) 0, streamId); 100 | frame = Unpooled.wrappedBuffer(frame, header, headerBlock.readSlice(HTTP_MAX_LENGTH)); 101 | } 102 | header = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE); 103 | writeFrameHeader( 104 | header, 105 | headerBlock.readableBytes(), 106 | HTTP_CONTINUATION_FRAME, 107 | HTTP_FLAG_END_HEADERS, 108 | streamId 109 | ); 110 | frame = Unpooled.wrappedBuffer(frame, header, headerBlock); 111 | } 112 | return frame; 113 | } 114 | 115 | /** 116 | * Encode an HTTP/2 PRIORITY Frame 117 | */ 118 | public ByteBuf encodePriorityFrame(int streamId, boolean exclusive, int dependency, int weight) { 119 | int length = 5; 120 | byte flags = 0; 121 | ByteBuf frame = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + length); 122 | writeFrameHeader(frame, length, HTTP_PRIORITY_FRAME, flags, streamId); 123 | if (exclusive) { 124 | frame.writeInt(dependency | 0x80000000); 125 | } else { 126 | frame.writeInt(dependency); 127 | } 128 | frame.writeByte(weight - 1); 129 | return frame; 130 | } 131 | 132 | /** 133 | * Encode an HTTP/2 RST_STREAM Frame 134 | */ 135 | public ByteBuf encodeRstStreamFrame(int streamId, int errorCode) { 136 | int length = 4; 137 | byte flags = 0; 138 | ByteBuf frame = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + length); 139 | writeFrameHeader(frame, length, HTTP_RST_STREAM_FRAME, flags, streamId); 140 | frame.writeInt(errorCode); 141 | return frame; 142 | } 143 | 144 | /** 145 | * Encode an HTTP/2 SETTINGS Frame 146 | */ 147 | public ByteBuf encodeSettingsFrame(HttpSettingsFrame httpSettingsFrame) { 148 | Set ids = httpSettingsFrame.getIds(); 149 | int length = ids.size() * 6; 150 | byte flags = httpSettingsFrame.isAck() ? HTTP_FLAG_ACK : 0; 151 | int streamId = 0; 152 | ByteBuf frame = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + length); 153 | writeFrameHeader(frame, length, HTTP_SETTINGS_FRAME, flags, streamId); 154 | for (int id : ids) { 155 | frame.writeShort(id); 156 | frame.writeInt(httpSettingsFrame.getValue(id)); 157 | } 158 | return frame; 159 | } 160 | 161 | /** 162 | * Encode an HTTP/2 PUSH_PROMISE Frame 163 | */ 164 | public ByteBuf encodePushPromiseFrame(int streamId, int promisedStreamId, ByteBuf headerBlock) { 165 | boolean needsContinuations = headerBlock.readableBytes() > HTTP_MAX_LENGTH - 4; 166 | int length = needsContinuations ? HTTP_MAX_LENGTH - 4 : headerBlock.readableBytes(); 167 | byte flags = needsContinuations ? 0 : HTTP_FLAG_END_HEADERS; 168 | ByteBuf header = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + 4); 169 | writeFrameHeader(header, length + 4, HTTP_PUSH_PROMISE_FRAME, flags, streamId); 170 | header.writeInt(promisedStreamId); 171 | ByteBuf frame = Unpooled.wrappedBuffer(header, headerBlock.readSlice(length)); 172 | if (needsContinuations) { 173 | while (headerBlock.readableBytes() > HTTP_MAX_LENGTH) { 174 | header = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE); 175 | writeFrameHeader(header, HTTP_MAX_LENGTH, HTTP_CONTINUATION_FRAME, (byte) 0, streamId); 176 | frame = Unpooled.wrappedBuffer(frame, header, headerBlock.readSlice(HTTP_MAX_LENGTH)); 177 | } 178 | header = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE); 179 | writeFrameHeader( 180 | header, 181 | headerBlock.readableBytes(), 182 | HTTP_CONTINUATION_FRAME, 183 | HTTP_FLAG_END_HEADERS, 184 | streamId 185 | ); 186 | frame = Unpooled.wrappedBuffer(frame, header, headerBlock); 187 | } 188 | return frame; 189 | } 190 | 191 | /** 192 | * Encode an HTTP/2 PING Frame 193 | */ 194 | public ByteBuf encodePingFrame(long data, boolean ack) { 195 | int length = 8; 196 | byte flags = ack ? HTTP_FLAG_ACK : 0; 197 | int streamId = 0; 198 | ByteBuf frame = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + length); 199 | writeFrameHeader(frame, length, HTTP_PING_FRAME, flags, streamId); 200 | frame.writeLong(data); 201 | return frame; 202 | } 203 | 204 | /** 205 | * Encode an HTTP/2 GOAWAY Frame 206 | */ 207 | public ByteBuf encodeGoAwayFrame(int lastStreamId, int errorCode) { 208 | int length = 8; 209 | byte flags = 0; 210 | int streamId = 0; 211 | ByteBuf frame = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + length); 212 | writeFrameHeader(frame, length, HTTP_GOAWAY_FRAME, flags, streamId); 213 | frame.writeInt(lastStreamId); 214 | frame.writeInt(errorCode); 215 | return frame; 216 | } 217 | 218 | /** 219 | * Encode an HTTP/2 WINDOW_UPDATE Frame 220 | */ 221 | public ByteBuf encodeWindowUpdateFrame(int streamId, int windowSizeIncrement) { 222 | int length = 4; 223 | byte flags = 0; 224 | ByteBuf frame = Unpooled.buffer(HTTP_FRAME_HEADER_SIZE + length); 225 | writeFrameHeader(frame, length, HTTP_WINDOW_UPDATE_FRAME, flags, streamId); 226 | frame.writeInt(windowSizeIncrement); 227 | return frame; 228 | } 229 | 230 | private void writeFrameHeader(ByteBuf buffer, int length, int type, byte flags, int streamId) { 231 | buffer.writeMedium(length); 232 | buffer.writeByte(type); 233 | buffer.writeByte(flags); 234 | buffer.writeInt(streamId); 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpStreamDecoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import java.util.List; 19 | import java.util.Map; 20 | import java.util.concurrent.CancellationException; 21 | import java.util.concurrent.ConcurrentHashMap; 22 | 23 | import io.netty.buffer.ByteBuf; 24 | import io.netty.channel.ChannelHandlerContext; 25 | import io.netty.handler.codec.DecoderResult; 26 | import io.netty.handler.codec.MessageToMessageDecoder; 27 | import io.netty.handler.codec.http.DefaultHttpContent; 28 | import io.netty.handler.codec.http.DefaultLastHttpContent; 29 | import io.netty.handler.codec.http.HttpHeaders; 30 | import io.netty.handler.codec.http.HttpMethod; 31 | import io.netty.handler.codec.http.HttpResponseStatus; 32 | import io.netty.handler.codec.http.HttpVersion; 33 | import io.netty.handler.codec.http.LastHttpContent; 34 | 35 | /** 36 | * Decodes {@link HttpFrame}s into {@link StreamedHttpRequest}s. 37 | */ 38 | public class HttpStreamDecoder extends MessageToMessageDecoder { 39 | 40 | private static final CancellationException CANCELLATION_EXCEPTION = 41 | new CancellationException("HTTP/2 RST_STREAM Frame Received"); 42 | 43 | private final Map messageMap = 44 | new ConcurrentHashMap(); 45 | private final Map trailerMap = 46 | new ConcurrentHashMap(); 47 | 48 | @Override 49 | protected void decode(ChannelHandlerContext ctx, Object msg, List out) throws Exception { 50 | if (msg instanceof HttpHeadersFrame) { 51 | 52 | HttpHeadersFrame httpHeadersFrame = (HttpHeadersFrame) msg; 53 | int streamId = httpHeadersFrame.getStreamId(); 54 | StreamedHttpMessage message = messageMap.get(streamId); 55 | 56 | if (message == null) { 57 | if (httpHeadersFrame.headers().contains(":status")) { 58 | 59 | // If a client receives a reply with a truncated header block, 60 | // reply with a RST_STREAM frame with error code INTERNAL_ERROR. 61 | if (httpHeadersFrame.isTruncated()) { 62 | HttpRstStreamFrame httpRstStreamFrame = 63 | new DefaultHttpRstStreamFrame(streamId, HttpErrorCode.INTERNAL_ERROR); 64 | out.add(httpRstStreamFrame); 65 | return; 66 | } 67 | 68 | try { 69 | StreamedHttpResponse response = createHttpResponse(httpHeadersFrame); 70 | 71 | if (httpHeadersFrame.isLast()) { 72 | HttpHeaders.setContentLength(response, 0); 73 | response.getContent().close(); 74 | } else { 75 | // Response body will follow in a series of Data Frames 76 | if (!HttpHeaders.isContentLengthSet(response)) { 77 | HttpHeaders.setTransferEncodingChunked(response); 78 | } 79 | messageMap.put(streamId, response); 80 | } 81 | out.add(response); 82 | } catch (Exception e) { 83 | // If a client receives a SYN_REPLY without valid getStatus and version headers 84 | // the client must reply with a RST_STREAM frame indicating a PROTOCOL_ERROR 85 | HttpRstStreamFrame httpRstStreamFrame = 86 | new DefaultHttpRstStreamFrame(streamId, HttpErrorCode.PROTOCOL_ERROR); 87 | ctx.writeAndFlush(httpRstStreamFrame); 88 | out.add(httpRstStreamFrame); 89 | } 90 | 91 | } else { 92 | 93 | // If a client sends a request with a truncated header block, the server must 94 | // reply with a HTTP 431 REQUEST HEADER FIELDS TOO LARGE reply. 95 | if (httpHeadersFrame.isTruncated()) { 96 | httpHeadersFrame = new DefaultHttpHeadersFrame(streamId); 97 | httpHeadersFrame.setLast(true); 98 | httpHeadersFrame.headers().set( 99 | ":status", HttpResponseStatus.REQUEST_HEADER_FIELDS_TOO_LARGE.code()); 100 | ctx.writeAndFlush(httpHeadersFrame); 101 | return; 102 | } 103 | 104 | try { 105 | message = createHttpRequest(httpHeadersFrame); 106 | 107 | if (httpHeadersFrame.isLast()) { 108 | message.setDecoderResult(DecoderResult.SUCCESS); 109 | message.getContent().close(); 110 | } else { 111 | // Request body will follow in a series of Data Frames 112 | messageMap.put(streamId, message); 113 | } 114 | 115 | out.add(message); 116 | 117 | } catch (Exception e) { 118 | // If a client sends a SYN_STREAM without all of the method, url (host and path), 119 | // scheme, and version headers the server must reply with a HTTP 400 BAD REQUEST reply. 120 | // Also sends HTTP 400 BAD REQUEST reply if header name/value pairs are invalid 121 | httpHeadersFrame = new DefaultHttpHeadersFrame(streamId); 122 | httpHeadersFrame.setLast(true); 123 | httpHeadersFrame.headers().set(":status", HttpResponseStatus.BAD_REQUEST.code()); 124 | ctx.writeAndFlush(httpHeadersFrame); 125 | } 126 | } 127 | } else { 128 | LastHttpContent trailer = trailerMap.remove(streamId); 129 | if (trailer == null) { 130 | trailer = new DefaultLastHttpContent(); 131 | } 132 | 133 | // Ignore trailers in a truncated HEADERS frame. 134 | if (!httpHeadersFrame.isTruncated()) { 135 | for (Map.Entry e: httpHeadersFrame.headers()) { 136 | trailer.trailingHeaders().add(e.getKey(), e.getValue()); 137 | } 138 | } 139 | 140 | if (httpHeadersFrame.isLast()) { 141 | messageMap.remove(streamId); 142 | message.addContent(trailer); 143 | } else { 144 | trailerMap.put(streamId, trailer); 145 | } 146 | } 147 | 148 | } else if (msg instanceof HttpDataFrame) { 149 | 150 | HttpDataFrame httpDataFrame = (HttpDataFrame) msg; 151 | int streamId = httpDataFrame.getStreamId(); 152 | StreamedHttpMessage message = messageMap.get(streamId); 153 | 154 | // If message is not in map discard Data Frame. 155 | if (message == null) { 156 | return; 157 | } 158 | 159 | ByteBuf content = httpDataFrame.content(); 160 | if (content.isReadable()) { 161 | content.retain(); 162 | message.addContent(new DefaultHttpContent(content)); 163 | } 164 | 165 | if (httpDataFrame.isLast()) { 166 | messageMap.remove(streamId); 167 | message.addContent(LastHttpContent.EMPTY_LAST_CONTENT); 168 | message.setDecoderResult(DecoderResult.SUCCESS); 169 | } 170 | 171 | } else if (msg instanceof HttpRstStreamFrame) { 172 | 173 | HttpRstStreamFrame httpRstStreamFrame = (HttpRstStreamFrame) msg; 174 | int streamId = httpRstStreamFrame.getStreamId(); 175 | StreamedHttpMessage message = messageMap.remove(streamId); 176 | 177 | if (message != null) { 178 | message.getContent().close(); 179 | message.setDecoderResult(DecoderResult.failure(CANCELLATION_EXCEPTION)); 180 | } 181 | 182 | } else { 183 | // HttpGoAwayFrame 184 | out.add(msg); 185 | } 186 | } 187 | 188 | private StreamedHttpRequest createHttpRequest(HttpHeadersFrame httpHeadersFrame) 189 | throws Exception { 190 | // Create the first line of the request from the name/value pairs 191 | HttpMethod method = HttpMethod.valueOf(httpHeadersFrame.headers().get(":method")); 192 | String url = httpHeadersFrame.headers().get(":path"); 193 | 194 | httpHeadersFrame.headers().remove(":method"); 195 | httpHeadersFrame.headers().remove(":path"); 196 | 197 | StreamedHttpRequest request = new StreamedHttpRequest(HttpVersion.HTTP_1_1, method, url); 198 | 199 | // Remove the scheme header 200 | httpHeadersFrame.headers().remove(":scheme"); 201 | 202 | // Replace the SPDY host header with the HTTP host header 203 | String host = httpHeadersFrame.headers().get(":authority"); 204 | httpHeadersFrame.headers().remove(":authority"); 205 | httpHeadersFrame.headers().set("host", host); 206 | 207 | for (Map.Entry e : httpHeadersFrame.headers()) { 208 | String name = e.getKey(); 209 | String value = e.getValue(); 210 | if (name.charAt(0) != ':') { 211 | request.headers().add(name, value); 212 | } 213 | } 214 | 215 | // Set the Stream-ID as a header 216 | request.headers().set("X-SPDY-Stream-ID", httpHeadersFrame.getStreamId()); 217 | 218 | // The Connection and Keep-Alive headers are no longer valid 219 | HttpHeaders.setKeepAlive(request, true); 220 | 221 | // Transfer-Encoding header is not valid 222 | request.headers().remove(HttpHeaders.Names.TRANSFER_ENCODING); 223 | 224 | if (httpHeadersFrame.isLast()) { 225 | request.getContent().close(); 226 | request.setDecoderResult(DecoderResult.SUCCESS); 227 | } else { 228 | request.setDecoderResult(DecoderResult.UNFINISHED); 229 | } 230 | 231 | return request; 232 | } 233 | 234 | private StreamedHttpResponse createHttpResponse(HttpHeadersFrame httpHeadersFrame) 235 | throws Exception { 236 | // Create the first line of the request from the name/value pairs 237 | HttpResponseStatus status = HttpResponseStatus.valueOf(Integer.parseInt( 238 | httpHeadersFrame.headers().get(":status"))); 239 | 240 | httpHeadersFrame.headers().remove(":status"); 241 | 242 | StreamedHttpResponse response = new StreamedHttpResponse(HttpVersion.HTTP_1_1, status); 243 | for (Map.Entry e : httpHeadersFrame.headers()) { 244 | String name = e.getKey(); 245 | String value = e.getValue(); 246 | if (name.charAt(0) != ':') { 247 | response.headers().add(name, value); 248 | } 249 | } 250 | 251 | // Set the Stream-ID as a header 252 | response.headers().set("X-SPDY-Stream-ID", httpHeadersFrame.getStreamId()); 253 | 254 | // The Connection and Keep-Alive headers are no longer valid 255 | HttpHeaders.setKeepAlive(response, true); 256 | 257 | // Transfer-Encoding header is not valid 258 | response.headers().remove(HttpHeaders.Names.TRANSFER_ENCODING); 259 | response.headers().remove(HttpHeaders.Names.TRAILER); 260 | 261 | if (httpHeadersFrame.isLast()) { 262 | response.getContent().close(); 263 | response.setDecoderResult(DecoderResult.SUCCESS); 264 | } else { 265 | response.setDecoderResult(DecoderResult.UNFINISHED); 266 | } 267 | 268 | return response; 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpStreamEncoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import java.util.Map; 19 | 20 | import io.netty.buffer.ByteBuf; 21 | import io.netty.channel.ChannelFuture; 22 | import io.netty.channel.ChannelFutureListener; 23 | import io.netty.channel.ChannelHandlerContext; 24 | import io.netty.channel.ChannelOutboundHandlerAdapter; 25 | import io.netty.channel.ChannelPromise; 26 | import io.netty.handler.codec.http.HttpContent; 27 | import io.netty.handler.codec.http.HttpHeaders; 28 | import io.netty.handler.codec.http.HttpMessage; 29 | import io.netty.handler.codec.http.HttpRequest; 30 | import io.netty.handler.codec.http.HttpResponse; 31 | import io.netty.handler.codec.http.LastHttpContent; 32 | import io.netty.util.ReferenceCountUtil; 33 | import io.netty.util.concurrent.Future; 34 | import io.netty.util.concurrent.FutureListener; 35 | 36 | /** 37 | * Encodes {@link StreamedHttpResponse}s into {@link HttpFrame}s. 38 | */ 39 | public class HttpStreamEncoder extends ChannelOutboundHandlerAdapter { 40 | 41 | private static final int MAX_DATA_LENGTH = 0x2000; // Limit Data Frames to 8k 42 | 43 | private int currentStreamId; 44 | 45 | @Override 46 | public void write(final ChannelHandlerContext ctx, Object msg, ChannelPromise promise) 47 | throws Exception { 48 | if (msg instanceof HttpRequest) { 49 | 50 | HttpRequest httpRequest = (HttpRequest) msg; 51 | HttpHeadersFrame httpHeadersFrame = createHttpHeadersFrame(httpRequest); 52 | currentStreamId = httpHeadersFrame.getStreamId(); 53 | 54 | ChannelPromise writeFuture = getMessageFuture(ctx, promise, currentStreamId, httpRequest); 55 | if (promise == writeFuture) { 56 | httpHeadersFrame.setLast(true); 57 | } else { 58 | promise = writeFuture; 59 | } 60 | 61 | ctx.write(httpHeadersFrame, promise); 62 | 63 | } else if (msg instanceof HttpResponse) { 64 | 65 | HttpResponse httpResponse = (HttpResponse) msg; 66 | HttpHeadersFrame httpHeadersFrame = createHttpHeadersFrame(httpResponse); 67 | currentStreamId = httpHeadersFrame.getStreamId(); 68 | 69 | ChannelPromise writeFuture = getMessageFuture(ctx, promise, currentStreamId, httpResponse); 70 | if (promise == writeFuture) { 71 | httpHeadersFrame.setLast(true); 72 | } else { 73 | promise = writeFuture; 74 | } 75 | 76 | ctx.write(httpHeadersFrame, promise); 77 | 78 | } else if (msg instanceof HttpContent) { 79 | 80 | HttpContent chunk = (HttpContent) msg; 81 | writeChunk(ctx, promise, currentStreamId, chunk); 82 | 83 | } else { 84 | // Unknown message type 85 | ctx.write(msg, promise); 86 | } 87 | } 88 | 89 | private ChannelPromise getMessageFuture( 90 | final ChannelHandlerContext ctx, 91 | final ChannelPromise promise, 92 | final int streamId, 93 | HttpMessage message 94 | ) { 95 | if (message instanceof StreamedHttpMessage 96 | && !((StreamedHttpMessage) message).getContent().isClosed()) { 97 | final Pipe pipe = ((StreamedHttpMessage) message).getContent(); 98 | 99 | ChannelPromise writeFuture = ctx.channel().newPromise(); 100 | writeFuture.addListener(new ChannelFutureListener() { 101 | @Override 102 | public void operationComplete(ChannelFuture future) throws Exception { 103 | // Channel's thread 104 | // First frame has been written 105 | 106 | if (future.isSuccess()) { 107 | pipe.receive().addListener( 108 | new ChunkListener(ctx, streamId, pipe, promise)); 109 | } else if (future.isCancelled()) { 110 | pipe.close(); 111 | promise.cancel(true); 112 | } else { 113 | pipe.close(); 114 | promise.setFailure(future.cause()); 115 | } 116 | } 117 | }); 118 | 119 | return writeFuture; 120 | } else { 121 | return promise; 122 | } 123 | } 124 | 125 | 126 | /** 127 | * Listens to chunks being ready on a pipe. 128 | */ 129 | private class ChunkListener implements FutureListener { 130 | private final ChannelHandlerContext ctx; 131 | private final int streamId; 132 | private final Pipe pipe; 133 | private final ChannelPromise completionFuture; 134 | 135 | ChunkListener( 136 | ChannelHandlerContext ctx, 137 | int streamId, 138 | Pipe pipe, 139 | ChannelPromise completionFuture 140 | ) { 141 | this.ctx = ctx; 142 | this.streamId = streamId; 143 | this.pipe = pipe; 144 | this.completionFuture = completionFuture; 145 | } 146 | 147 | @Override 148 | public void operationComplete(final Future future) throws Exception { 149 | final FutureListener chunkListener = this; 150 | 151 | ctx.executor().execute(new Runnable() { 152 | @Override 153 | public void run() { 154 | if (future.isSuccess()) { 155 | HttpContent content = future.getNow(); 156 | ChannelPromise writeFuture; 157 | 158 | if (content instanceof LastHttpContent) { 159 | writeFuture = completionFuture; 160 | } else { 161 | writeFuture = ctx.channel().newPromise(); 162 | writeFuture.addListener(new ChannelFutureListener() { 163 | @Override 164 | public void operationComplete(ChannelFuture future) throws Exception { 165 | if (future.isSuccess()) { 166 | pipe.receive().addListener(chunkListener); 167 | } else if (future.isCancelled()) { 168 | pipe.close(); 169 | completionFuture.cancel(true); 170 | } else { 171 | pipe.close(); 172 | completionFuture.setFailure(future.cause()); 173 | } 174 | } 175 | }); 176 | } 177 | 178 | writeChunk(ctx, writeFuture, streamId, content); 179 | } else { 180 | // Somebody closed the pipe 181 | // Send a reset frame to the channel and complete the completion future 182 | 183 | ctx.writeAndFlush( 184 | new DefaultHttpRstStreamFrame(streamId, HttpErrorCode.INTERNAL_ERROR)); 185 | 186 | if (future.isCancelled()) { 187 | completionFuture.cancel(true); 188 | } else { 189 | completionFuture.setFailure(future.cause()); 190 | } 191 | } 192 | } 193 | }); 194 | } 195 | } 196 | 197 | /** 198 | * Writes an HTTP chunk downstream as one or more HTTP/2 frames. 199 | */ 200 | protected void writeChunk( 201 | ChannelHandlerContext ctx, ChannelPromise future, int streamId, HttpContent content) { 202 | 203 | HttpFrame[] httpFrames = createHttpDataFrames(streamId, content.content()); 204 | 205 | if (content instanceof LastHttpContent) { 206 | LastHttpContent trailer = (LastHttpContent) content; 207 | HttpHeaders trailers = trailer.trailingHeaders(); 208 | if (trailers.isEmpty()) { 209 | if (httpFrames.length == 0) { 210 | HttpDataFrame httpDataFrame = new DefaultHttpDataFrame(streamId); 211 | httpDataFrame.setLast(true); 212 | httpFrames = new HttpFrame[1]; 213 | httpFrames[0] = httpDataFrame; 214 | } else { 215 | HttpDataFrame httpDataFrame = (HttpDataFrame) httpFrames[httpFrames.length - 1]; 216 | httpDataFrame.setLast(true); 217 | } 218 | } else { 219 | // Create HTTP HEADERS frame out of trailers 220 | HttpHeadersFrame httpHeadersFrame = new DefaultHttpHeadersFrame(streamId); 221 | httpHeadersFrame.setLast(true); 222 | for (Map.Entry entry : trailer.trailingHeaders()) { 223 | httpHeadersFrame.headers().add(entry.getKey(), entry.getValue()); 224 | } 225 | if (httpFrames.length == 0) { 226 | httpFrames = new HttpFrame[1]; 227 | httpFrames[0] = httpHeadersFrame; 228 | } else { 229 | HttpFrame[] copy = new HttpFrame[httpFrames.length + 1]; 230 | for (int i = 0; i < httpFrames.length; i++) { 231 | copy[i] = httpFrames[i]; 232 | } 233 | copy[httpFrames.length] = httpHeadersFrame; 234 | httpFrames = copy; 235 | } 236 | } 237 | } 238 | 239 | ChannelPromise frameFuture = getFrameFuture(ctx, future, httpFrames); 240 | 241 | // Trigger a write 242 | frameFuture.setSuccess(); 243 | } 244 | 245 | private static ChannelPromise getFrameFuture( 246 | ChannelHandlerContext ctx, ChannelPromise future, HttpFrame[] httpFrames) { 247 | ChannelPromise frameFuture = future; 248 | for (int i = httpFrames.length; --i >= 0; ) { 249 | future = ctx.channel().newPromise(); 250 | future.addListener(new HttpFrameWriter(ctx, frameFuture, httpFrames[i])); 251 | frameFuture = future; 252 | } 253 | return frameFuture; 254 | } 255 | 256 | private static class HttpFrameWriter implements ChannelFutureListener { 257 | 258 | private final ChannelHandlerContext ctx; 259 | private final ChannelPromise promise; 260 | private final Object msg; 261 | 262 | HttpFrameWriter(ChannelHandlerContext ctx, ChannelPromise promise, Object msg) { 263 | this.ctx = ctx; 264 | this.promise = promise; 265 | this.msg = msg; 266 | } 267 | 268 | public void operationComplete(ChannelFuture future) throws Exception { 269 | if (future.isSuccess()) { 270 | ctx.writeAndFlush(msg, promise); 271 | } else if (future.isCancelled()) { 272 | ReferenceCountUtil.release(msg); 273 | promise.cancel(true); 274 | } else { 275 | ReferenceCountUtil.release(msg); 276 | promise.setFailure(future.cause()); 277 | } 278 | } 279 | } 280 | 281 | private HttpHeadersFrame createHttpHeadersFrame(HttpRequest httpRequest) 282 | throws Exception { 283 | // Get the Stream-ID from the headers 284 | int streamId = HttpHeaders.getIntHeader(httpRequest, "X-SPDY-Stream-ID"); 285 | httpRequest.headers().remove("X-SPDY-Stream-ID"); 286 | 287 | // The Connection, Keep-Alive, Proxy-Connection, and Transfer-Encoding 288 | // headers are not valid and MUST not be sent. 289 | httpRequest.headers().remove(HttpHeaders.Names.CONNECTION); 290 | httpRequest.headers().remove("Keep-Alive"); 291 | httpRequest.headers().remove("Proxy-Connection"); 292 | httpRequest.headers().remove(HttpHeaders.Names.TRANSFER_ENCODING); 293 | 294 | HttpHeadersFrame httpHeadersFrame = new DefaultHttpHeadersFrame(streamId); 295 | 296 | // Unfold the first line of the request into name/value pairs 297 | httpHeadersFrame.headers().add(":method", httpRequest.getMethod().name()); 298 | httpHeadersFrame.headers().set(":scheme", "https"); 299 | httpHeadersFrame.headers().add(":path", httpRequest.getUri()); 300 | 301 | // Replace the HTTP host header with the SPDY host header 302 | String host = httpRequest.headers().get(HttpHeaders.Names.HOST); 303 | httpRequest.headers().remove(HttpHeaders.Names.HOST); 304 | httpHeadersFrame.headers().add(":authority", host); 305 | 306 | // Transfer the remaining HTTP headers 307 | for (Map.Entry entry : httpRequest.headers()) { 308 | httpHeadersFrame.headers().add(entry.getKey(), entry.getValue()); 309 | } 310 | 311 | return httpHeadersFrame; 312 | } 313 | 314 | private HttpHeadersFrame createHttpHeadersFrame(HttpResponse httpResponse) 315 | throws Exception { 316 | // Get the Stream-ID from the headers 317 | int streamId = HttpHeaders.getIntHeader(httpResponse, "X-SPDY-Stream-ID"); 318 | httpResponse.headers().remove("X-SPDY-Stream-ID"); 319 | 320 | // The Connection, Keep-Alive, Proxy-Connection, and Transfer-Encoding 321 | // headers are not valid and MUST not be sent. 322 | httpResponse.headers().remove(HttpHeaders.Names.CONNECTION); 323 | httpResponse.headers().remove("Keep-Alive"); 324 | httpResponse.headers().remove("Proxy-Connection"); 325 | httpResponse.headers().remove(HttpHeaders.Names.TRANSFER_ENCODING); 326 | 327 | HttpHeadersFrame httpHeadersFrame = new DefaultHttpHeadersFrame(streamId); 328 | 329 | // Unfold the first line of the response into name/value pairs 330 | httpHeadersFrame.headers().set(":status", httpResponse.getStatus().code()); 331 | 332 | // Transfer the remaining HTTP headers 333 | for (Map.Entry entry : httpResponse.headers()) { 334 | httpHeadersFrame.headers().add(entry.getKey(), entry.getValue()); 335 | } 336 | 337 | return httpHeadersFrame; 338 | } 339 | 340 | private HttpDataFrame[] createHttpDataFrames(int streamId, ByteBuf content) { 341 | int readableBytes = content.readableBytes(); 342 | int count = readableBytes / MAX_DATA_LENGTH; 343 | if (readableBytes % MAX_DATA_LENGTH > 0) { 344 | count++; 345 | } 346 | HttpDataFrame[] httpDataFrames = new HttpDataFrame[count]; 347 | for (int i = 0; i < count; i++) { 348 | int dataSize = Math.min(content.readableBytes(), MAX_DATA_LENGTH); 349 | HttpDataFrame httpDataFrame = new DefaultHttpDataFrame(streamId, content.readSlice(dataSize)); 350 | httpDataFrames[i] = httpDataFrame; 351 | } 352 | return httpDataFrames; 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /src/test/java/com/twitter/http2/PipeTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import org.junit.Before; 19 | import org.junit.Test; 20 | 21 | import io.netty.util.concurrent.Future; 22 | 23 | import static org.junit.Assert.assertFalse; 24 | import static org.junit.Assert.assertNotNull; 25 | import static org.junit.Assert.assertSame; 26 | import static org.junit.Assert.assertTrue; 27 | 28 | public class PipeTest { 29 | 30 | private static final Object MESSAGE = new Object(); 31 | private static final Object MESSAGE1 = new Object(); 32 | private static final Object MESSAGE2 = new Object(); 33 | 34 | // 35 | // Test Plan 36 | // 37 | // o Choose from an alphabet, { S, C, R }, where the letters stand for Send, Close, and Receive. 38 | // o The tests are named using the alphabet. 39 | // For example, testSR tests a Send then Receive. 40 | // At each point in the test, all the state in the current 41 | // and previous futures and values are tested as well. 42 | // 43 | // Some assumptions: 44 | // o If a future is not done, then it will not be succeeded, 45 | // cancelled, or failed, as per the future spec. 46 | // o If a future is done, then it will be only one of the three other future states. 47 | // 48 | 49 | private Pipe pipe; 50 | 51 | @Before 52 | public void createPipe() { 53 | pipe = new Pipe(); 54 | } 55 | 56 | @Test 57 | public void testS() { 58 | Future sendFuture = pipe.send(MESSAGE); 59 | assertFalse(sendFuture.isDone()); 60 | } 61 | 62 | @Test 63 | public void testR() { 64 | Future recvFuture = pipe.receive(); 65 | assertFalse(recvFuture.isDone()); 66 | } 67 | 68 | @Test 69 | public void testSR() { 70 | Future sendFuture = pipe.send(MESSAGE); 71 | assertFalse(sendFuture.isDone()); 72 | 73 | Future recvFuture = pipe.receive(); 74 | assertSame(MESSAGE, recvFuture.getNow()); 75 | assertTrue(sendFuture.isSuccess()); 76 | } 77 | 78 | @Test 79 | public void testRS() { 80 | Future recvFuture = pipe.receive(); 81 | assertFalse(recvFuture.isDone()); 82 | 83 | Future sendFuture = pipe.send(MESSAGE); 84 | assertTrue(sendFuture.isSuccess()); 85 | assertSame(MESSAGE, recvFuture.getNow()); 86 | } 87 | 88 | @Test 89 | public void testSS() { 90 | Future sendFuture1 = pipe.send(MESSAGE1); 91 | assertFalse(sendFuture1.isDone()); 92 | 93 | Future sendFuture2 = pipe.send(MESSAGE2); 94 | assertFalse(sendFuture2.isDone()); 95 | } 96 | 97 | 98 | @Test 99 | public void testRR() { 100 | Future recvFuture1 = pipe.receive(); 101 | assertFalse(recvFuture1.isDone()); 102 | 103 | Future recvFuture2 = pipe.receive(); 104 | assertFalse(recvFuture2.isDone()); 105 | } 106 | 107 | @Test 108 | public void testSRR() { 109 | Future sendFuture1 = pipe.send(MESSAGE); 110 | assertFalse(sendFuture1.isDone()); 111 | 112 | Future recvFuture1 = pipe.receive(); 113 | assertSame(MESSAGE, recvFuture1.getNow()); 114 | assertTrue(sendFuture1.isSuccess()); 115 | 116 | Future recvFuture2 = pipe.receive(); 117 | assertFalse(recvFuture2.isDone()); 118 | assertTrue(recvFuture1.isSuccess()); 119 | assertTrue(sendFuture1.isSuccess()); 120 | } 121 | 122 | @Test 123 | public void testSSR() { 124 | Future sendFuture1 = pipe.send(MESSAGE1); 125 | assertFalse(sendFuture1.isDone()); 126 | 127 | Future sendFuture2 = pipe.send(MESSAGE2); 128 | assertFalse(sendFuture2.isDone()); 129 | assertFalse(sendFuture1.isDone()); 130 | 131 | Future recvFuture = pipe.receive(); 132 | assertSame(MESSAGE1, recvFuture.getNow()); 133 | assertFalse(sendFuture2.isDone()); 134 | assertTrue(sendFuture1.isSuccess()); 135 | } 136 | 137 | @Test 138 | public void testSRS() { 139 | Future sendFuture1 = pipe.send(MESSAGE1); 140 | assertFalse(sendFuture1.isDone()); 141 | 142 | Future recvFuture = pipe.receive(); 143 | assertSame(MESSAGE1, recvFuture.getNow()); 144 | assertTrue(sendFuture1.isSuccess()); 145 | 146 | Future sendFuture2 = pipe.send(MESSAGE2); 147 | assertFalse(sendFuture2.isDone()); 148 | assertTrue(recvFuture.isSuccess()); 149 | assertTrue(sendFuture1.isSuccess()); 150 | } 151 | 152 | @Test 153 | public void testRSR() { 154 | Future recvFuture1 = pipe.receive(); 155 | assertFalse(recvFuture1.isDone()); 156 | 157 | Future sendFuture = pipe.send(MESSAGE); 158 | assertTrue(sendFuture.isSuccess()); 159 | assertSame(MESSAGE, recvFuture1.getNow()); 160 | 161 | Future recvFuture2 = pipe.receive(); 162 | assertFalse(recvFuture2.isDone()); 163 | assertTrue(sendFuture.isSuccess()); 164 | assertTrue(recvFuture1.isSuccess()); 165 | } 166 | 167 | @Test 168 | public void testRSS() { 169 | Future recvFuture = pipe.receive(); 170 | assertFalse(recvFuture.isDone()); 171 | 172 | Future sendFuture1 = pipe.send(MESSAGE1); 173 | assertTrue(sendFuture1.isSuccess()); 174 | assertSame(MESSAGE1, recvFuture.getNow()); 175 | 176 | Future sendFuture2 = pipe.send(MESSAGE2); 177 | assertFalse(sendFuture2.isDone()); 178 | assertTrue(sendFuture1.isSuccess()); 179 | assertTrue(recvFuture.isSuccess()); 180 | } 181 | 182 | @Test 183 | public void testRRS() { 184 | Future recvFuture1 = pipe.receive(); 185 | assertFalse(recvFuture1.isDone()); 186 | 187 | Future recvFuture2 = pipe.receive(); 188 | assertFalse(recvFuture2.isDone()); 189 | assertFalse(recvFuture1.isDone()); 190 | 191 | Future sendFuture = pipe.send(MESSAGE); 192 | assertTrue(sendFuture.isSuccess()); 193 | assertFalse(recvFuture2.isDone()); 194 | assertSame(MESSAGE, recvFuture1.getNow()); 195 | } 196 | 197 | @Test 198 | public void testSSRR() { 199 | Future sendFuture1 = pipe.send(MESSAGE1); 200 | assertFalse(sendFuture1.isDone()); 201 | 202 | Future sendFuture2 = pipe.send(MESSAGE2); 203 | assertFalse(sendFuture2.isDone()); 204 | assertFalse(sendFuture1.isDone()); 205 | 206 | Future recvFuture1 = pipe.receive(); 207 | assertSame(MESSAGE1, recvFuture1.getNow()); 208 | assertFalse(sendFuture2.isDone()); 209 | assertTrue(sendFuture1.isSuccess()); 210 | 211 | Future recvFuture2 = pipe.receive(); 212 | assertSame(MESSAGE2, recvFuture2.getNow()); 213 | assertTrue(recvFuture1.isSuccess()); 214 | assertTrue(sendFuture2.isSuccess()); 215 | assertTrue(sendFuture1.isSuccess()); 216 | } 217 | 218 | @Test 219 | public void testRRSS() { 220 | Future recvFuture1 = pipe.receive(); 221 | assertFalse(recvFuture1.isDone()); 222 | 223 | Future recvFuture2 = pipe.receive(); 224 | assertFalse(recvFuture2.isDone()); 225 | assertFalse(recvFuture1.isDone()); 226 | 227 | Future sendFuture1 = pipe.send(MESSAGE1); 228 | assertTrue(sendFuture1.isSuccess()); 229 | assertFalse(recvFuture2.isDone()); 230 | assertSame(MESSAGE1, recvFuture1.getNow()); 231 | 232 | Future sendFuture2 = pipe.send(MESSAGE2); 233 | assertTrue(sendFuture2.isSuccess()); 234 | assertTrue(sendFuture1.isSuccess()); 235 | assertSame(MESSAGE2, recvFuture2.getNow()); 236 | assertSame(MESSAGE1, recvFuture1.getNow()); 237 | } 238 | 239 | @Test 240 | public void testRSSR() { 241 | Future recvFuture1 = pipe.receive(); 242 | assertFalse(recvFuture1.isDone()); 243 | 244 | Future sendFuture1 = pipe.send(MESSAGE1); 245 | assertTrue(sendFuture1.isSuccess()); 246 | assertSame(MESSAGE1, recvFuture1.getNow()); 247 | 248 | Future sendFuture2 = pipe.send(MESSAGE2); 249 | assertFalse(sendFuture2.isDone()); 250 | assertTrue(sendFuture1.isSuccess()); 251 | assertTrue(recvFuture1.isSuccess()); 252 | 253 | Future recvFuture2 = pipe.receive(); 254 | assertSame(MESSAGE2, recvFuture2.getNow()); 255 | assertTrue(sendFuture2.isSuccess()); 256 | assertTrue(sendFuture1.isSuccess()); 257 | assertTrue(recvFuture1.isSuccess()); 258 | } 259 | 260 | @Test 261 | public void testSRRS() { 262 | Future sendFuture1 = pipe.send(MESSAGE1); 263 | assertFalse(sendFuture1.isDone()); 264 | 265 | Future recvFuture1 = pipe.receive(); 266 | assertSame(MESSAGE1, recvFuture1.getNow()); 267 | assertTrue(sendFuture1.isSuccess()); 268 | 269 | Future recvFuture2 = pipe.receive(); 270 | assertFalse(recvFuture2.isDone()); 271 | assertTrue(recvFuture1.isSuccess()); 272 | assertTrue(sendFuture1.isSuccess()); 273 | 274 | Future sendFuture2 = pipe.send(MESSAGE2); 275 | assertTrue(sendFuture2.isSuccess()); 276 | assertSame(MESSAGE2, recvFuture2.getNow()); 277 | assertTrue(recvFuture1.isSuccess()); 278 | assertTrue(sendFuture1.isSuccess()); 279 | } 280 | 281 | @Test 282 | public void testSSRRR() { 283 | Future sendFuture1 = pipe.send(MESSAGE1); 284 | assertFalse(sendFuture1.isDone()); 285 | 286 | Future sendFuture2 = pipe.send(MESSAGE2); 287 | assertFalse(sendFuture2.isDone()); 288 | assertFalse(sendFuture1.isDone()); 289 | 290 | Future recvFuture1 = pipe.receive(); 291 | assertTrue(recvFuture1.isSuccess()); 292 | assertSame(MESSAGE1, recvFuture1.getNow()); 293 | assertFalse(sendFuture2.isDone()); 294 | assertTrue(sendFuture1.isSuccess()); 295 | 296 | Future recvFuture2 = pipe.receive(); 297 | assertTrue(recvFuture2.isSuccess()); 298 | assertSame(MESSAGE2, recvFuture2.getNow()); 299 | assertTrue(sendFuture2.isSuccess()); 300 | assertTrue(recvFuture1.isSuccess()); 301 | assertTrue(sendFuture2.isSuccess()); 302 | assertTrue(sendFuture1.isSuccess()); 303 | 304 | Future recvFuture3 = pipe.receive(); 305 | assertFalse(recvFuture3.isDone()); 306 | } 307 | 308 | @Test 309 | public void testCS() { 310 | pipe.close(); 311 | 312 | Future sendFuture = pipe.send(MESSAGE); 313 | assertNotNull(sendFuture.cause()); 314 | } 315 | 316 | @Test 317 | public void testSC() { 318 | Future sendFuture = pipe.send(MESSAGE); 319 | assertFalse(sendFuture.isDone()); 320 | 321 | pipe.close(); 322 | assertFalse(sendFuture.isDone()); 323 | } 324 | 325 | @Test 326 | public void testCR() { 327 | pipe.close(); 328 | 329 | Future recvFuture = pipe.receive(); 330 | assertNotNull(recvFuture.cause()); 331 | } 332 | 333 | @Test 334 | public void testRC() { 335 | Future recvFuture = pipe.receive(); 336 | assertFalse(recvFuture.isDone()); 337 | 338 | pipe.close(); 339 | assertNotNull(recvFuture.isDone()); 340 | } 341 | 342 | @Test 343 | public void testCSR() { 344 | pipe.close(); 345 | 346 | Future sendFuture = pipe.send(MESSAGE); 347 | assertNotNull(sendFuture.cause()); 348 | 349 | Future recvFuture = pipe.receive(); 350 | assertNotNull(recvFuture.cause()); 351 | assertNotNull(sendFuture.cause()); 352 | } 353 | 354 | @Test 355 | public void testSCR() { 356 | Future sendFuture = pipe.send(MESSAGE); 357 | assertFalse(sendFuture.isDone()); 358 | 359 | pipe.close(); 360 | assertFalse(sendFuture.isDone()); 361 | 362 | Future recvFuture = pipe.receive(); 363 | assertSame(MESSAGE, recvFuture.getNow()); 364 | assertTrue(sendFuture.isSuccess()); 365 | } 366 | 367 | @Test 368 | public void testSRCS() { 369 | Future sendFuture1 = pipe.send(MESSAGE1); 370 | assertFalse(sendFuture1.isDone()); 371 | 372 | Future recvFuture = pipe.receive(); 373 | assertSame(MESSAGE1, recvFuture.getNow()); 374 | assertTrue(sendFuture1.isSuccess()); 375 | 376 | pipe.close(); 377 | 378 | Future sendFuture2 = pipe.send(MESSAGE2); 379 | assertNotNull(sendFuture2.cause()); 380 | } 381 | 382 | @Test 383 | public void testSRCR() { 384 | Future sendFuture = pipe.send(MESSAGE); 385 | assertFalse(sendFuture.isDone()); 386 | 387 | Future recvFuture1 = pipe.receive(); 388 | assertSame(MESSAGE, recvFuture1.getNow()); 389 | assertTrue(sendFuture.isSuccess()); 390 | 391 | pipe.close(); 392 | 393 | Future recvFuture2 = pipe.receive(); 394 | assertNotNull(recvFuture2.cause()); 395 | assertTrue(sendFuture.isSuccess()); 396 | } 397 | 398 | @Test 399 | public void testCRS() { 400 | pipe.close(); 401 | 402 | Future recvFuture = pipe.receive(); 403 | assertNotNull(recvFuture.cause()); 404 | 405 | Future sendFuture = pipe.send(MESSAGE); 406 | assertNotNull(sendFuture.cause()); 407 | assertNotNull(recvFuture.cause()); 408 | } 409 | 410 | @Test 411 | public void testRCS() { 412 | Future recvFuture = pipe.receive(); 413 | assertFalse(recvFuture.isDone()); 414 | 415 | pipe.close(); 416 | assertNotNull(recvFuture.cause()); 417 | 418 | Future sendFuture = pipe.send(MESSAGE); 419 | assertNotNull(sendFuture.cause()); 420 | assertNotNull(recvFuture.cause()); 421 | } 422 | 423 | @Test 424 | public void testRSCR() { 425 | Future recvFuture1 = pipe.receive(); 426 | assertFalse(recvFuture1.isDone()); 427 | 428 | Future sendFuture1 = pipe.send(MESSAGE); 429 | assertTrue(sendFuture1.isSuccess()); 430 | assertSame(MESSAGE, recvFuture1.getNow()); 431 | 432 | pipe.close(); 433 | 434 | Future recvFuture2 = pipe.receive(); 435 | assertNotNull(recvFuture2.cause()); 436 | assertTrue(sendFuture1.isSuccess()); 437 | assertTrue(recvFuture1.isSuccess()); 438 | } 439 | 440 | @Test 441 | public void testRSCS() { 442 | Future recvFuture = pipe.receive(); 443 | assertFalse(recvFuture.isDone()); 444 | 445 | Future sendFuture1 = pipe.send(MESSAGE1); 446 | assertTrue(sendFuture1.isSuccess()); 447 | assertSame(MESSAGE1, recvFuture.getNow()); 448 | 449 | pipe.close(); 450 | 451 | Future sendFuture2 = pipe.send(MESSAGE2); 452 | assertNotNull(sendFuture2.cause()); 453 | assertTrue(sendFuture1.isSuccess()); 454 | assertTrue(recvFuture.isSuccess()); 455 | } 456 | } 457 | -------------------------------------------------------------------------------- /src/main/java/com/twitter/http2/HttpConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import java.util.Comparator; 19 | import java.util.Map; 20 | import java.util.Set; 21 | import java.util.TreeSet; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | import java.util.concurrent.ConcurrentLinkedQueue; 24 | import java.util.concurrent.atomic.AtomicInteger; 25 | 26 | import io.netty.channel.ChannelPromise; 27 | import io.netty.util.internal.EmptyArrays; 28 | 29 | import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_WEIGHT; 30 | import static com.twitter.http2.HttpCodecUtil.HTTP_CONNECTION_STREAM_ID; 31 | 32 | final class HttpConnection { 33 | 34 | private static final HttpProtocolException STREAM_CLOSED = 35 | new HttpProtocolException("Stream closed"); 36 | 37 | static { 38 | STREAM_CLOSED.setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); 39 | } 40 | 41 | private final AtomicInteger activeLocalStreams = new AtomicInteger(); 42 | private final AtomicInteger activeRemoteStreams = new AtomicInteger(); 43 | private final Map streams = new ConcurrentHashMap(); 44 | 45 | private final AtomicInteger sendWindowSize; 46 | private final AtomicInteger receiveWindowSize; 47 | 48 | public HttpConnection(int sendWindowSize, int receiveWindowSize) { 49 | streams.put(HTTP_CONNECTION_STREAM_ID, new Node(null)); 50 | this.sendWindowSize = new AtomicInteger(sendWindowSize); 51 | this.receiveWindowSize = new AtomicInteger(receiveWindowSize); 52 | } 53 | 54 | int numActiveStreams(boolean remote) { 55 | if (remote) { 56 | return activeRemoteStreams.get(); 57 | } else { 58 | return activeLocalStreams.get(); 59 | } 60 | } 61 | 62 | boolean noActiveStreams() { 63 | return activeRemoteStreams.get() + activeLocalStreams.get() == 0; 64 | } 65 | 66 | void acceptStream( 67 | int streamId, boolean remoteSideClosed, boolean localSideClosed, 68 | int streamSendWindowSize, int streamReceiveWindowSize, boolean remote) { 69 | StreamState state = null; 70 | if (!remoteSideClosed || !localSideClosed) { 71 | state = new StreamState( 72 | remoteSideClosed, localSideClosed, streamSendWindowSize, streamReceiveWindowSize); 73 | } 74 | Node node = new Node(state); 75 | node.parent = streams.get(HTTP_CONNECTION_STREAM_ID); 76 | streams.put(streamId, node); 77 | if (state != null) { 78 | if (remote) { 79 | activeRemoteStreams.incrementAndGet(); 80 | } else { 81 | activeLocalStreams.incrementAndGet(); 82 | } 83 | } 84 | } 85 | 86 | private StreamState removeActiveStream(int streamId, boolean remote) { 87 | Node stream = streams.remove(streamId); 88 | if (stream != null && stream.state != null) { 89 | StreamState state = stream.state; 90 | stream.close(); 91 | if (remote) { 92 | activeRemoteStreams.decrementAndGet(); 93 | } else { 94 | activeLocalStreams.decrementAndGet(); 95 | } 96 | return state; 97 | } 98 | return null; 99 | } 100 | 101 | void removeStream(int streamId, boolean remote) { 102 | StreamState state = removeActiveStream(streamId, remote); 103 | if (state != null) { 104 | state.clearPendingWrites(STREAM_CLOSED); 105 | } 106 | } 107 | 108 | boolean isRemoteSideClosed(int streamId) { 109 | Node stream = streams.get(streamId); 110 | StreamState state = stream == null ? null : stream.state; 111 | return state == null || state.isRemoteSideClosed(); 112 | } 113 | 114 | void closeRemoteSide(int streamId, boolean remote) { 115 | Node stream = streams.get(streamId); 116 | StreamState state = stream == null ? null : stream.state; 117 | if (state != null) { 118 | state.closeRemoteSide(); 119 | if (state.isLocalSideClosed()) { 120 | removeActiveStream(streamId, remote); 121 | } 122 | } 123 | } 124 | 125 | boolean isLocalSideClosed(int streamId) { 126 | Node stream = streams.get(streamId); 127 | StreamState state = stream == null ? null : stream.state; 128 | return state == null || state.isLocalSideClosed(); 129 | } 130 | 131 | void closeLocalSide(int streamId, boolean remote) { 132 | Node stream = streams.get(streamId); 133 | StreamState state = stream == null ? null : stream.state; 134 | if (state != null) { 135 | state.closeLocalSide(); 136 | if (state.isRemoteSideClosed()) { 137 | removeActiveStream(streamId, remote); 138 | } 139 | } 140 | } 141 | 142 | int getSendWindowSize(int streamId) { 143 | if (streamId == HTTP_CONNECTION_STREAM_ID) { 144 | return sendWindowSize.get(); 145 | } 146 | 147 | Node stream = streams.get(streamId); 148 | StreamState state = stream == null ? null : stream.state; 149 | return state != null ? state.getSendWindowSize() : -1; 150 | } 151 | 152 | int updateSendWindowSize(int streamId, int deltaWindowSize) { 153 | if (streamId == HTTP_CONNECTION_STREAM_ID) { 154 | return sendWindowSize.addAndGet(deltaWindowSize); 155 | } 156 | 157 | Node stream = streams.get(streamId); 158 | StreamState state = stream == null ? null : stream.state; 159 | return state != null ? state.updateSendWindowSize(deltaWindowSize) : -1; 160 | } 161 | 162 | int updateReceiveWindowSize(int streamId, int deltaWindowSize) { 163 | if (streamId == HTTP_CONNECTION_STREAM_ID) { 164 | return receiveWindowSize.addAndGet(deltaWindowSize); 165 | } 166 | 167 | Node stream = streams.get(streamId); 168 | StreamState state = stream == null ? null : stream.state; 169 | if (state == null) { 170 | return -1; 171 | } 172 | if (deltaWindowSize > 0) { 173 | state.setReceiveWindowSizeLowerBound(0); 174 | } 175 | return state.updateReceiveWindowSize(deltaWindowSize); 176 | } 177 | 178 | int getReceiveWindowSizeLowerBound(int streamId) { 179 | if (streamId == HTTP_CONNECTION_STREAM_ID) { 180 | return 0; 181 | } 182 | 183 | Node stream = streams.get(streamId); 184 | StreamState state = stream == null ? null : stream.state; 185 | return state != null ? state.getReceiveWindowSizeLowerBound() : 0; 186 | } 187 | 188 | void updateAllSendWindowSizes(int deltaWindowSize) { 189 | for (Node stream : streams.values()) { 190 | StreamState state = stream.state; 191 | if (state != null) { 192 | state.updateSendWindowSize(deltaWindowSize); 193 | } 194 | } 195 | } 196 | 197 | void updateAllReceiveWindowSizes(int deltaWindowSize) { 198 | for (Node stream : streams.values()) { 199 | StreamState state = stream.state; 200 | if (state != null) { 201 | state.updateReceiveWindowSize(deltaWindowSize); 202 | if (deltaWindowSize < 0) { 203 | state.setReceiveWindowSizeLowerBound(deltaWindowSize); 204 | } 205 | } 206 | } 207 | } 208 | 209 | boolean putPendingWrite(int streamId, PendingWrite evt) { 210 | Node stream = streams.get(streamId); 211 | StreamState state = stream == null ? null : stream.state; 212 | return state != null && state.putPendingWrite(evt); 213 | } 214 | 215 | PendingWrite getPendingWrite(int streamId) { 216 | if (streamId == HTTP_CONNECTION_STREAM_ID) { 217 | Node connection = streams.get(HTTP_CONNECTION_STREAM_ID); 218 | return getPendingWrite(connection); 219 | } 220 | 221 | Node stream = streams.get(streamId); 222 | StreamState state = stream == null ? null : stream.state; 223 | return state != null ? state.getPendingWrite() : null; 224 | } 225 | 226 | PendingWrite getPendingWrite(Node node) { 227 | PendingWrite e = null; 228 | if (node.state != null && node.state.getSendWindowSize() > 0) { 229 | e = node.state.getPendingWrite(); 230 | } 231 | if (e == null) { 232 | for (Node child : node.children) { 233 | e = getPendingWrite(child); 234 | if (e != null) { 235 | break; 236 | } 237 | } 238 | } 239 | return e; 240 | } 241 | 242 | PendingWrite removePendingWrite(int streamId) { 243 | Node stream = streams.get(streamId); 244 | StreamState state = stream == null ? null : stream.state; 245 | return state != null ? state.removePendingWrite() : null; 246 | } 247 | 248 | /** 249 | * Set the priority of the stream. 250 | */ 251 | boolean setPriority(int streamId, boolean exclusive, int dependency, int weight) { 252 | Node stream = streams.get(streamId); 253 | if (stream == null) { 254 | // stream closed? 255 | return false; 256 | } 257 | 258 | Node parent = streams.get(dependency); 259 | if (parent == null) { 260 | // garbage collected? 261 | stream.parent.removeDependent(stream); 262 | 263 | // set to default priority 264 | Node root = streams.get(HTTP_CONNECTION_STREAM_ID); 265 | root.addDependent(false, stream); 266 | stream.setWeight(HTTP_DEFAULT_WEIGHT); 267 | return false; 268 | } 269 | 270 | // check if we need to restructure the tree 271 | if (parent == stream.parent) { 272 | if (exclusive) { 273 | // move dependents to stream 274 | parent.addDependent(true, stream); 275 | } 276 | } else { 277 | stream.parent.removeDependent(stream); 278 | parent.addDependent(exclusive, stream); 279 | } 280 | 281 | stream.setWeight(weight); 282 | return true; 283 | } 284 | 285 | private static final class StreamState { 286 | 287 | private boolean remoteSideClosed; 288 | private boolean localSideClosed; 289 | private final AtomicInteger sendWindowSize; 290 | private final AtomicInteger receiveWindowSize; 291 | private int receiveWindowSizeLowerBound; 292 | private final ConcurrentLinkedQueue pendingWriteQueue = 293 | new ConcurrentLinkedQueue(); 294 | 295 | StreamState( 296 | boolean remoteSideClosed, boolean localSideClosed, 297 | int sendWindowSize, int receiveWindowSize) { 298 | this.remoteSideClosed = remoteSideClosed; 299 | this.localSideClosed = localSideClosed; 300 | this.sendWindowSize = new AtomicInteger(sendWindowSize); 301 | this.receiveWindowSize = new AtomicInteger(receiveWindowSize); 302 | } 303 | 304 | boolean isRemoteSideClosed() { 305 | return remoteSideClosed; 306 | } 307 | 308 | void closeRemoteSide() { 309 | remoteSideClosed = true; 310 | } 311 | 312 | boolean isLocalSideClosed() { 313 | return localSideClosed; 314 | } 315 | 316 | void closeLocalSide() { 317 | localSideClosed = true; 318 | } 319 | 320 | int getSendWindowSize() { 321 | return sendWindowSize.get(); 322 | } 323 | 324 | int updateSendWindowSize(int deltaWindowSize) { 325 | return sendWindowSize.addAndGet(deltaWindowSize); 326 | } 327 | 328 | int updateReceiveWindowSize(int deltaWindowSize) { 329 | return receiveWindowSize.addAndGet(deltaWindowSize); 330 | } 331 | 332 | int getReceiveWindowSizeLowerBound() { 333 | return receiveWindowSizeLowerBound; 334 | } 335 | 336 | void setReceiveWindowSizeLowerBound(int receiveWindowSizeLowerBound) { 337 | this.receiveWindowSizeLowerBound = receiveWindowSizeLowerBound; 338 | } 339 | 340 | boolean putPendingWrite(PendingWrite msg) { 341 | return pendingWriteQueue.offer(msg); 342 | } 343 | 344 | PendingWrite getPendingWrite() { 345 | return pendingWriteQueue.peek(); 346 | } 347 | 348 | PendingWrite removePendingWrite() { 349 | return pendingWriteQueue.poll(); 350 | } 351 | 352 | void clearPendingWrites(Throwable cause) { 353 | for (; ; ) { 354 | PendingWrite pendingWrite = pendingWriteQueue.poll(); 355 | if (pendingWrite == null) { 356 | break; 357 | } 358 | pendingWrite.fail(cause); 359 | } 360 | } 361 | } 362 | 363 | private static final class Node { 364 | 365 | private static final Comparator COMPARATOR = new WeightComparator(); 366 | 367 | public Node parent; // the dependency of the stream 368 | public int weight; // the weight of the dependency 369 | 370 | // Children should be iterator in weighted order 371 | public Set children = new TreeSet(COMPARATOR); // the dependents of the stream 372 | public int dependentWeights; // the total weight of all the dependents of the stream 373 | 374 | public StreamState state; 375 | 376 | public Node(StreamState state) { 377 | this.state = state; 378 | } 379 | 380 | public void close() { 381 | this.state = null; 382 | } 383 | 384 | public void setWeight(int weight) { 385 | // Remove and re-add parent to maintain comparator order 386 | parent.removeDependent(this); 387 | this.weight = weight; 388 | parent.addDependent(false, this); 389 | } 390 | 391 | public void addDependent(boolean exclusive, Node node) { 392 | removeDependent(node); 393 | if (exclusive) { 394 | for (Node child : children) { 395 | node.addDependent(false, child); 396 | } 397 | children.clear(); 398 | dependentWeights = 0; 399 | } 400 | children.add(node); 401 | dependentWeights += node.weight; 402 | } 403 | 404 | public void removeDependent(Node node) { 405 | if (children.remove(node)) { 406 | dependentWeights -= node.weight; 407 | } 408 | } 409 | } 410 | 411 | private static final class WeightComparator implements Comparator { 412 | @Override 413 | public int compare(Node n1, Node n2) { 414 | return n2.weight - n1.weight; 415 | } 416 | } 417 | 418 | public static final class PendingWrite { 419 | public final HttpDataFrame httpDataFrame; 420 | public final ChannelPromise promise; 421 | 422 | PendingWrite(HttpDataFrame httpDataFrame, ChannelPromise promise) { 423 | this.httpDataFrame = httpDataFrame; 424 | this.promise = promise; 425 | } 426 | 427 | void fail(Throwable cause) { 428 | // httpDataFrame.release(); 429 | promise.setFailure(cause); 430 | } 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /src/test/java/com/twitter/http2/HttpFrameEncoderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Twitter, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.twitter.http2; 17 | 18 | import java.util.Random; 19 | 20 | import io.netty.buffer.ByteBuf; 21 | import io.netty.buffer.Unpooled; 22 | import org.junit.Test; 23 | 24 | import static io.netty.util.ReferenceCountUtil.releaseLater; 25 | import static org.junit.Assert.assertEquals; 26 | import static org.junit.Assert.assertFalse; 27 | import static org.junit.Assert.assertTrue; 28 | 29 | import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_DEPENDENCY; 30 | import static com.twitter.http2.HttpCodecUtil.HTTP_DEFAULT_WEIGHT; 31 | import static com.twitter.http2.HttpCodecUtil.HTTP_MAX_LENGTH; 32 | 33 | public class HttpFrameEncoderTest { 34 | 35 | private static final Random RANDOM = new Random(); 36 | 37 | private static final HttpFrameEncoder ENCODER = new HttpFrameEncoder(); 38 | 39 | @Test 40 | public void testHttpDataFrame() throws Exception { 41 | int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 42 | ByteBuf data = Unpooled.buffer(1024); 43 | for (int i = 0; i < 256; i++) { 44 | data.writeInt(RANDOM.nextInt()); 45 | } 46 | ByteBuf frame = releaseLater( 47 | ENCODER.encodeDataFrame(streamId, false, data.duplicate()) 48 | ); 49 | assertDataFrame(frame, streamId, false, data); 50 | } 51 | 52 | @Test 53 | public void testEmptyHttpDataFrame() throws Exception { 54 | int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 55 | ByteBuf frame = releaseLater( 56 | ENCODER.encodeDataFrame(streamId, false, Unpooled.EMPTY_BUFFER) 57 | ); 58 | assertDataFrame(frame, streamId, false, Unpooled.EMPTY_BUFFER); 59 | } 60 | 61 | @Test 62 | public void testLastHttpDataFrame() throws Exception { 63 | int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 64 | ByteBuf frame = releaseLater( 65 | ENCODER.encodeDataFrame(streamId, true, Unpooled.EMPTY_BUFFER) 66 | ); 67 | assertDataFrame(frame, streamId, true, Unpooled.EMPTY_BUFFER); 68 | } 69 | 70 | @Test 71 | public void testHttpHeadersFrame() throws Exception { 72 | int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 73 | boolean exclusive = RANDOM.nextBoolean(); 74 | int dependency = RANDOM.nextInt() & 0x7FFFFFFF; 75 | int weight = (RANDOM.nextInt() & 0xFF) + 1; 76 | ByteBuf headerBlock = Unpooled.buffer(1024); 77 | for (int i = 0; i < 256; i++) { 78 | headerBlock.writeInt(RANDOM.nextInt()); 79 | } 80 | ByteBuf frame = releaseLater( 81 | ENCODER.encodeHeadersFrame( 82 | streamId, false, exclusive, dependency, weight, headerBlock.duplicate()) 83 | ); 84 | assertHeadersFrame(frame, streamId, exclusive, dependency, weight, false, headerBlock); 85 | } 86 | 87 | @Test 88 | public void testEmptyHttpHeadersFrame() throws Exception { 89 | int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 90 | boolean exclusive = RANDOM.nextBoolean(); 91 | int dependency = RANDOM.nextInt() & 0x7FFFFFFF; 92 | int weight = (RANDOM.nextInt() & 0xFF) + 1; 93 | ByteBuf frame = releaseLater( 94 | ENCODER.encodeHeadersFrame( 95 | streamId, false, exclusive, dependency, weight, Unpooled.EMPTY_BUFFER) 96 | ); 97 | assertHeadersFrame( 98 | frame, streamId, exclusive, dependency, weight, false, Unpooled.EMPTY_BUFFER); 99 | } 100 | 101 | @Test 102 | public void testLastHttpHeadersFrame() throws Exception { 103 | int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 104 | boolean exclusive = false; 105 | int dependency = HTTP_DEFAULT_DEPENDENCY; 106 | int weight = HTTP_DEFAULT_WEIGHT; 107 | ByteBuf frame = releaseLater( 108 | ENCODER.encodeHeadersFrame( 109 | streamId, true, exclusive, dependency, weight, Unpooled.EMPTY_BUFFER) 110 | ); 111 | assertHeadersFrame( 112 | frame, streamId, exclusive, dependency, weight, true, Unpooled.EMPTY_BUFFER); 113 | } 114 | 115 | @Test 116 | public void testContinuedHttpHeadersFrame() throws Exception { 117 | int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 118 | boolean exclusive = RANDOM.nextBoolean(); 119 | int dependency = RANDOM.nextInt() & 0x7FFFFFFF; 120 | int weight = (RANDOM.nextInt() & 0xFF) + 1; 121 | ByteBuf headerBlock = Unpooled.buffer(2 * HTTP_MAX_LENGTH); 122 | for (int i = 0; i < 2 * HTTP_MAX_LENGTH; i++) { 123 | headerBlock.writeByte(RANDOM.nextInt()); 124 | } 125 | ByteBuf frame = releaseLater( 126 | ENCODER.encodeHeadersFrame( 127 | streamId, false, exclusive, dependency, weight, headerBlock.duplicate()) 128 | ); 129 | assertHeadersFrame( 130 | frame, streamId, exclusive, dependency, weight, false, headerBlock); 131 | } 132 | 133 | @Test 134 | public void testHttpPriorityFrame() throws Exception { 135 | int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 136 | boolean exclusive = RANDOM.nextBoolean(); 137 | int dependency = RANDOM.nextInt() & 0x7FFFFFFF; 138 | int weight = (RANDOM.nextInt() & 0xFF) + 1; 139 | ByteBuf frame = releaseLater( 140 | ENCODER.encodePriorityFrame(streamId, exclusive, dependency, weight) 141 | ); 142 | assertPriorityFrame(frame, streamId, exclusive, dependency, weight); 143 | } 144 | 145 | @Test 146 | public void testHttpRstStreamFrame() throws Exception { 147 | int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 148 | int errorCode = RANDOM.nextInt(); 149 | ByteBuf frame = releaseLater( 150 | ENCODER.encodeRstStreamFrame(streamId, errorCode) 151 | ); 152 | assertRstStreamFrame(frame, streamId, errorCode); 153 | } 154 | 155 | @Test 156 | public void testHttpSettingsFrame() throws Exception { 157 | int id = RANDOM.nextInt() & 0xFFFF; 158 | int value = RANDOM.nextInt(); 159 | HttpSettingsFrame httpSettingsFrame = new DefaultHttpSettingsFrame(); 160 | httpSettingsFrame.setValue(id, value); 161 | ByteBuf frame = releaseLater( 162 | ENCODER.encodeSettingsFrame(httpSettingsFrame) 163 | ); 164 | assertSettingsFrame(frame, false, id, value); 165 | } 166 | 167 | @Test 168 | public void testHttpSettingsAckFrame() throws Exception { 169 | HttpSettingsFrame httpSettingsFrame = new DefaultHttpSettingsFrame(); 170 | httpSettingsFrame.setAck(true); 171 | ByteBuf frame = releaseLater( 172 | ENCODER.encodeSettingsFrame(httpSettingsFrame) 173 | ); 174 | assertSettingsFrame(frame, true, 0, 0); 175 | } 176 | 177 | @Test 178 | public void testHttpPushPromiseFrame() throws Exception { 179 | int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 180 | int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 181 | ByteBuf headerBlock = Unpooled.buffer(1024); 182 | for (int i = 0; i < 256; i++) { 183 | headerBlock.writeInt(RANDOM.nextInt()); 184 | } 185 | ByteBuf frame = releaseLater( 186 | ENCODER.encodePushPromiseFrame(streamId, promisedStreamId, headerBlock.duplicate()) 187 | ); 188 | assertPushPromiseFrame(frame, streamId, promisedStreamId, headerBlock); 189 | } 190 | 191 | @Test 192 | public void testEmptyHttpPushPromiseFrame() throws Exception { 193 | int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 194 | int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 195 | ByteBuf frame = releaseLater( 196 | ENCODER.encodePushPromiseFrame(streamId, promisedStreamId, Unpooled.EMPTY_BUFFER) 197 | ); 198 | assertPushPromiseFrame(frame, streamId, promisedStreamId, Unpooled.EMPTY_BUFFER); 199 | } 200 | 201 | @Test 202 | public void testContinuedHttpPushPromiseFrame() throws Exception { 203 | int streamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 204 | int promisedStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 205 | ByteBuf headerBlock = Unpooled.buffer(2 * HTTP_MAX_LENGTH); 206 | for (int i = 0; i < 2 * HTTP_MAX_LENGTH; i++) { 207 | headerBlock.writeByte(RANDOM.nextInt()); 208 | } 209 | ByteBuf frame = releaseLater( 210 | ENCODER.encodePushPromiseFrame(streamId, promisedStreamId, headerBlock.duplicate()) 211 | ); 212 | assertPushPromiseFrame(frame, streamId, promisedStreamId, headerBlock); 213 | } 214 | 215 | @Test 216 | public void testHttpPingFrame() throws Exception { 217 | long data = RANDOM.nextLong(); 218 | ByteBuf frame = releaseLater( 219 | ENCODER.encodePingFrame(data, false) 220 | ); 221 | assertPingFrame(frame, false, data); 222 | } 223 | 224 | @Test 225 | public void testHttpPongFrame() throws Exception { 226 | long data = RANDOM.nextLong(); 227 | ByteBuf frame = releaseLater( 228 | ENCODER.encodePingFrame(data, true) 229 | ); 230 | assertPingFrame(frame, true, data); 231 | } 232 | 233 | @Test 234 | public void testHttpGoAwayFrame() throws Exception { 235 | int lastStreamId = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 236 | int errorCode = RANDOM.nextInt(); 237 | ByteBuf frame = releaseLater( 238 | ENCODER.encodeGoAwayFrame(lastStreamId, errorCode) 239 | ); 240 | assertGoAwayFrame(frame, lastStreamId, errorCode); 241 | } 242 | 243 | @Test 244 | public void testHttpWindowUpdateFrame() throws Exception { 245 | int streamId = RANDOM.nextInt() & 0x7FFFFFFF; // connection identifier allowed 246 | int windowSizeIncrement = RANDOM.nextInt() & 0x7FFFFFFF | 0x01; 247 | ByteBuf frame = releaseLater( 248 | ENCODER.encodeWindowUpdateFrame(streamId, windowSizeIncrement) 249 | ); 250 | assertWindowUpdateFrame(frame, streamId, windowSizeIncrement); 251 | } 252 | 253 | private static void assertDataFrame(ByteBuf frame, int streamId, boolean last, ByteBuf data) { 254 | byte type = 0x00; 255 | byte flags = 0x00; 256 | if (last) { 257 | flags |= 0x01; 258 | } 259 | int length = assertFrameHeader(frame, type, flags, streamId); 260 | assertEquals(data.readableBytes(), length); 261 | for (int i = 0; i < length; i++) { 262 | assertEquals(data.getByte(i), frame.readByte()); 263 | } 264 | assertFalse(frame.isReadable()); 265 | } 266 | 267 | private static void assertHeadersFrame( 268 | ByteBuf frame, 269 | int streamId, 270 | boolean exclusive, 271 | int dependency, 272 | int weight, 273 | boolean last, 274 | ByteBuf headerBlock 275 | ) { 276 | boolean hasPriority = 277 | exclusive || dependency != HTTP_DEFAULT_DEPENDENCY || weight != HTTP_DEFAULT_WEIGHT; 278 | int maxLength = hasPriority ? HTTP_MAX_LENGTH - 5 : HTTP_MAX_LENGTH; 279 | byte type = 0x01; 280 | byte flags = 0x00; 281 | if (last) { 282 | flags |= 0x01; 283 | } 284 | if (headerBlock.readableBytes() <= maxLength) { 285 | flags |= 0x04; 286 | } 287 | if (hasPriority) { 288 | flags |= 0x20; 289 | } 290 | int length = assertFrameHeader(frame, type, flags, streamId); 291 | if (hasPriority) { 292 | assertTrue(length >= 5); 293 | if (exclusive) { 294 | assertEquals(dependency | 0x80000000, frame.readInt()); 295 | } else { 296 | assertEquals(dependency, frame.readInt()); 297 | } 298 | assertEquals(weight - 1, frame.readUnsignedByte()); 299 | length -= 5; 300 | } 301 | assertTrue(length <= headerBlock.readableBytes()); 302 | for (int i = 0; i < length; i++) { 303 | assertEquals(headerBlock.readByte(), frame.readByte()); 304 | } 305 | while (headerBlock.isReadable()) { 306 | type = 0x09; 307 | flags = 0x00; 308 | if (headerBlock.readableBytes() <= HTTP_MAX_LENGTH) { 309 | flags |= 0x04; 310 | } 311 | length = assertFrameHeader(frame, type, flags, streamId); 312 | assertTrue(length <= headerBlock.readableBytes()); 313 | for (int i = 0; i < length; i++) { 314 | assertEquals(headerBlock.readByte(), frame.readByte()); 315 | } 316 | } 317 | assertFalse(frame.isReadable()); 318 | } 319 | 320 | private static void assertPriorityFrame( 321 | ByteBuf frame, int streamId, boolean exclusive, int dependency, int weight 322 | ) { 323 | byte type = 0x02; 324 | byte flags = 0x00; 325 | assertEquals(5, assertFrameHeader(frame, type, flags, streamId)); 326 | if (exclusive) { 327 | assertEquals(dependency | 0x80000000, frame.readInt()); 328 | } else { 329 | assertEquals(dependency, frame.readInt()); 330 | } 331 | assertEquals(weight - 1, frame.readUnsignedByte()); 332 | assertFalse(frame.isReadable()); 333 | } 334 | 335 | private static void assertRstStreamFrame(ByteBuf frame, int streamId, int errorCode) { 336 | byte type = 0x03; 337 | byte flags = 0x00; 338 | assertEquals(4, assertFrameHeader(frame, type, flags, streamId)); 339 | assertEquals(errorCode, frame.readInt()); 340 | assertFalse(frame.isReadable()); 341 | } 342 | 343 | private static void assertSettingsFrame(ByteBuf frame, boolean ack, int id, int value) { 344 | byte type = 0x04; 345 | byte flags = ack ? (byte) 0x01 : 0x00; 346 | int length = assertFrameHeader(frame, type, flags, 0); 347 | if (ack) { 348 | assertEquals(0, length); 349 | } else { 350 | assertEquals(6, length); 351 | assertEquals(id, frame.readUnsignedShort()); 352 | assertEquals(value, frame.readInt()); 353 | } 354 | assertFalse(frame.isReadable()); 355 | } 356 | 357 | private static void assertPushPromiseFrame( 358 | ByteBuf frame, int streamId, int promisedStreamId, ByteBuf headerBlock) { 359 | int maxLength = HTTP_MAX_LENGTH - 4; 360 | byte type = 0x05; 361 | byte flags = 0x00; 362 | if (headerBlock.readableBytes() <= maxLength) { 363 | flags |= 0x04; 364 | } 365 | int length = assertFrameHeader(frame, type, flags, streamId); 366 | assertTrue(length >= 4); 367 | assertEquals(promisedStreamId, frame.readInt()); 368 | length -= 4; 369 | assertTrue(length <= headerBlock.readableBytes()); 370 | for (int i = 0; i < length; i++) { 371 | assertEquals(headerBlock.readByte(), frame.readByte()); 372 | } 373 | while (headerBlock.isReadable()) { 374 | type = 0x09; 375 | flags = 0x00; 376 | if (headerBlock.readableBytes() <= HTTP_MAX_LENGTH) { 377 | flags |= 0x04; 378 | } 379 | length = assertFrameHeader(frame, type, flags, streamId); 380 | assertTrue(length <= headerBlock.readableBytes()); 381 | for (int i = 0; i < length; i++) { 382 | assertEquals(headerBlock.readByte(), frame.readByte()); 383 | } 384 | } 385 | assertFalse(frame.isReadable()); 386 | } 387 | 388 | private static void assertPingFrame(ByteBuf frame, boolean pong, long data) { 389 | byte type = 0x06; 390 | byte flags = 0x00; 391 | if (pong) { 392 | flags |= 0x01; 393 | } 394 | assertEquals(8, assertFrameHeader(frame, type, flags, 0)); 395 | assertEquals(data, frame.readLong()); 396 | assertFalse(frame.isReadable()); 397 | } 398 | 399 | private static void assertGoAwayFrame(ByteBuf frame, int lastStreamId, int errorCode) { 400 | byte type = 0x07; 401 | byte flags = 0x00; 402 | assertEquals(8, assertFrameHeader(frame, type, flags, 0)); 403 | assertEquals(lastStreamId, frame.readInt()); 404 | assertEquals(errorCode, frame.readInt()); 405 | assertFalse(frame.isReadable()); 406 | } 407 | 408 | private static void assertWindowUpdateFrame( 409 | ByteBuf frame, int streamId, int windowSizeIncrement 410 | ) { 411 | byte type = 0x08; 412 | byte flags = 0x00; 413 | assertEquals(4, assertFrameHeader(frame, type, flags, streamId)); 414 | assertEquals(windowSizeIncrement, frame.readInt()); 415 | assertFalse(frame.isReadable()); 416 | } 417 | 418 | // Verifies the type, flag, and streamId in the frame header and returns the length. 419 | private static int assertFrameHeader(ByteBuf frame, byte type, byte flags, int streamId) { 420 | int length = frame.readUnsignedMedium(); 421 | assertEquals(type, frame.readByte()); 422 | assertEquals(flags, frame.readByte()); 423 | assertEquals(streamId, frame.readInt()); 424 | return length; 425 | } 426 | } 427 | --------------------------------------------------------------------------------