Back port HTTP/2 codec from master to 4.1

Motivation:

HTTP/2 codec was implemented in master branch.
Since, master is not yet stable and will be some time before it gets released, backporting it to 4.1, enables people to use the codec with a stable netty version.

Modification:

The code has been copied from master branch as is, with minor modifications to suit the `ChannelHandler` API in 4.x.
Apart from that change, there are two backward incompatible API changes included, namely,

- Added an abstract method:

  `public abstract Map.Entry<CharSequence, CharSequence> forEachEntry(EntryVisitor<CharSequence> visitor)
            throws Exception;`

to `HttpHeaders` and implemented the same in `DefaultHttpHeaders` as a delegate to the internal `TextHeader` instance.

- Added a method:

`FullHttpMessage copy(ByteBuf newContent);`

in `FullHttpMessage` with the implementations copied from relevant places in the master branch.

- Added missing abstract method related to setting/adding short values to `HttpHeaders`

Result:

HTTP/2 codec can be used with netty 4.1
This commit is contained in:
Nitesh Kant 2015-01-19 16:48:11 -08:00 committed by Scott Mitchell
parent ed10513238
commit 2d24e1f27d
105 changed files with 20730 additions and 26 deletions

View File

@ -22,6 +22,7 @@ import io.netty.buffer.Unpooled;
* Default implementation of {@link FullHttpRequest}.
*/
public class DefaultFullHttpRequest extends DefaultHttpRequest implements FullHttpRequest {
private static final int HASH_CODE_PRIME = 31;
private final ByteBuf content;
private final HttpHeaders trailingHeader;
private final boolean validateHeaders;
@ -34,6 +35,10 @@ public class DefaultFullHttpRequest extends DefaultHttpRequest implements FullHt
this(httpVersion, method, uri, content, true);
}
public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri, boolean validateHeaders) {
this(httpVersion, method, uri, Unpooled.buffer(0), validateHeaders);
}
public DefaultFullHttpRequest(HttpVersion httpVersion, HttpMethod method, String uri,
ByteBuf content, boolean validateHeaders) {
super(httpVersion, method, uri, validateHeaders);
@ -112,15 +117,41 @@ public class DefaultFullHttpRequest extends DefaultHttpRequest implements FullHt
return this;
}
@Override
public FullHttpRequest copy() {
/**
* Copy this object
*
* @param copyContent
* <ul>
* <li>{@code true} if this object's {@link #content()} should be used to copy.</li>
* <li>{@code false} if {@code newContent} should be used instead.</li>
* </ul>
* @param newContent
* <ul>
* <li>if {@code copyContent} is false then this will be used in the copy's content.</li>
* <li>if {@code null} then a default buffer of 0 size will be selected</li>
* </ul>
* @return A copy of this object
*/
private FullHttpRequest copy(boolean copyContent, ByteBuf newContent) {
DefaultFullHttpRequest copy = new DefaultFullHttpRequest(
protocolVersion(), method(), uri(), content().copy(), validateHeaders);
protocolVersion(), method(), uri(),
copyContent ? content().copy() :
newContent == null ? Unpooled.buffer(0) : newContent);
copy.headers().set(headers());
copy.trailingHeaders().set(trailingHeaders());
return copy;
}
@Override
public FullHttpRequest copy(ByteBuf newContent) {
return copy(false, newContent);
}
@Override
public FullHttpRequest copy() {
return copy(true, null);
}
@Override
public FullHttpRequest duplicate() {
DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest(
@ -130,6 +161,28 @@ public class DefaultFullHttpRequest extends DefaultHttpRequest implements FullHt
return duplicate;
}
@Override
public int hashCode() {
int result = 1;
result = HASH_CODE_PRIME * result + content().hashCode();
result = HASH_CODE_PRIME * result + trailingHeaders().hashCode();
result = HASH_CODE_PRIME * result + super.hashCode();
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DefaultFullHttpRequest)) {
return false;
}
DefaultFullHttpRequest other = (DefaultFullHttpRequest) o;
return super.equals(other) &&
content().equals(other.content()) &&
trailingHeaders().equals(other.trailingHeaders());
}
@Override
public String toString() {
return HttpMessageUtil.appendFullRequest(new StringBuilder(256), this).toString();

View File

@ -36,6 +36,10 @@ public class DefaultFullHttpResponse extends DefaultHttpResponse implements Full
this(version, status, content, true);
}
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status, boolean validateHeaders) {
this(version, status, Unpooled.buffer(0), validateHeaders);
}
public DefaultFullHttpResponse(HttpVersion version, HttpResponseStatus status,
ByteBuf content, boolean validateHeaders) {
super(version, status, validateHeaders);
@ -108,15 +112,41 @@ public class DefaultFullHttpResponse extends DefaultHttpResponse implements Full
return this;
}
@Override
public FullHttpResponse copy() {
/**
* Copy this object
*
* @param copyContent
* <ul>
* <li>{@code true} if this object's {@link #content()} should be used to copy.</li>
* <li>{@code false} if {@code newContent} should be used instead.</li>
* </ul>
* @param newContent
* <ul>
* <li>if {@code copyContent} is false then this will be used in the copy's content.</li>
* <li>if {@code null} then a default buffer of 0 size will be selected</li>
* </ul>
* @return A copy of this object
*/
private FullHttpResponse copy(boolean copyContent, ByteBuf newContent) {
DefaultFullHttpResponse copy = new DefaultFullHttpResponse(
protocolVersion(), status(), content().copy(), validateHeaders);
protocolVersion(), status(),
copyContent ? content().copy() :
newContent == null ? Unpooled.buffer(0) : newContent);
copy.headers().set(headers());
copy.trailingHeaders().set(trailingHeaders());
return copy;
}
@Override
public FullHttpResponse copy(ByteBuf newContent) {
return copy(false, newContent);
}
@Override
public FullHttpResponse copy() {
return copy(true, null);
}
@Override
public FullHttpResponse duplicate() {
DefaultFullHttpResponse duplicate = new DefaultFullHttpResponse(protocolVersion(), status(),

View File

@ -20,6 +20,7 @@ import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.DefaultHeaders.NameConverter;
import io.netty.handler.codec.DefaultTextHeaders;
import io.netty.handler.codec.DefaultTextHeaders.DefaultTextValueTypeConverter;
import io.netty.handler.codec.Headers.EntryVisitor;
import io.netty.handler.codec.TextHeaders;
import java.util.Calendar;
@ -27,6 +28,7 @@ import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
/**
@ -303,6 +305,12 @@ public class DefaultHttpHeaders extends HttpHeaders {
return this;
}
@Override
public HttpHeaders addShort(CharSequence name, short value) {
headers.addShort(name, value);
return this;
}
@Override
public HttpHeaders remove(String name) {
headers.remove(name);
@ -345,6 +353,12 @@ public class DefaultHttpHeaders extends HttpHeaders {
return this;
}
@Override
public HttpHeaders setShort(CharSequence name, short value) {
headers.setShort(name, value);
return this;
}
@Override
public HttpHeaders clear() {
headers.clear();
@ -371,6 +385,16 @@ public class DefaultHttpHeaders extends HttpHeaders {
return headers.getInt(name, defaultValue);
}
@Override
public Short getShort(CharSequence name) {
return headers.getShort(name);
}
@Override
public short getShort(CharSequence name, short defaultValue) {
return headers.getInt(name, defaultValue);
}
@Override
public Long getTimeMillis(CharSequence name) {
return headers.getTimeMillis(name);
@ -426,11 +450,32 @@ public class DefaultHttpHeaders extends HttpHeaders {
return headers.contains(name, value, ignoreCase);
}
@Override
public Entry<CharSequence, CharSequence> forEachEntry(EntryVisitor<CharSequence> visitor) throws Exception {
return headers.forEachEntry(visitor);
}
@Override
public Set<String> names() {
return headers.namesAndConvert(String.CASE_INSENSITIVE_ORDER);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DefaultHttpHeaders)) {
return false;
}
DefaultHttpHeaders other = (DefaultHttpHeaders) o;
return headers.equals(other.headers);
}
@Override
public int hashCode() {
return headers.hashCode();
}
void encode(ByteBuf buf) throws Exception {
headers.forEachEntry(new HttpHeadersEncoder(buf));
}

View File

@ -19,7 +19,7 @@ package io.netty.handler.codec.http;
* The default {@link HttpMessage} implementation.
*/
public abstract class DefaultHttpMessage extends DefaultHttpObject implements HttpMessage {
private static final int HASH_CODE_PRIME = 31;
private HttpVersion version;
private final HttpHeaders headers;
@ -57,6 +57,28 @@ public abstract class DefaultHttpMessage extends DefaultHttpObject implements Ht
return version;
}
@Override
public int hashCode() {
int result = 1;
result = HASH_CODE_PRIME * result + headers.hashCode();
result = HASH_CODE_PRIME * result + version.hashCode();
result = HASH_CODE_PRIME * result + super.hashCode();
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DefaultHttpMessage)) {
return false;
}
DefaultHttpMessage other = (DefaultHttpMessage) o;
return headers().equals(other.headers()) &&
protocolVersion().equals(other.protocolVersion()) &&
super.equals(o);
}
@Override
public HttpMessage setProtocolVersion(HttpVersion version) {
if (version == null) {

View File

@ -19,6 +19,7 @@ import io.netty.handler.codec.DecoderResult;
public class DefaultHttpObject implements HttpObject {
private static final int HASH_CODE_PRIME = 31;
private DecoderResult decoderResult = DecoderResult.SUCCESS;
protected DefaultHttpObject() {
@ -43,4 +44,22 @@ public class DefaultHttpObject implements HttpObject {
}
this.decoderResult = decoderResult;
}
@Override
public int hashCode() {
int result = 1;
result = HASH_CODE_PRIME * result + decoderResult.hashCode();
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DefaultHttpObject)) {
return false;
}
DefaultHttpObject other = (DefaultHttpObject) o;
return decoderResult().equals(other.decoderResult());
}
}

View File

@ -15,13 +15,11 @@
*/
package io.netty.handler.codec.http;
import io.netty.util.internal.StringUtil;
/**
* The default {@link HttpRequest} implementation.
*/
public class DefaultHttpRequest extends DefaultHttpMessage implements HttpRequest {
private static final int HASH_CODE_PRIME = 31;
private HttpMethod method;
private String uri;
@ -102,6 +100,28 @@ public class DefaultHttpRequest extends DefaultHttpMessage implements HttpReques
return this;
}
@Override
public int hashCode() {
int result = 1;
result = HASH_CODE_PRIME * result + method.hashCode();
result = HASH_CODE_PRIME * result + uri.hashCode();
result = HASH_CODE_PRIME * result + super.hashCode();
return result;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof DefaultHttpRequest)) {
return false;
}
DefaultHttpRequest other = (DefaultHttpRequest) o;
return method().equals(other.method()) &&
uri().equalsIgnoreCase(other.uri()) &&
super.equals(o);
}
@Override
public String toString() {
return HttpMessageUtil.appendRequest(new StringBuilder(256), this).toString();

View File

@ -15,11 +15,23 @@
*/
package io.netty.handler.codec.http;
import io.netty.buffer.ByteBuf;
/**
* Combines {@link HttpMessage} and {@link LastHttpContent} into one
* message. So it represent a <i>complete</i> http message.
*/
public interface FullHttpMessage extends HttpMessage, LastHttpContent {
/**
* Create a copy of this {@link FullHttpMessage} with alternative content.
*
* @param newContent The buffer to use instead of this {@link FullHttpMessage}'s content in the copy operation.
* <p>
* NOTE: retain will NOT be called on this buffer. {@code null} results in an empty default choice buffer.
* @return The result of the copy operation
*/
FullHttpMessage copy(ByteBuf newContent);
@Override
FullHttpMessage copy();

View File

@ -15,11 +15,16 @@
*/
package io.netty.handler.codec.http;
import io.netty.buffer.ByteBuf;
/**
* Combinate the {@link HttpRequest} and {@link FullHttpMessage}, so the request is a <i>complete</i> HTTP
* request.
*/
public interface FullHttpRequest extends HttpRequest, FullHttpMessage {
@Override
FullHttpRequest copy(ByteBuf newContent);
@Override
FullHttpRequest copy();

View File

@ -15,11 +15,16 @@
*/
package io.netty.handler.codec.http;
import io.netty.buffer.ByteBuf;
/**
* Combination of a {@link HttpResponse} and {@link FullHttpMessage}.
* So it represent a <i>complete</i> http response.
*/
public interface FullHttpResponse extends HttpResponse, FullHttpMessage {
@Override
FullHttpResponse copy(ByteBuf newContent);
@Override
FullHttpResponse copy();

View File

@ -40,7 +40,7 @@ import java.util.concurrent.atomic.AtomicLong;
*
* @see HttpServerCodec
*/
public final class HttpClientCodec extends ChannelHandlerAppender {
public final class HttpClientCodec extends ChannelHandlerAppender implements HttpClientUpgradeHandler.SourceCodec {
/** A queue that is used for correlating a request and a response. */
private final Queue<HttpMethod> queue = new ArrayDeque<HttpMethod>();
@ -86,6 +86,16 @@ public final class HttpClientCodec extends ChannelHandlerAppender {
this.failOnMissingResponse = failOnMissingResponse;
}
/**
* Upgrades to another protocol from HTTP. Removes the {@link Decoder} and {@link Encoder} from
* the pipeline.
*/
@Override
public void upgradeFrom(ChannelHandlerContext ctx) {
ctx.pipeline().remove(Decoder.class);
ctx.pipeline().remove(Encoder.class);
}
/**
* Returns the encoder of this codec.
*/

View File

@ -0,0 +1,272 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandler;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.AsciiString;
import java.net.SocketAddress;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static io.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS;
import static io.netty.util.ReferenceCountUtil.release;
/**
* Client-side handler for handling an HTTP upgrade handshake to another protocol. When the first
* HTTP request is sent, this handler will add all appropriate headers to perform an upgrade to the
* new protocol. If the upgrade fails (i.e. response is not 101 Switching Protocols), this handler
* simply removes itself from the pipeline. If the upgrade is successful, upgrades the pipeline to
* the new protocol.
*/
public class HttpClientUpgradeHandler extends HttpObjectAggregator implements ChannelOutboundHandler {
/**
* User events that are fired to notify about upgrade status.
*/
public enum UpgradeEvent {
/**
* The Upgrade request was sent to the server.
*/
UPGRADE_ISSUED,
/**
* The Upgrade to the new protocol was successful.
*/
UPGRADE_SUCCESSFUL,
/**
* The Upgrade was unsuccessful due to the server not issuing
* with a 101 Switching Protocols response.
*/
UPGRADE_REJECTED
}
/**
* The source codec that is used in the pipeline initially.
*/
public interface SourceCodec {
/**
* Removes this codec (i.e. all associated handlers) from the pipeline.
*/
void upgradeFrom(ChannelHandlerContext ctx);
}
/**
* A codec that the source can be upgraded to.
*/
public interface UpgradeCodec {
/**
* Returns the name of the protocol supported by this codec, as indicated by the {@code 'UPGRADE'} header.
*/
String protocol();
/**
* Sets any protocol-specific headers required to the upgrade request. Returns the names of
* all headers that were added. These headers will be used to populate the CONNECTION header.
*/
Collection<String> setUpgradeHeaders(ChannelHandlerContext ctx, HttpRequest upgradeRequest);
/**
* Performs an HTTP protocol upgrade from the source codec. This method is responsible for
* adding all handlers required for the new protocol.
*
* @param ctx the context for the current handler.
* @param upgradeResponse the 101 Switching Protocols response that indicates that the server
* has switched to this protocol.
*/
void upgradeTo(ChannelHandlerContext ctx, FullHttpResponse upgradeResponse) throws Exception;
}
private final SourceCodec sourceCodec;
private final UpgradeCodec upgradeCodec;
private boolean upgradeRequested;
/**
* Constructs the client upgrade handler.
*
* @param sourceCodec the codec that is being used initially.
* @param upgradeCodec the codec that the client would like to upgrade to.
* @param maxContentLength the maximum length of the aggregated content.
*/
public HttpClientUpgradeHandler(SourceCodec sourceCodec, UpgradeCodec upgradeCodec,
int maxContentLength) {
super(maxContentLength);
if (sourceCodec == null) {
throw new NullPointerException("sourceCodec");
}
if (upgradeCodec == null) {
throw new NullPointerException("upgradeCodec");
}
this.sourceCodec = sourceCodec;
this.upgradeCodec = upgradeCodec;
}
@Override
public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {
ctx.bind(localAddress, promise);
}
@Override
public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
ctx.connect(remoteAddress, localAddress, promise);
}
@Override
public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.disconnect(promise);
}
@Override
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.close(promise);
}
@Override
public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.deregister(promise);
}
@Override
public void read(ChannelHandlerContext ctx) throws Exception {
ctx.read();
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
throws Exception {
if (!(msg instanceof HttpRequest)) {
ctx.write(msg, promise);
return;
}
if (upgradeRequested) {
promise.setFailure(new IllegalStateException(
"Attempting to write HTTP request with upgrade in progress"));
return;
}
upgradeRequested = true;
setUpgradeRequestHeaders(ctx, (HttpRequest) msg);
// Continue writing the request.
ctx.write(msg, promise);
// Notify that the upgrade request was issued.
ctx.fireUserEventTriggered(UpgradeEvent.UPGRADE_ISSUED);
// Now we wait for the next HTTP response to see if we switch protocols.
}
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
protected void decode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out)
throws Exception {
FullHttpResponse response = null;
try {
if (!upgradeRequested) {
throw new IllegalStateException("Read HTTP response without requesting protocol switch");
}
if (msg instanceof FullHttpResponse) {
response = (FullHttpResponse) msg;
// Need to retain since the base class will release after returning from this method.
response.retain();
out.add(response);
} else {
// Call the base class to handle the aggregation of the full request.
super.decode(ctx, msg, out);
if (out.isEmpty()) {
// The full request hasn't been created yet, still awaiting more data.
return;
}
assert out.size() == 1;
response = (FullHttpResponse) out.get(0);
}
if (!SWITCHING_PROTOCOLS.equals(response.status())) {
// The server does not support the requested protocol, just remove this handler
// and continue processing HTTP.
// NOTE: not releasing the response since we're letting it propagate to the
// next handler.
ctx.fireUserEventTriggered(UpgradeEvent.UPGRADE_REJECTED);
removeThisHandler(ctx);
return;
}
CharSequence upgradeHeader = response.headers().get(HttpHeaderNames.UPGRADE);
if (upgradeHeader == null) {
throw new IllegalStateException(
"Switching Protocols response missing UPGRADE header");
}
if (!AsciiString.equalsIgnoreCase(upgradeCodec.protocol(), upgradeHeader)) {
throw new IllegalStateException(
"Switching Protocols response with unexpected UPGRADE protocol: "
+ upgradeHeader);
}
// Upgrade to the new protocol.
sourceCodec.upgradeFrom(ctx);
upgradeCodec.upgradeTo(ctx, response);
// Notify that the upgrade to the new protocol completed successfully.
ctx.fireUserEventTriggered(UpgradeEvent.UPGRADE_SUCCESSFUL);
// We switched protocols, so we're done with the upgrade response.
// Release it and clear it from the output.
response.release();
out.clear();
removeThisHandler(ctx);
} catch (Throwable t) {
release(response);
ctx.fireExceptionCaught(t);
removeThisHandler(ctx);
}
}
private static void removeThisHandler(ChannelHandlerContext ctx) {
ctx.pipeline().remove(ctx.name());
}
/**
* Adds all upgrade request headers necessary for an upgrade to the supported protocols.
*/
private void setUpgradeRequestHeaders(ChannelHandlerContext ctx, HttpRequest request) {
// Set the UPGRADE header on the request.
request.headers().set(HttpHeaderNames.UPGRADE, upgradeCodec.protocol());
// Add all protocol-specific headers to the request.
Set<String> connectionParts = new LinkedHashSet<String>(2);
connectionParts.addAll(upgradeCodec.setUpgradeHeaders(ctx, request));
// Set the CONNECTION header from the set of all protocol-specific headers that were added.
StringBuilder builder = new StringBuilder();
for (String part : connectionParts) {
builder.append(part);
builder.append(',');
}
builder.append(HttpHeaderNames.UPGRADE);
request.headers().set(HttpHeaderNames.CONNECTION, builder.toString());
}
}

View File

@ -76,6 +76,10 @@ public final class HttpHeaderValues {
* {@code "deflate"}
*/
public static final AsciiString DEFLATE = new AsciiString("deflate");
/**
* {@code "x-deflate"}
*/
public static final AsciiString X_DEFLATE = new AsciiString("x-deflate");
/**
* {@code "file"}
* See {@link HttpHeaderNames#CONTENT_DISPOSITION}
@ -95,6 +99,10 @@ public final class HttpHeaderValues {
* {@code "gzip"}
*/
public static final AsciiString GZIP = new AsciiString("gzip");
/**
* {@code "x-gzip"}
*/
public static final AsciiString X_GZIP = new AsciiString("x-gzip");
/**
* {@code "identity"}
*/

View File

@ -17,6 +17,8 @@ package io.netty.handler.codec.http;
import io.netty.buffer.ByteBuf;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.Headers.EntryVisitor;
import io.netty.util.internal.PlatformDependent;
import java.text.ParseException;
import java.util.Calendar;
@ -29,6 +31,7 @@ import java.util.Map.Entry;
import java.util.Set;
import static io.netty.handler.codec.http.HttpConstants.*;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
/**
* Provides the constants for the standard HTTP header names and values and
@ -55,6 +58,16 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
return defaultValue;
}
@Override
public Short getShort(CharSequence name) {
return null;
}
@Override
public short getShort(CharSequence name, short defaultValue) {
return defaultValue;
}
@Override
public Long getTimeMillis(CharSequence name) {
return null;
@ -105,6 +118,11 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
throw new UnsupportedOperationException("read only");
}
@Override
public HttpHeaders addShort(CharSequence name, short value) {
throw new UnsupportedOperationException("read only");
}
@Override
public HttpHeaders set(String name, Object value) {
throw new UnsupportedOperationException("read only");
@ -120,6 +138,11 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
throw new UnsupportedOperationException("read only");
}
@Override
public HttpHeaders setShort(CharSequence name, short value) {
throw new UnsupportedOperationException("read only");
}
@Override
public HttpHeaders remove(String name) {
throw new UnsupportedOperationException("read only");
@ -130,6 +153,11 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
throw new UnsupportedOperationException("read only");
}
@Override
public Entry<CharSequence, CharSequence> forEachEntry(EntryVisitor<CharSequence> visitor) throws Exception {
return null; // Since this is an empty header collection
}
@Override
public Iterator<Entry<String, String>> iterator() {
return entries().iterator();
@ -1336,6 +1364,27 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
*/
public abstract int getInt(CharSequence name, int defaultValue);
/**
* Returns the short value of a header with the specified name. If there are more than one values for the
* specified name, the first value is returned.
*
* @param name the name of the header to search
* @return the first header value if the header is found and its value is a short. {@code null} if there's no
* such header or its value is not a short.
*/
public abstract Short getShort(CharSequence name);
/**
* Returns the short value of a header with the specified name. If there are more than one values for the
* specified name, the first value is returned.
*
* @param name the name of the header to search
* @param defaultValue the default value
* @return the first header value if the header is found and its value is a short. {@code defaultValue} if
* there's no such header or its value is not a short.
*/
public abstract short getShort(CharSequence name, short defaultValue);
/**
* Returns the date value of a header with the specified name. If there are more than one values for the
* specified name, the first value is returned.
@ -1478,6 +1527,14 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
*/
public abstract HttpHeaders addInt(CharSequence name, int value);
/**
* Add the {@code name} to {@code value}.
* @param name The name to modify
* @param value The value
* @return {@code this}
*/
public abstract HttpHeaders addShort(CharSequence name, short value);
/**
* @see {@link #set(CharSequence, Object)}
*/
@ -1534,17 +1591,39 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
* @return {@code this}
*/
public HttpHeaders set(HttpHeaders headers) {
if (headers == null) {
throw new NullPointerException("headers");
}
checkNotNull(headers, "headers");
clear();
if (headers.isEmpty()) {
return this;
}
for (Map.Entry<String, String> e: headers) {
add(e.getKey(), e.getValue());
try {
headers.forEachEntry(addAllVisitor());
} catch (Exception e) {
PlatformDependent.throwException(e);
}
return this;
}
/**
* Retains all current headers but calls {@link #set(String, Object)} for each entry in {@code headers}
*
* @param headers The headers used to {@link #set(String, Object)} values in this instance
* @return {@code this}
*/
public HttpHeaders setAll(HttpHeaders headers) {
checkNotNull(headers, "headers");
if (headers.isEmpty()) {
return this;
}
try {
headers.forEachEntry(setAllVisitor());
} catch (Exception e) {
PlatformDependent.throwException(e);
}
return this;
}
@ -1557,6 +1636,14 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
*/
public abstract HttpHeaders setInt(CharSequence name, int value);
/**
* Set the {@code name} to {@code value}. This will remove all previous values associated with {@code name}.
* @param name The name to modify
* @param value The value
* @return {@code this}
*/
public abstract HttpHeaders setShort(CharSequence name, short value);
/**
* @see {@link #remove(CharSequence)}
*/
@ -1613,4 +1700,34 @@ public abstract class HttpHeaders implements Iterable<Map.Entry<String, String>>
public boolean contains(CharSequence name, CharSequence value, boolean ignoreCase) {
return contains(name.toString(), value.toString(), ignoreCase);
}
/**
* Provide a means of iterating over elements in this map with low GC
*
* @param visitor The visitor which will visit each element in this map
* @return The last entry before iteration stopped or {@code null} if iteration went past the end
*/
public abstract Map.Entry<CharSequence, CharSequence> forEachEntry(EntryVisitor<CharSequence> visitor)
throws Exception;
private EntryVisitor<CharSequence> setAllVisitor() {
return new EntryVisitor<CharSequence>() {
@Override
public boolean visit(Entry<CharSequence, CharSequence> entry) {
set(entry.getKey(), entry.getValue());
return true;
}
};
}
private EntryVisitor<CharSequence> addAllVisitor() {
return new EntryVisitor<CharSequence>() {
@Override
public boolean visit(Entry<CharSequence, CharSequence> entry) {
add(entry.getKey(), entry.getValue());
return true;
}
};
}
}

View File

@ -285,15 +285,41 @@ public class HttpObjectAggregator
super(request, content, trailingHeaders);
}
@Override
public FullHttpRequest copy() {
/**
* Copy this object
*
* @param copyContent
* <ul>
* <li>{@code true} if this object's {@link #content()} should be used to copy.</li>
* <li>{@code false} if {@code newContent} should be used instead.</li>
* </ul>
* @param newContent
* <ul>
* <li>if {@code copyContent} is false then this will be used in the copy's content.</li>
* <li>if {@code null} then a default buffer of 0 size will be selected</li>
* </ul>
* @return A copy of this object
*/
private FullHttpRequest copy(boolean copyContent, ByteBuf newContent) {
DefaultFullHttpRequest copy = new DefaultFullHttpRequest(
getProtocolVersion(), getMethod(), getUri(), content().copy());
protocolVersion(), method(), uri(),
copyContent ? content().copy() :
newContent == null ? Unpooled.buffer(0) : newContent);
copy.headers().set(headers());
copy.trailingHeaders().set(trailingHeaders());
return copy;
}
@Override
public FullHttpRequest copy(ByteBuf newContent) {
return copy(false, newContent);
}
@Override
public FullHttpRequest copy() {
return copy(true, null);
}
@Override
public FullHttpRequest duplicate() {
DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest(
@ -378,15 +404,41 @@ public class HttpObjectAggregator
super(message, content, trailingHeaders);
}
@Override
public FullHttpResponse copy() {
/**
* Copy this object
*
* @param copyContent
* <ul>
* <li>{@code true} if this object's {@link #content()} should be used to copy.</li>
* <li>{@code false} if {@code newContent} should be used instead.</li>
* </ul>
* @param newContent
* <ul>
* <li>if {@code copyContent} is false then this will be used in the copy's content.</li>
* <li>if {@code null} then a default buffer of 0 size will be selected</li>
* </ul>
* @return A copy of this object
*/
private FullHttpResponse copy(boolean copyContent, ByteBuf newContent) {
DefaultFullHttpResponse copy = new DefaultFullHttpResponse(
getProtocolVersion(), getStatus(), content().copy());
protocolVersion(), status(),
copyContent ? content().copy() :
newContent == null ? Unpooled.buffer(0) : newContent);
copy.headers().set(headers());
copy.trailingHeaders().set(trailingHeaders());
return copy;
}
@Override
public FullHttpResponse copy(ByteBuf newContent) {
return copy(false, newContent);
}
@Override
public FullHttpResponse copy() {
return copy(true, null);
}
@Override
public FullHttpResponse duplicate() {
DefaultFullHttpResponse duplicate = new DefaultFullHttpResponse(getProtocolVersion(), getStatus(),

View File

@ -16,6 +16,7 @@
package io.netty.handler.codec.http;
import io.netty.channel.ChannelHandlerAppender;
import io.netty.channel.ChannelHandlerContext;
/**
@ -24,7 +25,8 @@ import io.netty.channel.ChannelHandlerAppender;
*
* @see HttpClientCodec
*/
public final class HttpServerCodec extends ChannelHandlerAppender {
public final class HttpServerCodec extends ChannelHandlerAppender implements
HttpServerUpgradeHandler.SourceCodec {
/**
* Creates a new instance with the default decoder options
@ -50,6 +52,16 @@ public final class HttpServerCodec extends ChannelHandlerAppender {
new HttpResponseEncoder());
}
/**
* Upgrades to another protocol from HTTP. Removes the {@link HttpRequestDecoder} and
* {@link HttpResponseEncoder} from the pipeline.
*/
@Override
public void upgradeFrom(ChannelHandlerContext ctx) {
ctx.pipeline().remove(HttpRequestDecoder.class);
ctx.pipeline().remove(HttpResponseEncoder.class);
}
/**
* Returns the encoder of this codec.
*/

View File

@ -0,0 +1,372 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.AsciiString;
import io.netty.util.ReferenceCountUtil;
import io.netty.util.ReferenceCounted;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import static io.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
/**
* A server-side handler that receives HTTP requests and optionally performs a protocol switch if
* the requested protocol is supported. Once an upgrade is performed, this handler removes itself
* from the pipeline.
*/
public class HttpServerUpgradeHandler extends HttpObjectAggregator {
/**
* The source codec that is used in the pipeline initially.
*/
public interface SourceCodec {
/**
* Removes this codec (i.e. all associated handlers) from the pipeline.
*/
void upgradeFrom(ChannelHandlerContext ctx);
}
/**
* A codec that the source can be upgraded to.
*/
public interface UpgradeCodec {
/**
* Returns the name of the protocol supported by this codec, as indicated by the
* {@link HttpHeaderNames#UPGRADE} header.
*/
String protocol();
/**
* Gets all protocol-specific headers required by this protocol for a successful upgrade.
* Any supplied header will be required to appear in the {@link HttpHeaderNames#CONNECTION} header as well.
*/
Collection<String> requiredUpgradeHeaders();
/**
* Adds any headers to the 101 Switching protocols response that are appropriate for this protocol.
*/
void prepareUpgradeResponse(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest,
FullHttpResponse upgradeResponse);
/**
* Performs an HTTP protocol upgrade from the source codec. This method is responsible for
* adding all handlers required for the new protocol.
*
* @param ctx the context for the current handler.
* @param upgradeRequest the request that triggered the upgrade to this protocol. The
* upgraded protocol is responsible for sending the response.
* @param upgradeResponse a 101 Switching Protocols response that is populated with the
* {@link HttpHeaderNames#CONNECTION} and {@link HttpHeaderNames#UPGRADE} headers.
* The protocol is required to send this before sending any other frames back to the client.
* The headers may be augmented as necessary by the protocol before sending.
*/
void upgradeTo(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest, FullHttpResponse upgradeResponse);
}
/**
* User event that is fired to notify about the completion of an HTTP upgrade
* to another protocol. Contains the original upgrade request so that the response
* (if required) can be sent using the new protocol.
*/
public static final class UpgradeEvent implements ReferenceCounted {
private final String protocol;
private final FullHttpRequest upgradeRequest;
private UpgradeEvent(String protocol, FullHttpRequest upgradeRequest) {
this.protocol = protocol;
this.upgradeRequest = upgradeRequest;
}
/**
* The protocol that the channel has been upgraded to.
*/
public String protocol() {
return protocol;
}
/**
* Gets the request that triggered the protocol upgrade.
*/
public FullHttpRequest upgradeRequest() {
return upgradeRequest;
}
@Override
public int refCnt() {
return upgradeRequest.refCnt();
}
@Override
public UpgradeEvent retain() {
upgradeRequest.retain();
return this;
}
@Override
public UpgradeEvent retain(int increment) {
upgradeRequest.retain(increment);
return this;
}
@Override
public UpgradeEvent touch() {
upgradeRequest.touch();
return this;
}
@Override
public UpgradeEvent touch(Object hint) {
upgradeRequest.touch(hint);
return this;
}
@Override
public boolean release() {
return upgradeRequest.release();
}
@Override
public boolean release(int decrement) {
return upgradeRequest.release();
}
@Override
public String toString() {
return "UpgradeEvent [protocol=" + protocol + ", upgradeRequest=" + upgradeRequest + ']';
}
}
private final Map<String, UpgradeCodec> upgradeCodecMap;
private final SourceCodec sourceCodec;
private boolean handlingUpgrade;
/**
* Constructs the upgrader with the supported codecs.
*
* @param sourceCodec the codec that is being used initially.
* @param upgradeCodecs the codecs (in order of preference) that this server supports
* upgrading to from the source codec.
* @param maxContentLength the maximum length of the aggregated content.
*/
public HttpServerUpgradeHandler(SourceCodec sourceCodec,
Collection<UpgradeCodec> upgradeCodecs, int maxContentLength) {
super(maxContentLength);
if (sourceCodec == null) {
throw new NullPointerException("sourceCodec");
}
if (upgradeCodecs == null) {
throw new NullPointerException("upgradeCodecs");
}
this.sourceCodec = sourceCodec;
upgradeCodecMap = new LinkedHashMap<String, UpgradeCodec>(upgradeCodecs.size());
for (UpgradeCodec upgradeCodec : upgradeCodecs) {
String name = upgradeCodec.protocol().toUpperCase(Locale.US);
upgradeCodecMap.put(name, upgradeCodec);
}
}
@Override
protected void decode(ChannelHandlerContext ctx, HttpObject msg, List<Object> out)
throws Exception {
// Determine if we're already handling an upgrade request or just starting a new one.
handlingUpgrade |= isUpgradeRequest(msg);
if (!handlingUpgrade) {
// Not handling an upgrade request, just pass it to the next handler.
ReferenceCountUtil.retain(msg);
out.add(msg);
return;
}
FullHttpRequest fullRequest;
if (msg instanceof FullHttpRequest) {
fullRequest = (FullHttpRequest) msg;
ReferenceCountUtil.retain(msg);
out.add(msg);
} else {
// Call the base class to handle the aggregation of the full request.
super.decode(ctx, msg, out);
if (out.isEmpty()) {
// The full request hasn't been created yet, still awaiting more data.
return;
}
// Finished aggregating the full request, get it from the output list.
assert out.size() == 1;
handlingUpgrade = false;
fullRequest = (FullHttpRequest) out.get(0);
}
if (upgrade(ctx, fullRequest)) {
// The upgrade was successful, remove the message from the output list
// so that it's not propagated to the next handler. This request will
// be propagated as a user event instead.
out.clear();
}
// The upgrade did not succeed, just allow the full request to propagate to the
// next handler.
}
/**
* Determines whether or not the message is an HTTP upgrade request.
*/
private static boolean isUpgradeRequest(HttpObject msg) {
return msg instanceof HttpRequest && ((HttpRequest) msg).headers().get(HttpHeaderNames.UPGRADE) != null;
}
/**
* Attempts to upgrade to the protocol(s) identified by the {@link HttpHeaderNames#UPGRADE} header (if provided
* in the request).
*
* @param ctx the context for this handler.
* @param request the HTTP request.
* @return {@code true} if the upgrade occurred, otherwise {@code false}.
*/
private boolean upgrade(final ChannelHandlerContext ctx, final FullHttpRequest request) {
// Select the best protocol based on those requested in the UPGRADE header.
CharSequence upgradeHeader = request.headers().get(HttpHeaderNames.UPGRADE);
final UpgradeCodec upgradeCodec = selectUpgradeCodec(upgradeHeader);
if (upgradeCodec == null) {
// None of the requested protocols are supported, don't upgrade.
return false;
}
// Make sure the CONNECTION header is present.
CharSequence connectionHeader = request.headers().get(HttpHeaderNames.CONNECTION);
if (connectionHeader == null) {
return false;
}
// Make sure the CONNECTION header contains UPGRADE as well as all protocol-specific headers.
Collection<String> requiredHeaders = upgradeCodec.requiredUpgradeHeaders();
Set<CharSequence> values = splitHeader(connectionHeader);
if (!values.contains(HttpHeaderNames.UPGRADE) || !values.containsAll(requiredHeaders)) {
return false;
}
// Ensure that all required protocol-specific headers are found in the request.
for (String requiredHeader : requiredHeaders) {
if (!request.headers().contains(requiredHeader)) {
return false;
}
}
// Create the user event to be fired once the upgrade completes.
final UpgradeEvent event = new UpgradeEvent(upgradeCodec.protocol(), request);
// Prepare and send the upgrade response. Wait for this write to complete before upgrading,
// since we need the old codec in-place to properly encode the response.
final FullHttpResponse upgradeResponse = createUpgradeResponse(upgradeCodec);
upgradeCodec.prepareUpgradeResponse(ctx, request, upgradeResponse);
ctx.writeAndFlush(upgradeResponse).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
try {
if (future.isSuccess()) {
// Perform the upgrade to the new protocol.
sourceCodec.upgradeFrom(ctx);
upgradeCodec.upgradeTo(ctx, request, upgradeResponse);
// Notify that the upgrade has occurred. Retain the event to offset
// the release() in the finally block.
ctx.fireUserEventTriggered(event.retain());
// Remove this handler from the pipeline.
ctx.pipeline().remove(HttpServerUpgradeHandler.this);
} else {
future.channel().close();
}
} finally {
// Release the event if the upgrade event wasn't fired.
event.release();
}
}
});
return true;
}
/**
* Looks up the most desirable supported upgrade codec from the list of choices in the UPGRADE
* header. If no suitable codec was found, returns {@code null}.
*/
private UpgradeCodec selectUpgradeCodec(CharSequence upgradeHeader) {
Set<CharSequence> requestedProtocols = splitHeader(upgradeHeader);
// Retain only the protocols that are in the protocol map. Maintain the original insertion
// order into the protocolMap, so that the first one in the remaining set is the most
// desirable protocol for the server.
Set<String> supportedProtocols = new LinkedHashSet<String>(upgradeCodecMap.keySet());
supportedProtocols.retainAll(requestedProtocols);
if (!supportedProtocols.isEmpty()) {
String protocol = supportedProtocols.iterator().next().toUpperCase(Locale.US);
return upgradeCodecMap.get(protocol);
}
return null;
}
/**
* Creates the 101 Switching Protocols response message.
*/
private static FullHttpResponse createUpgradeResponse(UpgradeCodec upgradeCodec) {
DefaultFullHttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, SWITCHING_PROTOCOLS);
res.headers().add(HttpHeaderNames.CONNECTION, HttpHeaderValues.UPGRADE);
res.headers().add(HttpHeaderNames.UPGRADE, upgradeCodec.protocol());
res.headers().add(HttpHeaderNames.CONTENT_LENGTH, "0");
return res;
}
/**
* Splits a comma-separated header value. The returned set is case-insensitive and contains each
* part with whitespace removed.
*/
private static Set<CharSequence> splitHeader(CharSequence header) {
StringBuilder builder = new StringBuilder(header.length());
Set<CharSequence> protocols = new TreeSet<CharSequence>(AsciiString.CHARSEQUENCE_CASE_INSENSITIVE_ORDER);
for (int i = 0; i < header.length(); ++i) {
char c = header.charAt(i);
if (Character.isWhitespace(c)) {
// Don't include any whitespace.
continue;
}
if (c == ',') {
// Add the string and reset the builder for the next protocol.
protocols.add(builder.toString());
builder.setLength(0);
} else {
builder.append(c);
}
}
// Add the last protocol
if (builder.length() > 0) {
protocols.add(builder.toString());
}
return protocols;
}
}

View File

@ -1225,15 +1225,41 @@ public class HttpPostRequestEncoder implements ChunkedInput<HttpContent> {
return this;
}
@Override
public FullHttpRequest copy() {
/**
* Copy this object
*
* @param copyContent
* <ul>
* <li>{@code true} if this object's {@link #content()} should be used to copy.</li>
* <li>{@code false} if {@code newContent} should be used instead.</li>
* </ul>
* @param newContent
* <ul>
* <li>if {@code copyContent} is false then this will be used in the copy's content.</li>
* <li>if {@code null} then a default buffer of 0 size will be selected</li>
* </ul>
* @return A copy of this object
*/
private FullHttpRequest copy(boolean copyContent, ByteBuf newContent) {
DefaultFullHttpRequest copy = new DefaultFullHttpRequest(
getProtocolVersion(), getMethod(), getUri(), content().copy());
protocolVersion(), method(), uri(),
copyContent ? content().copy() :
newContent == null ? buffer(0) : newContent);
copy.headers().set(headers());
copy.trailingHeaders().set(trailingHeaders());
return copy;
}
@Override
public FullHttpRequest copy(ByteBuf newContent) {
return copy(false, newContent);
}
@Override
public FullHttpRequest copy() {
return copy(true, null);
}
@Override
public FullHttpRequest duplicate() {
DefaultFullHttpRequest duplicate = new DefaultFullHttpRequest(

54
codec-http2/pom.xml Normal file
View File

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2014 The Netty Project
~
~ The Netty Project licenses this file to you under the Apache License,
~ version 2.0 (the "License"); you may not use this file except in compliance
~ with the License. You may obtain a copy of the License at:
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
~ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
~ License for the specific language governing permissions and limitations
~ under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.netty</groupId>
<artifactId>netty-parent</artifactId>
<version>4.1.0.Beta4-SNAPSHOT</version>
</parent>
<artifactId>netty-codec-http2</artifactId>
<packaging>jar</packaging>
<name>Netty/Codec/HTTP2</name>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>netty-codec-http</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>netty-handler</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.twitter</groupId>
<artifactId>hpack</artifactId>
</dependency>
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jzlib</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,312 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_ENCODING;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaderValues.DEFLATE;
import static io.netty.handler.codec.http.HttpHeaderValues.GZIP;
import static io.netty.handler.codec.http.HttpHeaderValues.IDENTITY;
import static io.netty.handler.codec.http.HttpHeaderValues.X_DEFLATE;
import static io.netty.handler.codec.http.HttpHeaderValues.X_GZIP;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.ChannelPromiseAggregator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.compression.ZlibCodecFactory;
import io.netty.handler.codec.compression.ZlibWrapper;
/**
* A HTTP2 encoder that will compress data frames according to the {@code content-encoding} header for each stream.
* The compression provided by this class will be applied to the data for the entire stream.
*/
public class CompressorHttp2ConnectionEncoder extends DefaultHttp2ConnectionEncoder {
private static final Http2ConnectionAdapter CLEAN_UP_LISTENER = new Http2ConnectionAdapter() {
@Override
public void streamRemoved(Http2Stream stream) {
final EmbeddedChannel compressor = stream.getProperty(CompressorHttp2ConnectionEncoder.class);
if (compressor != null) {
cleanup(stream, compressor);
}
}
};
private final int compressionLevel;
private final int windowBits;
private final int memLevel;
/**
* Builder for new instances of {@link CompressorHttp2ConnectionEncoder}
*/
public static class Builder extends DefaultHttp2ConnectionEncoder.Builder {
protected int compressionLevel = 6;
protected int windowBits = 15;
protected int memLevel = 8;
public Builder compressionLevel(int compressionLevel) {
this.compressionLevel = compressionLevel;
return this;
}
public Builder windowBits(int windowBits) {
this.windowBits = windowBits;
return this;
}
public Builder memLevel(int memLevel) {
this.memLevel = memLevel;
return this;
}
@Override
public CompressorHttp2ConnectionEncoder build() {
return new CompressorHttp2ConnectionEncoder(this);
}
}
protected CompressorHttp2ConnectionEncoder(Builder builder) {
super(builder);
if (builder.compressionLevel < 0 || builder.compressionLevel > 9) {
throw new IllegalArgumentException("compressionLevel: " + builder.compressionLevel + " (expected: 0-9)");
}
if (builder.windowBits < 9 || builder.windowBits > 15) {
throw new IllegalArgumentException("windowBits: " + builder.windowBits + " (expected: 9-15)");
}
if (builder.memLevel < 1 || builder.memLevel > 9) {
throw new IllegalArgumentException("memLevel: " + builder.memLevel + " (expected: 1-9)");
}
compressionLevel = builder.compressionLevel;
windowBits = builder.windowBits;
memLevel = builder.memLevel;
connection().addListener(CLEAN_UP_LISTENER);
}
@Override
public ChannelFuture writeData(final ChannelHandlerContext ctx, final int streamId, ByteBuf data, int padding,
final boolean endOfStream, ChannelPromise promise) {
final Http2Stream stream = connection().stream(streamId);
final EmbeddedChannel channel = stream == null ? null :
(EmbeddedChannel) stream.getProperty(CompressorHttp2ConnectionEncoder.class);
if (channel == null) {
// The compressor may be null if no compatible encoding type was found in this stream's headers
return super.writeData(ctx, streamId, data, padding, endOfStream, promise);
}
try {
// The channel will release the buffer after being written
channel.writeOutbound(data);
ByteBuf buf = nextReadableBuf(channel);
if (buf == null) {
if (endOfStream) {
if (channel.finish()) {
buf = nextReadableBuf(channel);
}
return super.writeData(ctx, streamId, buf == null ? Unpooled.EMPTY_BUFFER : buf, padding,
true, promise);
}
// END_STREAM is not set and the assumption is data is still forthcoming.
promise.setSuccess();
return promise;
}
ChannelPromiseAggregator aggregator = new ChannelPromiseAggregator(promise);
ChannelPromise bufPromise = ctx.newPromise();
aggregator.add(bufPromise);
for (;;) {
ByteBuf nextBuf = nextReadableBuf(channel);
boolean compressedEndOfStream = nextBuf == null && endOfStream;
if (compressedEndOfStream && channel.finish()) {
nextBuf = nextReadableBuf(channel);
compressedEndOfStream = nextBuf == null;
}
final ChannelPromise nextPromise;
if (nextBuf != null) {
// We have to add the nextPromise to the aggregator before doing the current write. This is so
// completing the current write before the next write is done won't complete the aggregate promise
nextPromise = ctx.newPromise();
aggregator.add(nextPromise);
} else {
nextPromise = null;
}
super.writeData(ctx, streamId, buf, padding, compressedEndOfStream, bufPromise);
if (nextBuf == null) {
break;
}
padding = 0; // Padding is only communicated once on the first iteration
buf = nextBuf;
bufPromise = nextPromise;
}
return promise;
} finally {
if (endOfStream) {
cleanup(stream, channel);
}
}
}
@Override
public ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endStream, ChannelPromise promise) {
initCompressor(streamId, headers, endStream);
return super.writeHeaders(ctx, streamId, headers, padding, endStream, promise);
}
@Override
public ChannelFuture writeHeaders(final ChannelHandlerContext ctx, final int streamId, final Http2Headers headers,
final int streamDependency, final short weight, final boolean exclusive, final int padding,
final boolean endOfStream, final ChannelPromise promise) {
initCompressor(streamId, headers, endOfStream);
return super.writeHeaders(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endOfStream,
promise);
}
/**
* Returns a new {@link EmbeddedChannel} that encodes the HTTP2 message content encoded in the specified
* {@code contentEncoding}.
*
* @param contentEncoding the value of the {@code content-encoding} header
* @return a new {@link ByteToMessageDecoder} if the specified encoding is supported. {@code null} otherwise
* (alternatively, you can throw a {@link Http2Exception} to block unknown encoding).
* @throws Http2Exception If the specified encoding is not not supported and warrants an exception
*/
protected EmbeddedChannel newContentCompressor(AsciiString contentEncoding) throws Http2Exception {
if (GZIP.equalsIgnoreCase(contentEncoding) || X_GZIP.equalsIgnoreCase(contentEncoding)) {
return newCompressionChannel(ZlibWrapper.GZIP);
}
if (DEFLATE.equalsIgnoreCase(contentEncoding) || X_DEFLATE.equalsIgnoreCase(contentEncoding)) {
return newCompressionChannel(ZlibWrapper.ZLIB);
}
// 'identity' or unsupported
return null;
}
/**
* Returns the expected content encoding of the decoded content. Returning {@code contentEncoding} is the default
* behavior, which is the case for most compressors.
*
* @param contentEncoding the value of the {@code content-encoding} header
* @return the expected content encoding of the new content.
* @throws Http2Exception if the {@code contentEncoding} is not supported and warrants an exception
*/
protected AsciiString getTargetContentEncoding(AsciiString contentEncoding) throws Http2Exception {
return contentEncoding;
}
/**
* Generate a new instance of an {@link EmbeddedChannel} capable of compressing data
* @param wrapper Defines what type of encoder should be used
*/
private EmbeddedChannel newCompressionChannel(ZlibWrapper wrapper) {
return new EmbeddedChannel(ZlibCodecFactory.newZlibEncoder(wrapper, compressionLevel, windowBits,
memLevel));
}
/**
* Checks if a new compressor object is needed for the stream identified by {@code streamId}. This method will
* modify the {@code content-encoding} header contained in {@code headers}.
*
* @param streamId The identifier for the headers inside {@code headers}
* @param headers Object representing headers which are to be written
* @param endOfStream Indicates if the stream has ended
*/
private void initCompressor(int streamId, Http2Headers headers, boolean endOfStream) {
final Http2Stream stream = connection().stream(streamId);
if (stream == null) {
return;
}
EmbeddedChannel compressor = stream.getProperty(CompressorHttp2ConnectionEncoder.class);
if (compressor == null) {
if (!endOfStream) {
AsciiString encoding = headers.get(CONTENT_ENCODING);
if (encoding == null) {
encoding = IDENTITY;
}
try {
compressor = newContentCompressor(encoding);
if (compressor != null) {
stream.setProperty(CompressorHttp2ConnectionEncoder.class, compressor);
AsciiString targetContentEncoding = getTargetContentEncoding(encoding);
if (IDENTITY.equalsIgnoreCase(targetContentEncoding)) {
headers.remove(CONTENT_ENCODING);
} else {
headers.set(CONTENT_ENCODING, targetContentEncoding);
}
}
} catch (Throwable ignored) {
// Ignore
}
}
} else if (endOfStream) {
cleanup(stream, compressor);
}
if (compressor != null) {
// The content length will be for the decompressed data. Since we will compress the data
// this content-length will not be correct. Instead of queuing messages or delaying sending
// header frames...just remove the content-length header
headers.remove(CONTENT_LENGTH);
}
}
/**
* Release remaining content from {@link EmbeddedChannel} and remove the compressor from the {@link Http2Stream}.
*
* @param stream The stream for which {@code compressor} is the compressor for
* @param compressor The compressor for {@code stream}
*/
private static void cleanup(Http2Stream stream, EmbeddedChannel compressor) {
if (compressor.finish()) {
for (;;) {
final ByteBuf buf = compressor.readOutbound();
if (buf == null) {
break;
}
buf.release();
}
}
stream.removeProperty(CompressorHttp2ConnectionEncoder.class);
}
/**
* Read the next compressed {@link ByteBuf} from the {@link EmbeddedChannel} or {@code null} if one does not exist.
*
* @param compressor The channel to read from
* @return The next decoded {@link ByteBuf} from the {@link EmbeddedChannel} or {@code null} if one does not exist
*/
private static ByteBuf nextReadableBuf(EmbeddedChannel compressor) {
for (;;) {
final ByteBuf buf = compressor.readOutbound();
if (buf == null) {
return null;
}
if (!buf.isReadable()) {
buf.release();
continue;
}
return buf;
}
}
}

View File

@ -0,0 +1,928 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_WEIGHT;
import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_WEIGHT;
import static io.netty.handler.codec.http2.Http2CodecUtil.immediateRemovalPolicy;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.REFUSED_STREAM;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import static io.netty.handler.codec.http2.Http2Exception.streamError;
import static io.netty.handler.codec.http2.Http2Stream.State.CLOSED;
import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_LOCAL;
import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_REMOTE;
import static io.netty.handler.codec.http2.Http2Stream.State.IDLE;
import static io.netty.handler.codec.http2.Http2Stream.State.OPEN;
import static io.netty.handler.codec.http2.Http2Stream.State.RESERVED_LOCAL;
import static io.netty.handler.codec.http2.Http2Stream.State.RESERVED_REMOTE;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.handler.codec.http2.Http2StreamRemovalPolicy.Action;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.collection.IntObjectMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Simple implementation of {@link Http2Connection}.
*/
public class DefaultHttp2Connection implements Http2Connection {
private final Set<Listener> listeners = new HashSet<Listener>(4);
private final IntObjectMap<Http2Stream> streamMap = new IntObjectHashMap<Http2Stream>();
private final ConnectionStream connectionStream = new ConnectionStream();
private final Set<Http2Stream> activeStreams = new LinkedHashSet<Http2Stream>();
private final DefaultEndpoint<Http2LocalFlowController> localEndpoint;
private final DefaultEndpoint<Http2RemoteFlowController> remoteEndpoint;
private final Http2StreamRemovalPolicy removalPolicy;
/**
* Creates a connection with an immediate stream removal policy.
*
* @param server
* whether or not this end-point is the server-side of the HTTP/2 connection.
*/
public DefaultHttp2Connection(boolean server) {
this(server, immediateRemovalPolicy());
}
/**
* Creates a new connection with the given settings.
*
* @param server
* whether or not this end-point is the server-side of the HTTP/2 connection.
* @param removalPolicy
* the policy to be used for removal of closed stream.
*/
public DefaultHttp2Connection(boolean server, Http2StreamRemovalPolicy removalPolicy) {
this.removalPolicy = checkNotNull(removalPolicy, "removalPolicy");
localEndpoint = new DefaultEndpoint<Http2LocalFlowController>(server);
remoteEndpoint = new DefaultEndpoint<Http2RemoteFlowController>(!server);
// Tell the removal policy how to remove a stream from this connection.
removalPolicy.setAction(new Action() {
@Override
public void removeStream(Http2Stream stream) {
DefaultHttp2Connection.this.removeStream((DefaultStream) stream);
}
});
// Add the connection stream to the map.
streamMap.put(connectionStream.id(), connectionStream);
}
@Override
public void addListener(Listener listener) {
listeners.add(listener);
}
@Override
public void removeListener(Listener listener) {
listeners.remove(listener);
}
@Override
public boolean isServer() {
return localEndpoint.isServer();
}
@Override
public Http2Stream connectionStream() {
return connectionStream;
}
@Override
public Http2Stream requireStream(int streamId) throws Http2Exception {
Http2Stream stream = stream(streamId);
if (stream == null) {
throw connectionError(PROTOCOL_ERROR, "Stream does not exist %d", streamId);
}
return stream;
}
@Override
public Http2Stream stream(int streamId) {
return streamMap.get(streamId);
}
@Override
public int numActiveStreams() {
return activeStreams.size();
}
@Override
public Set<Http2Stream> activeStreams() {
return Collections.unmodifiableSet(activeStreams);
}
@Override
public Endpoint<Http2LocalFlowController> local() {
return localEndpoint;
}
@Override
public Endpoint<Http2RemoteFlowController> remote() {
return remoteEndpoint;
}
@Override
public boolean isGoAway() {
return goAwaySent() || goAwayReceived();
}
@Override
public Http2Stream createLocalStream(int streamId) throws Http2Exception {
return local().createStream(streamId);
}
@Override
public Http2Stream createRemoteStream(int streamId) throws Http2Exception {
return remote().createStream(streamId);
}
@Override
public boolean goAwayReceived() {
return localEndpoint.lastKnownStream >= 0;
}
@Override
public void goAwayReceived(int lastKnownStream) {
localEndpoint.lastKnownStream(lastKnownStream);
}
@Override
public boolean goAwaySent() {
return remoteEndpoint.lastKnownStream >= 0;
}
@Override
public void goAwaySent(int lastKnownStream) {
remoteEndpoint.lastKnownStream(lastKnownStream);
}
private void removeStream(DefaultStream stream) {
// Notify the listeners of the event first.
for (Listener listener : listeners) {
listener.streamRemoved(stream);
}
// Remove it from the map and priority tree.
streamMap.remove(stream.id());
stream.parent().removeChild(stream);
}
private void activate(DefaultStream stream) {
if (activeStreams.add(stream)) {
// Update the number of active streams initiated by the endpoint.
stream.createdBy().numActiveStreams++;
// Notify the listeners.
for (Listener listener : listeners) {
listener.streamActive(stream);
}
}
}
/**
* Simple stream implementation. Streams can be compared to each other by priority.
*/
private class DefaultStream implements Http2Stream {
private final int id;
private State state = IDLE;
private short weight = DEFAULT_PRIORITY_WEIGHT;
private DefaultStream parent;
private IntObjectMap<DefaultStream> children = newChildMap();
private int totalChildWeights;
private boolean resetSent;
private boolean resetReceived;
private boolean endOfStreamSent;
private boolean endOfStreamReceived;
private PropertyMap data;
DefaultStream(int id) {
this.id = id;
data = new LazyPropertyMap(this);
}
@Override
public final int id() {
return id;
}
@Override
public final State state() {
return state;
}
@Override
public boolean isEndOfStreamReceived() {
return endOfStreamReceived;
}
@Override
public Http2Stream endOfStreamReceived() {
endOfStreamReceived = true;
return this;
}
@Override
public boolean isEndOfStreamSent() {
return endOfStreamSent;
}
@Override
public Http2Stream endOfStreamSent() {
endOfStreamSent = true;
return this;
}
@Override
public boolean isResetReceived() {
return resetReceived;
}
@Override
public Http2Stream resetReceived() {
resetReceived = true;
return this;
}
@Override
public boolean isResetSent() {
return resetSent;
}
@Override
public Http2Stream resetSent() {
resetSent = true;
return this;
}
@Override
public boolean isReset() {
return resetSent || resetReceived;
}
@Override
public Object setProperty(Object key, Object value) {
return data.put(key, value);
}
@Override
public <V> V getProperty(Object key) {
return data.get(key);
}
@Override
public <V> V removeProperty(Object key) {
return data.remove(key);
}
@Override
public final boolean isRoot() {
return parent == null;
}
@Override
public final short weight() {
return weight;
}
@Override
public final int totalChildWeights() {
return totalChildWeights;
}
@Override
public final DefaultStream parent() {
return parent;
}
@Override
public final boolean isDescendantOf(Http2Stream stream) {
Http2Stream next = parent();
while (next != null) {
if (next == stream) {
return true;
}
next = next.parent();
}
return false;
}
@Override
public final boolean isLeaf() {
return numChildren() == 0;
}
@Override
public final int numChildren() {
return children.size();
}
@Override
public final Collection<? extends Http2Stream> children() {
return children.values();
}
@Override
public final boolean hasChild(int streamId) {
return child(streamId) != null;
}
@Override
public final Http2Stream child(int streamId) {
return children.get(streamId);
}
@Override
public Http2Stream setPriority(int parentStreamId, short weight, boolean exclusive) throws Http2Exception {
if (weight < MIN_WEIGHT || weight > MAX_WEIGHT) {
throw new IllegalArgumentException(String.format(
"Invalid weight: %d. Must be between %d and %d (inclusive).", weight, MIN_WEIGHT, MAX_WEIGHT));
}
DefaultStream newParent = (DefaultStream) stream(parentStreamId);
if (newParent == null) {
// Streams can depend on other streams in the IDLE state. We must ensure
// the stream has been "created" in order to use it in the priority tree.
newParent = createdBy().createStream(parentStreamId);
} else if (this == newParent) {
throw new IllegalArgumentException("A stream cannot depend on itself");
}
// Already have a priority. Re-prioritize the stream.
weight(weight);
if (newParent != parent() || exclusive) {
List<ParentChangedEvent> events;
if (newParent.isDescendantOf(this)) {
events = new ArrayList<ParentChangedEvent>(2 + (exclusive ? newParent.numChildren(): 0));
parent.takeChild(newParent, false, events);
} else {
events = new ArrayList<ParentChangedEvent>(1 + (exclusive ? newParent.numChildren() : 0));
}
newParent.takeChild(this, exclusive, events);
notifyParentChanged(events);
}
return this;
}
@Override
public Http2Stream open(boolean halfClosed) throws Http2Exception {
switch (state) {
case IDLE:
state = halfClosed ? isLocal() ? HALF_CLOSED_LOCAL : HALF_CLOSED_REMOTE : OPEN;
break;
case RESERVED_LOCAL:
state = HALF_CLOSED_REMOTE;
break;
case RESERVED_REMOTE:
state = HALF_CLOSED_LOCAL;
break;
default:
throw streamError(id, PROTOCOL_ERROR, "Attempting to open a stream in an invalid state: " + state);
}
activate(this);
return this;
}
@Override
public Http2Stream close() {
if (state == CLOSED) {
return this;
}
state = CLOSED;
deactivate(this);
// Mark this stream for removal.
removalPolicy.markForRemoval(this);
return this;
}
private void deactivate(DefaultStream stream) {
if (activeStreams.remove(stream)) {
// Update the number of active streams initiated by the endpoint.
stream.createdBy().numActiveStreams--;
// Notify the listeners.
for (Listener listener : listeners) {
listener.streamInactive(stream);
}
}
}
@Override
public Http2Stream closeLocalSide() {
switch (state) {
case OPEN:
state = HALF_CLOSED_LOCAL;
notifyHalfClosed(this);
break;
case HALF_CLOSED_LOCAL:
break;
default:
close();
break;
}
return this;
}
@Override
public Http2Stream closeRemoteSide() {
switch (state) {
case OPEN:
state = HALF_CLOSED_REMOTE;
notifyHalfClosed(this);
break;
case HALF_CLOSED_REMOTE:
break;
default:
close();
break;
}
return this;
}
private void notifyHalfClosed(Http2Stream stream) {
for (Listener listener : listeners) {
listener.streamHalfClosed(stream);
}
}
@Override
public final boolean remoteSideOpen() {
return state == HALF_CLOSED_LOCAL || state == OPEN || state == RESERVED_REMOTE;
}
@Override
public final boolean localSideOpen() {
return state == HALF_CLOSED_REMOTE || state == OPEN || state == RESERVED_LOCAL;
}
final DefaultEndpoint<? extends Http2FlowController> createdBy() {
return localEndpoint.createdStreamId(id) ? localEndpoint : remoteEndpoint;
}
final boolean isLocal() {
return localEndpoint.createdStreamId(id);
}
final void weight(short weight) {
if (weight != this.weight) {
if (parent != null) {
int delta = weight - this.weight;
parent.totalChildWeights += delta;
}
final short oldWeight = this.weight;
this.weight = weight;
for (Listener l : listeners) {
l.onWeightChanged(this, oldWeight);
}
}
}
final IntObjectMap<DefaultStream> removeAllChildren() {
totalChildWeights = 0;
IntObjectMap<DefaultStream> prevChildren = children;
children = newChildMap();
return prevChildren;
}
/**
* Adds a child to this priority. If exclusive is set, any children of this node are moved to being dependent on
* the child.
*/
final void takeChild(DefaultStream child, boolean exclusive, List<ParentChangedEvent> events) {
DefaultStream oldParent = child.parent();
events.add(new ParentChangedEvent(child, oldParent));
notifyParentChanging(child, this);
child.parent = this;
if (exclusive && !children.isEmpty()) {
// If it was requested that this child be the exclusive dependency of this node,
// move any previous children to the child node, becoming grand children
// of this node.
for (DefaultStream grandchild : removeAllChildren().values()) {
child.takeChild(grandchild, false, events);
}
}
if (children.put(child.id(), child) == null) {
totalChildWeights += child.weight();
}
if (oldParent != null && oldParent.children.remove(child.id()) != null) {
oldParent.totalChildWeights -= child.weight();
}
}
/**
* Removes the child priority and moves any of its dependencies to being direct dependencies on this node.
*/
final void removeChild(DefaultStream child) {
if (children.remove(child.id()) != null) {
List<ParentChangedEvent> events = new ArrayList<ParentChangedEvent>(1 + child.children.size());
events.add(new ParentChangedEvent(child, child.parent()));
notifyParentChanging(child, null);
child.parent = null;
totalChildWeights -= child.weight();
// Move up any grand children to be directly dependent on this node.
for (DefaultStream grandchild : child.children.values()) {
takeChild(grandchild, false, events);
}
notifyParentChanged(events);
}
}
}
/**
* Allows the data map to be lazily initialized for {@link DefaultStream}.
*/
private interface PropertyMap {
Object put(Object key, Object value);
<V> V get(Object key);
<V> V remove(Object key);
}
/**
* Provides actual {@link HashMap} functionality for {@link DefaultStream}'s application data.
*/
private static final class DefaultProperyMap implements PropertyMap {
private final Map<Object, Object> data;
DefaultProperyMap(int initialSize) {
data = new HashMap<Object, Object>(initialSize);
}
@Override
public Object put(Object key, Object value) {
return data.put(key, value);
}
@SuppressWarnings("unchecked")
@Override
public <V> V get(Object key) {
return (V) data.get(key);
}
@SuppressWarnings("unchecked")
@Override
public <V> V remove(Object key) {
return (V) data.remove(key);
}
}
/**
* Provides the lazy initialization for the {@link DefaultStream} data map.
*/
private static final class LazyPropertyMap implements PropertyMap {
private static final int DEFAULT_INITIAL_SIZE = 4;
private final DefaultStream stream;
LazyPropertyMap(DefaultStream stream) {
this.stream = stream;
}
@Override
public Object put(Object key, Object value) {
stream.data = new DefaultProperyMap(DEFAULT_INITIAL_SIZE);
return stream.data.put(key, value);
}
@Override
public <V> V get(Object key) {
stream.data = new DefaultProperyMap(DEFAULT_INITIAL_SIZE);
return stream.data.get(key);
}
@Override
public <V> V remove(Object key) {
stream.data = new DefaultProperyMap(DEFAULT_INITIAL_SIZE);
return stream.data.remove(key);
}
}
private static IntObjectMap<DefaultStream> newChildMap() {
return new IntObjectHashMap<DefaultStream>(4);
}
/**
* Allows a correlation to be made between a stream and its old parent before a parent change occurs
*/
private static final class ParentChangedEvent {
private final Http2Stream stream;
private final Http2Stream oldParent;
/**
* Create a new instance
* @param stream The stream who has had a parent change
* @param oldParent The previous parent
*/
ParentChangedEvent(Http2Stream stream, Http2Stream oldParent) {
this.stream = stream;
this.oldParent = oldParent;
}
/**
* Notify all listeners of the tree change event
* @param l The listener to notify
*/
public void notifyListener(Listener l) {
l.priorityTreeParentChanged(stream, oldParent);
}
}
/**
* Notify all listeners of the priority tree change events (in ascending order)
* @param events The events (top down order) which have changed
*/
private void notifyParentChanged(List<ParentChangedEvent> events) {
for (int i = 0; i < events.size(); ++i) {
ParentChangedEvent event = events.get(i);
for (Listener l : listeners) {
event.notifyListener(l);
}
}
}
private void notifyParentChanging(Http2Stream stream, Http2Stream newParent) {
for (Listener l : listeners) {
l.priorityTreeParentChanging(stream, newParent);
}
}
/**
* Stream class representing the connection, itself.
*/
private final class ConnectionStream extends DefaultStream {
ConnectionStream() {
super(CONNECTION_STREAM_ID);
}
@Override
public Http2Stream setPriority(int parentStreamId, short weight, boolean exclusive) {
throw new UnsupportedOperationException();
}
@Override
public Http2Stream open(boolean halfClosed) {
throw new UnsupportedOperationException();
}
@Override
public Http2Stream close() {
throw new UnsupportedOperationException();
}
@Override
public Http2Stream closeLocalSide() {
throw new UnsupportedOperationException();
}
@Override
public Http2Stream closeRemoteSide() {
throw new UnsupportedOperationException();
}
}
/**
* Simple endpoint implementation.
*/
private final class DefaultEndpoint<F extends Http2FlowController> implements Endpoint<F> {
private final boolean server;
private int nextStreamId;
private int lastStreamCreated;
private int lastKnownStream = -1;
private boolean pushToAllowed = true;
private F flowController;
/**
* The maximum number of active streams allowed to be created by this endpoint.
*/
private int maxStreams;
/**
* The current number of active streams created by this endpoint.
*/
private int numActiveStreams;
DefaultEndpoint(boolean server) {
this.server = server;
// Determine the starting stream ID for this endpoint. Client-initiated streams
// are odd and server-initiated streams are even. Zero is reserved for the
// connection. Stream 1 is reserved client-initiated stream for responding to an
// upgrade from HTTP 1.1.
nextStreamId = server ? 2 : 1;
// Push is disallowed by default for servers and allowed for clients.
pushToAllowed = !server;
maxStreams = Integer.MAX_VALUE;
}
@Override
public int nextStreamId() {
// For manually created client-side streams, 1 is reserved for HTTP upgrade, so
// start at 3.
return nextStreamId > 1 ? nextStreamId : nextStreamId + 2;
}
@Override
public boolean createdStreamId(int streamId) {
boolean even = (streamId & 1) == 0;
return server == even;
}
@Override
public boolean acceptingNewStreams() {
return nextStreamId() > 0 && numActiveStreams + 1 <= maxStreams;
}
@Override
public DefaultStream createStream(int streamId) throws Http2Exception {
checkNewStreamAllowed(streamId);
// Create and initialize the stream.
DefaultStream stream = new DefaultStream(streamId);
// Update the next and last stream IDs.
nextStreamId = streamId + 2;
lastStreamCreated = streamId;
addStream(stream);
return stream;
}
@Override
public boolean isServer() {
return server;
}
@Override
public DefaultStream reservePushStream(int streamId, Http2Stream parent) throws Http2Exception {
if (parent == null) {
throw connectionError(PROTOCOL_ERROR, "Parent stream missing");
}
if (isLocal() ? !parent.localSideOpen() : !parent.remoteSideOpen()) {
throw connectionError(PROTOCOL_ERROR, "Stream %d is not open for sending push promise", parent.id());
}
if (!opposite().allowPushTo()) {
throw connectionError(PROTOCOL_ERROR, "Server push not allowed to opposite endpoint.");
}
checkNewStreamAllowed(streamId);
// Create and initialize the stream.
DefaultStream stream = new DefaultStream(streamId);
stream.state = isLocal() ? RESERVED_LOCAL : RESERVED_REMOTE;
// Update the next and last stream IDs.
nextStreamId = streamId + 2;
lastStreamCreated = streamId;
// Register the stream.
addStream(stream);
return stream;
}
private void addStream(DefaultStream stream) {
// Add the stream to the map and priority tree.
streamMap.put(stream.id(), stream);
List<ParentChangedEvent> events = new ArrayList<ParentChangedEvent>(1);
connectionStream.takeChild(stream, false, events);
// Notify the listeners of the event.
for (Listener listener : listeners) {
listener.streamAdded(stream);
}
notifyParentChanged(events);
}
@Override
public void allowPushTo(boolean allow) {
if (allow && server) {
throw new IllegalArgumentException("Servers do not allow push");
}
pushToAllowed = allow;
}
@Override
public boolean allowPushTo() {
return pushToAllowed;
}
@Override
public int numActiveStreams() {
return numActiveStreams;
}
@Override
public int maxStreams() {
return maxStreams;
}
@Override
public void maxStreams(int maxStreams) {
this.maxStreams = maxStreams;
}
@Override
public int lastStreamCreated() {
return lastStreamCreated;
}
@Override
public int lastKnownStream() {
return lastKnownStream >= 0 ? lastKnownStream : lastStreamCreated;
}
private void lastKnownStream(int lastKnownStream) {
boolean alreadyNotified = isGoAway();
this.lastKnownStream = lastKnownStream;
if (!alreadyNotified) {
notifyGoingAway();
}
}
private void notifyGoingAway() {
for (Listener listener : listeners) {
listener.goingAway();
}
}
@Override
public F flowController() {
return flowController;
}
@Override
public void flowController(F flowController) {
this.flowController = checkNotNull(flowController, "flowController");
}
@Override
public Endpoint<? extends Http2FlowController> opposite() {
return isLocal() ? remoteEndpoint : localEndpoint;
}
private void checkNewStreamAllowed(int streamId) throws Http2Exception {
if (isGoAway()) {
throw connectionError(PROTOCOL_ERROR, "Cannot create a stream since the connection is going away");
}
verifyStreamId(streamId);
if (!acceptingNewStreams()) {
throw connectionError(REFUSED_STREAM, "Maximum streams exceeded for this endpoint.");
}
}
private void verifyStreamId(int streamId) throws Http2Exception {
if (streamId < 0) {
throw new Http2NoMoreStreamIdsException();
}
if (streamId < nextStreamId) {
throw connectionError(PROTOCOL_ERROR, "Request stream %d is behind the next expected stream %d",
streamId, nextStreamId);
}
if (!createdStreamId(streamId)) {
throw connectionError(PROTOCOL_ERROR, "Request stream %d is not correct for %s connection",
streamId, server ? "server" : "client");
}
}
private boolean isLocal() {
return this == localEndpoint;
}
}
}

View File

@ -0,0 +1,603 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.STREAM_CLOSED;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import static io.netty.handler.codec.http2.Http2Exception.streamError;
import static io.netty.handler.codec.http2.Http2Stream.State.CLOSED;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import java.util.List;
/**
* Provides the default implementation for processing inbound frame events
* and delegates to a {@link Http2FrameListener}
* <p>
* This class will read HTTP/2 frames and delegate the events to a {@link Http2FrameListener}
* <p>
* This interface enforces inbound flow control functionality through {@link Http2InboundFlowController}
*/
public class DefaultHttp2ConnectionDecoder implements Http2ConnectionDecoder {
private final Http2FrameListener internalFrameListener = new FrameReadListener();
private final Http2Connection connection;
private final Http2LifecycleManager lifecycleManager;
private final Http2ConnectionEncoder encoder;
private final Http2FrameReader frameReader;
private final Http2FrameListener listener;
private boolean prefaceReceived;
/**
* Builder for instances of {@link DefaultHttp2ConnectionDecoder}.
*/
public static class Builder implements Http2ConnectionDecoder.Builder {
private Http2Connection connection;
private Http2LifecycleManager lifecycleManager;
private Http2ConnectionEncoder encoder;
private Http2FrameReader frameReader;
private Http2FrameListener listener;
@Override
public Builder connection(Http2Connection connection) {
this.connection = connection;
return this;
}
@Override
public Builder lifecycleManager(Http2LifecycleManager lifecycleManager) {
this.lifecycleManager = lifecycleManager;
return this;
}
@Override
public Http2LifecycleManager lifecycleManager() {
return lifecycleManager;
}
@Override
public Builder frameReader(Http2FrameReader frameReader) {
this.frameReader = frameReader;
return this;
}
@Override
public Builder listener(Http2FrameListener listener) {
this.listener = listener;
return this;
}
@Override
public Builder encoder(Http2ConnectionEncoder encoder) {
this.encoder = encoder;
return this;
}
@Override
public Http2ConnectionDecoder build() {
return new DefaultHttp2ConnectionDecoder(this);
}
}
public static Builder newBuilder() {
return new Builder();
}
protected DefaultHttp2ConnectionDecoder(Builder builder) {
connection = checkNotNull(builder.connection, "connection");
frameReader = checkNotNull(builder.frameReader, "frameReader");
lifecycleManager = checkNotNull(builder.lifecycleManager, "lifecycleManager");
encoder = checkNotNull(builder.encoder, "encoder");
listener = checkNotNull(builder.listener, "listener");
if (connection.local().flowController() == null) {
connection.local().flowController(
new DefaultHttp2LocalFlowController(connection, encoder.frameWriter()));
}
}
@Override
public Http2Connection connection() {
return connection;
}
@Override
public final Http2LocalFlowController flowController() {
return connection.local().flowController();
}
@Override
public Http2FrameListener listener() {
return listener;
}
@Override
public boolean prefaceReceived() {
return prefaceReceived;
}
@Override
public void decodeFrame(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Http2Exception {
frameReader.readFrame(ctx, in, internalFrameListener);
}
@Override
public Http2Settings localSettings() {
Http2Settings settings = new Http2Settings();
Http2FrameReader.Configuration config = frameReader.configuration();
Http2HeaderTable headerTable = config.headerTable();
Http2FrameSizePolicy frameSizePolicy = config.frameSizePolicy();
settings.initialWindowSize(flowController().initialWindowSize());
settings.maxConcurrentStreams(connection.remote().maxStreams());
settings.headerTableSize(headerTable.maxHeaderTableSize());
settings.maxFrameSize(frameSizePolicy.maxFrameSize());
settings.maxHeaderListSize(headerTable.maxHeaderListSize());
if (!connection.isServer()) {
// Only set the pushEnabled flag if this is a client endpoint.
settings.pushEnabled(connection.local().allowPushTo());
}
return settings;
}
@Override
public void localSettings(Http2Settings settings) throws Http2Exception {
Boolean pushEnabled = settings.pushEnabled();
Http2FrameReader.Configuration config = frameReader.configuration();
Http2HeaderTable inboundHeaderTable = config.headerTable();
Http2FrameSizePolicy inboundFrameSizePolicy = config.frameSizePolicy();
if (pushEnabled != null) {
if (connection.isServer()) {
throw connectionError(PROTOCOL_ERROR, "Server sending SETTINGS frame with ENABLE_PUSH specified");
}
connection.local().allowPushTo(pushEnabled);
}
Long maxConcurrentStreams = settings.maxConcurrentStreams();
if (maxConcurrentStreams != null) {
int value = (int) Math.min(maxConcurrentStreams, Integer.MAX_VALUE);
connection.remote().maxStreams(value);
}
Long headerTableSize = settings.headerTableSize();
if (headerTableSize != null) {
inboundHeaderTable.maxHeaderTableSize((int) Math.min(headerTableSize, Integer.MAX_VALUE));
}
Integer maxHeaderListSize = settings.maxHeaderListSize();
if (maxHeaderListSize != null) {
inboundHeaderTable.maxHeaderListSize(maxHeaderListSize);
}
Integer maxFrameSize = settings.maxFrameSize();
if (maxFrameSize != null) {
inboundFrameSizePolicy.maxFrameSize(maxFrameSize);
}
Integer initialWindowSize = settings.initialWindowSize();
if (initialWindowSize != null) {
flowController().initialWindowSize(initialWindowSize);
}
}
@Override
public void close() {
frameReader.close();
}
private int unconsumedBytes(Http2Stream stream) {
return flowController().unconsumedBytes(stream);
}
/**
* Handles all inbound frames from the network.
*/
private final class FrameReadListener implements Http2FrameListener {
@Override
public int onDataRead(final ChannelHandlerContext ctx, int streamId, ByteBuf data,
int padding, boolean endOfStream) throws Http2Exception {
verifyPrefaceReceived();
// Check if we received a data frame for a stream which is half-closed
Http2Stream stream = connection.requireStream(streamId);
verifyEndOfStreamNotReceived(stream);
verifyGoAwayNotReceived();
verifyRstStreamNotReceived(stream);
// We should ignore this frame if RST_STREAM was sent or if GO_AWAY was sent with a
// lower stream ID.
boolean shouldApplyFlowControl = false;
boolean shouldIgnore = shouldIgnoreFrame(stream, false);
Http2Exception error = null;
switch (stream.state()) {
case OPEN:
case HALF_CLOSED_LOCAL:
shouldApplyFlowControl = true;
break;
case HALF_CLOSED_REMOTE:
case CLOSED:
if (stream.isResetSent()) {
shouldApplyFlowControl = true;
}
if (!shouldIgnore) {
error = streamError(stream.id(), STREAM_CLOSED, "Stream %d in unexpected state: %s",
stream.id(), stream.state());
}
break;
default:
if (!shouldIgnore) {
error = streamError(stream.id(), PROTOCOL_ERROR,
"Stream %d in unexpected state: %s", stream.id(), stream.state());
}
break;
}
int bytesToReturn = data.readableBytes() + padding;
int unconsumedBytes = unconsumedBytes(stream);
Http2LocalFlowController flowController = flowController();
try {
// If we should apply flow control, do so now.
if (shouldApplyFlowControl) {
flowController.receiveFlowControlledFrame(ctx, stream, data, padding, endOfStream);
// Update the unconsumed bytes after flow control is applied.
unconsumedBytes = unconsumedBytes(stream);
}
// If we should ignore this frame, do so now.
if (shouldIgnore) {
return bytesToReturn;
}
// If the stream was in an invalid state to receive the frame, throw the error.
if (error != null) {
throw error;
}
// Call back the application and retrieve the number of bytes that have been
// immediately processed.
bytesToReturn = listener.onDataRead(ctx, streamId, data, padding, endOfStream);
return bytesToReturn;
} catch (Http2Exception e) {
// If an exception happened during delivery, the listener may have returned part
// of the bytes before the error occurred. If that's the case, subtract that from
// the total processed bytes so that we don't return too many bytes.
int delta = unconsumedBytes - unconsumedBytes(stream);
bytesToReturn -= delta;
throw e;
} catch (RuntimeException e) {
// If an exception happened during delivery, the listener may have returned part
// of the bytes before the error occurred. If that's the case, subtract that from
// the total processed bytes so that we don't return too many bytes.
int delta = unconsumedBytes - unconsumedBytes(stream);
bytesToReturn -= delta;
throw e;
} finally {
// If appropriate, returned the processed bytes to the flow controller.
if (shouldApplyFlowControl && bytesToReturn > 0) {
flowController.consumeBytes(ctx, stream, bytesToReturn);
}
if (endOfStream) {
stream.endOfStreamReceived();
lifecycleManager.closeRemoteSide(stream, ctx.newSucceededFuture());
}
}
}
/**
* Verifies that the HTTP/2 connection preface has been received from the remote endpoint.
*/
private void verifyPrefaceReceived() throws Http2Exception {
if (!prefaceReceived) {
throw connectionError(PROTOCOL_ERROR, "Received non-SETTINGS as first frame.");
}
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endOfStream) throws Http2Exception {
onHeadersRead(ctx, streamId, headers, 0, DEFAULT_PRIORITY_WEIGHT, false, padding, endOfStream);
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception {
verifyPrefaceReceived();
Http2Stream stream = connection.stream(streamId);
verifyGoAwayNotReceived();
verifyRstStreamNotReceived(stream);
if (shouldIgnoreFrame(stream, false)) {
// Ignore this frame.
return;
}
if (stream == null) {
stream = connection.createRemoteStream(streamId).open(endOfStream);
} else {
verifyEndOfStreamNotReceived(stream);
switch (stream.state()) {
case RESERVED_REMOTE:
case IDLE:
stream.open(endOfStream);
break;
case OPEN:
case HALF_CLOSED_LOCAL:
// Allowed to receive headers in these states.
break;
case HALF_CLOSED_REMOTE:
case CLOSED:
// Stream error.
throw streamError(stream.id(), STREAM_CLOSED, "Stream %d in unexpected state: %s",
stream.id(), stream.state());
default:
// Connection error.
throw connectionError(PROTOCOL_ERROR, "Stream %d in unexpected state: %s", stream.id(),
stream.state());
}
}
listener.onHeadersRead(ctx, streamId, headers,
streamDependency, weight, exclusive, padding, endOfStream);
stream.setPriority(streamDependency, weight, exclusive);
// If the headers completes this stream, close it.
if (endOfStream) {
stream.endOfStreamReceived();
lifecycleManager.closeRemoteSide(stream, ctx.newSucceededFuture());
}
}
@Override
public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight,
boolean exclusive) throws Http2Exception {
verifyPrefaceReceived();
Http2Stream stream = connection.stream(streamId);
verifyGoAwayNotReceived();
if (shouldIgnoreFrame(stream, true)) {
// Ignore this frame.
return;
}
if (stream == null) {
// PRIORITY frames always identify a stream. This means that if a PRIORITY frame is the
// first frame to be received for a stream that we must create the stream.
stream = connection.createRemoteStream(streamId);
}
// This call will create a stream for streamDependency if necessary.
// For this reason it must be done before notifying the listener.
stream.setPriority(streamDependency, weight, exclusive);
listener.onPriorityRead(ctx, streamId, streamDependency, weight, exclusive);
}
@Override
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception {
verifyPrefaceReceived();
Http2Stream stream = connection.requireStream(streamId);
verifyRstStreamNotReceived(stream);
if (stream.state() == CLOSED) {
// RstStream frames must be ignored for closed streams.
return;
}
stream.resetReceived();
listener.onRstStreamRead(ctx, streamId, errorCode);
lifecycleManager.closeStream(stream, ctx.newSucceededFuture());
}
@Override
public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception {
verifyPrefaceReceived();
// Apply oldest outstanding local settings here. This is a synchronization point
// between endpoints.
Http2Settings settings = encoder.pollSentSettings();
if (settings != null) {
applyLocalSettings(settings);
}
listener.onSettingsAckRead(ctx);
}
/**
* Applies settings sent from the local endpoint.
*/
private void applyLocalSettings(Http2Settings settings) throws Http2Exception {
Boolean pushEnabled = settings.pushEnabled();
final Http2FrameReader.Configuration config = frameReader.configuration();
final Http2HeaderTable headerTable = config.headerTable();
final Http2FrameSizePolicy frameSizePolicy = config.frameSizePolicy();
if (pushEnabled != null) {
if (connection.isServer()) {
throw connectionError(PROTOCOL_ERROR, "Server sending SETTINGS frame with ENABLE_PUSH specified");
}
connection.local().allowPushTo(pushEnabled);
}
Long maxConcurrentStreams = settings.maxConcurrentStreams();
if (maxConcurrentStreams != null) {
int value = (int) Math.min(maxConcurrentStreams, Integer.MAX_VALUE);
connection.remote().maxStreams(value);
}
Long headerTableSize = settings.headerTableSize();
if (headerTableSize != null) {
headerTable.maxHeaderTableSize((int) Math.min(headerTableSize, Integer.MAX_VALUE));
}
Integer maxHeaderListSize = settings.maxHeaderListSize();
if (maxHeaderListSize != null) {
headerTable.maxHeaderListSize(maxHeaderListSize);
}
Integer maxFrameSize = settings.maxFrameSize();
if (maxFrameSize != null) {
frameSizePolicy.maxFrameSize(maxFrameSize);
}
Integer initialWindowSize = settings.initialWindowSize();
if (initialWindowSize != null) {
flowController().initialWindowSize(initialWindowSize);
}
}
@Override
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception {
encoder.remoteSettings(settings);
// Acknowledge receipt of the settings.
encoder.writeSettingsAck(ctx, ctx.newPromise());
// We've received at least one non-ack settings frame from the remote endpoint.
prefaceReceived = true;
listener.onSettingsRead(ctx, settings);
}
@Override
public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
verifyPrefaceReceived();
// Send an ack back to the remote client.
// Need to retain the buffer here since it will be released after the write completes.
encoder.writePing(ctx, true, data.retain(), ctx.newPromise());
ctx.flush();
listener.onPingRead(ctx, data);
}
@Override
public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
verifyPrefaceReceived();
listener.onPingAckRead(ctx, data);
}
@Override
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
Http2Headers headers, int padding) throws Http2Exception {
verifyPrefaceReceived();
Http2Stream parentStream = connection.requireStream(streamId);
verifyGoAwayNotReceived();
verifyRstStreamNotReceived(parentStream);
if (shouldIgnoreFrame(parentStream, false)) {
// Ignore frames for any stream created after we sent a go-away.
return;
}
// Reserve the push stream based with a priority based on the current stream's priority.
connection.remote().reservePushStream(promisedStreamId, parentStream);
listener.onPushPromiseRead(ctx, streamId, promisedStreamId, headers, padding);
}
@Override
public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData)
throws Http2Exception {
// Don't allow any more connections to be created.
connection.goAwayReceived(lastStreamId);
listener.onGoAwayRead(ctx, lastStreamId, errorCode, debugData);
}
@Override
public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
throws Http2Exception {
verifyPrefaceReceived();
Http2Stream stream = connection.requireStream(streamId);
verifyGoAwayNotReceived();
verifyRstStreamNotReceived(stream);
if (stream.state() == CLOSED || shouldIgnoreFrame(stream, false)) {
// Ignore frames for any stream created after we sent a go-away.
return;
}
// Update the outbound flow controller.
encoder.flowController().incrementWindowSize(ctx, stream, windowSizeIncrement);
listener.onWindowUpdateRead(ctx, streamId, windowSizeIncrement);
}
@Override
public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags,
ByteBuf payload) {
listener.onUnknownFrame(ctx, frameType, streamId, flags, payload);
}
/**
* Indicates whether or not frames for the given stream should be ignored based on the state of the
* stream/connection.
*/
private boolean shouldIgnoreFrame(Http2Stream stream, boolean allowResetSent) {
if (connection.goAwaySent() &&
(stream == null || connection.remote().lastStreamCreated() <= stream.id())) {
// Frames from streams created after we sent a go-away should be ignored.
// Frames for the connection stream ID (i.e. 0) will always be allowed.
return true;
}
// Also ignore inbound frames after we sent a RST_STREAM frame.
return stream != null && !allowResetSent && stream.isResetSent();
}
/**
* Verifies that a frame has not been received from remote endpoint with the
* {@code END_STREAM} flag set. If it was, throws a connection error.
*/
private void verifyEndOfStreamNotReceived(Http2Stream stream) throws Http2Exception {
if (stream.isEndOfStreamReceived()) {
// Connection error.
throw new Http2Exception(STREAM_CLOSED, String.format(
"Received frame for stream %d after receiving END_STREAM", stream.id()));
}
}
/**
* Verifies that a GO_AWAY frame was not previously received from the remote endpoint. If it was, throws a
* connection error.
*/
private void verifyGoAwayNotReceived() throws Http2Exception {
if (connection.goAwayReceived()) {
// Connection error.
throw connectionError(PROTOCOL_ERROR, "Received frames after receiving GO_AWAY");
}
}
/**
* Verifies that a RST_STREAM frame was not previously received for the given stream. If it was, throws a
* stream error.
*/
private void verifyRstStreamNotReceived(Http2Stream stream) throws Http2Exception {
if (stream != null && stream.isResetReceived()) {
// Stream error.
throw streamError(stream.id(), STREAM_CLOSED,
"Frame received after receiving RST_STREAM for stream: " + stream.id());
}
}
}
}

View File

@ -0,0 +1,467 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import java.util.ArrayDeque;
/**
* Default implementation of {@link Http2ConnectionEncoder}.
*/
public class DefaultHttp2ConnectionEncoder implements Http2ConnectionEncoder {
private final Http2FrameWriter frameWriter;
private final Http2Connection connection;
private final Http2LifecycleManager lifecycleManager;
// We prefer ArrayDeque to LinkedList because later will produce more GC.
// This initial capacity is plenty for SETTINGS traffic.
private final ArrayDeque<Http2Settings> outstandingLocalSettingsQueue = new ArrayDeque<Http2Settings>(4);
/**
* Builder for new instances of {@link DefaultHttp2ConnectionEncoder}.
*/
public static class Builder implements Http2ConnectionEncoder.Builder {
protected Http2FrameWriter frameWriter;
protected Http2Connection connection;
protected Http2LifecycleManager lifecycleManager;
@Override
public Builder connection(
Http2Connection connection) {
this.connection = connection;
return this;
}
@Override
public Builder lifecycleManager(
Http2LifecycleManager lifecycleManager) {
this.lifecycleManager = lifecycleManager;
return this;
}
@Override
public Http2LifecycleManager lifecycleManager() {
return lifecycleManager;
}
@Override
public Builder frameWriter(Http2FrameWriter frameWriter) {
this.frameWriter = frameWriter;
return this;
}
@Override
public Http2ConnectionEncoder build() {
return new DefaultHttp2ConnectionEncoder(this);
}
}
public static Builder newBuilder() {
return new Builder();
}
protected DefaultHttp2ConnectionEncoder(Builder builder) {
connection = checkNotNull(builder.connection, "connection");
frameWriter = checkNotNull(builder.frameWriter, "frameWriter");
lifecycleManager = checkNotNull(builder.lifecycleManager, "lifecycleManager");
if (connection.remote().flowController() == null) {
connection.remote().flowController(new DefaultHttp2RemoteFlowController(connection, frameWriter));
}
}
@Override
public Http2FrameWriter frameWriter() {
return frameWriter;
}
@Override
public Http2Connection connection() {
return connection;
}
@Override
public final Http2RemoteFlowController flowController() {
return connection().remote().flowController();
}
@Override
public void remoteSettings(Http2Settings settings) throws Http2Exception {
Boolean pushEnabled = settings.pushEnabled();
Http2FrameWriter.Configuration config = configuration();
Http2HeaderTable outboundHeaderTable = config.headerTable();
Http2FrameSizePolicy outboundFrameSizePolicy = config.frameSizePolicy();
if (pushEnabled != null) {
if (!connection.isServer()) {
throw connectionError(PROTOCOL_ERROR, "Client received SETTINGS frame with ENABLE_PUSH specified");
}
connection.remote().allowPushTo(pushEnabled);
}
Long maxConcurrentStreams = settings.maxConcurrentStreams();
if (maxConcurrentStreams != null) {
connection.local().maxStreams((int) Math.min(maxConcurrentStreams, Integer.MAX_VALUE));
}
Long headerTableSize = settings.headerTableSize();
if (headerTableSize != null) {
outboundHeaderTable.maxHeaderTableSize((int) Math.min(headerTableSize, Integer.MAX_VALUE));
}
Integer maxHeaderListSize = settings.maxHeaderListSize();
if (maxHeaderListSize != null) {
outboundHeaderTable.maxHeaderListSize(maxHeaderListSize);
}
Integer maxFrameSize = settings.maxFrameSize();
if (maxFrameSize != null) {
outboundFrameSizePolicy.maxFrameSize(maxFrameSize);
}
Integer initialWindowSize = settings.initialWindowSize();
if (initialWindowSize != null) {
flowController().initialWindowSize(initialWindowSize);
}
}
@Override
public ChannelFuture writeData(final ChannelHandlerContext ctx, final int streamId, ByteBuf data, int padding,
final boolean endOfStream, ChannelPromise promise) {
Http2Stream stream;
try {
if (connection.isGoAway()) {
throw new IllegalStateException("Sending data after connection going away.");
}
stream = connection.requireStream(streamId);
if (stream.isResetSent()) {
throw new IllegalStateException("Sending data after sending RST_STREAM.");
}
if (stream.isEndOfStreamSent()) {
throw new IllegalStateException("Sending data after sending END_STREAM.");
}
// Verify that the stream is in the appropriate state for sending DATA frames.
switch (stream.state()) {
case OPEN:
case HALF_CLOSED_REMOTE:
// Allowed sending DATA frames in these states.
break;
default:
throw new IllegalStateException(String.format(
"Stream %d in unexpected state: %s", stream.id(), stream.state()));
}
if (endOfStream) {
// Indicate that we have sent END_STREAM.
stream.endOfStreamSent();
}
} catch (Throwable e) {
data.release();
return promise.setFailure(e);
}
// Hand control of the frame to the flow controller.
ChannelFuture future =
flowController().sendFlowControlledFrame(ctx, stream, data, padding, endOfStream, promise);
future.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
// The write failed, handle the error.
lifecycleManager.onException(ctx, future.cause());
} else if (endOfStream) {
// Close the local side of the stream if this is the last frame
Http2Stream stream = connection.stream(streamId);
lifecycleManager.closeLocalSide(stream, ctx.newPromise());
}
}
});
return future;
}
@Override
public ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endStream, ChannelPromise promise) {
return writeHeaders(ctx, streamId, headers, 0, DEFAULT_PRIORITY_WEIGHT, false, padding, endStream, promise);
}
@Override
public ChannelFuture writeHeaders(final ChannelHandlerContext ctx, final int streamId,
final Http2Headers headers, final int streamDependency, final short weight,
final boolean exclusive, final int padding, final boolean endOfStream,
final ChannelPromise promise) {
Http2Stream stream = connection.stream(streamId);
ChannelFuture lastDataWrite = stream != null ? flowController().lastFlowControlledFrameSent(stream) : null;
try {
if (connection.isGoAway()) {
throw connectionError(PROTOCOL_ERROR, "Sending headers after connection going away.");
}
if (stream == null) {
stream = connection.createLocalStream(streamId).open(endOfStream);
} else {
if (stream.isResetSent()) {
throw new IllegalStateException("Sending headers after sending RST_STREAM.");
}
if (stream.isEndOfStreamSent()) {
throw new IllegalStateException("Sending headers after sending END_STREAM.");
}
// An existing stream...
switch (stream.state()) {
case RESERVED_LOCAL:
case IDLE:
stream.open(endOfStream);
break;
case OPEN:
case HALF_CLOSED_REMOTE:
// Allowed sending headers in these states.
break;
default:
throw new IllegalStateException(String.format(
"Stream %d in unexpected state: %s", stream.id(), stream.state()));
}
}
if (lastDataWrite != null && !endOfStream) {
throw new IllegalStateException(
"Sending non-trailing headers after data has been sent for stream: "
+ streamId);
}
} catch (Http2NoMoreStreamIdsException e) {
lifecycleManager.onException(ctx, e);
return promise.setFailure(e);
} catch (Throwable e) {
return promise.setFailure(e);
}
if (lastDataWrite == null) {
// No previous DATA frames to keep in sync with, just send it now.
return writeHeaders(ctx, stream, headers, streamDependency, weight, exclusive, padding,
endOfStream, promise);
}
// There were previous DATA frames sent. We need to send the HEADERS only after the most
// recent DATA frame to keep them in sync...
// Only write the HEADERS frame after the previous DATA frame has been written.
final Http2Stream theStream = stream;
lastDataWrite.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
// The DATA write failed, also fail this write.
promise.setFailure(future.cause());
return;
}
// Perform the write.
writeHeaders(ctx, theStream, headers, streamDependency, weight, exclusive, padding,
endOfStream, promise);
}
});
return promise;
}
/**
* Writes the given {@link Http2Headers} to the remote endpoint and updates stream state if appropriate.
*/
private ChannelFuture writeHeaders(ChannelHandlerContext ctx, Http2Stream stream,
Http2Headers headers, int streamDependency, short weight, boolean exclusive,
int padding, boolean endOfStream, ChannelPromise promise) {
ChannelFuture future =
frameWriter.writeHeaders(ctx, stream.id(), headers, streamDependency, weight,
exclusive, padding, endOfStream, promise);
ctx.flush();
// If the headers are the end of the stream, close it now.
if (endOfStream) {
stream.endOfStreamSent();
lifecycleManager.closeLocalSide(stream, promise);
}
return future;
}
@Override
public ChannelFuture writePriority(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight,
boolean exclusive, ChannelPromise promise) {
try {
if (connection.isGoAway()) {
throw connectionError(PROTOCOL_ERROR, "Sending priority after connection going away.");
}
// Update the priority on this stream.
Http2Stream stream = connection.stream(streamId);
if (stream == null) {
stream = connection.createLocalStream(streamId);
}
stream.setPriority(streamDependency, weight, exclusive);
} catch (Throwable e) {
return promise.setFailure(e);
}
ChannelFuture future = frameWriter.writePriority(ctx, streamId, streamDependency, weight, exclusive, promise);
ctx.flush();
return future;
}
@Override
public ChannelFuture writeRstStream(ChannelHandlerContext ctx, int streamId, long errorCode,
ChannelPromise promise) {
// Delegate to the lifecycle manager for proper updating of connection state.
return lifecycleManager.writeRstStream(ctx, streamId, errorCode, promise);
}
/**
* Writes a RST_STREAM frame to the remote endpoint.
* @param ctx the context to use for writing.
* @param streamId the stream for which to send the frame.
* @param errorCode the error code indicating the nature of the failure.
* @param promise the promise for the write.
* @param writeIfNoStream
* <ul>
* <li>{@code true} will force a write of a RST_STREAM even if the stream object does not exist locally.</li>
* <li>{@code false} will only send a RST_STREAM only if the stream is known about locally</li>
* </ul>
* @return the future for the write.
*/
public ChannelFuture writeRstStream(ChannelHandlerContext ctx, int streamId, long errorCode,
ChannelPromise promise, boolean writeIfNoStream) {
Http2Stream stream = connection.stream(streamId);
if (stream == null && !writeIfNoStream) {
// The stream may already have been closed ... ignore.
promise.setSuccess();
return promise;
}
ChannelFuture future = frameWriter.writeRstStream(ctx, streamId, errorCode, promise);
ctx.flush();
if (stream != null) {
stream.resetSent();
lifecycleManager.closeStream(stream, promise);
}
return future;
}
@Override
public ChannelFuture writeSettings(ChannelHandlerContext ctx, Http2Settings settings,
ChannelPromise promise) {
outstandingLocalSettingsQueue.add(settings);
try {
if (connection.isGoAway()) {
throw connectionError(PROTOCOL_ERROR, "Sending settings after connection going away.");
}
Boolean pushEnabled = settings.pushEnabled();
if (pushEnabled != null && connection.isServer()) {
throw connectionError(PROTOCOL_ERROR, "Server sending SETTINGS frame with ENABLE_PUSH specified");
}
} catch (Throwable e) {
return promise.setFailure(e);
}
ChannelFuture future = frameWriter.writeSettings(ctx, settings, promise);
ctx.flush();
return future;
}
@Override
public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) {
ChannelFuture future = frameWriter.writeSettingsAck(ctx, promise);
ctx.flush();
return future;
}
@Override
public ChannelFuture writePing(ChannelHandlerContext ctx, boolean ack, ByteBuf data,
ChannelPromise promise) {
if (connection.isGoAway()) {
data.release();
return promise.setFailure(connectionError(PROTOCOL_ERROR, "Sending ping after connection going away."));
}
ChannelFuture future = frameWriter.writePing(ctx, ack, data, promise);
ctx.flush();
return future;
}
@Override
public ChannelFuture writePushPromise(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
Http2Headers headers, int padding, ChannelPromise promise) {
try {
if (connection.isGoAway()) {
throw connectionError(PROTOCOL_ERROR, "Sending push promise after connection going away.");
}
// Reserve the promised stream.
Http2Stream stream = connection.requireStream(streamId);
connection.local().reservePushStream(promisedStreamId, stream);
} catch (Throwable e) {
return promise.setFailure(e);
}
ChannelFuture future = frameWriter.writePushPromise(ctx, streamId, promisedStreamId, headers, padding, promise);
ctx.flush();
return future;
}
@Override
public ChannelFuture writeGoAway(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData,
ChannelPromise promise) {
return lifecycleManager.writeGoAway(ctx, lastStreamId, errorCode, debugData, promise);
}
@Override
public ChannelFuture writeWindowUpdate(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement,
ChannelPromise promise) {
return promise.setFailure(new UnsupportedOperationException("Use the Http2[Inbound|Outbound]FlowController" +
" objects to control window sizes"));
}
@Override
public ChannelFuture writeFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags,
ByteBuf payload, ChannelPromise promise) {
return frameWriter.writeFrame(ctx, frameType, streamId, flags, payload, promise);
}
@Override
public void close() {
frameWriter.close();
}
@Override
public Http2Settings pollSentSettings() {
return outstandingLocalSettingsQueue.poll();
}
@Override
public Configuration configuration() {
return frameWriter.configuration();
}
}

View File

@ -0,0 +1,706 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTINGS_INITIAL_WINDOW_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_FRAME_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.FRAME_HEADER_LENGTH;
import static io.netty.handler.codec.http2.Http2CodecUtil.INT_FIELD_LENGTH;
import static io.netty.handler.codec.http2.Http2CodecUtil.PRIORITY_ENTRY_LENGTH;
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTINGS_MAX_FRAME_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTING_ENTRY_LENGTH;
import static io.netty.handler.codec.http2.Http2Error.FLOW_CONTROL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.FRAME_SIZE_ERROR;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2CodecUtil.isMaxFrameSizeValid;
import static io.netty.handler.codec.http2.Http2CodecUtil.readUnsignedInt;
import static io.netty.handler.codec.http2.Http2Exception.streamError;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import static io.netty.handler.codec.http2.Http2FrameTypes.CONTINUATION;
import static io.netty.handler.codec.http2.Http2FrameTypes.DATA;
import static io.netty.handler.codec.http2.Http2FrameTypes.GO_AWAY;
import static io.netty.handler.codec.http2.Http2FrameTypes.HEADERS;
import static io.netty.handler.codec.http2.Http2FrameTypes.PING;
import static io.netty.handler.codec.http2.Http2FrameTypes.PRIORITY;
import static io.netty.handler.codec.http2.Http2FrameTypes.PUSH_PROMISE;
import static io.netty.handler.codec.http2.Http2FrameTypes.RST_STREAM;
import static io.netty.handler.codec.http2.Http2FrameTypes.SETTINGS;
import static io.netty.handler.codec.http2.Http2FrameTypes.WINDOW_UPDATE;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http2.Http2FrameReader.Configuration;
/**
* A {@link Http2FrameReader} that supports all frame types defined by the HTTP/2 specification.
*/
public class DefaultHttp2FrameReader implements Http2FrameReader, Http2FrameSizePolicy, Configuration {
private enum State {
FRAME_HEADER,
FRAME_PAYLOAD,
ERROR
}
private final Http2HeadersDecoder headersDecoder;
private State state = State.FRAME_HEADER;
private byte frameType;
private int streamId;
private Http2Flags flags;
private int payloadLength;
private HeadersContinuation headersContinuation;
private int maxFrameSize;
public DefaultHttp2FrameReader() {
this(new DefaultHttp2HeadersDecoder());
}
public DefaultHttp2FrameReader(Http2HeadersDecoder headersDecoder) {
this.headersDecoder = headersDecoder;
maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
}
@Override
public Http2HeaderTable headerTable() {
return headersDecoder.configuration().headerTable();
}
@Override
public Configuration configuration() {
return this;
}
@Override
public Http2FrameSizePolicy frameSizePolicy() {
return this;
}
@Override
public void maxFrameSize(int max) throws Http2Exception {
if (!isMaxFrameSizeValid(max)) {
throw streamError(streamId, FRAME_SIZE_ERROR,
"Invalid MAX_FRAME_SIZE specified in sent settings: %d", max);
}
maxFrameSize = max;
}
@Override
public int maxFrameSize() {
return maxFrameSize;
}
@Override
public void close() {
if (headersContinuation != null) {
headersContinuation.close();
}
}
@Override
public void readFrame(ChannelHandlerContext ctx, ByteBuf input, Http2FrameListener listener)
throws Http2Exception {
try {
while (input.isReadable()) {
switch (state) {
case FRAME_HEADER:
processHeaderState(input);
if (state == State.FRAME_HEADER) {
// Wait until the entire header has arrived.
return;
}
// The header is complete, fall into the next case to process the payload.
// This is to ensure the proper handling of zero-length payloads. In this
// case, we don't want to loop around because there may be no more data
// available, causing us to exit the loop. Instead, we just want to perform
// the first pass at payload processing now.
case FRAME_PAYLOAD:
processPayloadState(ctx, input, listener);
if (state == State.FRAME_PAYLOAD) {
// Wait until the entire payload has arrived.
return;
}
break;
case ERROR:
input.skipBytes(input.readableBytes());
return;
default:
throw new IllegalStateException("Should never get here");
}
}
} catch (Http2Exception e) {
state = State.ERROR;
throw e;
} catch (RuntimeException e) {
state = State.ERROR;
throw e;
} catch (Error e) {
state = State.ERROR;
throw e;
}
}
private void processHeaderState(ByteBuf in) throws Http2Exception {
if (in.readableBytes() < FRAME_HEADER_LENGTH) {
// Wait until the entire frame header has been read.
return;
}
// Read the header and prepare the unmarshaller to read the frame.
payloadLength = in.readUnsignedMedium();
if (payloadLength > maxFrameSize) {
throw connectionError(PROTOCOL_ERROR, "Frame length: %d exceeds maximum: %d", payloadLength, maxFrameSize);
}
frameType = in.readByte();
flags = new Http2Flags(in.readUnsignedByte());
streamId = readUnsignedInt(in);
switch (frameType) {
case DATA:
verifyDataFrame();
break;
case HEADERS:
verifyHeadersFrame();
break;
case PRIORITY:
verifyPriorityFrame();
break;
case RST_STREAM:
verifyRstStreamFrame();
break;
case SETTINGS:
verifySettingsFrame();
break;
case PUSH_PROMISE:
verifyPushPromiseFrame();
break;
case PING:
verifyPingFrame();
break;
case GO_AWAY:
verifyGoAwayFrame();
break;
case WINDOW_UPDATE:
verifyWindowUpdateFrame();
break;
case CONTINUATION:
verifyContinuationFrame();
break;
default:
// Unknown frame type, could be an extension.
break;
}
// Start reading the payload for the frame.
state = State.FRAME_PAYLOAD;
}
private void processPayloadState(ChannelHandlerContext ctx, ByteBuf in, Http2FrameListener listener)
throws Http2Exception {
if (in.readableBytes() < payloadLength) {
// Wait until the entire payload has been read.
return;
}
// Get a view of the buffer for the size of the payload.
ByteBuf payload = in.readSlice(payloadLength);
// Read the payload and fire the frame event to the listener.
switch (frameType) {
case DATA:
readDataFrame(ctx, payload, listener);
break;
case HEADERS:
readHeadersFrame(ctx, payload, listener);
break;
case PRIORITY:
readPriorityFrame(ctx, payload, listener);
break;
case RST_STREAM:
readRstStreamFrame(ctx, payload, listener);
break;
case SETTINGS:
readSettingsFrame(ctx, payload, listener);
break;
case PUSH_PROMISE:
readPushPromiseFrame(ctx, payload, listener);
break;
case PING:
readPingFrame(ctx, payload, listener);
break;
case GO_AWAY:
readGoAwayFrame(ctx, payload, listener);
break;
case WINDOW_UPDATE:
readWindowUpdateFrame(ctx, payload, listener);
break;
case CONTINUATION:
readContinuationFrame(payload, listener);
break;
default:
readUnknownFrame(ctx, payload, listener);
break;
}
// Go back to reading the next frame header.
state = State.FRAME_HEADER;
}
private void verifyDataFrame() throws Http2Exception {
verifyNotProcessingHeaders();
verifyPayloadLength(payloadLength);
if (payloadLength < flags.getPaddingPresenceFieldLength()) {
throw streamError(streamId, FRAME_SIZE_ERROR,
"Frame length %d too small.", payloadLength);
}
}
private void verifyHeadersFrame() throws Http2Exception {
verifyNotProcessingHeaders();
verifyPayloadLength(payloadLength);
int requiredLength = flags.getPaddingPresenceFieldLength() + flags.getNumPriorityBytes();
if (payloadLength < requiredLength) {
throw streamError(streamId, FRAME_SIZE_ERROR,
"Frame length too small." + payloadLength);
}
}
private void verifyPriorityFrame() throws Http2Exception {
verifyNotProcessingHeaders();
if (payloadLength != PRIORITY_ENTRY_LENGTH) {
throw streamError(streamId, FRAME_SIZE_ERROR,
"Invalid frame length %d.", payloadLength);
}
}
private void verifyRstStreamFrame() throws Http2Exception {
verifyNotProcessingHeaders();
if (payloadLength != INT_FIELD_LENGTH) {
throw streamError(streamId, FRAME_SIZE_ERROR,
"Invalid frame length %d.", payloadLength);
}
}
private void verifySettingsFrame() throws Http2Exception {
verifyNotProcessingHeaders();
verifyPayloadLength(payloadLength);
if (streamId != 0) {
throw connectionError(PROTOCOL_ERROR, "A stream ID must be zero.");
}
if (flags.ack() && payloadLength > 0) {
throw connectionError(PROTOCOL_ERROR, "Ack settings frame must have an empty payload.");
}
if (payloadLength % SETTING_ENTRY_LENGTH > 0) {
throw connectionError(FRAME_SIZE_ERROR, "Frame length %d invalid.", payloadLength);
}
}
private void verifyPushPromiseFrame() throws Http2Exception {
verifyNotProcessingHeaders();
verifyPayloadLength(payloadLength);
// Subtract the length of the promised stream ID field, to determine the length of the
// rest of the payload (header block fragment + payload).
int minLength = flags.getPaddingPresenceFieldLength() + INT_FIELD_LENGTH;
if (payloadLength < minLength) {
throw streamError(streamId, FRAME_SIZE_ERROR,
"Frame length %d too small.", payloadLength);
}
}
private void verifyPingFrame() throws Http2Exception {
verifyNotProcessingHeaders();
if (streamId != 0) {
throw connectionError(PROTOCOL_ERROR, "A stream ID must be zero.");
}
if (payloadLength != 8) {
throw connectionError(FRAME_SIZE_ERROR,
"Frame length %d incorrect size for ping.", payloadLength);
}
}
private void verifyGoAwayFrame() throws Http2Exception {
verifyNotProcessingHeaders();
verifyPayloadLength(payloadLength);
if (streamId != 0) {
throw connectionError(PROTOCOL_ERROR, "A stream ID must be zero.");
}
if (payloadLength < 8) {
throw connectionError(FRAME_SIZE_ERROR, "Frame length %d too small.", payloadLength);
}
}
private void verifyWindowUpdateFrame() throws Http2Exception {
verifyNotProcessingHeaders();
verifyStreamOrConnectionId(streamId, "Stream ID");
if (payloadLength != INT_FIELD_LENGTH) {
throw streamError(streamId, FRAME_SIZE_ERROR,
"Invalid frame length %d.", payloadLength);
}
}
private void verifyContinuationFrame() throws Http2Exception {
verifyPayloadLength(payloadLength);
if (headersContinuation == null) {
throw connectionError(PROTOCOL_ERROR, "Received %s frame but not currently processing headers.",
frameType);
}
if (streamId != headersContinuation.getStreamId()) {
throw connectionError(PROTOCOL_ERROR, "Continuation stream ID does not match pending headers. "
+ "Expected %d, but received %d.", headersContinuation.getStreamId(), streamId);
}
if (payloadLength < flags.getPaddingPresenceFieldLength()) {
throw streamError(streamId, FRAME_SIZE_ERROR,
"Frame length %d too small for padding.", payloadLength);
}
}
private void readDataFrame(ChannelHandlerContext ctx, ByteBuf payload,
Http2FrameListener listener) throws Http2Exception {
short padding = readPadding(payload);
// Determine how much data there is to read by removing the trailing
// padding.
int dataLength = payload.readableBytes() - padding;
if (dataLength < 0) {
throw streamError(streamId, FRAME_SIZE_ERROR,
"Frame payload too small for padding.");
}
ByteBuf data = payload.readSlice(dataLength);
listener.onDataRead(ctx, streamId, data, padding, flags.endOfStream());
payload.skipBytes(payload.readableBytes());
}
private void readHeadersFrame(final ChannelHandlerContext ctx, ByteBuf payload,
Http2FrameListener listener) throws Http2Exception {
final int headersStreamId = streamId;
final Http2Flags headersFlags = flags;
final int padding = readPadding(payload);
// The callback that is invoked is different depending on whether priority information
// is present in the headers frame.
if (flags.priorityPresent()) {
long word1 = payload.readUnsignedInt();
final boolean exclusive = (word1 & 0x80000000L) != 0;
final int streamDependency = (int) (word1 & 0x7FFFFFFFL);
final short weight = (short) (payload.readUnsignedByte() + 1);
final ByteBuf fragment = payload.readSlice(payload.readableBytes() - padding);
// Create a handler that invokes the listener when the header block is complete.
headersContinuation = new HeadersContinuation() {
@Override
public int getStreamId() {
return headersStreamId;
}
@Override
public void processFragment(boolean endOfHeaders, ByteBuf fragment,
Http2FrameListener listener) throws Http2Exception {
final HeadersBlockBuilder hdrBlockBuilder = headersBlockBuilder();
hdrBlockBuilder.addFragment(fragment, ctx.alloc(), endOfHeaders);
if (endOfHeaders) {
listener.onHeadersRead(ctx, headersStreamId, hdrBlockBuilder.headers(),
streamDependency, weight, exclusive, padding, headersFlags.endOfStream());
close();
}
}
};
// Process the initial fragment, invoking the listener's callback if end of headers.
headersContinuation.processFragment(flags.endOfHeaders(), fragment, listener);
return;
}
// The priority fields are not present in the frame. Prepare a continuation that invokes
// the listener callback without priority information.
headersContinuation = new HeadersContinuation() {
@Override
public int getStreamId() {
return headersStreamId;
}
@Override
public void processFragment(boolean endOfHeaders, ByteBuf fragment,
Http2FrameListener listener) throws Http2Exception {
final HeadersBlockBuilder hdrBlockBuilder = headersBlockBuilder();
hdrBlockBuilder.addFragment(fragment, ctx.alloc(), endOfHeaders);
if (endOfHeaders) {
listener.onHeadersRead(ctx, headersStreamId, hdrBlockBuilder.headers(), padding,
headersFlags.endOfStream());
close();
}
}
};
// Process the initial fragment, invoking the listener's callback if end of headers.
final ByteBuf fragment = payload.readSlice(payload.readableBytes() - padding);
headersContinuation.processFragment(flags.endOfHeaders(), fragment, listener);
}
private void readPriorityFrame(ChannelHandlerContext ctx, ByteBuf payload,
Http2FrameListener listener) throws Http2Exception {
long word1 = payload.readUnsignedInt();
boolean exclusive = (word1 & 0x80000000L) != 0;
int streamDependency = (int) (word1 & 0x7FFFFFFFL);
short weight = (short) (payload.readUnsignedByte() + 1);
listener.onPriorityRead(ctx, streamId, streamDependency, weight, exclusive);
}
private void readRstStreamFrame(ChannelHandlerContext ctx, ByteBuf payload,
Http2FrameListener listener) throws Http2Exception {
long errorCode = payload.readUnsignedInt();
listener.onRstStreamRead(ctx, streamId, errorCode);
}
private void readSettingsFrame(ChannelHandlerContext ctx, ByteBuf payload,
Http2FrameListener listener) throws Http2Exception {
if (flags.ack()) {
listener.onSettingsAckRead(ctx);
} else {
int numSettings = payloadLength / SETTING_ENTRY_LENGTH;
Http2Settings settings = new Http2Settings();
for (int index = 0; index < numSettings; ++index) {
int id = payload.readUnsignedShort();
long value = payload.readUnsignedInt();
try {
settings.put(id, value);
} catch (IllegalArgumentException e) {
switch(id) {
case SETTINGS_MAX_FRAME_SIZE:
throw connectionError(FRAME_SIZE_ERROR, e, e.getMessage());
case SETTINGS_INITIAL_WINDOW_SIZE:
throw connectionError(FLOW_CONTROL_ERROR, e, e.getMessage());
default:
throw connectionError(PROTOCOL_ERROR, e, e.getMessage());
}
}
}
listener.onSettingsRead(ctx, settings);
}
}
private void readPushPromiseFrame(final ChannelHandlerContext ctx, ByteBuf payload,
Http2FrameListener listener) throws Http2Exception {
final int pushPromiseStreamId = streamId;
final int padding = readPadding(payload);
final int promisedStreamId = readUnsignedInt(payload);
// Create a handler that invokes the listener when the header block is complete.
headersContinuation = new HeadersContinuation() {
@Override
public int getStreamId() {
return pushPromiseStreamId;
}
@Override
public void processFragment(boolean endOfHeaders, ByteBuf fragment,
Http2FrameListener listener) throws Http2Exception {
headersBlockBuilder().addFragment(fragment, ctx.alloc(), endOfHeaders);
if (endOfHeaders) {
Http2Headers headers = headersBlockBuilder().headers();
listener.onPushPromiseRead(ctx, pushPromiseStreamId, promisedStreamId, headers,
padding);
close();
}
}
};
// Process the initial fragment, invoking the listener's callback if end of headers.
final ByteBuf fragment = payload.readSlice(payload.readableBytes() - padding);
headersContinuation.processFragment(flags.endOfHeaders(), fragment, listener);
}
private void readPingFrame(ChannelHandlerContext ctx, ByteBuf payload,
Http2FrameListener listener) throws Http2Exception {
ByteBuf data = payload.readSlice(payload.readableBytes());
if (flags.ack()) {
listener.onPingAckRead(ctx, data);
} else {
listener.onPingRead(ctx, data);
}
}
private static void readGoAwayFrame(ChannelHandlerContext ctx, ByteBuf payload,
Http2FrameListener listener) throws Http2Exception {
int lastStreamId = readUnsignedInt(payload);
long errorCode = payload.readUnsignedInt();
ByteBuf debugData = payload.readSlice(payload.readableBytes());
listener.onGoAwayRead(ctx, lastStreamId, errorCode, debugData);
}
private void readWindowUpdateFrame(ChannelHandlerContext ctx, ByteBuf payload,
Http2FrameListener listener) throws Http2Exception {
int windowSizeIncrement = readUnsignedInt(payload);
if (windowSizeIncrement == 0) {
throw streamError(streamId, PROTOCOL_ERROR,
"Received WINDOW_UPDATE with delta 0 for stream: %d", streamId);
}
listener.onWindowUpdateRead(ctx, streamId, windowSizeIncrement);
}
private void readContinuationFrame(ByteBuf payload, Http2FrameListener listener)
throws Http2Exception {
// Process the initial fragment, invoking the listener's callback if end of headers.
final ByteBuf continuationFragment = payload.readSlice(payload.readableBytes());
headersContinuation.processFragment(flags.endOfHeaders(), continuationFragment,
listener);
}
private void readUnknownFrame(ChannelHandlerContext ctx, ByteBuf payload, Http2FrameListener listener) {
payload = payload.readSlice(payload.readableBytes());
listener.onUnknownFrame(ctx, frameType, streamId, flags, payload);
}
/**
* If padding is present in the payload, reads the next byte as padding. Otherwise, returns zero.
*/
private short readPadding(ByteBuf payload) {
if (!flags.paddingPresent()) {
return 0;
}
return payload.readUnsignedByte();
}
/**
* Base class for processing of HEADERS and PUSH_PROMISE header blocks that potentially span
* multiple frames. The implementation of this interface will perform the final callback to the
* {@link Http2FrameListener} once the end of headers is reached.
*/
private abstract class HeadersContinuation {
private final HeadersBlockBuilder builder = new HeadersBlockBuilder();
/**
* Returns the stream for which headers are currently being processed.
*/
abstract int getStreamId();
/**
* Processes the next fragment for the current header block.
*
* @param endOfHeaders whether the fragment is the last in the header block.
* @param fragment the fragment of the header block to be added.
* @param listener the listener to be notified if the header block is completed.
*/
abstract void processFragment(boolean endOfHeaders, ByteBuf fragment,
Http2FrameListener listener) throws Http2Exception;
final HeadersBlockBuilder headersBlockBuilder() {
return builder;
}
/**
* Free any allocated resources.
*/
final void close() {
builder.close();
}
}
/**
* Utility class to help with construction of the headers block that may potentially span
* multiple frames.
*/
protected class HeadersBlockBuilder {
private ByteBuf headerBlock;
/**
* Adds a fragment to the block.
*
* @param fragment the fragment of the headers block to be added.
* @param alloc allocator for new blocks if needed.
* @param endOfHeaders flag indicating whether the current frame is the end of the headers.
* This is used for an optimization for when the first fragment is the full
* block. In that case, the buffer is used directly without copying.
*/
final void addFragment(ByteBuf fragment, ByteBufAllocator alloc, boolean endOfHeaders) {
if (headerBlock == null) {
if (endOfHeaders) {
// Optimization - don't bother copying, just use the buffer as-is. Need
// to retain since we release when the header block is built.
headerBlock = fragment.retain();
} else {
headerBlock = alloc.buffer(fragment.readableBytes());
headerBlock.writeBytes(fragment);
}
return;
}
if (headerBlock.isWritable(fragment.readableBytes())) {
// The buffer can hold the requested bytes, just write it directly.
headerBlock.writeBytes(fragment);
} else {
// Allocate a new buffer that is big enough to hold the entire header block so far.
ByteBuf buf = alloc.buffer(headerBlock.readableBytes() + fragment.readableBytes());
buf.writeBytes(headerBlock);
buf.writeBytes(fragment);
headerBlock.release();
headerBlock = buf;
}
}
/**
* Builds the headers from the completed headers block. After this is called, this builder
* should not be called again.
*/
Http2Headers headers() throws Http2Exception {
try {
return headersDecoder.decodeHeaders(headerBlock);
} finally {
close();
}
}
/**
* Closes this builder and frees any resources.
*/
void close() {
if (headerBlock != null) {
headerBlock.release();
headerBlock = null;
}
// Clear the member variable pointing at this instance.
headersContinuation = null;
}
}
private void verifyNotProcessingHeaders() throws Http2Exception {
if (headersContinuation != null) {
throw connectionError(PROTOCOL_ERROR, "Received frame of type %s while processing headers.", frameType);
}
}
private void verifyPayloadLength(int payloadLength) throws Http2Exception {
if (payloadLength > maxFrameSize) {
throw connectionError(PROTOCOL_ERROR, "Total payload length %d exceeds max frame length.", payloadLength);
}
}
private static void verifyStreamOrConnectionId(int streamId, String argumentName)
throws Http2Exception {
if (streamId < 0) {
throw connectionError(PROTOCOL_ERROR, "%s must be >= 0", argumentName);
}
}
}

View File

@ -0,0 +1,513 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_FRAME_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.FRAME_HEADER_LENGTH;
import static io.netty.handler.codec.http2.Http2CodecUtil.INT_FIELD_LENGTH;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_UNSIGNED_BYTE;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_UNSIGNED_INT;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_WEIGHT;
import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_WEIGHT;
import static io.netty.handler.codec.http2.Http2CodecUtil.PRIORITY_ENTRY_LENGTH;
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTING_ENTRY_LENGTH;
import static io.netty.handler.codec.http2.Http2CodecUtil.isMaxFrameSizeValid;
import static io.netty.handler.codec.http2.Http2CodecUtil.writeFrameHeader;
import static io.netty.handler.codec.http2.Http2CodecUtil.writeUnsignedInt;
import static io.netty.handler.codec.http2.Http2CodecUtil.writeUnsignedShort;
import static io.netty.handler.codec.http2.Http2Error.FRAME_SIZE_ERROR;
import static io.netty.handler.codec.http2.Http2FrameTypes.CONTINUATION;
import static io.netty.handler.codec.http2.Http2FrameTypes.DATA;
import static io.netty.handler.codec.http2.Http2FrameTypes.GO_AWAY;
import static io.netty.handler.codec.http2.Http2FrameTypes.HEADERS;
import static io.netty.handler.codec.http2.Http2FrameTypes.PING;
import static io.netty.handler.codec.http2.Http2FrameTypes.PRIORITY;
import static io.netty.handler.codec.http2.Http2FrameTypes.PUSH_PROMISE;
import static io.netty.handler.codec.http2.Http2FrameTypes.RST_STREAM;
import static io.netty.handler.codec.http2.Http2FrameTypes.SETTINGS;
import static io.netty.handler.codec.http2.Http2FrameTypes.WINDOW_UPDATE;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.CompositeByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http2.Http2FrameWriter.Configuration;
import io.netty.util.collection.IntObjectMap;
/**
* A {@link Http2FrameWriter} that supports all frame types defined by the HTTP/2 specification.
*/
public class DefaultHttp2FrameWriter implements Http2FrameWriter, Http2FrameSizePolicy, Configuration {
private static final String STREAM_ID = "Stream ID";
private static final String STREAM_DEPENDENCY = "Stream Dependency";
private final Http2HeadersEncoder headersEncoder;
private int maxFrameSize;
public DefaultHttp2FrameWriter() {
this(new DefaultHttp2HeadersEncoder());
}
public DefaultHttp2FrameWriter(Http2HeadersEncoder headersEncoder) {
this.headersEncoder = headersEncoder;
maxFrameSize = DEFAULT_MAX_FRAME_SIZE;
}
@Override
public Configuration configuration() {
return this;
}
@Override
public Http2HeaderTable headerTable() {
return headersEncoder.configuration().headerTable();
}
@Override
public Http2FrameSizePolicy frameSizePolicy() {
return this;
}
@Override
public void maxFrameSize(int max) throws Http2Exception {
if (!isMaxFrameSizeValid(max)) {
throw connectionError(FRAME_SIZE_ERROR, "Invalid MAX_FRAME_SIZE specified in sent settings: %d", max);
}
maxFrameSize = max;
}
@Override
public int maxFrameSize() {
return maxFrameSize;
}
@Override
public void close() { }
@Override
public ChannelFuture writeData(ChannelHandlerContext ctx, int streamId, ByteBuf data,
int padding, boolean endStream, ChannelPromise promise) {
try {
verifyStreamId(streamId, STREAM_ID);
verifyPadding(padding);
Http2Flags flags = new Http2Flags().paddingPresent(padding > 0).endOfStream(endStream);
int payloadLength = data.readableBytes() + padding + flags.getPaddingPresenceFieldLength();
verifyPayloadLength(payloadLength);
ByteBuf out = ctx.alloc().buffer(FRAME_HEADER_LENGTH + payloadLength);
writeFrameHeader(out, payloadLength, DATA, flags, streamId);
writePaddingLength(padding, out);
// Write the data.
out.writeBytes(data, data.readerIndex(), data.readableBytes());
// Write the required padding.
out.writeZero(padding);
return ctx.write(out, promise);
} catch (RuntimeException e) {
return promise.setFailure(e);
} finally {
data.release();
}
}
@Override
public ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId,
Http2Headers headers, int padding, boolean endStream, ChannelPromise promise) {
return writeHeadersInternal(ctx, promise, streamId, headers, padding, endStream,
false, 0, (short) 0, false);
}
@Override
public ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId,
Http2Headers headers, int streamDependency, short weight, boolean exclusive,
int padding, boolean endStream, ChannelPromise promise) {
return writeHeadersInternal(ctx, promise, streamId, headers, padding, endStream,
true, streamDependency, weight, exclusive);
}
@Override
public ChannelFuture writePriority(ChannelHandlerContext ctx, int streamId,
int streamDependency, short weight, boolean exclusive, ChannelPromise promise) {
try {
verifyStreamId(streamId, STREAM_ID);
verifyStreamId(streamDependency, STREAM_DEPENDENCY);
verifyWeight(weight);
ByteBuf frame = ctx.alloc().buffer(FRAME_HEADER_LENGTH + PRIORITY_ENTRY_LENGTH);
writeFrameHeader(frame, PRIORITY_ENTRY_LENGTH, PRIORITY,
new Http2Flags(), streamId);
long word1 = exclusive ? 0x80000000L | streamDependency : streamDependency;
writeUnsignedInt(word1, frame);
// Adjust the weight so that it fits into a single byte on the wire.
frame.writeByte(weight - 1);
return ctx.write(frame, promise);
} catch (RuntimeException e) {
return promise.setFailure(e);
}
}
@Override
public ChannelFuture writeRstStream(ChannelHandlerContext ctx, int streamId, long errorCode,
ChannelPromise promise) {
try {
verifyStreamId(streamId, STREAM_ID);
verifyErrorCode(errorCode);
ByteBuf frame = ctx.alloc().buffer(FRAME_HEADER_LENGTH + INT_FIELD_LENGTH);
writeFrameHeader(frame, INT_FIELD_LENGTH, RST_STREAM, new Http2Flags(),
streamId);
writeUnsignedInt(errorCode, frame);
return ctx.write(frame, promise);
} catch (RuntimeException e) {
return promise.setFailure(e);
}
}
@Override
public ChannelFuture writeSettings(ChannelHandlerContext ctx, Http2Settings settings,
ChannelPromise promise) {
try {
checkNotNull(settings, "settings");
int payloadLength = SETTING_ENTRY_LENGTH * settings.size();
ByteBuf frame = ctx.alloc().buffer(FRAME_HEADER_LENGTH + payloadLength);
writeFrameHeader(frame, payloadLength, SETTINGS, new Http2Flags(), 0);
for (IntObjectMap.Entry<Long> entry : settings.entries()) {
writeUnsignedShort(entry.key(), frame);
writeUnsignedInt(entry.value(), frame);
}
return ctx.write(frame, promise);
} catch (RuntimeException e) {
return promise.setFailure(e);
}
}
@Override
public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) {
try {
ByteBuf frame = ctx.alloc().buffer(FRAME_HEADER_LENGTH);
writeFrameHeader(frame, 0, SETTINGS, new Http2Flags().ack(true), 0);
return ctx.write(frame, promise);
} catch (RuntimeException e) {
return promise.setFailure(e);
}
}
@Override
public ChannelFuture writePing(ChannelHandlerContext ctx, boolean ack, ByteBuf data,
ChannelPromise promise) {
try {
ByteBuf frame = ctx.alloc().buffer(FRAME_HEADER_LENGTH + data.readableBytes());
Http2Flags flags = ack ? new Http2Flags().ack(true) : new Http2Flags();
writeFrameHeader(frame, data.readableBytes(), PING, flags, 0);
// Write the debug data.
frame.writeBytes(data, data.readerIndex(), data.readableBytes());
return ctx.write(frame, promise);
} catch (RuntimeException e) {
return promise.setFailure(e);
} finally {
data.release();
}
}
@Override
public ChannelFuture writePushPromise(ChannelHandlerContext ctx, int streamId,
int promisedStreamId, Http2Headers headers, int padding, ChannelPromise promise) {
ByteBuf headerBlock = null;
try {
verifyStreamId(streamId, STREAM_ID);
verifyStreamId(promisedStreamId, "Promised Stream ID");
verifyPadding(padding);
// Encode the entire header block into an intermediate buffer.
headerBlock = ctx.alloc().buffer();
headersEncoder.encodeHeaders(headers, headerBlock);
// Read the first fragment (possibly everything).
Http2Flags flags = new Http2Flags().paddingPresent(padding > 0);
int promisedStreamIdLength = INT_FIELD_LENGTH;
int nonFragmentLength = promisedStreamIdLength + padding + flags.getPaddingPresenceFieldLength();
int maxFragmentLength = maxFrameSize - nonFragmentLength;
ByteBuf fragment =
headerBlock.readSlice(Math.min(headerBlock.readableBytes(), maxFragmentLength));
flags.endOfHeaders(headerBlock.readableBytes() == 0);
int payloadLength = fragment.readableBytes() + nonFragmentLength;
ByteBuf firstFrame = ctx.alloc().buffer(FRAME_HEADER_LENGTH + payloadLength);
writeFrameHeader(firstFrame, payloadLength, PUSH_PROMISE, flags,
streamId);
writePaddingLength(padding, firstFrame);
// Write out the promised stream ID.
firstFrame.writeInt(promisedStreamId);
// Write the first fragment.
firstFrame.writeBytes(fragment);
// Write out the padding, if any.
firstFrame.writeZero(padding);
if (headerBlock.readableBytes() == 0) {
return ctx.write(firstFrame, promise);
}
// Create a composite buffer wrapping the first frame and any continuation frames.
return continueHeaders(ctx, promise, streamId, padding, headerBlock, firstFrame);
} catch (Exception e) {
return promise.setFailure(e);
} finally {
if (headerBlock != null) {
headerBlock.release();
}
}
}
@Override
public ChannelFuture writeGoAway(ChannelHandlerContext ctx, int lastStreamId, long errorCode,
ByteBuf debugData, ChannelPromise promise) {
try {
verifyStreamOrConnectionId(lastStreamId, "Last Stream ID");
verifyErrorCode(errorCode);
int payloadLength = 8 + debugData.readableBytes();
ByteBuf frame = ctx.alloc().buffer(FRAME_HEADER_LENGTH + payloadLength);
writeFrameHeader(frame, payloadLength, GO_AWAY, new Http2Flags(), 0);
frame.writeInt(lastStreamId);
writeUnsignedInt(errorCode, frame);
frame.writeBytes(debugData, debugData.readerIndex(), debugData.readableBytes());
return ctx.write(frame, promise);
} catch (RuntimeException e) {
return promise.setFailure(e);
} finally {
debugData.release();
}
}
@Override
public ChannelFuture writeWindowUpdate(ChannelHandlerContext ctx, int streamId,
int windowSizeIncrement, ChannelPromise promise) {
try {
verifyStreamOrConnectionId(streamId, STREAM_ID);
verifyWindowSizeIncrement(windowSizeIncrement);
ByteBuf frame = ctx.alloc().buffer(FRAME_HEADER_LENGTH + INT_FIELD_LENGTH);
writeFrameHeader(frame, INT_FIELD_LENGTH, WINDOW_UPDATE,
new Http2Flags(), streamId);
frame.writeInt(windowSizeIncrement);
return ctx.write(frame, promise);
} catch (RuntimeException e) {
return promise.setFailure(e);
}
}
@Override
public ChannelFuture writeFrame(ChannelHandlerContext ctx, byte frameType, int streamId,
Http2Flags flags, ByteBuf payload, ChannelPromise promise) {
try {
verifyStreamOrConnectionId(streamId, STREAM_ID);
ByteBuf frame = ctx.alloc().buffer(FRAME_HEADER_LENGTH + payload.readableBytes());
writeFrameHeader(frame, payload.readableBytes(), frameType, flags, streamId);
frame.writeBytes(payload);
return ctx.write(frame, promise);
} catch (RuntimeException e) {
return promise.setFailure(e);
}
}
private ChannelFuture writeHeadersInternal(ChannelHandlerContext ctx, ChannelPromise promise,
int streamId, Http2Headers headers, int padding, boolean endStream,
boolean hasPriority, int streamDependency, short weight, boolean exclusive) {
ByteBuf headerBlock = null;
try {
verifyStreamId(streamId, STREAM_ID);
if (hasPriority) {
verifyStreamOrConnectionId(streamDependency, STREAM_DEPENDENCY);
verifyPadding(padding);
verifyWeight(weight);
}
// Encode the entire header block.
headerBlock = ctx.alloc().buffer();
headersEncoder.encodeHeaders(headers, headerBlock);
Http2Flags flags =
new Http2Flags().endOfStream(endStream).priorityPresent(hasPriority).paddingPresent(padding > 0);
// Read the first fragment (possibly everything).
int nonFragmentBytes =
padding + flags.getNumPriorityBytes() + flags.getPaddingPresenceFieldLength();
int maxFragmentLength = maxFrameSize - nonFragmentBytes;
ByteBuf fragment =
headerBlock.readSlice(Math.min(headerBlock.readableBytes(), maxFragmentLength));
// Set the end of headers flag for the first frame.
flags.endOfHeaders(headerBlock.readableBytes() == 0);
int payloadLength = fragment.readableBytes() + nonFragmentBytes;
ByteBuf firstFrame = ctx.alloc().buffer(FRAME_HEADER_LENGTH + payloadLength);
writeFrameHeader(firstFrame, payloadLength, HEADERS, flags,
streamId);
// Write the padding length.
writePaddingLength(padding, firstFrame);
// Write the priority.
if (hasPriority) {
long word1 = exclusive ? 0x80000000L | streamDependency : streamDependency;
writeUnsignedInt(word1, firstFrame);
// Adjust the weight so that it fits into a single byte on the wire.
firstFrame.writeByte(weight - 1);
}
// Write the first fragment.
firstFrame.writeBytes(fragment);
// Write out the padding, if any.
firstFrame.writeZero(padding);
if (flags.endOfHeaders()) {
return ctx.write(firstFrame, promise);
}
// Create a composite buffer wrapping the first frame and any continuation frames.
return continueHeaders(ctx, promise, streamId, padding, headerBlock, firstFrame);
} catch (Exception e) {
return promise.setFailure(e);
} finally {
if (headerBlock != null) {
headerBlock.release();
}
}
}
/**
* Drains the header block and creates a composite buffer containing the first frame and a
* number of CONTINUATION frames.
*/
private ChannelFuture continueHeaders(ChannelHandlerContext ctx, ChannelPromise promise,
int streamId, int padding, ByteBuf headerBlock, ByteBuf firstFrame) {
// Create a composite buffer wrapping the first frame and any continuation frames.
CompositeByteBuf out = ctx.alloc().compositeBuffer();
out.addComponent(firstFrame);
int numBytes = firstFrame.readableBytes();
// Process any continuation frames there might be.
while (headerBlock.isReadable()) {
ByteBuf frame = createContinuationFrame(ctx, streamId, headerBlock, padding);
out.addComponent(frame);
numBytes += frame.readableBytes();
}
out.writerIndex(numBytes);
return ctx.write(out, promise);
}
/**
* Allocates a new buffer and writes a single continuation frame with a fragment of the header
* block to the output buffer.
*/
private ByteBuf createContinuationFrame(ChannelHandlerContext ctx, int streamId,
ByteBuf headerBlock, int padding) {
Http2Flags flags = new Http2Flags().paddingPresent(padding > 0);
int nonFragmentLength = padding + flags.getPaddingPresenceFieldLength();
int maxFragmentLength = maxFrameSize - nonFragmentLength;
ByteBuf fragment =
headerBlock.readSlice(Math.min(headerBlock.readableBytes(), maxFragmentLength));
int payloadLength = fragment.readableBytes() + nonFragmentLength;
ByteBuf frame = ctx.alloc().buffer(FRAME_HEADER_LENGTH + payloadLength);
flags = flags.endOfHeaders(headerBlock.readableBytes() == 0);
writeFrameHeader(frame, payloadLength, CONTINUATION, flags, streamId);
writePaddingLength(padding, frame);
frame.writeBytes(fragment);
// Write out the padding, if any.
frame.writeZero(padding);
return frame;
}
/**
* Writes the padding length field to the output buffer.
*/
private static void writePaddingLength(int paddingLength, ByteBuf out) {
if (paddingLength > MAX_UNSIGNED_BYTE) {
int padHigh = paddingLength / 256;
out.writeByte(padHigh);
}
// Always include PadLow if there is any padding at all.
if (paddingLength > 0) {
int padLow = paddingLength % 256;
out.writeByte(padLow);
}
}
private static void verifyStreamId(int streamId, String argumentName) {
if (streamId <= 0) {
throw new IllegalArgumentException(argumentName + " must be > 0");
}
}
private static void verifyStreamOrConnectionId(int streamId, String argumentName) {
if (streamId < 0) {
throw new IllegalArgumentException(argumentName + " must be >= 0");
}
}
private static void verifyPadding(int padding) {
if (padding < 0 || padding > MAX_UNSIGNED_BYTE) {
throw new IllegalArgumentException("Invalid padding value: " + padding);
}
}
private void verifyPayloadLength(int payloadLength) {
if (payloadLength > maxFrameSize) {
throw new IllegalArgumentException("Total payload length " + payloadLength
+ " exceeds max frame length.");
}
}
private static void verifyWeight(short weight) {
if (weight < MIN_WEIGHT || weight > MAX_WEIGHT) {
throw new IllegalArgumentException("Invalid weight: " + weight);
}
}
private static void verifyErrorCode(long errorCode) {
if (errorCode < 0 || errorCode > MAX_UNSIGNED_INT) {
throw new IllegalArgumentException("Invalid errorCode: " + errorCode);
}
}
private static void verifyWindowSizeIncrement(int windowSizeIncrement) {
if (windowSizeIncrement < 0) {
throw new IllegalArgumentException("WindowSizeIncrement must be >= 0");
}
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
/**
* Provides common functionality for {@link Http2HeaderTable}
*/
class DefaultHttp2HeaderTableListSize {
private int maxHeaderListSize = Integer.MAX_VALUE;
public void maxHeaderListSize(int max) throws Http2Exception {
if (max < 0) {
throw connectionError(PROTOCOL_ERROR, "Header List Size must be non-negative but was %d", max);
}
maxHeaderListSize = max;
}
public int maxHeaderListSize() {
return maxHeaderListSize;
}
}

View File

@ -0,0 +1,305 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.BinaryHeaders;
import io.netty.handler.codec.DefaultBinaryHeaders;
public class DefaultHttp2Headers extends DefaultBinaryHeaders implements Http2Headers {
/**
* Creates an instance that will convert all header names to lowercase.
*/
public DefaultHttp2Headers() {
this(true);
}
/**
* Creates an instance that can be configured to either do header field name conversion to
* lowercase, or not do any conversion at all.
* <p>
*
* <strong>Note</strong> that setting {@code forceKeyToLower} to {@code false} can violate the
* <a href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2">HTTP/2 specification</a>
* which specifies that a request or response containing an uppercase header field MUST be treated
* as malformed. Only set {@code forceKeyToLower} to {@code false} if you are explicitly using lowercase
* header field names and want to avoid the conversion to lowercase.
*
* @param forceKeyToLower if @{code false} no header name conversion will be performed
*/
public DefaultHttp2Headers(boolean forceKeyToLower) {
super(forceKeyToLower);
}
@Override
public Http2Headers add(AsciiString name, AsciiString value) {
super.add(name, value);
return this;
}
@Override
public Http2Headers add(AsciiString name, Iterable<? extends AsciiString> values) {
super.add(name, values);
return this;
}
@Override
public Http2Headers add(AsciiString name, AsciiString... values) {
super.add(name, values);
return this;
}
@Override
public Http2Headers addObject(AsciiString name, Object value) {
super.addObject(name, value);
return this;
}
@Override
public Http2Headers addObject(AsciiString name, Iterable<?> values) {
super.addObject(name, values);
return this;
}
@Override
public Http2Headers addObject(AsciiString name, Object... values) {
super.addObject(name, values);
return this;
}
@Override
public Http2Headers addBoolean(AsciiString name, boolean value) {
super.addBoolean(name, value);
return this;
}
@Override
public Http2Headers addChar(AsciiString name, char value) {
super.addChar(name, value);
return this;
}
@Override
public Http2Headers addByte(AsciiString name, byte value) {
super.addByte(name, value);
return this;
}
@Override
public Http2Headers addShort(AsciiString name, short value) {
super.addShort(name, value);
return this;
}
@Override
public Http2Headers addInt(AsciiString name, int value) {
super.addInt(name, value);
return this;
}
@Override
public Http2Headers addLong(AsciiString name, long value) {
super.addLong(name, value);
return this;
}
@Override
public Http2Headers addFloat(AsciiString name, float value) {
super.addFloat(name, value);
return this;
}
@Override
public Http2Headers addDouble(AsciiString name, double value) {
super.addDouble(name, value);
return this;
}
@Override
public Http2Headers addTimeMillis(AsciiString name, long value) {
super.addTimeMillis(name, value);
return this;
}
@Override
public Http2Headers add(BinaryHeaders headers) {
super.add(headers);
return this;
}
@Override
public Http2Headers set(AsciiString name, AsciiString value) {
super.set(name, value);
return this;
}
@Override
public Http2Headers set(AsciiString name, Iterable<? extends AsciiString> values) {
super.set(name, values);
return this;
}
@Override
public Http2Headers set(AsciiString name, AsciiString... values) {
super.set(name, values);
return this;
}
@Override
public Http2Headers setObject(AsciiString name, Object value) {
super.setObject(name, value);
return this;
}
@Override
public Http2Headers setObject(AsciiString name, Iterable<?> values) {
super.setObject(name, values);
return this;
}
@Override
public Http2Headers setObject(AsciiString name, Object... values) {
super.setObject(name, values);
return this;
}
@Override
public Http2Headers setBoolean(AsciiString name, boolean value) {
super.setBoolean(name, value);
return this;
}
@Override
public Http2Headers setChar(AsciiString name, char value) {
super.setChar(name, value);
return this;
}
@Override
public Http2Headers setByte(AsciiString name, byte value) {
super.setByte(name, value);
return this;
}
@Override
public Http2Headers setShort(AsciiString name, short value) {
super.setShort(name, value);
return this;
}
@Override
public Http2Headers setInt(AsciiString name, int value) {
super.setInt(name, value);
return this;
}
@Override
public Http2Headers setLong(AsciiString name, long value) {
super.setLong(name, value);
return this;
}
@Override
public Http2Headers setFloat(AsciiString name, float value) {
super.setFloat(name, value);
return this;
}
@Override
public Http2Headers setDouble(AsciiString name, double value) {
super.setDouble(name, value);
return this;
}
@Override
public Http2Headers setTimeMillis(AsciiString name, long value) {
super.setTimeMillis(name, value);
return this;
}
@Override
public Http2Headers set(BinaryHeaders headers) {
super.set(headers);
return this;
}
@Override
public Http2Headers setAll(BinaryHeaders headers) {
super.setAll(headers);
return this;
}
@Override
public Http2Headers clear() {
super.clear();
return this;
}
@Override
public Http2Headers method(AsciiString value) {
set(PseudoHeaderName.METHOD.value(), value);
return this;
}
@Override
public Http2Headers scheme(AsciiString value) {
set(PseudoHeaderName.SCHEME.value(), value);
return this;
}
@Override
public Http2Headers authority(AsciiString value) {
set(PseudoHeaderName.AUTHORITY.value(), value);
return this;
}
@Override
public Http2Headers path(AsciiString value) {
set(PseudoHeaderName.PATH.value(), value);
return this;
}
@Override
public Http2Headers status(AsciiString value) {
set(PseudoHeaderName.STATUS.value(), value);
return this;
}
@Override
public AsciiString method() {
return get(PseudoHeaderName.METHOD.value());
}
@Override
public AsciiString scheme() {
return get(PseudoHeaderName.SCHEME.value());
}
@Override
public AsciiString authority() {
return get(PseudoHeaderName.AUTHORITY.value());
}
@Override
public AsciiString path() {
return get(PseudoHeaderName.PATH.value());
}
@Override
public AsciiString status() {
return get(PseudoHeaderName.STATUS.value());
}
}

View File

@ -0,0 +1,120 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_MAX_HEADER_SIZE;
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.COMPRESSION_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import io.netty.handler.codec.AsciiString;
import java.io.IOException;
import java.io.InputStream;
import com.twitter.hpack.Decoder;
import com.twitter.hpack.HeaderListener;
public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder, Http2HeadersDecoder.Configuration {
private final Decoder decoder;
private final Http2HeaderTable headerTable;
public DefaultHttp2HeadersDecoder() {
this(DEFAULT_MAX_HEADER_SIZE, DEFAULT_HEADER_TABLE_SIZE);
}
public DefaultHttp2HeadersDecoder(int maxHeaderSize, int maxHeaderTableSize) {
decoder = new Decoder(maxHeaderSize, maxHeaderTableSize);
headerTable = new Http2HeaderTableDecoder();
}
@Override
public Http2HeaderTable headerTable() {
return headerTable;
}
@Override
public Configuration configuration() {
return this;
}
@Override
public Http2Headers decodeHeaders(ByteBuf headerBlock) throws Http2Exception {
InputStream in = new ByteBufInputStream(headerBlock);
try {
final Http2Headers headers = new DefaultHttp2Headers();
HeaderListener listener = new HeaderListener() {
@Override
public void addHeader(byte[] key, byte[] value, boolean sensitive) {
headers.add(new AsciiString(key, false), new AsciiString(value, false));
}
};
decoder.decode(in, listener);
boolean truncated = decoder.endHeaderBlock();
if (truncated) {
// TODO: what's the right thing to do here?
}
if (headers.size() > headerTable.maxHeaderListSize()) {
throw connectionError(PROTOCOL_ERROR, "Number of headers (%d) exceeds maxHeaderListSize (%d)",
headers.size(), headerTable.maxHeaderListSize());
}
return headers;
} catch (IOException e) {
throw connectionError(COMPRESSION_ERROR, e, e.getMessage());
} catch (Http2Exception e) {
throw e;
} catch (Throwable e) {
// Default handler for any other types of errors that may have occurred. For example,
// the the Header builder throws IllegalArgumentException if the key or value was invalid
// for any reason (e.g. the key was an invalid pseudo-header).
throw connectionError(PROTOCOL_ERROR, e, e.getMessage());
} finally {
try {
in.close();
} catch (IOException e) {
throw connectionError(INTERNAL_ERROR, e, e.getMessage());
}
}
}
/**
* {@link Http2HeaderTable} implementation to support {@link Http2HeadersDecoder}
*/
private final class Http2HeaderTableDecoder extends DefaultHttp2HeaderTableListSize implements Http2HeaderTable {
@Override
public void maxHeaderTableSize(int max) throws Http2Exception {
if (max < 0) {
throw connectionError(PROTOCOL_ERROR, "Header Table Size must be non-negative but was %d", max);
}
try {
decoder.setMaxHeaderTableSize(max);
} catch (Throwable t) {
throw connectionError(PROTOCOL_ERROR, t.getMessage(), t);
}
}
@Override
public int maxHeaderTableSize() {
return decoder.getMaxHeaderTableSize();
}
}
}

View File

@ -0,0 +1,142 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_HEADER_TABLE_SIZE;
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.COMPRESSION_ERROR;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufOutputStream;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.BinaryHeaders.EntryVisitor;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Collections;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import com.twitter.hpack.Encoder;
public class DefaultHttp2HeadersEncoder implements Http2HeadersEncoder, Http2HeadersEncoder.Configuration {
private final Encoder encoder;
private final ByteArrayOutputStream tableSizeChangeOutput = new ByteArrayOutputStream();
private final Set<String> sensitiveHeaders = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER);
private final Http2HeaderTable headerTable;
public DefaultHttp2HeadersEncoder() {
this(DEFAULT_HEADER_TABLE_SIZE, Collections.<String>emptySet());
}
public DefaultHttp2HeadersEncoder(int maxHeaderTableSize, Set<String> sensitiveHeaders) {
encoder = new Encoder(maxHeaderTableSize);
this.sensitiveHeaders.addAll(sensitiveHeaders);
headerTable = new Http2HeaderTableEncoder();
}
@Override
public void encodeHeaders(Http2Headers headers, ByteBuf buffer) throws Http2Exception {
final OutputStream stream = new ByteBufOutputStream(buffer);
try {
if (headers.size() > headerTable.maxHeaderListSize()) {
throw connectionError(PROTOCOL_ERROR, "Number of headers (%d) exceeds maxHeaderListSize (%d)",
headers.size(), headerTable.maxHeaderListSize());
}
// If there was a change in the table size, serialize the output from the encoder
// resulting from that change.
if (tableSizeChangeOutput.size() > 0) {
buffer.writeBytes(tableSizeChangeOutput.toByteArray());
tableSizeChangeOutput.reset();
}
// Write pseudo headers first as required by the HTTP/2 spec.
for (Http2Headers.PseudoHeaderName pseudoHeader : Http2Headers.PseudoHeaderName.values()) {
AsciiString name = pseudoHeader.value();
AsciiString value = headers.get(name);
if (value != null) {
encodeHeader(name, value, stream);
}
}
headers.forEachEntry(new EntryVisitor() {
@Override
public boolean visit(Entry<AsciiString, AsciiString> entry) throws Exception {
final AsciiString name = entry.getKey();
final AsciiString value = entry.getValue();
if (!Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
encodeHeader(name, value, stream);
}
return true;
}
});
} catch (Http2Exception e) {
throw e;
} catch (Throwable t) {
throw connectionError(COMPRESSION_ERROR, t, "Failed encoding headers block: %s", t.getMessage());
} finally {
try {
stream.close();
} catch (IOException e) {
throw connectionError(INTERNAL_ERROR, e, e.getMessage());
}
}
}
@Override
public Http2HeaderTable headerTable() {
return headerTable;
}
@Override
public Configuration configuration() {
return this;
}
private void encodeHeader(AsciiString key, AsciiString value, OutputStream stream) throws IOException {
boolean sensitive = sensitiveHeaders.contains(key.toString());
encoder.encodeHeader(stream, key.array(), value.array(), sensitive);
}
/**
* {@link Http2HeaderTable} implementation to support {@link Http2HeadersEncoder}
*/
private final class Http2HeaderTableEncoder extends DefaultHttp2HeaderTableListSize implements Http2HeaderTable {
@Override
public void maxHeaderTableSize(int max) throws Http2Exception {
if (max < 0) {
throw connectionError(PROTOCOL_ERROR, "Header Table Size must be non-negative but was %d", max);
}
try {
// No headers should be emitted. If they are, we throw.
encoder.setMaxHeaderTableSize(tableSizeChangeOutput, max);
} catch (IOException e) {
throw new Http2Exception(COMPRESSION_ERROR, e.getMessage(), e);
} catch (Throwable t) {
throw new Http2Exception(PROTOCOL_ERROR, t.getMessage(), t);
}
}
@Override
public int maxHeaderTableSize() {
return encoder.getMaxHeaderTableSize();
}
}
}

View File

@ -0,0 +1,394 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_INITIAL_WINDOW_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_INITIAL_WINDOW_SIZE;
import static io.netty.handler.codec.http2.Http2Error.FLOW_CONTROL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import static io.netty.handler.codec.http2.Http2Exception.streamError;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static java.lang.Math.max;
import static java.lang.Math.min;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http2.Http2Exception.CompositeStreamException;
import io.netty.handler.codec.http2.Http2Exception.StreamException;
/**
* Basic implementation of {@link Http2LocalFlowController}.
*/
public class DefaultHttp2LocalFlowController implements Http2LocalFlowController {
private static final int DEFAULT_COMPOSITE_EXCEPTION_SIZE = 4;
/**
* The default ratio of window size to initial window size below which a {@code WINDOW_UPDATE}
* is sent to expand the window.
*/
public static final float DEFAULT_WINDOW_UPDATE_RATIO = 0.5f;
private final Http2Connection connection;
private final Http2FrameWriter frameWriter;
private volatile float windowUpdateRatio;
private volatile int initialWindowSize = DEFAULT_WINDOW_SIZE;
public DefaultHttp2LocalFlowController(Http2Connection connection, Http2FrameWriter frameWriter) {
this(connection, frameWriter, DEFAULT_WINDOW_UPDATE_RATIO);
}
public DefaultHttp2LocalFlowController(Http2Connection connection,
Http2FrameWriter frameWriter, float windowUpdateRatio) {
this.connection = checkNotNull(connection, "connection");
this.frameWriter = checkNotNull(frameWriter, "frameWriter");
windowUpdateRatio(windowUpdateRatio);
// Add a flow state for the connection.
final Http2Stream connectionStream = connection.connectionStream();
connectionStream.setProperty(FlowState.class, new FlowState(connectionStream, initialWindowSize));
// Register for notification of new streams.
connection.addListener(new Http2ConnectionAdapter() {
@Override
public void streamAdded(Http2Stream stream) {
stream.setProperty(FlowState.class, new FlowState(stream, 0));
}
@Override
public void streamActive(Http2Stream stream) {
// Need to be sure the stream's initial window is adjusted for SETTINGS
// frames which may have been exchanged while it was in IDLE
state(stream).window(initialWindowSize);
}
});
}
@Override
public void initialWindowSize(int newWindowSize) throws Http2Exception {
int delta = newWindowSize - initialWindowSize;
initialWindowSize = newWindowSize;
CompositeStreamException compositeException = null;
for (Http2Stream stream : connection.activeStreams()) {
try {
// Increment flow control window first so state will be consistent if overflow is detected
FlowState state = state(stream);
state.incrementFlowControlWindows(delta);
state.incrementInitialStreamWindow(delta);
} catch (StreamException e) {
if (compositeException == null) {
compositeException = new CompositeStreamException(e.error(), DEFAULT_COMPOSITE_EXCEPTION_SIZE);
}
compositeException.add(e);
}
}
if (compositeException != null) {
throw compositeException;
}
}
@Override
public int initialWindowSize() {
return initialWindowSize;
}
@Override
public int windowSize(Http2Stream stream) {
return state(stream).window();
}
@Override
public void incrementWindowSize(ChannelHandlerContext ctx, Http2Stream stream, int delta) throws Http2Exception {
checkNotNull(ctx, "ctx");
FlowState state = state(stream);
// Just add the delta to the stream-specific initial window size so that the next time the window
// expands it will grow to the new initial size.
state.incrementInitialStreamWindow(delta);
state.writeWindowUpdateIfNeeded(ctx);
}
@Override
public void consumeBytes(ChannelHandlerContext ctx, Http2Stream stream, int numBytes)
throws Http2Exception {
state(stream).consumeBytes(ctx, numBytes);
}
@Override
public int unconsumedBytes(Http2Stream stream) {
return state(stream).unconsumedBytes();
}
private static void checkValidRatio(float ratio) {
if (Double.compare(ratio, 0.0) <= 0 || Double.compare(ratio, 1.0) >= 0) {
throw new IllegalArgumentException("Invalid ratio: " + ratio);
}
}
/**
* The window update ratio is used to determine when a window update must be sent. If the ratio
* of bytes processed since the last update has meet or exceeded this ratio then a window update will
* be sent. This is the global window update ratio that will be used for new streams.
* @param ratio the ratio to use when checking if a {@code WINDOW_UPDATE} is determined necessary for new streams.
* @throws IllegalArgumentException If the ratio is out of bounds (0, 1).
*/
public void windowUpdateRatio(float ratio) {
checkValidRatio(ratio);
windowUpdateRatio = ratio;
}
/**
* The window update ratio is used to determine when a window update must be sent. If the ratio
* of bytes processed since the last update has meet or exceeded this ratio then a window update will
* be sent. This is the global window update ratio that will be used for new streams.
*/
public float windowUpdateRatio() {
return windowUpdateRatio;
}
/**
* The window update ratio is used to determine when a window update must be sent. If the ratio
* of bytes processed since the last update has meet or exceeded this ratio then a window update will
* be sent. This window update ratio will only be applied to {@code streamId}.
* <p>
* Note it is the responsibly of the caller to ensure that the the
* initial {@code SETTINGS} frame is sent before this is called. It would
* be considered a {@link Http2Error#PROTOCOL_ERROR} if a {@code WINDOW_UPDATE}
* was generated by this method before the initial {@code SETTINGS} frame is sent.
* @param ctx the context to use if a {@code WINDOW_UPDATE} is determined necessary.
* @param stream the stream for which {@code ratio} applies to.
* @param ratio the ratio to use when checking if a {@code WINDOW_UPDATE} is determined necessary.
* @throws Http2Exception If a protocol-error occurs while generating {@code WINDOW_UPDATE} frames
*/
public void windowUpdateRatio(ChannelHandlerContext ctx, Http2Stream stream, float ratio) throws Http2Exception {
checkValidRatio(ratio);
FlowState state = state(stream);
state.windowUpdateRatio(ratio);
state.writeWindowUpdateIfNeeded(ctx);
}
/**
* The window update ratio is used to determine when a window update must be sent. If the ratio
* of bytes processed since the last update has meet or exceeded this ratio then a window update will
* be sent. This window update ratio will only be applied to {@code streamId}.
* @throws Http2Exception If no stream corresponding to {@code stream} could be found.
*/
public float windowUpdateRatio(Http2Stream stream) throws Http2Exception {
return state(stream).windowUpdateRatio();
}
@Override
public void receiveFlowControlledFrame(ChannelHandlerContext ctx, Http2Stream stream, ByteBuf data,
int padding, boolean endOfStream) throws Http2Exception {
int dataLength = data.readableBytes() + padding;
// Apply the connection-level flow control
connectionState().receiveFlowControlledFrame(dataLength);
// Apply the stream-level flow control
FlowState state = state(stream);
state.endOfStream(endOfStream);
state.receiveFlowControlledFrame(dataLength);
}
private FlowState connectionState() {
return state(connection.connectionStream());
}
private FlowState state(Http2Stream stream) {
checkNotNull(stream, "stream");
return stream.getProperty(FlowState.class);
}
/**
* Flow control window state for an individual stream.
*/
private final class FlowState {
private final Http2Stream stream;
/**
* The actual flow control window that is decremented as soon as {@code DATA} arrives.
*/
private int window;
/**
* A view of {@link #window} that is used to determine when to send {@code WINDOW_UPDATE}
* frames. Decrementing this window for received {@code DATA} frames is delayed until the
* application has indicated that the data has been fully processed. This prevents sending
* a {@code WINDOW_UPDATE} until the number of processed bytes drops below the threshold.
*/
private int processedWindow;
/**
* This is what is used to determine how many bytes need to be returned relative to {@link #processedWindow}.
* Each stream has their own initial window size.
*/
private volatile int initialStreamWindowSize;
/**
* This is used to determine when {@link #processedWindow} is sufficiently far away from
* {@link #initialStreamWindowSize} such that a {@code WINDOW_UPDATE} should be sent.
* Each stream has their own window update ratio.
*/
private volatile float streamWindowUpdateRatio;
private int lowerBound;
private boolean endOfStream;
FlowState(Http2Stream stream, int initialWindowSize) {
this.stream = stream;
window(initialWindowSize);
streamWindowUpdateRatio = windowUpdateRatio;
}
int window() {
return window;
}
void window(int initialWindowSize) {
window = processedWindow = initialStreamWindowSize = initialWindowSize;
}
void endOfStream(boolean endOfStream) {
this.endOfStream = endOfStream;
}
float windowUpdateRatio() {
return streamWindowUpdateRatio;
}
void windowUpdateRatio(float ratio) {
streamWindowUpdateRatio = ratio;
}
/**
* Increment the initial window size for this stream.
* @param delta The amount to increase the initial window size by.
*/
void incrementInitialStreamWindow(int delta) {
// Clip the delta so that the resulting initialStreamWindowSize falls within the allowed range.
int newValue = (int) min(MAX_INITIAL_WINDOW_SIZE,
max(MIN_INITIAL_WINDOW_SIZE, initialStreamWindowSize + (long) delta));
delta = newValue - initialStreamWindowSize;
initialStreamWindowSize += delta;
}
/**
* Increment the windows which are used to determine many bytes have been processed.
* @param delta The amount to increment the window by.
* @throws Http2Exception if integer overflow occurs on the window.
*/
void incrementFlowControlWindows(int delta) throws Http2Exception {
if (delta > 0 && window > MAX_INITIAL_WINDOW_SIZE - delta) {
throw streamError(stream.id(), FLOW_CONTROL_ERROR,
"Flow control window overflowed for stream: %d", stream.id());
}
window += delta;
processedWindow += delta;
lowerBound = delta < 0 ? delta : 0;
}
/**
* A flow control event has occurred and we should decrement the amount of available bytes for this stream.
* @param dataLength The amount of data to for which this stream is no longer eligible to use for flow control.
* @throws Http2Exception If too much data is used relative to how much is available.
*/
void receiveFlowControlledFrame(int dataLength) throws Http2Exception {
assert dataLength > 0;
// Apply the delta. Even if we throw an exception we want to have taken this delta into account.
window -= dataLength;
// Window size can become negative if we sent a SETTINGS frame that reduces the
// size of the transfer window after the peer has written data frames.
// The value is bounded by the length that SETTINGS frame decrease the window.
// This difference is stored for the connection when writing the SETTINGS frame
// and is cleared once we send a WINDOW_UPDATE frame.
if (window < lowerBound) {
throw streamError(stream.id(), FLOW_CONTROL_ERROR,
"Flow control window exceeded for stream: %d", stream.id());
}
}
/**
* Returns the processed bytes for this stream.
*/
void returnProcessedBytes(int delta) throws Http2Exception {
if (processedWindow - delta < window) {
throw streamError(stream.id(), INTERNAL_ERROR,
"Attempting to return too many bytes for stream %d", stream.id());
}
processedWindow -= delta;
}
void consumeBytes(ChannelHandlerContext ctx, int numBytes) throws Http2Exception {
if (stream.id() == CONNECTION_STREAM_ID) {
throw new UnsupportedOperationException("Returning bytes for the connection window is not supported");
}
if (numBytes <= 0) {
throw new IllegalArgumentException("numBytes must be positive");
}
// Return bytes to the connection window
FlowState connectionState = connectionState();
connectionState.returnProcessedBytes(numBytes);
connectionState.writeWindowUpdateIfNeeded(ctx);
// Return the bytes processed and update the window.
returnProcessedBytes(numBytes);
writeWindowUpdateIfNeeded(ctx);
}
int unconsumedBytes() {
return processedWindow - window;
}
/**
* Updates the flow control window for this stream if it is appropriate.
*/
void writeWindowUpdateIfNeeded(ChannelHandlerContext ctx) throws Http2Exception {
if (endOfStream || initialStreamWindowSize <= 0) {
return;
}
int threshold = (int) (initialStreamWindowSize * streamWindowUpdateRatio);
if (processedWindow <= threshold) {
writeWindowUpdate(ctx);
}
}
/**
* Called to perform a window update for this stream (or connection). Updates the window size back
* to the size of the initial window and sends a window update frame to the remote endpoint.
*/
void writeWindowUpdate(ChannelHandlerContext ctx) throws Http2Exception {
// Expand the window for this stream back to the size of the initial window.
int deltaWindowSize = initialStreamWindowSize - processedWindow;
try {
incrementFlowControlWindows(deltaWindowSize);
} catch (Throwable t) {
throw connectionError(INTERNAL_ERROR, t,
"Attempting to return too many bytes for stream %d", stream.id());
}
// Send a window update for the stream/connection.
frameWriter.writeWindowUpdate(ctx, stream.id(), deltaWindowSize, ctx.newPromise());
ctx.flush();
}
}
}

View File

@ -0,0 +1,710 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_SIZE;
import static io.netty.handler.codec.http2.Http2Error.FLOW_CONTROL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.streamError;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import static java.lang.Math.max;
import static java.lang.Math.min;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Basic implementation of {@link Http2RemoteFlowController}.
*/
public class DefaultHttp2RemoteFlowController implements Http2RemoteFlowController {
/**
* A {@link Comparator} that sorts streams in ascending order the amount of streamable data.
*/
private static final Comparator<Http2Stream> WEIGHT_ORDER = new Comparator<Http2Stream>() {
@Override
public int compare(Http2Stream o1, Http2Stream o2) {
return o2.weight() - o1.weight();
}
};
private final Http2Connection connection;
private final Http2FrameWriter frameWriter;
private int initialWindowSize = DEFAULT_WINDOW_SIZE;
private ChannelHandlerContext ctx;
private boolean frameSent;
public DefaultHttp2RemoteFlowController(Http2Connection connection, Http2FrameWriter frameWriter) {
this.connection = checkNotNull(connection, "connection");
this.frameWriter = checkNotNull(frameWriter, "frameWriter");
// Add a flow state for the connection.
connection.connectionStream().setProperty(FlowState.class,
new FlowState(connection.connectionStream(), initialWindowSize));
// Register for notification of new streams.
connection.addListener(new Http2ConnectionAdapter() {
@Override
public void streamAdded(Http2Stream stream) {
// Just add a new flow state to the stream.
stream.setProperty(FlowState.class, new FlowState(stream, 0));
}
@Override
public void streamActive(Http2Stream stream) {
// Need to be sure the stream's initial window is adjusted for SETTINGS
// frames which may have been exchanged while it was in IDLE
state(stream).window(initialWindowSize);
}
@Override
public void streamHalfClosed(Http2Stream stream) {
if (!stream.localSideOpen()) {
// Any pending frames can never be written, clear and
// write errors for any pending frames.
state(stream).clear();
}
}
@Override
public void streamInactive(Http2Stream stream) {
// Any pending frames can never be written, clear and
// write errors for any pending frames.
state(stream).clear();
}
@Override
public void priorityTreeParentChanged(Http2Stream stream, Http2Stream oldParent) {
Http2Stream parent = stream.parent();
if (parent != null) {
int delta = state(stream).streamableBytesForTree();
if (delta != 0) {
state(parent).incrementStreamableBytesForTree(delta);
}
}
}
@Override
public void priorityTreeParentChanging(Http2Stream stream, Http2Stream newParent) {
Http2Stream parent = stream.parent();
if (parent != null) {
int delta = -state(stream).streamableBytesForTree();
if (delta != 0) {
state(parent).incrementStreamableBytesForTree(delta);
}
}
}
});
}
@Override
public void initialWindowSize(int newWindowSize) throws Http2Exception {
if (newWindowSize < 0) {
throw new IllegalArgumentException("Invalid initial window size: " + newWindowSize);
}
int delta = newWindowSize - initialWindowSize;
initialWindowSize = newWindowSize;
for (Http2Stream stream : connection.activeStreams()) {
// Verify that the maximum value is not exceeded by this change.
state(stream).incrementStreamWindow(delta);
}
if (delta > 0) {
// The window size increased, send any pending frames for all streams.
writePendingBytes();
}
}
@Override
public int initialWindowSize() {
return initialWindowSize;
}
@Override
public int windowSize(Http2Stream stream) {
return state(stream).window();
}
@Override
public void incrementWindowSize(ChannelHandlerContext ctx, Http2Stream stream, int delta) throws Http2Exception {
if (stream.id() == CONNECTION_STREAM_ID) {
// Update the connection window and write any pending frames for all streams.
connectionState().incrementStreamWindow(delta);
writePendingBytes();
} else {
// Update the stream window and write any pending frames for the stream.
FlowState state = state(stream);
state.incrementStreamWindow(delta);
frameSent = false;
state.writeBytes(state.writableWindow());
if (frameSent) {
flush();
}
}
}
@Override
public ChannelFuture sendFlowControlledFrame(ChannelHandlerContext ctx, Http2Stream stream,
ByteBuf data, int padding, boolean endStream, ChannelPromise promise) {
checkNotNull(ctx, "ctx");
checkNotNull(promise, "promise");
checkNotNull(data, "data");
if (this.ctx != null && this.ctx != ctx) {
throw new IllegalArgumentException("Writing data from multiple ChannelHandlerContexts is not supported");
}
if (padding < 0) {
throw new IllegalArgumentException("padding must be >= 0");
}
// Save the context. We'll use this later when we write pending bytes.
this.ctx = ctx;
try {
FlowState state = state(stream);
int window = state.writableWindow();
boolean framesAlreadyQueued = state.hasFrame();
FlowState.Frame frame = state.newFrame(promise, data, padding, endStream);
if (!framesAlreadyQueued && window >= frame.size()) {
// Window size is large enough to send entire data frame
frame.write();
ctx.flush();
return promise;
}
// Enqueue the frame to be written when the window size permits.
frame.enqueue();
if (framesAlreadyQueued || window <= 0) {
// Stream already has frames pending or is stalled, don't send anything now.
return promise;
}
// Create and send a partial frame up to the window size.
frame.split(window).write();
ctx.flush();
} catch (Throwable e) {
promise.setFailure(e);
}
return promise;
}
@Override
public ChannelFuture lastFlowControlledFrameSent(Http2Stream stream) {
FlowState state = state(stream);
return state != null ? state.lastNewFrame() : null;
}
/**
* For testing purposes only. Exposes the number of streamable bytes for the tree rooted at
* the given stream.
*/
int streamableBytesForTree(Http2Stream stream) {
return state(stream).streamableBytesForTree();
}
private static FlowState state(Http2Stream stream) {
checkNotNull(stream, "stream");
return stream.getProperty(FlowState.class);
}
private FlowState connectionState() {
return state(connection.connectionStream());
}
/**
* Returns the flow control window for the entire connection.
*/
private int connectionWindow() {
return connectionState().window();
}
/**
* Flushes the {@link ChannelHandlerContext} if we've received any data frames.
*/
private void flush() {
if (ctx != null) {
ctx.flush();
}
}
/**
* Writes as many pending bytes as possible, according to stream priority.
*/
private void writePendingBytes() throws Http2Exception {
Http2Stream connectionStream = connection.connectionStream();
int connectionWindow = state(connectionStream).window();
if (connectionWindow > 0) {
frameSent = false;
writeChildren(connectionStream, connectionWindow);
for (Http2Stream stream : connection.activeStreams()) {
writeChildNode(state(stream));
}
if (frameSent) {
flush();
}
}
}
/**
* Write the children of {@code parent} in the priority tree. This will allocate bytes by stream weight.
* @param parent The parent of the nodes which will be written.
* @param connectionWindow The connection window this is available for use at this point in the tree.
* @return An object summarizing the write and allocation results.
*/
private int writeChildren(Http2Stream parent, int connectionWindow) {
FlowState state = state(parent);
if (state.streamableBytesForTree() <= 0) {
return 0;
}
int bytesAllocated = 0;
// If the number of streamable bytes for this tree will fit in the connection window
// then there is no need to prioritize the bytes...everyone sends what they have
if (state.streamableBytesForTree() <= connectionWindow) {
for (Http2Stream child : parent.children()) {
state = state(child);
int bytesForChild = state.streamableBytes();
if (bytesForChild > 0 || state.hasFrame()) {
state.allocate(bytesForChild);
writeChildNode(state);
bytesAllocated += bytesForChild;
connectionWindow -= bytesForChild;
}
int childBytesAllocated = writeChildren(child, connectionWindow);
bytesAllocated += childBytesAllocated;
connectionWindow -= childBytesAllocated;
}
return bytesAllocated;
}
// This is the priority algorithm which will divide the available bytes based
// upon stream weight relative to its peers
Http2Stream[] children = parent.children().toArray(new Http2Stream[parent.numChildren()]);
Arrays.sort(children, WEIGHT_ORDER);
int totalWeight = parent.totalChildWeights();
for (int tail = children.length; tail > 0;) {
int head = 0;
int nextTail = 0;
int nextTotalWeight = 0;
int nextConnectionWindow = connectionWindow;
for (; head < tail && nextConnectionWindow > 0; ++head) {
Http2Stream child = children[head];
state = state(child);
int weight = child.weight();
double weightRatio = weight / (double) totalWeight;
int bytesForTree = Math.min(nextConnectionWindow, (int) Math.ceil(connectionWindow * weightRatio));
int bytesForChild = Math.min(state.streamableBytes(), bytesForTree);
if (bytesForChild > 0 || state.hasFrame()) {
state.allocate(bytesForChild);
bytesAllocated += bytesForChild;
nextConnectionWindow -= bytesForChild;
bytesForTree -= bytesForChild;
// If this subtree still wants to send then re-insert into children list and re-consider for next
// iteration. This is needed because we don't yet know if all the peers will be able to use
// all of their "fair share" of the connection window, and if they don't use it then we should
// divide their unused shared up for the peers who still want to send.
if (state.streamableBytesForTree() - bytesForChild > 0) {
children[nextTail++] = child;
nextTotalWeight += weight;
}
if (state.streamableBytes() - bytesForChild == 0) {
writeChildNode(state);
}
}
if (bytesForTree > 0) {
int childBytesAllocated = writeChildren(child, bytesForTree);
bytesAllocated += childBytesAllocated;
nextConnectionWindow -= childBytesAllocated;
}
}
connectionWindow = nextConnectionWindow;
totalWeight = nextTotalWeight;
tail = nextTail;
}
return bytesAllocated;
}
/**
* Write bytes allocated to {@code state}
*/
private static void writeChildNode(FlowState state) {
state.writeBytes(state.allocated());
state.resetAllocated();
}
/**
* The outbound flow control state for a single stream.
*/
final class FlowState {
private final Queue<Frame> pendingWriteQueue;
private final Http2Stream stream;
private int window;
private int pendingBytes;
private int streamableBytesForTree;
private int allocated;
private ChannelFuture lastNewFrame;
FlowState(Http2Stream stream, int initialWindowSize) {
this.stream = stream;
window(initialWindowSize);
pendingWriteQueue = new ArrayDeque<Frame>(2);
}
int window() {
return window;
}
void window(int initialWindowSize) {
window = initialWindowSize;
}
/**
* Increment the number of bytes allocated to this stream by the priority algorithm
*/
void allocate(int bytes) {
allocated += bytes;
}
/**
* Gets the number of bytes that have been allocated to this stream by the priority algorithm.
*/
int allocated() {
return allocated;
}
/**
* Reset the number of bytes that have been allocated to this stream by the priority algorithm.
*/
void resetAllocated() {
allocated = 0;
}
/**
* Increments the flow control window for this stream by the given delta and returns the new value.
*/
int incrementStreamWindow(int delta) throws Http2Exception {
if (delta > 0 && Integer.MAX_VALUE - delta < window) {
throw streamError(stream.id(), FLOW_CONTROL_ERROR,
"Window size overflow for stream: %d", stream.id());
}
int previouslyStreamable = streamableBytes();
window += delta;
// Update this branch of the priority tree if the streamable bytes have changed for this node.
int streamableDelta = streamableBytes() - previouslyStreamable;
if (streamableDelta != 0) {
incrementStreamableBytesForTree(streamableDelta);
}
return window;
}
/**
* Returns the future for the last new frame created for this stream.
*/
ChannelFuture lastNewFrame() {
return lastNewFrame;
}
/**
* Returns the maximum writable window (minimum of the stream and connection windows).
*/
int writableWindow() {
return min(window, connectionWindow());
}
/**
* Returns the number of pending bytes for this node that will fit within the {@link #window}. This is used for
* the priority algorithm to determine the aggregate total for {@link #priorityBytes} at each node. Each node
* only takes into account it's stream window so that when a change occurs to the connection window, these
* values need not change (i.e. no tree traversal is required).
*/
int streamableBytes() {
return max(0, min(pendingBytes, window));
}
int streamableBytesForTree() {
return streamableBytesForTree;
}
/**
* Creates a new frame with the given values but does not add it to the pending queue.
*/
Frame newFrame(final ChannelPromise promise, ByteBuf data, int padding, boolean endStream) {
// Store this as the future for the most recent write attempt.
lastNewFrame = promise;
return new Frame(new SimplePromiseAggregator(promise), data, padding, endStream);
}
/**
* Indicates whether or not there are frames in the pending queue.
*/
boolean hasFrame() {
return !pendingWriteQueue.isEmpty();
}
/**
* Returns the the head of the pending queue, or {@code null} if empty.
*/
Frame peek() {
return pendingWriteQueue.peek();
}
/**
* Clears the pending queue and writes errors for each remaining frame.
*/
void clear() {
for (;;) {
Frame frame = pendingWriteQueue.poll();
if (frame == null) {
break;
}
frame.writeError(streamError(stream.id(), INTERNAL_ERROR,
"Stream closed before write could take place"));
}
}
/**
* Writes up to the number of bytes from the pending queue. May write less if limited by the writable window, by
* the number of pending writes available, or because a frame does not support splitting on arbitrary
* boundaries.
*/
int writeBytes(int bytes) {
if (!stream.localSideOpen()) {
return 0;
}
int bytesAttempted = 0;
int maxBytes = min(bytes, writableWindow());
while (hasFrame()) {
Frame pendingWrite = peek();
if (maxBytes >= pendingWrite.size()) {
// Window size is large enough to send entire data frame
bytesAttempted += pendingWrite.size();
pendingWrite.write();
} else if (maxBytes <= 0) {
// No data from the current frame can be written - we're done.
// We purposely check this after first testing the size of the
// pending frame to properly handle zero-length frame.
break;
} else {
// We can send a partial frame
Frame partialFrame = pendingWrite.split(maxBytes);
bytesAttempted += partialFrame.size();
partialFrame.write();
}
// Update the threshold.
maxBytes = min(bytes - bytesAttempted, writableWindow());
}
return bytesAttempted;
}
/**
* Recursively increments the streamable bytes for this branch in the priority tree starting at the current
* node.
*/
void incrementStreamableBytesForTree(int numBytes) {
streamableBytesForTree += numBytes;
if (!stream.isRoot()) {
state(stream.parent()).incrementStreamableBytesForTree(numBytes);
}
}
/**
* A wrapper class around the content of a data frame.
*/
private final class Frame {
final ByteBuf data;
final boolean endStream;
final SimplePromiseAggregator promiseAggregator;
final ChannelPromise promise;
int padding;
boolean enqueued;
Frame(SimplePromiseAggregator promiseAggregator, ByteBuf data, int padding, boolean endStream) {
this.data = data;
this.padding = padding;
this.endStream = endStream;
this.promiseAggregator = promiseAggregator;
promise = ctx.newPromise();
promiseAggregator.add(promise);
}
/**
* Gets the total size (in bytes) of this frame including the data and padding.
*/
int size() {
return data.readableBytes() + padding;
}
void enqueue() {
if (!enqueued) {
enqueued = true;
pendingWriteQueue.offer(this);
// Increment the number of pending bytes for this stream.
incrementPendingBytes(size());
}
}
/**
* Increments the number of pending bytes for this node. If there was any change to the number of bytes that
* fit into the stream window, then {@link #incrementStreamableBytesForTree} to recursively update this
* branch of the priority tree.
*/
private void incrementPendingBytes(int numBytes) {
int previouslyStreamable = streamableBytes();
pendingBytes += numBytes;
int delta = streamableBytes() - previouslyStreamable;
if (delta != 0) {
incrementStreamableBytesForTree(delta);
}
}
/**
* Writes the frame and decrements the stream and connection window sizes. If the frame is in the pending
* queue, the written bytes are removed from this branch of the priority tree.
* <p>
* Note: this does not flush the {@link ChannelHandlerContext}.
*/
void write() {
// Using a do/while loop because if the buffer is empty we still need to call
// the writer once to send the empty frame.
final Http2FrameSizePolicy frameSizePolicy = frameWriter.configuration().frameSizePolicy();
do {
int bytesToWrite = size();
int frameBytes = min(bytesToWrite, frameSizePolicy.maxFrameSize());
if (frameBytes == bytesToWrite) {
// All the bytes fit into a single HTTP/2 frame, just send it all.
try {
connectionState().incrementStreamWindow(-bytesToWrite);
incrementStreamWindow(-bytesToWrite);
} catch (Http2Exception e) { // Should never get here since we're decrementing.
throw new RuntimeException("Invalid window state when writing frame: " + e.getMessage(), e);
}
frameWriter.writeData(ctx, stream.id(), data, padding, endStream, promise);
frameSent = true;
decrementPendingBytes(bytesToWrite);
if (enqueued) {
// It's enqueued - remove it from the head of the pending write queue.
pendingWriteQueue.remove();
}
return;
}
// Split a chunk that will fit into a single HTTP/2 frame and write it.
Frame frame = split(frameBytes);
frame.write();
} while (size() > 0);
}
/**
* Discards this frame, writing an error. If this frame is in the pending queue, the unwritten bytes are
* removed from this branch of the priority tree.
*/
void writeError(Http2Exception cause) {
decrementPendingBytes(size());
data.release();
promise.setFailure(cause);
}
/**
* Creates a new frame that is a view of this frame's data. The {@code maxBytes} are first split from the
* data buffer. If not all the requested bytes are available, the remaining bytes are then split from the
* padding (if available).
*
* @param maxBytes the maximum number of bytes that is allowed in the created frame.
* @return the partial frame.
*/
Frame split(int maxBytes) {
// The requested maxBytes should always be less than the size of this frame.
assert maxBytes < size() : "Attempting to split a frame for the full size.";
// Get the portion of the data buffer to be split. Limit to the readable bytes.
int dataSplit = min(maxBytes, data.readableBytes());
// Split any remaining bytes from the padding.
int paddingSplit = min(maxBytes - dataSplit, padding);
ByteBuf splitSlice = data.readSlice(dataSplit).retain();
padding -= paddingSplit;
Frame frame = new Frame(promiseAggregator, splitSlice, paddingSplit, false);
int totalBytesSplit = dataSplit + paddingSplit;
decrementPendingBytes(totalBytesSplit);
return frame;
}
/**
* If this frame is in the pending queue, decrements the number of pending bytes for the stream.
*/
void decrementPendingBytes(int bytes) {
if (enqueued) {
incrementPendingBytes(-bytes);
}
}
}
}
/**
* Lightweight promise aggregator.
*/
private static final class SimplePromiseAggregator {
final ChannelPromise promise;
final AtomicInteger awaiting = new AtomicInteger();
final ChannelFutureListener listener = new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (!future.isSuccess()) {
promise.tryFailure(future.cause());
} else {
if (awaiting.decrementAndGet() == 0) {
promise.trySuccess();
}
}
}
};
SimplePromiseAggregator(ChannelPromise promise) {
this.promise = promise;
}
void add(ChannelPromise promise) {
awaiting.incrementAndGet();
promise.addListener(listener);
}
}
}

View File

@ -0,0 +1,106 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static java.util.concurrent.TimeUnit.NANOSECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.concurrent.ScheduledFuture;
/**
* A {@link Http2StreamRemovalPolicy} that periodically runs garbage collection on streams that have
* been marked for removal.
*/
public class DefaultHttp2StreamRemovalPolicy extends ChannelHandlerAdapter implements
Http2StreamRemovalPolicy, Runnable {
/**
* The interval (in ns) at which the removed priority garbage collector runs.
*/
private static final long GARBAGE_COLLECTION_INTERVAL = SECONDS.toNanos(5);
private final Queue<Garbage> garbage = new ArrayDeque<Garbage>();
private ScheduledFuture<?> timerFuture;
private Action action;
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// Schedule the periodic timer for performing the policy check.
timerFuture = ctx.channel().eventLoop().scheduleWithFixedDelay(this,
GARBAGE_COLLECTION_INTERVAL,
GARBAGE_COLLECTION_INTERVAL,
NANOSECONDS);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// Cancel the periodic timer.
if (timerFuture != null) {
timerFuture.cancel(false);
timerFuture = null;
}
}
@Override
public void setAction(Action action) {
this.action = action;
}
@Override
public void markForRemoval(Http2Stream stream) {
garbage.add(new Garbage(stream));
}
/**
* Runs garbage collection of any streams marked for removal >
* {@link #GARBAGE_COLLECTION_INTERVAL} nanoseconds ago.
*/
@Override
public void run() {
if (garbage.isEmpty() || action == null) {
return;
}
long time = System.nanoTime();
for (;;) {
Garbage next = garbage.peek();
if (next == null) {
break;
}
if (time - next.removalTime > GARBAGE_COLLECTION_INTERVAL) {
garbage.remove();
action.removeStream(next.stream);
} else {
break;
}
}
}
/**
* Wrapper around a stream and its removal time.
*/
private static final class Garbage {
private final long removalTime = System.nanoTime();
private final Http2Stream stream;
Garbage(Http2Stream stream) {
this.stream = stream;
}
}
}

View File

@ -0,0 +1,436 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_ENCODING;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaderValues.DEFLATE;
import static io.netty.handler.codec.http.HttpHeaderValues.GZIP;
import static io.netty.handler.codec.http.HttpHeaderValues.IDENTITY;
import static io.netty.handler.codec.http.HttpHeaderValues.X_DEFLATE;
import static io.netty.handler.codec.http.HttpHeaderValues.X_GZIP;
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.streamError;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.compression.ZlibCodecFactory;
import io.netty.handler.codec.compression.ZlibWrapper;
/**
* A HTTP2 frame listener that will decompress data frames according to the {@code content-encoding} header for each
* stream. The decompression provided by this class will be applied to the data for the entire stream.
*/
public class DelegatingDecompressorFrameListener extends Http2FrameListenerDecorator {
private static final Http2ConnectionAdapter CLEAN_UP_LISTENER = new Http2ConnectionAdapter() {
@Override
public void streamRemoved(Http2Stream stream) {
final Http2Decompressor decompressor = decompressor(stream);
if (decompressor != null) {
cleanup(stream, decompressor);
}
}
};
private final Http2Connection connection;
private final boolean strict;
private boolean flowControllerInitialized;
public DelegatingDecompressorFrameListener(Http2Connection connection, Http2FrameListener listener) {
this(connection, listener, true);
}
public DelegatingDecompressorFrameListener(Http2Connection connection, Http2FrameListener listener,
boolean strict) {
super(listener);
this.connection = connection;
this.strict = strict;
connection.addListener(CLEAN_UP_LISTENER);
}
@Override
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)
throws Http2Exception {
final Http2Stream stream = connection.stream(streamId);
final Http2Decompressor decompressor = decompressor(stream);
if (decompressor == null) {
// The decompressor may be null if no compatible encoding type was found in this stream's headers
return listener.onDataRead(ctx, streamId, data, padding, endOfStream);
}
final EmbeddedChannel channel = decompressor.decompressor();
final int compressedBytes = data.readableBytes() + padding;
int processedBytes = 0;
decompressor.incrementCompressedBytes(compressedBytes);
try {
// call retain here as it will call release after its written to the channel
channel.writeInbound(data.retain());
ByteBuf buf = nextReadableBuf(channel);
if (buf == null && endOfStream && channel.finish()) {
buf = nextReadableBuf(channel);
}
if (buf == null) {
if (endOfStream) {
listener.onDataRead(ctx, streamId, Unpooled.EMPTY_BUFFER, padding, true);
}
// No new decompressed data was extracted from the compressed data. This means the application could
// not be provided with data and thus could not return how many bytes were processed. We will assume
// there is more data coming which will complete the decompression block. To allow for more data we
// return all bytes to the flow control window (so the peer can send more data).
decompressor.incrementDecompressedByes(compressedBytes);
processedBytes = compressedBytes;
} else {
try {
decompressor.incrementDecompressedByes(padding);
for (;;) {
ByteBuf nextBuf = nextReadableBuf(channel);
boolean decompressedEndOfStream = nextBuf == null && endOfStream;
if (decompressedEndOfStream && channel.finish()) {
nextBuf = nextReadableBuf(channel);
decompressedEndOfStream = nextBuf == null;
}
decompressor.incrementDecompressedByes(buf.readableBytes());
processedBytes += listener.onDataRead(ctx, streamId, buf, padding, decompressedEndOfStream);
if (nextBuf == null) {
break;
}
padding = 0; // Padding is only communicated once on the first iteration
buf.release();
buf = nextBuf;
}
} finally {
buf.release();
}
}
decompressor.incrementProcessedBytes(processedBytes);
// The processed bytes will be translated to pre-decompressed byte amounts by DecompressorGarbageCollector
return processedBytes;
} catch (Http2Exception e) {
// Consider all the bytes consumed because there was an error
decompressor.incrementProcessedBytes(compressedBytes);
throw e;
} catch (Throwable t) {
// Consider all the bytes consumed because there was an error
decompressor.incrementProcessedBytes(compressedBytes);
throw streamError(stream.id(), INTERNAL_ERROR, t,
"Decompressor error detected while delegating data read on streamId %d", stream.id());
}
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endStream) throws Http2Exception {
initDecompressor(streamId, headers, endStream);
listener.onHeadersRead(ctx, streamId, headers, padding, endStream);
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception {
initDecompressor(streamId, headers, endStream);
listener.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endStream);
}
/**
* Returns a new {@link EmbeddedChannel} that decodes the HTTP2 message content encoded in the specified
* {@code contentEncoding}.
*
* @param contentEncoding the value of the {@code content-encoding} header
* @return a new {@link ByteToMessageDecoder} if the specified encoding is supported. {@code null} otherwise
* (alternatively, you can throw a {@link Http2Exception} to block unknown encoding).
* @throws Http2Exception If the specified encoding is not not supported and warrants an exception
*/
protected EmbeddedChannel newContentDecompressor(AsciiString contentEncoding) throws Http2Exception {
if (GZIP.equalsIgnoreCase(contentEncoding) ||
X_GZIP.equalsIgnoreCase(contentEncoding)) {
return new EmbeddedChannel(ZlibCodecFactory.newZlibDecoder(ZlibWrapper.GZIP));
}
if (DEFLATE.equalsIgnoreCase(contentEncoding) ||
X_DEFLATE.equalsIgnoreCase(contentEncoding)) {
final ZlibWrapper wrapper = strict ? ZlibWrapper.ZLIB : ZlibWrapper.ZLIB_OR_NONE;
// To be strict, 'deflate' means ZLIB, but some servers were not implemented correctly.
return new EmbeddedChannel(ZlibCodecFactory.newZlibDecoder(wrapper));
}
// 'identity' or unsupported
return null;
}
/**
* Returns the expected content encoding of the decoded content. This getMethod returns {@code "identity"} by
* default, which is the case for most decompressors.
*
* @param contentEncoding the value of the {@code content-encoding} header
* @return the expected content encoding of the new content.
* @throws Http2Exception if the {@code contentEncoding} is not supported and warrants an exception
*/
protected AsciiString getTargetContentEncoding(@SuppressWarnings("UnusedParameters") AsciiString contentEncoding)
throws Http2Exception {
return IDENTITY;
}
/**
* Checks if a new decompressor object is needed for the stream identified by {@code streamId}.
* This method will modify the {@code content-encoding} header contained in {@code headers}.
*
* @param streamId The identifier for the headers inside {@code headers}
* @param headers Object representing headers which have been read
* @param endOfStream Indicates if the stream has ended
* @throws Http2Exception If the {@code content-encoding} is not supported
*/
private void initDecompressor(int streamId, Http2Headers headers, boolean endOfStream) throws Http2Exception {
final Http2Stream stream = connection.stream(streamId);
if (stream == null) {
return;
}
Http2Decompressor decompressor = decompressor(stream);
if (decompressor == null && !endOfStream) {
// Determine the content encoding.
AsciiString contentEncoding = headers.get(CONTENT_ENCODING);
if (contentEncoding == null) {
contentEncoding = IDENTITY;
}
final EmbeddedChannel channel = newContentDecompressor(contentEncoding);
if (channel != null) {
decompressor = new Http2Decompressor(channel);
stream.setProperty(Http2Decompressor.class, decompressor);
// Decode the content and remove or replace the existing headers
// so that the message looks like a decoded message.
AsciiString targetContentEncoding = getTargetContentEncoding(contentEncoding);
if (IDENTITY.equalsIgnoreCase(targetContentEncoding)) {
headers.remove(CONTENT_ENCODING);
} else {
headers.set(CONTENT_ENCODING, targetContentEncoding);
}
}
}
if (decompressor != null) {
// The content length will be for the compressed data. Since we will decompress the data
// this content-length will not be correct. Instead of queuing messages or delaying sending
// header frames...just remove the content-length header
headers.remove(CONTENT_LENGTH);
// The first time that we initialize a decompressor, decorate the local flow controller to
// properly convert consumed bytes.
if (!flowControllerInitialized) {
flowControllerInitialized = true;
connection.local().flowController(new ConsumedBytesConverter(connection.local().flowController()));
}
}
}
private static Http2Decompressor decompressor(Http2Stream stream) {
return (Http2Decompressor) (stream == null? null : stream.getProperty(Http2Decompressor.class));
}
/**
* Release remaining content from the {@link EmbeddedChannel} and remove the decompressor
* from the {@link Http2Stream}.
*
* @param stream The stream for which {@code decompressor} is the decompressor for
* @param decompressor The decompressor for {@code stream}
*/
private static void cleanup(Http2Stream stream, Http2Decompressor decompressor) {
final EmbeddedChannel channel = decompressor.decompressor();
if (channel.finish()) {
for (;;) {
final ByteBuf buf = channel.readInbound();
if (buf == null) {
break;
}
buf.release();
}
}
decompressor = stream.removeProperty(Http2Decompressor.class);
}
/**
* Read the next decompressed {@link ByteBuf} from the {@link EmbeddedChannel}
* or {@code null} if one does not exist.
*
* @param decompressor The channel to read from
* @return The next decoded {@link ByteBuf} from the {@link EmbeddedChannel} or {@code null} if one does not exist
*/
private static ByteBuf nextReadableBuf(EmbeddedChannel decompressor) {
for (;;) {
final ByteBuf buf = decompressor.readInbound();
if (buf == null) {
return null;
}
if (!buf.isReadable()) {
buf.release();
continue;
}
return buf;
}
}
/**
* A decorator around the local flow controller that converts consumed bytes from uncompressed to compressed.
*/
private static final class ConsumedBytesConverter implements Http2LocalFlowController {
private final Http2LocalFlowController flowController;
ConsumedBytesConverter(Http2LocalFlowController flowController) {
this.flowController = checkNotNull(flowController, "flowController");
}
@Override
public void initialWindowSize(int newWindowSize) throws Http2Exception {
flowController.initialWindowSize(newWindowSize);
}
@Override
public int initialWindowSize() {
return flowController.initialWindowSize();
}
@Override
public int windowSize(Http2Stream stream) {
return flowController.windowSize(stream);
}
@Override
public void incrementWindowSize(ChannelHandlerContext ctx, Http2Stream stream, int delta)
throws Http2Exception {
flowController.incrementWindowSize(ctx, stream, delta);
}
@Override
public void receiveFlowControlledFrame(ChannelHandlerContext ctx, Http2Stream stream,
ByteBuf data, int padding, boolean endOfStream) throws Http2Exception {
flowController.receiveFlowControlledFrame(ctx, stream, data, padding, endOfStream);
}
@Override
public void consumeBytes(ChannelHandlerContext ctx, Http2Stream stream, int numBytes)
throws Http2Exception {
Http2Decompressor decompressor = decompressor(stream);
Http2Decompressor copy = null;
try {
if (decompressor != null) {
// Make a copy before hand in case any exceptions occur we will roll back the state
copy = new Http2Decompressor(decompressor);
// Convert the uncompressed consumed bytes to compressed (on the wire) bytes.
numBytes = decompressor.consumeProcessedBytes(numBytes);
}
flowController.consumeBytes(ctx, stream, numBytes);
} catch (Http2Exception e) {
if (copy != null) {
stream.setProperty(Http2Decompressor.class, copy);
}
throw e;
} catch (Throwable t) {
if (copy != null) {
stream.setProperty(Http2Decompressor.class, copy);
}
throw new Http2Exception(INTERNAL_ERROR,
"Error while returning bytes to flow control window", t);
}
}
@Override
public int unconsumedBytes(Http2Stream stream) {
return flowController.unconsumedBytes(stream);
}
}
/**
* Provides the state for stream {@code DATA} frame decompression.
*/
private static final class Http2Decompressor {
private final EmbeddedChannel decompressor;
private int processed;
private int compressed;
private int decompressed;
Http2Decompressor(Http2Decompressor rhs) {
this(rhs.decompressor);
processed = rhs.processed;
compressed = rhs.compressed;
decompressed = rhs.decompressed;
}
Http2Decompressor(EmbeddedChannel decompressor) {
this.decompressor = decompressor;
}
/**
* Responsible for taking compressed bytes in and producing decompressed bytes.
*/
EmbeddedChannel decompressor() {
return decompressor;
}
/**
* Increment the number of decompressed bytes processed by the application.
*/
void incrementProcessedBytes(int delta) {
if (processed + delta < 0) {
throw new IllegalArgumentException("processed bytes cannot be negative");
}
processed += delta;
}
/**
* Increment the number of bytes received prior to doing any decompression.
*/
void incrementCompressedBytes(int delta) {
if (compressed + delta < 0) {
throw new IllegalArgumentException("compressed bytes cannot be negative");
}
compressed += delta;
}
/**
* Increment the number of bytes after the decompression process. Under normal circumstances this
* delta should not exceed {@link Http2Decompressor#processedBytes()}.
*/
void incrementDecompressedByes(int delta) {
if (decompressed + delta < 0) {
throw new IllegalArgumentException("decompressed bytes cannot be negative");
}
decompressed += delta;
}
/**
* Decrements {@link Http2Decompressor#processedBytes()} by {@code processedBytes} and determines the ratio
* between {@code processedBytes} and {@link Http2Decompressor#decompressedBytes()}.
* This ratio is used to decrement {@link Http2Decompressor#decompressedBytes()} and
* {@link Http2Decompressor#compressedBytes()}.
* @param processedBytes The number of post-decompressed bytes that have been processed.
* @return The number of pre-decompressed bytes that have been consumed.
*/
int consumeProcessedBytes(int processedBytes) {
// Consume the processed bytes first to verify that is is a valid amount
incrementProcessedBytes(-processedBytes);
double consumedRatio = processedBytes / (double) decompressed;
int consumedCompressed = Math.min(compressed, (int) Math.ceil(compressed * consumedRatio));
incrementDecompressedByes(-Math.min(decompressed, (int) Math.ceil(decompressed * consumedRatio)));
incrementCompressedBytes(-consumedCompressed);
return consumedCompressed;
}
}
}

View File

@ -0,0 +1,281 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.BinaryHeaders;
import io.netty.handler.codec.EmptyBinaryHeaders;
public final class EmptyHttp2Headers extends EmptyBinaryHeaders implements Http2Headers {
public static final EmptyHttp2Headers INSTANCE = new EmptyHttp2Headers();
private EmptyHttp2Headers() {
}
@Override
public Http2Headers add(AsciiString name, AsciiString value) {
super.add(name, value);
return this;
}
@Override
public Http2Headers add(AsciiString name, Iterable<? extends AsciiString> values) {
super.add(name, values);
return this;
}
@Override
public Http2Headers add(AsciiString name, AsciiString... values) {
super.add(name, values);
return this;
}
@Override
public Http2Headers addObject(AsciiString name, Object value) {
super.addObject(name, value);
return this;
}
@Override
public Http2Headers addObject(AsciiString name, Iterable<?> values) {
super.addObject(name, values);
return this;
}
@Override
public Http2Headers addObject(AsciiString name, Object... values) {
super.addObject(name, values);
return this;
}
@Override
public Http2Headers addBoolean(AsciiString name, boolean value) {
super.addBoolean(name, value);
return this;
}
@Override
public Http2Headers addChar(AsciiString name, char value) {
super.addChar(name, value);
return this;
}
@Override
public Http2Headers addByte(AsciiString name, byte value) {
super.addByte(name, value);
return this;
}
@Override
public Http2Headers addShort(AsciiString name, short value) {
super.addShort(name, value);
return this;
}
@Override
public Http2Headers addInt(AsciiString name, int value) {
super.addInt(name, value);
return this;
}
@Override
public Http2Headers addLong(AsciiString name, long value) {
super.addLong(name, value);
return this;
}
@Override
public Http2Headers addFloat(AsciiString name, float value) {
super.addFloat(name, value);
return this;
}
@Override
public Http2Headers addDouble(AsciiString name, double value) {
super.addDouble(name, value);
return this;
}
@Override
public Http2Headers addTimeMillis(AsciiString name, long value) {
super.addTimeMillis(name, value);
return this;
}
@Override
public Http2Headers add(BinaryHeaders headers) {
super.add(headers);
return this;
}
@Override
public Http2Headers set(AsciiString name, AsciiString value) {
super.set(name, value);
return this;
}
@Override
public Http2Headers set(AsciiString name, Iterable<? extends AsciiString> values) {
super.set(name, values);
return this;
}
@Override
public Http2Headers set(AsciiString name, AsciiString... values) {
super.set(name, values);
return this;
}
@Override
public Http2Headers setObject(AsciiString name, Object value) {
super.setObject(name, value);
return this;
}
@Override
public Http2Headers setObject(AsciiString name, Iterable<?> values) {
super.setObject(name, values);
return this;
}
@Override
public Http2Headers setObject(AsciiString name, Object... values) {
super.setObject(name, values);
return this;
}
@Override
public Http2Headers setBoolean(AsciiString name, boolean value) {
super.setBoolean(name, value);
return this;
}
@Override
public Http2Headers setChar(AsciiString name, char value) {
super.setChar(name, value);
return this;
}
@Override
public Http2Headers setByte(AsciiString name, byte value) {
super.setByte(name, value);
return this;
}
@Override
public Http2Headers setShort(AsciiString name, short value) {
super.setShort(name, value);
return this;
}
@Override
public Http2Headers setInt(AsciiString name, int value) {
super.setInt(name, value);
return this;
}
@Override
public Http2Headers setLong(AsciiString name, long value) {
super.setLong(name, value);
return this;
}
@Override
public Http2Headers setFloat(AsciiString name, float value) {
super.setFloat(name, value);
return this;
}
@Override
public Http2Headers setDouble(AsciiString name, double value) {
super.setDouble(name, value);
return this;
}
@Override
public Http2Headers setTimeMillis(AsciiString name, long value) {
super.setTimeMillis(name, value);
return this;
}
@Override
public Http2Headers set(BinaryHeaders headers) {
super.set(headers);
return this;
}
@Override
public Http2Headers setAll(BinaryHeaders headers) {
super.setAll(headers);
return this;
}
@Override
public Http2Headers clear() {
super.clear();
return this;
}
@Override
public EmptyHttp2Headers method(AsciiString method) {
throw new UnsupportedOperationException();
}
@Override
public EmptyHttp2Headers scheme(AsciiString status) {
throw new UnsupportedOperationException();
}
@Override
public EmptyHttp2Headers authority(AsciiString authority) {
throw new UnsupportedOperationException();
}
@Override
public EmptyHttp2Headers path(AsciiString path) {
throw new UnsupportedOperationException();
}
@Override
public EmptyHttp2Headers status(AsciiString status) {
throw new UnsupportedOperationException();
}
@Override
public AsciiString method() {
return get(PseudoHeaderName.METHOD.value());
}
@Override
public AsciiString scheme() {
return get(PseudoHeaderName.SCHEME.value());
}
@Override
public AsciiString authority() {
return get(PseudoHeaderName.AUTHORITY.value());
}
@Override
public AsciiString path() {
return get(PseudoHeaderName.PATH.value());
}
@Override
public AsciiString status() {
return get(PseudoHeaderName.STATUS.value());
}
}

View File

@ -0,0 +1,120 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.base64.Base64Dialect.URL_SAFE;
import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME;
import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_SETTINGS_HEADER;
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTING_ENTRY_LENGTH;
import static io.netty.handler.codec.http2.Http2CodecUtil.writeUnsignedInt;
import static io.netty.handler.codec.http2.Http2CodecUtil.writeUnsignedShort;
import static io.netty.util.CharsetUtil.UTF_8;
import static io.netty.util.ReferenceCountUtil.release;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.base64.Base64;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpClientUpgradeHandler;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.util.collection.IntObjectMap;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Client-side cleartext upgrade codec from HTTP to HTTP/2.
*/
public class Http2ClientUpgradeCodec implements HttpClientUpgradeHandler.UpgradeCodec {
private static final List<String> UPGRADE_HEADERS = Collections.singletonList(HTTP_UPGRADE_SETTINGS_HEADER);
private final String handlerName;
private final Http2ConnectionHandler connectionHandler;
/**
* Creates the codec using a default name for the connection handler when adding to the
* pipeline.
*
* @param connectionHandler the HTTP/2 connection handler.
*/
public Http2ClientUpgradeCodec(Http2ConnectionHandler connectionHandler) {
this("http2ConnectionHandler", connectionHandler);
}
/**
* Creates the codec providing an upgrade to the given handler for HTTP/2.
*
* @param handlerName the name of the HTTP/2 connection handler to be used in the pipeline.
* @param connectionHandler the HTTP/2 connection handler.
*/
public Http2ClientUpgradeCodec(String handlerName,
Http2ConnectionHandler connectionHandler) {
this.handlerName = checkNotNull(handlerName, "handlerName");
this.connectionHandler = checkNotNull(connectionHandler, "connectionHandler");
}
@Override
public String protocol() {
return HTTP_UPGRADE_PROTOCOL_NAME;
}
@Override
public Collection<String> setUpgradeHeaders(ChannelHandlerContext ctx,
HttpRequest upgradeRequest) {
String settingsValue = getSettingsHeaderValue(ctx);
upgradeRequest.headers().set(HTTP_UPGRADE_SETTINGS_HEADER, settingsValue);
return UPGRADE_HEADERS;
}
@Override
public void upgradeTo(ChannelHandlerContext ctx, FullHttpResponse upgradeResponse)
throws Exception {
// Reserve local stream 1 for the response.
connectionHandler.onHttpClientUpgrade();
// Add the handler to the pipeline.
ctx.pipeline().addAfter(ctx.name(), handlerName, connectionHandler);
}
/**
* Converts the current settings for the handler to the Base64-encoded representation used in
* the HTTP2-Settings upgrade header.
*/
private String getSettingsHeaderValue(ChannelHandlerContext ctx) {
ByteBuf buf = null;
ByteBuf encodedBuf = null;
try {
// Get the local settings for the handler.
Http2Settings settings = connectionHandler.decoder().localSettings();
// Serialize the payload of the SETTINGS frame.
int payloadLength = SETTING_ENTRY_LENGTH * settings.size();
buf = ctx.alloc().buffer(payloadLength);
for (IntObjectMap.Entry<Long> entry : settings.entries()) {
writeUnsignedShort(entry.key(), buf);
writeUnsignedInt(entry.value(), buf);
}
// Base64 encode the payload and then convert to a string for the header.
encodedBuf = Base64.encode(buf, URL_SAFE);
return encodedBuf.toString(UTF_8);
} finally {
release(buf);
release(encodedBuf);
}
}
}

View File

@ -0,0 +1,203 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.util.CharsetUtil.UTF_8;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.http2.Http2StreamRemovalPolicy.Action;
/**
* Constants and utility method used for encoding/decoding HTTP2 frames.
*/
public final class Http2CodecUtil {
private static final byte[] CONNECTION_PREFACE = "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n".getBytes(UTF_8);
private static final byte[] EMPTY_PING = new byte[8];
public static final int CONNECTION_STREAM_ID = 0;
public static final int HTTP_UPGRADE_STREAM_ID = 1;
public static final String HTTP_UPGRADE_SETTINGS_HEADER = "HTTP2-Settings";
public static final String HTTP_UPGRADE_PROTOCOL_NAME = "h2c-16";
public static final String TLS_UPGRADE_PROTOCOL_NAME = "h2-16";
public static final int PING_FRAME_PAYLOAD_LENGTH = 8;
public static final short MAX_UNSIGNED_BYTE = 0xFF;
public static final int MAX_UNSIGNED_SHORT = 0xFFFF;
public static final long MAX_UNSIGNED_INT = 0xFFFFFFFFL;
public static final int FRAME_HEADER_LENGTH = 9;
public static final int SETTING_ENTRY_LENGTH = 6;
public static final int PRIORITY_ENTRY_LENGTH = 5;
public static final int INT_FIELD_LENGTH = 4;
public static final short MAX_WEIGHT = 256;
public static final short MIN_WEIGHT = 1;
public static final int SETTINGS_HEADER_TABLE_SIZE = 1;
public static final int SETTINGS_ENABLE_PUSH = 2;
public static final int SETTINGS_MAX_CONCURRENT_STREAMS = 3;
public static final int SETTINGS_INITIAL_WINDOW_SIZE = 4;
public static final int SETTINGS_MAX_FRAME_SIZE = 5;
public static final int SETTINGS_MAX_HEADER_LIST_SIZE = 6;
public static final int MAX_HEADER_TABLE_SIZE = Integer.MAX_VALUE; // Size limited by HPACK library
public static final long MAX_CONCURRENT_STREAMS = MAX_UNSIGNED_INT;
public static final int MAX_INITIAL_WINDOW_SIZE = Integer.MAX_VALUE;
public static final int MAX_FRAME_SIZE_LOWER_BOUND = 0x4000;
public static final int MAX_FRAME_SIZE_UPPER_BOUND = 0xFFFFFF;
public static final long MAX_HEADER_LIST_SIZE = Long.MAX_VALUE;
public static final long MIN_HEADER_TABLE_SIZE = 0;
public static final long MIN_CONCURRENT_STREAMS = 0;
public static final int MIN_INITIAL_WINDOW_SIZE = 0;
public static final long MIN_HEADER_LIST_SIZE = 0;
public static final int DEFAULT_WINDOW_SIZE = 65535;
public static final boolean DEFAULT_ENABLE_PUSH = true;
public static final short DEFAULT_PRIORITY_WEIGHT = 16;
public static final int DEFAULT_HEADER_TABLE_SIZE = 4096;
public static final int DEFAULT_MAX_HEADER_SIZE = 8192;
public static final int DEFAULT_MAX_FRAME_SIZE = MAX_FRAME_SIZE_LOWER_BOUND;
/**
* Indicates whether or not the given value for max frame size falls within the valid range.
*/
public static boolean isMaxFrameSizeValid(int maxFrameSize) {
return maxFrameSize >= MAX_FRAME_SIZE_LOWER_BOUND && maxFrameSize <= MAX_FRAME_SIZE_UPPER_BOUND;
}
/**
* Returns a buffer containing the the {@link #CONNECTION_PREFACE}.
*/
public static ByteBuf connectionPrefaceBuf() {
// Return a duplicate so that modifications to the reader index will not affect the original buffer.
return Unpooled.wrappedBuffer(CONNECTION_PREFACE);
}
/**
* Returns a buffer filled with all zeros that is the appropriate length for a PING frame.
*/
public static ByteBuf emptyPingBuf() {
// Return a duplicate so that modifications to the reader index will not affect the original buffer.
return Unpooled.wrappedBuffer(EMPTY_PING);
}
/**
* Returns a simple {@link Http2StreamRemovalPolicy} that immediately calls back the
* {@link Action} when a stream is marked for removal.
*/
public static Http2StreamRemovalPolicy immediateRemovalPolicy() {
return new Http2StreamRemovalPolicy() {
private Action action;
@Override
public void setAction(Action action) {
this.action = checkNotNull(action, "action");
}
@Override
public void markForRemoval(Http2Stream stream) {
if (action == null) {
throw new IllegalStateException(
"Action must be called before removing streams.");
}
action.removeStream(stream);
}
};
}
/**
* Iteratively looks through the causaility chain for the given exception and returns the first
* {@link Http2Exception} or {@code null} if none.
*/
public static Http2Exception getEmbeddedHttp2Exception(Throwable cause) {
while (cause != null) {
if (cause instanceof Http2Exception) {
return (Http2Exception) cause;
}
cause = cause.getCause();
}
return null;
}
/**
* Creates a buffer containing the error message from the given exception. If the cause is
* {@code null} returns an empty buffer.
*/
public static ByteBuf toByteBuf(ChannelHandlerContext ctx, Throwable cause) {
if (cause == null || cause.getMessage() == null) {
return Unpooled.EMPTY_BUFFER;
}
// Create the debug message.
byte[] msg = cause.getMessage().getBytes(UTF_8);
ByteBuf debugData = ctx.alloc().buffer(msg.length);
debugData.writeBytes(msg);
return debugData;
}
/**
* Reads a big-endian (31-bit) integer from the buffer.
*/
public static int readUnsignedInt(ByteBuf buf) {
return (buf.readByte() & 0x7F) << 24 | (buf.readByte() & 0xFF) << 16
| (buf.readByte() & 0xFF) << 8 | buf.readByte() & 0xFF;
}
/**
* Writes a big-endian (32-bit) unsigned integer to the buffer.
*/
public static void writeUnsignedInt(long value, ByteBuf out) {
out.writeByte((int) (value >> 24 & 0xFF));
out.writeByte((int) (value >> 16 & 0xFF));
out.writeByte((int) (value >> 8 & 0xFF));
out.writeByte((int) (value & 0xFF));
}
/**
* Writes a big-endian (16-bit) unsigned integer to the buffer.
*/
public static void writeUnsignedShort(int value, ByteBuf out) {
out.writeByte(value >> 8 & 0xFF);
out.writeByte(value & 0xFF);
}
/**
* Writes an HTTP/2 frame header to the output buffer.
*/
public static void writeFrameHeader(ByteBuf out, int payloadLength, byte type,
Http2Flags flags, int streamId) {
out.ensureWritable(FRAME_HEADER_LENGTH + payloadLength);
out.writeMedium(payloadLength);
out.writeByte(type);
out.writeByte(flags.value());
out.writeInt(streamId);
}
/**
* Fails the given promise with the cause and then re-throws the cause.
*/
public static <T extends Throwable> T failAndThrow(ChannelPromise promise, T cause) throws T {
if (!promise.isDone()) {
promise.setFailure(cause);
}
throw cause;
}
private Http2CodecUtil() { }
}

View File

@ -0,0 +1,296 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import java.util.Collection;
/**
* Manager for the state of an HTTP/2 connection with the remote end-point.
*/
public interface Http2Connection {
/**
* Listener for life-cycle events for streams in this connection.
*/
interface Listener {
/**
* Notifies the listener that the given stream was added to the connection. This stream may
* not yet be active (i.e. open/half-closed).
*/
void streamAdded(Http2Stream stream);
/**
* Notifies the listener that the given stream was made active (i.e. open in at least one
* direction).
*/
void streamActive(Http2Stream stream);
/**
* Notifies the listener that the given stream is now half-closed. The stream can be
* inspected to determine which side is closed.
*/
void streamHalfClosed(Http2Stream stream);
/**
* Notifies the listener that the given stream is now closed in both directions.
*/
void streamInactive(Http2Stream stream);
/**
* Notifies the listener that the given stream has now been removed from the connection and
* will no longer be returned via {@link Http2Connection#stream(int)}. The connection may
* maintain inactive streams for some time before removing them.
*/
void streamRemoved(Http2Stream stream);
/**
* Notifies the listener that a priority tree parent change has occurred. This method will be invoked
* in a top down order relative to the priority tree. This method will also be invoked after all tree
* structure changes have been made and the tree is in steady state relative to the priority change
* which caused the tree structure to change.
* @param stream The stream which had a parent change (new parent and children will be steady state)
* @param oldParent The old parent which {@code stream} used to be a child of (may be {@code null})
*/
void priorityTreeParentChanged(Http2Stream stream, Http2Stream oldParent);
/**
* Notifies the listener that a parent dependency is about to change
* This is called while the tree is being restructured and so the tree
* structure is not necessarily steady state.
* @param stream The stream which the parent is about to change to {@code newParent}
* @param newParent The stream which will be the parent of {@code stream}
*/
void priorityTreeParentChanging(Http2Stream stream, Http2Stream newParent);
/**
* Notifies the listener that the weight has changed for {@code stream}
* @param stream The stream which the weight has changed
* @param oldWeight The old weight for {@code stream}
*/
void onWeightChanged(Http2Stream stream, short oldWeight);
/**
* Called when a GO_AWAY frame has either been sent or received for the connection.
*/
void goingAway();
}
/**
* A view of the connection from one endpoint (local or remote).
*/
interface Endpoint<F extends Http2FlowController> {
/**
* Returns the next valid streamId for this endpoint. If negative, the stream IDs are
* exhausted for this endpoint an no further streams may be created.
*/
int nextStreamId();
/**
* Indicates whether the given streamId is from the set of IDs used by this endpoint to
* create new streams.
*/
boolean createdStreamId(int streamId);
/**
* Indicates whether or not this endpoint is currently accepting new streams. This will be
* be false if {@link #numActiveStreams()} + 1 >= {@link #maxStreams()} or if the stream IDs
* for this endpoint have been exhausted (i.e. {@link #nextStreamId()} < 0).
*/
boolean acceptingNewStreams();
/**
* Creates a stream initiated by this endpoint. This could fail for the following reasons:
* <ul>
* <li>The requested stream ID is not the next sequential ID for this endpoint.</li>
* <li>The stream already exists.</li>
* <li>The number of concurrent streams is above the allowed threshold for this endpoint.</li>
* <li>The connection is marked as going away.</li>
* </ul>
* <p>
* The caller is expected to {@link Http2Stream#open()} the stream.
* @param streamId The ID of the stream
* @see Http2Stream#open()
* @see Http2Stream#open(boolean)
*/
Http2Stream createStream(int streamId) throws Http2Exception;
/**
* Creates a push stream in the reserved state for this endpoint and notifies all listeners.
* This could fail for the following reasons:
* <ul>
* <li>Server push is not allowed to the opposite endpoint.</li>
* <li>The requested stream ID is not the next sequential stream ID for this endpoint.</li>
* <li>The number of concurrent streams is above the allowed threshold for this endpoint.</li>
* <li>The connection is marked as going away.</li>
* <li>The parent stream ID does not exist or is not open from the side sending the push
* promise.</li>
* <li>Could not set a valid priority for the new stream.</li>
* </ul>
*
* @param streamId the ID of the push stream
* @param parent the parent stream used to initiate the push stream.
*/
Http2Stream reservePushStream(int streamId, Http2Stream parent) throws Http2Exception;
/**
* Indicates whether or not this endpoint is the server-side of the connection.
*/
boolean isServer();
/**
* Sets whether server push is allowed to this endpoint.
*/
void allowPushTo(boolean allow);
/**
* Gets whether or not server push is allowed to this endpoint. This is always false
* for a server endpoint.
*/
boolean allowPushTo();
/**
* Gets the number of currently active streams that were created by this endpoint.
*/
int numActiveStreams();
/**
* Gets the maximum number of concurrent streams allowed by this endpoint.
*/
int maxStreams();
/**
* Sets the maximum number of concurrent streams allowed by this endpoint.
*/
void maxStreams(int maxStreams);
/**
* Gets the ID of the stream last successfully created by this endpoint.
*/
int lastStreamCreated();
/**
* Gets the last stream created by this endpoint that is "known" by the opposite endpoint.
* If a GOAWAY was received for this endpoint, this will be the last stream ID from the
* GOAWAY frame. Otherwise, this will be same as {@link #lastStreamCreated()}.
*/
int lastKnownStream();
/**
* Gets the flow controller for this endpoint.
*/
F flowController();
/**
* Sets the flow controller for this endpoint.
*/
void flowController(F flowController);
/**
* Gets the {@link Endpoint} opposite this one.
*/
Endpoint<? extends Http2FlowController> opposite();
}
/**
* Adds a listener of stream life-cycle events. Adding the same listener multiple times has no effect.
*/
void addListener(Listener listener);
/**
* Removes a listener of stream life-cycle events.
*/
void removeListener(Listener listener);
/**
* Attempts to get the stream for the given ID. If it doesn't exist, throws.
*/
Http2Stream requireStream(int streamId) throws Http2Exception;
/**
* Gets the stream if it exists. If not, returns {@code null}.
*/
Http2Stream stream(int streamId);
/**
* Gets the stream object representing the connection, itself (i.e. stream zero). This object
* always exists.
*/
Http2Stream connectionStream();
/**
* Gets the number of streams that are currently either open or half-closed.
*/
int numActiveStreams();
/**
* Gets all streams that are currently either open or half-closed. The returned collection is
* sorted by priority.
*/
Collection<Http2Stream> activeStreams();
/**
* Indicates whether or not the local endpoint for this connection is the server.
*/
boolean isServer();
/**
* Gets a view of this connection from the local {@link Endpoint}.
*/
Endpoint<Http2LocalFlowController> local();
/**
* Creates a new stream initiated by the local endpoint
* @see Endpoint#createStream(int)
*/
Http2Stream createLocalStream(int streamId) throws Http2Exception;
/**
* Gets a view of this connection from the remote {@link Endpoint}.
*/
Endpoint<Http2RemoteFlowController> remote();
/**
* Creates a new stream initiated by the remote endpoint.
* @see Endpoint#createStream(int)
*/
Http2Stream createRemoteStream(int streamId) throws Http2Exception;
/**
* Indicates whether or not a {@code GOAWAY} was received from the remote endpoint.
*/
boolean goAwayReceived();
/**
* Indicates that a {@code GOAWAY} was received from the remote endpoint and sets the last known stream.
*/
void goAwayReceived(int lastKnownStream);
/**
* Indicates whether or not a {@code GOAWAY} was sent to the remote endpoint.
*/
boolean goAwaySent();
/**
* Indicates that a {@code GOAWAY} was sent to the remote endpoint and sets the last known stream.
*/
void goAwaySent(int lastKnownStream);
/**
* Indicates whether or not either endpoint has received a GOAWAY.
*/
boolean isGoAway();
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
/**
* Provides empty implementations of all {@link Http2Connection.Listener} methods.
*/
public class Http2ConnectionAdapter implements Http2Connection.Listener {
@Override
public void streamAdded(Http2Stream stream) {
}
@Override
public void streamActive(Http2Stream stream) {
}
@Override
public void streamHalfClosed(Http2Stream stream) {
}
@Override
public void streamInactive(Http2Stream stream) {
}
@Override
public void streamRemoved(Http2Stream stream) {
}
@Override
public void goingAway() {
}
@Override
public void priorityTreeParentChanged(Http2Stream stream, Http2Stream oldParent) {
}
@Override
public void priorityTreeParentChanging(Http2Stream stream, Http2Stream newParent) {
}
@Override
public void onWeightChanged(Http2Stream stream, short oldWeight) {
}
}

View File

@ -0,0 +1,109 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import java.io.Closeable;
import java.util.List;
/**
* Handler for inbound traffic on behalf of {@link Http2ConnectionHandler}. Performs basic protocol
* conformance on inbound frames before calling the delegate {@link Http2FrameListener} for
* application-specific processing. Note that frames of an unknown type (i.e. HTTP/2 extensions)
* will skip all protocol checks and be given directly to the listener for processing.
*/
public interface Http2ConnectionDecoder extends Closeable {
/**
* Builder for new instances of {@link Http2ConnectionDecoder}.
*/
interface Builder {
/**
* Sets the {@link Http2Connection} to be used when building the decoder.
*/
Builder connection(Http2Connection connection);
/**
* Sets the {@link Http2LifecycleManager} to be used when building the decoder.
*/
Builder lifecycleManager(Http2LifecycleManager lifecycleManager);
/**
* Gets the {@link Http2LifecycleManager} to be used when building the decoder.
*/
Http2LifecycleManager lifecycleManager();
/**
* Sets the {@link Http2FrameReader} to be used when building the decoder.
*/
Builder frameReader(Http2FrameReader frameReader);
/**
* Sets the {@link Http2FrameListener} to be used when building the decoder.
*/
Builder listener(Http2FrameListener listener);
/**
* Sets the {@link Http2ConnectionEncoder} used when building the decoder.
*/
Builder encoder(Http2ConnectionEncoder encoder);
/**
* Creates a new decoder instance.
*/
Http2ConnectionDecoder build();
}
/**
* Provides direct access to the underlying connection.
*/
Http2Connection connection();
/**
* Provides the local flow controller for managing inbound traffic.
*/
Http2LocalFlowController flowController();
/**
* Provides direct access to the underlying frame listener.
*/
Http2FrameListener listener();
/**
* Called by the {@link Http2ConnectionHandler} to decode the next frame from the input buffer.
*/
void decodeFrame(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Http2Exception;
/**
* Gets the local settings for this endpoint of the HTTP/2 connection.
*/
Http2Settings localSettings();
/**
* Sets the local settings for this endpoint of the HTTP/2 connection.
*/
void localSettings(Http2Settings settings) throws Http2Exception;
/**
* Indicates whether or not the first initial {@code SETTINGS} frame was received from the remote endpoint.
*/
boolean prefaceReceived();
@Override
void close();
}

View File

@ -0,0 +1,92 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
/**
* Handler for outbound HTTP/2 traffic.
*/
public interface Http2ConnectionEncoder extends Http2FrameWriter {
/**
* Builder for new instances of {@link Http2ConnectionEncoder}.
*/
interface Builder {
/**
* Sets the {@link Http2Connection} to be used when building the encoder.
*/
Builder connection(Http2Connection connection);
/**
* Sets the {@link Http2LifecycleManager} to be used when building the encoder.
*/
Builder lifecycleManager(Http2LifecycleManager lifecycleManager);
/**
* Gets the {@link Http2LifecycleManager} to be used when building the encoder.
*/
Http2LifecycleManager lifecycleManager();
/**
* Sets the {@link Http2FrameWriter} to be used when building the encoder.
*/
Builder frameWriter(Http2FrameWriter frameWriter);
/**
* Creates a new encoder instance.
*/
Http2ConnectionEncoder build();
}
/**
* Provides direct access to the underlying connection.
*/
Http2Connection connection();
/**
* Provides the remote flow controller for managing outbound traffic.
*/
Http2RemoteFlowController flowController();
/**
* Provides direct access to the underlying frame writer object.
*/
Http2FrameWriter frameWriter();
/**
* Gets the local settings on the top of the queue that has been sent but not ACKed. This may
* return {@code null}.
*/
Http2Settings pollSentSettings();
/**
* Sets the settings for the remote endpoint of the HTTP/2 connection.
*/
void remoteSettings(Http2Settings settings) throws Http2Exception;
/**
* Writes the given data to the internal {@link Http2FrameWriter} without performing any
* state checks on the connection/stream.
*/
@Override
ChannelFuture writeFrame(ChannelHandlerContext ctx, byte frameType, int streamId,
Http2Flags flags, ByteBuf payload, ChannelPromise promise);
}

View File

@ -0,0 +1,516 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_STREAM_ID;
import static io.netty.handler.codec.http2.Http2CodecUtil.connectionPrefaceBuf;
import static io.netty.handler.codec.http2.Http2CodecUtil.getEmbeddedHttp2Exception;
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.NO_ERROR;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import static io.netty.handler.codec.http2.Http2Exception.isStreamError;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelOutboundHandler;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.http2.Http2Exception.CompositeStreamException;
import io.netty.handler.codec.http2.Http2Exception.StreamException;
import java.net.SocketAddress;
import java.util.Collection;
import java.util.List;
/**
* Provides the default implementation for processing inbound frame events
* and delegates to a {@link Http2FrameListener}
* <p>
* This class will read HTTP/2 frames and delegate the events to a {@link Http2FrameListener}
* <p>
* This interface enforces inbound flow control functionality through {@link Http2InboundFlowController}
*/
public class Http2ConnectionHandler extends ByteToMessageDecoder implements Http2LifecycleManager,
ChannelOutboundHandler {
private final Http2ConnectionDecoder decoder;
private final Http2ConnectionEncoder encoder;
private ByteBuf clientPrefaceString;
private boolean prefaceSent;
private ChannelFutureListener closeListener;
public Http2ConnectionHandler(boolean server, Http2FrameListener listener) {
this(new DefaultHttp2Connection(server), listener);
}
public Http2ConnectionHandler(Http2Connection connection, Http2FrameListener listener) {
this(connection, new DefaultHttp2FrameReader(), new DefaultHttp2FrameWriter(), listener);
}
public Http2ConnectionHandler(Http2Connection connection, Http2FrameReader frameReader,
Http2FrameWriter frameWriter, Http2FrameListener listener) {
this(DefaultHttp2ConnectionDecoder.newBuilder().connection(connection)
.frameReader(frameReader).listener(listener),
DefaultHttp2ConnectionEncoder.newBuilder().connection(connection)
.frameWriter(frameWriter));
}
/**
* Constructor for pre-configured encoder and decoder builders. Just sets the {@code this} as the
* {@link Http2LifecycleManager} and builds them.
*/
public Http2ConnectionHandler(Http2ConnectionDecoder.Builder decoderBuilder,
Http2ConnectionEncoder.Builder encoderBuilder) {
checkNotNull(decoderBuilder, "decoderBuilder");
checkNotNull(encoderBuilder, "encoderBuilder");
if (encoderBuilder.lifecycleManager() != decoderBuilder.lifecycleManager()) {
throw new IllegalArgumentException("Encoder and Decoder must share a lifecycle manager");
} else if (encoderBuilder.lifecycleManager() == null) {
encoderBuilder.lifecycleManager(this);
decoderBuilder.lifecycleManager(this);
}
// Build the encoder.
encoder = checkNotNull(encoderBuilder.build(), "encoder");
// Build the decoder.
decoderBuilder.encoder(encoder);
decoder = checkNotNull(decoderBuilder.build(), "decoder");
// Verify that the encoder and decoder use the same connection.
checkNotNull(encoder.connection(), "encoder.connection");
if (encoder.connection() != decoder.connection()) {
throw new IllegalArgumentException("Encoder and Decoder do not share the same connection object");
}
clientPrefaceString = clientPrefaceString(encoder.connection());
}
public Http2Connection connection() {
return encoder.connection();
}
public Http2ConnectionDecoder decoder() {
return decoder;
}
public Http2ConnectionEncoder encoder() {
return encoder;
}
/**
* Handles the client-side (cleartext) upgrade from HTTP to HTTP/2.
* Reserves local stream 1 for the HTTP/2 response.
*/
public void onHttpClientUpgrade() throws Http2Exception {
if (connection().isServer()) {
throw connectionError(PROTOCOL_ERROR, "Client-side HTTP upgrade requested for a server");
}
if (prefaceSent || decoder.prefaceReceived()) {
throw connectionError(PROTOCOL_ERROR, "HTTP upgrade must occur before HTTP/2 preface is sent or received");
}
// Create a local stream used for the HTTP cleartext upgrade.
connection().createLocalStream(HTTP_UPGRADE_STREAM_ID).open(true);
}
/**
* Handles the server-side (cleartext) upgrade from HTTP to HTTP/2.
* @param settings the settings for the remote endpoint.
*/
public void onHttpServerUpgrade(Http2Settings settings) throws Http2Exception {
if (!connection().isServer()) {
throw connectionError(PROTOCOL_ERROR, "Server-side HTTP upgrade requested for a client");
}
if (prefaceSent || decoder.prefaceReceived()) {
throw connectionError(PROTOCOL_ERROR, "HTTP upgrade must occur before HTTP/2 preface is sent or received");
}
// Apply the settings but no ACK is necessary.
encoder.remoteSettings(settings);
// Create a stream in the half-closed state.
connection().createRemoteStream(HTTP_UPGRADE_STREAM_ID).open(true);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// The channel just became active - send the connection preface to the remote endpoint.
sendPreface(ctx);
super.channelActive(ctx);
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// This handler was just added to the context. In case it was handled after
// the connection became active, send the connection preface now.
sendPreface(ctx);
}
@Override
protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
dispose();
}
@Override
public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) throws Exception {
ctx.bind(localAddress, promise);
}
@Override
public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
ctx.connect(remoteAddress, localAddress, promise);
}
@Override
public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.disconnect(promise);
}
@Override
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
// Avoid NotYetConnectedException
if (!ctx.channel().isActive()) {
ctx.close(promise);
return;
}
ChannelFuture future = writeGoAway(ctx, null);
// If there are no active streams, close immediately after the send is complete.
// Otherwise wait until all streams are inactive.
if (connection().numActiveStreams() == 0) {
future.addListener(new ClosingChannelFutureListener(ctx, promise));
} else {
closeListener = new ClosingChannelFutureListener(ctx, promise);
}
}
@Override
public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
ctx.deregister(promise);
}
@Override
public void read(ChannelHandlerContext ctx) throws Exception {
ctx.read();
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise);
}
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
ctx.flush();
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ChannelFuture future = ctx.newSucceededFuture();
final Collection<Http2Stream> streams = connection().activeStreams();
for (Http2Stream s : streams.toArray(new Http2Stream[streams.size()])) {
closeStream(s, future);
}
super.channelInactive(ctx);
}
/**
* Handles {@link Http2Exception} objects that were thrown from other handlers. Ignores all other exceptions.
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
if (getEmbeddedHttp2Exception(cause) != null) {
// Some exception in the causality chain is an Http2Exception - handle it.
onException(ctx, cause);
} else {
super.exceptionCaught(ctx, cause);
}
}
/**
* Closes the local side of the given stream. If this causes the stream to be closed, adds a
* hook to close the channel after the given future completes.
*
* @param stream the stream to be half closed.
* @param future If closing, the future after which to close the channel.
*/
@Override
public void closeLocalSide(Http2Stream stream, ChannelFuture future) {
switch (stream.state()) {
case HALF_CLOSED_LOCAL:
case OPEN:
stream.closeLocalSide();
break;
default:
closeStream(stream, future);
break;
}
}
/**
* Closes the remote side of the given stream. If this causes the stream to be closed, adds a
* hook to close the channel after the given future completes.
*
* @param stream the stream to be half closed.
* @param future If closing, the future after which to close the channel.
*/
@Override
public void closeRemoteSide(Http2Stream stream, ChannelFuture future) {
switch (stream.state()) {
case HALF_CLOSED_REMOTE:
case OPEN:
stream.closeRemoteSide();
break;
default:
closeStream(stream, future);
break;
}
}
/**
* Closes the given stream and adds a hook to close the channel after the given future
* completes.
*
* @param stream the stream to be closed.
* @param future the future after which to close the channel.
*/
@Override
public void closeStream(Http2Stream stream, ChannelFuture future) {
stream.close();
// If this connection is closing and there are no longer any
// active streams, close after the current operation completes.
if (closeListener != null && connection().numActiveStreams() == 0) {
future.addListener(closeListener);
}
}
/**
* Central handler for all exceptions caught during HTTP/2 processing.
*/
@Override
public void onException(ChannelHandlerContext ctx, Throwable cause) {
Http2Exception embedded = getEmbeddedHttp2Exception(cause);
if (isStreamError(embedded)) {
onStreamError(ctx, cause, (StreamException) embedded);
} else if (embedded instanceof CompositeStreamException) {
CompositeStreamException compositException = (CompositeStreamException) embedded;
for (StreamException streamException : compositException) {
onStreamError(ctx, cause, streamException);
}
} else {
onConnectionError(ctx, cause, embedded);
}
}
/**
* Handler for a connection error. Sends a GO_AWAY frame to the remote endpoint. Once all
* streams are closed, the connection is shut down.
*
* @param ctx the channel context
* @param cause the exception that was caught
* @param http2Ex the {@link Http2Exception} that is embedded in the causality chain. This may
* be {@code null} if it's an unknown exception.
*/
protected void onConnectionError(ChannelHandlerContext ctx, Throwable cause, Http2Exception http2Ex) {
if (http2Ex == null) {
http2Ex = new Http2Exception(INTERNAL_ERROR, cause.getMessage(), cause);
}
writeGoAway(ctx, http2Ex).addListener(new ClosingChannelFutureListener(ctx, ctx.newPromise()));
}
/**
* Handler for a stream error. Sends a {@code RST_STREAM} frame to the remote endpoint and closes the
* stream.
*
* @param ctx the channel context
* @param cause the exception that was caught
* @param http2Ex the {@link StreamException} that is embedded in the causality chain.
*/
protected void onStreamError(ChannelHandlerContext ctx, Throwable cause, StreamException http2Ex) {
writeRstStream(ctx, http2Ex.streamId(), http2Ex.error().code(), ctx.newPromise());
}
protected Http2FrameWriter frameWriter() {
return encoder().frameWriter();
}
/**
* Writes a {@code RST_STREAM} frame to the remote endpoint and updates the connection state appropriately.
*/
@Override
public ChannelFuture writeRstStream(ChannelHandlerContext ctx, int streamId, long errorCode,
ChannelPromise promise) {
Http2Stream stream = connection().stream(streamId);
ChannelFuture future = frameWriter().writeRstStream(ctx, streamId, errorCode, promise);
ctx.flush();
if (stream != null) {
stream.resetSent();
closeStream(stream, promise);
}
return future;
}
/**
* Sends a {@code GO_AWAY} frame to the remote endpoint and updates the connection state appropriately.
*/
@Override
public ChannelFuture writeGoAway(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData,
ChannelPromise promise) {
Http2Connection connection = connection();
if (connection.isGoAway()) {
debugData.release();
return ctx.newSucceededFuture();
}
ChannelFuture future = frameWriter().writeGoAway(ctx, lastStreamId, errorCode, debugData, promise);
ctx.flush();
connection.goAwaySent(lastStreamId);
return future;
}
/**
* Sends a {@code GO_AWAY} frame appropriate for the given exception.
*/
private ChannelFuture writeGoAway(ChannelHandlerContext ctx, Http2Exception cause) {
Http2Connection connection = connection();
if (connection.isGoAway()) {
return ctx.newSucceededFuture();
}
// The connection isn't alredy going away, send the GO_AWAY frame now to start
// the process.
long errorCode = cause != null ? cause.error().code() : NO_ERROR.code();
ByteBuf debugData = Http2CodecUtil.toByteBuf(ctx, cause);
int lastKnownStream = connection.remote().lastStreamCreated();
return writeGoAway(ctx, lastKnownStream, errorCode, debugData, ctx.newPromise());
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
try {
// Read the remaining of the client preface string if we haven't already.
// If this is a client endpoint, always returns true.
if (!readClientPrefaceString(in)) {
// Still processing the client preface.
return;
}
decoder.decodeFrame(ctx, in, out);
} catch (Throwable e) {
onException(ctx, e);
}
}
/**
* Sends the HTTP/2 connection preface upon establishment of the connection, if not already sent.
*/
private void sendPreface(final ChannelHandlerContext ctx) {
if (prefaceSent || !ctx.channel().isActive()) {
return;
}
prefaceSent = true;
if (!connection().isServer()) {
// Clients must send the preface string as the first bytes on the connection.
ctx.write(connectionPrefaceBuf()).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
}
// Both client and server must send their initial settings.
encoder.writeSettings(ctx, decoder.localSettings(), ctx.newPromise()).addListener(
ChannelFutureListener.CLOSE_ON_FAILURE);
}
/**
* Disposes of all resources.
*/
private void dispose() {
encoder.close();
decoder.close();
if (clientPrefaceString != null) {
clientPrefaceString.release();
clientPrefaceString = null;
}
}
/**
* Decodes the client connection preface string from the input buffer.
*
* @return {@code true} if processing of the client preface string is complete. Since client preface strings can
* only be received by servers, returns true immediately for client endpoints.
*/
private boolean readClientPrefaceString(ByteBuf in) throws Http2Exception {
if (clientPrefaceString == null) {
return true;
}
int prefaceRemaining = clientPrefaceString.readableBytes();
int bytesRead = Math.min(in.readableBytes(), prefaceRemaining);
// Read the portion of the input up to the length of the preface, if reached.
ByteBuf sourceSlice = in.readSlice(bytesRead);
// Read the same number of bytes from the preface buffer.
ByteBuf prefaceSlice = clientPrefaceString.readSlice(bytesRead);
// If the input so far doesn't match the preface, break the connection.
if (bytesRead == 0 || !prefaceSlice.equals(sourceSlice)) {
throw connectionError(PROTOCOL_ERROR, "HTTP/2 client preface string missing or corrupt.");
}
if (!clientPrefaceString.isReadable()) {
// Entire preface has been read.
clientPrefaceString.release();
clientPrefaceString = null;
return true;
}
return false;
}
/**
* Returns the client preface string if this is a client connection, otherwise returns {@code null}.
*/
private static ByteBuf clientPrefaceString(Http2Connection connection) {
return connection.isServer() ? connectionPrefaceBuf() : null;
}
/**
* Closes the channel when the future completes.
*/
private static final class ClosingChannelFutureListener implements ChannelFutureListener {
private final ChannelHandlerContext ctx;
private final ChannelPromise promise;
ClosingChannelFutureListener(ChannelHandlerContext ctx, ChannelPromise promise) {
this.ctx = ctx;
this.promise = promise;
}
@Override
public void operationComplete(ChannelFuture sentGoAwayFuture) throws Exception {
ctx.close(promise);
}
}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
/**
* Interface that defines an object capable of producing HTTP/2 data frames.
*/
public interface Http2DataWriter {
/**
* Writes a {@code DATA} frame to the remote endpoint.
*
* @param ctx the context to use for writing.
* @param streamId the stream for which to send the frame.
* @param data the payload of the frame.
* @param padding the amount of padding to be added to the end of the frame
* @param endStream indicates if this is the last frame to be sent for the stream.
* @param promise the promise for the write.
* @return the future for the write.
*/
ChannelFuture writeData(ChannelHandlerContext ctx, int streamId,
ByteBuf data, int padding, boolean endStream, ChannelPromise promise);
}

View File

@ -0,0 +1,49 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
/**
* All error codes identified by the HTTP/2 spec.
*/
public enum Http2Error {
NO_ERROR(0x0),
PROTOCOL_ERROR(0x1),
INTERNAL_ERROR(0x2),
FLOW_CONTROL_ERROR(0x3),
SETTINGS_TIMEOUT(0x4),
STREAM_CLOSED(0x5),
FRAME_SIZE_ERROR(0x6),
REFUSED_STREAM(0x7),
CANCEL(0x8),
COMPRESSION_ERROR(0x9),
CONNECT_ERROR(0xA),
ENHANCE_YOUR_CALM(0xB),
INADEQUATE_SECURITY(0xC),
HTTP_1_1_REQUIRED(0xD);
private final long code;
Http2Error(long code) {
this.code = code;
}
/**
* Gets the code for this error used on the wire.
*/
public long code() {
return code;
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
/**
* This class brings {@link Http2Connection.Listener} and {@link Http2FrameListener} together to provide
* NOOP implementation so inheriting classes can selectively choose which methods to override.
*/
public class Http2EventAdapter implements Http2Connection.Listener, Http2FrameListener {
@Override
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)
throws Http2Exception {
return data.readableBytes() + padding;
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endStream) throws Http2Exception {
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception {
}
@Override
public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight,
boolean exclusive) throws Http2Exception {
}
@Override
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception {
}
@Override
public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception {
}
@Override
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception {
}
@Override
public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
}
@Override
public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
}
@Override
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
Http2Headers headers, int padding) throws Http2Exception {
}
@Override
public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData)
throws Http2Exception {
}
@Override
public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
throws Http2Exception {
}
@Override
public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags,
ByteBuf payload) {
}
@Override
public void streamAdded(Http2Stream stream) {
}
@Override
public void streamActive(Http2Stream stream) {
}
@Override
public void streamHalfClosed(Http2Stream stream) {
}
@Override
public void streamInactive(Http2Stream stream) {
}
@Override
public void streamRemoved(Http2Stream stream) {
}
@Override
public void priorityTreeParentChanged(Http2Stream stream, Http2Stream oldParent) {
}
@Override
public void priorityTreeParentChanging(Http2Stream stream, Http2Stream newParent) {
}
@Override
public void onWeightChanged(Http2Stream stream, short oldWeight) {
}
@Override
public void goingAway() {
}
}

View File

@ -0,0 +1,176 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Exception thrown when an HTTP/2 error was encountered.
*/
public class Http2Exception extends Exception {
private static final long serialVersionUID = -6943456574080986447L;
private final Http2Error error;
public Http2Exception(Http2Error error) {
this.error = error;
}
public Http2Exception(Http2Error error, String message) {
super(message);
this.error = error;
}
public Http2Exception(Http2Error error, String message, Throwable cause) {
super(message, cause);
this.error = error;
}
public Http2Error error() {
return error;
}
/**
* Use if an error has occurred which can not be isolated to a single stream, but instead applies
* to the entire connection.
* @param error The type of error as defined by the HTTP/2 specification.
* @param fmt String with the content and format for the additional debug data.
* @param args Objects which fit into the format defined by {@code fmt}.
* @return An exception which can be translated into a HTTP/2 error.
*/
public static Http2Exception connectionError(Http2Error error, String fmt, Object... args) {
return new Http2Exception(error, String.format(fmt, args));
}
/**
* Use if an error has occurred which can not be isolated to a single stream, but instead applies
* to the entire connection.
* @param error The type of error as defined by the HTTP/2 specification.
* @param cause The object which caused the error.
* @param fmt String with the content and format for the additional debug data.
* @param args Objects which fit into the format defined by {@code fmt}.
* @return An exception which can be translated into a HTTP/2 error.
*/
public static Http2Exception connectionError(Http2Error error, Throwable cause,
String fmt, Object... args) {
return new Http2Exception(error, String.format(fmt, args), cause);
}
/**
* Use if an error which can be isolated to a single stream has occurred. If the {@code id} is not
* {@link Http2CodecUtil#CONNECTION_STREAM_ID} then a {@link Http2Exception.StreamException} will be returned.
* Otherwise the error is considered a connection error and a {@link Http2Exception} is returned.
* @param id The stream id for which the error is isolated to.
* @param error The type of error as defined by the HTTP/2 specification.
* @param fmt String with the content and format for the additional debug data.
* @param args Objects which fit into the format defined by {@code fmt}.
* @return If the {@code id} is not
* {@link Http2CodecUtil#CONNECTION_STREAM_ID} then a {@link Http2Exception.StreamException} will be returned.
* Otherwise the error is considered a connection error and a {@link Http2Exception} is returned.
*/
public static Http2Exception streamError(int id, Http2Error error, String fmt, Object... args) {
return CONNECTION_STREAM_ID == id ?
Http2Exception.connectionError(error, fmt, args) :
new StreamException(id, error, String.format(fmt, args));
}
/**
* Use if an error which can be isolated to a single stream has occurred. If the {@code id} is not
* {@link Http2CodecUtil#CONNECTION_STREAM_ID} then a {@link Http2Exception.StreamException} will be returned.
* Otherwise the error is considered a connection error and a {@link Http2Exception} is returned.
* @param id The stream id for which the error is isolated to.
* @param error The type of error as defined by the HTTP/2 specification.
* @param cause The object which caused the error.
* @param fmt String with the content and format for the additional debug data.
* @param args Objects which fit into the format defined by {@code fmt}.
* @return If the {@code id} is not
* {@link Http2CodecUtil#CONNECTION_STREAM_ID} then a {@link Http2Exception.StreamException} will be returned.
* Otherwise the error is considered a connection error and a {@link Http2Exception} is returned.
*/
public static Http2Exception streamError(int id, Http2Error error, Throwable cause,
String fmt, Object... args) {
return CONNECTION_STREAM_ID == id ?
Http2Exception.connectionError(error, cause, fmt, args) :
new StreamException(id, error, String.format(fmt, args), cause);
}
/**
* Check if an exception is isolated to a single stream or the entire connection.
* @param e The exception to check.
* @return {@code true} if {@code e} is an instance of {@link Http2Exception.StreamException}.
* {@code false} otherwise.
*/
public static boolean isStreamError(Http2Exception e) {
return e instanceof StreamException;
}
/**
* Get the stream id associated with an exception.
* @param e The exception to get the stream id for.
* @return {@link Http2CodecUtil#CONNECTION_STREAM_ID} if {@code e} is a connection error.
* Otherwise the stream id associated with the stream error.
*/
public static int streamId(Http2Exception e) {
return isStreamError(e) ? ((StreamException) e).streamId() : CONNECTION_STREAM_ID;
}
/**
* Represents an exception that can be isolated to a single stream (as opposed to the entire connection).
*/
public static final class StreamException extends Http2Exception {
private static final long serialVersionUID = 462766352505067095L;
private final int streamId;
StreamException(int streamId, Http2Error error, String message) {
super(error, message);
this.streamId = streamId;
}
StreamException(int streamId, Http2Error error, String message, Throwable cause) {
super(error, message, cause);
this.streamId = streamId;
}
public int streamId() {
return streamId;
}
}
/**
* Provides the ability to handle multiple stream exceptions with one throw statement.
*/
public static final class CompositeStreamException extends Http2Exception implements Iterable<StreamException> {
private static final long serialVersionUID = -434398146294199889L;
private final List<StreamException> exceptions;
public CompositeStreamException(Http2Error error, int initialCapacity) {
super(error);
exceptions = new ArrayList<StreamException>(initialCapacity);
}
public void add(StreamException e) {
exceptions.add(e);
}
@Override
public Iterator<StreamException> iterator() {
return exceptions.iterator();
}
}
}

View File

@ -0,0 +1,204 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
/**
* Provides utility methods for accessing specific flags as defined by the HTTP/2 spec.
*/
public final class Http2Flags {
public static final short END_STREAM = 0x1;
public static final short END_HEADERS = 0x4;
public static final short ACK = 0x1;
public static final short PADDED = 0x8;
public static final short PRIORITY = 0x20;
private short value;
public Http2Flags() {
}
public Http2Flags(short value) {
this.value = value;
}
/**
* Gets the underlying flags value.
*/
public short value() {
return value;
}
/**
* Determines whether the {@link #END_STREAM} flag is set. Only applies to DATA and HEADERS
* frames.
*/
public boolean endOfStream() {
return isFlagSet(END_STREAM);
}
/**
* Determines whether the {@link #END_HEADERS} flag is set. Only applies for HEADERS,
* PUSH_PROMISE, and CONTINUATION frames.
*/
public boolean endOfHeaders() {
return isFlagSet(END_HEADERS);
}
/**
* Determines whether the flag is set indicating the presence of the exclusive, stream
* dependency, and weight fields in a HEADERS frame.
*/
public boolean priorityPresent() {
return isFlagSet(PRIORITY);
}
/**
* Determines whether the flag is set indicating that this frame is an ACK. Only applies for
* SETTINGS and PING frames.
*/
public boolean ack() {
return isFlagSet(ACK);
}
/**
* For frames that include padding, indicates if the {@link #PADDED} field is present. Only
* applies to DATA, HEADERS, PUSH_PROMISE and CONTINUATION frames.
*/
public boolean paddingPresent() {
return isFlagSet(PADDED);
}
/**
* Gets the number of bytes expected for the priority fields of the payload. This is determined
* by the {@link #priorityPresent()} flag.
*/
public int getNumPriorityBytes() {
return priorityPresent() ? 5 : 0;
}
/**
* Gets the length in bytes of the padding presence field expected in the payload. This is
* determined by the {@link #paddingPresent()} flag.
*/
public int getPaddingPresenceFieldLength() {
return paddingPresent() ? 1 : 0;
}
/**
* Sets the {@link #END_STREAM} flag.
*/
public Http2Flags endOfStream(boolean endOfStream) {
return setFlag(endOfStream, END_STREAM);
}
/**
* Sets the {@link #END_HEADERS} flag.
*/
public Http2Flags endOfHeaders(boolean endOfHeaders) {
return setFlag(endOfHeaders, END_HEADERS);
}
/**
* Sets the {@link #PRIORITY} flag.
*/
public Http2Flags priorityPresent(boolean priorityPresent) {
return setFlag(priorityPresent, PRIORITY);
}
/**
* Sets the {@link #PADDED} flag.
*/
public Http2Flags paddingPresent(boolean paddingPresent) {
return setFlag(paddingPresent, PADDED);
}
/**
* Sets the {@link #ACK} flag.
*/
public Http2Flags ack(boolean ack) {
return setFlag(ack, ACK);
}
/**
* Generic method to set any flag.
* @param on if the flag should be enabled or disabled.
* @param mask the mask that identifies the bit for the flag.
* @return this instance.
*/
public Http2Flags setFlag(boolean on, short mask) {
if (on) {
value |= mask;
} else {
value &= ~mask;
}
return this;
}
/**
* Indicates whether or not a particular flag is set.
* @param mask the mask identifying the bit for the particular flag being tested
* @return {@code true} if the flag is set
*/
public boolean isFlagSet(short mask) {
return (value & mask) != 0;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + value;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
return value == ((Http2Flags) obj).value;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("value = ").append(value).append(" (");
if (ack()) {
builder.append("ACK,");
}
if (endOfHeaders()) {
builder.append("END_OF_HEADERS,");
}
if (endOfStream()) {
builder.append("END_OF_STREAM,");
}
if (priorityPresent()) {
builder.append("PRIORITY_PRESENT,");
}
if (paddingPresent()) {
builder.append("PADDING_PRESENT,");
}
builder.append(')');
return builder.toString();
}
}

View File

@ -0,0 +1,70 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.channel.ChannelHandlerContext;
/**
* Base interface for all HTTP/2 flow controllers.
*/
public interface Http2FlowController {
/**
* Sets the initial flow control window and updates all stream windows (but not the connection
* window) by the delta.
* <p>
* This method is used to apply the {@code SETTINGS_INITIAL_WINDOW_SIZE} value for an
* {@code SETTINGS} frame.
*
* @param newWindowSize the new initial window size.
* @throws Http2Exception thrown if any protocol-related error occurred.
*/
void initialWindowSize(int newWindowSize) throws Http2Exception;
/**
* Gets the initial flow control window size that is used as the basis for new stream flow
* control windows.
*/
int initialWindowSize();
/**
* Gets the number of bytes remaining in the flow control window size for the given stream.
*
* @param stream The subject stream. Use {@link Http2Connection#connectionStream()} for
* requesting the size of the connection window.
* @return the current size of the flow control window.
* @throws IllegalArgumentException if the given stream does not exist.
*/
int windowSize(Http2Stream stream);
/**
* Increments the size of the stream's flow control window by the given delta.
* <p>
* In the case of a {@link Http2RemoteFlowController} this is called upon receipt of a
* {@code WINDOW_UPDATE} frame from the remote endpoint to mirror the changes to the window
* size.
* <p>
* For a {@link Http2LocalFlowController} this can be called to request the expansion of the
* window size published by this endpoint. It is up to the implementation, however, as to when a
* {@code WINDOW_UPDATE} is actually sent.
*
* @param ctx The context for the calling handler
* @param stream The subject stream. Use {@link Http2Connection#connectionStream()} for
* requesting the size of the connection window.
* @param delta the change in size of the flow control window.
* @throws Http2Exception thrown if a protocol-related error occurred.
*/
void incrementWindowSize(ChannelHandlerContext ctx, Http2Stream stream, int delta) throws Http2Exception;
}

View File

@ -0,0 +1,88 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
/**
* Convenience class that provides no-op implementations for all methods of {@link Http2FrameListener}.
*/
public class Http2FrameAdapter implements Http2FrameListener {
@Override
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
boolean endOfStream) throws Http2Exception {
return data.readableBytes() + padding;
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
int padding, boolean endStream) throws Http2Exception {
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
int streamDependency, short weight, boolean exclusive, int padding, boolean endStream)
throws Http2Exception {
}
@Override
public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency,
short weight, boolean exclusive) throws Http2Exception {
}
@Override
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode)
throws Http2Exception {
}
@Override
public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception {
}
@Override
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings)
throws Http2Exception {
}
@Override
public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
}
@Override
public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
}
@Override
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
Http2Headers headers, int padding) throws Http2Exception {
}
@Override
public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode,
ByteBuf debugData) throws Http2Exception {
}
@Override
public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
throws Http2Exception {
}
@Override
public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags,
ByteBuf payload) {
}
}

View File

@ -0,0 +1,219 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
/**
* An listener of HTTP/2 frames.
*/
public interface Http2FrameListener {
/**
* Handles an inbound {@code DATA} frame.
*
* @param ctx the context from the handler where the frame was read.
* @param streamId the subject stream for the frame.
* @param data payload buffer for the frame. This buffer will be released by the codec.
* @param padding the number of padding bytes found at the end of the frame.
* @param endOfStream Indicates whether this is the last frame to be sent from the remote
* endpoint for this stream.
* @return the number of bytes that have been processed by the application. The returned bytes
* are used by the inbound flow controller to determine the appropriate time to expand
* the inbound flow control window (i.e. send {@code WINDOW_UPDATE}). Returning a value
* equal to the length of {@code data} + {@code padding} will effectively opt-out of
* application-level flow control for this frame. Returning a value less than the length
* of {@code data} + {@code padding} will defer the returning of the processed bytes,
* which the application must later return via
* {@link Http2InboundFlowState#returnProcessedBytes(ChannelHandlerContext, int)}. The
* returned value must be >= {@code 0} and <= {@code data.readableBytes()} +
* {@code padding}.
*/
int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
boolean endOfStream) throws Http2Exception;
/**
* Handles an inbound HEADERS frame.
* <p>
* Only one of the following methods will be called for each HEADERS frame sequence.
* One will be called when the END_HEADERS flag has been received.
* <ul>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
* </ul>
* <p>
* To say it another way; the {@link Http2Headers} will contain all of the headers
* for the current message exchange step (additional queuing is not necessary).
*
* @param ctx the context from the handler where the frame was read.
* @param streamId the subject stream for the frame.
* @param headers the received headers.
* @param padding the number of padding bytes found at the end of the frame.
* @param endOfStream Indicates whether this is the last frame to be sent from the remote endpoint
* for this stream.
*/
void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endOfStream) throws Http2Exception;
/**
* Handles an inbound HEADERS frame with priority information specified. Only called if END_HEADERS encountered.
*
* <p>
* Only one of the following methods will be called for each HEADERS frame sequence.
* One will be called when the END_HEADERS flag has been received.
* <ul>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
* </ul>
* <p>
* To say it another way; the {@link Http2Headers} will contain all of the headers
* for the current message exchange step (additional queuing is not necessary).
*
* @param ctx the context from the handler where the frame was read.
* @param streamId the subject stream for the frame.
* @param headers the received headers.
* @param streamDependency the stream on which this stream depends, or 0 if dependent on the
* connection.
* @param weight the new weight for the stream.
* @param exclusive whether or not the stream should be the exclusive dependent of its parent.
* @param padding the number of padding bytes found at the end of the frame.
* @param endOfStream Indicates whether this is the last frame to be sent from the remote endpoint
* for this stream.
*/
void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
int streamDependency, short weight, boolean exclusive, int padding, boolean endOfStream)
throws Http2Exception;
/**
* Handles an inbound PRIORITY frame.
*
* @param ctx the context from the handler where the frame was read.
* @param streamId the subject stream for the frame.
* @param streamDependency the stream on which this stream depends, or 0 if dependent on the
* connection.
* @param weight the new weight for the stream.
* @param exclusive whether or not the stream should be the exclusive dependent of its parent.
*/
void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency,
short weight, boolean exclusive) throws Http2Exception;
/**
* Handles an inbound RST_STREAM frame.
*
* @param ctx the context from the handler where the frame was read.
* @param streamId the stream that is terminating.
* @param errorCode the error code identifying the type of failure.
*/
void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception;
/**
* Handles an inbound SETTINGS acknowledgment frame.
* @param ctx the context from the handler where the frame was read.
*/
void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception;
/**
* Handles an inbound SETTINGS frame.
*
* @param ctx the context from the handler where the frame was read.
* @param settings the settings received from the remote endpoint.
*/
void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception;
/**
* Handles an inbound PING frame.
*
* @param ctx the context from the handler where the frame was read.
* @param data the payload of the frame. If this buffer needs to be retained by the listener
* they must make a copy.
*/
void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception;
/**
* Handles an inbound PING acknowledgment.
*
* @param ctx the context from the handler where the frame was read.
* @param data the payload of the frame. If this buffer needs to be retained by the listener
* they must make a copy.
*/
void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception;
/**
* Handles an inbound PUSH_PROMISE frame. Only called if END_HEADERS encountered.
* <p>
* Promised requests MUST be cacheable
* (see <a href="https://tools.ietf.org/html/rfc7231#section-4.2.3">[RFC7231], Section 4.2.3</a>) and
* MUST be safe (see <a href="https://tools.ietf.org/html/rfc7231#section-4.2.1">[RFC7231], Section 4.2.1</a>).
* If these conditions do not hold the application MUST throw a {@link Http2Exception.StreamException} with
* error type {@link Http2Error#PROTOCOL_ERROR}.
* <p>
* Only one of the following methods will be called for each HEADERS frame sequence.
* One will be called when the END_HEADERS flag has been received.
* <ul>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, boolean)}</li>
* <li>{@link #onHeadersRead(ChannelHandlerContext, int, Http2Headers, int, short, boolean, int, boolean)}</li>
* <li>{@link #onPushPromiseRead(ChannelHandlerContext, int, int, Http2Headers, int)}</li>
* </ul>
* <p>
* To say it another way; the {@link Http2Headers} will contain all of the headers
* for the current message exchange step (additional queuing is not necessary).
*
* @param ctx the context from the handler where the frame was read.
* @param streamId the stream the frame was sent on.
* @param promisedStreamId the ID of the promised stream.
* @param headers the received headers.
* @param padding the number of padding bytes found at the end of the frame.
*/
void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
Http2Headers headers, int padding) throws Http2Exception;
/**
* Handles an inbound GO_AWAY frame.
*
* @param ctx the context from the handler where the frame was read.
* @param lastStreamId the last known stream of the remote endpoint.
* @param errorCode the error code, if abnormal closure.
* @param debugData application-defined debug data. If this buffer needs to be retained by the
* listener they must make a copy.
*/
void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData)
throws Http2Exception;
/**
* Handles an inbound WINDOW_UPDATE frame.
*
* @param ctx the context from the handler where the frame was read.
* @param streamId the stream the frame was sent on.
* @param windowSizeIncrement the increased number of bytes of the remote endpoint's flow
* control window.
*/
void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
throws Http2Exception;
/**
* Handler for a frame not defined by the HTTP/2 spec.
*
* @param ctx the context from the handler where the frame was read.
* @param frameType the frame type from the HTTP/2 header.
* @param streamId the stream the frame was sent on.
* @param flags the flags in the frame header.
* @param payload the payload of the frame.
*/
void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags, ByteBuf payload);
}

View File

@ -0,0 +1,103 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
/**
* Provides a decorator around a {@link Http2FrameListener} and delegates all method calls
*/
public class Http2FrameListenerDecorator implements Http2FrameListener {
protected final Http2FrameListener listener;
public Http2FrameListenerDecorator(Http2FrameListener listener) {
this.listener = checkNotNull(listener, "listener");
}
@Override
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)
throws Http2Exception {
return listener.onDataRead(ctx, streamId, data, padding, endOfStream);
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endStream) throws Http2Exception {
listener.onHeadersRead(ctx, streamId, headers, padding, endStream);
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception {
listener.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endStream);
}
@Override
public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight,
boolean exclusive) throws Http2Exception {
listener.onPriorityRead(ctx, streamId, streamDependency, weight, exclusive);
}
@Override
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception {
listener.onRstStreamRead(ctx, streamId, errorCode);
}
@Override
public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception {
listener.onSettingsAckRead(ctx);
}
@Override
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception {
listener.onSettingsRead(ctx, settings);
}
@Override
public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
listener.onPingRead(ctx, data);
}
@Override
public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
listener.onPingAckRead(ctx, data);
}
@Override
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId, Http2Headers headers,
int padding) throws Http2Exception {
listener.onPushPromiseRead(ctx, streamId, promisedStreamId, headers, padding);
}
@Override
public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData)
throws Http2Exception {
listener.onGoAwayRead(ctx, lastStreamId, errorCode, debugData);
}
@Override
public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
throws Http2Exception {
listener.onWindowUpdateRead(ctx, streamId, windowSizeIncrement);
}
@Override
public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags,
ByteBuf payload) {
listener.onUnknownFrame(ctx, frameType, streamId, flags, payload);
}
}

View File

@ -0,0 +1,127 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.util.internal.logging.InternalLogLevel;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;
/**
* Logs HTTP2 frames for debugging purposes.
*/
public class Http2FrameLogger extends ChannelHandlerAdapter {
public enum Direction {
INBOUND,
OUTBOUND
}
private final InternalLogger logger;
private final InternalLogLevel level;
public Http2FrameLogger(InternalLogLevel level) {
this(level, InternalLoggerFactory.getInstance(Http2FrameLogger.class));
}
public Http2FrameLogger(InternalLogLevel level, InternalLogger logger) {
this.level = checkNotNull(level, "level");
this.logger = checkNotNull(logger, "logger");
}
public void logData(Direction direction, int streamId, ByteBuf data, int padding,
boolean endStream) {
log(direction,
"DATA: streamId=%d, padding=%d, endStream=%b, length=%d, bytes=%s",
streamId, padding, endStream, data.readableBytes(), ByteBufUtil.hexDump(data));
}
public void logHeaders(Direction direction, int streamId, Http2Headers headers, int padding,
boolean endStream) {
log(direction, "HEADERS: streamId:%d, headers=%s, padding=%d, endStream=%b",
streamId, headers, padding, endStream);
}
public void logHeaders(Direction direction, int streamId, Http2Headers headers,
int streamDependency, short weight, boolean exclusive, int padding, boolean endStream) {
log(direction,
"HEADERS: streamId:%d, headers=%s, streamDependency=%d, weight=%d, exclusive=%b, "
+ "padding=%d, endStream=%b", streamId, headers,
streamDependency, weight, exclusive, padding, endStream);
}
public void logPriority(Direction direction, int streamId, int streamDependency, short weight,
boolean exclusive) {
log(direction, "PRIORITY: streamId=%d, streamDependency=%d, weight=%d, exclusive=%b",
streamId, streamDependency, weight, exclusive);
}
public void logRstStream(Direction direction, int streamId, long errorCode) {
log(direction, "RST_STREAM: streamId=%d, errorCode=%d", streamId, errorCode);
}
public void logSettingsAck(Direction direction) {
log(direction, "SETTINGS ack=true");
}
public void logSettings(Direction direction, Http2Settings settings) {
log(direction, "SETTINGS: ack=false, settings=%s", settings);
}
public void logPing(Direction direction, ByteBuf data) {
log(direction, "PING: ack=false, length=%d, bytes=%s", data.readableBytes(), ByteBufUtil.hexDump(data));
}
public void logPingAck(Direction direction, ByteBuf data) {
log(direction, "PING: ack=true, length=%d, bytes=%s", data.readableBytes(), ByteBufUtil.hexDump(data));
}
public void logPushPromise(Direction direction, int streamId, int promisedStreamId,
Http2Headers headers, int padding) {
log(direction, "PUSH_PROMISE: streamId=%d, promisedStreamId=%d, headers=%s, padding=%d",
streamId, promisedStreamId, headers, padding);
}
public void logGoAway(Direction direction, int lastStreamId, long errorCode, ByteBuf debugData) {
log(direction, "GO_AWAY: lastStreamId=%d, errorCode=%d, length=%d, bytes=%s", lastStreamId,
errorCode, debugData.readableBytes(), ByteBufUtil.hexDump(debugData));
}
public void logWindowsUpdate(Direction direction, int streamId, int windowSizeIncrement) {
log(direction, "WINDOW_UPDATE: streamId=%d, windowSizeIncrement=%d", streamId,
windowSizeIncrement);
}
public void logUnknownFrame(Direction direction, byte frameType, int streamId, Http2Flags flags, ByteBuf data) {
log(direction, "UNKNOWN: frameType=%d, streamId=%d, flags=%d, length=%d, bytes=%s",
frameType & 0xFF, streamId, flags.value(), data.readableBytes(), ByteBufUtil.hexDump(data));
}
private void log(Direction direction, String format, Object... args) {
if (logger.isEnabled(level)) {
StringBuilder b = new StringBuilder(200);
b.append("\n----------------")
.append(direction.name())
.append("--------------------\n")
.append(String.format(format, args))
.append("\n------------------------------------");
logger.log(level, b.toString());
}
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import java.io.Closeable;
/**
* Reads HTTP/2 frames from an input {@link ByteBuf} and notifies the specified
* {@link Http2FrameListener} when frames are complete.
*/
public interface Http2FrameReader extends Closeable {
/**
* Configuration specific to {@link Http2FrameReader}
*/
interface Configuration {
/**
* Get the {@link Http2HeaderTable} for this {@link Http2FrameReader}
*/
Http2HeaderTable headerTable();
/**
* Get the {@link Http2FrameSizePolicy} for this {@link Http2FrameReader}
*/
Http2FrameSizePolicy frameSizePolicy();
}
/**
* Attempts to read the next frame from the input buffer. If enough data is available to fully
* read the frame, notifies the listener of the read frame.
*/
void readFrame(ChannelHandlerContext ctx, ByteBuf input, Http2FrameListener listener)
throws Http2Exception;
/**
* Get the configuration related elements for this {@link Http2FrameReader}
*/
Configuration configuration();
/**
* Closes this reader and frees any allocated resources.
*/
@Override
void close();
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
public interface Http2FrameSizePolicy {
/**
* Sets the maximum allowed frame size. Attempts to write frames longer than this maximum will fail.
*/
void maxFrameSize(int max) throws Http2Exception;
/**
* Gets the maximum allowed frame size.
*/
int maxFrameSize();
}

View File

@ -0,0 +1,35 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
/**
* Registry of all standard frame types defined by the HTTP/2 specification.
*/
public final class Http2FrameTypes {
public static final byte DATA = 0x0;
public static final byte HEADERS = 0x1;
public static final byte PRIORITY = 0x2;
public static final byte RST_STREAM = 0x3;
public static final byte SETTINGS = 0x4;
public static final byte PUSH_PROMISE = 0x5;
public static final byte PING = 0x6;
public static final byte GO_AWAY = 0x7;
public static final byte WINDOW_UPDATE = 0x8;
public static final byte CONTINUATION = 0x9;
private Http2FrameTypes() {
}
}

View File

@ -0,0 +1,203 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import java.io.Closeable;
/**
* A writer responsible for marshaling HTTP/2 frames to the channel. All of the write methods in
* this interface write to the context, but DO NOT FLUSH. To perform a flush, you must separately
* call {@link ChannelHandlerContext#flush()}.
*/
public interface Http2FrameWriter extends Http2DataWriter, Closeable {
/**
* Configuration specific to {@link Http2FrameWriter}
*/
interface Configuration {
/**
* Get the {@link Http2HeaderTable} for this {@link Http2FrameWriter}
*/
Http2HeaderTable headerTable();
/**
* Get the {@link Http2FrameSizePolicy} for this {@link Http2FrameWriter}
*/
Http2FrameSizePolicy frameSizePolicy();
}
/**
* Writes a HEADERS frame to the remote endpoint.
*
* @param ctx the context to use for writing.
* @param streamId the stream for which to send the frame.
* @param headers the headers to be sent.
* @param padding the amount of padding to be added to the end of the frame
* @param endStream indicates if this is the last frame to be sent for the stream.
* @param promise the promise for the write.
* @return the future for the write.
*/
ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
int padding, boolean endStream, ChannelPromise promise);
/**
* Writes a HEADERS frame with priority specified to the remote endpoint.
*
* @param ctx the context to use for writing.
* @param streamId the stream for which to send the frame.
* @param headers the headers to be sent.
* @param streamDependency the stream on which this stream should depend, or 0 if it should
* depend on the connection.
* @param weight the weight for this stream.
* @param exclusive whether this stream should be the exclusive dependant of its parent.
* @param padding the amount of padding to be added to the end of the frame
* @param endStream indicates if this is the last frame to be sent for the stream.
* @param promise the promise for the write.
* @return the future for the write.
*/
ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
int streamDependency, short weight, boolean exclusive, int padding, boolean endStream,
ChannelPromise promise);
/**
* Writes a PRIORITY frame to the remote endpoint.
*
* @param ctx the context to use for writing.
* @param streamId the stream for which to send the frame.
* @param streamDependency the stream on which this stream should depend, or 0 if it should
* depend on the connection.
* @param weight the weight for this stream.
* @param exclusive whether this stream should be the exclusive dependant of its parent.
* @param promise the promise for the write.
* @return the future for the write.
*/
ChannelFuture writePriority(ChannelHandlerContext ctx, int streamId, int streamDependency,
short weight, boolean exclusive, ChannelPromise promise);
/**
* Writes a RST_STREAM frame to the remote endpoint.
*
* @param ctx the context to use for writing.
* @param streamId the stream for which to send the frame.
* @param errorCode the error code indicating the nature of the failure.
* @param promise the promise for the write.
* @return the future for the write.
*/
ChannelFuture writeRstStream(ChannelHandlerContext ctx, int streamId, long errorCode,
ChannelPromise promise);
/**
* Writes a SETTINGS frame to the remote endpoint.
*
* @param ctx the context to use for writing.
* @param settings the settings to be sent.
* @param promise the promise for the write.
* @return the future for the write.
*/
ChannelFuture writeSettings(ChannelHandlerContext ctx, Http2Settings settings,
ChannelPromise promise);
/**
* Writes a SETTINGS acknowledgment to the remote endpoint.
*
* @param ctx the context to use for writing.
* @param promise the promise for the write.
* @return the future for the write.
*/
ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise);
/**
* Writes a PING frame to the remote endpoint.
*
* @param ctx the context to use for writing.
* @param ack indicates whether this is an ack of a PING frame previously received from the
* remote endpoint.
* @param data the payload of the frame.
* @param promise the promise for the write.
* @return the future for the write.
*/
ChannelFuture writePing(ChannelHandlerContext ctx, boolean ack, ByteBuf data,
ChannelPromise promise);
/**
* Writes a PUSH_PROMISE frame to the remote endpoint.
*
* @param ctx the context to use for writing.
* @param streamId the stream for which to send the frame.
* @param promisedStreamId the ID of the promised stream.
* @param headers the headers to be sent.
* @param padding the amount of padding to be added to the end of the frame
* @param promise the promise for the write.
* @return the future for the write.
*/
ChannelFuture writePushPromise(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
Http2Headers headers, int padding, ChannelPromise promise);
/**
* Writes a GO_AWAY frame to the remote endpoint.
*
* @param ctx the context to use for writing.
* @param lastStreamId the last known stream of this endpoint.
* @param errorCode the error code, if the connection was abnormally terminated.
* @param debugData application-defined debug data.
* @param promise the promise for the write.
* @return the future for the write.
*/
ChannelFuture writeGoAway(ChannelHandlerContext ctx, int lastStreamId, long errorCode,
ByteBuf debugData, ChannelPromise promise);
/**
* Writes a WINDOW_UPDATE frame to the remote endpoint.
*
* @param ctx the context to use for writing.
* @param streamId the stream for which to send the frame.
* @param windowSizeIncrement the number of bytes by which the local inbound flow control window
* is increasing.
* @param promise the promise for the write.
* @return the future for the write.
*/
ChannelFuture writeWindowUpdate(ChannelHandlerContext ctx, int streamId,
int windowSizeIncrement, ChannelPromise promise);
/**
* Generic write method for any HTTP/2 frame. This allows writing of non-standard frames.
*
* @param ctx the context to use for writing.
* @param frameType the frame type identifier.
* @param streamId the stream for which to send the frame.
* @param flags the flags to write for this frame.
* @param payload the payload to write for this frame.
* @param promise the promise for the write.
* @return the future for the write.
*/
ChannelFuture writeFrame(ChannelHandlerContext ctx, byte frameType, int streamId,
Http2Flags flags, ByteBuf payload, ChannelPromise promise);
/**
* Get the configuration related elements for this {@link Http2FrameWriter}
*/
Configuration configuration();
/**
* Closes this writer and frees any allocated resources.
*/
@Override
void close();
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
/**
* Extracts a common interface for encoding and processing HPACK header constraints
*/
public interface Http2HeaderTable {
/**
* Sets the maximum size of the HPACK header table used for decoding HTTP/2 headers.
*/
void maxHeaderTableSize(int max) throws Http2Exception;
/**
* Gets the maximum size of the HPACK header table used for decoding HTTP/2 headers.
*/
int maxHeaderTableSize();
/**
* Sets the maximum allowed header elements.
*/
void maxHeaderListSize(int max) throws Http2Exception;
/**
* Gets the maximum allowed header elements.
*/
int maxHeaderListSize();
}

View File

@ -0,0 +1,234 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.BinaryHeaders;
import java.util.HashSet;
import java.util.Set;
/**
* A collection of headers sent or received via HTTP/2.
*/
public interface Http2Headers extends BinaryHeaders {
/**
* HTTP/2 pseudo-headers names.
*/
enum PseudoHeaderName {
/**
* {@code :method}.
*/
METHOD(":method"),
/**
* {@code :scheme}.
*/
SCHEME(":scheme"),
/**
* {@code :authority}.
*/
AUTHORITY(":authority"),
/**
* {@code :path}.
*/
PATH(":path"),
/**
* {@code :status}.
*/
STATUS(":status");
private final AsciiString value;
private static final Set<AsciiString> PSEUDO_HEADERS = new HashSet<AsciiString>();
static {
for (PseudoHeaderName pseudoHeader : PseudoHeaderName.values()) {
PSEUDO_HEADERS.add(pseudoHeader.value());
}
}
PseudoHeaderName(String value) {
this.value = new AsciiString(value);
}
public AsciiString value() {
// Return a slice so that the buffer gets its own reader index.
return value;
}
/**
* Indicates whether the given header name is a valid HTTP/2 pseudo header.
*/
public static boolean isPseudoHeader(AsciiString header) {
return PSEUDO_HEADERS.contains(header);
}
}
@Override
Http2Headers add(AsciiString name, AsciiString value);
@Override
Http2Headers add(AsciiString name, Iterable<? extends AsciiString> values);
@Override
Http2Headers add(AsciiString name, AsciiString... values);
@Override
Http2Headers addObject(AsciiString name, Object value);
@Override
Http2Headers addObject(AsciiString name, Iterable<?> values);
@Override
Http2Headers addObject(AsciiString name, Object... values);
@Override
Http2Headers addBoolean(AsciiString name, boolean value);
@Override
Http2Headers addByte(AsciiString name, byte value);
@Override
Http2Headers addChar(AsciiString name, char value);
@Override
Http2Headers addShort(AsciiString name, short value);
@Override
Http2Headers addInt(AsciiString name, int value);
@Override
Http2Headers addLong(AsciiString name, long value);
@Override
Http2Headers addFloat(AsciiString name, float value);
@Override
Http2Headers addDouble(AsciiString name, double value);
@Override
Http2Headers addTimeMillis(AsciiString name, long value);
@Override
Http2Headers add(BinaryHeaders headers);
@Override
Http2Headers set(AsciiString name, AsciiString value);
@Override
Http2Headers set(AsciiString name, Iterable<? extends AsciiString> values);
@Override
Http2Headers set(AsciiString name, AsciiString... values);
@Override
Http2Headers setObject(AsciiString name, Object value);
@Override
Http2Headers setObject(AsciiString name, Iterable<?> values);
@Override
Http2Headers setObject(AsciiString name, Object... values);
@Override
Http2Headers setBoolean(AsciiString name, boolean value);
@Override
Http2Headers setByte(AsciiString name, byte value);
@Override
Http2Headers setChar(AsciiString name, char value);
@Override
Http2Headers setShort(AsciiString name, short value);
@Override
Http2Headers setInt(AsciiString name, int value);
@Override
Http2Headers setLong(AsciiString name, long value);
@Override
Http2Headers setFloat(AsciiString name, float value);
@Override
Http2Headers setDouble(AsciiString name, double value);
@Override
Http2Headers setTimeMillis(AsciiString name, long value);
@Override
Http2Headers set(BinaryHeaders headers);
@Override
Http2Headers setAll(BinaryHeaders headers);
@Override
Http2Headers clear();
/**
* Sets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header
*/
Http2Headers method(AsciiString value);
/**
* Sets the {@link PseudoHeaderName#SCHEME} header if there is no such header
*/
Http2Headers scheme(AsciiString value);
/**
* Sets the {@link PseudoHeaderName#AUTHORITY} header or {@code null} if there is no such header
*/
Http2Headers authority(AsciiString value);
/**
* Sets the {@link PseudoHeaderName#PATH} header or {@code null} if there is no such header
*/
Http2Headers path(AsciiString value);
/**
* Sets the {@link PseudoHeaderName#STATUS} header or {@code null} if there is no such header
*/
Http2Headers status(AsciiString value);
/**
* Gets the {@link PseudoHeaderName#METHOD} header or {@code null} if there is no such header
*/
AsciiString method();
/**
* Gets the {@link PseudoHeaderName#SCHEME} header or {@code null} if there is no such header
*/
AsciiString scheme();
/**
* Gets the {@link PseudoHeaderName#AUTHORITY} header or {@code null} if there is no such header
*/
AsciiString authority();
/**
* Gets the {@link PseudoHeaderName#PATH} header or {@code null} if there is no such header
*/
AsciiString path();
/**
* Gets the {@link PseudoHeaderName#STATUS} header or {@code null} if there is no such header
*/
AsciiString status();
}

View File

@ -0,0 +1,43 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
/**
* Decodes HPACK-encoded headers blocks into {@link Http2Headers}.
*/
public interface Http2HeadersDecoder {
/**
* Configuration related elements for the {@link Http2HeadersDecoder} interface
*/
interface Configuration {
/**
* Access the Http2HeaderTable for this {@link Http2HeadersDecoder}
*/
Http2HeaderTable headerTable();
}
/**
* Decodes the given headers block and returns the headers.
*/
Http2Headers decodeHeaders(ByteBuf headerBlock) throws Http2Exception;
/**
* Get the {@link Configuration} for this {@link Http2HeadersDecoder}
*/
Configuration configuration();
}

View File

@ -0,0 +1,46 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
/**
* Encodes {@link Http2Headers} into HPACK-encoded headers blocks.
*/
public interface Http2HeadersEncoder {
/**
* Configuration related elements for the {@link Http2HeadersEncoder} interface
*/
interface Configuration {
/**
* Access the Http2HeaderTable for this {@link Http2HeadersEncoder}
*/
Http2HeaderTable headerTable();
}
/**
* Encodes the given headers and writes the output headers block to the given output buffer.
*
* @param headers the headers to be encoded.
* @param buffer the buffer to receive the encoded headers.
*/
void encodeHeaders(Http2Headers headers, ByteBuf buffer) throws Http2Exception;
/**
* Get the {@link Configuration} for this {@link Http2HeadersEncoder}
*/
Configuration configuration();
}

View File

@ -0,0 +1,145 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2FrameLogger.Direction.INBOUND;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
/**
* Decorator around a {@link Http2FrameReader} that logs all inbound frames before calling
* back the listener.
*/
public class Http2InboundFrameLogger implements Http2FrameReader {
private final Http2FrameReader reader;
private final Http2FrameLogger logger;
public Http2InboundFrameLogger(Http2FrameReader reader, Http2FrameLogger logger) {
this.reader = checkNotNull(reader, "reader");
this.logger = checkNotNull(logger, "logger");
}
@Override
public void readFrame(ChannelHandlerContext ctx, ByteBuf input, final Http2FrameListener listener)
throws Http2Exception {
reader.readFrame(ctx, input, new Http2FrameListener() {
@Override
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data,
int padding, boolean endOfStream)
throws Http2Exception {
logger.logData(INBOUND, streamId, data, padding, endOfStream);
return listener.onDataRead(ctx, streamId, data, padding, endOfStream);
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId,
Http2Headers headers, int padding, boolean endStream)
throws Http2Exception {
logger.logHeaders(INBOUND, streamId, headers, padding, endStream);
listener.onHeadersRead(ctx, streamId, headers, padding, endStream);
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId,
Http2Headers headers, int streamDependency, short weight, boolean exclusive,
int padding, boolean endStream) throws Http2Exception {
logger.logHeaders(INBOUND, streamId, headers, streamDependency, weight, exclusive,
padding, endStream);
listener.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive,
padding, endStream);
}
@Override
public void onPriorityRead(ChannelHandlerContext ctx, int streamId,
int streamDependency, short weight, boolean exclusive) throws Http2Exception {
logger.logPriority(INBOUND, streamId, streamDependency, weight, exclusive);
listener.onPriorityRead(ctx, streamId, streamDependency, weight, exclusive);
}
@Override
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode)
throws Http2Exception {
logger.logRstStream(INBOUND, streamId, errorCode);
listener.onRstStreamRead(ctx, streamId, errorCode);
}
@Override
public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception {
logger.logSettingsAck(INBOUND);
listener.onSettingsAckRead(ctx);
}
@Override
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings)
throws Http2Exception {
logger.logSettings(INBOUND, settings);
listener.onSettingsRead(ctx, settings);
}
@Override
public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
logger.logPing(INBOUND, data);
listener.onPingRead(ctx, data);
}
@Override
public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
logger.logPingAck(INBOUND, data);
listener.onPingAckRead(ctx, data);
}
@Override
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId,
int promisedStreamId, Http2Headers headers, int padding) throws Http2Exception {
logger.logPushPromise(INBOUND, streamId, promisedStreamId, headers, padding);
listener.onPushPromiseRead(ctx, streamId, promisedStreamId, headers, padding);
}
@Override
public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode,
ByteBuf debugData) throws Http2Exception {
logger.logGoAway(INBOUND, lastStreamId, errorCode, debugData);
listener.onGoAwayRead(ctx, lastStreamId, errorCode, debugData);
}
@Override
public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
throws Http2Exception {
logger.logWindowsUpdate(INBOUND, streamId, windowSizeIncrement);
listener.onWindowUpdateRead(ctx, streamId, windowSizeIncrement);
}
@Override
public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId,
Http2Flags flags, ByteBuf payload) {
logger.logUnknownFrame(INBOUND, frameType, streamId, flags, payload);
listener.onUnknownFrame(ctx, frameType, streamId, flags, payload);
}
});
}
@Override
public void close() {
reader.close();
}
@Override
public Configuration configuration() {
return reader.configuration();
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
/**
* Manager for the life cycle of the HTTP/2 connection. Handles graceful shutdown of the channel,
* closing only after all of the streams have closed.
*/
public interface Http2LifecycleManager {
/**
* Closes the local side of the given stream. If this causes the stream to be closed, adds a
* hook to close the channel after the given future completes.
*
* @param stream the stream to be half closed.
* @param future If closing, the future after which to close the channel.
*/
void closeLocalSide(Http2Stream stream, ChannelFuture future);
/**
* Closes the remote side of the given stream. If this causes the stream to be closed, adds a
* hook to close the channel after the given future completes.
*
* @param stream the stream to be half closed.
* @param future If closing, the future after which to close the channel.
*/
void closeRemoteSide(Http2Stream stream, ChannelFuture future);
/**
* Closes the given stream and adds a hook to close the channel after the given future
* completes.
*
* @param stream the stream to be closed.
* @param future the future after which to close the channel.
*/
void closeStream(Http2Stream stream, ChannelFuture future);
/**
* Writes a {@code RST_STREAM} frame to the remote endpoint and updates the connection state
* appropriately.
*/
ChannelFuture writeRstStream(ChannelHandlerContext ctx, int streamId, long errorCode,
ChannelPromise promise);
/**
* Sends a {@code GO_AWAY} frame to the remote endpoint and updates the connection state
* appropriately.
*/
ChannelFuture writeGoAway(ChannelHandlerContext ctx, int lastStreamId, long errorCode,
ByteBuf debugData, ChannelPromise promise);
/**
* Processes the given exception.
*/
void onException(ChannelHandlerContext ctx, Throwable cause);
}

View File

@ -0,0 +1,69 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
/**
* A {@link Http2FlowController} for controlling the inbound flow of {@code DATA} frames from the remote
* endpoint.
*/
public interface Http2LocalFlowController extends Http2FlowController {
/**
* Receives an inbound {@code DATA} frame from the remote endpoint and applies flow control
* policies to it for both the {@code stream} as well as the connection. If any flow control
* policies have been violated, an exception is raised immediately, otherwise the frame is
* considered to have "passed" flow control.
*
* @param ctx the context from the handler where the frame was read.
* @param stream the subject stream for the received frame. The connection stream object must
* not be used.
* @param data payload buffer for the frame.
* @param padding the number of padding bytes found at the end of the frame.
* @param endOfStream Indicates whether this is the last frame to be sent from the remote
* endpoint for this stream.
* @throws Http2Exception if any flow control errors are encountered.
*/
void receiveFlowControlledFrame(ChannelHandlerContext ctx, Http2Stream stream, ByteBuf data, int padding,
boolean endOfStream) throws Http2Exception;
/**
* Indicates that the application has consumed a number of bytes for the given stream and is
* therefore ready to receive more data from the remote endpoint. The application must consume
* any bytes that it receives or the flow control window will collapse. Consuming bytes enables
* the flow controller to send {@code WINDOW_UPDATE} to restore a portion of the flow control
* window for the stream.
*
* @param ctx the channel handler context to use when sending a {@code WINDOW_UPDATE} if
* appropriate
* @param stream the stream for which window space should be freed. The connection stream object
* must not be used.
* @param numBytes the number of bytes to be returned to the flow control window.
* @throws Http2Exception if the number of bytes returned exceeds the {@link #unconsumedBytes()}
* for the stream.
*/
void consumeBytes(ChannelHandlerContext ctx, Http2Stream stream, int numBytes) throws Http2Exception;
/**
* The number of bytes for the given stream that have been received but not yet consumed by the
* application.
*
* @param stream the stream for which window space should be freed.
* @return the number of unconsumed bytes for the stream.
*/
int unconsumedBytes(Http2Stream stream);
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
/**
* This exception is thrown when there are no more stream IDs available for the current connection
*/
public class Http2NoMoreStreamIdsException extends Http2Exception {
private static final long serialVersionUID = -7756236161274851110L;
private static final String ERROR_MESSAGE = "No more streams can be created on this connection";
public Http2NoMoreStreamIdsException() {
super(PROTOCOL_ERROR, ERROR_MESSAGE);
}
public Http2NoMoreStreamIdsException(Throwable cause) {
super(PROTOCOL_ERROR, ERROR_MESSAGE, cause);
}
}

View File

@ -0,0 +1,155 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.TLS_UPGRADE_PROTOCOL_NAME;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.handler.codec.http.HttpResponseEncoder;
import io.netty.handler.ssl.SslHandler;
import java.util.List;
import javax.net.ssl.SSLEngine;
/**
* {@link io.netty.channel.ChannelHandler} which is responsible to setup the
* {@link io.netty.channel.ChannelPipeline} either for HTTP or HTTP2. This offers an easy way for
* users to support both at the same time while not care to much about the low-level details.
*/
public abstract class Http2OrHttpChooser extends ByteToMessageDecoder {
public enum SelectedProtocol {
/** Must be updated to match the HTTP/2 draft number. */
HTTP_2(TLS_UPGRADE_PROTOCOL_NAME),
HTTP_1_1("http/1.1"),
HTTP_1_0("http/1.0"),
UNKNOWN("Unknown");
private final String name;
SelectedProtocol(String defaultName) {
name = defaultName;
}
public String protocolName() {
return name;
}
/**
* Get an instance of this enum based on the protocol name returned by the NPN server provider
*
* @param name
* the protocol name
* @return the SelectedProtocol instance
*/
public static SelectedProtocol protocol(String name) {
for (SelectedProtocol protocol : SelectedProtocol.values()) {
if (protocol.protocolName().equals(name)) {
return protocol;
}
}
return UNKNOWN;
}
}
private final int maxHttpContentLength;
protected Http2OrHttpChooser(int maxHttpContentLength) {
this.maxHttpContentLength = maxHttpContentLength;
}
/**
* Return the {@link SelectedProtocol} for the {@link javax.net.ssl.SSLEngine}. If its not known
* yet implementations MUST return {@link SelectedProtocol#UNKNOWN}.
*/
protected abstract SelectedProtocol getProtocol(SSLEngine engine);
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (initPipeline(ctx)) {
// When we reached here we can remove this handler as its now clear
// what protocol we want to use
// from this point on. This will also take care of forward all
// messages.
ctx.pipeline().remove(this);
}
}
private boolean initPipeline(ChannelHandlerContext ctx) {
// Get the SslHandler from the ChannelPipeline so we can obtain the
// SslEngine from it.
SslHandler handler = ctx.pipeline().get(SslHandler.class);
if (handler == null) {
// HTTP2 is negotiated through SSL.
throw new IllegalStateException("SslHandler is needed for HTTP2");
}
SelectedProtocol protocol = getProtocol(handler.engine());
switch (protocol) {
case UNKNOWN:
// Not done with choosing the protocol, so just return here for now,
return false;
case HTTP_2:
addHttp2Handlers(ctx);
break;
case HTTP_1_0:
case HTTP_1_1:
addHttpHandlers(ctx);
break;
default:
throw new IllegalStateException("Unknown SelectedProtocol");
}
return true;
}
/**
* Add all {@link io.netty.channel.ChannelHandler}'s that are needed for HTTP_2.
*/
protected void addHttp2Handlers(ChannelHandlerContext ctx) {
ChannelPipeline pipeline = ctx.pipeline();
pipeline.addLast("http2ConnectionHandler", createHttp2RequestHandler());
}
/**
* Add all {@link io.netty.channel.ChannelHandler}'s that are needed for HTTP.
*/
protected void addHttpHandlers(ChannelHandlerContext ctx) {
ChannelPipeline pipeline = ctx.pipeline();
pipeline.addLast("httpRequestDecoder", new HttpRequestDecoder());
pipeline.addLast("httpResponseEncoder", new HttpResponseEncoder());
pipeline.addLast("httpChunkAggregator", new HttpObjectAggregator(maxHttpContentLength));
pipeline.addLast("httpRequestHandler", createHttp1RequestHandler());
}
/**
* Create the {@link io.netty.channel.ChannelHandler} that is responsible for handling the http
* requests when the {@link SelectedProtocol} was {@link SelectedProtocol#HTTP_1_0} or
* {@link SelectedProtocol#HTTP_1_1}
*/
protected abstract ChannelHandler createHttp1RequestHandler();
/**
* Create the {@link ChannelHandler} that is responsible for handling the http responses
* when the when the {@link SelectedProtocol} was {@link SelectedProtocol#HTTP_2}.
*/
protected abstract Http2ConnectionHandler createHttp2RequestHandler();
}

View File

@ -0,0 +1,133 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2FrameLogger.Direction.OUTBOUND;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
/**
* Decorator around a {@link Http2FrameWriter} that logs all outbound frames before calling the
* writer.
*/
public class Http2OutboundFrameLogger implements Http2FrameWriter {
private final Http2FrameWriter writer;
private final Http2FrameLogger logger;
public Http2OutboundFrameLogger(Http2FrameWriter writer, Http2FrameLogger logger) {
this.writer = checkNotNull(writer, "writer");
this.logger = checkNotNull(logger, "logger");
}
@Override
public ChannelFuture writeData(ChannelHandlerContext ctx, int streamId, ByteBuf data,
int padding, boolean endStream, ChannelPromise promise) {
logger.logData(OUTBOUND, streamId, data, padding, endStream);
return writer.writeData(ctx, streamId, data, padding, endStream, promise);
}
@Override
public ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId,
Http2Headers headers, int padding, boolean endStream, ChannelPromise promise) {
logger.logHeaders(OUTBOUND, streamId, headers, padding, endStream);
return writer.writeHeaders(ctx, streamId, headers, padding, endStream, promise);
}
@Override
public ChannelFuture writeHeaders(ChannelHandlerContext ctx, int streamId,
Http2Headers headers, int streamDependency, short weight, boolean exclusive,
int padding, boolean endStream, ChannelPromise promise) {
logger.logHeaders(OUTBOUND, streamId, headers, streamDependency, weight, exclusive,
padding, endStream);
return writer.writeHeaders(ctx, streamId, headers, streamDependency, weight,
exclusive, padding, endStream, promise);
}
@Override
public ChannelFuture writePriority(ChannelHandlerContext ctx, int streamId,
int streamDependency, short weight, boolean exclusive, ChannelPromise promise) {
logger.logPriority(OUTBOUND, streamId, streamDependency, weight, exclusive);
return writer.writePriority(ctx, streamId, streamDependency, weight, exclusive, promise);
}
@Override
public ChannelFuture writeRstStream(ChannelHandlerContext ctx,
int streamId, long errorCode, ChannelPromise promise) {
logger.logRstStream(OUTBOUND, streamId, errorCode);
return writer.writeRstStream(ctx, streamId, errorCode, promise);
}
@Override
public ChannelFuture writeSettings(ChannelHandlerContext ctx,
Http2Settings settings, ChannelPromise promise) {
logger.logSettings(OUTBOUND, settings);
return writer.writeSettings(ctx, settings, promise);
}
@Override
public ChannelFuture writeSettingsAck(ChannelHandlerContext ctx, ChannelPromise promise) {
logger.logSettingsAck(OUTBOUND);
return writer.writeSettingsAck(ctx, promise);
}
@Override
public ChannelFuture writePing(ChannelHandlerContext ctx, boolean ack,
ByteBuf data, ChannelPromise promise) {
logger.logPing(OUTBOUND, data);
return writer.writePing(ctx, ack, data, promise);
}
@Override
public ChannelFuture writePushPromise(ChannelHandlerContext ctx, int streamId,
int promisedStreamId, Http2Headers headers, int padding, ChannelPromise promise) {
logger.logPushPromise(OUTBOUND, streamId, promisedStreamId, headers, padding);
return writer.writePushPromise(ctx, streamId, promisedStreamId, headers, padding, promise);
}
@Override
public ChannelFuture writeGoAway(ChannelHandlerContext ctx, int lastStreamId, long errorCode,
ByteBuf debugData, ChannelPromise promise) {
logger.logGoAway(OUTBOUND, lastStreamId, errorCode, debugData);
return writer.writeGoAway(ctx, lastStreamId, errorCode, debugData, promise);
}
@Override
public ChannelFuture writeWindowUpdate(ChannelHandlerContext ctx,
int streamId, int windowSizeIncrement, ChannelPromise promise) {
logger.logWindowsUpdate(OUTBOUND, streamId, windowSizeIncrement);
return writer.writeWindowUpdate(ctx, streamId, windowSizeIncrement, promise);
}
@Override
public ChannelFuture writeFrame(ChannelHandlerContext ctx, byte frameType, int streamId,
Http2Flags flags, ByteBuf payload, ChannelPromise promise) {
logger.logUnknownFrame(OUTBOUND, frameType, streamId, flags, payload);
return writer.writeFrame(ctx, frameType, streamId, flags, payload, promise);
}
@Override
public void close() {
writer.close();
}
@Override
public Configuration configuration() {
return writer.configuration();
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
/**
* A {@link Http2FlowController} for controlling the flow of outbound {@code DATA} frames to the remote
* endpoint.
*/
public interface Http2RemoteFlowController extends Http2FlowController {
/**
* Writes or queues a {@code DATA} frame for transmission to the remote endpoint. There is no
* guarantee when the data will be written or whether it will be split into multiple frames
* before sending. The returned future will only be completed once all of the data has been
* successfully written to the remote endpoint.
* <p>
* Manually flushing the {@link ChannelHandlerContext} is not required, since the flow
* controller will flush as appropriate.
*
* @param ctx the context from the handler.
* @param stream the subject stream. Must not be the connection stream object.
* @param data payload buffer for the frame.
* @param padding the number of padding bytes to be added at the end of the frame.
* @param endStream Indicates whether this is the last frame to be sent to the remote endpoint
* for this stream.
* @param promise the promise to be completed when the data has been successfully written or a
* failure occurs.
* @return a future that is completed when the frame is sent to the remote endpoint.
*/
ChannelFuture sendFlowControlledFrame(ChannelHandlerContext ctx, Http2Stream stream,
ByteBuf data, int padding, boolean endStream, ChannelPromise promise);
/**
* Gets the {@link ChannelFuture} for the most recent frame that was sent for the given stream
* via a call to
* {@link #sendFlowControlledFrame(ChannelHandlerContext, Http2Stream, ByteBuf, int, boolean, ChannelPromise)}.
* This is useful for cases such as ensuring that {@code HEADERS} frames maintain send order with {@code DATA}
* frames.
*
* @param stream the subject stream. Must not be the connection stream object.
* @return the most recent sent frame, or {@code null} if no frame has been sent for the stream.
*/
ChannelFuture lastFlowControlledFrameSent(Http2Stream stream);
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.codec.http2;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Provides utilities related to security requirements specific to HTTP/2.
*/
public final class Http2SecurityUtil {
/**
* The following list is derived from <a
* href="http://docs.oracle.com/javase/8/docs/technotes/guides/security/SunProviders.html">SunJSSE Supported
* Ciphers</a> and <a
* href="https://wiki.mozilla.org/Security/Server_Side_TLS#Non-Backward_Compatible_Ciphersuite">Mozilla Cipher
* Suites</a> in accordance with the <a
* href="https://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-9.2.2">HTTP/2 Specification</a>.
*/
public static final List<String> CIPHERS;
private static final List<String> CIPHERS_JAVA_MOZILLA_INCREASED_SECURITY = Collections.unmodifiableList(Arrays
.asList(
/* Java 8 */
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", /* openssl = ECDHE-ECDSA-AES256-GCM-SHA384 */
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", /* openssl = ECDHE-ECDSA-AES128-GCM-SHA256 */
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", /* openssl = ECDHE-RSA-AES256-GCM-SHA384 */
/* REQUIRED BY HTTP/2 SPEC */
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", /* openssl = ECDHE-RSA-AES128-GCM-SHA256 */
/* REQUIRED BY HTTP/2 SPEC */
"TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", /* openssl = DHE-RSA-AES128-GCM-SHA256 */
"TLS_DHE_DSS_WITH_AES_128_GCM_SHA256" /* openssl = DHE-DSS-AES128-GCM-SHA256 */));
private static final List<String> CIPHERS_JAVA_NO_MOZILLA_INCREASED_SECURITY = Collections.unmodifiableList(Arrays
.asList(
/* Java 8 */
"TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", /* openssl = DHE-RSA-AES256-GCM-SHA384 */
"TLS_DHE_DSS_WITH_AES_256_GCM_SHA384" /* openssl = DHE-DSS-AES256-GCM-SHA384 */));
static {
List<String> ciphers = new ArrayList<String>(CIPHERS_JAVA_MOZILLA_INCREASED_SECURITY.size()
+ CIPHERS_JAVA_NO_MOZILLA_INCREASED_SECURITY.size());
ciphers.addAll(CIPHERS_JAVA_MOZILLA_INCREASED_SECURITY);
ciphers.addAll(CIPHERS_JAVA_NO_MOZILLA_INCREASED_SECURITY);
CIPHERS = Collections.unmodifiableList(ciphers);
}
private Http2SecurityUtil() { }
}

View File

@ -0,0 +1,160 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.base64.Base64Dialect.URL_SAFE;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http2.Http2CodecUtil.FRAME_HEADER_LENGTH;
import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_PROTOCOL_NAME;
import static io.netty.handler.codec.http2.Http2CodecUtil.HTTP_UPGRADE_SETTINGS_HEADER;
import static io.netty.handler.codec.http2.Http2CodecUtil.writeFrameHeader;
import static io.netty.handler.codec.http2.Http2FrameTypes.SETTINGS;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.base64.Base64;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpServerUpgradeHandler;
import io.netty.util.CharsetUtil;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* Server-side codec for performing a cleartext upgrade from HTTP/1.x to HTTP/2.
*/
public class Http2ServerUpgradeCodec implements HttpServerUpgradeHandler.UpgradeCodec {
private static final List<String> REQUIRED_UPGRADE_HEADERS =
Collections.singletonList(HTTP_UPGRADE_SETTINGS_HEADER);
private final String handlerName;
private final Http2ConnectionHandler connectionHandler;
private final Http2FrameReader frameReader;
/**
* Creates the codec using a default name for the connection handler when adding to the
* pipeline.
*
* @param connectionHandler the HTTP/2 connection handler.
*/
public Http2ServerUpgradeCodec(Http2ConnectionHandler connectionHandler) {
this("http2ConnectionHandler", connectionHandler);
}
/**
* Creates the codec providing an upgrade to the given handler for HTTP/2.
*
* @param handlerName the name of the HTTP/2 connection handler to be used in the pipeline.
* @param connectionHandler the HTTP/2 connection handler.
*/
public Http2ServerUpgradeCodec(String handlerName, Http2ConnectionHandler connectionHandler) {
this.handlerName = checkNotNull(handlerName, "handlerName");
this.connectionHandler = checkNotNull(connectionHandler, "connectionHandler");
frameReader = new DefaultHttp2FrameReader();
}
@Override
public String protocol() {
return HTTP_UPGRADE_PROTOCOL_NAME;
}
@Override
public Collection<String> requiredUpgradeHeaders() {
return REQUIRED_UPGRADE_HEADERS;
}
@Override
public void prepareUpgradeResponse(ChannelHandlerContext ctx, FullHttpRequest upgradeRequest,
FullHttpResponse upgradeResponse) {
try {
// Decode the HTTP2-Settings header and set the settings on the handler to make
// sure everything is fine with the request.
List<String> upgradeHeaders = upgradeRequest.headers().getAll(HTTP_UPGRADE_SETTINGS_HEADER);
if (upgradeHeaders.isEmpty() || upgradeHeaders.size() > 1) {
throw new IllegalArgumentException("There must be 1 and only 1 "
+ HTTP_UPGRADE_SETTINGS_HEADER + " header.");
}
Http2Settings settings = decodeSettingsHeader(ctx, upgradeHeaders.get(0));
connectionHandler.onHttpServerUpgrade(settings);
// Everything looks good, no need to modify the response.
} catch (Throwable e) {
// Send a failed response back to the client.
upgradeResponse.setStatus(BAD_REQUEST);
upgradeResponse.headers().clear();
}
}
@Override
public void upgradeTo(final ChannelHandlerContext ctx, FullHttpRequest upgradeRequest,
FullHttpResponse upgradeResponse) {
// Add the HTTP/2 connection handler to the pipeline immediately following the current
// handler.
ctx.pipeline().addAfter(ctx.name(), handlerName, connectionHandler);
}
/**
* Decodes the settings header and returns a {@link Http2Settings} object.
*/
private Http2Settings decodeSettingsHeader(ChannelHandlerContext ctx, CharSequence settingsHeader)
throws Http2Exception {
ByteBuf header = Unpooled.wrappedBuffer(AsciiString.getBytes(settingsHeader, CharsetUtil.UTF_8));
try {
// Decode the SETTINGS payload.
ByteBuf payload = Base64.decode(header, URL_SAFE);
// Create an HTTP/2 frame for the settings.
ByteBuf frame = createSettingsFrame(ctx, payload);
// Decode the SETTINGS frame and return the settings object.
return decodeSettings(ctx, frame);
} finally {
header.release();
}
}
/**
* Decodes the settings frame and returns the settings.
*/
private Http2Settings decodeSettings(ChannelHandlerContext ctx, ByteBuf frame) throws Http2Exception {
try {
final Http2Settings decodedSettings = new Http2Settings();
frameReader.readFrame(ctx, frame, new Http2FrameAdapter() {
@Override
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) {
decodedSettings.copyFrom(settings);
}
});
return decodedSettings;
} finally {
frame.release();
}
}
/**
* Creates an HTTP2-Settings header with the given payload. The payload buffer is released.
*/
private static ByteBuf createSettingsFrame(ChannelHandlerContext ctx, ByteBuf payload) {
ByteBuf frame = ctx.alloc().buffer(FRAME_HEADER_LENGTH + payload.readableBytes());
writeFrameHeader(frame, payload.readableBytes(), SETTINGS, new Http2Flags(), 0);
frame.writeBytes(payload);
payload.release();
return frame;
}
}

View File

@ -0,0 +1,247 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_CONCURRENT_STREAMS;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_HEADER_TABLE_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_INITIAL_WINDOW_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_HEADER_LIST_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_HEADER_TABLE_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_CONCURRENT_STREAMS;
import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_INITIAL_WINDOW_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_HEADER_LIST_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTINGS_ENABLE_PUSH;
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTINGS_HEADER_TABLE_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTINGS_INITIAL_WINDOW_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTINGS_MAX_CONCURRENT_STREAMS;
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTINGS_MAX_FRAME_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.SETTINGS_MAX_HEADER_LIST_SIZE;
import static io.netty.handler.codec.http2.Http2CodecUtil.isMaxFrameSizeValid;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.util.collection.IntObjectHashMap;
/**
* Settings for one endpoint in an HTTP/2 connection. Each of the values are optional as defined in
* the spec for the SETTINGS frame. Permits storage of arbitrary key/value pairs but provides helper
* methods for standard settings.
*/
public final class Http2Settings extends IntObjectHashMap<Long> {
public Http2Settings() {
this(6 /* number of standard settings */);
}
public Http2Settings(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
}
public Http2Settings(int initialCapacity) {
super(initialCapacity);
}
/**
* Overrides the superclass method to perform verification of standard HTTP/2 settings.
*
* @throws IllegalArgumentException if verification of the setting fails.
*/
@Override
public Long put(int key, Long value) {
verifyStandardSetting(key, value);
return super.put(key, value);
}
/**
* Gets the {@code SETTINGS_HEADER_TABLE_SIZE} value. If unavailable, returns {@code null}.
*/
public Long headerTableSize() {
return get(SETTINGS_HEADER_TABLE_SIZE);
}
/**
* Sets the {@code SETTINGS_HEADER_TABLE_SIZE} value.
*
* @throws IllegalArgumentException if verification of the setting fails.
*/
public Http2Settings headerTableSize(int value) {
put(SETTINGS_HEADER_TABLE_SIZE, (long) value);
return this;
}
/**
* Gets the {@code SETTINGS_ENABLE_PUSH} value. If unavailable, returns {@code null}.
*/
public Boolean pushEnabled() {
Long value = get(SETTINGS_ENABLE_PUSH);
if (value == null) {
return null;
}
return value != 0L;
}
/**
* Sets the {@code SETTINGS_ENABLE_PUSH} value.
*/
public Http2Settings pushEnabled(boolean enabled) {
put(SETTINGS_ENABLE_PUSH, enabled ? 1L : 0L);
return this;
}
/**
* Gets the {@code SETTINGS_MAX_CONCURRENT_STREAMS} value. If unavailable, returns {@code null}.
*/
public Long maxConcurrentStreams() {
return get(SETTINGS_MAX_CONCURRENT_STREAMS);
}
/**
* Sets the {@code SETTINGS_MAX_CONCURRENT_STREAMS} value.
*
* @throws IllegalArgumentException if verification of the setting fails.
*/
public Http2Settings maxConcurrentStreams(long value) {
put(SETTINGS_MAX_CONCURRENT_STREAMS, value);
return this;
}
/**
* Gets the {@code SETTINGS_INITIAL_WINDOW_SIZE} value. If unavailable, returns {@code null}.
*/
public Integer initialWindowSize() {
return getIntValue(SETTINGS_INITIAL_WINDOW_SIZE);
}
/**
* Sets the {@code SETTINGS_INITIAL_WINDOW_SIZE} value.
*
* @throws IllegalArgumentException if verification of the setting fails.
*/
public Http2Settings initialWindowSize(int value) {
put(SETTINGS_INITIAL_WINDOW_SIZE, (long) value);
return this;
}
/**
* Gets the {@code SETTINGS_MAX_FRAME_SIZE} value. If unavailable, returns {@code null}.
*/
public Integer maxFrameSize() {
return getIntValue(SETTINGS_MAX_FRAME_SIZE);
}
/**
* Sets the {@code SETTINGS_MAX_FRAME_SIZE} value.
*
* @throws IllegalArgumentException if verification of the setting fails.
*/
public Http2Settings maxFrameSize(int value) {
put(SETTINGS_MAX_FRAME_SIZE, (long) value);
return this;
}
/**
* Gets the {@code SETTINGS_MAX_HEADER_LIST_SIZE} value. If unavailable, returns {@code null}.
*/
public Integer maxHeaderListSize() {
return getIntValue(SETTINGS_MAX_HEADER_LIST_SIZE);
}
/**
* Sets the {@code SETTINGS_MAX_HEADER_LIST_SIZE} value.
*
* @throws IllegalArgumentException if verification of the setting fails.
*/
public Http2Settings maxHeaderListSize(int value) {
put(SETTINGS_MAX_HEADER_LIST_SIZE, (long) value);
return this;
}
/**
* Clears and then copies the given settings into this object.
*/
public Http2Settings copyFrom(Http2Settings settings) {
clear();
putAll(settings);
return this;
}
Integer getIntValue(int key) {
Long value = get(key);
if (value == null) {
return null;
}
return value.intValue();
}
private static void verifyStandardSetting(int key, Long value) {
checkNotNull(value, "value");
switch (key) {
case SETTINGS_HEADER_TABLE_SIZE:
if (value < MIN_HEADER_TABLE_SIZE || value > MAX_HEADER_TABLE_SIZE) {
throw new IllegalArgumentException("Setting HEADER_TABLE_SIZE is invalid: " + value);
}
break;
case SETTINGS_ENABLE_PUSH:
if (value != 0L && value != 1L) {
throw new IllegalArgumentException("Setting ENABLE_PUSH is invalid: " + value);
}
break;
case SETTINGS_MAX_CONCURRENT_STREAMS:
if (value < MIN_CONCURRENT_STREAMS || value > MAX_CONCURRENT_STREAMS) {
throw new IllegalArgumentException(
"Setting MAX_CONCURRENT_STREAMS is invalid: " + value);
}
break;
case SETTINGS_INITIAL_WINDOW_SIZE:
if (value < MIN_INITIAL_WINDOW_SIZE || value > MAX_INITIAL_WINDOW_SIZE) {
throw new IllegalArgumentException("Setting INITIAL_WINDOW_SIZE is invalid: "
+ value);
}
break;
case SETTINGS_MAX_FRAME_SIZE:
if (!isMaxFrameSizeValid(value.intValue())) {
throw new IllegalArgumentException("Setting MAX_FRAME_SIZE is invalid: " + value);
}
break;
case SETTINGS_MAX_HEADER_LIST_SIZE:
if (value < MIN_HEADER_LIST_SIZE || value > MAX_HEADER_LIST_SIZE) {
throw new IllegalArgumentException("Setting MAX_HEADER_LIST_SIZE is invalid: " + value);
}
break;
default:
throw new IllegalArgumentException("key");
}
}
@Override
protected String keyToString(int key) {
switch (key) {
case SETTINGS_HEADER_TABLE_SIZE:
return "HEADER_TABLE_SIZE";
case SETTINGS_ENABLE_PUSH:
return "ENABLE_PUSH";
case SETTINGS_MAX_CONCURRENT_STREAMS:
return "MAX_CONCURRENT_STREAMS";
case SETTINGS_INITIAL_WINDOW_SIZE:
return "INITIAL_WINDOW_SIZE";
case SETTINGS_MAX_FRAME_SIZE:
return "MAX_FRAME_SIZE";
case SETTINGS_MAX_HEADER_LIST_SIZE:
return "MAX_HEADER_LIST_SIZE";
default:
// Unknown keys.
return super.keyToString(key);
}
}
}

View File

@ -0,0 +1,226 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import java.util.Collection;
/**
* A single stream within an HTTP2 connection. Streams are compared to each other by priority.
*/
public interface Http2Stream {
/**
* The allowed states of an HTTP2 stream.
*/
enum State {
IDLE,
RESERVED_LOCAL,
RESERVED_REMOTE,
OPEN,
HALF_CLOSED_LOCAL,
HALF_CLOSED_REMOTE,
CLOSED
}
/**
* Gets the unique identifier for this stream within the connection.
*/
int id();
/**
* Gets the state of this stream.
*/
State state();
/**
* Add this stream to {@link Http2Connection#activeStreams()} and transition state to:
* <ul>
* <li>{@link State#OPEN} if {@link #state()} is {@link State#IDLE} and {@code halfClosed} is {@code false}.</li>
* <li>{@link State#HALF_CLOSED_LOCAL} if {@link #state()} is {@link State#IDLE} and {@code halfClosed}
* is {@code true} and the stream is local.</li>
* <li>{@link State#HALF_CLOSED_REMOTE} if {@link #state()} is {@link State#IDLE} and {@code halfClosed}
* is {@code true} and the stream is remote.</li>
* <li>{@link State#RESERVED_LOCAL} if {@link #state()} is {@link State#HALF_CLOSED_REMOTE}.</li>
* <li>{@link State#RESERVED_REMOTE} if {@link #state()} is {@link State#HALF_CLOSED_LOCAL}.</li>
* </ul>
*/
Http2Stream open(boolean halfClosed) throws Http2Exception;
/**
* Closes the stream.
*/
Http2Stream close();
/**
* Closes the local side of this stream. If this makes the stream closed, the child is closed as
* well.
*/
Http2Stream closeLocalSide();
/**
* Closes the remote side of this stream. If this makes the stream closed, the child is closed
* as well.
*/
Http2Stream closeRemoteSide();
/**
* Indicates whether a frame with {@code END_STREAM} set was received from the remote endpoint
* for this stream.
*/
boolean isEndOfStreamReceived();
/**
* Sets the flag indicating that a frame with {@code END_STREAM} set was received from the
* remote endpoint for this stream.
*/
Http2Stream endOfStreamReceived();
/**
* Indicates whether a frame with {@code END_STREAM} set was sent to the remote endpoint for
* this stream.
*/
boolean isEndOfStreamSent();
/**
* Sets the flag indicating that a frame with {@code END_STREAM} set was sent to the remote
* endpoint for this stream.
*/
Http2Stream endOfStreamSent();
/**
* Indicates whether a {@code RST_STREAM} frame has been received from the remote endpoint for this stream.
*/
boolean isResetReceived();
/**
* Sets the flag indicating that a {@code RST_STREAM} frame has been received from the remote endpoint
* for this stream. This does not affect the stream state.
*/
Http2Stream resetReceived();
/**
* Indicates whether a {@code RST_STREAM} frame has been sent from the local endpoint for this stream.
*/
boolean isResetSent();
/**
* Sets the flag indicating that a {@code RST_STREAM} frame has been sent from the local endpoint
* for this stream. This does not affect the stream state.
*/
Http2Stream resetSent();
/**
* Indicates whether or not this stream has been reset. This is a short form for
* {@link #isResetSent()} || {@link #isResetReceived()}.
*/
boolean isReset();
/**
* Indicates whether the remote side of this stream is open (i.e. the state is either
* {@link State#OPEN} or {@link State#HALF_CLOSED_LOCAL}).
*/
boolean remoteSideOpen();
/**
* Indicates whether the local side of this stream is open (i.e. the state is either
* {@link State#OPEN} or {@link State#HALF_CLOSED_REMOTE}).
*/
boolean localSideOpen();
/**
* Associates the application-defined data with this stream.
* @return The value that was previously associated with {@code key}, or {@code null} if there was none.
*/
Object setProperty(Object key, Object value);
/**
* Returns application-defined data if any was associated with this stream.
*/
<V> V getProperty(Object key);
/**
* Returns and removes application-defined data if any was associated with this stream.
*/
<V> V removeProperty(Object key);
/**
* Updates an priority for this stream. Calling this method may affect the straucture of the
* priority tree.
*
* @param parentStreamId the parent stream that given stream should depend on. May be {@code 0},
* if the stream has no dependencies and should be an immediate child of the
* connection.
* @param weight the weight to be assigned to this stream relative to its parent. This value
* must be between 1 and 256 (inclusive)
* @param exclusive indicates that the stream should be the exclusive dependent on its parent.
* This only applies if the stream has a parent.
* @return this stream.
*/
Http2Stream setPriority(int parentStreamId, short weight, boolean exclusive) throws Http2Exception;
/**
* Indicates whether or not this stream is the root node of the priority tree.
*/
boolean isRoot();
/**
* Indicates whether or not this is a leaf node (i.e. {@link #numChildren} is 0) of the priority tree.
*/
boolean isLeaf();
/**
* Returns weight assigned to the dependency with the parent. The weight will be a value
* between 1 and 256.
*/
short weight();
/**
* The total of the weights of all children of this stream.
*/
int totalChildWeights();
/**
* The parent (i.e. the node in the priority tree on which this node depends), or {@code null}
* if this is the root node (i.e. the connection, itself).
*/
Http2Stream parent();
/**
* Indicates whether or not this stream is a descendant in the priority tree from the given stream.
*/
boolean isDescendantOf(Http2Stream stream);
/**
* Returns the number of child streams directly dependent on this stream.
*/
int numChildren();
/**
* Indicates whether the given stream is a direct child of this stream.
*/
boolean hasChild(int streamId);
/**
* Attempts to find a child of this stream with the given ID. If not found, returns
* {@code null}.
*/
Http2Stream child(int streamId);
/**
* Gets all streams that are direct dependents on this stream.
*/
Collection<? extends Http2Stream> children();
}

View File

@ -0,0 +1,42 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
/**
* A policy for determining when it is appropriate to remove streams from an HTTP/2 stream registry.
*/
public interface Http2StreamRemovalPolicy {
/**
* Performs the action of removing the stream.
*/
interface Action {
/**
* Removes the stream from the registry.
*/
void removeStream(Http2Stream stream);
}
/**
* Sets the removal action.
*/
void setAction(Action action);
/**
* Marks the given stream for removal. When this policy has determined that the given stream
* should be removed, it will call back the {@link Action}.
*/
void markForRemoval(Http2Stream stream);
}

View File

@ -0,0 +1,98 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.ChannelPromiseAggregator;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.HttpHeaders;
/**
* Translates HTTP/1.x object writes into HTTP/2 frames.
* <p>
* See {@link InboundHttp2ToHttpAdapter} to get translation from HTTP/2 frames to HTTP/1.x objects.
*/
public class HttpToHttp2ConnectionHandler extends Http2ConnectionHandler {
public HttpToHttp2ConnectionHandler(boolean server, Http2FrameListener listener) {
super(server, listener);
}
public HttpToHttp2ConnectionHandler(Http2Connection connection, Http2FrameListener listener) {
super(connection, listener);
}
public HttpToHttp2ConnectionHandler(Http2Connection connection, Http2FrameReader frameReader,
Http2FrameWriter frameWriter, Http2FrameListener listener) {
super(connection, frameReader, frameWriter, listener);
}
public HttpToHttp2ConnectionHandler(Http2ConnectionDecoder.Builder decoderBuilder,
Http2ConnectionEncoder.Builder encoderBuilder) {
super(decoderBuilder, encoderBuilder);
}
/**
* Get the next stream id either from the {@link HttpHeaders} object or HTTP/2 codec
*
* @param httpHeaders The HTTP/1.x headers object to look for the stream id
* @return The stream id to use with this {@link HttpHeaders} object
* @throws Exception If the {@code httpHeaders} object specifies an invalid stream id
*/
private int getStreamId(HttpHeaders httpHeaders) throws Exception {
return httpHeaders.getInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), connection().local().nextStreamId());
}
/**
* Handles conversion of a {@link FullHttpMessage} to HTTP/2 frames.
*/
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
if (msg instanceof FullHttpMessage) {
FullHttpMessage httpMsg = (FullHttpMessage) msg;
boolean hasData = httpMsg.content().isReadable();
boolean httpMsgNeedRelease = true;
try {
// Provide the user the opportunity to specify the streamId
int streamId = getStreamId(httpMsg.headers());
// Convert and write the headers.
Http2Headers http2Headers = HttpUtil.toHttp2Headers(httpMsg);
Http2ConnectionEncoder encoder = encoder();
if (hasData) {
ChannelPromiseAggregator promiseAggregator = new ChannelPromiseAggregator(promise);
ChannelPromise headerPromise = ctx.newPromise();
ChannelPromise dataPromise = ctx.newPromise();
promiseAggregator.add(headerPromise, dataPromise);
encoder.writeHeaders(ctx, streamId, http2Headers, 0, false, headerPromise);
httpMsgNeedRelease = false;
encoder.writeData(ctx, streamId, httpMsg.content(), 0, true, dataPromise);
} else {
encoder.writeHeaders(ctx, streamId, http2Headers, 0, true, promise);
}
} catch (Throwable t) {
promise.tryFailure(t);
} finally {
if (httpMsgNeedRelease) {
httpMsg.release();
}
}
} else {
ctx.write(msg, promise);
}
}
}

View File

@ -0,0 +1,386 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import static io.netty.handler.codec.http2.Http2Exception.streamError;
import static io.netty.util.internal.ObjectUtil.checkNotNull;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.BinaryHeaders;
import io.netty.handler.codec.TextHeaders.EntryVisitor;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import java.net.URI;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;
/**
* Provides utility methods and constants for the HTTP/2 to HTTP conversion
*/
public final class HttpUtil {
/**
* The set of headers that should not be directly copied when converting headers from HTTP to HTTP/2.
*/
@SuppressWarnings("deprecation")
private static final Set<CharSequence> HTTP_TO_HTTP2_HEADER_BLACKLIST = new HashSet<CharSequence>() {
private static final long serialVersionUID = -5678614530214167043L;
{
add(HttpHeaderNames.CONNECTION);
add(HttpHeaderNames.KEEP_ALIVE);
add(HttpHeaderNames.PROXY_CONNECTION);
add(HttpHeaderNames.TRANSFER_ENCODING);
add(HttpHeaderNames.HOST);
add(HttpHeaderNames.UPGRADE);
add(ExtensionHeaderNames.STREAM_ID.text());
add(ExtensionHeaderNames.AUTHORITY.text());
add(ExtensionHeaderNames.SCHEME.text());
add(ExtensionHeaderNames.PATH.text());
}
};
/**
* This will be the method used for {@link HttpRequest} objects generated out of the HTTP message flow defined in <a
* href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a>
*/
public static final HttpMethod OUT_OF_MESSAGE_SEQUENCE_METHOD = HttpMethod.OPTIONS;
/**
* This will be the path used for {@link HttpRequest} objects generated out of the HTTP message flow defined in <a
* href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a>
*/
public static final String OUT_OF_MESSAGE_SEQUENCE_PATH = "";
/**
* This will be the status code used for {@link HttpResponse} objects generated out of the HTTP message flow defined
* in <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a>
*/
public static final HttpResponseStatus OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE = HttpResponseStatus.OK;
/**
* This pattern will use to avoid compile it each time it is used
* when we need to replace some part of authority.
*/
private static final Pattern AUTHORITY_REPLACEMENT_PATTERN = Pattern.compile("^.*@");
private HttpUtil() {
}
/**
* Provides the HTTP header extensions used to carry HTTP/2 information in HTTP objects
*/
public enum ExtensionHeaderNames {
/**
* HTTP extension header which will identify the stream id from the HTTP/2 event(s) responsible for generating a
* {@code HttpObject}
* <p>
* {@code "x-http2-stream-id"}
*/
STREAM_ID("x-http2-stream-id"),
/**
* HTTP extension header which will identify the authority pseudo header from the HTTP/2 event(s) responsible
* for generating a {@code HttpObject}
* <p>
* {@code "x-http2-authority"}
*/
AUTHORITY("x-http2-authority"),
/**
* HTTP extension header which will identify the scheme pseudo header from the HTTP/2 event(s) responsible for
* generating a {@code HttpObject}
* <p>
* {@code "x-http2-scheme"}
*/
SCHEME("x-http2-scheme"),
/**
* HTTP extension header which will identify the path pseudo header from the HTTP/2 event(s) responsible for
* generating a {@code HttpObject}
* <p>
* {@code "x-http2-path"}
*/
PATH("x-http2-path"),
/**
* HTTP extension header which will identify the stream id used to create this stream in a HTTP/2 push promise
* frame
* <p>
* {@code "x-http2-stream-promise-id"}
*/
STREAM_PROMISE_ID("x-http2-stream-promise-id"),
/**
* HTTP extension header which will identify the stream id which this stream is dependent on. This stream will
* be a child node of the stream id associated with this header value.
* <p>
* {@code "x-http2-stream-dependency-id"}
*/
STREAM_DEPENDENCY_ID("x-http2-stream-dependency-id"),
/**
* HTTP extension header which will identify the weight (if non-default and the priority is not on the default
* stream) of the associated HTTP/2 stream responsible responsible for generating a {@code HttpObject}
* <p>
* {@code "x-http2-stream-weight"}
*/
STREAM_WEIGHT("x-http2-stream-weight");
private final AsciiString text;
ExtensionHeaderNames(String text) {
this.text = new AsciiString(text);
}
public AsciiString text() {
return text;
}
}
/**
* Apply HTTP/2 rules while translating status code to {@link HttpResponseStatus}
*
* @param status The status from an HTTP/2 frame
* @return The HTTP/1.x status
* @throws Http2Exception If there is a problem translating from HTTP/2 to HTTP/1.x
*/
public static HttpResponseStatus parseStatus(AsciiString status) throws Http2Exception {
HttpResponseStatus result;
try {
result = HttpResponseStatus.parseLine(status);
if (result == HttpResponseStatus.SWITCHING_PROTOCOLS) {
throw connectionError(PROTOCOL_ERROR, "Invalid HTTP/2 status code '%d'", result.code());
}
} catch (Http2Exception e) {
throw e;
} catch (Throwable t) {
throw connectionError(PROTOCOL_ERROR, t,
"Unrecognized HTTP status code '%s' encountered in translation to HTTP/1.x", status);
}
return result;
}
/**
* Create a new object to contain the response data
*
* @param streamId The stream associated with the response
* @param http2Headers The initial set of HTTP/2 headers to create the response with
* @param validateHttpHeaders <ul>
* <li>{@code true} to validate HTTP headers in the http-codec</li>
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
* </ul>
* @return A new response object which represents headers/data
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
*/
public static FullHttpResponse toHttpResponse(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
throws Http2Exception {
HttpResponseStatus status = parseStatus(http2Headers.status());
// HTTP/2 does not define a way to carry the version or reason phrase that is included in an
// HTTP/1.1 status line.
FullHttpResponse msg = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, validateHttpHeaders);
addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
return msg;
}
/**
* Create a new object to contain the request data
*
* @param streamId The stream associated with the request
* @param http2Headers The initial set of HTTP/2 headers to create the request with
* @param validateHttpHeaders <ul>
* <li>{@code true} to validate HTTP headers in the http-codec</li>
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
* </ul>
* @return A new request object which represents headers/data
* @throws Http2Exception see {@link #addHttp2ToHttpHeaders(int, Http2Headers, FullHttpMessage, boolean)}
*/
public static FullHttpRequest toHttpRequest(int streamId, Http2Headers http2Headers, boolean validateHttpHeaders)
throws Http2Exception {
// HTTP/2 does not define a way to carry the version identifier that is
// included in the HTTP/1.1 request line.
final AsciiString method = checkNotNull(http2Headers.method(),
"method header cannot be null in conversion to HTTP/1.x");
final AsciiString path = checkNotNull(http2Headers.path(),
"path header cannot be null in conversion to HTTP/1.x");
FullHttpRequest msg = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.valueOf(method
.toString()), path.toString(), validateHttpHeaders);
addHttp2ToHttpHeaders(streamId, http2Headers, msg, false);
return msg;
}
/**
* Translate and add HTTP/2 headers to HTTP/1.x headers
*
* @param streamId The stream associated with {@code sourceHeaders}
* @param sourceHeaders The HTTP/2 headers to convert
* @param destinationMessage The object which will contain the resulting HTTP/1.x headers
* @param addToTrailer {@code true} to add to trailing headers. {@code false} to add to initial headers.
* @throws Http2Exception If not all HTTP/2 headers can be translated to HTTP/1.x
*/
public static void addHttp2ToHttpHeaders(int streamId, Http2Headers sourceHeaders,
FullHttpMessage destinationMessage, boolean addToTrailer) throws Http2Exception {
HttpHeaders headers = addToTrailer ? destinationMessage.trailingHeaders() : destinationMessage.headers();
boolean request = destinationMessage instanceof HttpRequest;
Http2ToHttpHeaderTranslator visitor = new Http2ToHttpHeaderTranslator(streamId, headers, request);
try {
sourceHeaders.forEachEntry(visitor);
} catch (Http2Exception ex) {
throw ex;
} catch (Throwable t) {
throw streamError(streamId, PROTOCOL_ERROR, t, "HTTP/2 to HTTP/1.x headers conversion error");
}
headers.remove(HttpHeaderNames.TRANSFER_ENCODING);
headers.remove(HttpHeaderNames.TRAILER);
if (!addToTrailer) {
headers.setInt(ExtensionHeaderNames.STREAM_ID.text(), streamId);
HttpHeaderUtil.setKeepAlive(destinationMessage, true);
}
}
/**
* Converts the given HTTP/1.x headers into HTTP/2 headers.
*/
public static Http2Headers toHttp2Headers(FullHttpMessage in) throws Exception {
final Http2Headers out = new DefaultHttp2Headers();
HttpHeaders inHeaders = in.headers();
if (in instanceof HttpRequest) {
HttpRequest request = (HttpRequest) in;
out.path(new AsciiString(request.uri()));
out.method(new AsciiString(request.method().toString()));
String value = inHeaders.get(HttpHeaderNames.HOST);
if (value != null) {
URI hostUri = URI.create(value);
// The authority MUST NOT include the deprecated "userinfo" subcomponent
value = hostUri.getAuthority();
if (value != null) {
out.authority(new AsciiString(AUTHORITY_REPLACEMENT_PATTERN.matcher(value).replaceFirst("")));
}
value = hostUri.getScheme();
if (value != null) {
out.scheme(new AsciiString(value));
}
}
// Consume the Authority extension header if present
CharSequence cValue = inHeaders.get(ExtensionHeaderNames.AUTHORITY.text());
if (cValue != null) {
out.authority(AsciiString.of(cValue));
}
// Consume the Scheme extension header if present
cValue = inHeaders.get(ExtensionHeaderNames.SCHEME.text());
if (cValue != null) {
out.scheme(AsciiString.of(cValue));
}
} else if (in instanceof HttpResponse) {
HttpResponse response = (HttpResponse) in;
out.status(new AsciiString(Integer.toString(response.status().code())));
}
// Add the HTTP headers which have not been consumed above
inHeaders.forEachEntry(new EntryVisitor() {
@Override
public boolean visit(Entry<CharSequence, CharSequence> entry) throws Exception {
final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase();
if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName)) {
AsciiString aValue = AsciiString.of(entry.getValue());
// https://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.2
// makes a special exception for TE
if (!aName.equalsIgnoreCase(HttpHeaderNames.TE) ||
aValue.equalsIgnoreCase(HttpHeaderValues.TRAILERS)) {
out.add(aName, aValue);
}
}
return true;
}
});
return out;
}
/**
* A visitor which translates HTTP/2 headers to HTTP/1 headers
*/
private static final class Http2ToHttpHeaderTranslator implements BinaryHeaders.EntryVisitor {
/**
* Translations from HTTP/2 header name to the HTTP/1.x equivalent.
*/
private static final Map<AsciiString, AsciiString>
REQUEST_HEADER_TRANSLATIONS = new HashMap<AsciiString, AsciiString>();
private static final Map<AsciiString, AsciiString>
RESPONSE_HEADER_TRANSLATIONS = new HashMap<AsciiString, AsciiString>();
static {
RESPONSE_HEADER_TRANSLATIONS.put(Http2Headers.PseudoHeaderName.AUTHORITY.value(),
ExtensionHeaderNames.AUTHORITY.text());
RESPONSE_HEADER_TRANSLATIONS.put(Http2Headers.PseudoHeaderName.SCHEME.value(),
ExtensionHeaderNames.SCHEME.text());
REQUEST_HEADER_TRANSLATIONS.putAll(RESPONSE_HEADER_TRANSLATIONS);
RESPONSE_HEADER_TRANSLATIONS.put(Http2Headers.PseudoHeaderName.PATH.value(),
ExtensionHeaderNames.PATH.text());
}
private final int streamId;
private final HttpHeaders output;
private final Map<AsciiString, AsciiString> translations;
/**
* Create a new instance
*
* @param output The HTTP/1.x headers object to store the results of the translation
* @param request if {@code true}, translates headers using the request translation map. Otherwise uses the
* response translation map.
*/
Http2ToHttpHeaderTranslator(int streamId, HttpHeaders output, boolean request) {
this.streamId = streamId;
this.output = output;
translations = request ? REQUEST_HEADER_TRANSLATIONS : RESPONSE_HEADER_TRANSLATIONS;
}
@Override
public boolean visit(Entry<AsciiString, AsciiString> entry) throws Http2Exception {
final AsciiString name = entry.getKey();
final AsciiString value = entry.getValue();
AsciiString translatedName = translations.get(name);
if (translatedName != null || !Http2Headers.PseudoHeaderName.isPseudoHeader(name)) {
if (translatedName == null) {
translatedName = name;
}
// http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.2.3
// All headers that start with ':' are only valid in HTTP/2 context
if (translatedName.isEmpty() || translatedName.charAt(0) == ':') {
throw streamError(streamId, PROTOCOL_ERROR,
"Invalid HTTP/2 header '%s' encountered in translation to HTTP/1.x", translatedName);
} else {
output.add(translatedName, value);
}
}
return true;
}
}
}

View File

@ -0,0 +1,362 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Error.INTERNAL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.TooLongFrameException;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpStatusClass;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.collection.IntObjectMap;
import static io.netty.util.internal.ObjectUtil.*;
/**
* This adapter provides just header/data events from the HTTP message flow defined
* here <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-8.1.">HTTP/2 Spec Message Flow</a>.
* <p>
* See {@link HttpToHttp2ConnectionHandler} to get translation from HTTP/1.x objects to HTTP/2 frames for writes.
*/
public class InboundHttp2ToHttpAdapter extends Http2EventAdapter {
private static final ImmediateSendDetector DEFAULT_SEND_DETECTOR = new ImmediateSendDetector() {
@Override
public boolean mustSendImmediately(FullHttpMessage msg) {
if (msg instanceof FullHttpResponse) {
return ((FullHttpResponse) msg).status().codeClass() == HttpStatusClass.INFORMATIONAL;
}
if (msg instanceof FullHttpRequest) {
return msg.headers().contains(HttpHeaderNames.EXPECT);
}
return false;
}
@Override
public FullHttpMessage copyIfNeeded(FullHttpMessage msg) {
if (msg instanceof FullHttpRequest) {
FullHttpRequest copy = ((FullHttpRequest) msg).copy(null);
copy.headers().remove(HttpHeaderNames.EXPECT);
return copy;
}
return null;
}
};
private final int maxContentLength;
protected final Http2Connection connection;
protected final boolean validateHttpHeaders;
private final ImmediateSendDetector sendDetector;
protected final IntObjectMap<FullHttpMessage> messageMap;
private final boolean propagateSettings;
public static class Builder {
private final Http2Connection connection;
private int maxContentLength;
private boolean validateHttpHeaders;
private boolean propagateSettings;
/**
* Creates a new {@link InboundHttp2ToHttpAdapter} builder for the specified {@link Http2Connection}.
*
* @param connection The object which will provide connection notification events for the current connection
*/
public Builder(Http2Connection connection) {
this.connection = connection;
}
/**
* Specifies the maximum length of the message content.
*
* @param maxContentLength the maximum length of the message content. If the length of the message content
* exceeds this value, a {@link TooLongFrameException} will be raised
* @return {@link Builder} the builder for the {@link InboundHttp2ToHttpAdapter}
*/
public Builder maxContentLength(int maxContentLength) {
this.maxContentLength = maxContentLength;
return this;
}
/**
* Specifies whether validation of HTTP headers should be performed.
*
* @param validate
* <ul>
* <li>{@code true} to validate HTTP headers in the http-codec</li>
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
* </ul>
* @return {@link Builder} the builder for the {@link InboundHttp2ToHttpAdapter}
*/
public Builder validateHttpHeaders(boolean validate) {
validateHttpHeaders = validate;
return this;
}
/**
* Specifies whether a read settings frame should be propagated alone the channel pipeline.
*
* @param propagate if {@code true} read settings will be passed along the pipeline. This can be useful
* to clients that need hold off sending data until they have received the settings.
* @return {@link Builder} the builder for the {@link InboundHttp2ToHttpAdapter}
*/
public Builder propagateSettings(boolean propagate) {
propagateSettings = propagate;
return this;
}
/**
* Builds/creates a new {@link InboundHttp2ToHttpAdapter} instance using this builders current settings.
*/
public InboundHttp2ToHttpAdapter build() {
InboundHttp2ToHttpAdapter instance = new InboundHttp2ToHttpAdapter(this);
connection.addListener(instance);
return instance;
}
}
protected InboundHttp2ToHttpAdapter(Builder builder) {
checkNotNull(builder.connection, "connection");
if (builder.maxContentLength <= 0) {
throw new IllegalArgumentException("maxContentLength must be a positive integer: "
+ builder.maxContentLength);
}
connection = builder.connection;
maxContentLength = builder.maxContentLength;
validateHttpHeaders = builder.validateHttpHeaders;
propagateSettings = builder.propagateSettings;
sendDetector = DEFAULT_SEND_DETECTOR;
messageMap = new IntObjectHashMap<FullHttpMessage>();
}
/**
* The streamId is out of scope for the HTTP message flow and will no longer be tracked
* @param streamId The stream id to remove associated state with
*/
protected void removeMessage(int streamId) {
messageMap.remove(streamId);
}
@Override
public void streamRemoved(Http2Stream stream) {
removeMessage(stream.id());
}
/**
* Set final headers and fire a channel read event
*
* @param ctx The context to fire the event on
* @param msg The message to send
* @param streamId the streamId of the message which is being fired
*/
protected void fireChannelRead(ChannelHandlerContext ctx, FullHttpMessage msg, int streamId) {
removeMessage(streamId);
HttpHeaderUtil.setContentLength(msg, msg.content().readableBytes());
ctx.fireChannelRead(msg);
}
/**
* Create a new {@link FullHttpMessage} based upon the current connection parameters
*
* @param streamId The stream id to create a message for
* @param headers The headers associated with {@code streamId}
* @param validateHttpHeaders
* <ul>
* <li>{@code true} to validate HTTP headers in the http-codec</li>
* <li>{@code false} not to validate HTTP headers in the http-codec</li>
* </ul>
* @throws Http2Exception
*/
protected FullHttpMessage newMessage(int streamId, Http2Headers headers, boolean validateHttpHeaders)
throws Http2Exception {
return connection.isServer() ? HttpUtil.toHttpRequest(streamId, headers,
validateHttpHeaders) : HttpUtil.toHttpResponse(streamId, headers, validateHttpHeaders);
}
/**
* Provides translation between HTTP/2 and HTTP header objects while ensuring the stream
* is in a valid state for additional headers.
*
* @param ctx The context for which this message has been received.
* Used to send informational header if detected.
* @param streamId The stream id the {@code headers} apply to
* @param headers The headers to process
* @param endOfStream {@code true} if the {@code streamId} has received the end of stream flag
* @param allowAppend
* <ul>
* <li>{@code true} if headers will be appended if the stream already exists.</li>
* <li>if {@code false} and the stream already exists this method returns {@code null}.</li>
* </ul>
* @param appendToTrailer
* <ul>
* <li>{@code true} if a message {@code streamId} already exists then the headers
* should be added to the trailing headers.</li>
* <li>{@code false} then appends will be done to the initial headers.</li>
* </ul>
* @return The object used to track the stream corresponding to {@code streamId}. {@code null} if
* {@code allowAppend} is {@code false} and the stream already exists.
* @throws Http2Exception If the stream id is not in the correct state to process the headers request
*/
protected FullHttpMessage processHeadersBegin(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
boolean endOfStream, boolean allowAppend, boolean appendToTrailer) throws Http2Exception {
FullHttpMessage msg = messageMap.get(streamId);
if (msg == null) {
msg = newMessage(streamId, headers, validateHttpHeaders);
} else if (allowAppend) {
try {
HttpUtil.addHttp2ToHttpHeaders(streamId, headers, msg, appendToTrailer);
} catch (Http2Exception e) {
removeMessage(streamId);
throw e;
}
} else {
msg = null;
}
if (sendDetector.mustSendImmediately(msg)) {
// Copy the message (if necessary) before sending. The content is not expected to be copied (or used) in
// this operation but just in case it is used do the copy before sending and the resource may be released
final FullHttpMessage copy = endOfStream ? null : sendDetector.copyIfNeeded(msg);
fireChannelRead(ctx, msg, streamId);
return copy;
}
return msg;
}
/**
* After HTTP/2 headers have been processed by {@link #processHeadersBegin} this method either
* sends the result up the pipeline or retains the message for future processing.
*
* @param ctx The context for which this message has been received
* @param streamId The stream id the {@code objAccumulator} corresponds to
* @param msg The object which represents all headers/data for corresponding to {@code streamId}
* @param endOfStream {@code true} if this is the last event for the stream
*/
private void processHeadersEnd(ChannelHandlerContext ctx, int streamId,
FullHttpMessage msg, boolean endOfStream) {
if (endOfStream) {
fireChannelRead(ctx, msg, streamId);
} else {
messageMap.put(streamId, msg);
}
}
@Override
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)
throws Http2Exception {
FullHttpMessage msg = messageMap.get(streamId);
if (msg == null) {
throw connectionError(PROTOCOL_ERROR, "Data Frame recieved for unknown stream id %d", streamId);
}
ByteBuf content = msg.content();
final int dataReadableBytes = data.readableBytes();
if (content.readableBytes() > maxContentLength - dataReadableBytes) {
throw connectionError(INTERNAL_ERROR,
"Content length exceeded max of %d for stream id %d", maxContentLength, streamId);
}
content.writeBytes(data, data.readerIndex(), dataReadableBytes);
if (endOfStream) {
fireChannelRead(ctx, msg, streamId);
}
// All bytes have been processed.
return dataReadableBytes + padding;
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endOfStream) throws Http2Exception {
FullHttpMessage msg = processHeadersBegin(ctx, streamId, headers, endOfStream, true, true);
if (msg != null) {
processHeadersEnd(ctx, streamId, msg, endOfStream);
}
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
short weight, boolean exclusive, int padding, boolean endOfStream) throws Http2Exception {
FullHttpMessage msg = processHeadersBegin(ctx, streamId, headers, endOfStream, true, true);
if (msg != null) {
processHeadersEnd(ctx, streamId, msg, endOfStream);
}
}
@Override
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception {
FullHttpMessage msg = messageMap.get(streamId);
if (msg != null) {
fireChannelRead(ctx, msg, streamId);
}
}
@Override
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
Http2Headers headers, int padding) throws Http2Exception {
// A push promise should not be allowed to add headers to an existing stream
FullHttpMessage msg = processHeadersBegin(ctx, promisedStreamId, headers, false, false, false);
if (msg == null) {
throw connectionError(PROTOCOL_ERROR, "Push Promise Frame recieved for pre-existing stream id %d",
promisedStreamId);
}
msg.headers().setInt(HttpUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), streamId);
processHeadersEnd(ctx, promisedStreamId, msg, false);
}
@Override
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception {
if (propagateSettings) {
// Provide an interface for non-listeners to capture settings
ctx.fireChannelRead(settings);
}
}
/**
* Allows messages to be sent up the pipeline before the next phase in the
* HTTP message flow is detected.
*/
private interface ImmediateSendDetector {
/**
* Determine if the response should be sent immediately, or wait for the end of the stream
*
* @param msg The response to test
* @return {@code true} if the message should be sent immediately
* {@code false) if we should wait for the end of the stream
*/
boolean mustSendImmediately(FullHttpMessage msg);
/**
* Determine if a copy must be made after an immediate send happens.
* <p>
* An example of this use case is if a request is received
* with a 'Expect: 100-continue' header. The message will be sent immediately,
* and the data will be queued and sent at the end of the stream.
*
* @param msg The message which has just been sent due to {@link #mustSendImmediately(FullHttpMessage)}
* @return A modified copy of the {@code msg} or {@code null} if a copy is not needed.
*/
FullHttpMessage copyIfNeeded(FullHttpMessage msg);
}
}

View File

@ -0,0 +1,225 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import java.util.Map.Entry;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.TextHeaders.EntryVisitor;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.util.collection.IntObjectHashMap;
import io.netty.util.collection.IntObjectMap;
import io.netty.util.internal.PlatformDependent;
/**
* Translate header/data/priority HTTP/2 frame events into HTTP events. Just as {@link InboundHttp2ToHttpAdapter}
* may generate multiple {@link FullHttpMessage} objects per stream, this class is more likely to
* generate multiple messages per stream because the chances of an HTTP/2 event happening outside
* the header/data message flow is more likely.
*/
public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpAdapter {
private static final AsciiString OUT_OF_MESSAGE_SEQUENCE_METHOD = new AsciiString(
HttpUtil.OUT_OF_MESSAGE_SEQUENCE_METHOD.toString());
private static final AsciiString OUT_OF_MESSAGE_SEQUENCE_PATH = new AsciiString(
HttpUtil.OUT_OF_MESSAGE_SEQUENCE_PATH);
private static final AsciiString OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE = new AsciiString(
HttpUtil.OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE.toString());
private final IntObjectMap<HttpHeaders> outOfMessageFlowHeaders;
public static final class Builder extends InboundHttp2ToHttpAdapter.Builder {
/**
* Creates a new {@link InboundHttp2ToHttpPriorityAdapter} builder for the specified {@link Http2Connection}.
*
* @param connection The object which will provide connection notification events for the current connection
*/
public Builder(Http2Connection connection) {
super(connection);
}
@Override
public InboundHttp2ToHttpPriorityAdapter build() {
final InboundHttp2ToHttpPriorityAdapter instance = new InboundHttp2ToHttpPriorityAdapter(this);
instance.connection.addListener(instance);
return instance;
}
}
InboundHttp2ToHttpPriorityAdapter(Builder builder) {
super(builder);
outOfMessageFlowHeaders = new IntObjectHashMap<HttpHeaders>();
}
@Override
protected void removeMessage(int streamId) {
super.removeMessage(streamId);
outOfMessageFlowHeaders.remove(streamId);
}
/**
* Get either the header or the trailing headers depending on which is valid to add to
* @param msg The message containing the headers and trailing headers
* @return The headers object which can be appended to or modified
*/
private static HttpHeaders getActiveHeaders(FullHttpMessage msg) {
return msg.content().isReadable() ? msg.trailingHeaders() : msg.headers();
}
/**
* This method will add the {@code headers} to the out of order headers map
* @param streamId The stream id associated with {@code headers}
* @param headers Newly encountered out of order headers which must be stored for future use
*/
private void importOutOfMessageFlowHeaders(int streamId, HttpHeaders headers) {
final HttpHeaders outOfMessageFlowHeader = outOfMessageFlowHeaders.get(streamId);
if (outOfMessageFlowHeader == null) {
outOfMessageFlowHeaders.put(streamId, headers);
} else {
outOfMessageFlowHeader.setAll(headers);
}
}
/**
* Take any saved out of order headers and export them to {@code headers}
* @param streamId The stream id to search for out of order headers for
* @param headers If any out of order headers exist for {@code streamId} they will be added to this object
*/
private void exportOutOfMessageFlowHeaders(int streamId, final HttpHeaders headers) {
final HttpHeaders outOfMessageFlowHeader = outOfMessageFlowHeaders.get(streamId);
if (outOfMessageFlowHeader != null) {
headers.setAll(outOfMessageFlowHeader);
}
}
/**
* This will remove all headers which are related to priority tree events
* @param headers The headers to remove the priority tree elements from
*/
private static void removePriorityRelatedHeaders(HttpHeaders headers) {
headers.remove(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text());
headers.remove(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text());
}
/**
* Initializes the pseudo header fields for out of message flow HTTP/2 headers
* @param headers The headers to be initialized with pseudo header values
*/
private void initializePseudoHeaders(Http2Headers headers) {
if (connection.isServer()) {
headers.method(OUT_OF_MESSAGE_SEQUENCE_METHOD).path(OUT_OF_MESSAGE_SEQUENCE_PATH);
} else {
headers.status(OUT_OF_MESSAGE_SEQUENCE_RETURN_CODE);
}
}
/**
* Add all the HTTP headers into the HTTP/2 headers object
* @param httpHeaders The HTTP headers to translate to HTTP/2
* @param http2Headers The target HTTP/2 headers
*/
private static void addHttpHeadersToHttp2Headers(HttpHeaders httpHeaders, final Http2Headers http2Headers) {
try {
httpHeaders.forEachEntry(new EntryVisitor() {
@Override
public boolean visit(Entry<CharSequence, CharSequence> entry) throws Exception {
http2Headers.add(AsciiString.of(entry.getKey()), AsciiString.of(entry.getValue()));
return true;
}
});
} catch (Exception ex) {
PlatformDependent.throwException(ex);
}
}
@Override
protected void fireChannelRead(ChannelHandlerContext ctx, FullHttpMessage msg, int streamId) {
exportOutOfMessageFlowHeaders(streamId, getActiveHeaders(msg));
super.fireChannelRead(ctx, msg, streamId);
}
@Override
protected FullHttpMessage processHeadersBegin(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
boolean endOfStream, boolean allowAppend, boolean appendToTrailer) throws Http2Exception {
FullHttpMessage msg = super.processHeadersBegin(ctx, streamId, headers,
endOfStream, allowAppend, appendToTrailer);
if (msg != null) {
exportOutOfMessageFlowHeaders(streamId, getActiveHeaders(msg));
}
return msg;
}
@Override
public void priorityTreeParentChanged(Http2Stream stream, Http2Stream oldParent) {
Http2Stream parent = stream.parent();
FullHttpMessage msg = messageMap.get(stream.id());
if (msg == null) {
// msg may be null if a HTTP/2 frame event in received outside the HTTP message flow
// For example a PRIORITY frame can be received in any state
// and the HTTP message flow exists in OPEN.
if (parent != null && !parent.equals(connection.connectionStream())) {
HttpHeaders headers = new DefaultHttpHeaders();
headers.setInt(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), parent.id());
importOutOfMessageFlowHeaders(stream.id(), headers);
}
} else {
if (parent == null) {
removePriorityRelatedHeaders(msg.headers());
removePriorityRelatedHeaders(msg.trailingHeaders());
} else if (!parent.equals(connection.connectionStream())) {
HttpHeaders headers = getActiveHeaders(msg);
headers.setInt(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), parent.id());
}
}
}
@Override
public void onWeightChanged(Http2Stream stream, short oldWeight) {
FullHttpMessage msg = messageMap.get(stream.id());
final HttpHeaders headers;
if (msg == null) {
// msg may be null if a HTTP/2 frame event in received outside the HTTP message flow
// For example a PRIORITY frame can be received in any state
// and the HTTP message flow exists in OPEN.
headers = new DefaultHttpHeaders();
importOutOfMessageFlowHeaders(stream.id(), headers);
} else {
headers = getActiveHeaders(msg);
}
headers.setShort(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), stream.weight());
}
@Override
public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight,
boolean exclusive) throws Http2Exception {
FullHttpMessage msg = messageMap.get(streamId);
if (msg == null) {
HttpHeaders httpHeaders = outOfMessageFlowHeaders.remove(streamId);
if (httpHeaders == null) {
throw connectionError(PROTOCOL_ERROR, "Priority Frame recieved for unknown stream id %d", streamId);
}
Http2Headers http2Headers = new DefaultHttp2Headers();
initializePseudoHeaders(http2Headers);
addHttpHeadersToHttp2Headers(httpHeaders, http2Headers);
msg = newMessage(streamId, http2Headers, validateHttpHeaders);
fireChannelRead(ctx, msg, streamId);
}
}
}

View File

@ -0,0 +1,20 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
/**
* Handlers for sending and receiving HTTP/2 frames.
*/
package io.netty.handler.codec.http2;

View File

@ -0,0 +1,359 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http2.Http2TestUtil.FrameAdapter;
import io.netty.handler.codec.http2.Http2TestUtil.FrameCountDown;
import io.netty.handler.codec.http2.Http2TestUtil.Http2Runnable;
import io.netty.util.CharsetUtil;
import io.netty.util.NetUtil;
import io.netty.util.concurrent.Future;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/**
* Test for data decompression in the HTTP/2 codec.
*/
public class DataCompressionHttp2Test {
private static final AsciiString GET = as("GET");
private static final AsciiString POST = as("POST");
private static final AsciiString PATH = as("/some/path");
@Mock
private Http2FrameListener serverListener;
@Mock
private Http2FrameListener clientListener;
private Http2ConnectionEncoder clientEncoder;
private ServerBootstrap sb;
private Bootstrap cb;
private Channel serverChannel;
private Channel clientChannel;
private CountDownLatch serverLatch;
private CountDownLatch clientLatch;
private CountDownLatch clientSettingsAckLatch;
private Http2Connection serverConnection;
private Http2Connection clientConnection;
private ByteArrayOutputStream serverOut;
@Before
public void setup() throws InterruptedException {
MockitoAnnotations.initMocks(this);
}
@After
public void cleaup() throws IOException {
serverOut.close();
}
@After
public void teardown() throws InterruptedException {
serverChannel.close().sync();
Future<?> serverGroup = sb.group().shutdownGracefully(0, 0, MILLISECONDS);
Future<?> serverChildGroup = sb.childGroup().shutdownGracefully(0, 0, MILLISECONDS);
Future<?> clientGroup = cb.group().shutdownGracefully(0, 0, MILLISECONDS);
serverGroup.sync();
serverChildGroup.sync();
clientGroup.sync();
}
@Test
public void justHeadersNoData() throws Exception {
bootstrapEnv(1, 1, 0, 1);
final Http2Headers headers = new DefaultHttp2Headers().method(GET).path(PATH)
.set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
// Required because the decompressor intercepts the onXXXRead events before
// our {@link Http2TestUtil$FrameAdapter} does.
FrameAdapter.getOrCreateStream(serverConnection, 3, false);
FrameAdapter.getOrCreateStream(clientConnection, 3, false);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
clientEncoder.writeHeaders(ctxClient(), 3, headers, 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitServer();
verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(3), eq(headers), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true));
}
@Test
public void gzipEncodingSingleEmptyMessage() throws Exception {
final String text = "";
final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
bootstrapEnv(1, 1, data.readableBytes(), 1);
try {
final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
.set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
// Required because the decompressor intercepts the onXXXRead events before
// our {@link Http2TestUtil$FrameAdapter} does.
Http2Stream stream = FrameAdapter.getOrCreateStream(serverConnection, 3, false);
FrameAdapter.getOrCreateStream(clientConnection, 3, false);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
clientEncoder.writeHeaders(ctxClient(), 3, headers, 0, false, newPromiseClient());
clientEncoder.writeData(ctxClient(), 3, data.retain(), 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitServer();
assertEquals(0, serverConnection.local().flowController().unconsumedBytes(stream));
assertEquals(text, serverOut.toString(CharsetUtil.UTF_8.name()));
} finally {
data.release();
}
}
@Test
public void gzipEncodingSingleMessage() throws Exception {
final String text = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccc";
final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
bootstrapEnv(1, 1, data.readableBytes(), 1);
try {
final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
.set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
// Required because the decompressor intercepts the onXXXRead events before
// our {@link Http2TestUtil$FrameAdapter} does.
Http2Stream stream = FrameAdapter.getOrCreateStream(serverConnection, 3, false);
FrameAdapter.getOrCreateStream(clientConnection, 3, false);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
clientEncoder.writeHeaders(ctxClient(), 3, headers, 0, false, newPromiseClient());
clientEncoder.writeData(ctxClient(), 3, data.retain(), 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitServer();
assertEquals(0, serverConnection.local().flowController().unconsumedBytes(stream));
assertEquals(text, serverOut.toString(CharsetUtil.UTF_8.name()));
} finally {
data.release();
}
}
@Test
public void gzipEncodingMultipleMessages() throws Exception {
final String text1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbbbbbbbbbbbbbbbbbbbbbbbbbbccccccccccccccccccccccc";
final String text2 = "dddddddddddddddddddeeeeeeeeeeeeeeeeeeeffffffffffffffffffff";
final ByteBuf data1 = Unpooled.copiedBuffer(text1.getBytes());
final ByteBuf data2 = Unpooled.copiedBuffer(text2.getBytes());
bootstrapEnv(1, 1, data1.readableBytes() + data2.readableBytes(), 1);
try {
final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
.set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.GZIP);
// Required because the decompressor intercepts the onXXXRead events before
// our {@link Http2TestUtil$FrameAdapter} does.
Http2Stream stream = FrameAdapter.getOrCreateStream(serverConnection, 3, false);
FrameAdapter.getOrCreateStream(clientConnection, 3, false);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
clientEncoder.writeHeaders(ctxClient(), 3, headers, 0, false, newPromiseClient());
clientEncoder.writeData(ctxClient(), 3, data1.retain(), 0, false, newPromiseClient());
clientEncoder.writeData(ctxClient(), 3, data2.retain(), 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitServer();
assertEquals(0, serverConnection.local().flowController().unconsumedBytes(stream));
assertEquals(new StringBuilder(text1).append(text2).toString(),
serverOut.toString(CharsetUtil.UTF_8.name()));
} finally {
data1.release();
data2.release();
}
}
@Test
public void deflateEncodingWriteLargeMessage() throws Exception {
final int BUFFER_SIZE = 1 << 12;
final byte[] bytes = new byte[BUFFER_SIZE];
new Random().nextBytes(bytes);
bootstrapEnv(1, 1, BUFFER_SIZE, 1);
final ByteBuf data = Unpooled.wrappedBuffer(bytes);
try {
final Http2Headers headers = new DefaultHttp2Headers().method(POST).path(PATH)
.set(HttpHeaderNames.CONTENT_ENCODING, HttpHeaderValues.DEFLATE);
// Required because the decompressor intercepts the onXXXRead events before
// our {@link Http2TestUtil$FrameAdapter} does.
Http2Stream stream = FrameAdapter.getOrCreateStream(serverConnection, 3, false);
FrameAdapter.getOrCreateStream(clientConnection, 3, false);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
clientEncoder.writeHeaders(ctxClient(), 3, headers, 0, false, newPromiseClient());
clientEncoder.writeData(ctxClient(), 3, data.retain(), 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitServer();
assertEquals(0, serverConnection.local().flowController().unconsumedBytes(stream));
assertEquals(data.resetReaderIndex().toString(CharsetUtil.UTF_8),
serverOut.toString(CharsetUtil.UTF_8.name()));
} finally {
data.release();
}
}
private void bootstrapEnv(int serverHalfClosedCount, int clientSettingsAckLatchCount,
int serverOutSize, int clientCount) throws Exception {
serverOut = new ByteArrayOutputStream(serverOutSize);
serverLatch = new CountDownLatch(serverHalfClosedCount);
clientLatch = new CountDownLatch(clientCount);
clientSettingsAckLatch = new CountDownLatch(clientSettingsAckLatchCount);
sb = new ServerBootstrap();
cb = new Bootstrap();
// Streams are created before the normal flow for this test, so these connection must be initialized up front.
serverConnection = new DefaultHttp2Connection(true);
clientConnection = new DefaultHttp2Connection(false);
serverConnection.addListener(new Http2ConnectionAdapter() {
@Override
public void streamHalfClosed(Http2Stream stream) {
serverLatch.countDown();
}
});
doAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock in) throws Throwable {
ByteBuf buf = (ByteBuf) in.getArguments()[2];
int padding = (Integer) in.getArguments()[3];
int processedBytes = buf.readableBytes() + padding;
buf.readBytes(serverOut, buf.readableBytes());
return processedBytes;
}
}).when(serverListener).onDataRead(any(ChannelHandlerContext.class), anyInt(),
any(ByteBuf.class), anyInt(), anyBoolean());
final CountDownLatch serverChannelLatch = new CountDownLatch(1);
sb.group(new NioEventLoopGroup(), new NioEventLoopGroup());
sb.channel(NioServerSocketChannel.class);
sb.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
Http2FrameWriter writer = new DefaultHttp2FrameWriter();
Http2ConnectionHandler connectionHandler =
new Http2ConnectionHandler(new DefaultHttp2ConnectionDecoder.Builder()
.connection(serverConnection)
.frameReader(new DefaultHttp2FrameReader())
.listener(
new DelegatingDecompressorFrameListener(serverConnection,
serverListener)),
new CompressorHttp2ConnectionEncoder.Builder().connection(
serverConnection).frameWriter(writer));
p.addLast(connectionHandler);
serverChannelLatch.countDown();
}
});
cb.group(new NioEventLoopGroup());
cb.channel(NioSocketChannel.class);
cb.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
FrameCountDown clientFrameCountDown = new FrameCountDown(clientListener,
clientSettingsAckLatch, clientLatch);
Http2FrameWriter writer = new DefaultHttp2FrameWriter();
Http2ConnectionHandler connectionHandler =
new Http2ConnectionHandler(new DefaultHttp2ConnectionDecoder.Builder()
.connection(clientConnection)
.frameReader(new DefaultHttp2FrameReader())
.listener(
new DelegatingDecompressorFrameListener(clientConnection,
clientFrameCountDown)),
new CompressorHttp2ConnectionEncoder.Builder().connection(
clientConnection).frameWriter(writer));
clientEncoder = connectionHandler.encoder();
p.addLast(connectionHandler);
}
});
serverChannel = sb.bind(new InetSocketAddress(0)).sync().channel();
int port = ((InetSocketAddress) serverChannel.localAddress()).getPort();
ChannelFuture ccf = cb.connect(new InetSocketAddress(NetUtil.LOCALHOST, port));
assertTrue(ccf.awaitUninterruptibly().isSuccess());
clientChannel = ccf.channel();
assertTrue(serverChannelLatch.await(5, SECONDS));
}
private void awaitServer() throws Exception {
assertTrue(clientSettingsAckLatch.await(5, SECONDS));
assertTrue(serverLatch.await(5, SECONDS));
serverOut.flush();
}
private ChannelHandlerContext ctxClient() {
return clientChannel.pipeline().firstContext();
}
private ChannelPromise newPromiseClient() {
return ctxClient().newPromise();
}
}

View File

@ -0,0 +1,482 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.buffer.Unpooled.EMPTY_BUFFER;
import static io.netty.buffer.Unpooled.wrappedBuffer;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
import static io.netty.handler.codec.http2.Http2CodecUtil.emptyPingBuf;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import static io.netty.handler.codec.http2.Http2Stream.State.OPEN;
import static io.netty.handler.codec.http2.Http2Stream.State.RESERVED_REMOTE;
import static io.netty.util.CharsetUtil.UTF_8;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.anyShort;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.DefaultChannelPromise;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicInteger;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/**
* Tests for {@link DefaultHttp2ConnectionDecoder}.
*/
public class DefaultHttp2ConnectionDecoderTest {
private static final int STREAM_ID = 1;
private static final int PUSH_STREAM_ID = 2;
private Http2ConnectionDecoder decoder;
@Mock
private Http2Connection connection;
@Mock
private Http2Connection.Endpoint<Http2RemoteFlowController> remote;
@Mock
private Http2Connection.Endpoint<Http2LocalFlowController> local;
@Mock
private Http2LocalFlowController localFlow;
@Mock
private Http2RemoteFlowController remoteFlow;
@Mock
private ChannelHandlerContext ctx;
@Mock
private Channel channel;
private ChannelPromise promise;
@Mock
private ChannelFuture future;
@Mock
private Http2Stream stream;
@Mock
private Http2Stream pushStream;
@Mock
private Http2FrameListener listener;
@Mock
private Http2FrameReader reader;
@Mock
private Http2ConnectionEncoder encoder;
@Mock
private Http2LifecycleManager lifecycleManager;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
promise = new DefaultChannelPromise(channel);
when(channel.isActive()).thenReturn(true);
when(stream.id()).thenReturn(STREAM_ID);
when(stream.state()).thenReturn(OPEN);
when(stream.open(anyBoolean())).thenReturn(stream);
when(pushStream.id()).thenReturn(PUSH_STREAM_ID);
when(connection.activeStreams()).thenReturn(Collections.singletonList(stream));
when(connection.stream(STREAM_ID)).thenReturn(stream);
when(connection.requireStream(STREAM_ID)).thenReturn(stream);
when(connection.local()).thenReturn(local);
when(local.flowController()).thenReturn(localFlow);
when(encoder.flowController()).thenReturn(remoteFlow);
when(connection.remote()).thenReturn(remote);
doAnswer(new Answer<Http2Stream>() {
@Override
public Http2Stream answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
return local.createStream((Integer) args[0]);
}
}).when(connection).createLocalStream(anyInt());
doAnswer(new Answer<Http2Stream>() {
@Override
public Http2Stream answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
return remote.createStream((Integer) args[0]);
}
}).when(connection).createRemoteStream(anyInt());
when(local.createStream(eq(STREAM_ID))).thenReturn(stream);
when(local.reservePushStream(eq(PUSH_STREAM_ID), eq(stream))).thenReturn(pushStream);
when(remote.createStream(eq(STREAM_ID))).thenReturn(stream);
when(remote.reservePushStream(eq(PUSH_STREAM_ID), eq(stream))).thenReturn(pushStream);
when(ctx.alloc()).thenReturn(UnpooledByteBufAllocator.DEFAULT);
when(ctx.channel()).thenReturn(channel);
when(ctx.newSucceededFuture()).thenReturn(future);
when(ctx.newPromise()).thenReturn(promise);
when(ctx.write(any())).thenReturn(future);
decoder = DefaultHttp2ConnectionDecoder.newBuilder().connection(connection)
.frameReader(reader).encoder(encoder)
.listener(listener).lifecycleManager(lifecycleManager).build();
// Simulate receiving the initial settings from the remote endpoint.
decode().onSettingsRead(ctx, new Http2Settings());
verify(listener).onSettingsRead(eq(ctx), eq(new Http2Settings()));
assertTrue(decoder.prefaceReceived());
verify(encoder).writeSettingsAck(eq(ctx), eq(promise));
// Simulate receiving the SETTINGS ACK for the initial settings.
decode().onSettingsAckRead(ctx);
}
@Test
public void dataReadAfterGoAwayShouldApplyFlowControl() throws Exception {
when(connection.goAwaySent()).thenReturn(true);
final ByteBuf data = dummyData();
int padding = 10;
int processedBytes = data.readableBytes() + padding;
mockFlowControl(processedBytes);
try {
decode().onDataRead(ctx, STREAM_ID, data, 10, true);
verify(localFlow).receiveFlowControlledFrame(eq(ctx), eq(stream), eq(data), eq(padding), eq(true));
verify(localFlow).consumeBytes(eq(ctx), eq(stream), eq(processedBytes));
// Verify that the event was absorbed and not propagated to the oberver.
verify(listener, never()).onDataRead(eq(ctx), anyInt(), any(ByteBuf.class), anyInt(), anyBoolean());
} finally {
data.release();
}
}
@Test(expected = Http2Exception.class)
public void dataReadForStreamInInvalidStateShouldThrow() throws Exception {
// Throw an exception when checking stream state.
when(stream.state()).thenReturn(Http2Stream.State.CLOSED);
final ByteBuf data = dummyData();
try {
decode().onDataRead(ctx, STREAM_ID, data, 10, true);
} finally {
data.release();
}
}
@Test
public void dataReadAfterGoAwayForStreamInInvalidStateShouldIgnore() throws Exception {
// Throw an exception when checking stream state.
when(stream.state()).thenReturn(Http2Stream.State.CLOSED);
when(connection.goAwaySent()).thenReturn(true);
final ByteBuf data = dummyData();
try {
decode().onDataRead(ctx, STREAM_ID, data, 10, true);
verify(localFlow, never()).receiveFlowControlledFrame(eq(ctx), eq(stream), eq(data), eq(10), eq(true));
verify(listener, never()).onDataRead(eq(ctx), anyInt(), any(ByteBuf.class), anyInt(), anyBoolean());
} finally {
data.release();
}
}
@Test
public void dataReadAfterRstStreamForStreamInInvalidStateShouldIgnore() throws Exception {
// Throw an exception when checking stream state.
when(stream.state()).thenReturn(Http2Stream.State.CLOSED);
when(stream.isResetSent()).thenReturn(true);
final ByteBuf data = dummyData();
try {
decode().onDataRead(ctx, STREAM_ID, data, 10, true);
verify(localFlow).receiveFlowControlledFrame(eq(ctx), eq(stream), eq(data), eq(10), eq(true));
verify(listener, never()).onDataRead(eq(ctx), anyInt(), any(ByteBuf.class), anyInt(), anyBoolean());
} finally {
data.release();
}
}
@Test
public void dataReadWithEndOfStreamShouldCloseRemoteSide() throws Exception {
final ByteBuf data = dummyData();
try {
decode().onDataRead(ctx, STREAM_ID, data, 10, true);
verify(localFlow).receiveFlowControlledFrame(eq(ctx), eq(stream), eq(data), eq(10), eq(true));
verify(lifecycleManager).closeRemoteSide(eq(stream), eq(future));
verify(listener).onDataRead(eq(ctx), eq(STREAM_ID), eq(data), eq(10), eq(true));
} finally {
data.release();
}
}
@Test
public void errorDuringDeliveryShouldReturnCorrectNumberOfBytes() throws Exception {
final ByteBuf data = dummyData();
final int padding = 10;
final AtomicInteger unprocessed = new AtomicInteger(data.readableBytes() + padding);
doAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock in) throws Throwable {
return unprocessed.get();
}
}).when(localFlow).unconsumedBytes(eq(stream));
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock in) throws Throwable {
int delta = (Integer) in.getArguments()[2];
int newValue = unprocessed.addAndGet(-delta);
if (newValue < 0) {
throw new RuntimeException("Returned too many bytes");
}
return null;
}
}).when(localFlow).consumeBytes(eq(ctx), eq(stream), anyInt());
// When the listener callback is called, process a few bytes and then throw.
doAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock in) throws Throwable {
localFlow.consumeBytes(ctx, stream, 4);
throw new RuntimeException("Fake Exception");
}
}).when(listener).onDataRead(eq(ctx), eq(STREAM_ID), any(ByteBuf.class), eq(10), eq(true));
try {
decode().onDataRead(ctx, STREAM_ID, data, padding, true);
fail("Expected exception");
} catch (RuntimeException cause) {
verify(localFlow)
.receiveFlowControlledFrame(eq(ctx), eq(stream), eq(data), eq(padding), eq(true));
verify(lifecycleManager).closeRemoteSide(eq(stream), eq(future));
verify(listener).onDataRead(eq(ctx), eq(STREAM_ID), eq(data), eq(padding), eq(true));
assertEquals(0, localFlow.unconsumedBytes(stream));
} finally {
data.release();
}
}
@Test
public void headersReadAfterGoAwayShouldBeIgnored() throws Exception {
when(connection.goAwaySent()).thenReturn(true);
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false);
verify(remote, never()).createStream(eq(STREAM_ID));
verify(stream, never()).open(anyBoolean());
// Verify that the event was absorbed and not propagated to the oberver.
verify(listener, never()).onHeadersRead(eq(ctx), anyInt(), any(Http2Headers.class), anyInt(), anyBoolean());
verify(remote, never()).createStream(anyInt());
verify(stream, never()).open(anyBoolean());
}
@Test
public void headersReadForUnknownStreamShouldCreateStream() throws Exception {
final int streamId = 5;
when(remote.createStream(eq(streamId))).thenReturn(stream);
decode().onHeadersRead(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false);
verify(remote).createStream(eq(streamId));
verify(stream).open(eq(false));
verify(listener).onHeadersRead(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false));
}
@Test
public void headersReadForUnknownStreamShouldCreateHalfClosedStream() throws Exception {
final int streamId = 5;
when(remote.createStream(eq(streamId))).thenReturn(stream);
decode().onHeadersRead(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, true);
verify(remote).createStream(eq(streamId));
verify(stream).open(eq(true));
verify(listener).onHeadersRead(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true));
}
@Test
public void headersReadForPromisedStreamShouldHalfOpenStream() throws Exception {
when(stream.state()).thenReturn(RESERVED_REMOTE);
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false);
verify(stream).open(false);
verify(listener).onHeadersRead(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false));
}
@Test
public void headersReadForPromisedStreamShouldCloseStream() throws Exception {
when(stream.state()).thenReturn(RESERVED_REMOTE);
decode().onHeadersRead(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true);
verify(stream).open(true);
verify(lifecycleManager).closeRemoteSide(eq(stream), eq(future));
verify(listener).onHeadersRead(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true));
}
@Test
public void pushPromiseReadAfterGoAwayShouldBeIgnored() throws Exception {
when(connection.goAwaySent()).thenReturn(true);
decode().onPushPromiseRead(ctx, STREAM_ID, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0);
verify(remote, never()).reservePushStream(anyInt(), any(Http2Stream.class));
verify(listener, never()).onPushPromiseRead(eq(ctx), anyInt(), anyInt(), any(Http2Headers.class), anyInt());
}
@Test
public void pushPromiseReadShouldSucceed() throws Exception {
decode().onPushPromiseRead(ctx, STREAM_ID, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0);
verify(remote).reservePushStream(eq(PUSH_STREAM_ID), eq(stream));
verify(listener).onPushPromiseRead(eq(ctx), eq(STREAM_ID), eq(PUSH_STREAM_ID),
eq(EmptyHttp2Headers.INSTANCE), eq(0));
}
@Test
public void priorityReadAfterGoAwayShouldBeIgnored() throws Exception {
when(connection.goAwaySent()).thenReturn(true);
decode().onPriorityRead(ctx, STREAM_ID, 0, (short) 255, true);
verify(stream, never()).setPriority(anyInt(), anyShort(), anyBoolean());
verify(listener, never()).onPriorityRead(eq(ctx), anyInt(), anyInt(), anyShort(), anyBoolean());
}
@Test
public void priorityReadShouldSucceed() throws Exception {
when(connection.stream(STREAM_ID)).thenReturn(null);
when(connection.requireStream(STREAM_ID)).thenReturn(null);
decode().onPriorityRead(ctx, STREAM_ID, 0, (short) 255, true);
verify(stream).setPriority(eq(0), eq((short) 255), eq(true));
verify(listener).onPriorityRead(eq(ctx), eq(STREAM_ID), eq(0), eq((short) 255), eq(true));
verify(connection).createRemoteStream(STREAM_ID);
verify(stream, never()).open(anyBoolean());
}
@Test
public void windowUpdateReadAfterGoAwayShouldBeIgnored() throws Exception {
when(connection.goAwaySent()).thenReturn(true);
decode().onWindowUpdateRead(ctx, STREAM_ID, 10);
verify(remoteFlow, never()).incrementWindowSize(eq(ctx), any(Http2Stream.class), anyInt());
verify(listener, never()).onWindowUpdateRead(eq(ctx), anyInt(), anyInt());
}
@Test(expected = Http2Exception.class)
public void windowUpdateReadForUnknownStreamShouldThrow() throws Exception {
when(connection.requireStream(5)).thenThrow(connectionError(PROTOCOL_ERROR, ""));
decode().onWindowUpdateRead(ctx, 5, 10);
}
@Test
public void windowUpdateReadShouldSucceed() throws Exception {
decode().onWindowUpdateRead(ctx, STREAM_ID, 10);
verify(remoteFlow).incrementWindowSize(eq(ctx), eq(stream), eq(10));
verify(listener).onWindowUpdateRead(eq(ctx), eq(STREAM_ID), eq(10));
}
@Test
public void rstStreamReadAfterGoAwayShouldSucceed() throws Exception {
when(connection.goAwaySent()).thenReturn(true);
decode().onRstStreamRead(ctx, STREAM_ID, PROTOCOL_ERROR.code());
verify(lifecycleManager).closeStream(eq(stream), eq(future));
verify(listener).onRstStreamRead(eq(ctx), anyInt(), anyLong());
}
@Test(expected = Http2Exception.class)
public void rstStreamReadForUnknownStreamShouldThrow() throws Exception {
when(connection.requireStream(5)).thenThrow(connectionError(PROTOCOL_ERROR, ""));
decode().onRstStreamRead(ctx, 5, PROTOCOL_ERROR.code());
}
@Test
public void rstStreamReadShouldCloseStream() throws Exception {
decode().onRstStreamRead(ctx, STREAM_ID, PROTOCOL_ERROR.code());
verify(lifecycleManager).closeStream(eq(stream), eq(future));
verify(listener).onRstStreamRead(eq(ctx), eq(STREAM_ID), eq(PROTOCOL_ERROR.code()));
}
@Test
public void pingReadWithAckShouldNotifylistener() throws Exception {
decode().onPingAckRead(ctx, emptyPingBuf());
verify(listener).onPingAckRead(eq(ctx), eq(emptyPingBuf()));
}
@Test
public void pingReadShouldReplyWithAck() throws Exception {
decode().onPingRead(ctx, emptyPingBuf());
verify(encoder).writePing(eq(ctx), eq(true), eq(emptyPingBuf()), eq(promise));
verify(listener, never()).onPingAckRead(eq(ctx), any(ByteBuf.class));
}
@Test
public void settingsReadWithAckShouldNotifylistener() throws Exception {
decode().onSettingsAckRead(ctx);
// Take into account the time this was called during setup().
verify(listener, times(2)).onSettingsAckRead(eq(ctx));
}
@Test
public void settingsReadShouldSetValues() throws Exception {
when(connection.isServer()).thenReturn(true);
Http2Settings settings = new Http2Settings();
settings.pushEnabled(true);
settings.initialWindowSize(123);
settings.maxConcurrentStreams(456);
settings.headerTableSize(789);
decode().onSettingsRead(ctx, settings);
verify(encoder).remoteSettings(settings);
verify(listener).onSettingsRead(eq(ctx), eq(settings));
}
@Test
public void goAwayShouldReadShouldUpdateConnectionState() throws Exception {
decode().onGoAwayRead(ctx, 1, 2L, EMPTY_BUFFER);
verify(connection).goAwayReceived(1);
verify(listener).onGoAwayRead(eq(ctx), eq(1), eq(2L), eq(EMPTY_BUFFER));
}
private static ByteBuf dummyData() {
// The buffer is purposely 8 bytes so it will even work for a ping frame.
return wrappedBuffer("abcdefgh".getBytes(UTF_8));
}
/**
* Calls the decode method on the handler and gets back the captured internal listener
*/
private Http2FrameListener decode() throws Exception {
ArgumentCaptor<Http2FrameListener> internallistener = ArgumentCaptor.forClass(Http2FrameListener.class);
doNothing().when(reader).readFrame(eq(ctx), any(ByteBuf.class), internallistener.capture());
decoder.decodeFrame(ctx, EMPTY_BUFFER, Collections.emptyList());
return internallistener.getValue();
}
private void mockFlowControl(final int processedBytes) throws Http2Exception {
doAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
return processedBytes;
}
}).when(listener).onDataRead(any(ChannelHandlerContext.class), anyInt(),
any(ByteBuf.class), anyInt(), anyBoolean());
}
}

View File

@ -0,0 +1,379 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.buffer.Unpooled.wrappedBuffer;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
import static io.netty.handler.codec.http2.Http2CodecUtil.emptyPingBuf;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2Stream.State.HALF_CLOSED_LOCAL;
import static io.netty.handler.codec.http2.Http2Stream.State.OPEN;
import static io.netty.handler.codec.http2.Http2Stream.State.RESERVED_LOCAL;
import static io.netty.util.CharsetUtil.UTF_8;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.DefaultChannelPromise;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/**
* Tests for {@link DefaultHttp2ConnectionEncoder}
*/
public class DefaultHttp2ConnectionEncoderTest {
private static final int STREAM_ID = 1;
private static final int PUSH_STREAM_ID = 2;
private Http2ConnectionEncoder encoder;
@Mock
private Http2Connection connection;
@Mock
private Http2Connection.Endpoint<Http2RemoteFlowController> remote;
@Mock
private Http2Connection.Endpoint<Http2LocalFlowController> local;
@Mock
private Http2RemoteFlowController remoteFlow;
@Mock
private ChannelHandlerContext ctx;
@Mock
private Channel channel;
private ChannelPromise promise;
@Mock
private ChannelFuture future;
@Mock
private Http2Stream stream;
@Mock
private Http2Stream pushStream;
@Mock
private Http2FrameListener listener;
@Mock
private Http2FrameWriter writer;
@Mock
private Http2LifecycleManager lifecycleManager;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
promise = new DefaultChannelPromise(channel);
when(channel.isActive()).thenReturn(true);
when(stream.id()).thenReturn(STREAM_ID);
when(stream.state()).thenReturn(OPEN);
when(stream.open(anyBoolean())).thenReturn(stream);
when(pushStream.id()).thenReturn(PUSH_STREAM_ID);
when(connection.activeStreams()).thenReturn(Collections.singletonList(stream));
when(connection.stream(STREAM_ID)).thenReturn(stream);
when(connection.requireStream(STREAM_ID)).thenReturn(stream);
when(connection.local()).thenReturn(local);
when(connection.remote()).thenReturn(remote);
when(remote.flowController()).thenReturn(remoteFlow);
doAnswer(new Answer<Http2Stream>() {
@Override
public Http2Stream answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
return local.createStream((Integer) args[0]);
}
}).when(connection).createLocalStream(anyInt());
doAnswer(new Answer<Http2Stream>() {
@Override
public Http2Stream answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
return remote.createStream((Integer) args[0]);
}
}).when(connection).createRemoteStream(anyInt());
when(local.createStream(eq(STREAM_ID))).thenReturn(stream);
when(local.reservePushStream(eq(PUSH_STREAM_ID), eq(stream))).thenReturn(pushStream);
when(remote.createStream(eq(STREAM_ID))).thenReturn(stream);
when(remote.reservePushStream(eq(PUSH_STREAM_ID), eq(stream))).thenReturn(pushStream);
when(writer.writeSettings(eq(ctx), any(Http2Settings.class), eq(promise))).thenReturn(future);
when(writer.writeGoAway(eq(ctx), anyInt(), anyInt(), any(ByteBuf.class), eq(promise)))
.thenReturn(future);
when(remoteFlow.sendFlowControlledFrame(eq(ctx), any(Http2Stream.class),
any(ByteBuf.class), anyInt(), anyBoolean(), eq(promise))).thenReturn(future);
when(ctx.alloc()).thenReturn(UnpooledByteBufAllocator.DEFAULT);
when(ctx.channel()).thenReturn(channel);
when(ctx.newSucceededFuture()).thenReturn(future);
when(ctx.newPromise()).thenReturn(promise);
when(ctx.write(any())).thenReturn(future);
encoder = DefaultHttp2ConnectionEncoder.newBuilder().connection(connection)
.frameWriter(writer).lifecycleManager(lifecycleManager).build();
}
@Test
public void dataWriteAfterGoAwayShouldFail() throws Exception {
when(connection.isGoAway()).thenReturn(true);
final ByteBuf data = dummyData();
try {
ChannelFuture future = encoder.writeData(ctx, STREAM_ID, data, 0, false, promise);
assertTrue(future.awaitUninterruptibly().cause() instanceof IllegalStateException);
} finally {
while (data.refCnt() > 0) {
data.release();
}
}
}
@Test
public void dataWriteShouldSucceed() throws Exception {
final ByteBuf data = dummyData();
try {
encoder.writeData(ctx, STREAM_ID, data, 0, false, promise);
verify(remoteFlow).sendFlowControlledFrame(eq(ctx), eq(stream), eq(data), eq(0), eq(false), eq(promise));
} finally {
data.release();
}
}
@Test
public void dataWriteShouldHalfCloseStream() throws Exception {
reset(future);
final ByteBuf data = dummyData();
try {
encoder.writeData(ctx, STREAM_ID, data, 0, true, promise);
verify(remoteFlow).sendFlowControlledFrame(eq(ctx), eq(stream), eq(data), eq(0), eq(true), eq(promise));
// Invoke the listener callback indicating that the write completed successfully.
ArgumentCaptor<ChannelFutureListener> captor = ArgumentCaptor.forClass(ChannelFutureListener.class);
verify(future).addListener(captor.capture());
when(future.isSuccess()).thenReturn(true);
captor.getValue().operationComplete(future);
verify(lifecycleManager).closeLocalSide(eq(stream), eq(promise));
} finally {
data.release();
}
}
@Test
public void headersWriteAfterGoAwayShouldFail() throws Exception {
when(connection.isGoAway()).thenReturn(true);
ChannelFuture future = encoder.writeHeaders(
ctx, 5, EmptyHttp2Headers.INSTANCE, 0, (short) 255, false, 0, false, promise);
verify(local, never()).createStream(anyInt());
verify(stream, never()).open(anyBoolean());
verify(writer, never()).writeHeaders(eq(ctx), anyInt(), any(Http2Headers.class), anyInt(), anyBoolean(),
eq(promise));
assertTrue(future.awaitUninterruptibly().cause() instanceof Http2Exception);
}
@Test
public void headersWriteForUnknownStreamShouldCreateStream() throws Exception {
int streamId = 5;
when(stream.id()).thenReturn(streamId);
mockFutureAddListener(true);
when(local.createStream(eq(streamId))).thenReturn(stream);
encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, false, promise);
verify(local).createStream(eq(streamId));
verify(stream).open(eq(false));
verify(writer).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
}
@Test
public void headersWriteShouldCreateHalfClosedStream() throws Exception {
int streamId = 5;
when(stream.id()).thenReturn(streamId);
mockFutureAddListener(true);
when(local.createStream(eq(streamId))).thenReturn(stream);
encoder.writeHeaders(ctx, streamId, EmptyHttp2Headers.INSTANCE, 0, true, promise);
verify(local).createStream(eq(streamId));
verify(stream).open(eq(true));
verify(writer).writeHeaders(eq(ctx), eq(streamId), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise));
}
@Test
public void headersWriteAfterDataShouldWait() throws Exception {
final AtomicReference<ChannelFutureListener> listener = new AtomicReference<ChannelFutureListener>();
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
listener.set((ChannelFutureListener) invocation.getArguments()[0]);
return null;
}
}).when(future).addListener(any(ChannelFutureListener.class));
// Indicate that there was a previous data write operation that the headers must wait for.
when(remoteFlow.lastFlowControlledFrameSent(any(Http2Stream.class))).thenReturn(future);
encoder.writeHeaders(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true, promise);
verify(writer, never()).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise));
// Now complete the previous data write operation and verify that the headers were written.
when(future.isSuccess()).thenReturn(true);
listener.get().operationComplete(future);
verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise));
}
@Test
public void headersWriteShouldOpenStreamForPush() throws Exception {
mockFutureAddListener(true);
when(stream.state()).thenReturn(RESERVED_LOCAL);
encoder.writeHeaders(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, false, promise);
verify(stream).open(false);
verify(stream, never()).closeLocalSide();
verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(false), eq(promise));
}
@Test
public void headersWriteShouldClosePushStream() throws Exception {
mockFutureAddListener(true);
when(stream.state()).thenReturn(RESERVED_LOCAL).thenReturn(HALF_CLOSED_LOCAL);
encoder.writeHeaders(ctx, STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, true, promise);
verify(stream).open(true);
verify(lifecycleManager).closeLocalSide(eq(stream), eq(promise));
verify(writer).writeHeaders(eq(ctx), eq(STREAM_ID), eq(EmptyHttp2Headers.INSTANCE), eq(0),
eq(DEFAULT_PRIORITY_WEIGHT), eq(false), eq(0), eq(true), eq(promise));
}
@Test
public void pushPromiseWriteAfterGoAwayShouldFail() throws Exception {
when(connection.isGoAway()).thenReturn(true);
ChannelFuture future =
encoder.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID,
EmptyHttp2Headers.INSTANCE, 0, promise);
assertTrue(future.awaitUninterruptibly().cause() instanceof Http2Exception);
}
@Test
public void pushPromiseWriteShouldReserveStream() throws Exception {
encoder.writePushPromise(ctx, STREAM_ID, PUSH_STREAM_ID, EmptyHttp2Headers.INSTANCE, 0, promise);
verify(local).reservePushStream(eq(PUSH_STREAM_ID), eq(stream));
verify(writer).writePushPromise(eq(ctx), eq(STREAM_ID), eq(PUSH_STREAM_ID),
eq(EmptyHttp2Headers.INSTANCE), eq(0), eq(promise));
}
@Test
public void priorityWriteAfterGoAwayShouldFail() throws Exception {
when(connection.isGoAway()).thenReturn(true);
ChannelFuture future = encoder.writePriority(ctx, STREAM_ID, 0, (short) 255, true, promise);
assertTrue(future.awaitUninterruptibly().cause() instanceof Http2Exception);
}
@Test
public void priorityWriteShouldSetPriorityForStream() throws Exception {
when(connection.stream(STREAM_ID)).thenReturn(null);
when(connection.requireStream(STREAM_ID)).thenReturn(null);
encoder.writePriority(ctx, STREAM_ID, 0, (short) 255, true, promise);
verify(stream).setPriority(eq(0), eq((short) 255), eq(true));
verify(writer).writePriority(eq(ctx), eq(STREAM_ID), eq(0), eq((short) 255), eq(true), eq(promise));
verify(connection).createLocalStream(STREAM_ID);
verify(stream, never()).open(anyBoolean());
}
@Test
public void rstStreamWriteForUnknownStreamShouldIgnore() throws Exception {
encoder.writeRstStream(ctx, 5, PROTOCOL_ERROR.code(), promise);
verify(writer, never()).writeRstStream(eq(ctx), anyInt(), anyLong(), eq(promise));
}
@Test
public void rstStreamWriteShouldCloseStream() throws Exception {
encoder.writeRstStream(ctx, STREAM_ID, PROTOCOL_ERROR.code(), promise);
verify(lifecycleManager).writeRstStream(eq(ctx), eq(STREAM_ID), eq(PROTOCOL_ERROR.code()), eq(promise));
}
@Test
public void pingWriteAfterGoAwayShouldFail() throws Exception {
when(connection.isGoAway()).thenReturn(true);
ChannelFuture future = encoder.writePing(ctx, false, emptyPingBuf(), promise);
assertTrue(future.awaitUninterruptibly().cause() instanceof Http2Exception);
}
@Test
public void pingWriteShouldSucceed() throws Exception {
encoder.writePing(ctx, false, emptyPingBuf(), promise);
verify(writer).writePing(eq(ctx), eq(false), eq(emptyPingBuf()), eq(promise));
}
@Test
public void settingsWriteAfterGoAwayShouldFail() throws Exception {
when(connection.isGoAway()).thenReturn(true);
ChannelFuture future = encoder.writeSettings(ctx, new Http2Settings(), promise);
assertTrue(future.awaitUninterruptibly().cause() instanceof Http2Exception);
}
@Test
public void settingsWriteShouldNotUpdateSettings() throws Exception {
Http2Settings settings = new Http2Settings();
settings.initialWindowSize(100);
settings.pushEnabled(false);
settings.maxConcurrentStreams(1000);
settings.headerTableSize(2000);
encoder.writeSettings(ctx, settings, promise);
verify(writer).writeSettings(eq(ctx), eq(settings), eq(promise));
}
private void mockFutureAddListener(boolean success) {
when(future.isSuccess()).thenReturn(success);
if (!success) {
when(future.cause()).thenReturn(new Exception("Fake Exception"));
}
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock invocation) throws Throwable {
ChannelFutureListener listener = (ChannelFutureListener) invocation.getArguments()[0];
listener.operationComplete(future);
return null;
}
}).when(future).addListener(any(ChannelFutureListener.class));
}
private static ByteBuf dummyData() {
// The buffer is purposely 8 bytes so it will even work for a ping frame.
return wrappedBuffer("abcdefgh".getBytes(UTF_8));
}
}

View File

@ -0,0 +1,606 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.MIN_WEIGHT;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_PRIORITY_WEIGHT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.assertSame;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyShort;
import static org.mockito.Matchers.eq;
import static org.mockito.Matchers.isNull;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import io.netty.handler.codec.http2.Http2Connection.Endpoint;
import io.netty.handler.codec.http2.Http2Stream.State;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Tests for {@link DefaultHttp2Connection}.
*/
public class DefaultHttp2ConnectionTest {
private DefaultHttp2Connection server;
private DefaultHttp2Connection client;
@Mock
private Http2Connection.Listener clientListener;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
server = new DefaultHttp2Connection(true);
client = new DefaultHttp2Connection(false);
client.addListener(clientListener);
}
@Test(expected = Http2Exception.class)
public void getStreamOrFailWithoutStreamShouldFail() throws Http2Exception {
server.requireStream(100);
}
@Test
public void getStreamWithoutStreamShouldReturnNull() {
assertNull(server.stream(100));
}
@Test
public void serverCreateStreamShouldSucceed() throws Http2Exception {
Http2Stream stream = server.local().createStream(2).open(false);
assertEquals(2, stream.id());
assertEquals(State.OPEN, stream.state());
assertEquals(1, server.activeStreams().size());
assertEquals(2, server.local().lastStreamCreated());
stream = server.local().createStream(4).open(true);
assertEquals(4, stream.id());
assertEquals(State.HALF_CLOSED_LOCAL, stream.state());
assertEquals(2, server.activeStreams().size());
assertEquals(4, server.local().lastStreamCreated());
stream = server.remote().createStream(3).open(true);
assertEquals(3, stream.id());
assertEquals(State.HALF_CLOSED_REMOTE, stream.state());
assertEquals(3, server.activeStreams().size());
assertEquals(3, server.remote().lastStreamCreated());
stream = server.remote().createStream(5).open(false);
assertEquals(5, stream.id());
assertEquals(State.OPEN, stream.state());
assertEquals(4, server.activeStreams().size());
assertEquals(5, server.remote().lastStreamCreated());
}
@Test
public void clientCreateStreamShouldSucceed() throws Http2Exception {
Http2Stream stream = client.remote().createStream(2).open(false);
assertEquals(2, stream.id());
assertEquals(State.OPEN, stream.state());
assertEquals(1, client.activeStreams().size());
assertEquals(2, client.remote().lastStreamCreated());
stream = client.remote().createStream(4).open(true);
assertEquals(4, stream.id());
assertEquals(State.HALF_CLOSED_REMOTE, stream.state());
assertEquals(2, client.activeStreams().size());
assertEquals(4, client.remote().lastStreamCreated());
stream = client.local().createStream(3).open(true);
assertEquals(3, stream.id());
assertEquals(State.HALF_CLOSED_LOCAL, stream.state());
assertEquals(3, client.activeStreams().size());
assertEquals(3, client.local().lastStreamCreated());
stream = client.local().createStream(5).open(false);
assertEquals(5, stream.id());
assertEquals(State.OPEN, stream.state());
assertEquals(4, client.activeStreams().size());
assertEquals(5, client.local().lastStreamCreated());
}
@Test
public void serverReservePushStreamShouldSucceed() throws Http2Exception {
Http2Stream stream = server.remote().createStream(3).open(true);
Http2Stream pushStream = server.local().reservePushStream(2, stream);
assertEquals(2, pushStream.id());
assertEquals(State.RESERVED_LOCAL, pushStream.state());
assertEquals(1, server.activeStreams().size());
assertEquals(2, server.local().lastStreamCreated());
}
@Test
public void clientReservePushStreamShouldSucceed() throws Http2Exception {
Http2Stream stream = server.remote().createStream(3).open(true);
Http2Stream pushStream = server.local().reservePushStream(4, stream);
assertEquals(4, pushStream.id());
assertEquals(State.RESERVED_LOCAL, pushStream.state());
assertEquals(1, server.activeStreams().size());
assertEquals(4, server.local().lastStreamCreated());
}
@Test(expected = Http2Exception.class)
public void newStreamBehindExpectedShouldThrow() throws Http2Exception {
server.local().createStream(0).open(true);
}
@Test(expected = Http2Exception.class)
public void newStreamNotForServerShouldThrow() throws Http2Exception {
server.local().createStream(11).open(true);
}
@Test(expected = Http2Exception.class)
public void newStreamNotForClientShouldThrow() throws Http2Exception {
client.local().createStream(10).open(true);
}
@Test(expected = Http2Exception.class)
public void maxAllowedStreamsExceededShouldThrow() throws Http2Exception {
server.local().maxStreams(0);
server.local().createStream(2).open(true);
}
@Test(expected = Http2Exception.class)
public void reserveWithPushDisallowedShouldThrow() throws Http2Exception {
Http2Stream stream = server.remote().createStream(3).open(true);
server.remote().allowPushTo(false);
server.local().reservePushStream(2, stream);
}
@Test(expected = Http2Exception.class)
public void goAwayReceivedShouldDisallowCreation() throws Http2Exception {
server.goAwayReceived(0);
server.remote().createStream(3).open(true);
}
@Test
public void closeShouldSucceed() throws Http2Exception {
Http2Stream stream = server.remote().createStream(3).open(true);
stream.close();
assertEquals(State.CLOSED, stream.state());
assertTrue(server.activeStreams().isEmpty());
}
@Test
public void closeLocalWhenOpenShouldSucceed() throws Http2Exception {
Http2Stream stream = server.remote().createStream(3).open(false);
stream.closeLocalSide();
assertEquals(State.HALF_CLOSED_LOCAL, stream.state());
assertEquals(1, server.activeStreams().size());
}
@Test
public void closeRemoteWhenOpenShouldSucceed() throws Http2Exception {
Http2Stream stream = server.remote().createStream(3).open(false);
stream.closeRemoteSide();
assertEquals(State.HALF_CLOSED_REMOTE, stream.state());
assertEquals(1, server.activeStreams().size());
}
@Test
public void closeOnlyOpenSideShouldClose() throws Http2Exception {
Http2Stream stream = server.remote().createStream(3).open(true);
stream.closeLocalSide();
assertEquals(State.CLOSED, stream.state());
assertTrue(server.activeStreams().isEmpty());
}
@Test(expected = Http2Exception.class)
public void localStreamInvalidStreamIdShouldThrow() throws Http2Exception {
client.createLocalStream(Integer.MAX_VALUE + 2).open(false);
}
@Test(expected = Http2Exception.class)
public void remoteStreamInvalidStreamIdShouldThrow() throws Http2Exception {
client.createRemoteStream(Integer.MAX_VALUE + 1).open(false);
}
@Test
public void localStreamCanDependUponIdleStream() throws Http2Exception {
Http2Stream streamA = client.local().createStream(1).open(false);
streamA.setPriority(3, MIN_WEIGHT, true);
verifyDependUponIdleStream(streamA, client.stream(3), client.local());
}
@Test
public void remoteStreamCanDependUponIdleStream() throws Http2Exception {
Http2Stream streamA = client.remote().createStream(2).open(false);
streamA.setPriority(4, MIN_WEIGHT, true);
verifyDependUponIdleStream(streamA, client.stream(4), client.remote());
}
@Test
public void prioritizeShouldUseDefaults() throws Exception {
Http2Stream stream = client.local().createStream(1).open(false);
assertEquals(1, client.connectionStream().numChildren());
assertEquals(stream, client.connectionStream().child(1));
assertEquals(DEFAULT_PRIORITY_WEIGHT, stream.weight());
assertEquals(0, stream.parent().id());
assertEquals(0, stream.numChildren());
}
@Test
public void reprioritizeWithNoChangeShouldDoNothing() throws Exception {
Http2Stream stream = client.local().createStream(1).open(false);
stream.setPriority(0, DEFAULT_PRIORITY_WEIGHT, false);
assertEquals(1, client.connectionStream().numChildren());
assertEquals(stream, client.connectionStream().child(1));
assertEquals(DEFAULT_PRIORITY_WEIGHT, stream.weight());
assertEquals(0, stream.parent().id());
assertEquals(0, stream.numChildren());
}
@Test
public void insertExclusiveShouldAddNewLevel() throws Exception {
Http2Stream streamA = client.local().createStream(1).open(false);
Http2Stream streamB = client.local().createStream(3).open(false);
Http2Stream streamC = client.local().createStream(5).open(false);
Http2Stream streamD = client.local().createStream(7).open(false);
streamB.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
streamC.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
streamD.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, true);
assertEquals(4, client.numActiveStreams());
// Level 0
Http2Stream p = client.connectionStream();
assertEquals(1, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
// Level 1
p = p.child(streamA.id());
assertNotNull(p);
assertEquals(0, p.parent().id());
assertEquals(1, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
// Level 2
p = p.child(streamD.id());
assertNotNull(p);
assertEquals(streamA.id(), p.parent().id());
assertEquals(2, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
// Level 3
p = p.child(streamB.id());
assertNotNull(p);
assertEquals(streamD.id(), p.parent().id());
assertEquals(0, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
p = p.parent().child(streamC.id());
assertNotNull(p);
assertEquals(streamD.id(), p.parent().id());
assertEquals(0, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
}
@Test
public void weightChangeWithNoTreeChangeShouldNotifyListeners() throws Http2Exception {
Http2Stream streamA = client.local().createStream(1).open(false);
Http2Stream streamB = client.local().createStream(3).open(false);
Http2Stream streamC = client.local().createStream(5).open(false);
Http2Stream streamD = client.local().createStream(7).open(false);
streamB.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
streamC.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
streamD.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, true);
assertEquals(4, client.numActiveStreams());
short oldWeight = streamD.weight();
short newWeight = (short) (oldWeight + 1);
reset(clientListener);
streamD.setPriority(streamD.parent().id(), newWeight, false);
verify(clientListener).onWeightChanged(eq(streamD), eq(oldWeight));
assertEquals(streamD.weight(), newWeight);
verify(clientListener, never()).priorityTreeParentChanging(any(Http2Stream.class),
any(Http2Stream.class));
verify(clientListener, never()).priorityTreeParentChanged(any(Http2Stream.class),
any(Http2Stream.class));
}
@Test
public void removeShouldRestructureTree() throws Exception {
Http2Stream streamA = client.local().createStream(1).open(false);
Http2Stream streamB = client.local().createStream(3).open(false);
Http2Stream streamC = client.local().createStream(5).open(false);
Http2Stream streamD = client.local().createStream(7).open(false);
streamB.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
streamC.setPriority(streamB.id(), DEFAULT_PRIORITY_WEIGHT, false);
streamD.setPriority(streamB.id(), DEFAULT_PRIORITY_WEIGHT, false);
// Default removal policy will cause it to be removed immediately.
streamB.close();
// Level 0
Http2Stream p = client.connectionStream();
assertEquals(1, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
// Level 1
p = p.child(streamA.id());
assertNotNull(p);
assertEquals(0, p.parent().id());
assertEquals(2, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
// Level 2
p = p.child(streamC.id());
assertNotNull(p);
assertEquals(streamA.id(), p.parent().id());
assertEquals(0, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
p = p.parent().child(streamD.id());
assertNotNull(p);
assertEquals(streamA.id(), p.parent().id());
assertEquals(0, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
}
@Test
public void circularDependencyShouldRestructureTree() throws Exception {
// Using example from http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-5.3.3
// Initialize all the nodes
Http2Stream streamA = client.local().createStream(1).open(false);
verifyParentChanged(streamA, null);
Http2Stream streamB = client.local().createStream(3).open(false);
verifyParentChanged(streamB, null);
Http2Stream streamC = client.local().createStream(5).open(false);
verifyParentChanged(streamC, null);
Http2Stream streamD = client.local().createStream(7).open(false);
verifyParentChanged(streamD, null);
Http2Stream streamE = client.local().createStream(9).open(false);
verifyParentChanged(streamE, null);
Http2Stream streamF = client.local().createStream(11).open(false);
verifyParentChanged(streamF, null);
// Build the tree
streamB.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
verify(clientListener, never()).onWeightChanged(eq(streamB), anyShort());
verifyParentChanged(streamB, client.connectionStream());
verifyParentChanging(streamB, client.connectionStream());
streamC.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
verify(clientListener, never()).onWeightChanged(eq(streamC), anyShort());
verifyParentChanged(streamC, client.connectionStream());
verifyParentChanging(streamC, client.connectionStream());
streamD.setPriority(streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
verify(clientListener, never()).onWeightChanged(eq(streamD), anyShort());
verifyParentChanged(streamD, client.connectionStream());
verifyParentChanging(streamD, client.connectionStream());
streamE.setPriority(streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
verify(clientListener, never()).onWeightChanged(eq(streamE), anyShort());
verifyParentChanged(streamE, client.connectionStream());
verifyParentChanging(streamE, client.connectionStream());
streamF.setPriority(streamD.id(), DEFAULT_PRIORITY_WEIGHT, false);
verify(clientListener, never()).onWeightChanged(eq(streamF), anyShort());
verifyParentChanged(streamF, client.connectionStream());
verifyParentChanging(streamF, client.connectionStream());
assertEquals(6, client.numActiveStreams());
// Non-exclusive re-prioritization of a->d.
reset(clientListener);
streamA.setPriority(streamD.id(), DEFAULT_PRIORITY_WEIGHT, false);
verify(clientListener, never()).onWeightChanged(eq(streamA), anyShort());
verifyParentChanging(Arrays.asList(streamD, streamA), Arrays.asList(client.connectionStream(), streamD));
verifyParentsChanged(Arrays.asList(streamD, streamA), Arrays.asList(streamC, client.connectionStream()));
// Level 0
Http2Stream p = client.connectionStream();
assertEquals(1, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
// Level 1
p = p.child(streamD.id());
assertNotNull(p);
assertEquals(2, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
// Level 2
p = p.child(streamF.id());
assertNotNull(p);
assertEquals(0, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
p = p.parent().child(streamA.id());
assertNotNull(p);
assertEquals(2, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
// Level 3
p = p.child(streamB.id());
assertNotNull(p);
assertEquals(0, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
p = p.parent().child(streamC.id());
assertNotNull(p);
assertEquals(1, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
// Level 4;
p = p.child(streamE.id());
assertNotNull(p);
assertEquals(0, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
}
@Test
public void circularDependencyWithExclusiveShouldRestructureTree() throws Exception {
// Using example from http://tools.ietf.org/html/draft-ietf-httpbis-http2-16#section-5.3.3
// Initialize all the nodes
Http2Stream streamA = client.local().createStream(1).open(false);
verifyParentChanged(streamA, null);
Http2Stream streamB = client.local().createStream(3).open(false);
verifyParentChanged(streamB, null);
Http2Stream streamC = client.local().createStream(5).open(false);
verifyParentChanged(streamC, null);
Http2Stream streamD = client.local().createStream(7).open(false);
verifyParentChanged(streamD, null);
Http2Stream streamE = client.local().createStream(9).open(false);
verifyParentChanged(streamE, null);
Http2Stream streamF = client.local().createStream(11).open(false);
verifyParentChanged(streamF, null);
// Build the tree
streamB.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
verify(clientListener, never()).onWeightChanged(eq(streamB), anyShort());
verifyParentChanged(streamB, client.connectionStream());
verifyParentChanging(streamB, client.connectionStream());
streamC.setPriority(streamA.id(), DEFAULT_PRIORITY_WEIGHT, false);
verify(clientListener, never()).onWeightChanged(eq(streamC), anyShort());
verifyParentChanged(streamC, client.connectionStream());
verifyParentChanging(streamC, client.connectionStream());
streamD.setPriority(streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
verify(clientListener, never()).onWeightChanged(eq(streamD), anyShort());
verifyParentChanged(streamD, client.connectionStream());
verifyParentChanging(streamD, client.connectionStream());
streamE.setPriority(streamC.id(), DEFAULT_PRIORITY_WEIGHT, false);
verify(clientListener, never()).onWeightChanged(eq(streamE), anyShort());
verifyParentChanged(streamE, client.connectionStream());
verifyParentChanging(streamE, client.connectionStream());
streamF.setPriority(streamD.id(), DEFAULT_PRIORITY_WEIGHT, false);
verify(clientListener, never()).onWeightChanged(eq(streamF), anyShort());
verifyParentChanged(streamF, client.connectionStream());
verifyParentChanging(streamF, client.connectionStream());
assertEquals(6, client.numActiveStreams());
// Exclusive re-prioritization of a->d.
reset(clientListener);
streamA.setPriority(streamD.id(), DEFAULT_PRIORITY_WEIGHT, true);
verify(clientListener, never()).onWeightChanged(eq(streamA), anyShort());
verifyParentChanging(Arrays.asList(streamD, streamA, streamF),
Arrays.asList(client.connectionStream(), streamD, streamA));
verifyParentsChanged(Arrays.asList(streamD, streamA, streamF),
Arrays.asList(streamC, client.connectionStream(), streamD));
// Level 0
Http2Stream p = client.connectionStream();
assertEquals(1, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
// Level 1
p = p.child(streamD.id());
assertNotNull(p);
assertEquals(1, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
// Level 2
p = p.child(streamA.id());
assertNotNull(p);
assertEquals(3, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
// Level 3
p = p.child(streamB.id());
assertNotNull(p);
assertEquals(0, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
p = p.parent().child(streamF.id());
assertNotNull(p);
assertEquals(0, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
p = p.parent().child(streamC.id());
assertNotNull(p);
assertEquals(1, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
// Level 4;
p = p.child(streamE.id());
assertNotNull(p);
assertEquals(0, p.numChildren());
assertEquals(p.numChildren() * DEFAULT_PRIORITY_WEIGHT, p.totalChildWeights());
}
private void verifyParentChanging(List<Http2Stream> expectedArg1, List<Http2Stream> expectedArg2) {
assertSame(expectedArg1.size(), expectedArg2.size());
ArgumentCaptor<Http2Stream> arg1Captor = ArgumentCaptor.forClass(Http2Stream.class);
ArgumentCaptor<Http2Stream> arg2Captor = ArgumentCaptor.forClass(Http2Stream.class);
verify(clientListener, times(expectedArg1.size())).priorityTreeParentChanging(arg1Captor.capture(),
arg2Captor.capture());
List<Http2Stream> capturedArg1 = arg1Captor.getAllValues();
List<Http2Stream> capturedArg2 = arg2Captor.getAllValues();
assertSame(capturedArg1.size(), capturedArg2.size());
assertSame(capturedArg1.size(), expectedArg1.size());
for (int i = 0; i < capturedArg1.size(); ++i) {
assertEquals(expectedArg1.get(i), capturedArg1.get(i));
assertEquals(expectedArg2.get(i), capturedArg2.get(i));
}
}
private void verifyParentsChanged(List<Http2Stream> expectedArg1, List<Http2Stream> expectedArg2) {
assertSame(expectedArg1.size(), expectedArg2.size());
ArgumentCaptor<Http2Stream> arg1Captor = ArgumentCaptor.forClass(Http2Stream.class);
ArgumentCaptor<Http2Stream> arg2Captor = ArgumentCaptor.forClass(Http2Stream.class);
verify(clientListener, times(expectedArg1.size())).priorityTreeParentChanged(arg1Captor.capture(),
arg2Captor.capture());
List<Http2Stream> capturedArg1 = arg1Captor.getAllValues();
List<Http2Stream> capturedArg2 = arg2Captor.getAllValues();
assertSame(capturedArg1.size(), capturedArg2.size());
assertSame(capturedArg1.size(), expectedArg1.size());
for (int i = 0; i < capturedArg1.size(); ++i) {
assertEquals(expectedArg1.get(i), capturedArg1.get(i));
assertEquals(expectedArg2.get(i), capturedArg2.get(i));
}
}
private static void verifyDependUponIdleStream(Http2Stream streamA, Http2Stream streamB, Endpoint<?> endpoint) {
assertNotNull(streamB);
assertEquals(streamB.id(), endpoint.lastStreamCreated());
assertEquals(State.IDLE, streamB.state());
assertEquals(MIN_WEIGHT, streamA.weight());
assertEquals(DEFAULT_PRIORITY_WEIGHT, streamB.weight());
assertEquals(streamB, streamA.parent());
assertEquals(1, streamB.numChildren());
assertEquals(streamA, streamB.children().iterator().next());
}
@SuppressWarnings("unchecked")
private static <T> T streamEq(T stream) {
return (T) (stream == null ? isNull(Http2Stream.class) : eq(stream));
}
private void verifyParentChanging(Http2Stream stream, Http2Stream newParent) {
verify(clientListener).priorityTreeParentChanging(streamEq(stream), streamEq(newParent));
}
private void verifyParentChanged(Http2Stream stream, Http2Stream oldParent) {
verify(clientListener).priorityTreeParentChanged(streamEq(stream), streamEq(oldParent));
}
}

View File

@ -0,0 +1,488 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_UNSIGNED_INT;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.randomString;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.Unpooled;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.util.CharsetUtil;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Integration tests for {@link DefaultHttp2FrameReader} and {@link DefaultHttp2FrameWriter}.
*/
public class DefaultHttp2FrameIOTest {
private DefaultHttp2FrameReader reader;
private DefaultHttp2FrameWriter writer;
private ByteBufAllocator alloc;
@Mock
private ChannelHandlerContext ctx;
@Mock
private Http2FrameListener listener;
@Mock
private ChannelPromise promise;
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
alloc = UnpooledByteBufAllocator.DEFAULT;
when(ctx.alloc()).thenReturn(alloc);
reader = new DefaultHttp2FrameReader();
writer = new DefaultHttp2FrameWriter();
}
@Test
public void emptyDataShouldRoundtrip() throws Exception {
final ByteBuf data = Unpooled.EMPTY_BUFFER;
writer.writeData(ctx, 1000, data, 0, false, promise);
ByteBuf frame = null;
try {
frame = captureWrite();
reader.readFrame(ctx, frame, listener);
verify(listener).onDataRead(eq(ctx), eq(1000), eq(data), eq(0), eq(false));
} finally {
if (frame != null) {
frame.release();
}
data.release();
}
}
@Test
public void dataShouldRoundtrip() throws Exception {
final ByteBuf data = dummyData();
writer.writeData(ctx, 1000, data.retain().duplicate(), 0, false, promise);
ByteBuf frame = null;
try {
frame = captureWrite();
reader.readFrame(ctx, frame, listener);
verify(listener).onDataRead(eq(ctx), eq(1000), eq(data), eq(0), eq(false));
} finally {
if (frame != null) {
frame.release();
}
data.release();
}
}
@Test
public void dataWithPaddingShouldRoundtrip() throws Exception {
final ByteBuf data = dummyData();
writer.writeData(ctx, 1, data.retain().duplicate(), 0xFF, true, promise);
ByteBuf frame = null;
try {
frame = captureWrite();
reader.readFrame(ctx, frame, listener);
verify(listener).onDataRead(eq(ctx), eq(1), eq(data), eq(0xFF), eq(true));
} finally {
if (frame != null) {
frame.release();
}
data.release();
}
}
@Test
public void priorityShouldRoundtrip() throws Exception {
writer.writePriority(ctx, 1, 2, (short) 255, true, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onPriorityRead(eq(ctx), eq(1), eq(2), eq((short) 255), eq(true));
} finally {
frame.release();
}
}
@Test
public void rstStreamShouldRoundtrip() throws Exception {
writer.writeRstStream(ctx, 1, MAX_UNSIGNED_INT, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onRstStreamRead(eq(ctx), eq(1), eq(MAX_UNSIGNED_INT));
} finally {
frame.release();
}
}
@Test
public void emptySettingsShouldRoundtrip() throws Exception {
writer.writeSettings(ctx, new Http2Settings(), promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onSettingsRead(eq(ctx), eq(new Http2Settings()));
} finally {
frame.release();
}
}
@Test
public void settingsShouldStripShouldRoundtrip() throws Exception {
Http2Settings settings = new Http2Settings();
settings.pushEnabled(true);
settings.headerTableSize(4096);
settings.initialWindowSize(123);
settings.maxConcurrentStreams(456);
writer.writeSettings(ctx, settings, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onSettingsRead(eq(ctx), eq(settings));
} finally {
frame.release();
}
}
@Test
public void settingsAckShouldRoundtrip() throws Exception {
writer.writeSettingsAck(ctx, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onSettingsAckRead(eq(ctx));
} finally {
frame.release();
}
}
@Test
public void pingShouldRoundtrip() throws Exception {
ByteBuf data = dummyData();
writer.writePing(ctx, false, data.retain().duplicate(), promise);
ByteBuf frame = null;
try {
frame = captureWrite();
reader.readFrame(ctx, frame, listener);
verify(listener).onPingRead(eq(ctx), eq(data));
} finally {
if (frame != null) {
frame.release();
}
data.release();
}
}
@Test
public void pingAckShouldRoundtrip() throws Exception {
ByteBuf data = dummyData();
writer.writePing(ctx, true, data.retain().duplicate(), promise);
ByteBuf frame = null;
try {
frame = captureWrite();
reader.readFrame(ctx, frame, listener);
verify(listener).onPingAckRead(eq(ctx), eq(data));
} finally {
if (frame != null) {
frame.release();
}
data.release();
}
}
@Test
public void goAwayShouldRoundtrip() throws Exception {
ByteBuf data = dummyData();
writer.writeGoAway(ctx, 1, MAX_UNSIGNED_INT, data.retain().duplicate(), promise);
ByteBuf frame = null;
try {
frame = captureWrite();
reader.readFrame(ctx, frame, listener);
verify(listener).onGoAwayRead(eq(ctx), eq(1), eq(MAX_UNSIGNED_INT), eq(data));
} finally {
if (frame != null) {
frame.release();
}
data.release();
}
}
@Test
public void windowUpdateShouldRoundtrip() throws Exception {
writer.writeWindowUpdate(ctx, 1, Integer.MAX_VALUE, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onWindowUpdateRead(eq(ctx), eq(1), eq(Integer.MAX_VALUE));
} finally {
frame.release();
}
}
@Test
public void emptyHeadersShouldRoundtrip() throws Exception {
Http2Headers headers = EmptyHttp2Headers.INSTANCE;
writer.writeHeaders(ctx, 1, headers, 0, true, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onHeadersRead(eq(ctx), eq(1), eq(headers), eq(0), eq(true));
} finally {
frame.release();
}
}
@Test
public void emptyHeadersWithPaddingShouldRoundtrip() throws Exception {
Http2Headers headers = EmptyHttp2Headers.INSTANCE;
writer.writeHeaders(ctx, 1, headers, 0xFF, true, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onHeadersRead(eq(ctx), eq(1), eq(headers), eq(0xFF), eq(true));
} finally {
frame.release();
}
}
@Test
public void binaryHeadersWithoutPriorityShouldRoundtrip() throws Exception {
Http2Headers headers = dummyBinaryHeaders();
writer.writeHeaders(ctx, 1, headers, 0, true, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onHeadersRead(eq(ctx), eq(1), eq(headers), eq(0), eq(true));
} finally {
frame.release();
}
}
@Test
public void headersWithoutPriorityShouldRoundtrip() throws Exception {
Http2Headers headers = dummyHeaders();
writer.writeHeaders(ctx, 1, headers, 0, true, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onHeadersRead(eq(ctx), eq(1), eq(headers), eq(0), eq(true));
} finally {
frame.release();
}
}
@Test
public void headersWithPaddingWithoutPriorityShouldRoundtrip() throws Exception {
Http2Headers headers = dummyHeaders();
writer.writeHeaders(ctx, 1, headers, 0xFF, true, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onHeadersRead(eq(ctx), eq(1), eq(headers), eq(0xFF), eq(true));
} finally {
frame.release();
}
}
@Test
public void headersWithPriorityShouldRoundtrip() throws Exception {
Http2Headers headers = dummyHeaders();
writer.writeHeaders(ctx, 1, headers, 2, (short) 3, true, 0, true, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener)
.onHeadersRead(eq(ctx), eq(1), eq(headers), eq(2), eq((short) 3), eq(true), eq(0), eq(true));
} finally {
frame.release();
}
}
@Test
public void headersWithPaddingWithPriorityShouldRoundtrip() throws Exception {
Http2Headers headers = dummyHeaders();
writer.writeHeaders(ctx, 1, headers, 2, (short) 3, true, 0xFF, true, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onHeadersRead(eq(ctx), eq(1), eq(headers), eq(2), eq((short) 3), eq(true), eq(0xFF),
eq(true));
} finally {
frame.release();
}
}
@Test
public void continuedHeadersShouldRoundtrip() throws Exception {
Http2Headers headers = largeHeaders();
writer.writeHeaders(ctx, 1, headers, 2, (short) 3, true, 0, true, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener)
.onHeadersRead(eq(ctx), eq(1), eq(headers), eq(2), eq((short) 3), eq(true), eq(0), eq(true));
} finally {
frame.release();
}
}
@Test
public void continuedHeadersWithPaddingShouldRoundtrip() throws Exception {
Http2Headers headers = largeHeaders();
writer.writeHeaders(ctx, 1, headers, 2, (short) 3, true, 0xFF, true, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onHeadersRead(eq(ctx), eq(1), eq(headers), eq(2), eq((short) 3), eq(true), eq(0xFF),
eq(true));
} finally {
frame.release();
}
}
@Test
public void emptypushPromiseShouldRoundtrip() throws Exception {
Http2Headers headers = EmptyHttp2Headers.INSTANCE;
writer.writePushPromise(ctx, 1, 2, headers, 0, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onPushPromiseRead(eq(ctx), eq(1), eq(2), eq(headers), eq(0));
} finally {
frame.release();
}
}
@Test
public void pushPromiseShouldRoundtrip() throws Exception {
Http2Headers headers = dummyHeaders();
writer.writePushPromise(ctx, 1, 2, headers, 0, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onPushPromiseRead(eq(ctx), eq(1), eq(2), eq(headers), eq(0));
} finally {
frame.release();
}
}
@Test
public void pushPromiseWithPaddingShouldRoundtrip() throws Exception {
Http2Headers headers = dummyHeaders();
writer.writePushPromise(ctx, 1, 2, headers, 0xFF, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onPushPromiseRead(eq(ctx), eq(1), eq(2), eq(headers), eq(0xFF));
} finally {
frame.release();
}
}
@Test
public void continuedPushPromiseShouldRoundtrip() throws Exception {
Http2Headers headers = largeHeaders();
writer.writePushPromise(ctx, 1, 2, headers, 0, promise);
ByteBuf frame = captureWrite();
reader.readFrame(ctx, frame, listener);
verify(listener).onPushPromiseRead(eq(ctx), eq(1), eq(2), eq(headers), eq(0));
frame.release();
}
@Test
public void continuedPushPromiseWithPaddingShouldRoundtrip() throws Exception {
Http2Headers headers = largeHeaders();
writer.writePushPromise(ctx, 1, 2, headers, 0xFF, promise);
ByteBuf frame = captureWrite();
try {
reader.readFrame(ctx, frame, listener);
verify(listener).onPushPromiseRead(eq(ctx), eq(1), eq(2), eq(headers), eq(0xFF));
} finally {
frame.release();
}
}
private ByteBuf captureWrite() {
ArgumentCaptor<ByteBuf> captor = ArgumentCaptor.forClass(ByteBuf.class);
verify(ctx).write(captor.capture(), eq(promise));
return captor.getValue();
}
private ByteBuf dummyData() {
return alloc.buffer().writeBytes("abcdefgh".getBytes(CharsetUtil.UTF_8));
}
private static Http2Headers dummyBinaryHeaders() {
DefaultHttp2Headers headers = new DefaultHttp2Headers();
for (int ix = 0; ix < 10; ++ix) {
headers.add(randomString(), randomString());
}
return headers;
}
private static Http2Headers dummyHeaders() {
return new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path"))
.add(as("accept"), as("*/*"));
}
private static Http2Headers largeHeaders() {
DefaultHttp2Headers headers = new DefaultHttp2Headers();
for (int i = 0; i < 100; ++i) {
String key = "this-is-a-test-header-key-" + i;
String value = "this-is-a-test-header-value-" + i;
headers.add(as(key), as(value));
}
return headers;
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_HEADER_TABLE_SIZE;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.randomBytes;
import static io.netty.util.CharsetUtil.UTF_8;
import static org.junit.Assert.assertEquals;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import java.io.ByteArrayOutputStream;
import org.junit.Before;
import org.junit.Test;
import com.twitter.hpack.Encoder;
/**
* Tests for {@link DefaultHttp2HeadersDecoder}.
*/
public class DefaultHttp2HeadersDecoderTest {
private DefaultHttp2HeadersDecoder decoder;
@Before
public void setup() {
decoder = new DefaultHttp2HeadersDecoder();
}
@Test
public void decodeShouldSucceed() throws Exception {
ByteBuf buf = encode(b(":method"), b("GET"), b("akey"), b("avalue"), randomBytes(), randomBytes());
try {
Http2Headers headers = decoder.decodeHeaders(buf);
assertEquals(3, headers.size());
assertEquals("GET", headers.method().toString());
assertEquals("avalue", headers.get(as("akey")).toString());
} finally {
buf.release();
}
}
private static byte[] b(String string) {
return string.getBytes(UTF_8);
}
private static ByteBuf encode(byte[]... entries) throws Exception {
Encoder encoder = new Encoder(MAX_HEADER_TABLE_SIZE);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
for (int ix = 0; ix < entries.length;) {
byte[] key = entries[ix++];
byte[] value = entries[ix++];
encoder.encodeHeader(stream, key, value, false);
}
return Unpooled.wrappedBuffer(stream.toByteArray());
}
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static org.junit.Assert.assertTrue;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.junit.Before;
import org.junit.Test;
/**
* Tests for {@link DefaultHttp2HeadersEncoder}.
*/
public class DefaultHttp2HeadersEncoderTest {
private DefaultHttp2HeadersEncoder encoder;
@Before
public void setup() {
encoder = new DefaultHttp2HeadersEncoder();
}
@Test
public void encodeShouldSucceed() throws Http2Exception {
Http2Headers headers = headers();
ByteBuf buf = Unpooled.buffer();
try {
encoder.encodeHeaders(headers, buf);
assertTrue(buf.writerIndex() > 0);
} finally {
buf.release();
}
}
@Test(expected = Http2Exception.class)
public void headersExceedMaxSetSizeShouldFail() throws Http2Exception {
Http2Headers headers = headers();
encoder.headerTable().maxHeaderListSize(2);
encoder.encodeHeaders(headers, Unpooled.buffer());
}
private static Http2Headers headers() {
return new DefaultHttp2Headers().method(as("GET")).add(as("a"), as("1"))
.add(as("a"), as("2"));
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version
* 2.0 (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.handler.codec.AsciiString;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
public class DefaultHttp2HeadersTest {
private static final AsciiString NAME = new AsciiString("Test");
private static final AsciiString VALUE = new AsciiString("some value");
@Test
public void defaultLowercase() {
Http2Headers headers = new DefaultHttp2Headers().set(NAME, VALUE);
assertEquals(first(headers), NAME.toLowerCase());
}
@Test
public void caseInsensitive() {
Http2Headers headers = new DefaultHttp2Headers(false).set(NAME, VALUE);
assertEquals(first(headers), NAME);
}
private static AsciiString first(Http2Headers headers) {
return headers.names().iterator().next();
}
}

View File

@ -0,0 +1,296 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.CONNECTION_STREAM_ID;
import static io.netty.handler.codec.http2.Http2CodecUtil.DEFAULT_WINDOW_SIZE;
import static org.junit.Assert.assertEquals;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Tests for {@link DefaultHttp2LocalFlowController}.
*/
public class DefaultHttp2LocalFlowControllerTest {
private static final int STREAM_ID = 1;
private DefaultHttp2LocalFlowController controller;
@Mock
private ByteBuf buffer;
@Mock
private Http2FrameWriter frameWriter;
@Mock
private ChannelHandlerContext ctx;
@Mock
private ChannelPromise promise;
private DefaultHttp2Connection connection;
private static float updateRatio = 0.5f;
@Before
public void setup() throws Http2Exception {
MockitoAnnotations.initMocks(this);
when(ctx.newPromise()).thenReturn(promise);
connection = new DefaultHttp2Connection(false);
controller = new DefaultHttp2LocalFlowController(connection, frameWriter, updateRatio);
connection.local().createStream(STREAM_ID).open(false);
}
@Test
public void dataFrameShouldBeAccepted() throws Http2Exception {
receiveFlowControlledFrame(STREAM_ID, 10, 0, false);
verifyWindowUpdateNotSent();
}
@Test
public void windowUpdateShouldSendOnceBytesReturned() throws Http2Exception {
int dataSize = (int) (DEFAULT_WINDOW_SIZE * updateRatio) + 1;
receiveFlowControlledFrame(STREAM_ID, dataSize, 0, false);
// Return only a few bytes and verify that the WINDOW_UPDATE hasn't been sent.
consumeBytes(STREAM_ID, 10);
verifyWindowUpdateNotSent(CONNECTION_STREAM_ID);
// Return the rest and verify the WINDOW_UPDATE is sent.
consumeBytes(STREAM_ID, dataSize - 10);
verifyWindowUpdateSent(STREAM_ID, dataSize);
verifyWindowUpdateSent(CONNECTION_STREAM_ID, dataSize);
}
@Test(expected = Http2Exception.class)
public void connectionFlowControlExceededShouldThrow() throws Http2Exception {
// Window exceeded because of the padding.
receiveFlowControlledFrame(STREAM_ID, DEFAULT_WINDOW_SIZE, 1, true);
}
@Test
public void windowUpdateShouldNotBeSentAfterEndOfStream() throws Http2Exception {
int dataSize = (int) (DEFAULT_WINDOW_SIZE * updateRatio) + 1;
// Set end-of-stream on the frame, so no window update will be sent for the stream.
receiveFlowControlledFrame(STREAM_ID, dataSize, 0, true);
verifyWindowUpdateNotSent(CONNECTION_STREAM_ID);
verifyWindowUpdateNotSent(STREAM_ID);
consumeBytes(STREAM_ID, dataSize);
verifyWindowUpdateSent(CONNECTION_STREAM_ID, dataSize);
verifyWindowUpdateNotSent(STREAM_ID);
}
@Test
public void halfWindowRemainingShouldUpdateAllWindows() throws Http2Exception {
int dataSize = (int) (DEFAULT_WINDOW_SIZE * updateRatio) + 1;
int initialWindowSize = DEFAULT_WINDOW_SIZE;
int windowDelta = getWindowDelta(initialWindowSize, initialWindowSize, dataSize);
// Don't set end-of-stream so we'll get a window update for the stream as well.
receiveFlowControlledFrame(STREAM_ID, dataSize, 0, false);
consumeBytes(STREAM_ID, dataSize);
verifyWindowUpdateSent(CONNECTION_STREAM_ID, windowDelta);
verifyWindowUpdateSent(STREAM_ID, windowDelta);
}
@Test
public void initialWindowUpdateShouldAllowMoreFrames() throws Http2Exception {
// Send a frame that takes up the entire window.
int initialWindowSize = DEFAULT_WINDOW_SIZE;
receiveFlowControlledFrame(STREAM_ID, initialWindowSize, 0, false);
assertEquals(0, window(STREAM_ID));
assertEquals(0, window(CONNECTION_STREAM_ID));
consumeBytes(STREAM_ID, initialWindowSize);
assertEquals(initialWindowSize, window(STREAM_ID));
assertEquals(DEFAULT_WINDOW_SIZE, window(CONNECTION_STREAM_ID));
// Update the initial window size to allow another frame.
int newInitialWindowSize = 2 * initialWindowSize;
controller.initialWindowSize(newInitialWindowSize);
assertEquals(newInitialWindowSize, window(STREAM_ID));
assertEquals(DEFAULT_WINDOW_SIZE, window(CONNECTION_STREAM_ID));
// Clear any previous calls to the writer.
reset(frameWriter);
// Send the next frame and verify that the expected window updates were sent.
receiveFlowControlledFrame(STREAM_ID, initialWindowSize, 0, false);
consumeBytes(STREAM_ID, initialWindowSize);
int delta = newInitialWindowSize - initialWindowSize;
verifyWindowUpdateSent(STREAM_ID, delta);
verifyWindowUpdateSent(CONNECTION_STREAM_ID, delta);
}
@Test
public void connectionWindowShouldAdjustWithMultipleStreams() throws Http2Exception {
int newStreamId = 3;
connection.local().createStream(newStreamId).open(false);
try {
assertEquals(DEFAULT_WINDOW_SIZE, window(STREAM_ID));
assertEquals(DEFAULT_WINDOW_SIZE, window(CONNECTION_STREAM_ID));
// Test that both stream and connection window are updated (or not updated) together
int data1 = (int) (DEFAULT_WINDOW_SIZE * updateRatio) + 1;
receiveFlowControlledFrame(STREAM_ID, data1, 0, false);
verifyWindowUpdateNotSent(STREAM_ID);
verifyWindowUpdateNotSent(CONNECTION_STREAM_ID);
assertEquals(DEFAULT_WINDOW_SIZE - data1, window(STREAM_ID));
assertEquals(DEFAULT_WINDOW_SIZE - data1, window(CONNECTION_STREAM_ID));
consumeBytes(STREAM_ID, data1);
verifyWindowUpdateSent(STREAM_ID, data1);
verifyWindowUpdateSent(CONNECTION_STREAM_ID, data1);
reset(frameWriter);
// Create a scenario where data is depleted from multiple streams, but not enough data
// to generate a window update on those streams. The amount will be enough to generate
// a window update for the connection stream.
--data1;
int data2 = data1 >> 1;
receiveFlowControlledFrame(STREAM_ID, data1, 0, false);
receiveFlowControlledFrame(newStreamId, data1, 0, false);
verifyWindowUpdateNotSent(STREAM_ID);
verifyWindowUpdateNotSent(newStreamId);
verifyWindowUpdateNotSent(CONNECTION_STREAM_ID);
assertEquals(DEFAULT_WINDOW_SIZE - data1, window(STREAM_ID));
assertEquals(DEFAULT_WINDOW_SIZE - data1, window(newStreamId));
assertEquals(DEFAULT_WINDOW_SIZE - (data1 << 1), window(CONNECTION_STREAM_ID));
consumeBytes(STREAM_ID, data1);
consumeBytes(newStreamId, data2);
verifyWindowUpdateNotSent(STREAM_ID);
verifyWindowUpdateNotSent(newStreamId);
verifyWindowUpdateSent(CONNECTION_STREAM_ID, data1 + data2);
assertEquals(DEFAULT_WINDOW_SIZE - data1, window(STREAM_ID));
assertEquals(DEFAULT_WINDOW_SIZE - data1, window(newStreamId));
assertEquals(DEFAULT_WINDOW_SIZE - (data1 - data2), window(CONNECTION_STREAM_ID));
} finally {
connection.stream(newStreamId).close();
}
}
@Test
public void globalRatioShouldImpactStreams() throws Http2Exception {
float ratio = 0.6f;
controller.windowUpdateRatio(ratio);
testRatio(ratio, DEFAULT_WINDOW_SIZE << 1, 3, false);
}
@Test
public void streamlRatioShouldImpactStreams() throws Http2Exception {
float ratio = 0.6f;
testRatio(ratio, DEFAULT_WINDOW_SIZE << 1, 3, true);
}
private void testRatio(float ratio, int newDefaultWindowSize, int newStreamId, boolean setStreamRatio)
throws Http2Exception {
int delta = newDefaultWindowSize - DEFAULT_WINDOW_SIZE;
controller.incrementWindowSize(ctx, stream(0), delta);
Http2Stream stream = connection.local().createStream(newStreamId).open(false);
if (setStreamRatio) {
controller.windowUpdateRatio(ctx, stream, ratio);
}
controller.incrementWindowSize(ctx, stream, delta);
reset(frameWriter);
try {
int data1 = (int) (newDefaultWindowSize * ratio) + 1;
int data2 = (int) (DEFAULT_WINDOW_SIZE * updateRatio) >> 1;
receiveFlowControlledFrame(STREAM_ID, data2, 0, false);
receiveFlowControlledFrame(newStreamId, data1, 0, false);
verifyWindowUpdateNotSent(STREAM_ID);
verifyWindowUpdateNotSent(newStreamId);
verifyWindowUpdateNotSent(CONNECTION_STREAM_ID);
assertEquals(DEFAULT_WINDOW_SIZE - data2, window(STREAM_ID));
assertEquals(newDefaultWindowSize - data1, window(newStreamId));
assertEquals(newDefaultWindowSize - data2 - data1, window(CONNECTION_STREAM_ID));
consumeBytes(STREAM_ID, data2);
consumeBytes(newStreamId, data1);
verifyWindowUpdateNotSent(STREAM_ID);
verifyWindowUpdateSent(newStreamId, data1);
verifyWindowUpdateSent(CONNECTION_STREAM_ID, data1 + data2);
assertEquals(DEFAULT_WINDOW_SIZE - data2, window(STREAM_ID));
assertEquals(newDefaultWindowSize, window(newStreamId));
assertEquals(newDefaultWindowSize, window(CONNECTION_STREAM_ID));
} finally {
connection.stream(newStreamId).close();
}
}
private static int getWindowDelta(int initialSize, int windowSize, int dataSize) {
int newWindowSize = windowSize - dataSize;
return initialSize - newWindowSize;
}
private void receiveFlowControlledFrame(int streamId, int dataSize, int padding,
boolean endOfStream) throws Http2Exception {
final ByteBuf buf = dummyData(dataSize);
try {
controller.receiveFlowControlledFrame(ctx, stream(streamId), buf, padding, endOfStream);
} finally {
buf.release();
}
}
private static ByteBuf dummyData(int size) {
final ByteBuf buffer = Unpooled.buffer(size);
buffer.writerIndex(size);
return buffer;
}
private void consumeBytes(int streamId, int numBytes) throws Http2Exception {
controller.consumeBytes(ctx, stream(streamId), numBytes);
}
private void verifyWindowUpdateSent(int streamId, int windowSizeIncrement) throws Http2Exception {
verify(frameWriter).writeWindowUpdate(eq(ctx), eq(streamId), eq(windowSizeIncrement), eq(promise));
}
private void verifyWindowUpdateNotSent(int streamId) {
verify(frameWriter, never()).writeWindowUpdate(eq(ctx), eq(streamId), anyInt(), eq(promise));
}
private void verifyWindowUpdateNotSent() {
verify(frameWriter, never()).writeWindowUpdate(any(ChannelHandlerContext.class), anyInt(), anyInt(),
any(ChannelPromise.class));
}
private int window(int streamId) throws Http2Exception {
return controller.windowSize(stream(streamId));
}
private Http2Stream stream(int streamId) throws Http2Exception {
return connection.requireStream(streamId);
}
}

View File

@ -0,0 +1,195 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.buffer.Unpooled.copiedBuffer;
import static io.netty.handler.codec.http2.Http2CodecUtil.connectionPrefaceBuf;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.util.CharsetUtil.UTF_8;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.DefaultChannelPromise;
import java.util.Collections;
import java.util.List;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Matchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/**
* Tests for {@link Http2ConnectionHandler}
*/
public class Http2ConnectionHandlerTest {
private static final int STREAM_ID = 1;
private Http2ConnectionHandler handler;
@Mock
private Http2Connection connection;
@Mock
private Http2Connection.Endpoint<Http2RemoteFlowController> remote;
@Mock
private Http2Connection.Endpoint<Http2LocalFlowController> local;
@Mock
private ChannelHandlerContext ctx;
@Mock
private Channel channel;
private ChannelPromise promise;
@Mock
private ChannelFuture future;
@Mock
private Http2Stream stream;
@Mock
private Http2ConnectionDecoder.Builder decoderBuilder;
@Mock
private Http2ConnectionEncoder.Builder encoderBuilder;
@Mock
private Http2ConnectionDecoder decoder;
@Mock
private Http2ConnectionEncoder encoder;
@Mock
private Http2FrameWriter frameWriter;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
promise = new DefaultChannelPromise(channel);
when(encoderBuilder.build()).thenReturn(encoder);
when(decoderBuilder.build()).thenReturn(decoder);
when(encoder.connection()).thenReturn(connection);
when(decoder.connection()).thenReturn(connection);
when(encoder.frameWriter()).thenReturn(frameWriter);
when(frameWriter.writeGoAway(eq(ctx), anyInt(), anyInt(), any(ByteBuf.class), eq(promise))).thenReturn(future);
when(channel.isActive()).thenReturn(true);
when(connection.remote()).thenReturn(remote);
when(connection.local()).thenReturn(local);
when(connection.activeStreams()).thenReturn(Collections.singletonList(stream));
when(stream.open(anyBoolean())).thenReturn(stream);
doAnswer(new Answer<Http2Stream>() {
@Override
public Http2Stream answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
return local.createStream((Integer) args[0]);
}
}).when(connection).createLocalStream(anyInt());
doAnswer(new Answer<Http2Stream>() {
@Override
public Http2Stream answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
return remote.createStream((Integer) args[0]);
}
}).when(connection).createRemoteStream(anyInt());
when(encoder.writeSettings(eq(ctx), any(Http2Settings.class), eq(promise))).thenReturn(future);
when(ctx.alloc()).thenReturn(UnpooledByteBufAllocator.DEFAULT);
when(ctx.channel()).thenReturn(channel);
when(ctx.newSucceededFuture()).thenReturn(future);
when(ctx.newPromise()).thenReturn(promise);
when(ctx.write(any())).thenReturn(future);
handler = newHandler();
}
private Http2ConnectionHandler newHandler() {
return new Http2ConnectionHandler(decoderBuilder, encoderBuilder);
}
@After
public void tearDown() throws Exception {
handler.handlerRemoved(ctx);
}
@Test
public void clientShouldSendClientPrefaceStringWhenActive() throws Exception {
when(connection.isServer()).thenReturn(false);
handler.channelActive(ctx);
verify(ctx).write(eq(connectionPrefaceBuf()));
}
@Test
public void serverShouldNotSendClientPrefaceStringWhenActive() throws Exception {
when(connection.isServer()).thenReturn(true);
handler.channelActive(ctx);
verify(ctx, never()).write(eq(connectionPrefaceBuf()));
}
@Test
public void serverReceivingInvalidClientPrefaceStringShouldHandleException() throws Exception {
when(connection.isServer()).thenReturn(true);
handler = newHandler();
handler.channelRead(ctx, copiedBuffer("BAD_PREFACE", UTF_8));
ArgumentCaptor<ByteBuf> captor = ArgumentCaptor.forClass(ByteBuf.class);
verify(frameWriter).writeGoAway(eq(ctx), eq(0), eq(PROTOCOL_ERROR.code()),
captor.capture(), eq(promise));
captor.getValue().release();
}
@Test
public void serverReceivingValidClientPrefaceStringShouldContinueReadingFrames() throws Exception {
when(connection.isServer()).thenReturn(true);
handler.channelRead(ctx, connectionPrefaceBuf());
verify(decoder).decodeFrame(eq(ctx), any(ByteBuf.class), Matchers.<List<Object>>any());
}
@Test
public void channelInactiveShouldCloseStreams() throws Exception {
handler.channelInactive(ctx);
verify(stream).close();
}
@Test
public void connectionErrorShouldStartShutdown() throws Exception {
Http2Exception e = new Http2Exception(PROTOCOL_ERROR);
when(remote.lastStreamCreated()).thenReturn(STREAM_ID);
handler.exceptionCaught(ctx, e);
ArgumentCaptor<ByteBuf> captor = ArgumentCaptor.forClass(ByteBuf.class);
verify(frameWriter).writeGoAway(eq(ctx), eq(STREAM_ID), eq(PROTOCOL_ERROR.code()),
captor.capture(), eq(promise));
captor.getValue().release();
}
}

View File

@ -0,0 +1,480 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.randomString;
import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel;
import static io.netty.handler.codec.http2.Http2TestUtil.FrameCountDown;
import static io.netty.util.CharsetUtil.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http2.Http2TestUtil.Http2Runnable;
import io.netty.util.NetUtil;
import io.netty.util.concurrent.Future;
import java.io.ByteArrayOutputStream;
import java.net.InetSocketAddress;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/**
* Tests the full HTTP/2 framing stack including the connection and preface handlers.
*/
public class Http2ConnectionRoundtripTest {
@Mock
private Http2FrameListener clientListener;
@Mock
private Http2FrameListener serverListener;
private Http2ConnectionHandler http2Client;
private ServerBootstrap sb;
private Bootstrap cb;
private Channel serverChannel;
private Channel clientChannel;
private FrameCountDown serverFrameCountDown;
private CountDownLatch requestLatch;
private CountDownLatch serverSettingsAckLatch;
private CountDownLatch dataLatch;
private CountDownLatch trailersLatch;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
mockFlowControl(clientListener);
mockFlowControl(serverListener);
}
@After
public void teardown() throws Exception {
serverChannel.close().sync();
Future<?> serverGroup = sb.group().shutdownGracefully(0, 0, MILLISECONDS);
Future<?> serverChildGroup = sb.childGroup().shutdownGracefully(0, 0, MILLISECONDS);
Future<?> clientGroup = cb.group().shutdownGracefully(0, 0, MILLISECONDS);
serverGroup.sync();
serverChildGroup.sync();
clientGroup.sync();
}
@Test
public void http2ExceptionInPipelineShouldCloseConnection() throws Exception {
bootstrapEnv(1, 1, 1, 1);
// Create a latch to track when the close occurs.
final CountDownLatch closeLatch = new CountDownLatch(1);
clientChannel.closeFuture().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
closeLatch.countDown();
}
});
// Create a single stream by sending a HEADERS frame to the server.
final Http2Headers headers = dummyHeaders();
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0, false,
newPromise());
}
});
// Wait for the server to create the stream.
assertTrue(serverSettingsAckLatch.await(5, SECONDS));
assertTrue(requestLatch.await(5, SECONDS));
// Add a handler that will immediately throw an exception.
clientChannel.pipeline().addFirst(new ChannelHandlerAdapter() {
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
throw Http2Exception.connectionError(PROTOCOL_ERROR, "Fake Exception");
}
});
// Wait for the close to occur.
assertTrue(closeLatch.await(5, SECONDS));
assertFalse(clientChannel.isOpen());
}
@Test
public void listenerExceptionShouldCloseConnection() throws Exception {
final Http2Headers headers = dummyHeaders();
doThrow(new RuntimeException("Fake Exception")).when(serverListener).onHeadersRead(
any(ChannelHandlerContext.class), eq(3), eq(headers), eq(0), eq((short) 16),
eq(false), eq(0), eq(false));
bootstrapEnv(1, 0, 1, 1);
// Create a latch to track when the close occurs.
final CountDownLatch closeLatch = new CountDownLatch(1);
clientChannel.closeFuture().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
closeLatch.countDown();
}
});
// Create a single stream by sending a HEADERS frame to the server.
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0, false,
newPromise());
}
});
// Wait for the server to create the stream.
assertTrue(serverSettingsAckLatch.await(5, SECONDS));
assertTrue(requestLatch.await(5, SECONDS));
// Wait for the close to occur.
assertTrue(closeLatch.await(5, SECONDS));
assertFalse(clientChannel.isOpen());
}
@Test
public void nonHttp2ExceptionInPipelineShouldNotCloseConnection() throws Exception {
bootstrapEnv(1, 1, 1, 1);
// Create a latch to track when the close occurs.
final CountDownLatch closeLatch = new CountDownLatch(1);
clientChannel.closeFuture().addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture future) throws Exception {
closeLatch.countDown();
}
});
// Create a single stream by sending a HEADERS frame to the server.
final Http2Headers headers = dummyHeaders();
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0, false,
newPromise());
}
});
// Wait for the server to create the stream.
assertTrue(serverSettingsAckLatch.await(5, SECONDS));
assertTrue(requestLatch.await(5, SECONDS));
// Add a handler that will immediately throw an exception.
clientChannel.pipeline().addFirst(new ChannelHandlerAdapter() {
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
throw new RuntimeException("Fake Exception");
}
});
// The close should NOT occur.
assertFalse(closeLatch.await(5, SECONDS));
assertTrue(clientChannel.isOpen());
}
@Test
public void noMoreStreamIdsShouldSendGoAway() throws Exception {
bootstrapEnv(1, 1, 3, 1);
// Create a single stream by sending a HEADERS frame to the server.
final Http2Headers headers = dummyHeaders();
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0,
true, newPromise());
}
});
assertTrue(serverSettingsAckLatch.await(5, SECONDS));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
http2Client.encoder().writeHeaders(ctx(), Integer.MAX_VALUE + 1, headers, 0, (short) 16, false, 0,
true, newPromise());
}
});
assertTrue(requestLatch.await(5, SECONDS));
verify(serverListener).onGoAwayRead(any(ChannelHandlerContext.class), eq(0),
eq(Http2Error.PROTOCOL_ERROR.code()), any(ByteBuf.class));
}
@Test
public void flowControlProperlyChunksLargeMessage() throws Exception {
final Http2Headers headers = dummyHeaders();
// Create a large message to send.
final int length = 10485760; // 10MB
// Create a buffer filled with random bytes.
final ByteBuf data = randomBytes(length);
final ByteArrayOutputStream out = new ByteArrayOutputStream(length);
doAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock in) throws Throwable {
ByteBuf buf = (ByteBuf) in.getArguments()[2];
int padding = (Integer) in.getArguments()[3];
int processedBytes = buf.readableBytes() + padding;
buf.readBytes(out, buf.readableBytes());
return processedBytes;
}
}).when(serverListener).onDataRead(any(ChannelHandlerContext.class), eq(3),
any(ByteBuf.class), eq(0), anyBoolean());
try {
// Initialize the data latch based on the number of bytes expected.
bootstrapEnv(length, 1, 2, 1);
// Create the stream and send all of the data at once.
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0,
false, newPromise());
http2Client.encoder().writeData(ctx(), 3, data.retain(), 0, false, newPromise());
// Write trailers.
http2Client.encoder().writeHeaders(ctx(), 3, headers, 0, (short) 16, false, 0,
true, newPromise());
}
});
// Wait for the trailers to be received.
assertTrue(serverSettingsAckLatch.await(5, SECONDS));
assertTrue(trailersLatch.await(5, SECONDS));
// Verify that headers and trailers were received.
verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(3), eq(headers), eq(0),
eq((short) 16), eq(false), eq(0), eq(false));
verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(3), eq(headers), eq(0),
eq((short) 16), eq(false), eq(0), eq(true));
// Verify we received all the bytes.
assertEquals(0, dataLatch.getCount());
out.flush();
byte[] received = out.toByteArray();
assertArrayEquals(data.array(), received);
} finally {
data.release();
out.close();
}
}
@Test
public void stressTest() throws Exception {
final Http2Headers headers = dummyHeaders();
final String pingMsg = "12345678";
int length = 10;
final ByteBuf data = randomBytes(length);
final String dataAsHex = ByteBufUtil.hexDump(data);
final ByteBuf pingData = Unpooled.copiedBuffer(pingMsg, UTF_8);
final int numStreams = 2000;
// Collect all the ping buffers as we receive them at the server.
final String[] receivedPings = new String[numStreams];
doAnswer(new Answer<Void>() {
int nextIndex;
@Override
public Void answer(InvocationOnMock in) throws Throwable {
receivedPings[nextIndex++] = ((ByteBuf) in.getArguments()[1]).toString(UTF_8);
return null;
}
}).when(serverListener).onPingRead(any(ChannelHandlerContext.class), any(ByteBuf.class));
// Collect all the data buffers as we receive them at the server.
final StringBuilder[] receivedData = new StringBuilder[numStreams];
doAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock in) throws Throwable {
int streamId = (Integer) in.getArguments()[1];
ByteBuf buf = (ByteBuf) in.getArguments()[2];
int padding = (Integer) in.getArguments()[3];
int processedBytes = buf.readableBytes() + padding;
int streamIndex = (streamId - 3) / 2;
StringBuilder builder = receivedData[streamIndex];
if (builder == null) {
builder = new StringBuilder(dataAsHex.length());
receivedData[streamIndex] = builder;
}
builder.append(ByteBufUtil.hexDump(buf));
return processedBytes;
}
}).when(serverListener).onDataRead(any(ChannelHandlerContext.class), anyInt(),
any(ByteBuf.class), anyInt(), anyBoolean());
try {
bootstrapEnv(numStreams * length, 1, numStreams * 4, numStreams);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
int upperLimit = 3 + 2 * numStreams;
for (int streamId = 3; streamId < upperLimit; streamId += 2) {
// Send a bunch of data on each stream.
http2Client.encoder().writeHeaders(ctx(), streamId, headers, 0, (short) 16,
false, 0, false, newPromise());
http2Client.encoder().writePing(ctx(), false, pingData.slice().retain(),
newPromise());
http2Client.encoder().writeData(ctx(), streamId, data.slice().retain(), 0,
false, newPromise());
// Write trailers.
http2Client.encoder().writeHeaders(ctx(), streamId, headers, 0, (short) 16,
false, 0, true, newPromise());
}
}
});
// Wait for all frames to be received.
assertTrue(serverSettingsAckLatch.await(60, SECONDS));
assertTrue(trailersLatch.await(60, SECONDS));
verify(serverListener, times(numStreams)).onHeadersRead(any(ChannelHandlerContext.class), anyInt(),
eq(headers), eq(0), eq((short) 16), eq(false), eq(0), eq(false));
verify(serverListener, times(numStreams)).onHeadersRead(any(ChannelHandlerContext.class), anyInt(),
eq(headers), eq(0), eq((short) 16), eq(false), eq(0), eq(true));
verify(serverListener, times(numStreams)).onPingRead(any(ChannelHandlerContext.class),
any(ByteBuf.class));
verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class),
anyInt(), any(ByteBuf.class), eq(0), eq(true));
for (StringBuilder builder : receivedData) {
assertEquals(dataAsHex, builder.toString());
}
for (String receivedPing : receivedPings) {
assertEquals(pingMsg, receivedPing);
}
} finally {
data.release();
pingData.release();
}
}
private void bootstrapEnv(int dataCountDown, int settingsAckCount,
int requestCountDown, int trailersCountDown) throws Exception {
requestLatch = new CountDownLatch(requestCountDown);
serverSettingsAckLatch = new CountDownLatch(settingsAckCount);
dataLatch = new CountDownLatch(dataCountDown);
trailersLatch = new CountDownLatch(trailersCountDown);
sb = new ServerBootstrap();
cb = new Bootstrap();
sb.group(new NioEventLoopGroup(), new NioEventLoopGroup());
sb.channel(NioServerSocketChannel.class);
sb.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
serverFrameCountDown =
new FrameCountDown(serverListener, serverSettingsAckLatch,
requestLatch, dataLatch, trailersLatch);
p.addLast(new Http2ConnectionHandler(true, serverFrameCountDown));
}
});
cb.group(new NioEventLoopGroup());
cb.channel(NioSocketChannel.class);
cb.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new Http2ConnectionHandler(false, clientListener));
}
});
serverChannel = sb.bind(new InetSocketAddress(0)).sync().channel();
int port = ((InetSocketAddress) serverChannel.localAddress()).getPort();
ChannelFuture ccf = cb.connect(new InetSocketAddress(NetUtil.LOCALHOST, port));
assertTrue(ccf.awaitUninterruptibly().isSuccess());
clientChannel = ccf.channel();
http2Client = clientChannel.pipeline().get(Http2ConnectionHandler.class);
}
private ChannelHandlerContext ctx() {
return clientChannel.pipeline().firstContext();
}
private ChannelPromise newPromise() {
return ctx().newPromise();
}
private static Http2Headers dummyHeaders() {
return new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path/resource2")).add(randomString(), randomString());
}
private void mockFlowControl(Http2FrameListener listener) throws Http2Exception {
doAnswer(new Answer<Integer>() {
@Override
public Integer answer(InvocationOnMock invocation) throws Throwable {
ByteBuf buf = (ByteBuf) invocation.getArguments()[2];
int padding = (Integer) invocation.getArguments()[3];
int processedBytes = buf.readableBytes() + padding;
return processedBytes;
}
}).when(listener).onDataRead(any(ChannelHandlerContext.class), anyInt(),
any(ByteBuf.class), anyInt(), anyBoolean());
}
/**
* Creates a {@link ByteBuf} of the given length, filled with random bytes.
*/
private static ByteBuf randomBytes(int length) {
final byte[] bytes = new byte[length];
new Random().nextBytes(bytes);
return Unpooled.wrappedBuffer(bytes);
}
}

View File

@ -0,0 +1,399 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.randomString;
import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel;
import static io.netty.util.CharsetUtil.UTF_8;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http2.Http2TestUtil.Http2Runnable;
import io.netty.util.NetUtil;
import io.netty.util.concurrent.Future;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/**
* Tests encoding/decoding each HTTP2 frame type.
*/
public class Http2FrameRoundtripTest {
@Mock
private Http2FrameListener serverListener;
private Http2FrameWriter frameWriter;
private ServerBootstrap sb;
private Bootstrap cb;
private Channel serverChannel;
private Channel clientChannel;
private volatile CountDownLatch requestLatch;
private Http2TestUtil.FrameAdapter serverAdapter;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
}
@After
public void teardown() throws Exception {
serverChannel.close().sync();
Future<?> serverGroup = sb.group().shutdownGracefully(0, 0, TimeUnit.MILLISECONDS);
Future<?> serverChildGroup = sb.childGroup().shutdownGracefully(0, 0, TimeUnit.MILLISECONDS);
Future<?> clientGroup = cb.group().shutdownGracefully(0, 0, TimeUnit.MILLISECONDS);
serverGroup.sync();
serverChildGroup.sync();
clientGroup.sync();
serverAdapter = null;
}
@Test
public void dataFrameShouldMatch() throws Exception {
final String text = "hello world";
final ByteBuf data = Unpooled.copiedBuffer(text, UTF_8);
final List<String> receivedBuffers = Collections.synchronizedList(new ArrayList<String>());
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock in) throws Throwable {
receivedBuffers.add(((ByteBuf) in.getArguments()[2]).toString(UTF_8));
return null;
}
}).when(serverListener).onDataRead(any(ChannelHandlerContext.class), eq(0x7FFFFFFF),
any(ByteBuf.class), eq(100), eq(true));
try {
bootstrapEnv(1);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeData(ctx(), 0x7FFFFFFF, data.slice().retain(), 100, true, newPromise());
ctx().flush();
}
});
awaitRequests();
verify(serverListener).onDataRead(any(ChannelHandlerContext.class), eq(0x7FFFFFFF),
any(ByteBuf.class), eq(100), eq(true));
assertEquals(1, receivedBuffers.size());
assertEquals(text, receivedBuffers.get(0));
} finally {
data.release();
}
}
@Test
public void headersFrameWithoutPriorityShouldMatch() throws Exception {
final Http2Headers headers = headers();
bootstrapEnv(1);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctx(), 0x7FFFFFFF, headers, 0, true, newPromise());
ctx().flush();
}
});
awaitRequests();
verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(0x7FFFFFFF),
eq(headers), eq(0), eq(true));
}
@Test
public void headersFrameWithPriorityShouldMatch() throws Exception {
final Http2Headers headers = headers();
bootstrapEnv(1);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctx(), 0x7FFFFFFF, headers, 4, (short) 255,
true, 0, true, newPromise());
ctx().flush();
}
});
awaitRequests();
verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(0x7FFFFFFF),
eq(headers), eq(4), eq((short) 255), eq(true), eq(0), eq(true));
}
@Test
public void goAwayFrameShouldMatch() throws Exception {
final String text = "test";
final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
final List<String> receivedBuffers = Collections.synchronizedList(new ArrayList<String>());
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock in) throws Throwable {
receivedBuffers.add(((ByteBuf) in.getArguments()[3]).toString(UTF_8));
return null;
}
}).when(serverListener).onGoAwayRead(any(ChannelHandlerContext.class), eq(0x7FFFFFFF),
eq(0xFFFFFFFFL), eq(data));
bootstrapEnv(1);
try {
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeGoAway(ctx(), 0x7FFFFFFF, 0xFFFFFFFFL, data.retain(), newPromise());
ctx().flush();
}
});
awaitRequests();
verify(serverListener).onGoAwayRead(any(ChannelHandlerContext.class), eq(0x7FFFFFFF),
eq(0xFFFFFFFFL), any(ByteBuf.class));
assertEquals(1, receivedBuffers.size());
assertEquals(text, receivedBuffers.get(0));
} finally {
data.release();
}
}
@Test
public void pingFrameShouldMatch() throws Exception {
String text = "01234567";
final ByteBuf data = Unpooled.copiedBuffer(text, UTF_8);
final List<String> receivedBuffers = Collections.synchronizedList(new ArrayList<String>());
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock in) throws Throwable {
receivedBuffers.add(((ByteBuf) in.getArguments()[1]).toString(UTF_8));
return null;
}
}).when(serverListener).onPingAckRead(any(ChannelHandlerContext.class), eq(data));
try {
bootstrapEnv(1);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writePing(ctx(), true, data.retain(), newPromise());
ctx().flush();
}
});
awaitRequests();
verify(serverListener).onPingAckRead(any(ChannelHandlerContext.class), any(ByteBuf.class));
assertEquals(1, receivedBuffers.size());
for (String receivedData : receivedBuffers) {
assertEquals(text, receivedData);
}
} finally {
data.release();
}
}
@Test
public void priorityFrameShouldMatch() throws Exception {
bootstrapEnv(1);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writePriority(ctx(), 0x7FFFFFFF, 1, (short) 1, true, newPromise());
ctx().flush();
}
});
awaitRequests();
verify(serverListener).onPriorityRead(any(ChannelHandlerContext.class), eq(0x7FFFFFFF),
eq(1), eq((short) 1), eq(true));
}
@Test
public void pushPromiseFrameShouldMatch() throws Exception {
final Http2Headers headers = headers();
bootstrapEnv(1);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writePushPromise(ctx(), 0x7FFFFFFF, 1, headers, 5, newPromise());
ctx().flush();
}
});
awaitRequests();
verify(serverListener).onPushPromiseRead(any(ChannelHandlerContext.class), eq(0x7FFFFFFF),
eq(1), eq(headers), eq(5));
}
@Test
public void rstStreamFrameShouldMatch() throws Exception {
bootstrapEnv(1);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeRstStream(ctx(), 0x7FFFFFFF, 0xFFFFFFFFL, newPromise());
ctx().flush();
}
});
awaitRequests();
verify(serverListener).onRstStreamRead(any(ChannelHandlerContext.class), eq(0x7FFFFFFF),
eq(0xFFFFFFFFL));
}
@Test
public void settingsFrameShouldMatch() throws Exception {
bootstrapEnv(1);
final Http2Settings settings = new Http2Settings();
settings.initialWindowSize(10);
settings.maxConcurrentStreams(1000);
settings.headerTableSize(4096);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeSettings(ctx(), settings, newPromise());
ctx().flush();
}
});
awaitRequests();
verify(serverListener).onSettingsRead(any(ChannelHandlerContext.class), eq(settings));
}
@Test
public void windowUpdateFrameShouldMatch() throws Exception {
bootstrapEnv(1);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeWindowUpdate(ctx(), 0x7FFFFFFF, 0x7FFFFFFF, newPromise());
ctx().flush();
}
});
awaitRequests();
verify(serverListener).onWindowUpdateRead(any(ChannelHandlerContext.class), eq(0x7FFFFFFF),
eq(0x7FFFFFFF));
}
@Test
public void stressTest() throws Exception {
final Http2Headers headers = headers();
final String text = "hello world";
final ByteBuf data = Unpooled.copiedBuffer(text.getBytes());
final int numStreams = 10000;
final List<String> receivedBuffers = Collections.synchronizedList(new ArrayList<String>(numStreams));
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock in) throws Throwable {
receivedBuffers.add(((ByteBuf) in.getArguments()[2]).toString(UTF_8));
return null;
}
}).when(serverListener).onDataRead(any(ChannelHandlerContext.class), anyInt(), eq(data), eq(0), eq(true));
try {
final int expectedFrames = numStreams * 2;
bootstrapEnv(expectedFrames);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
for (int i = 1; i < numStreams + 1; ++i) {
frameWriter.writeHeaders(ctx(), i, headers, 0, (short) 16, false, 0, false, newPromise());
frameWriter.writeData(ctx(), i, data.retain(), 0, true, newPromise());
ctx().flush();
}
}
});
awaitRequests(60);
verify(serverListener, times(numStreams)).onDataRead(any(ChannelHandlerContext.class), anyInt(),
any(ByteBuf.class), eq(0), eq(true));
assertEquals(numStreams, receivedBuffers.size());
for (String receivedData : receivedBuffers) {
assertEquals(text, receivedData);
}
} finally {
data.release();
}
}
private void awaitRequests(long seconds) throws InterruptedException {
assertTrue(requestLatch.await(seconds, SECONDS));
}
private void awaitRequests() throws InterruptedException {
awaitRequests(5);
}
private void bootstrapEnv(int requestCountDown) throws Exception {
requestLatch = new CountDownLatch(requestCountDown);
frameWriter = new DefaultHttp2FrameWriter();
sb = new ServerBootstrap();
cb = new Bootstrap();
sb.group(new NioEventLoopGroup(), new NioEventLoopGroup());
sb.channel(NioServerSocketChannel.class);
sb.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
serverAdapter = new Http2TestUtil.FrameAdapter(serverListener, requestLatch);
p.addLast("reader", serverAdapter);
}
});
cb.group(new NioEventLoopGroup());
cb.channel(NioSocketChannel.class);
cb.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast("reader", new Http2TestUtil.FrameAdapter(null, null));
}
});
serverChannel = sb.bind(new InetSocketAddress(0)).sync().channel();
int port = ((InetSocketAddress) serverChannel.localAddress()).getPort();
ChannelFuture ccf = cb.connect(new InetSocketAddress(NetUtil.LOCALHOST, port));
assertTrue(ccf.awaitUninterruptibly().isSuccess());
clientChannel = ccf.channel();
}
private ChannelHandlerContext ctx() {
return clientChannel.pipeline().firstContext();
}
private ChannelPromise newPromise() {
return ctx().newPromise();
}
private static Http2Headers headers() {
return new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path/resource2")).add(randomString(), randomString());
}
}

View File

@ -0,0 +1,101 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.randomString;
import static org.junit.Assert.assertEquals;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
/**
* Tests for encoding/decoding HTTP2 header blocks.
*/
public class Http2HeaderBlockIOTest {
private DefaultHttp2HeadersDecoder decoder;
private DefaultHttp2HeadersEncoder encoder;
private ByteBuf buffer;
@Before
public void setup() {
encoder = new DefaultHttp2HeadersEncoder();
decoder = new DefaultHttp2HeadersDecoder();
buffer = Unpooled.buffer();
}
@After
public void teardown() {
buffer.release();
}
@Test
public void roundtripShouldBeSuccessful() throws Http2Exception {
Http2Headers in = headers();
assertRoundtripSuccessful(in);
}
@Test
public void successiveCallsShouldSucceed() throws Http2Exception {
Http2Headers in =
new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path"))
.add(as("accept"), as("*/*"));
assertRoundtripSuccessful(in);
in =
new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path/resource1"))
.add(as("accept"), as("image/jpeg"))
.add(as("cache-control"), as("no-cache"));
assertRoundtripSuccessful(in);
in =
new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path/resource2"))
.add(as("accept"), as("image/png"))
.add(as("cache-control"), as("no-cache"));
assertRoundtripSuccessful(in);
}
@Test
public void setMaxHeaderSizeShouldBeSuccessful() throws Http2Exception {
encoder.headerTable().maxHeaderTableSize(10);
Http2Headers in = headers();
assertRoundtripSuccessful(in);
assertEquals(10, decoder.headerTable().maxHeaderTableSize());
}
private void assertRoundtripSuccessful(Http2Headers in) throws Http2Exception {
encoder.encodeHeaders(in, buffer);
Http2Headers out = decoder.decodeHeaders(buffer);
assertEquals(in, out);
}
private static Http2Headers headers() {
return new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path/resource2"))
.add(as("accept"), as("image/png")).add(as("cache-control"), as("no-cache"))
.add(as("custom"), as("value1")).add(as("custom"), as("value2"))
.add(as("custom"), as("value3")).add(as("custom"), as("custom4"))
.add(randomString(), randomString());
}
}

View File

@ -0,0 +1,64 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2CodecUtil.MAX_FRAME_SIZE_UPPER_BOUND;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import org.junit.Before;
import org.junit.Test;
/**
* Tests for {@link Http2Settings}.
*/
public class Http2SettingsTest {
private Http2Settings settings;
@Before
public void setup() {
settings = new Http2Settings();
}
@Test
public void standardSettingsShouldBeNotSet() {
assertEquals(0, settings.size());
assertNull(settings.headerTableSize());
assertNull(settings.initialWindowSize());
assertNull(settings.maxConcurrentStreams());
assertNull(settings.pushEnabled());
assertNull(settings.maxFrameSize());
assertNull(settings.maxHeaderListSize());
}
@Test
public void standardSettingsShouldBeSet() {
settings.initialWindowSize(1);
settings.maxConcurrentStreams(2);
settings.pushEnabled(true);
settings.headerTableSize(3);
settings.maxFrameSize(MAX_FRAME_SIZE_UPPER_BOUND);
settings.maxHeaderListSize(4);
assertEquals(1, (int) settings.initialWindowSize());
assertEquals(2L, (long) settings.maxConcurrentStreams());
assertTrue(settings.pushEnabled());
assertEquals(3L, (long) settings.headerTableSize());
assertEquals(MAX_FRAME_SIZE_UPPER_BOUND, (int) settings.maxFrameSize());
assertEquals(4L, (long) settings.maxHeaderListSize());
}
}

View File

@ -0,0 +1,381 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.ByteToMessageDecoder;
import java.util.List;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
/**
* Utilities for the integration tests.
*/
final class Http2TestUtil {
/**
* Interface that allows for running a operation that throws a {@link Http2Exception}.
*/
interface Http2Runnable {
void run() throws Http2Exception;
}
/**
* Runs the given operation within the event loop thread of the given {@link Channel}.
*/
static void runInChannel(Channel channel, final Http2Runnable runnable) {
channel.eventLoop().execute(new Runnable() {
@Override
public void run() {
try {
runnable.run();
} catch (Http2Exception e) {
throw new RuntimeException(e);
}
}
});
}
/**
* Converts a {@link String} into an {@link AsciiString}.
*/
public static AsciiString as(String value) {
return new AsciiString(value);
}
/**
* Converts a byte array into an {@link AsciiString}.
*/
public static AsciiString as(byte[] value) {
return new AsciiString(value);
}
/**
* Returns a byte array filled with random data.
*/
public static byte[] randomBytes() {
byte[] data = new byte[100];
new Random().nextBytes(data);
return data;
}
/**
* Returns an {@link AsciiString} that wraps a randomly-filled byte array.
*/
public static AsciiString randomString() {
return as(randomBytes());
}
private Http2TestUtil() {
}
static class FrameAdapter extends ByteToMessageDecoder {
private final Http2Connection connection;
private final Http2FrameListener listener;
private final DefaultHttp2FrameReader reader;
private final CountDownLatch latch;
FrameAdapter(Http2FrameListener listener, CountDownLatch latch) {
this(null, listener, latch);
}
FrameAdapter(Http2Connection connection, Http2FrameListener listener, CountDownLatch latch) {
this(connection, new DefaultHttp2FrameReader(), listener, latch);
}
FrameAdapter(Http2Connection connection, DefaultHttp2FrameReader reader, Http2FrameListener listener,
CountDownLatch latch) {
this.connection = connection;
this.listener = listener;
this.reader = reader;
this.latch = latch;
}
public Http2Stream getOrCreateStream(int streamId, boolean halfClosed) throws Http2Exception {
return getOrCreateStream(connection, streamId, halfClosed);
}
public static Http2Stream getOrCreateStream(Http2Connection connection, int streamId, boolean halfClosed)
throws Http2Exception {
if (connection != null) {
Http2Stream stream = connection.stream(streamId);
if (stream == null) {
if (connection.isServer() && streamId % 2 == 0 || !connection.isServer() && streamId % 2 != 0) {
stream = connection.local().createStream(streamId).open(halfClosed);
} else {
stream = connection.remote().createStream(streamId).open(halfClosed);
}
}
return stream;
}
return null;
}
private void closeStream(Http2Stream stream) {
closeStream(stream, false);
}
protected void closeStream(Http2Stream stream, boolean dataRead) {
if (stream != null) {
stream.close();
}
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
reader.readFrame(ctx, in, new Http2FrameListener() {
@Override
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
boolean endOfStream) throws Http2Exception {
Http2Stream stream = getOrCreateStream(streamId, endOfStream);
int processed = listener.onDataRead(ctx, streamId, data, padding, endOfStream);
if (endOfStream) {
closeStream(stream, true);
}
latch.countDown();
return processed;
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endStream) throws Http2Exception {
Http2Stream stream = getOrCreateStream(streamId, endStream);
listener.onHeadersRead(ctx, streamId, headers, padding, endStream);
if (endStream) {
closeStream(stream);
}
latch.countDown();
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers,
int streamDependency, short weight, boolean exclusive, int padding, boolean endStream)
throws Http2Exception {
Http2Stream stream = getOrCreateStream(streamId, endStream);
if (stream != null) {
stream.setPriority(streamDependency, weight, exclusive);
}
listener.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding,
endStream);
if (endStream) {
closeStream(stream);
}
latch.countDown();
}
@Override
public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight,
boolean exclusive) throws Http2Exception {
Http2Stream stream = getOrCreateStream(streamId, false);
if (stream != null) {
stream.setPriority(streamDependency, weight, exclusive);
}
listener.onPriorityRead(ctx, streamId, streamDependency, weight, exclusive);
latch.countDown();
}
@Override
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode)
throws Http2Exception {
Http2Stream stream = getOrCreateStream(streamId, false);
listener.onRstStreamRead(ctx, streamId, errorCode);
closeStream(stream);
latch.countDown();
}
@Override
public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception {
listener.onSettingsAckRead(ctx);
latch.countDown();
}
@Override
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception {
listener.onSettingsRead(ctx, settings);
latch.countDown();
}
@Override
public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
listener.onPingRead(ctx, data);
latch.countDown();
}
@Override
public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
listener.onPingAckRead(ctx, data);
latch.countDown();
}
@Override
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
Http2Headers headers, int padding) throws Http2Exception {
getOrCreateStream(promisedStreamId, false);
listener.onPushPromiseRead(ctx, streamId, promisedStreamId, headers, padding);
latch.countDown();
}
@Override
public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData)
throws Http2Exception {
listener.onGoAwayRead(ctx, lastStreamId, errorCode, debugData);
latch.countDown();
}
@Override
public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
throws Http2Exception {
getOrCreateStream(streamId, false);
listener.onWindowUpdateRead(ctx, streamId, windowSizeIncrement);
latch.countDown();
}
@Override
public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags,
ByteBuf payload) {
listener.onUnknownFrame(ctx, frameType, streamId, flags, payload);
latch.countDown();
}
});
}
}
/**
* A decorator around a {@link Http2FrameListener} that counts down the latch so that we can await the completion of
* the request.
*/
static class FrameCountDown implements Http2FrameListener {
private final Http2FrameListener listener;
private final CountDownLatch messageLatch;
private final CountDownLatch settingsAckLatch;
private final CountDownLatch dataLatch;
private final CountDownLatch trailersLatch;
FrameCountDown(Http2FrameListener listener, CountDownLatch settingsAckLatch, CountDownLatch messageLatch) {
this(listener, settingsAckLatch, messageLatch, null, null);
}
FrameCountDown(Http2FrameListener listener, CountDownLatch settingsAckLatch, CountDownLatch messageLatch,
CountDownLatch dataLatch, CountDownLatch trailersLatch) {
this.listener = listener;
this.messageLatch = messageLatch;
this.settingsAckLatch = settingsAckLatch;
this.dataLatch = dataLatch;
this.trailersLatch = trailersLatch;
}
@Override
public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endOfStream)
throws Http2Exception {
int numBytes = data.readableBytes();
int processed = listener.onDataRead(ctx, streamId, data, padding, endOfStream);
messageLatch.countDown();
if (dataLatch != null) {
for (int i = 0; i < numBytes; ++i) {
dataLatch.countDown();
}
}
return processed;
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int padding,
boolean endStream) throws Http2Exception {
listener.onHeadersRead(ctx, streamId, headers, padding, endStream);
messageLatch.countDown();
if (trailersLatch != null && endStream) {
trailersLatch.countDown();
}
}
@Override
public void onHeadersRead(ChannelHandlerContext ctx, int streamId, Http2Headers headers, int streamDependency,
short weight, boolean exclusive, int padding, boolean endStream) throws Http2Exception {
listener.onHeadersRead(ctx, streamId, headers, streamDependency, weight, exclusive, padding, endStream);
messageLatch.countDown();
if (trailersLatch != null && endStream) {
trailersLatch.countDown();
}
}
@Override
public void onPriorityRead(ChannelHandlerContext ctx, int streamId, int streamDependency, short weight,
boolean exclusive) throws Http2Exception {
listener.onPriorityRead(ctx, streamId, streamDependency, weight, exclusive);
messageLatch.countDown();
}
@Override
public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode) throws Http2Exception {
listener.onRstStreamRead(ctx, streamId, errorCode);
messageLatch.countDown();
}
@Override
public void onSettingsAckRead(ChannelHandlerContext ctx) throws Http2Exception {
listener.onSettingsAckRead(ctx);
settingsAckLatch.countDown();
}
@Override
public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) throws Http2Exception {
listener.onSettingsRead(ctx, settings);
messageLatch.countDown();
}
@Override
public void onPingRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
listener.onPingRead(ctx, data);
messageLatch.countDown();
}
@Override
public void onPingAckRead(ChannelHandlerContext ctx, ByteBuf data) throws Http2Exception {
listener.onPingAckRead(ctx, data);
messageLatch.countDown();
}
@Override
public void onPushPromiseRead(ChannelHandlerContext ctx, int streamId, int promisedStreamId,
Http2Headers headers, int padding) throws Http2Exception {
listener.onPushPromiseRead(ctx, streamId, promisedStreamId, headers, padding);
messageLatch.countDown();
}
@Override
public void onGoAwayRead(ChannelHandlerContext ctx, int lastStreamId, long errorCode, ByteBuf debugData)
throws Http2Exception {
listener.onGoAwayRead(ctx, lastStreamId, errorCode, debugData);
messageLatch.countDown();
}
@Override
public void onWindowUpdateRead(ChannelHandlerContext ctx, int streamId, int windowSizeIncrement)
throws Http2Exception {
listener.onWindowUpdateRead(ctx, streamId, windowSizeIncrement);
messageLatch.countDown();
}
@Override
public void onUnknownFrame(ChannelHandlerContext ctx, byte frameType, int streamId, Http2Flags flags,
ByteBuf payload) {
listener.onUnknownFrame(ctx, frameType, streamId, flags, payload);
messageLatch.countDown();
}
}
}

View File

@ -0,0 +1,225 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpMethod.POST;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.util.CharsetUtil.UTF_8;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyShort;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http2.Http2TestUtil.FrameCountDown;
import io.netty.util.NetUtil;
import io.netty.util.concurrent.Future;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
/**
* Testing the {@link HttpToHttp2ConnectionHandler} for {@link FullHttpRequest} objects into HTTP/2 frames
*/
public class HttpToHttp2ConnectionHandlerTest {
private static final int WAIT_TIME_SECONDS = 5;
@Mock
private Http2FrameListener clientListener;
@Mock
private Http2FrameListener serverListener;
private ServerBootstrap sb;
private Bootstrap cb;
private Channel serverChannel;
private Channel clientChannel;
private CountDownLatch requestLatch;
private CountDownLatch serverSettingsAckLatch;
private FrameCountDown serverFrameCountDown;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
}
@After
public void teardown() throws Exception {
serverChannel.close().sync();
Future<?> serverGroup = sb.group().shutdownGracefully(0, 0, MILLISECONDS);
Future<?> serverChildGroup = sb.childGroup().shutdownGracefully(0, 0, MILLISECONDS);
Future<?> clientGroup = cb.group().shutdownGracefully(0, 0, MILLISECONDS);
serverGroup.sync();
serverChildGroup.sync();
clientGroup.sync();
}
@Test
public void testJustHeadersRequest() throws Exception {
bootstrapEnv(2, 1);
final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, "/example");
final HttpHeaders httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
httpHeaders.set(HttpHeaderNames.HOST,
"http://my-user_name@www.example.org:5555/example");
httpHeaders.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "www.example.org:5555");
httpHeaders.set(HttpUtil.ExtensionHeaderNames.SCHEME.text(), "http");
httpHeaders.add("foo", "goo");
httpHeaders.add("foo", "goo2");
httpHeaders.add("foo2", "goo2");
final Http2Headers http2Headers =
new DefaultHttp2Headers().method(as("GET")).path(as("/example"))
.authority(as("www.example.org:5555")).scheme(as("http"))
.add(as("foo"), as("goo")).add(as("foo"), as("goo2"))
.add(as("foo2"), as("goo2"));
ChannelPromise writePromise = newPromise();
ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise);
assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
assertTrue(writePromise.isSuccess());
assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
assertTrue(writeFuture.isSuccess());
awaitRequests();
verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(5),
eq(http2Headers), eq(0), anyShort(), anyBoolean(), eq(0), eq(true));
verify(serverListener, never()).onDataRead(any(ChannelHandlerContext.class), anyInt(),
any(ByteBuf.class), anyInt(), anyBoolean());
}
@Test
public void testRequestWithBody() throws Exception {
final String text = "foooooogoooo";
final List<String> receivedBuffers = Collections.synchronizedList(new ArrayList<String>());
doAnswer(new Answer<Void>() {
@Override
public Void answer(InvocationOnMock in) throws Throwable {
receivedBuffers.add(((ByteBuf) in.getArguments()[2]).toString(UTF_8));
return null;
}
}).when(serverListener).onDataRead(any(ChannelHandlerContext.class), eq(3),
any(ByteBuf.class), eq(0), eq(true));
bootstrapEnv(3, 1);
final FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST, "/example",
Unpooled.copiedBuffer(text, UTF_8));
final HttpHeaders httpHeaders = request.headers();
httpHeaders.set(HttpHeaderNames.HOST, "http://your_user-name123@www.example.org:5555/example");
httpHeaders.add("foo", "goo");
httpHeaders.add("foo", "goo2");
httpHeaders.add("foo2", "goo2");
final Http2Headers http2Headers =
new DefaultHttp2Headers().method(as("POST")).path(as("/example"))
.authority(as("www.example.org:5555")).scheme(as("http"))
.add(as("foo"), as("goo")).add(as("foo"), as("goo2"))
.add(as("foo2"), as("goo2"));
ChannelPromise writePromise = newPromise();
ChannelFuture writeFuture = clientChannel.writeAndFlush(request, writePromise);
assertTrue(writePromise.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
assertTrue(writePromise.isSuccess());
assertTrue(writeFuture.awaitUninterruptibly(WAIT_TIME_SECONDS, SECONDS));
assertTrue(writeFuture.isSuccess());
awaitRequests();
verify(serverListener).onHeadersRead(any(ChannelHandlerContext.class), eq(3), eq(http2Headers), eq(0),
anyShort(), anyBoolean(), eq(0), eq(false));
verify(serverListener).onDataRead(any(ChannelHandlerContext.class), eq(3), any(ByteBuf.class), eq(0),
eq(true));
assertEquals(1, receivedBuffers.size());
assertEquals(text, receivedBuffers.get(0));
}
private void bootstrapEnv(int requestCountDown, int serverSettingsAckCount) throws Exception {
requestLatch = new CountDownLatch(requestCountDown);
serverSettingsAckLatch = new CountDownLatch(serverSettingsAckCount);
sb = new ServerBootstrap();
cb = new Bootstrap();
sb.group(new NioEventLoopGroup(), new NioEventLoopGroup());
sb.channel(NioServerSocketChannel.class);
sb.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
serverFrameCountDown = new FrameCountDown(serverListener, serverSettingsAckLatch, requestLatch);
p.addLast(new HttpToHttp2ConnectionHandler(true, serverFrameCountDown));
}
});
cb.group(new NioEventLoopGroup());
cb.channel(NioSocketChannel.class);
cb.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(new HttpToHttp2ConnectionHandler(false, clientListener));
}
});
serverChannel = sb.bind(new InetSocketAddress(0)).sync().channel();
int port = ((InetSocketAddress) serverChannel.localAddress()).getPort();
ChannelFuture ccf = cb.connect(new InetSocketAddress(NetUtil.LOCALHOST, port));
assertTrue(ccf.awaitUninterruptibly().isSuccess());
clientChannel = ccf.channel();
}
private void awaitRequests() throws Exception {
assertTrue(requestLatch.await(WAIT_TIME_SECONDS, SECONDS));
}
private ChannelHandlerContext ctx() {
return clientChannel.pipeline().firstContext();
}
private ChannelPromise newPromise() {
return ctx().newPromise();
}
}

View File

@ -0,0 +1,824 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2Exception.isStreamError;
import static io.netty.handler.codec.http2.Http2CodecUtil.getEmbeddedHttp2Exception;
import static io.netty.handler.codec.http2.Http2TestUtil.as;
import static io.netty.handler.codec.http2.Http2TestUtil.runInChannel;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import static java.util.concurrent.TimeUnit.SECONDS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerAdapter;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.AsciiString;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.Http2TestUtil.FrameAdapter;
import io.netty.handler.codec.http2.Http2TestUtil.Http2Runnable;
import io.netty.util.CharsetUtil;
import io.netty.util.NetUtil;
import io.netty.util.concurrent.Future;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
/**
* Testing the {@link InboundHttp2ToHttpPriorityAdapter} and base class {@link InboundHttp2ToHttpAdapter} for HTTP/2
* frames into {@link HttpObject}s
*/
public class InboundHttp2ToHttpAdapterTest {
private List<FullHttpMessage> capturedRequests;
private List<FullHttpMessage> capturedResponses;
@Mock
private HttpResponseListener serverListener;
@Mock
private HttpResponseListener clientListener;
@Mock
private HttpSettingsListener settingsListener;
private Http2FrameWriter frameWriter;
private ServerBootstrap sb;
private Bootstrap cb;
private Channel serverChannel;
private Channel serverConnectedChannel;
private Channel clientChannel;
private volatile CountDownLatch serverLatch;
private volatile CountDownLatch clientLatch;
private volatile CountDownLatch settingsLatch;
private int maxContentLength;
private HttpResponseDelegator serverDelegator;
private HttpResponseDelegator clientDelegator;
private HttpSettingsDelegator settingsDelegator;
private Http2Exception serverException;
@Before
public void setup() throws Exception {
MockitoAnnotations.initMocks(this);
clientDelegator = null;
serverDelegator = null;
serverConnectedChannel = null;
maxContentLength = 1024;
setServerLatch(1);
setClientLatch(1);
setSettingsLatch(1);
frameWriter = new DefaultHttp2FrameWriter();
sb = new ServerBootstrap();
cb = new Bootstrap();
sb.group(new NioEventLoopGroup(), new NioEventLoopGroup());
sb.channel(NioServerSocketChannel.class);
sb.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
Http2Connection connection = new DefaultHttp2Connection(true);
p.addLast(
"reader",
new HttpAdapterFrameAdapter(connection,
new InboundHttp2ToHttpPriorityAdapter.Builder(connection)
.maxContentLength(maxContentLength)
.validateHttpHeaders(true)
.propagateSettings(true)
.build(),
new CountDownLatch(10)));
serverDelegator = new HttpResponseDelegator(serverListener, serverLatch);
p.addLast(serverDelegator);
serverConnectedChannel = ch;
settingsDelegator = new HttpSettingsDelegator(settingsListener, settingsLatch);
p.addLast(settingsDelegator);
p.addLast(new ChannelHandlerAdapter() {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
Http2Exception e = getEmbeddedHttp2Exception(cause);
if (e != null) {
serverException = e;
serverLatch.countDown();
} else {
super.exceptionCaught(ctx, cause);
}
}
});
}
});
cb.group(new NioEventLoopGroup());
cb.channel(NioSocketChannel.class);
cb.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
Http2Connection connection = new DefaultHttp2Connection(false);
p.addLast(
"reader",
new HttpAdapterFrameAdapter(connection,
new InboundHttp2ToHttpPriorityAdapter.Builder(connection)
.maxContentLength(maxContentLength)
.build(),
new CountDownLatch(10)));
clientDelegator = new HttpResponseDelegator(clientListener, clientLatch);
p.addLast(clientDelegator);
}
});
serverChannel = sb.bind(new InetSocketAddress(0)).sync().channel();
int port = ((InetSocketAddress) serverChannel.localAddress()).getPort();
ChannelFuture ccf = cb.connect(new InetSocketAddress(NetUtil.LOCALHOST, port));
assertTrue(ccf.awaitUninterruptibly().isSuccess());
clientChannel = ccf.channel();
}
@After
public void teardown() throws Exception {
cleanupCapturedRequests();
cleanupCapturedResponses();
serverChannel.close().sync();
Future<?> serverGroup = sb.group().shutdownGracefully(0, 0, MILLISECONDS);
Future<?> serverChildGroup = sb.childGroup().shutdownGracefully(0, 0, MILLISECONDS);
Future<?> clientGroup = cb.group().shutdownGracefully(0, 0, MILLISECONDS);
serverGroup.sync();
serverChildGroup.sync();
clientGroup.sync();
clientDelegator = null;
serverDelegator = null;
clientChannel = null;
serverChannel = null;
serverConnectedChannel = null;
}
@Test
public void clientRequestSingleHeaderNoDataFrames() throws Exception {
final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
"/some/path/resource2", true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.set(HttpUtil.ExtensionHeaderNames.SCHEME.text(), "https");
httpHeaders.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "example.org");
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")).scheme(as("https"))
.authority(as("example.org")).path(as("/some/path/resource2"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(serverListener).messageReceived(requestCaptor.capture());
capturedRequests = requestCaptor.getAllValues();
assertEquals(request, capturedRequests.get(0));
} finally {
request.release();
}
}
@Test
public void clientRequestSingleHeaderNonAsciiShouldThrow() throws Exception {
final Http2Headers http2Headers = new DefaultHttp2Headers()
.method(as("GET"))
.scheme(as("https"))
.authority(as("example.org"))
.path(as("/some/path/resource2"))
.add(new AsciiString("çã".getBytes(CharsetUtil.UTF_8)),
new AsciiString("Ãã".getBytes(CharsetUtil.UTF_8)));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
assertTrue(isStreamError(serverException));
}
@Test
public void clientRequestOneDataFrame() throws Exception {
final String text = "hello world";
final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
"/some/path/resource2", content, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")).path(
as("/some/path/resource2"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter.writeData(ctxClient(), 3, content.retain(), 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(serverListener).messageReceived(requestCaptor.capture());
capturedRequests = requestCaptor.getAllValues();
assertEquals(request, capturedRequests.get(0));
} finally {
request.release();
}
}
@Test
public void clientRequestMultipleDataFrames() throws Exception {
final String text = "hello world big time data!";
final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
"/some/path/resource2", content, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")).path(
as("/some/path/resource2"));
final int midPoint = text.length() / 2;
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter.writeData(ctxClient(), 3, content.slice(0, midPoint).retain(), 0, false,
newPromiseClient());
frameWriter.writeData(ctxClient(), 3, content.slice(midPoint, text.length() - midPoint).retain(),
0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(serverListener).messageReceived(requestCaptor.capture());
capturedRequests = requestCaptor.getAllValues();
assertEquals(request, capturedRequests.get(0));
} finally {
request.release();
}
}
@Test
public void clientRequestMultipleEmptyDataFrames() throws Exception {
final String text = "";
final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
"/some/path/resource2", content, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")).path(
as("/some/path/resource2"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter.writeData(ctxClient(), 3, content.retain(), 0, false, newPromiseClient());
frameWriter.writeData(ctxClient(), 3, content.retain(), 0, false, newPromiseClient());
frameWriter.writeData(ctxClient(), 3, content.retain(), 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(serverListener).messageReceived(requestCaptor.capture());
capturedRequests = requestCaptor.getAllValues();
assertEquals(request, capturedRequests.get(0));
} finally {
request.release();
}
}
@Test
public void clientRequestMultipleHeaders() throws Exception {
// writeHeaders will implicitly add an END_HEADERS tag each time and so this test does not follow the HTTP
// message flow. We currently accept this message flow and just add the second headers to the trailing headers.
final String text = "";
final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
"/some/path/resource2", content, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
HttpHeaders trailingHeaders = request.trailingHeaders();
trailingHeaders.set("FoO", "goo");
trailingHeaders.set("foO2", "goo2");
trailingHeaders.add("fOo2", "goo3");
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")).path(
as("/some/path/resource2"));
final Http2Headers http2Headers2 = new DefaultHttp2Headers().set(as("foo"), as("goo"))
.set(as("foo2"), as("goo2")).add(as("foo2"), as("goo3"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter.writeHeaders(ctxClient(), 3, http2Headers2, 0, false, newPromiseClient());
frameWriter.writeData(ctxClient(), 3, content.retain(), 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(serverListener).messageReceived(requestCaptor.capture());
capturedRequests = requestCaptor.getAllValues();
assertEquals(request, capturedRequests.get(0));
} finally {
request.release();
}
}
@Test
public void clientRequestTrailingHeaders() throws Exception {
final String text = "some data";
final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET,
"/some/path/resource2", content, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
HttpHeaders trailingHeaders = request.trailingHeaders();
trailingHeaders.set("Foo", "goo");
trailingHeaders.set("fOo2", "goo2");
trailingHeaders.add("foO2", "goo3");
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("GET")).path(
as("/some/path/resource2"));
final Http2Headers http2Headers2 = new DefaultHttp2Headers().set(as("foo"), as("goo"))
.set(as("foo2"), as("goo2")).add(as("foo2"), as("goo3"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter.writeData(ctxClient(), 3, content.retain(), 0, false, newPromiseClient());
frameWriter.writeHeaders(ctxClient(), 3, http2Headers2, 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(serverListener).messageReceived(requestCaptor.capture());
capturedRequests = requestCaptor.getAllValues();
assertEquals(request, capturedRequests.get(0));
} finally {
request.release();
}
}
@Test
public void clientRequestStreamDependencyInHttpMessageFlow() throws Exception {
setServerLatch(2);
final String text = "hello world big time data!";
final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
final String text2 = "hello world big time data...number 2!!";
final ByteBuf content2 = Unpooled.copiedBuffer(text2.getBytes());
final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT,
"/some/path/resource", content, true);
final FullHttpMessage request2 = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT,
"/some/path/resource2", content2, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
HttpHeaders httpHeaders2 = request2.headers();
httpHeaders2.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
httpHeaders2.setInt(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3);
httpHeaders2.setInt(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), 123);
httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length());
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("PUT")).path(
as("/some/path/resource"));
final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(as("PUT")).path(
as("/some/path/resource2"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter.writeHeaders(ctxClient(), 5, http2Headers2, 0, false, newPromiseClient());
frameWriter.writePriority(ctxClient(), 5, 3, (short) 123, true, newPromiseClient());
frameWriter.writeData(ctxClient(), 3, content.retain(), 0, true, newPromiseClient());
frameWriter.writeData(ctxClient(), 5, content2.retain(), 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
ArgumentCaptor<FullHttpMessage> httpObjectCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(serverListener, times(2)).messageReceived(httpObjectCaptor.capture());
capturedRequests = httpObjectCaptor.getAllValues();
assertEquals(request, capturedRequests.get(0));
assertEquals(request2, capturedRequests.get(1));
} finally {
request.release();
request2.release();
}
}
@Test
public void clientRequestStreamDependencyOutsideHttpMessageFlow() throws Exception {
setServerLatch(3);
final String text = "hello world big time data!";
final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
final String text2 = "hello world big time data...number 2!!";
final ByteBuf content2 = Unpooled.copiedBuffer(text2.getBytes());
final FullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT,
"/some/path/resource", content, true);
final FullHttpMessage request2 = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT,
"/some/path/resource2", content2, true);
final FullHttpMessage request3 = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
HttpUtil.OUT_OF_MESSAGE_SEQUENCE_METHOD, HttpUtil.OUT_OF_MESSAGE_SEQUENCE_PATH, true);
try {
HttpHeaders httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
HttpHeaders httpHeaders2 = request2.headers();
httpHeaders2.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length());
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("PUT")).path(
as("/some/path/resource"));
final Http2Headers http2Headers2 = new DefaultHttp2Headers().method(as("PUT")).path(
as("/some/path/resource2"));
HttpHeaders httpHeaders3 = request3.headers();
httpHeaders3.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
httpHeaders3.setInt(HttpUtil.ExtensionHeaderNames.STREAM_DEPENDENCY_ID.text(), 3);
httpHeaders3.setInt(HttpUtil.ExtensionHeaderNames.STREAM_WEIGHT.text(), 222);
httpHeaders3.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
frameWriter.writeHeaders(ctxClient(), 5, http2Headers2, 0, false, newPromiseClient());
frameWriter.writeData(ctxClient(), 3, content.retain(), 0, true, newPromiseClient());
frameWriter.writeData(ctxClient(), 5, content2.retain(), 0, true, newPromiseClient());
frameWriter.writePriority(ctxClient(), 5, 3, (short) 222, false, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
ArgumentCaptor<FullHttpMessage> httpObjectCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(serverListener, times(3)).messageReceived(httpObjectCaptor.capture());
capturedRequests = httpObjectCaptor.getAllValues();
assertEquals(request, capturedRequests.get(0));
assertEquals(request2, capturedRequests.get(1));
assertEquals(request3, capturedRequests.get(2));
} finally {
request.release();
request2.release();
request3.release();
}
}
@Test
public void serverRequestPushPromise() throws Exception {
setClientLatch(2);
final String text = "hello world big time data!";
final ByteBuf content = Unpooled.copiedBuffer(text.getBytes());
final String text2 = "hello world smaller data?";
final ByteBuf content2 = Unpooled.copiedBuffer(text2.getBytes());
final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK,
content, true);
final FullHttpMessage response2 = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CREATED,
content2, true);
final FullHttpMessage request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/push/test",
true);
try {
HttpHeaders httpHeaders = response.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
HttpHeaders httpHeaders2 = response2.headers();
httpHeaders2.set(HttpUtil.ExtensionHeaderNames.SCHEME.text(), "https");
httpHeaders2.set(HttpUtil.ExtensionHeaderNames.AUTHORITY.text(), "example.org");
httpHeaders2.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 5);
httpHeaders2.setInt(HttpUtil.ExtensionHeaderNames.STREAM_PROMISE_ID.text(), 3);
httpHeaders2.setInt(HttpHeaderNames.CONTENT_LENGTH, text2.length());
httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
final Http2Headers http2Headers3 = new DefaultHttp2Headers().method(as("GET")).path(as("/push/test"));
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers3, 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(serverListener).messageReceived(requestCaptor.capture());
capturedRequests = requestCaptor.getAllValues();
assertEquals(request, capturedRequests.get(0));
final Http2Headers http2Headers = new DefaultHttp2Headers().status(as("200"));
final Http2Headers http2Headers2 = new DefaultHttp2Headers().status(as("201")).scheme(as("https"))
.authority(as("example.org"));
runInChannel(serverConnectedChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxServer(), 3, http2Headers, 0, false, newPromiseServer());
frameWriter.writePushPromise(ctxServer(), 3, 5, http2Headers2, 0, newPromiseServer());
frameWriter.writeData(ctxServer(), 3, content.retain(), 0, true, newPromiseServer());
frameWriter.writeData(ctxServer(), 5, content2.retain(), 0, true, newPromiseServer());
ctxServer().flush();
}
});
awaitResponses();
ArgumentCaptor<FullHttpMessage> responseCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(clientListener, times(2)).messageReceived(responseCaptor.capture());
capturedResponses = responseCaptor.getAllValues();
assertEquals(response, capturedResponses.get(0));
assertEquals(response2, capturedResponses.get(1));
} finally {
request.release();
response.release();
response2.release();
}
}
@Test
public void serverResponseHeaderInformational() throws Exception {
final FullHttpMessage request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, "/info/test",
true);
HttpHeaders httpHeaders = request.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.set(HttpHeaderNames.EXPECT, HttpHeaderValues.CONTINUE);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
final Http2Headers http2Headers = new DefaultHttp2Headers().method(as("PUT")).path(as("/info/test"))
.set(as(HttpHeaderNames.EXPECT.toString()), as(HttpHeaderValues.CONTINUE.toString()));
final FullHttpMessage response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE);
final String text = "a big payload";
final ByteBuf payload = Unpooled.copiedBuffer(text.getBytes());
final FullHttpMessage request2 = request.copy(payload);
final FullHttpMessage response2 = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
try {
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxClient(), 3, http2Headers, 0, false, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
ArgumentCaptor<FullHttpMessage> requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(serverListener).messageReceived(requestCaptor.capture());
capturedRequests = requestCaptor.getAllValues();
assertEquals(request, capturedRequests.get(0));
cleanupCapturedRequests();
reset(serverListener);
httpHeaders = response.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
final Http2Headers http2HeadersResponse = new DefaultHttp2Headers().status(as("100"));
runInChannel(serverConnectedChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxServer(), 3, http2HeadersResponse, 0, false, newPromiseServer());
ctxServer().flush();
}
});
awaitResponses();
ArgumentCaptor<FullHttpMessage> responseCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(clientListener).messageReceived(responseCaptor.capture());
capturedResponses = responseCaptor.getAllValues();
assertEquals(response, capturedResponses.get(0));
cleanupCapturedResponses();
reset(clientListener);
setServerLatch(1);
httpHeaders = request2.headers();
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, text.length());
httpHeaders.remove(HttpHeaderNames.EXPECT);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeData(ctxClient(), 3, payload.retain(), 0, true, newPromiseClient());
ctxClient().flush();
}
});
awaitRequests();
requestCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(serverListener).messageReceived(requestCaptor.capture());
capturedRequests = requestCaptor.getAllValues();
assertEquals(request2, capturedRequests.get(0));
setClientLatch(1);
httpHeaders = response2.headers();
httpHeaders.setInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text(), 3);
httpHeaders.setInt(HttpHeaderNames.CONTENT_LENGTH, 0);
final Http2Headers http2HeadersResponse2 = new DefaultHttp2Headers().status(as("200"));
runInChannel(serverConnectedChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeHeaders(ctxServer(), 3, http2HeadersResponse2, 0, true, newPromiseServer());
ctxServer().flush();
}
});
awaitResponses();
responseCaptor = ArgumentCaptor.forClass(FullHttpMessage.class);
verify(clientListener).messageReceived(responseCaptor.capture());
capturedResponses = responseCaptor.getAllValues();
assertEquals(response2, capturedResponses.get(0));
} finally {
request.release();
request2.release();
response.release();
response2.release();
}
}
@Test
public void propagateSettings() throws Exception {
final Http2Settings settings = new Http2Settings().pushEnabled(true);
runInChannel(clientChannel, new Http2Runnable() {
@Override
public void run() {
frameWriter.writeSettings(ctxClient(), settings, newPromiseClient());
ctxClient().flush();
}
});
assertTrue(settingsLatch.await(3, SECONDS));
ArgumentCaptor<Http2Settings> settingsCaptor = ArgumentCaptor.forClass(Http2Settings.class);
verify(settingsListener).messageReceived(settingsCaptor.capture());
assertEquals(settings, settingsCaptor.getValue());
}
private void cleanupCapturedRequests() {
if (capturedRequests != null) {
for (FullHttpMessage capturedRequest : capturedRequests) {
capturedRequest.release();
}
capturedRequests = null;
}
}
private void cleanupCapturedResponses() {
if (capturedResponses != null) {
for (FullHttpMessage capturedResponse : capturedResponses) {
capturedResponse.release();
}
capturedResponses = null;
}
}
private void setServerLatch(int count) {
serverLatch = new CountDownLatch(count);
if (serverDelegator != null) {
serverDelegator.latch(serverLatch);
}
}
private void setClientLatch(int count) {
clientLatch = new CountDownLatch(count);
if (clientDelegator != null) {
clientDelegator.latch(clientLatch);
}
}
private void setSettingsLatch(int count) {
settingsLatch = new CountDownLatch(count);
if (settingsDelegator != null) {
settingsDelegator.latch(settingsLatch);
}
}
private void awaitRequests() throws Exception {
assertTrue(serverLatch.await(2, SECONDS));
}
private void awaitResponses() throws Exception {
assertTrue(clientLatch.await(2, SECONDS));
}
private ChannelHandlerContext ctxClient() {
return clientChannel.pipeline().firstContext();
}
private ChannelPromise newPromiseClient() {
return ctxClient().newPromise();
}
private ChannelHandlerContext ctxServer() {
return serverConnectedChannel.pipeline().firstContext();
}
private ChannelPromise newPromiseServer() {
return ctxServer().newPromise();
}
private interface HttpResponseListener {
void messageReceived(HttpObject obj);
}
private interface HttpSettingsListener {
void messageReceived(Http2Settings settings);
}
private static final class HttpResponseDelegator extends SimpleChannelInboundHandler<HttpObject> {
private final HttpResponseListener listener;
private volatile CountDownLatch latch;
HttpResponseDelegator(HttpResponseListener listener, CountDownLatch latch) {
super(false);
this.listener = listener;
this.latch = latch;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
listener.messageReceived(msg);
latch.countDown();
}
public void latch(CountDownLatch latch) {
this.latch = latch;
}
}
private static final class HttpSettingsDelegator extends SimpleChannelInboundHandler<Http2Settings> {
private final HttpSettingsListener listener;
private volatile CountDownLatch latch;
HttpSettingsDelegator(HttpSettingsListener listener, CountDownLatch latch) {
super(false);
this.listener = listener;
this.latch = latch;
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Http2Settings settings) throws Exception {
listener.messageReceived(settings);
latch.countDown();
}
public void latch(CountDownLatch latch) {
this.latch = latch;
}
}
private static final class HttpAdapterFrameAdapter extends FrameAdapter {
HttpAdapterFrameAdapter(Http2Connection connection, Http2FrameListener listener, CountDownLatch latch) {
super(connection, listener, latch);
}
@Override
protected void closeStream(Http2Stream stream, boolean dataRead) {
if (!dataRead) { // NOTE: Do not close the stream to allow the out of order messages to be processed
super.closeStream(stream, dataRead);
}
}
}
}

View File

@ -55,6 +55,11 @@
<artifactId>netty-codec-http</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>netty-codec-http2</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>netty-codec-memcache</artifactId>

View File

@ -0,0 +1,28 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.example.http2;
/**
* Utility methods used by the example client and server.
*/
public final class Http2ExampleUtil {
/**
* Response header sent in response to the http->http2 cleartext upgrade request.
*/
public static final String UPGRADE_RESPONSE_HEADER = "Http-To-Http2-Upgrade";
private Http2ExampleUtil() { }
}

View File

@ -0,0 +1,136 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.example.http2.client;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http2.Http2OrHttpChooser.SelectedProtocol;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectorFailureBehavior;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.util.CharsetUtil;
import java.net.URI;
import java.util.concurrent.TimeUnit;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpMethod.POST;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
/**
* An HTTP2 client that allows you to send HTTP2 frames to a server. Inbound and outbound frames are
* logged. When run from the command-line, sends a single HEADERS frame to the server and gets back
* a "Hello World" response.
*/
public final class Http2Client {
static final boolean SSL = System.getProperty("ssl") != null;
static final String HOST = System.getProperty("host", "127.0.0.1");
static final int PORT = Integer.parseInt(System.getProperty("port", SSL? "8443" : "8080"));
static final String URL = System.getProperty("url", "/whatever");
static final String URL2 = System.getProperty("url2");
static final String URL2DATA = System.getProperty("url2data", "test data!");
public static void main(String[] args) throws Exception {
// Configure SSL.
final SslContext sslCtx;
if (SSL) {
sslCtx = SslContext.newClientContext(SslProvider.JDK,
null, InsecureTrustManagerFactory.INSTANCE,
Http2SecurityUtil.CIPHERS,
/* NOTE: the following filter may not include all ciphers required by the HTTP/2 specification
* Please refer to the HTTP/2 specification for cipher requirements. */
SupportedCipherSuiteFilter.INSTANCE,
new ApplicationProtocolConfig(
Protocol.ALPN,
SelectorFailureBehavior.FATAL_ALERT,
SelectedListenerFailureBehavior.FATAL_ALERT,
SelectedProtocol.HTTP_2.protocolName(),
SelectedProtocol.HTTP_1_1.protocolName()),
0, 0);
} else {
sslCtx = null;
}
EventLoopGroup workerGroup = new NioEventLoopGroup();
Http2ClientInitializer initializer = new Http2ClientInitializer(sslCtx, Integer.MAX_VALUE);
try {
// Configure the client.
Bootstrap b = new Bootstrap();
b.group(workerGroup);
b.channel(NioSocketChannel.class);
b.option(ChannelOption.SO_KEEPALIVE, true);
b.remoteAddress(HOST, PORT);
b.handler(initializer);
// Start the client.
Channel channel = b.connect().syncUninterruptibly().channel();
System.out.println("Connected to [" + HOST + ':' + PORT + ']');
// Wait for the HTTP/2 upgrade to occur.
Http2SettingsHandler http2SettingsHandler = initializer.settingsHandler();
http2SettingsHandler.awaitSettings(5, TimeUnit.SECONDS);
HttpResponseHandler responseHandler = initializer.responseHandler();
int streamId = 3;
URI hostName = URI.create((SSL ? "https" : "http") + "://" + HOST + ':' + PORT);
System.err.println("Sending request(s)...");
if (URL != null) {
// Create a simple GET request.
FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, GET, URL);
request.headers().add(HttpHeaderNames.HOST, hostName);
request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE);
channel.writeAndFlush(request);
responseHandler.put(streamId, channel.newPromise());
streamId += 2;
}
if (URL2 != null) {
// Create a simple POST request with a body.
FullHttpRequest request = new DefaultFullHttpRequest(HTTP_1_1, POST, URL2,
Unpooled.copiedBuffer(URL2DATA.getBytes(CharsetUtil.UTF_8)));
request.headers().add(HttpHeaderNames.HOST, hostName);
request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE);
channel.writeAndFlush(request);
responseHandler.put(streamId, channel.newPromise());
streamId += 2;
}
responseHandler.awaitResponses(5, TimeUnit.SECONDS);
System.out.println("Finished HTTP/2 request(s)");
// Wait until the connection is closed.
channel.close().syncUninterruptibly();
} finally {
workerGroup.shutdownGracefully();
}
}
}

View File

@ -0,0 +1,158 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.example.http2.client;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.DefaultFullHttpRequest;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpClientUpgradeHandler;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpVersion;
import io.netty.handler.codec.http2.DefaultHttp2Connection;
import io.netty.handler.codec.http2.DefaultHttp2FrameReader;
import io.netty.handler.codec.http2.DefaultHttp2FrameWriter;
import io.netty.handler.codec.http2.DelegatingDecompressorFrameListener;
import io.netty.handler.codec.http2.Http2ClientUpgradeCodec;
import io.netty.handler.codec.http2.Http2Connection;
import io.netty.handler.codec.http2.Http2FrameLogger;
import io.netty.handler.codec.http2.Http2FrameReader;
import io.netty.handler.codec.http2.Http2FrameWriter;
import io.netty.handler.codec.http2.Http2InboundFrameLogger;
import io.netty.handler.codec.http2.Http2OutboundFrameLogger;
import io.netty.handler.codec.http2.HttpToHttp2ConnectionHandler;
import io.netty.handler.codec.http2.InboundHttp2ToHttpAdapter;
import io.netty.handler.ssl.SslContext;
import io.netty.util.internal.logging.InternalLoggerFactory;
import static io.netty.util.internal.logging.InternalLogLevel.INFO;
/**
* Configures the client pipeline to support HTTP/2 frames.
*/
public class Http2ClientInitializer extends ChannelInitializer<SocketChannel> {
private static final Http2FrameLogger logger =
new Http2FrameLogger(INFO, InternalLoggerFactory.getInstance(Http2ClientInitializer.class));
private final SslContext sslCtx;
private final int maxContentLength;
private HttpToHttp2ConnectionHandler connectionHandler;
private HttpResponseHandler responseHandler;
private Http2SettingsHandler settingsHandler;
public Http2ClientInitializer(SslContext sslCtx, int maxContentLength) {
this.sslCtx = sslCtx;
this.maxContentLength = maxContentLength;
}
@Override
public void initChannel(SocketChannel ch) throws Exception {
final Http2Connection connection = new DefaultHttp2Connection(false);
final Http2FrameWriter frameWriter = frameWriter();
connectionHandler = new HttpToHttp2ConnectionHandler(connection,
frameReader(),
frameWriter,
new DelegatingDecompressorFrameListener(connection,
new InboundHttp2ToHttpAdapter.Builder(connection)
.maxContentLength(maxContentLength)
.propagateSettings(true)
.build()));
responseHandler = new HttpResponseHandler();
settingsHandler = new Http2SettingsHandler(ch.newPromise());
if (sslCtx != null) {
configureSsl(ch);
} else {
configureClearText(ch);
}
}
public HttpResponseHandler responseHandler() {
return responseHandler;
}
public Http2SettingsHandler settingsHandler() {
return settingsHandler;
}
protected void configureEndOfPipeline(ChannelPipeline pipeline) {
pipeline.addLast("Http2SettingsHandler", settingsHandler);
pipeline.addLast("HttpResponseHandler", responseHandler);
}
/**
* Configure the pipeline for TLS NPN negotiation to HTTP/2.
*/
private void configureSsl(SocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("SslHandler", sslCtx.newHandler(ch.alloc()));
pipeline.addLast("Http2Handler", connectionHandler);
configureEndOfPipeline(pipeline);
}
/**
* Configure the pipeline for a cleartext upgrade from HTTP to HTTP/2.
*/
private void configureClearText(SocketChannel ch) {
HttpClientCodec sourceCodec = new HttpClientCodec();
Http2ClientUpgradeCodec upgradeCodec = new Http2ClientUpgradeCodec(connectionHandler);
HttpClientUpgradeHandler upgradeHandler = new HttpClientUpgradeHandler(sourceCodec, upgradeCodec, 65536);
ch.pipeline().addLast("Http2SourceCodec", sourceCodec);
ch.pipeline().addLast("Http2UpgradeHandler", upgradeHandler);
ch.pipeline().addLast("Http2UpgradeRequestHandler", new UpgradeRequestHandler());
ch.pipeline().addLast("Logger", new UserEventLogger());
}
/**
* A handler that triggers the cleartext upgrade to HTTP/2 by sending an initial HTTP request.
*/
private final class UpgradeRequestHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
DefaultFullHttpRequest upgradeRequest =
new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/");
ctx.writeAndFlush(upgradeRequest);
ctx.fireChannelActive();
// Done with this handler, remove it from the pipeline.
ctx.pipeline().remove(this);
Http2ClientInitializer.this.configureEndOfPipeline(ctx.pipeline());
}
}
/**
* Class that logs any User Events triggered on this channel.
*/
private static class UserEventLogger extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
System.out.println("User Event Triggered: " + evt);
ctx.fireUserEventTriggered(evt);
}
}
private static Http2FrameReader frameReader() {
return new Http2InboundFrameLogger(new DefaultHttp2FrameReader(), logger);
}
private static Http2FrameWriter frameWriter() {
return new Http2OutboundFrameLogger(new DefaultHttp2FrameWriter(), logger);
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.example.http2.client;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http2.Http2Settings;
import java.util.concurrent.TimeUnit;
/**
* Reads the first {@link Http2Settings} object and notifies a {@link io.netty.channel.ChannelPromise}
*/
public class Http2SettingsHandler extends SimpleChannelInboundHandler<Http2Settings> {
private ChannelPromise promise;
/**
* Create new instance
*
* @param promise Promise object used to notify when first settings are received
*/
public Http2SettingsHandler(ChannelPromise promise) {
this.promise = promise;
}
/**
* Wait for this handler to be added after the upgrade to HTTP/2, and for initial preface
* handshake to complete.
*
* @param timeout Time to wait
* @param unit {@link java.util.concurrent.TimeUnit} for {@code timeout}
* @throws Exception if timeout or other failure occurs
*/
public void awaitSettings(long timeout, TimeUnit unit) throws Exception {
if (!promise.awaitUninterruptibly(timeout, unit)) {
throw new IllegalStateException("Timed out waiting for settings");
}
if (!promise.isSuccess()) {
throw new RuntimeException(promise.cause());
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Http2Settings msg) throws Exception {
promise.setSuccess();
// Only care about the first settings message
ctx.pipeline().remove(this);
}
}

View File

@ -0,0 +1,101 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License, version 2.0 (the
* "License"); you may not use this file except in compliance with the License. You may obtain a
* copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package io.netty.example.http2.client;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http2.HttpUtil;
import io.netty.util.CharsetUtil;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.TimeUnit;
/**
* Process {@link io.netty.handler.codec.http.FullHttpResponse} translated from HTTP/2 frames
*/
public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
private SortedMap<Integer, ChannelPromise> streamidPromiseMap;
public HttpResponseHandler() {
streamidPromiseMap = new TreeMap<Integer, ChannelPromise>();
}
/**
* Create an association between an anticipated response stream id and a {@link io.netty.channel.ChannelPromise}
*
* @param streamId The stream for which a response is expected
* @param promise The promise object that will be used to wait/notify events
* @return The previous object associated with {@code streamId}
* @see HttpResponseHandler#awaitResponses(long, java.util.concurrent.TimeUnit)
*/
public ChannelPromise put(int streamId, ChannelPromise promise) {
return streamidPromiseMap.put(streamId, promise);
}
/**
* Wait (sequentially) for a time duration for each anticipated response
*
* @param timeout Value of time to wait for each response
* @param unit Units associated with {@code timeout}
* @see HttpResponseHandler#put(int, io.netty.channel.ChannelPromise)
*/
public void awaitResponses(long timeout, TimeUnit unit) {
Iterator<Entry<Integer, ChannelPromise>> itr = streamidPromiseMap.entrySet().iterator();
while (itr.hasNext()) {
Entry<Integer, ChannelPromise> entry = itr.next();
ChannelPromise promise = entry.getValue();
if (!promise.awaitUninterruptibly(timeout, unit)) {
throw new IllegalStateException("Timed out waiting for response on stream id " + entry.getKey());
}
if (!promise.isSuccess()) {
throw new RuntimeException(promise.cause());
}
System.out.println("---Stream id: " + entry.getKey() + " received---");
itr.remove();
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception {
Integer streamId = msg.headers().getInt(HttpUtil.ExtensionHeaderNames.STREAM_ID.text());
if (streamId == null) {
System.err.println("HttpResponseHandler unexpected message received: " + msg);
return;
}
ChannelPromise promise = streamidPromiseMap.get(streamId);
if (promise == null) {
System.err.println("Message received for unknown stream id " + streamId);
} else {
// Do stuff with the message (for now just print it)
ByteBuf content = msg.content();
if (content.isReadable()) {
int contentLength = content.readableBytes();
byte[] arr = new byte[contentLength];
content.readBytes(arr);
System.out.println(new String(arr, 0, contentLength, CharsetUtil.UTF_8));
}
promise.setSuccess();
}
}
}

View File

@ -0,0 +1,67 @@
/*
* Copyright 2014 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.example.http2.server;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaderUtil;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpRequest;
import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH;
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
/**
* HTTP handler that responds with a "Hello World"
*/
public class HelloWorldHttp1Handler extends SimpleChannelInboundHandler<HttpRequest> {
@Override
public void channelRead0(ChannelHandlerContext ctx, HttpRequest req) throws Exception {
if (HttpHeaderUtil.is100ContinueExpected(req)) {
ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE));
}
boolean keepAlive = HttpHeaderUtil.isKeepAlive(req);
ByteBuf content = ctx.alloc().buffer();
content.writeBytes(HelloWorldHttp2Handler.RESPONSE_BYTES.duplicate());
FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, content);
response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
response.headers().setInt(CONTENT_LENGTH, response.content().readableBytes());
if (!keepAlive) {
ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
} else {
response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE);
ctx.writeAndFlush(response);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
cause.printStackTrace();
ctx.close();
}
}

Some files were not shown because too many files have changed in this diff Show More