Skip to content

Commit c473d5b

Browse files
Refactor StaticFileHandler and test suite to use HttpRequest and Http… (#107)
* Refactor StaticFileHandler and test suite to use HttpRequest and HttpResponseBuilder. * Refactor test cases to use HttpResponseBuilder assertions and enhance URI normalization in StaticFileHandler. * Add support for 405 responses to non-GET methods in StaticFileHandler and tests. * Set `Allow` header to "GET" for 405 responses in `StaticFileHandler`, update test coverage (#111) * Comprehensive Unit Tests for FilterChainImpl after implemented TerminalHandler (#113) * Add unit test for `FilterChainImpl` to verify terminal handler invocation without filters * Test confirms that chain.doFilter() correctly triggers the next filter in sequence and terminates at the handler. * Test ensures TerminalHandler is bypassed when a filter blocks the chain --------- Co-authored-by: Caroline Nordbrandt <caroline_nordbradt@hotmail.com>
1 parent 2f851e9 commit c473d5b

8 files changed

Lines changed: 277 additions & 111 deletions

File tree

src/main/java/org/example/ConnectionHandler.java

Lines changed: 10 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
package org.example;
22

33
import org.example.config.AppConfig;
4-
import org.example.filter.IpFilter;
5-
import org.example.httpparser.HttpParser;
6-
import org.example.httpparser.HttpRequest;
7-
import java.util.ArrayList;
8-
import java.util.List;
4+
import org.example.config.ConfigLoader;
95
import org.example.filter.Filter;
106
import org.example.filter.FilterChainImpl;
7+
import org.example.filter.IpFilter;
118
import org.example.http.HttpResponseBuilder;
12-
import org.example.config.ConfigLoader;
9+
import org.example.httpparser.HttpParser;
10+
import org.example.httpparser.HttpRequest;
1311

1412
import java.io.IOException;
1513
import java.net.Socket;
14+
import java.util.ArrayList;
15+
import java.util.List;
1616

1717
public class ConnectionHandler implements AutoCloseable {
1818

@@ -69,36 +69,13 @@ public void runConnectionHandler() throws IOException {
6969
String clientIp = client.getInetAddress().getHostAddress();
7070
request.setAttribute("clientIp", clientIp);
7171

72-
HttpResponseBuilder response = applyFilters(request);
73-
74-
int statusCode = response.getStatusCode();
75-
if (statusCode == HttpResponseBuilder.SC_FORBIDDEN ||
76-
statusCode == HttpResponseBuilder.SC_BAD_REQUEST) {
77-
byte[] responseBytes = response.build();
78-
client.getOutputStream().write(responseBytes);
79-
client.getOutputStream().flush();
80-
return;
81-
}
82-
83-
resolveTargetFile(parser.getUri());
84-
sfh.sendGetRequest(client.getOutputStream(), uri);
85-
}
86-
87-
private HttpResponseBuilder applyFilters(HttpRequest request) {
8872
HttpResponseBuilder response = new HttpResponseBuilder();
8973

90-
FilterChainImpl chain = new FilterChainImpl(filters);
74+
FilterChainImpl chain = new FilterChainImpl(filters, sfh);
9175
chain.doFilter(request, response);
9276

93-
return response;
94-
}
95-
96-
private void resolveTargetFile(String uri) {
97-
if (uri == null || "/".equals(uri)) {
98-
this.uri = "index.html";
99-
} else {
100-
this.uri = uri.startsWith("/") ? uri.substring(1) : uri;
101-
}
77+
client.getOutputStream().write(response.build());
78+
client.getOutputStream().flush();
10279
}
10380

10481
@Override
@@ -129,4 +106,4 @@ private IpFilter createIpFilterFromConfig(AppConfig.IpFilterConfig config) {
129106
filter.init();
130107
return filter;
131108
}
132-
}
109+
}
Lines changed: 75 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,112 @@
11
package org.example;
22

33
import org.example.http.HttpResponseBuilder;
4-
import static org.example.http.HttpResponseBuilder.*;
4+
import org.example.httpparser.HttpRequest;
55

66
import java.io.File;
77
import java.io.IOException;
8-
import java.io.OutputStream;
98
import java.nio.file.Files;
109

11-
public class StaticFileHandler {
12-
private final String WEB_ROOT;
13-
private byte[] fileBytes;
14-
private int statusCode;
10+
import static org.example.http.HttpResponseBuilder.*;
11+
12+
public class StaticFileHandler implements org.example.server.TerminalHandler {
13+
private final String webRoot;
14+
public static final String TEXT_PLAIN_CHARSET_UTF_8 = "text/plain; charset=utf-8";
1515

1616
// Constructor for production
1717
public StaticFileHandler() {
18-
WEB_ROOT = "www";
18+
webRoot = "www";
1919
}
2020

2121
// Constructor for tests, otherwise the www folder won't be seen
2222
public StaticFileHandler(String webRoot) {
23-
WEB_ROOT = webRoot;
23+
this.webRoot = webRoot;
2424
}
2525

26-
private void handleGetRequest(String uri) throws IOException {
26+
private void handleGetRequest(HttpRequest request, HttpResponseBuilder response) {
27+
byte[] fileBytes;
28+
if (!"GET".equals(request.getMethod())) {
29+
fileBytes = "405 Method Not Allowed".getBytes(java.nio.charset.StandardCharsets.UTF_8);
30+
response.setContentType(TEXT_PLAIN_CHARSET_UTF_8);
31+
response.setStatusCode(SC_METHOD_NOT_ALLOWED);
32+
response.setHeader("Allow", "GET");
33+
response.setBody(fileBytes);
34+
return;
35+
}
36+
37+
String uri = request.getPath();
38+
if (uri == null) {
39+
uri = "";
40+
}
41+
2742
// Sanitize URI
2843
int q = uri.indexOf('?');
2944
if (q >= 0) uri = uri.substring(0, q);
3045
int h = uri.indexOf('#');
3146
if (h >= 0) uri = uri.substring(0, h);
3247
uri = uri.replace("\0", "");
33-
if (uri.startsWith("/")) uri = uri.substring(1);
48+
49+
uri = defaultFile(uri);
3450

3551
// Path traversal check
36-
File root = new File(WEB_ROOT).getCanonicalFile();
37-
File file = new File(root, uri).getCanonicalFile();
38-
if (!file.toPath().startsWith(root.toPath())) {
39-
fileBytes = "403 Forbidden".getBytes(java.nio.charset.StandardCharsets.UTF_8);
40-
statusCode = SC_FORBIDDEN;
41-
return;
52+
File root;
53+
File file;
54+
try {
55+
root = new File(webRoot).getCanonicalFile();
56+
file = new File(root, uri).getCanonicalFile();
57+
} catch (IOException e) {
58+
throw new RuntimeException(e);
4259
}
4360

44-
// Read file
45-
if (file.isFile()) {
46-
fileBytes = Files.readAllBytes(file.toPath());
47-
statusCode = SC_OK;
61+
if (!file.toPath().startsWith(root.toPath())) {
62+
fileBytes = "403 Forbidden".getBytes(java.nio.charset.StandardCharsets.UTF_8);
63+
response.setContentType(TEXT_PLAIN_CHARSET_UTF_8);
64+
response.setStatusCode(SC_FORBIDDEN);
65+
} else if (file.isFile()) {
66+
try {
67+
fileBytes = Files.readAllBytes(file.toPath());
68+
} catch (IOException e) {
69+
throw new RuntimeException(e);
70+
}
71+
response.setContentTypeFromFilename(file.toString());
72+
response.setStatusCode(SC_OK);
4873
} else {
49-
File errorFile = new File(WEB_ROOT, "pageNotFound.html");
74+
File errorFile = new File(webRoot, "pageNotFound.html");
5075
if (errorFile.isFile()) {
51-
fileBytes = Files.readAllBytes(errorFile.toPath());
76+
try {
77+
fileBytes = Files.readAllBytes(errorFile.toPath());
78+
} catch (IOException e) {
79+
throw new RuntimeException(e);
80+
}
81+
response.setContentTypeFromFilename(errorFile.toString());
5282
} else {
5383
fileBytes = "404 Not Found".getBytes(java.nio.charset.StandardCharsets.UTF_8);
84+
response.setContentType(TEXT_PLAIN_CHARSET_UTF_8);
5485
}
55-
statusCode = SC_NOT_FOUND;
86+
response.setStatusCode(SC_NOT_FOUND);
5687
}
88+
response.setBody(fileBytes);
5789
}
5890

59-
public void sendGetRequest(OutputStream outputStream, String uri) throws IOException {
60-
handleGetRequest(uri);
61-
HttpResponseBuilder response = new HttpResponseBuilder();
62-
response.setStatusCode(statusCode);
63-
// Use MimeTypeDetector instead of hardcoded text/html
64-
response.setContentTypeFromFilename(uri);
65-
response.setBody(fileBytes);
66-
outputStream.write(response.build());
67-
outputStream.flush();
91+
@Override
92+
public void handle(HttpRequest request, HttpResponseBuilder response) {
93+
handleGetRequest(request, response);
94+
}
95+
96+
private String defaultFile(String uri) {
97+
if (uri == null || uri.isBlank()) {
98+
return "index.html";
99+
}
100+
String normalized = uri;
101+
while (normalized.startsWith("/")) {
102+
normalized = normalized.substring(1);
103+
}
104+
if (normalized.isEmpty()) {
105+
return "index.html";
106+
}
107+
if (normalized.endsWith("/")) {
108+
normalized = normalized + "index.html";
109+
}
110+
return normalized;
68111
}
69112
}
Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
package org.example.filter;
22

3-
import org.example.httpparser.HttpRequest;
43
import org.example.http.HttpResponseBuilder;
4+
import org.example.httpparser.HttpRequest;
5+
import org.example.server.TerminalHandler;
56

67
import java.util.List;
7-
import java.util.function.BiConsumer;
8+
import java.util.Objects;
89

910
public class FilterChainImpl implements FilterChain {
1011

1112
private final List<Filter> filters;
12-
private final BiConsumer<HttpRequest, HttpResponseBuilder> terminalHandler;
13+
private final TerminalHandler terminalHandler;
1314
private int index = 0;
1415

1516
public FilterChainImpl(List<Filter> filters) {
@@ -19,9 +20,9 @@ public FilterChainImpl(List<Filter> filters) {
1920
}
2021

2122
public FilterChainImpl(List<Filter> filters,
22-
BiConsumer<HttpRequest, HttpResponseBuilder> terminalHandler) {
23-
this.filters = filters;
24-
this.terminalHandler = terminalHandler;
23+
TerminalHandler terminalHandler) {
24+
this.filters = Objects.requireNonNull(filters, "filters must not be null");
25+
this.terminalHandler = Objects.requireNonNull(terminalHandler, "terminalHandler must not be null");
2526
}
2627

2728
@Override
@@ -30,7 +31,7 @@ public void doFilter(HttpRequest request, HttpResponseBuilder response) {
3031
Filter next = filters.get(index++);
3132
next.doFilter(request, response, this);
3233
} else {
33-
terminalHandler.accept(request, response);
34+
terminalHandler.handle(request, response);
3435
}
3536
}
3637
}

src/main/java/org/example/http/HttpResponseBuilder.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public class HttpResponseBuilder {
2626
public static final int SC_UNAUTHORIZED = 401;
2727
public static final int SC_FORBIDDEN = 403;
2828
public static final int SC_NOT_FOUND = 404;
29+
public static final int SC_METHOD_NOT_ALLOWED = 405;
2930

3031
// SERVER ERROR
3132
public static final int SC_INTERNAL_SERVER_ERROR = 500;
@@ -57,6 +58,7 @@ public class HttpResponseBuilder {
5758
Map.entry(SC_UNAUTHORIZED, "Unauthorized"),
5859
Map.entry(SC_FORBIDDEN, "Forbidden"),
5960
Map.entry(SC_NOT_FOUND, "Not Found"),
61+
Map.entry(SC_METHOD_NOT_ALLOWED, "Method Not Allowed"),
6062
Map.entry(SC_INTERNAL_SERVER_ERROR, "Internal Server Error"),
6163
Map.entry(SC_BAD_GATEWAY, "Bad Gateway"),
6264
Map.entry(SC_SERVICE_UNAVAILABLE, "Service Unavailable"),
@@ -166,4 +168,8 @@ public byte[] getByteBody() {
166168

167169
return bytebody == null ? null : Arrays.copyOf(bytebody, bytebody.length);
168170
}
171+
172+
public void setContentType(String contentType) {
173+
setHeader("Content-Type", contentType);
174+
}
169175
}

src/main/java/org/example/server/ConfigurableFilterPipeline.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22

33
import org.example.filter.Filter;
44
import org.example.filter.FilterChainImpl;
5-
import org.example.httpparser.HttpRequest;
65
import org.example.http.HttpResponseBuilder;
6+
import org.example.httpparser.HttpRequest;
77

88
import java.util.ArrayList;
99
import java.util.Comparator;
1010
import java.util.List;
1111
import java.util.Objects;
12-
import java.util.function.BiConsumer;
1312

1413
public class ConfigurableFilterPipeline {
1514

@@ -22,7 +21,7 @@ public ConfigurableFilterPipeline(List<FilterRegistration> registrations) {
2221
}
2322

2423
public HttpResponseBuilder execute(HttpRequest request,
25-
BiConsumer<HttpRequest, HttpResponseBuilder> terminalHandler) {
24+
TerminalHandler terminalHandler) {
2625

2726
Objects.requireNonNull(request, "request must not be null");
2827
Objects.requireNonNull(terminalHandler, "terminalHandler must not be null");
@@ -70,4 +69,4 @@ private boolean matchesAny(List<String> patterns, String path) {
7069

7170
return false;
7271
}
73-
}
72+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package org.example.server;
2+
3+
import org.example.http.HttpResponseBuilder;
4+
import org.example.httpparser.HttpRequest;
5+
6+
public interface TerminalHandler {
7+
void handle(HttpRequest request, HttpResponseBuilder responseBuilder);
8+
}

0 commit comments

Comments
 (0)