luci-app-ddns: update to support ddns-scripts 2.7.6
[oweals/luci.git] / applications / luci-app-ddns / luasrc / tools / ddns.lua
1 -- Copyright 2014-2016 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 PID from run file and verify if still running
78 function get_pid(section)
79         local uci  = UCI.cursor()
80         local rdir = uci:get("ddns", "global", "ddns_rundir") or "/var/run/ddns"
81         local pid  = tonumber(NXFS.readfile("%s/%s.pid" % { rdir, section } ) or 0 )
82         if pid > 0 and not NX.kill(pid, 0) then
83                 pid = 0
84         end
85         uci:unload("ddns")
86         return pid
87 end
88
89 -- replacement of build-in read of UCI option
90 -- modified AbstractValue.cfgvalue(self, section) from cbi.lua
91 -- needed to read from other option then current value definition
92 function read_value(self, section, option)
93         local value
94         if self.tag_error[section] then
95                 value = self:formvalue(section)
96         else
97                 value = self.map:get(section, option)
98         end
99
100         if not value then
101                 return nil
102         elseif not self.cast or self.cast == type(value) then
103                 return value
104         elseif self.cast == "string" then
105                 if type(value) == "table" then
106                         return value[1]
107                 end
108         elseif self.cast == "table" then
109                 return { value }
110         end
111 end
112
113 -- replacement of build-in parse of "Value"
114 -- modified AbstractValue.parse(self, section, novld) from cbi.lua
115 -- validate is called if rmempty/optional true or false
116 -- before write check if forcewrite, value eq default, and more
117 function value_parse(self, section, novld)
118         local fvalue = self:formvalue(section)
119         local fexist = ( fvalue and (#fvalue > 0) )     -- not "nil" and "not empty"
120         local cvalue = self:cfgvalue(section)
121         local rm_opt = ( self.rmempty or self.optional )
122         local eq_cfg                                    -- flag: equal cfgvalue
123
124         -- If favlue and cvalue are both tables and have the same content
125         -- make them identical
126         if type(fvalue) == "table" and type(cvalue) == "table" then
127                 eq_cfg = (#fvalue == #cvalue)
128                 if eq_cfg then
129                         for i=1, #fvalue do
130                                 if cvalue[i] ~= fvalue[i] then
131                                         eq_cfg = false
132                                 end
133                         end
134                 end
135                 if eq_cfg then
136                         fvalue = cvalue
137                 end
138         end
139
140         -- removed parameter "section" from function call because used/accepted nowhere
141         -- also removed call to function "transfer"
142         local vvalue, errtxt = self:validate(fvalue)
143
144         -- error handling; validate return "nil"
145         if not vvalue then
146                 if novld then           -- and "novld" set
147                         return          -- then exit without raising an error
148                 end
149
150                 if fexist then          -- and there is a formvalue
151                         self:add_error(section, "invalid", errtxt or self.title .. ": invalid")
152                         return          -- so data are invalid
153                 elseif not rm_opt then  -- and empty formvalue but NOT (rmempty or optional) set
154                         self:add_error(section, "missing", errtxt or self.title .. ": missing")
155                         return          -- so data is missing
156                 elseif errtxt then
157                         self:add_error(section, "invalid", errtxt)
158                         return
159                 end
160 --              error  ("\n option: " .. self.option ..
161 --                      "\n fvalue: " .. tostring(fvalue) ..
162 --                      "\n fexist: " .. tostring(fexist) ..
163 --                      "\n cvalue: " .. tostring(cvalue) ..
164 --                      "\n vvalue: " .. tostring(vvalue) ..
165 --                      "\n vexist: " .. tostring(vexist) ..
166 --                      "\n rm_opt: " .. tostring(rm_opt) ..
167 --                      "\n eq_cfg: " .. tostring(eq_cfg) ..
168 --                      "\n eq_def: " .. tostring(eq_def) ..
169 --                      "\n novld : " .. tostring(novld) ..
170 --                      "\n errtxt: " .. tostring(errtxt) )
171         end
172
173         -- lets continue with value returned from validate
174         eq_cfg  = ( vvalue == cvalue )                                  -- update equal_config flag
175         local vexist = ( vvalue and (#vvalue > 0) ) and true or false   -- not "nil" and "not empty"
176         local eq_def = ( vvalue == self.default )                       -- equal_default flag
177
178         -- (rmempty or optional) and (no data or equal_default)
179         if rm_opt and (not vexist or eq_def) then
180                 if self:remove(section) then            -- remove data from UCI
181                         self.section.changed = true     -- and push events
182                 end
183                 return
184         end
185
186         -- not forcewrite and no changes, so nothing to write
187         if not self.forcewrite and eq_cfg then
188                 return
189         end
190
191         -- we should have a valid value here
192         assert (vvalue, "\n option: " .. self.option ..
193                         "\n fvalue: " .. tostring(fvalue) ..
194                         "\n fexist: " .. tostring(fexist) ..
195                         "\n cvalue: " .. tostring(cvalue) ..
196                         "\n vvalue: " .. tostring(vvalue) ..
197                         "\n vexist: " .. tostring(vexist) ..
198                         "\n rm_opt: " .. tostring(rm_opt) ..
199                         "\n eq_cfg: " .. tostring(eq_cfg) ..
200                         "\n eq_def: " .. tostring(eq_def) ..
201                         "\n errtxt: " .. tostring(errtxt) )
202
203         -- write data to UCI; raise event only on changes
204         if self:write(section, vvalue) and not eq_cfg then
205                 self.section.changed = true
206         end
207 end
208
209 -----------------------------------------------------------------------------
210 -- copied from https://svn.nmap.org/nmap/nselib/url.lua
211 -- @author Diego Nehab
212 -- @author Eddie Bell <ejlbell@gmail.com>
213 --[[
214     URI parsing, composition and relative URL resolution
215     LuaSocket toolkit.
216     Author: Diego Nehab
217     RCS ID: $Id: url.lua,v 1.37 2005/11/22 08:33:29 diego Exp $
218     parse_query and build_query added For nmap (Eddie Bell <ejlbell@gmail.com>)
219 ]]--
220 ---
221 -- Parses a URL and returns a table with all its parts according to RFC 2396.
222 --
223 -- The following grammar describes the names given to the URL parts.
224 -- <code>
225 -- <url> ::= <scheme>://<authority>/<path>;<params>?<query>#<fragment>
226 -- <authority> ::= <userinfo>@<host>:<port>
227 -- <userinfo> ::= <user>[:<password>]
228 -- <path> :: = {<segment>/}<segment>
229 -- </code>
230 --
231 -- The leading <code>/</code> in <code>/<path></code> is considered part of
232 -- <code><path></code>.
233 -- @param url URL of request.
234 -- @param default Table with default values for each field.
235 -- @return A table with the following fields, where RFC naming conventions have
236 --   been preserved:
237 --     <code>scheme</code>, <code>authority</code>, <code>userinfo</code>,
238 --     <code>user</code>, <code>password</code>, <code>host</code>,
239 --     <code>port</code>, <code>path</code>, <code>params</code>,
240 --     <code>query</code>, and <code>fragment</code>.
241 -----------------------------------------------------------------------------
242 function parse_url(url) --, default)
243         -- initialize default parameters
244         local parsed = {}
245 --      for i,v in base.pairs(default or parsed) do
246 --              parsed[i] = v
247 --      end
248
249         -- remove whitespace
250 --      url = string.gsub(url, "%s", "")
251         -- get fragment
252         url = string.gsub(url, "#(.*)$",
253                 function(f)
254                         parsed.fragment = f
255                         return ""
256                 end)
257         -- get scheme. Lower-case according to RFC 3986 section 3.1.
258         url = string.gsub(url, "^([%w][%w%+%-%.]*)%:",
259                 function(s)
260                         parsed.scheme = string.lower(s);
261                         return ""
262                 end)
263         -- get authority
264         url = string.gsub(url, "^//([^/]*)",
265                 function(n)
266                         parsed.authority = n
267                         return ""
268                 end)
269         -- get query stringing
270         url = string.gsub(url, "%?(.*)",
271                 function(q)
272                         parsed.query = q
273                         return ""
274                 end)
275         -- get params
276         url = string.gsub(url, "%;(.*)",
277                 function(p)
278                         parsed.params = p
279                         return ""
280                 end)
281         -- path is whatever was left
282         parsed.path = url
283
284         local authority = parsed.authority
285         if not authority then
286                 return parsed
287         end
288         authority = string.gsub(authority,"^([^@]*)@",
289                 function(u)
290                         parsed.userinfo = u;
291                         return ""
292                 end)
293         authority = string.gsub(authority, ":([0-9]*)$",
294                 function(p)
295                         if p ~= "" then
296                                 parsed.port = p
297                         end;
298                         return ""
299                 end)
300         if authority ~= "" then
301                 parsed.host = authority
302         end
303
304         local userinfo = parsed.userinfo
305         if not userinfo then
306                 return parsed
307         end
308         userinfo = string.gsub(userinfo, ":([^:]*)$",
309                 function(p)
310                         parsed.password = p;
311                         return ""
312                 end)
313         parsed.user = userinfo
314         return parsed
315 end