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