luci-app-ddns: improve performance
[project/luci.git] / applications / luci-app-ddns / luasrc / tools / ddns.lua
1 -- Copyright 2014-2018 Christian Schoenebeck <christian dot schoenebeck at gmail dot com>
2 -- Licensed to the public under the Apache License 2.0.
3
4 module("luci.tools.ddns", package.seeall)
5
6 local NX = require "nixio"
7 local NXFS = require "nixio.fs"
8 local UCI = require "luci.model.uci"
9 local SYS = require "luci.sys"
10
11 function env_info(type)
12
13 if ( type == "has_ssl" ) or ( type == "has_proxy" ) or ( type == "has_forceip" )
14 or ( type == "has_bindnet" ) or ( type == "has_fetch" )
15 or ( type == "has_wgetssl" ) or ( type == "has_curl" )
16 or ( type == "has_curlssl" ) or ( type == "has_curlpxy" )
17 or ( type == "has_fetchssl" ) or ( type == "has_bbwget" ) then
18
19 local function has_wgetssl()
20 return (SYS.call( [[which wget-ssl >/dev/null 2>&1]] ) == 0) -- and true or nil
21 end
22
23 local function has_curlssl()
24 return (SYS.call( [[$(which curl) -V 2>&1 | grep "Protocols:" | grep -qF "https"]] ) ~= 0)
25 end
26
27 local function has_fetch()
28 return (SYS.call( [[which uclient-fetch >/dev/null 2>&1]] ) == 0)
29 end
30
31 local function has_fetchssl()
32 return NXFS.access("/lib/libustream-ssl.so")
33 end
34
35 local function has_curl()
36 return (SYS.call( [[which curl >/dev/null 2>&1]] ) == 0)
37 end
38
39 local function has_curlpxy()
40 return (SYS.call( [[grep -i "all_proxy" /usr/lib/libcurl.so* >/dev/null 2>&1]] ) == 0)
41 end
42
43 local function has_bbwget()
44 return (SYS.call( [[$(which wget) -V 2>&1 | grep -iqF "busybox"]] ) == 0)
45 end
46
47 if type == "has_wgetssl" then
48 return has_wgetssl()
49
50 elseif type == "has_curl" then
51 return has_curl()
52
53 elseif type == "has_curlssl" then
54 return has_curlssl()
55
56 elseif type == "has_curlpxy" then
57 return has_curlpxy()
58
59 elseif type == "has_fetch" then
60 return has_fetch()
61
62 elseif type == "has_fetchssl" then
63 return has_fetchssl()
64
65 elseif type == "has_bbwget" then
66 return has_bbwget()
67
68 elseif type == "has_ssl" then
69 if has_wgetssl() then return true end
70 if has_curlssl() then return true end
71 if (has_fetch() and has_fetchssl()) then return true end
72 return false
73
74 elseif type == "has_proxy" then
75 if has_wgetssl() then return true end
76 if has_curlpxy() then return true end
77 if has_fetch() then return true end
78 if has_bbwget() then return true end
79 return false
80
81 elseif type == "has_forceip" then
82 if has_wgetssl() then return true end
83 if has_curl() then return true end
84 if has_fetch() then return true end -- only really needed for transfer
85 return false
86
87 elseif type == "has_bindnet" then
88 if has_curl() then return true end
89 if has_wgetssl() then return true end
90 return false
91 end
92
93 elseif ( type == "has_dnsserver" ) or ( type == "has_bindhost" ) or ( type == "has_hostip" ) or ( type == "has_nslookup" ) then
94 local function has_bindhost()
95 if (SYS.call( [[which host >/dev/null 2>&1]] ) == 0) then return true end
96 if (SYS.call( [[which host >/dev/null 2>&1]] ) == 0) then return true end
97 if (SYS.call( [[which khost >/dev/null 2>&1]] ) == 0) then return true end
98 if (SYS.call( [[which drill >/dev/null 2>&1]] ) == 0) then return true end
99 return false
100 end
101
102 local function has_hostip()
103 return (SYS.call( [[which hostip >/dev/null 2>&1]] ) == 0)
104 end
105
106 local function has_nslookup()
107 return (SYS.call( [[$(which nslookup) localhost 2>&1 | grep -qF "(null)"]] ) ~= 0)
108 end
109
110 if type == "has_bindhost" then
111 return has_bindhost()
112 elseif type == "has_hostip" then
113 return has_hostip()
114 elseif type == "has_nslookup" then
115 return has_nslookup()
116 elseif tyep == "has_dnsserver" then
117 if has_bindhost() then return true end
118 if has_hostip() then return true end
119 if has_nslookup() then return true end
120 return false
121 end
122
123 elseif type == "has_ipv6" then
124 return (NXFS.access("/proc/net/ipv6_route") and NXFS.access("/usr/sbin/ip6tables"))
125
126 elseif type == "has_cacerts" then
127 --old _check_certs() local function
128 local _, v = NXFS.glob("/etc/ssl/certs/*.crt")
129 if ( v == 0 ) then _, v = NXFS.glob("/etc/ssl/certs/*.pem") end
130 return (v > 0)
131 else
132 return
133 end
134
135 end
136
137 -- function to calculate seconds from given interval and unit
138 function calc_seconds(interval, unit)
139 if not tonumber(interval) then
140 return nil
141 elseif unit == "days" then
142 return (tonumber(interval) * 86400) -- 60 sec * 60 min * 24 h
143 elseif unit == "hours" then
144 return (tonumber(interval) * 3600) -- 60 sec * 60 min
145 elseif unit == "minutes" then
146 return (tonumber(interval) * 60) -- 60 sec
147 elseif unit == "seconds" then
148 return tonumber(interval)
149 else
150 return nil
151 end
152 end
153
154 -- convert epoch date to given format
155 function epoch2date(epoch, format)
156 if not format or #format < 2 then
157 local uci = UCI.cursor()
158 format = uci:get("ddns", "global", "ddns_dateformat") or "%F %R"
159 uci:unload("ddns")
160 end
161 format = format:gsub("%%n", "<br />") -- replace newline
162 format = format:gsub("%%t", " ") -- replace tab
163 return os.date(format, epoch)
164 end
165
166 -- read lastupdate from [section].update file
167 function get_lastupd(section)
168 local uci = UCI.cursor()
169 local rdir = uci:get("ddns", "global", "ddns_rundir") or "/var/run/ddns"
170 local etime = tonumber(NXFS.readfile("%s/%s.update" % { rdir, section } ) or 0 )
171 uci:unload("ddns")
172 return etime
173 end
174
175 -- read registered IP from [section].ip file
176 function get_regip(section, chk_sec)
177 local uci = UCI.cursor()
178 local rdir = uci:get("ddns", "global", "ddns_rundir") or "/var/run/ddns"
179 local ip = "NOFILE"
180 if NXFS.access("%s/%s.ip" % { rdir, section }) then
181 local ftime = NXFS.stat("%s/%s.ip" % { rdir, section }, "ctime") or 0
182 local otime = os.time()
183 -- give ddns-scripts time (9 sec) to update file
184 if otime < (ftime + chk_sec + 9) then
185 ip = NXFS.readfile("%s/%s.ip" % { rdir, section })
186 end
187 end
188 uci:unload("ddns")
189 return ip
190 end
191
192 -- read PID from run file and verify if still running
193 function get_pid(section)
194 local uci = UCI.cursor()
195 local rdir = uci:get("ddns", "global", "ddns_rundir") or "/var/run/ddns"
196 local pid = tonumber(NXFS.readfile("%s/%s.pid" % { rdir, section } ) or 0 )
197 if pid > 0 and not NX.kill(pid, 0) then
198 pid = 0
199 end
200 uci:unload("ddns")
201 return pid
202 end
203
204 -- replacement of build-in read of UCI option
205 -- modified AbstractValue.cfgvalue(self, section) from cbi.lua
206 -- needed to read from other option then current value definition
207 function read_value(self, section, option)
208 local value
209 if self.tag_error[section] then
210 value = self:formvalue(section)
211 else
212 value = self.map:get(section, option)
213 end
214
215 if not value then
216 return nil
217 elseif not self.cast or self.cast == type(value) then
218 return value
219 elseif self.cast == "string" then
220 if type(value) == "table" then
221 return value[1]
222 end
223 elseif self.cast == "table" then
224 return { value }
225 end
226 end
227
228 -- replacement of build-in parse of "Value"
229 -- modified AbstractValue.parse(self, section, novld) from cbi.lua
230 -- validate is called if rmempty/optional true or false
231 -- before write check if forcewrite, value eq default, and more
232 function value_parse(self, section, novld)
233 local fvalue = self:formvalue(section)
234 local fexist = ( fvalue and (#fvalue > 0) ) -- not "nil" and "not empty"
235 local cvalue = self:cfgvalue(section)
236 local rm_opt = ( self.rmempty or self.optional )
237 local eq_cfg -- flag: equal cfgvalue
238
239 -- If favlue and cvalue are both tables and have the same content
240 -- make them identical
241 if type(fvalue) == "table" and type(cvalue) == "table" then
242 eq_cfg = (#fvalue == #cvalue)
243 if eq_cfg then
244 for i=1, #fvalue do
245 if cvalue[i] ~= fvalue[i] then
246 eq_cfg = false
247 end
248 end
249 end
250 if eq_cfg then
251 fvalue = cvalue
252 end
253 end
254
255 -- removed parameter "section" from function call because used/accepted nowhere
256 -- also removed call to function "transfer"
257 local vvalue, errtxt = self:validate(fvalue)
258
259 -- error handling; validate return "nil"
260 if not vvalue then
261 if novld then -- and "novld" set
262 return -- then exit without raising an error
263 end
264
265 if fexist then -- and there is a formvalue
266 self:add_error(section, "invalid", errtxt or self.title .. ": invalid")
267 return -- so data are invalid
268 elseif not rm_opt then -- and empty formvalue but NOT (rmempty or optional) set
269 self:add_error(section, "missing", errtxt or self.title .. ": missing")
270 return -- so data is missing
271 elseif errtxt then
272 self:add_error(section, "invalid", errtxt)
273 return
274 end
275 -- error ("\n option: " .. self.option ..
276 -- "\n fvalue: " .. tostring(fvalue) ..
277 -- "\n fexist: " .. tostring(fexist) ..
278 -- "\n cvalue: " .. tostring(cvalue) ..
279 -- "\n vvalue: " .. tostring(vvalue) ..
280 -- "\n vexist: " .. tostring(vexist) ..
281 -- "\n rm_opt: " .. tostring(rm_opt) ..
282 -- "\n eq_cfg: " .. tostring(eq_cfg) ..
283 -- "\n eq_def: " .. tostring(eq_def) ..
284 -- "\n novld : " .. tostring(novld) ..
285 -- "\n errtxt: " .. tostring(errtxt) )
286 end
287
288 -- lets continue with value returned from validate
289 eq_cfg = ( vvalue == cvalue ) -- update equal_config flag
290 local vexist = ( vvalue and (#vvalue > 0) ) and true or false -- not "nil" and "not empty"
291 local eq_def = ( vvalue == self.default ) -- equal_default flag
292
293 -- (rmempty or optional) and (no data or equal_default)
294 if rm_opt and (not vexist or eq_def) then
295 if self:remove(section) then -- remove data from UCI
296 self.section.changed = true -- and push events
297 end
298 return
299 end
300
301 -- not forcewrite and no changes, so nothing to write
302 if not self.forcewrite and eq_cfg then
303 return
304 end
305
306 -- we should have a valid value here
307 assert (vvalue, "\n option: " .. self.option ..
308 "\n fvalue: " .. tostring(fvalue) ..
309 "\n fexist: " .. tostring(fexist) ..
310 "\n cvalue: " .. tostring(cvalue) ..
311 "\n vvalue: " .. tostring(vvalue) ..
312 "\n vexist: " .. tostring(vexist) ..
313 "\n rm_opt: " .. tostring(rm_opt) ..
314 "\n eq_cfg: " .. tostring(eq_cfg) ..
315 "\n eq_def: " .. tostring(eq_def) ..
316 "\n errtxt: " .. tostring(errtxt) )
317
318 -- write data to UCI; raise event only on changes
319 if self:write(section, vvalue) and not eq_cfg then
320 self.section.changed = true
321 end
322 end
323
324 -----------------------------------------------------------------------------
325 -- copied from https://svn.nmap.org/nmap/nselib/url.lua
326 -- @author Diego Nehab
327 -- @author Eddie Bell <ejlbell@gmail.com>
328 --[[
329 URI parsing, composition and relative URL resolution
330 LuaSocket toolkit.
331 Author: Diego Nehab
332 RCS ID: $Id: url.lua,v 1.37 2005/11/22 08:33:29 diego Exp $
333 parse_query and build_query added For nmap (Eddie Bell <ejlbell@gmail.com>)
334 ]]--
335 ---
336 -- Parses a URL and returns a table with all its parts according to RFC 2396.
337 --
338 -- The following grammar describes the names given to the URL parts.
339 -- <code>
340 -- <url> ::= <scheme>://<authority>/<path>;<params>?<query>#<fragment>
341 -- <authority> ::= <userinfo>@<host>:<port>
342 -- <userinfo> ::= <user>[:<password>]
343 -- <path> :: = {<segment>/}<segment>
344 -- </code>
345 --
346 -- The leading <code>/</code> in <code>/<path></code> is considered part of
347 -- <code><path></code>.
348 -- @param url URL of request.
349 -- @param default Table with default values for each field.
350 -- @return A table with the following fields, where RFC naming conventions have
351 -- been preserved:
352 -- <code>scheme</code>, <code>authority</code>, <code>userinfo</code>,
353 -- <code>user</code>, <code>password</code>, <code>host</code>,
354 -- <code>port</code>, <code>path</code>, <code>params</code>,
355 -- <code>query</code>, and <code>fragment</code>.
356 -----------------------------------------------------------------------------
357 function parse_url(url) --, default)
358 -- initialize default parameters
359 local parsed = {}
360 -- for i,v in base.pairs(default or parsed) do
361 -- parsed[i] = v
362 -- end
363
364 -- remove whitespace
365 -- url = string.gsub(url, "%s", "")
366 -- get fragment
367 url = string.gsub(url, "#(.*)$",
368 function(f)
369 parsed.fragment = f
370 return ""
371 end)
372 -- get scheme. Lower-case according to RFC 3986 section 3.1.
373 url = string.gsub(url, "^([%w][%w%+%-%.]*)%:",
374 function(s)
375 parsed.scheme = string.lower(s);
376 return ""
377 end)
378 -- get authority
379 url = string.gsub(url, "^//([^/]*)",
380 function(n)
381 parsed.authority = n
382 return ""
383 end)
384 -- get query stringing
385 url = string.gsub(url, "%?(.*)",
386 function(q)
387 parsed.query = q
388 return ""
389 end)
390 -- get params
391 url = string.gsub(url, "%;(.*)",
392 function(p)
393 parsed.params = p
394 return ""
395 end)
396 -- path is whatever was left
397 parsed.path = url
398
399 local authority = parsed.authority
400 if not authority then
401 return parsed
402 end
403 authority = string.gsub(authority,"^([^@]*)@",
404 function(u)
405 parsed.userinfo = u;
406 return ""
407 end)
408 authority = string.gsub(authority, ":([0-9]*)$",
409 function(p)
410 if p ~= "" then
411 parsed.port = p
412 end;
413 return ""
414 end)
415 if authority ~= "" then
416 parsed.host = authority
417 end
418
419 local userinfo = parsed.userinfo
420 if not userinfo then
421 return parsed
422 end
423 userinfo = string.gsub(userinfo, ":([^:]*)$",
424 function(p)
425 parsed.password = p;
426 return ""
427 end)
428 parsed.user = userinfo
429 return parsed
430 end