Update On Tue Apr 7 21:19:03 CEST 2026

This commit is contained in:
github-action[bot]
2026-04-07 21:19:03 +02:00
parent f1bf867156
commit 3c2aac940c
264 changed files with 18847 additions and 88048 deletions
+1 -2
View File
@@ -106,9 +106,8 @@ make menuconfig
* [MIT License](https://github.com/vernesong/OpenClash/blob/master/LICENSE)
* 内核 [Mihomo](https://github.com/MetaCubeX/mihomo) by [MetaCubeX](https://github.com/MetaCubeX)
* 本项目代码基于 [Luci For Clash](https://github.com/frainzy1477/luci-app-clash) by [frainzy1477](https://github.com/frainzy1477)
* GEOIP数据库 [GeoLite2](https://dev.maxmind.com/geoip/geoip2/geolite2/) by [MaxMind](https://www.maxmind.com)
* IP检查 [IP](https://ip.skk.moe/) by [SukkaW](https://ip.skk.moe/)
* 控制面板 [zashboard](https://github.com/Zephyruso/zashboard) by [Dreamacro](https://github.com/Zephyruso)
* 控制面板 [zashboard](https://github.com/Zephyruso/zashboard) by [Zephyruso](https://github.com/Zephyruso)
* 控制面板 [yacd](https://github.com/haishanh/yacd) by [haishanh](https://github.com/haishanh)
* 流媒体解锁检测 [RegionRestrictionCheck](https://github.com/lmc999/RegionRestrictionCheck) by [lmc999](https://github.com/lmc999)
+4
View File
@@ -1,4 +1,8 @@
[
{
"zh": "覆写模块从 v0.47.081 开始支持 [YAML] 块来进行配置文件覆写,格式使用 yaml, 从而降低代码覆写编辑的难度, 使用方式请参考 default 文件的示例。",
"en": "Overwrite module now supports YAML blocks for configuration file overwriting, using YAML format, which reduces the difficulty of code overwriting. Please refer to the default file example for usage."
},
{
"zh": "覆写模块现已支持远程订阅,更加方便插件的远程管理及统一配置、一键订阅,欢迎各位分享配置。",
"en": "Overwrite module now supports subscription, more convenient remote management and configuration, one-click subscription, welcome to share the configuration."
+1 -1
View File
@@ -1,7 +1,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-openclash
PKG_VERSION:=0.47.075
PKG_VERSION:=0.47.086
PKG_MAINTAINER:=vernesong <https://github.com/vernesong/OpenClash>
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
@@ -798,7 +798,7 @@ function sub_info_get()
if #providers_data == 0 then
if not url_result then
luci.http.status(400, "Subscription information not found")
luci.http.status(500, "Subscription information not found")
return
end
end
@@ -841,7 +841,7 @@ function action_switch_rule_mode()
mode = luci.http.formvalue("rule_mode")
if not mode then
luci.http.status(400, "Missing parameters")
luci.http.status(500, "Missing parameters")
return
end
@@ -1098,7 +1098,7 @@ end
function action_default_dashboard()
local default_dashboard = luci.http.formvalue("name")
if not default_dashboard or (default_dashboard ~= "Dashboard" and default_dashboard ~= "Yacd" and default_dashboard ~= "Metacubexd" and default_dashboard ~= "Zashboard") then
luci.http.status(400, "Set Failed")
luci.http.status(500, "Set Failed")
return
end
if not fs.isdirectory("/usr/share/openclash/ui/" .. string.lower(default_dashboard)) then
@@ -1644,6 +1644,13 @@ function rename_file()
uci:set("openclash", s[".name"], "name", fs.filename(new_file_name))
end
end)
uci:foreach("openclash", "subscribe_info",
function(s)
if s.name == fs.filename(old_file_name) and fs.filename(new_file_name) ~= new_file_name then
uci:set("openclash", s[".name"], "name", fs.filename(new_file_name))
end
end)
uci:foreach("openclash", "groups",
function(s)
@@ -2073,114 +2080,101 @@ function action_myip_check()
luci.http.write_json(result)
end
function action_website_check()
local domain = luci.http.formvalue("domain")
local result = {
success = false,
response_time = 0,
error = ""
}
function latency_test(addr)
local result = { success = false, response_time = 0, error = "" }
if not domain then
if not addr then
result.error = "Missing domain parameter"
luci.http.prepare_content("application/json")
luci.http.write_json(result)
return
return result
end
local test_domain = domain
local test_url
if test_domain:match("^https?://") then
test_domain = test_domain:gsub("^https?://([^/]+)/?.*$", "%1")
if addr:match("^https?://") then
addr = addr:gsub("^https?://([^/]+)/?.*$", "%1")
end
if domain == "https://raw.githubusercontent.com/" or test_domain == "raw.githubusercontent.com" then
test_url = "https://raw.githubusercontent.com/vernesong/OpenClash/dev/img/logo.png"
local urls = {}
if addr == "raw.githubusercontent.com" then
table.insert(urls, "https://raw.githubusercontent.com/vernesong/OpenClash/dev/img/logo.png")
else
test_url = "https://" .. test_domain .. "/favicon.ico"
table.insert(urls, "https://" .. addr .. "/favicon.ico")
end
local cmd = string.format(
'curl -sL -m 5 --connect-timeout 3 --retry 2 -w "%%{http_code},%%{time_total},%%{time_connect},%%{time_appconnect}" "%s" -o /dev/null 2>/dev/null',
test_url
)
table.insert(urls, "https://" .. addr)
local output = luci.sys.exec(cmd)
for i, test_url in ipairs(urls) do
local cmd = string.format(
'curl -sI -m 5 --connect-timeout 3 --retry 2 -w "%%{http_code},%%{time_total},%%{time_connect},%%{time_appconnect}" "%s" -o /dev/null 2>/dev/null',
test_url
)
if output and output ~= "" then
local http_code, time_total, time_connect, time_appconnect = output:match("(%d+),([%d%.]+),([%d%.]+),([%d%.]+)")
local output = luci.sys.exec(cmd)
if output and output ~= "" then
local http_code, time_total, time_connect, time_appconnect =
output:match("(%d+),([%d%.]+),([%d%.]+),([%d%.]+)")
if http_code and tonumber(http_code) then
local code = tonumber(http_code)
local response_time = 0
if time_appconnect and tonumber(time_appconnect) and tonumber(time_appconnect) > 0 then
response_time = math.floor(tonumber(time_appconnect) * 1000)
elseif time_connect and tonumber(time_connect) then
response_time = math.floor(tonumber(time_connect) * 1000)
else
response_time = math.floor((tonumber(time_total) or 0) * 1000)
if not http_code then
http_code, time_total, time_appconnect = output:match("(%d+),([%d%.]+),([%d%.]+)")
time_connect = nil
end
if code >= 200 and code < 400 then
result.success = true
result.response_time = response_time
elseif code == 403 or code == 404 then
result.success = true
result.response_time = response_time
else
local fallback_url
if domain == "https://raw.githubusercontent.com/" or test_domain == "raw.githubusercontent.com" then
fallback_url = "https://raw.githubusercontent.com/vernesong/OpenClash/dev/img/logo.png"
if http_code and tonumber(http_code) then
local code = tonumber(http_code)
local response_time = 0
if time_appconnect and tonumber(time_appconnect) and tonumber(time_appconnect) > 0 then
response_time = math.floor(tonumber(time_appconnect) * 1000)
elseif time_connect and tonumber(time_connect) and tonumber(time_connect) > 0 then
response_time = math.floor(tonumber(time_connect) * 1000)
else
fallback_url = "https://" .. test_domain .. "/"
response_time = math.floor((tonumber(time_total) or 0) * 1000)
end
local fallback_cmd = string.format(
'curl -sI -m 5 --connect-timeout 3 -w "%%{http_code},%%{time_total},%%{time_appconnect}" "%s" -o /dev/null 2>/dev/null',
fallback_url
)
local fallback_output = luci.sys.exec(fallback_cmd)
if fallback_output and fallback_output ~= "" then
local fb_code, fb_total, fb_appconnect = fallback_output:match("(%d+),([%d%.]+),([%d%.]+)")
if fb_code and tonumber(fb_code) then
local fb_code_num = tonumber(fb_code)
local fb_response_time = 0
if fb_appconnect and tonumber(fb_appconnect) and tonumber(fb_appconnect) > 0 then
fb_response_time = math.floor(tonumber(fb_appconnect) * 1000)
else
fb_response_time = math.floor((tonumber(fb_total) or 0) * 1000)
end
if fb_code_num >= 200 and fb_code_num < 400 then
result.success = true
result.response_time = fb_response_time
elseif fb_code_num == 403 or fb_code_num == 404 then
result.success = true
result.response_time = fb_response_time
else
result.success = false
result.error = "HTTP " .. fb_code_num
result.response_time = fb_response_time
end
else
result.success = false
result.error = "Connection failed"
end
if (code >= 200 and code < 400) or code == 403 or code == 404 then
result.success = true
result.response_time = response_time
return result
else
if i == #urls then
result.success = false
result.error = "HTTP " .. code
result.response_time = response_time
return result
end
end
else
if i == #urls then
result.success = false
result.error = "Connection failed"
result.error = "Invalid response"
return result
end
end
else
result.success = false
result.error = "Invalid response"
if i == #urls then
result.success = false
result.error = "No response"
return result
end
end
else
result.success = false
result.error = "No response"
end
return result
end
function action_website_check()
local domain = luci.http.formvalue("domain")
if not domain then
luci.http.prepare_content("application/json")
luci.http.write_json({
success = false,
response_time = 0,
error = "Missing domain parameter"
})
return
end
local result = latency_test(domain)
luci.http.prepare_content("application/json")
luci.http.write_json(result)
end
@@ -2257,7 +2251,7 @@ function action_switch_oc_setting()
local value = luci.http.formvalue("value")
if not setting or not value then
luci.http.status(400, "Missing parameters")
luci.http.status(500, "Missing parameters")
return
end
@@ -2490,7 +2484,7 @@ function action_switch_oc_setting()
end
uci:commit("openclash")
else
luci.http.status(400, "Invalid setting")
luci.http.status(500, "Invalid setting")
return
end
@@ -2910,14 +2904,14 @@ function action_oc_action()
local config_file = luci.http.formvalue("config_file")
if not action then
luci.http.status(400, "Missing action parameter")
luci.http.status(500, "Missing action parameter")
return
end
if config_file and config_file ~= "" then
local config_path = "/etc/openclash/config/" .. config_file
if not fs.access(config_path) then
luci.http.status(404, "Config file not found")
luci.http.status(500, "Config file not found")
return
end
@@ -2952,7 +2946,7 @@ function action_oc_action()
luci.sys.call("ps | grep openclash | grep -v grep | awk '{print $1}' | xargs -r kill -9 >/dev/null 2>&1")
luci.sys.call("/etc/init.d/openclash restart >/dev/null 2>&1")
else
luci.http.status(400, "Invalid action parameter")
luci.http.status(500, "Invalid action parameter")
return
end
@@ -3111,7 +3105,7 @@ function action_config_file_read()
local config_file = luci.http.formvalue("config_file")
if not config_file then
luci.http.status(400, "Missing config_file parameter")
luci.http.status(500, "Missing config_file parameter")
return
end
@@ -3202,12 +3196,12 @@ function action_config_file_save()
end
if not config_file then
luci.http.status(400, "Missing config_file parameter")
luci.http.status(500, "Missing config_file parameter")
return
end
if not content then
luci.http.status(400, "Missing content parameter")
luci.http.status(500, "Missing content parameter")
return
end
@@ -3467,9 +3461,7 @@ function action_add_subscription()
end
end
if #params > 0 then
for i, param in ipairs(params) do
uci:set_list("openclash", section_id, "custom_params", param)
end
uci:set_list("openclash", section_id, "custom_params", params)
end
end
@@ -3483,9 +3475,7 @@ function action_add_subscription()
end
end
if #keywords > 0 then
for i, kw in ipairs(keywords) do
uci:set_list("openclash", section_id, "keyword", kw)
end
uci:set_list("openclash", section_id, "keyword", keywords)
end
end
@@ -3499,9 +3489,7 @@ function action_add_subscription()
end
end
if #ex_keywords > 0 then
for i, ex_kw in ipairs(ex_keywords) do
uci:set_list("openclash", section_id, "ex_keyword", ex_kw)
end
uci:set_list("openclash", section_id, "ex_keyword", ex_keywords)
end
end
@@ -3530,6 +3518,16 @@ end
function action_upload_overwrite()
local upload = luci.http.formvalue("config_file")
local filename = luci.http.formvalue("filename")
local config_values = {}
local raw_config = luci.http.formvalue("config") or ""
if raw_config ~= "" then
for line in raw_config:gmatch("[^\n]+") do
local config_value = line:match("^%s*(.-)%s*$")
if config_value and config_value ~= "" then
table.insert(config_values, config_value)
end
end
end
local enable = luci.http.formvalue("enable")
local order = luci.http.formvalue("order")
luci.http.prepare_content("application/json")
@@ -3575,6 +3573,10 @@ function action_upload_overwrite()
uci:foreach("openclash", "config_overwrite", function(s)
if s.name == section_name then
found = true
uci:delete("openclash", s[".name"], "config")
if #config_values > 0 then
uci:set_list("openclash", s[".name"], "config", config_values)
end
if s.enable == nil or (s.enable ~= nil and enable ~= nil) then
if enable == nil then
enable = 0
@@ -3600,6 +3602,10 @@ function action_upload_overwrite()
local sid = uci:add("openclash", "config_overwrite")
uci:set("openclash", sid, "name", section_name)
uci:set("openclash", sid, "type", "file")
uci:delete("openclash", sid, "config")
if #config_values > 0 then
uci:set_list("openclash", sid, "config", config_values)
end
if enable ~= nil then
uci:set("openclash", sid, "enable", tostring(enable))
else
@@ -3657,8 +3663,22 @@ function action_overwrite_subscribe_info()
local result = {}
uci:foreach("openclash", "config_overwrite", function(s)
if s.name then
local config_value = ""
if s.config then
local config_list = {}
for _, item in ipairs(s.config) do
if item and item ~= "" then
table.insert(config_list, tostring(item))
end
end
if #config_list > 0 then
config_value = config_list
end
end
result[s.name] = {
url = s.url or "",
config = config_value,
update_days = s.update_days or "",
update_hour = s.update_hour or "",
order = tonumber(s.order) or 0,
@@ -3673,7 +3693,7 @@ function action_overwrite_subscribe_info()
return
elseif method == "POST" then
if not section_name then
luci.http.status(400, "Missing filename")
luci.http.status(500, "Missing filename")
return
end
local url = luci.http.formvalue("url") or ""
@@ -3681,6 +3701,16 @@ function action_overwrite_subscribe_info()
local update_hour = luci.http.formvalue("update_hour") or ""
local order = luci.http.formvalue("order")
local param = luci.http.formvalue("param") or ""
local config_values = {}
local raw_config = luci.http.formvalue("config") or ""
if raw_config ~= "" then
for line in raw_config:gmatch("[^\n]+") do
local config_value = line:match("^%s*(.-)%s*$")
if config_value and config_value ~= "" then
table.insert(config_values, config_value)
end
end
end
typ = luci.http.formvalue("type") or typ or "file"
local enable = luci.http.formvalue("enable")
@@ -3713,6 +3743,10 @@ function action_overwrite_subscribe_info()
if s.name == old_section_name then
uci:set("openclash", s[".name"], "name", section_name)
uci:set("openclash", s[".name"], "url", url)
uci:delete("openclash", s[".name"], "config")
if #config_values > 0 then
uci:set_list("openclash", s[".name"], "config", config_values)
end
uci:set("openclash", s[".name"], "update_days", update_days)
uci:set("openclash", s[".name"], "update_hour", update_hour)
uci:set("openclash", s[".name"], "type", typ)
@@ -3754,6 +3788,10 @@ function action_overwrite_subscribe_info()
uci:foreach("openclash", "config_overwrite", function(s)
if s.name == section_name then
uci:set("openclash", s[".name"], "url", url)
uci:delete("openclash", s[".name"], "config")
if #config_values > 0 then
uci:set_list("openclash", s[".name"], "config", config_values)
end
uci:set("openclash", s[".name"], "update_days", update_days)
uci:set("openclash", s[".name"], "update_hour", update_hour)
uci:set("openclash", s[".name"], "type", typ)
@@ -3785,6 +3823,10 @@ function action_overwrite_subscribe_info()
local sid = uci:add("openclash", "config_overwrite")
uci:set("openclash", sid, "name", section_name)
uci:set("openclash", sid, "url", url)
uci:delete("openclash", sid, "config")
if #config_values > 0 then
uci:set_list("openclash", sid, "config", config_values)
end
uci:set("openclash", sid, "update_days", update_days)
uci:set("openclash", sid, "update_hour", update_hour)
uci:set("openclash", sid, "type", typ)
@@ -3835,7 +3877,7 @@ function action_overwrite_subscribe_info()
luci.http.write_json({status="success"})
return
else
luci.http.status(405, "Method Not Allowed")
luci.http.status(500, "Method Not Allowed")
end
end
@@ -3923,7 +3965,7 @@ end
function action_get_subscribe_data()
local filename = luci.http.formvalue("filename")
if not filename then
luci.http.status(400, "Bad Request")
luci.http.status(500, "Bad Request")
return
end
@@ -3941,7 +3983,7 @@ end
function action_get_subscribe_info_data()
local filename = luci.http.formvalue("filename")
if not filename then
luci.http.status(400, "Bad Request")
luci.http.status(500, "Bad Request")
return
end
luci.http.prepare_content("application/json")
@@ -11,7 +11,7 @@ local datatype = require "luci.cbi.datatypes"
-- 优化 CBI UI(新版 LuCI 专用)
local function optimize_cbi_ui()
luci.http.write([[
HTTP.write([[
<script type="text/javascript">
// 修正上移、下移按钮名称
document.querySelectorAll("input.btn.cbi-button.cbi-button-up").forEach(function(btn) {
@@ -497,15 +497,15 @@ ds.anonymous = true
ds.addremove = true
ds.sortable = true
ds.template = "openclash/tblsection"
ds.extedit = luci.dispatcher.build_url("admin/services/openclash/custom-dns-edit/%s")
ds.extedit = DISP.build_url("admin/services/openclash/custom-dns-edit/%s")
function ds.create(self, section)
local sid = TypedSection.create(self, section)
if sid then
local name = luci.http.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
local name = HTTP.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
if name and #name > 0 then
self.map.uci:set("openclash", sid, "group", name)
end
luci.http.redirect(ds.extedit % sid)
HTTP.redirect(ds.extedit % sid)
return
end
end
@@ -5,6 +5,8 @@ local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local json = require "luci.jsonc"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local sid = arg[1]
font_red = [[<b style=color:red>]]
@@ -22,9 +24,9 @@ m.description=translate("Convert Subscribe function of Online is Supported By su
"<br/>"..translate("If you need to customize the external configuration file (subscription conversion template), please write it according to the instructions, upload it to the accessible location of the external network, and fill in the address correctly when using it")..
"<br/>"..
"<br/>"..translate("If you have a recommended external configuration file (subscription conversion template), you can modify by following The file format of /usr/share/openclash/res/sub_ini.list and pr")
m.redirect = luci.dispatcher.build_url("admin/services/openclash/config-subscribe")
m.redirect = DISP.build_url("admin/services/openclash/config-subscribe")
if m.uci:get(openclash, sid) ~= "config_subscribe" then
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
return
end
@@ -182,7 +184,7 @@ o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
@@ -190,7 +192,7 @@ o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
@@ -11,7 +11,7 @@ local uci = require "luci.model.uci".cursor()
-- 优化 CBI UI(新版 LuCI 专用)
local function optimize_cbi_ui()
luci.http.write([[
HTTP.write([[
<script type="text/javascript">
// 修正上移、下移按钮名称
document.querySelectorAll("input.btn.cbi-button.cbi-button-up").forEach(function(btn) {
@@ -90,11 +90,11 @@ s.anonymous = true
s.addremove = true
s.sortable = true
s.template = "cbi/tblsection"
s.extedit = luci.dispatcher.build_url("admin/services/openclash/config-subscribe-edit/%s")
s.extedit = DISP.build_url("admin/services/openclash/config-subscribe-edit/%s")
function s.create(...)
local sid = TypedSection.create(...)
if sid then
luci.http.redirect(s.extedit % sid)
HTTP.redirect(s.extedit % sid)
return
end
end
@@ -204,14 +204,14 @@ end
btnis.write=function(a,t)
uci:set("openclash", "config", "config_path", "/etc/openclash/config/"..e[t].name)
uci:commit("openclash")
HTTP.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "config"))
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
btned=tb:option(Button,"edit",translate("Edit"))
btned.inputstyle="apply"
btned.write=function(a,t)
local file_path = "etc/openclash/config/" .. fs.basename(e[t].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "config", "%s") % file_path)
local file_path = "/etc/openclash/config/" .. fs.basename(e[t].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "config") .. "?file=" .. HTTP.urlencode(file_path))
end
btnrn=tb:option(DummyValue,"/etc/openclash/config/",translate("Rename"))
@@ -262,8 +262,8 @@ btnapply.write = function(self, t)
local action = self.map:formvalue("cbid." .. self.map.config .. "." .. t .. ".actions")
if action == "servers_manage" then
local file_path = "etc/openclash/config/" .. fs.basename(e[t].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "servers", "%s") % file_path)
local file_path = "/etc/openclash/config/" .. fs.basename(e[t].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "servers") .. "?file=" .. HTTP.urlencode(file_path))
elseif action == "copy" then
local num = 1
while true do
@@ -273,7 +273,7 @@ btnapply.write = function(self, t)
break
end
end
HTTP.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "config"))
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
elseif action == "download" then
local sPath, sFile, fd, block
sPath = "/etc/openclash/config/"..e[t].name
@@ -3,6 +3,8 @@ local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local SYS = require "luci.sys"
local DISP = require "luci.dispatcher"
local HTTP = require "luci.http"
local sid = arg[1]
font_red = [[<b style=color:red>]]
@@ -13,9 +15,9 @@ bold_off = [[</strong>]]
m = Map(openclash, translate("Add Custom DNS Servers"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/config-overwrite")
m.redirect = DISP.build_url("admin/services/openclash/config-overwrite")
if m.uci:get(openclash, sid) ~= "dns_servers" then
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
return
end
@@ -167,7 +169,7 @@ o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
@@ -175,7 +177,7 @@ o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
@@ -4,15 +4,14 @@ local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local sid = arg[1]
local file_path = ""
local file_path = fs.get_file_path_from_request()
for i = 2, #(arg) do
file_path = file_path .. "/" .. luci.http.urlencode(arg[i])
end
if not fs.isfile(file_path) and file_path ~= "" then
file_path = luci.http.urldecode(file_path)
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "servers"))
return
end
font_red = [[<b style=color:red>]]
@@ -33,9 +32,9 @@ end
m = Map(openclash, translate("Edit Group"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/servers%s" % file_path)
m.redirect = DISP.build_url("admin/services/openclash/servers") .. "?file=" .. HTTP.urlencode(file_path)
if m.uci:get(openclash, sid) ~= "groups" then
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
return
end
@@ -314,11 +313,11 @@ o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
local old_name = m.uci:get(openclash, sid, "old_name") or ""
local new_name = luci.http.formvalue("cbid.openclash." .. sid .. ".name") or m.uci:get(openclash, sid, "name")
local new_name = HTTP.formvalue("cbid.openclash." .. sid .. ".name") or m.uci:get(openclash, sid, "name")
sync_group_name(sid, old_name, new_name)
m.uci:set(openclash, sid, "old_name", new_name)
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
@@ -326,7 +325,7 @@ o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
@@ -2,19 +2,17 @@ local NXFS = require "nixio.fs"
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local fs = require "luci.openclash"
local file_path = ""
local DISP = require "luci.dispatcher"
local file_path = fs.get_file_path_from_request()
for i = 2, #(arg) do
file_path = file_path .. "/" .. luci.http.urlencode(arg[i])
end
if not fs.isfile(file_path) and file_path ~= "" then
file_path = luci.http.urldecode(file_path)
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "%s") % arg[1])
return
end
m = Map("openclash", translate("File Edit"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/"..arg[1])
m.redirect = DISP.build_url("admin", "services", "openclash", "other-file-edit", "%s") % arg[1].."?file="..HTTP.urlencode(file_path)
s = m:section(TypedSection, "openclash")
s.anonymous = true
s.addremove=false
@@ -47,14 +45,14 @@ o = a:option(Button, "Commit", " ")
o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
luci.http.redirect(m.redirect)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "%s") % arg[1])
end
m:append(Template("openclash/config_editor"))
@@ -3,16 +3,15 @@ local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local sys = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local sid = arg[1]
local fs = require "luci.openclash"
local file_path = ""
local file_path = fs.get_file_path_from_request()
for i = 2, #(arg) do
file_path = file_path .. "/" .. luci.http.urlencode(arg[i])
end
if not fs.isfile(file_path) and file_path ~= "" then
file_path = luci.http.urldecode(file_path)
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "servers"))
return
end
font_red = [[<b style=color:red>]]
@@ -33,9 +32,9 @@ end
m = Map(openclash, translate("Edit Proxy-Provider"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/servers%s" % file_path)
m.redirect = DISP.build_url("admin/services/openclash/servers") .. "?file=" .. HTTP.urlencode(file_path)
if m.uci:get(openclash, sid) ~= "proxy-provider" then
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
return
end
@@ -182,7 +181,7 @@ o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
@@ -190,7 +189,7 @@ o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
@@ -38,8 +38,8 @@ p.inputstyle="apply"
Button.render(p,x,r)
end
btned1.write=function(r,x)
local file_path = "etc/openclash/proxy_provider/" .. fs.basename(p[x].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "proxy-provider-file-manage", "%s") %file_path)
local file_path = "/etc/openclash/proxy_provider/" .. fs.basename(p[x].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "proxy-provider-file-manage") .. "?file=" .. HTTP.urlencode(file_path))
end
btndl1 = tb1:option(Button,"download1",translate("Download Config"))
@@ -38,8 +38,8 @@ g.inputstyle="apply"
Button.render(g,n,h)
end
btned1.write=function(h,n)
local file_path = "etc/openclash/rule_provider/" .. fs.basename(g[n].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "rule-providers-file-manage", "%s") %file_path)
local file_path = "/etc/openclash/rule_provider/" .. fs.basename(g[n].name)
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "other-file-edit", "rule-providers-file-manage") .. "?file=" .. HTTP.urlencode(file_path))
end
btndl2 = tb2:option(Button,"download2",translate("Download Config"))
@@ -4,16 +4,15 @@ local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local sys = require "luci.sys"
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local sid = arg[1]
local uuid = luci.sys.exec("cat /proc/sys/kernel/random/uuid")
local file_path = ""
local file_path = fs.get_file_path_from_request()
for i = 2, #(arg) do
file_path = file_path .. "/" .. luci.http.urlencode(arg[i])
end
if not fs.isfile(file_path) and file_path ~= "" then
file_path = luci.http.urldecode(file_path)
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "servers"))
return
end
font_red = [[<b style=color:red>]]
@@ -108,10 +107,10 @@ local obfs = {
m = Map(openclash, translate("Edit Server"))
m.pageaction = false
m.redirect = luci.dispatcher.build_url("admin/services/openclash/servers%s" % file_path)
m.redirect = DISP.build_url("admin/services/openclash/servers") .. "?file=" .. HTTP.urlencode(file_path)
if m.uci:get(openclash, sid) ~= "servers" then
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
return
end
@@ -158,6 +157,8 @@ o:value("http", translate("HTTP(S)"))
o:value("direct", translate("DIRECT"))
o:value("dns", translate("DNS"))
o:value("ssh", translate("SSH"))
o:value("masque", translate("MASQUE"))
o:value("trusttunnel", translate("TrustTunnel"))
o.description = translate("Using incorrect encryption mothod may causes service fail to start")
@@ -184,6 +185,8 @@ o:depends("type", "snell")
o:depends("type", "socks5")
o:depends("type", "http")
o:depends("type", "ssh")
o:depends("type", "masque")
o:depends("type", "trusttunnel")
o = s:option(Value, "port", translate("Server Port"))
o.datatype = "port"
@@ -205,6 +208,8 @@ o:depends("type", "snell")
o:depends("type", "socks5")
o:depends("type", "http")
o:depends("type", "ssh")
o:depends("type", "masque")
o:depends("type", "trusttunnel")
o = s:option(Flag, "flag_port_hopping", translate("Enable Port Hopping"))
o:depends("type", "hysteria")
@@ -508,6 +513,8 @@ o:depends({type = "snell", snell_version = "3"})
o:depends("type", "wireguard")
o:depends("type", "direct")
o:depends("type", "anytls")
o:depends("type", "masque")
o:depends("type", "trusttunnel")
o = s:option(ListValue, "udp_over_tcp", translate("udp-over-tcp"))
o.rmempty = true
@@ -549,6 +556,7 @@ o.default = "tcp"
o:value("tcp", translate("tcp"))
o:value("ws", translate("websocket (ws)"))
o:value("grpc", translate("grpc"))
o:value("xhttp", translate("xhttp"))
o:depends("type", "vless")
o = s:option(ListValue, "obfs_vmess", translate("obfs-mode"))
@@ -637,6 +645,21 @@ o.placeholder = translate("Host: v2ray.com")
o:depends("obfs_vmess", "websocket")
o:depends("obfs_vless", "ws")
o = s:option(Value, "xhttp_opts_path", translate("xhttp-opts-path"))
o.rmempty = true
o.placeholder = translate("/path")
o:depends("obfs_vless", "xhttp")
o = s:option(Value, "xhttp_opts_host", translate("xhttp-opts-host"))
o.rmempty = true
o.placeholder = translate("xxx.com")
o:depends("obfs_vless", "xhttp")
o = s:option(Value, "vless_encryption", translate("encryption"))
o.rmempty = true
o.placeholder = translate("mlkem768x25519plus.native/xorpub/random.1rtt/0rtt.(padding len).(padding gap).(X25519 Password).(ML-KEM-768 Client)...")
o:depends("obfs_vless", "tcp")
o = s:option(Value, "vless_flow", translate("flow"))
o.rmempty = true
o.default = "xtls-rprx-direct"
@@ -696,6 +719,7 @@ o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o:depends("type", "tuic")
o:depends("type", "anytls")
o:depends("type", "trusttunnel")
-- [[ TLS ]]--
o = s:option(ListValue, "tls", translate("TLS"))
@@ -743,6 +767,7 @@ o:depends("type", "http")
o:depends("type", "hysteria")
o:depends("type", "hysteria2")
o:depends("type", "anytls")
o:depends("type", "trusttunnel")
-- [[ headers ]]--
o = s:option(DynamicList, "http_headers", translate("headers"))
@@ -787,6 +812,7 @@ o:value("h2")
o:value("http/1.1")
o:depends("type", "trojan")
o:depends("type", "anytls")
o:depends("type", "trusttunnel")
-- [[ alpn ]]--
o = s:option(DynamicList, "hysteria_alpn", translate("alpn"))
@@ -936,7 +962,7 @@ o:value("true")
o:value("false")
o:depends("type", "vmess")
-- [[ AnyTLS ]]--
-- [[ AnyTLS ]] --
o = s:option(Value, "idle_session_check_interval", translate("idle-session-check-interval"))
o.rmempty = true
o.default = "30"
@@ -952,6 +978,70 @@ o.rmempty = true
o.default = "0"
o:depends("type", "anytls")
-- [[ MASQUE ]] --
o = s:option(Value, "masque_private_key", translate("private-key"))
o:depends("type", "masque")
o.rmempty = true
o = s:option(Value, "masque_public_key", translate("public-key"))
o:depends("type", "masque")
o.rmempty = true
o = s:option(Value, "masque_ip", translate("IP"))
o:depends("type", "masque")
o.rmempty = true
o = s:option(Value, "masque_ipv6", translate("IPv6"))
o:depends("type", "masque")
o.rmempty = true
o = s:option(Value, "masque_mtu", translate("MTU"))
o:depends("type", "masque")
o.rmempty = true
o = s:option(ListValue, "masque_remote_dns_resolve", translate("Remote DNS Resolve"))
o.rmempty = true
o.default = "true"
o:value("true")
o:value("false")
o:depends("type", "masque")
o = s:option(DynamicList, "masque_dns", translate("DNS"))
o.rmempty = true
o.placeholder = translate("8.8.8.8")
o:depends("type", "masque")
-- [[ TrustTunnel ]] --
o = s:option(Value, "trusttunnel_username", translate("Username"))
o.rmempty = false
o.placeholder = "user"
o:depends("type", "trusttunnel")
o = s:option(Value, "trusttunnel_password", translate("Password"))
o.password = true
o.rmempty = false
o:depends("type", "trusttunnel")
o = s:option(ListValue, "trusttunnel_health_check", translate("Health Check"))
o.rmempty = true
o.default = "true"
o:value("true")
o:value("false")
o:depends("type", "trusttunnel")
o = s:option(ListValue, "trusttunnel_quic", translate("QUIC"))
o.rmempty = true
o.default = "false"
o:value("true")
o:value("false")
o:depends("type", "trusttunnel")
o = s:option(ListValue, "trusttunnel_congestion_controller", translate("Congestion Controller"))
o.rmempty = true
o.default = "bbr"
o:value("bbr")
o:depends("type", "trusttunnel")
-- [[ Fast Open ]]--
o = s:option(ListValue, "fast_open", translate("Fast Open"))
o.rmempty = true
@@ -1010,6 +1100,7 @@ o:depends({type = "vmess", obfs_vmess = "http"})
o:depends({type = "vmess", obfs_vmess = "h2"})
o:depends({type = "vmess", obfs_vmess = "grpc"})
o:depends("type", "anytls")
o:depends("type", "trusttunnel")
-- [[ ip version ]]--
o = s:option(ListValue, "ip_version", translate("IP Version"))
@@ -1204,7 +1295,7 @@ o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit(openclash)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
@@ -1212,7 +1303,7 @@ o.inputtitle = translate("Back Settings")
o.inputstyle = "reset"
o.write = function()
m.uci:revert(openclash, sid)
luci.http.redirect(m.redirect)
HTTP.redirect(m.redirect)
end
m:append(Template("openclash/toolbar_show"))
@@ -3,18 +3,18 @@ local m, s, o
local openclash = "openclash"
local uci = luci.model.uci.cursor()
local fs = require "luci.openclash"
local file_path = ""
local HTTP = require "luci.http"
local DISP = require "luci.dispatcher"
local file_path = fs.get_file_path_from_request()
for i = 1, #(arg) do
file_path = file_path .. "/" .. luci.http.urlencode(arg[i])
end
if not fs.isfile(file_path) and file_path ~= "" then
file_path = luci.http.urldecode(file_path)
if not file_path then
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
return
end
m = Map(openclash, translate("Servers & Groups manage"))
m.pageaction = false
m.redirect = DISP.build_url("admin/services/openclash/servers") .. "?file=" .. HTTP.urlencode(file_path)
m.description=translate("Attention:")..
"<br/>"..translate("1. Before modifying the configuration file, please click the button below to read the configuration file")..
"<br/>"..translate("2. Proxy-providers address can be directly filled in the subscription link")..
@@ -28,15 +28,15 @@ gs.anonymous = true
gs.addremove = true
gs.sortable = true
gs.template = "openclash/tblsection"
gs.extedit = luci.dispatcher.build_url("admin/services/openclash/groups-config/%s"..file_path)
gs.extedit = DISP.build_url("admin/services/openclash/groups-config/%s").."?file="..file_path
function gs.create(self, section)
local sid = TypedSection.create(self, section)
if sid then
local name = luci.http.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
local name = HTTP.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
if name and #name > 0 then
self.map.uci:set("openclash", sid, "config", name)
end
luci.http.redirect(gs.extedit % sid)
HTTP.redirect(gs.extedit % sid)
return
end
end
@@ -70,15 +70,15 @@ ps.anonymous = true
ps.addremove = true
ps.sortable = true
ps.template = "openclash/tblsection"
ps.extedit = luci.dispatcher.build_url("admin/services/openclash/proxy-provider-config/%s"..file_path)
ps.extedit = DISP.build_url("admin/services/openclash/proxy-provider-config/%s").."?file="..file_path
function ps.create(self, section)
local sid = TypedSection.create(self, section)
if sid then
local name = luci.http.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
local name = HTTP.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
if name and #name > 0 then
self.map.uci:set("openclash", sid, "config", name)
end
luci.http.redirect(ps.extedit % sid)
HTTP.redirect(ps.extedit % sid)
return
end
end
@@ -111,15 +111,15 @@ ss.anonymous = true
ss.addremove = true
ss.sortable = true
ss.template = "openclash/tblsection"
ss.extedit = luci.dispatcher.build_url("admin/services/openclash/servers-config/%s"..file_path)
ss.extedit = DISP.build_url("admin/services/openclash/servers-config/%s").."?file="..file_path
function ss.create(self, section)
local sid = TypedSection.create(self, section)
if sid then
local name = luci.http.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
local name = HTTP.formvalue("cbi.cts.tagname.".. self.config .. "." .. self.sectiontype)
if name and #name > 0 then
self.map.uci:set("openclash", sid, "config", name)
end
luci.http.redirect(ss.extedit % sid)
HTTP.redirect(ss.extedit % sid)
return
end
end
@@ -223,6 +223,7 @@ o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
luci.sys.call("/usr/share/openclash/yml_groups_get.sh \"%s\" 2>/dev/null" % file_path)
HTTP.redirect(m.redirect)
end
o = a:option(Button, "Commit", " ")
@@ -230,6 +231,7 @@ o.inputtitle = translate("Commit Settings")
o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
HTTP.redirect(m.redirect)
end
o = a:option(Button, "Apply", " ")
@@ -238,13 +240,14 @@ o.inputstyle = "apply"
o.write = function()
m.uci:commit("openclash")
luci.sys.call("/usr/share/openclash/yml_groups_set.sh \"%s\" >/dev/null 2>&1 &" % file_path)
HTTP.redirect(m.redirect)
end
o = a:option(Button,"Back", " ")
o.inputtitle = translate("Back Settings")
o.inputstyle = "apply"
o.write = function()
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "openclash", "config"))
HTTP.redirect(DISP.build_url("admin", "services", "openclash", "config"))
end
m:append(Template("openclash/toolbar_show"))
@@ -18,7 +18,7 @@ end
-- 优化 CBI UI(新版 LuCI 专用)
local function optimize_cbi_ui()
luci.http.write([[
HTTP.write([[
<script type="text/javascript">
// 修正上移、下移按钮名称
document.querySelectorAll("input.btn.cbi-button.cbi-button-up").forEach(function(btn) {
@@ -31,6 +31,7 @@ local fs = require "nixio.fs"
local nutil = require "nixio.util"
local uci = require "luci.model.uci".cursor()
local SYS = require "luci.sys"
local HTTP = require "luci.http"
local type = type
local string = string
@@ -335,4 +336,24 @@ function uci_get_config(section, key)
val = uci:get("openclash", section, key)
end
return val
end
function get_file_path_from_request()
local file_path
local referer = HTTP.getenv("HTTP_REFERER")
if referer then
local _, _, file_value = referer:find("file=([^&]*)$")
if file_value and file_value ~= "" then
file_path = HTTP.urldecode(file_value)
end
end
if not file_path or file_path == "/" then
file_path = HTTP.formvalue("file")
if not file_path then
file_path = HTTP.urldecode(file_path)
end
end
return file_path
end
@@ -110,6 +110,12 @@
white-space: nowrap;
flex: 1 1 auto;
}
.oc .config-editor-title #editTitle,
.oc .config-editor-title #config-file-name {
user-select: text;
-webkit-user-select: text;
cursor: text;
}
.oc .config-editor-title .config-file-name {
color: var(--primary-color);
font-weight: 700;
@@ -362,6 +368,149 @@
min-width: 0;
box-sizing: border-box;
}
.oc .overwrite-config-dropdown {
position: relative;
width: 100%;
}
.oc .overwrite-config-dropdown-btn {
width: 100%;
min-height: 34px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
cursor: pointer;
font-size: 13px;
line-height: 1.4;
}
.oc .overwrite-config-dropdown.form-select-wrapper {
width: 100%;
}
.oc .overwrite-config-dropdown-btn.form-select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
}
.oc .overwrite-config-dropdown-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: calc(100% - 20px);
text-align: left;
}
.oc .overwrite-config-dropdown-arrow {
margin-left: 8px;
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid var(--text-secondary);
flex-shrink: 0;
}
.oc .overwrite-config-dropdown-panel {
display: none;
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
max-height: 220px;
overflow-y: auto;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
background: var(--bg-white);
box-shadow: var(--shadow-md);
z-index: 10003;
}
.oc .overwrite-config-dropdown.open .overwrite-config-dropdown-panel {
display: block;
}
.oc .overwrite-config-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 10px;
border-bottom: 1px solid var(--border-light);
background: var(--bg-white);
color: var(--text-primary);
cursor: pointer;
}
.oc .overwrite-config-option:last-child {
border-bottom: none;
}
.oc .overwrite-config-option:hover {
background: var(--primary-color);
color: var(--select-hover);
}
.oc[data-darkmode="true"] .overwrite-config-option {
background: var(--bg-gray);
border-bottom-color: var(--border-light);
}
.oc[data-darkmode="true"] .overwrite-config-option:hover {
background: var(--primary-color);
color: var(--select-hover);
}
.oc .overwrite-config-option:hover .overwrite-config-option-state {
border-color: var(--select-hover);
}
.oc[data-darkmode="true"] .overwrite-config-option-state {
background: var(--bg-white);
border-color: var(--border-color);
}
.oc .overwrite-config-option.disabled-by-all {
cursor: not-allowed;
opacity: 0.55;
background: var(--bg-gray);
color: var(--text-secondary);
}
.oc .overwrite-config-option.disabled-by-all:hover {
background: var(--bg-gray);
color: var(--text-secondary);
}
.oc .overwrite-config-option.disabled-by-all .overwrite-config-option-state {
border-color: var(--border-light);
color: transparent;
}
.oc .overwrite-config-option-left {
display: flex;
align-items: center;
min-width: 0;
flex: 1;
}
.oc .overwrite-config-option-left input[type="checkbox"] {
position: absolute;
opacity: 0;
pointer-events: none;
}
.oc .overwrite-config-option-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
}
.oc .overwrite-config-option-state {
flex-shrink: 0;
width: 16px;
height: 16px;
border: 1px solid var(--border-color);
border-radius: 3px;
display: inline-flex;
align-items: center;
justify-content: center;
color: transparent;
background: var(--bg-white);
font-size: 12px;
line-height: 1;
}
.oc .overwrite-config-option.selected .overwrite-config-option-state {
border-color: var(--primary-color);
background: var(--primary-color);
color: var(--select-hover);
}
.oc .overwrite-config-dropdown.disabled .overwrite-config-dropdown-btn {
opacity: 0.6;
cursor: not-allowed;
}
.oc .overwrite-card-row {
display: flex;
flex-wrap: nowrap;
@@ -1407,6 +1556,11 @@ var ConfigEditor = {
.then(function(data) {
if (data.status === 'success') {
self.overwriteSubInfo = (data && data.data) ? data.data : {};
Object.keys(self.overwriteSubInfo).forEach(function(key) {
var subInfo = self.overwriteSubInfo[key] || {};
subInfo.config = self.parseOverwriteConfigValue(subInfo.config);
self.overwriteSubInfo[key] = subInfo;
});
self.overwriteFiles.sort(function(a, b) {
var an = a.name || (a.path ? a.path.split('/').pop() : '');
var bn = b.name || (b.path ? b.path.split('/').pop() : '');
@@ -1530,6 +1684,7 @@ var ConfigEditor = {
formData.append('update_hour', sub.update_hour || '');
formData.append('order', (typeof sub.order !== 'undefined' && sub.order !== null && sub.order !== '') ? sub.order : idx);
formData.append('param', sub.param || '');
formData.append('config', self.parseOverwriteConfigValue(sub.config).join('\n'));
formData.append('enable', newEnable);
fetch('/cgi-bin/luci/admin/services/openclash/overwrite_subscribe_info', {
method: 'POST',
@@ -1590,6 +1745,7 @@ var ConfigEditor = {
formData.append('update_hour', sub.update_hour || '');
formData.append('order', (typeof sub.order !== 'undefined' && sub.order !== null && sub.order !== '') ? sub.order : idx);
formData.append('param', sub.param || '');
formData.append('config', self.parseOverwriteConfigValue(sub.config).join('\n'));
formData.append('enable', typeof sub.enable !== 'undefined' ? sub.enable : 1);
fetch('/cgi-bin/luci/admin/services/openclash/overwrite_subscribe_info', {
method: 'POST',
@@ -2116,6 +2272,7 @@ var ConfigEditor = {
formData.append('update_days', sub.update_days || '');
formData.append('update_hour', sub.update_hour || '');
formData.append('param', sub.param || '');
formData.append('config', self.parseOverwriteConfigValue(sub.config).join('\n'));
formData.append('order', idx);
reqs.push(fetch('/cgi-bin/luci/admin/services/openclash/overwrite_subscribe_info', {
method: 'POST',
@@ -2127,12 +2284,225 @@ var ConfigEditor = {
});
},
getOverwriteConfigFiles: function() {
var rawList = [];
if (window.configFiles && Array.isArray(window.configFiles)) {
rawList = window.configFiles;
} else if (window.ConfigFileManager && Array.isArray(window.ConfigFileManager.configList)) {
rawList = window.ConfigFileManager.configList;
}
return rawList.map(function(file) {
if (typeof file === 'string') {
return {
name: file,
path: file
};
}
return {
name: file.name || file.filename || file.path || '',
path: file.path || file.filepath || file.name || ''
};
}).filter(function(file) {
return !!file.path;
});
},
parseOverwriteConfigValue: function(value) {
if (!value) return [];
if (Array.isArray(value)) {
return value.filter(function(item) { return !!item; });
}
return String(value).split(/[\r\n,;]+/).map(function(item) {
return item.trim();
}).filter(function(item) {
return !!item;
});
},
renderOverwriteConfigDropdown: function(configValue, dropdownId) {
var files = this.getOverwriteConfigFiles();
var selected = this.parseOverwriteConfigValue(configValue);
var selectedMap = {};
selected.forEach(function(item) {
selectedMap[item] = true;
});
if (!selected.length) {
selectedMap['all'] = true;
}
var allChecked = !!selectedMap['all'];
var allOptionHtml = `
<label class="overwrite-config-option${allChecked ? ' selected' : ''}" data-path="all" data-all-option="1">
<span class="overwrite-config-option-left">
<input type="checkbox" value="all"${allChecked ? ' checked' : ''}>
<span class="overwrite-config-option-name" title="<%:Use For All Config File%>"><%:Use For All Config File%></span>
</span>
<span class="overwrite-config-option-state">${allChecked ? '✓' : ''}</span>
</label>
`;
var fileOptionsHtml = files.length ? files.map(function(file) {
var checked = (!allChecked && (selectedMap[file.path] || selectedMap[file.name])) ? ' checked' : '';
var selectedClass = checked ? ' selected' : '';
var disabledClass = allChecked ? ' disabled-by-all' : '';
var stateText = checked ? '✓' : '';
var disabled = allChecked;
return `
<label class="overwrite-config-option${selectedClass}${disabledClass}" data-path="${file.path}">
<span class="overwrite-config-option-left">
<input type="checkbox" value="${file.path}"${checked}${disabled ? ' disabled' : ''}>
<span class="overwrite-config-option-name" title="${file.name}">${file.name}</span>
</span>
<span class="overwrite-config-option-state">${stateText}</span>
</label>
`;
}).join('') : '<div class="overwrite-config-option"><span class="overwrite-config-option-name"><%:No config files found%></span></div>';
var optionsHtml = allOptionHtml + fileOptionsHtml;
return `
<div class="overwrite-config-dropdown form-select-wrapper" id="${dropdownId}">
<button type="button" class="overwrite-config-dropdown-btn form-select">
<span class="overwrite-config-dropdown-text"><%:Use For All Config File%></span>
<span class="overwrite-config-dropdown-arrow"></span>
</button>
<div class="overwrite-config-dropdown-panel">
${optionsHtml}
</div>
</div>
`;
},
updateOverwriteConfigDropdownLabel: function(dropdown) {
if (!dropdown) return;
var textEl = dropdown.querySelector('.overwrite-config-dropdown-text');
if (!textEl) return;
var checked = dropdown.querySelectorAll('.overwrite-config-option input[type="checkbox"]:checked');
if (!checked.length) {
textEl.textContent = '<%:Use For All Config File%>';
return;
}
if (checked.length === 1) {
var oneName = checked[0].closest('.overwrite-config-option').querySelector('.overwrite-config-option-name');
textEl.textContent = oneName ? oneName.textContent : '<%:1 file selected%>';
return;
}
textEl.textContent = checked.length + ' <%:files selected%>';
},
bindOverwriteConfigDropdown: function(container) {
if (!container || container.dataset.inited === '1') return;
container.dataset.inited = '1';
var self = this;
var btn = container.querySelector('.overwrite-config-dropdown-btn');
var panel = container.querySelector('.overwrite-config-dropdown-panel');
function syncAllExclusiveState() {
var allInput = container.querySelector('.overwrite-config-option input[type="checkbox"][value="all"]');
var allOption = allInput ? allInput.closest('.overwrite-config-option') : null;
var allState = allOption ? allOption.querySelector('.overwrite-config-option-state') : null;
var allSelected = !!(allInput && allInput.checked);
if (allOption) {
allOption.classList.toggle('selected', allSelected);
}
if (allState) {
allState.textContent = allSelected ? '✓' : '';
}
container.querySelectorAll('.overwrite-config-option input[type="checkbox"]').forEach(function(input) {
if (input.value === 'all') return;
var option = input.closest('.overwrite-config-option');
var state = option ? option.querySelector('.overwrite-config-option-state') : null;
if (allSelected) {
input.checked = false;
}
input.disabled = allSelected;
if (option) {
option.classList.toggle('selected', input.checked);
option.classList.toggle('disabled-by-all', allSelected);
option.setAttribute('aria-disabled', allSelected ? 'true' : 'false');
}
if (state) {
state.textContent = input.checked ? '✓' : '';
}
});
}
this.updateOverwriteConfigDropdownLabel(container);
syncAllExclusiveState();
this.updateOverwriteConfigDropdownLabel(container);
if (btn && !btn.disabled) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
container.classList.toggle('open');
});
}
container.querySelectorAll('.overwrite-config-option input[type="checkbox"]').forEach(function(input) {
input.addEventListener('change', function() {
if (input.value === 'all') {
syncAllExclusiveState();
} else if (input.checked) {
var allInput = container.querySelector('.overwrite-config-option input[type="checkbox"][value="all"]');
if (allInput) {
allInput.checked = false;
}
syncAllExclusiveState();
}
var option = input.closest('.overwrite-config-option');
var state = option ? option.querySelector('.overwrite-config-option-state') : null;
if (option) {
option.classList.toggle('selected', input.checked);
}
if (state) {
state.textContent = input.checked ? '✓' : '';
}
self.updateOverwriteConfigDropdownLabel(container);
});
});
if (panel) {
panel.addEventListener('click', function(e) {
e.stopPropagation();
});
}
document.addEventListener('click', function(e) {
if (!container.contains(e.target)) {
container.classList.remove('open');
}
});
},
getOverwriteConfigSelection: function(root, dropdownId) {
var dropdown = root.querySelector('#' + dropdownId);
if (!dropdown) return [];
var selected = Array.from(dropdown.querySelectorAll('.overwrite-config-option input[type="checkbox"]:checked')).map(function(input) {
return input.value;
}).filter(function(value) {
return !!value;
});
if (selected.indexOf('all') !== -1) {
return ['all'];
}
return selected;
},
renderOverwriteSubForm: function(options) {
var name = options.name || '';
var sub = options.sub || {};
var readonly = !!options.readonly;
var showTabs = !!options.showTabs;
var activeTab = options.activeTab || 'file';
var fileConfigDropdown = this.renderOverwriteConfigDropdown(sub.config || '', 'overwrite-upload-config-dropdown');
var subscribeConfigDropdown = this.renderOverwriteConfigDropdown(sub.config || '', 'overwrite-subscribe-config-dropdown');
var tabsHtml = showTabs ? `
<div class="upload-mode-selector">
@@ -2160,6 +2530,10 @@ var ConfigEditor = {
var fileContent = `
<form id="overwrite-upload-form-file" style="display:${activeTab==='file'?'block':'none'};">
<div class="form-group" style="margin-bottom:20px;">
<label for="overwrite-upload-config"><%:Config File%>:</label>
${fileConfigDropdown}
</div>
<div class="upload-zone" id="overwrite-upload-zone">
<div class="upload-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
@@ -2187,6 +2561,10 @@ var ConfigEditor = {
<label><%:File Name%>:</label>
<input type="text" class="form-input" name="filename" id="overwrite-subscribe-filename" value="${name}" ${readonly ? 'readonly' : ''} placeholder="<%:Please enter a filename%>">
</div>
<div class="form-group">
<label for="overwrite-subscribe-config"><%:Config File%>:</label>
${subscribeConfigDropdown}
</div>
<div class="form-group">
<label><%:Type%>:</label>
<div class="form-select-wrapper">
@@ -2299,6 +2677,9 @@ var ConfigEditor = {
ocDiv.appendChild(overlay);
document.body.appendChild(ocDiv);
this.bindOverwriteConfigDropdown(model.querySelector('#overwrite-upload-config-dropdown'));
this.bindOverwriteConfigDropdown(model.querySelector('#overwrite-subscribe-config-dropdown'));
var tabFile = model.querySelector('#overwrite-upload-mode-file');
var tabSub = model.querySelector('#overwrite-upload-mode-subscribe');
var contentFile = model.querySelector('#overwrite-upload-content-file');
@@ -2439,11 +2820,13 @@ var ConfigEditor = {
var reader = new FileReader();
reader.onload = function(e) {
var fileContent = e.target.result;
var selectedConfigPaths = self.getOverwriteConfigSelection(model, 'overwrite-upload-config-dropdown');
var formData = new FormData();
formData.append('filename', filename);
formData.append('config_file', fileContent);
formData.append('order', self.overwriteFileCardCount + 1);
formData.append('enable', '0');
formData.append('config', selectedConfigPaths.join('\n'));
fetch('/cgi-bin/luci/admin/services/openclash/upload_overwrite', {
method: 'POST',
@@ -2479,6 +2862,7 @@ var ConfigEditor = {
var update_hour = form.querySelector('#overwrite-subscribe-update-hour').value;
var type = form.querySelector('#overwrite-subscribe-type').value;
var param = form.querySelector('#overwrite-subscribe-param').value.trim();
var selectedConfigPaths = self.getOverwriteConfigSelection(model, 'overwrite-subscribe-config-dropdown');
if (!filename) {
alert('<%:Please enter a filename%>');
return;
@@ -2501,6 +2885,7 @@ var ConfigEditor = {
formData.append('param', param);
formData.append('order', self.overwriteFileCardCount + 1);
formData.append('enable', '0');
formData.append('config', selectedConfigPaths.join('\n'));
if (type === 'http') {
formData.append('url', url);
formData.append('update_days', update_days);
@@ -2603,6 +2988,8 @@ var ConfigEditor = {
ocDiv.appendChild(overlay);
document.body.appendChild(ocDiv);
this.bindOverwriteConfigDropdown(model.querySelector('#overwrite-subscribe-config-dropdown'));
var typeSelect = model.querySelector('#overwrite-subscribe-type');
var urlGroup = model.querySelector('#overwrite-subscribe-url-group');
var updateGroup = model.querySelector('#overwrite-subscribe-update-group');
@@ -2663,6 +3050,7 @@ var ConfigEditor = {
var type = form.querySelector('#overwrite-subscribe-type').value;
var url = form.querySelector('#overwrite-subscribe-url').value.trim();
var param = form.querySelector('#overwrite-subscribe-param').value.trim();
var selectedConfigPaths = self.getOverwriteConfigSelection(model, 'overwrite-subscribe-config-dropdown');
if (!newName) {
alert('<%:Please enter a filename%>');
@@ -2689,6 +3077,8 @@ var ConfigEditor = {
formData.delete('update_days');
formData.delete('update_hour');
}
formData.delete('config');
formData.append('config', selectedConfigPaths.join('\n'));
if (newName !== name) {
formData.append('old_filename', name);
}
@@ -2761,6 +3151,13 @@ var ConfigEditor = {
}
this.hideMergeView();
this.hide();
if (window.OverwriteSubscribeManager && typeof window.OverwriteSubscribeManager.load === 'function') {
window.OverwriteSubscribeManager.load(true);
}
try {
window.dispatchEvent(new Event('oc-overwrite-updated'));
} catch (e) {}
},
updateZoom: function(newZoom) {
@@ -2800,6 +3197,29 @@ var ConfigEditor = {
this.updateZoom(100);
},
isPointOnTextContent: function(el, clientX, clientY) {
if (!el) return false;
var textWalker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null, false);
var textNode;
while ((textNode = textWalker.nextNode())) {
if (!textNode.nodeValue || !textNode.nodeValue.trim()) {
continue;
}
var range = document.createRange();
range.selectNodeContents(textNode);
var rects = range.getClientRects();
for (var i = 0; i < rects.length; i++) {
var rect = rects[i];
if (clientX >= rect.left && clientX <= rect.right && clientY >= rect.top && clientY <= rect.bottom) {
return true;
}
}
}
return false;
},
makeDraggable: function() {
var self = this;
var header = this.model.querySelector('.config-editor-header');
@@ -2807,7 +3227,12 @@ var ConfigEditor = {
var isDragging = false;
header.addEventListener('mousedown', function(e) {
if (e.target.closest('.config-editor-actions')) {
var target = e.target && e.target.nodeType === 1 ? e.target : e.target.parentElement;
if (target && target.closest('.config-editor-actions')) {
return;
}
var textEl = target ? target.closest('#editTitle, #config-file-name') : null;
if (textEl && self.isPointOnTextContent(textEl, e.clientX, e.clientY)) {
return;
}
@@ -2864,7 +3289,13 @@ var ConfigEditor = {
}
header.addEventListener('touchstart', function(e) {
if (e.target.closest('.config-editor-actions')) {
var target = e.target && e.target.nodeType === 1 ? e.target : e.target.parentElement;
if (target && target.closest('.config-editor-actions')) {
return;
}
var touch = e.touches && e.touches[0] ? e.touches[0] : null;
var textEl = target ? target.closest('#editTitle, #config-file-name') : null;
if (touch && textEl && self.isPointOnTextContent(textEl, touch.clientX, touch.clientY)) {
return;
}
if (e.touches.length !== 1) return;
@@ -933,7 +933,7 @@
}
else {
if (!refresh_http) {
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(10-5+1)+5)*1000);
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
}
if (!refresh_ip) {
if (use_router_mode) {
@@ -1074,7 +1074,7 @@
get_router_ip_info();
HTTP.runcheck();
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(10-5+1)+5)*1000);
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
if (localStorage.getItem('privacy_my_ip') !== 'true') {
refresh_ip = setInterval("get_router_ip_info()", Math.floor(Math.random()*(40-15+1)+15)*1000);
}
@@ -1097,7 +1097,7 @@
myip_Load();
HTTP.runcheck();
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(10-5+1)+5)*1000);
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
if (localStorage.getItem('privacy_my_ip') !== 'true') {
refresh_ip = setInterval("myip_Load()", Math.floor(Math.random()*(40-15+1)+15)*1000);
}
@@ -1249,7 +1249,7 @@
if (use_router_mode) {
get_router_ip_info();
HTTP.runcheck();
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(10-5+1)+5)*1000);
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
if (localStorage.getItem('privacy_my_ip') !== 'true') {
refresh_ip = setInterval("get_router_ip_info()", Math.floor(Math.random()*(40-15+1)+15)*1000);
}
@@ -1268,7 +1268,7 @@
myip_Load();
HTTP.runcheck();
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(10-5+1)+5)*1000);
refresh_http = setInterval("HTTP.runcheck()", Math.floor(Math.random()*(20-5+1)+5)*1000);
if (localStorage.getItem('privacy_my_ip') !== 'true') {
refresh_ip = setInterval("myip_Load()", Math.floor(Math.random()*(40-15+1)+15)*1000);
}
@@ -6,6 +6,7 @@
--text-primary: #374151;
--text-secondary: #64748b;
--text-title: #4d4d4d;
--select-hover: #ebebeb;
--border-color: #b1b1b1;
--border-light: #e2e8f0;
--hover-bg: #f8fafc;
@@ -37,6 +38,7 @@
--text-primary: #ebebeb;
--text-secondary: #d0cfcf;
--text-title: #e5e7eb;
--select-hover: #ebebeb;
--border-color: #939393;
--border-light: #6b7280;
--hover-bg: #374151;
@@ -62,7 +64,7 @@
border-radius: var(--radius-lg);
}
.select-popup {
.oc .select-popup {
position: fixed;
top: 50%;
left: 50%;
@@ -77,9 +79,11 @@
width: 60%;
box-shadow: var(--shadow-md);
transition: all var(--transition-normal);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.close-btn {
.oc .close-btn {
background: none;
border: 1px solid var(--border-light);
color: var(--text-secondary);
@@ -97,7 +101,7 @@
margin-left: 8px;
}
.close-btn:hover {
.oc .close-btn:hover {
background: var(--hover-bg) !important;
border-color: var(--primary-color) !important;
color: var(--primary-color) !important;
@@ -105,23 +109,23 @@
box-shadow: var(--shadow-sm) !important;
}
.close-btn:focus {
.oc .close-btn:focus {
outline: none;
box-shadow: var(--shadow-sm) !important;
border-color: var(--primary-color) !important;
}
.close-btn svg {
.oc .close-btn svg {
width: 14px !important;
height: 14px !important;
flex-shrink: 0 !important;
}
.select-popup.hidden {
.oc .select-popup.hidden {
display: none;
}
.select-popup-header {
.oc .select-popup-header {
display: flex;
align-items: center;
justify-content: space-between;
@@ -135,7 +139,7 @@
box-shadow: var(--shadow-sm);
}
.select-config-section {
.oc .select-config-section {
background: var(--bg-gray);
border: 1px solid var(--border-light);
border-radius: var(--radius-md);
@@ -144,14 +148,14 @@
box-shadow: var(--shadow-sm);
}
.config-grid {
.oc .config-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--gap-size);
align-items: center;
}
.config-item {
.oc .config-item {
display: flex;
flex-direction: column;
gap: 8px;
@@ -160,7 +164,7 @@
position: relative;
}
.config-item::after {
.oc .config-item::after {
content: '';
position: absolute;
right: 12px;
@@ -175,7 +179,7 @@
z-index: 1;
}
.config-editor-title {
.oc .config-editor-title {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
@@ -187,7 +191,7 @@
flex: 1;
}
.config-label {
.oc .config-label {
font-size: 13px;
font-weight: 600;
color: var(--text-secondary);
@@ -195,7 +199,7 @@
position: relative;
}
.config-label::after {
.oc .config-label::after {
content: '';
position: absolute;
bottom: -4px;
@@ -206,7 +210,7 @@
background-color: var(--border-color);
}
.select-class {
.oc .select-class {
width: 100% !important;
height: var(--control-height);
padding: 8px 32px 8px 12px;
@@ -222,17 +226,17 @@
min-width: 0;
}
.select-class:hover {
.oc .select-class:hover {
border-color: var(--primary-color);
}
.select-class:focus {
.oc .select-class:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.help-link {
.oc .help-link {
display: inline-flex;
align-items: center;
color: var(--primary-color);
@@ -243,7 +247,7 @@
border-radius: var(--radius-sm);
}
.select-class option {
.oc .select-class option {
width: 100%;
padding: 8px 12px;
font-size: 13px;
@@ -255,12 +259,12 @@
color: var(--text-primary);
}
.help-link:hover {
.oc .help-link:hover {
opacity: 1;
background: rgba(59, 130, 246, 0.1);
}
.select-popup-body {
.oc .select-popup-body {
max-height: 300px;
overflow-y: auto;
border: 1px solid var(--border-light);
@@ -268,7 +272,7 @@
background: var(--bg-gray);
}
.select-option {
.oc .select-option {
padding: 12px 16px;
cursor: pointer;
border-bottom: 1px solid var(--border-light);
@@ -281,31 +285,31 @@
flex-direction: row;
}
.select-option span {
.oc .select-option span {
white-space: normal;
word-break: break-all;
}
.select-option:last-child {
.oc .select-option:last-child {
border-bottom: none;
}
.select-option:hover {
.oc .select-option:hover {
background-color: var(--hover-bg);
color: var(--primary-color);
}
.select-option[data-value="custom"] {
.oc .select-option[data-value="custom"] {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(99, 102, 241, 0.1));
border-color: rgba(59, 130, 246, 0.2);
font-weight: 500;
}
.select-option[data-value="custom"]:hover {
.oc .select-option[data-value="custom"]:hover {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(99, 102, 241, 0.2));
}
.custom-option-input {
.oc .custom-option-input {
margin: 12px auto;
padding: 12px;
width: calc(100% - 24px) !important;
@@ -317,18 +321,18 @@
transition: all var(--transition-fast);
}
.custom-option-input:focus {
.oc .custom-option-input:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
}
.custom-option-input::placeholder {
.oc .custom-option-input::placeholder {
color: var(--text-secondary);
opacity: 0.7;
}
#addCustomOption {
.oc #addCustomOption {
background: var(--primary-color);
color: white;
font-weight: 500;
@@ -338,12 +342,12 @@
text-align: center;
}
#addCustomOption:hover {
.oc #addCustomOption:hover {
background: #2563eb;
color: white;
}
.cdn-status {
.oc .cdn-status {
font-size: 12px;
font-weight: 500;
padding: 4px 8px;
@@ -352,45 +356,45 @@
white-space: nowrap !important;
}
.cdn-status.fast {
.oc .cdn-status.fast {
background: rgba(5, 150, 105, 0.1);
color: var(--success-color);
}
.cdn-status.medium {
.oc .cdn-status.medium {
background: rgba(245, 158, 11, 0.1);
color: var(--warning-color);
}
.cdn-status.slow {
.oc .cdn-status.slow {
background: rgba(220, 38, 38, 0.1);
color: var(--error-color);
}
.cdn-status.error {
.oc .cdn-status.error {
background: rgba(220, 38, 38, 0.1);
color: var(--error-color);
}
@media screen and (max-width: 1200px) {
.config-grid {
.oc .config-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.config-item {
.oc .config-item {
width: 100%;
max-width: 100%;
}
.select-class {
.oc .select-class {
max-width: 100%;
font-size: 13px;
}
}
@media screen and (max-width: 768px) {
.select-popup {
.oc .select-popup {
min-width: 80%;
width: 80%;
padding: 15px;
@@ -398,76 +402,76 @@
overflow-y: auto;
}
.config-grid {
.oc .config-grid {
grid-template-columns: 1fr;
gap: 12px;
}
.config-item {
.oc .config-item {
text-align: center;
}
.select-popup-header {
.oc .select-popup-header {
font-size: 16px;
margin-bottom: 16px;
padding-bottom: 12px;
}
.select-config-section {
.oc .select-config-section {
padding: 12px;
margin-bottom: 16px;
}
.select-popup-body {
.oc .select-popup-body {
max-height: 200px;
}
.select-option {
.oc .select-option {
padding: 10px 12px;
font-size: 13px;
align-items: flex-start;
gap: 4px;
}
.custom-option-input {
.oc .custom-option-input {
font-size: 13px;
padding: 10px;
}
}
@media screen and (max-width: 575px) {
.select-popup {
.oc .select-popup {
min-width: 80%;
width: 80%;
padding: 12px;
}
.select-popup-header {
.oc .select-popup-header {
font-size: 15px;
margin-bottom: 12px;
}
.select-config-section {
.oc .select-config-section {
padding: 10px;
}
.config-label {
.oc .config-label {
font-size: 12px;
}
.select-class {
.oc .select-class {
font-size: 12px;
padding: 6px 28px 6px 10px;
}
.config-item::after {
.oc .config-item::after {
right: 10px;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
border-top: 3px solid var(--text-secondary);
}
.select-option {
.oc .select-option {
padding: 8px 10px;
font-size: 12px;
}
@@ -546,97 +550,99 @@
</style>
<body>
<div id="selectPopup" class="oc select-popup hidden">
<div class="select-popup-header">
<div class="config-editor-title">
<%:Check Update%>
</div>
<div class="config-editor-actions">
<button type="button" class="icon-btn close-btn" onclick="closeSelectPopup()" title="<%:Close%>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
</div>
<div class="select-config-section">
<div class="config-grid">
<div class="config-item">
<div class="config-label"><%:Compiled Version%></div>
<select class="select-class" id="CORE_VERSION_CDN">
<option value="linux-386"><%:linux-386%></option>
<option value="linux-amd64-v1"><%:linux-amd64-v1(x86-64)%></option>
<option value="linux-amd64-v2"><%:linux-amd64-v2(x86-64)%></option>
<option value="linux-amd64-v3"><%:linux-amd64-v3(x86-64)%></option>
<option value="linux-armv5"><%:linux-armv5%></option>
<option value="linux-armv6"><%:linux-armv6%></option>
<option value="linux-armv7"><%:linux-armv7%></option>
<option value="linux-arm64"><%:linux-arm64(armv8)%></option>
<option value="linux-loong64-abi1"><%:linux-loong64-abi1%></option>
<option value="linux-loong64-abi2"><%:linux-loong64-abi2%></option>
<option value="linux-riscv64"><%:linux-riscv64%></option>
<option value="linux-s390x"><%:linux-s390x%></option>
<option value="linux-mips-hardfloat"><%:linux-mips-hardfloat%></option>
<option value="linux-mips-softfloat"><%:linux-mips-softfloat%></option>
<option value="linux-mips64"><%:linux-mips64%></option>
<option value="linux-mips64le"><%:linux-mips64le%></option>
<option value="linux-mipsle-softfloat"><%:linux-mipsle-softfloat%></option>
<option value="linux-mipsle-hardfloat"><%:linux-mipsle-hardfloat%></option>
<option value="0"><%:Not Set%></option>
</select>
<div class="oc">
<div id="selectPopup" class="select-popup hidden">
<div class="select-popup-header">
<div class="config-editor-title">
<%:Check Update%>
</div>
<div class="config-item">
<div class="config-label"><%:Release Branch%></div>
<select class="select-class" id="RELEASE_BRANCH_CDN">
<option value="master">Master</option>
<option value="dev">Developer</option>
</select>
<div class="config-editor-actions">
<button type="button" class="icon-btn close-btn" onclick="closeSelectPopup()" title="<%:Close%>">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="config-item">
<div class="config-label">
<%:Smart Core%>
<a href="javascript:void(0);" onclick="window.open('https://github.com/vernesong/mihomo/releases', '_blank');" class="help-link" title="<%:View core infos that support smart group%>">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</a>
</div>
<div class="select-config-section">
<div class="config-grid">
<div class="config-item">
<div class="config-label"><%:Compiled Version%></div>
<select class="select-class" id="CORE_VERSION_CDN">
<option value="linux-386"><%:linux-386%></option>
<option value="linux-amd64-v1"><%:linux-amd64-v1(x86-64)%></option>
<option value="linux-amd64-v2"><%:linux-amd64-v2(x86-64)%></option>
<option value="linux-amd64-v3"><%:linux-amd64-v3(x86-64)%></option>
<option value="linux-armv5"><%:linux-armv5%></option>
<option value="linux-armv6"><%:linux-armv6%></option>
<option value="linux-armv7"><%:linux-armv7%></option>
<option value="linux-arm64"><%:linux-arm64(armv8)%></option>
<option value="linux-loong64-abi1"><%:linux-loong64-abi1%></option>
<option value="linux-loong64-abi2"><%:linux-loong64-abi2%></option>
<option value="linux-riscv64"><%:linux-riscv64%></option>
<option value="linux-s390x"><%:linux-s390x%></option>
<option value="linux-mips-hardfloat"><%:linux-mips-hardfloat%></option>
<option value="linux-mips-softfloat"><%:linux-mips-softfloat%></option>
<option value="linux-mips64"><%:linux-mips64%></option>
<option value="linux-mips64le"><%:linux-mips64le%></option>
<option value="linux-mipsle-softfloat"><%:linux-mipsle-softfloat%></option>
<option value="linux-mipsle-hardfloat"><%:linux-mipsle-hardfloat%></option>
<option value="0"><%:Not Set%></option>
</select>
</div>
<div class="config-item">
<div class="config-label"><%:Release Branch%></div>
<select class="select-class" id="RELEASE_BRANCH_CDN">
<option value="master">Master</option>
<option value="dev">Developer</option>
</select>
</div>
<div class="config-item">
<div class="config-label">
<%:Smart Core%>
<a href="javascript:void(0);" onclick="window.open('https://github.com/vernesong/mihomo/releases', '_blank');" class="help-link" title="<%:View core infos that support smart group%>">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
</a>
</div>
<select class="select-class" id="SMART_ENABLE_CDN">
<option value="0"><%:Disabled%></option>
<option value="1"><%:Enable%></option>
</select>
</div>
<select class="select-class" id="SMART_ENABLE_CDN">
<option value="0"><%:Disabled%></option>
<option value="1"><%:Enable%></option>
</select>
</div>
</div>
</div>
<div class="select-popup-body">
<div class="select-option" data-value="https://raw.githubusercontent.com/">
<span>https://raw.githubusercontent.com/ (<%:RAW address%>)</span>
<div class="select-popup-body">
<div class="select-option" data-value="https://raw.githubusercontent.com/">
<span>https://raw.githubusercontent.com/ (<%:RAW address%>)</span>
</div>
<div class="select-option" data-value="https://fastly.jsdelivr.net/">
<span>https://fastly.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="https://testingcf.jsdelivr.net/">
<span>https://testingcf.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="https://cdn.jsdelivr.net/">
<span>https://cdn.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="custom">
<span><%:Custom Your CDN URL%></span>
<span style="display: flex; align-items: center; height: 100%;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none" style="display: block;">
<polygon points="6,9 18,9 12,15"/>
</svg>
</span>
</div>
<input type="text" id="customOptionInput" class="custom-option-input" value="https://ghfast.top/" placeholder="<%:Type CDN URL, Format Like%> https://ghfast.top/" style="display: none;">
<div class="select-option" id="addCustomOption" style="display: none;"><%:Add%></div>
</div>
<div class="select-option" data-value="https://fastly.jsdelivr.net/">
<span>https://fastly.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="https://testingcf.jsdelivr.net/">
<span>https://testingcf.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="https://cdn.jsdelivr.net/">
<span>https://cdn.jsdelivr.net/</span>
</div>
<div class="select-option" data-value="custom">
<span><%:Custom Your CDN URL%></span>
<span style="display: flex; align-items: center; height: 100%;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none" style="display: block;">
<polygon points="6,9 18,9 12,15"/>
</svg>
</span>
</div>
<input type="text" id="customOptionInput" class="custom-option-input" value="https://ghfast.top/" placeholder="<%:Type CDN URL, Format Like%> https://ghfast.top/" style="display: none;">
<div class="select-option" id="addCustomOption" style="display: none;"><%:Add%></div>
</div>
</div>
</body>
File diff suppressed because it is too large Load Diff
@@ -8,6 +8,7 @@
--text-primary: #374151;
--text-secondary: #64748b;
--text-title: #4d4d4d;
--select-hover: #ebebeb;
--border-color: #b1b1b1;
--border-light: #e2e8f0;
--hover-bg: #f8fafc;
@@ -44,6 +45,7 @@
--text-primary: #ebebeb;
--text-secondary: #d0cfcf;
--text-title: #e5e7eb;
--select-hover: #ebebeb;
--border-color: #939393;
--border-light: #6b7280;
--hover-bg: #374151;
@@ -911,7 +913,7 @@
.oc .subscription-info-details {
display: flex;
flex-direction: column;
gap: 8px;
gap: 5px;
flex: 1;
min-height: 80px;
padding-left: 20px;
@@ -962,20 +964,115 @@
display: block;
}
.oc .subscription-actions-row {
width: 100%;
justify-content: flex-end;
align-items: center;
gap: 8px;
}
.oc .subscription-overwrite-tags {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex: 1;
margin-right: auto;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
padding-bottom: 2px;
}
.oc .subscription-overwrite-tags::-webkit-scrollbar {
display: none;
}
.oc .subscription-overwrite-tags.drag-scroll-enabled {
cursor: grab;
user-select: none;
}
.oc .subscription-overwrite-tags.dragging {
cursor: grabbing;
}
.oc .subscription-overwrite-tag {
height: 28px;
width: fit-content;
padding: 0 8px;
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
border: 1px solid var(--border-light);
border-radius: var(--radius-sm);
background: var(--bg-white);
color: var(--text-secondary);
font-size: 12px;
font-weight: 600;
white-space: nowrap;
word-break: keep-all;
line-height: 1;
box-sizing: border-box;
transition: all var(--transition-fast);
box-shadow: var(--shadow-sm);
}
.oc .subscription-overwrite-tag.type-http {
color: var(--primary-color);
border-color: var(--primary-color);
}
.oc .subscription-overwrite-tag.type-file {
color: var(--success-color);
border-color: var(--success-color);
}
.oc .subscription-overwrite-tag.type-unknown {
color: var(--warning-color);
border-color: var(--warning-color);
}
.oc .subscription-action-buttons {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.oc .subscription-info-text {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.4;
text-overflow: ellipsis;
overflow: hidden;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
display: block;
scrollbar-width: none;
-ms-overflow-style: none;
cursor: default;
user-select: text;
-webkit-overflow-scrolling: touch;
}
.oc .subscription-info-text::-webkit-scrollbar {
display: none;
}
.oc .subscription-info-text.is-draggable {
cursor: grab;
user-select: none;
}
.oc .subscription-info-text.dragging {
cursor: grabbing;
}
/* Multiple Providers Grid Layout */
.oc .subscription-providers-grid {
display: flex;
gap: 12px;
gap: 10px;
padding: 8px 0;
max-width: 100%;
width: 100%;
@@ -1528,6 +1625,13 @@
min-width: 24px;
}
.oc .subscription-overwrite-tag {
height: 24px;
width: fit-content;
font-size: 10px;
padding: 0 6px;
}
.oc .icon-btn svg {
width: 12px !important;
height: 12px !important;
@@ -1906,21 +2010,24 @@
</div>
<div id="subscription-info-text" class="subscription-info-text"><%:Collecting data...%></div>
</div>
<div class="card-actions">
<button type="button" class="icon-btn action-btn" id="refresh-subscription" title="<%:Refresh%>" onclick="return refreshSubscriptionInfo()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 4v6h-6"></path>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</button>
<button type="button" class="icon-btn action-btn" id="config-subscription-url" title="<%:Specify URL%>" onclick="return setSubscriptionUrl()">
<svg width="14" height="14" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="4">
<path d="M4 6H44V36H29L24 41L19 36H4V6Z"/>
<path d="M23 21H25.0025" />
<path d="M33.001 21H34.9999"/>
<path d="M13.001 21H14.9999"/>
</svg>
</button>
<div class="card-actions subscription-actions-row">
<div id="subscription-overwrite-tags" class="subscription-overwrite-tags" style="display: none;"></div>
<div class="subscription-action-buttons">
<button type="button" class="icon-btn action-btn" id="refresh-subscription" title="<%:Refresh%>" onclick="return refreshSubscriptionInfo()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 4v6h-6"></path>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path>
</svg>
</button>
<button type="button" class="icon-btn action-btn" id="config-subscription-url" title="<%:Specify URL%>" onclick="return setSubscriptionUrl()">
<svg width="14" height="14" viewBox="0 0 48 48" fill="none" stroke="currentColor" stroke-width="4">
<path d="M4 6H44V36H29L24 41L19 36H4V6Z"/>
<path d="M23 21H25.0025" />
<path d="M33.001 21H34.9999"/>
<path d="M13.001 21H14.9999"/>
</svg>
</button>
</div>
</div>
</div>
</div>
@@ -2528,6 +2635,7 @@
this.rawCurrentConfig = currentConfigFile;
this.configList = configFiles;
window.configFiles = configFiles;
this.currentConfig = currentConfigFile;
this.currentConfigIndex = -1;
@@ -2829,12 +2937,14 @@
}
SubscriptionManager.getSubscriptionInfo();
}
OverwriteSubscribeManager.render(OverwriteSubscribeManager.data);
} else {
this.currentConfig = '';
this.currentConfigIndex = -1;
SubscriptionManager.currentConfigFile = '';
this.hideSubscriptionDisplay();
this.updateNavigationArrows();
OverwriteSubscribeManager.render(OverwriteSubscribeManager.data);
}
},
@@ -2913,6 +3023,8 @@
SubscriptionManager.getSubscriptionInfo();
}
OverwriteSubscribeManager.render(OverwriteSubscribeManager.data);
return true;
},
@@ -2936,14 +3048,112 @@
updateTimer: null,
isInitialized: false,
enableOverflowDrag: function(element) {
if (!element) return;
function updateDragState() {
var isOverflowing = element.scrollWidth > element.clientWidth + 1;
if (isOverflowing) {
element.classList.add('is-draggable');
} else {
element.classList.remove('is-draggable');
element.classList.remove('dragging');
element.scrollLeft = 0;
}
}
if (!element._dragBound) {
var isDragging = false;
var startX = 0;
var startScrollLeft = 0;
var touchIdentifier = null;
function endDrag() {
if (!isDragging) return;
isDragging = false;
touchIdentifier = null;
element.classList.remove('dragging');
}
element.addEventListener('mousedown', function(e) {
if (!element.classList.contains('is-draggable')) return;
isDragging = true;
startX = e.pageX - element.getBoundingClientRect().left;
startScrollLeft = element.scrollLeft;
element.classList.add('dragging');
e.preventDefault();
});
element.addEventListener('mousemove', function(e) {
if (!isDragging) return;
var x = e.pageX - element.getBoundingClientRect().left;
var walk = (x - startX) * 1.2;
element.scrollLeft = startScrollLeft - walk;
e.preventDefault();
});
element.addEventListener('mouseleave', function() {
endDrag();
});
element.addEventListener('mouseup', function() {
endDrag();
});
element.addEventListener('touchstart', function(e) {
if (!element.classList.contains('is-draggable')) return;
if (!e.touches || e.touches.length !== 1) return;
var touch = e.touches[0];
isDragging = true;
touchIdentifier = touch.identifier;
startX = touch.pageX - element.getBoundingClientRect().left;
startScrollLeft = element.scrollLeft;
element.classList.add('dragging');
}, { passive: true });
element.addEventListener('touchmove', function(e) {
if (!isDragging || !e.touches || e.touches.length === 0) return;
var touch = null;
for (var i = 0; i < e.touches.length; i++) {
if (e.touches[i].identifier === touchIdentifier) {
touch = e.touches[i];
break;
}
}
if (!touch) return;
var x = touch.pageX - element.getBoundingClientRect().left;
var walk = (x - startX) * 1.2;
element.scrollLeft = startScrollLeft - walk;
e.preventDefault();
}, { passive: false });
element.addEventListener('touchend', function() {
endDrag();
});
element.addEventListener('touchcancel', function() {
endDrag();
});
element.addEventListener('dragstart', function(e) {
e.preventDefault();
});
element._dragBound = true;
}
updateDragState();
},
init: function() {
if (this.isInitialized) return;
this.isInitialized = true;
setTimeout(function() {
SubscriptionManager.loadSubscriptionInfo();
SubscriptionManager.startAutoUpdate();
}, 500);
SubscriptionManager.loadSubscriptionInfo();
SubscriptionManager.startAutoUpdate();
},
loadSubscriptionInfo: function() {
@@ -3023,17 +3233,18 @@
localStorage.setItem('sub_info_' + filename, JSON.stringify(status));
if (status.providers && status.providers.length === 0) {
SubscriptionManager.showNoInfo();
return;
}
SubscriptionManager.displaySubscriptionInfo(status);
return;
}
} else {
needsErrorHandling = true;
if (!cachedData) {
SubscriptionManager.showNoInfo();
}
}
if (needsErrorHandling) {
SubscriptionManager.handleError(cachedData);
SubscriptionManager.handleError(status);
}
}, true);
}
@@ -3112,6 +3323,7 @@
infoText.textContent = infoString;
infoText.title = tooltipString;
this.enableOverflowDrag(infoText);
},
displayMultipleProviders: function(providers, progressSection) {
@@ -3169,6 +3381,7 @@
infoText.textContent = infoString;
infoText.title = tooltipString;
SubscriptionManager.enableOverflowDrag(infoText);
card.appendChild(infoText);
progressSection.appendChild(card);
@@ -3298,21 +3511,23 @@
}
},
handleError: function(cachedData) {
if (!cachedData) {
this.showNoInfo();
}
handleError: function(status) {
if (this.retryCount >= this.maxRetries) {
this.retryCount = 0;
if (this.currentConfigFile && cachedData) {
localStorage.removeItem('sub_info_' + this.extractFilename(this.currentConfigFile));
if (this.currentConfigFile && status && status.providers) {
localStorage.setItem('sub_info_' + filename, JSON.stringify(status));
}
if (!status.providers || status.providers.length === 0) {
this.showNoInfo();
}
if (status.providers && status.providers.length > 0) {
SubscriptionManager.displaySubscriptionInfo(status);
}
} else {
this.retryCount++;
setTimeout(function() {
SubscriptionManager.getSubscriptionInfo();
}, 15000);
}, 5000);
}
},
@@ -3478,6 +3693,251 @@
}
};
var OverwriteSubscribeManager = {
container: null,
data: null,
isLoading: false,
_dragBound: false,
_isDragging: false,
_dragStartX: 0,
_dragStartScrollLeft: 0,
_touchIdentifier: null,
init: function() {
this.container = document.getElementById('subscription-overwrite-tags');
if (!this.container) return;
this.bindDragScroll();
this.load(false);
},
hasOverflow: function() {
if (!this.container) return false;
return this.container.scrollWidth > this.container.clientWidth + 1;
},
updateDragScrollState: function() {
if (!this.container) return;
if (this.hasOverflow()) {
this.container.classList.add('drag-scroll-enabled');
} else {
this.container.classList.remove('drag-scroll-enabled');
this.container.classList.remove('dragging');
}
},
bindDragScroll: function() {
if (!this.container || this._dragBound) {
return;
}
var self = this;
function endDrag() {
if (!self._isDragging) return;
self._isDragging = false;
self._touchIdentifier = null;
self.container.classList.remove('dragging');
}
this.container.addEventListener('mousedown', function(e) {
if (!self.hasOverflow()) {
return;
}
self._isDragging = true;
self._dragStartX = e.pageX - self.container.offsetLeft;
self._dragStartScrollLeft = self.container.scrollLeft;
self.container.classList.add('dragging');
e.preventDefault();
});
this.container.addEventListener('mousemove', function(e) {
if (!self._isDragging) {
return;
}
var x = e.pageX - self.container.offsetLeft;
var walk = (x - self._dragStartX) * 1.5;
self.container.scrollLeft = self._dragStartScrollLeft - walk;
e.preventDefault();
});
this.container.addEventListener('mouseleave', function() {
endDrag();
});
this.container.addEventListener('mouseup', function() {
endDrag();
});
this.container.addEventListener('touchstart', function(e) {
if (!self.hasOverflow() || !e.touches || e.touches.length !== 1) {
return;
}
var touch = e.touches[0];
self._isDragging = true;
self._touchIdentifier = touch.identifier;
self._dragStartX = touch.pageX - self.container.offsetLeft;
self._dragStartScrollLeft = self.container.scrollLeft;
self.container.classList.add('dragging');
}, { passive: true });
this.container.addEventListener('touchmove', function(e) {
if (!self._isDragging || !e.touches || e.touches.length === 0) {
return;
}
var touch = null;
for (var i = 0; i < e.touches.length; i++) {
if (e.touches[i].identifier === self._touchIdentifier) {
touch = e.touches[i];
break;
}
}
if (!touch) {
return;
}
var x = touch.pageX - self.container.offsetLeft;
var walk = (x - self._dragStartX) * 1.5;
self.container.scrollLeft = self._dragStartScrollLeft - walk;
e.preventDefault();
}, { passive: false });
this.container.addEventListener('touchend', function() {
endDrag();
});
this.container.addEventListener('touchcancel', function() {
endDrag();
});
this.container.addEventListener('dragstart', function(e) {
e.preventDefault();
});
this._dragBound = true;
},
load: function(force) {
if (!this.container || this.isLoading) {
return;
}
this.isLoading = true;
var self = this;
StateManager.cachedXHRGet('<%=luci.dispatcher.build_url("admin", "services", "openclash", "overwrite_subscribe_info")%>', function(x, status) {
self.isLoading = false;
if (x && x.status == 200 && status && status.status === 'success' && status.data) {
self.data = status.data;
self.render(status.data);
} else {
self.render(null);
}
}, !!force);
},
getTypeClass: function(type) {
var normalizedType = String(type || '').toLowerCase();
if (normalizedType === 'http') return 'type-http';
if (normalizedType === 'file') return 'type-file';
return 'type-unknown';
},
parseModuleConfigList: function(configValue) {
if (!Array.isArray(configValue)) {
return [];
}
return configValue.map(function(item) {
return String(item || '').trim();
}).filter(function(item) {
return !!item;
});
},
isModuleForCurrentConfig: function(moduleInfo, currentConfigPath) {
var configList = this.parseModuleConfigList(moduleInfo ? moduleInfo.config : null);
if (!configList.length) {
return true;
}
if (configList.indexOf('all') !== -1) {
return true;
}
var fullPath = String(currentConfigPath || '').trim();
if (!fullPath) {
return false;
}
return configList.indexOf(fullPath) !== -1;
},
getEnabledModules: function(data) {
if (!data || typeof data !== 'object') {
return [];
}
var currentConfigPath = ConfigFileManager.getCurrentConfig() || ConfigFileManager.getSelectedConfig();
var modules = [];
for (var moduleName in data) {
if (!Object.prototype.hasOwnProperty.call(data, moduleName)) {
continue;
}
var moduleInfo = data[moduleName] || {};
if (parseInt(moduleInfo.enable, 10) === 1 && this.isModuleForCurrentConfig(moduleInfo, currentConfigPath)) {
modules.push({
name: moduleName,
type: moduleInfo.type || 'unknown',
order: parseInt(moduleInfo.order, 10) || 0
});
}
}
modules.sort(function(a, b) {
if (a.order === b.order) {
return String(a.name).localeCompare(String(b.name));
}
return a.order - b.order;
});
return modules;
},
render: function(data) {
if (!this.container) return;
var enabledModules = this.getEnabledModules(data);
this.container.innerHTML = '';
if (enabledModules.length === 0) {
this.container.style.display = 'none';
this.updateDragScrollState();
return;
}
for (var i = 0; i < enabledModules.length; i++) {
var moduleItem = enabledModules[i];
var tag = document.createElement('span');
tag.className = 'subscription-overwrite-tag ' + this.getTypeClass(moduleItem.type);
tag.title = moduleItem.name;
tag.textContent = moduleItem.name;
if (moduleItem.type == 'http') {
tag.title += ' [<%:HTTP Module%>]';
} else if (moduleItem.type == 'file') {
tag.title += ' [<%:File Module%>]';
} else {
tag.title += ' [<%:Unknown%>]';
}
this.container.appendChild(tag);
}
this.container.style.display = 'flex';
this.updateDragScrollState();
}
};
var LogManager = {
isPolling: false,
pollTimer: null,
@@ -4029,7 +4489,12 @@
DarkModeDetector.init();
ConfigFileManager.init();
window.addEventListener('oc-overwrite-updated', function() {
OverwriteSubscribeManager.load(true);
});
setTimeout(function() {
OverwriteSubscribeManager.init();
SubscriptionManager.init();
}, 300);
@@ -4745,7 +5210,7 @@
}
function telegrampage() {
url6 = 'https://t.me/ctcgfw_openwrt_discuss';
url6 = 'https://t.me/openclash_group';
winOpen(url6);
}
@@ -5326,6 +5791,7 @@
var filename = SubscriptionManager.extractFilename(currentConfig);
localStorage.removeItem('sub_info_' + filename);
SubscriptionManager.getSubscriptionInfo();
OverwriteSubscribeManager.load(true);
return false;
}
@@ -5506,4 +5972,4 @@
DarkModeDetector.init();
}
//]]></script>
</html>
</html>
@@ -789,9 +789,9 @@ function sub_info_refresh_<%=idname%>(force) {
var save_info = (localStorage.getItem("sub_info_<%=filename%>")) ? JSON.parse(localStorage.getItem("sub_info_<%=filename%>")) : null;
var shouldFetchNew = true;
if (save_info && !force) {
if (!force) {
dispaly_progressbar('<%=idname%>', save_info);
if (save_info.get_time) {
if (save_info && save_info.get_time) {
var currentTime = Math.floor(Date.now() / 1000);
var cacheTime = parseInt(save_info.get_time);
var timeDiff = currentTime - cacheTime;
@@ -803,12 +803,6 @@ function sub_info_refresh_<%=idname%>(force) {
}
}
if (retry_<%=idname%> >= 3) {
localStorage.removeItem("sub_info_<%=filename%>");
retry_<%=idname%> = 0;
shouldFetchNew = false;
}
if (!shouldFetchNew) {
s_<%=idname%> = setTimeout("sub_info_refresh_<%=idname%>(false)", 60000*15);
return;
@@ -839,8 +833,13 @@ function sub_info_refresh_<%=idname%>(force) {
if (needsErrorHandling && retry_<%=idname%> <= 2) {
retry_<%=idname%>++;
s_<%=idname%> = setTimeout("sub_info_refresh_<%=idname%>(false)", 15000);
s_<%=idname%> = setTimeout("sub_info_refresh_<%=idname%>(false)", 5000);
} else {
retry_<%=idname%> = 0;
dispaly_progressbar('<%=idname%>', status);
if (status && status.providers) {
localStorage.setItem("sub_info_<%=filename%>", JSON.stringify(status));
}
s_<%=idname%> = setTimeout("sub_info_refresh_<%=idname%>(false)", 60000*15);
};
});
@@ -2,10 +2,10 @@
<div class="cbi-value-field" id="switch_dashboard_<%=self.option%>">
<%:Collecting data...%>
</div>
<div class="cbi-value-field" id="delete_dashboard_<%=self.option%>">
<div class="cbi-value-field" id="delete_dashboard_<%=self.option%>" style="padding-left: 5px;">
<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Delete%>" onclick="return delete_dashboard(this, '<%=self.option%>')"/>
</div>
<div class="cbi-value-field" id="default_dashboard_<%=self.option%>">
<div class="cbi-value-field" id="default_dashboard_<%=self.option%>" style="padding-left: 5px;">
<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Set to Default%>" onclick="return default_dashboard(this, '<%=self.option%>')"/>
</div>
@@ -51,11 +51,9 @@ else
end
end
local cfg_name
if os.getenv("PATH_INFO") then
local cfg_path = string.match(os.getenv("PATH_INFO"), "/config/(.+)$")
cfg_name = cfg_path and string.match(cfg_path, "([^/]+)$") or nil
end
local file_path = fs.get_file_path_from_request()
local cfg_name = (file_path and fs.basename(file_path)) or nil
if not cfg_name and uci:get("openclash", "config", "config_path") then
cfg_name = fs.basename(uci:get("openclash", "config", "config_path"))
end
@@ -392,7 +390,13 @@ local sectiontype = "_"..self.config.."_"..string.match(self.sectiontype, "[%w_]
<%- end
%> alt="<%:Edit%>" title="<%:Edit%>" />
<%- end; if self.addremove then -%>
<input class="btn cbi-button cbi-button-remove" type="submit" value="<%:Delete%>" onclick="this.form.cbi_state='del-section'; return switch_to_tab<%=sectiontype%>()" name="cbi.rts.<%=self.config%>.<%=k%>" alt="<%:Delete%>" title="<%:Delete%>" />
<%- if file_path then -%>
<input class="btn cbi-button cbi-button-remove" type="submit" value="<%:Delete%>"
onclick="this.form.cbi_state='del-section'; var redirectUrl = 'servers?file=<%=file_path%>'; var originalSubmit = this.form.onsubmit; this.form.onsubmit = function(e) { var result = true; if (originalSubmit) { result = originalSubmit.call(this, e); } if (result !== false) { setTimeout(function() { window.location.href = redirectUrl; }, 200); } return result; }; return switch_to_tab<%=sectiontype%>()"
name="cbi.rts.<%=self.config%>.<%=k%>" alt="<%:Delete%>" title="<%:Delete%>" />
<%- else -%>
<input class="btn cbi-button cbi-button-remove" type="submit" value="<%:Delete%>" onclick="this.form.cbi_state='del-section'; return switch_to_tab<%=sectiontype%>()" name="cbi.rts.<%=self.config%>.<%=k%>" alt="<%:Delete%>" title="<%:Delete%>" />
<%- end -%>
<%- end -%>
</td>
<%- end -%>
@@ -503,7 +507,6 @@ local sectiontype = "_"..self.config.."_"..string.match(self.sectiontype, "[%w_]
}
localStorage.setItem("id<%=sectiontype%>",'cbi-<%=self.config%>-<%=self.sectiontype%>');
return true;
};
function onload<%=sectiontype%>() {
@@ -2666,8 +2666,8 @@ msgstr "Inicio de TUN fallido; intentando reiniciar..."
msgid "Core Start Failed, Please Check The Log Infos!"
msgstr "Fallo al iniciar el núcleo. Revisa el registro de núcleo."
msgid "Core Status Abnormal, Please Check The Log Infos!"
msgstr "Estado del núcleo anómalo. Consulta el registro de núcleo."
msgid "Core Initial Configuration Timeout, Please Check The Log Infos!"
msgstr "Tiempo de espera agotado en la configuración inicial del núcleo, ¡consulta la información del registro!"
msgid "Forced Sniff Pure IP Connections"
msgstr "Forzar sniffing de conexiones IP puras"
@@ -3773,6 +3773,12 @@ msgstr "Editar información de módulo"
msgid "Subscription"
msgstr "Suscripción"
msgid "HTTP Module"
msgstr "Módulo HTTP"
msgid "File Module"
msgstr "Módulo de archivo"
msgid "Overwrite Module"
msgstr "Módulo de sobrescritura"
@@ -3834,4 +3840,19 @@ msgid "Serial Number"
msgstr "Número de serie"
msgid "Set Custom Overwrite Script Failed,"
msgstr "Fallo al ejecutar el script de sobrescritura personalizado,"
msgstr "Fallo al ejecutar el script de sobrescritura personalizado,"
msgid "Load YAML Override Block"
msgstr "Cargar bloque de sobrescritura YAML"
msgid "Invalid YAML Override format, skipped..."
msgstr "Formato de sobrescritura YAML no válido, omitido..."
msgid "Parse YAML Override failed:"
msgstr "Error al analizar bloque de sobrescritura YAML:"
msgid "YAML overwrite failed:"
msgstr "Error en la sobrescritura YAML:"
msgid "files selected"
msgstr "archivos seleccionados"
@@ -2664,8 +2664,8 @@ msgstr "TUN 接口启动失败,尝试重启内核..."
msgid "Core Start Failed, Please Check The Log Infos!"
msgstr "内核启动失败,请查看《内核日志》排查失败原因!"
msgid "Core Status Abnormal, Please Check The Log Infos!"
msgstr "内核状态异常,请查看《内核日志》排查异常原因!"
msgid "Core Initial Configuration Timeout, Please Check The Log Infos!"
msgstr "内核加载配置超时,请查看《内核日志》排查原因!"
msgid "Forced Sniff Pure IP Connections"
msgstr "强制探测(嗅探)所有纯 IP 的连接"
@@ -3780,6 +3780,12 @@ msgstr "编辑模块信息"
msgid "Subscription"
msgstr "订阅"
msgid "HTTP Module"
msgstr "远程模块"
msgid "File Module"
msgstr "本地模块"
msgid "Overwrite Module"
msgstr "覆写模块"
@@ -3841,4 +3847,19 @@ msgid "Serial Number"
msgstr "序号"
msgid "Set Custom Overwrite Script Failed,"
msgstr "自定义覆写命令执行失败,"
msgstr "自定义覆写命令执行失败,"
msgid "Load YAML Override Block"
msgstr "加载 YAML 覆写模块"
msgid "Invalid YAML Override format, skipped..."
msgstr "无效的 YAML 覆写格式,跳过..."
msgid "Parse YAML Override failed:"
msgstr "YAML 覆写模块解析失败:"
msgid "YAML overwrite failed:"
msgstr "YAML 覆写模块执行失败:"
msgid "files selected"
msgstr "配置已选择"
@@ -9,6 +9,7 @@ USE_PROCD=1
. $IPKG_INSTROOT/usr/share/openclash/ruby.sh
. $IPKG_INSTROOT/usr/share/openclash/log.sh
. $IPKG_INSTROOT/usr/share/openclash/uci.sh
. $IPKG_INSTROOT/usr/share/openclash/openclash_curl.sh
[ -f /etc/openwrt_release ] && {
FW4=$(command -v fw4)
@@ -35,25 +36,25 @@ add_cron()
{
[ "$(tail -n1 /etc/crontabs/root | wc -l)" -eq 0 ] && [ -n "$(cat /etc/crontabs/root 2>/dev/null)" ] && echo >> /etc/crontabs/root
[ -z "$(grep "openclash.sh" "$CRON_FILE" 2>/dev/null)" ] && {
[ "$(uci_get_config "auto_update")" -eq 1 ] && [ "$(uci_get_config "config_auto_update_mode")" -ne 1 ] && echo "0 $(uci_get_config "auto_update_time" || 1) * * $(uci_get_config "config_update_week_time" || 0) /usr/share/openclash/openclash.sh" >> $CRON_FILE
[ "$(uci_get_config "auto_update")" -eq 1 ] && [ "$(uci_get_config "config_auto_update_mode")" -ne 1 ] && echo "0 $(uci_get_config "auto_update_time" || 1) * * $(uci_get_config "config_update_week_time" || 0) /usr/share/openclash/openclash.sh #openclash-cron-task" >> $CRON_FILE
}
[ -z "$(grep "openclash_ipdb.sh" "$CRON_FILE" 2>/dev/null)" ] && {
[ "$(uci_get_config "geo_auto_update")" -eq 1 ] && echo "0 $(uci_get_config "geo_update_day_time" || 1) * * $(uci_get_config "geo_update_week_time" || 0) /usr/share/openclash/openclash_ipdb.sh" >> $CRON_FILE
[ "$(uci_get_config "geo_auto_update")" -eq 1 ] && echo "0 $(uci_get_config "geo_update_day_time" || 1) * * $(uci_get_config "geo_update_week_time" || 0) /usr/share/openclash/openclash_ipdb.sh #openclash-cron-task" >> $CRON_FILE
}
[ -z "$(grep "openclash_geosite.sh" "$CRON_FILE" 2>/dev/null)" ] && {
[ "$(uci_get_config "geosite_auto_update")" -eq 1 ] && echo "0 $(uci_get_config "geosite_update_day_time" || 1) * * $(uci_get_config "geosite_update_week_time" || 0) /usr/share/openclash/openclash_geosite.sh" >> $CRON_FILE
[ "$(uci_get_config "geosite_auto_update")" -eq 1 ] && echo "0 $(uci_get_config "geosite_update_day_time" || 1) * * $(uci_get_config "geosite_update_week_time" || 0) /usr/share/openclash/openclash_geosite.sh #openclash-cron-task" >> $CRON_FILE
}
[ -z "$(grep "openclash_geoip.sh" "$CRON_FILE" 2>/dev/null)" ] && {
[ "$(uci_get_config "geoip_auto_update")" -eq 1 ] && echo "0 $(uci_get_config "geoip_update_day_time" || 1) * * $(uci_get_config "geoip_update_week_time" || 0) /usr/share/openclash/openclash_geoip.sh" >> $CRON_FILE
[ "$(uci_get_config "geoip_auto_update")" -eq 1 ] && echo "0 $(uci_get_config "geoip_update_day_time" || 1) * * $(uci_get_config "geoip_update_week_time" || 0) /usr/share/openclash/openclash_geoip.sh #openclash-cron-task" >> $CRON_FILE
}
[ -z "$(grep "openclash_geoasn.sh" "$CRON_FILE" 2>/dev/null)" ] && {
[ "$(uci_get_config "geoasn_auto_update")" -eq 1 ] && echo "0 $(uci_get_config "geoasn_update_day_time" || 1) * * $(uci_get_config "geoasn_update_week_time" || 0) /usr/share/openclash/openclash_geoasn.sh" >> $CRON_FILE
[ "$(uci_get_config "geoasn_auto_update")" -eq 1 ] && echo "0 $(uci_get_config "geoasn_update_day_time" || 1) * * $(uci_get_config "geoasn_update_week_time" || 0) /usr/share/openclash/openclash_geoasn.sh #openclash-cron-task" >> $CRON_FILE
}
[ -z "$(grep "openclash_chnroute.sh" "$CRON_FILE" 2>/dev/null)" ] && {
[ "$(uci_get_config "chnr_auto_update")" -eq 1 ] && echo "0 $(uci_get_config "chnr_update_day_time" || 1) * * $(uci_get_config "chnr_update_week_time" || 0) /usr/share/openclash/openclash_chnroute.sh" >> $CRON_FILE
[ "$(uci_get_config "chnr_auto_update")" -eq 1 ] && echo "0 $(uci_get_config "chnr_update_day_time" || 1) * * $(uci_get_config "chnr_update_week_time" || 0) /usr/share/openclash/openclash_chnroute.sh #openclash-cron-task" >> $CRON_FILE
}
[ -z "$(grep "/etc/init.d/openclash" "$CRON_FILE" 2>/dev/null)" ] && {
[ "$(uci_get_config "auto_restart")" -eq 1 ] && echo "0 $(uci_get_config "auto_restart_day_time" || 1) * * $(uci_get_config "auto_restart_week_time" || 0) /etc/init.d/openclash restart" >> $CRON_FILE
[ "$(uci_get_config "auto_restart")" -eq 1 ] && echo "0 $(uci_get_config "auto_restart_day_time" || 1) * * $(uci_get_config "auto_restart_week_time" || 0) /etc/init.d/openclash restart #openclash-cron-task" >> $CRON_FILE
}
config_load "openclash"
@@ -65,13 +66,7 @@ add_cron()
del_cron()
{
sed -i '/openclash.sh/d' $CRON_FILE
sed -i '/openclash_ipdb.sh/d' $CRON_FILE
sed -i '/openclash_geoip.sh/d' $CRON_FILE
sed -i '/openclash_geosite.sh/d' $CRON_FILE
sed -i '/openclash_geoasn.sh/d' $CRON_FILE
sed -i '/openclash_chnroute.sh/d' $CRON_FILE
sed -i '/\/etc\/init.d\/openclash/d' $CRON_FILE
sed -i '/#openclash-cron-task/d' $CRON_FILE
sed -i '/#openclash-overwrite-download/d' $CRON_FILE
/etc/init.d/cron restart
} >/dev/null 2>&1
@@ -85,7 +80,7 @@ save_dnsmasq_server() {
}
set_dnsmasq_server() {
if [ -z "$1" ] || [ "$1" == "127.0.0.1#${dns_port}" ]; then
if [ -z "$1" ] || [ "$1" == "127.0.0.1#${dns_port}" ]; then
return
fi
@@ -209,11 +204,11 @@ change_dns() {
if [ "$1" -eq 1 ] && [ "$ipv6_dns" -eq 1 ] && [ -n "$(ip6tables -t mangle -L 2>&1 | grep -o 'Chain')" ]; then
#dnsmasq answer ipv6
uci -q set openclash.config.dnsmasq_filter_aaaa="$(uci -q get dhcp.@dnsmasq[0].filter_aaaa)"
uci -q set dhcp.@dnsmasq[0].filter_aaaa=0
uci -q set openclash.config.filter_aaaa_dns=1
else
uci -q set openclash.config.filter_aaaa_dns=0
fi
uci -q set dhcp.@dnsmasq[0].filter_aaaa=0
uci -q set openclash.config.filter_aaaa_dns=1
else
uci -q set openclash.config.filter_aaaa_dns=0
fi
uci -q commit dhcp
uci -q commit openclash
@@ -702,24 +697,25 @@ check_core_status()
ip_="ip -6"
fi
#wait 120s most for tun interface start
while ( [ -n "$(pidof clash)" ] && [ -z "$($ip_ route list |grep utun)" ] && [ "$TUN_WAIT" -le 120 ] )
#wait 300s most for core start
while ( [ -n "$(pidof clash)" ] && [ -z "$($ip_ route list |grep utun)" ] && [ "$TUN_WAIT" -le 300 ] )
do
$ip_ link set utun up
let TUN_WAIT++
sleep 1
done
if [ -n "$(pidof clash)" ] && [ -z "$($ip_ route list |grep utun)" ] && [ "$TUN_WAIT" -gt 120 ]; then
if [ -n "$(pidof clash)" ] && [ -z "$($ip_ route list |grep utun)" ] && [ "$TUN_WAIT" -gt 300 ]; then
while ( [ -n "$(pidof clash)" ] && [ -z "$($ip_ route list |grep utun)" ] && [ "$TUN_RESTART" -le 3 ] )
do
LOG_WARN "TUN Interface Start Failed, Try to Restart Again..."
start_run_core
sleep 120
let TUN_RESTART++
sleep 300
done
if [ -n "$(pidof clash)" ] && [ -z "$($ip_ route list |grep utun)" ] && [ "$TUN_RESTART" -gt 3 ]; then
LOG_WARN "TUN Interface Start Failed, Please Check The Dependence or Try to Restart Again!"
LOG_ERROR "TUN Interface Start Failed, Please Check The Dependence or Try to Restart Again!"
LOG_ERROR "Core Initial Configuration Timeout, Please Check The Log Infos!"
start_fail
fi
fi
@@ -742,7 +738,7 @@ check_core_status()
fi
else
reg4='^(([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])$'
while ( [ -n "$(pidof clash)" ] && [ "$CORE_HTTP_CODE" != "200" ] && [ "$TUN_WAIT" -le 120 ] && [ -n "$(echo ${lan_ip} | grep -Eo ${reg4})" ] )
while ( [ -n "$(pidof clash)" ] && [ "$CORE_HTTP_CODE" != "200" ] && [ "$TUN_WAIT" -le 300 ] && [ -n "$(echo ${lan_ip} | grep -Eo ${reg4})" ] )
do
CORE_HTTP_CODE=$(curl -m 5 -o /dev/null -s -w '%{http_code}' -H 'Content-Type: application/json' -H "Authorization: Bearer ${da_password}" -XGET http://${lan_ip}:${cn_port}/group)
let TUN_WAIT++
@@ -750,9 +746,10 @@ check_core_status()
done
if [ -z "$(echo ${lan_ip} | grep -Eo ${reg4})" ]; then
LOG_ERROR "LAN IP Address Get Error, Please Check The LAN Interface Setting or Choose the Correct Interface in the Setting!"
start_fail
fi
if [ "$CORE_HTTP_CODE" != "200" ]; then
LOG_ERROR "Core Status Abnormal, Please Check The Log Infos!"
if [ -n "$(pidof clash)" ] && [ "$CORE_HTTP_CODE" != "200" ]; then
LOG_ERROR "Core Initial Configuration Timeout, Please Check The Log Infos!"
start_fail
fi
fi
@@ -1443,7 +1440,7 @@ if [ -n "$common_ports" ] && [ "$common_ports" != "0" ]; then
fi
case $enable_redirect_dns in
"1")
"1")
LOG_TIP "DNS Hijacking Mode is Dnsmasq Redirect..."
;;
"2")
@@ -3244,9 +3241,9 @@ add_overwrite_cron()
fi
eval "restart_flag=\${OVERWRITE_RESTART_FLAG_${name}}"
cron_cmd="curl -fsSL -m 30 \"$url\" -o \"/etc/openclash/overwrite/$name\" >/dev/null 2>&1"
cron_cmd="$cron source /usr/share/openclash/openclash_curl.sh && DOWNLOAD_FILE_CURL \"$url\" \"/etc/openclash/overwrite/$name\" \"/etc/openclash/overwrite/$name\""
if [ "$restart_flag" = "1" ]; then
cron_cmd="$cron_cmd && /etc/init.d/openclash restart"
cron_cmd="$cron_cmd && [ \"\$?\" -eq 0 ] && /etc/init.d/openclash restart"
fi
cron_cmd="$cron_cmd #openclash-overwrite-download"
@@ -3276,6 +3273,26 @@ check_type() {
esac
}
overwrite_config_match_check()
{
local section="$1" name config
config_get "name" "$section" "name" ""
config_get "config" "$section" "config" ""
[ -z "$name" ] || [ "$name" != "$2" ] || [ -z "$config" ] && return
config_list_foreach "$section" "config" overwrite_config_match_item
}
overwrite_config_match_item()
{
local config_path_item="$1"
[ -z "$config_path_item" ] && return
[ "$config_path_item" = "all" ] && OVERWRITE_CONFIG_MATCHED=1 && return
[ "$config_path_item" = "$(uci_get_config "config_path")" ] && OVERWRITE_CONFIG_MATCHED=1
}
overwrite_file()
{
clear_overwrite_set
@@ -3404,7 +3421,11 @@ EOF
name=$(echo "$entry" | cut -d'|' -f2)
sid=$(echo "$entry" | cut -d'|' -f3)
enabled_flag=$(echo "$entry" | cut -d'|' -f4)
OVERWRITE_CONFIG_MATCHED=0
config_load "openclash"
config_foreach overwrite_config_match_check "config_overwrite" "$name"
[ "$OVERWRITE_CONFIG_MATCHED" -eq 0 ] && continue
[ "$enabled_flag" != "1" ] && continue
[ -z "$name" ] && continue
@@ -3436,14 +3457,16 @@ EOF
in_general=0
in_overwrite=0
in_yaml=0
download_file_lines=""
while IFS= read -r line || [ -n "$line" ]; do
trimmed=$(printf "%s" "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
case "$trimmed" in
"[General]"*) in_general=1; in_overwrite=0; continue;;
"[Overwrite]"*) in_general=0; in_overwrite=1; continue;;
"["*"]"*) in_general=0; in_overwrite=0; continue;;
"[General]"*) in_general=1; in_overwrite=0; in_yaml=0; continue;;
"[Overwrite]"*) in_general=0; in_overwrite=1; in_yaml=0; continue;;
"[YAML]"*) in_general=0; in_overwrite=0; in_yaml=1; continue;;
"["*"]"*) in_general=0; in_overwrite=0; in_yaml=0; continue;;
esac
[ -z "$trimmed" ] && continue
echo "$trimmed" | grep -qE '^[#;]' && continue
@@ -3483,20 +3506,13 @@ ${trimmed}"
LOG_TIP "DOWNLOAD FILE for【Download Job => file: $file, url: $url, path: $path, ua: ${ua:-null}, force: ${force:-false}】"
if command -v curl >/dev/null 2>&1; then
if [ -n "$ua" ]; then
curl -fsSL -m 30 -H "User-Agent: $ua" "$url" -o "$path" >/dev/null 2>&1
DOWNLOAD_FILE_CURL "$url" "$path" "$path" "$ua"
else
curl -fsSL -m 30 "$url" -o "$path" >/dev/null 2>&1
fi
rc=$?
else
if [ -n "$ua" ]; then
wget -T 30 --user-agent="$ua" -qO "$path" "$url" >/dev/null 2>&1
else
wget -T 30 -qO "$path" "$url" >/dev/null 2>&1
DOWNLOAD_FILE_CURL "$url" "$path" "$path"
fi
rc=$?
fi
if [ $rc -ne 0 ] && [ ! -f "$path" ]; then
if [ $rc -eq 1 ] || [ ! -f "$path" ]; then
LOG_ERROR "DOWNLOAD FILE failed for【Download Job => file: $file, url: $url, path: $path】"
download_failed=1
break
@@ -3506,12 +3522,12 @@ ${trimmed}"
LOG_TIP "Add Cron for【Cron Job => time: $cron, url: $url, path: $path, restart: ${restart:-false}】"
if ! grep -q "$url" $CRON_FILE 2>/dev/null; then
if [ -n "$ua" ]; then
cron_cmd="$cron curl -fsSL -m 30 -H \"User-Agent: $ua\" \"$url\" -o \"$path\" >/dev/null 2>&1"
cron_cmd="$cron source /usr/share/openclash/openclash_curl.sh && DOWNLOAD_FILE_CURL \"$url\" \"$path\" \"$path\" \"$ua\""
else
cron_cmd="$cron curl -fsSL -m 30 \"$url\" -o \"$path\" >/dev/null 2>&1"
cron_cmd="$cron source /usr/share/openclash/openclash_curl.sh && DOWNLOAD_FILE_CURL \"$url\" \"$path\" \"$path\""
fi
if [ "$restart" = "1" ] || [ "$restart" = "true" ]; then
cron_cmd="$cron_cmd && /etc/init.d/openclash restart"
cron_cmd="$cron_cmd && [ \"\$?\" -eq 0 ] && /etc/init.d/openclash restart"
fi
cron_cmd="$cron_cmd #openclash-overwrite-download"
echo "$cron_cmd" >> $CRON_FILE
@@ -3523,12 +3539,15 @@ ${trimmed}"
in_general=0
in_overwrite=0
in_yaml=0
yaml_content=""
while IFS= read -r line || [ -n "$line" ]; do
trimmed=$(printf "%s" "$line" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
case "$trimmed" in
"[General]"*) in_general=1; in_overwrite=0; continue;;
"[Overwrite]"*) in_general=0; in_overwrite=1; continue;;
"["*"]"*) in_general=0; in_overwrite=0; continue;;
"[General]"*) in_general=1; in_overwrite=0; in_yaml=0; continue;;
"[Overwrite]"*) in_general=0; in_overwrite=1; in_yaml=0; continue;;
"[YAML]"*) in_general=0; in_overwrite=0; in_yaml=1; continue;;
"["*"]"*) in_general=0; in_overwrite=0; in_yaml=0; continue;;
esac
[ -z "$trimmed" ] && continue
echo "$trimmed" | grep -qE '^[#;]' && continue
@@ -3573,8 +3592,6 @@ ${trimmed}"
val_key=$(eval "echo \"$val_clean\"")
if check_type "$key_u" "$val_clean"; then
uci -q set openclash.@overwrite[0]."$key_l"="$val_key"
else
LOG_WARN "skip General key type error【General Key => $name: $key_u, value: $val_key】"
fi
else
LOG_WARN "skip General key not allowed【General Key => $name: $key_u】"
@@ -3587,14 +3604,53 @@ ${trimmed}"
else
LOG_WARN "skip invalid Overwrite command【Ruby Script => $name: $trimmed】"
fi
elif [ "$in_yaml" -eq 1 ]; then
yaml_content="${yaml_content}$(eval "echo \"$line\"")"$'\n'
fi
done < "$file"
if [ -n "$yaml_content" ]; then
LOG_TIP "Load YAML Override Block【YAML Block => $name】"
cat >> "$overwrite_script" <<'EOF'
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
# YAML Override Block Processing
begin
yaml_override_content = <<-'YAML_CONTENT'
EOF
echo "$yaml_content" >> "$overwrite_script"
cat >> "$overwrite_script" <<'EOF'
YAML_CONTENT
begin
yaml_data = YAML.load(yaml_override_content)
if yaml_data.is_a?(Hash)
Value = YAML.load_file('$CONFIG_FILE')
Overwrite_Value = YAML.overwrite(Value, yaml_data)
File.open('$CONFIG_FILE', 'w') do |f|
YAML.dump(Overwrite_Value, f)
end
else
YAML.LOG_WARN('Invalid YAML Override format, skipped...')
end
rescue => e
YAML.LOG_ERROR('Parse YAML Override failed:【%s】' % [e.message])
end
rescue => e
YAML.LOG_ERROR('Parse YAML Override failed:【%s】' % [e.message])
end
" >> $LOG_FILE 2>&1
EOF
fi
cat >> "$overwrite_script" <<'EOF'
if [ -f "/tmp/yaml_openclash_ruby_parts/$OPENCLASH_OVERWRITE_SID" ]; then
ruby_code=$(cat "/tmp/yaml_openclash_ruby_parts/$OPENCLASH_OVERWRITE_SID")
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
Value = YAML.load_file('$CONFIG_FILE');
Value = YAML.load_file('$CONFIG_FILE')
threads = []
$ruby_code
threads.each(&:join)
@@ -3662,7 +3718,7 @@ get_config()
else
fakeip_range6=$(uci_get_config "fakeip_range6")
fi
[ -z "$fakeip_range6" ] && fakeip_range6="fdfe:dcba:9876::1/64"
[ -z "$fakeip_range6" ] && fakeip_range6=0
lan_interface_name=$(uci_get_config "lan_interface_name" || echo 0)
if [ "$lan_interface_name" = "0" ]; then
@@ -3820,14 +3876,14 @@ start_service()
fi
rm -rf /tmp/yaml_*
}
} >/dev/null 2>&1
echo "OpenClash Already Start!"
}
stop_service()
{
enable=$(uci_get_config "enable")
get_config
LOG_TIP "OpenClash Stoping..."
LOG_OUT "Step 1: Backup The Current Groups State..."
@@ -3869,6 +3925,7 @@ stop_service()
del_cron
clear_overwrite_set
rm -rf /tmp/openclash_jobs
rm -rf /tmp/yaml_*
} >/dev/null 2>&1
@@ -3877,7 +3934,6 @@ stop_service()
revert_dnsmasq()
{
get_config
redirect_dns=$(uci_get_config "redirect_dns")
dnsmasq_server=$(uci_get_config "dnsmasq_server")
dnsmasq_noresolv=$(uci_get_config "dnsmasq_noresolv")
@@ -3895,20 +3951,20 @@ restart()
echo "OpenClash Restart..."
LOG_TIP "OpenClash Restart..."
check_run_quick
stop
stop_service
start
}
start_watchdog()
{
procd_open_instance "openclash-watchdog"
procd_set_param command "/usr/share/openclash/openclash_watchdog.sh"
procd_close_instance
procd_set_param command "/usr/share/openclash/openclash_watchdog.sh"
procd_close_instance
}
reload_service()
{
enable=$(uci_get_config "enable")
get_config
MAX_RELOAD=10
if pidof clash >/dev/null && [ "$enable" == "1" ] && [ "$1" == "firewall" ]; then
#sleep for avoiding system unready
@@ -3935,14 +3991,12 @@ reload_service()
LOG_OUT "【${CUR_RELOAD_NUM}/$MAX_RELOAD】Reload OpenClash Firewall Rules..."
revert_firewall
do_run_mode
get_config
check_core_status &
fi
if pidof clash >/dev/null && [ "$enable" == "1" ] && [ "$1" == "manual" ]; then
LOG_OUT "Manually Reload Firewall Rules..."
revert_firewall
do_run_mode
get_config
check_core_status &
fi
if pidof clash >/dev/null && [ "$enable" == "1" ] && [ "$1" == "revert" ]; then
@@ -3952,7 +4006,6 @@ reload_service()
fi
if pidof clash >/dev/null && [ "$enable" == "1" ] && [ "$1" == "restore" ]; then
do_run_mode
get_config
# used for config subscribe, not background for avoiding system unready
check_core_status
fi
@@ -3960,12 +4013,11 @@ reload_service()
boot()
{
delay_start=$(uci_get_config "delay_start" || echo 0)
enable=$(uci_get_config "enable")
if [ "$delay_start" -gt 0 ] && [ "$enable" == "1" ]; then
LOG_OUT "Enable Delay Start, OpenClash Will Start After【$delay_start】Seconds..."
sleep "$delay_start"
fi
restart
}
delay_start=$(uci_get_config "delay_start" || echo 0)
enable=$(uci_get_config "enable")
if [ "$delay_start" -gt 0 ] && [ "$enable" == "1" ]; then
LOG_OUT "Enable Delay Start, OpenClash Will Start After【$delay_start】Seconds..."
sleep "$delay_start"
fi
restart
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 201 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,3 +1,21 @@
rule-providers:
## google:
## type: http
## path: ./rule1.yaml
## url: "https://raw.githubusercontent.com/../Google.yaml"
## interval: 600
## proxy: DIRECT
## behavior: classical
## format: yaml
## size-limit: 0
## header:
## User-Agent:
## - "mihomo/1.18.3"
## Authorization:
## - 'token 1231231'
## payload:
## - 'DOMAIN-SUFFIX,google.com'
rules:
##- PROCESS-NAME,curl,DIRECT #匹配路由自身进程(curl直连)
##- DOMAIN-SUFFIX,google.com,Proxy #匹配域名后缀(交由Proxy代理服务器组)
@@ -1,3 +1,20 @@
rule-providers:
## google:
## type: http
## path: ./rule1.yaml
## url: "https://raw.githubusercontent.com/../Google.yaml"
## interval: 600
## proxy: DIRECT
## behavior: classical
## format: yaml
## size-limit: 0
## header:
## User-Agent:
## - "mihomo/1.18.3"
## Authorization:
## - 'token 1231231'
## payload:
## - 'DOMAIN-SUFFIX,google.com'
rules:
##- DOMAIN-SUFFIX,google.com,Proxy #匹配域名后缀(交由Proxy代理服务器组)
##- DOMAIN-KEYWORD,google,Proxy #匹配域名关键字(交由Proxy代理服务器组)
@@ -73,7 +73,6 @@
# ENABLE_TCP_CONCURRENT (启用 TCP 并发, 0: 插件不覆写, 1: 启用)
# FIND_PROCESS_MODE (进程查找模式, 0: 插件不覆写, off: 关闭, always, strict)
# GLOBAL_CLIENT_FINGERPRINT (全局客户端指纹, none/random/chrome/firefox/safari/ios/android/edge/360/qq/0: 插件不覆写)
# ENABLE_UNIFIED_DELAY (启用统一延迟计算, 0: 插件不覆写, 1: 启用)
# ENABLE_RULE_PROXY (仅代理命中规则流量, 0: 禁用, 1: 启用)
@@ -209,6 +208,298 @@
#指定展示订阅信息的 URL 地址
#SUB_INFO_URL = https://default-demo.yaml
# ==========================================================
# ==== 以下为 YAML 块覆写示例 ====
# ==========================================================
# YAML 覆写块识别标记
# [YAML]
# ----------------------------------------------------------
# 操作符速查表:
# key → 默认合并(Hash 递归合并,其他直接覆盖)
# key! → 强制覆盖整个值
# key+ → 数组后置追加
# +key → 数组前置插入
# key- → 数组差集删除 / 非数组则删除该键
# key* → 批量条件更新(需配合 where/set)
# <key>后缀 → 同上(+, !, *, -),适用于键名含特殊字符时
# +<key> → 数组前置插入,适用于键名含特殊字符时
# ----------------------------------------------------------
# 1. 默认合并 - 合并哈希键值,键不存在则添加,Hash 值递归合并
# 示例:修改 dns.enable 并添加新键
# dns:
# enable: true
# cache-algorithm: lru
# 示例:直接替换标量值
# mixed-port: 10802
# allow-lan: false
# 示例:合并 tun 配置(仅修改指定字段,其余保留)
# tun:
# enable: true
# stack: gvisor
# ----------------------------------------------------------
# 2. 强制覆盖 (key!) - 强制替换整个值,不做递归合并
# 示例:替换整个 dns.fake-ip-filter 数组
# dns:
# fake-ip-filter!:
# - '*.lan'
# - 'new.domain.com'
# 示例:强制覆盖整个 rules 数组
# rules!:
# - DOMAIN-SUFFIX,example.com,DIRECT
# - MATCH,PROXY
# 示例:使用 <> 强制覆盖整个 dns 配置(键名含特殊字符时)
# <dns>!:
# enable: false
# nameserver:
# - '114.114.114.114'
# ----------------------------------------------------------
# 3. 数组后置 (key+) - 将新元素追加到数组末尾
# 示例:向 dns.nameserver 末尾追加 DNS
# dns:
# nameserver+:
# - '1.1.1.1'
# - '8.8.8.8'
# 示例:向 rules 末尾追加规则
# rules+:
# - DOMAIN-SUFFIX,example.com,REJECT
# 示例:使用 <> 后置
# dns:
# <nameserver>+:
# - '8.8.8.8'
# ----------------------------------------------------------
# 4. 数组前置 (+key / +<key>) - 将新元素插入到数组开头
# 示例:向 dns.nameserver 开头插入 DNS
# dns:
# +nameserver:
# - '223.5.5.5'
# - '119.29.29.29'
# 示例:向 rules 开头插入规则(优先匹配)
# +rules:
# - DOMAIN-SUFFIX,priority.com,DIRECT
# 示例:使用 +<> 前置插入(键名含特殊字符时)
# dns:
# +<nameserver>:
# - '119.29.29.29'
# ----------------------------------------------------------
# 5. 数组删除 (key-) - 从数组中移除指定元素,非数组则删除整个键
# 示例:从 dns.nameserver 中移除指定 DNS
# dns:
# nameserver-:
# - '8.8.8.8'
# - '8.8.4.4'
# 示例:从 rules 中移除指定规则
# rules-:
# - DOMAIN-SUFFIX,old.com,REJECT
# 示例:删除整个键(值为 null/~ 或空)
# dns:
# cache-algorithm-:
# ----------------------------------------------------------
# 6. 批量条件更新 (key*) - 按 where 条件匹配集合元素,用 set 子句更新
#
# 支持的集合类型:
# - 数组(元素为 Hash 或 String
# - Hash(值为 Hash
#
# where 条件支持:
# - 普通值相等: type: select
# - 正则匹配: name: '/^HK/'
# - 数组包含: proxies: ['proxy1'] (检查数组是否包含所有指定元素)
# - Hash 键匹配: key: 'some-key' (仅用于 Hash 类型集合)
# - 字符串匹配: value: 'some-string' (仅用于字符串数组)
#
# set 子句支持的操作符(同顶层操作符):
# field → 直接覆盖该字段
# field! → 强制覆盖该字段
# field+ → 数组后置追加
# +field → 数组前置插入
# field- → 数组差集删除 / 非数组则删除该键
# value → 替换字符串数组中的匹配项
# value: ~ → 删除字符串数组中的匹配项
# key: ~ → 删除 Hash 中的匹配键(仅 Hash 集合)
# key-: → 同上
# 示例:按 type 匹配,替换 proxy-groups 的 proxies
# proxy-groups*:
# where:
# type: select
# set:
# proxies:
# - 'new-proxy1'
# - 'new-proxy2'
# 示例:按 type 匹配,向 proxies 末尾追加节点
# proxy-groups*:
# where:
# type: select
# set:
# proxies+:
# - 'new-proxy3'
# 示例:按 type 匹配,向 proxies 开头插入节点
# proxy-groups*:
# where:
# type: url-test
# set:
# +proxies:
# - 'new-proxy1'
# 示例:按 type 匹配,从 proxies 中移除指定节点
# proxy-groups*:
# where:
# type: select
# set:
# proxies-:
# - 'old-proxy1'
# - 'old-proxy2'
# 示例:使用数组包含条件(proxies 中必须包含指定元素才匹配)
# proxy-groups*:
# where:
# type: select
# proxies:
# - 'old-proxy1'
# set:
# proxies:
# - 'new-proxy1'
# - 'new-proxy2'
# 示例:使用正则匹配 name 字段
# proxy-groups*:
# where:
# name: '/^HK/'
# set:
# +proxies:
# - 'hk-new-proxy'
# 示例:删除 proxy-groups 中某个字段(set value 为 null/~
# proxy-groups*:
# where:
# name: Proxy
# set:
# icon-:
# 示例:按 type 匹配修改 proxies 中 socks5 节点端口
# proxies*:
# where:
# type: socks5
# set:
# port: 1080
# 示例:更新 hosts Hash 中的指定键
# hosts*:
# where:
# key: '*.mihomo.dev'
# set:
# '*.mihomo.dev': '::1'
# 示例:删除 hosts Hash 中的匹配键
# hosts*:
# where:
# key: '*.old.dev'
# set:
# key-:
# 示例:替换字符串数组中的匹配项
# rules*:
# where:
# value: 'DOMAIN-SUFFIX,old.com,REJECT'
# set:
# value: 'DOMAIN-SUFFIX,new.com,DIRECT'
# 示例:使用正则匹配并删除字符串数组中的规则
# rules*:
# where:
# value: '/,REJECT$/'
# set:
# value:
# 示例:删除 fake-ip-filter 中的指定项
# dns:
# fake-ip-filter*:
# where:
# value: '+.localhost.ptlogin2.qq.com'
# set:
# value:
# 示例:使用 <> 批量更新
# <proxy-groups>*:
# where:
# type: url-test
# set:
# interval: 300
# ----------------------------------------------------------
# 7. 组合操作 - 在同一块内同时使用多个操作符
# 示例:同时前置和后置追加 DNS(必须写在同一 dns 块内)
# dns:
# nameserver+:
# - '1.0.0.1'
# +nameserver:
# - '119.29.29.29'
# 示例:同时删除旧 DNS 并前置插入新 DNS
# dns:
# nameserver-:
# - '8.8.8.8'
# +nameserver:
# - '223.5.5.5'
# ----------------------------------------------------------
# 8. <key> 语法补充 - 适用于键名含特殊字符时
# 后缀支持:+(后置)、-(删除)、!(强制覆盖)、*(批量更新),空串为默认合并
# 前置插入请使用 +<key> 写法(前缀形式)
# 示例:使用 <> 后置追加
# dns:
# <nameserver>+:
# - '8.8.8.8'
# 示例:使用 <> 删除数组元素
# dns:
# <nameserver>-:
# - '8.8.8.8'
# 示例:使用 <> 强制覆盖
# dns:
# <nameserver>!:
# - '114.114.114.114'
# 示例:使用 <> 批量更新(等同于 key*
# <proxy-groups>*:
# where:
# type: url-test
# set:
# interval: 300
# 示例:使用 +<> 前置插入(键名含特殊字符时的前置写法)
# dns:
# +<nameserver>:
# - '119.29.29.29'
# ==========================================================
# ==== 以下为仅修改插件设置示例 ====
# ==========================================================
@@ -166,8 +166,10 @@ if [ -f "/tmp/openclash.bak" ]; then
cp -rf "/tmp/openclash/." "/etc/openclash/" >/dev/null 2>&1
#ui
if [ -d "/tmp/openclash_ui/" ]; then
rm -rf "/usr/share/openclash/ui/" >/dev/null 2>&1
cp -rf "/tmp/openclash_ui/." "/usr/share/openclash/ui/" >/dev/null 2>&1
if [ -d "/tmp/openclash_ui/metacubexd/" ] || [ -d "/tmp/openclash_ui/zashboard/" ] || [ -d "/tmp/openclash_ui/yacd/" ] || [ -d "/tmp/openclash_ui/dashboard/" ]; then
rm -rf "/usr/share/openclash/ui/" >/dev/null 2>&1
cp -rf "/tmp/openclash_ui/." "/usr/share/openclash/ui/" >/dev/null 2>&1
fi
rm -rf "/tmp/openclash_ui/" >/dev/null 2>&1
fi
#pac
@@ -1,119 +1,442 @@
module YAML
class << self
alias_method :load, :unsafe_load if YAML.respond_to? :unsafe_load
alias_method :original_dump, :dump
alias_method :original_load_file, :load_file
end
class << self
alias_method :load, :unsafe_load if YAML.respond_to? :unsafe_load
alias_method :original_dump, :dump
alias_method :original_load_file, :load_file
end
def self.LOG(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Info] " + "#{info}"
end
def self.LOG(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Info] " + "#{info}"
end
def self.LOG_ERROR(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Error] " + "#{info}"
end
def self.LOG_ERROR(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Error] " + "#{info}"
end
def self.LOG_WARN(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Warning] " + "#{info}"
end
def self.LOG_WARN(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Warning] " + "#{info}"
end
def self.LOG_TIP(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Tip] " + "#{info}"
end
def self.LOG_TIP(info)
puts Time.new.strftime("%Y-%m-%d %H:%M:%S") + " [Tip] " + "#{info}"
end
# Keep `short-id` as string before YAML parsing so leading zeros are preserved.
# This is required for REALITY short-id values like `00000000`.
def self.load_file(filename, *args, **kwargs)
yaml_content = File.read(filename)
processed_content = fix_short_id_quotes(yaml_content)
# Keep `short-id` as string before YAML parsing so leading zeros are preserved.
# This is required for REALITY short-id values like `00000000`.
def self.load_file(filename, *args, **kwargs)
yaml_content = File.read(filename)
processed_content = fix_short_id_quotes(yaml_content)
if kwargs.empty?
load(processed_content, *args)
else
load(processed_content, *args, **kwargs)
end
end
if kwargs.empty?
load(processed_content, *args)
else
load(processed_content, *args, **kwargs)
end
end
def self.dump(obj, io = nil, **options)
begin
if io.nil?
yaml_content = original_dump(obj, **options)
fix_short_id_quotes(yaml_content)
elsif io.respond_to?(:write)
require 'stringio'
temp_io = StringIO.new
original_dump(obj, temp_io, **options)
yaml_content = temp_io.string
processed_content = fix_short_id_quotes(yaml_content)
io.write(processed_content)
io
else
yaml_content = original_dump(obj, io, **options)
fix_short_id_quotes(yaml_content)
end
rescue => e
LOG_ERROR("Write file failed:【%s】" % [e.message])
nil
end
end
def self.dump(obj, io = nil, **options)
begin
if io.nil?
yaml_content = original_dump(obj, **options)
fix_short_id_quotes(yaml_content)
elsif io.respond_to?(:write)
require 'stringio'
temp_io = StringIO.new
original_dump(obj, temp_io, **options)
yaml_content = temp_io.string
processed_content = fix_short_id_quotes(yaml_content)
io.write(processed_content)
io
else
yaml_content = original_dump(obj, io, **options)
fix_short_id_quotes(yaml_content)
end
rescue => e
LOG_ERROR("Write file failed:【%s】" % [e.message])
nil
end
end
private
private
SHORT_ID_REGEX = /^(\s*)short-id:\s*(.*)$/
LIST_ITEM_REGEX = /^(\s*)-\s*(.*)$/
KEY_REGEX = /^(\s*)([a-zA-Z0-9_-]+):\s*(.*)$/
QUOTED_VALUE_REGEX = /^["'].*["']$/
SHORT_ID_REGEX = /^(\s*)short-id:\s*(.*)$/
LIST_ITEM_REGEX = /^(\s*)-\s*(.*)$/
KEY_REGEX = /^(\s*)([a-zA-Z0-9_-]+):\s*(.*)$/
QUOTED_VALUE_REGEX = /^(["'].*["']|null)$/
# Inline map support, e.g. reality-opts: { ..., short-id: 00000000 }
INLINE_SHORT_ID_REGEX = /(short-id:\s*)(?!["'\[])([^\s,"'{}\[\]\n\r]+)(?=\s*(?:[,}\]\n\r]|$))/m.freeze
# Inline map support, e.g. reality-opts: { ..., short-id: 00000000 }
INLINE_SHORT_ID_REGEX = /(short-id:\s*)(?!["'\[]|null)([^\s,"'{}\[\]\n\r]+)(?=\s*(?:[,}\]\n\r]|$))/m.freeze
def self.fix_short_id_quotes(yaml_content)
return yaml_content unless yaml_content.include?('short-id:')
def self.fix_short_id_quotes(yaml_content)
return yaml_content unless yaml_content.include?('short-id:')
begin
# First, normalize inline-map style unquoted short-id.
processed = yaml_content.gsub(INLINE_SHORT_ID_REGEX) do
"#{$1}\"#{$2}\""
end
begin
# First, normalize inline-map style unquoted short-id.
processed = yaml_content.gsub(INLINE_SHORT_ID_REGEX) do
"#{$1}\"#{$2}\""
end
lines = processed.lines
short_id_indices = lines.each_index.select { |i| lines[i] =~ SHORT_ID_REGEX }
short_id_indices.each do |short_id_index|
line = lines[short_id_index]
if line =~ SHORT_ID_REGEX
indent = $1
value = $2.strip
if value.empty?
(short_id_index + 1...lines.size).each do |i|
line = lines[i]
next if line.strip.empty?
if line[/^\s*/].length <= short_id_indent_len
break
end
if line =~ LIST_ITEM_REGEX
indent = $1
value = $2.strip
if value =~ KEY_REGEX
break
end
if value !~ QUOTED_VALUE_REGEX
lines[i] = "#{indent}- \"#{value}\"\n"
end
elsif line =~ KEY_REGEX
break
end
end
else
if value !~ QUOTED_VALUE_REGEX
lines[short_id_index] = "#{indent}short-id: \"#{value}\"\n"
end
end
end
end
lines.join
rescue => e
LOG_ERROR("Fix short-id values type failed:【%s】" % [e.message])
yaml_content
end
end
end
lines = processed.lines
short_id_indices = lines.each_index.select { |i| lines[i] =~ SHORT_ID_REGEX }
short_id_indices.each do |short_id_index|
line = lines[short_id_index]
if line =~ SHORT_ID_REGEX
indent = $1
value = $2.strip
if value.empty?
(short_id_index + 1...lines.size).each do |i|
line = lines[i]
next if line.strip.empty?
if line[/^\s*/].length <= indent.length
break
end
if line =~ LIST_ITEM_REGEX
indent = $1
value = $2.strip
if value =~ KEY_REGEX
break
end
if value !~ QUOTED_VALUE_REGEX
lines[i] = "#{indent}- \"#{value}\"\n"
end
elsif line =~ KEY_REGEX
break
end
end
else
if value !~ QUOTED_VALUE_REGEX
lines[short_id_index] = "#{indent}short-id: \"#{value}\"\n"
end
end
end
end
lines.join
rescue => e
LOG_ERROR("Fix short-id values type failed:【%s】" % [e.message])
yaml_content
end
end
def self.overwrite(base, override)
return override if base.nil?
return base if override.nil?
current_key = nil
current_operation = nil
begin
case override
when Hash
result = base.is_a?(Hash) ? base.dup : {}
override.each do |key, value|
current_key = key
processed_key, operation = parse_key(key)
current_operation = operation
applied = apply_operation(result[processed_key], value, operation)
if applied.equal?(DELETED_SENTINEL)
result.delete(processed_key)
else
result[processed_key] = applied
end
end
result
else
override
end
rescue => e
LOG_ERROR("YAML overwrite failed:【key: %s, operation: %s, error: %s】" % [current_key, current_operation, e.message])
base
end
end
private
def self.parse_key(key)
key_str = key.to_s
# +<key>
if key_str.start_with?('+<') && key_str.include?('>')
close_idx = key_str.index('>')
inner_key = key_str[2...close_idx]
return inner_key, :prepend_array
end
# <key>suffix
if key_str.start_with?('<') && key_str.include?('>')
close_idx = key_str.index('>')
inner_key = key_str[1...close_idx]
suffix = key_str[(close_idx + 1)..-1]
return inner_key, determine_operation(suffix)
end
# 前缀 +key
if key_str.start_with?('+')
return key_str[1..-1], :prepend_array
end
# 尾部(支持 +, !, *, -
if key_str =~ /^(.*?)([+!*\-])$/
return Regexp.last_match(1), determine_operation(Regexp.last_match(2))
end
[key_str, :merge]
end
def self.determine_operation(suffix)
case suffix
when '+'
:append_array
when '-'
:delete
when '!'
:force_overwrite
when '*'
:batch_update
else
:merge
end
end
def self.match_value(target, condition)
return false if target.nil? || condition.nil?
begin
if condition.is_a?(String) && condition.start_with?('/') && condition.end_with?('/')
pattern = condition[1...-1]
regexp = Regexp.new(pattern)
if target.is_a?(Array)
target.any? { |item| item.to_s =~ regexp }
else
target.to_s =~ regexp
end
elsif condition.is_a?(Array) && target.is_a?(Array)
condition.all? { |c| target.include?(c) }
else
target == condition
end
rescue => e
LOG_ERROR("YAML overwrite failed:【(match value) => target: %s, condition: %s, error: %s】" % [target, condition, e.message])
false
end
end
def self.deep_dup(obj)
case obj
when Array
obj.map { |x| deep_dup(x) }
when Hash
obj.transform_values { |v| deep_dup(v) }
else
obj.dup rescue obj
end
end
def self.merge_hash(base, value, prepend: false)
if prepend
result = {}
value.each do |k, v|
if base.key?(k)
result[k] = apply_operation(base[k], v, :merge)
else
result[k] = deep_dup(v)
end
end
base.each do |k, v|
result[k] = deep_dup(v) unless result.key?(k)
end
result
else
result = deep_dup(base)
value.each do |k, v|
if result.key?(k)
result[k] = apply_operation(result[k], v, :merge)
else
result[k] = deep_dup(v)
end
end
result
end
end
def self.delete_from_hash(base, value)
result = deep_dup(base)
case value
when Array
value.each { |k| result.delete(k) }
when Hash
value.each do |k, v|
if v.nil? || v == true
result.delete(k)
elsif result[k].is_a?(Hash) && v.is_a?(Hash)
nested = apply_operation(result[k], v, :delete)
if nested.equal?(DELETED_SENTINEL)
result.delete(k)
else
result[k] = nested
end
else
result.delete(k)
end
end
else
result.delete(value)
end
result
end
DELETED_SENTINEL = Object.new.freeze
def self.apply_operation(base, value, operation)
case operation
when :delete
if base.is_a?(Array) && value.is_a?(Array)
base - value
elsif base.is_a?(Array) && !value.nil?
base - [value]
elsif base.is_a?(Hash)
delete_from_hash(base, value)
else
DELETED_SENTINEL
end
when :force_overwrite
deep_dup(value)
when :prepend_array
if base.is_a?(Array) && value.is_a?(Array)
(deep_dup(value) + base).uniq
elsif base.is_a?(Hash) && value.is_a?(Hash)
merge_hash(base, value, prepend: true)
else
deep_dup(value)
end
when :append_array
if base.is_a?(Array) && value.is_a?(Array)
base_dup = base.dup
deep_dup(value).each { |v| base_dup.delete(v) }
base_dup + deep_dup(value)
elsif base.is_a?(Hash) && value.is_a?(Hash)
merge_hash(base, value, prepend: false)
else
deep_dup(value)
end
when :batch_update
batch_update_items(base, value)
when :merge
if base.is_a?(Hash) && value.is_a?(Hash)
overwrite(base, value)
elsif value.nil?
base
else
deep_dup(value)
end
else
deep_dup(value)
end
end
def self.apply_set_fields(item, set_values)
keys_to_delete = []
set_values.each do |k, v|
processed_key, operation = parse_key(k)
result = apply_operation(item[processed_key], v, operation)
if result.equal?(DELETED_SENTINEL)
keys_to_delete << processed_key
else
item[processed_key] = result
end
end
keys_to_delete.each { |k| item.delete(k) }
end
def self.match_item(item, where_conditions, key = nil)
where_conditions.all? do |k, v|
if k == 'key' && !key.nil?
match_value(key, v)
elsif item.is_a?(Hash)
match_value(item[k] || item[k.to_s], v)
elsif item.is_a?(String) && k == 'value'
match_value(item, v)
else
false
end
end
end
def self.batch_update_items(collection, update_spec)
return collection unless update_spec.is_a?(Hash)
begin
where_conditions = update_spec['where'] || {}
set_values = update_spec['set'] || {}
if collection.is_a?(Array)
result = collection.dup
delete_indices = []
result.each_with_index do |item, index|
match = match_item(item, where_conditions)
if match
if item.is_a?(Hash)
apply_set_fields(item, set_values)
elsif item.is_a?(String) && set_values.key?('value')
new_value = set_values['value']
if new_value.nil?
delete_indices << index
else
result[index] = deep_dup(new_value)
end
end
end
end
delete_indices.reverse_each { |i| result.delete_at(i) }
result
elsif collection.is_a?(Hash)
if where_conditions.any? { |k, _| k != 'key' } &&
match_item(collection, where_conditions)
result = collection.dup
apply_set_fields(result, set_values)
result
else
result = collection.dup
keys_to_delete = []
result.each do |key, value|
next unless value.is_a?(Hash)
match = match_item(value, where_conditions, key)
if match
if set_values.key?('key-') || (set_values.key?('key') && set_values['key'].nil?)
keys_to_delete << key
else
apply_set_fields(value, set_values)
end
end
end
keys_to_delete.each { |k| result.delete(k) }
result
end
elsif collection.nil?
nil
else
collection
end
rescue => e
LOG_ERROR("YAML overwrite failed:【(batch update) => update_spec: %s, error: %s】" % [update_spec, e.message])
collection
end
end
end
@@ -16,7 +16,6 @@ del_lock() {
set_lock
inc_job_counter
restart=0
if [ -n "$1" ] && [ "$1" != "one_key_update" ]; then
/usr/share/openclash/openclash_version.sh "$1" 2>/dev/null
@@ -29,7 +28,7 @@ fi
if [ ! -f "/tmp/openclash_last_version" ]; then
LOG_ERROR "Failed to get version information, please try again later..."
SLOG_CLEAN
dec_job_counter_and_restart "$restart"
dec_job_counter_and_restart "0"
del_lock
exit 0
fi
@@ -69,14 +68,12 @@ if [ "$1" = "one_key_update" ]; then
LOG_TIP "If the download fails, try setting the CDN in Overwrite Settings - General Settings - Github Address Modify Options"
fi
if [ -n "$2" ]; then
/usr/share/openclash/openclash_core.sh "Meta" "$1" "$2" >/dev/null 2>&1 &
/usr/share/openclash/openclash_core.sh "Meta" "$1" "$2" >/dev/null 2>&1
github_address_mod="$2"
else
/usr/share/openclash/openclash_core.sh "Meta" "$1" >/dev/null 2>&1 &
/usr/share/openclash/openclash_core.sh "Meta" "$1" >/dev/null 2>&1
github_address_mod=0
fi
wait
else
if [ "$github_address_mod" = "0" ]; then
LOG_TIP "If the download fails, try setting the CDN in Overwrite Settings - General Settings - Github Address Modify Options"
@@ -175,9 +172,8 @@ if [ -n "$OP_CV" ] && [ -n "$OP_LV" ] && version_compare "$OP_CV" "$OP_LV" && [
elif [ -x "/usr/bin/apk" ]; then
LOG_ERROR "【OpenClash - v$LAST_VER】Pre update test failed after 3 attempts, the file is saved in /tmp/openclash.apk, please try to update manually with【apk add -q --force-overwrite --clean-protected --allow-untrusted /tmp/openclash.apk】"
fi
SLOG_CLEAN
dec_job_counter_and_restart "$restart"
dec_job_counter_and_restart "0"
del_lock
exit 0
fi
@@ -191,8 +187,8 @@ if [ -n "$OP_CV" ] && [ -n "$OP_LV" ] && version_compare "$OP_CV" "$OP_LV" && [
LOG_ERROR "【OpenClash - v$LAST_VER】Download Failed after 3 attempts, please check the network or try again later!"
rm -rf /tmp/openclash.ipk >/dev/null 2>&1
rm -rf /tmp/openclash.apk >/dev/null 2>&1
dec_job_counter_and_restart "0"
SLOG_CLEAN
dec_job_counter_and_restart "$restart"
del_lock
exit 0
fi
@@ -200,9 +196,8 @@ if [ -n "$OP_CV" ] && [ -n "$OP_LV" ] && version_compare "$OP_CV" "$OP_LV" && [
done
cat > /tmp/openclash_update.sh <<"EOF"
#!/bin/sh
START_LOG="/tmp/openclash_start.log"
LOG_FILE="/tmp/openclash.log"
LOGTIME=$(date "+%Y-%m-%d %H:%M:%S")
. /usr/share/openclash/log.sh
. /usr/share/openclash/openclash_ps.sh
UPDATE_LOCK="/tmp/lock/openclash_update_install.lock"
mkdir -p /tmp/lock
@@ -224,27 +219,6 @@ fi
trap 'del_update_lock; exit' INT TERM EXIT
LOG_ERROR()
{
if [ -n "${1}" ]; then
echo -e "${1}" > $START_LOG
echo -e "${LOGTIME} [Error] ${1}" >> $LOG_FILE
fi
}
LOG_TIP()
{
if [ -n "${1}" ]; then
echo -e "${1}" > $START_LOG
echo -e "${LOGTIME} [Tip] ${1}" >> $LOG_FILE
fi
}
SLOG_CLEAN()
{
echo "" > $START_LOG
}
check_install_success()
{
local target_version="$1"
@@ -357,9 +331,9 @@ else
elif [ -x "/usr/bin/apk" ]; then
LOG_ERROR "OpenClash update failed after 3 attempts, the file is saved in /tmp/openclash.apk, please try to update manually with【apk add -q --force-overwrite --clean-protected --allow-untrusted /tmp/openclash.apk】"
fi
SLOG_CLEAN
fi
dec_job_counter_and_restart "0"
SLOG_CLEAN
del_update_lock
EOF
chmod 4755 /tmp/openclash_update.sh
@@ -367,6 +341,8 @@ EOF
if [ ! -f "/tmp/openclash_update.sh" ] || [ ! -s "/tmp/openclash_update.sh" ] || [ ! -x "/tmp/openclash_update.sh" ]; then
LOG_ERROR "Failed to create update script!"
rm -rf /tmp/openclash_update.sh
dec_job_counter_and_restart "0"
SLOG_CLEAN
del_lock
exit 1
fi
@@ -405,8 +381,8 @@ else
else
LOG_TIP "OpenClash has not been updated, stop continuing!"
fi
dec_job_counter_and_restart "0"
SLOG_CLEAN
dec_job_counter_and_restart "$restart"
fi
del_lock
@@ -5,20 +5,10 @@
. /usr/share/openclash/uci.sh
LOG_FILE="/tmp/openclash.log"
CONFIG_FILE="/etc/openclash/$(uci_get_config "config_path" |awk -F '/' '{print $5}' 2>/dev/null)"
ipv6_enable=$(uci_get_config "ipv6_enable" || echo 0)
enable_redirect_dns=$(uci_get_config "enable_redirect_dns")
dns_port=$(uci_get_config "dns_port")
disable_masq_cache=$(uci_get_config "disable_masq_cache")
cfg_update_interval=$(uci_get_config "config_update_interval" || echo 60)
log_size=$(uci_get_config "log_size" || echo 1024)
router_self_proxy=$(uci_get_config "router_self_proxy" || echo 1)
stream_auto_select_interval=$(uci_get_config "stream_auto_select_interval" || echo 30)
skip_proxy_address=$(uci_get_config "skip_proxy_address" || echo 0)
CFG_UPDATE_INT=1
CFG_UPDATE_INT=0
SKIP_PROXY_ADDRESS=1
SKIP_PROXY_ADDRESS_INTERVAL=30
STREAM_AUTO_SELECT=1
STREAM_AUTO_SELECT=0
FIREWALL_RELOAD=0
MAX_FIREWALL_RELOAD=3
FW4=$(command -v fw4)
@@ -155,11 +145,20 @@ end" 2>/dev/null >> $LOG_FILE
while :;
do
CONFIG_FILE="/etc/openclash/$(uci_get_config "config_path" |awk -F '/' '{print $5}' 2>/dev/null)"
ipv6_enable=$(uci_get_config "ipv6_enable" || echo 0)
enable_redirect_dns=$(uci_get_config "enable_redirect_dns")
dns_port=$(uci_get_config "dns_port")
disable_masq_cache=$(uci_get_config "disable_masq_cache")
log_size=$(uci_get_config "log_size" || echo 1024)
router_self_proxy=$(uci_get_config "router_self_proxy" || echo 1)
skip_proxy_address=$(uci_get_config "skip_proxy_address" || echo 0)
cfg_update=$(uci_get_config "auto_update")
cfg_update_mode=$(uci_get_config "config_auto_update_mode")
cfg_update_interval_now=$(uci_get_config "config_update_interval" || echo 60)
cfg_update_interval=$(uci_get_config "config_update_interval" || echo 60)
stream_auto_select=$(uci_get_config "stream_auto_select" || echo 0)
stream_auto_select_interval_now=$(uci_get_config "stream_auto_select_interval" || echo 30)
stream_auto_select_interval=$(uci_get_config "stream_auto_select_interval" || echo 30)
stream_auto_select_netflix=$(uci_get_config "stream_auto_select_netflix" || echo 0)
stream_auto_select_disney=$(uci_get_config "stream_auto_select_disney" || echo 0)
stream_auto_select_hbo_max=$(uci_get_config "stream_auto_select_hbo_max" || echo 0)
@@ -362,18 +361,16 @@ fi
## 配置文件循环更新
if [ "$cfg_update" -eq 1 ] && [ "$cfg_update_mode" -eq 1 ]; then
[ "$cfg_update_interval" -ne "$cfg_update_interval_now" ] && CFG_UPDATE_INT=0 && cfg_update_interval="$cfg_update_interval_now"
if [ "$CFG_UPDATE_INT" -ne 0 ]; then
[ "$(expr "$CFG_UPDATE_INT" % "$cfg_update_interval_now")" -eq 0 ] && /usr/share/openclash/openclash.sh
[ "$(expr "$CFG_UPDATE_INT" % "$cfg_update_interval")" -eq 0 ] && /usr/share/openclash/openclash.sh
fi
CFG_UPDATE_INT=$(expr "$CFG_UPDATE_INT" + 1)
fi
##STREAMING_UNLOCK_CHECK
if [ "$stream_auto_select" -eq 1 ] && [ "$router_self_proxy" -eq 1 ]; then
[ "$stream_auto_select_interval" -ne "$stream_auto_select_interval_now" ] && STREAM_AUTO_SELECT=1 && stream_auto_select_interval="$stream_auto_select_interval_now"
if [ "$STREAM_AUTO_SELECT" -ne 0 ]; then
if [ "$(expr "$STREAM_AUTO_SELECT" % "$stream_auto_select_interval_now")" -eq 0 ] || [ "$STREAM_AUTO_SELECT" -eq 1 ]; then
if [ "$(expr "$STREAM_AUTO_SELECT" % "$stream_auto_select_interval")" -eq 0 ] || [ "$STREAM_AUTO_SELECT" -eq 1 ]; then
if [ "$stream_auto_select_netflix" -eq 1 ]; then
LOG_TIP "Start Auto Select Proxy For Netflix Unlock..."
/usr/share/openclash/openclash_streaming_unlock.lua "Netflix" >> $LOG_FILE
@@ -672,6 +672,7 @@ proxies: # socks5
grpc-opts:
grpc-service-name: "example"
# grpc-user-agent: "grpc-go/1.36.0"
# ping-interval: 0 # 默认关闭,单位为秒
# ip-version: ipv4
# vless
@@ -762,6 +763,7 @@ proxies: # socks5
grpc-opts:
grpc-service-name: "grpc"
# grpc-user-agent: "grpc-go/1.36.0"
# ping-interval: 0 # 默认关闭,单位为秒
reality-opts:
public-key: CrrQSjAG_YkHLwvM2M-7XkKJilgL5upBKCp0od0tLhE
@@ -790,6 +792,69 @@ proxies: # socks5
# v2ray-http-upgrade: false
# v2ray-http-upgrade-fast-open: false
- name: "vless-xhttp"
type: vless
server: server
port: 443
uuid: uuid
udp: true
tls: true
network: xhttp
alpn:
- h2
# ech-opts: ...
# reality-opts: ...
# skip-cert-verify: false
# fingerprint: ...
# certificate: ...
# private-key: ...
servername: xxx.com
client-fingerprint: chrome
encryption: ""
xhttp-opts:
path: "/"
host: xxx.com
# mode: "stream-one" # Available: "stream-one", "stream-up" or "packet-up"
# headers:
# X-Forwarded-For: ""
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# reuse-settings: # aka XMUX
# max-connections: "16-32"
# max-concurrency: "0"
# c-max-reuse-times: "0"
# h-max-request-times: "600-900"
# h-max-reusable-secs: "1800-3000"
# download-settings:
# ## xhttp part
# path: "/"
# host: xxx.com
# headers:
# X-Forwarded-For: ""
# no-grpc-header: false
# x-padding-bytes: "100-1000"
# reuse-settings: # aka XMUX
# max-connections: "16-32"
# max-concurrency: "0"
# c-max-reuse-times: "0"
# h-max-request-times: "600-900"
# h-max-reusable-secs: "1800-3000"
# ## proxy part
# server: server
# port: 443
# tls: true
# alpn:
# - h2
# ech-opts: ...
# reality-opts: ...
# skip-cert-verify: false
# fingerprint: ...
# certificate: ...
# private-key: ...
# servername: xxx.com
# client-fingerprint: chrome
# Trojan
- name: "trojan"
type: trojan
@@ -833,6 +898,7 @@ proxies: # socks5
grpc-opts:
grpc-service-name: "example"
# grpc-user-agent: "grpc-go/1.36.0"
# ping-interval: 0 # 默认关闭,单位为秒
- name: trojan-ws
server: server
@@ -1080,6 +1146,8 @@ proxies: # socks5
# multiplexing: MULTIPLEXING_LOW
# 如果想开启 0-RTT 握手,请设置为 HANDSHAKE_NO_WAIT,否则请设置为 HANDSHAKE_STANDARD。默认值为 HANDSHAKE_STANDARD
# handshake-mode: HANDSHAKE_STANDARD
# 一个 base64 字符串用于微调网络行为
# traffic-pattern: ""
# sudoku
- name: sudoku
@@ -1090,15 +1158,17 @@ proxies: # socks5
aead-method: chacha20-poly1305 # 可选:chacha20-poly1305、aes-128-gcm、none(不建议;且 enable-pure-downlink=false 时不可用)
padding-min: 2 # 最小填充率(0-100
padding-max: 7 # 最大填充率(0-100,必须 >= padding-min
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy 前者全ascii映射,后者保证熵值(汉明1)低于3
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合。启用此处则需配置`table-type`为`prefer_entropy`
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
http-mask: true # 是否启用http掩码
# http-mask-mode: legacy # 可选:legacy(默认)、streamsplit-stream)、poll、auto(先 stream 再 poll);stream/poll/auto 支持走 CDN/反代
# http-mask-tls: true # 可选:仅在 http-mask-mode 为 stream/poll/auto 时生效;true 强制 httpsfalse 强制 http(不会根据端口自动推断)
# http-mask-host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 http-mask-mode 为 stream/poll/auto 时生效
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
# http-mask-multiplex: off # 可选:off(默认)、auto(复用底层 HTTP 连接,减少建链 RTT)、on(Sudoku mux 单隧道多目标;仅在 http-mask-mode=stream/poll/auto 生效
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy、up_ascii_down_entropy、up_entropy_down_ascii
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合;只对 entropy 方向生效
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),非空时覆盖 custom-table
# 推荐:使用 httpmask 对象统一管理 HTTPMask 相关字段:
httpmask:
disable: false # true 禁用所有 HTTP 伪装/隧道
mode: legacy # 可选:legacy(默认)、streamsplit-stream)、poll、auto(先 streampoll)、wsWebSocket 隧道)
# tls: true # 可选:按需开启 HTTPS/WSS
# host: "" # 可选:覆盖 Host/SNI(支持 example.com 或 example.com:443);仅在 modestream/poll/auto/ws 时生效
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload、/aabbcc/ws
# multiplex: "off" # 可选字符串:off(默认)、auto(复用底层 HTTP 连接,减少建链 RTT)、on(Sudoku mux 单隧道多目标;仅在 mode=stream/poll/auto 生效;ws 强制 off
enable-pure-downlink: false # 可选:false=带宽优化下行(更快,要求 aead-method != none);true=纯 Sudoku 下行
# anytls
@@ -1118,6 +1188,23 @@ proxies: # socks5
# - http/1.1
# skip-cert-verify: true
# trusttunnel
- name: trusttunnel
type: trusttunnel
server: 1.2.3.4
port: 443
username: username
password: password
# client-fingerprint: chrome
health-check: true
udp: true
# sni: "example.com"
# alpn:
# - h2
# skip-cert-verify: true
# quic: true # 默认为false
# congestion-controller: bbr
# dns 出站会将请求劫持到内部 dns 模块,所有请求均在内部处理
- name: "dns-out"
type: dns
@@ -1555,6 +1642,12 @@ listeners:
flow: xtls-rprx-vision
# ws-path: "/" # 如果不为空则开启 websocket 传输层
# grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层
# xhttp-config: # 如果不为空则开启 xhttp 传输层
# path: "/"
# host: ""
# mode: auto # Available: "stream-one", "stream-up" or "packet-up"
# no-sse-header: false
# sc-stream-up-server-secs: "20-80"
# -------------------------
# vless encryption服务端配置:
# (原生外观 / 只 XOR 公钥 / 全随机数。1-RTT 每次下发随机 300 到 600 秒的 ticket 以便 0-RTT 复用 / 只允许 1-RTT
@@ -1631,6 +1724,8 @@ listeners:
users:
username1: password1
username2: password2
# 一个 base64 字符串用于微调网络行为
# traffic-pattern: ""
- name: sudoku-in-1
type: sudoku
@@ -1640,14 +1735,18 @@ listeners:
aead-method: chacha20-poly1305 # 可选:chacha20-poly1305、aes-128-gcm、none(不建议;且 enable-pure-downlink=false 时不可用)
padding-min: 1 # 最小填充率(0-100
padding-max: 15 # 最大填充率(0-100,必须 >= padding-min
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy 前者全ascii映射,后者保证熵值(汉明1)低于3
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合。启用此处则需配置`table-type`为`prefer_entropy`
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于 xvp 模式轮换;非空时覆盖 custom-table
table-type: prefer_ascii # 可选值:prefer_ascii、prefer_entropy、up_ascii_down_entropy、up_entropy_down_ascii
# custom-table: xpxvvpvv # 可选,自定义字节布局,必须包含2个x、2个p、4个v,可随意组合;只对 entropy 方向生效
# custom-tables: ["xpxvvpvv", "vxpvxvvp"] # 可选,自定义字节布局列表(x/v/p),用于多表轮换;非空时覆盖 custom-table
handshake-timeout: 5 # 可选(秒)
enable-pure-downlink: false # 可选:false=带宽优化下行(更快,要求 aead-method != none);true=纯 Sudoku 下行
disable-http-mask: false # 可选:禁用 http 掩码/隧道(默认为 false
# http-mask-mode: legacy # 可选:legacy(默认)、streamsplit-stream)、poll、auto(先 stream 再 poll);stream/poll/auto 支持走 CDN/反代
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload
# 推荐:使用 httpmask 对象统一管理 HTTPMask 相关字段:
httpmask:
disable: false # true 禁用所有 HTTP 伪装/隧道
mode: legacy # 可选:legacy(默认)、streamsplit-stream)、poll、auto(先 stream 再 poll)、wsWebSocket 隧道)
# path-root: "" # 可选:HTTP 隧道端点一级路径前缀(双方需一致),例如 "aabbcc" 或 "/aabbcc/" => /aabbcc/session、/aabbcc/stream、/aabbcc/api/v1/upload、/aabbcc/ws
#
# fallback: "127.0.0.1:80" # 可选:用于可连接请求的回落转发,可与其他服务共端口
@@ -1734,6 +1833,30 @@ listeners:
# masquerade: http://127.0.0.1:8080 #作为反向代理
# masquerade: https://127.0.0.1:8080 #作为反向代理
- name: trusttunnel-in-1
type: trusttunnel
port: 10821 # 支持使用ports格式,例如200,302 or 200,204,401-429,501-503
listen: 0.0.0.0
# rule: sub-rule-name1 # 默认使用 rules,如果未找到 sub-rule 则直接使用 rules
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
users:
- username: 1
password: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68
certificate: ./server.crt # 证书 PEM 格式,或者 证书的路径
private-key: ./server.key # 证书对应的私钥 PEM 格式,或者私钥路径
network: ["tcp", "udp"] # http2+http3
congestion-controller: bbr
# 下面两项为mTLS配置项,如果client-auth-type设置为 "verify-if-given" 或 "require-and-verify" 则client-auth-cert必须不为空
# client-auth-type: "" # 可选值:""、"request"、"require-any"、"verify-if-given"、"require-and-verify"
# client-auth-cert: string # 证书 PEM 格式,或者 证书的路径
# 如果填写则开启ech(可由 mihomo generate ech-keypair <明文域名> 生成)
# ech-key: |
# -----BEGIN ECH KEYS-----
# ACATwY30o/RKgD6hgeQxwrSiApLaCgU+HKh7B6SUrAHaDwBD/g0APwAAIAAgHjzK
# madSJjYQIf9o1N5GXjkW4DEEeb17qMxHdwMdNnwADAABAAEAAQACAAEAAwAIdGVz
# dC5jb20AAA==
# -----END ECH KEYS-----
# 注意,listeners中的tun仅提供给高级用户使用,普通用户应使用顶层配置中的tun
- name: tun-in-1
type: tun
@@ -185,7 +185,7 @@ set_disable_qtype()
yml_dns_get()
{
local section="$1" regex='^([0-9a-fA-F]{0,4}:){1,7}[0-9a-fA-F]{0,4}$'
local enabled port type ip group dns_type dns_address interface specific_group node_resolve http3 ecs_subnet ecs_override disable_qtype_param
local enabled port type ip group dns_type dns_address interface specific_group node_resolve direct_nameserver http3 skip_cert_verify ecs_subnet ecs_override disable_qtype_param disable_qtype disable_ipv4 disable_ipv6 disable_reuse
config_get_bool "enabled" "$section" "enabled" "1"
[ "$enabled" = "0" ] && return
@@ -480,7 +480,7 @@ begin
else
Value['dns']['enhanced-mode'] = 'fake-ip'
Value['dns']['fake-ip-range'] = fake_ip_range
if Value['dns']['ipv6']
if Value['dns']['ipv6'] and fake_ip_range6 != '0'
Value['dns']['fake-ip-range6'] = fake_ip_range6
end
end
@@ -592,7 +592,7 @@ ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
};
end;
#Mieru
#Mieru
if x['type'] == 'mieru' then
threads << Thread.new{
#port-range
@@ -1138,6 +1138,19 @@ ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
uci_commands << uci_set + 'reality_short_id=\"' + x['reality-opts']['short-id'].to_s + '\"'
end
end
if x.key?('encryption') then
uci_commands << uci_set + 'vless_encryption=\"' + x['encryption'].to_s + '\"'
end
elsif x['network'].to_s == 'xhttp'
uci_commands << uci_set + 'obfs_vless=xhttp'
if x.key?('xhttp-opts') then
if x['xhttp-opts'].key?('path') then
uci_commands << uci_set + 'xhttp_opts_path=\"' + x['xhttp-opts']['path'].to_s + '\"'
end
if x['xhttp-opts'].key?('host') then
uci_commands << uci_set + 'xhttp_opts_host=\"' + x['xhttp-opts']['host'].to_s + '\"'
end
end
end
end
};
@@ -1424,6 +1437,132 @@ ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
};
end;
if x['type'] == 'masque' then
threads << Thread.new{
#private-key
if x.key?('private-key') then
uci_commands << uci_set + 'masque_private_key=\"' + x['private-key'].to_s + '\"'
end
};
threads << Thread.new{
#public-key
if x.key?('public-key') then
uci_commands << uci_set + 'masque_public_key=\"' + x['public-key'].to_s + '\"'
end
};
threads << Thread.new{
#ip
if x.key?('ip') then
uci_commands << uci_set + 'masque_ip=\"' + x['ip'].to_s + '\"'
end
};
threads << Thread.new{
#ipv6
if x.key?('ipv6') then
uci_commands << uci_set + 'masque_ipv6=\"' + x['ipv6'].to_s + '\"'
end
};
threads << Thread.new{
#mtu
if x.key?('mtu') then
uci_commands << uci_set + 'masque_mtu=\"' + x['mtu'].to_s + '\"'
end
};
threads << Thread.new{
#remote-dns-resolve
if x.key?('remote-dns-resolve') then
uci_commands << uci_set + 'masque_remote_dns_resolve=\"' + x['remote-dns-resolve'].to_s + '\"'
end
};
threads << Thread.new{
#dns
if x.key?('dns') then
dns = uci_del + 'dns >/dev/null 2>&1'
system(dns)
x['dns'].each{
|x|
uci_commands << uci_add + 'masque_dns=\"' + x.to_s + '\"'
}
end
};
end;
if x['type'] == 'trusttunnel' then
threads << Thread.new{
#username
if x.key?('username') then
uci_commands << uci_set + 'trusttunnel_username=\"' + x['username'].to_s + '\"'
end
};
threads << Thread.new{
#password
if x.key?('password') then
uci_commands << uci_set + 'trusttunnel_password=\"' + x['password'].to_s + '\"'
end
};
threads << Thread.new{
#health-check
if x.key?('health-check') then
uci_commands << uci_set + 'trusttunnel_health_check=\"' + x['health-check'].to_s + '\"'
end
};
threads << Thread.new{
#quic
if x.key?('quic') then
uci_commands << uci_set + 'trusttunnel_quic=\"' + x['quic'].to_s + '\"'
end
};
threads << Thread.new{
#congestion-controller
if x.key?('congestion-controller') then
uci_commands << uci_set + 'trusttunnel_congestion_controller=\"' + x['congestion-controller'].to_s + '\"'
end
};
#alpn
threads << Thread.new{
if x.key?('alpn') then
alpn = uci_del + 'alpn >/dev/null 2>&1'
system(alpn)
x['alpn'].each{
|x|
uci_commands << uci_add + 'alpn=\"' + x.to_s + '\"'
}
end
};
#sni
threads << Thread.new{
if x.key?('sni') then
uci_commands << uci_set + 'sni=\"' + x['sni'].to_s + '\"'
end
};
#skip-cert-verify
threads << Thread.new{
if x.key?('skip-cert-verify') then
uci_commands << uci_set + 'skip_cert_verify=\"' + x['skip-cert-verify'].to_s + '\"'
end
};
#client_fingerprint
threads << Thread.new{
if x.key?('client-fingerprint') then
uci_commands << uci_set + 'client_fingerprint=\"' + x['client-fingerprint'].to_s + '\"'
end
};
end;
#加入策略组
threads << Thread.new{
#加入策略组
@@ -186,135 +186,6 @@ yml_servers_set()
config_get "name" "$section" "name" ""
config_get "server" "$section" "server" ""
config_get "port" "$section" "port" ""
config_get "dialer_proxy" "$section" "dialer_proxy" ""
config_get "cipher" "$section" "cipher" ""
config_get "cipher_ssr" "$section" "cipher_ssr" ""
config_get "password" "$section" "password" ""
config_get "securitys" "$section" "securitys" ""
config_get "udp" "$section" "udp" ""
config_get "obfs" "$section" "obfs" ""
config_get "obfs_ssr" "$section" "obfs_ssr" ""
config_get "obfs_param" "$section" "obfs_param" ""
config_get "obfs_vmess" "$section" "obfs_vmess" ""
config_get "obfs_trojan" "$section" "obfs_trojan" ""
config_get "protocol" "$section" "protocol" ""
config_get "protocol_param" "$section" "protocol_param" ""
config_get "host" "$section" "host" ""
config_get "mux" "$section" "mux" ""
config_get "custom" "$section" "custom" ""
config_get "tls" "$section" "tls" ""
config_get "skip_cert_verify" "$section" "skip_cert_verify" ""
config_get "path" "$section" "path" ""
config_get "alterId" "$section" "alterId" ""
config_get "uuid" "$section" "uuid" ""
config_get "auth_name" "$section" "auth_name" ""
config_get "auth_pass" "$section" "auth_pass" ""
config_get "psk" "$section" "psk" ""
config_get "obfs_snell" "$section" "obfs_snell" ""
config_get "snell_version" "$section" "snell_version" ""
config_get "sni" "$section" "sni" ""
config_get "alpn" "$section" "alpn" ""
config_get "http_path" "$section" "http_path" ""
config_get "keep_alive" "$section" "keep_alive" ""
config_get "servername" "$section" "servername" ""
config_get "h2_path" "$section" "h2_path" ""
config_get "h2_host" "$section" "h2_host" ""
config_get "grpc_service_name" "$section" "grpc_service_name" ""
config_get "ws_opts_path" "$section" "ws_opts_path" ""
config_get "ws_opts_headers" "$section" "ws_opts_headers" ""
config_get "max_early_data" "$section" "max_early_data" ""
config_get "early_data_header_name" "$section" "early_data_header_name" ""
config_get "trojan_ws_path" "$section" "trojan_ws_path" ""
config_get "trojan_ws_headers" "$section" "trojan_ws_headers" ""
config_get "interface_name" "$section" "interface_name" ""
config_get "routing_mark" "$section" "routing_mark" ""
config_get "obfs_vless" "$section" "obfs_vless" ""
config_get "vless_flow" "$section" "vless_flow" ""
config_get "http_headers" "$section" "http_headers" ""
config_get "hysteria_protocol" "$section" "hysteria_protocol" ""
config_get "hysteria2_protocol" "$section" "hysteria2_protocol" ""
config_get "hysteria_up" "$section" "hysteria_up" ""
config_get "hysteria_down" "$section" "hysteria_down" ""
config_get "hysteria_alpn" "$section" "hysteria_alpn" ""
config_get "hysteria_obfs" "$section" "hysteria_obfs" ""
config_get "hysteria_auth" "$section" "hysteria_auth" ""
config_get "hysteria_auth_str" "$section" "hysteria_auth_str" ""
config_get "hysteria_ca" "$section" "hysteria_ca" ""
config_get "hysteria_ca_str" "$section" "hysteria_ca_str" ""
config_get "recv_window_conn" "$section" "recv_window_conn" ""
config_get "recv_window" "$section" "recv_window" ""
config_get "disable_mtu_discovery" "$section" "disable_mtu_discovery" ""
config_get "initial_stream_receive_window" "$section" "initial_stream_receive_window" ""
config_get "max_stream_receive_window" "$section" "max_stream_receive_window" ""
config_get "initial_connection_receive_window" "$section" "initial_connection_receive_window" ""
config_get "max_connection_receive_window" "$section" "max_connection_receive_window" ""
config_get "xudp" "$section" "xudp" ""
config_get "packet_encoding" "$section" "packet_encoding" ""
config_get "global_padding" "$section" "global_padding" ""
config_get "authenticated_length" "$section" "authenticated_length" ""
config_get "wg_ip" "$section" "wg_ip" ""
config_get "wg_ipv6" "$section" "wg_ipv6" ""
config_get "private_key" "$section" "private_key" ""
config_get "public_key" "$section" "public_key" ""
config_get "preshared_key" "$section" "preshared_key" ""
config_get "wg_dns" "$section" "wg_dns" ""
config_get "public_key" "$section" "public_key" ""
config_get "preshared_key" "$section" "preshared_key" ""
config_get "wg_mtu" "$section" "wg_mtu" ""
config_get "tc_ip" "$section" "tc_ip" ""
config_get "tc_token" "$section" "tc_token" ""
config_get "tc_uuid" "$section" "tc_uuid" ""
config_get "tc_password" "$section" "tc_password" ""
config_get "udp_relay_mode" "$section" "udp_relay_mode" ""
config_get "congestion_controller" "$section" "congestion_controller" ""
config_get "tc_alpn" "$section" "tc_alpn" ""
config_get "disable_sni" "$section" "disable_sni" ""
config_get "reduce_rtt" "$section" "reduce_rtt" ""
config_get "heartbeat_interval" "$section" "heartbeat_interval" ""
config_get "request_timeout" "$section" "request_timeout" ""
config_get "max_udp_relay_packet_size" "$section" "max_udp_relay_packet_size" ""
config_get "fast_open" "$section" "fast_open" ""
config_get "fingerprint" "$section" "fingerprint" ""
config_get "ports" "$section" "ports" ""
config_get "hop_interval" "$section" "hop_interval" ""
config_get "max_open_streams" "$section" "max_open_streams" ""
config_get "obfs_password" "$section" "obfs_password" ""
config_get "packet_addr" "$section" "packet_addr" ""
config_get "client_fingerprint" "$section" "client_fingerprint" ""
config_get "ip_version" "$section" "ip_version" ""
config_get "tfo" "$section" "tfo" ""
config_get "udp_over_tcp" "$section" "udp_over_tcp" ""
config_get "reality_public_key" "$section" "reality_public_key" ""
config_get "reality_short_id" "$section" "reality_short_id" ""
config_get "obfs_version_hint" "$section" "obfs_version_hint" ""
config_get "obfs_restls_script" "$section" "obfs_restls_script" ""
config_get "multiplex" "$section" "multiplex" ""
config_get "multiplex_protocol" "$section" "multiplex_protocol" ""
config_get "multiplex_max_connections" "$section" "multiplex_max_connections" ""
config_get "multiplex_min_streams" "$section" "multiplex_min_streams" ""
config_get "multiplex_max_streams" "$section" "multiplex_max_streams" ""
config_get "multiplex_padding" "$section" "multiplex_padding" ""
config_get "multiplex_statistic" "$section" "multiplex_statistic" ""
config_get "multiplex_only_tcp" "$section" "multiplex_only_tcp" ""
config_get "other_parameters" "$section" "other_parameters" ""
config_get "hysteria_obfs_password" "$section" "hysteria_obfs_password" ""
config_get "port_range" "$section" "port_range" ""
config_get "username" "$section" "username" ""
config_get "transport" "$section" "transport" "TCP"
config_get "multiplexing" "$section" "multiplexing" "MULTIPLEXING_LOW"
config_get "private_key" "$section" "private_key" ""
config_get "private_key_passphrase" "$section" "private_key_passphrase" ""
config_get "host_key" "$section" "host_key" ""
config_get "host_key_algorithms" "$section" "host_key_algorithms" ""
config_get "idle_session_check_interval" "$section" "idle_session_check_interval" ""
config_get "idle_session_timeout" "$section" "idle_session_timeout" ""
config_get "min_idle_session" "$section" "min_idle_session" ""
config_get "sudoku_key" "$section" "sudoku_key" ""
config_get "aead_method" "$section" "aead_method" "none"
config_get "padding_min" "$section" "padding_min" ""
config_get "padding_max" "$section" "padding_max" ""
config_get "table_type" "$section" "table_type" "prefer_ascii"
config_get "http_mask" "$section" "http_mask" "true"
if [ "$enabled" = "0" ]; then
return
@@ -336,11 +207,12 @@ yml_servers_set()
return
fi
if [ -z "$password" ]; then
if [ "$type" = "ss" ] || [ "$type" = "trojan" ] || [ "$type" = "ssr" ]; then
return
fi
fi
if [ "$type" = "ss" ] || [ "$type" = "trojan" ] || [ "$type" = "ssr" ]; then
config_get "password" "$section" "password" ""
if [ -z "$password" ]; then
return
fi
fi
if [ ! -z "$config" ] && [ "$config" != "$CONFIG_NAME" ] && [ "$config" != "all" ]; then
return
@@ -364,59 +236,27 @@ yml_servers_set()
fi
LOG_OUT "Start Writing【$CONFIG_NAME - $type - $name】Proxy To Config File..."
if [ "$obfs" != "none" ] && [ -n "$obfs" ]; then
if [ "$obfs" = "websocket" ]; then
obfss="plugin: v2ray-plugin"
elif [ "$obfs" = "shadow-tls" ]; then
obfss="plugin: shadow-tls"
elif [ "$obfs" = "restls" ]; then
obfss="plugin: restls"
else
obfss="plugin: obfs"
fi
else
obfss=""
fi
if [ "$obfs_vless" = "ws" ]; then
obfs_vless="network: ws"
fi
if [ "$obfs_vless" = "grpc" ]; then
obfs_vless="network: grpc"
fi
if [ "$obfs_vless" = "tcp" ]; then
obfs_vless="network: tcp"
fi
if [ "$obfs_vmess" = "websocket" ]; then
obfs_vmess="network: ws"
fi
if [ "$obfs_vmess" = "http" ]; then
obfs_vmess="network: http"
fi
if [ "$obfs_vmess" = "h2" ]; then
obfs_vmess="network: h2"
fi
if [ "$obfs_vmess" = "grpc" ]; then
obfs_vmess="network: grpc"
fi
if [ ! -z "$custom" ] && [ "$type" = "vmess" ]; then
custom="Host: \"$custom\""
fi
if [ ! -z "$path" ]; then
if [ "$type" != "vmess" ]; then
path="path: \"$path\""
elif [ "$obfs_vmess" = "network: ws" ]; then
path="ws-path: \"$path\""
fi
fi
config_get "dialer_proxy" "$section" "dialer_proxy" ""
config_get "udp" "$section" "udp" ""
config_get "skip_cert_verify" "$section" "skip_cert_verify" ""
config_get "tls" "$section" "tls" ""
config_get "sni" "$section" "sni" ""
config_get "alpn" "$section" "alpn" ""
config_get "fingerprint" "$section" "fingerprint" ""
config_get "client_fingerprint" "$section" "client_fingerprint" ""
config_get "ip_version" "$section" "ip_version" ""
config_get "tfo" "$section" "tfo" ""
config_get "multiplex" "$section" "multiplex" ""
config_get "multiplex_protocol" "$section" "multiplex_protocol" ""
config_get "multiplex_max_connections" "$section" "multiplex_max_connections" ""
config_get "multiplex_min_streams" "$section" "multiplex_min_streams" ""
config_get "multiplex_max_streams" "$section" "multiplex_max_streams" ""
config_get "multiplex_padding" "$section" "multiplex_padding" ""
config_get "multiplex_statistic" "$section" "multiplex_statistic" ""
config_get "multiplex_only_tcp" "$section" "multiplex_only_tcp" ""
config_get "interface_name" "$section" "interface_name" ""
config_get "routing_mark" "$section" "routing_mark" ""
config_get "other_parameters" "$section" "other_parameters" ""
if [ "$client_fingerprint" = "none" ]; then
client_fingerprint=""
@@ -428,6 +268,35 @@ yml_servers_set()
#ss
if [ "$type" = "ss" ]; then
config_get "cipher" "$section" "cipher" ""
config_get "obfs" "$section" "obfs" ""
config_get "host" "$section" "host" ""
config_get "mux" "$section" "mux" ""
config_get "custom" "$section" "custom" ""
config_get "path" "$section" "path" ""
config_get "obfs_password" "$section" "obfs_password" ""
config_get "obfs_version_hint" "$section" "obfs_version_hint" ""
config_get "obfs_restls_script" "$section" "obfs_restls_script" ""
config_get "udp_over_tcp" "$section" "udp_over_tcp" ""
if [ "$obfs" != "none" ] && [ -n "$obfs" ]; then
if [ "$obfs" = "websocket" ]; then
obfss="plugin: v2ray-plugin"
elif [ "$obfs" = "shadow-tls" ]; then
obfss="plugin: shadow-tls"
elif [ "$obfs" = "restls" ]; then
obfss="plugin: restls"
else
obfss="plugin: obfs"
fi
else
obfss=""
fi
if [ ! -z "$path" ]; then
path="path: \"$path\""
fi
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -528,6 +397,12 @@ fi
#ssr
if [ "$type" = "ssr" ]; then
config_get "cipher_ssr" "$section" "cipher_ssr" ""
config_get "obfs_ssr" "$section" "obfs_ssr" ""
config_get "protocol" "$section" "protocol" ""
config_get "obfs_param" "$section" "obfs_param" ""
config_get "protocol_param" "$section" "protocol_param" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -557,6 +432,48 @@ fi
#vmess
if [ "$type" = "vmess" ]; then
config_get "uuid" "$section" "uuid" ""
config_get "alterId" "$section" "alterId" ""
config_get "securitys" "$section" "securitys" ""
config_get "xudp" "$section" "xudp" ""
config_get "packet_encoding" "$section" "packet_encoding" ""
config_get "global_padding" "$section" "global_padding" ""
config_get "authenticated_length" "$section" "authenticated_length" ""
config_get "servername" "$section" "servername" ""
config_get "obfs_vmess" "$section" "obfs_vmess" ""
config_get "custom" "$section" "custom" ""
config_get "path" "$section" "path" ""
config_get "ws_opts_path" "$section" "ws_opts_path" ""
config_get "ws_opts_headers" "$section" "ws_opts_headers" ""
config_get "max_early_data" "$section" "max_early_data" ""
config_get "early_data_header_name" "$section" "early_data_header_name" ""
config_get "http_path" "$section" "http_path" ""
config_get "keep_alive" "$section" "keep_alive" ""
config_get "h2_path" "$section" "h2_path" ""
config_get "h2_host" "$section" "h2_host" ""
config_get "grpc_service_name" "$section" "grpc_service_name" ""
if [ "$obfs_vmess" = "websocket" ]; then
obfs_vmess="network: ws"
fi
if [ "$obfs_vmess" = "http" ]; then
obfs_vmess="network: http"
fi
if [ "$obfs_vmess" = "h2" ]; then
obfs_vmess="network: h2"
fi
if [ "$obfs_vmess" = "grpc" ]; then
obfs_vmess="network: grpc"
fi
if [ ! -z "$custom" ]; then
custom="Host: \"$custom\""
fi
if [ ! -z "$path" ] && [ "$obfs_vmess" = "network: ws" ]; then
path="ws-path: \"$path\""
fi
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -702,6 +619,11 @@ fi
#anytls
if [ "$type" = "anytls" ]; then
config_get "password" "$section" "password" ""
config_get "idle_session_check_interval" "$section" "idle_session_check_interval" ""
config_get "idle_session_timeout" "$section" "idle_session_timeout" ""
config_get "min_idle_session" "$section" "min_idle_session" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -758,6 +680,11 @@ fi
#Mieru
if [ "$type" = "mieru" ]; then
config_get "port_range" "$section" "port_range" ""
config_get "username" "$section" "username" ""
config_get "transport" "$section" "transport" "TCP"
config_get "multiplexing" "$section" "multiplexing" "MULTIPLEXING_LOW"
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -788,6 +715,21 @@ fi
#Tuic
if [ "$type" = "tuic" ]; then
config_get "tc_ip" "$section" "tc_ip" ""
config_get "tc_token" "$section" "tc_token" ""
config_get "tc_uuid" "$section" "tc_uuid" ""
config_get "tc_password" "$section" "tc_password" ""
config_get "udp_relay_mode" "$section" "udp_relay_mode" ""
config_get "congestion_controller" "$section" "congestion_controller" ""
config_get "tc_alpn" "$section" "tc_alpn" ""
config_get "disable_sni" "$section" "disable_sni" ""
config_get "reduce_rtt" "$section" "reduce_rtt" ""
config_get "fast_open" "$section" "fast_open" ""
config_get "heartbeat_interval" "$section" "heartbeat_interval" ""
config_get "request_timeout" "$section" "request_timeout" ""
config_get "max_udp_relay_packet_size" "$section" "max_udp_relay_packet_size" ""
config_get "max_open_streams" "$section" "max_open_streams" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -874,6 +816,14 @@ fi
#WireGuard
if [ "$type" = "wireguard" ]; then
config_get "wg_ip" "$section" "wg_ip" ""
config_get "wg_ipv6" "$section" "wg_ipv6" ""
config_get "private_key" "$section" "private_key" ""
config_get "public_key" "$section" "public_key" ""
config_get "preshared_key" "$section" "preshared_key" ""
config_get "wg_dns" "$section" "wg_dns" ""
config_get "wg_mtu" "$section" "wg_mtu" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -905,7 +855,7 @@ cat >> "$SERVER_FILE" <<-EOF
preshared-key: "$preshared_key"
EOF
fi
if [ -n "$preshared_key" ]; then
if [ -n "$wg_dns" ]; then
cat >> "$SERVER_FILE" <<-EOF
dns:
EOF
@@ -925,6 +875,22 @@ fi
#hysteria
if [ "$type" = "hysteria" ]; then
config_get "hysteria_protocol" "$section" "hysteria_protocol" ""
config_get "hysteria_up" "$section" "hysteria_up" ""
config_get "hysteria_down" "$section" "hysteria_down" ""
config_get "hysteria_alpn" "$section" "hysteria_alpn" ""
config_get "hysteria_obfs" "$section" "hysteria_obfs" ""
config_get "hysteria_auth" "$section" "hysteria_auth" ""
config_get "hysteria_auth_str" "$section" "hysteria_auth_str" ""
config_get "hysteria_ca" "$section" "hysteria_ca" ""
config_get "hysteria_ca_str" "$section" "hysteria_ca_str" ""
config_get "recv_window_conn" "$section" "recv_window_conn" ""
config_get "recv_window" "$section" "recv_window" ""
config_get "disable_mtu_discovery" "$section" "disable_mtu_discovery" ""
config_get "fast_open" "$section" "fast_open" ""
config_get "ports" "$section" "ports" ""
config_get "hop_interval" "$section" "hop_interval" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1029,6 +995,22 @@ fi
#hysteria2
if [ "$type" = "hysteria2" ]; then
config_get "password" "$section" "password" ""
config_get "hysteria_up" "$section" "hysteria_up" ""
config_get "hysteria_down" "$section" "hysteria_down" ""
config_get "hysteria_alpn" "$section" "hysteria_alpn" ""
config_get "hysteria_obfs" "$section" "hysteria_obfs" ""
config_get "hysteria_obfs_password" "$section" "hysteria_obfs_password" ""
config_get "hysteria_ca" "$section" "hysteria_ca" ""
config_get "hysteria_ca_str" "$section" "hysteria_ca_str" ""
config_get "initial_stream_receive_window" "$section" "initial_stream_receive_window" ""
config_get "max_stream_receive_window" "$section" "max_stream_receive_window" ""
config_get "initial_connection_receive_window" "$section" "initial_connection_receive_window" ""
config_get "max_connection_receive_window" "$section" "max_connection_receive_window" ""
config_get "ports" "$section" "ports" ""
config_get "hysteria2_protocol" "$section" "hysteria2_protocol" ""
config_get "hop_interval" "$section" "hop_interval" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1096,7 +1078,7 @@ EOF
fi
if [ -n "$max_stream_receive_window" ]; then
cat >> "$SERVER_FILE" <<-EOF
max_stream_receive_window: "$max_stream_receive_window"
max-stream-receive-window: "$max_stream_receive_window"
EOF
fi
if [ -n "$initial_connection_receive_window" ]; then
@@ -1133,6 +1115,35 @@ fi
#vless
if [ "$type" = "vless" ]; then
config_get "uuid" "$section" "uuid" ""
config_get "xudp" "$section" "xudp" ""
config_get "packet_addr" "$section" "packet_addr" ""
config_get "packet_encoding" "$section" "packet_encoding" ""
config_get "servername" "$section" "servername" ""
config_get "obfs_vless" "$section" "obfs_vless" ""
config_get "ws_opts_path" "$section" "ws_opts_path" ""
config_get "ws_opts_headers" "$section" "ws_opts_headers" ""
config_get "grpc_service_name" "$section" "grpc_service_name" ""
config_get "reality_public_key" "$section" "reality_public_key" ""
config_get "reality_short_id" "$section" "reality_short_id" ""
config_get "vless_flow" "$section" "vless_flow" ""
config_get "xhttp_opts_path" "$section" "xhttp_opts_path" ""
config_get "xhttp_opts_host" "$section" "xhttp_opts_host" ""
config_get "vless_encryption" "$section" "vless_encryption" ""
if [ "$obfs_vless" = "ws" ]; then
obfs_vless="network: ws"
fi
if [ "$obfs_vless" = "grpc" ]; then
obfs_vless="network: grpc"
fi
if [ "$obfs_vless" = "tcp" ]; then
obfs_vless="network: tcp"
fi
if [ "$obfs_vless" = "xhttp" ]; then
obfs_vless="network: xhttp"
fi
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1232,6 +1243,11 @@ EOF
if [ ! -z "$vless_flow" ]; then
cat >> "$SERVER_FILE" <<-EOF
flow: "$vless_flow"
EOF
fi
if [ -n "$vless_encryption" ]; then
cat >> "$SERVER_FILE" <<-EOF
encryption: "$vless_encryption"
EOF
fi
if [ -n "$reality_public_key" ] || [ -n "$reality_short_id" ]; then
@@ -1247,6 +1263,21 @@ EOF
if [ -n "$reality_short_id" ]; then
cat >> "$SERVER_FILE" <<-EOF
short-id: "$reality_short_id"
EOF
fi
fi
if [ "$obfs_vless" = "network: xhttp" ]; then
cat >> "$SERVER_FILE" <<-EOF
xhttp-opts:
EOF
if [ -n "$xhttp_opts_path" ]; then
cat >> "$SERVER_FILE" <<-EOF
path: "$xhttp_opts_path"
EOF
fi
if [ -n "$xhttp_opts_host" ]; then
cat >> "$SERVER_FILE" <<-EOF
host: "$xhttp_opts_host"
EOF
fi
fi
@@ -1276,6 +1307,13 @@ fi
#ssh
if [ "$type" = "ssh" ]; then
config_get "auth_name" "$section" "auth_name" ""
config_get "auth_pass" "$section" "auth_pass" ""
config_get "private_key" "$section" "private_key" ""
config_get "private_key_passphrase" "$section" "private_key_passphrase" ""
config_get "host_key" "$section" "host_key" ""
config_get "host_key_algorithms" "$section" "host_key_algorithms" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1318,6 +1356,9 @@ fi
#socks5
if [ "$type" = "socks5" ]; then
config_get "auth_name" "$section" "auth_name" ""
config_get "auth_pass" "$section" "auth_pass" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1358,6 +1399,10 @@ fi
#http
if [ "$type" = "http" ]; then
config_get "auth_name" "$section" "auth_name" ""
config_get "auth_pass" "$section" "auth_pass" ""
config_get "http_headers" "$section" "http_headers" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1399,6 +1444,11 @@ fi
#trojan
if [ "$type" = "trojan" ]; then
config_get "grpc_service_name" "$section" "grpc_service_name" ""
config_get "obfs_trojan" "$section" "obfs_trojan" ""
config_get "trojan_ws_path" "$section" "trojan_ws_path" ""
config_get "trojan_ws_headers" "$section" "trojan_ws_headers" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1467,6 +1517,11 @@ fi
#snell
if [ "$type" = "snell" ]; then
config_get "psk" "$section" "psk" ""
config_get "snell_version" "$section" "snell_version" ""
config_get "obfs_snell" "$section" "obfs_snell" ""
config_get "host" "$section" "host" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1490,6 +1545,13 @@ fi
#Sudoku
if [ "$type" = "sudoku" ]; then
config_get "sudoku_key" "$section" "sudoku_key" ""
config_get "aead_method" "$section" "aead_method" "none"
config_get "padding_min" "$section" "padding_min" ""
config_get "padding_max" "$section" "padding_max" ""
config_get "table_type" "$section" "table_type" "prefer_ascii"
config_get "http_mask" "$section" "http_mask" "true"
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
@@ -1528,6 +1590,132 @@ EOF
fi
fi
#MASQUE
if [ "$type" = "masque" ]; then
config_get "masque_private_key" "$section" "masque_private_key" ""
config_get "masque_public_key" "$section" "masque_public_key" ""
config_get "masque_ip" "$section" "masque_ip" ""
config_get "masque_ipv6" "$section" "masque_ipv6" ""
config_get "masque_mtu" "$section" "masque_mtu" ""
config_get "masque_remote_dns_resolve" "$section" "masque_remote_dns_resolve" ""
config_get "masque_dns" "$section" "masque_dns" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
server: "$server"
port: $port
EOF
if [ -n "$masque_private_key" ]; then
cat >> "$SERVER_FILE" <<-EOF
private-key: "$masque_private_key"
EOF
fi
if [ -n "$masque_public_key" ]; then
cat >> "$SERVER_FILE" <<-EOF
public-key: "$masque_public_key"
EOF
fi
if [ -n "$masque_ip" ]; then
cat >> "$SERVER_FILE" <<-EOF
ip: "$masque_ip"
EOF
fi
if [ -n "$masque_ipv6" ]; then
cat >> "$SERVER_FILE" <<-EOF
ipv6: "$masque_ipv6"
EOF
fi
if [ -n "$masque_mtu" ]; then
cat >> "$SERVER_FILE" <<-EOF
mtu: $masque_mtu
EOF
fi
if [ -n "$masque_remote_dns_resolve" ]; then
cat >> "$SERVER_FILE" <<-EOF
remote-dns-resolve: $masque_remote_dns_resolve
EOF
fi
if [ ! -z "$udp" ]; then
cat >> "$SERVER_FILE" <<-EOF
udp: $udp
EOF
fi
if [ ! -z "$masque_dns" ]; then
cat >> "$SERVER_FILE" <<-EOF
dns:
EOF
config_list_foreach "$section" "masque_dns" set_alpn
fi
fi
#TrustTunnel
if [ "$type" = "trusttunnel" ]; then
config_get "trusttunnel_username" "$section" "trusttunnel_username" ""
config_get "trusttunnel_password" "$section" "trusttunnel_password" ""
config_get "trusttunnel_health_check" "$section" "trusttunnel_health_check" ""
config_get "trusttunnel_quic" "$section" "trusttunnel_quic" ""
config_get "trusttunnel_congestion_controller" "$section" "trusttunnel_congestion_controller" ""
cat >> "$SERVER_FILE" <<-EOF
- name: "$name"
type: $type
server: "$server"
port: $port
EOF
if [ -n "$trusttunnel_username" ]; then
cat >> "$SERVER_FILE" <<-EOF
username: "$trusttunnel_username"
EOF
fi
if [ -n "$trusttunnel_password" ]; then
cat >> "$SERVER_FILE" <<-EOF
password: "$trusttunnel_password"
EOF
fi
if [ -n "$trusttunnel_health_check" ]; then
cat >> "$SERVER_FILE" <<-EOF
health-check: $trusttunnel_health_check
EOF
fi
if [ -n "$trusttunnel_quic" ]; then
cat >> "$SERVER_FILE" <<-EOF
quic: $trusttunnel_quic
EOF
fi
if [ -n "$trusttunnel_congestion_controller" ]; then
cat >> "$SERVER_FILE" <<-EOF
congestion-controller: "$trusttunnel_congestion_controller"
EOF
fi
if [ -n "$client_fingerprint" ]; then
cat >> "$SERVER_FILE" <<-EOF
client-fingerprint: "$client_fingerprint"
EOF
fi
if [ -n "$udp" ]; then
cat >> "$SERVER_FILE" <<-EOF
udp: $udp
EOF
fi
if [ -n "$sni" ]; then
cat >> "$SERVER_FILE" <<-EOF
sni: "$sni"
EOF
fi
if [ ! -z "$alpn" ]; then
cat >> "$SERVER_FILE" <<-EOF
alpn:
EOF
config_list_foreach "$section" "alpn" set_alpn
fi
if [ ! -z "$skip_cert_verify" ]; then
cat >> "$SERVER_FILE" <<-EOF
skip-cert-verify: $skip_cert_verify
EOF
fi
fi
#ip_version
if [ ! -z "$ip_version" ]; then
cat >> "$SERVER_FILE" <<-EOF
@@ -150,6 +150,13 @@ yml_other_set()
[]
end;
rule_providers_array = case custom_data.class.to_s
when 'Hash'
custom_data['rule-providers'].to_a if custom_data['rule-providers'].class.to_s == 'Hash'
else
[]
end;
next unless rules_array;
ipv4_regex = /^(\d{1,3}\.){3}\d{1,3}$/;
@@ -209,6 +216,11 @@ yml_other_set()
else
Value['rules'] = valid_rules.uniq;
end;
if rule_providers_array and not rule_providers_array.empty? then
Value['rule-providers'] ||= {};
Value['rule-providers'] = Value['rule-providers'].merge!(custom_data['rule-providers']);
end;
end;
};