/*
* 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.nio.ByteBuffer;
import static org.glassfish.jersey.jdk.connector.HttpParserUtils.isSpaceOrTab;
import static org.glassfish.jersey.jdk.connector.HttpParserUtils.skipSpaces;
/**
* @author Alexey Stashok
* @author Petr Janouch (petr.janouch at oracle.com)
*/
abstract class TransferEncodingParser {
abstract boolean parse(ByteBuffer input) throws ParseException;
static TransferEncodingParser createFixedLengthParser(AsynchronousBodyInputStream responseBody, int expectedLength) {
return new FixedLengthEncodingParser(responseBody, expectedLength);
}
static TransferEncodingParser createChunkParser(AsynchronousBodyInputStream responseBody,
HttpParser httpParser, int maxHeadersSize) {
return new ChunkedEncodingParser(responseBody, httpParser, maxHeadersSize);
}
private static class FixedLengthEncodingParser extends TransferEncodingParser {
private final int expectedLength;
private final AsynchronousBodyInputStream responseBody;
private volatile int consumedLength = 0;
FixedLengthEncodingParser(AsynchronousBodyInputStream responseBody, int expectedLength) {
this.expectedLength = expectedLength;
this.responseBody = responseBody;
}
@Override
boolean parse(ByteBuffer input) throws ParseException {
if (input.remaining() + consumedLength > expectedLength) {
throw new ParseException(LocalizationMessages.HTTP_BODY_SIZE_OVERFLOW());
}
byte[] data = new byte[input.remaining()];
input.get(data);
ByteBuffer parsed = ByteBuffer.wrap(data);
responseBody.notifyDataAvailable(parsed);
consumedLength += data.length;
return consumedLength == expectedLength;
}
}
private static class ChunkedEncodingParser extends TransferEncodingParser {
private static final int MAX_HTTP_CHUNK_SIZE_LENGTH = 16;
private static final long CHUNK_SIZE_OVERFLOW = Long.MAX_VALUE >> 4;
private static final int CHUNK_LENGTH_PARSED_STATE = 3;
private static final int[] DEC = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
00, 01, 02, 03, 04, 05, 06, 07, 8, 9, -1, -1, -1, -1, -1, -1,
-1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, 10, 11, 12, 13, 14, 15, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
};
private final HttpParserUtils.ContentParsingState contentParsingState = new HttpParserUtils
.ContentParsingState();
private final HttpParserUtils.HeaderParsingState headerParsingState;
private final AsynchronousBodyInputStream responseBody;
private final HttpParser httpParser;
private final int maxHeadersSize;
ChunkedEncodingParser(AsynchronousBodyInputStream responseBody, HttpParser httpParser, int maxHeadersSize) {
this.responseBody = responseBody;
this.httpParser = httpParser;
this.headerParsingState = httpParser.getHeaderParsingState();
this.maxHeadersSize = maxHeadersSize;
}
// Taken with small modifications from Grizzly ChunkedTransferEncoding.parsePacket
@Override
boolean parse(ByteBuffer input) throws ParseException {
while (input.hasRemaining()) {
boolean isLastChunk = contentParsingState.isLastChunk;
// Check if HTTP chunk length was parsed
if (!isLastChunk && contentParsingState.chunkRemainder <= 0) {
if (!parseTrailerCRLF(input)) {
return false;
}
if (!parseHttpChunkLength(input)) {
// if not a HEAD request and we don't have enough data to
// parse chunk length - shutdownNow execution
return false;
}
} else {
// HTTP content starts from position 0 in the input Buffer (HTTP chunk header is not part of the input Buffer)
//contentParsingState.chunkContentStart = 0;
contentParsingState.chunkContentStart = input.position();
}
// Get the position in the input Buffer, where actual HTTP content starts
int chunkContentStart = contentParsingState.chunkContentStart;
if (contentParsingState.chunkLength == 0) {
// if it's the last HTTP chunk
if (!isLastChunk) {
// set it's the last chunk
contentParsingState.isLastChunk = true;
isLastChunk = true;
// start trailer parsing
initTrailerParsing();
}
// Check if trailer is present
if (!parseLastChunkTrailer(input)) {
// if yes - and there is not enough input data - shutdownNow the
// filterchain processing
return false;
}
// move the content start position after trailer parsing
chunkContentStart = headerParsingState.offset;
}
if (isLastChunk) {
input.position(chunkContentStart);
return true;
}
// Get the number of bytes remaining in the current chunk
final long thisPacketRemaining = contentParsingState.chunkRemainder;
// Get the number of content bytes available in the current input Buffer
final int contentAvailable = input.limit() - chunkContentStart;
input.position(chunkContentStart);
ByteBuffer data;
if (contentAvailable > thisPacketRemaining) {
// If input Buffer has part of the next message - slice it
data = Utils.split(input, (int) (chunkContentStart + thisPacketRemaining));
} else {
data = Utils.split(input, chunkContentStart + input.remaining());
}
contentParsingState.chunkRemainder -= data.remaining();
responseBody.notifyDataAvailable(data);
}
return false;
}
// Taken with small modifications from Grizzly ChunkedTransferEncoding.parseHttpChunkLength
private boolean parseHttpChunkLength(final ByteBuffer input) throws ParseException {
while (true) {
switch (headerParsingState.state) {
case 0: {// Initialize chunk parsing
final int pos = input.position();
headerParsingState.start = pos;
headerParsingState.offset = pos;
headerParsingState.packetLimit = pos + MAX_HTTP_CHUNK_SIZE_LENGTH;
headerParsingState.state = 1;
break;
}
case 1: { // Skip heading spaces (it's not allowed by the spec, but some servers put it there)
final int nonSpaceIdx = skipSpaces(input,
headerParsingState.offset, headerParsingState.packetLimit);
if (nonSpaceIdx == -1) {
headerParsingState.offset = input.limit();
headerParsingState.state = 1;
headerParsingState.checkOverflow(LocalizationMessages.HTTP_CHUNK_ENCODING_PREFIX_OVERFLOW());
return false;
}
headerParsingState.offset = nonSpaceIdx;
headerParsingState.state = 2;
break;
}
case 2: { // Scan chunk size
int offset = headerParsingState.offset;
int limit = Math.min(headerParsingState.packetLimit, input.limit());
long value = headerParsingState.parsingNumericValue;
while (offset < limit) {
final byte b = input.get(offset);
if (isSpaceOrTab(b) || /*trailing spaces are not allowed by the spec, but some server put it there*/
b == HttpParserUtils.CR || b == HttpParserUtils.SEMI_COLON) {
headerParsingState.checkpoint = offset;
} else if (b == HttpParserUtils.LF) {
contentParsingState.chunkContentStart = offset + 1;
contentParsingState.chunkLength = value;
contentParsingState.chunkRemainder = value;
headerParsingState.state = CHUNK_LENGTH_PARSED_STATE;
return true;
} else if (headerParsingState.checkpoint == -1) {
if (DEC[b & 0xFF] != -1 && checkOverflow(value)) {
value = (value << 4) + (DEC[b & 0xFF]);
} else {
throw new ParseException(
LocalizationMessages.HTTP_INVALID_CHUNK_SIZE_HEX_VALUE(b));
}
} else {
throw new ParseException(LocalizationMessages.HTTP_UNEXPECTED_CHUNK_HEADER());
}
offset++;
}
headerParsingState.parsingNumericValue = value;
headerParsingState.offset = offset;
headerParsingState.checkOverflow(LocalizationMessages.HTTP_CHUNK_ENCODING_PREFIX_OVERFLOW());
return false;
}
}
}
}
// Taken with small modifications from Grizzly ChunkedTransferEncoding.parseTrailerCRLF
private boolean parseTrailerCRLF(ByteBuffer input) {
if (headerParsingState.state == CHUNK_LENGTH_PARSED_STATE) {
while (input.hasRemaining()) {
if (input.get() == HttpParserUtils.LF) {
headerParsingState.recycle();
return input.hasRemaining();
}
}
return false;
}
return true;
}
/**
* @return <tt>false</tt> if next left bit-shift by 4 bits will cause overflow,
* or <tt>true</tt> otherwise
*/
private boolean checkOverflow(final long chunkLength) {
return chunkLength <= CHUNK_SIZE_OVERFLOW;
}
private void initTrailerParsing() {
headerParsingState.subState = 0;
final int start = contentParsingState.chunkContentStart;
headerParsingState.start = start;
headerParsingState.offset = start;
headerParsingState.packetLimit = start + maxHeadersSize;
}
// Taken with small modifications from Grizzly ChunkedTransferEncoding.parseLastChunkTrailer
private boolean parseLastChunkTrailer(final ByteBuffer input) throws ParseException {
boolean result = httpParser.parseHeadersFromBuffer(input, true);
if (!result) {
headerParsingState.checkOverflow(LocalizationMessages.HTTP_TRAILER_HEADER_OVERFLOW());
}
return result;
}
}
}