luci-app-ddns: Convert to JS
authorPaul Donald <newtwen+github@gmail.com>
Thu, 21 Nov 2024 17:34:45 +0000 (18:34 +0100)
committerPaul Donald <newtwen+github@gmail.com>
Thu, 21 Nov 2024 17:34:45 +0000 (18:34 +0100)
Signed-off-by: Paul Donald <newtwen+github@gmail.com>
applications/luci-app-ddns/root/usr/libexec/rpcd/luci.ddns [deleted file]
applications/luci-app-ddns/root/usr/share/rpcd/ucode/ddns.uc [new file with mode: 0644]

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 (executable)
index b2e60cb..0000000
+++ /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", "<br />")   -- 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 (file)
index 0000000..3a8e5cf
--- /dev/null
@@ -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, '<br />'); // Replace '%n' with '<br />'
+       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 };