577 lines
19 KiB
C++
577 lines
19 KiB
C++
/*
|
|
EQ2Emulator: Everquest II Server Emulator
|
|
Copyright (C) 2007 EQ2EMulator Development Team (http://www.eq2emulator.net)
|
|
|
|
This file is part of EQ2Emulator.
|
|
*/
|
|
|
|
// Debug and common includes
|
|
#include "../common/debug.h"
|
|
|
|
// Standard library includes
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <chrono>
|
|
#include <cstring>
|
|
#include <ctime>
|
|
#include <filesystem>
|
|
#include <format>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <random>
|
|
#include <signal.h>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <thread>
|
|
#include <termios.h>
|
|
#include <fcntl.h>
|
|
|
|
// Project common includes
|
|
#include "../common/queue.h"
|
|
#include "../common/timer.h"
|
|
#include "../common/seperator.h"
|
|
#include "../common/packet_functions.h"
|
|
#include "../common/EQStreamFactory.h"
|
|
#include "../common/MiscFunctions.h"
|
|
#include "../common/version.h"
|
|
#include "../common/PacketStruct.h"
|
|
#include "../common/DataBuffer.h"
|
|
#include "../common/ConfigReader.h"
|
|
#include "../common/Log.h"
|
|
#include "../common/JsonParser.h"
|
|
#include "../common/Common_Defines.h"
|
|
#include "../common/CRC16.h"
|
|
#include "../common/timer.h"
|
|
#include "../common/unix.h"
|
|
|
|
// Login server includes
|
|
#include "net.h"
|
|
#include "client.h"
|
|
#include "LoginDatabase.h"
|
|
#include "LWorld.h"
|
|
|
|
// Global instances
|
|
EQStreamFactory eqsf(LoginStream); // Factory for creating EQ network streams
|
|
std::map<std::int16_t, OpcodeManager*> EQOpcodeManager; // Maps version to opcode manager
|
|
NetConnection net; // Main network connection instance
|
|
ClientList client_list; // List of connected clients
|
|
LWorldList world_list; // List of connected world servers
|
|
LoginDatabase database; // Database connection manager
|
|
ConfigReader configReader; // Configuration file reader
|
|
std::map<std::int16_t, std::int16_t> EQOpcodeVersions; // Maps client versions to opcode versions
|
|
Timer statTimer(60000); // Timer for periodic statistics updates (1 minute)
|
|
|
|
// Global control flag for main loop
|
|
volatile bool RunLoops = true;
|
|
|
|
// Forward declarations
|
|
bool ReadLoginConfig();
|
|
|
|
/**
|
|
* @brief Check if a key has been pressed (Linux implementation)
|
|
* @return true if a key is available, false otherwise
|
|
*/
|
|
int kbhit()
|
|
{
|
|
struct termios oldt, newt;
|
|
int ch;
|
|
int oldf;
|
|
|
|
// Get current terminal settings
|
|
tcgetattr(STDIN_FILENO, &oldt);
|
|
newt = oldt;
|
|
|
|
// Disable canonical mode and echo
|
|
newt.c_lflag &= ~(ICANON | ECHO);
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &newt);
|
|
|
|
// Set non-blocking mode
|
|
oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
|
|
fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
|
|
|
|
ch = getchar();
|
|
|
|
// Restore terminal settings
|
|
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
|
|
fcntl(STDIN_FILENO, F_SETFL, oldf);
|
|
|
|
if(ch != EOF)
|
|
{
|
|
ungetc(ch, stdin);
|
|
return 1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Main entry point for the login server
|
|
* @param argc Number of command line arguments
|
|
* @param argv Array of command line arguments
|
|
* @return Exit code (0 for success, 1 for failure)
|
|
*/
|
|
int main(int argc, char** argv)
|
|
{
|
|
// Set up signal handler for graceful shutdown
|
|
if (signal(SIGINT, CatchSignal) == SIG_ERR)
|
|
{
|
|
std::cerr << "Could not set signal handler" << std::endl;
|
|
}
|
|
|
|
// Initialize logging system
|
|
LogStart();
|
|
LogParseConfigs();
|
|
|
|
// Display welcome header
|
|
net.WelcomeHeader();
|
|
|
|
// Initialize random number generator with current time
|
|
std::random_device rd;
|
|
std::mt19937 gen(rd());
|
|
|
|
// Read and validate login server configuration
|
|
if (!net.ReadLoginConfig())
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
// Initialize web server if configured
|
|
net.InitWebServer(
|
|
net.GetWebLoginAddress(),
|
|
net.GetWebLoginPort(),
|
|
net.GetWebCertFile(),
|
|
net.GetWebKeyFile(),
|
|
net.GetWebKeyPassword(),
|
|
net.GetWebHardcodeUser(),
|
|
net.GetWebHardcodePassword()
|
|
);
|
|
|
|
// Load structure definition files for packet parsing
|
|
const std::array<const char*, 2> structList =
|
|
{
|
|
"CommonStructs.xml",
|
|
"LoginStructs.xml"
|
|
};
|
|
|
|
for (const auto& structFile : structList)
|
|
{
|
|
LogWrite(INIT__INFO, 0, "Init", "Loading Structs File %s..", structFile);
|
|
|
|
if (configReader.processXML_Elements(structFile))
|
|
{
|
|
LogWrite(INIT__INFO, 0, "Init", "Loading Structs File %s completed..", structFile);
|
|
}
|
|
else
|
|
{
|
|
LogWrite(INIT__ERROR, 0, "Init", "Loading Structs File %s FAILED!", structFile);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Initialize world server list
|
|
LogWrite(INIT__INFO, 0, "Init", "Initialize World List..");
|
|
world_list.Init();
|
|
|
|
// Log listening configuration
|
|
if (eqsf.listen_ip_address)
|
|
{
|
|
LogWrite(INIT__INFO, 0, "Init", "Login server listening on %s port %i",
|
|
eqsf.listen_ip_address, net.GetPort());
|
|
}
|
|
else
|
|
{
|
|
LogWrite(INIT__INFO, 0, "Init", "Login server listening on port %i", net.GetPort());
|
|
}
|
|
|
|
// Open network port for incoming connections
|
|
if (!eqsf.Open(net.GetPort()))
|
|
{
|
|
LogWrite(INIT__ERROR, 0, "Init", "Failed to open port %i.", net.GetPort());
|
|
return 1;
|
|
}
|
|
|
|
// Set server running state
|
|
net.login_running = true;
|
|
net.login_uptime = getCurrentTimestamp();
|
|
|
|
// Update console window title (no-op on Linux)
|
|
net.UpdateWindowTitle();
|
|
|
|
// Initialize timeout timer for connection management
|
|
EQStream* eqs = nullptr;
|
|
auto timeoutTimer = std::make_unique<Timer>(5000);
|
|
timeoutTimer->Start();
|
|
|
|
// Main server loop
|
|
while (RunLoops)
|
|
{
|
|
// Update current time for all timers
|
|
Timer::SetCurrentTime();
|
|
|
|
// Process new incoming connections
|
|
while ((eqs = eqsf.Pop()))
|
|
{
|
|
struct in_addr in;
|
|
in.s_addr = eqs->GetRemoteIP();
|
|
|
|
LogWrite(LOGIN__INFO, 0, "Login", "New client from IP: %s on port %i",
|
|
inet_ntoa(in), ntohs(eqs->GetRemotePort()));
|
|
|
|
// Create new client instance and add to client list
|
|
auto client = std::make_unique<Client>(eqs);
|
|
eqs->SetClientVersion(0);
|
|
client_list.Add(client.release());
|
|
net.numclients++;
|
|
net.UpdateWindowTitle();
|
|
}
|
|
|
|
// Check for timed out connections
|
|
if (timeoutTimer->Check())
|
|
{
|
|
eqsf.CheckTimeout();
|
|
}
|
|
|
|
// Update statistics periodically
|
|
if (statTimer.Check())
|
|
{
|
|
world_list.UpdateWorldStats();
|
|
database.RemoveOldWorldServerStats();
|
|
database.FixBugReport();
|
|
}
|
|
|
|
// Process active clients and world servers
|
|
client_list.Process();
|
|
world_list.Process();
|
|
|
|
// Handle console input (Linux version)
|
|
if (kbhit())
|
|
{
|
|
int hitkey = getchar();
|
|
|
|
switch (hitkey)
|
|
{
|
|
case 'l':
|
|
case 'L':
|
|
{
|
|
// List all connected world servers
|
|
world_list.ListWorldsToConsole();
|
|
break;
|
|
}
|
|
|
|
case 'v':
|
|
case 'V':
|
|
{
|
|
// Display version information
|
|
std::cout << "========Version Info=========\n";
|
|
std::cout << std::format("{} {}\n", EQ2EMU_MODULE, CURRENT_VERSION);
|
|
std::cout << std::format("Last Compiled on {} {}\n", COMPILE_DATE, COMPILE_TIME);
|
|
std::cout << "=============================\n\n";
|
|
break;
|
|
}
|
|
|
|
case 'H':
|
|
case 'h':
|
|
{
|
|
// Display help menu
|
|
std::cout << "===========Help=============\n";
|
|
std::cout << "Available Commands:\n";
|
|
std::cout << "l = Listing of World Servers\n";
|
|
std::cout << "v = Login Version\n";
|
|
std::cout << "q = Quit server\n";
|
|
std::cout << "============================\n\n";
|
|
break;
|
|
}
|
|
|
|
case 'q':
|
|
case 'Q':
|
|
{
|
|
// Quit server gracefully
|
|
std::cout << "Shutting down server...\n";
|
|
RunLoops = false;
|
|
break;
|
|
}
|
|
|
|
default:
|
|
{
|
|
std::cout << "Invalid Command. Press 'h' for help.\n";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Small sleep to prevent CPU spinning
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
|
|
// Cleanup and shutdown
|
|
eqsf.Close();
|
|
world_list.Shutdown();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* @brief Signal handler for graceful shutdown
|
|
* @param sig_num The signal number received
|
|
*/
|
|
void CatchSignal(int sig_num)
|
|
{
|
|
std::cout << "Got signal " << sig_num << std::endl;
|
|
RunLoops = false;
|
|
}
|
|
|
|
/**
|
|
* @brief Constructor for NetConnection class
|
|
* Initializes all member variables to their default values
|
|
*/
|
|
NetConnection::NetConnection() noexcept
|
|
: port_{5999}
|
|
, listening_socket_{0}
|
|
, master_address_{}
|
|
, uplink_port_{0}
|
|
, uplink_account_{}
|
|
, uplink_password_{}
|
|
, login_mode_{ServerMode::Standalone}
|
|
, uplink_wrong_version_{false}
|
|
, numclients{0}
|
|
, numservers{0}
|
|
, allow_account_creation_{true}
|
|
, expansion_flag_{0x7CFF} // Full support = 0x7CFF
|
|
, cities_flag_{0xFF} // All cities enabled
|
|
, default_subscription_level_{0xFFFFFFFF}
|
|
, enabled_races_{0xFFFF} // All races enabled
|
|
, web_login_port_{0}
|
|
, login_webserver_{nullptr}
|
|
, login_running{false}
|
|
, login_uptime{getCurrentTimestamp()}
|
|
{
|
|
// Initialize address buffer
|
|
std::memset(address, 0, sizeof(address));
|
|
}
|
|
|
|
/**
|
|
* @brief Destructor for NetConnection class
|
|
* Ensures proper cleanup of web server resources
|
|
*/
|
|
NetConnection::~NetConnection()
|
|
{
|
|
// Unique_ptr automatically handles cleanup of login_webserver
|
|
}
|
|
|
|
/**
|
|
* @brief Reads and parses the login server configuration file
|
|
* @return true if configuration loaded successfully, false otherwise
|
|
*/
|
|
bool NetConnection::ReadLoginConfig()
|
|
{
|
|
// Load JSON configuration file
|
|
JsonParser parser(MAIN_CONFIG_FILE);
|
|
if (!parser.IsLoaded())
|
|
{
|
|
LogWrite(INIT__ERROR, 0, "Init", "Failed to find %s in server directory..", MAIN_CONFIG_FILE);
|
|
return false;
|
|
}
|
|
|
|
// Parse server port configuration
|
|
std::string serverport = parser.getValue("loginconfig.serverport");
|
|
std::string serverip = parser.getValue("loginconfig.serverip");
|
|
|
|
if (!parser.convertStringToUnsignedShort(serverport, port_))
|
|
{
|
|
LogWrite(INIT__ERROR, 0, "Init", "Failed to translate loginconfig.serverport..");
|
|
return false;
|
|
}
|
|
|
|
// Set listening IP address if specified
|
|
if (!serverip.empty())
|
|
{
|
|
eqsf.listen_ip_address = new char[serverip.size() + 1];
|
|
std::strcpy(eqsf.listen_ip_address, serverip.c_str());
|
|
}
|
|
else
|
|
{
|
|
safe_delete(eqsf.listen_ip_address);
|
|
eqsf.listen_ip_address = nullptr;
|
|
}
|
|
|
|
// Parse account creation setting
|
|
std::string acctcreate_str = parser.getValue("loginconfig.accountcreation");
|
|
std::uint16_t allow_acct = 0;
|
|
parser.convertStringToUnsignedShort(acctcreate_str, allow_acct);
|
|
allow_account_creation_ = (allow_acct > 0);
|
|
|
|
// Parse expansion and feature flags
|
|
std::string expflag_str = parser.getValue("loginconfig.expansionflag");
|
|
parser.convertStringToUnsignedInt(expflag_str, expansion_flag_);
|
|
|
|
std::string citiesflag_str = parser.getValue("loginconfig.citiesflag");
|
|
parser.convertStringToUnsignedChar(citiesflag_str, cities_flag_);
|
|
|
|
std::string defaultsublevel_str = parser.getValue("loginconfig.defaultsubscriptionlevel");
|
|
parser.convertStringToUnsignedInt(defaultsublevel_str, default_subscription_level_);
|
|
|
|
std::string enableraces_str = parser.getValue("loginconfig.enabledraces");
|
|
parser.convertStringToUnsignedInt(enableraces_str, enabled_races_);
|
|
|
|
// Parse web server configuration
|
|
web_login_address_ = parser.getValue("loginconfig.webloginaddress");
|
|
web_cert_file_ = parser.getValue("loginconfig.webcertfile");
|
|
web_key_file_ = parser.getValue("loginconfig.webkeyfile");
|
|
web_key_password_ = parser.getValue("loginconfig.webkeypassword");
|
|
web_hardcode_user_ = parser.getValue("loginconfig.webhardcodeuser");
|
|
web_hardcode_password_ = parser.getValue("loginconfig.webhardcodepassword");
|
|
|
|
std::string webloginport_str = parser.getValue("loginconfig.webloginport");
|
|
parser.convertStringToUnsignedShort(webloginport_str, web_login_port_);
|
|
|
|
LogWrite(INIT__INFO, 0, "Init", "%s loaded..", MAIN_CONFIG_FILE);
|
|
|
|
// Initialize database connection
|
|
LogWrite(INIT__INFO, 0, "Init", "Database init begin..");
|
|
if (!database.Init())
|
|
{
|
|
LogWrite(INIT__ERROR, 0, "Init", "Database init FAILED!");
|
|
LogStop();
|
|
return false;
|
|
}
|
|
|
|
// Load opcode definitions from database
|
|
LogWrite(INIT__INFO, 0, "Init", "Loading opcodes 2.0..");
|
|
EQOpcodeVersions = database.GetVersions();
|
|
|
|
for (const auto& [version, _] : EQOpcodeVersions)
|
|
{
|
|
EQOpcodeManager[version] = new RegularOpcodeManager();
|
|
auto opcodes = database.GetOpcodes(version);
|
|
|
|
if (!EQOpcodeManager[version]->LoadOpcodes(&opcodes))
|
|
{
|
|
LogWrite(INIT__ERROR, 0, "Init", "Loading opcodes failed. Make sure you have sourced the opcodes.sql file!");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @brief Updates the console window title with server statistics
|
|
* @param iNewTitle Optional custom title string
|
|
* Note: This is a no-op on Linux, kept for API compatibility
|
|
*/
|
|
void NetConnection::UpdateWindowTitle(const char* iNewTitle)
|
|
{
|
|
// No-op on Linux - terminal titles are handled differently
|
|
// Could potentially use ANSI escape codes if needed:
|
|
// std::cout << "\033]0;" << title << "\007";
|
|
}
|
|
|
|
/**
|
|
* @brief Displays the welcome header with ASCII art and information
|
|
*/
|
|
void NetConnection::WelcomeHeader()
|
|
{
|
|
// Use ANSI color codes for Linux terminal
|
|
const char* WHITE_BOLD = "\033[1;37m";
|
|
const char* YELLOW_BOLD = "\033[1;33m";
|
|
const char* GREEN_BOLD = "\033[1;32m";
|
|
const char* MAGENTA_BOLD = "\033[1;35m";
|
|
const char* RESET = "\033[0m";
|
|
|
|
// Display module and version information
|
|
std::cout << WHITE_BOLD;
|
|
std::cout << std::format("Module: {}, Version: {}", EQ2EMU_MODULE, CURRENT_VERSION);
|
|
|
|
// Display copyright and license information
|
|
std::cout << YELLOW_BOLD;
|
|
std::cout << "\n\nCopyright (C) 2007-2021 EQ2Emulator. https://www.eq2emu.com \n\n";
|
|
std::cout << "EQ2Emulator is free software: you can redistribute it and/or modify\n";
|
|
std::cout << "it under the terms of the GNU General Public License as published by\n";
|
|
std::cout << "the Free Software Foundation, either version 3 of the License, or\n";
|
|
std::cout << "(at your option) any later version.\n\n";
|
|
std::cout << "EQ2Emulator is distributed in the hope that it will be useful,\n";
|
|
std::cout << "but WITHOUT ANY WARRANTY; without even the implied warranty of\n";
|
|
std::cout << "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n";
|
|
std::cout << "GNU General Public License for more details.\n\n";
|
|
|
|
// Display ASCII art logo
|
|
std::cout << GREEN_BOLD;
|
|
std::cout << " /$$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$$ \n";
|
|
std::cout << "| $$_____/ /$$__ $$ /$$__ $$| $$_____/ \n";
|
|
std::cout << "| $$ | $$ \\ $$|__/ \\ $$| $$ /$$$$$$/$$$$ /$$ /$$\n";
|
|
std::cout << "| $$$$$ | $$ | $$ /$$$$$$/| $$$$$ | $$_ $$_ $$| $$ | $$\n";
|
|
std::cout << "| $$__/ | $$ | $$ /$$____/ | $$__/ | $$ \\ $$ \\ $$| $$ | $$\n";
|
|
std::cout << "| $$ | $$/$$ $$| $$ | $$ | $$ | $$ | $$| $$ | $$\n";
|
|
std::cout << "| $$$$$$$$| $$$$$$/| $$$$$$$$| $$$$$$$$| $$ | $$ | $$| $$$$$$/\n";
|
|
std::cout << "|________/ \\____ $$$|________/|________/|__/ |__/ |__/ \\______/ \n";
|
|
std::cout << " \\__/ \n\n";
|
|
|
|
// Display community links
|
|
std::cout << MAGENTA_BOLD;
|
|
std::cout << " Website : https://eq2emu.com \n";
|
|
std::cout << " Wiki : https://wiki.eq2emu.com \n";
|
|
std::cout << " Git : https://git.eq2emu.com \n";
|
|
std::cout << " Discord : https://discord.gg/5Cavm9NYQf \n\n";
|
|
|
|
std::cout << WHITE_BOLD;
|
|
std::cout << "For more detailed logging, modify 'Level' param the log_config.xml file.\n\n";
|
|
|
|
// Reset terminal colors
|
|
std::cout << RESET;
|
|
std::cout.flush();
|
|
}
|
|
|
|
/**
|
|
* @brief Initializes the web server for HTTP/HTTPS API access
|
|
* @param web_ipaddr IP address to bind to
|
|
* @param web_port Port number for web server
|
|
* @param cert_file SSL certificate file path
|
|
* @param key_file SSL key file path
|
|
* @param key_password Password for SSL key
|
|
* @param hardcode_user Basic auth username
|
|
* @param hardcode_password Basic auth password
|
|
*/
|
|
void NetConnection::InitWebServer(std::string_view web_ipaddr,
|
|
std::uint16_t web_port,
|
|
std::string_view cert_file,
|
|
std::string_view key_file,
|
|
std::string_view key_password,
|
|
std::string_view hardcode_user,
|
|
std::string_view hardcode_password)
|
|
{
|
|
// Only initialize if address and port are configured
|
|
if (!web_ipaddr.empty() && web_port > 0)
|
|
{
|
|
try
|
|
{
|
|
// Create web server instance with SSL configuration
|
|
login_webserver_ = std::make_unique<WebServer>(
|
|
std::string(web_ipaddr),
|
|
web_port,
|
|
std::string(cert_file),
|
|
std::string(key_file),
|
|
std::string(key_password),
|
|
std::string(hardcode_user),
|
|
std::string(hardcode_password)
|
|
);
|
|
|
|
// Register API routes
|
|
login_webserver_->register_route("/status", NetConnection::Web_loginhandle_status);
|
|
login_webserver_->register_route("/worlds", NetConnection::Web_loginhandle_worlds);
|
|
|
|
// Start web server
|
|
login_webserver_->run();
|
|
|
|
LogWrite(INIT__INFO, 0, "Init", "Login Web Server is listening on %s:%u..",
|
|
web_ipaddr.data(), web_port);
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
LogWrite(INIT__ERROR, 0, "Init", "Login Web Server failed to listen on %s:%u due to reason %s",
|
|
web_ipaddr.data(), web_port, e.what());
|
|
}
|
|
}
|
|
} |