Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5da87cf14d | |||
| 1edd9d4323 | |||
| ce968d20ac | |||
| 60c64bab18 | |||
| f3432b119c | |||
| 049cfc5287 | |||
| 891a61ac16 | |||
| a0dc165995 | |||
| 4dc02f783a | |||
| 1282d67640 | |||
| f116e7f5ea | |||
| d06b6be4a2 | |||
| b43a6ade97 | |||
| f18345b30d | |||
| 67582015d3 | |||
| 94473b40de | |||
| 7e41af0da8 | |||
| 88adcc7c9a |
+25
-3
@@ -2,12 +2,14 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"s-ui/database"
|
||||
"s-ui/logger"
|
||||
"s-ui/service"
|
||||
"s-ui/util"
|
||||
"s-ui/util/common"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -88,7 +90,8 @@ func (a *APIHandler) postHandler(c *gin.Context) {
|
||||
obj := c.Request.FormValue("object")
|
||||
act := c.Request.FormValue("action")
|
||||
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 {
|
||||
jsonMsg(c, "save", err)
|
||||
return
|
||||
@@ -108,6 +111,15 @@ func (a *APIHandler) postHandler(c *gin.Context) {
|
||||
link := c.Request.FormValue("link")
|
||||
result, _, err := util.GetOutbound(link, 0)
|
||||
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:
|
||||
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
|
||||
}
|
||||
@@ -187,6 +199,16 @@ func (a *APIHandler) getHandler(c *gin.Context) {
|
||||
options := c.Query("o")
|
||||
keypair := a.ServerService.GenKeypair(kType, options)
|
||||
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:
|
||||
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 {
|
||||
data := make(map[string]interface{}, 0)
|
||||
id := c.Query("id")
|
||||
|
||||
for _, obj := range objs {
|
||||
switch obj {
|
||||
case "inbounds":
|
||||
id := c.Query("id")
|
||||
inbounds, err := a.InboundService.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -287,7 +309,7 @@ func (a *APIHandler) loadPartialData(c *gin.Context, objs []string) error {
|
||||
}
|
||||
data[obj] = tlsConfigs
|
||||
case "clients":
|
||||
clients, err := a.ClientService.GetAll()
|
||||
clients, err := a.ClientService.Get(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -60,22 +60,10 @@ func moveJsonToDb(db *gorm.DB) error {
|
||||
} else {
|
||||
tls_server, _ := json.MarshalIndent(tlsObj, "", " ")
|
||||
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{
|
||||
Name: tag,
|
||||
Server: tls_server,
|
||||
Client: tls_client,
|
||||
Client: json.RawMessage("{}"),
|
||||
}
|
||||
err = db.Create(newTls).Error
|
||||
if err != nil {
|
||||
@@ -243,7 +231,34 @@ func migrateTls(db *gorm.DB) error {
|
||||
if !db.Migrator().HasColumn(&model.Tls{}, "inbounds") {
|
||||
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 {
|
||||
@@ -276,6 +291,10 @@ func migrateClients(db *gorm.DB) 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 {
|
||||
err := moveJsonToDb(db)
|
||||
if err != nil {
|
||||
@@ -289,5 +308,9 @@ func to1_2(db *gorm.DB) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return migrateClients(db)
|
||||
err = migrateClients(db)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return migrateChanges(db)
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.2.0-beta.2
|
||||
1.2.0-beta.3
|
||||
@@ -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,6 +9,7 @@ type Endpoint struct {
|
||||
Type string `json:"type" form:"type"`
|
||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||
Options json.RawMessage `json:"-" form:"-"`
|
||||
Ext json.RawMessage `json:"ext" form:"ext"`
|
||||
}
|
||||
|
||||
func (o *Endpoint) UnmarshalJSON(data []byte) error {
|
||||
@@ -27,9 +28,11 @@ func (o *Endpoint) UnmarshalJSON(data []byte) error {
|
||||
delete(raw, "type")
|
||||
o.Tag = raw["tag"].(string)
|
||||
delete(raw, "tag")
|
||||
o.Ext, _ = json.MarshalIndent(raw["ext"], "", " ")
|
||||
delete(raw, "ext")
|
||||
|
||||
// Remaining fields
|
||||
o.Options, err = json.Marshal(raw)
|
||||
o.Options, err = json.MarshalIndent(raw, "", " ")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -37,7 +40,12 @@ func (o *Endpoint) UnmarshalJSON(data []byte) error {
|
||||
func (o Endpoint) MarshalJSON() ([]byte, error) {
|
||||
// Combine fixed fields and dynamic fields into one map
|
||||
combined := make(map[string]interface{})
|
||||
combined["type"] = o.Type
|
||||
switch o.Type {
|
||||
case "warp":
|
||||
combined["type"] = "wireguard"
|
||||
default:
|
||||
combined["type"] = o.Type
|
||||
}
|
||||
combined["tag"] = o.Tag
|
||||
|
||||
if o.Options != nil {
|
||||
|
||||
@@ -26,9 +26,9 @@ type Client struct {
|
||||
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
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"`
|
||||
Links json.RawMessage `json:"links" form:"links"`
|
||||
Links json.RawMessage `json:"links,omitempty" form:"links"`
|
||||
Volume int64 `json:"volume" form:"volume"`
|
||||
Expiry int64 `json:"expiry" form:"expiry"`
|
||||
Down int64 `json:"down" form:"down"`
|
||||
@@ -49,9 +49,8 @@ type Stats struct {
|
||||
type Changes struct {
|
||||
Id uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
DateTime int64 `json:"dateTime"`
|
||||
Actor string `json:"Actor"`
|
||||
Key string `json:"key" form:"key"`
|
||||
Action string `json:"action" form:"action"`
|
||||
Index uint `json:"index" form:"index"`
|
||||
Obj json.RawMessage `json:"obj" form:"obj"`
|
||||
Actor string `json:"actor"`
|
||||
Key string `json:"key"`
|
||||
Action string `json:"action"`
|
||||
Obj json.RawMessage `json:"obj"`
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func (o *Outbound) UnmarshalJSON(data []byte) error {
|
||||
delete(raw, "tag")
|
||||
|
||||
// Remaining fields
|
||||
o.Options, err = json.Marshal(raw)
|
||||
o.Options, err = json.MarshalIndent(raw, "", " ")
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+5
-7
@@ -7,10 +7,9 @@ require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/sagernet/sing v0.6.0-beta.9
|
||||
github.com/sagernet/sing-box v1.11.0-beta.20
|
||||
github.com/sagernet/sing-dns v0.4.0-beta.1
|
||||
github.com/shirou/gopsutil/v3 v3.24.5
|
||||
github.com/sagernet/sing v0.6.0-beta.12
|
||||
github.com/sagernet/sing-box v1.11.0-beta.24
|
||||
github.com/sagernet/sing-dns v0.4.0-beta.2
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
|
||||
gorm.io/driver/sqlite v1.5.7
|
||||
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/reality v0.0.0-20230406110435-ee17307e7691 // indirect
|
||||
github.com/sagernet/sing-mux v0.3.0-alpha.1 // indirect
|
||||
github.com/sagernet/sing-quic v0.4.0-beta.3 // 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-shadowsocks2 v0.2.0 // 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.2 // indirect
|
||||
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
|
||||
github.com/sagernet/utls v1.6.7 // indirect
|
||||
github.com/sagernet/wireguard-go v0.0.1-beta.5 // indirect
|
||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
|
||||
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/numcpus v0.9.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
|
||||
+17
-58
@@ -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-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 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/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
|
||||
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
|
||||
@@ -172,68 +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/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
|
||||
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.5/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.8/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.6.0-beta.9 h1:P8lKa5hN53fRNAVCIKy5cWd6/kLO5c4slhdsfehSmHs=
|
||||
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.6 h1:MPdL2Yem/xM0RhejCO7krYvl1Zbd1zkSjKluKpHnHPQ=
|
||||
github.com/sagernet/sing-box v1.11.0-beta.6/go.mod h1:6dO5V0A37cLlhvKnxCmZinSpZXz7ZSk11x3rgI+xH1I=
|
||||
github.com/sagernet/sing-box v1.11.0-beta.11 h1:bVR0n3oQ3hGcuc/CSS7axsOeRNCRlCGkYVOKl0wxbsw=
|
||||
github.com/sagernet/sing-box v1.11.0-beta.11/go.mod h1:GZnZUzUHZ6Bgm7D/i8unNORv3537u1s03tLXFdxCRpg=
|
||||
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-box v1.11.0-beta.20 h1:snuBUv7p7m1eBJKd9Kycnq4TA6V4t6ea2XxZPCEaDAM=
|
||||
github.com/sagernet/sing-box v1.11.0-beta.20/go.mod h1:w5cd6P5pLrF7bGu3Z3k6jkbJqS7ByjK/Y34INVA7k5Q=
|
||||
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 v0.6.0-beta.11 h1:jWCNlZI1Vdj8lQeBrjRZIQfNwlqMk0ZRqMJuPfTJupI=
|
||||
github.com/sagernet/sing v0.6.0-beta.11/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.6.0-beta.12 h1:2DnTJcvypK3/PM/8JjmgG8wVK48gdcpRwU98c4J/a7s=
|
||||
github.com/sagernet/sing v0.6.0-beta.12/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-box v1.11.0-beta.22 h1:UQrhqbUyJUZ1GvT3yNu4ANdZC8s1YdgN92jtvPd559g=
|
||||
github.com/sagernet/sing-box v1.11.0-beta.22/go.mod h1:CYFqT0KvhmGhs3hp6htI8x6DugWZgdiAde+Fyufxmek=
|
||||
github.com/sagernet/sing-box v1.11.0-beta.24 h1:6rUl8t6Cb0p9ML1eUobWgODL75c5iszxNvVABcWCivU=
|
||||
github.com/sagernet/sing-box v1.11.0-beta.24/go.mod h1:DmL1WKyrfaAEu5z88CtUeQBfELaEdUyQzLS5nzmRg8o=
|
||||
github.com/sagernet/sing-dns v0.4.0-beta.2 h1:HW94bUEp7K/vf5DlYz646LTZevQtJ0250jZa/UZRlbY=
|
||||
github.com/sagernet/sing-dns v0.4.0-beta.2/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/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-alpha.4/go.mod h1:h5RkKTmUhudJKzK7c87FPXD5w1bJjVyxMN9+opZcctA=
|
||||
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-quic v0.4.0-beta.3 h1:cOBjlhVdRZmBm6hIw1GleERpnTSFdBB2htgx5kQ5uqg=
|
||||
github.com/sagernet/sing-quic v0.4.0-beta.3/go.mod h1:1UNObFodd8CnS3aCT53x9cigjPSCl3P//8dfBMCwBDM=
|
||||
github.com/sagernet/sing-quic v0.4.0-beta.4 h1:kKiMLGaxvVLDCSvCMYo4PtWd1xU6FTL7xvUAQfXO09g=
|
||||
github.com/sagernet/sing-quic v0.4.0-beta.4/go.mod h1:1UNObFodd8CnS3aCT53x9cigjPSCl3P//8dfBMCwBDM=
|
||||
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-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
|
||||
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/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.2/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||
github.com/sagernet/sing-tun v0.6.0-beta.6 h1:xaIHoH78MqTSvZqQ4SQto8pC1A+X4qXReDRNaC8DQeI=
|
||||
github.com/sagernet/sing-tun v0.6.0-beta.6/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||
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/sing-tun v0.6.0-beta.8 h1:GFNt/w8r1v30zC/hfCytk8C9+N/f1DfvosFXJkyJlrw=
|
||||
github.com/sagernet/sing-tun v0.6.0-beta.8/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
|
||||
github.com/sagernet/sing-vmess v0.2.0-beta.2 h1:obAkAL35X7ql4RnGzDg4dBYIRpGXRKqcN4LyLZpZGSs=
|
||||
github.com/sagernet/sing-vmess v0.2.0-beta.2/go.mod h1:HGhf9XUdeE2iOWrX0hQNFgXPbKyGlzpeYFyX0c/pykk=
|
||||
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/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/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/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/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/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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -243,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.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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
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/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
|
||||
@@ -274,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/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
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/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
@@ -286,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.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
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/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -298,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.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.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/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
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/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
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.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/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
@@ -333,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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
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/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
|
||||
gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"s-ui/logger"
|
||||
"s-ui/util"
|
||||
"s-ui/util/common"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -16,14 +17,32 @@ type ClientService struct {
|
||||
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()
|
||||
clients := []model.Client{}
|
||||
err := db.Model(model.Client{}).Scan(&clients).Error
|
||||
var client []model.Client
|
||||
err := db.Model(model.Client{}).Where("id in ?", strings.Split(id, ",")).Scan(&client).Error
|
||||
if err != nil {
|
||||
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) {
|
||||
@@ -138,6 +157,56 @@ func (s *ClientService) updateLinksWithFixedInbounds(tx *gorm.DB, clients []*mod
|
||||
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 {
|
||||
var clients []model.Client
|
||||
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 {
|
||||
var inbounds []model.Inbound
|
||||
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
|
||||
}
|
||||
for _, inbound := range inbounds {
|
||||
|
||||
@@ -116,10 +116,11 @@ func (s *ConfigService) StopCore() error {
|
||||
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 inboundIds []uint
|
||||
var inboundId uint
|
||||
var objs []string = []string{obj}
|
||||
|
||||
db := database.GetDB()
|
||||
tx := db.Begin()
|
||||
@@ -134,10 +135,11 @@ func (s *ConfigService) Save(obj string, act string, data json.RawMessage, login
|
||||
switch obj {
|
||||
case "clients":
|
||||
inboundIds, err = s.ClientService.Save(tx, act, data, hostname)
|
||||
objs = append(objs, "inbounds")
|
||||
case "tls":
|
||||
inboundIds, err = s.TlsService.Save(tx, act, data)
|
||||
case "inbounds":
|
||||
inboundId, err = s.InboundService.Save(tx, act, data, hostname)
|
||||
inboundId, err = s.InboundService.Save(tx, act, data, initUsers, hostname)
|
||||
case "outbounds":
|
||||
err = s.OutboundService.Save(tx, act, data)
|
||||
case "endpoints":
|
||||
@@ -171,7 +173,6 @@ func (s *ConfigService) Save(obj string, act string, data json.RawMessage, login
|
||||
// Commit changes so far
|
||||
tx.Commit()
|
||||
LastUpdate = time.Now().Unix()
|
||||
var objs []string = []string{obj}
|
||||
tx = db.Begin()
|
||||
|
||||
// Update side changes
|
||||
@@ -184,8 +185,10 @@ func (s *ConfigService) Save(obj string, act string, data json.RawMessage, login
|
||||
}
|
||||
objs = append(objs, "clients")
|
||||
}
|
||||
if obj == "inbounds" && act != "add" {
|
||||
if obj == "inbounds" {
|
||||
switch act {
|
||||
case "new":
|
||||
err = s.ClientService.UpdateClientsOnInboundAdd(tx, initUsers, inboundId, hostname)
|
||||
case "edit":
|
||||
err = s.ClientService.UpdateLinksByInboundChange(tx, []uint{inboundId}, hostname)
|
||||
case "del":
|
||||
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type EndpointService struct{}
|
||||
type EndpointService struct {
|
||||
WarpService
|
||||
}
|
||||
|
||||
func (o *EndpointService) GetAll() (*[]map[string]interface{}, error) {
|
||||
db := database.GetDB()
|
||||
@@ -25,6 +27,7 @@ func (o *EndpointService) GetAll() (*[]map[string]interface{}, error) {
|
||||
"id": endpoint.Id,
|
||||
"type": endpoint.Type,
|
||||
"tag": endpoint.Tag,
|
||||
"ext": endpoint.Ext,
|
||||
}
|
||||
if endpoint.Options != nil {
|
||||
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
|
||||
}
|
||||
|
||||
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() {
|
||||
configData, err := endpoint.MarshalJSON()
|
||||
if err != nil {
|
||||
|
||||
+97
-14
@@ -48,6 +48,7 @@ func (s *InboundService) GetAll() (*[]map[string]interface{}, error) {
|
||||
}
|
||||
var data []map[string]interface{}
|
||||
for _, inbound := range inbounds {
|
||||
var shadowtls_version uint
|
||||
inbData := map[string]interface{}{
|
||||
"id": inbound.Id,
|
||||
"type": inbound.Type,
|
||||
@@ -61,7 +62,25 @@ func (s *InboundService) GetAll() (*[]map[string]interface{}, error) {
|
||||
}
|
||||
inbData["listen"] = restFields["listen"]
|
||||
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)
|
||||
}
|
||||
return &data, nil
|
||||
@@ -77,7 +96,7 @@ func (s *InboundService) FromIds(ids []uint) ([]*model.Inbound, error) {
|
||||
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 id uint
|
||||
|
||||
@@ -88,7 +107,6 @@ func (s *InboundService) Save(tx *gorm.DB, act string, data json.RawMessage, hos
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
id = inbound.Id
|
||||
if inbound.TlsId > 0 {
|
||||
err = tx.Model(model.Tls{}).Where("id = ?", inbound.TlsId).Find(&inbound.Tls).Error
|
||||
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 act == "edit" {
|
||||
var oldTag string
|
||||
@@ -114,7 +143,11 @@ func (s *InboundService) Save(tx *gorm.DB, act string, data json.RawMessage, hos
|
||||
return 0, err
|
||||
}
|
||||
|
||||
inboundConfig, err = s.addUsers(tx, inboundConfig, inbound.Id, inbound.Type)
|
||||
if act == "edit" {
|
||||
inboundConfig, err = s.addUsers(tx, inboundConfig, inbound.Id, inbound.Type)
|
||||
} else {
|
||||
inboundConfig, err = s.initUsers(tx, inboundConfig, initUserIds, inbound.Type)
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
@@ -124,16 +157,6 @@ func (s *InboundService) Save(tx *gorm.DB, act string, data json.RawMessage, hos
|
||||
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":
|
||||
var tag string
|
||||
err = json.Unmarshal(data, &tag)
|
||||
@@ -214,6 +237,13 @@ func (s *InboundService) addUsers(db *gorm.DB, inboundJson []byte, inboundId uin
|
||||
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" {
|
||||
@@ -233,7 +263,60 @@ func (s *InboundService) addUsers(db *gorm.DB, inboundJson []byte, inboundId uin
|
||||
usersJson = append(usersJson, json.RawMessage(user))
|
||||
}
|
||||
|
||||
inbound["users"] = usersJson
|
||||
if len(usersJson) > 0 || inboundType != "shadowsocks" {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ func (s *ServerService) GenKeypair(keyType string, options string) []string {
|
||||
case "reality":
|
||||
return s.generateRealityKeyPair()
|
||||
case "wireguard":
|
||||
return generateWireGuardKey(options)
|
||||
return s.generateWireGuardKey(options)
|
||||
}
|
||||
|
||||
return []string{"Failed to generate keypair"}
|
||||
@@ -195,7 +195,7 @@ func (s *ServerService) generateRealityKeyPair() []string {
|
||||
return []string{"PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]), "PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:])}
|
||||
}
|
||||
|
||||
func generateWireGuardKey(pk string) []string {
|
||||
func (s *ServerService) generateWireGuardKey(pk string) []string {
|
||||
if len(pk) > 0 {
|
||||
key, _ := wgtypes.ParseKey(pk)
|
||||
return []string{key.PublicKey().String()}
|
||||
|
||||
@@ -26,8 +26,7 @@ var defaultConfig = `{
|
||||
"protocol": [
|
||||
"dns"
|
||||
],
|
||||
"outbound": "dns-out",
|
||||
"action": "route"
|
||||
"action": "hijack-dns"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -6,8 +6,6 @@ import (
|
||||
"s-ui/logger"
|
||||
"s-ui/util/common"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
@@ -63,7 +61,7 @@ func (s *UserService) CheckUser(username string, password string, remoteIP strin
|
||||
Where("username = ? and password = ?", username, password).
|
||||
First(user).
|
||||
Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
if database.IsNotFound(err) {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
logger.Warning("check user err:", err, " IP: ", remoteIP)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -21,7 +21,6 @@ const defaultJson = `
|
||||
"mtu": 9000,
|
||||
"auto_route": true,
|
||||
"strict_route": false,
|
||||
"sniff": true,
|
||||
"endpoint_independent_nat": false,
|
||||
"stack": "system",
|
||||
"platform": {
|
||||
@@ -36,7 +35,6 @@ const defaultJson = `
|
||||
"type": "mixed",
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 2080,
|
||||
"sniff": true,
|
||||
"users": []
|
||||
}
|
||||
]
|
||||
@@ -204,14 +202,6 @@ func (j *JsonService) addDefaultOutbounds(outbounds *[]map[string]interface{}, o
|
||||
"type": "direct",
|
||||
"tag": "direct",
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out",
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"tag": "block",
|
||||
},
|
||||
}
|
||||
*outbounds = append(outbound, *outbounds...)
|
||||
}
|
||||
|
||||
@@ -24,3 +24,7 @@ func Random(n int) string {
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
func RandomInt(n int) int {
|
||||
return rnd.Intn(n)
|
||||
}
|
||||
|
||||
+52
-8
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"s-ui/database/model"
|
||||
"s-ui/util/common"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -19,7 +20,7 @@ func LinkGenerator(clientConfig json.RawMessage, i *model.Inbound, hostname stri
|
||||
|
||||
var tls map[string]interface{}
|
||||
if i.TlsId > 0 {
|
||||
json.Unmarshal(i.Tls.Client, &tls)
|
||||
tls = prepareTls(i.Tls)
|
||||
}
|
||||
|
||||
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 {
|
||||
addrRemark, _ := addr["remark"].(string)
|
||||
Addrs[index]["remark"] = i.Tag + addrRemark
|
||||
if addrTls, ok := addr["tls"].(map[string]interface{}); ok {
|
||||
if i.TlsId > 0 {
|
||||
newTls := map[string]interface{}{}
|
||||
if oldTls, hasOldTls := tls["tls"].(map[string]interface{}); hasOldTls {
|
||||
for k, v := range oldTls {
|
||||
for k, v := range tls {
|
||||
newTls[k] = v
|
||||
}
|
||||
|
||||
// Override tls
|
||||
if addrTls, ok := addr["tls"].(map[string]interface{}); ok {
|
||||
for k, v := range addrTls {
|
||||
newTls[k] = v
|
||||
}
|
||||
}
|
||||
// Override tls
|
||||
for k, v := range addrTls {
|
||||
newTls[k] = v
|
||||
}
|
||||
Addrs[index]["tls"] = newTls
|
||||
}
|
||||
}
|
||||
@@ -80,6 +82,28 @@ func LinkGenerator(clientConfig json.RawMessage, i *model.Inbound, hostname stri
|
||||
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(
|
||||
userConfig map[string]map[string]interface{},
|
||||
inbound map[string]interface{},
|
||||
@@ -507,3 +531,23 @@ func getTransportParams(t interface{}) map[string]string {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
@@ -98,6 +98,13 @@ func addTls(out *map[string]interface{}, tls *model.Tls) {
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
Generated
+675
-380
File diff suppressed because it is too large
Load Diff
+22
-22
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.2.0-beta.2",
|
||||
"version": "1.2.0-beta.3",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
@@ -10,33 +10,33 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "7.4.47",
|
||||
"axios": "^1.7.4",
|
||||
"chart.js": "^4.4.3",
|
||||
"axios": "^1.7.9",
|
||||
"chart.js": "^4.4.7",
|
||||
"clipboard": "^2.0.11",
|
||||
"core-js": "^3.37.1",
|
||||
"core-js": "^3.40.0",
|
||||
"moment": "^2.30.1",
|
||||
"notivue": "^2.4.4",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"notivue": "^2.4.5",
|
||||
"pinia": "^2.3.0",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"vue": "^3.4.31",
|
||||
"vue-chartjs": "^5.3.1",
|
||||
"vue-i18n": "^9.14.2",
|
||||
"vue-router": "^4.4.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-chartjs": "^5.3.2",
|
||||
"vue-i18n": "^11.0.1",
|
||||
"vue-router": "^4.5.0",
|
||||
"vue3-persian-datetime-picker": "^1.2.2",
|
||||
"vuetify": "^3.6.10"
|
||||
"vuetify": "^3.7.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.24.7",
|
||||
"@types/node": "^20.14.9",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"@babel/types": "^7.26.5",
|
||||
"@types/node": "^22.10.7",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"material-design-icons-iconfont": "^6.7.0",
|
||||
"sass": "1.77.6",
|
||||
"typescript": "^5.5.2",
|
||||
"unplugin-fonts": "^1.1.1",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-vuetify": "^2.0.3",
|
||||
"vue-tsc": "^2.0.22"
|
||||
"sass": "1.83.4",
|
||||
"typescript": "^5.7.3",
|
||||
"unplugin-fonts": "^1.3.1",
|
||||
"vite": "^6.0.7",
|
||||
"vite-plugin-vuetify": "^2.0.4",
|
||||
"vue-tsc": "^2.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<LogVue
|
||||
v-model="logModal.visible"
|
||||
:visible="logModal.visible"
|
||||
@close="closeLogs"
|
||||
/>
|
||||
<LogVue v-model="logModal.visible" :control="logModal" :visible="logModal.visible" />
|
||||
<Backup v-model="backupModal.visible" :control="backupModal" :visible="backupModal.visible" />
|
||||
<v-container class="fill-height" :loading="loading">
|
||||
<v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" >
|
||||
<v-row class="d-flex align-center justify-center">
|
||||
@@ -46,6 +43,8 @@
|
||||
</v-row>
|
||||
</v-card>
|
||||
</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-row>
|
||||
<v-row>
|
||||
@@ -86,18 +85,8 @@
|
||||
<v-col cols="3">S-UI</v-col>
|
||||
<v-col cols="9">
|
||||
<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-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 cols="3">{{ $t('main.info.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 { i18n } from '@/locales'
|
||||
import LogVue from '@/layouts/modals/Logs.vue'
|
||||
import Backup from '@/layouts/modals/Backup.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const menu = ref(false)
|
||||
@@ -235,17 +225,9 @@ onBeforeUnmount(() => {
|
||||
stopTimer()
|
||||
})
|
||||
|
||||
const logModal = ref({
|
||||
visible: false,
|
||||
})
|
||||
const logModal = ref({ visible: false })
|
||||
|
||||
const openLogs = () => {
|
||||
logModal.value.visible = true
|
||||
}
|
||||
|
||||
const closeLogs = () => {
|
||||
logModal.value.visible = false
|
||||
}
|
||||
const backupModal = ref({ visible: false })
|
||||
|
||||
const restartSingbox = async () => {
|
||||
loading.value = true
|
||||
|
||||
@@ -153,7 +153,6 @@ export default {
|
||||
"mtu": 9000,
|
||||
"auto_route": true,
|
||||
"strict_route": false,
|
||||
"sniff": true,
|
||||
"endpoint_independent_nat": false,
|
||||
"stack": "system",
|
||||
"exclude_package": [],
|
||||
@@ -169,7 +168,6 @@ export default {
|
||||
"type": "mixed",
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 2080,
|
||||
"sniff": true,
|
||||
"users": []
|
||||
}
|
||||
],
|
||||
@@ -199,24 +197,24 @@ export default {
|
||||
"tag": "local-dns",
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
},
|
||||
{
|
||||
"address": "rcode://success",
|
||||
"tag": "block"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"clash_mode": "Global",
|
||||
"source_ip_cidr": [
|
||||
"172.19.0.0/30"
|
||||
"172.19.0.0/30",
|
||||
"fdfe:dcba:9876::1/126"
|
||||
],
|
||||
"action": "route",
|
||||
"server": "proxy-dns"
|
||||
},
|
||||
{
|
||||
"source_ip_cidr": [
|
||||
"172.19.0.0/30"
|
||||
"172.19.0.0/30",
|
||||
"fdfe:dcba:9876::1/126"
|
||||
],
|
||||
"action": "route",
|
||||
"server": "proxy-dns"
|
||||
}
|
||||
],
|
||||
@@ -319,10 +317,10 @@ export default {
|
||||
if (v) {
|
||||
this.subJsonExt.dns = this.defaultDns
|
||||
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 {
|
||||
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 (this.rules.length == 0) delete this.subJsonExt.rules
|
||||
}
|
||||
@@ -348,7 +346,7 @@ export default {
|
||||
if (v?.length>0) {
|
||||
if (sIndex === -1) {
|
||||
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 {
|
||||
this.dns.servers[sIndex].address = v
|
||||
}
|
||||
@@ -369,7 +367,7 @@ export default {
|
||||
if (ruleIndex >= 0){
|
||||
this.dns.rules[ruleIndex].rule_set = v
|
||||
} else {
|
||||
this.dns.rules.push({ rule_set: v, server: "direct-dns" })
|
||||
this.dns.rules.push({ rule_set: v, action: "route", server: "direct-dns" })
|
||||
}
|
||||
} else {
|
||||
if (ruleIndex != -1) this.dns.rules.splice(ruleIndex,1)
|
||||
@@ -395,7 +393,7 @@ export default {
|
||||
this.rules[ruleIndex].rule_set = v
|
||||
} else {
|
||||
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 {
|
||||
if (ruleIndex != -1) this.rules.splice(ruleIndex,1)
|
||||
@@ -405,17 +403,17 @@ export default {
|
||||
},
|
||||
ruleToBlock: {
|
||||
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 : []
|
||||
},
|
||||
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 (ruleIndex >= 0){
|
||||
this.rules[ruleIndex].rule_set = v
|
||||
} else {
|
||||
if (this.rules == undefined) this.subJsonExt.rules = []
|
||||
this.rules.push({ rule_set: v, outbound: "block" })
|
||||
this.rules.push({ rule_set: v, action: "reject" })
|
||||
}
|
||||
} else {
|
||||
if (ruleIndex != -1) this.rules.splice(ruleIndex,1)
|
||||
|
||||
@@ -2,34 +2,41 @@
|
||||
<v-card :subtitle="$t('pages.clients')">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch
|
||||
v-model="hasUser"
|
||||
@change="() => {inbound.users = hasUser? [] : undefined}"
|
||||
color="primary"
|
||||
:label="$t('in.clients')"
|
||||
hide-details></v-switch>
|
||||
<v-select v-model="data.model" :items="initUsersModels" @update:model-value="data.values = []" hide-details></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.model == 'group'">
|
||||
<v-select v-model="data.values" multiple chips :items="groupNames" :label="$t('client.group')" hide-details></v-select>
|
||||
</v-col>
|
||||
<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-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { i18n } from '@/locales';
|
||||
|
||||
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
props: ['data', 'clients'],
|
||||
data() {
|
||||
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: {
|
||||
cardTitle() {
|
||||
this.hasUser = Object.hasOwn(this.$props.inbound,'users')
|
||||
return this.$props.inbound?.type.toUpperCase()
|
||||
clientNames() {
|
||||
return this.$props.clients.map((c:any) => { return { title: c.name, value: c.id } } )
|
||||
},
|
||||
groupNames() {
|
||||
return Array.from(new Set(this.$props.clients.map((c:any) => c.group)))
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.hasUser = Object.hasOwn(this.$props.inbound,'users')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -6,6 +6,7 @@
|
||||
hide-details
|
||||
:items="[1,2,3]"
|
||||
:label="$t('version')"
|
||||
:disabled="data.id > 0"
|
||||
v-model="version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
@@ -115,23 +116,18 @@ export default {
|
||||
switch (newValue) {
|
||||
case 1:
|
||||
delete this.Inbound.password
|
||||
delete this.Inbound.users
|
||||
delete this.Inbound.handshake_for_server_name
|
||||
break;
|
||||
case 2:
|
||||
if (!this.Inbound.password) {
|
||||
this.Inbound.password = ""
|
||||
}
|
||||
delete this.Inbound.users
|
||||
if (!this.Inbound.handshake_for_server_name) {
|
||||
this.Inbound.handshake_for_server_name = {}
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
delete this.Inbound.password
|
||||
if (!Object.hasOwn(this.Inbound, 'users')) {
|
||||
this.Inbound.users = []
|
||||
}
|
||||
if (!this.Inbound.handshake_for_server_name) {
|
||||
this.Inbound.handshake_for_server_name = {}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -13,7 +13,7 @@
|
||||
<v-col cols="12" sm="8">
|
||||
<v-text-field
|
||||
v-model="options.public_key"
|
||||
disabled
|
||||
readonly
|
||||
:label="$t('tls.pubKey')"
|
||||
append-icon="mdi-refresh"
|
||||
@click:append="getWgPubKey()"
|
||||
|
||||
@@ -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>
|
||||
@@ -1,12 +1,18 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card class="rounded-lg" :loading="loading">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.client') }}
|
||||
</v-card-title>
|
||||
<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-container style="padding: 0;">
|
||||
<v-container style="padding: 0;" :hidden="loading">
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
align-tabs="center"
|
||||
@@ -74,10 +80,14 @@
|
||||
v-model="clientInbounds"
|
||||
:items="inboundTags"
|
||||
:label="$t('client.inboundTags')"
|
||||
clearable
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-select>
|
||||
hide-details>
|
||||
<template v-slot:append>
|
||||
<v-icon @click="setAllInbounds" icon="mdi-set-all" v-tooltip:top="$t('all')" />
|
||||
</template>
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
@@ -178,12 +188,13 @@
|
||||
</template>
|
||||
|
||||
<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 { HumanReadable } from '@/plugins/utils'
|
||||
import Data from '@/store/modules/data';
|
||||
|
||||
export default {
|
||||
props: ['visible', 'data', 'id', 'inboundTags', 'groups'],
|
||||
props: ['visible', 'id', 'inboundTags', 'groups'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
@@ -198,21 +209,23 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData() {
|
||||
async updateData() {
|
||||
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.title = "edit"
|
||||
this.clientConfig = this.client.config
|
||||
this.loading = false
|
||||
}
|
||||
else {
|
||||
this.client = createClient()
|
||||
this.title = "add"
|
||||
this.clientConfig = randomConfigs('client')
|
||||
}
|
||||
this.links = this.client.links.filter(l => l.type == 'local')
|
||||
this.extLinks = this.client.links.filter(l => l.type == 'external')
|
||||
this.subLinks = this.client.links.filter(l => l.type == 'sub')
|
||||
this.links = this.client.links?.filter(l => l.type == 'local')?? []
|
||||
this.extLinks = this.client.links?.filter(l => l.type == 'external')?? []
|
||||
this.subLinks = this.client.links?.filter(l => l.type == 'sub')?? []
|
||||
this.tab = "t1"
|
||||
},
|
||||
closeModal() {
|
||||
@@ -230,12 +243,15 @@ export default {
|
||||
},
|
||||
setDate(newDate:number){
|
||||
this.client.expiry = newDate
|
||||
},
|
||||
setAllInbounds(){
|
||||
this.client.inbounds = this.inboundTags.map((i:any) => i.value).sort()
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
clientInbounds: {
|
||||
get() { return this.client.inbounds.length>0 ? this.client.inbounds : [] },
|
||||
set(v:number[]) { this.client.inbounds = v.length == 0 ? [] : v }
|
||||
get() { return this.client.inbounds.length>0 ? this.client.inbounds.sort() : [] },
|
||||
set(v:number[]) { this.client.inbounds = v.length == 0 ? [] : v.sort() }
|
||||
},
|
||||
expDate: {
|
||||
get() { return this.client.expiry},
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
<pre dir="ltr">{{ bulkData }}</pre>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
<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" />
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
@@ -50,6 +51,7 @@ import { EpTypes, createEndpoint } from '@/types/endpoints'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
import Dial from '@/components/Dial.vue'
|
||||
import Wireguard from '@/components/protocols/Wireguard.vue'
|
||||
import Warp from '@/components/protocols/Warp.vue'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { push } from 'notivue'
|
||||
import { i18n } from '@/locales'
|
||||
@@ -151,6 +153,6 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { Dial, Wireguard }
|
||||
components: { Dial, Wireguard, Warp }
|
||||
}
|
||||
</script>
|
||||
@@ -50,7 +50,7 @@
|
||||
<Tun v-if="inbound.type == inTypes.Tun" :data="inbound" />
|
||||
<TProxy v-if="inbound.type == inTypes.TProxy" :inbound="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" />
|
||||
<Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="in" :data="inbound" />
|
||||
</v-window-item>
|
||||
@@ -95,7 +95,7 @@
|
||||
</template>
|
||||
|
||||
<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 Listen from '@/components/Listen.vue'
|
||||
@@ -125,7 +125,11 @@ export default {
|
||||
loading: false,
|
||||
side: "s",
|
||||
inTypes: InTypes,
|
||||
HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks],
|
||||
inboundWithUsers: inboundWithUsers,
|
||||
initUsers: {
|
||||
model: 'none',
|
||||
values: <any>[],
|
||||
},
|
||||
HasInData: [
|
||||
InTypes.SOCKS,
|
||||
InTypes.HTTP,
|
||||
@@ -179,6 +183,10 @@ export default {
|
||||
this.loading = false
|
||||
}
|
||||
this.side = "s"
|
||||
this.initUsers = {
|
||||
model: 'none',
|
||||
values: [],
|
||||
}
|
||||
},
|
||||
changeType() {
|
||||
if (!this.inbound.listen_port) this.inbound.listen_port = RandomUtil.randomIntRange(10000, 60000)
|
||||
@@ -205,7 +213,22 @@ export default {
|
||||
},
|
||||
saveChanges() {
|
||||
this.loading = true
|
||||
this.$emit('save', this.inbound)
|
||||
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.loading = false
|
||||
},
|
||||
},
|
||||
@@ -217,6 +240,15 @@ export default {
|
||||
if (this.OnlyTLS.includes(this.inbound.type) && this.inbound.tls_id == 0) return false
|
||||
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: {
|
||||
visible(newValue) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<v-col>{{ $t('basic.log.title') }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-icon icon="mdi-close" @click="$emit('close')" />
|
||||
<v-icon icon="mdi-close" @click="control.visible = false" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
@@ -48,10 +48,10 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import HttpUtils from '@/plugins/httputil';
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
|
||||
export default {
|
||||
props: ['visible'],
|
||||
props: ['control', 'visible'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
@@ -77,11 +77,11 @@ export default {
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
visible(v) {
|
||||
this.lines = []
|
||||
this.logLevel = 'info'
|
||||
this.logCount = 10
|
||||
if (newValue) {
|
||||
if (v) {
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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-row>
|
||||
<v-col>QrCode</v-col>
|
||||
@@ -9,7 +9,13 @@
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<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-model="tab"
|
||||
density="compact"
|
||||
@@ -24,19 +30,19 @@
|
||||
<v-row>
|
||||
<v-col style="text-align: center;">
|
||||
<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-row>
|
||||
<v-row>
|
||||
<v-col style="text-align: center;">
|
||||
<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-row>
|
||||
<v-row>
|
||||
<v-col style="text-align: center;">
|
||||
<v-chip>SING-BOX</v-chip><br />
|
||||
<QrcodeVue :value="singbox" :size="size" @click="copyToClipboard(singbox)" :margin="1" style="border-radius: .8rem;" />
|
||||
<v-chip>SING-BOX (scan only)</v-chip><br />
|
||||
<QrcodeVue :value="singbox" :size="size" :margin="1" style="border-radius: .8rem; cursor: not-allowed;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
@@ -44,7 +50,7 @@
|
||||
<v-row v-for="l in clientLinks">
|
||||
<v-col style="text-align: center;">
|
||||
<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-row>
|
||||
</v-window-item>
|
||||
@@ -62,13 +68,21 @@ import { i18n } from '@/locales'
|
||||
import { push } from 'notivue'
|
||||
|
||||
export default {
|
||||
props: ['index', 'visible'],
|
||||
props: ['id', 'visible'],
|
||||
data() {
|
||||
return {
|
||||
tab: "sub",
|
||||
client: <any>{},
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async load() {
|
||||
this.loading = true
|
||||
const newData = await Data().loadClients(this.$props.id)
|
||||
this.client = newData
|
||||
this.loading = false
|
||||
},
|
||||
copyToClipboard(txt:string) {
|
||||
const hiddenButton = document.createElement('button')
|
||||
hiddenButton.className = 'clipboard-btn'
|
||||
@@ -101,11 +115,6 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
clients() { return Data().clients },
|
||||
client() {
|
||||
if ( typeof this.$props.index != 'number' ) return <any>{}
|
||||
return this.clients[this.$props.index]
|
||||
},
|
||||
clientSub() {
|
||||
return Data().subURI + this.client.name
|
||||
},
|
||||
@@ -126,6 +135,7 @@ export default {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.tab = "sub"
|
||||
this.load()
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="ruleData.sniff"
|
||||
v-model="ruleData.sniffer"
|
||||
:items="sniffers"
|
||||
:label="$t('rule.sniffer')"
|
||||
multiple
|
||||
@@ -254,7 +254,7 @@ export default {
|
||||
this.loading = true
|
||||
let newRule = <any>{
|
||||
action: this.ruleData.action,
|
||||
invert: this.ruleData.invert,
|
||||
invert: this.ruleData.invert? this.ruleData.invert : undefined,
|
||||
}
|
||||
|
||||
// Filter action data
|
||||
@@ -263,25 +263,25 @@ export default {
|
||||
newRule.outbound = this.ruleData.outbound
|
||||
break
|
||||
case 'route-options':
|
||||
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.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.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.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.udp_disable_domain_unmapping = this.ruleData.udp_disable_domain_unmapping? 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
|
||||
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
|
||||
break
|
||||
case 'sniff':
|
||||
newRule.sniffer = this.ruleData.sniffer.length > 0 ? this.ruleData.sniffer : undefined
|
||||
newRule.timeout = this.ruleData.timeout.length > 0 ? this.ruleData.timeout : undefined
|
||||
newRule.sniffer = this.ruleData.sniffer?.length > 0 ? this.ruleData.sniffer : undefined
|
||||
newRule.timeout = this.ruleData.timeout?.length > 0 ? this.ruleData.timeout : undefined
|
||||
break
|
||||
case 'resolve':
|
||||
newRule.strategy = this.ruleData.strategy.length > 0 ? this.ruleData.strategy : undefined
|
||||
newRule.server = this.ruleData.server.length > 0 ? this.ruleData.server : undefined
|
||||
newRule.strategy = this.ruleData.strategy?.length > 0 ? this.ruleData.strategy : undefined
|
||||
newRule.server = this.ruleData.server?.length > 0 ? this.ruleData.server : undefined
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,17 @@
|
||||
<v-radio-group v-model="limit" @change="loadData" density="compact" :loading="loading" inline hide-details>
|
||||
<v-radio v-for="p in periods" :label="p.title" :value="p.value"></v-radio>
|
||||
</v-radio-group>
|
||||
<v-container id="container" style="height:40vh;">
|
||||
<v-alert :text="$t('noData')" type="warning" variant="outlined" v-if="alert"></v-alert>
|
||||
<Line v-if="loaded" :data="usage" :options="<any>options" />
|
||||
<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>
|
||||
<Line v-if="loaded" :data="usage" :options="<any>options" />
|
||||
</template>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
@@ -334,11 +334,6 @@ export default {
|
||||
],
|
||||
fingerprints: [
|
||||
{ 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: "Microsoft Edge", value: "edge" },
|
||||
{ title: "Apple Safari", value: "safari" },
|
||||
@@ -356,7 +351,7 @@ export default {
|
||||
if (this.$props.id > 0) {
|
||||
const newData = <tls>JSON.parse(this.$props.data)
|
||||
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 = {}
|
||||
this.tlsType = newData.server?.reality == undefined ? 0 : 1
|
||||
this.usePath = newData.server?.key == undefined ? 0 : 1
|
||||
|
||||
@@ -72,6 +72,13 @@ export default {
|
||||
threads: "Threads",
|
||||
memory: "Memory",
|
||||
running: "Running"
|
||||
},
|
||||
backup: {
|
||||
title: "Backup & Restore",
|
||||
backup: "Download Backup",
|
||||
restore: "Restore",
|
||||
exclStats: "Exclude graphs",
|
||||
exclChanges: "Exclude changes",
|
||||
}
|
||||
},
|
||||
objects: {
|
||||
|
||||
@@ -72,7 +72,14 @@ export default {
|
||||
threads: "نخها",
|
||||
memory: "حافظه",
|
||||
running: "اجرا"
|
||||
}
|
||||
},
|
||||
backup: {
|
||||
title: "پشتیبانگیری و بازیابی",
|
||||
backup: "دریافت پشتیبان",
|
||||
restore: "بازیابی",
|
||||
exclStats: "بدون گرافها",
|
||||
exclChanges: "بدون تغییرات",
|
||||
},
|
||||
},
|
||||
objects: {
|
||||
inbound: "ورودی",
|
||||
|
||||
@@ -72,6 +72,13 @@ export default {
|
||||
threads: "Потоки",
|
||||
memory: "Память",
|
||||
running: "Работает"
|
||||
},
|
||||
backup: {
|
||||
title: "Резервное копирование и восстановление",
|
||||
backup: "Скачать резервную копию",
|
||||
restore: "Восстановить",
|
||||
exclStats: "Исключить графики",
|
||||
exclChanges: "Исключить изменения",
|
||||
}
|
||||
},
|
||||
objects: {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { title } from "process";
|
||||
|
||||
export default {
|
||||
message: "Chào mừng OHB",
|
||||
success: "Thành công",
|
||||
@@ -70,6 +72,13 @@ export default {
|
||||
threads: "Luồng",
|
||||
memory: "Bộ nhớ",
|
||||
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: {
|
||||
|
||||
@@ -70,7 +70,14 @@ export default {
|
||||
threads: "线程",
|
||||
memory: "内存",
|
||||
running: "运行状态"
|
||||
}
|
||||
},
|
||||
backup: {
|
||||
title: "备份与恢复",
|
||||
backup: "下载备份",
|
||||
restore: "恢复",
|
||||
exclStats: "排除图表数据",
|
||||
exclChanges: "排除变更数据",
|
||||
},
|
||||
},
|
||||
objects: {
|
||||
inbound: "入站",
|
||||
|
||||
@@ -71,7 +71,14 @@ export default {
|
||||
threads: "線程",
|
||||
memory: "內存",
|
||||
running: "運行狀態"
|
||||
}
|
||||
},
|
||||
backup: {
|
||||
title: "備份與恢復",
|
||||
backup: "下載備份",
|
||||
restore: "恢復",
|
||||
exclStats: "排除圖表記錄",
|
||||
exclChanges: "排除更改記錄",
|
||||
},
|
||||
},
|
||||
objects: {
|
||||
inbound: "入站",
|
||||
|
||||
@@ -3,8 +3,7 @@ import { defineStore } from 'pinia'
|
||||
import { push } from 'notivue'
|
||||
import { i18n } from '@/locales'
|
||||
import { Inbound } from '@/types/inbounds'
|
||||
import { Outbound } from '@/types/outbounds'
|
||||
import { Endpoint } from '@/types/endpoints'
|
||||
import { Client } from '@/types/clients'
|
||||
|
||||
const Data = defineStore('Data', {
|
||||
state: () => ({
|
||||
@@ -13,10 +12,10 @@ const Data = defineStore('Data', {
|
||||
subURI: "",
|
||||
onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]},
|
||||
config: <any>{},
|
||||
inbounds: <Inbound[]>[],
|
||||
outbounds: <Outbound[]>[],
|
||||
endpoints: <Endpoint[]>[],
|
||||
clients: [],
|
||||
inbounds: <any[]>[],
|
||||
outbounds: <any[]>[],
|
||||
endpoints: <any[]>[],
|
||||
clients: <any>[],
|
||||
tlsConfigs: <any[]>[],
|
||||
}),
|
||||
actions: {
|
||||
@@ -55,11 +54,20 @@ const Data = defineStore('Data', {
|
||||
}
|
||||
return <Inbound[]>[]
|
||||
},
|
||||
async save (object: string, action: string, data: any): 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 = {
|
||||
object: object,
|
||||
action: action,
|
||||
data: JSON.stringify(data, null, 2),
|
||||
initUsers: initUsers?.join(',') ?? undefined
|
||||
}
|
||||
const msg = await HttpUtils.post('api/save', postData)
|
||||
if (msg.success) {
|
||||
|
||||
@@ -10,9 +10,9 @@ export interface Client {
|
||||
id?: number
|
||||
enable: boolean
|
||||
name: string
|
||||
config: Config
|
||||
config?: Config
|
||||
inbounds: number[]
|
||||
links: Link[]
|
||||
links?: Link[]
|
||||
volume: number
|
||||
expiry: number
|
||||
up: number
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Dial } from "./outbounds"
|
||||
|
||||
export const EpTypes = {
|
||||
Wireguard: 'wireguard',
|
||||
Warp: 'warp',
|
||||
}
|
||||
|
||||
type EpType = typeof EpTypes[keyof typeof EpTypes]
|
||||
@@ -34,6 +35,10 @@ export interface WireGuard extends EndpointBasics, Dial {
|
||||
workers?: number
|
||||
}
|
||||
|
||||
export interface Warp extends WireGuard {
|
||||
ext: any
|
||||
}
|
||||
|
||||
// Create interfaces dynamically based on EpTypes keys
|
||||
type InterfaceMap = {
|
||||
[Key in keyof typeof EpTypes]: {
|
||||
@@ -48,6 +53,7 @@ export type Endpoint = InterfaceMap[keyof InterfaceMap]
|
||||
// Create defaultValues object dynamically
|
||||
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: ''}] },
|
||||
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 {
|
||||
|
||||
@@ -52,32 +52,6 @@ interface InboundBasics extends Listen {
|
||||
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 {
|
||||
server: string
|
||||
server_port: number
|
||||
@@ -88,30 +62,21 @@ export interface Direct extends InboundBasics {
|
||||
override_address?: string
|
||||
override_port?: number
|
||||
}
|
||||
export interface Mixed extends InboundBasics {
|
||||
users?: UsernamePass[]
|
||||
}
|
||||
export interface SOCKS extends InboundBasics {
|
||||
users?: UsernamePass[]
|
||||
}
|
||||
export interface HTTP extends InboundBasics {
|
||||
users?: UsernamePass[]
|
||||
}
|
||||
export interface Mixed extends InboundBasics {}
|
||||
export interface SOCKS extends InboundBasics {}
|
||||
export interface HTTP extends InboundBasics {}
|
||||
export interface Shadowsocks extends InboundBasics {
|
||||
method: string
|
||||
password: string
|
||||
network?: "udp" | "tcp"
|
||||
users?: NamePass[]
|
||||
multiplex?: iMultiplex
|
||||
}
|
||||
export interface VMess extends InboundBasics {
|
||||
users: VmessUser[]
|
||||
tls: iTls
|
||||
multiplex?: iMultiplex
|
||||
transport?: Transport
|
||||
}
|
||||
export interface Trojan extends InboundBasics {
|
||||
users: NamePass[]
|
||||
tls: iTls
|
||||
fallback?: {
|
||||
server: string
|
||||
@@ -121,14 +86,12 @@ export interface Trojan extends InboundBasics {
|
||||
transport?: Transport
|
||||
}
|
||||
export interface Naive extends InboundBasics {
|
||||
users: UsernamePass[]
|
||||
tls: iTls,
|
||||
}
|
||||
export interface Hysteria extends InboundBasics {
|
||||
up_mbps: number
|
||||
down_mbps: number
|
||||
obfs?: string
|
||||
users: NameAuth[]
|
||||
recv_window_conn?: number
|
||||
recv_window_client?: number
|
||||
max_conn_client?: number
|
||||
@@ -137,7 +100,6 @@ export interface Hysteria extends InboundBasics {
|
||||
export interface ShadowTLS extends InboundBasics {
|
||||
version: 1|2|3
|
||||
password?: string
|
||||
users?: NamePass[]
|
||||
handshake: ShadowTLSHandShake
|
||||
handshake_for_server_name?: {
|
||||
[server_name: string]: ShadowTLSHandShake
|
||||
@@ -145,12 +107,10 @@ export interface ShadowTLS extends InboundBasics {
|
||||
strict_mode?: boolean
|
||||
}
|
||||
export interface VLESS extends InboundBasics {
|
||||
users: VlessUser[]
|
||||
multiplex?: iMultiplex
|
||||
transport?: Transport
|
||||
}
|
||||
export interface TUIC extends InboundBasics {
|
||||
users: TuicUser[]
|
||||
congestion_control: ""|"cubic"|"new_reno"|"bbr"
|
||||
auth_timeout?: string
|
||||
zero_rtt_handshake?: boolean
|
||||
@@ -163,7 +123,6 @@ export interface Hysteria2 extends InboundBasics {
|
||||
type?: "salamander"
|
||||
password: string
|
||||
}
|
||||
users: NamePass[]
|
||||
ignore_client_bandwidth?: boolean
|
||||
masquerade?: string | {
|
||||
type: string
|
||||
@@ -245,7 +204,7 @@ type userEnabledTypes = {
|
||||
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
|
||||
type InboundWithUser = userEnabledTypes[keyof userEnabledTypes]
|
||||
@@ -257,14 +216,14 @@ const defaultValues: Record<InType, Inbound> = {
|
||||
socks: <SOCKS>{ type: InTypes.SOCKS },
|
||||
http: <HTTP>{ type: InTypes.HTTP, tls_id: 0 },
|
||||
shadowsocks: <Shadowsocks>{ type: InTypes.Shadowsocks, method: 'none', multiplex: {} },
|
||||
vmess: <VMess>{ type: InTypes.VMess, users: <VmessUser[]>[], tls_id: 0, multiplex: {}, transport: {} },
|
||||
trojan: <Trojan>{ type: InTypes.Trojan, users: <NamePass[]>[], tls_id: 0, multiplex: {}, transport: {} },
|
||||
naive: <Naive>{ type: InTypes.Naive, users: <UsernamePass[]>[], tls_id: 0 },
|
||||
hysteria: <Hysteria>{ type: InTypes.Hysteria, users: <NameAuth[]>[], up_mbps: 100, down_mbps: 100, tls_id: 0 },
|
||||
shadowtls: <ShadowTLS>{ type: InTypes.ShadowTLS, version: 3, users: <NamePass[]>[], handshake: {}, handshake_for_server_name: {} },
|
||||
tuic: <TUIC>{ type: InTypes.TUIC, users: <TuicUser[]>[], congestion_control: "cubic", tls_id: 0 },
|
||||
hysteria2: <Hysteria2>{ type: InTypes.Hysteria2, users: <NamePass[]>[], tls_id: 0 },
|
||||
vless: <VLESS>{ type: InTypes.VLESS, users: <VlessUser[]>[], tls_id: 0, multiplex: {}, transport: {} },
|
||||
vmess: <VMess>{ type: InTypes.VMess, tls_id: 0, multiplex: {}, transport: {} },
|
||||
trojan: <Trojan>{ type: InTypes.Trojan, tls_id: 0, multiplex: {}, transport: {} },
|
||||
naive: <Naive>{ type: InTypes.Naive, tls_id: 0 },
|
||||
hysteria: <Hysteria>{ type: InTypes.Hysteria, up_mbps: 100, down_mbps: 100, tls_id: 0 },
|
||||
shadowtls: <ShadowTLS>{ type: InTypes.ShadowTLS, version: 3, handshake: {}, handshake_for_server_name: {} },
|
||||
tuic: <TUIC>{ type: InTypes.TUIC, congestion_control: "cubic", tls_id: 0 },
|
||||
hysteria2: <Hysteria2>{ type: InTypes.Hysteria2, tls_id: 0 },
|
||||
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 },
|
||||
redirect: <Redirect>{ type: InTypes.Redirect },
|
||||
tproxy: <TProxy>{ type: InTypes.TProxy },
|
||||
|
||||
@@ -264,10 +264,6 @@ const outboundTags = computed((): string[] => {
|
||||
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 dnsServersTags = computed((): string[] => {
|
||||
|
||||
+16
-160
@@ -4,7 +4,6 @@
|
||||
v-model="modal.visible"
|
||||
:visible="modal.visible"
|
||||
:id="modal.id"
|
||||
:data="modal.data"
|
||||
:groups="groups"
|
||||
:inboundTags="inboundTags"
|
||||
@close="closeModal"
|
||||
@@ -21,7 +20,7 @@
|
||||
<QrCode
|
||||
v-model="qrcode.visible"
|
||||
:visible="qrcode.visible"
|
||||
:index="qrcode.index"
|
||||
:id="qrcode.id"
|
||||
@close="closeQrCode"
|
||||
/>
|
||||
<Stats
|
||||
@@ -116,129 +115,8 @@
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</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-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-row>
|
||||
<v-col cols="12">
|
||||
<v-data-table
|
||||
:headers="headers"
|
||||
@@ -246,21 +124,19 @@
|
||||
:hide-default-footer="filterSettings.enabled ? filterSettings.filteredClients.length<=10 : clients.length<=10"
|
||||
hide-no-data
|
||||
fixed-header
|
||||
:group-by="groupBy"
|
||||
item-value="name"
|
||||
:mobile="smAndDown"
|
||||
mobile-breakpoint="sm"
|
||||
width="100%"
|
||||
class="elevation-3 rounded"
|
||||
>
|
||||
<template v-slot:group-header="{ item, columns, toggleGroup, isGroupOpen }">
|
||||
<tr>
|
||||
<td :colspan="columns.length" @click="toggleGroup(item)" style="min-height: fit-content; text-align: center;">
|
||||
<v-icon :icon="isGroupOpen(item) ? '$expand' : '$next'"></v-icon>
|
||||
{{ item.value.length>0 ? item.value : $t('none') }}
|
||||
<v-badge :content="(filterSettings.enabled ? filterSettings.filteredClients : clients).filter(c => c.group == item.value).length" inline color="success" />
|
||||
</td>
|
||||
</tr>
|
||||
<template v-slot:item.inbounds="{ item }">
|
||||
<span>
|
||||
<v-tooltip activator="parent" dir="ltr" location="start" 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 }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-slot:item.volume="{ item }">
|
||||
<div class="text-start">
|
||||
@@ -363,13 +239,13 @@ const isOnline = (cname: string) => computed(() => {
|
||||
return Data().onlines?.user ? Data().onlines.user.includes(cname) : false
|
||||
})
|
||||
|
||||
const inbounds = computed((): Inbound[] => {
|
||||
return <Inbound[]> Data().inbounds?? []
|
||||
const inbounds = computed((): any[] => {
|
||||
return Data().inbounds?? []
|
||||
})
|
||||
|
||||
const inboundTags = computed((): any[] => {
|
||||
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[] => {
|
||||
@@ -387,12 +263,6 @@ const filterSettings = ref({
|
||||
text: '',
|
||||
filteredClients: <any[]>[]
|
||||
})
|
||||
const tableView = ref(localStorage.getItem('clientView') == 'table')
|
||||
|
||||
const toggleClientView = () => {
|
||||
localStorage.setItem('clientView',tableView.value ? 'tile' : 'table')
|
||||
tableView.value = !tableView.value
|
||||
}
|
||||
|
||||
const filterItems = [
|
||||
{ title: i18n.global.t('none'), value: '' },
|
||||
@@ -403,30 +273,24 @@ const filterItems = [
|
||||
|
||||
const headers = [
|
||||
{ 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('stats.volume'), key: 'volume' },
|
||||
{ title: i18n.global.t('date.expiry'), key: 'expiry' },
|
||||
{ title: i18n.global.t('online'), key: 'online' },
|
||||
{ key: 'data-table-group', width: 0 },
|
||||
]
|
||||
const groupBy = [
|
||||
{
|
||||
key: 'group'
|
||||
}
|
||||
]
|
||||
|
||||
const modal = ref({
|
||||
visible: false,
|
||||
id: 0,
|
||||
data: "",
|
||||
})
|
||||
|
||||
const delOverlay = ref(new Array<boolean>(clients.value.length).fill(false))
|
||||
|
||||
const showModal = async (id: number) => {
|
||||
modal.value.id = id
|
||||
modal.value.data = id == 0 ? '' : JSON.stringify(clients.value.findLast(o => o.id == id))
|
||||
modal.value.visible = true
|
||||
}
|
||||
const closeModal = () => {
|
||||
@@ -455,12 +319,11 @@ const delClient = async (id: number) => {
|
||||
|
||||
const qrcode = ref({
|
||||
visible: false,
|
||||
index: 0,
|
||||
id: 0,
|
||||
})
|
||||
|
||||
const showQrCode = (id: number) => {
|
||||
const clientIndex = clients.value.findIndex(c => c.id === id)
|
||||
qrcode.value.index = clientIndex
|
||||
qrcode.value.id = id
|
||||
qrcode.value.visible = true
|
||||
}
|
||||
const closeQrCode = () => {
|
||||
@@ -481,13 +344,6 @@ const closeStats = () => {
|
||||
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 = () => {
|
||||
let filteredClients = clients.value.slice()
|
||||
if (filterSettings.value.group != '-') {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<v-row>
|
||||
<v-col>{{ $t('in.port') }}</v-col>
|
||||
<v-col>
|
||||
{{ item.listen_port?? '-' }}
|
||||
{{ item.listen_port>0 ? item.listen_port : '-' }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
|
||||
@@ -51,11 +51,11 @@
|
||||
<v-row>
|
||||
<v-col>{{ $t('pages.clients') }}</v-col>
|
||||
<v-col>
|
||||
<template v-if="inboundWithUsers.includes(item.type)">
|
||||
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="findInboundUsers(item).length > 0">
|
||||
<span v-for="u in findInboundUsers(item)">{{ u }}<br /></span>
|
||||
<template v-if="item.users">
|
||||
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="item.users.length > 0">
|
||||
<span v-for="u in item.users">{{ u }}<br /></span>
|
||||
</v-tooltip>
|
||||
{{ findInboundUsers(item).length }}
|
||||
{{ item.users.length }}
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</v-col>
|
||||
@@ -110,8 +110,7 @@ import InboundVue from '@/layouts/modals/Inbound.vue'
|
||||
import Stats from '@/layouts/modals/Stats.vue'
|
||||
import { Config } from '@/types/config'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Inbound, inboundWithUsers } from '@/types/inbounds'
|
||||
import { Client } from '@/types/clients'
|
||||
import { Inbound } from '@/types/inbounds'
|
||||
import { i18n } from '@/locales'
|
||||
import { push } from 'notivue'
|
||||
|
||||
@@ -135,12 +134,8 @@ const outTags = computed((): string[] => {
|
||||
return appConfig.value.outbounds?.map(i => i.tag)
|
||||
})
|
||||
|
||||
const clients = computed((): Client[] => {
|
||||
return <Client[]> Data().clients
|
||||
})
|
||||
|
||||
const onlines = computed(() => {
|
||||
return Data().onlines.inbound ? inbounds.value.map(i => Data().onlines.inbound.includes(i.tag)) : []
|
||||
return Data().onlines.inbound?? []
|
||||
})
|
||||
|
||||
const modal = ref({
|
||||
@@ -157,7 +152,7 @@ const showModal = (id: number) => {
|
||||
const closeModal = () => {
|
||||
modal.value.visible = false
|
||||
}
|
||||
const saveModal = async (data:Inbound) => {
|
||||
const saveModal = async (data:Inbound, initUsers?: number[]) => {
|
||||
// Check duplicate tag
|
||||
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)) {
|
||||
@@ -168,7 +163,7 @@ const saveModal = async (data:Inbound) => {
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -180,10 +175,6 @@ const delInbound = async (id: number) => {
|
||||
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({
|
||||
visible: false,
|
||||
resource: "inbound",
|
||||
|
||||
@@ -223,7 +223,7 @@ const outboundTags = 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>)
|
||||
|
||||
@@ -89,7 +89,6 @@ import TlsVue from '@/layouts/modals/Tls.vue'
|
||||
import Data from '@/store/modules/data'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Inbound } from '@/types/inbounds'
|
||||
import { Client } from '@/types/clients'
|
||||
import { tls } from '@/types/tls'
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
const clients = computed((): any[] => {
|
||||
return <Client[]>Data().clients
|
||||
})
|
||||
|
||||
const modal = ref({
|
||||
visible: false,
|
||||
id: 0,
|
||||
|
||||
Reference in New Issue
Block a user