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);
    }
}