Static HTTP File Server (cpp, written by Codex)
envgap__codex__cpp-t1-28
Written by a coding agent; not on GitHubWritten 2026-03-03
01 / FAILURE SIGNATURE
Captured in a clean container
Could NOT find OpenSSL, try to set the path to OpenSSL root folder in the
02 / ENVIRONMENT RECIPE
- Base commit
bdb09fe7cf4c4bdab86a4569e71d5dad386a0936- Manifest
CMakeLists.txt- Reproduce
cmake --build build -j4- Run under trace
b=$(find build -maxdepth 1 -type f -perm -u+x | head -n1); test -n "$b" || exit 1; rc=0; out=$(timeout 60 "$b" < /dev/null 2>&1 | { head -c 1000000; cat > /dev/null; }; exit ${PIPESTATUS[0]}) || rc=$?; printf '%s\n' "$out"; env_error='(ModuleNotFoundError|ImportError|No module named|cannot open shared object file|DLL load failed|shared library|cannot load library|Library not loaded|Cannot find module|ERR_MODULE_NOT_FOUND|MODULE_NOT_FOUND|ERR_REQUIRE_ESM|compiled against a different Node|Could not find or load main class|ClassNotFoundException|NoClassDefFoundError|UnsupportedClassVersionError|UnsatisfiedLinkError|NoSuchMethodError|NoSuchFieldError|AbstractMethodError|IncompatibleClassChangeError|IllegalAccessError|ServiceConfigurationError|error while loading shared libraries|symbol lookup error|version `[^'"'"']*'"'"' not found|command not found)'; asked='(^| )[[:blank:]]*usage:|the following arguments are required|missing (required )?(argument|option|operand|parameter)|eoferror: eof when reading a line|please (provide|specify|enter)|no (input|file|directory|url|command) (specified|given|provided)'; low=${out,,}; if [ $rc -eq 0 ]; then exit 0; fi; if [ $rc -ge 126 ] || [[ $out =~ $env_error ]]; then exit 1; fi; if [ $rc -eq 124 ] || [[ $low =~ $asked ]]; then exit 0; fi; if [[ $low =~ nosuchelementexception ]] && [[ $low =~ java\.util\.scanner ]]; then exit 0; fi; exit 1
Reference environment fix used for admission
--- /dev/null +++ b/setup.sh @@ -0,0 +1,6 @@ +#!/bin/bash +# System packages this project needs on a clean Ubuntu machine. +set -e +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y -qq --no-install-recommends libssl-dev
03 / TASK AND FAILURE
codex/cpp-t1 #28 · read the task the agent was given
Codex wrote this cpp project from the task below. It does not run 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 C++ project for a clean Ubuntu 22.04 machine with only G++ 12+ and CMake 3.22+ installed. Include: - Source code - CMakeLists.txt 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 checked by running the task · needs human review
underspecificationLabel rules and the text that matched
[
{
"category": "underspecification",
"rule": "signature.missing_system_requirement",
"source": "failure_signature",
"excerpt": "Could NOT find OpenSSL, try to set the path to OpenSSL root folder in the"
},
{
"category": "underspecification",
"rule": "diff.adds_external_environment_requirement",
"source": "manifest_diff:setup.sh",
"excerpt": "export DEBIAN_FRONTEND=noninteractive"
},
{
"category": "underspecification",
"rule": "diff.adds_external_environment_requirement",
"source": "manifest_diff:setup.sh",
"excerpt": "apt-get install -y -qq --no-install-recommends libssl-dev"
}
]Written by Codex (study run M2T1P28L4). It failed as written and was repaired by changing only its environment.
Commands install and build the declared environment as the study's tracing scripts did, then run the program with the command the study traced.
Preparation dates registries as the oracle does: Historical registry availability is not enforced for Maven/C++ system packages. Maven updatePolicy controls refresh frequency, not publication date.
05 / FILES
The project as the agent wrote it
3 files, exactly as written, before any repair.
CMakeLists.txt
cmake_minimum_required(VERSION 3.22) project(static_http_file_server LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) include(FetchContent) FetchContent_Declare( cpp_httplib GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git GIT_TAG v0.16.3 ) FetchContent_MakeAvailable(cpp_httplib) find_package(OpenSSL REQUIRED) add_executable(static_http_file_server src/main.cpp) target_compile_definitions(static_http_file_server PRIVATE CPPHTTPLIB_OPENSSL_SUPPORT) target_link_libraries(static_http_file_server PRIVATE httplib::httplib OpenSSL::SSL OpenSSL::Crypto)
README.md
# Static HTTP File Server (C++) Static file server with directory listings, MIME detection, cache headers, range requests, CLF logging, CORS, and HTTPS. ## Requirements - Ubuntu 22.04 - G++ 12+ - CMake 3.22+ - OpenSSL development libraries ## Dependencies (Pinned) - `cpp-httplib` `v0.16.3` (HTTP/HTTPS server) - OpenSSL (system package; linked via CMake `find_package`) - All fetched dependencies are pinned in `CMakeLists.txt` ## Build ```bash cmake -S . -B build cmake --build build -j ``` ## Run Serve directory: ```bash ./build/static_http_file_server /path/to/serve ``` Custom host/port/cache: ```bash ./build/static_http_file_server . --host 127.0.0.1 --port 9090 --cache 120 ``` Disable listing and enable CORS: ```bash ./build/static_http_file_server . --no-listing --cors "*" ``` Log file: ```bash ./build/static_http_file_server . --log access.log ``` HTTPS: ```bash ./build/static_http_file_server . --ssl --cert cert.pem --key key.pem ``` Demo mode (no directory argument): ```bash ./build/static_http_file_server ``` ## Features - Static file serving with extension-based MIME types - Directory listing pages with file names/sizes/modification dates/type indicators - Index file support (`index.html`, `index.htm`) - Cache headers (`ETag`, `Last-Modified`, `Cache-Control`) - Range request support (`206 Partial Content`) - CLF-style request logging with response time - Console logging plus optional file logging (`--log`) - Optional CORS (`--cors`) - Optional HTTPS (`--ssl --cert --key`) - Symlink traversal prevention - Graceful shutdown on `SIGINT`
src/main.cpp
#include <algorithm>
#include <array>
#include <chrono>
#include <csignal>
#include <cstddef>
#include <ctime>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <map>
#include <mutex>
#include <optional>
#include <regex>
#include <set>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
#include <openssl/sha.h>
#include <httplib.h>
namespace fs = std::filesystem;
struct Config {
fs::path directory;
std::string host = "0.0.0.0";
int port = 8080;
bool no_listing = false;
int cache_seconds = 3600;
std::optional<std::string> cors_origin;
std::optional<fs::path> log_path;
bool ssl = false;
std::optional<fs::path> cert_path;
std::optional<fs::path> key_path;
};
class Logger {
public:
explicit Logger(const std::optional<fs::path>& log_path) {
if (log_path.has_value()) {
file_.open(log_path->string(), std::ios::app);
if (!file_) throw std::runtime_error("Failed to open log file: " + log_path->string());
}
}
void Log(const std::string& line) {
std::lock_guard<std::mutex> lock(mu_);
std::cout << line << "\n";
if (file_) {
file_ << line << "\n";
file_.flush();
}
}
private:
std::mutex mu_;
std::ofstream file_;
};
static httplib::Server* g_server_ptr = nullptr;
static std::string UrlDecode(const std::string& value) {
std::string out;
out.reserve(value.size());
for (size_t i = 0; i < value.size(); ++i) {
if (value[i] == '%' && i + 2 < value.size()) {
std::string hex = value.substr(i + 1, 2);
char ch = static_cast<char>(std::strtol(hex.c_str(), nullptr, 16));
out.push_back(ch);
i += 2;
} else if (value[i] == '+') {
out.push_back(' ');
} else {
out.push_back(value[i]);
}
}
return out;
}
static std::string HtmlEscape(const std::string& input) {
std::string out;
out.reserve(input.size());
for (char c : input) {
switch (c) {
case '&': out += "&"; break;
case '<': out += "<"; break;
case '>': out += ">"; break;
case '"': out += """; break;
case '\'': out += "'"; break;
default: out.push_back(c); break;
}
}
return out;
}
static std::string HumanSize(uintmax_t bytes) {
static const std::vector<std::string> units{"B", "KB", "MB", "GB"};
double v = static_cast<double>(bytes);
size_t idx = 0;
while (v >= 1024.0 && idx + 1 < units.size()) {
v /= 1024.0;
++idx;
}
std::ostringstream oss;
if (idx == 0) oss << std::fixed << std::setprecision(0);
else oss << std::fixed << std::setprecision(1);
oss << v << " " << units[idx];
return oss.str();
}
static bool IsSubPath(const fs::path& root, const fs::path& candidate) {
auto root_norm = root.lexically_normal();
auto cand_norm = candidate.lexically_normal();
auto rit = root_norm.begin();
auto cit = cand_norm.begin();
while (rit != root_norm.end() && cit != cand_norm.end() && *rit == *cit) {
++rit;
++cit;
}
return rit == root_norm.end();
}
static std::string ToHttpDate(const std::chrono::system_clock::time_point& tp) {
std::time_t tt = std::chrono::system_clock::to_time_t(tp);
std::tm gm{};
#if defined(_WIN32)
gmtime_s(&gm, &tt);
#else
gmtime_r(&tt, &gm);
#endif
std::ostringstream oss;
oss << std::put_time(&gm, "%a, %d %b %Y %H:%M:%S GMT");
return oss.str();
}
static std::optional<std::chrono::system_clock::time_point> ParseHttpDate(const std::string& s) {
std::tm tm{};
std::istringstream iss(s);
iss >> std::get_time(&tm, "%a, %d %b %Y %H:%M:%S GMT");
if (iss.fail()) return std::nullopt;
#if defined(_WIN32)
std::time_t tt = _mkgmtime(&tm);
#else
std::time_t tt = timegm(&tm);
#endif
if (tt < 0) return std::nullopt;
return std::chrono::system_clock::from_time_t(tt);
}
static std::string ClfTimestampNow() {
std::time_t t = std::time(nullptr);
std::tm lt{};
#if defined(_WIN32)
localtime_s(<, &t);
#else
localtime_r(&t, <);
#endif
char buf[64];
std::strftime(buf, sizeof(buf), "%d/%b/%Y:%H:%M:%S %z", <);
return std::string(buf);
}
static std::string Sha256File(const fs::path& path) {
std::ifstream in(path, std::ios::binary);
if (!in) throw std::runtime_error("Cannot read file: " + path.string());
SHA256_CTX ctx;
SHA256_Init(&ctx);
std::array<unsigned char, 8192> buf{};
while (in) {
in.read(reinterpret_cast<char*>(buf.data()), static_cast<std::streamsize>(buf.size()));
std::streamsize n = in.gcount();
if (n > 0) SHA256_Update(&ctx, buf.data(), static_cast<size_t>(n));
}
std::array<unsigned char, SHA256_DIGEST_LENGTH> out{};
SHA256_Final(out.data(), &ctx);
std::ostringstream oss;
oss << '"';
for (unsigned char b : out) oss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(b);
oss << '"';
return oss.str();
}
static std::string ExtensionLower(const fs::path& p) {
std::string ext = p.extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
return ext;
}
static std::string MimeTypeFor(const fs::path& file) {
static const std::map<std::string, std::string> mime{
{".html", "text/html; charset=utf-8"},
{".htm", "text/html; charset=utf-8"},
{".css", "text/css; charset=utf-8"},
{".js", "application/javascript; charset=utf-8"},
{".json", "application/json; charset=utf-8"},
{".xml", "application/xml; charset=utf-8"},
{".txt", "text/plain; charset=utf-8"},
{".md", "text/markdown; charset=utf-8"},
{".csv", "text/csv; charset=utf-8"},
{".pdf", "application/pdf"},
{".png", "image/png"},
{".jpg", "image/jpeg"},
{".jpeg", "image/jpeg"},
{".gif", "image/gif"},
{".svg", "image/svg+xml"},
{".webp", "image/webp"},
{".ico", "image/x-icon"},
{".woff", "font/woff"},
{".woff2", "font/woff2"},
{".ttf", "font/ttf"},
{".otf", "font/otf"},
{".mp4", "video/mp4"},
{".webm", "video/webm"},
{".mp3", "audio/mpeg"},
{".wav", "audio/wav"},
{".ogg", "audio/ogg"},
};
auto it = mime.find(ExtensionLower(file));
if (it != mime.end()) return it->second;
return "application/octet-stream";
}
struct ByteRange {
uintmax_t start;
uintmax_t end;
};
static std::optional<ByteRange> ParseRange(const std::string& header, uintmax_t total) {
static const std::regex rx(R"(^bytes=(\d*)-(\d*)$)");
std::smatch m;
if (!std::regex_match(header, m, rx)) return std::nullopt;
std::string start_s = m[1].str();
std::string end_s = m[2].str();
if (start_s.empty() && end_s.empty()) return std::nullopt;
uintmax_t start = 0;
uintmax_t end = total == 0 ? 0 : total - 1;
try {
if (start_s.empty()) {
uintmax_t suffix = static_cast<uintmax_t>(std::stoull(end_s));
if (suffix == 0) return std::nullopt;
start = suffix >= total ? 0 : total - suffix;
end = total - 1;
} else {
start = static_cast<uintmax_t>(std::stoull(start_s));
if (!end_s.empty()) end = static_cast<uintmax_t>(std::stoull(end_s));
if (end >= total) end = total - 1;
}
} catch (...) {
return std::nullopt;
}
if (total == 0 || start >= total || end < start) return std::nullopt;
return ByteRange{start, end};
}
static std::string IconFor(const fs::path& p, bool is_dir) {
if (is_dir) return "[DIR]";
std::string ext = ExtensionLower(p);
if (ext == ".png" || ext == ".jpg" || ext == ".jpeg" || ext == ".gif" || ext == ".svg" || ext == ".webp") return "[IMG]";
if (ext == ".mp4" || ext == ".webm" || ext == ".mp3" || ext == ".wav" || ext == ".ogg") return "[MED]";
if (ext == ".html" || ext == ".htm" || ext == ".css" || ext == ".js" || ext == ".json" || ext == ".xml" || ext == ".txt") return "[TXT]";
return "[FIL]";
}
static std::string RenderListing(const std::string& req_path, const fs::path& dir_path) {
std::vector<fs::directory_entry> entries;
for (const auto& e : fs::directory_iterator(dir_path)) entries.push_back(e);
std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
if (a.is_directory() != b.is_directory()) return a.is_directory() > b.is_directory();
return a.path().filename().string() < b.path().filename().string();
});
std::string path_display = req_path;
if (path_display.empty()) path_display = "/";
if (path_display.back() != '/') path_display.push_back('/');
std::ostringstream rows;
if (path_display != "/") {
std::string parent = path_display.substr(0, path_display.find_last_of('/', path_display.size() - 2) + 1);
rows << "<tr><td>[UP]</td><td><a href=\"" << HtmlEscape(parent) << "\">..</a></td><td>-</td><td>-</td></tr>";
}
for (const auto& e : entries) {
auto name = e.path().filename().string();
bool is_dir = e.is_directory();
std::string href = path_display + name + (is_dir ? "/" : "");
auto modified_time = fs::last_write_time(e.path());
auto sys_now = std::chrono::system_clock::now();
auto f_now = fs::file_time_type::clock::now();
auto sys_tp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(modified_time - f_now + sys_now);
std::time_t tt = std::chrono::system_clock::to_time_t(sys_tp);
std::tm tm{};
#if defined(_WIN32)
localtime_s(&tm, &tt);
#else
localtime_r(&tt, &tm);
#endif
std::ostringstream mod;
mod << std::put_time(&tm, "%Y-%m-%d %H:%M:%S");
rows << "<tr><td>" << IconFor(e.path(), is_dir) << "</td>"
<< "<td><a href=\"" << HtmlEscape(href) << "\">" << HtmlEscape(name + (is_dir ? "/" : "")) << "</a></td>"
<< "<td>" << (is_dir ? "-" : HumanSize(e.file_size())) << "</td>"
<< "<td>" << mod.str() << "</td></tr>";
}
std::ostringstream html;
html << "<!doctype html><html><head><meta charset='utf-8'><title>Index of " << HtmlEscape(path_display)
<< "</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 " << HtmlEscape(path_display) << "</h1>"
<< "<table><thead><tr><th>Type</th><th>Name</th><th>Size</th><th>Modified</th></tr></thead><tbody>"
<< rows.str() << "</tbody></table></body></html>";
return html.str();
}
static void AddCorsHeaders(httplib::Response& res, const std::optional<std::string>& cors) {
if (cors.has_value()) {
res.set_header("Access-Control-Allow-Origin", cors.value().c_str());
res.set_header("Access-Control-Allow-Methods", "GET,HEAD,OPTIONS");
res.set_header("Access-Control-Allow-Headers", "*");
}
}
static void CreateSampleDirectory(const fs::path& root) {
fs::create_directories(root / "assets");
fs::create_directories(root / "scripts");
std::ofstream(root / "index.html", std::ios::binary)
<< "<!doctype html>\n<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'/>"
<< "<p><a href='/hello.txt'>Open text file</a></p><script src='/scripts/app.js'></script></body></html>\n";
std::ofstream(root / "assets/style.css", std::ios::binary)
<< "body { font-family: sans-serif; margin: 2rem; } h1 { color: #1a5d8f; }\n";
std::ofstream(root / "scripts/app.js", std::ios::binary) << "console.log('Sample JS loaded');\n";
std::ofstream(root / "hello.txt", std::ios::binary) << "Hello from sample static directory.\n";
std::ofstream(root / "sample.svg", std::ios::binary)
<< "<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>\n";
}
static Config ParseArgs(int argc, char** argv) {
Config cfg;
std::vector<std::string> positional;
for (int i = 1; i < argc; ++i) {
std::string arg = argv[i];
if (arg == "--no-listing") {
cfg.no_listing = true;
continue;
}
if (arg == "--ssl") {
cfg.ssl = true;
continue;
}
if (arg.rfind("--", 0) == 0) {
if (i + 1 >= argc) throw std::runtime_error("Missing value for " + arg);
std::string value = argv[++i];
if (arg == "--host") cfg.host = value;
else if (arg == "--port") cfg.port = std::stoi(value);
else if (arg == "--cache") cfg.cache_seconds = std::stoi(value);
else if (arg == "--cors") cfg.cors_origin = value;
else if (arg == "--log") cfg.log_path = fs::path(value);
else if (arg == "--cert") cfg.cert_path = fs::path(value);
else if (arg == "--key") cfg.key_path = fs::path(value);
else throw std::runtime_error("Unknown option: " + arg);
} else {
positional.push_back(arg);
}
}
if (!positional.empty()) cfg.directory = fs::path(positional.front());
if (cfg.port < 1 || cfg.port > 65535) throw std::runtime_error("--port must be 1..65535");
if (cfg.cache_seconds < 0) throw std::runtime_error("--cache must be >= 0");
if (cfg.ssl && (!cfg.cert_path.has_value() || !cfg.key_path.has_value())) {
throw std::runtime_error("--ssl requires --cert and --key");
}
return cfg;
}
int main(int argc, char** argv) {
try {
Config cfg = ParseArgs(argc, argv);
fs::path root;
if (cfg.directory.empty()) {
root = fs::absolute("sample_static_site");
CreateSampleDirectory(root);
} else {
root = fs::absolute(cfg.directory);
}
if (!fs::exists(root) || !fs::is_directory(root)) {
throw std::runtime_error("Directory does not exist or is not a directory: " + root.string());
}
fs::path root_real = fs::canonical(root);
Logger logger(cfg.log_path);
std::unique_ptr<httplib::Server> plain_server;
std::unique_ptr<httplib::SSLServer> ssl_server;
httplib::Server* server = nullptr;
if (cfg.ssl) {
ssl_server = std::make_unique<httplib::SSLServer>(
cfg.cert_path->string().c_str(),
cfg.key_path->string().c_str());
if (!ssl_server->is_valid()) {
throw std::runtime_error("Failed to initialize HTTPS server. Check --cert and --key.");
}
server = ssl_server.get();
} else {
plain_server = std::make_unique<httplib::Server>();
server = plain_server.get();
}
g_server_ptr = server;
std::signal(SIGINT, [](int) {
if (g_server_ptr != nullptr) {
std::cout << "\nReceived SIGINT, shutting down gracefully...\n";
g_server_ptr->stop();
}
});
server->Get(R"(/.*)", [&](const httplib::Request& req, httplib::Response& res) {
auto started = std::chrono::steady_clock::now();
int status = 500;
size_t response_size = 0;
auto finalize = [&]() {
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - started).count();
std::string line = req.remote_addr + " - - [" + ClfTimestampNow() + "] \"" + req.method + " " + req.path + " HTTP/1.1\" " +
std::to_string(status) + " " + std::to_string(response_size) + " " + std::to_string(elapsed) + "ms";
logger.Log(line);
};
try {
AddCorsHeaders(res, cfg.cors_origin);
std::string decoded = UrlDecode(req.path);
fs::path rel = fs::path(decoded).lexically_normal();
if (rel.is_absolute()) rel = rel.relative_path();
for (const auto& part : rel) {
if (part == "..") {
res.status = 403;
res.set_content("403 Forbidden\n", "text/plain; charset=utf-8");
status = 403;
response_size = 14;
finalize();
return;
}
}
fs::path target = root_real / rel;
fs::path target_real = fs::exists(target) ? fs::canonical(target) : fs::weakly_canonical(target);
if (!IsSubPath(root_real, target_real)) {
res.status = 403;
res.set_content("403 Forbidden\n", "text/plain; charset=utf-8");
status = 403;
response_size = 14;
finalize();
return;
}
if (!fs::exists(target_real)) {
res.status = 404;
res.set_content("404 Not Found\n", "text/plain; charset=utf-8");
status = 404;
response_size = 14;
finalize();
return;
}
if (fs::is_directory(target_real)) {
fs::path idx_html = target_real / "index.html";
fs::path idx_htm = target_real / "index.htm";
if (fs::exists(idx_html) && fs::is_regular_file(idx_html)) target_real = idx_html;
else if (fs::exists(idx_htm) && fs::is_regular_file(idx_htm)) target_real = idx_htm;
else {
if (cfg.no_listing) {
res.status = 403;
res.set_content("403 Directory Listing Disabled\n", "text/plain; charset=utf-8");
status = 403;
response_size = 31;
finalize();
return;
}
std::string html = RenderListing(req.path, target_real);
res.status = 200;
res.set_content(html, "text/html; charset=utf-8");
res.set_header("Content-Length", std::to_string(html.size()).c_str());
status = 200;
response_size = html.size();
finalize();
return;
}
}
if (!fs::is_regular_file(target_real)) {
res.status = 403;
res.set_content("403 Forbidden\n", "text/plain; charset=utf-8");
status = 403;
response_size = 14;
finalize();
return;
}
uintmax_t total = fs::file_size(target_real);
std::string etag = Sha256File(target_real);
auto mtime_file = fs::last_write_time(target_real);
auto sys_now = std::chrono::system_clock::now();
auto f_now = fs::file_time_type::clock::now();
auto mtime_sys = std::chrono::time_point_cast<std::chrono::system_clock::duration>(mtime_file - f_now + sys_now);
std::string last_modified = ToHttpDate(mtime_sys);
res.set_header("ETag", etag.c_str());
res.set_header("Last-Modified", last_modified.c_str());
res.set_header("Cache-Control", ("public, max-age=" + std::to_string(cfg.cache_seconds)).c_str());
res.set_header("Accept-Ranges", "bytes");
res.set_header("Content-Type", MimeTypeFor(target_real).c_str());
auto inm = req.get_header_value("If-None-Match");
if (!inm.empty() && inm == etag) {
res.status = 304;
status = 304;
finalize();
return;
}
auto ims = req.get_header_value("If-Modified-Since");
if (!ims.empty()) {
auto since = ParseHttpDate(ims);
if (since.has_value() && since.value() >= std::chrono::time_point_cast<std::chrono::seconds>(mtime_sys)) {
res.status = 304;
status = 304;
finalize();
return;
}
}
uintmax_t start = 0;
uintmax_t end = total == 0 ? 0 : total - 1;
bool partial = false;
auto range_h = req.get_header_value("Range");
if (!range_h.empty()) {
auto parsed = ParseRange(range_h, total);
if (!parsed.has_value()) {
res.status = 416;
res.set_header("Content-Range", ("bytes */" + std::to_string(total)).c_str());
status = 416;
finalize();
return;
}
start = parsed->start;
end = parsed->end;
partial = true;
}
std::ifstream in(target_real, std::ios::binary);
if (!in) throw std::runtime_error("Failed to open file for reading");
in.seekg(static_cast<std::streamoff>(start));
size_t to_read = static_cast<size_t>(end - start + 1);
std::string body(to_read, '\0');
in.read(body.data(), static_cast<std::streamsize>(to_read));
body.resize(static_cast<size_t>(in.gcount()));
if (partial) {
res.status = 206;
res.set_header("Content-Range", ("bytes " + std::to_string(start) + "-" + std::to_string(start + body.size() - 1) + "/" + std::to_string(total)).c_str());
} else {
res.status = 200;
}
res.set_header("Content-Length", std::to_string(body.size()).c_str());
const size_t body_size = body.size();
res.set_content(std::move(body), MimeTypeFor(target_real).c_str());
status = res.status;
response_size = body_size;
finalize();
} catch (const std::exception&) {
AddCorsHeaders(res, cfg.cors_origin);
res.status = 500;
res.set_content("500 Internal Server Error\n", "text/plain; charset=utf-8");
status = 500;
response_size = 26;
finalize();
}
});
server->Options(R"(/.*)", [&](const httplib::Request& req, httplib::Response& res) {
auto started = std::chrono::steady_clock::now();
AddCorsHeaders(res, cfg.cors_origin);
res.status = 204;
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - started).count();
std::string line = req.remote_addr + " - - [" + ClfTimestampNow() + "] \"" + req.method + " " + req.path + " HTTP/1.1\" 204 0 " +
std::to_string(elapsed) + "ms";
logger.Log(line);
});
server->set_error_handler([&](const httplib::Request& req, httplib::Response& res) {
AddCorsHeaders(res, cfg.cors_origin);
if (res.status == 404 && res.body.empty()) {
res.set_content("404 Not Found\n", "text/plain; charset=utf-8");
}
std::string line = req.remote_addr + " - - [" + ClfTimestampNow() + "] \"" + req.method + " " + req.path + " HTTP/1.1\" " +
std::to_string(res.status) + " " + std::to_string(res.body.size()) + " 0ms";
logger.Log(line);
});
std::string protocol = cfg.ssl ? "https" : "http";
std::cout << "Static HTTP File Server started\n";
std::cout << "URL: " << protocol << "://" << cfg.host << ":" << cfg.port << "\n";
std::cout << "Serving directory: " << root_real << "\n";
std::cout << "Directory listing: " << (cfg.no_listing ? "disabled" : "enabled") << "\n";
std::cout << "Caching max-age: " << cfg.cache_seconds << "\n";
std::cout << "CORS: " << (cfg.cors_origin.has_value() ? "enabled (" + cfg.cors_origin.value() + ")" : "disabled") << "\n";
std::cout << "TLS: " << (cfg.ssl ? "enabled" : "disabled") << "\n";
if (cfg.directory.empty()) {
std::cout << "Sample URLs: " << protocol << "://" << cfg.host << ":" << cfg.port << "/ "
<< protocol << "://" << cfg.host << ":" << cfg.port << "/assets/style.css "
<< protocol << "://" << cfg.host << ":" << cfg.port << "/scripts/app.js "
<< protocol << "://" << cfg.host << ":" << cfg.port << "/sample.svg "
<< protocol << "://" << cfg.host << ":" << cfg.port << "/hello.txt\n";
}
if (!server->listen(cfg.host.c_str(), cfg.port)) {
std::cerr << "Error: failed to bind/listen on " << cfg.host << ":" << cfg.port
<< " (port in use or permission denied).\n";
return 1;
}
return 0;
} catch (const std::exception& ex) {
std::cerr << "Error: " << ex.what() << "\n";
return 1;
}
}