Merge pull request #3318 from Ansuel/rework-ddns
[oweals/luci.git] / applications / luci-app-ddns / root / usr / libexec / rpcd / luci.ddns
1 #!/usr/bin/env lua
2
3 local json = require "luci.jsonc"
4 local nixio = require "nixio"
5 local fs   = require "nixio.fs"
6 local UCI = require "luci.model.uci"
7 local sys  = require "luci.sys"
8 local util = require "luci.util"
9
10 local luci_helper = "/usr/lib/ddns/dynamic_dns_lucihelper.sh"
11 local srv_name    = "ddns-scripts"
12
13 -- convert epoch date to given format
14 local function epoch2date(epoch, format)
15         if not format or #format < 2 then
16                 local uci = UCI.cursor()
17                 format    = uci:get("ddns", "global", "ddns_dateformat") or "%F %R"
18                 uci:unload("ddns")
19         end
20         format = format:gsub("%%n", "<br />")   -- replace newline
21         format = format:gsub("%%t", "    ")     -- replace tab
22         return os.date(format, epoch)
23 end
24
25 -- function to calculate seconds from given interval and unit
26 local function calc_seconds(interval, unit)
27         if not tonumber(interval) then
28                 return nil
29         elseif unit == "days" then
30                 return (tonumber(interval) * 86400)     -- 60 sec * 60 min * 24 h
31         elseif unit == "hours" then
32                 return (tonumber(interval) * 3600)      -- 60 sec * 60 min
33         elseif unit == "minutes" then
34                 return (tonumber(interval) * 60)        -- 60 sec
35         elseif unit == "seconds" then
36                 return tonumber(interval)
37         else
38                 return nil
39         end
40 end
41
42 local methods = {
43         get_services_log = {
44                 args = { service_name = "service_name" },
45                 call = function(args)
46                         local result = "File not found or empty"
47                         local uci = UCI.cursor()
48
49                         local dirlog = uci:get('ddns', 'global', 'ddns_logdir') or "/var/log/ddns"
50
51                         -- Fallback to default logdir with unsecure path
52                         if dirlog:match('%.%.%/') then dirlog = "/var/log/ddns" end
53
54                         if args and args.service_name and fs.access("%s/%s.log" % { dirlog, args.service_name }) then
55                                 result = fs.readfile("%s/%s.log" % { dirlog, args.service_name })
56                         end
57
58                         uci.unload()
59
60                         return { result = result }
61                 end
62         },
63         get_services_status = {
64                 call = function()
65                         local uci = UCI.cursor()
66
67                         local rundir = uci:get("ddns", "global", "ddns_rundir") or "/var/run/ddns"
68                         local date_format = uci:get("ddns", "global", "ddns_dateformat")
69                         local res = {}
70
71                         uci:foreach("ddns", "service", function (s)
72                                 local ip, last_update, next_update
73                                 local section   = s[".name"]
74                                 if fs.access("%s/%s.ip" % { rundir, section }) then
75                                         ip = fs.readfile("%s/%s.ip" % { rundir, section })
76                                 else
77                                         local dnsserver = s["dns_server"] or ""
78                                         local force_ipversion = tonumber(s["force_ipversion"] or 0)
79                                         local force_dnstcp = tonumber(s["force_dnstcp"] or 0)
80                                         local is_glue = tonumber(s["is_glue"] or 0)
81                                         local command = { luci_helper , [[ -]] }
82                                         local lookup_host = s["lookup_host"] or "_nolookup_"
83
84                                         if (use_ipv6 == 1) then command[#command+1] = [[6]] end
85                                         if (force_ipversion == 1) then command[#command+1] = [[f]] end
86                                         if (force_dnstcp == 1) then command[#command+1] = [[t]] end
87                                         if (is_glue == 1) then command[#command+1] = [[g]] end
88                                         command[#command+1] = [[l ]]
89                                         command[#command+1] = lookup_host
90                                         command[#command+1] = [[ -S ]]
91                                         command[#command+1] = section
92                                         if (#dnsserver > 0) then command[#command+1] = [[ -d ]] .. dnsserver end
93                                         command[#command+1] = [[ -- get_registered_ip]]
94                                         line = util.exec(table.concat(command))
95                                 end
96
97                                 local last_update = tonumber(fs.readfile("%s/%s.update" % { rundir, section } ) or 0)
98                                 local next_update, converted_last_update
99                                 local pid  = tonumber(fs.readfile("%s/%s.pid" % { rundir, section } ) or 0)
100
101                                 if pid > 0 and not nixio.kill(pid, 0) then
102                                         pid = 0
103                                 end
104
105                                 local uptime   = sys.uptime()
106
107                                 local force_seconds = calc_seconds(
108                                         tonumber(s["force_interval"]) or 72,
109                                         s["force_unit"] or "hours" )
110
111                                 -- process running but update needs to happen
112                                 -- problems if force_seconds > uptime
113                                 force_seconds = (force_seconds > uptime) and uptime or force_seconds
114
115                                 if last_update > 0 then
116                                         local epoch = os.time() - uptime + last_update + force_seconds
117                                         -- use linux date to convert epoch
118                                         converted_last_update = epoch2date(epoch,date_format)
119                                         next_update = epoch2date(epoch + force_seconds)
120                                 end
121
122                                 if pid > 0 and ( last_update + force_seconds - uptime ) <= 0 then
123                                         next_update = "Verify"
124
125                                 -- run once
126                                 elseif force_seconds == 0 then
127                                         next_update = "Run once"
128
129                                 -- no process running and NOT enabled
130                                 elseif pid == 0 and s['enabled'] == '0' then
131                                         next_update  = "Disabled"
132
133                                 -- no process running and enabled
134                                 elseif pid == 0 and s['enabled'] ~= '0' then
135                                         next_update = "Stopped"
136                                 end
137
138                                 res[section] = {
139                                         ip = ip and ip:gsub("\n","") or nil,
140                                         last_update = last_update ~= 0 and converted_last_update or nil,
141                                         next_update = next_update or nil,
142                                         pid = pid or nil,
143                                 } 
144                         end
145                         )
146
147                         uci:unload("ddns")
148
149                         return res
150
151                 end
152         },
153         get_ddns_state = {
154                 call = function()
155                         local ipkg = require "luci.model.ipkg"
156                         local uci = UCI.cursor()
157                         local dateformat = uci:get("ddns", "global", "ddns_dateformat") or "%F %R"
158                         uci:unload("ddns")
159                         local ver, srv_ver_cmd
160                         local res = {}
161
162                         if ipkg then
163                                 ver = ipkg.info(srv_name)[srv_name].Version
164                         else
165                                 srv_ver_cmd = luci_helper .. " -V | awk {'print $2'} "
166                                 ver = util.exec(srv_ver_cmd)
167                         end
168
169                         res['_version'] = ver and #ver > 0 and ver or nil
170                         res['_enabled'] = sys.init.enabled("ddns")
171                         res['_curr_dateformat'] = os.date(dateformat)
172
173                         return res
174                 end
175         },
176         get_env = {
177                 call = function()
178                         local res = {}
179                         local cache = {}
180
181                         local function has_wget()
182                                 return (sys.call( [[which wget >/dev/null 2>&1]] ) == 0)
183                         end
184
185                         local function has_wgetssl()
186                                 if cache['has_wgetssl'] then return cache['has_wgetssl'] end
187                                 local res = (sys.call( [[which wget-ssl >/dev/null 2>&1]] ) == 0)
188                                 cache['has_wgetssl'] = res
189                                 return res
190                         end
191
192                         local function has_curlssl()
193                                 return (sys.call( [[$(which curl) -V 2>&1 | grep -qF "https"]] ) == 0)
194                         end
195
196                         local function has_fetch()
197                                 if cache['has_fetch'] then return cache['has_fetch'] end
198                                 local res = (sys.call( [[which uclient-fetch >/dev/null 2>&1]] ) == 0)
199                                 cache['has_fetch'] = res
200                                 return res
201                         end
202
203                         local function has_fetchssl()
204                                 return fs.access("/lib/libustream-ssl.so")
205                         end
206
207                         local function has_curl()
208                                 if cache['has_curl'] then return cache['has_curl'] end
209                                 local res = (sys.call( [[which curl >/dev/null 2>&1]] ) == 0)
210                                 cache['has_curl'] = res
211                                 return res
212                         end
213
214                         local function has_curlpxy()
215                                 return (sys.call( [[grep -i "all_proxy" /usr/lib/libcurl.so* >/dev/null 2>&1]] ) == 0)
216                         end
217
218                         local function has_bbwget()
219                                 return (sys.call( [[$(which wget) -V 2>&1 | grep -iqF "busybox"]] ) == 0)
220                         end
221
222                         res['has_wget'] = has_wget() or false
223                         res['has_curl'] = has_curl() or false
224
225                         res['has_ssl'] = has_wgetssl() or has_curlssl() or (has_fetch() and has_fetchssl()) or false
226                         res['has_proxy'] = has_wgetssl() or has_curlpxy() or has_fetch() or has_bbwget or false
227                         res['has_forceip'] = has_wgetssl() or has_curl() or has_fetch() or false
228                         res['has_bindnet'] = has_curl() or has_wgetssl() or false
229
230                         local function has_bindhost()
231                                 if cache['has_bindhost'] then return cache['has_bindhost'] end
232                                 local res = (sys.call( [[which host >/dev/null 2>&1]] ) == 0)
233                                 if res then
234                                         cache['has_bindhost'] = res
235                                         return true
236                                 end
237                                 res = (sys.call( [[which khost >/dev/null 2>&1]] ) == 0)
238                                 if res then
239                                         cache['has_bindhost'] = res
240                                         return true
241                                 end
242                                 res = (sys.call( [[which drill >/dev/null 2>&1]] ) == 0)
243                                 if res then
244                                         cache['has_bindhost'] = res
245                                         return true
246                                 end
247                                 cache['has_bindhost'] = false
248                                 return false
249                         end
250
251                         res['has_bindhost'] = cache['has_bindhost'] or has_bindhost() or false
252
253                         local function has_hostip()
254                                 return (sys.call( [[which hostip >/dev/null 2>&1]] ) == 0)
255                         end
256
257                         local function has_nslookup()
258                                 return (sys.call( [[which nslookup >/dev/null 2>&1]] ) == 0)
259                         end
260
261                         res['has_dnsserver'] = cache['has_bindhost'] or has_nslookup() or has_hostip() or has_bindhost() or false
262
263                         local function check_certs()
264                                 local _, v = fs.glob("/etc/ssl/certs/*.crt")
265                                 if ( v == 0 ) then _, v = fs.glob("/etc/ssl/certs/*.pem") end
266                                 return (v > 0)
267                         end
268
269                         res['has_cacerts'] = check_certs() or false
270                         
271                         res['has_ipv6'] = (fs.access("/proc/net/ipv6_route") and fs.access("/usr/sbin/ip6tables"))
272
273                         return res
274                 end
275         }
276 }
277
278 local function parseInput()
279         local parse = json.new()
280         local done, err
281
282         while true do
283                 local chunk = io.read(4096)
284                 if not chunk then
285                         break
286                 elseif not done and not err then
287                         done, err = parse:parse(chunk)
288                 end
289         end
290
291         if not done then
292                 print(json.stringify({ error = err or "Incomplete input" }))
293                 os.exit(1)
294         end
295
296         return parse:get()
297 end
298
299 local function validateArgs(func, uargs)
300         local method = methods[func]
301         if not method then
302                 print(json.stringify({ error = "Method not found" }))
303                 os.exit(1)
304         end
305
306         if type(uargs) ~= "table" then
307                 print(json.stringify({ error = "Invalid arguments" }))
308                 os.exit(1)
309         end
310
311         uargs.ubus_rpc_session = nil
312
313         local k, v
314         local margs = method.args or {}
315         for k, v in pairs(uargs) do
316                 if margs[k] == nil or
317                    (v ~= nil and type(v) ~= type(margs[k]))
318                 then
319                         print(json.stringify({ error = "Invalid arguments" }))
320                         os.exit(1)
321                 end
322         end
323
324         return method
325 end
326
327 if arg[1] == "list" then
328         local _, method, rv = nil, nil, {}
329         for _, method in pairs(methods) do rv[_] = method.args or {} end
330         print((json.stringify(rv):gsub(":%[%]", ":{}")))
331 elseif arg[1] == "call" then
332         local args = parseInput()
333         local method = validateArgs(arg[2], args)
334         local result, code = method.call(args)
335         print((json.stringify(result):gsub("^%[%]$", "{}")))
336         os.exit(code or 0)
337 end