fix static file serving, add cookie support
This commit is contained in:
parent
538ae9f8b4
commit
78b2d8f8fb
136
cookie.hpp
Normal file
136
cookie.hpp
Normal 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;
|
||||
}
|
||||
};
|
@ -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
20
http_request.hpp
Normal 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;
|
||||
};
|
@ -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 {
|
||||
|
8
main.cpp
8
main.cpp
@ -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";
|
||||
|
16
server.hpp
16
server.hpp
@ -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) {
|
||||
// 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;
|
||||
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user