This commit is contained in:
Daniel
2024-11-03 22:36:53 +02:00
parent cabf8283f8
commit 69b7a3596c
3 changed files with 262 additions and 42 deletions

View File

@@ -1,10 +1,15 @@
package io.github.lumijiez.core.http; package io.github.lumijiez.core.http;
import io.github.lumijiez.core.util.UrlParser; import io.github.lumijiez.core.util.UrlParser;
import io.github.lumijiez.core.logging.Logger;
import java.io.BufferedReader; import java.io.BufferedReader;
import java.io.IOException; import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
public class HttpRequest { public class HttpRequest {
@@ -12,26 +17,38 @@ public class HttpRequest {
private String path; private String path;
private String httpVersion; private String httpVersion;
private final Map<String, String> headers; private final Map<String, String> headers;
private final Map<String, List<String>> queryParams;
private UrlParser urlParser; private UrlParser urlParser;
private HttpRequestBody body;
private Map<String, String> formData;
private final Map<String, String> cookies;
public HttpRequest(BufferedReader in) throws IOException { public HttpRequest(BufferedReader in) throws IOException {
this.headers = new HashMap<>(); this.headers = new HashMap<>();
this.queryParams = new HashMap<>();
this.cookies = new HashMap<>();
parseRequest(in); parseRequest(in);
parseCookies();
if (hasBody()) {
parseBody(in);
}
} }
private void parseRequest(BufferedReader in) throws IOException { private void parseRequest(BufferedReader in) throws IOException {
String requestLine = in.readLine(); String requestLine = in.readLine();
if (requestLine != null && !requestLine.trim().isEmpty()) { if (requestLine == null || requestLine.trim().isEmpty()) {
String[] tokens = requestLine.split(" "); throw new IOException("Empty request line");
if (tokens.length == 3) {
this.method = tokens[0];
this.path = tokens[1];
this.httpVersion = tokens[2];
} else {
throw new IOException("Invalid request line format.");
}
} }
String[] tokens = requestLine.split(" ");
if (tokens.length != 3) {
throw new IOException("Invalid request line format: " + requestLine);
}
this.method = tokens[0].toUpperCase();
parsePathAndQuery(tokens[1]);
this.httpVersion = tokens[2];
String headerLine; String headerLine;
while ((headerLine = in.readLine()) != null && !headerLine.trim().isEmpty()) { while ((headerLine = in.readLine()) != null && !headerLine.trim().isEmpty()) {
int separator = headerLine.indexOf(':'); int separator = headerLine.indexOf(':');
@@ -39,37 +56,148 @@ public class HttpRequest {
String key = headerLine.substring(0, separator).trim().toLowerCase(); String key = headerLine.substring(0, separator).trim().toLowerCase();
String value = headerLine.substring(separator + 1).trim(); String value = headerLine.substring(separator + 1).trim();
headers.put(key, value); headers.put(key, value);
Logger.debug("HTTP", "Header: " + key + " = " + value);
} }
} }
} }
public boolean isKeepAlive() { private void parsePathAndQuery(String fullPath) throws IOException {
String connection = headers.get("connection"); int queryStart = fullPath.indexOf('?');
if ("close".equalsIgnoreCase(connection)) { if (queryStart != -1) {
this.path = fullPath.substring(0, queryStart);
parseQueryString(fullPath.substring(queryStart + 1));
} else {
this.path = fullPath;
}
this.path = URLDecoder.decode(this.path, StandardCharsets.UTF_8);
}
private void parseQueryString(String queryString) throws IOException {
String[] pairs = queryString.split("&");
for (String pair : pairs) {
int idx = pair.indexOf("=");
if (idx > 0) {
String key = URLDecoder.decode(pair.substring(0, idx), StandardCharsets.UTF_8);
String value = URLDecoder.decode(pair.substring(idx + 1), StandardCharsets.UTF_8);
queryParams.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
Logger.debug("HTTP", "Query param: " + key + " = " + value);
}
}
}
private void parseCookies() {
String cookieHeader = headers.get("cookie");
if (cookieHeader != null) {
String[] cookiePairs = cookieHeader.split(";");
for (String cookiePair : cookiePairs) {
String[] parts = cookiePair.trim().split("=", 2);
if (parts.length == 2) {
cookies.put(parts[0], parts[1]);
Logger.debug("HTTP", "Cookie: " + parts[0] + " = " + parts[1]);
}
}
}
}
private boolean hasBody() {
if ("GET".equals(method) || "HEAD".equals(method)) {
return false; return false;
} }
return "HTTP/1.1".equals(httpVersion) ||
"keep-alive".equalsIgnoreCase(connection); String contentLengthHeader = headers.get("content-length");
return contentLengthHeader != null &&
Integer.parseInt(contentLengthHeader) > 0;
} }
public void setUrlParser(UrlParser urlParser) { private void parseBody(BufferedReader in) throws IOException {
this.urlParser = urlParser; int contentLength = Integer.parseInt(headers.get("content-length"));
String contentType = headers.getOrDefault("content-type", "text/plain");
if ("chunked".equalsIgnoreCase(headers.get("transfer-encoding"))) {
parseChunkedBody(in);
return;
}
char[] buffer = new char[contentLength];
int totalRead = 0;
while (totalRead < contentLength) {
int read = in.read(buffer, totalRead, contentLength - totalRead);
if (read == -1) {
throw new IOException("Unexpected end of stream");
}
totalRead += read;
}
String content = new String(buffer);
if (contentType.startsWith("multipart/form-data")) {
HttpMultipartParser parser = new HttpMultipartParser(in, contentType);
this.formData = parser.parse();
Logger.debug("HTTP", "Parsed multipart form data: " + formData.size() + " parts");
} else if (contentType.startsWith("application/x-www-form-urlencoded")) {
this.formData = parseUrlEncodedForm(content);
Logger.debug("HTTP", "Parsed URL encoded form data: " + formData.size() + " fields");
} else {
this.body = new HttpRequestBody(content, HttpContentType.fromString(contentType));
Logger.debug("HTTP", "Parsed body with content type: " + contentType);
}
} }
public String getPathParam(String name) { private void parseChunkedBody(BufferedReader in) throws IOException {
return urlParser != null ? urlParser.getPathParam(name) : null; StringBuilder content = new StringBuilder();
while (true) {
String chunkSizeLine = in.readLine();
if (chunkSizeLine == null) {
throw new IOException("Unexpected end of stream in chunked body");
}
int chunkSize = Integer.parseInt(chunkSizeLine.trim(), 16);
if (chunkSize == 0) {
break;
}
char[] chunk = new char[chunkSize];
int totalRead = 0;
while (totalRead < chunkSize) {
int read = in.read(chunk, totalRead, chunkSize - totalRead);
if (read == -1) {
throw new IOException("Unexpected end of stream in chunk");
}
totalRead += read;
}
content.append(chunk);
in.readLine();
}
String line;
while ((line = in.readLine()) != null && !line.isEmpty()) {
// TO DO
// Trailing headers
// :/
}
String contentType = headers.getOrDefault("content-type", "text/plain");
this.body = new HttpRequestBody(content.toString(), HttpContentType.fromString(contentType));
} }
public String getQueryParam(String name) { private Map<String, String> parseUrlEncodedForm(String content) {
return urlParser != null ? urlParser.getQueryParam(name) : null; Map<String, String> params = new HashMap<>();
} if (content == null || content.trim().isEmpty()) {
return params;
}
public Map<String, String> getPathParams() { String[] pairs = content.split("&");
return urlParser != null ? urlParser.getPathParams() : Map.of(); for (String pair : pairs) {
} String[] keyValue = pair.split("=", 2);
if (keyValue.length == 2) {
public Map<String, String> getQueryParams() { String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8);
return urlParser != null ? urlParser.getQueryParams() : Map.of(); String value = URLDecoder.decode(keyValue[1], StandardCharsets.UTF_8);
params.put(key, value);
}
}
return params;
} }
public String getMethod() { public String getMethod() {
@@ -83,4 +211,62 @@ public class HttpRequest {
public String getHttpVersion() { public String getHttpVersion() {
return httpVersion; return httpVersion;
} }
}
public String getHeader(String name) {
return headers.get(name.toLowerCase());
}
public Map<String, String> getHeaders() {
return new HashMap<>(headers);
}
public HttpRequestBody getBody() {
return body;
}
public Map<String, String> getFormData() {
return formData != null ? new HashMap<>(formData) : Map.of();
}
public String getCookie(String name) {
return cookies.get(name);
}
public Map<String, String> getCookies() {
return new HashMap<>(cookies);
}
public String getQueryParam(String name) {
List<String> values = queryParams.get(name);
return values != null && !values.isEmpty() ? values.get(0) : null;
}
public List<String> getQueryParams(String name) {
return queryParams.getOrDefault(name, new ArrayList<>());
}
public Map<String, List<String>> getAllQueryParams() {
return new HashMap<>(queryParams);
}
public void setUrlParser(UrlParser urlParser) {
this.urlParser = urlParser;
}
public String getPathParam(String name) {
return urlParser != null ? urlParser.getPathParam(name) : null;
}
public Map<String, String> getPathParams() {
return urlParser != null ? urlParser.getPathParams() : Map.of();
}
public boolean isKeepAlive() {
String connection = headers.get("connection");
if ("close".equalsIgnoreCase(connection)) {
return false;
}
return "HTTP/1.1".equals(httpVersion) ||
"keep-alive".equalsIgnoreCase(connection);
}
}

View File

@@ -1,37 +1,66 @@
package io.github.lumijiez.core.http; package io.github.lumijiez.core.http;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import io.github.lumijiez.core.logging.Logger; import io.github.lumijiez.core.logging.Logger;
import java.io.BufferedWriter; import java.io.BufferedWriter;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class HttpResponse { public class HttpResponse {
private final BufferedWriter out; private final BufferedWriter out;
private final Map<String, String> headers;
private static final ObjectMapper jsonMapper = new ObjectMapper();
private static final XmlMapper xmlMapper = new XmlMapper();
public HttpResponse(BufferedWriter out) { public HttpResponse(BufferedWriter out) {
this.out = out; this.out = out;
this.headers = new HashMap<>();
headers.put("Connection", "keep-alive");
headers.put("Keep-Alive", "timeout=30");
}
public void setHeader(String name, String value) {
headers.put(name, value);
} }
public void sendResponse(HttpStatus status, String message) throws IOException { public void sendResponse(HttpStatus status, String message) throws IOException {
sendResponse(status, message, HttpContentType.TEXT_PLAIN);
}
public void sendResponse(HttpStatus status, String message, HttpContentType contentType) throws IOException {
Logger.info("HTTP", "Outgoing: " + status.getCode() + " " + message); Logger.info("HTTP", "Outgoing: " + status.getCode() + " " + message);
out.write("HTTP/1.1 " + status.getCode() + " " + status.getMessage()); writeStatusLine(status);
writeHeaders(contentType, message.getBytes(StandardCharsets.UTF_8).length);
out.write("\r\n"); out.write("\r\n");
out.write("Content-Type: text/plain");
out.write("\r\n");
out.write("Content-Length: " + message.getBytes(StandardCharsets.UTF_8).length);
out.write("\r\n");
out.write("Connection: keep-alive");
out.write("\r\n");
out.write("Keep-Alive: timeout=30");
out.write("\r\n");
out.write("\r\n");
out.write(message); out.write(message);
out.flush(); out.flush();
} }
public void sendJson(HttpStatus status, Object obj) throws IOException {
String jsonContent = jsonMapper.writeValueAsString(obj);
sendResponse(status, jsonContent, HttpContentType.APPLICATION_JSON);
}
public void sendXml(HttpStatus status, Object obj) throws IOException {
String xmlContent = xmlMapper.writeValueAsString(obj);
sendResponse(status, xmlContent, HttpContentType.APPLICATION_XML);
}
private void writeStatusLine(HttpStatus status) throws IOException {
out.write("HTTP/1.1 " + status.getCode() + " " + status.getMessage() + "\r\n");
}
private void writeHeaders(HttpContentType contentType, int contentLength) throws IOException {
headers.put("Content-Type", contentType.getValue());
headers.put("Content-Length", String.valueOf(contentLength));
for (Map.Entry<String, String> header : headers.entrySet()) {
out.write(header.getKey() + ": " + header.getValue() + "\r\n");
}
}
} }

View File

@@ -32,6 +32,11 @@ public class Main {
res.sendResponse(HttpStatus.OK, "All good, lil bro"); res.sendResponse(HttpStatus.OK, "All good, lil bro");
}); });
server.GET("/user", (req, res) -> {
Product product = productDao.getProductById(5);
res.sendJson(HttpStatus.OK, product);
});
server.GET("/products", (req, res) -> { server.GET("/products", (req, res) -> {
Product product = productDao.getProductById(5); Product product = productDao.getProductById(5);
res.sendResponse(HttpStatus.OK, product.toString()); res.sendResponse(HttpStatus.OK, product.toString());