Switch to drogon
All checks were successful
Build-Test-Publish / build (linux/amd64) (push) Successful in 56s
Build-Test-Publish / build (linux/arm64) (push) Successful in 1m40s
Build-Test-Publish / create-manifest (push) Successful in 18s

This commit is contained in:
Your Name
2025-06-15 16:19:23 +12:00
parent 69de93c793
commit eedd39a533
10 changed files with 538 additions and 10738 deletions

View File

@@ -18,9 +18,12 @@
#include "utils.hpp"
#include "welcome_page.hpp"
#include "rate_limiter.hpp"
#include "HttpController.hpp"
namespace simple_object_storage {
Server* Server::instance_ = nullptr;
bool Server::init_db() {
try {
@@ -33,33 +36,33 @@ bool Server::init_db() {
}
}
bool Server::validate_write_request(const httplib::Request &req, httplib::Response &res, const std::vector<std::string> &required_params, std::map<std::string, std::string> &params)
bool Server::validate_write_request(const drogon::HttpRequestPtr &req, drogon::HttpResponsePtr &res, const std::vector<std::string> &required_params, std::map<std::string, std::string> &params)
{
std::string client_ip = req.remote_addr;
std::string client_ip = req->getPeerAddr().toIp();
// Check if the client is already over the limit (do NOT increment)
if (auth_rate_limiter_->is_over_limit(client_ip)) {
res.status = 429;
res->setStatusCode(drogon::k429TooManyRequests);
nlohmann::json response = {{"result", "error"}, {"error", "Too many authentication attempts. Please try again later."}};
res.set_content(response.dump(), "application/json");
res->setBody(response.dump());
res->setContentTypeCode(drogon::CT_APPLICATION_JSON);
return false;
}
// Get token from Authorization header
std::string token;
if (req.has_header("Authorization")) {
const auto& auth_header = req.get_header_value("Authorization");
if (auth_header.substr(0, 7) == "Bearer ") {
token = auth_header.substr(7);
}
auto auth_header = req->getHeader("Authorization");
if (!auth_header.empty() && auth_header.substr(0, 7) == "Bearer ") {
token = auth_header.substr(7);
}
if (token.empty()) {
// Only count failed attempt (increment the limiter)
auth_rate_limiter_->is_allowed(client_ip); // This will increment the count
res.status = 401;
res->setStatusCode(drogon::k401Unauthorized);
nlohmann::json response = {{"result", "error"}, {"error", "Missing or invalid Authorization header"}};
res.set_content(response.dump(), "application/json");
res->setBody(response.dump());
res->setContentTypeCode(drogon::CT_APPLICATION_JSON);
return false;
}
@@ -67,23 +70,26 @@ bool Server::validate_write_request(const httplib::Request &req, httplib::Respon
if (!write_token_valid) {
// Only count failed attempt (increment the limiter)
auth_rate_limiter_->is_allowed(client_ip); // This will increment the count
res.status = 403;
res->setStatusCode(drogon::k403Forbidden);
nlohmann::json response = {{"result", "error"}, {"error", "Invalid write token"}};
res.set_content(response.dump(), "application/json");
res->setBody(response.dump());
res->setContentTypeCode(drogon::CT_APPLICATION_JSON);
return false;
}
// If authentication is successful, do not increment rate limiter
for (const auto& param : req.params) {
auto req_params = req->getParameters();
for (const auto& param : req_params) {
params[param.first] = param.second;
}
for (const auto& param : required_params) {
if (!req.has_param(param)) {
res.status = 400;
if (req->getParameter(param).empty()) {
res->setStatusCode(drogon::k400BadRequest);
nlohmann::json response = {{"result", "error"}, {"error", "Missing required query parameter: " + param}};
res.set_content(response.dump(), "application/json");
res->setBody(response.dump());
res->setContentTypeCode(drogon::CT_APPLICATION_JSON);
return false;
}
}
@@ -93,6 +99,7 @@ bool Server::validate_write_request(const httplib::Request &req, httplib::Respon
Server::Server(const ServerConfig& config)
: config_(config), running_(false) {
Server::setInstance(this);
if (!std::filesystem::exists(config_.object_store_path)) {
std::cerr << "Object store directory does not exist: " << config_.object_store_path << std::endl;
@@ -120,6 +127,9 @@ Server::Server(const ServerConfig& config)
Server::~Server() {
stop();
if (Server::instance_ == this) {
Server::setInstance(nullptr);
}
}
bool Server::start() {
@@ -130,115 +140,53 @@ bool Server::start() {
setup_routes();
running_ = true;
if (!server_.listen(config_.host.c_str(), config_.port)) {
// Configure Drogon
drogon::app().addListener(config_.host, config_.port);
drogon::app().setThreadNum(16);
// Start the server
try {
drogon::app().run();
} catch (const std::exception& e) {
running_ = false;
std::cerr << "Failed to listen on " << config_.host << ":" << config_.port << std::endl;
std::cerr << "Failed to start server: " << e.what() << std::endl;
return false;
}
return true; // Should not be reached if listen is blocking and successful
return true;
}
void Server::stop() {
if (running_) {
server_.stop();
drogon::app().quit();
running_ = false;
std::cout << "Server stopped." << std::endl;
}
}
void Server::setup_routes() {
// Add CORS preflight handler for all routes
server_.Options(".*", [this](const httplib::Request& req, httplib::Response& res) {
handle_cors_preflight(req, res);
// Configure global filters for CORS
drogon::app().registerPostHandlingAdvice([this](const drogon::HttpRequestPtr &req, const drogon::HttpResponsePtr &resp) {
add_cors_headers(req, resp);
});
// Add CORS headers to all responses
server_.set_post_routing_handler([this](const httplib::Request& req, httplib::Response& res) {
add_cors_headers(req, res);
});
server_.Get("/index.html", [](const httplib::Request&, httplib::Response& res) {
res.set_content(welcome_page(), "text/html");
});
// Get hash for label:tag
server_.Get("/hash/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
handle_get_hash(req, res);
});
// Get version for label:tag
server_.Get("/version/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
handle_get_version(req, res);
});
// Check if object exists by hash or label:tag
server_.Get("/exists/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
handle_exists(req, res);
});
// Get directory listing
server_.Get("/dir", [this](const httplib::Request& req, httplib::Response& res) {
handle_get_directory(req, res);
});
// Upload object with streaming support
server_.Put("/upload", [this](const httplib::Request& req, httplib::Response& res) {
put_handler_->handle_put_object(req, res);
});
// Update object metadata (new endpoint)
server_.Put("/update", [this](const httplib::Request& req, httplib::Response& res) {
update_handler_->handle_update_object(req, res);
});
// Handle PUT requests to other paths
server_.Put("/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
res.status = 404;
nlohmann::json response = {{"result", "error"}, {"error", "Not found - put requests must be to /upload"}};
res.set_content(response.dump(), "application/json");
});
// Get metadata for label:tag
server_.Get("/meta/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
handle_get_metadata(req, res);
});
// Delete an object (and all tags on that object)
server_.Get("/deleteobject", [this](const httplib::Request& req, httplib::Response& res) {
handle_delete_object(req, res);
});
server_.Get("/status", [this](const httplib::Request& req, httplib::Response& res) {
res.set_content(nlohmann::json({{"result", "success"}, {"status", "ok"}}).dump(), "application/json");
});
// Get object by hash or label:tag
server_.Get("/object/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
handle_get_object(req, res);
});
// Welcome page and download object.
server_.Get("/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
if (req.path == "/") {
res.set_content(welcome_page(), "text/html");
return;
}
// if the path is not /, then it's a hash or label:tag
handle_get_object(req, res);
});
// Handle OPTIONS requests for CORS preflight
drogon::app().registerHandlerViaRegex(".*", [this](const drogon::HttpRequestPtr &req,
std::function<void(const drogon::HttpResponsePtr &)> &&callback) {
handle_cors_preflight(req, std::move(callback));
}, {}, "OPTIONS");
}
void Server::handle_cors_preflight(const httplib::Request& req, httplib::Response& res) {
add_cors_headers(req, res);
res.status = 204; // No content
void Server::handle_cors_preflight(const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr &)>&& callback) {
auto resp = drogon::HttpResponse::newHttpResponse();
add_cors_headers(req, resp);
resp->setStatusCode(drogon::k204NoContent);
callback(resp);
}
void Server::add_cors_headers(const httplib::Request& req, httplib::Response& res) {
void Server::add_cors_headers(const drogon::HttpRequestPtr& req, const drogon::HttpResponsePtr& res) {
// Get the origin from the request
std::string origin = req.get_header_value("Origin");
std::string origin = req->getHeader("Origin");
// If no origin header, no CORS headers needed
if (origin.empty()) {
@@ -255,21 +203,21 @@ void Server::add_cors_headers(const httplib::Request& req, httplib::Response& re
}
if (origin_allowed) {
res.set_header("Access-Control-Allow-Origin", origin);
res->addHeader("Access-Control-Allow-Origin", origin);
// Add other CORS headers
std::string methods = join(config_.allowed_methods, ", ");
res.set_header("Access-Control-Allow-Methods", methods);
res->addHeader("Access-Control-Allow-Methods", methods);
std::string headers = join(config_.allowed_headers, ", ");
res.set_header("Access-Control-Allow-Headers", headers);
res->addHeader("Access-Control-Allow-Headers", headers);
if (config_.allow_credentials) {
res.set_header("Access-Control-Allow-Credentials", "true");
res->addHeader("Access-Control-Allow-Credentials", "true");
}
// Add max age for preflight requests
res.set_header("Access-Control-Max-Age", "86400"); // 24 hours
res->addHeader("Access-Control-Max-Age", "86400"); // 24 hours
}
}
@@ -283,56 +231,69 @@ std::string Server::join(const std::vector<std::string>& strings, const std::str
return result;
}
void Server::handle_get_object(const httplib::Request& req, httplib::Response& res) {
const auto& key = req.matches[1].str();
void Server::handle_get_object(const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr &)>&& callback, const std::string& key) {
std::string hash_str = key;
// first check if the key matches.
dbEntry entry;
if (!db_->get(key, entry)) {
res.status = 404;
auto resp = drogon::HttpResponse::newHttpResponse();
resp->setStatusCode(drogon::k404NotFound);
nlohmann::json response = {{"result", "error"}, {"error", "Couldn't find: " + key}};
res.set_content(response.dump(), "application/json");
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
return;
}
// Construct the file path using the hash string
std::filesystem::path file_path = config_.object_store_path / entry.hash;
if (!std::filesystem::exists(file_path) || !std::filesystem::is_regular_file(file_path)) {
res.status = 404;
auto resp = drogon::HttpResponse::newHttpResponse();
resp->setStatusCode(drogon::k404NotFound);
nlohmann::json response = {{"result", "error"}, {"error", "Hash recognised, but object missing for: " + entry.hash}};
res.set_content(response.dump(), "application/json");
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
return;
}
// Send file using Response::set_file_content
std::string content_type = "application/octet-stream"; // Basic default
res.set_file_content(file_path.string(), content_type);
// No JSON response for file content
// Send file
auto resp = drogon::HttpResponse::newFileResponse(file_path.string());
callback(resp);
}
void Server::handle_get_hash(const httplib::Request& req, httplib::Response& res) {
const auto& labeltag = req.matches[1].str();
void Server::handle_get_hash(const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr &)>&& callback, const std::string& key) {
const auto& labeltag = key;
dbEntry entry;
if (!db_->get(labeltag, entry)) {
res.status = 404;
auto resp = drogon::HttpResponse::newHttpResponse();
resp->setStatusCode(drogon::k404NotFound);
nlohmann::json response = {{"result", "error"}, {"error", "Label:tag not found"}};
res.set_content(response.dump(), "application/json");
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
return;
}
auto resp = drogon::HttpResponse::newHttpResponse();
nlohmann::json response = {{"result", "success"}, {"hash", entry.hash}};
res.set_content(response.dump(), "application/json");
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
}
void Server::handle_get_directory(const httplib::Request& /*req*/, httplib::Response& res) {
void Server::handle_get_directory(const drogon::HttpRequestPtr& /*req*/, std::function<void(const drogon::HttpResponsePtr &)>&& callback) {
std::vector<dbEntry> entries;
if (!db_->list(entries)) {
res.status = 500;
auto resp = drogon::HttpResponse::newHttpResponse();
resp->setStatusCode(drogon::k500InternalServerError);
nlohmann::json response = {{"result", "error"}, {"error", "Failed to retrieve directory listing"}};
res.set_content(response.dump(), "application/json");
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
return;
}
@@ -342,34 +303,41 @@ void Server::handle_get_directory(const httplib::Request& /*req*/, httplib::Resp
entries_array.push_back({{"labeltags", entry.labeltags}, {"hash", entry.hash}});
}
auto resp = drogon::HttpResponse::newHttpResponse();
nlohmann::json response = {{"result", "success"}, {"entries", entries_array}};
res.set_content(response.dump(), "application/json");
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
}
void Server::handle_get_metadata(const httplib::Request& req, httplib::Response& res) {
const auto& key = req.matches[1].str();
void Server::handle_get_metadata(const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr &)>&& callback, const std::string& key) {
std::string hash_str = key;
// Check if the key is a label:tag format
dbEntry entry;
nlohmann::json response;
auto resp = drogon::HttpResponse::newHttpResponse();
if (db_->get(key, entry)) {
// Got it from label:tag, use the hash
try {
response = {{"result", "success"}, {"metadata", entry.metadata}};
resp->setBody(response.dump());
} catch (const nlohmann::json::exception& e) {
std::cerr << "Error serializing metadata for hash " << hash_str << ": " << e.what() << std::endl;
res.status = 500;
resp->setStatusCode(drogon::k500InternalServerError);
response = {{"result", "error"}, {"error", "Internal server error: Failed to serialize metadata"}};
resp->setBody(response.dump());
}
}
else
{
res.status = 404;
resp->setStatusCode(drogon::k404NotFound);
response = {{"result", "error"}, {"error", "Invalid hash: " + hash_str}};
resp->setBody(response.dump());
}
res.set_content(response.dump(), "application/json");
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
}
std::pair<std::string, std::string> Server::parse_labeltag(const std::string& labeltag) const {
@@ -380,28 +348,34 @@ std::pair<std::string, std::string> Server::parse_labeltag(const std::string& la
return {labeltag.substr(0, colon_pos), labeltag.substr(colon_pos + 1)};
}
void Server::handle_delete_object(const httplib::Request& req, httplib::Response& res) {
void Server::handle_delete_object(const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr &)>&& callback) {
dbEntry entry;
auto resp = drogon::HttpResponse::newHttpResponse();
{
std::map<std::string, std::string> params;
if (!validate_write_request(req, res, {"hash"}, params)) {
if (!validate_write_request(req, resp, {"hash"}, params)) {
callback(resp);
return;
}
if (!db_->get(params["hash"], entry)) {
res.status = 404;
resp->setStatusCode(drogon::k404NotFound);
nlohmann::json response = {{"result", "error"}, {"error", "Object not found for: " + params["hash"]}};
res.set_content(response.dump(), "application/json");
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
return;
}
} // we only use sanitised data from here on out.
// Remove all tags that reference this hash
if (!db_->remove_by_hash(entry.hash)) {
res.status = 500;
resp->setStatusCode(drogon::k500InternalServerError);
nlohmann::json response = {{"result", "error"}, {"error", "Failed to remove some or all associated tags"}};
res.set_content(response.dump(), "application/json");
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
return;
}
@@ -412,19 +386,22 @@ void Server::handle_delete_object(const httplib::Request& req, httplib::Response
std::filesystem::remove(file_path);
} catch (const std::filesystem::filesystem_error& e) {
std::cerr << "Error deleting object file: " << e.what() << std::endl;
res.status = 500;
resp->setStatusCode(drogon::k500InternalServerError);
nlohmann::json response = {{"result", "error"}, {"error", "Failed to delete object file: " + std::string(e.what())}};
res.set_content(response.dump(), "application/json");
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
return;
}
}
nlohmann::json response = {{"result", "success"}};
res.set_content(response.dump(), "application/json");
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
}
void Server::handle_exists(const httplib::Request& req, httplib::Response& res) {
const auto& key = req.matches[1].str();
void Server::handle_exists(const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr &)>&& callback, const std::string& key) {
nlohmann::json response;
dbEntry entry;
if (db_->get(key, entry)) {
@@ -432,18 +409,27 @@ void Server::handle_exists(const httplib::Request& req, httplib::Response& res)
} else {
response = {{"result", "success"}, {"exists", false}};
}
res.set_content(response.dump(), "application/json");
auto resp = drogon::HttpResponse::newHttpResponse();
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
}
void Server::handle_get_version(const httplib::Request& req, httplib::Response& res) {
const auto& key = req.matches[1].str();
void Server::handle_get_version(const drogon::HttpRequestPtr& req, std::function<void(const drogon::HttpResponsePtr &)>&& callback) {
// Get the key from the URL path
auto path = req->getPath();
auto pos = path.rfind('/');
std::string key = (pos != std::string::npos) ? path.substr(pos + 1) : "";
std::string hash_str = key;
// Check if the key is a label:tag format
dbEntry entry;
auto resp = drogon::HttpResponse::newHttpResponse();
if (!db_->get(key, entry)) {
nlohmann::json response = {{"result", "failed"}, {"error", "Failed to get version for: " + key}};
res.set_content(response.dump(), "application/json");
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
return;
}
@@ -453,7 +439,9 @@ void Server::handle_get_version(const httplib::Request& req, httplib::Response&
} else {
response = {{"result", "failed"}, {"error", "No version found for: " + key}};
}
res.set_content(response.dump(), "application/json");
resp->setBody(response.dump());
resp->setContentTypeCode(drogon::CT_APPLICATION_JSON);
callback(resp);
}
} // namespace simple_object_storage