This commit rework the app with the new ddns changes. DDns services are now stored in a dedicated list and the service specific data is stored in a dedicated json. This json can both preinstalled with a companion package or be downloaded on demand. The new app now check if the script is present and give a button to install it if not present in the system. The app now will search for all the available service in the services directory and optionally if present will include in the list the service not installed from a static list. Special service that use a separate script (for example cloudflare-v4) will install directly in the services directory and will be included automatically. The app now reset the ddns rule settings on service change. Also rework the app to drop any global function and rework the function to use more default way to get data. Signed-off-by: Ansuel Smith <ansuelsmth@gmail.com>
340 lines
9.7 KiB
Lua
Executable file
340 lines
9.7 KiB
Lua
Executable file
#!/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" )
|
|
|
|
-- process running but update needs to happen
|
|
-- problems if force_seconds > uptime
|
|
force_seconds = (force_seconds > uptime) and uptime or force_seconds
|
|
|
|
if last_update > 0 then
|
|
local epoch = os.time() - uptime + last_update + force_seconds
|
|
-- use linux date to convert epoch
|
|
converted_last_update = epoch2date(epoch,date_format)
|
|
next_update = epoch2date(epoch + force_seconds)
|
|
end
|
|
|
|
if pid > 0 and ( last_update + force_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 ipkg = require "luci.model.ipkg"
|
|
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 ver, srv_ver_cmd
|
|
local res = {}
|
|
|
|
if ipkg then
|
|
ver = ipkg.info(srv_name)[srv_name].Version
|
|
else
|
|
srv_ver_cmd = luci_helper .. " -V | awk {'print $2'} "
|
|
ver = util.exec(srv_ver_cmd)
|
|
end
|
|
|
|
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 = (sys.call( [[command -v wget-ssl >/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"))
|
|
|
|
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
|