418 lines
10 KiB
Go
418 lines
10 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"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)
|
|
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)
|
|
setNodeLastCheck(node)
|
|
|
|
if c.Request.Method == http.MethodGet {
|
|
Success(c, true)
|
|
return
|
|
}
|
|
|
|
var payload map[string][]string
|
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
Success(c, true)
|
|
return
|
|
}
|
|
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)
|
|
}
|