#include #include #include #include #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(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(_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_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 split_message(const std::string& s, const std::string& delimiter) { std::vector 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 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 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 services; }; std::vector 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()); } }