From 22fcc720ec5d2ad1426509136447172281d345a0 Mon Sep 17 00:00:00 2001 From: Paul Donald Date: Thu, 21 Nov 2024 18:34:45 +0100 Subject: [PATCH] luci-app-ddns: Convert to JS Signed-off-by: Paul Donald --- .../root/usr/libexec/rpcd/luci.ddns | 346 ------------------ .../root/usr/share/rpcd/ucode/ddns.uc | 330 +++++++++++++++++ 2 files changed, 330 insertions(+), 346 deletions(-) delete mode 100755 applications/luci-app-ddns/root/usr/libexec/rpcd/luci.ddns create mode 100644 applications/luci-app-ddns/root/usr/share/rpcd/ucode/ddns.uc diff --git a/applications/luci-app-ddns/root/usr/libexec/rpcd/luci.ddns b/applications/luci-app-ddns/root/usr/libexec/rpcd/luci.ddns deleted file mode 100755 index b2e60cb3c2..0000000000 --- a/applications/luci-app-ddns/root/usr/libexec/rpcd/luci.ddns +++ /dev/null @@ -1,346 +0,0 @@ -#!/usr/bin/env lua - -local json = require "luci.jsonc" -local nixio = require "nixio" -local fs = require "nixio.fs" -local UCI = require "luci.model.uci" -local sys = require "luci.sys" -local util = require "luci.util" - -local ddns_package_path = "/usr/share/ddns" -local luci_helper = "/usr/lib/ddns/dynamic_dns_lucihelper.sh" -local srv_name = "ddns-scripts" - --- convert epoch date to given format -local function epoch2date(epoch, format) - if not format or #format < 2 then - local uci = UCI.cursor() - format = uci:get("ddns", "global", "ddns_dateformat") or "%F %R" - uci:unload("ddns") - end - format = format:gsub("%%n", "
") -- replace newline - format = format:gsub("%%t", " ") -- replace tab - return os.date(format, epoch) -end - --- function to calculate seconds from given interval and unit -local function calc_seconds(interval, unit) - if not tonumber(interval) then - return nil - elseif unit == "days" then - return (tonumber(interval) * 86400) -- 60 sec * 60 min * 24 h - elseif unit == "hours" then - return (tonumber(interval) * 3600) -- 60 sec * 60 min - elseif unit == "minutes" then - return (tonumber(interval) * 60) -- 60 sec - elseif unit == "seconds" then - return tonumber(interval) - else - return nil - end -end - -local methods = { - get_services_log = { - args = { service_name = "service_name" }, - call = function(args) - local result = "File not found or empty" - local uci = UCI.cursor() - - local dirlog = uci:get('ddns', 'global', 'ddns_logdir') or "/var/log/ddns" - - -- Fallback to default logdir with unsecure path - if dirlog:match('%.%.%/') then dirlog = "/var/log/ddns" end - - if args and args.service_name and fs.access("%s/%s.log" % { dirlog, args.service_name }) then - result = fs.readfile("%s/%s.log" % { dirlog, args.service_name }) - end - - uci.unload() - - return { result = result } - end - }, - get_services_status = { - call = function() - local uci = UCI.cursor() - - local rundir = uci:get("ddns", "global", "ddns_rundir") or "/var/run/ddns" - local date_format = uci:get("ddns", "global", "ddns_dateformat") - local res = {} - - uci:foreach("ddns", "service", function (s) - local ip, last_update, next_update - local section = s[".name"] - if fs.access("%s/%s.ip" % { rundir, section }) then - ip = fs.readfile("%s/%s.ip" % { rundir, section }) - else - local dnsserver = s["dns_server"] or "" - local force_ipversion = tonumber(s["force_ipversion"] or 0) - local force_dnstcp = tonumber(s["force_dnstcp"] or 0) - local is_glue = tonumber(s["is_glue"] or 0) - local command = { luci_helper , [[ -]] } - local lookup_host = s["lookup_host"] or "_nolookup_" - - if (use_ipv6 == 1) then command[#command+1] = [[6]] end - if (force_ipversion == 1) then command[#command+1] = [[f]] end - if (force_dnstcp == 1) then command[#command+1] = [[t]] end - if (is_glue == 1) then command[#command+1] = [[g]] end - command[#command+1] = [[l ]] - command[#command+1] = lookup_host - command[#command+1] = [[ -S ]] - command[#command+1] = section - if (#dnsserver > 0) then command[#command+1] = [[ -d ]] .. dnsserver end - command[#command+1] = [[ -- get_registered_ip]] - line = util.exec(table.concat(command)) - end - - local last_update = tonumber(fs.readfile("%s/%s.update" % { rundir, section } ) or 0) - local next_update, converted_last_update - local pid = tonumber(fs.readfile("%s/%s.pid" % { rundir, section } ) or 0) - - if pid > 0 and not nixio.kill(pid, 0) then - pid = 0 - end - - local uptime = sys.uptime() - - local force_seconds = calc_seconds( - tonumber(s["force_interval"]) or 72, - s["force_unit"] or "hours" ) - - local check_seconds = calc_seconds( - tonumber(s["check_interval"]) or 10, - s["check_unit"] or "minutes" ) - - if last_update > 0 then - local epoch = os.time() - uptime + last_update - -- use linux date to convert epoch - converted_last_update = epoch2date(epoch,date_format) - next_update = epoch2date(epoch + force_seconds + check_seconds) - end - - if pid > 0 and ( last_update + force_seconds + check_seconds - uptime ) <= 0 then - next_update = "Verify" - - -- run once - elseif force_seconds == 0 then - next_update = "Run once" - - -- no process running and NOT enabled - elseif pid == 0 and s['enabled'] == '0' then - next_update = "Disabled" - - -- no process running and enabled - elseif pid == 0 and s['enabled'] ~= '0' then - next_update = "Stopped" - end - - res[section] = { - ip = ip and ip:gsub("\n","") or nil, - last_update = last_update ~= 0 and converted_last_update or nil, - next_update = next_update or nil, - pid = pid or nil, - } - end - ) - - uci:unload("ddns") - - return res - - end - }, - get_ddns_state = { - call = function() - local uci = UCI.cursor() - local dateformat = uci:get("ddns", "global", "ddns_dateformat") or "%F %R" - local services_mtime = fs.stat(ddns_package_path .. "/list", 'mtime') - uci:unload("ddns") - local res = {} - local ver - - local ok, ctrl = pcall(io.lines, "/usr/lib/opkg/info/%s.control" % srv_name) - if ok then - for line in ctrl do - ver = line:match("^Version: (.+)$") - - if ver then - break - end - end - end - - ver = ver or util.trim(util.exec("%s -V | awk {'print $2'}" % luci_helper)) - - res['_version'] = ver and #ver > 0 and ver or nil - res['_enabled'] = sys.init.enabled("ddns") - res['_curr_dateformat'] = os.date(dateformat) - res['_services_list'] = services_mtime and os.date(dateformat, services_mtime) or 'NO_LIST' - - return res - end - }, - get_env = { - call = function() - local res = {} - local cache = {} - - local function has_wget() - return (sys.call( [[command -v wget >/dev/null 2>&1]] ) == 0) - end - - local function has_wgetssl() - if cache['has_wgetssl'] then return cache['has_wgetssl'] end - local res = has_wget() and (sys.call( [[wget --version | grep -qF +https >/dev/null 2>&1]] ) == 0) - cache['has_wgetssl'] = res - return res - end - - local function has_curlssl() - return (sys.call( [[$(command -v curl) -V 2>&1 | grep -qF "https"]] ) == 0) - end - - local function has_fetch() - if cache['has_fetch'] then return cache['has_fetch'] end - local res = (sys.call( [[command -v uclient-fetch >/dev/null 2>&1]] ) == 0) - cache['has_fetch'] = res - return res - end - - local function has_fetchssl() - return fs.access("/lib/libustream-ssl.so") - end - - local function has_curl() - if cache['has_curl'] then return cache['has_curl'] end - local res = (sys.call( [[command -v curl >/dev/null 2>&1]] ) == 0) - cache['has_curl'] = res - return res - end - - local function has_curlpxy() - return (sys.call( [[grep -i "all_proxy" /usr/lib/libcurl.so* >/dev/null 2>&1]] ) == 0) - end - - local function has_bbwget() - return (sys.call( [[$(command -v wget) -V 2>&1 | grep -iqF "busybox"]] ) == 0) - end - - res['has_wget'] = has_wget() or false - res['has_curl'] = has_curl() or false - - res['has_ssl'] = has_wgetssl() or has_curlssl() or (has_fetch() and has_fetchssl()) or false - res['has_proxy'] = has_wgetssl() or has_curlpxy() or has_fetch() or has_bbwget or false - res['has_forceip'] = has_wgetssl() or has_curl() or has_fetch() or false - res['has_bindnet'] = has_curl() or has_wgetssl() or false - - local function has_bindhost() - if cache['has_bindhost'] then return cache['has_bindhost'] end - local res = (sys.call( [[command -v host >/dev/null 2>&1]] ) == 0) - if res then - cache['has_bindhost'] = res - return true - end - res = (sys.call( [[command -v khost >/dev/null 2>&1]] ) == 0) - if res then - cache['has_bindhost'] = res - return true - end - res = (sys.call( [[command -v drill >/dev/null 2>&1]] ) == 0) - if res then - cache['has_bindhost'] = res - return true - end - cache['has_bindhost'] = false - return false - end - - res['has_bindhost'] = cache['has_bindhost'] or has_bindhost() or false - - local function has_hostip() - return (sys.call( [[command -v hostip >/dev/null 2>&1]] ) == 0) - end - - local function has_nslookup() - return (sys.call( [[command -v nslookup >/dev/null 2>&1]] ) == 0) - end - - res['has_dnsserver'] = cache['has_bindhost'] or has_nslookup() or has_hostip() or has_bindhost() or false - - local function check_certs() - local _, v = fs.glob("/etc/ssl/certs/*.crt") - if ( v == 0 ) then _, v = fs.glob("/etc/ssl/certs/*.pem") end - return (v > 0) - end - - res['has_cacerts'] = check_certs() or false - - res['has_ipv6'] = (fs.access("/proc/net/ipv6_route") and - (fs.access("/usr/sbin/ip6tables") or fs.access("/usr/sbin/nft"))) - - return res - end - } -} - -local function parseInput() - local parse = json.new() - local done, err - - while true do - local chunk = io.read(4096) - if not chunk then - break - elseif not done and not err then - done, err = parse:parse(chunk) - end - end - - if not done then - print(json.stringify({ error = err or "Incomplete input" })) - os.exit(1) - end - - return parse:get() -end - -local function validateArgs(func, uargs) - local method = methods[func] - if not method then - print(json.stringify({ error = "Method not found" })) - os.exit(1) - end - - if type(uargs) ~= "table" then - print(json.stringify({ error = "Invalid arguments" })) - os.exit(1) - end - - uargs.ubus_rpc_session = nil - - local k, v - local margs = method.args or {} - for k, v in pairs(uargs) do - if margs[k] == nil or - (v ~= nil and type(v) ~= type(margs[k])) - then - print(json.stringify({ error = "Invalid arguments" })) - os.exit(1) - end - end - - return method -end - -if arg[1] == "list" then - local _, method, rv = nil, nil, {} - for _, method in pairs(methods) do rv[_] = method.args or {} end - print((json.stringify(rv):gsub(":%[%]", ":{}"))) -elseif arg[1] == "call" then - local args = parseInput() - local method = validateArgs(arg[2], args) - local result, code = method.call(args) - print((json.stringify(result):gsub("^%[%]$", "{}"))) - os.exit(code or 0) -end diff --git a/applications/luci-app-ddns/root/usr/share/rpcd/ucode/ddns.uc b/applications/luci-app-ddns/root/usr/share/rpcd/ucode/ddns.uc new file mode 100644 index 0000000000..3a8e5cf50a --- /dev/null +++ b/applications/luci-app-ddns/root/usr/share/rpcd/ucode/ddns.uc @@ -0,0 +1,330 @@ +#!/usr/bin/env ucode + +'use strict'; + +import { readfile, mkstemp, open, popen, stat, glob } from 'fs'; +import { init_list, init_index, init_enabled, init_action, conntrack_list, process_list } from 'luci.sys'; +import { isnan } from 'math'; +import { cursor } from 'uci'; + +const uci = cursor(); +const ddns_log_path = '/var/log/ddns'; +const ddns_package_path = '/usr/share/ddns'; +const ddns_run_path = '/var/run/ddns'; +const luci_helper = '/usr/lib/ddns/dynamic_dns_lucihelper.sh'; +const srv_name = 'ddns-scripts'; +const opkg_info_path = '/usr/lib/opkg/info'; + + + + +function get_dateformat() { + return uci.get('ddns', 'global', 'ddns_dateformat') || '%F %R'; +} + +function uptime() { + return split(readfile('/proc/uptime', 256), ' ')?.[0]; +} + +function killcmd(procid, signal) { + if (!signal) { + signal = 0; + } + // by default, we simply re-nice a process to check it is running + return system(`kill -${signal} ${procid}`); +} + +function trimnonewline(input) { + return replace(trim(input), /\n/g, ''); +} + +function get_date(seconds, format) { + return trimnonewline( popen(`date -d @${seconds} "+${format}" 2>/dev/null`, 'r')?.read?.('line') ); +} + +// convert epoch date to given format +function epoch2date(epoch, format) { + if (!format || length(format) < 2) { + format = get_dateformat(); + // uci.unload('ddns'); //don't do this in uci.foreach loops + } + format = replace(format, /%n/g, '
'); // Replace '%n' with '
' + format = replace(format, /%t/g, ' '); // Replace '%t' with four spaces + + return get_date(epoch, format); +} + +// function to calculate seconds from given interval and unit +function calc_seconds(interval, unit) { + let parsedInterval = int(interval); + if (isnan(parsedInterval)) { + return null; + } + + switch (unit) { + case 'days': + return parsedInterval * 86400; // 60 sec * 60 min * 24 h + case 'hours': + return parsedInterval * 3600; // 60 sec * 60 min + case 'minutes': + return parsedInterval * 60; // 60 sec + case 'seconds': + return parsedInterval; + default: + return null; + } +} + +const methods = { + get_services_log: { + args: { service_name: 'service_name' }, + call: function(request) { + let result = 'File not found or empty'; + + // Get the log directory. Fall back to '/var/log/ddns' if not found + let logdir = uci.get('ddns', 'global', 'ddns_logdir') || ddns_log_path; + + // Fall back to default logdir with insecure path + if (match(logdir, /\.\.\//)) { + logdir = ddns_log_path; + } + + // Check if service_name is provided and log file exists + if (request.args && request.args.service_name && stat(`${logdir}/${request.args.service_name}.log`)?.type == 'file' ) { + result = readfile(`${logdir}/${request.args.service_name}.log`); + } + + uci.unload(); + return { result: result }; + } + }, + + get_services_status: { + call: function() { + const rundir = uci.get('ddns', 'global', 'ddns_rundir') || ddns_run_path; + // const dateFormat = get_dateformat(); + let res = {}; + + uci.foreach('ddns', 'service', function(s) { + /* uci.foreach danger zone: if you inadvertently call uci.unload('ddns') + anywhere in this foreach loop, you will produce some spectacular undefined behaviour */ + let ip, lastUpdate, nextUpdate; + const section = s['.name']; + if (section == '.anonymous') + return; + + if (stat(`${rundir}/${section}.ip`)?.type == 'file') { + ip = readfile(`${rundir}/${section}.ip`); + } else { + const dnsServer = s['dns_server'] || ''; + const forceIpVersion = int(s['force_ipversion'] || 0); + const forceDnsTcp = int(s['force_dnstcp'] || 0); + const isGlue = int(s['is_glue'] || 0); + const useIpv6 = int(s['use_ipv6'] || 0); + const lookupHost = s['lookup_host'] || '_nolookup_'; + let command = [luci_helper]; + + if (useIpv6 == 1) push(command, '-6'); + if (forceIpVersion == 1) push(command, '-f'); + if (forceDnsTcp == 1) push(command, '-t'); + if (isGlue == 1) push(command, '-g'); + + push(command, '-l', lookupHost); + push(command, '-S', section); + if (length(dnsServer) > 0) push(command, '-d', dnsServer); + push(command, '-- get_registered_ip'); + + const result = system(`${join(' ', command)}`); + } + + lastUpdate = int(readfile(`${rundir}/${section}.update`) || 0); + + let pid = int(readfile(`${rundir}/${section}.pid`) || 0); + + // if killcmd succeeds (0) to re-nice the process, we do not assume the pid is dead + if (pid > 0 && killcmd(pid)) { + pid = 0; + } + + let _uptime = int(uptime()); + + const forceSeconds = calc_seconds( + int(s['force_interval']) || 72, + s['force_unit'] || 'hours' + ); + + const checkSeconds = calc_seconds( + int(s['check_interval']) || 10, + s['check_unit'] || 'minutes' + ); + + let convertedLastUpdate; + if (lastUpdate > 0) { + const epoch = time() - _uptime + lastUpdate; + convertedLastUpdate = epoch2date(epoch); + // convertedLastUpdate = get_date(epoch, dateFormat); + nextUpdate = epoch2date(epoch + forceSeconds + checkSeconds); + // nextUpdate = get_date(epoch + forceSeconds + checkSeconds, dateFormat); + } + + if (pid > 0 && (lastUpdate + forceSeconds + checkSeconds - _uptime) <= 0) { + nextUpdate = 'Verify'; + } else if (forceSeconds === 0) { + nextUpdate = 'Run once'; + } else if (pid == 0 && s['enabled'] == '0') { + nextUpdate = 'Disabled'; + } else if (pid == 0 && s['enabled'] != '0') { + nextUpdate = 'Stopped'; + } + + res[section] = { + ip: ip ? replace(ip, '\n', '') : null, + last_update: lastUpdate !== 0 ? convertedLastUpdate : null, + next_update: nextUpdate || null, + pid: pid || null, + }; + }); + + uci.unload('ddns'); + return res; + } + }, + + get_ddns_state: { + call: function() { + // const dateFormat = get_dateformat(); + + const services_mtime = stat(ddns_package_path + '/list')?.mtime; + // uci.unload('ddns'); + let res = {}; + let ver, control; + + if (stat(opkg_info_path + `/${srv_name}.control`)?.type == 'file') { + control = readfile(opkg_info_path + `/${srv_name}.control`); + } + + for (let line in split(control, '\n')) { + ver = match(line, /^Version: (.+)$/)?.[1]; + if ( ver && length(ver) > 0 ) + break; + } + + ver = ver || trimnonewline(popen(`${luci_helper} -V | awk {'print $2'}`, 'r')?.read?.('line')); + + res['_version'] = ver; + res['_enabled'] = init_enabled('ddns'); + // res['_curr_dateformat'] = get_date(time(), dateFormat); + res['_curr_dateformat'] = epoch2date(time()); + res['_services_list'] = (services_mtime && epoch2date(services_mtime)) || 'NO_LIST'; + + uci.unload('ddns'); + return res; + } + }, + + get_env: { + call: function () { + let res = {}; + let cache = {}; + + const hasCommand = (command) => { + if (system(`command -v ${command}`) == 0) + return true; + else + return false; + }; + + const hasWget = () => hasCommand('wget'); + + const hasWgetSsl = () => { + if (cache['has_wgetssl']) return cache['has_wgetssl']; + const result = hasWget() && system(`wget 2>&1 | grep -iqF 'https'`) == 0 ? true: false; + cache['has_wgetssl'] = result; + return result; + }; + + const hasCurl = () => { + if (cache['has_curl']) return cache['has_curl']; + const result = hasCommand('curl'); + cache['has_curl'] = result; + return result; + }; + + const hasCurlSsl = () => { + return system(`curl -V 2>&1 | grep -qF 'https'`) == 0 ? true: false; + }; + + const hasFetch = () => { + if (cache['has_fetch']) return cache['has_fetch']; + const result = hasCommand('uclient-fetch'); + cache['has_fetch'] = result; + return result; + }; + + const hasFetchSsl = () => { + return stat('/lib/libustream-ssl.so') == 0 ? true: false; + }; + + const hasCurlPxy = () => { + return system(`grep -i 'all_proxy' /usr/lib/libcurl.so*`) == 0 ? true: false; + }; + + const hasBbwget = () => { + return system(`wget -V 2>&1 | grep -iqF 'busybox'`) == 0 ? true: false; + }; + + + res['has_wget'] = hasWget(); + res['has_curl'] = hasCurl(); + + res['has_ssl'] = hasWgetSsl() || hasCurlSsl() || (hasFetch() && hasFetchSsl()); + res['has_proxy'] = hasWgetSsl() || hasCurlPxy() || hasFetch() || hasBbwget(); + res['has_forceip'] = hasWgetSsl() || hasCurl() || hasFetch(); + res['has_bindnet'] = hasCurl() || hasWgetSsl(); + + const hasBindHost = () => { + if (cache['has_bindhost']) return cache['has_bindhost']; + const commands = ['host', 'khost', 'drill']; + for (let command in commands) { + if (hasCommand(command)) { + cache['has_bindhost'] = true; + return true; + } + } + + cache['has_bindhost'] = false; + return false; + }; + + res['has_bindhost'] = cache['has_bindhost'] || hasBindHost(); + + const hasHostIp = () => { + return hasCommand('hostip'); + }; + + const hasNslookup = () => { + return hasCommand('nslookup'); + }; + + res['has_dnsserver'] = cache['has_bindhost'] || hasNslookup() || hasHostIp() || hasBindHost(); + + const checkCerts = () => { + let present = false; + for (let cert in glob('/etc/ssl/certs/*.crt', '/etc/ssl/certs/*.pem')) { + if (cert != null) + present = true; + } + return present; + }; + + res['has_cacerts'] = checkCerts(); + + res['has_ipv6'] = (stat('/proc/net/ipv6_route')?.type == 'file' && + (stat('/usr/sbin/ip6tables')?.type == 'file' || stat('/usr/sbin/nft')?.type == 'file')); + + return res; + } + } +}; + +return { 'luci.ddns': methods }; -- 2.30.2