273 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			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());
 | 
						|
  }
 | 
						|
}
 |