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