Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 96564f1f86 | |||
| cf6b61fe96 | |||
| e1aaa3d748 | |||
| 209561497a | |||
| 60b374e5d4 | |||
| ea8538148f | |||
| b3a2078ed6 | |||
| f169064fbc | |||
| 7d441723ba | |||
| a41140190f | |||
| bb5cd91bc9 | |||
| 5b6f6daaa8 | |||
| ba06ad598d | |||
| 69725ee5af | |||
| 0d36b811dc | |||
| 6672a2721f | |||
| 6b24506ddd | |||
| 3298fd4e0d | |||
| 6dc7c93030 | |||
| 7c127f07bb | |||
| b5a2dd18f5 | |||
| 53ed86c373 | |||
| ccbd591b39 | |||
| f5792c9d82 | |||
| 4a2ac30a95 | |||
| 45b03d8472 | |||
| db3270feaa | |||
| b144aecb6a | |||
| 474e5156bb | |||
| d057076251 | |||
| 29cc6fd3e3 | |||
| dd0f770c1a |
@@ -47,7 +47,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: core/
|
||||
push: true
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
fi
|
||||
|
||||
#### Build Sing-Box
|
||||
export VERSION=v1.8.14
|
||||
export VERSION=v1.9.3
|
||||
git clone -b $VERSION https://github.com/SagerNet/sing-box
|
||||
cd sing-box
|
||||
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 \
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
| Multi-Client/Inbound | :heavy_check_mark: |
|
||||
| Advanced Traffic Routing Interface | :heavy_check_mark: |
|
||||
| Client & Traffic & System Status | :heavy_check_mark: |
|
||||
| Subscription Service (link + info) | :heavy_check_mark: |
|
||||
| Subscription Service (link/json + info)| :heavy_check_mark: |
|
||||
| Dark/Light Theme | :heavy_check_mark: |
|
||||
|
||||
|
||||
@@ -104,6 +104,56 @@ docker build -t s-ui .
|
||||
|
||||
</details>
|
||||
|
||||
## Manual run + contribution
|
||||
|
||||
<details>
|
||||
<summary>Click for details</summary>
|
||||
|
||||
### Build and run whole project
|
||||
```shell
|
||||
./runSUI.sh
|
||||
```
|
||||
|
||||
### - Frontend
|
||||
|
||||
Frontend codes are in `frontend` folder in the root of repository.
|
||||
|
||||
To run it localy for instant developement you can use (apply automatic changes on file save):
|
||||
```shell
|
||||
cd frontend
|
||||
npm run dev
|
||||
```
|
||||
> By this command it will run a `vite` web server on separate port `3000`, with backend proxy to `http://localhost:2095`. You can change it in `frontend/vite.config.mts`.
|
||||
|
||||
To build fronend:
|
||||
```shell
|
||||
cd frontend
|
||||
npm run build
|
||||
```
|
||||
|
||||
### - Backend
|
||||
Backend codes are in `backend` folder in the root of repository.
|
||||
> Please build fronend once before!
|
||||
|
||||
To build backend:
|
||||
```shell
|
||||
cd backend
|
||||
|
||||
# remove old frontend compiled files
|
||||
rm -fr web/html/*
|
||||
# apply new frontend compiled files
|
||||
cp -R ../frontend/dist/ web/html/
|
||||
# build
|
||||
go build -o ../sui main.go
|
||||
```
|
||||
|
||||
To run backend (from root folder of repository):
|
||||
```shell
|
||||
./sui
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Languages
|
||||
|
||||
- English
|
||||
@@ -169,4 +219,4 @@ certbot certonly --standalone --register-unsafely-without-email --non-interactiv
|
||||
</details>
|
||||
|
||||
## Stargazers over Time
|
||||
[](https://starchart.cc/alireza0/s-ui)
|
||||
[](https://starchart.cc/alireza0/s-ui)
|
||||
|
||||
@@ -3,6 +3,7 @@ package api
|
||||
import (
|
||||
"s-ui/logger"
|
||||
"s-ui/service"
|
||||
"s-ui/util"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -15,6 +16,7 @@ type APIHandler struct {
|
||||
service.ConfigService
|
||||
service.ClientService
|
||||
service.TlsService
|
||||
service.InDataService
|
||||
service.PanelService
|
||||
service.StatsService
|
||||
service.ServerService
|
||||
@@ -94,6 +96,10 @@ func (a *APIHandler) postHandler(c *gin.Context) {
|
||||
case "restartApp":
|
||||
err = a.PanelService.RestartPanel(3)
|
||||
jsonMsg(c, "restartApp", err)
|
||||
case "linkConvert":
|
||||
link := c.Request.FormValue("link")
|
||||
result, _, err := util.GetOutbound(link, 0)
|
||||
jsonObj(c, result, err)
|
||||
default:
|
||||
jsonMsg(c, "API call", nil)
|
||||
}
|
||||
@@ -163,6 +169,11 @@ func (a *APIHandler) getHandler(c *gin.Context) {
|
||||
count := c.Query("c")
|
||||
changes := a.ConfigService.GetChanges(actor, chngKey, count)
|
||||
jsonObj(c, changes, nil)
|
||||
case "keypairs":
|
||||
kType := c.Query("k")
|
||||
options := c.Query("o")
|
||||
keypair := a.ServerService.GenKeypair(kType, options)
|
||||
jsonObj(c, keypair, nil)
|
||||
default:
|
||||
jsonMsg(c, "API call", nil)
|
||||
}
|
||||
@@ -201,6 +212,10 @@ func (a *APIHandler) loadData(c *gin.Context) (interface{}, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
inData, err := a.InDataService.GetAll()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0])
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -208,6 +223,7 @@ func (a *APIHandler) loadData(c *gin.Context) (interface{}, error) {
|
||||
data["config"] = *config
|
||||
data["clients"] = clients
|
||||
data["tls"] = tlsConfigs
|
||||
data["inData"] = inData
|
||||
data["subURI"] = subURI
|
||||
data["onlines"] = onlines
|
||||
} else {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"s-ui/config"
|
||||
"s-ui/database"
|
||||
"s-ui/database/model"
|
||||
@@ -13,7 +14,14 @@ import (
|
||||
)
|
||||
|
||||
func migrateDb() {
|
||||
err := database.OpenDB(config.GetDBPath())
|
||||
// void running on first install
|
||||
path := config.GetDBPath()
|
||||
_, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = database.OpenDB(path)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.0.5
|
||||
1.0.0
|
||||
@@ -60,6 +60,7 @@ func InitDB(dbPath string) error {
|
||||
err = db.AutoMigrate(
|
||||
&model.Setting{},
|
||||
&model.Tls{},
|
||||
&model.InboundData{},
|
||||
&model.User{},
|
||||
&model.Stats{},
|
||||
&model.Client{},
|
||||
|
||||
@@ -16,6 +16,13 @@ type Tls struct {
|
||||
Client json.RawMessage `json:"client" form:"client"`
|
||||
}
|
||||
|
||||
type InboundData struct {
|
||||
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Tag string `json:"tag" form:"tag"`
|
||||
Addrs json.RawMessage `json:"addrs" form:"addrs"`
|
||||
OutJson json.RawMessage `json:"outJson" form:"outJson"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Username string `json:"username" form:"username"`
|
||||
|
||||
@@ -18,6 +18,7 @@ var LastUpdate int64
|
||||
type ConfigService struct {
|
||||
ClientService
|
||||
TlsService
|
||||
InDataService
|
||||
singbox.Controller
|
||||
SettingService
|
||||
}
|
||||
@@ -80,7 +81,7 @@ func (s *ConfigService) GetConfig() (*SingBoxConfig, error) {
|
||||
|
||||
func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string) error {
|
||||
var err error
|
||||
var clientChanges, tlsChanges, settingChanges, configChanges []model.Changes
|
||||
var clientChanges, tlsChanges, inChanges, settingChanges, configChanges []model.Changes
|
||||
if _, ok := changes["clients"]; ok {
|
||||
err = json.Unmarshal([]byte(changes["clients"]), &clientChanges)
|
||||
if err != nil {
|
||||
@@ -93,6 +94,12 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, ok := changes["inData"]; ok {
|
||||
err = json.Unmarshal([]byte(changes["inData"]), &inChanges)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, ok := changes["settings"]; ok {
|
||||
err = json.Unmarshal([]byte(changes["settings"]), &settingChanges)
|
||||
if err != nil {
|
||||
@@ -128,6 +135,12 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(inChanges) > 0 {
|
||||
err = s.InDataService.Save(tx, inChanges)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(settingChanges) > 0 {
|
||||
err = s.SettingService.Save(tx, settingChanges)
|
||||
if err != nil {
|
||||
@@ -185,14 +198,19 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
|
||||
|
||||
// Log changes
|
||||
dt := time.Now().Unix()
|
||||
allChanges := append(append(clientChanges, settingChanges...), append(configChanges, tlsChanges...)...)
|
||||
for index := range allChanges {
|
||||
allChanges[index].DateTime = dt
|
||||
allChanges[index].Actor = loginUser
|
||||
}
|
||||
err = tx.Model(model.Changes{}).Create(&allChanges).Error
|
||||
if err != nil {
|
||||
return err
|
||||
allChanges := append(clientChanges, settingChanges...)
|
||||
allChanges = append(allChanges, configChanges...)
|
||||
allChanges = append(allChanges, tlsChanges...)
|
||||
allChanges = append(allChanges, inChanges...)
|
||||
if len(allChanges) > 0 {
|
||||
for index := range allChanges {
|
||||
allChanges[index].DateTime = dt
|
||||
allChanges[index].Actor = loginUser
|
||||
}
|
||||
err = tx.Model(model.Changes{}).Create(&allChanges).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
LastUpdate = dt
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"s-ui/database"
|
||||
"s-ui/database/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type InDataService struct {
|
||||
}
|
||||
|
||||
func (s *InDataService) GetAll() ([]model.InboundData, error) {
|
||||
db := database.GetDB()
|
||||
inData := []model.InboundData{}
|
||||
err := db.Model(model.InboundData{}).Scan(&inData).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return inData, nil
|
||||
}
|
||||
|
||||
func (s *InDataService) Save(tx *gorm.DB, changes []model.Changes) error {
|
||||
var err error
|
||||
for _, change := range changes {
|
||||
inData := model.InboundData{}
|
||||
err = json.Unmarshal(change.Obj, &inData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch change.Action {
|
||||
case "new":
|
||||
err = tx.Create(&inData).Error
|
||||
case "del":
|
||||
err = tx.Where("id = ?", change.Index).Delete(model.InboundData{}).Error
|
||||
default:
|
||||
err = tx.Save(inData).Error
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -159,3 +159,23 @@ func (s *ServerService) GetLogs(service string, count string, level string) []st
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func (s *ServerService) GenKeypair(keyType string, options string) []string {
|
||||
if len(keyType) == 0 {
|
||||
return []string{"No keypair to generate"}
|
||||
}
|
||||
sbExec := s.GetBinaryPath()
|
||||
cmdArgs := []string{"generate", keyType + "-keypair"}
|
||||
if keyType == "tls" || keyType == "ech" {
|
||||
cmdArgs = append(cmdArgs, options)
|
||||
}
|
||||
// Run the command
|
||||
cmd := exec.Command(sbExec, cmdArgs...)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return []string{"Failed to generate keypair"}
|
||||
}
|
||||
return strings.Split(out.String(), "\n")
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ var defaultValueMap = map[string]string{
|
||||
"subEncode": "true",
|
||||
"subShowInfo": "false",
|
||||
"subURI": "",
|
||||
"subJsonExt": "",
|
||||
}
|
||||
|
||||
type SettingService struct {
|
||||
@@ -65,7 +66,7 @@ func (s *SettingService) GetAllSetting() (*map[string]string, error) {
|
||||
}
|
||||
|
||||
// Due to security principles
|
||||
delete(allSetting, "webSecret")
|
||||
delete(allSetting, "secret")
|
||||
|
||||
return &allSetting, nil
|
||||
}
|
||||
@@ -127,9 +128,9 @@ func (s *SettingService) getBool(key string) (bool, error) {
|
||||
return strconv.ParseBool(str)
|
||||
}
|
||||
|
||||
func (s *SettingService) setBool(key string, value bool) error {
|
||||
return s.setString(key, strconv.FormatBool(value))
|
||||
}
|
||||
// func (s *SettingService) setBool(key string, value bool) error {
|
||||
// return s.setString(key, strconv.FormatBool(value))
|
||||
// }
|
||||
|
||||
func (s *SettingService) getInt(key string) (int, error) {
|
||||
str, err := s.getString(key)
|
||||
@@ -347,6 +348,10 @@ func (s *SettingService) Save(tx *gorm.DB, changes []model.Changes) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubJsonExt() (string, error) {
|
||||
return s.getString("subJsonExt")
|
||||
}
|
||||
|
||||
func (s *SettingService) fileExists(path string) error {
|
||||
_, err := os.Stat(path)
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"s-ui/database"
|
||||
"s-ui/database/model"
|
||||
"s-ui/service"
|
||||
"s-ui/util"
|
||||
)
|
||||
|
||||
const defaultJson = `
|
||||
{
|
||||
"inbounds": [
|
||||
{
|
||||
"type": "tun",
|
||||
"inet4_address": "172.19.0.1/30",
|
||||
"mtu": 9000,
|
||||
"auto_route": true,
|
||||
"strict_route": false,
|
||||
"sniff": true,
|
||||
"endpoint_independent_nat": false,
|
||||
"stack": "system",
|
||||
"platform": {
|
||||
"http_proxy": {
|
||||
"enabled": true,
|
||||
"server": "127.0.0.1",
|
||||
"server_port": 2080
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "mixed",
|
||||
"listen": "127.0.0.1",
|
||||
"listen_port": 2080,
|
||||
"sniff": true,
|
||||
"users": []
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
type JsonService struct {
|
||||
service.SettingService
|
||||
LinkService
|
||||
}
|
||||
|
||||
func (j *JsonService) GetJson(subId string, format string) (*string, error) {
|
||||
var jsonConfig map[string]interface{}
|
||||
|
||||
client, inDatas, err := j.getData(subId)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outbounds, outTags, err := j.getOutbounds(client.Config, inDatas)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
links := j.LinkService.GetLinks(&client.Links, "external", "")
|
||||
for index, link := range links {
|
||||
json, tag, err := util.GetOutbound(link, index)
|
||||
if err == nil && len(tag) > 0 {
|
||||
*outbounds = append(*outbounds, *json)
|
||||
*outTags = append(*outTags, tag)
|
||||
}
|
||||
}
|
||||
|
||||
j.addDefaultOutbounds(outbounds, outTags)
|
||||
|
||||
err = json.Unmarshal([]byte(defaultJson), &jsonConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jsonConfig["outbounds"] = outbounds
|
||||
|
||||
// Add other objects from settings
|
||||
j.addOthers(&jsonConfig)
|
||||
|
||||
result, _ := json.MarshalIndent(jsonConfig, " ", " ")
|
||||
resultStr := string(result)
|
||||
return &resultStr, nil
|
||||
}
|
||||
|
||||
func (j *JsonService) getData(subId string) (*model.Client, *[]model.InboundData, error) {
|
||||
db := database.GetDB()
|
||||
client := &model.Client{}
|
||||
err := db.Model(model.Client{}).Where("enable = true and name = ?", subId).First(client).Error
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var inbounds []string
|
||||
err = json.Unmarshal(client.Inbounds, &inbounds)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
inDatas := &[]model.InboundData{}
|
||||
err = db.Model(model.InboundData{}).Where("tag in ?", inbounds).Find(&inDatas).Error
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return client, inDatas, nil
|
||||
}
|
||||
|
||||
func (j *JsonService) getOutbounds(clientConfig json.RawMessage, inDatas *[]model.InboundData) (*[]map[string]interface{}, *[]string, error) {
|
||||
var outbounds []map[string]interface{}
|
||||
var configs map[string]interface{}
|
||||
var outTags []string
|
||||
|
||||
err := json.Unmarshal(clientConfig, &configs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, inData := range *inDatas {
|
||||
if len(inData.OutJson) < 5 {
|
||||
continue
|
||||
}
|
||||
var outbound map[string]interface{}
|
||||
err = json.Unmarshal(inData.OutJson, &outbound)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
protocol, _ := outbound["type"].(string)
|
||||
config, _ := configs[protocol].(map[string]interface{})
|
||||
for key, value := range config {
|
||||
if key != "alterId" && key != "name" && key != "username" {
|
||||
outbound[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
var addrs []map[string]interface{}
|
||||
err = json.Unmarshal(inData.Addrs, &addrs)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
tag := outbound["tag"].(string)
|
||||
if len(addrs) == 0 {
|
||||
outTags = append(outTags, tag)
|
||||
outbounds = append(outbounds, outbound)
|
||||
} else {
|
||||
for index, addr := range addrs {
|
||||
// Copy original config
|
||||
newOut := make(map[string]interface{}, len(outbound))
|
||||
for key, value := range outbound {
|
||||
newOut[key] = value
|
||||
}
|
||||
// Change and push copied config
|
||||
newOut["server"], _ = addr["server"].(string)
|
||||
port, _ := addr["server_port"].(float64)
|
||||
newOut["server_port"] = int(port)
|
||||
remark, _ := addr["remark"].(string)
|
||||
newTag := fmt.Sprintf("%d.%s%s", index+1, tag, remark)
|
||||
outTags = append(outTags, newTag)
|
||||
newOut["tag"] = newTag
|
||||
outbounds = append(outbounds, newOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &outbounds, &outTags, nil
|
||||
}
|
||||
|
||||
func (j *JsonService) addDefaultOutbounds(outbounds *[]map[string]interface{}, outTags *[]string) {
|
||||
outbound := []map[string]interface{}{
|
||||
{
|
||||
"outbounds": append([]string{"auto", "direct"}, *outTags...),
|
||||
"tag": "proxy",
|
||||
"type": "selector",
|
||||
},
|
||||
{
|
||||
"tag": "auto",
|
||||
"type": "urltest",
|
||||
"outbounds": outTags,
|
||||
"url": "http://www.gstatic.com/generate_204",
|
||||
"interval": "10m",
|
||||
"tolerance": 50,
|
||||
},
|
||||
{
|
||||
"type": "direct",
|
||||
"tag": "direct",
|
||||
},
|
||||
{
|
||||
"type": "dns",
|
||||
"tag": "dns-out",
|
||||
},
|
||||
{
|
||||
"type": "block",
|
||||
"tag": "block",
|
||||
},
|
||||
}
|
||||
*outbounds = append(outbound, *outbounds...)
|
||||
}
|
||||
|
||||
func (j *JsonService) addOthers(jsonConfig *map[string]interface{}) error {
|
||||
rules := []interface{}{
|
||||
map[string]interface{}{
|
||||
"clash_mode": "Direct",
|
||||
"outbound": "direct",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"clash_mode": "Global",
|
||||
"outbound": "proxy",
|
||||
},
|
||||
}
|
||||
route := map[string]interface{}{
|
||||
"auto_detect_interface": true,
|
||||
"final": "proxy",
|
||||
"rules": rules,
|
||||
}
|
||||
|
||||
othersStr, err := j.SettingService.GetSubJsonExt()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(othersStr) == 0 {
|
||||
(*jsonConfig)["route"] = route
|
||||
return nil
|
||||
}
|
||||
var othersJson map[string]interface{}
|
||||
err = json.Unmarshal([]byte(othersStr), &othersJson)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := othersJson["log"]; ok {
|
||||
(*jsonConfig)["log"] = othersJson["log"]
|
||||
}
|
||||
if _, ok := othersJson["dns"]; ok {
|
||||
(*jsonConfig)["dns"] = othersJson["dns"]
|
||||
}
|
||||
if _, ok := othersJson["experimental"]; ok {
|
||||
(*jsonConfig)["experimental"] = othersJson["lexperimentalog"]
|
||||
}
|
||||
if _, ok := othersJson["rule_set"]; ok {
|
||||
route["rule_set"] = othersJson["rule_set"]
|
||||
}
|
||||
if settingRules, ok := othersJson["rules"].([]interface{}); ok {
|
||||
route["rules"] = append(rules, settingRules...)
|
||||
}
|
||||
(*jsonConfig)["route"] = route
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"s-ui/logger"
|
||||
"s-ui/util"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Link struct {
|
||||
Type string `json:"type"`
|
||||
Remark string `json:"remark"`
|
||||
Uri string `json:"uri"`
|
||||
}
|
||||
|
||||
type LinkService struct {
|
||||
}
|
||||
|
||||
func (s *LinkService) GetLinks(linkJson *json.RawMessage, types string, clientInfo string) []string {
|
||||
links := []Link{}
|
||||
var result []string
|
||||
err := json.Unmarshal(*linkJson, &links)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
for _, link := range links {
|
||||
switch link.Type {
|
||||
case "external":
|
||||
result = append(result, link.Uri)
|
||||
case "sub":
|
||||
result = append(result, s.getExternalSub(link.Uri)...)
|
||||
case "local":
|
||||
if types == "all" {
|
||||
result = append(result, s.addClientInfo(link.Uri, clientInfo))
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (s *LinkService) addClientInfo(uri string, clientInfo string) string {
|
||||
protocol := strings.Split(uri, "://")
|
||||
if len(protocol) < 2 {
|
||||
return uri
|
||||
}
|
||||
switch protocol[0] {
|
||||
case "vmess":
|
||||
var vmessJson map[string]interface{}
|
||||
config, err := util.B64StrToByte(protocol[1])
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error decoding vmess content:", err)
|
||||
return uri
|
||||
}
|
||||
err = json.Unmarshal(config, &vmessJson)
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error decoding vmess content:", err)
|
||||
return uri
|
||||
}
|
||||
vmessJson["ps"] = vmessJson["ps"].(string) + clientInfo
|
||||
result, err := json.MarshalIndent(vmessJson, "", " ")
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error decoding vmess + clientInfo content:", err)
|
||||
return uri
|
||||
}
|
||||
return "vmess://" + util.ByteToB64Str(result)
|
||||
default:
|
||||
return uri + clientInfo
|
||||
}
|
||||
}
|
||||
|
||||
func (s *LinkService) getExternalSub(url string) []string {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
// Make the HTTP request
|
||||
response, err := client.Get(url)
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error making HTTP request:", err)
|
||||
return nil
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// Read the response body
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error reading response body:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Convert if the content is Base64 encoded
|
||||
links := util.StrOrBase64Encoded(string(body))
|
||||
return strings.Split(links, "\n")
|
||||
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
type SubHandler struct {
|
||||
service.SettingService
|
||||
SubService
|
||||
JsonService
|
||||
}
|
||||
|
||||
func NewSubHandler(g *gin.RouterGroup) {
|
||||
@@ -23,17 +24,28 @@ func (s *SubHandler) initRouter(g *gin.RouterGroup) {
|
||||
|
||||
func (s *SubHandler) subs(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
result, headers, err := s.SubService.GetSubs(subId)
|
||||
if err != nil || result == nil {
|
||||
logger.Error(err)
|
||||
c.String(400, "Error!")
|
||||
format, isFormat := c.GetQuery("format")
|
||||
if isFormat {
|
||||
result, err := s.JsonService.GetJson(subId, format)
|
||||
if err != nil || result == nil {
|
||||
logger.Error(err)
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
c.String(200, *result)
|
||||
}
|
||||
} else {
|
||||
result, headers, err := s.SubService.GetSubs(subId)
|
||||
if err != nil || result == nil {
|
||||
logger.Error(err)
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
|
||||
// Add headers
|
||||
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
|
||||
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
|
||||
c.Writer.Header().Set("Profile-Title", headers[2])
|
||||
// Add headers
|
||||
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
|
||||
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
|
||||
c.Writer.Header().Set("Profile-Title", headers[2])
|
||||
|
||||
c.String(200, *result)
|
||||
c.String(200, *result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-102
@@ -1,15 +1,10 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"s-ui/database"
|
||||
"s-ui/database/model"
|
||||
"s-ui/logger"
|
||||
"s-ui/service"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -17,12 +12,7 @@ import (
|
||||
|
||||
type SubService struct {
|
||||
service.SettingService
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Type string `json:"type"`
|
||||
Remark string `json:"remark"`
|
||||
Uri string `json:"uri"`
|
||||
LinkService
|
||||
}
|
||||
|
||||
func (s *SubService) GetSubs(subId string) (*string, []string, error) {
|
||||
@@ -35,29 +25,14 @@ func (s *SubService) GetSubs(subId string) (*string, []string, error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
links := []Link{}
|
||||
err = json.Unmarshal([]byte(client.Links), &links)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
clientInfo := ""
|
||||
subShowInfo, _ := s.SettingService.GetSubShowInfo()
|
||||
if subShowInfo {
|
||||
clientInfo = s.getClientInfo(client)
|
||||
}
|
||||
|
||||
var result string
|
||||
for _, link := range links {
|
||||
switch link.Type {
|
||||
case "external":
|
||||
result += fmt.Sprintln(link.Uri)
|
||||
case "sub":
|
||||
result += s.getExternalSub(link.Uri)
|
||||
case "local":
|
||||
result += fmt.Sprintln(s.addClientInfo(link.Uri, clientInfo))
|
||||
}
|
||||
}
|
||||
linksArray := s.LinkService.GetLinks(&client.Links, "all", clientInfo)
|
||||
result := strings.Join(linksArray, "\n")
|
||||
|
||||
var headers []string
|
||||
updateInterval, _ := s.SettingService.GetSubUpdates()
|
||||
@@ -90,80 +65,6 @@ func (s *SubService) getClientInfo(c *model.Client) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SubService) addClientInfo(uri string, clientInfo string) string {
|
||||
protocol := strings.Split(uri, "://")
|
||||
if len(protocol) < 2 {
|
||||
return uri
|
||||
}
|
||||
switch protocol[0] {
|
||||
case "vmess":
|
||||
var vmessJson map[string]interface{}
|
||||
config, err := base64.StdEncoding.DecodeString(protocol[1])
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error decoding vmess content:", err)
|
||||
return uri
|
||||
}
|
||||
err = json.Unmarshal(config, &vmessJson)
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error decoding vmess content:", err)
|
||||
return uri
|
||||
}
|
||||
vmessJson["ps"] = vmessJson["ps"].(string) + clientInfo
|
||||
result, err := json.MarshalIndent(vmessJson, "", " ")
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error decoding vmess + clientInfo content:", err)
|
||||
return uri
|
||||
}
|
||||
return "vmess://" + base64.StdEncoding.EncodeToString(result)
|
||||
default:
|
||||
return uri + clientInfo
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SubService) getExternalSub(url string) string {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
// Make the HTTP request
|
||||
response, err := client.Get(url)
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error making HTTP request:", err)
|
||||
return ""
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
// Read the response body
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error reading response body:", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Check if the content is Base64 encoded
|
||||
isBase64 := s.isBase64Encoded(string(body))
|
||||
if isBase64 {
|
||||
// Decode Base64 content
|
||||
decodedText, err := base64.StdEncoding.DecodeString(string(body))
|
||||
if err != nil {
|
||||
logger.Warning("sub: Error decoding Base64 content:", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(decodedText)
|
||||
} else {
|
||||
return string(body)
|
||||
}
|
||||
}
|
||||
|
||||
// Function to check if a string is Base64 encoded
|
||||
func (s *SubService) isBase64Encoded(str string) bool {
|
||||
_, err := base64.StdEncoding.DecodeString(str)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (s *SubService) formatTraffic(trafficBytes int64) string {
|
||||
if trafficBytes < 1024 {
|
||||
return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1))
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package util
|
||||
|
||||
import "encoding/base64"
|
||||
|
||||
// Function to return decoded bytes if a string is Base64 encoded
|
||||
func StrOrBase64Encoded(str string) string {
|
||||
decoded, err := base64.StdEncoding.DecodeString(str)
|
||||
if err == nil {
|
||||
return string(decoded)
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
func B64StrToByte(str string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(str)
|
||||
}
|
||||
|
||||
func ByteToB64Str(b []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(b)
|
||||
}
|
||||
@@ -0,0 +1,473 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"s-ui/util/common"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func GetOutbound(uri string, i int) (*map[string]interface{}, string, error) {
|
||||
u, err := url.Parse(uri)
|
||||
if err == nil {
|
||||
switch u.Scheme {
|
||||
case "vmess":
|
||||
return vmess(u.Host, i)
|
||||
case "vless":
|
||||
return vless(u, i)
|
||||
case "trojan":
|
||||
return trojan(u, i)
|
||||
case "hy", "hysteria":
|
||||
return hy(u, i)
|
||||
case "hy2", "hysteria2":
|
||||
return hy2(u, i)
|
||||
case "tuic":
|
||||
return tuic(u, i)
|
||||
case "ss", "shadowsocks":
|
||||
return ss(u, i)
|
||||
}
|
||||
}
|
||||
return nil, "", common.NewError("Unsupported link format")
|
||||
}
|
||||
|
||||
func vmess(data string, i int) (*map[string]interface{}, string, error) {
|
||||
dataByte, err := B64StrToByte(data)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
var dataJson map[string]interface{}
|
||||
err = json.Unmarshal(dataByte, &dataJson)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
transport := map[string]interface{}{}
|
||||
tp_net, _ := dataJson["net"].(string)
|
||||
tp_type, _ := dataJson["type"].(string)
|
||||
tp_host, _ := dataJson["host"].(string)
|
||||
tp_path, _ := dataJson["path"].(string)
|
||||
switch strings.ToLower(tp_net) {
|
||||
case "tcp", "":
|
||||
if tp_type == "http" {
|
||||
transport["type"] = tp_type
|
||||
if len(tp_host) > 0 {
|
||||
transport["host"] = strings.Split(tp_host, ",")
|
||||
}
|
||||
transport["path"] = tp_path
|
||||
}
|
||||
case "http", "h2":
|
||||
transport["type"] = "http"
|
||||
if len(tp_host) > 0 {
|
||||
transport["host"] = strings.Split(tp_host, ",")
|
||||
}
|
||||
transport["path"] = tp_path
|
||||
case "ws":
|
||||
transport["type"] = tp_net
|
||||
transport["path"] = tp_path
|
||||
transport["early_data_header_name"] = "Sec-WebSocket-Protocol"
|
||||
if len(tp_host) > 0 {
|
||||
transport["headers"] = map[string]interface{}{
|
||||
"Host": tp_host,
|
||||
}
|
||||
}
|
||||
case "quic":
|
||||
transport["type"] = tp_net
|
||||
case "grpc":
|
||||
transport["type"] = tp_net
|
||||
transport["service_name"] = tp_path
|
||||
case "httpupgrade":
|
||||
transport["type"] = tp_net
|
||||
transport["path"] = tp_path
|
||||
transport["host"] = tp_host
|
||||
default:
|
||||
return nil, "", common.NewError("Invalid vmess")
|
||||
}
|
||||
tls := map[string]interface{}{}
|
||||
vmess_tls, _ := dataJson["tls"].(string)
|
||||
if vmess_tls == "tls" {
|
||||
tls["enabled"] = true
|
||||
tls_sni, _ := dataJson["sni"].(string)
|
||||
tls_alpn, _ := dataJson["alpn"].(string)
|
||||
_, tls_insecure := dataJson["allowInsecure"]
|
||||
tls_fp, _ := dataJson["fp"].(string)
|
||||
if len(tls_sni) > 0 {
|
||||
tls["server_name"] = tls_sni
|
||||
}
|
||||
if len(tls_alpn) > 0 {
|
||||
tls["alpn"] = strings.Split(tls_alpn, ",")
|
||||
}
|
||||
if tls_insecure {
|
||||
tls["insecure"] = true
|
||||
}
|
||||
if len(tls_fp) > 0 {
|
||||
tls["utls"] = map[string]interface{}{
|
||||
"enabled": true,
|
||||
"fingerprint": tls_fp,
|
||||
}
|
||||
}
|
||||
}
|
||||
tag, _ := dataJson["ps"].(string)
|
||||
if i > 0 {
|
||||
tag = fmt.Sprintf("%d.%s", i, tag)
|
||||
}
|
||||
alter_id, ok := dataJson["aid"].(int)
|
||||
if !ok {
|
||||
alter_id = 0
|
||||
}
|
||||
vmess := map[string]interface{}{
|
||||
"type": "vmess",
|
||||
"tag": tag,
|
||||
"server": dataJson["add"],
|
||||
"server_port": dataJson["port"],
|
||||
"uuid": dataJson["id"],
|
||||
"security": "auto",
|
||||
"alter_id": alter_id,
|
||||
"tls": tls,
|
||||
"transport": transport,
|
||||
}
|
||||
return &vmess, tag, err
|
||||
}
|
||||
|
||||
func vless(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||
query, _ := url.ParseQuery(u.RawQuery)
|
||||
security := query.Get("security")
|
||||
host, portStr, _ := net.SplitHostPort(u.Host)
|
||||
port := 80
|
||||
if len(portStr) > 0 {
|
||||
port, _ = strconv.Atoi(portStr)
|
||||
} else {
|
||||
if security == "tls" || security == "reality" {
|
||||
port = 443
|
||||
}
|
||||
}
|
||||
tp_type := query.Get("type")
|
||||
tag := u.Fragment
|
||||
if i > 0 {
|
||||
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||
}
|
||||
vless := map[string]interface{}{
|
||||
"type": "vless",
|
||||
"tag": tag,
|
||||
"server": host,
|
||||
"server_port": port,
|
||||
"uuid": u.User.Username(),
|
||||
"flow": query.Get("flow"),
|
||||
"tls": getTls(security, &query),
|
||||
"transport": getTransport(tp_type, &query),
|
||||
}
|
||||
return &vless, tag, nil
|
||||
}
|
||||
|
||||
func trojan(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||
query, _ := url.ParseQuery(u.RawQuery)
|
||||
security := query.Get("security")
|
||||
host, portStr, _ := net.SplitHostPort(u.Host)
|
||||
port := 80
|
||||
if len(portStr) > 0 {
|
||||
port, _ = strconv.Atoi(portStr)
|
||||
} else {
|
||||
if security == "tls" || security == "reality" {
|
||||
port = 443
|
||||
}
|
||||
}
|
||||
tp_type := query.Get("type")
|
||||
tag := u.Fragment
|
||||
if i > 0 {
|
||||
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||
}
|
||||
trojan := map[string]interface{}{
|
||||
"type": "trojan",
|
||||
"tag": tag,
|
||||
"server": host,
|
||||
"server_port": port,
|
||||
"password": u.User.Username(),
|
||||
"tls": getTls(security, &query),
|
||||
"transport": getTransport(tp_type, &query),
|
||||
}
|
||||
return &trojan, tag, nil
|
||||
}
|
||||
|
||||
func hy(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||
query, _ := url.ParseQuery(u.RawQuery)
|
||||
host, portStr, _ := net.SplitHostPort(u.Host)
|
||||
port := 443
|
||||
if len(portStr) > 0 {
|
||||
port, _ = strconv.Atoi(portStr)
|
||||
}
|
||||
|
||||
tls := map[string]interface{}{
|
||||
"enabled": true,
|
||||
"server_name": query.Get("peer"),
|
||||
}
|
||||
alpn := query.Get("alpn")
|
||||
insecure := query.Get("insecure")
|
||||
if len(alpn) > 0 {
|
||||
tls["alpn"] = strings.Split(alpn, ",")
|
||||
}
|
||||
if insecure == "1" || insecure == "true" {
|
||||
tls["insecure"] = true
|
||||
}
|
||||
|
||||
tag := u.Fragment
|
||||
if i > 0 {
|
||||
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||
}
|
||||
hy := map[string]interface{}{
|
||||
"type": "hysteria",
|
||||
"tag": tag,
|
||||
"server": host,
|
||||
"server_port": port,
|
||||
"obfs": query.Get("obfsParam"),
|
||||
"auth_str": query.Get("auth"),
|
||||
"tls": tls,
|
||||
}
|
||||
down, _ := strconv.Atoi(query.Get("downmbps"))
|
||||
up, _ := strconv.Atoi(query.Get("upmbps"))
|
||||
recv_window_conn, _ := strconv.Atoi(query.Get("recv_window_conn"))
|
||||
recv_window, _ := strconv.Atoi(query.Get("recv_window"))
|
||||
if down > 0 {
|
||||
hy["down_mbps"] = down
|
||||
}
|
||||
if up > 0 {
|
||||
hy["up_mbps"] = up
|
||||
}
|
||||
if recv_window_conn > 0 {
|
||||
hy["recv_window_conn"] = recv_window_conn
|
||||
}
|
||||
if recv_window > 0 {
|
||||
hy["recv_window"] = recv_window
|
||||
}
|
||||
return &hy, tag, nil
|
||||
}
|
||||
|
||||
func hy2(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||
query, _ := url.ParseQuery(u.RawQuery)
|
||||
host, portStr, _ := net.SplitHostPort(u.Host)
|
||||
port := 443
|
||||
if len(portStr) > 0 {
|
||||
port, _ = strconv.Atoi(portStr)
|
||||
}
|
||||
|
||||
tls := map[string]interface{}{
|
||||
"enabled": true,
|
||||
"server_name": query.Get("sni"),
|
||||
}
|
||||
alpn := query.Get("alpn")
|
||||
insecure := query.Get("insecure")
|
||||
if len(alpn) > 0 {
|
||||
tls["alpn"] = strings.Split(alpn, ",")
|
||||
}
|
||||
if insecure == "1" || insecure == "true" {
|
||||
tls["insecure"] = true
|
||||
}
|
||||
|
||||
tag := u.Fragment
|
||||
if i > 0 {
|
||||
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||
}
|
||||
hy2 := map[string]interface{}{
|
||||
"type": "hysteria2",
|
||||
"tag": tag,
|
||||
"server": host,
|
||||
"server_port": port,
|
||||
"password": u.User.Username(),
|
||||
"tls": tls,
|
||||
}
|
||||
down, _ := strconv.Atoi(query.Get("downmbps"))
|
||||
up, _ := strconv.Atoi(query.Get("upmbps"))
|
||||
obfs := query.Get("obfs")
|
||||
if down > 0 {
|
||||
hy2["down_mbps"] = down
|
||||
}
|
||||
if up > 0 {
|
||||
hy2["up_mbps"] = up
|
||||
}
|
||||
if obfs == "salamander" {
|
||||
hy2["obfs"] = map[string]interface{}{
|
||||
"type": "salamander",
|
||||
"password": query.Get("obfs-password"),
|
||||
}
|
||||
}
|
||||
return &hy2, tag, nil
|
||||
}
|
||||
|
||||
func tuic(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||
query, _ := url.ParseQuery(u.RawQuery)
|
||||
host, portStr, _ := net.SplitHostPort(u.Host)
|
||||
port := 443
|
||||
if len(portStr) > 0 {
|
||||
port, _ = strconv.Atoi(portStr)
|
||||
}
|
||||
|
||||
tls := map[string]interface{}{
|
||||
"enabled": true,
|
||||
"server_name": query.Get("sni"),
|
||||
}
|
||||
alpn := query.Get("alpn")
|
||||
insecure := query.Get("allow_insecure")
|
||||
disable_sni := query.Get("disable_sni")
|
||||
if len(alpn) > 0 {
|
||||
tls["alpn"] = strings.Split(alpn, ",")
|
||||
}
|
||||
if insecure == "1" || insecure == "true" {
|
||||
tls["insecure"] = true
|
||||
}
|
||||
if disable_sni == "1" || disable_sni == "true" {
|
||||
tls["disable_sni"] = true
|
||||
}
|
||||
|
||||
tag := u.Fragment
|
||||
if i > 0 {
|
||||
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||
}
|
||||
password, _ := u.User.Password()
|
||||
tuic := map[string]interface{}{
|
||||
"type": "tuic",
|
||||
"tag": tag,
|
||||
"server": host,
|
||||
"server_port": port,
|
||||
"uuid": u.User.Username(),
|
||||
"password": password,
|
||||
"congestion_control": query.Get("congestion_control"),
|
||||
"udp_relay_mode": query.Get("udp_relay_mode"),
|
||||
"tls": tls,
|
||||
}
|
||||
return &tuic, tag, nil
|
||||
}
|
||||
|
||||
func ss(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||
query, _ := url.ParseQuery(u.RawQuery)
|
||||
host, portStr, _ := net.SplitHostPort(u.Host)
|
||||
port := 443
|
||||
if len(portStr) > 0 {
|
||||
port, _ = strconv.Atoi(portStr)
|
||||
}
|
||||
method := u.User.Username()
|
||||
password, ok := u.User.Password()
|
||||
if !ok {
|
||||
decrypted := StrOrBase64Encoded(method)
|
||||
decrypted_arr := strings.Split(decrypted, ":")
|
||||
if len(decrypted_arr) > 1 {
|
||||
method = decrypted_arr[0]
|
||||
password = strings.Join(decrypted_arr[1:], ":")
|
||||
} else {
|
||||
return nil, "", common.NewError("Unsupported shadowsocks")
|
||||
}
|
||||
}
|
||||
|
||||
tag := u.Fragment
|
||||
if i > 0 {
|
||||
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||
}
|
||||
ss := map[string]interface{}{
|
||||
"type": "shadowsocks",
|
||||
"tag": tag,
|
||||
"server": host,
|
||||
"server_port": port,
|
||||
"method": method,
|
||||
"password": password,
|
||||
}
|
||||
|
||||
v2ray_type := query.Get("type")
|
||||
if len(v2ray_type) > 0 {
|
||||
pl_arr := []string{}
|
||||
host_header := query.Get("host")
|
||||
if query.Get("security") == "tls" {
|
||||
pl_arr = append(pl_arr, "tls")
|
||||
}
|
||||
if v2ray_type == "quic" {
|
||||
pl_arr = append(pl_arr, "mode=quic")
|
||||
}
|
||||
if len(host_header) > 0 {
|
||||
pl_arr = append(pl_arr, "host="+host_header)
|
||||
}
|
||||
ss["plugin"] = "v2ray-plugin"
|
||||
ss["plugin_opts"] = strings.Join(pl_arr, ";")
|
||||
}
|
||||
plugin := query.Get("plugin")
|
||||
if len(plugin) > 0 {
|
||||
pl_arr := strings.Split(plugin, ";")
|
||||
if len(pl_arr) > 0 {
|
||||
ss["plugin"] = pl_arr[0]
|
||||
ss["plugin_opts"] = strings.Join(pl_arr[1:], ";")
|
||||
}
|
||||
}
|
||||
return &ss, tag, nil
|
||||
}
|
||||
|
||||
func getTransport(tp_type string, q *url.Values) *map[string]interface{} {
|
||||
transport := map[string]interface{}{}
|
||||
tp_host := q.Get("host")
|
||||
tp_path := q.Get("path")
|
||||
switch strings.ToLower(tp_type) {
|
||||
case "tcp", "":
|
||||
if q.Get("headerType") == "http" {
|
||||
transport["type"] = "http"
|
||||
if len(tp_host) > 0 {
|
||||
transport["host"] = strings.Split(tp_host, ",")
|
||||
}
|
||||
transport["path"] = tp_path
|
||||
}
|
||||
case "http", "h2":
|
||||
transport["type"] = "http"
|
||||
if len(tp_host) > 0 {
|
||||
transport["host"] = strings.Split(tp_host, ",")
|
||||
}
|
||||
transport["path"] = tp_path
|
||||
case "ws":
|
||||
transport["type"] = "ws"
|
||||
transport["path"] = tp_path
|
||||
if len(tp_host) > 0 {
|
||||
transport["headers"] = map[string]interface{}{
|
||||
"Host": tp_host,
|
||||
}
|
||||
}
|
||||
case "quic":
|
||||
transport["type"] = "quic"
|
||||
case "grpc":
|
||||
transport["type"] = "grpc"
|
||||
transport["service_name"] = q.Get("serviceName")
|
||||
case "httpupgrade":
|
||||
transport["type"] = "httpupgrade"
|
||||
transport["path"] = tp_path
|
||||
transport["host"] = tp_host
|
||||
}
|
||||
return &transport
|
||||
}
|
||||
|
||||
func getTls(security string, q *url.Values) *map[string]interface{} {
|
||||
tls := map[string]interface{}{}
|
||||
tls_fp := q.Get("fp")
|
||||
tls_sni := q.Get("sni")
|
||||
tls_insecure := q.Get("allowInsecure")
|
||||
tls_alpn := q.Get("alpn")
|
||||
switch security {
|
||||
case "tls":
|
||||
tls["enabled"] = true
|
||||
case "reality":
|
||||
tls["enabled"] = true
|
||||
tls["reality"] = map[string]interface{}{
|
||||
"enabled": true,
|
||||
"public_key": q.Get("pbk"),
|
||||
"short_id": q.Get("sid"),
|
||||
}
|
||||
}
|
||||
if len(tls_sni) > 0 {
|
||||
tls["server_name"] = tls_sni
|
||||
}
|
||||
if len(tls_alpn) > 0 {
|
||||
tls["alpn"] = strings.Split(tls_alpn, ",")
|
||||
}
|
||||
if tls_insecure == "1" || tls_insecure == "true" {
|
||||
tls["insecure"] = true
|
||||
}
|
||||
if len(tls_fp) > 0 {
|
||||
tls["utls"] = map[string]interface{}{
|
||||
"enabled": true,
|
||||
"fingerprint": tls_fp,
|
||||
}
|
||||
}
|
||||
return &tls
|
||||
}
|
||||
+1
-1
@@ -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.13
|
||||
ARG SINGBOX_VER=v1.9.3
|
||||
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}
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ services:
|
||||
- "2096:2096"
|
||||
networks:
|
||||
- s-ui
|
||||
entrypoint: "./sui"
|
||||
entrypoint: "./sui migrate && ./sui"
|
||||
|
||||
sing-box:
|
||||
image: alireza7/s-ui-singbox
|
||||
|
||||
Generated
+736
-887
File diff suppressed because it is too large
Load Diff
+13
-15
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
@@ -9,7 +9,7 @@
|
||||
"lint": "eslint . --fix --ignore-path .gitignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "7.0.96",
|
||||
"@mdi/font": "7.4.47",
|
||||
"axios": "^1.7.2",
|
||||
"chart.js": "^4.4.3",
|
||||
"clipboard": "^2.0.11",
|
||||
@@ -19,26 +19,24 @@
|
||||
"pinia": "^2.1.7",
|
||||
"qrcode.vue": "^3.4.1",
|
||||
"roboto-fontface": "^0.10.0",
|
||||
"vue": "^3.2.0",
|
||||
"vue": "^3.4.31",
|
||||
"vue-chartjs": "^5.3.1",
|
||||
"vue-i18n": "^9.13.1",
|
||||
"vue-router": "^4.3.2",
|
||||
"vue-router": "^4.4.0",
|
||||
"vue3-persian-datetime-picker": "^1.2.2",
|
||||
"vuetify": "^3.6.7"
|
||||
"vuetify": "^3.6.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"@babel/types": "^7.24.7",
|
||||
"@types/node": "^20.14.9",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"eslint-plugin-vue": "^9.26.0",
|
||||
"material-design-icons-iconfont": "^6.7.0",
|
||||
"sass": "^1.77.2",
|
||||
"typescript": "^5.4.5",
|
||||
"sass": "^1.77.6",
|
||||
"typescript": "^5.5.2",
|
||||
"unplugin-fonts": "^1.1.1",
|
||||
"vite": "^4.5.3",
|
||||
"vite-plugin-vuetify": "^1.0.2",
|
||||
"vue-tsc": "^1.8.27"
|
||||
"vite": "^5.3.2",
|
||||
"vite-plugin-vuetify": "^2.0.3",
|
||||
"vue-tsc": "^2.0.22"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.addr')"
|
||||
hide-details
|
||||
required
|
||||
v-model="addr.server">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.port')"
|
||||
hide-details
|
||||
type="number"
|
||||
required
|
||||
v-model.number="addr.server_port"></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionRemark">
|
||||
<v-text-field
|
||||
:label="$t('in.remark')"
|
||||
hide-details
|
||||
v-model="addr.remark">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionTLS">
|
||||
<v-switch
|
||||
:label="$t('tls.enable')"
|
||||
color="primary"
|
||||
hide-details
|
||||
@update:model-value="updateTls($event)"
|
||||
v-model="addr.tls" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionSNI">
|
||||
<v-text-field
|
||||
label="SNI"
|
||||
hide-details
|
||||
v-model="addr.server_name">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="optionInsecure">
|
||||
<v-switch
|
||||
:label="$t('tls.insecure')"
|
||||
hide-details
|
||||
color="primary"
|
||||
v-model="addr.insecure" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto" align="end" justify="center">
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('in.mdOption') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="optionRemark" color="primary" :label="$t('in.remark')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="hasTls">
|
||||
<v-switch v-model="optionTLS" color="primary" :label="$t('objects.tls')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="addr.tls">
|
||||
<v-switch v-model="optionSNI" color="primary" label="SNI" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="addr.tls">
|
||||
<v-switch v-model="optionInsecure" color="primary" :label="$t('tls.insecure')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['addr', 'hasTls'],
|
||||
data() {
|
||||
return {
|
||||
menu: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
optionTLS: {
|
||||
get(): boolean { return this.$props.addr.tls != undefined },
|
||||
set(v:boolean) { this.$props.addr.tls = v ? true : undefined; this.updateTls(v) }
|
||||
},
|
||||
optionSNI: {
|
||||
get(): boolean { return this.$props.addr.server_name != undefined },
|
||||
set(v:boolean) { this.$props.addr.server_name = v ? '' : undefined }
|
||||
},
|
||||
optionRemark: {
|
||||
get(): boolean { return this.$props.addr.remark != undefined },
|
||||
set(v:boolean) { this.$props.addr.remark = v ? '' : undefined }
|
||||
},
|
||||
optionInsecure: {
|
||||
get(): boolean { return this.$props.addr.insecure != undefined },
|
||||
set(v:boolean) { this.$props.addr.insecure = v ? false : undefined }
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateTls(v:boolean) {
|
||||
if (!v) {
|
||||
delete this.$props.addr.insecure
|
||||
delete this.$props.addr.server_name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -42,6 +42,7 @@
|
||||
<script lang="ts">
|
||||
import DatePicker from 'vue3-persian-datetime-picker'
|
||||
import { i18n } from '@/locales'
|
||||
import 'moment/locale/vi'
|
||||
import 'moment/locale/zh-cn'
|
||||
import 'moment/locale/zh-tw'
|
||||
|
||||
@@ -58,7 +59,14 @@ export default {
|
||||
computed: {
|
||||
locale() {
|
||||
const l = i18n.global.locale.value
|
||||
return l.replace('zh', 'zh-')
|
||||
switch (l) {
|
||||
case "zhHans":
|
||||
return "zh-cn"
|
||||
case "zhHant":
|
||||
return "zh-tw"
|
||||
default:
|
||||
return l
|
||||
}
|
||||
},
|
||||
dateFormatted() {
|
||||
if (this.expDate == 0) return i18n.global.t('unlimited')
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<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('dial.options') }}</v-btn>
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('dial.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<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('listen.options') }}</v-btn>
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('listen.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<v-col cols="auto">
|
||||
<v-dialog v-model="menu" :close-on-content-click="false" transition="scale-transition" max-width="800">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" variant="tonal">{{ $t('main.tiles') }} <v-icon icon="mdi-star-plus" /></v-btn>
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('main.tiles') }} <v-icon icon="mdi-star-plus" /></v-btn>
|
||||
</template>
|
||||
<v-card rounded="xl">
|
||||
<v-card-title>
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<template>
|
||||
<v-card :subtitle="type">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.SOCKS">
|
||||
<v-select
|
||||
hide-details
|
||||
:items="['4','4a','5']"
|
||||
:label="$t('version')"
|
||||
v-model="inData.outJson.version">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="needNetwork">
|
||||
<Network :data="inData.outJson" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="needUot">
|
||||
<UoT :data="inData.outJson" />
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.HTTP">
|
||||
<v-text-field
|
||||
:label="$t('transport.path')"
|
||||
hide-details
|
||||
v-model="inData.outJson.path">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.VMess || type == inTypes.VLESS">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('types.vless.udpEnc')"
|
||||
:items="['none','packetaddr','xudp']"
|
||||
v-model="packet_encoding">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<template v-if="type == inTypes.VMess">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('types.vmess.security')"
|
||||
:items="vmessSecurities"
|
||||
v-model="inData.outJson.security">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="inData.outJson.global_padding" color="primary" :label="$t('types.vmess.globalPadding')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch v-model="inData.outJson.authenticated_length" color="primary" :label="$t('types.vmess.authLen')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</template>
|
||||
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.Hysteria">
|
||||
<v-text-field
|
||||
label="Recv window"
|
||||
hide-details
|
||||
type="number"
|
||||
min="0"
|
||||
v-model.number="inData.outJson.recv_window">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<template v-if="type == inTypes.TUIC">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
label="UDP Relay Mode"
|
||||
:items="['native', 'quic']"
|
||||
clearable
|
||||
@click:clear="delete inData.outJson.udp_relay_mode"
|
||||
v-model="inData.outJson.udp_relay_mode">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-switch color="primary" label="UDP Over Stream" v-model="inData.outJson.udp_over_stream" hide-details></v-switch>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
<Headers :data="inData.outJson" v-if="type == inTypes.HTTP" />
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { InTypes } from '@/types/inbounds'
|
||||
import Network from './Network.vue'
|
||||
import UoT from './UoT.vue'
|
||||
import Headers from './Headers.vue'
|
||||
|
||||
export default {
|
||||
props: ['inData', 'type'],
|
||||
data() {
|
||||
return {
|
||||
inTypes: InTypes,
|
||||
vmessSecurities: [
|
||||
"auto",
|
||||
"none",
|
||||
"zero",
|
||||
"aes-128-gcm",
|
||||
"aes-128-ctr",
|
||||
"chacha20-poly1305",
|
||||
],
|
||||
haveNetwork: [
|
||||
InTypes.SOCKS,
|
||||
InTypes.Shadowsocks,
|
||||
InTypes.VMess,
|
||||
InTypes.Trojan,
|
||||
InTypes.Hysteria,
|
||||
InTypes.VLESS,
|
||||
InTypes.TUIC,
|
||||
InTypes.Hysteria2,
|
||||
],
|
||||
havUoT: [
|
||||
InTypes.SOCKS,
|
||||
InTypes.Shadowsocks,
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
needNetwork():boolean { return this.haveNetwork.includes(this.$props.type) },
|
||||
needUot():boolean { return this.havUoT.includes(this.$props.type) },
|
||||
packet_encoding: {
|
||||
get() { return this.$props.inData.outJson.packet_encoding != undefined ? this.$props.inData.outJson.packet_encoding : 'none'; },
|
||||
set(v:string) { this.$props.inData.outJson.packet_encoding = v != "none" ? v : undefined }
|
||||
},
|
||||
},
|
||||
components: { Network, UoT, Headers }
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,368 @@
|
||||
<template>
|
||||
<v-card>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select
|
||||
v-model="ruleToDirect"
|
||||
:items="geoList"
|
||||
:label="$t('setting.toDirect')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-select
|
||||
v-model="ruleToBlock"
|
||||
:items="geoList"
|
||||
:label="$t('setting.toBlock')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="enableLog">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('basic.log.level')"
|
||||
:items="levels"
|
||||
v-model="subJsonExt.log.level">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-switch v-model="subJsonExt.log.timestamp" color="primary" :label="$t('setting.timestamp')" hide-details />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="enableDns">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-text-field
|
||||
v-model="proxyDns"
|
||||
hide-details
|
||||
:label="$t('setting.globalDns')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-text-field
|
||||
v-model="directDns"
|
||||
hide-details
|
||||
clearable
|
||||
:label="$t('setting.directDns')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="directDns.length>0">
|
||||
<v-select
|
||||
v-model="dnsToDirect"
|
||||
:items="geositeList"
|
||||
:label="$t('setting.toDirectDns')"
|
||||
multiple
|
||||
chips
|
||||
hide-details
|
||||
></v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('setting.jsonSubOptions') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch v-model="enableLog" color="primary" :label="$t('basic.log.title')" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="enableDns" color="primary" label="DNS" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch v-model="enableExp" color="primary" label="Experimental" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
props: ['settings'],
|
||||
data() {
|
||||
return {
|
||||
menu: false,
|
||||
subJsonExt: <any>{},
|
||||
levels: ["trace", "debug", "info", "warn", "error", "fatal", "panic"],
|
||||
defaultLog: {
|
||||
"level": "info",
|
||||
"timestamp": true
|
||||
},
|
||||
defaultExp: {
|
||||
"clash_api": {
|
||||
"external_controller": "127.0.0.1:9090",
|
||||
"external_ui": "ui",
|
||||
"secret": "",
|
||||
"external_ui_download_url": "https://mirror.ghproxy.com/https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip",
|
||||
"external_ui_download_detour": "direct",
|
||||
"default_mode": "rule"
|
||||
},
|
||||
"cache_file": {
|
||||
"enabled": true,
|
||||
"store_fakeip": false
|
||||
}
|
||||
},
|
||||
defaultDns: {
|
||||
"servers": [
|
||||
{
|
||||
"address": "tcp://8.8.8.8",
|
||||
"detour": "proxy",
|
||||
"address_resolver": "local-dns",
|
||||
"tag": "proxy-dns"
|
||||
},
|
||||
{
|
||||
"tag": "local-dns",
|
||||
"address": "local",
|
||||
"detour": "direct"
|
||||
},
|
||||
{
|
||||
"address": "rcode://success",
|
||||
"tag": "block"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"clash_mode": "Global",
|
||||
"source_ip_cidr": [
|
||||
"172.19.0.0/30"
|
||||
],
|
||||
"server": "proxy-dns"
|
||||
},
|
||||
{
|
||||
"source_ip_cidr": [
|
||||
"172.19.0.0/30"
|
||||
],
|
||||
"server": "proxy-dns"
|
||||
}
|
||||
],
|
||||
"final": "local-dns",
|
||||
"strategy": "prefer_ipv4"
|
||||
},
|
||||
geositeList: [
|
||||
{ title: "Private", value: "geosite-private" },
|
||||
{ title: "Ads", value: "geosite-ads" },
|
||||
{ title: "🇮🇷 Iran", value: "geosite-ir" },
|
||||
{ title: "🇨🇳 China", value: "geosite-cn" },
|
||||
{ title: "🇻🇳 Vietnam", value: "geosite-vn" },
|
||||
],
|
||||
geoList: [
|
||||
{ title: "Site-Private", value: "geoip-private" },
|
||||
{ title: "IP-Private", value: "geosite-private" },
|
||||
{ title: "Site-Ads", value: "geosite-ads" },
|
||||
{ title: "🇮🇷 Site-Iran", value: "geosite-ir" },
|
||||
{ title: "🇮🇷 IP-Iran", value: "geoip-ir" },
|
||||
{ title: "🇨🇳 Site-China", value: "geosite-cn" },
|
||||
{ title: "🇨🇳 IP-China", value: "geoip-cn" },
|
||||
{ title: "🇻🇳 Site-Vietnam", value: "geosite-vn" },
|
||||
{ title: "🇻🇳 IP-Vietnam", value: "geoip-vn" },
|
||||
],
|
||||
geo: [
|
||||
{
|
||||
tag: "geosite-ads",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ads-all.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geosite-private",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/private.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geosite-ir",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ir.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geosite-cn",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geosite-vn",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://github.com/Thaomtam/Geosite-vn/raw/rule-set/Geosite-vn.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geoip-private",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/private.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geoip-ir",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/ir.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geoip-cn",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs",
|
||||
download_detour: "direct"
|
||||
},
|
||||
{
|
||||
tag: "geoip-vn",
|
||||
type: "remote",
|
||||
format: "binary",
|
||||
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/vn.srs",
|
||||
download_detour: "direct"
|
||||
}
|
||||
],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
enableLog: {
|
||||
get() :boolean { return this.subJsonExt?.log != undefined },
|
||||
set(v:boolean) { v ? this.subJsonExt.log = this.defaultLog : delete this.subJsonExt.log }
|
||||
},
|
||||
enableDns: {
|
||||
get() :boolean { return this.subJsonExt?.dns != undefined },
|
||||
set(v:boolean) {
|
||||
if (v) {
|
||||
this.subJsonExt.dns = this.defaultDns
|
||||
if (this.rules == undefined) this.subJsonExt.rules = []
|
||||
this.subJsonExt.rules.unshift({ protocol: "dns", outbound: "dns-out" })
|
||||
} else {
|
||||
delete this.subJsonExt.dns
|
||||
const ruleDnsIndex = this.subJsonExt?.rules?.findIndex((r:any) => r.protocol = "dns" && r.outbound == "dns-out")
|
||||
if (ruleDnsIndex >= 0) this.subJsonExt.rules.splice(ruleDnsIndex,1)
|
||||
if (this.rules.length == 0) delete this.subJsonExt.rules
|
||||
}
|
||||
}
|
||||
},
|
||||
enableExp: {
|
||||
get() :boolean { return this.subJsonExt?.experimental != undefined },
|
||||
set(v:boolean) { v ? this.subJsonExt.experimental = this.defaultExp : delete this.subJsonExt.experimental }
|
||||
},
|
||||
dns():any { return this.subJsonExt?.dns?? undefined },
|
||||
proxyDns: {
|
||||
get() :string { return this.dns?.servers[0]?.address?? "" },
|
||||
set(v:string) { this.dns.servers[0].address = v.length>0 ? v : "8.8.8.8" }
|
||||
},
|
||||
directDns: {
|
||||
get() :string { return this.dns?.servers?.findLast((d:any) => d.tag == "direct-dns")?.address?? "" },
|
||||
set(v:string) {
|
||||
const sIndex = this.dns.servers.findIndex((d:any) => d.tag == "direct-dns")
|
||||
if (v?.length>0) {
|
||||
if (sIndex === -1) {
|
||||
this.dns.servers.push({ tag: "direct-dns", address: v, detour: "direct" })
|
||||
this.dns.rules.push({ clash_mode: "Direct", server: "direct-dns" })
|
||||
} else {
|
||||
this.dns.servers[sIndex].address = v
|
||||
}
|
||||
} else {
|
||||
this.dns.servers.splice(sIndex,1)
|
||||
this.dns.rules = this.dns.rules.filter((r:any) => r.server != "direct-dns")
|
||||
}
|
||||
},
|
||||
},
|
||||
dnsToDirect: {
|
||||
get() :string[] {
|
||||
const ruleIndex = this.dns?.rules?.findIndex((r:any) => r.server == "direct-dns" && Object.hasOwn(r,'rule_set'))
|
||||
return ruleIndex >= 0 ? this.dns.rules[ruleIndex].rule_set : []
|
||||
},
|
||||
set(v:string[]) {
|
||||
const ruleIndex = this.dns?.rules?.findIndex((r:any) => r.server == "direct-dns" && Object.hasOwn(r,'rule_set'))
|
||||
if (v.length>0) {
|
||||
if (ruleIndex >= 0){
|
||||
this.dns.rules[ruleIndex].rule_set = v
|
||||
} else {
|
||||
this.dns.rules.push({ rule_set: v, server: "direct-dns" })
|
||||
}
|
||||
} else {
|
||||
if (ruleIndex != -1) this.dns.rules.splice(ruleIndex,1)
|
||||
}
|
||||
this.updateRuleSets()
|
||||
}
|
||||
},
|
||||
rules():any { return this.subJsonExt?.rules?? undefined },
|
||||
ruleToDirect: {
|
||||
get() :string[] {
|
||||
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "direct" && Object.hasOwn(r,'rule_set'))
|
||||
return ruleIndex >= 0 ? this.rules[ruleIndex].rule_set : []
|
||||
},
|
||||
set(v:string[]) {
|
||||
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "direct" && Object.hasOwn(r,'rule_set'))
|
||||
if (v.length>0) {
|
||||
if (ruleIndex >= 0){
|
||||
this.rules[ruleIndex].rule_set = v
|
||||
} else {
|
||||
if (this.rules == undefined) this.subJsonExt.rules = []
|
||||
this.rules.push({ rule_set: v, outbound: "direct" })
|
||||
}
|
||||
} else {
|
||||
if (ruleIndex != -1) this.rules.splice(ruleIndex,1)
|
||||
}
|
||||
this.updateRuleSets()
|
||||
}
|
||||
},
|
||||
ruleToBlock: {
|
||||
get() :string[] {
|
||||
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "block" && Object.hasOwn(r,'rule_set'))
|
||||
return ruleIndex >= 0 ? this.rules[ruleIndex].rule_set : []
|
||||
},
|
||||
set(v:string[]) {
|
||||
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "block" && Object.hasOwn(r,'rule_set'))
|
||||
if (v.length>0) {
|
||||
if (ruleIndex >= 0){
|
||||
this.rules[ruleIndex].rule_set = v
|
||||
} else {
|
||||
if (this.rules == undefined) this.subJsonExt.rules = []
|
||||
this.rules.push({ rule_set: v, outbound: "block" })
|
||||
}
|
||||
} else {
|
||||
if (ruleIndex != -1) this.rules.splice(ruleIndex,1)
|
||||
}
|
||||
this.updateRuleSets()
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateRuleSets(){
|
||||
let tags = <string[]>[]
|
||||
if (this.dns?.rules?.length>0) this.dns.rules.forEach((r:any) => { if (r.rule_set) tags.push(...r.rule_set) })
|
||||
if (this.rules?.length>0) this.rules.forEach((r:any) => { if (r.rule_set) tags.push(...r.rule_set) })
|
||||
if (tags.length>0){
|
||||
this.subJsonExt.rule_set = this.geo.filter((g:any) => tags.includes(g.tag))
|
||||
} else {
|
||||
delete this.subJsonExt.rule_set
|
||||
}
|
||||
if (this.rules.length == 0) delete this.subJsonExt.rules
|
||||
}
|
||||
},
|
||||
mounted(){
|
||||
this.subJsonExt = this.$props.settings?.subJsonExt?.length>0 ? JSON.parse(this.$props.settings.subJsonExt) : <any>{}
|
||||
},
|
||||
watch:{
|
||||
subJsonExt:{
|
||||
handler(v) {
|
||||
this.$props.settings.subJsonExt = Object.keys(v).length>0 ? JSON.stringify(v, null, 2) : ""
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -16,7 +16,7 @@
|
||||
<script lang="ts">
|
||||
|
||||
export default {
|
||||
props: ['inbound', 'id'],
|
||||
props: ['inbound'],
|
||||
data() {
|
||||
return {
|
||||
hasUser: false,
|
||||
|
||||
@@ -26,7 +26,7 @@ const theme = computed(() =>{
|
||||
})
|
||||
|
||||
const direction = computed(() => {
|
||||
return vuetify.locale.current.value == 'fa' ? 'rtl' : 'ltr'
|
||||
return vuetify.locale.isRtl ? 'rtl' : 'ltr'
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
<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('types.hy.hyOptions') }}</v-btn>
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.hy.hyOptions') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<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('types.hy.hy2Options') }}</v-btn>
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.hy.hy2Options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
<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('types.ssh.options') }}</v-btn>
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.ssh.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<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('types.lb.urlTestOptions') }}</v-btn>
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.lb.urlTestOptions') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<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('types.wg.options') }}</v-btn>
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.wg.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
<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>
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.acme.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
@@ -30,9 +30,23 @@
|
||||
>{{ $t('tls.useText') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
icon="mdi-key-star"
|
||||
@click="genECH"
|
||||
:loading="loading">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ $t('actions.generate') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="useEchPath == 0">
|
||||
<v-col cols="12" sm="6">
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
:label="$t('tls.keyPath')"
|
||||
hide-details
|
||||
@@ -41,7 +55,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
:label="$t('tls.key')"
|
||||
hide-details
|
||||
@@ -50,7 +64,7 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
:label="$t('tls.cert')"
|
||||
hide-details
|
||||
@@ -63,15 +77,65 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { i18n } from '@/locales'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { ech } from '@/types/inTls'
|
||||
import { push } from 'notivue'
|
||||
|
||||
export default {
|
||||
props: ['iTls','oTls'],
|
||||
data() {
|
||||
return {
|
||||
useEchPath: 0
|
||||
useEchPath: this.$props.iTls?.ech?.key? 1:0,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async genECH(){
|
||||
this.loading = true
|
||||
const msg = await HttpUtils.get('api/keypairs', { k: "ech", o: this.iTls.server_name?? "''" })
|
||||
this.loading = false
|
||||
if (msg.success && this.iTls.ech && this.oTls.ech) {
|
||||
this.iTls.ech.key_path=undefined
|
||||
this.useEchPath = 1
|
||||
if (msg.obj.length>0){
|
||||
let config = <string[]>[]
|
||||
let key = <string[]>[]
|
||||
let isConfig = false
|
||||
let isKey = false
|
||||
|
||||
msg.obj.forEach((line:string) => {
|
||||
if (line === "-----BEGIN ECH CONFIGS-----") {
|
||||
isConfig = true
|
||||
isKey = false
|
||||
config.push(line)
|
||||
} else if (line === "-----END ECH CONFIGS-----") {
|
||||
isConfig = false
|
||||
config.push(line)
|
||||
} else if (line === "-----BEGIN ECH KEYS-----") {
|
||||
isKey = true
|
||||
isConfig = false
|
||||
key.push(line)
|
||||
} else if (line === "-----END ECH KEYS-----") {
|
||||
isKey = false
|
||||
key.push(line)
|
||||
} else if (isConfig) {
|
||||
config.push(line)
|
||||
} else if (isKey) {
|
||||
key.push(line)
|
||||
}
|
||||
})
|
||||
this.iTls.ech.key = key?? undefined
|
||||
this.oTls.ech.config = config?? undefined
|
||||
|
||||
} else {
|
||||
push.error({
|
||||
message: i18n.global.t('error') + ": " + msg.obj
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
ech() {
|
||||
return <ech>this.$props.iTls.ech
|
||||
@@ -7,7 +7,7 @@
|
||||
<v-col cols="12" sm="6" md="4" v-if="tls.enabled">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Preset"
|
||||
:label="$t('template')"
|
||||
:items="tlsItems"
|
||||
@update:model-value="changeTlsItem($event)"
|
||||
v-model="tlsId">
|
||||
@@ -116,7 +116,7 @@
|
||||
<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>
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
@@ -143,6 +143,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { i18n } from '@/locales'
|
||||
import { iTls, defaultInTls } from '@/types/inTls'
|
||||
export default {
|
||||
props: ['inbound', 'tlsConfigs', 'tls_id'],
|
||||
@@ -183,7 +184,7 @@ export default {
|
||||
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 } } )]
|
||||
return [ { title: i18n.global.t('none'), value: 0 }, ...this.$props.tlsConfigs?.map((t:any) => { return { title: t.name, value: t.id } } )]
|
||||
},
|
||||
tlsId: {
|
||||
get() { return this.tls_id.value?? 0 },
|
||||
@@ -191,7 +192,10 @@ export default {
|
||||
},
|
||||
tlsEnable: {
|
||||
get() { return this.tls.enabled?? false },
|
||||
set(newValue: boolean) { this.$props.inbound.tls = newValue ? { enabled: true } : {} }
|
||||
set(newValue: boolean) {
|
||||
this.$props.inbound.tls = newValue ? { enabled: true } : {}
|
||||
this.$props.tls_id.value = 0
|
||||
}
|
||||
},
|
||||
tlsOptional(): boolean {
|
||||
return !['hysteria','hysteria2','tuic','naive'].includes(this.$props.inbound.type)
|
||||
@@ -177,7 +177,7 @@
|
||||
<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>
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
@@ -32,11 +32,11 @@ const saveChanges = () => {
|
||||
}
|
||||
|
||||
const oldData = computed((): any => {
|
||||
return {config: store.oldData.config, clients: store.oldData.clients, tls: store.oldData.tlsConfigs}
|
||||
return {config: store.oldData.config, clients: store.oldData.clients, tls: store.oldData.tlsConfigs, inData: store.oldData.inData}
|
||||
})
|
||||
|
||||
const newData = computed((): any => {
|
||||
return {config: store.config, clients: store.clients, tls: store.tlsConfigs}
|
||||
return {config: store.config, clients: store.clients, tls: store.tlsConfigs, inData: store.inData}
|
||||
})
|
||||
|
||||
const stateChange = computed((): any => {
|
||||
|
||||
@@ -120,7 +120,14 @@ export default {
|
||||
computed: {
|
||||
locale() {
|
||||
const l = i18n.global.locale.value
|
||||
return l.replace('zh', 'zh-')
|
||||
switch (l) {
|
||||
case "zhHans":
|
||||
return "zh-cn"
|
||||
case "zhHant":
|
||||
return "zh-tw"
|
||||
default:
|
||||
return l
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{{ $t('actions.' + title) + " " + $t('objects.client') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px;">
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-container style="padding: 0;">
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
@@ -38,6 +38,33 @@
|
||||
<DatePick :expiry="expDate" @submit="setDate" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="index != -1">
|
||||
<v-col cols="12" sm="6" md="4" class="d-flex flex-column">
|
||||
<div class="d-flex justify-space-between align-center">
|
||||
<div>
|
||||
{{ $t('stats.usage') }}: {{ total }}<sup dir="ltr" v-if="percent>0">({{ percent }}%)</sup>
|
||||
</div>
|
||||
<v-btn density="compact" variant="text" icon="mdi-restore" @click="client.up=0;client.down=0">
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ $t('reset') }}
|
||||
</v-tooltip>
|
||||
<v-icon />
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-progress-linear
|
||||
v-model="percent"
|
||||
:color="percentColor"
|
||||
v-if="client.volume>0"
|
||||
bottom
|
||||
>
|
||||
</v-progress-linear>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-icon icon="mdi-upload" color="orange" /><span class="text-orange">{{ up }}</span>
|
||||
/
|
||||
<v-icon icon="mdi-download" color="success" /><span class="text-success">{{ down }}</span>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>
|
||||
<v-combobox
|
||||
@@ -156,6 +183,7 @@
|
||||
import { Link } from '@/plugins/link'
|
||||
import { createClient, randomConfigs, updateConfigs } from '@/types/clients'
|
||||
import DatePick from '@/components/DateTime.vue'
|
||||
import { HumanReadable } from '@/plugins/utils'
|
||||
|
||||
export default {
|
||||
props: ['visible', 'data', 'index', 'inboundTags', 'stats'],
|
||||
@@ -222,7 +250,12 @@ export default {
|
||||
Volume: {
|
||||
get() { return this.client.volume == 0 ? 0 : (this.client.volume / (1024 ** 3)) },
|
||||
set(v:number) { this.client.volume = v > 0 ? v*(1024 ** 3) : 0 }
|
||||
}
|
||||
},
|
||||
up() :string { return HumanReadable.sizeFormat(this.client.up) },
|
||||
down() :string { return HumanReadable.sizeFormat(this.client.down) },
|
||||
total() :string { return HumanReadable.sizeFormat(this.client.down + this.client.up) },
|
||||
percent() :number { return this.client.volume>0 ? Math.round((this.client.up + this.client.down) *100 / this.client.volume) : 0 },
|
||||
percentColor() :string { return (this.client.up+this.client.down) >= this.client.volume ? 'error' : this.percent>90 ? 'warning' : 'success' },
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
|
||||
@@ -5,35 +5,64 @@
|
||||
{{ $t('actions.' + title) + " " + $t('objects.inbound') }}
|
||||
</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('type')"
|
||||
:items="Object.keys(inTypes).map((key,index) => ({title: key, value: Object.values(inTypes)[index]}))"
|
||||
v-model="inbound.type"
|
||||
@update:modelValue="changeType">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="inbound.tag" :label="$t('objects.tag')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Listen :inbound="inbound" :inTags="inTags" />
|
||||
<Direct v-if="inbound.type == inTypes.Direct" direction="in" :data="inbound" />
|
||||
<Shadowsocks v-if="inbound.type == inTypes.Shadowsocks" direction="in" :data="inbound" />
|
||||
<Hysteria v-if="inbound.type == inTypes.Hysteria" direction="in" :data="inbound" />
|
||||
<Hysteria2 v-if="inbound.type == inTypes.Hysteria2" direction="in" :data="inbound" />
|
||||
<Naive v-if="inbound.type == inTypes.Naive" :inbound="inbound" />
|
||||
<ShadowTls v-if="inbound.type == inTypes.ShadowTLS" direction="in" :data="inbound" :outTags="outTags" />
|
||||
<Tuic v-if="inbound.type == inTypes.TUIC" direction="in" :data="inbound" />
|
||||
<TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" />
|
||||
<Transport v-if="Object.hasOwn(inbound,'transport')" :data="inbound" />
|
||||
<Users v-if="HasOptionalUser.includes(inbound.type)" :inbound="inbound" :id="id" />
|
||||
<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 style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-container style="padding: 0;">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('type')"
|
||||
:items="Object.keys(inTypes).map((key,index) => ({title: key, value: Object.values(inTypes)[index]}))"
|
||||
v-model="inbound.type"
|
||||
@update:modelValue="changeType">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="inbound.tag" :label="$t('objects.tag')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-tabs
|
||||
v-if="HasInData.includes(inbound.type)"
|
||||
v-model="side"
|
||||
density="compact"
|
||||
fixed-tabs
|
||||
align-tabs="center"
|
||||
>
|
||||
<v-tab value="s">{{ $t('in.sSide') }}</v-tab>
|
||||
<v-tab value="c">{{ $t('in.cSide') }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="side" style="margin-top: 10px;">
|
||||
<v-window-item value="s">
|
||||
<Listen :inbound="inbound" :inTags="inTags" />
|
||||
<Direct v-if="inbound.type == inTypes.Direct" direction="in" :data="inbound" />
|
||||
<Shadowsocks v-if="inbound.type == inTypes.Shadowsocks" direction="in" :data="inbound" />
|
||||
<Hysteria v-if="inbound.type == inTypes.Hysteria" direction="in" :data="inbound" />
|
||||
<Hysteria2 v-if="inbound.type == inTypes.Hysteria2" direction="in" :data="inbound" />
|
||||
<Naive v-if="inbound.type == inTypes.Naive" :inbound="inbound" />
|
||||
<ShadowTls v-if="inbound.type == inTypes.ShadowTLS" direction="in" :data="inbound" :outTags="outTags" />
|
||||
<Tuic v-if="inbound.type == inTypes.TUIC" direction="in" :data="inbound" />
|
||||
<TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" />
|
||||
<Transport v-if="Object.hasOwn(inbound,'transport')" :data="inbound" />
|
||||
<Users v-if="HasOptionalUser.includes(inbound.type)" :inbound="inbound" />
|
||||
<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-window-item>
|
||||
<v-window-item value="c">
|
||||
<OutJsonVue :inData="inData" :type="inbound.type" />
|
||||
<v-card>
|
||||
<v-card-subtitle>{{ $t('in.multiDomain') }}
|
||||
<v-icon @click="add_addr" icon="mdi-plus"></v-icon>
|
||||
</v-card-subtitle>
|
||||
<template v-for="addr,index in inData.addrs">
|
||||
{{ $t('in.addr') }} #{{ (index+1) }} <v-icon icon="mdi-delete" @click="inData.addrs.splice(index,1)" />
|
||||
<v-divider></v-divider>
|
||||
<AddrVue :addr="addr" :hasTls="Object.hasOwn(inbound,'tls')" />
|
||||
</template>
|
||||
</v-card>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
@@ -59,6 +88,9 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { InTypes, createInbound } from '@/types/inbounds'
|
||||
import { Addr, InData } from '@/plugins/inData'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
|
||||
import Listen from '@/components/Listen.vue'
|
||||
import Direct from '@/components/protocols/Direct.vue'
|
||||
import Users from '@/components/Users.vue'
|
||||
@@ -68,46 +100,83 @@ import Hysteria2 from '@/components/protocols/Hysteria2.vue'
|
||||
import Naive from '@/components/protocols/Naive.vue'
|
||||
import ShadowTls from '@/components/protocols/ShadowTls.vue'
|
||||
import Tuic from '@/components/protocols/Tuic.vue'
|
||||
import InTls from '@/components/InTLS.vue'
|
||||
import InTls from '@/components/tls/InTLS.vue'
|
||||
import TProxy from '@/components/protocols/TProxy.vue'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
import Multiplex from '@/components/Multiplex.vue'
|
||||
import Transport from '@/components/Transport.vue'
|
||||
import AddrVue from '@/components/Addr.vue'
|
||||
import OutJsonVue from '@/components/OutJson.vue'
|
||||
export default {
|
||||
props: ['visible', 'data', 'id', 'stats', 'inTags', 'outTags', 'tlsConfigs'],
|
||||
props: ['visible', 'data', 'cData', 'index', 'stats', 'inTags', 'outTags', 'tlsConfigs'],
|
||||
emits: ['close', 'save'],
|
||||
data() {
|
||||
return {
|
||||
inbound: createInbound("direct",{ "tag": "" }),
|
||||
inData: <InData>{},
|
||||
title: "add",
|
||||
loading: false,
|
||||
side: "s",
|
||||
inTypes: InTypes,
|
||||
inboundStats: false,
|
||||
tls_id: { value: 0 },
|
||||
HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks],
|
||||
HasInData: [
|
||||
InTypes.SOCKS,
|
||||
InTypes.HTTP,
|
||||
InTypes.Shadowsocks,
|
||||
InTypes.VMess,
|
||||
InTypes.ShadowTLS,
|
||||
InTypes.Trojan,
|
||||
InTypes.Hysteria,
|
||||
InTypes.VLESS,
|
||||
InTypes.TUIC,
|
||||
InTypes.Hysteria2,
|
||||
InTypes.Naive,
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
updateData() {
|
||||
if (this.$props.id != -1) {
|
||||
if (this.$props.index != -1) {
|
||||
const newData = JSON.parse(this.$props.data)
|
||||
this.inbound = createInbound(newData.type, newData)
|
||||
this.tls_id.value = this.$props.tlsConfigs?.findLast((t:any) => t.inbounds?.includes(this.inbound.tag))?.id?? 0
|
||||
if (this.HasInData.includes(this.inbound.type)){
|
||||
this.inData = this.$props.cData?.length> 0 ? <InData>JSON.parse(this.$props.cData) : <InData>{id: 0, tag: this.inbound.tag, addrs: [], outJson: {}}
|
||||
} else {
|
||||
this.inData = <InData>{id: -1}
|
||||
}
|
||||
this.title = "edit"
|
||||
}
|
||||
else {
|
||||
const port = RandomUtil.randomIntRange(10000, 60000)
|
||||
this.inbound = createInbound("direct",{ tag: "direct-"+port ,listen: "::", listen_port: port })
|
||||
this.tls_id.value = 0
|
||||
if (this.HasInData.includes(this.inbound.type)){
|
||||
this.inData = <InData>{id: 0, tag: this.inbound.tag, addrs: [], outJson: {}}
|
||||
} else {
|
||||
this.inData = <InData>{id: -1}
|
||||
}
|
||||
this.title = "add"
|
||||
}
|
||||
this.inboundStats = this.$props.stats
|
||||
this.side = "s"
|
||||
},
|
||||
changeType() {
|
||||
// Tag change only in add outbound
|
||||
const tag = this.$props.id != -1 ? this.inbound.tag : this.inbound.type + "-" + this.inbound.listen_port
|
||||
const tag = this.$props.index != -1 ? this.inbound.tag : this.inbound.type + "-" + this.inbound.listen_port
|
||||
// Use previous data
|
||||
const prevConfig = { tag: tag ,listen: this.inbound.listen, listen_port: this.inbound.listen_port }
|
||||
this.inbound = createInbound(this.inbound.type, prevConfig)
|
||||
if (this.HasInData.includes(this.inbound.type)){
|
||||
this.inData = <InData>{id: 0, tag: this.inbound.tag, addrs: [], outJson: {}}
|
||||
} else {
|
||||
this.inData = <InData>{id: -1}
|
||||
}
|
||||
this.side = "s"
|
||||
},
|
||||
add_addr() {
|
||||
this.inData.addrs.push(<Addr>{ server: location.hostname, server_port: this.inbound.listen_port })
|
||||
},
|
||||
closeModal() {
|
||||
this.updateData() // reset
|
||||
@@ -115,7 +184,7 @@ export default {
|
||||
},
|
||||
saveChanges() {
|
||||
this.loading = true
|
||||
this.$emit('save', this.inbound, this.inboundStats, this.tls_id.value)
|
||||
this.$emit('save', this.inbound, this.inboundStats, this.tls_id.value, this.inData)
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
@@ -126,6 +195,10 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { Listen, InTls, Hysteria2, Naive, Direct, Shadowsocks, Users, Hysteria, ShadowTls, TProxy, Multiplex, Tuic, Transport }
|
||||
components: {
|
||||
Listen, InTls, Hysteria2, Naive, Direct, Shadowsocks,
|
||||
Users, Hysteria, ShadowTls, TProxy, Multiplex, Tuic, Transport,
|
||||
AddrVue, OutJsonVue
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -5,61 +5,84 @@
|
||||
{{ $t('actions.' + title) + " " + $t('objects.outbound') }}
|
||||
</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('type')"
|
||||
:items="Object.keys(outTypes).map((key,index) => ({title: key, value: Object.values(outTypes)[index]}))"
|
||||
v-model="outbound.type"
|
||||
@update:modelValue="changeType">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="outbound.tag" :label="$t('objects.tag')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="!NoServer.includes(outbound.type)">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.addr')"
|
||||
hide-details
|
||||
v-model="outbound.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.number="outbound.server_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Direct v-if="outbound.type == outTypes.Direct" direction="out" :data="outbound" />
|
||||
<Socks v-if="outbound.type == outTypes.SOCKS" :data="outbound" />
|
||||
<Http v-if="outbound.type == outTypes.HTTP" :data="outbound" />
|
||||
<Shadowsocks v-if="outbound.type == outTypes.Shadowsocks" direction="out" :data="outbound" />
|
||||
<Vmess v-if="outbound.type == outTypes.VMess" :data="outbound" />
|
||||
<Trojan v-if="outbound.type == outTypes.Trojan" :data="outbound" />
|
||||
<Wireguard v-if="outbound.type == outTypes.Wireguard" :data="outbound" />
|
||||
<Hysteria v-if="outbound.type == outTypes.Hysteria" direction="out" :data="outbound" />
|
||||
<ShadowTls v-if="outbound.type == outTypes.ShadowTLS" :data="outbound" />
|
||||
<Vless v-if="outbound.type == outTypes.VLESS" :data="outbound" />
|
||||
<Tuic v-if="outbound.type == outTypes.TUIC" direction="out" :data="outbound" />
|
||||
<Hysteria2 v-if="outbound.type == outTypes.Hysteria2" direction="out" :data="outbound" />
|
||||
<Tor v-if="outbound.type == outTypes.Tor" :data="outbound" />
|
||||
<Ssh v-if="outbound.type == outTypes.SSH" :data="outbound" />
|
||||
<Selector v-if="outbound.type == outTypes.Selector" :data="outbound" :tags="tags" />
|
||||
<UrlTest v-if="outbound.type == outTypes.URLTest" :data="outbound" :tags="tags" />
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-container style="padding: 0;">
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
align-tabs="center"
|
||||
>
|
||||
<v-tab value="t1">{{ $t('client.basics') }}</v-tab>
|
||||
<v-tab value="t2">{{ $t('client.external') }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="tab">
|
||||
<v-window-item value="t1">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('type')"
|
||||
:items="Object.keys(outTypes).map((key,index) => ({title: key, value: Object.values(outTypes)[index]}))"
|
||||
v-model="outbound.type"
|
||||
@update:modelValue="changeType">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field v-model="outbound.tag" :label="$t('objects.tag')" hide-details></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="!NoServer.includes(outbound.type)">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-text-field
|
||||
:label="$t('out.addr')"
|
||||
hide-details
|
||||
v-model="outbound.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.number="outbound.server_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<Direct v-if="outbound.type == outTypes.Direct" direction="out" :data="outbound" />
|
||||
<Socks v-if="outbound.type == outTypes.SOCKS" :data="outbound" />
|
||||
<Http v-if="outbound.type == outTypes.HTTP" :data="outbound" />
|
||||
<Shadowsocks v-if="outbound.type == outTypes.Shadowsocks" direction="out" :data="outbound" />
|
||||
<Vmess v-if="outbound.type == outTypes.VMess" :data="outbound" />
|
||||
<Trojan v-if="outbound.type == outTypes.Trojan" :data="outbound" />
|
||||
<Wireguard v-if="outbound.type == outTypes.Wireguard" :data="outbound" />
|
||||
<Hysteria v-if="outbound.type == outTypes.Hysteria" direction="out" :data="outbound" />
|
||||
<ShadowTls v-if="outbound.type == outTypes.ShadowTLS" :data="outbound" />
|
||||
<Vless v-if="outbound.type == outTypes.VLESS" :data="outbound" />
|
||||
<Tuic v-if="outbound.type == outTypes.TUIC" direction="out" :data="outbound" />
|
||||
<Hysteria2 v-if="outbound.type == outTypes.Hysteria2" direction="out" :data="outbound" />
|
||||
<Tor v-if="outbound.type == outTypes.Tor" :data="outbound" />
|
||||
<Ssh v-if="outbound.type == outTypes.SSH" :data="outbound" />
|
||||
<Selector v-if="outbound.type == outTypes.Selector" :data="outbound" :tags="tags" />
|
||||
<UrlTest v-if="outbound.type == outTypes.URLTest" :data="outbound" :tags="tags" />
|
||||
|
||||
<Transport v-if="Object.hasOwn(outbound,'transport')" :data="outbound" />
|
||||
<OutTLS v-if="Object.hasOwn(outbound,'tls')" :outbound="outbound" />
|
||||
<Multiplex v-if="Object.hasOwn(outbound,'multiplex')" direction="out" :data="outbound" />
|
||||
<Dial v-if="!NoDial.includes(outbound.type)" :dial="outbound" :outTags="tags" />
|
||||
<v-switch v-model="outboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
|
||||
<Transport v-if="Object.hasOwn(outbound,'transport')" :data="outbound" />
|
||||
<OutTLS v-if="Object.hasOwn(outbound,'tls')" :outbound="outbound" />
|
||||
<Multiplex v-if="Object.hasOwn(outbound,'multiplex')" direction="out" :data="outbound" />
|
||||
<Dial v-if="!NoDial.includes(outbound.type)" :dial="outbound" :outTags="tags" />
|
||||
<v-switch v-model="outboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
|
||||
</v-window-item>
|
||||
<v-window-item value="t2">
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-text-field v-model="link" :label="$t('client.external')" hide-details />
|
||||
</v-col>
|
||||
<v-col cols="12" align="center">
|
||||
<v-btn hide-details variant="tonal" :loading="loading" @click="linkConvert">{{ $t('submit') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
@@ -89,7 +112,7 @@ import RandomUtil from '@/plugins/randomUtil'
|
||||
import Dial from '@/components/Dial.vue'
|
||||
import Multiplex from '@/components/Multiplex.vue'
|
||||
import Transport from '@/components/Transport.vue'
|
||||
import OutTLS from '@/components/OutTLS.vue'
|
||||
import OutTLS from '@/components/tls/OutTLS.vue'
|
||||
import Direct from '@/components/protocols/Direct.vue'
|
||||
import Socks from '@/components/protocols/Socks.vue'
|
||||
import Http from '@/components/protocols/Http.vue'
|
||||
@@ -106,6 +129,7 @@ import Tor from '@/components/protocols/Tor.vue'
|
||||
import Ssh from '@/components/protocols/Ssh.vue'
|
||||
import Selector from '@/components/protocols/Selector.vue'
|
||||
import UrlTest from '@/components/protocols/UrlTest.vue'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
export default {
|
||||
props: ['visible', 'data', 'id', 'stats', 'tags'],
|
||||
emits: ['close', 'save'],
|
||||
@@ -113,6 +137,8 @@ export default {
|
||||
return {
|
||||
outbound: createOutbound("direct",{ "tag": "" }),
|
||||
title: "add",
|
||||
tab: "t1",
|
||||
link: "",
|
||||
loading: false,
|
||||
outTypes: OutTypes,
|
||||
outboundStats: false,
|
||||
@@ -131,6 +157,7 @@ export default {
|
||||
this.outbound = createOutbound("direct",{ tag: "direct-" + RandomUtil.randomSeq(3) })
|
||||
this.title = "add"
|
||||
}
|
||||
this.tab = "t1"
|
||||
this.outboundStats = this.$props.stats
|
||||
},
|
||||
changeType() {
|
||||
@@ -149,6 +176,18 @@ export default {
|
||||
this.$emit('save', this.outbound, this.outboundStats)
|
||||
this.loading = false
|
||||
},
|
||||
async linkConvert() {
|
||||
if (this.link.length>0){
|
||||
this.loading = true
|
||||
const msg = await HttpUtils.post('api/linkConvert', { link: this.link })
|
||||
this.loading = false
|
||||
if (msg.success) {
|
||||
this.outbound = msg.obj
|
||||
this.tab = "t1"
|
||||
this.link = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(newValue) {
|
||||
|
||||
@@ -9,19 +9,46 @@
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col style="text-align: center;" @click="copyToClipboard(clientSub)">
|
||||
<v-chip>{{ $t('setting.sub') }}</v-chip>
|
||||
<QrcodeVue :value="clientSub" :size="300" :margin="1" style="border-radius: 1rem;" />
|
||||
</v-col>
|
||||
</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 />
|
||||
<QrcodeVue :value="l.uri" :size="300" :margin="1" style="border-radius: 1rem;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-card-text style="overflow-y: auto; padding: 0">
|
||||
<v-tabs
|
||||
v-model="tab"
|
||||
density="compact"
|
||||
fixed-tabs
|
||||
align-tabs="center"
|
||||
>
|
||||
<v-tab value="sub">{{ $t('setting.sub') }}</v-tab>
|
||||
<v-tab value="link">{{ $t('client.links') }}</v-tab>
|
||||
</v-tabs>
|
||||
<v-window v-model="tab" style="margin-top: 10px;">
|
||||
<v-window-item value="sub">
|
||||
<v-row>
|
||||
<v-col style="text-align: center;">
|
||||
<v-chip>{{ $t('setting.sub') }}</v-chip><br />
|
||||
<QrcodeVue :value="clientSub" :size="size" @click="copyToClipboard(clientSub)" :margin="1" style="border-radius: 1rem;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col style="text-align: center;">
|
||||
<v-chip>{{ $t('setting.jsonSub') }}</v-chip><br />
|
||||
<QrcodeVue :value="clientSub + '?format=json'" :size="size" @click="copyToClipboard(clientSub + '?format=json')" :margin="1" style="border-radius: 1rem;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col style="text-align: center;">
|
||||
<v-chip>SING-BOX</v-chip><br />
|
||||
<QrcodeVue :value="singbox" :size="size" @click="copyToClipboard(singbox)" :margin="1" style="border-radius: .8rem;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
<v-window-item value="link">
|
||||
<v-row v-for="l in clientLinks">
|
||||
<v-col style="text-align: center;">
|
||||
<v-chip>{{ l.remark?? $t('client.' + l.type) }}</v-chip><br />
|
||||
<QrcodeVue :value="l.uri" :size="size" @click="copyToClipboard(l.uri)" :margin="1" style="border-radius: .5rem;" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
@@ -35,9 +62,10 @@ import { i18n } from '@/locales'
|
||||
import { push } from 'notivue'
|
||||
|
||||
export default {
|
||||
props: ['index'],
|
||||
props: ['index', 'visible'],
|
||||
data() {
|
||||
return {
|
||||
tab: "sub",
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -81,10 +109,26 @@ export default {
|
||||
clientSub() {
|
||||
return Data().subURI + this.client.name
|
||||
},
|
||||
singbox() {
|
||||
const url = Data().subURI + this.client.name + "?format=json"
|
||||
return "sing-box://import-remote-profile?url=" + encodeURIComponent(url) + "#" + this.client.name
|
||||
},
|
||||
clientLinks() {
|
||||
return this.client.links?? []
|
||||
},
|
||||
size() {
|
||||
if (window.innerWidth > 380) return 300
|
||||
if (window.innerWidth > 330) return 280
|
||||
return 250
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.tab = "sub"
|
||||
}
|
||||
},
|
||||
},
|
||||
components: { QrcodeVue }
|
||||
}
|
||||
</script>
|
||||
@@ -3,8 +3,8 @@
|
||||
<v-card class="rounded-lg" :loading="loading" color="background">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>
|
||||
{{ $t('stats.graphTitle') + " - " + $t('objects.' + resource) + " : " + tag }}
|
||||
<v-col cols="auto">
|
||||
{{ $t('stats.graphTitle') }}
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto"><v-icon icon="mdi-close" @click="$emit('close')"></v-icon></v-col>
|
||||
@@ -12,7 +12,13 @@
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text style="padding: 0 16px;">
|
||||
<v-container id="container">
|
||||
<div style="text-align: center; margin: 5px;">
|
||||
{{ $t('objects.' + resource) + " : " + tag }}
|
||||
</div>
|
||||
<v-radio-group v-model="limit" @change="loadData" density="compact" :loading="loading" inline hide-details>
|
||||
<v-radio v-for="p in periods" :label="p.title" :value="p.value"></v-radio>
|
||||
</v-radio-group>
|
||||
<v-container id="container" style="height:40vh;">
|
||||
<v-alert :text="$t('noData')" type="warning" variant="outlined" v-if="alert"></v-alert>
|
||||
<Line v-if="loaded" :data="usage" :options="<any>options" />
|
||||
</v-container>
|
||||
@@ -60,13 +66,29 @@ export default {
|
||||
loaded: false,
|
||||
alert: false,
|
||||
intervalId: <any>0,
|
||||
limit: 1,
|
||||
periods: [
|
||||
{ value: 1, title: i18n.global.n(1) + i18n.global.t('date.h')},
|
||||
{ value: 6, title: i18n.global.n(6) + i18n.global.t('date.h')},
|
||||
{ value: 12, title: i18n.global.n(12) + i18n.global.t('date.h')},
|
||||
{ value: 24, title: i18n.global.n(1) + i18n.global.t('date.d')},
|
||||
{ value: 48, title: i18n.global.n(2) + i18n.global.t('date.d')},
|
||||
{ value: 240, title: i18n.global.n(10) + i18n.global.t('date.d')},
|
||||
{ value: 480, title: i18n.global.n(20) + i18n.global.t('date.d')},
|
||||
{ value: 720, title: i18n.global.n(30) + i18n.global.t('date.d')},
|
||||
{ value: 1440, title: i18n.global.n(60) + i18n.global.t('date.d')},
|
||||
{ value: 2160, title: i18n.global.n(90) + i18n.global.t('date.d')},
|
||||
],
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
intersect: false,
|
||||
mode: 'index',
|
||||
},
|
||||
elements: {
|
||||
point: { pointStyle: 'crossRot' }
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
@@ -99,13 +121,13 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadData(limit: number) {
|
||||
async loadData() {
|
||||
this.loading = true
|
||||
const data = await HttpUtils.get('api/stats', { resource: this.resource, tag: this.tag, limit: limit })
|
||||
const data = await HttpUtils.get('api/stats', { resource: this.resource, tag: this.tag, limit: this.limit })
|
||||
if (data.success && data.obj) {
|
||||
const obj = <any[]>data.obj
|
||||
const l = String(i18n.global.locale) == 'fa' ? "fa-IR" : "en-US"
|
||||
const oneStep = limit * 3600 * 1000 / 360 // Each 10 sec
|
||||
const oneStep = this.limit * 3600 * 1000 / 360 // Each 10 sec
|
||||
const now = new Date().getTime()
|
||||
const steps = <number[]>[]
|
||||
for (let i = 360; i >= 0; i--) {
|
||||
@@ -145,8 +167,10 @@ export default {
|
||||
],
|
||||
}
|
||||
this.loaded = true
|
||||
this.alert = false
|
||||
} else {
|
||||
this.alert = true
|
||||
this.loaded = false
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
@@ -163,10 +187,10 @@ export default {
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
const limit = 1
|
||||
this.loadData(limit)
|
||||
this.limit = 1
|
||||
this.loadData()
|
||||
this.intervalId = setInterval(() => {
|
||||
this.loadData(limit)
|
||||
this.loadData()
|
||||
}, 10000)
|
||||
} else {
|
||||
this.loaded = false
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{{ $t('actions.' + title) + " " + $t('objects.tls') }}
|
||||
</v-card-title>
|
||||
<v-divider></v-divider>
|
||||
<v-card-text>
|
||||
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
|
||||
<v-card class="rounded-lg">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
@@ -90,6 +90,20 @@
|
||||
>{{ $t('tls.useText') }}</v-btn>
|
||||
</v-btn-toggle>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
icon="mdi-key-star"
|
||||
@click="genSelfSigned"
|
||||
:loading="loading">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ $t('actions.generate') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="usePath == 0">
|
||||
<v-col cols="12" sm="6">
|
||||
@@ -108,14 +122,14 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-else>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
:label="$t('tls.cert')"
|
||||
hide-details
|
||||
v-model="certText">
|
||||
</v-textarea>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6">
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
:label="$t('tls.key')"
|
||||
hide-details
|
||||
@@ -124,14 +138,6 @@
|
||||
</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>
|
||||
@@ -158,26 +164,42 @@
|
||||
v-model="server_port">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
variant="tonal"
|
||||
density="compact"
|
||||
icon="mdi-key-star"
|
||||
@click="genRealityKey"
|
||||
:loading="loading">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top">
|
||||
{{ $t('actions.generate') }}
|
||||
</v-tooltip>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-col cols="12">
|
||||
<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-col cols="12">
|
||||
<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-col cols="12">
|
||||
<v-text-field
|
||||
label="Short IDs"
|
||||
hide-details
|
||||
append-icon="mdi-refresh"
|
||||
@click:append="randomSID"
|
||||
v-model="short_id">
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
@@ -193,11 +215,21 @@
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
<v-row v-if="outTls.utls != undefined">
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
hide-details
|
||||
label="Fingerprint"
|
||||
:items="fingerprints"
|
||||
v-model="outTls.utls.fingerprint">
|
||||
</v-select>
|
||||
</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.options') }}</v-btn>
|
||||
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.options') }}</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-list>
|
||||
@@ -217,15 +249,15 @@
|
||||
<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-item>
|
||||
<v-switch v-model="optionFP" color="primary" label="UTLS" hide-details></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
@@ -259,8 +291,12 @@
|
||||
<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'
|
||||
import AcmeVue from '@/components/tls/Acme.vue'
|
||||
import EchVue from '@/components/tls/Ech.vue'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { push } from 'notivue'
|
||||
import { i18n } from '@/locales'
|
||||
import RandomUtil from '@/plugins/randomUtil'
|
||||
export default {
|
||||
props: ['visible', 'data', 'index'],
|
||||
emits: ['close', 'save'],
|
||||
@@ -334,7 +370,10 @@ export default {
|
||||
},
|
||||
changeTlsType(){
|
||||
if (this.tlsType) {
|
||||
this.tls.server = <iTls>{ enabled: true, reality: { enabled: true, handshake: { server_port: 443 } }, server_name: "" }
|
||||
this.tls.server = <iTls>{
|
||||
enabled: true,
|
||||
reality: { enabled: true, handshake: { server_port: 443 }, short_id: RandomUtil.randomShortId() },
|
||||
server_name: "" }
|
||||
this.tls.client = <oTls>{ reality: { public_key: "" } }
|
||||
} else {
|
||||
this.tls.server = <iTls>{ enabled: true }
|
||||
@@ -350,6 +389,75 @@ export default {
|
||||
this.$emit('save', this.tls)
|
||||
this.loading = false
|
||||
},
|
||||
async genSelfSigned(){
|
||||
this.loading = true
|
||||
const msg = await HttpUtils.get('api/keypairs', { k: "tls", o: this.inTls.server_name?? "''" })
|
||||
this.loading = false
|
||||
if (msg.success) {
|
||||
this.inTls.key_path=undefined
|
||||
this.inTls.certificate_path=undefined
|
||||
this.usePath = 1
|
||||
if (msg.obj.length>0){
|
||||
let privateKey = <string[]>[]
|
||||
let publicKey = <string[]>[]
|
||||
let isPrivateKey = false
|
||||
let isPublicKey = false
|
||||
|
||||
msg.obj.forEach((line:string) => {
|
||||
if (line === "-----BEGIN PRIVATE KEY-----") {
|
||||
isPrivateKey = true
|
||||
isPublicKey = false
|
||||
privateKey.push(line)
|
||||
} else if (line === "-----END PRIVATE KEY-----") {
|
||||
isPrivateKey = false
|
||||
privateKey.push(line)
|
||||
} else if (line === "-----BEGIN CERTIFICATE-----") {
|
||||
isPublicKey = true
|
||||
isPrivateKey = false
|
||||
publicKey.push(line)
|
||||
} else if (line === "-----END CERTIFICATE-----") {
|
||||
isPublicKey = false
|
||||
publicKey.push(line)
|
||||
} else if (isPrivateKey) {
|
||||
privateKey.push(line)
|
||||
} else if (isPublicKey) {
|
||||
publicKey.push(line)
|
||||
}
|
||||
})
|
||||
this.inTls.key = privateKey?? undefined
|
||||
this.inTls.certificate = publicKey?? undefined
|
||||
|
||||
} else {
|
||||
push.error({
|
||||
message: i18n.global.t('error') + ": " + msg.obj
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
async genRealityKey(){
|
||||
this.loading = true
|
||||
const msg = await HttpUtils.get('api/keypairs', { k: "reality" })
|
||||
this.loading = false
|
||||
if (msg.success) {
|
||||
msg.obj.forEach((line:string) => {
|
||||
if (this.inTls.reality && this.outTls.reality){
|
||||
if (line.startsWith("PrivateKey")){
|
||||
this.inTls.reality.private_key = line.substring(12)
|
||||
}
|
||||
if (line.startsWith("PublicKey")){
|
||||
this.outTls.reality.public_key = line.substring(11)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
push.error({
|
||||
message: i18n.global.t('error') + ": " + msg.obj
|
||||
})
|
||||
}
|
||||
},
|
||||
randomSID(){
|
||||
this.short_id = RandomUtil.randomShortId().join(',')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inTls(): iTls {
|
||||
|
||||
@@ -4,6 +4,9 @@ export default {
|
||||
failed: "failed",
|
||||
enable: "Enable",
|
||||
disable: "Disable",
|
||||
none: "None",
|
||||
all: "All",
|
||||
filter: "Filter",
|
||||
loading: "Loading...",
|
||||
confirm: "Are you sure ?",
|
||||
yes: "yes",
|
||||
@@ -24,6 +27,7 @@ export default {
|
||||
email: "Email",
|
||||
commaSeparated: "(comma separated)",
|
||||
count: "Count",
|
||||
template: "Template",
|
||||
error: {
|
||||
dplData: "Duplicate Data",
|
||||
core: "Sing-Box Error",
|
||||
@@ -87,13 +91,15 @@ export default {
|
||||
actions: {
|
||||
action: "Action",
|
||||
add: "Add",
|
||||
new: "Add",
|
||||
new: "New",
|
||||
edit: "Edit",
|
||||
del: "Delete",
|
||||
clone: "Clone",
|
||||
save: "Save",
|
||||
update: "Update",
|
||||
submit: "Submit",
|
||||
set: "Set",
|
||||
generate: "Generate",
|
||||
disable: "Disable",
|
||||
close: "Close",
|
||||
restartApp: "Restart App",
|
||||
@@ -139,6 +145,14 @@ export default {
|
||||
path: "Default Path",
|
||||
update: "Automatic Update Time",
|
||||
subUri: "Subscription URI",
|
||||
jsonSub: "JSON Subscription",
|
||||
toDirect: "Route to Direct",
|
||||
toBlock: "Route to Block",
|
||||
timestamp: "Timestamp",
|
||||
globalDns: "Global DNS",
|
||||
directDns: "Direct DNS",
|
||||
toDirectDns: "Route to Direct DNS",
|
||||
jsonSubOptions: "Other Options",
|
||||
},
|
||||
client: {
|
||||
name: "Name",
|
||||
@@ -223,6 +237,11 @@ export default {
|
||||
port: "Port",
|
||||
clients: "Enable Clients",
|
||||
ssMethod: "Method",
|
||||
sSide: "Server Side",
|
||||
cSide: "Client Side",
|
||||
multiDomain: "Multi Domain",
|
||||
remark: "Remark",
|
||||
mdOption: "Multi Domain Options",
|
||||
},
|
||||
listen: {
|
||||
sniffing: "Sniffing",
|
||||
@@ -312,6 +331,7 @@ export default {
|
||||
final: "Final",
|
||||
server: "Server",
|
||||
firstServer: "First Server",
|
||||
addrResolver: "Address Resolver",
|
||||
},
|
||||
routing: {
|
||||
title: "Routing",
|
||||
|
||||
@@ -4,6 +4,9 @@ export default {
|
||||
failed: "خطا",
|
||||
enable: "فعال",
|
||||
disable: "غیرفعال",
|
||||
none: "هیچ",
|
||||
all: "همه",
|
||||
filter: "فیلتر",
|
||||
loading: "در حال بارگذاری...",
|
||||
confirm: "آیا مطمئن هستید ؟",
|
||||
yes: "بله",
|
||||
@@ -24,6 +27,7 @@ export default {
|
||||
email: "ایمیل",
|
||||
commaSeparated: "(جداشده با کاما)",
|
||||
count: "تعداد",
|
||||
template: "الگو",
|
||||
error: {
|
||||
dplData: "داده تکراری",
|
||||
core: "خطا در سینگباکس",
|
||||
@@ -86,13 +90,15 @@ export default {
|
||||
actions: {
|
||||
action: "فرمان",
|
||||
add: "ایجاد",
|
||||
new: "ایجاد",
|
||||
new: "جدید",
|
||||
edit: "ویرایش",
|
||||
del: "حذف",
|
||||
clone: "شبیهسازی",
|
||||
save: "ذخیره",
|
||||
update: "بروزرسانی",
|
||||
submit: "ارسال",
|
||||
set: "تنظیم",
|
||||
generate: "تولید",
|
||||
disable: "غیرفعال",
|
||||
close: "بستن",
|
||||
restartApp: "ریستارت پنل",
|
||||
@@ -138,6 +144,14 @@ export default {
|
||||
path: "مسیر پیشفرض",
|
||||
update: "زمان بروزرسانی خودکار",
|
||||
subUri: "آدرس نهایی سابسکریپشن",
|
||||
jsonSub: "سابسکریپشن JSON",
|
||||
toDirect: "هدایت مستقیم",
|
||||
toBlock: "بستن مسیر",
|
||||
timestamp: "نمایش زمان",
|
||||
globalDns: "DNS کلی",
|
||||
directDns: "DNS مستقیم",
|
||||
toDirectDns: "هدایت به DNS مستقیم",
|
||||
jsonSubOptions: "گزینههای دیگر",
|
||||
},
|
||||
client: {
|
||||
name: "نام",
|
||||
@@ -222,6 +236,11 @@ export default {
|
||||
port: "پورت",
|
||||
clients: "فعالسازی کاربران",
|
||||
ssMethod: "روش",
|
||||
sSide: "سمت سرور",
|
||||
cSide: "سمت کاربر",
|
||||
multiDomain: "دامنه چندگانه",
|
||||
remark: "شرح",
|
||||
mdOption: "گزینههای دامنه چندگانه",
|
||||
},
|
||||
listen: {
|
||||
sniffing: "شنود آدرس",
|
||||
@@ -311,6 +330,7 @@ export default {
|
||||
final: "سرور نهایی",
|
||||
server: "سرور",
|
||||
firstServer: "سرور نخست",
|
||||
addrResolver: "حل کننده دامنه",
|
||||
},
|
||||
routing: {
|
||||
title: "مسیریابی",
|
||||
|
||||
@@ -13,8 +13,8 @@ export const i18n = createI18n({
|
||||
en: en,
|
||||
fa: fa,
|
||||
vi: vi,
|
||||
zhcn: zhcn,
|
||||
zhtw: zhtw
|
||||
zhHans: zhcn,
|
||||
zhHant: zhtw
|
||||
},
|
||||
})
|
||||
|
||||
@@ -22,6 +22,6 @@ export const languages = [
|
||||
{ title: 'English', value: 'en' },
|
||||
{ title: 'فارسی', value: 'fa' },
|
||||
{ title: 'Tiếng Việt', value: 'vi' },
|
||||
{ title: '简体中文', value: 'zhcn' },
|
||||
{ title: '繁體中文', value: 'zhtw' },
|
||||
{ title: '简体中文', value: 'zhHans' },
|
||||
{ title: '繁體中文', value: 'zhHant' },
|
||||
]
|
||||
|
||||
@@ -4,6 +4,9 @@ export default {
|
||||
failed: "Thất bại",
|
||||
enable: "Kích hoạt",
|
||||
disable: "Vô hiệu hóa",
|
||||
none: "Không",
|
||||
all: "Tất cả",
|
||||
filter: "Bộ lọc",
|
||||
loading: "Đang tải...",
|
||||
confirm: "Bạn chắc chắn chứ?",
|
||||
yes: "có",
|
||||
@@ -24,6 +27,7 @@ export default {
|
||||
email: "Email",
|
||||
commaSeparated: "(được phân tách bằng dấu phẩy)",
|
||||
count: "Đếm",
|
||||
template: "Mẫu",
|
||||
error: {
|
||||
dplData: "Dữ liệu trùng lặp",
|
||||
core: "Lỗi Sing-Box",
|
||||
@@ -87,13 +91,15 @@ export default {
|
||||
actions: {
|
||||
action: "Hành động",
|
||||
add: "Thêm",
|
||||
new: "Thêm",
|
||||
new: "Mới",
|
||||
edit: "Chỉnh sửa",
|
||||
del: "Xóa",
|
||||
clone: "Nhân bản",
|
||||
save: "Lưu",
|
||||
update: "Cập nhật",
|
||||
submit: "Gửi",
|
||||
set: "Đặt",
|
||||
generate: "Tạo ra",
|
||||
disable: "Vô hiệu hóa",
|
||||
close: "Đóng",
|
||||
restartApp: "Khởi động lại ứng dụng",
|
||||
@@ -139,6 +145,14 @@ export default {
|
||||
path: "Đường dẫn mặc định",
|
||||
update: "Thời gian cập nhật tự động",
|
||||
subUri: "URI đăng ký",
|
||||
jsonSub: "Đăng ký JSON",
|
||||
toDirect: "Chuyển hướng tới Trực tiếp",
|
||||
toBlock: "Chuyển hướng tới Chặn",
|
||||
timestamp: "Dấu thời gian",
|
||||
globalDns: "DNS Toàn cầu",
|
||||
directDns: "DNS Trực tiếp",
|
||||
toDirectDns: "Chuyển hướng tới DNS Trực tiếp",
|
||||
jsonSubOptions: "Tùy chọn Khác",
|
||||
},
|
||||
client: {
|
||||
name: "Tên",
|
||||
@@ -224,6 +238,11 @@ export default {
|
||||
sniffing: "Đang Sniffing",
|
||||
clients: "Kích hoạt khách hàng",
|
||||
ssMethod: "Phương thức",
|
||||
sSide: "Phía Máy chủ",
|
||||
cSide: "Phía Khách hàng",
|
||||
multiDomain: "Nhiều Tên miền",
|
||||
remark: "Ghi chú",
|
||||
mdOption: "Tùy chọn Nhiều Tên miền",
|
||||
},
|
||||
listen: {
|
||||
sniffing: "Đang Sniffing",
|
||||
@@ -313,6 +332,7 @@ export default {
|
||||
final: "Cuối cùng",
|
||||
server: "Máy chủ",
|
||||
firstServer: "Máy chủ Đầu tiên",
|
||||
addrResolver: "Trình phân giải địa chỉ",
|
||||
},
|
||||
routing: {
|
||||
title: "Định tuyến",
|
||||
|
||||
@@ -4,6 +4,9 @@ export default {
|
||||
failed: "失败",
|
||||
enable: "启用",
|
||||
disable: "禁用",
|
||||
none: "无",
|
||||
all: "全部",
|
||||
filter: "过滤器",
|
||||
loading: "加载中...",
|
||||
confirm: "是否确定?",
|
||||
yes: "确认",
|
||||
@@ -24,6 +27,7 @@ export default {
|
||||
email: "电子邮件",
|
||||
commaSeparated: "(逗号分隔)",
|
||||
count: "计数",
|
||||
template: "模板",
|
||||
error: {
|
||||
dplData: "重复数据",
|
||||
core: "Sing-Box 错误",
|
||||
@@ -74,26 +78,28 @@ export default {
|
||||
rule: "规则",
|
||||
user: "用户",
|
||||
tag: "标签",
|
||||
listen: "听",
|
||||
listen: "监听",
|
||||
dial: "拨号",
|
||||
tls: "TLS",
|
||||
multiplex: "多路复用",
|
||||
transport: "传输",
|
||||
method: "方法",
|
||||
headers: "标头",
|
||||
key: "钥匙",
|
||||
value: "价值",
|
||||
key: "键",
|
||||
value: "值",
|
||||
},
|
||||
actions: {
|
||||
action: "操作",
|
||||
add: "添加",
|
||||
new: "添加",
|
||||
new: "新建",
|
||||
edit: "编辑",
|
||||
del: "删除",
|
||||
clone: "克隆",
|
||||
save: "保存",
|
||||
update: "更新",
|
||||
submit: "提交",
|
||||
set: "设置",
|
||||
generate: "生成",
|
||||
disable: "禁用",
|
||||
close: "关闭",
|
||||
restartApp: "重启面板",
|
||||
@@ -126,19 +132,27 @@ export default {
|
||||
sub: "订阅",
|
||||
addr: "地址",
|
||||
port: "端口",
|
||||
webPath: "基本 URI",
|
||||
webPath: "面板路径",
|
||||
domain: "域名",
|
||||
sslKey: "SSL 密钥 (Key) 路径",
|
||||
sslCert: "SSL 证书 (cert) 路径",
|
||||
webUri: "面板 URI",
|
||||
sessionAge: "会话最大连接数",
|
||||
trafficAge: "流量最大年龄",
|
||||
sessionAge: "会话超时时限",
|
||||
trafficAge: "流量过期时限",
|
||||
timeLoc: "时区",
|
||||
subEncode: "启用编码",
|
||||
subEncode: "启用 Base64 编码",
|
||||
subInfo: "启用用户信息",
|
||||
path: "默认路径",
|
||||
update: "自动更新时间",
|
||||
subUri: "订阅 URL",
|
||||
subUri: "订阅 URI",
|
||||
jsonSub: "JSON 订阅",
|
||||
toDirect: "路由到直连",
|
||||
toBlock: "路由到阻止",
|
||||
timestamp: "时间戳",
|
||||
globalDns: "全局 DNS",
|
||||
directDns: "直连 DNS",
|
||||
toDirectDns: "路由到直连 DNS",
|
||||
jsonSubOptions: "其他选项",
|
||||
},
|
||||
client: {
|
||||
name: "名称",
|
||||
@@ -183,10 +197,10 @@ export default {
|
||||
tuic: {
|
||||
congControl: "拥塞控制",
|
||||
authTimeout: "认证超时",
|
||||
hb: "心跳",
|
||||
hb: "心跳包",
|
||||
},
|
||||
vless: {
|
||||
flow: "流量",
|
||||
flow: "流控",
|
||||
udpEnc: "UDP 数据包编码",
|
||||
},
|
||||
vmess: {
|
||||
@@ -224,22 +238,27 @@ export default {
|
||||
sniffing: "嗅探",
|
||||
clients: "启用客户端",
|
||||
ssMethod: "方法",
|
||||
sSide: "服务器端",
|
||||
cSide: "客户端",
|
||||
multiDomain: "多域名",
|
||||
remark: "备注",
|
||||
mdOption: "多域名选项",
|
||||
},
|
||||
listen: {
|
||||
sniffing: "嗅探",
|
||||
sniffingTimeout: "嗅探超时",
|
||||
sniffingOverride: "覆盖目的地",
|
||||
sniffingOverride: "覆盖目标地址",
|
||||
options: "监听选项",
|
||||
tcpOptions: "TCP选项",
|
||||
udpOptions: "UDP选项",
|
||||
detour: "绕道",
|
||||
tcpOptions: "TCP 选项",
|
||||
udpOptions: "UDP 选项",
|
||||
detour: "转发",
|
||||
detourText: "转发到入站",
|
||||
domainStrategy: "域名策略",
|
||||
domainStrategy: "域名解析策略",
|
||||
},
|
||||
dial: {
|
||||
bindIf: "绑定到网络接口",
|
||||
bindIp4: "绑定到IPv4",
|
||||
bindIp6: "绑定到IPv6",
|
||||
bindIp4: "绑定到 IPv4",
|
||||
bindIp6: "绑定到 IPv6",
|
||||
reuseAddr: "重用监听地址",
|
||||
connTimeout: "连接超时",
|
||||
fbTimeout: "回退超时",
|
||||
@@ -248,22 +267,22 @@ export default {
|
||||
},
|
||||
transport: {
|
||||
enable: "启用传输",
|
||||
host: "主机",
|
||||
hosts: "主机列表",
|
||||
path: "路径",
|
||||
httpMethod: "请求方法",
|
||||
host: "主机域名",
|
||||
hosts: "主机域名列表",
|
||||
path: "HTTP 请求路径",
|
||||
httpMethod: "HTTP 请求方法",
|
||||
idleTimeout: "空闲超时",
|
||||
pingTimeout: "Ping超时",
|
||||
grpcServiceName: "服务名称",
|
||||
grpcPws: "允许无流",
|
||||
pingTimeout: "Ping 超时",
|
||||
grpcServiceName: "gRPC 服务名称",
|
||||
grpcPws: "允许无流时保持连接",
|
||||
},
|
||||
mux: {
|
||||
enable: "启用多路复用",
|
||||
maxConn: "最大连接数",
|
||||
minStr: "最小流数",
|
||||
maxStr: "最大流数",
|
||||
padding: "仅填充",
|
||||
enableBrutal: "启用强力模式",
|
||||
padding: "仅允许填充连接",
|
||||
enableBrutal: "启用 TCP Brutal",
|
||||
},
|
||||
out: {
|
||||
addr: "服务器地址",
|
||||
@@ -274,22 +293,22 @@ export default {
|
||||
simple: "简单",
|
||||
logical: "逻辑",
|
||||
mode: "模式",
|
||||
invert: "反转",
|
||||
invert: "反选结果",
|
||||
ipVer: "IP 版本",
|
||||
domain: "域名",
|
||||
domainSufix: "域名后缀",
|
||||
domainKw: "域名关键词",
|
||||
domainRgx: "域名正则表达式",
|
||||
ip: "IP CIDR",
|
||||
privateIp: "无效 IP 范围",
|
||||
privateIp: "匹配非公开 IP",
|
||||
port: "端口",
|
||||
portRange: "端口范围",
|
||||
srcCidr: "源 IP CIDR",
|
||||
srcPrivateIp: "无效源 IP",
|
||||
srcPrivateIp: "匹配非公开源 IP",
|
||||
srcPort: "源端口",
|
||||
srcPortRange: "源端口范围",
|
||||
ruleset: "规则集",
|
||||
rulesetMatchSrc: "规则集 IP CIDR 匹配源",
|
||||
rulesetMatchSrc: "规则集 IP CIDR 匹配源 IP",
|
||||
options: "规则选项",
|
||||
domainRules: "域名/IP",
|
||||
srcIpRules: "源 IP",
|
||||
@@ -313,6 +332,7 @@ export default {
|
||||
final: "最终",
|
||||
server: "服务器",
|
||||
firstServer: "首选服务器",
|
||||
addrResolver: "地址解析器",
|
||||
},
|
||||
routing: {
|
||||
title: "路由",
|
||||
@@ -322,7 +342,7 @@ export default {
|
||||
autoBind: "自动绑定网卡",
|
||||
},
|
||||
exp: {
|
||||
storeFakeIp: "存储虚假 IP",
|
||||
storeFakeIp: "持久化 Fake-IP",
|
||||
},
|
||||
},
|
||||
tls : {
|
||||
@@ -339,7 +359,7 @@ export default {
|
||||
cs: "密码套件",
|
||||
privKey: "私钥",
|
||||
pubKey: "公钥",
|
||||
disableSni: "禁用SNI",
|
||||
disableSni: "禁用 SNI",
|
||||
insecure: "允许不安全",
|
||||
acme: {
|
||||
options: "ACME 选项",
|
||||
|
||||
@@ -5,6 +5,9 @@ export default {
|
||||
failed: "失敗",
|
||||
enable: "啟用",
|
||||
disable: "禁用",
|
||||
none: "無",
|
||||
all: "全部",
|
||||
filter: "過濾器",
|
||||
loading: "加載中...",
|
||||
confirm: "是否確定?",
|
||||
yes: "確認",
|
||||
@@ -25,6 +28,7 @@ export default {
|
||||
email: "電子郵件",
|
||||
commaSeparated: "(逗號分隔)",
|
||||
count: "計數",
|
||||
template: "模板",
|
||||
error: {
|
||||
dplData: "重複數據",
|
||||
core: "Sing-Box 錯誤",
|
||||
@@ -88,13 +92,15 @@ export default {
|
||||
actions: {
|
||||
action: "操作",
|
||||
add: "添加",
|
||||
new: "添加",
|
||||
new: "新建",
|
||||
edit: "編輯",
|
||||
del: "刪除",
|
||||
clone: "克隆",
|
||||
save: "保存",
|
||||
update: "更新",
|
||||
submit: "提交",
|
||||
set: "設置",
|
||||
generate: "生成",
|
||||
disable: "禁用",
|
||||
close: "關閉",
|
||||
restartApp: "重啟面板",
|
||||
@@ -140,6 +146,14 @@ export default {
|
||||
path: "默認路徑",
|
||||
update: "自動更新時間",
|
||||
subUri: "訂閱 URL",
|
||||
jsonSub: "JSON 訂閱",
|
||||
toDirect: "路由到直連",
|
||||
toBlock: "路由到阻止",
|
||||
timestamp: "時間戳",
|
||||
globalDns: "全局 DNS",
|
||||
directDns: "直連 DNS",
|
||||
toDirectDns: "路由到直連 DNS",
|
||||
jsonSubOptions: "其他選項",
|
||||
},
|
||||
client: {
|
||||
name: "名稱",
|
||||
@@ -225,6 +239,11 @@ export default {
|
||||
sniffing: "嗅探",
|
||||
clients: "啟用客戶端",
|
||||
ssMethod: "方法",
|
||||
sSide: "服務器端",
|
||||
cSide: "客戶端",
|
||||
multiDomain: "多域名",
|
||||
remark: "備註",
|
||||
mdOption: "多域名選項",
|
||||
},
|
||||
listen: {
|
||||
sniffing: "嗅探",
|
||||
@@ -314,6 +333,7 @@ export default {
|
||||
final: "最終",
|
||||
server: "服務器",
|
||||
firstServer: "首選服務器",
|
||||
addrResolver: "地址解析器",
|
||||
},
|
||||
routing: {
|
||||
title: "路由",
|
||||
|
||||
@@ -28,10 +28,10 @@ import { createNotivue } from 'notivue'
|
||||
import 'notivue/notification.css'
|
||||
import 'notivue/animations.css'
|
||||
const notivue = createNotivue({
|
||||
position: 'top-center',
|
||||
position: 'bottom-center',
|
||||
limit: 4,
|
||||
enqueue: false,
|
||||
avoidDuplicates: false,
|
||||
avoidDuplicates: true,
|
||||
notifications: {
|
||||
global: {
|
||||
duration: 3000
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export interface Addr {
|
||||
server: string
|
||||
server_port: number
|
||||
tls?: boolean
|
||||
insecure?: boolean
|
||||
server_name?: string
|
||||
remark?: string
|
||||
}
|
||||
|
||||
export interface InData {
|
||||
id: number
|
||||
tag: string
|
||||
addrs: Addr[]
|
||||
outJson: any
|
||||
}
|
||||
+290
-76
@@ -1,5 +1,6 @@
|
||||
import { Hysteria, Hysteria2, InTypes, Inbound, Naive, Shadowsocks, TUIC, Trojan, VLESS, VMess } from "@/types/inbounds"
|
||||
import { HTTP, WebSocket, gRPC, HTTPUpgrade, Transport, TrspTypes } from "@/types/transport";
|
||||
import { HTTP, WebSocket, gRPC, HTTPUpgrade, Transport, TrspTypes } from "@/types/transport"
|
||||
import RandomUtil from "./randomUtil"
|
||||
|
||||
export interface Link {
|
||||
type: "local" | "external" | "sub"
|
||||
@@ -13,49 +14,63 @@ function utf8ToBase64(utf8String: string): string {
|
||||
}
|
||||
|
||||
export namespace LinkUtil {
|
||||
export function linkGenerator(user: string, inbound: Inbound, tlsClient: any = null): string {
|
||||
const addr = location.hostname
|
||||
export function linkGenerator(user: string, inbound: Inbound, tlsClient: any = {}, addrs: any[] = []): string[] {
|
||||
switch(inbound.type){
|
||||
case InTypes.Shadowsocks:
|
||||
return shadowsocksLink(user,<Shadowsocks>inbound, addr)
|
||||
return shadowsocksLink(user,<Shadowsocks>inbound, addrs)
|
||||
case InTypes.Naive:
|
||||
return naiveLink(user,<Naive>inbound, addr, tlsClient)
|
||||
return naiveLink(user,<Naive>inbound, addrs, tlsClient)
|
||||
case InTypes.Hysteria:
|
||||
return hysteriaLink(user,<Hysteria>inbound, addr, tlsClient)
|
||||
return hysteriaLink(user,<Hysteria>inbound, addrs, tlsClient)
|
||||
case InTypes.Hysteria2:
|
||||
return hysteria2Link(user,<Hysteria2>inbound, addr, tlsClient)
|
||||
return hysteria2Link(user,<Hysteria2>inbound, addrs, tlsClient)
|
||||
case InTypes.TUIC:
|
||||
return tuicLink(user,<TUIC>inbound, addr, tlsClient)
|
||||
return tuicLink(user,<TUIC>inbound, addrs, tlsClient)
|
||||
case InTypes.VLESS:
|
||||
return vlessLink(user,<VLESS>inbound, addr, tlsClient)
|
||||
return vlessLink(user,<VLESS>inbound, addrs, tlsClient)
|
||||
case InTypes.Trojan:
|
||||
return trojanLink(user,<Trojan>inbound, addr, tlsClient)
|
||||
return trojanLink(user,<Trojan>inbound, addrs, tlsClient)
|
||||
case InTypes.VMess:
|
||||
return vmessLink(user,<VMess>inbound, addr, tlsClient)
|
||||
return vmessLink(user,<VMess>inbound, addrs, tlsClient)
|
||||
}
|
||||
return ''
|
||||
return []
|
||||
}
|
||||
|
||||
function shadowsocksLink(user: string, inbound: Shadowsocks, addr: string): string {
|
||||
function shadowsocksLink(user: string, inbound: Shadowsocks, addrs: any[]): string[] {
|
||||
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
|
||||
}
|
||||
|
||||
const uri = new URL(`ss://${utf8ToBase64(inbound.method + ':' + password.join(':'))}@${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
return uri.toString()
|
||||
|
||||
let links = <string[]>[]
|
||||
if (addrs.length == 0) {
|
||||
const uri = new URL(`ss://${utf8ToBase64(inbound.method + ':' + password.join(':'))}@${location.hostname}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
links.push(uri.toString())
|
||||
} else {
|
||||
addrs.forEach(a => {
|
||||
const uri = new URL(`ss://${utf8ToBase64(inbound.method + ':' + password.join(':'))}@${a.server}:${a.server_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
|
||||
links.push(uri.toString())
|
||||
})
|
||||
}
|
||||
return links
|
||||
}
|
||||
|
||||
function hysteriaLink(user: string, inbound: Hysteria, addr: string, tlsClient: any): string {
|
||||
function hysteriaLink(user: string, inbound: Hysteria, addrs: any[], tlsClient: any): string[] {
|
||||
const auth = inbound.users.find(i => i.name == user)?.auth_str
|
||||
const params = {
|
||||
upmbps: inbound.up_mbps?? null,
|
||||
@@ -67,17 +82,43 @@ export namespace LinkUtil {
|
||||
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)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
|
||||
let links = <string[]>[]
|
||||
if (addrs.length == 0) {
|
||||
const uri = new URL(`hysteria://${location.hostname}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
links.push(uri.toString())
|
||||
} else {
|
||||
addrs.forEach(a => {
|
||||
const uri = new URL(`hysteria://${a.server}:${a.server_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
if (a.server_name?.length>0) {
|
||||
uri.searchParams.set('peer', a.server_name)
|
||||
} else {
|
||||
inbound.tls.server_name ? uri.searchParams.set('peer', inbound.tls.server_name) : uri.searchParams.delete('peer')
|
||||
}
|
||||
if (a.insecure) {
|
||||
uri.searchParams.set('insecure', '1')
|
||||
} else {
|
||||
tlsClient.insecure ? uri.searchParams.set('insecure', '1') : uri.searchParams.delete('insecure')
|
||||
}
|
||||
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
|
||||
links.push(uri.toString())
|
||||
})
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
return uri.toString()
|
||||
return links
|
||||
}
|
||||
|
||||
function hysteria2Link(user: string, inbound: Hysteria2, addr: string, tlsClient: any): string {
|
||||
function hysteria2Link(user: string, inbound: Hysteria2, addrs: any[], tlsClient: any): string[] {
|
||||
const password = inbound.users.find(i => i.name == user)?.password
|
||||
const params = {
|
||||
upmbps: inbound.up_mbps?? null,
|
||||
@@ -89,36 +130,85 @@ export namespace LinkUtil {
|
||||
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)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
|
||||
let links = <string[]>[]
|
||||
if (addrs.length == 0) {
|
||||
const uri = new URL(`hysteria2://${password}@${location.hostname}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
links.push(uri.toString())
|
||||
} else {
|
||||
addrs.forEach(a => {
|
||||
const uri = new URL(`hysteria2://${password}@${a.server}:${a.server_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
if (a.server_name?.length>0) {
|
||||
uri.searchParams.set('sni', a.server_name)
|
||||
} else {
|
||||
inbound.tls.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
|
||||
}
|
||||
if (a.insecure) {
|
||||
uri.searchParams.set('insecure', '1')
|
||||
} else {
|
||||
tlsClient.insecure ? uri.searchParams.set('insecure', '1') : uri.searchParams.delete('insecure')
|
||||
}
|
||||
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
|
||||
links.push(uri.toString())
|
||||
})
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
return uri.toString()
|
||||
return links
|
||||
}
|
||||
|
||||
function naiveLink(user: string, inbound: Naive, addr: string, tlsClient: any): string {
|
||||
function naiveLink(user: string, inbound: Naive, addrs: any[], 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,
|
||||
allowInsecure: tlsClient?.insecure ? 1 : null
|
||||
}
|
||||
const uri = `http2://${utf8ToBase64(user + ":" + password + "@" + addr + ":" + inbound.listen_port)}`
|
||||
const paramsArray = []
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
paramsArray.push(`${key}=${encodeURIComponent(value.toString())}`)
|
||||
|
||||
let links = <string[]>[]
|
||||
if (addrs.length == 0) {
|
||||
const params = {
|
||||
padding: 1,
|
||||
peer: inbound.tls.server_name?? null,
|
||||
alpn: inbound.tls.alpn?.join(',')?? null,
|
||||
tfo: inbound.tcp_fast_open? 1 : 0,
|
||||
allowInsecure: tlsClient?.insecure ? 1 : null
|
||||
}
|
||||
const uri = `http2://${utf8ToBase64(user + ":" + password + "@" + location.hostname + ":" + inbound.listen_port)}`
|
||||
const paramsArray = []
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
paramsArray.push(`${key}=${encodeURIComponent(value.toString())}`)
|
||||
}
|
||||
}
|
||||
links.push(uri.toString() + "?" + paramsArray.join('&') + "#" + inbound.tag)
|
||||
} else {
|
||||
addrs.forEach(a => {
|
||||
const params = {
|
||||
padding: 1,
|
||||
peer: a.server_name?.length>0 ? a.server_name : inbound.tls.server_name?? null,
|
||||
alpn: inbound.tls.alpn?.join(',')?? null,
|
||||
tfo: inbound.tcp_fast_open? 1 : 0,
|
||||
allowInsecure: a.insecure ? 1 : tlsClient?.insecure ? 1 : null
|
||||
}
|
||||
const uri = `http2://${utf8ToBase64(user + ":" + password + "@" + a.server + ":" + a.server_port)}`
|
||||
const paramsArray = []
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
paramsArray.push(`${key}=${encodeURIComponent(value.toString())}`)
|
||||
}
|
||||
}
|
||||
links.push(uri.toString() + "?" + paramsArray.join('&') + "#" + encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag))
|
||||
})
|
||||
}
|
||||
return uri.toString() + "?" + paramsArray.join('&') + "#" + inbound.tag
|
||||
return links
|
||||
}
|
||||
|
||||
function tuicLink(user: string, inbound: TUIC, addr: string, tlsClient: any): string {
|
||||
function tuicLink(user: string, inbound: TUIC, addrs: any[], tlsClient: any): string[] {
|
||||
const u = inbound.users.find(i => i.name == user)
|
||||
const params = {
|
||||
sni: inbound.tls.server_name?? null,
|
||||
@@ -127,14 +217,40 @@ export namespace LinkUtil {
|
||||
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)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
|
||||
let links = <string[]>[]
|
||||
if (addrs.length == 0) {
|
||||
const uri = new URL(`tuic://${u?.uuid}:${u?.password}@${location.hostname}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
links.push(uri.toString())
|
||||
} else {
|
||||
addrs.forEach(a => {
|
||||
const uri = new URL(`tuic://${u?.uuid}:${u?.password}@${a.server}:${a.server_port}`)
|
||||
for (const [key, value] of Object.entries(params)){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
if (a.server_name?.length>0) {
|
||||
uri.searchParams.set('sni', a.server_name)
|
||||
} else {
|
||||
inbound.tls.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
|
||||
}
|
||||
if (a.insecure) {
|
||||
uri.searchParams.set('allowInsecure', '1')
|
||||
} else {
|
||||
tlsClient.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
|
||||
}
|
||||
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
|
||||
links.push(uri.toString())
|
||||
})
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
return uri.toString()
|
||||
return links
|
||||
}
|
||||
|
||||
function getTransportParams(t:Transport): any {
|
||||
@@ -170,7 +286,7 @@ export namespace LinkUtil {
|
||||
return params
|
||||
}
|
||||
|
||||
function vlessLink(user: string, inbound: VLESS, addr: string, tlsClient: any): string {
|
||||
function vlessLink(user: string, inbound: VLESS, addrs: any[], tlsClient: any): string[] {
|
||||
const u = inbound.users.find(i => i.name == user)
|
||||
const transport = <Transport>inbound.transport
|
||||
|
||||
@@ -185,19 +301,54 @@ export namespace LinkUtil {
|
||||
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
|
||||
sid: inbound.tls?.reality?.enabled ? (inbound.tls?.reality?.short_id?.length>0 ? inbound.tls.reality.short_id[RandomUtil.randomInt(inbound.tls.reality.short_id.length)] : null) : null
|
||||
}
|
||||
const uri = new URL(`vless://${u?.uuid}@${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries({...params, ...tParams})){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
let links = <string[]>[]
|
||||
if (addrs.length == 0) {
|
||||
const uri = new URL(`vless://${u?.uuid}@${location.hostname}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries({...params, ...tParams})){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
links.push(uri.toString())
|
||||
} else {
|
||||
addrs.forEach(a => {
|
||||
const uri = new URL(`vless://${u?.uuid}@${a.server}:${a.server_port}`)
|
||||
for (const [key, value] of Object.entries({...params, ...tParams})){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
if (a.tls != undefined){
|
||||
if (a.tls) {
|
||||
uri.searchParams.set('security','tls')
|
||||
} else {
|
||||
uri.searchParams.delete('security')
|
||||
uri.searchParams.delete('sni')
|
||||
uri.searchParams.delete('alpn')
|
||||
uri.searchParams.delete('allowInsecure')
|
||||
}
|
||||
}
|
||||
if (a.server_name?.length>0) {
|
||||
uri.searchParams.set('sni', a.server_name)
|
||||
} else {
|
||||
inbound.tls?.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
|
||||
}
|
||||
if (a.insecure) {
|
||||
uri.searchParams.set('allowInsecure', '1')
|
||||
} else {
|
||||
tlsClient.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
|
||||
}
|
||||
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
|
||||
links.push(uri.toString())
|
||||
})
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
return uri.toString()
|
||||
return links
|
||||
}
|
||||
|
||||
function trojanLink(user: string, inbound: Trojan, addr: string, tlsClient: any): string {
|
||||
function trojanLink(user: string, inbound: Trojan, addrs: any[], tlsClient: any): string[] {
|
||||
const u = inbound.users.find(i => i.name == user)
|
||||
const transport = <Transport>inbound.transport
|
||||
|
||||
@@ -211,19 +362,55 @@ export namespace LinkUtil {
|
||||
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
|
||||
sid: inbound.tls?.reality?.enabled ? (inbound.tls?.reality?.short_id?.length>0 ? inbound.tls.reality.short_id[RandomUtil.randomInt(inbound.tls.reality.short_id.length)] : null) : null
|
||||
}
|
||||
const uri = new URL(`trojan://${u?.password}@${addr}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries({...params, ...tParams})){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
|
||||
let links = <string[]>[]
|
||||
if (addrs.length == 0) {
|
||||
const uri = new URL(`trojan://${u?.password}@${location.hostname}:${inbound.listen_port}`)
|
||||
for (const [key, value] of Object.entries({...params, ...tParams})){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
links.push(uri.toString())
|
||||
} else {
|
||||
addrs.forEach(a => {
|
||||
const uri = new URL(`trojan://${u?.password}@${a.server}:${a.server_port}`)
|
||||
for (const [key, value] of Object.entries({...params, ...tParams})){
|
||||
if (value) {
|
||||
uri.searchParams.set(key, value.toString())
|
||||
}
|
||||
}
|
||||
if (a.tls != undefined){
|
||||
if (a.tls) {
|
||||
uri.searchParams.set('security','tls')
|
||||
} else {
|
||||
uri.searchParams.delete('security')
|
||||
uri.searchParams.delete('sni')
|
||||
uri.searchParams.delete('alpn')
|
||||
uri.searchParams.delete('allowInsecure')
|
||||
}
|
||||
}
|
||||
if (a.server_name?.length>0) {
|
||||
uri.searchParams.set('sni', a.server_name)
|
||||
} else {
|
||||
inbound.tls?.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
|
||||
}
|
||||
if (a.insecure) {
|
||||
uri.searchParams.set('allowInsecure', '1')
|
||||
} else {
|
||||
tlsClient.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
|
||||
}
|
||||
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
|
||||
links.push(uri.toString())
|
||||
})
|
||||
}
|
||||
uri.hash = encodeURIComponent(inbound.tag)
|
||||
return uri.toString()
|
||||
return links
|
||||
}
|
||||
|
||||
function vmessLink(user: string, inbound: VMess, addr: string, tlsClient: any): string {
|
||||
function vmessLink(user: string, inbound: VMess, addrs: any[], tlsClient: any): string[] {
|
||||
const u = inbound.users.find(i => i.name == user)
|
||||
const transport = <Transport>inbound.transport
|
||||
|
||||
@@ -232,7 +419,7 @@ export namespace LinkUtil {
|
||||
|
||||
const params = {
|
||||
v: 2,
|
||||
add: addr,
|
||||
add: location.hostname,
|
||||
aid: u?.alterId,
|
||||
host: tParams.host?? undefined,
|
||||
id: u?.uuid,
|
||||
@@ -245,6 +432,33 @@ export namespace LinkUtil {
|
||||
tls: Object.keys(inbound.tls).length>0? 'tls' : 'none',
|
||||
allowInsecure: tlsClient?.insecure ? 1 : undefined
|
||||
}
|
||||
return 'vmess://' + utf8ToBase64(JSON.stringify(params, null, 2))
|
||||
let links = <string[]>[]
|
||||
if (addrs.length == 0) {
|
||||
links.push('vmess://' + utf8ToBase64(JSON.stringify(params, null, 2)))
|
||||
} else {
|
||||
addrs.forEach(a => {
|
||||
let newParams = {...params}
|
||||
newParams.add = a.server
|
||||
newParams.port = a.server_port
|
||||
if (a.tls != undefined){
|
||||
if (a.tls) {
|
||||
newParams.tls = 'tls'
|
||||
} else {
|
||||
newParams.tls = 'none'
|
||||
delete newParams.sni
|
||||
delete newParams.allowInsecure
|
||||
}
|
||||
}
|
||||
if (a.server_name?.length>0) {
|
||||
newParams.sni = a.server_name
|
||||
}
|
||||
if (a.insecure) {
|
||||
newParams.allowInsecure = 1
|
||||
}
|
||||
newParams.ps = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
|
||||
links.push('vmess://' + utf8ToBase64(JSON.stringify(newParams, null, 2)))
|
||||
})
|
||||
}
|
||||
return links
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Hysteria, Hysteria2, Inbound, InTypes, Shadowsocks, Trojan, TUIC, VLESS, VMess, ShadowTLS } from "@/types/inbounds"
|
||||
import { iTls } from "@/types/inTls"
|
||||
import { oTls } from "@/types/outTls"
|
||||
|
||||
export function fillData(out: any, inbound: Inbound, tlsClient: any) {
|
||||
if (Object.hasOwn(inbound, 'tls')) {
|
||||
const inb = <any>inbound
|
||||
addTls(out,inb.tls,tlsClient)
|
||||
} else {
|
||||
delete out.tls
|
||||
}
|
||||
out.type = inbound.type
|
||||
out.tag = inbound.tag
|
||||
out.server = location.hostname
|
||||
out.server_port = inbound.listen_port
|
||||
switch(inbound.type){
|
||||
case InTypes.HTTP || InTypes.SOCKS:
|
||||
return
|
||||
case InTypes.Shadowsocks:
|
||||
shadowsocksOut(out, <Shadowsocks>inbound)
|
||||
return
|
||||
case InTypes.ShadowTLS:
|
||||
shadowTlsOut(out, <ShadowTLS>inbound)
|
||||
return
|
||||
case InTypes.Hysteria:
|
||||
hysteriaOut(out, <Hysteria>inbound)
|
||||
return
|
||||
case InTypes.Hysteria2:
|
||||
hysteria2Out(out, <Hysteria2>inbound)
|
||||
return
|
||||
case InTypes.TUIC:
|
||||
tuicOut(out, <TUIC>inbound)
|
||||
return
|
||||
case InTypes.VLESS:
|
||||
vlessOut(out, <VLESS>inbound)
|
||||
return
|
||||
case InTypes.Trojan:
|
||||
trojanOut(out, <Trojan>inbound)
|
||||
return
|
||||
case InTypes.VMess:
|
||||
vmessOut(out, <VMess>inbound)
|
||||
return
|
||||
}
|
||||
Object.keys(out).forEach(key => delete out[key])
|
||||
}
|
||||
|
||||
function addTls(out: any, tls: iTls, tlsClient: oTls){
|
||||
out.tls = tlsClient
|
||||
if(tls.enabled) out.tls.enabled = tls.enabled
|
||||
if(tls.server_name) out.tls.server_name = tls.server_name
|
||||
if(tls.alpn) out.tls.alpn = tls.alpn
|
||||
if(tls.min_version) out.tls.min_version = tls.min_version
|
||||
if(tls.max_version) out.tls.max_version = tls.max_version
|
||||
if(tls.cipher_suites) out.tls.cipher_suites = tls.cipher_suites
|
||||
}
|
||||
|
||||
function shadowsocksOut(out: any, inbound: Shadowsocks) {
|
||||
out.method = inbound.method
|
||||
out.multiplex = inbound.multiplex
|
||||
}
|
||||
|
||||
function shadowTlsOut(out: any, inbound: ShadowTLS) {
|
||||
if (inbound.version == 3) {
|
||||
out.version = 3
|
||||
} else {
|
||||
Object.keys(out).forEach(key => delete out[key])
|
||||
}
|
||||
out.tls = { enabled: true }
|
||||
}
|
||||
|
||||
function hysteriaOut(out: any, inbound: Hysteria) {
|
||||
out.up_mbps = inbound.down_mbps
|
||||
out.down_mbps = inbound.up_mbps
|
||||
out.obfs = inbound.obfs
|
||||
out.recv_window_conn = inbound.recv_window_conn
|
||||
out.disable_mtu_discovery = inbound.disable_mtu_discovery
|
||||
}
|
||||
|
||||
function hysteria2Out(out: any, inbound: Hysteria2) {
|
||||
out.up_mbps = inbound.down_mbps
|
||||
out.down_mbps = inbound.up_mbps
|
||||
out.obfs = inbound.obfs
|
||||
}
|
||||
|
||||
function tuicOut(out: any, inbound: TUIC) {
|
||||
out.congestion_control = inbound.congestion_control?? "cubic"
|
||||
out.zero_rtt_handshake = inbound.zero_rtt_handshake
|
||||
out.heartbeat = inbound.heartbeat
|
||||
}
|
||||
|
||||
function vlessOut(out: any, inbound: VLESS) {
|
||||
out.multiplex = inbound.multiplex
|
||||
out.transport = inbound.transport
|
||||
}
|
||||
|
||||
function trojanOut(out: any, inbound: Trojan) {
|
||||
out.multiplex = inbound.multiplex
|
||||
out.transport = inbound.transport
|
||||
}
|
||||
|
||||
function vmessOut(out: any, inbound: VMess) {
|
||||
out.multiplex = inbound.multiplex
|
||||
out.transport = inbound.transport
|
||||
}
|
||||
@@ -35,8 +35,8 @@ const RandomUtil = {
|
||||
return btoa(String.fromCharCode(...array))
|
||||
},
|
||||
randomShortId(): string[] {
|
||||
let shortIds = ['','','','']
|
||||
for (var ii = 0; ii < 4; ii++) {
|
||||
let shortIds = new Array(24).fill('')
|
||||
for (var ii = 0; ii < 24; ii++) {
|
||||
for (var jj = 0; jj < this.randomInt(8); jj++){
|
||||
let randomNum = this.randomInt(256)
|
||||
shortIds[ii] += ('0' + randomNum.toString(16)).slice(-2)
|
||||
|
||||
@@ -37,11 +37,11 @@ export const FindDiff = {
|
||||
|
||||
return differences
|
||||
},
|
||||
Clients(value1: any[], value2: any[]): any {
|
||||
ArrObj(value1: any[], value2: any[], key: string): any {
|
||||
const differences: any[] = []
|
||||
value1.forEach((v1,index) => {
|
||||
if(index >= value2.length) differences.push({key: "clients", action: "new", obj: v1})
|
||||
else if(!this.deepCompare(v1,value2[index])) differences.push({key: "clients", action: "edit", obj: v1})
|
||||
if(index >= value2.length) differences.push({key: key, action: "new", obj: v1})
|
||||
else if(!this.deepCompare(v1,value2[index])) differences.push({key: key, action: "edit", obj: v1})
|
||||
})
|
||||
return differences
|
||||
},
|
||||
@@ -76,8 +76,8 @@ export const FindDiff = {
|
||||
|
||||
// Check if both objects are plain objects
|
||||
if (typeof obj1 === 'object' && typeof obj2 === 'object' && obj1 !== null && obj2 !== null) {
|
||||
const keys1 = Object.keys(obj1)
|
||||
const keys2 = Object.keys(obj2)
|
||||
const keys1 = Object.keys(obj1).filter(key => obj1[key] !== undefined)
|
||||
const keys2 = Object.keys(obj2).filter(key => obj2[key] !== undefined)
|
||||
|
||||
if (keys1.length !== keys2.length) {
|
||||
return false
|
||||
|
||||
@@ -9,7 +9,7 @@ import '@mdi/font/css/materialdesignicons.css'
|
||||
import 'vuetify/styles'
|
||||
|
||||
import colors from 'vuetify/util/colors'
|
||||
import { fa, en, vi, zhHans as zhcn, zhHant as zhtw } from 'vuetify/locale'
|
||||
import { fa, en, vi, zhHans, zhHant } 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, vi, zhcn, zhtw },
|
||||
messages: { en, fa, vi, zhHans, zhHant },
|
||||
},
|
||||
})
|
||||
|
||||
@@ -10,10 +10,11 @@ const Data = defineStore('Data', {
|
||||
reloadItems: localStorage.getItem("reloadItems")?.split(',')?? <string[]>[],
|
||||
subURI: "",
|
||||
onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]},
|
||||
oldData: <{config: any, clients: any[], tlsConfigs: any[]}>{},
|
||||
config: {},
|
||||
oldData: <{config: any, clients: any[], tlsConfigs: any[], inData: any[]}>{},
|
||||
config: <any>{},
|
||||
clients: [],
|
||||
tlsConfigs: [],
|
||||
inData: [],
|
||||
}),
|
||||
actions: {
|
||||
async loadData() {
|
||||
@@ -25,6 +26,7 @@ const Data = defineStore('Data', {
|
||||
if (msg.obj.config) this.oldData.config = msg.obj.config
|
||||
if (msg.obj.clients) this.oldData.clients = msg.obj.clients
|
||||
if (msg.obj.tls) this.oldData.tlsConfigs = msg.obj.tls
|
||||
if (msg.obj.inData) this.oldData.inData = msg.obj.inData
|
||||
this.onlines = msg.obj.onlines
|
||||
if (msg.obj.lastLog) {
|
||||
push.error({
|
||||
@@ -41,31 +43,51 @@ const Data = defineStore('Data', {
|
||||
if (data.config) this.config = data.config
|
||||
if (data.clients) this.clients = data.clients
|
||||
if (data.tls) this.tlsConfigs = data.tls
|
||||
if (data.inData) this.inData = data.inData
|
||||
}
|
||||
}
|
||||
},
|
||||
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)),
|
||||
config: JSON.stringify(FindDiff.Config(this.config,this.oldData.config), null, 2),
|
||||
clients: JSON.stringify(FindDiff.ArrObj(this.clients,this.oldData.clients, "clients"), null, 2),
|
||||
tls: JSON.stringify(FindDiff.ArrObj(this.tlsConfigs,this.oldData.tlsConfigs, "tls"), null, 2),
|
||||
inData: JSON.stringify(FindDiff.ArrObj(this.inData,this.oldData.inData, "inData"), null, 2),
|
||||
}
|
||||
const msg = await HttpUtils.post('api/save',diff)
|
||||
if(msg.success) {
|
||||
this.lastLoad = 0
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
async delInbound(index: number) {
|
||||
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)),
|
||||
clients: JSON.stringify(FindDiff.ArrObj(this.clients,this.oldData.clients, "clients"), null, 2),
|
||||
tls: JSON.stringify(FindDiff.ArrObj(this.tlsConfigs,this.oldData.tlsConfigs, "tls"), null, 2),
|
||||
inData: <string|undefined> undefined,
|
||||
}
|
||||
|
||||
// Validate inData
|
||||
let invalidInData = <any[]>[]
|
||||
this.inData.forEach((d:any) => {
|
||||
const inboundIndex = this.config.inbounds.findIndex((i:any) => i.tag == d.tag)
|
||||
if (inboundIndex == -1) invalidInData.push({key: "inData", action: "del", index: d.id, obj: null})
|
||||
})
|
||||
if (invalidInData.length>0) {
|
||||
diff.inData = JSON.stringify(invalidInData)
|
||||
}
|
||||
const msg = await HttpUtils.post('api/save',diff)
|
||||
if(msg.success) {
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
async delInData(id: number) {
|
||||
const diff = {
|
||||
inData: JSON.stringify([{key: "inData", action: "del", index: id, obj: null}])
|
||||
}
|
||||
await HttpUtils.post('api/save',diff)
|
||||
},
|
||||
async delOutbound(index: number) {
|
||||
const diff = {
|
||||
config: JSON.stringify([{key: "outbounds", action: "del", index: index, obj: null}]),
|
||||
|
||||
@@ -78,7 +78,6 @@ const loadData = async () => {
|
||||
const msg = await HttpUtils.get('api/users')
|
||||
loading.value = false
|
||||
if (msg.success) {
|
||||
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])) : "- -"
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
<v-expansion-panel :title="$t('basic.log.title')">
|
||||
<v-expansion-panel-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-switch v-model="appConfig.log.disabled" color="primary" :label="$t('disable')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('basic.log.level')"
|
||||
@@ -14,14 +14,14 @@
|
||||
v-model="appConfig.log.level">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-text-field
|
||||
v-model="appConfig.log.output"
|
||||
hide-details
|
||||
:label="$t('basic.log.output')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-switch v-model="appConfig.log.timestamp" color="primary" :label="$t('basic.log.timestamp')" hide-details></v-switch>
|
||||
</v-col>
|
||||
</v-row>
|
||||
@@ -30,7 +30,7 @@
|
||||
<v-expansion-panel title="DNS">
|
||||
<v-expansion-panel-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('basic.dns.final')"
|
||||
@@ -38,7 +38,7 @@
|
||||
v-model="finalDns">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('listen.domainStrategy')"
|
||||
@@ -48,7 +48,7 @@
|
||||
v-model="appConfig.dns.strategy">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" align-self="center">
|
||||
<v-col cols="12" sm="6" md="3" lg="2" align-self="center">
|
||||
<v-btn @click="addDnsServer" rounded>
|
||||
<v-icon icon="mdi-plus" />{{ $t('basic.dns.server') }}
|
||||
</v-btn>
|
||||
@@ -58,7 +58,7 @@
|
||||
{{ $t('basic.dns.server') + ' ' + (index+1) }} <v-icon icon="mdi-delete" @click="appConfig.dns.servers.splice(index,1)" />
|
||||
<v-divider></v-divider>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-text-field
|
||||
v-model="s.tag"
|
||||
hide-details
|
||||
@@ -67,14 +67,23 @@
|
||||
:label="$t('objects.tag')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-text-field
|
||||
v-model="s.address"
|
||||
hide-details
|
||||
:label="$t('out.addr')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-text-field
|
||||
v-model="s.address_resolver"
|
||||
hide-details
|
||||
clearable
|
||||
@click:clear="delete s.address_resolver"
|
||||
:label="$t('basic.dns.addrResolver')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('objects.outbound')"
|
||||
@@ -84,7 +93,7 @@
|
||||
v-model="s.detour">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('listen.domainStrategy')"
|
||||
@@ -101,17 +110,17 @@
|
||||
<v-expansion-panel title="NTP">
|
||||
<v-expansion-panel-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-switch v-model="enableNtp" color="primary" :label="$t('enable')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.ntp?.enabled">
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.ntp?.enabled">
|
||||
<v-text-field
|
||||
v-model="appConfig.ntp.server"
|
||||
hide-details
|
||||
:label="$t('out.addr')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.ntp?.enabled">
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.ntp?.enabled">
|
||||
<v-text-field
|
||||
v-model="appConfig.ntp.server_port"
|
||||
hide-details
|
||||
@@ -121,7 +130,7 @@
|
||||
:label="$t('out.port')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.ntp?.enabled">
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.ntp?.enabled">
|
||||
<v-text-field
|
||||
v-model="ntpInterval"
|
||||
hide-details
|
||||
@@ -138,7 +147,7 @@
|
||||
<v-expansion-panel :title="$t('basic.routing.title')">
|
||||
<v-expansion-panel-text>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-select
|
||||
hide-details
|
||||
:label="$t('basic.routing.defaultOut')"
|
||||
@@ -148,7 +157,7 @@
|
||||
v-model="appConfig.route.final">
|
||||
</v-select>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-text-field
|
||||
v-model="appConfig.route.default_interface"
|
||||
hide-details
|
||||
@@ -157,7 +166,7 @@
|
||||
:label="$t('basic.routing.defaultIf')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-text-field
|
||||
v-model.number="routeMark"
|
||||
hide-details
|
||||
@@ -166,7 +175,7 @@
|
||||
:label="$t('basic.routing.defaultRm')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-switch
|
||||
v-model="appConfig.route.auto_detect_interface"
|
||||
color="primary"
|
||||
@@ -182,24 +191,24 @@
|
||||
Cache File
|
||||
<v-divider></v-divider>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-switch v-model="enableCacheFile" color="primary" :label="$t('enable')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.cache_file">
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.cache_file">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.cache_file.path"
|
||||
hide-details
|
||||
:label="$t('transport.path')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.cache_file">
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.cache_file">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.cache_file.cache_id"
|
||||
hide-details
|
||||
label="Cache ID"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.cache_file">
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.cache_file">
|
||||
<v-switch v-model="appConfig.experimental.cache_file.store_fakeip"
|
||||
color="primary"
|
||||
:label="$t('basic.exp.storeFakeIp')"
|
||||
@@ -209,45 +218,45 @@
|
||||
Clash API
|
||||
<v-divider></v-divider>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-switch v-model="enableClashApi" color="primary" :label="$t('enable')" hide-details></v-switch>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api">
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.clash_api.external_controller"
|
||||
hide-details
|
||||
label="External Controller"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api">
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.clash_api.external_ui"
|
||||
hide-details
|
||||
label="External UI"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api">
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.clash_api.external_ui_download_url"
|
||||
hide-details
|
||||
label="UI Download URL"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api">
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.clash_api.external_ui_download_detour"
|
||||
hide-details
|
||||
label="UI Download detour"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api">
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.clash_api.secret"
|
||||
hide-details
|
||||
label="Secret"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api">
|
||||
<v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.clash_api.default_mode"
|
||||
hide-details
|
||||
@@ -258,14 +267,14 @@
|
||||
V2Ray API
|
||||
<v-divider></v-divider>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-text-field
|
||||
v-model="appConfig.experimental.v2ray_api.listen"
|
||||
hide-details
|
||||
:label="$t('objects.listen')"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" sm="6" md="3">
|
||||
<v-col cols="12" sm="6" md="3" lg="2">
|
||||
<v-switch v-model="appConfig.experimental.v2ray_api.stats.enabled"
|
||||
color="primary"
|
||||
:label="$t('stats.enable')"
|
||||
|
||||
+141
-101
@@ -23,105 +23,123 @@
|
||||
:tag="stats.tag"
|
||||
@close="closeStats"
|
||||
/>
|
||||
<v-row>
|
||||
<v-col cols="12" justify="center" align="center">
|
||||
<v-row justify="center" align="center">
|
||||
<v-col cols="auto">
|
||||
<v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-select
|
||||
hide-details
|
||||
variant="underlined"
|
||||
density="compact"
|
||||
:label="$t('filter')"
|
||||
:items="filterItems"
|
||||
v-model="filter">
|
||||
</v-select>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in clients" :key="item.id">
|
||||
<v-card rounded="xl" elevation="5" min-width="200">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ item.name }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-switch color="primary"
|
||||
v-model="clients[index].enable"
|
||||
@update:model-value="buildInboundsUsers(item.inbounds)"
|
||||
hideDetails density="compact" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-card-subtitle style="margin-top: -20px;">
|
||||
<v-row>
|
||||
<v-col>{{ item.desc }}</v-col>
|
||||
</v-row>
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>{{ $t('pages.inbounds') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="item.inbounds != ''">
|
||||
<span v-for="i in item.inbounds">{{ i }}<br /></span>
|
||||
</v-tooltip>
|
||||
{{ item.inbounds.length }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('stats.volume') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.volume == 0 ? $t('unlimited') : HumanReadable.sizeFormat(item.volume) }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('date.expiry') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.expiry == 0 ? $t('unlimited') : HumanReadable.remainedDays(item.expiry)?? $t('date.expired') }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('stats.usage') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
<v-tooltip activator="parent" location="bottom">
|
||||
{{ $t('stats.upload') }}:{{ HumanReadable.sizeFormat(item.up) }}<br />
|
||||
{{ $t('stats.download') }}:{{ HumanReadable.sizeFormat(item.down) }}<br />
|
||||
<template v-if="item.volume>0">
|
||||
{{ $t('remained') }}: {{ HumanReadable.sizeFormat(item.volume - (item.up + item.down)) }}
|
||||
<template v-for="(item, index) in clients" :key="item.id">
|
||||
<v-col cols="12" sm="4" md="3" lg="2" :style="checkFilter(item)? '' : 'opacity: .2'">
|
||||
<v-card rounded="xl" elevation="5" min-width="200">
|
||||
<v-card-title>
|
||||
<v-row>
|
||||
<v-col>{{ item.name }}</v-col>
|
||||
<v-spacer></v-spacer>
|
||||
<v-col cols="auto">
|
||||
<v-switch color="primary"
|
||||
v-model="clients[index].enable"
|
||||
@update:model-value="buildInboundsUsers(item.inbounds)"
|
||||
hideDetails density="compact" />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-title>
|
||||
<v-card-subtitle style="margin-top: -20px;">
|
||||
<v-row>
|
||||
<v-col>{{ item.desc }}</v-col>
|
||||
</v-row>
|
||||
</v-card-subtitle>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col>{{ $t('pages.inbounds') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="item.inbounds != ''">
|
||||
<span v-for="i in item.inbounds">{{ i }}<br /></span>
|
||||
</v-tooltip>
|
||||
{{ item.inbounds.length }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('stats.volume') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.volume == 0 ? $t('unlimited') : HumanReadable.sizeFormat(item.volume) }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('date.expiry') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
{{ item.expiry == 0 ? $t('unlimited') : HumanReadable.remainedDays(item.expiry)?? $t('date.expired') }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('stats.usage') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
<v-tooltip activator="parent" location="bottom">
|
||||
{{ $t('stats.upload') }}:{{ HumanReadable.sizeFormat(item.up) }}<br />
|
||||
{{ $t('stats.download') }}:{{ HumanReadable.sizeFormat(item.down) }}<br />
|
||||
<template v-if="item.volume>0">
|
||||
{{ $t('remained') }}: {{ HumanReadable.sizeFormat(item.volume - (item.up + item.down)) }}
|
||||
</template>
|
||||
</v-tooltip>
|
||||
{{ HumanReadable.sizeFormat(item.up + item.down) }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('online') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
<template v-if="onlines[index]">
|
||||
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
{{ HumanReadable.sizeFormat(item.up + item.down) }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row>
|
||||
<v-col>{{ $t('online') }}</v-col>
|
||||
<v-col dir="ltr">
|
||||
<template v-if="onlines[index]">
|
||||
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
|
||||
</template>
|
||||
<template v-else>-</template>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions style="padding: 0;">
|
||||
<v-btn icon="mdi-account-edit" @click="showModal(index)">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn style="margin-inline-start:0;" icon="mdi-account-minus" color="warning" @click="delOverlay[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="delClient(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-btn icon="mdi-qrcode" @click="showQrCode(index)" />
|
||||
<v-btn icon="mdi-chart-line" @click="showStats(item.name)" />
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<template v-else>-</template>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
<v-divider></v-divider>
|
||||
<v-card-actions style="padding: 0;">
|
||||
<v-btn icon="mdi-account-edit" @click="showModal(index)">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn style="margin-inline-start:0;" icon="mdi-account-minus" color="warning" @click="delOverlay[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="delClient(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-btn icon="mdi-qrcode" @click="showQrCode(index)">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" text="QR-Code"></v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn icon="mdi-chart-line" @click="showStats(item.name)" v-if="v2rayStats.users.includes(item.name)">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</template>
|
||||
</v-row>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
@@ -163,6 +181,28 @@ const inboundTags = computed((): string[] => {
|
||||
return inbounds.value?.filter(i => i.tag != "" && Object.hasOwn(i,'users')).map(i => i.tag)
|
||||
})
|
||||
|
||||
const filter = ref("")
|
||||
|
||||
const filterItems = [
|
||||
{ title: i18n.global.t('none'), value: '' },
|
||||
{ title: i18n.global.t('disable'), value: 'disable' },
|
||||
{ title: i18n.global.t('date.expired'), value: 'expired' },
|
||||
{ title: i18n.global.t('online'), value: 'online' },
|
||||
]
|
||||
|
||||
const checkFilter = (c:any) :boolean => {
|
||||
switch (filter.value) {
|
||||
case "disable":
|
||||
return !c.enable
|
||||
case "expired":
|
||||
return HumanReadable.remainedDays(c.expiry) == null
|
||||
case "online":
|
||||
return Data().onlines?.user?.includes(c.name)
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const modal = ref({
|
||||
visible: false,
|
||||
index: -1,
|
||||
@@ -193,10 +233,6 @@ const saveModal = (data:any, stats:boolean) => {
|
||||
if(modal.value.index == -1) {
|
||||
clients.value.push(data)
|
||||
} else {
|
||||
const oldData = createClient(clients.value[modal.value.index])
|
||||
oldData.inbounds.forEach((i:string) => {
|
||||
if (!data.inbounds.includes(i)) data.inbounds.push(i)
|
||||
})
|
||||
clients.value[modal.value.index] = data
|
||||
}
|
||||
|
||||
@@ -261,9 +297,13 @@ const updateLinks = (c:Client):Link[] => {
|
||||
const newLinks = <Link[]>[]
|
||||
clientInbounds.forEach(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 })
|
||||
const cData = <any>Data().inData?.findLast((d:any) => d.tag == i.tag)
|
||||
const addrs = cData ? <any[]>cData.addrs : []
|
||||
const uris = LinkUtil.linkGenerator(c.name,i, tlsConfig?.client?? {}, addrs)
|
||||
if (uris.length>0){
|
||||
uris.forEach(uri => {
|
||||
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
|
||||
})
|
||||
}
|
||||
})
|
||||
let links = c.links && c.links.length>0? c.links : <Link[]>[]
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
<InboundVue
|
||||
v-model="modal.visible"
|
||||
:visible="modal.visible"
|
||||
:id="modal.id"
|
||||
:index="modal.index"
|
||||
:stats="modal.stats"
|
||||
:data="modal.data"
|
||||
:cData="modal.cData"
|
||||
:inTags="inTags"
|
||||
:outTags="outTags"
|
||||
:tlsConfigs="tlsConfigs"
|
||||
@@ -93,7 +94,10 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-overlay>
|
||||
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)" />
|
||||
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)" v-if="v2rayStats.inbounds.includes(item.tag)">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@@ -111,6 +115,7 @@ import { Client } from '@/types/clients'
|
||||
import { Link, LinkUtil } from '@/plugins/link'
|
||||
import { i18n } from '@/locales'
|
||||
import { push } from 'notivue'
|
||||
import { fillData } from '@/plugins/outJson'
|
||||
|
||||
const appConfig = computed((): Config => {
|
||||
return <Config> Data().config
|
||||
@@ -124,6 +129,10 @@ const tlsConfigs = computed((): any[] => {
|
||||
return <any[]> Data().tlsConfigs
|
||||
})
|
||||
|
||||
const inData = computed((): any[] => {
|
||||
return <any[]> Data().inData
|
||||
})
|
||||
|
||||
const inTags = computed((): string[] => {
|
||||
return inbounds.value?.map(i => i.tag)
|
||||
})
|
||||
@@ -146,44 +155,56 @@ const v2rayStats = computed((): V2rayApiStats => {
|
||||
|
||||
const modal = ref({
|
||||
visible: false,
|
||||
id: -1,
|
||||
index: -1,
|
||||
data: "",
|
||||
cData: "",
|
||||
stats: false,
|
||||
})
|
||||
|
||||
let delOverlay = ref(new Array<boolean>)
|
||||
|
||||
const showModal = (id: number) => {
|
||||
modal.value.id = id
|
||||
modal.value.data = id == -1 ? '' : JSON.stringify(inbounds.value[id])
|
||||
modal.value.stats = id == -1 ? false : v2rayStats.value.inbounds.includes(inbounds.value[id].tag)
|
||||
const showModal = (index: number) => {
|
||||
modal.value.index = index
|
||||
if (index == -1){
|
||||
modal.value.data = ''
|
||||
modal.value.cData = ''
|
||||
modal.value.stats = false
|
||||
} else {
|
||||
modal.value.data = JSON.stringify(inbounds.value[index])
|
||||
modal.value.stats = v2rayStats.value.inbounds.includes(inbounds.value[index].tag)
|
||||
const inDataIndex = inData.value.findIndex(d => d.tag == inbounds.value[index].tag)
|
||||
modal.value.cData = inDataIndex == -1 ? '' : JSON.stringify(inData.value[inDataIndex])
|
||||
}
|
||||
modal.value.visible = true
|
||||
}
|
||||
const closeModal = () => {
|
||||
modal.value.visible = false
|
||||
}
|
||||
const saveModal = (data:Inbound, stats: boolean, tls_id: number) => {
|
||||
const saveModal = (data:Inbound, stats: boolean, tls_id: number, cData: any) => {
|
||||
// Check duplicate tag
|
||||
const oldTag = modal.value.id != -1 ? inbounds.value[modal.value.id].tag : null
|
||||
const oldTag = modal.value.index != -1 ? inbounds.value[modal.value.index].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
|
||||
}
|
||||
if (cData.id != -1) {
|
||||
cData.tag = data.tag
|
||||
fillData(cData.outJson, data,tls_id>0 ? tlsConfigs.value.findLast(t => t.id == tls_id).client : {})
|
||||
}
|
||||
|
||||
// New or Edit
|
||||
if (modal.value.id == -1) {
|
||||
if (modal.value.index == -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)
|
||||
if (cData.id != -1){
|
||||
inData.value.push(cData)
|
||||
}
|
||||
} else {
|
||||
const oldTag = inbounds.value[modal.value.id].tag
|
||||
const oldTag = inbounds.value[modal.value.index].tag
|
||||
const sIndex = v2rayStats.value.inbounds.findIndex(i => i == data.tag) // Find if new tag exists
|
||||
|
||||
// Update tls preset
|
||||
@@ -191,9 +212,6 @@ const saveModal = (data:Inbound, stats: boolean, tls_id: number) => {
|
||||
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)
|
||||
@@ -208,8 +226,25 @@ const saveModal = (data:Inbound, stats: boolean, tls_id: number) => {
|
||||
if (sIndex != -1) v2rayStats.value.inbounds.splice(sIndex,1)
|
||||
}
|
||||
|
||||
inbounds.value[modal.value.id] = data
|
||||
inbounds.value[modal.value.index] = data
|
||||
const inDataIndex = inData.value.findIndex(indata => indata.tag == oldTag)
|
||||
if (cData.id != -1) {
|
||||
if (inDataIndex == -1){
|
||||
inData.value.push(cData)
|
||||
} else {
|
||||
inData.value[inDataIndex] = cData
|
||||
}
|
||||
} else if (inDataIndex != -1) {
|
||||
Data().delInData(inData.value[inDataIndex].id)
|
||||
inData.value.splice(inDataIndex,1)
|
||||
}
|
||||
}
|
||||
// Update tls preset
|
||||
if (tls_id>0) {
|
||||
tlsConfigs.value.findLast(t => t.id == tls_id).inbounds.push(data.tag)
|
||||
tlsConfigs.value.sort()
|
||||
}
|
||||
|
||||
if (Object.hasOwn(data,'users')) {
|
||||
// Set users
|
||||
data = buildInboundsUsers(data)
|
||||
@@ -218,7 +253,7 @@ const saveModal = (data:Inbound, stats: boolean, tls_id: number) => {
|
||||
}
|
||||
modal.value.visible = false
|
||||
}
|
||||
const updateLinks = (i: InboundWithUser) => {
|
||||
const updateLinks = (i: 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)
|
||||
@@ -226,10 +261,14 @@ const updateLinks = (i: InboundWithUser) => {
|
||||
const clientInbounds = <Inbound[]>inbounds.value.filter(inb => client?.inbounds.includes(inb.tag))
|
||||
const newLinks = <Link[]>[]
|
||||
clientInbounds.forEach(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 })
|
||||
const tlsClient = tlsConfigs?.value.findLast((t:any) => t.inbounds.includes(i.tag))?.client?? {}
|
||||
const cData = <any>Data().inData?.findLast((d:any) => d.tag == i.tag)
|
||||
const addrs = cData ? <any[]>cData.addrs : []
|
||||
const uris = LinkUtil.linkGenerator(client.name,i, tlsClient, addrs)
|
||||
if (uris.length>0){
|
||||
uris.forEach(uri => {
|
||||
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
|
||||
})
|
||||
}
|
||||
})
|
||||
let links = client.links && client.links.length>0? client.links : <Link[]>[]
|
||||
@@ -249,10 +288,10 @@ const delInbound = (index: number) => {
|
||||
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)
|
||||
const c_index = clients.value.findIndex(c => u.username? u.username == c.name : u.name == c.name)
|
||||
if (c_index != -1) {
|
||||
const clientInbounds = clients.value[c_index].inbounds.filter((x:string) => x!=tag)
|
||||
clients.value[c_index].inbounds = clientInbounds
|
||||
clients.value[c_index].inbounds = clients.value[c_index].inbounds.filter((x:string) => x!=tag)
|
||||
clients.value[c_index].links = clients.value[c_index].links.filter((x:any) => x.remark!=tag)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -277,7 +316,7 @@ const delInbound = (index: number) => {
|
||||
}
|
||||
delOverlay.value[index] = false
|
||||
}
|
||||
const buildInboundsUsers = (inbound:InboundWithUser):Inbound => {
|
||||
const buildInboundsUsers = (inbound:any):Inbound => {
|
||||
const users = <any>[]
|
||||
const inboundClients = clients.value.filter(c => c.enable && c.inbounds.includes(inbound.tag))
|
||||
inboundClients.forEach(c => {
|
||||
|
||||
@@ -82,7 +82,10 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-overlay>
|
||||
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)" />
|
||||
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)" v-if="v2rayStats.outbounds.includes(item.tag)">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<v-row>
|
||||
<v-col cols="12">{{ $t('rule.ruleset') }}</v-col>
|
||||
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>rulesets" :key="item.tag">
|
||||
<v-card rounded="xl" elevation="5" min-width="200" :title="index">
|
||||
<v-card rounded="xl" elevation="5" min-width="200" :title="index+1">
|
||||
<v-card-subtitle style="margin-top: -20px;">
|
||||
<v-row>
|
||||
<v-col>{{ $t('ruleset.' + item.type) }}</v-col>
|
||||
@@ -86,7 +86,7 @@
|
||||
<v-row>
|
||||
<v-col cols="12">{{ $t('pages.rules') }}</v-col>
|
||||
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>rules">
|
||||
<v-card rounded="xl" elevation="5" min-width="200" :title="index">
|
||||
<v-card rounded="xl" elevation="5" min-width="200" :title="index+1">
|
||||
<v-card-subtitle style="margin-top: -20px;">
|
||||
<v-row>
|
||||
<v-col>{{ item.type != undefined ? $t('rule.logical') + ' (' + item.mode + ')' : $t('rule.simple') }}</v-col>
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
>
|
||||
<v-tab value="t1">{{ $t('setting.interface') }}</v-tab>
|
||||
<v-tab value="t2">{{ $t('setting.sub') }}</v-tab>
|
||||
<v-tab value="t3">Language</v-tab>
|
||||
<v-tab value="t3">{{ $t('setting.jsonSub') }}</v-tab>
|
||||
<v-tab value="t4">Language</v-tab>
|
||||
</v-tabs>
|
||||
<v-card-text>
|
||||
<v-row align="center" justify="center" style="margin-bottom: 10px;">
|
||||
@@ -128,6 +129,10 @@
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="t3">
|
||||
<SubJsonExtVue :settings="settings" />
|
||||
</v-window-item>
|
||||
|
||||
<v-window-item value="t4">
|
||||
<v-row>
|
||||
<v-col cols="12" sm="6" md="4">
|
||||
<v-select
|
||||
@@ -146,11 +151,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useLocale } from "vuetify"
|
||||
import { useLocale } from 'vuetify'
|
||||
import { languages } from '@/locales'
|
||||
import { Ref, computed, inject, onMounted, ref } from "vue"
|
||||
import HttpUtils from "@/plugins/httputil"
|
||||
import { FindDiff } from "@/plugins/utils"
|
||||
import { Ref, computed, inject, onMounted, ref } from 'vue'
|
||||
import HttpUtils from '@/plugins/httputil'
|
||||
import { FindDiff } from '@/plugins/utils'
|
||||
import SubJsonExtVue from '@/components/SubJsonExt.vue'
|
||||
const locale = useLocale()
|
||||
const tab = ref("t1")
|
||||
const loading:Ref = inject('loading')?? ref(false)
|
||||
@@ -177,6 +183,7 @@ const settings = ref({
|
||||
subEncode: "true",
|
||||
subShowInfo: "false",
|
||||
subURI: "",
|
||||
subJsonExt: "",
|
||||
})
|
||||
|
||||
onMounted(async () => {loadData()})
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</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 rounded="xl" elevation="5" min-width="200" :title="(item.id? item.id + '. ' : '*') + item.name">
|
||||
<v-card-subtitle style="margin-top: -20px;">
|
||||
{{ item.server?.server_name?.length>0 ? item.server.server_name : "-" }}
|
||||
</v-card-subtitle>
|
||||
@@ -71,6 +71,10 @@
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-overlay>
|
||||
<v-btn icon="mdi-content-duplicate" @click="clone(index)">
|
||||
<v-icon />
|
||||
<v-tooltip activator="parent" location="top" :text="$t('actions.clone')"></v-tooltip>
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
@@ -111,6 +115,15 @@ const showModal = (index: number) => {
|
||||
modal.value.data = index == -1 ? '{}' : JSON.stringify(tlsConfigs.value[index])
|
||||
modal.value.visible = true
|
||||
}
|
||||
const clone = (index: number) => {
|
||||
let data = JSON.parse(JSON.stringify(tlsConfigs.value[index]))
|
||||
data.id = 0
|
||||
data.inbounds = []
|
||||
while (tlsConfigs.value.findIndex(t => t.name == data.name) != -1){
|
||||
data.name += "-copy"
|
||||
}
|
||||
saveModal(data)
|
||||
}
|
||||
const closeModal = () => {
|
||||
modal.value.visible = false
|
||||
}
|
||||
@@ -144,9 +157,13 @@ const updateLinks = (i:any,tlsClient:any) => {
|
||||
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 })
|
||||
const cData = <any>Data().inData?.findLast((d:any) => d.tag == i.tag)
|
||||
const addrs = cData ? <any[]>cData.addrs : []
|
||||
const uris = LinkUtil.linkGenerator(client,i, tlsClient, addrs)
|
||||
if (uris.length>0){
|
||||
uris.forEach(uri => {
|
||||
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
|
||||
})
|
||||
}
|
||||
})
|
||||
let links = client.links && client.links.length>0? client.links : <Link[]>[]
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
"include": ["vite.config.mts"],
|
||||
"exclude": []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user