significant work on login

This commit is contained in:
Sky Johnson 2025-08-30 19:05:47 -05:00
parent ab2b2600d0
commit bc6ba6ed6c
11 changed files with 443 additions and 150 deletions

View File

@ -192,7 +192,7 @@ func printBanner(config *login.ServerConfig) {
fmt.Printf("Server Name: %s\n", config.ServerName)
fmt.Printf("Listen Address: %s:%d\n", config.ListenAddr, config.ListenPort)
fmt.Printf("Web Interface: %s:%d\n", config.WebAddr, config.WebPort)
fmt.Printf("Database: mysql://%s@%s:%d/%s\n", config.DatabaseUsername, config.DatabaseAddress, config.DatabasePort, config.DatabaseName)
fmt.Printf("Database: %s:***@tcp(%s:%d)/%s\n", config.DatabaseUsername, config.DatabaseAddress, config.DatabasePort, config.DatabaseName)
fmt.Printf("Log Level: %s\n", config.LogLevel)
fmt.Printf("World Servers: %d configured\n", len(config.WorldServers))
fmt.Println("================================================================================")

View File

@ -9,7 +9,6 @@ import (
"sync"
"time"
"eq2emu/internal/common/opcodes"
"eq2emu/internal/packets"
"eq2emu/internal/udp"
)
@ -165,49 +164,78 @@ func (c *Client) HandlePacket(data []byte) error {
log.Printf("Client %s: Received opcode 0x%04x, size %d", c.ipAddress, opcode, len(payload))
// Use login server opcodes from C++ version
switch opcode {
case opcodes.OpLoginRequestMsg:
case 0x0200: // OP_Login2 - Primary login authentication
return c.handleLoginRequest(payload)
case opcodes.OpAllCharactersDescRequestMsg:
return c.handleCharacterSelectRequest(payload)
case opcodes.OpPlayCharacterRequestMsg:
return c.handlePlayCharacterRequest(payload)
case opcodes.OpDeleteCharacterRequestMsg:
return c.handleDeleteCharacterRequest(payload)
case opcodes.OpCreateCharacterRequestMsg:
return c.handleCreateCharacterRequest(payload)
case 0x0300: // OP_GetLoginInfo - Request login information from client
return c.handleGetLoginInfo(payload)
case 0x4600: // OP_ServerList - Complete server list
return c.handleServerListRequest(payload)
case 0x4800: // OP_RequestServerStatus - Request current server status
return c.handleServerStatusRequest(payload)
default:
log.Printf("Client %s: Unknown opcode 0x%04x", c.ipAddress, opcode)
return nil
}
}
// handleLoginRequest processes a login request from the client
// handleLoginRequest processes a login request from the client (OP_Login2)
func (c *Client) handleLoginRequest(payload []byte) error {
if c.state != ClientStateNew {
return fmt.Errorf("invalid state for login request: %s", c.state)
}
// Parse the packet using the proper packet system
clientVersion := uint32(562) // Assume version 562 for now
data, err := packets.ParsePacketFields(payload, "LoginRequest", clientVersion)
if err != nil {
return fmt.Errorf("failed to parse login request: %w", err)
c.state = ClientStateAuthenticating
log.Printf("Client %s: Processing login request", c.ipAddress)
// Parse login packet structure (simplified for MVP)
if len(payload) < 8 {
return fmt.Errorf("login packet too short")
}
// Extract fields based on the XML definition
accessCode := getStringField(data, "accesscode")
unknown1 := getStringField(data, "unknown1")
username := getStringField(data, "username")
password := getStringField(data, "password")
// Basic parsing - in real implementation this would use proper packet structures
// For now, we'll implement a simple test that creates an account if it doesn't exist
log.Printf("Login request - Username: %s, Access: %s, Unknown: %s",
username, accessCode, unknown1)
// Extract username and password from payload (simplified)
username, password, err := c.parseLoginCredentials(payload)
if err != nil {
return fmt.Errorf("failed to parse credentials: %w", err)
}
c.state = ClientStateAuthenticating
log.Printf("Client %s: Attempting login for user '%s'", c.ipAddress, username)
// Authenticate user
return c.authenticateUser(username, password)
// Try to authenticate the user
account, err := c.database.GetLoginAccount(username, password)
if err != nil {
if strings.Contains(err.Error(), "account not found") {
// Create new account automatically (as per C++ logic)
log.Printf("Creating new account for user '%s'", username)
account, err = c.database.CreateAccount(username, password, "unknown@example.com")
if err != nil {
return c.sendLoginFailed(fmt.Sprintf("Failed to create account: %v", err))
}
} else {
return c.sendLoginFailed(fmt.Sprintf("Authentication failed: %v", err))
}
}
// Authentication successful
c.accountID = account.ID
c.accountName = account.Username
c.accountEmail = account.Email
c.accessLevel = account.AccessLevel
c.state = ClientStateAuthenticated
// Update last login
err = c.database.UpdateLastLogin(account.ID, c.ipAddress)
if err != nil {
log.Printf("Failed to update last login: %v", err)
}
log.Printf("User '%s' authenticated successfully (ID: %d)", username, account.ID)
return c.sendLoginSuccess()
}
// Helper function to safely extract string fields from packet data
@ -538,4 +566,67 @@ func (c *Client) IsTimedOut(timeout time.Duration) bool {
c.mu.RLock()
defer c.mu.RUnlock()
return time.Since(c.lastActivity) > timeout
}
// parseLoginCredentials extracts username and password from login payload
func (c *Client) parseLoginCredentials(payload []byte) (string, string, error) {
// This is a simplified implementation - real implementation would parse the exact packet structure
// For testing, we'll hardcode some credentials or parse very basic format
// For MVP, let's implement a simple test login
username := "testuser"
password := "testpass"
return username, password, nil
}
// sendLoginSuccess sends a successful login response
func (c *Client) sendLoginSuccess() error {
// Build login success packet (OP_LoginInfo = 0x0100)
response := make([]byte, 4)
binary.LittleEndian.PutUint16(response[0:2], 0x0100) // OP_LoginInfo
binary.LittleEndian.PutUint16(response[2:4], 0x0001) // Success status
packet := &udp.ApplicationPacket{
Data: response,
}
c.connection.SendPacket(packet)
return nil
}
// sendLoginFailed sends a login failure response
func (c *Client) sendLoginFailed(reason string) error {
log.Printf("Login failed for client %s: %s", c.ipAddress, reason)
// Build login failure packet
response := make([]byte, 4)
binary.LittleEndian.PutUint16(response[0:2], 0x0100) // OP_LoginInfo
binary.LittleEndian.PutUint16(response[2:4], 0x0000) // Failure status
packet := &udp.ApplicationPacket{
Data: response,
}
c.state = ClientStateDisconnected
c.connection.SendPacket(packet)
return nil
}
// handleGetLoginInfo processes a get login info request
func (c *Client) handleGetLoginInfo(payload []byte) error {
log.Printf("Client %s: Get login info request", c.ipAddress)
return nil
}
// handleServerListRequest processes a server list request
func (c *Client) handleServerListRequest(payload []byte) error {
log.Printf("Client %s: Server list request", c.ipAddress)
return nil
}
// handleServerStatusRequest processes a server status request
func (c *Client) handleServerStatusRequest(payload []byte) error {
log.Printf("Client %s: Server status request", c.ipAddress)
return nil
}

View File

@ -2,7 +2,6 @@ package login
import (
"fmt"
"net/url"
"strings"
)
@ -49,11 +48,11 @@ type WorldServerInfo struct {
Population int `json:"population"` // Current player count
MaxPlayers int `json:"max_players"` // Maximum allowed players
Description string `json:"description"`
// Server flags
Locked bool `json:"locked"`
Hidden bool `json:"hidden"`
// Connection tracking
LastHeartbeat int64 `json:"last_heartbeat"`
}
@ -76,19 +75,19 @@ func (c *ServerConfig) Validate() error {
if c.DatabaseAddress == "" {
c.DatabaseAddress = "localhost" // Default to localhost
}
if c.DatabasePort <= 0 {
c.DatabasePort = 3306 // Default MySQL port
}
if c.DatabaseUsername == "" {
return fmt.Errorf("database_username is required")
}
if c.DatabasePassword == "" {
return fmt.Errorf("database_password is required")
}
if c.DatabaseName == "" {
return fmt.Errorf("database_name is required")
}
@ -141,8 +140,8 @@ func (c *ServerConfig) Validate() error {
func (c *ServerConfig) BuildDatabaseDSN() string {
// Format: username:password@tcp(address:port)/database?parseTime=true&charset=utf8mb4
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?parseTime=true&charset=utf8mb4",
url.QueryEscape(c.DatabaseUsername),
url.QueryEscape(c.DatabasePassword),
c.DatabaseUsername,
c.DatabasePassword,
c.DatabaseAddress,
c.DatabasePort,
c.DatabaseName)
@ -218,7 +217,7 @@ func (w *WorldServerInfo) GetPopulationPercentage() float64 {
// GetPopulationLevel returns a string representation of the population level
func (w *WorldServerInfo) GetPopulationLevel() string {
pct := w.GetPopulationPercentage()
switch {
case pct >= 95:
return "FULL"
@ -237,4 +236,4 @@ func (w *WorldServerInfo) GetPopulationLevel() string {
func (w *WorldServerInfo) Clone() *WorldServerInfo {
clone := *w
return &clone
}
}

View File

@ -55,22 +55,26 @@ func NewLoginDB(dsn string) (*LoginDB, error) {
}
// GetLoginAccount retrieves a login account by username and password
func (db *LoginDB) GetLoginAccount(username, hashedPassword string) (*LoginAccount, error) {
// GetLoginAccount retrieves a login account by username and password (C++ compatible)
func (db *LoginDB) GetLoginAccount(username, password string) (*LoginAccount, error) {
var account LoginAccount
query := "SELECT id, username, password, email, status, access_level, created_date, last_login, last_ip FROM login_accounts WHERE username = ? AND password = ?"
// Using the same query as C++ version: SHA512 hash comparison
query := "SELECT id, name, passwd, email_address, created_date, last_update, ip_address FROM account WHERE name = ? AND passwd = sha2(?, 512)"
row := db.QueryRow(query, username, hashedPassword)
row := db.QueryRow(query, username, password)
// Variables to handle nullable fields
var email, lastIP sql.NullString
var lastLogin sql.NullInt64
err := row.Scan(
&account.ID,
&account.Username,
&account.Password,
&account.Email,
&account.Status,
&account.AccessLevel,
&account.Password, // This will be the hash from DB
&email,
&account.CreatedDate,
&account.LastLogin,
&account.LastIP,
&lastLogin,
&lastIP,
)
if err != nil {
if err == sql.ErrNoRows {
@ -79,6 +83,27 @@ func (db *LoginDB) GetLoginAccount(username, hashedPassword string) (*LoginAccou
return nil, fmt.Errorf("database query error: %w", err)
}
// Handle nullable fields
if email.Valid {
account.Email = email.String
} else {
account.Email = "Unknown"
}
if lastLogin.Valid {
account.LastLogin = lastLogin.Int64
}
if lastIP.Valid {
account.LastIP = lastIP.String
} else {
account.LastIP = "0.0.0.0"
}
// Set default values
account.Status = "Active"
account.AccessLevel = 0
return &account, nil
}
@ -129,10 +154,10 @@ func (db *LoginDB) GetCharacters(accountID int32) ([]*Character, error) {
return characters, nil
}
// UpdateLastLogin updates the last login time and IP for an account
// UpdateLastLogin updates the last login time and IP for an account (C++ compatible)
func (db *LoginDB) UpdateLastLogin(accountID int32, ipAddress string) error {
now := time.Now().Unix()
query := "UPDATE login_accounts SET last_login = ?, last_ip = ? WHERE id = ?"
query := "UPDATE account SET last_update = ?, ip_address = ? WHERE id = ?"
_, err := db.Exec(query, now, ipAddress, accountID)
return err
@ -153,12 +178,12 @@ func (db *LoginDB) UpdateServerStats(serverType string, clientCount, worldCount
return err
}
// CreateAccount creates a new login account
func (db *LoginDB) CreateAccount(username, hashedPassword, email string, accessLevel int16) (*LoginAccount, error) {
// CreateAccount creates a new login account (C++ compatible)
func (db *LoginDB) CreateAccount(username, password, email string) (*LoginAccount, error) {
now := time.Now().Unix()
// Check if username already exists
exists, err := db.Exists("SELECT 1 FROM login_accounts WHERE username = ?", username)
// Check if username already exists
exists, err := db.Exists("SELECT 1 FROM account WHERE name = ?", username)
if err != nil {
return nil, fmt.Errorf("failed to check username: %w", err)
}
@ -167,11 +192,11 @@ func (db *LoginDB) CreateAccount(username, hashedPassword, email string, accessL
return nil, fmt.Errorf("username already exists")
}
// Insert new account
// Insert new account using same schema as C++ version
accountID, err := db.InsertReturningID(
`INSERT INTO login_accounts (username, password, email, access_level, created_date, status)
VALUES (?, ?, ?, ?, ?, 'Active')`,
username, hashedPassword, email, accessLevel, now,
`INSERT INTO account (name, passwd, email_address, created_date, account_enabled)
VALUES (?, sha2(?, 512), ?, ?, 1)`,
username, password, email, now,
)
if err != nil {
return nil, fmt.Errorf("failed to create account: %w", err)
@ -181,13 +206,13 @@ func (db *LoginDB) CreateAccount(username, hashedPassword, email string, accessL
return &LoginAccount{
ID: int32(accountID),
Username: username,
Password: hashedPassword,
Password: "", // Don't return hash
Email: email,
Status: "Active",
AccessLevel: accessLevel,
AccessLevel: 0,
CreatedDate: now,
LastLogin: 0,
LastIP: "",
LastIP: "0.0.0.0",
}, nil
}
@ -236,13 +261,13 @@ func (db *LoginDB) DeleteCharacter(characterID int32) error {
return err
}
// GetAccountStats retrieves statistics about login accounts
// GetAccountStats retrieves statistics about login accounts (C++ compatible)
func (db *LoginDB) GetAccountStats() (map[string]int, error) {
stats := make(map[string]int)
// Count total accounts
var totalAccounts int
err := db.QueryRow("SELECT COUNT(*) FROM login_accounts").Scan(&totalAccounts)
err := db.QueryRow("SELECT COUNT(*) FROM account").Scan(&totalAccounts)
if err != nil {
return nil, err
}
@ -250,7 +275,7 @@ func (db *LoginDB) GetAccountStats() (map[string]int, error) {
// Count active accounts
var activeAccounts int
err = db.QueryRow("SELECT COUNT(*) FROM login_accounts WHERE status = 'Active'").Scan(&activeAccounts)
err = db.QueryRow("SELECT COUNT(*) FROM account WHERE account_enabled = 1").Scan(&activeAccounts)
if err != nil {
return nil, err
}
@ -258,7 +283,7 @@ func (db *LoginDB) GetAccountStats() (map[string]int, error) {
// Count total characters
var totalCharacters int
err = db.QueryRow("SELECT COUNT(*) FROM characters WHERE deleted_date = 0").Scan(&totalCharacters)
err = db.QueryRow("SELECT COUNT(*) FROM login_characters WHERE deleted = 0").Scan(&totalCharacters)
if err != nil {
return nil, err
}

View File

@ -90,9 +90,19 @@ func (s *Server) Start() error {
}
log.Printf("Starting login server on %s:%d", s.config.ListenAddr, s.config.ListenPort)
log.Printf("UDP server will bind to: %s:%d", s.config.ListenAddr, s.config.ListenPort)
log.Printf("Max clients: %d", s.config.MaxClients)
// Start UDP server (it doesn't return an error in this implementation)
go s.udpServer.Start()
// Start UDP server and handle any startup errors
log.Printf("Starting UDP server...")
s.wg.Add(1)
go func() {
defer s.wg.Done()
if err := s.udpServer.Start(); err != nil {
log.Printf("UDP server error: %v", err)
}
}()
log.Printf("UDP server started")
// Start web server if configured
if s.webServer != nil {
@ -196,17 +206,24 @@ func (s *Server) Process() {
// handleUDPPacket handles incoming UDP packets from clients
func (s *Server) handleUDPPacket(conn *udp.Connection, packet *udp.ApplicationPacket) {
log.Printf("Login server received application packet from %s: %d bytes", conn.GetClientAddr(), len(packet.Data))
// Find or create client for this connection
client := s.clientList.GetByConnection(conn)
if client == nil {
client = NewClient(conn, s.database)
s.clientList.Add(client)
log.Printf("New client connected from %s", conn.GetClientAddr())
log.Printf("New client connected from %s (total clients: %d)", conn.GetClientAddr(), s.clientList.Count())
} else {
log.Printf("Using existing client for %s", conn.GetClientAddr())
}
// Process packet
log.Printf("Forwarding packet to client handler for %s", conn.GetClientAddr())
if err := client.HandlePacket(packet.Data); err != nil {
log.Printf("Error handling packet from %s: %v", conn.GetClientAddr(), err)
} else {
log.Printf("Successfully processed packet from %s", conn.GetClientAddr())
}
}

View File

@ -12,6 +12,26 @@ type InternalOpcode int32
const (
OP_Unknown InternalOpcode = iota
// Login server specific opcodes (from C++ LoginServer)
OP_Login2
OP_GetLoginInfo
OP_LoginInfo
OP_SessionId
OP_SessionKey
OP_Disconnect
OP_AllFinish
OP_Ack5
OP_SendServersFragment
OP_ServerList
OP_RequestServerStatus
OP_SendServerStatus
OP_Version
OP_LoginBanner
OP_PlayCharacterRequest
OP_CharacterList
OP_CharacterCreate
OP_CharacterDelete
// Login and authentication operations
OP_LoginReplyMsg
OP_LoginByNumRequestMsg
@ -295,6 +315,25 @@ const (
// OpcodeNames maps internal opcodes to their string names for debugging
var OpcodeNames = map[InternalOpcode]string{
OP_Unknown: "OP_Unknown",
// Login server opcodes
OP_Login2: "OP_Login2",
OP_GetLoginInfo: "OP_GetLoginInfo",
OP_LoginInfo: "OP_LoginInfo",
OP_SessionId: "OP_SessionId",
OP_SessionKey: "OP_SessionKey",
OP_Disconnect: "OP_Disconnect",
OP_AllFinish: "OP_AllFinish",
OP_Ack5: "OP_Ack5",
OP_SendServersFragment: "OP_SendServersFragment",
OP_ServerList: "OP_ServerList",
OP_RequestServerStatus: "OP_RequestServerStatus",
OP_SendServerStatus: "OP_SendServerStatus",
OP_Version: "OP_Version",
OP_LoginBanner: "OP_LoginBanner",
OP_PlayCharacterRequest: "OP_PlayCharacterRequest",
OP_CharacterList: "OP_CharacterList",
OP_CharacterCreate: "OP_CharacterCreate",
OP_CharacterDelete: "OP_CharacterDelete",
OP_LoginReplyMsg: "OP_LoginReplyMsg",
OP_LoginByNumRequestMsg: "OP_LoginByNumRequestMsg",
OP_WSLoginRequestMsg: "OP_WSLoginRequestMsg",

View File

@ -1,10 +1,10 @@
package udp
import (
"crypto/rand"
"encoding/binary"
"eq2emu/internal/common/opcodes"
"errors"
"fmt"
"net"
"sync"
"time"
@ -76,7 +76,7 @@ type Connection struct {
// Session parameters
sessionID uint32 // Unique session identifier
key uint32 // Encryption key
key uint32 // Encryption key (default 0x33624702 for CRC calculations)
compressed bool // Whether compression is enabled
encoded bool // Whether encoding is enabled
maxLength uint32 // Maximum packet length
@ -106,6 +106,7 @@ func NewConnection(addr *net.UDPAddr, conn *net.UDPConn, handler PacketHandler,
conn: conn,
handler: handler,
state: StateClosed,
key: 0x33624702, // Default EQ2 session key for CRC calculations
maxLength: config.MaxPacketSize,
lastActivity: time.Now(),
config: config,
@ -130,9 +131,20 @@ func (c *Connection) ProcessPacket(data []byte) {
c.lastActivity = time.Now()
packet, err := ParseProtocolPacket(data)
// Debug: Print raw packet hex
fmt.Printf("Raw packet from %s (%d bytes): ", c.addr.String(), len(data))
for i, b := range data {
fmt.Printf("%02X ", b)
if i > 0 && (i+1)%16 == 0 {
fmt.Printf("\n ")
}
}
fmt.Printf("\n")
packet, err := ParseProtocolPacketWithCRC(data, c.key)
if err != nil {
return // Silently drop malformed packets
fmt.Printf("Error parsing protocol packet from %s: %v\n", c.addr.String(), err)
return // Drop packets with invalid CRC or other errors
}
// Route packet based on opcode
@ -174,13 +186,12 @@ func (c *Connection) handleSessionRequest(packet *ProtocolPacket) {
c.fragmentMgr = NewFragmentManager(requestedMaxLen)
}
// Generate random encryption key
keyBytes := make([]byte, 4)
rand.Read(keyBytes)
c.key = binary.LittleEndian.Uint32(keyBytes)
// Keep the default key for CRC calculations (don't generate random key)
// The C++ code sets Key = 0x33624702 in the session request handler
// c.key is already initialized to this value in NewConnection
// Initialize encryption
c.crypto.SetKey(keyBytes)
// For encryption, we could generate a different key later if needed
// but for now keep the CRC key constant
c.sendSessionResponse()
c.state = StateEstablished

View File

@ -29,24 +29,16 @@ type ApplicationPacket struct {
}
// ParseProtocolPacket parses raw UDP data into a ProtocolPacket
// Handles variable opcode sizing and CRC validation based on EQ2 protocol
// Handles EQ2 protocol format: [header_byte][opcode][data...]
func ParseProtocolPacket(data []byte) (*ProtocolPacket, error) {
if len(data) < 2 {
return nil, ErrPacketTooSmall
}
var opcode uint8
var dataStart int
// EQ2 protocol uses 1-byte opcodes normally, 2-byte for opcodes >= 0xFF
// When opcode >= 0xFF, it's prefixed with 0x00
if data[0] == 0x00 && len(data) > 2 {
opcode = data[1]
dataStart = 2
} else {
opcode = data[0]
dataStart = 1
}
// EQ2 raw UDP packet format: [header][opcode][payload...]
// The opcode is at position 1, data starts at position 2
opcode := data[1]
dataStart := 2
// Extract payload, handling CRC for non-session packets
var payload []byte
@ -58,10 +50,8 @@ func ParseProtocolPacket(data []byte) (*ProtocolPacket, error) {
// Payload excludes the 2-byte CRC suffix
payload = data[dataStart : len(data)-2]
// Validate CRC on the entire packet from beginning
if !ValidateCRC(data) {
return nil, fmt.Errorf("%w for opcode 0x%02X", ErrInvalidCRC, opcode)
}
// Note: CRC validation moved to connection level where we have the session key
// For now, we'll accept all packets and let connection handle CRC validation
} else {
payload = data[dataStart:]
}
@ -73,24 +63,32 @@ func ParseProtocolPacket(data []byte) (*ProtocolPacket, error) {
}, nil
}
// Serialize converts ProtocolPacket back to wire format with proper opcode encoding and CRC
func (p *ProtocolPacket) Serialize() []byte {
var result []byte
// Handle variable opcode encoding
if p.Opcode == 0xFF {
// 2-byte opcode format: [0x00][actual_opcode][data]
result = make([]byte, 2+len(p.Data))
result[0] = 0x00
result[1] = p.Opcode
copy(result[2:], p.Data)
} else {
// 1-byte opcode format: [opcode][data]
result = make([]byte, 1+len(p.Data))
result[0] = p.Opcode
copy(result[1:], p.Data)
// ParseProtocolPacketWithCRC parses raw UDP data into a ProtocolPacket with CRC validation
// Handles EQ2 protocol format: [header_byte][opcode][data...]
func ParseProtocolPacketWithCRC(data []byte, key uint32) (*ProtocolPacket, error) {
packet, err := ParseProtocolPacket(data)
if err != nil {
return nil, err
}
// Validate CRC for packets that require it
if requiresCRC(packet.Opcode) {
if !ValidateCRCWithKey(data, key) {
return nil, fmt.Errorf("%w for opcode 0x%02X", ErrInvalidCRC, packet.Opcode)
}
}
return packet, nil
}
// Serialize converts ProtocolPacket back to wire format with EQ2 format: [header][opcode][data]
func (p *ProtocolPacket) Serialize() []byte {
// EQ2 format: [header_byte][opcode][data...]
result := make([]byte, 2+len(p.Data))
result[0] = 0x00 // Header byte - set to 0 for now
result[1] = p.Opcode
copy(result[2:], p.Data)
// Add CRC for packets that require it
if requiresCRC(p.Opcode) {
result = AppendCRC(result)

View File

@ -3,72 +3,153 @@ package udp
import (
"crypto/rc4"
"errors"
"fmt"
)
// EQ2EMu CRC32 polynomial (reversed)
const crcPolynomial = 0xEDB88320
// Pre-computed CRC32 lookup table for fast calculation
var crcTable [256]uint32
// init builds the CRC lookup table at package initialization
func init() {
for i := range crcTable {
crc := uint32(i)
for range 8 {
if crc&1 == 1 {
crc = (crc >> 1) ^ crcPolynomial
} else {
crc >>= 1
}
}
crcTable[i] = crc
}
// CRC32 lookup table for polynomial 0xEDB88320 (IEEE 802.3 standard)
var crcTable = [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,
}
// CalculateCRC32 computes CRC32 using EQ2EMu's algorithm
// Returns 16-bit value by truncating the upper bits
func CalculateCRC32(data []byte) uint16 {
crc := uint32(0xFFFFFFFF)
// CRC16 computes CRC32 checksum with custom key initialization for EQ2 protocol
// This function implements the reverse-engineered CRC calculation used in EQ2
// despite being named CRC16, it actually performs 32-bit CRC calculation
func CRC16(buf []byte, key uint32) uint32 {
crc := key // Initialize with provided key
// Use lookup table for efficient calculation
for _, b := range data {
crc = crcTable[byte(crc)^b] ^ (crc >> 8)
// Pre-process the key through multiple CRC table lookups
// This mirrors the original assembly implementation's key initialization
crc = ^crc // Invert initial key
crc &= 0xFF // Mask to lowest byte
crc = crcTable[crc] // First table lookup
crc ^= 0x00FFFFFF // XOR with mask
// Process second byte of original key
temp := key >> 8 // Shift key right 8 bits
temp ^= crc // XOR with current CRC
crc >>= 8 // Shift CRC right 8 bits
temp &= 0xFF // Mask to byte
crc &= 0x00FFFFFF // Mask CRC to 24 bits
crc ^= crcTable[temp] // Second table lookup
// Process third byte of original key
temp = key >> 16 // Shift key right 16 bits
temp ^= crc // XOR with current CRC
crc >>= 8 // Shift CRC right 8 bits
temp &= 0xFF // Mask to byte
temp2 := crcTable[temp] // Third table lookup
crc &= 0x00FFFFFF // Mask CRC to 24 bits
crc ^= temp2 // XOR with lookup result
// Process fourth byte of original key
fourthByte := key >> 24 // Extract highest byte
fourthByte ^= crc // XOR with current CRC
fourthByte &= 0xFF // Mask to byte
temp2 = crcTable[fourthByte] // Fourth table lookup
crc >>= 8 // Shift CRC right 8 bits
crc &= 0x00FFFFFF // Mask CRC to 24 bits
crc ^= temp2 // XOR with lookup result
// Process each byte in the input buffer
for _, b := range buf {
byteVal := uint32(b) & 0xFF // Extract current byte
byteVal ^= crc // XOR byte with current CRC
crc >>= 8 // Shift CRC right 8 bits
byteVal &= 0xFF // Ensure byte is masked
lookup := crcTable[byteVal] // Table lookup for this byte
crc &= 0x00FFFFFF // Mask CRC to 24 bits
crc ^= lookup // XOR with lookup result
}
// Return inverted result truncated to 16 bits
return uint16(^crc)
return ^crc // Return inverted final CRC
}
// ValidateCRC checks if packet has valid CRC
// ValidateCRCWithKey checks if packet has valid CRC using the provided key
// Expects CRC to be the last 2 bytes of data
func ValidateCRC(data []byte) bool {
if len(data) < 2 {
func ValidateCRCWithKey(data []byte, key uint32) bool {
if len(data) < 4 { // Need at least [header][opcode][data][CRC][CRC]
return false
}
// Split payload and CRC
payload := data[:len(data)-2]
expectedCRC := uint16(data[len(data)-2]) | (uint16(data[len(data)-1]) << 8)
// The CRC should be calculated on the serialized packet (opcode + data), excluding header and CRC
// EQ2 packet format: [header][opcode][data...][CRC16]
// CRC covers: [opcode][data...] (starting from byte 1, excluding header and CRC)
payload := data[1 : len(data)-2] // Skip header, include opcode, exclude CRC
expectedCRC := uint16(data[len(data)-1])<<8 | uint16(data[len(data)-2]) // Big-endian CRC
// Calculate and compare
actualCRC := CalculateCRC32(payload)
actualCRC := uint16(CRC16(payload, key))
// Debug output
fmt.Printf("CRC Debug - Key: 0x%08X, Expected: 0x%04X, Actual: 0x%04X, Match: %v\n",
key, expectedCRC, actualCRC, expectedCRC == actualCRC)
fmt.Printf("CRC payload (%d bytes): ", len(payload))
for i, b := range payload {
fmt.Printf("%02X ", b)
if i > 0 && (i+1)%16 == 0 {
fmt.Printf("\n ")
}
}
fmt.Printf("\n")
return expectedCRC == actualCRC
}
// AppendCRC adds 16-bit CRC to the end of data
func AppendCRC(data []byte) []byte {
crc := CalculateCRC32(data)
// ValidateCRC checks if packet has valid CRC using default key (0)
// Expects CRC to be the last 2 bytes of data
func ValidateCRC(data []byte) bool {
return ValidateCRCWithKey(data, 0)
}
// AppendCRCWithKey adds 16-bit CRC to the end of data using the provided key
func AppendCRCWithKey(data []byte, key uint32) []byte {
crc := uint16(CRC16(data, key))
result := make([]byte, len(data)+2)
copy(result, data)
// Append CRC in little-endian format
// Append CRC in big-endian format (as per C++ htons)
result[len(data)] = byte(crc)
result[len(data)+1] = byte(crc >> 8)
return result
}
// AppendCRC adds 16-bit CRC to the end of data using default key (0)
func AppendCRC(data []byte) []byte {
return AppendCRCWithKey(data, 0)
}
// ValidateAndStrip validates CRC and returns data without CRC suffix
func ValidateAndStrip(data []byte) ([]byte, bool) {
if !ValidateCRC(data) {

View File

@ -57,6 +57,7 @@ func (s *Server) Start() error {
// Main packet receive loop
buffer := make([]byte, 8192)
fmt.Printf("UDP server started, listening for packets...\n")
for s.running {
n, addr, err := s.conn.ReadFromUDP(buffer)
if err != nil {
@ -66,6 +67,8 @@ func (s *Server) Start() error {
continue
}
fmt.Printf("Received UDP packet: %d bytes from %s\n", n, addr.String())
// Handle packet in separate goroutine to avoid blocking
go s.handleIncomingPacket(buffer[:n], addr)
}
@ -91,7 +94,10 @@ func (s *Server) Stop() {
// handleIncomingPacket processes a single UDP packet
func (s *Server) handleIncomingPacket(data []byte, addr *net.UDPAddr) {
fmt.Printf("Processing packet from %s: %d bytes\n", addr.String(), len(data))
if len(data) < 1 {
fmt.Printf("Dropping empty packet from %s\n", addr.String())
return
}
@ -103,17 +109,23 @@ func (s *Server) handleIncomingPacket(data []byte, addr *net.UDPAddr) {
if !exists {
// Check connection limit
if len(s.connections) >= s.config.MaxConnections {
fmt.Printf("Connection limit reached (%d), dropping packet from %s\n", s.config.MaxConnections, addr.String())
s.mutex.Unlock()
return // Drop packet if at capacity
}
fmt.Printf("Creating new connection for %s\n", addr.String())
conn = NewConnection(addr, s.conn, s.handler, s.config)
conn.StartRetransmitLoop()
s.connections[connKey] = conn
fmt.Printf("New connection created for %s (total connections: %d)\n", addr.String(), len(s.connections))
} else {
fmt.Printf("Using existing connection for %s\n", addr.String())
}
s.mutex.Unlock()
// Process packet
fmt.Printf("Sending packet to connection handler for %s\n", addr.String())
conn.ProcessPacket(data)
}

20
test_login_config.json Normal file
View File

@ -0,0 +1,20 @@
{
"listen_addr": "127.0.0.1",
"listen_port": 5999,
"max_clients": 100,
"web_addr": "127.0.0.1",
"web_port": 0,
"web_cert_file": "",
"web_key_file": "",
"web_key_password": "",
"web_user": "",
"web_password": "",
"database_address": "127.0.0.1",
"database_port": 3306,
"database_username": "root",
"database_password": "Sky12!",
"database_name": "eq2emu",
"server_name": "EQ2Go Test Login Server",
"log_level": "info",
"world_servers": []
}