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 * https://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 io.netty.handler.codec.http;
17
18 import io.netty.buffer.ByteBuf;
19 import io.netty.channel.Channel;
20 import io.netty.channel.ChannelHandlerContext;
21 import io.netty.channel.ChannelPipeline;
22 import io.netty.channel.CombinedChannelDuplexHandler;
23 import io.netty.handler.codec.PrematureChannelClosureException;
24
25 import java.util.ArrayDeque;
26 import java.util.List;
27 import java.util.Queue;
28 import java.util.concurrent.atomic.AtomicLong;
29
30 import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS;
31 import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_ALLOW_PARTIAL_CHUNKS;
32 import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_CHUNK_SIZE;
33 import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_HEADER_SIZE;
34 import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_INITIAL_LINE_LENGTH;
35 import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_VALIDATE_HEADERS;
36
37 /**
38 * A combination of {@link HttpRequestEncoder} and {@link HttpResponseDecoder}
39 * which enables easier client side HTTP implementation. {@link HttpClientCodec}
40 * provides additional state management for <tt>HEAD</tt> and <tt>CONNECT</tt>
41 * requests, which {@link HttpResponseDecoder} lacks. Please refer to
42 * {@link HttpResponseDecoder} to learn what additional state management needs
43 * to be done for <tt>HEAD</tt> and <tt>CONNECT</tt> and why
44 * {@link HttpResponseDecoder} can not handle it by itself.
45 * <p>
46 * If the {@link Channel} is closed and there are missing responses,
47 * a {@link PrematureChannelClosureException} is thrown.
48 *
49 * <h3>Header Validation</h3>
50 *
51 * It is recommended to always enable header validation.
52 * <p>
53 * Without header validation, your system can become vulnerable to
54 * <a href="https://cwe.mitre.org/data/definitions/113.html">
55 * CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Response Splitting')
56 * </a>.
57 * <p>
58 * This recommendation stands even when both peers in the HTTP exchange are trusted,
59 * as it helps with defence-in-depth.
60 *
61 * @see HttpServerCodec
62 */
63 public final class HttpClientCodec extends CombinedChannelDuplexHandler<HttpResponseDecoder, HttpRequestEncoder>
64 implements HttpClientUpgradeHandler.SourceCodec {
65 public static final boolean DEFAULT_FAIL_ON_MISSING_RESPONSE = false;
66 public static final boolean DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST = false;
67
68 /** A queue that is used for correlating a request and a response. */
69 private final Queue<HttpMethod> queue = new ArrayDeque<HttpMethod>();
70 private final boolean parseHttpAfterConnectRequest;
71
72 /** If true, decoding stops (i.e. pass-through) */
73 private boolean done;
74
75 private final AtomicLong requestResponseCounter = new AtomicLong();
76 private final boolean failOnMissingResponse;
77
78 /**
79 * Creates a new instance with the default decoder options
80 * ({@code maxInitialLineLength (4096)}, {@code maxHeaderSize (8192)}, and
81 * {@code maxChunkSize (8192)}).
82 */
83 public HttpClientCodec() {
84 this(new HttpDecoderConfig(),
85 DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST,
86 DEFAULT_FAIL_ON_MISSING_RESPONSE);
87 }
88
89 /**
90 * Creates a new instance with the specified decoder options.
91 */
92 public HttpClientCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
93 this(new HttpDecoderConfig()
94 .setMaxInitialLineLength(maxInitialLineLength)
95 .setMaxHeaderSize(maxHeaderSize)
96 .setMaxChunkSize(maxChunkSize),
97 DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST,
98 DEFAULT_FAIL_ON_MISSING_RESPONSE);
99 }
100
101 /**
102 * Creates a new instance with the specified decoder options.
103 */
104 public HttpClientCodec(
105 int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse) {
106 this(new HttpDecoderConfig()
107 .setMaxInitialLineLength(maxInitialLineLength)
108 .setMaxHeaderSize(maxHeaderSize)
109 .setMaxChunkSize(maxChunkSize),
110 DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST,
111 failOnMissingResponse);
112 }
113
114 /**
115 * Creates a new instance with the specified decoder options.
116 *
117 * @deprecated Prefer the {@link #HttpClientCodec(int, int, int, boolean)} constructor,
118 * to always enable header validation.
119 */
120 @Deprecated
121 public HttpClientCodec(
122 int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
123 boolean validateHeaders) {
124 this(new HttpDecoderConfig()
125 .setMaxInitialLineLength(maxInitialLineLength)
126 .setMaxHeaderSize(maxHeaderSize)
127 .setMaxChunkSize(maxChunkSize)
128 .setValidateHeaders(validateHeaders),
129 DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST,
130 failOnMissingResponse);
131 }
132
133 /**
134 * Creates a new instance with the specified decoder options.
135 *
136 * @deprecated Prefer the {@link #HttpClientCodec(HttpDecoderConfig, boolean, boolean)} constructor,
137 * to always enable header validation.
138 */
139 @Deprecated
140 public HttpClientCodec(
141 int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
142 boolean validateHeaders, boolean parseHttpAfterConnectRequest) {
143 this(new HttpDecoderConfig()
144 .setMaxInitialLineLength(maxInitialLineLength)
145 .setMaxHeaderSize(maxHeaderSize)
146 .setMaxChunkSize(maxChunkSize)
147 .setValidateHeaders(validateHeaders),
148 parseHttpAfterConnectRequest,
149 failOnMissingResponse);
150 }
151
152 /**
153 * Creates a new instance with the specified decoder options.
154 *
155 * @deprecated Prefer the {@link #HttpClientCodec(HttpDecoderConfig, boolean, boolean)} constructor,
156 * to always enable header validation.
157 */
158 @Deprecated
159 public HttpClientCodec(
160 int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
161 boolean validateHeaders, int initialBufferSize) {
162 this(new HttpDecoderConfig()
163 .setMaxInitialLineLength(maxInitialLineLength)
164 .setMaxHeaderSize(maxHeaderSize)
165 .setMaxChunkSize(maxChunkSize)
166 .setValidateHeaders(validateHeaders)
167 .setInitialBufferSize(initialBufferSize),
168 DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST,
169 failOnMissingResponse);
170 }
171
172 /**
173 * Creates a new instance with the specified decoder options.
174 *
175 * @deprecated Prefer the {@link #HttpClientCodec(HttpDecoderConfig, boolean, boolean)} constructor,
176 * to always enable header validation.
177 */
178 @Deprecated
179 public HttpClientCodec(
180 int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
181 boolean validateHeaders, int initialBufferSize, boolean parseHttpAfterConnectRequest) {
182 this(new HttpDecoderConfig()
183 .setMaxInitialLineLength(maxInitialLineLength)
184 .setMaxHeaderSize(maxHeaderSize)
185 .setMaxChunkSize(maxChunkSize)
186 .setValidateHeaders(validateHeaders)
187 .setInitialBufferSize(initialBufferSize),
188 parseHttpAfterConnectRequest,
189 failOnMissingResponse);
190 }
191 /**
192 * Creates a new instance with the specified decoder options.
193 *
194 * @deprecated Prefer the {@link #HttpClientCodec(HttpDecoderConfig, boolean, boolean)} constructor,
195 * to always enable header validation.
196 */
197 @Deprecated
198 public HttpClientCodec(
199 int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
200 boolean validateHeaders, int initialBufferSize, boolean parseHttpAfterConnectRequest,
201 boolean allowDuplicateContentLengths) {
202 this(new HttpDecoderConfig()
203 .setMaxInitialLineLength(maxInitialLineLength)
204 .setMaxHeaderSize(maxHeaderSize)
205 .setMaxChunkSize(maxChunkSize)
206 .setValidateHeaders(validateHeaders)
207 .setInitialBufferSize(initialBufferSize)
208 .setAllowDuplicateContentLengths(allowDuplicateContentLengths),
209 parseHttpAfterConnectRequest,
210 failOnMissingResponse);
211 }
212
213 /**
214 * Creates a new instance with the specified decoder options.
215 *
216 * @deprecated Prefer the {@link #HttpClientCodec(HttpDecoderConfig, boolean, boolean)}
217 * constructor, to always enable header validation.
218 */
219 @Deprecated
220 public HttpClientCodec(
221 int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
222 boolean validateHeaders, int initialBufferSize, boolean parseHttpAfterConnectRequest,
223 boolean allowDuplicateContentLengths, boolean allowPartialChunks) {
224 this(new HttpDecoderConfig()
225 .setMaxInitialLineLength(maxInitialLineLength)
226 .setMaxHeaderSize(maxHeaderSize)
227 .setMaxChunkSize(maxChunkSize)
228 .setValidateHeaders(validateHeaders)
229 .setInitialBufferSize(initialBufferSize)
230 .setAllowDuplicateContentLengths(allowDuplicateContentLengths)
231 .setAllowPartialChunks(allowPartialChunks),
232 parseHttpAfterConnectRequest,
233 failOnMissingResponse);
234 }
235
236 /**
237 * Creates a new instance with the specified decoder options.
238 */
239 public HttpClientCodec(
240 HttpDecoderConfig config, boolean parseHttpAfterConnectRequest, boolean failOnMissingResponse) {
241 init(new Decoder(config), new Encoder());
242 this.parseHttpAfterConnectRequest = parseHttpAfterConnectRequest;
243 this.failOnMissingResponse = failOnMissingResponse;
244 }
245
246 /**
247 * Prepares to upgrade to another protocol from HTTP. Disables the {@link Encoder}.
248 */
249 @Override
250 public void prepareUpgradeFrom(ChannelHandlerContext ctx) {
251 ((Encoder) outboundHandler()).upgraded = true;
252 }
253
254 /**
255 * Upgrades to another protocol from HTTP. Removes the {@link Decoder} and {@link Encoder} from
256 * the pipeline.
257 */
258 @Override
259 public void upgradeFrom(ChannelHandlerContext ctx) {
260 final ChannelPipeline p = ctx.pipeline();
261 p.remove(this);
262 }
263
264 public void setSingleDecode(boolean singleDecode) {
265 inboundHandler().setSingleDecode(singleDecode);
266 }
267
268 public boolean isSingleDecode() {
269 return inboundHandler().isSingleDecode();
270 }
271
272 private final class Encoder extends HttpRequestEncoder {
273
274 boolean upgraded;
275
276 @Override
277 protected void encode(
278 ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
279
280 if (upgraded) {
281 // HttpObjectEncoder overrides .write and does not release msg, so we don't need to retain it here
282 out.add(msg);
283 return;
284 }
285
286 if (msg instanceof HttpRequest) {
287 queue.offer(((HttpRequest) msg).method());
288 }
289
290 super.encode(ctx, msg, out);
291
292 if (failOnMissingResponse && !done) {
293 // check if the request is chunked if so do not increment
294 if (msg instanceof LastHttpContent) {
295 // increment as its the last chunk
296 requestResponseCounter.incrementAndGet();
297 }
298 }
299 }
300 }
301
302 private final class Decoder extends HttpResponseDecoder {
303 Decoder(HttpDecoderConfig config) {
304 super(config);
305 }
306
307 @Override
308 protected void decode(
309 ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
310 if (done) {
311 int readable = actualReadableBytes();
312 if (readable == 0) {
313 // if non is readable just return null
314 // https://github.com/netty/netty/issues/1159
315 return;
316 }
317 out.add(buffer.readBytes(readable));
318 } else {
319 int oldSize = out.size();
320 super.decode(ctx, buffer, out);
321 if (failOnMissingResponse) {
322 int size = out.size();
323 for (int i = oldSize; i < size; i++) {
324 decrement(out.get(i));
325 }
326 }
327 }
328 }
329
330 private void decrement(Object msg) {
331 if (msg == null) {
332 return;
333 }
334
335 // check if it's an Header and its transfer encoding is not chunked.
336 if (msg instanceof LastHttpContent) {
337 requestResponseCounter.decrementAndGet();
338 }
339 }
340
341 @Override
342 protected boolean isContentAlwaysEmpty(HttpMessage msg) {
343 // Get the method of the HTTP request that corresponds to the
344 // current response.
345 //
346 // Even if we do not use the method to compare we still need to poll it to ensure we keep
347 // request / response pairs in sync.
348 HttpMethod method = queue.poll();
349
350 final HttpResponseStatus status = ((HttpResponse) msg).status();
351 final HttpStatusClass statusClass = status.codeClass();
352 final int statusCode = status.code();
353 if (statusClass == HttpStatusClass.INFORMATIONAL) {
354 // An informational response should be excluded from paired comparison.
355 // Just delegate to super method which has all the needed handling.
356 return super.isContentAlwaysEmpty(msg);
357 }
358
359 // If the remote peer did for example send multiple responses for one request (which is not allowed per
360 // spec but may still be possible) method will be null so guard against it.
361 if (method != null) {
362 char firstChar = method.name().charAt(0);
363 switch (firstChar) {
364 case 'H':
365 // According to 4.3, RFC2616:
366 // All responses to the HEAD request method MUST NOT include a
367 // message-body, even though the presence of entity-header fields
368 // might lead one to believe they do.
369 if (HttpMethod.HEAD.equals(method)) {
370 return true;
371
372 // The following code was inserted to work around the servers
373 // that behave incorrectly. It has been commented out
374 // because it does not work with well behaving servers.
375 // Please note, even if the 'Transfer-Encoding: chunked'
376 // header exists in the HEAD response, the response should
377 // have absolutely no content.
378 //
379 //// Interesting edge case:
380 //// Some poorly implemented servers will send a zero-byte
381 //// chunk if Transfer-Encoding of the response is 'chunked'.
382 ////
383 //// return !msg.isChunked();
384 }
385 break;
386 case 'C':
387 // Successful CONNECT request results in a response with empty body.
388 if (statusCode == 200) {
389 if (HttpMethod.CONNECT.equals(method)) {
390 // Proxy connection established - Parse HTTP only if configured by
391 // parseHttpAfterConnectRequest, else pass through.
392 if (!parseHttpAfterConnectRequest) {
393 done = true;
394 queue.clear();
395 }
396 return true;
397 }
398 }
399 break;
400 default:
401 break;
402 }
403 }
404 return super.isContentAlwaysEmpty(msg);
405 }
406
407 @Override
408 public void channelInactive(ChannelHandlerContext ctx)
409 throws Exception {
410 super.channelInactive(ctx);
411
412 if (failOnMissingResponse) {
413 long missingResponses = requestResponseCounter.get();
414 if (missingResponses > 0) {
415 ctx.fireExceptionCaught(new PrematureChannelClosureException(
416 "channel gone inactive with " + missingResponses +
417 " missing response(s)"));
418 }
419 }
420 }
421 }
422 }