#include #include #include #include #include // For random number generation #include // For seeding random number generator #include // For getAllKeys #include // For litecask values #include // For std::runtime_error #include #include "server.hpp" #include "hash.hpp" #include "compress.hpp" #include "string_utils.hpp" // Include the new utility header #include "put_handler.hpp" #include "utils.hpp" #include "welcome_page.hpp" namespace simple_object_storage { bool Server::init_db() { try { std::filesystem::path db_path = config_.object_store_path / "index.db"; db_ = std::make_unique(db_path); return true; } catch (const std::runtime_error& e) { std::cerr << "Database initialization error: " << e.what() << std::endl; return false; } } bool Server::validate_write_request(const httplib::Request &req, httplib::Response &res, const std::vector &required_params, std::map ¶ms) { std::string client_ip = req.remote_addr; // Check if the client is already over the limit (do NOT increment) if (auth_rate_limiter_->is_over_limit(client_ip)) { res.status = 429; nlohmann::json response = {{"result", "error"}, {"error", "Too many authentication attempts. Please try again later."}}; res.set_content(response.dump(), "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); } } 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; nlohmann::json response = {{"result", "error"}, {"error", "Missing or invalid Authorization header"}}; res.set_content(response.dump(), "application/json"); return false; } bool write_token_valid = std::find(config_.write_tokens.begin(), config_.write_tokens.end(), token) != config_.write_tokens.end(); 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; nlohmann::json response = {{"result", "error"}, {"error", "Invalid write token"}}; res.set_content(response.dump(), "application/json"); return false; } // If authentication is successful, do not increment rate limiter 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; nlohmann::json response = {{"result", "error"}, {"error", "Missing required query parameter: " + param}}; res.set_content(response.dump(), "application/json"); return false; } } return true; } Server::Server(const ServerConfig& config) : config_(config), running_(false) { if (!std::filesystem::exists(config_.object_store_path)) { std::cerr << "Object store directory does not exist: " << config_.object_store_path << std::endl; return; } // Initialize the database if (!init_db()) { // Error already printed in init_db // Consider throwing or setting an error state } // Initialize the put handler put_handler_ = std::make_unique(*this); // Initialize rate limiter auth_rate_limiter_ = std::make_unique( config_.auth_rate_limit, std::chrono::seconds(config_.auth_window_seconds) ); } Server::~Server() { stop(); } bool Server::start() { if (!db_) { // Check if DB initialization failed std::cerr << "Database is not initialized. Cannot start server." << std::endl; return false; } setup_routes(); running_ = true; if (!server_.listen(config_.host.c_str(), config_.port)) { running_ = false; std::cerr << "Failed to listen on " << config_.host << ":" << config_.port << std::endl; return false; } return true; // Should not be reached if listen is blocking and successful } void Server::stop() { if (running_) { server_.stop(); 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); }); // 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); }); // 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); }); } void Server::handle_cors_preflight(const httplib::Request& req, httplib::Response& res) { add_cors_headers(req, res); res.status = 204; // No content } void Server::add_cors_headers(const httplib::Request& req, httplib::Response& res) { // Get the origin from the request std::string origin = req.get_header_value("Origin"); // If no origin header, no CORS headers needed if (origin.empty()) { return; } // Check if origin is allowed bool origin_allowed = false; if (config_.allowed_origins.empty() || std::find(config_.allowed_origins.begin(), config_.allowed_origins.end(), "*") != config_.allowed_origins.end()) { origin_allowed = true; } else { origin_allowed = std::find(config_.allowed_origins.begin(), config_.allowed_origins.end(), origin) != config_.allowed_origins.end(); } if (origin_allowed) { res.set_header("Access-Control-Allow-Origin", origin); // Add other CORS headers std::string methods = join(config_.allowed_methods, ", "); res.set_header("Access-Control-Allow-Methods", methods); std::string headers = join(config_.allowed_headers, ", "); res.set_header("Access-Control-Allow-Headers", headers); if (config_.allow_credentials) { res.set_header("Access-Control-Allow-Credentials", "true"); } // Add max age for preflight requests res.set_header("Access-Control-Max-Age", "86400"); // 24 hours } } std::string Server::join(const std::vector& strings, const std::string& delimiter) { if (strings.empty()) return ""; std::string result = strings[0]; for (size_t i = 1; i < strings.size(); ++i) { result += delimiter + strings[i]; } return result; } void Server::handle_get_object(const httplib::Request& req, httplib::Response& res) { const auto& key = req.matches[1].str(); std::string hash_str = key; // first check if the key matches. dbEntry entry; if (!db_->get(key, entry)) { res.status = 404; nlohmann::json response = {{"result", "error"}, {"error", "Couldn't find: " + key}}; res.set_content(response.dump(), "application/json"); 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; nlohmann::json response = {{"result", "error"}, {"error", "Hash recognised, but object missing for: " + entry.hash}}; res.set_content(response.dump(), "application/json"); 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 } void Server::handle_get_hash(const httplib::Request& req, httplib::Response& res) { const auto& labeltag = req.matches[1].str(); dbEntry entry; if (!db_->get(labeltag, entry)) { res.status = 404; nlohmann::json response = {{"result", "error"}, {"error", "Label:tag not found"}}; res.set_content(response.dump(), "application/json"); return; } nlohmann::json response = {{"result", "success"}, {"hash", entry.hash}}; res.set_content(response.dump(), "application/json"); } void Server::handle_get_directory(const httplib::Request& /*req*/, httplib::Response& res) { std::vector entries; if (!db_->list(entries)) { res.status = 500; nlohmann::json response = {{"result", "error"}, {"error", "Failed to retrieve directory listing"}}; res.set_content(response.dump(), "application/json"); return; } // output the labeltags as an array of label:tag pairs nlohmann::json entries_array = nlohmann::json::array(); for (const auto& entry : entries) { entries_array.push_back({{"labeltags", entry.labeltags}, {"hash", entry.hash}}); } nlohmann::json response = {{"result", "success"}, {"entries", entries_array}}; res.set_content(response.dump(), "application/json"); } void Server::handle_get_metadata(const httplib::Request& req, httplib::Response& res) { const auto& key = req.matches[1].str(); std::string hash_str = key; // Check if the key is a label:tag format dbEntry entry; nlohmann::json response; if (db_->get(key, entry)) { // Got it from label:tag, use the hash try { response = {{"result", "success"}, {"metadata", entry.metadata}}; } catch (const nlohmann::json::exception& e) { std::cerr << "Error serializing metadata for hash " << hash_str << ": " << e.what() << std::endl; res.status = 500; response = {{"result", "error"}, {"error", "Internal server error: Failed to serialize metadata"}}; } } else { res.status = 404; response = {{"result", "error"}, {"error", "Invalid hash: " + hash_str}}; } res.set_content(response.dump(), "application/json"); } std::pair Server::parse_labeltag(const std::string& labeltag) const { size_t colon_pos = labeltag.find(':'); if (colon_pos == std::string::npos || colon_pos == 0 || colon_pos == labeltag.length() - 1) { return {"", ""}; } return {labeltag.substr(0, colon_pos), labeltag.substr(colon_pos + 1)}; } void Server::handle_delete_object(const httplib::Request& req, httplib::Response& res) { dbEntry entry; { std::map params; if (!validate_write_request(req, res, {"hash"}, params)) { return; } if (!db_->get(params["hash"], entry)) { res.status = 404; nlohmann::json response = {{"result", "error"}, {"error", "Object not found for: " + params["hash"]}}; res.set_content(response.dump(), "application/json"); 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; nlohmann::json response = {{"result", "error"}, {"error", "Failed to remove some or all associated tags"}}; res.set_content(response.dump(), "application/json"); return; } // Remove the file std::filesystem::path file_path = config_.object_store_path / entry.hash; if (std::filesystem::exists(file_path) && std::filesystem::is_regular_file(file_path)) { try { 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; nlohmann::json response = {{"result", "error"}, {"error", "Failed to delete object file: " + std::string(e.what())}}; res.set_content(response.dump(), "application/json"); return; } } nlohmann::json response = {{"result", "success"}}; res.set_content(response.dump(), "application/json"); } void Server::handle_exists(const httplib::Request& req, httplib::Response& res) { const auto& key = req.matches[1].str(); nlohmann::json response; dbEntry entry; if (db_->get(key, entry)) { response = {{"result", "success"}, {"exists", true}}; } else { response = {{"result", "success"}, {"exists", false}}; } res.set_content(response.dump(), "application/json"); } void Server::handle_get_version(const httplib::Request& req, httplib::Response& res) { const auto& key = req.matches[1].str(); std::string hash_str = key; // Check if the key is a label:tag format dbEntry entry; if (!db_->get(key, entry)) { nlohmann::json response = {{"result", "failed"}, {"error", "Failed to get version for: " + key}}; res.set_content(response.dump(), "application/json"); return; } nlohmann::json response; if (entry.metadata.contains("version")) { response = {{"result", "success"}, {"version", entry.metadata["version"]}}; } else { response = {{"result", "failed"}, {"error", "No version found for: " + key}}; } res.set_content(response.dump(), "application/json"); } } // namespace simple_object_storage