Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ build/

Main.java
keystore.p12
public
public
development/
random.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,20 @@ class HttpConnectionHandler implements ConnectionHandler {
final Router router;
final HttpParser parser;
private int maxEmptyLines = 10;
private int maxBodySize = 10 * 1024 * 1024;
private long maxBodySize = 10 * 1024 * 1024;
private int maxHeaderCount = 100;
private int maxLineSize = 8192; // 8 KB Limit
private int maxHeaderSize = 8192; // 8 KB Limit
private String staticFilesPath = "./public";
private long maxChunkSize = 1024 * 1024; // 1 MB Limit
private long maxChunkCount = 1000;

/**
* The constructor for HttpConnectionHandler.
*
* @param router the router for HTTP Handler
* @param parser the parser for HTTP Parser
*/
public HttpConnectionHandler(Router router, HttpParser parser) {
this.router = router;
this.parser = parser;
Expand All @@ -36,24 +44,39 @@ public void handle(Socket client) {
BufferedInputStream inputStream = new BufferedInputStream(client.getInputStream());
OutputStream outputStream = client.getOutputStream()) {
client.setSoTimeout(5000);
Response res = new Response(outputStream, staticFilesPath);

Response res = new Response(outputStream, staticFilesPath, false); // At first, it is false,
// Because we won't continue sending responses after the error
try {
Request req;
while ((req = parser.parseRequest(this, inputStream, res)) != null) {
res = new Response(outputStream, staticFilesPath);
res = new Response(outputStream, staticFilesPath, req.isKeepAlive()); // Now when we respond, that's when the isKeepAlive is needed
client.setSoTimeout(20000);
router.handle(req, res);
if (!res.isSent()) {
res.status(404).send("404 - Not Found");
if (!res.isSent()){
if (res.isChunked()){
res.finishChunkedResponse();
}
else {
res.status(404).send("404 - Not Found");
}
}
try {
if (!req.isCached()) {
inputStream.skipNBytes(req.contentLength() - req.bytesRead());
if (!req.isCached()){
if (req.isChunked()){
req.body();
}
else {
inputStream.skipNBytes(req.contentLength() - req.bytesRead());
}
}
} catch (EOFException e) {
break;
}

if (!req.isKeepAlive()) {
break;
}

}
} catch (IllegalArgumentException e) {
res.status(400).send("400 - Bad Request (Malformed URL)");
Expand All @@ -64,6 +87,8 @@ public void handle(Socket client) {
} catch (Exception e) {
res.status(500).send("500 - Internal Server Error");
throw e;
} finally {
res.finishChunkedResponse();
}

} catch (Exception e) {
Expand Down Expand Up @@ -95,7 +120,7 @@ public void setMaxEmptyLines(int maxEmptyLines) {
*
* @return return the max body size in a request
*/
int getMaxBodySize() {
long getMaxBodySize() {
return maxBodySize;
}

Expand All @@ -104,7 +129,7 @@ int getMaxBodySize() {
*
* @param maxBodySize the max body size
*/
public void setMaxBodySize(int maxBodySize) {
public void setMaxBodySize(long maxBodySize) {
this.maxBodySize = maxBodySize;
}

Expand Down Expand Up @@ -162,6 +187,22 @@ public void setMaxHeaderSize(int maxHeaderSize) {
this.maxHeaderSize = maxHeaderSize;
}

long getMaxChunkSize() {
return maxChunkSize;
}

public void setMaxChunkSize(int maxChunkSize) {
this.maxChunkSize = maxChunkSize;
}

long getMaxChunkCount() {
return maxChunkCount;
}

public void setMaxChunkCount(long maxChunkCount) {
this.maxChunkCount = maxChunkCount;
}

/**
* Sets the path/directory in which all static files are set.
*
Expand Down
108 changes: 80 additions & 28 deletions src/main/java/io/github/bernardusz/levtus/engine/HttpParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,15 @@ class HttpParser {
*
* @param handler The handler that uses HttpParser for parsing
* @param inputStream The stream to read the HTTP request from
* @param response The response object to be used for sending responses for accepting 100-Continue request
* @param response The response object to be used for sending responses for accepting 100-Continue
* request
* @return {@link Request} A fully parsed Request object, or null if the stream is empty
* @throws IOException If a network or stream error occurs
* @throws BadRequestException If the request line is invalid or headers are malformed
* @throws PayloadTooLargeException If the request's body size exceeds {@link
* LevtusEngine#getMaxBodySize()} or {@link HttpConnectionHandler#getMaxBodySize()}, set via
* {@link LevtusEngine#setMaxBodySize(int)} or {@link
* HttpConnectionHandler#setMaxBodySize(int)}
* {@link LevtusEngine#setMaxBodySize(long)} or {@link
* HttpConnectionHandler#setMaxBodySize(long)}
* @throws HeaderTooLargeException If the header's total size exceeds {@link
* LevtusEngine#getMaxHeaderSize()} or {@link HttpConnectionHandler#getMaxHeaderSize()}, set
* via {@link LevtusEngine#setMaxHeaderSize(int)} or {@link
Expand All @@ -71,6 +72,7 @@ Request parseRequest(HttpConnectionHandler handler, InputStream inputStream, Res
}

String method = parseMethod(requestLine);
HttpProtocol protocol = parseHttpProtocol(requestLine);

// Parse the Header
Map<String, List<String>> headers =
Expand Down Expand Up @@ -105,7 +107,16 @@ Request parseRequest(HttpConnectionHandler handler, InputStream inputStream, Res
validateBodySize(headers, limit, response);

return new Request(
method, normalizedPath, headers, queryParams, inputStream, handler.getMaxBodySize());
method,
normalizedPath,
headers,
queryParams,
inputStream,
handler.getMaxBodySize(),
handler.getMaxChunkSize(),
handler.getMaxChunkCount(),
protocol
);
}

/**
Expand Down Expand Up @@ -143,7 +154,7 @@ String parseRequestLine(InputStream inputStream, int maxLineSize, int maxEmptyLi
String parseMethod(String requestLine) throws BadRequestException {
String[] parts = requestLine.split(" ", 3);
if (parts.length != 3) {
throw new BadRequestException("400 - Bad Request");
throw new BadRequestException("400 - Bad Request (Invalid request line)");
}

if (!parts[2].matches("HTTP/1\\.[01]")) {
Expand All @@ -157,6 +168,32 @@ String parseMethod(String requestLine) throws BadRequestException {
return parts[0];
}

/**
* The helper method for parsing the protocol of an HTTP request.
*
* @param requestLine the parsed request line
* @return the HttpProtocol in the form of an enum {@link HttpProtocol}
* @throws BadRequestException if the request line is invalid or the HTTP version is not supported
* @throws LevtusNotImplementedException if the HTTP version is not supported
*/
HttpProtocol parseHttpProtocol(String requestLine) throws BadRequestException, LevtusNotImplementedException {
String[] parts = requestLine.split(" ", 3);
if (parts.length != 3) {
throw new BadRequestException("400 - Bad Request (Invalid request line)");
}

if (!parts[2].matches("HTTP/1\\.[01]")) {
if (parts[2].matches("HTTP/[0-9]+\\.[0-9]+")) {
throw new LevtusNotImplementedException("505 - Unsupported HTTP version");
} else {
throw new BadRequestException("400 - Bad Request (Invalid HTTP version)");
}
}

return parts[2].matches("HTTP/1.1") ? HttpProtocol.HTTP_1_1 : HttpProtocol.HTTP_1_0;
}


/**
* The helper method for parsing the headers of an HTTP request.
*
Expand Down Expand Up @@ -204,9 +241,6 @@ Map<String, List<String>> parseHeaders(
if (headers.get("host").size() > 1) {
throw new BadRequestException("400 - Bad Request (Duplicate host header)");
}
if (headers.get("transfer-encoding") != null) {
throw new LevtusNotImplementedException(headers.get("transfer-encoding").getFirst());
}

return headers;
}
Expand All @@ -230,30 +264,48 @@ Map<String, List<String>> parseHeaders(
*/
void validateBodySize(Map<String, List<String>> headers, long maxBodySize, Response response)
throws PayloadTooLargeException, BadRequestException {
int contentLength;
List<String> lengthStrList =
headers.getOrDefault("content-length", new ArrayList<>(List.of("0")));
if (lengthStrList.size() > 1) {
throw new BadRequestException("400 - Bad Request (Multiple content-length headers)");
}
String lengthStr = lengthStrList.getFirst();
if (lengthStr != null && !lengthStr.isEmpty()) {
try {
contentLength = Integer.parseInt(lengthStr);
if (contentLength > maxBodySize) {
throw new PayloadTooLargeException(
"Payload Too Large: " + contentLength + " exceeds limit of " + maxBodySize);
List<String> chunkedHeaders = headers.get("transfer-encoding");

boolean isChunked =
chunkedHeaders != null
&& !chunkedHeaders.isEmpty()
&& chunkedHeaders.stream().anyMatch("chunked"::equalsIgnoreCase);

if (isChunked) {
headers.remove("content-length");
} else {
int contentLength;
List<String> lengthStrList =
headers.getOrDefault("content-length", new ArrayList<>(List.of("0")));

if (lengthStrList != null) {
if (lengthStrList.isEmpty()) {
throw new BadRequestException("400 - Bad Request (Missing content-length header)");
}

if (lengthStrList.size() > 1) {
throw new BadRequestException("400 - Bad Request (Multiple content-length headers)");
}
List<String> expectHeaders = headers.get("expect");
if (expectHeaders != null
&& !expectHeaders.isEmpty()
&& expectHeaders.getFirst().equalsIgnoreCase("100-continue")) {
response.status(100).send();
String lengthStr = lengthStrList.getFirst();
try {
contentLength = Integer.parseInt(lengthStr);
if (contentLength > maxBodySize) {
throw new PayloadTooLargeException(
"Payload Too Large: " + contentLength + " exceeds limit of " + maxBodySize);
}

} catch (NumberFormatException e) {
throw new BadRequestException("400 - Bad Request (Content Length is invalid)");
}
} catch (NumberFormatException e) {
throw new BadRequestException("400 - Bad Request (Content Length is invalid)");
}
}
List<String> expectHeaders = headers.get("expect");

if (expectHeaders != null
&& !expectHeaders.isEmpty()
&& expectHeaders.getFirst().equalsIgnoreCase("100-continue")) {
response.status(100).send();
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.github.bernardusz.levtus.engine;

/**
* Enum representing the HTTP protocol versions supported by Levtus.
*/
public enum HttpProtocol {
HTTP_1_1,
HTTP_1_0
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ public void setMaxEmptyLines(int maxEmptyLines) {
*
* @return return the max body size in a request
*/
int getMaxBodySize() {
long getMaxBodySize() {
return handler.getMaxBodySize();
}

Expand All @@ -130,7 +130,7 @@ int getMaxBodySize() {
*
* @param maxBodySize the max body size
*/
public void setMaxBodySize(int maxBodySize) {
public void setMaxBodySize(long maxBodySize) {
handler.setMaxBodySize(maxBodySize);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.github.bernardusz.levtus.exception.developer;

/**
* Developer Exception for Chunked Transfer Encoding
*
* <p>Primarily used internally by the Levtus engine during the HTTP parsing phase. When developer switch the transfer mode to chunked when they had already used normal/bulk mode</p>
*/
public class ChunkedTransferException extends DeveloperException {
/**
* Constructs a new ChunkedTransferException with the specified message.
*
* @param message the message to be passed to the superclass constructor
*/
public ChunkedTransferException(String message) {
super(message);
}

/**
* Construct a new ChunkedTransferException with the specified message and the cause of Exception
*
* @param message the message to be passed to superclass constructor
* @param cause the cause of the exception
*/
public ChunkedTransferException(String message, Throwable cause){
super(message, cause);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Thrown when an unrecoverable I/O error occurs within the Levtus framework. This is a runtime
* exception to keep the framework API fluent and boilerplate-free.
*/
public class LevtusIOException extends RuntimeException {
public class LevtusIOException extends DeveloperException {

/**
* Constructs a new LevtusIOException with the specified message.
Expand Down
Loading
Loading