Merge pull request #715 from chris5560/master
[project/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) and (has_bindhost or has_hostip))
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", "date_format") 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 run_dir = uci:get("ddns", "global", "run_dir") or "/var/run/ddns"
72 local etime = tonumber(NXFS.readfile("%s/%s.update" % { run_dir, 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 run_dir = uci:get("ddns", "global", "run_dir") or "/var/run/ddns"
81 local pid = tonumber(NXFS.readfile("%s/%s.pid" % { run_dir, 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