View Javadoc
1   /*
2    * Copyright 2012 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package org.jboss.netty.handler.codec.http;
17  
18  import org.jboss.netty.buffer.ChannelBuffer;
19  import org.jboss.netty.buffer.ChannelBuffers;
20  import org.jboss.netty.channel.Channel;
21  import org.jboss.netty.channel.ChannelHandlerContext;
22  import org.jboss.netty.channel.ChannelPipeline;
23  import org.jboss.netty.handler.codec.frame.TooLongFrameException;
24  import org.jboss.netty.handler.codec.http.HttpMessageDecoder.State;
25  import org.jboss.netty.handler.codec.replay.ReplayingDecoder;
26  
27  import java.util.List;
28  
29  /**
30   * Decodes {@link ChannelBuffer}s into {@link HttpMessage}s and
31   * {@link HttpChunk}s.
32   *
33   * <h3>Parameters that prevents excessive memory consumption</h3>
34   * <table border="1">
35   * <tr>
36   * <th>Name</th><th>Meaning</th>
37   * </tr>
38   * <tr>
39   * <td>{@code maxInitialLineLength}</td>
40   * <td>The maximum length of the initial line
41   *     (e.g. {@code "GET / HTTP/1.0"} or {@code "HTTP/1.0 200 OK"})
42   *     If the length of the initial line exceeds this value, a
43   *     {@link TooLongFrameException} will be raised.</td>
44   * </tr>
45   * <tr>
46   * <td>{@code maxHeaderSize}</td>
47   * <td>The maximum length of all headers.  If the sum of the length of each
48   *     header exceeds this value, a {@link TooLongFrameException} will be raised.</td>
49   * </tr>
50   * <tr>
51   * <td>{@code maxChunkSize}</td>
52   * <td>The maximum length of the content or each chunk.  If the content length
53   *     (or the length of each chunk) exceeds this value, the content or chunk
54   *     will be split into multiple {@link HttpChunk}s whose length is
55   *     {@code maxChunkSize} at maximum.</td>
56   * </tr>
57   * </table>
58   *
59   * <h3>Chunked Content</h3>
60   *
61   * If the content of an HTTP message is greater than {@code maxChunkSize} or
62   * the transfer encoding of the HTTP message is 'chunked', this decoder
63   * generates one {@link HttpMessage} instance and its following
64   * {@link HttpChunk}s per single HTTP message to avoid excessive memory
65   * consumption. For example, the following HTTP message:
66   * <pre>
67   * GET / HTTP/1.1
68   * Transfer-Encoding: chunked
69   *
70   * 1a
71   * abcdefghijklmnopqrstuvwxyz
72   * 10
73   * 1234567890abcdef
74   * 0
75   * Content-MD5: ...
76   * <i>[blank line]</i>
77   * </pre>
78   * triggers {@link HttpRequestDecoder} to generate 4 objects:
79   * <ol>
80   * <li>An {@link HttpRequest} whose {@link HttpMessage#isChunked() chunked}
81   *     property is {@code true},</li>
82   * <li>The first {@link HttpChunk} whose content is {@code 'abcdefghijklmnopqrstuvwxyz'},</li>
83   * <li>The second {@link HttpChunk} whose content is {@code '1234567890abcdef'}, and</li>
84   * <li>An {@link HttpChunkTrailer} which marks the end of the content.</li>
85   * </ol>
86   *
87   * If you prefer not to handle {@link HttpChunk}s by yourself for your
88   * convenience, insert {@link HttpChunkAggregator} 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  * @apiviz.landmark
101  */
102 public abstract class HttpMessageDecoder extends ReplayingDecoder<State> {
103 
104     private final int maxInitialLineLength;
105     private final int maxHeaderSize;
106     private final int maxChunkSize;
107     private HttpMessage message;
108     private ChannelBuffer content;
109     private long chunkSize;
110     private int headerSize;
111     private int contentRead;
112 
113     /**
114      * The internal state of {@link HttpMessageDecoder}.
115      * <em>Internal use only</em>.
116      * @apiviz.exclude
117      */
118     protected enum State {
119         SKIP_CONTROL_CHARS,
120         READ_INITIAL,
121         READ_HEADER,
122         READ_VARIABLE_LENGTH_CONTENT,
123         READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS,
124         READ_FIXED_LENGTH_CONTENT,
125         READ_FIXED_LENGTH_CONTENT_AS_CHUNKS,
126         READ_CHUNK_SIZE,
127         READ_CHUNKED_CONTENT,
128         READ_CHUNKED_CONTENT_AS_CHUNKS,
129         READ_CHUNK_DELIMITER,
130         READ_CHUNK_FOOTER,
131         UPGRADED
132     }
133 
134     /**
135      * Creates a new instance with the default
136      * {@code maxInitialLineLength (4096}}, {@code maxHeaderSize (8192)}, and
137      * {@code maxChunkSize (8192)}.
138      */
139     protected HttpMessageDecoder() {
140         this(4096, 8192, 8192);
141     }
142 
143     /**
144      * Creates a new instance with the specified parameters.
145      */
146     protected HttpMessageDecoder(
147             int maxInitialLineLength, int maxHeaderSize, int maxChunkSize) {
148 
149         super(State.SKIP_CONTROL_CHARS, true);
150 
151         if (maxInitialLineLength <= 0) {
152             throw new IllegalArgumentException(
153                     "maxInitialLineLength must be a positive integer: " +
154                     maxInitialLineLength);
155         }
156         if (maxHeaderSize <= 0) {
157             throw new IllegalArgumentException(
158                     "maxHeaderSize must be a positive integer: " +
159                     maxHeaderSize);
160         }
161         if (maxChunkSize < 0) {
162             throw new IllegalArgumentException(
163                     "maxChunkSize must be a positive integer: " +
164                     maxChunkSize);
165         }
166         this.maxInitialLineLength = maxInitialLineLength;
167         this.maxHeaderSize = maxHeaderSize;
168         this.maxChunkSize = maxChunkSize;
169     }
170 
171     @Override
172     protected Object decode(
173             ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer, State state) throws Exception {
174         switch (state) {
175         case SKIP_CONTROL_CHARS: {
176             try {
177                 skipControlCharacters(buffer);
178                 checkpoint(State.READ_INITIAL);
179             } finally {
180                 checkpoint();
181             }
182         }
183         case READ_INITIAL: {
184             String[] initialLine = splitInitialLine(readLine(buffer, maxInitialLineLength));
185             if (initialLine.length < 3) {
186                 // Invalid initial line - ignore.
187                 checkpoint(State.SKIP_CONTROL_CHARS);
188                 return null;
189             }
190 
191             message = createMessage(initialLine);
192             checkpoint(State.READ_HEADER);
193         }
194         case READ_HEADER: {
195             State nextState = readHeaders(buffer);
196             checkpoint(nextState);
197             if (nextState == State.READ_CHUNK_SIZE) {
198                 // Chunked encoding
199                 message.setChunked(true);
200                 // Generate HttpMessage first.  HttpChunks will follow.
201                 return message;
202             }
203             if (nextState == State.SKIP_CONTROL_CHARS) {
204                 // No content is expected.
205                 // Remove the headers which are not supposed to be present not
206                 // to confuse subsequent handlers.
207                 message.headers().remove(HttpHeaders.Names.TRANSFER_ENCODING);
208                 resetState();
209                 return message;
210             }
211             long contentLength = HttpHeaders.getContentLength(message, -1);
212             if (contentLength == 0 || contentLength == -1 && isDecodingRequest()) {
213                 content = ChannelBuffers.EMPTY_BUFFER;
214                 return reset();
215             }
216 
217             switch (nextState) {
218             case READ_FIXED_LENGTH_CONTENT:
219                 if (contentLength > maxChunkSize || HttpHeaders.is100ContinueExpected(message)) {
220                     // Generate HttpMessage first.  HttpChunks will follow.
221                     checkpoint(State.READ_FIXED_LENGTH_CONTENT_AS_CHUNKS);
222                     message.setChunked(true);
223                     // chunkSize will be decreased as the READ_FIXED_LENGTH_CONTENT_AS_CHUNKS
224                     // state reads data chunk by chunk.
225                     chunkSize = HttpHeaders.getContentLength(message, -1);
226                     return message;
227                 }
228                 break;
229             case READ_VARIABLE_LENGTH_CONTENT:
230                 if (buffer.readableBytes() > maxChunkSize || HttpHeaders.is100ContinueExpected(message)) {
231                     // Generate HttpMessage first.  HttpChunks will follow.
232                     checkpoint(State.READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS);
233                     message.setChunked(true);
234                     return message;
235                 }
236                 break;
237             default:
238                 throw new IllegalStateException("Unexpected state: " + nextState);
239             }
240             // We return null here, this forces decode to be called again where we will decode the content
241             return null;
242         }
243         case READ_VARIABLE_LENGTH_CONTENT: {
244             int toRead = actualReadableBytes();
245             if (toRead > maxChunkSize) {
246                 toRead = maxChunkSize;
247             }
248             if (!message.isChunked()) {
249                 message.setChunked(true);
250                 return new Object[] {message, new DefaultHttpChunk(buffer.readBytes(toRead))};
251             } else {
252                 return new DefaultHttpChunk(buffer.readBytes(toRead));
253             }
254         }
255         case READ_VARIABLE_LENGTH_CONTENT_AS_CHUNKS: {
256             // Keep reading data as a chunk until the end of connection is reached.
257             int toRead = actualReadableBytes();
258             if (toRead > maxChunkSize) {
259                 toRead = maxChunkSize;
260             }
261             HttpChunk chunk = new DefaultHttpChunk(buffer.readBytes(toRead));
262 
263             if (!buffer.readable()) {
264                 // Reached to the end of the connection.
265                 reset();
266                 if (!chunk.isLast()) {
267                     // Append the last chunk.
268                     return new Object[] { chunk, HttpChunk.LAST_CHUNK };
269                 }
270             }
271             return chunk;
272         }
273         case READ_FIXED_LENGTH_CONTENT: {
274             return readFixedLengthContent(buffer);
275         }
276         case READ_FIXED_LENGTH_CONTENT_AS_CHUNKS: {
277             long chunkSize = this.chunkSize;
278             int readLimit = actualReadableBytes();
279 
280             // Check if the buffer is readable first as we use the readable byte count
281             // to create the HttpChunk. This is needed as otherwise we may end up with
282             // create a HttpChunk instance that contains an empty buffer and so is
283             // handled like it is the last HttpChunk.
284             //
285             // See https://github.com/netty/netty/issues/433
286             if (readLimit == 0) {
287                 return null;
288             }
289 
290             int toRead = readLimit;
291             if (toRead > maxChunkSize) {
292                 toRead = maxChunkSize;
293             }
294             if (toRead > chunkSize) {
295                 toRead = (int) chunkSize;
296             }
297             HttpChunk chunk = new DefaultHttpChunk(buffer.readBytes(toRead));
298             if (chunkSize > toRead) {
299                 chunkSize -= toRead;
300             } else {
301                 chunkSize = 0;
302             }
303             this.chunkSize = chunkSize;
304 
305             if (chunkSize == 0) {
306                 // Read all content.
307                 reset();
308                 if (!chunk.isLast()) {
309                     // Append the last chunk.
310                     return new Object[] { chunk, HttpChunk.LAST_CHUNK };
311                 }
312             }
313             return chunk;
314         }
315         /**
316          * everything else after this point takes care of reading chunked content. basically, read chunk size,
317          * read chunk, read and ignore the CRLF and repeat until 0
318          */
319         case READ_CHUNK_SIZE: {
320             String line = readLine(buffer, maxInitialLineLength);
321             int chunkSize = getChunkSize(line);
322             this.chunkSize = chunkSize;
323             if (chunkSize == 0) {
324                 checkpoint(State.READ_CHUNK_FOOTER);
325                 return null;
326             } else if (chunkSize > maxChunkSize) {
327                 // A chunk is too large. Split them into multiple chunks again.
328                 checkpoint(State.READ_CHUNKED_CONTENT_AS_CHUNKS);
329             } else {
330                 checkpoint(State.READ_CHUNKED_CONTENT);
331             }
332         }
333         case READ_CHUNKED_CONTENT: {
334             assert chunkSize <= Integer.MAX_VALUE;
335             HttpChunk chunk = new DefaultHttpChunk(buffer.readBytes((int) chunkSize));
336             checkpoint(State.READ_CHUNK_DELIMITER);
337             return chunk;
338         }
339         case READ_CHUNKED_CONTENT_AS_CHUNKS: {
340             assert chunkSize <= Integer.MAX_VALUE;
341             int chunkSize = (int) this.chunkSize;
342             int readLimit = actualReadableBytes();
343 
344             // Check if the buffer is readable first as we use the readable byte count
345             // to create the HttpChunk. This is needed as otherwise we may end up with
346             // create a HttpChunk instance that contains an empty buffer and so is
347             // handled like it is the last HttpChunk.
348             //
349             // See https://github.com/netty/netty/issues/433
350             if (readLimit == 0) {
351                 return null;
352             }
353 
354             int toRead = chunkSize;
355             if (toRead > maxChunkSize) {
356                 toRead = maxChunkSize;
357             }
358             if (toRead > readLimit) {
359                 toRead = readLimit;
360             }
361             HttpChunk chunk = new DefaultHttpChunk(buffer.readBytes(toRead));
362             if (chunkSize > toRead) {
363                 chunkSize -= toRead;
364             } else {
365                 chunkSize = 0;
366             }
367             this.chunkSize = chunkSize;
368 
369             if (chunkSize == 0) {
370                 // Read all content.
371                 checkpoint(State.READ_CHUNK_DELIMITER);
372             }
373 
374             if (!chunk.isLast()) {
375                 return chunk;
376             }
377         }
378         case READ_CHUNK_DELIMITER: {
379             for (;;) {
380                 byte next = buffer.readByte();
381                 if (next == HttpConstants.CR) {
382                     if (buffer.readByte() == HttpConstants.LF) {
383                         checkpoint(State.READ_CHUNK_SIZE);
384                         return null;
385                     }
386                 } else if (next == HttpConstants.LF) {
387                     checkpoint(State.READ_CHUNK_SIZE);
388                     return null;
389                 }
390             }
391         }
392         case READ_CHUNK_FOOTER: {
393             HttpChunkTrailer trailer = readTrailingHeaders(buffer);
394             if (maxChunkSize == 0) {
395                 // Chunked encoding disabled.
396                 return reset();
397             } else {
398                 reset();
399                 // The last chunk, which is empty
400                 return trailer;
401             }
402         }
403         case UPGRADED: {
404             int readableBytes = actualReadableBytes();
405             if (readableBytes > 0) {
406                 // Keep on consuming as otherwise we may trigger an DecoderException,
407                 // other handler will replace this codec with the upgraded protocol codec to
408                 // take the traffic over at some point then.
409                 // See https://github.com/netty/netty/issues/2173
410                 return buffer.readBytes(actualReadableBytes());
411             } else {
412                 return null;
413             }
414         }
415         default: {
416             throw new Error("Shouldn't reach here.");
417         }
418         }
419     }
420 
421     protected boolean isContentAlwaysEmpty(HttpMessage msg) {
422         if (msg instanceof HttpResponse) {
423             HttpResponse res = (HttpResponse) msg;
424             int code = res.getStatus().getCode();
425 
426             // Correctly handle return codes of 1xx.
427             //
428             // See:
429             //     - http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html Section 4.4
430             //     - https://github.com/netty/netty/issues/222
431             if (code >= 100 && code < 200) {
432                 if (code == 101 && !res.headers().contains(HttpHeaders.Names.SEC_WEBSOCKET_ACCEPT)) {
433                     // It's Hixie 76 websocket handshake response
434                     return false;
435                 }
436                 return true;
437             }
438 
439             switch (code) {
440             case 204: case 205: case 304:
441                 return true;
442             }
443         }
444         return false;
445     }
446 
447     private Object reset() {
448         HttpMessage message = this.message;
449         ChannelBuffer content = this.content;
450 
451         if (content != null) {
452             message.setContent(content);
453             this.content = null;
454         }
455 
456         resetState();
457         this.message = null;
458 
459         return message;
460     }
461 
462     private void resetState() {
463         if (!isDecodingRequest()) {
464             HttpResponse res = (HttpResponse) message;
465             if (res != null && res.getStatus().getCode() == 101) {
466                 checkpoint(State.UPGRADED);
467                 return;
468             }
469         }
470 
471         checkpoint(State.SKIP_CONTROL_CHARS);
472     }
473 
474     private static void skipControlCharacters(ChannelBuffer buffer) {
475         for (;;) {
476             char c = (char) buffer.readUnsignedByte();
477             if (!Character.isISOControl(c) &&
478                 !Character.isWhitespace(c)) {
479                 buffer.readerIndex(buffer.readerIndex() - 1);
480                 break;
481             }
482         }
483     }
484 
485     private Object readFixedLengthContent(ChannelBuffer buffer) {
486         //we have a content-length so we just read the correct number of bytes
487         long length = HttpHeaders.getContentLength(message, -1);
488         assert length <= Integer.MAX_VALUE;
489         int toRead = (int) length - contentRead;
490         if (toRead > actualReadableBytes()) {
491             toRead = actualReadableBytes();
492         }
493         contentRead += toRead;
494         if (length < contentRead) {
495             if (!message.isChunked()) {
496                 message.setChunked(true);
497                 return new Object[] {message, new DefaultHttpChunk(buffer.readBytes(toRead))};
498             } else {
499                 return new DefaultHttpChunk(buffer.readBytes(toRead));
500             }
501         }
502         if (content == null) {
503             content = buffer.readBytes((int) length);
504         } else {
505             content.writeBytes(buffer, (int) length);
506         }
507         return reset();
508     }
509 
510     private State readHeaders(ChannelBuffer buffer) throws TooLongFrameException {
511         headerSize = 0;
512         final HttpMessage message = this.message;
513         String line = readHeader(buffer);
514         String name = null;
515         String value = null;
516         if (line.length() != 0) {
517             message.headers().clear();
518             do {
519                 char firstChar = line.charAt(0);
520                 if (name != null && (firstChar == ' ' || firstChar == '\t')) {
521                     value = value + ' ' + line.trim();
522                 } else {
523                     if (name != null) {
524                         message.headers().add(name, value);
525                     }
526                     String[] header = splitHeader(line);
527                     name = header[0];
528                     value = header[1];
529                 }
530 
531                 line = readHeader(buffer);
532             } while (line.length() != 0);
533 
534             // Add the last header.
535             if (name != null) {
536                 message.headers().add(name, value);
537             }
538         }
539 
540         State nextState;
541 
542         if (isContentAlwaysEmpty(message)) {
543             nextState = State.SKIP_CONTROL_CHARS;
544         } else if (message.isChunked()) {
545             // HttpMessage.isChunked() returns true when either:
546             // 1) HttpMessage.setChunked(true) was called or
547             // 2) 'Transfer-Encoding' is 'chunked'.
548             // Because this decoder did not call HttpMessage.setChunked(true)
549             // yet, HttpMessage.isChunked() should return true only when
550             // 'Transfer-Encoding' is 'chunked'.
551             nextState = State.READ_CHUNK_SIZE;
552         } else if (HttpHeaders.getContentLength(message, -1) >= 0) {
553             nextState = State.READ_FIXED_LENGTH_CONTENT;
554         } else {
555             nextState = State.READ_VARIABLE_LENGTH_CONTENT;
556         }
557         return nextState;
558     }
559 
560     private HttpChunkTrailer readTrailingHeaders(ChannelBuffer buffer) throws TooLongFrameException {
561         headerSize = 0;
562         String line = readHeader(buffer);
563         String lastHeader = null;
564         if (line.length() != 0) {
565             HttpChunkTrailer trailer = new DefaultHttpChunkTrailer();
566             do {
567                 char firstChar = line.charAt(0);
568                 if (lastHeader != null && (firstChar == ' ' || firstChar == '\t')) {
569                     List<String> current = trailer.trailingHeaders().getAll(lastHeader);
570                     if (!current.isEmpty()) {
571                         int lastPos = current.size() - 1;
572                         String newString = current.get(lastPos) + line.trim();
573                         current.set(lastPos, newString);
574                     } else {
575                         // Content-Length, Transfer-Encoding, or Trailer
576                     }
577                 } else {
578                     String[] header = splitHeader(line);
579                     String name = header[0];
580                     if (!name.equalsIgnoreCase(HttpHeaders.Names.CONTENT_LENGTH) &&
581                         !name.equalsIgnoreCase(HttpHeaders.Names.TRANSFER_ENCODING) &&
582                         !name.equalsIgnoreCase(HttpHeaders.Names.TRAILER)) {
583                         trailer.trailingHeaders().add(name, header[1]);
584                     }
585                     lastHeader = name;
586                 }
587 
588                 line = readHeader(buffer);
589             } while (line.length() != 0);
590 
591             return trailer;
592         }
593 
594         return HttpChunk.LAST_CHUNK;
595     }
596 
597     private String readHeader(ChannelBuffer buffer) throws TooLongFrameException {
598         StringBuilder sb = new StringBuilder(64);
599         int headerSize = this.headerSize;
600 
601         loop:
602         for (;;) {
603             char nextByte = (char) buffer.readByte();
604             headerSize ++;
605 
606             switch (nextByte) {
607             case HttpConstants.CR:
608                 nextByte = (char) buffer.readByte();
609                 headerSize ++;
610                 if (nextByte == HttpConstants.LF) {
611                     break loop;
612                 }
613                 break;
614             case HttpConstants.LF:
615                 break loop;
616             }
617 
618             // Abort decoding if the header part is too large.
619             if (headerSize >= maxHeaderSize) {
620                 // TODO: Respond with Bad Request and discard the traffic
621                 //    or close the connection.
622                 //       No need to notify the upstream handlers - just log.
623                 //       If decoding a response, just throw an exception.
624                 throw new TooLongFrameException(
625                         "HTTP header is larger than " +
626                         maxHeaderSize + " bytes.");
627             }
628 
629             sb.append(nextByte);
630         }
631 
632         this.headerSize = headerSize;
633         return sb.toString();
634     }
635 
636     protected abstract boolean isDecodingRequest();
637     protected abstract HttpMessage createMessage(String[] initialLine) throws Exception;
638 
639     private static int getChunkSize(String hex) {
640         hex = hex.trim();
641         for (int i = 0; i < hex.length(); i ++) {
642             char c = hex.charAt(i);
643             if (c == ';' || Character.isWhitespace(c) || Character.isISOControl(c)) {
644                 hex = hex.substring(0, i);
645                 break;
646             }
647         }
648 
649         return Integer.parseInt(hex, 16);
650     }
651 
652     private static String readLine(ChannelBuffer buffer, int maxLineLength) throws TooLongFrameException {
653         StringBuilder sb = new StringBuilder(64);
654         int lineLength = 0;
655         while (true) {
656             byte nextByte = buffer.readByte();
657             if (nextByte == HttpConstants.CR) {
658                 nextByte = buffer.readByte();
659                 if (nextByte == HttpConstants.LF) {
660                     return sb.toString();
661                 }
662             } else if (nextByte == HttpConstants.LF) {
663                 return sb.toString();
664             } else {
665                 if (lineLength >= maxLineLength) {
666                     // TODO: Respond with Bad Request and discard the traffic
667                     //    or close the connection.
668                     //       No need to notify the upstream handlers - just log.
669                     //       If decoding a response, just throw an exception.
670                     throw new TooLongFrameException(
671                             "An HTTP line is larger than " + maxLineLength +
672                             " bytes.");
673                 }
674                 lineLength ++;
675                 sb.append((char) nextByte);
676             }
677         }
678     }
679 
680     private static String[] splitInitialLine(String sb) {
681         int aStart;
682         int aEnd;
683         int bStart;
684         int bEnd;
685         int cStart;
686         int cEnd;
687 
688         aStart = findNonWhitespace(sb, 0);
689         aEnd = findWhitespace(sb, aStart);
690 
691         bStart = findNonWhitespace(sb, aEnd);
692         bEnd = findWhitespace(sb, bStart);
693 
694         cStart = findNonWhitespace(sb, bEnd);
695         cEnd = findEndOfString(sb);
696 
697         return new String[] {
698                 sb.substring(aStart, aEnd),
699                 sb.substring(bStart, bEnd),
700                 cStart < cEnd? sb.substring(cStart, cEnd) : "" };
701     }
702 
703     private static String[] splitHeader(String sb) {
704         final int length = sb.length();
705         int nameStart;
706         int nameEnd;
707         int colonEnd;
708         int valueStart;
709         int valueEnd;
710 
711         nameStart = findNonWhitespace(sb, 0);
712         for (nameEnd = nameStart; nameEnd < length; nameEnd ++) {
713             char ch = sb.charAt(nameEnd);
714             if (ch == ':' || Character.isWhitespace(ch)) {
715                 break;
716             }
717         }
718 
719         for (colonEnd = nameEnd; colonEnd < length; colonEnd ++) {
720             if (sb.charAt(colonEnd) == ':') {
721                 colonEnd ++;
722                 break;
723             }
724         }
725 
726         valueStart = findNonWhitespace(sb, colonEnd);
727         if (valueStart == length) {
728             return new String[] {
729                     sb.substring(nameStart, nameEnd),
730                     ""
731             };
732         }
733 
734         valueEnd = findEndOfString(sb);
735         return new String[] {
736                 sb.substring(nameStart, nameEnd),
737                 sb.substring(valueStart, valueEnd)
738         };
739     }
740 
741     private static int findNonWhitespace(String sb, int offset) {
742         int result;
743         for (result = offset; result < sb.length(); result ++) {
744             if (!Character.isWhitespace(sb.charAt(result))) {
745                 break;
746             }
747         }
748         return result;
749     }
750 
751     private static int findWhitespace(String sb, int offset) {
752         int result;
753         for (result = offset; result < sb.length(); result ++) {
754             if (Character.isWhitespace(sb.charAt(result))) {
755                 break;
756             }
757         }
758         return result;
759     }
760 
761     private static int findEndOfString(String sb) {
762         int result;
763         for (result = sb.length(); result > 0; result --) {
764             if (!Character.isWhitespace(sb.charAt(result - 1))) {
765                 break;
766             }
767         }
768         return result;
769     }
770 }