s-ui/api/apiV2Handler.go
2026-09-16 00:53:43 +02:00

167 lines
3.9 KiB
Go

package api
import (
"crypto/subtle"
"encoding/json"
"sync"
"time"
"github.com/alireza0/s-ui/logger"
"github.com/alireza0/s-ui/util/common"
"github.com/gin-gonic/gin"
)
type TokenInMemory struct {
Token string
Expiry int64
Username string
}
type APIv2Handler struct {
ApiService
// tokensMu guards tokens. Every request reads the slice while addToken and
// deleteToken replace it from a gin handler on another connection.
tokensMu sync.RWMutex
tokens []TokenInMemory
}
func NewAPIv2Handler(g *gin.RouterGroup) *APIv2Handler {
a := &APIv2Handler{}
a.ReloadTokens()
a.initRouter(g)
return a
}
func (a *APIv2Handler) initRouter(g *gin.RouterGroup) {
g.Use(func(c *gin.Context) {
a.checkToken(c)
})
g.POST("/:postAction", a.postHandler)
g.GET("/:getAction", a.getHandler)
}
func (a *APIv2Handler) postHandler(c *gin.Context) {
username := a.findUsername(c)
action := c.Param("postAction")
switch action {
case "save":
a.ApiService.Save(c, username)
case "restartApp":
a.ApiService.RestartApp(c)
case "restartSb":
a.ApiService.RestartSb(c)
case "maintenance":
a.ApiService.SetMaintenance(c)
case "resetTraffic":
a.ApiService.ResetTraffic(c)
case "linkConvert":
a.ApiService.LinkConvert(c)
case "subConvert":
a.ApiService.SubConvert(c)
case "importdb":
a.ApiService.ImportDb(c)
case "closeSessions":
a.ApiService.CloseSessions(c)
case "getCertPing":
a.ApiService.GetCertPing(c)
default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
}
func (a *APIv2Handler) getHandler(c *gin.Context) {
action := c.Param("getAction")
switch action {
case "load":
a.ApiService.LoadData(c)
case "inbounds", "outbounds", "endpoints", "services", "tls", "clients", "config":
err := a.ApiService.LoadPartialData(c, []string{action})
if err != nil {
jsonMsg(c, action, err)
}
return
case "users":
a.ApiService.GetUsers(c)
case "settings":
a.ApiService.GetSettings(c)
case "stats":
a.ApiService.GetStats(c)
case "status":
a.ApiService.GetStatus(c)
case "onlines":
a.ApiService.GetOnlines(c)
case "sessions":
a.ApiService.GetSessions(c)
case "logs":
a.ApiService.GetLogs(c)
case "changes":
a.ApiService.CheckChanges(c)
case "keypairs":
a.ApiService.GetKeypairs(c)
case "getdb":
a.ApiService.GetDb(c)
case "checkOutbound":
a.ApiService.GetCheckOutbound(c)
default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
}
func (a *APIv2Handler) findUsername(c *gin.Context) string {
token := c.Request.Header.Get("Token")
if token == "" {
return ""
}
a.tokensMu.RLock()
defer a.tokensMu.RUnlock()
now := time.Now().Unix()
for _, t := range a.tokens {
// Expired entries are skipped, not spliced out. Deleting from the
// slice being ranged over shifted every later element back by one and
// skipped it, so one expired token could hide the valid token stored
// immediately after it -- and the write happened with no lock held.
if t.Expiry > 0 && t.Expiry < now {
continue
}
// Constant time, so the response time of a wrong token does not reveal
// how many leading characters were right.
if subtle.ConstantTimeCompare([]byte(t.Token), []byte(token)) == 1 {
return t.Username
}
}
return ""
}
func (a *APIv2Handler) checkToken(c *gin.Context) {
username := a.findUsername(c)
if username != "" {
c.Next()
return
}
jsonMsg(c, "", common.NewError("invalid token"))
c.Abort()
}
func (a *APIv2Handler) ReloadTokens() {
tokens, err := a.ApiService.LoadTokens()
if err != nil {
logger.Error("unable to load tokens: ", err)
return
}
var newTokens []TokenInMemory
if err := json.Unmarshal(tokens, &newTokens); err != nil {
// Published only on success. The old code installed the half-filled
// slice from a failed unmarshal, which revoked every working token.
logger.Error("unable to load tokens: ", err)
return
}
a.tokensMu.Lock()
a.tokens = newTokens
a.tokensMu.Unlock()
}