This commit is contained in:
admin8800
2026-05-10 06:41:44 +00:00
parent d104b6e40d
commit 3eb70ee9ed
252 changed files with 42215 additions and 2 deletions
+107
View File
@@ -0,0 +1,107 @@
package api
import (
"strings"
"github.com/alireza0/s-ui/util/common"
"github.com/gin-gonic/gin"
)
type APIHandler struct {
ApiService
apiv2 *APIv2Handler
}
func NewAPIHandler(g *gin.RouterGroup, a2 *APIv2Handler) {
a := &APIHandler{
apiv2: a2,
}
a.initRouter(g)
}
func (a *APIHandler) initRouter(g *gin.RouterGroup) {
g.Use(func(c *gin.Context) {
path := c.Request.URL.Path
if !strings.HasSuffix(path, "login") && !strings.HasSuffix(path, "logout") {
checkLogin(c)
}
})
g.POST("/:postAction", a.postHandler)
g.GET("/:getAction", a.getHandler)
}
func (a *APIHandler) postHandler(c *gin.Context) {
loginUser := GetLoginUser(c)
action := c.Param("postAction")
switch action {
case "login":
a.ApiService.Login(c)
case "changePass":
a.ApiService.ChangePass(c)
case "save":
a.ApiService.Save(c, loginUser)
case "restartApp":
a.ApiService.RestartApp(c)
case "restartSb":
a.ApiService.RestartSb(c)
case "linkConvert":
a.ApiService.LinkConvert(c)
case "subConvert":
a.ApiService.SubConvert(c)
case "importdb":
a.ApiService.ImportDb(c)
case "addToken":
a.ApiService.AddToken(c)
a.apiv2.ReloadTokens()
case "deleteToken":
a.ApiService.DeleteToken(c)
a.apiv2.ReloadTokens()
default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
}
func (a *APIHandler) getHandler(c *gin.Context) {
action := c.Param("getAction")
switch action {
case "logout":
a.ApiService.Logout(c)
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 "logs":
a.ApiService.GetLogs(c)
case "changes":
a.ApiService.CheckChanges(c)
case "keypairs":
a.ApiService.GetKeypairs(c)
case "getdb":
a.ApiService.GetDb(c)
case "tokens":
a.ApiService.GetTokens(c)
case "singbox-config":
a.ApiService.GetSingboxConfig(c)
case "checkOutbound":
a.ApiService.GetCheckOutbound(c)
default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
}
+405
View File
@@ -0,0 +1,405 @@
package api
import (
"encoding/json"
"strconv"
"time"
"github.com/alireza0/s-ui/database"
"github.com/alireza0/s-ui/logger"
"github.com/alireza0/s-ui/service"
"github.com/alireza0/s-ui/util"
"github.com/gin-gonic/gin"
)
type ApiService struct {
service.SettingService
service.UserService
service.ConfigService
service.ClientService
service.TlsService
service.InboundService
service.OutboundService
service.EndpointService
service.ServicesService
service.PanelService
service.StatsService
service.ServerService
}
func (a *ApiService) LoadData(c *gin.Context) {
data, err := a.getData(c)
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, data, nil)
}
func (a *ApiService) getData(c *gin.Context) (interface{}, error) {
data := make(map[string]interface{}, 0)
lu := c.Query("lu")
isUpdated, err := a.ConfigService.CheckChanges(lu)
if err != nil {
return "", err
}
onlines, err := a.StatsService.GetOnlines()
sysInfo := a.ServerService.GetSingboxInfo()
if sysInfo["running"] == false {
logs := a.ServerService.GetLogs("1", "debug")
if len(logs) > 0 {
data["lastLog"] = logs[0]
}
}
if err != nil {
return "", err
}
if isUpdated {
config, err := a.SettingService.GetConfig()
if err != nil {
return "", err
}
clients, err := a.ClientService.GetAll()
if err != nil {
return "", err
}
tlsConfigs, err := a.TlsService.GetAll()
if err != nil {
return "", err
}
inbounds, err := a.InboundService.GetAll()
if err != nil {
return "", err
}
outbounds, err := a.OutboundService.GetAll()
if err != nil {
return "", err
}
endpoints, err := a.EndpointService.GetAll()
if err != nil {
return "", err
}
services, err := a.ServicesService.GetAll()
if err != nil {
return "", err
}
subURI, err := a.SettingService.GetFinalSubURI(getHostname(c))
if err != nil {
return "", err
}
trafficAge, err := a.SettingService.GetTrafficAge()
if err != nil {
return "", err
}
data["config"] = json.RawMessage(config)
data["clients"] = clients
data["tls"] = tlsConfigs
data["inbounds"] = inbounds
data["outbounds"] = outbounds
data["endpoints"] = endpoints
data["services"] = services
data["subURI"] = subURI
data["enableTraffic"] = trafficAge > 0
data["onlines"] = onlines
} else {
data["onlines"] = onlines
}
return data, nil
}
func (a *ApiService) LoadPartialData(c *gin.Context, objs []string) error {
data := make(map[string]interface{}, 0)
id := c.Query("id")
for _, obj := range objs {
switch obj {
case "inbounds":
inbounds, err := a.InboundService.Get(id)
if err != nil {
return err
}
data[obj] = inbounds
case "outbounds":
outbounds, err := a.OutboundService.GetAll()
if err != nil {
return err
}
data[obj] = outbounds
case "endpoints":
endpoints, err := a.EndpointService.GetAll()
if err != nil {
return err
}
data[obj] = endpoints
case "services":
services, err := a.ServicesService.GetAll()
if err != nil {
return err
}
data[obj] = services
case "tls":
tlsConfigs, err := a.TlsService.GetAll()
if err != nil {
return err
}
data[obj] = tlsConfigs
case "clients":
clients, err := a.ClientService.Get(id)
if err != nil {
return err
}
data[obj] = clients
case "config":
config, err := a.SettingService.GetConfig()
if err != nil {
return err
}
data[obj] = json.RawMessage(config)
case "settings":
settings, err := a.SettingService.GetAllSetting()
if err != nil {
return err
}
data[obj] = settings
}
}
jsonObj(c, data, nil)
return nil
}
func (a *ApiService) GetUsers(c *gin.Context) {
users, err := a.UserService.GetUsers()
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, *users, nil)
}
func (a *ApiService) GetSettings(c *gin.Context) {
data, err := a.SettingService.GetAllSetting()
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, data, err)
}
func (a *ApiService) GetStats(c *gin.Context) {
resource := c.Query("resource")
tag := c.Query("tag")
limit, err := strconv.Atoi(c.Query("limit"))
if err != nil {
limit = 100
}
data, err := a.StatsService.GetStats(resource, tag, limit)
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, data, err)
}
func (a *ApiService) GetStatus(c *gin.Context) {
request := c.Query("r")
result := a.ServerService.GetStatus(request)
jsonObj(c, result, nil)
}
func (a *ApiService) GetOnlines(c *gin.Context) {
onlines, err := a.StatsService.GetOnlines()
jsonObj(c, onlines, err)
}
func (a *ApiService) GetLogs(c *gin.Context) {
count := c.Query("c")
level := c.Query("l")
logs := a.ServerService.GetLogs(count, level)
jsonObj(c, logs, nil)
}
func (a *ApiService) CheckChanges(c *gin.Context) {
actor := c.Query("a")
chngKey := c.Query("k")
count := c.Query("c")
changes := a.ConfigService.GetChanges(actor, chngKey, count)
jsonObj(c, changes, nil)
}
func (a *ApiService) GetKeypairs(c *gin.Context) {
kType := c.Query("k")
options := c.Query("o")
keypair := a.ServerService.GenKeypair(kType, options)
jsonObj(c, keypair, nil)
}
func (a *ApiService) GetDb(c *gin.Context) {
exclude := c.Query("exclude")
db, err := database.GetDb(exclude)
if err != nil {
jsonMsg(c, "", err)
return
}
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename=s-ui_"+time.Now().Format("20060102-150405")+".db")
c.Writer.Write(db)
}
func (a *ApiService) postActions(c *gin.Context) (string, json.RawMessage, error) {
var data map[string]json.RawMessage
err := c.ShouldBind(&data)
if err != nil {
return "", nil, err
}
return string(data["action"]), data["data"], nil
}
func (a *ApiService) Login(c *gin.Context) {
remoteIP := getRemoteIp(c)
loginUser, err := a.UserService.Login(c.Request.FormValue("user"), c.Request.FormValue("pass"), remoteIP)
if err != nil {
jsonMsg(c, "", err)
return
}
sessionMaxAge, err := a.SettingService.GetSessionMaxAge()
if err != nil {
logger.Infof("Unable to get session's max age from DB")
}
err = SetLoginUser(c, loginUser, sessionMaxAge)
if err == nil {
logger.Info("user ", loginUser, " login success")
} else {
logger.Warning("login failed: ", err)
}
jsonMsg(c, "", nil)
}
func (a *ApiService) ChangePass(c *gin.Context) {
id := c.Request.FormValue("id")
oldPass := c.Request.FormValue("oldPass")
newUsername := c.Request.FormValue("newUsername")
newPass := c.Request.FormValue("newPass")
err := a.UserService.ChangePass(id, oldPass, newUsername, newPass)
if err == nil {
logger.Info("change user credentials success")
jsonMsg(c, "save", nil)
} else {
logger.Warning("change user credentials failed:", err)
jsonMsg(c, "", err)
}
}
func (a *ApiService) Save(c *gin.Context, loginUser string) {
hostname := getHostname(c)
obj := c.Request.FormValue("object")
act := c.Request.FormValue("action")
data := c.Request.FormValue("data")
initUsers := c.Request.FormValue("initUsers")
objs, err := a.ConfigService.Save(obj, act, json.RawMessage(data), initUsers, loginUser, hostname)
if err != nil {
jsonMsg(c, "save", err)
return
}
err = a.LoadPartialData(c, objs)
if err != nil {
jsonMsg(c, obj, err)
}
}
func (a *ApiService) RestartApp(c *gin.Context) {
err := a.PanelService.RestartPanel(3)
jsonMsg(c, "restartApp", err)
}
func (a *ApiService) RestartSb(c *gin.Context) {
err := a.ConfigService.RestartCore()
jsonMsg(c, "restartSb", err)
}
func (a *ApiService) LinkConvert(c *gin.Context) {
link := c.Request.FormValue("link")
result, _, err := util.GetOutbound(link, 0)
jsonObj(c, result, err)
}
func (a *ApiService) SubConvert(c *gin.Context) {
link := c.Request.FormValue("link")
result, err := util.GetExternalSub(link)
jsonObj(c, result, err)
}
func (a *ApiService) ImportDb(c *gin.Context) {
file, _, err := c.Request.FormFile("db")
if err != nil {
jsonMsg(c, "", err)
return
}
defer file.Close()
err = database.ImportDB(file)
jsonMsg(c, "", err)
}
func (a *ApiService) Logout(c *gin.Context) {
loginUser := GetLoginUser(c)
if loginUser != "" {
logger.Infof("user %s logout", loginUser)
}
ClearSession(c)
jsonMsg(c, "", nil)
}
func (a *ApiService) LoadTokens() ([]byte, error) {
return a.UserService.LoadTokens()
}
func (a *ApiService) GetTokens(c *gin.Context) {
loginUser := GetLoginUser(c)
tokens, err := a.UserService.GetUserTokens(loginUser)
jsonObj(c, tokens, err)
}
func (a *ApiService) AddToken(c *gin.Context) {
loginUser := GetLoginUser(c)
expiry := c.Request.FormValue("expiry")
expiryInt, err := strconv.ParseInt(expiry, 10, 64)
if err != nil {
jsonMsg(c, "", err)
return
}
desc := c.Request.FormValue("desc")
token, err := a.UserService.AddToken(loginUser, expiryInt, desc)
jsonObj(c, token, err)
}
func (a *ApiService) DeleteToken(c *gin.Context) {
tokenId := c.Request.FormValue("id")
err := a.UserService.DeleteToken(tokenId)
jsonMsg(c, "", err)
}
func (a *ApiService) GetSingboxConfig(c *gin.Context) {
rawConfig, err := a.ConfigService.GetConfig("")
if err != nil {
c.Status(400)
c.Writer.WriteString(err.Error())
return
}
c.Header("Content-Type", "application/json")
c.Header("Content-Disposition", "attachment; filename=config_"+time.Now().Format("20060102-150405")+".json")
c.Writer.Write(*rawConfig)
}
func (a *ApiService) GetCheckOutbound(c *gin.Context) {
tag := c.Query("tag")
link := c.Query("link")
result := a.ConfigService.CheckOutbound(tag, link)
jsonObj(c, result, nil)
}
+134
View File
@@ -0,0 +1,134 @@
package api
import (
"encoding/json"
"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
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 "linkConvert":
a.ApiService.LinkConvert(c)
case "subConvert":
a.ApiService.SubConvert(c)
case "importdb":
a.ApiService.ImportDb(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 "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")
for index, t := range *a.tokens {
if t.Expiry > 0 && t.Expiry < time.Now().Unix() {
(*a.tokens) = append((*a.tokens)[:index], (*a.tokens)[index+1:]...)
continue
}
if t.Token == token {
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 {
var newTokens []TokenInMemory
err = json.Unmarshal(tokens, &newTokens)
if err != nil {
logger.Error("unable to load tokens: ", err)
}
a.tokens = &newTokens
} else {
logger.Error("unable to load tokens: ", err)
}
}
+69
View File
@@ -0,0 +1,69 @@
package api
import (
"encoding/gob"
"github.com/alireza0/s-ui/database/model"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
const (
loginUser = "LOGIN_USER"
)
func init() {
gob.Register(model.User{})
}
func SetLoginUser(c *gin.Context, userName string, maxAge int) error {
options := sessions.Options{
Path: "/",
Secure: false,
}
if maxAge > 0 {
options.MaxAge = maxAge * 60
}
s := sessions.Default(c)
s.Set(loginUser, userName)
s.Options(options)
return s.Save()
}
func SetMaxAge(c *gin.Context) error {
s := sessions.Default(c)
s.Options(sessions.Options{
Path: "/",
})
return s.Save()
}
func GetLoginUser(c *gin.Context) string {
s := sessions.Default(c)
obj := s.Get(loginUser)
if obj == nil {
return ""
}
objStr, ok := obj.(string)
if !ok {
return ""
}
return objStr
}
func IsLogin(c *gin.Context) bool {
return GetLoginUser(c) != ""
}
func ClearSession(c *gin.Context) {
s := sessions.Default(c)
s.Clear()
s.Options(sessions.Options{
Path: "/",
MaxAge: -1,
})
s.Save()
}
+92
View File
@@ -0,0 +1,92 @@
package api
import (
"net"
"net/http"
"strings"
"github.com/alireza0/s-ui/logger"
"github.com/gin-gonic/gin"
)
type Msg struct {
Success bool `json:"success"`
Msg string `json:"msg"`
Obj interface{} `json:"obj"`
}
func getRemoteIp(c *gin.Context) string {
value := c.GetHeader("X-Forwarded-For")
if value != "" {
ips := strings.Split(value, ",")
return ips[0]
} else {
addr := c.Request.RemoteAddr
ip, _, _ := net.SplitHostPort(addr)
return ip
}
}
func getHostname(c *gin.Context) string {
host := c.Request.Host
if strings.Contains(host, ":") {
host, _, _ = net.SplitHostPort(c.Request.Host)
if strings.Contains(host, ":") {
host = "[" + host + "]"
}
}
return host
}
func jsonMsg(c *gin.Context, msg string, err error) {
jsonMsgObj(c, msg, nil, err)
}
func jsonObj(c *gin.Context, obj interface{}, err error) {
jsonMsgObj(c, "", obj, err)
}
func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
m := Msg{
Obj: obj,
}
if err == nil {
m.Success = true
if msg != "" {
m.Msg = msg
}
} else {
m.Success = false
m.Msg = msg + ": " + err.Error()
logger.Warning("failed :", err)
}
c.JSON(http.StatusOK, m)
}
func pureJsonMsg(c *gin.Context, success bool, msg string) {
if success {
c.JSON(http.StatusOK, Msg{
Success: true,
Msg: msg,
})
} else {
c.JSON(http.StatusOK, Msg{
Success: false,
Msg: msg,
})
}
}
func checkLogin(c *gin.Context) {
if !IsLogin(c) {
if c.GetHeader("X-Requested-With") == "XMLHttpRequest" {
pureJsonMsg(c, false, "Invalid login")
} else {
c.Redirect(http.StatusTemporaryRedirect, "/login")
}
c.Abort()
} else {
c.Next()
}
}