HttpParser.java
/*
* Copyright (c) 2015, 2024 Oracle and/or its affiliates. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0, which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the
* Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
* version 2 with the GNU Classpath Exception, which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
*/
package org.glassfish.jersey.jdk.connector.internal;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.Buffer;
import java.util.List;
import javax.ws.rs.core.HttpHeaders;
/**
* @author Alexey Stashok
* @author Petr Janouch
*/
class HttpParser {
private static final String ENCODING = "ISO-8859-1";
private static final int BUFFER_STEP_SIZE = 256;
// this is package private because of the test
static final int INIT_BUFFER_SIZE = 1024;
private final HttpParserUtils.HeaderParsingState headerParsingState;
private final int bufferMaxSize;
private final int maxHeaderSize;
private volatile ByteBuffer buffer = ByteBuffer.allocate(INIT_BUFFER_SIZE);
private volatile boolean headerParsed;
private volatile boolean expectContent;
private volatile String protocolVersion;
private volatile int code;
private volatile HttpResponse httpResponse;
private volatile TransferEncodingParser transferEncodingParser;
private volatile boolean complete;
HttpParser(int maxHeaderSize, int bufferMaxSize) {
headerParsingState = new HttpParserUtils.HeaderParsingState(maxHeaderSize);
this.bufferMaxSize = bufferMaxSize;
this.maxHeaderSize = maxHeaderSize;
}
void reset(boolean expectContent) {
this.expectContent = expectContent;
headerParsed = false;
// see https://jira.mongodb.org/browse/JAVA-2559 for the casts
((Buffer) buffer).clear();
((Buffer) buffer).flip();
complete = false;
headerParsingState.recycle();
}
boolean isHeaderParsed() {
return headerParsed;
}
boolean isComplete() {
return complete;
}
HttpResponse getHttpResponse() {
return httpResponse;
}
void parse(ByteBuffer input) throws ParseException {
if (buffer.remaining() > 0) {
input = Utils.appendBuffers(buffer, input, bufferMaxSize, BUFFER_STEP_SIZE);
}
if (!headerParsed && !parseHeader(input)) {
saveRemaining(input);
return;
}
httpResponse.setHasContent(expectContent);
if (expectContent) {
if (transferEncodingParser.parse(input)) {
complete = true;
} else {
saveRemaining(input);
}
} else {
// We don't expect content
complete = true;
}
if (complete && input.hasRemaining()) {
throw new ParseException(LocalizationMessages.UNEXPECTED_DATA_IN_BUFFER());
}
if (complete) {
httpResponse.getBodyStream().notifyAllDataRead();
}
}
private void saveRemaining(ByteBuffer input) {
// some of the fields use 0 nad -1 as a special state -> if the field is <= 0, just let it be
headerParsingState.start = headerParsingState.start > 0
? headerParsingState.start - ((Buffer) input).position() : headerParsingState.start;
headerParsingState.offset = headerParsingState.offset > 0
? headerParsingState.offset - ((Buffer) input).position() : headerParsingState.offset;
headerParsingState.packetLimit = headerParsingState.packetLimit > 0
? headerParsingState.packetLimit - ((Buffer) input).position() : headerParsingState.packetLimit;
headerParsingState.checkpoint = headerParsingState.checkpoint > 0
? headerParsingState.checkpoint - ((Buffer) input).position() : headerParsingState.checkpoint;
headerParsingState.checkpoint2 = headerParsingState.checkpoint2 > 0
? headerParsingState.checkpoint2 - ((Buffer) input).position() : headerParsingState.checkpoint2;
if (input.hasRemaining()) {
if (input != buffer) {
((Buffer) buffer).clear();
((Buffer) buffer).flip();
buffer = Utils.appendBuffers(buffer, input, bufferMaxSize, BUFFER_STEP_SIZE);
} else {
buffer.compact();
((Buffer) buffer).flip();
}
}
}
// Taken with small modifications from Grizzly HttpCodecFilter.parseHeaderFromBuffer
// (change: operations in phase 2 are translated to fit this parser)
private boolean parseHeader(ByteBuffer input) throws ParseException {
while (true) {
switch (headerParsingState.state) {
case 0: { // parsing initial line
if (!decodeInitialLineFromBuffer(input)) {
headerParsingState.checkOverflow(LocalizationMessages.HTTP_INITIAL_LINE_OVERFLOW());
return false;
}
headerParsingState.state++;
break;
}
case 1: { // parsing headers
if (!parseHeadersFromBuffer(input, false)) {
headerParsingState.checkOverflow(LocalizationMessages.HTTP_PACKET_HEADER_OVERFLOW());
return false;
}
headerParsingState.state++;
break;
}
case 2: { // Headers are ready
((Buffer) input).position(headerParsingState.offset);
// if headers get parsed - set the flag
headerParsed = true;
decideTransferEncoding();
// recycle header parsing state
headerParsingState.recycle();
return true;
}
default:
throw new IllegalStateException();
}
}
}
// Taken unmodified from Grizzly HttpClientFilter.decodeInitialLineFromBuffer
private boolean decodeInitialLineFromBuffer(final ByteBuffer input) throws ParseException {
final int packetLimit = headerParsingState.packetLimit;
//noinspection LoopStatementThatDoesntLoop
while (true) {
int subState = headerParsingState.subState;
switch (subState) {
case 0: { // HTTP protocol
final int spaceIdx =
findSpace(input, headerParsingState.offset, packetLimit);
if (spaceIdx == -1) {
headerParsingState.offset = ((Buffer) input).limit();
return false;
}
protocolVersion = parseString(input, headerParsingState.start, spaceIdx);
headerParsingState.start = -1;
headerParsingState.offset = spaceIdx;
headerParsingState.subState++;
break;
}
case 1: { // skip spaces after the HTTP protocol
final int nonSpaceIdx =
HttpParserUtils.skipSpaces(input, headerParsingState.offset, packetLimit);
if (nonSpaceIdx == -1) {
headerParsingState.offset = ((Buffer) input).limit();
return false;
}
headerParsingState.start = nonSpaceIdx;
headerParsingState.offset = nonSpaceIdx + 1;
headerParsingState.subState++;
break;
}
case 2: { // parse the status code
if (headerParsingState.offset + 3 > ((Buffer) input).limit()) {
return false;
}
code = parseInt(input, headerParsingState.start, headerParsingState.start + 3);
headerParsingState.start = -1;
headerParsingState.offset += 3;
headerParsingState.subState++;
break;
}
case 3: { // skip spaces after the status code
final int nonSpaceIdx =
HttpParserUtils.skipSpaces(input, headerParsingState.offset, packetLimit);
if (nonSpaceIdx == -1) {
headerParsingState.offset = ((Buffer) input).limit();
return false;
}
headerParsingState.start = nonSpaceIdx;
headerParsingState.offset = nonSpaceIdx;
headerParsingState.subState++;
break;
}
case 4: { // HTTP response reason-phrase
if (!findEOL(input)) {
headerParsingState.offset = ((Buffer) input).limit();
return false;
}
String reasonPhrase = parseString(input, headerParsingState.start, headerParsingState.checkpoint);
headerParsingState.subState = 0;
headerParsingState.start = -1;
headerParsingState.checkpoint = -1;
httpResponse = new HttpResponse(protocolVersion, code, reasonPhrase);
if (httpResponse.getStatusCode() == 100) {
// reset the parsing state in preparation to parse
// another initial line which represents the final
// response from the server after it has sent a
// 100-Continue.
headerParsingState.offset += 2;
headerParsingState.start = 0;
((Buffer) input).position(headerParsingState.offset);
input.compact();
headerParsingState.offset = 0;
return false;
}
return true;
}
default:
throw new IllegalStateException();
}
}
}
// Taken unmodified from Grizzly from HttpCodecFilter.parseHeadersFromBuffer
boolean parseHeadersFromBuffer(final ByteBuffer input, boolean parsingTrailerHeaders) throws ParseException {
do {
if (headerParsingState.subState == 0) {
final int eol = checkEOL(input);
if (eol == 0) { // EOL
return true;
} else if (eol == -2) { // not enough data
return false;
}
}
if (!parseHeaderFromBuffer(input, parsingTrailerHeaders)) {
return false;
}
} while (true);
}
// Taken unmodified from Grizzly HttpCodecFilter.parseHeaderFromBuffer
private boolean parseHeaderFromBuffer(final ByteBuffer input, boolean parsingTrailerHeaders) throws ParseException {
while (true) {
final int subState = headerParsingState.subState;
switch (subState) {
case 0: { // start to parse the header
headerParsingState.start = headerParsingState.offset;
headerParsingState.subState++;
break;
}
case 1: { // parse header name
if (!parseHeaderName(input)) {
return false;
}
headerParsingState.subState++;
headerParsingState.start = -1;
break;
}
case 2: { // skip value preceding spaces
final int nonSpaceIdx = HttpParserUtils
.skipSpaces(input, headerParsingState.offset, headerParsingState.packetLimit);
if (nonSpaceIdx == -1) {
headerParsingState.offset = ((Buffer) input).limit();
return false;
}
headerParsingState.subState++;
headerParsingState.offset = nonSpaceIdx;
if (headerParsingState.start == -1) {
// Starting to parse header (will be called only for the first line of the multi line header)
headerParsingState.start = nonSpaceIdx;
headerParsingState.checkpoint = nonSpaceIdx;
headerParsingState.checkpoint2 = nonSpaceIdx;
}
break;
}
case 3: { // parse header value
final int result = parseHeaderValue(input, parsingTrailerHeaders);
if (result == -1) {
return false;
} else if (result == -2) {
// Multiline header detected. Skip preceding spaces
headerParsingState.subState = 2;
break;
}
headerParsingState.subState = 0;
headerParsingState.start = -1;
return true;
}
default:
throw new IllegalStateException();
}
}
}
// Taken with small modifications from Grizzly HttpCodecFilter.parseHeaderName
// (change: Grizzly also initializes value store)
private boolean parseHeaderName(final ByteBuffer input) throws ParseException {
final int limit = Math.min(((Buffer) input).limit(), headerParsingState.packetLimit);
final int start = headerParsingState.start;
int offset = headerParsingState.offset;
while (offset < limit) {
byte b = input.get(offset);
if (b == HttpParserUtils.COLON) {
headerParsingState.headerName = parseString(input, start, offset);
headerParsingState.offset = offset + 1;
return true;
} else if ((b >= HttpParserUtils.A) && (b <= HttpParserUtils.Z)) {
b -= HttpParserUtils.LC_OFFSET;
input.put(offset, b);
}
offset++;
}
headerParsingState.offset = offset;
return false;
}
// Taken with small modifications from Grizzly HttpCodecFilter.parseHeaderValue
// (change: Grizzly saves teh value as a buffer, we split it and add to response)
private int parseHeaderValue(ByteBuffer input, boolean parsingTrailerHeaders) throws ParseException {
final int limit = Math.min(((Buffer) input).limit(), headerParsingState.packetLimit);
int offset = headerParsingState.offset;
final boolean hasShift = (offset != headerParsingState.checkpoint);
while (offset < limit) {
final byte b = input.get(offset);
/* This if is not in Grizzly, it is used for parsing comma separated values.
Grizzly separates the header in Header class. */
if (b == HttpParserUtils.COMMA && !isInseparableHeader()) {
headerParsingState.offset = offset + 1;
String value = parseString(input,
headerParsingState.start, headerParsingState.checkpoint2);
httpResponse.addHeader(headerParsingState.headerName, value);
headerParsingState.start = headerParsingState.checkpoint2;
return -2;
}
if (b == HttpParserUtils.CR) {
// do nothing
} else if (b == HttpParserUtils.LF) {
// Check if it's not multi line header
if (offset + 1 < limit) {
final byte b2 = input.get(offset + 1);
if (b2 == HttpParserUtils.SP || b2 == HttpParserUtils.HT) {
input.put(headerParsingState.checkpoint++, b2);
headerParsingState.offset = offset + 2;
return -2;
} else {
headerParsingState.offset = offset + 1;
String value = parseString(input,
headerParsingState.start, headerParsingState.checkpoint2);
if (parsingTrailerHeaders) {
httpResponse.addTrailerHeader(headerParsingState.headerName, value);
} else {
httpResponse.addHeader(headerParsingState.headerName, value);
}
return 0;
}
}
headerParsingState.offset = offset;
return -1;
} else if (b == HttpParserUtils.SP) {
if (hasShift) {
input.put(headerParsingState.checkpoint++, b);
} else {
headerParsingState.checkpoint++;
}
} else {
if (hasShift) {
input.put(headerParsingState.checkpoint++, b);
} else {
headerParsingState.checkpoint++;
}
headerParsingState.checkpoint2 = headerParsingState.checkpoint;
}
offset++;
}
headerParsingState.offset = offset;
return -1;
}
private boolean isInseparableHeader() {
/* Authenticate headers contain comma separated list of properties, which would be normally treated as separate header
values */
return Constants.WWW_AUTHENTICATE.equalsIgnoreCase(headerParsingState.headerName)
|| Constants.PROXY_AUTHENTICATE.equalsIgnoreCase(headerParsingState.headerName)
|| HttpHeaders.SET_COOKIE.equalsIgnoreCase(headerParsingState.headerName);
}
private void decideTransferEncoding() throws ParseException {
int statusCode = httpResponse.getStatusCode();
if (statusCode == 204 || statusCode == 205 || statusCode == 304) {
expectContent = false;
}
if (httpResponse.getHeaders().size() == 0) {
expectContent = false;
}
List<String> transferEncodings = httpResponse.getHeader(Constants.TRANSFER_ENCODING_HEADER);
if (transferEncodings != null) {
String transferEncoding = transferEncodings.get(0);
if (Constants.TRANSFER_ENCODING_CHUNKED.equalsIgnoreCase(transferEncoding)) {
transferEncodingParser = TransferEncodingParser
.createChunkParser(httpResponse.getBodyStream(), this, maxHeaderSize);
}
return;
}
List<String> contentLengths = httpResponse.getHeader(HttpHeaders.CONTENT_LENGTH);
if (contentLengths != null) {
try {
long bodyLength = Long.parseLong(contentLengths.get(0));
if (bodyLength == 0) {
expectContent = false;
return;
}
if (bodyLength <= 0) {
throw new ParseException(LocalizationMessages.HTTP_NEGATIVE_CONTENT_LENGTH());
}
transferEncodingParser = TransferEncodingParser
.createFixedLengthParser(httpResponse.getBodyStream(), bodyLength);
} catch (NumberFormatException e) {
throw new ParseException(LocalizationMessages.HTTP_INVALID_CONTENT_LENGTH());
}
return;
} else if (httpResponse.getHasContent()) {
// missing Content-Length
transferEncodingParser = TransferEncodingParser
.createFixedLengthParser(httpResponse.getBodyStream(), Long.MAX_VALUE);
return;
}
// TODO what now? Expect no content or fail loudly?
}
// Taken unmodified from Grizzly HttpCodecUtils.findSpace
private int findSpace(final ByteBuffer input, int offset, final int packetLimit) {
final int limit = Math.min(((Buffer) input).limit(), packetLimit);
while (offset < limit) {
final byte b = input.get(offset);
if (HttpParserUtils.isSpaceOrTab(b)) {
return offset;
}
offset++;
}
return -1;
}
// Taken unmodified from Grizzly HttpCodecUtils.findEOL
private boolean findEOL(final ByteBuffer input) {
int offset = headerParsingState.offset;
final int limit = Math.min(((Buffer) input).limit(), headerParsingState.packetLimit);
while (offset < limit) {
final byte b = input.get(offset);
if (b == HttpParserUtils.CR) {
headerParsingState.checkpoint = offset;
} else if (b == HttpParserUtils.LF) {
if (headerParsingState.checkpoint == -1) {
headerParsingState.checkpoint = offset;
}
headerParsingState.offset = offset + 1;
return true;
}
offset++;
}
headerParsingState.offset = offset;
return false;
}
// Taken unmodified from Grizzly HttpCodecUtils.checkEOL
private int checkEOL(final ByteBuffer input) {
final int offset = headerParsingState.offset;
final int avail = ((Buffer) input).limit() - offset;
final byte b1;
final byte b2;
if (avail >= 2) { // if more than 2 bytes available
final short s = input.getShort(offset);
b1 = (byte) (s >>> 8);
b2 = (byte) (s & 0xFF);
} else if (avail == 1) { // if one byte available
b1 = input.get(offset);
b2 = -1;
} else {
return -2;
}
return checkCRLF(b1, b2);
}
// Taken unmodified from Grizzly HttpCodecUtils.checkCRLF
private int checkCRLF(byte b1, byte b2) {
if (b1 == HttpParserUtils.CR) {
if (b2 == HttpParserUtils.LF) {
headerParsingState.offset += 2;
return 0;
} else if (b2 == -1) {
return -2;
}
} else if (b1 == HttpParserUtils.LF) {
headerParsingState.offset++;
return 0;
}
return -1;
}
HttpParserUtils.HeaderParsingState getHeaderParsingState() {
return headerParsingState;
}
private String parseString(ByteBuffer input, int startIdx, int endIdx) throws ParseException {
byte[] bytes = new byte[endIdx - startIdx];
((Buffer) input).position(startIdx);
input.get(bytes, 0, endIdx - startIdx);
try {
return new String(bytes, ENCODING);
} catch (UnsupportedEncodingException e) {
throw new ParseException("Unsupported encoding: " + ENCODING, e);
}
}
private int parseInt(ByteBuffer input, int startIdx, int endIdx) throws ParseException {
String value = parseString(input, startIdx, endIdx);
return Integer.valueOf(value);
}
}