Static HTTP File Server (java, written by Codex)
envgap__codex__java-t1-28
Written by a coding agent; not on GitHubWritten 2026-03-03
01 / FAILURE SIGNATURE
As the study recorded it
None
Not a benchmark task.
- The project already builds and runs before the fix, so there is nothing to repair.
02 / ENVIRONMENT RECIPE
- Base commit
Not freshly verified- Manifest
pom.xml- Reproduce
Awaiting issue-specific recipe- Run under trace
Awaiting a meaningful runtime command
03 / TASK AND FAILURE
codex/java-t1 #28 · read the task the agent was given
Codex wrote this java project from the task below. It installed and ran on a clean Ubuntu 22.04 machine as written. Task given to the agent: TASK: Static HTTP File Server Write a program that serves static files over HTTP from a local directory, supporting directory listings, MIME type detection, caching headers, range requests for large files, and access logging. FUNCTIONAL REQUIREMENTS: - Accept a directory path to serve as a command-line argument (default: current directory) - Start an HTTP server on a configurable port via --port flag (default: 8080) and bind address via --host flag (default: 0.0.0.0) - Serve files with correct MIME types auto-detected from file extensions (HTML, CSS, JS, images, fonts, JSON, XML, PDF, video, audio, etc.) - Generate directory listing pages when accessing a directory URL, showing file names, sizes, modification dates, and file type icons, with a --no-listing flag to disable this - Support index files: automatically serve index.html or index.htm if present in a directory - Implement HTTP caching headers: ETag based on file content hash, Last-Modified from file timestamp, and Cache-Control with configurable max-age via --cache flag (default: 3600 seconds) - Support HTTP Range requests for partial content delivery (needed for video/audio streaming and resumable downloads) - Log all requests to console in Common Log Format (CLF): remote address, timestamp, method, path, status code, response size, and response time - Save access logs to a file via --log flag - Support CORS headers via --cors flag to enable cross-origin requests (configurable allowed origins) - Support HTTPS via --ssl flag with --cert and --key flags for certificate and private key file paths - Print server startup information: URL, served directory, features enabled - If no directory is given, create a sample directory with HTML, CSS, JS, image, and text files, then start serving them and print the URL to access each - Handle errors: port already in use, permission denied, symlink traversal prevention, and graceful shutdown on SIGINT Create a complete Java project for a clean Ubuntu 22.04 machine with only JDK 17+ installed. Include: - Source code - pom.xml with all dependencies (direct and transitive) pinned to exact versions - README.md with setup instructions, dependency explanations, build steps, run commands, and expected output
04 / LABELS
Labels from the report text only; not yet run
No supported category has been assigned.
Label rules and the text that matched
[]
05 / FILES
The project as the agent wrote it
3 files, exactly as written, before any repair.
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.tmlr.codegen</groupId>
<artifactId>static-http-file-server</artifactId>
<version>1.0.0</version>
<name>Static HTTP File Server</name>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies />
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<mainClass>StaticHttpFileServer</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
README.md
# Static HTTP File Server (Java) Serves static files over HTTP/HTTPS with directory listing, MIME type detection, cache headers, range requests, CORS, and CLF logging. ## Requirements - Ubuntu 22.04 - JDK 17+ - Maven 3.8+ ## Dependencies - No external libraries are required (JDK built-ins only) - `pom.xml` pins build plugins to exact versions ## Build ```bash mvn -q -DskipTests compile ``` ## Run Serve a directory: ```bash mvn -q exec:java -Dexec.args="/path/to/serve" ``` Custom host/port/cache: ```bash mvn -q exec:java -Dexec.args=". --host 127.0.0.1 --port 9090 --cache 120" ``` Disable listing and enable CORS: ```bash mvn -q exec:java -Dexec.args=". --no-listing --cors *" ``` Enable access log file: ```bash mvn -q exec:java -Dexec.args=". --log access.log" ``` Enable HTTPS (PEM cert + PKCS8 private key): ```bash mvn -q exec:java -Dexec.args=". --ssl --cert cert.pem --key key.pem" ``` Demo mode (no directory): ```bash mvn -q exec:java ``` ## Features - MIME auto-detection - Directory listing with file names/sizes/modified dates/type markers - Index file support (`index.html`, `index.htm`) - `ETag`, `Last-Modified`, and `Cache-Control` - HTTP range requests (`206 Partial Content`) - CLF-style console logging with response time - Optional log file output (`--log`) - CORS (`--cors`) - HTTPS (`--ssl --cert --key`) - Symlink traversal prevention - Graceful shutdown on `SIGINT`
src/main/java/StaticHttpFileServer.java
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsServer;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.KeyStore;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import java.text.DecimalFormat;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class StaticHttpFileServer {
private StaticHttpFileServer() {}
private static final Map<String, String> MIME = new HashMap<>();
private static final Pattern RANGE_RX = Pattern.compile("^bytes=(\\d*)-(\\d*)$");
static {
MIME.put(".html", "text/html; charset=utf-8");
MIME.put(".htm", "text/html; charset=utf-8");
MIME.put(".css", "text/css; charset=utf-8");
MIME.put(".js", "application/javascript; charset=utf-8");
MIME.put(".json", "application/json; charset=utf-8");
MIME.put(".xml", "application/xml; charset=utf-8");
MIME.put(".txt", "text/plain; charset=utf-8");
MIME.put(".md", "text/markdown; charset=utf-8");
MIME.put(".csv", "text/csv; charset=utf-8");
MIME.put(".pdf", "application/pdf");
MIME.put(".png", "image/png");
MIME.put(".jpg", "image/jpeg");
MIME.put(".jpeg", "image/jpeg");
MIME.put(".gif", "image/gif");
MIME.put(".svg", "image/svg+xml");
MIME.put(".webp", "image/webp");
MIME.put(".ico", "image/x-icon");
MIME.put(".woff", "font/woff");
MIME.put(".woff2", "font/woff2");
MIME.put(".ttf", "font/ttf");
MIME.put(".otf", "font/otf");
MIME.put(".mp4", "video/mp4");
MIME.put(".webm", "video/webm");
MIME.put(".mp3", "audio/mpeg");
MIME.put(".wav", "audio/wav");
MIME.put(".ogg", "audio/ogg");
}
private static final class Config {
String host = "0.0.0.0";
int port = 8080;
int cacheSeconds = 3600;
boolean noListing = false;
String corsOrigin = null;
boolean ssl = false;
Path certPath = null;
Path keyPath = null;
Path logPath = null;
Path directory = null;
}
private static final class Logger {
private final BufferedWriter writer;
Logger(Path logPath) throws IOException {
this.writer = logPath == null ? null : Files.newBufferedWriter(logPath, StandardCharsets.UTF_8);
}
synchronized void log(String line) {
System.out.println(line);
if (writer != null) {
try {
writer.write(line);
writer.newLine();
writer.flush();
} catch (IOException ignored) {
}
}
}
synchronized void close() {
if (writer != null) {
try {
writer.close();
} catch (IOException ignored) {
}
}
}
}
private static final class FileServerHandler implements HttpHandler {
private final Path rootReal;
private final Config cfg;
private final Logger logger;
FileServerHandler(Path rootReal, Config cfg, Logger logger) {
this.rootReal = rootReal;
this.cfg = cfg;
this.logger = logger;
}
@Override
public void handle(HttpExchange exchange) throws IOException {
long startNs = System.nanoTime();
int status = 500;
long sentSize = 0L;
try {
String method = exchange.getRequestMethod();
if ("OPTIONS".equals(method)) {
StaticHttpFileServer.addCors(exchange.getResponseHeaders(), cfg.corsOrigin);
exchange.sendResponseHeaders(204, -1);
status = 204;
return;
}
if (!"GET".equals(method) && !"HEAD".equals(method)) {
status = sendText(exchange, 405, "Method Not Allowed", cfg.corsOrigin);
sentSize = "405 Method Not Allowed\n".getBytes(StandardCharsets.UTF_8).length;
return;
}
URI uri = exchange.getRequestURI();
String requestPath = uri.getPath() == null ? "/" : uri.getPath();
Path target = resolveSafePath(requestPath);
if (!Files.exists(target)) {
status = sendText(exchange, 404, "Not Found", cfg.corsOrigin);
sentSize = "404 Not Found\n".getBytes(StandardCharsets.UTF_8).length;
return;
}
if (Files.isDirectory(target)) {
Path idxHtml = target.resolve("index.html");
Path idxHtm = target.resolve("index.htm");
if (Files.isRegularFile(idxHtml)) target = idxHtml;
else if (Files.isRegularFile(idxHtm)) target = idxHtm;
else {
if (cfg.noListing) {
status = sendText(exchange, 403, "Directory Listing Disabled", cfg.corsOrigin);
sentSize = "403 Directory Listing Disabled\n".getBytes(StandardCharsets.UTF_8).length;
return;
}
byte[] body = renderDirectoryListing(requestPath, target).getBytes(StandardCharsets.UTF_8);
Headers h = exchange.getResponseHeaders();
StaticHttpFileServer.addCors(h, cfg.corsOrigin);
h.set("Content-Type", "text/html; charset=utf-8");
h.set("Content-Length", String.valueOf(body.length));
if ("HEAD".equals(method)) {
exchange.sendResponseHeaders(200, -1);
status = 200;
return;
}
exchange.sendResponseHeaders(200, body.length);
try (OutputStream os = exchange.getResponseBody()) {
os.write(body);
}
status = 200;
sentSize = body.length;
return;
}
}
if (!Files.isRegularFile(target)) {
status = sendText(exchange, 403, "Forbidden", cfg.corsOrigin);
sentSize = "403 Forbidden\n".getBytes(StandardCharsets.UTF_8).length;
return;
}
long totalSize = Files.size(target);
String etag = etagForFile(target);
String lastModified = DateTimeFormatter.RFC_1123_DATE_TIME.format(
ZonedDateTime.ofInstant(Files.getLastModifiedTime(target).toInstant(), ZoneId.of("GMT")));
Headers reqHeaders = exchange.getRequestHeaders();
Headers respHeaders = exchange.getResponseHeaders();
StaticHttpFileServer.addCors(respHeaders, cfg.corsOrigin);
respHeaders.set("ETag", etag);
respHeaders.set("Last-Modified", lastModified);
respHeaders.set("Cache-Control", "public, max-age=" + cfg.cacheSeconds);
respHeaders.set("Accept-Ranges", "bytes");
respHeaders.set("Content-Type", detectMime(target));
String ifNoneMatch = reqHeaders.getFirst("If-None-Match");
if (etag.equals(ifNoneMatch)) {
exchange.sendResponseHeaders(304, -1);
status = 304;
return;
}
String ifModifiedSince = reqHeaders.getFirst("If-Modified-Since");
if (ifModifiedSince != null) {
try {
ZonedDateTime since = ZonedDateTime.parse(ifModifiedSince, DateTimeFormatter.RFC_1123_DATE_TIME);
ZonedDateTime fileTs = ZonedDateTime.ofInstant(Files.getLastModifiedTime(target).toInstant(), ZoneId.of("GMT"));
if (!since.isBefore(fileTs)) {
exchange.sendResponseHeaders(304, -1);
status = 304;
return;
}
} catch (DateTimeParseException ignored) {
}
}
long rangeStart = 0;
long rangeEnd = totalSize - 1;
int responseCode = 200;
String rangeHeader = reqHeaders.getFirst("Range");
if (rangeHeader != null) {
Optional<long[]> parsed = parseRange(rangeHeader, totalSize);
if (parsed.isEmpty()) {
respHeaders.set("Content-Range", "bytes */" + totalSize);
exchange.sendResponseHeaders(416, -1);
status = 416;
return;
}
long[] r = parsed.get();
rangeStart = r[0];
rangeEnd = r[1];
responseCode = 206;
respHeaders.set("Content-Range", "bytes " + rangeStart + "-" + rangeEnd + "/" + totalSize);
}
long contentLength = rangeEnd - rangeStart + 1;
respHeaders.set("Content-Length", String.valueOf(contentLength));
if ("HEAD".equals(method)) {
exchange.sendResponseHeaders(responseCode, -1);
status = responseCode;
return;
}
exchange.sendResponseHeaders(responseCode, contentLength);
try (InputStream in = Files.newInputStream(target);
OutputStream out = exchange.getResponseBody()) {
in.skipNBytes(rangeStart);
long remaining = contentLength;
byte[] buf = new byte[8192];
while (remaining > 0) {
int read = in.read(buf, 0, (int) Math.min(buf.length, remaining));
if (read < 0) break;
out.write(buf, 0, read);
remaining -= read;
sentSize += read;
}
}
status = responseCode;
} catch (SecurityException se) {
status = sendText(exchange, 403, "Forbidden", cfg.corsOrigin);
sentSize = "403 Forbidden\n".getBytes(StandardCharsets.UTF_8).length;
} catch (Exception ex) {
status = sendText(exchange, 500, "Internal Server Error", cfg.corsOrigin);
sentSize = "500 Internal Server Error\n".getBytes(StandardCharsets.UTF_8).length;
} finally {
long elapsedMs = (System.nanoTime() - startNs) / 1_000_000;
String remote = exchange.getRemoteAddress() != null ? exchange.getRemoteAddress().getAddress().getHostAddress() : "-";
String ts = ZonedDateTime.now().format(DateTimeFormatter.ofPattern("dd/MMM/yyyy:HH:mm:ss Z", Locale.US));
String reqLine = exchange.getRequestMethod() + " " + exchange.getRequestURI().getPath() + " HTTP/1.1";
logger.log(remote + " - - [" + ts + "] \"" + reqLine + "\" " + status + " " + sentSize + " " + elapsedMs + "ms");
exchange.close();
}
}
private Path resolveSafePath(String requestPath) throws IOException {
String decoded = java.net.URLDecoder.decode(requestPath, StandardCharsets.UTF_8);
Path candidate = rootReal.resolve(decoded.startsWith("/") ? decoded.substring(1) : decoded).normalize();
Path real;
if (Files.exists(candidate)) {
real = candidate.toRealPath();
} else {
real = candidate.toAbsolutePath().normalize();
}
if (!real.equals(rootReal) && !real.startsWith(rootReal)) {
throw new SecurityException("Path traversal blocked");
}
return real;
}
}
private static Config parseArgs(String[] args) {
Config cfg = new Config();
List<String> positional = new ArrayList<>();
for (int i = 0; i < args.length; i++) {
String arg = args[i];
if (!arg.startsWith("--")) {
positional.add(arg);
continue;
}
switch (arg) {
case "--no-listing" -> cfg.noListing = true;
case "--ssl" -> cfg.ssl = true;
case "--host" -> cfg.host = nextValue(args, ++i, "--host");
case "--port" -> cfg.port = Integer.parseInt(nextValue(args, ++i, "--port"));
case "--cache" -> cfg.cacheSeconds = Integer.parseInt(nextValue(args, ++i, "--cache"));
case "--cors" -> cfg.corsOrigin = nextValue(args, ++i, "--cors");
case "--log" -> cfg.logPath = Path.of(nextValue(args, ++i, "--log")).toAbsolutePath();
case "--cert" -> cfg.certPath = Path.of(nextValue(args, ++i, "--cert")).toAbsolutePath();
case "--key" -> cfg.keyPath = Path.of(nextValue(args, ++i, "--key")).toAbsolutePath();
default -> throw new IllegalArgumentException("Unknown option: " + arg);
}
}
if (!positional.isEmpty()) {
cfg.directory = Path.of(positional.get(0)).toAbsolutePath();
}
if (cfg.port < 1 || cfg.port > 65535) throw new IllegalArgumentException("--port must be between 1 and 65535");
if (cfg.cacheSeconds < 0) throw new IllegalArgumentException("--cache must be >= 0");
if (cfg.ssl && (cfg.certPath == null || cfg.keyPath == null)) {
throw new IllegalArgumentException("--ssl requires --cert and --key");
}
return cfg;
}
private static String nextValue(String[] args, int idx, String flag) {
if (idx >= args.length) throw new IllegalArgumentException("Missing value for " + flag);
return args[idx];
}
private static void createSampleDirectory(Path root) throws IOException {
Files.createDirectories(root.resolve("assets"));
Files.createDirectories(root.resolve("scripts"));
Files.writeString(root.resolve("index.html"), """
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>Sample Static Server</title>
<link rel="stylesheet" href="/assets/style.css" />
</head>
<body>
<h1>Sample Static Server</h1>
<p>Static serving works.</p>
<img src="/sample.svg" width="180" alt="sample image" />
<p><a href="/hello.txt">Open text file</a></p>
<script src="/scripts/app.js"></script>
</body>
</html>
""", StandardCharsets.UTF_8);
Files.writeString(root.resolve("assets/style.css"),
"body { font-family: sans-serif; margin: 2rem; } h1 { color: #1a5d8f; }\n", StandardCharsets.UTF_8);
Files.writeString(root.resolve("scripts/app.js"), "console.log('Sample JS loaded');\n", StandardCharsets.UTF_8);
Files.writeString(root.resolve("hello.txt"), "Hello from sample static directory.\n", StandardCharsets.UTF_8);
Files.writeString(root.resolve("sample.svg"), """
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 120">
<rect width="320" height="120" fill="#e9f3fb"/>
<circle cx="60" cy="60" r="32" fill="#1a5d8f"/>
<text x="110" y="68" font-size="22" fill="#1a5d8f">Static Server</text>
</svg>
""", StandardCharsets.UTF_8);
}
private static int sendText(HttpExchange ex, int status, String text, String corsOrigin) throws IOException {
byte[] body = (status + " " + text + "\n").getBytes(StandardCharsets.UTF_8);
addCors(ex.getResponseHeaders(), corsOrigin);
ex.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
ex.getResponseHeaders().set("Content-Length", String.valueOf(body.length));
ex.sendResponseHeaders(status, body.length);
try (OutputStream os = ex.getResponseBody()) {
os.write(body);
}
return status;
}
private static void addCors(Headers headers, String origin) {
if (origin != null) {
headers.set("Access-Control-Allow-Origin", origin);
headers.set("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS");
headers.set("Access-Control-Allow-Headers", "*");
}
}
private static String detectMime(Path file) throws IOException {
String type = Files.probeContentType(file);
if (type != null) return type;
String name = file.getFileName().toString().toLowerCase(Locale.ROOT);
int idx = name.lastIndexOf('.');
if (idx >= 0) {
String ext = name.substring(idx);
if (MIME.containsKey(ext)) return MIME.get(ext);
}
return "application/octet-stream";
}
private static String etagForFile(Path file) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
try (InputStream in = Files.newInputStream(file)) {
byte[] buf = new byte[8192];
int read;
while ((read = in.read(buf)) >= 0) {
if (read == 0) continue;
md.update(buf, 0, read);
}
}
return "\"" + bytesToHex(md.digest()) + "\"";
}
private static String bytesToHex(byte[] data) {
StringBuilder sb = new StringBuilder(data.length * 2);
for (byte b : data) sb.append(String.format("%02x", b));
return sb.toString();
}
private static Optional<long[]> parseRange(String rangeHeader, long totalSize) {
Matcher m = RANGE_RX.matcher(rangeHeader);
if (!m.matches()) return Optional.empty();
String startS = m.group(1);
String endS = m.group(2);
try {
long start;
long end;
if (startS.isEmpty() && endS.isEmpty()) return Optional.empty();
if (startS.isEmpty()) {
long suffixLen = Long.parseLong(endS);
if (suffixLen <= 0) return Optional.empty();
start = Math.max(0, totalSize - suffixLen);
end = totalSize - 1;
} else {
start = Long.parseLong(startS);
end = endS.isEmpty() ? totalSize - 1 : Long.parseLong(endS);
if (end >= totalSize) end = totalSize - 1;
}
if (start < 0 || end < start || start >= totalSize) return Optional.empty();
return Optional.of(new long[]{start, end});
} catch (NumberFormatException e) {
return Optional.empty();
}
}
private static String renderDirectoryListing(String reqPath, Path dir) throws IOException {
String pathDisplay = reqPath.endsWith("/") ? reqPath : reqPath + "/";
List<Path> entries = new ArrayList<>();
try (DirectoryStream<Path> ds = Files.newDirectoryStream(dir)) {
for (Path p : ds) entries.add(p);
}
entries.sort(Comparator.<Path, Boolean>comparing(Files::isRegularFile).thenComparing(p -> p.getFileName().toString()));
StringBuilder rows = new StringBuilder();
if (!"/".equals(pathDisplay)) {
String parent = Path.of(pathDisplay).getParent() == null ? "/" : Path.of(pathDisplay).getParent().toString().replace('\\', '/');
if (!parent.endsWith("/")) parent += "/";
rows.append("<tr><td>[UP]</td><td><a href=\"").append(escape(parent))
.append("\">..</a></td><td>-</td><td>-</td></tr>");
}
for (Path p : entries) {
boolean isDir = Files.isDirectory(p);
String name = p.getFileName().toString() + (isDir ? "/" : "");
String href = (pathDisplay.endsWith("/") ? pathDisplay : pathDisplay + "/") + p.getFileName() + (isDir ? "/" : "");
String size = isDir ? "-" : humanSize(Files.size(p));
String modified = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.format(ZonedDateTime.ofInstant(Files.getLastModifiedTime(p).toInstant(), ZoneId.systemDefault()));
rows.append("<tr><td>").append(iconFor(p.getFileName().toString(), isDir))
.append("</td><td><a href=\"").append(escape(href)).append("\">").append(escape(name)).append("</a></td>")
.append("<td>").append(size).append("</td><td>").append(modified).append("</td></tr>");
}
return "<!doctype html><html><head><meta charset='utf-8'><title>Index of " + escape(pathDisplay) + "</title>"
+ "<style>body{font-family:sans-serif;margin:1.2rem}table{border-collapse:collapse;width:100%}"
+ "td,th{border-bottom:1px solid #ddd;padding:.5rem;text-align:left}</style></head><body>"
+ "<h1>Index of " + escape(pathDisplay) + "</h1>"
+ "<table><thead><tr><th>Type</th><th>Name</th><th>Size</th><th>Modified</th></tr></thead>"
+ "<tbody>" + rows + "</tbody></table></body></html>";
}
private static String iconFor(String name, boolean isDir) {
if (isDir) return "[DIR]";
String low = name.toLowerCase(Locale.ROOT);
if (low.endsWith(".png") || low.endsWith(".jpg") || low.endsWith(".jpeg") || low.endsWith(".gif") || low.endsWith(".svg")) return "[IMG]";
if (low.endsWith(".mp4") || low.endsWith(".webm") || low.endsWith(".mp3") || low.endsWith(".wav")) return "[MED]";
if (low.endsWith(".html") || low.endsWith(".css") || low.endsWith(".js") || low.endsWith(".json") || low.endsWith(".xml") || low.endsWith(".txt")) return "[TXT]";
return "[FIL]";
}
private static String escape(String input) {
return input.replace("&", "&").replace("<", "<").replace(">", ">")
.replace("\"", """).replace("'", "'");
}
private static String humanSize(long size) {
String[] units = {"B", "KB", "MB", "GB"};
double v = size;
int idx = 0;
while (v >= 1024.0 && idx < units.length - 1) {
v /= 1024.0;
idx++;
}
return idx == 0 ? String.format(Locale.US, "%.0f %s", v, units[idx]) : String.format(Locale.US, "%.1f %s", v, units[idx]);
}
private static SSLContext sslContextFromPem(Path certPath, Path keyPath) throws Exception {
X509Certificate cert;
try (InputStream certIn = Files.newInputStream(certPath)) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
cert = (X509Certificate) cf.generateCertificate(certIn);
}
String pem = Files.readString(keyPath, StandardCharsets.UTF_8)
.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] keyDer = Base64.getDecoder().decode(pem);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyDer);
PrivateKey key = tryKeyAlgorithms(keySpec, "RSA", "EC", "DSA");
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(null, null);
char[] password = "changeit".toCharArray();
ks.setKeyEntry("server", key, password, new Certificate[]{cert});
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ks);
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom());
return ctx;
}
private static PrivateKey tryKeyAlgorithms(PKCS8EncodedKeySpec spec, String... algorithms) throws Exception {
Exception last = null;
for (String alg : algorithms) {
try {
return KeyFactory.getInstance(alg).generatePrivate(spec);
} catch (Exception ex) {
last = ex;
}
}
throw last == null ? new IllegalArgumentException("Unsupported private key") : last;
}
public static void main(String[] args) {
HttpServer server = null;
Logger logger = null;
try {
Config cfg = parseArgs(args);
Path root;
if (cfg.directory == null) {
root = Path.of("sample_static_site").toAbsolutePath();
createSampleDirectory(root);
} else {
root = cfg.directory.toAbsolutePath();
}
if (!Files.isDirectory(root)) {
throw new IllegalArgumentException("Directory not found: " + root);
}
Path rootReal = root.toRealPath();
logger = new Logger(cfg.logPath);
InetSocketAddress address = new InetSocketAddress(cfg.host, cfg.port);
if (cfg.ssl) {
HttpsServer https = HttpsServer.create(address, 0);
SSLContext ssl = sslContextFromPem(cfg.certPath, cfg.keyPath);
https.setHttpsConfigurator(new HttpsConfigurator(ssl));
server = https;
} else {
server = HttpServer.create(address, 0);
}
server.createContext("/", new FileServerHandler(rootReal, cfg, logger));
server.setExecutor(Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()));
final HttpServer serverRef = server;
final Logger loggerRef = logger;
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("\nReceived SIGINT, shutting down gracefully...");
serverRef.stop(1);
loggerRef.close();
}));
String proto = cfg.ssl ? "https" : "http";
System.out.println("Static HTTP File Server started");
System.out.println("URL: " + proto + "://" + cfg.host + ":" + cfg.port);
System.out.println("Serving directory: " + rootReal);
System.out.println("Directory listing: " + (cfg.noListing ? "disabled" : "enabled"));
System.out.println("Caching max-age: " + cfg.cacheSeconds);
System.out.println("CORS: " + (cfg.corsOrigin == null ? "disabled" : "enabled (" + cfg.corsOrigin + ")"));
System.out.println("TLS: " + (cfg.ssl ? "enabled" : "disabled"));
if (cfg.directory == null) {
System.out.println("Sample URLs: " + proto + "://" + cfg.host + ":" + cfg.port + "/ "
+ proto + "://" + cfg.host + ":" + cfg.port + "/assets/style.css "
+ proto + "://" + cfg.host + ":" + cfg.port + "/scripts/app.js "
+ proto + "://" + cfg.host + ":" + cfg.port + "/sample.svg "
+ proto + "://" + cfg.host + ":" + cfg.port + "/hello.txt");
}
server.start();
} catch (java.net.BindException be) {
System.err.println("Error: port is already in use.");
System.exit(1);
} catch (java.net.SocketException se) {
System.err.println("Error: permission denied or invalid host/port.");
System.exit(1);
} catch (Exception ex) {
System.err.println("Error: " + ex.getMessage());
System.exit(1);
}
}
}