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_fakeip_cache"}, call("action_flush_fakeip_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", "dler_info"}, call("action_dler_info")) entry({"admin", "services", "openclash", "dler_checkin"}, call("action_dler_checkin")) entry({"admin", "services", "openclash", "dler_logout"}, call("action_dler_logout")) entry({"admin", "services", "openclash", "dler_login"}, call("action_dler_login")) entry({"admin", "services", "openclash", "dler_login_info_save"}, call("action_dler_login_info_save")) 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", "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", "servers"},cbi("openclash/servers"),_("Onekey Create"), 50).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", "rule-providers-settings"},cbi("openclash/rule-providers-settings"),_("Rule Providers Append"), 60).leaf = true entry({"admin", "services", "openclash", "game-rules-manage"},form("openclash/game-rules-manage"), nil).leaf = true entry({"admin", "services", "openclash", "rule-providers-manage"},form("openclash/rule-providers-manage"), 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", "game-rules-file-manage"},form("openclash/game-rules-file-manage"), nil).leaf = true entry({"admin", "services", "openclash", "config-subscribe"},cbi("openclash/config-subscribe"),_("Config Subscribe"), 70).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", "rule-providers-config"},cbi("openclash/rule-providers-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")) 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 = uci:get("openclash", "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() if is_running() then local config_path = uci:get("openclash", "config", "config_path") if config_path then local config_filename = fs.basename(config_path) local runtime_config_path = "/etc/openclash/" .. config_filename 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') if config port = config['external-controller'] if port port = port.to_s if port:include?(':') port = port.split(':')[-1] end puts port end end end " 2>/dev/null ]], runtime_config_path)):gsub("%s+", "") if ruby_result and ruby_result ~= "" then return ruby_result end end end return uci:get("openclash", "config", "cn_port") or "9090" end local function mode() return uci:get("openclash", "config", "en_mode") end local function daip() return fs.lanip() end local function dase() if is_running() then local config_path = uci:get("openclash", "config", "config_path") if config_path then local config_filename = fs.basename(config_path) local runtime_config_path = "/etc/openclash/" .. config_filename 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') if config dase = config['secret'] puts \"#{dase}\" end end " 2>/dev/null ]], runtime_config_path)):gsub("%s+", "") return ruby_result end end return uci:get("openclash", "config", "dashboard_password") end local function db_foward_domain() return uci:get("openclash", "config", "dashboard_forward_domain") end local function db_foward_port() return uci:get("openclash", "config", "dashboard_forward_port") end local function db_foward_ssl() return uci:get("openclash", "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 = "" local line_trans = "" if nixio.fs.access("/tmp/openclash_start.log") then info = luci.sys.exec("sed -n '$p' /tmp/openclash_start.log 2>/dev/null") line_trans = info if string.len(info) > 0 then if not string.find (info, "【") or not string.find (info, "】") then line_trans = trans_line_nolabel(info) else line_trans = trans_line(info) end end end return line_trans 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 nixio.fs.access(meta_core_path) then return "0" else return "1" end end local function coremetacv() local v = "0" if not nixio.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 status = process_status("/usr/share/openclash/clash_version.sh") local core_meta_lv = "" local core_smart_enable = uci:get("openclash", "config", "smart_enable") or "0" if not status then if fs.access("/tmp/clash_last_version") and tonumber(os.time() - fs.mtime("/tmp/clash_last_version")) < 1800 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 action_get_last_version() core_meta_lv = "loading..." end else core_meta_lv = "loading..." end 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"] 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 status = process_status("/usr/share/openclash/openclash_version.sh") local oplv = "" if not status then if fs.access("/tmp/openclash_last_version") and tonumber(os.time() - fs.mtime("/tmp/openclash_last_version")) < 1800 then oplv = luci.sys.exec("sed -n 1p /tmp/openclash_last_version 2>/dev/null |tr -d '\n'") else action_get_last_version() oplv = "loading..." end else oplv = "loading..." end return oplv end local function opup() luci.sys.call("rm -rf /tmp/*_last_version 2>/dev/null && bash /usr/share/openclash/openclash_version.sh >/dev/null 2>&1") 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") luci.sys.call("rm -rf /tmp/*_last_version 2>/dev/null && bash /usr/share/openclash/clash_version.sh >/dev/null 2>&1") return luci.sys.call(string.format("/usr/share/openclash/openclash_core.sh '%s' >/dev/null 2>&1 &", type)) end local function corever() return uci:get("openclash", "config", "core_version") or "0" end local function release_branch() return uci:get("openclash", "config", "release_branch") or "master" end local function smart_enable() return uci:get("openclash", "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("rm -rf /tmp/clash_last_version 2>/dev/null && bash /usr/share/openclash/clash_version.sh '%s' >/dev/null 2>&1", cdn_url)) luci.sys.call(string.format("bash /usr/share/openclash/openclash_core.sh 'Meta' '%s' >/dev/null 2>&1 &", cdn_url)) else luci.sys.call("rm -rf /tmp/clash_last_version 2>/dev/null && bash /usr/share/openclash/clash_version.sh >/dev/null 2>&1") luci.sys.call("bash /usr/share/openclash/openclash_core.sh 'Meta' >/dev/null 2>&1 &") end end function download_rule() local filename = luci.http.formvalue("filename") local state = luci.sys.call(string.format('/usr/share/openclash/openclash_download_rule_list.sh "%s" >/dev/null 2>&1',filename)) return state end function action_flush_fakeip_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 state = luci.sys.exec(string.format('curl -sL -m 3 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPOST http://"%s":"%s"/cache/fakeip/flush', dase, daip, cn_port)) end luci.http.prepare_content("application/json") luci.http.write_json({ flush_status = state; }) end function action_update_config() -- filename or config_file is basename local filename = luci.http.formvalue("filename") local config_file = luci.http.formvalue("config_file") if not filename and config_file then filename = config_file end 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 start config update" }) end end function action_restore_config() uci:set("openclash", "config", "enable", "0") uci:commit("openclash") 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/yml_change.sh /usr/share/openclash/yml_change.sh >/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("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("rm -rf /tmp/*_last_version 2>/dev/null && bash /usr/share/openclash/openclash_update.sh 'one_key_update' '%s' >/dev/null 2>&1 &", cdn_url)) else return luci.sys.call("rm -rf /tmp/*_last_version 2>/dev/null && bash /usr/share/openclash/openclash_update.sh 'one_key_update' >/dev/null 2>&1 &") end end local function dler_login_info_save() uci:set("openclash", "config", "dler_email", luci.http.formvalue("email")) uci:set("openclash", "config", "dler_passwd", luci.http.formvalue("passwd")) uci:set("openclash", "config", "dler_checkin", luci.http.formvalue("checkin")) uci:set("openclash", "config", "dler_checkin_interval", luci.http.formvalue("interval")) if tonumber(luci.http.formvalue("multiple")) > 50 then uci:set("openclash", "config", "dler_checkin_multiple", "50") elseif tonumber(luci.http.formvalue("multiple")) < 1 or not tonumber(luci.http.formvalue("multiple")) then uci:set("openclash", "config", "dler_checkin_multiple", "1") else uci:set("openclash", "config", "dler_checkin_multiple", luci.http.formvalue("multiple")) end uci:commit("openclash") return "success" end local function dler_login() local info, token, get_sub, sub_info, sub_key, sub_match local sub_path = "/tmp/dler_sub" local email = uci:get("openclash", "config", "dler_email") local passwd = uci:get("openclash", "config", "dler_passwd") if email and passwd then info = luci.sys.exec(string.format("curl -sL -H 'Content-Type: application/json' -d '{\"email\":\"%s\", \"passwd\":\"%s\"}' -X POST https://dler.cloud/api/v1/login", email, passwd)) if info then info = json.parse(info) end if info and info.ret == 200 then token = info.data.token uci:set("openclash", "config", "dler_token", token) uci:commit("openclash") get_sub = string.format("curl -sL -H 'Content-Type: application/json' -d '{\"access_token\":\"%s\"}' -X POST https://dler.cloud/api/v1/managed/clash -o %s", token, sub_path) luci.sys.exec(get_sub) sub_info = fs.readfile(sub_path) if sub_info then sub_info = json.parse(sub_info) end if sub_info and sub_info.ret == 200 then sub_key = {"smart","ss","vmess","trojan"} for _,v in ipairs(sub_key) do while true do sub_match = false uci:foreach("openclash", "config_subscribe", function(s) if s.name == "Dler Cloud - " .. v and s.address == sub_info[v] then sub_match = true end end) if sub_match then break end luci.sys.exec(string.format('sid=$(uci -q add openclash config_subscribe) && uci -q set openclash."$sid".name="Dler Cloud - %s" && uci -q set openclash."$sid".address="%s"', v, sub_info[v])) uci:commit("openclash") break end luci.sys.exec(string.format('curl -sL -m 3 --retry 2 --user-agent "clash" "%s" -o "/etc/openclash/config/Dler Cloud - %s.yaml" >/dev/null 2>&1', sub_info[v], v)) end end return info.ret else uci:delete("openclash", "config", "dler_token") uci:commit("openclash") fs.unlink(sub_path) fs.unlink("/tmp/dler_checkin") fs.unlink("/tmp/dler_info") if info and info.msg then return info.msg else return "login faild" end end else uci:delete("openclash", "config", "dler_token") uci:commit("openclash") fs.unlink(sub_path) fs.unlink("/tmp/dler_checkin") fs.unlink("/tmp/dler_info") return "email or passwd is wrong" end end local function dler_logout() local info, token local token = uci:get("openclash", "config", "dler_token") if token then info = luci.sys.exec(string.format("curl -sL -H 'Content-Type: application/json' -d '{\"access_token\":\"%s\"}' -X POST https://dler.cloud/api/v1/logout", token)) if info then info = json.parse(info) end if info and info.ret == 200 then uci:delete("openclash", "config", "dler_token") uci:delete("openclash", "config", "dler_checkin") uci:delete("openclash", "config", "dler_checkin_interval") uci:delete("openclash", "config", "dler_checkin_multiple") uci:commit("openclash") fs.unlink("/tmp/dler_sub") fs.unlink("/tmp/dler_checkin") fs.unlink("/tmp/dler_info") return info.ret else if info and info.msg then return info.msg else return "logout faild" end end else return "logout faild" end end local function dler_info() local info, path, get_info local token = uci:get("openclash", "config", "dler_token") local email = uci:get("openclash", "config", "dler_email") local passwd = uci:get("openclash", "config", "dler_passwd") path = "/tmp/dler_info" if token and email and passwd then get_info = string.format("curl -sL -H 'Content-Type: application/json' -d '{\"email\":\"%s\", \"passwd\":\"%s\"}' -X POST https://dler.cloud/api/v1/information -o %s", email, passwd, path) if not nixio.fs.access(path) then luci.sys.exec(get_info) else if fs.readfile(path) == "" or not fs.readfile(path) then luci.sys.exec(get_info) else if (os.time() - fs.mtime(path) > 900) then luci.sys.exec(get_info) end end end info = fs.readfile(path) if info then info = json.parse(info) end if info and info.ret == 200 and info.data then return info.data elseif info and info.msg and info.msg ~= "api error, ignore" then luci.sys.exec(string.format("echo -e %s Dler Cloud Account Login Failed, The Error Info is【%s】 >> /tmp/openclash.log", os.date("%Y-%m-%d %H:%M:%S"), info.msg)) info.msg = "api error, ignore" fs.writefile(path, json.stringify(info)) elseif info and info.msg and info.msg == "api error, ignore" then return "error" else fs.unlink(path) luci.sys.exec(string.format("echo -e %s Dler Cloud Account Login Failed! Please Check And Try Again... >> /tmp/openclash.log", os.date("%Y-%m-%d %H:%M:%S"))) end return "error" else return "error" end end local function dler_checkin() local info local path = "/tmp/dler_checkin" local token = uci:get("openclash", "config", "dler_token") local email = uci:get("openclash", "config", "dler_email") local passwd = uci:get("openclash", "config", "dler_passwd") local multiple = uci:get("openclash", "config", "dler_checkin_multiple") or 1 if token and email and passwd then info = luci.sys.exec(string.format("curl -sL -H 'Content-Type: application/json' -d '{\"email\":\"%s\", \"passwd\":\"%s\", \"multiple\":\"%s\"}' -X POST https://dler.cloud/api/v1/checkin", email, passwd, multiple)) if info then info = json.parse(info) end if info and info.ret == 200 then fs.unlink("/tmp/dler_info") fs.writefile(path, info) luci.sys.exec(string.format("echo -e %s Dler Cloud Checkin Successful, Result:【%s】 >> /tmp/openclash.log", os.date("%Y-%m-%d %H:%M:%S"), info.data.checkin)) return info else if info and info.msg then luci.sys.exec(string.format("echo -e %s Dler Cloud Checkin Failed, Result:【%s】 >> /tmp/openclash.log", os.date("%Y-%m-%d %H:%M:%S"), info.msg)) else luci.sys.exec(string.format("echo -e %s Dler Cloud Checkin Failed! Please Check And Try Again... >> /tmp/openclash.log",os.date("%Y-%m-%d %H:%M:%S"))) end return info end else return "error" 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 uci:get("openclash", "config", "config_path") then return string.sub(uci:get("openclash", "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 nixio.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 uci:set("openclash", s[".name"], "url", url) uci:commit("openclash") info = "Success" end end end ) if not info then if url == "" then info = "Delete success" else uci:section("openclash", "subscribe_info", nil, {name = filename, url = url}) uci:commit("openclash") info = "Success" end end end luci.http.prepare_content("application/json") luci.http.write_json({ info = info; }) end function sub_info_get() local sub_ua, filepath, filename, sub_url, sub_info, info, upload, download, total, expire, http_code, len, percent, day_left, day_expire, surplus, used local info_tb = {} 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 uci:foreach("openclash", "subscribe_info", function(s) if s.name == filename and s.url and string.find(s.url, "http") then string.gsub(s.url, '[^\n]+', function(w) table.insert(info_tb, w) end) sub_url = info_tb[1] end end ) if not sub_url then 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 ) end if not sub_url then sub_info = "No Sub Info Found" else info = luci.sys.exec(string.format("curl -sLI -X GET -m 10 -w 'http_code='%%{http_code} -H 'User-Agent: %s' '%s'", sub_ua, sub_url)) if not info or tonumber(string.sub(string.match(info, "http_code=%d+"), 11, -1)) ~= 200 then info = luci.sys.exec(string.format("curl -sLI -X GET -m 10 -w 'http_code='%%{http_code} -H 'User-Agent: Quantumultx' '%s'", sub_url)) end if info then http_code=string.sub(string.match(info, "http_code=%d+"), 11, -1) if tonumber(http_code) == 200 then info = string.lower(info) if string.find(info, "subscription%-userinfo") then info = luci.sys.exec("echo '%s' |grep 'subscription-userinfo'" %info) upload = string.sub(string.match(info, "upload=%d+"), 8, -1) or nil download = string.sub(string.match(info, "download=%d+"), 10, -1) or nil total = tonumber(string.format("%.1f",string.sub(string.match(info, "total=%d+"), 7, -1))) or nil used = tonumber(string.format("%.1f",(upload + download))) or nil if string.match(info, "expire=%d+") then day_expire = tonumber(string.sub(string.match(info, "expire=%d+"), 8, -1)) or nil end 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 if total and total > 0 then total = fs.filesize(total) elseif total and total == 0.0 then total = "∞" else total = "null" end used = fs.filesize(used) sub_info = "Successful" else sub_info = "No Sub Info Found" end end end end end luci.http.prepare_content("application/json") luci.http.write_json({ http_code = http_code, sub_info = sub_info, surplus = surplus, used = used, total = total, percent = percent, day_left = day_left, expire = expire, get_time = os.time(); }) end function action_rule_mode() local mode, info, core_type if is_running() then local daip = daip() local dase = dase() or "" local cn_port = cn_port() core_type = uci:get("openclash", "config", "core_type") or "Meta" if not daip or not cn_port then return end info = json.parse(luci.sys.exec(string.format('curl -sL -m 3 -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 = uci:get("openclash", "config", "proxy_mode") or "rule" end else mode = uci:get("openclash", "config", "proxy_mode") or "rule" end luci.http.prepare_content("application/json") luci.http.write_json({ mode = mode, core_type = core_type; }) 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 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 -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; }) else if mode then uci:set("openclash", "config", "proxy_mode", mode) uci:commit("openclash") end end 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 = uci:get("openclash", "config", "operation_mode") if operation_mode == "redir-host" then uci:set("openclash", "config", "en_mode", "redir-host"..mode) elseif operation_mode == "fake-ip" then uci:set("openclash", "config", "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 -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 = uci:get("openclash", "config", "log_level") or "info" end else level = uci:get("openclash", "config", "log_level") or "info" end luci.http.prepare_content("application/json") luci.http.write_json({ log_level = level; }) end function action_switch_log() local level, info if is_running() then local daip = daip() local dase = dase() or "" local cn_port = cn_port() level = luci.http.formvalue("log_level") 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 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPATCH http://"%s":"%s"/configs -d \'{\"log-level\": \"%s\"}\'', dase, daip, cn_port, level)) if info ~= "" then luci.http.status(500, "Switch Faild") end else luci.http.status(500, "Switch Faild") end luci.http.prepare_content("application/json") luci.http.write_json({ info = info; }) 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 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("%s+", "") 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 = load_avg }) 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 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 -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 -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("%s+", "") or "0" 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 = load_avg }) 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_dler_login_info_save() luci.http.prepare_content("application/json") luci.http.write_json({ dler_login_info_save = dler_login_info_save(); }) end function action_dler_info() luci.http.prepare_content("application/json") luci.http.write_json({ dler_info = dler_info(); }) end function action_dler_checkin() luci.http.prepare_content("application/json") luci.http.write_json({ dler_checkin = dler_checkin(); }) end function action_dler_logout() luci.http.prepare_content("application/json") luci.http.write_json({ dler_logout = dler_logout(); }) end function action_dler_login() luci.http.prepare_content("application/json") luci.http.write_json({ dler_login = dler_login(); }) 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 = uci:get("openclash", "config", "dashboard_type") or "Official" local yacd_type = uci:get("openclash", "config", "yacd_type") or "Official" luci.http.prepare_content("application/json") luci.http.write_json({ dashboard_type = dashboard_type, yacd_type = yacd_type; }) 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) == 1 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) == 1 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_op_mode() local op_mode = uci:get("openclash", "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 = uci:get("openclash", "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 = is_running(), 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(), core_type = uci:get("openclash", "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 luci.sys.call("bash /usr/share/openclash/clash_version.sh &") end if not process_status("/usr/share/openclash/openclash_version.sh") then luci.sys.call("bash /usr/share/openclash/openclash_version.sh &") 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 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 = " DBG | INF |level=| WRN | ERR | FTL " local limit = 1000 local start_line = (log_len > 0 and total_lines > log_len) and (log_len + 1) or 1 local 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 ) local core_raw = luci.sys.exec(core_cmd) local 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 ) local oc_raw = luci.sys.exec(oc_cmd) local core_log = "" if core_raw and core_raw ~= "" then local core_logs = {} for line in core_raw:gmatch("[^\n]+") do local line_trans = line if string.match(string.sub(line, 0, 8), "%d%d:%d%d:%d%d") then line_trans = '"'..os.date("%Y-%m-%d", os.time()).. " "..os.date("%H:%M:%S", tonumber(string.sub(line, 0, 8)))..'"'..string.sub(line, 9, -1) end table.insert(core_logs, line_trans) end if #core_logs > 0 then core_log = table.concat(core_logs, "\n") end end local oc_log = "" if oc_raw and oc_raw ~= "" then local oc_logs = {} for line in oc_raw:gmatch("[^\n]+") do local line_trans if not string.find(line, "【") or not string.find(line, "】") then line_trans = trans_line_nolabel(line) else line_trans = trans_line(line) end table.insert(oc_logs, line_trans) end if #oc_logs > 0 then oc_log = table.concat(oc_logs, "\n") end end 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 local old_backup_file_path = "/etc/openclash/backup/" .. old_file_name local new_backup_file_path = "/etc/openclash/backup/" .. new_file_name if fs.rename(old_file_path, new_file_path) then if file_path == "/etc/openclash/config/" then if uci:get("openclash", "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 if fs.isfile(old_backup_file_path) then fs.rename(old_backup_file_path, new_backup_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", "other_rules", 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", "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", "rule_provider_config", 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:foreach("openclash", "game_config", 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", "rule_providers", 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) local line_trans 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 if not string.find (ln, "【") or not string.find (ln, "】") then line_trans = trans_line_nolabel(ln) else line_trans = trans_line(ln) end luci.http.write(line_trans) 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") local line_trans 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 if not string.find (ln, "【") or not string.find (ln, "】") then line_trans = trans_line_nolabel(ln) else line_trans = trans_line(ln) end luci.http.write(line_trans) 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_nolabel(data) if data == nil or data == "" then return "" end local line_trans = "" if string.len(data) >= 19 and string.match(string.sub(data, 0, 19), "%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d") then line_trans = string.sub(data, 0, 20)..luci.i18n.translate(string.sub(data, 21, -1)) else line_trans = luci.i18n.translate(data) end return line_trans end function trans_line(data) if data == nil or data == "" then return "" end local no_trans = {} local line_trans = "" local a = string.find(data, "【") if not a then if string.len(data) >= 19 and string.match(string.sub(data, 0, 19), "%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d") then return string.sub(data, 0, 20) .. luci.i18n.translate(string.sub(data, 21, -1)) else return luci.i18n.translate(data) end end local b_pos = string.find(data, "】") if not b_pos then return luci.i18n.translate(data) end local b = b_pos + 2 local c = 21 local d = 0 local v local x while true do table.insert(no_trans, a) table.insert(no_trans, b) local next_a = string.find(data, "【", b+1) local next_b = string.find(data, "】", b+1) if next_a and next_b then a = next_a b = next_b + 2 else break end end if #no_trans % 2 ~= 0 then table.remove(no_trans) end for k = 1, #no_trans, 2 do x = no_trans[k] v = no_trans[k+1] if x and v then if x <= 21 or not string.match(string.sub(data, 0, 19), "%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d") then line_trans = line_trans .. luci.i18n.translate(string.sub(data, d, x - 1)) .. string.sub(data, x, v) d = v + 1 elseif v <= string.len(data) then line_trans = line_trans .. luci.i18n.translate(string.sub(data, c, x - 1)) .. string.sub(data, x, v) end c = v + 1 end end if c > string.len(data) then if d == 0 then if string.match(string.sub(data, 0, 19), "%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d") then line_trans = string.sub(data, 0, 20) .. line_trans end end else if d == 0 then if string.match(string.sub(data, 0, 19), "%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d") then line_trans = string.sub(data, 0, 20) .. line_trans .. luci.i18n.translate(string.sub(data, c, -1)) end else line_trans = line_trans .. luci.i18n.translate(string.sub(data, c, -1)) 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 -sL -m 10 -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 = 150 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, 100000000) end for name, info in pairs(queries) do if not completed[name] then 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 8 -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 cmd = string.format( 'curl -sL -m 5 --connect-timeout 3 -w "%%{http_code},%%{time_total},%%{time_connect},%%{time_appconnect}" "https://%s/favicon.ico" -o /dev/null 2>/dev/null', domain ) 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_cmd = string.format( 'curl -sI -m 3 --connect-timeout 2 -w "%%{http_code},%%{time_total},%%{time_appconnect}" "https://%s/" -o /dev/null 2>/dev/null', domain ) 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 function get_info_from_uci() local mixed_port = uci:get("openclash", "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) end local config_path = uci:get("openclash", "config", "config_path") if config_path then local config_filename = fs.basename(config_path) local runtime_config_path = "/etc/openclash/" .. config_filename if nixio.fs.access(runtime_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') mixed_port = '' auth_user = '' auth_pass = '' if config if config['mixed-port'] mixed_port = config['mixed-port'].to_s end if config['authentication'] && config['authentication'].is_a?(Array) && !config['authentication'].empty? auth_entry = config['authentication'][0] if auth_entry.is_a?(String) && auth_entry.include?(':') username, password = auth_entry.split(':', 2) auth_user = username || '' auth_pass = password || '' end end end puts \"#{mixed_port},#{auth_user},#{auth_pass}\" rescue puts ',,' end " 2>/dev/null ]], runtime_config_path)):gsub("%s+", "") local runtime_mixed_port, runtime_auth_user, runtime_auth_pass = ruby_result:match("([^,]*),([^,]*),([^,]*)") if runtime_mixed_port and runtime_mixed_port ~= "" then result.mixed_port = runtime_mixed_port else local uci_mixed_port = uci:get("openclash", "config", "mixed_port") if uci_mixed_port and uci_mixed_port ~= "" then result.mixed_port = uci_mixed_port else result.mixed_port = "7893" end end if runtime_auth_user and runtime_auth_user ~= "" and runtime_auth_pass and runtime_auth_pass ~= "" then result.auth_user = runtime_auth_user result.auth_pass = runtime_auth_pass else 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) end else get_info_from_uci() end else get_info_from_uci() 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 function get_uci_settings() local meta_sniffer = uci:get("openclash", "config", "enable_meta_sniffer") if meta_sniffer == "1" then result.meta_sniffer = "1" end local respect_rules = uci:get("openclash", "config", "enable_respect_rules") if respect_rules == "1" then result.respect_rules = "1" end end if is_running() then local config_path = uci:get("openclash", "config", "config_path") if config_path then local config_filename = fs.basename(config_path) local runtime_config_path = "/etc/openclash/" .. config_filename if nixio.fs.access(runtime_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') if config sniffer_enabled = config['sniffer'] && config['sniffer']['enable'] ? '1' : '0' respect_rules_enabled = config['dns'] && config['dns']['respect-rules'] == true ? '1' : '0' puts \"#{sniffer_enabled},#{respect_rules_enabled}\" else puts '0,0' end rescue puts '0,0' end " 2>/dev/null ]], runtime_config_path)):gsub("%s+", "") local sniffer_result, respect_rules_result = ruby_result:match("(%d),(%d)") if sniffer_result and respect_rules_result then result.meta_sniffer = sniffer_result result.respect_rules = respect_rules_result else get_uci_settings() end else get_uci_settings() end else get_uci_settings() end else get_uci_settings() end local oversea = uci:get("openclash", "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 = uci:get("openclash", "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 = uci:get("openclash", "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 -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 uci:set("openclash", "config", "enable_meta_sniffer", value) uci:set("openclash", "config", "enable_meta_sniffer_pure_ip", value) uci:commit("openclash") 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 elseif setting == "respect_rules" then uci:set("openclash", "config", "enable_respect_rules", value) uci:commit("openclash") 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 elseif setting == "oversea" then uci:set("openclash", "config", "china_ip_route", value) uci:commit("openclash") if is_running() then 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 uci:get("openclash", "config", "stream_auto_select_interval") then uci:set("openclash", "config", "stream_auto_select_interval", "10") end if not uci:get("openclash", "config", "stream_auto_select_logic") then uci:set("openclash", "config", "stream_auto_select_logic", "Urltest") end if not uci:get("openclash", "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 uci:get("openclash", "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 uci:get("openclash", "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 uci:get("openclash", "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 = "" local auth_exists = false local function get_auth_from_uci() 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 auth_exists = true return false end end) end local config_path = uci:get("openclash", "config", "config_path") if config_path then local config_filename = fs.basename(config_path) local runtime_config_path = "/etc/openclash/" .. config_filename if nixio.fs.access(runtime_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') if config && config['authentication'] && config['authentication'].is_a?(Array) && !config['authentication'].empty? auth_entry = config['authentication'][0] if auth_entry.is_a?(String) && auth_entry.include?(':') username, password = auth_entry.split(':', 2) puts \"#{username},#{password}\" else puts ',' end else puts ',' end rescue puts ',' end " 2>/dev/null ]], runtime_config_path)):gsub("%s+", "") local runtime_user, runtime_pass = ruby_result:match("([^,]*),([^,]*)") if runtime_user and runtime_user ~= "" and runtime_pass and runtime_pass ~= "" then auth_user = runtime_user auth_pass = runtime_pass auth_exists = true else get_auth_from_uci() end else get_auth_from_uci() end else get_auth_from_uci() end local proxy_ip = daip() local mixed_port = uci:get("openclash", "config", "mixed_port") or "7893" if not proxy_ip then result.error = "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/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 nixio.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 = "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 = "warning: 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 nixio.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 nixio.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 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 nixio.fs.access(config_path) then luci.http.status(404, "Config file not found") return end uci:set("openclash", "config", "config_path", config_path) end if action == "start" then uci:set("openclash", "config", "enable", "1") uci:commit("openclash") luci.sys.call("/etc/init.d/openclash start >/dev/null 2>&1") elseif action == "stop" then uci:set("openclash", "config", "enable", "0") uci:commit("openclash") 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 uci:set("openclash", "config", "enable", "1") uci:commit("openclash") 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 = uci:get("openclash", "config", "config_path") if config_path then current_config = config_path end local config_dir = "/etc/openclash/config/" if nixio.fs.access(config_dir) then for file in nixio.fs.dir(config_dir) do local full_path = config_dir .. file local stat = nixio.fs.stat(full_path) if stat and stat.type == "reg" 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 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 string.find(filename, "[/\\]") or string.find(filename, "%.%.") then luci.http.write_json({ status = "error", message = "Invalid filename characters" }) 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, 1000) 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 nixio.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 is_overwrite = (config_file == "/etc/openclash/custom/openclash_custom_overwrite.sh") if not is_overwrite then if string.match(config_file, "^/etc/openclash/[^/%.]+%.ya?ml$") then local stat = nixio.fs.stat(config_file) if stat and stat.type == "reg" then 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) or "" 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) } }) return else 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 end if not string.match(config_file, "^/etc/openclash/config/[^/%.]+%.ya?ml$") then luci.http.prepare_content("application/json") luci.http.write_json({ status = "error", message = "Invalid config file path" }) return end end if not nixio.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 local stat = nixio.fs.stat(config_file) if not stat or stat.type ~= "reg" 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") if not is_overwrite then if not string.match(config_file, "^/etc/openclash/config/[^/%.]+%.ya?ml$") then luci.http.prepare_content("application/json") luci.http.write_json({ status = "error", message = "Invalid config 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 nixio.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 = nixio.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 "https://api.dler.io/sub" 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 exists = false uci:foreach("openclash", "config_subscribe", function(s) if s.name == name then exists = true return false end end) if exists then luci.http.write_json({ status = "error", message = "Subscription with this name already exists" }) return 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 = uci:add("openclash", "config_subscribe") 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) uci:set("openclash", section_id, "convert_address", convert_address) uci:set("openclash", section_id, "template", template) 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) if custom_params and custom_params ~= "" 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 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 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 if de_ex_keyword and de_ex_keyword ~= "" then local de_ex_keywords = {} for line in de_ex_keyword:gmatch("[^\n]+") do local de_ex_kw = line:match("^%s*(.-)%s*$") if de_ex_kw and de_ex_kw ~= "" then table.insert(de_ex_keywords, de_ex_kw) end end if #de_ex_keywords > 0 then for i, de_ex_kw in ipairs(de_ex_keywords) do uci:set_list("openclash", section_id, "de_ex_keyword", de_ex_kw) end end end uci:commit("openclash") luci.http.write_json({ status = "success", message = "Subscription added successfully", 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 subscription configuration" }) end end