all adjustments

This commit is contained in:
Alireza Ahmadi
2025-01-03 23:32:03 +01:00
parent ed48cdca33
commit fe428ed412
62 changed files with 2352 additions and 1975 deletions
+108 -10
View File
@@ -1,9 +1,11 @@
package api
import (
"encoding/json"
"s-ui/logger"
"s-ui/service"
"s-ui/util"
"s-ui/util/common"
"strconv"
"strings"
@@ -17,6 +19,8 @@ type APIHandler struct {
service.ClientService
service.TlsService
service.InboundService
service.OutboundService
service.EndpointService
service.PanelService
service.StatsService
service.ServerService
@@ -43,6 +47,7 @@ func (a *APIHandler) postHandler(c *gin.Context) {
var err error
action := c.Param("postAction")
remoteIP := getRemoteIp(c)
loginUser := GetLoginUser(c)
switch action {
case "login":
@@ -79,13 +84,21 @@ func (a *APIHandler) postHandler(c *gin.Context) {
jsonMsg(c, "", err)
}
case "save":
loginUser := GetLoginUser(c)
data := map[string]string{}
err = c.ShouldBind(&data)
if err == nil {
err = a.ConfigService.SaveChanges(data, loginUser)
obj := c.Request.FormValue("object")
act := c.Request.FormValue("action")
data := c.Request.FormValue("data")
userLinks := c.Request.FormValue("userLinks")
outJsons := c.Request.FormValue("outJsons")
err = a.ConfigService.Save(obj, act, json.RawMessage(data), json.RawMessage(userLinks), json.RawMessage(outJsons), loginUser)
if err != nil {
jsonMsg(c, "save", err)
return
}
jsonMsg(c, "save", err)
err = a.loadPartialData(c, obj, len(outJsons) > 5, len(userLinks) > 5)
if err != nil {
jsonMsg(c, obj, err)
}
return
case "restartApp":
err = a.PanelService.RestartPanel(3)
jsonMsg(c, "restartApp", err)
@@ -97,7 +110,7 @@ func (a *APIHandler) postHandler(c *gin.Context) {
result, _, err := util.GetOutbound(link, 0)
jsonObj(c, result, err)
default:
jsonMsg(c, "API call", nil)
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
}
@@ -119,6 +132,12 @@ func (a *APIHandler) getHandler(c *gin.Context) {
return
}
jsonObj(c, data, nil)
case "inbounds", "outbounds", "endpoints", "tls", "clients", "config":
err := a.loadPartialData(c, action, false, false)
if err != nil {
jsonMsg(c, action, err)
}
return
case "users":
users, err := a.UserService.GetUsers()
if err != nil {
@@ -170,7 +189,7 @@ func (a *APIHandler) getHandler(c *gin.Context) {
keypair := a.ServerService.GenKeypair(kType, options)
jsonObj(c, keypair, nil)
default:
jsonMsg(c, "API call", nil)
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
}
@@ -195,7 +214,7 @@ func (a *APIHandler) loadData(c *gin.Context) (interface{}, error) {
return "", err
}
if isUpdated {
config, err := a.ConfigService.GetConfig()
config, err := a.SettingService.GetConfig()
if err != nil {
return "", err
}
@@ -211,14 +230,24 @@ func (a *APIHandler) loadData(c *gin.Context) (interface{}, error) {
if err != nil {
return "", err
}
outbounds, err := a.OutboundService.GetAll()
if err != nil {
return "", err
}
endpoints, err := a.EndpointService.GetAll()
if err != nil {
return "", err
}
subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0])
if err != nil {
return "", err
}
data["config"] = *config
data["config"] = json.RawMessage(config)
data["clients"] = clients
data["tls"] = tlsConfigs
data["inbounds"] = inbounds
data["outbounds"] = outbounds
data["endpoints"] = endpoints
data["subURI"] = subURI
data["onlines"] = onlines
} else {
@@ -227,3 +256,72 @@ func (a *APIHandler) loadData(c *gin.Context) (interface{}, error) {
return data, nil
}
func (a *APIHandler) loadPartialData(c *gin.Context, obj string, plusInbounds bool, plusClients bool) error {
data := make(map[string]interface{}, 0)
switch obj {
case "inbounds":
id := c.Query("id")
inbounds, err := a.InboundService.Get(id)
if err != nil {
return err
}
data[obj] = inbounds
case "outbounds":
outbounds, err := a.OutboundService.GetAll()
if err != nil {
return err
}
data[obj] = outbounds
case "endpoints":
endpoints, err := a.EndpointService.GetAll()
if err != nil {
return err
}
data[obj] = endpoints
case "tls":
tlsConfigs, err := a.TlsService.GetAll()
if err != nil {
return err
}
data[obj] = tlsConfigs
case "clients":
clients, err := a.ClientService.GetAll()
if err != nil {
return err
}
data[obj] = clients
case "config":
config, err := a.SettingService.GetConfig()
if err != nil {
return err
}
data[obj] = json.RawMessage(config)
}
if plusInbounds {
inbounds, err := a.InboundService.GetAll()
if err != nil {
return err
}
data["inbounds"] = inbounds
}
if plusClients {
clients, err := a.ClientService.GetAll()
if err != nil {
return err
}
data["clients"] = clients
}
jsonObj(c, data, nil)
return nil
}
func (a *APIHandler) postActions(c *gin.Context) (string, json.RawMessage, error) {
var data map[string]json.RawMessage
err := c.ShouldBind(&data)
if err != nil {
return "", nil, err
}
return string(data["action"]), data["data"], nil
}
+1 -1
View File
@@ -46,7 +46,7 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
}
} else {
m.Success = false
m.Msg = msg + err.Error()
m.Msg = msg + ": " + err.Error()
logger.Warning("failed :", err)
}
c.JSON(http.StatusOK, m)
+2 -2
View File
@@ -43,7 +43,7 @@ func (a *APP) Init() error {
a.core = core.NewCore()
a.cronJob = cronjob.NewCronJob(a.core)
a.cronJob = cronjob.NewCronJob()
a.webServer = web.NewServer()
a.subServer = sub.NewServer()
@@ -81,7 +81,7 @@ func (a *APP) Start() error {
return err
}
err = a.configService.StartCore()
err = a.configService.StartCore("")
if err != nil {
logger.Error(err)
}
+15 -2
View File
@@ -60,9 +60,22 @@ 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,
}
err = db.Create(newTls).Error
if err != nil {
@@ -76,10 +89,10 @@ func moveJsonToDb(db *gorm.DB) error {
var inbData InboundData
db.Raw("select id,addrs,out_json from inbound_data where tag = ?", tag).Find(&inbData)
if inbData.Id > 0 {
inbObj["outJson"] = inbData.OutJson
inbObj["out_json"] = inbData.OutJson
inbObj["addrs"] = inbData.Addrs
} else {
inbObj["outJson"] = json.RawMessage("{}")
inbObj["out_json"] = json.RawMessage("{}")
inbObj["addrs"] = json.RawMessage("[]")
}
inbJson, _ := json.Marshal(inbObj)
+12 -16
View File
@@ -109,20 +109,16 @@ func NewBox(options Options) (*Box, error) {
defaultLogWriter = io.Discard
}
var logFactory log.Factory
if factory == nil {
logFactory, err = NewFactory(log.Options{
Context: ctx,
Options: sbCommon.PtrValueOrDefault(options.Log),
DefaultWriter: defaultLogWriter,
BaseTime: createdAt,
})
if err != nil {
return nil, common.NewError("create log factory", err)
}
factory = logFactory
} else {
logFactory = factory
logFactory, err = NewFactory(log.Options{
Context: ctx,
Options: sbCommon.PtrValueOrDefault(options.Log),
DefaultWriter: defaultLogWriter,
BaseTime: createdAt,
})
if err != nil {
return nil, common.NewError("create log factory", err)
}
factory = logFactory
routeOptions := sbCommon.PtrValueOrDefault(options.Route)
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
@@ -158,7 +154,7 @@ func NewBox(options Options) (*Box, error) {
endpointOptions.Options,
)
if err != nil {
return nil, common.NewError("initialize endpoint["+string(i)+"] "+tag, err)
return nil, common.NewError("initialize endpoint["+F.ToString(i)+"] "+tag, err)
}
}
for i, inboundOptions := range options.Inbounds {
@@ -202,7 +198,7 @@ func NewBox(options Options) (*Box, error) {
outboundOptions.Options,
)
if err != nil {
return nil, common.NewError("initialize outbound["+string(i)+"] "+tag, err)
return nil, common.NewError("initialize outbound["+F.ToString(i)+"] "+tag, err)
}
}
outboundManager.Initialize(sbCommon.Must1(
@@ -368,7 +364,7 @@ func (s *Box) Close() error {
close(s.done)
}
err := sbCommon.Close(
s.inbound, s.outbound, s.router, s.connection, s.network,
s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.network,
)
for _, lifecycleService := range s.services {
err1 := lifecycleService.Close()
+2 -2
View File
@@ -58,7 +58,7 @@ func (c *Core) AddOutbound(config []byte) error {
factory.NewLogger("outbound/"+outbound_config.Type+"["+outbound_config.Tag+"]"),
outbound_config.Tag,
outbound_config.Type,
outbound_config)
outbound_config.Options)
if err != nil {
return err
}
@@ -92,7 +92,7 @@ func (c *Core) AddEndpoint(config []byte) error {
factory.NewLogger("endpoint/"+endpoint_config.Type+"["+endpoint_config.Tag+"]"),
endpoint_config.Tag,
endpoint_config.Type,
endpoint_config)
endpoint_config.Options)
if err != nil {
return err
}
+17
View File
@@ -0,0 +1,17 @@
package cronjob
import (
"s-ui/service"
)
type CheckCoreJob struct {
service.ConfigService
}
func NewCheckCoreJob() *CheckCoreJob {
return &CheckCoreJob{}
}
func (s *CheckCoreJob) Run() {
s.ConfigService.StartCore("")
}
+4 -6
View File
@@ -1,7 +1,6 @@
package cronjob
import (
"s-ui/core"
"time"
"github.com/robfig/cron/v3"
@@ -9,13 +8,10 @@ import (
type CronJob struct {
cron *cron.Cron
Core *core.Core
}
func NewCronJob(c *core.Core) *CronJob {
return &CronJob{
Core: c,
}
func NewCronJob() *CronJob {
return &CronJob{}
}
func (c *CronJob) Start(loc *time.Location, trafficAge int) error {
@@ -29,6 +25,8 @@ func (c *CronJob) Start(loc *time.Location, trafficAge int) error {
c.cron.AddJob("@every 1m", NewDepleteJob())
// Start deleting old stats
c.cron.AddJob("@daily", NewDelStatsJob(trafficAge))
// Start core if it is not running
c.cron.AddJob("@every 5s", NewCheckCoreJob())
}()
return nil
+2 -2
View File
@@ -6,7 +6,7 @@ import (
)
type DepleteJob struct {
service.ConfigService
service.ClientService
}
func NewDepleteJob() *DepleteJob {
@@ -14,7 +14,7 @@ func NewDepleteJob() *DepleteJob {
}
func (s *DepleteJob) Run() {
err := s.ConfigService.DepleteClients()
err := s.ClientService.DepleteClients()
if err != nil {
logger.Warning("Disable depleted users failed: ", err)
return
+7 -5
View File
@@ -1,11 +1,13 @@
package model
import "encoding/json"
import (
"encoding/json"
)
type Endpoint struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Type string `json:"type" form:"type"`
Tag string `json:"tag" form:"tag"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
Options json.RawMessage `json:"-" form:"-"`
}
@@ -17,10 +19,10 @@ func (o *Endpoint) UnmarshalJSON(data []byte) error {
}
// Extract fixed fields and store the rest in Options
if val, exists := raw["id"]; exists {
o.Id = val.(uint)
delete(raw, "id")
if val, exists := raw["id"].(float64); exists {
o.Id = uint(val)
}
delete(raw, "id")
o.Type, _ = raw["type"].(string)
delete(raw, "type")
o.Tag = raw["tag"].(string)
+29 -7
View File
@@ -7,14 +7,14 @@ import (
type Inbound struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Type string `json:"type" form:"type"`
Tag string `json:"tag" form:"tag"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
// Foreign key to tls table
TlsId uint `json:"tls_id" form:"tls_id"`
Tls *Tls `json:"tls" form:"tls" gorm:"foreignKey:TlsId;references:Id"`
Addrs json.RawMessage `json:"addrs" form:"addrs"`
OutJson json.RawMessage `json:"outJson" form:"outJson"`
OutJson json.RawMessage `json:"out_json" form:"out_json"`
Options json.RawMessage `json:"-" form:"-"`
}
@@ -26,10 +26,10 @@ func (i *Inbound) UnmarshalJSON(data []byte) error {
}
// Extract fixed fields and store the rest in Options
if val, exists := raw["id"].(uint); exists {
i.Id = val
delete(raw, "id")
if val, exists := raw["id"].(float64); exists {
i.Id = uint(val)
}
delete(raw, "id")
i.Type, _ = raw["type"].(string)
delete(raw, "type")
i.Tag, _ = raw["tag"].(string)
@@ -48,8 +48,8 @@ func (i *Inbound) UnmarshalJSON(data []byte) error {
delete(raw, "addrs")
// OutJson
i.OutJson, _ = json.MarshalIndent(raw["outJson"], "", " ")
delete(raw, "outJson")
i.OutJson, _ = json.MarshalIndent(raw["out_json"], "", " ")
delete(raw, "out_json")
// Remaining fields
i.Options, err = json.MarshalIndent(raw, "", " ")
@@ -79,3 +79,25 @@ func (i Inbound) MarshalJSON() ([]byte, error) {
return json.Marshal(combined)
}
func (i Inbound) MarshalFull() (*map[string]interface{}, error) {
combined := make(map[string]interface{})
combined["id"] = i.Id
combined["type"] = i.Type
combined["tag"] = i.Tag
combined["tls_id"] = i.TlsId
combined["addrs"] = i.Addrs
combined["out_json"] = i.OutJson
if i.Options != nil {
var restFields map[string]json.RawMessage
if err := json.Unmarshal(i.Options, &restFields); err != nil {
return nil, err
}
for k, v := range restFields {
combined[k] = v
}
}
return &combined, nil
}
+4 -4
View File
@@ -5,7 +5,7 @@ import "encoding/json"
type Outbound struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Type string `json:"type" form:"type"`
Tag string `json:"tag" form:"tag"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
Options json.RawMessage `json:"-" form:"-"`
}
@@ -17,10 +17,10 @@ func (o *Outbound) UnmarshalJSON(data []byte) error {
}
// Extract fixed fields and store the rest in Options
if val, exists := raw["id"]; exists {
o.Id = val.(uint)
delete(raw, "id")
if val, exists := raw["id"].(float64); exists {
o.Id = uint(val)
}
delete(raw, "id")
o.Type, _ = raw["type"].(string)
delete(raw, "type")
o.Tag = raw["tag"].(string)
+8 -8
View File
@@ -7,8 +7,8 @@ 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.8
github.com/sagernet/sing-box v1.11.0-beta.11
github.com/sagernet/sing v0.6.0-beta.9
github.com/sagernet/sing-box v1.11.0-beta.19
github.com/sagernet/sing-dns v0.4.0-beta.1
github.com/shirou/gopsutil/v3 v3.24.5
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
@@ -88,11 +88,11 @@ 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-alpha.4 // indirect
github.com/sagernet/sing-quic v0.4.0-beta.2 // 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.6 // indirect
github.com/sagernet/sing-tun v0.6.0-beta.7.0.20241229131914-aa9d9c62966f // indirect
github.com/sagernet/sing-vmess v0.2.0-beta.1 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/sagernet/utls v1.6.7 // indirect
@@ -110,13 +110,13 @@ require (
go.uber.org/zap v1.27.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.11.0 // indirect
golang.org/x/crypto v0.29.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.20.0 // indirect
golang.org/x/net v0.31.0 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/sys v0.27.0 // indirect
golang.org/x/text v0.20.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.24.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
+21
View File
@@ -174,16 +174,24 @@ github.com/sagernet/sing v0.6.0-beta.5 h1:RD2j8WmJsvAbbBkAlJWaiYmnd+v/JohBiweoew
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-dns v0.4.0-beta.1 h1:W1XkdhigwxDOMgMDVB+9kdomCpb7ExsZfB4acPcTZFY=
github.com/sagernet/sing-dns v0.4.0-beta.1/go.mod h1:8wuFcoFkWM4vJuQyg8e97LyvDwe0/Vl7G839WLcKDs8=
github.com/sagernet/sing-mux v0.3.0-alpha.1 h1:IgNX5bJBpL41gGbp05pdDOvh/b5eUQ6cv9240+Ngipg=
github.com/sagernet/sing-mux v0.3.0-alpha.1/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-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=
@@ -194,6 +202,10 @@ github.com/sagernet/sing-tun v0.6.0-beta.2 h1:GK7r2jWKm7RhlJGTq4QadgFcebQia1c3BO
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/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
@@ -254,6 +266,8 @@ 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=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
@@ -264,6 +278,8 @@ 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=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -274,13 +290,18 @@ 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/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=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+1 -1
View File
@@ -26,7 +26,7 @@ func InitLogger(level logging.Level) {
backend, err = logging.NewSyslogBackend("")
if err != nil {
println("Unable to use syslog: " + err.Error())
fmt.Println("Unable to use syslog: " + err.Error())
backend = logging.NewLogBackend(os.Stderr, "", 0)
}
if config.IsSystemd() && err != nil {
+109 -23
View File
@@ -11,6 +11,7 @@ import (
)
type ClientService struct {
InboundService
}
func (s *ClientService) GetAll() ([]model.Client, error) {
@@ -23,48 +24,117 @@ func (s *ClientService) GetAll() ([]model.Client, error) {
return clients, nil
}
func (s *ClientService) Save(tx *gorm.DB, changes []model.Changes) error {
func (s *ClientService) Save(tx *gorm.DB, act string, data json.RawMessage) ([]uint, error) {
var err error
for _, change := range changes {
client := model.Client{}
err = json.Unmarshal(change.Obj, &client)
var inboundIds []uint
switch act {
case "new", "edit":
var client model.Client
err = json.Unmarshal(data, &client)
if err != nil {
return nil, err
}
err = json.Unmarshal(client.Inbounds, &inboundIds)
if err != nil {
return nil, err
}
err = tx.Save(&client).Error
if err != nil {
return nil, err
}
case "del":
var id uint
err = json.Unmarshal(data, &id)
if err != nil {
return nil, err
}
var client model.Client
err = tx.Where("id = ?", id).First(&client).Error
if err != nil {
return nil, err
}
err = json.Unmarshal(client.Inbounds, &inboundIds)
if err != nil {
return nil, err
}
err = tx.Where("id = ?", id).Delete(model.Client{}).Error
if err != nil {
return nil, err
}
}
return inboundIds, nil
}
func (s *ClientService) UpdateLinks(tx *gorm.DB, links json.RawMessage) error {
var userLinks []interface{}
err := json.Unmarshal(links, &userLinks)
if err != nil {
return err
}
for _, userLink := range userLinks {
userLinkData, _ := userLink.(map[string]interface{})
userId, _ := userLinkData["id"].(float64)
links, err := json.MarshalIndent(userLinkData["links"], "", " ")
if err != nil {
return err
}
switch change.Action {
case "new":
err = tx.Create(&client).Error
case "del":
err = tx.Where("id = ?", change.Index).Delete(model.Client{}).Error
default:
err = tx.Save(client).Error
if inbounds, ok := userLinkData["inbounds"]; ok {
inbounds, err := json.MarshalIndent(inbounds, "", " ")
if err != nil {
return err
}
err = tx.Model(model.Client{}).Where("id = ?", uint(userId)).Update("inbounds", inbounds).Error
if err != nil {
return err
}
}
err = tx.Model(model.Client{}).Where("id = ?", uint(userId)).Update("links", links).Error
if err != nil {
return err
}
}
return err
return nil
}
func (s *ClientService) DepleteClients() ([]string, []string, error) {
func (s *ClientService) DepleteClients() error {
var err error
var clients []model.Client
var changes []model.Changes
var users []string
var inboundIds []uint
now := time.Now().Unix()
db := database.GetDB()
err = db.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", now).Scan(&clients).Error
tx := db.Begin()
defer func() {
if err == nil {
tx.Commit()
if len(inboundIds) > 0 && corePtr.IsRunning() {
err1 := s.InboundService.RestartInbounds(tx, inboundIds)
if err1 != nil {
logger.Error("unable to restart inbounds: ", err1)
}
}
} else {
tx.Rollback()
}
}()
err = tx.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", now).Scan(&clients).Error
if err != nil {
return nil, nil, err
return err
}
dt := time.Now().Unix()
var users, inbounds []string
for _, client := range clients {
logger.Debug("Client ", client.Name, " is going to be disabled")
users = append(users, client.Name)
var userInbounds []string
var userInbounds []uint
json.Unmarshal(client.Inbounds, &userInbounds)
inbounds = append(inbounds, userInbounds...)
inboundIds = s.uniqueAppendInboundIds(inboundIds, userInbounds)
changes = append(changes, model.Changes{
DateTime: dt,
Actor: "DepleteJob",
@@ -76,16 +146,32 @@ func (s *ClientService) DepleteClients() ([]string, []string, error) {
// Save changes
if len(changes) > 0 {
err = db.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", now).Update("enable", false).Error
err = tx.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", now).Update("enable", false).Error
if err != nil {
return nil, nil, err
return err
}
err = db.Model(model.Changes{}).Create(&changes).Error
err = tx.Model(model.Changes{}).Create(&changes).Error
if err != nil {
return nil, nil, err
return err
}
LastUpdate = dt
}
return users, inbounds, nil
return nil
}
// avoid duplicate inboundIds
func (s *ClientService) uniqueAppendInboundIds(a []uint, b []uint) []uint {
m := make(map[uint]bool)
for _, v := range a {
m[v] = true
}
for _, v := range b {
m[v] = true
}
var res []uint
for k := range m {
res = append(res, k)
}
return res
}
+72 -297
View File
@@ -2,12 +2,12 @@ package service
import (
"encoding/json"
"os"
"s-ui/config"
"s-ui/core"
"s-ui/database"
"s-ui/database/model"
"s-ui/logger"
"s-ui/util/common"
"strconv"
"time"
)
@@ -48,10 +48,13 @@ func (s *ConfigService) InitConfig() error {
return nil
}
func (s *ConfigService) GetConfig() (*SingBoxConfig, error) {
data, err := s.SettingService.GetConfig()
if err != nil {
return nil, err
func (s *ConfigService) GetConfig(data string) (*SingBoxConfig, error) {
var err error
if len(data) == 0 {
data, err = s.SettingService.GetConfig()
if err != nil {
return nil, err
}
}
singboxConfig := SingBoxConfig{}
err = json.Unmarshal([]byte(data), &singboxConfig)
@@ -74,8 +77,11 @@ func (s *ConfigService) GetConfig() (*SingBoxConfig, error) {
return &singboxConfig, nil
}
func (s *ConfigService) StartCore() error {
singboxConfig, err := s.GetConfig()
func (s *ConfigService) StartCore(defaultConfig string) error {
if corePtr.IsRunning() {
return nil
}
singboxConfig, err := s.GetConfig(defaultConfig)
if err != nil {
return err
}
@@ -93,11 +99,19 @@ func (s *ConfigService) StartCore() error {
}
func (s *ConfigService) RestartCore() error {
err := s.StartCore()
err := s.StopCore()
if err != nil {
return err
}
return s.StartCore()
return s.StartCore("")
}
func (s *ConfigService) restartCoreWithConfig(config json.RawMessage) error {
err := s.StopCore()
if err != nil {
return err
}
return s.StartCore(string(config))
}
func (s *ConfigService) StopCore() error {
@@ -109,220 +123,80 @@ func (s *ConfigService) StopCore() error {
return nil
}
func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string) error {
func (s *ConfigService) Save(obj string, act string, data json.RawMessage, userLinks json.RawMessage, outJsons json.RawMessage, loginUser string) error {
var err error
var clientChanges, tlsChanges, inChanges, settingChanges, configChanges []model.Changes
if _, ok := changes["clients"]; ok {
err = json.Unmarshal([]byte(changes["clients"]), &clientChanges)
if err != nil {
return err
}
}
if _, ok := changes["tls"]; ok {
err = json.Unmarshal([]byte(changes["tls"]), &tlsChanges)
if err != nil {
return err
}
}
if _, ok := changes["inData"]; ok {
err = json.Unmarshal([]byte(changes["inData"]), &inChanges)
if err != nil {
return err
}
}
if _, ok := changes["settings"]; ok {
err = json.Unmarshal([]byte(changes["settings"]), &settingChanges)
if err != nil {
return err
}
}
if _, ok := changes["config"]; ok {
err = json.Unmarshal([]byte(changes["config"]), &configChanges)
if err != nil {
return err
}
}
var inboundIds []uint
db := database.GetDB()
tx := db.Begin()
defer func() {
if err == nil {
tx.Commit()
if len(inboundIds) > 0 && corePtr.IsRunning() {
err1 := s.InboundService.RestartInbounds(tx, inboundIds)
if err1 != nil {
logger.Error("unable to restart inbounds: ", err1)
}
}
// Try to start core if it is not running
if !corePtr.IsRunning() {
s.StartCore("")
}
} else {
tx.Rollback()
}
}()
if len(clientChanges) > 0 {
err = s.ClientService.Save(tx, clientChanges)
switch obj {
case "clients":
inboundIds, err = s.ClientService.Save(tx, act, data)
case "tls":
inboundIds, err = s.TlsService.Save(tx, act, data)
case "inbounds":
err = s.InboundService.Save(tx, act, data)
case "outbounds":
err = s.OutboundService.Save(tx, act, data)
case "endpoints":
err = s.EndpointService.Save(tx, act, data)
case "config":
err = s.SettingService.SaveConfig(tx, data)
if err != nil {
return err
}
err = s.restartCoreWithConfig(data)
default:
return common.NewError("unknown object: ", obj)
}
if len(tlsChanges) > 0 {
err = s.TlsService.Save(tx, tlsChanges)
if err != nil {
return err
}
if err != nil {
return err
}
// if len(inChanges) > 0 {
// err = s.InDataService.Save(tx, inChanges)
// if err != nil {
// return err
// }
// }
if len(settingChanges) > 0 {
err = s.SettingService.Save(tx, settingChanges)
if err != nil {
return err
}
}
needRestart := false
if len(configChanges) > 0 {
singboxConfig, err := s.GetConfig()
if err != nil {
return err
}
newConfig := *singboxConfig
for _, change := range configChanges {
rawObject := change.Obj
switch change.Key {
case "all":
err = json.Unmarshal(rawObject, &newConfig)
if err != nil {
return err
}
needRestart = true
case "log":
newConfig.Log = rawObject
needRestart = true
case "dns":
newConfig.Dns = rawObject
needRestart = true
case "ntp":
newConfig.Ntp = rawObject
needRestart = true
case "route":
newConfig.Route = rawObject
needRestart = true
case "experimental":
newConfig.Experimental = rawObject
needRestart = true
case "inbounds":
if change.Action == "edit" {
var object map[string]interface{}
err = json.Unmarshal(newConfig.Inbounds[change.Index], &object)
if err != nil {
return err
}
if tag, ok := object["tag"].(string); ok {
err = corePtr.RemoveInbound(tag)
if err == nil {
err = corePtr.AddInbound(rawObject)
if err != nil {
needRestart = true
}
} else {
needRestart = true
}
} else {
needRestart = true
}
newConfig.Inbounds[change.Index] = rawObject
} else if change.Action == "del" {
var object map[string]interface{}
err = json.Unmarshal(newConfig.Inbounds[change.Index], &object)
if err != nil {
return err
}
if tag, ok := object["tag"].(string); ok {
err = corePtr.RemoveInbound(tag)
if err != nil {
needRestart = true
}
} else {
needRestart = true
}
newConfig.Inbounds = append(newConfig.Inbounds[:change.Index], newConfig.Inbounds[change.Index+1:]...)
} else {
newConfig.Inbounds = append(newConfig.Inbounds, rawObject)
err = corePtr.AddInbound(rawObject)
if err != nil {
logger.Debug(err)
needRestart = true
}
}
case "outbounds":
if change.Action == "edit" {
var object map[string]interface{}
err = json.Unmarshal(newConfig.Outbounds[change.Index], &object)
if err != nil {
return err
}
if tag, ok := object["tag"].(string); ok {
err = corePtr.RemoveOutbound(tag)
if err == nil {
err = corePtr.AddOutbound(rawObject)
if err != nil {
needRestart = true
}
} else {
needRestart = true
}
} else {
needRestart = true
}
newConfig.Outbounds[change.Index] = rawObject
} else if change.Action == "del" {
var object map[string]interface{}
err = json.Unmarshal(newConfig.Outbounds[change.Index], &object)
if err != nil {
return err
}
if tag, ok := object["tag"].(string); ok {
err = corePtr.RemoveOutbound(tag)
if err != nil {
needRestart = true
}
} else {
needRestart = true
}
newConfig.Outbounds = append(newConfig.Outbounds[:change.Index], newConfig.Outbounds[change.Index+1:]...)
} else {
err = corePtr.AddOutbound(rawObject)
if err != nil {
logger.Debug(err)
needRestart = true
}
newConfig.Outbounds = append(newConfig.Outbounds, rawObject)
}
}
}
err = s.Save(&newConfig, needRestart)
if len(userLinks) > 0 {
err = s.ClientService.UpdateLinks(tx, userLinks)
if err != nil {
return err
}
}
if len(outJsons) > 0 {
err = s.InboundService.UpdateOutJsons(tx, outJsons)
if err != nil {
return err
}
}
// Log changes
dt := time.Now().Unix()
allChanges := append(clientChanges, settingChanges...)
allChanges = append(allChanges, configChanges...)
allChanges = append(allChanges, tlsChanges...)
allChanges = append(allChanges, inChanges...)
if len(allChanges) > 0 {
for index := range allChanges {
allChanges[index].DateTime = dt
allChanges[index].Actor = loginUser
}
err = tx.Model(model.Changes{}).Create(&allChanges).Error
if err != nil {
return err
}
err = tx.Create(&model.Changes{
DateTime: dt,
Actor: loginUser,
Key: obj,
Action: act,
Obj: data,
}).Error
if err != nil {
return err
}
LastUpdate = dt
LastUpdate = time.Now().Unix()
return nil
}
@@ -345,105 +219,6 @@ func (s *ConfigService) CheckChanges(lu string) (bool, error) {
}
}
func (s *ConfigService) Save(singboxConfig *SingBoxConfig, needRestart bool) error {
configPath := config.GetBinFolderPath()
_, err := os.Stat(configPath + "/config.json")
if os.IsNotExist(err) {
err = os.MkdirAll(configPath, 01764)
if err != nil {
return err
}
} else if err != nil {
return err
}
data, err := json.MarshalIndent(singboxConfig, "", " ")
if err != nil {
return err
}
err = os.WriteFile(configPath+"/config.json", data, 0764)
if err != nil {
return err
}
if needRestart {
err = s.RestartCore()
if err != nil {
return err
}
}
// s.Controller.Restart()
return nil
}
func (s *ConfigService) DepleteClients() error {
users, inboundIds, err := s.ClientService.DepleteClients()
if err != nil || len(users) == 0 || len(inboundIds) == 0 {
return err
}
// inbounds, err := s.InboundService.FromIds(inboundIds)
// if err != nil {
// return err
// }
// for inbound_index, inbound := range inbounds {
// var inboundJson map[string]interface{}
// json.Unmarshal(inbound.Options, &inboundJson)
// inbound_users, ok := inboundJson["users"].([]interface{})
// if ok {
// var updatedUsers []interface{}
// for _, user := range inbound_users {
// userMap, ok := user.(map[string]interface{})
// if ok {
// name, exists := userMap["name"].(string)
// if exists && s.contains(users, name) {
// // Skip the user exists
// continue
// }
// username, exists := userMap["username"].(string)
// if exists && s.contains(users, username) {
// // Skip the username exists
// continue
// }
// }
// updatedUsers = append(updatedUsers, user)
// }
// // Exception for Naive and ShadowTLSv3
// if len(updatedUsers) == 0 {
// if inboundJson["type"].(string) == "naive" ||
// (inboundJson["type"].(string) == "shadowtls" &&
// inboundJson["version"].(float64) == 3) {
// updatedUsers = append(updatedUsers, make(map[string]interface{}))
// }
// }
// inboundJson["users"] = updatedUsers
// }
// modifiedInbound, err := json.MarshalIndent(inboundJson, "", " ")
// if err != nil {
// return err
// }
// inbounds[inbound_index] = modifiedInbound
// }
// err = s.Save(singboxConfig, true)
// if err != nil {
// return err
// }
return nil
}
func (s *ConfigService) contains(slice []string, item string) bool {
for _, str := range slice {
if str == item {
return true
}
}
return false
}
func (s *ConfigService) GetChanges(actor string, chngKey string, count string) []model.Changes {
c, _ := strconv.Atoi(count)
whereString := "`id`>0"
+72 -11
View File
@@ -2,6 +2,7 @@ package service
import (
"encoding/json"
"os"
"s-ui/database"
"s-ui/database/model"
@@ -10,24 +11,32 @@ import (
type EndpointService struct{}
func (o *EndpointService) GetAll() ([]*model.Endpoint, error) {
func (o *EndpointService) GetAll() (*[]map[string]interface{}, error) {
db := database.GetDB()
endpoints := []*model.Endpoint{}
err := db.Model(model.Endpoint{}).Scan(&endpoints).Error
if err != nil {
return nil, err
}
return endpoints, nil
}
func (o *EndpointService) Get(id uint) (*model.Endpoint, error) {
db := database.GetDB()
endpoint := &model.Endpoint{}
err := db.First(endpoint, id).Error
if err != nil {
return nil, err
var data []map[string]interface{}
for _, endpoint := range endpoints {
epData := map[string]interface{}{
"id": endpoint.Id,
"type": endpoint.Type,
"tag": endpoint.Tag,
}
if endpoint.Options != nil {
var restFields map[string]json.RawMessage
if err := json.Unmarshal(endpoint.Options, &restFields); err != nil {
return nil, err
}
for k, v := range restFields {
epData[k] = v
}
}
data = append(data, epData)
}
return endpoint, nil
return &data, nil
}
func (o *EndpointService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
@@ -46,3 +55,55 @@ func (o *EndpointService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
}
return endpointsJson, nil
}
func (s *EndpointService) Save(tx *gorm.DB, action string, data json.RawMessage) error {
var err error
switch action {
case "new", "edit":
var endpoint model.Endpoint
err = endpoint.UnmarshalJSON(data)
if err != nil {
return err
}
if corePtr.IsRunning() {
configData, err := endpoint.MarshalJSON()
if err != nil {
return err
}
if action == "edit" {
err = corePtr.RemoveEndpoint(endpoint.Tag)
if err != nil && err != os.ErrInvalid {
return err
}
}
err = corePtr.AddEndpoint(configData)
if err != nil {
return err
}
}
err = tx.Save(&endpoint).Error
if err != nil {
return err
}
case "del":
var tag string
err = json.Unmarshal(data, &tag)
if err != nil {
return err
}
if corePtr.IsRunning() {
err = corePtr.RemoveEndpoint(tag)
if err != nil && err != os.ErrInvalid {
return err
}
}
err = tx.Where("tag = ?", tag).Delete(model.Endpoint{}).Error
if err != nil {
return err
}
}
return nil
}
+174 -12
View File
@@ -2,22 +2,67 @@ package service
import (
"encoding/json"
"os"
"s-ui/database"
"s-ui/database/model"
"strings"
"gorm.io/gorm"
)
type InboundService struct{}
func (s *InboundService) GetAll() (*[]map[string]interface{}, error) {
func (s *InboundService) Get(ids string) (*[]map[string]interface{}, error) {
if ids == "" {
return s.GetAll()
}
return s.getById(ids)
}
func (s *InboundService) getById(ids string) (*[]map[string]interface{}, error) {
var inbound []model.Inbound
var result []map[string]interface{}
db := database.GetDB()
inbounds := []map[string]interface{}{}
err := db.Model(model.Inbound{}).Select("id, tag, type, address, port, tls_id , count(users) as ucount").Scan(&inbounds).Error
err := db.Model(model.Inbound{}).Where("id in ?", strings.Split(ids, ",")).Scan(&inbound).Error
if err != nil {
return nil, err
}
return &inbounds, nil
for _, inb := range inbound {
inbData, err := inb.MarshalFull()
if err != nil {
return nil, err
}
result = append(result, *inbData)
}
return &result, nil
}
func (s *InboundService) GetAll() (*[]map[string]interface{}, error) {
db := database.GetDB()
inbounds := []model.Inbound{}
err := db.Model(model.Inbound{}).Scan(&inbounds).Error
if err != nil {
return nil, err
}
var data []map[string]interface{}
for _, inbound := range inbounds {
inbData := map[string]interface{}{
"id": inbound.Id,
"type": inbound.Type,
"tag": inbound.Tag,
"tls_id": inbound.TlsId,
}
if inbound.Options != nil {
var restFields map[string]json.RawMessage
if err := json.Unmarshal(inbound.Options, &restFields); err != nil {
return nil, err
}
inbData["listen"] = restFields["listen"]
inbData["listen_port"] = restFields["listen_port"]
}
data = append(data, inbData)
}
return &data, nil
}
func (s *InboundService) FromIds(ids []uint) ([]*model.Inbound, error) {
@@ -30,8 +75,91 @@ func (s *InboundService) FromIds(ids []uint) ([]*model.Inbound, error) {
return inbounds, nil
}
func (s *InboundService) Save(db *gorm.DB, inbounds []*model.Inbound) error {
return db.Save(inbounds).Error
func (s *InboundService) Save(tx *gorm.DB, act string, data json.RawMessage) error {
var err error
switch act {
case "new", "edit":
var inbound model.Inbound
err = inbound.UnmarshalJSON(data)
if err != nil {
return err
}
if corePtr.IsRunning() {
if act == "edit" {
err = corePtr.RemoveInbound(inbound.Tag)
if err != nil && err != os.ErrInvalid {
return err
}
}
if inbound.TlsId > 0 {
err = tx.Model(model.Tls{}).Where("id = ?", inbound.TlsId).Find(&inbound.Tls).Error
if err != nil {
return err
}
}
inboundConfig, err := inbound.MarshalJSON()
if err != nil {
return err
}
inboundConfig, err = s.addUsers(tx, inboundConfig, inbound.Id, inbound.Type)
if err != nil {
return err
}
err = corePtr.AddInbound(inboundConfig)
if err != nil {
return err
}
}
err = tx.Save(&inbound).Error
if err != nil {
return err
}
case "del":
var tag string
err = json.Unmarshal(data, &tag)
if err != nil {
return err
}
if corePtr.IsRunning() {
err = corePtr.RemoveInbound(tag)
if err != nil && err != os.ErrInvalid {
return err
}
}
err = tx.Where("tag = ?", tag).Delete(model.Inbound{}).Error
if err != nil {
return err
}
}
return nil
}
func (s *InboundService) UpdateOutJsons(tx *gorm.DB, data json.RawMessage) error {
var outJsons []interface{}
err := json.Unmarshal(data, &outJsons)
if err != nil {
return err
}
for _, outJson := range outJsons {
outJsonData := outJson.(map[string]interface{})
tag := outJsonData["tag"].(string)
outJson, err := json.MarshalIndent(outJsonData["out_json"], "", " ")
if err != nil {
return err
}
err = tx.Model(model.Inbound{}).Where("tag = ?", tag).Update("out_json", outJson).Error
if err != nil {
return err
}
}
return nil
}
func (s *InboundService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
@@ -46,12 +174,9 @@ func (s *InboundService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
if err != nil {
return nil, err
}
switch inbound.Type {
case "mixed", "socks", "http", "shadowsocks", "vmess", "trojan", "naive", "hysteria", "shadowtls", "tuic", "hysteria2", "vless":
inboundJson, err = s.addUsers(db, inboundJson, inbound.Id, inbound.Type)
if err != nil {
return nil, err
}
inboundJson, err = s.addUsers(db, inboundJson, inbound.Id, inbound.Type)
if err != nil {
return nil, err
}
inboundsJson = append(inboundsJson, inboundJson)
}
@@ -59,11 +184,24 @@ func (s *InboundService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
}
func (s *InboundService) addUsers(db *gorm.DB, inboundJson []byte, inboundId uint, inboundType string) ([]byte, error) {
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 == "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, json_each(clients.inbounds) as je
@@ -80,3 +218,27 @@ func (s *InboundService) addUsers(db *gorm.DB, inboundJson []byte, inboundId uin
inbound["users"] = usersJson
return json.Marshal(inbound)
}
func (s *InboundService) RestartInbounds(tx *gorm.DB, ids []uint) error {
var inbounds []*model.Inbound
err := tx.Model(model.Inbound{}).Preload("Tls").Where("id in ?", ids).Find(&inbounds).Error
if err != nil {
return err
}
for _, inbound := range inbounds {
err = corePtr.RemoveInbound(inbound.Tag)
if err != nil && err != os.ErrInvalid {
return err
}
inboundConfig, err := inbound.MarshalJSON()
if err != nil {
return err
}
inboundConfig, err = s.addUsers(tx, inboundConfig, inbound.Id, inbound.Type)
err = corePtr.AddInbound(inboundConfig)
if err != nil {
return err
}
}
return nil
}
+72 -11
View File
@@ -2,6 +2,7 @@ package service
import (
"encoding/json"
"os"
"s-ui/database"
"s-ui/database/model"
@@ -10,24 +11,32 @@ import (
type OutboundService struct{}
func (o *OutboundService) GetAll() ([]*model.Outbound, error) {
func (o *OutboundService) GetAll() (*[]map[string]interface{}, error) {
db := database.GetDB()
outbounds := []*model.Outbound{}
err := db.Model(model.Outbound{}).Scan(&outbounds).Error
if err != nil {
return nil, err
}
return outbounds, nil
}
func (o *OutboundService) Get(id uint) (*model.Outbound, error) {
db := database.GetDB()
outbound := &model.Outbound{}
err := db.First(outbound, id).Error
if err != nil {
return nil, err
var data []map[string]interface{}
for _, outbound := range outbounds {
outData := map[string]interface{}{
"id": outbound.Id,
"type": outbound.Type,
"tag": outbound.Tag,
}
if outbound.Options != nil {
var restFields map[string]json.RawMessage
if err := json.Unmarshal(outbound.Options, &restFields); err != nil {
return nil, err
}
for k, v := range restFields {
outData[k] = v
}
}
data = append(data, outData)
}
return outbound, nil
return &data, nil
}
func (o *OutboundService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
@@ -46,3 +55,55 @@ func (o *OutboundService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
}
return outboundsJson, nil
}
func (s *OutboundService) Save(tx *gorm.DB, action string, data json.RawMessage) error {
var err error
switch action {
case "new", "edit":
var outbound model.Outbound
err = outbound.UnmarshalJSON(data)
if err != nil {
return err
}
if corePtr.IsRunning() {
configData, err := outbound.MarshalJSON()
if err != nil {
return err
}
if action == "edit" {
err = corePtr.RemoveOutbound(outbound.Tag)
if err != nil && err != os.ErrInvalid {
return err
}
}
err = corePtr.AddOutbound(configData)
if err != nil {
return err
}
}
err = tx.Save(&outbound).Error
if err != nil {
return err
}
case "del":
var tag string
err = json.Unmarshal(data, &tag)
if err != nil {
return err
}
if corePtr.IsRunning() {
err = corePtr.RemoveOutbound(tag)
if err != nil && err != os.ErrInvalid {
return err
}
}
err = tx.Where("tag = ?", tag).Delete(model.Outbound{}).Error
if err != nil {
return err
}
}
return nil
}
+8
View File
@@ -343,6 +343,14 @@ func (s *SettingService) SetConfig(config string) error {
return s.setString("config", config)
}
func (s *SettingService) SaveConfig(tx *gorm.DB, config json.RawMessage) error {
configs, err := json.MarshalIndent(config, "", " ")
if err != nil {
return err
}
return tx.Model(model.Setting{}).Where("key = ?", "config").Update("value", string(configs)).Error
}
func (s *SettingService) Save(tx *gorm.DB, changes []model.Changes) error {
var err error
for _, change := range changes {
+6 -2
View File
@@ -74,7 +74,7 @@ func (s *StatsService) SaveStats() error {
return err
}
func (s *StatsService) GetStats(resorce string, tag string, limit int) ([]model.Stats, error) {
func (s *StatsService) GetStats(resource string, tag string, limit int) ([]model.Stats, error) {
var err error
var result []model.Stats
@@ -82,7 +82,11 @@ func (s *StatsService) GetStats(resorce string, tag string, limit int) ([]model.
timeDiff := currentTime - (int64(limit) * 3600)
db := database.GetDB()
err = db.Model(model.Stats{}).Where("resource = ? AND tag = ? AND date_time > ?", resorce, tag, timeDiff).Scan(&result).Error
resources := []string{resource}
if resource == "endpoint" {
resources = []string{"inbound", "outbound"}
}
err = db.Model(model.Stats{}).Where("resource in ? AND tag = ? AND date_time > ?", resources, tag, timeDiff).Scan(&result).Error
if err != nil {
return nil, err
}
+37 -15
View File
@@ -4,11 +4,13 @@ import (
"encoding/json"
"s-ui/database"
"s-ui/database/model"
"s-ui/util/common"
"gorm.io/gorm"
)
type TlsService struct {
InboundService
}
func (s *TlsService) GetAll() ([]model.Tls, error) {
@@ -22,25 +24,45 @@ func (s *TlsService) GetAll() ([]model.Tls, error) {
return tlsConfig, nil
}
func (s *TlsService) Save(tx *gorm.DB, changes []model.Changes) error {
func (s *TlsService) Save(tx *gorm.DB, action string, data json.RawMessage) ([]uint, error) {
var err error
for _, change := range changes {
tlsConfig := model.Tls{}
err = json.Unmarshal(change.Obj, &tlsConfig)
var inboundIds []uint
switch action {
case "new", "edit":
var tls model.Tls
err = json.Unmarshal(data, &tls)
if err != nil {
return err
}
switch change.Action {
case "new":
err = tx.Create(&tlsConfig).Error
case "del":
err = tx.Where("id = ?", change.Index).Delete(model.Tls{}).Error
default:
err = tx.Save(tlsConfig).Error
return nil, err
}
err = tx.Save(&tls).Error
if err != nil {
return err
return nil, err
}
err = tx.Model(model.Inbound{}).Select("id").Where("tls_id = ?", tls.Id).Scan(&inboundIds).Error
if err != nil {
return nil, err
}
return inboundIds, nil
case "del":
var id uint
err = json.Unmarshal(data, &id)
if err != nil {
return nil, err
}
var inboundCount int64
err = tx.Model(model.Inbound{}).Where("tls_id = ?", id).Count(&inboundCount).Error
if err != nil {
return nil, err
}
if inboundCount > 0 {
return nil, common.NewError("tls in use")
}
err = tx.Where("id = ?", id).Delete(model.Tls{}).Error
if err != nil {
return nil, err
}
}
return err
return nil, nil
}
+6 -11
View File
@@ -87,27 +87,22 @@ func (j *JsonService) GetJson(subId string, format string) (*string, error) {
return &resultStr, nil
}
func (j *JsonService) getData(subId string) (*model.Client, *[]model.InboundData, error) {
func (j *JsonService) getData(subId string) (*model.Client, []*model.Inbound, error) {
db := database.GetDB()
client := &model.Client{}
err := db.Model(model.Client{}).Where("enable = true and name = ?", subId).First(client).Error
if err != nil {
return nil, nil, err
}
var inbounds []string
err = json.Unmarshal(client.Inbounds, &inbounds)
var inbounds []*model.Inbound
err = db.Model(model.Inbound{}).Where("tag in ?", client.Inbounds).Find(&inbounds).Error
if err != nil {
return nil, nil, err
}
inDatas := &[]model.InboundData{}
err = db.Model(model.InboundData{}).Where("tag in ?", inbounds).Find(&inDatas).Error
if err != nil {
return nil, nil, err
}
return client, inDatas, nil
return client, inbounds, nil
}
func (j *JsonService) getOutbounds(clientConfig json.RawMessage, inDatas *[]model.InboundData) (*[]map[string]interface{}, *[]string, error) {
func (j *JsonService) getOutbounds(clientConfig json.RawMessage, inbounds []*model.Inbound) (*[]map[string]interface{}, *[]string, error) {
var outbounds []map[string]interface{}
var configs map[string]interface{}
var outTags []string
@@ -116,7 +111,7 @@ func (j *JsonService) getOutbounds(clientConfig json.RawMessage, inDatas *[]mode
if err != nil {
return nil, nil, err
}
for _, inData := range *inDatas {
for _, inData := range inbounds {
if len(inData.OutJson) < 5 {
continue
}
+360 -350
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -14,6 +14,8 @@
:label="$t('in.port')"
hide-details
type="number"
min="1"
max="65535"
required
v-model.number="inbound.listen_port"></v-text-field>
</v-col>
+14 -14
View File
@@ -6,20 +6,20 @@
hide-details
:items="['4','4a','5']"
:label="$t('version')"
v-model="inData.outJson.version">
v-model="inData.out_json.version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="needNetwork">
<Network :data="inData.outJson" />
<Network :data="inData.out_json" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="needUot">
<UoT :data="inData.outJson" />
<UoT :data="inData.out_json" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.HTTP">
<v-text-field
:label="$t('transport.path')"
hide-details
v-model="inData.outJson.path">
v-model="inData.out_json.path">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.VMess || type == inTypes.VLESS">
@@ -36,14 +36,14 @@
hide-details
:label="$t('types.vmess.security')"
:items="vmessSecurities"
v-model="inData.outJson.security">
v-model="inData.out_json.security">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inData.outJson.global_padding" color="primary" :label="$t('types.vmess.globalPadding')" hide-details></v-switch>
<v-switch v-model="inData.out_json.global_padding" color="primary" :label="$t('types.vmess.globalPadding')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inData.outJson.authenticated_length" color="primary" :label="$t('types.vmess.authLen')" hide-details></v-switch>
<v-switch v-model="inData.out_json.authenticated_length" color="primary" :label="$t('types.vmess.authLen')" hide-details></v-switch>
</v-col>
</template>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.Hysteria">
@@ -52,7 +52,7 @@
hide-details
type="number"
min="0"
v-model.number="inData.outJson.recv_window">
v-model.number="inData.out_json.recv_window">
</v-text-field>
</v-col>
<template v-if="type == inTypes.TUIC">
@@ -62,16 +62,16 @@
label="UDP Relay Mode"
:items="['native', 'quic']"
clearable
@click:clear="delete inData.outJson.udp_relay_mode"
v-model="inData.outJson.udp_relay_mode">
@click:clear="delete inData.out_json.udp_relay_mode"
v-model="inData.out_json.udp_relay_mode">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="UDP Over Stream" v-model="inData.outJson.udp_over_stream" hide-details></v-switch>
<v-switch color="primary" label="UDP Over Stream" v-model="inData.out_json.udp_over_stream" hide-details></v-switch>
</v-col>
</template>
</v-row>
<Headers :data="inData.outJson" v-if="type == inTypes.HTTP" />
<Headers :data="inData.out_json" v-if="type == inTypes.HTTP" />
</v-card>
</template>
@@ -114,8 +114,8 @@ export default {
needNetwork():boolean { return this.haveNetwork.includes(this.$props.type) },
needUot():boolean { return this.havUoT.includes(this.$props.type) },
packet_encoding: {
get() { return this.$props.inData.outJson.packet_encoding != undefined ? this.$props.inData.outJson.packet_encoding : 'none'; },
set(v:string) { this.$props.inData.outJson.packet_encoding = v != "none" ? v : undefined }
get() { return this.$props.inData.out_json.packet_encoding != undefined ? this.$props.inData.out_json.packet_encoding : 'none'; },
set(v:string) { this.$props.inData.out_json.packet_encoding = v != "none" ? v : undefined }
},
},
components: { Network, UoT, Headers }
+21 -2
View File
@@ -4,7 +4,7 @@
<v-text-field
:label="$t('out.addr')"
hide-details
v-model="data.server">
v-model="address">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
@@ -13,7 +13,16 @@
type="number"
min="0"
hide-details
v-model="data.server_port">
v-model="port">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="KeepAlive"
type="number"
min="0"
hide-details
v-model="data.persistent_keepalive_interval">
</v-text-field>
</v-col>
</v-row>
@@ -36,6 +45,8 @@
</template>
<script lang="ts">
import { KeepAlive } from 'vue';
export default {
props: ['data'],
data() {
@@ -54,6 +65,14 @@ export default {
}
}
},
address: {
get() { return this.$props.data.address },
set(v:string) { this.$props.data.address = v.length > 0 ? v : undefined }
},
port: {
get() { return this.$props.data.port },
set(v:number) { this.$props.data.port = v > 0 ? v : undefined }
}
}
}
</script>
+54 -45
View File
@@ -2,22 +2,40 @@
<v-card subtitle="Wireguard">
<v-row>
<v-col cols="12" sm="8">
<v-text-field v-model="data.private_key" :label="$t('types.wg.privKey')" hide-details></v-text-field>
<v-text-field
v-model="data.private_key"
:label="$t('types.wg.privKey')"
append-icon="mdi-key-star"
@click:append="newKey()"
hide-details>
</v-text-field>
</v-col>
<v-col cols="12" sm="8">
<v-text-field v-model="data.peer_public_key" :label="$t('types.wg.pubKey')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="8" v-if="data.pre_shared_key != undefined">
<v-text-field v-model="data.pre_shared_key" :label="$t('types.wg.psk')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="8">
<v-text-field v-model="local_ips" :label="$t('types.wg.localIp') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
<v-text-field v-model="address" :label="$t('types.wg.localIp') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="data.reserved != undefined">
<v-text-field v-model="reserved" :label="'Reserved ' + $t('commaSeparated')" hide-details></v-text-field>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('in.port')"
hide-details
type="number"
min=1
v-model.number="data.listen_port">
</v-text-field>
</v-col>
<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-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="data.workers != undefined">
<v-text-field
:label="$t('types.wg.worker')"
@@ -39,24 +57,16 @@
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<Network :data="data" />
<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.interface_name != undefined">
<v-col cols="12" sm="6" md="4" v-if="data.name != undefined">
<v-text-field
:label="$t('types.wg.ifName')"
hide-details
v-model.number="data.interface_name">
v-model="data.name">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.system_interface" color="primary" :label="$t('types.wg.sysIf')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.gso" color="primary" :label="$t('types.wg.gso')" hide-details></v-switch>
</v-col>
</v-row>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
@@ -66,10 +76,7 @@
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionPsk" color="primary" :label="$t('types.wg.psk')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionRsrv" color="primary" label="Reserved" hide-details></v-switch>
<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>
@@ -80,9 +87,6 @@
<v-list-item>
<v-switch v-model="optionInterface" color="primary" :label="$t('types.wg.ifName')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionPeers" color="primary" :label="$t('types.wg.multiPeer')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
@@ -95,7 +99,7 @@
<template v-for="(p, index) in data.peers">
<v-card style="margin-top: 1rem;">
<v-card-subtitle>
{{ $t('types.wg.peer') + ' ' + (index+1) }} <v-icon icon="mdi-delete" @click="data.peers.splice(index,1)" />
{{ $t('types.wg.peer') + ' ' + (index+1) }} <v-icon icon="mdi-delete" @click="data.peers.splice(index,1)" v-if="data.peers.length > 1" />
</v-card-subtitle>
<Peer :data="p" />
</v-card>
@@ -104,9 +108,8 @@
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
import Peer from '@/components/WgPeer.vue'
import { WgPeer } from '@/types/outbounds'
import WgUtil from '@/plugins/wgUtil'
export default {
props: ['data'],
@@ -117,13 +120,19 @@ export default {
},
methods: {
addPeer() {
this.$props.data.peers.push({server: '', port: ''})
this.$props.data.peers.push({
address: '',
port: this.$props.data.listen_port
})
},
newKey() {
this.$props.data.private_key = WgUtil.generateKeypair().privateKey
}
},
computed: {
optionPsk: {
get(): boolean { return this.$props.data.pre_shared_key != undefined },
set(v:boolean) { this.$props.data.pre_shared_key = v ? "" : undefined }
optionUdp: {
get(): boolean { return this.$props.data.udp_timeout != undefined },
set(v:boolean) { this.$props.data.udp_timeout = v ? "5m" : undefined }
},
optionRsrv: {
get(): boolean { return this.$props.data.reserved != undefined },
@@ -138,16 +147,12 @@ export default {
set(v:boolean) { this.$props.data.mtu = v ? 1408 : undefined }
},
optionInterface: {
get(): boolean { return this.$props.data.interface_name != undefined },
set(v:boolean) { this.$props.data.interface_name = v ? "" : undefined }
get(): boolean { return this.$props.data.name != undefined },
set(v:boolean) { this.$props.data.name = v ? "" : undefined }
},
optionPeers: {
get(): boolean { return this.$props.data.peers != undefined },
set(v:boolean) { this.$props.data.peers = v ? <WgPeer[]>[] : undefined }
},
local_ips: {
get() { return this.$props.data.local_address?.join(',') },
set(v:string) { this.$props.data.local_address = v.length > 0 ? v.split(',') : undefined }
address: {
get() { return this.$props.data.address?.join(',') },
set(v:string) { this.$props.data.address = v.length > 0 ? v.split(',') : undefined }
},
reserved: {
get() { return this.$props.data.reserved?.join(',') },
@@ -157,7 +162,11 @@ export default {
}
}
},
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' }
}
},
components: { Network, Peer }
components: { Peer }
}
</script>
+3 -220
View File
@@ -1,242 +1,25 @@
<template>
<v-card :subtitle="$t('objects.tls')">
<v-row>
<v-col cols="12" sm="6" md="4" v-if="tlsOptional">
<v-switch color="primary" :label="$t('tls.enable')" v-model="tlsEnable" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="tls.enabled">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('template')"
:items="tlsItems"
@update:model-value="changeTlsItem($event)"
v-model="tlsId">
v-model="inbound.tls_id">
</v-select>
</v-col>
</v-row>
<template v-if="tls.enabled && tlsId == 0">
<v-row>
<v-col cols="auto">
<v-btn-toggle v-model="usePath"
class="rounded-xl"
density="compact"
variant="outlined"
shaped
mandatory>
<v-btn
@click="tls.key=undefined; tls.certificate=undefined"
>{{ $t('tls.usePath') }}</v-btn>
<v-btn
@click="tls.key_path=undefined; tls.certificate_path=undefined"
>{{ $t('tls.useText') }}</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<v-row v-if="usePath == 0">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.certPath')"
hide-details
v-model="tls.certificate_path">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.keyPath')"
hide-details
v-model="tls.key_path">
</v-text-field>
</v-col>
</v-row>
<v-row v-else>
<v-col cols="12" sm="6">
<v-textarea
:label="$t('tls.cert')"
hide-details
v-model="certText">
</v-textarea>
</v-col>
<v-col cols="12" sm="6">
<v-textarea
:label="$t('tls.key')"
hide-details
v-model="keyText">
</v-textarea>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="tls.server_name != undefined">
<v-text-field
label="SNI"
hide-details
v-model="tls.server_name">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="tls.alpn">
<v-select
hide-details
label="ALPN"
multiple
:items="alpn"
v-model="tls.alpn">
</v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="tls.min_version">
<v-select
hide-details
:label="$t('tls.minVer')"
:items="tlsVersions"
v-model="tls.min_version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="tls.max_version">
<v-select
hide-details
:label="$t('tls.maxVer')"
:items="tlsVersions"
v-model="tls.max_version">
</v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="8" v-if="tls.cipher_suites != undefined">
<v-select
hide-details
:label="$t('tls.cs')"
multiple
:items="cipher_suites"
v-model="tls.cipher_suites">
</v-select>
</v-col>
</v-row>
</template>
<v-card-actions v-if="tls.enabled && tlsId == 0">
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start" v-if="tls.enabled">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionSNI" color="primary" label="SNI" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionALPN" color="primary" label="ALPN" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionMinV" color="primary" :label="$t('tls.minVer')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionMaxV" color="primary" :label="$t('tls.maxVer')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionCS" color="primary" :label="$t('tls.cs')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
import { i18n } from '@/locales'
import { iTls, defaultInTls } from '@/types/inTls'
export default {
props: ['inbound', 'tlsConfigs', 'tls_id'],
data() {
return {
menu: false,
usePath: this.$props.inbound.tls.key == undefined ? 0 : 1,
defaults: defaultInTls,
alpn: [
{ title: "H3", value: 'h3' },
{ title: "H2", value: 'h2' },
{ title: "Http/1.1", value: 'http/1.1' },
],
tlsVersions: [ '1.0', '1.1', '1.2', '1.3' ],
cipher_suites: [
{ title: "RSA-AES128-CBC-SHA", value: "TLS_RSA_WITH_AES_128_CBC_SHA" },
{ title: "RSA-AES256-CBC-SHA", value: "TLS_RSA_WITH_AES_256_CBC_SHA" },
{ title: "RSA-AES128-GCM-SHA256", value: "TLS_RSA_WITH_AES_128_GCM_SHA256" },
{ title: "RSA-AES256-GCM-SHA384", value: "TLS_RSA_WITH_AES_256_GCM_SHA384" },
{ title: "AES128-GCM-SHA256", value: "TLS_AES_128_GCM_SHA256" },
{ title: "AES256-GCM-SHA384", value: "TLS_AES_256_GCM_SHA384" },
{ title: "CHACHA20-POLY1305-SHA256", value: "TLS_CHACHA20_POLY1305_SHA256" },
{ title: "ECDHE-ECDSA-AES128-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" },
{ title: "ECDHE-ECDSA-AES256-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" },
{ title: "ECDHE-RSA-AES128-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" },
{ title: "ECDHE-RSA-AES256-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" },
{ title: "ECDHE-ECDSA-AES128-GCM-SHA256", value: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" },
{ title: "ECDHE-ECDSA-AES256-GCM-SHA384", value: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" },
{ title: "ECDHE-RSA-AES128-GCM-SHA256", value: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" },
{ title: "ECDHE-RSA-AES256-GCM-SHA384", value: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" },
{ title: "ECDHE-ECDSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" },
{ title: "ECDHE-RSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" }
]
}
},
props: ['inbound', 'tlsConfigs'],
computed: {
tls(): iTls {
return <iTls> this.$props.inbound.tls
},
tlsItems(): any[] {
return [ { title: i18n.global.t('none'), value: 0 }, ...this.$props.tlsConfigs?.map((t:any) => { return { title: t.name, value: t.id } } )]
},
tlsId: {
get() { return this.tls_id.value?? 0 },
set(newValue: boolean) { this.$props.tls_id.value = newValue }
},
tlsEnable: {
get() { return this.tls.enabled?? false },
set(newValue: boolean) {
this.$props.inbound.tls = newValue ? { enabled: true } : {}
this.$props.tls_id.value = 0
}
},
tlsOptional(): boolean {
return !['hysteria','hysteria2','tuic','naive'].includes(this.$props.inbound.type)
},
certText: {
get(): string { return this.tls.certificate ? this.tls.certificate.join('\n') : '' },
set(newValue:string) { this.tls.certificate = newValue.split('\n') }
},
keyText: {
get(): string { return this.tls.key ? this.tls.key.join('\n') : '' },
set(newValue:string) { this.tls.key = newValue.split('\n') }
},
optionSNI: {
get(): boolean { return this.tls.server_name != undefined },
set(v:boolean) { this.tls.server_name = v ? '' : undefined }
},
optionALPN: {
get(): boolean { return this.tls.alpn != undefined },
set(v:boolean) { this.tls.alpn = v ? defaultInTls.alpn : undefined }
},
optionMinV: {
get(): boolean { return this.tls.min_version != undefined },
set(v:boolean) { this.tls.min_version = v ? defaultInTls.min_version : undefined }
},
optionMaxV: {
get(): boolean { return this.tls.max_version != undefined },
set(v:boolean) { this.tls.max_version = v ? defaultInTls.max_version : undefined }
},
optionCS: {
get(): boolean { return this.tls.cipher_suites != undefined },
set(v:boolean) { this.tls.cipher_suites = v ? defaultInTls.cipher_suites : undefined }
}
},
methods: {
changeTlsItem(id: number){
if (id>0) {
const tlsConfig = this.$props.tlsConfigs?.findLast((t:any) => t.id == id)
if (tlsConfig) this.$props.inbound.tls = tlsConfig.server
} else {
this.$props.inbound.tls = { enabled: this.tls.enabled }
}
}
}
}
+1 -22
View File
@@ -3,16 +3,13 @@
<v-icon v-if="isMobile" icon="mdi-menu" @click="$emit('toggleDrawer')" />
<span v-else style="width: 24px"></span>
<v-app-bar-title :text="$t(<string>route.name)" class="align-center text-center " />
<v-btn prepend-icon="mdi-content-save" v-if="stateChange" :text="$t('actions.save')" @click="saveChanges"></v-btn>
<v-icon icon="mdi-theme-light-dark" @click="toggleTheme()" style="margin: 0 10px;"></v-icon>
</v-app-bar>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue"
import { ref } from "vue"
import { useTheme } from "vuetify"
import { FindDiff } from "@/plugins/utils"
import Data from "@/store/modules/data"
import { useRoute } from "vue-router";
defineProps(['isMobile'])
@@ -21,27 +18,9 @@ const route = useRoute();
const theme = useTheme()
const darkMode = ref(localStorage.getItem('theme') == "dark")
const store = Data()
const toggleTheme = () => {
darkMode.value = !darkMode.value
theme.global.name.value = darkMode.value ? "dark" : "light"
localStorage.setItem('theme', theme.global.name.value)
}
const saveChanges = () => {
store.pushData()
}
const oldData = computed((): any => {
return {config: store.oldData.config, clients: store.oldData.clients, tls: store.oldData.tlsConfigs, inData: store.oldData.inData}
})
const newData = computed((): any => {
return {config: store.config, clients: store.clients, tls: store.tlsConfigs, inData: store.inData}
})
const stateChange = computed((): any => {
return !FindDiff.deepCompare(newData.value,oldData.value)
})
</script>
+1
View File
@@ -53,6 +53,7 @@ const menu = [
{ title: 'pages.inbounds', icon: 'mdi-cloud-download', path: '/inbounds' },
{ title: 'pages.clients', icon: 'mdi-account-multiple', path: '/clients' },
{ title: 'pages.outbounds', icon: 'mdi-cloud-upload', path: '/outbounds' },
{ title: 'pages.endpoints', icon: 'mdi-cloud-tags', path: '/endpoints' },
{ title: 'pages.rules', icon: 'mdi-routes', path: '/rules' },
{ title: 'pages.tls', icon: 'mdi-certificate', path: '/tls' },
{ title: 'pages.basics', icon: 'mdi-application-cog', path: '/basics' },
+5 -11
View File
@@ -41,7 +41,7 @@
<DatePick :expiry="expDate" @submit="setDate" />
</v-col>
</v-row>
<v-row v-if="index != -1">
<v-row v-if="id > 0">
<v-col cols="12" sm="6" md="4" class="d-flex flex-column">
<div class="d-flex justify-space-between align-center">
<div>
@@ -80,11 +80,6 @@
></v-combobox>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<v-switch v-model="clientStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-col>
</v-row>
</v-window-item>
<v-window-item value="t2">
<v-row v-for="(value, key) in clientConfig" :key="key">
@@ -189,7 +184,7 @@ import DatePick from '@/components/DateTime.vue'
import { HumanReadable } from '@/plugins/utils'
export default {
props: ['visible', 'data', 'index', 'inboundTags', 'groups', 'stats'],
props: ['visible', 'data', 'id', 'inboundTags', 'groups'],
emits: ['close', 'save'],
data() {
return {
@@ -206,7 +201,7 @@ export default {
},
methods: {
updateData() {
if (this.$props.index != -1) {
if (this.$props.id > 0) {
const newData = JSON.parse(this.$props.data)
this.client = createClient(newData)
this.title = "edit"
@@ -217,7 +212,6 @@ export default {
this.title = "add"
this.clientConfig = randomConfigs('client')
}
this.clientStats = this.$props.stats
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')
@@ -243,8 +237,8 @@ export default {
},
computed: {
clientInbounds: {
get() { return this.client.inbounds.length>0 ? this.client.inbounds.filter(i => this.inboundTags.includes(i)) : [] },
set(newValue:string[]) { this.client.inbounds = newValue.length == 0 ? [] : newValue }
get() { return this.client.inbounds.length>0 ? this.client.inbounds : [] },
set(v:any[]) { this.client.inbounds = v.length == 0 ? [] : v.map(i => i.value) }
},
expDate: {
get() { return this.client.expiry},
+1 -8
View File
@@ -59,11 +59,6 @@
></v-combobox>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<v-switch v-model="bulkData.clientStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
@@ -109,7 +104,6 @@ export default {
clientInbounds: [],
expiry: 0,
Volume: 0,
clientStats: false,
},
patterns: [
{ title: i18n.global.t("bulk.random"), value: "random" },
@@ -129,7 +123,6 @@ export default {
clientInbounds: [],
expiry: 0,
Volume: 0,
clientStats: false,
}
},
closeModal() {
@@ -157,7 +150,7 @@ export default {
group: this.bulkData.group
}))
}
this.$emit('save', this.clients, this.bulkData.clientInbounds, this.bulkData.clientStats)
this.$emit('save', this.clients, this.bulkData.clientInbounds)
this.resetData() // reset to default
this.loading = false
},
+153
View File
@@ -0,0 +1,153 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="800">
<v-card class="rounded-lg">
<v-card-title>
{{ $t('actions.' + title) + " " + $t('objects.endpoint') }}
</v-card-title>
<v-divider></v-divider>
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-container style="padding: 0;">
<v-tabs
v-model="tab"
align-tabs="center"
>
<v-tab value="t1">{{ $t('client.basics') }}</v-tab>
<v-tab value="t2">{{ $t('client.external') }}</v-tab>
</v-tabs>
<v-window v-model="tab">
<v-window-item value="t1">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('type')"
:items="Object.keys(epTypes).map((key,index) => ({title: key, value: Object.values(epTypes)[index]}))"
v-model="endpoint.type"
@update:modelValue="changeType">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="endpoint.tag" :label="$t('objects.tag')" hide-details></v-text-field>
</v-col>
</v-row>
<Wireguard v-if="endpoint.type == epTypes.Wireguard" :data="endpoint" />
<Dial :dial="endpoint" :outTags="tags" />
</v-window-item>
<v-window-item value="t2">
<v-row>
<v-col cols="12">
<v-text-field v-model="link" :label="$t('client.external')" hide-details />
</v-col>
<v-col cols="12" align="center">
<v-btn hide-details variant="tonal" :loading="loading" @click="linkConvert">{{ $t('submit') }}</v-btn>
</v-col>
</v-row>
</v-window-item>
</v-window>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue-darken-1"
variant="text"
@click="closeModal"
>
{{ $t('actions.close') }}
</v-btn>
<v-btn
color="blue-darken-1"
variant="text"
:loading="loading"
@click="saveChanges"
>
{{ $t('actions.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
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 HttpUtils from '@/plugins/httputil'
import WgUtil from '@/plugins/wgUtil'
export default {
props: ['visible', 'data', 'id', 'tags'],
emits: ['close', 'save'],
data() {
return {
endpoint: createEndpoint("wireguard",{ "tag": "" }),
title: "add",
tab: "t1",
link: "",
loading: false,
epTypes: EpTypes,
}
},
methods: {
updateData() {
if (this.$props.id > 0) {
const newData = JSON.parse(this.$props.data)
this.endpoint = createEndpoint(newData.type, newData)
this.title = "edit"
}
else {
const port = RandomUtil.randomIntRange(10000, 60000)
const randomIPoctet = RandomUtil.randomIntRange(1, 255)
this.endpoint = createEndpoint("wireguard",{
tag: "wireguard-" + RandomUtil.randomSeq(3),
address: ['10.0.0.'+ randomIPoctet.toString() +'/32','fe80::'+ randomIPoctet.toString(16) +'/128'],
listen_port: port,
private_key: WgUtil.generateKeypair().privateKey,
peers: [{
public_key: WgUtil.generateKeypair().publicKey,
allowed_ips: ['0.0.0.0/0', '::/0']
}]
})
this.title = "add"
}
this.tab = "t1"
},
changeType() {
// Tag change only in add endpoint
const tag = this.$props.id > 0 ? this.endpoint.tag : this.endpoint.type + "-" + RandomUtil.randomSeq(3)
// Use previous data
const prevConfig = { id: this.endpoint.id, tag: tag ,listen: this.endpoint.listen, listen_port: this.endpoint.listen_port }
this.endpoint = createEndpoint(this.endpoint.type, prevConfig)
},
closeModal() {
this.updateData() // reset
this.$emit('close')
},
saveChanges() {
this.loading = true
this.$emit('save', this.endpoint)
this.loading = false
},
async linkConvert() {
if (this.link.length>0){
this.loading = true
const msg = await HttpUtils.post('api/linkConvert', { link: this.link })
this.loading = false
if (msg.success) {
this.endpoint = msg.obj
this.tab = "t1"
this.link = ""
}
}
}
},
watch: {
visible(newValue) {
if (newValue) {
this.updateData()
}
},
},
components: { Dial, Wireguard }
}
</script>
+63 -42
View File
@@ -1,12 +1,18 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="800">
<v-card class="rounded-lg">
<v-dialog transition="dialog-bottom-transition" width="800" @after-enter="updateData">
<v-card class="rounded-lg" :loading="loading">
<v-card-title>
{{ $t('actions.' + title) + " " + $t('objects.inbound') }}
</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-row>
<v-col cols="12" sm="6" md="4">
<v-select
@@ -45,19 +51,18 @@
<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" />
<InTls v-if="Object.hasOwn(inbound,'tls')" :inbound="inbound" :tlsConfigs="tlsConfigs" :tls_id="tls_id" />
<InTls v-if="HasTls.includes(inbound.type)" :inbound="inbound" :tlsConfigs="tlsConfigs" :tls_id="inbound.tls_id" />
<Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="in" :data="inbound" />
<v-switch v-model="inboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-window-item>
<v-window-item value="c">
<OutJsonVue :inData="inData" :type="inbound.type" />
<Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="out" :data="inData.outJson" />
<OutJsonVue :inData="inbound" :type="inbound.type" />
<Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="out" :data="inbound.out_json" />
<v-card>
<v-card-subtitle>{{ $t('in.multiDomain') }}
<v-icon @click="add_addr" icon="mdi-plus"></v-icon>
</v-card-subtitle>
<template v-for="addr,index in inData.addrs">
{{ $t('in.addr') }} #{{ (index+1) }} <v-icon icon="mdi-delete" @click="inData.addrs.splice(index,1)" />
<template v-for="addr,index in inbound.addrs">
{{ $t('in.addr') }} #{{ (index+1) }} <v-icon icon="mdi-delete" @click="inbound.addrs?.splice(index,1)" />
<v-divider></v-divider>
<AddrVue :addr="addr" :hasTls="Object.hasOwn(inbound,'tls')" />
</template>
@@ -79,6 +84,7 @@
color="blue-darken-1"
variant="text"
:loading="loading"
:disabled="!validate"
@click="saveChanges"
>
{{ $t('actions.save') }}
@@ -89,8 +95,7 @@
</template>
<script lang="ts">
import { InTypes, createInbound } from '@/types/inbounds'
import { Addr, InData } from '@/plugins/inData'
import { InTypes, createInbound, Addr } from '@/types/inbounds'
import RandomUtil from '@/plugins/randomUtil'
import Listen from '@/components/Listen.vue'
@@ -109,19 +114,17 @@ import Multiplex from '@/components/Multiplex.vue'
import Transport from '@/components/Transport.vue'
import AddrVue from '@/components/Addr.vue'
import OutJsonVue from '@/components/OutJson.vue'
import Data from '@/store/modules/data'
export default {
props: ['visible', 'data', 'cData', 'index', 'stats', 'inTags', 'outTags', 'tlsConfigs'],
props: ['visible', 'id', 'inTags', 'outTags', 'tlsConfigs'],
emits: ['close', 'save'],
data() {
return {
inbound: createInbound("direct",{ "tag": "" }),
inData: <InData>{},
inbound: createInbound("direct",{ id:0, "tag": "" }),
title: "add",
loading: false,
side: "s",
inTypes: InTypes,
inboundStats: false,
tls_id: { value: 0 },
HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks],
HasInData: [
InTypes.SOCKS,
@@ -136,56 +139,65 @@ export default {
InTypes.TUIC,
InTypes.Hysteria2,
InTypes.Naive,
]
],
HasTls: [
InTypes.HTTP,
InTypes.VMess,
InTypes.Trojan,
InTypes.Naive,
InTypes.Hysteria,
InTypes.TUIC,
InTypes.Hysteria2,
InTypes.VLESS,
],
OnlyTLS: [InTypes.Hysteria, InTypes.Hysteria2, InTypes.TUIC, InTypes.Naive ],
}
},
methods: {
async loadData() {
this.loading = true
const inboundArray = await Data().loadInbounds([this.$props.id])
this.inbound = inboundArray[0]
this.loading = false
},
updateData() {
if (this.$props.index != -1) {
const newData = JSON.parse(this.$props.data)
this.inbound = createInbound(newData.type, newData)
this.tls_id.value = this.$props.tlsConfigs?.findLast((t:any) => t.inbounds?.includes(this.inbound.tag))?.id?? 0
if (this.HasInData.includes(this.inbound.type)){
this.inData = this.$props.cData?.length> 0 ? <InData>JSON.parse(this.$props.cData) : <InData>{id: 0, tag: this.inbound.tag, addrs: [], outJson: {}}
} else {
this.inData = <InData>{id: -1}
}
if (this.$props.id > 0) {
this.loadData()
this.title = "edit"
}
else {
const port = RandomUtil.randomIntRange(10000, 60000)
this.inbound = createInbound("direct",{ tag: "direct-"+port ,listen: "::", listen_port: port })
this.tls_id.value = 0
this.inbound = createInbound("direct",{ id: 0, tag: "direct-"+port ,listen: "::", listen_port: port })
if (this.HasInData.includes(this.inbound.type)){
this.inData = <InData>{id: 0, tag: this.inbound.tag, addrs: [], outJson: {}}
this.inbound.addrs = []
this.inbound.out_json = {}
} else {
this.inData = <InData>{id: -1}
delete this.inbound.addrs
delete this.inbound.out_json
}
this.title = "add"
this.loading = false
}
this.inboundStats = this.$props.stats
this.side = "s"
},
changeType() {
if (!this.inbound.listen_port) this.inbound.listen_port = RandomUtil.randomIntRange(10000, 60000)
// Tag change only in add inbound
const tag = this.$props.index != -1 ? this.inbound.tag : this.inbound.type + "-" + this.inbound.listen_port
const tag = this.$props.id > 0 ? this.inbound.tag : this.inbound.type + "-" + this.inbound.listen_port
// Use previous data
const prevConfig = { tag: tag ,listen: this.inbound.listen?? "::", listen_port: this.inbound.listen_port }
const prevConfig = { id: this.inbound.id, tag: tag ,listen: this.inbound.listen?? "::", listen_port: this.inbound.listen_port }
this.inbound = createInbound(this.inbound.type, this.inbound.type != this.inTypes.Tun ? prevConfig : { tag: tag })
if (this.HasInData.includes(this.inbound.type)){
if (this.inData.id == -1) this.inData.id = 0
this.inData.addrs = []
this.inData.outJson = {}
this.inData.tag = tag
this.inbound.addrs = []
this.inbound.out_json = {}
} else {
this.inData = <InData>{id: -1}
delete this.inbound.addrs
delete this.inbound.out_json
}
this.tls_id.value = 0
this.side = "s"
},
add_addr() {
this.inData.addrs.push(<Addr>{ server: location.hostname, server_port: this.inbound.listen_port })
this.inbound.addrs?.push(<Addr>{ server: location.hostname, server_port: this.inbound.listen_port })
},
closeModal() {
this.updateData() // reset
@@ -193,14 +205,23 @@ export default {
},
saveChanges() {
this.loading = true
this.$emit('save', this.inbound, this.inboundStats, this.tls_id.value, this.inData)
this.$emit('save', this.inbound)
this.loading = false
},
},
computed: {
validate() {
if (this.inbound == undefined) return false
if (this.inbound.tag == "") return false
if (this.inbound.listen_port > 65535 || this.inbound.listen_port < 1) return false
if (this.OnlyTLS.includes(this.inbound.type) && this.inbound.tls_id == 0) return false
return true
},
},
watch: {
visible(newValue) {
if (newValue) {
this.updateData()
this.loading = true
}
},
},
+5 -9
View File
@@ -54,7 +54,6 @@
<Shadowsocks v-if="outbound.type == outTypes.Shadowsocks" direction="out" :data="outbound" />
<Vmess v-if="outbound.type == outTypes.VMess" :data="outbound" />
<Trojan v-if="outbound.type == outTypes.Trojan" :data="outbound" />
<Wireguard v-if="outbound.type == outTypes.Wireguard" :data="outbound" />
<Hysteria v-if="outbound.type == outTypes.Hysteria" direction="out" :data="outbound" />
<ShadowTls v-if="outbound.type == outTypes.ShadowTLS" :data="outbound" />
<Vless v-if="outbound.type == outTypes.VLESS" :data="outbound" />
@@ -69,7 +68,6 @@
<OutTLS v-if="Object.hasOwn(outbound,'tls')" :outbound="outbound" />
<Multiplex v-if="Object.hasOwn(outbound,'multiplex')" direction="out" :data="outbound" />
<Dial v-if="!NoDial.includes(outbound.type)" :dial="outbound" :outTags="tags" />
<v-switch v-model="outboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-window-item>
<v-window-item value="t2">
<v-row>
@@ -131,7 +129,7 @@ import Selector from '@/components/protocols/Selector.vue'
import UrlTest from '@/components/protocols/UrlTest.vue'
import HttpUtils from '@/plugins/httputil'
export default {
props: ['visible', 'data', 'id', 'stats', 'tags'],
props: ['visible', 'data', 'id', 'tags'],
emits: ['close', 'save'],
data() {
return {
@@ -141,14 +139,13 @@ export default {
link: "",
loading: false,
outTypes: OutTypes,
outboundStats: false,
NoDial: [OutTypes.Block, OutTypes.DNS, OutTypes.Selector, OutTypes.URLTest],
NoServer: [OutTypes.Direct, OutTypes.Block, OutTypes.DNS, OutTypes.Selector, OutTypes.URLTest, OutTypes.Tor],
}
},
methods: {
updateData() {
if (this.$props.id != -1) {
if (this.$props.id > 0) {
const newData = JSON.parse(this.$props.data)
this.outbound = createOutbound(newData.type, newData)
this.title = "edit"
@@ -158,13 +155,12 @@ export default {
this.title = "add"
}
this.tab = "t1"
this.outboundStats = this.$props.stats
},
changeType() {
// Tag change only in add outbound
const tag = this.$props.id != -1 ? this.outbound.tag : this.outbound.type + "-" + RandomUtil.randomSeq(3)
const tag = this.$props.id > 0 ? this.outbound.tag : this.outbound.type + "-" + RandomUtil.randomSeq(3)
// Use previous data
const prevConfig = { tag: tag ,listen: this.outbound.listen, listen_port: this.outbound.listen_port }
const prevConfig = { id: this.outbound.id, tag: tag ,listen: this.outbound.listen, listen_port: this.outbound.listen_port }
this.outbound = createOutbound(this.outbound.type, prevConfig)
},
closeModal() {
@@ -173,7 +169,7 @@ export default {
},
saveChanges() {
this.loading = true
this.$emit('save', this.outbound, this.outboundStats)
this.$emit('save', this.outbound)
this.loading = false
},
async linkConvert() {
+10 -8
View File
@@ -298,11 +298,11 @@ import { push } from 'notivue'
import { i18n } from '@/locales'
import RandomUtil from '@/plugins/randomUtil'
export default {
props: ['visible', 'data', 'index'],
props: ['visible', 'data', 'id'],
emits: ['close', 'save'],
data() {
return {
tls: { id: -1, name: '', inbounds: [], server: <iTls>{ enabled: true }, client: <oTls>{} },
tls: { id: 0, name: '', server: <iTls>{ enabled: true }, client: <oTls>{} },
title: "add",
loading: false,
menu: false,
@@ -354,15 +354,17 @@ export default {
},
methods: {
updateData() {
if (this.$props.index != -1) {
if (this.$props.id > 0) {
const newData = JSON.parse(this.$props.data)
this.tls = newData
if (this.tls.server == null) this.tls.server = {}
if (this.tls.client == null) this.tls.client = {}
this.tlsType = newData.server?.reality == undefined ? 0 : 1
this.usePath = newData.server?.key == undefined ? 0 : 1
this.title = "edit"
}
else {
this.tls = { id: 0, name: '', inbounds: [], server: {enabled: true}, client: {} }
this.tls = { id: 0, name: '', server: {enabled: true}, client: {} }
this.tlsType = 0
this.usePath = 0
this.title = "add"
@@ -461,10 +463,10 @@ export default {
},
computed: {
inTls(): iTls {
return <iTls> this.tls.server
return this.tls.server
},
outTls(): oTls {
return <oTls> this.tls.client
return this.tls.client
},
certText: {
get(): string { return this.inTls.certificate ? this.inTls.certificate.join('\n') : '' },
@@ -476,11 +478,11 @@ export default {
},
disableSni: {
get() { return this.outTls.disable_sni ?? false },
set(v: boolean) { this.outTls.disable_sni = v ? true : undefined }
set(v: boolean) { this.tls.client.disable_sni = v ? true : undefined }
},
insecure: {
get() { return this.outTls.insecure ?? false },
set(v: boolean) { this.outTls.insecure = v ? true : undefined }
set(v: boolean) { this.tls.client.insecure = v ? true : undefined }
},
server_port: {
get() { return this.inTls.reality?.handshake?.server_port ? this.inTls.reality.handshake.server_port : 443 },
+5 -2
View File
@@ -1,3 +1,5 @@
import { en } from "vuetify/lib/locale/index.mjs";
export default {
message: "Welcome",
success: "success",
@@ -37,6 +39,7 @@ export default {
home: "Home",
inbounds: "Inbounds",
outbounds: "Outbounds",
endpoints: "Endpoints",
clients: "Clients",
rules: "Rules",
tls: "TLS Settings",
@@ -75,6 +78,8 @@ export default {
inbound: "Inbound",
client: "Client",
outbound: "Outbound",
endpoint: "Endpoint",
config: "Config",
rule: "Rule",
user: "User",
tag: "Tag",
@@ -228,7 +233,6 @@ export default {
worker: "Workers",
ifName: "Interface Name",
sysIf: "System Interface",
gso: "Segmentation Offload",
options: "Wireguard Options",
multiPeer: "Multi Peer",
allowedIp: "Allowed IPs",
@@ -394,7 +398,6 @@ export default {
download: "Download",
volume: "Volume",
usage: "Usage",
enable: "Enable Statistics",
graphTitle: "Traffic Chart",
B: "B",
KB: "KB",
+5 -2
View File
@@ -1,3 +1,5 @@
import { config } from "process";
export default {
message: "خوش آمدید",
success: "موفق",
@@ -37,6 +39,7 @@ export default {
home: "خانه",
inbounds: "ورودی‌ها",
outbounds: "خروجی‌ها",
endpoints: "درگاه‌ها",
clients: "کاربران",
rules: "قوانین",
tls: "رمزنگاری‌ها",
@@ -75,6 +78,8 @@ export default {
inbound: "ورودی‌",
client: "کاربر",
outbound: "خروجی‌",
endpoint: "درگاه",
config: "پیکربندی",
rule: "قانون",
user: "کاربر",
tag: "برچسب",
@@ -227,7 +232,6 @@ export default {
worker: "عملگرها",
ifName: "نام اینترفیس",
sysIf: "استفاده از اینترفیس سیستم",
gso: "بارگذاری تقسیم‌بندی عمومی",
options: "گزینه‌های Wireguard",
multiPeer: "چند همتایی",
allowedIp: "آدرس‌های مجاز",
@@ -393,7 +397,6 @@ export default {
download: "دانلود",
volume: "حجم",
usage: "استفاده",
enable: "فعال سازی کنترل ترافیک",
graphTitle: "نمودار ترافیک",
B: "ب",
KB: "ک‌ب",
+5 -2
View File
@@ -1,3 +1,5 @@
import { config } from "process";
export default {
message: "Добро пожаловать",
success: "успех",
@@ -37,6 +39,7 @@ export default {
home: "Главная",
inbounds: "Входящие",
outbounds: "Исходящие",
endpoints: "Эндпоинты",
clients: "Клиенты",
rules: "Правила",
tls: "Настройки TLS",
@@ -75,6 +78,8 @@ export default {
inbound: "Входящий",
client: "Клиент",
outbound: "Исходящий",
endpoint: "Точка входа",
config: "Настройки",
rule: "Правило",
user: "Пользователь",
tag: "Тег",
@@ -228,7 +233,6 @@ export default {
worker: "Работники",
ifName: "Имя интерфейса",
sysIf: "Системный интерфейс",
gso: "Отключение сегментации",
options: "Параметры Wireguard",
multiPeer: "Множественный пир",
allowedIp: "Разрешенные IP",
@@ -394,7 +398,6 @@ export default {
download: "Скачивание",
volume: "Объем",
usage: "Использование",
enable: "Включить статистику",
graphTitle: "График трафика",
B: "Б",
KB: "КБ",
+3 -2
View File
@@ -37,6 +37,7 @@ export default {
home: "Trang chủ",
inbounds: "Đầu Vào",
outbounds: "Đầu ra",
endpoints: "Câu hình",
clients: "Khách hàng",
rules: "Quy tắc",
tls: "Cài đặt TLS",
@@ -75,6 +76,8 @@ export default {
inbound: "Đầu Vào",
client: "Máy Khách hàng",
outbound: "Đầu Ra",
endpoint: "Điểm cuối",
config: "Câu hình",
rule: "Quy tắc",
user: "Người dùng",
tag: "Thẻ",
@@ -228,7 +231,6 @@ export default {
worker: "Công nhân",
ifName: "Tên Giao diện",
sysIf: "Giao diện Hệ thống",
gso: "Giao Thức GSO",
options: "Tùy chọn Wireguard",
multiPeer: "Nhiều Đối tác",
allowedIp: "IPs được Phép",
@@ -395,7 +397,6 @@ export default {
download: "Tải xuống",
volume: "Thể tích",
usage: "Sử dụng",
enable: "Kích hoạt thống kê",
graphTitle: "Biểu đồ lưu lượng",
B: "B",
KB: "KB",
+3 -2
View File
@@ -37,6 +37,7 @@ export default {
home: "主页",
inbounds: "入站管理",
outbounds: "出站管理",
endpoints: "节点管理",
clients: "用户管理",
rules: "路由列表",
tls: "TLS 设置",
@@ -75,6 +76,8 @@ export default {
inbound: "入站",
client: "客户端",
outbound: "出站",
endpoint: "节点",
config: "配置",
rule: "规则",
user: "用户",
tag: "标签",
@@ -228,7 +231,6 @@ export default {
worker: "工作线程",
ifName: "接口名称",
sysIf: "系统接口",
gso: "分段卸载",
options: "WireGuard 选项",
multiPeer: "多对等体",
allowedIp: "允许的 IP 地址",
@@ -395,7 +397,6 @@ export default {
download: "下载",
volume: "流量",
usage: "已用",
enable: "启用统计",
graphTitle: "流量图表",
B: "B",
KB: "KB",
+3 -2
View File
@@ -38,6 +38,7 @@ export default {
home: "主頁",
inbounds: "入站管理",
outbounds: "出站管理",
endpoints: "端點管理",
clients: "用戶管理",
rules: "路由列表",
tls: "TLS 設置",
@@ -76,6 +77,8 @@ export default {
inbound: "入站",
client: "客戶端",
outbound: "出站",
endpoint: "端點",
config: "配置",
rule: "規則",
user: "用戶",
tag: "標簽",
@@ -229,7 +232,6 @@ export default {
worker: "工作線程",
ifName: "介面名稱",
sysIf: "系統介面",
gso: "分段卸載",
options: "Wireguard 選項",
multiPeer: "多對等方",
allowedIp: "允許的 IP",
@@ -396,7 +398,6 @@ export default {
download: "下載",
volume: "流量",
usage: "已用",
enable: "啟用統計",
graphTitle: "流量圖表",
B: "B",
KB: "KB",
-15
View File
@@ -1,15 +0,0 @@
export interface Addr {
server: string
server_port: number
tls?: boolean
insecure?: boolean
server_name?: string
remark?: string
}
export interface InData {
id: number
tag: string
addrs: Addr[]
outJson: any
}
+59 -59
View File
@@ -15,24 +15,24 @@ function utf8ToBase64(utf8String: string): string {
}
export namespace LinkUtil {
export function linkGenerator(user: Client, inbound: Inbound, tlsClient: any = {}, addrs: any[] = []): string[] {
export function linkGenerator(user: Client, inbound: Inbound, tls: any = {}, addrs: any[] = []): string[] {
switch(inbound.type){
case InTypes.Shadowsocks:
return shadowsocksLink(user,<Shadowsocks>inbound, addrs)
case InTypes.Naive:
return naiveLink(user,<Naive>inbound, addrs, tlsClient)
return naiveLink(user,<Naive>inbound, addrs, tls)
case InTypes.Hysteria:
return hysteriaLink(user,<Hysteria>inbound, addrs, tlsClient)
return hysteriaLink(user,<Hysteria>inbound, addrs, tls)
case InTypes.Hysteria2:
return hysteria2Link(user,<Hysteria2>inbound, addrs, tlsClient)
return hysteria2Link(user,<Hysteria2>inbound, addrs, tls)
case InTypes.TUIC:
return tuicLink(user,<TUIC>inbound, addrs, tlsClient)
return tuicLink(user,<TUIC>inbound, addrs, tls)
case InTypes.VLESS:
return vlessLink(user,<VLESS>inbound, addrs, tlsClient)
return vlessLink(user,<VLESS>inbound, addrs, tls)
case InTypes.Trojan:
return trojanLink(user,<Trojan>inbound, addrs, tlsClient)
return trojanLink(user,<Trojan>inbound, addrs, tls)
case InTypes.VMess:
return vmessLink(user,<VMess>inbound, addrs, tlsClient)
return vmessLink(user,<VMess>inbound, addrs, tls)
}
return []
}
@@ -71,17 +71,17 @@ export namespace LinkUtil {
return links
}
function hysteriaLink(user: Client, inbound: Hysteria, addrs: any[], tlsClient: any): string[] {
function hysteriaLink(user: Client, inbound: Hysteria, addrs: any[], tls: any): string[] {
const auth = user.config.hysteria.auth_str
const params = {
upmbps: inbound.up_mbps?? null,
downmbps: inbound.down_mbps?? null,
auth: auth?? null,
peer: inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null,
peer: tls?.server?.server_name?? null,
alpn: tls?.server?.alpn?.join(',')?? null,
obfsParam: inbound.obfs?? null,
fastopen: inbound.tcp_fast_open? 1 : 0,
insecure: tlsClient?.insecure ? 1 : null
insecure: tls?.client?.insecure ? 1 : null
}
let links = <string[]>[]
@@ -105,12 +105,12 @@ export namespace LinkUtil {
if (a.server_name?.length>0) {
uri.searchParams.set('peer', a.server_name)
} else {
inbound.tls.server_name ? uri.searchParams.set('peer', inbound.tls.server_name) : uri.searchParams.delete('peer')
tls?.server?.server_name ? uri.searchParams.set('peer', tls?.server?.server_name) : uri.searchParams.delete('peer')
}
if (a.insecure) {
uri.searchParams.set('insecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('insecure', '1') : uri.searchParams.delete('insecure')
tls?.client?.insecure ? uri.searchParams.set('insecure', '1') : uri.searchParams.delete('insecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
@@ -119,17 +119,17 @@ export namespace LinkUtil {
return links
}
function hysteria2Link(user: Client, inbound: Hysteria2, addrs: any[], tlsClient: any): string[] {
function hysteria2Link(user: Client, inbound: Hysteria2, addrs: any[], tls: any): string[] {
const password = user.config.hysteria2.password
const params = {
upmbps: inbound.up_mbps?? null,
downmbps: inbound.down_mbps?? null,
sni: inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null,
sni: tls?.server?.server_name?? null,
alpn: tls?.server?.alpn?.join(',')?? null,
obfs: inbound.obfs?.type?? null,
'obfs-password': inbound.obfs?.password?? null,
fastopen: inbound.tcp_fast_open? 1 : 0,
insecure: tlsClient?.insecure ? 1 : null
insecure: tls?.client?.insecure ? 1 : null
}
let links = <string[]>[]
@@ -153,12 +153,12 @@ export namespace LinkUtil {
if (a.server_name?.length>0) {
uri.searchParams.set('sni', a.server_name)
} else {
inbound.tls.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
tls?.server?.server_name ? uri.searchParams.set('sni', tls?.server?.server_name) : uri.searchParams.delete('sni')
}
if (a.insecure) {
uri.searchParams.set('insecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('insecure', '1') : uri.searchParams.delete('insecure')
tls?.client?.insecure ? uri.searchParams.set('insecure', '1') : uri.searchParams.delete('insecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
@@ -167,17 +167,17 @@ export namespace LinkUtil {
return links
}
function naiveLink(user: Client, inbound: Naive, addrs: any[], tlsClient: any): string[] {
function naiveLink(user: Client, inbound: Naive, addrs: any[], tls: any): string[] {
const password = user.config.naive.password
let links = <string[]>[]
if (addrs.length == 0) {
const params = {
padding: 1,
peer: inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null,
peer: tls?.server?.server_name?? null,
alpn: tls?.server?.alpn?.join(',')?? null,
tfo: inbound.tcp_fast_open? 1 : 0,
allowInsecure: tlsClient?.insecure ? 1 : null
allowInsecure: tls?.client?.insecure ? 1 : null
}
const uri = `http2://${utf8ToBase64(user.name + ":" + password + "@" + location.hostname + ":" + inbound.listen_port)}`
const paramsArray = []
@@ -191,10 +191,10 @@ export namespace LinkUtil {
addrs.forEach(a => {
const params = {
padding: 1,
peer: a.server_name?.length>0 ? a.server_name : inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null,
peer: a.server_name?.length>0 ? a.server_name : tls?.server?.server_name?? null,
alpn: tls?.server?.alpn?.join(',')?? null,
tfo: inbound.tcp_fast_open? 1 : 0,
allowInsecure: a.insecure ? 1 : tlsClient?.insecure ? 1 : null
allowInsecure: a.insecure ? 1 : tls?.client?.insecure ? 1 : null
}
const uri = `http2://${utf8ToBase64(user + ":" + password + "@" + a.server + ":" + a.server_port)}`
const paramsArray = []
@@ -209,14 +209,14 @@ export namespace LinkUtil {
return links
}
function tuicLink(user: Client, inbound: TUIC, addrs: any[], tlsClient: any): string[] {
function tuicLink(user: Client, inbound: TUIC, addrs: any[], tls: any): string[] {
const u = user.config.tuic
const params = {
sni: inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null,
sni: tls?.server?.server_name?? null,
alpn: tls?.server?.alpn?.join(',')?? null,
congestion_control: inbound.congestion_control?? null,
allowInsecure: tlsClient?.insecure ? 1 : null,
disable_sni: tlsClient?.disable_sni ? 1 : null
allowInsecure: tls?.client?.insecure ? 1 : null,
disable_sni: tls?.client?.disable_sni ? 1 : null
}
let links = <string[]>[]
@@ -240,12 +240,12 @@ export namespace LinkUtil {
if (a.server_name?.length>0) {
uri.searchParams.set('sni', a.server_name)
} else {
inbound.tls.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
tls?.server?.server_name ? uri.searchParams.set('sni', tls?.server?.server_name) : uri.searchParams.delete('sni')
}
if (a.insecure) {
uri.searchParams.set('allowInsecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
tls?.client?.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
@@ -287,7 +287,7 @@ export namespace LinkUtil {
return params
}
function vlessLink(user: Client, inbound: VLESS, addrs: any[], tlsClient: any): string[] {
function vlessLink(user: Client, inbound: VLESS, addrs: any[], tls: any): string[] {
const u = user.config.vless
const transport = <Transport>inbound.transport
@@ -295,14 +295,14 @@ export namespace LinkUtil {
const params = {
type: transport?.type?? 'tcp',
security: inbound.tls?.enabled? inbound.tls?.reality?.enabled ? 'reality' : 'tls' : null,
alpn: inbound.tls?.alpn?.join(',')?? null,
sni: inbound.tls?.server_name?? null,
flow: inbound.tls?.enabled ? u?.flow?? null : null,
allowInsecure: tlsClient?.insecure ? 1 : null,
fp: tlsClient?.utls?.enabled ? tlsClient.utls.fingerprint : null,
pbk: tlsClient?.reality?.public_key?? null,
sid: inbound.tls?.reality?.enabled ? (inbound.tls?.reality?.short_id?.length>0 ? inbound.tls.reality.short_id[RandomUtil.randomInt(inbound.tls.reality.short_id.length)] : null) : null
security: tls?.server?.enabled? tls?.server?.reality?.enabled ? 'reality' : 'tls' : null,
alpn: tls?.server?.alpn?.join(',')?? null,
sni: tls?.server?.server_name?? null,
flow: tls?.server?.enabled ? u?.flow?? null : null,
allowInsecure: tls?.client?.insecure ? 1 : null,
fp: tls?.client?.utls?.enabled ? tls.client.utls.fingerprint : null,
pbk: tls?.client?.reality?.public_key?? null,
sid: tls?.server?.reality?.enabled ? (tls?.server?.reality?.short_id?.length>0 ? tls.server.reality.short_id[RandomUtil.randomInt(tls.server.reality.short_id.length)] : null) : null
}
let links = <string[]>[]
if (addrs.length == 0) {
@@ -335,12 +335,12 @@ export namespace LinkUtil {
if (a.server_name?.length>0) {
uri.searchParams.set('sni', a.server_name)
} else {
inbound.tls?.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
tls?.server?.server_name ? uri.searchParams.set('sni', tls?.server?.server_name) : uri.searchParams.delete('sni')
}
if (a.insecure) {
uri.searchParams.set('allowInsecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
tls?.client?.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
@@ -349,7 +349,7 @@ export namespace LinkUtil {
return links
}
function trojanLink(user: Client, inbound: Trojan, addrs: any[], tlsClient: any): string[] {
function trojanLink(user: Client, inbound: Trojan, addrs: any[], tls: any): string[] {
const u = user.config.trojan
const transport = <Transport>inbound.transport
@@ -357,13 +357,13 @@ export namespace LinkUtil {
const params = {
type: transport?.type?? 'tcp',
security: inbound.tls?.enabled? inbound.tls?.reality?.enabled ? 'reality' : 'tls' : null,
alpn: inbound.tls?.alpn?.join(',')?? null,
sni: inbound.tls?.server_name?? null,
allowInsecure: tlsClient?.insecure ? 1 : null,
fp: tlsClient?.utls?.enabled ? tlsClient.utls.fingerprint : null,
pbk: tlsClient?.reality?.public_key?? null,
sid: inbound.tls?.reality?.enabled ? (inbound.tls?.reality?.short_id?.length>0 ? inbound.tls.reality.short_id[RandomUtil.randomInt(inbound.tls.reality.short_id.length)] : null) : null
security: tls?.server?.enabled? tls?.server?.reality?.enabled ? 'reality' : 'tls' : null,
alpn: tls?.server?.alpn?.join(',')?? null,
sni: tls?.server?.server_name?? null,
allowInsecure: tls?.client?.insecure ? 1 : null,
fp: tls?.client?.utls?.enabled ? tls.client.utls.fingerprint : null,
pbk: tls?.client?.reality?.public_key?? null,
sid: tls?.server?.reality?.enabled ? (tls?.server?.reality?.short_id?.length>0 ? tls?.server?.reality.short_id[RandomUtil.randomInt(tls?.server?.reality.short_id.length)] : null) : null
}
let links = <string[]>[]
@@ -397,12 +397,12 @@ export namespace LinkUtil {
if (a.server_name?.length>0) {
uri.searchParams.set('sni', a.server_name)
} else {
inbound.tls?.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
tls?.server?.server_name ? uri.searchParams.set('sni', tls?.server?.server_name) : uri.searchParams.delete('sni')
}
if (a.insecure) {
uri.searchParams.set('allowInsecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
tls?.client?.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
@@ -411,7 +411,7 @@ export namespace LinkUtil {
return links
}
function vmessLink(user: Client, inbound: VMess, addrs: any[], tlsClient: any): string[] {
function vmessLink(user: Client, inbound: VMess, addrs: any[], tls: any): string[] {
const u = user.config.vmess
const transport = <Transport>inbound.transport
@@ -429,9 +429,9 @@ export namespace LinkUtil {
path: tParams.path?? undefined,
port: inbound.listen_port,
ps: inbound.tag,
sni: inbound.tls.server_name?? undefined,
tls: Object.keys(inbound.tls).length>0? 'tls' : 'none',
allowInsecure: tlsClient?.insecure ? 1 : undefined
sni: tls?.server?.server_name?? undefined,
tls: tls?.server && Object.keys(tls.server).length>0? 'tls' : 'none',
allowInsecure: tls?.client?.insecure ? 1 : undefined
}
let links = <string[]>[]
if (addrs.length == 0) {
+18 -19
View File
@@ -3,50 +3,49 @@ import { iTls } from "@/types/inTls"
import { oTls } from "@/types/outTls"
import RandomUtil from "./randomUtil"
export function fillData(out: any, inbound: Inbound, tlsClient: any) {
if (Object.hasOwn(inbound, 'tls')) {
const inb = <any>inbound
addTls(out,inb.tls,tlsClient)
export function fillData(inbound: Inbound, tls: any | null = null) {
if (tls != null) {
addTls(inbound.out_json, tls.server, tls.client)
} else {
delete out.tls
delete inbound.out_json.tls
}
out.type = inbound.type
out.tag = inbound.tag
out.server = location.hostname
out.server_port = inbound.listen_port
inbound.out_json.type = inbound.type
inbound.out_json.tag = inbound.tag
inbound.out_json.server = location.hostname
inbound.out_json.server_port = inbound.listen_port
switch(inbound.type){
case InTypes.HTTP: case InTypes.SOCKS: case InTypes.Mixed:
return
case InTypes.Shadowsocks:
shadowsocksOut(out, <Shadowsocks>inbound)
shadowsocksOut(inbound.out_json, <Shadowsocks>inbound)
return
case InTypes.ShadowTLS:
shadowTlsOut(out, <ShadowTLS>inbound)
shadowTlsOut(inbound.out_json, <ShadowTLS>inbound)
return
case InTypes.Hysteria:
hysteriaOut(out, <Hysteria>inbound)
hysteriaOut(inbound.out_json, <Hysteria>inbound)
return
case InTypes.Hysteria2:
hysteria2Out(out, <Hysteria2>inbound)
hysteria2Out(inbound.out_json, <Hysteria2>inbound)
return
case InTypes.TUIC:
tuicOut(out, <TUIC>inbound)
tuicOut(inbound.out_json, <TUIC>inbound)
return
case InTypes.VLESS:
vlessOut(out, <VLESS>inbound)
vlessOut(inbound.out_json, <VLESS>inbound)
return
case InTypes.Trojan:
trojanOut(out, <Trojan>inbound)
trojanOut(inbound.out_json, <Trojan>inbound)
return
case InTypes.VMess:
vmessOut(out, <VMess>inbound)
vmessOut(inbound.out_json, <VMess>inbound)
return
}
Object.keys(out).forEach(key => delete out[key])
Object.keys(inbound.out_json).forEach(key => delete inbound.out_json[key])
}
function addTls(out: any, tls: iTls, tlsClient: oTls){
out.tls = tlsClient
out.tls = tlsClient?? {}
if(tls.enabled) out.tls.enabled = tls.enabled
if(tls.server_name) out.tls.server_name = tls.server_name
if(tls.alpn) out.tls.alpn = tls.alpn
+188
View File
@@ -0,0 +1,188 @@
const WgUtil = {
gf(init?: Float64Array): Float64Array {
var r = new Float64Array(16)
if (init) {
for (var i = 0; i < init.length; ++i)
r[i] = init[i]
}
return r
},
pack(o: Float64Array, n: Float64Array): Float64Array {
var b, m = this.gf(), t = this.gf()
for (var i = 0; i < 16; ++i)
t[i] = n[i]
this.carry(t)
this.carry(t)
this.carry(t)
for (var j = 0; j < 2; ++j) {
m[0] = t[0] - 0xffed
for (var i = 1; i < 15; ++i) {
m[i] = t[i] - 0xffff - ((m[i - 1] >> 16) & 1)
m[i - 1] &= 0xffff
}
m[15] = t[15] - 0x7fff - ((m[14] >> 16) & 1)
b = (m[15] >> 16) & 1
m[14] &= 0xffff
this.cswap(t, m, 1 - b)
}
for (var i = 0; i < 16; ++i) {
o[2 * i] = t[i] & 0xff
o[2 * i + 1] = t[i] >> 8
}
},
carry(o: Float64Array) {
var c
for (var i = 0; i < 16; ++i) {
o[(i + 1) % 16] += (i < 15 ? 1 : 38) * Math.floor(o[i] / 65536)
o[i] &= 0xffff
}
},
cswap(p: Float64Array, q: Float64Array, b: number) {
var t, c = ~(b - 1)
for (var i = 0; i < 16; ++i) {
t = c & (p[i] ^ q[i])
p[i] ^= t
q[i] ^= t
}
},
add(o: Float64Array, a: Float64Array, b: Float64Array) {
for (var i = 0; i < 16; ++i)
o[i] = (a[i] + b[i]) | 0
},
subtract(o: Float64Array, a: Float64Array, b: Float64Array) {
for (var i = 0; i < 16; ++i)
o[i] = (a[i] - b[i]) | 0
},
multmod(o: Float64Array, a: Float64Array, b: Float64Array) {
var t = new Float64Array(31)
for (var i = 0; i < 16; ++i) {
for (var j = 0; j < 16; ++j)
t[i + j] += a[i] * b[j]
}
for (var i = 0; i < 15; ++i)
t[i] += 38 * t[i + 16]
for (var i = 0; i < 16; ++i)
o[i] = t[i]
this.carry(o)
this.carry(o)
},
invert(o: Float64Array, i: Float64Array) {
var c = this.gf()
for (var a = 0; a < 16; ++a)
c[a] = i[a]
for (var a = 253; a >= 0; --a) {
this.multmod(c, c, c)
if (a !== 2 && a !== 4)
this.multmod(c, c, i)
}
for (var a = 0; a < 16; ++a)
o[a] = c[a]
},
clamp(z) {
z[31] = (z[31] & 127) | 64
z[0] &= 248
},
generatePublicKey(privateKey: Uint8Array): Uint8Array {
var r, z = new Uint8Array(32)
var a = this.gf([1]),
b = this.gf([9]),
c = this.gf(),
d = this.gf([1]),
e = this.gf(),
f = this.gf(),
_121665 = this.gf([0xdb41, 1]),
_9 = this.gf([9])
for (var i = 0; i < 32; ++i)
z[i] = privateKey[i]
this.clamp(z)
for (var i = 254; i >= 0; --i) {
r = (z[i >>> 3] >>> (i & 7)) & 1
this.cswap(a, b, r)
this.cswap(c, d, r)
this.add(e, a, c)
this.subtract(a, a, c)
this.add(c, b, d)
this.subtract(b, b, d)
this.multmod(d, e, e)
this.multmod(f, a, a)
this.multmod(a, c, a)
this.multmod(c, b, e)
this.add(e, a, c)
this.subtract(a, a, c)
this.multmod(b, a, a)
this.subtract(c, d, f)
this.multmod(a, c, _121665)
this.add(a, a, d)
this.multmod(c, c, a)
this.multmod(a, d, f)
this.multmod(d, b, _9)
this.multmod(b, e, e)
this.cswap(a, b, r)
this.cswap(c, d, r)
}
this.invert(c, c)
this.multmod(a, a, c)
this.pack(z, a)
return z
},
generatePresharedKey(): Uint8Array {
var privateKey = new Uint8Array(32)
window.crypto.getRandomValues(privateKey)
return privateKey
},
generatePrivateKey(): Uint8Array {
var privateKey = this.generatePresharedKey()
this.clamp(privateKey)
return privateKey
},
encodeBase64(dest: Uint8Array, src: Uint8Array) {
var input = Uint8Array.from([(src[0] >> 2) & 63, ((src[0] << 4) | (src[1] >> 4)) & 63, ((src[1] << 2) | (src[2] >> 6)) & 63, src[2] & 63])
for (var i = 0; i < 4; ++i)
dest[i] = input[i] + 65 +
(((25 - input[i]) >> 8) & 6) -
(((51 - input[i]) >> 8) & 75) -
(((61 - input[i]) >> 8) & 15) +
(((62 - input[i]) >> 8) & 3)
},
keyToBase64(key: Uint8Array): string {
var i, base64 = new Uint8Array(44)
for (i = 0; i < 32 / 3; ++i)
this.encodeBase64(base64.subarray(i * 4), key.subarray(i * 3))
this.encodeBase64(base64.subarray(i * 4), Uint8Array.from([key[i * 3 + 0], key[i * 3 + 1], 0]))
base64[43] = 61
return String.fromCharCode.apply(null, base64)
},
keyFromBase64(encoded: string): Uint8Array {
const binaryStr = atob(encoded)
const bytes = new Uint8Array(binaryStr.length)
for (let i = 0; i < binaryStr.length; i++) {
bytes[i] = binaryStr.charCodeAt(i)
}
return bytes
},
generateKeypair(secretKey?: string ='') {
var privateKey = secretKey.length>0 ? this.keyFromBase64(secretKey) : this.generatePrivateKey()
var publicKey = this.generatePublicKey(privateKey)
return {
publicKey: this.keyToBase64(publicKey),
privateKey: secretKey.length>0 ? secretKey : this.keyToBase64(privateKey)
}
}
}
export default WgUtil
+5
View File
@@ -34,6 +34,11 @@ const routes = [
name: 'pages.outbounds',
component: () => import('@/views/Outbounds.vue'),
},
{
path: '/endpoints',
name: 'pages.endpoints',
component: () => import('@/views/Endpoints.vue'),
},
{
path: '/rules',
name: 'pages.rules',
+41 -78
View File
@@ -3,6 +3,9 @@ import HttpUtils from '@/plugins/httputil'
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'
const Data = defineStore('Data', {
state: () => ({
@@ -10,23 +13,17 @@ const Data = defineStore('Data', {
reloadItems: localStorage.getItem("reloadItems")?.split(',')?? <string[]>[],
subURI: "",
onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]},
oldData: <{config: any, clients: any[], tlsConfigs: any[], inData: any[]}>{},
config: <any>{},
inbounds: <Inbound[]>[],
outbounds: <Outbound[]>[],
endpoints: <Endpoint[]>[],
clients: [],
tlsConfigs: [],
inData: [],
tlsConfigs: <any[]>[],
}),
actions: {
async loadData() {
const msg = await HttpUtils.get('api/load', this.lastLoad >0 ? {lu: this.lastLoad} : {} )
if(msg.success) {
this.lastLoad = Math.floor((new Date()).getTime()/1000)
// Set new data
if (msg.obj.config) this.oldData.config = msg.obj.config
if (msg.obj.clients) this.oldData.clients = msg.obj.clients
if (msg.obj.tls) this.oldData.tlsConfigs = msg.obj.tls
if (msg.obj.inData) this.oldData.inData = msg.obj.inData
this.onlines = msg.obj.onlines
if (msg.obj.lastLog) {
push.error({
@@ -37,84 +34,50 @@ const Data = defineStore('Data', {
}
if (msg.obj.config) {
// To avoid ref copy
const data = JSON.parse(JSON.stringify(msg.obj))
if (data.subURI) this.subURI = data.subURI
if (data.config) this.config = data.config
if (data.clients) this.clients = data.clients
if (data.tls) this.tlsConfigs = data.tls
if (data.inData) this.inData = data.inData
this.setNewData(msg.obj)
}
}
},
async pushData() {
const diff = {
config: JSON.stringify(FindDiff.Config(this.config,this.oldData.config), null, 2),
clients: JSON.stringify(FindDiff.ArrObj(this.clients,this.oldData.clients, "clients"), null, 2),
tls: JSON.stringify(FindDiff.ArrObj(this.tlsConfigs,this.oldData.tlsConfigs, "tls"), null, 2),
inData: JSON.stringify(FindDiff.ArrObj(this.inData,this.oldData.inData, "inData"), null, 2),
}
const msg = await HttpUtils.post('api/save',diff)
if(msg.success) {
this.lastLoad = 0
this.loadData()
}
setNewData(data: any) {
this.lastLoad = Math.floor((new Date()).getTime()/1000)
if (data.subURI) this.subURI = data.subURI
if (data.config) this.config = data.config
if (data.clients) this.clients = data.clients
if (data.inbounds) this.inbounds = data.inbounds
if (data.outbounds) this.outbounds = data.outbounds
if (data.endpoints) this.endpoints = data.endpoints
if (data.tls) this.tlsConfigs = data.tls
},
async delInbound(index: number) {
const diff = {
config: JSON.stringify([{key: "inbounds", action: "del", index: index, obj: null}]),
clients: JSON.stringify(FindDiff.ArrObj(this.clients,this.oldData.clients, "clients"), null, 2),
tls: JSON.stringify(FindDiff.ArrObj(this.tlsConfigs,this.oldData.tlsConfigs, "tls"), null, 2),
inData: <string|undefined> undefined,
}
// Validate inData
let invalidInData = <any[]>[]
this.inData.forEach((d:any) => {
const inboundIndex = this.config.inbounds.findIndex((i:any) => i.tag == d.tag)
if (inboundIndex == -1) invalidInData.push({key: "inData", action: "del", index: d.id, obj: null})
})
if (invalidInData.length>0) {
diff.inData = JSON.stringify(invalidInData)
}
const msg = await HttpUtils.post('api/save',diff)
async loadInbounds(ids: number[]): Promise<Inbound[]> {
const options = ids.length > 0 ? {id: ids.join(",")} : {}
const msg = await HttpUtils.get('api/inbounds', options)
if(msg.success) {
this.loadData()
return msg.obj.inbounds
}
return <Inbound[]>[]
},
async delInData(id: number) {
const diff = {
inData: JSON.stringify([{key: "inData", action: "del", index: id, obj: null}])
async save (object: string, action: string, data: any, userLinks: any[] | null = null, outJsons: any[] | null = null): Promise<boolean> {
let postData = {
object: object,
action: action,
data: JSON.stringify(data),
userLinks: userLinks == null ? undefined : JSON.stringify(userLinks),
outJsons: outJsons == null ? undefined : JSON.stringify(outJsons),
}
await HttpUtils.post('api/save',diff)
},
async delOutbound(index: number) {
const diff = {
config: JSON.stringify([{key: "outbounds", action: "del", index: index, obj: null}]),
if (userLinks == null) {
delete postData.userLinks
}
const msg = await HttpUtils.post('api/save',diff)
if(msg.success) {
this.loadData()
}
},
async delClient(id: number) {
const diff = {
config: JSON.stringify(FindDiff.Config(this.config,this.oldData.config)),
clients:JSON.stringify([{key: "clients", action: "del", index: id, obj: null}]),
}
const msg = await HttpUtils.post('api/save',diff)
if(msg.success) {
this.loadData()
}
},
async delTls(id: number) {
const diff = {
tls:JSON.stringify([{key: "tls", action: "del", index: id, obj: null}]),
}
const msg = await HttpUtils.post('api/save',diff)
if(msg.success) {
this.loadData()
const msg = await HttpUtils.post('api/save', postData)
if (msg.success) {
const objectName = ['tls', 'config'].includes(object) ? object : object.substring(0, object.length - 1)
push.success({
title: i18n.global.t('success'),
duration: 5000,
message: i18n.global.t('actions.' + action) + " " + i18n.global.t('objects.' + objectName)
})
this.setNewData(msg.obj)
}
return msg.success
}
},
})
+1 -1
View File
@@ -6,7 +6,7 @@ export interface Client {
enable: boolean
name: string
config: Config
inbounds: string[]
inbounds: number[]
links: Link[]
volume: number
expiry: number
-23
View File
@@ -83,8 +83,6 @@ interface RouteRuleSet {
interface Experimental {
cache_file?: CacheFile
clash_api?: ClashApi
v2ray_api: V2rayApi
}
interface CacheFile {
@@ -94,27 +92,6 @@ interface CacheFile {
store_fakeip?: boolean
}
interface V2rayApi {
listen: string
stats: V2rayApiStats
}
export interface V2rayApiStats {
enabled: boolean
inbounds: string[]
outbounds: string[]
users: string[]
}
interface ClashApi {
external_controller?: string
external_ui?: string
external_ui_download_url?: string
external_ui_download_detour?: string
secret?: string
default_mode?: string
}
export interface Config {
log: Log
dns: Dns
+58
View File
@@ -0,0 +1,58 @@
import { Dial } from "./outbounds"
export const EpTypes = {
Wireguard: 'wireguard',
}
type EpType = typeof EpTypes[keyof typeof EpTypes]
interface EndpointBasics {
id: number
type: EpType
tag: string
}
export interface WgPeer {
address: string
port: number
public_key: string
pre_shared_key?: string
allowed_ips?: string[]
persistent_keepalive_interval?: number
reserved?: number[]
}
export interface WireGuard extends EndpointBasics, Dial {
system?: boolean
name?: string
mtu?: number
address: string[]
private_key: string
listen_port: number,
peers: WgPeer[]
udp_timeout?: string,
workers?: number
}
// Create interfaces dynamically based on EpTypes keys
type InterfaceMap = {
[Key in keyof typeof EpTypes]: {
type: string
[otherProperties: string]: any; // You can add other properties as needed
}
}
// Create union type from InterfaceMap
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: ''}] },
}
export function createEndpoint<T extends Endpoint>(type: string,json?: Partial<T>): Endpoint {
const defaultObject: Endpoint = { ...defaultValues[type], ...(json || {}) }
return defaultObject
}
+24 -14
View File
@@ -24,6 +24,15 @@ export const InTypes = {
type InType = typeof InTypes[keyof typeof InTypes]
export interface Addr {
server: string
server_port: number
tls?: boolean
insecure?: boolean
server_name?: string
remark?: string
}
export interface Listen {
listen: string
listen_port: number
@@ -39,8 +48,12 @@ export interface Listen {
}
interface InboundBasics extends Listen {
id: number
type: InType
tag: string
tls_id: number
addrs?: Addr[]
out_json?: any
}
interface UsernamePass {
@@ -87,7 +100,6 @@ export interface SOCKS extends InboundBasics {
}
export interface HTTP extends InboundBasics {
users?: UsernamePass[]
tls?: iTls,
}
export interface Shadowsocks extends InboundBasics {
method: string
@@ -125,7 +137,6 @@ export interface Hysteria extends InboundBasics {
recv_window_client?: number
max_conn_client?: number
disable_mtu_discovery?: boolean
tls: iTls
}
export interface ShadowTLS extends InboundBasics {
version: 1|2|3
@@ -139,7 +150,6 @@ export interface ShadowTLS extends InboundBasics {
}
export interface VLESS extends InboundBasics {
users: VlessUser[]
tls?: iTls
multiplex?: iMultiplex
transport?: Transport
}
@@ -149,7 +159,6 @@ export interface TUIC extends InboundBasics {
auth_timeout?: string
zero_rtt_handshake?: boolean
heartbeat?: string
tls: iTls
}
export interface Hysteria2 extends InboundBasics {
up_mbps?: number
@@ -160,7 +169,6 @@ export interface Hysteria2 extends InboundBasics {
}
users: NamePass[]
ignore_client_bandwidth?: boolean
tls: iTls
masquerade?: string
brutal_debug?: boolean
}
@@ -234,24 +242,26 @@ type userEnabledTypes = {
vless: VLESS
}
export const inboundWithUsers = ['mixed', 'socks:', 'http', 'shadowsocks', 'vmess', 'trojan', 'naive', 'hysteria', 'shadowtls', 'tuic', 'hysteria2', 'vless']
// Create union type from userEnabledTypes
export type InboundWithUser = userEnabledTypes[keyof userEnabledTypes]
type InboundWithUser = userEnabledTypes[keyof userEnabledTypes]
// Create defaultValues object dynamically
const defaultValues: Record<InType, Inbound> = {
direct: <Direct>{ type: InTypes.Direct },
mixed: <Mixed>{ type: InTypes.Mixed },
socks: <SOCKS>{ type: InTypes.SOCKS },
http: <HTTP>{ type: InTypes.HTTP, tls: {} },
http: <HTTP>{ type: InTypes.HTTP, tls_id: 0 },
shadowsocks: <Shadowsocks>{ type: InTypes.Shadowsocks, method: 'none', multiplex: {} },
vmess: <VMess>{ type: InTypes.VMess, users: <VmessUser[]>[], tls: {}, multiplex: {}, transport: {} },
trojan: <Trojan>{ type: InTypes.Trojan, users: <NamePass[]>[], tls: {}, multiplex: {}, transport: {} },
naive: <Naive>{ type: InTypes.Naive, users: <UsernamePass[]>[], tls: { enabled: true } },
hysteria: <Hysteria>{ type: InTypes.Hysteria, users: <NameAuth[]>[], up_mbps: 100, down_mbps: 100, tls: { enabled: true } },
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: { enabled: true } },
hysteria2: <Hysteria2>{ type: InTypes.Hysteria2, users: <NamePass[]>[], tls: { enabled: true } },
vless: <VLESS>{ type: InTypes.VLESS, users: <VlessUser[]>[], tls: {}, multiplex: {}, transport: {} },
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: {} },
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 },
+1 -19
View File
@@ -10,7 +10,6 @@ export const OutTypes = {
Shadowsocks: 'shadowsocks',
VMess: 'vmess',
Trojan: 'trojan',
Wireguard: 'wireguard',
Hysteria: 'hysteria',
VLESS: 'vless',
ShadowTLS: 'shadowtls',
@@ -41,6 +40,7 @@ export interface Dial {
}
interface OutboundBasics {
id: number
type: OutType
tag: string
}
@@ -124,23 +124,6 @@ export interface Trojan extends OutboundBasics, Dial {
transport?: Transport
}
export interface WireGuard extends OutboundBasics, Dial {
server?: string
server_port?: number
system_interface?: boolean
gso?: boolean
interface_name?: string
local_address: string[]
private_key: string
peers?: WgPeer[]
peer_public_key?: string
pre_shared_key?: string
reserved?: number[]
workers?: number
mtu?: number
network?: "udp" | "tcp"
}
export interface Hysteria extends OutboundBasics, Dial {
server: string
server_port: number
@@ -263,7 +246,6 @@ const defaultValues: Record<OutType, Outbound> = {
shadowsocks: { type: OutTypes.Shadowsocks, method: 'none', multiplex: {} },
vmess: { type: OutTypes.VMess, tls: {}, multiplex: {}, transport: {}, security: 'auto', global_padding: false },
trojan: { type: OutTypes.Trojan, tls: {}, multiplex: {}, transport: {} },
wireguard: { type: OutTypes.Wireguard, local_address: ['10.0.0.2/32','fe80::2/128'], private_key: '' },
hysteria: { type: OutTypes.Hysteria, up_mbps: 100, down_mbps: 100, tls: { enabled: true } },
shadowtls: { type: OutTypes.ShadowTLS, version: 3, tls: { enabled: true } },
vless: { type: OutTypes.VLESS, tls: {}, multiplex: {}, transport: {} },
+35 -109
View File
@@ -1,4 +1,11 @@
<template>
<v-row style="margin-bottom: 10px;">
<v-col cols="12" justify="center" align="center">
<v-btn variant="outlined" color="warning" @click="saveConfig" :loading="loading" :disabled="stateChange">
{{ $t('actions.save') }}
</v-btn>
</v-col>
</v-row>
<v-expansion-panels>
<v-expansion-panel :title="$t('basic.log.title')">
<v-expansion-panel-text>
@@ -11,6 +18,8 @@
hide-details
:label="$t('basic.log.level')"
:items="levels"
clearable
@click:clear="delete appConfig.log.level"
v-model="appConfig.log.level">
</v-select>
</v-col>
@@ -215,128 +224,49 @@
hide-details></v-switch>
</v-col>
</v-row>
Clash API
<v-divider></v-divider>
<v-row>
<v-col cols="12" sm="6" md="3" lg="2">
<v-switch v-model="enableClashApi" color="primary" :label="$t('enable')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field
v-model="appConfig.experimental.clash_api.external_controller"
hide-details
label="External Controller"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field
v-model="appConfig.experimental.clash_api.external_ui"
hide-details
label="External UI"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field
v-model="appConfig.experimental.clash_api.external_ui_download_url"
hide-details
label="UI Download URL"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field
v-model="appConfig.experimental.clash_api.external_ui_download_detour"
hide-details
label="UI Download detour"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field
v-model="appConfig.experimental.clash_api.secret"
hide-details
label="Secret"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field
v-model="appConfig.experimental.clash_api.default_mode"
hide-details
label="Default Mode"
></v-text-field>
</v-col>
</v-row>
V2Ray API
<v-divider></v-divider>
<v-row>
<v-col cols="12" sm="6" md="3" lg="2">
<v-text-field
v-model="appConfig.experimental.v2ray_api.listen"
hide-details
:label="$t('objects.listen')"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2">
<v-switch v-model="appConfig.experimental.v2ray_api.stats.enabled"
color="primary"
:label="$t('stats.enable')"
hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="appConfig.experimental.v2ray_api.stats.enabled">
<v-col cols="12" sm="6">
<v-select
hide-details
:label="$t('pages.inbounds')"
multiple chips closable-chips
:items="inboundTags"
v-model="appConfig.experimental.v2ray_api.stats.inbounds">
</v-select>
</v-col>
<v-col cols="12" sm="6">
<v-select
hide-details
:label="$t('pages.outbounds')"
multiple chips closable-chips
:items="outboundTags"
v-model="appConfig.experimental.v2ray_api.stats.outbounds">
</v-select>
</v-col>
<v-col cols="12" sm="6">
<v-select
hide-details
:label="$t('pages.clients')"
multiple chips closable-chips
:items="clientNames"
v-model="appConfig.experimental.v2ray_api.stats.users">
</v-select>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</template>
<script lang="ts" setup>
import Data from '@/store/modules/data';
import Dial from '@/components/Dial.vue';
import { computed } from 'vue';
import { Config, Ntp } from '@/types/config';
import { Client } from '@/types/clients';
import Data from '@/store/modules/data'
import Dial from '@/components/Dial.vue'
import { computed, ref, onMounted } from 'vue'
import { Config, Ntp } from '@/types/config'
import { Client } from '@/types/clients'
import { FindDiff } from '@/plugins/utils'
const oldConfig = ref({})
const loading = ref(false)
const appConfig = computed((): Config => {
return <Config> Data().config
})
const inboundTags = computed((): string[] => {
return appConfig.value.inbounds.map(i => i.tag)
onMounted(async () => {
oldConfig.value = JSON.parse(JSON.stringify(Data().config))
})
const stateChange = computed(() => {
return FindDiff.deepCompare(appConfig.value,oldConfig.value)
})
const saveConfig = async () => {
loading.value = true
const success = await Data().save("config", "set", appConfig.value)
if (success) {
oldConfig.value = JSON.parse(JSON.stringify(Data().config))
loading.value = false
}
}
const outboundTags = computed((): string[] => {
return appConfig.value.outbounds.map(o => o.tag)
return [...Data().outbounds?.map((o:any) => o.tag), ...Data().endpoints?.map((e:any) => e.tag)]
})
const clientNames = computed((): string[] => {
const clients = <Client[]>Data().clients
return clients?.map(c => c.name)
return Data().clients.map((c:any) => c.name)
})
const levels = ["trace", "debug", "info", "warn", "error", "fatal", "panic"]
@@ -384,8 +314,4 @@ const enableCacheFile = computed({
}
})
const enableClashApi = computed({
get() { return appConfig.value.experimental.clash_api != undefined },
set(v:boolean) { v ? appConfig.value.experimental.clash_api = {} : delete appConfig.value.experimental.clash_api }
})
</script>
+30 -112
View File
@@ -3,10 +3,9 @@
<ClientModal
v-model="modal.visible"
:visible="modal.visible"
:index="modal.index"
:id="modal.id"
:data="modal.data"
:groups="groups"
:stats="modal.stats"
:inboundTags="inboundTags"
@close="closeModal"
@save="saveModal"
@@ -34,7 +33,7 @@
/>
<v-row justify="center" align="center">
<v-col cols="auto">
<v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn>
<v-btn color="primary" @click="showModal(0)">{{ $t('actions.add') }}</v-btn>
</v-col>
<v-col cols="auto">
<v-menu v-model="actionMenu" :close-on-content-click="false" location="bottom center">
@@ -147,7 +146,6 @@
<v-col cols="auto">
<v-switch color="primary"
v-model="item.enable"
@update:model-value="buildInboundsUsers(item.inbounds)"
hideDetails density="compact" />
</v-col>
</v-row>
@@ -162,7 +160,7 @@
<v-col>{{ $t('pages.inbounds') }}</v-col>
<v-col dir="ltr">
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="item.inbounds != ''">
<span v-for="i in item.inbounds">{{ i }}<br /></span>
<span v-for="i in item.inbounds">{{ inbounds.find(inb => inb.id == i)?.tag }}<br /></span>
</v-tooltip>
{{ item.inbounds.length }}
</v-col>
@@ -230,7 +228,7 @@
<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-if="v2rayStats.users.includes(item.name)">
<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>
@@ -324,7 +322,7 @@
>
mdi-qrcode
</v-icon>
<v-icon icon="mdi-chart-line" @click="showStats(item.name)" v-if="v2rayStats.users.includes(item.name)">
<v-icon icon="mdi-chart-line" @click="showStats(item.name)">
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-icon>
</template>
@@ -349,8 +347,7 @@ import QrCode from '@/layouts/modals/QrCode.vue'
import Stats from '@/layouts/modals/Stats.vue'
import { Client, createClient } from '@/types/clients'
import { computed, ref } from 'vue'
import { Config, V2rayApiStats } from '@/types/config'
import { InTypes, Inbound,InboundWithUser, ShadowTLS, VLESS } from '@/types/inbounds'
import { Inbound, inboundWithUsers } from '@/types/inbounds'
import { Link, LinkUtil } from '@/plugins/link'
import { HumanReadable } from '@/plugins/utils'
import { i18n } from '@/locales'
@@ -367,21 +364,13 @@ const isOnline = (cname: string) => computed(() => {
return Data().onlines?.user ? Data().onlines.user.includes(cname) : false
})
const appConfig = computed((): Config => {
return <Config> Data().config
})
const v2rayStats = computed((): V2rayApiStats => {
return <V2rayApiStats> appConfig.value.experimental.v2ray_api.stats
})
const inbounds = computed((): Inbound[] => {
return <Inbound[]> appConfig.value?.inbounds
return <Inbound[]> Data().inbounds?? []
})
const inboundTags = computed((): string[] => {
const inboundTags = computed((): any[] => {
if (!inbounds.value) return []
return inbounds.value?.filter(i => i.tag != "" && Object.hasOwn(i,'users')).map(i => i.tag)
return inbounds.value?.filter(i => i.tag != "" && inboundWithUsers.includes(i.type)).map(i => { return { title: i.tag, value: i.id } })
})
const groups = computed((): string[] => {
@@ -430,102 +419,44 @@ const groupBy = [
const modal = ref({
visible: false,
index: -1,
id: 0,
data: "",
stats: false,
})
const delOverlay = ref(new Array<boolean>(clients.value.length).fill(false))
const showModal = (id: number) => {
const index = id == -1 ? -1 : clients.value.findIndex(c => c.id == id)
modal.value.index = index
modal.value.data = index == -1 ? '' : JSON.stringify(clients.value[index])
modal.value.stats = index == -1 ? false : v2rayStats.value.users.includes(clients.value[index].name)
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 = () => {
modal.value.visible = false
}
const saveModal = (data:any, stats:boolean) => {
const saveModal = async (data:any) => {
// Check duplicate name
const oldName = modal.value.index != -1 ? clients.value[modal.value.index].name : null
const oldName = modal.value.id > 0 ? clients.value.findLast(i => i.id == modal.value.id)?.name : null
if (data.name != oldName && clients.value.findIndex(c => c.name == data.name) != -1) {
push.error({
message: i18n.global.t('error.dplData') + ": " + i18n.global.t('client.name')
})
return
}
if(modal.value.index == -1) {
clients.value.push(data)
} else {
clients.value[modal.value.index] = data
}
// Rebuild affected inbounds
buildInboundsUsers(data.inbounds)
// Rebuild links
data.links = updateLinks(data)
const clientInbounds = data.inbounds.length == 0 ? [] : await Data().loadInbounds(data.inbounds)
data.links = updateLinks(data, clientInbounds)
// Set Client Stats
const sIndex = v2rayStats.value.users.findIndex(i => i == data.name) // Find if new user exists
if (oldName != data.name) {
v2rayStats.value.users = v2rayStats.value.users.filter(item => item != oldName)
}
if (stats) {
// Add if dos not exist
if (data.name.length>0 && sIndex == -1) v2rayStats.value.users.push(data.name)
} else {
// Delete if exists
if (sIndex != -1) v2rayStats.value.users.splice(sIndex,1)
}
modal.value.visible = false
// save data
const success = await Data().save("clients", modal.value.id == 0 ? "new" : "edit", data)
if (success) modal.value.visible = false
}
const buildInboundsUsers = (inboundTags:string[]) => {
inboundTags.forEach(tag => {
const inbound_index = inbounds.value.findIndex(i => i.tag == tag)
if (inbound_index != -1){
const users = <any>[]
const newInbound = <InboundWithUser>inbounds.value[inbound_index]
const inboundClients = clients.value.filter(c => c.enable && c.inbounds.includes(tag))
inboundClients.forEach(c => {
// Remove flow in non tls VLESS
if (newInbound.type == InTypes.VLESS) {
const vlessInbound = <VLESS>newInbound
if (!vlessInbound.tls?.enabled || vlessInbound.transport?.type) delete(c.config?.vless?.flow)
}
users.push(c.config[newInbound.type])
})
newInbound.users = users
// Exceptions for Naive and ShadowTLSv3
if (users.length == 0){
if (newInbound.type == InTypes.Naive) {
newInbound.users = <any>[{}]
} else {
if (newInbound.type == InTypes.ShadowTLS){
const ssTls = <ShadowTLS>newInbound
if (ssTls.version == 3) newInbound.users = <any>[{}]
}
}
}
inbounds.value[inbound_index] = newInbound
}
})
}
const updateLinks = (c:Client):Link[] => {
const clientInbounds = <Inbound[]>inbounds.value.filter(i => c.inbounds.includes(i.tag))
const updateLinks = (c:Client, clientInbounds:Inbound[]):Link[] => {
const newLinks = <Link[]>[]
clientInbounds.forEach(i =>{
const tlsConfig = <any>Data().tlsConfigs?.findLast((t:any) => t.inbounds.includes(i.tag))
const cData = <any>Data().inData?.findLast((d:any) => d.tag == i.tag)
const addrs = cData ? <any[]>cData.addrs : []
const uris = LinkUtil.linkGenerator(c,i, tlsConfig?.client?? {}, addrs)
const tls = i.tls_id && i.tls_id>0 ? Data().tlsConfigs?.findLast((t:any) => t.id == i.tls_id) : undefined
const uris = LinkUtil.linkGenerator(c,i, tls, i.addrs)
if (uris.length>0){
uris.forEach(uri => {
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
@@ -537,21 +468,10 @@ const updateLinks = (c:Client):Link[] => {
return links
}
const delClient = (id: number) => {
const clientIndex = clients.value.findIndex(c => c.id === id)
const oldData = createClient(clients.value[clientIndex])
// Delete stats if exists and will be orphaned
const tagCounts = clients.value.filter(i => i.name == oldData.name).length
const sIndex = v2rayStats.value.users.findIndex(i => i == oldData.name)
if (tagCounts == 1 && sIndex != -1){
v2rayStats.value.users.splice(sIndex,1)
}
clients.value.splice(clientIndex,1)
buildInboundsUsers(oldData.inbounds)
if (id>0) Data().delClient(id)
delOverlay.value[clientIndex] = false
const delClient = async (id: number) => {
const index = clients.value.findIndex(c => c.id === id)
const success = await Data().save("clients", "del", id)
if (success) delOverlay.value[index] = false
}
const qrcode = ref({
@@ -636,14 +556,12 @@ const closeBulk = () => {
addBulkModal.value = false
}
const saveBulk = (bulkClients: Client[], clientInbounds: string[], clientStats: boolean) => {
const saveBulk = async (bulkClients: Client[], clientInbounds: number[]) => {
const inboundData = clientInbounds.length == 0 ? [] : await Data().loadInbounds(clientInbounds)
bulkClients.forEach((c,c_index) => {
bulkClients[c_index].links = updateLinks(c)
bulkClients[c_index].links = updateLinks(c, inboundData)
})
clients.value.push(...bulkClients)
buildInboundsUsers(clientInbounds)
// Stats
if (clientStats) v2rayStats.value.users.push(...bulkClients.map(bc => bc.name))
closeBulk()
}
</script>
+166
View File
@@ -0,0 +1,166 @@
<template>
<EndpointVue
v-model="modal.visible"
:visible="modal.visible"
:id="modal.id"
:data="modal.data"
:tags="endpointTags"
@close="closeModal"
@save="saveModal"
/>
<Stats
v-model="stats.visible"
:visible="stats.visible"
:resource="stats.resource"
:tag="stats.tag"
@close="closeStats"
/>
<v-row>
<v-col cols="12" justify="center" align="center">
<v-btn color="primary" @click="showModal(0)">{{ $t('actions.add') }}</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>endpoints" :key="item.tag">
<v-card rounded="xl" elevation="5" min-width="200" :title="item.tag">
<v-card-subtitle style="margin-top: -20px;">
<v-row>
<v-col>{{ item.type }}</v-col>
</v-row>
</v-card-subtitle>
<v-card-text>
<v-row>
<v-col>{{ $t('in.addr') }}</v-col>
<v-col dir="ltr">
{{ item.address?.length>0 ? item.address[0] : '-' }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('in.port') }}</v-col>
<v-col dir="ltr">
{{ item.listen_port?? '-' }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('types.wg.peers') }}</v-col>
<v-col dir="ltr">
{{ item.peers.length?? '-' }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('online') }}</v-col>
<v-col dir="ltr">
<template v-if="onlines.includes(item.tag)">
<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-file-edit" @click="showModal(item.id)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
</v-btn>
<v-btn icon="mdi-file-remove" style="margin-inline-start:0;" color="warning" @click="delOverlay[index] = true">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.del')"></v-tooltip>
</v-btn>
<v-overlay
v-model="delOverlay[index]"
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="delEndpoint(item.tag)">{{ $t('yes') }}</v-btn>
<v-btn color="success" variant="outlined" @click="delOverlay[index] = false">{{ $t('no') }}</v-btn>
</v-card-actions>
</v-card>
</v-overlay>
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-btn>
</v-card-actions>
</v-card>
</v-col>
</v-row>
</template>
<script lang="ts" setup>
import Data from '@/store/modules/data'
import EndpointVue from '@/layouts/modals/Endpoint.vue'
import Stats from '@/layouts/modals/Stats.vue'
import { Endpoint } from '@/types/endpoints';
import { computed, ref } from 'vue'
import { i18n } from '@/locales';
import { push } from 'notivue';
const endpoints = computed((): Endpoint[] => {
return <Endpoint[]> Data().endpoints
})
const endpointTags = computed((): any[] => {
return endpoints.value?.map((o:Endpoint) => o.tag)
})
const onlines = computed(() => {
return [...Data().onlines.inbound?? [], ...Data().onlines.outbound??[] ]
})
const modal = ref({
visible: false,
id: 0,
data: "",
})
let delOverlay = ref(new Array<boolean>)
const showModal = (id: number) => {
modal.value.id = id
modal.value.data = id == 0 ? '' : JSON.stringify(endpoints.value.findLast(o => o.id == id))
modal.value.visible = true
}
const closeModal = () => {
modal.value.visible = false
}
const saveModal = async (data:Endpoint) => {
// Check duplicate tag
const oldTag = modal.value.id > 0 ? endpoints.value.findLast(i => i.id == modal.value.id)?.tag : null
if (data.tag != oldTag && endpointTags.value.includes(data.tag)) {
push.error({
message: i18n.global.t('error.dplData') + ": " + i18n.global.t('objects.tag')
})
return
}
// save data
const success = await Data().save("endpoints", modal.value.id == 0 ? "new" : "edit", data)
if (success) modal.value.visible = false
}
const stats = ref({
visible: false,
resource: "endpoint",
tag: "",
})
const delEndpoint = async (tag: string) => {
const index = endpoints.value.findIndex(i => i.tag == tag)
const success = await Data().save("endpoints", "del", tag)
if (success) delOverlay.value[index] = false
}
const showStats = (tag: string) => {
stats.value.tag = tag
stats.value.visible = true
}
const closeStats = () => {
stats.value.visible = false
}
</script>
+82 -201
View File
@@ -2,10 +2,7 @@
<InboundVue
v-model="modal.visible"
:visible="modal.visible"
:index="modal.index"
:stats="modal.stats"
:data="modal.data"
:cData="modal.cData"
:id="modal.id"
:inTags="inTags"
:outTags="outTags"
:tlsConfigs="tlsConfigs"
@@ -21,7 +18,7 @@
/>
<v-row>
<v-col cols="12" justify="center" align="center">
<v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn>
<v-btn color="primary" @click="showModal(0)">{{ $t('actions.add') }}</v-btn>
</v-col>
</v-row>
<v-row>
@@ -48,22 +45,25 @@
<v-row>
<v-col>{{ $t('objects.tls') }}</v-col>
<v-col dir="ltr">
{{ Object.hasOwn(item,'tls') ? $t(item.tls?.enabled ? 'enable' : 'disable') : '-' }}
{{ item.tls_id > 0 ? $t('enable') : $t('disable') }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('pages.clients') }}</v-col>
<v-col dir="ltr">
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="Object.hasOwn(item,'users')">
<span v-for="u in findInbounsUsers(item)">{{ u }}<br /></span>
</v-tooltip>
{{ Array.isArray(item.users) ? item.users.length : '-' }}
<template v-if="inboundWithUsers.includes(item.tag)">
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="findInboundUsers(item.tag).length > 0">
<span v-for="u in findInboundUsers(item.tag)">{{ u }}<br /></span>
</v-tooltip>
{{ findInboundUsers(item.tag).length }}
</template>
<template v-else>-</template>
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('online') }}</v-col>
<v-col dir="ltr">
<template v-if="onlines[index]">
<template v-if="onlines.includes(item.tag)">
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
</template>
<template v-else>-</template>
@@ -72,7 +72,7 @@
</v-card-text>
<v-divider></v-divider>
<v-card-actions style="padding: 0;">
<v-btn icon="mdi-file-edit" @click="showModal(index)">
<v-btn icon="mdi-file-edit" @click="showModal(item.id)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
</v-btn>
@@ -89,12 +89,12 @@
<v-divider></v-divider>
<v-card-text>{{ $t('confirm') }}</v-card-text>
<v-card-actions>
<v-btn color="error" variant="outlined" @click="delInbound(index)">{{ $t('yes') }}</v-btn>
<v-btn color="error" variant="outlined" @click="delInbound(item.id)">{{ $t('yes') }}</v-btn>
<v-btn color="success" variant="outlined" @click="delOverlay[index] = false">{{ $t('no') }}</v-btn>
</v-card-actions>
</v-card>
</v-overlay>
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)" v-if="v2rayStats.inbounds.includes(item.tag)">
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-btn>
@@ -108,9 +108,9 @@
import Data from '@/store/modules/data'
import InboundVue from '@/layouts/modals/Inbound.vue'
import Stats from '@/layouts/modals/Stats.vue'
import { Config, V2rayApiStats } from '@/types/config'
import { computed, ref } from 'vue'
import { InTypes, Inbound, InboundWithUser, ShadowTLS, VLESS } from '@/types/inbounds'
import { Config } from '@/types/config'
import { computed, onMounted, ref } from 'vue'
import { Inbound, inboundWithUsers } from '@/types/inbounds'
import { Client } from '@/types/clients'
import { Link, LinkUtil } from '@/plugins/link'
import { i18n } from '@/locales'
@@ -122,17 +122,13 @@ const appConfig = computed((): Config => {
})
const inbounds = computed((): Inbound[] => {
return <Inbound[]> appConfig.value.inbounds
return <Inbound[]> Data().inbounds
})
const tlsConfigs = computed((): any[] => {
return <any[]> Data().tlsConfigs
})
const inData = computed((): any[] => {
return <any[]> Data().inData
})
const inTags = computed((): string[] => {
return inbounds.value?.map(i => i.tag)
})
@@ -149,216 +145,101 @@ const onlines = computed(() => {
return Data().onlines.inbound ? inbounds.value.map(i => Data().onlines.inbound.includes(i.tag)) : []
})
const v2rayStats = computed((): V2rayApiStats => {
return <V2rayApiStats> appConfig.value.experimental?.v2ray_api.stats
})
const modal = ref({
visible: false,
index: -1,
data: "",
cData: "",
stats: false,
id: 0,
})
let delOverlay = ref(new Array<boolean>)
const showModal = (index: number) => {
modal.value.index = index
if (index == -1){
modal.value.data = ''
modal.value.cData = ''
modal.value.stats = false
} else {
modal.value.data = JSON.stringify(inbounds.value[index])
modal.value.stats = v2rayStats.value.inbounds.includes(inbounds.value[index].tag)
const inDataIndex = inData.value.findIndex(d => d.tag == inbounds.value[index].tag)
modal.value.cData = inDataIndex == -1 ? '' : JSON.stringify(inData.value[inDataIndex])
}
const showModal = (id: number) => {
modal.value.id = id
modal.value.visible = true
}
const closeModal = () => {
modal.value.visible = false
}
const saveModal = (data:Inbound, stats: boolean, tls_id: number, cData: any) => {
const saveModal = async (data:Inbound) => {
// Check duplicate tag
const oldTag = modal.value.index != -1 ? inbounds.value[modal.value.index].tag : null
if (data.tag != oldTag && inTags.value.includes(data.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)) {
push.error({
message: i18n.global.t('error.dplData') + ": " + i18n.global.t('objects.tag')
})
return
}
if (cData.id != -1) {
cData.tag = data.tag
fillData(cData.outJson, data,tls_id>0 ? tlsConfigs.value.findLast(t => t.id == tls_id).client : {})
// Fill outjson
if (data.out_json){
fillData(data, data.tls_id > 0 ? tlsConfigs?.value.findLast((t:any) => t.id == data.tls_id) : null)
}
// New or Edit
if (modal.value.index == -1) {
inbounds.value.push(data)
if (stats && data.tag.length>0) {
v2rayStats.value.inbounds.push(data.tag)
}
if (cData.id != -1){
inData.value.push(cData)
}
} else {
const oldTag = inbounds.value[modal.value.index].tag
const sIndex = v2rayStats.value.inbounds.findIndex(i => i == data.tag) // Find if new tag exists
// Update tls preset
const oldTlsConfigIndex = tlsConfigs?.value.findIndex(t => t.inbounds?.includes(oldTag))
if (oldTlsConfigIndex != -1){
tlsConfigs.value[oldTlsConfigIndex].inbounds = tlsConfigs?.value[oldTlsConfigIndex].inbounds.filter((i:string) => i != oldTag)
}
if (oldTag != data.tag) {
v2rayStats.value.inbounds = v2rayStats.value.inbounds.filter(item => item != oldTag)
changeClientInboundsTag(oldTag,data.tag)
}
if (stats) {
// Add if dos not exist
if (data.tag.length>0 && sIndex == -1) v2rayStats.value.inbounds.push(data.tag)
} else {
// Delete if exists
if (sIndex != -1) v2rayStats.value.inbounds.splice(sIndex,1)
}
inbounds.value[modal.value.index] = data
const inDataIndex = inData.value.findIndex(indata => indata.tag == oldTag)
if (cData.id != -1) {
if (inDataIndex == -1){
inData.value.push(cData)
} else {
inData.value[inDataIndex] = cData
}
} else if (inDataIndex != -1) {
Data().delInData(inData.value[inDataIndex].id)
inData.value.splice(inDataIndex,1)
}
}
// Update tls preset
if (tls_id>0) {
tlsConfigs.value.findLast(t => t.id == tls_id).inbounds.push(data.tag)
tlsConfigs.value.sort()
let userLinkDiff = []
// Update links
if (data.id > 0 && oldInbound != null) {
userLinkDiff = updateLinks(data,oldInbound)
}
if (Object.hasOwn(data,'users')) {
// Set users
data = buildInboundsUsers(data)
// Update links
updateLinks(data)
}
modal.value.visible = false
// save data
const success = await Data().save("inbounds", modal.value.id == 0 ? "new" : "edit", data, userLinkDiff)
if (success) modal.value.visible = false
}
const updateLinks = (i: any) => {
if(i.users){
const uClients = clients.value.filter(c => c.inbounds.includes(i.tag))
uClients.forEach((u:Client) => {
const clientInbounds = <Inbound[]>inbounds.value.filter(inb => u.inbounds.includes(inb.tag))
const newLinks = <Link[]>[]
clientInbounds.forEach(i =>{
const tlsClient = tlsConfigs?.value.findLast((t:any) => t.inbounds.includes(i.tag))?.client?? {}
const cData = <any>Data().inData?.findLast((d:any) => d.tag == i.tag)
const addrs = cData ? <any[]>cData.addrs : []
const uris = LinkUtil.linkGenerator(u,i, tlsClient, addrs)
if (uris.length>0){
uris.forEach(uri => {
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
})
}
})
let links = u.links && u.links.length>0? u.links : <Link[]>[]
links = [...newLinks, ...links.filter(l => l.type != 'local')]
const updateLinks = (i: Inbound, o: Inbound): any[] => {
let diff = <any[]>[]
const uClients = clients.value.filter(c => c.inbounds.includes(i.id))
if (uClients.length == 0) return diff
u.links = links
if (inboundWithUsers.includes(o.type) && !inboundWithUsers.includes(i.type)){
// Remove old inbound links if new type does not support users
uClients.forEach((u:Client) => {
u.inbounds = u.inbounds.filter(i => i != o.id)
const otherLocalLinks = u.links.filter(l => l.type == 'local' && l.remark != o.tag)
let links = u.links && u.links.length>0? u.links : <Link[]>[]
links = [...otherLocalLinks, ...links.filter(l => l.type != 'local')]
diff.push({ id: u.id, links: links, inbounds: u.inbounds })
})
} else if(inboundWithUsers.includes(i.type)){
// Add new inbound links if new type supports users
const tls = tlsConfigs?.value.findLast((t:any) => t.id == i.tls_id)
uClients.forEach((u:Client) => {
const otherLocalLinks = u.links.filter(l => l.type == 'local' && l.remark != i.tag)
const uris = LinkUtil.linkGenerator(u,i, tls, i.addrs)
let newLinks = <Link[]>[]
if (uris.length>0){
uris.forEach(uri => {
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
})
}
let links = u.links && u.links.length>0? u.links : <Link[]>[]
links = [...otherLocalLinks, ...newLinks, ...links.filter(l => l.type != 'local')]
diff.push({ id: u.id, links: links, inbounds: u.inbounds })
})
}
return diff
}
const delInbound = (index: number) => {
const delInbound = async (id: number) => {
const index = inbounds.value.findIndex(i => i.id == id)
const inb = inbounds.value[index]
inbounds.value.splice(index,1)
const tag = inb.tag
if (Object.hasOwn(inb,'users')) {
const inbU = <InboundWithUser>inb
if (inbU.users && inbU.users.length>0){
inbU.users.forEach((u:any) => {
const c_index = clients.value.findIndex(c => u.username? u.username == c.name : u.name == c.name)
if (c_index != -1) {
clients.value[c_index].inbounds = clients.value[c_index].inbounds.filter((x:string) => x!=tag)
clients.value[c_index].links = clients.value[c_index].links.filter((x:any) => x.remark!=tag)
}
})
}
}
// Delete binded tls if exists
if (Object.hasOwn(inb,'tls')) {
const oldTlsConfigIndex = tlsConfigs?.value.findIndex(t => t.inbounds?.includes(inb.tag))
if (oldTlsConfigIndex != -1){
tlsConfigs.value[oldTlsConfigIndex].inbounds = tlsConfigs?.value[oldTlsConfigIndex].inbounds.filter((i:string) => i != inb.tag)
}
}
// Delete stats if exists and will be orphaned
const tagCounts = inbounds.value.filter(i => i.tag == inb.tag).length
const sIndex = v2rayStats.value.inbounds.findIndex(i => i == inb.tag)
if (tagCounts == 1 && sIndex != -1){
v2rayStats.value.inbounds.splice(sIndex,1)
}
if (index < Data().oldData.config.inbounds.length){
Data().delInbound(index)
} else {
// Delete new inbound's inData if exists
const inDataIndex = Data().inData.findIndex((d:any) => d.tag == tag)
if (inDataIndex != -1) Data().inData.splice(inDataIndex, 1)
}
delOverlay.value[index] = false
}
const buildInboundsUsers = (inbound:any):Inbound => {
const users = <any>[]
const inboundClients = clients.value.filter(c => c.enable && c.inbounds.includes(inbound.tag))
inboundClients.forEach(c => {
// Remove flow in non tls VLESS
if (inbound.type == InTypes.VLESS) {
const vlessInbound = <VLESS>inbound
if (!vlessInbound.tls?.enabled || vlessInbound.transport?.type) delete(c.config?.vless?.flow)
}
users.push(c.config[inbound.type])
})
inbound.users = users
// Exceptions for Naive and ShadowTLSv3
if (users.length == 0){
if (inbound.type == InTypes.Naive){
inbound.users = <any>[{}]
} else {
if (inbound.type == InTypes.ShadowTLS){
const ssTls = <ShadowTLS>inbound
if (ssTls.version == 3) inbound.users = <any>[{}]
}
}
}
return <Inbound>inbound
}
const changeClientInboundsTag = (oldtag: string, newTag:string) => {
clients.value.forEach((c, c_index) => {
const inbound_index = c.inbounds.findIndex(i => i == oldtag)
if (inbound_index != -1) {
c.inbounds[inbound_index] = newTag
clients.value[c_index].inbounds = c.inbounds
}
let diff = <any[]>[]
// delete inbound in client table
const inboundClients = clients.value.filter(c => c.inbounds.includes(id))
inboundClients.forEach((c:Client) => {
c.inbounds = c.inbounds.filter((x:number) => x!=id)
c.links = c.links.filter((x:any) => x.remark!=tag)
diff.push({ id: c.id, links: c.links, inbounds: c.inbounds })
})
}
const findInbounsUsers = (inbound: InboundWithUser): string[] => {
if (inbound.users === null || !Array.isArray(inbound.users) || inbound.users.length == 0) return []
const users = inbound.users.map(user => "username" in user ? user.username : user.name)
return users
const success = await Data().save("inbounds", "del", tag, diff)
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({
+25 -56
View File
@@ -3,7 +3,6 @@
v-model="modal.visible"
:visible="modal.visible"
:id="modal.id"
:stats="modal.stats"
:data="modal.data"
:tags="outboundTags"
@close="closeModal"
@@ -18,7 +17,7 @@
/>
<v-row>
<v-col cols="12" justify="center" align="center">
<v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn>
<v-btn color="primary" @click="showModal(0)">{{ $t('actions.add') }}</v-btn>
</v-col>
</v-row>
<v-row>
@@ -51,7 +50,7 @@
<v-row>
<v-col>{{ $t('online') }}</v-col>
<v-col dir="ltr">
<template v-if="onlines[index]">
<template v-if="onlines.includes(item.tag)">
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
</template>
<template v-else>-</template>
@@ -60,7 +59,7 @@
</v-card-text>
<v-divider></v-divider>
<v-card-actions style="padding: 0;">
<v-btn icon="mdi-file-edit" @click="showModal(index)">
<v-btn icon="mdi-file-edit" @click="showModal(item.id)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
</v-btn>
@@ -77,12 +76,12 @@
<v-divider></v-divider>
<v-card-text>{{ $t('confirm') }}</v-card-text>
<v-card-actions>
<v-btn color="error" variant="outlined" @click="delOutbound(index)">{{ $t('yes') }}</v-btn>
<v-btn color="error" variant="outlined" @click="delOutbound(item.tag)">{{ $t('yes') }}</v-btn>
<v-btn color="success" variant="outlined" @click="delOverlay[index] = false">{{ $t('no') }}</v-btn>
</v-card-actions>
</v-card>
</v-overlay>
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)" v-if="v2rayStats.outbounds.includes(item.tag)">
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-btn>
@@ -96,79 +95,56 @@
import Data from '@/store/modules/data'
import OutboundVue from '@/layouts/modals/Outbound.vue'
import Stats from '@/layouts/modals/Stats.vue'
import { Config, V2rayApiStats } from '@/types/config';
import { Outbound } from '@/types/outbounds';
import { computed, ref } from 'vue'
import { i18n } from '@/locales';
import { push } from 'notivue';
const appConfig = computed((): Config => {
return <Config> Data().config
})
const outbounds = computed((): Outbound[] => {
return <Outbound[]> appConfig.value.outbounds
return <Outbound[]> Data().outbounds
})
const outboundTags = computed((): string[] => {
const outboundTags = computed((): any[] => {
return outbounds.value?.map((o:Outbound) => o.tag)
})
const onlines = computed(() => {
return Data().onlines.outbound ? outbounds.value.map(i => Data().onlines.outbound.includes(i.tag)) : []
})
const v2rayStats = computed((): V2rayApiStats => {
return <V2rayApiStats> appConfig.value.experimental?.v2ray_api.stats
return Data().onlines.outbound?? []
})
const modal = ref({
visible: false,
id: -1,
id: 0,
data: "",
stats: false,
})
let delOverlay = ref(new Array<boolean>)
const showModal = (id: number) => {
modal.value.id = id
modal.value.data = id == -1 ? '' : JSON.stringify(outbounds.value[id])
modal.value.stats = id == -1 ? false : v2rayStats.value.outbounds.includes(outbounds.value[id].tag)
modal.value.data = id == 0 ? '' : JSON.stringify(outbounds.value.findLast(o => o.id == id))
modal.value.visible = true
}
const closeModal = () => {
modal.value.visible = false
}
const saveModal = (data:Outbound, stats: boolean) => {
const saveModal = async (data:Outbound) => {
// Check duplicate tag
const oldTag = modal.value.id != -1 ? outbounds.value[modal.value.id].tag : null
const oldTag = modal.value.id > 0 ? outbounds.value.findLast(i => i.id == modal.value.id)?.tag : null
if (data.tag != oldTag && outboundTags.value.includes(data.tag)) {
push.error({
message: i18n.global.t('error.dplData') + ": " + i18n.global.t('objects.tag')
})
return
}
// New or Edit
if (modal.value.id == -1) {
outbounds.value.push(data)
if (stats && data.tag.length>0) {
v2rayStats.value.outbounds.push(data.tag)
}
} else {
const sIndex = v2rayStats.value.outbounds.findIndex(i => i == data.tag) // Find if new tag exists
if (stats) {
// Add if dos not exist
if (data.tag.length>0 && sIndex == -1) v2rayStats.value.outbounds.push(data.tag)
} else {
// Delete if exists
if (sIndex != -1) v2rayStats.value.outbounds.splice(sIndex,1)
}
outbounds.value[modal.value.id] = data
// save data
const success = await Data().save("outbounds", modal.value.id == 0 ? "new" : "edit", data)
if (!success) {
return
}
modal.value.visible = false
}
@@ -178,21 +154,10 @@ const stats = ref({
tag: "",
})
const delOutbound = (index: number) => {
const inb = outbounds.value[index]
outbounds.value.splice(index,1)
const tag = inb.tag
// Delete stats if exists and will be orphaned
const tagCounts = outbounds.value.filter(i => i.tag == inb.tag).length
const sIndex = v2rayStats.value.outbounds.findIndex(i => i == inb.tag)
if (tagCounts == 1 && sIndex != -1){
v2rayStats.value.outbounds.splice(sIndex,1)
}
if (index < Data().oldData.config.outbounds.length){
Data().delOutbound(index)
}
delOverlay.value[index] = false
const delOutbound = async (tag: string) => {
const index = outbounds.value.findIndex(i => i.tag == tag)
const success = await Data().save("outbounds", "del", tag)
if (success) delOverlay.value[index] = false
}
const showStats = (tag: string) => {
@@ -202,4 +167,8 @@ const showStats = (tag: string) => {
const closeStats = () => {
stats.value.visible = false
}
function awaitData() {
throw new Error('Function not implemented.');
}
</script>
+29 -5
View File
@@ -24,6 +24,9 @@
<v-col cols="12" justify="center" align="center">
<v-btn color="primary" @click="showRuleModal(-1)" style="margin: 0 5px;">{{ $t('rule.add') }}</v-btn>
<v-btn color="primary" @click="showRulesetModal(-1)" style="margin: 0 5px;">{{ $t('ruleset.add') }}</v-btn>
<v-btn variant="outlined" color="warning" @click="saveConfig" :loading="loading" :disabled="stateChange">
{{ $t('actions.save') }}
</v-btn>
</v-col>
</v-row>
<v-row>
@@ -144,16 +147,37 @@
<script lang="ts" setup>
import Data from '@/store/modules/data'
import { computed, ref } from 'vue'
import { computed, ref, onMounted } from 'vue'
import RuleVue from '@/layouts/modals/Rule.vue'
import RulesetVue from '@/layouts/modals/Ruleset.vue'
import { Config } from '@/types/config'
import { logicalRule, ruleset } from '@/types/rules'
import { FindDiff } from '@/plugins/utils'
const oldConfig = ref({})
const loading = ref(false)
const appConfig = computed((): Config => {
return <Config> Data().config
})
onMounted(async () => {
oldConfig.value = JSON.parse(JSON.stringify(Data().config))
})
const stateChange = computed(() => {
return FindDiff.deepCompare(appConfig.value,oldConfig.value)
})
const saveConfig = async () => {
loading.value = true
const success = await Data().save("config", "set", appConfig.value)
if (success) {
oldConfig.value = JSON.parse(JSON.stringify(Data().config))
loading.value = false
}
}
const clients = computed((): string[] => {
return Data().clients.map((c:any) => c.name)
})
@@ -189,11 +213,11 @@ const rulesetTags = computed((): any[] => {
})
const outboundTags = computed((): string[] => {
return appConfig.value.outbounds?.map((o:any) => o.tag)
return [...Data().outbounds?.map((o:any) => o.tag), ...Data().endpoints?.map((e:any) => e.tag)]
})
const inboundTags = computed((): string[] => {
return appConfig.value.inbounds?.map((i:any) => i.tag)
return [...Data().inbounds?.map((o:any) => o.tag), ...Data().endpoints?.map((e:any) => e.tag)]
})
let delRuleOverlay = ref(new Array<boolean>)
@@ -268,7 +292,7 @@ const draggedItemIndex = ref(null);
const onDragStart = (index: any) => {
draggedItemIndex.value = index;
};
}
const onDrop = (index: any) => {
if (draggedItemIndex.value !== null) {
@@ -278,5 +302,5 @@ const onDrop = (index: any) => {
rules.value.splice(index, 0, draggedItem);
draggedItemIndex.value = null;
}
};
}
</script>
+82 -71
View File
@@ -2,19 +2,19 @@
<TlsVue
v-model="modal.visible"
:visible="modal.visible"
:index="modal.index"
:id="modal.id"
:data="modal.data"
@close="closeModal"
@save="saveModal"
/>
<v-row>
<v-col cols="12" justify="center" align="center">
<v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn>
<v-btn color="primary" @click="showModal(0)">{{ $t('actions.add') }}</v-btn>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>tlsConfigs" :key="item.id">
<v-card rounded="xl" elevation="5" min-width="200" :title="(item.id? item.id + '. ' : '*') + item.name">
<v-card rounded="xl" elevation="5" min-width="200" :title="item.name">
<v-card-subtitle style="margin-top: -20px;">
{{ item.server?.server_name?.length>0 ? item.server.server_name : "-" }}
</v-card-subtitle>
@@ -22,10 +22,13 @@
<v-row>
<v-col>{{ $t('pages.inbounds') }}</v-col>
<v-col dir="ltr">
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="item.inbounds?.length>0">
<span v-for="i in item.inbounds">{{ i }}<br /></span>
</v-tooltip>
{{ item.inbounds?.length }}
<template v-if="tlsInbounds(item.id).length>0">
<v-tooltip activator="parent" dir="ltr" location="bottom">
<span v-for="i in tlsInbounds(item.id)">{{ i }}<br /></span>
</v-tooltip>
{{ tlsInbounds.length }}
</template>
<template v-else>-</template>
</v-col>
</v-row>
<v-row>
@@ -49,11 +52,11 @@
</v-card-text>
<v-divider></v-divider>
<v-card-actions style="padding: 0;">
<v-btn icon="mdi-file-edit" @click="showModal(index)">
<v-btn icon="mdi-file-edit" @click="showModal(item.id)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
</v-btn>
<v-btn v-if="item.inbounds?.length == 0" icon="mdi-file-remove" style="margin-inline-start:0;" color="warning" @click="delOverlay[index] = true">
<v-btn v-if="tlsInbounds(item.id).length == 0" icon="mdi-file-remove" style="margin-inline-start:0;" color="warning" @click="delOverlay[index] = true">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.del')"></v-tooltip>
</v-btn>
@@ -66,12 +69,12 @@
<v-divider></v-divider>
<v-card-text>{{ $t('confirm') }}</v-card-text>
<v-card-actions>
<v-btn color="error" variant="outlined" @click="delTls(index)">{{ $t('yes') }}</v-btn>
<v-btn color="error" variant="outlined" @click="delTls(item.id)">{{ $t('yes') }}</v-btn>
<v-btn color="success" variant="outlined" @click="delOverlay[index] = false">{{ $t('no') }}</v-btn>
</v-card-actions>
</v-card>
</v-overlay>
<v-btn icon="mdi-content-duplicate" @click="clone(index)">
<v-btn icon="mdi-content-duplicate" @click="clone(item)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.clone')"></v-tooltip>
</v-btn>
@@ -85,8 +88,7 @@
import TlsVue from '@/layouts/modals/Tls.vue'
import Data from '@/store/modules/data'
import { computed, ref } from 'vue'
import { Config } from '@/types/config'
import { Inbound } from '@/types/inbounds'
import { Inbound, inboundWithUsers } from '@/types/inbounds'
import { Client } from '@/types/clients'
import { Link, LinkUtil } from '@/plugins/link'
import { fillData } from '@/plugins/outJson'
@@ -95,13 +97,13 @@ const tlsConfigs = computed((): any[] => {
return Data().tlsConfigs
})
const inbounds = computed((): any[] => {
return <any[]>(<Config>Data().config)?.inbounds
const inbounds = computed((): Inbound[] => {
return Data().inbounds
})
const inData = computed((): any[] => {
return <any[]> Data().inData
})
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
@@ -109,21 +111,20 @@ const clients = computed((): any[] => {
const modal = ref({
visible: false,
index: -1,
id: 0,
data: "",
})
const delOverlay = ref(new Array<boolean>(tlsConfigs.value.length).fill(false))
const showModal = (index: number) => {
modal.value.index = index
modal.value.data = index == -1 ? '{}' : JSON.stringify(tlsConfigs.value[index])
const showModal = (id: number) => {
modal.value.id = id
modal.value.data = id == 0 ? '{}' : JSON.stringify(tlsConfigs.value.findLast(t => t.id == id))
modal.value.visible = true
}
const clone = (index: number) => {
let data = JSON.parse(JSON.stringify(tlsConfigs.value[index]))
const clone = (obj: any) => {
let data = JSON.parse(JSON.stringify(obj))
data.id = 0
data.inbounds = []
while (tlsConfigs.value.findIndex(t => t.name == data.name) != -1){
data.name += "-copy"
}
@@ -132,57 +133,67 @@ const clone = (index: number) => {
const closeModal = () => {
modal.value.visible = false
}
const saveModal = (data:any) => {
const saveModal = async (data:any) => {
let outJsons = <any[]>[]
let userLinks = <any[]>[]
// New or Edit
if (modal.value.index == -1) {
tlsConfigs.value.push(data)
} else {
tlsConfigs.value[modal.value.index] = data
inbounds?.value.filter(i => tlsConfigs.value[modal.value.index].inbounds.includes(i.tag)).forEach(i =>{
if (i.tls != undefined) i.tls = data.server
updateInData(i,data.client)
updateLinks(i,data.client)
})
}
modal.value.visible = false
}
const delTls = (index: number) => {
if (index < Data().oldData.tlsConfigs.length){
Data().delTls(tlsConfigs.value[index].id)
}
tlsConfigs.value.splice(index,1)
delOverlay.value[index] = false
}
const updateLinks = (i:any,tlsClient:any) => {
if(i.users){
const uClients = clients.value.filter(c => c.inbounds.includes(i.tag))
uClients.forEach((client:any) => {
const clientInbounds = <Inbound[]>inbounds.value.filter(inb => client?.inbounds.includes(inb.tag))
const newLinks = <Link[]>[]
clientInbounds.forEach(i =>{
const cData = <any>Data().inData?.findLast((d:any) => d.tag == i.tag)
const addrs = cData ? <any[]>cData.addrs : []
const uris = LinkUtil.linkGenerator(client,i, tlsClient, addrs)
if (uris.length>0){
uris.forEach(uri => {
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
})
if (modal.value.id > 0) {
const inboundIds = inbounds.value.filter(i => i.tls_id == modal.value.id).map(i => i.id)
if (inboundIds.length > 0) {
const tlsInbounds = inboundIds.length == 0 ? [] : await Data().loadInbounds(inboundIds)
for (const inbound of tlsInbounds) {
// Fill outjson
if (inbound.out_json) {
fillData(inbound, data)
}
})
let links = client.links && client.links.length>0? client.links : <Link[]>[]
links = [...newLinks, ...links.filter((l:Link) => l.type != 'local')]
outJsons.push({tag: inbound.tag,out_jsons: inbound.out_json})
// Update links
const diff = updateLinks(inbound)
diff.forEach((d: any) => {
if (userLinks.findIndex(l => l.id == d.id) == -1) {
userLinks.push(d)
} else {
const index = userLinks.findIndex(l => l.id == d.id)
userLinks[index].links = d.links
}
})
}
}
client.links = links
}
const success = await Data().save("tls", data.id == 0 ? "new" : "edit", data, userLinks.length > 0 ? null: userLinks, outJsons.length > 0 ? null: outJsons)
if (success) modal.value.visible = false
}
const delTls = async (id: number) => {
const index = tlsConfigs.value.findIndex(t => t.id == id)
const success = await Data().save("tls", "del", id)
if (success) delOverlay.value[index] = false
}
const updateLinks = (i: Inbound): any[] => {
let diff = <any[]>[]
if(inboundWithUsers.includes(i.type) && i.id != 0){
const uClients = clients.value.filter(c => c.inbounds.includes(i.id))
const tlsClient = tlsConfigs?.value.findLast((t:any) => t.id == i.tls_id)
uClients.forEach((u:Client) => {
const otherLocalLinks = u.links.filter(l => l.type == 'local' && l.remark != i.tag)
const uris = LinkUtil.linkGenerator(u,i, tlsClient, i.addrs)
let newLinks = <Link[]>[]
if (uris.length>0){
uris.forEach(uri => {
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
})
}
let links = u.links && u.links.length>0? u.links : <Link[]>[]
links = [...otherLocalLinks, ...newLinks, ...links.filter(l => l.type != 'local')]
u.links = links
diff.push({ id: u.id, links: links })
})
}
return diff
}
const updateInData = (i:any, c:any) => {
const inDataIndex = inData.value.findIndex(d => d.tag == i.tag)
if (inDataIndex != -1) {
fillData(inData.value[inDataIndex].outJson, i, c)
}
}
</script>