private void init() { if (isInitialized) return; synchronized (this) { if (isInitialized) return; try { group = AsynchronousChannelGroup.withThreadPool( new ThreadPoolExecutor( config.getAsynchronousCorePoolSize(), config.getAsynchronousMaximumPoolSize(), config.getAsynchronousPoolKeepAliveTime(), TimeUnit.MILLISECONDS, new LinkedTransferQueue<Runnable>(), new ThreadFactory() { @Override public Thread newThread(Runnable r) { return new Thread(r, "firefly asynchronous server thread"); } })); log.info( "create asychronous I/O thread pool. core pool size: {}, max pool size: {}, pool keep alive time: {}ms", config.getAsynchronousCorePoolSize(), config.getAsynchronousMaximumPoolSize(), config.getAsynchronousPoolKeepAliveTime()); EventManager eventManager = new DefaultEventManager(config); worker = new AsynchronousTcpWorker(config, eventManager); } catch (IOException e) { log.error("initialization server channel group error", e); } isInitialized = true; } }
@Override protected BeanDefinition getBeanDefinition(Class<?> c) { if (c.isAnnotationPresent(Controller.class) || c.isAnnotationPresent(Component.class)) { log.info("classes [{}]", c.getName()); return componentParser(c); } else if (c.isAnnotationPresent(Interceptor.class)) { log.info("classes [{}]", c.getName()); return interceptorParser(c); } else return null; }
private void loadParam() { if (!loadParam) { try { loadParam(queryString); String contentType = getContentType(); if (method.equals("POST") && contentType != null && contentType.startsWith("application/x-www-form-urlencoded")) { int contentLength = getContentLength(); byte[] data = new byte[contentLength]; byte[] buf = new byte[1024]; ServletInputStream input = getInputStream(); try { int readBytes = 0; for (int len = 0; (len = input.read(buf)) != -1; ) { System.arraycopy(buf, 0, data, readBytes, len); readBytes += len; if (readBytes >= contentLength) break; } loadParam(new String(data, characterEncoding)); } finally { input.close(); } } } catch (Throwable t) { log.error("load param error", t); } loadParam = true; } }
@Override public void start(String host, int port) { if (config == null) throw new NetException("server config is null"); init(); listen(bind(host, port)); log.info("server start. host: {}, port: {}", host, port); }
private IllegalCharacterException(State state, byte ch, ByteBuffer buffer) { super(400, String.format("Illegal character 0x%X", ch)); // Bug #460642 - don't reveal buffers to end user log.warn( String.format( "Illegal character 0x%X in state=%s for buffer %s", ch, state, BufferUtils.toDetailString(buffer))); }
private AsynchronousServerSocketChannel bind(String host, int port) { AsynchronousServerSocketChannel serverSocketChannel = null; try { serverSocketChannel = AsynchronousServerSocketChannel.open(group); serverSocketChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true); serverSocketChannel.bind(new InetSocketAddress(host, port), BACKLOG); } catch (Exception e) { log.error("ServerSocket bind error", e); } return serverSocketChannel; }
public static String systemPageTemplate(int status, String content) { StringBuilder ret = new StringBuilder(); try { ret.append("<!DOCTYPE html><html><head><title>firefly</title></head><body><h2>HTTP ERROR ") .append(status) .append("</h2><div>") .append(StringUtils.escapeXML(URLDecoder.decode(content, "UTF-8"))) .append("</div><hr/><i><small>firefly framework</small></i></body></html>"); } catch (UnsupportedEncodingException e) { log.error("url decode error", e); } return ret.toString(); }
/** if http method is POST or PUT, when the business process finish, it must close piped stream */ void releaseInputStreamData() { try { if (getContentLength() > 0) { if (bodyPipedStream != null) { bodyPipedStream.close(); } if (multipartFormData != null) { multipartFormData.close(); } } } catch (Throwable t) { log.error("release input stream error", t); } }
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; }
public static void responseSystemPage( HttpServletRequest request, HttpServletResponse response, String charset, int status, String content) { response.setStatus(status); response.setCharacterEncoding(charset); response.setHeader("Content-Type", "text/html; charset=" + charset); PrintWriter writer = null; try { try { writer = response.getWriter(); } catch (Throwable t) { log.error("responseSystemPage error", t); } writer.print(systemPageTemplate(status, content)); } finally { if (writer != null) writer.close(); } }
private void setWebBeanDefinition(AnnotationBeanDefinition beanDefinition, Class<?> c) { beanDefinition.setClassName(c.getName()); String id = getId(c); beanDefinition.setId(id); String[] names = ReflectUtils.getInterfaceNames(c); beanDefinition.setInterfaceNames(names); List<Field> fields = getInjectField(c); beanDefinition.setInjectFields(fields); List<Method> methods = getInjectMethod(c); beanDefinition.setInjectMethods(methods); try { Object object = c.newInstance(); beanDefinition.setObject(object); } catch (Throwable t) { log.error("set web bean error", 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()) { 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; }
@Override public void close() throws IOException { try { flush(); if (!keepAlive) session.close(); } finally { if (!hasSavedAccessLog) { access.info( "{}|{}|{}|{}|{}|{}|{}|{}|{}|{}", session.getSessionId(), Monitor.CONN_COUNT.get(), request.getHeader("X-Forwarded-For"), request.getRemoteAddr(), response.getStatus(), request.getProtocol(), request.getMethod(), request.getRequestURI(), request.getQueryString(), request.getTimeDifference()); hasSavedAccessLog = true; } } }
/* * 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; }
/** * Parse until next Event. * * @param buffer the buffer to parse * @return True if an {@link RequestHandler} method was called and it returned true; */ public boolean parseNext(ByteBuffer buffer) { if (DEBUG) log.debug("parseNext s={} {}", _state, BufferUtils.toDetailString(buffer)); try { // Start a request/response if (_state == State.START) { _version = null; _method = null; _methodString = null; _endOfContent = EndOfContent.UNKNOWN_CONTENT; _header = null; if (quickStart(buffer)) return true; } // Request/response line if (_state.ordinal() >= State.START.ordinal() && _state.ordinal() < State.HEADER.ordinal()) { if (parseLine(buffer)) return true; } // parse headers if (_state.ordinal() >= State.HEADER.ordinal() && _state.ordinal() < State.CONTENT.ordinal()) { if (parseHeaders(buffer)) return true; } // parse content if (_state.ordinal() >= State.CONTENT.ordinal() && _state.ordinal() < State.END.ordinal()) { // Handle HEAD response if (_responseStatus > 0 && _headResponse) { setState(State.END); return _handler.messageComplete(); } else { if (parseContent(buffer)) return true; } } // handle end states if (_state == State.END) { // eat white space while (buffer.remaining() > 0 && buffer.get(buffer.position()) <= HttpTokens.SPACE) buffer.get(); } else if (_state == State.CLOSE) { // Seeking EOF if (BufferUtils.hasContent(buffer)) { // Just ignore data when closed _headerBytes += buffer.remaining(); BufferUtils.clear(buffer); if (_maxHeaderBytes > 0 && _headerBytes > _maxHeaderBytes) { // Don't want to waste time reading data of a closed // request throw new IllegalStateException("too much data seeking EOF"); } } } else if (_state == State.CLOSED) { BufferUtils.clear(buffer); } // Handle EOF if (_eof && !buffer.hasRemaining()) { switch (_state) { case CLOSED: break; case START: setState(State.CLOSED); _handler.earlyEOF(); break; case END: case CLOSE: setState(State.CLOSED); break; case EOF_CONTENT: setState(State.CLOSED); return _handler.messageComplete(); case CONTENT: case CHUNKED_CONTENT: case CHUNK_SIZE: case CHUNK_PARAMS: case CHUNK: setState(State.CLOSED); _handler.earlyEOF(); break; default: if (DEBUG) log.debug("{} EOF in {}", this, _state); setState(State.CLOSED); _handler.badMessage(400, null); break; } } } catch (BadMessageException e) { BufferUtils.clear(buffer); Throwable cause = e.getCause(); boolean stack = log.isDebugEnable() || (!(cause instanceof NumberFormatException) && (cause instanceof RuntimeException || cause instanceof Error)); if (stack) log.warn( "bad HTTP parsed: " + e._code + (e.getReason() != null ? " " + e.getReason() : "") + " for " + _handler, e); else log.warn( "bad HTTP parsed: " + e._code + (e.getReason() != null ? " " + e.getReason() : "") + " for " + _handler); setState(State.CLOSE); _handler.badMessage(e.getCode(), e.getReason()); } catch (NumberFormatException | IllegalStateException e) { BufferUtils.clear(buffer); log.warn("parse exception: {} in {} for {}", e.toString(), _state, _handler); if (DEBUG) log.debug("parse exception", e); switch (_state) { case CLOSED: break; case CLOSE: _handler.earlyEOF(); break; default: setState(State.CLOSE); _handler.badMessage(400, null); } } catch (Exception | Error e) { BufferUtils.clear(buffer); log.warn("parse exception: " + e.toString() + " for " + _handler, e); switch (_state) { case CLOSED: break; case CLOSE: _handler.earlyEOF(); break; default: setState(State.CLOSE); _handler.badMessage(400, null); } } return false; }
/* * 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 = BufferUtils.toString( buffer, buffer.position() - 1, fn.length(), StandardCharsets.US_ASCII); if (fv == null) v = null; else { v = BufferUtils.toString( buffer, buffer.position() + fn.length() + 1, fv.length(), StandardCharsets.ISO_8859_1); field = new HttpField(field.getHeader(), n, v); } } else { n = field.getName(); v = field.getValue(); } _header = field.getHeader(); _headerString = n; if (v == null) { // Header only setState(State.HEADER_VALUE); _string.setLength(0); _length = 0; buffer.position(buffer.position() + n.length() + 1); break; } else { // Header and value int pos = buffer.position() + n.length() + v.length() + 1; byte b = buffer.get(pos); if (b == HttpTokens.CARRIAGE_RETURN || b == HttpTokens.LINE_FEED) { _field = field; _valueString = v; setState(State.HEADER_IN_VALUE); if (b == HttpTokens.CARRIAGE_RETURN) { _cr = true; buffer.position(pos + 1); } else buffer.position(pos); break; } else { setState(State.HEADER_IN_VALUE); setString(v); buffer.position(pos); break; } } } } // New header setState(State.HEADER_IN_NAME); _string.setLength(0); _string.append((char) ch); _length = 1; } } break; case HEADER_IN_NAME: if (ch == HttpTokens.COLON) { if (_headerString == null) { _headerString = takeString(); _header = HttpHeader.CACHE.get(_headerString); } _length = -1; setState(State.HEADER_VALUE); break; } if (ch > HttpTokens.SPACE) { if (_header != null) { setString(_header.asString()); _header = null; _headerString = null; } _string.append((char) ch); if (ch > HttpTokens.SPACE) _length = _string.length(); break; } throw new IllegalCharacterException(_state, ch, buffer); case HEADER_VALUE: if (ch > HttpTokens.SPACE || ch < 0) { _string.append((char) (0xff & ch)); _length = _string.length(); setState(State.HEADER_IN_VALUE); break; } if (ch == HttpTokens.SPACE || ch == HttpTokens.TAB) break; if (ch == HttpTokens.LINE_FEED) { _value = null; _string.setLength(0); _valueString = 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; }
protected void setState(State state) { if (DEBUG) log.debug("{} --> {}", _state, state); _state = state; }
/** Request that the associated data source be closed */ public void close() { if (DEBUG) log.debug("close {}", this); setState(State.CLOSE); }
/** Signal that the associated data source is at EOF */ public void atEOF() { if (DEBUG) log.debug("atEOF {}", this); _eof = true; }
@Override public void dispatcher(HttpServletRequest request, HttpServletResponse response) { String encoding = webContext.getEncoding(); try { request.setCharacterEncoding(encoding); } catch (Throwable t) { log.error("dispatcher error", t); } response.setCharacterEncoding(encoding); String uri = request.getRequestURI(); String prePath = request.getContextPath() + request.getServletPath(); String invokeUri = uri.substring(prePath.length()); String key = request.getMethod() + "@" + invokeUri; String beforeIntercept = "b#" + invokeUri; String afterIntercept = "a#" + invokeUri; Set<MvcMetaInfo> beforeSet = webContext.getBean(beforeIntercept); Set<MvcMetaInfo> afterSet = webContext.getBean(afterIntercept); log.debug("uri map [{}]", key); MvcMetaInfo mvcMetaInfo = webContext.getBean(key); if (mvcMetaInfo != null) { Object ret = null; Object beforeRet = null; // 前置拦截器的返回值 MvcMetaInfo lastBefore = null; // 最后得到的前置拦截器 Object afterRet = null; // 后置拦截器的返回值 MvcMetaInfo lastAfter = null; // 最后得到的后置拦截器 // 前置拦截栈调用 if (beforeSet != null) { for (MvcMetaInfo before : beforeSet) { Object[] beforeP = getParams(request, response, before, null); beforeRet = before.invoke(beforeP); if (beforeRet != null) { lastBefore = before; break; } } } if (beforeRet == null) { // controller调用 Object[] p = getParams(request, response, mvcMetaInfo, null); ret = mvcMetaInfo.invoke(p); // 后置拦截栈调用 if (afterSet != null) { for (MvcMetaInfo after : afterSet) { Object[] afterP = getParams(request, response, after, ret); afterRet = after.invoke(afterP); if (afterRet != null) { lastAfter = after; break; } } } } // 视图渲染 try { if (afterRet != null) { lastAfter.getViewHandle().render(request, response, afterRet); } else if (beforeRet != null) { lastBefore.getViewHandle().render(request, response, beforeRet); } else { mvcMetaInfo.getViewHandle().render(request, response, ret); } } catch (Throwable t) { log.error("dispatcher error", t); } } else { String msg = request.getRequestURI() + " not register"; SystemHtmlPage.responseSystemPage( request, response, webContext.getEncoding(), HttpServletResponse.SC_NOT_FOUND, msg); } }
/** * 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>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>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>If the system property "org.fireflysource.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> * * @see <a href="http://tools.ietf.org/html/rfc7230">RFC 7230</a> */ public class HttpParser { private static Log log = LogFactory.getInstance().getLog("firefly-system"); public static final boolean __STRICT = Boolean.getBoolean("org.fireflysource.http.HttpParser.STRICT"); public static final 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 static final 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 static final EnumSet<State> __idleStates = EnumSet.of(State.START, State.END, State.CLOSE, State.CLOSED); private static final EnumSet<State> __completeStates = EnumSet.of(State.END, State.CLOSE, State.CLOSED); private final boolean DEBUG = log.isDebugEnable(); // 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 static final 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) { 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 = BufferUtils.toString( buffer, buffer.position() - 1, fn.length(), StandardCharsets.US_ASCII); if (fv == null) v = null; else { v = BufferUtils.toString( buffer, buffer.position() + fn.length() + 1, fv.length(), StandardCharsets.ISO_8859_1); field = new HttpField(field.getHeader(), n, v); } } else { n = field.getName(); v = field.getValue(); } _header = field.getHeader(); _headerString = n; if (v == null) { // Header only setState(State.HEADER_VALUE); _string.setLength(0); _length = 0; buffer.position(buffer.position() + n.length() + 1); break; } else { // Header and value int pos = buffer.position() + n.length() + v.length() + 1; byte b = buffer.get(pos); if (b == HttpTokens.CARRIAGE_RETURN || b == HttpTokens.LINE_FEED) { _field = field; _valueString = v; setState(State.HEADER_IN_VALUE); if (b == HttpTokens.CARRIAGE_RETURN) { _cr = true; buffer.position(pos + 1); } else buffer.position(pos); break; } else { setState(State.HEADER_IN_VALUE); setString(v); buffer.position(pos); break; } } } } // New header setState(State.HEADER_IN_NAME); _string.setLength(0); _string.append((char) ch); _length = 1; } } break; case HEADER_IN_NAME: if (ch == HttpTokens.COLON) { if (_headerString == null) { _headerString = takeString(); _header = HttpHeader.CACHE.get(_headerString); } _length = -1; setState(State.HEADER_VALUE); break; } if (ch > HttpTokens.SPACE) { if (_header != null) { setString(_header.asString()); _header = null; _headerString = null; } _string.append((char) ch); if (ch > HttpTokens.SPACE) _length = _string.length(); break; } throw new IllegalCharacterException(_state, ch, buffer); case HEADER_VALUE: if (ch > HttpTokens.SPACE || ch < 0) { _string.append((char) (0xff & ch)); _length = _string.length(); setState(State.HEADER_IN_VALUE); break; } if (ch == HttpTokens.SPACE || ch == HttpTokens.TAB) break; if (ch == HttpTokens.LINE_FEED) { _value = null; _string.setLength(0); _valueString = 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, BufferUtils.toDetailString(buffer)); try { // Start a request/response if (_state == State.START) { _version = null; _method = null; _methodString = null; _endOfContent = EndOfContent.UNKNOWN_CONTENT; _header = null; if (quickStart(buffer)) return true; } // Request/response line if (_state.ordinal() >= State.START.ordinal() && _state.ordinal() < State.HEADER.ordinal()) { if (parseLine(buffer)) return true; } // parse headers if (_state.ordinal() >= State.HEADER.ordinal() && _state.ordinal() < State.CONTENT.ordinal()) { if (parseHeaders(buffer)) return true; } // parse content if (_state.ordinal() >= State.CONTENT.ordinal() && _state.ordinal() < State.END.ordinal()) { // Handle HEAD response if (_responseStatus > 0 && _headResponse) { setState(State.END); return _handler.messageComplete(); } else { if (parseContent(buffer)) return true; } } // handle end states if (_state == State.END) { // eat white space while (buffer.remaining() > 0 && buffer.get(buffer.position()) <= HttpTokens.SPACE) buffer.get(); } else if (_state == State.CLOSE) { // Seeking EOF if (BufferUtils.hasContent(buffer)) { // Just ignore data when closed _headerBytes += buffer.remaining(); BufferUtils.clear(buffer); if (_maxHeaderBytes > 0 && _headerBytes > _maxHeaderBytes) { // Don't want to waste time reading data of a closed // request throw new IllegalStateException("too much data seeking EOF"); } } } else if (_state == State.CLOSED) { BufferUtils.clear(buffer); } // Handle EOF if (_eof && !buffer.hasRemaining()) { switch (_state) { case CLOSED: break; case START: setState(State.CLOSED); _handler.earlyEOF(); break; case END: case CLOSE: setState(State.CLOSED); break; case EOF_CONTENT: setState(State.CLOSED); return _handler.messageComplete(); case CONTENT: case CHUNKED_CONTENT: case CHUNK_SIZE: case CHUNK_PARAMS: case CHUNK: setState(State.CLOSED); _handler.earlyEOF(); break; default: if (DEBUG) log.debug("{} EOF in {}", this, _state); setState(State.CLOSED); _handler.badMessage(400, null); break; } } } catch (BadMessageException e) { BufferUtils.clear(buffer); Throwable cause = e.getCause(); boolean stack = log.isDebugEnable() || (!(cause instanceof NumberFormatException) && (cause instanceof RuntimeException || cause instanceof Error)); if (stack) log.warn( "bad HTTP parsed: " + e._code + (e.getReason() != null ? " " + e.getReason() : "") + " for " + _handler, e); else log.warn( "bad HTTP parsed: " + e._code + (e.getReason() != null ? " " + e.getReason() : "") + " for " + _handler); setState(State.CLOSE); _handler.badMessage(e.getCode(), e.getReason()); } catch (NumberFormatException | IllegalStateException e) { BufferUtils.clear(buffer); log.warn("parse exception: {} in {} for {}", e.toString(), _state, _handler); if (DEBUG) log.debug("parse exception", e); switch (_state) { case CLOSED: break; case CLOSE: _handler.earlyEOF(); break; default: setState(State.CLOSE); _handler.badMessage(400, null); } } catch (Exception | Error e) { BufferUtils.clear(buffer); log.warn("parse exception: " + e.toString() + " for " + _handler, e); switch (_state) { case CLOSED: break; case CLOSE: _handler.earlyEOF(); break; default: setState(State.CLOSE); _handler.badMessage(400, null); } } return false; } protected boolean parseContent(ByteBuffer buffer) { int remaining = buffer.remaining(); if (remaining == 0 && _state == State.CONTENT) { long content = _contentLength - _contentPosition; if (content == 0) { setState(State.END); return _handler.messageComplete(); } } // Handle _content byte ch; while (_state.ordinal() < State.END.ordinal() && remaining > 0) { switch (_state) { case EOF_CONTENT: _contentChunk = buffer.asReadOnlyBuffer(); _contentPosition += remaining; buffer.position(buffer.position() + remaining); if (_handler.content(_contentChunk)) return true; break; case CONTENT: { long content = _contentLength - _contentPosition; if (content == 0) { setState(State.END); return _handler.messageComplete(); } else { _contentChunk = buffer.asReadOnlyBuffer(); // limit content by expected size if (remaining > content) { // We can cast remaining to an int as we know that it is // smaller than // or equal to length which is already an int. _contentChunk.limit(_contentChunk.position() + (int) content); } _contentPosition += _contentChunk.remaining(); buffer.position(buffer.position() + _contentChunk.remaining()); if (_handler.content(_contentChunk)) return true; if (_contentPosition == _contentLength) { setState(State.END); return _handler.messageComplete(); } } break; } case CHUNKED_CONTENT: { ch = next(buffer); if (ch > HttpTokens.SPACE) { _chunkLength = TypeUtils.convertHexDigit(ch); _chunkPosition = 0; setState(State.CHUNK_SIZE); } break; } case CHUNK_SIZE: { ch = next(buffer); if (ch == 0) break; if (ch == HttpTokens.LINE_FEED) { if (_chunkLength == 0) setState(State.CHUNK_END); else setState(State.CHUNK); } else if (ch <= HttpTokens.SPACE || ch == HttpTokens.SEMI_COLON) setState(State.CHUNK_PARAMS); else _chunkLength = _chunkLength * 16 + TypeUtils.convertHexDigit(ch); break; } case CHUNK_PARAMS: { ch = next(buffer); if (ch == HttpTokens.LINE_FEED) { if (_chunkLength == 0) setState(State.CHUNK_END); else setState(State.CHUNK); } break; } case CHUNK: { int chunk = _chunkLength - _chunkPosition; if (chunk == 0) { setState(State.CHUNKED_CONTENT); } else { _contentChunk = buffer.asReadOnlyBuffer(); if (remaining > chunk) _contentChunk.limit(_contentChunk.position() + chunk); chunk = _contentChunk.remaining(); _contentPosition += chunk; _chunkPosition += chunk; buffer.position(buffer.position() + chunk); if (_handler.content(_contentChunk)) return true; } break; } case CHUNK_END: { // TODO handle chunk trailer ch = next(buffer); if (ch == 0) break; if (ch == HttpTokens.LINE_FEED) { setState(State.END); return _handler.messageComplete(); } throw new IllegalCharacterException(_state, ch, buffer); } case CLOSED: { BufferUtils.clear(buffer); return false; } default: break; } remaining = buffer.remaining(); } return false; } public boolean isAtEOF() { return _eof; } /** Signal that the associated data source is at EOF */ public void atEOF() { if (DEBUG) log.debug("atEOF {}", this); _eof = true; } /** Request that the associated data source be closed */ public void close() { if (DEBUG) log.debug("close {}", this); setState(State.CLOSE); } public void reset() { if (DEBUG) log.debug("reset {}", this); // reset state if (_state == State.CLOSE || _state == State.CLOSED) return; setState(State.START); _endOfContent = EndOfContent.UNKNOWN_CONTENT; _contentLength = -1; _contentPosition = 0; _responseStatus = 0; _contentChunk = null; _headerBytes = 0; _host = false; } protected void setState(State state) { if (DEBUG) log.debug("{} --> {}", _state, state); _state = state; } public Trie<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, BufferUtils.toDetailString(buffer))); } } }