Browse Source

luci-app-ssr-plus: Add Xray new version `XHTTP` transport (formerly `SplitHTTP` transport) support.

See: https://github.com/XTLS/Xray-core/discussions/4113

说明:本次新增的 `XHTTP` 传输协议,在使用新版本的Xray情况下,可将旧版本节点的 `SplitHTTP` 传输协议修改为新版本的 `XHTTP` 传输协议,但旧版本的Xray只能使用 `SplitHTTP` 传输协议,因此暂不删除旧版本的`SplitHTTP` 传输协议支持。
zxlhhyccc 9 months ago
parent
commit
3c5883a6fc

+ 3 - 3
luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua

@@ -204,12 +204,12 @@ for key, server_type in pairs(type_table) do
 end
 
 -- Socks User
-o = s:option(Value, "socks5_user", translate("Socks5 User"), translate("Only when auth is password valid, Mandatory."))
+o = s:option(Value, "socks5_user", translate("Socks5 User"), translate("Only when Socks5 Auth Mode is password valid, Mandatory."))
 o.rmempty = true
 o:depends("socks5_auth", "password")
 
 -- Socks Password
-o = s:option(Value, "socks5_pass", translate("Socks5 Password"), translate("Only when auth is password valid, Not mandatory."))
+o = s:option(Value, "socks5_pass", translate("Socks5 Password"), translate("Only when Socks5 Auth Mode is password valid, Not mandatory."))
 o.password = true
 o.rmempty = true
 o:depends("socks5_auth", "password")
@@ -263,7 +263,7 @@ o.default = 0
 s = m:section(TypedSection, "xray_noise_packets", translate("Xray Noise Packets"))
 s.description = translate(
     "<font style='color:red'>" .. translate("To send noise packets, select \"Noise\" in Xray Settings.") .. "</font>" ..
-    "<br/><font><b>" .. translate("For specific usage, see: ") .. "</b></font>" ..
+    "<br/><font><b>" .. translate("For specific usage, see:") .. "</b></font>" ..
     "<a href='https://xtls.github.io/config/outbounds/freedom.html' target='_blank'>" ..
     "<font style='color:green'><b>" .. translate("Click to the page") .. "</b></font></a>")
 s.template = "cbi/tblsection"

+ 74 - 1
luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua

@@ -4,6 +4,7 @@
 require "nixio.fs"
 require "luci.sys"
 require "luci.http"
+require "luci.jsonc"
 require "luci.model.ipkg"
 
 local m, s, o
@@ -504,7 +505,6 @@ o.rmempty = true
 o.default = ""
 o:depends("type", "tuic")
 
-
 o = s:option(ListValue, "udp_relay_mode", translate("UDP relay mode"))
 o:depends("type", "tuic")
 o:value("native", translate("native UDP characteristics"))
@@ -623,6 +623,7 @@ o:value("kcp", "mKCP")
 o:value("ws", "WebSocket")
 o:value("httpupgrade", "HTTPUpgrade")
 o:value("splithttp", "SplitHTTP")
+o:value("xhttp", "XHTTP")
 o:value("h2", "HTTP/2")
 o:value("quic", "QUIC")
 o:value("grpc", "gRPC")
@@ -703,6 +704,78 @@ o = s:option(Value, "splithttp_path", translate("Splithttp Path"))
 o:depends("transport", "splithttp")
 o.rmempty = true
 
+-- [[ XHTTP部分 ]]--
+o = s:option(ListValue, "xhttp_alpn", translate("XHTTP Alpn"))
+o.default = ""
+o:value("", translate("Default"))
+o:value("h3")
+o:value("h2")
+o:value("h3,h2")
+o:value("http/1.1")
+o:value("h2,http/1.1")
+o:value("h3,h2,http/1.1")
+o:depends("transport", "xhttp")
+
+o = s:option(ListValue, "xhttp_mode", translate("XHTTP Mode"))
+o:depends("transport", "xhttp")
+o.default = "auto"
+o:value("auto")
+o:value("packet-up")
+o:value("stream-up")
+o:value("stream-one")
+
+o = s:option(Value, "xhttp_host", translate("XHTTP Host"))
+o:depends({transport = "xhttp", tls = false})
+o.rmempty = true
+
+o = s:option(Value, "xhttp_path", translate("XHTTP Path"))
+o.placeholder = "/"
+o:depends("transport", "xhttp")
+o.rmempty = true
+
+o = s:option(Flag, "enable_xhttp_extra", translate("XHTTP Extra"))
+o.description = translate("Enable this option to configure XHTTP Extra (JSON format).")
+o.default = "0"
+o.rmempty = false
+o:depends("transport", "xhttp")
+
+o = s:option(TextValue, "xhttp_extra", " ")
+o.description = translate(
+    "<font><b>" .. translate("Configure XHTTP Extra Settings (JSON format), see:") .. "</b></font>" ..
+    " <a href='https://xtls.github.io/config/transports/splithttp.html#extra' target='_blank'>" ..
+    "<font style='color:green'><b>" .. translate("Click to the page") .. "</b></font></a>")
+o:depends("enable_xhttp_extra", true)
+o.rmempty = true
+o.rows = 10
+o.wrap = "off"
+o.custom_write = function(self, section, value)
+    m:set(section, "xhttp_extra", value)
+    local success, data = pcall(luci.jsonc.parse, value)
+    if success and data then
+        local address = (data.extra and data.extra.downloadSettings and data.extra.downloadSettings.address)
+            or (data.downloadSettings and data.downloadSettings.address)
+        if address and address ~= "" then
+            m:set(section, "download_address", address)
+        else
+            m:del(section, "download_address")
+        end
+    else
+        m:del(section, "download_address")
+    end
+end
+o.validate = function(self, value)
+    value = value:gsub("\r\n", "\n"):gsub("^[ \t]*\n", ""):gsub("\n[ \t]*$", ""):gsub("\n[ \t]*\n", "\n")
+    if value:sub(-1) == "\n" then
+        value = value:sub(1, -2)
+    end
+    local success, data = pcall(luci.jsonc.parse, value)
+    if not success or not data then
+        return nil, translate("Invalid JSON format")
+    end
+
+    return value
+end
+
 -- [[ H2部分 ]]--
 
 -- H2域名

+ 36 - 0
luci-app-ssr-plus/luasrc/view/shadowsocksr/ssrurl.htm

@@ -263,6 +263,18 @@ function import_ssr_url(btn, urlname, sid) {
 				}
 				document.getElementsByName('cbid.shadowsocksr.' + sid + '.splithttp_path')[0].value = params.get("path") ? decodeURIComponent(params.get("path")) : "/";
 				break;
+			case "xhttp":
+				if (params.get("security") !== "tls") {
+					document.getElementsByName('cbid.shadowsocksr.' + sid + '.xhttp_host')[0].value = params.get("host") ? decodeURIComponent(params.get("host")) : "";
+				}
+				document.getElementsByName('cbid.shadowsocksr.' + sid + '.xhttp_mode')[0].value = params.get("mode") || "auto";
+				document.getElementsByName('cbid.shadowsocksr.' + sid + '.xhttp_path')[0].value = params.get("path") ? decodeURIComponent(params.get("path")) : "/";
+				if (params.get("extra") && params.get("extra").trim() !== "") {
+					document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_xhttp_extra')[0].checked = true; // 设置 enable_xhttp_extra 为 true
+					document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_xhttp_extra')[0].dispatchEvent(event); // 触发事件
+					document.getElementsByName('cbid.shadowsocksr.' + sid + '.xhttp_extra')[0].value = params.get("extra") || "";
+				}
+				break;
 			case "kcp":
 				document.getElementsByName('cbid.shadowsocksr.' + sid + '.kcp_guise')[0].value = params.get("headerType") || "none";
 				document.getElementsByName('cbid.shadowsocksr.' + sid + '.seed')[0].value = params.get("seed") || "";
@@ -338,6 +350,16 @@ function import_ssr_url(btn, urlname, sid) {
 				document.getElementsByName('cbid.shadowsocksr.' + sid + '.splithttp_host')[0].value = ssm.host;
 				document.getElementsByName('cbid.shadowsocksr.' + sid + '.splithttp_path')[0].value = ssm.path;
 			}
+			if (ssm.net == "xhttp") {
+				document.getElementsByName('cbid.shadowsocksr.' + sid + '.xhttp_mode')[0].value = ssm.mode;
+				document.getElementsByName('cbid.shadowsocksr.' + sid + '.xhttp_host')[0].value = ssm.host;
+				document.getElementsByName('cbid.shadowsocksr.' + sid + '.xhttp_path')[0].value = ssm.path;
+				if (params.get("extra") && params.get("extra").trim() !== "") {
+					document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_xhttp_extra')[0].checked = true; // 设置 enable_xhttp_extra 为 true
+					document.getElementsByName('cbid.shadowsocksr.' + sid + '.enable_xhttp_extra')[0].dispatchEvent(event); // 触发事件
+					document.getElementsByName('cbid.shadowsocksr.' + sid + '.xhttp_extra')[0].value = ssm.extra;
+				}
+			}
 			if (ssm.net == "h2") {
 				document.getElementsByName('cbid.shadowsocksr.' + sid + '.h2_host')[0].value = ssm.host;
 				document.getElementsByName('cbid.shadowsocksr.' + sid + '.h2_path')[0].value = ssm.path;
@@ -352,6 +374,7 @@ function import_ssr_url(btn, urlname, sid) {
 			if (ssm.tls == "tls") {
 				document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].checked = true;
 				document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls')[0].dispatchEvent(event);
+				document.getElementsByName('cbid.shadowsocksr.' + sid + '.xhttp_alpn')[0].value = ssm.alpn;
 				document.getElementsByName('cbid.shadowsocksr.' + sid + '.tls_host')[0].value = ssm.sni || ssm.host;
 			}
 			if (ssm.mux !== undefined) {
@@ -412,6 +435,7 @@ function import_ssr_url(btn, urlname, sid) {
 				setElementValue('cbid.shadowsocksr.' + sid + '.tls_flow', params.get("flow") || "none");
 				dispatchEventIfExists('cbid.shadowsocksr.' + sid + '.tls_flow', event);
 
+				setElementValue('cbid.shadowsocksr.' + sid + '.xhttp_alpn', params.get("alpn") || "");
 				setElementValue('cbid.shadowsocksr.' + sid + '.fingerprint', params.get("fp") || "");
 				setElementValue('cbid.shadowsocksr.' + sid + '.tls_host', params.get("sni") || "");
 			}
@@ -434,6 +458,18 @@ function import_ssr_url(btn, urlname, sid) {
 				}
 				setElementValue('cbid.shadowsocksr.' + sid + '.splithttp_path', params.get("path") ? decodeURIComponent(params.get("path")) : "/");
 				break;
+			case "xhttp":
+				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") || "");

+ 27 - 6
luci-app-ssr-plus/po/zh_Hans/ssr-plus.po

@@ -901,14 +901,14 @@ msgstr "Socks 协议的认证方式,默认值:noauth。"
 msgid "Socks5 User"
 msgstr "Socks5 用户名"
 
-msgid "Only when auth is password valid, Mandatory."
-msgstr "仅当 auth 为 password 时有效,必填。"
+msgid "Only when Socks5 Auth Mode is password valid, Mandatory."
+msgstr "仅当 Socks5 认证方式为 Password 时有效,必填。"
 
 msgid "Socks5 Password"
 msgstr "Socks5 密码"
 
-msgid "Only when auth is password valid, Not mandatory."
-msgstr "仅当 auth 为 password 时有效,非必填。"
+msgid "Only when Socks5 Auth Mode is password valid, Not mandatory."
+msgstr "仅当 Socks5 认证方式为 Password 时有效,非必填。"
 
 msgid "Enabled Mixed"
 msgstr "启用 Mixed"
@@ -952,8 +952,8 @@ msgstr "UDP 噪声,在某些情况下可以绕过一些针对 UDP 协议的限
 msgid "To send noise packets, select \"Noise\" in Xray Settings."
 msgstr "在 Xray 设置中勾选 “噪声” 以发送噪声包。"
 
-msgid "For specific usage, see: "
-msgstr "具体使用方法参见:"
+msgid "For specific usage, see:"
+msgstr "具体使用方法,请参见:"
 
 msgid "Click to the page"
 msgstr "点击前往"
@@ -1018,6 +1018,27 @@ msgstr "SplitHTTP 主机名"
 msgid "Splithttp Path"
 msgstr "SplitHTTP 路径"
 
+msgid "XHTTP Mode"
+msgstr "XHTTP 模式"
+
+msgid "XHTTP Host"
+msgstr "XHTTP 主机名"
+
+msgid "XHTTP Path"
+msgstr "XHTTP 路径"
+
+msgid "XHTTP Extra"
+msgstr "XHTTP 附加项"
+
+msgid "Enable this option to configure XHTTP Extra (JSON format)."
+msgstr "启用此选项配置 XHTTP 附加项(JSON 格式)。"
+
+msgid "Configure XHTTP Extra Settings (JSON format), see:"
+msgstr "配置 XHTTP 额外设置(JSON 格式),请参见:"
+
+msgid "Invalid JSON format"
+msgstr "无效的 JSON 格式"
+
 msgid "HTTP/2 Host"
 msgstr "HTTP/2 主机名"
 

+ 20 - 5
luci-app-ssr-plus/root/usr/share/shadowsocksr/gen_config.lua

@@ -4,7 +4,7 @@ local ucursor = require "luci.model.uci".cursor()
 local json = require "luci.jsonc"
 
 local server_section = arg[1]
-local proto = arg[2]
+local proto = arg[2] or "tcp"
 local local_port = arg[3] or "0"
 local socks_port = arg[4] or "0"
 
@@ -179,7 +179,7 @@ end
 
 	-- 开启 socks 代理
 	-- 检查是否启用 socks 代理
-if proto:find("tcp") and socks_port ~= "0" then
+if proto and proto:find("tcp") and socks_port ~= "0" then
     table.insert(Xray.inbounds, {
         -- socks
         protocol = "socks",
@@ -209,7 +209,7 @@ end
 				security = (server.xtls == '1') and "xtls" or (server.tls == '1') and "tls" or (server.reality == '1') and "reality" or nil,
 				tlsSettings = (server.tls == '1') and {
 					-- tls
-					alpn = server.tls_alpn,
+					alpn = (server.transport == "xhttp" and server.xhttp_alpn ~= "") and server.xhttp_alpn or server.tls_alpn,
 					fingerprint = server.fingerprint,
 					allowInsecure = (server.insecure == "1"),
 					serverName = server.tls_host,
@@ -225,6 +225,7 @@ end
 					minVersion = "1.3"
 				} or nil,
 				realitySettings = (server.reality == '1') and {
+					alpn =  (server.transport == "xhttp" and server.xhttp_alpn ~= "") and server.xhttp_alpn or nil,
 					publicKey = server.reality_publickey,
 					shortId = server.reality_shortid,
 					spiderX = server.reality_spiderx,
@@ -271,6 +272,20 @@ end
 					host = (server.splithttp_host or server.tls_host) or nil,
 					path = server.splithttp_path or "/"
 				} or nil,
+				xhttpSettings = (server.transport == "xhttp") and {
+					-- xhttp
+					mode = server.xhttp_mode or "auto",
+					host = (server.xhttp_host or server.tls_host) or nil,
+					path = server.xhttp_path or "/",
+					extra = (server.enable_xhttp_extra == "1" and server.xhttp_extra) and (function()
+						local success, parsed = pcall(json.parse, server.xhttp_extra)
+							if success then
+								return parsed.extra or parsed
+							else
+								return nil
+							end
+						end)() or nil
+				} or nil,
 				httpSettings = (server.transport == "h2") and {
 					-- h2
 					path = server.h2_path or "",
@@ -387,7 +402,7 @@ local ss = {
 	server_port = tonumber(server.server_port),
 	local_address = "0.0.0.0",
 	local_port = tonumber(local_port),
-	mode = (proto == "tcp,udp") and "tcp_and_udp" or proto .. "_only",
+	mode = (proto == "tcp,udp") and "tcp_and_udp" or (proto .. "_only"),
 	password = server.password,
 	method = server.encrypt_method_ss,
 	timeout = tonumber(server.timeout),
@@ -395,7 +410,7 @@ local ss = {
 	reuse_port = true
 }
 local hysteria = {
-	server = (server.server_port and (server.port_range and (server.server .. ":" .. server.server_port .. "," .. server.port_range) or server.server .. ":" .. server.server_port) or (server.port_range and server.server .. ":" .. server.port_range or server.server .. ":443")),
+	server = (server.server_port and (server.port_range and (server.server .. ":" .. server.server_port .. "," .. server.port_range) or (server.server .. ":" .. server.server_port) or (server.port_range and server.server .. ":" .. server.port_range or server.server .. ":443"))),
 	bandwidth = (server.uplink_capacity or server.downlink_capacity) and {
 	up = tonumber(server.uplink_capacity) and tonumber(server.uplink_capacity) .. " mbps" or nil,
 	down = tonumber(server.downlink_capacity) and tonumber(server.downlink_capacity) .. " mbps" or nil 

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

@@ -193,6 +193,24 @@ local function processData(szType, content)
 			result.splithttp_host = info.host
 			result.splithttp_path = info.path
 		end
+		if info.net == 'xhttp' then
+			result.xhttp_mode = info.mode
+			result.xhttp_host = info.host
+			result.xhttp_path = info.path
+			-- 检查 extra 参数是否存在且非空
+			result.enable_xhttp_extra = (info.extra and info.extra ~= "") and "1" or nil
+			result.xhttp_extra = (info.extra and info.extra ~= "") and info.extra or nil
+			-- 尝试解析 JSON 数据
+			local success, Data = pcall(jsonParse, info.extra)
+			if success and Data 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
+		end
 		if info.net == 'h2' then
 			result.h2_host = info.host
 			result.h2_path = info.path
@@ -231,6 +249,9 @@ local function processData(szType, content)
 		end
 		if info.tls == "tls" or info.tls == "1" then
 			result.tls = "1"
+			if info.alpn and info.alpn ~= "" then
+				result.xhttp_alpn = info.alpn
+			end
 			if info.sni and info.sni ~= "" then
 				result.tls_host = info.sni
 			elseif info.host then
@@ -353,6 +374,11 @@ local function processData(szType, content)
 				local t = split(v, '=')
 				params[t[1]] = t[2]
 			end
+			if params.alpn then
+				-- 处理 alpn 参数
+				result.xhttp_alpn = params.alpn
+			end
+
 			if params.sni then
 				-- 未指定peer(sni)默认使用remote addr
 				result.tls_host = params.sni
@@ -385,6 +411,23 @@ local function processData(szType, content)
 			elseif result.transport == "splithttp" then
 				result.splithttp_host = (result.tls ~= "1") and (params.host and UrlDecode(params.host)) or nil
 				result.splithttp_path = params.path and UrlDecode(params.path) or "/"
+			elseif result.transport == "xhttp" 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)
+				if success and Data 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
 			elseif result.transport == "http" or result.transport == "h2" then
 				result.transport = "h2"
 				result.h2_host = params.host and UrlDecode(params.host) or nil
@@ -426,6 +469,7 @@ local function processData(szType, content)
 		result.vless_encryption = params.encryption or "none"
 		result.transport = params.type or "tcp"
 		result.tls = (params.security == "tls" or params.security == "xtls") and "1" or "0"
+		result.xhttp_alpn = params.alpn or ""
 		result.tls_host = params.sni
 		result.tls_flow = (params.security == "tls" or params.security == "reality") and params.flow or nil
 		result.fingerprint = params.fp
@@ -442,6 +486,23 @@ local function processData(szType, content)
 		elseif result.transport == "splithttp" then
 			result.splithttp_host = (result.tls ~= "1") and (params.host and UrlDecode(params.host)) or nil
 			result.splithttp_path = params.path and UrlDecode(params.path) or "/"
+		elseif result.transport == "xhttp" 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)
+			if success and Data 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"