src/jdk.incubator.httpclient/share/classes/jdk/incubator/http/Http1HeaderParser.java
branchhttp-client-branch
changeset 55763 634d8e14c172
child 48535 5f9977540ac9
child 55792 0936888d5a4a
equal deleted inserted replaced
55762:e947a3a50a95 55763:634d8e14c172
       
     1 /*
       
     2  * Copyright (c) 2017, Oracle and/or its affiliates. All rights reserved.
       
     3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
       
     4  *
       
     5  * This code is free software; you can redistribute it and/or modify it
       
     6  * under the terms of the GNU General Public License version 2 only, as
       
     7  * published by the Free Software Foundation.  Oracle designates this
       
     8  * particular file as subject to the "Classpath" exception as provided
       
     9  * by Oracle in the LICENSE file that accompanied this code.
       
    10  *
       
    11  * This code is distributed in the hope that it will be useful, but WITHOUT
       
    12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
       
    13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
       
    14  * version 2 for more details (a copy is included in the LICENSE file that
       
    15  * accompanied this code).
       
    16  *
       
    17  * You should have received a copy of the GNU General Public License version
       
    18  * 2 along with this work; if not, write to the Free Software Foundation,
       
    19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
       
    20  *
       
    21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
       
    22  * or visit www.oracle.com if you need additional information or have any
       
    23  * questions.
       
    24  */
       
    25 
       
    26 package jdk.incubator.http;
       
    27 
       
    28 import java.net.ProtocolException;
       
    29 import java.nio.ByteBuffer;
       
    30 import java.util.ArrayList;
       
    31 import java.util.HashMap;
       
    32 import java.util.List;
       
    33 import java.util.Locale;
       
    34 import java.util.Map;
       
    35 import static java.lang.String.format;
       
    36 import static java.util.Objects.requireNonNull;
       
    37 
       
    38 class Http1HeaderParser {
       
    39 
       
    40     private static final char CR = '\r';
       
    41     private static final char LF = '\n';
       
    42     private static final char HT = '\t';
       
    43     private static final char SP = ' ';
       
    44 
       
    45     private StringBuilder sb = new StringBuilder();
       
    46     private String statusLine;
       
    47     private int responseCode;
       
    48     private HttpHeaders headers;
       
    49     private Map<String,List<String>> privateMap = new HashMap<>();
       
    50 
       
    51     enum State { STATUS_LINE,
       
    52                  STATUS_LINE_FOUND_CR,
       
    53                  STATUS_LINE_END,
       
    54                  STATUS_LINE_END_CR,
       
    55                  HEADER,
       
    56                  HEADER_FOUND_CR,
       
    57                  HEADER_FOUND_LF,
       
    58                  HEADER_FOUND_CR_LF,
       
    59                  HEADER_FOUND_CR_LF_CR,
       
    60                  FINISHED }
       
    61 
       
    62     private State state = State.STATUS_LINE;
       
    63 
       
    64     /** Returns the status-line. */
       
    65     String statusLine() { return statusLine; }
       
    66 
       
    67     /** Returns the response code. */
       
    68     int responseCode() { return responseCode; }
       
    69 
       
    70     /** Returns the headers, possibly empty. */
       
    71     HttpHeaders headers() { assert state == State.FINISHED; return headers; }
       
    72 
       
    73     /**
       
    74      * Parses HTTP/1.X status-line and headers from the given bytes. Must be
       
    75      * called successive times, with additional data, until returns true.
       
    76      *
       
    77      * All given ByteBuffers will be consumed, until ( possibly ) the last one
       
    78      * ( when true is returned ), which may not be fully consumed.
       
    79      *
       
    80      * @param input the ( partial ) header data
       
    81      * @return true iff the end of the headers block has been reached
       
    82      */
       
    83     boolean parse(ByteBuffer input) throws ProtocolException {
       
    84         requireNonNull(input, "null input");
       
    85 
       
    86         while (input.hasRemaining() && state != State.FINISHED) {
       
    87             switch (state) {
       
    88                 case STATUS_LINE:
       
    89                     readResumeStatusLine(input);
       
    90                     break;
       
    91                 case STATUS_LINE_FOUND_CR:
       
    92                     readStatusLineFeed(input);
       
    93                     break;
       
    94                 case STATUS_LINE_END:
       
    95                     maybeStartHeaders(input);
       
    96                     break;
       
    97                 case STATUS_LINE_END_CR:
       
    98                     maybeEndHeaders(input);
       
    99                     break;
       
   100                 case HEADER:
       
   101                     readResumeHeader(input);
       
   102                     break;
       
   103                 // fallthrough
       
   104                 case HEADER_FOUND_CR:
       
   105                 case HEADER_FOUND_LF:
       
   106                     resumeOrLF(input);
       
   107                     break;
       
   108                 case HEADER_FOUND_CR_LF:
       
   109                     resumeOrSecondCR(input);
       
   110                     break;
       
   111                 case HEADER_FOUND_CR_LF_CR:
       
   112                     resumeOrEndHeaders(input);
       
   113                     break;
       
   114                 default:
       
   115                     throw new InternalError(
       
   116                             "Unexpected state: " + String.valueOf(state));
       
   117             }
       
   118         }
       
   119 
       
   120         return state == State.FINISHED;
       
   121     }
       
   122 
       
   123     private void readResumeStatusLine(ByteBuffer input) {
       
   124         char c = 0;
       
   125         while (input.hasRemaining() && (c =(char)input.get()) != CR) {
       
   126             sb.append(c);
       
   127         }
       
   128 
       
   129         if (c == CR) {
       
   130             state = State.STATUS_LINE_FOUND_CR;
       
   131         }
       
   132     }
       
   133 
       
   134     private void readStatusLineFeed(ByteBuffer input) throws ProtocolException {
       
   135         char c = (char)input.get();
       
   136         if (c != LF) {
       
   137             throw protocolException("Bad trailing char, \"%s\", when parsing status-line, \"%s\"",
       
   138                                     c, sb.toString());
       
   139         }
       
   140 
       
   141         statusLine = sb.toString();
       
   142         sb = new StringBuilder();
       
   143         if (!statusLine.startsWith("HTTP/1.")) {
       
   144             throw protocolException("Invalid status line: \"%s\"", statusLine);
       
   145         }
       
   146         if (statusLine.length() < 12) {
       
   147             throw protocolException("Invalid status line: \"%s\"", statusLine);
       
   148         }
       
   149         responseCode = Integer.parseInt(statusLine.substring(9, 12));
       
   150 
       
   151         state = State.STATUS_LINE_END;
       
   152     }
       
   153 
       
   154     private void maybeStartHeaders(ByteBuffer input) {
       
   155         assert state == State.STATUS_LINE_END;
       
   156         assert sb.length() == 0;
       
   157         char c = (char)input.get();
       
   158         if (c == CR) {
       
   159             state = State.STATUS_LINE_END_CR;
       
   160         } else {
       
   161             sb.append(c);
       
   162             state = State.HEADER;
       
   163         }
       
   164     }
       
   165 
       
   166     private void maybeEndHeaders(ByteBuffer input) throws ProtocolException {
       
   167         assert state == State.STATUS_LINE_END_CR;
       
   168         assert sb.length() == 0;
       
   169         char c = (char)input.get();
       
   170         if (c == LF) {
       
   171             headers = ImmutableHeaders.of(privateMap);
       
   172             privateMap = null;
       
   173             state = State.FINISHED;  // no headers
       
   174         } else {
       
   175             throw protocolException("Unexpected \"%s\", after status-line CR", c);
       
   176         }
       
   177     }
       
   178 
       
   179     private void readResumeHeader(ByteBuffer input) {
       
   180         assert state == State.HEADER;
       
   181         assert input.hasRemaining();
       
   182         while (input.hasRemaining()) {
       
   183             char c = (char)input.get();
       
   184             if (c == CR) {
       
   185                 state = State.HEADER_FOUND_CR;
       
   186                 break;
       
   187             } else if (c == LF) {
       
   188                 state = State.HEADER_FOUND_LF;
       
   189                 break;
       
   190             }
       
   191 
       
   192             if (c == HT)
       
   193                 c = SP;
       
   194             sb.append(c);
       
   195         }
       
   196     }
       
   197 
       
   198     private void addHeaderFromString(String headerString) {
       
   199         assert sb.length() == 0;
       
   200         int idx = headerString.indexOf(':');
       
   201         if (idx == -1)
       
   202             return;
       
   203         String name = headerString.substring(0, idx).trim();
       
   204         if (name.isEmpty())
       
   205             return;
       
   206         String value = headerString.substring(idx + 1, headerString.length()).trim();
       
   207 
       
   208         privateMap.computeIfAbsent(name.toLowerCase(Locale.US),
       
   209                                    k -> new ArrayList<>()).add(value);
       
   210     }
       
   211 
       
   212     private void resumeOrLF(ByteBuffer input) {
       
   213         assert state == State.HEADER_FOUND_CR || state == State.HEADER_FOUND_LF;
       
   214         char c = (char)input.get();
       
   215         if (c == LF && state == State.HEADER_FOUND_CR) {
       
   216             String headerString = sb.toString();
       
   217             sb = new StringBuilder();
       
   218             addHeaderFromString(headerString);
       
   219             state = State.HEADER_FOUND_CR_LF;
       
   220         } else if (c == SP || c == HT) {
       
   221             sb.append(SP); // parity with MessageHeaders
       
   222             state = State.HEADER;
       
   223         } else {
       
   224             sb = new StringBuilder();
       
   225             sb.append(c);
       
   226             state = State.HEADER;
       
   227         }
       
   228     }
       
   229 
       
   230     private void resumeOrSecondCR(ByteBuffer input) {
       
   231         assert state == State.HEADER_FOUND_CR_LF;
       
   232         assert sb.length() == 0;
       
   233         char c = (char)input.get();
       
   234         if (c == CR) {
       
   235             state = State.HEADER_FOUND_CR_LF_CR;
       
   236         } else {
       
   237             sb.append(c);
       
   238             state = State.HEADER;
       
   239         }
       
   240     }
       
   241 
       
   242     private void resumeOrEndHeaders(ByteBuffer input) throws ProtocolException {
       
   243         assert state == State.HEADER_FOUND_CR_LF_CR;
       
   244         char c = (char)input.get();
       
   245         if (c == LF) {
       
   246             state = State.FINISHED;
       
   247             headers = ImmutableHeaders.of(privateMap);
       
   248             privateMap = null;
       
   249         } else {
       
   250             throw protocolException("Unexpected \"%s\", after CR LF CR", c);
       
   251         }
       
   252     }
       
   253 
       
   254     private ProtocolException protocolException(String format, Object... args) {
       
   255         return new ProtocolException(format(format, args));
       
   256     }
       
   257 }