Skip to content

Commit

Permalink
Support for HTTP request body
Browse files Browse the repository at this point in the history
  • Loading branch information
incubos authored and Andrei Pangin committed Nov 7, 2017
1 parent 3674cae commit 4f03724
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 12 deletions.
107 changes: 96 additions & 11 deletions src/one/nio/http/HttpSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@
import one.nio.util.Utf8;

import java.io.IOException;
import java.nio.BufferUnderflowException;
import java.util.LinkedList;

public class HttpSession extends Session {
private static final int MAX_HEADERS = 48;
private static final int MAX_FRAGMENT_LENGTH = 2048;
private static final int MAX_PIPELINE_LENGTH = 256;
private static final int HTTP_VERSION_LENGTH = " HTTP/1.0".length();
static final int MAX_REQUEST_BODY_LENGTH = 65536;

protected static final Request FIN = new Request(0, "", false);

Expand All @@ -38,6 +40,7 @@ public class HttpSession extends Session {
protected int fragmentLength;
protected Request parsing;
protected Request handling;
protected int requestBodyOffset = 0;

public HttpSession(Socket socket, HttpServer server) {
super(socket);
Expand Down Expand Up @@ -85,6 +88,11 @@ protected void processRead(byte[] buffer) throws IOException {
log.debug("Bad request", e);
}
sendError(Response.BAD_REQUEST, e.getMessage());
} catch (BufferUnderflowException e) {
if (log.isDebugEnabled()) {
log.debug("Request entity too large", e);
}
sendError(Response.REQUEST_ENTITY_TOO_LARGE, "");
}
}

Expand All @@ -102,35 +110,112 @@ protected void handleSocketClosed() {
}
}

protected int getMaxRequestBodyLength() {
return MAX_REQUEST_BODY_LENGTH;
}

/**
* @return number of consumed bytes
*/
protected int startParsingRequestBody(
final int contentLength,
final byte[] buffer,
final int bufferOffset,
final int bufferLength) throws IOException {
if (contentLength < 0) {
throw new IllegalArgumentException("Negative request Content-Length");
}

if (contentLength > getMaxRequestBodyLength()) {
throw new BufferUnderflowException();
}

final byte[] body = new byte[contentLength];
parsing.setBody(body);

// Start consuming the body
requestBodyOffset = Math.min(bufferLength - bufferOffset, body.length);
System.arraycopy(
buffer,
bufferOffset,
body,
0,
requestBodyOffset);

return requestBodyOffset;
}

protected void handleParsedRequest() throws IOException {
if (handling == null) {
server.handleRequest(handling = parsing, this);
} else if (pipeline.size() < MAX_PIPELINE_LENGTH) {
pipeline.addLast(parsing);
} else {
throw new IOException("Pipeline length exceeded");
}
parsing = null;
requestBodyOffset = 0;
}

protected int processHttpBuffer(byte[] buffer, int length) throws IOException, HttpException {
int lineStart = 0;
for (int i = 0; i < length; i++) {
int i = 0; // Current position in the buffer

if (parsing != null && parsing.getBody() != null) { // Resume consuming request body
final byte[] body = parsing.getBody();
i = Math.min(length, body.length - requestBodyOffset);
System.arraycopy(buffer, 0, body, requestBodyOffset, i);
requestBodyOffset += i;
if (requestBodyOffset < body.length) {
// All the buffer copied to body, but that is not enough -- wait for next data
return length;
} else {
// Process current request
if (closing) {
return i;
} else {
handleParsedRequest();
}
}
}

int lineStart = i;
for (; i < length; i++) {
if (buffer[i] != '\n') continue;

int lineLength = i - lineStart;
if (i > 0 && buffer[i - 1] == '\r') lineLength--;

// Skip '\n'
i++;

if (parsing == null) {
parsing = parseRequest(buffer, lineStart, lineLength);
} else if (lineLength > 0) {
if (parsing.getHeaderCount() < MAX_HEADERS) {
parsing.addHeader(Utf8.read(buffer, lineStart, lineLength));
}
} else {
} else { // Empty line -- there is next request or body of the current request
final String contentLengthValue = parsing.getHeader("Content-Length: ");
if (contentLengthValue != null) { // Start parsing request body
final int contentLength = Integer.valueOf(contentLengthValue);
i += startParsingRequestBody(contentLength, buffer, i, length);
if (requestBodyOffset < parsing.getBody().length) {
// Consumed all the buffer data, but some bytes are still left
return i;
}
}

// Process current request
if (closing) {
return i + 1;
} else if (handling == null) {
server.handleRequest(handling = parsing, this);
} else if (pipeline.size() < MAX_PIPELINE_LENGTH) {
pipeline.addLast(parsing);
return i;
} else {
throw new IOException("Pipeline length exceeded");
handleParsedRequest();
}
parsing = null;
}

lineStart = i + 1;
lineStart = i;
}

return lineStart;
}

Expand Down
3 changes: 2 additions & 1 deletion src/one/nio/http/Request.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,15 @@ public class Request {
private static final byte[] HTTP10_HEADER = Utf8.toBytes(" HTTP/1.0\r\n");
private static final byte[] HTTP11_HEADER = Utf8.toBytes(" HTTP/1.1\r\n");
private static final int PROTOCOL_HEADER_LENGTH = 13;
private static final byte[] EMPTY_BODY = new byte[0];

private int method;
private String uri;
private boolean http11;
private int params;
private int headerCount;
private String[] headers;
private byte[] body;
private byte[] body = EMPTY_BODY;

public Request(int method, String uri, boolean http11) {
this.method = method;
Expand Down
128 changes: 128 additions & 0 deletions test/one/nio/http/HttpRequestBodyTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package one.nio.http;

import one.nio.net.ConnectionString;
import one.nio.server.ServerConfig;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import java.io.IOException;
import java.util.concurrent.ThreadLocalRandom;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;

/**
* Unit tests for client and server support for HTTP request body
*
* @author Vadim Tsesko <mail@incubos.org>
*/
public class HttpRequestBodyTest {
private static final String URL = "http://0.0.0.0:8181";
private static final String ENDPOINT = "/echoBody";

private static HttpServer server;
private static HttpClient client;

@BeforeClass
public static void beforeAll() throws IOException {
server = new TestServer(ServerConfig.from(URL));
server.start();
client = new HttpClient(new ConnectionString(URL));
}

@AfterClass
public static void afterAll() {
client.close();
server.stop();
}

@Test
public void maxPostBody() throws Exception {
final byte[] body = new byte[HttpSession.MAX_REQUEST_BODY_LENGTH];
ThreadLocalRandom.current().nextBytes(body);

final Response response = client.post(ENDPOINT, body);
assertEquals(200, response.getStatus());
assertArrayEquals(body, response.getBody());
}

@Test
public void tooBigPostBody() throws Exception {
final byte[] body = new byte[HttpSession.MAX_REQUEST_BODY_LENGTH + 1];
ThreadLocalRandom.current().nextBytes(body);

final Response response = client.post(ENDPOINT, body);
assertEquals(413, response.getStatus());
}

@Test
public void emptyPostBody() throws Exception {
final Response response = client.post(ENDPOINT);
assertEquals(200, response.getStatus());
assertEquals(0, response.getBody().length);
}

@Test
public void put() throws Exception {
final byte[] body = new byte[HttpSession.MAX_REQUEST_BODY_LENGTH];
ThreadLocalRandom.current().nextBytes(body);

final Response response = client.put(ENDPOINT, body);
assertEquals(200, response.getStatus());
assertArrayEquals(body, response.getBody());
}

@Test
public void tooBigPutBody() throws Exception {
final byte[] body = new byte[HttpSession.MAX_REQUEST_BODY_LENGTH + 1];
ThreadLocalRandom.current().nextBytes(body);

final Response response = client.put(ENDPOINT, body);
assertEquals(413, response.getStatus());
}

@Test
public void emptyPutBody() throws Exception {
final Response response = client.put(ENDPOINT);
assertEquals(200, response.getStatus());
assertEquals(0, response.getBody().length);
}

@Test
public void patch() throws Exception {
final byte[] body = new byte[HttpSession.MAX_REQUEST_BODY_LENGTH];
ThreadLocalRandom.current().nextBytes(body);

final Response response = client.patch(ENDPOINT, body);
assertEquals(200, response.getStatus());
assertArrayEquals(body, response.getBody());
}

@Test
public void tooBigPatchBody() throws Exception {
final byte[] body = new byte[HttpSession.MAX_REQUEST_BODY_LENGTH + 1];
ThreadLocalRandom.current().nextBytes(body);

final Response response = client.patch(ENDPOINT, body);
assertEquals(413, response.getStatus());
}

@Test
public void emptyPatchBody() throws Exception {
final Response response = client.patch(ENDPOINT);
assertEquals(200, response.getStatus());
assertEquals(0, response.getBody().length);
}

public static class TestServer extends HttpServer {
TestServer(ServerConfig config) throws IOException {
super(config);
}

@Path(ENDPOINT)
public Response echoBody(Request request) throws IOException {
return Response.ok(request.getBody());
}
}
}

0 comments on commit 4f03724

Please sign in to comment.