DefaultHttp2Headers should throw exception of type Http2Exception

Motivation:
The DefaultHttp2Headers code is throwing a IllegalArgumentException if an invalid character is detected. This is being ignored by the HTTP/2 codec instead of generating a GOAWAY.

Modifications:
- Throw a Http2Exception of type PROTOCOL_ERROR in accordance with https://tools.ietf.org/html/rfc7540#section-8.1.2.6
- Update examples which were building invalid headers

Result:
More compliant with https://tools.ietf.org/html/rfc7540#section-8.1.2.6
This commit is contained in:
Scott Mitchell 2015-09-10 23:54:57 -07:00
parent 2a27d581a9
commit 1d4d5fe312
5 changed files with 46 additions and 24 deletions

View File

@ -14,6 +14,8 @@
*/
package io.netty.handler.codec.http2;
import static io.netty.handler.codec.http2.Http2Exception.connectionError;
import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR;
import io.netty.handler.codec.ByteStringValueConverter;
import io.netty.handler.codec.DefaultHeaders;
import io.netty.handler.codec.Headers;
@ -25,19 +27,26 @@ public class DefaultHttp2Headers extends DefaultHeaders<ByteString> implements H
private static final ByteProcessor HTTP2_NAME_VALIDATOR_PROCESSOR = new ByteProcessor() {
@Override
public boolean process(byte value) throws Exception {
if (value >= 'A' && value <= 'Z') {
throw new IllegalArgumentException("name must be all lower case but found: " + (char) value);
}
return true;
return value < 'A' || value > 'Z';
}
};
private static final NameValidator<ByteString> HTTP2_NAME_VALIDATOR = new NameValidator<ByteString>() {
@Override
public void validateName(ByteString name) {
final int index;
try {
name.forEachByte(HTTP2_NAME_VALIDATOR_PROCESSOR);
} catch (Exception e) {
index = name.forEachByte(HTTP2_NAME_VALIDATOR_PROCESSOR);
} catch (Http2Exception e) {
PlatformDependent.throwException(e);
return;
} catch (Throwable t) {
PlatformDependent.throwException(connectionError(PROTOCOL_ERROR, t,
"unexpected error. invalid header name [%s]", name));
return;
}
if (index != -1) {
PlatformDependent.throwException(connectionError(PROTOCOL_ERROR, "invalid header name [%s]", name));
}
}
};

View File

@ -65,7 +65,7 @@ public class DefaultHttp2HeadersTest {
}
}
@Test(expected = IllegalArgumentException.class)
@Test(expected = Http2Exception.class)
public void testHeaderNameValidation() {
Http2Headers headers = newHeaders();

View File

@ -31,7 +31,7 @@ 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";
public static final String UPGRADE_RESPONSE_HEADER = "http-to-http2-upgrade";
/**
* Size of the block to be read from the input stream.

View File

@ -25,7 +25,9 @@ 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.http.HttpScheme;
import io.netty.handler.codec.http2.Http2SecurityUtil;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
@ -37,9 +39,9 @@ import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.SupportedCipherSuiteFilter;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.util.AsciiString;
import io.netty.util.CharsetUtil;
import java.net.URI;
import java.util.concurrent.TimeUnit;
import static io.netty.handler.codec.http.HttpMethod.GET;
@ -106,16 +108,17 @@ public final class Http2Client {
HttpResponseHandler responseHandler = initializer.responseHandler();
int streamId = 3;
URI hostName = URI.create((SSL ? "https" : "http") + "://" + HOST + ':' + PORT);
HttpScheme scheme = SSL ? HttpScheme.HTTPS : HttpScheme.HTTP;
AsciiString hostName = new AsciiString(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(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme.name());
request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE);
channel.writeAndFlush(request);
responseHandler.put(streamId, channel.newPromise());
responseHandler.put(streamId, channel.writeAndFlush(request), channel.newPromise());
streamId += 2;
}
if (URL2 != null) {
@ -123,10 +126,10 @@ public final class Http2Client {
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(HttpConversionUtil.ExtensionHeaderNames.SCHEME.text(), scheme.name());
request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.GZIP);
request.headers().add(HttpHeaderNames.ACCEPT_ENCODING, HttpHeaderValues.DEFLATE);
channel.writeAndFlush(request);
responseHandler.put(streamId, channel.newPromise());
responseHandler.put(streamId, channel.writeAndFlush(request), channel.newPromise());
streamId += 2;
}
responseHandler.awaitResponses(5, TimeUnit.SECONDS);

View File

@ -15,6 +15,7 @@
package io.netty.example.http2.helloworld.client;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.channel.SimpleChannelInboundHandler;
@ -22,6 +23,7 @@ import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http2.HttpConversionUtil;
import io.netty.util.CharsetUtil;
import java.util.AbstractMap.SimpleEntry;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.SortedMap;
@ -33,22 +35,23 @@ import java.util.concurrent.TimeUnit;
*/
public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpResponse> {
private SortedMap<Integer, ChannelPromise> streamidPromiseMap;
private SortedMap<Integer, Entry<ChannelFuture, ChannelPromise>> streamidPromiseMap;
public HttpResponseHandler() {
streamidPromiseMap = new TreeMap<Integer, ChannelPromise>();
streamidPromiseMap = new TreeMap<Integer, Entry<ChannelFuture, 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 writeFuture A future that represent the request write operation
* @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);
public Entry<ChannelFuture, ChannelPromise> put(int streamId, ChannelFuture writeFuture, ChannelPromise promise) {
return streamidPromiseMap.put(streamId, new SimpleEntry<ChannelFuture, ChannelPromise>(writeFuture, promise));
}
/**
@ -59,10 +62,17 @@ public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpRes
* @see HttpResponseHandler#put(int, io.netty.channel.ChannelPromise)
*/
public void awaitResponses(long timeout, TimeUnit unit) {
Iterator<Entry<Integer, ChannelPromise>> itr = streamidPromiseMap.entrySet().iterator();
Iterator<Entry<Integer, Entry<ChannelFuture, ChannelPromise>>> itr = streamidPromiseMap.entrySet().iterator();
while (itr.hasNext()) {
Entry<Integer, ChannelPromise> entry = itr.next();
ChannelPromise promise = entry.getValue();
Entry<Integer, Entry<ChannelFuture, ChannelPromise>> entry = itr.next();
ChannelFuture writeFuture = entry.getValue().getKey();
if (!writeFuture.awaitUninterruptibly(timeout, unit)) {
throw new IllegalStateException("Timed out waiting to write for stream id " + entry.getKey());
}
if (!writeFuture.isSuccess()) {
throw new RuntimeException(writeFuture.cause());
}
ChannelPromise promise = entry.getValue().getValue();
if (!promise.awaitUninterruptibly(timeout, unit)) {
throw new IllegalStateException("Timed out waiting for response on stream id " + entry.getKey());
}
@ -82,8 +92,8 @@ public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpRes
return;
}
ChannelPromise promise = streamidPromiseMap.get(streamId);
if (promise == null) {
Entry<ChannelFuture, ChannelPromise> entry = streamidPromiseMap.get(streamId);
if (entry == null) {
System.err.println("Message received for unknown stream id " + streamId);
} else {
// Do stuff with the message (for now just print it)
@ -95,7 +105,7 @@ public class HttpResponseHandler extends SimpleChannelInboundHandler<FullHttpRes
System.out.println(new String(arr, 0, contentLength, CharsetUtil.UTF_8));
}
promise.setSuccess();
entry.getValue().setSuccess();
}
}
}