package org.httpkit.server;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.httpkit.HttpMethod;
import org.httpkit.HttpUtils;
import org.httpkit.HttpVersion;
import org.httpkit.LineReader;
import org.httpkit.LineTooLargeException;
import org.httpkit.ProtocolException;
import org.httpkit.RequestTooLargeException;
import static org.httpkit.HttpUtils.*;
import static org.httpkit.HttpVersion.HTTP_1_0;
import static org.httpkit.HttpVersion.HTTP_1_1;
public class HttpDecoder {
public enum State {
ALL_READ, CONNECTION_OPEN, READ_INITIAL, READ_HEADER, READ_FIXED_LENGTH_CONTENT, READ_CHUNK_SIZE, READ_CHUNKED_CONTENT, READ_CHUNK_FOOTER, READ_CHUNK_DELIMITER,
}
/**
* Pattern for matching numbers 0 to 255. We use this in the IPV4 address pattern to prevent invalid sequences
* from be parsed by InetAddress.getByName and thus being treated as a name instead of an address.
*/
private static final String IPV4SEG = "(?:0|1\\d{0,2}|2(?:[0-4]\\d*|5[0-5]?|[6-9])?|[3-9]\\d?)";
private static final String IPV4ADDR = IPV4SEG + "(?:\\." + IPV4SEG + "){3}";
/**
* Pattern for a port number. We are not as strict in our pattern matching as we are with ipv4 address
* and instead rely upon Integer.parseInt and a range check. Port 0 is invalid, so we disallow that here.
*/
private static final String PORT = "[1-9]\\d{0,4}";
/**
* The PROXY protocol header is quite strict in what it allows, specifying for example that only
* a single space character (\x20) is allowed between components.
*/
private static final Pattern PROXY_PATTERN = Pattern.compile(
"PROXY\\x20TCP4\\x20(" + IPV4ADDR + ")\\x20(" + IPV4ADDR +")\\x20(" + PORT + ")\\x20(" + PORT + ")"
);
private State state;
private ProxyProtocolOption proxyProtocolOption;
private int readRemaining = 0; // bytes need read
private int readCount = 0; // already read bytes count
private String xForwardedFor;
private String xForwardedProto;
private int xForwardedPort;
HttpRequest request; // package visible
private Map<String, Object> headers = new TreeMap<String, Object>();
byte[] content;
private final int maxBody;
private final LineReader lineReader;
public HttpDecoder(int maxBody, int maxLine, ProxyProtocolOption proxyProtocolOption) {
this.maxBody = maxBody;
this.lineReader = new LineReader(maxLine);
this.proxyProtocolOption = (proxyProtocolOption == null)
? ProxyProtocolOption.DISABLED : proxyProtocolOption;
this.state = (proxyProtocolOption == ProxyProtocolOption.DISABLED)
? State.READ_INITIAL : State.CONNECTION_OPEN;
}
private boolean parseProxyLine(String line) throws ProtocolException {
// PROXY TCP4 255.255.255.255 255.255.255.255 65535 65535\r\n
if (!line.startsWith("PROXY ")) {
return false;
}
final Matcher m = PROXY_PATTERN.matcher(line);
if (!m.matches()) {
throw new ProtocolException("Unsupported or malformed proxy header: "+line);
}
try {
final InetAddress clientAddr = InetAddress.getByName(m.group(1));
final InetAddress proxyAddr = InetAddress.getByName(m.group(2));
final int clientPort = Integer.parseInt(m.group(3), 10);
final int proxyPort = Integer.parseInt(m.group(4), 10);
if (((clientPort | proxyPort) & ~0xffff) != 0) {
throw new ProtocolException("Invalid port number: "+line);
}
xForwardedFor = clientAddr.getHostAddress();
if (proxyPort == 80) {
xForwardedProto = "http";
} else if (proxyPort == 443) {
xForwardedProto = "https";
}
xForwardedPort = proxyPort;
return true;
} catch (NumberFormatException ex) {
throw new ProtocolException("Malformed port in: "+line);
} catch (UnknownHostException ex) {
throw new ProtocolException("Malformed address in: "+line);
}
}
private void createRequest(String sb) throws ProtocolException {
int aStart;
int aEnd;
int bStart;
int bEnd;
int cStart;
int cEnd;
aStart = findNonWhitespace(sb, 0);
aEnd = findWhitespace(sb, aStart);
bStart = findNonWhitespace(sb, aEnd);
bEnd = findWhitespace(sb, bStart);
cStart = findNonWhitespace(sb, bEnd);
cEnd = findEndOfString(sb, cStart);
if (cStart < cEnd) {
try {
HttpMethod method = HttpMethod.valueOf(sb.substring(aStart, aEnd).toUpperCase());
HttpVersion version = HTTP_1_1;
if ("HTTP/1.0".equals(sb.substring(cStart, cEnd))) {
version = HTTP_1_0;
}
request = new HttpRequest(method, sb.substring(bStart, bEnd), version);
} catch (Exception e) {
throw new ProtocolException("method not understand");
}
} else {
throw new ProtocolException("not http?");
}
}
public boolean requiresContinue() {
String expect = (String) headers.get(EXPECT);
return (request != null && request.version == HTTP_1_1 &&
expect != null && CONTINUE.equalsIgnoreCase(expect));
}
public HttpRequest decode(ByteBuffer buffer) throws LineTooLargeException,
ProtocolException, RequestTooLargeException {
String line;
while (buffer.hasRemaining()) {
switch (state) {
case ALL_READ:
return request;
case CONNECTION_OPEN:
line = lineReader.readLine(buffer);
if (line != null) {
// parseProxyLines returns true if the line parsed
// false if it was not a PROXY line
// or throws ProtocolException, if the PROXY line is malformed or unsupported.
if (parseProxyLine(line)) {
// valid proxy header
state = State.READ_INITIAL;
} else if (proxyProtocolOption == ProxyProtocolOption.OPTIONAL) {
// did not parse as a proxy header, try to create a request from it
// as the READ_INITIAL state would.
createRequest(line);
state = State.READ_HEADER;
} else {
throw new ProtocolException("Expected PROXY header, got: "+line);
}
}
break;
case READ_INITIAL:
line = lineReader.readLine(buffer);
if (line != null) {
createRequest(line);
state = State.READ_HEADER;
}
break;
case READ_HEADER:
readHeaders(buffer);
break;
case READ_CHUNK_SIZE:
line = lineReader.readLine(buffer);
if (line != null) {
readRemaining = getChunkSize(line);
if (readRemaining == 0) {
state = State.READ_CHUNK_FOOTER;
} else {
throwIfBodyIsTooLarge();
if (content == null) {
content = new byte[readRemaining];
} else if (content.length < readCount + readRemaining) {
// *1.3 to protect slow client
int newLength = (int) ((readRemaining + readCount) * 1.3);
content = Arrays.copyOf(content, newLength);
}
state = State.READ_CHUNKED_CONTENT;
}
}
break;
case READ_FIXED_LENGTH_CONTENT:
readFixedLength(buffer);
if (readRemaining == 0) {
finish();
}
break;
case READ_CHUNKED_CONTENT:
readFixedLength(buffer);
if (readRemaining == 0) {
state = State.READ_CHUNK_DELIMITER;
}
break;
case READ_CHUNK_FOOTER:
readEmptyLine(buffer);
finish();
break;
case READ_CHUNK_DELIMITER:
readEmptyLine(buffer);
state = State.READ_CHUNK_SIZE;
break;
}
}
return state == State.ALL_READ ? request : null;
}
private void finish() {
state = State.ALL_READ;
request.setBody(content, readCount);
}
void readEmptyLine(ByteBuffer buffer) {
byte b = buffer.get();
if (b == CR && buffer.hasRemaining()) {
buffer.get(); // should be LF
}
}
void readFixedLength(ByteBuffer buffer) {
int toRead = Math.min(buffer.remaining(), readRemaining);
buffer.get(content, readCount, toRead);
readRemaining -= toRead;
readCount += toRead;
}
private void readHeaders(ByteBuffer buffer) throws LineTooLargeException,
RequestTooLargeException, ProtocolException {
if (proxyProtocolOption == ProxyProtocolOption.OPTIONAL
|| proxyProtocolOption == ProxyProtocolOption.ENABLED) {
headers.put("x-forwarded-for", xForwardedFor);
headers.put("x-forwarded-proto", xForwardedProto);
headers.put("x-forwarded-port", xForwardedPort);
}
String line = lineReader.readLine(buffer);
while (line != null && !line.isEmpty()) {
HttpUtils.splitAndAddHeader(line, headers);
line = lineReader.readLine(buffer);
}
if (line == null) {
return;
}
request.setHeaders(headers);
String te = HttpUtils.getStringValue(headers, TRANSFER_ENCODING);
if (CHUNKED.equals(te)) {
state = State.READ_CHUNK_SIZE;
} else {
String cl = HttpUtils.getStringValue(headers, CONTENT_LENGTH);
if (cl != null) {
try {
readRemaining = Integer.parseInt(cl);
if (readRemaining > 0) {
throwIfBodyIsTooLarge();
content = new byte[readRemaining];
state = State.READ_FIXED_LENGTH_CONTENT;
} else {
state = State.ALL_READ;
}
} catch (NumberFormatException e) {
throw new ProtocolException(e.getMessage());
}
} else {
state = State.ALL_READ;
}
}
}
public void reset() {
state = State.READ_INITIAL;
headers = new TreeMap<String, Object>();
readCount = 0;
content = null;
lineReader.reset();
request = null;
}
private void throwIfBodyIsTooLarge() throws RequestTooLargeException {
if (readCount + readRemaining > maxBody) {
throw new RequestTooLargeException("request body " + (readCount + readRemaining)
+ "; max request body " + maxBody);
}
}
}