/* * Copyright (C) 2015 SoftIndex LLC. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.datakernel.http; import io.datakernel.bytebuf.ByteBuf; import io.datakernel.bytebuf.ByteBufQueue; import io.datakernel.bytebuf.ByteBufStrings; import io.datakernel.eventloop.AsyncTcpSocket; import io.datakernel.eventloop.Eventloop; import io.datakernel.exception.AsyncTimeoutException; import io.datakernel.exception.ParseException; import static io.datakernel.bytebuf.ByteBufStrings.*; import static io.datakernel.http.GzipProcessor.fromGzip; import static io.datakernel.http.HttpHeaders.*; @SuppressWarnings("ThrowableInstanceNeverThrown") public abstract class AbstractHttpConnection implements AsyncTcpSocket.EventHandler { public static final AsyncTimeoutException READ_TIMEOUT_ERROR = new AsyncTimeoutException("HTTP connection read timeout"); public static final AsyncTimeoutException WRITE_TIMEOUT_ERROR = new AsyncTimeoutException("HTTP connection write timeout"); public static final ParseException CLOSED_CONNECTION = new ParseException("HTTP connection unexpectedly closed"); public static final ParseException HEADER_NAME_ABSENT = new ParseException("Header name is absent"); public static final ParseException TOO_BIG_HTTP_MESSAGE = new ParseException("Too big HttpMessage"); public static final ParseException MALFORMED_CHUNK = new ParseException("Malformed chunk"); public static final ParseException TOO_LONG_HEADER = new ParseException("Header line exceeds max header size"); public static final ParseException TOO_MANY_HEADERS = new ParseException("Too many headers"); public static final ParseException UNEXPECTED_READ = new ParseException("Unexpected read data"); public static final int MAX_HEADER_LINE_SIZE = 8 * 1024; // http://stackoverflow.com/questions/686217/maximum-on-http-header-values public static final int MAX_HEADERS = 100; // http://httpd.apache.org/docs/2.2/mod/core.html#limitrequestfields protected static final HttpHeaders.Value CONNECTION_KEEP_ALIVE_HEADER = HttpHeaders.asBytes(CONNECTION, "keep-alive"); protected static final HttpHeaders.Value CONNECTION_CLOSE_HEADER = HttpHeaders.asBytes(CONNECTION, "close"); private static final byte[] CONNECTION_KEEP_ALIVE = encodeAscii("keep-alive"); private static final byte[] TRANSFER_ENCODING_CHUNKED = encodeAscii("chunked"); protected static final int UNKNOWN_LENGTH = -1; protected final Eventloop eventloop; protected final AsyncTcpSocket asyncTcpSocket; protected final ByteBufQueue readQueue = ByteBufQueue.create(); protected boolean keepAlive; protected final ByteBufQueue bodyQueue = ByteBufQueue.create(); protected static final byte NOTHING = 0; protected static final byte END_OF_STREAM = 1; protected static final byte FIRSTLINE = 2; protected static final byte HEADERS = 3; protected static final byte BODY = 4; protected static final byte CHUNK_LENGTH = 5; protected static final byte CHUNK = 6; protected byte reading; protected static final byte[] CONTENT_ENCODING_GZIP = encodeAscii("gzip"); private boolean isGzipped = false; protected boolean remoteExpectsGzip = false; private boolean isChunked = false; private int chunkSize = 0; protected int contentLength; private final int maxHttpMessageSize; private int maxHeaders; private static final int MAX_CHUNK_HEADER_CHARS = 16; private int maxChunkHeaderChars; protected final char[] headerChars; ConnectionsLinkedList pool; AbstractHttpConnection prev; AbstractHttpConnection next; long poolTimestamp; /** * Creates a new instance of AbstractHttpConnection * * @param eventloop eventloop which will handle its I/O operations */ public AbstractHttpConnection(Eventloop eventloop, AsyncTcpSocket asyncTcpSocket, char[] headerChars, int maxHttpMessageSize) { this.eventloop = eventloop; this.headerChars = headerChars; assert headerChars.length >= MAX_HEADER_LINE_SIZE; this.maxHttpMessageSize = maxHttpMessageSize; this.asyncTcpSocket = asyncTcpSocket; reset(); } boolean isClosed() { return pool == null; } public final void close() { if (isClosed()) return; asyncTcpSocket.close(); readQueue.clear(); onClosed(); } abstract protected void onClosed(); protected final void closeWithError(final Exception e) { if (isClosed()) return; asyncTcpSocket.close(); onClosedWithError(e); } @Override public abstract void onClosedWithError(Exception e); protected void reset() { assert eventloop.inEventloopThread(); contentLength = UNKNOWN_LENGTH; isChunked = false; bodyQueue.clear(); } /** * This method is called after reading Http message. * * @param bodyBuf the received message */ protected abstract void onHttpMessage(ByteBuf bodyBuf); private ByteBuf takeHeader() { int offset = 0; for (int i = 0; i < readQueue.remainingBufs(); i++) { ByteBuf buf = readQueue.peekBuf(i); for (int p = buf.readPosition(); p < buf.writePosition(); p++) { if (buf.at(p) == LF) { // check if multiline header(CRLF + 1*(SP|HT)) rfc2616#2.2 if (isMultilineHeader(buf, p)) { preprocessMultiLine(buf, p); continue; } ByteBuf line = readQueue.takeExactSize(offset + p - buf.readPosition() + 1); if (line.readRemaining() >= 2 && line.peek(line.readRemaining() - 2) == CR) { line.moveWritePosition(-2); } else { line.moveWritePosition(-1); } return line; } } offset += buf.readRemaining(); } return null; } private boolean isMultilineHeader(ByteBuf buf, int p) { return p + 1 < buf.writePosition() && (buf.at(p + 1) == SP || buf.at(p + 1) == HT) && isDataBetweenStartAndLF(buf, p); } private boolean isDataBetweenStartAndLF(ByteBuf buf, int p) { return !(p == buf.readPosition() || (p - buf.readPosition() == 1 && buf.at(p - 1) == CR)); } private void preprocessMultiLine(ByteBuf buf, int pos) { buf.array()[pos] = SP; if (buf.at(pos - 1) == CR) { buf.array()[pos - 1] = SP; } } private void onHeader(ByteBuf line) throws ParseException { int pos = line.readPosition(); int hashCode = 1; while (pos < line.writePosition()) { byte b = line.at(pos); if (b == ':') break; if (b >= 'A' && b <= 'Z') b += 'a' - 'A'; hashCode = 31 * hashCode + b; pos++; } check(pos != line.writePosition(), HEADER_NAME_ABSENT); HttpHeader httpHeader = HttpHeaders.of(line.array(), line.readPosition(), pos - line.readPosition(), hashCode); pos++; // RFC 2616, section 19.3 Tolerant Applications while (pos < line.writePosition() && (line.at(pos) == SP || line.at(pos) == HT)) { pos++; } line.readPosition(pos); onHeader(httpHeader, line); } /** * This method is called after receiving the line of the header. * * @param line received line of header. */ protected abstract void onFirstLine(ByteBuf line) throws ParseException; protected void onHeader(HttpHeader header, final ByteBuf value) throws ParseException { assert !isClosed(); assert eventloop.inEventloopThread(); if (header == CONTENT_LENGTH) { contentLength = ByteBufStrings.decodeDecimal(value.array(), value.readPosition(), value.readRemaining()); if (contentLength > maxHttpMessageSize) { value.recycle(); throw TOO_BIG_HTTP_MESSAGE; } } else if (header == CONNECTION) { keepAlive = equalsLowerCaseAscii(CONNECTION_KEEP_ALIVE, value.array(), value.readPosition(), value.readRemaining()); } else if (header == TRANSFER_ENCODING) { isChunked = equalsLowerCaseAscii(TRANSFER_ENCODING_CHUNKED, value.array(), value.readPosition(), value.readRemaining()); } else if (header == CONTENT_ENCODING) { isGzipped = equalsLowerCaseAscii(CONTENT_ENCODING_GZIP, value.array(), value.readPosition(), value.readRemaining()); } else if (header == ACCEPT_ENCODING) { remoteExpectsGzip = contains(value, CONTENT_ENCODING_GZIP); } } private boolean contains(ByteBuf value, byte[] bytes) { int pos = value.readPosition(); while (pos < value.writePosition()) { if (value.array()[pos] == bytes[0] && value.readRemaining() >= bytes.length) { if (equalsLowerCaseAscii(bytes, value.array(), pos, bytes.length)) { return true; } else { pos += bytes.length; } } else { pos++; } } return false; } private void readBody() throws ParseException { assert !isClosed(); assert eventloop.inEventloopThread(); if (reading == BODY) { if (contentLength == UNKNOWN_LENGTH) { check(bodyQueue.remainingBytes() + readQueue.remainingBytes() <= maxHttpMessageSize, TOO_BIG_HTTP_MESSAGE); readQueue.drainTo(bodyQueue); return; } int bytesToRead = contentLength - bodyQueue.remainingBytes(); int actualBytes = readQueue.drainTo(bodyQueue, bytesToRead); if (actualBytes == bytesToRead) { // if (!readQueue.isEmpty()) // throw new IllegalStateException("Extra bytes outside of HTTP message"); onCompleteMessage(bodyQueue.takeRemaining()); } } else { assert reading == CHUNK || reading == CHUNK_LENGTH; readChunks(); } } @SuppressWarnings("ConstantConditions") private void readChunks() throws ParseException { assert !isClosed(); assert eventloop.inEventloopThread(); while (!readQueue.isEmpty()) { if (reading == CHUNK_LENGTH) { byte c = readQueue.peekByte(); if (c == SP) { readQueue.getByte(); } else if (c >= '0' && c <= '9') { chunkSize = (chunkSize << 4) + (c - '0'); readQueue.getByte(); } else if (c >= 'a' && c <= 'f') { chunkSize = (chunkSize << 4) + (c - 'a' + 10); readQueue.getByte(); } else if (c >= 'A' && c <= 'F') { chunkSize = (chunkSize << 4) + (c - 'A' + 10); readQueue.getByte(); } else { if (chunkSize != 0) { if (!readQueue.hasRemainingBytes(2)) { break; } byte c1 = readQueue.getByte(); byte c2 = readQueue.getByte(); check(c1 == CR && c2 == LF, MALFORMED_CHUNK); check(bodyQueue.remainingBytes() + contentLength <= maxHttpMessageSize, TOO_BIG_HTTP_MESSAGE); reading = CHUNK; } else { if (!readQueue.hasRemainingBytes(4)) { break; } byte c1 = readQueue.getByte(); byte c2 = readQueue.getByte(); byte c3 = readQueue.getByte(); byte c4 = readQueue.getByte(); check(c1 == CR && c2 == LF && c3 == CR && c4 == LF, MALFORMED_CHUNK); // if (!readQueue.isEmpty()) // throw new IllegalStateException("Extra bytes outside of chunk"); onCompleteMessage(bodyQueue.takeRemaining()); return; } } check(--maxChunkHeaderChars >= 0, MALFORMED_CHUNK); } if (reading == CHUNK) { int read = readQueue.drainTo(bodyQueue, chunkSize); chunkSize -= read; if (chunkSize == 0) { if (!readQueue.hasRemainingBytes(2)) { break; } byte c1 = readQueue.getByte(); byte c2 = readQueue.getByte(); check(c1 == CR && c2 == LF, MALFORMED_CHUNK); reading = CHUNK_LENGTH; maxChunkHeaderChars = MAX_CHUNK_HEADER_CHARS; } } } } private void onCompleteMessage(ByteBuf raw) throws ParseException { if (isGzipped) { if (raw.readRemaining() > 0) { raw = fromGzip(raw); } isGzipped = false; } onHttpMessage(raw); } @Override public final void onRead(ByteBuf buf) { assert eventloop.inEventloopThread(); assert !isClosed(); if (buf != null) readQueue.add(buf); if (reading == NOTHING) { return; } if (reading == END_OF_STREAM && readQueue.hasRemaining()) { closeWithError(UNEXPECTED_READ); return; } if (readQueue.hasRemaining()) { try { doRead(); } catch (ParseException e) { closeWithError(e); } } if ((reading != NOTHING || readQueue.isEmpty()) && !isClosed()) { asyncTcpSocket.read(); } } private void doRead() throws ParseException { if (reading < BODY) { while (true) { assert !isClosed(); assert reading == FIRSTLINE || reading == HEADERS; ByteBuf headerBuf = takeHeader(); if (headerBuf == null) { // states that more bytes are being required check(!readQueue.hasRemainingBytes(MAX_HEADER_LINE_SIZE), TOO_LONG_HEADER); return; } if (!headerBuf.canRead()) { headerBuf.recycle(); if (reading == FIRSTLINE) throw new ParseException("Empty response from server"); if (isChunked) { reading = CHUNK_LENGTH; maxChunkHeaderChars = MAX_CHUNK_HEADER_CHARS; } else reading = BODY; break; } if (reading == FIRSTLINE) { onFirstLine(headerBuf); reading = HEADERS; maxHeaders = MAX_HEADERS; } else { check(--maxHeaders >= 0, TOO_MANY_HEADERS); onHeader(headerBuf); } } } assert !isClosed(); assert reading >= BODY; readBody(); } private static void check(boolean expression, ParseException e) throws ParseException { if (!expression) { throw e; } } @Override public String toString() { return ", socket=" + asyncTcpSocket + ", readQueue=" + readQueue + ", closed=" + isClosed() + ", keepAlive=" + keepAlive + ", bodyQueue=" + bodyQueue + ", reading=" + readingToString(reading) + ", isGzipped=" + isGzipped + ", remoteExpectsGzip=" + remoteExpectsGzip + ", isChunked=" + isChunked + ", chunkSize=" + chunkSize + ", contentLength=" + contentLength + ", poolTimestamp=" + poolTimestamp; } private String readingToString(byte reading) { switch (reading) { case NOTHING: return "NOTHING"; case END_OF_STREAM: return "END_OF_STREAM"; case FIRSTLINE: return "FIRSTLINE"; case HEADERS: return "HEADERS"; case BODY: return "BODY"; case CHUNK_LENGTH: return "CHUNK_LENGTH"; case CHUNK: return "CHUNK"; } return ""; } }