mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-23 00:17:16 +08:00
3919 lines
117 KiB
Lua
3919 lines
117 KiB
Lua
module("luci.controller.openclash", package.seeall)
|
||
|
||
function index()
|
||
if not nixio.fs.access("/etc/config/openclash") then
|
||
return
|
||
end
|
||
|
||
local page
|
||
|
||
page = entry({"admin", "services", "openclash"}, alias("admin", "services", "openclash", "client"), _("OpenClash"), 50)
|
||
page.dependent = true
|
||
page.acl_depends = { "luci-app-openclash" }
|
||
entry({"admin", "services", "openclash", "client"},form("openclash/client"),_("Overviews"), 20).leaf = true
|
||
entry({"admin", "services", "openclash", "status"},call("action_status")).leaf=true
|
||
entry({"admin", "services", "openclash", "startlog"},call("action_start")).leaf=true
|
||
entry({"admin", "services", "openclash", "refresh_log"},call("action_refresh_log"))
|
||
entry({"admin", "services", "openclash", "del_log"},call("action_del_log"))
|
||
entry({"admin", "services", "openclash", "del_start_log"},call("action_del_start_log"))
|
||
entry({"admin", "services", "openclash", "close_all_connection"},call("action_close_all_connection"))
|
||
entry({"admin", "services", "openclash", "reload_firewall"},call("action_reload_firewall"))
|
||
entry({"admin", "services", "openclash", "lastversion"},call("action_lastversion"))
|
||
entry({"admin", "services", "openclash", "save_corever_branch"},call("action_save_corever_branch"))
|
||
entry({"admin", "services", "openclash", "update"},call("action_update"))
|
||
entry({"admin", "services", "openclash", "get_last_version"},call("action_get_last_version"))
|
||
entry({"admin", "services", "openclash", "update_info"},call("action_update_info"))
|
||
entry({"admin", "services", "openclash", "update_ma"},call("action_update_ma"))
|
||
entry({"admin", "services", "openclash", "opupdate"},call("action_opupdate"))
|
||
entry({"admin", "services", "openclash", "coreupdate"},call("action_coreupdate"))
|
||
entry({"admin", "services", "openclash", "flush_dns_cache"}, call("action_flush_dns_cache"))
|
||
entry({"admin", "services", "openclash", "flush_smart_cache"}, call("action_flush_smart_cache"))
|
||
entry({"admin", "services", "openclash", "update_config"}, call("action_update_config"))
|
||
entry({"admin", "services", "openclash", "download_rule"}, call("action_download_rule"))
|
||
entry({"admin", "services", "openclash", "restore"}, call("action_restore_config"))
|
||
entry({"admin", "services", "openclash", "backup"}, call("action_backup"))
|
||
entry({"admin", "services", "openclash", "backup_ex_core"}, call("action_backup_ex_core"))
|
||
entry({"admin", "services", "openclash", "backup_only_core"}, call("action_backup_only_core"))
|
||
entry({"admin", "services", "openclash", "backup_only_config"}, call("action_backup_only_config"))
|
||
entry({"admin", "services", "openclash", "backup_only_rule"}, call("action_backup_only_rule"))
|
||
entry({"admin", "services", "openclash", "backup_only_proxy"}, call("action_backup_only_proxy"))
|
||
entry({"admin", "services", "openclash", "remove_all_core"}, call("action_remove_all_core"))
|
||
entry({"admin", "services", "openclash", "one_key_update"}, call("action_one_key_update"))
|
||
entry({"admin", "services", "openclash", "one_key_update_check"}, call("action_one_key_update_check"))
|
||
entry({"admin", "services", "openclash", "switch_mode"}, call("action_switch_mode"))
|
||
entry({"admin", "services", "openclash", "op_mode"}, call("action_op_mode"))
|
||
entry({"admin", "services", "openclash", "sub_info_get"}, call("sub_info_get"))
|
||
entry({"admin", "services", "openclash", "config_name"}, call("action_config_name"))
|
||
entry({"admin", "services", "openclash", "switch_config"}, call("action_switch_config"))
|
||
entry({"admin", "services", "openclash", "toolbar_show"}, call("action_toolbar_show"))
|
||
entry({"admin", "services", "openclash", "toolbar_show_sys"}, call("action_toolbar_show_sys"))
|
||
entry({"admin", "services", "openclash", "diag_connection"}, call("action_diag_connection"))
|
||
entry({"admin", "services", "openclash", "diag_dns"}, call("action_diag_dns"))
|
||
entry({"admin", "services", "openclash", "gen_debug_logs"}, call("action_gen_debug_logs"))
|
||
entry({"admin", "services", "openclash", "log_level"}, call("action_log_level"))
|
||
entry({"admin", "services", "openclash", "switch_log"}, call("action_switch_log"))
|
||
entry({"admin", "services", "openclash", "rule_mode"}, call("action_rule_mode"))
|
||
entry({"admin", "services", "openclash", "switch_rule_mode"}, call("action_switch_rule_mode"))
|
||
entry({"admin", "services", "openclash", "switch_run_mode"}, call("action_switch_run_mode"))
|
||
entry({"admin", "services", "openclash", "dashboard_type"}, call("action_dashboard_type"))
|
||
entry({"admin", "services", "openclash", "switch_dashboard"}, call("action_switch_dashboard"))
|
||
entry({"admin", "services", "openclash", "delete_dashboard"}, call("action_delete_dashboard"))
|
||
entry({"admin", "services", "openclash", "default_dashboard"}, call("action_default_dashboard"))
|
||
entry({"admin", "services", "openclash", "get_run_mode"}, call("action_get_run_mode"))
|
||
entry({"admin", "services", "openclash", "create_file"}, call("create_file"))
|
||
entry({"admin", "services", "openclash", "rename_file"}, call("rename_file"))
|
||
entry({"admin", "services", "openclash", "manual_stream_unlock_test"}, call("manual_stream_unlock_test"))
|
||
entry({"admin", "services", "openclash", "all_proxies_stream_test"}, call("all_proxies_stream_test"))
|
||
entry({"admin", "services", "openclash", "set_subinfo_url"}, call("set_subinfo_url"))
|
||
entry({"admin", "services", "openclash", "check_core"}, call("action_check_core"))
|
||
entry({"admin", "services", "openclash", "core_download"}, call("core_download"))
|
||
entry({"admin", "services", "openclash", "announcement"}, call("action_announcement"))
|
||
entry({"admin", "services", "openclash", "settings"},cbi("openclash/settings"),_("Plugin Settings"), 30).leaf = true
|
||
entry({"admin", "services", "openclash", "config-overwrite"},cbi("openclash/config-overwrite"),_("Overwrite Settings"), 40).leaf = true
|
||
entry({"admin", "services", "openclash", "config-subscribe"},cbi("openclash/config-subscribe"),_("Config Subscribe"), 60).leaf = true
|
||
entry({"admin", "services", "openclash", "servers"},cbi("openclash/servers"),nil).leaf = true
|
||
entry({"admin", "services", "openclash", "other-rules-edit"},cbi("openclash/other-rules-edit"), nil).leaf = true
|
||
entry({"admin", "services", "openclash", "custom-dns-edit"},cbi("openclash/custom-dns-edit"), nil).leaf = true
|
||
entry({"admin", "services", "openclash", "other-file-edit"},cbi("openclash/other-file-edit"), nil).leaf = true
|
||
entry({"admin", "services", "openclash", "proxy-provider-file-manage"},form("openclash/proxy-provider-file-manage"), nil).leaf = true
|
||
entry({"admin", "services", "openclash", "rule-providers-file-manage"},form("openclash/rule-providers-file-manage"), nil).leaf = true
|
||
entry({"admin", "services", "openclash", "config-subscribe-edit"},cbi("openclash/config-subscribe-edit"), nil).leaf = true
|
||
entry({"admin", "services", "openclash", "servers-config"},cbi("openclash/servers-config"), nil).leaf = true
|
||
entry({"admin", "services", "openclash", "groups-config"},cbi("openclash/groups-config"), nil).leaf = true
|
||
entry({"admin", "services", "openclash", "proxy-provider-config"},cbi("openclash/proxy-provider-config"), nil).leaf = true
|
||
entry({"admin", "services", "openclash", "config"},form("openclash/config"),_("Config Manage"), 80).leaf = true
|
||
entry({"admin", "services", "openclash", "log"},cbi("openclash/log"),_("Server Logs"), 90).leaf = true
|
||
entry({"admin", "services", "openclash", "myip_check"}, call("action_myip_check"))
|
||
entry({"admin", "services", "openclash", "website_check"}, call("action_website_check"))
|
||
entry({"admin", "services", "openclash", "proxy_info"}, call("action_proxy_info"))
|
||
entry({"admin", "services", "openclash", "oc_settings"}, call("action_oc_settings"))
|
||
entry({"admin", "services", "openclash", "switch_oc_setting"}, call("action_switch_oc_setting"))
|
||
entry({"admin", "services", "openclash", "generate_pac"}, call("action_generate_pac"))
|
||
entry({"admin", "services", "openclash", "action"}, call("action_oc_action"))
|
||
entry({"admin", "services", "openclash", "config_file_list"}, call("action_config_file_list"))
|
||
entry({"admin", "services", "openclash", "config_file_read"}, call("action_config_file_read"))
|
||
entry({"admin", "services", "openclash", "config_file_save"}, call("action_config_file_save"))
|
||
entry({"admin", "services", "openclash", "upload_config"}, call("action_upload_config"))
|
||
entry({"admin", "services", "openclash", "add_subscription"}, call("action_add_subscription"))
|
||
entry({"admin", "services", "openclash", "upload_overwrite"}, call("action_upload_overwrite"))
|
||
entry({"admin", "services", "openclash", "overwrite_subscribe_info"}, call("action_overwrite_subscribe_info"))
|
||
entry({"admin", "services", "openclash", "overwrite_file_list"}, call("action_overwrite_file_list"))
|
||
entry({"admin", "services", "openclash", "delete_overwrite_file"}, call("delete_overwrite_file"))
|
||
entry({"admin", "services", "openclash", "get_subscribe_data"}, call("action_get_subscribe_data"))
|
||
entry({"admin", "services", "openclash", "get_subscribe_info_data"}, call("action_get_subscribe_info_data"))
|
||
end
|
||
|
||
local fs = require "luci.openclash"
|
||
local json = require "luci.jsonc"
|
||
local uci = require("luci.model.uci").cursor()
|
||
local datatype = require "luci.cbi.datatypes"
|
||
local opkg
|
||
local device_name = uci:get("system", "@system[0]", "hostname")
|
||
local device_arh = luci.sys.exec("uname -m |tr -d '\n'")
|
||
|
||
if pcall(require, "luci.model.ipkg") then
|
||
opkg = require "luci.model.ipkg"
|
||
else
|
||
opkg = nil
|
||
end
|
||
|
||
local core_path_mode = fs.uci_get_config("config", "small_flash_memory")
|
||
if core_path_mode ~= "1" then
|
||
meta_core_path="/etc/openclash/core/clash_meta"
|
||
else
|
||
meta_core_path="/tmp/etc/openclash/core/clash_meta"
|
||
end
|
||
|
||
local function is_running()
|
||
return luci.sys.call("pidof clash >/dev/null") == 0
|
||
end
|
||
|
||
local function is_start()
|
||
return process_status("/etc/init.d/openclash")
|
||
end
|
||
|
||
local function cn_port()
|
||
return fs.uci_get_config("config", "cn_port") or "9090"
|
||
end
|
||
|
||
local function mode()
|
||
return fs.uci_get_config("config", "en_mode")
|
||
end
|
||
|
||
local function daip()
|
||
return fs.lanip()
|
||
end
|
||
|
||
local function dase()
|
||
return fs.uci_get_config("config", "dashboard_password")
|
||
end
|
||
|
||
local function db_foward_domain()
|
||
return fs.uci_get_config("config", "dashboard_forward_domain")
|
||
end
|
||
|
||
local function db_foward_port()
|
||
return fs.uci_get_config("config", "dashboard_forward_port")
|
||
end
|
||
|
||
local function db_foward_ssl()
|
||
return fs.uci_get_config("config", "dashboard_forward_ssl") or 0
|
||
end
|
||
|
||
local function check_lastversion()
|
||
luci.sys.exec("bash /usr/share/openclash/openclash_version.sh 2>/dev/null")
|
||
return luci.sys.exec("sed -n '/^https:/,$p' /tmp/openclash_last_version 2>/dev/null")
|
||
end
|
||
|
||
local function startlog()
|
||
local info = ""
|
||
if fs.access("/tmp/openclash_start.log") then
|
||
info = luci.sys.exec("sed -n '$p' /tmp/openclash_start.log 2>/dev/null")
|
||
if string.len(info) > 0 then
|
||
info = trans_line(info)
|
||
end
|
||
end
|
||
return info
|
||
end
|
||
|
||
local function pkg_type()
|
||
if fs.access("/usr/bin/apk") then
|
||
return "apk"
|
||
else
|
||
return "opkg"
|
||
end
|
||
end
|
||
|
||
local function coremodel()
|
||
if opkg and opkg.info("libc") and opkg.info("libc")["libc"] then
|
||
return opkg.info("libc")["libc"]["Architecture"]
|
||
else
|
||
if pkg_type() == "opkg" then
|
||
return luci.sys.exec("rm -f /var/lock/opkg.lock && opkg status libc 2>/dev/null |grep 'Architecture' |awk -F ': ' '{print $2}' 2>/dev/null")
|
||
else
|
||
return luci.sys.exec("apk list libc 2>/dev/null |awk '{print $2}'")
|
||
end
|
||
end
|
||
end
|
||
|
||
local function check_core()
|
||
if not fs.access(meta_core_path) then
|
||
return "0"
|
||
else
|
||
return "1"
|
||
end
|
||
end
|
||
|
||
local function coremetacv()
|
||
local v = "0"
|
||
if not fs.access(meta_core_path) then
|
||
return v
|
||
else
|
||
v = luci.sys.exec(string.format("%s -v 2>/dev/null |awk -F ' ' '{print $3}' |head -1 |tr -d '\n'", meta_core_path))
|
||
if not v or v == "" then
|
||
return "0"
|
||
end
|
||
end
|
||
return v
|
||
end
|
||
|
||
local function corelv()
|
||
local core_meta_lv = ""
|
||
local core_smart_enable = fs.uci_get_config("config", "smart_enable") or "0"
|
||
if fs.access("/tmp/clash_last_version") then
|
||
if core_smart_enable == "1" then
|
||
core_meta_lv = luci.sys.exec("sed -n 2p /tmp/clash_last_version 2>/dev/null |tr -d '\n'")
|
||
else
|
||
core_meta_lv = luci.sys.exec("sed -n 1p /tmp/clash_last_version 2>/dev/null |tr -d '\n'")
|
||
end
|
||
else
|
||
core_meta_lv = "loading..."
|
||
end
|
||
|
||
action_get_last_version()
|
||
return core_meta_lv
|
||
end
|
||
|
||
local function opcv()
|
||
local v
|
||
local info = opkg and opkg.info("luci-app-openclash")
|
||
if info and info["luci-app-openclash"] and info["luci-app-openclash"]["Version"] and info["luci-app-openclash"]["Installed-Time"] then
|
||
v = info["luci-app-openclash"]["Version"]
|
||
else
|
||
if pkg_type() == "opkg" then
|
||
v = luci.sys.exec("rm -f /var/lock/opkg.lock && opkg status luci-app-openclash 2>/dev/null |grep 'Version' |awk -F 'Version: ' '{print $2}' |tr -d '\n'")
|
||
else
|
||
v = luci.sys.exec("apk list luci-app-openclash 2>/dev/null|grep 'installed' | grep -oE '[0-9]+(\\.[0-9]+)*' | head -1 |tr -d '\n'")
|
||
end
|
||
end
|
||
if v and v ~= "" then
|
||
return "v" .. v
|
||
else
|
||
return "0"
|
||
end
|
||
end
|
||
|
||
local function oplv()
|
||
local oplv = ""
|
||
|
||
if fs.access("/tmp/openclash_last_version") then
|
||
oplv = luci.sys.exec("sed -n 1p /tmp/openclash_last_version 2>/dev/null |tr -d '\n'")
|
||
else
|
||
oplv = "loading..."
|
||
end
|
||
|
||
action_get_last_version()
|
||
return oplv
|
||
end
|
||
|
||
local function opup()
|
||
return luci.sys.call("bash /usr/share/openclash/openclash_update.sh >/dev/null 2>&1 &")
|
||
end
|
||
|
||
local function coreup()
|
||
uci:set("openclash", "config", "enable", "1")
|
||
uci:commit("openclash")
|
||
local type = luci.http.formvalue("core_type")
|
||
return luci.sys.call(string.format("/usr/share/openclash/openclash_core.sh '%s' >/dev/null 2>&1 &", type))
|
||
end
|
||
|
||
local function corever()
|
||
return fs.uci_get_config("config", "core_version") or "0"
|
||
end
|
||
|
||
local function release_branch()
|
||
return fs.uci_get_config("config", "release_branch") or "master"
|
||
end
|
||
|
||
local function smart_enable()
|
||
return fs.uci_get_config("config", "smart_enable") or "0"
|
||
end
|
||
|
||
local function save_corever_branch()
|
||
if luci.http.formvalue("core_ver") then
|
||
uci:set("openclash", "config", "core_version", luci.http.formvalue("core_ver"))
|
||
end
|
||
if luci.http.formvalue("release_branch") then
|
||
uci:set("openclash", "config", "release_branch", luci.http.formvalue("release_branch"))
|
||
end
|
||
if luci.http.formvalue("smart_enable") then
|
||
uci:set("openclash", "config", "smart_enable", luci.http.formvalue("smart_enable"))
|
||
end
|
||
uci:commit("openclash")
|
||
return "success"
|
||
end
|
||
|
||
local function upchecktime()
|
||
local corecheck = os.date("%Y-%m-%d %H:%M:%S",fs.mtime("/tmp/clash_last_version"))
|
||
local opcheck
|
||
if not corecheck or corecheck == "" then
|
||
opcheck = os.date("%Y-%m-%d %H:%M:%S",fs.mtime("/tmp/openclash_last_version"))
|
||
if not opcheck or opcheck == "" then
|
||
return "1"
|
||
else
|
||
return opcheck
|
||
end
|
||
else
|
||
return corecheck
|
||
end
|
||
end
|
||
|
||
function core_download()
|
||
local cdn_url = luci.http.formvalue("url")
|
||
if cdn_url then
|
||
luci.sys.call(string.format("bash /usr/share/openclash/openclash_core.sh 'Meta' '%s' >/dev/null 2>&1 &", cdn_url))
|
||
else
|
||
luci.sys.call("bash /usr/share/openclash/openclash_core.sh 'Meta' >/dev/null 2>&1 &")
|
||
end
|
||
|
||
end
|
||
|
||
function action_flush_dns_cache()
|
||
local state = 0
|
||
if is_running() then
|
||
local daip = daip()
|
||
local dase = dase() or ""
|
||
local cn_port = cn_port()
|
||
if not daip or not cn_port then return end
|
||
fake_ip_state = luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPOST http://"%s":"%s"/cache/fakeip/flush', dase, daip, cn_port))
|
||
dns_state = luci.sys.exec(string.format('curl -sL -m 3 --retry 2-H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPOST http://"%s":"%s"/cache/dns/flush', dase, daip, cn_port))
|
||
end
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
flush_status = dns_state;
|
||
})
|
||
end
|
||
|
||
function action_flush_smart_cache()
|
||
local state = 0
|
||
if is_running() then
|
||
local daip = daip()
|
||
local dase = dase() or ""
|
||
local cn_port = cn_port()
|
||
if not daip or not cn_port then return end
|
||
flush_state = luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPOST http://"%s":"%s"/cache/smart/flush', dase, daip, cn_port))
|
||
end
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
flush_status = flush_state;
|
||
})
|
||
end
|
||
|
||
function action_update_config()
|
||
-- filename is basename
|
||
local filename = luci.http.formvalue("filename")
|
||
|
||
luci.http.prepare_content("application/json")
|
||
|
||
if not filename then
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Config file not found"
|
||
})
|
||
return
|
||
end
|
||
|
||
local update_result = luci.sys.call(string.format("/usr/share/openclash/openclash.sh '%s' >/dev/null 2>&1", filename))
|
||
|
||
if update_result == 0 then
|
||
luci.http.write_json({
|
||
status = "success",
|
||
message = "Config update started successfully",
|
||
filename = filename
|
||
})
|
||
else
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Failed to update config"
|
||
})
|
||
end
|
||
end
|
||
|
||
function action_restore_config()
|
||
uci:set("openclash", "config", "enable", "0")
|
||
uci:commit("openclash")
|
||
luci.sys.call("mkdir -p /etc/openclash/custom >/dev/null 2>&1")
|
||
luci.sys.call("mkdir -p /etc/openclash/overwrite >/dev/null 2>&1")
|
||
luci.sys.call("/etc/init.d/openclash stop >/dev/null 2>&1")
|
||
luci.sys.call("cp /usr/share/openclash/backup/openclash /etc/config/openclash >/dev/null 2>&1 &")
|
||
luci.sys.call("cp /usr/share/openclash/backup/openclash_custom* /etc/openclash/custom/ >/dev/null 2>&1 &")
|
||
luci.sys.call("cp /usr/share/openclash/backup/openclash_force_sniffing* /etc/openclash/custom/ >/dev/null 2>&1 &")
|
||
luci.sys.call("cp /usr/share/openclash/backup/openclash_sniffing* /etc/openclash/custom/ >/dev/null 2>&1 &")
|
||
luci.sys.call("cp /usr/share/openclash/backup/china_ip_route.ipset /etc/openclash/china_ip_route.ipset >/dev/null 2>&1 &")
|
||
luci.sys.call("cp /usr/share/openclash/backup/china_ip6_route.ipset /etc/openclash/china_ip6_route.ipset >/dev/null 2>&1 &")
|
||
luci.sys.call("cp /usr/share/openclash/backup/overwrite/default /etc/openclash/overwrite/default >/dev/null 2>&1 &")
|
||
luci.sys.call("rm -rf /etc/openclash/history/* >/dev/null 2>&1 &")
|
||
end
|
||
|
||
function action_remove_all_core()
|
||
luci.sys.call("rm -rf /etc/openclash/core/* >/dev/null 2>&1")
|
||
end
|
||
|
||
function action_one_key_update()
|
||
local cdn_url = luci.http.formvalue("url")
|
||
if cdn_url then
|
||
return luci.sys.call(string.format("bash /usr/share/openclash/openclash_update.sh 'one_key_update' '%s' >/dev/null 2>&1 &", cdn_url))
|
||
else
|
||
return luci.sys.call("bash /usr/share/openclash/openclash_update.sh 'one_key_update' >/dev/null 2>&1 &")
|
||
end
|
||
end
|
||
|
||
local function config_name()
|
||
local e,a={}
|
||
for t,o in ipairs(fs.glob("/etc/openclash/config/*"))do
|
||
a=fs.stat(o)
|
||
if a then
|
||
e[t]={}
|
||
e[t].name=fs.basename(o)
|
||
end
|
||
end
|
||
return json.parse(json.stringify(e)) or e
|
||
end
|
||
|
||
local function config_path()
|
||
if fs.uci_get_config("config", "config_path") then
|
||
return string.sub(fs.uci_get_config("config", "config_path"), 23, -1)
|
||
else
|
||
return ""
|
||
end
|
||
end
|
||
|
||
function action_switch_config()
|
||
local config_file = luci.http.formvalue("config_file")
|
||
local config_name = luci.http.formvalue("config_name")
|
||
|
||
if not config_file and config_name then
|
||
config_file = "/etc/openclash/config/" .. config_name
|
||
end
|
||
|
||
if not config_file then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "No config file specified"
|
||
})
|
||
return
|
||
end
|
||
|
||
if not fs.access(config_file) then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Config file does not exist: " .. config_file
|
||
})
|
||
return
|
||
end
|
||
|
||
uci:set("openclash", "config", "config_path", config_file)
|
||
uci:set("openclash", "config", "enable", "1")
|
||
uci:commit("openclash")
|
||
|
||
luci.sys.call("/etc/init.d/openclash restart >/dev/null 2>&1 &")
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "success",
|
||
message = "Config file switched successfully",
|
||
config_file = config_file
|
||
})
|
||
end
|
||
|
||
function set_subinfo_url()
|
||
local filename, url, info
|
||
filename = luci.http.formvalue("filename")
|
||
url = luci.http.formvalue("url")
|
||
if not filename then
|
||
info = "Oops: The config file name seems to be incorrect"
|
||
end
|
||
if url ~= "" and not string.find(url, "http") then
|
||
info = "Oops: The url link format seems to be incorrect"
|
||
end
|
||
if not info then
|
||
uci:foreach("openclash", "subscribe_info",
|
||
function(s)
|
||
if s.name == filename then
|
||
if url == "" then
|
||
uci:delete("openclash", s[".name"])
|
||
uci:commit("openclash")
|
||
info = "Delete success"
|
||
else
|
||
local url_list = {}
|
||
for line in string.gmatch(url, "[^\n]+") do
|
||
if line ~= "" then
|
||
table.insert(url_list, line)
|
||
end
|
||
end
|
||
uci:delete("openclash", s[".name"], "url")
|
||
uci:set_list("openclash", s[".name"], "url", url_list)
|
||
uci:commit("openclash")
|
||
info = "Success"
|
||
end
|
||
end
|
||
end
|
||
)
|
||
if not info then
|
||
if url == "" then
|
||
info = "Delete success"
|
||
else
|
||
local url_list = {}
|
||
for line in string.gmatch(url, "[^\n]+") do
|
||
if line ~= "" then
|
||
table.insert(url_list, line)
|
||
end
|
||
end
|
||
uci:section("openclash", "subscribe_info", nil, {name = filename, url = url_list})
|
||
uci:commit("openclash")
|
||
info = "Success"
|
||
end
|
||
end
|
||
end
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
info = info;
|
||
})
|
||
end
|
||
|
||
function fetch_sub_info(sub_url, sub_ua)
|
||
local info, upload, download, total, day_expire, http_code
|
||
local used, expire, day_left, percent, surplus
|
||
|
||
info = luci.sys.exec(string.format("curl -sLI -X GET -m 10 --retry 2 -w 'http_code=%%{http_code}' -H 'User-Agent: %s' '%s'", sub_ua, sub_url))
|
||
local http_match = string.match(info, "http_code=(%d+)")
|
||
if not info or not http_match or tonumber(http_match) ~= 200 then
|
||
info = luci.sys.exec(string.format("curl -sLI -X GET -m 10 --retry 2 -w 'http_code=%%{http_code}' -H 'User-Agent: Quantumultx' '%s'", sub_url))
|
||
http_match = string.match(info, "http_code=(%d+)")
|
||
end
|
||
|
||
if info and http_match then
|
||
http_code = http_match
|
||
if tonumber(http_code) == 200 then
|
||
info = string.lower(info)
|
||
if string.find(info, "subscription%-userinfo") then
|
||
local sub_info_line = ""
|
||
for line in info:gmatch("[^\r\n]+") do
|
||
if string.find(line, "subscription%-userinfo") then
|
||
sub_info_line = line
|
||
break
|
||
end
|
||
end
|
||
info = sub_info_line
|
||
local upload_match = string.match(info, "upload=(%d+)")
|
||
local download_match = string.match(info, "download=(%d+)")
|
||
local total_match = string.match(info, "total=(%d+)")
|
||
local expire_match = string.match(info, "expire=(%d+)")
|
||
|
||
upload = upload_match and tonumber(upload_match) or nil
|
||
download = download_match and tonumber(download_match) or nil
|
||
total = total_match and tonumber(string.format("%.1f", total_match)) or nil
|
||
used = upload and download and tonumber(string.format("%.1f", upload + download)) or nil
|
||
day_expire = expire_match and tonumber(expire_match) or nil
|
||
|
||
if day_expire and day_expire == 0 then
|
||
expire = luci.i18n.translate("Long-term")
|
||
elseif day_expire then
|
||
expire = os.date("%Y-%m-%d %H:%M:%S", day_expire) or "null"
|
||
else
|
||
expire = "null"
|
||
end
|
||
|
||
if day_expire and day_expire ~= 0 and os.time() <= day_expire then
|
||
day_left = math.ceil((day_expire - os.time()) / (3600*24))
|
||
if math.ceil(day_left / 365) > 50 then
|
||
day_left = "∞"
|
||
end
|
||
elseif day_expire and day_expire == 0 then
|
||
day_left = "∞"
|
||
elseif day_expire == nil then
|
||
day_left = "null"
|
||
else
|
||
day_left = 0
|
||
end
|
||
|
||
if used and total and used <= total and total > 0 then
|
||
percent = string.format("%.1f",((total-used)/total)*100) or "100"
|
||
surplus = fs.filesize(total - used)
|
||
elseif used and total and used > total and total > 0 then
|
||
percent = "0"
|
||
surplus = "-"..fs.filesize(total - used)
|
||
elseif used and total and used < total and total == 0.0 then
|
||
percent = "0"
|
||
surplus = fs.filesize(total - used)
|
||
elseif used and total and used == total and total == 0.0 then
|
||
percent = "0"
|
||
surplus = "0.0 KB"
|
||
elseif used and total and used > total and total == 0.0 then
|
||
percent = "100"
|
||
surplus = fs.filesize(total - used)
|
||
elseif used == nil and total and total > 0.0 then
|
||
percent = 100
|
||
surplus = fs.filesize(total)
|
||
elseif used == nil and total and total == 0.0 then
|
||
percent = 100
|
||
surplus = "∞"
|
||
else
|
||
percent = 0
|
||
surplus = "null"
|
||
end
|
||
|
||
local total_formatted, used_formatted
|
||
if total and total > 0 then
|
||
total_formatted = fs.filesize(total)
|
||
elseif total and total == 0.0 then
|
||
total_formatted = "∞"
|
||
else
|
||
total_formatted = "null"
|
||
end
|
||
used_formatted = fs.filesize(used)
|
||
|
||
return {
|
||
http_code = http_code,
|
||
surplus = surplus,
|
||
used = used_formatted,
|
||
total = total_formatted,
|
||
percent = percent,
|
||
day_left = day_left,
|
||
expire = expire
|
||
}
|
||
end
|
||
end
|
||
end
|
||
|
||
return nil
|
||
end
|
||
|
||
local function parse_url_with_name(raw_url, default_name)
|
||
local url, name = string.match(raw_url, "^(.-)#name=(.+)$")
|
||
if url then
|
||
return url, name
|
||
else
|
||
return raw_url, default_name
|
||
end
|
||
end
|
||
|
||
function get_sub_url(filename)
|
||
local sub_url = nil
|
||
local info_tb = {}
|
||
local providers = {}
|
||
|
||
-- Priority 1: subscribe_info
|
||
uci:foreach("openclash", "subscribe_info",
|
||
function(s)
|
||
if s.name == filename and s.url then
|
||
if type(s.url) == "table" then
|
||
info_tb = s.url
|
||
else
|
||
string.gsub(s.url, '[^\n]+', function(w) table.insert(info_tb, w) end)
|
||
end
|
||
if #info_tb == 1 then
|
||
local url, _ = parse_url_with_name(info_tb[1], filename)
|
||
sub_url = url
|
||
elseif #info_tb > 1 then
|
||
for _, raw in ipairs(info_tb) do
|
||
local url, name = parse_url_with_name(raw, filename)
|
||
table.insert(providers, {name = name, url = url})
|
||
end
|
||
end
|
||
end
|
||
end
|
||
)
|
||
|
||
if sub_url then
|
||
return {type = "single", url = sub_url}
|
||
end
|
||
|
||
if #providers > 0 then
|
||
return {type = "multiple", providers = providers}
|
||
end
|
||
|
||
-- Priority 2: YAML proxy-providers (use actual config file content first)
|
||
local config_path = "/etc/openclash/config/" .. fs.basename(filename .. ".yaml")
|
||
|
||
if fs.access(config_path) then
|
||
local ruby_result = luci.sys.exec(string.format([[
|
||
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e '
|
||
begin
|
||
config = YAML.load_file("%s")
|
||
providers = []
|
||
if config && config["proxy-providers"]
|
||
config["proxy-providers"].each do |name, provider|
|
||
# Only include providers with non-empty URLs
|
||
if provider && provider["url"] && !provider["url"].to_s.empty?
|
||
providers << {"name" => name, "url" => provider["url"].to_s}
|
||
end
|
||
end
|
||
end
|
||
# Manual JSON output (ruby-json is not a dependency)
|
||
result = "["
|
||
providers.each_with_index do |p, i|
|
||
result << "," if i > 0
|
||
# Escape quotes in name and URL
|
||
name_escaped = p["name"].gsub("\"", "\\\\\"")
|
||
url_escaped = p["url"].gsub("\"", "\\\\\"")
|
||
result << "{\"name\":\"#{name_escaped}\",\"url\":\"#{url_escaped}\"}"
|
||
end
|
||
result << "]"
|
||
puts result
|
||
rescue => e
|
||
puts "[]"
|
||
end
|
||
' 2>/dev/null || echo '[]'
|
||
]], config_path)):gsub("\n", "")
|
||
|
||
if ruby_result and ruby_result ~= "" and ruby_result ~= "[]" then
|
||
local success, parsed_providers = pcall(function()
|
||
return json.parse(ruby_result)
|
||
end)
|
||
|
||
if success and parsed_providers and #parsed_providers > 0 then
|
||
return {type = "multiple", providers = parsed_providers}
|
||
end
|
||
end
|
||
end
|
||
|
||
-- Priority 3: config_subscribe table (last fallback)
|
||
uci:foreach("openclash", "config_subscribe",
|
||
function(s)
|
||
if s.name == filename and s.address and string.find(s.address, "http") then
|
||
string.gsub(s.address, '[^\n]+', function(w) table.insert(info_tb, w) end)
|
||
sub_url = info_tb[1]
|
||
end
|
||
end
|
||
)
|
||
|
||
if sub_url then
|
||
return {type = "single", url = sub_url}
|
||
end
|
||
|
||
return nil
|
||
end
|
||
|
||
function sub_info_get()
|
||
local sub_ua, filename, sub_info, url_result
|
||
local providers_data = {}
|
||
|
||
filename = luci.http.formvalue("filename")
|
||
sub_info = ""
|
||
sub_ua = "Clash"
|
||
|
||
uci:foreach("openclash", "config_subscribe",
|
||
function(s)
|
||
if s.name == filename and s.sub_ua then
|
||
sub_ua = s.sub_ua
|
||
end
|
||
end
|
||
)
|
||
|
||
if filename and not is_start() then
|
||
url_result = get_sub_url(filename)
|
||
|
||
if not url_result then
|
||
sub_info = "No Sub Info Found"
|
||
elseif url_result.type == "single" then
|
||
local info = fetch_sub_info(url_result.url, sub_ua)
|
||
if info then
|
||
table.insert(providers_data, info)
|
||
sub_info = "Successful"
|
||
else
|
||
sub_info = "No Sub Info Found"
|
||
end
|
||
elseif url_result.type == "multiple" then
|
||
for i, provider in ipairs(url_result.providers) do
|
||
local info = fetch_sub_info(provider.url, sub_ua)
|
||
if info then
|
||
info.provider_name = provider.name
|
||
table.insert(providers_data, info)
|
||
end
|
||
end
|
||
|
||
if #providers_data > 0 then
|
||
sub_info = "Successful"
|
||
else
|
||
sub_info = "No Sub Info Found"
|
||
end
|
||
end
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
sub_info = sub_info,
|
||
providers = providers_data,
|
||
get_time = os.time(),
|
||
url_result = url_result
|
||
})
|
||
end
|
||
|
||
function action_rule_mode()
|
||
local mode, info
|
||
if is_running() then
|
||
local daip = daip()
|
||
local dase = dase() or ""
|
||
local cn_port = cn_port()
|
||
if not daip or not cn_port then return end
|
||
info = json.parse(luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/configs', dase, daip, cn_port)))
|
||
if info then
|
||
mode = info["mode"]
|
||
else
|
||
mode = fs.uci_get_config("config", "proxy_mode") or "rule"
|
||
end
|
||
else
|
||
mode = fs.uci_get_config("config", "proxy_mode") or "rule"
|
||
end
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
mode = mode;
|
||
})
|
||
end
|
||
|
||
function action_switch_rule_mode()
|
||
local mode, info
|
||
local daip = daip()
|
||
local dase = dase() or ""
|
||
local cn_port = cn_port()
|
||
mode = luci.http.formvalue("rule_mode")
|
||
|
||
if not mode then
|
||
luci.http.status(400, "Missing parameters")
|
||
return
|
||
end
|
||
|
||
if is_running() then
|
||
if not daip or not cn_port then luci.http.status(500, "Switch Faild") return end
|
||
info = luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPATCH http://"%s":"%s"/configs -d \'{\"mode\": \"%s\"}\'', dase, daip, cn_port, mode))
|
||
if info ~= "" then
|
||
luci.http.status(500, "Switch Faild")
|
||
end
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
info = info;
|
||
})
|
||
end
|
||
uci:set("openclash", "config", "proxy_mode", mode)
|
||
uci:set("openclash", "@overwrite[0]", "proxy_mode", mode)
|
||
uci:commit("openclash")
|
||
end
|
||
|
||
function action_get_run_mode()
|
||
if mode() then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
mode = mode();
|
||
})
|
||
else
|
||
luci.http.status(500, "Get Faild")
|
||
return
|
||
end
|
||
end
|
||
|
||
function action_switch_run_mode()
|
||
local mode, operation_mode
|
||
mode = luci.http.formvalue("run_mode")
|
||
operation_mode = fs.uci_get_config("config", "operation_mode")
|
||
if operation_mode == "redir-host" then
|
||
uci:set("openclash", "config", "en_mode", "redir-host"..mode)
|
||
uci:set("openclash", "@overwrite[0]", "en_mode", "redir-host"..mode)
|
||
elseif operation_mode == "fake-ip" then
|
||
uci:set("openclash", "config", "en_mode", "fake-ip"..mode)
|
||
uci:set("openclash", "@overwrite[0]", "en_mode", "fake-ip"..mode)
|
||
end
|
||
uci:commit("openclash")
|
||
if is_running() then
|
||
luci.sys.exec("/etc/init.d/openclash restart >/dev/null 2>&1 &")
|
||
end
|
||
end
|
||
|
||
function action_log_level()
|
||
local level, info
|
||
if is_running() then
|
||
local daip = daip()
|
||
local dase = dase() or ""
|
||
local cn_port = cn_port()
|
||
if not daip or not cn_port then return end
|
||
info = json.parse(luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/configs', dase, daip, cn_port)))
|
||
if info then
|
||
level = info["log-level"]
|
||
else
|
||
level = fs.uci_get_config("config", "log_level") or "info"
|
||
end
|
||
else
|
||
level = fs.uci_get_config("config", "log_level") or "info"
|
||
end
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
log_level = level;
|
||
})
|
||
end
|
||
|
||
local function s(e)
|
||
local t=0
|
||
local a={' B/S',' KB/S',' MB/S',' GB/S',' TB/S',' PB/S'}
|
||
if (e<=1024) then
|
||
return e..a[1]
|
||
else
|
||
repeat
|
||
e=e/1024
|
||
t=t+1
|
||
until(e<=1024)
|
||
return string.format("%.1f",e)..a[t]
|
||
end
|
||
end
|
||
|
||
function action_toolbar_show_sys()
|
||
local cpu = "0"
|
||
local load_avg = "0"
|
||
local cpu_count = luci.sys.exec("grep -c ^processor /proc/cpuinfo 2>/dev/null"):gsub("\n", "") or 1
|
||
|
||
local pid = luci.sys.exec("pgrep -f '^[^ ]*clash' | head -1 | tr -d '\n' 2>/dev/null")
|
||
|
||
if pid and pid ~= "" then
|
||
cpu = luci.sys.exec(string.format([[
|
||
top -b -n1 | awk -v pid="%s" '
|
||
BEGIN { cpu_col=0; }
|
||
$0 ~ /%%CPU/ {
|
||
for(i=1;i<=NF;i++) if($i=="%%CPU") cpu_col=i;
|
||
next
|
||
}
|
||
cpu_col>0 && $1==pid { print $cpu_col }
|
||
'
|
||
]], pid))
|
||
if cpu and cpu ~= "" then
|
||
cpu = string.match(cpu, "%d+%.?%d*") or "0"
|
||
else
|
||
cpu = "0"
|
||
end
|
||
|
||
load_avg = luci.sys.exec("awk '{print $2; exit}' /proc/loadavg 2>/dev/null"):gsub("\n", "") or "0"
|
||
|
||
if not string.match(load_avg, "^[0-9]*%.?[0-9]*$") then
|
||
load_avg = "0"
|
||
end
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
cpu = cpu,
|
||
load_avg = tostring(math.floor(tonumber(load_avg) / tonumber(cpu_count) * 100));
|
||
})
|
||
end
|
||
|
||
function action_toolbar_show()
|
||
local pid = luci.sys.exec("pgrep -f '^[^ ]*clash' | head -1 | tr -d '\n' 2>/dev/null")
|
||
local traffic, connections, connection, up, down, up_total, down_total, mem, cpu, load_avg, cpu_count
|
||
if pid and pid ~= "" then
|
||
local daip = daip()
|
||
local dase = dase() or ""
|
||
local cn_port = cn_port()
|
||
if not daip or not cn_port then return end
|
||
|
||
traffic = json.parse(luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/traffic', dase, daip, cn_port)))
|
||
connections = json.parse(luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/connections', dase, daip, cn_port)))
|
||
|
||
if traffic and connections and connections.connections then
|
||
connection = #(connections.connections)
|
||
up = s(traffic.up)
|
||
down = s(traffic.down)
|
||
up_total = fs.filesize(connections.uploadTotal)
|
||
down_total = fs.filesize(connections.downloadTotal)
|
||
else
|
||
up = "0 B/S"
|
||
down = "0 B/S"
|
||
up_total = "0 KB"
|
||
down_total = "0 KB"
|
||
connection = "0"
|
||
end
|
||
|
||
mem = tonumber(luci.sys.exec(string.format("cat /proc/%s/status 2>/dev/null |grep -w VmRSS |awk '{print $2}'", pid)))
|
||
cpu = luci.sys.exec(string.format([[
|
||
top -b -n1 | awk -v pid="%s" '
|
||
BEGIN { cpu_col=0; }
|
||
$0 ~ /%%CPU/ {
|
||
for(i=1;i<=NF;i++) if($i=="%%CPU") cpu_col=i;
|
||
next
|
||
}
|
||
cpu_col>0 && $1==pid { print $cpu_col }
|
||
'
|
||
]], pid))
|
||
|
||
if mem and cpu then
|
||
mem = fs.filesize(mem*1024) or "0 KB"
|
||
cpu = string.match(cpu, "%d+%.?%d*") or "0"
|
||
else
|
||
mem = "0 KB"
|
||
cpu = "0"
|
||
end
|
||
|
||
load_avg = luci.sys.exec("awk '{print $2; exit}' /proc/loadavg 2>/dev/null"):gsub("\n", "") or "0"
|
||
cpu_count = luci.sys.exec("grep -c ^processor /proc/cpuinfo 2>/dev/null"):gsub("\n", "") or 1
|
||
|
||
if not string.match(load_avg, "^[0-9]*%.?[0-9]*$") then
|
||
load_avg = "0"
|
||
end
|
||
else
|
||
return
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
connections = connection,
|
||
up = up,
|
||
down = down,
|
||
up_total = up_total,
|
||
down_total = down_total,
|
||
mem = mem,
|
||
cpu = cpu,
|
||
load_avg = tostring(math.floor(tonumber(load_avg) / tonumber(cpu_count) * 100));
|
||
})
|
||
end
|
||
|
||
function action_config_name()
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
config_name = config_name(),
|
||
config_path = config_path();
|
||
})
|
||
end
|
||
|
||
function action_save_corever_branch()
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
save_corever_branch = save_corever_branch();
|
||
})
|
||
end
|
||
|
||
function action_one_key_update_check()
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
corever = corever();
|
||
})
|
||
end
|
||
|
||
function action_dashboard_type()
|
||
local dashboard_type = fs.uci_get_config("config", "dashboard_type") or "Official"
|
||
local yacd_type = fs.uci_get_config("config", "yacd_type") or "Official"
|
||
local default_dashboard = fs.uci_get_config("config", "default_dashboard") or ""
|
||
if not fs.isdirectory("/usr/share/openclash/ui/" .. default_dashboard) then
|
||
default_dashboard = ""
|
||
end
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
dashboard_type = dashboard_type,
|
||
yacd_type = yacd_type,
|
||
yacd = fs.isdirectory("/usr/share/openclash/ui/yacd"),
|
||
dashboard = fs.isdirectory("/usr/share/openclash/ui/dashboard"),
|
||
metacubexd = fs.isdirectory("/usr/share/openclash/ui/metacubexd"),
|
||
zashboard = fs.isdirectory("/usr/share/openclash/ui/zashboard"),
|
||
default_dashboard = default_dashboard;
|
||
})
|
||
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")
|
||
return
|
||
end
|
||
if not fs.isdirectory("/usr/share/openclash/ui/" .. string.lower(default_dashboard)) then
|
||
luci.http.status(500, "Set Failed")
|
||
return
|
||
end
|
||
uci:set("openclash", "config", "default_dashboard", string.lower(default_dashboard))
|
||
uci:commit("openclash")
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
default_dashboard = default_dashboard;
|
||
})
|
||
end
|
||
|
||
function action_switch_dashboard()
|
||
local switch_name = luci.http.formvalue("name")
|
||
local switch_type = luci.http.formvalue("type")
|
||
local state = luci.sys.call(string.format('/usr/share/openclash/openclash_download_dashboard.sh "%s" "%s" >/dev/null 2>&1', switch_name, switch_type))
|
||
if switch_name == "Dashboard" and tonumber(state) == 0 then
|
||
if switch_type == "Official" then
|
||
uci:set("openclash", "config", "dashboard_type", "Official")
|
||
uci:commit("openclash")
|
||
else
|
||
uci:set("openclash", "config", "dashboard_type", "Meta")
|
||
uci:commit("openclash")
|
||
end
|
||
elseif switch_name == "Yacd" and tonumber(state) == 0 then
|
||
if switch_type == "Official" then
|
||
uci:set("openclash", "config", "yacd_type", "Official")
|
||
uci:commit("openclash")
|
||
else
|
||
uci:set("openclash", "config", "yacd_type", "Meta")
|
||
uci:commit("openclash")
|
||
end
|
||
end
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
download_state = state;
|
||
})
|
||
end
|
||
|
||
function action_delete_dashboard()
|
||
local delete_name = luci.http.formvalue("name")
|
||
local delete_path = string.format("/usr/share/openclash/ui/%s", string.lower(delete_name))
|
||
|
||
local panels = {
|
||
"/usr/share/openclash/ui/dashboard",
|
||
"/usr/share/openclash/ui/yacd",
|
||
"/usr/share/openclash/ui/metacubexd",
|
||
"/usr/share/openclash/ui/zashboard"
|
||
}
|
||
local existing_panels = 0
|
||
for _, path in ipairs(panels) do
|
||
if fs.isdirectory(path) then
|
||
existing_panels = existing_panels + 1
|
||
end
|
||
end
|
||
|
||
if existing_panels <= 1 then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
delete_state = 0,
|
||
error = "Cannot delete the last remaining dashboard"
|
||
})
|
||
return
|
||
end
|
||
|
||
local state = luci.sys.call(string.format("rm -rf '%s' >/dev/null 2>&1", delete_path)) == 0 and 1 or 0
|
||
if tonumber(state) == 1 then
|
||
if delete_name == "Dashboard" then
|
||
uci:set("openclash", "config", "dashboard_type", "Official")
|
||
uci:commit("openclash")
|
||
elseif delete_name == "Yacd" then
|
||
uci:set("openclash", "config", "yacd_type", "Official")
|
||
uci:commit("openclash")
|
||
end
|
||
if fs.uci_get_config("config", "default_dashboard") == string.lower(delete_name) then
|
||
uci:set("openclash", "config", "default_dashboard", "")
|
||
uci:commit("openclash")
|
||
end
|
||
end
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
delete_state = state;
|
||
})
|
||
end
|
||
|
||
function action_op_mode()
|
||
local op_mode = fs.uci_get_config("config", "operation_mode")
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
op_mode = op_mode;
|
||
})
|
||
end
|
||
|
||
function action_switch_mode()
|
||
local switch_mode = fs.uci_get_config("config", "operation_mode")
|
||
if switch_mode == "redir-host" then
|
||
uci:set("openclash", "config", "operation_mode", "fake-ip")
|
||
uci:commit("openclash")
|
||
else
|
||
uci:set("openclash", "config", "operation_mode", "redir-host")
|
||
uci:commit("openclash")
|
||
end
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
switch_mode = switch_mode;
|
||
})
|
||
end
|
||
|
||
function action_status()
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
clash = uci:get("openclash", "config", "enable") == "1",
|
||
daip = daip(),
|
||
dase = dase(),
|
||
db_foward_port = db_foward_port(),
|
||
db_foward_domain = db_foward_domain(),
|
||
db_forward_ssl = db_foward_ssl(),
|
||
cn_port = cn_port(),
|
||
yacd = fs.isdirectory("/usr/share/openclash/ui/yacd"),
|
||
dashboard = fs.isdirectory("/usr/share/openclash/ui/dashboard"),
|
||
metacubexd = fs.isdirectory("/usr/share/openclash/ui/metacubexd"),
|
||
zashboard = fs.isdirectory("/usr/share/openclash/ui/zashboard"),
|
||
core_type = fs.uci_get_config("config", "core_type") or "Meta";
|
||
})
|
||
end
|
||
|
||
function action_lastversion()
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
lastversion = check_lastversion();
|
||
})
|
||
end
|
||
|
||
function action_start()
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
startlog = startlog();
|
||
})
|
||
end
|
||
|
||
function action_get_last_version()
|
||
if not process_status("/usr/share/openclash/clash_version.sh") then
|
||
if tonumber(os.time() - (fs.mtime("/tmp/clash_last_version") or 0)) > 1800 then
|
||
luci.sys.call("bash /usr/share/openclash/clash_version.sh &")
|
||
end
|
||
end
|
||
if not process_status("/usr/share/openclash/openclash_version.sh") then
|
||
if tonumber(os.time() - (fs.mtime("/tmp/openclash_last_version") or 0)) > 1800 then
|
||
luci.sys.call("bash /usr/share/openclash/openclash_version.sh &")
|
||
end
|
||
end
|
||
end
|
||
|
||
function action_update()
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
coremodel = coremodel(),
|
||
coremetacv = coremetacv(),
|
||
corelv = corelv(),
|
||
opcv = opcv(),
|
||
oplv = oplv(),
|
||
upchecktime = upchecktime();
|
||
})
|
||
end
|
||
|
||
function action_update_info()
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
corever = corever(),
|
||
release_branch = release_branch(),
|
||
smart_enable = smart_enable();
|
||
})
|
||
end
|
||
|
||
function action_update_ma()
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
oplv = oplv(),
|
||
pkg_type = pkg_type(),
|
||
corelv = corelv(),
|
||
corever = corever();
|
||
})
|
||
end
|
||
|
||
function action_opupdate()
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
opup = opup();
|
||
})
|
||
end
|
||
|
||
function action_check_core()
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
core_status = check_core();
|
||
})
|
||
end
|
||
|
||
function action_coreupdate()
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
coreup = coreup();
|
||
})
|
||
end
|
||
|
||
function action_close_all_connection()
|
||
return luci.sys.call("sh /usr/share/openclash/openclash_history_get.sh 'close_all_conection'")
|
||
end
|
||
|
||
function action_reload_firewall()
|
||
return luci.sys.call("/etc/init.d/openclash reload 'manual' >/dev/null 2>&1 &")
|
||
end
|
||
|
||
function action_download_rule()
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
rule_download_status = download_rule();
|
||
})
|
||
end
|
||
|
||
function action_refresh_log()
|
||
luci.http.prepare_content("application/json")
|
||
local logfile = "/tmp/openclash.log"
|
||
local log_len = tonumber(luci.http.formvalue("log_len")) or 0
|
||
local core_refresh = luci.http.formvalue("core_refresh") == "true"
|
||
|
||
if not fs.access(logfile) then
|
||
luci.http.write_json({
|
||
len = 0,
|
||
update = false,
|
||
core_log = "",
|
||
oc_log = ""
|
||
})
|
||
return
|
||
end
|
||
|
||
local total_lines = tonumber(luci.sys.exec("wc -l < " .. logfile)) or 0
|
||
|
||
if total_lines == log_len and log_len > 0 then
|
||
luci.http.write_json({
|
||
len = total_lines,
|
||
update = false,
|
||
core_log = "",
|
||
oc_log = ""
|
||
})
|
||
return
|
||
end
|
||
|
||
local exclude_pattern = "UDP%-Receive%-Buffer%-Size|^Sec%-Fetch%-Mode|^User%-Agent|^Access%-Control|^Accept|^Origin|^Referer|^Connection|^Pragma|^Cache%-"
|
||
local core_pattern = "level=|^time="
|
||
local limit = 1000
|
||
local start_line = (log_len > 0 and total_lines > log_len) and (log_len + 1) or 1
|
||
local core_cmd, oc_cmd, core_raw, oc_raw
|
||
local core_logs = {}
|
||
local oc_logs = {}
|
||
|
||
core_cmd = string.format(
|
||
"tail -n +%d '%s' | grep -v -E '%s' | grep -E '%s' | tail -n %d",
|
||
start_line, logfile, exclude_pattern, core_pattern, limit
|
||
)
|
||
|
||
oc_cmd = string.format(
|
||
"tail -n +%d '%s' | grep -v -E '%s' | grep -v -E '%s' | tail -n %d",
|
||
start_line, logfile, exclude_pattern, core_pattern, limit
|
||
)
|
||
|
||
if core_refresh then
|
||
core_raw = luci.sys.exec(core_cmd)
|
||
end
|
||
|
||
oc_raw = luci.sys.exec(oc_cmd)
|
||
|
||
if core_raw and core_raw ~= "" then
|
||
for line in core_raw:gmatch("[^\n]+") do
|
||
table.insert(core_logs, line)
|
||
end
|
||
end
|
||
|
||
if oc_raw and oc_raw ~= "" then
|
||
for line in oc_raw:gmatch("[^\n]+") do
|
||
if not string.match(string.sub(line, 1, 19), "%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d") then
|
||
line = os.date("%Y-%m-%d %H:%M:%S") .. ' [Fatal] ' .. line
|
||
end
|
||
table.insert(oc_logs, trans_line(line))
|
||
end
|
||
end
|
||
|
||
if #core_logs > limit then
|
||
core_logs = {table.unpack(core_logs, #core_logs - limit + 1)}
|
||
end
|
||
if #oc_logs > limit then
|
||
oc_logs = {table.unpack(oc_logs, #oc_logs - limit + 1)}
|
||
end
|
||
|
||
local core_log = #core_logs > 0 and table.concat(core_logs, "\n") or ""
|
||
local oc_log = #oc_logs > 0 and table.concat(oc_logs, "\n") or ""
|
||
|
||
luci.http.write_json({
|
||
len = total_lines,
|
||
update = true,
|
||
core_log = core_log,
|
||
oc_log = oc_log
|
||
})
|
||
end
|
||
|
||
function action_del_log()
|
||
luci.sys.exec(": > /tmp/openclash.log")
|
||
return
|
||
end
|
||
|
||
function action_del_start_log()
|
||
luci.sys.exec("echo '##FINISH##' > /tmp/openclash_start.log")
|
||
return
|
||
end
|
||
|
||
function split(str,delimiter)
|
||
local dLen = string.len(delimiter)
|
||
local newDeli = ''
|
||
for i=1,dLen,1 do
|
||
newDeli = newDeli .. "["..string.sub(delimiter,i,i).."]"
|
||
end
|
||
|
||
local locaStart,locaEnd = string.find(str,newDeli)
|
||
local arr = {}
|
||
local n = 1
|
||
while locaStart ~= nil
|
||
do
|
||
if locaStart>0 then
|
||
arr[n] = string.sub(str,1,locaStart-1)
|
||
n = n + 1
|
||
end
|
||
|
||
str = string.sub(str,locaEnd+1,string.len(str))
|
||
locaStart,locaEnd = string.find(str,newDeli)
|
||
end
|
||
if str ~= nil then
|
||
arr[n] = str
|
||
end
|
||
return arr
|
||
end
|
||
|
||
function action_diag_connection()
|
||
local addr = luci.http.formvalue("addr")
|
||
if addr and (datatype.hostname(addr) or datatype.ipaddr(addr)) then
|
||
local cmd = string.format("/usr/share/openclash/openclash_debug_getcon.lua %s", addr)
|
||
luci.http.prepare_content("text/plain")
|
||
local util = io.popen(cmd)
|
||
if util and util ~= "" then
|
||
while true do
|
||
local ln = util:read("*l")
|
||
if not ln then break end
|
||
luci.http.write(ln)
|
||
luci.http.write("\n")
|
||
end
|
||
util:close()
|
||
end
|
||
return
|
||
end
|
||
luci.http.status(500, "Bad address")
|
||
end
|
||
|
||
function action_diag_dns()
|
||
local addr = luci.http.formvalue("addr")
|
||
if addr and datatype.hostname(addr)then
|
||
local cmd = string.format("/usr/share/openclash/openclash_debug_dns.lua %s", addr)
|
||
luci.http.prepare_content("text/plain")
|
||
local util = io.popen(cmd)
|
||
if util and util ~= "" then
|
||
while true do
|
||
local ln = util:read("*l")
|
||
if not ln then break end
|
||
luci.http.write(ln)
|
||
luci.http.write("\n")
|
||
end
|
||
util:close()
|
||
end
|
||
return
|
||
end
|
||
luci.http.status(500, "Bad address")
|
||
end
|
||
|
||
function action_gen_debug_logs()
|
||
local gen_log = luci.sys.call("/usr/share/openclash/openclash_debug.sh")
|
||
if not gen_log then return end
|
||
local logfile = "/tmp/openclash_debug.log"
|
||
if not fs.access(logfile) then
|
||
return
|
||
end
|
||
luci.http.prepare_content("text/plain; charset=utf-8")
|
||
local file=io.open(logfile, "r+")
|
||
file:seek("set")
|
||
local info = ""
|
||
for line in file:lines() do
|
||
if info ~= "" then
|
||
info = info.."\n"..line
|
||
else
|
||
info = line
|
||
end
|
||
end
|
||
file:close()
|
||
luci.http.write(info)
|
||
end
|
||
|
||
function action_backup()
|
||
local config = luci.sys.call("cp /etc/config/openclash /etc/openclash/openclash >/dev/null 2>&1")
|
||
local reader = ltn12_popen("tar -C '/etc/openclash/' -cz . 2>/dev/null")
|
||
|
||
luci.http.header(
|
||
'Content-Disposition', 'attachment; filename="Backup-OpenClash-%s-%s-%s.tar.gz"' %{
|
||
device_name, device_arh, os.date("%Y-%m-%d-%H-%M-%S")
|
||
})
|
||
|
||
luci.http.prepare_content("application/x-targz")
|
||
luci.ltn12.pump.all(reader, luci.http.write)
|
||
luci.sys.call("rm -rf /etc/openclash/openclash >/dev/null 2>&1")
|
||
end
|
||
|
||
function action_backup_ex_core()
|
||
local config = luci.sys.call("cp /etc/config/openclash /etc/openclash/openclash >/dev/null 2>&1")
|
||
local reader = ltn12_popen("echo 'core' > /tmp/oc_exclude.txt && tar -C '/etc/openclash/' -X '/tmp/oc_exclude.txt' -cz . 2>/dev/null")
|
||
|
||
luci.http.header(
|
||
'Content-Disposition', 'attachment; filename="Backup-OpenClash-Exclude-Cores-%s-%s-%s.tar.gz"' %{
|
||
device_name, device_arh, os.date("%Y-%m-%d-%H-%M-%S")
|
||
})
|
||
|
||
luci.http.prepare_content("application/x-targz")
|
||
luci.ltn12.pump.all(reader, luci.http.write)
|
||
luci.sys.call("rm -rf /etc/openclash/openclash >/dev/null 2>&1")
|
||
end
|
||
|
||
function action_backup_only_config()
|
||
local reader = ltn12_popen("tar -C '/etc/openclash' -cz './config' 2>/dev/null")
|
||
|
||
luci.http.header(
|
||
'Content-Disposition', 'attachment; filename="Backup-OpenClash-Config-%s-%s-%s.tar.gz"' %{
|
||
device_name, device_arh, os.date("%Y-%m-%d-%H-%M-%S")
|
||
})
|
||
|
||
luci.http.prepare_content("application/x-targz")
|
||
luci.ltn12.pump.all(reader, luci.http.write)
|
||
end
|
||
|
||
function action_backup_only_core()
|
||
local reader = ltn12_popen("tar -C '/etc/openclash' -cz './core' 2>/dev/null")
|
||
|
||
luci.http.header(
|
||
'Content-Disposition', 'attachment; filename="Backup-OpenClash-Cores-%s-%s-%s.tar.gz"' %{
|
||
device_name, device_arh, os.date("%Y-%m-%d-%H-%M-%S")
|
||
})
|
||
|
||
luci.http.prepare_content("application/x-targz")
|
||
luci.ltn12.pump.all(reader, luci.http.write)
|
||
end
|
||
|
||
function action_backup_only_rule()
|
||
local reader = ltn12_popen("tar -C '/etc/openclash' -cz './rule_provider' 2>/dev/null")
|
||
|
||
luci.http.header(
|
||
'Content-Disposition', 'attachment; filename="Backup-OpenClash-Only-Rule-Provider-%s-%s-%s.tar.gz"' %{
|
||
device_name, device_arh, os.date("%Y-%m-%d-%H-%M-%S")
|
||
})
|
||
|
||
luci.http.prepare_content("application/x-targz")
|
||
luci.ltn12.pump.all(reader, luci.http.write)
|
||
end
|
||
|
||
function action_backup_only_proxy()
|
||
local reader = ltn12_popen("tar -C '/etc/openclash' -cz './proxy_provider' 2>/dev/null")
|
||
|
||
luci.http.header(
|
||
'Content-Disposition', 'attachment; filename="Backup-OpenClash-Proxy-Provider-%s-%s-%s.tar.gz"' %{
|
||
device_name, device_arh, os.date("%Y-%m-%d-%H-%M-%S")
|
||
})
|
||
|
||
luci.http.prepare_content("application/x-targz")
|
||
luci.ltn12.pump.all(reader, luci.http.write)
|
||
end
|
||
|
||
function ltn12_popen(command)
|
||
|
||
local fdi, fdo = nixio.pipe()
|
||
local pid = nixio.fork()
|
||
|
||
if pid > 0 then
|
||
fdo:close()
|
||
local close
|
||
return function()
|
||
local buffer = fdi:read(2048)
|
||
local wpid, stat = nixio.waitpid(pid, "nohang")
|
||
if not close and wpid and stat == "exited" then
|
||
close = true
|
||
end
|
||
|
||
if buffer and #buffer > 0 then
|
||
return buffer
|
||
elseif close then
|
||
fdi:close()
|
||
return nil
|
||
end
|
||
end
|
||
elseif pid == 0 then
|
||
nixio.dup(fdo, nixio.stdout)
|
||
fdi:close()
|
||
fdo:close()
|
||
nixio.exec("/bin/sh", "-c", command)
|
||
end
|
||
end
|
||
|
||
function create_file()
|
||
local file_name = luci.http.formvalue("filename")
|
||
local file_path = luci.http.formvalue("filepath")..file_name
|
||
fs.writefile(file_path, "")
|
||
if not fs.isfile(file_path) then
|
||
luci.http.status(500, "Create File Faild")
|
||
end
|
||
return
|
||
end
|
||
|
||
function rename_file()
|
||
local new_file_name = luci.http.formvalue("new_file_name")
|
||
local file_path = luci.http.formvalue("file_path")
|
||
local old_file_name = luci.http.formvalue("file_name")
|
||
local old_file_path = file_path .. old_file_name
|
||
local new_file_path = file_path .. new_file_name
|
||
local old_run_file_path = "/etc/openclash/" .. old_file_name
|
||
local new_run_file_path = "/etc/openclash/" .. new_file_name
|
||
if fs.rename(old_file_path, new_file_path) then
|
||
if file_path == "/etc/openclash/config/" then
|
||
if fs.uci_get_config("config", "config_path") == old_file_path then
|
||
uci:set("openclash", "config", "config_path", new_file_path)
|
||
end
|
||
|
||
if fs.isfile(old_run_file_path) then
|
||
fs.rename(old_run_file_path, new_run_file_path)
|
||
end
|
||
|
||
uci:foreach("openclash", "config_subscribe",
|
||
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)
|
||
if s.config == old_file_name and fs.filename(new_file_name) ~= new_file_name then
|
||
uci:set("openclash", s[".name"], "config", new_file_name)
|
||
end
|
||
end)
|
||
|
||
uci:foreach("openclash", "proxy-provider",
|
||
function(s)
|
||
if s.config == old_file_name and fs.filename(new_file_name) ~= new_file_name then
|
||
uci:set("openclash", s[".name"], "config", new_file_name)
|
||
end
|
||
end)
|
||
|
||
uci:foreach("openclash", "servers",
|
||
function(s)
|
||
if s.config == old_file_name and fs.filename(new_file_name) ~= new_file_name then
|
||
uci:set("openclash", s[".name"], "config", new_file_name)
|
||
end
|
||
end)
|
||
|
||
uci:commit("openclash")
|
||
end
|
||
luci.http.status(200, "Rename File Successful")
|
||
else
|
||
luci.http.status(500, "Rename File Faild")
|
||
end
|
||
return
|
||
end
|
||
|
||
function manual_stream_unlock_test()
|
||
local type = luci.http.formvalue("type")
|
||
local cmd = string.format('/usr/share/openclash/openclash_streaming_unlock.lua "%s"', type)
|
||
luci.http.prepare_content("text/plain; charset=utf-8")
|
||
local util = io.popen(cmd)
|
||
if util and util ~= "" then
|
||
while true do
|
||
local ln = util:read("*l")
|
||
if ln then
|
||
luci.http.write(trans_line(ln))
|
||
luci.http.write("\n")
|
||
end
|
||
if not process_status("openclash_streaming_unlock.lua "..type) or not process_status("openclash_streaming_unlock.lua ") then
|
||
break
|
||
end
|
||
end
|
||
util:close()
|
||
return
|
||
end
|
||
luci.http.status(500, "Something Wrong While Testing...")
|
||
end
|
||
|
||
function all_proxies_stream_test()
|
||
local type = luci.http.formvalue("type")
|
||
local cmd = string.format('/usr/share/openclash/openclash_streaming_unlock.lua "%s" "%s"', type, "all")
|
||
luci.http.prepare_content("text/plain; charset=utf-8")
|
||
local util = io.popen(cmd)
|
||
if util and util ~= "" then
|
||
while true do
|
||
local ln = util:read("*l")
|
||
if ln then
|
||
luci.http.write(trans_line(ln))
|
||
luci.http.write("\n")
|
||
end
|
||
if not process_status("openclash_streaming_unlock.lua "..type) or not process_status("openclash_streaming_unlock.lua ") then
|
||
break
|
||
end
|
||
end
|
||
util:close()
|
||
return
|
||
end
|
||
luci.http.status(500, "Something Wrong While Testing...")
|
||
end
|
||
|
||
function trans_line(data)
|
||
if data == nil or data == "" then
|
||
return ""
|
||
end
|
||
|
||
local line_trans = ""
|
||
|
||
local has_timestamp = string.len(data) >= 19 and string.match(string.sub(data, 1, 19), "%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d")
|
||
local time_part = ""
|
||
local level_part = ""
|
||
local content_start = has_timestamp and 21 or 1
|
||
|
||
if has_timestamp then
|
||
time_part = string.sub(data, 1, 20)
|
||
local level_start, level_end, level_content = string.find(data, "%[([^%]]+)%]", 21)
|
||
if level_start and level_end and level_start == 21 then
|
||
level_part = "[" .. luci.i18n.translate(level_content) .. "] "
|
||
content_start = level_end + 2
|
||
end
|
||
end
|
||
|
||
local segments = {}
|
||
local last_pos = content_start
|
||
local pos = string.find(data, "【", content_start)
|
||
|
||
while pos do
|
||
if pos > last_pos then
|
||
table.insert(segments, {
|
||
type = "trans",
|
||
text = string.sub(data, last_pos, pos - 1)
|
||
})
|
||
end
|
||
|
||
local close_pos = string.find(data, "】", pos + 1)
|
||
if not close_pos then
|
||
table.insert(segments, {
|
||
type = "trans",
|
||
text = string.sub(data, pos, -1)
|
||
})
|
||
break
|
||
end
|
||
|
||
table.insert(segments, {
|
||
type = "no_trans",
|
||
text = string.sub(data, pos, close_pos + 2)
|
||
})
|
||
|
||
last_pos = close_pos + 3
|
||
pos = string.find(data, "【", last_pos)
|
||
end
|
||
|
||
if last_pos <= string.len(data) then
|
||
table.insert(segments, {
|
||
type = "trans",
|
||
text = string.sub(data, last_pos, -1)
|
||
})
|
||
end
|
||
|
||
line_trans = time_part .. level_part
|
||
for _, seg in ipairs(segments) do
|
||
if seg.type == "trans" then
|
||
line_trans = line_trans .. luci.i18n.translate(seg.text)
|
||
else
|
||
line_trans = line_trans .. seg.text
|
||
end
|
||
end
|
||
|
||
return line_trans
|
||
end
|
||
|
||
function process_status(name)
|
||
local ps_version = luci.sys.exec("ps --version 2>&1 |grep -c procps-ng |tr -d '\n'")
|
||
local cmd
|
||
if ps_version == "1" then
|
||
cmd = string.format("ps -efw |grep '%s' |grep -v grep", name)
|
||
else
|
||
cmd = string.format("ps -w |grep '%s' |grep -v grep", name)
|
||
end
|
||
local result = luci.sys.exec(cmd)
|
||
return result ~= nil and result ~= "" and not result:match("^%s*$")
|
||
end
|
||
|
||
function action_announcement()
|
||
if not fs.access("/tmp/openclash_announcement") or fs.readfile("/tmp/openclash_announcement") == "" or fs.mtime("/tmp/openclash_announcement") < (os.time() - 86400) then
|
||
local HTTP_CODE = luci.sys.exec("curl -SsL -m 5 -w '%{http_code}' -o /tmp/openclash_announcement https://raw.githubusercontent.com/vernesong/OpenClash/dev/announcement 2>/dev/null")
|
||
if HTTP_CODE ~= "200" then
|
||
fs.unlink("/tmp/openclash_announcement")
|
||
end
|
||
end
|
||
local info = luci.sys.exec("cat /tmp/openclash_announcement 2>/dev/null") or ""
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
content = info;
|
||
})
|
||
end
|
||
|
||
function action_myip_check()
|
||
local result = {}
|
||
local random = math.random(100000000)
|
||
|
||
local services = {
|
||
{
|
||
name = "upaiyun",
|
||
url = string.format("https://pubstatic.b0.upaiyun.com/?_upnode&z=%d", random),
|
||
parser = function(data)
|
||
if data and data ~= "" then
|
||
local ok, upaiyun_json = pcall(json.parse, data)
|
||
if ok and upaiyun_json and upaiyun_json.remote_addr then
|
||
local geo_parts = {}
|
||
if upaiyun_json.remote_addr_location then
|
||
if upaiyun_json.remote_addr_location.country and upaiyun_json.remote_addr_location.country ~= "" then
|
||
table.insert(geo_parts, upaiyun_json.remote_addr_location.country)
|
||
end
|
||
if upaiyun_json.remote_addr_location.province and upaiyun_json.remote_addr_location.province ~= "" then
|
||
table.insert(geo_parts, upaiyun_json.remote_addr_location.province)
|
||
end
|
||
if upaiyun_json.remote_addr_location.city and upaiyun_json.remote_addr_location.city ~= "" then
|
||
table.insert(geo_parts, upaiyun_json.remote_addr_location.city)
|
||
end
|
||
if upaiyun_json.remote_addr_location.isp and upaiyun_json.remote_addr_location.isp ~= "" then
|
||
table.insert(geo_parts, upaiyun_json.remote_addr_location.isp)
|
||
end
|
||
end
|
||
|
||
return {
|
||
ip = upaiyun_json.remote_addr,
|
||
geo = table.concat(geo_parts, " ")
|
||
}
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
},
|
||
{
|
||
name = "ipip",
|
||
url = string.format("http://myip.ipip.net?z=%d", random),
|
||
parser = function(data)
|
||
if data and data ~= "" then
|
||
local ip = string.match(data, "当前 IP:([%d%.]+)")
|
||
local geo = string.match(data, "来自于:(.+)")
|
||
|
||
if ip and geo then
|
||
geo = string.gsub(geo, "%s+", " ")
|
||
geo = string.gsub(geo, "^%s*(.-)%s*$", "%1")
|
||
|
||
return {
|
||
ip = ip,
|
||
geo = geo
|
||
}
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
},
|
||
{
|
||
name = "ipsb",
|
||
url = string.format("https://api-ipv4.ip.sb/geoip?z=%d", random),
|
||
parser = function(data)
|
||
if data and data ~= "" then
|
||
local ok, ipsb_json = pcall(json.parse, data)
|
||
if ok and ipsb_json and ipsb_json.ip then
|
||
local geo_parts = {}
|
||
if ipsb_json.country and ipsb_json.country ~= "" then
|
||
table.insert(geo_parts, ipsb_json.country)
|
||
end
|
||
if ipsb_json.isp and ipsb_json.isp ~= "" then
|
||
table.insert(geo_parts, ipsb_json.isp)
|
||
end
|
||
|
||
return {
|
||
ip = ipsb_json.ip,
|
||
geo = table.concat(geo_parts, " ")
|
||
}
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
},
|
||
{
|
||
name = "ipify",
|
||
url = string.format("https://api.ipify.org/?format=json&z=%d", random),
|
||
parser = function(data)
|
||
if data and data ~= "" then
|
||
local ok, ipify_json = pcall(json.parse, data)
|
||
if ok and ipify_json and ipify_json.ip then
|
||
return {
|
||
ip = ipify_json.ip,
|
||
geo = ""
|
||
}
|
||
end
|
||
end
|
||
return nil
|
||
end
|
||
}
|
||
}
|
||
|
||
local function create_concurrent_query(service)
|
||
local fdi, fdo = nixio.pipe()
|
||
if not fdi or not fdo then
|
||
return nil
|
||
end
|
||
|
||
local pid = nixio.fork()
|
||
|
||
if pid > 0 then
|
||
fdo:close()
|
||
return {
|
||
pid = pid,
|
||
service_name = service.name,
|
||
fdi = fdi,
|
||
closed = false,
|
||
reader = function()
|
||
local buffer = fdi:read(4096)
|
||
if buffer and #buffer > 0 then
|
||
return buffer
|
||
else
|
||
return nil
|
||
end
|
||
end,
|
||
close = function()
|
||
if fdi and not fdi.closed then
|
||
pcall(fdi.close, fdi)
|
||
fdi.closed = true
|
||
end
|
||
end
|
||
}
|
||
elseif pid == 0 then
|
||
nixio.dup(fdo, nixio.stdout)
|
||
fdi:close()
|
||
fdo:close()
|
||
|
||
local cmd = string.format(
|
||
'curl -SsL -m 5 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" "%s" 2>/dev/null',
|
||
service.url
|
||
)
|
||
nixio.exec("/bin/sh", "-c", cmd)
|
||
else
|
||
if fdi then fdi:close() end
|
||
if fdo then fdo:close() end
|
||
return nil
|
||
end
|
||
end
|
||
|
||
local queries = {}
|
||
|
||
for _, service in ipairs(services) do
|
||
local query = create_concurrent_query(service)
|
||
if query then
|
||
queries[service.name] = {
|
||
query = query,
|
||
parser = service.parser,
|
||
data = ""
|
||
}
|
||
end
|
||
end
|
||
|
||
if next(queries) == nil then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
error = "Failed to create any queries"
|
||
})
|
||
return
|
||
end
|
||
|
||
local max_iterations = 140
|
||
local iteration = 0
|
||
local completed = {}
|
||
|
||
while iteration < max_iterations do
|
||
iteration = iteration + 1
|
||
|
||
for name, info in pairs(queries) do
|
||
if not completed[name] then
|
||
local wpid, stat = nixio.waitpid(info.query.pid, "nohang")
|
||
local buffer = info.query.reader()
|
||
|
||
if buffer then
|
||
info.data = info.data .. buffer
|
||
end
|
||
|
||
if wpid then
|
||
pcall(info.query.close)
|
||
completed[name] = true
|
||
|
||
local parsed_result = info.parser(info.data)
|
||
if parsed_result then
|
||
result[name] = parsed_result
|
||
end
|
||
|
||
queries[name] = nil
|
||
else
|
||
local still_running = luci.sys.call(string.format("kill -0 %d 2>/dev/null", info.query.pid)) == 0
|
||
if not still_running then
|
||
pcall(info.query.close)
|
||
completed[name] = true
|
||
|
||
local parsed_result = info.parser(info.data)
|
||
if parsed_result then
|
||
result[name] = parsed_result
|
||
end
|
||
|
||
queries[name] = nil
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
local remaining_count = 0
|
||
for _ in pairs(queries) do
|
||
remaining_count = remaining_count + 1
|
||
end
|
||
|
||
if remaining_count == 0 then
|
||
break
|
||
end
|
||
|
||
nixio.nanosleep(0, 50000000)
|
||
end
|
||
|
||
for name, info in pairs(queries) do
|
||
if not completed[name] then
|
||
result[name] = { ip = "", geo = "", error = "timeout" }
|
||
pcall(nixio.kill, info.query.pid, nixio.const.SIGTERM)
|
||
pcall(nixio.waitpid, info.query.pid, 0)
|
||
pcall(info.query.close)
|
||
end
|
||
end
|
||
|
||
if result.ipify and result.ipify.ip then
|
||
local geo_cmd = string.format(
|
||
'curl -sL -m 5 --retry 2 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" "https://api-ipv4.ip.sb/geoip/%s" 2>/dev/null',
|
||
result.ipify.ip
|
||
)
|
||
local geo_data = luci.sys.exec(geo_cmd)
|
||
|
||
if geo_data and geo_data ~= "" then
|
||
local ok_geo, geo_json = pcall(json.parse, geo_data)
|
||
if ok_geo and geo_json and geo_json.ip then
|
||
local geo_parts = {}
|
||
if geo_json.country and geo_json.country ~= "" then
|
||
table.insert(geo_parts, geo_json.country)
|
||
end
|
||
if geo_json.isp and geo_json.isp ~= "" then
|
||
table.insert(geo_parts, geo_json.isp)
|
||
end
|
||
result.ipify.geo = table.concat(geo_parts, " ")
|
||
end
|
||
end
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json(result)
|
||
end
|
||
|
||
function action_website_check()
|
||
local domain = luci.http.formvalue("domain")
|
||
local result = {
|
||
success = false,
|
||
response_time = 0,
|
||
error = ""
|
||
}
|
||
|
||
if not domain then
|
||
result.error = "Missing domain parameter"
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json(result)
|
||
return
|
||
end
|
||
|
||
local test_domain = domain
|
||
local test_url
|
||
|
||
if test_domain:match("^https?://") then
|
||
test_domain = test_domain: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"
|
||
else
|
||
test_url = "https://" .. test_domain .. "/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
|
||
)
|
||
|
||
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)
|
||
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"
|
||
else
|
||
fallback_url = "https://" .. test_domain .. "/"
|
||
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
|
||
else
|
||
result.success = false
|
||
result.error = "Connection failed"
|
||
end
|
||
end
|
||
else
|
||
result.success = false
|
||
result.error = "Invalid response"
|
||
end
|
||
else
|
||
result.success = false
|
||
result.error = "No response"
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json(result)
|
||
end
|
||
|
||
function action_proxy_info()
|
||
local result = {
|
||
mixed_port = "",
|
||
auth_user = "",
|
||
auth_pass = ""
|
||
}
|
||
|
||
|
||
local mixed_port = fs.uci_get_config("config", "mixed_port")
|
||
if mixed_port and mixed_port ~= "" then
|
||
result.mixed_port = mixed_port
|
||
else
|
||
result.mixed_port = "7893"
|
||
end
|
||
|
||
uci:foreach("openclash", "authentication", function(section)
|
||
if section.enabled == "1" and result.auth_user == "" then
|
||
if section.username and section.username ~= "" then
|
||
result.auth_user = section.username
|
||
end
|
||
if section.password and section.password ~= "" then
|
||
result.auth_pass = section.password
|
||
end
|
||
return false
|
||
end
|
||
end)
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json(result)
|
||
end
|
||
|
||
function action_oc_settings()
|
||
local result = {
|
||
meta_sniffer = "0",
|
||
respect_rules = "0",
|
||
oversea = "0",
|
||
stream_unlock = "0"
|
||
}
|
||
|
||
local meta_sniffer = fs.uci_get_config("config", "enable_meta_sniffer")
|
||
if meta_sniffer == "1" then
|
||
result.meta_sniffer = "1"
|
||
end
|
||
|
||
local respect_rules = fs.uci_get_config("config", "enable_respect_rules")
|
||
if respect_rules == "1" then
|
||
result.respect_rules = "1"
|
||
end
|
||
|
||
local oversea = fs.uci_get_config("config", "china_ip_route")
|
||
if oversea == "1" then
|
||
result.oversea = "1"
|
||
elseif oversea == "2" then
|
||
result.oversea = "2"
|
||
else
|
||
result.oversea = "0"
|
||
end
|
||
|
||
local stream_unlock = fs.uci_get_config("config", "stream_auto_select")
|
||
if stream_unlock == "1" then
|
||
result.stream_unlock = "1"
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json(result)
|
||
end
|
||
|
||
function action_switch_oc_setting()
|
||
local setting = luci.http.formvalue("setting")
|
||
local value = luci.http.formvalue("value")
|
||
|
||
if not setting or not value then
|
||
luci.http.status(400, "Missing parameters")
|
||
return
|
||
end
|
||
|
||
local function get_runtime_config_path()
|
||
local config_path = fs.uci_get_config("config", "config_path")
|
||
if not config_path then
|
||
return nil
|
||
end
|
||
local config_filename = fs.basename(config_path)
|
||
return "/etc/openclash/" .. config_filename
|
||
end
|
||
|
||
local function update_runtime_config(ruby_cmd)
|
||
local runtime_config_path = get_runtime_config_path()
|
||
if not runtime_config_path then
|
||
luci.http.status(500, "No config path found")
|
||
return false
|
||
end
|
||
|
||
local ruby_result = luci.sys.call(ruby_cmd)
|
||
if ruby_result ~= 0 then
|
||
luci.http.status(500, "Failed to modify config file")
|
||
return false
|
||
end
|
||
|
||
local daip = daip()
|
||
local dase = dase() or ""
|
||
local cn_port = cn_port()
|
||
if not daip or not cn_port then
|
||
luci.http.status(500, "Switch Failed")
|
||
return false
|
||
end
|
||
|
||
local reload_result = luci.sys.exec(string.format('curl -sL -m 5 --connect-timeout 2 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPUT http://"%s":"%s"/configs?force=true -d \'{"path":"%s"}\' 2>&1', dase, daip, cn_port, runtime_config_path))
|
||
|
||
if reload_result ~= "" then
|
||
luci.http.status(500, "Switch Failed")
|
||
return false
|
||
end
|
||
|
||
return true
|
||
end
|
||
|
||
if setting == "meta_sniffer" then
|
||
if is_running() then
|
||
local runtime_config_path = get_runtime_config_path()
|
||
local ruby_cmd
|
||
|
||
if value == "1" then
|
||
ruby_cmd = string.format([[
|
||
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
|
||
begin
|
||
config_path = '%s'
|
||
|
||
config = File.exist?(config_path) ? YAML.load_file(config_path) : {}
|
||
config ||= {}
|
||
|
||
if config['sniffer']&.dig('enable') == true &&
|
||
config['sniffer']&.dig('parse-pure-ip') == true &&
|
||
config['sniffer']&.dig('sniff')
|
||
exit 0
|
||
end
|
||
|
||
config['sniffer'] = {
|
||
'enable' => true,
|
||
'parse-pure-ip' => true,
|
||
'override-destination' => false
|
||
}
|
||
|
||
custom_sniffer_path = '/etc/openclash/custom/openclash_custom_sniffer.yaml'
|
||
if File.exist?(custom_sniffer_path)
|
||
begin
|
||
custom_sniffer = YAML.load_file(custom_sniffer_path)
|
||
if custom_sniffer&.dig('sniffer')
|
||
config['sniffer'].merge!(custom_sniffer['sniffer'])
|
||
end
|
||
rescue
|
||
end
|
||
end
|
||
|
||
unless config['sniffer']['sniff']
|
||
config['sniffer']['sniff'] = {
|
||
'QUIC' => { 'ports' => [443] },
|
||
'TLS' => { 'ports' => [443, '8443'] },
|
||
'HTTP' => { 'ports' => [80, '8080-8880'], 'override-destination' => true }
|
||
}
|
||
end
|
||
|
||
unless config['sniffer']['force-domain']
|
||
config['sniffer']['force-domain'] = ['+.netflix.com', '+.nflxvideo.net', '+.amazonaws.com']
|
||
end
|
||
|
||
unless config['sniffer']['skip-domain']
|
||
config['sniffer']['skip-domain'] = ['+.apple.com', 'Mijia Cloud', 'dlg.io.mi.com']
|
||
end
|
||
|
||
temp_path = config_path + '.tmp'
|
||
File.open(temp_path, 'w') { |f| YAML.dump(config, f) }
|
||
File.rename(temp_path, config_path)
|
||
|
||
rescue => e
|
||
File.unlink(temp_path) if File.exist?(temp_path)
|
||
exit 1
|
||
end
|
||
" 2>/dev/null
|
||
]], runtime_config_path)
|
||
else
|
||
ruby_cmd = string.format([[
|
||
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
|
||
begin
|
||
config_path = '%s'
|
||
|
||
if File.exist?(config_path)
|
||
config = YAML.load_file(config_path)
|
||
if config&.dig('sniffer', 'enable') == false
|
||
exit 0
|
||
end
|
||
else
|
||
config = {}
|
||
end
|
||
|
||
config ||= {}
|
||
config['sniffer'] = { 'enable' => false }
|
||
|
||
temp_path = config_path + '.tmp'
|
||
File.open(temp_path, 'w') { |f| YAML.dump(config, f) }
|
||
File.rename(temp_path, config_path)
|
||
|
||
rescue => e
|
||
File.unlink(temp_path) if File.exist?(temp_path)
|
||
exit 1
|
||
end
|
||
" 2>/dev/null
|
||
]], runtime_config_path)
|
||
end
|
||
|
||
if not update_runtime_config(ruby_cmd) then
|
||
return
|
||
end
|
||
end
|
||
uci:set("openclash", "config", "enable_meta_sniffer", tonumber(value))
|
||
uci:set("openclash", "config", "enable_meta_sniffer_pure_ip", tonumber(value))
|
||
uci:set("openclash", "@overwrite[0]", "enable_meta_sniffer", tonumber(value))
|
||
uci:set("openclash", "@overwrite[0]", "enable_meta_sniffer_pure_ip", tonumber(value))
|
||
uci:commit("openclash")
|
||
elseif setting == "respect_rules" then
|
||
if is_running() then
|
||
local runtime_config_path = get_runtime_config_path()
|
||
local target_value = (value == "1") and "true" or "false"
|
||
|
||
local ruby_cmd = string.format([[
|
||
ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e "
|
||
begin
|
||
config_path = '%s'
|
||
target_value = %s
|
||
|
||
if File.exist?(config_path)
|
||
config = YAML.load_file(config_path)
|
||
if config&.dig('dns', 'respect-rules') == target_value
|
||
if target_value == true && (!config&.dig('dns', 'proxy-server-nameserver') || config['dns']['proxy-server-nameserver'].empty?)
|
||
else
|
||
exit 0
|
||
end
|
||
end
|
||
else
|
||
config = {}
|
||
end
|
||
|
||
config ||= {}
|
||
config['dns'] ||= {}
|
||
config['dns']['respect-rules'] = target_value
|
||
|
||
if target_value == true
|
||
if !config['dns']['proxy-server-nameserver'] || config['dns']['proxy-server-nameserver'].empty?
|
||
config['dns']['proxy-server-nameserver'] = ['114.114.114.114', '119.29.29.29', '8.8.8.8', '1.1.1.1']
|
||
end
|
||
end
|
||
|
||
temp_path = config_path + '.tmp'
|
||
File.open(temp_path, 'w') { |f| YAML.dump(config, f) }
|
||
File.rename(temp_path, config_path)
|
||
|
||
rescue => e
|
||
File.unlink(temp_path) if File.exist?(temp_path)
|
||
exit 1
|
||
end
|
||
" 2>/dev/null
|
||
]], runtime_config_path, target_value)
|
||
|
||
if not update_runtime_config(ruby_cmd) then
|
||
return
|
||
end
|
||
end
|
||
uci:set("openclash", "config", "enable_respect_rules", tonumber(value))
|
||
uci:set("openclash", "@overwrite[0]", "enable_respect_rules", tonumber(value))
|
||
uci:commit("openclash")
|
||
elseif setting == "oversea" then
|
||
uci:set("openclash", "config", "china_ip_route", value)
|
||
uci:commit("openclash")
|
||
if is_running() then
|
||
uci:set("openclash", "@overwrite[0]", "china_ip_route", value)
|
||
uci:commit("openclash")
|
||
luci.sys.exec("/etc/init.d/openclash restart >/dev/null 2>&1 &")
|
||
end
|
||
elseif setting == "stream_unlock" then
|
||
uci:set("openclash", "config", "stream_auto_select", value)
|
||
if not fs.uci_get_config("config", "stream_auto_select_interval") then
|
||
uci:set("openclash", "config", "stream_auto_select_interval", "10")
|
||
end
|
||
if not fs.uci_get_config("config", "stream_auto_select_logic") then
|
||
uci:set("openclash", "config", "stream_auto_select_logic", "Urltest")
|
||
end
|
||
if not fs.uci_get_config("config", "stream_auto_select_expand_group") then
|
||
uci:set("openclash", "config", "stream_auto_select_expand_group", "0")
|
||
end
|
||
|
||
uci:set("openclash", "config", "stream_auto_select_netflix", "1")
|
||
if not fs.uci_get_config("config", "stream_auto_select_group_key_netflix") then
|
||
uci:set("openclash", "config", "stream_auto_select_group_key_netflix", "Netflix|奈飞")
|
||
end
|
||
|
||
uci:set("openclash", "config", "stream_auto_select_disney", "1")
|
||
if not fs.uci_get_config("config", "stream_auto_select_group_key_disney") then
|
||
uci:set("openclash", "config", "stream_auto_select_group_key_disney", "Disney|迪士尼")
|
||
end
|
||
|
||
uci:set("openclash", "config", "stream_auto_select_hbo_max", "1")
|
||
if not fs.uci_get_config("config", "stream_auto_select_group_key_hbo_max") then
|
||
uci:set("openclash", "config", "stream_auto_select_group_key_hbo_max", "HBO|HBO Max")
|
||
end
|
||
uci:commit("openclash")
|
||
else
|
||
luci.http.status(400, "Invalid setting")
|
||
return
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "success",
|
||
setting = setting,
|
||
value = value
|
||
})
|
||
end
|
||
|
||
function action_generate_pac()
|
||
local result = {
|
||
pac_url = "",
|
||
error = ""
|
||
}
|
||
|
||
local auth_user = ""
|
||
local auth_pass = ""
|
||
|
||
uci:foreach("openclash", "authentication", function(section)
|
||
if section.enabled == "1" and section.username and section.username ~= ""
|
||
and section.password and section.password ~= "" then
|
||
auth_user = section.username
|
||
auth_pass = section.password
|
||
return false
|
||
end
|
||
end)
|
||
|
||
local proxy_ip = daip()
|
||
local mixed_port = fs.uci_get_config("config", "mixed_port") or "7893"
|
||
|
||
if not proxy_ip then
|
||
result.error = luci.i18n.translate("Unable to get proxy IP")
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json(result)
|
||
return
|
||
end
|
||
|
||
local function generate_random_string()
|
||
local random_cmd = "tr -cd 'a-zA-Z0-9' </dev/urandom 2>/dev/null| head -c16 || date +%N| md5sum |head -c16"
|
||
local random_string = luci.sys.exec(random_cmd):gsub("\n", "")
|
||
return random_string
|
||
end
|
||
|
||
local function count_pac_lines(content)
|
||
if not content or content == "" then
|
||
return 0
|
||
end
|
||
local lines = 0
|
||
for _ in content:gmatch("[^\n]*\n?") do
|
||
lines = lines + 1
|
||
end
|
||
if not content:match("\n$") then
|
||
lines = lines - 1
|
||
end
|
||
return lines
|
||
end
|
||
|
||
local new_proxy_string = string.format("PROXY %s:%s; DIRECT", proxy_ip, mixed_port)
|
||
local new_pac_content = generate_pac_content(proxy_ip, mixed_port, auth_user, auth_pass)
|
||
local new_pac_lines = count_pac_lines(new_pac_content)
|
||
|
||
local pac_dir = "/www/luci-static/resources/openclash/pac/"
|
||
local pac_filename = nil
|
||
local pac_file_path = nil
|
||
local random_suffix = nil
|
||
local need_update = true
|
||
|
||
luci.sys.call("mkdir -p " .. pac_dir)
|
||
|
||
local find_cmd = "find " .. pac_dir .. " -name 'pac_*' -type f 2>/dev/null"
|
||
local existing_files = luci.sys.exec(find_cmd)
|
||
if existing_files and existing_files ~= "" then
|
||
for file_path in existing_files:gmatch("[^\n]+") do
|
||
if fs.access(file_path) then
|
||
local file_content = fs.readfile(file_path)
|
||
if file_content then
|
||
local existing_proxy = string.match(file_content, 'return%s+"(PROXY%s+[^"]*)"')
|
||
if not existing_proxy then
|
||
existing_proxy = string.match(file_content, 'return%s*"(PROXY%s+[^"]*)"')
|
||
end
|
||
|
||
if existing_proxy and existing_proxy == new_proxy_string then
|
||
local existing_lines = count_pac_lines(file_content)
|
||
if existing_lines == new_pac_lines then
|
||
pac_filename = file_path:match("([^/]+)$")
|
||
pac_file_path = file_path
|
||
random_suffix = pac_filename:match("^pac_(.+)$")
|
||
need_update = false
|
||
break
|
||
else
|
||
local file = io.open(file_path, "w")
|
||
if file then
|
||
file:write(new_pac_content)
|
||
file:close()
|
||
luci.sys.call("chmod 644 " .. file_path)
|
||
|
||
pac_filename = file_path:match("([^/]+)$")
|
||
pac_file_path = file_path
|
||
random_suffix = pac_filename:match("^pac_(.+)$")
|
||
need_update = false
|
||
break
|
||
end
|
||
end
|
||
elseif existing_proxy and string.find(existing_proxy, "^PROXY%s+[%d%.]+:[%d]+") then
|
||
local updated_content = string.gsub(file_content,
|
||
'return%s*"PROXY%s+[^"]*"',
|
||
'return "' .. new_proxy_string .. '"')
|
||
|
||
if updated_content ~= file_content then
|
||
local updated_lines = count_pac_lines(updated_content)
|
||
local final_content
|
||
|
||
if updated_lines == new_pac_lines then
|
||
final_content = updated_content
|
||
else
|
||
final_content = new_pac_content
|
||
end
|
||
|
||
local file = io.open(file_path, "w")
|
||
if file then
|
||
file:write(final_content)
|
||
file:close()
|
||
luci.sys.call("chmod 644 " .. file_path)
|
||
|
||
pac_filename = file_path:match("([^/]+)$")
|
||
pac_file_path = file_path
|
||
random_suffix = pac_filename:match("^pac_(.+)$")
|
||
need_update = false
|
||
break
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
if need_update then
|
||
luci.sys.call("rm -f " .. pac_dir .. "pac_* 2>/dev/null")
|
||
|
||
random_suffix = generate_random_string()
|
||
pac_filename = "pac_" .. random_suffix
|
||
pac_file_path = pac_dir .. pac_filename
|
||
|
||
local file = io.open(pac_file_path, "w")
|
||
if file then
|
||
file:write(new_pac_content)
|
||
file:close()
|
||
|
||
luci.sys.call("chmod 644 " .. pac_file_path)
|
||
else
|
||
result.error = luci.i18n.translate("Failed to write PAC file")
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json(result)
|
||
return
|
||
end
|
||
else
|
||
luci.sys.call(string.format("find %s -name 'pac_*' -type f ! -name '%s' -delete 2>/dev/null", pac_dir, pac_filename))
|
||
end
|
||
|
||
local pac_url = generate_pac_url_with_client_info(pac_filename, random_suffix)
|
||
result.pac_url = pac_url
|
||
|
||
if not auth_exists then
|
||
result.error = luci.i18n.translate("No authentication configured, please be aware of the risk of information leakage!")
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json(result)
|
||
end
|
||
|
||
function generate_pac_url_with_client_info(pac_filename, random_suffix)
|
||
local client_protocol = luci.http.formvalue("client_protocol")
|
||
local client_hostname = luci.http.formvalue("client_hostname")
|
||
local client_host = luci.http.formvalue("client_host")
|
||
local client_port = luci.http.formvalue("client_port")
|
||
|
||
local request_scheme = "http"
|
||
local host = "localhost"
|
||
|
||
if client_protocol and (client_protocol == "http" or client_protocol == "https") then
|
||
request_scheme = client_protocol
|
||
else
|
||
if luci.http.getenv("HTTPS") == "on" or
|
||
luci.http.getenv("HTTP_X_FORWARDED_PROTO") == "https" or
|
||
luci.http.getenv("REQUEST_SCHEME") == "https" then
|
||
request_scheme = "https"
|
||
end
|
||
end
|
||
|
||
if client_host and client_host ~= "" then
|
||
host = client_host
|
||
elseif client_hostname and client_hostname ~= "" then
|
||
host = client_hostname
|
||
if client_port and client_port ~= "" then
|
||
if (request_scheme == "http" and client_port ~= "80") or
|
||
(request_scheme == "https" and client_port ~= "443") then
|
||
host = host .. ":" .. client_port
|
||
end
|
||
end
|
||
else
|
||
local server_name = luci.http.getenv("SERVER_NAME")
|
||
local http_host = luci.http.getenv("HTTP_HOST")
|
||
local server_port = luci.http.getenv("SERVER_PORT")
|
||
local proxy_ip = daip()
|
||
|
||
if http_host and http_host ~= "" then
|
||
host = http_host
|
||
elseif server_name and server_name ~= "" then
|
||
host = server_name
|
||
if server_port and server_port ~= "" then
|
||
if (request_scheme == "http" and server_port ~= "80") or
|
||
(request_scheme == "https" and server_port ~= "443") then
|
||
host = host .. ":" .. server_port
|
||
end
|
||
end
|
||
elseif proxy_ip and proxy_ip ~= "" then
|
||
host = proxy_ip
|
||
if server_port and server_port ~= "" then
|
||
if (request_scheme == "http" and server_port ~= "80") or
|
||
(request_scheme == "https" and server_port ~= "443") then
|
||
host = host .. ":" .. server_port
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
local random_param = ""
|
||
if random_suffix and #random_suffix >= 8 then
|
||
math.randomseed(os.time())
|
||
for i = 1, 8 do
|
||
local pos = math.random(1, #random_suffix)
|
||
random_param = random_param .. string.sub(random_suffix, pos, pos)
|
||
end
|
||
else
|
||
random_param = random_suffix or tostring(os.time())
|
||
end
|
||
|
||
local pac_url = request_scheme .. "://" .. host .. "/luci-static/resources/openclash/pac/" .. pac_filename .. "?v=" .. random_param
|
||
|
||
return pac_url
|
||
end
|
||
|
||
function generate_pac_content(proxy_ip, proxy_port, auth_user, auth_pass)
|
||
local proxy_string = string.format("PROXY %s:%s; DIRECT", proxy_ip, proxy_port)
|
||
|
||
local ipv4_networks = {}
|
||
local ipv4_file = "/etc/openclash/custom/openclash_custom_localnetwork_ipv4.list"
|
||
if fs.access(ipv4_file) then
|
||
local content = fs.readfile(ipv4_file)
|
||
if content then
|
||
for line in content:gmatch("[^\r\n]+") do
|
||
line = line:match("^%s*(.-)%s*$")
|
||
if line and line ~= "" and not line:match("^//") and not line:match("^#") then
|
||
local network, mask = line:match("([%d%.]+)/(%d+)")
|
||
if network and mask then
|
||
local mask_bits = tonumber(mask)
|
||
if mask_bits and mask_bits >= 0 and mask_bits <= 32 then
|
||
local subnet_masks = {
|
||
[0] = "0.0.0.0", [1] = "128.0.0.0", [2] = "192.0.0.0", [3] = "224.0.0.0",
|
||
[4] = "240.0.0.0", [5] = "248.0.0.0", [6] = "252.0.0.0", [7] = "254.0.0.0",
|
||
[8] = "255.0.0.0", [9] = "255.128.0.0", [10] = "255.192.0.0", [11] = "255.224.0.0",
|
||
[12] = "255.240.0.0", [13] = "255.248.0.0", [14] = "255.252.0.0", [15] = "255.254.0.0",
|
||
[16] = "255.255.0.0", [17] = "255.255.128.0", [18] = "255.255.192.0", [19] = "255.255.224.0",
|
||
[20] = "255.255.240.0", [21] = "255.255.248.0", [22] = "255.255.252.0", [23] = "255.255.254.0",
|
||
[24] = "255.255.255.0", [25] = "255.255.255.128", [26] = "255.255.255.192", [27] = "255.255.255.224",
|
||
[28] = "255.255.255.240", [29] = "255.255.255.248", [30] = "255.255.255.252", [31] = "255.255.255.254",
|
||
[32] = "255.255.255.255"
|
||
}
|
||
local subnet_mask = subnet_masks[mask_bits]
|
||
if subnet_mask then
|
||
table.insert(ipv4_networks, {network = network, mask = subnet_mask})
|
||
end
|
||
end
|
||
else
|
||
local single_ip = line:match("^([%d%.]+)$")
|
||
if single_ip and single_ip:match("^%d+%.%d+%.%d+%.%d+$") then
|
||
table.insert(ipv4_networks, {network = single_ip, mask = "255.255.255.255"})
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
local ipv6_networks = {}
|
||
local ipv6_file = "/etc/openclash/custom/openclash_custom_localnetwork_ipv6.list"
|
||
if fs.access(ipv6_file) then
|
||
local content = fs.readfile(ipv6_file)
|
||
if content then
|
||
for line in content:gmatch("[^\r\n]+") do
|
||
line = line:match("^%s*(.-)%s*$")
|
||
if line and line ~= "" and not line:match("^//") and not line:match("^#") then
|
||
local prefix, prefix_len = line:match("([:%da-fA-F]+)/(%d+)")
|
||
if prefix and prefix_len then
|
||
table.insert(ipv6_networks, {prefix = prefix, prefix_len = tonumber(prefix_len)})
|
||
else
|
||
local single_ipv6 = line:match("^([:%da-fA-F]+)$")
|
||
if single_ipv6 and single_ipv6:match("^[:%da-fA-F]+$") then
|
||
table.insert(ipv6_networks, {prefix = single_ipv6, prefix_len = 128})
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
local ipv4_checks = {}
|
||
for _, net in ipairs(ipv4_networks) do
|
||
table.insert(ipv4_checks, string.format('isInNet(resolved_ip, "%s", "%s")', net.network, net.mask))
|
||
end
|
||
local ipv4_check_code = ""
|
||
if #ipv4_checks > 0 then
|
||
ipv4_check_code = "if (" .. table.concat(ipv4_checks, " ||\n ") .. ") {\n return \"DIRECT\";\n }"
|
||
end
|
||
|
||
local ipv6_checks = {}
|
||
for _, net in ipairs(ipv6_networks) do
|
||
if net.prefix_len == 128 then
|
||
table.insert(ipv6_checks, string.format('resolved_ipv6 === "%s"', net.prefix))
|
||
else
|
||
local prefix_hex = net.prefix:gsub(":+$", "")
|
||
table.insert(ipv6_checks, string.format('resolved_ipv6.indexOf("%s") === 0', prefix_hex))
|
||
end
|
||
end
|
||
local ipv6_check_code = ""
|
||
if #ipv6_checks > 0 then
|
||
ipv6_check_code = "if (" .. table.concat(ipv6_checks, " ||\n ") .. ") {\n return \"DIRECT\";\n }"
|
||
end
|
||
|
||
local pac_script = string.format([[
|
||
// OpenClash PAC File
|
||
var _failureCount = 0;
|
||
var _lastCheckTime = 0;
|
||
var _isProxyDown = false;
|
||
var _checkInterval = 300000; // 5分钟 = 300000毫秒
|
||
|
||
// Access Check
|
||
function _checkNetworkConnectivity() {
|
||
var currentTime = Date.now();
|
||
|
||
if (currentTime - _lastCheckTime < _checkInterval) {
|
||
return !_isProxyDown;
|
||
}
|
||
|
||
_lastCheckTime = currentTime;
|
||
|
||
try {
|
||
var test1 = dnsResolve("www.gstatic.com");
|
||
var test2 = dnsResolve("captive.apple.com");
|
||
|
||
if (test1 || test2) {
|
||
if (_isProxyDown) {
|
||
_isProxyDown = false;
|
||
_failureCount = 0;
|
||
}
|
||
return true;
|
||
} else {
|
||
_failureCount++;
|
||
if (_failureCount >= 3) {
|
||
_isProxyDown = true;
|
||
}
|
||
return false;
|
||
}
|
||
} catch (e) {
|
||
_failureCount++;
|
||
if (_failureCount >= 3) {
|
||
_isProxyDown = true;
|
||
}
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function FindProxyForURL(url, host) {
|
||
if (isPlainHostName(host) ||
|
||
host === "127.0.0.1" ||
|
||
host === "::1" ||
|
||
host === "localhost") {
|
||
return "DIRECT";
|
||
}
|
||
|
||
// IPv4
|
||
var resolved_ip = dnsResolve(host);
|
||
if (resolved_ip) {
|
||
%s
|
||
}
|
||
|
||
// IPv6
|
||
var resolved_ipv6 = dnsResolveEx(host);
|
||
if (resolved_ipv6) {
|
||
%s
|
||
}
|
||
|
||
if (_checkNetworkConnectivity()) {
|
||
return "%s";
|
||
} else {
|
||
return "DIRECT";
|
||
}
|
||
}
|
||
|
||
function FindProxyForURLEx(url, host) {
|
||
return FindProxyForURL(url, host);
|
||
}
|
||
]], ipv4_check_code, ipv6_check_code, proxy_string)
|
||
|
||
return pac_script
|
||
end
|
||
|
||
local function is_safe_filename(filename)
|
||
return filename and filename:match("^[%w%._%-]+$") and not filename:match("^%.")
|
||
end
|
||
|
||
function action_oc_action()
|
||
local action = luci.http.formvalue("action")
|
||
local config_file = luci.http.formvalue("config_file")
|
||
|
||
if not action then
|
||
luci.http.status(400, "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")
|
||
return
|
||
end
|
||
|
||
if uci:get("openclash", "config", "config_path") ~= config_path then
|
||
uci:set("openclash", "config", "config_path", config_path)
|
||
end
|
||
end
|
||
|
||
if action == "start" then
|
||
if uci:get("openclash", "config", "enable") ~= "1" then
|
||
uci:set("openclash", "config", "enable", "1")
|
||
uci:commit("openclash")
|
||
end
|
||
if not is_running() then
|
||
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 start >/dev/null 2>&1")
|
||
else
|
||
luci.sys.call("/etc/init.d/openclash restart >/dev/null 2>&1")
|
||
end
|
||
elseif action == "stop" then
|
||
if uci:get("openclash", "config", "enable") ~= "0" then
|
||
uci:set("openclash", "config", "enable", "0")
|
||
uci:commit("openclash")
|
||
end
|
||
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 stop >/dev/null 2>&1")
|
||
elseif action == "restart" then
|
||
if uci:get("openclash", "config", "enable") ~= "1" then
|
||
uci:set("openclash", "config", "enable", "1")
|
||
uci:commit("openclash")
|
||
end
|
||
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")
|
||
return
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({status = "success", action = action})
|
||
end
|
||
|
||
function action_config_file_list()
|
||
local config_files = {}
|
||
local current_config = ""
|
||
|
||
local config_path = fs.uci_get_config("config", "config_path")
|
||
if config_path then
|
||
current_config = config_path
|
||
end
|
||
|
||
local config_dir = "/etc/openclash/config/"
|
||
if fs.access(config_dir) then
|
||
local files = fs.dir(config_dir)
|
||
if files then
|
||
for _, file in ipairs(files) do
|
||
local full_path = config_dir .. file
|
||
local stat = fs.stat(full_path)
|
||
if stat and stat.type == "regular" then
|
||
if string.match(file, "%.ya?ml$") then
|
||
table.insert(config_files, {
|
||
name = file,
|
||
path = full_path,
|
||
size = stat.size,
|
||
mtime = stat.mtime
|
||
})
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
table.sort(config_files, function(a, b)
|
||
return a.mtime > b.mtime
|
||
end)
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
config_files = config_files,
|
||
current_config = current_config,
|
||
total_count = #config_files
|
||
})
|
||
end
|
||
|
||
function action_upload_config()
|
||
local upload = luci.http.formvalue("config_file")
|
||
local filename = luci.http.formvalue("filename")
|
||
|
||
luci.http.prepare_content("application/json")
|
||
|
||
if not upload or upload == "" then
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "No file uploaded"
|
||
})
|
||
return
|
||
end
|
||
|
||
if not filename or filename == "" then
|
||
filename = "upload_" .. os.date("%Y%m%d_%H%M%S")
|
||
end
|
||
|
||
if not is_safe_filename(filename) then
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Invalid filename"
|
||
})
|
||
return
|
||
end
|
||
|
||
if not string.match(filename, "%.ya?ml$") then
|
||
filename = filename .. ".yaml"
|
||
end
|
||
|
||
local config_dir = "/etc/openclash/config/"
|
||
local target_path = config_dir .. filename
|
||
|
||
if string.len(upload) == 0 then
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Uploaded file is empty"
|
||
})
|
||
return
|
||
end
|
||
|
||
local file_size = string.len(upload)
|
||
if file_size > 10 * 1024 * 1024 then
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = string.format("File size (%s) exceeds 10MB limit", fs.filesize(file_size))
|
||
})
|
||
return
|
||
end
|
||
|
||
local yaml_valid = false
|
||
local content_start = string.sub(upload, 1, 5000)
|
||
|
||
if string.find(content_start, "proxy%-providers:") or
|
||
string.find(content_start, "proxies:") or
|
||
string.find(content_start, "rules:") or
|
||
string.find(content_start, "port:") or
|
||
string.find(content_start, "mode:") then
|
||
yaml_valid = true
|
||
end
|
||
|
||
if not yaml_valid then
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Invalid config file format - missing required YAML sections"
|
||
})
|
||
return
|
||
end
|
||
|
||
luci.sys.call("mkdir -p " .. config_dir)
|
||
|
||
local fp = io.open(target_path, "w")
|
||
if fp then
|
||
fp:write(upload)
|
||
fp:close()
|
||
|
||
luci.sys.call(string.format("chmod 644 '%s'", target_path))
|
||
luci.sys.call(string.format("chown root:root '%s'", target_path))
|
||
|
||
local written_content = fs.readfile(target_path)
|
||
if not written_content or string.len(written_content) ~= file_size then
|
||
fs.unlink(target_path)
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "File write verification failed"
|
||
})
|
||
return
|
||
end
|
||
|
||
luci.http.write_json({
|
||
status = "success",
|
||
message = "Config file uploaded successfully",
|
||
filename = filename,
|
||
file_path = target_path,
|
||
file_size = file_size,
|
||
readable_size = fs.filesize(file_size)
|
||
})
|
||
else
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Failed to save config file to disk"
|
||
})
|
||
end
|
||
end
|
||
|
||
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")
|
||
return
|
||
end
|
||
|
||
local allow = false
|
||
if config_file == "/etc/openclash/custom/openclash_custom_overwrite.sh" then
|
||
allow = true
|
||
elseif config_file:match("^/etc/openclash/overwrite/[^/]+$") and not string.find(config_file, "%.%.") then
|
||
allow = true
|
||
elseif config_file:match("^/etc/openclash/[^/]+%.ya?ml$") then
|
||
allow = true
|
||
elseif config_file:match("^/etc/openclash/config/[^/]+%.ya?ml$") and not string.find(config_file, "%.%.") then
|
||
allow = true
|
||
end
|
||
|
||
if not allow then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Invalid config file path"
|
||
})
|
||
return
|
||
end
|
||
|
||
if not fs.access(config_file) then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "success",
|
||
content = "",
|
||
file_info = {
|
||
path = config_file,
|
||
size = 0,
|
||
mtime = 0,
|
||
readable_size = "0 KB",
|
||
last_modified = ""
|
||
}
|
||
})
|
||
return
|
||
end
|
||
|
||
local stat = fs.stat(config_file)
|
||
if not stat or stat.type ~= "regular" then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Config file is not a regular file"
|
||
})
|
||
return
|
||
end
|
||
|
||
if stat.size > 10 * 1024 * 1024 then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Config file too large (max 10MB)"
|
||
})
|
||
return
|
||
end
|
||
|
||
local content = fs.readfile(config_file)
|
||
if content == nil then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Failed to read config file"
|
||
})
|
||
return
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "success",
|
||
content = content,
|
||
file_info = {
|
||
path = config_file,
|
||
size = stat.size,
|
||
mtime = stat.mtime,
|
||
readable_size = fs.filesize(stat.size),
|
||
last_modified = os.date("%Y-%m-%d %H:%M:%S", stat.mtime)
|
||
}
|
||
})
|
||
end
|
||
|
||
function action_config_file_save()
|
||
local config_file = luci.http.formvalue("config_file")
|
||
local content = luci.http.formvalue("content")
|
||
if content then
|
||
content = content:gsub("\r\n", "\n"):gsub("\r", "\n")
|
||
end
|
||
|
||
if not config_file then
|
||
luci.http.status(400, "Missing config_file parameter")
|
||
return
|
||
end
|
||
|
||
if not content then
|
||
luci.http.status(400, "Missing content parameter")
|
||
return
|
||
end
|
||
|
||
local is_overwrite = (config_file == "/etc/openclash/custom/openclash_custom_overwrite.sh" or config_file:match("^/etc/openclash/overwrite/[^/]+$"))
|
||
|
||
if not is_overwrite then
|
||
if not string.match(config_file, "^/etc/openclash/config/[^/]+%.ya?ml$") or string.find(config_file, "%.%.") then
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Invalid config file path"
|
||
})
|
||
return
|
||
end
|
||
else
|
||
if not (config_file == "/etc/openclash/custom/openclash_custom_overwrite.sh" or (config_file:match("^/etc/openclash/overwrite/[^/]+$") and not string.find(config_file, "%.%."))) then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Invalid overwrite file path"
|
||
})
|
||
return
|
||
end
|
||
end
|
||
|
||
if string.len(content) > 10 * 1024 * 1024 then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Content too large (max 10MB)"
|
||
})
|
||
return
|
||
end
|
||
|
||
local backup_file = nil
|
||
if fs.access(config_file) then
|
||
backup_file = config_file .. ".backup." .. os.time()
|
||
local backup_success = luci.sys.call(string.format("cp '%s' '%s'", config_file, backup_file))
|
||
if backup_success ~= 0 then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Failed to create backup file"
|
||
})
|
||
return
|
||
end
|
||
end
|
||
|
||
local success = fs.writefile(config_file, content)
|
||
if not success then
|
||
if backup_file then
|
||
luci.sys.call(string.format("mv '%s' '%s'", backup_file, config_file))
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Failed to write config file"
|
||
})
|
||
return
|
||
end
|
||
|
||
local written_content = fs.readfile(config_file)
|
||
if written_content ~= content then
|
||
if backup_file then
|
||
luci.sys.call(string.format("mv '%s' '%s'", backup_file, config_file))
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "File write verification failed"
|
||
})
|
||
return
|
||
end
|
||
|
||
if not is_overwrite then
|
||
luci.sys.call(string.format("chmod 644 '%s'", config_file))
|
||
end
|
||
luci.sys.call(string.format("chown root:root '%s'", config_file))
|
||
|
||
if backup_file then
|
||
luci.sys.call(string.format([[
|
||
(
|
||
config_dir="$(dirname '%s')"
|
||
config_basename="$(basename '%s')"
|
||
cd "$config_dir" 2>/dev/null || exit 0
|
||
rm -f "${config_basename}.backup."* 2>/dev/null
|
||
) &
|
||
]], config_file, config_file))
|
||
end
|
||
|
||
local stat = fs.stat(config_file)
|
||
local file_info = {}
|
||
if stat then
|
||
file_info = {
|
||
path = config_file,
|
||
size = stat.size,
|
||
mtime = stat.mtime,
|
||
readable_size = fs.filesize(stat.size),
|
||
last_modified = os.date("%Y-%m-%d %H:%M:%S", stat.mtime)
|
||
}
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "success",
|
||
message = "Config file saved successfully",
|
||
file_info = file_info,
|
||
backup_created = backup_file and true or false
|
||
})
|
||
end
|
||
|
||
function action_add_subscription()
|
||
local name = luci.http.formvalue("name")
|
||
local address = luci.http.formvalue("address")
|
||
local sub_ua = luci.http.formvalue("sub_ua") or "clash.meta"
|
||
local sub_convert = luci.http.formvalue("sub_convert") or "0"
|
||
local convert_address = luci.http.formvalue("convert_address") or ""
|
||
local template = luci.http.formvalue("template") or ""
|
||
local emoji = luci.http.formvalue("emoji") or "false"
|
||
local udp = luci.http.formvalue("udp") or "false"
|
||
local skip_cert_verify = luci.http.formvalue("skip_cert_verify") or "false"
|
||
local sort = luci.http.formvalue("sort") or "false"
|
||
local node_type = luci.http.formvalue("node_type") or "false"
|
||
local rule_provider = luci.http.formvalue("rule_provider") or "false"
|
||
local custom_params = luci.http.formvalue("custom_params") or ""
|
||
local keyword = luci.http.formvalue("keyword") or ""
|
||
local ex_keyword = luci.http.formvalue("ex_keyword") or ""
|
||
local de_ex_keyword = luci.http.formvalue("de_ex_keyword") or ""
|
||
|
||
luci.http.prepare_content("application/json")
|
||
|
||
if not name or not address then
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Missing name or address parameter"
|
||
})
|
||
return
|
||
end
|
||
|
||
local is_valid_url = false
|
||
|
||
if sub_convert == "1" then
|
||
if string.find(address, "^https?://") and not string.find(address, "\n") and not string.find(address, "|") then
|
||
is_valid_url = true
|
||
elseif string.find(address, "\n") or string.find(address, "|") then
|
||
local links = {}
|
||
if string.find(address, "\n") then
|
||
for line in address:gmatch("[^\n]+") do
|
||
table.insert(links, line:match("^%s*(.-)%s*$"))
|
||
end
|
||
else
|
||
for link in address:gmatch("[^|]+") do
|
||
table.insert(links, link:match("^%s*(.-)%s*$"))
|
||
end
|
||
end
|
||
|
||
for _, link in ipairs(links) do
|
||
if link and link ~= "" then
|
||
if string.find(link, "^https?://") or string.find(link, "^[a-zA-Z]+://") then
|
||
is_valid_url = true
|
||
break
|
||
end
|
||
end
|
||
end
|
||
else
|
||
if string.find(address, "^[a-zA-Z]+://") and
|
||
not string.find(address, "\n") and not string.find(address, "|") then
|
||
is_valid_url = true
|
||
end
|
||
end
|
||
else
|
||
if string.find(address, "^https?://") and not string.find(address, "\n") and not string.find(address, "|") then
|
||
is_valid_url = true
|
||
end
|
||
end
|
||
|
||
if not is_valid_url then
|
||
local error_msg
|
||
if sub_convert == "1" then
|
||
error_msg = "Invalid subscription URL format. Support: HTTP/HTTPS subscription URLs, or protocol links, can be separated by newlines or |"
|
||
else
|
||
error_msg = "Invalid subscription URL format. Only single HTTP/HTTPS subscription URL is supported when subscription conversion is disabled"
|
||
end
|
||
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = error_msg
|
||
})
|
||
return
|
||
end
|
||
|
||
local existing_section_id = nil
|
||
uci:foreach("openclash", "config_subscribe", function(s)
|
||
if s.name == name then
|
||
existing_section_id = s['.name']
|
||
return false
|
||
end
|
||
end)
|
||
|
||
local normalized_address = address
|
||
if sub_convert == "1" and (string.find(address, "\n") or string.find(address, "|")) then
|
||
local links = {}
|
||
if string.find(address, "\n") then
|
||
for line in address:gmatch("[^\n]+") do
|
||
local link = line:match("^%s*(.-)%s*$")
|
||
if link and link ~= "" then
|
||
table.insert(links, link)
|
||
end
|
||
end
|
||
else
|
||
for link in address:gmatch("[^|]+") do
|
||
local clean_link = link:match("^%s*(.-)%s*$")
|
||
if clean_link and clean_link ~= "" then
|
||
table.insert(links, clean_link)
|
||
end
|
||
end
|
||
end
|
||
normalized_address = table.concat(links, "\n")
|
||
else
|
||
normalized_address = address:match("^%s*(.-)%s*$")
|
||
end
|
||
|
||
local section_id
|
||
if existing_section_id then
|
||
section_id = existing_section_id
|
||
else
|
||
section_id = uci:add("openclash", "config_subscribe")
|
||
end
|
||
|
||
if section_id then
|
||
uci:set("openclash", section_id, "name", name)
|
||
uci:set("openclash", section_id, "address", normalized_address)
|
||
uci:set("openclash", section_id, "sub_ua", sub_ua)
|
||
uci:set("openclash", section_id, "sub_convert", sub_convert)
|
||
if sub_convert == "1" then
|
||
uci:set("openclash", section_id, "convert_address", convert_address)
|
||
uci:set("openclash", section_id, "template", template)
|
||
else
|
||
uci:delete("openclash", section_id, "convert_address")
|
||
uci:delete("openclash", section_id, "template")
|
||
end
|
||
uci:set("openclash", section_id, "emoji", emoji)
|
||
uci:set("openclash", section_id, "udp", udp)
|
||
uci:set("openclash", section_id, "skip_cert_verify", skip_cert_verify)
|
||
uci:set("openclash", section_id, "sort", sort)
|
||
uci:set("openclash", section_id, "node_type", node_type)
|
||
uci:set("openclash", section_id, "rule_provider", rule_provider)
|
||
|
||
uci:delete("openclash", section_id, "custom_params")
|
||
if custom_params and custom_params ~= "" and sub_convert == "1" then
|
||
local params = {}
|
||
for line in custom_params:gmatch("[^\n]+") do
|
||
local param = line:match("^%s*(.-)%s*$")
|
||
if param and param ~= "" then
|
||
table.insert(params, param)
|
||
end
|
||
end
|
||
if #params > 0 then
|
||
for i, param in ipairs(params) do
|
||
uci:set_list("openclash", section_id, "custom_params", param)
|
||
end
|
||
end
|
||
end
|
||
|
||
uci:delete("openclash", section_id, "keyword")
|
||
if keyword and keyword ~= "" then
|
||
local keywords = {}
|
||
for line in keyword:gmatch("[^\n]+") do
|
||
local kw = line:match("^%s*(.-)%s*$")
|
||
if kw and kw ~= "" then
|
||
table.insert(keywords, kw)
|
||
end
|
||
end
|
||
if #keywords > 0 then
|
||
for i, kw in ipairs(keywords) do
|
||
uci:set_list("openclash", section_id, "keyword", kw)
|
||
end
|
||
end
|
||
end
|
||
|
||
uci:delete("openclash", section_id, "ex_keyword")
|
||
if ex_keyword and ex_keyword ~= "" then
|
||
local ex_keywords = {}
|
||
for line in ex_keyword:gmatch("[^\n]+") do
|
||
local ex_kw = line:match("^%s*(.-)%s*$")
|
||
if ex_kw and ex_kw ~= "" then
|
||
table.insert(ex_keywords, ex_kw)
|
||
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
|
||
end
|
||
end
|
||
|
||
uci:set("openclash", section_id, "de_ex_keyword", de_ex_keyword)
|
||
|
||
uci:commit("openclash")
|
||
|
||
local action_msg = existing_section_id and "Subscription updated successfully" or "Subscription added successfully"
|
||
luci.http.write_json({
|
||
status = "success",
|
||
message = action_msg,
|
||
name = name,
|
||
address = normalized_address,
|
||
sub_ua = sub_ua,
|
||
sub_convert = sub_convert,
|
||
multiple_links = sub_convert == "1" and (string.find(normalized_address, "\n") and true or false)
|
||
})
|
||
else
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Failed to add/update subscription configuration"
|
||
})
|
||
end
|
||
end
|
||
|
||
function action_upload_overwrite()
|
||
local upload = luci.http.formvalue("config_file")
|
||
local filename = luci.http.formvalue("filename")
|
||
local enable = luci.http.formvalue("enable")
|
||
local order = luci.http.formvalue("order")
|
||
luci.http.prepare_content("application/json")
|
||
if not upload or upload == "" then
|
||
luci.http.write_json({status = "error", message = "No file uploaded"})
|
||
return
|
||
end
|
||
if not filename or filename == "" then
|
||
filename = "upload_" .. os.date("%Y%m%d_%H%M%S")
|
||
end
|
||
if not is_safe_filename(filename) then
|
||
luci.http.write_json({status = "error", message = "Invalid filename"})
|
||
return
|
||
end
|
||
local overwrite_dir = "/etc/openclash/overwrite/"
|
||
luci.sys.call("mkdir -p " .. overwrite_dir)
|
||
local target_path = overwrite_dir .. filename
|
||
if string.len(upload) == 0 then
|
||
luci.http.write_json({status = "error", message = "Uploaded file is empty"})
|
||
return
|
||
end
|
||
local file_size = string.len(upload)
|
||
if file_size > 10 * 1024 * 1024 then
|
||
luci.http.write_json({status = "error", message = string.format("File size (%s) exceeds 10MB limit", require("luci.openclash").filesize(file_size))})
|
||
return
|
||
end
|
||
local fp = io.open(target_path, "w")
|
||
if fp then
|
||
fp:write(upload)
|
||
fp:close()
|
||
luci.sys.call(string.format("chmod 644 '%s'", target_path))
|
||
luci.sys.call(string.format("chown root:root '%s'", target_path))
|
||
local written_content = fs.readfile(target_path)
|
||
if not written_content or string.len(written_content) ~= file_size then
|
||
fs.unlink(target_path)
|
||
luci.http.write_json({status = "error", message = "File write verification failed"})
|
||
return
|
||
end
|
||
|
||
local section_name = filename
|
||
local found = false
|
||
|
||
uci:foreach("openclash", "config_overwrite", function(s)
|
||
if s.name == section_name then
|
||
found = true
|
||
if s.enable == nil or (s.enable ~= nil and enable ~= nil) then
|
||
if enable == nil then
|
||
enable = 0
|
||
end
|
||
uci:set("openclash", s[".name"], "enable", tostring(enable))
|
||
end
|
||
if s.order == nil or (s.order ~= nil and s.order ~= order and order ~= nil) then
|
||
if order == nil then
|
||
local max_order = -1
|
||
uci:foreach("openclash", "config_overwrite", function(s)
|
||
local o = tonumber(s.order)
|
||
if o and o > max_order then max_order = o end
|
||
end)
|
||
order = tostring(max_order + 1)
|
||
end
|
||
uci:set("openclash", s[".name"], "order", order)
|
||
else
|
||
uci:set("openclash", s[".name"], "order", tonumber(order))
|
||
end
|
||
end
|
||
end)
|
||
if not found then
|
||
local sid = uci:add("openclash", "config_overwrite")
|
||
uci:set("openclash", sid, "name", section_name)
|
||
uci:set("openclash", sid, "type", "file")
|
||
if enable ~= nil then
|
||
uci:set("openclash", sid, "enable", tostring(enable))
|
||
else
|
||
uci:set("openclash", sid, "enable", 0)
|
||
end
|
||
if order ~= nil then
|
||
uci:set("openclash", sid, "order", tostring(order))
|
||
else
|
||
local max_order = -1
|
||
uci:foreach("openclash", "config_overwrite", function(s)
|
||
local o = tonumber(s.order)
|
||
if o and o > max_order then max_order = o end
|
||
end)
|
||
uci:set("openclash", sid, "order", tostring(max_order + 1))
|
||
end
|
||
end
|
||
|
||
uci:commit("openclash")
|
||
|
||
luci.http.write_json({
|
||
status = "success",
|
||
message = "Overwrite file uploaded successfully",
|
||
filename = filename,
|
||
file_path = target_path,
|
||
file_size = file_size,
|
||
readable_size = fs.filesize(file_size)
|
||
})
|
||
else
|
||
luci.http.write_json({status = "error", message = "Failed to save file to disk"})
|
||
end
|
||
end
|
||
|
||
function action_overwrite_subscribe_info()
|
||
local method = luci.http.getenv("REQUEST_METHOD")
|
||
local filename = luci.http.formvalue("filename")
|
||
local old_filename = luci.http.formvalue("old_filename")
|
||
local typ = luci.http.formvalue("type") or "file"
|
||
local section_name = nil
|
||
local old_section_name = nil
|
||
|
||
if filename and not is_safe_filename(filename) then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({status = "error", message = "Invalid filename"})
|
||
return
|
||
end
|
||
|
||
if filename then
|
||
section_name = filename:match("([^/]+)$")
|
||
end
|
||
if old_filename then
|
||
old_section_name = old_filename:match("([^/]+)$")
|
||
end
|
||
|
||
if method == "GET" then
|
||
local result = {}
|
||
uci:foreach("openclash", "config_overwrite", function(s)
|
||
if s.name then
|
||
result[s.name] = {
|
||
url = s.url or "",
|
||
update_days = s.update_days or "",
|
||
update_hour = s.update_hour or "",
|
||
order = tonumber(s.order) or 0,
|
||
type = s.type or "file",
|
||
param = s.param or "",
|
||
enable = tonumber(s.enable) or 0
|
||
}
|
||
end
|
||
end)
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({status="success", data=result})
|
||
return
|
||
elseif method == "POST" then
|
||
if not section_name then
|
||
luci.http.status(400, "Missing filename")
|
||
return
|
||
end
|
||
local url = luci.http.formvalue("url") or ""
|
||
local update_days = luci.http.formvalue("update_days") or ""
|
||
local update_hour = luci.http.formvalue("update_hour") or ""
|
||
local order = luci.http.formvalue("order")
|
||
local param = luci.http.formvalue("param") or ""
|
||
typ = luci.http.formvalue("type") or typ or "file"
|
||
local enable = luci.http.formvalue("enable")
|
||
|
||
if typ == "http" then
|
||
if not url or url == "" then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Subscribe URL cannot be empty"
|
||
})
|
||
return
|
||
end
|
||
local is_valid_url = false
|
||
if url:match("^https?://") and not url:find("\n") and not url:find("|") then
|
||
is_valid_url = true
|
||
end
|
||
if not is_valid_url then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
status = "error",
|
||
message = "Invalid subscribe URL format, only single HTTP/HTTPS link is supported"
|
||
})
|
||
return
|
||
end
|
||
end
|
||
|
||
local found = false
|
||
if old_section_name and old_section_name ~= "" and old_section_name ~= section_name then
|
||
uci:foreach("openclash", "config_overwrite", function(s)
|
||
if s.name == old_section_name then
|
||
uci:set("openclash", s[".name"], "name", section_name)
|
||
uci:set("openclash", s[".name"], "url", url)
|
||
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)
|
||
uci:set("openclash", s[".name"], "param", param)
|
||
if s.order == nil or (s.order ~= nil and s.order ~= order and order ~= nil) then
|
||
if order == nil then
|
||
local max_order = -1
|
||
uci:foreach("openclash", "config_overwrite", function(s)
|
||
local o = tonumber(s.order)
|
||
if o and o > max_order then max_order = o end
|
||
end)
|
||
order = tostring(max_order + 1)
|
||
end
|
||
uci:set("openclash", s[".name"], "order", order)
|
||
else
|
||
uci:set("openclash", s[".name"], "order", tonumber(order) or 1)
|
||
end
|
||
if s.enable == nil or (s.enable ~= nil and enable ~= nil) then
|
||
if enable == nil then
|
||
enable = 0
|
||
end
|
||
uci:set("openclash", s[".name"], "enable", tostring(enable))
|
||
end
|
||
found = true
|
||
end
|
||
end)
|
||
local overwrite_dir = "/etc/openclash/overwrite/"
|
||
local old_file = overwrite_dir .. old_section_name
|
||
local new_file = overwrite_dir .. section_name
|
||
if fs.access(old_file) and not fs.access(new_file) then
|
||
fs.rename(old_file, new_file)
|
||
end
|
||
uci:commit("openclash")
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({status="success"})
|
||
return
|
||
end
|
||
if not found then
|
||
uci:foreach("openclash", "config_overwrite", function(s)
|
||
if s.name == section_name then
|
||
uci:set("openclash", s[".name"], "url", url)
|
||
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)
|
||
uci:set("openclash", s[".name"], "param", param)
|
||
if s.order == nil or (s.order ~= nil and s.order ~= order and order ~= nil) then
|
||
if order == nil then
|
||
local max_order = -1
|
||
uci:foreach("openclash", "config_overwrite", function(s)
|
||
local o = tonumber(s.order)
|
||
if o and o > max_order then max_order = o end
|
||
end)
|
||
order = tostring(max_order + 1)
|
||
end
|
||
uci:set("openclash", s[".name"], "order", order)
|
||
else
|
||
uci:set("openclash", s[".name"], "order", tonumber(order))
|
||
end
|
||
if s.enable == nil or (s.enable ~= nil and enable ~= nil) then
|
||
if enable == nil then
|
||
enable = 0
|
||
end
|
||
uci:set("openclash", s[".name"], "enable", tostring(enable))
|
||
end
|
||
found = true
|
||
end
|
||
end)
|
||
end
|
||
if not found then
|
||
local sid = uci:add("openclash", "config_overwrite")
|
||
uci:set("openclash", sid, "name", section_name)
|
||
uci:set("openclash", sid, "url", url)
|
||
uci:set("openclash", sid, "update_days", update_days)
|
||
uci:set("openclash", sid, "update_hour", update_hour)
|
||
uci:set("openclash", sid, "type", typ)
|
||
uci:set("openclash", sid, "param", param)
|
||
if order == nil then
|
||
local max_order = -1
|
||
uci:foreach("openclash", "config_overwrite", function(s)
|
||
local o = tonumber(s.order)
|
||
if o and o > max_order then max_order = o end
|
||
end)
|
||
order = tostring(max_order + 1)
|
||
else
|
||
order = tostring(order)
|
||
end
|
||
uci:set("openclash", sid, "order", order)
|
||
uci:set("openclash", sid, "enable", 0)
|
||
end
|
||
uci:commit("openclash")
|
||
|
||
if typ == "file" then
|
||
local overwrite_dir = "/etc/openclash/overwrite/"
|
||
local file_path = overwrite_dir .. section_name
|
||
if not fs.access(file_path) then
|
||
fs.writefile(file_path, "")
|
||
end
|
||
elseif typ == "http" then
|
||
local overwrite_dir = "/etc/openclash/overwrite/"
|
||
local file_path = overwrite_dir .. section_name
|
||
if url and url ~= "" then
|
||
local cmd = string.format('curl -sL --connect-timeout 5 -m 15 --retry 2 "%s" -o "%s"', url, file_path)
|
||
local ret = luci.sys.call(cmd)
|
||
if not fs.access(file_path) then
|
||
fs.writefile(file_path, "")
|
||
end
|
||
if ret ~= 0 or not fs.access(file_path) or fs.stat(file_path).size == 0 then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({status="error", message="Download failed"})
|
||
return
|
||
end
|
||
else
|
||
if not fs.access(file_path) then
|
||
fs.writefile(file_path, "")
|
||
end
|
||
end
|
||
end
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({status="success"})
|
||
return
|
||
else
|
||
luci.http.status(405, "Method Not Allowed")
|
||
end
|
||
end
|
||
|
||
function action_overwrite_file_list()
|
||
local overwrite_files = {}
|
||
local custom_file = "/etc/openclash/custom/openclash_custom_overwrite.sh"
|
||
|
||
if fs.access(custom_file) then
|
||
local stat = fs.stat(custom_file)
|
||
if stat and stat.type == "regular" then
|
||
table.insert(overwrite_files, {
|
||
name = "openclash_custom_overwrite.sh",
|
||
path = custom_file,
|
||
size = stat.size,
|
||
mtime = stat.mtime
|
||
})
|
||
end
|
||
end
|
||
|
||
local overwrite_dir = "/etc/openclash/overwrite/"
|
||
if fs.access(overwrite_dir) then
|
||
local files = fs.dir(overwrite_dir)
|
||
if files then
|
||
for _, file in ipairs(files) do
|
||
local full_path = overwrite_dir .. file
|
||
local stat = fs.stat(full_path)
|
||
if stat and stat.type == "regular" then
|
||
table.insert(overwrite_files, {
|
||
name = file,
|
||
path = full_path,
|
||
size = stat.size,
|
||
mtime = stat.mtime
|
||
})
|
||
end
|
||
end
|
||
end
|
||
end
|
||
|
||
table.sort(overwrite_files, function(a, b)
|
||
return (a.mtime or 0) > (b.mtime or 0)
|
||
end)
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({
|
||
overwrite_files = overwrite_files,
|
||
total_count = #overwrite_files
|
||
})
|
||
end
|
||
|
||
function delete_overwrite_file()
|
||
local filename = luci.http.formvalue("filename")
|
||
if not filename or filename == "" then
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({status="error", message="Missing filename"})
|
||
return
|
||
end
|
||
local overwrite_dir = "/etc/openclash/overwrite/"
|
||
local file_path = overwrite_dir .. filename
|
||
|
||
if fs.access(file_path) then
|
||
fs.unlink(file_path)
|
||
end
|
||
|
||
uci:foreach("openclash", "config_overwrite", function(s)
|
||
if s.name == filename then
|
||
uci:delete("openclash", s[".name"])
|
||
end
|
||
end)
|
||
uci:commit("openclash")
|
||
|
||
local order_list = {}
|
||
uci:foreach("openclash", "config_overwrite", function(s)
|
||
table.insert(order_list, { section = s[".name"], order = tonumber(s.order) or 0 })
|
||
end)
|
||
table.sort(order_list, function(a, b) return a.order < b.order end)
|
||
for idx, item in ipairs(order_list) do
|
||
uci:set("openclash", item.section, "order", tostring(idx - 1))
|
||
end
|
||
uci:commit("openclash")
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json({status="success"})
|
||
end
|
||
|
||
function action_get_subscribe_data()
|
||
local filename = luci.http.formvalue("filename")
|
||
if not filename then
|
||
luci.http.status(400, "Bad Request")
|
||
return
|
||
end
|
||
|
||
local data = {}
|
||
uci:foreach("openclash", "config_subscribe", function(s)
|
||
if s.name == filename then
|
||
data = s
|
||
end
|
||
end)
|
||
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json(data)
|
||
end
|
||
|
||
function action_get_subscribe_info_data()
|
||
local filename = luci.http.formvalue("filename")
|
||
if not filename then
|
||
luci.http.status(400, "Bad Request")
|
||
return
|
||
end
|
||
luci.http.prepare_content("application/json")
|
||
luci.http.write_json(get_sub_url(filename))
|
||
end |