264 lines
6.1 KiB
Go
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
|
|
}
|
|
}`
|