clean /old/login/net
This commit is contained in:
parent
76fa77d590
commit
ffb75045a4
88
crc16.go
88
crc16.go
@ -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))
|
||||
}
|
@ -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"
|
||||
|
||||
#ifdef WIN32
|
||||
#define snprintf _snprintf
|
||||
#define vsnprintf _vsnprintf
|
||||
#define strncasecmp _strnicmp
|
||||
#define strcasecmp _stricmp
|
||||
#include <conio.h>
|
||||
#else
|
||||
#include <stdlib.h>
|
||||
#include "../common/CRC16.h"
|
||||
#include "../common/timer.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());
|
||||
|
||||
// Read and validate login server configuration
|
||||
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++)
|
||||
{
|
||||
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();
|
||||
|
||||
// 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());
|
||||
{
|
||||
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
|
||||
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
}
|
280
old/login/net.h
280
old/login/net.h
@ -4,144 +4,208 @@
|
||||
|
||||
This file is part of EQ2Emulator.
|
||||
*/
|
||||
#ifdef WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#include <windows.h>
|
||||
#include <winsock.h>
|
||||
#else
|
||||
|
||||
#pragma once
|
||||
|
||||
// Linux network headers
|
||||
#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>
|
||||
// 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
|
||||
};
|
56
opcodes.go
56
opcodes.go
@ -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
356
packet.go
@ -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
675
stream.go
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user