Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
b9600f6
ska vara klart nu har glömt att commit jobbet inser jag dock
Boppler12 Feb 17, 2026
07e47bf
har lagt till i line 20-25 samt line 29
Boppler12 Feb 17, 2026
8e0ab50
tester för FileCacheTest
Boppler12 Feb 17, 2026
fffdebf
updaterat i StaticFileHandler och skapat CacheFilter
Boppler12 Feb 20, 2026
c69b9dd
ska vara klart nu har glömt att commit jobbet inser jag dock
Boppler12 Feb 17, 2026
bfa3129
har lagt till i line 20-25 samt line 29
Boppler12 Feb 21, 2026
3b34b91
tester för FileCacheTest
Boppler12 Feb 17, 2026
b2ef693
updaterat i StaticFileHandler och skapat CacheFilter
Boppler12 Feb 20, 2026
515bc8c
Merge remote-tracking branch 'origin/issue36' into Server‑side-cachin…
Boppler12 Feb 21, 2026
0a53b7f
vad problem med github
Boppler12 Feb 21, 2026
ddd7b40
vad problem med github
Boppler12 Feb 21, 2026
0c47d58
nu ska det funka
Boppler12 Feb 21, 2026
dd0530a
tog i bort cache git för missar
Boppler12 Feb 23, 2026
7f73baf
la till concurrentHashMap i rad 8
Boppler12 Feb 23, 2026
0a3c08c
refactored caching by removing FileCache completely; implemented an i…
Boppler12 Feb 23, 2026
56cc3e8
removed extra whitespace in MAX_CACHE_BYTES declaration
Boppler12 Feb 24, 2026
d20bb6f
removed things not used in code
Boppler12 Feb 24, 2026
3231ee1
ändringar som kodrabbit förstog
Boppler12 Feb 25, 2026
99f5fd7
ändringar som kodrabbit förstog
Boppler12 Feb 25, 2026
04cba97
ändringar som kodrabbit förstog
Boppler12 Feb 25, 2026
c158677
refactored StaticFileHandler to improve error handling, enhance sanit…
Boppler12 Feb 25, 2026
811ccce
fixa lite och hade glömt att commit vissa saker efter ändringar
Boppler12 Feb 25, 2026
1df4286
glömde av att fixa problem i cachefilter
Boppler12 Feb 25, 2026
19d0a48
updated CacheFilter to clean dead comments and import Paths/Path; imp…
Boppler12 Feb 25, 2026
d4227b0
fixat så problem som kommer up vid commit
Boppler12 Feb 25, 2026
3497f8a
Merge branch 'main' into issue36
Boppler12 Feb 25, 2026
bb0d51a
kom up problem när github kör ska ha fixat det tror jag
Boppler12 Feb 25, 2026
f5b2f44
kom up problem när github kör ska ha fixat det tror jag
Boppler12 Feb 25, 2026
704f8d8
Gjort det coderabbit bätt mig om
Boppler12 Feb 25, 2026
3f107c6
fixa problem
Boppler12 Feb 25, 2026
d87171b
fixat mer
Boppler12 Feb 25, 2026
c74b88b
fixat mer från coderabbit
Boppler12 Feb 25, 2026
baa3981
daniel har hjälp mig med ändringar
Boppler12 Feb 25, 2026
f3bb1ac
daniel har hjälp mig med ändringar
Boppler12 Feb 26, 2026
2f8312b
fixat så uri rensas och återanvänds, lade till ny metod för sanering
Boppler12 Feb 26, 2026
32cbdd3
Merge branch 'main' into issue36
Boppler12 Feb 26, 2026
331615a
fixa compleir issue
Boppler12 Feb 26, 2026
1be780f
behövde fixa saker efter en rebase
Boppler12 Feb 26, 2026
56ba520
should have made it according to the mall given
Boppler12 Feb 28, 2026
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
3 changes: 2 additions & 1 deletion src/main/java/org/example/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ public static void main(String[] args) {

int port = resolvePort(args, appConfig.server().port());

new TcpServer(port, ConnectionHandler::new).start();
FileCache fileCache = new FileCache(10);
new TcpServer(port, socket -> new ConnectionHandler(socket, fileCache)).start();
}

static int resolvePort(String[] args, int configPort) {
Expand Down
102 changes: 53 additions & 49 deletions src/main/java/org/example/ConnectionHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,23 @@
import java.io.IOException;
import java.net.Socket;

public class ConnectionHandler implements AutoCloseable {
import java.io.OutputStream;
import java.nio.file.NoSuchFileException;

Socket client;
String uri;
public class ConnectionHandler implements AutoCloseable {
private final Socket client;
private final List<Filter> filters;
String webRoot;
private final String webRoot;
private final FileCache fileCache;

public ConnectionHandler(Socket client) {
this.client = client;
this.filters = buildFilters();
this.webRoot = null;
public ConnectionHandler(Socket client, FileCache fileCache) {
this(client, "www", fileCache);
}

public ConnectionHandler(Socket client, String webRoot) {
public ConnectionHandler(Socket client, String webRoot, FileCache fileCache) {
this.client = client;
this.webRoot = webRoot;
this.fileCache = fileCache;
this.filters = buildFilters();
}

Expand All @@ -40,67 +41,73 @@ private List<Filter> buildFilters() {
if (Boolean.TRUE.equals(ipFilterConfig.enabled())) {
list.add(createIpFilterFromConfig(ipFilterConfig));
}
// Add more filters here...
return list;
}

public void runConnectionHandler() throws IOException {
StaticFileHandler sfh;
HttpParser parser = parseRequest();
HttpRequest request = buildHttpRequest(parser);

if (webRoot != null) {
sfh = new StaticFileHandler(webRoot);
} else {
sfh = new StaticFileHandler();
}
if (isForbiddenByFilters(request)) return;
if (isPathTraversal(parser.getUri())) return;

serveFile(parser.getUri());
}

private HttpParser parseRequest() throws IOException {
HttpParser parser = new HttpParser();
parser.setReader(client.getInputStream());
parser.parseRequest();
parser.parseHttp();
return parser;
}

private HttpRequest buildHttpRequest(HttpParser parser) {
HttpRequest request = new HttpRequest(
parser.getMethod(),
parser.getUri(),
parser.getVersion(),
parser.getHeadersMap(),
""
parser.getMethod(), parser.getUri(), parser.getVersion(),
parser.getHeadersMap(), ""
);
request.setAttribute("clientIp", client.getInetAddress().getHostAddress());
return request;
}

String clientIp = client.getInetAddress().getHostAddress();
request.setAttribute("clientIp", clientIp);

HttpResponseBuilder response = applyFilters(request);
private boolean isForbiddenByFilters(HttpRequest request) throws IOException {
HttpResponseBuilder response = new HttpResponseBuilder();
new FilterChainImpl(filters).doFilter(request, response);

int statusCode = response.getStatusCode();
if (statusCode == HttpResponseBuilder.SC_FORBIDDEN ||
statusCode == HttpResponseBuilder.SC_BAD_REQUEST) {
byte[] responseBytes = response.build();
client.getOutputStream().write(responseBytes);
int status = response.getStatusCode();
if (status == HttpResponseBuilder.SC_FORBIDDEN || status == HttpResponseBuilder.SC_BAD_REQUEST) {
client.getOutputStream().write(response.build());
client.getOutputStream().flush();
return;
return true;
}

resolveTargetFile(parser.getUri());
sfh.sendGetRequest(client.getOutputStream(), uri);
return false;
}

private HttpResponseBuilder applyFilters(HttpRequest request) {
HttpResponseBuilder response = new HttpResponseBuilder();

FilterChainImpl chain = new FilterChainImpl(filters);
chain.doFilter(request, response);

return response;
private boolean isPathTraversal(String uri) throws IOException {
if (uri.contains("..")) {
sendErrorResponse(client.getOutputStream(), 403, "Forbidden");
return true;
}
return false;
}

private void resolveTargetFile(String uri) {
if (uri == null || "/".equals(uri)) {
this.uri = "index.html";
} else {
this.uri = uri.startsWith("/") ? uri.substring(1) : uri;
private void serveFile(String uri) throws IOException {
StaticFileHandler sfh = new StaticFileHandler(webRoot, fileCache);
try {
sfh.sendGetRequest(client.getOutputStream(), uri);
} catch (NoSuchFileException e) {
sendErrorResponse(client.getOutputStream(), 404, "Not Found");
} catch (Exception e) {
sendErrorResponse(client.getOutputStream(), 500, "Internal Server Error");
}
}

private void sendErrorResponse(OutputStream out, int statusCode, String message) throws IOException {
out.write(HttpResponseBuilder.createErrorResponse(statusCode, message));
out.flush();
}

@Override
public void close() throws Exception {
client.close();
Expand All @@ -109,19 +116,16 @@ public void close() throws Exception {
private IpFilter createIpFilterFromConfig(AppConfig.IpFilterConfig config) {
IpFilter filter = new IpFilter();

// Set mode
if ("ALLOWLIST".equalsIgnoreCase(config.mode())) {
filter.setMode(IpFilter.FilterMode.ALLOWLIST);
} else {
filter.setMode(IpFilter.FilterMode.BLOCKLIST);
}

// Add blocked IPs
for (String ip : config.blockedIps()) {
filter.addBlockedIp(ip);
}

// Add allowed IPs
for (String ip : config.allowedIps()) {
filter.addAllowedIp(ip);
}
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/org/example/FileCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.example;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;

public class FileCache {
private final Map<String, byte[]> cache;

public FileCache(int maxSize) {
this.cache = Collections.synchronizedMap(new LinkedHashMap<String, byte[]>(maxSize, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<String, byte[]> eldest) {
return size() > maxSize;
}
});
}

public byte[] get(String key) { return cache.get(key); }
public void put(String key, byte[] content) { cache.put(key, content); }
public void clear() { cache.clear(); }
public int size() { return cache.size(); }
}

81 changes: 35 additions & 46 deletions src/main/java/org/example/StaticFileHandler.java
Original file line number Diff line number Diff line change
@@ -1,69 +1,58 @@

package org.example;

import org.example.http.HttpResponseBuilder;
import static org.example.http.HttpResponseBuilder.*;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;

public class StaticFileHandler {
private final String WEB_ROOT;
private byte[] fileBytes;
private int statusCode;
private static final String DEFAULT_WEB_ROOT = "www";
private final String webRoot;
Comment thread
Boppler12 marked this conversation as resolved.
private final FileCache fileCache;

// Constructor for production
public StaticFileHandler() {
WEB_ROOT = "www";
public StaticFileHandler(FileCache fileCache) {
this(DEFAULT_WEB_ROOT, fileCache);
}

// Constructor for tests, otherwise the www folder won't be seen
public StaticFileHandler(String webRoot) {
WEB_ROOT = webRoot;
public StaticFileHandler(String webRoot, FileCache fileCache) {
this.webRoot = webRoot;
this.fileCache = fileCache;
}

private void handleGetRequest(String uri) throws IOException {
// Sanitize URI
int q = uri.indexOf('?');
if (q >= 0) uri = uri.substring(0, q);
int h = uri.indexOf('#');
if (h >= 0) uri = uri.substring(0, h);
uri = uri.replace("\0", "");
if (uri.startsWith("/")) uri = uri.substring(1);
public void sendGetRequest(OutputStream outputStream, String uri) throws IOException {
String sanitizedUri = sanitizeUri(uri);
Path filePath = Path.of(webRoot, sanitizedUri);

// Path traversal check
File root = new File(WEB_ROOT).getCanonicalFile();
File file = new File(root, uri).getCanonicalFile();
if (!file.toPath().startsWith(root.toPath())) {
fileBytes = "403 Forbidden".getBytes(java.nio.charset.StandardCharsets.UTF_8);
statusCode = SC_FORBIDDEN;
return;
}
// Använd fullständig sökväg som cache-nyckel
String cacheKey = filePath.toString();
byte[] fileBytes = fileCache.get(cacheKey);

// Read file
if (file.isFile()) {
fileBytes = Files.readAllBytes(file.toPath());
statusCode = SC_OK;
} else {
File errorFile = new File(WEB_ROOT, "pageNotFound.html");
if (errorFile.isFile()) {
fileBytes = Files.readAllBytes(errorFile.toPath());
} else {
fileBytes = "404 Not Found".getBytes(java.nio.charset.StandardCharsets.UTF_8);
}
statusCode = SC_NOT_FOUND;
if (fileBytes == null) {
fileBytes = Files.readAllBytes(filePath);
fileCache.put(cacheKey, fileBytes);
}
}

public void sendGetRequest(OutputStream outputStream, String uri) throws IOException {
handleGetRequest(uri);
HttpResponseBuilder response = new HttpResponseBuilder();
response.setStatusCode(statusCode);
// Use MimeTypeDetector instead of hardcoded text/html
response.setContentTypeFromFilename(uri);
response.setContentTypeFromFilename(sanitizedUri);
response.setBody(fileBytes);
outputStream.write(response.build());
outputStream.flush();
}

private String sanitizeUri(String uri) {
if (uri == null || uri.isEmpty() || uri.equals("/")) return "index.html";

// Ta bort query, anchors, start-snedstreck och null-bytes
String cleanUri = uri.split("[?#]")[0]
.replaceAll("^/+", "")
.replace("\0", "");

return cleanUri.isEmpty() ? "index.html" : cleanUri;
}

public void clearCache() {
fileCache.clear();
}
}
17 changes: 2 additions & 15 deletions src/main/java/org/example/TcpServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Map;

public class TcpServer {

Expand Down Expand Up @@ -43,29 +40,19 @@ protected void handleClient(Socket client) {
}

private void processRequest(Socket client) throws Exception {
ConnectionHandler handler = null;
try{
handler = connectionFactory.create(client);
try (ConnectionHandler handler = connectionFactory.create(client)) {
handler.runConnectionHandler();
} catch (Exception e) {
handleInternalServerError(client);
} finally {
if(handler != null)
handler.close();
}
}


private void handleInternalServerError(Socket client){
HttpResponseBuilder response = new HttpResponseBuilder();
response.setStatusCode(HttpResponseBuilder.SC_INTERNAL_SERVER_ERROR);
response.setHeaders(Map.of("Content-Type", "text/plain; charset=utf-8"));
response.setBody("⚠️ Internal Server Error 500 ⚠️");

if (!client.isClosed()) {
try {
OutputStream out = client.getOutputStream();
out.write(response.build());
out.write(HttpResponseBuilder.createErrorResponse(500, "Internal Server Error"));
out.flush();
} catch (IOException e) {
System.err.println("Failed to send 500 response: " + e.getMessage());
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/org/example/http/HttpResponseBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,18 @@ public void setContentTypeFromFilename(String filename) {
setHeader("Content-Type", mimeType);
}

public void setError(int statusCode, String message) {
setStatusCode(statusCode);
setHeader("Content-Type", "text/html; charset=UTF-8");
setBody(String.format("<html><body><h1>%d %s</h1></body></html>", statusCode, message));
}

public static byte[] createErrorResponse(int statusCode, String message) {
HttpResponseBuilder builder = new HttpResponseBuilder();
builder.setError(statusCode, message);
return builder.build();
}

/*
* Builds the complete HTTP response as a byte array and preserves binary content without corruption.
* @return Complete HTTP response (headers + body) as byte[]
Expand Down
Loading