rewrite router, add kv and session store

This commit is contained in:
Sky Johnson 2025-06-13 12:32:48 -05:00
parent 78b2d8f8fb
commit e3b7bddf05
7 changed files with 225 additions and 90 deletions

View File

@ -1,6 +1,7 @@
#pragma once
#include "http_common.hpp"
#include "http_request.hpp"
#include "router.hpp"
#include "cookie.hpp"
#include <string_view>
@ -10,25 +11,6 @@
using std::string_view;
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;
// Cookie helper method
string_view get_cookie(string_view name) const {
return CookieHelpers::get_cookie(cookies, name);
}
};
class HttpParser {
public:
static HttpRequest parse(string_view data) {

View File

@ -5,6 +5,9 @@
#include <unordered_map>
#include <vector>
using std::string_view;
using std::string;
struct HttpRequest {
HttpMethod method = HttpMethod::UNKNOWN;
string_view path;
@ -12,9 +15,17 @@ struct HttpRequest {
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::unordered_map<string, string> params; // URL parameters
std::vector<Cookie> cookies; // Parsed cookies
size_t content_length = 0;
bool valid = false;
string_view get_cookie(string_view name) const {
return CookieHelpers::get_cookie(cookies, name);
}
string session_id() const {
return string(get_cookie("session_id"));
}
};

66
kv_store.hpp Normal file
View File

@ -0,0 +1,66 @@
#pragma once
#include <unordered_map>
#include <mutex>
#include <fstream>
#include <optional>
#include <string>
using std::string;
class KeyValueStore {
public:
void set(const string& key, const string& value) {
std::lock_guard<std::mutex> lock(mutex_);
data_[key] = value;
}
std::optional<string> get(const string& key) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = data_.find(key);
return it != data_.end() ? std::optional<string>{it->second} : std::nullopt;
}
bool del(const string& key) {
std::lock_guard<std::mutex> lock(mutex_);
return data_.erase(key) > 0;
}
size_t size() {
std::lock_guard<std::mutex> lock(mutex_);
return data_.size();
}
void set_file(const string& filename) {
filename_ = filename;
}
void load() {
std::ifstream file(filename_);
if (!file.is_open()) return;
std::lock_guard<std::mutex> lock(mutex_);
string line;
while (std::getline(file, line)) {
size_t eq = line.find('=');
if (eq != string::npos) {
data_[line.substr(0, eq)] = line.substr(eq + 1);
}
}
}
void save() {
std::lock_guard<std::mutex> lock(mutex_);
std::ofstream file(filename_);
if (!file.is_open()) return;
for (const auto& [key, value] : data_) {
file << key << "=" << value << "\n";
}
}
private:
std::unordered_map<string, string> data_;
mutable std::mutex mutex_;
string filename_ = "server_store.txt";
};

View File

@ -45,7 +45,6 @@ int main() {
res.status = 201;
});
// Request info route
router.get("/request-info", [](const HttpRequest& req, HttpResponse& res) {
std::string info = "Method: " + std::to_string(static_cast<int>(req.method)) + "\n";
info += "Path: " + std::string(req.path) + "\n";
@ -55,15 +54,16 @@ int main() {
res.set_text(info);
});
// Different HTTP methods
router.put("/users/:id", [](const HttpRequest& req, HttpResponse& res) {
std::string id = req.params.at("id");
res.set_json("{\"message\":\"User " + id + " updated\"}");
router.get("/foo", [](const HttpRequest& req, HttpResponse& res) {
res.set_text("Admin");
});
router.del("/users/:id", [](const HttpRequest& req, HttpResponse& res) {
std::string id = req.params.at("id");
res.set_json("{\"message\":\"User " + id + " deleted\"}");
router.get("/admin/counter", [](const HttpRequest& req, HttpResponse& res) {
auto current = server->store.get("visit_count");
int count = current ? std::stoi(current.value()) : 0;
server->store.set("visit_count", std::to_string(count + 1));
res.set_text("Counter: " + std::to_string(count + 1));
});
server = new HttpServer(8080, router);

View File

@ -15,13 +15,12 @@ using std::string_view;
using Handler = std::function<void(const HttpRequest&, HttpResponse&)>;
struct TrieNode {
std::string prefix;
std::string param_name; // For parameter nodes
std::string param_name;
std::unordered_map<HttpMethod, Handler> handlers;
std::unordered_map<char, std::unique_ptr<TrieNode>> children;
bool is_param = false;
std::unordered_map<std::string, std::unique_ptr<TrieNode>> children;
std::unique_ptr<TrieNode> param_child;
TrieNode(string_view p = "") : prefix(p) {}
TrieNode() = default;
};
class Router {
@ -39,7 +38,7 @@ private:
}
bool is_param = path[i] == ':';
if (is_param) i++; // skip ':'
if (is_param) i++;
size_t start = i;
while (i < path.length() && path[i] != '/') i++;
@ -47,31 +46,17 @@ private:
std::string segment(path.substr(start, i - start));
if (is_param) {
// All parameters share the same node
auto it = current->children.find(':');
if (it == current->children.end()) {
auto new_node = std::make_unique<TrieNode>();
new_node->is_param = true;
new_node->param_name = segment;
current->children[':'] = std::move(new_node);
if (!current->param_child) {
current->param_child = std::make_unique<TrieNode>();
}
current = current->children[':'].get();
current->param_child->param_name = segment;
current = current->param_child.get();
} else {
char first_char = segment[0];
auto it = current->children.find(first_char);
auto it = current->children.find(segment);
if (it == current->children.end()) {
auto new_node = std::make_unique<TrieNode>(segment);
current->children[first_char] = std::move(new_node);
current = current->children[first_char].get();
} else {
current = it->second.get();
if (current->prefix != segment) {
// Handle prefix mismatch - split node if needed
auto new_node = std::make_unique<TrieNode>(segment);
current->children[first_char] = std::move(new_node);
current = current->children[first_char].get();
}
current->children[segment] = std::make_unique<TrieNode>();
}
current = current->children[segment].get();
}
}
return current;
@ -91,19 +76,19 @@ private:
while (i < path.length() && path[i] != '/') i++;
string_view segment = path.substr(start, i - start);
std::string segment_str(segment);
// Try exact match first
auto it = current->children.find(segment[0]);
if (it != current->children.end() && it->second->prefix == segment) {
auto it = current->children.find(segment_str);
if (it != current->children.end()) {
current = it->second.get();
continue;
}
// Try parameter match
auto param_it = current->children.find(':');
if (param_it != current->children.end()) {
current = param_it->second.get();
params[current->param_name] = std::string(segment);
if (current->param_child) {
params[current->param_child->param_name] = segment_str;
current = current->param_child.get();
continue;
}
@ -153,29 +138,3 @@ public:
return false;
}
};
// Usage example:
/*
Router router;
router.get("/users", [](const HttpRequest& req, HttpResponse& res) {
res.set_json("{\"users\": []}");
});
router.get("/users/:id", [](const HttpRequest& req, HttpResponse& res) {
std::string id = req.params.at("id");
res.set_json("{\"id\": \"" + id + "\"}");
});
router.post("/users", [](const HttpRequest& req, HttpResponse& res) {
// Use req.body to get POST data
res.status = 201;
res.set_json("{\"created\": true}");
});
router.get("/users/:id/posts/:postId", [](const HttpRequest& req, HttpResponse& res) {
std::string userId = req.params.at("id");
std::string postId = req.params.at("postId");
res.set_json("{\"userId\": \"" + userId + "\", \"postId\": \"" + postId + "\"}");
});
*/

View File

@ -5,6 +5,8 @@
#include "http_parser.hpp"
#include "http_response.hpp"
#include "static_file_handler.hpp"
#include "kv_store.hpp"
#include "session_store.hpp"
#include <iostream>
#include <string.h>
#include <string_view>
@ -16,10 +18,15 @@
class HttpServer {
public:
KeyValueStore store;
SessionStore sessions;
explicit HttpServer(uint16_t port, Router& router) : port_(port), router_(router) {}
~HttpServer() {
stop();
store.save();
sessions.save();
for (auto& worker : workers_) {
if (worker->thread.joinable()) {
worker->thread.join();
@ -28,13 +35,16 @@ public:
}
bool start() {
store.load();
sessions.load();
unsigned int num_cores = std::thread::hardware_concurrency();
if (num_cores == 0) num_cores = 1;
workers_.reserve(num_cores);
for (unsigned int i = 0; i < num_cores; ++i) {
auto worker = std::make_unique<Worker>(port_, router_, static_handler_);
auto worker = std::make_unique<Worker>(port_, router_, static_handler_, sessions);
if (!worker->socket.start()) return false;
workers_.push_back(std::move(worker));
}
@ -95,11 +105,12 @@ private:
EpollSocket socket;
Router& router;
std::shared_ptr<StaticFileHandler>& static_handler;
SessionStore& sessions;
std::array<char, BUFFER_SIZE> buffer;
std::thread thread;
Worker(uint16_t port, Router& r, std::shared_ptr<StaticFileHandler>& sh)
: socket(port), router(r), static_handler(sh) {
Worker(uint16_t port, Router& r, std::shared_ptr<StaticFileHandler>& sh, SessionStore& s)
: socket(port), router(r), static_handler(sh), sessions(s) {
socket.on_connection([this](int fd) { handle_connection(fd); });
socket.on_data([this](int fd) { handle_data(fd); });
socket.on_disconnect([this](int fd) { handle_disconnect(fd); });
@ -139,24 +150,52 @@ private:
return;
}
// Handle session
std::string session_id = get_session_id(req);
if (session_id.empty()) {
session_id = sessions.create();
}
HttpResponse response;
// Try router first
if (router.handle(req, response)) {
set_session_cookie(response, session_id);
send_http_response(client_fd, response, req.version);
return;
}
// Then try static files
if (static_handler && static_handler->handle(req, response)) {
set_session_cookie(response, session_id);
send_http_response(client_fd, response, req.version);
} else {
response.status = 404;
response.set_text("Not Found");
set_session_cookie(response, session_id);
send_http_response(client_fd, response, req.version);
}
}
std::string get_session_id(const HttpRequest& req) {
auto it = req.headers.find("Cookie");
if (it == req.headers.end()) return "";
std::string_view cookies = it->second;
size_t pos = cookies.find("session_id=");
if (pos == std::string_view::npos) return "";
pos += 11; // length of "session_id="
size_t end = cookies.find(';', pos);
if (end == std::string_view::npos) end = cookies.length();
return std::string(cookies.substr(pos, end - pos));
}
void set_session_cookie(HttpResponse& response, const std::string& session_id) {
response.headers["Set-Cookie"] = "session_id=" + session_id + "; HttpOnly; Path=/; SameSite=Strict";
}
void send_http_response(int client_fd, const HttpResponse& response, string_view version) {
std::string http_response = HttpResponseBuilder::build_response(response, version);
send_raw_response(client_fd, http_response);

78
session_store.hpp Normal file
View File

@ -0,0 +1,78 @@
#pragma once
#include "kv_store.hpp"
#include <random>
#include <unordered_map>
#include <mutex>
class SessionStore {
public:
SessionStore() : rng_(std::random_device{}()) {}
std::string create() {
std::string id = generate_id();
std::lock_guard<std::mutex> lock(mutex_);
sessions_[id] = {};
return id;
}
std::optional<std::string> get(const std::string& id, const std::string& key) {
std::lock_guard<std::mutex> lock(mutex_);
auto it = sessions_.find(id);
if (it == sessions_.end()) return std::nullopt;
auto data_it = it->second.find(key);
return data_it != it->second.end() ? std::optional<std::string>{data_it->second} : std::nullopt;
}
void set(const std::string& id, const std::string& key, const std::string& value) {
std::lock_guard<std::mutex> lock(mutex_);
sessions_[id][key] = value;
}
bool del(const std::string& id) {
std::lock_guard<std::mutex> lock(mutex_);
return sessions_.erase(id) > 0;
}
size_t size() {
std::lock_guard<std::mutex> lock(mutex_);
return sessions_.size();
}
void set_file(const std::string& filename) {
store_.set_file(filename);
}
void load() {
store_.load();
}
void save() {
std::lock_guard<std::mutex> lock(mutex_);
for (const auto& [id, data] : sessions_) {
for (const auto& [key, value] : data) {
store_.set(id + ":" + key, value);
}
}
store_.save();
}
private:
std::unordered_map<std::string, std::unordered_map<std::string, std::string>> sessions_;
mutable std::mutex mutex_;
KeyValueStore store_;
std::mt19937 rng_;
std::string generate_id() {
static const char chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
std::string id;
id.reserve(16);
for (int i = 0; i < 16; ++i) {
id += chars[rng_() % (sizeof(chars) - 1)];
}
return id;
}
};