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