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.netty.handler.codec.http;
17  
18  import io.netty.buffer.ByteBuf;
19  import io.netty.buffer.Unpooled;
20  import io.netty.channel.ChannelHandlerContext;
21  import io.netty.channel.ChannelPipeline;
22  import io.netty.handler.codec.ByteToMessageDecoder;
23  import io.netty.handler.codec.DecoderResult;
24  import io.netty.handler.codec.PrematureChannelClosureException;
25  import io.netty.handler.codec.TooLongFrameException;
26  import io.netty.util.AsciiString;
27  import io.netty.util.ByteProcessor;
28  import io.netty.util.internal.StringUtil;
29  
30  import java.util.List;
31  import java.util.concurrent.atomic.AtomicBoolean;
32  
33  import static io.netty.util.internal.ObjectUtil.checkNotNull;
34  
35  /**
36   * Decodes {@link ByteBuf}s into {@link HttpMessage}s and
37   * {@link HttpContent}s.
38   *
39   * <h3>Parameters that prevents excessive memory consumption</h3>
40   * <table border="1">
41   * <tr>
42   * <th>Name</th><th>Default value</th><th>Meaning</th>
43   * </tr>
44   * <tr>
45   * <td>{@code maxInitialLineLength}</td>
46   * <td>{@value #DEFAULT_MAX_INITIAL_LINE_LENGTH}</td>
47   * <td>The maximum length of the initial line
48   *     (e.g. {@code "GET / HTTP/1.0"} or {@code "HTTP/1.0 200 OK"})
49   *     If the length of the initial line exceeds this value, a
50   *     {@link TooLongHttpLineException} will be raised.</td>
51   * </tr>
52   * <tr>
53   * <td>{@code maxHeaderSize}</td>
54   * <td>{@value #DEFAULT_MAX_HEADER_SIZE}</td>
55   * <td>The maximum length of all headers.  If the sum of the length of each
56   *     header exceeds this value, a {@link TooLongHttpHeaderException} will be raised.</td>
57   * </tr>
58   * <tr>
59   * <td>{@code maxChunkSize}</td>
60   * <td>{@value #DEFAULT_MAX_CHUNK_SIZE}</td>
61   * <td>The maximum length of the content or each chunk.  If the content length
62   *     (or the length of each chunk) exceeds this value, the content or chunk
63   *     will be split into multiple {@link HttpContent}s whose length is
64   *     {@code maxChunkSize} at maximum.</td>
65   * </tr>
66   * </table>
67   *
68   * <h3>Parameters that control parsing behavior</h3>
69   * <table border="1">
70   * <tr>
71   * <th>Name</th><th>Default value</th><th>Meaning</th>
72   * </tr>
73   * <tr>
74   * <td>{@code allowDuplicateContentLengths}</td>
75   * <td>{@value #DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS}</td>
76   * <td>When set to {@code false}, will reject any messages that contain multiple Content-Length header fields.
77   *     When set to {@code true}, will allow multiple Content-Length headers only if they are all the same decimal value.
78   *     The duplicated field-values will be replaced with a single valid Content-Length field.
79   *     See <a href="https://tools.ietf.org/html/rfc7230#section-3.3.2">RFC 7230, Section 3.3.2</a>.</td>
80   * </tr>
81   * <tr>
82   * <td>{@code allowPartialChunks}</td>
83   * <td>{@value #DEFAULT_ALLOW_PARTIAL_CHUNKS}</td>
84   * <td>If the length of a chunk exceeds the {@link ByteBuf}s readable bytes and {@code allowPartialChunks}
85   *     is set to {@code true}, the chunk will be split into multiple {@link HttpContent}s.
86   *     Otherwise, if the chunk size does not exceed {@code maxChunkSize} and {@code allowPartialChunks}
87   *     is set to {@code false}, the {@link ByteBuf} is not decoded into an {@link HttpContent} until
88   *     the readable bytes are greater or equal to the chunk size.</td>
89   * </tr>
90   * </table>
91   *
92   * <h3>Chunked Content</h3>
93   *
94   * If the content of an HTTP message is greater than {@code maxChunkSize} or
95   * the transfer encoding of the HTTP message is 'chunked', this decoder
96   * generates one {@link HttpMessage} instance and its following
97   * {@link HttpContent}s per single HTTP message to avoid excessive memory
98   * consumption. For example, the following HTTP message:
99   * <pre>
100  * GET / HTTP/1.1
101  * Transfer-Encoding: chunked
102  *
103  * 1a
104  * abcdefghijklmnopqrstuvwxyz
105  * 10
106  * 1234567890abcdef
107  * 0
108  * Content-MD5: ...
109  * <i>[blank line]</i>
110  * </pre>
111  * triggers {@link HttpRequestDecoder} to generate 3 objects:
112  * <ol>
113  * <li>An {@link HttpRequest},</li>
114  * <li>The first {@link HttpContent} whose content is {@code 'abcdefghijklmnopqrstuvwxyz'},</li>
115  * <li>The second {@link LastHttpContent} whose content is {@code '1234567890abcdef'}, which marks
116  * the end of the content.</li>
117  * </ol>
118  *
119  * If you prefer not to handle {@link HttpContent}s by yourself for your
120  * convenience, insert {@link HttpObjectAggregator} after this decoder in the
121  * {@link ChannelPipeline}.  However, please note that your server might not
122  * be as memory efficient as without the aggregator.
123  *
124  * <h3>Extensibility</h3>
125  *
126  * Please note that this decoder is designed to be extended to implement
127  * a protocol derived from HTTP, such as
128  * <a href="https://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
129  * <a href="https://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol">ICAP</a>.
130  * To implement the decoder of such a derived protocol, extend this class and
131  * implement all abstract methods properly.
132  *
133  * <h3>Header Validation</h3>
134  *
135  * It is recommended to always enable header validation.
136  * <p>
137  * Without header validation, your system can become vulnerable to
138  * <a href="https://cwe.mitre.org/data/definitions/113.html">
139  *     CWE-113: Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Response Splitting')
140  * </a>.
141  * <p>
142  * This recommendation stands even when both peers in the HTTP exchange are trusted,
143  * as it helps with defence-in-depth.
144  */
145 public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
146     public static final int DEFAULT_MAX_INITIAL_LINE_LENGTH = 4096;
147     public static final int DEFAULT_MAX_HEADER_SIZE = 8192;
148     public static final boolean DEFAULT_CHUNKED_SUPPORTED = true;
149     public static final boolean DEFAULT_ALLOW_PARTIAL_CHUNKS = true;
150     public static final int DEFAULT_MAX_CHUNK_SIZE = 8192;
151     public static final boolean DEFAULT_VALIDATE_HEADERS = true;
152     public static final int DEFAULT_INITIAL_BUFFER_SIZE = 128;
153     public static final boolean DEFAULT_ALLOW_DUPLICATE_CONTENT_LENGTHS = false;
154     private final int maxChunkSize;
155     private final boolean chunkedSupported;
156     private final boolean allowPartialChunks;
157     /**
158      * This field is no longer used. It is only kept around for backwards compatibility purpose.
159      */
160     @Deprecated
161     protected final boolean validateHeaders;
162     protected final HttpHeadersFactory headersFactory;
163     protected final HttpHeadersFactory trailersFactory;
164     private final boolean allowDuplicateContentLengths;
165     private final ByteBuf parserScratchBuffer;
166     private final HeaderParser headerParser;
167     private final LineParser lineParser;
168 
169     private HttpMessage message;
170     private long chunkSize;
171     private long contentLength = Long.MIN_VALUE;
172     private boolean chunked;
173     private boolean isSwitchingToNonHttp1Protocol;
174 
175     private final AtomicBoolean resetRequested = new AtomicBoolean();
176 
177     // These will be updated by splitHeader(...)
178     private AsciiString name;
179     private String value;
180     private LastHttpContent trailer;
181 
182     @Override
183     protected void handlerRemoved0(ChannelHandlerContext ctx) throws Exception {
184         try {
185             parserScratchBuffer.release();
186         } finally {
187             super.handlerRemoved0(ctx);
188         }
189     }
190 
191     /**
192      * The internal state of {@link HttpObjectDecoder}.
193      * <em>Internal use only</em>.
194      */
195     private enum State {
196         SKIP_CONTROL_CHARS,
197         READ_INITIAL,
198         READ_HEADER,
199         READ_VARIABLE_LENGTH_CONTENT,
200         READ_FIXED_LENGTH_CONTENT,
201         READ_CHUNK_SIZE,
202         READ_CHUNKED_CONTENT,
203         READ_CHUNK_DELIMITER,
204         READ_CHUNK_FOOTER,
205         BAD_MESSAGE,
206         UPGRADED
207     }
208 
209     private State currentState = State.SKIP_CONTROL_CHARS;
210 
211     /**
212      * Creates a new instance with the default
213      * {@code maxInitialLineLength (4096)}, {@code maxHeaderSize (8192)}, and
214      * {@code maxChunkSize (8192)}.
215      */
216     protected HttpObjectDecoder() {
217         this(new HttpDecoderConfig());
218     }
219 
220     /**
221      * Creates a new instance with the specified parameters.
222      *
223      * @deprecated Use {@link #HttpObjectDecoder(HttpDecoderConfig)} instead.
224      */
225     @Deprecated
226     protected HttpObjectDecoder(
227             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean chunkedSupported) {
228         this(new HttpDecoderConfig()
229                 .setMaxInitialLineLength(maxInitialLineLength)
230                 .setMaxHeaderSize(maxHeaderSize)
231                 .setMaxChunkSize(maxChunkSize)
232                 .setChunkedSupported(chunkedSupported));
233     }
234 
235     /**
236      * Creates a new instance with the specified parameters.
237      *
238      * @deprecated Use {@link #HttpObjectDecoder(HttpDecoderConfig)} instead.
239      */
240     @Deprecated
241     protected HttpObjectDecoder(
242             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
243             boolean chunkedSupported, boolean validateHeaders) {
244         this(new HttpDecoderConfig()
245                 .setMaxInitialLineLength(maxInitialLineLength)
246                 .setMaxHeaderSize(maxHeaderSize)
247                 .setMaxChunkSize(maxChunkSize)
248                 .setChunkedSupported(chunkedSupported)
249                 .setValidateHeaders(validateHeaders));
250     }
251 
252     /**
253      * Creates a new instance with the specified parameters.
254      *
255      * @deprecated Use {@link #HttpObjectDecoder(HttpDecoderConfig)} instead.
256      */
257     @Deprecated
258     protected HttpObjectDecoder(
259             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
260             boolean chunkedSupported, boolean validateHeaders, int initialBufferSize) {
261         this(new HttpDecoderConfig()
262                 .setMaxInitialLineLength(maxInitialLineLength)
263                 .setMaxHeaderSize(maxHeaderSize)
264                 .setMaxChunkSize(maxChunkSize)
265                 .setChunkedSupported(chunkedSupported)
266                 .setValidateHeaders(validateHeaders)
267                 .setInitialBufferSize(initialBufferSize));
268     }
269 
270     /**
271      * Creates a new instance with the specified parameters.
272      *
273      * @deprecated Use {@link #HttpObjectDecoder(HttpDecoderConfig)} instead.
274      */
275     @Deprecated
276     protected HttpObjectDecoder(
277             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
278             boolean chunkedSupported, boolean validateHeaders, int initialBufferSize,
279             boolean allowDuplicateContentLengths) {
280         this(new HttpDecoderConfig()
281                 .setMaxInitialLineLength(maxInitialLineLength)
282                 .setMaxHeaderSize(maxHeaderSize)
283                 .setMaxChunkSize(maxChunkSize)
284                 .setChunkedSupported(chunkedSupported)
285                 .setValidateHeaders(validateHeaders)
286                 .setInitialBufferSize(initialBufferSize)
287                 .setAllowDuplicateContentLengths(allowDuplicateContentLengths));
288     }
289 
290     /**
291      * Creates a new instance with the specified parameters.
292      *
293      * @deprecated Use {@link #HttpObjectDecoder(HttpDecoderConfig)} instead.
294      */
295     @Deprecated
296     protected HttpObjectDecoder(
297             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
298             boolean chunkedSupported, boolean validateHeaders, int initialBufferSize,
299             boolean allowDuplicateContentLengths, boolean allowPartialChunks) {
300         this(new HttpDecoderConfig()
301                 .setMaxInitialLineLength(maxInitialLineLength)
302                 .setMaxHeaderSize(maxHeaderSize)
303                 .setMaxChunkSize(maxChunkSize)
304                 .setChunkedSupported(chunkedSupported)
305                 .setValidateHeaders(validateHeaders)
306                 .setInitialBufferSize(initialBufferSize)
307                 .setAllowDuplicateContentLengths(allowDuplicateContentLengths)
308                 .setAllowPartialChunks(allowPartialChunks));
309     }
310 
311     /**
312      * Creates a new instance with the specified configuration.
313      */
314     protected HttpObjectDecoder(HttpDecoderConfig config) {
315         checkNotNull(config, "config");
316 
317         parserScratchBuffer = Unpooled.buffer(config.getInitialBufferSize());
318         lineParser = new LineParser(parserScratchBuffer, config.getMaxInitialLineLength());
319         headerParser = new HeaderParser(parserScratchBuffer, config.getMaxHeaderSize());
320         maxChunkSize = config.getMaxChunkSize();
321         chunkedSupported = config.isChunkedSupported();
322         headersFactory = config.getHeadersFactory();
323         trailersFactory = config.getTrailersFactory();
324         validateHeaders = isValidating(headersFactory);
325         allowDuplicateContentLengths = config.isAllowDuplicateContentLengths();
326         allowPartialChunks = config.isAllowPartialChunks();
327     }
328 
329     protected boolean isValidating(HttpHeadersFactory headersFactory) {
330         if (headersFactory instanceof DefaultHttpHeadersFactory) {
331             DefaultHttpHeadersFactory builder = (DefaultHttpHeadersFactory) headersFactory;
332             return builder.isValidatingHeaderNames() || builder.isValidatingHeaderValues();
333         }
334         return true; // We can't actually tell in this case, but we assume some validation is taking place.
335     }
336 
337     @Override
338     protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
339         if (resetRequested.get()) {
340             resetNow();
341         }
342 
343         switch (currentState) {
344         case SKIP_CONTROL_CHARS:
345             // Fall-through
346         case READ_INITIAL: try {
347             ByteBuf line = lineParser.parse(buffer);
348             if (line == null) {
349                 return;
350             }
351             final String[] initialLine = splitInitialLine(line);
352             assert initialLine.length == 3 : "initialLine::length must be 3";
353 
354             message = createMessage(initialLine);
355             currentState = State.READ_HEADER;
356             // fall-through
357         } catch (Exception e) {
358             out.add(invalidMessage(message, buffer, e));
359             return;
360         }
361         case READ_HEADER: try {
362             State nextState = readHeaders(buffer);
363             if (nextState == null) {
364                 return;
365             }
366             currentState = nextState;
367             switch (nextState) {
368             case SKIP_CONTROL_CHARS:
369                 // fast-path
370                 // No content is expected.
371                 addCurrentMessage(out);
372                 out.add(LastHttpContent.EMPTY_LAST_CONTENT);
373                 resetNow();
374                 return;
375             case READ_CHUNK_SIZE:
376                 if (!chunkedSupported) {
377                     throw new IllegalArgumentException("Chunked messages not supported");
378                 }
379                 // Chunked encoding - generate HttpMessage first.  HttpChunks will follow.
380                 addCurrentMessage(out);
381                 return;
382             default:
383                 /*
384                  * RFC 7230, 3.3.3 (https://tools.ietf.org/html/rfc7230#section-3.3.3) states that if a
385                  * request does not have either a transfer-encoding or a content-length header then the message body
386                  * length is 0. However, for a response the body length is the number of octets received prior to the
387                  * server closing the connection. So we treat this as variable length chunked encoding.
388                  */
389                 if (contentLength == 0 || contentLength == -1 && isDecodingRequest()) {
390                     addCurrentMessage(out);
391                     out.add(LastHttpContent.EMPTY_LAST_CONTENT);
392                     resetNow();
393                     return;
394                 }
395 
396                 assert nextState == State.READ_FIXED_LENGTH_CONTENT ||
397                         nextState == State.READ_VARIABLE_LENGTH_CONTENT;
398 
399                 addCurrentMessage(out);
400 
401                 if (nextState == State.READ_FIXED_LENGTH_CONTENT) {
402                     // chunkSize will be decreased as the READ_FIXED_LENGTH_CONTENT state reads data chunk by chunk.
403                     chunkSize = contentLength;
404                 }
405 
406                 // We return here, this forces decode to be called again where we will decode the content
407                 return;
408             }
409         } catch (Exception e) {
410             out.add(invalidMessage(message, buffer, e));
411             return;
412         }
413         case READ_VARIABLE_LENGTH_CONTENT: {
414             // Keep reading data as a chunk until the end of connection is reached.
415             int toRead = Math.min(buffer.readableBytes(), maxChunkSize);
416             if (toRead > 0) {
417                 ByteBuf content = buffer.readRetainedSlice(toRead);
418                 out.add(new DefaultHttpContent(content));
419             }
420             return;
421         }
422         case READ_FIXED_LENGTH_CONTENT: {
423             int readLimit = buffer.readableBytes();
424 
425             // Check if the buffer is readable first as we use the readable byte count
426             // to create the HttpChunk. This is needed as otherwise we may end up with
427             // create an HttpChunk instance that contains an empty buffer and so is
428             // handled like it is the last HttpChunk.
429             //
430             // See https://github.com/netty/netty/issues/433
431             if (readLimit == 0) {
432                 return;
433             }
434 
435             int toRead = Math.min(readLimit, maxChunkSize);
436             if (toRead > chunkSize) {
437                 toRead = (int) chunkSize;
438             }
439             ByteBuf content = buffer.readRetainedSlice(toRead);
440             chunkSize -= toRead;
441 
442             if (chunkSize == 0) {
443                 // Read all content.
444                 out.add(new DefaultLastHttpContent(content, trailersFactory));
445                 resetNow();
446             } else {
447                 out.add(new DefaultHttpContent(content));
448             }
449             return;
450         }
451         /*
452          * everything else after this point takes care of reading chunked content. basically, read chunk size,
453          * read chunk, read and ignore the CRLF and repeat until 0
454          */
455         case READ_CHUNK_SIZE: try {
456             ByteBuf line = lineParser.parse(buffer);
457             if (line == null) {
458                 return;
459             }
460             int chunkSize = getChunkSize(line.array(), line.arrayOffset() + line.readerIndex(), line.readableBytes());
461             this.chunkSize = chunkSize;
462             if (chunkSize == 0) {
463                 currentState = State.READ_CHUNK_FOOTER;
464                 return;
465             }
466             currentState = State.READ_CHUNKED_CONTENT;
467             // fall-through
468         } catch (Exception e) {
469             out.add(invalidChunk(buffer, e));
470             return;
471         }
472         case READ_CHUNKED_CONTENT: {
473             assert chunkSize <= Integer.MAX_VALUE;
474             int toRead = Math.min((int) chunkSize, maxChunkSize);
475             if (!allowPartialChunks && buffer.readableBytes() < toRead) {
476                 return;
477             }
478             toRead = Math.min(toRead, buffer.readableBytes());
479             if (toRead == 0) {
480                 return;
481             }
482             HttpContent chunk = new DefaultHttpContent(buffer.readRetainedSlice(toRead));
483             chunkSize -= toRead;
484 
485             out.add(chunk);
486 
487             if (chunkSize != 0) {
488                 return;
489             }
490             currentState = State.READ_CHUNK_DELIMITER;
491             // fall-through
492         }
493         case READ_CHUNK_DELIMITER: {
494             final int wIdx = buffer.writerIndex();
495             int rIdx = buffer.readerIndex();
496             while (wIdx > rIdx) {
497                 byte next = buffer.getByte(rIdx++);
498                 if (next == HttpConstants.LF) {
499                     currentState = State.READ_CHUNK_SIZE;
500                     break;
501                 }
502             }
503             buffer.readerIndex(rIdx);
504             return;
505         }
506         case READ_CHUNK_FOOTER: try {
507             LastHttpContent trailer = readTrailingHeaders(buffer);
508             if (trailer == null) {
509                 return;
510             }
511             out.add(trailer);
512             resetNow();
513             return;
514         } catch (Exception e) {
515             out.add(invalidChunk(buffer, e));
516             return;
517         }
518         case BAD_MESSAGE: {
519             // Keep discarding until disconnection.
520             buffer.skipBytes(buffer.readableBytes());
521             break;
522         }
523         case UPGRADED: {
524             int readableBytes = buffer.readableBytes();
525             if (readableBytes > 0) {
526                 // Keep on consuming as otherwise we may trigger an DecoderException,
527                 // other handler will replace this codec with the upgraded protocol codec to
528                 // take the traffic over at some point then.
529                 // See https://github.com/netty/netty/issues/2173
530                 out.add(buffer.readBytes(readableBytes));
531             }
532             break;
533         }
534         default:
535             break;
536         }
537     }
538 
539     @Override
540     protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
541         super.decodeLast(ctx, in, out);
542 
543         if (resetRequested.get()) {
544             // If a reset was requested by decodeLast() we need to do it now otherwise we may produce a
545             // LastHttpContent while there was already one.
546             resetNow();
547         }
548 
549         // Handle the last unfinished message.
550         switch (currentState) {
551             case READ_VARIABLE_LENGTH_CONTENT:
552                 if (!chunked && !in.isReadable()) {
553                     // End of connection.
554                     out.add(LastHttpContent.EMPTY_LAST_CONTENT);
555                     resetNow();
556                 }
557                 return;
558             case READ_HEADER:
559                 // If we are still in the state of reading headers we need to create a new invalid message that
560                 // signals that the connection was closed before we received the headers.
561                 out.add(invalidMessage(message, Unpooled.EMPTY_BUFFER,
562                         new PrematureChannelClosureException("Connection closed before received headers")));
563                 resetNow();
564                 return;
565             case READ_CHUNK_DELIMITER: // fall-trough
566             case READ_CHUNK_FOOTER: // fall-trough
567             case READ_CHUNKED_CONTENT: // fall-trough
568             case READ_CHUNK_SIZE: // fall-trough
569             case READ_FIXED_LENGTH_CONTENT:
570                 // Check if the closure of the connection signifies the end of the content.
571                 boolean prematureClosure;
572                 if (isDecodingRequest() || chunked) {
573                     // The last request did not wait for a response.
574                     prematureClosure = true;
575                 } else {
576                     // Compare the length of the received content and the 'Content-Length' header.
577                     // If the 'Content-Length' header is absent, the length of the content is determined by the end of
578                     // the connection, so it is perfectly fine.
579                     prematureClosure = contentLength > 0;
580                 }
581                 if (!prematureClosure) {
582                     out.add(LastHttpContent.EMPTY_LAST_CONTENT);
583                 }
584                 resetNow();
585                 return;
586             case SKIP_CONTROL_CHARS: // fall-trough
587             case READ_INITIAL:// fall-trough
588             case BAD_MESSAGE: // fall-trough
589             case UPGRADED: // fall-trough
590                 // Do nothing
591                 break;
592             default:
593                 throw new IllegalStateException("Unhandled state " + currentState);
594         }
595     }
596 
597     @Override
598     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
599         if (evt instanceof HttpExpectationFailedEvent) {
600             switch (currentState) {
601             case READ_FIXED_LENGTH_CONTENT:
602             case READ_VARIABLE_LENGTH_CONTENT:
603             case READ_CHUNK_SIZE:
604                 reset();
605                 break;
606             default:
607                 break;
608             }
609         }
610         super.userEventTriggered(ctx, evt);
611     }
612 
613     private void addCurrentMessage(List<Object> out) {
614         HttpMessage message = this.message;
615         assert message != null;
616         this.message = null;
617         out.add(message);
618     }
619 
620     protected boolean isContentAlwaysEmpty(HttpMessage msg) {
621         if (msg instanceof HttpResponse) {
622             HttpResponse res = (HttpResponse) msg;
623             final HttpResponseStatus status = res.status();
624             final int code = status.code();
625             final HttpStatusClass statusClass = status.codeClass();
626 
627             // Correctly handle return codes of 1xx.
628             //
629             // See:
630             //     - https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html Section 4.4
631             //     - https://github.com/netty/netty/issues/222
632             if (statusClass == HttpStatusClass.INFORMATIONAL) {
633                 // One exception: Hixie 76 websocket handshake response
634                 return !(code == 101 && !res.headers().contains(HttpHeaderNames.SEC_WEBSOCKET_ACCEPT)
635                          && res.headers().contains(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET, true));
636             }
637 
638             switch (code) {
639             case 204: case 304:
640                 return true;
641             default:
642                 return false;
643             }
644         }
645         return false;
646     }
647 
648     /**
649      * Returns true if the server switched to a different protocol than HTTP/1.0 or HTTP/1.1, e.g. HTTP/2 or Websocket.
650      * Returns false if the upgrade happened in a different layer, e.g. upgrade from HTTP/1.1 to HTTP/1.1 over TLS.
651      */
652     protected boolean isSwitchingToNonHttp1Protocol(HttpResponse msg) {
653         if (msg.status().code() != HttpResponseStatus.SWITCHING_PROTOCOLS.code()) {
654             return false;
655         }
656         String newProtocol = msg.headers().get(HttpHeaderNames.UPGRADE);
657         return newProtocol == null ||
658                 !newProtocol.contains(HttpVersion.HTTP_1_0.text()) &&
659                 !newProtocol.contains(HttpVersion.HTTP_1_1.text());
660     }
661 
662     /**
663      * Resets the state of the decoder so that it is ready to decode a new message.
664      * This method is useful for handling a rejected request with {@code Expect: 100-continue} header.
665      */
666     public void reset() {
667         resetRequested.lazySet(true);
668     }
669 
670     private void resetNow() {
671         message = null;
672         name = null;
673         value = null;
674         contentLength = Long.MIN_VALUE;
675         chunked = false;
676         lineParser.reset();
677         headerParser.reset();
678         trailer = null;
679         if (isSwitchingToNonHttp1Protocol) {
680             isSwitchingToNonHttp1Protocol = false;
681             currentState = State.UPGRADED;
682             return;
683         }
684 
685         resetRequested.lazySet(false);
686         currentState = State.SKIP_CONTROL_CHARS;
687     }
688 
689     private HttpMessage invalidMessage(HttpMessage current, ByteBuf in, Exception cause) {
690         currentState = State.BAD_MESSAGE;
691         message = null;
692         trailer = null;
693 
694         // Advance the readerIndex so that ByteToMessageDecoder does not complain
695         // when we produced an invalid message without consuming anything.
696         in.skipBytes(in.readableBytes());
697 
698         if (current == null) {
699             current = createInvalidMessage();
700         }
701         current.setDecoderResult(DecoderResult.failure(cause));
702 
703         return current;
704     }
705 
706     private HttpContent invalidChunk(ByteBuf in, Exception cause) {
707         currentState = State.BAD_MESSAGE;
708         message = null;
709         trailer = null;
710 
711         // Advance the readerIndex so that ByteToMessageDecoder does not complain
712         // when we produced an invalid message without consuming anything.
713         in.skipBytes(in.readableBytes());
714 
715         HttpContent chunk = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER);
716         chunk.setDecoderResult(DecoderResult.failure(cause));
717         return chunk;
718     }
719 
720     private State readHeaders(ByteBuf buffer) {
721         final HttpMessage message = this.message;
722         final HttpHeaders headers = message.headers();
723 
724         final HeaderParser headerParser = this.headerParser;
725 
726         ByteBuf line = headerParser.parse(buffer);
727         if (line == null) {
728             return null;
729         }
730         int lineLength = line.readableBytes();
731         while (lineLength > 0) {
732             final byte[] lineContent = line.array();
733             final int startLine = line.arrayOffset() + line.readerIndex();
734             final byte firstChar = lineContent[startLine];
735             if (name != null && (firstChar == ' ' || firstChar == '\t')) {
736                 //please do not make one line from below code
737                 //as it breaks +XX:OptimizeStringConcat optimization
738                 String trimmedLine = langAsciiString(lineContent, startLine, lineLength).trim();
739                 String valueStr = value;
740                 value = valueStr + ' ' + trimmedLine;
741             } else {
742                 if (name != null) {
743                     headers.add(name, value);
744                 }
745                 splitHeader(lineContent, startLine, lineLength);
746             }
747 
748             line = headerParser.parse(buffer);
749             if (line == null) {
750                 return null;
751             }
752             lineLength = line.readableBytes();
753         }
754 
755         // Add the last header.
756         if (name != null) {
757             headers.add(name, value);
758         }
759 
760         // reset name and value fields
761         name = null;
762         value = null;
763 
764         // Done parsing initial line and headers. Set decoder result.
765         HttpMessageDecoderResult decoderResult = new HttpMessageDecoderResult(lineParser.size, headerParser.size);
766         message.setDecoderResult(decoderResult);
767 
768         List<String> contentLengthFields = headers.getAll(HttpHeaderNames.CONTENT_LENGTH);
769         if (!contentLengthFields.isEmpty()) {
770             HttpVersion version = message.protocolVersion();
771             boolean isHttp10OrEarlier = version.majorVersion() < 1 || (version.majorVersion() == 1
772                     && version.minorVersion() == 0);
773             // Guard against multiple Content-Length headers as stated in
774             // https://tools.ietf.org/html/rfc7230#section-3.3.2:
775             contentLength = HttpUtil.normalizeAndGetContentLength(contentLengthFields,
776                     isHttp10OrEarlier, allowDuplicateContentLengths);
777             if (contentLength != -1) {
778                 String lengthValue = contentLengthFields.get(0).trim();
779                 if (contentLengthFields.size() > 1 || // don't unnecessarily re-order headers
780                         !lengthValue.equals(Long.toString(contentLength))) {
781                     headers.set(HttpHeaderNames.CONTENT_LENGTH, contentLength);
782                 }
783             }
784         } else {
785             // We know the content length if it's a Web Socket message even if
786             // Content-Length header is missing.
787             contentLength = HttpUtil.getWebSocketContentLength(message);
788         }
789         if (!isDecodingRequest() && message instanceof HttpResponse) {
790             HttpResponse res = (HttpResponse) message;
791             this.isSwitchingToNonHttp1Protocol = isSwitchingToNonHttp1Protocol(res);
792         }
793         if (isContentAlwaysEmpty(message)) {
794             HttpUtil.setTransferEncodingChunked(message, false);
795             return State.SKIP_CONTROL_CHARS;
796         }
797         if (HttpUtil.isTransferEncodingChunked(message)) {
798             this.chunked = true;
799             if (!contentLengthFields.isEmpty() && message.protocolVersion() == HttpVersion.HTTP_1_1) {
800                 handleTransferEncodingChunkedWithContentLength(message);
801             }
802             return State.READ_CHUNK_SIZE;
803         }
804         if (contentLength >= 0) {
805             return State.READ_FIXED_LENGTH_CONTENT;
806         }
807         return State.READ_VARIABLE_LENGTH_CONTENT;
808     }
809 
810     /**
811      * Invoked when a message with both a "Transfer-Encoding: chunked" and a "Content-Length" header field is detected.
812      * The default behavior is to <i>remove</i> the Content-Length field, but this method could be overridden
813      * to change the behavior (to, e.g., throw an exception and produce an invalid message).
814      * <p>
815      * See: https://tools.ietf.org/html/rfc7230#section-3.3.3
816      * <pre>
817      *     If a message is received with both a Transfer-Encoding and a
818      *     Content-Length header field, the Transfer-Encoding overrides the
819      *     Content-Length.  Such a message might indicate an attempt to
820      *     perform request smuggling (Section 9.5) or response splitting
821      *     (Section 9.4) and ought to be handled as an error.  A sender MUST
822      *     remove the received Content-Length field prior to forwarding such
823      *     a message downstream.
824      * </pre>
825      * Also see:
826      * https://github.com/apache/tomcat/blob/b693d7c1981fa7f51e58bc8c8e72e3fe80b7b773/
827      * java/org/apache/coyote/http11/Http11Processor.java#L747-L755
828      * https://github.com/nginx/nginx/blob/0ad4393e30c119d250415cb769e3d8bc8dce5186/
829      * src/http/ngx_http_request.c#L1946-L1953
830      */
831     protected void handleTransferEncodingChunkedWithContentLength(HttpMessage message) {
832         message.headers().remove(HttpHeaderNames.CONTENT_LENGTH);
833         contentLength = Long.MIN_VALUE;
834     }
835 
836     private LastHttpContent readTrailingHeaders(ByteBuf buffer) {
837         final HeaderParser headerParser = this.headerParser;
838         ByteBuf line = headerParser.parse(buffer);
839         if (line == null) {
840             return null;
841         }
842         LastHttpContent trailer = this.trailer;
843         int lineLength = line.readableBytes();
844         if (lineLength == 0 && trailer == null) {
845             // We have received the empty line which signals the trailer is complete and did not parse any trailers
846             // before. Just return an empty last content to reduce allocations.
847             return LastHttpContent.EMPTY_LAST_CONTENT;
848         }
849 
850         CharSequence lastHeader = null;
851         if (trailer == null) {
852             trailer = this.trailer = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, trailersFactory);
853         }
854         while (lineLength > 0) {
855             final byte[] lineContent = line.array();
856             final int startLine = line.arrayOffset() + line.readerIndex();
857             final byte firstChar = lineContent[startLine];
858             if (lastHeader != null && (firstChar == ' ' || firstChar == '\t')) {
859                 List<String> current = trailer.trailingHeaders().getAll(lastHeader);
860                 if (!current.isEmpty()) {
861                     int lastPos = current.size() - 1;
862                     //please do not make one line from below code
863                     //as it breaks +XX:OptimizeStringConcat optimization
864                     String lineTrimmed = langAsciiString(lineContent, startLine, line.readableBytes()).trim();
865                     String currentLastPos = current.get(lastPos);
866                     current.set(lastPos, currentLastPos + lineTrimmed);
867                 }
868             } else {
869                 splitHeader(lineContent, startLine, lineLength);
870                 AsciiString headerName = name;
871                 if (!HttpHeaderNames.CONTENT_LENGTH.contentEqualsIgnoreCase(headerName) &&
872                         !HttpHeaderNames.TRANSFER_ENCODING.contentEqualsIgnoreCase(headerName) &&
873                         !HttpHeaderNames.TRAILER.contentEqualsIgnoreCase(headerName)) {
874                     trailer.trailingHeaders().add(headerName, value);
875                 }
876                 lastHeader = name;
877                 // reset name and value fields
878                 name = null;
879                 value = null;
880             }
881             line = headerParser.parse(buffer);
882             if (line == null) {
883                 return null;
884             }
885             lineLength = line.readableBytes();
886         }
887 
888         this.trailer = null;
889         return trailer;
890     }
891 
892     protected abstract boolean isDecodingRequest();
893     protected abstract HttpMessage createMessage(String[] initialLine) throws Exception;
894     protected abstract HttpMessage createInvalidMessage();
895 
896     /**
897      * It skips any whitespace char and return the number of skipped bytes.
898      */
899     private static int skipWhiteSpaces(byte[] hex, int start, int length) {
900         for (int i = 0; i < length; i++) {
901             if (!isWhitespace(hex[start + i])) {
902                 return i;
903             }
904         }
905         return length;
906     }
907 
908     private static int getChunkSize(byte[] hex, int start, int length) {
909         // trim the leading bytes if white spaces, if any
910         final int skipped = skipWhiteSpaces(hex, start, length);
911         if (skipped == length) {
912             // empty case
913             throw new NumberFormatException();
914         }
915         start += skipped;
916         length -= skipped;
917         int result = 0;
918         for (int i = 0; i < length; i++) {
919             final int digit = StringUtil.decodeHexNibble(hex[start + i]);
920             if (digit == -1) {
921                 // uncommon path
922                 final byte b = hex[start + i];
923                 if (b == ';' || isControlOrWhitespaceAsciiChar(b)) {
924                     if (i == 0) {
925                         // empty case
926                         throw new NumberFormatException("Empty chunk size");
927                     }
928                     return result;
929                 }
930                 // non-hex char fail-fast path
931                 throw new NumberFormatException("Invalid character in chunk size");
932             }
933             result *= 16;
934             result += digit;
935             if (result < 0) {
936                 throw new NumberFormatException("Chunk size overflow: " + result);
937             }
938         }
939         return result;
940     }
941 
942     private String[] splitInitialLine(ByteBuf asciiBuffer) {
943         final byte[] asciiBytes = asciiBuffer.array();
944 
945         final int arrayOffset = asciiBuffer.arrayOffset();
946 
947         final int startContent = arrayOffset + asciiBuffer.readerIndex();
948 
949         final int end = startContent + asciiBuffer.readableBytes();
950 
951         byte lastByte = asciiBytes[end - 1];
952         if (isControlOrWhitespaceAsciiChar(lastByte)) {
953             if (isDecodingRequest() || !isOWS(lastByte)) {
954                 // There should no extra control or whitespace char in case of a request.
955                 // In case of a response there might be a SP if there is no reason-phrase given.
956                 // See
957                 //  - https://datatracker.ietf.org/doc/html/rfc2616#section-5.1
958                 //  - https://datatracker.ietf.org/doc/html/rfc9112#name-status-line
959                 throw new IllegalArgumentException(
960                         "Illegal character in request line: 0x" + Integer.toHexString(lastByte));
961             }
962         }
963 
964         final int aStart = findNonSPLenient(asciiBytes, startContent, end);
965         final int aEnd = findSPLenient(asciiBytes, aStart, end);
966 
967         final int bStart = findNonSPLenient(asciiBytes, aEnd, end);
968         final int bEnd = findSPLenient(asciiBytes, bStart, end);
969 
970         final int cStart = findNonSPLenient(asciiBytes, bEnd, end);
971         final int cEnd = findEndOfString(asciiBytes, Math.max(cStart - 1, startContent), end);
972 
973         return new String[]{
974                 splitFirstWordInitialLine(asciiBytes, aStart, aEnd - aStart),
975                 splitSecondWordInitialLine(asciiBytes, bStart, bEnd - bStart),
976                 cStart < cEnd ? splitThirdWordInitialLine(asciiBytes, cStart, cEnd - cStart) : StringUtil.EMPTY_STRING};
977     }
978 
979     protected String splitFirstWordInitialLine(final byte[] asciiContent, int start, int length) {
980         return langAsciiString(asciiContent, start, length);
981     }
982 
983     protected String splitSecondWordInitialLine(final byte[] asciiContent, int start, int length) {
984         return langAsciiString(asciiContent, start, length);
985     }
986 
987     protected String splitThirdWordInitialLine(final byte[] asciiContent, int start, int length) {
988         return langAsciiString(asciiContent, start, length);
989     }
990 
991     /**
992      * This method shouldn't exist: look at https://bugs.openjdk.org/browse/JDK-8295496 for more context
993      */
994     private static String langAsciiString(final byte[] asciiContent, int start, int length) {
995         if (length == 0) {
996             return StringUtil.EMPTY_STRING;
997         }
998         // DON'T REMOVE: it helps JIT to use a simpler intrinsic stub for System::arrayCopy based on the call-site
999         if (start == 0) {
1000             if (length == asciiContent.length) {
1001                 return new String(asciiContent, 0, 0, asciiContent.length);
1002             }
1003             return new String(asciiContent, 0, 0, length);
1004         }
1005         return new String(asciiContent, 0, start, length);
1006     }
1007 
1008     private void splitHeader(byte[] line, int start, int length) {
1009         final int end = start + length;
1010         int nameEnd;
1011         final int nameStart = start;
1012         // hoist this load out of the loop, because it won't change!
1013         final boolean isDecodingRequest = isDecodingRequest();
1014         for (nameEnd = nameStart; nameEnd < end; nameEnd ++) {
1015             byte ch = line[nameEnd];
1016             // https://tools.ietf.org/html/rfc7230#section-3.2.4
1017             //
1018             // No whitespace is allowed between the header field-name and colon. In
1019             // the past, differences in the handling of such whitespace have led to
1020             // security vulnerabilities in request routing and response handling. A
1021             // server MUST reject any received request message that contains
1022             // whitespace between a header field-name and colon with a response code
1023             // of 400 (Bad Request). A proxy MUST remove any such whitespace from a
1024             // response message before forwarding the message downstream.
1025             if (ch == ':' ||
1026                     // In case of decoding a request we will just continue processing and header validation
1027                     // is done in the DefaultHttpHeaders implementation.
1028                     //
1029                     // In the case of decoding a response we will "skip" the whitespace.
1030                     (!isDecodingRequest && isOWS(ch))) {
1031                 break;
1032             }
1033         }
1034 
1035         if (nameEnd == end) {
1036             // There was no colon present at all.
1037             throw new IllegalArgumentException("No colon found");
1038         }
1039         int colonEnd;
1040         for (colonEnd = nameEnd; colonEnd < end; colonEnd ++) {
1041             if (line[colonEnd] == ':') {
1042                 colonEnd ++;
1043                 break;
1044             }
1045         }
1046         name = splitHeaderName(line, nameStart, nameEnd - nameStart);
1047         final int valueStart = findNonWhitespace(line, colonEnd, end);
1048         if (valueStart == end) {
1049             value = StringUtil.EMPTY_STRING;
1050         } else {
1051             final int valueEnd = findEndOfString(line, start, end);
1052             // no need to make uses of the ByteBuf's toString ASCII method here, and risk to get JIT confused
1053             value = langAsciiString(line, valueStart, valueEnd - valueStart);
1054         }
1055     }
1056 
1057     protected AsciiString splitHeaderName(byte[] sb, int start, int length) {
1058         return new AsciiString(sb, start, length, true);
1059     }
1060 
1061     private static int findNonSPLenient(byte[] sb, int offset, int end) {
1062         for (int result = offset; result < end; ++result) {
1063             byte c = sb[result];
1064             // See https://tools.ietf.org/html/rfc7230#section-3.5
1065             if (isSPLenient(c)) {
1066                 continue;
1067             }
1068             if (isWhitespace(c)) {
1069                 // Any other whitespace delimiter is invalid
1070                 throw new IllegalArgumentException("Invalid separator");
1071             }
1072             return result;
1073         }
1074         return end;
1075     }
1076 
1077     private static int findSPLenient(byte[] sb, int offset, int end) {
1078         for (int result = offset; result < end; ++result) {
1079             if (isSPLenient(sb[result])) {
1080                 return result;
1081             }
1082         }
1083         return end;
1084     }
1085 
1086     private static final boolean[] SP_LENIENT_BYTES;
1087     private static final boolean[] LATIN_WHITESPACE;
1088 
1089     static {
1090         // See https://tools.ietf.org/html/rfc7230#section-3.5
1091         SP_LENIENT_BYTES = new boolean[256];
1092         SP_LENIENT_BYTES[128 + ' '] = true;
1093         SP_LENIENT_BYTES[128 + 0x09] = true;
1094         SP_LENIENT_BYTES[128 + 0x0B] = true;
1095         SP_LENIENT_BYTES[128 + 0x0C] = true;
1096         SP_LENIENT_BYTES[128 + 0x0D] = true;
1097         // TO SAVE PERFORMING Character::isWhitespace ceremony
1098         LATIN_WHITESPACE = new boolean[256];
1099         for (byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++) {
1100             LATIN_WHITESPACE[128 + b] = Character.isWhitespace(b);
1101         }
1102     }
1103 
1104     private static boolean isSPLenient(byte c) {
1105         // See https://tools.ietf.org/html/rfc7230#section-3.5
1106         return SP_LENIENT_BYTES[c + 128];
1107     }
1108 
1109     private static boolean isWhitespace(byte b) {
1110         return LATIN_WHITESPACE[b + 128];
1111     }
1112 
1113     private static int findNonWhitespace(byte[] sb, int offset, int end) {
1114         for (int result = offset; result < end; ++result) {
1115             byte c = sb[result];
1116             if (!isWhitespace(c)) {
1117                 return result;
1118             } else if (!isOWS(c)) {
1119                 // Only OWS is supported for whitespace
1120                 throw new IllegalArgumentException("Invalid separator, only a single space or horizontal tab allowed," +
1121                         " but received a '" + c + "' (0x" + Integer.toHexString(c) + ")");
1122             }
1123         }
1124         return end;
1125     }
1126 
1127     private static int findEndOfString(byte[] sb, int start, int end) {
1128         for (int result = end - 1; result > start; --result) {
1129             if (!isOWS(sb[result])) {
1130                 return result + 1;
1131             }
1132         }
1133         return 0;
1134     }
1135 
1136     private static boolean isOWS(byte ch) {
1137         return ch == ' ' || ch == 0x09;
1138     }
1139 
1140     private static class HeaderParser {
1141         protected final ByteBuf seq;
1142         protected final int maxLength;
1143         int size;
1144 
1145         HeaderParser(ByteBuf seq, int maxLength) {
1146             this.seq = seq;
1147             this.maxLength = maxLength;
1148         }
1149 
1150         public ByteBuf parse(ByteBuf buffer) {
1151             final int readableBytes = buffer.readableBytes();
1152             final int readerIndex = buffer.readerIndex();
1153             final int maxBodySize = maxLength - size;
1154             assert maxBodySize >= 0;
1155             // adding 2 to account for both CR (if present) and LF
1156             // don't remove 2L: it's key to cover maxLength = Integer.MAX_VALUE
1157             final long maxBodySizeWithCRLF = maxBodySize + 2L;
1158             final int toProcess = (int) Math.min(maxBodySizeWithCRLF, readableBytes);
1159             final int toIndexExclusive = readerIndex + toProcess;
1160             assert toIndexExclusive >= readerIndex;
1161             final int indexOfLf = buffer.indexOf(readerIndex, toIndexExclusive, HttpConstants.LF);
1162             if (indexOfLf == -1) {
1163                 if (readableBytes > maxBodySize) {
1164                     // TODO: Respond with Bad Request and discard the traffic
1165                     //    or close the connection.
1166                     //       No need to notify the upstream handlers - just log.
1167                     //       If decoding a response, just throw an exception.
1168                     throw newException(maxLength);
1169                 }
1170                 return null;
1171             }
1172             final int endOfSeqIncluded;
1173             if (indexOfLf > readerIndex && buffer.getByte(indexOfLf - 1) == HttpConstants.CR) {
1174                 // Drop CR if we had a CRLF pair
1175                 endOfSeqIncluded = indexOfLf - 1;
1176             } else {
1177                 endOfSeqIncluded = indexOfLf;
1178             }
1179             final int newSize = endOfSeqIncluded - readerIndex;
1180             if (newSize == 0) {
1181                 seq.clear();
1182                 buffer.readerIndex(indexOfLf + 1);
1183                 return seq;
1184             }
1185             int size = this.size + newSize;
1186             if (size > maxLength) {
1187                 throw newException(maxLength);
1188             }
1189             this.size = size;
1190             seq.clear();
1191             seq.writeBytes(buffer, readerIndex, newSize);
1192             buffer.readerIndex(indexOfLf + 1);
1193             return seq;
1194         }
1195 
1196         public void reset() {
1197             size = 0;
1198         }
1199 
1200         protected TooLongFrameException newException(int maxLength) {
1201             return new TooLongHttpHeaderException("HTTP header is larger than " + maxLength + " bytes.");
1202         }
1203     }
1204 
1205     private final class LineParser extends HeaderParser {
1206 
1207         LineParser(ByteBuf seq, int maxLength) {
1208             super(seq, maxLength);
1209         }
1210 
1211         @Override
1212         public ByteBuf parse(ByteBuf buffer) {
1213             // Suppress a warning because HeaderParser.reset() is supposed to be called
1214             reset();
1215             final int readableBytes = buffer.readableBytes();
1216             if (readableBytes == 0) {
1217                 return null;
1218             }
1219             final int readerIndex = buffer.readerIndex();
1220             if (currentState == State.SKIP_CONTROL_CHARS && skipControlChars(buffer, readableBytes, readerIndex)) {
1221                 return null;
1222             }
1223             return super.parse(buffer);
1224         }
1225 
1226         private boolean skipControlChars(ByteBuf buffer, int readableBytes, int readerIndex) {
1227             assert currentState == State.SKIP_CONTROL_CHARS;
1228             final int maxToSkip = Math.min(maxLength, readableBytes);
1229             final int firstNonControlIndex = buffer.forEachByte(readerIndex, maxToSkip, SKIP_CONTROL_CHARS_BYTES);
1230             if (firstNonControlIndex == -1) {
1231                 buffer.skipBytes(maxToSkip);
1232                 if (readableBytes > maxLength) {
1233                     throw newException(maxLength);
1234                 }
1235                 return true;
1236             }
1237             // from now on we don't care about control chars
1238             buffer.readerIndex(firstNonControlIndex);
1239             currentState = State.READ_INITIAL;
1240             return false;
1241         }
1242 
1243         @Override
1244         protected TooLongFrameException newException(int maxLength) {
1245             return new TooLongHttpLineException("An HTTP line is larger than " + maxLength + " bytes.");
1246         }
1247     }
1248 
1249     private static final boolean[] ISO_CONTROL_OR_WHITESPACE;
1250 
1251     static {
1252         ISO_CONTROL_OR_WHITESPACE = new boolean[256];
1253         for (byte b = Byte.MIN_VALUE; b < Byte.MAX_VALUE; b++) {
1254             ISO_CONTROL_OR_WHITESPACE[128 + b] = Character.isISOControl(b) || isWhitespace(b);
1255         }
1256     }
1257 
1258     private static final ByteProcessor SKIP_CONTROL_CHARS_BYTES = new ByteProcessor() {
1259 
1260         @Override
1261         public boolean process(byte value) {
1262             return ISO_CONTROL_OR_WHITESPACE[128 + value];
1263         }
1264     };
1265 
1266     private static boolean isControlOrWhitespaceAsciiChar(byte b) {
1267         return ISO_CONTROL_OR_WHITESPACE[128 + b];
1268     }
1269 }