org.eclipse.jetty.http.HttpParser Maven / Gradle / Ivy
//
// ========================================================================
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
//
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
// ========================================================================
//
package org.eclipse.jetty.http;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import org.eclipse.jetty.http.HttpTokens.EndOfContent;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.HostPort;
import org.eclipse.jetty.util.Index;
import org.eclipse.jetty.util.NanoTime;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.Utf8StringBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.eclipse.jetty.http.HttpCompliance.RFC7230;
import static org.eclipse.jetty.http.HttpCompliance.Violation;
import static org.eclipse.jetty.http.HttpCompliance.Violation.CASE_SENSITIVE_FIELD_NAME;
import static org.eclipse.jetty.http.HttpCompliance.Violation.DUPLICATE_HOST_HEADERS;
import static org.eclipse.jetty.http.HttpCompliance.Violation.HTTP_0_9;
import static org.eclipse.jetty.http.HttpCompliance.Violation.MULTIPLE_CONTENT_LENGTHS;
import static org.eclipse.jetty.http.HttpCompliance.Violation.NO_COLON_AFTER_FIELD_NAME;
import static org.eclipse.jetty.http.HttpCompliance.Violation.TRANSFER_ENCODING_WITH_CONTENT_LENGTH;
import static org.eclipse.jetty.http.HttpCompliance.Violation.UNSAFE_HOST_HEADER;
import static org.eclipse.jetty.http.HttpCompliance.Violation.WHITESPACE_AFTER_FIELD_NAME;
import static org.eclipse.jetty.http.HttpTokens.CARRIAGE_RETURN;
import static org.eclipse.jetty.http.HttpTokens.LINE_FEED;
/**
* 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 Index#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
- 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 final Logger LOG = LoggerFactory.getLogger(HttpParser.class);
public static final int INITIAL_URI_LENGTH = 256;
private static final int MAX_CHUNK_LENGTH = Integer.MAX_VALUE / 16 - 16;
private static final String UNMATCHED_VALUE = "\u0000";
/**
* 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 static final Index CACHE = new Index.Builder()
.caseSensitive(false)
.with(HttpFields.CONNECTION_CLOSE)
.with(HttpFields.CONNECTION_KEEPALIVE)
.with(new HttpField(HttpHeader.CONNECTION, HttpHeaderValue.UPGRADE))
.with(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip"))
.with(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip, deflate"))
.with(new HttpField(HttpHeader.ACCEPT_ENCODING, "gzip, deflate, br"))
.with(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-US,enq=0.5"))
.with(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-GB,en-USq=0.8,enq=0.6"))
.with(new HttpField(HttpHeader.ACCEPT_LANGUAGE, "en-AU,enq=0.9,it-ITq=0.8,itq=0.7,en-GBq=0.6,en-USq=0.5"))
.with(new HttpField(HttpHeader.ACCEPT_CHARSET, "ISO-8859-1,utf-8q=0.7,*q=0.3"))
.with(new HttpField(HttpHeader.ACCEPT, "*/*"))
.with(new HttpField(HttpHeader.ACCEPT, "image/png,image/*q=0.8,*/*q=0.5"))
.with(new HttpField(HttpHeader.ACCEPT, "text/html,application/xhtml+xml,application/xmlq=0.9,*/*q=0.8"))
.with(new HttpField(HttpHeader.ACCEPT, "text/html,application/xhtml+xml,application/xmlq=0.9,image/webp,image/apng,*/*q=0.8"))
.with(new HttpField(HttpHeader.ACCEPT_RANGES, HttpHeaderValue.BYTES))
.with(new HttpField(HttpHeader.PRAGMA, "no-cache"))
.with(new HttpField(HttpHeader.CACHE_CONTROL, "private, no-cache, no-cache=Set-Cookie, proxy-revalidate"))
.with(new HttpField(HttpHeader.CACHE_CONTROL, "no-cache"))
.with(new HttpField(HttpHeader.CACHE_CONTROL, "max-age=0"))
.with(HttpFields.CONTENT_LENGTH_0)
.with(new HttpField(HttpHeader.CONTENT_ENCODING, "gzip"))
.with(new HttpField(HttpHeader.CONTENT_ENCODING, "deflate"))
.with(new HostPortHttpField("localhost"))
.with(new HttpField(HttpHeader.TRANSFER_ENCODING, "chunked"))
.with(new HttpField(HttpHeader.EXPIRES, "Fri, 01 Jan 1990 00:00:00 GMT"))
.withAll(() ->
{
Map map = new LinkedHashMap<>();
// 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);
map.put(field.toString(), field);
for (String charset : new String[]{"utf-8", "iso-8859-1"})
{
PreEncodedHttpField field1 = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + ";charset=" + charset);
map.put(field1.toString(), field1);
PreEncodedHttpField field2 = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + "; charset=" + charset);
map.put(field2.toString(), field2);
PreEncodedHttpField field3 = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + ";charset=" + charset.toUpperCase(Locale.ENGLISH));
map.put(field3.toString(), field3);
PreEncodedHttpField field4 = new PreEncodedHttpField(HttpHeader.CONTENT_TYPE, type + "; charset=" + charset.toUpperCase(Locale.ENGLISH));
map.put(field4.toString(), field4);
}
}
return map;
})
.withAll(() ->
{
Map map = new LinkedHashMap<>();
for (HttpHeader h : HttpHeader.values())
{
HttpField httpField = new HttpField(h, UNMATCHED_VALUE);
map.put(h + ": ", httpField);
}
return map;
})
.build();
private static final Index.Mutable NO_CACHE = new Index.Builder()
.caseSensitive(false)
.mutable()
.maxCapacity(0)
.build();
private static final long HTTP_1_0_AS_LONG = stringAsLong("HTTP/1.0");
private static final long HTTP_1_1_AS_LONG = stringAsLong("HTTP/1.1");
private static final long GET_SLASH_HT_AS_LONG = stringAsLong("GET / HT");
private static final long TP_SLASH_1_0_CRLF_AS_LONG = stringAsLong("TP/1.0\r\n");
private static final long TP_SLASH_1_1_CRLF_AS_LONG = stringAsLong("TP/1.1\r\n");
private static final long SPACE_200_OK_CR_AS_LONG = stringAsLong(" 200 OK\r");
private static final int CRLF_AS_SHORT = ((0xFF & '\r') << Byte.SIZE) | (0xFF & '\n');
private static long stringAsLong(String s)
{
if (s == null || s.length() != Long.BYTES)
throw new IllegalArgumentException();
long l = 0;
for (char c : s.toCharArray())
{
l = l << Byte.SIZE | ((long)c & 0xFFL);
}
return l;
}
// States
public enum FieldState
{
FIELD,
IN_NAME,
VALUE,
IN_VALUE,
WS_AFTER_NAME,
}
// States
public enum State
{
START,
METHOD,
RESPONSE_VERSION,
SPACE1,
STATUS,
URI,
SPACE2,
REQUEST_VERSION,
REASON,
PROXY,
HEADER,
CONTENT,
EOF_CONTENT,
CHUNKED_CONTENT,
CHUNK_SIZE,
CHUNK_PARAMS,
CHUNK,
CONTENT_END,
TRAILER,
END,
CLOSE, // The associated stream/endpoint should be closed
CLOSED // The associated stream/endpoint is at EOF
}
private static final EnumSet __idleStates = EnumSet.of(State.START, State.END, State.CLOSE, State.CLOSED);
private static final EnumSet __completeStates = EnumSet.of(State.END, State.CLOSE, State.CLOSED);
private static final EnumSet __terminatedStates = EnumSet.of(State.CLOSE, State.CLOSED);
private final boolean debugEnabled = LOG.isDebugEnabled(); // Cache debug to help branch prediction
private final HttpHandler _handler;
private final RequestHandler _requestHandler;
private final ResponseHandler _responseHandler;
private final boolean _requestParser;
private final int _maxHeaderBytes;
private final HttpCompliance _complianceMode;
private final Utf8StringBuilder _uri = new Utf8StringBuilder(INITIAL_URI_LENGTH);
private final FieldCache _fieldCache = new FieldCache();
private HttpField _field;
private HttpHeader _header;
private long _beginNanoTime;
private String _headerString;
private String _valueString;
private int _responseStatus;
private int _headerBytes;
private String _parsedHost;
private boolean _headerComplete;
private volatile State _state = State.START;
private volatile FieldState _fieldState = FieldState.FIELD;
private volatile boolean _eof;
private HttpMethod _method;
private String _methodString;
private HttpVersion _version;
private EndOfContent _endOfContent;
private boolean _hasContentLength;
private boolean _hasTransferEncoding;
private long _contentLength = -1;
private long _contentPosition;
private int _chunkLength;
private int _chunkPosition;
private boolean _headResponse;
private boolean _cr;
private ByteBuffer _contentChunk;
private int _length;
private final StringBuilder _string = new StringBuilder();
private static HttpCompliance compliance()
{
return 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)
{
this(handler, null, maxHeaderBytes, compliance == null ? compliance() : compliance);
}
public HttpParser(ResponseHandler handler, int maxHeaderBytes, HttpCompliance compliance)
{
this(null, handler, maxHeaderBytes, compliance == null ? compliance() : compliance);
}
private HttpParser(RequestHandler requestHandler, ResponseHandler responseHandler, int maxHeaderBytes, HttpCompliance compliance)
{
_requestHandler = requestHandler;
_responseHandler = responseHandler;
_requestParser = _requestHandler != null;
if (!_requestParser && _responseHandler == null)
throw new IllegalStateException();
_handler = _requestParser ? requestHandler : responseHandler;
_maxHeaderBytes = maxHeaderBytes;
_complianceMode = compliance;
}
public long getBeginNanoTime()
{
return _beginNanoTime;
}
public HttpHandler getHandler()
{
return _handler;
}
public int getHeaderCacheSize()
{
return _fieldCache.getCapacity();
}
public void setHeaderCacheSize(int headerCacheSize)
{
_fieldCache.setCapacity(headerCacheSize);
}
public boolean isHeaderCacheCaseSensitive()
{
return _fieldCache.isCaseSensitive();
}
public void setHeaderCacheCaseSensitive(boolean headerCacheCaseSensitive)
{
_fieldCache.setCaseSensitive(headerCacheCaseSensitive);
}
protected void checkViolation(Violation violation) throws BadMessageException
{
if (violation.isAllowedBy(_complianceMode))
reportComplianceViolation(violation, violation.getDescription());
else
throw new BadMessageException(violation.getDescription());
}
protected void reportComplianceViolation(Violation violation)
{
reportComplianceViolation(violation, violation.getDescription());
}
protected void reportComplianceViolation(Violation violation, String reason)
{
if (_requestParser)
_requestHandler.onViolation(new ComplianceViolation.Event(_complianceMode, violation, reason));
}
protected String caseInsensitiveHeader(String orig, String normative)
{
if (CASE_SENSITIVE_FIELD_NAME.isAllowedBy(_complianceMode))
return normative;
if (!orig.equals(normative))
reportComplianceViolation(CASE_SENSITIVE_FIELD_NAME, orig);
return orig;
}
public long getContentLength()
{
return _contentLength;
}
public long getContentRead()
{
return _contentPosition;
}
public int getHeaderLength()
{
return _headerBytes;
}
/**
* 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 isTerminated()
{
return __terminatedStates.contains(_state);
}
public boolean isState(State state)
{
return _state == state;
}
private HttpTokens.Token next(ByteBuffer buffer)
{
byte ch = buffer.get();
HttpTokens.Token t = HttpTokens.getToken(ch);
switch (t.getType())
{
case CNTL:
throw new IllegalCharacterException(_state, t, buffer);
case LF:
_cr = false;
break;
case CR:
if (_cr)
throw new BadMessageException("Bad EOL");
if (buffer.hasRemaining())
{
// Don't count the CRs and LFs of the chunked encoding.
if (_maxHeaderBytes > 0 && (_state == State.HEADER || _state == State.TRAILER))
_headerBytes++;
ch = buffer.get();
t = HttpTokens.TOKENS[0xff & ch];
return switch (t.getType())
{
case CNTL -> throw new IllegalCharacterException(_state, t, buffer);
case LF -> t;
default -> throw new BadMessageException("Bad EOL");
};
}
_cr = true;
return null;
case ALPHA:
case DIGIT:
case TCHAR:
case VCHAR:
case HTAB:
case SPACE:
case OTEXT:
case COLON:
if (_cr)
throw new BadMessageException("Bad EOL");
break;
default:
break;
}
return t;
}
private boolean quickStartRequestLine(ByteBuffer buffer)
{
int position = buffer.position();
int remaining = buffer.remaining();
if (remaining >= 2 * Long.BYTES)
{
// try to match "GET / HTTP/1.x\r\n" with two longs
long lookahead = buffer.getLong(position);
if (lookahead == GET_SLASH_HT_AS_LONG)
{
long v = buffer.getLong(position + Long.BYTES);
if (v == TP_SLASH_1_1_CRLF_AS_LONG)
{
buffer.position(position + 2 * Long.BYTES);
_methodString = HttpMethod.GET.asString();
_version = HttpVersion.HTTP_1_1;
_fieldCache.prepare();
setState(State.HEADER);
_requestHandler.startRequest(_methodString, "/", _version);
return true;
}
if (v == TP_SLASH_1_0_CRLF_AS_LONG)
{
buffer.position(position + 2 * Long.BYTES);
_methodString = HttpMethod.GET.asString();
_version = HttpVersion.HTTP_1_0;
_fieldCache.prepare();
setState(State.HEADER);
_requestHandler.startRequest(_methodString, "/", _version);
return true;
}
}
else
{
// else lookup just the method using the first 4 bytes of the already fetched long as a lookahead.
_method = HttpMethod.lookAheadGet(buffer, (int)((lookahead >> 32)));
}
}
else if (remaining >= Integer.BYTES)
{
// otherwise try a lookahead to match just the method
_method = HttpMethod.lookAheadGet(buffer);
}
if (_method != null)
{
_methodString = _method.asString();
// The lookAheadGet method above checks for the trailing space,
// so it is safe to move the position 1 more than the method length.
position = position + _methodString.length() + 1;
buffer.position(position);
setState(State.SPACE1);
return true;
}
return false;
}
private boolean quickStartResponseLine(ByteBuffer buffer)
{
int position = buffer.position();
int remaining = buffer.remaining();
if (remaining > Long.BYTES)
{
// Match version as a long
long v = buffer.getLong(position);
if (v == HTTP_1_1_AS_LONG)
_version = HttpVersion.HTTP_1_1;
else if (v == HTTP_1_0_AS_LONG)
_version = HttpVersion.HTTP_1_0;
if (_version != null)
{
position += Long.BYTES;
// Try to make 200 OK as a long
if (remaining > 2 * Long.BYTES &&
buffer.get(position + Long.BYTES) == '\n' &&
buffer.getLong(position) == SPACE_200_OK_CR_AS_LONG)
{
buffer.position(position + 9);
_responseStatus = HttpStatus.OK_200;
_fieldCache.prepare();
setState(State.HEADER);
_responseHandler.startResponse(_version, _responseStatus, "OK");
return true;
}
if (buffer.get(position) == ' ')
{
buffer.position(position + 1);
setState(State.SPACE1);
return true;
}
// Probably a bad version like HTTP/1.11
_version = null;
}
}
return false;
}
/* Quick lookahead for the start state looking for a request method or an HTTP version,
* otherwise skip white space until something else to parse.
*/
private void quickStart(ByteBuffer buffer)
{
if (_requestParser)
{
if (quickStartRequestLine(buffer))
return;
}
else
{
if (quickStartResponseLine(buffer))
return;
}
// Quick start look
while (_state == State.START && buffer.hasRemaining())
{
HttpTokens.Token t = next(buffer);
if (t == null)
break;
switch (t.getType())
{
case ALPHA, DIGIT, TCHAR, VCHAR ->
{
_string.setLength(0);
_string.append(t.getChar());
setState(_requestParser ? State.METHOD : State.RESPONSE_VERSION);
return;
}
case OTEXT, SPACE, HTAB -> throw new IllegalCharacterException(_state, t, buffer);
default ->
{
}
}
// 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);
}
}
}
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;
}
private boolean handleHeaderContentMessage()
{
boolean handleHeader = _handler.headerComplete();
_headerComplete = true;
if (handleHeader)
return true;
setState(State.CONTENT_END);
return handleContentMessage();
}
private boolean handleContentMessage()
{
boolean handleContent = _handler.contentComplete();
if (handleContent)
return true;
setState(State.END);
return _handler.messageComplete();
}
/* 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
HttpTokens.Token t = next(buffer);
if (t == null)
break;
if (_maxHeaderBytes > 0 && ++_headerBytes > _maxHeaderBytes)
{
if (_state == State.URI)
{
LOG.warn("URI is too large >{}", _maxHeaderBytes);
throw new BadMessageException(HttpStatus.URI_TOO_LONG_414);
}
else
{
if (_requestParser)
LOG.warn("request is too large >{}", _maxHeaderBytes);
else
LOG.warn("response is too large >{}", _maxHeaderBytes);
throw new BadMessageException(HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431);
}
}
switch (_state)
{
case METHOD:
switch (t.getType())
{
case SPACE:
_length = _string.length();
_methodString = takeString();
HttpMethod method = HttpMethod.CACHE.get(_methodString);
if (method != null)
{
_methodString = method.asString();
}
else if (Violation.CASE_INSENSITIVE_METHOD.isAllowedBy(_complianceMode))
{
method = HttpMethod.INSENSITIVE_CACHE.get(_methodString);
if (method != null)
{
_methodString = method.asString();
reportComplianceViolation(Violation.CASE_INSENSITIVE_METHOD, _methodString);
}
}
setState(State.SPACE1);
break;
case LF:
throw new BadMessageException("No URI");
case ALPHA:
case DIGIT:
case TCHAR:
_string.append(t.getChar());
break;
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
case RESPONSE_VERSION:
switch (t.getType())
{
case SPACE:
_length = _string.length();
String version = takeString();
_version = HttpVersion.CACHE.get(version);
checkVersion();
setState(State.SPACE1);
break;
case ALPHA:
case DIGIT:
case TCHAR:
case VCHAR:
case COLON:
_string.append(t.getChar());
break;
case CR:
case LF:
throw new BadMessageException("No Status");
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
case SPACE1:
switch (t.getType())
{
case SPACE:
break;
case ALPHA:
case DIGIT:
case TCHAR:
case VCHAR:
case COLON:
if (!_requestParser)
{
if (t.getType() != HttpTokens.Type.DIGIT)
throw new IllegalCharacterException(_state, t, buffer);
setState(State.STATUS);
setResponseStatus(t.getByte() - '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.URI_TOO_LONG_414);
}
_uri.append(array, p - 1, len + 1);
buffer.position(i - buffer.arrayOffset());
}
else
_uri.append(t.getByte());
}
break;
default:
throw new BadMessageException(_requestParser ? "No URI" : "No Status");
}
break;
case STATUS:
assert !_requestParser;
switch (t.getType())
{
case SPACE:
setState(State.SPACE2);
break;
case DIGIT:
_responseStatus = _responseStatus * 10 + (t.getByte() - '0');
if (_responseStatus >= 1000)
throw new BadMessageException("Bad status");
break;
case LF:
_fieldCache.prepare();
setState(State.HEADER);
_responseHandler.startResponse(_version, _responseStatus, null);
break;
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
case URI:
assert _requestParser;
int position = buffer.position();
int remaining = buffer.remaining();
switch (t.getType())
{
case SPACE:
int endOfVersion = position + Long.BYTES;
if (remaining >= (Long.BYTES + 2) &&
buffer.getShort(endOfVersion) == CRLF_AS_SHORT)
{
// try look-ahead for request HTTP Version
long versionAsLong = buffer.getLong(position);
HttpVersion version = versionAsLong == HTTP_1_1_AS_LONG
? HttpVersion.HTTP_1_1
: versionAsLong == HTTP_1_0_AS_LONG ? HttpVersion.HTTP_1_0 : null;
if (version != null)
{
buffer.position(endOfVersion + 2);
_version = version;
_string.setLength(0);
checkVersion();
_fieldCache.prepare();
setState(State.HEADER);
_requestHandler.startRequest(_methodString, _uri.toCompleteString(), _version);
continue;
}
}
setState(State.SPACE2);
break;
case LF:
// HTTP/0.9
if (Violation.HTTP_0_9.isAllowedBy(_complianceMode))
{
reportComplianceViolation(HTTP_0_9, HTTP_0_9.getDescription());
_requestHandler.startRequest(_methodString, _uri.toCompleteString(), HttpVersion.HTTP_0_9);
setState(State.CONTENT);
_endOfContent = EndOfContent.NO_CONTENT;
BufferUtil.clear(buffer);
handle = handleHeaderContentMessage();
}
else
{
throw new HttpException.RuntimeException(HttpStatus.HTTP_VERSION_NOT_SUPPORTED_505, "HTTP/0.9 not supported");
}
break;
case ALPHA:
case DIGIT:
case TCHAR:
case VCHAR:
case COLON:
case OTEXT:
_uri.append(t.getByte());
break;
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
case SPACE2:
switch (t.getType())
{
case SPACE:
break;
case ALPHA:
case DIGIT:
case TCHAR:
case VCHAR:
case COLON:
_string.setLength(0);
_string.append(t.getChar());
_length = 1;
setState(_requestParser ? State.REQUEST_VERSION : State.REASON);
break;
case LF:
if (!_requestParser)
{
_fieldCache.prepare();
setState(State.HEADER);
_responseHandler.startResponse(_version, _responseStatus, null);
}
else
{
// HTTP/0.9
checkViolation(Violation.HTTP_0_9);
_requestHandler.startRequest(_methodString, _uri.toCompleteString(), HttpVersion.HTTP_0_9);
setState(State.CONTENT);
_endOfContent = EndOfContent.NO_CONTENT;
BufferUtil.clear(buffer);
handle = handleHeaderContentMessage();
}
break;
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
case REQUEST_VERSION:
switch (t.getType())
{
case LF:
if (_version == null)
{
_length = _string.length();
_version = HttpVersion.CACHE.get(takeString());
}
checkVersion();
_fieldCache.prepare();
setState(State.HEADER);
_requestHandler.startRequest(_methodString, _uri.toCompleteString(), _version);
continue;
case ALPHA:
case DIGIT:
case TCHAR:
case VCHAR:
case COLON:
_string.append(t.getChar());
break;
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
case REASON:
assert !_requestParser;
switch (t.getType())
{
case LF:
String reason = takeString();
_fieldCache.prepare();
setState(State.HEADER);
_responseHandler.startResponse(_version, _responseStatus, reason);
continue;
case ALPHA:
case DIGIT:
case TCHAR:
case VCHAR:
case COLON:
case OTEXT: // TODO should this be UTF8
_string.append(t.getChar());
_length = _string.length();
break;
case SPACE:
case HTAB:
_string.append(t.getChar());
break;
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
default:
throw new IllegalStateException(_state.toString());
}
}
return handle;
}
private void checkVersion()
{
if (_version == null)
throw new HttpException.RuntimeException(HttpStatus.HTTP_VERSION_NOT_SUPPORTED_505, "Unknown Version");
if (_version.getVersion() < 10 || _version.getVersion() > 20)
throw new HttpException.RuntimeException(HttpStatus.HTTP_VERSION_NOT_SUPPORTED_505, "Unsupported Version");
}
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 addToFieldCache = false;
switch (_header)
{
case CONTENT_LENGTH:
if (_hasTransferEncoding)
checkViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH);
long contentLength = convertContentLength(_valueString);
if (_hasContentLength)
{
checkViolation(MULTIPLE_CONTENT_LENGTHS);
if (contentLength != _contentLength)
throw new BadMessageException(MULTIPLE_CONTENT_LENGTHS.getDescription());
}
_hasContentLength = true;
if (_endOfContent != EndOfContent.CHUNKED_CONTENT)
{
_contentLength = contentLength;
_endOfContent = EndOfContent.CONTENT_LENGTH;
}
break;
case TRANSFER_ENCODING:
_hasTransferEncoding = true;
if (_hasContentLength)
checkViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH);
// we encountered another Transfer-Encoding header, but chunked was already set
if (_endOfContent == EndOfContent.CHUNKED_CONTENT)
throw new BadMessageException("Bad Transfer-Encoding, chunked not last");
if (HttpHeaderValue.CHUNKED.is(_valueString))
{
_endOfContent = EndOfContent.CHUNKED_CONTENT;
_contentLength = -1;
}
else
{
List values = new QuotedCSV(_valueString).getValues();
int chunked = -1;
int len = values.size();
for (int i = 0; i < len; i++)
{
if (HttpHeaderValue.CHUNKED.is(values.get(i)))
{
if (chunked != -1)
throw new BadMessageException("Bad Transfer-Encoding, multiple chunked tokens");
chunked = i;
// declared chunked
_endOfContent = EndOfContent.CHUNKED_CONTENT;
_contentLength = -1;
}
// we have a non-chunked token after a declared chunked token
else if (_endOfContent == EndOfContent.CHUNKED_CONTENT)
{
throw new BadMessageException("Bad Transfer-Encoding, chunked not last");
}
}
}
break;
case HOST:
if (_parsedHost != null)
{
if (LOG.isWarnEnabled())
LOG.warn("Encountered multiple `Host` headers. Previous `Host` header already seen as `{}`, new `Host` header has appeared as `{}`", _parsedHost, _valueString);
checkViolation(DUPLICATE_HOST_HEADERS);
}
_parsedHost = _valueString;
if (!(_field instanceof HostPortHttpField) && _valueString != null && !_valueString.isEmpty())
{
if (UNSAFE_HOST_HEADER.isAllowedBy(_complianceMode))
{
_field = new HostPortHttpField(_header,
CASE_SENSITIVE_FIELD_NAME.isAllowedBy(_complianceMode) ? _headerString : _header.asString(),
HostPort.unsafe(_valueString));
}
else
{
_field = new HostPortHttpField(_header,
CASE_SENSITIVE_FIELD_NAME.isAllowedBy(_complianceMode) ? _headerString : _header.asString(),
_valueString);
}
addToFieldCache = _fieldCache.isEnabled();
}
break;
case CONNECTION:
// Don't cache headers if not persistent
if (_field == null)
_field = new HttpField(_header, caseInsensitiveHeader(_headerString, _header.asString()), _valueString);
if (getHeaderCacheSize() > 0 && _field.contains(HttpHeaderValue.CLOSE.asString()))
_fieldCache.setCapacity(-1);
break;
case AUTHORIZATION:
case ACCEPT:
case ACCEPT_CHARSET:
case ACCEPT_ENCODING:
case ACCEPT_LANGUAGE:
case COOKIE:
case CACHE_CONTROL:
case USER_AGENT:
addToFieldCache = _field == null && _fieldCache.cacheable(_header, _valueString);
break;
default:
break;
}
// Cache field?
if (addToFieldCache)
{
if (_field == null)
_field = new HttpField(_header, caseInsensitiveHeader(_headerString, _header.asString()), _valueString);
_fieldCache.add(_field);
}
}
if (LOG.isDebugEnabled())
LOG.debug("parsedHeader({}) header={}, headerString=[{}], valueString=[{}]", _field, _header, _headerString, _valueString);
_handler.parsedHeader(_field != null ? _field : new HttpField(_header, _headerString, _valueString));
}
_headerString = _valueString = null;
_header = null;
_field = null;
}
private void parsedTrailer()
{
// handler last header if any. Delayed to here just in case there was a continuation line (above)
if (_headerString != null || _valueString != null)
_handler.parsedTrailer(_field != null ? _field : new HttpField(_header, _headerString, _valueString));
_headerString = _valueString = null;
_header = null;
_field = null;
}
private long convertContentLength(String valueString)
{
if (valueString == null || valueString.length() == 0)
throw new BadMessageException("Invalid Content-Length Value", new NumberFormatException());
long value = 0;
int length = valueString.length();
for (int i = 0; i < length; i++)
{
char c = valueString.charAt(i);
if (c < '0' || c > '9')
throw new BadMessageException("Invalid Content-Length Value", new NumberFormatException());
value = Math.addExact(Math.multiplyExact(value, 10), c - '0');
}
return value;
}
/*
* Parse the message headers and return true if the handler has signalled for a return
*/
protected boolean parseFields(ByteBuffer buffer)
{
// Process headers
while ((_state == State.HEADER || _state == State.TRAILER) && buffer.hasRemaining())
{
// process each character
HttpTokens.Token t = next(buffer);
if (t == null)
break;
if (_maxHeaderBytes > 0 && ++_headerBytes > _maxHeaderBytes)
{
boolean header = _state == State.HEADER;
LOG.warn("{} is too large {}>{}", header ? "Header" : "Trailer", _headerBytes, _maxHeaderBytes);
throw new BadMessageException(header
? HttpStatus.REQUEST_HEADER_FIELDS_TOO_LARGE_431
: HttpStatus.PAYLOAD_TOO_LARGE_413);
}
switch (_fieldState)
{
case FIELD:
switch (t.getType())
{
case COLON:
case SPACE:
case HTAB:
{
checkViolation(Violation.MULTILINE_FIELD_VALUE);
// header value without name - continuation?
if (StringUtil.isEmpty(_valueString))
{
_string.setLength(0);
_length = 0;
}
else
{
setString(_valueString);
_string.append(' ');
_length++;
_valueString = null;
}
setState(FieldState.VALUE);
break;
}
case LF:
{
// process previous header
if (_state == State.HEADER)
parsedHeader();
else
parsedTrailer();
_contentPosition = 0;
// End of headers or trailers?
if (_state == State.TRAILER)
{
setState(State.END);
return _handler.messageComplete();
}
// We found Transfer-Encoding headers, but none declared the 'chunked' token
if (_hasTransferEncoding && _endOfContent != EndOfContent.CHUNKED_CONTENT)
{
if (_responseHandler == null || _endOfContent != EndOfContent.EOF_CONTENT)
{
// Transfer-Encoding chunked not specified
// https://tools.ietf.org/html/rfc7230#section-3.3.1
throw new BadMessageException("Bad Transfer-Encoding, chunked not last");
}
}
// Was there a required host header?
if (_parsedHost == null && _version == HttpVersion.HTTP_1_1 && _requestParser)
{
throw new BadMessageException("No Host");
}
// is it a response that cannot have a body?
if (!_requestParser && // 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 we don't know framing
else if (_endOfContent == EndOfContent.UNKNOWN_CONTENT)
{
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?
boolean handle = _handler.headerComplete();
_headerComplete = true;
switch (_endOfContent)
{
case EOF_CONTENT -> setState(State.EOF_CONTENT);
case CHUNKED_CONTENT -> setState(State.CHUNKED_CONTENT);
default -> setState(State.CONTENT);
}
return handle;
}
case ALPHA:
case DIGIT:
case TCHAR:
{
// process previous header
if (_state == State.HEADER)
parsedHeader();
else
parsedTrailer();
// handle new header
if (buffer.hasRemaining())
{
// Try a look ahead for the known header name and value in dynamic, then static cache.
// Need to use an offset of -1 and to increase the remaining since we have already consumed
// the first ALPHA/DIGIT/TCHAR byte to switch to this case.
HttpField cachedField = _fieldCache.getBest(buffer, -1, buffer.remaining() + 1);
if (cachedField == null)
cachedField = CACHE.getBest(buffer, -1, buffer.remaining() + 1);
if (cachedField != null)
{
String n = cachedField.getName();
String v = cachedField.getValue();
if (v != UNMATCHED_VALUE)
{
if (CASE_SENSITIVE_FIELD_NAME.isAllowedBy(_complianceMode))
{
// Have to get the fields exactly from the buffer to match case
String en = BufferUtil.toString(buffer, buffer.position() - 1, n.length(), StandardCharsets.US_ASCII);
if (!n.equals(en))
{
reportComplianceViolation(CASE_SENSITIVE_FIELD_NAME, en);
n = en;
cachedField = new HttpField(cachedField.getHeader(), n, v);
}
}
if (isHeaderCacheCaseSensitive())
{
String ev = BufferUtil.toString(buffer, buffer.position() + n.length() + 1, v.length(), StandardCharsets.ISO_8859_1);
if (!v.equals(ev))
{
v = ev;
cachedField = new HttpField(cachedField.getHeader(), n, v);
}
}
}
_header = cachedField.getHeader();
_headerString = n;
int posAfterName = buffer.position() + n.length() + 1;
if (v == UNMATCHED_VALUE || (posAfterName + v.length()) >= buffer.limit())
{
// Header only
setState(FieldState.VALUE);
_string.setLength(0);
_length = 0;
buffer.position(posAfterName);
break;
}
// Header and value
int posAfterValue = posAfterName + v.length();
byte peek = buffer.get(posAfterValue);
if (peek == CARRIAGE_RETURN || peek == LINE_FEED)
{
_field = cachedField;
_valueString = v;
buffer.position(posAfterValue + 1);
if (peek == LINE_FEED)
{
setState(FieldState.FIELD);
break;
}
setState(FieldState.IN_VALUE);
_cr = true;
break;
}
setState(FieldState.IN_VALUE);
setString(v);
buffer.position(posAfterValue);
break;
}
}
// New header
setState(FieldState.IN_NAME);
_string.setLength(0);
_string.append(t.getChar());
_length = 1;
break;
}
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
case IN_NAME:
switch (t.getType())
{
case SPACE:
case HTAB:
//Ignore trailing whitespaces ?
if (WHITESPACE_AFTER_FIELD_NAME.isAllowedBy(_complianceMode))
{
_headerString = takeString();
reportComplianceViolation(WHITESPACE_AFTER_FIELD_NAME, "Space after " + _headerString);
_header = HttpHeader.CACHE.get(_headerString);
_length = -1;
setState(FieldState.WS_AFTER_NAME);
break;
}
throw new IllegalCharacterException(_state, t, buffer);
case COLON:
_headerString = takeString();
_header = HttpHeader.CACHE.get(_headerString);
_length = -1;
setState(FieldState.VALUE);
break;
case LF:
_headerString = takeString();
_header = HttpHeader.CACHE.get(_headerString);
_string.setLength(0);
_valueString = "";
_length = -1;
if (NO_COLON_AFTER_FIELD_NAME.isAllowedBy(_complianceMode))
{
reportComplianceViolation(NO_COLON_AFTER_FIELD_NAME, "Field " + _headerString);
setState(FieldState.FIELD);
break;
}
throw new IllegalCharacterException(_state, t, buffer);
case ALPHA:
case DIGIT:
case TCHAR:
_string.append(t.getChar());
_length = _string.length();
break;
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
case WS_AFTER_NAME:
switch (t.getType())
{
case SPACE:
case HTAB:
break;
case COLON:
setState(FieldState.VALUE);
break;
case LF:
if (NO_COLON_AFTER_FIELD_NAME.isAllowedBy(_complianceMode))
{
reportComplianceViolation(NO_COLON_AFTER_FIELD_NAME, "Field " + _headerString);
setState(FieldState.FIELD);
break;
}
throw new IllegalCharacterException(_state, t, buffer);
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
case VALUE:
switch (t.getType())
{
case LF:
_string.setLength(0);
_valueString = "";
_length = -1;
setState(FieldState.FIELD);
break;
case SPACE:
case HTAB:
break;
case ALPHA:
case DIGIT:
case TCHAR:
case VCHAR:
case COLON:
case OTEXT: // TODO review? should this be a utf8 string?
_string.append(t.getChar());
_length = _string.length();
setState(FieldState.IN_VALUE);
break;
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
case IN_VALUE:
switch (t.getType())
{
case LF:
if (_length > 0)
{
_valueString = takeString();
_length = -1;
}
setState(FieldState.FIELD);
break;
case SPACE:
case HTAB:
_string.append(t.getChar());
break;
case ALPHA:
case DIGIT:
case TCHAR:
case VCHAR:
case COLON:
case OTEXT: // TODO review? should this be a utf8 string?
_string.append(t.getChar());
_length = _string.length();
break;
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
default:
throw new IllegalStateException(_state.toString());
}
}
return false;
}
/**
* 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 (debugEnabled)
LOG.debug("parseNext s={} {}", _state, BufferUtil.toDetailString(buffer));
try
{
// Start a request/response
if (_state == State.START)
{
_handler.messageBegin();
_version = null;
_method = null;
_methodString = null;
_endOfContent = EndOfContent.UNKNOWN_CONTENT;
_header = null;
if (buffer.hasRemaining())
_beginNanoTime = NanoTime.now(); // TODO #9900 check beginNanoTime's accuracy
quickStart(buffer);
}
// Request/response line
if (_state.ordinal() < State.HEADER.ordinal())
{
if (parseLine(buffer))
return true;
}
// parse headers
if (_state == State.HEADER)
{
if (parseFields(buffer))
return true;
}
// parse content
if (_state.ordinal() >= State.CONTENT.ordinal() && _state.ordinal() < State.TRAILER.ordinal())
{
// Handle HEAD response
if (_responseStatus > 0 && _headResponse)
{
if (_state != State.CONTENT_END)
{
setState(State.CONTENT_END);
return handleContentMessage();
}
else
{
setState(State.END);
return _handler.messageComplete();
}
}
else
{
if (parseContent(buffer))
return true;
}
}
// parse headers
if (_state == State.TRAILER)
{
if (parseFields(buffer))
return true;
}
// handle end states
if (_state == State.END)
{
// Eat CR or LF white space, but not SP.
int whiteSpace = 0;
while (buffer.remaining() > 0)
{
byte b = buffer.get(buffer.position());
if (b != CARRIAGE_RETURN && b != LINE_FEED)
break;
buffer.get();
++whiteSpace;
}
if (debugEnabled && whiteSpace > 0)
LOG.debug("Discarded {} CR or LF characters", whiteSpace);
}
else if (isTerminated())
{
BufferUtil.clear(buffer);
}
// Handle EOF
if (isAtEOF() && !buffer.hasRemaining())
{
switch (_state)
{
case CLOSED:
break;
case END:
case CLOSE:
setState(State.CLOSED);
break;
case EOF_CONTENT:
case TRAILER:
if (_fieldState == FieldState.FIELD)
{
// Be forgiving of missing last CRLF
setState(State.CONTENT_END);
boolean handle = handleContentMessage();
if (handle && _state == State.CONTENT_END)
return true;
setState(State.CLOSED);
return handle;
}
setState(State.CLOSED);
_handler.earlyEOF();
break;
case START:
case CONTENT:
case CHUNKED_CONTENT:
case CHUNK_SIZE:
case CHUNK_PARAMS:
case CHUNK:
setState(State.CLOSED);
_handler.earlyEOF();
break;
default:
if (debugEnabled)
LOG.debug("{} EOF in {}", this, _state);
setState(State.CLOSED);
_handler.badMessage(new BadMessageException(HttpStatus.BAD_REQUEST_400));
break;
}
}
}
catch (Throwable x)
{
BufferUtil.clear(buffer);
HttpException bad = x instanceof HttpException http
? http
: new BadMessageException(_requestParser ? "Bad Request" : "Bad Response", x);
badMessage(bad);
}
return false;
}
protected void badMessage(HttpException x)
{
if (debugEnabled)
LOG.debug("Parse exception: {} for {}", this, _handler, x);
setState(State.CLOSE);
if (_headerComplete)
_handler.earlyEOF();
else
_handler.badMessage(x);
}
protected boolean parseContent(ByteBuffer buffer)
{
int remaining = buffer.remaining();
if (remaining == 0)
{
switch (_state)
{
case CONTENT:
long content = _contentLength - _contentPosition;
if (_endOfContent == EndOfContent.NO_CONTENT || content == 0)
{
setState(State.CONTENT_END);
return handleContentMessage();
}
break;
case CONTENT_END:
setState(_endOfContent == EndOfContent.EOF_CONTENT ? State.CLOSED : State.END);
return _handler.messageComplete();
default:
// No bytes to parse, return immediately.
return false;
}
}
// Handle content.
while (_state.ordinal() < State.TRAILER.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 (_endOfContent == EndOfContent.NO_CONTENT || content == 0)
{
setState(State.CONTENT_END);
return handleContentMessage();
}
else
{
_contentChunk = buffer.asReadOnlyBuffer();
// limit content by expected size if _contentLength is >= 0 (i.e.: not infinite)
if (_contentLength > -1 && 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.CONTENT_END);
return handleContentMessage();
}
}
break;
}
case CHUNKED_CONTENT:
{
HttpTokens.Token t = next(buffer);
if (t == null)
break;
switch (t.getType())
{
case LF:
break;
case DIGIT:
_chunkLength = t.getHexDigit();
_chunkPosition = 0;
setState(State.CHUNK_SIZE);
break;
case ALPHA:
if (t.isHexDigit())
{
_chunkLength = t.getHexDigit();
_chunkPosition = 0;
setState(State.CHUNK_SIZE);
break;
}
throw new IllegalCharacterException(_state, t, buffer);
default:
throw new IllegalCharacterException(_state, t, buffer);
}
break;
}
case CHUNK_SIZE:
{
HttpTokens.Token t = next(buffer);
if (t == null)
break;
switch (t.getType())
{
case LF:
if (_chunkLength == 0)
{
setState(State.TRAILER);
if (_handler.contentComplete())
return true;
}
else
setState(State.CHUNK);
break;
default:
if (t.isHexDigit())
{
if (_chunkLength > MAX_CHUNK_LENGTH)
throw new BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413);
_chunkLength = _chunkLength * 16 + t.getHexDigit();
}
else if (t.getChar() == ';')
{
setState(State.CHUNK_PARAMS);
}
else
{
throw new IllegalCharacterException(_state, t, buffer);
}
}
break;
}
case CHUNK_PARAMS:
{
HttpTokens.Token t = next(buffer);
if (t == null)
break;
switch (t.getType())
{
case LF:
if (_chunkLength == 0)
{
setState(State.TRAILER);
if (_handler.contentComplete())
return true;
}
else
setState(State.CHUNK);
break;
default:
break; // TODO review
}
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 CONTENT_END:
{
setState(_endOfContent == EndOfContent.EOF_CONTENT ? State.CLOSED : State.END);
return _handler.messageComplete();
}
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 (debugEnabled)
LOG.debug("atEOF {}", this);
_eof = true;
}
/**
* Request that the associated data source be closed
*/
public void close()
{
if (debugEnabled)
LOG.debug("close {}", this);
setState(State.CLOSE);
}
public void reset()
{
if (debugEnabled)
LOG.debug("reset {}", this);
// reset state
if (_state == State.CLOSE || _state == State.CLOSED)
return;
setState(State.START);
_endOfContent = EndOfContent.UNKNOWN_CONTENT;
_contentLength = -1;
_hasContentLength = false;
_hasTransferEncoding = false;
_contentPosition = 0;
_responseStatus = 0;
_contentChunk = null;
_headerBytes = 0;
_parsedHost = null;
_headerComplete = false;
}
public void servletUpgrade()
{
setState(State.CONTENT);
_endOfContent = EndOfContent.UNKNOWN_CONTENT;
_contentLength = -1;
}
protected void setState(State state)
{
if (debugEnabled)
{
String info;
switch (state)
{
case SPACE1:
info = _requestHandler == null ? _version.asString() : _methodString;
break;
case SPACE2:
info = _requestHandler == null ? Integer.toString(_responseStatus) : _uri.toCompleteString();
break;
case CONTENT_END:
case TRAILER:
info = Long.toString(_contentPosition);
break;
default:
info = null;
}
if (info == null)
LOG.debug("{} --> {}", _state, state);
else
LOG.debug("{} --> {}({})", _state, state, info);
}
_state = state;
}
protected void setState(FieldState state)
{
if (debugEnabled)
{
if (state != FieldState.FIELD)
LOG.debug("{}:{} --> {}", _state, _fieldState, state);
else
LOG.debug("{}:{} --> {}({}: {})", _state, _fieldState, state, _field != null ? _field : _headerString, _valueString);
}
_fieldState = state;
}
public Index getFieldCache()
{
_fieldCache.prepare();
return _fieldCache.getCache();
}
@Override
public String toString()
{
return String.format("%s{s=%s,%d of %d}",
getClass().getSimpleName(),
_state,
getContentRead(),
getContentLength());
}
/* 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
{
default void messageBegin() {}
boolean content(ByteBuffer item);
boolean headerComplete();
boolean contentComplete();
boolean messageComplete();
/**
* This is the method called by parser when an HTTP Header name and value is found
*
* @param field The field parsed
*/
void parsedHeader(HttpField field);
/**
* This is the method called by parser when an HTTP Trailer name and value is found
*
* @param field The field parsed
*/
default void parsedTrailer(HttpField field)
{
}
/**
* Called to signal that an EOF was received unexpectedly
* during the parsing of an HTTP message
*/
void earlyEOF();
/**
* Called to indicate that a {@link ComplianceViolation} has occurred.
* @param event the Compliance Violation event
*/
default void onViolation(ComplianceViolation.Event event)
{
}
/**
* Called to signal that a bad HTTP message has been received.
*
* @param failure the failure with the bad message information
*/
default void badMessage(HttpException failure)
{
}
}
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
*/
void 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
*/
void startResponse(HttpVersion version, int status, String reason);
}
private static class IllegalCharacterException extends BadMessageException
{
private IllegalCharacterException(State state, HttpTokens.Token token, ByteBuffer buffer)
{
super(String.format("Illegal character %s", token));
if (LOG.isDebugEnabled())
LOG.debug(String.format("Illegal character %s in state=%s for buffer %s", token, state, BufferUtil.toDetailString(buffer)));
}
}
private static class FieldCache
{
private int _size = 1024;
private Index.Mutable _cache;
private List _cacheableFields;
private boolean _caseSensitive;
public int getCapacity()
{
return _size;
}
public void setCapacity(int size)
{
_size = size;
if (_size <= 0)
_cache = NO_CACHE;
else
_cache = null;
}
public boolean isCaseSensitive()
{
return _caseSensitive;
}
public void setCaseSensitive(boolean caseSensitive)
{
_caseSensitive = caseSensitive;
}
public boolean isEnabled()
{
return _cache != NO_CACHE;
}
public Index getCache()
{
return _cache;
}
public HttpField getBest(ByteBuffer buffer, int i, int remaining)
{
Index.Mutable cache = _cache;
return cache == null ? null : _cache.getBest(buffer, i, remaining);
}
public void add(HttpField field)
{
if (_cache == null)
{
if (_cacheableFields == null)
_cacheableFields = new ArrayList<>();
_cacheableFields.add(field);
}
else if (!_cache.put(field))
{
_cache.clear();
_cache.put(field);
}
}
public boolean cacheable(HttpHeader header, String valueString)
{
return isEnabled() && header != null && valueString != null && valueString.length() <= _size;
}
private void prepare()
{
if (_cache == null && _cacheableFields != null)
{
_cache = Index.buildMutableVisibleAsciiAlphabet(_caseSensitive, _size);
for (HttpField f : _cacheableFields)
{
if (!_cache.put(f))
break;
}
_cacheableFields.clear();
_cacheableFields = null;
}
}
}
}