();
diff --git a/src/main/java/com/dasanjos/java/http/HttpResponse.java b/src/main/java/com/dasanjos/java/http/HttpResponse.java
index 393e1de..7f5ca48 100644
--- a/src/main/java/com/dasanjos/java/http/HttpResponse.java
+++ b/src/main/java/com/dasanjos/java/http/HttpResponse.java
@@ -1,5 +1,11 @@
package com.dasanjos.java.http;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -8,8 +14,6 @@
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
-import java.util.logging.Level;
-import java.util.logging.Logger;
/**
@@ -18,7 +22,7 @@
*/
public class HttpResponse {
- private static final Logger LOGGER = Logger.getLogger(HttpResponse.class.getName());
+ private static final Logger LOGGER = LoggerFactory.getLogger(HttpResponse.class);
public static final String VERSION = "HTTP/1.0";
@@ -34,36 +38,54 @@ public HttpResponse(HttpRequest req) throws IOException {
break;
case GET:
try {
- // TODO fix dir bug http://localhost:8080/src/test
- File file = new File("." + req.uri);
- if (file.isDirectory()) {
- fillHeaders(Status._200);
-
- headers.add(ContentType.HTML.toString());
+ File root = new File(".");
+ String rootPath = root.getCanonicalPath();
+ File file = new File(root, req.uri);
+ String filePath = file.getCanonicalPath();
+
+ if (!filePath.startsWith(rootPath)) {
+ LOGGER.warn("Path traversal attempt: {}", req.uri);
+ fillHeaders(Status._403);
+ fillResponse(Status._403.toString());
+ } else if (file.isDirectory()) {
+ File indexFile = new File(file, "index.html");
+ if (indexFile.exists()) {
+ fillHeaders(Status._200);
+ setContentType(indexFile.getName(), headers);
+ fillResponse(getBytes(indexFile));
+ } else {
+ fillHeaders(Status._200);
+ headers.add(ContentType.HTML.toString());
+ }
StringBuilder result = new StringBuilder("Index of ");
result.append(req.uri);
result.append("Index of ");
result.append(req.uri);
result.append("
");
- // TODO add Parent Directory
+ if (!req.uri.equals("/")) {
+ result.append("..\n");
+ }
+
File[] files = file.listFiles();
for (File subfile : files) {
- result.append(" " + subfile.getPath() + "\n");
+ String fileName = subfile.getName();
+ String link = new File(req.uri, fileName).getPath();
+ result.append(" " + fileName + "\n");
}
result.append("
");
fillResponse(result.toString());
} else if (file.exists()) {
- fillHeaders(Status._200);
+ fillHeaders(Status._200);
setContentType(req.uri, headers);
fillResponse(getBytes(file));
} else {
- LOGGER.info("File not found:" + req.uri);
+ LOGGER.info("File not found: {}", req.uri);
fillHeaders(Status._404);
fillResponse(Status._404.toString());
}
} catch (Exception e) {
- LOGGER.log(Level.SEVERE, "Response Error", e);
+ LOGGER.error("Response Error", e);
fillHeaders(Status._400);
fillResponse(Status._400.toString());
}
@@ -125,7 +147,7 @@ private void setContentType(String uri, List list) {
String ext = uri.substring(uri.indexOf(".") + 1);
list.add(ContentType.valueOf(ext.toUpperCase()).toString());
} catch (Exception e) {
- LOGGER.log(Level.SEVERE, "ContentType not found: " + e, e);
+ LOGGER.error("ContentType not found", e);
}
}
}
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
new file mode 100644
index 0000000..4bd2e37
--- /dev/null
+++ b/src/main/resources/logback.xml
@@ -0,0 +1,11 @@
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
\ No newline at end of file
From 408ff50fd04f3b41a77a3f41f28e128f437f9a02 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sat, 10 Jan 2026 20:38:12 +0000
Subject: [PATCH 02/10] feat: Improve web server security, functionality, and
dependencies
This commit introduces a series of improvements to the web server, addressing security vulnerabilities, bugs, and outdated dependencies.
- Fix path traversal vulnerability by validating file paths.
- Correct directory listing links to be relative.
- Upgrade Java version to 1.8 and JUnit to 4.13.2.
- Replace custom logger with SLF4J and Logback.
- Add "Parent Directory" link to directory listings.
- Implement default file handling for `index.html`.
- Expand and correct MIME types in `ContentType.java`.
Reverted Java version to 1.8 due to build environment constraints.
From 76089c44d304fbb79af9d192ad4d7cb926e9dc57 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sat, 10 Jan 2026 20:58:00 +0000
Subject: [PATCH 03/10] feat: Improve web server and add unit tests
This commit introduces a series of improvements to the web server, addressing security vulnerabilities, bugs, and outdated dependencies, and adds a comprehensive suite of unit tests to cover the new functionality.
- Fix path traversal vulnerability by validating file paths.
- Correct directory listing links to be relative and use forward slashes.
- Replace custom logger with SLF4J and Logback.
- Add "Parent Directory" link to directory listings.
- Implement default file handling for `index.html`.
- Expand and correct MIME types in `ContentType.java`.
- Add unit tests for all new features and fixes.
- Set Java version to 1.8 due to build environment constraints.
---
pom.xml | 2 +-
.../com/dasanjos/java/RequestHandler.java | 3 +-
.../com/dasanjos/java/http/HttpResponse.java | 19 +++--
src/main/resources/logback.xml | 2 +-
.../dasanjos/java/http/ContentTypeTest.java | 24 +++---
.../dasanjos/java/http/HttpResponseTest.java | 80 +++++++++++++++++--
6 files changed, 102 insertions(+), 28 deletions(-)
diff --git a/pom.xml b/pom.xml
index 75fb927..8c06f99 100644
--- a/pom.xml
+++ b/pom.xml
@@ -58,4 +58,4 @@
1.2.6
-
\ No newline at end of file
+
diff --git a/src/main/java/com/dasanjos/java/RequestHandler.java b/src/main/java/com/dasanjos/java/RequestHandler.java
index d0efd61..35f93f4 100644
--- a/src/main/java/com/dasanjos/java/RequestHandler.java
+++ b/src/main/java/com/dasanjos/java/RequestHandler.java
@@ -4,6 +4,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.File;
import java.net.Socket;
import com.dasanjos.java.http.HttpResponse;
@@ -23,7 +24,7 @@ public RequestHandler(Socket socket) {
public void run() {
try {
HttpRequest req = new HttpRequest(socket.getInputStream());
- HttpResponse res = new HttpResponse(req);
+ HttpResponse res = new HttpResponse(req, new File("."));
res.write(socket.getOutputStream());
} catch (Exception e) {
LOGGER.error("Runtime Error", e);
diff --git a/src/main/java/com/dasanjos/java/http/HttpResponse.java b/src/main/java/com/dasanjos/java/http/HttpResponse.java
index 7f5ca48..cd5410b 100644
--- a/src/main/java/com/dasanjos/java/http/HttpResponse.java
+++ b/src/main/java/com/dasanjos/java/http/HttpResponse.java
@@ -31,16 +31,18 @@ public class HttpResponse {
byte[] body;
public HttpResponse(HttpRequest req) throws IOException {
+ this(req, new File("."));
+ }
+ public HttpResponse(HttpRequest req, File webRoot) throws IOException {
switch (req.method) {
case HEAD:
fillHeaders(Status._200);
break;
case GET:
try {
- File root = new File(".");
- String rootPath = root.getCanonicalPath();
- File file = new File(root, req.uri);
+ String rootPath = webRoot.getCanonicalPath();
+ File file = new File(webRoot, req.uri);
String filePath = file.getCanonicalPath();
if (!filePath.startsWith(rootPath)) {
@@ -49,14 +51,15 @@ public HttpResponse(HttpRequest req) throws IOException {
fillResponse(Status._403.toString());
} else if (file.isDirectory()) {
File indexFile = new File(file, "index.html");
- if (indexFile.exists()) {
+ if (indexFile.exists() && !indexFile.isDirectory()) {
fillHeaders(Status._200);
setContentType(indexFile.getName(), headers);
fillResponse(getBytes(indexFile));
- } else {
- fillHeaders(Status._200);
- headers.add(ContentType.HTML.toString());
+ return;
}
+
+ fillHeaders(Status._200);
+ headers.add(ContentType.HTML.toString());
StringBuilder result = new StringBuilder("Index of ");
result.append(req.uri);
result.append("Index of ");
@@ -70,7 +73,7 @@ public HttpResponse(HttpRequest req) throws IOException {
File[] files = file.listFiles();
for (File subfile : files) {
String fileName = subfile.getName();
- String link = new File(req.uri, fileName).getPath();
+ String link = new File(req.uri, fileName).getPath().replace(File.separator, "/");
result.append(" " + fileName + "\n");
}
result.append("
");
diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml
index 4bd2e37..b1f9bfe 100644
--- a/src/main/resources/logback.xml
+++ b/src/main/resources/logback.xml
@@ -8,4 +8,4 @@
-
\ No newline at end of file
+
diff --git a/src/test/java/com/dasanjos/java/http/ContentTypeTest.java b/src/test/java/com/dasanjos/java/http/ContentTypeTest.java
index 35a875f..05a884e 100644
--- a/src/test/java/com/dasanjos/java/http/ContentTypeTest.java
+++ b/src/test/java/com/dasanjos/java/http/ContentTypeTest.java
@@ -7,16 +7,18 @@ public class ContentTypeTest {
@Test
public void getCorrectContentTypeByExtension() {
- Assert.assertEquals(ContentType.CSS, ContentType.valueOf("CSS"));
- Assert.assertEquals(ContentType.GIF, ContentType.valueOf("GIF"));
- Assert.assertEquals(ContentType.HTM, ContentType.valueOf("HTM"));
- Assert.assertEquals(ContentType.HTML, ContentType.valueOf("HTML"));
- Assert.assertEquals(ContentType.ICO, ContentType.valueOf("ICO"));
- Assert.assertEquals(ContentType.JPG, ContentType.valueOf("JPG"));
- Assert.assertEquals(ContentType.JPEG, ContentType.valueOf("JPEG"));
- Assert.assertEquals(ContentType.PNG, ContentType.valueOf("PNG"));
- Assert.assertEquals(ContentType.TXT, ContentType.valueOf("TXT"));
- Assert.assertEquals(ContentType.XML, ContentType.valueOf("XML"));
+ Assert.assertEquals("Content-Type: text/css", ContentType.CSS.toString());
+ Assert.assertEquals("Content-Type: image/gif", ContentType.GIF.toString());
+ Assert.assertEquals("Content-Type: text/html", ContentType.HTM.toString());
+ Assert.assertEquals("Content-Type: text/html", ContentType.HTML.toString());
+ Assert.assertEquals("Content-Type: image/x-icon", ContentType.ICO.toString());
+ Assert.assertEquals("Content-Type: image/jpeg", ContentType.JPG.toString());
+ Assert.assertEquals("Content-Type: image/jpeg", ContentType.JPEG.toString());
+ Assert.assertEquals("Content-Type: image/png", ContentType.PNG.toString());
+ Assert.assertEquals("Content-Type: text/plain", ContentType.TXT.toString());
+ Assert.assertEquals("Content-Type: text/xml", ContentType.XML.toString());
+ Assert.assertEquals("Content-Type: application/pdf", ContentType.PDF.toString());
+ Assert.assertEquals("Content-Type: application/json", ContentType.JSON.toString());
+ Assert.assertEquals("Content-Type: application/javascript", ContentType.JS.toString());
}
-
}
diff --git a/src/test/java/com/dasanjos/java/http/HttpResponseTest.java b/src/test/java/com/dasanjos/java/http/HttpResponseTest.java
index 487cd17..d0cbaff 100644
--- a/src/test/java/com/dasanjos/java/http/HttpResponseTest.java
+++ b/src/test/java/com/dasanjos/java/http/HttpResponseTest.java
@@ -1,17 +1,26 @@
package com.dasanjos.java.http;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-
import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
public class HttpResponseTest {
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
@Test
public void doHeadRequest() throws IOException {
HttpRequest req = new HttpRequest(new ByteArrayInputStream("HEAD / HTTP/1.1\n\n".getBytes()));
- HttpResponse res = new HttpResponse(req);
+ HttpResponse res = new HttpResponse(req, tempFolder.getRoot());
Assert.assertTrue(res.headers.size() > 0);
Assert.assertEquals(HttpResponse.VERSION + " " + Status._200, res.headers.get(0));
@@ -20,7 +29,7 @@ public void doHeadRequest() throws IOException {
@Test
public void doGetRequest() throws IOException {
HttpRequest req = new HttpRequest(new ByteArrayInputStream("GET / HTTP/1.1\n\n".getBytes()));
- HttpResponse res = new HttpResponse(req);
+ HttpResponse res = new HttpResponse(req, tempFolder.getRoot());
Assert.assertTrue(res.headers.size() > 0);
Assert.assertEquals(HttpResponse.VERSION + " " + Status._200, res.headers.get(0));
@@ -29,9 +38,68 @@ public void doGetRequest() throws IOException {
@Test
public void unknownRequest() throws IOException {
HttpRequest req = new HttpRequest(new ByteArrayInputStream("WHAT / HTTP/1.1\n\n".getBytes()));
- HttpResponse res = new HttpResponse(req);
+ HttpResponse res = new HttpResponse(req, tempFolder.getRoot());
Assert.assertTrue(res.headers.size() > 0);
Assert.assertEquals(HttpResponse.VERSION + " " + Status._400, res.headers.get(0));
}
+
+ @Test
+ public void pathTraversalRequest() throws IOException {
+ HttpRequest req = new HttpRequest(new ByteArrayInputStream("GET ../../../../../../../etc/passwd HTTP/1.1\n\n".getBytes()));
+ HttpResponse res = new HttpResponse(req, tempFolder.getRoot());
+
+ Assert.assertTrue(res.headers.size() > 0);
+ Assert.assertEquals(HttpResponse.VERSION + " " + Status._403, res.headers.get(0));
+ }
+
+ @Test
+ public void directoryListingRequest() throws IOException {
+ File testFile = tempFolder.newFile("test.txt");
+ HttpRequest req = new HttpRequest(new ByteArrayInputStream("GET / HTTP/1.1\n\n".getBytes()));
+ HttpResponse res = new HttpResponse(req, tempFolder.getRoot());
+ String body = new String(res.body, StandardCharsets.UTF_8);
+
+ Assert.assertTrue(res.headers.size() > 0);
+ Assert.assertEquals(HttpResponse.VERSION + " " + Status._200, res.headers.get(0));
+ Assert.assertTrue("Body should contain link to test.txt", body.contains(" test.txt".replace("/", File.separator)));
+ }
+
+ @Test
+ public void directoryListingParentLinkRequest() throws IOException {
+ File subDir = tempFolder.newFolder("subdir");
+ HttpRequest req = new HttpRequest(new ByteArrayInputStream(("GET " + subDir.getName() + "/ HTTP/1.1\n\n").getBytes()));
+ HttpResponse res = new HttpResponse(req, tempFolder.getRoot());
+ String body = new String(res.body, StandardCharsets.UTF_8);
+
+ Assert.assertTrue(res.headers.size() > 0);
+ Assert.assertEquals(HttpResponse.VERSION + " " + Status._200, res.headers.get(0));
+ Assert.assertTrue("Body should contain parent directory link", body.contains(".."));
+ }
+
+ @Test
+ public void directoryListingNoParentLinkAtRoot() throws IOException {
+ HttpRequest req = new HttpRequest(new ByteArrayInputStream("GET / HTTP/1.1\n\n".getBytes()));
+ HttpResponse res = new HttpResponse(req, tempFolder.getRoot());
+ String body = new String(res.body, StandardCharsets.UTF_8);
+
+ Assert.assertTrue(res.headers.size() > 0);
+ Assert.assertEquals(HttpResponse.VERSION + " " + Status._200, res.headers.get(0));
+ Assert.assertFalse("Body should not contain parent directory link", body.contains(".."));
+ }
+
+ @Test
+ public void defaultFileRequest() throws IOException {
+ File indexFile = tempFolder.newFile("index.html");
+ try (FileWriter writer = new FileWriter(indexFile)) {
+ writer.write("Hello");
+ }
+ HttpRequest req = new HttpRequest(new ByteArrayInputStream("GET / HTTP/1.1\n\n".getBytes()));
+ HttpResponse res = new HttpResponse(req, tempFolder.getRoot());
+ String body = new String(res.body, StandardCharsets.UTF_8);
+
+ Assert.assertTrue(res.headers.size() > 0);
+ Assert.assertEquals(HttpResponse.VERSION + " " + Status._200, res.headers.get(0));
+ Assert.assertEquals("Hello", body);
+ }
}
From f88d0fb08f90c1f7c15ca09ac1a20a422763a671 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sat, 10 Jan 2026 21:14:30 +0000
Subject: [PATCH 04/10] feat: Improve web server and add unit tests
This commit introduces a series of improvements to the web server, addressing security vulnerabilities, bugs, and outdated dependencies, and adds a comprehensive suite of unit tests to cover the new functionality.
- Fix path traversal vulnerability by validating file paths.
- Correct directory listing links to be relative and use forward slashes.
- Replace custom logger with SLF4J and Logback.
- Add "Parent Directory" link to directory listings.
- Implement default file handling for `index.html`.
- Expand and correct MIME types in `ContentType.java`.
- Add unit tests for all new features and fixes, including for RequestHandler.
- Set Java version to 1.8 due to build environment constraints that prevent compiling for Java 6.
---
pom.xml | 6 +++
.../com/dasanjos/java/RequestHandlerTest.java | 40 +++++++++++++++++++
2 files changed, 46 insertions(+)
create mode 100644 src/test/java/com/dasanjos/java/RequestHandlerTest.java
diff --git a/pom.xml b/pom.xml
index 8c06f99..5a679b2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -47,6 +47,12 @@
4.13.2
test
+
+ org.mockito
+ mockito-core
+ 3.12.4
+ test
+
org.slf4j
slf4j-api
diff --git a/src/test/java/com/dasanjos/java/RequestHandlerTest.java b/src/test/java/com/dasanjos/java/RequestHandlerTest.java
new file mode 100644
index 0000000..7c6eee8
--- /dev/null
+++ b/src/test/java/com/dasanjos/java/RequestHandlerTest.java
@@ -0,0 +1,40 @@
+package com.dasanjos.java;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.net.Socket;
+
+import static org.mockito.Mockito.when;
+import static org.junit.Assert.assertTrue;
+
+public class RequestHandlerTest {
+
+ @Test
+ public void testRequestHandler() throws Exception {
+ // Create a mock socket
+ Socket mockSocket = Mockito.mock(Socket.class);
+
+ // Create input and output streams
+ String request = "GET / HTTP/1.1\n\n";
+ ByteArrayInputStream inputStream = new ByteArrayInputStream(request.getBytes());
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+
+ // Stub the socket's getInputStream and getOutputStream methods
+ when(mockSocket.getInputStream()).thenReturn(inputStream);
+ when(mockSocket.getOutputStream()).thenReturn(outputStream);
+
+ // Create a RequestHandler and run it
+ RequestHandler requestHandler = new RequestHandler(mockSocket);
+ requestHandler.run();
+
+ // Verify that the response contains a valid HTTP status line
+ String response = outputStream.toString("UTF-8");
+ assertTrue("Response should contain a valid HTTP status line", response.startsWith("HTTP/1.0 200 OK"));
+
+ // Verify that the socket was closed
+ Mockito.verify(mockSocket).close();
+ }
+}
From 0b199b505f8885e59f5ba3029da0964867e33541 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sat, 10 Jan 2026 21:32:03 +0000
Subject: [PATCH 05/10] feat: Improve server, add unit and integration tests
This commit delivers a comprehensive set of improvements to the web server, including critical security fixes, new features, and extensive test coverage.
- **Security:** Patched a critical path traversal vulnerability by validating requested file paths against the web root.
- **Features:**
- Implemented default file handling to serve `index.html` when a directory is requested.
- Enhanced directory listings with a "Parent Directory" link for better navigation.
- **Refactoring & Modernization:**
- Replaced the custom logger with the standard SLF4J and Logback frameworks for improved maintainability.
- Refactored the `ContentType` enum for correctness and extensibility.
- Made the `WebServer` root directory configurable and implemented a graceful shutdown mechanism.
- Set the project to use Java 1.8 due to build environment constraints that prevent compiling for Java 6.
- **Testing:**
- Added a comprehensive suite of new unit tests for `HttpResponse` and `RequestHandler`, using mocks where appropriate.
- Added a new suite of integration tests using Apache HttpClient to validate end-to-end HTTP 1.0 compatibility, covering file serving, 404 errors, directory listings, and the path traversal fix.
---
pom.xml | 6 +
.../com/dasanjos/java/RequestHandler.java | 8 +-
.../java/com/dasanjos/java/WebServer.java | 64 ++++++---
.../com/dasanjos/java/RequestHandlerTest.java | 3 +-
.../java/WebServerIntegrationTest.java | 129 ++++++++++++++++++
5 files changed, 190 insertions(+), 20 deletions(-)
create mode 100644 src/test/java/com/dasanjos/java/WebServerIntegrationTest.java
diff --git a/pom.xml b/pom.xml
index 5a679b2..820be78 100644
--- a/pom.xml
+++ b/pom.xml
@@ -53,6 +53,12 @@
3.12.4
test
+
+ org.apache.httpcomponents
+ httpclient
+ 4.5.13
+ test
+
org.slf4j
slf4j-api
diff --git a/src/main/java/com/dasanjos/java/RequestHandler.java b/src/main/java/com/dasanjos/java/RequestHandler.java
index 35f93f4..7a1ea5d 100644
--- a/src/main/java/com/dasanjos/java/RequestHandler.java
+++ b/src/main/java/com/dasanjos/java/RequestHandler.java
@@ -15,16 +15,18 @@ public class RequestHandler implements Runnable {
private static final Logger LOGGER = LoggerFactory.getLogger(RequestHandler.class);
- private Socket socket;
+ private final Socket socket;
+ private final File webRoot;
- public RequestHandler(Socket socket) {
+ public RequestHandler(Socket socket, File webRoot) {
this.socket = socket;
+ this.webRoot = webRoot;
}
public void run() {
try {
HttpRequest req = new HttpRequest(socket.getInputStream());
- HttpResponse res = new HttpResponse(req, new File("."));
+ HttpResponse res = new HttpResponse(req, webRoot);
res.write(socket.getOutputStream());
} catch (Exception e) {
LOGGER.error("Runtime Error", e);
diff --git a/src/main/java/com/dasanjos/java/WebServer.java b/src/main/java/com/dasanjos/java/WebServer.java
index b0ddc7a..b27a453 100644
--- a/src/main/java/com/dasanjos/java/WebServer.java
+++ b/src/main/java/com/dasanjos/java/WebServer.java
@@ -3,6 +3,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.concurrent.ExecutorService;
@@ -16,10 +17,20 @@ public class WebServer {
private static final Logger LOGGER = LoggerFactory.getLogger(WebServer.class);
private static final int DEFAULT_PORT = 8080;
-
private static final int N_THREADS = 3;
+ private final File webRoot;
private volatile boolean running = true;
+ private ServerSocket serverSocket;
+ private ExecutorService executor;
+
+ public WebServer() {
+ this(new File("."));
+ }
+
+ public WebServer(File webRoot) {
+ this.webRoot = webRoot;
+ }
public static void main(String args[]) {
try {
@@ -30,22 +41,43 @@ public static void main(String args[]) {
}
}
- public void start(int port) throws IOException {
- ServerSocket s = new ServerSocket(port);
- LOGGER.info("Web server listening on port {} \r\nPress CTRL-C to quit", port);
- final ExecutorService executor = Executors.newFixedThreadPool(N_THREADS);
- //Add Graceful Shutdown
- Runtime.getRuntime().addShutdownHook(new Thread() {
- public void run() {
- running = false;
- LOGGER.info("Shutting down server...");
- executor.shutdown();
+ public void start(int port) throws IOException {
+ serverSocket = new ServerSocket(port);
+ LOGGER.info("Web server listening on port {} \r\nPress CTRL-C to quit", getPort());
+ executor = Executors.newFixedThreadPool(N_THREADS);
+
+ // Add Graceful Shutdown
+ Runtime.getRuntime().addShutdownHook(new Thread(() -> stop()));
+
+ while (running) {
+ try {
+ executor.submit(new RequestHandler(serverSocket.accept(), webRoot));
+ } catch (IOException e) {
+ if (running) {
+ LOGGER.error("Error accepting connection", e);
+ }
+ }
+ }
+ }
+
+ public void stop() {
+ running = false;
+ LOGGER.info("Shutting down server...");
+ if (executor != null) {
+ executor.shutdown();
+ }
+ try {
+ if (serverSocket != null && !serverSocket.isClosed()) {
+ serverSocket.close();
}
- });
- while (running) {
- executor.submit(new RequestHandler(s.accept()));
- }
- }
+ } catch (IOException e) {
+ LOGGER.error("Error closing server socket", e);
+ }
+ }
+
+ public int getPort() {
+ return serverSocket != null ? serverSocket.getLocalPort() : -1;
+ }
/**
* Parse command line arguments (string[] args) for valid port number
diff --git a/src/test/java/com/dasanjos/java/RequestHandlerTest.java b/src/test/java/com/dasanjos/java/RequestHandlerTest.java
index 7c6eee8..cd79017 100644
--- a/src/test/java/com/dasanjos/java/RequestHandlerTest.java
+++ b/src/test/java/com/dasanjos/java/RequestHandlerTest.java
@@ -5,6 +5,7 @@
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
+import java.io.File;
import java.net.Socket;
import static org.mockito.Mockito.when;
@@ -27,7 +28,7 @@ public void testRequestHandler() throws Exception {
when(mockSocket.getOutputStream()).thenReturn(outputStream);
// Create a RequestHandler and run it
- RequestHandler requestHandler = new RequestHandler(mockSocket);
+ RequestHandler requestHandler = new RequestHandler(mockSocket, new File("."));
requestHandler.run();
// Verify that the response contains a valid HTTP status line
diff --git a/src/test/java/com/dasanjos/java/WebServerIntegrationTest.java b/src/test/java/com/dasanjos/java/WebServerIntegrationTest.java
new file mode 100644
index 0000000..90ae310
--- /dev/null
+++ b/src/test/java/com/dasanjos/java/WebServerIntegrationTest.java
@@ -0,0 +1,129 @@
+package com.dasanjos.java;
+
+import org.apache.http.HttpVersion;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpHead;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+
+public class WebServerIntegrationTest {
+
+ private WebServer webServer;
+ private Thread serverThread;
+ private int port;
+
+ @Rule
+ public TemporaryFolder tempFolder = new TemporaryFolder();
+
+ @Before
+ public void setUp() throws Exception {
+ // Start the web server on a random free port
+ webServer = new WebServer(tempFolder.getRoot());
+ serverThread = new Thread(() -> {
+ try {
+ webServer.start(0); // 0 means a random available port
+ } catch (IOException e) {
+ if (!serverThread.isInterrupted()) {
+ e.printStackTrace();
+ }
+ }
+ });
+ serverThread.start();
+
+ // Wait until the server has started and get the port
+ do {
+ Thread.sleep(100);
+ port = webServer.getPort();
+ } while (port == -1);
+ }
+
+ @After
+ public void tearDown() {
+ webServer.stop();
+ serverThread.interrupt();
+ }
+
+ @Test
+ public void testGetRequest() throws IOException {
+ File testFile = tempFolder.newFile("test.txt");
+ try (FileWriter writer = new FileWriter(testFile)) {
+ writer.write("Hello, world!");
+ }
+
+ CloseableHttpClient client = HttpClients.createDefault();
+ HttpGet request = new HttpGet("http://localhost:" + port + "/test.txt");
+ request.setProtocolVersion(HttpVersion.HTTP_1_0);
+ try (CloseableHttpResponse response = client.execute(request)) {
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void testHeadRequest() throws IOException {
+ CloseableHttpClient client = HttpClients.createDefault();
+ HttpHead request = new HttpHead("http://localhost:" + port + "/");
+ request.setProtocolVersion(HttpVersion.HTTP_1_0);
+ try (CloseableHttpResponse response = client.execute(request)) {
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void testNotFound() throws IOException {
+ CloseableHttpClient client = HttpClients.createDefault();
+ HttpGet request = new HttpGet("http://localhost:" + port + "/nonexistent.txt");
+ request.setProtocolVersion(HttpVersion.HTTP_1_0);
+ try (CloseableHttpResponse response = client.execute(request)) {
+ assertEquals(404, response.getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void testDirectoryListing() throws IOException {
+ tempFolder.newFile("test.txt");
+
+ CloseableHttpClient client = HttpClients.createDefault();
+ HttpGet request = new HttpGet("http://localhost:" + port + "/");
+ request.setProtocolVersion(HttpVersion.HTTP_1_0);
+ try (CloseableHttpResponse response = client.execute(request)) {
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void testPathTraversal() throws IOException {
+ CloseableHttpClient client = HttpClients.createDefault();
+ HttpGet request = new HttpGet("http://localhost:" + port + "/../../../../../../../etc/passwd");
+ request.setProtocolVersion(HttpVersion.HTTP_1_0);
+ try (CloseableHttpResponse response = client.execute(request)) {
+ assertEquals(403, response.getStatusLine().getStatusCode());
+ }
+ }
+
+ @Test
+ public void testDefaultFile() throws IOException {
+ File indexFile = tempFolder.newFile("index.html");
+ try (FileWriter writer = new FileWriter(indexFile)) {
+ writer.write("Hello");
+ }
+
+ CloseableHttpClient client = HttpClients.createDefault();
+ HttpGet request = new HttpGet("http://localhost:" + port + "/");
+ request.setProtocolVersion(HttpVersion.HTTP_1_0);
+ try (CloseableHttpResponse response = client.execute(request)) {
+ assertEquals(200, response.getStatusLine().getStatusCode());
+ }
+ }
+}
From 69b08b97f7e14300d66990da45401c553356c630 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Sat, 10 Jan 2026 22:08:45 +0000
Subject: [PATCH 06/10] feat: Improve server, add tests, and enhance HEAD
support
This commit delivers a comprehensive set of improvements to the web server, including critical security fixes, new features, extensive test coverage, and full compliance for the HTTP `HEAD` method.
- **Security:** Patched a critical path traversal vulnerability by validating requested file paths against the web root.
- **Features:**
- Implemented default file handling to serve `index.html` when a directory is requested.
- Enhanced directory listings with a "Parent Directory" link for better navigation.
- **HTTP `HEAD` Method:**
- Refactored `HttpResponse` to correctly handle `HEAD` requests, ensuring it returns the same headers as a `GET` request but without the response body.
- **Refactoring & Modernization:**
- Replaced the custom logger with the standard SLF4J and Logback frameworks.
- Refactored the `ContentType` enum for correctness and extensibility.
- Made the `WebServer` root directory configurable and implemented a graceful shutdown mechanism.
- Set the project to use Java 1.8 due to build environment constraints.
- **Testing:**
- Added a comprehensive suite of new unit tests for `HttpResponse` and `RequestHandler`.
- Added a new suite of integration tests to validate end-to-end HTTP 1.0 compatibility, now including an enhanced test for the `HEAD` method.
---
.../com/dasanjos/java/http/HttpResponse.java | 16 +++++++-----
.../java/WebServerIntegrationTest.java | 25 ++++++++++++++++---
2 files changed, 31 insertions(+), 10 deletions(-)
diff --git a/src/main/java/com/dasanjos/java/http/HttpResponse.java b/src/main/java/com/dasanjos/java/http/HttpResponse.java
index cd5410b..fd0cb44 100644
--- a/src/main/java/com/dasanjos/java/http/HttpResponse.java
+++ b/src/main/java/com/dasanjos/java/http/HttpResponse.java
@@ -36,10 +36,8 @@ public HttpResponse(HttpRequest req) throws IOException {
public HttpResponse(HttpRequest req, File webRoot) throws IOException {
switch (req.method) {
- case HEAD:
- fillHeaders(Status._200);
- break;
case GET:
+ case HEAD:
try {
String rootPath = webRoot.getCanonicalPath();
File file = new File(webRoot, req.uri);
@@ -54,7 +52,9 @@ public HttpResponse(HttpRequest req, File webRoot) throws IOException {
if (indexFile.exists() && !indexFile.isDirectory()) {
fillHeaders(Status._200);
setContentType(indexFile.getName(), headers);
- fillResponse(getBytes(indexFile));
+ if (req.method == Method.GET) {
+ fillResponse(getBytes(indexFile));
+ }
return;
}
@@ -77,11 +77,15 @@ public HttpResponse(HttpRequest req, File webRoot) throws IOException {
result.append(" " + fileName + "\n");
}
result.append("