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 }