Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 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
6df1039
code funkar ska lägga till tester till StaticFileHandler
Boppler12 Feb 22, 2026
8f240f9
lagt till en webrott som behövs för testerna jag jobbar på =)
Boppler12 Feb 22, 2026
cb5e5ad
lagt till tester för StaticFileHandler
Boppler12 Feb 22, 2026
c79c1a6
förbättrat FileCache med LRU-policy och tråd-säkerhet, lagt till bätt…
Boppler12 Feb 25, 2026
1c57ca3
Fixar problem
Boppler12 Feb 25, 2026
919be74
Feature/13 implement config file (#22)
MartinStenhagen Feb 17, 2026
a8868e0
Enhancement/404 page not found (#53)
codebyNorthsteep Feb 17, 2026
21079fb
Feature/issue59 run configloader (#61)
MartinStenhagen Feb 18, 2026
d121cfa
23 define and create filter interface (#46)
eraiicphu Feb 18, 2026
ad9e1c4
Feature/mime type detection #8 (#47)
gitnes94 Feb 18, 2026
d12df61
Dockerfile update (#52) (#63)
Xeutos Feb 19, 2026
5514dfc
Added comprehensive README.MD (#67)
gitnes94 Feb 19, 2026
4bc2b13
Fix: Path traversal vulnerability in StaticFileHandler (#65)
apaegs Feb 19, 2026
34febcd
Resolve port: CLI > config > default (#29)
viktorlindell12 Feb 23, 2026
64742ca
Refactor status codes to constants #71 (#77)
eeebbaandersson Feb 23, 2026
4b79dd4
fixed file path (#86)
gurkvatten Feb 24, 2026
1231b1a
Fix path in Dockerfile for `www` directory copy operation (#87)
codebyNorthsteep Feb 24, 2026
304a0e9
Feature/27 ipfilter (#70)
apaegs Feb 24, 2026
94630dc
Feature/LocaleFilter (#81)
AntonAhlqvist Feb 25, 2026
654449d
updaterat i StaticFileHandler och skapat CacheFilter
Boppler12 Feb 20, 2026
028bf60
förbättrat FileCache med LRU-policy och tråd-säkerhet, lagt till bätt…
Boppler12 Feb 25, 2026
b2f045a
förbättrat StaticFileHandler med fil-strömning för stora filer, minne…
Boppler12 Feb 25, 2026
754e7fe
förbättrat HttpResponseBuilder med stöd för header-bygge, optimerad c…
Boppler12 Feb 25, 2026
e8781bd
Merge branch 'main' into Implement-file-streaming-for-large-files-#68
Boppler12 Feb 25, 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
42 changes: 42 additions & 0 deletions src/main/java/org/example/CacheFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.example;

import java.io.IOException;
import java.util.Objects;

public class CacheFilter {
private final FileCache cache = new FileCache();

public byte[] getOrFetch(String uri, FileProvider provider) throws IOException {
Objects.requireNonNull(uri, "URI kan inte vara null");
Objects.requireNonNull(provider, "Provider kan inte vara null");

// Atomic operation: get() returnerar null om entry inte finns
byte[] cached = cache.get(uri);

if (cached != null) {
logCacheHit(uri);
return cached;
}

logCacheMiss(uri);
byte[] fileBytes = provider.fetch(uri);

if (fileBytes != null) {
cache.put(uri, fileBytes);
}
return fileBytes;
}

private void logCacheHit(String uri) {
System.out.println("Cache hit for: " + uri);
}

private void logCacheMiss(String uri) {
System.out.println("Cache miss for: " + uri);
}

@FunctionalInterface
public interface FileProvider {
byte[] fetch(String uri) throws IOException;
}
}
156 changes: 156 additions & 0 deletions src/main/java/org/example/FileCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@

package org.example;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
* Thread-safe file cache med LRU eviction policy.
* Lagrar upp till MAX_CACHE_SIZE bytes totalt.
*/
public class FileCache {
private static final long MAX_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
private static final int MAX_ENTRIES = 1000;

private final Map<String, CacheEntry> cache;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private long currentSize = 0;

public FileCache() {
// LinkedHashMap med access-order LRU
// accessOrder=true betyder: get() och put() uppdaterar ordningen
this.cache = new LinkedHashMap<String, CacheEntry>(16, 0.75f, true);
}

/**
* Hämta data från cache OCH uppdatera LRU-ordningen.
* MÅSTE ha write-lock eftersom get() modifierar LinkedHashMap's internal state.
*/
public byte[] get(String key) {
Objects.requireNonNull(key, "Key kan inte vara null");
lock.writeLock().lock(); // ← WRITE-LOCK, inte read-lock!
try {
CacheEntry entry = cache.get(key);
return entry != null ? entry.data : null;
} finally {
lock.writeLock().unlock();
}
}

/**
* Hämta data utan att uppdatera LRU-ordningen (för diagnostik).
* Kan använda read-lock eftersom vi inte modifierar LinkedHashMap's state.
*/
public byte[] peek(String key) {
Objects.requireNonNull(key, "Key kan inte vara null");
lock.readLock().lock();
try {
CacheEntry entry = cache.get(key);
return entry != null ? entry.data : null;
} finally {
lock.readLock().unlock();
}
}

public void put(String key, byte[] value) {
Objects.requireNonNull(key, "Key kan inte vara null");
Objects.requireNonNull(value, "Value kan inte vara null");

lock.writeLock().lock();
try {
// Guard mot filer större än cache
if (value.length > MAX_CACHE_SIZE) {
System.out.println("⚠️ Skipping oversized entry: " + key +
" (" + (value.length / 1024 / 1024) + "MB > " +
(MAX_CACHE_SIZE / 1024 / 1024) + "MB)");
return;
}

// Ta bort gamla posten om den finns för att uppdatera storlek
CacheEntry oldEntry = cache.remove(key);
if (oldEntry != null) {
currentSize -= oldEntry.data.length;
}

// Evicta medan nödvändigt INNAN vi lägger till ny post
while ((currentSize + value.length > MAX_CACHE_SIZE ||
cache.size() >= MAX_ENTRIES) && !cache.isEmpty()) {
evictLeastRecentlyUsedUnsafe();
}

// Lägg till ny post
cache.put(key, new CacheEntry(value));
currentSize += value.length;
} finally {
lock.writeLock().unlock();
}
}

/**
* Evicta minst nyligen använd entry (MÅSTE VARA UNDER WRITE LOCK)
*/
private void evictLeastRecentlyUsedUnsafe() {
// LinkedHashMap är sorterad efter access order (LRU)
// Första entry är den minst nyligen använda
Map.Entry<String, CacheEntry> eldest = cache.entrySet().iterator().next();

String key = eldest.getKey();
CacheEntry entry = eldest.getValue();

cache.remove(key);
currentSize -= entry.data.length;

System.out.println("✗ Evicted from cache: " + key +
" (" + (entry.data.length / 1024) + "KB)");
}

public void clear() {
lock.writeLock().lock();
try {
cache.clear();
currentSize = 0;
} finally {
lock.writeLock().unlock();
}
}

public int size() {
lock.readLock().lock();
try {
return cache.size();
} finally {
lock.readLock().unlock();
}
}

public long getCurrentSizeInBytes() {
lock.readLock().lock();
try {
return currentSize;
} finally {
lock.readLock().unlock();
}
}

public boolean contains(String key) {
lock.readLock().lock();
try {
return cache.containsKey(key);
} finally {
lock.readLock().unlock();
}
}

private static class CacheEntry {
final byte[] data;
final long timestamp;

CacheEntry(byte[] data) {
this.data = data;
this.timestamp = System.currentTimeMillis();
}
}
}
118 changes: 85 additions & 33 deletions src/main/java/org/example/StaticFileHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,67 +3,119 @@
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.io.*;
import java.nio.file.Files;
import java.util.Objects;

public class StaticFileHandler {
private static final long FILE_SIZE_THRESHOLD = 1024 * 1024; // 1MB
private static final int BUFFER_SIZE = 8192; // 8KB

private final String WEB_ROOT;
private byte[] fileBytes;
private int statusCode;

// Constructor for production
public StaticFileHandler() {
WEB_ROOT = "www";
}

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

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 {
Objects.requireNonNull(outputStream, "outputStream kan inte vara null");
Objects.requireNonNull(uri, "uri kan inte vara null");

// Null-byte detection BEFORE sanitizing
if (uri.contains("\0")) {
sendErrorResponse(outputStream, SC_FORBIDDEN, "403 Forbidden");
return;
}

// Sanitize URI (now safe - no null-bytes to remove)
uri = sanitizeUri(uri);

// 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;
sendErrorResponse(outputStream, SC_FORBIDDEN, "403 Forbidden");
return;
}

// Read file
// Send file or error
if (file.isFile()) {
fileBytes = Files.readAllBytes(file.toPath());
statusCode = SC_OK;
sendFile(outputStream, file, uri);
} 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;
// Return 404, NOT the error file with 200 OK
sendErrorResponse(outputStream, SC_NOT_FOUND, "404 Not Found");
}
}
Comment thread
Boppler12 marked this conversation as resolved.

public void sendGetRequest(OutputStream outputStream, String uri) throws IOException {
handleGetRequest(uri);
private String sanitizeUri(String 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.replaceAll("^/+", ""); // Removed .replace("\0", "") since we check earlier
return uri;
}

private void sendFile(OutputStream out, File file, String uri) throws IOException {
long fileSize = file.length();

// For small files, use cached approach
if (fileSize < FILE_SIZE_THRESHOLD) {
sendFileWithCache(out, file, uri);
} else {
// For large files, stream with headers
sendFileStreamed(out, file, uri, fileSize);
}
}

private void sendFileWithCache(OutputStream out, File file, String uri) throws IOException {
byte[] fileBytes = Files.readAllBytes(file.toPath());

HttpResponseBuilder response = new HttpResponseBuilder();
response.setStatusCode(statusCode);
// Use MimeTypeDetector instead of hardcoded text/html
response.setStatusCode(SC_OK);
response.setContentTypeFromFilename(uri);
response.setBody(fileBytes);
outputStream.write(response.build());
outputStream.flush();

out.write(response.build());
out.flush();
}

private void sendFileStreamed(OutputStream out, File file, String uri, long fileSize) throws IOException {
// Send headers first
HttpResponseBuilder response = new HttpResponseBuilder();
response.setStatusCode(SC_OK);
response.setContentTypeFromFilename(uri);
response.setHeader("Content-Length", String.valueOf(fileSize));
response.setHeader("Connection", "close");

byte[] headers = response.buildHeaders();
out.write(headers);

// Stream body
try (InputStream in = Files.newInputStream(file.toPath())) {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead;

while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
out.flush();
}
}

private void sendErrorResponse(OutputStream out, int statusCode, String message) throws IOException {
HttpResponseBuilder response = new HttpResponseBuilder();
response.setStatusCode(statusCode);
response.setContentTypeFromFilename("error.html");
response.setBody(message.getBytes(java.nio.charset.StandardCharsets.UTF_8));

out.write(response.build());
out.flush();
}
}
25 changes: 25 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,31 @@ public void setContentTypeFromFilename(String filename) {
setHeader("Content-Type", mimeType);
}

public byte[] buildHeaders() {
StringBuilder headerBuilder = new StringBuilder();

String reason = REASON_PHRASES.getOrDefault(statusCode, "");
headerBuilder.append(PROTOCOL).append(" ").append(statusCode);
if (!reason.isEmpty()) {
headerBuilder.append(" ").append(reason);
}
headerBuilder.append(CRLF);

headers.forEach((k, v) -> headerBuilder.append(k).append(": ").append(v).append(CRLF));

if (!headers.containsKey("Content-Length")) {
headerBuilder.append("Content-Length: ").append(0).append(CRLF);
}

if (!headers.containsKey("Connection")) {
headerBuilder.append("Connection: close").append(CRLF);
}

headerBuilder.append(CRLF);

return headerBuilder.toString().getBytes(StandardCharsets.UTF_8);
}

/*
* 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