Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ed6d49d61 | |||
| 8e369535bf | |||
| 6a5e0a940b | |||
| 3b24819309 | |||
| b5920cdc07 | |||
| cf620962bb | |||
| 17f1126c23 | |||
| 16203fdece | |||
| 12fe21906e | |||
| 1b9d5e9378 | |||
| c152a977c6 | |||
| 341baf69de | |||
| dedd4b3ee3 | |||
| bfbf9777e9 | |||
| 2cabf0aefb | |||
| c994f4b24a | |||
| f136229539 | |||
| 40fbb22b74 | |||
| 9547038164 | |||
| aca870e78f | |||
| dbf01c2086 | |||
| c3debcec5a | |||
| c179bf8a37 | |||
| 21add1f3ce | |||
| 9968f3885f | |||
| 2ac13ef8f4 | |||
| 4900c14295 | |||
| 55a6d78114 | |||
| caa115bbe3 | |||
| e3be3be9d9 | |||
| 988675a7a7 | |||
| 458f0c20da | |||
| f8fbc3c329 | |||
| 89bc3b5b23 | |||
| edfe0c86e7 | |||
| 6865c8b49d | |||
| 07947c9665 | |||
| 09616b6fac | |||
| 15105710bc |
@@ -0,0 +1 @@
|
||||
github: alireza0
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install upx -yq
|
||||
sudo apt-get update
|
||||
if [ "${{ matrix.platform }}" == "arm64" ]; then
|
||||
sudo apt install gcc-aarch64-linux-gnu
|
||||
elif [ "${{ matrix.platform }}" == "armv7" ]; then
|
||||
@@ -69,22 +69,18 @@ jobs:
|
||||
fi
|
||||
|
||||
#### Build Sing-Box
|
||||
git clone -b v1.8.13 https://github.com/SagerNet/sing-box
|
||||
export VERSION=v1.8.14
|
||||
git clone -b $VERSION https://github.com/SagerNet/sing-box
|
||||
cd sing-box
|
||||
go build -v -gcflags=all="-l -B -C" -mod=mod -trimpath \
|
||||
-ldflags "-s -w -buildid= -extldflags '-static'" -a \
|
||||
-tags='netgo osusergo static_build with_quic with_grpc with_wireguard with_ech with_utls with_reality_server with_acme with_v2ray_api with_clash_api with_gvisor' \
|
||||
go build -tags with_quic,with_grpc,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_v2ray_api,with_clash_api,with_gvisor \
|
||||
-v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' -s -w -buildid=" \
|
||||
-o sing-box ./cmd/sing-box
|
||||
upx --ultra-brute -9 -v --lzma --best --force sing-box
|
||||
cd ..
|
||||
|
||||
### Build s-ui
|
||||
cd backend
|
||||
go build -v -gcflags=all="-l -B -C" -mod=mod -trimpath \
|
||||
-ldflags "-s -w -buildid= -extldflags '-static'" -a -tags='netgo osusergo static_build sqlite_omit_load_extension' \
|
||||
-o ../sui main.go
|
||||
go build -o ../sui main.go
|
||||
cd ..
|
||||
upx --ultra-brute -9 -v --lzma --best --force sui
|
||||
|
||||
mkdir s-ui
|
||||
cp sui s-ui/
|
||||
|
||||
+5
-4
@@ -3,17 +3,18 @@ WORKDIR /app
|
||||
COPY frontend/ ./
|
||||
RUN npm install && npm run build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS backend-builder
|
||||
FROM golang:1.22-alpine AS backend-builder
|
||||
WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
|
||||
ENV CGO_ENABLED=1
|
||||
RUN apk --no-cache --update add build-base gcc wget unzip
|
||||
ENV GOARCH=$TARGETARCH
|
||||
RUN apk update && apk --no-cache --update add build-base gcc wget unzip
|
||||
COPY backend/ ./
|
||||
COPY --from=front-builder /app/dist/ /app/web/html/
|
||||
RUN go build -o sui main.go
|
||||
RUN go build -ldflags="-w -s" -o sui main.go
|
||||
|
||||
FROM --platform=$BUILDPLATFORM alpine
|
||||
FROM --platform=$TARGETPLATFORM alpine
|
||||
LABEL org.opencontainers.image.authors="alireza7@gmail.com"
|
||||
ENV TZ=Asia/Tehran
|
||||
WORKDIR /app
|
||||
|
||||
+35
-6
@@ -1,7 +1,6 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"s-ui/logger"
|
||||
"s-ui/service"
|
||||
"strconv"
|
||||
@@ -15,6 +14,7 @@ type APIHandler struct {
|
||||
service.UserService
|
||||
service.ConfigService
|
||||
service.ClientService
|
||||
service.TlsService
|
||||
service.PanelService
|
||||
service.StatsService
|
||||
service.ServerService
|
||||
@@ -151,19 +151,40 @@ func (a *APIHandler) getHandler(c *gin.Context) {
|
||||
case "onlines":
|
||||
onlines, err := a.StatsService.GetOnlines()
|
||||
jsonObj(c, onlines, err)
|
||||
case "logs":
|
||||
service := c.Query("s")
|
||||
count := c.Query("c")
|
||||
level := c.Query("l")
|
||||
logs := a.ServerService.GetLogs(service, count, level)
|
||||
jsonObj(c, logs, nil)
|
||||
case "changes":
|
||||
actor := c.Query("a")
|
||||
chngKey := c.Query("k")
|
||||
count := c.Query("c")
|
||||
changes := a.ConfigService.GetChanges(actor, chngKey, count)
|
||||
jsonObj(c, changes, nil)
|
||||
default:
|
||||
jsonMsg(c, "API call", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *APIHandler) loadData(c *gin.Context) (string, error) {
|
||||
var data string
|
||||
func (a *APIHandler) loadData(c *gin.Context) (interface{}, error) {
|
||||
data := make(map[string]interface{}, 0)
|
||||
lu := c.Query("lu")
|
||||
isUpdated, err := a.ConfigService.CheckChnages(lu)
|
||||
isUpdated, err := a.ConfigService.CheckChanges(lu)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
onlines, err := a.StatsService.GetOnlines()
|
||||
|
||||
sysInfo := a.ServerService.GetSingboxInfo()
|
||||
if sysInfo["running"] == false {
|
||||
logs := a.ServerService.GetLogs("sing-box", "1", "debug")
|
||||
if len(logs) > 0 {
|
||||
data["lastLog"] = logs[0]
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -176,13 +197,21 @@ func (a *APIHandler) loadData(c *gin.Context) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tlsConfigs, err := a.TlsService.GetAll()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
data = fmt.Sprintf(`{"config": %s,"clients": %s,"subURI": "%s", "onlines": %s}`, string(*config), clients, subURI, onlines)
|
||||
data["config"] = *config
|
||||
data["clients"] = clients
|
||||
data["tls"] = tlsConfigs
|
||||
data["subURI"] = subURI
|
||||
data["onlines"] = onlines
|
||||
} else {
|
||||
data = fmt.Sprintf(`{"onlines": %s}`, onlines)
|
||||
data["onlines"] = onlines
|
||||
}
|
||||
|
||||
return data, nil
|
||||
|
||||
@@ -40,6 +40,7 @@ func ParseCmd() {
|
||||
fmt.Println()
|
||||
fmt.Println("Commands:")
|
||||
fmt.Println(" admin set/reset/show first admin credentials")
|
||||
fmt.Println(" migrate migrate form older version")
|
||||
fmt.Println(" setting set/reset/show settings")
|
||||
fmt.Println()
|
||||
adminCmd.Usage()
|
||||
@@ -70,6 +71,9 @@ func ParseCmd() {
|
||||
showAdmin()
|
||||
}
|
||||
|
||||
case "migrate":
|
||||
migrateDb()
|
||||
|
||||
case "setting":
|
||||
err := settingCmd.Parse(os.Args[2:])
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"s-ui/config"
|
||||
"s-ui/database"
|
||||
"s-ui/database/model"
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func migrateDb() {
|
||||
err := database.OpenDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
db := database.GetDB()
|
||||
tx := db.Begin()
|
||||
defer func() {
|
||||
if err == nil {
|
||||
tx.Commit()
|
||||
} else {
|
||||
tx.Rollback()
|
||||
}
|
||||
}()
|
||||
fmt.Println("Start migrating database...")
|
||||
err = migrateClientSchema(tx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
err = changesObj(tx)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Println("Migration done!")
|
||||
}
|
||||
|
||||
func migrateClientSchema(db *gorm.DB) error {
|
||||
rows, err := db.Raw("PRAGMA table_info(clients)").Rows()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var (
|
||||
cid int
|
||||
cname string
|
||||
ctype string
|
||||
notnull int
|
||||
dfltValue interface{}
|
||||
pk int
|
||||
)
|
||||
|
||||
rows.Scan(&cid, &cname, &ctype, ¬null, &dfltValue, &pk)
|
||||
if cname == "config" || cname == "inbounds" || cname == "links" {
|
||||
if ctype == "text" {
|
||||
fmt.Printf("Column %s has type TEXT\n", cname)
|
||||
oldData := make([]struct {
|
||||
Id uint
|
||||
Data string
|
||||
}, 0)
|
||||
db.Model(model.Client{}).Select("id", cname+" as data").Scan(&oldData)
|
||||
for _, data := range oldData {
|
||||
var newData []byte
|
||||
switch cname {
|
||||
case "inbounds":
|
||||
inbounds := strings.Split(data.Data, ",")
|
||||
newData, _ = json.MarshalIndent(inbounds, " ", " ")
|
||||
case "config":
|
||||
jsonData := map[string]interface{}{}
|
||||
json.Unmarshal([]byte(data.Data), &jsonData)
|
||||
newData, _ = json.MarshalIndent(jsonData, " ", " ")
|
||||
case "links":
|
||||
jsonData := make([]interface{}, 0)
|
||||
json.Unmarshal([]byte(data.Data), &jsonData)
|
||||
newData, _ = json.MarshalIndent(jsonData, " ", " ")
|
||||
}
|
||||
err = db.Model(model.Client{}).Where("id = ?", data.Id).UpdateColumn(cname, newData).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
db.AutoMigrate(model.Client{})
|
||||
return nil
|
||||
}
|
||||
|
||||
func changesObj(db *gorm.DB) error {
|
||||
return db.Exec("UPDATE changes SET obj = CAST('\"' || CAST(obj AS TEXT) || '\"' AS BLOB) WHERE actor = ? and obj not like ?", "DepleteJob", "\"%\"").Error
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
0.0.3
|
||||
0.0.5
|
||||
@@ -22,4 +22,5 @@ func (s *DelStatsJob) Run() {
|
||||
logger.Warning("Deleting old statistics failed: ", err)
|
||||
return
|
||||
}
|
||||
logger.Debug("Stats older than ", s.trafficAge, " days were deleted")
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func initUser() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitDB(dbPath string) error {
|
||||
func OpenDB(dbPath string) error {
|
||||
dir := path.Dir(dbPath)
|
||||
err := os.MkdirAll(dir, 01740)
|
||||
if err != nil {
|
||||
@@ -48,12 +48,18 @@ func InitDB(dbPath string) error {
|
||||
Logger: gormLogger,
|
||||
}
|
||||
db, err = gorm.Open(sqlite.Open(dbPath), c)
|
||||
return err
|
||||
}
|
||||
|
||||
func InitDB(dbPath string) error {
|
||||
err := OpenDB(dbPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = db.AutoMigrate(
|
||||
&model.Setting{},
|
||||
&model.Tls{},
|
||||
&model.User{},
|
||||
&model.Stats{},
|
||||
&model.Client{},
|
||||
|
||||
@@ -8,6 +8,14 @@ type Setting struct {
|
||||
Value string `json:"value" form:"value"`
|
||||
}
|
||||
|
||||
type Tls struct {
|
||||
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Inbounds json.RawMessage `json:"inbounds" form:"inbounds"`
|
||||
Server json.RawMessage `json:"server" form:"server"`
|
||||
Client json.RawMessage `json:"client" form:"client"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Username string `json:"username" form:"username"`
|
||||
@@ -16,17 +24,17 @@ type User struct {
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Config string `json:"config" form:"config"`
|
||||
Inbounds string `json:"inbounds" form:"inbounds"`
|
||||
Links string `json:"links" form:"links"`
|
||||
Volume int64 `json:"volume" form:"volume"`
|
||||
Expiry int64 `json:"expiry" form:"expiry"`
|
||||
Down int64 `json:"down" form:"down"`
|
||||
Up int64 `json:"up" form:"up"`
|
||||
Desc string `json:"desc" from:"desc"`
|
||||
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
Name string `json:"name" form:"name"`
|
||||
Config json.RawMessage `json:"config" form:"config"`
|
||||
Inbounds json.RawMessage `json:"inbounds" form:"inbounds"`
|
||||
Links json.RawMessage `json:"links" form:"links"`
|
||||
Volume int64 `json:"volume" form:"volume"`
|
||||
Expiry int64 `json:"expiry" form:"expiry"`
|
||||
Down int64 `json:"down" form:"down"`
|
||||
Up int64 `json:"up" form:"up"`
|
||||
Desc string `json:"desc" from:"desc"`
|
||||
}
|
||||
|
||||
type Stats struct {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"s-ui/database"
|
||||
"s-ui/database/model"
|
||||
"s-ui/logger"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -14,18 +13,14 @@ import (
|
||||
type ClientService struct {
|
||||
}
|
||||
|
||||
func (s *ClientService) GetAll() (string, error) {
|
||||
func (s *ClientService) GetAll() ([]model.Client, error) {
|
||||
db := database.GetDB()
|
||||
clients := []model.Client{}
|
||||
err := db.Model(model.Client{}).Scan(&clients).Error
|
||||
if err != nil {
|
||||
return "", err
|
||||
return nil, err
|
||||
}
|
||||
data, err := json.Marshal(clients)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
return clients, nil
|
||||
}
|
||||
|
||||
func (s *ClientService) Save(tx *gorm.DB, changes []model.Changes) error {
|
||||
@@ -62,18 +57,20 @@ func (s *ClientService) DepleteClients() ([]string, []string, error) {
|
||||
return nil, nil, 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)
|
||||
userInbounds := strings.Split(client.Inbounds, ",")
|
||||
var userInbounds []string
|
||||
json.Unmarshal(client.Inbounds, &userInbounds)
|
||||
inbounds = append(inbounds, userInbounds...)
|
||||
changes = append(changes, model.Changes{
|
||||
DateTime: time.Now().Unix(),
|
||||
DateTime: dt,
|
||||
Actor: "DepleteJob",
|
||||
Key: "clients",
|
||||
Action: "disable",
|
||||
Obj: json.RawMessage(client.Name),
|
||||
Obj: json.RawMessage("\"" + client.Name + "\""),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -87,6 +84,7 @@ func (s *ClientService) DepleteClients() ([]string, []string, error) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
LastUpdate = dt
|
||||
}
|
||||
|
||||
return users, inbounds, nil
|
||||
|
||||
+83
-44
@@ -6,14 +6,18 @@ import (
|
||||
"s-ui/config"
|
||||
"s-ui/database"
|
||||
"s-ui/database/model"
|
||||
"s-ui/logger"
|
||||
"s-ui/singbox"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ApiAddr string
|
||||
var LastUpdate int64
|
||||
|
||||
type ConfigService struct {
|
||||
ClientService
|
||||
TlsService
|
||||
singbox.Controller
|
||||
SettingService
|
||||
}
|
||||
@@ -51,27 +55,44 @@ func (s *ConfigService) InitConfig() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s.RefreshApiAddr(&data)
|
||||
var singboxConfig SingBoxConfig
|
||||
err = json.Unmarshal(data, &singboxConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.RefreshApiAddr(&singboxConfig)
|
||||
}
|
||||
|
||||
func (s *ConfigService) GetConfig() (*[]byte, error) {
|
||||
func (s *ConfigService) GetConfig() (*SingBoxConfig, error) {
|
||||
configPath := config.GetBinFolderPath()
|
||||
data, err := os.ReadFile(configPath + "/config.json")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &data, nil
|
||||
singboxConfig := SingBoxConfig{}
|
||||
err = json.Unmarshal(data, &singboxConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &singboxConfig, nil
|
||||
}
|
||||
|
||||
func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string) error {
|
||||
var err error
|
||||
var clientChanges, settingChanges, configChanges []model.Changes
|
||||
var clientChanges, tlsChanges, 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["settings"]; ok {
|
||||
err = json.Unmarshal([]byte(changes["settings"]), &settingChanges)
|
||||
if err != nil {
|
||||
@@ -101,6 +122,12 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(tlsChanges) > 0 {
|
||||
err = s.TlsService.Save(tx, tlsChanges)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(settingChanges) > 0 {
|
||||
err = s.SettingService.Save(tx, settingChanges)
|
||||
if err != nil {
|
||||
@@ -112,11 +139,7 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newConfig := SingBoxConfig{}
|
||||
err = json.Unmarshal(*singboxConfig, &newConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newConfig := *singboxConfig
|
||||
for _, change := range configChanges {
|
||||
rawObject := change.Obj
|
||||
switch change.Key {
|
||||
@@ -154,12 +177,7 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
|
||||
}
|
||||
}
|
||||
|
||||
// Save to config.json
|
||||
data, err := json.MarshalIndent(newConfig, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.Save(&data)
|
||||
err = s.Save(&newConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -167,7 +185,7 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
|
||||
|
||||
// Log changes
|
||||
dt := time.Now().Unix()
|
||||
allChanges := append(append(clientChanges, settingChanges...), configChanges...)
|
||||
allChanges := append(append(clientChanges, settingChanges...), append(configChanges, tlsChanges...)...)
|
||||
for index := range allChanges {
|
||||
allChanges[index].DateTime = dt
|
||||
allChanges[index].Actor = loginUser
|
||||
@@ -177,20 +195,30 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
|
||||
return err
|
||||
}
|
||||
|
||||
LastUpdate = dt
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ConfigService) CheckChnages(lu string) (bool, error) {
|
||||
func (s *ConfigService) CheckChanges(lu string) (bool, error) {
|
||||
if lu == "" {
|
||||
return true, nil
|
||||
}
|
||||
db := database.GetDB()
|
||||
var count int64
|
||||
err := db.Model(model.Changes{}).Where("date_time > " + lu).Count(&count).Error
|
||||
return count > 0, err
|
||||
if LastUpdate == 0 {
|
||||
db := database.GetDB()
|
||||
var count int64
|
||||
err := db.Model(model.Changes{}).Where("date_time > " + lu).Count(&count).Error
|
||||
if err == nil {
|
||||
LastUpdate = time.Now().Unix()
|
||||
}
|
||||
return count > 0, err
|
||||
} else {
|
||||
intLu, err := strconv.ParseInt(lu, 10, 64)
|
||||
return LastUpdate > intLu, err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ConfigService) Save(data *[]byte) error {
|
||||
func (s *ConfigService) Save(singboxConfig *SingBoxConfig) error {
|
||||
configPath := config.GetBinFolderPath()
|
||||
_, err := os.Stat(configPath + "/config.json")
|
||||
if os.IsNotExist(err) {
|
||||
@@ -202,35 +230,36 @@ func (s *ConfigService) Save(data *[]byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(configPath+"/config.json", *data, 0764)
|
||||
data, err := json.MarshalIndent(singboxConfig, " ", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.RefreshApiAddr(data)
|
||||
err = os.WriteFile(configPath+"/config.json", data, 0764)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.RefreshApiAddr(singboxConfig)
|
||||
s.Controller.Restart()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ConfigService) RefreshApiAddr(data *[]byte) error {
|
||||
func (s *ConfigService) RefreshApiAddr(singboxConfig *SingBoxConfig) error {
|
||||
Env_API := config.GetEnvApi()
|
||||
if len(Env_API) > 0 {
|
||||
ApiAddr = Env_API
|
||||
} else {
|
||||
var err error
|
||||
if data == nil {
|
||||
data, err = s.GetConfig()
|
||||
if singboxConfig == nil {
|
||||
singboxConfig, err = s.GetConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
singboxConfig := SingBoxConfig{}
|
||||
err = json.Unmarshal(*data, &singboxConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var experimental struct {
|
||||
V2rayApi struct {
|
||||
Listen string `json:"listen"`
|
||||
@@ -257,12 +286,7 @@ func (s *ConfigService) DepleteClients() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newConfig := SingBoxConfig{}
|
||||
err = json.Unmarshal(*singboxConfig, &newConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for inbound_index, inbound := range newConfig.Inbounds {
|
||||
for inbound_index, inbound := range singboxConfig.Inbounds {
|
||||
var inboundJson map[string]interface{}
|
||||
json.Unmarshal(inbound, &inboundJson)
|
||||
if s.contains(inbounds, inboundJson["tag"].(string)) {
|
||||
@@ -301,13 +325,10 @@ func (s *ConfigService) DepleteClients() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newConfig.Inbounds[inbound_index] = modifiedInbound
|
||||
singboxConfig.Inbounds[inbound_index] = modifiedInbound
|
||||
}
|
||||
modifiedConfig, err := json.MarshalIndent(newConfig, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = s.Save(&modifiedConfig)
|
||||
|
||||
err = s.Save(singboxConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -322,3 +343,21 @@ func (s *ConfigService) contains(slice []string, item string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *ConfigService) GetChanges(actor string, chngKey string, count string) []model.Changes {
|
||||
c, _ := strconv.Atoi(count)
|
||||
whereString := "`id`>0"
|
||||
if len(actor) > 0 {
|
||||
whereString += " and `actor`='" + actor + "'"
|
||||
}
|
||||
if len(chngKey) > 0 {
|
||||
whereString += " and `key`='" + chngKey + "'"
|
||||
}
|
||||
db := database.GetDB()
|
||||
var chngs []model.Changes
|
||||
err := db.Model(model.Changes{}).Where(whereString).Order("`id` desc").Limit(c).Scan(&chngs).Error
|
||||
if err != nil {
|
||||
logger.Warning(err)
|
||||
}
|
||||
return chngs
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"s-ui/config"
|
||||
"s-ui/logger"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/shirou/gopsutil/v3/cpu"
|
||||
@@ -135,3 +138,24 @@ func (s *ServerService) GetSystemInfo() map[string]interface{} {
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func (s *ServerService) GetLogs(service string, count string, level string) []string {
|
||||
c, _ := strconv.Atoi(count)
|
||||
var lines []string
|
||||
if service == "sing-box" {
|
||||
cmdArgs := []string{"journalctl", "-u", service, "--no-pager", "-n", count, "-p", level}
|
||||
// Run the command
|
||||
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return []string{"Failed to run journalctl command!"}
|
||||
}
|
||||
lines = strings.Split(out.String(), "\n")
|
||||
} else {
|
||||
lines = logger.GetLogs(c, level)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ var defaultValueMap = map[string]string{
|
||||
"webListen": "",
|
||||
"webDomain": "",
|
||||
"webPort": "2095",
|
||||
"webSecret": common.Random(32),
|
||||
"secret": common.Random(32),
|
||||
"webCertFile": "",
|
||||
"webKeyFile": "",
|
||||
"webPath": "/app/",
|
||||
@@ -191,11 +191,11 @@ func (s *SettingService) SetWebPath(webPath string) error {
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSecret() ([]byte, error) {
|
||||
secret, err := s.getString("webSecret")
|
||||
if secret == defaultValueMap["webSecret"] {
|
||||
err := s.saveSetting("webSecret", secret)
|
||||
secret, err := s.getString("secret")
|
||||
if secret == defaultValueMap["secret"] {
|
||||
err := s.saveSetting("secret", secret)
|
||||
if err != nil {
|
||||
logger.Warning("save webSecret failed:", err)
|
||||
logger.Warning("save secret failed:", err)
|
||||
}
|
||||
}
|
||||
return []byte(secret), err
|
||||
@@ -318,10 +318,10 @@ func (s *SettingService) Save(tx *gorm.DB, changes []model.Changes) error {
|
||||
json.Unmarshal(change.Obj, &obj)
|
||||
|
||||
// Secure file existance check
|
||||
if key == "webCertFile" ||
|
||||
if obj != "" && (key == "webCertFile" ||
|
||||
key == "webKeyFile" ||
|
||||
key == "subCertFile" ||
|
||||
key == "subKeyFile" {
|
||||
key == "subKeyFile") {
|
||||
err = s.fileExists(obj)
|
||||
if err != nil {
|
||||
return common.NewError(" -> ", obj, " is not exists")
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"s-ui/database"
|
||||
"s-ui/database/model"
|
||||
"time"
|
||||
@@ -86,12 +85,8 @@ func (s *StatsService) GetStats(resorce string, tag string, limit int) ([]model.
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *StatsService) GetOnlines() (string, error) {
|
||||
onlines, err := json.Marshal(onlineResources)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(onlines), nil
|
||||
func (s *StatsService) GetOnlines() (onlines, error) {
|
||||
return *onlineResources, nil
|
||||
}
|
||||
func (s *StatsService) DelOldStats(days int) error {
|
||||
oldTime := time.Now().AddDate(0, 0, -(days)).Unix()
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"s-ui/database"
|
||||
"s-ui/database/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TlsService struct {
|
||||
}
|
||||
|
||||
func (s *TlsService) GetAll() ([]model.Tls, error) {
|
||||
db := database.GetDB()
|
||||
tlsConfig := []model.Tls{}
|
||||
err := db.Model(model.Tls{}).Scan(&tlsConfig).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
func (s *TlsService) Save(tx *gorm.DB, changes []model.Changes) error {
|
||||
var err error
|
||||
for _, change := range changes {
|
||||
tlsConfig := model.Tls{}
|
||||
err = json.Unmarshal(change.Obj, &tlsConfig)
|
||||
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
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -1,13 +1,26 @@
|
||||
package common
|
||||
|
||||
import "math/rand"
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
var allSeq [62]rune
|
||||
var (
|
||||
allSeq []rune
|
||||
rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
)
|
||||
|
||||
func init() {
|
||||
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
for _, char := range chars {
|
||||
allSeq = append(allSeq, char)
|
||||
}
|
||||
}
|
||||
|
||||
func Random(n int) string {
|
||||
runes := make([]rune, n)
|
||||
for i := 0; i < n; i++ {
|
||||
runes[i] = allSeq[rand.Intn(len(allSeq))]
|
||||
runes[i] = allSeq[rnd.Intn(len(allSeq))]
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS singbox-builder
|
||||
LABEL maintainer="Alireza <alireza7@gmail.com>"
|
||||
WORKDIR /app
|
||||
ARG TARGETOS TARGETARCH
|
||||
ARG SINGBOX_VER=v1.8.10
|
||||
ARG SINGBOX_VER=v1.8.13
|
||||
ARG SINGBOX_TAGS="with_quic,with_grpc,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_v2ray_api,with_clash_api,with_gvisor"
|
||||
ARG GOPROXY=""
|
||||
ENV GOPROXY ${GOPROXY}
|
||||
@@ -18,7 +18,7 @@ RUN set -ex \
|
||||
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$SINGBOX_VER\" -s -w -buildid=" \
|
||||
./cmd/sing-box
|
||||
|
||||
FROM --platform=$BUILDPLATFORM alpine
|
||||
FROM --platform=$TARGETPLATFORM alpine
|
||||
LABEL maintainer="Alireza <alireza7@gmail.com>"
|
||||
ENV TZ=Asia/Tehran
|
||||
WORKDIR /app
|
||||
|
||||
@@ -42,4 +42,12 @@ do
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Check if sin-box crashed
|
||||
if ! kill -0 $tokill > /dev/null 2>&1; then
|
||||
if [ "$signal" != "stop" ]; then
|
||||
echo "Sing-Box with PID $tokill crashed. Breaking the loop..."
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
@@ -1,6 +1,4 @@
|
||||
---
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
s-ui:
|
||||
image: alireza7/s-ui
|
||||
|
||||
Generated
+597
-1912
File diff suppressed because it is too large
Load Diff
+21
-20
@@ -3,41 +3,42 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"dev": "vite --host",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "7.0.96",
|
||||
"axios": "^1.6.5",
|
||||
"chart.js": "^4.4.1",
|
||||
"axios": "^1.7.2",
|
||||
"chart.js": "^4.4.3",
|
||||
"clipboard": "^2.0.11",
|
||||
"core-js": "^3.29.0",
|
||||
"core-js": "^3.37.1",
|
||||
"moment": "^2.30.1",
|
||||
"notivue": "^2.4.4",
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"roboto-fontface": "*",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"vue": "^3.2.0",
|
||||
"vue-chartjs": "^5.3.0",
|
||||
"vue-i18n": "^9.8.0",
|
||||
"vue-router": "^4.0.0",
|
||||
"vue-chartjs": "^5.3.1",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.3.2",
|
||||
"vue3-persian-datetime-picker": "^1.2.2",
|
||||
"vuetify": "^3.0.0"
|
||||
"vuetify": "^3.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.21.4",
|
||||
"@types/node": "^18.15.0",
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"@vue/eslint-config-typescript": "^11.0.0",
|
||||
"eslint": "^8.22.0",
|
||||
"eslint-plugin-vue": "^9.3.0",
|
||||
"@babel/types": "^7.24.5",
|
||||
"@types/node": "^18.19.33",
|
||||
"@vitejs/plugin-vue": "^4.6.2",
|
||||
"@vue/eslint-config-typescript": "^11.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"material-design-icons-iconfont": "^6.7.0",
|
||||
"sass": "^1.60.0",
|
||||
"typescript": "^5.0.0",
|
||||
"unplugin-fonts": "^1.0.3",
|
||||
"sass": "^1.77.2",
|
||||
"typescript": "^5.4.5",
|
||||
"unplugin-fonts": "^1.1.1",
|
||||
"vite": "^4.5.3",
|
||||
"vite-plugin-vuetify": "^1.0.0",
|
||||
"vue-tsc": "^1.2.0"
|
||||
"vite-plugin-vuetify": "^1.0.2",
|
||||
"vue-tsc": "^1.8.27"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<v-card subtitle="ACME" style="background-color: inherit;">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('enable')" v-model="enabled" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" v-if="enabled">
|
||||
<v-text-field
|
||||
:label="$t('rule.domain') + ' ' + $t('commaSeparated')"
|
||||
hide-details
|
||||
v-model="domains">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="enabled">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionDir">
|
||||
<v-text-field
|
||||
:label="$t('tls.acme.dataDir')"
|
||||
hide-details
|
||||
v-model="acme.data_directory">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionDefault">
|
||||
<v-combobox
|
||||
v-model="acme.default_server_name"
|
||||
:items="acme.domain"
|
||||
:label="$t('tls.acme.defaultDomain')"
|
||||
hide-details
|
||||
></v-combobox>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionEmail">
|
||||
<v-text-field
|
||||
:label="$t('email')"
|
||||
hide-details
|
||||
v-model="acme.email">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionChallenge">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.acme.httpChallenge')" v-model="acme.disable_http_challenge" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.acme.tlsChallenge')" v-model="acme.disable_tls_alpn_challenge" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionPorts">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('tls.acme.altHport')"
|
||||
hide-details
|
||||
type="number"
|
||||
min=1
|
||||
max="65532"
|
||||
v-model.number="acme.alternative_http_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('tls.acme.altTport')"
|
||||
hide-details
|
||||
type="number"
|
||||
min=1
|
||||
max="65532"
|
||||
v-model.number="acme.alternative_tls_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="optionProvider">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
v-model="caProvider"
|
||||
:items="providerList"
|
||||
:label="$t('tls.acme.caProvider')"
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" v-if="caProvider == ''">
|
||||
<v-text-field
|
||||
:label="$t('tls.acme.customCa')"
|
||||
hide-details
|
||||
v-model="acme.provider">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="acme.external_account != undefined">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="Key ID"
|
||||
hide-details
|
||||
v-model="acme.external_account.key_id">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
label="MAC Key"
|
||||
hide-details
|
||||
v-model="acme.external_account.mac_key">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="acme.dns01_challenge != undefined">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
:label="$t('tls.acme.dns01Provider')"
|
||||
hide-details
|
||||
:items="dnsProviders.map(d => d.provider)"
|
||||
@update:model-value="acme.dns01_challenge = { provider: $event }"
|
||||
v-model="acme.dns01_challenge.provider">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4"
|
||||
v-for="item in dnsProviders.filter(d => d.provider == acme.dns01_challenge?.provider)[0]?.params"
|
||||
:key="item">
|
||||
<v-text-field
|
||||
:label="item"
|
||||
hide-details
|
||||
v-model="acme.dns01_challenge[item]">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details>{{ $t('tls.acme.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDir" color="primary" :label="$t('tls.acme.dataDir')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDefault" color="primary" :label="$t('tls.acme.defaultDomain')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionEmail" color="primary" :label="$t('email')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionChallenge" color="primary" :label="$t('tls.acme.disableChallenges')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionPorts" color="primary" :label="$t('tls.acme.altPorts')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionProvider" color="primary" :label="$t('tls.acme.caProvider')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionExt" color="primary" :label="$t('tls.acme.extAcc')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionDns01" color="primary" :label="$t('tls.acme.dns01')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { acme } from '@/types/inTls'
|
||||
|
||||
export default {
|
||||
props: ['tls'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
providerList: [
|
||||
{ title: "Let's Encrypt", value: "letsencrypt" },
|
||||
{ title: "ZeroSSL", value: "zerossl" },
|
||||
{ title: "Custom", value: "" }
|
||||
],
|
||||
dnsProviders: [
|
||||
{ provider: "cloudflare", params: [ "api_token" ] },
|
||||
{ provider: "alidns", params: [ "access_key_id","access_key_secret","region_id" ] }
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
acme() {
|
||||
return <acme>this.$props.tls.acme
|
||||
},
|
||||
enabled: {
|
||||
get() { return this.acme != undefined },
|
||||
set(v: boolean) { this.$props.tls.acme = v ? { domain: [] } : undefined }
|
||||
},
|
||||
domains: {
|
||||
get() { return this.acme?.domain ? this.acme.domain.join(',') : "" },
|
||||
set(v: string) {
|
||||
if(!v.endsWith(',')) {
|
||||
this.acme.domain = v.length > 0 ? v.split(',') : []
|
||||
}
|
||||
}
|
||||
},
|
||||
caProvider: {
|
||||
get() { return this.acme?.provider && ['letsencrypt','zerossl'].includes(this.acme.provider) ? this.acme?.provider : '' },
|
||||
set(v: string) { this.acme.provider = ['letsencrypt','zerossl'].includes(v) ? v : 'https://' }
|
||||
},
|
||||
optionDir: {
|
||||
get(): boolean { return this.acme?.data_directory != undefined },
|
||||
set(v:boolean) { this.acme.data_directory = v ? '' : undefined }
|
||||
},
|
||||
optionDefault: {
|
||||
get(): boolean { return this.acme?.default_server_name != undefined },
|
||||
set(v:boolean) { this.acme.default_server_name = v ? this.domains.length>0 ? this.domains[0] : '' : undefined }
|
||||
},
|
||||
optionEmail: {
|
||||
get(): boolean { return this.acme?.email != undefined },
|
||||
set(v:boolean) { this.acme.email = v ? '' : undefined }
|
||||
},
|
||||
optionChallenge: {
|
||||
get(): boolean { return this.acme?.disable_http_challenge != undefined || this.acme?.disable_tls_alpn_challenge != undefined },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.acme.disable_http_challenge = false
|
||||
this.acme.disable_tls_alpn_challenge = false
|
||||
} else {
|
||||
delete this.acme.disable_http_challenge
|
||||
delete this.acme.disable_tls_alpn_challenge
|
||||
}
|
||||
}
|
||||
},
|
||||
optionPorts: {
|
||||
get(): boolean { return this.acme?.alternative_http_port != undefined || this.acme?.alternative_tls_port != undefined },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.acme.alternative_http_port = 80
|
||||
this.acme.alternative_tls_port = 443
|
||||
} else {
|
||||
delete this.acme.alternative_http_port
|
||||
delete this.acme.alternative_tls_port
|
||||
}
|
||||
}
|
||||
},
|
||||
optionProvider: {
|
||||
get(): boolean { return this.acme?.provider != undefined },
|
||||
set(v:boolean) { this.acme.provider = v ? 'letsencrypt' : undefined }
|
||||
},
|
||||
optionExt: {
|
||||
get(): boolean { return this.acme?.external_account != undefined },
|
||||
set(v:boolean) { this.acme.external_account = v ? { key_id: '', mac_key: '' } : undefined }
|
||||
},
|
||||
optionDns01: {
|
||||
get(): boolean { return this.acme?.dns01_challenge != undefined },
|
||||
set(v:boolean) { this.acme.dns01_challenge = v ? { provider: 'cloudflare' } : undefined }
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<v-card subtitle="ECH" style="background-color: inherit;">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('enable')" v-model="enabled" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="enabled">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" label="Post-Quantum Schemes" v-model="ech.pq_signature_schemes_enabled" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" label="Disable Adaptive Size" v-model="ech.dynamic_record_sizing_disabled" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<v-btn-toggle v-model="useEchPath"
|
||||
class="rounded-xl"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
shaped
|
||||
mandatory>
|
||||
<v-btn
|
||||
@click="delete ech.key"
|
||||
>{{ $t('tls.usePath') }}</v-btn>
|
||||
<v-btn
|
||||
@click="delete ech.key_path"
|
||||
>{{ $t('tls.useText') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="useEchPath == 0">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('tls.keyPath')"
|
||||
hide-details
|
||||
v-model="ech.key_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-textarea
|
||||
:label="$t('tls.key')"
|
||||
hide-details
|
||||
v-model="echKeyText">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-textarea
|
||||
:label="$t('tls.cert')"
|
||||
hide-details
|
||||
v-model="echConfigText">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ech } from '@/types/inTls'
|
||||
|
||||
export default {
|
||||
props: ['iTls','oTls'],
|
||||
data() {
|
||||
return {
|
||||
useEchPath: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
ech() {
|
||||
return <ech>this.$props.iTls.ech
|
||||
},
|
||||
enabled: {
|
||||
get() { return this.ech?.enabled?? false },
|
||||
set(v: boolean) {
|
||||
this.$props.iTls.ech = v ? { enabled: true } : undefined
|
||||
this.$props.oTls.ech = v ? {} : undefined
|
||||
}
|
||||
},
|
||||
echKeyText: {
|
||||
get(): string { return this.ech?.key ? this.ech.key.join('\n') : '' },
|
||||
set(newValue:string) { this.ech.key = newValue.split('\n') }
|
||||
},
|
||||
echConfigText: {
|
||||
get(): string { return this.oTls.ech?.config ? this.oTls.ech.config.join('\n') : '' },
|
||||
set(newValue:string) { this.oTls.ech.config = newValue.split('\n') }
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,11 +1,20 @@
|
||||
<template>
|
||||
<v-card :subtitle="$t('objects.tls')">
|
||||
<v-row v-if="tlsOptional">
|
||||
<v-col cols="auto">
|
||||
<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-select
|
||||
hide-details
|
||||
label="Preset"
|
||||
:items="tlsItems"
|
||||
@update:model-value="changeTlsItem($event)"
|
||||
v-model="tlsId">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<template v-if="tls.enabled">
|
||||
<template v-if="tls.enabled && tlsId == 0">
|
||||
<v-row>
|
||||
<v-col cols="auto">
|
||||
<v-btn-toggle v-model="usePath"
|
||||
@@ -103,32 +112,32 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-card-actions v-if="tls.enabled">
|
||||
<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>{{ $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>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details>{{ $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>
|
||||
@@ -136,11 +145,11 @@
|
||||
<script lang="ts">
|
||||
import { iTls, defaultInTls } from '@/types/inTls'
|
||||
export default {
|
||||
props: ['inbound'],
|
||||
props: ['inbound', 'tlsConfigs', 'tls_id'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
usePath: 0,
|
||||
usePath: this.$props.inbound.tls.key == undefined ? 0 : 1,
|
||||
defaults: defaultInTls,
|
||||
alpn: [
|
||||
{ title: "H3", value: 'h3' },
|
||||
@@ -173,8 +182,15 @@ export default {
|
||||
tls(): iTls {
|
||||
return <iTls> this.$props.inbound.tls
|
||||
},
|
||||
tlsItems(): any[] {
|
||||
return [ { title: '', 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 Object.hasOwn(this.$props.inbound.tls, 'enabled') ? this.tls.enabled : false },
|
||||
get() { return this.tls.enabled?? false },
|
||||
set(newValue: boolean) { this.$props.inbound.tls = newValue ? { enabled: true } : {} }
|
||||
},
|
||||
tlsOptional(): boolean {
|
||||
@@ -190,23 +206,33 @@ export default {
|
||||
},
|
||||
optionSNI: {
|
||||
get(): boolean { return this.tls.server_name != undefined },
|
||||
set(v:boolean) { this.$props.inbound.tls.server_name = v ? '' : undefined }
|
||||
set(v:boolean) { this.tls.server_name = v ? '' : undefined }
|
||||
},
|
||||
optionALPN: {
|
||||
get(): boolean { return this.tls.alpn != undefined },
|
||||
set(v:boolean) { this.$props.inbound.tls.alpn = v ? defaultInTls.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.$props.inbound.tls.min_version = v ? defaultInTls.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.$props.inbound.tls.max_version = v ? defaultInTls.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.$props.inbound.tls.cipher_suites = v ? defaultInTls.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,4 +1,10 @@
|
||||
<template>
|
||||
<LogVue
|
||||
v-model="logModal.visible"
|
||||
:visible="logModal.visible"
|
||||
:logType="logModal.logType"
|
||||
@close="closeLogs"
|
||||
/>
|
||||
<v-container class="fill-height">
|
||||
<v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" >
|
||||
<v-row class="d-flex align-center justify-center">
|
||||
@@ -45,7 +51,7 @@
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3" v-for="i in reloadItems" :key="i">
|
||||
<v-card class="rounded-lg" variant="outlined" height="200px"
|
||||
<v-card class="rounded-lg" variant="outlined" height="210px"
|
||||
:title="menuItems.flatMap(cat => cat.value).find(m => m.value == i)?.title">
|
||||
<v-card-text style="padding: 0 16px;" align="center" justify="center">
|
||||
<Gauge :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'g'" />
|
||||
@@ -80,13 +86,19 @@
|
||||
</v-col>
|
||||
<v-col cols="3">S-UI</v-col>
|
||||
<v-col cols="9">
|
||||
<v-chip density="compact" color="primary" variant="flat">
|
||||
<v-chip density="compact" color="blue">
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ $t('main.info.threads') }}: {{ tilesData.sys?.appThreads }}<br />
|
||||
{{ $t('main.info.memory') }}: {{ HumanReadable.sizeFormat(tilesData.sys?.appMem) }}
|
||||
</v-tooltip>
|
||||
v{{ tilesData.sys?.appVersion }}
|
||||
</v-chip>
|
||||
<v-chip density="compact" color="transparent" style="cursor: pointer;" @click="openLogs('s-ui')">
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ $t('basic.log.title') + " - S-UI" }}
|
||||
</v-tooltip>
|
||||
<v-icon icon="mdi-list-box-outline" color="blue" />
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="3">{{ $t('main.info.uptime') }}</v-col>
|
||||
<v-col cols="9">{{ HumanReadable.formatSecond(tilesData.uptime) }}</v-col>
|
||||
@@ -97,7 +109,13 @@
|
||||
<v-col cols="4">{{ $t('main.info.running') }}</v-col>
|
||||
<v-col cols="8">
|
||||
<v-chip density="compact" color="success" variant="flat" v-if="tilesData.sbd?.running">{{ $t('yes') }}</v-chip>
|
||||
<v-chip density="compact" color="error" variant="flat" v-else>{{ $t('no') }}</v-chip>
|
||||
<v-chip density="compact" color="error" variant="flat" v-else>{{ $t('no') }}</v-chip>
|
||||
<v-chip density="compact" color="transparent" style="cursor: pointer;" @click="openLogs('sing-box')">
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ $t('basic.log.title') + " - Sing-Box" }}
|
||||
</v-tooltip>
|
||||
<v-icon icon="mdi-list-box-outline" :color="tilesData.sbd?.running ? 'success': 'error'" />
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="4">{{ $t('main.info.memory') }}</v-col>
|
||||
<v-col cols="8">
|
||||
@@ -148,6 +166,7 @@ import Gauge from '@/components/tiles/Gauge.vue'
|
||||
import History from '@/components/tiles/History.vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { i18n } from '@/locales'
|
||||
import LogVue from '@/layouts/modals/Logs.vue'
|
||||
|
||||
const menu = ref(false)
|
||||
const menuItems = [
|
||||
@@ -215,4 +234,19 @@ onMounted(() => {
|
||||
onBeforeUnmount(() => {
|
||||
stopTimer()
|
||||
})
|
||||
|
||||
const logModal = ref({
|
||||
visible: false,
|
||||
logType: "s-ui"
|
||||
})
|
||||
|
||||
const openLogs = (logType: string) => {
|
||||
logModal.value.logType = logType
|
||||
logModal.value.visible = true
|
||||
}
|
||||
|
||||
const closeLogs = () => {
|
||||
logModal.value.logType = "s-ui"
|
||||
logModal.value.visible = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,18 +1,38 @@
|
||||
<template>
|
||||
<v-snackbar
|
||||
v-model="sb.showMsg"
|
||||
location="top"
|
||||
:color="snackbar.color"
|
||||
:timeout="snackbar.timeout">
|
||||
{{ snackbar.message }}
|
||||
</v-snackbar>
|
||||
<Notivue v-slot="item">
|
||||
<NotivueSwipe :item="item">
|
||||
<Notification
|
||||
:item="item"
|
||||
:theme="theme"
|
||||
:dir="direction"
|
||||
:icons="outlinedIcons"
|
||||
:hideClose="true"
|
||||
@click="item.clear"
|
||||
/>
|
||||
</NotivueSwipe>
|
||||
</Notivue>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import Message from '@/store/modules/message'
|
||||
import { Notivue, Notification, NotivueSwipe, outlinedIcons, pastelTheme, darkTheme } from 'notivue'
|
||||
import { computed } from 'vue'
|
||||
import { useTheme } from 'vuetify'
|
||||
import vuetify from '@/plugins/vuetify';
|
||||
|
||||
const sb = Message()
|
||||
const Theme = useTheme()
|
||||
|
||||
const snackbar = ref(sb.snackbar)
|
||||
const theme = computed(() =>{
|
||||
return Theme.global.name.value == "light" ? pastelTheme : darkTheme
|
||||
})
|
||||
|
||||
const direction = computed(() => {
|
||||
return vuetify.locale.current.value == 'fa' ? 'rtl' : 'ltr'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--nv-z: 10020;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -20,16 +20,6 @@
|
||||
v-model.number="override_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="direction == 'out'">
|
||||
<v-select
|
||||
:label="$t('types.direct.proxyProtocol')"
|
||||
:items="[1,2]"
|
||||
hide-details
|
||||
clearable
|
||||
@click:clear="delete data.proxy_protocol"
|
||||
v-model.number="data.proxy_protocol">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<v-card subtitle="Hysteria2">
|
||||
<v-row v-if="direction == 'in'">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-col cols="12" sm="6" md="4" v-if="data.masquerade != undefined">
|
||||
<v-text-field
|
||||
label="HTTP3 server on auth fail"
|
||||
hide-details
|
||||
@@ -46,7 +46,7 @@
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="data.obfs">
|
||||
<v-row v-if="data.obfs != undefined">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.hy.obfs')"
|
||||
@@ -66,6 +66,9 @@
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionObfs" color="primary" :label="$t('types.hy.obfs')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionMasq" color="primary" label="Masquerade" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
@@ -95,6 +98,10 @@ export default {
|
||||
optionObfs: {
|
||||
get(): boolean { return this.$props.data.obfs != undefined },
|
||||
set(v:boolean) { this.$props.data.obfs = v ? { type: "salamander", password: "" } : undefined }
|
||||
},
|
||||
optionMasq: {
|
||||
get(): boolean { return this.$props.data.masquerade != undefined },
|
||||
set(v:boolean) { this.$props.data.masquerade = v ? "" : undefined }
|
||||
}
|
||||
},
|
||||
components: { Network }
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<v-row v-if="Inbound.handshake_for_server_name != undefined">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.shdwTls.adHS')"
|
||||
:label="$t('types.shdwTls.addHS')"
|
||||
hide-details
|
||||
append-icon="mdi-plus"
|
||||
@click:append="addHandshakeServer()"
|
||||
|
||||
@@ -49,8 +49,7 @@ const gaugeColor = computed(() => {
|
||||
background: `rgb(var(--v-theme-${gaugeColor}))`
|
||||
}">
|
||||
</div>
|
||||
<span class="gauge__cover" dir="ltr" v-html="data.text">
|
||||
</span>
|
||||
<div class="gauge__cover"><span dir="ltr" v-html="data.text"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, ref,watch } from "vue"
|
||||
import { computed, ref } from "vue"
|
||||
import { useTheme } from "vuetify"
|
||||
import { FindDiff } from "@/plugins/utils"
|
||||
import Data from "@/store/modules/data"
|
||||
@@ -32,11 +32,11 @@ const saveChanges = () => {
|
||||
}
|
||||
|
||||
const oldData = computed((): any => {
|
||||
return {config: store.oldData.config, clients: store.oldData.clients}
|
||||
return {config: store.oldData.config, clients: store.oldData.clients, tls: store.oldData.tlsConfigs}
|
||||
})
|
||||
|
||||
const newData = computed((): any => {
|
||||
return {config: store.config, clients: store.clients}
|
||||
return {config: store.config, clients: store.clients, tls: store.tlsConfigs}
|
||||
})
|
||||
|
||||
const stateChange = computed((): any => {
|
||||
|
||||
@@ -54,6 +54,7 @@ const menu = [
|
||||
{ title: 'pages.clients', icon: 'mdi-account-multiple', path: '/clients' },
|
||||
{ title: 'pages.outbounds', icon: 'mdi-cloud-upload', path: '/outbounds' },
|
||||
{ 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' },
|
||||
{ title: 'pages.admins', icon: 'mdi-account-tie', path: '/admins' },
|
||||
{ title: 'pages.settings', icon: 'mdi-cog', path: '/settings' },
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="90%" max-width="800" :loading="loading">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ $t('admin.changes') }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto"><v-icon icon="mdi-close-box" @click="$emit('close')" /></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="4" md="3">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('admin.actor')"
|
||||
:items="['', 'DepleteJob', ...admins]"
|
||||
v-model="user"
|
||||
@update:model-value="loadData">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="4" md="3">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('admin.key')"
|
||||
:items="['', 'inbounds', 'outbounds', 'clients', 'route', 'tls', 'experimental']"
|
||||
v-model="key"
|
||||
@update:model-value="loadData">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4" md="3">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('count')"
|
||||
:items="[10,20,30,50,100]"
|
||||
v-model.number="chngCount"
|
||||
@update:model-value="loadData">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="auto" align="center" justify="center">
|
||||
<v-btn
|
||||
icon="mdi-refresh"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="loadData">
|
||||
<v-icon />
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-data-table
|
||||
:headers="changesHeaders"
|
||||
:items="changes"
|
||||
item-value="id"
|
||||
density="compact"
|
||||
show-expand
|
||||
items-per-page="10"
|
||||
>
|
||||
<template v-slot:item.dateTime="{ value }">
|
||||
<v-chip variant="text" dir="ltr" density="compact">
|
||||
{{ dateFormatted(value) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.action="{ value }">
|
||||
<v-chip density="compact">
|
||||
{{ $t('actions.' + value) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:expanded-row="{ columns, item }">
|
||||
<tr>
|
||||
<td :colspan="columns.length">
|
||||
<v-card dir="ltr" v-if="item.index>0">Index: {{ item.index }}</v-card>
|
||||
<v-card style="background-color: background" dir="ltr"><pre>{{ item.obj }}</pre></v-card>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { i18n } from '@/locales'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
|
||||
export default {
|
||||
props: ['admins', 'actor', 'visible'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
changes: <any[]>[],
|
||||
user: '',
|
||||
key: '',
|
||||
chngCount: 10,
|
||||
expanded: [],
|
||||
changesHeaders: [
|
||||
{ title: 'ID', key: 'id' },
|
||||
{ title: i18n.global.t('admin.date') + '-' + i18n.global.t('admin.time'), key: 'dateTime' },
|
||||
{ title: i18n.global.t('admin.actor'), key: 'Actor' },
|
||||
{ title: i18n.global.t('admin.key'), key: 'key' },
|
||||
{ title: i18n.global.t('admin.action'), key: 'action' },
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await HttpUtils.get('api/changes',{ a: this.user, k: this.key, c: this.chngCount })
|
||||
if (data.success) {
|
||||
this.changes = data.obj?? []
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
dateFormatted(dt: number): string {
|
||||
const date = new Date(dt*1000)
|
||||
return date.toLocaleString(this.locale)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
locale() {
|
||||
const l = i18n.global.locale.value
|
||||
return l.replace('zh', 'zh-')
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
this.changes = []
|
||||
this.user = this.$props.actor
|
||||
this.key = ''
|
||||
this.chngCount = 10
|
||||
if (newValue) {
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -179,7 +179,7 @@ export default {
|
||||
const newData = JSON.parse(this.$props.data)
|
||||
this.client = createClient(newData)
|
||||
this.title = "edit"
|
||||
this.clientConfig = JSON.parse(this.client.config)
|
||||
this.clientConfig = this.client.config
|
||||
}
|
||||
else {
|
||||
this.client = createClient()
|
||||
@@ -187,10 +187,9 @@ export default {
|
||||
this.clientConfig = randomConfigs('client')
|
||||
}
|
||||
this.clientStats = this.$props.stats
|
||||
const allLinks = <Link[]>JSON.parse(this.client.links)
|
||||
this.links = allLinks.filter(l => l.type == 'local')
|
||||
this.extLinks = allLinks.filter(l => l.type == 'external')
|
||||
this.subLinks = allLinks.filter(l => l.type == 'sub')
|
||||
this.links = this.client.links.filter(l => l.type == 'local')
|
||||
this.extLinks = this.client.links.filter(l => l.type == 'external')
|
||||
this.subLinks = this.client.links.filter(l => l.type == 'sub')
|
||||
this.tab = "t1"
|
||||
},
|
||||
closeModal() {
|
||||
@@ -199,11 +198,11 @@ export default {
|
||||
},
|
||||
saveChanges() {
|
||||
this.loading = true
|
||||
this.client.config = updateConfigs(JSON.stringify(this.clientConfig), this.client.name)
|
||||
this.client.links = JSON.stringify([
|
||||
...this.links,
|
||||
...this.extLinks.filter(l => l.uri != ''),
|
||||
...this.subLinks.filter(l => l.uri != '')])
|
||||
this.client.config = updateConfigs(this.clientConfig, this.client.name)
|
||||
this.client.links = [
|
||||
...this.links,
|
||||
...this.extLinks.filter(l => l.uri != ''),
|
||||
...this.subLinks.filter(l => l.uri != '')]
|
||||
this.$emit('save', this.client, this.clientStats)
|
||||
this.loading = false
|
||||
},
|
||||
@@ -213,8 +212,8 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
clientInbounds: {
|
||||
get() { return this.client.inbounds == "" ? [] : this.client.inbounds.split(',').filter(i => this.inboundTags.includes(i)) },
|
||||
set(newValue:string[]) { this.client.inbounds = newValue.length == 0 ? "" : newValue.join(',') }
|
||||
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 }
|
||||
},
|
||||
expDate: {
|
||||
get() { return this.client.expiry},
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<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" :id="id" />
|
||||
<InTls v-if="Object.hasOwn(inbound,'tls')" :inbound="inbound" />
|
||||
<InTls v-if="Object.hasOwn(inbound,'tls')" :inbound="inbound" :tlsConfigs="tlsConfigs" :tls_id="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-card-text>
|
||||
@@ -57,7 +57,6 @@
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
|
||||
<script lang="ts">
|
||||
import { InTypes, createInbound } from '@/types/inbounds'
|
||||
import Listen from '@/components/Listen.vue'
|
||||
@@ -75,7 +74,7 @@ import RandomUtil from '@/plugins/randomUtil'
|
||||
import Multiplex from '@/components/Multiplex.vue'
|
||||
import Transport from '@/components/Transport.vue'
|
||||
export default {
|
||||
props: ['visible', 'data', 'id', 'stats', 'inTags', 'outTags'],
|
||||
props: ['visible', 'data', 'id', 'stats', 'inTags', 'outTags', 'tlsConfigs'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
@@ -84,6 +83,7 @@ export default {
|
||||
loading: false,
|
||||
inTypes: InTypes,
|
||||
inboundStats: false,
|
||||
tls_id: { value: 0 },
|
||||
HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks],
|
||||
}
|
||||
},
|
||||
@@ -92,6 +92,7 @@ export default {
|
||||
if (this.$props.id != -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
|
||||
this.title = "edit"
|
||||
}
|
||||
else {
|
||||
@@ -114,7 +115,7 @@ export default {
|
||||
},
|
||||
saveChanges() {
|
||||
this.loading = true
|
||||
this.$emit('save', this.inbound, this.inboundStats)
|
||||
this.$emit('save', this.inbound, this.inboundStats, this.tls_id.value)
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="90%" max-width="1200" :loading="loading">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ $t('basic.log.title') + " - " + (logType == 's-ui'? "S-UI" : "Sing-Box") }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-icon icon="mdi-close" @click="$emit('close')" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('basic.log.level')"
|
||||
:items="logLevels"
|
||||
v-model="logLevel"
|
||||
@update:model-value="loadData">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('count')"
|
||||
:items="[10,20,30,50,100]"
|
||||
v-model.number="logCount"
|
||||
@update:model-value="loadData">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="auto" align="center" justify="center">
|
||||
<v-btn
|
||||
icon="mdi-refresh"
|
||||
variant="tonal"
|
||||
:loading="loading"
|
||||
@click="loadData">
|
||||
<v-icon />
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card style="background-color: background" dir="ltr" v-html="lines.join('<br />')"></v-card>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import HttpUtils from '@/plugins/httputil';
|
||||
|
||||
export default {
|
||||
props: ['logType', 'visible'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
lines: [],
|
||||
logLevel: 'info',
|
||||
logLevels: [
|
||||
{ title: 'DEBUG', value: 'debug' },
|
||||
{ title: 'INFO', value: 'info' },
|
||||
{ title: 'WARNING', value: 'warning' },
|
||||
{ title: 'ERROR', value: 'err' },
|
||||
],
|
||||
logCount: 10,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await HttpUtils.get('api/logs',{ s: this.$props.logType, c: this.logCount, l: this.logLevel })
|
||||
if (data.success) {
|
||||
this.lines = data.obj?? []
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
this.lines = []
|
||||
this.logLevel = 'info'
|
||||
this.logCount = 10
|
||||
if (newValue) {
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -30,7 +30,7 @@
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.addr')"
|
||||
:label="$t('out.port')"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="400">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card class="rounded-lg" id="qrcode-modal">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>QrCode</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="1"><v-icon icon="mdi-close-box" @click="$emit('close')" ></v-icon></v-col>
|
||||
<v-col cols="auto"><v-icon icon="mdi-close-box" @click="$emit('close')" /></v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
@@ -18,7 +18,7 @@
|
||||
</v-row>
|
||||
<v-row v-for="l in clientLinks">
|
||||
<v-col style="text-align: center;" @click="copyToClipboard(l.uri)">
|
||||
<v-chip>{{ l.remark }}</v-chip><br />
|
||||
<v-chip>{{ l.remark?? "-" }}</v-chip><br />
|
||||
<QrcodeVue :value="l.uri" :size="300" :margin="1" style="border-radius: 1rem;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -31,38 +31,45 @@
|
||||
import QrcodeVue from 'qrcode.vue'
|
||||
import Data from '@/store/modules/data'
|
||||
import Clipboard from 'clipboard'
|
||||
import Message from '@/store/modules/message'
|
||||
import { i18n } from '@/locales'
|
||||
import { push } from 'notivue'
|
||||
|
||||
export default {
|
||||
props: ['index'],
|
||||
data() {
|
||||
return {
|
||||
msg: Message(),
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
copyToClipboard(txt:string) {
|
||||
const hiddenButton = document.createElement('button')
|
||||
hiddenButton.className = 'clipboard-btn'
|
||||
document.body.appendChild(hiddenButton)
|
||||
|
||||
const clipboard = new Clipboard('.clipboard-btn', {
|
||||
text: () => txt
|
||||
text: () => txt,
|
||||
container: document.getElementById('qrcode-modal')?? undefined
|
||||
});
|
||||
|
||||
clipboard.on('success', () => {
|
||||
clipboard.destroy()
|
||||
this.msg.showMessage(i18n.global.t('copyToClipboard') + " : " + i18n.global.t('success'),'success',5000)
|
||||
push.success({
|
||||
message: i18n.global.t('success') + ": " + i18n.global.t('copyToClipboard'),
|
||||
duration: 5000,
|
||||
})
|
||||
})
|
||||
|
||||
clipboard.on('error', () => {
|
||||
clipboard.destroy()
|
||||
this.msg.showMessage(i18n.global.t('copyToClipboard') + " : " + i18n.global.t('failed'),'error',5000)
|
||||
push.error({
|
||||
message: i18n.global.t('failed') + ": " + i18n.global.t('copyToClipboard'),
|
||||
duration: 5000,
|
||||
})
|
||||
})
|
||||
|
||||
// Perform click on hidden button to trigger copy
|
||||
const hiddenButton = document.createElement('button');
|
||||
hiddenButton.className = 'clipboard-btn';
|
||||
document.body.appendChild(hiddenButton);
|
||||
hiddenButton.click();
|
||||
document.body.removeChild(hiddenButton);
|
||||
hiddenButton.click()
|
||||
document.body.removeChild(hiddenButton)
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -75,7 +82,7 @@ export default {
|
||||
return Data().subURI + this.client.name
|
||||
},
|
||||
clientLinks() {
|
||||
return JSON.parse(this.client.links?? "[]")
|
||||
return this.client.links?? []
|
||||
}
|
||||
},
|
||||
components: { QrcodeVue }
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
<template>
|
||||
<v-dialog transition="dialog-bottom-transition" width="800">
|
||||
<v-card class="rounded-lg">
|
||||
<v-card-title>
|
||||
{{ $t('actions.' + title) + " " + $t('objects.tls') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-card class="rounded-lg">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('client.name')"
|
||||
hide-details
|
||||
v-model="tls.name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col align="end">
|
||||
<v-btn-toggle v-model="tlsType"
|
||||
class="rounded-xl"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
@update:model-value="changeTlsType"
|
||||
shaped
|
||||
mandatory>
|
||||
<v-btn>TLS</v-btn>
|
||||
<v-btn>Reality</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="inTls.server_name != undefined">
|
||||
<v-text-field
|
||||
label="SNI"
|
||||
hide-details
|
||||
v-model="inTls.server_name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<template v-if="tlsType == 0">
|
||||
<v-col cols="12" sm="6" md="4" v-if="inTls.min_version">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('tls.minVer')"
|
||||
:items="tlsVersions"
|
||||
v-model="inTls.min_version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="inTls.max_version">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('tls.maxVer')"
|
||||
:items="tlsVersions"
|
||||
v-model="inTls.max_version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="inTls.alpn">
|
||||
<v-select
|
||||
hide-details
|
||||
label="ALPN"
|
||||
multiple
|
||||
:items="alpn"
|
||||
v-model="inTls.alpn">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" md="8" v-if="inTls.cipher_suites != undefined">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('tls.cs')"
|
||||
multiple
|
||||
:items="cipher_suites"
|
||||
v-model="inTls.cipher_suites">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
<template v-if="tlsType == 0">
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-btn-toggle v-model="usePath"
|
||||
class="rounded-xl"
|
||||
density="compact"
|
||||
variant="outlined"
|
||||
shaped
|
||||
mandatory>
|
||||
<v-btn
|
||||
@click="inTls.key=undefined; inTls.certificate=undefined"
|
||||
>{{ $t('tls.usePath') }}</v-btn>
|
||||
<v-btn
|
||||
@click="inTls.key_path=undefined; inTls.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">
|
||||
<v-text-field
|
||||
:label="$t('tls.certPath')"
|
||||
hide-details
|
||||
v-model="inTls.certificate_path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-text-field
|
||||
:label="$t('tls.keyPath')"
|
||||
hide-details
|
||||
v-model="inTls.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="outTls.utls != undefined">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Fingerprint"
|
||||
:items="fingerprints"
|
||||
v-model="outTls.utls.fingerprint">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.disableSni')" v-model="disableSni" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" :label="$t('tls.insecure')" v-model="insecure" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<template v-if="outTls.reality && inTls.reality">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('types.shdwTls.hs')"
|
||||
hide-details
|
||||
v-model="inTls.reality.handshake.server">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.port')"
|
||||
type="number"
|
||||
min="0"
|
||||
hide-details
|
||||
v-model="server_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
:label="$t('tls.privKey')"
|
||||
hide-details
|
||||
v-model="inTls.reality.private_key">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
:label="$t('tls.pubKey')"
|
||||
hide-details
|
||||
v-model="outTls.reality.public_key">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
label="Short IDs"
|
||||
hide-details
|
||||
v-model="short_id">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionTime">
|
||||
<v-text-field
|
||||
label="Max Time Diference"
|
||||
type="number"
|
||||
min="1"
|
||||
:suffix="$t('date.m')"
|
||||
hide-details
|
||||
v-model="max_time">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details>{{ $t('tls.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<template v-if="tlsType == 0">
|
||||
<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-item>
|
||||
<v-switch v-model="optionFP" color="primary" label="UTLS" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionTime" color="primary" label="Max Time Difference" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
<AcmeVue :tls="inTls" />
|
||||
<EchVue :iTls="inTls" :oTls="outTls" />
|
||||
</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 { iTls, defaultInTls } from '@/types/inTls'
|
||||
import { oTls, defaultOutTls } from '@/types/outTls'
|
||||
import AcmeVue from '@/components/Acme.vue'
|
||||
import EchVue from '@/components/Ech.vue'
|
||||
export default {
|
||||
props: ['visible', 'data', 'index'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
tls: { id: -1, name: '', inbounds: [], server: <iTls>{ enabled: true }, client: <oTls>{} },
|
||||
title: "add",
|
||||
loading: false,
|
||||
menu: false,
|
||||
tlsType: 0,
|
||||
usePath: 0,
|
||||
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" }
|
||||
],
|
||||
fingerprints: [
|
||||
{ title: "Chrome", value: "chrome" },
|
||||
{ title: "Chrome PSK", value: "chrome_psk" },
|
||||
{ title: "Chrome PSK Shuffle", value: "chrome_psk_shuffle" },
|
||||
{ title: "Chrome Padding PSK Shuffle", value: "chrome_padding_psk_shuffle" },
|
||||
{ title: "Chrome Post-Quantum", value: "chrome_pq" },
|
||||
{ title: "Chrome Post-Quantum PSK", value: "chrome_pq_psk" },
|
||||
{ title: "Firefox", value: "firefox" },
|
||||
{ title: "Microsoft Edge", value: "edge" },
|
||||
{ title: "Apple Safari", value: "safari" },
|
||||
{ title: "360", value: "360" },
|
||||
{ title: "QQ", value: "qq" },
|
||||
{ title: "Apple IOS", value: "ios" },
|
||||
{ title: "Android", value: "android" },
|
||||
{ title: "Random", value: "random" },
|
||||
{ title: "Randomized", value: "randomized" },
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData() {
|
||||
if (this.$props.index != -1) {
|
||||
const newData = JSON.parse(this.$props.data)
|
||||
this.tls = newData
|
||||
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.tlsType = 0
|
||||
this.usePath = 0
|
||||
this.title = "add"
|
||||
}
|
||||
},
|
||||
changeTlsType(){
|
||||
if (this.tlsType) {
|
||||
this.tls.server = <iTls>{ enabled: true, reality: { enabled: true, handshake: { server_port: 443 } }, server_name: "" }
|
||||
this.tls.client = <oTls>{ reality: { public_key: "" } }
|
||||
} else {
|
||||
this.tls.server = <iTls>{ enabled: true }
|
||||
this.tls.client = <oTls>{}
|
||||
}
|
||||
},
|
||||
closeModal() {
|
||||
this.updateData() // reset
|
||||
this.$emit('close')
|
||||
},
|
||||
saveChanges() {
|
||||
this.loading = true
|
||||
this.$emit('save', this.tls)
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
inTls(): iTls {
|
||||
return <iTls> this.tls.server
|
||||
},
|
||||
outTls(): oTls {
|
||||
return <oTls> this.tls.client
|
||||
},
|
||||
certText: {
|
||||
get(): string { return this.inTls.certificate ? this.inTls.certificate.join('\n') : '' },
|
||||
set(v:string) { this.inTls.certificate = v.split('\n') }
|
||||
},
|
||||
keyText: {
|
||||
get(): string { return this.inTls.key ? this.inTls.key.join('\n') : '' },
|
||||
set(v:string) { this.inTls.key = v.split('\n') }
|
||||
},
|
||||
disableSni: {
|
||||
get() { return this.outTls.disable_sni ?? false },
|
||||
set(v: boolean) { this.outTls.disable_sni = v ? true : undefined }
|
||||
},
|
||||
insecure: {
|
||||
get() { return this.outTls.insecure ?? false },
|
||||
set(v: boolean) { this.outTls.insecure = v ? true : undefined }
|
||||
},
|
||||
server_port: {
|
||||
get() { return this.inTls.reality?.handshake?.server_port ? this.inTls.reality.handshake.server_port : 443 },
|
||||
set(v: any) {
|
||||
if (this.inTls.reality){
|
||||
this.inTls.reality.handshake.server_port = v.length == 0 || v == 0 ? 443 : parseInt(v)
|
||||
}
|
||||
}
|
||||
},
|
||||
short_id: {
|
||||
get() { return this.inTls.reality?.short_id ? this.inTls.reality.short_id.join(',') : undefined },
|
||||
set(v: string) {
|
||||
if (this.inTls.reality){
|
||||
this.inTls.reality.short_id = v.length > 0 ? v.split(',') : []
|
||||
}
|
||||
}
|
||||
},
|
||||
max_time: {
|
||||
get() { return this.inTls?.reality?.max_time_difference ? this.inTls.reality.max_time_difference.replace('m','') : 1 },
|
||||
set(v: number) {
|
||||
if (this.inTls.reality){
|
||||
this.inTls.reality.max_time_difference = v > 0 ? v + 'm' : '1m'
|
||||
}
|
||||
}
|
||||
},
|
||||
optionSNI: {
|
||||
get(): boolean { return this.inTls.server_name != undefined },
|
||||
set(v:boolean) { this.inTls.server_name = v ? '' : undefined }
|
||||
},
|
||||
optionALPN: {
|
||||
get(): boolean { return this.inTls.alpn != undefined },
|
||||
set(v:boolean) { this.inTls.alpn = v ? defaultInTls.alpn : undefined }
|
||||
},
|
||||
optionMinV: {
|
||||
get(): boolean { return this.inTls.min_version != undefined },
|
||||
set(v:boolean) { this.inTls.min_version = v ? defaultInTls.min_version : undefined }
|
||||
},
|
||||
optionMaxV: {
|
||||
get(): boolean { return this.inTls.max_version != undefined },
|
||||
set(v:boolean) { this.inTls.max_version = v ? defaultInTls.max_version : undefined }
|
||||
},
|
||||
optionCS: {
|
||||
get(): boolean { return this.inTls.cipher_suites != undefined },
|
||||
set(v:boolean) { this.inTls.cipher_suites = v ? defaultInTls.cipher_suites : undefined }
|
||||
},
|
||||
optionFP: {
|
||||
get(): boolean { return this.outTls.utls != undefined },
|
||||
set(v:boolean) { this.outTls.utls = v ? defaultOutTls.utls : undefined }
|
||||
},
|
||||
optionEch: {
|
||||
get(): boolean { return this.outTls.ech != undefined },
|
||||
set(v:boolean) { this.outTls.ech = v ? defaultOutTls.ech : undefined }
|
||||
},
|
||||
optionTime: {
|
||||
get(): boolean { return this.inTls?.reality?.max_time_difference != undefined },
|
||||
set(v:boolean) { if (this.inTls.reality) this.inTls.reality.max_time_difference = v ? "1m" : undefined }
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.updateData()
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { AcmeVue, EchVue }
|
||||
}
|
||||
</script>
|
||||
@@ -21,9 +21,12 @@ export default {
|
||||
invalidLogin: "Invalid Login!",
|
||||
online: "Online",
|
||||
version: "Version",
|
||||
email: "Email",
|
||||
commaSeparated: "(comma separated)",
|
||||
count: "Count",
|
||||
error: {
|
||||
dplData: "Duplicate Data",
|
||||
core: "Sing-Box Error",
|
||||
},
|
||||
pages: {
|
||||
login: "Login",
|
||||
@@ -32,6 +35,7 @@ export default {
|
||||
outbounds: "Outbounds",
|
||||
clients: "Clients",
|
||||
rules: "Rules",
|
||||
tls: "TLS Settings",
|
||||
basics: "Basics",
|
||||
admins: "Admins",
|
||||
settings: "Settings",
|
||||
@@ -83,11 +87,14 @@ export default {
|
||||
actions: {
|
||||
action: "Action",
|
||||
add: "Add",
|
||||
new: "Add",
|
||||
edit: "Edit",
|
||||
del: "Delete",
|
||||
save: "Save",
|
||||
update: "Update",
|
||||
submit: "Submit",
|
||||
set: "Set",
|
||||
disable: "Disable",
|
||||
close: "Close",
|
||||
restartApp: "Restart App",
|
||||
},
|
||||
@@ -109,6 +116,10 @@ export default {
|
||||
lastLogin: "Last login",
|
||||
date: "Date",
|
||||
time: "Time",
|
||||
changes: "Changes",
|
||||
actor: "Actor",
|
||||
key: "Key",
|
||||
action: "Action",
|
||||
},
|
||||
setting: {
|
||||
interface: "Interface",
|
||||
@@ -145,7 +156,6 @@ export default {
|
||||
direct: {
|
||||
overrideAddr: "Override Address",
|
||||
overridePort: "Override Port",
|
||||
proxyProtocol: "Proxy Protocol",
|
||||
},
|
||||
hy: {
|
||||
obfs: "Obfuscated Password",
|
||||
@@ -326,9 +336,26 @@ export default {
|
||||
minVer: "Minimum Version",
|
||||
maxVer: "Maximum Version",
|
||||
cs: "Cipher suits",
|
||||
privKey: "Private Key",
|
||||
pubKey: "Public Key",
|
||||
disableSni: "Disable SNI",
|
||||
insecure: "Allow Insecure",
|
||||
acme: {
|
||||
options: "ACME Options",
|
||||
dataDir: "Data Directory",
|
||||
defaultDomain: "Default Domain",
|
||||
disableChallenges: "Disable Challenges",
|
||||
httpChallenge: "Disable HTTP Challenge",
|
||||
tlsChallenge: "Disable TLS Challenge",
|
||||
altPorts: "Alternative Ports",
|
||||
altHport: "Alternative HTTP Port",
|
||||
altTport: "Alternative TLS Port",
|
||||
caProvider: "CA Provider",
|
||||
customCa: "Custom CA Provider",
|
||||
extAcc: "External Account",
|
||||
dns01: "DNS01 Challenge",
|
||||
dns01Provider: "DNS01 Challenge Provider",
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
upload: "Upload",
|
||||
|
||||
@@ -21,9 +21,12 @@ export default {
|
||||
invalidLogin: "ورود نامعتبر!",
|
||||
online: "آنلاین",
|
||||
version: "نسخه",
|
||||
email: "ایمیل",
|
||||
commaSeparated: "(جداشده با کاما)",
|
||||
count: "تعداد",
|
||||
error: {
|
||||
dplData: "داده تکراری",
|
||||
core: "خطا در سینگباکس",
|
||||
},
|
||||
pages: {
|
||||
login: "ورود",
|
||||
@@ -32,6 +35,7 @@ export default {
|
||||
outbounds: "خروجیها",
|
||||
clients: "کاربران",
|
||||
rules: "قوانین",
|
||||
tls: "رمزنگاریها",
|
||||
basics: "ترازها",
|
||||
admins: "ادمینها",
|
||||
settings: "پیکربندی",
|
||||
@@ -82,11 +86,14 @@ export default {
|
||||
actions: {
|
||||
action: "فرمان",
|
||||
add: "ایجاد",
|
||||
new: "ایجاد",
|
||||
edit: "ویرایش",
|
||||
del: "حذف",
|
||||
save: "ذخیره",
|
||||
update: "بروزرسانی",
|
||||
submit: "ارسال",
|
||||
set: "تنظیم",
|
||||
disable: "غیرفعال",
|
||||
close: "بستن",
|
||||
restartApp: "ریستارت پنل",
|
||||
},
|
||||
@@ -108,6 +115,10 @@ export default {
|
||||
lastLogin: "آخرین ورود",
|
||||
date: "تاریخ",
|
||||
time: "ساعت",
|
||||
changes: "تغییرات",
|
||||
actor: "مجری",
|
||||
key: "کلید",
|
||||
action: "عمل",
|
||||
},
|
||||
setting: {
|
||||
interface: "نما",
|
||||
@@ -144,7 +155,6 @@ export default {
|
||||
direct: {
|
||||
overrideAddr: "جایگزین آدرس",
|
||||
overridePort: "جایگزین پورت",
|
||||
proxyProtocol: "پروتکل پراکسی",
|
||||
},
|
||||
hy: {
|
||||
obfs: "رمز مبهم کننده",
|
||||
@@ -325,9 +335,26 @@ export default {
|
||||
minVer: "کمینه نسخه",
|
||||
maxVer: "بیشینه نسخه",
|
||||
cs: "مدلهای رمزنگاری",
|
||||
privKey: "کلید خصوصی",
|
||||
pubKey: "کلید عمومی",
|
||||
disableSni: "غیرفعالسازی SNI",
|
||||
insecure: "تایید ارتباط ناامن",
|
||||
acme: {
|
||||
options: "گزینههای ACME",
|
||||
dataDir: "مسیر دادهها",
|
||||
defaultDomain: "دامنه پیشفرض",
|
||||
disableChallenges: "بستن چالشها",
|
||||
httpChallenge: "بستن چالش HTTP",
|
||||
tlsChallenge: "بستن چالش TLS",
|
||||
altPorts: "پورتهای جایگزین",
|
||||
altHport: "پورت جایگزین HTTP",
|
||||
altTport: "پورت جایگزین TLS",
|
||||
caProvider: "فراهم کننده گواهی",
|
||||
customCa: "فراهم کننده دیگر",
|
||||
extAcc: "حساب خارجی",
|
||||
dns01: "چالش DNS01",
|
||||
dns01Provider: "فراهم کننده چالش DNS01",
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
upload: "آپلود",
|
||||
|
||||
@@ -21,9 +21,12 @@ export default {
|
||||
invalidLogin: "Đăng nhập không hợp lệ!",
|
||||
online: "Trực tuyến",
|
||||
version: "Phiên bản",
|
||||
email: "Email",
|
||||
commaSeparated: "(được phân tách bằng dấu phẩy)",
|
||||
count: "Đếm",
|
||||
error: {
|
||||
dplData: "Dữ liệu trùng lặp",
|
||||
core: "Lỗi Sing-Box",
|
||||
},
|
||||
pages: {
|
||||
login: "Đăng nhập",
|
||||
@@ -32,6 +35,7 @@ export default {
|
||||
outbounds: "Đầu ra",
|
||||
clients: "Khách hàng",
|
||||
rules: "Quy tắc",
|
||||
tls: "Cài đặt TLS",
|
||||
basics: "Cơ bản",
|
||||
admins: "Quản trị viên",
|
||||
settings: "Cài đặt",
|
||||
@@ -83,11 +87,14 @@ export default {
|
||||
actions: {
|
||||
action: "Hành động",
|
||||
add: "Thêm",
|
||||
new: "Thêm",
|
||||
edit: "Chỉnh sửa",
|
||||
del: "Xóa",
|
||||
save: "Lưu",
|
||||
update: "Cập nhật",
|
||||
submit: "Gửi",
|
||||
set: "Đặt",
|
||||
disable: "Vô hiệu hóa",
|
||||
close: "Đóng",
|
||||
restartApp: "Khởi động lại ứng dụng",
|
||||
},
|
||||
@@ -109,6 +116,10 @@ export default {
|
||||
lastLogin: "Lân đăng nhập cuôi",
|
||||
date: "Ngày",
|
||||
time: "Thời gian",
|
||||
changes: "Thay đổi",
|
||||
actor: "Diễn viên",
|
||||
key: "Khóa",
|
||||
action: "Hành động",
|
||||
},
|
||||
setting: {
|
||||
interface: "Giao diện",
|
||||
@@ -145,7 +156,6 @@ export default {
|
||||
direct: {
|
||||
overrideAddr: "Ghi đè Địa chỉ",
|
||||
overridePort: "Ghi đè Cổng",
|
||||
proxyProtocol: "Giao thức Proxy",
|
||||
},
|
||||
hy: {
|
||||
obfs: "Mật khẩu đã được Ẩn",
|
||||
@@ -327,9 +337,26 @@ export default {
|
||||
minVer: "Phiên bản Tối thiểu",
|
||||
maxVer: "Phiên bản Tối đa",
|
||||
cs: "Các bộ mã hóa",
|
||||
privKey: "Khóa riêng",
|
||||
pubKey: "Khóa Công khai",
|
||||
disableSni: "Tắt SNI",
|
||||
insecure: "Cho phép Không an toàn",
|
||||
acme: {
|
||||
options: "Tùy chọn ACME",
|
||||
dataDir: "Thư mục Dữ liệu",
|
||||
defaultDomain: "Tên miền Mặc định",
|
||||
disableChallenges: "Vô hiệu hóa Thách thức",
|
||||
httpChallenge: "Vô hiệu hóa Thách thức HTTP",
|
||||
tlsChallenge: "Vô hiệu hóa Thách thức TLS",
|
||||
altPorts: "Cổng Thay thế",
|
||||
altHport: "Cổng HTTP Thay thế",
|
||||
altTport: "Cổng TLS Thay thế",
|
||||
caProvider: "Nhà cung cấp CA",
|
||||
customCa: "Nhà cung cấp CA Tùy chỉnh",
|
||||
extAcc: "Tài khoản Bên ngoài",
|
||||
dns01: "Thách thức DNS01",
|
||||
dns01Provider: "Nhà cung cấp Thách thức DNS01"
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
upload: "Tải lên",
|
||||
|
||||
@@ -21,9 +21,12 @@ export default {
|
||||
invalidLogin: "登录无效!",
|
||||
online: "在线",
|
||||
version: "版本",
|
||||
email: "电子邮件",
|
||||
commaSeparated: "(逗号分隔)",
|
||||
count: "计数",
|
||||
error: {
|
||||
dplData: "重复数据",
|
||||
core: "Sing-Box 错误",
|
||||
},
|
||||
pages: {
|
||||
login: "登录",
|
||||
@@ -32,6 +35,7 @@ export default {
|
||||
outbounds: "出站管理",
|
||||
clients: "用户管理",
|
||||
rules: "路由列表",
|
||||
tls: "TLS 设置",
|
||||
basics: "基础信息",
|
||||
admins: "管理员",
|
||||
settings: "设置",
|
||||
@@ -83,11 +87,14 @@ export default {
|
||||
actions: {
|
||||
action: "操作",
|
||||
add: "添加",
|
||||
new: "添加",
|
||||
edit: "编辑",
|
||||
del: "删除",
|
||||
save: "保存",
|
||||
update: "更新",
|
||||
submit: "提交",
|
||||
set: "设置",
|
||||
disable: "禁用",
|
||||
close: "关闭",
|
||||
restartApp: "重启面板",
|
||||
},
|
||||
@@ -109,6 +116,10 @@ export default {
|
||||
lastLogin: "上次登录",
|
||||
date: "日期",
|
||||
time: "时间",
|
||||
changes: "更改",
|
||||
actor: "执行者",
|
||||
key: "键",
|
||||
action: "操作",
|
||||
},
|
||||
setting: {
|
||||
interface: "界面",
|
||||
@@ -145,7 +156,6 @@ export default {
|
||||
direct: {
|
||||
overrideAddr: "覆盖地址",
|
||||
overridePort: "覆盖端口",
|
||||
proxyProtocol: "代理协议",
|
||||
},
|
||||
hy: {
|
||||
obfs: "混淆密码",
|
||||
@@ -327,9 +337,26 @@ export default {
|
||||
minVer: "最低版本",
|
||||
maxVer: "最高版本",
|
||||
cs: "密码套件",
|
||||
privKey: "私钥",
|
||||
pubKey: "公钥",
|
||||
disableSni: "禁用SNI",
|
||||
insecure: "允许不安全",
|
||||
acme: {
|
||||
options: "ACME 选项",
|
||||
dataDir: "数据目录",
|
||||
defaultDomain: "默认域名",
|
||||
disableChallenges: "禁用挑战",
|
||||
httpChallenge: "禁用 HTTP 挑战",
|
||||
tlsChallenge: "禁用 TLS 挑战",
|
||||
altPorts: "替代端口",
|
||||
altHport: "替代 HTTP 端口",
|
||||
altTport: "替代 TLS 端口",
|
||||
caProvider: "CA 提供商",
|
||||
customCa: "自定义 CA 提供商",
|
||||
extAcc: "外部账户",
|
||||
dns01: "DNS01 挑战",
|
||||
dns01Provider: "DNS01 挑战提供商"
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
upload: "上传",
|
||||
|
||||
@@ -22,9 +22,12 @@ export default {
|
||||
invalidLogin: "登錄無效!",
|
||||
online: "在線",
|
||||
version: "版本",
|
||||
email: "電子郵件",
|
||||
commaSeparated: "(逗號分隔)",
|
||||
count: "計數",
|
||||
error: {
|
||||
dplData: "重複數據",
|
||||
core: "Sing-Box 錯誤",
|
||||
},
|
||||
pages: {
|
||||
login: "登錄",
|
||||
@@ -33,6 +36,7 @@ export default {
|
||||
outbounds: "出站管理",
|
||||
clients: "用戶管理",
|
||||
rules: "路由列表",
|
||||
tls: "TLS 設置",
|
||||
basics: "基礎信息",
|
||||
admins: "管理員",
|
||||
settings: "設置",
|
||||
@@ -84,11 +88,14 @@ export default {
|
||||
actions: {
|
||||
action: "操作",
|
||||
add: "添加",
|
||||
new: "添加",
|
||||
edit: "編輯",
|
||||
del: "刪除",
|
||||
save: "保存",
|
||||
update: "更新",
|
||||
submit: "提交",
|
||||
set: "設置",
|
||||
disable: "禁用",
|
||||
close: "關閉",
|
||||
restartApp: "重啟面板",
|
||||
},
|
||||
@@ -110,6 +117,10 @@ export default {
|
||||
lastLogin: "上次登入",
|
||||
date: "日期",
|
||||
time: "時間",
|
||||
changes: "更改",
|
||||
actor: "執行者",
|
||||
key: "鍵",
|
||||
action: "操作",
|
||||
},
|
||||
setting: {
|
||||
interface: "界面",
|
||||
@@ -146,7 +157,6 @@ export default {
|
||||
direct: {
|
||||
overrideAddr: "覆蓋地址",
|
||||
overridePort: "覆蓋端口",
|
||||
proxyProtocol: "代理協議",
|
||||
},
|
||||
hy: {
|
||||
obfs: "混淆密碼",
|
||||
@@ -328,9 +338,26 @@ export default {
|
||||
minVer: "最低版本",
|
||||
maxVer: "最高版本",
|
||||
cs: "加密套件",
|
||||
privKey: "私鑰",
|
||||
pubKey: "公鑰",
|
||||
disableSni: "停用 SNI",
|
||||
insecure: "允許不安全連線",
|
||||
acme: {
|
||||
options: "ACME 選項",
|
||||
dataDir: "數據目錄",
|
||||
defaultDomain: "默認域名",
|
||||
disableChallenges: "禁用挑戰",
|
||||
httpChallenge: "禁用 HTTP 挑戰",
|
||||
tlsChallenge: "禁用 TLS 挑戰",
|
||||
altPorts: "替代端口",
|
||||
altHport: "替代 HTTP 端口",
|
||||
altTport: "替代 TLS 端口",
|
||||
caProvider: "CA 提供商",
|
||||
customCa: "自定義 CA 提供商",
|
||||
extAcc: "外部賬戶",
|
||||
dns01: "DNS01 挑戰",
|
||||
dns01Provider: "DNS01 挑戰提供商"
|
||||
},
|
||||
},
|
||||
stats: {
|
||||
upload: "上傳",
|
||||
|
||||
@@ -23,6 +23,22 @@ import { registerPlugins } from '@/plugins'
|
||||
import { i18n } from '@/locales'
|
||||
import Vue3PersianDatetimePicker from 'vue3-persian-datetime-picker'
|
||||
|
||||
// Notivue
|
||||
import { createNotivue } from 'notivue'
|
||||
import 'notivue/notification.css'
|
||||
import 'notivue/animations.css'
|
||||
const notivue = createNotivue({
|
||||
position: 'top-center',
|
||||
limit: 4,
|
||||
enqueue: false,
|
||||
avoidDuplicates: false,
|
||||
notifications: {
|
||||
global: {
|
||||
duration: 3000
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const app = createApp(App)
|
||||
@@ -34,5 +50,6 @@ app
|
||||
.use(router)
|
||||
.use(store)
|
||||
.use(i18n)
|
||||
.use(notivue)
|
||||
.component('DatePicker', Vue3PersianDatetimePicker)
|
||||
.mount('#app')
|
||||
|
||||
@@ -1,12 +1,29 @@
|
||||
import axios from 'axios'
|
||||
import axios from 'axios';
|
||||
|
||||
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
|
||||
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
|
||||
|
||||
axios.defaults.baseURL = "./"
|
||||
const pendingRequests = new Map()
|
||||
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
// Generate a unique key for the request
|
||||
const requestKey = `${config.method}:${config.url}`
|
||||
|
||||
// Check if there is already a pending request with the same key
|
||||
if (pendingRequests.has(requestKey)) {
|
||||
const cancelSource = pendingRequests.get(requestKey)
|
||||
cancelSource.cancel('Duplicate request cancelled')
|
||||
}
|
||||
|
||||
// Create a new cancel token for the request
|
||||
const cancelSource = axios.CancelToken.source()
|
||||
config.cancelToken = cancelSource.token
|
||||
|
||||
// Store the cancel token in the pending requests map
|
||||
pendingRequests.set(requestKey, cancelSource)
|
||||
|
||||
if (config.data instanceof FormData) {
|
||||
config.headers['Content-Type'] = 'multipart/form-data'
|
||||
}
|
||||
@@ -15,6 +32,26 @@ axios.interceptors.request.use(
|
||||
(error) => Promise.reject(error),
|
||||
)
|
||||
|
||||
axios.interceptors.response.use(
|
||||
(response) => {
|
||||
// Remove the request from the pending requests map
|
||||
const requestKey = `${response.config.method}:${response.config.url}`
|
||||
pendingRequests.delete(requestKey)
|
||||
return response
|
||||
},
|
||||
(error) => {
|
||||
if (axios.isCancel(error)) {
|
||||
// Handle duplicate request cancellation here if needed
|
||||
console.warn(error.message)
|
||||
} else {
|
||||
// Remove the request from the pending requests map on error
|
||||
const requestKey = `${error.config.method}:${error.config.url}`
|
||||
pendingRequests.delete(requestKey)
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
);
|
||||
|
||||
const api = axios.create()
|
||||
|
||||
export default api
|
||||
export default api
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import api from './api'
|
||||
import { i18n } from '@/locales'
|
||||
import router from '@/router'
|
||||
import Message from "@/store/modules/message"
|
||||
import { push } from 'notivue'
|
||||
|
||||
export interface Msg {
|
||||
success: boolean
|
||||
@@ -10,18 +10,27 @@ export interface Msg {
|
||||
}
|
||||
|
||||
function _handleMsg(msg: any): void {
|
||||
const sb = Message()
|
||||
if (!isMsg(msg)) {
|
||||
return
|
||||
}
|
||||
if(msg.msg){
|
||||
if (!msg.success && msg.msg == "Invalid login") {
|
||||
sb.showMessage(i18n.global.t('invalidLogin'),'error', 5000)
|
||||
push.error({
|
||||
title: i18n.global.t('invalidLogin'),
|
||||
})
|
||||
logout()
|
||||
return
|
||||
}
|
||||
const message = msg.success ? i18n.global.t('success') + ": " + i18n.global.t('actions.' + msg.msg) : i18n.global.t('failed') + ": " + msg.msg
|
||||
sb.showMessage(message, msg.success ? 'success' : 'error', 5000)
|
||||
if (msg.success) {
|
||||
push.success({
|
||||
message: i18n.global.t('success') + ": " + i18n.global.t('actions.' + msg.msg),
|
||||
})
|
||||
} else {
|
||||
push.error({
|
||||
title: i18n.global.t('failed'),
|
||||
message: msg.msg
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Hysteria, Hysteria2, InTypes, Inbound, Naive, Shadowsocks, TUIC, Trojan, VLESS, VMess } from "@/types/inbounds"
|
||||
import { HTTP, WebSocket, QUIC, gRPC, HTTPUpgrade, Transport, TrspTypes } from "@/types/transport";
|
||||
import { HTTP, WebSocket, gRPC, HTTPUpgrade, Transport, TrspTypes } from "@/types/transport";
|
||||
|
||||
export interface Link {
|
||||
type: "local" | "external" | "sub"
|
||||
@@ -13,25 +13,25 @@ function utf8ToBase64(utf8String: string): string {
|
||||
}
|
||||
|
||||
export namespace LinkUtil {
|
||||
export function linkGenerator(user: string, inbound: Inbound): string {
|
||||
export function linkGenerator(user: string, inbound: Inbound, tlsClient: any = null): string {
|
||||
const addr = location.hostname
|
||||
switch(inbound.type){
|
||||
case InTypes.Shadowsocks:
|
||||
return shadowsocksLink(user,<Shadowsocks>inbound,addr)
|
||||
return shadowsocksLink(user,<Shadowsocks>inbound, addr)
|
||||
case InTypes.Naive:
|
||||
return naiveLink(user,<Naive>inbound,addr)
|
||||
return naiveLink(user,<Naive>inbound, addr, tlsClient)
|
||||
case InTypes.Hysteria:
|
||||
return hysteriaLink(user,<Hysteria>inbound,addr)
|
||||
return hysteriaLink(user,<Hysteria>inbound, addr, tlsClient)
|
||||
case InTypes.Hysteria2:
|
||||
return hysteria2Link(user,<Hysteria2>inbound,addr)
|
||||
return hysteria2Link(user,<Hysteria2>inbound, addr, tlsClient)
|
||||
case InTypes.TUIC:
|
||||
return tuicLink(user,<TUIC>inbound,addr)
|
||||
return tuicLink(user,<TUIC>inbound, addr, tlsClient)
|
||||
case InTypes.VLESS:
|
||||
return vlessLink(user,<VLESS>inbound,addr)
|
||||
return vlessLink(user,<VLESS>inbound, addr, tlsClient)
|
||||
case InTypes.Trojan:
|
||||
return trojanLink(user,<Trojan>inbound,addr)
|
||||
return trojanLink(user,<Trojan>inbound, addr, tlsClient)
|
||||
case InTypes.VMess:
|
||||
return vmessLink(user,<VMess>inbound,addr)
|
||||
return vmessLink(user,<VMess>inbound, addr, tlsClient)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
@@ -40,7 +40,6 @@ export namespace LinkUtil {
|
||||
const userPass = inbound.users?.find(i => i.name == user)?.password
|
||||
const password = [userPass]
|
||||
if (inbound.method.startsWith('2022')) password.push(inbound.password)
|
||||
|
||||
const params = {
|
||||
tfo: inbound.tcp_fast_open? 1 : null,
|
||||
network: inbound.network?? null
|
||||
@@ -56,7 +55,7 @@ export namespace LinkUtil {
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
function hysteriaLink(user: string, inbound: Hysteria, addr: string): string {
|
||||
function hysteriaLink(user: string, inbound: Hysteria, addr: string, tlsClient: any): string {
|
||||
const auth = inbound.users.find(i => i.name == user)?.auth_str
|
||||
const params = {
|
||||
upmbps: inbound.up_mbps?? null,
|
||||
@@ -65,7 +64,8 @@ export namespace LinkUtil {
|
||||
peer: inbound.tls.server_name?? null,
|
||||
alpn: inbound.tls.alpn?.join(',')?? null,
|
||||
obfsParam: inbound.obfs?? null,
|
||||
fastopen: inbound.tcp_fast_open? 1 : 0
|
||||
fastopen: inbound.tcp_fast_open? 1 : 0,
|
||||
insecure: tlsClient?.insecure ? 1 : null
|
||||
}
|
||||
const uri = new URL(`hysteria://${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
@@ -77,7 +77,7 @@ export namespace LinkUtil {
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
function hysteria2Link(user: string, inbound: Hysteria2, addr: string): string {
|
||||
function hysteria2Link(user: string, inbound: Hysteria2, addr: string, tlsClient: any): string {
|
||||
const password = inbound.users.find(i => i.name == user)?.password
|
||||
const params = {
|
||||
upmbps: inbound.up_mbps?? null,
|
||||
@@ -86,7 +86,8 @@ export namespace LinkUtil {
|
||||
alpn: inbound.tls.alpn?.join(',')?? null,
|
||||
obfs: inbound.obfs?.type?? null,
|
||||
'obfs-password': inbound.obfs?.password?? null,
|
||||
fastopen: inbound.tcp_fast_open? 1 : 0
|
||||
fastopen: inbound.tcp_fast_open? 1 : 0,
|
||||
insecure: tlsClient?.insecure ? 1 : null
|
||||
}
|
||||
const uri = new URL(`hysteria2://${password}@${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
@@ -98,13 +99,14 @@ export namespace LinkUtil {
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
function naiveLink(user: string, inbound: Naive, addr: string): string {
|
||||
function naiveLink(user: string, inbound: Naive, addr: string, tlsClient: any): string {
|
||||
const password = inbound.users.find(i => i.username == user)?.password
|
||||
const params = {
|
||||
padding: 1,
|
||||
peer: inbound.tls.server_name?? null,
|
||||
alpn: inbound.tls.alpn?.join(',')?? null,
|
||||
tfo: inbound.tcp_fast_open? 1 : 0
|
||||
tfo: inbound.tcp_fast_open? 1 : 0,
|
||||
allowInsecure: tlsClient?.insecure ? 1 : null
|
||||
}
|
||||
const uri = `http2://${utf8ToBase64(user + ":" + password + "@" + addr + ":" + inbound.listen_port)}`
|
||||
const paramsArray = []
|
||||
@@ -116,12 +118,14 @@ export namespace LinkUtil {
|
||||
return uri.toString() + "?" + paramsArray.join('&') + "#" + inbound.tag
|
||||
}
|
||||
|
||||
function tuicLink(user: string, inbound: TUIC, addr: string): string {
|
||||
function tuicLink(user: string, inbound: TUIC, addr: string, tlsClient: any): string {
|
||||
const u = inbound.users.find(i => i.name == user)
|
||||
const params = {
|
||||
sni: inbound.tls.server_name?? null,
|
||||
alpn: inbound.tls.alpn?.join(',')?? null,
|
||||
congestion_control: inbound.congestion_control?? null
|
||||
congestion_control: inbound.congestion_control?? null,
|
||||
allowInsecure: tlsClient?.insecure ? 1 : null,
|
||||
disable_sni: tlsClient?.disable_sni ? 1 : null
|
||||
}
|
||||
const uri = new URL(`tuic://${u?.uuid}:${u?.password}@${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
@@ -166,7 +170,7 @@ export namespace LinkUtil {
|
||||
return params
|
||||
}
|
||||
|
||||
function vlessLink(user: string, inbound: VLESS, addr: string): string {
|
||||
function vlessLink(user: string, inbound: VLESS, addr: string, tlsClient: any): string {
|
||||
const u = inbound.users.find(i => i.name == user)
|
||||
const transport = <Transport>inbound.transport
|
||||
|
||||
@@ -174,10 +178,14 @@ export namespace LinkUtil {
|
||||
|
||||
const params = {
|
||||
type: transport?.type?? 'tcp',
|
||||
security: inbound.tls?.enabled? 'tls' : null,
|
||||
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
|
||||
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[0] : null) : null
|
||||
}
|
||||
const uri = new URL(`vless://${u?.uuid}@${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries({...params, ...tParams})){
|
||||
@@ -189,7 +197,7 @@ export namespace LinkUtil {
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
function trojanLink(user: string, inbound: Trojan, addr: string): string {
|
||||
function trojanLink(user: string, inbound: Trojan, addr: string, tlsClient: any): string {
|
||||
const u = inbound.users.find(i => i.name == user)
|
||||
const transport = <Transport>inbound.transport
|
||||
|
||||
@@ -197,9 +205,13 @@ export namespace LinkUtil {
|
||||
|
||||
const params = {
|
||||
type: transport?.type?? 'tcp',
|
||||
security: inbound.tls?.enabled? 'tls' : null,
|
||||
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[0] : null) : null
|
||||
}
|
||||
const uri = new URL(`trojan://${u?.password}@${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries({...params, ...tParams})){
|
||||
@@ -211,7 +223,7 @@ export namespace LinkUtil {
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
function vmessLink(user: string, inbound: VMess, addr: string): string {
|
||||
function vmessLink(user: string, inbound: VMess, addr: string, tlsClient: any): string {
|
||||
const u = inbound.users.find(i => i.name == user)
|
||||
const transport = <Transport>inbound.transport
|
||||
|
||||
@@ -222,15 +234,17 @@ export namespace LinkUtil {
|
||||
v: 2,
|
||||
add: addr,
|
||||
aid: u?.alterId,
|
||||
host: tParams.host,
|
||||
host: tParams.host?? undefined,
|
||||
id: u?.uuid,
|
||||
net: transport?.type?? 'tcp',
|
||||
path: tParams.path,
|
||||
net: transport?.type == undefined || transport?.type == 'http' ? 'tcp' : transport.type,
|
||||
type: transport?.type == 'http' ? 'http' : undefined,
|
||||
path: tParams.path?? undefined,
|
||||
port: inbound.listen_port,
|
||||
ps: inbound.tag,
|
||||
sni: inbound.tls.server_name?? '',
|
||||
tls: Object.keys(inbound.tls).length>0? 'tls' : 'none'
|
||||
sni: inbound.tls.server_name?? undefined,
|
||||
tls: Object.keys(inbound.tls).length>0? 'tls' : 'none',
|
||||
allowInsecure: tlsClient?.insecure ? 1 : undefined
|
||||
}
|
||||
return 'vmess://' + utf8ToBase64(JSON.stringify(params))
|
||||
return 'vmess://' + utf8ToBase64(JSON.stringify(params, null, 2))
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'vuetify/styles'
|
||||
|
||||
import colors from 'vuetify/util/colors'
|
||||
import { fa, en } from 'vuetify/locale'
|
||||
import { fa, en, vi, zhHans as zhcn, zhHant as zhtw } from 'vuetify/locale'
|
||||
|
||||
// Composables
|
||||
import { createVuetify } from 'vuetify'
|
||||
@@ -53,6 +53,6 @@ export default createVuetify({
|
||||
locale: {
|
||||
locale: localStorage.getItem("locale") ?? 'en',
|
||||
fallback: 'en',
|
||||
messages: { en, fa },
|
||||
messages: { en, fa, vi, zhcn, zhtw },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -39,6 +39,11 @@ const routes = [
|
||||
name: 'pages.rules',
|
||||
component: () => import('@/views/Rules.vue'),
|
||||
},
|
||||
{
|
||||
path: '/tls',
|
||||
name: 'pages.tls',
|
||||
component: () => import('@/views/Tls.vue'),
|
||||
},
|
||||
{
|
||||
path: '/basics',
|
||||
name: 'pages.basics',
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { FindDiff } from '@/plugins/utils'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { defineStore } from 'pinia'
|
||||
import { onMounted } from 'vue'
|
||||
import { push } from 'notivue'
|
||||
import { i18n } from '@/locales'
|
||||
|
||||
const Data = defineStore('Data', {
|
||||
state: () => ({
|
||||
@@ -9,9 +10,10 @@ const Data = defineStore('Data', {
|
||||
reloadItems: localStorage.getItem("reloadItems")?.split(',')?? <string[]>[],
|
||||
subURI: "",
|
||||
onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]},
|
||||
oldData: <{config: any, clients: any[]}>{},
|
||||
oldData: <{config: any, clients: any[], tlsConfigs: any[]}>{},
|
||||
config: {},
|
||||
clients: [],
|
||||
tlsConfigs: [],
|
||||
}),
|
||||
actions: {
|
||||
async loadData() {
|
||||
@@ -20,21 +22,33 @@ const Data = defineStore('Data', {
|
||||
this.lastLoad = Math.floor((new Date()).getTime()/1000)
|
||||
|
||||
// Set new data
|
||||
const data = JSON.parse(msg.obj)
|
||||
if (data.config) this.config = data.config
|
||||
if (data.clients) this.clients = data.clients
|
||||
if (data.subURI) this.subURI = data.subURI
|
||||
this.onlines = data.onlines
|
||||
|
||||
// To avoid ref copy
|
||||
if (data.config) this.oldData.config = { ...JSON.parse(msg.obj).config }
|
||||
if (data.clients) this.oldData.clients = [ ...JSON.parse(msg.obj).clients ]
|
||||
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
|
||||
this.onlines = msg.obj.onlines
|
||||
if (msg.obj.lastLog) {
|
||||
push.error({
|
||||
title: i18n.global.t('error.core'),
|
||||
duration: 5000,
|
||||
message: msg.obj.lastLog
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
async pushData() {
|
||||
const diff = {
|
||||
config: JSON.stringify(FindDiff.Config(this.config,this.oldData.config)),
|
||||
clients: JSON.stringify(FindDiff.Clients(this.clients,this.oldData.clients)),
|
||||
tls: JSON.stringify(FindDiff.Clients(this.tlsConfigs,this.oldData.tlsConfigs)),
|
||||
}
|
||||
const msg = await HttpUtils.post('api/save',diff)
|
||||
if(msg.success) {
|
||||
@@ -45,6 +59,7 @@ const Data = defineStore('Data', {
|
||||
const diff = {
|
||||
config: JSON.stringify([{key: "inbounds", action: "del", index: index, obj: null}]),
|
||||
clients: JSON.stringify(FindDiff.Clients(this.clients,this.oldData.clients)),
|
||||
tls: JSON.stringify(FindDiff.Clients(this.tlsConfigs,this.oldData.tlsConfigs)),
|
||||
}
|
||||
const msg = await HttpUtils.post('api/save',diff)
|
||||
if(msg.success) {
|
||||
@@ -69,6 +84,15 @@ const Data = defineStore('Data', {
|
||||
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()
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
const Message = defineStore('msg', {
|
||||
state: () => ({
|
||||
showMsg: false,
|
||||
snackbar: {
|
||||
message: '',
|
||||
timeout: 5000,
|
||||
color: '',
|
||||
}
|
||||
}),
|
||||
actions: {
|
||||
showMessage(message:string, color='success',timeout=5000) {
|
||||
this.snackbar.message = message
|
||||
this.snackbar.color = color
|
||||
this.snackbar.timeout = timeout
|
||||
this.showMsg = true
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export default Message
|
||||
@@ -1,12 +1,13 @@
|
||||
import { Link } from "@/plugins/link"
|
||||
import RandomUtil from "@/plugins/randomUtil"
|
||||
|
||||
export interface Client {
|
||||
id?: number
|
||||
enable: boolean
|
||||
name: string
|
||||
config: string
|
||||
inbounds: string
|
||||
links: string
|
||||
config: Config
|
||||
inbounds: string[]
|
||||
links: Link[]
|
||||
volume: number
|
||||
expiry: number
|
||||
up: number
|
||||
@@ -17,9 +18,9 @@ export interface Client {
|
||||
const defaultClient: Client = {
|
||||
enable: true,
|
||||
name: "",
|
||||
config: "[]",
|
||||
inbounds: "",
|
||||
links: "[]",
|
||||
config: {},
|
||||
inbounds: [],
|
||||
links: [],
|
||||
volume: 0,
|
||||
expiry: 0,
|
||||
up: 0,
|
||||
@@ -35,12 +36,11 @@ type Config = {
|
||||
}
|
||||
}
|
||||
|
||||
export function updateConfigs(configs: string, newUserName: string): string {
|
||||
const updatedConfigs: Config = JSON.parse(configs)
|
||||
export function updateConfigs(configs: Config, newUserName: string): Config {
|
||||
|
||||
for (const key in updatedConfigs) {
|
||||
if (updatedConfigs.hasOwnProperty(key)) {
|
||||
const config = updatedConfigs[key]
|
||||
for (const key in configs) {
|
||||
if (configs.hasOwnProperty(key)) {
|
||||
const config = configs[key]
|
||||
if (config.hasOwnProperty("name")) {
|
||||
config.name = newUserName
|
||||
} else if (config.hasOwnProperty("username")) {
|
||||
@@ -49,7 +49,7 @@ export function updateConfigs(configs: string, newUserName: string): string {
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(updatedConfigs)
|
||||
return configs
|
||||
}
|
||||
|
||||
export function randomConfigs(user: string): Config {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { Dial } from "./dial"
|
||||
|
||||
export interface iTls {
|
||||
enabled?: boolean
|
||||
server_name?: string
|
||||
@@ -9,6 +11,50 @@ export interface iTls {
|
||||
certificate_path?: string
|
||||
key?: string[]
|
||||
key_path?: string
|
||||
acme?: acme
|
||||
ech?: ech
|
||||
reality?: reality
|
||||
}
|
||||
|
||||
export interface acme {
|
||||
domain: string[]
|
||||
data_directory?: string
|
||||
default_server_name?: string
|
||||
email?: string
|
||||
provider?: string
|
||||
disable_http_challenge?: boolean
|
||||
disable_tls_alpn_challenge?: boolean
|
||||
alternative_http_port?: number
|
||||
alternative_tls_port?: number
|
||||
external_account?: {
|
||||
key_id: string
|
||||
mac_key: string
|
||||
}
|
||||
dns01_challenge?: {
|
||||
provider: string
|
||||
[key: string]: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface ech {
|
||||
enabled: boolean
|
||||
pq_signature_schemes_enabled?: boolean
|
||||
dynamic_record_sizing_disabled?: boolean
|
||||
key?: string[]
|
||||
key_path?: string
|
||||
}
|
||||
|
||||
interface realityHanshake extends Dial {
|
||||
server: string
|
||||
server_port: number
|
||||
}
|
||||
|
||||
export interface reality {
|
||||
enabled: boolean
|
||||
handshake: realityHanshake
|
||||
private_key: string
|
||||
short_id: string[]
|
||||
max_time_difference?: string
|
||||
}
|
||||
|
||||
export const defaultInTls: iTls = {
|
||||
|
||||
@@ -57,7 +57,6 @@ export interface WgPeer {
|
||||
export interface Direct extends OutboundBasics, Dial {
|
||||
override_address?: string
|
||||
override_port?: number
|
||||
proxy_protocol?: 0 | 1 | 2
|
||||
}
|
||||
|
||||
export interface Block extends OutboundBasics {}
|
||||
|
||||
@@ -6,6 +6,18 @@
|
||||
@close="closeEditModal"
|
||||
@save="saveEditModal"
|
||||
/>
|
||||
<ChngModal
|
||||
v-model="changesModal.visible"
|
||||
:visible="changesModal.visible"
|
||||
:admins="users.map((u:any) => u.username)"
|
||||
:actor="changesModal.actor"
|
||||
@close="closeChangesModal"
|
||||
/>
|
||||
<v-row>
|
||||
<v-col cols="12" justify="center" align="center">
|
||||
<v-btn color="primary" @click="showChangesModal('')">{{ $t('admin.changes') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>users" :key="item.id">
|
||||
<v-card rounded="xl" elevation="5" min-width="200" :title="item.username">
|
||||
@@ -16,19 +28,19 @@
|
||||
<v-row>
|
||||
<v-col>{{ $t('admin.date') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.lastLogin.split(" ")[0]?? '-' }}
|
||||
{{ item.loginDate }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('admin.time') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.lastLogin.split(" ")[1]?? '-' }}
|
||||
{{ item.loginTime }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>IP</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.lastLogin.split(" ")[2]?? '-' }}
|
||||
{{ item.ip }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
@@ -38,6 +50,10 @@
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn icon="mdi-list-box-outline" @click="showChangesModal(item.username)">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('admin.changes')"></v-tooltip>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@@ -45,13 +61,15 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import AdminModal from '@/layouts/modals/Admin.vue';
|
||||
import HttpUtils from '@/plugins/httputil';
|
||||
import { Ref, ref, inject, onMounted } from 'vue';
|
||||
import AdminModal from '@/layouts/modals/Admin.vue'
|
||||
import ChngModal from '@/layouts/modals/Changes.vue'
|
||||
import { i18n } from '@/locales'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { Ref, ref, inject, onMounted } from 'vue'
|
||||
|
||||
const loading:Ref = inject('loading')?? ref(false)
|
||||
|
||||
const users = ref([])
|
||||
const users = ref(<any[]>[])
|
||||
|
||||
onMounted(async () => {loadData()})
|
||||
|
||||
@@ -60,10 +78,28 @@ const loadData = async () => {
|
||||
const msg = await HttpUtils.get('api/users')
|
||||
loading.value = false
|
||||
if (msg.success) {
|
||||
users.value = msg.obj
|
||||
console.log(msg.obj)
|
||||
msg.obj.forEach((u:any) => {
|
||||
const lastLogin = u.lastLogin.split(" ")
|
||||
const localLastLogin = lastLogin.length > 2 ? dateFormatted(Date.parse(lastLogin[0] + " " + lastLogin[1])) : "- -"
|
||||
const loginDateTime = localLastLogin.split(" ")
|
||||
users.value.push({
|
||||
id: u.id,
|
||||
username: u.username,
|
||||
loginDate: loginDateTime[0],
|
||||
loginTime: loginDateTime[1],
|
||||
ip: lastLogin[2]?? "-",
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const dateFormatted = (dt: number): string => {
|
||||
const locale = i18n.global.locale.value.replace('zh', 'zh-')
|
||||
const date = new Date(dt)
|
||||
return date.toLocaleString(locale)
|
||||
}
|
||||
|
||||
const editModal = ref({
|
||||
visible: false,
|
||||
user: {},
|
||||
@@ -75,6 +111,7 @@ const showEditModal = (user: any) => {
|
||||
}
|
||||
const closeEditModal = () => {
|
||||
editModal.value.visible = false
|
||||
editModal.value.user = {}
|
||||
}
|
||||
const saveEditModal = async (data:any) => {
|
||||
loading.value=true
|
||||
@@ -88,4 +125,17 @@ const saveEditModal = async (data:any) => {
|
||||
loading.value=false
|
||||
}
|
||||
}
|
||||
|
||||
const changesModal = ref({
|
||||
visible: false,
|
||||
actor: '',
|
||||
})
|
||||
const showChangesModal = (actor: string) => {
|
||||
changesModal.value.actor = actor
|
||||
changesModal.value.visible = true
|
||||
}
|
||||
const closeChangesModal = () => {
|
||||
changesModal.value.visible = false
|
||||
changesModal.value.actor = ''
|
||||
}
|
||||
</script>
|
||||
@@ -36,7 +36,10 @@
|
||||
<v-col>{{ item.name }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-switch color="primary" v-model="clients[index].enable" hideDetails density="compact" />
|
||||
<v-switch color="primary"
|
||||
v-model="clients[index].enable"
|
||||
@update:model-value="buildInboundsUsers(item.inbounds)"
|
||||
hideDetails density="compact" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
@@ -50,9 +53,9 @@
|
||||
<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.split(',')">{{ i }}<br /></span>
|
||||
<span v-for="i in item.inbounds">{{ i }}<br /></span>
|
||||
</v-tooltip>
|
||||
{{ item.inbounds != '' ? item.inbounds.split(',').length : 0 }}
|
||||
{{ item.inbounds.length }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
@@ -132,8 +135,8 @@ import { Config, V2rayApiStats } from '@/types/config'
|
||||
import { InTypes, Inbound,InboundWithUser, ShadowTLS, VLESS } from '@/types/inbounds'
|
||||
import { Link, LinkUtil } from '@/plugins/link'
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
import Message from '@/store/modules/message'
|
||||
import { i18n } from '@/locales'
|
||||
import { push } from 'notivue'
|
||||
|
||||
const clients = computed((): any[] => {
|
||||
return Data().clients
|
||||
@@ -179,26 +182,26 @@ const closeModal = () => {
|
||||
modal.value.visible = false
|
||||
}
|
||||
const saveModal = (data:any, stats:boolean) => {
|
||||
if (clients.value.findIndex(c => c.name == data.name) != modal.value.index) {
|
||||
const sb = Message()
|
||||
sb.showMessage(i18n.global.t('error.dplData') + ': ' + i18n.global.t('client.name') ,'error', 5000)
|
||||
// Check duplicate name
|
||||
const oldName = modal.value.index != -1 ? clients.value[modal.value.index].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
|
||||
}
|
||||
const inboundTags: string[] = data.inbounds.split(',')?? []
|
||||
let oldName:string = ""
|
||||
if(modal.value.index == -1) {
|
||||
clients.value.push(data)
|
||||
} else {
|
||||
const oldData = createClient(clients.value[modal.value.index])
|
||||
oldName = oldData.name
|
||||
oldData.inbounds.split(',').forEach((i:string) => {
|
||||
if (!inboundTags.includes(i)) inboundTags.push(i)
|
||||
oldData.inbounds.forEach((i:string) => {
|
||||
if (!data.inbounds.includes(i)) data.inbounds.push(i)
|
||||
})
|
||||
clients.value[modal.value.index] = data
|
||||
}
|
||||
|
||||
// Rebuild affected inbounds
|
||||
buildInboundsUsers(inboundTags)
|
||||
buildInboundsUsers(data.inbounds)
|
||||
|
||||
// Rebuild links
|
||||
data.links = updateLinks(data)
|
||||
@@ -226,15 +229,14 @@ const buildInboundsUsers = (inboundTags:string[]) => {
|
||||
if (inbound_index != -1){
|
||||
const users = <any>[]
|
||||
const newInbound = <InboundWithUser>inbounds.value[inbound_index]
|
||||
const inboundClients = clients.value.filter(c => c.enable && c.inbounds.split(',').includes(tag))
|
||||
const inboundClients = clients.value.filter(c => c.enable && c.inbounds.includes(tag))
|
||||
inboundClients.forEach(c => {
|
||||
const clientConfig = JSON.parse(c.config)
|
||||
// Remove flow in non tls VLESS
|
||||
if (newInbound.type == InTypes.VLESS) {
|
||||
const vlessInbound = <VLESS>newInbound
|
||||
if (!vlessInbound.tls?.enabled || vlessInbound.transport?.type) delete(clientConfig["vless"].flow)
|
||||
if (!vlessInbound.tls?.enabled || vlessInbound.transport?.type) delete(c.config?.vless?.flow)
|
||||
}
|
||||
users.push(clientConfig[newInbound.type])
|
||||
users.push(c.config[newInbound.type])
|
||||
})
|
||||
newInbound.users = users
|
||||
|
||||
@@ -254,19 +256,20 @@ const buildInboundsUsers = (inboundTags:string[]) => {
|
||||
}
|
||||
})
|
||||
}
|
||||
const updateLinks = (c:Client):string => {
|
||||
const clientInbounds = <Inbound[]>inbounds.value.filter(i => c.inbounds.split(',').includes(i.tag))
|
||||
const updateLinks = (c:Client):Link[] => {
|
||||
const clientInbounds = <Inbound[]>inbounds.value.filter(i => c.inbounds.includes(i.tag))
|
||||
const newLinks = <Link[]>[]
|
||||
clientInbounds.forEach(i =>{
|
||||
const uri = LinkUtil.linkGenerator(c.name,i)
|
||||
const tlsConfig = <any>Data().tlsConfigs?.findLast((t:any) => t.inbounds.includes(i.tag))
|
||||
const uri = LinkUtil.linkGenerator(c.name,i,tlsConfig?.client)
|
||||
if (uri.length>0){
|
||||
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
|
||||
}
|
||||
})
|
||||
let links = c.links && c.links.length>0? <Link[]>JSON.parse(c.links) : <Link[]>[]
|
||||
let links = c.links && c.links.length>0? c.links : <Link[]>[]
|
||||
links = [...newLinks, ...links.filter(l => l.type != 'local')]
|
||||
|
||||
return JSON.stringify(links)
|
||||
return links
|
||||
}
|
||||
const delClient = (clientIndex: number) => {
|
||||
const id = clients.value[clientIndex].id
|
||||
@@ -280,7 +283,7 @@ const delClient = (clientIndex: number) => {
|
||||
}
|
||||
|
||||
clients.value.splice(clientIndex,1)
|
||||
buildInboundsUsers(oldData.inbounds.split(','))
|
||||
buildInboundsUsers(oldData.inbounds)
|
||||
if (id>0) Data().delClient(id)
|
||||
delOverlay.value[clientIndex] = false
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
:data="modal.data"
|
||||
:inTags="inTags"
|
||||
:outTags="outTags"
|
||||
:tlsConfigs="tlsConfigs"
|
||||
@close="closeModal"
|
||||
@save="saveModal"
|
||||
/>
|
||||
@@ -108,8 +109,8 @@ import { computed, ref } from 'vue'
|
||||
import { InTypes, Inbound, InboundWithUser, ShadowTLS, VLESS } from '@/types/inbounds'
|
||||
import { Client } from '@/types/clients'
|
||||
import { Link, LinkUtil } from '@/plugins/link'
|
||||
import Message from '@/store/modules/message'
|
||||
import { i18n } from '@/locales'
|
||||
import { push } from 'notivue'
|
||||
|
||||
const appConfig = computed((): Config => {
|
||||
return <Config> Data().config
|
||||
@@ -119,6 +120,10 @@ const inbounds = computed((): Inbound[] => {
|
||||
return <Inbound[]> appConfig.value.inbounds
|
||||
})
|
||||
|
||||
const tlsConfigs = computed((): any[] => {
|
||||
return <any[]> Data().tlsConfigs
|
||||
})
|
||||
|
||||
const inTags = computed((): string[] => {
|
||||
return inbounds.value?.map(i => i.tag)
|
||||
})
|
||||
@@ -157,22 +162,39 @@ const showModal = (id: number) => {
|
||||
const closeModal = () => {
|
||||
modal.value.visible = false
|
||||
}
|
||||
const saveModal = (data:Inbound, stats: boolean) => {
|
||||
if (inbounds.value.findIndex(c => c.tag == data.tag) != modal.value.id) {
|
||||
const sb = Message()
|
||||
sb.showMessage(i18n.global.t('error.dplData') + ': ' + i18n.global.t('objects.tag') ,'error', 5000)
|
||||
const saveModal = (data:Inbound, stats: boolean, tls_id: number) => {
|
||||
// Check duplicate tag
|
||||
const oldTag = modal.value.id != -1 ? inbounds.value[modal.value.id].tag : null
|
||||
if (data.tag != oldTag && inTags.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) {
|
||||
inbounds.value.push(data)
|
||||
if (stats && data.tag.length>0) {
|
||||
v2rayStats.value.inbounds.push(data.tag)
|
||||
}
|
||||
// Update tls preset
|
||||
if (tls_id>0) {
|
||||
tlsConfigs.value.findLast(t => t.id == tls_id).inbounds.push(data.tag)
|
||||
}
|
||||
} else {
|
||||
const oldTag = inbounds.value[modal.value.id].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 (tls_id>0) {
|
||||
tlsConfigs.value.findLast(t => t.id == tls_id).inbounds.push(data.tag)
|
||||
}
|
||||
|
||||
if (oldTag != data.tag) {
|
||||
v2rayStats.value.inbounds = v2rayStats.value.inbounds.filter(item => item != oldTag)
|
||||
changeClientInboundsTag(oldTag,data.tag)
|
||||
@@ -188,10 +210,12 @@ const saveModal = (data:Inbound, stats: boolean) => {
|
||||
|
||||
inbounds.value[modal.value.id] = data
|
||||
}
|
||||
// Set users
|
||||
data = buildInboundsUsers(data)
|
||||
// Update links
|
||||
if (Object.hasOwn(data,'users')) updateLinks(data)
|
||||
if (Object.hasOwn(data,'users')) {
|
||||
// Set users
|
||||
data = buildInboundsUsers(data)
|
||||
// Update links
|
||||
updateLinks(data)
|
||||
}
|
||||
modal.value.visible = false
|
||||
}
|
||||
const updateLinks = (i: InboundWithUser) => {
|
||||
@@ -199,18 +223,19 @@ const updateLinks = (i: InboundWithUser) => {
|
||||
i.users.forEach((u:any) => {
|
||||
const client = clients.value.find(c => u.username? c.name == u.username : c.name == u.name)
|
||||
if (client){
|
||||
const clientInbounds = <Inbound[]>inbounds.value.filter(i => client?.inbounds.split(',').includes(i.tag))
|
||||
const clientInbounds = <Inbound[]>inbounds.value.filter(inb => client?.inbounds.includes(inb.tag))
|
||||
const newLinks = <Link[]>[]
|
||||
clientInbounds.forEach(i =>{
|
||||
const uri = LinkUtil.linkGenerator(client.name,i)
|
||||
const tlsClient = tlsConfigs?.value.findLast((t:any) => t.inbounds.includes(i.tag))?.client?? null
|
||||
const uri = LinkUtil.linkGenerator(client.name,i, tlsClient)
|
||||
if (uri.length>0){
|
||||
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
|
||||
}
|
||||
})
|
||||
let links = client.links && client.links.length>0? <Link[]>JSON.parse(client.links) : <Link[]>[]
|
||||
let links = client.links && client.links.length>0? client.links : <Link[]>[]
|
||||
links = [...newLinks, ...links.filter(l => l.type != 'local')]
|
||||
|
||||
client.links = JSON.stringify(links)
|
||||
client.links = links
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -220,19 +245,26 @@ const delInbound = (index: number) => {
|
||||
inbounds.value.splice(index,1)
|
||||
const tag = inb.tag
|
||||
|
||||
if (Object.hasOwn(inb,'users')){
|
||||
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.user == c.name)
|
||||
if (c_index != -1) {
|
||||
const clientInbounds = clients.value[c_index].inbounds.split(',').filter((x:string) => x!=tag)
|
||||
clients.value[c_index].inbounds = clientInbounds.join(',')
|
||||
const clientInbounds = clients.value[c_index].inbounds.filter((x:string) => x!=tag)
|
||||
clients.value[c_index].inbounds = clientInbounds
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -247,15 +279,14 @@ const delInbound = (index: number) => {
|
||||
}
|
||||
const buildInboundsUsers = (inbound:InboundWithUser):Inbound => {
|
||||
const users = <any>[]
|
||||
const inboundClients = clients.value.filter(c => c.enable && c.inbounds.split(',').includes(inbound.tag))
|
||||
const inboundClients = clients.value.filter(c => c.enable && c.inbounds.includes(inbound.tag))
|
||||
inboundClients.forEach(c => {
|
||||
const clientConfig = JSON.parse(c.config)
|
||||
// Remove flow in non tls VLESS
|
||||
if (inbound.type == InTypes.VLESS) {
|
||||
const vlessInbound = <VLESS>inbound
|
||||
if (!vlessInbound.tls?.enabled || vlessInbound.transport?.type) delete(clientConfig["vless"].flow)
|
||||
if (!vlessInbound.tls?.enabled || vlessInbound.transport?.type) delete(c.config?.vless?.flow)
|
||||
}
|
||||
users.push(clientConfig[inbound.type])
|
||||
users.push(c.config[inbound.type])
|
||||
})
|
||||
inbound.users = users
|
||||
|
||||
@@ -275,11 +306,10 @@ const buildInboundsUsers = (inbound:InboundWithUser):Inbound => {
|
||||
}
|
||||
const changeClientInboundsTag = (oldtag: string, newTag:string) => {
|
||||
clients.value.forEach((c, c_index) => {
|
||||
const inboundsArray = c.inbounds.split(',')
|
||||
const inbound_index = inboundsArray.findIndex(i => i == oldtag)
|
||||
const inbound_index = c.inbounds.findIndex(i => i == oldtag)
|
||||
if (inbound_index != -1) {
|
||||
inboundsArray[inbound_index] = newTag
|
||||
clients.value[c_index].inbounds = inboundsArray.join(',')
|
||||
c.inbounds[inbound_index] = newTag
|
||||
clients.value[c_index].inbounds = c.inbounds
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -96,8 +96,8 @@ import Stats from '@/layouts/modals/Stats.vue'
|
||||
import { Config, V2rayApiStats } from '@/types/config';
|
||||
import { Outbound } from '@/types/outbounds';
|
||||
import { computed, ref } from 'vue'
|
||||
import Message from '@/store/modules/message';
|
||||
import { i18n } from '@/locales';
|
||||
import { push } from 'notivue';
|
||||
|
||||
const appConfig = computed((): Config => {
|
||||
return <Config> Data().config
|
||||
@@ -139,9 +139,12 @@ const closeModal = () => {
|
||||
modal.value.visible = false
|
||||
}
|
||||
const saveModal = (data:Outbound, stats: boolean) => {
|
||||
if (outbounds.value.findIndex(c => c.tag == data.tag) != modal.value.id) {
|
||||
const sb = Message()
|
||||
sb.showMessage(i18n.global.t('error.dplData') + ': ' + i18n.global.t('objects.tag') ,'error', 5000)
|
||||
// Check duplicate tag
|
||||
const oldTag = modal.value.id != -1 ? outbounds.value[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
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<v-text-field v-model="settings.webListen" :label="$t('setting.addr')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="webPort" :label="$t('setting.port')" hide-details></v-text-field>
|
||||
<v-text-field v-model.number="webPort" min="1" type="number" :label="$t('setting.port')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="settings.webPath" :label="$t('setting.webPath')" hide-details></v-text-field>
|
||||
@@ -51,8 +51,9 @@
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model.number="sessionMaxAge"
|
||||
min="0"
|
||||
:label="$t('setting.sessionAge')"
|
||||
:suffix="$t('date.h')"
|
||||
:suffix="$t('date.m')"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
@@ -60,6 +61,7 @@
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model.number="trafficAge"
|
||||
min="0"
|
||||
:label="$t('setting.trafficAge')"
|
||||
:suffix="$t('date.d')"
|
||||
hide-details
|
||||
@@ -87,7 +89,7 @@
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model="subPort"
|
||||
v-model.number="subPort"
|
||||
min="1"
|
||||
:label="$t('setting.port')"
|
||||
hide-details></v-text-field>
|
||||
@@ -114,6 +116,7 @@
|
||||
<v-text-field
|
||||
type="number"
|
||||
v-model.number="subUpdates"
|
||||
min="0"
|
||||
:label="$t('setting.update')"
|
||||
hide-details
|
||||
></v-text-field>
|
||||
@@ -248,28 +251,28 @@ const subShowInfo = computed({
|
||||
})
|
||||
|
||||
const webPort = computed({
|
||||
get: () => { return parseInt(settings.value.webPort) },
|
||||
set: (v:number) => { settings.value.webPort = v.toString() }
|
||||
get: () => { return settings.value.webPort.length>0 ? parseInt(settings.value.webPort) : 2095 },
|
||||
set: (v:number) => { settings.value.webPort = v>0 ? v.toString() : "2095" }
|
||||
})
|
||||
|
||||
const sessionMaxAge = computed({
|
||||
get: () => { return parseInt(settings.value.sessionMaxAge) },
|
||||
set: (v:number) => { settings.value.sessionMaxAge = v.toString() }
|
||||
get: () => { return settings.value.sessionMaxAge.length>0 ? parseInt(settings.value.sessionMaxAge) : 0 },
|
||||
set: (v:number) => { settings.value.sessionMaxAge = v>0 ? v.toString() : "0" }
|
||||
})
|
||||
|
||||
const trafficAge = computed({
|
||||
get: () => { return parseInt(settings.value.trafficAge) },
|
||||
set: (v:number) => { settings.value.trafficAge = v.toString() }
|
||||
get: () => { return settings.value.trafficAge.length>0 ? parseInt(settings.value.trafficAge) : 0 },
|
||||
set: (v:number) => { settings.value.trafficAge = v>0 ? v.toString() : "0" }
|
||||
})
|
||||
|
||||
const subPort = computed({
|
||||
get: () => { return parseInt(settings.value.subPort) },
|
||||
set: (v:number) => { settings.value.subPort = v.toString() }
|
||||
get: () => { return settings.value.subPort.length>0 ? parseInt(settings.value.subPort) : 2096 },
|
||||
set: (v:number) => { settings.value.subPort = v>0 ? v.toString() : "2096" }
|
||||
})
|
||||
|
||||
const subUpdates = computed({
|
||||
get: () => { return parseInt(settings.value.subUpdates) },
|
||||
set: (v:number) => { settings.value.subUpdates = v.toString() }
|
||||
get: () => { return settings.value.subUpdates.length>0 ? parseInt(settings.value.subUpdates) : 12 },
|
||||
set: (v:number) => { settings.value.subUpdates = v>0 ? v.toString() : "12" }
|
||||
})
|
||||
|
||||
const stateChange = computed(() => {
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<TlsVue
|
||||
v-model="modal.visible"
|
||||
:visible="modal.visible"
|
||||
:index="modal.index"
|
||||
: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-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.name">
|
||||
<v-card-subtitle style="margin-top: -20px;">
|
||||
{{ item.server?.server_name?.length>0 ? item.server.server_name : "-" }}
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<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 }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>ACME</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ $t(item.server?.acme == undefined ? 'no' : 'yes') }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>ECH</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ $t(item.server?.ech == undefined ? 'no' : 'yes') }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>Reality</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ $t(item.server?.reality == undefined ? 'no' : 'yes') }}
|
||||
</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(index)">
|
||||
<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-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="delTls(index)">{{ $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-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
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 { Client } from '@/types/clients';
|
||||
import { Link, LinkUtil } from '@/plugins/link';
|
||||
|
||||
const tlsConfigs = computed((): any[] => {
|
||||
return Data().tlsConfigs
|
||||
})
|
||||
|
||||
const inbounds = computed((): any[] => {
|
||||
return <any[]>(<Config>Data().config)?.inbounds
|
||||
})
|
||||
|
||||
const clients = computed((): any[] => {
|
||||
return <Client[]>Data().clients
|
||||
})
|
||||
|
||||
const modal = ref({
|
||||
visible: false,
|
||||
index: -1,
|
||||
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])
|
||||
modal.value.visible = true
|
||||
}
|
||||
const closeModal = () => {
|
||||
modal.value.visible = false
|
||||
}
|
||||
const saveModal = (data: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
|
||||
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 && i.users.length>0){
|
||||
i.users.forEach((u:any) => {
|
||||
const client = clients.value.find(c => u.username? c.name == u.username : c.name == u.name)
|
||||
if (client){
|
||||
const clientInbounds = <Inbound[]>inbounds.value.filter(inb => client?.inbounds.includes(inb.tag))
|
||||
const newLinks = <Link[]>[]
|
||||
clientInbounds.forEach(i =>{
|
||||
const uri = LinkUtil.linkGenerator(client.name,i,tlsClient)
|
||||
if (uri.length>0){
|
||||
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
|
||||
}
|
||||
})
|
||||
let links = client.links && client.links.length>0? client.links : <Link[]>[]
|
||||
links = [...newLinks, ...links.filter((l:Link) => l.type != 'local')]
|
||||
|
||||
client.links = links
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -76,6 +76,9 @@ install_base() {
|
||||
}
|
||||
|
||||
config_after_install() {
|
||||
echo -e "${yellow}Migration... ${plain}"
|
||||
/usr/local/s-ui/sui migrate
|
||||
|
||||
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
|
||||
read -p "Do you want to continue with the modification [y/n]? ": config_confirm
|
||||
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
|
||||
|
||||
Reference in New Issue
Block a user