Files
SingBox-Gopanel/internal/handler/node_api.go
CN-JS-HuiBai 1ed31b9292
All checks were successful
build / build (api, amd64, linux) (push) Successful in -47s
build / build (api, arm64, linux) (push) Successful in -48s
build / build (api.exe, amd64, windows) (push) Successful in -47s
first commit
2026-04-17 09:49:16 +08:00

411 lines
10 KiB
Go

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