diff --git a/sub/subService.go b/sub/subService.go index 23fad299..a1cbbb0f 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -53,6 +53,7 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream) stream["security"] = masterStream["security"] stream["tlsSettings"] = masterStream["tlsSettings"] + stream["externalProxy"] = masterStream["externalProxy"] modifiedStream, _ := json.MarshalIndent(stream, "", " ") inbound.StreamSettings = string(modifiedStream) } @@ -96,7 +97,14 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) { db := database.GetDB() var inbounds []*model.Inbound - err := db.Model(model.Inbound{}).Preload("ClientStats").Where("settings like ? and enable = ?", fmt.Sprintf(`%%"subId": "%s"%%`, subId), true).Find(&inbounds).Error + err := db.Model(model.Inbound{}).Preload("ClientStats").Where(`id in ( + SELECT DISTINCT inbounds.id + FROM inbounds, + JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client + WHERE + protocol in ('vmess','vless','trojan','shadowsocks') + AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ? + )`, subId, true).Find(&inbounds).Error if err != nil { return nil, err } @@ -196,7 +204,6 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { } security, _ := stream["security"].(string) - var domains []interface{} obj["tls"] = security if security == "tls" { tlsSetting, _ := stream["tlsSettings"].(map[string]interface{}) @@ -208,24 +215,18 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { } obj["alpn"] = strings.Join(alpn, ",") } + if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { + obj["sni"], _ = sniValue.(string) + } + tlsSettings, _ := searchKey(tlsSetting, "settings") if tlsSetting != nil { - if sniValue, ok := searchKey(tlsSettings, "serverName"); ok { - obj["sni"], _ = sniValue.(string) - } if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { obj["fp"], _ = fpValue.(string) } if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok { obj["allowInsecure"], _ = insecure.(bool) } - if domainSettings, ok := searchKey(tlsSettings, "domains"); ok { - domains, _ = domainSettings.([]interface{}) - } - } - serverName, _ := tlsSetting["serverName"].(string) - if serverName != "" { - obj["add"] = serverName } } @@ -239,16 +240,30 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string { } obj["id"] = clients[clientIndex].ID - if len(domains) > 0 { + externalProxies, _ := stream["externalProxy"].([]interface{}) + + if len(externalProxies) > 0 { links := "" - for index, d := range domains { - domain := d.(map[string]interface{}) - obj["ps"] = s.genRemark(inbound, email, domain["remark"].(string)) - obj["add"] = domain["domain"].(string) + for index, externalProxy := range externalProxies { + ep, _ := externalProxy.(map[string]interface{}) + newSecurity, _ := ep["forceTls"].(string) + newObj := map[string]interface{}{} + for key, value := range obj { + if !(newSecurity == "none" && (key == "alpn" || key == "sni" || key == "fp" || key == "allowInsecure")) { + newObj[key] = value + } + } + newObj["ps"] = s.genRemark(inbound, email, ep["remark"].(string)) + newObj["add"] = ep["dest"].(string) + newObj["port"] = int(ep["port"].(float64)) + + if newSecurity != "same" { + newObj["tls"] = newSecurity + } if index > 0 { links += "\n" } - jsonStr, _ := json.MarshalIndent(obj, "", " ") + jsonStr, _ := json.MarshalIndent(newObj, "", " ") links += "vmess://" + base64.StdEncoding.EncodeToString(jsonStr) } return links @@ -323,7 +338,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { } security, _ := stream["security"].(string) - var domains []interface{} if security == "tls" { params["security"] = "tls" tlsSetting, _ := stream["tlsSettings"].(map[string]interface{}) @@ -335,11 +349,12 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { if len(alpn) > 0 { params["alpn"] = strings.Join(alpn, ",") } + if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { + params["sni"], _ = sniValue.(string) + } + tlsSettings, _ := searchKey(tlsSetting, "settings") if tlsSetting != nil { - if sniValue, ok := searchKey(tlsSettings, "serverName"); ok { - params["sni"], _ = sniValue.(string) - } if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { params["fp"], _ = fpValue.(string) } @@ -348,19 +363,11 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { params["allowInsecure"] = "1" } } - if domainSettings, ok := searchKey(tlsSettings, "domains"); ok { - domains, _ = domainSettings.([]interface{}) - } } if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { params["flow"] = clients[clientIndex].Flow } - - serverName, _ := tlsSetting["serverName"].(string) - if serverName != "" { - address = serverName - } } if security == "reality" { @@ -389,11 +396,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { params["spx"] = spx } } - if serverName, ok := searchKey(realitySettings, "serverName"); ok { - if sname, ok := serverName.(string); ok && len(sname) > 0 { - address = sname - } - } } if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { @@ -412,7 +414,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { if len(alpn) > 0 { params["alpn"] = strings.Join(alpn, ",") } - + if sniValue, ok := searchKey(xtlsSetting, "serverName"); ok { + params["sni"], _ = sniValue.(string) + } xtlsSettings, _ := searchKey(xtlsSetting, "settings") if xtlsSetting != nil { if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok { @@ -423,25 +427,55 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { params["allowInsecure"] = "1" } } - if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok { - params["sni"], _ = sniValue.(string) - } } if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { params["flow"] = clients[clientIndex].Flow } - - serverName, _ := xtlsSetting["serverName"].(string) - if serverName != "" { - address = serverName - } } if security != "tls" && security != "reality" && security != "xtls" { params["security"] = "none" } + externalProxies, _ := stream["externalProxy"].([]interface{}) + + if len(externalProxies) > 0 { + links := "" + for index, externalProxy := range externalProxies { + ep, _ := externalProxy.(map[string]interface{}) + newSecurity, _ := ep["forceTls"].(string) + dest, _ := ep["dest"].(string) + port := int(ep["port"].(float64)) + link := fmt.Sprintf("vless://%s@%s:%d", uuid, dest, port) + + if newSecurity != "same" { + params["security"] = newSecurity + } else { + params["security"] = security + } + url, _ := url.Parse(link) + q := url.Query() + + for k, v := range params { + if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) { + q.Add(k, v) + } + } + + // Set the new query values on the URL + url.RawQuery = q.Encode() + + url.Fragment = s.genRemark(inbound, email, ep["remark"].(string)) + + if index > 0 { + links += "\n" + } + links += url.String() + } + return links + } + link := fmt.Sprintf("vless://%s@%s:%d", uuid, address, port) url, _ := url.Parse(link) q := url.Query() @@ -453,20 +487,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { // Set the new query values on the URL url.RawQuery = q.Encode() - if len(domains) > 0 { - links := "" - for index, d := range domains { - domain := d.(map[string]interface{}) - url.Fragment = s.genRemark(inbound, email, domain["remark"].(string)) - url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port) - if index > 0 { - links += "\n" - } - links += url.String() - } - return links - } - url.Fragment = s.genRemark(inbound, email, "") return url.String() } @@ -534,7 +554,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string } security, _ := stream["security"].(string) - var domains []interface{} if security == "tls" { params["security"] = "tls" tlsSetting, _ := stream["tlsSettings"].(map[string]interface{}) @@ -546,11 +565,11 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string if len(alpn) > 0 { params["alpn"] = strings.Join(alpn, ",") } + if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { + params["sni"], _ = sniValue.(string) + } tlsSettings, _ := searchKey(tlsSetting, "settings") if tlsSetting != nil { - if sniValue, ok := searchKey(tlsSettings, "serverName"); ok { - params["sni"], _ = sniValue.(string) - } if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { params["fp"], _ = fpValue.(string) } @@ -559,14 +578,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string params["allowInsecure"] = "1" } } - if domainSettings, ok := searchKey(tlsSettings, "domains"); ok { - domains, _ = domainSettings.([]interface{}) - } - } - - serverName, _ := tlsSetting["serverName"].(string) - if serverName != "" { - address = serverName } } @@ -596,11 +607,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string params["spx"] = spx } } - if serverName, ok := searchKey(realitySettings, "serverName"); ok { - if sname, ok := serverName.(string); ok && len(sname) > 0 { - address = sname - } - } } if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { @@ -619,6 +625,9 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string if len(alpn) > 0 { params["alpn"] = strings.Join(alpn, ",") } + if sniValue, ok := searchKey(xtlsSetting, "serverName"); ok { + params["sni"], _ = sniValue.(string) + } xtlsSettings, _ := searchKey(xtlsSetting, "settings") if xtlsSetting != nil { @@ -630,25 +639,55 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string params["allowInsecure"] = "1" } } - if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok { - params["sni"], _ = sniValue.(string) - } } if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 { params["flow"] = clients[clientIndex].Flow } - - serverName, _ := xtlsSetting["serverName"].(string) - if serverName != "" { - address = serverName - } } if security != "tls" && security != "reality" && security != "xtls" { params["security"] = "none" } + externalProxies, _ := stream["externalProxy"].([]interface{}) + + if len(externalProxies) > 0 { + links := "" + for index, externalProxy := range externalProxies { + ep, _ := externalProxy.(map[string]interface{}) + newSecurity, _ := ep["forceTls"].(string) + dest, _ := ep["dest"].(string) + port := int(ep["port"].(float64)) + link := fmt.Sprintf("trojan://%s@%s:%d", password, dest, port) + + if newSecurity != "same" { + params["security"] = newSecurity + } else { + params["security"] = security + } + url, _ := url.Parse(link) + q := url.Query() + + for k, v := range params { + if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) { + q.Add(k, v) + } + } + + // Set the new query values on the URL + url.RawQuery = q.Encode() + + url.Fragment = s.genRemark(inbound, email, ep["remark"].(string)) + + if index > 0 { + links += "\n" + } + links += url.String() + } + return links + } + link := fmt.Sprintf("trojan://%s@%s:%d", password, address, port) url, _ := url.Parse(link) @@ -661,20 +700,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string // Set the new query values on the URL url.RawQuery = q.Encode() - if len(domains) > 0 { - links := "" - for index, d := range domains { - domain := d.(map[string]interface{}) - url.Fragment = s.genRemark(inbound, email, domain["remark"].(string)) - url.Host = fmt.Sprintf("%s:%d", domain["domain"].(string), port) - if index > 0 { - links += "\n" - } - links += url.String() - } - return links - } - url.Fragment = s.genRemark(inbound, email, "") return url.String() } @@ -744,10 +769,78 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st } } + security, _ := stream["security"].(string) + if security == "tls" { + params["security"] = "tls" + tlsSetting, _ := stream["tlsSettings"].(map[string]interface{}) + alpns, _ := tlsSetting["alpn"].([]interface{}) + var alpn []string + for _, a := range alpns { + alpn = append(alpn, a.(string)) + } + if len(alpn) > 0 { + params["alpn"] = strings.Join(alpn, ",") + } + if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { + params["sni"], _ = sniValue.(string) + } + + tlsSettings, _ := searchKey(tlsSetting, "settings") + if tlsSetting != nil { + if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { + params["fp"], _ = fpValue.(string) + } + if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok { + if insecure.(bool) { + params["allowInsecure"] = "1" + } + } + } + } + encPart := fmt.Sprintf("%s:%s", method, clients[clientIndex].Password) if method[0] == '2' { encPart = fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password) } + + externalProxies, _ := stream["externalProxy"].([]interface{}) + + if len(externalProxies) > 0 { + links := "" + for index, externalProxy := range externalProxies { + ep, _ := externalProxy.(map[string]interface{}) + newSecurity, _ := ep["forceTls"].(string) + dest, _ := ep["dest"].(string) + port := int(ep["port"].(float64)) + link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), dest, port) + + if newSecurity != "same" { + params["security"] = newSecurity + } else { + params["security"] = security + } + url, _ := url.Parse(link) + q := url.Query() + + for k, v := range params { + if !(newSecurity == "none" && (k == "alpn" || k == "sni" || k == "fp" || k == "allowInsecure")) { + q.Add(k, v) + } + } + + // Set the new query values on the URL + url.RawQuery = q.Encode() + + url.Fragment = s.genRemark(inbound, email, ep["remark"].(string)) + + if index > 0 { + links += "\n" + } + links += url.String() + } + return links + } + link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port) url, _ := url.Parse(link) q := url.Query() @@ -758,6 +851,7 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st // Set the new query values on the URL url.RawQuery = q.Encode() + url.Fragment = s.genRemark(inbound, email, "") return url.String() } diff --git a/web/assets/js/model/xray.js b/web/assets/js/model/xray.js index 0418540a..97110ae9 100644 --- a/web/assets/js/model/xray.js +++ b/web/assets/js/model/xray.js @@ -578,27 +578,21 @@ TlsStreamSettings.Cert = class extends XrayCommonClass { }; TlsStreamSettings.Settings = class extends XrayCommonClass { - constructor(allowInsecure = false, fingerprint = '', serverName = '', domains = []) { + constructor(allowInsecure = false, fingerprint = '') { super(); this.allowInsecure = allowInsecure; this.fingerprint = fingerprint; - this.serverName = serverName; - this.domains = domains; } static fromJson(json = {}) { return new TlsStreamSettings.Settings( json.allowInsecure, json.fingerprint, - json.serverName, - json.domains, ); } toJson() { return { allowInsecure: this.allowInsecure, fingerprint: this.fingerprint, - serverName: this.serverName, - domains: this.domains, }; } }; @@ -692,21 +686,18 @@ XtlsStreamSettings.Cert = class extends XrayCommonClass { }; XtlsStreamSettings.Settings = class extends XrayCommonClass { - constructor(allowInsecure = false, serverName = '') { + constructor(allowInsecure = false) { super(); this.allowInsecure = allowInsecure; - this.serverName = serverName; } static fromJson(json = {}) { return new XtlsStreamSettings.Settings( json.allowInsecure, - json.servername, ); } toJson() { return { allowInsecure: this.allowInsecure, - serverName: this.serverName, }; } }; @@ -773,18 +764,16 @@ class RealityStreamSettings extends XrayCommonClass { } RealityStreamSettings.Settings = class extends XrayCommonClass { - constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, serverName = '', spiderX= '/') { + constructor(publicKey = '', fingerprint = UTLS_FINGERPRINT.UTLS_FIREFOX, spiderX= '/') { super(); this.publicKey = publicKey; this.fingerprint = fingerprint; - this.serverName = serverName; this.spiderX = spiderX; } static fromJson(json = {}) { return new RealityStreamSettings.Settings( json.publicKey, json.fingerprint, - json.serverName, json.spiderX, ); } @@ -792,7 +781,6 @@ RealityStreamSettings.Settings = class extends XrayCommonClass { return { publicKey: this.publicKey, fingerprint: this.fingerprint, - serverName: this.serverName, spiderX: this.spiderX, }; } @@ -829,6 +817,7 @@ class SockoptStreamSettings extends XrayCommonClass { class StreamSettings extends XrayCommonClass { constructor(network='tcp', security='none', + externalProxy = [], tlsSettings=new TlsStreamSettings(), xtlsSettings=new XtlsStreamSettings(), realitySettings = new RealityStreamSettings(), @@ -843,6 +832,7 @@ class StreamSettings extends XrayCommonClass { super(); this.network = network; this.security = security; + this.externalProxy = externalProxy; this.tls = tlsSettings; this.xtls = xtlsSettings; this.reality = realitySettings; @@ -901,10 +891,10 @@ class StreamSettings extends XrayCommonClass { } static fromJson(json={}) { - return new StreamSettings( json.network, json.security, + json.externalProxy, TlsStreamSettings.fromJson(json.tlsSettings), XtlsStreamSettings.fromJson(json.xtlsSettings), RealityStreamSettings.fromJson(json.realitySettings), @@ -923,6 +913,7 @@ class StreamSettings extends XrayCommonClass { return { network: network, security: this.security, + externalProxy: this.externalProxy, tlsSettings: this.isTls ? this.tls.toJson() : undefined, xtlsSettings: this.isXtls ? this.xtls.toJson() : undefined, realitySettings: this.isReality ? this.reality.toJson() : undefined, @@ -982,6 +973,16 @@ class Inbound extends XrayCommonClass { return this.clientStats; } + get clients() { + switch (this.protocol) { + case Protocols.VMESS: return this.settings.vmesses; + case Protocols.VLESS: return this.settings.vlesses; + case Protocols.TROJAN: return this.settings.trojans; + case Protocols.SHADOWSOCKS: return this.isSSMultiUser ? this.settings.shadowsockses : null; + default: return null; + } + } + get protocol() { return this._protocol; } @@ -1132,26 +1133,8 @@ class Inbound extends XrayCommonClass { } isExpiry(index) { - switch (this.protocol) { - case Protocols.VMESS: - if(this.settings.vmesses[index].expiryTime > 0) - return this.settings.vmesses[index].expiryTime < new Date().getTime(); - return false - case Protocols.VLESS: - if(this.settings.vlesses[index].expiryTime > 0) - return this.settings.vlesses[index].expiryTime < new Date().getTime(); - return false - case Protocols.TROJAN: - if(this.settings.trojans[index].expiryTime > 0) - return this.settings.trojans[index].expiryTime < new Date().getTime(); - return false - case Protocols.SHADOWSOCKS: - if(this.settings.shadowsockses.length > 0 && this.settings.shadowsockses[index].expiryTime > 0) - return this.settings.shadowsockses[index].expiryTime < new Date().getTime(); - return false - default: - return false; - } + let exp = this.clients[index].expiryTime; + return exp > 0 ? exp < new Date().getTime() : false; } canEnableTls() { @@ -1195,19 +1178,20 @@ class Inbound extends XrayCommonClass { this.sniffing = new Sniffing(); } - genVmessLink(address='', remark='', clientIndex=0) { + genVmessLink(address='', port=this.port, forceTls, remark='', clientId) { if (this.protocol !== Protocols.VMESS) { return ''; } + const security = forceTls == 'same' ? this.stream.security : forceTls; let obj = { v: '2', ps: remark, add: address, - port: this.port, - id: this.settings.vmesses[clientIndex].id, + port: port, + id: clientId, net: this.stream.network, type: 'none', - tls: this.stream.security, + tls: security, }; let network = this.stream.network; if (network === 'tcp') { @@ -1247,12 +1231,9 @@ class Inbound extends XrayCommonClass { } } - if (this.stream.security === 'tls') { - if (!ObjectUtil.isEmpty(this.stream.tls.server)) { - obj.add = this.stream.tls.server; - } - if (!ObjectUtil.isEmpty(this.stream.tls.settings.serverName)){ - obj.sni = this.stream.tls.settings.serverName; + if (security === 'tls') { + if (!ObjectUtil.isEmpty(this.stream.tls.sni)){ + obj.sni = this.stream.tls.server; } if (!ObjectUtil.isEmpty(this.stream.tls.settings.fingerprint)){ obj.fp = this.stream.tls.settings.fingerprint; @@ -1268,11 +1249,10 @@ class Inbound extends XrayCommonClass { return 'vmess://' + base64(JSON.stringify(obj, null, 2)); } - genVLESSLink(address = '', remark='', clientIndex=0) { - const settings = this.settings; - const uuid = settings.vlesses[clientIndex].id; - const port = this.port; + genVLESSLink(address = '', port=this.port, forceTls, remark='', clientId, flow) { + const uuid = clientId; const type = this.stream.network; + const security = forceTls == 'same' ? this.stream.security : forceTls; const params = new Map(); params.set("type", this.stream.network); switch (type) { @@ -1323,58 +1303,51 @@ class Inbound extends XrayCommonClass { break; } - if (this.tls) { + if (security === 'tls') { params.set("security", "tls"); - params.set("fp" , this.stream.tls.settings.fingerprint); - params.set("alpn", this.stream.tls.alpn); - if(this.stream.tls.settings.allowInsecure){ - params.set("allowInsecure", "1"); - } - if (!ObjectUtil.isEmpty(this.stream.tls.server)) { - address = this.stream.tls.server; - } - if (this.stream.tls.settings.serverName !== ''){ - params.set("sni", this.stream.tls.settings.serverName); - } - if (type === "tcp" && this.settings.vlesses[clientIndex].flow.length > 0) { - params.set("flow", this.settings.vlesses[clientIndex].flow); + if (this.stream.isTls){ + params.set("fp" , this.stream.tls.settings.fingerprint); + params.set("alpn", this.stream.tls.alpn); + if(this.stream.tls.settings.allowInsecure){ + params.set("allowInsecure", "1"); + } + if (!ObjectUtil.isEmpty(this.stream.tls.server)){ + params.set("sni", this.stream.tls.server); + } + if (type == "tcp" && !ObjectUtil.isEmpty(flow)) { + params.set("flow", flow); + } } } - else if (this.xtls) { + else if (security === 'xtls') { params.set("security", "xtls"); params.set("alpn", this.stream.xtls.alpn); if(this.stream.xtls.settings.allowInsecure){ params.set("allowInsecure", "1"); } - if (!ObjectUtil.isEmpty(this.stream.xtls.server)) { - address = this.stream.xtls.server; - } - if (this.stream.xtls.settings.serverName !== ''){ - params.set("sni", this.stream.xtls.settings.serverName); + if (!ObjectUtil.isEmpty(this.stream.xtls.server)){ + params.set("sni", this.stream.xtls.server); } params.set("flow", this.settings.vlesses[clientIndex].flow); } - else if (this.reality) { + else if (security === 'reality') { params.set("security", "reality"); - params.set("fp", this.stream.reality.settings.fingerprint); params.set("pbk", this.stream.reality.settings.publicKey); + params.set("fp", this.stream.reality.settings.fingerprint); if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) { params.set("sni", this.stream.reality.serverNames.split(",")[0]); } - if (this.stream.network === 'tcp' && !ObjectUtil.isEmpty(this.settings.vlesses[clientIndex].flow)) { - params.set("flow", this.settings.vlesses[clientIndex].flow); - } if (this.stream.reality.shortIds.length > 0) { params.set("sid", this.stream.reality.shortIds.split(",")[0]); } - if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) { - address = this.stream.reality.settings.serverName; - } if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) { params.set("spx", this.stream.reality.settings.spiderX); } + if (type == 'tcp' && !ObjectUtil.isEmpty(flow)) { + params.set("flow", flow); + } } else { @@ -1390,10 +1363,10 @@ class Inbound extends XrayCommonClass { return url.toString(); } - genSSLink(address='', remark='', clientIndex = 0) { + genSSLink(address='', port=this.port, forceTls, remark='', clientPassword) { let settings = this.settings; - const port = this.port; const type = this.stream.network; + const security = forceTls == 'same' ? this.stream.security : forceTls; const params = new Map(); params.set("type", this.stream.network); switch (type) { @@ -1444,11 +1417,26 @@ class Inbound extends XrayCommonClass { break; } + if (security === 'tls') { + params.set("security", "tls"); + if (this.stream.isTls){ + params.set("fp" , this.stream.tls.settings.fingerprint); + params.set("alpn", this.stream.tls.alpn); + if(this.stream.tls.settings.allowInsecure){ + params.set("allowInsecure", "1"); + } + if (!ObjectUtil.isEmpty(this.stream.tls.server)){ + params.set("sni", this.stream.tls.server); + } + } + } + + let password = new Array(); if (this.isSS2022) password.push(settings.password); - if (this.isSSMultiUser) password.push(settings.shadowsockses[clientIndex].password); + if (this.isSSMultiUser) password.push(clientPassword); - let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${this.port}`; + let link = `ss://${safeBase64(settings.method + ':' + password.join(':'))}@${address}:${port}`; const url = new URL(link); for (const [key, value] of params) { url.searchParams.set(key, value) @@ -1457,9 +1445,8 @@ class Inbound extends XrayCommonClass { return url.toString(); } - genTrojanLink(address = '', remark = '', clientIndex = 0) { - let settings = this.settings; - const port = this.port; + genTrojanLink(address = '', port=this.port, forceTls, remark = '', clientPassword) { + const security = forceTls == 'same' ? this.stream.security : forceTls; const type = this.stream.network; const params = new Map(); params.set("type", this.stream.network); @@ -1511,48 +1498,41 @@ class Inbound extends XrayCommonClass { break; } - if (this.tls) { + if (security === 'tls') { params.set("security", "tls"); - params.set("fp" , this.stream.tls.settings.fingerprint); - params.set("alpn", this.stream.tls.alpn); - if(this.stream.tls.settings.allowInsecure){ - params.set("allowInsecure", "1"); + if (this.stream.isTls){ + params.set("fp" , this.stream.tls.settings.fingerprint); + params.set("alpn", this.stream.tls.alpn); + if(this.stream.tls.settings.allowInsecure){ + params.set("allowInsecure", "1"); + } + if (!ObjectUtil.isEmpty(this.stream.tls.server)){ + params.set("sni", this.stream.tls.server); + } } - if (!ObjectUtil.isEmpty(this.stream.tls.server)) { - address = this.stream.tls.server; - } - if (this.stream.tls.settings.serverName !== ''){ - params.set("sni", this.stream.tls.settings.serverName); - } } - else if (this.reality) { + else if (security === 'reality') { params.set("security", "reality"); - params.set("fp", this.stream.reality.settings.fingerprint); params.set("pbk", this.stream.reality.settings.publicKey); + params.set("fp", this.stream.reality.settings.fingerprint); if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) { params.set("sni", this.stream.reality.serverNames.split(",")[0]); } if (this.stream.reality.shortIds.length > 0) { params.set("sid", this.stream.reality.shortIds.split(",")[0]); } - if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) { - address = this.stream.reality.settings.serverName; - } if (!ObjectUtil.isEmpty(this.stream.reality.settings.spiderX)) { params.set("spx", this.stream.reality.settings.spiderX); } } - else if (this.xtls) { + else if (security === 'xtls') { params.set("security", "xtls"); params.set("alpn", this.stream.xtls.alpn); if(this.stream.xtls.settings.allowInsecure){ params.set("allowInsecure", "1"); } - if (!ObjectUtil.isEmpty(this.stream.xtls.server)) { - address = this.stream.xtls.server; - } if (this.stream.xtls.settings.serverName !== ''){ params.set("sni", this.stream.xtls.settings.serverName); } @@ -1563,7 +1543,7 @@ class Inbound extends XrayCommonClass { params.set("security", "none"); } - const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}`; + const link = `trojan://${clientPassword}@${address}:${port}`; const url = new URL(link); for (const [key, value] of params) { url.searchParams.set(key, value) @@ -1572,38 +1552,55 @@ class Inbound extends XrayCommonClass { return url.toString(); } - genLink(address='', remark='', clientIndex=0) { + genLink(address='', port=this.port, forceTls='same', remark='', client) { switch (this.protocol) { - case Protocols.VMESS: - return this.genVmessLink(address, remark, clientIndex); + case Protocols.VMESS: + return this.genVmessLink(address, port, forceTls, remark, client.id); case Protocols.VLESS: - return this.genVLESSLink(address, remark, clientIndex); + return this.genVLESSLink(address, port, forceTls, remark, client.id, client.flow); case Protocols.SHADOWSOCKS: - return this.genSSLink(address, remark, clientIndex); + return this.genSSLink(address, port, forceTls, remark, this.isSSMultiUser ? client.password : ''); case Protocols.TROJAN: - return this.genTrojanLink(address, remark, clientIndex); + return this.genTrojanLink(address, port, forceTls, remark, client.password); default: return ''; } } - genInboundLinks(address = '', remark = '') { - let link = ''; - switch (this.protocol) { - case Protocols.VMESS: - case Protocols.VLESS: - case Protocols.TROJAN: - case Protocols.SHADOWSOCKS: - JSON.parse(this.settings).clients.forEach((client,index) => { - if(this.tls && !ObjectUtil.isArrEmpty(this.stream.tls.settings.domains)){ - this.stream.tls.settings.domains.forEach((domain) => { - link += this.genLink(domain.domain, [remark, client.email, domain.remark].filter(x => x.length > 0).join('-'), index) + '\r\n'; - }); - } else { - link += this.genLink(address, [remark, client.email].filter(x => x.length > 0).join('-'), index) + '\r\n'; - } + genAllLinks(remark='', client){ + let result = []; + let email = client ? client.email : ''; + let addr = !ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0" ? this.listen : location.hostname; + let port = this.port + if(ObjectUtil.isArrEmpty(this.stream.externalProxy)){ + let r = [remark, email].filter(x => x.length > 0).join('-'); + result.push({ + remark: r, + link: this.genLink(addr, port, 'same', r, client) + }); + } else { + this.stream.externalProxy.forEach((ep) => { + let r = [remark, email, ep.remark].filter(x => x.length > 0).join('-') + result.push({ + remark: r, + link: this.genLink(ep.dest, ep.port, ep.forceTls, r, client) }); - return link; - default: return ''; + }); + } + return result; + } + + genInboundLinks(remark = '') { + if(this.clients){ + let links = []; + this.clients.forEach((client) => { + genAllLinks(remark,client).forEach(l => { + links.push(l.link); + }) + }); + return links.join('\r\n'); + } else { + if(this.protocol == Protocols.SHADOWSOCKS && !this.isSSMultiUser) return this.genSSLink(this.listen, this.port, remark); + return ''; } } diff --git a/web/html/common/qrcode_modal.html b/web/html/common/qrcode_modal.html index 85ada3d4..0df369ff 100644 --- a/web/html/common/qrcode_modal.html +++ b/web/html/common/qrcode_modal.html @@ -22,39 +22,25 @@ const qrModal = { title: '', - clientIndex: 0, - inbound: new Inbound(), dbInbound: new DBInbound(), client: null, qrcodes: [], clipboard: null, visible: false, subId: '', - show: function (title = '', dbInbound = new DBInbound(), clientIndex = 0) { + show: function (title = '', dbInbound, client) { this.title = title; - this.clientIndex = clientIndex; this.dbInbound = dbInbound; this.inbound = dbInbound.toInbound(); - settings = JSON.parse(this.inbound.settings); - this.client = settings.clients[clientIndex]; - remark = [this.dbInbound.remark, ( this.client ? this.client.email : '')].filter(Boolean).join('-'); - address = this.dbInbound.address; + this.client = client; this.subId = ''; this.qrcodes = []; - if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) { - this.inbound.stream.tls.settings.domains.forEach((domain) => { - remarkText = [remark, domain.remark].filter(Boolean).join('-'); - this.qrcodes.push({ - remark: remarkText, - link: this.inbound.genLink(domain.domain, remarkText, clientIndex) - }); - }); - } else { + this.inbound.genAllLinks(this.dbInbound.remark, client).forEach(l => { this.qrcodes.push({ - remark: remark, - link: this.inbound.genLink(address, remark, clientIndex) + remark: l.remark, + link: l.link }); - } + }); this.visible = true; }, close: function () { diff --git a/web/html/xui/client_modal.html b/web/html/xui/client_modal.html index 265a2cac..1bc48a6a 100644 --- a/web/html/xui/client_modal.html +++ b/web/html/xui/client_modal.html @@ -38,7 +38,7 @@ this.isEdit = isEdit; this.dbInbound = new DBInbound(dbInbound); this.inbound = dbInbound.toInbound(); - this.clients = this.getClients(this.inbound.protocol, this.inbound.settings); + this.clients = this.inbound.clients; this.index = index === null ? this.clients.length : index; this.delayedStart = false; if (isEdit) { @@ -51,16 +51,7 @@ } this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email); this.confirm = confirm; - }, - getClients(protocol, clientSettings) { - switch (protocol) { - case Protocols.VMESS: return clientSettings.vmesses; - case Protocols.VLESS: return clientSettings.vlesses; - case Protocols.TROJAN: return clientSettings.trojans; - case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses; - default: return null; - } - }, + }, getClientId(protocol, client) { switch (protocol) { case Protocols.TROJAN: return client.password; diff --git a/web/html/xui/form/inbound.html b/web/html/xui/form/inbound.html index 40a96d1e..b46ce951 100644 --- a/web/html/xui/form/inbound.html +++ b/web/html/xui/form/inbound.html @@ -96,6 +96,7 @@ {{template "form/streamSettings"}} + {{template "form/externalProxy" }} diff --git a/web/html/xui/form/stream/external_proxy.html b/web/html/xui/form/stream/external_proxy.html new file mode 100644 index 00000000..bb80070c --- /dev/null +++ b/web/html/xui/form/stream/external_proxy.html @@ -0,0 +1,32 @@ +{{define "form/externalProxy"}} + + + + + + + + + + + + + + + {{ i18n "pages.inbounds.same" }} + {{ i18n "none" }} + TLS + + + + + + + + + - + + + + + +{{end}} diff --git a/web/html/xui/form/tls_settings.html b/web/html/xui/form/tls_settings.html index eb201862..a77bec40 100644 --- a/web/html/xui/form/tls_settings.html +++ b/web/html/xui/form/tls_settings.html @@ -24,26 +24,6 @@ - - - - - - Domains: - + - - - - - - - - - - - - - - auto @@ -61,7 +41,7 @@ - + - - - - + @@ -179,9 +156,6 @@ style="width: 135px" :dropdown-class-name="themeSwitcher.currentTheme"> [[ key ]] - - - diff --git a/web/html/xui/inbound_info_modal.html b/web/html/xui/inbound_info_modal.html index 554d10a3..1390bbcd 100644 --- a/web/html/xui/inbound_info_modal.html +++ b/web/html/xui/inbound_info_modal.html @@ -265,27 +265,10 @@ this.index = index; this.inbound = dbInbound.toInbound(); this.dbInbound = new DBInbound(dbInbound); - this.settings = JSON.parse(this.inbound.settings); - this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null; - this.isExpired = this.inbound.isExpiry(index); - this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : []; - remark = [this.dbInbound.remark, ( this.clientSettings ? this.clientSettings.email : '')].filter(Boolean).join('-'); - address = this.dbInbound.address; - this.links = []; - if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) { - this.inbound.stream.tls.settings.domains.forEach((domain) => { - remarkText = [remark, domain.remark].filter(Boolean).join('-'); - this.links.push({ - remark: remarkText, - link: this.inbound.genLink(domain.domain, remarkText, index) - }); - }); - } else { - this.links.push({ - remark: remark, - link: this.inbound.genLink(address, remark, index) - }); - } + this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null; + this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index): this.dbInbound.isExpiry; + this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : []; + this.links = this.inbound.genAllLinks(this.dbInbound.remark, this.clientSettings); if (this.clientSettings) { if (this.clientSettings.subId) { this.subLink = this.genSubLink(this.clientSettings.subId); diff --git a/web/html/xui/inbound_modal.html b/web/html/xui/inbound_modal.html index df42225c..4ef8f2d6 100644 --- a/web/html/xui/inbound_modal.html +++ b/web/html/xui/inbound_modal.html @@ -43,15 +43,6 @@ loading(loading) { inModal.confirmLoading = loading; }, - getClients(protocol, clientSettings) { - switch (protocol) { - case Protocols.VMESS: return clientSettings.vmesses; - case Protocols.VLESS: return clientSettings.vlesses; - case Protocols.TROJAN: return clientSettings.trojans; - case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses; - default: return null; - } - }, }; new Vue({ @@ -70,7 +61,7 @@ return inModal.isEdit; }, get client() { - return inModal.getClients(this.inbound.protocol, this.inbound.settings)[0]; + return inModal.inbound.clients[0]; }, get delayedExpireDays() { return this.client && this.client.expiryTime < 0 ? this.client.expiryTime / -86400000 : 0; @@ -78,16 +69,19 @@ set delayedExpireDays(days) { this.client.expiryTime = -86400000 * days; }, - get multiDomain() { - return this.inbound.stream.tls.settings.domains.length > 0; + get externalProxy() { + return this.inbound.stream.externalProxy.length > 0; }, - set multiDomain(value) { + set externalProxy(value) { if (value) { - inModal.inbound.stream.tls.server = ""; - inModal.inbound.stream.tls.settings.domains = [{ remark: "", domain: window.location.hostname }]; + inModal.inbound.stream.externalProxy = [{ + forceTls: "same", + dest: window.location.hostname, + port: inModal.inbound.port, + remark: "" + }]; } else { - inModal.inbound.stream.tls.server = ""; - inModal.inbound.stream.tls.settings.domains = []; + inModal.inbound.stream.externalProxy = []; } } }, diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html index e73e5f5d..13808ac9 100644 --- a/web/html/xui/inbounds.html +++ b/web/html/xui/inbounds.html @@ -618,7 +618,7 @@ }, getClientCounts(dbInbound, inbound) { let clientCount = 0, active = [], deactive = [], depleted = [], expiring = [], online = []; - clients = this.getClients(dbInbound.protocol, inbound.settings); + clients = inbound.clients; clientStats = dbInbound.clientStats now = new Date().getTime() if (clients) { @@ -968,15 +968,6 @@ this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`); } }, - getClients(protocol, clientSettings) { - switch (protocol) { - case Protocols.VMESS: return clientSettings.vmesses; - case Protocols.VLESS: return clientSettings.vlesses; - case Protocols.TROJAN: return clientSettings.trojans; - case Protocols.SHADOWSOCKS: return clientSettings.shadowsockses; - default: return null; - } - }, getClientId(protocol, client) { switch (protocol) { case Protocols.TROJAN: return client.password; @@ -996,8 +987,9 @@ newDbInbound.listen = rootInbound.listen; newDbInbound.port = rootInbound.port; newInbound = newDbInbound.toInbound(); - newInbound.stream.security = 'tls'; + newInbound.stream.security = rootInbound.stream.security; newInbound.stream.tls = rootInbound.stream.tls; + newInbound.stream.externalProxy = rootInbound.stream.externalProxy; newDbInbound.streamSettings = newInbound.stream.toString(); } } @@ -1005,17 +997,17 @@ }, showQrcode(dbInboundId, client) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); - inbound = dbInbound.toInbound(); - clients = this.getClients(dbInbound.protocol, inbound.settings); - index = this.findIndexOfClient(dbInbound.protocol, clients, client); newDbInbound = this.checkFallback(dbInbound); - qrModal.show('{{ i18n "qrCode"}}', newDbInbound, index); + qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client); }, showInfo(dbInboundId, client) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); - inbound = dbInbound.toInbound(); - clients = this.getClients(dbInbound.protocol, inbound.settings); - index = this.findIndexOfClient(dbInbound.protocol, clients, client); + index=0; + if (dbInbound.isMultiUser()){ + inbound = dbInbound.toInbound(); + clients = inbound.clients; + index = this.findIndexOfClient(dbInbound.protocol, clients, client); + } newDbInbound = this.checkFallback(dbInbound); infoModal.show(newDbInbound, index); }, @@ -1027,7 +1019,7 @@ this.loading() dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); inbound = dbInbound.toInbound(); - clients = this.getClients(dbInbound.protocol, inbound.settings); + clients = inbound.clients; index = this.findIndexOfClient(dbInbound.protocol, clients, client); clients[index].enable = !clients[index].enable; clientId = this.getClientId(dbInbound.protocol, clients[index]); @@ -1041,15 +1033,7 @@ } }, getInboundClients(dbInbound) { - if (dbInbound.protocol == Protocols.VLESS) { - return dbInbound.toInbound().settings.vlesses; - } else if (dbInbound.protocol == Protocols.VMESS) { - return dbInbound.toInbound().settings.vmesses; - } else if (dbInbound.protocol == Protocols.TROJAN) { - return dbInbound.toInbound().settings.trojans; - } else if (dbInbound.protocol == Protocols.SHADOWSOCKS) { - return dbInbound.toInbound().settings.shadowsockses; - } + return dbInbound.toInbound().clients; }, resetClientTraffic(client, dbInboundId, confirmation = true) { if (confirmation){ @@ -1179,11 +1163,11 @@ txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks, newDbInbound.remark); }, exportAllLinks() { - let copyText = ''; + let copyText = []; for (const dbInbound of this.dbInbounds) { - copyText += dbInbound.genInboundLinks; + copyText.push(dbInbound.genInboundLinks); } - txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText, 'All-Inbounds'); + txtModal.show('{{ i18n "pages.inbounds.export"}}', copyText.join('\r\n'), 'All-Inbounds'); }, async startDataRefreshLoop() { while (this.isRefreshEnabled) { diff --git a/web/service/inbound.go b/web/service/inbound.go index 5ff88d38..58a3de9f 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -1795,6 +1795,46 @@ func (s *InboundService) MigrationRequirements() { // Remove orphaned traffics tx.Where("inbound_id = 0").Delete(xray.ClientTraffic{}) + + // Migrate old MultiDomain to External Proxy + var externalProxy []struct { + Id int + Port int + StreamSettings []byte + } + err = tx.Raw(`select id, port, stream_settings + from inbounds + WHERE protocol in ('vmess','vless','trojan') + AND json_extract(stream_settings, '$.security') = 'tls' + AND json_extract(stream_settings, '$.tlsSettings.settings.domains') IS NOT NULL`).Scan(&externalProxy).Error + if err != nil || len(externalProxy) == 0 { + return + } + + for _, ep := range externalProxy { + var reverses interface{} + var stream map[string]interface{} + json.Unmarshal(ep.StreamSettings, &stream) + if tlsSettings, ok := stream["tlsSettings"].(map[string]interface{}); ok { + if settings, ok := tlsSettings["settings"].(map[string]interface{}); ok { + if domains, ok := settings["domains"].([]interface{}); ok { + for _, domain := range domains { + if domainMap, ok := domain.(map[string]interface{}); ok { + domainMap["forceTls"] = "same" + domainMap["port"] = ep.Port + domainMap["dest"] = domainMap["domain"].(string) + delete(domainMap, "domain") + } + } + } + reverses = settings["domains"] + delete(settings, "domains") + } + } + stream["externalProxy"] = reverses + newStream, _ := json.MarshalIndent(stream, " ", " ") + tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream) + } } func (s *InboundService) MigrateDB() { diff --git a/web/service/xray.go b/web/service/xray.go index 1d046a44..7233cec5 100644 --- a/web/service/xray.go +++ b/web/service/xray.go @@ -135,19 +135,24 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) { inbound.Settings = string(modifiedSettings) } - // Unmarshal stream JSON - var stream map[string]interface{} - json.Unmarshal([]byte(inbound.StreamSettings), &stream) + if len(inbound.StreamSettings) > 0 { + // Unmarshal stream JSON + var stream map[string]interface{} + json.Unmarshal([]byte(inbound.StreamSettings), &stream) - // Remove the "settings" field under "tlsSettings" and "realitySettings" - tlsSettings, ok1 := stream["tlsSettings"].(map[string]interface{}) - realitySettings, ok2 := stream["realitySettings"].(map[string]interface{}) - if ok1 || ok2 { - if ok1 { - delete(tlsSettings, "settings") - } else if ok2 { - delete(realitySettings, "settings") + // Remove the "settings" field under "tlsSettings" and "realitySettings" + tlsSettings, ok1 := stream["tlsSettings"].(map[string]interface{}) + realitySettings, ok2 := stream["realitySettings"].(map[string]interface{}) + if ok1 || ok2 { + if ok1 { + delete(tlsSettings, "settings") + } else if ok2 { + delete(realitySettings, "settings") + } } + + delete(stream, "externalProxy") + newStream, err := json.MarshalIndent(stream, "", " ") if err != nil { return nil, err diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 86d8ef0b..2aa31d4d 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -176,6 +176,7 @@ "telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot or use '/id' command in bot )" "subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations" "info" = "Info" +"same" = "Same" [pages.client] "add" = "Add Client" diff --git a/web/translation/translate.es_ES.toml b/web/translation/translate.es_ES.toml index dd189aae..e4053759 100644 --- a/web/translation/translate.es_ES.toml +++ b/web/translation/translate.es_ES.toml @@ -176,6 +176,7 @@ "telegramDesc" = "Utiliza el ID de Telegram sin @ o los IDs de chat (puedes obtenerlo aquí @userinfobot o usando el comando '/id' en el bot)." "subscriptionDesc" = "Puedes encontrar tu enlace de suscripción en Detalles, también puedes usar el mismo nombre para varias configuraciones." "info" = "Info" +"same" = "misma" [pages.client] "add" = "Agregar Cliente" diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index 8d5001bd..20085215 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -176,6 +176,7 @@ "telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)" "subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید" "info" = "اطلاعات" +"same" = "همسان" [pages.client] "add" = "کاربر جدید" diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 460dfa36..f37e4f2c 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -176,6 +176,7 @@ "telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)" "subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее', также вы можете использовать одно и то же имя для нескольких конфигураций" "info" = "Информация" +"same" = "Тот же" [pages.client] "add" = "Добавить пользователя" diff --git a/web/translation/translate.vi_VN.toml b/web/translation/translate.vi_VN.toml index 6c51b0e8..79691dfb 100644 --- a/web/translation/translate.vi_VN.toml +++ b/web/translation/translate.vi_VN.toml @@ -176,6 +176,7 @@ "telegramDesc" = "Sử dụng Telegram ID mà không cần ký hiệu @ hoặc chat IDs (bạn có thể nhận được nó ở đây @userinfobot hoặc sử dụng lệnh '/id' trong bot)" "subscriptionDesc" = "Bạn có thể tìm liên kết đăng ký của mình trong Chi tiết, cũng như bạn có thể sử dụng cùng tên cho nhiều cấu hình khác nhau" "info" = "Thông tin" +"same" = "Giống nhau" [pages.client] "add" = "Thêm Client" diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml index 2eeced8f..a32ac8c6 100644 --- a/web/translation/translate.zh_Hans.toml +++ b/web/translation/translate.zh_Hans.toml @@ -176,6 +176,7 @@ "telegramDesc" = "使用 Telegram ID,不包含 @ 符号或聊天 ID(可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)" "subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称" "info" = "信息" +"same" = "相同" [pages.client] "add" = "添加客户端"