eq2go/internal/login/client.go

632 lines
18 KiB
Go

package login
import (
"crypto/md5"
"encoding/binary"
"fmt"
"log"
"strings"
"sync"
"time"
"eq2emu/internal/packets"
"eq2emu/internal/udp"
)
// ClientState represents the current state of a login client
type ClientState int
const (
ClientStateNew ClientState = iota
ClientStateAuthenticating
ClientStateAuthenticated
ClientStateCharacterSelect
ClientStateDisconnected
)
// String returns the string representation of the client state
func (cs ClientState) String() string {
switch cs {
case ClientStateNew:
return "New"
case ClientStateAuthenticating:
return "Authenticating"
case ClientStateAuthenticated:
return "Authenticated"
case ClientStateCharacterSelect:
return "CharacterSelect"
case ClientStateDisconnected:
return "Disconnected"
default:
return "Unknown"
}
}
// Client represents a connected login client
type Client struct {
connection *udp.Connection
database *LoginDB
// Client information
accountID int32
accountName string
accountEmail string
accessLevel int16
// Authentication data
loginKey string
sessionKey string
ipAddress string
// Client state
state ClientState
clientVersion uint16
protocolVersion uint16
// Character data
characters []CharacterInfo
// Timing
connectTime time.Time
lastActivity time.Time
// Synchronization
mu sync.RWMutex
}
// CharacterInfo represents character information for the character select screen
type CharacterInfo struct {
ID int32 `json:"id"`
AccountID int32 `json:"account_id"`
Name string `json:"name"`
Race int8 `json:"race"`
Class int8 `json:"class"`
Gender int8 `json:"gender"`
Level int16 `json:"level"`
Zone int32 `json:"zone"`
ZoneInstance int32 `json:"zone_instance"`
ServerID int16 `json:"server_id"`
LastPlayed int64 `json:"last_played"`
CreatedDate int64 `json:"created_date"`
DeletedDate int64 `json:"deleted_date"`
// Appearance data
ModelType int16 `json:"model_type"`
SogaModelType int16 `json:"soga_model_type"`
HeadType int16 `json:"head_type"`
SogaHeadType int16 `json:"soga_head_type"`
WingType int16 `json:"wing_type"`
ChestType int16 `json:"chest_type"`
LegsType int16 `json:"legs_type"`
SogaChestType int16 `json:"soga_chest_type"`
SogaLegsType int16 `json:"soga_legs_type"`
HairType int16 `json:"hair_type"`
FacialHairType int16 `json:"facial_hair_type"`
SogaHairType int16 `json:"soga_hair_type"`
SogaFacialHairType int16 `json:"soga_facial_hair_type"`
// Colors
HairTypeColor int16 `json:"hair_type_color"`
HairTypeHighlight int16 `json:"hair_type_highlight"`
HairColor1 int16 `json:"hair_color1"`
HairColor2 int16 `json:"hair_color2"`
HairHighlight int16 `json:"hair_highlight"`
EyeColor1 int16 `json:"eye_color1"`
EyeColor2 int16 `json:"eye_color2"`
SkinColor int16 `json:"skin_color"`
// Soga colors
SogaHairTypeColor int16 `json:"soga_hair_type_color"`
SogaHairTypeHighlight int16 `json:"soga_hair_type_highlight"`
SogaHairColor1 int16 `json:"soga_hair_color1"`
SogaHairColor2 int16 `json:"soga_hair_color2"`
SogaHairHighlight int16 `json:"soga_hair_highlight"`
SogaEyeColor1 int16 `json:"soga_eye_color1"`
SogaEyeColor2 int16 `json:"soga_eye_color2"`
SogaSkinColor int16 `json:"soga_skin_color"`
// Additional data
CurrentLanguage int8 `json:"current_language"`
ChosenLanguage int8 `json:"chosen_language"`
}
// NewClient creates a new login client
func NewClient(conn *udp.Connection, db *LoginDB) *Client {
now := time.Now()
client := &Client{
connection: conn,
database: db,
state: ClientStateNew,
connectTime: now,
lastActivity: now,
ipAddress: conn.GetClientAddr().String(),
characters: make([]CharacterInfo, 0),
}
return client
}
// HandlePacket processes an incoming packet from the client
func (c *Client) HandlePacket(data []byte) error {
c.mu.Lock()
defer c.mu.Unlock()
c.lastActivity = time.Now()
if len(data) < 2 {
return fmt.Errorf("packet too short")
}
// Extract opcode
opcode := binary.LittleEndian.Uint16(data[:2])
payload := data[2:]
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 0x0200: // OP_Login2 - Primary login authentication
return c.handleLoginRequest(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 (OP_Login2)
func (c *Client) handleLoginRequest(payload []byte) error {
if c.state != ClientStateNew {
return fmt.Errorf("invalid state for login request: %s", c.state)
}
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")
}
// 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
// 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)
}
log.Printf("Client %s: Attempting login for user '%s'", c.ipAddress, username)
// 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
func getStringField(data map[string]any, field string) string {
if val, ok := data[field]; ok {
if str, ok := val.(string); ok {
return str
}
}
return ""
}
// authenticateUser authenticates the user credentials
func (c *Client) authenticateUser(username, password string) error {
username = strings.TrimSpace(strings.Trim(username, "\x00"))
// Hash the password (MD5 for EQ2 compatibility)
hasher := md5.New()
hasher.Write([]byte(password))
hashedPassword := fmt.Sprintf("%x", hasher.Sum(nil))
// Query database for account
account, err := c.database.GetLoginAccount(username, hashedPassword)
if err != nil {
log.Printf("Authentication failed for %s: %v", username, err)
return c.sendLoginReply(0, "Invalid username or password")
}
// Check account status
if account.Status != "Active" {
log.Printf("Account %s is not active: %s", username, account.Status)
return c.sendLoginReply(0, "Account is suspended")
}
// Store account information
c.accountID = account.ID
c.accountName = account.Username
c.accountEmail = account.Email
c.accessLevel = account.AccessLevel
c.state = ClientStateAuthenticated
log.Printf("User %s (ID: %d) authenticated successfully", username, account.ID)
// Generate session key
c.sessionKey = c.generateSessionKey()
// Update last login
c.database.UpdateLastLogin(c.accountID, c.ipAddress)
// Send successful login reply
return c.sendLoginReply(1, "Welcome to EverQuest II")
}
// sendLoginReply sends a login reply to the client
func (c *Client) sendLoginReply(success int8, message string) error {
// Build login reply using the packet system
clientVersion := uint32(562) // TODO: Track actual client version
data := map[string]any{
"login_response": success,
"unknown": message,
}
if success == 1 {
data["account_id"] = c.accountID
// Add other required fields for successful login
data["parental_control_flag"] = uint8(0)
data["parental_control_timer"] = uint32(0)
// TODO: Add more fields as needed based on client version
}
packet, err := packets.BuildPacket("LoginReplyMsg", data, clientVersion, 0)
if err != nil {
return fmt.Errorf("failed to build login reply packet: %w", err)
}
// Send the packet
appPacket := &udp.ApplicationPacket{
Data: packet,
}
c.connection.SendPacket(appPacket)
// If login successful, send character list
if success == 1 {
if err := c.loadCharacters(); err != nil {
log.Printf("Failed to load characters: %v", err)
}
return c.sendCharacterList()
}
return nil
}
// loadCharacters loads the character list for this account
func (c *Client) loadCharacters() error {
characters, err := c.database.GetCharacters(c.accountID)
if err != nil {
return fmt.Errorf("failed to load characters: %w", err)
}
c.characters = make([]CharacterInfo, len(characters))
for i, char := range characters {
c.characters[i] = CharacterInfo{
ID: char.ID,
AccountID: char.AccountID,
Name: strings.TrimSpace(strings.Trim(char.Name, "\x00")),
Race: char.Race,
Class: char.Class,
Gender: char.Gender,
Level: char.Level,
Zone: char.Zone,
ServerID: char.ServerID,
LastPlayed: char.LastPlayed,
CreatedDate: char.CreatedDate,
}
}
return nil
}
// handleCharacterSelectRequest handles character selection
func (c *Client) handleCharacterSelectRequest(payload []byte) error {
if c.state != ClientStateAuthenticated {
return fmt.Errorf("invalid state for character select: %s", c.state)
}
c.state = ClientStateCharacterSelect
// Send character list
if err := c.loadCharacters(); err != nil {
return fmt.Errorf("failed to load characters: %w", err)
}
return c.sendCharacterList()
}
// sendCharacterList sends the character list to the client
func (c *Client) sendCharacterList() error {
// For now, send character profiles individually using CharSelectProfile packet
// In the real implementation, this would be sent as part of LoginReplyMsg
for _, char := range c.characters {
data := map[string]any{
"version": uint32(562),
"charid": uint32(char.ID),
"server_id": uint32(char.ServerID),
"name": char.Name,
"unknown": uint8(0),
"race": uint8(char.Race),
"class": uint8(char.Class),
"gender": uint8(char.Gender),
"level": uint32(char.Level),
"zone": "Qeynos Harbor", // TODO: Get actual zone name
"unknown1": uint32(0),
"unknown2": uint32(0),
"created_date": uint32(char.CreatedDate),
"last_played": uint32(char.LastPlayed),
"unknown3": uint32(0),
"unknown4": uint32(0),
"zonename2": "Qeynos Harbor",
"zonedesc": "The Harbor District",
"unknown5": uint32(0),
"server_name": "EQ2Go Server",
"account_id": uint32(c.accountID),
}
// Add appearance data with defaults for now
// TODO: Load actual character appearance data
clientVersion := uint32(562)
packet, err := packets.BuildPacket("CharSelectProfile", data, clientVersion, 0)
if err != nil {
log.Printf("Failed to build character profile packet: %v", err)
continue
}
appPacket := &udp.ApplicationPacket{
Data: packet,
}
c.connection.SendPacket(appPacket)
}
return nil
}
// handlePlayCharacterRequest handles play character request
func (c *Client) handlePlayCharacterRequest(payload []byte) error {
if c.state != ClientStateCharacterSelect && c.state != ClientStateAuthenticated {
return fmt.Errorf("invalid state for play character: %s", c.state)
}
if len(payload) < 8 {
return fmt.Errorf("play character packet too short")
}
characterID := binary.LittleEndian.Uint32(payload[:4])
serverID := binary.LittleEndian.Uint16(payload[4:6])
log.Printf("Play character request - Character: %d, Server: %d", characterID, serverID)
// Find character
var character *CharacterInfo
for i := range c.characters {
if c.characters[i].ID == int32(characterID) {
character = &c.characters[i]
break
}
}
if character == nil {
return fmt.Errorf("character %d not found", characterID)
}
// TODO: Forward to world server
return c.sendPlayCharacterReply(character, "127.0.0.1", 9000)
}
// sendPlayCharacterReply sends play character reply to client
func (c *Client) sendPlayCharacterReply(character *CharacterInfo, worldIP string, worldPort int) error {
data := map[string]any{
"response": uint8(1), // Success
"server": worldIP,
"port": uint16(worldPort),
"account_id": uint32(c.accountID),
"access_code": uint32(12345), // TODO: Generate proper access code
}
clientVersion := uint32(562)
packet, err := packets.BuildPacket("PlayResponse", data, clientVersion, 0)
if err != nil {
return fmt.Errorf("failed to build play response packet: %w", err)
}
appPacket := &udp.ApplicationPacket{
Data: packet,
}
c.connection.SendPacket(appPacket)
return nil
}
// handleDeleteCharacterRequest handles character deletion
func (c *Client) handleDeleteCharacterRequest(payload []byte) error {
// TODO: Implement character deletion
return nil
}
// handleCreateCharacterRequest handles character creation
func (c *Client) handleCreateCharacterRequest(payload []byte) error {
// TODO: Implement character creation
return nil
}
// generateSessionKey generates a unique session key for this client
func (c *Client) generateSessionKey() string {
now := time.Now()
data := fmt.Sprintf("%d-%s-%d", c.accountID, c.ipAddress, now.Unix())
hasher := md5.New()
hasher.Write([]byte(data))
return fmt.Sprintf("%x", hasher.Sum(nil))
}
// Disconnect disconnects the client
func (c *Client) Disconnect(reason string) {
c.mu.Lock()
defer c.mu.Unlock()
if c.state == ClientStateDisconnected {
return
}
log.Printf("Disconnecting client %s: %s", c.ipAddress, reason)
c.state = ClientStateDisconnected
if c.connection != nil {
c.connection.Close()
}
}
// GetState returns the current client state (thread-safe)
func (c *Client) GetState() ClientState {
c.mu.RLock()
defer c.mu.RUnlock()
return c.state
}
// GetAccountID returns the account ID (thread-safe)
func (c *Client) GetAccountID() int32 {
c.mu.RLock()
defer c.mu.RUnlock()
return c.accountID
}
// GetAccountName returns the account name (thread-safe)
func (c *Client) GetAccountName() string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.accountName
}
// GetIPAddress returns the client IP address
func (c *Client) GetIPAddress() string {
return c.ipAddress
}
// GetConnection returns the UDP connection
func (c *Client) GetConnection() *udp.Connection {
return c.connection
}
// GetConnectTime returns when the client connected
func (c *Client) GetConnectTime() time.Time {
return c.connectTime
}
// GetLastActivity returns the last activity time
func (c *Client) GetLastActivity() time.Time {
c.mu.RLock()
defer c.mu.RUnlock()
return c.lastActivity
}
// IsTimedOut returns whether the client has timed out
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
}