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 "http_common.hpp"
|
||||||
#include "router.hpp"
|
#include "router.hpp"
|
||||||
|
#include "cookie.hpp"
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
using std::string_view;
|
using std::string_view;
|
||||||
|
|
||||||
@ -16,9 +18,15 @@ struct HttpRequest {
|
|||||||
string_view body;
|
string_view body;
|
||||||
std::unordered_map<string_view, string_view> headers;
|
std::unordered_map<string_view, string_view> headers;
|
||||||
std::unordered_map<std::string, std::string> params; // URL parameters
|
std::unordered_map<std::string, std::string> params; // URL parameters
|
||||||
|
std::vector<Cookie> cookies; // Parsed cookies
|
||||||
size_t content_length = 0;
|
size_t content_length = 0;
|
||||||
|
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
|
|
||||||
|
// Cookie helper method
|
||||||
|
string_view get_cookie(string_view name) const {
|
||||||
|
return CookieHelpers::get_cookie(cookies, name);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class HttpParser {
|
class HttpParser {
|
||||||
@ -83,6 +91,10 @@ public:
|
|||||||
if (name.size() == 14 && strncasecmp(name.data(), "content-length", 14) == 0) {
|
if (name.size() == 14 && strncasecmp(name.data(), "content-length", 14) == 0) {
|
||||||
req.content_length = parse_int(value);
|
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;
|
ptr = header_end + 2;
|
||||||
}
|
}
|
||||||
@ -144,6 +156,7 @@ private:
|
|||||||
char c1 = s1[i] >= 'A' && s1[i] <= 'Z' ? s1[i] + 32 : s1[i];
|
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];
|
char c2 = s2[i] >= 'A' && s2[i] <= 'Z' ? s2[i] + 32 : s2[i];
|
||||||
if (c1 != c2) return c1 - c2;
|
if (c1 != c2) return c1 - c2;
|
||||||
|
if (c1 == 0) break;
|
||||||
}
|
}
|
||||||
return 0;
|
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
|
#pragma once
|
||||||
|
|
||||||
|
#include "cookie.hpp"
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
@ -27,6 +28,18 @@ struct HttpResponse {
|
|||||||
body = html;
|
body = html;
|
||||||
content_type = "text/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 {
|
class HttpResponseBuilder {
|
||||||
@ -34,7 +47,7 @@ private:
|
|||||||
static constexpr size_t BUFFER_SIZE = 4096;
|
static constexpr size_t BUFFER_SIZE = 4096;
|
||||||
static constexpr const char* STATUS_LINES[] = {
|
static constexpr const char* STATUS_LINES[] = {
|
||||||
"HTTP/1.1 200 OK\r\n",
|
"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 400 Bad Request\r\n",
|
||||||
"HTTP/1.1 404 Not Found\r\n",
|
"HTTP/1.1 404 Not Found\r\n",
|
||||||
"HTTP/1.1 500 Internal Server Error\r\n"
|
"HTTP/1.1 500 Internal Server Error\r\n"
|
||||||
@ -116,4 +129,4 @@ public:
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
8
main.cpp
8
main.cpp
@ -20,6 +20,7 @@ int main() {
|
|||||||
// Root route
|
// Root route
|
||||||
router.get("/", [](const HttpRequest& req, HttpResponse& res) {
|
router.get("/", [](const HttpRequest& req, HttpResponse& res) {
|
||||||
res.set_text("Hello, World! HTTP Server with Router\n");
|
res.set_text("Hello, World! HTTP Server with Router\n");
|
||||||
|
res.set_cookie("test_cookie", "hey there!");
|
||||||
});
|
});
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
@ -67,14 +68,7 @@ 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");
|
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";
|
||||||
|
20
server.hpp
20
server.hpp
@ -83,8 +83,8 @@ public:
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
void serve_static(const std::string& static_dir) {
|
void serve_static(const std::string& static_dir, const std::string& url_prefix = "") {
|
||||||
static_handler_ = std::make_shared<StaticFileHandler>(static_dir);
|
static_handler_ = std::make_shared<StaticFileHandler>(static_dir, url_prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -141,18 +141,14 @@ private:
|
|||||||
|
|
||||||
HttpResponse response;
|
HttpResponse response;
|
||||||
|
|
||||||
// Try static files first
|
// Try router first
|
||||||
if (static_handler) {
|
if (router.handle(req, response)) {
|
||||||
static_handler->handle(req, response);
|
send_http_response(client_fd, response, req.version);
|
||||||
if (response.status == 200) {
|
return;
|
||||||
send_http_response(client_fd, response, req.version);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then try router
|
// Then try static files
|
||||||
response = HttpResponse{}; // Reset response
|
if (static_handler && static_handler->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 {
|
||||||
response.status = 404;
|
response.status = 404;
|
||||||
|
@ -18,6 +18,7 @@ namespace fs = std::filesystem;
|
|||||||
|
|
||||||
class StaticFileHandler {
|
class StaticFileHandler {
|
||||||
private:
|
private:
|
||||||
|
string url_prefix_;
|
||||||
string root_path_;
|
string root_path_;
|
||||||
std::unordered_map<string, std::pair<string, std::chrono::time_point<std::chrono::file_clock>>> cache_;
|
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
|
size_t max_cache_size_ = 50 * 1024 * 1024; // 50MB cache limit
|
||||||
@ -70,36 +71,47 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
public:
|
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() != '/') {
|
if (!root_path_.empty() && root_path_.back() != '/') {
|
||||||
root_path_ += '/';
|
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) {
|
if (req.method != HttpMethod::GET && req.method != HttpMethod::HEAD) {
|
||||||
res.status = 405;
|
return false;
|
||||||
res.set_text("Method Not Allowed");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!is_path_safe(req.path)) {
|
// Check URL prefix match
|
||||||
res.status = 403;
|
if (!url_prefix_.empty()) {
|
||||||
res.set_text("Forbidden");
|
if (req.path.size() < url_prefix_.size() ||
|
||||||
return;
|
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 (!is_path_safe(file_path_part)) {
|
||||||
if (file_path.back() == '/') {
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string file_path = root_path_;
|
||||||
|
if (file_path_part.empty() || file_path_part == "/") {
|
||||||
file_path += "index.html";
|
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)) {
|
if (!fs::exists(file_path) || !fs::is_regular_file(file_path)) {
|
||||||
res.status = 404;
|
return false;
|
||||||
res.set_text("Not Found");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get file info
|
// Get file info
|
||||||
@ -114,9 +126,7 @@ public:
|
|||||||
// Read file
|
// Read file
|
||||||
res.body = read_file(file_path);
|
res.body = read_file(file_path);
|
||||||
if (res.body.empty()) {
|
if (res.body.empty()) {
|
||||||
res.status = 500;
|
return false;
|
||||||
res.set_text("Internal Server Error");
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache if small enough
|
// Cache if small enough
|
||||||
@ -150,5 +160,7 @@ public:
|
|||||||
if (req.method == HttpMethod::HEAD) {
|
if (req.method == HttpMethod::HEAD) {
|
||||||
res.body.clear();
|
res.body.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user