package main import ( "encoding/base64" "strings" "time" "github.com/goccy/go-json" "github.com/valyala/fasthttp" ) type WebRouter struct { server *LoginServer } func (wr *WebRouter) Handler(ctx *fasthttp.RequestCtx) { path := string(ctx.Path()) // Basic auth check if !wr.checkAuth(ctx) { ctx.Response.Header.Set("WWW-Authenticate", `Basic realm="EQ2 Login Server"`) ctx.SetStatusCode(fasthttp.StatusUnauthorized) ctx.SetBodyString("Unauthorized") return } switch path { case "/": wr.handleStatus(ctx) case "/status": wr.handleStatus(ctx) case "/worlds": wr.handleWorlds(ctx) case "/clients": wr.handleClients(ctx) case "/stats": wr.handleStats(ctx) default: ctx.SetStatusCode(fasthttp.StatusNotFound) ctx.SetBodyString("Not Found") } } func (wr *WebRouter) checkAuth(ctx *fasthttp.RequestCtx) bool { if wr.server.config.Web.Username == "" { return true // No auth required } auth := ctx.Request.Header.Peek("Authorization") if len(auth) == 0 { return false } if !strings.HasPrefix(string(auth), "Basic ") { return false } encoded := string(auth[6:]) decoded, err := base64.StdEncoding.DecodeString(encoded) if err != nil { return false } parts := strings.SplitN(string(decoded), ":", 2) if len(parts) != 2 { return false } return parts[0] == wr.server.config.Web.Username && parts[1] == wr.server.config.Web.Password } func (wr *WebRouter) handleStatus(ctx *fasthttp.RequestCtx) { ctx.Response.Header.Set("Content-Type", "application/json") totalClients, authClients := wr.server.clients.GetStats() totalWorlds, onlineWorlds := wr.server.worlds.GetStats() conn := wr.server.db.Get(wr.server.ctx) defer wr.server.db.Put(conn) totalAccounts, totalChars, _ := getAccountStats(conn) status := map[string]any{ "server_name": "EQ2 Login Server", "status": "online", "uptime": time.Since(wr.server.startTime).Seconds(), "uptime_string": formatDuration(time.Since(wr.server.startTime)), "version": "1.0.0-go", "clients": map[string]int{ "total": totalClients, "authenticated": authClients, }, "worlds": map[string]int{ "total": totalWorlds, "online": onlineWorlds, }, "database": map[string]int{ "accounts": totalAccounts, "characters": totalChars, }, "timestamp": time.Now().Unix(), } data, _ := json.Marshal(status) ctx.SetBody(data) } func (wr *WebRouter) handleWorlds(ctx *fasthttp.RequestCtx) { ctx.Response.Header.Set("Content-Type", "application/json") worlds := wr.server.worlds.GetWorldList() response := map[string]any{ "worlds": worlds, "count": len(worlds), } data, _ := json.Marshal(response) ctx.SetBody(data) } func (wr *WebRouter) handleClients(ctx *fasthttp.RequestCtx) { ctx.Response.Header.Set("Content-Type", "application/json") wr.server.clients.mu.RLock() defer wr.server.clients.mu.RUnlock() clients := make([]map[string]any, 0, len(wr.server.clients.clients)) for _, client := range wr.server.clients.clients { clients = append(clients, map[string]any{ "address": client.address, "connected": client.connected.Unix(), "state": stateToString(client.state), "account_name": client.accountName, "version": client.version, }) } response := map[string]any{ "clients": clients, "count": len(clients), } data, _ := json.Marshal(response) ctx.SetBody(data) } func (wr *WebRouter) handleStats(ctx *fasthttp.RequestCtx) { ctx.Response.Header.Set("Content-Type", "application/json") // Get comprehensive stats totalClients, authClients := wr.server.clients.GetStats() totalWorlds, onlineWorlds := wr.server.worlds.GetStats() conn := wr.server.db.Get(wr.server.ctx) defer wr.server.db.Put(conn) totalAccounts, totalChars, _ := getAccountStats(conn) stats := map[string]any{ "server": map[string]any{ "uptime": time.Since(wr.server.startTime).Seconds(), "start_time": wr.server.startTime.Unix(), "version": "1.0.0-go", }, "clients": map[string]any{ "total": totalClients, "authenticated": authClients, "guest": totalClients - authClients, }, "worlds": map[string]any{ "total": totalWorlds, "online": onlineWorlds, "offline": totalWorlds - onlineWorlds, }, "database": map[string]any{ "accounts": totalAccounts, "characters": totalChars, "avg_chars": float64(totalChars) / float64(max(totalAccounts, 1)), }, "config": map[string]any{ "max_clients": wr.server.config.Server.MaxClients, "max_world_servers": wr.server.config.Server.MaxWorldServers, "allow_account_creation": wr.server.config.Game.AllowAccountCreation, "max_chars_per_account": wr.server.config.Game.MaxCharactersPerAccount, }, } data, _ := json.Marshal(stats) ctx.SetBody(data) } func stateToString(state ClientState) string { switch state { case StateConnected: return "connected" case StateAuthenticated: return "authenticated" case StateCharacterSelect: return "character_select" case StateDisconnected: return "disconnected" default: return "unknown" } } func formatDuration(d time.Duration) string { days := int(d.Hours()) / 24 hours := int(d.Hours()) % 24 minutes := int(d.Minutes()) % 60 if days > 0 { return formatTime(days, "day") + ", " + formatTime(hours, "hour") + ", " + formatTime(minutes, "minute") } if hours > 0 { return formatTime(hours, "hour") + ", " + formatTime(minutes, "minute") } return formatTime(minutes, "minute") } func formatTime(value int, unit string) string { if value == 1 { return "1 " + unit } return string(rune(value)) + " " + unit + "s" } func max(a, b int) int { if a > b { return a } return b } // Example login_config.json file const ExampleConfig = `{ "server": { "port": 5998, "max_clients": 1000, "max_world_servers": 10 }, "database": { "path": "login.db" }, "web": { "enabled": true, "address": "0.0.0.0", "port": 8080, "username": "admin", "password": "password" }, "game": { "allow_account_creation": true, "expansion_flag": 32767, "cities_flag": 255, "default_subscription_level": 4294967295, "enabled_races": 65535, "max_characters_per_account": 7 } }`