src/java.net.http/share/classes/jdk/internal/net/http/Http1HeaderParser.java
branchhttp-client-branch
changeset 56092 fd85b2bf2b0d
parent 56089 42208b2f224e
child 56451 9585061fdb04
equal deleted inserted replaced
56091:aedd6133e7a0 56092:fd85b2bf2b0d
       
     1 /*
       
     2  * Copyright (c) 2017, 2018, 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.internal.net.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 java.net.http.HttpHeaders;
       
    36 import static java.lang.String.format;
       
    37 import static java.util.Objects.requireNonNull;
       
    38 
       
    39 class Http1HeaderParser {
       
    40 
       
    41     private static final char CR = '\r';
       
    42     private static final char LF = '\n';
       
    43     private static final char HT = '\t';
       
    44     private static final char SP = ' ';
       
    45 
       
    46     private StringBuilder sb = new StringBuilder();
       
    47     private String statusLine;
       
    48     private int responseCode;
       
    49     private HttpHeaders headers;
       
    50     private Map<String,List<String>> privateMap = new HashMap<>();
       
    51 
       
    52     enum State { STATUS_LINE,
       
    53                  STATUS_LINE_FOUND_CR,
       
    54                  STATUS_LINE_END,
       
    55                  STATUS_LINE_END_CR,
       
    56                  HEADER,
       
    57                  HEADER_FOUND_CR,
       
    58                  HEADER_FOUND_LF,
       
    59                  HEADER_FOUND_CR_LF,
       
    60                  HEADER_FOUND_CR_LF_CR,
       
    61                  FINISHED }
       
    62 
       
    63     private State state = State.STATUS_LINE;
       
    64 
       
    65     /** Returns the status-line. */
       
    66     String statusLine() { return statusLine; }
       
    67 
       
    68     /** Returns the response code. */
       
    69     int responseCode() { return responseCode; }
       
    70 
       
    71     /** Returns the headers, possibly empty. */
       
    72     HttpHeaders headers() { assert state == State.FINISHED; return headers; }
       
    73 
       
    74     /**
       
    75      * Parses HTTP/1.X status-line and headers from the given bytes. Must be
       
    76      * called successive times, with additional data, until returns true.
       
    77      *
       
    78      * All given ByteBuffers will be consumed, until ( possibly ) the last one
       
    79      * ( when true is returned ), which may not be fully consumed.
       
    80      *
       
    81      * @param input the ( partial ) header data
       
    82      * @return true iff the end of the headers block has been reached
       
    83      */
       
    84     boolean parse(ByteBuffer input) throws ProtocolException {
       
    85         requireNonNull(input, "null input");
       
    86 
       
    87         while (input.hasRemaining() && state != State.FINISHED) {
       
    88             switch (state) {
       
    89                 case STATUS_LINE:
       
    90                     readResumeStatusLine(input);
       
    91                     break;
       
    92                 case STATUS_LINE_FOUND_CR:
       
    93                     readStatusLineFeed(input);
       
    94                     break;
       
    95                 case STATUS_LINE_END:
       
    96                     maybeStartHeaders(input);
       
    97                     break;
       
    98                 case STATUS_LINE_END_CR:
       
    99                     maybeEndHeaders(input);
       
   100                     break;
       
   101                 case HEADER:
       
   102                     readResumeHeader(input);
       
   103                     break;
       
   104                 // fallthrough
       
   105                 case HEADER_FOUND_CR:
       
   106                 case HEADER_FOUND_LF:
       
   107                     resumeOrLF(input);
       
   108                     break;
       
   109                 case HEADER_FOUND_CR_LF:
       
   110                     resumeOrSecondCR(input);
       
   111                     break;
       
   112                 case HEADER_FOUND_CR_LF_CR:
       
   113                     resumeOrEndHeaders(input);
       
   114                     break;
       
   115                 default:
       
   116                     throw new InternalError(
       
   117                             "Unexpected state: " + String.valueOf(state));
       
   118             }
       
   119         }
       
   120 
       
   121         return state == State.FINISHED;
       
   122     }
       
   123 
       
   124     private void readResumeStatusLine(ByteBuffer input) {
       
   125         char c = 0;
       
   126         while (input.hasRemaining() && (c =(char)input.get()) != CR) {
       
   127             sb.append(c);
       
   128         }
       
   129 
       
   130         if (c == CR) {
       
   131             state = State.STATUS_LINE_FOUND_CR;
       
   132         }
       
   133     }
       
   134 
       
   135     private void readStatusLineFeed(ByteBuffer input) throws ProtocolException {
       
   136         char c = (char)input.get();
       
   137         if (c != LF) {
       
   138             throw protocolException("Bad trailing char, \"%s\", when parsing status-line, \"%s\"",
       
   139                                     c, sb.toString());
       
   140         }
       
   141 
       
   142         statusLine = sb.toString();
       
   143         sb = new StringBuilder();
       
   144         if (!statusLine.startsWith("HTTP/1.")) {
       
   145             throw protocolException("Invalid status line: \"%s\"", statusLine);
       
   146         }
       
   147         if (statusLine.length() < 12) {
       
   148             throw protocolException("Invalid status line: \"%s\"", statusLine);
       
   149         }
       
   150         responseCode = Integer.parseInt(statusLine.substring(9, 12));
       
   151 
       
   152         state = State.STATUS_LINE_END;
       
   153     }
       
   154 
       
   155     private void maybeStartHeaders(ByteBuffer input) {
       
   156         assert state == State.STATUS_LINE_END;
       
   157         assert sb.length() == 0;
       
   158         char c = (char)input.get();
       
   159         if (c == CR) {
       
   160             state = State.STATUS_LINE_END_CR;
       
   161         } else {
       
   162             sb.append(c);
       
   163             state = State.HEADER;
       
   164         }
       
   165     }
       
   166 
       
   167     private void maybeEndHeaders(ByteBuffer input) throws ProtocolException {
       
   168         assert state == State.STATUS_LINE_END_CR;
       
   169         assert sb.length() == 0;
       
   170         char c = (char)input.get();
       
   171         if (c == LF) {
       
   172             headers = ImmutableHeaders.of(privateMap);
       
   173             privateMap = null;
       
   174             state = State.FINISHED;  // no headers
       
   175         } else {
       
   176             throw protocolException("Unexpected \"%s\", after status-line CR", c);
       
   177         }
       
   178     }
       
   179 
       
   180     private void readResumeHeader(ByteBuffer input) {
       
   181         assert state == State.HEADER;
       
   182         assert input.hasRemaining();
       
   183         while (input.hasRemaining()) {
       
   184             char c = (char)input.get();
       
   185             if (c == CR) {
       
   186                 state = State.HEADER_FOUND_CR;
       
   187                 break;
       
   188             } else if (c == LF) {
       
   189                 state = State.HEADER_FOUND_LF;
       
   190                 break;
       
   191             }
       
   192 
       
   193             if (c == HT)
       
   194                 c = SP;
       
   195             sb.append(c);
       
   196         }
       
   197     }
       
   198 
       
   199     private void addHeaderFromString(String headerString) {
       
   200         assert sb.length() == 0;
       
   201         int idx = headerString.indexOf(':');
       
   202         if (idx == -1)
       
   203             return;
       
   204         String name = headerString.substring(0, idx).trim();
       
   205         if (name.isEmpty())
       
   206             return;
       
   207         String value = headerString.substring(idx + 1, headerString.length()).trim();
       
   208 
       
   209         privateMap.computeIfAbsent(name.toLowerCase(Locale.US),
       
   210                                    k -> new ArrayList<>()).add(value);
       
   211     }
       
   212 
       
   213     private void resumeOrLF(ByteBuffer input) {
       
   214         assert state == State.HEADER_FOUND_CR || state == State.HEADER_FOUND_LF;
       
   215         char c = (char)input.get();
       
   216         if (c == LF && state == State.HEADER_FOUND_CR) {
       
   217             // header value will be flushed by
       
   218             // resumeOrSecondCR if next line does not
       
   219             // begin by SP or HT
       
   220             state = State.HEADER_FOUND_CR_LF;
       
   221         } else if (c == SP || c == HT) {
       
   222             sb.append(SP); // parity with MessageHeaders
       
   223             state = State.HEADER;
       
   224         } else {
       
   225             sb = new StringBuilder();
       
   226             sb.append(c);
       
   227             state = State.HEADER;
       
   228         }
       
   229     }
       
   230 
       
   231     private void resumeOrSecondCR(ByteBuffer input) {
       
   232         assert state == State.HEADER_FOUND_CR_LF;
       
   233         char c = (char)input.get();
       
   234         if (c == CR) {
       
   235             if (sb.length() > 0) {
       
   236                 // no continuation line - flush
       
   237                 // previous header value.
       
   238                 String headerString = sb.toString();
       
   239                 sb = new StringBuilder();
       
   240                 addHeaderFromString(headerString);
       
   241             }
       
   242             state = State.HEADER_FOUND_CR_LF_CR;
       
   243         } else if (c == SP || c == HT) {
       
   244             assert sb.length() != 0;
       
   245             sb.append(SP); // continuation line
       
   246             state = State.HEADER;
       
   247         } else {
       
   248             if (sb.length() > 0) {
       
   249                 // no continuation line - flush
       
   250                 // previous header value.
       
   251                 String headerString = sb.toString();
       
   252                 sb = new StringBuilder();
       
   253                 addHeaderFromString(headerString);
       
   254             }
       
   255             sb.append(c);
       
   256             state = State.HEADER;
       
   257         }
       
   258     }
       
   259 
       
   260     private void resumeOrEndHeaders(ByteBuffer input) throws ProtocolException {
       
   261         assert state == State.HEADER_FOUND_CR_LF_CR;
       
   262         char c = (char)input.get();
       
   263         if (c == LF) {
       
   264             state = State.FINISHED;
       
   265             headers = ImmutableHeaders.of(privateMap);
       
   266             privateMap = null;
       
   267         } else {
       
   268             throw protocolException("Unexpected \"%s\", after CR LF CR", c);
       
   269         }
       
   270     }
       
   271 
       
   272     private ProtocolException protocolException(String format, Object... args) {
       
   273         return new ProtocolException(format(format, args));
       
   274     }
       
   275 }