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