first commit
This commit is contained in:
410
internal/handler/node_api.go
Normal file
410
internal/handler/node_api.go
Normal file
@@ -0,0 +1,410 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user