bettola/server/src/network_manager.cpp

273 lines
9.9 KiB
C++

#include <cstdio>
#include <exception>
#include <functional>
#include <memory>
#include "network_manager.h"
#include "db/database_manager.h"
#include "asio/error_code.hpp"
#include "asio/ip/tcp.hpp"
#include "command_processor.h"
#include "player.h"
#include "machine.h"
#include "net/tcp_connection.h"
#include "vfs.h"
NetworkManager::NetworkManager(const std::string& db_path) :
_db_manager(std::make_unique<DatabaseManager>(db_path)),
_acceptor(_io_context),
_machine_manager(_db_manager.get()) {
_db_manager->init();
_seed_npc_machines();
/* Load NPC machines from the database. */
auto npc_machines = _db_manager->machines().get_all_npcs();
for(const auto& machine_data : npc_machines) {
_world_machines[machine_data.ip_address] =
_machine_manager.load_machine(machine_data.id, _db_manager.get());
}
fprintf(stderr, "Created world with %zu networks\n", _world_machines.size());
}
NetworkManager::~NetworkManager(void) {
for(auto const& [ip, machine] : _world_machines) {
delete machine;
}
stop();
}
void NetworkManager::stop(void) {
_io_context.stop();
if(_context_thread.joinable()) {
_context_thread.join();
}
}
void NetworkManager::start(uint16_t port) {
try {
asio::ip::tcp::endpoint endpoint(asio::ip::tcp::v4(), port);
_acceptor.open(endpoint.protocol());
_acceptor.set_option(asio::ip::tcp::acceptor::reuse_address(true));
_acceptor.bind(endpoint);
_acceptor.listen();
fprintf(stderr, "BettolaServer started on port %d\n", port);
start_accept();
/* Run io_context in its own thread so it doesn't block main. */
_context_thread = std::thread([this]() { _io_context.run(); });
} catch (const std::exception& e) {
fprintf(stderr, "BettolaServer exception: %s\n", e.what());
}
}
void NetworkManager::start_accept(void) {
_acceptor.async_accept(
[this](const asio::error_code& ec, asio::ip::tcp::socket socket) {
if(!ec) {
fprintf(stderr, "New connection from: %s\n",
socket.remote_endpoint().address().to_string().c_str());
auto new_connection =
std::make_shared<net::TcpConnection>(_io_context, std::move(socket));
/* Create a new player for this connection. */
uint32_t player_id = _next_player_id++;
Machine* player_machine = _machine_manager.create_machine(player_id, "player.home",
"player");
auto new_player = std::make_unique<Player>(player_id, player_machine,
_world_machines, _db_manager.get(),
&_machine_manager);
Player* new_player_ptr = new_player.get();
_players[player_id] = std::move(new_player);
new_connection->set_id(player_id);
/* Callback for new connection. Capture new_connection's shared_ptr
* to keep it alive and ident it.
*/
new_connection->start(
[this, new_connection](const std::string& msg) {
this->on_message(new_connection, msg);
},
[this, new_connection]() { this->on_disconnect(new_connection); });
/* Send initial prompt. */
std::string prompt = "\n" + get_full_path(new_player_ptr->cmd_processor->get_current_dir());
new_connection->send(prompt);
_connections.push_back(new_connection);
} else {
fprintf(stderr, "Accept error: %s\n", ec.message().c_str());
}
/* Continue listening for the next connection. */
start_accept();
});
}
static std::vector<std::string> split_message(const std::string& s, const std::string& delimiter) {
std::vector<std::string> tokens;
size_t start = 0, end = 0;
while((end = s.find(delimiter, start)) != std::string::npos) {
tokens.push_back(s.substr(start, end-start));
start = end + delimiter.length();
}
tokens.push_back(s.substr(start));
return tokens;
}
void NetworkManager::on_message(std::shared_ptr<net::TcpConnection> connection,
const std::string& message) {
Player* player = _players[connection->get_id()].get();
if(!player) {
fprintf(stderr, "Error: Receiving message from unknown player.\n");
return;
}
if(player->state == PlayerState::AUTHENTICATING) {
if(message.rfind("C_ACC::", 0) == 0) {
std::string payload = message.substr(7);
auto parts = split_message(payload, "::");
if(parts.size() == 3) {
if(_db_manager->create_player(parts[0], parts[1], parts[2],
_machine_manager.get_vfs_template())) {
long long machine_id = _db_manager->players().get_home_machine_id(parts[0]);
Machine* home_machine = _machine_manager.load_machine(machine_id, _db_manager.get());
delete player->cmd_processor; /* Delete old command processor. */
player->cmd_processor = new CommandProcessor(home_machine, _world_machines,
_db_manager.get(),
&_machine_manager);
player->state = PlayerState::ACTIVE;
connection->send("C_ACC_SUCCESS");
/* send initial prompt. */
std::string prompt = "\n" + get_full_path(player->cmd_processor->get_current_dir());
connection->send(prompt);
} else {
connection->send("C_ACC_FAIL");
}
}
} else if(message.rfind("LOGIN::", 0) == 0) {
std::string payload = message.substr(7);
auto parts = split_message(payload, "::");
if(parts.size() == 2) {
if(_db_manager->players().authenticate(parts[0], parts[1])) {
long long machine_id = _db_manager->players().get_home_machine_id(parts[0]);
printf("DEBUG: Loading machine %lld for player %s\n", machine_id, parts[0].c_str());
Machine* home_machine = _machine_manager.load_machine(machine_id, _db_manager.get());
delete player->cmd_processor; /* Delete old command processor. */
player->cmd_processor = new CommandProcessor(home_machine, _world_machines,
_db_manager.get(), &_machine_manager);
player->state = PlayerState::ACTIVE;
connection->send("LOGIN_SUCCESS");
/* Send initial prompt. */
std::string prompt = "\n" + get_full_path(player->cmd_processor->get_current_dir());
connection->send(prompt);
} else {
connection->send("LOGIN_FAIL");
}
}
} else {
/* Ignore all other messages while authing. */
connection->send("ERR: Not authenticated.\n");
}
return;
}
/* === PLAYER BECOMES ACTIVE HERE === */
/* Check for "special" message prefixes. */
if(message.rfind("WRITEF::", 0) == 0) {
/* Message format: WRITEF::/path/to/file::content. */
std::string payload = message.substr(8);
size_t separator_pos = payload.find("::");
if(separator_pos != std::string::npos) {
std::string filepath = payload.substr(0, separator_pos);
std::string content = payload.substr(separator_pos+2);
fprintf(stderr, "[Player %u] Write file: \'%s\'\n", player->id, filepath.c_str());
player->cmd_processor->write_file(filepath, content);
/* Response not required for a file write. */
}
return;
}
if(message.rfind("READF::", 0) == 0) {
std::string filepath = message.substr(7);
fprintf(stderr, "[Player %u] Read file: '%s'\n", player->id, filepath.c_str());
std::string content = player->cmd_processor->read_file(filepath);
/* Send the content back to the client. */
connection->send("FILEDATA::" + filepath + "::" + content);
return;
}
/* If no prefix, treat as normal terminal command. */
fprintf(stderr, "[Player %u] Command: '%s'\n", player->id, message.c_str());
std::string response = player->cmd_processor->process_command(message);
if(response == "__CLOSE_CONNECTION__") {
connection->send(response);
return;
}
std::string new_prompt = get_full_path(player->cmd_processor->get_current_dir());
response += "\n" + new_prompt;
connection->send(response);
}
void NetworkManager::on_disconnect(std::shared_ptr<net::TcpConnection> connection) {
uint32_t player_id = connection->get_id();
fprintf(stderr, "[Player %u] Disconnected.\n", player_id);
_connections.erase(
std::remove(_connections.begin(), _connections.end(), connection), _connections.end());
_players.erase(player_id);
}
void NetworkManager::_seed_npc_machines(void) {
int npc_count = _db_manager->machines().get_npc_count();
if(npc_count > 0) {
return; /* NPC already in db. */
}
fprintf(stderr, "First run: Seeding NPC machines into database...\n");
struct NpcDef {
std::string hostname;
std::string ip;
std::map<int, std::string> services;
};
std::vector<NpcDef> npc_defs = {
{"dns.google", "8.8.8.8", {{80, "HTTPD v2.4"}}},
{"corp.internal", "10.0.2.15", {{21, "FTPd v3.0"}}}
};
try {
for(const auto& def : npc_defs) {
long long machine_id = _db_manager->machines().create({}, def.hostname, def.ip);
/* Create a basic VFS for the NPC machines. */
long long root_id = _db_manager->vfs().create_node(machine_id, nullptr, "/", DIR_NODE);
_db_manager->vfs().create_node(machine_id, &root_id, "etc", DIR_NODE);
long long bin_id = _db_manager->vfs().create_node(machine_id, &root_id, "bin", DIR_NODE);
vfs_node* template_bin = _machine_manager.get_vfs_template()->children["bin"];
for(auto const& [name, node] : template_bin->children) {
_db_manager->vfs().create_node(machine_id, &bin_id, name, FILE_NODE, node->content);
}
for(const auto& service : def.services) {
_db_manager->services().create(machine_id, service.first, service.second);
}
}
} catch(const std::exception& e) {
fprintf(stderr, "Error seeding NPCs: %s. Database may be in an inconsistent state.\n", e.what());
}
}