fix static file serving, add cookie support

This commit is contained in:
Sky Johnson 2025-06-13 11:11:13 -05:00
parent 538ae9f8b4
commit 78b2d8f8fb
7 changed files with 223 additions and 39 deletions

136
cookie.hpp Normal file
View File

@ -0,0 +1,136 @@
#pragma once
#include <string_view>
#include <vector>
#include <string>
using std::string_view;
struct Cookie {
string_view name;
string_view value;
Cookie(string_view n, string_view v) : name(n), value(v) {}
};
class CookieParser {
public:
static std::vector<Cookie> parse(string_view cookie_header) {
std::vector<Cookie> cookies;
const char* ptr = cookie_header.data();
const char* end = ptr + cookie_header.size();
while (ptr < end) {
// Skip whitespace and semicolons
while (ptr < end && (*ptr == ' ' || *ptr == ';')) ptr++;
if (ptr >= end) break;
// Find name end (=)
const char* name_start = ptr;
while (ptr < end && *ptr != '=' && *ptr != ';') ptr++;
if (ptr >= end || *ptr != '=') break;
string_view name(name_start, ptr - name_start);
ptr++; // Skip '='
// Find value end (; or end)
const char* value_start = ptr;
while (ptr < end && *ptr != ';') ptr++;
string_view value(value_start, ptr - value_start);
// Trim whitespace from name and value
name = trim(name);
value = trim(value);
if (!name.empty()) {
cookies.emplace_back(name, value);
}
}
return cookies;
}
private:
static string_view trim(string_view str) {
const char* start = str.data();
const char* end = start + str.size();
// Trim leading whitespace
while (start < end && *start == ' ') start++;
// Trim trailing whitespace
while (end > start && *(end - 1) == ' ') end--;
return string_view(start, end - start);
}
};
// Cookie helpers for request/response handling
class CookieHelpers {
public:
// Get cookie value from request, returns empty string_view if not found
static string_view get_cookie(const std::vector<Cookie>& cookies, string_view name) {
for (const auto& cookie : cookies) {
if (cookie.name == name) {
return cookie.value;
}
}
return {};
}
// Build Set-Cookie header value
static std::string build_set_cookie(string_view name, string_view value,
int max_age = -1, string_view path = "", string_view domain = "",
bool secure = false, bool http_only = false) {
std::string result;
result.reserve(256);
result += name;
result += "=";
result += value;
if (max_age >= 0) {
result += "; Max-Age=";
result += std::to_string(max_age);
}
if (!path.empty()) {
result += "; Path=";
result += path;
}
if (!domain.empty()) {
result += "; Domain=";
result += domain;
}
if (secure) {
result += "; Secure";
}
if (http_only) {
result += "; HttpOnly";
}
return result;
}
// Build delete cookie header (expires immediately)
static std::string build_delete_cookie(string_view name, string_view path = "") {
std::string result;
result.reserve(128);
result += name;
result += "=; Max-Age=0";
if (!path.empty()) {
result += "; Path=";
result += path;
}
return result;
}
};

View File

@ -2,9 +2,11 @@
#include "http_common.hpp"
#include "router.hpp"
#include "cookie.hpp"
#include <string_view>
#include <unordered_map>
#include <string>
#include <vector>
using std::string_view;
@ -16,9 +18,15 @@ struct HttpRequest {
string_view body;
std::unordered_map<string_view, string_view> headers;
std::unordered_map<std::string, std::string> params; // URL parameters
std::vector<Cookie> cookies; // Parsed cookies
size_t content_length = 0;
bool valid = false;
// Cookie helper method
string_view get_cookie(string_view name) const {
return CookieHelpers::get_cookie(cookies, name);
}
};
class HttpParser {
@ -83,6 +91,10 @@ public:
if (name.size() == 14 && strncasecmp(name.data(), "content-length", 14) == 0) {
req.content_length = parse_int(value);
}
// Check for Cookie header
else if (name.size() == 6 && strncasecmp(name.data(), "cookie", 6) == 0) {
req.cookies = CookieParser::parse(value);
}
ptr = header_end + 2;
}
@ -144,6 +156,7 @@ private:
char c1 = s1[i] >= 'A' && s1[i] <= 'Z' ? s1[i] + 32 : s1[i];
char c2 = s2[i] >= 'A' && s2[i] <= 'Z' ? s2[i] + 32 : s2[i];
if (c1 != c2) return c1 - c2;
if (c1 == 0) break;
}
return 0;
}

20
http_request.hpp Normal file
View File

@ -0,0 +1,20 @@
#pragma once
#include "http_common.hpp"
#include "cookie.hpp"
#include <unordered_map>
#include <vector>
struct HttpRequest {
HttpMethod method = HttpMethod::UNKNOWN;
string_view path;
string_view query;
string_view version;
string_view body;
std::unordered_map<string_view, string_view> headers;
std::unordered_map<std::string, std::string> params; // URL parameters
std::vector<Cookie> cookies; // Parsed cookies
size_t content_length = 0;
bool valid = false;
};

View File

@ -1,5 +1,6 @@
#pragma once
#include "cookie.hpp"
#include <string>
#include <string_view>
#include <unordered_map>
@ -27,6 +28,18 @@ struct HttpResponse {
body = html;
content_type = "text/html";
}
// Cookie helper methods
void set_cookie(string_view name, string_view value, int max_age = -1,
string_view path = "", string_view domain = "", bool secure = false, bool http_only = false) {
std::string cookie_header = CookieHelpers::build_set_cookie(name, value, max_age, path, domain, secure, http_only);
headers["Set-Cookie"] = cookie_header;
}
void delete_cookie(string_view name, string_view path = "") {
std::string cookie_header = CookieHelpers::build_delete_cookie(name, path);
headers["Set-Cookie"] = cookie_header;
}
};
class HttpResponseBuilder {
@ -34,7 +47,7 @@ private:
static constexpr size_t BUFFER_SIZE = 4096;
static constexpr const char* STATUS_LINES[] = {
"HTTP/1.1 200 OK\r\n",
"HTTP/1.1 201 Created\r\n",
"HTTP/1.1 201 Created\r\n",
"HTTP/1.1 400 Bad Request\r\n",
"HTTP/1.1 404 Not Found\r\n",
"HTTP/1.1 500 Internal Server Error\r\n"
@ -116,4 +129,4 @@ public:
return result;
}
};
};

View File

@ -20,6 +20,7 @@ int main() {
// Root route
router.get("/", [](const HttpRequest& req, HttpResponse& res) {
res.set_text("Hello, World! HTTP Server with Router\n");
res.set_cookie("test_cookie", "hey there!");
});
// API routes
@ -67,14 +68,7 @@ int main() {
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()) {
std::cerr << "Failed to start server\n";

View File

@ -83,8 +83,8 @@ public:
return "";
}
void serve_static(const std::string& static_dir) {
static_handler_ = std::make_shared<StaticFileHandler>(static_dir);
void serve_static(const std::string& static_dir, const std::string& url_prefix = "") {
static_handler_ = std::make_shared<StaticFileHandler>(static_dir, url_prefix);
}
private:
@ -141,18 +141,14 @@ private:
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;
}
// Try router first
if (router.handle(req, response)) {
send_http_response(client_fd, response, req.version);
return;
}
// Then try router
response = HttpResponse{}; // Reset response
if (router.handle(req, response)) {
// Then try static files
if (static_handler && static_handler->handle(req, response)) {
send_http_response(client_fd, response, req.version);
} else {
response.status = 404;

View File

@ -18,6 +18,7 @@ namespace fs = std::filesystem;
class StaticFileHandler {
private:
string url_prefix_;
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
@ -70,36 +71,47 @@ private:
}
public:
explicit StaticFileHandler(string_view root_path) : root_path_(root_path) {
explicit StaticFileHandler(string_view root_path, string_view url_prefix = "")
: url_prefix_(url_prefix), root_path_(root_path) {
if (!root_path_.empty() && root_path_.back() != '/') {
root_path_ += '/';
}
if (!url_prefix_.empty() && url_prefix_.back() != '/') {
url_prefix_ += '/';
}
}
void handle(const HttpRequest& req, HttpResponse& res) {
bool 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;
return false;
}
if (!is_path_safe(req.path)) {
res.status = 403;
res.set_text("Forbidden");
return;
// Check URL prefix match
if (!url_prefix_.empty()) {
if (req.path.size() < url_prefix_.size() ||
req.path.substr(0, url_prefix_.size()) != url_prefix_) {
return false;
}
}
string file_path = root_path_ + string(req.path.substr(1)); // Remove leading /
// Get path after prefix
string_view file_path_part = url_prefix_.empty() ? req.path : req.path.substr(url_prefix_.size());
// Default to index.html for directories
if (file_path.back() == '/') {
if (!is_path_safe(file_path_part)) {
return false;
}
string file_path = root_path_;
if (file_path_part.empty() || file_path_part == "/") {
file_path += "index.html";
} else {
// Remove leading slash if present
if (file_path_part[0] == '/') file_path_part = file_path_part.substr(1);
file_path += string(file_path_part);
}
if (!fs::exists(file_path) || !fs::is_regular_file(file_path)) {
res.status = 404;
res.set_text("Not Found");
return;
return false;
}
// Get file info
@ -114,9 +126,7 @@ public:
// Read file
res.body = read_file(file_path);
if (res.body.empty()) {
res.status = 500;
res.set_text("Internal Server Error");
return;
return false;
}
// Cache if small enough
@ -150,5 +160,7 @@ public:
if (req.method == HttpMethod::HEAD) {
res.body.clear();
}
return true;
}
};