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