Browse Source

luci-app-ssr-plus: Add `Xray: shadowsocks` import and subscribe.
说明:因暂无 Xray shadowsocks 的 `reality` 协议节点,故暂保留可以导入和订阅 reality 传输协议功能。

zxlhhyccc 1 tháng trước cách đây
mục cha
commit
0ec475e5ab

+ 80 - 62
.github/workflows/multi-arch-test-build.yml

@@ -1,6 +1,10 @@
 name: Test Build
 
 on:
+  push:
+    branches:
+      - master
+      - main
   pull_request:
     paths-ignore:
       - '**.md'
@@ -15,16 +19,22 @@ jobs:
         include:
           - arch: arm_cortex-a9_vfpv3-d16
             target: mvebu-cortexa9
+
           - arch: mips_24kc
             target: ath79-generic
+
           - arch: mipsel_24kc
             target: mt7621
+
           - arch: aarch64_cortex-a53
             target: mvebu-cortexa53
+
           - arch: arm_cortex-a15_neon-vfpv4
             target: armvirt-32
+
           - arch: i386_pentium-mmx
             target: x86-geode
+
           - arch: x86_64
             target: x86-64
 
@@ -39,17 +49,14 @@ jobs:
           large-packages: true
           docker-images: true
           swap-storage: true
-          apt: true
-          brew: true
-          port: true
 
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v4
         with:
           fetch-depth: 0
 
       - name: Determine branch name
         run: |
-          if [ -n "$GITHUB_BASE_REF" ]; then
+          if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
             BRANCH="${GITHUB_BASE_REF#refs/heads/}"
           else
             BRANCH="${GITHUB_REF#refs/heads/}"
@@ -59,111 +66,122 @@ jobs:
 
       - name: Determine changed packages
         run: |
-          if [ -n "$GITHUB_BASE_REF" ]; then
-            BASE_REF="origin/$GITHUB_BASE_REF"
+          # only detect packages with changes
+          PKG_ROOTS=$(find . -name Makefile | \
+            grep -v ".*/src/Makefile" | \
+            sed -e 's@./\(.*\)/Makefile@\1/@')
+          
+          if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
+            BASE_REF="origin/${BRANCH}"
           else
             BASE_REF="HEAD^"
           fi
           
-          PKG_ROOTS=$(find . -name Makefile | \
-            grep -E "/(src)?/Makefile" | \
-            sed -e 's@/Makefile@@' -e 's@^\./@@' | \
-            sort -u)
-          
-          CHANGES=$(git diff --diff-filter=d --name-only $BASE_REF..HEAD)
-          
+          CHANGES=$(git diff --diff-filter=d --name-only $BASE_REF HEAD)
+
           PACKAGES=""
           for ROOT in $PKG_ROOTS; do
             for CHANGE in $CHANGES; do
-              if [[ "$CHANGE" == "$ROOT/"* ]] || [[ "$CHANGE" == "$ROOT/Makefile" ]]; then
-                PACKAGE_NAME=$(echo "$ROOT" | awk -F/ '{print $NF}')
-                PACKAGES+="$PACKAGE_NAME "
+              if [[ "$CHANGE" == "$ROOT"* ]]; then
+                PACKAGE_NAME=$(echo "$ROOT" | sed -e 's@\(.*\)/@\1@')
+                PACKAGES="$PACKAGES $PACKAGE_NAME"
                 break
               fi
             done
           done
           
+          # fallback to test packages if nothing explicitly changes
           PACKAGES="${PACKAGES:-luci-app-ssr-plus}"
-          
-          echo "Building packages: $PACKAGES"
+          # clear spaces
+          PACKAGES=$(echo $PACKAGES | xargs)
+
+          echo "Building $PACKAGES"
           echo "PACKAGES=$PACKAGES" >> $GITHUB_ENV
 
       - name: Build
-        uses: immortalwrt/gh-action-sdk@v5
+        uses: immortalwrt/gh-action-sdk@v7
         env:
           ARCH: ${{ matrix.arch }}
           FEEDNAME: packages_ci
           V: s
-        with:
           target: ${{ matrix.target }}
           packages: ${{ env.PACKAGES }}
 
       - name: Move created packages to project dir
-        run: |
-          mkdir -p artifacts/packages
-          cp -f bin/packages/${{ matrix.arch }}/packages_ci/*.ipk artifacts/packages/ 2>/dev/null || true
-          cp -f bin/packages/${{ matrix.arch }}/packages_ci/Packages* artifacts/ 2>/dev/null || true
-
-      - name: Collect build logs
-        run: |
-          mkdir -p artifacts/logs
-          cp -f logs/* artifacts/logs/ 2>/dev/null || true
-          cp -f .config artifacts/ 2>/dev/null || true
-          cp -f tmp/.config* artifacts/ 2>/dev/null || true
+        run: cp bin/packages/${{ matrix.arch }}/packages_ci/*.ipk . || true
 
       - name: Collect metadata
         run: |
-          if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
-            PRNUMBER=$(echo $GITHUB_REF | awk -F/ '{print $3}')
+          MERGE_ID=$(git rev-parse --short HEAD)
+          
+          if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then
+            BASE_ID=$(git rev-parse --short origin/$BRANCH)
+            HEAD_ID=$(git rev-parse --short HEAD)
+            PRNUMBER=${GITHUB_REF_NAME%/merge}
           else
-            PRNUMBER=""
+            BASE_ID=$(git rev-parse --short HEAD^)
+            HEAD_ID=$MERGE_ID
+            PRNUMBER="push-$MERGE_ID"
           fi
           
-          MERGE_ID=$(git rev-parse --short HEAD)
           echo "MERGE_ID=$MERGE_ID" >> $GITHUB_ENV
+          echo "BASE_ID=$BASE_ID" >> $GITHUB_ENV
+          echo "HEAD_ID=$HEAD_ID" >> $GITHUB_ENV
           echo "PRNUMBER=$PRNUMBER" >> $GITHUB_ENV
-          echo "ARCHIVE_NAME=${{matrix.arch}}-PR${PRNUMBER}-$MERGE_ID" >> $GITHUB_ENV
+          echo "ARCHIVE_NAME=${{ matrix.arch }}-$PRNUMBER" >> $GITHUB_ENV
 
       - name: Generate metadata
         run: |
-          cat << _EOF_ > PKG-INFO
-Metadata-Version: 2.1
-Name: ${{env.ARCHIVE_NAME}}
-Version: $BRANCH
-Author: $GITHUB_ACTOR
-Home-page: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/pull/$PRNUMBER
-Download-URL: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID
-Summary: $PACKAGES
-Platform: ${{ matrix.arch }}
-
-Packages for ImmortalWrt $BRANCH running on ${{matrix.arch}}, built from PR $PRNUMBER
-at commit $HEAD_ID, against $BRANCH at commit $BASE_ID, with merge SHA $MERGE_ID.
-
-Modified packages:
-_EOF_
-          for p in $PACKAGES
-          do
+          shopt -s nullglob
+          IPKS=( *.ipk )
+          
+          cat << EOF > PKG-INFO
+          Metadata-Version: 2.1
+          Name: ${{ env.ARCHIVE_NAME }}
+          Version: $BRANCH
+          Author: $GITHUB_ACTOR
+          Home-page: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY
+          Download-URL: $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID
+          Summary: $PACKAGES
+          Platform: ${{ matrix.arch }}
+
+          Packages for ImmortalWrt $BRANCH running on ${{ matrix.arch }}, built from $PRNUMBER
+          at commit $HEAD_ID, against $BRANCH at commit $BASE_ID.
+
+          Modified packages:
+          EOF
+          
+          for p in $PACKAGES; do
             echo "  $p" >> PKG-INFO
           done
+
           echo >> PKG-INFO
-          echo "Full file listing:" >> PKG-INFO
-          ls -al artifacts/packages/*.ipk 2>/dev/null || echo "No packages built" >> PKG-INFO
+          echo "Package count: ${#IPKS[@]}" >> PKG-INFO
+          echo "Package size summary:" >> PKG-INFO
+          for f in "${IPKS[@]}"; do
+            du -sh "$f" >> PKG-INFO
+          done
+
+          if [ ${#IPKS[@]} -eq 0 ]; then
+            echo "No packages built." >> PKG-INFO
+          fi
+
           cat PKG-INFO
 
       - name: Store packages
         uses: actions/upload-artifact@v4
         with:
-          name: ${{env.ARCHIVE_NAME}}-packages
+          name: ${{ env.ARCHIVE_NAME }}-packages
           path: |
-            artifacts/packages/
-            artifacts/Packages*
+            Packages
+            Packages.*
+            *.ipk
             PKG-INFO
 
       - name: Store logs
         uses: actions/upload-artifact@v4
         with:
-          name: ${{env.ARCHIVE_NAME}}-logs
+          name: ${{ env.ARCHIVE_NAME }}-logs
           path: |
-            artifacts/logs/
-            artifacts/.config*
+            logs/
             PKG-INFO

+ 162 - 4
luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm

@@ -96,6 +96,19 @@ function import_ssr_url(btn, urlname, sid) {
 		//var ssu = ssrurl.match(/ssr:\/\/([A-Za-z0-9_-]+)/i);
 		var ssu = ssrurl.split('://');
 		//console.log(ssu.length);
+		if (ssu[0] === "ss") {
+		    var queryStr = "";
+			if (ssu[1].indexOf("?") > -1) {
+			    queryStr = ssu[1].split("?")[1]; // 提取 ? 后面的参数
+				queryStr = queryStr.replace(/&([a-zA-Z]+);/g, '&'); // 转义 &amp; 为 &
+			}
+			var params = new URLSearchParams(queryStr);
+			if (params.get("type")) {
+			    // 替换协议头
+				ssrurl = ssrurl.replace(/^ss:\/\//i, "shadowsocks://");
+				var ssu = ssrurl.split('://');
+			}
+		}
 		var event = document.createEvent("HTMLEvents");
 		event.initEvent("change", true, true);
 		switch (ssu[0]) {
@@ -164,7 +177,7 @@ function import_ssr_url(btn, urlname, sid) {
 			s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
 			return false;
 		case "ss":
-			var url0 = ssu[1] || "";
+			var url0 = ((ssu[1] || "").replace(/&([a-zA-Z]+);/g, '&').replace(/\s*#\s*/, '#').trim());
 			var param = "";
 
 			// 先分离 #(alias)
@@ -194,7 +207,7 @@ function import_ssr_url(btn, urlname, sid) {
 				var userInfoSplitIndex = userInfo.indexOf(":");
 				if(userInfoSplitIndex < 0) {
 					// 格式错误
-					s.innerHTML = "<font style='color:red'>Userinfo format error</font>";
+					s.innerHTML = "<font style='color:red'><%:Userinfo format error.%></font>";
 					break;
 				}
 				var method = userInfo.substring(0, userInfoSplitIndex);
@@ -217,7 +230,7 @@ function import_ssr_url(btn, urlname, sid) {
 				var decodedUrl0 = decodeURIComponent(url0);
 				var sstr = b64decsafe(decodedUrl0);
 				if (!sstr) {
-					s.innerHTML = "<font style='color:red'>Base64 sstr failed</font>";
+					s.innerHTML = "<font style='color:red'><%:Base64 sstr failed.%></font>";
 					break;
 				}
 
@@ -239,7 +252,7 @@ function import_ssr_url(btn, urlname, sid) {
 					var server = mNormal[3];
 					var port = mNormal[4];
 				} else {
-					s.innerHTML = "<font style='color:red'>SS URL base64 sstr format not recognized</font>";
+					s.innerHTML = "<font style='color:red'><%:SS URL base64 sstr format not recognized.%></font>";
 					break;
 				}
 
@@ -651,6 +664,151 @@ function import_ssr_url(btn, urlname, sid) {
 			}
 			s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
 			return false;
+		case "shadowsocks":
+			try {
+				// 处理完整 ss:// 链接
+				var urlinfo = ssu[1].replace(/&([a-zA-Z]+);/g, '&').replace(/\s*#\s*/, '#').trim();
+				// 拆分 @,判断是否是 base64 userinfo 的格式
+				var parts = urlinfo.split("@");
+				if (parts.length > 1) {
+					// @ 前是 base64(method:password),后面是 server:port?params
+					var userinfo = b64decsafe(parts[0]);
+					var sepIndex = userinfo.indexOf(":");
+					if (sepIndex > -1) {
+						method = userinfo.slice(0, sepIndex);
+						password = userinfo.slice(sepIndex + 1); 
+					}
+				}
+				var url = new URL("http://" + urlinfo);
+
+				var params = url.searchParams;
+
+			} catch(e) {
+				alert(e);
+				return false;
+			}
+			// Check if the elements exist before trying to modify them
+			function setElementValue(name, value) {
+				const element = document.getElementsByName(name)[0];
+				if (element) {
+					if (typeof value === 'boolean') {
+						element.checked = value;
+					} else {
+						element.value = value;
+					}
+				}
+			}
+			function dispatchEventIfExists(name, event) {
+				const element = document.getElementsByName(name)[0];
+				if (element) {
+					element.dispatchEvent(event);
+				}
+			}
+			setElementValue('cbid.shadowsocksr.' + sid + '.alias', url.hash ? decodeURIComponent(url.hash.slice(1)) : "");
+			setElementValue('cbid.shadowsocksr.' + sid + '.type', "v2ray");
+			dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.type', event);
+			setElementValue('cbid.shadowsocksr.' + sid + '.v2ray_protocol', "shadowsocks");
+			dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.v2ray_protocol', event);
+			setElementValue('cbid.shadowsocksr.' + sid + '.server', url.hostname);
+			setElementValue('cbid.shadowsocksr.' + sid + '.server_port', url.port || "80");
+			setElementValue('cbid.shadowsocksr.' + sid + '.password', password || url.username);
+			setElementValue('cbid.shadowsocksr.' + sid + '.transport', 
+				params.get("type") === "http" ? "h2" : 
+				(["xhttp", "splithttp"].includes(params.get("type")) ? "xhttp" :
+				(["tcp", "raw"].includes(params.get("type")) ? "raw" : 
+				(params.get("type") || "raw")))
+			);
+			dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.transport', event);
+			setElementValue('cbid.shadowsocksr.' + sid + '.encrypt_method_ss', method || params.get("encryption") || "none");
+			if ([ "tls", "xtls", "reality" ].includes(params.get("security"))) {
+				setElementValue('cbid.shadowsocksr.' + sid + '.' + params.get("security"), true);
+				dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.' + params.get("security"), event);
+
+				if (params.get("security") === "tls") {
+					if (params.get("ech") && params.get("ech").trim() !== "") {
+						setElementValue('cbid.shadowsocksr.' + sid + '.enable_ech', true); // 设置 enable_ech 为 true
+						dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_ech', event); // 触发事件
+						setElementValue('cbid.shadowsocksr.' + sid + '.ech_config', params.get("ech") || "");
+					}
+					if (params.get("allowInsecure") === "1") {
+						setElementValue('cbid.shadowsocksr.' + sid + '.insecure', true); // 设置 insecure 为 true
+						dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.insecure', event); // 触发事件
+					}
+				}
+				if (params.get("security") === "reality") {
+					setElementValue('cbid.shadowsocksr.' + sid + '.reality_publickey', params.get("pbk") ? decodeURIComponent(params.get("pbk")) : "");
+					setElementValue('cbid.shadowsocksr.' + sid + '.reality_shortid', params.get("sid") || "");
+					setElementValue('cbid.shadowsocksr.' + sid + '.reality_spiderx', params.get("spx") ? decodeURIComponent(params.get("spx")) : "");
+					if (params.get("pqv") && params.get("pqv").trim() !== "") {
+						setElementValue('cbid.shadowsocksr.' + sid + '.enable_mldsa65verify', true); // 设置 enable_mldsa65verify 为 true
+						dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_mldsa65verify', event); // 触发事件
+						setElementValue('cbid.shadowsocksr.' + sid + '.reality_mldsa65verify', params.get("pqv") || "");
+					}
+				}
+				setElementValue('cbid.shadowsocksr.' + sid + '.tls_flow', params.get("flow") || "none");
+				dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.tls_flow', event);
+
+				setElementValue('cbid.shadowsocksr.' + sid + '.tls_alpn', params.get("alpn") || "");
+				setElementValue('cbid.shadowsocksr.' + sid + '.fingerprint', params.get("fp") || "");
+				setElementValue('cbid.shadowsocksr.' + sid + '.tls_host', params.get("sni") || "");
+			}
+			switch (params.get("type")) {
+			case "ws":
+				if (params.get("security") !== "tls") {
+					setElementValue('cbid.shadowsocksr.' + sid + '.ws_host', params.get("host") ? decodeURIComponent(params.get("host")) : "");
+				}
+				setElementValue('cbid.shadowsocksr.' + sid + '.ws_path', params.get("path") ? decodeURIComponent(params.get("path")) : "/");
+				break;
+			case "httpupgrade":
+				if (params.get("security") !== "tls") {
+					setElementValue('cbid.shadowsocksr.' + sid + '.httpupgrade_host', params.get("host") ? decodeURIComponent(params.get("host")) : "");
+				}
+				setElementValue('cbid.shadowsocksr.' + sid + '.httpupgrade_path', params.get("path") ? decodeURIComponent(params.get("path")) : "/");
+				break;
+			case "xhttp":
+			case "splithttp":
+				if (params.get("security") !== "tls") {
+					setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_host', params.get("host") ? decodeURIComponent(params.get("host")) : "");
+				}
+				setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_mode', params.get("mode") || "auto");
+				setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_path', params.get("path") ? decodeURIComponent(params.get("path")) : "/");
+				if (params.get("extra") && params.get("extra").trim() !== "") {
+					setElementValue('cbid.shadowsocksr.' + sid + '.enable_xhttp_extra', true); // 设置 enable_xhttp_extra 为 true
+					dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.enable_xhttp_extra', event); // 触发事件
+					setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_extra', params.get("extra") || "");
+				}
+				break;
+			case "kcp":
+				setElementValue('cbid.shadowsocksr.' + sid + '.kcp_guise', params.get("headerType") || "none");
+				setElementValue('cbid.shadowsocksr.' + sid + '.seed', params.get("seed") || "");
+				break;
+			case "http":
+			/* this is non-standard, bullshit */
+			case "h2":
+				setElementValue('cbid.shadowsocksr.' + sid + '.h2_host', params.get("host") ? decodeURIComponent(params.get("host")) : "");
+				setElementValue('cbid.shadowsocksr.' + sid + '.h2_path', params.get("path") ? decodeURIComponent(params.get("path")) : "");
+				break;
+			case "quic":
+				setElementValue('cbid.shadowsocksr.' + sid + '.quic_guise', params.get("headerType") || "none");
+				setElementValue('cbid.shadowsocksr.' + sid + '.quic_security', params.get("quicSecurity") || "none");
+				setElementValue('cbid.shadowsocksr.' + sid + '.quic_key', params.get("key") || "");
+				break;
+			case "grpc":
+				setElementValue('cbid.shadowsocksr.' + sid + '.serviceName', params.get("serviceName") || "");
+				setElementValue('cbid.shadowsocksr.' + sid + '.grpc_mode', params.get("mode") || "gun");
+				break;
+			case "tcp":
+			case "raw":
+				setElementValue('cbid.shadowsocksr.' + sid + '.tcp_guise', params.get("headerType") || "none");
+				dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.tcp_guise', event);
+				if (params.get("headerType") === "http") {
+					setElementValue('cbid.shadowsocksr.' + sid + '.http_host', params.get("host") ? decodeURIComponent(params.get("host")) : "");
+					setElementValue('cbid.shadowsocksr.' + sid + '.http_path', params.get("path") ? decodeURIComponent(params.get("path")) : "");
+				}
+				break;
+			}
+			s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
+			return false;
 		default:
 			s.innerHTML = "<font style=\'color:red\'><%:Invalid format.%></font>";
 			return false;

+ 21 - 8
luci-app-ssr-plus/po/templates/ssr-plus.pot

@@ -219,6 +219,10 @@ msgstr ""
 msgid "Baidu Public DNS (180.76.76.76)"
 msgstr ""
 
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:233
+msgid "Base64 sstr failed."
+msgstr ""
+
 #: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1034
 #: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1044
 msgid "BitTorrent (uTP)"
@@ -969,16 +973,17 @@ msgstr ""
 msgid "If you have a self-signed certificate,please check the box"
 msgstr ""
 
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:661
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:819
 msgid "Import"
 msgstr ""
 
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:164
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:307
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:339
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:435
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:522
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:652
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:177
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:320
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:352
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:448
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:535
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:665
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:810
 msgid "Import configuration information successfully."
 msgstr ""
 
@@ -998,7 +1003,7 @@ msgstr ""
 msgid "Invalid JSON format"
 msgstr ""
 
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:655
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:813
 msgid "Invalid format."
 msgstr ""
 
@@ -1585,6 +1590,10 @@ msgstr ""
 msgid "Running Mode"
 msgstr ""
 
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:255
+msgid "SS URL base64 sstr format not recognized."
+msgstr ""
+
 #: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:13
 msgid "SSR Client"
 msgstr ""
@@ -2054,6 +2063,10 @@ msgstr ""
 msgid "User-Agent"
 msgstr ""
 
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:210
+msgid "Userinfo format error."
+msgstr ""
+
 #: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:402
 #: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:110
 #: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server.lua:117

+ 21 - 8
luci-app-ssr-plus/po/zh_Hans/ssr-plus.po

@@ -221,6 +221,10 @@ msgstr "【百度】连通性检查"
 msgid "Baidu Public DNS (180.76.76.76)"
 msgstr ""
 
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:233
+msgid "Base64 sstr failed."
+msgstr "Base64 解码失败。"
+
 #: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1034
 #: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1044
 msgid "BitTorrent (uTP)"
@@ -980,16 +984,17 @@ msgstr ""
 msgid "If you have a self-signed certificate,please check the box"
 msgstr "如果你使用自签证书,请选择"
 
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:661
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:819
 msgid "Import"
 msgstr "导入配置信息"
 
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:164
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:307
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:339
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:435
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:522
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:652
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:177
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:320
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:352
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:448
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:535
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:665
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:810
 msgid "Import configuration information successfully."
 msgstr "导入配置信息成功。"
 
@@ -1009,7 +1014,7 @@ msgstr "接口控制"
 msgid "Invalid JSON format"
 msgstr "无效的 JSON 格式"
 
-#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:655
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:813
 msgid "Invalid format."
 msgstr "无效的格式。"
 
@@ -1598,6 +1603,10 @@ msgstr "运行中"
 msgid "Running Mode"
 msgstr "运行模式"
 
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:255
+msgid "SS URL base64 sstr format not recognized."
+msgstr "无法识别 SS URL 的 Base64 格式。"
+
 #: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:13
 msgid "SSR Client"
 msgstr "客户端"
@@ -2070,6 +2079,10 @@ msgstr "用户已取消。"
 msgid "User-Agent"
 msgstr "用户代理(User-Agent)"
 
+#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm:210
+msgid "Userinfo format error."
+msgstr "用户信息格式错误。"
+
 #: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:402
 #: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:110
 #: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server.lua:117

+ 144 - 0
luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua

@@ -173,6 +173,34 @@ local function isCompleteJSON(str)
 	local success, _ = pcall(jsonParse, str)
 	return success
 end
+local function detectNodeType(rawContent)
+    -- 去掉 # 前后空格,处理 HTML 转义 &amp;
+    local content = trim(rawContent:gsub("&[a-zA-Z]+;", "&"):gsub("%s*#%s*", "#"))
+
+    -- 找到 # 分隔位置
+    local idx_sp = content:find("#") or 0
+    local info = content:sub(1, idx_sp > 0 and idx_sp - 1 or #content):gsub("/%?", "?")
+
+    -- 拆 base64 主体和 ? 参数部分
+    local uri_main, query_str = info:match("^([^?]+)%??(.*)$")
+
+    local params = {}
+    if query_str and query_str ~= "" then
+        for _, v in ipairs(split(query_str, '&')) do
+            local t = split(v, '=')
+            if #t >= 2 then
+                params[t[1]] = UrlDecode(t[2])
+            end
+        end
+    end
+
+    -- 判断是否是 Xray-SS 节点
+    if params["type"] then
+        return "shadowsocks"
+    else
+        return "ss"
+    end
+end
 -- 处理数据
 local function processData(szType, content)
 	local result = {type = szType, local_port = 1234, kcp_param = '--nocomp'}
@@ -183,6 +211,14 @@ local function processData(szType, content)
 		end
 	end
 
+	-- 协议头识别
+	if szType == "ss" then
+		local nodeType = detectNodeType(content)
+		if nodeType == "shadowsocks" then
+			szType = "shadowsocks"  -- 替换类型
+		end
+	end
+
 	if szType == "hysteria2" or szType == "hy2" then
 		local url = URL.parse("http://" .. content)
 		local params = url.query
@@ -399,6 +435,7 @@ local function processData(szType, content)
 			result.security = info.security
 		end
 	elseif szType == "ss" then
+		local content = trim(content:gsub("&[a-zA-Z]+;", "&"):gsub("%s*#%s*", "#"))
 		local idx_sp = content:find("#") or 0
 		local alias = ""
 		if idx_sp > 0 then
@@ -761,6 +798,113 @@ local function processData(szType, content)
 		result.server_port = url.port
 		result.vmess_id = url.user
 		result.vless_encryption = params.encryption or "none"
+		result.transport = params.type or "raw"
+		if result.transport == "tcp" then
+			result.transport = "raw"
+		end
+		if result.transport == "splithttp" then
+			result.transport = "xhttp"
+		end
+		result.tls = (params.security == "tls" or params.security == "xtls") and "1" or "0"
+		if params.alpn and params.alpn ~= "" then
+			local alpn = {}
+			for v in params.alpn:gmatch("[^,]+") do
+				table.insert(alpn, v)
+			end
+			result.tls_alpn = alpn
+		end
+		result.tls_host = params.sni
+		result.tls_flow = (params.security == "tls" or params.security == "reality") and params.flow or nil
+		result.fingerprint = params.fp
+		result.reality = (params.security == "reality") and "1" or "0"
+		result.reality_publickey = params.pbk and UrlDecode(params.pbk) or nil
+		result.reality_shortid = params.sid
+		result.reality_spiderx = params.spx and UrlDecode(params.spx) or nil
+		-- 检查 ech 参数是否存在且非空
+		result.enable_ech = (params.ech and params.ech ~= "") and "1" or nil
+		result.ech_config = (params.ech and params.ech ~= "") and params.ech or nil
+		-- 检查 pqv 参数是否存在且非空
+		result.enable_mldsa65verify = (params.pqv and params.pqv ~= "") and "1" or nil
+		result.reality_mldsa65verify = (params.pqv and params.pqv ~= "") and params.pqv or nil
+		if result.transport == "ws" then
+			result.ws_host = (result.tls ~= "1") and (params.host and UrlDecode(params.host)) or nil
+			result.ws_path = params.path and UrlDecode(params.path) or "/"
+		elseif result.transport == "httpupgrade" then
+			result.httpupgrade_host = (result.tls ~= "1") and (params.host and UrlDecode(params.host)) or nil
+			result.httpupgrade_path = params.path and UrlDecode(params.path) or "/"
+		elseif result.transport == "xhttp" or result.transport == "splithttp" then
+			result.xhttp_host = (result.tls ~= "1") and (params.host and UrlDecode(params.host)) or nil
+			result.xhttp_mode = params.mode or "auto"
+			result.xhttp_path = params.path and UrlDecode(params.path) or "/"
+			-- 检查 extra 参数是否存在且非空
+			result.enable_xhttp_extra = (params.extra and params.extra ~= "") and "1" or nil
+			result.xhttp_extra = (params.extra and params.extra ~= "") and params.extra or nil
+			-- 尝试解析 JSON 数据
+			local success, Data = pcall(jsonParse, params.extra or "")
+			if success and type(Data) == "table" then
+				local address = (Data.extra and Data.extra.downloadSettings and Data.extra.downloadSettings.address)
+					or (Data.downloadSettings and Data.downloadSettings.address)
+				result.download_address = address and address ~= "" and address or nil
+			else
+				-- 如果解析失败,清空下载地址
+				result.download_address = nil
+			end
+		-- make it compatible with bullshit, "h2" transport is non-existent at all
+		elseif result.transport == "http" or result.transport == "h2" then
+			result.transport = "h2"
+			result.h2_host = params.host and UrlDecode(params.host) or nil
+			result.h2_path = params.path and UrlDecode(params.path) or nil
+		elseif result.transport == "kcp" then
+			result.kcp_guise = params.headerType or "none"
+			result.seed = params.seed
+			result.mtu = 1350
+			result.tti = 50
+			result.uplink_capacity = 5
+			result.downlink_capacity = 20
+			result.read_buffer_size = 2
+			result.write_buffer_size = 2
+		elseif result.transport == "quic" then
+			result.quic_guise = params.headerType or "none"
+			result.quic_security = params.quicSecurity or "none"
+			result.quic_key = params.key
+		elseif result.transport == "grpc" then
+			result.serviceName = params.serviceName
+			result.grpc_mode = params.mode or "gun"
+		elseif result.transport == "tcp" or result.transport == "raw" then
+			result.tcp_guise = params.headerType or "none"
+			if result.tcp_guise == "http" then
+				result.tcp_host = params.host and UrlDecode(params.host) or nil
+				result.tcp_path = params.path and UrlDecode(params.path) or nil
+			end
+		end
+	elseif szType == "shadowsocks" then
+		local content = trim(content:gsub("&[a-zA-Z]+;", "&"):gsub("%s*#%s*", "#"))
+		local idx_sp = content:find("#") or 0
+		local alias = ""
+		if idx_sp > 0 then
+        	alias = UrlDecode(content:sub(idx_sp + 1))
+		end
+		local info = content:sub(1, idx_sp > 0 and idx_sp - 1 or #content)
+		local url = URL.parse("http://" .. info)
+		local params = url.query
+
+		result.alias = alias
+		result.type = "v2ray"
+		result.v2ray_protocol = "shadowsocks"
+		result.server = url.host
+		result.server_port = url.port
+
+		-- 判断 @ 前部分是否为 Base64
+		local is_base64_decoded = base64Decode(UrlDecode(url.user))
+		if is_base64_decoded:find(":") then
+        	-- 新格式:method:password
+        	result.encrypt_method_ss, result.password = is_base64_decoded:match("^(.-):(.*)$")
+		else
+        	-- 旧格式:UUID 直接作为密码
+        	result.password = url.user
+        	result.encrypt_method_ss = params.encryption or "none"
+		end
+
 		result.transport = params.type or "raw"
 		if result.transport == "tcp" then
 			result.transport = "raw"