com.firefly.codec.http2.decode.HttpParser Maven / Gradle / Ivy
package com.firefly.codec.http2.decode;
import static com.firefly.codec.http2.model.HttpTokens.CARRIAGE_RETURN;
import static com.firefly.codec.http2.model.HttpTokens.LINE_FEED;
import static com.firefly.codec.http2.model.HttpTokens.SPACE;
import static com.firefly.codec.http2.model.HttpTokens.TAB;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Locale;
import com.firefly.codec.http2.model.BadMessageException;
import com.firefly.codec.http2.model.HostPortHttpField;
import com.firefly.codec.http2.model.HttpCompliance;
import com.firefly.codec.http2.model.HttpField;
import com.firefly.codec.http2.model.HttpFields;
import com.firefly.codec.http2.model.HttpHeader;
import com.firefly.codec.http2.model.HttpHeaderValue;
import com.firefly.codec.http2.model.HttpMethod;
import com.firefly.codec.http2.model.HttpStatus;
import com.firefly.codec.http2.model.HttpTokens;
import com.firefly.codec.http2.model.HttpTokens.EndOfContent;
import com.firefly.codec.http2.model.HttpVersion;
import com.firefly.codec.http2.model.PreEncodedHttpField;
import com.firefly.utils.collection.ArrayTernaryTrie;
import com.firefly.utils.collection.ArrayTrie;
import com.firefly.utils.collection.Trie;
import com.firefly.utils.io.BufferUtils;
import com.firefly.utils.lang.TypeUtils;
import com.firefly.utils.lang.Utf8StringBuilder;
import com.firefly.utils.log.Log;
import com.firefly.utils.log.LogFactory;
/**
* A Parser for 1.0 and 1.1 as defined by RFC7230
*
* This parser parses HTTP client and server messages from buffers passed in the
* {@link #parseNext(ByteBuffer)} method. The parsed elements of the HTTP
* message are passed as event calls to the {@link HttpHandler} instance the
* parser is constructed with. If the passed handler is a {@link RequestHandler}
* then server side parsing is performed and if it is a {@link ResponseHandler},
* then client side parsing is done.
*
*
* The contract of the {@link HttpHandler} API is that if a call returns true
* then the call to {@link #parseNext(ByteBuffer)} will return as soon as
* possible also with a true response. Typically this indicates that the parsing
* has reached a stage where the caller should process the events accumulated by
* the handler. It is the preferred calling style that handling such as calling
* a servlet to process a request, should be done after a true return from
* {@link #parseNext(ByteBuffer)} rather than from within the scope of a call
* like {@link RequestHandler#messageComplete()}
*
*
* For performance, the parse is heavily dependent on the
* {@link Trie#getBest(ByteBuffer, int, int)} method to look ahead in a single
* pass for both the structure ( : and CRLF ) and semantic (which header and
* value) of a header. Specifically the static {@link HttpHeader#CACHE} is used
* to lookup common combinations of headers and values (eg. "Connection: close"
* ), or just header names (eg. "Connection:" ). For headers who's value is not
* known statically (eg. Host, COOKIE) then a per parser dynamic Trie of
* {@link HttpFields} from previous parsed messages is used to help the parsing
* of subsequent messages.
*
*
* The parser can work in varying compliance modes:
*
* - RFC7230
* - (default) Compliance with RFC7230
* - RFC2616
* - Wrapped headers and HTTP/0.9 supported
* - LEGACY
* - (aka STRICT) Adherence to Servlet Specification requirement for exact
* case of header names, bypassing the header caches, which are case
* insensitive, otherwise equivalent to RFC2616
*
*
* @see RFC 7230
*/
public class HttpParser {
private static Log log = LogFactory.getInstance().getLog("firefly-system");
public final static int INITIAL_URI_LENGTH = 256;
/**
* Cache of common {@link HttpField}s including:
*
* - Common static combinations such as:
*
* - Connection: close
*
- Accept-Encoding: gzip
*
- Content-Length: 0
*
* - Combinations of Content-Type header for common mime types by common
* charsets
*
- Most common headers with null values so that a lookup will at least
* determine the header name even if the name:value combination is not
* cached
*
*/
public final static Trie CACHE = new ArrayTrie<>(2048);
// States
public enum State
{
START,
METHOD,
RESPONSE_VERSION,
SPACE1,
STATUS,
URI,
SPACE2,
REQUEST_VERSION,
REASON,
PROXY,
HEADER,
HEADER_IN_NAME,
HEADER_VALUE,
HEADER_IN_VALUE,
CONTENT,
EOF_CONTENT,
CHUNKED_CONTENT,
CHUNK_SIZE,
CHUNK_PARAMS,
CHUNK,
CHUNK_END,
END,
CLOSE, // The associated stream/endpoint should be closed
CLOSED // The associated stream/endpoint is at EOF
}
private final static EnumSet __idleStates = EnumSet.of(State.START, State.END, State.CLOSE, State.CLOSED);
private final static EnumSet __completeStates = EnumSet.of(State.END, State.CLOSE, State.CLOSED);
private final boolean DEBUG = log.isDebugEnabled(); // Cache debug to help branch prediction
private final HttpHandler _handler;
private final RequestHandler _requestHandler;
private final ResponseHandler _responseHandler;
private final int _maxHeaderBytes;
private final HttpCompliance _compliance;
private HttpField _field;
private HttpHeader _header;
private String _headerString;
private HttpHeaderValue _value;
private String _valueString;
private int _responseStatus;
private int _headerBytes;
private boolean _host;
private volatile State _state = State.START;
private volatile boolean _eof;
private HttpMethod _method;
private String _methodString;
private HttpVersion _version;
private Utf8StringBuilder _uri = new Utf8StringBuilder(INITIAL_URI_LENGTH); // Tune?
private EndOfContent _endOfContent;
private long _contentLength;
private long _contentPosition;
private int _chunkLength;
private int _chunkPosition;
private boolean _headResponse;
private boolean _cr;
private ByteBuffer _contentChunk;
private Trie _connectionFields;
private int _length;
private final StringBuilder _string = new StringBuilder();
static {
CACHE.put(new HttpField(HttpHeader.CONNECTION, HttpHeaderValue.CLOSE));
CACHE.put(new HttpField(HttpHeader.CONNECTION, HttpHeaderValue.KEEP_ALIVE));
CACHE.put(new HttpField(HttpHeader.CONNECTION, HttpHeaderValue.UPGRADE));
CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip"));
CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip, deflate"));
CACHE.put(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip,deflate,sdch"));
CACHE.put(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-US,en;q=0.5"));
CACHE.put(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-GB,en-US;q=0.8,en;q=0.6"));
CACHE.put(new HttpField(HttpHeader.ACCEPT_CHARSET, "ISO-8859-1,utf-8;q=0.7,*;q=0.3"));
CACHE.put(new HttpField(HttpHeader.ACCEPT, "*/*"));
CACHE.put(new HttpField(HttpHeader.ACCEPT, "image/png,image/*;q=0.8,*/*;q=0.5"));
CACHE.put(new HttpField(HttpHeader.ACCEPT, "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"));
CACHE.put(new HttpField(HttpHeader.PRAGMA, "no-cache"));
CACHE.put(new HttpField(HttpHeader.CACHE_CONTROL, "private, no-cache, no-cache=Set-Cookie, proxy-revalidate"));
CACHE.put(new HttpField(HttpHeader.CACHE_CONTROL, "no-cache"));
CACHE.put(new HttpField(HttpHeader.CONTENT_LENGTH, "0"));
CACHE.put(new HttpField(HttpHeader.CONTENT_ENCODING, "gzip"));
CACHE.put(new HttpField(HttpHeader.CONTENT_ENCODING, "deflate"));
CACHE.put(new HttpField(HttpHeader.TRANSFER_ENCODING, "chunked"));
CACHE.put(new HttpField(HttpHeader.EXPIRES, "Fri, 01 Jan 1990 00:00:00 GMT"));
// Add common Content types as fields
for (String type : new String[] { "text/plain", "text/html", "text/xml", "text/json", "application/json",
"application/x-www-form-urlencoded" }) {
HttpField field = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type);
CACHE.put(field);
for (String charset : new String[] { "utf-8", "iso-8859-1" }) {
CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + ";charset=" + charset));
CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + "; charset=" + charset));
CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,
type + ";charset=" + charset.toUpperCase(Locale.ENGLISH)));
CACHE.put(new PreEncodedHttpField(HttpHeader.CONTENT_TYPE,
type + "; charset=" + charset.toUpperCase(Locale.ENGLISH)));
}
}
// Add headers with null values so HttpParser can avoid looking up name
// again for unknown values
for (HttpHeader h : HttpHeader.values())
if (!CACHE.put(new HttpField(h, (String) null)))
throw new IllegalStateException("CACHE FULL");
// Add some more common headers
CACHE.put(new HttpField(HttpHeader.REFERER, (String) null));
CACHE.put(new HttpField(HttpHeader.IF_MODIFIED_SINCE, (String) null));
CACHE.put(new HttpField(HttpHeader.IF_NONE_MATCH, (String) null));
CACHE.put(new HttpField(HttpHeader.AUTHORIZATION, (String) null));
CACHE.put(new HttpField(HttpHeader.COOKIE, (String) null));
}
private static HttpCompliance compliance() {
return HttpCompliance.RFC7230;
}
public HttpParser(RequestHandler handler) {
this(handler, -1, compliance());
}
public HttpParser(ResponseHandler handler) {
this(handler, -1, compliance());
}
public HttpParser(RequestHandler handler, int maxHeaderBytes) {
this(handler, maxHeaderBytes, compliance());
}
public HttpParser(ResponseHandler handler, int maxHeaderBytes) {
this(handler, maxHeaderBytes, compliance());
}
public HttpParser(RequestHandler handler, HttpCompliance compliance) {
this(handler, -1, compliance);
}
public HttpParser(RequestHandler handler, int maxHeaderBytes, HttpCompliance compliance) {
_handler = handler;
_requestHandler = handler;
_responseHandler = null;
_maxHeaderBytes = maxHeaderBytes;
_compliance = compliance == null ? compliance() : compliance;
}
public HttpParser(ResponseHandler handler, int maxHeaderBytes, HttpCompliance compliance) {
_handler = handler;
_requestHandler = null;
_responseHandler = handler;
_maxHeaderBytes = maxHeaderBytes;
_compliance = compliance == null ? compliance() : compliance;
}
public long getContentLength() {
return _contentLength;
}
public long getContentRead() {
return _contentPosition;
}
/**
* Set if a HEAD response is expected
*
* @param head
* true if head response is expected
*/
public void setHeadResponse(boolean head) {
_headResponse = head;
}
protected void setResponseStatus(int status) {
_responseStatus = status;
}
public State getState() {
return _state;
}
public boolean inContentState() {
return _state.ordinal() >= State.CONTENT.ordinal() && _state.ordinal() < State.END.ordinal();
}
public boolean inHeaderState() {
return _state.ordinal() < State.CONTENT.ordinal();
}
public boolean isChunking() {
return _endOfContent == EndOfContent.CHUNKED_CONTENT;
}
public boolean isStart() {
return isState(State.START);
}
public boolean isClose() {
return isState(State.CLOSE);
}
public boolean isClosed() {
return isState(State.CLOSED);
}
public boolean isIdle() {
return __idleStates.contains(_state);
}
public boolean isComplete() {
return __completeStates.contains(_state);
}
public boolean isState(State state) {
return _state == state;
}
enum CharState {
ILLEGAL, CR, LF, LEGAL
}
private final static CharState[] __charState;
static {
// token = 1*tchar
// tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
// / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
// / DIGIT / ALPHA
// ; any VCHAR, except delimiters
// quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
// qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
// obs-text = %x80-FF
// comment = "(" *( ctext / quoted-pair / comment ) ")"
// ctext = HTAB / SP / %x21-27 / %x2A-5B / %x5D-7E / obs-text
// quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
__charState = new CharState[256];
Arrays.fill(__charState, CharState.ILLEGAL);
__charState[LINE_FEED] = CharState.LF;
__charState[CARRIAGE_RETURN] = CharState.CR;
__charState[TAB] = CharState.LEGAL;
__charState[SPACE] = CharState.LEGAL;
__charState['!'] = CharState.LEGAL;
__charState['#'] = CharState.LEGAL;
__charState['$'] = CharState.LEGAL;
__charState['%'] = CharState.LEGAL;
__charState['&'] = CharState.LEGAL;
__charState['\''] = CharState.LEGAL;
__charState['*'] = CharState.LEGAL;
__charState['+'] = CharState.LEGAL;
__charState['-'] = CharState.LEGAL;
__charState['.'] = CharState.LEGAL;
__charState['^'] = CharState.LEGAL;
__charState['_'] = CharState.LEGAL;
__charState['`'] = CharState.LEGAL;
__charState['|'] = CharState.LEGAL;
__charState['~'] = CharState.LEGAL;
__charState['"'] = CharState.LEGAL;
__charState['\\'] = CharState.LEGAL;
__charState['('] = CharState.LEGAL;
__charState[')'] = CharState.LEGAL;
Arrays.fill(__charState, 0x21, 0x27 + 1, CharState.LEGAL);
Arrays.fill(__charState, 0x2A, 0x5B + 1, CharState.LEGAL);
Arrays.fill(__charState, 0x5D, 0x7E + 1, CharState.LEGAL);
Arrays.fill(__charState, 0x80, 0xFF + 1, CharState.LEGAL);
}
private byte next(ByteBuffer buffer) {
byte ch = buffer.get();
CharState s = __charState[0xff & ch];
switch (s) {
case ILLEGAL:
throw new IllegalCharacterException(_state, ch, buffer);
case LF:
_cr = false;
break;
case CR:
if (_cr)
throw new BadMessageException("Bad EOL");
_cr = true;
if (buffer.hasRemaining()) {
if (_maxHeaderBytes > 0 && _state.ordinal() < State.END.ordinal())
_headerBytes++;
return next(buffer);
}
// Can return 0 here to indicate the need for more characters,
// because a real 0 in the buffer would cause a BadMessage below
return 0;
case LEGAL:
if (_cr)
throw new BadMessageException("Bad EOL");
}
return ch;
}
/*
* Quick lookahead for the start state looking for a request method or a
* HTTP version, otherwise skip white space until something else to parse.
*/
private boolean quickStart(ByteBuffer buffer) {
if (_requestHandler != null) {
_method = HttpMethod.lookAheadGet(buffer);
if (_method != null) {
_methodString = _method.asString();
buffer.position(buffer.position() + _methodString.length() + 1);
setState(State.SPACE1);
return false;
}
} else if (_responseHandler != null) {
_version = HttpVersion.lookAheadGet(buffer);
if (_version != null) {
buffer.position(buffer.position() + _version.asString().length() + 1);
setState(State.SPACE1);
return false;
}
}
// Quick start look
while (_state == State.START && buffer.hasRemaining()) {
int ch = next(buffer);
if (ch > SPACE) {
_string.setLength(0);
_string.append((char) ch);
setState(_requestHandler != null ? State.METHOD : State.RESPONSE_VERSION);
return false;
} else if (ch == 0)
break;
else if (ch < 0)
throw new BadMessageException();
// count this white space as a header byte to avoid DOS
if (_maxHeaderBytes > 0 && ++_headerBytes > _maxHeaderBytes) {
log.warn("padding is too large > {}", _maxHeaderBytes);
throw new BadMessageException(HttpStatus.BAD_REQUEST_400);
}
}
return false;
}
private void setString(String s) {
_string.setLength(0);
_string.append(s);
_length = s.length();
}
private String takeString() {
_string.setLength(_length);
String s = _string.toString();
_string.setLength(0);
_length = -1;
return s;
}
/*
* Parse a request or response line
*/
private boolean parseLine(ByteBuffer buffer) {
boolean handle = false;
// Process headers
while (_state.ordinal() < State.HEADER.ordinal() && buffer.hasRemaining() && !handle) {
// process each character
byte ch = next(buffer);
if (ch == 0)
break;
if (_maxHeaderBytes > 0 && ++_headerBytes > _maxHeaderBytes) {
if (_state == State.URI) {
log.warn("URI is too large > {}", _maxHeaderBytes);
throw new BadMessageException(HttpStatus.REQUEST_URI_TOO_LONG_414);
} else {
if (_requestHandler != null)
log.warn("request is too large > {}", _maxHeaderBytes);
else
log.warn("response is too large > {}", _maxHeaderBytes);
throw new BadMessageException(HttpStatus.REQUEST_ENTITY_TOO_LARGE_413);
}
}
switch (_state) {
case METHOD:
if (ch == SPACE) {
_length = _string.length();
_methodString = takeString();
HttpMethod method = HttpMethod.CACHE.get(_methodString);
if (method != null && _compliance != HttpCompliance.LEGACY)
_methodString = method.asString();
setState(State.SPACE1);
} else if (ch < SPACE) {
if (ch == LINE_FEED)
throw new BadMessageException("No URI");
else
throw new IllegalCharacterException(_state, ch, buffer);
} else
_string.append((char) ch);
break;
case RESPONSE_VERSION:
if (ch == HttpTokens.SPACE) {
_length = _string.length();
String version = takeString();
_version = HttpVersion.CACHE.get(version);
if (_version == null)
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Unknown Version");
setState(State.SPACE1);
} else if (ch < HttpTokens.SPACE)
throw new IllegalCharacterException(_state, ch, buffer);
else
_string.append((char) ch);
break;
case SPACE1:
if (ch > HttpTokens.SPACE || ch < 0) {
if (_responseHandler != null) {
setState(State.STATUS);
setResponseStatus(ch - '0');
} else {
_uri.reset();
setState(State.URI);
// quick scan for space or EoBuffer
if (buffer.hasArray()) {
byte[] array = buffer.array();
int p = buffer.arrayOffset() + buffer.position();
int l = buffer.arrayOffset() + buffer.limit();
int i = p;
while (i < l && array[i] > HttpTokens.SPACE)
i++;
int len = i - p;
_headerBytes += len;
if (_maxHeaderBytes > 0 && ++_headerBytes > _maxHeaderBytes) {
log.warn("URI is too large > {}", _maxHeaderBytes);
throw new BadMessageException(HttpStatus.REQUEST_URI_TOO_LONG_414);
}
_uri.append(array, p - 1, len + 1);
buffer.position(i - buffer.arrayOffset());
} else
_uri.append(ch);
}
} else if (ch < HttpTokens.SPACE) {
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,
_requestHandler != null ? "No URI" : "No Status");
}
break;
case STATUS:
if (ch == HttpTokens.SPACE) {
setState(State.SPACE2);
} else if (ch >= '0' && ch <= '9') {
_responseStatus = _responseStatus * 10 + (ch - '0');
} else if (ch < HttpTokens.SPACE && ch >= 0) {
setState(State.HEADER);
handle = _responseHandler.startResponse(_version, _responseStatus, null) || handle;
} else {
throw new BadMessageException();
}
break;
case URI:
if (ch == HttpTokens.SPACE) {
setState(State.SPACE2);
} else if (ch < HttpTokens.SPACE && ch >= 0) {
// HTTP/0.9
if (_compliance.ordinal() >= HttpCompliance.RFC7230.ordinal())
throw new BadMessageException("HTTP/0.9 not supported");
handle = _requestHandler.startRequest(_methodString, _uri.toString(), HttpVersion.HTTP_0_9);
setState(State.END);
BufferUtils.clear(buffer);
handle = _handler.headerComplete() || handle;
handle = _handler.messageComplete() || handle;
return handle;
} else {
_uri.append(ch);
}
break;
case SPACE2:
if (ch > HttpTokens.SPACE) {
_string.setLength(0);
_string.append((char) ch);
if (_responseHandler != null) {
_length = 1;
setState(State.REASON);
} else {
setState(State.REQUEST_VERSION);
// try quick look ahead for HTTP Version
HttpVersion version;
if (buffer.position() > 0 && buffer.hasArray())
version = HttpVersion.lookAheadGet(buffer.array(),
buffer.arrayOffset() + buffer.position() - 1,
buffer.arrayOffset() + buffer.limit());
else
version = HttpVersion.CACHE.getBest(buffer, 0, buffer.remaining());
if (version != null) {
int pos = buffer.position() + version.asString().length() - 1;
if (pos < buffer.limit()) {
byte n = buffer.get(pos);
if (n == HttpTokens.CARRIAGE_RETURN) {
_cr = true;
_version = version;
_string.setLength(0);
buffer.position(pos + 1);
} else if (n == HttpTokens.LINE_FEED) {
_version = version;
_string.setLength(0);
buffer.position(pos);
}
}
}
}
} else if (ch == HttpTokens.LINE_FEED) {
if (_responseHandler != null) {
setState(State.HEADER);
handle = _responseHandler.startResponse(_version, _responseStatus, null) || handle;
} else {
// HTTP/0.9
if (_compliance.ordinal() >= HttpCompliance.RFC7230.ordinal())
throw new BadMessageException("HTTP/0.9 not supported");
handle = _requestHandler.startRequest(_methodString, _uri.toString(), HttpVersion.HTTP_0_9);
setState(State.END);
BufferUtils.clear(buffer);
handle = _handler.headerComplete() || handle;
handle = _handler.messageComplete() || handle;
return handle;
}
} else if (ch < 0)
throw new BadMessageException();
break;
case REQUEST_VERSION:
if (ch == HttpTokens.LINE_FEED) {
if (_version == null) {
_length = _string.length();
_version = HttpVersion.CACHE.get(takeString());
}
if (_version == null)
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Unknown Version");
// Should we try to cache header fields?
if (_connectionFields == null && _version.getVersion() >= HttpVersion.HTTP_1_1.getVersion()
&& _handler.getHeaderCacheSize() > 0) {
int header_cache = _handler.getHeaderCacheSize();
_connectionFields = new ArrayTernaryTrie<>(header_cache);
}
setState(State.HEADER);
handle = _requestHandler.startRequest(_methodString, _uri.toString(), _version) || handle;
continue;
} else if (ch >= HttpTokens.SPACE)
_string.append((char) ch);
else
throw new BadMessageException();
break;
case REASON:
if (ch == HttpTokens.LINE_FEED) {
String reason = takeString();
setState(State.HEADER);
handle = _responseHandler.startResponse(_version, _responseStatus, reason) || handle;
continue;
} else if (ch >= HttpTokens.SPACE) {
_string.append((char) ch);
if (ch != ' ' && ch != '\t')
_length = _string.length();
} else
throw new BadMessageException();
break;
default:
throw new IllegalStateException(_state.toString());
}
}
return handle;
}
private void parsedHeader() {
// handler last header if any. Delayed to here just in case there was a
// continuation line (above)
if (_headerString != null || _valueString != null) {
// Handle known headers
if (_header != null) {
boolean add_to_connection_trie = false;
switch (_header) {
case CONTENT_LENGTH:
if (_endOfContent == EndOfContent.CONTENT_LENGTH) {
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Duplicate Content-Length");
} else if (_endOfContent != EndOfContent.CHUNKED_CONTENT) {
_contentLength = convertContentLength(_valueString);
if (_contentLength <= 0)
_endOfContent = EndOfContent.NO_CONTENT;
else
_endOfContent = EndOfContent.CONTENT_LENGTH;
}
break;
case TRANSFER_ENCODING:
if (_value == HttpHeaderValue.CHUNKED) {
_endOfContent = EndOfContent.CHUNKED_CONTENT;
_contentLength = -1;
} else {
if (_valueString.endsWith(HttpHeaderValue.CHUNKED.toString()))
_endOfContent = EndOfContent.CHUNKED_CONTENT;
else if (_valueString.contains(HttpHeaderValue.CHUNKED.toString()))
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad chunking");
}
break;
case HOST:
_host = true;
if (!(_field instanceof HostPortHttpField)) {
_field = new HostPortHttpField(_header,
_compliance == HttpCompliance.LEGACY ? _headerString : _header.asString(),
_valueString);
add_to_connection_trie = _connectionFields != null;
}
break;
case CONNECTION:
// Don't cache if not persistent
if (_valueString != null && _valueString.contains("close"))
_connectionFields = null;
break;
case AUTHORIZATION:
case ACCEPT:
case ACCEPT_CHARSET:
case ACCEPT_ENCODING:
case ACCEPT_LANGUAGE:
case COOKIE:
case CACHE_CONTROL:
case USER_AGENT:
add_to_connection_trie = _connectionFields != null && _field == null;
break;
default:
break;
}
if (add_to_connection_trie && !_connectionFields.isFull() && _header != null && _valueString != null) {
if (_field == null)
_field = new HttpField(_header,
_compliance == HttpCompliance.LEGACY ? _headerString : _header.asString(),
_valueString);
_connectionFields.put(_field);
}
}
_handler.parsedHeader(_field != null ? _field : new HttpField(_header, _headerString, _valueString));
}
_headerString = _valueString = null;
_header = null;
_value = null;
_field = null;
}
private long convertContentLength(String valueString) {
try {
return Long.parseLong(valueString);
} catch (NumberFormatException e) {
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Invalid Content-Length Value");
}
}
/*
* Parse the message headers and return true if the handler has signaled for
* a return
*/
protected boolean parseHeaders(ByteBuffer buffer) {
boolean handle = false;
// Process headers
while (_state.ordinal() < State.CONTENT.ordinal() && buffer.hasRemaining() && !handle) {
// process each character
byte ch = next(buffer);
if (ch == 0)
break;
if (_maxHeaderBytes > 0 && ++_headerBytes > _maxHeaderBytes) {
log.warn("Header is too large > {}", _maxHeaderBytes);
throw new BadMessageException(HttpStatus.REQUEST_ENTITY_TOO_LARGE_413);
}
switch (_state) {
case HEADER:
switch (ch) {
case HttpTokens.COLON:
case HttpTokens.SPACE:
case HttpTokens.TAB: {
if (_compliance.ordinal() >= HttpCompliance.RFC7230.ordinal())
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "Bad Continuation");
// header value without name - continuation?
if (_valueString == null) {
_string.setLength(0);
_length = 0;
} else {
setString(_valueString);
_string.append(' ');
_length++;
_valueString = null;
}
setState(State.HEADER_VALUE);
break;
}
case HttpTokens.LINE_FEED: {
// process previous header
parsedHeader();
_contentPosition = 0;
// End of headers!
// Was there a required host header?
if (!_host && _version == HttpVersion.HTTP_1_1 && _requestHandler != null) {
throw new BadMessageException(HttpStatus.BAD_REQUEST_400, "No Host");
}
// is it a response that cannot have a body?
if (_responseHandler !=null && // response
(_responseStatus == 304 || // not-modified response
_responseStatus == 204 || // no-content response
_responseStatus < 200)) {// 1xx response
_endOfContent=EndOfContent.NO_CONTENT; // ignore any other headers set
} else if (_endOfContent == EndOfContent.UNKNOWN_CONTENT) { // else if we don't know framing
if (_responseStatus == 0 // request
|| _responseStatus == 304 // not-modified response
|| _responseStatus == 204 // no-content response
|| _responseStatus < 200) // 1xx response
_endOfContent=EndOfContent.NO_CONTENT;
else
_endOfContent=EndOfContent.EOF_CONTENT;
}
// How is the message ended?
switch (_endOfContent) {
case EOF_CONTENT:
setState(State.EOF_CONTENT);
handle = _handler.headerComplete() || handle;
return handle;
case CHUNKED_CONTENT:
setState(State.CHUNKED_CONTENT);
handle = _handler.headerComplete() || handle;
return handle;
case NO_CONTENT:
setState(State.END);
handle = _handler.headerComplete() || handle;
handle = _handler.messageComplete() || handle;
return handle;
default:
setState(State.CONTENT);
handle = _handler.headerComplete() || handle;
return handle;
}
}
default: {
// now handle the ch
if (ch < HttpTokens.SPACE)
throw new BadMessageException();
// process previous header
parsedHeader();
// handle new header
if (buffer.hasRemaining()) {
// Try a look ahead for the known header name and value.
HttpField field = _connectionFields == null ? null
: _connectionFields.getBest(buffer, -1, buffer.remaining());
if (field == null)
field = CACHE.getBest(buffer, -1, buffer.remaining());
if (field != null) {
final String n;
final String v;
if (_compliance == HttpCompliance.LEGACY) {
// Have to get the fields exactly from the
// buffer to match case
String fn = field.getName();
String fv = field.getValue();
n = BufferUtils.toString(buffer, buffer.position() - 1, fn.length(),
StandardCharsets.US_ASCII);
if (fv == null)
v = null;
else {
v = BufferUtils.toString(buffer, buffer.position() + fn.length() + 1, fv.length(),
StandardCharsets.ISO_8859_1);
field = new HttpField(field.getHeader(), n, v);
}
} else {
n = field.getName();
v = field.getValue();
}
_header = field.getHeader();
_headerString = n;
if (v == null) {
// Header only
setState(State.HEADER_VALUE);
_string.setLength(0);
_length = 0;
buffer.position(buffer.position() + n.length() + 1);
break;
} else {
// Header and value
int pos = buffer.position() + n.length() + v.length() + 1;
byte b = buffer.get(pos);
if (b == HttpTokens.CARRIAGE_RETURN || b == HttpTokens.LINE_FEED) {
_field = field;
_valueString = v;
setState(State.HEADER_IN_VALUE);
if (b == HttpTokens.CARRIAGE_RETURN) {
_cr = true;
buffer.position(pos + 1);
} else
buffer.position(pos);
break;
} else {
setState(State.HEADER_IN_VALUE);
setString(v);
buffer.position(pos);
break;
}
}
}
}
// New header
setState(State.HEADER_IN_NAME);
_string.setLength(0);
_string.append((char) ch);
_length = 1;
}
}
break;
case HEADER_IN_NAME:
if (ch == HttpTokens.COLON) {
if (_headerString == null) {
_headerString = takeString();
_header = HttpHeader.CACHE.get(_headerString);
}
_length = -1;
setState(State.HEADER_VALUE);
break;
}
if (ch > HttpTokens.SPACE) {
if (_header != null) {
setString(_header.asString());
_header = null;
_headerString = null;
}
_string.append((char) ch);
if (ch > HttpTokens.SPACE)
_length = _string.length();
break;
}
throw new IllegalCharacterException(_state, ch, buffer);
case HEADER_VALUE:
if (ch > HttpTokens.SPACE || ch < 0) {
_string.append((char) (0xff & ch));
_length = _string.length();
setState(State.HEADER_IN_VALUE);
break;
}
if (ch == HttpTokens.SPACE || ch == HttpTokens.TAB)
break;
if (ch == HttpTokens.LINE_FEED) {
_value = null;
_string.setLength(0);
_valueString = "";
_length = -1;
setState(State.HEADER);
break;
}
throw new IllegalCharacterException(_state, ch, buffer);
case HEADER_IN_VALUE:
if (ch >= HttpTokens.SPACE || ch < 0 || ch == HttpTokens.TAB) {
if (_valueString != null) {
setString(_valueString);
_valueString = null;
_field = null;
}
_string.append((char) (0xff & ch));
if (ch > HttpTokens.SPACE || ch < 0)
_length = _string.length();
break;
}
if (ch == HttpTokens.LINE_FEED) {
if (_length > 0) {
_value = null;
_valueString = takeString();
_length = -1;
}
setState(State.HEADER);
break;
}
throw new IllegalCharacterException(_state, ch, buffer);
default:
throw new IllegalStateException(_state.toString());
}
}
return handle;
}
/**
* Parse until next Event.
*
* @param buffer
* the buffer to parse
* @return True if an {@link RequestHandler} method was called and it
* returned true;
*/
public boolean parseNext(ByteBuffer buffer) {
if (DEBUG)
log.debug("parseNext s={} {}", _state, BufferUtils.toDetailString(buffer));
try {
// Start a request/response
if (_state == State.START) {
_version = null;
_method = null;
_methodString = null;
_endOfContent = EndOfContent.UNKNOWN_CONTENT;
_header = null;
if (quickStart(buffer))
return true;
}
// Request/response line
if (_state.ordinal() >= State.START.ordinal() && _state.ordinal() < State.HEADER.ordinal()) {
if (parseLine(buffer))
return true;
}
// parse headers
if (_state.ordinal() >= State.HEADER.ordinal() && _state.ordinal() < State.CONTENT.ordinal()) {
if (parseHeaders(buffer))
return true;
}
// parse content
if (_state.ordinal() >= State.CONTENT.ordinal() && _state.ordinal() < State.END.ordinal()) {
// Handle HEAD response
if (_responseStatus > 0 && _headResponse) {
setState(State.END);
return _handler.messageComplete();
} else {
if (parseContent(buffer))
return true;
}
}
// handle end states
if (_state == State.END) {
// eat white space
while (buffer.remaining() > 0 && buffer.get(buffer.position()) <= HttpTokens.SPACE)
buffer.get();
} else if (_state == State.CLOSE) {
// Seeking EOF
if (BufferUtils.hasContent(buffer)) {
// Just ignore data when closed
_headerBytes += buffer.remaining();
BufferUtils.clear(buffer);
if (_maxHeaderBytes > 0 && _headerBytes > _maxHeaderBytes) {
// Don't want to waste time reading data of a closed
// request
throw new IllegalStateException("too much data seeking EOF");
}
}
} else if (_state == State.CLOSED) {
BufferUtils.clear(buffer);
}
// Handle EOF
if (_eof && !buffer.hasRemaining()) {
switch (_state) {
case CLOSED:
break;
case START:
setState(State.CLOSED);
_handler.earlyEOF();
break;
case END:
case CLOSE:
setState(State.CLOSED);
break;
case EOF_CONTENT:
setState(State.CLOSED);
return _handler.messageComplete();
case CONTENT:
case CHUNKED_CONTENT:
case CHUNK_SIZE:
case CHUNK_PARAMS:
case CHUNK:
setState(State.CLOSED);
_handler.earlyEOF();
break;
default:
if (DEBUG)
log.debug("{} EOF in {}", this, _state);
setState(State.CLOSED);
_handler.badMessage(400, null);
break;
}
}
} catch (BadMessageException e) {
BufferUtils.clear(buffer);
Throwable cause = e.getCause();
boolean stack = log.isDebugEnabled() || (!(cause instanceof NumberFormatException)
&& (cause instanceof RuntimeException || cause instanceof Error));
if (stack)
log.warn("bad HTTP parsed: " + e._code + (e.getReason() != null ? " " + e.getReason() : "") + " for "
+ _handler, e);
else
log.warn("bad HTTP parsed: " + e._code + (e.getReason() != null ? " " + e.getReason() : "") + " for "
+ _handler);
setState(State.CLOSE);
_handler.badMessage(e.getCode(), e.getReason());
} catch (NumberFormatException | IllegalStateException e) {
BufferUtils.clear(buffer);
log.warn("parse exception: {} in {} for {}", e.toString(), _state, _handler);
if (DEBUG)
log.debug("parse exception", e);
switch (_state) {
case CLOSED:
break;
case CLOSE:
_handler.earlyEOF();
break;
default:
setState(State.CLOSE);
_handler.badMessage(400, null);
}
} catch (Exception | Error e) {
BufferUtils.clear(buffer);
log.warn("parse exception: " + e.toString() + " for " + _handler, e);
switch (_state) {
case CLOSED:
break;
case CLOSE:
_handler.earlyEOF();
break;
default:
setState(State.CLOSE);
_handler.badMessage(400, null);
}
}
return false;
}
protected boolean parseContent(ByteBuffer buffer) {
int remaining = buffer.remaining();
if (remaining == 0 && _state == State.CONTENT) {
long content = _contentLength - _contentPosition;
if (content == 0) {
setState(State.END);
return _handler.messageComplete();
}
}
// Handle _content
byte ch;
while (_state.ordinal() < State.END.ordinal() && remaining > 0) {
switch (_state) {
case EOF_CONTENT:
_contentChunk = buffer.asReadOnlyBuffer();
_contentPosition += remaining;
buffer.position(buffer.position() + remaining);
if (_handler.content(_contentChunk))
return true;
break;
case CONTENT: {
long content = _contentLength - _contentPosition;
if (content == 0) {
setState(State.END);
return _handler.messageComplete();
} else {
_contentChunk = buffer.asReadOnlyBuffer();
// limit content by expected size
if (remaining > content) {
// We can cast remaining to an int as we know that it is
// smaller than
// or equal to length which is already an int.
_contentChunk.limit(_contentChunk.position() + (int) content);
}
_contentPosition += _contentChunk.remaining();
buffer.position(buffer.position() + _contentChunk.remaining());
if (_handler.content(_contentChunk))
return true;
if (_contentPosition == _contentLength) {
setState(State.END);
return _handler.messageComplete();
}
}
break;
}
case CHUNKED_CONTENT: {
ch = next(buffer);
if (ch > HttpTokens.SPACE) {
_chunkLength = TypeUtils.convertHexDigit(ch);
_chunkPosition = 0;
setState(State.CHUNK_SIZE);
}
break;
}
case CHUNK_SIZE: {
ch = next(buffer);
if (ch == 0)
break;
if (ch == HttpTokens.LINE_FEED) {
if (_chunkLength == 0)
setState(State.CHUNK_END);
else
setState(State.CHUNK);
} else if (ch <= HttpTokens.SPACE || ch == HttpTokens.SEMI_COLON)
setState(State.CHUNK_PARAMS);
else
_chunkLength = _chunkLength * 16 + TypeUtils.convertHexDigit(ch);
break;
}
case CHUNK_PARAMS: {
ch = next(buffer);
if (ch == HttpTokens.LINE_FEED) {
if (_chunkLength == 0)
setState(State.CHUNK_END);
else
setState(State.CHUNK);
}
break;
}
case CHUNK: {
int chunk = _chunkLength - _chunkPosition;
if (chunk == 0) {
setState(State.CHUNKED_CONTENT);
} else {
_contentChunk = buffer.asReadOnlyBuffer();
if (remaining > chunk)
_contentChunk.limit(_contentChunk.position() + chunk);
chunk = _contentChunk.remaining();
_contentPosition += chunk;
_chunkPosition += chunk;
buffer.position(buffer.position() + chunk);
if (_handler.content(_contentChunk))
return true;
}
break;
}
case CHUNK_END: {
// TODO handle chunk trailer
ch = next(buffer);
if (ch == 0)
break;
if (ch == HttpTokens.LINE_FEED) {
setState(State.END);
return _handler.messageComplete();
}
throw new IllegalCharacterException(_state, ch, buffer);
}
case CLOSED: {
BufferUtils.clear(buffer);
return false;
}
default:
break;
}
remaining = buffer.remaining();
}
return false;
}
public boolean isAtEOF() {
return _eof;
}
/**
* Signal that the associated data source is at EOF
*/
public void atEOF() {
if (DEBUG)
log.debug("atEOF {}", this);
_eof = true;
}
/**
* Request that the associated data source be closed
*/
public void close() {
if (DEBUG)
log.debug("close {}", this);
setState(State.CLOSE);
}
public void reset() {
if (DEBUG)
log.debug("reset {}", this);
// reset state
if (_state == State.CLOSE || _state == State.CLOSED)
return;
setState(State.START);
_endOfContent = EndOfContent.UNKNOWN_CONTENT;
_contentLength = -1;
_contentPosition = 0;
_responseStatus = 0;
_contentChunk = null;
_headerBytes = 0;
_host = false;
}
protected void setState(State state) {
if (DEBUG)
log.debug("{} --> {}", _state, state);
_state = state;
}
public Trie getFieldCache() {
return _connectionFields;
}
@Override
public String toString() {
return String.format("%s{s=%s,%d of %d}", getClass().getSimpleName(), _state, _contentPosition, _contentLength);
}
/*
* Event Handler interface These methods return true if the caller should
* process the events so far received (eg return from parseNext and call
* HttpChannel.handle). If multiple callbacks are called in sequence (eg
* headerComplete then messageComplete) from the same point in the parsing
* then it is sufficient for the caller to process the events only once.
*/
public interface HttpHandler {
public boolean content(ByteBuffer item);
public boolean headerComplete();
public boolean messageComplete();
/**
* This is the method called by parser when a HTTP Header name and value
* is found
*
* @param field
* The field parsed
*/
public void parsedHeader(HttpField field);
/**
* Called to signal that an EOF was received unexpectedly during the
* parsing of a HTTP message
*/
public void earlyEOF();
/**
* Called to signal that a bad HTTP message has been received.
*
* @param status
* The bad status to send
* @param reason
* The textual reason for badness
*/
public void badMessage(int status, String reason);
/**
* @return the size in bytes of the per parser header cache
*/
public int getHeaderCacheSize();
}
public interface RequestHandler extends HttpHandler {
/**
* This is the method called by parser when the HTTP request line is
* parsed
*
* @param method
* The method
* @param uri
* The raw bytes of the URI. These are copied into a
* ByteBuffer that will not be changed until this parser is
* reset and reused.
* @param version
* the http version in use
* @return true if handling parsing should return.
*/
public boolean startRequest(String method, String uri, HttpVersion version);
}
public interface ResponseHandler extends HttpHandler {
/**
* This is the method called by parser when the HTTP request line is
* parsed
*
* @param version
* the http version in use
* @param status
* the response status
* @param reason
* the response reason phrase
* @return true if handling parsing should return
*/
public boolean startResponse(HttpVersion version, int status, String reason);
}
@SuppressWarnings("serial")
private static class IllegalCharacterException extends BadMessageException {
private IllegalCharacterException(State state, byte ch, ByteBuffer buffer) {
super(400, String.format("Illegal character 0x%X", ch));
// Bug #460642 - don't reveal buffers to end user
log.warn(String.format("Illegal character 0x%X in state=%s for buffer %s", ch, state,
BufferUtils.toDetailString(buffer)));
}
}
}