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    *   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 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  import io.netty.util.ReferenceCountUtil;
25  
26  import java.util.ArrayDeque;
27  import java.util.List;
28  import java.util.Queue;
29  import java.util.concurrent.atomic.AtomicLong;
30  
31  import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS;
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   *
46   * If the {@link Channel} is closed and there are missing responses,
47   * a {@link PrematureChannelClosureException} is thrown.
48   *
49   * @see HttpServerCodec
50   */
51  public final class HttpClientCodec extends CombinedChannelDuplexHandler<HttpResponseDecoder, HttpRequestEncoder>
52          implements HttpClientUpgradeHandler.SourceCodec {
53      public static final boolean DEFAULT_FAIL_ON_MISSING_RESPONSE = false;
54      public static final boolean DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST = false;
55  
56      /** A queue that is used for correlating a request and a response. */
57      private final Queue<HttpMethod> queue = new ArrayDeque<HttpMethod>();
58      private final boolean parseHttpAfterConnectRequest;
59  
60      /** If true, decoding stops (i.e. pass-through) */
61      private boolean done;
62  
63      private final AtomicLong requestResponseCounter = new AtomicLong();
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      public HttpClientCodec() {
72          this(DEFAULT_MAX_INITIAL_LINE_LENGTH, DEFAULT_MAX_HEADER_SIZE, DEFAULT_MAX_CHUNK_SIZE,
73               DEFAULT_FAIL_ON_MISSING_RESPONSE);
74      }
75  
76      /**
77       * Creates a new instance with the specified decoder options.
78       */
79      public HttpClientCodec(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
80          this(maxInitialLineLength, maxHeaderSize, maxChunkSize, DEFAULT_FAIL_ON_MISSING_RESPONSE);
81      }
82  
83      /**
84       * Creates a new instance with the specified decoder options.
85       */
86      public HttpClientCodec(
87              int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse) {
88          this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, DEFAULT_VALIDATE_HEADERS);
89      }
90  
91      /**
92       * Creates a new instance with the specified decoder options.
93       */
94      public HttpClientCodec(
95              int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
96              boolean validateHeaders) {
97          this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, validateHeaders,
98               DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST);
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             boolean validateHeaders, boolean parseHttpAfterConnectRequest) {
107         init(new Decoder(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders), new Encoder());
108         this.failOnMissingResponse = failOnMissingResponse;
109         this.parseHttpAfterConnectRequest = parseHttpAfterConnectRequest;
110     }
111 
112     /**
113      * Creates a new instance with the specified decoder options.
114      */
115     public HttpClientCodec(
116             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
117             boolean validateHeaders, int initialBufferSize) {
118         this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, validateHeaders,
119              initialBufferSize, DEFAULT_PARSE_HTTP_AFTER_CONNECT_REQUEST);
120     }
121 
122     /**
123      * Creates a new instance with the specified decoder options.
124      */
125     public HttpClientCodec(
126             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
127             boolean validateHeaders, int initialBufferSize, boolean parseHttpAfterConnectRequest) {
128         this(maxInitialLineLength, maxHeaderSize, maxChunkSize, failOnMissingResponse, validateHeaders,
129              initialBufferSize, parseHttpAfterConnectRequest, DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS);
130     }
131 
132     /**
133      * Creates a new instance with the specified decoder options.
134      */
135     public HttpClientCodec(
136             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean failOnMissingResponse,
137             boolean validateHeaders, int initialBufferSize, boolean parseHttpAfterConnectRequest,
138             boolean allowDuplicateContentLengths) {
139         init(new Decoder(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize,
140                          allowDuplicateContentLengths),
141              new Encoder());
142         this.parseHttpAfterConnectRequest = parseHttpAfterConnectRequest;
143         this.failOnMissingResponse = failOnMissingResponse;
144     }
145 
146     /**
147      * Prepares to upgrade to another protocol from HTTP. Disables the {@link Encoder}.
148      */
149     @Override
150     public void prepareUpgradeFrom(ChannelHandlerContext ctx) {
151         ((Encoder) outboundHandler()).upgraded = true;
152     }
153 
154     /**
155      * Upgrades to another protocol from HTTP. Removes the {@link Decoder} and {@link Encoder} from
156      * the pipeline.
157      */
158     @Override
159     public void upgradeFrom(ChannelHandlerContext ctx) {
160         final ChannelPipeline p = ctx.pipeline();
161         p.remove(this);
162     }
163 
164     public void setSingleDecode(boolean singleDecode) {
165         inboundHandler().setSingleDecode(singleDecode);
166     }
167 
168     public boolean isSingleDecode() {
169         return inboundHandler().isSingleDecode();
170     }
171 
172     private final class Encoder extends HttpRequestEncoder {
173 
174         boolean upgraded;
175 
176         @Override
177         protected void encode(
178                 ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
179 
180             if (upgraded) {
181                 out.add(ReferenceCountUtil.retain(msg));
182                 return;
183             }
184 
185             if (msg instanceof HttpRequest) {
186                 queue.offer(((HttpRequest) msg).method());
187             }
188 
189             super.encode(ctx, msg, out);
190 
191             if (failOnMissingResponse && !done) {
192                 // check if the request is chunked if so do not increment
193                 if (msg instanceof LastHttpContent) {
194                     // increment as its the last chunk
195                     requestResponseCounter.incrementAndGet();
196                 }
197             }
198         }
199     }
200 
201     private final class Decoder extends HttpResponseDecoder {
202         Decoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean validateHeaders) {
203             super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders);
204         }
205 
206         Decoder(int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean validateHeaders,
207                 int initialBufferSize, boolean allowDuplicateContentLengths) {
208             super(maxInitialLineLength, maxHeaderSize, maxChunkSize, validateHeaders, initialBufferSize,
209                   allowDuplicateContentLengths);
210         }
211 
212         @Override
213         protected void decode(
214                 ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
215             if (done) {
216                 int readable = actualReadableBytes();
217                 if (readable == 0) {
218                     // if non is readable just return null
219                     // https://github.com/netty/netty/issues/1159
220                     return;
221                 }
222                 out.add(buffer.readBytes(readable));
223             } else {
224                 int oldSize = out.size();
225                 super.decode(ctx, buffer, out);
226                 if (failOnMissingResponse) {
227                     int size = out.size();
228                     for (int i = oldSize; i < size; i++) {
229                         decrement(out.get(i));
230                     }
231                 }
232             }
233         }
234 
235         private void decrement(Object msg) {
236             if (msg == null) {
237                 return;
238             }
239 
240             // check if it's an Header and its transfer encoding is not chunked.
241             if (msg instanceof LastHttpContent) {
242                 requestResponseCounter.decrementAndGet();
243             }
244         }
245 
246         @Override
247         protected boolean isContentAlwaysEmpty(HttpMessage msg) {
248             // Get the method of the HTTP request that corresponds to the
249             // current response.
250             //
251             // Even if we do not use the method to compare we still need to poll it to ensure we keep
252             // request / response pairs in sync.
253             HttpMethod method = queue.poll();
254 
255             final int statusCode = ((HttpResponse) msg).status().code();
256             if (statusCode >= 100 && statusCode < 200) {
257                 // An informational response should be excluded from paired comparison.
258                 // Just delegate to super method which has all the needed handling.
259                 return super.isContentAlwaysEmpty(msg);
260             }
261 
262             // If the remote peer did for example send multiple responses for one request (which is not allowed per
263             // spec but may still be possible) method will be null so guard against it.
264             if (method != null) {
265                 char firstChar = method.name().charAt(0);
266                 switch (firstChar) {
267                     case 'H':
268                         // According to 4.3, RFC2616:
269                         // All responses to the HEAD request method MUST NOT include a
270                         // message-body, even though the presence of entity-header fields
271                         // might lead one to believe they do.
272                         if (HttpMethod.HEAD.equals(method)) {
273                             return true;
274 
275                             // The following code was inserted to work around the servers
276                             // that behave incorrectly.  It has been commented out
277                             // because it does not work with well behaving servers.
278                             // Please note, even if the 'Transfer-Encoding: chunked'
279                             // header exists in the HEAD response, the response should
280                             // have absolutely no content.
281                             //
282                             //// Interesting edge case:
283                             //// Some poorly implemented servers will send a zero-byte
284                             //// chunk if Transfer-Encoding of the response is 'chunked'.
285                             ////
286                             //// return !msg.isChunked();
287                         }
288                         break;
289                     case 'C':
290                         // Successful CONNECT request results in a response with empty body.
291                         if (statusCode == 200) {
292                             if (HttpMethod.CONNECT.equals(method)) {
293                                 // Proxy connection established - Parse HTTP only if configured by
294                                 // parseHttpAfterConnectRequest, else pass through.
295                                 if (!parseHttpAfterConnectRequest) {
296                                     done = true;
297                                     queue.clear();
298                                 }
299                                 return true;
300                             }
301                         }
302                         break;
303                 }
304             }
305             return super.isContentAlwaysEmpty(msg);
306         }
307 
308         @Override
309         public void channelInactive(ChannelHandlerContext ctx)
310                 throws Exception {
311             super.channelInactive(ctx);
312 
313             if (failOnMissingResponse) {
314                 long missingResponses = requestResponseCounter.get();
315                 if (missingResponses > 0) {
316                     ctx.fireExceptionCaught(new PrematureChannelClosureException(
317                             "channel gone inactive with " + missingResponses +
318                             " missing response(s)"));
319                 }
320             }
321         }
322     }
323 }