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