View Javadoc
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 }