Ver Fonte

luci-app-ssr-plus: Fix and add `ss+shadow-tls` subscribe.

zxlhhyccc há 4 meses atrás
pai
commit
88421a7fb6

+ 3 - 0
luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua

@@ -349,6 +349,9 @@ end
 if is_finded("xray-plugin") then
 	o:value("xray-plugin", translate("xray-plugin"))
 end
+if is_finded("shadow-tls") then
+	o:value("shadow-tls", translate("shadow-tls"))
+end
 o:value("custom", translate("Custom"))
 o.rmempty = true
 o:depends({enable_plugin = true})

+ 109 - 79
luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm

@@ -118,7 +118,7 @@ function import_ssr_url(btn, urlname, sid) {
 				document.getElementsByName('cbid.shadowsocksr.' + sid + '.lazy_mode')[0].dispatchEvent(event);
 			}
 			if (params.get("mport")) {
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_port_hopping')[0].checked = true; // 设置 flag_transport 为 true
+				document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_port_hopping')[0].checked = true; // 设置 flag_port_hopping 为 true
 				document.getElementsByName('cbid.shadowsocksr.' + sid + '.flag_port_hopping')[0].dispatchEvent(event); // 触发事件
 
 			document.getElementsByName('cbid.shadowsocksr.' + sid + '.port_range')[0].value = params.get("mport") || "";
@@ -158,91 +158,122 @@ function import_ssr_url(btn, urlname, sid) {
 			s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
 			return false;
 		case "ss":
-			var url0, param = "";
-			var sipIndex = ssu[1].indexOf("@");
-			var ploc = ssu[1].indexOf("#");
-			if (ploc > 0) {
-				url0 = ssu[1].substr(0, ploc);
-				param = ssu[1].substr(ploc + 1);
-			} else {
-				url0 = ssu[1];
-			}
-			if (sipIndex != -1) {
-				// SIP002
-				var userInfo = b64decsafe(url0.substr(0, sipIndex));
-				// console.log("userInfo:", userInfo); // 打印解析后的 userInfo
-				var temp = url0.substr(sipIndex + 1).split("/?");
-				var serverInfo = temp[0].split(":");
+			var url0 = ssu[1] || "";
+			var param = "";
+
+			// 先分离 #(alias)
+			var hashIndex = url0.indexOf("#");
+			if (hashIndex >= 0) {
+				param = url0.substring(hashIndex + 1);
+				url0 = url0.substring(0, hashIndex);
+			}
+
+			// 再分离 ?(参数)
+			var queryIndex = url0.indexOf("?");
+			var queryStr = "";
+			if (queryIndex >= 0) {
+				queryStr = url0.substring(queryIndex + 1);
+				url0 = url0.substring(0, queryIndex);
+			}
+
+			var params = Object.fromEntries(new URLSearchParams(queryStr));
+
+			// 判断是否 SIP002 格式(即含 @)
+			if (url0.indexOf("@") !== -1) {
+				// === SIP002 格式 ===
+				var sipIndex = url0.indexOf("@");
+				var userInfoB64 = url0.substring(0, sipIndex);
+				var userInfo = b64decsafe(userInfoB64);
+				var userInfoSplitIndex = userInfo.indexOf(":");
+				var method = userInfo.substring(0, userInfoSplitIndex);
+				var password = userInfo.substring(userInfoSplitIndex + 1);
+
+				var serverPart = url0.substring(url0.indexOf("@") + 1);
+				var serverInfo = serverPart.split(":");
 				var server = serverInfo[0];
-				var port = serverInfo[1].replace("/","");
-				var method, password, enable_plugin, plugin, pluginOpts;
-
-				// 解析 plugin 参数
-				if (temp[1]) {
-					var pluginInfo = decodeURIComponent(temp[1]);
-					// 使用正则匹配 plugin 参数
-					var pluginNameInfo = pluginInfo.match(/plugin=([^&]+)/);
-					if (pluginNameInfo) {
-						var pluginParams = pluginNameInfo[1].split(";");
-						plugin = pluginParams.shift(); // 获取 plugin
-						pluginOpts = pluginParams.length > 0 ? pluginParams.join(";") : "";
-					}
+				var port = serverInfo[1];
+
+				var plugin = "", pluginOpts = "";
+				if (params.plugin) {
+					var pluginParams = decodeURIComponent(params.plugin).split(";");
+					plugin = pluginParams.shift();
+					pluginOpts = pluginParams.join(";");
 				}
-				// 解析 userInfo(解析加密方法和密码)
-				var userInfoSplitIndex = userInfo.indexOf(":");
-				if (userInfoSplitIndex !== -1) {
-					method = userInfo.substr(0, userInfoSplitIndex);  // 提取加密方法
-					password = userInfo.substr(userInfoSplitIndex + 1);  // 提取密码
-					if (!method || method.trim() === "") {
-					    method = "none";  // 如果加密方法为空,设置为 "none"
-					}
+			} else {
+				// === Base64 SS2022 / 普通格式 的整体编码格式 ===
+				var sstr = b64decsafe(url0);
+				if (!sstr) {
+					s.innerHTML = "<font style='color:red'>Base64 sstr failed</font>";
+					break;
 				}
-				var has_ss_type = (ss_type === "ss-rust") ? "ss-rust" : "ss-libev";
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = ssu[0];
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].value = has_ss_type;
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].dispatchEvent(event);
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = server;
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = port;
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.password')[0].value = password || "";
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.encrypt_method_ss')[0].value = method;
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.encrypt_method_ss')[0].dispatchEvent(event);
-				if (plugin && plugin !== "none") {
-					document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].checked = true; // 设置 enable_plugin 为 true
-					document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].dispatchEvent(event); // 触发事件
-
-					document.getElementsByName('cbid.shadowsocksr.' + sid + '.plugin')[0].value = plugin || "none";
-					document.getElementsByName('cbid.shadowsocksr.' + sid + '.plugin')[0].dispatchEvent(event);
-					if (plugin !== undefined) {
-						document.getElementsByName('cbid.shadowsocksr.' + sid + '.plugin_opts')[0].value = pluginOpts || "";
-					}
+
+				// 支持 SS2022 / 普通格式
+				var regex2022 = /^([^:]+):([^:]+):([^@]+)@([^:]+):(\d+)$/;
+				var regexNormal = /^([^:]+):([^@]+)@([^:]+):(\d+)$/;
+
+				var m2022 = sstr.match(regex2022);
+				var mNormal = sstr.match(regexNormal);
+
+				if (m2022) {
+					var method = m2022[1];
+					var password = m2022[2] + ":" + m2022[3];
+					var server = m2022[4];
+					var port = m2022[5];
+				} else if (mNormal) {
+					var method = mNormal[1];
+					var password = mNormal[2];
+					var server = mNormal[3];
+					var port = mNormal[4];
 				} else {
-					document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].checked = false;
+					s.innerHTML = "<font style='color:red'>SS URL base64 sstr format not recognized</font>";
+					break;
 				}
-				if (param !== undefined) {
-					document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = decodeURI(param);
+
+				var plugin = "", pluginOpts = "";
+				if (params["shadow-tls"]) {
+					try {
+						var decoded_tls = JSON.parse(atob(decodeURIComponent(params["shadow-tls"])));
+						plugin = "shadow-tls";
+						var versionFlag = "";
+						if (decoded_tls.version && !isNaN(decoded_tls.version)) {
+                			versionFlag = "v" + decoded_tls.version + "=1;";
+						}
+						pluginOpts = versionFlag + "host=" + (decoded_tls.host || "") + ";passwd=" + (decoded_tls.password || "");
+					} catch (e) {
+						console.log("shadow-tls decode failed:", e);
+					}
 				}
-				s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
-			} else {
-				var sstr = b64decsafe(url0);
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = ssu[0];
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
-				var team = sstr.split('@');
-				var part1 = team[0].split(':');
-				var part2 = team[1].split(':');
-				var method = (part1[0] && part1[0].trim() !== "") ? part1[0].trim() : "none";
-				var password = part1[1] || "";
-				var server = part2[0];
-				var port = part2[1];
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = server;
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = port;
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.password')[0].value = password;
-				document.getElementsByName('cbid.shadowsocksr.' + sid + '.encrypt_method_ss')[0].value = method;
-				if (param != undefined) {
-					document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = decodeURI(param);
+			}
+
+			// === 填充配置项 ===
+			var has_ss_type = (ss_type === "ss-rust") ? "ss-rust" : "ss-libev";
+
+			document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].value = ssu[0];
+			document.getElementsByName('cbid.shadowsocksr.' + sid + '.type')[0].dispatchEvent(event);
+			document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].value = has_ss_type;
+			document.getElementsByName('cbid.shadowsocksr.' + sid + '.has_ss_type')[0].dispatchEvent(event);
+			document.getElementsByName('cbid.shadowsocksr.' + sid + '.server')[0].value = server;
+			document.getElementsByName('cbid.shadowsocksr.' + sid + '.server_port')[0].value = port;
+			document.getElementsByName('cbid.shadowsocksr.' + sid + '.password')[0].value = password || "";
+			document.getElementsByName('cbid.shadowsocksr.' + sid + '.encrypt_method_ss')[0].value = method;
+			document.getElementsByName('cbid.shadowsocksr.' + sid + '.encrypt_method_ss')[0].dispatchEvent(event);
+
+			if (plugin && plugin !== "none") {
+				document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].checked = true; // 设置 enable_plugin 为 true
+				document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].dispatchEvent(event); // 触发事件
+				document.getElementsByName('cbid.shadowsocksr.' + sid + '.plugin')[0].value = plugin;
+				document.getElementsByName('cbid.shadowsocksr.' + sid + '.plugin')[0].dispatchEvent(event);
+				if (plugin !== undefined) {
+					document.getElementsByName('cbid.shadowsocksr.' + sid + '.plugin_opts')[0].value = pluginOpts || "";
 				}
-				s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
+			} else {
+				document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_plugin')[0].checked = false;
 			}
+
+			if (param != undefined) {
+				document.getElementsByName('cbid.shadowsocksr.' + sid + '.alias')[0].value = decodeURIComponent(param);
+			}
+			s.innerHTML = "<font style=\'color:green\'><%:Import configuration information successfully.%></font>";
 			return false;
 		case "ssr":
 			var sstr = b64decsafe(ssu[1]);
@@ -581,4 +612,3 @@ function import_ssr_url(btn, urlname, sid) {
 <span id="<%=self.option%>-status"></span>
 <%+cbi/valuefooter%>
 
-

+ 127 - 56
luci-app-ssr-plus/root/usr/share/shadowsocksr/subscribe.lua

@@ -191,7 +191,7 @@ local function processData(szType, content)
 		-- 调试输出所有参数
 		-- log("Hysteria2 原始参数:")
 		-- for k,v in pairs(params) do
-			-- log(k.."="..v)
+		--	log(k.."="..v)
 		-- end
 
 		result.alias = url.fragment and UrlDecode(url.fragment) or nil
@@ -203,9 +203,9 @@ local function processData(szType, content)
 			result.transport_protocol = params.protocol or "udp"
 		end
 		result.hy2_auth = url.user
-		result.uplink_capacity = params.upmbps or "5"
-		result.downlink_capacity = params.downmbps or "20"
-		if params.obfs then
+		result.uplink_capacity = tonumber((params.upmbps or ""):match("^(%d+)")) or 5
+		result.downlink_capacity = tonumber((params.downmbps or ""):match("^(%d+)")) or 20
+		if params["obfs-password"] or params["obfs_password"] then
 			result.flag_obfs = "1"
 			result.obfs_type = params.obfs
 			result.salamander = params["obfs-password"] or params["obfs_password"]
@@ -347,75 +347,146 @@ local function processData(szType, content)
 			result.tls = "0"
 		end
 	elseif szType == "ss" then
-		local idx_sp = 0
+		local idx_sp = content:find("#") or 0
 		local alias = ""
-		if content:find("#") then
-			idx_sp = content:find("#")
-			alias = content:sub(idx_sp + 1, -1)
+		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 hostInfo = split(base64Decode(info), "@")
-		if #hostInfo < 2 then
-			--log("SS节点格式错误,解码后内容:", base64Decode(info))
+
+		-- 拆 base64 主体和 ? 参数部分
+		local uri_main, query_str = info:match("^([^?]+)%??(.*)$")
+		--log("SS 节点格式:", uri_main)
+		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
+
+		local is_old_format = uri_main:find("@") and not uri_main:find("://.*@")
+		local base64_str, host_port, userinfo, server, port, method, password
+
+		if is_old_format then
+			-- 旧格式:base64(method:pass)@host:port
+			base64_str, host_port = uri_main:match("^([^@]+)@(.-)$")
+			log("SS 节点旧格式解析:", base64_str)
+			if not base64_str or not host_port then
+				log("SS 节点旧格式解析失败:", uri_main)
+				return nil
+			end
+			local decoded = base64Decode(UrlDecode(base64_str))
+			if not decoded then
+				log("SS base64 解码失败(旧格式):", base64_str)
+				return nil
+			end
+			userinfo = decoded
+		else
+			-- 新格式:base64(method:pass@host:port)
+			local decoded = base64Decode(UrlDecode(uri_main))
+			if not decoded then
+				log("SS base64 解码失败(新格式):", uri_main)
+				return nil
+			end
+			userinfo, host_port = decoded:match("^(.-)@(.-)$")
+			if not userinfo or not host_port then
+				log("SS 解码内容缺失 @ 分隔:", decoded)
+				return nil
+			end
+		end
+
+		-- 解析加密方式和密码(允许密码包含冒号)
+		local split_pos = userinfo:find(":")
+		if not split_pos then
+			log("SS 用户信息格式错误:", userinfo)
 			return nil
 		end
-		local host = split(hostInfo[2], ":")
-		if #host < 2 then
-			--log("SS节点主机格式错误:", hostInfo[2])
+		method = userinfo:sub(1, split_pos - 1)
+		password = userinfo:sub(split_pos + 1)
+
+		-- 解析服务器地址和端口(兼容 IPv6)
+		if host_port:find("^%[.*%]:%d+$") then
+			server, port = host_port:match("^%[(.*)%]:(%d+)$")
+		else
+			server, port = host_port:match("^(.-):(%d+)$")
+		end
+		if not server or not port then
+			log("SS 节点服务器信息格式错误:", host_port)
 			return nil
-		end  
-		-- 提取用户信息
-		local userinfo = base64Decode(hostInfo[1])
-		local method, password = userinfo:match("^([^:]*):(.*)$")   
-		-- 填充结果
-		result.alias = UrlDecode(alias)
+		end
+
+		-- 填充 result
+		result.alias = alias
 		result.type = v2_ss
 		result.v2ray_protocol = (v2_ss == "v2ray") and "shadowsocks" or nil
 		result.has_ss_type = has_ss_type
 		result.encrypt_method_ss = method
 		result.password = password
-		result.server = host[1]
-		-- 处理端口和插件
-		local port_part = host[2]
-		if port_part:find("/%?") then
-			local query = split(port_part, "/%?")
-			result.server_port = query[1]
-			if query[2] then
-				local params = {}
-				for _, v in pairs(split(query[2], '&')) do
-					local t = split(v, '=')
-					if #t >= 2 then
-						params[t[1]] = t[2]
-					end
-				end
-				if params.plugin then
-					local plugin_info = UrlDecode(params.plugin)
-					local idx_pn = plugin_info:find(";")
-					if idx_pn then
-						result.plugin = plugin_info:sub(1, idx_pn - 1)
-						result.plugin_opts = plugin_info:sub(idx_pn + 1, #plugin_info)
-					else
-						result.plugin = plugin_info
-						result.plugin_opts = ""
-					end
-					-- 部分机场下发的插件名为 simple-obfs,这里应该改为 obfs-local
-					if result.plugin == "simple-obfs" then
-						result.plugin = "obfs-local"
-					end
-					-- 如果插件不为 none,确保 enable_plugin 为 1
-					if result.plugin ~= "none" and result.plugin ~= "" then
+		result.server = server
+		result.server_port = port
+
+		-- 插件处理
+		if params.plugin then
+			local plugin_info = UrlDecode(params.plugin)
+			local idx_pn = plugin_info:find(";")
+			if idx_pn then
+				result.plugin = plugin_info:sub(1, idx_pn - 1)
+				result.plugin_opts = plugin_info:sub(idx_pn + 1, #plugin_info)
+			else
+				result.plugin = plugin_info
+				result.plugin_opts = ""
+			end
+			-- 部分机场下发的插件名为 simple-obfs,这里应该改为 obfs-local
+			if result.plugin == "simple-obfs" then
+				result.plugin = "obfs-local"
+			end
+			-- 如果插件不为 none,确保 enable_plugin 为 1
+			if result.plugin ~= "none" and result.plugin ~= "" then
+				result.enable_plugin = 1
+			end
+		elseif has_ss_type and has_ss_type ~= "ss-libev" then
+			if params["shadow-tls"] then
+				-- 特别处理 shadow-tls 作为插件
+				-- log("原始 shadow-tls 参数:", params["shadow-tls"])
+				local decoded_tls = base64Decode(UrlDecode(params["shadow-tls"]))
+				--log("SS 节点 shadow-tls 解码后:", decoded_tls or "nil")
+				if decoded_tls then
+					local ok, st = pcall(jsonParse, decoded_tls)
+					if ok and st then
+
+						result.plugin = "shadow-tls"
 						result.enable_plugin = 1
+					
+						local version_flag = ""
+						if st.version and tonumber(st.version) then
+					    	version_flag = string.format("v%s=1;", st.version)
+						end
+					
+						-- 合成 plugin_opts 格式:v%s=1;host=xxx;password=xxx
+						result.plugin_opts = string.format("%shost=%s;passwd=%s",
+					    	version_flag,
+							st.host or "",
+							st.password or "")
+					else
+						log("shadow-tls JSON 解析失败")
 					end
 				end
 			end
 		else
-			result.server_port = port_part:gsub("/","")
+			if params["shadow-tls"] then
+				log("错误:ShadowSocks-libev 不支持使用 shadow-tls 插件")
+				return nil, "ShadowSocks-libev 不支持使用 shadow-tls 插件"
+			end
 		end
-		-- 检查加密方法
+
+		-- 检查加密方法是否受支持
 		if not checkTabValue(encrypt_methods_ss)[method] then
-        		-- 1202 年了还不支持 SS AEAD 的屑机场
-        		-- log("不支持的SS加密方法:", method)
-        		result.server = nil
+			-- 1202 年了还不支持 SS AEAD 的屑机场
+			-- log("不支持的SS加密方法:", method)
+			result.server = nil
 		end
 	elseif szType == "sip008" then
 		result.type = v2_ss
@@ -798,7 +869,7 @@ local execute = function()
 		local service_stopped = false
 		for k, url in ipairs(subscribe_url) do
 			local raw, new_md5 = curl(url)
-			--log("raw 长度: "..#raw)
+			log("raw 长度: "..#raw)
 			local groupHash = md5(url)
 			local old_md5 = read_old_md5(groupHash)