Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.hyperfoil.http.connection.Http1xResponseHandler Maven / Gradle / Ivy
package io.hyperfoil.http.connection;
import io.hyperfoil.http.api.HttpRequest;
import io.hyperfoil.http.api.HttpConnection;
import io.hyperfoil.http.api.HttpResponseHandlers;
import io.hyperfoil.core.util.Util;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.util.AsciiString;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;
public class Http1xResponseHandler extends BaseResponseHandler {
private static final Logger log = LoggerFactory.getLogger(Http1xResponseHandler.class);
private static final boolean trace = log.isTraceEnabled();
private static final byte CR = 13;
private static final byte LF = 10;
private static final int MAX_LINE_LENGTH = 4096;
private State state = State.STATUS;
private boolean crRead = false;
private int contentLength = -1;
private ByteBuf lastLine;
private int status = 0;
private boolean chunked = false;
private int skipChunkBytes;
private enum State {
STATUS,
HEADERS,
BODY,
TRAILERS
}
Http1xResponseHandler(HttpConnection connection) {
super(connection);
}
@Override
protected boolean isRequestStream(int streamId) {
return true;
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
if (lastLine == null) {
lastLine = ctx.alloc().buffer(MAX_LINE_LENGTH);
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
if (lastLine != null) {
lastLine.release();
lastLine = null;
}
super.channelInactive(ctx);
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) {
if (lastLine != null) {
lastLine.release();
lastLine = null;
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
int readerIndex = buf.readerIndex();
while (true) {
switch (state) {
case STATUS:
readerIndex = readStatus(ctx, buf, readerIndex);
break;
case HEADERS:
readerIndex = readHeaders(ctx, buf, readerIndex);
break;
case BODY:
readerIndex = readBody(ctx, buf, readerIndex);
break;
case TRAILERS:
readerIndex = readTrailers(ctx, buf, readerIndex);
break;
}
if (readerIndex < 0) {
return;
}
}
} else {
log.error("Unexpected message type: {}", msg);
super.channelRead(ctx, msg);
}
}
private int readStatus(ChannelHandlerContext ctx, ByteBuf buf, int readerIndex) {
int lineStartIndex = buf.readerIndex();
for (; readerIndex < buf.writerIndex(); ++readerIndex) {
byte val = buf.getByte(readerIndex);
if (val == CR) {
crRead = true;
} else if (val == LF && crRead) {
crRead = false;
ByteBuf lineBuf = buf;
if (lastLine.isReadable()) {
assert lineStartIndex == buf.readerIndex();
copyLastLine(buf, lineStartIndex, readerIndex);
lineBuf = lastLine;
lineStartIndex = 0;
}
// skip HTTP version
int j = lineStartIndex;
for (; j < lineBuf.writerIndex(); ++j) {
if (lineBuf.getByte(j) == ' ') {
break;
}
}
status = readDecNumber(lineBuf, j);
if (status >= 100 && status < 200 || status == 204 || status == 304) {
contentLength = 0;
}
onStatus(status);
state = State.HEADERS;
lastLine.writerIndex(0);
return readerIndex + 1;
} else {
crRead = false;
}
}
copyLastLine(buf, lineStartIndex, readerIndex);
passFullBuffer(ctx, buf);
return -1;
}
private int readHeaders(ChannelHandlerContext ctx, ByteBuf buf, int readerIndex) throws Exception {
int lineStartIndex = readerIndex;
int lineEndIndex;
for (; readerIndex < buf.writerIndex(); ++readerIndex) {
byte val = buf.getByte(readerIndex);
if (val == CR) {
crRead = true;
} else if (val == LF && crRead) {
crRead = false;
ByteBuf lineBuf;
// lineStartIndex is valid only if lastLine is empty - otherwise we would ignore an incomplete line
// in the buffer
if (readerIndex - lineStartIndex == 1 && lastLine.writerIndex() == 0
|| lastLine.writerIndex() == 1 && readerIndex == buf.readerIndex()) {
// empty line ends the headers
HttpRequest httpRequest = connection.peekRequest(0);
// Unsolicited response 408 may not have a matching request
if (httpRequest != null) {
switch (httpRequest.method) {
case HEAD:
case CONNECT:
contentLength = 0;
chunked = false;
}
}
state = State.BODY;
lastLine.writerIndex(0);
if (contentLength >= 0) {
responseBytes = readerIndex - buf.readerIndex() + contentLength + 1;
}
return readerIndex + 1;
} else if (lastLine.isReadable()) {
copyLastLine(buf, lineStartIndex, readerIndex);
lineBuf = lastLine;
lineEndIndex = lastLine.readableBytes() + readerIndex - lineStartIndex - 1; // account the CR
lineStartIndex = 0;
} else {
lineBuf = buf;
lineEndIndex = readerIndex - 1; // account the CR
}
if (matches(lineBuf, lineStartIndex, HttpHeaderNames.CONTENT_LENGTH)) {
contentLength = readDecNumber(lineBuf, lineStartIndex + HttpHeaderNames.CONTENT_LENGTH.length() + 1);
} else if (matches(lineBuf, lineStartIndex, HttpHeaderNames.TRANSFER_ENCODING)) {
chunked = matches(lineBuf, lineStartIndex + HttpHeaderNames.TRANSFER_ENCODING.length() + 1, HttpHeaderValues.CHUNKED);
skipChunkBytes = 0;
}
int endOfNameIndex = lineStartIndex, startOfValueIndex = lineStartIndex;
for (int i = lineStartIndex + 1; i < lineEndIndex; ++i) {
if (lineBuf.getByte(i) == ':') {
for (endOfNameIndex = i - 1; endOfNameIndex >= lineStartIndex && lineBuf.getByte(endOfNameIndex) == ' '; --endOfNameIndex)
;
for (startOfValueIndex = i + 1; startOfValueIndex < lineEndIndex && lineBuf.getByte(startOfValueIndex) == ' '; ++startOfValueIndex)
;
break;
}
}
onHeaderRead(lineBuf, lineStartIndex, endOfNameIndex + 1, startOfValueIndex, lineEndIndex);
lastLine.writerIndex(0);
lineStartIndex = readerIndex + 1;
} else {
crRead = false;
}
}
copyLastLine(buf, lineStartIndex, readerIndex);
passFullBuffer(ctx, buf);
return -1;
}
private int readBody(ChannelHandlerContext ctx, ByteBuf buf, int readerIndex) throws Exception {
if (chunked) {
int readable = buf.writerIndex() - readerIndex;
if (skipChunkBytes > readable) {
int readableBody = Math.min(skipChunkBytes - 2, readable);
skipChunkBytes -= readable;
onBodyPart(buf, readerIndex, readableBody, false);
passFullBuffer(ctx, buf);
return -1;
} else {
// skipChunkBytes includes the CRLF
onBodyPart(buf, readerIndex, skipChunkBytes - 2, false);
readerIndex += skipChunkBytes;
skipChunkBytes = 0;
return readChunks(ctx, buf, readerIndex);
}
} else if (responseBytes > 0) {
boolean isLastPart = buf.readableBytes() >= responseBytes;
onBodyPart(buf, readerIndex, Math.min(buf.writerIndex(), buf.readerIndex() + responseBytes) - readerIndex, isLastPart);
if (isLastPart) {
reset();
}
handleBuffer(ctx, buf, 0);
return -1;
} else {
// Body length is unknown and it is not chunked => the request is delimited by connection close
// TODO: make sure we invoke this with isLastPart=true once
onBodyPart(buf, readerIndex, buf.writerIndex() - readerIndex, false);
passFullBuffer(ctx, buf);
return -1;
}
}
private int readChunks(ChannelHandlerContext ctx, ByteBuf buf, int readerIndex) {
int lineStartOffset = readerIndex;
for (; readerIndex < buf.writerIndex(); ++readerIndex) {
byte val = buf.getByte(readerIndex);
if (val == CR) {
crRead = true;
} else if (val == LF && crRead) {
try {
ByteBuf lineBuf = buf;
if (lastLine.isReadable()) {
copyLastLine(buf, lineStartOffset, readerIndex);
lineBuf = lastLine;
lineStartOffset = 0;
}
int partSize = readHexNumber(lineBuf, lineStartOffset);
if (partSize == 0) {
onBodyPart(Unpooled.EMPTY_BUFFER, 0, 0, true);
chunked = false;
state = State.TRAILERS;
return readerIndex + 1;
} else if (readerIndex + 3 + partSize < buf.writerIndex()) {
onBodyPart(buf, readerIndex + 1, partSize, false);
readerIndex += partSize; // + 1 from for loop, +2 below
if (buf.getByte(++readerIndex) != CR || buf.getByte(++readerIndex) != LF) {
throw new IllegalStateException("Chunk must end with CRLF!");
}
lineStartOffset = readerIndex + 1;
assert skipChunkBytes == 0;
} else {
onBodyPart(buf, readerIndex + 1, Math.min(buf.writerIndex() - readerIndex - 1, partSize), false);
skipChunkBytes = readerIndex + 3 + partSize - buf.writerIndex();
passFullBuffer(ctx, buf);
return -1;
}
} finally {
crRead = false;
lastLine.writerIndex(0);
}
} else {
crRead = false;
}
}
copyLastLine(buf, lineStartOffset, buf.writerIndex());
passFullBuffer(ctx, buf);
return -1;
}
private int readTrailers(ChannelHandlerContext ctx, ByteBuf buf, int readerIndex) throws Exception {
int lineStartIndex = readerIndex;
for (; readerIndex < buf.writerIndex(); ++readerIndex) {
byte val = buf.getByte(readerIndex);
if (val == CR) {
crRead = true;
} else if (val == LF && crRead) {
if (readerIndex - lineStartIndex == 1 || lastLine.writerIndex() == 1 && readerIndex == buf.readerIndex()) {
// empty line ends the trailers and whole message
responseBytes = readerIndex + 1 - buf.readerIndex();
reset();
handleBuffer(ctx, buf, 0);
return -1;
}
lineStartIndex = readerIndex + 1;
} else {
crRead = false;
}
}
copyLastLine(buf, lineStartIndex, readerIndex);
passFullBuffer(ctx, buf);
return -1;
}
private void reset() {
state = State.STATUS;
status = 0;
chunked = false;
skipChunkBytes = 0;
contentLength = -1;
lastLine.writerIndex(0);
crRead = false;
}
private void copyLastLine(ByteBuf buf, int lineStartOffset, int readerIndex) {
// copy last line (incomplete) to lastLine
int lineBytes = readerIndex - lineStartOffset;
if (lastLine.writerIndex() + lineBytes > lastLine.capacity()) {
throw new IllegalStateException("Too long header line.");
} else if (lineBytes > 0) {
buf.getBytes(lineStartOffset, lastLine, lastLine.writerIndex(), lineBytes);
lastLine.writerIndex(lastLine.writerIndex() + lineBytes);
}
}
private void passFullBuffer(ChannelHandlerContext ctx, ByteBuf buf) {
HttpRequest request = connection.peekRequest(0);
// Note: we cannot reliably know if this is the last part as the body might be delimited by closing the connection.
onRawData(request, buf, false);
onData(ctx, buf);
}
private boolean matches(ByteBuf buf, int bufOffset, AsciiString string) {
bufOffset = skipWhitespaces(buf, bufOffset);
if (bufOffset + string.length() > buf.writerIndex()) {
return false;
}
for (int i = 0; i < string.length(); ++i) {
if (!Util.compareIgnoreCase(buf.getByte(bufOffset + i), string.byteAt(i))) {
return false;
}
}
return true;
}
private int readHexNumber(ByteBuf buf, int index) {
index = skipWhitespaces(buf, index);
int value = 0;
for (; index < buf.writerIndex(); ++index) {
byte b = buf.getByte(index);
int v = toHex((char) b);
if (v < 0) {
if (b != CR) {
log.error("Error reading buffer, starting from {}, current index {} (char: {}), status {}:\n{}",
buf.readerIndex(), index, b, this,
ByteBufUtil.prettyHexDump(buf, buf.readerIndex(), buf.readableBytes()));
throw new IllegalStateException("Part size must be followed by CRLF!");
}
return value;
}
value = value * 16 + v;
}
// we expect that we've read the and we should see them
throw new IllegalStateException();
}
private int toHex(char c) {
if (c >= '0' && c <= '9') {
return c - '0';
} else if (c >= 'a' && c <= 'f') {
return c - 'a' + 10;
} else if (c >= 'A' && c <= 'F') {
return c - 'A' + 10;
} else {
return -1;
}
}
private int readDecNumber(ByteBuf buf, int index) {
index = skipWhitespaces(buf, index);
int value = 0;
for (; index < buf.writerIndex(); ++index) {
byte b = buf.getByte(index);
if (b < '0' || b > '9') {
return value;
}
value = value * 10 + (b - '0');
}
// we expect that we've read the and we should see them
throw new IllegalStateException();
}
private int skipWhitespaces(ByteBuf buf, int index) {
for (; index < buf.writerIndex(); ++index) {
byte b = buf.getByte(index);
if (b != ' ' && b != '\t') break;
}
return index;
}
@Override
public String toString() {
return "Http1xRawBytesHandler{" +
"state=" + state +
", crRead=" + crRead +
", contentLength=" + contentLength +
", status=" + status +
", chunked=" + chunked +
", skipChunkBytes=" + skipChunkBytes +
'}';
}
@Override
protected void onData(ChannelHandlerContext ctx, ByteBuf buf) {
// noop - do not send to upper layers
buf.release();
}
@Override
protected void onStatus(int status) {
HttpRequest request = connection.peekRequest(0);
if (request == null) {
if (HttpResponseStatus.REQUEST_TIMEOUT.code() == status) {
// HAProxy sends 408 when we allocate the connection but do not use it within 10 seconds.
log.debug("Closing connection {} as server timed out waiting for our first request.", connection);
} else {
log.error("Received unsolicited response (status {}) on {}", status, connection);
}
return;
}
if (request.isCompleted()) {
log.trace("Request on connection {} has been already completed (error in handlers?), ignoring", connection);
} else {
HttpResponseHandlers handlers = request.handlers();
request.enter();
try {
handlers.handleStatus(request, status, null); // TODO parse reason
} finally {
request.exit();
}
request.session.proceed();
}
}
@Override
protected void onHeaderRead(ByteBuf buf, int startOfName, int endOfName, int startOfValue, int endOfValue) {
HttpRequest request = connection.peekRequest(0);
if (request == null) {
if (trace) {
String name = Util.toString(buf, startOfName, endOfName - startOfName);
String value = Util.toString(buf, startOfValue, endOfValue - startOfValue);
log.trace("No request, received headers: {}: {}", name, value);
}
} else if (request.isCompleted()) {
log.trace("Request on connection {} has been already completed (error in handlers?), ignoring", connection);
} else {
HttpResponseHandlers handlers = request.handlers();
request.enter();
try {
String name = Util.toString(buf, startOfName, endOfName - startOfName);
String value = Util.toString(buf, startOfValue, endOfValue - startOfValue);
handlers.handleHeader(request, name, value);
} finally {
request.exit();
}
request.session.proceed();
}
}
@Override
protected void onBodyPart(ByteBuf buf, int startOffset, int length, boolean isLastPart) {
if (length < 0 || length == 0 && !isLastPart) {
return;
}
HttpRequest request = connection.peekRequest(0);
// When previous handlers throw an error the request is already completed
if (request != null && !request.isCompleted()) {
HttpResponseHandlers handlers = request.handlers();
request.enter();
try {
handlers.handleBodyPart(request, buf, startOffset, length, isLastPart);
} finally {
request.exit();
}
request.session.proceed();
}
}
@Override
protected void onCompletion(HttpRequest request) {
connection.removeRequest(0, request);
// When previous handlers throw an error the request is already completed
if (!request.isCompleted()) {
request.enter();
try {
request.handlers().handleEnd(request, true);
if (trace) {
log.trace("Completed response on {}", this);
}
} finally {
request.exit();
}
request.session.proceed();
}
assert request.isCompleted();
request.release();
if (trace) {
log.trace("Releasing request");
}
((Http1xConnection) connection).releasePoolAndPulse();
}
}