View Javadoc

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