Compare commits

..

25 Commits

Author SHA1 Message Date
Alireza Ahmadi 5da87cf14d v1.2.0-beta.3 2025-01-20 01:39:05 +01:00
Alireza Ahmadi 1edd9d4323 upgrade backend dependencies 2025-01-20 01:28:32 +01:00
Alireza Ahmadi ce968d20ac upgrade frontend dependencies 2025-01-20 01:28:17 +01:00
Alireza Ahmadi 60c64bab18 small fixes 2025-01-20 01:27:37 +01:00
Alireza Ahmadi f3432b119c option backup restore #238 2025-01-20 01:26:02 +01:00
Alireza Ahmadi 049cfc5287 clean up changes 2025-01-19 19:43:19 +01:00
Alireza Ahmadi 891a61ac16 small fixes 2025-01-19 02:26:10 +01:00
Alireza Ahmadi a0dc165995 option warp endpoint #345 2025-01-19 01:52:25 +01:00
Alireza Ahmadi 4dc02f783a skeleton for stats 2025-01-18 16:04:00 +01:00
Alireza Ahmadi 1282d67640 qrcode loading data 2025-01-18 16:03:16 +01:00
Alireza Ahmadi f116e7f5ea option all inbounds for client modal #399 2025-01-18 12:28:35 +01:00
Alireza Ahmadi d06b6be4a2 init users on create inbound #411 2025-01-18 10:55:22 +01:00
Alireza Ahmadi b43a6ade97 shadowtls users only in v3 2025-01-18 10:34:42 +01:00
Alireza Ahmadi f18345b30d only table in clients page & better load and save 2025-01-18 10:28:04 +01:00
Alireza Ahmadi 67582015d3 tls fix and migration patch 2025-01-11 12:32:52 +01:00
Alireza Ahmadi 94473b40de fix errors in UI 2025-01-11 11:26:32 +01:00
Alireza Ahmadi 7e41af0da8 fix tls in link and json 2025-01-11 11:25:40 +01:00
Alireza Ahmadi 88adcc7c9a update default config 2025-01-11 11:24:45 +01:00
Alireza Ahmadi 8fb5d3964a v1.2.0-beta.2 2025-01-07 23:55:28 +01:00
Alireza Ahmadi 3d92431269 upgrade to sing-box v1.11.0-beta.20 2025-01-07 23:50:37 +01:00
Alireza Ahmadi f06840eaea fix scripts 2025-01-07 23:41:38 +01:00
Alireza Ahmadi 906769e21a fix hysteria2 masquerade options 2025-01-07 23:37:06 +01:00
Alireza Ahmadi 3641bbe25f fix wireguard and peer data types 2025-01-07 22:22:19 +01:00
Alireza Ahmadi a39a669d75 small fixes 2025-01-07 19:27:26 +01:00
Alireza Ahmadi 9f6771ec09 fix save settings 2025-01-07 19:26:47 +01:00
62 changed files with 2193 additions and 1014 deletions
+32 -4
View File
@@ -2,12 +2,14 @@ package api
import ( import (
"encoding/json" "encoding/json"
"s-ui/database"
"s-ui/logger" "s-ui/logger"
"s-ui/service" "s-ui/service"
"s-ui/util" "s-ui/util"
"s-ui/util/common" "s-ui/util/common"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -88,7 +90,8 @@ func (a *APIHandler) postHandler(c *gin.Context) {
obj := c.Request.FormValue("object") obj := c.Request.FormValue("object")
act := c.Request.FormValue("action") act := c.Request.FormValue("action")
data := c.Request.FormValue("data") data := c.Request.FormValue("data")
objs, err := a.ConfigService.Save(obj, act, json.RawMessage(data), loginUser, hostname) initUsers := c.Request.FormValue("initUsers")
objs, err := a.ConfigService.Save(obj, act, json.RawMessage(data), initUsers, loginUser, hostname)
if err != nil { if err != nil {
jsonMsg(c, "save", err) jsonMsg(c, "save", err)
return return
@@ -108,6 +111,15 @@ func (a *APIHandler) postHandler(c *gin.Context) {
link := c.Request.FormValue("link") link := c.Request.FormValue("link")
result, _, err := util.GetOutbound(link, 0) result, _, err := util.GetOutbound(link, 0)
jsonObj(c, result, err) jsonObj(c, result, err)
case "importdb":
file, _, err := c.Request.FormFile("db")
if err != nil {
jsonMsg(c, "", err)
return
}
defer file.Close()
err = database.ImportDB(file)
jsonMsg(c, "", err)
default: default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action)) jsonMsg(c, "failed", common.NewError("unknown action: ", action))
} }
@@ -144,7 +156,7 @@ func (a *APIHandler) getHandler(c *gin.Context) {
return return
} }
jsonObj(c, *users, nil) jsonObj(c, *users, nil)
case "setting": case "settings":
data, err := a.SettingService.GetAllSetting() data, err := a.SettingService.GetAllSetting()
if err != nil { if err != nil {
jsonMsg(c, "", err) jsonMsg(c, "", err)
@@ -187,6 +199,16 @@ func (a *APIHandler) getHandler(c *gin.Context) {
options := c.Query("o") options := c.Query("o")
keypair := a.ServerService.GenKeypair(kType, options) keypair := a.ServerService.GenKeypair(kType, options)
jsonObj(c, keypair, nil) jsonObj(c, keypair, nil)
case "getdb":
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)
default: default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action)) jsonMsg(c, "failed", common.NewError("unknown action: ", action))
} }
@@ -258,11 +280,11 @@ func (a *APIHandler) loadData(c *gin.Context) (interface{}, error) {
func (a *APIHandler) loadPartialData(c *gin.Context, objs []string) error { func (a *APIHandler) loadPartialData(c *gin.Context, objs []string) error {
data := make(map[string]interface{}, 0) data := make(map[string]interface{}, 0)
id := c.Query("id")
for _, obj := range objs { for _, obj := range objs {
switch obj { switch obj {
case "inbounds": case "inbounds":
id := c.Query("id")
inbounds, err := a.InboundService.Get(id) inbounds, err := a.InboundService.Get(id)
if err != nil { if err != nil {
return err return err
@@ -287,7 +309,7 @@ func (a *APIHandler) loadPartialData(c *gin.Context, objs []string) error {
} }
data[obj] = tlsConfigs data[obj] = tlsConfigs
case "clients": case "clients":
clients, err := a.ClientService.GetAll() clients, err := a.ClientService.Get(id)
if err != nil { if err != nil {
return err return err
} }
@@ -298,6 +320,12 @@ func (a *APIHandler) loadPartialData(c *gin.Context, objs []string) error {
return err return err
} }
data[obj] = json.RawMessage(config) data[obj] = json.RawMessage(config)
case "settings":
settings, err := a.SettingService.GetAllSetting()
if err != nil {
return err
}
data[obj] = settings
} }
} }
+8
View File
@@ -62,6 +62,10 @@ func migrateClientSchema(db *gorm.DB) error {
return nil return nil
} }
func deleteOldWebSecret(db *gorm.DB) error {
return db.Exec("DELETE FROM settings WHERE key = ?", "webSecret").Error
}
func changesObj(db *gorm.DB) error { func changesObj(db *gorm.DB) error {
return db.Exec("UPDATE changes SET obj = CAST('\"' || CAST(obj AS TEXT) || '\"' AS BLOB) WHERE actor = ? and obj not like ?", "DepleteJob", "\"%\"").Error return db.Exec("UPDATE changes SET obj = CAST('\"' || CAST(obj AS TEXT) || '\"' AS BLOB) WHERE actor = ? and obj not like ?", "DepleteJob", "\"%\"").Error
} }
@@ -71,6 +75,10 @@ func to1_1(db *gorm.DB) error {
if err != nil { if err != nil {
return err return err
} }
err = deleteOldWebSecret(db)
if err != nil {
return err
}
err = changesObj(db) err = changesObj(db)
if err != nil { if err != nil {
return err return err
+38 -15
View File
@@ -60,22 +60,10 @@ func moveJsonToDb(db *gorm.DB) error {
} else { } else {
tls_server, _ := json.MarshalIndent(tlsObj, "", " ") tls_server, _ := json.MarshalIndent(tlsObj, "", " ")
if len(tls_server) > 5 { if len(tls_server) > 5 {
tlsObject := tlsObj.(map[string]interface{})
tlsClientObj := map[string]interface{}{}
if enabled, ok := tlsObject["enabled"]; ok {
tlsClientObj["enabled"] = enabled
}
if alpn, ok := tlsObject["alpn"]; ok {
tlsClientObj["alpn"] = alpn
}
if sni, ok := tlsObject["server_name"]; ok {
tlsClientObj["server_name"] = sni
}
tls_client, _ := json.MarshalIndent(tlsClientObj, "", " ")
newTls := &model.Tls{ newTls := &model.Tls{
Name: tag, Name: tag,
Server: tls_server, Server: tls_server,
Client: tls_client, Client: json.RawMessage("{}"),
} }
err = db.Create(newTls).Error err = db.Create(newTls).Error
if err != nil { if err != nil {
@@ -243,7 +231,34 @@ func migrateTls(db *gorm.DB) error {
if !db.Migrator().HasColumn(&model.Tls{}, "inbounds") { if !db.Migrator().HasColumn(&model.Tls{}, "inbounds") {
return nil return nil
} }
return db.Migrator().DropColumn(&model.Tls{}, "inbounds") err := db.Migrator().DropColumn(&model.Tls{}, "inbounds")
if err != nil {
return err
}
var tlsConfig []model.Tls
err = db.Model(model.Tls{}).Scan(&tlsConfig).Error
if err != nil {
return err
}
for index, tls := range tlsConfig {
var tlsClient map[string]interface{}
err = json.Unmarshal(tls.Client, &tlsClient)
if err != nil {
continue
}
for key := range tlsClient {
switch key {
case "insecure", "disable_sni", "utls", "ech", "reality":
continue
default:
delete(tlsClient, key)
}
}
tlsConfig[index].Client, _ = json.MarshalIndent(tlsClient, "", " ")
}
return db.Save(&tlsConfig).Error
} }
func dropInboundData(db *gorm.DB) error { func dropInboundData(db *gorm.DB) error {
@@ -276,6 +291,10 @@ func migrateClients(db *gorm.DB) error {
return db.Save(oldClients).Error return db.Save(oldClients).Error
} }
func migrateChanges(db *gorm.DB) error {
return db.Migrator().DropColumn(&model.Changes{}, "index")
}
func to1_2(db *gorm.DB) error { func to1_2(db *gorm.DB) error {
err := moveJsonToDb(db) err := moveJsonToDb(db)
if err != nil { if err != nil {
@@ -289,5 +308,9 @@ func to1_2(db *gorm.DB) error {
if err != nil { if err != nil {
return err return err
} }
return migrateClients(db) err = migrateClients(db)
if err != nil {
return err
}
return migrateChanges(db)
} }
+1 -1
View File
@@ -1 +1 @@
1.2.0-beta.1 1.2.0-beta.3
+271
View File
@@ -0,0 +1,271 @@
package database
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"s-ui/cmd/migration"
"s-ui/config"
"s-ui/database/model"
"s-ui/logger"
"s-ui/util/common"
"strings"
"syscall"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func GetDb(exclude string) ([]byte, error) {
exclude_changes, exclude_stats := false, false
for _, table := range strings.Split(exclude, ",") {
if table == "changes" {
exclude_changes = true
} else if table == "stats" {
exclude_stats = true
}
}
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
return nil, err
}
dbPath := dir + config.GetName() + "_" + time.Now().Format("20060102-200203") + ".db"
backupDb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
return nil, err
}
err = backupDb.AutoMigrate(
&model.Setting{},
&model.Tls{},
&model.Inbound{},
&model.Outbound{},
&model.Endpoint{},
&model.User{},
&model.Stats{},
&model.Client{},
&model.Changes{},
)
if err != nil {
return nil, err
}
var settings []model.Setting
var tls []model.Tls
var inbound []model.Inbound
var outbound []model.Outbound
var endpoint []model.Endpoint
var users []model.User
var clients []model.Client
var stats []model.Stats
var changes []model.Changes
// Perform scans and handle errors
if err := db.Model(&model.Setting{}).Scan(&settings).Error; err != nil {
return nil, err
}
if err := db.Model(&model.Tls{}).Scan(&tls).Error; err != nil {
return nil, err
}
if err := db.Model(&model.Inbound{}).Scan(&inbound).Error; err != nil {
return nil, err
}
if err := db.Model(&model.Outbound{}).Scan(&outbound).Error; err != nil {
return nil, err
}
if err := db.Model(&model.Endpoint{}).Scan(&endpoint).Error; err != nil {
return nil, err
}
if err := db.Model(&model.User{}).Scan(&users).Error; err != nil {
return nil, err
}
if err := db.Model(&model.Client{}).Scan(&clients).Error; err != nil {
return nil, err
}
// Save each model
for _, mdl := range []interface{}{settings, tls, inbound, outbound, endpoint, users, clients} {
if err := backupDb.Save(mdl).Error; err != nil {
return nil, err
}
}
if !exclude_stats {
if err := db.Model(&model.Stats{}).Scan(&stats).Error; err != nil {
return nil, err
}
if err := backupDb.Save(stats).Error; err != nil {
return nil, err
}
}
if !exclude_changes {
if err := db.Model(&model.Changes{}).Scan(&changes).Error; err != nil {
return nil, err
}
if err := backupDb.Save(changes).Error; err != nil {
return nil, err
}
}
// Update WAL
err = backupDb.Exec("PRAGMA wal_checkpoint;").Error
if err != nil {
return nil, err
}
bdb, _ := backupDb.DB()
bdb.Close()
// Open the file for reading
file, err := os.Open(dbPath)
if err != nil {
return nil, err
}
defer file.Close()
defer os.Remove(dbPath)
// Read the file contents
fileContents, err := io.ReadAll(file)
if err != nil {
return nil, err
}
return fileContents, nil
}
func ImportDB(file multipart.File) error {
// Check if the file is a SQLite database
isValidDb, err := IsSQLiteDB(file)
if err != nil {
return common.NewErrorf("Error checking db file format: %v", err)
}
if !isValidDb {
return common.NewError("Invalid db file format")
}
// Reset the file reader to the beginning
_, err = file.Seek(0, 0)
if err != nil {
return common.NewErrorf("Error resetting file reader: %v", err)
}
// Save the file as temporary file
tempPath := fmt.Sprintf("%s.temp", config.GetDBPath())
// Remove the existing fallback file (if any) before creating one
_, err = os.Stat(tempPath)
if err == nil {
errRemove := os.Remove(tempPath)
if errRemove != nil {
return common.NewErrorf("Error removing existing temporary db file: %v", errRemove)
}
}
// Create the temporary file
tempFile, err := os.Create(tempPath)
if err != nil {
return common.NewErrorf("Error creating temporary db file: %v", err)
}
defer tempFile.Close()
// Remove temp file before returning
defer os.Remove(tempPath)
// Close old DB
old_db, _ := db.DB()
old_db.Close()
// Save uploaded file to temporary file
_, err = io.Copy(tempFile, file)
if err != nil {
return common.NewErrorf("Error saving db: %v", err)
}
// Check if we can init db or not
newDb, err := gorm.Open(sqlite.Open(tempPath), &gorm.Config{})
if err != nil {
return common.NewErrorf("Error checking db: %v", err)
}
newDb_db, _ := newDb.DB()
newDb_db.Close()
// Backup the current database for fallback
fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
// Remove the existing fallback file (if any)
_, err = os.Stat(fallbackPath)
if err == nil {
errRemove := os.Remove(fallbackPath)
if errRemove != nil {
return common.NewErrorf("Error removing existing fallback db file: %v", errRemove)
}
}
// Move the current database to the fallback location
err = os.Rename(config.GetDBPath(), fallbackPath)
if err != nil {
return common.NewErrorf("Error backing up temporary db file: %v", err)
}
// Remove the temporary file before returning
defer os.Remove(fallbackPath)
// Move temp to DB path
err = os.Rename(tempPath, config.GetDBPath())
if err != nil {
errRename := os.Rename(fallbackPath, config.GetDBPath())
if errRename != nil {
return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename)
}
return common.NewErrorf("Error moving db file: %v", err)
}
// Migrate DB
migration.MigrateDb()
err = InitDB(config.GetDBPath())
if err != nil {
errRename := os.Rename(fallbackPath, config.GetDBPath())
if errRename != nil {
return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
}
return common.NewErrorf("Error migrating db: %v", err)
}
// Restart app
err = SendSighup()
if err != nil {
return common.NewErrorf("Error restarting app: %v", err)
}
return nil
}
func IsSQLiteDB(file io.Reader) (bool, error) {
signature := []byte("SQLite format 3\x00")
buf := make([]byte, len(signature))
_, err := file.Read(buf)
if err != nil {
return false, err
}
return bytes.Equal(buf, signature), nil
}
func SendSighup() error {
// Get the current process
process, err := os.FindProcess(os.Getpid())
if err != nil {
return err
}
// Send SIGHUP to the current process
go func() {
time.Sleep(3 * time.Second)
err := process.Signal(syscall.SIGHUP)
if err != nil {
logger.Error("send signal SIGHUP failed:", err)
}
}()
return nil
}
+9 -1
View File
@@ -9,6 +9,7 @@ type Endpoint struct {
Type string `json:"type" form:"type"` Type string `json:"type" form:"type"`
Tag string `json:"tag" form:"tag" gorm:"unique"` Tag string `json:"tag" form:"tag" gorm:"unique"`
Options json.RawMessage `json:"-" form:"-"` Options json.RawMessage `json:"-" form:"-"`
Ext json.RawMessage `json:"ext" form:"ext"`
} }
func (o *Endpoint) UnmarshalJSON(data []byte) error { func (o *Endpoint) UnmarshalJSON(data []byte) error {
@@ -27,9 +28,11 @@ func (o *Endpoint) UnmarshalJSON(data []byte) error {
delete(raw, "type") delete(raw, "type")
o.Tag = raw["tag"].(string) o.Tag = raw["tag"].(string)
delete(raw, "tag") delete(raw, "tag")
o.Ext, _ = json.MarshalIndent(raw["ext"], "", " ")
delete(raw, "ext")
// Remaining fields // Remaining fields
o.Options, err = json.Marshal(raw) o.Options, err = json.MarshalIndent(raw, "", " ")
return err return err
} }
@@ -37,7 +40,12 @@ func (o *Endpoint) UnmarshalJSON(data []byte) error {
func (o Endpoint) MarshalJSON() ([]byte, error) { func (o Endpoint) MarshalJSON() ([]byte, error) {
// Combine fixed fields and dynamic fields into one map // Combine fixed fields and dynamic fields into one map
combined := make(map[string]interface{}) combined := make(map[string]interface{})
switch o.Type {
case "warp":
combined["type"] = "wireguard"
default:
combined["type"] = o.Type combined["type"] = o.Type
}
combined["tag"] = o.Tag combined["tag"] = o.Tag
if o.Options != nil { if o.Options != nil {
+6 -7
View File
@@ -26,9 +26,9 @@ type Client struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Enable bool `json:"enable" form:"enable"` Enable bool `json:"enable" form:"enable"`
Name string `json:"name" form:"name"` Name string `json:"name" form:"name"`
Config json.RawMessage `json:"config" form:"config"` Config json.RawMessage `json:"config,omitempty" form:"config"`
Inbounds json.RawMessage `json:"inbounds" form:"inbounds"` Inbounds json.RawMessage `json:"inbounds" form:"inbounds"`
Links json.RawMessage `json:"links" form:"links"` Links json.RawMessage `json:"links,omitempty" form:"links"`
Volume int64 `json:"volume" form:"volume"` Volume int64 `json:"volume" form:"volume"`
Expiry int64 `json:"expiry" form:"expiry"` Expiry int64 `json:"expiry" form:"expiry"`
Down int64 `json:"down" form:"down"` Down int64 `json:"down" form:"down"`
@@ -49,9 +49,8 @@ type Stats struct {
type Changes struct { type Changes struct {
Id uint64 `json:"id" gorm:"primaryKey;autoIncrement"` Id uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
DateTime int64 `json:"dateTime"` DateTime int64 `json:"dateTime"`
Actor string `json:"Actor"` Actor string `json:"actor"`
Key string `json:"key" form:"key"` Key string `json:"key"`
Action string `json:"action" form:"action"` Action string `json:"action"`
Index uint `json:"index" form:"index"` Obj json.RawMessage `json:"obj"`
Obj json.RawMessage `json:"obj" form:"obj"`
} }
+1 -1
View File
@@ -27,7 +27,7 @@ func (o *Outbound) UnmarshalJSON(data []byte) error {
delete(raw, "tag") delete(raw, "tag")
// Remaining fields // Remaining fields
o.Options, err = json.Marshal(raw) o.Options, err = json.MarshalIndent(raw, "", " ")
return err return err
} }
+6 -8
View File
@@ -7,10 +7,9 @@ require (
github.com/gin-gonic/gin v1.10.0 github.com/gin-gonic/gin v1.10.0
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/sagernet/sing v0.6.0-beta.9 github.com/sagernet/sing v0.6.0-beta.12
github.com/sagernet/sing-box v1.11.0-beta.19 github.com/sagernet/sing-box v1.11.0-beta.24
github.com/sagernet/sing-dns v0.4.0-beta.1 github.com/sagernet/sing-dns v0.4.0-beta.2
github.com/shirou/gopsutil/v3 v3.24.5
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
gorm.io/driver/sqlite v1.5.7 gorm.io/driver/sqlite v1.5.7
gorm.io/gorm v1.25.12 gorm.io/gorm v1.25.12
@@ -91,18 +90,17 @@ require (
github.com/sagernet/quic-go v0.48.2-beta.1 // indirect github.com/sagernet/quic-go v0.48.2-beta.1 // indirect
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect
github.com/sagernet/sing-mux v0.3.0-alpha.1 // indirect github.com/sagernet/sing-mux v0.3.0-alpha.1 // indirect
github.com/sagernet/sing-quic v0.4.0-beta.2 // indirect github.com/sagernet/sing-quic v0.4.0-beta.4 // indirect
github.com/sagernet/sing-shadowsocks v0.2.7 // indirect github.com/sagernet/sing-shadowsocks v0.2.7 // indirect
github.com/sagernet/sing-shadowsocks2 v0.2.0 // indirect github.com/sagernet/sing-shadowsocks2 v0.2.0 // indirect
github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 // indirect github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 // indirect
github.com/sagernet/sing-tun v0.6.0-beta.7.0.20241229131914-aa9d9c62966f // indirect github.com/sagernet/sing-tun v0.6.0-beta.8 // indirect
github.com/sagernet/sing-vmess v0.2.0-beta.1 // indirect github.com/sagernet/sing-vmess v0.2.0-beta.2 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/sagernet/utls v1.6.7 // indirect github.com/sagernet/utls v1.6.7 // indirect
github.com/sagernet/wireguard-go v0.0.1-beta.5 // indirect github.com/sagernet/wireguard-go v0.0.1-beta.5 // indirect
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
github.com/shirou/gopsutil/v4 v4.24.12 github.com/shirou/gopsutil/v4 v4.24.12
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.9.0 // indirect github.com/tklauser/numcpus v0.9.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+19 -54
View File
@@ -114,8 +114,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
@@ -172,62 +170,40 @@ github.com/sagernet/quic-go v0.48.2-beta.1/go.mod h1:1WgdDIVD1Gybp40JTWketeSfKA/
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU= github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo= github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.6.0-beta.5 h1:RD2j8WmJsvAbbBkAlJWaiYmnd+v/JohBiweoew7kMwo= github.com/sagernet/sing v0.6.0-beta.11 h1:jWCNlZI1Vdj8lQeBrjRZIQfNwlqMk0ZRqMJuPfTJupI=
github.com/sagernet/sing v0.6.0-beta.5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.6.0-beta.11/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.6.0-beta.8 h1:PoxDdN7y8D4oImT3cQ05Sq1ZYnYsJberkUkIEHIGwWE= github.com/sagernet/sing v0.6.0-beta.12 h1:2DnTJcvypK3/PM/8JjmgG8wVK48gdcpRwU98c4J/a7s=
github.com/sagernet/sing v0.6.0-beta.8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.6.0-beta.12/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.6.0-beta.9 h1:P8lKa5hN53fRNAVCIKy5cWd6/kLO5c4slhdsfehSmHs= github.com/sagernet/sing-box v1.11.0-beta.22 h1:UQrhqbUyJUZ1GvT3yNu4ANdZC8s1YdgN92jtvPd559g=
github.com/sagernet/sing v0.6.0-beta.9/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing-box v1.11.0-beta.22/go.mod h1:CYFqT0KvhmGhs3hp6htI8x6DugWZgdiAde+Fyufxmek=
github.com/sagernet/sing-box v1.11.0-beta.6 h1:MPdL2Yem/xM0RhejCO7krYvl1Zbd1zkSjKluKpHnHPQ= github.com/sagernet/sing-box v1.11.0-beta.24 h1:6rUl8t6Cb0p9ML1eUobWgODL75c5iszxNvVABcWCivU=
github.com/sagernet/sing-box v1.11.0-beta.6/go.mod h1:6dO5V0A37cLlhvKnxCmZinSpZXz7ZSk11x3rgI+xH1I= github.com/sagernet/sing-box v1.11.0-beta.24/go.mod h1:DmL1WKyrfaAEu5z88CtUeQBfELaEdUyQzLS5nzmRg8o=
github.com/sagernet/sing-box v1.11.0-beta.11 h1:bVR0n3oQ3hGcuc/CSS7axsOeRNCRlCGkYVOKl0wxbsw= github.com/sagernet/sing-dns v0.4.0-beta.2 h1:HW94bUEp7K/vf5DlYz646LTZevQtJ0250jZa/UZRlbY=
github.com/sagernet/sing-box v1.11.0-beta.11/go.mod h1:GZnZUzUHZ6Bgm7D/i8unNORv3537u1s03tLXFdxCRpg= github.com/sagernet/sing-dns v0.4.0-beta.2/go.mod h1:8wuFcoFkWM4vJuQyg8e97LyvDwe0/Vl7G839WLcKDs8=
github.com/sagernet/sing-box v1.11.0-beta.15 h1:oWcs/PHgKaeWKbTfgz/020KEVvDqQv/tQWe7zpyktkc=
github.com/sagernet/sing-box v1.11.0-beta.15/go.mod h1:+QZDsF4HkdiGcMfz+JNOfONLh9CnZjIwJJQNWEzhiaQ=
github.com/sagernet/sing-box v1.11.0-beta.19 h1:uL2xlXpz4t7BduLbXiLe5QqpyiMhvNNRThBzhTJ4p00=
github.com/sagernet/sing-box v1.11.0-beta.19/go.mod h1:UXUN/lwRT9mAM8PK7upPOwgqooOV2vU+CcjBfwT1rYg=
github.com/sagernet/sing-dns v0.4.0-beta.1 h1:W1XkdhigwxDOMgMDVB+9kdomCpb7ExsZfB4acPcTZFY=
github.com/sagernet/sing-dns v0.4.0-beta.1/go.mod h1:8wuFcoFkWM4vJuQyg8e97LyvDwe0/Vl7G839WLcKDs8=
github.com/sagernet/sing-mux v0.3.0-alpha.1 h1:IgNX5bJBpL41gGbp05pdDOvh/b5eUQ6cv9240+Ngipg= github.com/sagernet/sing-mux v0.3.0-alpha.1 h1:IgNX5bJBpL41gGbp05pdDOvh/b5eUQ6cv9240+Ngipg=
github.com/sagernet/sing-mux v0.3.0-alpha.1/go.mod h1:FTcImmdfW38Lz7b+HQ+mxxOth1lz4ao8uEnz+MwIJQE= github.com/sagernet/sing-mux v0.3.0-alpha.1/go.mod h1:FTcImmdfW38Lz7b+HQ+mxxOth1lz4ao8uEnz+MwIJQE=
github.com/sagernet/sing-quic v0.4.0-alpha.4 h1:P9xAx3nIfcqb9M8jfgs0uLm+VxCcaY++FCqaBfHY3dQ= github.com/sagernet/sing-quic v0.4.0-beta.4 h1:kKiMLGaxvVLDCSvCMYo4PtWd1xU6FTL7xvUAQfXO09g=
github.com/sagernet/sing-quic v0.4.0-alpha.4/go.mod h1:h5RkKTmUhudJKzK7c87FPXD5w1bJjVyxMN9+opZcctA= github.com/sagernet/sing-quic v0.4.0-beta.4/go.mod h1:1UNObFodd8CnS3aCT53x9cigjPSCl3P//8dfBMCwBDM=
github.com/sagernet/sing-quic v0.4.0-beta.2 h1:ikoQ7zTR0o/2rlI5H5FeNC0j5bQJJHb1uoyXFRu3yGk=
github.com/sagernet/sing-quic v0.4.0-beta.2/go.mod h1:1UNObFodd8CnS3aCT53x9cigjPSCl3P//8dfBMCwBDM=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8= github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE= github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg= github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ= github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 h1:RPrpgAdkP5td0vLfS5ldvYosFjSsZtRPxiyLV6jyKg0= github.com/sagernet/sing-shadowtls v0.2.0-alpha.2 h1:RPrpgAdkP5td0vLfS5ldvYosFjSsZtRPxiyLV6jyKg0=
github.com/sagernet/sing-shadowtls v0.2.0-alpha.2/go.mod h1:0j5XlzKxaWRIEjc1uiSKmVoWb0k+L9QgZVb876+thZA= github.com/sagernet/sing-shadowtls v0.2.0-alpha.2/go.mod h1:0j5XlzKxaWRIEjc1uiSKmVoWb0k+L9QgZVb876+thZA=
github.com/sagernet/sing-tun v0.6.0-beta.2 h1:GK7r2jWKm7RhlJGTq4QadgFcebQia1c3BO3OlYMcQJ0= github.com/sagernet/sing-tun v0.6.0-beta.8 h1:GFNt/w8r1v30zC/hfCytk8C9+N/f1DfvosFXJkyJlrw=
github.com/sagernet/sing-tun v0.6.0-beta.2/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE= github.com/sagernet/sing-tun v0.6.0-beta.8/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-tun v0.6.0-beta.6 h1:xaIHoH78MqTSvZqQ4SQto8pC1A+X4qXReDRNaC8DQeI= github.com/sagernet/sing-vmess v0.2.0-beta.2 h1:obAkAL35X7ql4RnGzDg4dBYIRpGXRKqcN4LyLZpZGSs=
github.com/sagernet/sing-tun v0.6.0-beta.6/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE= github.com/sagernet/sing-vmess v0.2.0-beta.2/go.mod h1:HGhf9XUdeE2iOWrX0hQNFgXPbKyGlzpeYFyX0c/pykk=
github.com/sagernet/sing-tun v0.6.0-beta.7 h1:FCSX8oGBqb0H57AAvfGeeH/jMGYWCOg6XWkN/oeES+0=
github.com/sagernet/sing-tun v0.6.0-beta.7/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-tun v0.6.0-beta.7.0.20241229131914-aa9d9c62966f h1:dTnXP0e3LbSa4EpUmuOGhllanKPei4vPKfzlLvk76Pc=
github.com/sagernet/sing-tun v0.6.0-beta.7.0.20241229131914-aa9d9c62966f/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-vmess v0.2.0-beta.1 h1:5sXQ23uwNlZuDvygzi0dFtnG0Csm/SNqTjAHXJkpuj4=
github.com/sagernet/sing-vmess v0.2.0-beta.1/go.mod h1:fLyE1emIcvQ5DV8reFWnufquZ7MkCSYM5ThodsR9NrQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo= github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8= github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8=
github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM= github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
github.com/sagernet/wireguard-go v0.0.1-beta.4 h1:8uyM5fxfEXdu4RH05uOK+v25i3lTNdCYMPSAUJ14FnI=
github.com/sagernet/wireguard-go v0.0.1-beta.4/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc= github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
github.com/sagernet/wireguard-go v0.0.1-beta.5/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo= github.com/sagernet/wireguard-go v0.0.1-beta.5/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA= github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shirou/gopsutil/v4 v4.24.12 h1:qvePBOk20e0IKA1QXrIIU+jmk+zEiYVVx06WjBRlZo4= github.com/shirou/gopsutil/v4 v4.24.12 h1:qvePBOk20e0IKA1QXrIIU+jmk+zEiYVVx06WjBRlZo4=
github.com/shirou/gopsutil/v4 v4.24.12/go.mod h1:DCtMPAad2XceTeIAbGyVfycbYQNBGk2P8cvDi7/VN9o= github.com/shirou/gopsutil/v4 v4.24.12/go.mod h1:DCtMPAad2XceTeIAbGyVfycbYQNBGk2P8cvDi7/VN9o=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -237,8 +213,8 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
@@ -268,8 +244,6 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/W
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
@@ -280,8 +254,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -292,18 +264,13 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
@@ -327,8 +294,6 @@ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I= gorm.io/driver/sqlite v1.5.7 h1:8NvsrhP0ifM7LX9G4zPB97NwovUakUxc+2V2uuf3Z1I=
gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4= gorm.io/driver/sqlite v1.5.7/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
+74 -5
View File
@@ -7,6 +7,7 @@ import (
"s-ui/logger" "s-ui/logger"
"s-ui/util" "s-ui/util"
"s-ui/util/common" "s-ui/util/common"
"strings"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
@@ -16,14 +17,32 @@ type ClientService struct {
InboundService InboundService
} }
func (s *ClientService) GetAll() ([]model.Client, error) { func (s *ClientService) Get(id string) (*[]model.Client, error) {
if id == "" {
return s.GetAll()
}
return s.getById(id)
}
func (s *ClientService) getById(id string) (*[]model.Client, error) {
db := database.GetDB() db := database.GetDB()
clients := []model.Client{} var client []model.Client
err := db.Model(model.Client{}).Scan(&clients).Error err := db.Model(model.Client{}).Where("id in ?", strings.Split(id, ",")).Scan(&client).Error
if err != nil { if err != nil {
return nil, err return nil, err
} }
return clients, nil
return &client, nil
}
func (s *ClientService) GetAll() (*[]model.Client, error) {
db := database.GetDB()
var clients []model.Client
err := db.Model(model.Client{}).Select("`id`, `enable`, `name`, `desc`, `group`, `inbounds`, `up`, `down`, `volume`, `expiry`").Scan(&clients).Error
if err != nil {
return nil, err
}
return &clients, nil
} }
func (s *ClientService) Save(tx *gorm.DB, act string, data json.RawMessage, hostname string) ([]uint, error) { func (s *ClientService) Save(tx *gorm.DB, act string, data json.RawMessage, hostname string) ([]uint, error) {
@@ -138,6 +157,56 @@ func (s *ClientService) updateLinksWithFixedInbounds(tx *gorm.DB, clients []*mod
return nil return nil
} }
func (s *ClientService) UpdateClientsOnInboundAdd(tx *gorm.DB, initIds string, inboundId uint, hostname string) error {
clientIds := strings.Split(initIds, ",")
var clients []model.Client
err := tx.Model(model.Client{}).Where("id in ?", clientIds).Find(&clients).Error
if err != nil {
return err
}
var inbound model.Inbound
err = tx.Model(model.Inbound{}).Preload("Tls").Where("id = ?", inboundId).Find(&inbound).Error
if err != nil {
return err
}
for _, client := range clients {
// Add inbounds
var clientInbounds []uint
json.Unmarshal(client.Inbounds, &clientInbounds)
clientInbounds = append(clientInbounds, inboundId)
client.Inbounds, err = json.MarshalIndent(clientInbounds, "", " ")
if err != nil {
return err
}
// Add links
var clientLinks, newClientLinks []map[string]string
json.Unmarshal(client.Links, &clientLinks)
newLinks := util.LinkGenerator(client.Config, &inbound, hostname)
for _, newLink := range newLinks {
newClientLinks = append(newClientLinks, map[string]string{
"remark": inbound.Tag,
"type": "local",
"uri": newLink,
})
}
for _, clientLink := range clientLinks {
if clientLink["remark"] != inbound.Tag {
newClientLinks = append(newClientLinks, clientLink)
}
}
client.Links, err = json.MarshalIndent(newClientLinks, "", " ")
if err != nil {
return err
}
err = tx.Save(&client).Error
if err != nil {
return err
}
}
return nil
}
func (s *ClientService) UpdateClientsOnInboundDelete(tx *gorm.DB, id uint, tag string) error { func (s *ClientService) UpdateClientsOnInboundDelete(tx *gorm.DB, id uint, tag string) error {
var clients []model.Client var clients []model.Client
err := tx.Table("clients"). err := tx.Table("clients").
@@ -182,7 +251,7 @@ func (s *ClientService) UpdateClientsOnInboundDelete(tx *gorm.DB, id uint, tag s
func (s *ClientService) UpdateLinksByInboundChange(tx *gorm.DB, inbounIds []uint, hostname string) error { func (s *ClientService) UpdateLinksByInboundChange(tx *gorm.DB, inbounIds []uint, hostname string) error {
var inbounds []model.Inbound var inbounds []model.Inbound
err := tx.Model(model.Inbound{}).Preload("Tls").Where("id in ? and type in ?", inbounIds, util.InboundTypeWithLink).Find(&inbounds).Error err := tx.Model(model.Inbound{}).Preload("Tls").Where("id in ? and type in ?", inbounIds, util.InboundTypeWithLink).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound { if err != nil && database.IsNotFound(err) {
return err return err
} }
for _, inbound := range inbounds { for _, inbound := range inbounds {
+9 -4
View File
@@ -116,10 +116,11 @@ func (s *ConfigService) StopCore() error {
return nil return nil
} }
func (s *ConfigService) Save(obj string, act string, data json.RawMessage, loginUser string, hostname string) ([]string, error) { func (s *ConfigService) Save(obj string, act string, data json.RawMessage, initUsers string, loginUser string, hostname string) ([]string, error) {
var err error var err error
var inboundIds []uint var inboundIds []uint
var inboundId uint var inboundId uint
var objs []string = []string{obj}
db := database.GetDB() db := database.GetDB()
tx := db.Begin() tx := db.Begin()
@@ -134,10 +135,11 @@ func (s *ConfigService) Save(obj string, act string, data json.RawMessage, login
switch obj { switch obj {
case "clients": case "clients":
inboundIds, err = s.ClientService.Save(tx, act, data, hostname) inboundIds, err = s.ClientService.Save(tx, act, data, hostname)
objs = append(objs, "inbounds")
case "tls": case "tls":
inboundIds, err = s.TlsService.Save(tx, act, data) inboundIds, err = s.TlsService.Save(tx, act, data)
case "inbounds": case "inbounds":
inboundId, err = s.InboundService.Save(tx, act, data, hostname) inboundId, err = s.InboundService.Save(tx, act, data, initUsers, hostname)
case "outbounds": case "outbounds":
err = s.OutboundService.Save(tx, act, data) err = s.OutboundService.Save(tx, act, data)
case "endpoints": case "endpoints":
@@ -148,6 +150,8 @@ func (s *ConfigService) Save(obj string, act string, data json.RawMessage, login
return nil, err return nil, err
} }
err = s.restartCoreWithConfig(data) err = s.restartCoreWithConfig(data)
case "settings":
err = s.SettingService.Save(tx, data)
default: default:
return nil, common.NewError("unknown object: ", obj) return nil, common.NewError("unknown object: ", obj)
} }
@@ -169,7 +173,6 @@ func (s *ConfigService) Save(obj string, act string, data json.RawMessage, login
// Commit changes so far // Commit changes so far
tx.Commit() tx.Commit()
LastUpdate = time.Now().Unix() LastUpdate = time.Now().Unix()
var objs []string = []string{obj}
tx = db.Begin() tx = db.Begin()
// Update side changes // Update side changes
@@ -182,8 +185,10 @@ func (s *ConfigService) Save(obj string, act string, data json.RawMessage, login
} }
objs = append(objs, "clients") objs = append(objs, "clients")
} }
if obj == "inbounds" && act != "add" { if obj == "inbounds" {
switch act { switch act {
case "new":
err = s.ClientService.UpdateClientsOnInboundAdd(tx, initUsers, inboundId, hostname)
case "edit": case "edit":
err = s.ClientService.UpdateLinksByInboundChange(tx, []uint{inboundId}, hostname) err = s.ClientService.UpdateLinksByInboundChange(tx, []uint{inboundId}, hostname)
case "del": case "del":
+23 -1
View File
@@ -10,7 +10,9 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type EndpointService struct{} type EndpointService struct {
WarpService
}
func (o *EndpointService) GetAll() (*[]map[string]interface{}, error) { func (o *EndpointService) GetAll() (*[]map[string]interface{}, error) {
db := database.GetDB() db := database.GetDB()
@@ -25,6 +27,7 @@ func (o *EndpointService) GetAll() (*[]map[string]interface{}, error) {
"id": endpoint.Id, "id": endpoint.Id,
"type": endpoint.Type, "type": endpoint.Type,
"tag": endpoint.Tag, "tag": endpoint.Tag,
"ext": endpoint.Ext,
} }
if endpoint.Options != nil { if endpoint.Options != nil {
var restFields map[string]json.RawMessage var restFields map[string]json.RawMessage
@@ -68,6 +71,25 @@ func (s *EndpointService) Save(tx *gorm.DB, act string, data json.RawMessage) er
return err return err
} }
if endpoint.Type == "warp" {
if act == "new" {
err = s.WarpService.RegisterWarp(&endpoint)
if err != nil {
return err
}
} else {
var old_license string
err = tx.Model(model.Endpoint{}).Select("json_extract(ext, '$.license_key')").Where("id = ?", endpoint.Id).Find(&old_license).Error
if err != nil {
return err
}
err = s.WarpService.SetWarpLicense(old_license, &endpoint)
if err != nil {
return err
}
}
}
if corePtr.IsRunning() { if corePtr.IsRunning() {
configData, err := endpoint.MarshalJSON() configData, err := endpoint.MarshalJSON()
if err != nil { if err != nil {
+95 -12
View File
@@ -48,6 +48,7 @@ func (s *InboundService) GetAll() (*[]map[string]interface{}, error) {
} }
var data []map[string]interface{} var data []map[string]interface{}
for _, inbound := range inbounds { for _, inbound := range inbounds {
var shadowtls_version uint
inbData := map[string]interface{}{ inbData := map[string]interface{}{
"id": inbound.Id, "id": inbound.Id,
"type": inbound.Type, "type": inbound.Type,
@@ -61,7 +62,25 @@ func (s *InboundService) GetAll() (*[]map[string]interface{}, error) {
} }
inbData["listen"] = restFields["listen"] inbData["listen"] = restFields["listen"]
inbData["listen_port"] = restFields["listen_port"] inbData["listen_port"] = restFields["listen_port"]
if inbound.Type == "shadowtls" {
json.Unmarshal(restFields["version"], &shadowtls_version)
} }
}
switch inbound.Type {
case "mixed", "socks", "http", "shadowsocks", "vmess", "trojan", "naive", "hysteria", "shadowtls", "tuic", "hysteria2", "vless":
if inbound.Type == "shadowtls" && shadowtls_version < 3 {
break
}
var users []string
err = db.Raw("SELECT clients.name FROM clients, json_each(clients.inbounds) as je WHERE je.value = ?", inbound.Id).Scan(&users).Error
if err != nil {
return nil, err
}
if len(users) > 0 || inbound.Type != "shadowsocks" {
inbData["users"] = users
}
}
data = append(data, inbData) data = append(data, inbData)
} }
return &data, nil return &data, nil
@@ -77,7 +96,7 @@ func (s *InboundService) FromIds(ids []uint) ([]*model.Inbound, error) {
return inbounds, nil return inbounds, nil
} }
func (s *InboundService) Save(tx *gorm.DB, act string, data json.RawMessage, hostname string) (uint, error) { func (s *InboundService) Save(tx *gorm.DB, act string, data json.RawMessage, initUserIds string, hostname string) (uint, error) {
var err error var err error
var id uint var id uint
@@ -88,7 +107,6 @@ func (s *InboundService) Save(tx *gorm.DB, act string, data json.RawMessage, hos
if err != nil { if err != nil {
return 0, err return 0, err
} }
id = inbound.Id
if inbound.TlsId > 0 { if inbound.TlsId > 0 {
err = tx.Model(model.Tls{}).Where("id = ?", inbound.TlsId).Find(&inbound.Tls).Error err = tx.Model(model.Tls{}).Where("id = ?", inbound.TlsId).Find(&inbound.Tls).Error
if err != nil { if err != nil {
@@ -96,6 +114,17 @@ func (s *InboundService) Save(tx *gorm.DB, act string, data json.RawMessage, hos
} }
} }
err = util.FillOutJson(&inbound, hostname)
if err != nil {
return 0, err
}
err = tx.Save(&inbound).Error
if err != nil {
return 0, err
}
id = inbound.Id
if corePtr.IsRunning() { if corePtr.IsRunning() {
if act == "edit" { if act == "edit" {
var oldTag string var oldTag string
@@ -114,7 +143,11 @@ func (s *InboundService) Save(tx *gorm.DB, act string, data json.RawMessage, hos
return 0, err return 0, err
} }
if act == "edit" {
inboundConfig, err = s.addUsers(tx, inboundConfig, inbound.Id, inbound.Type) inboundConfig, err = s.addUsers(tx, inboundConfig, inbound.Id, inbound.Type)
} else {
inboundConfig, err = s.initUsers(tx, inboundConfig, initUserIds, inbound.Type)
}
if err != nil { if err != nil {
return 0, err return 0, err
} }
@@ -124,16 +157,6 @@ func (s *InboundService) Save(tx *gorm.DB, act string, data json.RawMessage, hos
return 0, err return 0, err
} }
} }
err = util.FillOutJson(&inbound, hostname)
if err != nil {
return 0, err
}
err = tx.Save(&inbound).Error
if err != nil {
return 0, err
}
case "del": case "del":
var tag string var tag string
err = json.Unmarshal(data, &tag) err = json.Unmarshal(data, &tag)
@@ -214,6 +237,13 @@ func (s *InboundService) addUsers(db *gorm.DB, inboundJson []byte, inboundId uin
if err != nil { if err != nil {
return nil, err return nil, err
} }
if inboundType == "shadowtls" {
version, _ := inbound["version"].(float64)
if int(version) < 3 {
return inboundJson, nil
}
}
if inboundType == "shadowsocks" { if inboundType == "shadowsocks" {
method, _ := inbound["method"].(string) method, _ := inbound["method"].(string)
if method == "2022-blake3-aes-128-gcm" { if method == "2022-blake3-aes-128-gcm" {
@@ -233,7 +263,60 @@ func (s *InboundService) addUsers(db *gorm.DB, inboundJson []byte, inboundId uin
usersJson = append(usersJson, json.RawMessage(user)) usersJson = append(usersJson, json.RawMessage(user))
} }
if len(usersJson) > 0 || inboundType != "shadowsocks" {
inbound["users"] = usersJson inbound["users"] = usersJson
}
return json.Marshal(inbound)
}
func (s *InboundService) initUsers(db *gorm.DB, inboundJson []byte, clientIds string, inboundType string) ([]byte, error) {
ClientIds := strings.Split(clientIds, ",")
if len(ClientIds) == 0 {
return inboundJson, nil
}
switch inboundType {
case "mixed", "socks", "http", "shadowsocks", "vmess", "trojan", "naive", "hysteria", "shadowtls", "tuic", "hysteria2", "vless":
break
default:
return inboundJson, nil
}
var inbound map[string]interface{}
err := json.Unmarshal(inboundJson, &inbound)
if err != nil {
return nil, err
}
if inboundType == "shadowtls" {
version, _ := inbound["version"].(float64)
if int(version) < 3 {
return inboundJson, nil
}
}
if inboundType == "shadowsocks" {
method, _ := inbound["method"].(string)
if method == "2022-blake3-aes-128-gcm" {
inboundType = "shadowsocks16"
}
}
var users []string
err = db.Raw(`SELECT json_extract(clients.config, ?)
FROM clients
WHERE enable = true AND id in ?`,
"$."+inboundType, ClientIds).Scan(&users).Error
if err != nil {
return nil, err
}
var usersJson []json.RawMessage
for _, user := range users {
usersJson = append(usersJson, json.RawMessage(user))
}
if len(usersJson) > 0 || inboundType != "shadowsocks" {
inbound["users"] = usersJson
}
return json.Marshal(inbound) return json.Marshal(inbound)
} }
+8 -4
View File
@@ -163,7 +163,7 @@ func (s *ServerService) GenKeypair(keyType string, options string) []string {
case "reality": case "reality":
return s.generateRealityKeyPair() return s.generateRealityKeyPair()
case "wireguard": case "wireguard":
return generateWireGuardKey() return s.generateWireGuardKey(options)
} }
return []string{"Failed to generate keypair"} return []string{"Failed to generate keypair"}
@@ -195,10 +195,14 @@ func (s *ServerService) generateRealityKeyPair() []string {
return []string{"PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]), "PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:])} return []string{"PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]), "PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:])}
} }
func generateWireGuardKey() []string { func (s *ServerService) generateWireGuardKey(pk string) []string {
privateKey, err := wgtypes.GeneratePrivateKey() if len(pk) > 0 {
key, _ := wgtypes.ParseKey(pk)
return []string{key.PublicKey().String()}
}
wgKeys, err := wgtypes.GeneratePrivateKey()
if err != nil { if err != nil {
return []string{"Failed to generate wireguard keypair: ", err.Error()} return []string{"Failed to generate wireguard keypair: ", err.Error()}
} }
return []string{"PrivateKey: " + privateKey.String(), "PublicKey: " + privateKey.PublicKey().String()} return []string{"PrivateKey: " + wgKeys.String(), "PublicKey: " + wgKeys.PublicKey().String()}
} }
+8 -8
View File
@@ -26,8 +26,7 @@ var defaultConfig = `{
"protocol": [ "protocol": [
"dns" "dns"
], ],
"outbound": "dns-out", "action": "hijack-dns"
"action": "route"
} }
] ]
}, },
@@ -351,13 +350,14 @@ func (s *SettingService) SaveConfig(tx *gorm.DB, config json.RawMessage) error {
return tx.Model(model.Setting{}).Where("key = ?", "config").Update("value", string(configs)).Error return tx.Model(model.Setting{}).Where("key = ?", "config").Update("value", string(configs)).Error
} }
func (s *SettingService) Save(tx *gorm.DB, changes []model.Changes) error { func (s *SettingService) Save(tx *gorm.DB, data json.RawMessage) error {
var err error var err error
for _, change := range changes { var settings map[string]string
key := change.Key err = json.Unmarshal(data, &settings)
var obj string if err != nil {
json.Unmarshal(change.Obj, &obj) return err
}
for key, obj := range settings {
// Secure file existance check // Secure file existance check
if obj != "" && (key == "webCertFile" || if obj != "" && (key == "webCertFile" ||
key == "webKeyFile" || key == "webKeyFile" ||
+1 -3
View File
@@ -6,8 +6,6 @@ import (
"s-ui/logger" "s-ui/logger"
"s-ui/util/common" "s-ui/util/common"
"time" "time"
"gorm.io/gorm"
) )
type UserService struct { type UserService struct {
@@ -63,7 +61,7 @@ func (s *UserService) CheckUser(username string, password string, remoteIP strin
Where("username = ? and password = ?", username, password). Where("username = ? and password = ?", username, password).
First(user). First(user).
Error Error
if err == gorm.ErrRecordNotFound { if database.IsNotFound(err) {
return nil return nil
} else if err != nil { } else if err != nil {
logger.Warning("check user err:", err, " IP: ", remoteIP) logger.Warning("check user err:", err, " IP: ", remoteIP)
+229
View File
@@ -0,0 +1,229 @@
package service
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"s-ui/database/model"
"s-ui/logger"
"s-ui/util/common"
"strconv"
"time"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
)
type WarpService struct{}
func (s *WarpService) getWarpInfo(ep *model.Endpoint) ([]byte, error) {
var warpData map[string]string
err := json.Unmarshal(ep.Ext, &warpData)
if err != nil {
return nil, err
}
url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s", warpData["device_id"])
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
buffer := bytes.NewBuffer(make([]byte, 8192))
buffer.Reset()
_, err = buffer.ReadFrom(resp.Body)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
func (s *WarpService) RegisterWarp(ep *model.Endpoint) error {
tos := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
privateKey, _ := wgtypes.GenerateKey()
publicKey := privateKey.PublicKey().String()
hostName, _ := os.Hostname()
data := fmt.Sprintf(`{"key":"%s","tos":"%s","type": "PC","model": "s-ui", "name": "%s"}`, publicKey, tos, hostName)
url := "https://api.cloudflareclient.com/v0a2158/reg"
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
if err != nil {
return err
}
req.Header.Add("CF-Client-Version", "a-7.21-0721")
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
buffer := bytes.NewBuffer(make([]byte, 8192))
buffer.Reset()
_, err = buffer.ReadFrom(resp.Body)
if err != nil {
return err
}
var rspData map[string]interface{}
err = json.Unmarshal(buffer.Bytes(), &rspData)
if err != nil {
return err
}
deviceId := rspData["id"].(string)
token := rspData["token"].(string)
license, ok := rspData["account"].(map[string]interface{})["license"].(string)
if !ok {
logger.Debug("Error accessing license value.")
return err
}
warpData := map[string]string{
"access_token": token,
"device_id": deviceId,
"license_key": license,
}
ep.Ext, err = json.MarshalIndent(warpData, "", " ")
if err != nil {
return err
}
warpInfo, err := s.getWarpInfo(ep)
if err != nil {
return err
}
var warpDetails map[string]interface{}
err = json.Unmarshal(warpInfo, &warpDetails)
if err != nil {
return err
}
warpConfig, _ := warpDetails["config"].(map[string]interface{})
clientId, _ := warpConfig["client_id"].(string)
reserved := s.getReserved(clientId)
interfaceConfig, _ := warpConfig["interface"].(map[string]interface{})
addresses, _ := interfaceConfig["addresses"].(map[string]interface{})
v4, _ := addresses["v4"].(string)
v6, _ := addresses["v6"].(string)
peer, _ := warpConfig["peers"].([]interface{})[0].(map[string]interface{})
peerEndpoint, _ := peer["endpoint"].(map[string]interface{})["host"].(string)
peerEpAddress, peerEpPort, err := net.SplitHostPort(peerEndpoint)
if err != nil {
return err
}
peerPublicKey, _ := peer["public_key"].(string)
peerPort, _ := strconv.Atoi(peerEpPort)
peers := []map[string]interface{}{
{
"address": peerEpAddress,
"port": peerPort,
"public_key": peerPublicKey,
"allowed_ips": []string{"0.0.0.0/0", "::/0"},
"reserved": reserved,
},
}
var epOptions map[string]interface{}
err = json.Unmarshal(ep.Options, &epOptions)
if err != nil {
return err
}
epOptions["private_key"] = privateKey.String()
epOptions["address"] = []string{fmt.Sprintf("%s/32", v4), fmt.Sprintf("%s/128", v6)}
epOptions["listen_port"] = 0
epOptions["peers"] = peers
ep.Options, err = json.MarshalIndent(epOptions, "", " ")
return err
}
func (s *WarpService) getReserved(clientID string) []int {
var reserved []int
decoded, err := base64.StdEncoding.DecodeString(clientID)
if err != nil {
return nil
}
hexString := ""
for _, char := range decoded {
hex := fmt.Sprintf("%02x", char)
hexString += hex
}
for i := 0; i < len(hexString); i += 2 {
hexByte := hexString[i : i+2]
decValue, err := strconv.ParseInt(hexByte, 16, 32)
if err != nil {
return nil
}
reserved = append(reserved, int(decValue))
}
return reserved
}
func (s *WarpService) SetWarpLicense(old_license string, ep *model.Endpoint) error {
var warpData map[string]string
err := json.Unmarshal(ep.Ext, &warpData)
if err != nil {
return err
}
if warpData["license_key"] == old_license {
return nil
}
url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s/account", warpData["device_id"])
data := fmt.Sprintf(`{"license": "%s"}`, warpData["license_key"])
req, err := http.NewRequest("PUT", url, bytes.NewBuffer([]byte(data)))
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
buffer := bytes.NewBuffer(make([]byte, 8192))
buffer.Reset()
_, err = buffer.ReadFrom(resp.Body)
if err != nil {
return err
}
var response map[string]interface{}
err = json.Unmarshal(buffer.Bytes(), &response)
if err != nil {
return err
}
if success, ok := response["success"].(bool); ok && success == false {
errorArr, _ := response["errors"].([]interface{})
errorObj := errorArr[0].(map[string]interface{})
return common.NewError(errorObj["code"], errorObj["message"])
}
return nil
}
-10
View File
@@ -21,7 +21,6 @@ const defaultJson = `
"mtu": 9000, "mtu": 9000,
"auto_route": true, "auto_route": true,
"strict_route": false, "strict_route": false,
"sniff": true,
"endpoint_independent_nat": false, "endpoint_independent_nat": false,
"stack": "system", "stack": "system",
"platform": { "platform": {
@@ -36,7 +35,6 @@ const defaultJson = `
"type": "mixed", "type": "mixed",
"listen": "127.0.0.1", "listen": "127.0.0.1",
"listen_port": 2080, "listen_port": 2080,
"sniff": true,
"users": [] "users": []
} }
] ]
@@ -204,14 +202,6 @@ func (j *JsonService) addDefaultOutbounds(outbounds *[]map[string]interface{}, o
"type": "direct", "type": "direct",
"tag": "direct", "tag": "direct",
}, },
{
"type": "dns",
"tag": "dns-out",
},
{
"type": "block",
"tag": "block",
},
} }
*outbounds = append(outbound, *outbounds...) *outbounds = append(outbound, *outbounds...)
} }
+4
View File
@@ -24,3 +24,7 @@ func Random(n int) string {
} }
return string(runes) return string(runes)
} }
func RandomInt(n int) int {
return rnd.Intn(n)
}
+49 -5
View File
@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"net/url" "net/url"
"s-ui/database/model" "s-ui/database/model"
"s-ui/util/common"
"strings" "strings"
) )
@@ -19,7 +20,7 @@ func LinkGenerator(clientConfig json.RawMessage, i *model.Inbound, hostname stri
var tls map[string]interface{} var tls map[string]interface{}
if i.TlsId > 0 { if i.TlsId > 0 {
json.Unmarshal(i.Tls.Client, &tls) tls = prepareTls(i.Tls)
} }
var userConfig map[string]map[string]interface{} var userConfig map[string]map[string]interface{}
@@ -42,17 +43,18 @@ func LinkGenerator(clientConfig json.RawMessage, i *model.Inbound, hostname stri
for index, addr := range Addrs { for index, addr := range Addrs {
addrRemark, _ := addr["remark"].(string) addrRemark, _ := addr["remark"].(string)
Addrs[index]["remark"] = i.Tag + addrRemark Addrs[index]["remark"] = i.Tag + addrRemark
if addrTls, ok := addr["tls"].(map[string]interface{}); ok { if i.TlsId > 0 {
newTls := map[string]interface{}{} newTls := map[string]interface{}{}
if oldTls, hasOldTls := tls["tls"].(map[string]interface{}); hasOldTls { for k, v := range tls {
for k, v := range oldTls {
newTls[k] = v newTls[k] = v
} }
}
// Override tls // Override tls
if addrTls, ok := addr["tls"].(map[string]interface{}); ok {
for k, v := range addrTls { for k, v := range addrTls {
newTls[k] = v newTls[k] = v
} }
}
Addrs[index]["tls"] = newTls Addrs[index]["tls"] = newTls
} }
} }
@@ -80,6 +82,28 @@ func LinkGenerator(clientConfig json.RawMessage, i *model.Inbound, hostname stri
return []string{} return []string{}
} }
func prepareTls(t *model.Tls) map[string]interface{} {
var iTls, oTls map[string]interface{}
json.Unmarshal(t.Client, &oTls)
json.Unmarshal(t.Server, &iTls)
for k, v := range iTls {
switch k {
case "enabled", "server_name", "alpn":
oTls[k] = v
case "reality":
reality := v.(map[string]interface{})
clientReality := oTls["reality"].(map[string]interface{})
clientReality["enabled"] = reality["enabled"]
if short_ids, hasSIds := reality["short_ids"].([]interface{}); hasSIds && len(short_ids) > 0 {
clientReality["short_id"] = short_ids[common.RandomInt(len(short_ids))]
}
oTls["reality"] = clientReality
}
}
return oTls
}
func shadowsocksLink( func shadowsocksLink(
userConfig map[string]map[string]interface{}, userConfig map[string]map[string]interface{},
inbound map[string]interface{}, inbound map[string]interface{},
@@ -507,3 +531,23 @@ func getTransportParams(t interface{}) map[string]string {
} }
return params return params
} }
func getTlsParams(t interface{}) map[string]string {
params := map[string]string{}
if tls, hasTls := t.(map[string]interface{}); hasTls {
if sni, ok := tls["server_name"].(string); ok {
params["sni"] = sni
}
if alpn, ok := tls["alpn"].([]interface{}); ok {
alpnList := make([]string, len(alpn))
for i, v := range alpn {
alpnList[i] = v.(string)
}
params["alpn"] = strings.Join(alpnList, ",")
}
if insecure, ok := tls["insecure"].(bool); ok && insecure {
params["insecure"] = "1"
}
}
return params
}
+7
View File
@@ -98,6 +98,13 @@ func addTls(out *map[string]interface{}, tls *model.Tls) {
} }
tlsConfig["reality"] = realityConfig tlsConfig["reality"] = realityConfig
} }
if ech, ok := tlsServer["ech"].(map[string]interface{}); ok && ech["enabled"].(bool) {
echConfig := tlsConfig["ech"].(map[string]interface{})
echConfig["enabled"] = true
echConfig["pq_signature_schemes_enabled"] = ech["pq_signature_schemes_enabled"]
echConfig["dynamic_record_sizing_disabled"] = ech["dynamic_record_sizing_disabled"]
tlsConfig["ech"] = echConfig
}
(*out)["tls"] = tlsConfig (*out)["tls"] = tlsConfig
} }
+675 -380
View File
File diff suppressed because it is too large Load Diff
+22 -22
View File
@@ -1,6 +1,6 @@
{ {
"name": "frontend", "name": "frontend",
"version": "1.2.0-beta.1", "version": "1.2.0-beta.3",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",
@@ -10,33 +10,33 @@
}, },
"dependencies": { "dependencies": {
"@mdi/font": "7.4.47", "@mdi/font": "7.4.47",
"axios": "^1.7.4", "axios": "^1.7.9",
"chart.js": "^4.4.3", "chart.js": "^4.4.7",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"core-js": "^3.37.1", "core-js": "^3.40.0",
"moment": "^2.30.1", "moment": "^2.30.1",
"notivue": "^2.4.4", "notivue": "^2.4.5",
"pinia": "^2.1.7", "pinia": "^2.3.0",
"qrcode.vue": "^3.4.1", "qrcode.vue": "^3.6.0",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"vue": "^3.4.31", "vue": "^3.5.13",
"vue-chartjs": "^5.3.1", "vue-chartjs": "^5.3.2",
"vue-i18n": "^9.14.2", "vue-i18n": "^11.0.1",
"vue-router": "^4.4.0", "vue-router": "^4.5.0",
"vue3-persian-datetime-picker": "^1.2.2", "vue3-persian-datetime-picker": "^1.2.2",
"vuetify": "^3.6.10" "vuetify": "^3.7.6"
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.24.7", "@babel/types": "^7.26.5",
"@types/node": "^20.14.9", "@types/node": "^22.10.7",
"@vitejs/plugin-vue": "^5.0.5", "@vitejs/plugin-vue": "^5.2.1",
"eslint-plugin-vue": "^9.26.0", "eslint-plugin-vue": "^9.32.0",
"material-design-icons-iconfont": "^6.7.0", "material-design-icons-iconfont": "^6.7.0",
"sass": "1.77.6", "sass": "1.83.4",
"typescript": "^5.5.2", "typescript": "^5.7.3",
"unplugin-fonts": "^1.1.1", "unplugin-fonts": "^1.3.1",
"vite": "^5.4.6", "vite": "^6.0.7",
"vite-plugin-vuetify": "^2.0.3", "vite-plugin-vuetify": "^2.0.4",
"vue-tsc": "^2.0.22" "vue-tsc": "^2.2.0"
} }
} }
+7 -25
View File
@@ -1,9 +1,6 @@
<template> <template>
<LogVue <LogVue v-model="logModal.visible" :control="logModal" :visible="logModal.visible" />
v-model="logModal.visible" <Backup v-model="backupModal.visible" :control="backupModal" :visible="backupModal.visible" />
:visible="logModal.visible"
@close="closeLogs"
/>
<v-container class="fill-height" :loading="loading"> <v-container class="fill-height" :loading="loading">
<v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" > <v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" >
<v-row class="d-flex align-center justify-center"> <v-row class="d-flex align-center justify-center">
@@ -46,6 +43,8 @@
</v-row> </v-row>
</v-card> </v-card>
</v-dialog> </v-dialog>
<v-btn variant="tonal" hide-details style="margin-inline-start: 10px;" @click="backupModal.visible = true">{{ $t('main.backup.title') }} <v-icon icon="mdi-backup-restore" /></v-btn>
<v-btn variant="tonal" hide-details style="margin-inline-start: 10px;" @click="logModal.visible = true">{{ $t('basic.log.title') }} <v-icon icon="mdi-list-box-outline" /></v-btn>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
@@ -86,18 +85,8 @@
<v-col cols="3">S-UI</v-col> <v-col cols="3">S-UI</v-col>
<v-col cols="9"> <v-col cols="9">
<v-chip density="compact" color="blue"> <v-chip density="compact" color="blue">
<v-tooltip activator="parent" location="top">
{{ $t('main.info.threads') }}: {{ tilesData.sys?.appThreads }}<br />
{{ $t('main.info.memory') }}: {{ HumanReadable.sizeFormat(tilesData.sys?.appMem) }}
</v-tooltip>
v{{ tilesData.sys?.appVersion }} v{{ tilesData.sys?.appVersion }}
</v-chip> </v-chip>
<v-chip density="compact" color="transparent" style="cursor: pointer;" @click="openLogs()">
<v-tooltip activator="parent" location="top">
{{ $t('basic.log.title') + " - S-UI" }}
</v-tooltip>
<v-icon icon="mdi-list-box-outline" color="blue" />
</v-chip>
</v-col> </v-col>
<v-col cols="3">{{ $t('main.info.uptime') }}</v-col> <v-col cols="3">{{ $t('main.info.uptime') }}</v-col>
<v-col cols="9">{{ HumanReadable.formatSecond(tilesData.uptime) }}</v-col> <v-col cols="9">{{ HumanReadable.formatSecond(tilesData.uptime) }}</v-col>
@@ -166,6 +155,7 @@ import History from '@/components/tiles/History.vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { i18n } from '@/locales' import { i18n } from '@/locales'
import LogVue from '@/layouts/modals/Logs.vue' import LogVue from '@/layouts/modals/Logs.vue'
import Backup from '@/layouts/modals/Backup.vue'
const loading = ref(false) const loading = ref(false)
const menu = ref(false) const menu = ref(false)
@@ -235,17 +225,9 @@ onBeforeUnmount(() => {
stopTimer() stopTimer()
}) })
const logModal = ref({ const logModal = ref({ visible: false })
visible: false,
})
const openLogs = () => { const backupModal = ref({ visible: false })
logModal.value.visible = true
}
const closeLogs = () => {
logModal.value.visible = false
}
const restartSingbox = async () => { const restartSingbox = async () => {
loading.value = true loading.value = true
+14 -16
View File
@@ -153,7 +153,6 @@ export default {
"mtu": 9000, "mtu": 9000,
"auto_route": true, "auto_route": true,
"strict_route": false, "strict_route": false,
"sniff": true,
"endpoint_independent_nat": false, "endpoint_independent_nat": false,
"stack": "system", "stack": "system",
"exclude_package": [], "exclude_package": [],
@@ -169,7 +168,6 @@ export default {
"type": "mixed", "type": "mixed",
"listen": "127.0.0.1", "listen": "127.0.0.1",
"listen_port": 2080, "listen_port": 2080,
"sniff": true,
"users": [] "users": []
} }
], ],
@@ -199,24 +197,24 @@ export default {
"tag": "local-dns", "tag": "local-dns",
"address": "local", "address": "local",
"detour": "direct" "detour": "direct"
},
{
"address": "rcode://success",
"tag": "block"
} }
], ],
"rules": [ "rules": [
{ {
"clash_mode": "Global", "clash_mode": "Global",
"source_ip_cidr": [ "source_ip_cidr": [
"172.19.0.0/30" "172.19.0.0/30",
"fdfe:dcba:9876::1/126"
], ],
"action": "route",
"server": "proxy-dns" "server": "proxy-dns"
}, },
{ {
"source_ip_cidr": [ "source_ip_cidr": [
"172.19.0.0/30" "172.19.0.0/30",
"fdfe:dcba:9876::1/126"
], ],
"action": "route",
"server": "proxy-dns" "server": "proxy-dns"
} }
], ],
@@ -319,10 +317,10 @@ export default {
if (v) { if (v) {
this.subJsonExt.dns = this.defaultDns this.subJsonExt.dns = this.defaultDns
if (this.rules == undefined) this.subJsonExt.rules = [] if (this.rules == undefined) this.subJsonExt.rules = []
this.subJsonExt.rules.unshift({ protocol: "dns", outbound: "dns-out" }) this.subJsonExt.rules.unshift({ protocol: "dns", action: "hijack-dns" })
} else { } else {
delete this.subJsonExt.dns delete this.subJsonExt.dns
const ruleDnsIndex = this.subJsonExt?.rules?.findIndex((r:any) => r.protocol == "dns" && r.outbound == "dns-out") const ruleDnsIndex = this.subJsonExt?.rules?.findIndex((r:any) => r.protocol == "dns" && r.action == "hijack-dns")
if (ruleDnsIndex >= 0) this.subJsonExt.rules.splice(ruleDnsIndex,1) if (ruleDnsIndex >= 0) this.subJsonExt.rules.splice(ruleDnsIndex,1)
if (this.rules.length == 0) delete this.subJsonExt.rules if (this.rules.length == 0) delete this.subJsonExt.rules
} }
@@ -348,7 +346,7 @@ export default {
if (v?.length>0) { if (v?.length>0) {
if (sIndex === -1) { if (sIndex === -1) {
this.dns.servers.push({ tag: "direct-dns", address: v, detour: "direct" }) this.dns.servers.push({ tag: "direct-dns", address: v, detour: "direct" })
this.dns.rules.push({ clash_mode: "Direct", server: "direct-dns" }) this.dns.rules.push({ clash_mode: "Direct", action: "route", server: "direct-dns" })
} else { } else {
this.dns.servers[sIndex].address = v this.dns.servers[sIndex].address = v
} }
@@ -369,7 +367,7 @@ export default {
if (ruleIndex >= 0){ if (ruleIndex >= 0){
this.dns.rules[ruleIndex].rule_set = v this.dns.rules[ruleIndex].rule_set = v
} else { } else {
this.dns.rules.push({ rule_set: v, server: "direct-dns" }) this.dns.rules.push({ rule_set: v, action: "route", server: "direct-dns" })
} }
} else { } else {
if (ruleIndex != -1) this.dns.rules.splice(ruleIndex,1) if (ruleIndex != -1) this.dns.rules.splice(ruleIndex,1)
@@ -395,7 +393,7 @@ export default {
this.rules[ruleIndex].rule_set = v this.rules[ruleIndex].rule_set = v
} else { } else {
if (this.rules == undefined) this.subJsonExt.rules = [] if (this.rules == undefined) this.subJsonExt.rules = []
this.rules.push({ rule_set: v, outbound: "direct" }) this.rules.push({ rule_set: v, action: "route", outbound: "direct" })
} }
} else { } else {
if (ruleIndex != -1) this.rules.splice(ruleIndex,1) if (ruleIndex != -1) this.rules.splice(ruleIndex,1)
@@ -405,17 +403,17 @@ export default {
}, },
ruleToBlock: { ruleToBlock: {
get() :string[] { get() :string[] {
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "block" && Object.hasOwn(r,'rule_set')) const ruleIndex = this.rules?.findIndex((r:any) => r.action == "reject" && Object.hasOwn(r,'rule_set'))
return ruleIndex >= 0 ? this.rules[ruleIndex].rule_set : [] return ruleIndex >= 0 ? this.rules[ruleIndex].rule_set : []
}, },
set(v:string[]) { set(v:string[]) {
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "block" && Object.hasOwn(r,'rule_set')) const ruleIndex = this.rules?.findIndex((r:any) => r.action == "reject" && Object.hasOwn(r,'rule_set'))
if (v.length>0) { if (v.length>0) {
if (ruleIndex >= 0){ if (ruleIndex >= 0){
this.rules[ruleIndex].rule_set = v this.rules[ruleIndex].rule_set = v
} else { } else {
if (this.rules == undefined) this.subJsonExt.rules = [] if (this.rules == undefined) this.subJsonExt.rules = []
this.rules.push({ rule_set: v, outbound: "block" }) this.rules.push({ rule_set: v, action: "reject" })
} }
} else { } else {
if (ruleIndex != -1) this.rules.splice(ruleIndex,1) if (ruleIndex != -1) this.rules.splice(ruleIndex,1)
+20 -13
View File
@@ -2,34 +2,41 @@
<v-card :subtitle="$t('pages.clients')"> <v-card :subtitle="$t('pages.clients')">
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-switch <v-select v-model="data.model" :items="initUsersModels" @update:model-value="data.values = []" hide-details></v-select>
v-model="hasUser" </v-col>
@change="() => {inbound.users = hasUser? [] : undefined}" <v-col cols="12" sm="6" md="4" v-if="data.model == 'group'">
color="primary" <v-select v-model="data.values" multiple chips :items="groupNames" :label="$t('client.group')" hide-details></v-select>
:label="$t('in.clients')" </v-col>
hide-details></v-switch> <v-col cols="12" sm="8" v-if="data.model == 'client'">
<v-select v-model="data.values" multiple chips :items="clientNames" :label="$t('pages.clients')" hide-details></v-select>
</v-col> </v-col>
</v-row> </v-row>
</v-card> </v-card>
</template> </template>
<script lang="ts"> <script lang="ts">
import { i18n } from '@/locales';
export default { export default {
props: ['inbound'], props: ['data', 'clients'],
data() { data() {
return { return {
hasUser: false, initUsersModels: [
{ title: i18n.global.t('none'), value: 'none' },
{ title: i18n.global.t('all'), value: 'all' },
{ title: i18n.global.t('client.group'), value: 'group' },
{ title: i18n.global.t('pages.clients'), value: 'client' },
],
} }
}, },
computed: { computed: {
cardTitle() { clientNames() {
this.hasUser = Object.hasOwn(this.$props.inbound,'users') return this.$props.clients.map((c:any) => { return { title: c.name, value: c.id } } )
return this.$props.inbound?.type.toUpperCase()
}, },
groupNames() {
return Array.from(new Set(this.$props.clients.map((c:any) => c.group)))
}, },
mounted() {
this.hasUser = Object.hasOwn(this.$props.inbound,'users')
} }
} }
</script> </script>
+7 -2
View File
@@ -13,7 +13,7 @@
type="number" type="number"
min="0" min="0"
hide-details hide-details
v-model="port"> v-model.number="port">
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
@@ -21,8 +21,9 @@
label="KeepAlive" label="KeepAlive"
type="number" type="number"
min="0" min="0"
:suffix="$t('date.s')"
hide-details hide-details
v-model="data.persistent_keepalive_interval"> v-model.number="keepAlive">
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@@ -72,6 +73,10 @@ export default {
port: { port: {
get() { return this.$props.data.port }, get() { return this.$props.data.port },
set(v:number) { this.$props.data.port = v > 0 ? v : undefined } set(v:number) { this.$props.data.port = v > 0 ? v : undefined }
},
keepAlive: {
get() { return this.$props.data.persistent_keepalive_interval?? 0 },
set(v:number) { this.$props.data.persistent_keepalive_interval = v > 0 ? v : undefined }
} }
} }
} }
@@ -77,12 +77,12 @@
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-switch
label="Rewrite Host" label="Rewrite Host"
placeholder="example.com"
v-model="data.masquerade.rewrite_host" v-model="data.masquerade.rewrite_host"
color="primary"
hide-details> hide-details>
</v-text-field> </v-switch>
</v-col> </v-col>
</v-row> </v-row>
<template v-if="masqueradeType == 'string'"> <template v-if="masqueradeType == 'string'">
@@ -6,6 +6,7 @@
hide-details hide-details
:items="[1,2,3]" :items="[1,2,3]"
:label="$t('version')" :label="$t('version')"
:disabled="data.id > 0"
v-model="version"> v-model="version">
</v-select> </v-select>
</v-col> </v-col>
@@ -115,23 +116,18 @@ export default {
switch (newValue) { switch (newValue) {
case 1: case 1:
delete this.Inbound.password delete this.Inbound.password
delete this.Inbound.users
delete this.Inbound.handshake_for_server_name delete this.Inbound.handshake_for_server_name
break; break;
case 2: case 2:
if (!this.Inbound.password) { if (!this.Inbound.password) {
this.Inbound.password = "" this.Inbound.password = ""
} }
delete this.Inbound.users
if (!this.Inbound.handshake_for_server_name) { if (!this.Inbound.handshake_for_server_name) {
this.Inbound.handshake_for_server_name = {} this.Inbound.handshake_for_server_name = {}
} }
break; break;
case 3: case 3:
delete this.Inbound.password delete this.Inbound.password
if (!Object.hasOwn(this.Inbound, 'users')) {
this.Inbound.users = []
}
if (!this.Inbound.handshake_for_server_name) { if (!this.Inbound.handshake_for_server_name) {
this.Inbound.handshake_for_server_name = {} this.Inbound.handshake_for_server_name = {}
} }
+156
View File
@@ -0,0 +1,156 @@
<template>
<v-card subtitle="Warp">
<template v-if="data.id>0">
<table dir="ltr" width="100%">
<tbody>
<tr>
<td>Device ID</td>
<td>{{ data.ext.device_id }}</td>
</tr>
<tr>
<td>Access Token</td>
<td>{{ data.ext.access_token }}</td>
</tr>
<tr>
<td>{{ $t('types.wg.privKey') }}</td>
<td>{{ data.private_key }}</td>
</tr>
<tr>
<td>{{ $t('types.wg.localIp') }}</td>
<td>{{ data.address.join(',') }}</td>
</tr>
<tr>
<td colspan="2">
<v-text-field
v-model="data.ext.license_key"
label="License Key"
hide-details>
</v-text-field>
</td>
</tr>
</tbody>
</table>
<v-card :subtitle="$t('types.wg.peer')">
<table dir="ltr" width="100%">
<tbody>
<tr>
<td>{{ $t('out.addr') }}</td>
<td>{{ data.peers[0].address + ":" + data.peers[0].port }}</td>
</tr>
<tr>
<td>{{ $t('types.wg.pubKey') }}</td>
<td>{{ data.peers[0].public_key }}</td>
</tr>
<tr>
<td>{{ $t('types.wg.allowedIp') }}</td>
<td>{{ data.peers[0].allowed_ips.join(',') }}</td>
</tr>
<tr>
<td>Reserved</td>
<td>[{{ data.peers[0].reserved.join(',') }}]</td>
</tr>
</tbody>
</table>
</v-card>
</template>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="data.udp_timeout != undefined">
<v-text-field
label="UDP Timeout"
hide-details
type="number"
min=0
:suffix="$t('date.m')"
v-model.number="udp_timeout">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.workers != undefined">
<v-text-field
:label="$t('types.wg.worker')"
hide-details
type="number"
min=1
v-model.number="data.workers">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.mtu != undefined">
<v-text-field
label="MTU"
hide-details
type="number"
min=0
v-model.number="data.mtu">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.system" color="primary" :label="$t('types.wg.sysIf')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.system">
<v-text-field
:label="$t('types.wg.ifName')"
hide-details
v-model="ifName">
</v-text-field>
</v-col>
</v-row>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.wg.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionUdp" color="primary" label="UDP Timeout" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionWorker" color="primary" :label="$t('types.wg.worker')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionMtu" color="primary" label="MTU" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
export default {
props: ['data'],
data() {
return {
menu: false,
}
},
methods: {
},
computed: {
optionUdp: {
get(): boolean { return this.$props.data.udp_timeout != undefined },
set(v:boolean) { this.$props.data.udp_timeout = v ? "5m" : undefined }
},
optionWorker: {
get(): boolean { return this.$props.data.workers != undefined },
set(v:boolean) { this.$props.data.workers = v ? 2 : undefined }
},
optionMtu: {
get(): boolean { return this.$props.data.mtu != undefined },
set(v:boolean) { this.$props.data.mtu = v ? 1408 : undefined }
},
ifName: {
get() { return this.$props.data.name?? '' },
set(v:string) { this.$props.data.name = v.length > 0 ? v : undefined }
},
udp_timeout: {
get() { return this.$props.data.udp_timeout ? parseInt(this.$props.data.udp_timeout.replace('m','')) : 5 },
set(v:number) { this.$props.data.udp_timeout = v > 0 ? v + 'm' : '5m' }
}
}
}
</script>
+22 -10
View File
@@ -10,6 +10,16 @@
hide-details> hide-details>
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="8">
<v-text-field
v-model="options.public_key"
readonly
:label="$t('tls.pubKey')"
append-icon="mdi-refresh"
@click:append="getWgPubKey()"
hide-details>
</v-text-field>
</v-col>
<v-col cols="12" sm="8"> <v-col cols="12" sm="8">
<v-text-field v-model="address" :label="$t('types.wg.localIp') + ' ' + $t('commaSeparated')" hide-details></v-text-field> <v-text-field v-model="address" :label="$t('types.wg.localIp') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
</v-col> </v-col>
@@ -59,11 +69,11 @@
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-switch v-model="data.system" color="primary" :label="$t('types.wg.sysIf')" hide-details></v-switch> <v-switch v-model="data.system" color="primary" :label="$t('types.wg.sysIf')" hide-details></v-switch>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4" v-if="data.name != undefined"> <v-col cols="12" sm="6" md="4" v-if="data.system">
<v-text-field <v-text-field
:label="$t('types.wg.ifName')" :label="$t('types.wg.ifName')"
hide-details hide-details
v-model="data.name"> v-model="ifName">
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@@ -84,9 +94,6 @@
<v-list-item> <v-list-item>
<v-switch v-model="optionMtu" color="primary" label="MTU" hide-details></v-switch> <v-switch v-model="optionMtu" color="primary" label="MTU" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item>
<v-switch v-model="optionInterface" color="primary" :label="$t('types.wg.ifName')" hide-details></v-switch>
</v-list-item>
</v-list> </v-list>
</v-card> </v-card>
</v-menu> </v-menu>
@@ -111,8 +118,8 @@
import Peer from '@/components/WgPeer.vue' import Peer from '@/components/WgPeer.vue'
export default { export default {
props: ['data'], props: ['data', 'options'],
emits: ["newWgKey"], emits: ['newWgKey', 'getWgPubKey'],
data() { data() {
return { return {
menu: false, menu: false,
@@ -125,6 +132,11 @@ export default {
newKey() { newKey() {
this.$emit('newWgKey') this.$emit('newWgKey')
}, },
getWgPubKey() {
const privKey = this.$props.data.private_key
if (privKey.length == 0) return
this.$emit('getWgPubKey', privKey)
},
}, },
computed: { computed: {
optionUdp: { optionUdp: {
@@ -143,9 +155,9 @@ export default {
get(): boolean { return this.$props.data.mtu != undefined }, get(): boolean { return this.$props.data.mtu != undefined },
set(v:boolean) { this.$props.data.mtu = v ? 1408 : undefined } set(v:boolean) { this.$props.data.mtu = v ? 1408 : undefined }
}, },
optionInterface: { ifName: {
get(): boolean { return this.$props.data.name != undefined }, get() { return this.$props.data.name?? '' },
set(v:boolean) { this.$props.data.name = v ? "" : undefined } set(v:string) { this.$props.data.name = v.length > 0 ? v : undefined }
}, },
address: { address: {
get() { return this.$props.data.address?.join(',') }, get() { return this.$props.data.address?.join(',') },
+91
View File
@@ -0,0 +1,91 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="90%" max-width="500">
<v-card class="rounded-lg">
<v-card-title>
<v-row>
<v-col>{{ $t('main.backup.title') }}</v-col>
<v-spacer></v-spacer>
<v-col cols="auto">
<v-icon icon="mdi-close" @click="control.visible = false" />
</v-col>
</v-row>
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-row>
<v-col cols="auto">
<v-checkbox v-model="exclude" :label="$t('main.backup.exclStats')" value="stats" hide-details></v-checkbox>
</v-col>
<v-col cols="auto">
<v-checkbox v-model="exclude" :label="$t('main.backup.exclChanges')" value="changes" hide-details></v-checkbox>
</v-col>
<v-spacer></v-spacer>
<v-col cols="auto" align-self="center">
<v-btn color="primary" @click="backup()" hide-details>{{ $t('main.backup.backup') }}</v-btn>
</v-col>
</v-row>
<v-row>
<v-spacer></v-spacer>
<v-col cols="auto" align-self="center">
<v-btn color="primary" @click="restore()" hide-details>{{ $t('main.backup.restore') }}</v-btn>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import HttpUtils from '@/plugins/httputil'
export default {
props: ['control', 'visible'],
data() {
return {
exclude: ["stats", "changes"],
}
},
methods: {
backup() {
const excludeOption = this.exclude.length>0 ? '?exclude=' +this.exclude.join(',') : ''
window.location.href = 'api/getdb' + excludeOption
},
restore() {
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.accept = '.db'
fileInput.addEventListener('change', async (event: Event) => {
const inputElement = event.target as HTMLInputElement
const dbFile = inputElement.files ? inputElement.files[0] : null
if (dbFile) {
const formData = new FormData()
formData.append('db', dbFile)
this.control.visible = false
const uploadMsg = await HttpUtils.post('api/importdb', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
})
if (uploadMsg.success) {
await new Promise(resolve => setTimeout(resolve, 1000))
location.reload()
}
}
})
fileInput.click()
}
},
watch: {
visible(v) {
if (v) {
this.exclude = ["stats", "changes"]
}
},
},
}
</script>
+29 -13
View File
@@ -1,12 +1,18 @@
<template> <template>
<v-dialog transition="dialog-bottom-transition" width="800"> <v-dialog transition="dialog-bottom-transition" width="800">
<v-card class="rounded-lg"> <v-card class="rounded-lg" :loading="loading">
<v-card-title> <v-card-title>
{{ $t('actions.' + title) + " " + $t('objects.client') }} {{ $t('actions.' + title) + " " + $t('objects.client') }}
</v-card-title> </v-card-title>
<v-divider></v-divider> <v-divider></v-divider>
<v-skeleton-loader
class="mx-auto border"
width="95%"
type="card, text, divider, list-item-two-line"
v-if="loading"
></v-skeleton-loader>
<v-card-text style="padding: 0 16px; overflow-y: scroll;"> <v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-container style="padding: 0;"> <v-container style="padding: 0;" :hidden="loading">
<v-tabs <v-tabs
v-model="tab" v-model="tab"
align-tabs="center" align-tabs="center"
@@ -74,10 +80,14 @@
v-model="clientInbounds" v-model="clientInbounds"
:items="inboundTags" :items="inboundTags"
:label="$t('client.inboundTags')" :label="$t('client.inboundTags')"
clearable
multiple multiple
chips chips
hide-details hide-details>
></v-select> <template v-slot:append>
<v-icon @click="setAllInbounds" icon="mdi-set-all" v-tooltip:top="$t('all')" />
</template>
</v-select>
</v-col> </v-col>
</v-row> </v-row>
</v-window-item> </v-window-item>
@@ -178,12 +188,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { createClient, randomConfigs, updateConfigs, Link } from '@/types/clients' import { createClient, randomConfigs, updateConfigs, Link, Client } from '@/types/clients'
import DatePick from '@/components/DateTime.vue' import DatePick from '@/components/DateTime.vue'
import { HumanReadable } from '@/plugins/utils' import { HumanReadable } from '@/plugins/utils'
import Data from '@/store/modules/data';
export default { export default {
props: ['visible', 'data', 'id', 'inboundTags', 'groups'], props: ['visible', 'id', 'inboundTags', 'groups'],
emits: ['close', 'save'], emits: ['close', 'save'],
data() { data() {
return { return {
@@ -198,21 +209,23 @@ export default {
} }
}, },
methods: { methods: {
updateData() { async updateData() {
if (this.$props.id > 0) { if (this.$props.id > 0) {
const newData = JSON.parse(this.$props.data) this.loading = true
const newData = await Data().loadClients(this.$props.id)
this.client = createClient(newData) this.client = createClient(newData)
this.title = "edit" this.title = "edit"
this.clientConfig = this.client.config this.clientConfig = this.client.config
this.loading = false
} }
else { else {
this.client = createClient() this.client = createClient()
this.title = "add" this.title = "add"
this.clientConfig = randomConfigs('client') this.clientConfig = randomConfigs('client')
} }
this.links = this.client.links.filter(l => l.type == 'local') this.links = this.client.links?.filter(l => l.type == 'local')?? []
this.extLinks = this.client.links.filter(l => l.type == 'external') this.extLinks = this.client.links?.filter(l => l.type == 'external')?? []
this.subLinks = this.client.links.filter(l => l.type == 'sub') this.subLinks = this.client.links?.filter(l => l.type == 'sub')?? []
this.tab = "t1" this.tab = "t1"
}, },
closeModal() { closeModal() {
@@ -230,12 +243,15 @@ export default {
}, },
setDate(newDate:number){ setDate(newDate:number){
this.client.expiry = newDate this.client.expiry = newDate
},
setAllInbounds(){
this.client.inbounds = this.inboundTags.map((i:any) => i.value).sort()
} }
}, },
computed: { computed: {
clientInbounds: { clientInbounds: {
get() { return this.client.inbounds.length>0 ? this.client.inbounds : [] }, get() { return this.client.inbounds.length>0 ? this.client.inbounds.sort() : [] },
set(v:number[]) { this.client.inbounds = v.length == 0 ? [] : v } set(v:number[]) { this.client.inbounds = v.length == 0 ? [] : v.sort() }
}, },
expDate: { expDate: {
get() { return this.client.expiry}, get() { return this.client.expiry},
@@ -61,7 +61,6 @@
</v-col> </v-col>
</v-row> </v-row>
</v-container> </v-container>
<pre dir="ltr">{{ bulkData }}</pre>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
+20 -6
View File
@@ -20,7 +20,8 @@
<v-text-field v-model="endpoint.tag" :label="$t('objects.tag')" hide-details></v-text-field> <v-text-field v-model="endpoint.tag" :label="$t('objects.tag')" hide-details></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<Wireguard v-if="endpoint.type == epTypes.Wireguard" :data="endpoint" @newWgKey="newWgKey" /> <Wireguard v-if="endpoint.type == epTypes.Wireguard" :data="endpoint" :options="options" @getWgPubKey="getWgPubKey" @newWgKey="newWgKey" />
<Warp v-if="endpoint.type == epTypes.Warp" :data="endpoint" />
<Dial :dial="endpoint" :outTags="tags" /> <Dial :dial="endpoint" :outTags="tags" />
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
@@ -50,6 +51,7 @@ import { EpTypes, createEndpoint } from '@/types/endpoints'
import RandomUtil from '@/plugins/randomUtil' import RandomUtil from '@/plugins/randomUtil'
import Dial from '@/components/Dial.vue' import Dial from '@/components/Dial.vue'
import Wireguard from '@/components/protocols/Wireguard.vue' import Wireguard from '@/components/protocols/Wireguard.vue'
import Warp from '@/components/protocols/Warp.vue'
import HttpUtils from '@/plugins/httputil' import HttpUtils from '@/plugins/httputil'
import { push } from 'notivue' import { push } from 'notivue'
import { i18n } from '@/locales' import { i18n } from '@/locales'
@@ -61,9 +63,9 @@ export default {
endpoint: createEndpoint("wireguard",{ "tag": "" }), endpoint: createEndpoint("wireguard",{ "tag": "" }),
title: "add", title: "add",
tab: "t1", tab: "t1",
link: "",
loading: false, loading: false,
epTypes: EpTypes, epTypes: EpTypes,
options: <any>{},
} }
}, },
methods: { methods: {
@@ -71,21 +73,24 @@ export default {
if (this.$props.id > 0) { if (this.$props.id > 0) {
const newData = JSON.parse(this.$props.data) const newData = JSON.parse(this.$props.data)
this.endpoint = createEndpoint(newData.type, newData) this.endpoint = createEndpoint(newData.type, newData)
this.options = {}
this.title = "edit" this.title = "edit"
} }
else { else {
const port = RandomUtil.randomIntRange(10000, 60000) const port = RandomUtil.randomIntRange(10000, 60000)
const randomIPoctet = RandomUtil.randomIntRange(1, 255) const randomIPoctet = RandomUtil.randomIntRange(1, 255)
const wgKeys = (await this.genWgKey())
this.endpoint = createEndpoint("wireguard",{ this.endpoint = createEndpoint("wireguard",{
tag: "wireguard-" + RandomUtil.randomSeq(3), tag: "wireguard-" + RandomUtil.randomSeq(3),
address: ['10.0.0.'+ randomIPoctet.toString() +'/32','fe80::'+ randomIPoctet.toString(16) +'/128'], address: ['10.0.0.'+ randomIPoctet.toString() +'/32','fe80::'+ randomIPoctet.toString(16) +'/128'],
listen_port: port, listen_port: port,
private_key: (await this.genWgKey()).private_key, private_key: wgKeys.private_key,
peers: [{ peers: [{
public_key: (await this.genWgKey()).public_key, public_key: (await this.genWgKey()).public_key,
allowed_ips: ['0.0.0.0/0', '::/0'] allowed_ips: ['0.0.0.0/0', '::/0']
}] }]
}) })
this.options.public_key = wgKeys.public_key
this.title = "add" this.title = "add"
} }
this.tab = "t1" this.tab = "t1"
@@ -130,15 +135,24 @@ export default {
async newWgKey(){ async newWgKey(){
const newKeys = await this.genWgKey() const newKeys = await this.genWgKey()
this.endpoint.private_key = newKeys.private_key this.endpoint.private_key = newKeys.private_key
this.options.public_key = newKeys.public_key
},
async getWgPubKey(private_key: string) {
this.loading = true
const msg = await HttpUtils.get('api/keypairs', { k: "wireguard", o: private_key })
if (msg.success) {
this.options.public_key = msg.obj
}
this.loading = false
} }
}, },
watch: { watch: {
visible(newValue) { visible(v) {
if (newValue) { if (v) {
this.updateData() this.updateData()
} }
}, },
}, },
components: { Dial, Wireguard } components: { Dial, Wireguard, Warp }
} }
</script> </script>
+35 -3
View File
@@ -50,7 +50,7 @@
<Tun v-if="inbound.type == inTypes.Tun" :data="inbound" /> <Tun v-if="inbound.type == inTypes.Tun" :data="inbound" />
<TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" /> <TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" />
<Transport v-if="Object.hasOwn(inbound,'transport')" :data="inbound" /> <Transport v-if="Object.hasOwn(inbound,'transport')" :data="inbound" />
<Users v-if="HasOptionalUser.includes(inbound.type)" :inbound="inbound" /> <Users v-if="hasUser" :clients="clients" :data="initUsers" />
<InTls v-if="HasTls.includes(inbound.type)" :inbound="inbound" :tlsConfigs="tlsConfigs" :tls_id="inbound.tls_id" /> <InTls v-if="HasTls.includes(inbound.type)" :inbound="inbound" :tlsConfigs="tlsConfigs" :tls_id="inbound.tls_id" />
<Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="in" :data="inbound" /> <Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="in" :data="inbound" />
</v-window-item> </v-window-item>
@@ -95,7 +95,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { InTypes, createInbound, Addr } from '@/types/inbounds' import { InTypes, createInbound, Addr, inboundWithUsers, ShadowTLS } from '@/types/inbounds'
import RandomUtil from '@/plugins/randomUtil' import RandomUtil from '@/plugins/randomUtil'
import Listen from '@/components/Listen.vue' import Listen from '@/components/Listen.vue'
@@ -125,7 +125,11 @@ export default {
loading: false, loading: false,
side: "s", side: "s",
inTypes: InTypes, inTypes: InTypes,
HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks], inboundWithUsers: inboundWithUsers,
initUsers: {
model: 'none',
values: <any>[],
},
HasInData: [ HasInData: [
InTypes.SOCKS, InTypes.SOCKS,
InTypes.HTTP, InTypes.HTTP,
@@ -179,6 +183,10 @@ export default {
this.loading = false this.loading = false
} }
this.side = "s" this.side = "s"
this.initUsers = {
model: 'none',
values: [],
}
}, },
changeType() { changeType() {
if (!this.inbound.listen_port) this.inbound.listen_port = RandomUtil.randomIntRange(10000, 60000) if (!this.inbound.listen_port) this.inbound.listen_port = RandomUtil.randomIntRange(10000, 60000)
@@ -205,7 +213,22 @@ export default {
}, },
saveChanges() { saveChanges() {
this.loading = true this.loading = true
if (this.hasUser) {
let clientIds = []
switch (this.initUsers.model) {
case 'all':
clientIds = this.clients.map((c:any) => c.id)
break
case 'group':
clientIds = this.clients.filter((c:any) => this.initUsers.values.includes(c.group)).map((c:any) => c.id)
break
case 'user':
clientIds = this.initUsers.values
}
this.$emit('save', this.inbound, clientIds.length > 0 ? clientIds : undefined)
} else {
this.$emit('save', this.inbound) this.$emit('save', this.inbound)
}
this.loading = false this.loading = false
}, },
}, },
@@ -217,6 +240,15 @@ export default {
if (this.OnlyTLS.includes(this.inbound.type) && this.inbound.tls_id == 0) return false if (this.OnlyTLS.includes(this.inbound.type) && this.inbound.tls_id == 0) return false
return true return true
}, },
clients() {
return Data().clients?? []
},
hasUser() {
if (this.$props.id > 0) return false
if (!inboundWithUsers.includes(this.inbound.type)) return false
if (this.inbound.type == InTypes.ShadowTLS && (<ShadowTLS>this.inbound).version < 3 ) return false
return true
}
}, },
watch: { watch: {
visible(newValue) { visible(newValue) {
+5 -5
View File
@@ -6,7 +6,7 @@
<v-col>{{ $t('basic.log.title') }}</v-col> <v-col>{{ $t('basic.log.title') }}</v-col>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-col cols="auto"> <v-col cols="auto">
<v-icon icon="mdi-close" @click="$emit('close')" /> <v-icon icon="mdi-close" @click="control.visible = false" />
</v-col> </v-col>
</v-row> </v-row>
</v-card-title> </v-card-title>
@@ -48,10 +48,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import HttpUtils from '@/plugins/httputil'; import HttpUtils from '@/plugins/httputil'
export default { export default {
props: ['visible'], props: ['control', 'visible'],
data() { data() {
return { return {
loading: false, loading: false,
@@ -77,11 +77,11 @@ export default {
} }
}, },
watch: { watch: {
visible(newValue) { visible(v) {
this.lines = [] this.lines = []
this.logLevel = 'info' this.logLevel = 'info'
this.logCount = 10 this.logCount = 10
if (newValue) { if (v) {
this.loadData() this.loadData()
} }
}, },
+23 -13
View File
@@ -1,6 +1,6 @@
<template> <template>
<v-dialog transition="dialog-bottom-transition" width="400"> <v-dialog transition="dialog-bottom-transition" width="400">
<v-card class="rounded-lg" id="qrcode-modal"> <v-card class="rounded-lg" id="qrcode-modal" :loading="loading">
<v-card-title> <v-card-title>
<v-row> <v-row>
<v-col>QrCode</v-col> <v-col>QrCode</v-col>
@@ -9,7 +9,13 @@
</v-row> </v-row>
</v-card-title> </v-card-title>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-text style="overflow-y: auto; padding: 0"> <v-skeleton-loader
class="mx-auto border"
width="80%"
type="text, image, divider, text, image"
v-if="loading"
></v-skeleton-loader>
<v-card-text style="overflow-y: auto; padding: 0" :hidden="loading">
<v-tabs <v-tabs
v-model="tab" v-model="tab"
density="compact" density="compact"
@@ -24,19 +30,19 @@
<v-row> <v-row>
<v-col style="text-align: center;"> <v-col style="text-align: center;">
<v-chip>{{ $t('setting.sub') }}</v-chip><br /> <v-chip>{{ $t('setting.sub') }}</v-chip><br />
<QrcodeVue :value="clientSub" :size="size" @click="copyToClipboard(clientSub)" :margin="1" style="border-radius: 1rem;" /> <QrcodeVue :value="clientSub" :size="size" @click="copyToClipboard(clientSub)" :margin="1" style="border-radius: 1rem; cursor: copy;" />
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col style="text-align: center;"> <v-col style="text-align: center;">
<v-chip>{{ $t('setting.jsonSub') }}</v-chip><br /> <v-chip>{{ $t('setting.jsonSub') }}</v-chip><br />
<QrcodeVue :value="clientSub + '?format=json'" :size="size" @click="copyToClipboard(clientSub + '?format=json')" :margin="1" style="border-radius: 1rem;" /> <QrcodeVue :value="clientSub + '?format=json'" :size="size" @click="copyToClipboard(clientSub + '?format=json')" :margin="1" style="border-radius: 1rem; cursor: copy;" />
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col style="text-align: center;"> <v-col style="text-align: center;">
<v-chip>SING-BOX</v-chip><br /> <v-chip>SING-BOX (scan only)</v-chip><br />
<QrcodeVue :value="singbox" :size="size" @click="copyToClipboard(singbox)" :margin="1" style="border-radius: .8rem;" /> <QrcodeVue :value="singbox" :size="size" :margin="1" style="border-radius: .8rem; cursor: not-allowed;" />
</v-col> </v-col>
</v-row> </v-row>
</v-window-item> </v-window-item>
@@ -44,7 +50,7 @@
<v-row v-for="l in clientLinks"> <v-row v-for="l in clientLinks">
<v-col style="text-align: center;"> <v-col style="text-align: center;">
<v-chip>{{ l.remark?? $t('client.' + l.type) }}</v-chip><br /> <v-chip>{{ l.remark?? $t('client.' + l.type) }}</v-chip><br />
<QrcodeVue :value="l.uri" :size="size" @click="copyToClipboard(l.uri)" :margin="1" style="border-radius: .5rem;" /> <QrcodeVue :value="l.uri" :size="size" @click="copyToClipboard(l.uri)" :margin="1" style="border-radius: .5rem; cursor: copy;" />
</v-col> </v-col>
</v-row> </v-row>
</v-window-item> </v-window-item>
@@ -62,13 +68,21 @@ import { i18n } from '@/locales'
import { push } from 'notivue' import { push } from 'notivue'
export default { export default {
props: ['index', 'visible'], props: ['id', 'visible'],
data() { data() {
return { return {
tab: "sub", tab: "sub",
client: <any>{},
loading: false,
} }
}, },
methods: { methods: {
async load() {
this.loading = true
const newData = await Data().loadClients(this.$props.id)
this.client = newData
this.loading = false
},
copyToClipboard(txt:string) { copyToClipboard(txt:string) {
const hiddenButton = document.createElement('button') const hiddenButton = document.createElement('button')
hiddenButton.className = 'clipboard-btn' hiddenButton.className = 'clipboard-btn'
@@ -101,11 +115,6 @@ export default {
} }
}, },
computed: { computed: {
clients() { return Data().clients },
client() {
if ( typeof this.$props.index != 'number' ) return <any>{}
return this.clients[this.$props.index]
},
clientSub() { clientSub() {
return Data().subURI + this.client.name return Data().subURI + this.client.name
}, },
@@ -126,6 +135,7 @@ export default {
visible(v) { visible(v) {
if (v) { if (v) {
this.tab = "sub" this.tab = "sub"
this.load()
} }
}, },
}, },
+12 -12
View File
@@ -113,7 +113,7 @@
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-select <v-select
v-model="ruleData.sniff" v-model="ruleData.sniffer"
:items="sniffers" :items="sniffers"
:label="$t('rule.sniffer')" :label="$t('rule.sniffer')"
multiple multiple
@@ -254,7 +254,7 @@ export default {
this.loading = true this.loading = true
let newRule = <any>{ let newRule = <any>{
action: this.ruleData.action, action: this.ruleData.action,
invert: this.ruleData.invert, invert: this.ruleData.invert? this.ruleData.invert : undefined,
} }
// Filter action data // Filter action data
@@ -263,25 +263,25 @@ export default {
newRule.outbound = this.ruleData.outbound newRule.outbound = this.ruleData.outbound
break break
case 'route-options': case 'route-options':
newRule.override_address = this.ruleData.override_address.length > 0 ? this.ruleData.override_address : undefined newRule.override_address = this.ruleData.override_address?.length > 0 ? this.ruleData.override_address : undefined
newRule.override_port = this.ruleData.override_port > 0 ? this.ruleData.override_port : undefined newRule.override_port = this.ruleData?.override_port > 0 ? this.ruleData.override_port : undefined
newRule.network_strategy = this.ruleData.network_strategy.length > 0 ? this.ruleData.network_strategy : undefined newRule.network_strategy = this.ruleData.network_strategy?.length > 0 ? this.ruleData.network_strategy : undefined
newRule.fallback_delay = this.ruleData.fallback_delay.length > 0 ? this.ruleData.fallback_delay : undefined newRule.fallback_delay = this.ruleData.fallback_delay?.length > 0 ? this.ruleData.fallback_delay : undefined
newRule.udp_disable_domain_unmapping = this.ruleData.udp_disable_domain_unmapping? true : undefined newRule.udp_disable_domain_unmapping = this.ruleData.udp_disable_domain_unmapping? true : undefined
newRule.udp_connect = this.ruleData.udp_connect? true : undefined newRule.udp_connect = this.ruleData.udp_connect? true : undefined
newRule.udp_timeout = this.ruleData.udp_timeout.length > 0 ? this.ruleData.udp_timeout : undefined newRule.udp_timeout = this.ruleData.udp_timeout?.length > 0 ? this.ruleData.udp_timeout : undefined
break break
case 'reject': case 'reject':
newRule.method = this.ruleData.method.length > 0 ? this.ruleData.method : undefined newRule.method = this.ruleData.method?.length > 0 ? this.ruleData.method : undefined
newRule.no_drop = this.ruleData.no_drop? true : undefined newRule.no_drop = this.ruleData.no_drop? true : undefined
break break
case 'sniff': case 'sniff':
newRule.sniffer = this.ruleData.sniffer.length > 0 ? this.ruleData.sniffer : undefined newRule.sniffer = this.ruleData.sniffer?.length > 0 ? this.ruleData.sniffer : undefined
newRule.timeout = this.ruleData.timeout.length > 0 ? this.ruleData.timeout : undefined newRule.timeout = this.ruleData.timeout?.length > 0 ? this.ruleData.timeout : undefined
break break
case 'resolve': case 'resolve':
newRule.strategy = this.ruleData.strategy.length > 0 ? this.ruleData.strategy : undefined newRule.strategy = this.ruleData.strategy?.length > 0 ? this.ruleData.strategy : undefined
newRule.server = this.ruleData.server.length > 0 ? this.ruleData.server : undefined newRule.server = this.ruleData.server?.length > 0 ? this.ruleData.server : undefined
break break
} }
+8
View File
@@ -19,8 +19,16 @@
<v-radio v-for="p in periods" :label="p.title" :value="p.value"></v-radio> <v-radio v-for="p in periods" :label="p.title" :value="p.value"></v-radio>
</v-radio-group> </v-radio-group>
<v-container id="container" style="height:40vh;"> <v-container id="container" style="height:40vh;">
<v-skeleton-loader
class="mx-auto border"
width="95%"
type="image"
v-if="loading"
></v-skeleton-loader>
<template v-else>
<v-alert :text="$t('noData')" type="warning" variant="outlined" v-if="alert"></v-alert> <v-alert :text="$t('noData')" type="warning" variant="outlined" v-if="alert"></v-alert>
<Line v-if="loaded" :data="usage" :options="<any>options" /> <Line v-if="loaded" :data="usage" :options="<any>options" />
</template>
</v-container> </v-container>
</v-card-text> </v-card-text>
</v-card> </v-card>
+1 -6
View File
@@ -334,11 +334,6 @@ export default {
], ],
fingerprints: [ fingerprints: [
{ title: "Chrome", value: "chrome" }, { title: "Chrome", value: "chrome" },
{ title: "Chrome PSK", value: "chrome_psk" },
{ title: "Chrome PSK Shuffle", value: "chrome_psk_shuffle" },
{ title: "Chrome Padding PSK Shuffle", value: "chrome_padding_psk_shuffle" },
{ title: "Chrome Post-Quantum", value: "chrome_pq" },
{ title: "Chrome Post-Quantum PSK", value: "chrome_pq_psk" },
{ title: "Firefox", value: "firefox" }, { title: "Firefox", value: "firefox" },
{ title: "Microsoft Edge", value: "edge" }, { title: "Microsoft Edge", value: "edge" },
{ title: "Apple Safari", value: "safari" }, { title: "Apple Safari", value: "safari" },
@@ -356,7 +351,7 @@ export default {
if (this.$props.id > 0) { if (this.$props.id > 0) {
const newData = <tls>JSON.parse(this.$props.data) const newData = <tls>JSON.parse(this.$props.data)
this.tls = newData this.tls = newData
if (this.tls.server == null) this.tls.server = {} if (this.tls.server == null) this.tls.server = { enabled: true }
if (this.tls.client == null) this.tls.client = {} if (this.tls.client == null) this.tls.client = {}
this.tlsType = newData.server?.reality == undefined ? 0 : 1 this.tlsType = newData.server?.reality == undefined ? 0 : 1
this.usePath = newData.server?.key == undefined ? 0 : 1 this.usePath = newData.server?.key == undefined ? 0 : 1
+7
View File
@@ -72,6 +72,13 @@ export default {
threads: "Threads", threads: "Threads",
memory: "Memory", memory: "Memory",
running: "Running" running: "Running"
},
backup: {
title: "Backup & Restore",
backup: "Download Backup",
restore: "Restore",
exclStats: "Exclude graphs",
exclChanges: "Exclude changes",
} }
}, },
objects: { objects: {
+8 -1
View File
@@ -72,7 +72,14 @@ export default {
threads: "نخ‌ها", threads: "نخ‌ها",
memory: "حافظه", memory: "حافظه",
running: "اجرا" running: "اجرا"
} },
backup: {
title: "پشتیبان‌گیری و بازیابی",
backup: "دریافت پشتیبان",
restore: "بازیابی",
exclStats: "بدون گراف‌ها",
exclChanges: "بدون تغییرات",
},
}, },
objects: { objects: {
inbound: "ورودی‌", inbound: "ورودی‌",
+7
View File
@@ -72,6 +72,13 @@ export default {
threads: "Потоки", threads: "Потоки",
memory: "Память", memory: "Память",
running: "Работает" running: "Работает"
},
backup: {
title: "Резервное копирование и восстановление",
backup: "Скачать резервную копию",
restore: "Восстановить",
exclStats: "Исключить графики",
exclChanges: "Исключить изменения",
} }
}, },
objects: { objects: {
+9
View File
@@ -1,3 +1,5 @@
import { title } from "process";
export default { export default {
message: "Chào mừng OHB", message: "Chào mừng OHB",
success: "Thành công", success: "Thành công",
@@ -70,6 +72,13 @@ export default {
threads: "Luồng", threads: "Luồng",
memory: "Bộ nhớ", memory: "Bộ nhớ",
running: "Đang chạy" running: "Đang chạy"
},
backup: {
title: "Sao lưu và khôi phục",
backup: "Tải xuống bản sao lưu",
restore: "Khôi phục",
exclStats: "Loại trừ các biểu đồ",
exclChanges: "Loại trừ các thay đổi",
} }
}, },
objects: { objects: {
+8 -1
View File
@@ -70,7 +70,14 @@ export default {
threads: "线程", threads: "线程",
memory: "内存", memory: "内存",
running: "运行状态" running: "运行状态"
} },
backup: {
title: "备份与恢复",
backup: "下载备份",
restore: "恢复",
exclStats: "排除图表数据",
exclChanges: "排除变更数据",
},
}, },
objects: { objects: {
inbound: "入站", inbound: "入站",
+8 -1
View File
@@ -71,7 +71,14 @@ export default {
threads: "線程", threads: "線程",
memory: "內存", memory: "內存",
running: "運行狀態" running: "運行狀態"
} },
backup: {
title: "備份與恢復",
backup: "下載備份",
restore: "恢復",
exclStats: "排除圖表記錄",
exclChanges: "排除更改記錄",
},
}, },
objects: { objects: {
inbound: "入站", inbound: "入站",
-49
View File
@@ -5,55 +5,6 @@ type OBJ = {
} }
export const FindDiff = { export const FindDiff = {
Config(obj1: OBJ, obj2: OBJ): any[] {
const differences: any[] = []
if(!obj2){
return [ { key: "all", obj: obj1 } ]
}
for (const key in obj1) {
if (obj2.hasOwnProperty(key)) {
const value1 = obj1[key]
const value2 = obj2[key]
if (Array.isArray(value1)){
value1.forEach((v1,index) => {
if(index >= value2.length){
differences.push({key: key, action: "new", index: index, obj: v1})
} else if(!this.deepCompare(v1,value2[index])) {
differences.push({key: key, action: "edit", index: index, obj: v1})
}
})
} else {
if (!this.deepCompare(value1,value2)) {
differences.push({ key: key, action: "set", obj: value1})
}
}
} else {
differences.push({ key: key, action: "set", obj: obj1[key]})
}
}
return differences
},
ArrObj(value1: any[], value2: any[], key: string): any {
const differences: any[] = []
value1.forEach((v1,index) => {
if(index >= value2.length) differences.push({key: key, action: "new", obj: v1})
else if(!this.deepCompare(v1,value2[index])) differences.push({key: key, action: "edit", obj: v1})
})
return differences
},
Settings(obj1: OBJ, obj2: OBJ): any {
const differences: any[] = []
for (const key in obj1) {
if (obj1[key] != obj2[key]) {
differences.push({ key: key, action: "set", obj: obj1[key]})
}
}
return differences
},
deepCompare(obj1: any, obj2: any): boolean { deepCompare(obj1: any, obj2: any): boolean {
// Check if the types of both objects are the same // Check if the types of both objects are the same
if (typeof obj1 !== typeof obj2) { if (typeof obj1 !== typeof obj2) {
+16 -13
View File
@@ -1,11 +1,9 @@
import { FindDiff } from '@/plugins/utils'
import HttpUtils from '@/plugins/httputil' import HttpUtils from '@/plugins/httputil'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { push } from 'notivue' import { push } from 'notivue'
import { i18n } from '@/locales' import { i18n } from '@/locales'
import { Inbound } from '@/types/inbounds' import { Inbound } from '@/types/inbounds'
import { Outbound } from '@/types/outbounds' import { Client } from '@/types/clients'
import { Endpoint } from '@/types/endpoints'
const Data = defineStore('Data', { const Data = defineStore('Data', {
state: () => ({ state: () => ({
@@ -14,10 +12,10 @@ const Data = defineStore('Data', {
subURI: "", subURI: "",
onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]}, onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]},
config: <any>{}, config: <any>{},
inbounds: <Inbound[]>[], inbounds: <any[]>[],
outbounds: <Outbound[]>[], outbounds: <any[]>[],
endpoints: <Endpoint[]>[], endpoints: <any[]>[],
clients: [], clients: <any>[],
tlsConfigs: <any[]>[], tlsConfigs: <any[]>[],
}), }),
actions: { actions: {
@@ -56,15 +54,20 @@ const Data = defineStore('Data', {
} }
return <Inbound[]>[] return <Inbound[]>[]
}, },
async save (object: string, action: string, data: any, userLinks: any[] | null = null, outJsons: any[] | null = null): Promise<boolean> { async loadClients(id: number): Promise<Client> {
const options = id > 0 ? {id: id} : {}
const msg = await HttpUtils.get('api/clients', options)
if(msg.success) {
return <Client>msg.obj.clients[0]??{}
}
return <Client>{}
},
async save (object: string, action: string, data: any, initUsers?: number[]): Promise<boolean> {
let postData = { let postData = {
object: object, object: object,
action: action, action: action,
data: JSON.stringify(data, null, 2), data: JSON.stringify(data, null, 2),
userLinks: userLinks == null ? undefined : JSON.stringify(userLinks), initUsers: initUsers?.join(',') ?? undefined
}
if (userLinks == null) {
delete postData.userLinks
} }
const msg = await HttpUtils.post('api/save', postData) const msg = await HttpUtils.post('api/save', postData)
if (msg.success) { if (msg.success) {
@@ -78,7 +81,7 @@ const Data = defineStore('Data', {
} }
return msg.success return msg.success
} }
}, }
}) })
export default Data export default Data
+2 -2
View File
@@ -10,9 +10,9 @@ export interface Client {
id?: number id?: number
enable: boolean enable: boolean
name: string name: string
config: Config config?: Config
inbounds: number[] inbounds: number[]
links: Link[] links?: Link[]
volume: number volume: number
expiry: number expiry: number
up: number up: number
+6
View File
@@ -2,6 +2,7 @@ import { Dial } from "./outbounds"
export const EpTypes = { export const EpTypes = {
Wireguard: 'wireguard', Wireguard: 'wireguard',
Warp: 'warp',
} }
type EpType = typeof EpTypes[keyof typeof EpTypes] type EpType = typeof EpTypes[keyof typeof EpTypes]
@@ -34,6 +35,10 @@ export interface WireGuard extends EndpointBasics, Dial {
workers?: number workers?: number
} }
export interface Warp extends WireGuard {
ext: any
}
// Create interfaces dynamically based on EpTypes keys // Create interfaces dynamically based on EpTypes keys
type InterfaceMap = { type InterfaceMap = {
[Key in keyof typeof EpTypes]: { [Key in keyof typeof EpTypes]: {
@@ -48,6 +53,7 @@ export type Endpoint = InterfaceMap[keyof InterfaceMap]
// Create defaultValues object dynamically // Create defaultValues object dynamically
const defaultValues: Record<EpType, Endpoint> = { const defaultValues: Record<EpType, Endpoint> = {
wireguard: { type: EpTypes.Wireguard, address: ['10.0.0.2/32','fe80::2/128'], private_key: '', listen_port: 0, peers: [{ address: '', port: 0, public_key: ''}] }, wireguard: { type: EpTypes.Wireguard, address: ['10.0.0.2/32','fe80::2/128'], private_key: '', listen_port: 0, peers: [{ address: '', port: 0, public_key: ''}] },
warp: { type: EpTypes.Warp, address: [], private_key: '', listen_port: 0, mtu: 1420, peers: [{ address: '', port: 0, public_key: ''}] },
} }
export function createEndpoint<T extends Endpoint>(type: string,json?: Partial<T>): Endpoint { export function createEndpoint<T extends Endpoint>(type: string,json?: Partial<T>): Endpoint {
+13 -54
View File
@@ -52,32 +52,6 @@ interface InboundBasics extends Listen {
out_json?: any out_json?: any
} }
interface UsernamePass {
username: string
password: string
}
interface NamePass {
name: string
password: string
}
interface NameUUID {
name: string
uuid: string
}
interface NameAuth {
name: string
auth_str: string
}
interface VmessUser extends NameUUID {
alterId: number
}
interface VlessUser extends NameUUID {
flow: string
}
interface TuicUser extends NameUUID {
password?: string
}
interface ShadowTLSHandShake extends Dial { interface ShadowTLSHandShake extends Dial {
server: string server: string
server_port: number server_port: number
@@ -88,30 +62,21 @@ export interface Direct extends InboundBasics {
override_address?: string override_address?: string
override_port?: number override_port?: number
} }
export interface Mixed extends InboundBasics { export interface Mixed extends InboundBasics {}
users?: UsernamePass[] export interface SOCKS extends InboundBasics {}
} export interface HTTP extends InboundBasics {}
export interface SOCKS extends InboundBasics {
users?: UsernamePass[]
}
export interface HTTP extends InboundBasics {
users?: UsernamePass[]
}
export interface Shadowsocks extends InboundBasics { export interface Shadowsocks extends InboundBasics {
method: string method: string
password: string password: string
network?: "udp" | "tcp" network?: "udp" | "tcp"
users?: NamePass[]
multiplex?: iMultiplex multiplex?: iMultiplex
} }
export interface VMess extends InboundBasics { export interface VMess extends InboundBasics {
users: VmessUser[]
tls: iTls tls: iTls
multiplex?: iMultiplex multiplex?: iMultiplex
transport?: Transport transport?: Transport
} }
export interface Trojan extends InboundBasics { export interface Trojan extends InboundBasics {
users: NamePass[]
tls: iTls tls: iTls
fallback?: { fallback?: {
server: string server: string
@@ -121,14 +86,12 @@ export interface Trojan extends InboundBasics {
transport?: Transport transport?: Transport
} }
export interface Naive extends InboundBasics { export interface Naive extends InboundBasics {
users: UsernamePass[]
tls: iTls, tls: iTls,
} }
export interface Hysteria extends InboundBasics { export interface Hysteria extends InboundBasics {
up_mbps: number up_mbps: number
down_mbps: number down_mbps: number
obfs?: string obfs?: string
users: NameAuth[]
recv_window_conn?: number recv_window_conn?: number
recv_window_client?: number recv_window_client?: number
max_conn_client?: number max_conn_client?: number
@@ -137,7 +100,6 @@ export interface Hysteria extends InboundBasics {
export interface ShadowTLS extends InboundBasics { export interface ShadowTLS extends InboundBasics {
version: 1|2|3 version: 1|2|3
password?: string password?: string
users?: NamePass[]
handshake: ShadowTLSHandShake handshake: ShadowTLSHandShake
handshake_for_server_name?: { handshake_for_server_name?: {
[server_name: string]: ShadowTLSHandShake [server_name: string]: ShadowTLSHandShake
@@ -145,12 +107,10 @@ export interface ShadowTLS extends InboundBasics {
strict_mode?: boolean strict_mode?: boolean
} }
export interface VLESS extends InboundBasics { export interface VLESS extends InboundBasics {
users: VlessUser[]
multiplex?: iMultiplex multiplex?: iMultiplex
transport?: Transport transport?: Transport
} }
export interface TUIC extends InboundBasics { export interface TUIC extends InboundBasics {
users: TuicUser[]
congestion_control: ""|"cubic"|"new_reno"|"bbr" congestion_control: ""|"cubic"|"new_reno"|"bbr"
auth_timeout?: string auth_timeout?: string
zero_rtt_handshake?: boolean zero_rtt_handshake?: boolean
@@ -163,13 +123,12 @@ export interface Hysteria2 extends InboundBasics {
type?: "salamander" type?: "salamander"
password: string password: string
} }
users: NamePass[]
ignore_client_bandwidth?: boolean ignore_client_bandwidth?: boolean
masquerade?: string | { masquerade?: string | {
type: string type: string
directory?: string directory?: string
url?: string url?: string
rewrite_host?: string rewrite_host?: boolean
status_code?: number status_code?: number
headers?: Headers[] headers?: Headers[]
content?: string content?: string
@@ -245,7 +204,7 @@ type userEnabledTypes = {
vless: VLESS vless: VLESS
} }
export const inboundWithUsers = ['mixed', 'socks:', 'http', 'shadowsocks', 'vmess', 'trojan', 'naive', 'hysteria', 'shadowtls', 'tuic', 'hysteria2', 'vless'] export const inboundWithUsers = ['mixed', 'socks', 'http', 'shadowsocks', 'vmess', 'trojan', 'naive', 'hysteria', 'shadowtls', 'tuic', 'hysteria2', 'vless']
// Create union type from userEnabledTypes // Create union type from userEnabledTypes
type InboundWithUser = userEnabledTypes[keyof userEnabledTypes] type InboundWithUser = userEnabledTypes[keyof userEnabledTypes]
@@ -257,14 +216,14 @@ const defaultValues: Record<InType, Inbound> = {
socks: <SOCKS>{ type: InTypes.SOCKS }, socks: <SOCKS>{ type: InTypes.SOCKS },
http: <HTTP>{ type: InTypes.HTTP, tls_id: 0 }, http: <HTTP>{ type: InTypes.HTTP, tls_id: 0 },
shadowsocks: <Shadowsocks>{ type: InTypes.Shadowsocks, method: 'none', multiplex: {} }, shadowsocks: <Shadowsocks>{ type: InTypes.Shadowsocks, method: 'none', multiplex: {} },
vmess: <VMess>{ type: InTypes.VMess, users: <VmessUser[]>[], tls_id: 0, multiplex: {}, transport: {} }, vmess: <VMess>{ type: InTypes.VMess, tls_id: 0, multiplex: {}, transport: {} },
trojan: <Trojan>{ type: InTypes.Trojan, users: <NamePass[]>[], tls_id: 0, multiplex: {}, transport: {} }, trojan: <Trojan>{ type: InTypes.Trojan, tls_id: 0, multiplex: {}, transport: {} },
naive: <Naive>{ type: InTypes.Naive, users: <UsernamePass[]>[], tls_id: 0 }, naive: <Naive>{ type: InTypes.Naive, tls_id: 0 },
hysteria: <Hysteria>{ type: InTypes.Hysteria, users: <NameAuth[]>[], up_mbps: 100, down_mbps: 100, tls_id: 0 }, hysteria: <Hysteria>{ type: InTypes.Hysteria, up_mbps: 100, down_mbps: 100, tls_id: 0 },
shadowtls: <ShadowTLS>{ type: InTypes.ShadowTLS, version: 3, users: <NamePass[]>[], handshake: {}, handshake_for_server_name: {} }, shadowtls: <ShadowTLS>{ type: InTypes.ShadowTLS, version: 3, handshake: {}, handshake_for_server_name: {} },
tuic: <TUIC>{ type: InTypes.TUIC, users: <TuicUser[]>[], congestion_control: "cubic", tls_id: 0 }, tuic: <TUIC>{ type: InTypes.TUIC, congestion_control: "cubic", tls_id: 0 },
hysteria2: <Hysteria2>{ type: InTypes.Hysteria2, users: <NamePass[]>[], tls_id: 0 }, hysteria2: <Hysteria2>{ type: InTypes.Hysteria2, tls_id: 0 },
vless: <VLESS>{ type: InTypes.VLESS, users: <VlessUser[]>[], tls_id: 0, multiplex: {}, transport: {} }, vless: <VLESS>{ type: InTypes.VLESS, tls_id: 0, multiplex: {}, transport: {} },
tun: <Tun>{ type: InTypes.Tun, mtu: 9000, stack: 'system', udp_timeout: '5m', auto_route: false }, tun: <Tun>{ type: InTypes.Tun, mtu: 9000, stack: 'system', udp_timeout: '5m', auto_route: false },
redirect: <Redirect>{ type: InTypes.Redirect }, redirect: <Redirect>{ type: InTypes.Redirect },
tproxy: <TProxy>{ type: InTypes.TProxy }, tproxy: <TProxy>{ type: InTypes.TProxy },
-5
View File
@@ -234,7 +234,6 @@ import Data from '@/store/modules/data'
import Dial from '@/components/Dial.vue' import Dial from '@/components/Dial.vue'
import { computed, ref, onMounted } from 'vue' import { computed, ref, onMounted } from 'vue'
import { Config, Ntp } from '@/types/config' import { Config, Ntp } from '@/types/config'
import { Client } from '@/types/clients'
import { FindDiff } from '@/plugins/utils' import { FindDiff } from '@/plugins/utils'
const oldConfig = ref({}) const oldConfig = ref({})
@@ -265,10 +264,6 @@ const outboundTags = computed((): string[] => {
return [...Data().outbounds?.map((o:any) => o.tag), ...Data().endpoints?.map((e:any) => e.tag)] return [...Data().outbounds?.map((o:any) => o.tag), ...Data().endpoints?.map((e:any) => e.tag)]
}) })
const clientNames = computed((): string[] => {
return Data().clients.map((c:any) => c.name)
})
const levels = ["trace", "debug", "info", "warn", "error", "fatal", "panic"] const levels = ["trace", "debug", "info", "warn", "error", "fatal", "panic"]
const dnsServersTags = computed((): string[] => { const dnsServersTags = computed((): string[] => {
+15 -159
View File
@@ -4,7 +4,6 @@
v-model="modal.visible" v-model="modal.visible"
:visible="modal.visible" :visible="modal.visible"
:id="modal.id" :id="modal.id"
:data="modal.data"
:groups="groups" :groups="groups"
:inboundTags="inboundTags" :inboundTags="inboundTags"
@close="closeModal" @close="closeModal"
@@ -21,7 +20,7 @@
<QrCode <QrCode
v-model="qrcode.visible" v-model="qrcode.visible"
:visible="qrcode.visible" :visible="qrcode.visible"
:index="qrcode.index" :id="qrcode.id"
@close="closeQrCode" @close="closeQrCode"
/> />
<Stats <Stats
@@ -116,129 +115,8 @@
</v-card> </v-card>
</v-menu> </v-menu>
</v-col> </v-col>
<v-col cols="auto">
<v-btn hide-details variant="text" icon @click="toggleClientView">
<v-icon :icon="tableView ? 'mdi-table-eye' : 'mdi-table-eye-off'" :color="tableView ? 'primary' : ''"></v-icon>
</v-btn>
</v-col>
</v-row>
<template v-for="group in groups" v-if="!tableView">
<v-row>
<v-col class="v-card-subtitle">
{{ group.length>0 ? group : $t('none') }}
<v-badge :content="(filterSettings.enabled ? filterSettings.filteredClients : clients).filter(c => c.group == group).length" inline color="info" />
<v-icon
:icon="openedGroups.includes(group) ? 'mdi-arrow-collapse-up' : 'mdi-arrow-collapse-down'"
size="small"
variant="text"
@click="toggleGroupOpen(group)"
></v-icon>
</v-col>
</v-row>
<v-row v-if="openedGroups.includes(group)">
<template v-for="item in (filterSettings.enabled ? filterSettings.filteredClients : clients).filter(c => c.group == group)" :key="item.id">
<v-col cols="12" sm="4" md="3" lg="2">
<v-card rounded="xl" elevation="5" min-width="200">
<v-card-title>
<v-row>
<v-col>{{ item.name }}</v-col>
<v-spacer></v-spacer>
<v-col cols="auto">
<v-switch color="primary"
v-model="item.enable"
hideDetails density="compact" />
</v-col>
</v-row>
</v-card-title>
<v-card-subtitle style="margin-top: -20px;">
<v-row>
<v-col>{{ item.desc }}</v-col>
</v-row>
</v-card-subtitle>
<v-card-text>
<v-row>
<v-col>{{ $t('pages.inbounds') }}</v-col>
<v-col>
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="item.inbounds != ''">
<span v-for="i in item.inbounds">{{ inbounds.find(inb => inb.id == i)?.tag }}<br /></span>
</v-tooltip>
{{ item.inbounds.length }}
</v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col>{{ $t('stats.volume') }}</v-col>
<v-col>
{{ item.volume == 0 ? $t('unlimited') : HumanReadable.sizeFormat(item.volume) }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('date.expiry') }}</v-col>
<v-col>
{{ HumanReadable.remainedDays(item.expiry) }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('stats.usage') }}</v-col>
<v-col>
<v-tooltip activator="parent" location="bottom">
{{ $t('stats.upload') }}:{{ HumanReadable.sizeFormat(item.up) }}<br />
{{ $t('stats.download') }}:{{ HumanReadable.sizeFormat(item.down) }}<br />
<template v-if="item.volume>0">
{{ $t('remained') }}: {{ HumanReadable.sizeFormat(item.volume - (item.up + item.down)) }}
</template>
</v-tooltip>
{{ HumanReadable.sizeFormat(item.up + item.down) }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('online') }}</v-col>
<v-col>
<template v-if="isOnline(item.name).value">
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
</template>
<template v-else>-</template>
</v-col>
</v-row>
</v-card-text>
<v-divider></v-divider>
<v-card-actions style="padding: 0;">
<v-btn icon="mdi-account-edit" @click="showModal(item.id)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
</v-btn>
<v-btn style="margin-inline-start:0;" icon="mdi-account-minus" color="warning" @click="delOverlay[clients.findIndex(c => c.id == item.id)] = true">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.del')"></v-tooltip>
</v-btn>
<v-overlay
v-model="delOverlay[clients.findIndex(c => c.id == item.id)]"
contained
class="align-center justify-center"
>
<v-card :title="$t('actions.del')" rounded="lg">
<v-divider></v-divider>
<v-card-text>{{ $t('confirm') }}</v-card-text>
<v-card-actions>
<v-btn color="error" variant="outlined" @click="delClient(item.id)">{{ $t('yes') }}</v-btn>
<v-btn color="success" variant="outlined" @click="delOverlay[clients.findIndex(c => c.id == item.id)] = false">{{ $t('no') }}</v-btn>
</v-card-actions>
</v-card>
</v-overlay>
<v-btn icon="mdi-qrcode" @click="showQrCode(item.id)">
<v-icon />
<v-tooltip activator="parent" location="top" text="QR-Code"></v-tooltip>
</v-btn>
<v-btn icon="mdi-chart-line" @click="showStats(item.name)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</template>
</v-row>
</template>
<v-row v-else>
<v-col cols="12"> <v-col cols="12">
<v-data-table <v-data-table
:headers="headers" :headers="headers"
@@ -246,21 +124,19 @@
:hide-default-footer="filterSettings.enabled ? filterSettings.filteredClients.length<=10 : clients.length<=10" :hide-default-footer="filterSettings.enabled ? filterSettings.filteredClients.length<=10 : clients.length<=10"
hide-no-data hide-no-data
fixed-header fixed-header
:group-by="groupBy"
item-value="name" item-value="name"
:mobile="smAndDown" :mobile="smAndDown"
mobile-breakpoint="sm" mobile-breakpoint="sm"
width="100%" width="100%"
class="elevation-3 rounded" class="elevation-3 rounded"
> >
<template v-slot:group-header="{ item, columns, toggleGroup, isGroupOpen }"> <template v-slot:item.inbounds="{ item }">
<tr> <span>
<td :colspan="columns.length" @click="toggleGroup(item)" style="min-height: fit-content; text-align: center;"> <v-tooltip activator="parent" dir="ltr" location="start" v-if="item.inbounds != ''">
<v-icon :icon="isGroupOpen(item) ? '$expand' : '$next'"></v-icon> <span v-for="i in item.inbounds">{{ inbounds.find(inb => inb.id == i)?.tag }}<br /></span>
{{ item.value.length>0 ? item.value : $t('none') }} </v-tooltip>
<v-badge :content="(filterSettings.enabled ? filterSettings.filteredClients : clients).filter(c => c.group == item.value).length" inline color="success" /> {{ item.inbounds.length }}
</td> </span>
</tr>
</template> </template>
<template v-slot:item.volume="{ item }"> <template v-slot:item.volume="{ item }">
<div class="text-start"> <div class="text-start">
@@ -363,13 +239,13 @@ const isOnline = (cname: string) => computed(() => {
return Data().onlines?.user ? Data().onlines.user.includes(cname) : false return Data().onlines?.user ? Data().onlines.user.includes(cname) : false
}) })
const inbounds = computed((): Inbound[] => { const inbounds = computed((): any[] => {
return <Inbound[]> Data().inbounds?? [] return Data().inbounds?? []
}) })
const inboundTags = computed((): any[] => { const inboundTags = computed((): any[] => {
if (!inbounds.value) return [] if (!inbounds.value) return []
return inbounds.value?.filter(i => i.tag != "" && inboundWithUsers.includes(i.type)).map(i => { return { title: i.tag, value: i.id } }) return inbounds.value?.filter(i => i.tag != "" && i.users).map(i => { return { title: i.tag, value: i.id } })
}) })
const groups = computed((): string[] => { const groups = computed((): string[] => {
@@ -387,12 +263,6 @@ const filterSettings = ref({
text: '', text: '',
filteredClients: <any[]>[] filteredClients: <any[]>[]
}) })
const tableView = ref(localStorage.getItem('clientView') == 'table')
const toggleClientView = () => {
localStorage.setItem('clientView',tableView.value ? 'tile' : 'table')
tableView.value = !tableView.value
}
const filterItems = [ const filterItems = [
{ title: i18n.global.t('none'), value: '' }, { title: i18n.global.t('none'), value: '' },
@@ -403,30 +273,24 @@ const filterItems = [
const headers = [ const headers = [
{ title: i18n.global.t('client.name'), key: 'name' }, { title: i18n.global.t('client.name'), key: 'name' },
{ title: i18n.global.t('client.desc'), key: 'desc', sortable: false }, { title: i18n.global.t('client.group'), key: 'group' },
{ title: i18n.global.t('pages.inbounds'), key: 'inbounds', width: 10 },
{ title: i18n.global.t('actions.action'), key: 'actions', sortable: false}, { title: i18n.global.t('actions.action'), key: 'actions', sortable: false},
{ title: i18n.global.t('stats.volume'), key: 'volume' }, { title: i18n.global.t('stats.volume'), key: 'volume' },
{ title: i18n.global.t('date.expiry'), key: 'expiry' }, { title: i18n.global.t('date.expiry'), key: 'expiry' },
{ title: i18n.global.t('online'), key: 'online' }, { title: i18n.global.t('online'), key: 'online' },
{ key: 'data-table-group', width: 0 }, { key: 'data-table-group', width: 0 },
] ]
const groupBy = [
{
key: 'group'
}
]
const modal = ref({ const modal = ref({
visible: false, visible: false,
id: 0, id: 0,
data: "",
}) })
const delOverlay = ref(new Array<boolean>(clients.value.length).fill(false)) const delOverlay = ref(new Array<boolean>(clients.value.length).fill(false))
const showModal = async (id: number) => { const showModal = async (id: number) => {
modal.value.id = id modal.value.id = id
modal.value.data = id == 0 ? '' : JSON.stringify(clients.value.findLast(o => o.id == id))
modal.value.visible = true modal.value.visible = true
} }
const closeModal = () => { const closeModal = () => {
@@ -455,12 +319,11 @@ const delClient = async (id: number) => {
const qrcode = ref({ const qrcode = ref({
visible: false, visible: false,
index: 0, id: 0,
}) })
const showQrCode = (id: number) => { const showQrCode = (id: number) => {
const clientIndex = clients.value.findIndex(c => c.id === id) qrcode.value.id = id
qrcode.value.index = clientIndex
qrcode.value.visible = true qrcode.value.visible = true
} }
const closeQrCode = () => { const closeQrCode = () => {
@@ -481,13 +344,6 @@ const closeStats = () => {
stats.value.visible = false stats.value.visible = false
} }
var openedGroups = ref(<string[]>[""])
const toggleGroupOpen = (g: string) => {
const index = openedGroups.value.findIndex(og => og == g)
index == -1 ? openedGroups.value.push(g) : openedGroups.value.splice(index,1)
}
const doFilter = () => { const doFilter = () => {
let filteredClients = clients.value.slice() let filteredClients = clients.value.slice()
if (filterSettings.value.group != '-') { if (filterSettings.value.group != '-') {
+1 -1
View File
@@ -38,7 +38,7 @@
<v-row> <v-row>
<v-col>{{ $t('in.port') }}</v-col> <v-col>{{ $t('in.port') }}</v-col>
<v-col> <v-col>
{{ item.listen_port?? '-' }} {{ item.listen_port>0 ? item.listen_port : '-' }}
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
+8 -17
View File
@@ -51,11 +51,11 @@
<v-row> <v-row>
<v-col>{{ $t('pages.clients') }}</v-col> <v-col>{{ $t('pages.clients') }}</v-col>
<v-col> <v-col>
<template v-if="inboundWithUsers.includes(item.type)"> <template v-if="item.users">
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="findInboundUsers(item).length > 0"> <v-tooltip activator="parent" dir="ltr" location="bottom" v-if="item.users.length > 0">
<span v-for="u in findInboundUsers(item)">{{ u }}<br /></span> <span v-for="u in item.users">{{ u }}<br /></span>
</v-tooltip> </v-tooltip>
{{ findInboundUsers(item).length }} {{ item.users.length }}
</template> </template>
<template v-else>-</template> <template v-else>-</template>
</v-col> </v-col>
@@ -110,8 +110,7 @@ import InboundVue from '@/layouts/modals/Inbound.vue'
import Stats from '@/layouts/modals/Stats.vue' import Stats from '@/layouts/modals/Stats.vue'
import { Config } from '@/types/config' import { Config } from '@/types/config'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { Inbound, inboundWithUsers } from '@/types/inbounds' import { Inbound } from '@/types/inbounds'
import { Client } from '@/types/clients'
import { i18n } from '@/locales' import { i18n } from '@/locales'
import { push } from 'notivue' import { push } from 'notivue'
@@ -135,12 +134,8 @@ const outTags = computed((): string[] => {
return appConfig.value.outbounds?.map(i => i.tag) return appConfig.value.outbounds?.map(i => i.tag)
}) })
const clients = computed((): Client[] => {
return <Client[]> Data().clients
})
const onlines = computed(() => { const onlines = computed(() => {
return Data().onlines.inbound ? inbounds.value.map(i => Data().onlines.inbound.includes(i.tag)) : [] return Data().onlines.inbound?? []
}) })
const modal = ref({ const modal = ref({
@@ -157,7 +152,7 @@ const showModal = (id: number) => {
const closeModal = () => { const closeModal = () => {
modal.value.visible = false modal.value.visible = false
} }
const saveModal = async (data:Inbound) => { const saveModal = async (data:Inbound, initUsers?: number[]) => {
// Check duplicate tag // Check duplicate tag
const oldInbound = modal.value.id > 0 ? inbounds.value.findLast(i => i.id == modal.value.id) : null const oldInbound = modal.value.id > 0 ? inbounds.value.findLast(i => i.id == modal.value.id) : null
if (data.tag != oldInbound?.tag && inTags.value.includes(data.tag)) { if (data.tag != oldInbound?.tag && inTags.value.includes(data.tag)) {
@@ -168,7 +163,7 @@ const saveModal = async (data:Inbound) => {
} }
// save data // save data
const success = await Data().save("inbounds", modal.value.id == 0 ? "new" : "edit", data) const success = await Data().save("inbounds", modal.value.id == 0 ? "new" : "edit", data, initUsers)
if (success) modal.value.visible = false if (success) modal.value.visible = false
} }
@@ -180,10 +175,6 @@ const delInbound = async (id: number) => {
if (success) delOverlay.value[index] = false if (success) delOverlay.value[index] = false
} }
const findInboundUsers = (i: Inbound): string[] => {
return clients.value.filter(c => c.inbounds.includes(i.id)).map(c => c.name)
}
const stats = ref({ const stats = ref({
visible: false, visible: false,
resource: "inbound", resource: "inbound",
+1 -1
View File
@@ -223,7 +223,7 @@ const outboundTags = computed((): string[] => {
}) })
const inboundTags = computed((): string[] => { const inboundTags = computed((): string[] => {
return [...Data().inbounds?.map((o:any) => o.tag), ...Data().endpoints?.map((e:any) => e.tag)] return [...Data().inbounds?.map((o:any) => o.tag), ...Data().endpoints?.filter((e:any) => e.listen_port > 0).map((e:any) => e.tag)]
}) })
let delRuleOverlay = ref(new Array<boolean>) let delRuleOverlay = ref(new Array<boolean>)
+18 -11
View File
@@ -14,7 +14,7 @@
<v-card-text> <v-card-text>
<v-row align="center" justify="center" style="margin-bottom: 10px;"> <v-row align="center" justify="center" style="margin-bottom: 10px;">
<v-col cols="auto"> <v-col cols="auto">
<v-btn color="primary" @click="saveChanges" :loading="loading" :disabled="!stateChange"> <v-btn color="primary" @click="save" :loading="loading" :disabled="!stateChange">
{{ $t('actions.save') }} {{ $t('actions.save') }}
</v-btn> </v-btn>
</v-col> </v-col>
@@ -152,11 +152,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useLocale } from 'vuetify' import { useLocale } from 'vuetify'
import { languages } from '@/locales' import { i18n, languages } from '@/locales'
import { Ref, computed, inject, onMounted, ref } from 'vue' import { Ref, computed, inject, onMounted, ref } from 'vue'
import HttpUtils from '@/plugins/httputil' import HttpUtils from '@/plugins/httputil'
import { FindDiff } from '@/plugins/utils' import { FindDiff } from '@/plugins/utils'
import SubJsonExtVue from '@/components/SubJsonExt.vue' import SubJsonExtVue from '@/components/SubJsonExt.vue'
import { push } from 'notivue'
const locale = useLocale() const locale = useLocale()
const tab = ref("t1") const tab = ref("t1")
const loading:Ref = inject('loading')?? ref(false) const loading:Ref = inject('loading')?? ref(false)
@@ -195,22 +196,28 @@ const changeLocale = (l: any) => {
const loadData = async () => { const loadData = async () => {
loading.value = true loading.value = true
const msg = await HttpUtils.get('api/setting') const msg = await HttpUtils.get('api/settings')
loading.value = false loading.value = false
if (msg.success) { if (msg.success) {
settings.value = msg.obj setData(msg.obj)
oldSettings.value = { ...msg.obj }
} }
} }
const saveChanges = async () => { const setData = (data: any) => {
settings.value = data
oldSettings.value = { ...data }
}
const save = async () => {
loading.value = true loading.value = true
const diff = { const msg = await HttpUtils.post('api/save', { object: 'settings', action: 'set', data: JSON.stringify(settings.value) })
settings: JSON.stringify(FindDiff.Settings(settings.value,oldSettings.value)),
}
const msg = await HttpUtils.post('api/save', diff)
if (msg.success) { if (msg.success) {
loadData() push.success({
title: i18n.global.t('success'),
duration: 5000,
message: i18n.global.t('actions.set') + " " + i18n.global.t('pages.settings')
})
setData(msg.obj.settings)
} }
loading.value = false loading.value = false
} }
-5
View File
@@ -89,7 +89,6 @@ import TlsVue from '@/layouts/modals/Tls.vue'
import Data from '@/store/modules/data' import Data from '@/store/modules/data'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { Inbound } from '@/types/inbounds' import { Inbound } from '@/types/inbounds'
import { Client } from '@/types/clients'
import { tls } from '@/types/tls' import { tls } from '@/types/tls'
const tlsConfigs = computed((): any[] => { const tlsConfigs = computed((): any[] => {
@@ -104,10 +103,6 @@ const tlsInbounds = (id: number): string[] => {
return inbounds.value.filter(i => i.tls_id == id).map(i => i.tag) return inbounds.value.filter(i => i.tls_id == id).map(i => i.tag)
} }
const clients = computed((): any[] => {
return <Client[]>Data().clients
})
const modal = ref({ const modal = ref({
visible: false, visible: false,
id: 0, id: 0,
+2 -2
View File
@@ -224,7 +224,7 @@ install_s-ui() {
tar zxvf s-ui-linux-$(arch).tar.gz tar zxvf s-ui-linux-$(arch).tar.gz
rm s-ui-linux-$(arch).tar.gz -f rm s-ui-linux-$(arch).tar.gz -f
chmod +x s-ui/sui /s-ui/s-ui.sh chmod +x s-ui/sui s-ui/s-ui.sh
cp s-ui/s-ui.sh /usr/bin/s-ui cp s-ui/s-ui.sh /usr/bin/s-ui
cp -rf s-ui /usr/local/ cp -rf s-ui /usr/local/
cp -f s-ui/*.service /etc/systemd/system/ cp -f s-ui/*.service /etc/systemd/system/
@@ -236,7 +236,7 @@ install_s-ui() {
systemctl enable s-ui --now systemctl enable s-ui --now
echo -e "${green}s-ui v${last_version}${plain} installation finished, it is up and running now..." echo -e "${green}s-ui v${last_version}${plain} installation finished, it is up and running now..."
echo -e "You may access the Panel with following URL(s):${yellow}" echo -e "You may access the Panel with following URL(s):${green}"
/usr/local/s-ui/sui uri /usr/local/s-ui/sui uri
echo -e "${plain}" echo -e "${plain}"
echo -e "" echo -e ""
+3 -2
View File
@@ -251,6 +251,7 @@ set_setting() {
view_setting() { view_setting() {
/usr/local/s-ui/sui setting -show /usr/local/s-ui/sui setting -show
view_uri
before_show_menu before_show_menu
} }
@@ -261,7 +262,7 @@ view_uri() {
before_show_menu before_show_menu
fi fi
LOGI "You may access the Panel with following URL(s):" LOGI "You may access the Panel with following URL(s):"
echo -e "${yellow}${info}${reset}" echo -e "${green}${info}${plain}"
} }
start() { start() {
@@ -850,7 +851,7 @@ show_menu() {
check_install && set_setting check_install && set_setting
;; ;;
10) 10)
check_install && view_setting && view_uri check_install && view_setting
;; ;;
11) 11)
check_install && start s-ui check_install && start s-ui