/* 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // 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 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 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 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(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(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( 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()); } } }