diff --git a/sub/subService.go b/sub/subService.go index 79cc881e91..a4eef64728 100644 --- a/sub/subService.go +++ b/sub/subService.go @@ -3,17 +3,18 @@ package sub import ( "encoding/base64" "fmt" - "time" - ptime "github.com/yaa110/go-persian-calendar" "net/url" "strings" + "time" "x-ui/database" "x-ui/database/model" "x-ui/logger" + "x-ui/util/common" "x-ui/web/service" "x-ui/xray" - "x-ui/util/common" + "github.com/goccy/go-json" + ptime "github.com/yaa110/go-persian-calendar" ) type SubService struct { @@ -57,7 +58,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, []string, err } for _, client := range clients { if client.Enable && client.SubID == subId { - link := s.getLink(inbound, client.Email,client.ExpiryTime) + link := s.getLink(inbound, client.Email, client.ExpiryTime) result = append(result, link) clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email)) } @@ -143,7 +144,7 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string, expiryTi } remainedTraffic := s.getRemainedTraffic(email) - expiryTimeString := getExpiryTime(expiryTime) + expiryTimeString := getExpiryTime(expiryTime) remark := fmt.Sprintf("%s: %s- %s", email, remainedTraffic, expiryTimeString) obj := map[string]interface{}{ @@ -456,7 +457,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string, expiryTi url.RawQuery = q.Encode() remainedTraffic := s.getRemainedTraffic(email) - expiryTimeString := getExpiryTime(expiryTime) + expiryTimeString := getExpiryTime(expiryTime) remark := fmt.Sprintf("%s: %s- %s", email, remainedTraffic, expiryTimeString) @@ -668,7 +669,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string, expiryT url.RawQuery = q.Encode() remainedTraffic := s.getRemainedTraffic(email) - expiryTimeString := getExpiryTime(expiryTime) + expiryTimeString := getExpiryTime(expiryTime) remark := fmt.Sprintf("%s: %s- %s", email, remainedTraffic, expiryTimeString) @@ -695,6 +696,8 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, ex if inbound.Protocol != model.Shadowsocks { return "" } + var stream map[string]interface{} + json.Unmarshal([]byte(inbound.StreamSettings), &stream) clients, _ := s.inboundService.GetClients(inbound) var settings map[string]interface{} @@ -708,13 +711,69 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string, ex break } } + streamNetwork := stream["network"].(string) + params := make(map[string]string) + params["type"] = streamNetwork + + switch streamNetwork { + case "tcp": + tcp, _ := stream["tcpSettings"].(map[string]interface{}) + header, _ := tcp["header"].(map[string]interface{}) + typeStr, _ := header["type"].(string) + if typeStr == "http" { + request := header["request"].(map[string]interface{}) + requestPath, _ := request["path"].([]interface{}) + params["path"] = requestPath[0].(string) + headers, _ := request["headers"].(map[string]interface{}) + params["host"] = searchHost(headers) + params["headerType"] = "http" + } + case "kcp": + kcp, _ := stream["kcpSettings"].(map[string]interface{}) + header, _ := kcp["header"].(map[string]interface{}) + params["headerType"] = header["type"].(string) + params["seed"] = kcp["seed"].(string) + case "ws": + ws, _ := stream["wsSettings"].(map[string]interface{}) + params["path"] = ws["path"].(string) + headers, _ := ws["headers"].(map[string]interface{}) + params["host"] = searchHost(headers) + case "http": + http, _ := stream["httpSettings"].(map[string]interface{}) + params["path"] = http["path"].(string) + params["host"] = searchHost(http) + case "quic": + quic, _ := stream["quicSettings"].(map[string]interface{}) + params["quicSecurity"] = quic["security"].(string) + params["key"] = quic["key"].(string) + header := quic["header"].(map[string]interface{}) + params["headerType"] = header["type"].(string) + case "grpc": + grpc, _ := stream["grpcSettings"].(map[string]interface{}) + params["serviceName"] = grpc["serviceName"].(string) + if grpc["multiMode"].(bool) { + params["mode"] = "multi" + } + } + encPart := fmt.Sprintf("%s:%s:%s", method, inboundPassword, clients[clientIndex].Password) - - remainedTraffic := s.getRemainedTraffic(clients[clientIndex].Email) - expiryTimeString := getExpiryTime(expiryTime) + link := fmt.Sprintf("ss://%s@%s:%d", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port) + url, _ := url.Parse(link) + q := url.Query() + + for k, v := range params { + q.Add(k, v) + } + + // Set the new query values on the URL + url.RawQuery = q.Encode() + + remainedTraffic := s.getRemainedTraffic(email) + expiryTimeString := getExpiryTime(expiryTime) - remark := fmt.Sprintf("%s: %s- %s", clients[clientIndex].Email, remainedTraffic ,expiryTimeString) - return fmt.Sprintf("ss://%s@%s:%d#%s", base64.StdEncoding.EncodeToString([]byte(encPart)), address, inbound.Port, remark) + remark := fmt.Sprintf("%s: %s- %s", clients[clientIndex].Email, remainedTraffic, expiryTimeString) + url.Fragment = remark + return url.String() } func searchKey(data interface{}, key string) (interface{}, bool) { @@ -759,26 +818,26 @@ func searchHost(headers interface{}) string { return "" } -func getExpiryTime(expiryTime int64) string{ +func getExpiryTime(expiryTime int64) string { now := time.Now().Unix() expiryString := "" timeDifference := expiryTime/1000 - now - + if expiryTime == 0 { - expiryString = "♾ ⏳" - } else if timeDifference > 172800 { - expiryString = fmt.Sprintf("%s ⏳", ptime.Unix((expiryTime / 1000), 0).Format("yy-MM-dd hh:mm")) - } else if expiryTime < 0 { - expiryString = fmt.Sprintf("%d ⏳", expiryTime/-86400000) - } else { - expiryString = fmt.Sprintf("%s %d ⏳", "ساعت", timeDifference/3600) - } + expiryString = "♾ ⏳" + } else if timeDifference > 172800 { + expiryString = fmt.Sprintf("%s ⏳", ptime.Unix((expiryTime/1000), 0).Format("yy-MM-dd hh:mm")) + } else if expiryTime < 0 { + expiryString = fmt.Sprintf("%d ⏳", expiryTime/-86400000) + } else { + expiryString = fmt.Sprintf("%s %d ⏳", "ساعت", timeDifference/3600) + } return expiryString } -func (s *SubService) getRemainedTraffic( email string) string{ +func (s *SubService) getRemainedTraffic(email string) string { traffic, err := s.inboundService.GetClientTrafficByEmail(email) if err != nil { logger.Warning(err) @@ -788,8 +847,8 @@ func (s *SubService) getRemainedTraffic( email string) string{ if traffic.Total == 0 { remainedTraffic = "♾ 📊" } else { - remainedTraffic = fmt.Sprintf("%s%s" ,common.FormatTraffic(traffic.Total-(traffic.Up+traffic.Down)), "📊") + remainedTraffic = fmt.Sprintf("%s%s", common.FormatTraffic(traffic.Total-(traffic.Up+traffic.Down)), "📊") } return remainedTraffic -} \ No newline at end of file +} diff --git a/web/assets/js/model/xray.js b/web/assets/js/model/xray.js index 64931758c5..fe46e85f1f 100644 --- a/web/assets/js/model/xray.js +++ b/web/assets/js/model/xray.js @@ -16,8 +16,12 @@ const VmessMethods = { }; const SSMethods = { - BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm', - BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm', + CHACHA20_POLY1305: 'chacha20-poly1305', + AES_256_GCM: 'aes-256-gcm', + AES_128_GCM: 'aes-128-gcm', + BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm', + BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm', + BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305', }; const XTLS_FLOW_CONTROL = { @@ -511,7 +515,8 @@ class TlsStreamSettings extends XrayCommonClass { } if (!ObjectUtil.isEmpty(json.settings)) { - settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName, json.settings.domains); } + settings = new TlsStreamSettings.Settings(json.settings.allowInsecure , json.settings.fingerprint, json.settings.serverName, json.settings.domains); + } return new TlsStreamSettings( json.serverName, json.minVersion, @@ -980,7 +985,6 @@ class Inbound extends XrayCommonClass { } } - //for Reality get reality() { return this.stream.security === 'reality'; } @@ -1034,6 +1038,9 @@ class Inbound extends XrayCommonClass { return ""; } } + get isSSMultiUser() { + return [SSMethods.BLAKE3_AES_128_GCM,SSMethods.BLAKE3_AES_256_GCM].includes(this.method); + } get serverName() { if (this.stream.isTls || this.stream.isXtls || this.stream.isReality) { @@ -1103,7 +1110,7 @@ class Inbound extends XrayCommonClass { return this.settings.trojans[index].expiryTime < new Date().getTime(); return false case Protocols.SHADOWSOCKS: - if(this.settings.shadowsockses[index].expiryTime > 0) + if(this.settings.shadowsockses.length > 0 && this.settings.shadowsockses[index].expiryTime > 0) return this.settings.shadowsockses[index].expiryTime < new Date().getTime(); return false default: @@ -1184,6 +1191,7 @@ class Inbound extends XrayCommonClass { case Protocols.VMESS: case Protocols.VLESS: case Protocols.TROJAN: + case Protocols.SHADOWSOCKS: return true; default: return false; @@ -1410,8 +1418,66 @@ class Inbound extends XrayCommonClass { genSSLink(address='', remark='', clientIndex = 0) { let settings = this.settings; const port = this.port; + const type = this.stream.network; + const params = new Map(); + params.set("type", this.stream.network); + switch (type) { + case "tcp": + const tcp = this.stream.tcp; + if (tcp.type === 'http') { + const request = tcp.request; + params.set("path", request.path.join(',')); + const index = request.headers.findIndex(header => header.name.toLowerCase() === 'host'); + if (index >= 0) { + const host = request.headers[index].value; + params.set("host", host); + } + params.set("headerType", 'http'); + } + break; + case "kcp": + const kcp = this.stream.kcp; + params.set("headerType", kcp.type); + params.set("seed", kcp.seed); + break; + case "ws": + const ws = this.stream.ws; + params.set("path", ws.path); + const index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host'); + if (index >= 0) { + const host = ws.headers[index].value; + params.set("host", host); + } + break; + case "http": + const http = this.stream.http; + params.set("path", http.path); + params.set("host", http.host); + break; + case "quic": + const quic = this.stream.quic; + params.set("quicSecurity", quic.security); + params.set("key", quic.key); + params.set("headerType", quic.type); + break; + case "grpc": + const grpc = this.stream.grpc; + params.set("serviceName", grpc.serviceName); + if(grpc.multiMode){ + params.set("mode", "multi"); + } + break; + } - return 'ss://' + safeBase64(settings.method + ':' + settings.password + ':' +settings.shadowsockses[clientIndex].password) + '@' + address + ':' + this.port + '#' + encodeURIComponent(remark); + let clientPassword = this.isSSMultiUser ? ':' + settings.shadowsockses[clientIndex].password : ''; + + let link = `ss://${safeBase64(settings.method + ':' + settings.password + clientPassword)}@${address}:${this.port}`; + const url = new URL(link); + for (const [key, value] of params) { + url.searchParams.set(key, value) + } + url.hash = encodeURIComponent(remark); + return url.toString(); } genTrojanLink(address = '', remark = '', clientIndex = 0) { diff --git a/web/html/common/qrcode_modal.html b/web/html/common/qrcode_modal.html index 8edfa2de3b..12dd2060da 100644 --- a/web/html/common/qrcode_modal.html +++ b/web/html/common/qrcode_modal.html @@ -37,7 +37,7 @@ this.inbound = dbInbound.toInbound(); settings = JSON.parse(this.inbound.settings); this.client = settings.clients[clientIndex]; - remark = this.dbInbound.remark + "-" + this.client.email; + remark = this.dbInbound.remark + ( this.client ? "-" + this.client.email : ''); address = this.dbInbound.address; this.subId = ''; this.qrcodes = []; diff --git a/web/html/xui/form/protocol/shadowsocks.html b/web/html/xui/form/protocol/shadowsocks.html index 7af9637321..8e16b14364 100644 --- a/web/html/xui/form/protocol/shadowsocks.html +++ b/web/html/xui/form/protocol/shadowsocks.html @@ -1,5 +1,6 @@ {{define "form/shadowsocks"}} + - + [[ method ]] diff --git a/web/html/xui/inbound_info_modal.html b/web/html/xui/inbound_info_modal.html index 00cb5ce647..fae058ad5d 100644 --- a/web/html/xui/inbound_info_modal.html +++ b/web/html/xui/inbound_info_modal.html @@ -179,6 +179,19 @@ [[ inbound.settings.network ]] + @@ -251,7 +264,7 @@ 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.email; + remark = this.dbInbound.remark + ( this.clientSettings ? "-" + this.clientSettings.email : ''); address = this.dbInbound.address; this.links = []; if (this.inbound.tls && !ObjectUtil.isArrEmpty(this.inbound.stream.tls.settings.domains)) { diff --git a/web/html/xui/inbound_modal.html b/web/html/xui/inbound_modal.html index 60244be4ae..65988b14aa 100644 --- a/web/html/xui/inbound_modal.html +++ b/web/html/xui/inbound_modal.html @@ -54,23 +54,11 @@ }, }; - const protocols = { - VMESS: Protocols.VMESS, - VLESS: Protocols.VLESS, - TROJAN: Protocols.TROJAN, - SHADOWSOCKS: Protocols.SHADOWSOCKS, - DOKODEMO: Protocols.DOKODEMO, - SOCKS: Protocols.SOCKS, - HTTP: Protocols.HTTP, - }; - new Vue({ delimiters: ['[[', ']]'], el: '#inbound-modal', data: { inModal: inModal, - Protocols: protocols, - SSMethods: SSMethods, delayedStart: false, get inbound() { return inModal.inbound; @@ -117,6 +105,17 @@ }); } }, + SSMethodChange() { + if (this.inModal.inbound.isSSMultiUser) { + if (this.inModal.inbound.settings.shadowsockses.length ==0){ + this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()]; + } + } else { + if (this.inModal.inbound.settings.shadowsockses.length > 0){ + this.inModal.inbound.settings.shadowsockses = []; + } + } + }, setDefaultCertData(index) { inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert; inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey; diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html index 193c080b85..39c64aff56 100644 --- a/web/html/xui/inbounds.html +++ b/web/html/xui/inbounds.html @@ -131,7 +131,11 @@ {{ i18n "edit" }} -
{{ i18n "pages.inbounds.targetAddress" }}