package handler import ( "encoding/json" "fmt" "net/http" "strconv" "strings" "time" "xboard-go/internal/database" "xboard-go/internal/model" "xboard-go/internal/service" "github.com/gin-gonic/gin" ) func NodeUser(c *gin.Context) { node := c.MustGet("node").(*model.Server) setNodeLastCheck(node) users, err := service.AvailableUsersForNode(node) if err != nil { Fail(c, 500, "failed to fetch users") return } Success(c, gin.H{"users": users}) } func NodeShadowsocksTidalabUser(c *gin.Context) { node := c.MustGet("node").(*model.Server) if node.Type != "shadowsocks" { Fail(c, http.StatusBadRequest, "server is not a shadowsocks node") return } setNodeLastCheck(node) users, err := service.AvailableUsersForNode(node) if err != nil { Fail(c, http.StatusInternalServerError, "failed to fetch users") return } cipher := tidalabProtocolString(node.ProtocolSettings, "cipher") result := make([]gin.H, 0, len(users)) for _, user := range users { result = append(result, gin.H{ "id": user.ID, "port": node.ServerPort, "cipher": cipher, "secret": user.UUID, }) } c.JSON(http.StatusOK, gin.H{"data": result}) } func NodeTrojanTidalabUser(c *gin.Context) { node := c.MustGet("node").(*model.Server) if node.Type != "trojan" { Fail(c, http.StatusBadRequest, "server is not a trojan node") return } setNodeLastCheck(node) users, err := service.AvailableUsersForNode(node) if err != nil { Fail(c, http.StatusInternalServerError, "failed to fetch users") return } result := make([]gin.H, 0, len(users)) for _, user := range users { item := gin.H{ "id": user.ID, "trojan_user": gin.H{"password": user.UUID}, } if user.SpeedLimit != nil { item["speed_limit"] = *user.SpeedLimit } if user.DeviceLimit != nil { item["device_limit"] = *user.DeviceLimit } result = append(result, item) } c.JSON(http.StatusOK, gin.H{ "msg": "ok", "data": result, }) } func NodeConfig(c *gin.Context) { node := c.MustGet("node").(*model.Server) setNodeLastCheck(node) config := service.BuildNodeConfig(node) config["base_config"] = gin.H{ "push_interval": service.MustGetInt("server_push_interval", 60), "pull_interval": service.MustGetInt("server_pull_interval", 60), } c.JSON(http.StatusOK, config) } func NodeTrojanTidalabConfig(c *gin.Context) { node := c.MustGet("node").(*model.Server) if node.Type != "trojan" { Fail(c, http.StatusBadRequest, "server is not a trojan node") return } setNodeLastCheck(node) localPort, err := strconv.Atoi(strings.TrimSpace(c.Query("local_port"))) if err != nil || localPort <= 0 { Fail(c, http.StatusBadRequest, "local_port is required") return } serverName := tidalabProtocolString(node.ProtocolSettings, "server_name") if serverName == "" { serverName = strings.TrimSpace(node.Host) } if serverName == "" { serverName = "domain.com" } c.JSON(http.StatusOK, gin.H{ "run_type": "server", "local_addr": "0.0.0.0", "local_port": node.ServerPort, "remote_addr": "www.taobao.com", "remote_port": 80, "password": []string{}, "ssl": gin.H{ "cert": "server.crt", "key": "server.key", "sni": serverName, }, "api": gin.H{ "enabled": true, "api_addr": "127.0.0.1", "api_port": localPort, }, }) } func NodePush(c *gin.Context) { node := c.MustGet("node").(*model.Server) var payload map[string][]int64 if err := c.ShouldBindJSON(&payload); err != nil { Fail(c, 400, "invalid payload") return } for userIDRaw, traffic := range payload { if len(traffic) != 2 { continue } userID, err := strconv.Atoi(userIDRaw) if err != nil { continue } service.ApplyTrafficDelta(userID, node, traffic[0], traffic[1]) } setNodeLastPush(node, len(payload)) Success(c, true) } func NodeTidalabSubmit(c *gin.Context) { node := c.MustGet("node").(*model.Server) if node.Type != "shadowsocks" && node.Type != "trojan" { Fail(c, http.StatusBadRequest, "server type is not supported by tidalab submit") return } var payload []struct { UserID int `json:"user_id"` U int64 `json:"u"` D int64 `json:"d"` } if err := c.ShouldBindJSON(&payload); err != nil { Fail(c, http.StatusBadRequest, "invalid payload") return } for _, item := range payload { if item.UserID <= 0 { continue } service.ApplyTrafficDelta(item.UserID, node, item.U, item.D) } setNodeLastPush(node, len(payload)) c.JSON(http.StatusOK, gin.H{ "ret": 1, "msg": "ok", }) } func NodeAlive(c *gin.Context) { node := c.MustGet("node").(*model.Server) var payload map[string][]string if err := c.ShouldBindJSON(&payload); err != nil { Fail(c, 400, "invalid payload") return } for userIDRaw, ips := range payload { userID, err := strconv.Atoi(userIDRaw) if err != nil { continue } _ = service.SetDevices(userID, node.ID, ips) } Success(c, true) } func NodeAliveList(c *gin.Context) { node := c.MustGet("node").(*model.Server) users, err := service.AvailableUsersForNode(node) if err != nil { Fail(c, 500, "failed to fetch users") return } userIDs := make([]int, 0, len(users)) for _, user := range users { if user.DeviceLimit != nil && *user.DeviceLimit > 0 { userIDs = append(userIDs, user.ID) } } devices := service.GetUsersDevices(userIDs) alive := make(map[string][]string, len(devices)) for userID, ips := range devices { alive[strconv.Itoa(userID)] = ips } Success(c, gin.H{"alive": alive}) } func NodeStatus(c *gin.Context) { node := c.MustGet("node").(*model.Server) var status map[string]any if err := c.ShouldBindJSON(&status); err != nil { Fail(c, 400, "invalid payload") return } cacheTime := time.Duration(maxInt(300, service.MustGetInt("server_push_interval", 60)*3)) * time.Second _ = database.CacheSet(nodeLoadStatusKey(node), gin.H{ "cpu": status["cpu"], "mem": status["mem"], "swap": status["swap"], "disk": status["disk"], "kernel_status": status["kernel_status"], "updated_at": time.Now().Unix(), }, cacheTime) _ = database.CacheSet(nodeLastLoadKey(node), time.Now().Unix(), cacheTime) c.JSON(http.StatusOK, gin.H{"data": true, "code": 0, "message": "success"}) } func NodeHandshake(c *gin.Context) { websocket := gin.H{"enabled": false} if service.MustGetBool("server_ws_enable", true) { wsURL := strings.TrimSpace(service.MustGetString("server_ws_url", "")) if wsURL == "" { scheme := "ws" if c.Request.TLS != nil { scheme = "wss" } wsURL = fmt.Sprintf("%s://%s:8076", scheme, c.Request.Host) } websocket = gin.H{ "enabled": true, "ws_url": strings.TrimRight(wsURL, "/"), } } Success(c, gin.H{"websocket": websocket}) } func NodeReport(c *gin.Context) { node := c.MustGet("node").(*model.Server) setNodeLastCheck(node) var payload struct { Traffic map[string][]int64 `json:"traffic"` Alive map[string][]string `json:"alive"` Online map[string]int `json:"online"` Status map[string]any `json:"status"` Metrics map[string]any `json:"metrics"` } if err := c.ShouldBindJSON(&payload); err != nil { Fail(c, 400, "invalid payload") return } if len(payload.Traffic) > 0 { for userIDRaw, traffic := range payload.Traffic { if len(traffic) != 2 { continue } userID, err := strconv.Atoi(userIDRaw) if err != nil { continue } service.ApplyTrafficDelta(userID, node, traffic[0], traffic[1]) } setNodeLastPush(node, len(payload.Traffic)) } if len(payload.Alive) > 0 { for userIDRaw, ips := range payload.Alive { userID, err := strconv.Atoi(userIDRaw) if err != nil { continue } _ = service.SetDevices(userID, node.ID, ips) } } if len(payload.Online) > 0 { cacheTime := time.Duration(maxInt(300, service.MustGetInt("server_push_interval", 60)*3)) * time.Second for userIDRaw, conn := range payload.Online { key := fmt.Sprintf("USER_ONLINE_CONN_%s_%d_%s", strings.ToUpper(node.Type), node.ID, userIDRaw) _ = database.CacheSet(key, conn, cacheTime) } } if len(payload.Status) > 0 { cacheTime := time.Duration(maxInt(300, service.MustGetInt("server_push_interval", 60)*3)) * time.Second _ = database.CacheSet(nodeLoadStatusKey(node), gin.H{ "cpu": payload.Status["cpu"], "mem": payload.Status["mem"], "swap": payload.Status["swap"], "disk": payload.Status["disk"], "kernel_status": payload.Status["kernel_status"], "updated_at": time.Now().Unix(), }, cacheTime) _ = database.CacheSet(nodeLastLoadKey(node), time.Now().Unix(), cacheTime) } if len(payload.Metrics) > 0 { cacheTime := time.Duration(maxInt(300, service.MustGetInt("server_push_interval", 60)*3)) * time.Second payload.Metrics["updated_at"] = time.Now().Unix() _ = database.CacheSet(nodeMetricsKey(node), payload.Metrics, cacheTime) } Success(c, true) } func setNodeLastCheck(node *model.Server) { _ = database.CacheSet(nodeLastCheckKey(node), time.Now().Unix(), time.Hour) } func setNodeLastPush(node *model.Server, onlineUsers int) { _ = database.CacheSet(nodeOnlineKey(node), onlineUsers, time.Hour) _ = database.CacheSet(nodeLastPushKey(node), time.Now().Unix(), time.Hour) } func nodeLastCheckKey(node *model.Server) string { return fmt.Sprintf("SERVER_%s_LAST_CHECK_AT_%d", strings.ToUpper(node.Type), node.ID) } func nodeLastPushKey(node *model.Server) string { return fmt.Sprintf("SERVER_%s_LAST_PUSH_AT_%d", strings.ToUpper(node.Type), node.ID) } func nodeOnlineKey(node *model.Server) string { return fmt.Sprintf("SERVER_%s_ONLINE_USER_%d", strings.ToUpper(node.Type), node.ID) } func nodeLoadStatusKey(node *model.Server) string { return fmt.Sprintf("SERVER_%s_LOAD_STATUS_%d", strings.ToUpper(node.Type), node.ID) } func nodeLastLoadKey(node *model.Server) string { return fmt.Sprintf("SERVER_%s_LAST_LOAD_AT_%d", strings.ToUpper(node.Type), node.ID) } func nodeMetricsKey(node *model.Server) string { return fmt.Sprintf("SERVER_%s_METRICS_%d", strings.ToUpper(node.Type), node.ID) } func maxInt(a, b int) int { if a > b { return a } return b } func tidalabProtocolString(raw *string, key string) string { if raw == nil || strings.TrimSpace(*raw) == "" { return "" } decoded := map[string]any{} if err := json.Unmarshal([]byte(*raw), &decoded); err != nil { return "" } value, _ := decoded[key].(string) return strings.TrimSpace(value) }