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 io.netty.handler.codec.http;
17  
18  import static io.netty.util.internal.ObjectUtil.checkPositive;
19  
20  import io.netty.buffer.ByteBuf;
21  import io.netty.buffer.Unpooled;
22  import io.netty.channel.ChannelHandlerContext;
23  import io.netty.channel.ChannelPipeline;
24  import io.netty.handler.codec.ByteToMessageDecoder;
25  import io.netty.handler.codec.DecoderResult;
26  import io.netty.handler.codec.PrematureChannelClosureException;
27  import io.netty.handler.codec.TooLongFrameException;
28  import io.netty.util.ByteProcessor;
29  import io.netty.util.internal.AppendableCharSequence;
30  
31  import java.util.List;
32  
33  /**
34   * Decodes {@link ByteBuf}s into {@link HttpMessage}s and
35   * {@link HttpContent}s.
36   *
37   * <h3>Parameters that prevents excessive memory consumption</h3>
38   * <table border="1">
39   * <tr>
40   * <th>Name</th><th>Meaning</th>
41   * </tr>
42   * <tr>
43   * <td>{@code maxInitialLineLength}</td>
44   * <td>The maximum length of the initial line
45   *     (e.g. {@code "GET / HTTP/1.0"} or {@code "HTTP/1.0 200 OK"})
46   *     If the length of the initial line exceeds this value, a
47   *     {@link TooLongFrameException} will be raised.</td>
48   * </tr>
49   * <tr>
50   * <td>{@code maxHeaderSize}</td>
51   * <td>The maximum length of all headers.  If the sum of the length of each
52   *     header exceeds this value, a {@link TooLongFrameException} will be raised.</td>
53   * </tr>
54   * <tr>
55   * <td>{@code maxChunkSize}</td>
56   * <td>The maximum length of the content or each chunk.  If the content length
57   *     (or the length of each chunk) exceeds this value, the content or chunk
58   *     will be split into multiple {@link HttpContent}s whose length is
59   *     {@code maxChunkSize} at maximum.</td>
60   * </tr>
61   * </table>
62   *
63   * <h3>Chunked Content</h3>
64   *
65   * If the content of an HTTP message is greater than {@code maxChunkSize} or
66   * the transfer encoding of the HTTP message is 'chunked', this decoder
67   * generates one {@link HttpMessage} instance and its following
68   * {@link HttpContent}s per single HTTP message to avoid excessive memory
69   * consumption. For example, the following HTTP message:
70   * <pre>
71   * GET / HTTP/1.1
72   * Transfer-Encoding: chunked
73   *
74   * 1a
75   * abcdefghijklmnopqrstuvwxyz
76   * 10
77   * 1234567890abcdef
78   * 0
79   * Content-MD5: ...
80   * <i>[blank line]</i>
81   * </pre>
82   * triggers {@link HttpRequestDecoder} to generate 3 objects:
83   * <ol>
84   * <li>An {@link HttpRequest},</li>
85   * <li>The first {@link HttpContent} whose content is {@code 'abcdefghijklmnopqrstuvwxyz'},</li>
86   * <li>The second {@link LastHttpContent} whose content is {@code '1234567890abcdef'}, which marks
87   * the end of the content.</li>
88   * </ol>
89   *
90   * If you prefer not to handle {@link HttpContent}s by yourself for your
91   * convenience, insert {@link HttpObjectAggregator} after this decoder in the
92   * {@link ChannelPipeline}.  However, please note that your server might not
93   * be as memory efficient as without the aggregator.
94   *
95   * <h3>Extensibility</h3>
96   *
97   * Please note that this decoder is designed to be extended to implement
98   * a protocol derived from HTTP, such as
99   * <a href="http://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
100  * <a href="http://en.wikipedia.org/wiki/Internet_Content_Adaptation_Protocol">ICAP</a>.
101  * To implement the decoder of such a derived protocol, extend this class and
102  * implement all abstract methods properly.
103  */
104 public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
105     private static final String EMPTY_VALUE = "";
106 
107     private final int maxChunkSize;
108     private final boolean chunkedSupported;
109     protected final boolean validateHeaders;
110     private final HeaderParser headerParser;
111     private final LineParser lineParser;
112 
113     private HttpMessage message;
114     private long chunkSize;
115     private long contentLength = Long.MIN_VALUE;
116     private volatile boolean resetRequested;
117 
118     // These will be updated by splitHeader(...)
119     private CharSequence name;
120     private CharSequence value;
121 
122     private LastHttpContent trailer;
123 
124     /**
125      * The internal state of {@link HttpObjectDecoder}.
126      * <em>Internal use only</em>.
127      */
128     private enum State {
129         SKIP_CONTROL_CHARS,
130         READ_INITIAL,
131         READ_HEADER,
132         READ_VARIABLE_LENGTH_CONTENT,
133         READ_FIXED_LENGTH_CONTENT,
134         READ_CHUNK_SIZE,
135         READ_CHUNKED_CONTENT,
136         READ_CHUNK_DELIMITER,
137         READ_CHUNK_FOOTER,
138         BAD_MESSAGE,
139         UPGRADED
140     }
141 
142     private State currentState = State.SKIP_CONTROL_CHARS;
143 
144     /**
145      * Creates a new instance with the default
146      * {@code maxInitialLineLength (4096}}, {@code maxHeaderSize (8192)}, and
147      * {@code maxChunkSize (8192)}.
148      */
149     protected HttpObjectDecoder() {
150         this(4096, 8192, 8192, true);
151     }
152 
153     /**
154      * Creates a new instance with the specified parameters.
155      */
156     protected HttpObjectDecoder(
157             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize, boolean chunkedSupported) {
158         this(maxInitialLineLength, maxHeaderSize, maxChunkSize, chunkedSupported, true);
159     }
160 
161     /**
162      * Creates a new instance with the specified parameters.
163      */
164     protected HttpObjectDecoder(
165             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
166             boolean chunkedSupported, boolean validateHeaders) {
167         this(maxInitialLineLength, maxHeaderSize, maxChunkSize, chunkedSupported, validateHeaders, 128);
168     }
169 
170     protected HttpObjectDecoder(
171             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize,
172             boolean chunkedSupported, boolean validateHeaders, int initialBufferSize) {
173         checkPositive(maxInitialLineLength, "maxInitialLineLength");
174         checkPositive(maxHeaderSize, "maxHeaderSize");
175         checkPositive(maxChunkSize, "maxChunkSize");
176 
177         AppendableCharSequence seq = new AppendableCharSequence(initialBufferSize);
178         lineParser = new LineParser(seq, maxInitialLineLength);
179         headerParser = new HeaderParser(seq, maxHeaderSize);
180         this.maxChunkSize = maxChunkSize;
181         this.chunkedSupported = chunkedSupported;
182         this.validateHeaders = validateHeaders;
183     }
184 
185     @Override
186     protected void decode(ChannelHandlerContext ctx, ByteBuf buffer, List<Object> out) throws Exception {
187         if (resetRequested) {
188             resetNow();
189         }
190 
191         switch (currentState) {
192         case SKIP_CONTROL_CHARS: {
193             if (!skipControlCharacters(buffer)) {
194                 return;
195             }
196             currentState = State.READ_INITIAL;
197         }
198         case READ_INITIAL: try {
199             AppendableCharSequence line = lineParser.parse(buffer);
200             if (line == null) {
201                 return;
202             }
203             String[] initialLine = splitInitialLine(line);
204             if (initialLine.length < 3) {
205                 // Invalid initial line - ignore.
206                 currentState = State.SKIP_CONTROL_CHARS;
207                 return;
208             }
209 
210             message = createMessage(initialLine);
211             currentState = State.READ_HEADER;
212             // fall-through
213         } catch (Exception e) {
214             out.add(invalidMessage(buffer, e));
215             return;
216         }
217         case READ_HEADER: try {
218             State nextState = readHeaders(buffer);
219             if (nextState == null) {
220                 return;
221             }
222             currentState = nextState;
223             switch (nextState) {
224             case SKIP_CONTROL_CHARS:
225                 // fast-path
226                 // No content is expected.
227                 out.add(message);
228                 out.add(LastHttpContent.EMPTY_LAST_CONTENT);
229                 resetNow();
230                 return;
231             case READ_CHUNK_SIZE:
232                 if (!chunkedSupported) {
233                     throw new IllegalArgumentException("Chunked messages not supported");
234                 }
235                 // Chunked encoding - generate HttpMessage first.  HttpChunks will follow.
236                 out.add(message);
237                 return;
238             default:
239                 /**
240                  * <a href="https://tools.ietf.org/html/rfc7230#section-3.3.3">RFC 7230, 3.3.3</a> states that if a
241                  * request does not have either a transfer-encoding or a content-length header then the message body
242                  * length is 0. However for a response the body length is the number of octets received prior to the
243                  * server closing the connection. So we treat this as variable length chunked encoding.
244                  */
245                 long contentLength = contentLength();
246                 if (contentLength == 0 || contentLength == -1 && isDecodingRequest()) {
247                     out.add(message);
248                     out.add(LastHttpContent.EMPTY_LAST_CONTENT);
249                     resetNow();
250                     return;
251                 }
252 
253                 assert nextState == State.READ_FIXED_LENGTH_CONTENT ||
254                         nextState == State.READ_VARIABLE_LENGTH_CONTENT;
255 
256                 out.add(message);
257 
258                 if (nextState == State.READ_FIXED_LENGTH_CONTENT) {
259                     // chunkSize will be decreased as the READ_FIXED_LENGTH_CONTENT state reads data chunk by chunk.
260                     chunkSize = contentLength;
261                 }
262 
263                 // We return here, this forces decode to be called again where we will decode the content
264                 return;
265             }
266         } catch (Exception e) {
267             out.add(invalidMessage(buffer, e));
268             return;
269         }
270         case READ_VARIABLE_LENGTH_CONTENT: {
271             // Keep reading data as a chunk until the end of connection is reached.
272             int toRead = Math.min(buffer.readableBytes(), maxChunkSize);
273             if (toRead > 0) {
274                 ByteBuf content = buffer.readRetainedSlice(toRead);
275                 out.add(new DefaultHttpContent(content));
276             }
277             return;
278         }
279         case READ_FIXED_LENGTH_CONTENT: {
280             int readLimit = buffer.readableBytes();
281 
282             // Check if the buffer is readable first as we use the readable byte count
283             // to create the HttpChunk. This is needed as otherwise we may end up with
284             // create a HttpChunk instance that contains an empty buffer and so is
285             // handled like it is the last HttpChunk.
286             //
287             // See https://github.com/netty/netty/issues/433
288             if (readLimit == 0) {
289                 return;
290             }
291 
292             int toRead = Math.min(readLimit, maxChunkSize);
293             if (toRead > chunkSize) {
294                 toRead = (int) chunkSize;
295             }
296             ByteBuf content = buffer.readRetainedSlice(toRead);
297             chunkSize -= toRead;
298 
299             if (chunkSize == 0) {
300                 // Read all content.
301                 out.add(new DefaultLastHttpContent(content, validateHeaders));
302                 resetNow();
303             } else {
304                 out.add(new DefaultHttpContent(content));
305             }
306             return;
307         }
308         /**
309          * everything else after this point takes care of reading chunked content. basically, read chunk size,
310          * read chunk, read and ignore the CRLF and repeat until 0
311          */
312         case READ_CHUNK_SIZE: try {
313             AppendableCharSequence line = lineParser.parse(buffer);
314             if (line == null) {
315                 return;
316             }
317             int chunkSize = getChunkSize(line.toString());
318             this.chunkSize = chunkSize;
319             if (chunkSize == 0) {
320                 currentState = State.READ_CHUNK_FOOTER;
321                 return;
322             }
323             currentState = State.READ_CHUNKED_CONTENT;
324             // fall-through
325         } catch (Exception e) {
326             out.add(invalidChunk(buffer, e));
327             return;
328         }
329         case READ_CHUNKED_CONTENT: {
330             assert chunkSize <= Integer.MAX_VALUE;
331             int toRead = Math.min((int) chunkSize, maxChunkSize);
332             toRead = Math.min(toRead, buffer.readableBytes());
333             if (toRead == 0) {
334                 return;
335             }
336             HttpContent chunk = new DefaultHttpContent(buffer.readRetainedSlice(toRead));
337             chunkSize -= toRead;
338 
339             out.add(chunk);
340 
341             if (chunkSize != 0) {
342                 return;
343             }
344             currentState = State.READ_CHUNK_DELIMITER;
345             // fall-through
346         }
347         case READ_CHUNK_DELIMITER: {
348             final int wIdx = buffer.writerIndex();
349             int rIdx = buffer.readerIndex();
350             while (wIdx > rIdx) {
351                 byte next = buffer.getByte(rIdx++);
352                 if (next == HttpConstants.LF) {
353                     currentState = State.READ_CHUNK_SIZE;
354                     break;
355                 }
356             }
357             buffer.readerIndex(rIdx);
358             return;
359         }
360         case READ_CHUNK_FOOTER: try {
361             LastHttpContent trailer = readTrailingHeaders(buffer);
362             if (trailer == null) {
363                 return;
364             }
365             out.add(trailer);
366             resetNow();
367             return;
368         } catch (Exception e) {
369             out.add(invalidChunk(buffer, e));
370             return;
371         }
372         case BAD_MESSAGE: {
373             // Keep discarding until disconnection.
374             buffer.skipBytes(buffer.readableBytes());
375             break;
376         }
377         case UPGRADED: {
378             int readableBytes = buffer.readableBytes();
379             if (readableBytes > 0) {
380                 // Keep on consuming as otherwise we may trigger an DecoderException,
381                 // other handler will replace this codec with the upgraded protocol codec to
382                 // take the traffic over at some point then.
383                 // See https://github.com/netty/netty/issues/2173
384                 out.add(buffer.readBytes(readableBytes));
385             }
386             break;
387         }
388         }
389     }
390 
391     @Override
392     protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
393         super.decodeLast(ctx, in, out);
394 
395         if (resetRequested) {
396             // If a reset was requested by decodeLast() we need to do it now otherwise we may produce a
397             // LastHttpContent while there was already one.
398             resetNow();
399         }
400         // Handle the last unfinished message.
401         if (message != null) {
402             boolean chunked = HttpUtil.isTransferEncodingChunked(message);
403             if (currentState == State.READ_VARIABLE_LENGTH_CONTENT && !in.isReadable() && !chunked) {
404                 // End of connection.
405                 out.add(LastHttpContent.EMPTY_LAST_CONTENT);
406                 resetNow();
407                 return;
408             }
409 
410             if (currentState == State.READ_HEADER) {
411                 // If we are still in the state of reading headers we need to create a new invalid message that
412                 // signals that the connection was closed before we received the headers.
413                 out.add(invalidMessage(Unpooled.EMPTY_BUFFER,
414                         new PrematureChannelClosureException("Connection closed before received headers")));
415                 resetNow();
416                 return;
417             }
418 
419             // Check if the closure of the connection signifies the end of the content.
420             boolean prematureClosure;
421             if (isDecodingRequest() || chunked) {
422                 // The last request did not wait for a response.
423                 prematureClosure = true;
424             } else {
425                 // Compare the length of the received content and the 'Content-Length' header.
426                 // If the 'Content-Length' header is absent, the length of the content is determined by the end of the
427                 // connection, so it is perfectly fine.
428                 prematureClosure = contentLength() > 0;
429             }
430 
431             if (!prematureClosure) {
432                 out.add(LastHttpContent.EMPTY_LAST_CONTENT);
433             }
434             resetNow();
435         }
436     }
437 
438     @Override
439     public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
440         if (evt instanceof HttpExpectationFailedEvent) {
441             switch (currentState) {
442             case READ_FIXED_LENGTH_CONTENT:
443             case READ_VARIABLE_LENGTH_CONTENT:
444             case READ_CHUNK_SIZE:
445                 reset();
446                 break;
447             default:
448                 break;
449             }
450         }
451         super.userEventTriggered(ctx, evt);
452     }
453 
454     protected boolean isContentAlwaysEmpty(HttpMessage msg) {
455         if (msg instanceof HttpResponse) {
456             HttpResponse res = (HttpResponse) msg;
457             int code = res.status().code();
458 
459             // Correctly handle return codes of 1xx.
460             //
461             // See:
462             //     - http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html Section 4.4
463             //     - https://github.com/netty/netty/issues/222
464             if (code >= 100 && code < 200) {
465                 // One exception: Hixie 76 websocket handshake response
466                 return !(code == 101 && !res.headers().contains(HttpHeaderNames.SEC_WEBSOCKET_ACCEPT)
467                          && res.headers().contains(HttpHeaderNames.UPGRADE, HttpHeaderValues.WEBSOCKET, true));
468             }
469 
470             switch (code) {
471             case 204: case 304:
472                 return true;
473             }
474         }
475         return false;
476     }
477 
478     /**
479      * Returns true if the server switched to a different protocol than HTTP/1.0 or HTTP/1.1, e.g. HTTP/2 or Websocket.
480      * Returns false if the upgrade happened in a different layer, e.g. upgrade from HTTP/1.1 to HTTP/1.1 over TLS.
481      */
482     protected boolean isSwitchingToNonHttp1Protocol(HttpResponse msg) {
483         if (msg.status().code() != HttpResponseStatus.SWITCHING_PROTOCOLS.code()) {
484             return false;
485         }
486         String newProtocol = msg.headers().get(HttpHeaderNames.UPGRADE);
487         return newProtocol == null ||
488                 !newProtocol.contains(HttpVersion.HTTP_1_0.text()) &&
489                 !newProtocol.contains(HttpVersion.HTTP_1_1.text());
490     }
491 
492     /**
493      * Resets the state of the decoder so that it is ready to decode a new message.
494      * This method is useful for handling a rejected request with {@code Expect: 100-continue} header.
495      */
496     public void reset() {
497         resetRequested = true;
498     }
499 
500     private void resetNow() {
501         HttpMessage message = this.message;
502         this.message = null;
503         name = null;
504         value = null;
505         contentLength = Long.MIN_VALUE;
506         lineParser.reset();
507         headerParser.reset();
508         trailer = null;
509         if (!isDecodingRequest()) {
510             HttpResponse res = (HttpResponse) message;
511             if (res != null && isSwitchingToNonHttp1Protocol(res)) {
512                 currentState = State.UPGRADED;
513                 return;
514             }
515         }
516 
517         resetRequested = false;
518         currentState = State.SKIP_CONTROL_CHARS;
519     }
520 
521     private HttpMessage invalidMessage(ByteBuf in, Exception cause) {
522         currentState = State.BAD_MESSAGE;
523 
524         // Advance the readerIndex so that ByteToMessageDecoder does not complain
525         // when we produced an invalid message without consuming anything.
526         in.skipBytes(in.readableBytes());
527 
528         if (message == null) {
529             message = createInvalidMessage();
530         }
531         message.setDecoderResult(DecoderResult.failure(cause));
532 
533         HttpMessage ret = message;
534         message = null;
535         return ret;
536     }
537 
538     private HttpContent invalidChunk(ByteBuf in, Exception cause) {
539         currentState = State.BAD_MESSAGE;
540 
541         // Advance the readerIndex so that ByteToMessageDecoder does not complain
542         // when we produced an invalid message without consuming anything.
543         in.skipBytes(in.readableBytes());
544 
545         HttpContent chunk = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER);
546         chunk.setDecoderResult(DecoderResult.failure(cause));
547         message = null;
548         trailer = null;
549         return chunk;
550     }
551 
552     private static boolean skipControlCharacters(ByteBuf buffer) {
553         boolean skiped = false;
554         final int wIdx = buffer.writerIndex();
555         int rIdx = buffer.readerIndex();
556         while (wIdx > rIdx) {
557             int c = buffer.getUnsignedByte(rIdx++);
558             if (!Character.isISOControl(c) && !Character.isWhitespace(c)) {
559                 rIdx--;
560                 skiped = true;
561                 break;
562             }
563         }
564         buffer.readerIndex(rIdx);
565         return skiped;
566     }
567 
568     private State readHeaders(ByteBuf buffer) {
569         final HttpMessage message = this.message;
570         final HttpHeaders headers = message.headers();
571 
572         AppendableCharSequence line = headerParser.parse(buffer);
573         if (line == null) {
574             return null;
575         }
576         if (line.length() > 0) {
577             do {
578                 char firstChar = line.charAt(0);
579                 if (name != null && (firstChar == ' ' || firstChar == '\t')) {
580                     //please do not make one line from below code
581                     //as it breaks +XX:OptimizeStringConcat optimization
582                     String trimmedLine = line.toString().trim();
583                     String valueStr = String.valueOf(value);
584                     value = valueStr + ' ' + trimmedLine;
585                 } else {
586                     if (name != null) {
587                         headers.add(name, value);
588                     }
589                     splitHeader(line);
590                 }
591 
592                 line = headerParser.parse(buffer);
593                 if (line == null) {
594                     return null;
595                 }
596             } while (line.length() > 0);
597         }
598 
599         // Add the last header.
600         if (name != null) {
601             headers.add(name, value);
602         }
603         // reset name and value fields
604         name = null;
605         value = null;
606 
607         State nextState;
608 
609         if (isContentAlwaysEmpty(message)) {
610             HttpUtil.setTransferEncodingChunked(message, false);
611             nextState = State.SKIP_CONTROL_CHARS;
612         } else if (HttpUtil.isTransferEncodingChunked(message)) {
613             nextState = State.READ_CHUNK_SIZE;
614         } else if (contentLength() >= 0) {
615             nextState = State.READ_FIXED_LENGTH_CONTENT;
616         } else {
617             nextState = State.READ_VARIABLE_LENGTH_CONTENT;
618         }
619         return nextState;
620     }
621 
622     private long contentLength() {
623         if (contentLength == Long.MIN_VALUE) {
624             contentLength = HttpUtil.getContentLength(message, -1L);
625         }
626         return contentLength;
627     }
628 
629     private LastHttpContent readTrailingHeaders(ByteBuf buffer) {
630         AppendableCharSequence line = headerParser.parse(buffer);
631         if (line == null) {
632             return null;
633         }
634         LastHttpContent trailer = this.trailer;
635         if (line.length() == 0 && trailer == null) {
636             // We have received the empty line which signals the trailer is complete and did not parse any trailers
637             // before. Just return an empty last content to reduce allocations.
638             return LastHttpContent.EMPTY_LAST_CONTENT;
639         }
640 
641         CharSequence lastHeader = null;
642         if (trailer == null) {
643             trailer = this.trailer = new DefaultLastHttpContent(Unpooled.EMPTY_BUFFER, validateHeaders);
644         }
645         while (line.length() > 0) {
646             char firstChar = line.charAt(0);
647             if (lastHeader != null && (firstChar == ' ' || firstChar == '\t')) {
648                 List<String> current = trailer.trailingHeaders().getAll(lastHeader);
649                 if (!current.isEmpty()) {
650                     int lastPos = current.size() - 1;
651                     //please do not make one line from below code
652                     //as it breaks +XX:OptimizeStringConcat optimization
653                     String lineTrimmed = line.toString().trim();
654                     String currentLastPos = current.get(lastPos);
655                     current.set(lastPos, currentLastPos + lineTrimmed);
656                 }
657             } else {
658                 splitHeader(line);
659                 CharSequence headerName = name;
660                 if (!HttpHeaderNames.CONTENT_LENGTH.contentEqualsIgnoreCase(headerName) &&
661                         !HttpHeaderNames.TRANSFER_ENCODING.contentEqualsIgnoreCase(headerName) &&
662                         !HttpHeaderNames.TRAILER.contentEqualsIgnoreCase(headerName)) {
663                     trailer.trailingHeaders().add(headerName, value);
664                 }
665                 lastHeader = name;
666                 // reset name and value fields
667                 name = null;
668                 value = null;
669             }
670             line = headerParser.parse(buffer);
671             if (line == null) {
672                 return null;
673             }
674         }
675 
676         this.trailer = null;
677         return trailer;
678     }
679 
680     protected abstract boolean isDecodingRequest();
681     protected abstract HttpMessage createMessage(String[] initialLine) throws Exception;
682     protected abstract HttpMessage createInvalidMessage();
683 
684     private static int getChunkSize(String hex) {
685         hex = hex.trim();
686         for (int i = 0; i < hex.length(); i ++) {
687             char c = hex.charAt(i);
688             if (c == ';' || Character.isWhitespace(c) || Character.isISOControl(c)) {
689                 hex = hex.substring(0, i);
690                 break;
691             }
692         }
693 
694         return Integer.parseInt(hex, 16);
695     }
696 
697     private static String[] splitInitialLine(AppendableCharSequence sb) {
698         int aStart;
699         int aEnd;
700         int bStart;
701         int bEnd;
702         int cStart;
703         int cEnd;
704 
705         aStart = findNonWhitespace(sb, 0);
706         aEnd = findWhitespace(sb, aStart);
707 
708         bStart = findNonWhitespace(sb, aEnd);
709         bEnd = findWhitespace(sb, bStart);
710 
711         cStart = findNonWhitespace(sb, bEnd);
712         cEnd = findEndOfString(sb);
713 
714         return new String[] {
715                 sb.subStringUnsafe(aStart, aEnd),
716                 sb.subStringUnsafe(bStart, bEnd),
717                 cStart < cEnd? sb.subStringUnsafe(cStart, cEnd) : "" };
718     }
719 
720     private void splitHeader(AppendableCharSequence sb) {
721         final int length = sb.length();
722         int nameStart;
723         int nameEnd;
724         int colonEnd;
725         int valueStart;
726         int valueEnd;
727 
728         nameStart = findNonWhitespace(sb, 0);
729         for (nameEnd = nameStart; nameEnd < length; nameEnd ++) {
730             char ch = sb.charAt(nameEnd);
731             if (ch == ':' || Character.isWhitespace(ch)) {
732                 break;
733             }
734         }
735 
736         for (colonEnd = nameEnd; colonEnd < length; colonEnd ++) {
737             if (sb.charAt(colonEnd) == ':') {
738                 colonEnd ++;
739                 break;
740             }
741         }
742 
743         name = sb.subStringUnsafe(nameStart, nameEnd);
744         valueStart = findNonWhitespace(sb, colonEnd);
745         if (valueStart == length) {
746             value = EMPTY_VALUE;
747         } else {
748             valueEnd = findEndOfString(sb);
749             value = sb.subStringUnsafe(valueStart, valueEnd);
750         }
751     }
752 
753     private static int findNonWhitespace(AppendableCharSequence sb, int offset) {
754         for (int result = offset; result < sb.length(); ++result) {
755             if (!Character.isWhitespace(sb.charAtUnsafe(result))) {
756                 return result;
757             }
758         }
759         return sb.length();
760     }
761 
762     private static int findWhitespace(AppendableCharSequence sb, int offset) {
763         for (int result = offset; result < sb.length(); ++result) {
764             if (Character.isWhitespace(sb.charAtUnsafe(result))) {
765                 return result;
766             }
767         }
768         return sb.length();
769     }
770 
771     private static int findEndOfString(AppendableCharSequence sb) {
772         for (int result = sb.length() - 1; result > 0; --result) {
773             if (!Character.isWhitespace(sb.charAtUnsafe(result))) {
774                 return result + 1;
775             }
776         }
777         return 0;
778     }
779 
780     private static class HeaderParser implements ByteProcessor {
781         private final AppendableCharSequence seq;
782         private final int maxLength;
783         private int size;
784 
785         HeaderParser(AppendableCharSequence seq, int maxLength) {
786             this.seq = seq;
787             this.maxLength = maxLength;
788         }
789 
790         public AppendableCharSequence parse(ByteBuf buffer) {
791             final int oldSize = size;
792             seq.reset();
793             int i = buffer.forEachByte(this);
794             if (i == -1) {
795                 size = oldSize;
796                 return null;
797             }
798             buffer.readerIndex(i + 1);
799             return seq;
800         }
801 
802         public void reset() {
803             size = 0;
804         }
805 
806         @Override
807         public boolean process(byte value) throws Exception {
808             char nextByte = (char) (value & 0xFF);
809             if (nextByte == HttpConstants.CR) {
810                 return true;
811             }
812             if (nextByte == HttpConstants.LF) {
813                 return false;
814             }
815 
816             if (++ size > maxLength) {
817                 // TODO: Respond with Bad Request and discard the traffic
818                 //    or close the connection.
819                 //       No need to notify the upstream handlers - just log.
820                 //       If decoding a response, just throw an exception.
821                 throw newException(maxLength);
822             }
823 
824             seq.append(nextByte);
825             return true;
826         }
827 
828         protected TooLongFrameException newException(int maxLength) {
829             return new TooLongFrameException("HTTP header is larger than " + maxLength + " bytes.");
830         }
831     }
832 
833     private static final class LineParser extends HeaderParser {
834 
835         LineParser(AppendableCharSequence seq, int maxLength) {
836             super(seq, maxLength);
837         }
838 
839         @Override
840         public AppendableCharSequence parse(ByteBuf buffer) {
841             reset();
842             return super.parse(buffer);
843         }
844 
845         @Override
846         protected TooLongFrameException newException(int maxLength) {
847             return new TooLongFrameException("An HTTP line is larger than " + maxLength + " bytes.");
848         }
849     }
850 }