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