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