significant work on login
This commit is contained in:
parent
ab2b2600d0
commit
bc6ba6ed6c
@ -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("================================================================================")
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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
20
test_login_config.json
Normal 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": []
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user