From b1cb059540ecfc17c1f2b4d54c0db8e7cc743d8e Mon Sep 17 00:00:00 2001 From: Daniel Schobel Date: Tue, 28 Feb 2017 18:30:52 -0700 Subject: [PATCH] Motivation: It is generally useful to have origin http servers respond to "expect: continue-100" as soon as possible but applications without a HttpObjectAggregator in their pipelines must use boiler plate to do so. Modifications: Introduce the HttpServerExpectContinueHandler handler to make it easier. Result: Less boiler plate for http application authors. --- .../http/HttpServerExpectContinueHandler.java | 97 +++++++++++++++++++ .../HttpServerExpectContinueHandlerTest.java | 83 ++++++++++++++++ .../HttpHelloWorldServerHandler.java | 3 - .../HttpHelloWorldServerInitializer.java | 2 + 4 files changed, 182 insertions(+), 3 deletions(-) create mode 100644 codec-http/src/main/java/io/netty/handler/codec/http/HttpServerExpectContinueHandler.java create mode 100644 codec-http/src/test/java/io/netty/handler/codec/http/HttpServerExpectContinueHandlerTest.java diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerExpectContinueHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerExpectContinueHandler.java new file mode 100644 index 0000000000..4757ca29ac --- /dev/null +++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpServerExpectContinueHandler.java @@ -0,0 +1,97 @@ +/* + * Copyright 2017 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.buffer.Unpooled; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.util.ReferenceCountUtil; + +import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH; +import static io.netty.handler.codec.http.HttpResponseStatus.CONTINUE; +import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; + +/** + * Sends a 100 CONTINUE + * {@link HttpResponse} to {@link HttpRequest}s which contain a 'expect: 100-continue' header. It + * should only be used for applications which do not install the {@link HttpObjectAggregator}. + *

+ * By default it accepts all expectations. + *

+ * Since {@link HttpServerExpectContinueHandler} expects {@link HttpRequest}s it should be added after {@link + * HttpServerCodec} but before any other handlers that might send a {@link HttpResponse}.

+ *
+ *  {@link io.netty.channel.ChannelPipeline} p = ...;
+ *  ...
+ *  p.addLast("serverCodec", new {@link HttpServerCodec}());
+ *  p.addLast("respondExpectContinue", new {@link HttpServerExpectContinueHandler}());
+ *  ...
+ *  p.addLast("handler", new HttpRequestHandler());
+ *  
+ *
+ */ +public class HttpServerExpectContinueHandler extends ChannelInboundHandlerAdapter { + + private static final FullHttpResponse EXPECTATION_FAILED = new DefaultFullHttpResponse( + HTTP_1_1, HttpResponseStatus.EXPECTATION_FAILED, Unpooled.EMPTY_BUFFER); + + private static final FullHttpResponse ACCEPT = new DefaultFullHttpResponse( + HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER); + + static { + EXPECTATION_FAILED.headers().set(CONTENT_LENGTH, 0); + ACCEPT.headers().set(CONTENT_LENGTH, 0); + } + + /** + * Produces a {@link HttpResponse} for {@link HttpRequest}s which define an expectation. Returns {@code null} if the + * request should be rejected. See {@link #rejectResponse(HttpRequest)}. + */ + protected HttpResponse acceptMessage(@SuppressWarnings("unused") HttpRequest request) { + return ACCEPT.retainedDuplicate(); + } + + /** + * Returns the appropriate 4XX {@link HttpResponse} for the given {@link HttpRequest}. + */ + protected HttpResponse rejectResponse(@SuppressWarnings("unused") HttpRequest request) { + return EXPECTATION_FAILED.retainedDuplicate(); + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof HttpRequest) { + HttpRequest req = (HttpRequest) msg; + + if (HttpUtil.is100ContinueExpected(req)) { + HttpResponse accept = acceptMessage(req); + + if (accept == null) { + // the expectation failed so we refuse the request. + HttpResponse rejection = rejectResponse(req); + ReferenceCountUtil.release(msg); + ctx.writeAndFlush(rejection).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + return; + } + + ctx.writeAndFlush(accept).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); + req.headers().remove(HttpHeaderNames.EXPECT); + } + } + super.channelRead(ctx, msg); + } +} diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpServerExpectContinueHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpServerExpectContinueHandlerTest.java new file mode 100644 index 0000000000..e3288cefc1 --- /dev/null +++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpServerExpectContinueHandlerTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2017 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.embedded.EmbeddedChannel; +import io.netty.util.ReferenceCountUtil; +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +public class HttpServerExpectContinueHandlerTest { + + @Test + public void shouldRespondToExpectedHeader() { + EmbeddedChannel channel = new EmbeddedChannel(new HttpServerExpectContinueHandler() { + @Override + protected HttpResponse acceptMessage(HttpRequest request) { + HttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.CONTINUE); + response.headers().set("foo", "bar"); + return response; + } + }); + HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); + HttpUtil.set100ContinueExpected(request, true); + + channel.writeInbound(request); + HttpResponse response = channel.readOutbound(); + + assertThat(response.status(), is(HttpResponseStatus.CONTINUE)); + assertThat(response.headers().get("foo"), is("bar")); + ReferenceCountUtil.release(response); + + HttpRequest processedRequest = channel.readInbound(); + assertFalse(processedRequest.headers().contains(HttpHeaderNames.EXPECT)); + ReferenceCountUtil.release(processedRequest); + assertFalse(channel.finishAndReleaseAll()); + } + + @Test + public void shouldAllowCustomResponses() { + EmbeddedChannel channel = new EmbeddedChannel( + new HttpServerExpectContinueHandler() { + @Override + protected HttpResponse acceptMessage(HttpRequest request) { + return null; + } + + @Override + protected HttpResponse rejectResponse(HttpRequest request) { + return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, + HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE); + } + } + ); + + HttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, "/"); + HttpUtil.set100ContinueExpected(request, true); + + channel.writeInbound(request); + HttpResponse response = channel.readOutbound(); + + assertThat(response.status(), is(HttpResponseStatus.REQUEST_ENTITY_TOO_LARGE)); + ReferenceCountUtil.release(response); + + // request was swallowed + assertTrue(channel.inboundMessages().isEmpty()); + assertFalse(channel.finishAndReleaseAll()); + } +} diff --git a/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerHandler.java b/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerHandler.java index 15c9b722bf..511c719b92 100644 --- a/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerHandler.java +++ b/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerHandler.java @@ -45,9 +45,6 @@ public class HttpHelloWorldServerHandler extends ChannelInboundHandlerAdapter { if (msg instanceof HttpRequest) { HttpRequest req = (HttpRequest) msg; - if (HttpUtil.is100ContinueExpected(req)) { - ctx.write(new DefaultFullHttpResponse(HTTP_1_1, CONTINUE)); - } boolean keepAlive = HttpUtil.isKeepAlive(req); FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK, Unpooled.wrappedBuffer(CONTENT)); response.headers().set(CONTENT_TYPE, "text/plain"); diff --git a/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerInitializer.java b/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerInitializer.java index f6e638767c..648c04aac1 100644 --- a/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerInitializer.java +++ b/example/src/main/java/io/netty/example/http/helloworld/HttpHelloWorldServerInitializer.java @@ -19,6 +19,7 @@ import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelPipeline; import io.netty.channel.socket.SocketChannel; import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpServerExpectContinueHandler; import io.netty.handler.ssl.SslContext; public class HttpHelloWorldServerInitializer extends ChannelInitializer { @@ -36,6 +37,7 @@ public class HttpHelloWorldServerInitializer extends ChannelInitializer