IT WORKS
This commit is contained in:
@@ -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,25 +17,37 @@ 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()) {
|
||||||
|
throw new IOException("Empty request line");
|
||||||
|
}
|
||||||
|
|
||||||
String[] tokens = requestLine.split(" ");
|
String[] tokens = requestLine.split(" ");
|
||||||
if (tokens.length == 3) {
|
if (tokens.length != 3) {
|
||||||
this.method = tokens[0];
|
throw new IOException("Invalid request line format: " + requestLine);
|
||||||
this.path = tokens[1];
|
}
|
||||||
|
|
||||||
|
this.method = tokens[0].toUpperCase();
|
||||||
|
parsePathAndQuery(tokens[1]);
|
||||||
this.httpVersion = tokens[2];
|
this.httpVersion = tokens[2];
|
||||||
} else {
|
|
||||||
throw new IOException("Invalid request line format.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String headerLine;
|
String headerLine;
|
||||||
while ((headerLine = in.readLine()) != null && !headerLine.trim().isEmpty()) {
|
while ((headerLine = in.readLine()) != null && !headerLine.trim().isEmpty()) {
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPathParam(String name) {
|
char[] buffer = new char[contentLength];
|
||||||
return urlParser != null ? urlParser.getPathParam(name) : null;
|
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 getQueryParam(String name) {
|
private void parseChunkedBody(BufferedReader in) throws IOException {
|
||||||
return urlParser != null ? urlParser.getQueryParam(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");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, String> getPathParams() {
|
int chunkSize = Integer.parseInt(chunkSizeLine.trim(), 16);
|
||||||
return urlParser != null ? urlParser.getPathParams() : Map.of();
|
if (chunkSize == 0) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, String> getQueryParams() {
|
char[] chunk = new char[chunkSize];
|
||||||
return urlParser != null ? urlParser.getQueryParams() : Map.of();
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, String> parseUrlEncodedForm(String content) {
|
||||||
|
Map<String, String> params = new HashMap<>();
|
||||||
|
if (content == null || content.trim().isEmpty()) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] pairs = content.split("&");
|
||||||
|
for (String pair : pairs) {
|
||||||
|
String[] keyValue = pair.split("=", 2);
|
||||||
|
if (keyValue.length == 2) {
|
||||||
|
String key = URLDecoder.decode(keyValue[0], StandardCharsets.UTF_8);
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
Reference in New Issue
Block a user