1
0

clean /old/login/net

This commit is contained in:
Sky Johnson 2025-09-01 12:53:41 -05:00
parent 76fa77d590
commit ffb75045a4
7 changed files with 720 additions and 2088 deletions

View File

@ -1,88 +0,0 @@
package eq2net
// CRC16 calculates the CRC-16 checksum for EQ2 packets
// This matches the exact implementation from the original EQ2Emu
func CRC16(buf []byte, size int, key uint32) uint32 {
// CRC32 table used for EQ2's CRC16 calculation
intArray := [256]uint32{
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA, 0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988, 0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE, 0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC, 0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5,
0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172, 0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,
0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940, 0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116, 0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924, 0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,
0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A, 0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818, 0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,
0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E, 0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457,
0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C, 0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2, 0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB,
0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0, 0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086, 0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4, 0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,
0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A, 0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8, 0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE, 0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7,
0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC, 0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,
0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252, 0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60, 0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236, 0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F,
0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04, 0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A, 0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,
0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38, 0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21,
0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E, 0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C, 0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2, 0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB,
0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0, 0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6, 0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94, 0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D,
}
ecx := key
eax := ecx
eax = ^eax
eax &= 0xFF
eax = intArray[eax]
eax ^= 0x00FFFFFF
edx := ecx
edx = edx >> 8
edx = edx ^ eax
eax = eax >> 8
edx &= 0xFF
eax &= 0x00FFFFFF
eax ^= intArray[edx]
edx = ecx
edx = edx >> 0x10
edx ^= eax
eax = eax >> 8
edx &= 0xFF
esi := intArray[edx]
eax &= 0x00FFFFFF
eax ^= esi
ecx = ecx >> 0x18
ecx ^= eax
ecx &= 0xFF
esi = intArray[ecx]
eax = eax >> 8
eax &= 0x00FFFFFF
eax ^= esi
for x := range size {
edx := uint32(0)
edx = uint32(buf[x]) & 0x00FF
edx ^= eax
eax = eax >> 8
edx &= 0xFF
edi := intArray[edx]
eax &= 0x00FFFFFF
eax ^= edi
}
return ^eax
}
// CalculateCRC16 is a convenience wrapper
func CalculateCRC16(data []byte, key uint32) uint16 {
return uint16(CRC16(data, len(data), key))
}

View File

@ -4,278 +4,451 @@
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 <string.h>
#include <time.h>
#include <map>
#include <memory>
#include <random>
#include <signal.h>
#include <sstream>
#include <string>
#include <iostream>
#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 "net.h"
#include "client.h"
#include "LoginDatabase.h"
#include "LWorld.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"
#ifdef WIN32
#define snprintf _snprintf
#define vsnprintf _vsnprintf
#define strncasecmp _strnicmp
#define strcasecmp _stricmp
#include <conio.h>
#else
#include <stdlib.h>
#include "../common/unix.h"
#endif
EQStreamFactory eqsf(LoginStream);
map<int16,OpcodeManager*>EQOpcodeManager;
//TCPServer eqns(5999);
NetConnection net;
ClientList client_list;
LWorldList world_list;
LoginDatabase database;
ConfigReader configReader;
map<int16, int16> EQOpcodeVersions;
Timer statTimer(60000);
// 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();
#ifdef PUBLICLOGIN
char version[200], consoletitle[200];
#endif
#include "../common/timer.h"
/**
* @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;
#include "../common/CRC16.h"
#include <fstream>
// Get current terminal settings
tcgetattr(STDIN_FILENO, &oldt);
newt = oldt;
int main(int argc, char** argv){
#ifdef _DEBUG
_CrtSetDbgFlag( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
#endif
if (signal(SIGINT, CatchSignal) == SIG_ERR) {
cerr << "Could not set signal handler" << endl;
// 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;
}
LogStart();
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();
srand(time(NULL));
// Initialize random number generator with current time
std::random_device rd;
std::mt19937 gen(rd());
if(!net.ReadLoginConfig())
return 1;
net.InitWebServer(net.GetWebLoginAddress(), net.GetWebLoginPort(), net.GetWebCertFile(), net.GetWebKeyFile(), net.GetWebKeyPassword(), net.GetWebHardcodeUser(), net.GetWebHardcodePassword());
const char* structList[] = { "CommonStructs.xml", "LoginStructs.xml" };
for (int s = 0; s < sizeof(structList) / sizeof(const char*); s++)
// Read and validate login server configuration
if (!net.ReadLoginConfig())
{
LogWrite(INIT__INFO, 0, "Init", "Loading Structs File %s..", structList[s]);
if (configReader.processXML_Elements(structList[s]))
LogWrite(INIT__INFO, 0, "Init", "Loading Structs File %s completed..", structList[s]);
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!", structList[s]);
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();
if(eqsf.listen_ip_address)
LogWrite(INIT__INFO, 0, "Init", "Login server listening on %s port %i", eqsf.listen_ip_address, net.GetPort());
// 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());
/*}
else {
cout << "EQNetworkServer.Open() error" << endl;
return 1;
}*/
if (!eqsf.Open(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();
EQStream* eqs;
Timer* TimeoutTimer = new Timer(5000);
TimeoutTimer->Start();
while(RunLoops) {
// 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();
while ((eqs = eqsf.Pop())) {
// 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()));
Client* client = new Client(eqs);
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);
client_list.Add(client.release());
net.numclients++;
net.UpdateWindowTitle();
}
if(TimeoutTimer->Check()){
// Check for timed out connections
if (timeoutTimer->Check())
{
eqsf.CheckTimeout();
}
if(statTimer.Check()){
// 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();
#ifdef WIN32
if(kbhit())
// Handle console input (Linux version)
if (kbhit())
{
int hitkey = getch();
net.HitKey(hitkey);
}
#endif
Sleep(1);
}
//close
//eqns.Close();
eqsf.Close();
world_list.Shutdown();
return 0;
}
#ifdef WIN32
void NetConnection::HitKey(int keyhit)
{
switch(keyhit)
int hitkey = getchar();
switch (hitkey)
{
case 'l':
case 'L': {
case 'L':
{
// List all connected world servers
world_list.ListWorldsToConsole();
break;
}
case 'v':
case 'V':
{
printf("========Version Info=========\n");
printf("%s %s\n", EQ2EMU_MODULE, CURRENT_VERSION);
printf("Last Compiled on %s %s\n", COMPILE_DATE, COMPILE_TIME);
printf("=============================\n\n");
// 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': {
printf("===========Help=============\n");
printf("Available Commands:\n");
printf("l = Listing of World Servers\n");
printf("v = Login Version\n");
// printf("0 = Kick all connected world servers\n");
printf("============================\n\n");
break;
}
default:
printf("Invalid Command.\n");
break;
}
}
#endif
void CatchSignal(int sig_num) {
cout << "Got signal " << sig_num << endl;
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;
}
bool NetConnection::ReadLoginConfig() {
/**
* @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()) {
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)) {
if (!parser.convertStringToUnsignedShort(serverport, port_))
{
LogWrite(INIT__ERROR, 0, "Init", "Failed to translate loginconfig.serverport..");
return false;
}
if(serverip.size() > 0) {
// Set listening IP address if specified
if (!serverip.empty())
{
eqsf.listen_ip_address = new char[serverip.size() + 1];
strcpy(eqsf.listen_ip_address, serverip.c_str());
std::strcpy(eqsf.listen_ip_address, serverip.c_str());
}
else {
else
{
safe_delete(eqsf.listen_ip_address);
eqsf.listen_ip_address = nullptr;
}
// Parse account creation setting
std::string acctcreate_str = parser.getValue("loginconfig.accountcreation");
int16 allow_acct = 0;
std::uint16_t allow_acct = 0;
parser.convertStringToUnsignedShort(acctcreate_str, allow_acct);
allowAccountCreation = allow_acct > 0 ? true : false;
allow_account_creation_ = (allow_acct > 0);
// Parse expansion and feature flags
std::string expflag_str = parser.getValue("loginconfig.expansionflag");
parser.convertStringToUnsignedInt(expflag_str, expansionFlag);
parser.convertStringToUnsignedInt(expflag_str, expansion_flag_);
std::string citiesflag_str = parser.getValue("loginconfig.citiesflag");
parser.convertStringToUnsignedChar(citiesflag_str, citiesFlag);
parser.convertStringToUnsignedChar(citiesflag_str, cities_flag_);
std::string defaultsublevel_str = parser.getValue("loginconfig.defaultsubscriptionlevel");
parser.convertStringToUnsignedInt(defaultsublevel_str, defaultSubscriptionLevel);
parser.convertStringToUnsignedInt(defaultsublevel_str, default_subscription_level_);
std::string enableraces_str = parser.getValue("loginconfig.enabledraces");
parser.convertStringToUnsignedInt(enableraces_str, enabledRaces);
parser.convertStringToUnsignedInt(enableraces_str, enabled_races_);
web_loginaddress = parser.getValue("loginconfig.webloginaddress");
web_certfile = parser.getValue("loginconfig.webcertfile");
web_keyfile = parser.getValue("loginconfig.webkeyfile");
web_keypassword = parser.getValue("loginconfig.webkeypassword");
web_hardcodeuser = parser.getValue("loginconfig.webhardcodeuser");
web_hardcodepassword = parser.getValue("loginconfig.webhardcodepassword");
// 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_loginport);
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..");
//remove this when all database calls are using the new database class
if (!database.Init()) {
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();
map<int16,int16>::iterator version_itr2;
int16 version1 = 0;
for (version_itr2 = EQOpcodeVersions.begin(); version_itr2 != EQOpcodeVersions.end(); version_itr2++) {
version1 = version_itr2->first;
EQOpcodeManager[version1] = new RegularOpcodeManager();
map<string, uint16> eq = database.GetOpcodes(version1);
if(!EQOpcodeManager[version1]->LoadOpcodes(&eq)) {
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;
}
@ -284,80 +457,121 @@ bool NetConnection::ReadLoginConfig() {
return true;
}
void NetConnection::UpdateWindowTitle(char* iNewTitle) {
#ifdef WIN32
char tmp[500];
if (iNewTitle) {
snprintf(tmp, sizeof(tmp), "Login: %s", iNewTitle);
}
else {
snprintf(tmp, sizeof(tmp), "%s, Version: %s: %i Server(s), %i Client(s) Connected", EQ2EMU_MODULE, CURRENT_VERSION, net.numservers, net.numclients);
}
SetConsoleTitle(tmp);
#endif
/**
* @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()
{
#ifdef _WIN32
HANDLE console = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleTextAttribute(console, FOREGROUND_WHITE_BOLD);
#endif
printf("Module: %s, Version: %s", EQ2EMU_MODULE, CURRENT_VERSION);
#ifdef _WIN32
SetConsoleTextAttribute(console, FOREGROUND_YELLOW_BOLD);
#endif
printf("\n\nCopyright (C) 2007-2021 EQ2Emulator. https://www.eq2emu.com \n\n");
printf("EQ2Emulator is free software: you can redistribute it and/or modify\n");
printf("it under the terms of the GNU General Public License as published by\n");
printf("the Free Software Foundation, either version 3 of the License, or\n");
printf("(at your option) any later version.\n\n");
printf("EQ2Emulator is distributed in the hope that it will be useful,\n");
printf("but WITHOUT ANY WARRANTY; without even the implied warranty of\n");
printf("MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n");
printf("GNU General Public License for more details.\n\n");
#ifdef _WIN32
SetConsoleTextAttribute(console, FOREGROUND_GREEN_BOLD);
#endif
printf(" /$$$$$$$$ /$$$$$$ /$$$$$$ /$$$$$$$$ \n");
printf("| $$_____/ /$$__ $$ /$$__ $$| $$_____/ \n");
printf("| $$ | $$ \\ $$|__/ \\ $$| $$ /$$$$$$/$$$$ /$$ /$$\n");
printf("| $$$$$ | $$ | $$ /$$$$$$/| $$$$$ | $$_ $$_ $$| $$ | $$\n");
printf("| $$__/ | $$ | $$ /$$____/ | $$__/ | $$ \\ $$ \\ $$| $$ | $$\n");
printf("| $$ | $$/$$ $$| $$ | $$ | $$ | $$ | $$| $$ | $$\n");
printf("| $$$$$$$$| $$$$$$/| $$$$$$$$| $$$$$$$$| $$ | $$ | $$| $$$$$$/\n");
printf("|________/ \\____ $$$|________/|________/|__/ |__/ |__/ \\______/ \n");
printf(" \\__/ \n\n");
#ifdef _WIN32
SetConsoleTextAttribute(console, FOREGROUND_MAGENTA_BOLD);
#endif
printf(" Website : https://eq2emu.com \n");
printf(" Wiki : https://wiki.eq2emu.com \n");
printf(" Git : https://git.eq2emu.com \n");
printf(" Discord : https://discord.gg/5Cavm9NYQf \n\n");
#ifdef _WIN32
SetConsoleTextAttribute(console, FOREGROUND_WHITE_BOLD);
#endif
printf("For more detailed logging, modify 'Level' param the log_config.xml file.\n\n");
#ifdef _WIN32
SetConsoleTextAttribute(console, FOREGROUND_WHITE);
#endif
// 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";
fflush(stdout);
// 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();
}
void NetConnection::InitWebServer(std::string web_ipaddr, int16 web_port, std::string cert_file, std::string key_file, std::string key_password, std::string hardcode_user, std::string hardcode_password) {
if(web_ipaddr.size() > 0 && web_port > 0) {
try {
login_webserver = new WebServer(web_ipaddr, web_port, cert_file, key_file, key_password, hardcode_user, hardcode_password);
/**
* @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)
);
login_webserver->register_route("/status", NetConnection::Web_loginhandle_status);
login_webserver->register_route("/worlds", NetConnection::Web_loginhandle_worlds);
login_webserver->run();
LogWrite(INIT__INFO, 0, "Init", "Login Web Server is listening on %s:%u..", web_ipaddr.c_str(), web_port);
// 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.c_str(), web_port, e.what());
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());
}
}
}

View File

@ -4,144 +4,208 @@
This file is part of EQ2Emulator.
*/
#ifdef WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <winsock.h>
#else
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#endif
//#include <netdb.h>
#include <errno.h>
#include <fcntl.h>
#pragma once
// Linux network headers
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
// Standard library includes
#include <atomic>
#include <cstdint>
#include <string>
#include <string_view>
#include <memory>
// Project includes
#include "../common/types.h"
#include "../common/Web/WebServer.h"
#include "../common/MiscFunctions.h"
void CatchSignal(int sig_num);
enum eServerMode { Standalone, Master, Slave, Mesh };
// Forward declarations
class LWorld;
/**
* @brief Signal handler for graceful shutdown
* @param sig_num Signal number received
*/
void CatchSignal(int sig_num);
/**
* @brief Server operation modes
*/
enum class ServerMode : std::uint8_t
{
Standalone, // Server operates independently
Master, // Server acts as master node
Slave, // Server acts as slave node
Mesh // Server participates in mesh network
};
/**
* @brief Main network connection manager for the login server
*
* This class handles all network operations for the login server including:
* - Client connections and authentication
* - World server connections
* - Configuration management
* - Web server integration
*/
class NetConnection
{
public:
NetConnection() {
port = 5999;
listening_socket = 0;
memset(masteraddress, 0, sizeof(masteraddress));
uplinkport = 0;
memset(uplinkaccount, 0, sizeof(uplinkaccount));
memset(uplinkpassword, 0, sizeof(uplinkpassword));
LoginMode = Standalone;
Uplink_WrongVersion = false;
numclients = 0;
numservers = 0;
allowAccountCreation = true;
// full support = 0x7CFF
// 1 << 12 (-4096) = missing echoes of faydwer, disables Fae and Arasai (black portraits) and kelethin as starting city
// 1 << 13 (-8192) = disables sarnak (black portraits) and gorowyn as starting city
expansionFlag = 0x7CFF; // 0x4CF5
/* dword_1ECBA18 operand for race flag packs (sublevel 0,1,2?) -- (sublevel -1) controls starting zones omission 0xEE vs 0xCF (CF misses halas)
1 = city of qeynos
2 = city of freeport
4 = city of kelethin
8 = city of neriak
16 = gorowyn
32 = new halas
64 = queens colony
128 = outpost overlord
/**
* @brief Default constructor - initializes all network settings to defaults
*/
citiesFlag = 0xFF;
NetConnection() noexcept;
// sub_level 0xFFFFFFFF = blacks out all portraits for class alignments, considered non membership
// sub_level > 0 = class alignments still required, but portraits are viewable and race selectable
// sub_level = 2 membership, you can 'create characters on time locked servers' vs standard
// sub_level = 0 forces popup on close to web browser
defaultSubscriptionLevel = 0xFFFFFFFF;
/**
* @brief Destructor - ensures proper cleanup of network resources
*/
~NetConnection();
// disable extra races FAE(16) ARASAI (17) SARNAK (18) -- with 4096/8192 flags, no visibility of portraits
enabledRaces = 0xFFFF; // 0xCFFF
// Disable copy operations (network connection should be unique)
NetConnection(const NetConnection&) = delete;
NetConnection& operator=(const NetConnection&) = delete;
web_loginport = 0;
// Enable move operations for potential future use
NetConnection(NetConnection&&) = default;
NetConnection& operator=(NetConnection&&) = default;
login_webserver = nullptr;
/**
* @brief Updates the console window title with server statistics
* @param new_title Optional custom title (nullptr uses default format)
* Note: This is a no-op on Linux but kept for API compatibility
*/
void UpdateWindowTitle(const char* new_title = nullptr);
login_running = false;
login_uptime = getCurrentTimestamp();
}
~NetConnection() {
safe_delete(login_webserver);
}
void UpdateWindowTitle(char* iNewTitle = 0);
/**
* @brief Initializes the network connection
* @return true if initialization successful, false otherwise
*/
bool Init();
/**
* @brief Listens for new client connections
*/
void ListenNewClients();
void HitKey(int keyhit);
char address[1024];
int32 numclients;
int32 numservers;
int16 GetPort() { return port; }
void SetPort(int16 in_port) { port = in_port; }
eServerMode GetLoginMode() { return LoginMode; }
/**
* @brief Reads configuration from the login config file
* @return true if config loaded successfully, false otherwise
*/
bool ReadLoginConfig();
char* GetMasterAddress() { return masteraddress; }
int16 GetUplinkPort() { if (uplinkport != 0) return uplinkport; else return port; }
char* GetUplinkAccount() { return uplinkaccount; }
char* GetUplinkPassword() { return uplinkpassword; }
bool IsAllowingAccountCreation() { return allowAccountCreation; }
int32 GetExpansionFlag() { return expansionFlag; }
int8 GetCitiesFlag() { return citiesFlag; }
int32 GetDefaultSubscriptionLevel() { return defaultSubscriptionLevel; }
int32 GetEnabledRaces() { return enabledRaces; }
std::string GetWebLoginAddress() { return web_loginaddress; }
inline int16 GetWebLoginPort() { return web_loginport; }
std::string GetWebCertFile() { return web_certfile; }
std::string GetWebKeyFile() { return web_keyfile; }
std::string GetWebKeyPassword() { return web_keypassword; }
std::string GetWebHardcodeUser() { return web_hardcodeuser; }
std::string GetWebHardcodePassword() { return web_hardcodepassword; }
/**
* @brief Displays the welcome header with version and copyright info
*/
void WelcomeHeader();
void InitWebServer(std::string web_ipaddr, int16 web_port, std::string cert_file, std::string key_file, std::string key_password, std::string hardcode_user, std::string hardcode_password);
/**
* @brief Initializes the web server for HTTP/HTTPS API access
* @param web_ipaddr IP address to bind the web server to
* @param web_port Port number for the web server
* @param cert_file Path to SSL certificate file
* @param key_file Path to SSL key file
* @param key_password Password for the SSL key
* @param hardcode_user Hardcoded username for basic auth
* @param hardcode_password Hardcoded password for basic auth
*/
void 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);
static void Web_loginhandle_status(const http::request<http::string_body>& req, http::response<http::string_body>& res);
static void Web_loginhandle_worlds(const http::request<http::string_body>& req, http::response<http::string_body>& res);
// Static web server request handlers
/**
* @brief Handles /status endpoint requests
* @param req HTTP request object
* @param res HTTP response object to populate
*/
static void Web_loginhandle_status(const http::request<http::string_body>& req,
http::response<http::string_body>& res);
/**
* @brief Handles /worlds endpoint requests
* @param req HTTP request object
* @param res HTTP response object to populate
*/
static void Web_loginhandle_worlds(const http::request<http::string_body>& req,
http::response<http::string_body>& res);
// Getter methods for configuration values
[[nodiscard]] std::uint16_t GetPort() const noexcept { return port_; }
[[nodiscard]] ServerMode GetLoginMode() const noexcept { return login_mode_; }
[[nodiscard]] const char* GetMasterAddress() const noexcept { return master_address_.data(); }
[[nodiscard]] std::uint16_t GetUplinkPort() const noexcept
{
return (uplink_port_ != 0) ? uplink_port_ : port_;
}
[[nodiscard]] const char* GetUplinkAccount() const noexcept { return uplink_account_.data(); }
[[nodiscard]] const char* GetUplinkPassword() const noexcept { return uplink_password_.data(); }
[[nodiscard]] bool IsAllowingAccountCreation() const noexcept { return allow_account_creation_; }
[[nodiscard]] std::uint32_t GetExpansionFlag() const noexcept { return expansion_flag_; }
[[nodiscard]] std::uint8_t GetCitiesFlag() const noexcept { return cities_flag_; }
[[nodiscard]] std::uint32_t GetDefaultSubscriptionLevel() const noexcept { return default_subscription_level_; }
[[nodiscard]] std::uint32_t GetEnabledRaces() const noexcept { return enabled_races_; }
[[nodiscard]] const std::string& GetWebLoginAddress() const noexcept { return web_login_address_; }
[[nodiscard]] std::uint16_t GetWebLoginPort() const noexcept { return web_login_port_; }
[[nodiscard]] const std::string& GetWebCertFile() const noexcept { return web_cert_file_; }
[[nodiscard]] const std::string& GetWebKeyFile() const noexcept { return web_key_file_; }
[[nodiscard]] const std::string& GetWebKeyPassword() const noexcept { return web_key_password_; }
[[nodiscard]] const std::string& GetWebHardcodeUser() const noexcept { return web_hardcode_user_; }
[[nodiscard]] const std::string& GetWebHardcodePassword() const noexcept { return web_hardcode_password_; }
// Setter methods for configuration values
void SetPort(std::uint16_t port) noexcept { port_ = port; }
// Public state variables
char address[1024]{}; // Server address buffer
std::atomic<std::int32_t> numclients{0}; // Current number of connected clients
std::atomic<std::int32_t> numservers{0}; // Current number of connected world servers
std::atomic<bool> login_running{false}; // Flag indicating if login server is running
std::atomic<std::int64_t> login_uptime{0}; // Timestamp when server started
bool login_running;
std::atomic<int64> login_uptime;
protected:
friend class LWorld;
bool Uplink_WrongVersion;
friend class LWorld; // Allow LWorld access to protected members
/**
* @brief Flag indicating uplink version mismatch
*/
bool uplink_wrong_version_{false};
private:
int16 port;
int listening_socket;
char masteraddress[300];
int16 uplinkport;
char uplinkaccount[300];
char uplinkpassword[300];
eServerMode LoginMode;
bool allowAccountCreation;
int32 expansionFlag;
int8 citiesFlag;
int32 defaultSubscriptionLevel;
int32 enabledRaces;
std::string web_loginaddress;
std::string web_certfile;
std::string web_keyfile;
std::string web_keypassword;
std::string web_hardcodeuser;
std::string web_hardcodepassword;
int16 web_loginport;
WebServer* login_webserver;
// Network configuration
std::uint16_t port_{5999}; // Server listening port
int listening_socket_{0}; // Main listening socket descriptor
// Master server configuration (for non-standalone modes)
std::array<char, 300> master_address_{}; // Master server address
std::uint16_t uplink_port_{0}; // Port for uplink connection
std::array<char, 300> uplink_account_{}; // Account for uplink authentication
std::array<char, 300> uplink_password_{}; // Password for uplink authentication
ServerMode login_mode_{ServerMode::Standalone}; // Current server operation mode
// Account and expansion settings
bool allow_account_creation_{true}; // Allow new account creation
std::uint32_t expansion_flag_{0x7CFF}; // Enabled expansions bitfield
std::uint8_t cities_flag_{0xFF}; // Enabled starting cities bitfield
std::uint32_t default_subscription_level_{0xFFFFFFFF}; // Default subscription level for new accounts
std::uint32_t enabled_races_{0xFFFF}; // Enabled races bitfield
// Web server configuration
std::string web_login_address_; // Web server bind address
std::string web_cert_file_; // SSL certificate file path
std::string web_key_file_; // SSL key file path
std::string web_key_password_; // SSL key password
std::string web_hardcode_user_; // Basic auth username
std::string web_hardcode_password_; // Basic auth password
std::uint16_t web_login_port_{0}; // Web server port
std::unique_ptr<WebServer> login_webserver_; // Web server instance
};

View File

@ -1,56 +0,0 @@
package eq2net
// Protocol opcodes for UDP packet types
const (
OPSessionRequest uint8 = 0x01
OPSessionResponse uint8 = 0x02
OPCombined uint8 = 0x03
OPSessionDisconnect uint8 = 0x05
OPKeepAlive uint8 = 0x06
OPServerKeyRequest uint8 = 0x07
OPSessionStatResponse uint8 = 0x08
OPPacket uint8 = 0x09
OPFragment uint8 = 0x0d
OPOutOfOrderAck uint8 = 0x11
OPAck uint8 = 0x15
OPAppCombined uint8 = 0x19
OPOutOfSession uint8 = 0x1d
)
// Application opcodes
type EmuOpcode uint16
const (
OPUnknown EmuOpcode = iota
OPLoginReplyMsg
OPLoginByNumRequestMsg
OPWSLoginRequestMsg
OPESInitMsg
OPESReadyForClientsMsg
OPCreateZoneInstanceMsg
OPZoneInstanceCreateReplyMsg
OPZoneInstanceDestroyedMsg
OPExpectClientAsCharacterRequest
OPExpectClientAsCharacterReplyMs
OPZoneInfoMsg
OPCreateCharacterRequestMsg
OPDoneLoadingZoneResourcesMsg
OPDoneSendingInitialEntitiesMsg
OPDoneLoadingEntityResourcesMsg
OPDoneLoadingUIResourcesMsg
OPPredictionUpdateMsg
OPRemoteCmdMsg
OPSetRemoteCmdsMsg
OPGameWorldTimeMsg
OPMOTDMsg
OPZoneMOTDMsg
OPGuildRecruitingMemberInfo
OPGuildRecruiting
OPGuildRecruitingDetails
OPGuildRecruitingImage
OPAvatarCreatedMsg
OPAvatarDestroyedMsg
OPRequestCampMsg
OPMapRequest
OPCampStartedMsg
)

356
packet.go
View File

@ -1,356 +0,0 @@
package eq2net
import (
"bytes"
"compress/zlib"
"encoding/binary"
"fmt"
"net"
"time"
"unsafe"
)
// Packet represents a base packet structure
type Packet struct {
Buffer []byte
Size uint32
SrcIP net.IP
SrcPort uint16
DstIP net.IP
DstPort uint16
Priority uint32
Timestamp time.Time
Version int16
Opcode uint16
}
// NewPacket creates a new packet
func NewPacket(op uint16, buf []byte) *Packet {
p := &Packet{
Opcode: op,
Size: uint32(len(buf)),
Timestamp: time.Now(),
}
if buf != nil {
p.Buffer = make([]byte, len(buf))
copy(p.Buffer, buf)
}
return p
}
// SetSrcInfo sets source IP and port
func (p *Packet) SetSrcInfo(ip net.IP, port uint16) {
p.SrcIP = ip
p.SrcPort = port
}
// SetDstInfo sets destination IP and port
func (p *Packet) SetDstInfo(ip net.IP, port uint16) {
p.DstIP = ip
p.DstPort = port
}
// GetRawOpcode returns the raw opcode
func (p *Packet) GetRawOpcode() uint16 {
return p.Opcode
}
// ProtocolPacket represents a protocol-level packet
type ProtocolPacket struct {
*Packet
EQ2Compressed bool
PacketPrepared bool
PacketEncrypted bool
Acked bool
SentTime int32
AttemptCount int8
Sequence uint16
}
// NewProtocolPacket creates a new protocol packet from raw data
func NewProtocolPacket(buf []byte, opcode int) *ProtocolPacket {
if len(buf) < 2 {
return nil
}
var op uint16
if opcode >= 0 {
op = uint16(opcode)
} else {
op = binary.BigEndian.Uint16(buf[:2])
buf = buf[2:]
}
pp := &ProtocolPacket{
Packet: NewPacket(op, buf),
}
return pp
}
// NewProtocolPacketWithOp creates a new protocol packet with specific opcode
func NewProtocolPacketWithOp(op uint16, buf []byte) *ProtocolPacket {
return &ProtocolPacket{
Packet: NewPacket(op, buf),
}
}
// ValidateCRC validates the CRC of a packet
func ValidateCRC(buffer []byte, length int, key uint32) bool {
// SessionRequest, SessionResponse, OutOfSession are not CRC'd
if len(buffer) >= 2 && buffer[0] == 0x00 &&
(buffer[1] == OPSessionRequest || buffer[1] == OPSessionResponse || buffer[1] == OPOutOfSession) {
return true
}
// Check for special case
if len(buffer) >= 4 && buffer[2] == 0x00 && buffer[3] == 0x19 {
return true
}
// Calculate and compare CRC
if length < 2 {
return false
}
compCRC := CalculateCRC16(buffer[:length-2], key)
packetCRC := binary.BigEndian.Uint16(buffer[length-2:])
return packetCRC == 0 || compCRC == packetCRC
}
// Serialize serializes the protocol packet
func (p *ProtocolPacket) Serialize(offset int8) []byte {
// Calculate size including opcode
totalSize := len(p.Buffer) + 2
if offset != 0 {
totalSize += int(offset)
}
result := make([]byte, totalSize)
pos := 0
// Add offset if needed
if offset != 0 {
pos = int(offset)
}
// Write opcode
binary.BigEndian.PutUint16(result[pos:], p.Opcode)
pos += 2
// Copy buffer
if p.Buffer != nil {
copy(result[pos:], p.Buffer)
}
return result
}
// Copy creates a copy of the protocol packet
func (p *ProtocolPacket) Copy() *ProtocolPacket {
newPacket := &ProtocolPacket{
Packet: NewPacket(p.Opcode, p.Buffer),
EQ2Compressed: p.EQ2Compressed,
PacketPrepared: p.PacketPrepared,
PacketEncrypted: p.PacketEncrypted,
Acked: p.Acked,
SentTime: p.SentTime,
AttemptCount: p.AttemptCount,
Sequence: p.Sequence,
}
newPacket.Version = p.Version
newPacket.Priority = p.Priority
return newPacket
}
// Combine combines two protocol packets
func (p *ProtocolPacket) Combine(rhs *ProtocolPacket) bool {
if rhs.Opcode != p.Opcode {
return false
}
// Create new buffer with combined size
newSize := len(p.Buffer) + len(rhs.Buffer)
newBuffer := make([]byte, newSize)
copy(newBuffer, p.Buffer)
copy(newBuffer[len(p.Buffer):], rhs.Buffer)
p.Buffer = newBuffer
p.Size = uint32(newSize)
return true
}
// IsProtocolPacket checks if a buffer contains a protocol packet
func IsProtocolPacket(inBuff []byte, length uint32, trimCRC bool) bool {
if length < 2 {
return false
}
opcode := binary.BigEndian.Uint16(inBuff)
switch uint8(opcode & 0xFF) {
case OPSessionRequest, OPSessionResponse, OPCombined,
OPSessionDisconnect, OPKeepAlive, OPServerKeyRequest,
OPSessionStatResponse, OPPacket, OPFragment,
OPOutOfOrderAck, OPAck, OPAppCombined, OPOutOfSession:
return true
}
return false
}
// Decompress decompresses a buffer
func Decompress(buffer []byte, length uint32, newbuf []byte, newbufsize uint32) (uint32, error) {
reader := bytes.NewReader(buffer[:length])
zlibReader, err := zlib.NewReader(reader)
if err != nil {
return 0, err
}
defer zlibReader.Close()
buf := bytes.NewBuffer(newbuf[:0])
n, err := buf.ReadFrom(zlibReader)
if err != nil {
return 0, err
}
if uint32(n) > newbufsize {
return 0, fmt.Errorf("decompressed size exceeds buffer size")
}
return uint32(n), nil
}
// Compress compresses a buffer
func Compress(buffer []byte, length uint32, newbuf []byte, newbufsize uint32) (uint32, error) {
var buf bytes.Buffer
writer := zlib.NewWriter(&buf)
_, err := writer.Write(buffer[:length])
if err != nil {
return 0, err
}
err = writer.Close()
if err != nil {
return 0, err
}
compressed := buf.Bytes()
if uint32(len(compressed)) > newbufsize {
return 0, fmt.Errorf("compressed size exceeds buffer size")
}
copy(newbuf, compressed)
return uint32(len(compressed)), nil
}
// ChatDecode decodes chat data
func ChatDecode(buffer []byte, size int, decodeKey int) {
for i := 0; i+4 <= size; i++ {
c := buffer[i]
buffer[i] = buffer[i+2]
buffer[i+2] = c
c = buffer[i+1]
buffer[i+1] = buffer[i+3]
buffer[i+3] = c
key := uint32(decodeKey)
p := (*uint32)(unsafe.Pointer(&buffer[i]))
*p = (*p) ^ key
i += 3
}
}
// ChatEncode encodes chat data
func ChatEncode(buffer []byte, size int, encodeKey int) {
for i := 0; i+4 <= size; i++ {
key := uint32(encodeKey)
p := (*uint32)(unsafe.Pointer(&buffer[i]))
*p = (*p) ^ key
c := buffer[i]
buffer[i] = buffer[i+2]
buffer[i+2] = c
c = buffer[i+1]
buffer[i+1] = buffer[i+3]
buffer[i+3] = c
i += 3
}
}
// ApplicationPacket represents an application-level packet
type ApplicationPacket struct {
*Packet
EmuOpcode EmuOpcode
AppOpcodeSize uint8
}
// NewApplicationPacket creates a new application packet
func NewApplicationPacket(op EmuOpcode, buf []byte) *ApplicationPacket {
ap := &ApplicationPacket{
Packet: NewPacket(0, buf),
EmuOpcode: op,
AppOpcodeSize: 2, // Default for world/zone
}
return ap
}
// SetOpcode sets the opcode
func (p *ApplicationPacket) SetOpcode(op EmuOpcode) {
p.EmuOpcode = op
p.Opcode = uint16(op)
}
// GetOpcode returns the opcode
func (p *ApplicationPacket) GetOpcode() EmuOpcode {
return p.EmuOpcode
}
// Copy creates a copy of the application packet
func (p *ApplicationPacket) Copy() *ApplicationPacket {
newPacket := &ApplicationPacket{
Packet: NewPacket(p.Opcode, p.Buffer),
EmuOpcode: p.EmuOpcode,
AppOpcodeSize: p.AppOpcodeSize,
}
newPacket.Version = p.Version
return newPacket
}
// Serialize serializes the application packet
func (p *ApplicationPacket) Serialize() []byte {
size := len(p.Buffer) + int(p.AppOpcodeSize)
result := make([]byte, size)
// Write opcode based on size
if p.AppOpcodeSize == 1 {
result[0] = uint8(p.Opcode)
copy(result[1:], p.Buffer)
} else {
binary.LittleEndian.PutUint16(result, p.Opcode)
copy(result[2:], p.Buffer)
}
return result
}
// Combine combines two application packets
func (p *ApplicationPacket) Combine(rhs *ApplicationPacket) bool {
if rhs.EmuOpcode != p.EmuOpcode {
return false
}
newSize := len(p.Buffer) + len(rhs.Buffer)
newBuffer := make([]byte, newSize)
copy(newBuffer, p.Buffer)
copy(newBuffer[len(p.Buffer):], rhs.Buffer)
p.Buffer = newBuffer
p.Size = uint32(newSize)
return true
}

675
stream.go
View File

@ -1,675 +0,0 @@
package eq2net
import (
"encoding/binary"
"fmt"
"net"
"sync"
"time"
)
// StreamState represents the connection state
type StreamState int
const (
Established StreamState = iota
WaitClose
Closing
Disconnecting
Closed
)
// StreamType represents different stream types
type StreamType int
const (
UnknownStream StreamType = iota
LoginStream
WorldStream
ZoneStream
ChatOrMailStream
ChatStream
MailStream
EQ2Stream
)
// SessionRequest packet structure
type SessionRequest struct {
UnknownA uint32
Session uint32
MaxLength uint32
}
// SessionResponse packet structure
type SessionResponse struct {
Session uint32
Key uint32
UnknownA uint8
Format uint8
UnknownB uint8
MaxLength uint32
UnknownD uint32
}
// ClientSessionStats for tracking connection statistics
type ClientSessionStats struct {
RequestID uint16
LastLocalDelta uint32
AverageDelta uint32
LowDelta uint32
HighDelta uint32
LastRemoteDelta uint32
PacketsSent uint64
PacketsReceived uint64
}
// ServerSessionStats for server-side statistics
type ServerSessionStats struct {
RequestID uint16
CurrentTime uint32
Unknown1 uint32
ReceivedPackets uint32
Unknown2 uint32
SentPackets uint32
Unknown3 uint32
SentPackets2 uint32
Unknown4 uint32
ReceivedPackets2 uint32
}
// Stream represents a connection stream
type Stream struct {
mu sync.RWMutex
// Connection info
remoteAddr *net.UDPAddr
remoteIP net.IP
remotePort uint16
localAddr *net.UDPAddr
sessionID uint32
key uint32
state StreamState
streamType StreamType
// Buffers
buffer [8192]byte
oversizeBuffer []byte
oversizeOffset uint32
oversizeLength uint32
rogueBuffer []byte
rogueBufOffset uint32
rogueBufSize uint32
// Packet tracking
receivedPackets uint32
sentPackets uint32
nextInSeq uint16
nextOutSeq uint16
lastAckSent uint16
lastAckReceived uint16
// Timing
lastReceiveTime time.Time
lastSendTime time.Time
avgRoundTrip time.Duration
minRoundTrip time.Duration
maxRoundTrip time.Duration
retransmitTimeout time.Duration
// Queues
incomingQueue []*ProtocolPacket
outgoingQueue []*ProtocolPacket
sentQueue map[uint16]*ProtocolPacket
futurePackets map[uint16]*ProtocolPacket
// Options
compressed bool
encoded bool
maxLength uint32
opcodeSize uint8
// Callbacks
onPacket func(*ApplicationPacket)
onDisconnect func()
}
// NewStream creates a new stream
func NewStream(remoteAddr *net.UDPAddr, streamType StreamType) *Stream {
s := &Stream{
remoteAddr: remoteAddr,
remoteIP: remoteAddr.IP,
remotePort: uint16(remoteAddr.Port),
streamType: streamType,
state: Established,
sentQueue: make(map[uint16]*ProtocolPacket),
futurePackets: make(map[uint16]*ProtocolPacket),
lastReceiveTime: time.Now(),
lastSendTime: time.Now(),
retransmitTimeout: 500 * time.Millisecond,
maxLength: 512,
opcodeSize: 2,
}
// Set opcode size based on stream type
if streamType == LoginStream || streamType == ChatStream || streamType == MailStream {
s.opcodeSize = 1
}
return s
}
// SetSessionInfo sets the session ID and key
func (s *Stream) SetSessionInfo(sessionID uint32, key uint32) {
s.mu.Lock()
defer s.mu.Unlock()
s.sessionID = sessionID
s.key = key
}
// GetSessionID returns the session ID
func (s *Stream) GetSessionID() uint32 {
s.mu.RLock()
defer s.mu.RUnlock()
return s.sessionID
}
// GetKey returns the session key
func (s *Stream) GetKey() uint32 {
s.mu.RLock()
defer s.mu.RUnlock()
return s.key
}
// GetState returns the current stream state
func (s *Stream) GetState() StreamState {
s.mu.RLock()
defer s.mu.RUnlock()
return s.state
}
// SetState sets the stream state
func (s *Stream) SetState(state StreamState) {
s.mu.Lock()
defer s.mu.Unlock()
s.state = state
}
// GetRemoteAddr returns the remote address
func (s *Stream) GetRemoteAddr() *net.UDPAddr {
return s.remoteAddr
}
// Process processes an incoming packet
func (s *Stream) Process(data []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
s.lastReceiveTime = time.Now()
s.receivedPackets++
// Parse the packet
if len(data) < 2 {
return fmt.Errorf("packet too small")
}
opcode := binary.BigEndian.Uint16(data)
// Handle different packet types
switch uint8(opcode & 0xFF) {
case OPSessionRequest:
return s.handleSessionRequest(data)
case OPSessionResponse:
return s.handleSessionResponse(data)
case OPKeepAlive:
return s.handleKeepAlive(data)
case OPPacket:
return s.handlePacket(data)
case OPFragment:
return s.handleFragment(data)
case OPAck:
return s.handleAck(data)
case OPOutOfOrderAck:
return s.handleOutOfOrderAck(data)
case OPSessionDisconnect:
return s.handleDisconnect(data)
case OPCombined:
return s.handleCombined(data)
case OPAppCombined:
return s.handleAppCombined(data)
default:
return fmt.Errorf("unknown opcode: %02x", opcode&0xFF)
}
}
// handleSessionRequest handles session request packets
func (s *Stream) handleSessionRequest(data []byte) error {
if len(data) < 14 { // 2 byte opcode + 12 byte SessionRequest
return fmt.Errorf("session request too small")
}
req := &SessionRequest{}
req.UnknownA = binary.LittleEndian.Uint32(data[2:6])
req.Session = binary.LittleEndian.Uint32(data[6:10])
req.MaxLength = binary.LittleEndian.Uint32(data[10:14])
s.sessionID = req.Session
s.maxLength = req.MaxLength
// Send session response
return s.sendSessionResponse()
}
// handleSessionResponse handles session response packets
func (s *Stream) handleSessionResponse(data []byte) error {
if len(data) < 21 { // 2 byte opcode + 19 byte SessionResponse
return fmt.Errorf("session response too small")
}
resp := &SessionResponse{}
resp.Session = binary.LittleEndian.Uint32(data[2:6])
resp.Key = binary.LittleEndian.Uint32(data[6:10])
resp.UnknownA = data[10]
resp.Format = data[11]
resp.UnknownB = data[12]
resp.MaxLength = binary.LittleEndian.Uint32(data[13:17])
resp.UnknownD = binary.LittleEndian.Uint32(data[17:21])
s.sessionID = resp.Session
s.key = resp.Key
s.maxLength = resp.MaxLength
s.compressed = (resp.Format & 0x01) != 0
s.encoded = (resp.Format & 0x04) != 0
return nil
}
// handleKeepAlive handles keep-alive packets
func (s *Stream) handleKeepAlive(data []byte) error {
// Send keep-alive response
return s.sendKeepAliveResponse()
}
// handlePacket handles regular data packets
func (s *Stream) handlePacket(data []byte) error {
if len(data) < 4 {
return fmt.Errorf("packet too small")
}
// Extract sequence number
sequence := binary.BigEndian.Uint16(data[2:4])
// Validate CRC
if !ValidateCRC(data, len(data), s.key) {
return fmt.Errorf("CRC validation failed")
}
// Create protocol packet
packet := NewProtocolPacket(data[4:len(data)-2], -1) // Skip header and CRC
packet.Sequence = sequence
// Handle sequence
if sequence == s.nextInSeq {
// In order packet
s.processInOrderPacket(packet)
s.nextInSeq++
// Check for any future packets that are now in order
s.processFuturePackets()
// Send ACK
s.sendAck(sequence)
} else if sequence > s.nextInSeq {
// Future packet - store it
s.futurePackets[sequence] = packet
// Send out of order ACK
s.sendOutOfOrderAck(sequence)
}
// Ignore past packets (already processed)
return nil
}
// handleFragment handles fragmented packets
func (s *Stream) handleFragment(data []byte) error {
if len(data) < 8 {
return fmt.Errorf("fragment too small")
}
sequence := binary.BigEndian.Uint16(data[2:4])
totalSize := binary.BigEndian.Uint32(data[4:8])
// Validate CRC
if !ValidateCRC(data, len(data), s.key) {
return fmt.Errorf("CRC validation failed")
}
// Initialize oversize buffer if needed
if s.oversizeBuffer == nil || s.oversizeLength != totalSize {
s.oversizeBuffer = make([]byte, totalSize)
s.oversizeOffset = 0
s.oversizeLength = totalSize
}
// Copy fragment data
fragmentData := data[8 : len(data)-2] // Skip header and CRC
copy(s.oversizeBuffer[s.oversizeOffset:], fragmentData)
s.oversizeOffset += uint32(len(fragmentData))
// Send ACK
s.sendAck(sequence)
s.nextInSeq = sequence + 1
// Check if we have the complete packet
if s.oversizeOffset >= s.oversizeLength {
// Process the complete packet
packet := NewProtocolPacket(s.oversizeBuffer[:s.oversizeLength], -1)
s.processInOrderPacket(packet)
// Reset oversize buffer
s.oversizeBuffer = nil
s.oversizeOffset = 0
s.oversizeLength = 0
}
return nil
}
// handleAck handles acknowledgment packets
func (s *Stream) handleAck(data []byte) error {
if len(data) < 4 {
return fmt.Errorf("ack too small")
}
sequence := binary.BigEndian.Uint16(data[2:4])
// Remove acknowledged packet from sent queue
if packet, ok := s.sentQueue[sequence]; ok {
packet.Acked = true
delete(s.sentQueue, sequence)
// Update RTT based on this ACK
if packet.SentTime > 0 {
rtt := time.Since(time.Unix(int64(packet.SentTime), 0))
s.updateRTT(rtt)
}
}
s.lastAckReceived = sequence
return nil
}
// handleOutOfOrderAck handles out-of-order acknowledgments
func (s *Stream) handleOutOfOrderAck(data []byte) error {
if len(data) < 4 {
return fmt.Errorf("out of order ack too small")
}
sequence := binary.BigEndian.Uint16(data[2:4])
// Resend packets between lastAckReceived and sequence
for seq := s.lastAckReceived + 1; seq < sequence; seq++ {
if packet, ok := s.sentQueue[seq]; ok {
s.resendPacket(packet)
}
}
return nil
}
// handleDisconnect handles disconnect packets
func (s *Stream) handleDisconnect(data []byte) error {
s.state = Disconnecting
// Call disconnect callback
if s.onDisconnect != nil {
s.onDisconnect()
}
return nil
}
// handleCombined handles combined packets
func (s *Stream) handleCombined(data []byte) error {
if len(data) < 3 {
return fmt.Errorf("combined packet too small")
}
offset := 2 // Skip opcode
for offset < len(data)-2 { // Leave room for CRC
if offset+1 >= len(data) {
break
}
// Read sub-packet length
length := uint16(data[offset])
offset++
if length == 0xFF {
// Extended length
if offset+2 >= len(data) {
break
}
length = binary.BigEndian.Uint16(data[offset:])
offset += 2
}
// Extract sub-packet
if offset+int(length) > len(data) {
break
}
subPacket := data[offset : offset+int(length)]
offset += int(length)
// Process sub-packet
s.Process(subPacket)
}
return nil
}
// handleAppCombined handles application-level combined packets
func (s *Stream) handleAppCombined(data []byte) error {
// Similar to handleCombined but for application packets
return s.handleCombined(data)
}
// processInOrderPacket processes an in-order packet
func (s *Stream) processInOrderPacket(packet *ProtocolPacket) {
// Decompress if needed
if s.compressed && packet.EQ2Compressed {
decompressed := make([]byte, 8192)
size, err := Decompress(packet.Buffer, uint32(len(packet.Buffer)), decompressed, 8192)
if err == nil {
packet.Buffer = decompressed[:size]
packet.Size = size
}
}
// Decode if needed
if s.encoded && packet.PacketEncrypted {
ChatDecode(packet.Buffer, len(packet.Buffer), int(s.key))
packet.PacketEncrypted = false
}
// Convert to application packet and call callback
if s.onPacket != nil {
appPacket := s.makeApplicationPacket(packet)
if appPacket != nil {
s.onPacket(appPacket)
}
}
}
// processFuturePackets processes any future packets that are now in order
func (s *Stream) processFuturePackets() {
for {
if packet, ok := s.futurePackets[s.nextInSeq]; ok {
s.processInOrderPacket(packet)
delete(s.futurePackets, s.nextInSeq)
s.nextInSeq++
} else {
break
}
}
}
// makeApplicationPacket converts a protocol packet to an application packet
func (s *Stream) makeApplicationPacket(packet *ProtocolPacket) *ApplicationPacket {
if len(packet.Buffer) < int(s.opcodeSize) {
return nil
}
var opcode uint16
var data []byte
if s.opcodeSize == 1 {
opcode = uint16(packet.Buffer[0])
data = packet.Buffer[1:]
} else {
opcode = binary.LittleEndian.Uint16(packet.Buffer)
data = packet.Buffer[2:]
}
appPacket := NewApplicationPacket(EmuOpcode(opcode), data)
appPacket.AppOpcodeSize = s.opcodeSize
return appPacket
}
// Send queues a packet for sending
func (s *Stream) Send(packet *ApplicationPacket) error {
s.mu.Lock()
defer s.mu.Unlock()
// Convert to protocol packet
data := packet.Serialize()
protocolPacket := NewProtocolPacketWithOp(uint16(OPPacket), data)
// Add to outgoing queue
s.outgoingQueue = append(s.outgoingQueue, protocolPacket)
return nil
}
// sendSessionResponse sends a session response
func (s *Stream) sendSessionResponse() error {
resp := &SessionResponse{
Session: s.sessionID,
Key: s.key,
Format: 0,
MaxLength: s.maxLength,
}
if s.compressed {
resp.Format |= 0x01
}
if s.encoded {
resp.Format |= 0x04
}
// Build response packet
data := make([]byte, 21)
binary.BigEndian.PutUint16(data[0:2], uint16(OPSessionResponse))
binary.LittleEndian.PutUint32(data[2:6], resp.Session)
binary.LittleEndian.PutUint32(data[6:10], resp.Key)
data[10] = resp.UnknownA
data[11] = resp.Format
data[12] = resp.UnknownB
binary.LittleEndian.PutUint32(data[13:17], resp.MaxLength)
binary.LittleEndian.PutUint32(data[17:21], resp.UnknownD)
// Session response doesn't have CRC
return s.sendRaw(data)
}
// sendKeepAliveResponse sends a keep-alive response
func (s *Stream) sendKeepAliveResponse() error {
data := make([]byte, 2)
binary.BigEndian.PutUint16(data, uint16(OPKeepAlive))
return s.sendWithCRC(data)
}
// sendAck sends an acknowledgment
func (s *Stream) sendAck(sequence uint16) error {
data := make([]byte, 4)
binary.BigEndian.PutUint16(data[0:2], uint16(OPAck))
binary.BigEndian.PutUint16(data[2:4], sequence)
return s.sendWithCRC(data)
}
// sendOutOfOrderAck sends an out-of-order acknowledgment
func (s *Stream) sendOutOfOrderAck(sequence uint16) error {
data := make([]byte, 4)
binary.BigEndian.PutUint16(data[0:2], uint16(OPOutOfOrderAck))
binary.BigEndian.PutUint16(data[2:4], sequence)
return s.sendWithCRC(data)
}
// sendWithCRC sends data with CRC
func (s *Stream) sendWithCRC(data []byte) error {
// Calculate CRC
crc := CalculateCRC16(data, s.key)
// Append CRC
fullData := make([]byte, len(data)+2)
copy(fullData, data)
binary.BigEndian.PutUint16(fullData[len(data):], crc)
return s.sendRaw(fullData)
}
// sendRaw sends raw data (should be called from factory)
func (s *Stream) sendRaw(data []byte) error {
s.lastSendTime = time.Now()
s.sentPackets++
// The actual sending will be done by StreamFactory
return nil
}
// resendPacket resends a packet
func (s *Stream) resendPacket(packet *ProtocolPacket) {
packet.AttemptCount++
packet.SentTime = int32(time.Now().Unix())
// Re-queue for sending
s.outgoingQueue = append(s.outgoingQueue, packet)
}
// updateRTT updates the round-trip time statistics
func (s *Stream) updateRTT(rtt time.Duration) {
if s.minRoundTrip == 0 || rtt < s.minRoundTrip {
s.minRoundTrip = rtt
}
if rtt > s.maxRoundTrip {
s.maxRoundTrip = rtt
}
// Update average (simple moving average)
if s.avgRoundTrip == 0 {
s.avgRoundTrip = rtt
} else {
s.avgRoundTrip = (s.avgRoundTrip*3 + rtt) / 4
}
// Update retransmit timeout
s.retransmitTimeout = s.avgRoundTrip * 3
if s.retransmitTimeout > 5*time.Second {
s.retransmitTimeout = 5 * time.Second
}
}
// SetOnPacket sets the packet callback
func (s *Stream) SetOnPacket(callback func(*ApplicationPacket)) {
s.mu.Lock()
defer s.mu.Unlock()
s.onPacket = callback
}
// SetOnDisconnect sets the disconnect callback
func (s *Stream) SetOnDisconnect(callback func()) {
s.mu.Lock()
defer s.mu.Unlock()
s.onDisconnect = callback
}

View File

@ -1,471 +0,0 @@
package eq2net
import (
"crypto/rand"
"encoding/binary"
"fmt"
"net"
"sync"
"time"
)
// StreamFactory manages multiple UDP streams
type StreamFactory struct {
mu sync.RWMutex
// Network
conn *net.UDPConn
listenAddr *net.UDPAddr
streamType StreamType
// Streams
streams map[string]*Stream // Key is "IP:Port"
streamsByID map[uint32]*Stream // Key is session ID
// Options
maxStreams int
readTimeout time.Duration
writeTimeout time.Duration
// State
running bool
stopChan chan struct{}
// Callbacks
onNewStream func(*Stream)
onStreamClosed func(*Stream)
}
// NewStreamFactory creates a new stream factory
func NewStreamFactory(listenAddr string, streamType StreamType) (*StreamFactory, error) {
addr, err := net.ResolveUDPAddr("udp", listenAddr)
if err != nil {
return nil, err
}
sf := &StreamFactory{
listenAddr: addr,
streamType: streamType,
streams: make(map[string]*Stream),
streamsByID: make(map[uint32]*Stream),
maxStreams: 1000,
readTimeout: 30 * time.Second,
writeTimeout: 5 * time.Second,
stopChan: make(chan struct{}),
}
return sf, nil
}
// Start starts the stream factory
func (sf *StreamFactory) Start() error {
sf.mu.Lock()
defer sf.mu.Unlock()
if sf.running {
return fmt.Errorf("stream factory already running")
}
// Create UDP connection
conn, err := net.ListenUDP("udp", sf.listenAddr)
if err != nil {
return err
}
sf.conn = conn
// Set buffer sizes
conn.SetReadBuffer(65536)
conn.SetWriteBuffer(65536)
sf.running = true
// Start worker goroutines
go sf.readLoop()
go sf.writeLoop()
go sf.maintenanceLoop()
return nil
}
// Stop stops the stream factory
func (sf *StreamFactory) Stop() {
sf.mu.Lock()
defer sf.mu.Unlock()
if !sf.running {
return
}
sf.running = false
close(sf.stopChan)
if sf.conn != nil {
sf.conn.Close()
}
// Close all streams
for _, stream := range sf.streams {
stream.SetState(Closed)
if stream.onDisconnect != nil {
stream.onDisconnect()
}
}
sf.streams = make(map[string]*Stream)
sf.streamsByID = make(map[uint32]*Stream)
}
// readLoop reads packets from the UDP connection
func (sf *StreamFactory) readLoop() {
buffer := make([]byte, 65536)
for {
select {
case <-sf.stopChan:
return
default:
// Set read deadline
sf.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
// Read packet
n, remoteAddr, err := sf.conn.ReadFromUDP(buffer)
if err != nil {
// Check if it's a timeout (expected)
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
continue
}
// Actual error
continue
}
// Process packet
data := make([]byte, n)
copy(data, buffer[:n])
go sf.processPacket(data, remoteAddr)
}
}
}
// writeLoop handles writing packets to streams
func (sf *StreamFactory) writeLoop() {
ticker := time.NewTicker(10 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-sf.stopChan:
return
case <-ticker.C:
sf.processOutgoingPackets()
}
}
}
// maintenanceLoop performs periodic maintenance
func (sf *StreamFactory) maintenanceLoop() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-sf.stopChan:
return
case <-ticker.C:
sf.performMaintenance()
}
}
}
// processPacket processes an incoming packet
func (sf *StreamFactory) processPacket(data []byte, remoteAddr *net.UDPAddr) {
if len(data) < 2 {
return
}
// Get or create stream
stream := sf.getOrCreateStream(remoteAddr, data)
if stream == nil {
return
}
// Process packet in stream
err := stream.Process(data)
if err != nil {
// Handle error (log it, etc.)
}
}
// getOrCreateStream gets an existing stream or creates a new one
func (sf *StreamFactory) getOrCreateStream(remoteAddr *net.UDPAddr, data []byte) *Stream {
sf.mu.Lock()
defer sf.mu.Unlock()
key := fmt.Sprintf("%s:%d", remoteAddr.IP.String(), remoteAddr.Port)
// Check if stream exists
if stream, ok := sf.streams[key]; ok {
return stream
}
// Check if this is a session request
if len(data) >= 2 {
opcode := binary.BigEndian.Uint16(data)
if uint8(opcode&0xFF) == OPSessionRequest {
// Create new stream
stream := NewStream(remoteAddr, sf.streamType)
// Generate session ID and key
sessionID := sf.generateSessionID()
sessionKey := sf.generateKey()
stream.SetSessionInfo(sessionID, sessionKey)
// Add to maps
sf.streams[key] = stream
sf.streamsByID[sessionID] = stream
// Call new stream callback
if sf.onNewStream != nil {
sf.onNewStream(stream)
}
return stream
}
}
return nil
}
// processOutgoingPackets processes outgoing packets for all streams
func (sf *StreamFactory) processOutgoingPackets() {
sf.mu.RLock()
streams := make([]*Stream, 0, len(sf.streams))
for _, stream := range sf.streams {
streams = append(streams, stream)
}
sf.mu.RUnlock()
for _, stream := range streams {
sf.processStreamOutgoing(stream)
}
}
// processStreamOutgoing processes outgoing packets for a stream
func (sf *StreamFactory) processStreamOutgoing(stream *Stream) {
stream.mu.Lock()
defer stream.mu.Unlock()
// Process outgoing queue
for len(stream.outgoingQueue) > 0 {
packet := stream.outgoingQueue[0]
stream.outgoingQueue = stream.outgoingQueue[1:]
// Assign sequence number
packet.Sequence = stream.nextOutSeq
stream.nextOutSeq++
// Build packet data
data := sf.buildPacketData(stream, packet)
// Send packet
sf.sendPacket(stream.remoteAddr, data)
// Add to sent queue for acknowledgment tracking
packet.SentTime = int32(time.Now().Unix())
stream.sentQueue[packet.Sequence] = packet
}
// Check for retransmissions
now := time.Now()
for seq, packet := range stream.sentQueue {
sentTime := time.Unix(int64(packet.SentTime), 0)
if now.Sub(sentTime) > stream.retransmitTimeout {
if packet.AttemptCount < 3 {
// Retransmit
packet.AttemptCount++
packet.SentTime = int32(now.Unix())
data := sf.buildPacketData(stream, packet)
sf.sendPacket(stream.remoteAddr, data)
} else {
// Give up on this packet
delete(stream.sentQueue, seq)
}
}
}
}
// buildPacketData builds the complete packet data including headers and CRC
func (sf *StreamFactory) buildPacketData(stream *Stream, packet *ProtocolPacket) []byte {
// Check if we need to fragment
maxDataSize := int(stream.maxLength) - 6 // Header (4) + CRC (2)
if len(packet.Buffer) > maxDataSize {
// Need to fragment
return sf.buildFragmentData(stream, packet)
}
// Build regular packet
data := make([]byte, len(packet.Buffer)+6)
// Header
binary.BigEndian.PutUint16(data[0:2], uint16(OPPacket))
binary.BigEndian.PutUint16(data[2:4], packet.Sequence)
// Data
copy(data[4:], packet.Buffer)
// CRC
crc := CalculateCRC16(data[:len(data)-2], stream.key)
binary.BigEndian.PutUint16(data[len(data)-2:], crc)
return data
}
// buildFragmentData builds a fragment packet
func (sf *StreamFactory) buildFragmentData(stream *Stream, packet *ProtocolPacket) []byte {
// For now, just truncate (proper fragmentation would split into multiple packets)
// This is a simplified version - real implementation would handle multiple fragments
maxDataSize := int(stream.maxLength) - 10 // Header (8) + CRC (2)
dataSize := len(packet.Buffer)
if dataSize > maxDataSize {
dataSize = maxDataSize
}
data := make([]byte, dataSize+10)
// Header
binary.BigEndian.PutUint16(data[0:2], uint16(OPFragment))
binary.BigEndian.PutUint16(data[2:4], packet.Sequence)
binary.BigEndian.PutUint32(data[4:8], uint32(len(packet.Buffer)))
// Data
copy(data[8:], packet.Buffer[:dataSize])
// CRC
crc := CalculateCRC16(data[:len(data)-2], stream.key)
binary.BigEndian.PutUint16(data[len(data)-2:], crc)
return data
}
// sendPacket sends a packet
func (sf *StreamFactory) sendPacket(addr *net.UDPAddr, data []byte) error {
sf.conn.SetWriteDeadline(time.Now().Add(sf.writeTimeout))
_, err := sf.conn.WriteToUDP(data, addr)
return err
}
// performMaintenance performs periodic maintenance tasks
func (sf *StreamFactory) performMaintenance() {
sf.mu.Lock()
defer sf.mu.Unlock()
now := time.Now()
toRemove := []string{}
for key, stream := range sf.streams {
stream.mu.RLock()
lastReceive := stream.lastReceiveTime
state := stream.state
stream.mu.RUnlock()
// Check for timeout
if now.Sub(lastReceive) > sf.readTimeout && state == Established {
stream.SetState(Disconnecting)
toRemove = append(toRemove, key)
}
// Remove closed streams
if state == Closed || state == Disconnecting {
toRemove = append(toRemove, key)
}
}
// Remove dead streams
for _, key := range toRemove {
if stream, ok := sf.streams[key]; ok {
delete(sf.streams, key)
delete(sf.streamsByID, stream.GetSessionID())
// Call closed callback
if sf.onStreamClosed != nil {
sf.onStreamClosed(stream)
}
}
}
}
// generateSessionID generates a random session ID
func (sf *StreamFactory) generateSessionID() uint32 {
var id uint32
for {
binary.Read(rand.Reader, binary.BigEndian, &id)
// Make sure it's not already in use
if _, exists := sf.streamsByID[id]; !exists && id != 0 {
return id
}
}
}
// generateKey generates a random key
func (sf *StreamFactory) generateKey() uint32 {
var key uint32
binary.Read(rand.Reader, binary.BigEndian, &key)
return key
}
// GetStream gets a stream by remote address
func (sf *StreamFactory) GetStream(remoteAddr *net.UDPAddr) *Stream {
sf.mu.RLock()
defer sf.mu.RUnlock()
key := fmt.Sprintf("%s:%d", remoteAddr.IP.String(), remoteAddr.Port)
return sf.streams[key]
}
// GetStreamByID gets a stream by session ID
func (sf *StreamFactory) GetStreamByID(sessionID uint32) *Stream {
sf.mu.RLock()
defer sf.mu.RUnlock()
return sf.streamsByID[sessionID]
}
// GetStreamCount returns the number of active streams
func (sf *StreamFactory) GetStreamCount() int {
sf.mu.RLock()
defer sf.mu.RUnlock()
return len(sf.streams)
}
// SetOnNewStream sets the new stream callback
func (sf *StreamFactory) SetOnNewStream(callback func(*Stream)) {
sf.mu.Lock()
defer sf.mu.Unlock()
sf.onNewStream = callback
}
// SetOnStreamClosed sets the stream closed callback
func (sf *StreamFactory) SetOnStreamClosed(callback func(*Stream)) {
sf.mu.Lock()
defer sf.mu.Unlock()
sf.onStreamClosed = callback
}
// Broadcast sends a packet to all connected streams
func (sf *StreamFactory) Broadcast(packet *ApplicationPacket) {
sf.mu.RLock()
streams := make([]*Stream, 0, len(sf.streams))
for _, stream := range sf.streams {
if stream.GetState() == Established {
streams = append(streams, stream)
}
}
sf.mu.RUnlock()
for _, stream := range streams {
stream.Send(packet)
}
}