meson build, static file handler

This commit is contained in:
Sky Johnson 2025-06-13 09:25:42 -05:00
parent 9fd28d5703
commit bb49ac9275
6 changed files with 281 additions and 12 deletions

33
.gitignore vendored
View File

@ -1 +1,32 @@
build/server # Build directories
build/
# Executables
*.out
*.exe
server
# Object files
*.o
*.so
*.a
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# OS files
.DS_Store
Thumbs.db
# Test assets
assets/

View File

@ -1,7 +1,56 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <string_view>
#include <string>
using std::string_view;
enum class HttpMethod : uint8_t { enum class HttpMethod : uint8_t {
GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, UNKNOWN GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, UNKNOWN
}; };
class MimeTypes {
public:
static string_view get_mime_type(string_view extension) {
// Remove leading dot if present
if (!extension.empty() && extension[0] == '.') {
extension = extension.substr(1);
}
switch (extension.size()) {
case 2:
if (extension == "js") return "application/javascript";
break;
case 3:
if (extension == "css") return "text/css";
if (extension == "htm") return "text/html";
if (extension == "png") return "image/png";
if (extension == "jpg") return "image/jpeg";
if (extension == "gif") return "image/gif";
if (extension == "svg") return "image/svg+xml";
if (extension == "ico") return "image/x-icon";
if (extension == "ttf") return "font/ttf";
if (extension == "txt") return "text/plain";
if (extension == "pdf") return "application/pdf";
break;
case 4:
if (extension == "html") return "text/html";
if (extension == "json") return "application/json";
if (extension == "jpeg") return "image/jpeg";
if (extension == "woff") return "font/woff";
break;
case 5:
if (extension == "woff2") return "font/woff2";
break;
}
return "application/octet-stream";
}
static bool should_compress(string_view mime_type) {
return mime_type.starts_with("text/") ||
mime_type.starts_with("application/json") ||
mime_type.starts_with("application/javascript") ||
mime_type.starts_with("image/svg+xml");
}
};

View File

@ -67,19 +67,21 @@ int main() {
server = new HttpServer(8080, router); server = new HttpServer(8080, router);
std::cout << "Current working directory: " << std::filesystem::current_path() << std::endl;
server->serve_static("../assets");
if (std::filesystem::exists("../assets/index.html")) {
std::cout << "Found index.html\n";
} else {
std::cout << "Missing ../assets/index.html\n";
}
if (!server->start()) { if (!server->start()) {
std::cerr << "Failed to start server\n"; std::cerr << "Failed to start server\n";
return 1; return 1;
} }
std::cout << "Server running on http://localhost:8080\n"; std::cout << "Server running on http://localhost:8080\n";
std::cout << "Test routes:\n";
std::cout << " GET /\n";
std::cout << " GET /api/status\n";
std::cout << " GET /users\n";
std::cout << " GET /users/123\n";
std::cout << " POST /users\n";
std::cout << " GET /request-info\n";
std::cout << "Press Ctrl+C to stop\n"; std::cout << "Press Ctrl+C to stop\n";
server->run(); server->run();

14
meson.build Normal file
View File

@ -0,0 +1,14 @@
project('server', 'cpp', default_options: ['cpp_std=c++20'])
add_project_arguments('-Wno-unused-parameter', language: 'cpp')
zlib = dependency('zlib')
threads = dependency('threads')
server = executable('server',
'main.cpp',
dependencies: [zlib, threads],
cpp_args: ['-O3', '-march=native', '-flto', '-Wall', '-Wextra', '-DNDEBUG'],
link_args: ['-flto', '-s']
)
run_target('run', command: server)

View File

@ -4,6 +4,7 @@
#include "router.hpp" #include "router.hpp"
#include "http_parser.hpp" #include "http_parser.hpp"
#include "http_response.hpp" #include "http_response.hpp"
#include "static_file_handler.hpp"
#include <iostream> #include <iostream>
#include <string.h> #include <string.h>
#include <string_view> #include <string_view>
@ -33,7 +34,7 @@ public:
workers_.reserve(num_cores); workers_.reserve(num_cores);
for (unsigned int i = 0; i < num_cores; ++i) { for (unsigned int i = 0; i < num_cores; ++i) {
auto worker = std::make_unique<Worker>(port_, router_); auto worker = std::make_unique<Worker>(port_, router_, static_handler_);
if (!worker->socket.start()) return false; if (!worker->socket.start()) return false;
workers_.push_back(std::move(worker)); workers_.push_back(std::move(worker));
} }
@ -82,16 +83,23 @@ public:
return ""; return "";
} }
void serve_static(const std::string& static_dir) {
static_handler_ = std::make_shared<StaticFileHandler>(static_dir);
}
private: private:
static constexpr int BUFFER_SIZE = 65536; static constexpr int BUFFER_SIZE = 65536;
std::shared_ptr<StaticFileHandler> static_handler_;
struct Worker { struct Worker {
EpollSocket socket; EpollSocket socket;
Router& router; Router& router;
std::shared_ptr<StaticFileHandler>& static_handler;
std::array<char, BUFFER_SIZE> buffer; std::array<char, BUFFER_SIZE> buffer;
std::thread thread; std::thread thread;
Worker(uint16_t port, Router& r) : socket(port), router(r) { Worker(uint16_t port, Router& r, std::shared_ptr<StaticFileHandler>& sh)
: socket(port), router(r), static_handler(sh) {
socket.on_connection([this](int fd) { handle_connection(fd); }); socket.on_connection([this](int fd) { handle_connection(fd); });
socket.on_data([this](int fd) { handle_data(fd); }); socket.on_data([this](int fd) { handle_data(fd); });
socket.on_disconnect([this](int fd) { handle_disconnect(fd); }); socket.on_disconnect([this](int fd) { handle_disconnect(fd); });
@ -133,6 +141,17 @@ private:
HttpResponse response; HttpResponse response;
// Try static files first
if (static_handler) {
static_handler->handle(req, response);
if (response.status == 200) {
send_http_response(client_fd, response, req.version);
return;
}
}
// Then try router
response = HttpResponse{}; // Reset response
if (router.handle(req, response)) { if (router.handle(req, response)) {
send_http_response(client_fd, response, req.version); send_http_response(client_fd, response, req.version);
} else { } else {

154
static_file_handler.hpp Normal file
View File

@ -0,0 +1,154 @@
#pragma once
#include "http_parser.hpp"
#include "http_response.hpp"
#include "http_common.hpp"
#include <string>
#include <string_view>
#include <unordered_map>
#include <fstream>
#include <filesystem>
#include <chrono>
#include <zlib.h>
#include <cstring>
using std::string_view;
using std::string;
namespace fs = std::filesystem;
class StaticFileHandler {
private:
string root_path_;
std::unordered_map<string, std::pair<string, std::chrono::time_point<std::chrono::file_clock>>> cache_;
size_t max_cache_size_ = 50 * 1024 * 1024; // 50MB cache limit
string compress_gzip(const string& data) const {
z_stream zs;
memset(&zs, 0, sizeof(zs));
if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16, 8, Z_DEFAULT_STRATEGY) != Z_OK) {
return "";
}
zs.next_in = reinterpret_cast<Bytef*>(const_cast<char*>(data.data()));
zs.avail_in = data.size();
string compressed;
compressed.reserve(data.size() / 2);
char buffer[32768];
do {
zs.next_out = reinterpret_cast<Bytef*>(buffer);
zs.avail_out = sizeof(buffer);
int ret = deflate(&zs, Z_FINISH);
if (ret == Z_STREAM_ERROR) break;
size_t have = sizeof(buffer) - zs.avail_out;
compressed.append(buffer, have);
} while (zs.avail_out == 0);
deflateEnd(&zs);
return compressed;
}
string read_file(const string& path) const {
std::ifstream file(path, std::ios::binary);
if (!file.is_open()) return "";
file.seekg(0, std::ios::end);
size_t size = file.tellg();
file.seekg(0, std::ios::beg);
string content(size, '\0');
file.read(content.data(), size);
return content;
}
bool is_path_safe(string_view path) const {
return path.find("..") == string::npos;
}
public:
explicit StaticFileHandler(string_view root_path) : root_path_(root_path) {
if (!root_path_.empty() && root_path_.back() != '/') {
root_path_ += '/';
}
}
void handle(const HttpRequest& req, HttpResponse& res) {
if (req.method != HttpMethod::GET && req.method != HttpMethod::HEAD) {
res.status = 405;
res.set_text("Method Not Allowed");
return;
}
if (!is_path_safe(req.path)) {
res.status = 403;
res.set_text("Forbidden");
return;
}
string file_path = root_path_ + string(req.path.substr(1)); // Remove leading /
// Default to index.html for directories
if (file_path.back() == '/') {
file_path += "index.html";
}
if (!fs::exists(file_path) || !fs::is_regular_file(file_path)) {
res.status = 404;
res.set_text("Not Found");
return;
}
// Get file info
auto last_write = fs::last_write_time(file_path);
auto file_size = fs::file_size(file_path);
// Check cache
auto cache_it = cache_.find(file_path);
if (cache_it != cache_.end() && cache_it->second.second == last_write) {
res.body = cache_it->second.first;
} else {
// Read file
res.body = read_file(file_path);
if (res.body.empty()) {
res.status = 500;
res.set_text("Internal Server Error");
return;
}
// Cache if small enough
if (file_size < max_cache_size_ / 10) {
cache_[file_path] = {res.body, last_write};
}
}
// Set MIME type
string ext = fs::path(file_path).extension();
res.content_type = MimeTypes::get_mime_type(ext);
// Check if client accepts gzip
auto accept_encoding = req.headers.find("Accept-Encoding");
bool client_accepts_gzip = accept_encoding != req.headers.end() &&
accept_encoding->second.find("gzip") != string::npos;
// Compress if appropriate
if (client_accepts_gzip && MimeTypes::should_compress(res.content_type) && res.body.size() > 1024) {
string compressed = compress_gzip(res.body);
if (!compressed.empty() && compressed.size() < res.body.size()) {
res.body = std::move(compressed);
res.headers["Content-Encoding"] = "gzip";
}
}
// Add caching headers
res.headers["Cache-Control"] = "public, max-age=3600";
res.headers["ETag"] = "\"" + std::to_string(std::hash<string>{}(res.body)) + "\"";
if (req.method == HttpMethod::HEAD) {
res.body.clear();
}
}
};