1
0
2025-07-02 13:14:42 -05:00

264 lines
6.1 KiB
Go

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
}
}`