/*
* Copyright 2014 Red Hat, Inc.
*
* 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.nodyn.http;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.CompositeByteBuf;
import io.netty.buffer.Unpooled;
import io.nodyn.CallbackResult;
import io.nodyn.EventSource;
import java.nio.charset.Charset;
import java.util.*;
/**
* @author Bob McWhirter
*/
public class HTTPParser extends EventSource {
public static final String[] METHODS = new String[]{
"DELETE",
"GET",
"HEAD",
"POST",
"PUT",
"CONNECT",
"OPTIONS",
"TRACE",
"COPY",
"LOCK",
"MKCOL",
"MOVE",
"PROPFIND",
"PROPPATCH",
"SEARCH",
"UNLOCK",
"REPORT",
"MKACTIVITY",
"CHECKOUT",
"MERGE",
"MSEARCH",
"NOTIFY",
"SUBSCRIBE",
"UNSUBSCRIBE",
"PATCH",
"PURGE",
};
public static enum Error {
INVALID_EOF_STATE("stream ended at an unexpected time"),
HEADER_OVERFLOW("too many header bytes seen; overflow detected"),
CLOSED_CONNECTION("data received after completed connection: close message"),
INVALID_VERSION("invalid HTTP version"),
INVALID_STATUS("invalid HTTP status code"),
INVALID_METHOD("invalid HTTP method"),
INVALID_URL("invalid URL"),
INVALID_HOST("invalid host"),
INVALID_PORT("invalid port"),
INVALID_PATH("invalid path"),
INVALID_QUERY_STRING("invalid query string"),
INVALID_FRAGMENT("invalid fragment"),
LF_EXPECTED("LF character expected"),
INVALID_HEADER_TOKEN("invalid character in header"),
INVALID_CONTENT_LENGTH("invalid character in content-length header"),
INVALID_CHUNK_SIZE("invalid character in chunk size header"),
INVALID_CONSTANT("invalid constant string"),
INVALID_INTERNAL_STATE("encountered unexpected internal state"),
STRICT("strict mode assertion failed"),
PAUSED("parser is paused"),
UNKNOWN("an unknown error occurred");
private String text;
Error(String text) {
this.text = text;
}
}
private static final Charset UTF8 = Charset.forName("utf8");
private static final Charset ASCII = Charset.forName("us-ascii");
private static enum State {
REQUEST,
RESPONSE,
HEADERS,
BODY,
TRAILERS,
CHUNK_START,
CHUNK_BODY,
CHUNK_END,
}
public static final int REQUEST = 1;
public static final int RESPONSE = 2;
private boolean shouldReinitialize;
private int type;
private State state;
private Error error;
private CompositeByteBuf buf;
// common
private String url;
private int versionMajor;
private int versionMinor;
private Boolean shouldKeepAlive;
// server
private Integer method;
// client
private int statusCode;
private String statusMessage;
private boolean upgrade;
private boolean chunked;
private boolean skipBody;
private int length;
private List<String> headers = new ArrayList<>();
private List<String> trailers = new ArrayList<>();
private Set<String> expectedTrailers = new HashSet<>();
public HTTPParser() {
this.buf = Unpooled.compositeBuffer();
}
public String type() {
if ( this.type == REQUEST ) {
return "*** REQUEST";
} else if ( this.type == RESPONSE ) {
return "*** RESPONSE";
}
return "UNKNOWN";
}
public void reinitialize(int type) {
this.type = type;
if (this.type == REQUEST) {
this.state = State.REQUEST;
} else {
this.state = State.RESPONSE;
}
this.buf.clear();
this.method = null;
this.url = null;
this.versionMajor = 0;
this.versionMinor = 0;
this.headers.clear();
this.trailers.clear();
this.expectedTrailers.clear();
this.shouldKeepAlive = null;
this.chunked = false;
this.skipBody = false;
this.length = Integer.MAX_VALUE;
this.statusCode = 0;
this.statusMessage = "";
this.upgrade = false;
this.shouldReinitialize = false;
}
public Integer getMethod() {
return this.method;
}
public String getUrl() {
return this.url;
}
public int getVersionMajor() {
return this.versionMajor;
}
public int getVersionMinor() {
return this.versionMinor;
}
public int getStatusCode() {
return this.statusCode;
}
public String getStatusMessage() {
return this.statusMessage;
}
public boolean getUpgrade() {
return this.upgrade;
}
public String[] getHeaders() {
return (String[]) this.headers.toArray(new String[this.headers.size()]);
}
public String[] getTrailers() {
return (String[]) this.trailers.toArray(new String[this.headers.size()]);
}
public boolean getShouldKeepAlive() {
if (this.versionMajor == 1 && this.versionMinor == 1) {
if (this.shouldKeepAlive == null) {
return true;
}
return this.shouldKeepAlive;
} else {
return false;
}
}
public void setError(Error error) {
this.error = error;
}
public Error getError() {
return this.error;
}
protected boolean needsEof() {
if (this.type == REQUEST) {
return false;
}
if (((int) (this.statusCode / 100) == 1) ||
this.statusCode == 204 ||
this.statusCode == 304 ||
this.skipBody) {
return false;
}
if ( this.chunked || this.length != Integer.MAX_VALUE ) {
return false;
}
return true;
}
public int execute(ByteBuf buf) {
if (buf.readableBytes() == 0 && needsEof()) {
finish();
}
addBuffer(buf);
int startingLength = this.buf.readableBytes();
LOOP:
while (this.buf.readableBytes() > 0) {
switch (this.state) {
case REQUEST:
if (!readRequestLine()) {
break LOOP;
}
this.state = State.HEADERS;
continue LOOP;
case RESPONSE:
if (!readStatusLine()) {
break LOOP;
}
this.state = State.HEADERS;
continue LOOP;
case HEADERS:
int headerResult = readHeaders();
if (headerResult == 0) {
Object result = emit("headersComplete", CallbackResult.EMPTY_SUCCESS);
this.state = State.BODY;
if (result instanceof Boolean && ((Boolean) result).booleanValue()) {
this.skipBody = true;
}
if ( this.skipBody ) {
finish();
break LOOP;
} else {
if ( this.chunked ) {
this.state = State.BODY;
continue LOOP;
} else if (this.length == 0) {
finish();
break LOOP;
} else if (this.length != Integer.MAX_VALUE) {
this.state = State.BODY;
} else {
if ( this.type == REQUEST || ! needsEof() ) {
finish();
break LOOP;
} else {
this.state = State.BODY;
}
}
}
continue LOOP;
}
break LOOP;
case BODY:
if (this.chunked) {
this.state = State.CHUNK_START;
continue LOOP;
}
ByteBuf body = readBody();
emit("body", CallbackResult.createSuccess(body));
if ( this.length == 0 ) {
finish();
break LOOP;
}
continue LOOP;
case CHUNK_START:
if (!readChunkStart()) {
break LOOP;
}
if (this.length == 0) {
this.state = State.TRAILERS;
} else {
this.state = State.CHUNK_BODY;
}
continue LOOP;
case CHUNK_BODY:
ByteBuf chunkBody = readBody();
emit("body", CallbackResult.createSuccess(chunkBody));
if (this.length == 0) {
this.state = State.CHUNK_END;
}
continue LOOP;
case CHUNK_END:
if (!readChunkEnd()) {
break LOOP;
}
this.state = State.CHUNK_START;
continue LOOP;
case TRAILERS:
int trailerResult = readTrailers();
if (trailerResult == 0) {
finish();
}
break LOOP;
}
}
if (this.error != null) {
return -1 * this.error.ordinal();
}
int endingLength = this.buf.readableBytes();
int numRead = startingLength - endingLength;
if ( this.shouldReinitialize ) {
reinitialize( this.type );
}
return numRead;
}
void addBuffer(ByteBuf buf) {
this.buf.writeBytes(buf);
//this.buf.addComponent( buf );
}
int readableBytes() {
return this.buf.readableBytes();
}
int readerIndex() {
return this.buf.readerIndex();
}
protected ByteBuf readLine() {
int cr = buf.indexOf(readerIndex(), readerIndex() + readableBytes(), (byte) '\r');
if (cr < 0) {
return null;
}
if (buf.getByte(cr + 1) != '\n') {
return null;
}
int len = (cr + 2) - readerIndex();
ByteBuf line = buf.readSlice(len);
return line;
}
protected boolean readRequestLine() {
ByteBuf line = readLine();
if (line == null) {
return false;
}
int space = line.indexOf(line.readerIndex(), line.readerIndex() + line.readableBytes(), (byte) ' ');
if (space < 0) {
setError(Error.INVALID_METHOD);
return false;
}
int len = space - line.readerIndex();
ByteBuf methodBuf = line.readSlice(len);
String methodName = methodBuf.toString(UTF8);
for (int i = 0; i < METHODS.length; ++i) {
if (METHODS[i].equals(methodName)) {
this.method = i;
break;
}
}
if (this.method == null) {
setError(Error.INVALID_METHOD);
return false;
}
if ( "CONNECT".equals( methodName ) ) {
this.upgrade = true;
}
// skip the space
line.readByte();
space = line.indexOf(line.readerIndex(), line.readerIndex() + line.readableBytes(), (byte) ' ');
ByteBuf urlBuf = null;
ByteBuf versionBuf = null;
if (space < 0) {
// HTTP/1.0
urlBuf = line.readSlice(line.readableBytes());
} else {
len = space - line.readerIndex();
urlBuf = line.readSlice(len);
versionBuf = line.readSlice(line.readableBytes());
}
this.url = urlBuf.toString(UTF8).trim();
if (versionBuf != null) {
if (!readVersion(versionBuf)) {
setError(Error.INVALID_VERSION);
return false;
}
} else {
this.versionMajor = 1;
this.versionMinor = 0;
}
return true;
}
protected boolean readStatusLine() {
ByteBuf line = readLine();
if (line == null) {
return false;
}
int space = line.indexOf(line.readerIndex(), line.readerIndex() + line.readableBytes(), (byte) ' ');
if (space < 0) {
setError(Error.INVALID_VERSION);
return false;
}
int len = space - line.readerIndex();
ByteBuf versionBuf = line.readSlice(len);
if (!readVersion(versionBuf)) {
setError(Error.INVALID_VERSION);
return false;
}
// skip space
line.readByte();
space = line.indexOf(line.readerIndex(), line.readerIndex() + line.readableBytes(), (byte) ' ');
if (space < 0) {
setError(Error.INVALID_STATUS);
return false;
}
len = space - line.readerIndex();
ByteBuf statusBuf = line.readSlice(len);
int status = -1;
try {
status = Integer.parseInt(statusBuf.toString(UTF8));
} catch (NumberFormatException e) {
setError(Error.INVALID_STATUS);
return false;
}
if (status > 999 || status < 100) {
setError(Error.INVALID_STATUS);
return false;
}
this.statusCode = status;
// skip space
line.readByte();
ByteBuf messageBuf = line.readSlice(line.readableBytes());
this.statusMessage = messageBuf.toString(UTF8).trim();
return true;
}
protected boolean readVersion(ByteBuf versionBuf) {
int dotLoc = versionBuf.indexOf(versionBuf.readerIndex(), versionBuf.readerIndex() + versionBuf.readableBytes(), (byte) '.');
if (dotLoc < 0) {
return false;
}
char majorChar = (char) versionBuf.getByte(dotLoc - 1);
char minorChar = (char) versionBuf.getByte(dotLoc + 1);
try {
this.versionMajor = Integer.parseInt("" + majorChar);
this.versionMinor = Integer.parseInt("" + minorChar);
} catch (NumberFormatException e) {
return false;
}
return true;
}
protected int readHeaders() {
return readHeaders(this.headers, true);
}
protected int readTrailers() {
return readHeaders(this.trailers, false);
}
protected int readHeaders(List<String> target, boolean analyze) {
while (true) {
ByteBuf line = readLine();
if (line == null) {
// try again next time
return 1;
}
if (line.readableBytes() == 2) {
// end-of-headers
return 0;
}
if (!readHeader(line, target, analyze)) {
setError(Error.INVALID_HEADER_TOKEN);
return -1;
}
}
}
protected boolean readHeader(ByteBuf line, List<String> target, boolean analyze) {
int colonLoc = line.indexOf(line.readerIndex(), line.readerIndex() + line.readableBytes(), (byte) ':');
if (colonLoc < 0) {
// maybe it's a continued header
if ( line.readableBytes() > 1 ) {
char c = (char) line.getByte(0);
if ( c == ' ' || c == '\t' ) {
// it IS a continued header value
int lastIndex = this.headers.size() - 1;
String val = this.headers.get( lastIndex );
val = val + " " + line.toString( ASCII ).trim();
this.headers.set( lastIndex, val );
return true;
}
}
return false;
}
int len = colonLoc - line.readerIndex();
ByteBuf keyBuf = line.readSlice(len);
// skip colon
line.readByte();
ByteBuf valueBuf = line.readSlice(line.readableBytes());
String key = keyBuf.toString(UTF8).trim();
String value = valueBuf.toString(UTF8).trim();
target.add(key);
target.add(value);
if (analyze) {
return analyzeHeader(key.toLowerCase(), value);
}
return true;
}
protected boolean analyzeHeader(String name, String value) {
if ("content-length".equals(name)) {
try {
this.length = Integer.parseInt(value);
} catch (NumberFormatException e) {
setError(Error.INVALID_CONTENT_LENGTH);
return false;
}
} else if ("transfer-encoding".equals(name)) {
if (value.toLowerCase().contains("chunked")) {
this.chunked = true;
}
} else if ("connection".equals(name)) {
if (value.toLowerCase().contains("close")) {
this.shouldKeepAlive = false;
}
} else if ( "upgrade".equals(name) ) {
this.upgrade = true;
}
return true;
}
protected boolean readChunkStart() {
ByteBuf line = readLine();
if (line == null) {
return false;
}
try {
int len = Integer.parseInt(line.toString(UTF8).trim(), 16);
this.length = len;
} catch (NumberFormatException e) {
setError(Error.INVALID_CHUNK_SIZE);
return false;
}
return true;
}
protected boolean readChunkEnd() {
ByteBuf line = readLine();
if (line == null) {
return false;
}
if (line.readableBytes() != 2) {
setError(Error.INVALID_FRAGMENT);
return false;
}
return true;
}
protected ByteBuf readBody() {
ByteBuf data = null;
if (this.buf.readableBytes() <= this.length) {
data = this.buf.readSlice(this.buf.readableBytes());
this.length -= data.readableBytes();
} else {
data = this.buf.readSlice(this.length);
this.length = 0;
}
return data;
}
public void finish() {
if ( this.type == RESPONSE && this.statusCode == 100 ) {
reinitialize(RESPONSE);
return;
}
if ( this.skipBody ) {
return;
}
emit("messageComplete", CallbackResult.EMPTY_SUCCESS);
this.shouldReinitialize = true;
}
}