Pārlūkot izejas kodu

update - shadowsocks

Co-Authored-By: Alireza Ahmadi <[email protected]>
MHSanaei 1 gadu atpakaļ
vecāks
revīzija
c2e9ee3665

+ 84 - 25
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
-}
+}

+ 72 - 6
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) {

+ 1 - 1
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 = [];

+ 3 - 1
web/html/xui/form/protocol/shadowsocks.html

@@ -1,5 +1,6 @@
 {{define "form/shadowsocks"}}
 <a-form layout="inline" style="padding: 10px 0px;">
+    <template v-if="inbound.isSSMultiUser">
     <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
         <a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
             <a-form-item>
@@ -106,10 +107,11 @@
             </table>
         </a-collapse-panel>
     </a-collapse>
+</template>
 </a-form>
 <a-form layout="inline">
     <a-form-item label='{{ i18n "encryption" }}'>
-        <a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass">
+        <a-select v-model="inbound.settings.method" style="width: 250px;" :dropdown-class-name="themeSwitcher.darkCardClass" @change="SSMethodChange">
             <a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
         </a-select>
     </a-form-item>

+ 14 - 1
web/html/xui/inbound_info_modal.html

@@ -179,6 +179,19 @@
                 <td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
             </tr>
         </table>
+        <template v-if="inbound.protocol == Protocols.SHADOWSOCKS && !inbound.isSSMultiUser">
+            <a-divider>URL</a-divider>
+            <a-row v-for="(link,index) in infoModal.links">
+                <a-col :span="22"><a-tag color="cyan">[[ link.remark ]]</a-tag><br />[[ link.link ]]</a-col>
+                <a-col :span="2" style="text-align: right;">
+                    <a-tooltip title='{{ i18n "copy" }}'>
+                        <button class="ant-btn ant-btn-primary" :id="'copy-url-link-'+index" @click="copyToClipboard('copy-url-link-'+index, link.link)">
+                            <a-icon type="snippets"></a-icon>
+                        </button>
+                    </a-tooltip>
+                </a-col>
+            </a-row>
+        </template>
         <table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;">
             <tr>
                 <th>{{ i18n "pages.inbounds.targetAddress" }}</th>
@@ -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)) {

+ 11 - 12
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;

+ 11 - 4
web/html/xui/inbounds.html

@@ -131,7 +131,11 @@
                                             <a-icon type="edit"></a-icon>
                                             {{ i18n "edit" }}
                                         </a-menu-item>
-                                        <template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.isSS">
+                                        <a-menu-item key="qrcode" v-if="dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser">
+                                            <a-icon type="qrcode"></a-icon>
+                                            {{ i18n "qrCode" }}
+                                        </a-menu-item>
+                                        <template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess || dbInbound.toInbound().isSSMultiUser">
                                             <a-menu-item key="addClient">
                                                 <a-icon type="user-add"></a-icon>
                                                 {{ i18n "pages.client.add"}}
@@ -255,7 +259,7 @@
                                     {{template "client_table"}}
                                 </a-table>
                                 <a-table
-                                v-else-if="record.protocol === Protocols.TROJAN || record.protocol === Protocols.SHADOWSOCKS"
+                                v-else-if="record.protocol === Protocols.TROJAN || record.toInbound().isSSMultiUser"
                                 :row-key="client => client.id"
                                 :columns="innerTrojanColumns"
                                 :data-source="getInboundClients(record)"
@@ -274,7 +278,6 @@
 {{template "js" .}}
 {{template "component/themeSwitcher" .}}
 <script>
-
     const columns = [{
         title: '{{ i18n "pages.inbounds.operate" }}',
         align: 'center',
@@ -357,7 +360,7 @@
             trafficDiff: 0,
             defaultCert: '',
             defaultKey: '',
-            clientCount: {},
+            clientCount: [],
             isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
             refreshing: false,
             refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
@@ -409,12 +412,16 @@
             setInbounds(dbInbounds) {
                 this.inbounds.splice(0);
                 this.dbInbounds.splice(0);
+                this.clientCount.splice(0);
                 for (const inbound of dbInbounds) {
                     const dbInbound = new DBInbound(inbound);
                     to_inbound = dbInbound.toInbound()
                     this.inbounds.push(to_inbound);
                     this.dbInbounds.push(dbInbound);
                     if ([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(inbound.protocol)) {
+                        if (inbound.protocol === Protocols.SHADOWSOCKS && (!to_inbound.isSSMultiUser)) {
+                            continue;
+                        }
                         this.clientCount[inbound.id] = this.getClientCounts(inbound, to_inbound);
                     }
                 }