ResponseParser.java

/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2025 Red Hat, Inc., and individual contributors
 * as indicated by the @author tags.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package io.undertow.client.http;

import io.undertow.UndertowLogger;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import io.undertow.util.Protocols;

import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;

import static io.undertow.client.http.ResponseState.FIELD_NAME;
import static io.undertow.client.http.ResponseState.FIELD_VALUE;
import static io.undertow.client.http.ResponseState.MESSAGE_BODY;
import static io.undertow.client.http.ResponseState.REASON_PHRASE;
import static io.undertow.client.http.ResponseState.STATUS_CODE;
import static io.undertow.client.http.ResponseState.VERSION;
import static io.undertow.util.ParserUtils.COLON;
import static io.undertow.util.ParserUtils.CARRIAGE_RETURN;
import static io.undertow.util.ParserUtils.LINE_FEED;
import static io.undertow.util.ParserUtils.SPACE;
import static io.undertow.util.ParserUtils.getProtocolLength;
import static io.undertow.util.ParserUtils.isDigitChar;
import static io.undertow.util.ParserUtils.isObsoleteChar;
import static io.undertow.util.ParserUtils.isProtocolChar;
import static io.undertow.util.ParserUtils.isSpaceOrTabChar;
import static io.undertow.util.ParserUtils.isTokenChar;
import static io.undertow.util.ParserUtils.isVisibleAsciiChar;

/**
 * @author <a href="mailto:opalka.richard@gmail.com">Richard Opalka</a>
 */
final class ResponseParser {

    static final ResponseParser INSTANCE = new ResponseParser();

    void handle(final ByteBuffer buffer, final ResponseState state, final HttpResponseBuilder builder) {
        parseVersion(buffer, state, builder);
        parseStatusCode(buffer, state, builder);
        parseReasonPhrase(buffer, state, builder);
        while (buffer.hasRemaining() && !state.isComplete()) {
            parseFieldName(buffer, state, builder);
            parseFieldValue(buffer, state, builder);
        }
    }

    private void parseVersion(final ByteBuffer buffer, final ResponseState state, final HttpResponseBuilder builder) {
        if (state.state != VERSION || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (isProtocolChar(nextByte, sb.length())) {
                // http version data goes on
                sb.append(nextChar);
            } else if (nextByte == SPACE && sb.length() == getProtocolLength()) {
                // http version read complete
                final String version = sb.toString();
                if (Protocols.HTTP_0_9.toString().equals(version)) builder.setProtocol(Protocols.HTTP_0_9);
                else if (Protocols.HTTP_1_0.toString().equals(version)) builder.setProtocol(Protocols.HTTP_1_0);
                else if (Protocols.HTTP_1_1.toString().equals(version)) builder.setProtocol(Protocols.HTTP_1_1);
                else if (Protocols.HTTP_2_0.toString().equals(version)) builder.setProtocol(Protocols.HTTP_2_0);
                else {
                    if (UndertowLogger.RESPONSE_LOGGER.isTraceEnabled()) {
                        UndertowLogger.RESPONSE_LOGGER.tracef("Unsupported HTTP version %s; falling back to HTTP 1.1", version);
                    }
                    builder.setProtocol(Protocols.HTTP_1_1);
                }
                // prepare for next parsing phase
                state.parsedData.setLength(0);
                state.state = STATUS_CODE;
                return;
            } else throw new HttpResponseParseException("Wrong protocol version");
        }
    }

    private void parseStatusCode(final ByteBuffer buffer, final ResponseState state, final HttpResponseBuilder builder) {
        if (state.state != STATUS_CODE || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (isDigitChar(nextByte) && sb.length() < 3) {
                // status code data goes on
                sb.append(nextChar);
            } else if (nextByte == SPACE && sb.length() == 3) {
                // status code data read complete
                final String code = sb.toString();
                builder.setStatusCode(Integer.parseInt(code));
                // prepare for next parsing phase
                state.parsedData.setLength(0);
                state.state = REASON_PHRASE;
                return;
            } else throw new HttpResponseParseException("Wrong status code");
        }
    }

    private void parseReasonPhrase(final ByteBuffer buffer, final ResponseState state, final HttpResponseBuilder builder) {
        if (state.state != REASON_PHRASE || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (state.previousByte == 0 && (isVisibleAsciiChar(nextByte) || isSpaceOrTabChar(nextByte) || isObsoleteChar(nextByte))) {
                // reason phrase data goes on
                sb.append(nextChar);
            } else if (state.previousByte == 0 && nextByte == CARRIAGE_RETURN) {
                // reason phrase read complete - first separator
                state.previousByte = nextByte;
            } else if (state.previousByte == CARRIAGE_RETURN && nextByte == LINE_FEED) {
                // reason phrase read complete - second separator
                final String reason = sb.toString().trim();
                if (!reason.isEmpty()) builder.setReasonPhrase(reason);
                // prepare for next parsing phase
                state.previousByte = 0;
                state.parsedData.setLength(0);
                state.state = FIELD_NAME;
                return;
            } else throw new HttpResponseParseException("Wrong reason phrase");
        }
    }

    private void parseFieldName(final ByteBuffer buffer, final ResponseState state, final HttpResponseBuilder builder) {
        if (state.state != FIELD_NAME || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (state.previousByte == 0 && isTokenChar(nextByte)) {
                // field name data goes on
                sb.append(nextChar);
            } else if (state.previousByte == 0 && nextByte == COLON && sb.length() != 0) {
                // field name read complete
                final String headerName = sb.toString();
                state.headerName = Headers.fromCache(headerName);
                if (state.headerName == null) state.headerName = new HttpString(headerName);
                // prepare for next parsing phase
                state.parsedData.setLength(0);
                state.state = FIELD_VALUE;
                return;
            } else if (state.previousByte == 0 && nextByte == CARRIAGE_RETURN && sb.length() == 0) {
                // special case - no headers available
                state.previousByte = nextByte;
            } else if (state.previousByte == CARRIAGE_RETURN && nextByte == LINE_FEED && sb.length() == 0) {
                // prepare for next parsing phase
                state.previousByte = 0;
                state.state = MESSAGE_BODY;
                return;
            } else throw new HttpResponseParseException("Wrong header name");
        }
    }

    private void parseFieldValue(final ByteBuffer buffer, final ResponseState state, final HttpResponseBuilder builder) {
        if (state.state != FIELD_VALUE || !buffer.hasRemaining()) return;

        final StringBuilder sb = state.parsedData;
        byte nextByte;
        char nextChar;

        while (buffer.hasRemaining()) {
            nextByte = buffer.get();
            nextChar = (char) (nextByte & 0xFF);

            if (state.previousByte == 0 && sb.length() == 0 && isSpaceOrTabChar(nextByte)) {
                // heading OWS
            } else if (state.previousByte == 0 && (isVisibleAsciiChar(nextByte) || isObsoleteChar(nextByte))) {
                // field-vchar
                sb.append(nextChar);
            } else if (state.previousByte == 0 && isSpaceOrTabChar(nextByte)) {
                // field-vchar RWS or OWS or obs-fold
                state.previousByte = nextByte;
            } else if (isSpaceOrTabChar(state.previousByte) && (isVisibleAsciiChar(nextByte) || isObsoleteChar(nextByte))) {
                // field-vchar RWS field-vchar
                sb.append((char) (state.previousByte & 0xFF));
                sb.append(nextChar);
                state.previousByte = 0;
            } else if (isSpaceOrTabChar(state.previousByte) && isSpaceOrTabChar(nextByte)) {
                // field-vchar OWS or obs-fold
            } else if (isSpaceOrTabChar(state.previousByte) && nextByte == CARRIAGE_RETURN) {
                // field-vchar OWS or obs-fold
                state.previousByte = nextByte;
            } else if (state.previousByte == 0 && nextByte == CARRIAGE_RETURN) {
                // CRLF or obs-fold
                state.previousByte = nextByte;
            } else if (state.previousByte == CARRIAGE_RETURN && nextByte == LINE_FEED) {
                // CRLF or obs-fold
                state.previousByte = nextByte;
            } else if (state.previousByte == LINE_FEED && isSpaceOrTabChar(nextByte)) {
                // obs-fold
                if (sb.length() > 0) {
                    byte[] bytes = sb.toString().getBytes(StandardCharsets.US_ASCII);
                    if (!isSpaceOrTabChar(bytes[bytes.length - 1])) sb.append((char)SPACE);
                }
                state.previousByte = 0;
            } else if (state.previousByte == LINE_FEED && isTokenChar(nextByte)) {
                // CRLF header
                final String headerValue = sb.toString().trim();
                builder.getResponseHeaders().add(state.headerName, headerValue);
                // prepare for next parsing phase
                sb.setLength(0);
                sb.append(nextChar);
                state.headerName = null;
                state.previousByte = 0;
                state.state = FIELD_NAME;
                return;
            } else if (state.previousByte == LINE_FEED && (nextByte == CARRIAGE_RETURN)) {
                // CRLF CRLF
                final String headerValue = sb.toString().trim();
                builder.getResponseHeaders().add(state.headerName, headerValue);
                // prepare for next parsing phase
                sb.setLength(0);
                state.headerName = null;
                state.previousByte = nextByte;
                state.state = FIELD_NAME;
                return;
            } else throw new HttpResponseParseException("Wrong header value");
        }
    }}