1 /*
2 * Copyright 2010 Red Hat, Inc.
3 *
4 * Red Hat licenses this file to you under the Apache License, version 2.0
5 * (the "License"); you may not use this file except in compliance with the
6 * License. You may obtain a copy of the License at:
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 * License for the specific language governing permissions and limitations
14 * under the License.
15 */
16 package org.jboss.netty.handler.codec.http2;
17
18 import java.util.Queue;
19 import java.util.concurrent.atomic.AtomicLong;
20
21 import org.jboss.netty.buffer.ChannelBuffer;
22 import org.jboss.netty.channel.Channel;
23 import org.jboss.netty.channel.ChannelDownstreamHandler;
24 import org.jboss.netty.channel.ChannelEvent;
25 import org.jboss.netty.channel.ChannelHandlerContext;
26 import org.jboss.netty.channel.ChannelStateEvent;
27 import org.jboss.netty.channel.ChannelUpstreamHandler;
28 import org.jboss.netty.handler.codec.PrematureChannelClosureException;
29 import org.jboss.netty.util.internal.QueueFactory;
30
31 /**
32 * A combination of {@link HttpRequestEncoder} and {@link HttpResponseDecoder}
33 * which enables easier client side HTTP implementation. {@link HttpClientCodec}
34 * provides additional state management for <tt>HEAD</tt> and <tt>CONNECT</tt>
35 * requests, which {@link HttpResponseDecoder} lacks. Please refer to
36 * {@link HttpResponseDecoder} to learn what additional state management needs
37 * to be done for <tt>HEAD</tt> and <tt>CONNECT</tt> and why
38 * {@link HttpResponseDecoder} can not handle it by itself.
39 *
40 * @author <a href="http://www.jboss.org/netty/">The Netty Project</a>
41 * @author <a href="http://gleamynode.net/">Trustin Lee</a>
42 * @version $Rev: 1108 $, $Date: 2012-04-21 12:51:25 +0200 (sam., 21 avr. 2012) $
43 *
44 * @see HttpServerCodec
45 *
46 * @apiviz.has org.jboss.netty.handler.codec.http2.HttpResponseDecoder
47 * @apiviz.has org.jboss.netty.handler.codec.http2.HttpRequestEncoder
48 */
49 public class HttpClientCodec implements ChannelUpstreamHandler,
50 ChannelDownstreamHandler {
51
52 /** A queue that is used for correlating a request and a response. */
53 final Queue<HttpMethod> queue = QueueFactory.createQueue(HttpMethod.class);
54
55 /** If true, decoding stops (i.e. pass-through) */
56 volatile boolean done;
57
58 private final HttpRequestEncoder encoder = new Encoder();
59
60 private final HttpResponseDecoder decoder;
61
62 private final AtomicLong requestResponseCounter = new AtomicLong(0);
63
64 private final boolean failOnMissingResponse;
65
66 /**
67 * Creates a new instance with the default decoder options
68 * ({@code maxInitialLineLength (4096}}, {@code maxHeaderSize (8192)}, and
69 * {@code maxChunkSize (8192)}).
70 *
71 */
72 public HttpClientCodec() {
73 this(4096, 8192, 8192, false);
74 }
75
76 /**
77 * Creates a new instance with the specified decoder options.
78 */
79 public HttpClientCodec(int maxInitialLineLength, int maxHeaderSize,
80 int maxChunkSize) {
81 this(maxInitialLineLength, maxHeaderSize, maxChunkSize, false);
82 }
83
84 /**
85 * Creates a new instance with the specified decoder options.
86 */
87 public HttpClientCodec(int maxInitialLineLength, int maxHeaderSize,
88 int maxChunkSize, boolean failOnMissingResponse) {
89 decoder = new Decoder(maxInitialLineLength, maxHeaderSize, maxChunkSize);
90 this.failOnMissingResponse = failOnMissingResponse;
91 }
92
93 @Override
94 public void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e)
95 throws Exception {
96 decoder.handleUpstream(ctx, e);
97 }
98
99 @Override
100 public void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e)
101 throws Exception {
102 encoder.handleDownstream(ctx, e);
103 }
104
105 private final class Encoder extends HttpRequestEncoder {
106
107 Encoder() {
108 }
109
110 @Override
111 protected Object encode(ChannelHandlerContext ctx, Channel channel,
112 Object msg) throws Exception {
113 if (msg instanceof HttpRequest && !done) {
114 queue.offer(((HttpRequest) msg).getMethod());
115 }
116
117 Object obj = super.encode(ctx, channel, msg);
118
119 if (failOnMissingResponse) {
120 // check if the request is chunked if so do not increment
121 if (msg instanceof HttpRequest &&
122 !((HttpRequest) msg).isChunked()) {
123 requestResponseCounter.incrementAndGet();
124 } else if (msg instanceof HttpChunk &&
125 ((HttpChunk) msg).isLast()) {
126 // increment as its the last chunk
127 requestResponseCounter.incrementAndGet();
128 }
129 }
130
131 return obj;
132
133 }
134 }
135
136 private final class Decoder extends HttpResponseDecoder {
137
138 Decoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
139 super(maxInitialLineLength, maxHeaderSize, maxChunkSize);
140 }
141
142 @Override
143 protected Object decode(ChannelHandlerContext ctx, Channel channel,
144 ChannelBuffer buffer, State state) throws Exception {
145 if (done) {
146 return buffer.readBytes(actualReadableBytes());
147 } else {
148 Object msg = super.decode(ctx, channel, buffer, state);
149 if (failOnMissingResponse) {
150 decrement(msg);
151 }
152 return msg;
153 }
154 }
155
156 private void decrement(Object msg) {
157 if (msg == null) {
158 return;
159 }
160
161 // check if its a HttpMessage and its not chunked
162 if (msg instanceof HttpMessage && !((HttpMessage) msg).isChunked()) {
163 requestResponseCounter.decrementAndGet();
164 } else if (msg instanceof HttpChunk && ((HttpChunk) msg).isLast()) {
165 requestResponseCounter.decrementAndGet();
166 } else if (msg instanceof Object[]) {
167 // we just decrement it here as we only use this if the end of the chunk is reached
168 // It would be more safe to check all the objects in the array but would also be slower
169 requestResponseCounter.decrementAndGet();
170 }
171 }
172
173 @Override
174 protected boolean isContentAlwaysEmpty(HttpMessage msg) {
175 final int statusCode = ((HttpResponse) msg).getStatus().getCode();
176 if (statusCode == 100) {
177 // 100-continue response should be excluded from paired comparison.
178 return true;
179 }
180
181 // Get the method of the HTTP request that corresponds to the
182 // current response.
183 HttpMethod method = queue.poll();
184
185 char firstChar = method.getName().charAt(0);
186 switch (firstChar) {
187 case 'H':
188 // According to 4.3, RFC2616:
189 // All responses to the HEAD request method MUST NOT include a
190 // message-body, even though the presence of entity-header fields
191 // might lead one to believe they do.
192 if (HttpMethod.HEAD.equals(method)) {
193 return true;
194
195 // The following code was inserted to work around the servers
196 // that behave incorrectly. It has been commented out
197 // because it does not work with well behaving servers.
198 // Please note, even if the 'Transfer-Encoding: chunked'
199 // header exists in the HEAD response, the response should
200 // have absolutely no content.
201 //
202 //// Interesting edge case:
203 //// Some poorly implemented servers will send a zero-byte
204 //// chunk if Transfer-Encoding of the response is 'chunked'.
205 ////
206 //// return !msg.isChunked();
207 }
208 break;
209 case 'C':
210 // Successful CONNECT request results in a response with empty body.
211 if (statusCode == 200) {
212 if (HttpMethod.CONNECT.equals(method)) {
213 // Proxy connection established - Not HTTP anymore.
214 done = true;
215 queue.clear();
216 return true;
217 }
218 }
219 break;
220 }
221
222 return super.isContentAlwaysEmpty(msg);
223 }
224
225 @Override
226 public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e)
227 throws Exception {
228 super.channelClosed(ctx, e);
229
230 if (failOnMissingResponse) {
231 long missingResponses = requestResponseCounter.get();
232 if (missingResponses > 0) {
233 throw new PrematureChannelClosureException(
234 "Channel closed but still missing " +
235 missingResponses + " response(s)");
236 }
237 }
238 }
239
240 }
241 }