← All tasks
javacodex/java-t1 #28Not a task: already works

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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
                .replace("\"", "&quot;").replace("'", "&#39;");
    }

    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);
        }
    }
}