de.mklinger.qetcher.client.jetty.http.HttpParser Maven / Gradle / Ivy
Show all versions of qetcher-client-bundle Show documentation
//
// ========================================================================
// Copyright (c) 1995-2018 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
//
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
//
// You may elect to redistribute this code under either of these licenses.
// ========================================================================
//
package org.eclipse.jetty.http;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import org.eclipse.jetty.http.HttpTokens.EndOfContent;
import org.eclipse.jetty.util.ArrayTernaryTrie;
import org.eclipse.jetty.util.ArrayTrie;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Trie;
import org.eclipse.jetty.util.Utf8StringBuilder;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import static org.eclipse.jetty.http.HttpComplianceSection.MULTIPLE_CONTENT_LENGTHS;
import static org.eclipse.jetty.http.HttpComplianceSection.TRANSFER_ENCODING_WITH_CONTENT_LENGTH;
/* ------------------------------------------------------------ */
/** 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
{
public static final Logger LOG = Log.getLogger(HttpParser.class);
@Deprecated
public final static String __STRICT="org.eclipse.jetty.http.HttpParser.STRICT";
public final static int INITIAL_URI_LENGTH=256;
private final static int MAX_CHUNK_LENGTH=Integer.MAX_VALUE/16-16;
/**
* 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 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,
TRAILER,
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 ComplianceHandler _complianceHandler;
private final int _maxHeaderBytes;
private final HttpCompliance _compliance;
private final EnumSet _compliances;
private HttpField _field;
private HttpHeader _header;
private String _headerString;
private String _valueString;
private int _responseStatus;
private int _headerBytes;
private boolean _host;
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 Utf8StringBuilder _uri=new Utf8StringBuilder(INITIAL_URI_LENGTH); // Tune?
private EndOfContent _endOfContent;
private boolean _hasContentLength;
private long _contentLength = -1;
private long _contentPosition;
private int _chunkLength;
private int _chunkPosition;
private boolean _headResponse;
private boolean _cr;
private ByteBuffer _contentChunk;
private Trie _fieldCache;
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, br"));
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_LANGUAGE,"en-AU,en;q=0.9,it-IT;q=0.8,it;q=0.7,en-GB;q=0.6,en-US;q=0.5"));
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.ACCEPT,"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8"));
CACHE.put(new HttpField(HttpHeader.ACCEPT_RANGES,HttpHeaderValue.BYTES));
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.CACHE_CONTROL,"max-age=0"));
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()
{
Boolean strict = Boolean.getBoolean(__STRICT);
if (strict)
{
LOG.warn("Deprecated property used: "+__STRICT);
return HttpCompliance.LEGACY;
}
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());
}
/* ------------------------------------------------------------------------------- */
@Deprecated
public HttpParser(RequestHandler handler,int maxHeaderBytes,boolean strict)
{
this(handler,maxHeaderBytes,strict?HttpCompliance.LEGACY:compliance());
}
/* ------------------------------------------------------------------------------- */
@Deprecated
public HttpParser(ResponseHandler handler,int maxHeaderBytes,boolean strict)
{
this(handler,maxHeaderBytes,strict?HttpCompliance.LEGACY: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)
{
_handler=requestHandler!=null?requestHandler:responseHandler;
_requestHandler=requestHandler;
_responseHandler=responseHandler;
_maxHeaderBytes=maxHeaderBytes;
_compliance=compliance;
_compliances=compliance.sections();
_complianceHandler=(ComplianceHandler)(_handler instanceof ComplianceHandler?_handler:null);
}
/* ------------------------------------------------------------------------------- */
public HttpHandler getHandler()
{
return _handler;
}
/* ------------------------------------------------------------------------------- */
/** Check RFC compliance violation
* @param violation The compliance section violation
* @return True if the current compliance level is set so as to Not allow this violation
*/
protected boolean complianceViolation(HttpComplianceSection violation)
{
return complianceViolation(violation,null);
}
/* ------------------------------------------------------------------------------- */
/** Check RFC compliance violation
* @param violation The compliance section violation
* @param reason The reason for the violation
* @return True if the current compliance level is set so as to Not allow this violation
*/
protected boolean complianceViolation(HttpComplianceSection violation, String reason)
{
if (_compliances.contains(violation))
return true;
if (reason==null)
reason=violation.description;
if (_complianceHandler!=null)
_complianceHandler.onComplianceViolation(_compliance,violation,reason);
return false;
}
/* ------------------------------------------------------------------------------- */
protected void handleViolation(HttpComplianceSection section,String reason)
{
if (_complianceHandler!=null)
_complianceHandler.onComplianceViolation(_compliance,section,reason);
}
/* ------------------------------------------------------------------------------- */
protected String caseInsensitiveHeader(String orig, String normative)
{
if (_compliances.contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE))
return normative;
if (!orig.equals(normative))
handleViolation(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE,orig);
return orig;
}
/* ------------------------------------------------------------------------------- */
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()0 && (_state == State.HEADER || _state == State.TRAILER))
_headerBytes++;
return next(buffer);
}
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;
}
/* ------------------------------------------------------------------------------- */
/* 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())
{
HttpTokens.Token t = next(buffer);
if (t==null)
break;
switch(t.getType())
{
case ALPHA:
case DIGIT:
case TCHAR:
case VCHAR:
{
_string.setLength(0);
_string.append(t.getChar());
setState(_requestHandler!=null?State.METHOD:State.RESPONSE_VERSION);
return false;
}
case OTEXT:
case SPACE:
case HTAB:
throw new IllegalCharacterException(_state,t,buffer);
default:
break;
}
// 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;
}
/* ------------------------------------------------------------------------------- */
private boolean handleHeaderContentMessage()
{
boolean handle_header = _handler.headerComplete();
_headerComplete = true;
boolean handle_content = _handler.contentComplete();
boolean handle_message = _handler.messageComplete();
return handle_header || handle_content || handle_message;
}
/* ------------------------------------------------------------------------------- */
private boolean handleContentMessage()
{
boolean handle_content = _handler.contentComplete();
boolean handle_message = _handler.messageComplete();
return handle_content || handle_message;
}
/* ------------------------------------------------------------------------------- */
/* Parse a request or response line
*/
private boolean parseLine(ByteBuffer buffer)
{
boolean handle=false;
// Process headers
while (_state.ordinal()0 && ++_headerBytes>_maxHeaderBytes)
{
if (_state==State.URI)
{
LOG.warn("URI is too large >"+_maxHeaderBytes);
throw new BadMessageException(HttpStatus.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_HEADER_FIELDS_TOO_LARGE_431);
}
}
switch (_state)
{
case METHOD:
switch(t.getType())
{
case SPACE:
_length=_string.length();
_methodString=takeString();
if (_compliances.contains(HttpComplianceSection.METHOD_CASE_SENSITIVE))
{
HttpMethod method=HttpMethod.CACHE.get(_methodString);
if (method!=null)
_methodString = method.asString();
}
else
{
HttpMethod method=HttpMethod.INSENSITIVE_CACHE.get(_methodString);
if (method!=null)
{
if (!method.asString().equals(_methodString))
handleViolation(HttpComplianceSection.METHOD_CASE_SENSITIVE,_methodString);
_methodString = method.asString();
}
}
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;
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 (_responseHandler!=null)
{
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 (iHttpTokens.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(HttpStatus.BAD_REQUEST_400,_requestHandler!=null?"No URI":"No Status");
}
break;
case STATUS:
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:
setState(State.HEADER);
handle=_responseHandler.startResponse(_version, _responseStatus, null)||handle;
break;
default:
throw new IllegalCharacterException(_state,t,buffer);
}
break;
case URI:
switch(t.getType())
{
case SPACE:
setState(State.SPACE2);
break;
case LF:
// HTTP/0.9
if (complianceViolation(HttpComplianceSection.NO_HTTP_0_9,"No request version"))
throw new BadMessageException("HTTP/0.9 not supported");
handle=_requestHandler.startRequest(_methodString,_uri.toString(), HttpVersion.HTTP_0_9);
setState(State.END);
BufferUtil.clear(buffer);
handle= handleHeaderContentMessage() || handle;
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());
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=HttpVersion.HTTP_1_1.getVersion() && _handler.getHeaderCacheSize()>0)
{
int header_cache = _handler.getHeaderCacheSize();
_fieldCache=new ArrayTernaryTrie<>(header_cache);
}
setState(State.HEADER);
handle=_requestHandler.startRequest(_methodString,_uri.toString(), _version)||handle;
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:
switch(t.getType())
{
case LF:
String reason=takeString();
setState(State.HEADER);
handle=_responseHandler.startResponse(_version, _responseStatus, reason)||handle;
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 BadMessageException(HttpStatus.BAD_REQUEST_400,"Unknown Version");
if (_version.getVersion()<10 || _version.getVersion()>20)
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Bad 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 add_to_connection_trie=false;
switch (_header)
{
case CONTENT_LENGTH:
if (_hasContentLength)
{
if(complianceViolation(MULTIPLE_CONTENT_LENGTHS))
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,MULTIPLE_CONTENT_LENGTHS.description);
if (convertContentLength(_valueString)!=_contentLength)
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,MULTIPLE_CONTENT_LENGTHS.description);
}
_hasContentLength = true;
if (_endOfContent == EndOfContent.CHUNKED_CONTENT && complianceViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH))
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Bad Content-Length");
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 (_hasContentLength && complianceViolation(TRANSFER_ENCODING_WITH_CONTENT_LENGTH))
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Transfer-Encoding and Content-Length");
if (HttpHeaderValue.CHUNKED.is(_valueString))
{
_endOfContent=EndOfContent.CHUNKED_CONTENT;
_contentLength=-1;
}
else
{
List values = new QuotedCSV(_valueString).getValues();
if (values.size()>0 && HttpHeaderValue.CHUNKED.is(values.get(values.size()-1)))
{
_endOfContent=EndOfContent.CHUNKED_CONTENT;
_contentLength=-1;
}
else if (values.stream().anyMatch(HttpHeaderValue.CHUNKED::is))
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Bad chunking");
}
break;
case HOST:
_host=true;
if (!(_field instanceof HostPortHttpField) && _valueString!=null && !_valueString.isEmpty())
{
_field=new HostPortHttpField(_header,
_compliances.contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE)?_header.asString():_headerString,
_valueString);
add_to_connection_trie=_fieldCache!=null;
}
break;
case CONNECTION:
// Don't cache headers if not persistent
if (HttpHeaderValue.CLOSE.is(_valueString) || new QuotedCSV(_valueString).getValues().stream().anyMatch(HttpHeaderValue.CLOSE::is))
_fieldCache=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=_fieldCache!=null && _field==null;
break;
default: break;
}
if (add_to_connection_trie && !_fieldCache.isFull() && _header!=null && _valueString!=null)
{
if (_field==null)
_field=new HttpField(_header,caseInsensitiveHeader(_headerString,_header.asString()),_valueString);
_fieldCache.put(_field);
}
}
_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)
{
try
{
return Long.parseLong(valueString);
}
catch(NumberFormatException e)
{
LOG.ignore(e);
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Invalid Content-Length 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:
{
if (complianceViolation(HttpComplianceSection.NO_FIELD_FOLDING,_headerString))
throw new BadMessageException(HttpStatus.BAD_REQUEST_400,"Header Folding");
// header value without name - continuation?
if (_valueString==null || _valueString.isEmpty())
{
_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();
}
// 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 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?
switch (_endOfContent)
{
case EOF_CONTENT:
{
setState(State.EOF_CONTENT);
boolean handle=_handler.headerComplete();
_headerComplete=true;
return handle;
}
case CHUNKED_CONTENT:
{
setState(State.CHUNKED_CONTENT);
boolean handle=_handler.headerComplete();
_headerComplete=true;
return handle;
}
case NO_CONTENT:
{
setState(State.END);
return handleHeaderContentMessage();
}
default:
{
setState(State.CONTENT);
boolean handle=_handler.headerComplete();
_headerComplete=true;
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.
HttpField cached_field=_fieldCache==null?null:_fieldCache.getBest(buffer,-1,buffer.remaining());
if (cached_field==null)
cached_field=CACHE.getBest(buffer,-1,buffer.remaining());
if (cached_field!=null)
{
String n = cached_field.getName();
String v = cached_field.getValue();
if (!_compliances.contains(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE))
{
// 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))
{
handleViolation(HttpComplianceSection.FIELD_NAME_CASE_INSENSITIVE,en);
n = en;
cached_field = new HttpField(cached_field.getHeader(),n,v);
}
}
if (v!=null && !_compliances.contains(HttpComplianceSection.CASE_INSENSITIVE_FIELD_VALUE_CACHE))
{
String ev = BufferUtil.toString(buffer,buffer.position()+n.length()+1,v.length(),StandardCharsets.ISO_8859_1);
if (!v.equals(ev))
{
handleViolation(HttpComplianceSection.CASE_INSENSITIVE_FIELD_VALUE_CACHE,ev+"!="+v);
v = ev;
cached_field = new HttpField(cached_field.getHeader(),n,v);
}
}
_header=cached_field.getHeader();
_headerString=n;
if (v==null)
{
// Header only
setState(FieldState.VALUE);
_string.setLength(0);
_length=0;
buffer.position(buffer.position()+n.length()+1);
break;
}
// Header and value
int pos=buffer.position()+n.length()+v.length()+1;
byte peek=buffer.get(pos);
if (peek==HttpTokens.CARRIAGE_RETURN || peek==HttpTokens.LINE_FEED)
{
_field=cached_field;
_valueString=v;
setState(FieldState.IN_VALUE);
if (peek==HttpTokens.CARRIAGE_RETURN)
{
_cr=true;
buffer.position(pos+1);
}
else
buffer.position(pos);
break;
}
setState(FieldState.IN_VALUE);
setString(v);
buffer.position(pos);
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 (!complianceViolation(HttpComplianceSection.NO_WS_AFTER_FIELD_NAME,null))
{
_headerString=takeString();
_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 (!complianceViolation(HttpComplianceSection.FIELD_COLON,_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 (!complianceViolation(HttpComplianceSection.FIELD_COLON,_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 (DEBUG)
LOG.debug("parseNext s={} {}",_state,BufferUtil.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.CONTENT.ordinal() && _state.ordinal()0 && _headResponse)
{
setState(State.END);
return handleContentMessage();
}
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 white space
while (buffer.remaining()>0 && buffer.get(buffer.position())<=HttpTokens.SPACE)
buffer.get();
}
else if (isClose() || isClosed())
{
BufferUtil.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:
case TRAILER:
if (_fieldState==FieldState.FIELD)
{
// Be forgiving of missing last CRLF
setState(State.CLOSED);
return handleContentMessage();
}
setState(State.CLOSED);
_handler.earlyEOF();
break;
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(new BadMessageException(HttpStatus.BAD_REQUEST_400));
break;
}
}
}
catch(BadMessageException x)
{
BufferUtil.clear(buffer);
badMessage(x);
}
catch(Throwable x)
{
BufferUtil.clear(buffer);
badMessage(new BadMessageException(HttpStatus.BAD_REQUEST_400, _requestHandler != null ? "Bad Request" : "Bad Response", x));
}
return false;
}
protected void badMessage(BadMessageException x)
{
if (DEBUG)
LOG.debug("Parse exception: " + this + " for " + _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 && _state==State.CONTENT)
{
long content=_contentLength - _contentPosition;
if (content == 0)
{
setState(State.END);
return handleContentMessage();
}
}
// Handle _content
byte ch;
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 (content == 0)
{
setState(State.END);
return handleContentMessage();
}
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 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;
case SPACE:
setState(State.CHUNK_PARAMS);
break;
default:
if (t.isHexDigit())
{
if (_chunkLength>MAX_CHUNK_LENGTH)
throw new BadMessageException(HttpStatus.PAYLOAD_TOO_LARGE_413);
_chunkLength=_chunkLength * 16 + t.getHexDigit();
}
else
{
setState(State.CHUNK_PARAMS);
}
}
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 CLOSED:
{
BufferUtil.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;
_hasContentLength=false;
_contentPosition=0;
_responseStatus=0;
_contentChunk=null;
_headerBytes=0;
_host=false;
_headerComplete=false;
}
/* ------------------------------------------------------------------------------- */
protected void setState(State state)
{
if (DEBUG)
LOG.debug("{} --> {}",_state,state);
_state=state;
}
/* ------------------------------------------------------------------------------- */
protected void setState(FieldState state)
{
if (DEBUG)
LOG.debug("{}:{} --> {}",_state,_field!=null?_field:_headerString!=null?_headerString:_string,state);
_fieldState=state;
}
/* ------------------------------------------------------------------------------- */
public Trie getFieldCache()
{
return _fieldCache;
}
/* ------------------------------------------------------------------------------- */
@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 contentComplete();
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);
/**
* This is the method called by parser when a HTTP Trailer name and value is found
* @param field The field parsed
*/
public default void parsedTrailer(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 failure the failure with the bad message information
*/
public default void badMessage(BadMessageException failure)
{
badMessage(failure.getCode(), failure.getReason());
}
/**
* @deprecated use {@link #badMessage(BadMessageException)} instead
*/
@Deprecated
public default 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);
}
/* ------------------------------------------------------------------------------- */
/* ------------------------------------------------------------------------------- */
/* ------------------------------------------------------------------------------- */
public interface ComplianceHandler extends HttpHandler
{
@Deprecated
public default void onComplianceViolation(HttpCompliance compliance, HttpCompliance required, String reason) {}
public default void onComplianceViolation(HttpCompliance compliance, HttpComplianceSection violation, String details)
{
onComplianceViolation(compliance,HttpCompliance.requiredCompliance(violation), details);
}
}
/* ------------------------------------------------------------------------------- */
@SuppressWarnings("serial")
private static class IllegalCharacterException extends BadMessageException
{
private IllegalCharacterException(State state,HttpTokens.Token token,ByteBuffer buffer)
{
super(400,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)));
}
}
}