/* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved. * * The contents of this file are subject to the terms of either the GNU * General Public License Version 2 only ("GPL") or the Common Development * and Distribution License("CDDL") (collectively, the "License"). You * may not use this file except in compliance with the License. You can * obtain a copy of the License at * http://glassfish.java.net/public/CDDL+GPL_1_1.html * or packager/legal/LICENSE.txt. See the License for the specific * language governing permissions and limitations under the License. * * When distributing the software, include this License Header Notice in each * file and include the License file at packager/legal/LICENSE.txt. * * GPL Classpath Exception: * Oracle designates this particular file as subject to the "Classpath" * exception as provided by Oracle in the GPL Version 2 section of the License * file that accompanied this code. * * Modifications: * If applicable, add the following below the License Header, with the fields * enclosed by brackets [] replaced by your own identifying information: * "Portions Copyright [year] [name of copyright owner]" * * Contributor(s): * If you wish your version of this file to be governed by only the CDDL or * only the GPL Version 2, indicate your decision by adding "[Contributor] * elects to include this software in this distribution under the [CDDL or GPL * Version 2] license." If you don't indicate a single choice of license, a * recipient has the option to distribute your version of this file under * either the CDDL, the GPL Version 2 or to extend the choice of license to * its licensees as provided above. However, if you add GPL Version 2 code * and therefore, elected the GPL Version 2 license, then the option applies * only if the new code is made subject to such option by the copyright * holder. */ package org.glassfish.jersey.jdk.connector; import java.io.UnsupportedEncodingException; import java.nio.ByteBuffer; import java.util.List; import javax.ws.rs.core.HttpHeaders; /** * @author Alexey Stashok * @author Petr Janouch (petr.janouch at oracle.com) */ 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; buffer.clear(); 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 - input.position() : headerParsingState.start; headerParsingState.offset = headerParsingState.offset > 0 ? headerParsingState.offset - input.position() : headerParsingState.offset; headerParsingState.packetLimit = headerParsingState.packetLimit > 0 ? headerParsingState.packetLimit - input.position() : headerParsingState.packetLimit; headerParsingState.checkpoint = headerParsingState.checkpoint > 0 ? headerParsingState.checkpoint - input.position() : headerParsingState.checkpoint; headerParsingState.checkpoint2 = headerParsingState.checkpoint2 > 0 ? headerParsingState.checkpoint2 - input.position() : headerParsingState.checkpoint2; if (input.hasRemaining()) { if (input != buffer) { buffer.clear(); buffer.flip(); buffer = Utils.appendBuffers(buffer, input, bufferMaxSize, BUFFER_STEP_SIZE); } else { buffer.compact(); 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 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 = 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 = input.limit(); return false; } headerParsingState.start = nonSpaceIdx; headerParsingState.offset = nonSpaceIdx + 1; headerParsingState.subState++; break; } case 2: { // parse the status code if (headerParsingState.offset + 3 > 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 = input.limit(); return false; } headerParsingState.start = nonSpaceIdx; headerParsingState.offset = nonSpaceIdx; headerParsingState.subState++; break; } case 4: { // HTTP response reason-phrase if (!findEOL(input)) { headerParsingState.offset = 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; 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 = 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(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(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); } 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 { int bodyLength = Integer.parseInt(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; } // 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(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(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 = 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]; 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); } }