.
This commit is contained in:
@@ -26,6 +26,8 @@ add_executable(dropshell_template_registry ${SOURCES})
|
||||
# Link libraries
|
||||
target_link_libraries(dropshell_template_registry
|
||||
pthread
|
||||
ssl
|
||||
crypto
|
||||
)
|
||||
|
||||
# Set compile options for static linking
|
||||
|
10509
src/httplib.hpp
Normal file
10509
src/httplib.hpp
Normal file
File diff suppressed because it is too large
Load Diff
291
src/server.cpp
291
src/server.cpp
@@ -1,19 +1,35 @@
|
||||
#include "server.hpp"
|
||||
#include "hash.hpp"
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
#include <algorithm>
|
||||
#include <iostream>
|
||||
#include <random> // For random number generation
|
||||
#include <chrono> // For seeding random number generator
|
||||
|
||||
namespace dropshell {
|
||||
|
||||
// Simple RAII helper for file deletion
|
||||
class ScopeFileDeleter {
|
||||
public:
|
||||
ScopeFileDeleter(const std::filesystem::path& path) : path_(path), released_(false) {}
|
||||
~ScopeFileDeleter() {
|
||||
if (!released_) {
|
||||
try {
|
||||
std::filesystem::remove(path_);
|
||||
} catch (const std::filesystem::filesystem_error& e) {
|
||||
std::cerr << "Error deleting temp file: " << e.what() << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
void release() { released_ = true; }
|
||||
private:
|
||||
std::filesystem::path path_;
|
||||
bool released_;
|
||||
};
|
||||
|
||||
Server::Server(const ServerConfig& config)
|
||||
: config_(config), server_socket_(-1), running_(false) {
|
||||
: config_(config), running_(false) {
|
||||
// Create object store directory if it doesn't exist
|
||||
std::filesystem::create_directories(config_.object_store_path);
|
||||
}
|
||||
@@ -23,256 +39,163 @@ Server::~Server() {
|
||||
}
|
||||
|
||||
bool Server::start() {
|
||||
// Create socket
|
||||
server_socket_ = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (server_socket_ < 0) {
|
||||
std::cerr << "Failed to create socket" << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set socket options
|
||||
int opt = 1;
|
||||
if (setsockopt(server_socket_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
|
||||
std::cerr << "Failed to set socket options" << std::endl;
|
||||
close(server_socket_);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bind socket
|
||||
struct sockaddr_in address;
|
||||
address.sin_family = AF_INET;
|
||||
address.sin_addr.s_addr = INADDR_ANY;
|
||||
address.sin_port = htons(config_.port);
|
||||
|
||||
if (bind(server_socket_, (struct sockaddr*)&address, sizeof(address)) < 0) {
|
||||
std::cerr << "Failed to bind socket" << std::endl;
|
||||
close(server_socket_);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Listen for connections
|
||||
if (listen(server_socket_, 10) < 0) {
|
||||
std::cerr << "Failed to listen on socket" << std::endl;
|
||||
close(server_socket_);
|
||||
return false;
|
||||
}
|
||||
setup_routes();
|
||||
|
||||
std::cout << "Server starting on " << config_.host << ":" << config_.port << std::endl;
|
||||
running_ = true;
|
||||
std::cout << "Server started on port " << config_.port << std::endl;
|
||||
|
||||
// Accept connections
|
||||
while (running_) {
|
||||
int client_socket = accept(server_socket_, nullptr, nullptr);
|
||||
if (client_socket < 0) {
|
||||
if (running_) {
|
||||
std::cerr << "Failed to accept connection" << std::endl;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle connection in a new thread
|
||||
std::thread([this, client_socket]() {
|
||||
handle_connection(client_socket);
|
||||
close(client_socket);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
return true;
|
||||
return server_.listen(config_.host.c_str(), config_.port);
|
||||
}
|
||||
|
||||
void Server::stop() {
|
||||
running_ = false;
|
||||
if (server_socket_ >= 0) {
|
||||
close(server_socket_);
|
||||
server_socket_ = -1;
|
||||
if (running_) {
|
||||
server_.stop();
|
||||
running_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Server::handle_connection(int client_socket) {
|
||||
char buffer[4096];
|
||||
int bytes_read = read(client_socket, buffer, sizeof(buffer) - 1);
|
||||
if (bytes_read <= 0) {
|
||||
return;
|
||||
}
|
||||
buffer[bytes_read] = '\0';
|
||||
void Server::setup_routes() {
|
||||
// Welcome page
|
||||
server_.Get("/index.html", [](const httplib::Request&, httplib::Response& res) {
|
||||
res.set_content("<html><body><h1>Dropshell Template Registry</h1></body></html>", "text/html");
|
||||
});
|
||||
|
||||
std::string request(buffer);
|
||||
std::istringstream iss(request);
|
||||
std::string method, path, version;
|
||||
iss >> method >> path >> version;
|
||||
// Get object by hash or label:tag
|
||||
server_.Get("/object/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
handle_get_object(req, res);
|
||||
});
|
||||
|
||||
// Parse path
|
||||
if (path.empty() || path[0] != '/') {
|
||||
send_response(client_socket, 400, "text/plain", "Invalid path");
|
||||
return;
|
||||
}
|
||||
// Get hash for label:tag
|
||||
server_.Get("/hash/(.*)", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
handle_get_hash(req, res);
|
||||
});
|
||||
|
||||
if (path == "/index.html" || path == "/") {
|
||||
send_response(client_socket, 200, "text/html",
|
||||
"<html><body><h1>Dropshell Template Registry</h1></body></html>");
|
||||
return;
|
||||
}
|
||||
// Get directory listing
|
||||
server_.Get("/dir", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
handle_get_directory(req, res);
|
||||
});
|
||||
|
||||
if (path == "/dir") {
|
||||
handle_get_directory(client_socket);
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.find("/object/") == 0) {
|
||||
handle_get_object(client_socket, path.substr(8));
|
||||
return;
|
||||
}
|
||||
|
||||
if (path.find("/hash/") == 0) {
|
||||
handle_get_hash(client_socket, path.substr(6));
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle PUT requests
|
||||
if (method == "PUT") {
|
||||
size_t token_pos = path.find('/', 1);
|
||||
if (token_pos != std::string::npos) {
|
||||
std::string token = path.substr(1, token_pos - 1);
|
||||
std::string label_tag = path.substr(token_pos + 1);
|
||||
handle_put_object(client_socket, token, label_tag);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
send_response(client_socket, 404, "text/plain", "Not found");
|
||||
// Upload object
|
||||
server_.Put("/([^/]+)/([^/]+)", [this](const httplib::Request& req, httplib::Response& res) {
|
||||
handle_put_object(req, res);
|
||||
});
|
||||
}
|
||||
|
||||
void Server::handle_get_object(int client_socket, const std::string& path) {
|
||||
void Server::handle_get_object(const httplib::Request& req, httplib::Response& res) {
|
||||
const auto& path = req.matches[1];
|
||||
uint64_t hash = 0;
|
||||
|
||||
try {
|
||||
hash = std::stoull(path);
|
||||
} catch (...) {
|
||||
// Try to find hash by label:tag
|
||||
auto it = label_tag_index_.find(path);
|
||||
if (it == label_tag_index_.end()) {
|
||||
send_response(client_socket, 404, "text/plain", "Object not found");
|
||||
res.status = 404;
|
||||
res.set_content("Object not found", "text/plain");
|
||||
return;
|
||||
}
|
||||
hash = it->second;
|
||||
}
|
||||
|
||||
auto it = object_index_.find(hash);
|
||||
if (it == object_index_.end()) {
|
||||
send_response(client_socket, 404, "text/plain", "Object not found");
|
||||
return;
|
||||
}
|
||||
|
||||
std::filesystem::path file_path = config_.object_store_path / std::to_string(hash);
|
||||
if (!std::filesystem::exists(file_path)) {
|
||||
send_response(client_socket, 404, "text/plain", "Object file not found");
|
||||
res.status = 404;
|
||||
res.set_content("Object file not found", "text/plain");
|
||||
return;
|
||||
}
|
||||
|
||||
send_file(client_socket, file_path);
|
||||
// Send file using Response::set_file_content
|
||||
res.set_file_content(file_path.string());
|
||||
}
|
||||
|
||||
void Server::handle_get_hash(int client_socket, const std::string& path) {
|
||||
void Server::handle_get_hash(const httplib::Request& req, httplib::Response& res) {
|
||||
const auto& path = req.matches[1];
|
||||
auto it = label_tag_index_.find(path);
|
||||
if (it == label_tag_index_.end()) {
|
||||
send_response(client_socket, 404, "text/plain", "Label:tag not found");
|
||||
res.status = 404;
|
||||
res.set_content("Label:tag not found", "text/plain");
|
||||
return;
|
||||
}
|
||||
|
||||
send_response(client_socket, 200, "text/plain", std::to_string(it->second));
|
||||
res.set_content(std::to_string(it->second), "text/plain");
|
||||
}
|
||||
|
||||
void Server::handle_get_directory(int client_socket) {
|
||||
void Server::handle_get_directory(const httplib::Request& req, httplib::Response& res) {
|
||||
std::stringstream ss;
|
||||
for (const auto& [hash, info] : object_index_) {
|
||||
ss << info.label << ":" << info.tag << "," << hash << "\n";
|
||||
for (const auto& [labeltag, hash] : label_tag_index_) {
|
||||
ss << labeltag << "," << hash << "\n";
|
||||
}
|
||||
send_response(client_socket, 200, "text/plain", ss.str());
|
||||
res.set_content(ss.str(), "text/plain");
|
||||
}
|
||||
|
||||
void Server::handle_put_object(int client_socket, const std::string& token, const std::string& label_tag) {
|
||||
void Server::handle_put_object(const httplib::Request& req, httplib::Response& res) {
|
||||
const auto& token = req.matches[1];
|
||||
const auto& label_tag = req.matches[2];
|
||||
|
||||
if (!validate_write_token(token)) {
|
||||
send_response(client_socket, 403, "text/plain", "Invalid write token");
|
||||
res.status = 403;
|
||||
res.set_content("Invalid write token", "text/plain");
|
||||
return;
|
||||
}
|
||||
|
||||
auto [label, tag] = parse_label_tag(label_tag);
|
||||
if (label.empty() || tag.empty()) {
|
||||
send_response(client_socket, 400, "text/plain", "Invalid label:tag format");
|
||||
res.status = 400;
|
||||
res.set_content("Invalid label:tag format", "text/plain");
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate a random number for the temporary filename
|
||||
std::mt19937_64 rng(std::chrono::high_resolution_clock::now().time_since_epoch().count());
|
||||
std::uniform_int_distribution<uint64_t> dist;
|
||||
uint64_t random_num = dist(rng);
|
||||
std::string temp_filename = "temp_" + std::to_string(random_num);
|
||||
|
||||
// Create temporary file
|
||||
std::filesystem::path temp_path = config_.object_store_path / "temp";
|
||||
std::filesystem::path temp_path = config_.object_store_path / temp_filename;
|
||||
std::ofstream temp_file(temp_path, std::ios::binary);
|
||||
if (!temp_file) {
|
||||
send_response(client_socket, 500, "text/plain", "Failed to create temporary file");
|
||||
if (!temp_file.is_open()) {
|
||||
res.status = 500;
|
||||
res.set_content("Failed to create temporary file", "text/plain");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read request body
|
||||
char buffer[4096];
|
||||
int bytes_read;
|
||||
while ((bytes_read = read(client_socket, buffer, sizeof(buffer))) > 0) {
|
||||
temp_file.write(buffer, bytes_read);
|
||||
}
|
||||
// Write request body to temporary file
|
||||
temp_file.write(req.body.data(), req.body.size());
|
||||
temp_file.close();
|
||||
|
||||
// Ensure the temporary file is removed even if errors occur
|
||||
ScopeFileDeleter temp_file_deleter(temp_path);
|
||||
|
||||
// Calculate hash
|
||||
uint64_t hash = hash_file(temp_path.string());
|
||||
if (hash == 0) {
|
||||
std::filesystem::remove(temp_path);
|
||||
send_response(client_socket, 500, "text/plain", "Failed to calculate hash");
|
||||
return;
|
||||
res.status = 500;
|
||||
res.set_content("Failed to calculate hash", "text/plain");
|
||||
return; // Deleter will remove temp_file
|
||||
}
|
||||
|
||||
// Move file to final location
|
||||
std::filesystem::path final_path = config_.object_store_path / std::to_string(hash);
|
||||
if (!std::filesystem::exists(final_path)) {
|
||||
std::filesystem::rename(temp_path, final_path);
|
||||
try {
|
||||
std::filesystem::rename(temp_path, final_path);
|
||||
temp_file_deleter.release(); // Don't delete if rename succeeded
|
||||
} catch (const std::filesystem::filesystem_error& e) {
|
||||
std::cerr << "Error renaming temp file: " << e.what() << std::endl;
|
||||
res.status = 500;
|
||||
res.set_content("Failed to store object file", "text/plain");
|
||||
return; // Deleter will remove temp_file
|
||||
}
|
||||
} else {
|
||||
std::filesystem::remove(temp_path);
|
||||
// File with the same hash already exists, no need to move
|
||||
// temp_file_deleter will automatically remove the temp file
|
||||
}
|
||||
|
||||
// Update indices
|
||||
object_index_[hash] = {label, tag, hash};
|
||||
label_tag_index_[label + ":" + tag] = hash;
|
||||
|
||||
send_response(client_socket, 200, "text/plain", std::to_string(hash));
|
||||
}
|
||||
|
||||
void Server::send_response(int client_socket, int status_code, const std::string& content_type, const std::string& body) {
|
||||
std::stringstream ss;
|
||||
ss << "HTTP/1.1 " << status_code << " " << (status_code == 200 ? "OK" : "Error") << "\r\n";
|
||||
ss << "Content-Type: " << content_type << "\r\n";
|
||||
ss << "Content-Length: " << body.length() << "\r\n";
|
||||
ss << "Connection: close\r\n\r\n";
|
||||
ss << body;
|
||||
|
||||
std::string response = ss.str();
|
||||
write(client_socket, response.c_str(), response.length());
|
||||
}
|
||||
|
||||
void Server::send_file(int client_socket, const std::filesystem::path& file_path) {
|
||||
std::ifstream file(file_path, std::ios::binary);
|
||||
if (!file) {
|
||||
send_response(client_socket, 500, "text/plain", "Failed to open file");
|
||||
return;
|
||||
}
|
||||
|
||||
std::stringstream ss;
|
||||
ss << "HTTP/1.1 200 OK\r\n";
|
||||
ss << "Content-Type: application/octet-stream\r\n";
|
||||
ss << "Connection: close\r\n\r\n";
|
||||
std::string header = ss.str();
|
||||
write(client_socket, header.c_str(), header.length());
|
||||
|
||||
char buffer[4096];
|
||||
while (file.read(buffer, sizeof(buffer))) {
|
||||
write(client_socket, buffer, file.gcount());
|
||||
}
|
||||
if (file.gcount() > 0) {
|
||||
write(client_socket, buffer, file.gcount());
|
||||
}
|
||||
res.set_content(std::to_string(hash), "text/plain");
|
||||
}
|
||||
|
||||
bool Server::validate_write_token(const std::string& token) const {
|
||||
|
@@ -2,6 +2,7 @@
|
||||
#define SERVER_HPP
|
||||
|
||||
#include "config.hpp"
|
||||
#include "httplib.hpp"
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <memory>
|
||||
@@ -25,20 +26,17 @@ private:
|
||||
uint64_t hash;
|
||||
};
|
||||
|
||||
void handle_connection(int client_socket);
|
||||
void handle_get_object(int client_socket, const std::string& path);
|
||||
void handle_get_hash(int client_socket, const std::string& path);
|
||||
void handle_get_directory(int client_socket);
|
||||
void handle_put_object(int client_socket, const std::string& token, const std::string& label_tag);
|
||||
void send_response(int client_socket, int status_code, const std::string& content_type, const std::string& body);
|
||||
void send_file(int client_socket, const std::filesystem::path& file_path);
|
||||
void setup_routes();
|
||||
void handle_get_object(const httplib::Request& req, httplib::Response& res);
|
||||
void handle_get_hash(const httplib::Request& req, httplib::Response& res);
|
||||
void handle_get_directory(const httplib::Request& req, httplib::Response& res);
|
||||
void handle_put_object(const httplib::Request& req, httplib::Response& res);
|
||||
bool validate_write_token(const std::string& token) const;
|
||||
std::pair<std::string, std::string> parse_label_tag(const std::string& label_tag) const;
|
||||
|
||||
const ServerConfig& config_;
|
||||
int server_socket_;
|
||||
httplib::Server server_;
|
||||
std::atomic<bool> running_;
|
||||
std::unordered_map<uint64_t, ObjectInfo> object_index_;
|
||||
std::unordered_map<std::string, uint64_t> label_tag_index_;
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user