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 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                 int readable = actualReadableBytes();
138                 if (readable == 0) {
139                     return null;
140                 }
141                 return buffer.readBytes(readable);
142             } else {
143                 Object msg = super.decode(ctx, channel, buffer, state);
144                 if (failOnMissingResponse) {
145                     decrement(msg);
146                 }
147                 return msg;
148             }
149         }
150 
151         private void decrement(Object msg) {
152             if (msg == null) {
153                 return;
154             }
155 
156             // check if its a HttpMessage and its not chunked
157             if (msg instanceof HttpMessage && !((HttpMessage) msg).isChunked()) {
158                 requestResponseCounter.decrementAndGet();
159             } else if (msg instanceof HttpChunk && ((HttpChunk) msg).isLast()) {
160                 requestResponseCounter.decrementAndGet();
161             } else if (msg instanceof Object[]) {
162                 // we just decrement it here as we only use this if the end of the chunk is reached
163                 // It would be more safe to check all the objects in the array but would also be slower
164                 requestResponseCounter.decrementAndGet();
165             }
166         }
167         @Override
168         protected boolean isContentAlwaysEmpty(HttpMessage msg) {
169             final int statusCode = ((HttpResponse) msg).getStatus().getCode();
170             if (statusCode == 100) {
171                 // 100-continue response should be excluded from paired comparison.
172                 return true;
173             }
174 
175             // Get the method of the HTTP request that corresponds to the
176             // current response.
177             HttpMethod method = queue.poll();
178 
179             char firstChar = method.getName().charAt(0);
180             switch (firstChar) {
181             case 'H':
182                 // According to 4.3, RFC2616:
183                 // All responses to the HEAD request method MUST NOT include a
184                 // message-body, even though the presence of entity-header fields
185                 // might lead one to believe they do.
186                 if (HttpMethod.HEAD.equals(method)) {
187                     return true;
188 
189                     // The following code was inserted to work around the servers
190                     // that behave incorrectly.  It has been commented out
191                     // because it does not work with well behaving servers.
192                     // Please note, even if the 'Transfer-Encoding: chunked'
193                     // header exists in the HEAD response, the response should
194                     // have absolutely no content.
195                     //
196                     //// Interesting edge case:
197                     //// Some poorly implemented servers will send a zero-byte
198                     //// chunk if Transfer-Encoding of the response is 'chunked'.
199                     ////
200                     //// return !msg.isChunked();
201                 }
202                 break;
203             case 'C':
204                 // Successful CONNECT request results in a response with empty body.
205                 if (statusCode == 200) {
206                     if (HttpMethod.CONNECT.equals(method)) {
207                         // Proxy connection established - Not HTTP anymore.
208                         done = true;
209                         queue.clear();
210                         return true;
211                     }
212                 }
213                 break;
214             }
215 
216             return super.isContentAlwaysEmpty(msg);
217         }
218 
219         @Override
220         public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
221             super.channelClosed(ctx, e);
222             if (failOnMissingResponse) {
223                 long missingResponses = requestResponseCounter.get();
224                 if (missingResponses > 0) {
225                     throw new PrematureChannelClosureException(
226                             "Channel closed but still missing " + missingResponses + " response(s)");
227                 }
228             }
229         }
230     }
231 }