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