diff --git a/cookie.hpp b/cookie.hpp new file mode 100644 index 0000000..e502aa6 --- /dev/null +++ b/cookie.hpp @@ -0,0 +1,136 @@ +#pragma once + +#include +#include +#include + +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 parse(string_view cookie_header) { + std::vector 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& 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; + } +}; diff --git a/http_parser.hpp b/http_parser.hpp index d901b2f..2c93583 100644 --- a/http_parser.hpp +++ b/http_parser.hpp @@ -2,9 +2,11 @@ #include "http_common.hpp" #include "router.hpp" +#include "cookie.hpp" #include #include #include +#include using std::string_view; @@ -16,9 +18,15 @@ struct HttpRequest { string_view body; std::unordered_map headers; std::unordered_map params; // URL parameters + std::vector 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; } diff --git a/http_request.hpp b/http_request.hpp new file mode 100644 index 0000000..7fee4ea --- /dev/null +++ b/http_request.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "http_common.hpp" +#include "cookie.hpp" +#include +#include + +struct HttpRequest { + HttpMethod method = HttpMethod::UNKNOWN; + string_view path; + string_view query; + string_view version; + string_view body; + std::unordered_map headers; + std::unordered_map params; // URL parameters + std::vector cookies; // Parsed cookies + size_t content_length = 0; + + bool valid = false; +}; diff --git a/http_response.hpp b/http_response.hpp index 7c93b65..59f23a8 100644 --- a/http_response.hpp +++ b/http_response.hpp @@ -1,5 +1,6 @@ #pragma once +#include "cookie.hpp" #include #include #include @@ -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; } -}; \ No newline at end of file +}; diff --git a/main.cpp b/main.cpp index 19c280d..f18eadf 100644 --- a/main.cpp +++ b/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"; diff --git a/server.hpp b/server.hpp index fbbbb7a..318ebe4 100644 --- a/server.hpp +++ b/server.hpp @@ -83,8 +83,8 @@ public: return ""; } - void serve_static(const std::string& static_dir) { - static_handler_ = std::make_shared(static_dir); + void serve_static(const std::string& static_dir, const std::string& url_prefix = "") { + static_handler_ = std::make_shared(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; diff --git a/static_file_handler.hpp b/static_file_handler.hpp index ef479a3..d9021d6 100644 --- a/static_file_handler.hpp +++ b/static_file_handler.hpp @@ -18,6 +18,7 @@ namespace fs = std::filesystem; class StaticFileHandler { private: + string url_prefix_; string root_path_; std::unordered_map>> 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; } };