From 6672a2721fa5b7681e27b3c03f35c4c59c99f7b6 Mon Sep 17 00:00:00 2001 From: Alireza Ahmadi Date: Fri, 28 Jun 2024 15:55:37 +0200 Subject: [PATCH] subjson and multidomain --- backend/api/api.go | 6 + backend/database/db.go | 1 + backend/database/model/model.go | 7 + backend/service/config.go | 36 +- backend/service/inData.go | 46 ++ backend/service/setting.go | 7 +- backend/sub/jsonService.go | 255 +++++++++++ backend/sub/linkService.go | 100 ++++ backend/sub/subHandler.go | 30 +- backend/sub/subService.go | 105 +---- backend/util/base64.go | 20 + backend/util/linkToJson.go | 453 +++++++++++++++++++ frontend/src/components/Addr.vue | 114 +++++ frontend/src/components/OutJson.vue | 123 +++++ frontend/src/components/SubJsonExt.vue | 342 ++++++++++++++ frontend/src/components/{ => tls}/Acme.vue | 2 +- frontend/src/components/{ => tls}/Ech.vue | 0 frontend/src/components/{ => tls}/InTLS.vue | 2 +- frontend/src/components/{ => tls}/OutTLS.vue | 2 +- frontend/src/layouts/default/AppBar.vue | 4 +- frontend/src/layouts/modals/Inbound.vue | 146 ++++-- frontend/src/layouts/modals/Outbound.vue | 2 +- frontend/src/layouts/modals/Tls.vue | 4 +- frontend/src/plugins/inData.ts | 15 + frontend/src/plugins/link.ts | 359 ++++++++++++--- frontend/src/plugins/outJson.ts | 104 +++++ frontend/src/plugins/utils.ts | 6 +- frontend/src/store/modules/data.ts | 36 +- frontend/src/views/Clients.vue | 10 +- frontend/src/views/Inbounds.vue | 81 +++- frontend/src/views/Settings.vue | 17 +- frontend/src/views/Tls.vue | 10 +- 32 files changed, 2163 insertions(+), 282 deletions(-) create mode 100644 backend/service/inData.go create mode 100644 backend/sub/jsonService.go create mode 100644 backend/sub/linkService.go create mode 100644 backend/util/base64.go create mode 100644 backend/util/linkToJson.go create mode 100644 frontend/src/components/Addr.vue create mode 100644 frontend/src/components/OutJson.vue create mode 100644 frontend/src/components/SubJsonExt.vue rename frontend/src/components/{ => tls}/Acme.vue (98%) rename frontend/src/components/{ => tls}/Ech.vue (100%) rename frontend/src/components/{ => tls}/InTLS.vue (98%) rename frontend/src/components/{ => tls}/OutTLS.vue (99%) create mode 100644 frontend/src/plugins/inData.ts create mode 100644 frontend/src/plugins/outJson.ts diff --git a/backend/api/api.go b/backend/api/api.go index 11c16aa..8b16e82 100644 --- a/backend/api/api.go +++ b/backend/api/api.go @@ -15,6 +15,7 @@ type APIHandler struct { service.ConfigService service.ClientService service.TlsService + service.InDataService service.PanelService service.StatsService service.ServerService @@ -206,6 +207,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 @@ -213,6 +218,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 { diff --git a/backend/database/db.go b/backend/database/db.go index a6b055d..9e7c5b2 100644 --- a/backend/database/db.go +++ b/backend/database/db.go @@ -60,6 +60,7 @@ func InitDB(dbPath string) error { err = db.AutoMigrate( &model.Setting{}, &model.Tls{}, + &model.InboundData{}, &model.User{}, &model.Stats{}, &model.Client{}, diff --git a/backend/database/model/model.go b/backend/database/model/model.go index eca10f9..0004dcc 100644 --- a/backend/database/model/model.go +++ b/backend/database/model/model.go @@ -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"` diff --git a/backend/service/config.go b/backend/service/config.go index d083deb..c5f4e51 100644 --- a/backend/service/config.go +++ b/backend/service/config.go @@ -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 diff --git a/backend/service/inData.go b/backend/service/inData.go new file mode 100644 index 0000000..296b3bc --- /dev/null +++ b/backend/service/inData.go @@ -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 +} diff --git a/backend/service/setting.go b/backend/service/setting.go index f3a2c8d..ea48a9f 100644 --- a/backend/service/setting.go +++ b/backend/service/setting.go @@ -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 } @@ -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 diff --git a/backend/sub/jsonService.go b/backend/sub/jsonService.go new file mode 100644 index 0000000..fb72992 --- /dev/null +++ b/backend/sub/jsonService.go @@ -0,0 +1,255 @@ +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) + newTag := fmt.Sprintf("%d.%s", index+1, tag) + 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{}{ + "type": "logical", + "mode": "or", + "rules": []interface{}{ + map[string]interface{}{ + "port": 53, + }, + map[string]interface{}{ + "protocol": "dns", + }, + }, + "outbound": "dns-out", + }, + 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 +} diff --git a/backend/sub/linkService.go b/backend/sub/linkService.go new file mode 100644 index 0000000..b6145b7 --- /dev/null +++ b/backend/sub/linkService.go @@ -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") + +} diff --git a/backend/sub/subHandler.go b/backend/sub/subHandler.go index 5a97817..0ff672f 100644 --- a/backend/sub/subHandler.go +++ b/backend/sub/subHandler.go @@ -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) + } } } diff --git a/backend/sub/subService.go b/backend/sub/subService.go index 0dcf18d..8909d85 100644 --- a/backend/sub/subService.go +++ b/backend/sub/subService.go @@ -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)) diff --git a/backend/util/base64.go b/backend/util/base64.go new file mode 100644 index 0000000..c2327b0 --- /dev/null +++ b/backend/util/base64.go @@ -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) +} diff --git a/backend/util/linkToJson.go b/backend/util/linkToJson.go new file mode 100644 index 0000000..d78324d --- /dev/null +++ b/backend/util/linkToJson.go @@ -0,0 +1,453 @@ +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) + 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 := 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 := 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 := 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 := 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 := 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 := 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 +} diff --git a/frontend/src/components/Addr.vue b/frontend/src/components/Addr.vue new file mode 100644 index 0000000..a6341db --- /dev/null +++ b/frontend/src/components/Addr.vue @@ -0,0 +1,114 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/OutJson.vue b/frontend/src/components/OutJson.vue new file mode 100644 index 0000000..35dbf74 --- /dev/null +++ b/frontend/src/components/OutJson.vue @@ -0,0 +1,123 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/SubJsonExt.vue b/frontend/src/components/SubJsonExt.vue new file mode 100644 index 0000000..75a0666 --- /dev/null +++ b/frontend/src/components/SubJsonExt.vue @@ -0,0 +1,342 @@ + + + \ No newline at end of file diff --git a/frontend/src/components/Acme.vue b/frontend/src/components/tls/Acme.vue similarity index 98% rename from frontend/src/components/Acme.vue rename to frontend/src/components/tls/Acme.vue index 9fcc3d3..fea944f 100644 --- a/frontend/src/components/Acme.vue +++ b/frontend/src/components/tls/Acme.vue @@ -124,7 +124,7 @@ diff --git a/frontend/src/components/Ech.vue b/frontend/src/components/tls/Ech.vue similarity index 100% rename from frontend/src/components/Ech.vue rename to frontend/src/components/tls/Ech.vue diff --git a/frontend/src/components/InTLS.vue b/frontend/src/components/tls/InTLS.vue similarity index 98% rename from frontend/src/components/InTLS.vue rename to frontend/src/components/tls/InTLS.vue index 4159651..d13768d 100644 --- a/frontend/src/components/InTLS.vue +++ b/frontend/src/components/tls/InTLS.vue @@ -116,7 +116,7 @@ diff --git a/frontend/src/components/OutTLS.vue b/frontend/src/components/tls/OutTLS.vue similarity index 99% rename from frontend/src/components/OutTLS.vue rename to frontend/src/components/tls/OutTLS.vue index 7a58da5..5fcf2a2 100644 --- a/frontend/src/components/OutTLS.vue +++ b/frontend/src/components/tls/OutTLS.vue @@ -177,7 +177,7 @@ diff --git a/frontend/src/layouts/default/AppBar.vue b/frontend/src/layouts/default/AppBar.vue index 6b3d0e5..e0ed2f2 100644 --- a/frontend/src/layouts/default/AppBar.vue +++ b/frontend/src/layouts/default/AppBar.vue @@ -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 => { diff --git a/frontend/src/layouts/modals/Inbound.vue b/frontend/src/layouts/modals/Inbound.vue index c9719bf..828f036 100644 --- a/frontend/src/layouts/modals/Inbound.vue +++ b/frontend/src/layouts/modals/Inbound.vue @@ -5,35 +5,66 @@ {{ $t('actions.' + title) + " " + $t('objects.inbound') }} - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + Server Side + Client Side + + + + + + + + + + + + + + + + + + + + + + + Multi Domain + + + + +
{{ inData }}
+
+
+
@@ -59,6 +90,9 @@ \ No newline at end of file diff --git a/frontend/src/layouts/modals/Outbound.vue b/frontend/src/layouts/modals/Outbound.vue index e9faa45..3067886 100644 --- a/frontend/src/layouts/modals/Outbound.vue +++ b/frontend/src/layouts/modals/Outbound.vue @@ -89,7 +89,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' diff --git a/frontend/src/layouts/modals/Tls.vue b/frontend/src/layouts/modals/Tls.vue index 068c953..5c3219d 100644 --- a/frontend/src/layouts/modals/Tls.vue +++ b/frontend/src/layouts/modals/Tls.vue @@ -289,8 +289,8 @@