From ccd8763bcd19cae353b3fc59686fa17e7514291b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 18 Jul 2026 20:20:11 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=90=20Sync=202026-07-18=2020:20:11?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- luci-app-daede/Makefile | 2 +- .../luci-static/resources/view/daede/daed.js | 2 +- luci-app-daede/po/zh-cn/daede.po | 3 + luci-app-daede/po/zh_Hans/daede.po | 3 + .../share/luci-app-daede/daed-sub-update.sh | 14 +- luci-app-openclash/Makefile | 4 +- .../luasrc/controller/openclash.lua | 1741 +- .../luasrc/model/cbi/openclash/client.lua | 2 +- .../model/cbi/openclash/config-overwrite.lua | 10 +- .../cbi/openclash/config-subscribe-edit.lua | 17 +- .../luasrc/model/cbi/openclash/config.lua | 5 +- .../model/cbi/openclash/other-file-edit.lua | 1 + .../luasrc/model/cbi/openclash/settings.lua | 57 +- luci-app-openclash/luasrc/openclash.lua | 13 + .../luasrc/view/openclash/common.htm | 50 + .../luasrc/view/openclash/config_edit.htm | 402 +- .../luasrc/view/openclash/config_editor.htm | 266 +- .../view/openclash/config_merge_editor.htm | 472 + .../luasrc/view/openclash/config_upload.htm | 317 +- .../luasrc/view/openclash/debug.htm | 2 + .../luasrc/view/openclash/developer.htm | 2 +- .../luasrc/view/openclash/log.htm | 184 +- .../luasrc/view/openclash/myip.htm | 16 +- .../luasrc/view/openclash/oix_login.htm | 727 +- .../luasrc/view/openclash/oixcloud.htm | 11 +- .../view/openclash/other_stream_option.htm | 55 +- .../luasrc/view/openclash/select_git_cdn.htm | 61 +- .../luasrc/view/openclash/status.htm | 827 +- .../luasrc/view/openclash/sub_info_show.htm | 59 +- .../view/openclash/subconverter_version.htm | 41 + .../luasrc/view/openclash/tblsection.htm | 27 +- .../luasrc/view/openclash/toolbar_show.htm | 35 +- .../luasrc/view/openclash/update.htm | 40 +- .../luasrc/view/openclash/update_config.htm | 17 +- luci-app-openclash/po/es/openclash.es.po | 419 +- .../po/zh-cn/openclash.zh-cn.po | 311 +- luci-app-openclash/root/etc/init.d/openclash | 167 +- .../openclash_custom_chnroute6_pass.list | 10 +- .../openclash_custom_chnroute_pass.list | 10 +- .../openclash_custom_domain_dns_policy.list | 10 +- ...nclash_custom_proxy_server_dns_policy.list | 8 +- .../root/etc/openclash/overwrite/default | 2 +- .../root/usr/share/openclash/YAML.rb | 197 +- .../root/usr/share/openclash/clash_version.sh | 3 +- .../root/usr/share/openclash/openclash.sh | 28 +- .../usr/share/openclash/openclash_core.sh | 9 +- .../usr/share/openclash/openclash_curl.sh | 45 +- .../usr/share/openclash/openclash_debug.sh | 76 +- .../openclash/openclash_download_dashboard.sh | 116 +- .../openclash/openclash_streaming_unlock.lua | 30 +- .../usr/share/openclash/openclash_update.sh | 2 +- .../usr/share/openclash/openclash_watchdog.sh | 40 +- .../root/usr/share/openclash/yml_change.sh | 6 +- .../usr/share/openclash/yml_rules_change.sh | 6 +- .../resources/openclash/css/oc.css | 13899 ++++++++-------- ...sb_Bp.woff2 => TwemojiMozilla-flags.woff2} | Bin .../resources/openclash/img/oixcloud.png | Bin 0 -> 14905 bytes .../resources/openclash/js/cm6.min.js | 224 +- .../resources/openclash/js/common.js | 669 +- .../resources/openclash/js/oc-icons.js | 68 + .../openclash/js/subconverter-version.js | 388 + luci-app-openclash/tools/codemirror/entry.js | 2084 +-- .../tools/codemirror/package.json | 60 +- luci-app-ssr-plus/Makefile | 2 +- .../servers_subscribe_url_style.htm | 2 +- natflow/Makefile | 4 +- 66 files changed, 14022 insertions(+), 10358 deletions(-) create mode 100644 luci-app-openclash/luasrc/view/openclash/common.htm create mode 100644 luci-app-openclash/luasrc/view/openclash/config_merge_editor.htm create mode 100644 luci-app-openclash/luasrc/view/openclash/subconverter_version.htm rename luci-app-openclash/root/www/luci-static/resources/openclash/fonts/{TwemojiMozilla-flags-B12sb_Bp.woff2 => TwemojiMozilla-flags.woff2} (100%) create mode 100644 luci-app-openclash/root/www/luci-static/resources/openclash/img/oixcloud.png create mode 100644 luci-app-openclash/root/www/luci-static/resources/openclash/js/oc-icons.js create mode 100644 luci-app-openclash/root/www/luci-static/resources/openclash/js/subconverter-version.js diff --git a/luci-app-daede/Makefile b/luci-app-daede/Makefile index a96cee009..d9f6c1614 100644 --- a/luci-app-daede/Makefile +++ b/luci-app-daede/Makefile @@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-daede PKG_VERSION:=1.14.7 -PKG_RELEASE:=32 +PKG_RELEASE:=33 PKG_MAINTAINER:=kenzok8 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) diff --git a/luci-app-daede/htdocs/luci-static/resources/view/daede/daed.js b/luci-app-daede/htdocs/luci-static/resources/view/daede/daed.js index c6e760973..b24094aa2 100644 --- a/luci-app-daede/htdocs/luci-static/resources/view/daede/daed.js +++ b/luci-app-daede/htdocs/luci-static/resources/view/daede/daed.js @@ -99,7 +99,7 @@ function renderDaedSettings() { const err = (res.stderr || res.stdout || ('exit ' + res.code)).trim().split('\n')[0]; flash(_('Subscription update failed: %s').format(err), 'err', 9000); } else { - flash(_('Subscriptions updated'), 'ok'); + flash(_('Subscriptions updated (daed restarted)'), 'ok'); } }).catch(function(e) { flash(_('Subscription update failed: %s').format(e.message || e), 'err', 9000); diff --git a/luci-app-daede/po/zh-cn/daede.po b/luci-app-daede/po/zh-cn/daede.po index da1067771..e15506436 100644 --- a/luci-app-daede/po/zh-cn/daede.po +++ b/luci-app-daede/po/zh-cn/daede.po @@ -628,6 +628,9 @@ msgstr "更新失败:%s" msgid "Subscriptions updated (dae reloaded)" msgstr "订阅已更新(dae 已重载)" +msgid "Subscriptions updated (daed restarted)" +msgstr "订阅已更新,daed 已重启" + msgid "Section" msgstr "区块" diff --git a/luci-app-daede/po/zh_Hans/daede.po b/luci-app-daede/po/zh_Hans/daede.po index da1067771..e15506436 100644 --- a/luci-app-daede/po/zh_Hans/daede.po +++ b/luci-app-daede/po/zh_Hans/daede.po @@ -628,6 +628,9 @@ msgstr "更新失败:%s" msgid "Subscriptions updated (dae reloaded)" msgstr "订阅已更新(dae 已重载)" +msgid "Subscriptions updated (daed restarted)" +msgstr "订阅已更新,daed 已重启" + msgid "Section" msgstr "区块" diff --git a/luci-app-daede/root/usr/share/luci-app-daede/daed-sub-update.sh b/luci-app-daede/root/usr/share/luci-app-daede/daed-sub-update.sh index 1330aaa19..997838f8a 100755 --- a/luci-app-daede/root/usr/share/luci-app-daede/daed-sub-update.sh +++ b/luci-app-daede/root/usr/share/luci-app-daede/daed-sub-update.sh @@ -115,4 +115,16 @@ while IFS= read -r id || [ -n "$id" ]; do done < "$TMPDIR/ids" log "finished daed subscription update: ok=$ok fail=$fail total=$count" -[ "$fail" -eq 0 ] + +if [ "$fail" -ne 0 ]; then + exit 1 +fi + +if [ "$count" -gt 0 ] && [ "$ok" -gt 0 ] && /bin/pidof daed >/dev/null 2>&1; then + log "restarting daed to apply updated subscriptions" + if ! /etc/init.d/daed restart >>"$LOG" 2>&1; then + fail 8 "daed restart failed after subscription update" + fi +fi + +exit 0 diff --git a/luci-app-openclash/Makefile b/luci-app-openclash/Makefile index c2a96c470..8d2c91b1e 100644 --- a/luci-app-openclash/Makefile +++ b/luci-app-openclash/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-openclash -PKG_VERSION:=0.47.116 -PKG_RELEASE:=6 +PKG_VERSION:=0.47.133 +PKG_RELEASE:=7 PKG_MAINTAINER:=vernesong PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) diff --git a/luci-app-openclash/luasrc/controller/openclash.lua b/luci-app-openclash/luasrc/controller/openclash.lua index 4cd932fae..95a2cfd75 100644 --- a/luci-app-openclash/luasrc/controller/openclash.lua +++ b/luci-app-openclash/luasrc/controller/openclash.lua @@ -11,6 +11,7 @@ function index() 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", "conn_status"},call("action_conn_status")).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")) @@ -96,6 +97,7 @@ function index() entry({"admin", "services", "openclash", "config_file_save"}, call("action_config_file_save")) entry({"admin", "services", "openclash", "upload_config"}, call("action_upload_config")) entry({"admin", "services", "openclash", "add_subscription"}, call("action_add_subscription")) + entry({"admin", "services", "openclash", "subconverter_version"}, call("action_subconverter_version")) entry({"admin", "services", "openclash", "generate_age_key"}, call("action_generate_age_key")) entry({"admin", "services", "openclash", "cal_age_public_key"}, call("action_cal_age_public_key")) entry({"admin", "services", "openclash", "add_age_config"}, call("action_add_age_config")) @@ -110,15 +112,21 @@ function index() entry({"admin", "services", "openclash", "oix_logout"}, call("oix_logout")) entry({"admin", "services", "openclash", "oix_login"}, call("oix_login")) entry({"admin", "services", "openclash", "oix_login_info_save"}, call("oix_login_info_save")) + entry({"admin", "services", "openclash", "oix_params_sync"}, call("oix_params_sync")) + entry({"admin", "services", "openclash", "oix_params_get"}, call("oix_params_get")) end +local SYS = require "luci.sys" +local HTTP = require "luci.http" +local DISP = require "luci.dispatcher" +local UTIL = require "luci.util" 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'") +local device_arh = SYS.exec("uname -m |tr -d '\n'") if pcall(require, "luci.model.ipkg") then opkg = require "luci.model.ipkg" @@ -134,9 +142,12 @@ else end local function is_running() - return luci.sys.call("pidof clash >/dev/null") == 0 + return SYS.call("pidof clash >/dev/null") == 0 end +local CONFIG_PATH_PREFIX = "/etc/openclash/config/" +local CONFIG_PATH_PREFIX_LEN = #CONFIG_PATH_PREFIX + 1 + local function is_start() return process_status("/etc/init.d/openclash") end @@ -170,14 +181,14 @@ local function db_foward_ssl() 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") + SYS.exec("bash /usr/share/openclash/openclash_version.sh 2>/dev/null") + return SYS.exec("sed -n '/^https:/,$p' /tmp/openclash_last_version 2>/dev/null") end local function startlog() local info = "" if fs.access("/tmp/openclash_start.log") then - info = luci.sys.exec("sed -n '$p' /tmp/openclash_start.log 2>/dev/null") + info = SYS.exec("sed -n '$p' /tmp/openclash_start.log 2>/dev/null") if string.len(info) > 0 then info = trans_line(info) end @@ -190,9 +201,9 @@ local function coremodel() return opkg.info("libc")["libc"]["Architecture"] else if fs.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") + return 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("rm -f /lib/apk/db/lock && apk list libc 2>/dev/null |awk '{print $2}'") + return SYS.exec("rm -f /lib/apk/db/lock && apk list libc 2>/dev/null |awk '{print $2}'") end end end @@ -205,12 +216,61 @@ local function check_core() end end +local function parse_subconverter_version_url(raw_url) + raw_url = (raw_url or ""):gsub("^%s+", ""):gsub("%s+$", "") + if raw_url == "" then return nil, "empty" end + if not raw_url:match("^[a-zA-Z][a-zA-Z0-9+%.%-]*://") then + raw_url = "https://" .. raw_url + end + + local scheme, authority, path = raw_url:match("^(https?)://([^/?#]+)([^?#]*)") + if not scheme or not authority then return nil, "invalid" end + if authority:find("@", 1, true) then return nil, "invalid" end + if authority:match("%s") then return nil, "invalid" end + if raw_url:find("?", 1, true) or raw_url:find("#", 1, true) then return nil, "invalid" end + + local host, port + if authority:sub(1, 1) == "[" then + host, port = authority:match("^(%[[0-9a-fA-F:%.]+%]):?(%d*)$") + else + host, port = authority:match("^([^:]+):?(%d*)$") + end + if not host or host == "" then return nil, "invalid" end + if port and port ~= "" then + port = tonumber(port) + if not port or port < 1 or port > 65535 then return nil, "invalid" end + end + + path = (path and path ~= "") and path or "/" + if path ~= "/version" and not path:match("/version$") then return nil, "invalid" end + + return scheme .. "://" .. authority .. path +end + +local function sanitize_subconverter_version_text(text) + text = tostring(text or ""):gsub("\r", "\n") + local lines = {} + for line in text:gmatch("[^\n]+") do + line = line:gsub("^%s+", ""):gsub("%s+$", "") + if line ~= "" then lines[#lines + 1] = line end + end + text = table.concat(lines, " "):gsub("%s+", " "):gsub("^%s+", ""):gsub("%s+$", "") + if text == "" or #text > 220 then return "" end + if text:match("<%s*/?%s*[%a!][^>]*>") then + return "" + end + if not text:lower():match("subconverter") and not text:lower():match("backend") and not text:lower():match("version") and not text:match("v?%d+%.%d+") then + return "" + end + return text +end + local function coremetacv() local v = "0" if not fs.access(meta_core_path) then return v else - v = luci.sys.exec(string.format("%s -v 2>/dev/null |awk -F ' ' '{print $3}' |head -1 |tr -d '\n'", meta_core_path)) + v = 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 @@ -225,11 +285,11 @@ local function corelv() local oix_token = fs.uci_get_config("config", "oix_token") or "" if fs.access("/tmp/clash_last_version") then if core_type == "Oix" or oix_token ~= "" then - core_meta_lv = luci.sys.exec("sed -n 1p /tmp/clash_last_version 2>/dev/null |tr -d '\n'") + core_meta_lv = SYS.exec("sed -n 1p /tmp/clash_last_version 2>/dev/null |tr -d '\n'") elseif core_smart_enable == "1" then - core_meta_lv = luci.sys.exec("sed -n 2p /tmp/clash_last_version 2>/dev/null |tr -d '\n'") + core_meta_lv = 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'") + core_meta_lv = SYS.exec("sed -n 1p /tmp/clash_last_version 2>/dev/null |tr -d '\n'") end else core_meta_lv = "loading..." @@ -246,9 +306,9 @@ local function opcv() v = info["luci-app-openclash"]["Version"] else if fs.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'") + v = 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("rm -f /lib/apk/db/lock && apk list luci-app-openclash 2>/dev/null|grep 'installed' | grep -oE '[0-9]+(\\.[0-9]+)*' | head -1 |tr -d '\n'") + v = SYS.exec("rm -f /lib/apk/db/lock && 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 @@ -262,7 +322,7 @@ local function oplv() local oplv = "" if fs.access("/tmp/openclash_last_version") then - oplv = luci.sys.exec("sed -n 1p /tmp/openclash_last_version 2>/dev/null |tr -d '\n'") + oplv = SYS.exec("sed -n 1p /tmp/openclash_last_version 2>/dev/null |tr -d '\n'") else oplv = "loading..." end @@ -272,14 +332,14 @@ local function oplv() end local function opup() - return luci.sys.call("bash /usr/share/openclash/openclash_update.sh >/dev/null 2>&1 &") + return SYS.call("bash /usr/share/openclash/openclash_update.sh >/dev/null 2>&1 &") end local function coreup() uci:set("openclash", "config", "enable", "1") uci:commit("openclash") - local type = luci.http.formvalue("core_type") - return luci.sys.call(string.format("/usr/share/openclash/openclash_core.sh '%s' >/dev/null 2>&1 &", type)) + local type = HTTP.formvalue("core_type") + return SYS.call(string.format("/usr/share/openclash/openclash_core.sh '%s' >/dev/null 2>&1 &", type)) end local function corever() @@ -295,14 +355,14 @@ local function smart_enable() end local function save_corever_branch() - if luci.http.formvalue("core_ver") then - uci:set("openclash", "config", "core_version", luci.http.formvalue("core_ver")) + if HTTP.formvalue("core_ver") then + uci:set("openclash", "config", "core_version", HTTP.formvalue("core_ver")) end - if luci.http.formvalue("release_branch") then - uci:set("openclash", "config", "release_branch", luci.http.formvalue("release_branch")) + if HTTP.formvalue("release_branch") then + uci:set("openclash", "config", "release_branch", HTTP.formvalue("release_branch")) end - if luci.http.formvalue("smart_enable") then - uci:set("openclash", "config", "smart_enable", luci.http.formvalue("smart_enable")) + if HTTP.formvalue("smart_enable") then + uci:set("openclash", "config", "smart_enable", HTTP.formvalue("smart_enable")) end uci:commit("openclash") return "success" @@ -324,11 +384,11 @@ local function upchecktime() end function core_download() - local cdn_url = luci.http.formvalue("url") + local cdn_url = HTTP.formvalue("url") if cdn_url then - luci.sys.call(string.format("bash /usr/share/openclash/openclash_core.sh 'Meta' '%s' >/dev/null 2>&1 &", cdn_url)) + SYS.call(string.format("bash /usr/share/openclash/openclash_core.sh 'Meta' '%s' >/dev/null 2>&1 &", cdn_url)) else - luci.sys.call("bash /usr/share/openclash/openclash_core.sh 'Meta' >/dev/null 2>&1 &") + SYS.call("bash /usr/share/openclash/openclash_core.sh 'Meta' >/dev/null 2>&1 &") end end @@ -340,11 +400,11 @@ function action_flush_dns_cache() local dase = dase() or "" local cn_port = cn_port() if not daip or not cn_port then return end - fake_ip_state = luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPOST http://"%s":"%s"/cache/fakeip/flush', dase, daip, cn_port)) - dns_state = luci.sys.exec(string.format('curl -sL -m 3 --retry 2-H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPOST http://"%s":"%s"/cache/dns/flush', dase, daip, cn_port)) + fake_ip_state = SYS.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPOST http://"%s":"%s"/cache/fakeip/flush', dase, daip, cn_port)) + dns_state = SYS.exec(string.format('curl -sL -m 3 --retry 2-H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPOST http://"%s":"%s"/cache/dns/flush', dase, daip, cn_port)) end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ flush_status = dns_state; }) end @@ -356,38 +416,38 @@ function action_flush_smart_cache() local dase = dase() or "" local cn_port = cn_port() if not daip or not cn_port then return end - flush_state = luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPOST http://"%s":"%s"/cache/smart/flush', dase, daip, cn_port)) + flush_state = SYS.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPOST http://"%s":"%s"/cache/smart/flush', dase, daip, cn_port)) end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ flush_status = flush_state; }) end function action_update_config() -- filename is basename - local filename = luci.http.formvalue("filename") + local filename = HTTP.formvalue("filename") - luci.http.prepare_content("application/json") + HTTP.prepare_content("application/json") if not filename then - luci.http.write_json({ + 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)) + local update_result = SYS.call(string.format("/usr/share/openclash/openclash.sh '%s' >/dev/null 2>&1", filename)) if update_result == 0 then - luci.http.write_json({ + HTTP.write_json({ status = "success", message = "Config update started successfully", filename = filename }) else - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Failed to update config" }) @@ -397,31 +457,31 @@ end function action_restore_config() uci:set("openclash", "config", "enable", "0") uci:commit("openclash") - luci.sys.call("mkdir -p /etc/openclash/custom >/dev/null 2>&1") - luci.sys.call("mkdir -p /etc/openclash/overwrite >/dev/null 2>&1") - luci.sys.call("mkdir -p /etc/openclash/rule_provider >/dev/null 2>&1") - luci.sys.call("/etc/init.d/openclash stop >/dev/null 2>&1") - luci.sys.call("cp -f /usr/share/openclash/backup/openclash /etc/config/openclash >/dev/null 2>&1 &") - luci.sys.call("cp -f /usr/share/openclash/backup/openclash_custom* /etc/openclash/custom/ >/dev/null 2>&1 &") - luci.sys.call("cp -f /usr/share/openclash/backup/openclash_force_sniffing* /etc/openclash/custom/ >/dev/null 2>&1 &") - luci.sys.call("cp -f /usr/share/openclash/backup/openclash_sniffing* /etc/openclash/custom/ >/dev/null 2>&1 &") - luci.sys.call("cp -f /usr/share/openclash/backup/china_ip_route.ipset /etc/openclash/china_ip_route.ipset >/dev/null 2>&1 &") - luci.sys.call("cp -f /usr/share/openclash/backup/china_ip6_route.ipset /etc/openclash/china_ip6_route.ipset >/dev/null 2>&1 &") - luci.sys.call("cp -f /usr/share/openclash/backup/overwrite/default /etc/openclash/overwrite/default >/dev/null 2>&1 &") - luci.sys.call("cp -f /usr/share/openclash/backup/oc-cn-domain.mrs /etc/openclash/rule_provider/oc-cn-domain.mrs >/dev/null 2>&1 &") - luci.sys.call("rm -rf /etc/openclash/history/* >/dev/null 2>&1 &") + SYS.call("mkdir -p /etc/openclash/custom >/dev/null 2>&1") + SYS.call("mkdir -p /etc/openclash/overwrite >/dev/null 2>&1") + SYS.call("mkdir -p /etc/openclash/rule_provider >/dev/null 2>&1") + SYS.call("/etc/init.d/openclash stop >/dev/null 2>&1") + SYS.call("cp -f /usr/share/openclash/backup/openclash /etc/config/openclash >/dev/null 2>&1 &") + SYS.call("cp -f /usr/share/openclash/backup/openclash_custom* /etc/openclash/custom/ >/dev/null 2>&1 &") + SYS.call("cp -f /usr/share/openclash/backup/openclash_force_sniffing* /etc/openclash/custom/ >/dev/null 2>&1 &") + SYS.call("cp -f /usr/share/openclash/backup/openclash_sniffing* /etc/openclash/custom/ >/dev/null 2>&1 &") + SYS.call("cp -f /usr/share/openclash/backup/china_ip_route.ipset /etc/openclash/china_ip_route.ipset >/dev/null 2>&1 &") + SYS.call("cp -f /usr/share/openclash/backup/china_ip6_route.ipset /etc/openclash/china_ip6_route.ipset >/dev/null 2>&1 &") + SYS.call("cp -f /usr/share/openclash/backup/overwrite/default /etc/openclash/overwrite/default >/dev/null 2>&1 &") + SYS.call("cp -f /usr/share/openclash/backup/oc-cn-domain.mrs /etc/openclash/rule_provider/oc-cn-domain.mrs >/dev/null 2>&1 &") + 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") + SYS.call("rm -rf /etc/openclash/core/* >/dev/null 2>&1") end function action_one_key_update() - local cdn_url = luci.http.formvalue("url") + local cdn_url = HTTP.formvalue("url") if cdn_url then - return luci.sys.call(string.format("bash /usr/share/openclash/openclash_update.sh 'one_key_update' '%s' >/dev/null 2>&1 &", cdn_url)) + return SYS.call(string.format("bash /usr/share/openclash/openclash_update.sh 'one_key_update' '%s' >/dev/null 2>&1 &", cdn_url)) else - return luci.sys.call("bash /usr/share/openclash/openclash_update.sh 'one_key_update' >/dev/null 2>&1 &") + return SYS.call("bash /usr/share/openclash/openclash_update.sh 'one_key_update' >/dev/null 2>&1 &") end end @@ -434,28 +494,25 @@ local function config_name() e[t].name=fs.basename(o) end end - return json.parse(json.stringify(e)) or e + return e end local function config_path() - if fs.uci_get_config("config", "config_path") then - return string.sub(fs.uci_get_config("config", "config_path"), 23, -1) - else - return "" - end + local p = fs.uci_get_config("config", "config_path") + return p and string.sub(p, CONFIG_PATH_PREFIX_LEN) or "" end function action_switch_config() - local config_file = luci.http.formvalue("config_file") - local config_name = luci.http.formvalue("config_name") + local config_file = HTTP.formvalue("config_file") + local config_name = 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({ + HTTP.prepare_content("application/json") + HTTP.write_json({ status = "error", message = "No config file specified" }) @@ -463,8 +520,8 @@ function action_switch_config() end if not fs.access(config_file) then - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ status = "error", message = "Config file does not exist: " .. config_file }) @@ -475,10 +532,10 @@ function action_switch_config() uci:set("openclash", "config", "enable", "1") uci:commit("openclash") - luci.sys.call("/etc/init.d/openclash restart >/dev/null 2>&1 &") + SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1 &") - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ status = "success", message = "Config file switched successfully", config_file = config_file @@ -487,8 +544,8 @@ end function set_subinfo_url() local filename, url, info - filename = luci.http.formvalue("filename") - url = luci.http.formvalue("url") + filename = HTTP.formvalue("filename") + url = HTTP.formvalue("url") if not filename then info = "Oops: The config file name seems to be incorrect" end @@ -527,7 +584,7 @@ function set_subinfo_url() local url_list = {} for line in string.gmatch(url, "[^\n]+") do if line ~= "" then - table.insert(url_list, line) + url_list[#url_list + 1] = line end end uci:section("openclash", "subscribe_info", nil, {name = filename, url = url_list}) @@ -536,23 +593,37 @@ function set_subinfo_url() end end end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ info = info; }) end -function fetch_sub_info(sub_url, sub_ua) +function fetch_sub_info(sub_url, sub_ua, sub_headers, raw_info) local info, upload, download, total, day_expire, http_code local used, expire, day_left, percent, surplus - info = luci.sys.exec(string.format("curl -sLI -X GET -m 5 --retry 2 -w 'http_code=%%{http_code}' -H 'User-Agent: %s' '%s'", sub_ua, sub_url)) - local http_match = string.match(info, "http_code=(%d+)") - if not info or not http_match or tonumber(http_match) ~= 200 then - info = luci.sys.exec(string.format("curl -sLI -X GET -m 5 --retry 2 -w 'http_code=%%{http_code}' -H 'User-Agent: Quantumultx' '%s'", sub_url)) - http_match = string.match(info, "http_code=(%d+)") + if raw_info then + info = raw_info + else + local header_args = "" + if sub_headers and sub_headers ~= "" then + for hdr in sub_headers:gmatch("[^\n]+") do + hdr = hdr:match("^%s*(.-)%s*$") + if hdr and hdr ~= "" then + header_args = header_args .. string.format(" -H '%s'", hdr:gsub("'", "'\\''")) + end + end + end + + info = SYS.exec(string.format("curl -sLI -X GET -m 5 --retry 2 -w 'http_code=%%{http_code}' -H 'User-Agent: %s'%s '%s'", sub_ua, header_args, sub_url)) + local http_match = string.match(info, "http_code=(%d+)") + if not info or not http_match or tonumber(http_match) ~= 200 then + info = SYS.exec(string.format("curl -sLI -X GET -m 5 --retry 2 -w 'http_code=%%{http_code}' -H 'User-Agent: Quantumultx'%s '%s'", header_args, sub_url)) + end end + local http_match = string.match(info, "http_code=(%d+)") if info and http_match then http_code = http_match if tonumber(http_code) == 200 then @@ -669,21 +740,23 @@ function get_sub_url(filename) function(s) if s.name == filename and s.url then if type(s.url) == "table" then - info_tb = s.url + for _, v in ipairs(s.url) do + info_tb[#info_tb + 1] = v + end else - string.gsub(s.url, '[^\n]+', function(w) table.insert(info_tb, w) end) + string.gsub(s.url, '[^\n]+', function(w) info_tb[#info_tb + 1] = w end) end if #info_tb == 1 then local url, name = parse_url_with_name(info_tb[1], filename) if url ~= info_tb[1] then - table.insert(providers, {name = name, url = url}) + providers[#providers + 1] = {name = name, url = url} else sub_url = url end elseif #info_tb > 1 then for _, raw in ipairs(info_tb) do local url, name = parse_url_with_name(raw, filename) - table.insert(providers, {name = name, url = url}) + providers[#providers + 1] = {name = name, url = url} end end return false @@ -703,7 +776,7 @@ function get_sub_url(filename) local config_path = "/etc/openclash/config/" .. fs.basename(filename .. ".yaml") if fs.access(config_path) then - local ruby_result = luci.sys.exec(string.format([[ + local ruby_result = SYS.exec(string.format([[ ruby -ryaml -rYAML -I "/usr/share/openclash" -E UTF-8 -e ' begin config = YAML.load_file("%s") @@ -748,14 +821,14 @@ function get_sub_url(filename) 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) + string.gsub(s.address, '[^\n]+', function(w) info_tb[#info_tb + 1] = w end) if #info_tb == 1 then local url, _ = parse_url_with_name(info_tb[1], filename) sub_url = url elseif #info_tb > 1 then for _, raw in ipairs(info_tb) do local url, name = parse_url_with_name(raw, filename) - table.insert(providers, {name = name, url = url}) + providers[#providers + 1] = {name = name, url = url} end end end @@ -774,17 +847,24 @@ function get_sub_url(filename) end function sub_info_get() - local sub_ua, filename, sub_info, url_result + local sub_ua, sub_headers, filename, sub_info, url_result local providers_data = {} - filename = luci.http.formvalue("filename") + filename = HTTP.formvalue("filename") sub_info = "" sub_ua = "Clash" + sub_headers = "" uci:foreach("openclash", "config_subscribe", function(s) - if s.name == filename and s.sub_ua then - sub_ua = s.sub_ua + if s.name == filename then + if s.sub_ua then + sub_ua = s.sub_ua + end + local raw = uci:get_list("openclash", s['.name'], "sub_headers") + if raw then + sub_headers = table.concat(raw, "\n") + end return false end end @@ -795,16 +875,109 @@ function sub_info_get() if url_result then if url_result.type == "single" then - local info = fetch_sub_info(url_result.url, sub_ua) + local info = fetch_sub_info(url_result.url, sub_ua, sub_headers) if info then - table.insert(providers_data, info) + providers_data[#providers_data + 1] = info end - else + elseif #url_result.providers <= 1 then for i, provider in ipairs(url_result.providers) do - local info = fetch_sub_info(provider.url, sub_ua) + local info = fetch_sub_info(provider.url, sub_ua, sub_headers) if info then info.provider_name = provider.name - table.insert(providers_data, info) + providers_data[#providers_data + 1] = info + end + end + else + local header_args = "" + if sub_headers and sub_headers ~= "" then + for hdr in sub_headers:gmatch("[^\n]+") do + hdr = hdr:match("^%s*(.-)%s*$") + if hdr and hdr ~= "" then + header_args = header_args .. string.format(" -H '%s'", hdr:gsub("'", "'\\''")) + end + end + end + + local function _fork_provider_curl(url) + local fdi, fdo = nixio.pipe() + if not fdi or not fdo then return nil end + local child = nixio.fork() + if child > 0 then + fdo:close() + return {pid = child, fdi = fdi, buf = ""} + elseif child == 0 then + nixio.dup(fdo, nixio.stdout) + fdi:close() + fdo:close() + local cmd = string.format( + 'OUT=$(curl -sLI -X GET -m 5 --retry 2 -w "http_code=%%{http_code}" -H "User-Agent: %s"%s "%s" 2>/dev/null); ' + .. 'CODE=$(echo "$OUT" | grep -o "http_code=[0-9]*" | head -1 | cut -d= -f2); ' + .. 'if [ -n "$OUT" ] && [ "$CODE" = "200" ]; then echo "$OUT"; ' + .. 'else curl -sLI -X GET -m 5 --retry 2 -w "http_code=%%{http_code}" -H "User-Agent: Quantumultx"%s "%s" 2>/dev/null; fi', + sub_ua, header_args, url, header_args, 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 providers = url_result.providers + local jobs = {} + + for i, provider in ipairs(providers) do + local job = _fork_provider_curl(provider.url) + if job then + job.idx = i + job.name = provider.name + jobs[#jobs + 1] = job + end + end + + if #jobs > 0 then + local done_count = 0 + local max_wait = 120 + for _ = 1, max_wait do + for _, job in ipairs(jobs) do + if not job.done then + local buf = job.fdi:read(4096) + if buf then job.buf = job.buf .. buf end + if nixio.waitpid(job.pid, "nohang") then + pcall(job.fdi.close, job.fdi) + job.done = true + done_count = done_count + 1 + end + end + end + if done_count >= #jobs then break end + nixio.nanosleep(0, 10000000) + end + for _, job in ipairs(jobs) do + if not job.done then + pcall(job.fdi.close, job.fdi) + nixio.kill(job.pid, 9) + end + end + for _, job in ipairs(jobs) do + if job.done and job.buf ~= "" then + local info = fetch_sub_info(providers[job.idx].url, sub_ua, sub_headers, job.buf) + if info then + info.provider_name = job.name + providers_data[#providers_data + 1] = info + end + end + end + end + -- Fallback: if all forks failed, try serial + if #providers_data == 0 then + for i, provider in ipairs(providers) do + local info = fetch_sub_info(provider.url, sub_ua, sub_headers) + if info then + info.provider_name = provider.name + providers_data[#providers_data + 1] = info + end end end end @@ -813,39 +986,38 @@ function sub_info_get() if #providers_data == 0 then if not url_result then - luci.http.status(500, "Subscription information not found") + HTTP.status(500, "Subscription information not found") return end end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ providers = providers_data, get_time = os.time(), url_result = url_result }) end -function action_rule_mode() - local mode, info +function action_rule_mode(internal) + local mode if is_running() then - local daip = daip() - local dase = dase() or "" - local cn_port = cn_port() - if not daip or not cn_port then return end - info = json.parse(luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/configs', dase, daip, cn_port))) - if info then - mode = info["mode"] - else - mode = fs.uci_get_config("config", "proxy_mode") or "rule" + local daip_val = daip() + local dase_val = dase() or "" + local cn_port_val = cn_port() + if daip_val and cn_port_val then + local info = json.parse(SYS.exec(string.format( + 'curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/configs', + dase_val, daip_val, cn_port_val))) + if info and info["mode"] then + mode = info["mode"] + end end - else - mode = fs.uci_get_config("config", "proxy_mode") or "rule" end - luci.http.prepare_content("application/json") - luci.http.write_json({ - mode = mode; - }) + mode = mode or fs.uci_get_config("config", "proxy_mode") or "rule" + if internal then return { mode = mode } end + HTTP.prepare_content("application/json") + HTTP.write_json({ mode = mode }) end function action_switch_rule_mode() @@ -853,21 +1025,21 @@ function action_switch_rule_mode() local daip = daip() local dase = dase() or "" local cn_port = cn_port() - mode = luci.http.formvalue("rule_mode") + mode = HTTP.formvalue("rule_mode") if not mode then - luci.http.status(500, "Missing parameters") + HTTP.status(500, "Missing parameters") return end if is_running() then - if not daip or not cn_port then luci.http.status(500, "Switch Faild") return end - info = luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPATCH http://"%s":"%s"/configs -d \'{\"mode\": \"%s\"}\'', dase, daip, cn_port, mode)) + if not daip or not cn_port then HTTP.status(500, "Switch Failed") return end + info = SYS.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPATCH http://"%s":"%s"/configs -d \'{\"mode\": \"%s\"}\'', dase, daip, cn_port, mode)) if info ~= "" then - luci.http.status(500, "Switch Faild") + HTTP.status(500, "Switch Failed") end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ info = info; }) end @@ -878,19 +1050,19 @@ end function action_get_run_mode() if mode() then - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ mode = mode(); }) else - luci.http.status(500, "Get Faild") + HTTP.status(500, "Get Failed") return end end function action_switch_run_mode() local mode, operation_mode - mode = luci.http.formvalue("run_mode") + mode = HTTP.formvalue("run_mode") operation_mode = fs.uci_get_config("config", "operation_mode") if operation_mode == "redir-host" then uci:set("openclash", "config", "en_mode", "redir-host"..mode) @@ -901,7 +1073,7 @@ function action_switch_run_mode() end uci:commit("openclash") if is_running() then - luci.sys.exec("/etc/init.d/openclash restart >/dev/null 2>&1 &") + SYS.exec("/etc/init.d/openclash restart >/dev/null 2>&1 &") end end @@ -912,7 +1084,7 @@ function action_log_level() local dase = dase() or "" local cn_port = cn_port() if not daip or not cn_port then return end - info = json.parse(luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/configs', dase, daip, cn_port))) + info = json.parse(SYS.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/configs', dase, daip, cn_port))) if info then level = info["log-level"] else @@ -921,8 +1093,8 @@ function action_log_level() else level = fs.uci_get_config("config", "log_level") or "info" end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ log_level = level; }) end @@ -933,44 +1105,43 @@ function action_switch_log() 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 or not level then luci.http.status(500, "Switch Faild") return end - info = luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPATCH http://"%s":"%s"/configs -d \'{\"log-level\": \"%s\"}\'', dase, daip, cn_port, level)) + level = HTTP.formvalue("log_level") + if not daip or not cn_port or not level then HTTP.status(500, "Switch Failed") return end + info = SYS.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPATCH http://"%s":"%s"/configs -d \'{\"log-level\": \"%s\"}\'', dase, daip, cn_port, level)) if info ~= "" then - luci.http.status(500, "Switch Faild") + HTTP.status(500, "Switch Failed") end else - luci.http.status(500, "Switch Faild") + HTTP.status(500, "Switch Failed") end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + 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'} +local t=0 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] + until(e<=1024) + return math.floor(e * 10 + 0.5) / 10 .. a[t] end end function action_toolbar_show_sys() local cpu = "0" local load_avg = "0" - local cpu_count = luci.sys.exec("grep -c ^processor /proc/cpuinfo 2>/dev/null"):gsub("\n", "") or 1 - - local pid = luci.sys.exec("pgrep -f '^[^ ]*clash' | head -1 | tr -d '\n' 2>/dev/null") + local cpu_count = SYS.exec("grep -c ^processor /proc/cpuinfo 2>/dev/null"):gsub("\n", "") or 1 + local pid = SYS.exec("pgrep -f '^[^ ]*clash' | head -1 | tr -d '\n' 2>/dev/null") if pid and pid ~= "" then - cpu = luci.sys.exec(string.format([[ + cpu = SYS.exec(string.format([[ top -b -n1 | awk -v pid="%s" ' BEGIN { cpu_col=0; } $0 ~ /%%CPU/ { @@ -986,22 +1157,22 @@ function action_toolbar_show_sys() cpu = "0" end - load_avg = luci.sys.exec("awk '{print $2; exit}' /proc/loadavg 2>/dev/null"):gsub("\n", "") or "0" + load_avg = SYS.exec("awk '{print $2; exit}' /proc/loadavg 2>/dev/null"):gsub("\n", "") or "0" if not string.match(load_avg, "^[0-9]*%.?[0-9]*$") then load_avg = "0" end end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ cpu = cpu, load_avg = tostring(math.floor(tonumber(load_avg) / tonumber(cpu_count) * 100)); }) end function action_toolbar_show() - local pid = luci.sys.exec("pgrep -f '^[^ ]*clash' | head -1 | tr -d '\n' 2>/dev/null") + local pid = SYS.exec("pgrep -f '^[^ ]*clash' | head -1 | tr -d '\n' 2>/dev/null") local traffic, connections, connection, up, down, up_total, down_total, mem, cpu, load_avg, cpu_count if pid and pid ~= "" then local daip = daip() @@ -1009,8 +1180,72 @@ function action_toolbar_show() local cn_port = cn_port() if not daip or not cn_port then return end - traffic = json.parse(luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/traffic', dase, daip, cn_port))) - connections = json.parse(luci.sys.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/connections', dase, daip, cn_port))) + -- Parallel curl helper + local function _fork_curl(endpoint) + local fdi, fdo = nixio.pipe() + if not fdi or not fdo then return nil end + local child = nixio.fork() + if child > 0 then + fdo:close() + return {pid = child, fdi = fdi, buf = ""} + elseif child == 0 then + nixio.dup(fdo, nixio.stdout) + fdi:close() + fdo:close() + local cmd = string.format( + 'curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET "http://%s:%s/%s"', + dase, daip, cn_port, endpoint + ) + nixio.exec("/bin/sh", "-c", cmd) + else + if fdi then fdi:close() end + if fdo then fdo:close() end + return nil + end + end + + local t_job = _fork_curl("traffic") + local c_job = _fork_curl("connections") + + if t_job and c_job then + local t_done, c_done = false, false + local max_wait = 60 + for _ = 1, max_wait do + if not t_done then + local buf = t_job.fdi:read(4096) + if buf then t_job.buf = t_job.buf .. buf end + if nixio.waitpid(t_job.pid, "nohang") then + pcall(t_job.fdi.close, t_job.fdi) + t_done = true + end + end + if not c_done then + local buf = c_job.fdi:read(4096) + if buf then c_job.buf = c_job.buf .. buf end + if nixio.waitpid(c_job.pid, "nohang") then + pcall(c_job.fdi.close, c_job.fdi) + c_done = true + end + end + if t_done and c_done then break end + nixio.nanosleep(0, 10000000) + end + if not t_done then + pcall(t_job.fdi.close, t_job.fdi) + nixio.kill(t_job.pid, 9) + end + if not c_done then + pcall(c_job.fdi.close, c_job.fdi) + nixio.kill(c_job.pid, 9) + end + traffic = t_done and t_job.buf ~= "" and json.parse(t_job.buf) or nil + connections = c_done and c_job.buf ~= "" and json.parse(c_job.buf) or nil + else + if t_job then pcall(t_job.fdi.close, t_job.fdi) end + if c_job then pcall(c_job.fdi.close, c_job.fdi) end + traffic = json.parse(SYS.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/traffic', dase, daip, cn_port))) + connections = json.parse(SYS.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XGET http://"%s":"%s"/connections', dase, daip, cn_port))) + end if traffic and connections and connections.connections then connection = #(connections.connections) @@ -1026,8 +1261,8 @@ function action_toolbar_show() 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([[ + mem = tonumber(SYS.exec(string.format("cat /proc/%s/status 2>/dev/null |grep -w VmRSS |awk '{print $2}'", pid))) + cpu = SYS.exec(string.format([[ top -b -n1 | awk -v pid="%s" ' BEGIN { cpu_col=0; } $0 ~ /%%CPU/ { @@ -1046,8 +1281,8 @@ function action_toolbar_show() cpu = "0" end - load_avg = luci.sys.exec("awk '{print $2; exit}' /proc/loadavg 2>/dev/null"):gsub("\n", "") or "0" - cpu_count = luci.sys.exec("grep -c ^processor /proc/cpuinfo 2>/dev/null"):gsub("\n", "") or 1 + load_avg = SYS.exec("awk '{print $2; exit}' /proc/loadavg 2>/dev/null"):gsub("\n", "") or "0" + cpu_count = SYS.exec("grep -c ^processor /proc/cpuinfo 2>/dev/null"):gsub("\n", "") or 1 if not string.match(load_avg, "^[0-9]*%.?[0-9]*$") then load_avg = "0" @@ -1056,8 +1291,8 @@ function action_toolbar_show() return end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ connections = connection, up = up, down = down, @@ -1070,23 +1305,23 @@ function action_toolbar_show() end function action_config_name() - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + 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({ + HTTP.prepare_content("application/json") + HTTP.write_json({ save_corever_branch = save_corever_branch(); }) end function action_one_key_update_check() - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ corever = corever(); }) end @@ -1098,8 +1333,8 @@ function action_dashboard_type() if not fs.isdirectory("/usr/share/openclash/ui/" .. default_dashboard) then default_dashboard = "" end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ dashboard_type = dashboard_type, yacd_type = yacd_type, yacd = fs.isdirectory("/usr/share/openclash/ui/yacd"), @@ -1111,27 +1346,27 @@ function action_dashboard_type() end function action_default_dashboard() - local default_dashboard = luci.http.formvalue("name") + local default_dashboard = HTTP.formvalue("name") if not default_dashboard or (default_dashboard ~= "Dashboard" and default_dashboard ~= "Yacd" and default_dashboard ~= "Metacubexd" and default_dashboard ~= "Zashboard") then - luci.http.status(500, "Set Failed") + HTTP.status(500, "Set Failed") return end if not fs.isdirectory("/usr/share/openclash/ui/" .. string.lower(default_dashboard)) then - luci.http.status(500, "Set Failed") + HTTP.status(500, "Set Failed") return end uci:set("openclash", "config", "default_dashboard", string.lower(default_dashboard)) uci:commit("openclash") - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ default_dashboard = default_dashboard; }) end function action_switch_dashboard() - local switch_name = luci.http.formvalue("name") - local switch_type = luci.http.formvalue("type") - local state = luci.sys.call(string.format('/usr/share/openclash/openclash_download_dashboard.sh "%s" "%s" >/dev/null 2>&1', switch_name, switch_type)) + local switch_name = HTTP.formvalue("name") + local switch_type = HTTP.formvalue("type") + local state = SYS.call(string.format('/usr/share/openclash/openclash_download_dashboard.sh "%s" "%s" >/dev/null 2>&1', switch_name, switch_type)) if switch_name == "Dashboard" and tonumber(state) == 0 then if switch_type == "Official" then uci:set("openclash", "config", "dashboard_type", "Official") @@ -1149,14 +1384,14 @@ function action_switch_dashboard() uci:commit("openclash") end end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ download_state = state; }) end function action_delete_dashboard() - local delete_name = luci.http.formvalue("name") + local delete_name = HTTP.formvalue("name") local delete_path = string.format("/usr/share/openclash/ui/%s", string.lower(delete_name)) local panels = { @@ -1173,15 +1408,15 @@ function action_delete_dashboard() end if existing_panels <= 1 then - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ delete_state = 0, error = "Cannot delete the last remaining dashboard" }) return end - local state = luci.sys.call(string.format("rm -rf '%s' >/dev/null 2>&1", delete_path)) == 0 and 1 or 0 + local state = SYS.call(string.format("rm -rf '%s' >/dev/null 2>&1", delete_path)) == 0 and 1 or 0 if tonumber(state) == 1 then if delete_name == "Dashboard" then uci:set("openclash", "config", "dashboard_type", "Official") @@ -1195,16 +1430,16 @@ function action_delete_dashboard() uci:commit("openclash") end end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ delete_state = state; }) end function action_op_mode() local op_mode = fs.uci_get_config("config", "operation_mode") - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ op_mode = op_mode; }) end @@ -1218,40 +1453,75 @@ function action_switch_mode() uci:set("openclash", "config", "operation_mode", "redir-host") uci:commit("openclash") end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ switch_mode = switch_mode; }) end -function action_status() - luci.http.prepare_content("application/json") - luci.http.write_json({ +function action_conn_status(internal) + local data = { clash = uci:get("openclash", "config", "enable") == "1", daip = daip(), dase = dase(), db_foward_port = db_foward_port(), db_foward_domain = db_foward_domain(), + cn_port = cn_port() + } + if internal then return data end + HTTP.prepare_content("application/json") + HTTP.write_json(data) +end + +function action_status() + local status_data = action_conn_status(true) + local rule_data = action_rule_mode(true) + local oc_data = action_oc_settings(true) + local proxy_data = action_proxy_info(true) + + local result = { + -- status fields + clash = status_data.clash, + daip = status_data.daip, + dase = status_data.dase, + db_foward_port = status_data.db_foward_port, + db_foward_domain = status_data.db_foward_domain, db_forward_ssl = db_foward_ssl(), - cn_port = cn_port(), + cn_port = status_data.cn_port, yacd = fs.isdirectory("/usr/share/openclash/ui/yacd"), dashboard = fs.isdirectory("/usr/share/openclash/ui/dashboard"), metacubexd = fs.isdirectory("/usr/share/openclash/ui/metacubexd"), zashboard = fs.isdirectory("/usr/share/openclash/ui/zashboard"), - core_type = fs.uci_get_config("config", "core_type") or "Meta"; - }) + core_type = fs.uci_get_config("config", "core_type") or "Meta", + -- run_mode + run_mode = mode(), + -- rule_mode + rule_mode = rule_data.mode, + -- oc_settings + meta_sniffer = oc_data.meta_sniffer, + respect_rules = oc_data.respect_rules, + oversea = oc_data.oversea, + stream_unlock = oc_data.stream_unlock, + -- proxy_info + mixed_port = proxy_data.mixed_port, + auth_user = proxy_data.auth_user, + auth_pass = proxy_data.auth_pass, + } + + HTTP.prepare_content("application/json") + HTTP.write_json(result) end function action_lastversion() - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ lastversion = check_lastversion(); }) end function action_start() - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ startlog = startlog(); }) end @@ -1259,19 +1529,19 @@ end function action_get_last_version() if not process_status("/usr/share/openclash/clash_version.sh") then if tonumber(os.time() - (fs.mtime("/tmp/clash_last_version") or 0)) > 1800 then - luci.sys.call("bash /usr/share/openclash/clash_version.sh &") + SYS.call("bash /usr/share/openclash/clash_version.sh &") end end if not process_status("/usr/share/openclash/openclash_version.sh") then if tonumber(os.time() - (fs.mtime("/tmp/openclash_last_version") or 0)) > 1800 then - luci.sys.call("bash /usr/share/openclash/openclash_version.sh &") + SYS.call("bash /usr/share/openclash/openclash_version.sh &") end end end function action_update() - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ coremodel = coremodel(), coremetacv = coremetacv(), corelv = corelv(), @@ -1282,8 +1552,8 @@ function action_update() end function action_update_info() - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ corever = corever(), release_branch = release_branch(), smart_enable = smart_enable(); @@ -1291,59 +1561,60 @@ function action_update_info() end function action_update_ma() - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ oplv = oplv(), pkg_type = fs.pkg_type(), + oix_core = fs.uci_get_config("config", "oix_token") ~= "", corelv = corelv(), corever = corever(); }) end function action_opupdate() - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ opup = opup(); }) end function action_check_core() - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ core_status = check_core(); }) end function action_coreupdate() - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + 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'") + return 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 &") + return 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({ + HTTP.prepare_content("application/json") + HTTP.write_json({ rule_download_status = download_rule(); }) end function action_refresh_log() - luci.http.prepare_content("application/json") + HTTP.prepare_content("application/json") local logfile = "/tmp/openclash.log" - local log_len = tonumber(luci.http.formvalue("log_len")) or 0 - local core_refresh = luci.http.formvalue("core_refresh") == "true" + local log_len = tonumber(HTTP.formvalue("log_len")) or 0 + local core_refresh = HTTP.formvalue("core_refresh") == "true" if not fs.access(logfile) then - luci.http.write_json({ + HTTP.write_json({ len = 0, update = false, core_log = "", @@ -1352,10 +1623,10 @@ function action_refresh_log() return end - local total_lines = tonumber(luci.sys.exec("wc -l < " .. logfile)) or 0 + local total_lines = tonumber(SYS.exec("wc -l < " .. logfile)) or 0 if total_lines == log_len and log_len > 0 then - luci.http.write_json({ + HTTP.write_json({ len = total_lines, update = false, core_log = "", @@ -1374,24 +1645,24 @@ function action_refresh_log() local oc_logs = {} core_cmd = string.format( - "tail -n +%d '%s' | head -n %d | grep -v -E '%s' | grep -E '%s' | tail -n %d", - start_line, logfile, read_count, exclude_pattern, core_pattern, limit + "sed -n '%d,%dp' '%s' | grep -v -E '%s' | grep -E '%s' | tail -n %d", + start_line, start_line + read_count - 1, logfile, exclude_pattern, core_pattern, limit ) oc_cmd = string.format( - "tail -n +%d '%s' | head -n %d | grep -v -E '%s' | grep -v -E '%s' | tail -n %d", - start_line, logfile, read_count, exclude_pattern, core_pattern, limit + "sed -n '%d,%dp' '%s' | grep -v -E '%s' | grep -v -E '%s' | tail -n %d", + start_line, start_line + read_count - 1, logfile, exclude_pattern, core_pattern, limit ) if core_refresh then - core_raw = luci.sys.exec(core_cmd) + core_raw = SYS.exec(core_cmd) end - oc_raw = luci.sys.exec(oc_cmd) + oc_raw = SYS.exec(oc_cmd) if core_raw and core_raw ~= "" then for line in core_raw:gmatch("[^\n]+") do - table.insert(core_logs, line) + core_logs[#core_logs + 1] = line end end @@ -1400,7 +1671,7 @@ function action_refresh_log() if not string.match(string.sub(line, 1, 19), "%d%d%d%d%-%d%d%-%d%d %d%d:%d%d:%d%d") then line = os.date("%Y-%m-%d %H:%M:%S") .. ' [Fatal] ' .. line end - table.insert(oc_logs, trans_line(line)) + oc_logs[#oc_logs + 1] = trans_line(line) end end @@ -1414,7 +1685,7 @@ function action_refresh_log() local core_log = #core_logs > 0 and table.concat(core_logs, "\n") or "" local oc_log = #oc_logs > 0 and table.concat(oc_logs, "\n") or "" - luci.http.write_json({ + HTTP.write_json({ len = total_lines, update = true, core_log = core_log, @@ -1423,17 +1694,17 @@ function action_refresh_log() end function action_del_log() - local log_type = luci.http.formvalue("type") + local log_type = HTTP.formvalue("type") if log_type == "debug" then - luci.sys.exec(": > /tmp/openclash_debug.log") + SYS.exec(": > /tmp/openclash_debug.log") else - luci.sys.exec(": > /tmp/openclash.log") + SYS.exec(": > /tmp/openclash.log") end return end function action_del_start_log() - luci.sys.exec("echo '##FINISH##' > /tmp/openclash_start.log") + SYS.exec("echo '##FINISH##' > /tmp/openclash_start.log") return end @@ -1464,53 +1735,53 @@ function split(str,delimiter) end function action_diag_connection() - local addr = luci.http.formvalue("addr") + local addr = 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") + 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") + HTTP.write(ln) + HTTP.write("\n") end util:close() end return end - luci.http.status(500, "Bad address") + HTTP.status(500, "Bad address") end function action_diag_dns() - local addr = luci.http.formvalue("addr") + local addr = 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") + 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") + HTTP.write(ln) + HTTP.write("\n") end util:close() end return end - luci.http.status(500, "Bad address") + HTTP.status(500, "Bad address") end function action_gen_debug_logs() - local gen_log = luci.sys.call("/usr/share/openclash/openclash_debug.sh") + local gen_log = 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") + HTTP.prepare_content("text/plain; charset=utf-8") local file=io.open(logfile, "r+") file:seek("set") local info = "" @@ -1522,7 +1793,7 @@ function action_gen_debug_logs() end end file:close() - luci.http.write(info) + HTTP.write(info) end function action_get_debug_logs() @@ -1530,84 +1801,84 @@ function action_get_debug_logs() if not fs.access(logfile) then return end - luci.http.prepare_content("text/plain; charset=utf-8") - luci.http.write(fs.readfile(logfile) or "") + HTTP.prepare_content("text/plain; charset=utf-8") + HTTP.write(fs.readfile(logfile) or "") end function action_backup() - local config = luci.sys.call("cp /etc/config/openclash /etc/openclash/openclash >/dev/null 2>&1") + local config = 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( + 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") + HTTP.prepare_content("application/x-targz") + luci.ltn12.pump.all(reader, HTTP.write) + 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 config = 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( + 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") + HTTP.prepare_content("application/x-targz") + luci.ltn12.pump.all(reader, HTTP.write) + 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( + 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) + HTTP.prepare_content("application/x-targz") + luci.ltn12.pump.all(reader, HTTP.write) end function action_backup_only_core() local reader = ltn12_popen("tar -C '/etc/openclash' -cz './core' 2>/dev/null") - luci.http.header( + 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) + HTTP.prepare_content("application/x-targz") + luci.ltn12.pump.all(reader, 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( + 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) + HTTP.prepare_content("application/x-targz") + luci.ltn12.pump.all(reader, 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( + 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) + HTTP.prepare_content("application/x-targz") + luci.ltn12.pump.all(reader, HTTP.write) end function ltn12_popen(command) @@ -1641,19 +1912,19 @@ function ltn12_popen(command) end function create_file() - local file_name = luci.http.formvalue("filename") - local file_path = luci.http.formvalue("filepath")..file_name + local file_name = HTTP.formvalue("filename") + local file_path = HTTP.formvalue("filepath")..file_name fs.writefile(file_path, "") if not fs.isfile(file_path) then - luci.http.status(500, "Create File Faild") + HTTP.status(500, "Create File Failed") 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 new_file_name = HTTP.formvalue("new_file_name") + local file_path = HTTP.formvalue("file_path") + local old_file_name = 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 @@ -1670,24 +1941,24 @@ function rename_file() fs.config_refs(old_file_name, new_file_name) end - luci.http.status(200, "Rename File Successful") + HTTP.status(200, "Rename File Successful") else - luci.http.status(500, "Rename File Faild") + HTTP.status(500, "Rename File Failed") end return end function manual_stream_unlock_test() - local type = luci.http.formvalue("type") + local type = HTTP.formvalue("type") local cmd = string.format('/usr/share/openclash/openclash_streaming_unlock.lua "%s"', type) - luci.http.prepare_content("text/plain; charset=utf-8") + HTTP.prepare_content("text/plain; charset=utf-8") local util = io.popen(cmd) if util and util ~= "" then while true do local ln = util:read("*l") if ln then - luci.http.write(trans_line(ln)) - luci.http.write("\n") + HTTP.write(trans_line(ln)) + HTTP.write("\n") end if not process_status("openclash_streaming_unlock.lua "..type) or not process_status("openclash_streaming_unlock.lua ") then break @@ -1696,20 +1967,20 @@ function manual_stream_unlock_test() util:close() return end - luci.http.status(500, "Something Wrong While Testing...") + HTTP.status(500, "Something Wrong While Testing...") end function all_proxies_stream_test() - local type = luci.http.formvalue("type") + local type = HTTP.formvalue("type") local cmd = string.format('/usr/share/openclash/openclash_streaming_unlock.lua "%s" "%s"', type, "all") - luci.http.prepare_content("text/plain; charset=utf-8") + HTTP.prepare_content("text/plain; charset=utf-8") local util = io.popen(cmd) if util and util ~= "" then while true do local ln = util:read("*l") if ln then - luci.http.write(trans_line(ln)) - luci.http.write("\n") + HTTP.write(trans_line(ln)) + HTTP.write("\n") end if not process_status("openclash_streaming_unlock.lua "..type) or not process_status("openclash_streaming_unlock.lua ") then break @@ -1718,7 +1989,7 @@ function all_proxies_stream_test() util:close() return end - luci.http.status(500, "Something Wrong While Testing...") + HTTP.status(500, "Something Wrong While Testing...") end function trans_line(data) @@ -1748,35 +2019,35 @@ function trans_line(data) while pos do if pos > last_pos then - table.insert(segments, { + segments[#segments + 1] = { type = "trans", text = string.sub(data, last_pos, pos - 1) - }) + } end local close_pos = string.find(data, "】", pos + 1) if not close_pos then - table.insert(segments, { + segments[#segments + 1] = { type = "trans", text = string.sub(data, pos, -1) - }) + } break end - table.insert(segments, { + segments[#segments + 1] = { type = "no_trans", text = string.sub(data, pos, close_pos + 2) - }) + } last_pos = close_pos + 3 pos = string.find(data, "【", last_pos) end if last_pos <= string.len(data) then - table.insert(segments, { + segments[#segments + 1] = { type = "trans", text = string.sub(data, last_pos, -1) - }) + } end line_trans = time_part .. level_part @@ -1793,20 +2064,20 @@ end function process_status(name) local cmd = string.format("%s |grep '%s' |grep -v grep", fs.ps_cmd(), name) - local result = luci.sys.exec(cmd) + local result = 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") + local HTTP_CODE = 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({ + local info = SYS.exec("cat /tmp/openclash_announcement 2>/dev/null") or "" + HTTP.prepare_content("application/json") + HTTP.write_json({ content = info; }) end @@ -1972,8 +2243,8 @@ function action_myip_check() end if next(queries) == nil then - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ error = "Failed to create any queries" }) return @@ -1981,6 +2252,7 @@ function action_myip_check() local max_iterations = 140 local iteration = 0 + local delay = 10000000 -- 10ms initial local completed = {} while iteration < max_iterations do @@ -2006,7 +2278,7 @@ function action_myip_check() queries[name] = nil else - local still_running = luci.sys.call(string.format("kill -0 %d 2>/dev/null", info.query.pid)) == 0 + local still_running = 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 @@ -2031,7 +2303,8 @@ function action_myip_check() break end - nixio.nanosleep(0, 50000000) + nixio.nanosleep(0, delay) + delay = math.min(delay * 2, 100000000) -- grow to 100ms max end for name, info in pairs(queries) do @@ -2048,7 +2321,7 @@ function action_myip_check() 'curl -sL -m 5 --retry 2 -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" "https://api-ipv4.ip.sb/geoip/%s" 2>/dev/null', result.ipify.ip ) - local geo_data = luci.sys.exec(geo_cmd) + local geo_data = SYS.exec(geo_cmd) if geo_data and geo_data ~= "" then local ok_geo, geo_json = pcall(json.parse, geo_data) @@ -2065,8 +2338,8 @@ function action_myip_check() end end - luci.http.prepare_content("application/json") - luci.http.write_json(result) + HTTP.prepare_content("application/json") + HTTP.write_json(result) end function latency_test(addr) @@ -2098,7 +2371,7 @@ function latency_test(addr) test_url ) - local output = luci.sys.exec(cmd) + local output = SYS.exec(cmd) if output and output ~= "" then local http_code, time_total, time_connect, time_appconnect = output:match("(%d+),([%d%.]+),([%d%.]+),([%d%.]+)") @@ -2152,11 +2425,11 @@ function latency_test(addr) end function action_website_check() - local domain = luci.http.formvalue("domain") + local domain = HTTP.formvalue("domain") if not domain then - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ success = false, response_time = 0, error = "Missing domain parameter" @@ -2166,11 +2439,11 @@ function action_website_check() local result = latency_test(domain) - luci.http.prepare_content("application/json") - luci.http.write_json(result) + HTTP.prepare_content("application/json") + HTTP.write_json(result) end -function action_proxy_info() +function action_proxy_info(internal) local result = { mixed_port = "", auth_user = "", @@ -2197,11 +2470,12 @@ function action_proxy_info() end end) - luci.http.prepare_content("application/json") - luci.http.write_json(result) + if internal then return result end + HTTP.prepare_content("application/json") + HTTP.write_json(result) end -function action_oc_settings() +function action_oc_settings(internal) local result = { meta_sniffer = "0", respect_rules = "0", @@ -2233,16 +2507,17 @@ function action_oc_settings() result.stream_unlock = "1" end - luci.http.prepare_content("application/json") - luci.http.write_json(result) + if internal then return result end + HTTP.prepare_content("application/json") + HTTP.write_json(result) end function action_switch_oc_setting() - local setting = luci.http.formvalue("setting") - local value = luci.http.formvalue("value") + local setting = HTTP.formvalue("setting") + local value = HTTP.formvalue("value") if not setting or not value then - luci.http.status(500, "Missing parameters") + HTTP.status(500, "Missing parameters") return end @@ -2258,13 +2533,13 @@ function action_switch_oc_setting() 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") + HTTP.status(500, "No config path found") return false end - local ruby_result = luci.sys.call(ruby_cmd) + local ruby_result = SYS.call(ruby_cmd) if ruby_result ~= 0 then - luci.http.status(500, "Failed to modify config file") + HTTP.status(500, "Failed to modify config file") return false end @@ -2272,14 +2547,14 @@ function action_switch_oc_setting() local dase = dase() or "" local cn_port = cn_port() if not daip or not cn_port then - luci.http.status(500, "Switch Failed") + HTTP.status(500, "Switch Failed") return false end - local reload_result = luci.sys.exec(string.format('curl -sL -m 5 --connect-timeout 2 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPUT http://"%s":"%s"/configs?force=true -d \'{"path":"%s"}\' 2>&1', dase, daip, cn_port, runtime_config_path)) + local reload_result = SYS.exec(string.format('curl -sL -m 5 --connect-timeout 2 --retry 2 -H "Content-Type: application/json" -H "Authorization: Bearer %s" -XPUT http://"%s":"%s"/configs?force=true -d \'{"path":"%s"}\' 2>&1', dase, daip, cn_port, runtime_config_path)) if reload_result ~= "" then - luci.http.status(500, "Switch Failed") + HTTP.status(500, "Switch Failed") return false end @@ -2452,7 +2727,7 @@ function action_switch_oc_setting() end uci:set("openclash", "@overwrite[0]", "china_ip_route", value) uci:commit("openclash") - luci.sys.exec("/etc/init.d/openclash restart >/dev/null 2>&1 &") + 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) @@ -2482,12 +2757,12 @@ function action_switch_oc_setting() end uci:commit("openclash") else - luci.http.status(500, "Invalid setting") + HTTP.status(500, "Invalid setting") return end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ status = "success", setting = setting, value = value @@ -2517,14 +2792,14 @@ function action_generate_pac() if not proxy_ip then result.error = luci.i18n.translate("Unable to get proxy IP") - luci.http.prepare_content("application/json") - luci.http.write_json(result) + HTTP.prepare_content("application/json") + 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", "") + local random_string = SYS.exec(random_cmd):gsub("\n", "") return random_string end @@ -2552,10 +2827,10 @@ function action_generate_pac() local random_suffix = nil local need_update = true - luci.sys.call("mkdir -p " .. pac_dir) + 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) + local existing_files = SYS.exec(find_cmd) if existing_files and existing_files ~= "" then for file_path in existing_files:gmatch("[^\n]+") do if fs.access(file_path) then @@ -2579,7 +2854,7 @@ function action_generate_pac() if file then file:write(new_pac_content) file:close() - luci.sys.call("chmod 644 " .. file_path) + SYS.call("chmod 644 " .. file_path) pac_filename = file_path:match("([^/]+)$") pac_file_path = file_path @@ -2607,7 +2882,7 @@ function action_generate_pac() if file then file:write(final_content) file:close() - luci.sys.call("chmod 644 " .. file_path) + SYS.call("chmod 644 " .. file_path) pac_filename = file_path:match("([^/]+)$") pac_file_path = file_path @@ -2623,7 +2898,7 @@ function action_generate_pac() end if need_update then - luci.sys.call("rm -f " .. pac_dir .. "pac_* 2>/dev/null") + SYS.call("rm -f " .. pac_dir .. "pac_* 2>/dev/null") random_suffix = generate_random_string() pac_filename = "pac_" .. random_suffix @@ -2634,15 +2909,15 @@ function action_generate_pac() file:write(new_pac_content) file:close() - luci.sys.call("chmod 644 " .. pac_file_path) + SYS.call("chmod 644 " .. pac_file_path) else result.error = luci.i18n.translate("Failed to write PAC file") - luci.http.prepare_content("application/json") - luci.http.write_json(result) + HTTP.prepare_content("application/json") + 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)) + 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) @@ -2652,15 +2927,15 @@ function action_generate_pac() result.error = luci.i18n.translate("No authentication configured, please be aware of the risk of information leakage!") end - luci.http.prepare_content("application/json") - luci.http.write_json(result) + HTTP.prepare_content("application/json") + 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 client_protocol = HTTP.formvalue("client_protocol") + local client_hostname = HTTP.formvalue("client_hostname") + local client_host = HTTP.formvalue("client_host") + local client_port = HTTP.formvalue("client_port") local request_scheme = "http" local host = "localhost" @@ -2668,9 +2943,9 @@ function generate_pac_url_with_client_info(pac_filename, random_suffix) 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 + if HTTP.getenv("HTTPS") == "on" or + HTTP.getenv("HTTP_X_FORWARDED_PROTO") == "https" or + HTTP.getenv("REQUEST_SCHEME") == "https" then request_scheme = "https" end end @@ -2686,9 +2961,9 @@ function generate_pac_url_with_client_info(pac_filename, random_suffix) 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 server_name = HTTP.getenv("SERVER_NAME") + local http_host = HTTP.getenv("HTTP_HOST") + local server_port = HTTP.getenv("SERVER_PORT") local proxy_ip = daip() if http_host and http_host ~= "" then @@ -2898,23 +3173,23 @@ local function is_safe_filename(filename) end local function kill_process() - local cmd = string.format("%s |grep -E 'openclash|clash' |grep -v grep |awk '{print $1}' |xargs -r kill -9 >/dev/null 2>&1", fs.ps_cmd()) - luci.sys.call(cmd) + local cmd = string.format("%s |grep -E 'openclash|clash|mihomo' |grep -v grep |awk '{print $1}' |xargs -r kill -9 >/dev/null 2>&1", fs.ps_cmd()) + SYS.call(cmd) end function action_oc_action() - local action = luci.http.formvalue("action") - local config_file = luci.http.formvalue("config_file") + local action = HTTP.formvalue("action") + local config_file = HTTP.formvalue("config_file") if not action then - luci.http.status(500, "Missing action parameter") + HTTP.status(500, "Missing action parameter") return end if config_file and config_file ~= "" then local config_path = "/etc/openclash/config/" .. config_file if not fs.access(config_path) then - luci.http.status(500, "Config file not found") + HTTP.status(500, "Config file not found") return end @@ -2930,9 +3205,9 @@ function action_oc_action() end if not is_running() then kill_process() - luci.sys.call("/etc/init.d/openclash start >/dev/null 2>&1") + SYS.call("/etc/init.d/openclash start >/dev/null 2>&1") else - luci.sys.call("/etc/init.d/openclash restart >/dev/null 2>&1") + SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1") end elseif action == "stop" then if uci:get("openclash", "config", "enable") ~= "0" then @@ -2940,29 +3215,29 @@ function action_oc_action() uci:commit("openclash") end kill_process() - luci.sys.call("/etc/init.d/openclash stop >/dev/null 2>&1") + SYS.call("/etc/init.d/openclash stop >/dev/null 2>&1") elseif action == "restart" then if uci:get("openclash", "config", "enable") ~= "1" then uci:set("openclash", "config", "enable", "1") uci:commit("openclash") end kill_process() - luci.sys.call("/etc/init.d/openclash restart >/dev/null 2>&1") + SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1") else - luci.http.status(500, "Invalid action parameter") + HTTP.status(500, "Invalid action parameter") return end - luci.http.prepare_content("application/json") - luci.http.write_json({status = "success", action = action}) + HTTP.prepare_content("application/json") + HTTP.write_json({status = "success", action = action}) end function action_config_file_list() local config_files = {} local age_files = {} local current_config = "" - local config_path = fs.uci_get_config("config", "config_path") + if config_path then current_config = config_path end @@ -2981,6 +3256,35 @@ function action_config_file_list() end) local config_dir = "/etc/openclash/config/" + local fingerprint_parts = {} + local cache_valid = false + local client_fp = HTTP.formvalue("fingerprint") + + if fs.access(config_dir) then + local files = fs.dir(config_dir) + if files then + local yaml_files = {} + for _, f in ipairs(files) do + if string.match(f, "%.ya?ml$") then + local stat = fs.stat(config_dir .. f) + if stat and stat.type == "regular" then + yaml_files[#yaml_files + 1] = f .. ":" .. (stat.mtime or 0) + end + end + end + table.sort(yaml_files) + fingerprint_parts[#fingerprint_parts + 1] = table.concat(yaml_files, "|") + end + end + for _, a in ipairs(age_files) do + fingerprint_parts[#fingerprint_parts + 1] = "AGE:" .. a.name .. ":" .. (a.secret ~= "" and "1" or "0") + end + local fingerprint = table.concat(fingerprint_parts, "||") + + if client_fp and client_fp == fingerprint then + cache_valid = true + end + if fs.access(config_dir) then local files = fs.dir(config_dir) if files then @@ -2988,8 +3292,8 @@ function action_config_file_list() local full_path = config_dir .. file local stat = fs.stat(full_path) local name_no_ext = file:match("^(.*)%.ya?ml$") - if stat and stat.type == "regular" then - if name_no_ext then + if stat and stat.type == "regular" and string.match(file, "%.ya?ml$") then + if name_no_ext and #age_files > 0 and not cache_valid then local cfile = io.open(full_path,"r") if cfile then local content = cfile:read(1024) @@ -3000,43 +3304,42 @@ function action_config_file_list() break end end + cfile:close() end - cfile:close() - end - if string.match(file, "%.ya?ml$") then - table.insert(config_files, { - name = file, - path = full_path, - size = stat.size, - mtime = stat.mtime, - age = stat.age or false - }) end + table.insert(config_files, { + name = file, + path = full_path, + size = stat.size, + mtime = stat.mtime, + age = stat.age or false + }) end end end table.sort(config_files, function(a, b) - return a.mtime > b.mtime + return string.lower(a.name) < string.lower(b.name) end) end - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ config_files = config_files, current_config = current_config, - total_count = #config_files + total_count = #config_files, + fingerprint = fingerprint }) end function action_upload_config() - local upload = luci.http.formvalue("config_file") - local filename = luci.http.formvalue("filename") + local upload = HTTP.formvalue("config_file") + local filename = HTTP.formvalue("filename") - luci.http.prepare_content("application/json") + HTTP.prepare_content("application/json") if not upload or upload == "" then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "No file uploaded" }) @@ -3048,7 +3351,7 @@ function action_upload_config() end if not is_safe_filename(filename) then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Invalid filename" }) @@ -3063,7 +3366,7 @@ function action_upload_config() local target_path = config_dir .. filename if string.len(upload) == 0 then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Uploaded file is empty" }) @@ -3072,7 +3375,7 @@ function action_upload_config() local file_size = string.len(upload) if file_size > 10 * 1024 * 1024 then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = string.format("File size (%s) exceeds 10MB limit", fs.filesize(file_size)) }) @@ -3091,34 +3394,34 @@ function action_upload_config() end if not yaml_valid then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Invalid config file format - missing required YAML sections" }) return end - luci.sys.call("mkdir -p " .. config_dir) + 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)) + SYS.call(string.format("chmod 644 '%s'", target_path)) + SYS.call(string.format("chown root:root '%s'", target_path)) local written_content = fs.readfile(target_path) if not written_content or string.len(written_content) ~= file_size then fs.unlink(target_path) - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "File write verification failed" }) return end - luci.http.write_json({ + HTTP.write_json({ status = "success", message = "Config file uploaded successfully", filename = filename, @@ -3127,7 +3430,7 @@ function action_upload_config() readable_size = fs.filesize(file_size) }) else - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Failed to save config file to disk" }) @@ -3135,11 +3438,11 @@ function action_upload_config() end function action_config_file_read() - local config_file = luci.http.formvalue("config_file") - luci.http.prepare_content("application/json") + local config_file = HTTP.formvalue("config_file") + HTTP.prepare_content("application/json") if not config_file then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Missing config_file parameter" }) @@ -3158,7 +3461,7 @@ function action_config_file_read() end if not allow then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Invalid config file path" }) @@ -3166,7 +3469,7 @@ function action_config_file_read() end if not fs.access(config_file) then - luci.http.write_json({ + HTTP.write_json({ status = "success", content = "", file_info = { @@ -3182,7 +3485,7 @@ function action_config_file_read() local stat = fs.stat(config_file) if not stat or stat.type ~= "regular" then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Config file is not a regular file" }) @@ -3190,7 +3493,7 @@ function action_config_file_read() end if stat.size > 10 * 1024 * 1024 then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Config file too large (max 10MB)" }) @@ -3199,14 +3502,14 @@ function action_config_file_read() local content = fs.readfile(config_file) if content == nil then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Failed to read config file" }) return end - luci.http.write_json({ + HTTP.write_json({ status = "success", content = content, file_info = { @@ -3220,15 +3523,15 @@ function action_config_file_read() end function action_config_file_save() - local config_file = luci.http.formvalue("config_file") - local content = luci.http.formvalue("content") - luci.http.prepare_content("application/json") + local config_file = HTTP.formvalue("config_file") + local content = HTTP.formvalue("content") + HTTP.prepare_content("application/json") if content then content = content:gsub("\r\n", "\n"):gsub("\r", "\n") end if not config_file then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Missing config_file parameter" }) @@ -3236,7 +3539,7 @@ function action_config_file_save() end if not content then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Missing content parameter" }) @@ -3247,7 +3550,7 @@ function action_config_file_save() if not is_overwrite then if not string.match(config_file, "^/etc/openclash/config/[^/]+%.ya?ml$") or string.find(config_file, "%.%.") then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Invalid config file path" }) @@ -3255,7 +3558,7 @@ function action_config_file_save() end else if not (config_file == "/etc/openclash/custom/openclash_custom_overwrite.sh" or (config_file:match("^/etc/openclash/overwrite/[^/]+$") and not string.find(config_file, "%.%."))) then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Invalid overwrite file path" }) @@ -3264,7 +3567,7 @@ function action_config_file_save() end if string.len(content) > 10 * 1024 * 1024 then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Content too large (max 10MB)" }) @@ -3274,9 +3577,9 @@ function action_config_file_save() local backup_file = nil if fs.access(config_file) then backup_file = config_file .. ".backup." .. os.time() - local backup_success = luci.sys.call(string.format("cp '%s' '%s'", config_file, backup_file)) + local backup_success = SYS.call(string.format("cp '%s' '%s'", config_file, backup_file)) if backup_success ~= 0 then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Failed to create backup file" }) @@ -3287,10 +3590,10 @@ function action_config_file_save() 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)) + SYS.call(string.format("mv '%s' '%s'", backup_file, config_file)) end - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Failed to write config file" }) @@ -3300,10 +3603,10 @@ function action_config_file_save() 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)) + SYS.call(string.format("mv '%s' '%s'", backup_file, config_file)) end - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "File write verification failed" }) @@ -3311,12 +3614,12 @@ function action_config_file_save() end if not is_overwrite then - luci.sys.call(string.format("chmod 644 '%s'", config_file)) + SYS.call(string.format("chmod 644 '%s'", config_file)) end - luci.sys.call(string.format("chown root:root '%s'", config_file)) + SYS.call(string.format("chown root:root '%s'", config_file)) if backup_file then - luci.sys.call(string.format([[ + SYS.call(string.format([[ ( config_dir="$(dirname '%s')" config_basename="$(basename '%s')" @@ -3338,7 +3641,7 @@ function action_config_file_save() } end - luci.http.write_json({ + HTTP.write_json({ status = "success", message = "Config file saved successfully", file_info = file_info, @@ -3347,27 +3650,28 @@ function action_config_file_save() 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-verge/v2.4.5" - local sub_convert = luci.http.formvalue("sub_convert") or "0" - local convert_address = luci.http.formvalue("convert_address") or "" - local template = luci.http.formvalue("template") or "" - local custom_template_url = luci.http.formvalue("custom_template_url") 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") + local name = HTTP.formvalue("name") + local address = HTTP.formvalue("address") + local sub_ua = HTTP.formvalue("sub_ua") or "clash-verge/v2.4.5" + local sub_convert = HTTP.formvalue("sub_convert") or "0" + local convert_address = HTTP.formvalue("convert_address") or "" + local template = HTTP.formvalue("template") or "" + local custom_template_url = HTTP.formvalue("custom_template_url") or "" + local emoji = HTTP.formvalue("emoji") or "false" + local udp = HTTP.formvalue("udp") or "false" + local skip_cert_verify = HTTP.formvalue("skip_cert_verify") or "false" + local sort = HTTP.formvalue("sort") or "false" + local node_type = HTTP.formvalue("node_type") or "false" + local rule_provider = HTTP.formvalue("rule_provider") or "false" + local custom_params = HTTP.formvalue("custom_params") or "" + local keyword = HTTP.formvalue("keyword") or "" + local ex_keyword = HTTP.formvalue("ex_keyword") or "" + local de_ex_keyword = HTTP.formvalue("de_ex_keyword") or "" + local sub_headers = HTTP.formvalue("sub_headers") or "" + HTTP.prepare_content("application/json") if not name then - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Missing name parameter" }) @@ -3427,7 +3731,7 @@ function action_add_subscription() error_msg = "Invalid subscription URL format. Only single HTTP/HTTPS subscription URL is supported when subscription conversion is disabled" end - luci.http.write_json({ + HTTP.write_json({ status = "error", message = error_msg }) @@ -3480,6 +3784,21 @@ function action_add_subscription() uci:delete("openclash", section_id, "address") end uci:set("openclash", section_id, "sub_ua", sub_ua) + + uci:delete("openclash", section_id, "sub_headers") + if sub_headers and sub_headers ~= "" then + local headers = {} + for line in sub_headers:gmatch("[^\n]+") do + local h = line:match("^%s*(.-)%s*$") + if h and h ~= "" then + table.insert(headers, h) + end + end + if #headers > 0 then + uci:set_list("openclash", section_id, "sub_headers", headers) + end + end + uci:set("openclash", section_id, "sub_convert", sub_convert) if sub_convert == "1" then uci:set("openclash", section_id, "convert_address", convert_address) @@ -3548,7 +3867,7 @@ function action_add_subscription() uci:commit("openclash") local action_msg = existing_section_id and "Subscription updated successfully" or "Subscription added successfully" - luci.http.write_json({ + HTTP.write_json({ status = "success", message = action_msg, name = name, @@ -3558,7 +3877,7 @@ function action_add_subscription() multiple_links = sub_convert == "1" and (string.find(normalized_address, "\n") and true or false) }) else - luci.http.write_json({ + HTTP.write_json({ status = "error", message = "Failed to add/update subscription configuration" }) @@ -3566,10 +3885,10 @@ function action_add_subscription() end function action_upload_overwrite() - local upload = luci.http.formvalue("config_file") - local filename = luci.http.formvalue("filename") + local upload = HTTP.formvalue("config_file") + local filename = HTTP.formvalue("filename") local config_values = {} - local raw_config = luci.http.formvalue("config") or "" + local raw_config = HTTP.formvalue("config") or "" if raw_config ~= "" then for line in raw_config:gmatch("[^\n]+") do local config_value = line:match("^%s*(.-)%s*$") @@ -3578,42 +3897,42 @@ function action_upload_overwrite() end end end - local enable = luci.http.formvalue("enable") - local order = luci.http.formvalue("order") - luci.http.prepare_content("application/json") + local enable = HTTP.formvalue("enable") + local order = HTTP.formvalue("order") + HTTP.prepare_content("application/json") if not upload or upload == "" then - luci.http.write_json({status = "error", message = "No file uploaded"}) + HTTP.write_json({status = "error", message = "No file uploaded"}) return end if not filename or filename == "" then filename = "upload_" .. os.date("%Y%m%d_%H%M%S") end if not is_safe_filename(filename) then - luci.http.write_json({status = "error", message = "Invalid filename"}) + HTTP.write_json({status = "error", message = "Invalid filename"}) return end local overwrite_dir = "/etc/openclash/overwrite/" - luci.sys.call("mkdir -p " .. overwrite_dir) + SYS.call("mkdir -p " .. overwrite_dir) local target_path = overwrite_dir .. filename if string.len(upload) == 0 then - luci.http.write_json({status = "error", message = "Uploaded file is empty"}) + HTTP.write_json({status = "error", message = "Uploaded file is empty"}) return end local file_size = string.len(upload) if file_size > 10 * 1024 * 1024 then - luci.http.write_json({status = "error", message = string.format("File size (%s) exceeds 10MB limit", require("luci.openclash").filesize(file_size))}) + HTTP.write_json({status = "error", message = string.format("File size (%s) exceeds 10MB limit", require("luci.openclash").filesize(file_size))}) return end local fp = io.open(target_path, "w") if fp then fp:write(upload) fp:close() - luci.sys.call(string.format("chmod 644 '%s'", target_path)) - luci.sys.call(string.format("chown root:root '%s'", target_path)) + SYS.call(string.format("chmod 644 '%s'", target_path)) + SYS.call(string.format("chown root:root '%s'", target_path)) local written_content = fs.readfile(target_path) if not written_content or string.len(written_content) ~= file_size then fs.unlink(target_path) - luci.http.write_json({status = "error", message = "File write verification failed"}) + HTTP.write_json({status = "error", message = "File write verification failed"}) return end @@ -3676,7 +3995,7 @@ function action_upload_overwrite() uci:commit("openclash") - luci.http.write_json({ + HTTP.write_json({ status = "success", message = "Overwrite file uploaded successfully", filename = filename, @@ -3685,21 +4004,21 @@ function action_upload_overwrite() readable_size = fs.filesize(file_size) }) else - luci.http.write_json({status = "error", message = "Failed to save file to disk"}) + HTTP.write_json({status = "error", message = "Failed to save file to disk"}) end end function action_overwrite_subscribe_info() - local method = luci.http.getenv("REQUEST_METHOD") - local filename = luci.http.formvalue("filename") - local old_filename = luci.http.formvalue("old_filename") - local typ = luci.http.formvalue("type") or "file" + local method = HTTP.getenv("REQUEST_METHOD") + local filename = HTTP.formvalue("filename") + local old_filename = HTTP.formvalue("old_filename") + local typ = HTTP.formvalue("type") or "file" local section_name = nil local old_section_name = nil if filename and not is_safe_filename(filename) then - luci.http.prepare_content("application/json") - luci.http.write_json({status = "error", message = "Invalid filename"}) + HTTP.prepare_content("application/json") + HTTP.write_json({status = "error", message = "Invalid filename"}) return end @@ -3739,21 +4058,21 @@ function action_overwrite_subscribe_info() } end end) - luci.http.prepare_content("application/json") - luci.http.write_json({status="success", data=result}) + HTTP.prepare_content("application/json") + HTTP.write_json({status="success", data=result}) return elseif method == "POST" then if not section_name then - luci.http.status(500, "Missing filename") + HTTP.status(500, "Missing filename") return end - local url = luci.http.formvalue("url") or "" - local update_days = luci.http.formvalue("update_days") or "" - local update_hour = luci.http.formvalue("update_hour") or "" - local order = luci.http.formvalue("order") - local param = luci.http.formvalue("param") or "" + local url = HTTP.formvalue("url") or "" + local update_days = HTTP.formvalue("update_days") or "" + local update_hour = HTTP.formvalue("update_hour") or "" + local order = HTTP.formvalue("order") + local param = HTTP.formvalue("param") or "" local config_values = {} - local raw_config = luci.http.formvalue("config") or "" + local raw_config = HTTP.formvalue("config") or "" if raw_config ~= "" then for line in raw_config:gmatch("[^\n]+") do local config_value = line:match("^%s*(.-)%s*$") @@ -3762,13 +4081,13 @@ function action_overwrite_subscribe_info() end end end - typ = luci.http.formvalue("type") or typ or "file" - local enable = luci.http.formvalue("enable") + typ = HTTP.formvalue("type") or typ or "file" + local enable = HTTP.formvalue("enable") if typ == "http" then if not url or url == "" then - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ status = "error", message = "Subscribe URL cannot be empty" }) @@ -3779,8 +4098,8 @@ function action_overwrite_subscribe_info() is_valid_url = true end if not is_valid_url then - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ status = "error", message = "Invalid subscribe URL format, only single HTTP/HTTPS link is supported" }) @@ -3832,8 +4151,8 @@ function action_overwrite_subscribe_info() fs.rename(old_file, new_file) end uci:commit("openclash") - luci.http.prepare_content("application/json") - luci.http.write_json({status="success"}) + HTTP.prepare_content("application/json") + HTTP.write_json({status="success"}) return end if not found then @@ -3910,13 +4229,13 @@ function action_overwrite_subscribe_info() local file_path = overwrite_dir .. section_name if url and url ~= "" then local cmd = string.format('curl -sL --connect-timeout 5 -m 15 --retry 2 "%s" -o "%s"', url, file_path) - local ret = luci.sys.call(cmd) + local ret = SYS.call(cmd) if not fs.access(file_path) then fs.writefile(file_path, "") end if ret ~= 0 or not fs.access(file_path) or fs.stat(file_path).size == 0 then - luci.http.prepare_content("application/json") - luci.http.write_json({status="error", message="Download failed"}) + HTTP.prepare_content("application/json") + HTTP.write_json({status="error", message="Download failed"}) return end else @@ -3926,11 +4245,11 @@ function action_overwrite_subscribe_info() end end - luci.http.prepare_content("application/json") - luci.http.write_json({status="success"}) + HTTP.prepare_content("application/json") + HTTP.write_json({status="success"}) return else - luci.http.status(500, "Method Not Allowed") + HTTP.status(500, "Method Not Allowed") end end @@ -3973,18 +4292,18 @@ function action_overwrite_file_list() return (a.mtime or 0) > (b.mtime or 0) end) - luci.http.prepare_content("application/json") - luci.http.write_json({ + HTTP.prepare_content("application/json") + HTTP.write_json({ overwrite_files = overwrite_files, total_count = #overwrite_files }) end function delete_overwrite_file() - local filename = luci.http.formvalue("filename") + local filename = HTTP.formvalue("filename") if not filename or filename == "" then - luci.http.prepare_content("application/json") - luci.http.write_json({status="error", message="Missing filename"}) + HTTP.prepare_content("application/json") + HTTP.write_json({status="error", message="Missing filename"}) return end local overwrite_dir = "/etc/openclash/overwrite/" @@ -4012,14 +4331,14 @@ function delete_overwrite_file() end uci:commit("openclash") - luci.http.prepare_content("application/json") - luci.http.write_json({status="success"}) + HTTP.prepare_content("application/json") + HTTP.write_json({status="success"}) end function action_get_subscribe_data() - local filename = luci.http.formvalue("filename") + local filename = HTTP.formvalue("filename") if not filename then - luci.http.status(500, "Bad Request") + HTTP.status(500, "Bad Request") return end @@ -4027,6 +4346,14 @@ function action_get_subscribe_data() uci:foreach("openclash", "config_subscribe", function(s) if s.name == filename then data = s + -- UCI list fields: convert to newline-separated strings for frontend + local sid = s['.name'] + for _, field in ipairs({"sub_headers", "keyword", "ex_keyword", "custom_params"}) do + local raw = uci:get_list("openclash", sid, field) + if raw then + data[field] = table.concat(raw, "\n") + end + end return false end end) @@ -4044,62 +4371,91 @@ function action_get_subscribe_data() end end) - luci.http.prepare_content("application/json") - luci.http.write_json(data) + HTTP.prepare_content("application/json") + HTTP.write_json(data) end function action_get_subscribe_info_data() - local filename = luci.http.formvalue("filename") + local filename = HTTP.formvalue("filename") if not filename then - luci.http.status(500, "Bad Request") + HTTP.status(500, "Bad Request") return end - luci.http.prepare_content("application/json") - luci.http.write_json(get_sub_url(filename)) + HTTP.prepare_content("application/json") + HTTP.write_json(get_sub_url(filename)) +end + +function action_subconverter_version() + local raw_url = HTTP.formvalue("url") or "" + local version_url, err = parse_subconverter_version_url(raw_url) + HTTP.prepare_content("application/json") + + if not version_url then + HTTP.write_json({status = "error", message = err or "invalid"}) + return + end + + local cmd = table.concat({ + "curl -fsS --connect-timeout 3 -m 6 --retry 0", + "-H " .. util.shellquote("Accept: text/plain, */*"), + "-H " .. util.shellquote("Origin: https://openclash.local"), + "-H " .. util.shellquote("Sec-Fetch-Mode: cors"), + "-H " .. util.shellquote("Sec-Fetch-Dest: empty"), + "-H " .. util.shellquote("User-Agent: OpenClash Subconverter Version Check"), + util.shellquote(version_url), + "2>/dev/null | head -c 4096" + }, " ") + local version = sanitize_subconverter_version_text(SYS.exec(cmd)) + + if version == "" then + HTTP.write_json({status = "unrecognized"}) + else + HTTP.write_json({status = "success", version = version}) + end end function action_generate_age_key() - local algo = luci.http.formvalue("algo") or "keygen" + local algo = HTTP.formvalue("algo") or "keygen" local cmd = string.format("%s age %s", meta_core_path, (algo == "pq" and "keygen-pq" or "keygen")) - local out = luci.sys.exec(cmd .. " 2>/dev/null") + local out = SYS.exec(cmd .. " 2>/dev/null") local secret = out:match("(AGE%-SECRET%-KEY%-%S+)") local public = out:match("# public key: ([^\n\r]+)") - luci.http.prepare_content("application/json") + HTTP.prepare_content("application/json") if not secret then - luci.http.write_json({status = "error", message = "Failed to generate age key", output = out}) + HTTP.write_json({status = "error", message = "Failed to generate age key", output = out}) return end - luci.http.write_json({status = "success", secret = secret, public = public}) + HTTP.write_json({status = "success", secret = secret, public = public}) end function action_cal_age_public_key() - local secret = luci.http.formvalue("secret") or "" + local secret = HTTP.formvalue("secret") or "" if secret == "" then - luci.http.prepare_content("application/json") - luci.http.write_json({status = "error", message = "Secret key is required"}) + HTTP.prepare_content("application/json") + HTTP.write_json({status = "error", message = "Secret key is required"}) return end local cmd = string.format("%s age convert %s", meta_core_path, secret) - local out = luci.sys.exec(cmd .. " 2>/dev/null") - luci.http.prepare_content("application/json") + local out = SYS.exec(cmd .. " 2>/dev/null") + HTTP.prepare_content("application/json") if out and out:match("^age") then - luci.http.write_json({status = "success", public = out}) + HTTP.write_json({status = "success", public = out}) else - luci.http.write_json({status = "error", message = "Failed to calculate public key, invalid secret key", output = out}) + HTTP.write_json({status = "error", message = "Failed to calculate public key, invalid secret key", output = out}) end end function action_add_age_config() - local name = luci.http.formvalue("name") - local age_secret = luci.http.formvalue("age_secret") or "" - local age_public = luci.http.formvalue("age_public") or "" - local age_algo = luci.http.formvalue("age_algo") or "" + local name = HTTP.formvalue("name") + local age_secret = HTTP.formvalue("age_secret") or "" + local age_public = HTTP.formvalue("age_public") or "" + local age_algo = HTTP.formvalue("age_algo") or "" local age_section_id, age_section_hidden - luci.http.prepare_content("application/json") + HTTP.prepare_content("application/json") if not name or name == "" then - luci.http.write_json({status = "error", message = "Missing name parameter"}) + HTTP.write_json({status = "error", message = "Missing name parameter"}) return end @@ -4112,7 +4468,7 @@ function action_add_age_config() end) if age_section_hidden then - luci.http.write_json({status = "error", message = "Cannot modify hidden age configuration"}) + HTTP.write_json({status = "error", message = "Cannot modify hidden age configuration"}) return end @@ -4146,143 +4502,256 @@ function action_add_age_config() uci:commit("openclash") end - luci.http.write_json({status = "success"}) + HTTP.write_json({status = "success"}) end function oix_login_info_save() - uci:set("openclash", "config", "oix_email", luci.http.formvalue("email")) - uci:set("openclash", "config", "oix_passwd", luci.http.formvalue("passwd")) - uci:set("openclash", "config", "oix_checkin", luci.http.formvalue("checkin")) - uci:set("openclash", "config", "oix_checkin_interval", luci.http.formvalue("interval")) - if tonumber(luci.http.formvalue("multiple")) > 100 then - uci:set("openclash", "config", "oix_checkin_multiple", "100") - elseif tonumber(luci.http.formvalue("multiple")) < 1 or not tonumber(luci.http.formvalue("multiple")) then - uci:set("openclash", "config", "oix_checkin_multiple", "1") + local token = HTTP.formvalue("token") + if token and token ~= "" then + uci:set("openclash", "config", "oix_token", token) else - uci:set("openclash", "config", "oix_checkin_multiple", luci.http.formvalue("multiple")) + local email = HTTP.formvalue("email") + local passwd = HTTP.formvalue("passwd") + if email then uci:set("openclash", "config", "oix_email", email) end + if passwd then uci:set("openclash", "config", "oix_passwd", passwd) end end + local checkin = HTTP.formvalue("checkin") + if checkin then uci:set("openclash", "config", "oix_checkin", checkin) end + local interval = tonumber(HTTP.formvalue("interval")) + if interval then + if interval < 1 then interval = 1 end + if interval > 720 then interval = 720 end + uci:set("openclash", "config", "oix_checkin_interval", tostring(interval)) + end + local multiple = tonumber(HTTP.formvalue("multiple")) + if multiple then + if multiple < 1 then multiple = 1 end + if multiple > 100 then multiple = 100 end + uci:set("openclash", "config", "oix_checkin_multiple", tostring(multiple)) + end + local show_info_page = HTTP.formvalue("show_info_page") + if show_info_page then uci:set("openclash", "config", "oix_show_info_page", show_info_page) end + local default_params = HTTP.formvalue("default_params") + if default_params then uci:set("openclash", "config", "oix_default_params", default_params) end uci:commit("openclash") - luci.http.prepare_content("application/json") - luci.http.write_json({status = "success"}) + HTTP.prepare_content("application/json") + HTTP.write_json({status = "success"}) +end + +function oix_params_sync() + local params = HTTP.formvalue("params") or "" + if #params > 8192 then + HTTP.status(400, "Params too long") + HTTP.write_json({status = "error", msg = "params too long"}) + return + end + + uci:set("openclash", "config", "oix_params", params) + uci:commit("openclash") + + if is_running() then + local dase_val = dase() or "" + local daip_val = daip() + local cn_port_val = cn_port() + local auth_header = "" + if dase_val and dase_val ~= "" then + auth_header = string.format('-H "Authorization: Bearer %s"', dase_val) + end + + if params == "" then + SYS.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" %s -XDELETE http://"%s":"%s"/oix/options', auth_header, daip_val, cn_port_val)) + else + local encoded = params:gsub('"', '\\"'):gsub('\n', '') + SYS.exec(string.format('curl -sL -m 3 --retry 2 -H "Content-Type: application/json" %s -XPUT -d \'{"params":"%s"}\' http://"%s":"%s"/oix/options', auth_header, encoded, daip_val, cn_port_val)) + end + end + + HTTP.prepare_content("application/json") + HTTP.write_json({status = "success"}) +end + +function oix_params_get() + local result = {params = "", default_params = ""} + local home_dir = "/etc/openclash" + local params_file = home_dir .. "/.oix_params" + local default_params_file = home_dir .. "/.oix_default_params" + + if fs.access(params_file) then + local content = fs.readfile(params_file) + if content then + result.params = content:gsub("%s+", "") + end + end + if fs.access(default_params_file) then + local content = fs.readfile(default_params_file) + if content then + result.default_params = content:gsub("%s+", "") + end + end + + HTTP.prepare_content("application/json") + HTTP.write_json(result) +end + +local function fetch_oix_sub(token) + local get_sub = string.format("curl -sL -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -X POST https://oix-api.dler.io/api/v1/managed/clash", token) + local sub_info = SYS.exec(get_sub) + if sub_info then sub_info = json.parse(sub_info) end + if sub_info and sub_info.ret == 200 then + local sub_key = {"openclash"} + for _,v in ipairs(sub_key) do + while true do + local sub_match, sub_convert, sid = false, false, nil + uci:foreach("openclash", "config_subscribe", + function(s) + if s.name == "oixCloud - smart" and s.address == sub_info[v] then + sub_match = true; return false + end + if s.name == "oixCloud - smart" and s.address ~= sub_info[v] then + sub_convert = true; sid = s['.name']; return false + end + end) + if sub_match then break end + if sub_convert then uci:set("openclash", sid, "address", sub_info[v]) + elseif sub_info[v] then + sid = uci:add("openclash", "config_subscribe") + uci:set("openclash", sid, "name", "oixCloud - smart") + uci:set("openclash", sid, "address", sub_info[v]) + end + uci:commit("openclash") + break + end + if sub_info[v] then + SYS.exec(string.format('curl -sL -m 10 --retry 2 --user-agent "clash" "%s" -o "/etc/openclash/config/oixCloud - smart.yaml" >/dev/null 2>&1', sub_info[v])) + local core = coremetacv() + if core ~= "0" and not string.match(core, "oix") then + SYS.exec("/usr/share/openclash/openclash_core.sh Oix") + else + SYS.call("/etc/init.d/openclash restart >/dev/null 2>&1 &") + end + end + end + return true + end + return false end function oix_login() - local result, info, token, get_sub, sub_info, sub_key, sub_match, sub_convert, sid, sub_file, SIGNATURE + local result, info, token + local input_token = HTTP.formvalue("token") local email = fs.uci_get_config("config", "oix_email") local passwd = fs.uci_get_config("config", "oix_passwd") - token = fs.uci_get_config("config", "oix_token") - if email and passwd then - info = luci.sys.exec(string.format("curl -sL -H 'Content-Type: application/json' -H 'User-Agent: OpenClash' -d '{\"email\":\"%s\", \"passwd\":\"%s\", \"token_expire\":\"365\" }' -X POST https://oix-api.dler.io/api/v1/login", email, passwd)) - if info then - info = json.parse(info) + if input_token and input_token ~= "" then + -- Token direct login mode + uci:set("openclash", "config", "oix_token", input_token) + uci:commit("openclash") + token = input_token + if fetch_oix_sub(token) then + result = 200 + else + result = "invalid token" end - if info and info.ret == 200 then - -- because of uci cache, need reload after delete if get new - if token and token ~= "" then - oix_logout(token) + else + -- Email/password login mode + token = fs.uci_get_config("config", "oix_token") + if email and passwd then + info = SYS.exec(string.format("curl -sL -H 'Content-Type: application/json' -H 'User-Agent: OpenClash for oixCloud' -d '{\"email\":\"%s\", \"passwd\":\"%s\", \"token_expire\":\"365\" }' -X POST https://oix-api.dler.io/api/v1/login", email, passwd)) + if info then + info = json.parse(info) end - token = info.data.token - uci:set("openclash", "config", "oix_token", token) - uci:commit("openclash") - get_sub = string.format("curl -sL -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -X POST https://oix-api.dler.io/api/v1/managed/clash", token) - sub_info = luci.sys.exec(get_sub) - if sub_info then - sub_info = json.parse(sub_info) - end - if sub_info and sub_info.ret == 200 then - sub_key = {"openclash"} - for _,v in ipairs(sub_key) do - while true do - sub_match = false - sub_convert = false - uci:foreach("openclash", "config_subscribe", - function(s) - if s.name == "oixCloud - smart" and s.address == sub_info[v] then - sub_match = true - return false - end - if s.name == "oixCloud - smart" and s.address ~= sub_info[v] then - sub_convert = true - sid = s['.name'] - return false - end - end) - if sub_match then break end - if sub_convert then - uci:set("openclash", sid, "address", sub_info[v]) - elseif sub_info[v] then - sid = uci:add("openclash", "config_subscribe") - uci:set("openclash", sid, "name", "oixCloud - smart") - uci:set("openclash", sid, "address", sub_info[v]) - end - uci:commit("openclash") - break - end - if sub_info[v] then - luci.sys.exec(string.format('curl -sL -m 10 --retry 2 --user-agent "clash" "%s" -o "/etc/openclash/config/oixCloud - smart.yaml" >/dev/null 2>&1', sub_info[v])) - luci.sys.call("/etc/init.d/openclash restart >/dev/null 2>&1 &") - end + if info and info.ret == 200 then + if token and token ~= "" then + oix_logout(token) end + token = info.data.token + uci:set("openclash", "config", "oix_token", token) + uci:commit("openclash") + result = info.ret + fetch_oix_sub(token) + else + uci:delete("openclash", "config", "oix_token") + uci:commit("openclash") + fs.unlink("/tmp/oix_checkin") + fs.unlink("/tmp/oix_info") + if info and info.msg then + result = info.msg + else + result = "login failed" + end + HTTP.prepare_content("application/json") + HTTP.write_json({result = result}) + return end - result = info.ret else uci:delete("openclash", "config", "oix_token") uci:commit("openclash") fs.unlink("/tmp/oix_checkin") fs.unlink("/tmp/oix_info") - if info and info.msg then - result = info.msg - else - result = "login faild" - end + result = "email or passwd is wrong" + HTTP.prepare_content("application/json") + HTTP.write_json({result = result}) + return end - else - uci:delete("openclash", "config", "oix_token") - uci:commit("openclash") - fs.unlink("/tmp/oix_checkin") - fs.unlink("/tmp/oix_info") - result = "email or passwd is wrong" end - luci.http.prepare_content("application/json") - luci.http.write_json({result = result}) + HTTP.prepare_content("application/json") + HTTP.write_json({result = result}) end function oix_logout(oldtoken) local info, result, token + local is_token_login = false if not oldtoken then token = fs.uci_get_config("config", "oix_token") + is_token_login = not fs.uci_get_config("config", "oix_email") else token = oldtoken end if token then - info = luci.sys.exec(string.format("curl -sL -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -X POST https://oix-api.dler.io/api/v1/logout", token)) - if info then - info = json.parse(info) - end - if info and info.ret == 200 then + if is_token_login then uci:delete("openclash", "config", "oix_token") - if not oldtoken then - uci:delete("openclash", "config", "oix_checkin") - uci:delete("openclash", "config", "oix_checkin_interval") - uci:delete("openclash", "config", "oix_checkin_multiple") - end + uci:delete("openclash", "config", "oix_checkin") + uci:delete("openclash", "config", "oix_checkin_interval") + uci:delete("openclash", "config", "oix_checkin_multiple") + uci:delete("openclash", "config", "oix_params") + uci:delete("openclash", "config", "oix_default_params") uci:commit("openclash") fs.unlink("/tmp/oix_checkin") fs.unlink("/tmp/oix_info") - result = info.ret + result = 200 else - if info and info.msg then - result = info.msg + info = SYS.exec(string.format("curl -sL -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -X POST https://oix-api.dler.io/api/v1/logout", token)) + if info then + info = json.parse(info) + end + if info and info.ret == 200 then + uci:delete("openclash", "config", "oix_token") + if not oldtoken then + uci:delete("openclash", "config", "oix_email") + uci:delete("openclash", "config", "oix_passwd") + uci:delete("openclash", "config", "oix_checkin") + uci:delete("openclash", "config", "oix_checkin_interval") + uci:delete("openclash", "config", "oix_checkin_multiple") + uci:delete("openclash", "config", "oix_params") + uci:delete("openclash", "config", "oix_default_params") + end + uci:commit("openclash") + fs.unlink("/tmp/oix_checkin") + fs.unlink("/tmp/oix_info") + result = info.ret else - result = "logout faild" + if info and info.msg then + result = info.msg + else + result = "logout failed" + end end end else - result = "logout faild" + result = "logout failed" end if not oldtoken then - luci.http.prepare_content("application/json") - luci.http.write_json({result = result}) + HTTP.prepare_content("application/json") + HTTP.write_json({result = result}) end end @@ -4294,13 +4763,13 @@ function oix_info() if token then get_info = string.format("curl -sL -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -X POST https://oix-api.dler.io/api/v1/information -o %s", token, path) if not fs.access(path) then - luci.sys.exec(get_info) + SYS.exec(get_info) else if fs.readfile(path) == "" or not fs.readfile(path) then - luci.sys.exec(get_info) + SYS.exec(get_info) else if (os.time() - fs.mtime(path) > 900) then - luci.sys.exec(get_info) + SYS.exec(get_info) end end end @@ -4316,8 +4785,8 @@ function oix_info() fs.unlink(path) end end - luci.http.prepare_content("application/json") - luci.http.write_json({result = result}) + HTTP.prepare_content("application/json") + HTTP.write_json({result = result}) end function oix_checkin() @@ -4326,26 +4795,26 @@ function oix_checkin() local token = fs.uci_get_config("config", "oix_token") local multiple = fs.uci_get_config("config", "oix_checkin_multiple") or 1 if token then - info = luci.sys.exec(string.format("curl -sL -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '{\"multiple\":\"%s\"}' -X POST https://oix-api.dler.io/api/v1/checkin", token, token, multiple)) + info = SYS.exec(string.format("curl -sL -H 'Content-Type: application/json' -H 'Authorization: Bearer %s' -d '{\"multiple\":\"%s\"}' -X POST https://oix-api.dler.io/api/v1/checkin", token, multiple)) if info then info = json.parse(info) end if info and info.ret == 200 then fs.unlink("/tmp/oix_info") fs.writefile(path, info) - luci.sys.exec(string.format("echo -e %s oixCloud Checkin Successful, Result:【%s】 >> /tmp/openclash.log", os.date("%Y-%m-%d %H:%M:%S"), info.data.checkin)) + SYS.exec(string.format("echo -e %s [Info] oixCloud Checkin Successful, Result:【%s】 >> /tmp/openclash.log", os.date("%Y-%m-%d %H:%M:%S"), info.data.checkin)) result = info else if info and info.msg then - luci.sys.exec(string.format("echo -e %s oixCloud Checkin Failed, Result:【%s】 >> /tmp/openclash.log", os.date("%Y-%m-%d %H:%M:%S"), info.msg)) + SYS.exec(string.format("echo -e %s [Info] oixCloud 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 oixCloud Checkin Failed! Please Check And Try Again... >> /tmp/openclash.log",os.date("%Y-%m-%d %H:%M:%S"))) + SYS.exec(string.format("echo -e %s [Info] oixCloud Checkin Failed! Please Check And Try Again... >> /tmp/openclash.log",os.date("%Y-%m-%d %H:%M:%S"))) end result = info end else result = "error" end - luci.http.prepare_content("application/json") - luci.http.write_json({result = result}) -end \ No newline at end of file + HTTP.prepare_content("application/json") + HTTP.write_json({result = result}) +end diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/client.lua b/luci-app-openclash/luasrc/model/cbi/openclash/client.lua index 9ff273be0..439231301 100644 --- a/luci-app-openclash/luasrc/model/cbi/openclash/client.lua +++ b/luci-app-openclash/luasrc/model/cbi/openclash/client.lua @@ -13,7 +13,7 @@ m.reset = false m.submit = false m:section(SimpleSection).template = "openclash/status" -if fs.uci_get_config("config", "oix_token") then +if fs.uci_get_config("config", "oix_token") and fs.uci_get_config("config", "oix_show_info_page") ~= "0" then m:append(Template("openclash/oixcloud")) end m:append(Template("openclash/myip")) diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/config-overwrite.lua b/luci-app-openclash/luasrc/model/cbi/openclash/config-overwrite.lua index 9bd7ee0c1..8ef9f6e0d 100644 --- a/luci-app-openclash/luasrc/model/cbi/openclash/config-overwrite.lua +++ b/luci-app-openclash/luasrc/model/cbi/openclash/config-overwrite.lua @@ -94,7 +94,7 @@ o:value("http://captive.apple.com/generate_204") o.default = "0" o = s:taboption("settings", Value, "github_address_mod", translate("Github Address Modify")) -o.description = translate("Modify The Github Address In The Config And OpenClash With Proxy(CDN) To Prevent File Download Faild. Format Reference:").." ".."https://ghfast.top/" +o.description = translate("Modify The Github Address In The Config And OpenClash With Proxy(CDN) To Prevent File Download Failed. Format Reference:").." ".."https://ghfast.top/" o:value("0", translate("Disable")) o:value("https://fastly.jsdelivr.net/") o:value("https://testingcf.jsdelivr.net/") @@ -215,6 +215,7 @@ function custom_fallback_filter.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_fallback_filter.yaml", value) end end + return true end if op_mode == "fake-ip" then @@ -247,6 +248,7 @@ function custom_fake_black.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_fake_filter.list", value) end end + return true end end @@ -271,6 +273,7 @@ function custom_domain_dns_policy.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_domain_dns_policy.list", value) end end + return true end o = s:taboption("dns", Flag, "custom_proxy_server_policy", translate("Proxy-Server-Nameserver-Policy")) @@ -294,6 +297,7 @@ function custom_proxy_server_dns_policy.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_proxy_server_dns_policy.list", value) end end + return true end o = s:taboption("dns", Flag, "custom_host", translate("Hosts")) @@ -317,6 +321,7 @@ function custom_hosts.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_hosts.list", value) end end + return true end -- Meta @@ -384,6 +389,7 @@ function sniffer_custom.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_sniffer.yaml", value) end end + return true end -- Smart Settings @@ -476,6 +482,7 @@ function custom_rules.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_rules.list", value) end end + return true end custom_rules_2 = s:taboption("rules", Value, "custom_rules_2") @@ -496,6 +503,7 @@ function custom_rules_2.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_rules_2.list", value) end end + return true end -- [[ Edit Custom DNS ]] -- diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/config-subscribe-edit.lua b/luci-app-openclash/luasrc/model/cbi/openclash/config-subscribe-edit.lua index 40995f958..875302c28 100644 --- a/luci-app-openclash/luasrc/model/cbi/openclash/config-subscribe-edit.lua +++ b/luci-app-openclash/luasrc/model/cbi/openclash/config-subscribe-edit.lua @@ -9,6 +9,14 @@ local HTTP = require "luci.http" local DISP = require "luci.dispatcher" local sid = arg[1] local age_section +local backend_version_status = [[ +
+ +
+]] font_red = [[]] font_off = [[]] @@ -67,6 +75,12 @@ o:value("clash.meta/1.19.20") o:value("Clash") o.rmempty = true +---- Custom Headers +o = s:option(DynamicList, "sub_headers", translate("Custom Headers")) +o.description = font_red..bold_on..translate("Custom HTTP request headers, one per line, format: Header-Name: value")..bold_off..font_off +o.rmempty = true +o.placeholder = "x-hwid: my-device-id" + o = s:option(ListValue, "config_age_algo", translate("Age Key Type")) o.description = font_red..bold_on..translate("Age Encryption For Config, Click For More:")..bold_off..font_off.." "..""..translate("Age Encryption Introduce").."" o:value("keygen", "x25519") @@ -197,7 +211,7 @@ o.default = 0 ---- Convert Address o = s:option(Value, "convert_address", translate("Convert Address")) o.rmempty = true -o.description = font_red..bold_on..translate("Note: There is A Risk of Privacy Leakage in Online Convert")..bold_off..font_off +o.description = backend_version_status..font_red..bold_on..translate("Note: There is A Risk of Privacy Leakage in Online Convert")..bold_off..font_off o:depends("sub_convert", "1") o:value("https://api.asailor.org/sub", translate("api.asailor.org")) o:value("https://api.wcc.best/sub", translate("api.wcc.best")) @@ -331,4 +345,5 @@ o.write = function() end m:append(Template("openclash/toolbar_show")) +m:append(Template("openclash/subconverter_version")) return m diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/config.lua b/luci-app-openclash/luasrc/model/cbi/openclash/config.lua index 2d7607c60..fd616a0a1 100644 --- a/luci-app-openclash/luasrc/model/cbi/openclash/config.lua +++ b/luci-app-openclash/luasrc/model/cbi/openclash/config.lua @@ -350,7 +350,6 @@ local tab = { } s = m:section(Table, tab) -s.description = align_mid..translate("Support syntax check, press").." "..font_green..bold_on.."F10"..bold_off..font_off.." "..translate("to control diff option, press").." "..font_green..bold_on.."F11"..bold_off..font_off.." "..translate("to enter full screen editing mode")..align_mid_off s.anonymous = true s.addremove = false @@ -361,6 +360,8 @@ local conf_name = fs.basename(conf) if not conf_name then conf_name = "config.yaml" end local sconf = "/etc/openclash/"..conf_name +s.description = align_mid..translate("Current Config")..": "..font_green..bold_on..conf_name..bold_off..font_off..align_mid_off + sev = s:option(TextValue, "user") ---sev.description = align_mid..translate("Modify Your Config file:").." "..font_green..bold_on..conf_name..bold_off..font_off.." "..translate("Here, Except The Settings That Were Taken Over")..align_mid_off sev.rows = 40 @@ -421,7 +422,7 @@ o.write = function() HTTP.redirect(DISP.build_url("admin", "services", "openclash")) end -m:append(Template("openclash/config_editor")) +m:append(Template("openclash/config_merge_editor")) m:append(Template("openclash/config_upload")) return ful , form , p , m diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/other-file-edit.lua b/luci-app-openclash/luasrc/model/cbi/openclash/other-file-edit.lua index 0c28e0958..77a43e25d 100644 --- a/luci-app-openclash/luasrc/model/cbi/openclash/other-file-edit.lua +++ b/luci-app-openclash/luasrc/model/cbi/openclash/other-file-edit.lua @@ -29,6 +29,7 @@ function o.write(self, section, value) fs.writefile(file_path, value) end end + return true end function o.cfgvalue(self, section) diff --git a/luci-app-openclash/luasrc/model/cbi/openclash/settings.lua b/luci-app-openclash/luasrc/model/cbi/openclash/settings.lua index b7c9ddbab..1142c997f 100644 --- a/luci-app-openclash/luasrc/model/cbi/openclash/settings.lua +++ b/luci-app-openclash/luasrc/model/cbi/openclash/settings.lua @@ -229,6 +229,7 @@ function custom_domain_dns.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_domain_dns.list", value) end end + return true end ---- Access Control @@ -488,7 +489,7 @@ o.default = 0 o = s:taboption("traffic_control", Value, "common_ports", font_red..bold_on..translate("Common Ports Proxy Mode")..bold_off..font_off) o.description = translate("Only Common Ports, Prevent BT/P2P Passing") o:value("0", translate("Disable")) -o:value("21 22 23 53 80 123 143 194 443 465 587 853 993 995 998 2052 2053 2082 2083 2086 2095 2096 5222 5228 5229 5230 8080 8443 8880 8888 8889", translate("Default Common Ports")) +o:value("21 22 23 53 80 123 143 194 443 465 587 853 993 995 998 2052 2053 2082 2083 2086 2095 2096 2197 5222 5223 5228 5229 5230 8080 8443 8880 8888 8889", translate("Default Common Ports")) o.default = 0 o.placeholder = translate("443 or 21-443, Use Space to Separate") o:depends("en_mode", "redir-host") @@ -538,6 +539,7 @@ function o.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_localnetwork_ipv4.list", value) end end + return true end o = s:taboption("traffic_control", Value, "chnroute_pass", translate("Chnroute Bypassed List")) @@ -559,6 +561,7 @@ function o.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_chnroute_pass.list", value) end end + return true end --Stream Enhance @@ -1366,6 +1369,7 @@ function o.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_localnetwork_ipv6.list", value) end end + return true end o = s:taboption("ipv6", Value, "chnroute6_pass", translate("Chnroute6 Bypassed List")) @@ -1386,6 +1390,7 @@ function o.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_chnroute6_pass.list", value) end end + return true end ---- version update @@ -1410,6 +1415,7 @@ function o.write(self, section, value) fs.writefile("/etc/openclash/custom/openclash_custom_firewall_rules.sh", value) end end + return true end ---- debug @@ -1417,52 +1423,9 @@ o = s:taboption("debug", DummyValue, "", nil) o.template = "openclash/debug" ---- oixcloud -o = s:taboption("oixcloud", Value, "oix_email") -o.title = translate("Account Email Address") -o.rmempty = true - -o = s:taboption("oixcloud", Value, "oix_passwd") -o.title = translate("Account Password") -o.password = true -o.rmempty = true - -if fs.uci_get_config("config", "oix_token") then - o = s:taboption("oixcloud", Flag, "oix_checkin") - o.title = translate("Checkin") - o.default = 0 - o.rmempty = true -end - -o = s:taboption("oixcloud", Value, "oix_checkin_interval") -o.title = translate("Checkin Interval (hour)") -o:depends("oix_checkin", "1") -o.default = "1" -o.rmempty = true - -o = s:taboption("oixcloud", Value, "oix_checkin_multiple") -o.title = translate("Checkin Multiple") -o.datatype = "uinteger" -o.default = "1" -o:depends("oix_checkin", "1") -o.rmempty = true -o.description = font_green..bold_on..translate("Multiple Must Be a Positive Integer and No More Than 100")..bold_off..font_off -function o.validate(self, value) - if tonumber(value) < 1 then - return "1" - end - if tonumber(value) > 100 then - return "100" - end - return value -end - -o = s:taboption("oixcloud", DummyValue, "oix_login", translate("Account Login")) -o.template = "openclash/oix_login" -if fs.uci_get_config("config", "oix_token") then - o.value = font_green..bold_on..translate("Account logged in")..bold_off..font_off -else - o.value = font_red..bold_on..translate("Account not logged in")..bold_off..font_off -end +oixcloud_panel = s:taboption("oixcloud", DummyValue, "", nil) +oixcloud_panel.template = "openclash/oix_login" +oixcloud_panel.rawhtml = true local t = { {Commit, Apply} diff --git a/luci-app-openclash/luasrc/openclash.lua b/luci-app-openclash/luasrc/openclash.lua index b38bee381..919280e27 100644 --- a/luci-app-openclash/luasrc/openclash.lua +++ b/luci-app-openclash/luasrc/openclash.lua @@ -603,7 +603,19 @@ end --- Returns the installed version of luci-app-openclash. -- Supports both opkg and apk package managers. -- @return String containing the version number, or "0" if not found +-- NOTE: The module-level cache (_oc_version_cache) provides per-request +-- deduplication. In OpenWrt LuCI CGI mode each HTTP request spawns a new Lua +-- process, so cross-request caching is impossible. +-- Since require() returns the same module instance (package.loaded), the +-- first call caches the result and subsequent calls avoid redundant shell +-- commands. No time-based expiration is needed — the version is immutable +-- for the lifetime of a request. +local _oc_version_cache = nil + function oc_version() + if _oc_version_cache ~= nil then + return _oc_version_cache + end local v if pkg_type() == "opkg" then v = SYS.exec("rm -f /var/lock/opkg.lock && opkg status luci-app-openclash 2>/dev/null |grep '^Version:' |awk '{print $2}' |tr -d '\n'") @@ -613,6 +625,7 @@ function oc_version() if v == "" then v = "0" end + _oc_version_cache = v return v end diff --git a/luci-app-openclash/luasrc/view/openclash/common.htm b/luci-app-openclash/luasrc/view/openclash/common.htm new file mode 100644 index 000000000..97a4b63e7 --- /dev/null +++ b/luci-app-openclash/luasrc/view/openclash/common.htm @@ -0,0 +1,50 @@ + diff --git a/luci-app-openclash/luasrc/view/openclash/config_edit.htm b/luci-app-openclash/luasrc/view/openclash/config_edit.htm index 384c0c4cd..b92cd98e3 100644 --- a/luci-app-openclash/luasrc/view/openclash/config_edit.htm +++ b/luci-app-openclash/luasrc/view/openclash/config_edit.htm @@ -12,40 +12,22 @@
-
-
- <%:Loading config file...%> -
@@ -89,7 +67,7 @@ <%:Ready%>
- <%:Press F11 for fullscreen, Esc to exit fullscreen, Ctrl+Mouse Wheel to zoom%> + <%:Press%> F11 <%:for fullscreen,%> Esc <%:to exit fullscreen,%> Ctrl + <%:Mouse Wheel%> <%:to zoom%>
@@ -97,22 +75,26 @@ + + +<%+openclash/common%> \ No newline at end of file diff --git a/luci-app-openclash/luasrc/view/openclash/config_editor.htm b/luci-app-openclash/luasrc/view/openclash/config_editor.htm index bb7668827..0aaedf95c 100644 --- a/luci-app-openclash/luasrc/view/openclash/config_editor.htm +++ b/luci-app-openclash/luasrc/view/openclash/config_editor.htm @@ -2,91 +2,33 @@ local fs = require "luci.openclash" local plugin_version = fs.oc_version() %> + - + <%- -local uci = require("luci.model.uci").cursor() -local fs = require "luci.openclash" -local conf = fs.uci_get_config("config", "config_path") -if not conf then conf = "/etc/openclash/config/config.yaml" end -local conf_name = fs.basename(conf) -if not conf_name then conf_name = "config.yaml" end -local sconf = "/etc/openclash/"..conf_name local path_info = os.getenv("PATH_INFO") or "" local sid = path_info:match("(cfg[0-9a-f]+)") or "" -%> - - - - -<%- - if fs.isfile(sconf) then --%> - -<%- - else --%> - -<%- - end --%> - - - +<%+openclash/common%> + + + + +
+
+
+ + +
+
+
+ + + + + + diff --git a/luci-app-openclash/luasrc/view/openclash/config_upload.htm b/luci-app-openclash/luasrc/view/openclash/config_upload.htm index 4ae60f43f..d4dcf75ce 100644 --- a/luci-app-openclash/luasrc/view/openclash/config_upload.htm +++ b/luci-app-openclash/luasrc/view/openclash/config_upload.htm @@ -1,4 +1,10 @@
+<% + local fs = require "luci.openclash" + local plugin_version = fs.oc_version() +%> + +
@@ -7,10 +13,7 @@
@@ -18,20 +21,11 @@
@@ -40,11 +34,7 @@
- - - - - +
<%:Click to select file or drag and drop%>
@@ -87,6 +77,13 @@
+
+ +
<%:Add custom HTTP headers to the subscription request%>
+ + +
+
+
<%:Note: There is A Risk of Privacy Leakage in Online Convert%>
@@ -261,9 +262,7 @@
<%:Age Encryption For Config, Click For More:%> <%:Age Encryption Introduce%>
@@ -274,9 +273,7 @@
@@ -286,18 +283,10 @@
@@ -334,7 +323,7 @@
<%:Set Subscription Info URL%>
@@ -355,6 +344,80 @@
+
@@ -155,3 +156,4 @@ local dns_host = "www.instagram.com"
+
diff --git a/luci-app-openclash/luasrc/view/openclash/developer.htm b/luci-app-openclash/luasrc/view/openclash/developer.htm index ec83ce871..06d0d3bbf 100644 --- a/luci-app-openclash/luasrc/view/openclash/developer.htm +++ b/luci-app-openclash/luasrc/view/openclash/developer.htm @@ -73,7 +73,7 @@ immortalwrt.innerHTML = createAvatarHTML('https://avatars.githubusercontent.com/u/82308430?v=4', 'immortalwrt', 'immortalwrt', 'immortalwrt_rediret'); lmc999.innerHTML = createAvatarHTML('https://avatars.githubusercontent.com/u/39726914?v=4', 'lmc999', 'lmc999', 'lmc999_rediret'); MetaCubeX.innerHTML = createAvatarHTML('https://avatars.githubusercontent.com/u/84378451?v=4', 'MetaCubeX', 'MetaCubeX', 'MetaCubeX_rediret'); - oixcloud.innerHTML = createAvatarHTML('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAAErdZjwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGsGlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDggNzkuMTY0MDM2LCAyMDE5LzA4LzEzLTAxOjA2OjU3ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjEuMCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDI2LTA2LTExVDAwOjU0OjU4KzA4OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyNi0wNi0xMVQyMzoyNjowNyswODowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyNi0wNi0xMVQyMzoyNjowNyswODowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo0ZjA3Mjk4Yi03M2E1LTcxNDItODE2MS03ZTkyNDlmMGU0ODkiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OGM3YzA4NjMtYzdlOS0yYzQ1LTk1ODYtNDgxZjMxYzM0MmFjIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6OGM3YzA4NjMtYzdlOS0yYzQ1LTk1ODYtNDgxZjMxYzM0MmFjIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo4YzdjMDg2My1jN2U5LTJjNDUtOTU4Ni00ODFmMzFjMzQyYWMiIHN0RXZ0OndoZW49IjIwMjYtMDYtMTFUMDA6NTQ6NTgrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyMS4wIChXaW5kb3dzKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MTg5NzBjODctZDk2My1iMjRiLWJlNDEtOTkyMTE5MjhhNDIxIiBzdEV2dDp3aGVuPSIyMDI2LTA2LTExVDIzOjE0OjA5KzA4OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgMjEuMCAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjRmMDcyOThiLTczYTUtNzE0Mi04MTYxLTdlOTI0OWYwZTQ4OSIgc3RFdnQ6d2hlbj0iMjAyNi0wNi0xMVQyMzoyNjowNyswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDIxLjAgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PiagcG0AADMvSURBVHic7b15eFRFvv9fZ+k1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', 'oixCloud', 'oixcloud', 'oixcloud_rediret'); + oixcloud.innerHTML = createAvatarHTML('/luci-static/resources/openclash/img/oixcloud.png', 'oixCloud', 'oixcloud', 'oixcloud_rediret'); more.innerHTML = createMoreAvatarHTML('<%:More%>', 'more_rediret'); } diff --git a/luci-app-openclash/luasrc/view/openclash/log.htm b/luci-app-openclash/luasrc/view/openclash/log.htm index 6e44f8044..3545d770f 100644 --- a/luci-app-openclash/luasrc/view/openclash/log.htm +++ b/luci-app-openclash/luasrc/view/openclash/log.htm @@ -1,6 +1,12 @@ <%+cbi/valueheader%> +<% + local fs = require "luci.openclash" + local plugin_version = fs.oc_version() +%> + +<%+openclash/common%> -
+
@@ -45,7 +51,7 @@ - +
@@ -68,6 +74,37 @@ var wsRetryDelay = 2000; var wsRetryTimer = null; var isPolling = false; var core_refresh = true; +var wsHasData = false; +var coreEditorInitialized = false; +var editor_core; +var coreLogAccumulator = ''; +var pollInterval = 1000; + +function accumulateCoreLog(linesArr) { + var reversed = linesArr.slice().reverse(); + var batchStr = ''; + for (var k = 0; k < reversed.length; k++) { + batchStr += reversed[k] + '\n'; + } + if (coreLogAccumulator) { + var accFirstLineEnd = coreLogAccumulator.indexOf('\n'); + var accFirstLine = accFirstLineEnd > 0 ? coreLogAccumulator.substring(0, accFirstLineEnd) : coreLogAccumulator; + var batchTrimmed = batchStr.slice(0, -1); + var batchLastLineStart = batchTrimmed.lastIndexOf('\n'); + var batchLastLine = batchLastLineStart > 0 ? batchTrimmed.substring(batchLastLineStart + 1) : batchTrimmed; + if (accFirstLine === batchLastLine) { + batchStr = batchLastLineStart > 0 ? batchStr.substring(0, batchLastLineStart + 1) : ''; + } + } + coreLogAccumulator = batchStr + coreLogAccumulator; + var accLines = coreLogAccumulator.split('\n'); + if (accLines.length > 2000) { + accLines = accLines.slice(0, 1999); + accLines.push('...'); + coreLogAccumulator = accLines.join('\n'); + } + cl.value = coreLogAccumulator; +} function setLogTab(log_level) { var radio = document.getElementsByName("radios"); @@ -235,7 +272,7 @@ function p(s) { }; function getWebSocketConnectionInfo(callback) { - XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "status")%>', null, function(x, status) { + XHR.get('<%=luci.dispatcher.build_url("admin", "services", "openclash", "conn_status")%>', null, function(x, status) { if (x && x.status == 200 && status) { callback(status); } else { @@ -268,6 +305,7 @@ function coreLogWebSocketStop() { } catch (e) {} coreLogBuffer = []; + wsHasData = false; } function coreLogWebSocketRetry() { @@ -307,6 +345,7 @@ function coreLogWebSocket() { coreWebSocket = new WebSocket(wsUrl); coreWebSocket.onopen = function() { coreLogBuffer = []; + wsHasData = false; }; coreWebSocket.onmessage = function(event) { @@ -314,6 +353,7 @@ function coreLogWebSocket() { try { var msgData = JSON.parse(event.data); if (msgData && msgData.payload) { + wsHasData = true; var now = new Date(); var timestamp = now.getFullYear() + "-" + p(now.getMonth() + 1) + "-" + p(now.getDate()) + " " + p(now.getHours()) + ":" + p(now.getMinutes()) + ":" + p(now.getSeconds()); @@ -509,8 +549,9 @@ function smoothlyDisplayLogs(newLines, target, isEditor, currentContent, isActiv } } else if (isAtTop) { target.scrollTo(0, 0); - if (isFirstLoad) { - target.setCursor({line: 0, ch: 0}); + if (!cursorPos) { + var firstLineLen = target.state.doc.line(1).length; + target.setCursor({line: 0, ch: firstLineLen}); } } @@ -587,6 +628,13 @@ function smoothlyDisplayLogs(newLines, target, isEditor, currentContent, isActiv target.scrollIntoView({line: cursorPos.line, ch: cursorPos.ch}, 300); } } + if (isEditor && !cursorPos) { + var doc = target.state.doc; + if (doc.lines >= 1) { + var fl = doc.line(1); + target.dispatch({ selection: { anchor: fl.to } }); + } + } return; } @@ -652,6 +700,13 @@ function smoothlyDisplayLogs(newLines, target, isEditor, currentContent, isActiv target.scrollIntoView({line: cursorPos.line, ch: cursorPos.ch}, 300); } } + if (isEditor && !cursorPos) { + var doc = target.state.doc; + if (doc.lines >= 1) { + var fl = doc.line(1); + target.dispatch({ selection: { anchor: fl.to } }); + } + } } } @@ -677,17 +732,16 @@ function poll_log(){ } if (coreWebSocket && coreWebSocket.readyState == WebSocket.OPEN) { - core_refresh = false; + core_refresh = !wsHasData; if (coreLogBuffer.length > 0) { bufferToProcess = coreLogBuffer; coreLogBuffer = []; core_logs = line_tolocal(bufferToProcess.join('\n')); - if (editor_core) { + if (editor_core && activeTabId === 1) { currentCoreContent = editor_core.getValue(); - smoothlyDisplayLogs(core_logs, editor_core, true, currentCoreContent, activeTabId === 1); - } else if (cl) { - currentCoreContent = cl.innerHTML; - smoothlyDisplayLogs(core_logs, cl, false, currentCoreContent, activeTabId === 1); + smoothlyDisplayLogs(core_logs, editor_core, true, currentCoreContent, true); + } else { + accumulateCoreLog(core_logs); } } } else { @@ -695,7 +749,11 @@ function poll_log(){ } if (isPolling) { - r = setTimeout("poll_log()", 1000); + r = setTimeout("poll_log()", pollInterval); + return; + } + + if (activeTabId === 2) { return; } @@ -710,7 +768,7 @@ function poll_log(){ if (status) { if (!status.update) { isPolling = false; - r = setTimeout("poll_log()", 1000); + r = setTimeout("poll_log()", pollInterval); return; } @@ -736,19 +794,18 @@ function poll_log(){ core_logs = line_tolocal(status.core_log); if (core_logs.length > 0) { - if (editor_core) { + if (editor_core && activeTabId === 1) { currentCoreContent = editor_core.getValue(); - smoothlyDisplayLogs(core_logs, editor_core, true, currentCoreContent, activeTabId === 1); - } else if (cl) { - currentCoreContent = cl.innerHTML; - smoothlyDisplayLogs(core_logs, cl, false, currentCoreContent, activeTabId === 1); + smoothlyDisplayLogs(core_logs, editor_core, true, currentCoreContent, true); + } else { + accumulateCoreLog(core_logs); } } } } } isPolling = false; - r = setTimeout("poll_log()", 1000); + r = setTimeout("poll_log()", pollInterval); } ); }; @@ -769,12 +826,34 @@ window.onload = function(){ function handleTabSwitch(tab) { return function(e) { if (tab.className === 'cbi-tab') return; + var prevTabId = -1; for(var j=0; j diff --git a/luci-app-openclash/luasrc/view/openclash/myip.htm b/luci-app-openclash/luasrc/view/openclash/myip.htm index aabef06f1..85aeb160e 100644 --- a/luci-app-openclash/luasrc/view/openclash/myip.htm +++ b/luci-app-openclash/luasrc/view/openclash/myip.htm @@ -3,21 +3,20 @@ +<% + local fs = require "luci.openclash" + local plugin_version = fs.oc_version() +%> + - - - - - - @@ -97,10 +96,9 @@ - + <%:Refresh%> - - +

diff --git a/luci-app-openclash/luasrc/view/openclash/oix_login.htm b/luci-app-openclash/luasrc/view/openclash/oix_login.htm index 37610ce94..e8117d875 100644 --- a/luci-app-openclash/luasrc/view/openclash/oix_login.htm +++ b/luci-app-openclash/luasrc/view/openclash/oix_login.htm @@ -1,112 +1,629 @@ -<%+cbi/valueheader%> +<%- +local fs = require "luci.openclash" +local logged_in = fs.uci_get_config("config", "oix_token") and true or false +local checkin_enabled = fs.uci_get_config("config", "oix_checkin") == "1" +local checkin_interval = fs.uci_get_config("config", "oix_checkin_interval") or "1" +local checkin_multiple = fs.uci_get_config("config", "oix_checkin_multiple") or "1" +local oix_params = fs.uci_get_config("config", "oix_params") or "" +local oix_default_params = fs.uci_get_config("config", "oix_default_params") or "" +local oix_logo_src = "/luci-static/resources/openclash/img/oixcloud.png" +local function oix_logo_img(cls) + return '
oixCloud' +end +-%> +
+ +
+
+ + +
+
+
+ + + -- + +
+
+ + + +
+
+ +
+
+ + <%:Announcement%> +
+
+ +
+ +
+
+
<%:Plan Expiration Time%>
+
--
+
+
+
<%:Account Integral%>
+
--
+
+
+
<%:Aff Balances%>
+
--
+
+
+
<%:Account Balances%>
+
--
+
+
+
<%:Plan Traffic%>
+
--
+
+
+
<%:Plan Unused%>
+
--
+
+
+
<%:Plan Used%>
+
--
+
+
+
<%:Today Used%>
+
--
+
+
+ +
+
+
+ <%:Show on Overview%> + +
+
+ + <%:Display account info on the main page%> +
+
+
+
+ <%:Auto Checkin%> + +
+
+ + <%:Auto check-in for traffic rewards and penalties%> +
+
+
+
+ <%:Checkin Interval%> (<%:Hours%>): + +
+
+ + <%:Checkin interval must be between 1 and 720%> +
+
+
+
+ <%:Checkin Multiple%>: + +
+
+ + <%:Multiple Must Be a Positive Integer and No More Than 100%> +
+
+
+
+
+
+ <%:Optional Parameters%>: + +
+
+ + <%:For more information, please visit the%> <%:official documentation%> +
+
+
+ +
+
+
+ - - - -<%=self.value%> -<%+cbi/valuefooter%> +}; + +var OixMsg = { + _timer: null, + show: function(msg, level, timeout) { + var bar = document.getElementById('oix-msg-bar'); + var icons = { + success: '', + error: '', + info: '' + }; + bar.style.display = 'flex'; bar.innerHTML = (icons[level] || icons.info) + '' + msg + ''; + bar.className = 'oix-msg-bar oix-msg-' + (level || 'info'); + clearTimeout(this._timer); + var t = (timeout !== undefined) ? timeout : (level === 'error' ? 6000 : 3000); + if (t > 0) { var s=this; this._timer=setTimeout(function(){s.hide();},t); } + }, + hide: function() { document.getElementById('oix-msg-bar').style.display = 'none'; }, + field: function(id, msg) { + var hint = document.getElementById(id + '-hint'), inp = document.getElementById(id); + if (hint) { hint.textContent = msg || ''; hint.className = msg ? 'oix-field-hint oix-field-error' : 'oix-field-hint oix-field-help'; } + if (inp) { + var isNum = id === 'oix-checkin-interval' || id === 'oix-checkin-multiple' || id === 'oix-params'; + var base = isNum ? 'oix-input-num' : 'oix-input'; + inp.className = msg ? (base + ' oix-input-err') : (base + ' oix-input-ok'); + } + }, + clearAll: function() { + this.hide(); + ['oix-email','oix-passwd','oix-token'].forEach(function(id) { + var hint = document.getElementById(id + '-hint'), inp = document.getElementById(id); + if (hint) { hint.textContent = ''; hint.className = 'oix-field-hint oix-field-help'; } + if (inp) { inp.className = 'oix-input oix-input-ok'; } + }); + ['oix-checkin-interval','oix-checkin-multiple'].forEach(function(id) { + var hint = document.getElementById(id + '-hint'), inp = document.getElementById(id); + if (hint) { hint.textContent = ''; hint.className = 'oix-field-hint oix-field-help'; } + if (inp) { inp.className = 'oix-input-num oix-input-ok'; } + }); + } +}; + +document.addEventListener('DOMContentLoaded', function() { OixCloud.init(); }); +//]]> \ No newline at end of file diff --git a/luci-app-openclash/luasrc/view/openclash/oixcloud.htm b/luci-app-openclash/luasrc/view/openclash/oixcloud.htm index 174deef18..bd3c67b11 100644 --- a/luci-app-openclash/luasrc/view/openclash/oixcloud.htm +++ b/luci-app-openclash/luasrc/view/openclash/oixcloud.htm @@ -3,6 +3,11 @@ +<% + local fs = require "luci.openclash" + local plugin_version = fs.oc_version() +%> + @@ -15,7 +20,7 @@
- +
@@ -30,9 +35,7 @@