luci-base: form.js: treat inactive options as optional
[oweals/luci.git] / modules / luci-base / luasrc / sys.lua
1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Licensed to the public under the Apache License 2.0.
3
4 local io     = require "io"
5 local os     = require "os"
6 local table  = require "table"
7 local nixio  = require "nixio"
8 local fs     = require "nixio.fs"
9 local uci    = require "luci.model.uci"
10 local ntm    = require "luci.model.network"
11
12 local luci  = {}
13 luci.util   = require "luci.util"
14 luci.ip     = require "luci.ip"
15
16 local tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select, unpack =
17         tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select, unpack
18
19
20 module "luci.sys"
21
22 function call(...)
23         return os.execute(...) / 256
24 end
25
26 exec = luci.util.exec
27
28 -- containing the whole environment is returned otherwise this function returns
29 -- the corresponding string value for the given name or nil if no such variable
30 -- exists.
31 getenv = nixio.getenv
32
33 function hostname(newname)
34         if type(newname) == "string" and #newname > 0 then
35                 fs.writefile( "/proc/sys/kernel/hostname", newname )
36                 return newname
37         else
38                 return nixio.uname().nodename
39         end
40 end
41
42 function httpget(url, stream, target)
43         if not target then
44                 local source = stream and io.popen or luci.util.exec
45                 return source("wget -qO- %s" % luci.util.shellquote(url))
46         else
47                 return os.execute("wget -qO %s %s" %
48                         {luci.util.shellquote(target), luci.util.shellquote(url)})
49         end
50 end
51
52 function reboot()
53         return os.execute("reboot >/dev/null 2>&1")
54 end
55
56 function syslog()
57         return luci.util.exec("logread")
58 end
59
60 function dmesg()
61         return luci.util.exec("dmesg")
62 end
63
64 function uniqueid(bytes)
65         local rand = fs.readfile("/dev/urandom", bytes)
66         return rand and nixio.bin.hexlify(rand)
67 end
68
69 function uptime()
70         return nixio.sysinfo().uptime
71 end
72
73
74 net = {}
75
76 local function _nethints(what, callback)
77         local _, k, e, mac, ip, name, duid, iaid
78         local cur = uci.cursor()
79         local ifn = { }
80         local hosts = { }
81         local lookup = { }
82
83         local function _add(i, ...)
84                 local k = select(i, ...)
85                 if k then
86                         if not hosts[k] then hosts[k] = { } end
87                         hosts[k][1] = select(1, ...) or hosts[k][1]
88                         hosts[k][2] = select(2, ...) or hosts[k][2]
89                         hosts[k][3] = select(3, ...) or hosts[k][3]
90                         hosts[k][4] = select(4, ...) or hosts[k][4]
91                 end
92         end
93
94         luci.ip.neighbors(nil, function(neigh)
95                 if neigh.mac and neigh.family == 4 then
96                         _add(what, neigh.mac:string(), neigh.dest:string(), nil, nil)
97                 elseif neigh.mac and neigh.family == 6 then
98                         _add(what, neigh.mac:string(), nil, neigh.dest:string(), nil)
99                 end
100         end)
101
102         if fs.access("/etc/ethers") then
103                 for e in io.lines("/etc/ethers") do
104                         mac, name = e:match("^([a-fA-F0-9:-]+)%s+(%S+)")
105                         mac = luci.ip.checkmac(mac)
106                         if mac and name then
107                                 if luci.ip.checkip4(name) then
108                                         _add(what, mac, name, nil, nil)
109                                 else
110                                         _add(what, mac, nil, nil, name)
111                                 end
112                         end
113                 end
114         end
115
116         cur:foreach("dhcp", "dnsmasq",
117                 function(s)
118                         if s.leasefile and fs.access(s.leasefile) then
119                                 for e in io.lines(s.leasefile) do
120                                         mac, ip, name = e:match("^%d+ (%S+) (%S+) (%S+)")
121                                         mac = luci.ip.checkmac(mac)
122                                         if mac and ip then
123                                                 _add(what, mac, ip, nil, name ~= "*" and name)
124                                         end
125                                 end
126                         end
127                 end
128         )
129
130         cur:foreach("dhcp", "odhcpd",
131                 function(s)
132                         if type(s.leasefile) == "string" and fs.access(s.leasefile) then
133                                 for e in io.lines(s.leasefile) do
134                                         duid, iaid, name, _, ip = e:match("^# %S+ (%S+) (%S+) (%S+) (-?%d+) %S+ %S+ ([0-9a-f:.]+)/[0-9]+")
135                                         mac = net.duid_to_mac(duid)
136                                         if mac then
137                                                 if ip and iaid == "ipv4" then
138                                                         _add(what, mac, ip, nil, name ~= "*" and name)
139                                                 elseif ip then
140                                                         _add(what, mac, nil, ip, name ~= "*" and name)
141                                                 end
142                                         end
143                                 end
144                         end
145                 end
146         )
147
148         cur:foreach("dhcp", "host",
149                 function(s)
150                         for mac in luci.util.imatch(s.mac) do
151                                 mac = luci.ip.checkmac(mac)
152                                 if mac then
153                                         _add(what, mac, s.ip, nil, s.name)
154                                 end
155                         end
156                 end)
157
158         for _, e in ipairs(nixio.getifaddrs()) do
159                 if e.name ~= "lo" then
160                         ifn[e.name] = ifn[e.name] or { }
161                         if e.family == "packet" and e.addr and #e.addr == 17 then
162                                 ifn[e.name][1] = e.addr:upper()
163                         elseif e.family == "inet" then
164                                 ifn[e.name][2] = e.addr
165                         elseif e.family == "inet6" then
166                                 ifn[e.name][3] = e.addr
167                         end
168                 end
169         end
170
171         for _, e in pairs(ifn) do
172                 if e[what] and (e[2] or e[3]) then
173                         _add(what, e[1], e[2], e[3], e[4])
174                 end
175         end
176
177         for _, e in pairs(hosts) do
178                 lookup[#lookup+1] = (what > 1) and e[what] or (e[2] or e[3])
179         end
180
181         if #lookup > 0 then
182                 lookup = luci.util.ubus("network.rrdns", "lookup", {
183                         addrs   = lookup,
184                         timeout = 250,
185                         limit   = 1000
186                 }) or { }
187         end
188
189         for _, e in luci.util.kspairs(hosts) do
190                 callback(e[1], e[2], e[3], lookup[e[2]] or lookup[e[3]] or e[4])
191         end
192 end
193
194 --          Each entry contains the values in the following order:
195 --          [ "mac", "name" ]
196 function net.mac_hints(callback)
197         if callback then
198                 _nethints(1, function(mac, v4, v6, name)
199                         name = name or v4
200                         if name and name ~= mac then
201                                 callback(mac, name or v4)
202                         end
203                 end)
204         else
205                 local rv = { }
206                 _nethints(1, function(mac, v4, v6, name)
207                         name = name or v4
208                         if name and name ~= mac then
209                                 rv[#rv+1] = { mac, name or v4 }
210                         end
211                 end)
212                 return rv
213         end
214 end
215
216 --          Each entry contains the values in the following order:
217 --          [ "ip", "name" ]
218 function net.ipv4_hints(callback)
219         if callback then
220                 _nethints(2, function(mac, v4, v6, name)
221                         name = name or mac
222                         if name and name ~= v4 then
223                                 callback(v4, name)
224                         end
225                 end)
226         else
227                 local rv = { }
228                 _nethints(2, function(mac, v4, v6, name)
229                         name = name or mac
230                         if name and name ~= v4 then
231                                 rv[#rv+1] = { v4, name }
232                         end
233                 end)
234                 return rv
235         end
236 end
237
238 --          Each entry contains the values in the following order:
239 --          [ "ip", "name" ]
240 function net.ipv6_hints(callback)
241         if callback then
242                 _nethints(3, function(mac, v4, v6, name)
243                         name = name or mac
244                         if name and name ~= v6 then
245                                 callback(v6, name)
246                         end
247                 end)
248         else
249                 local rv = { }
250                 _nethints(3, function(mac, v4, v6, name)
251                         name = name or mac
252                         if name and name ~= v6 then
253                                 rv[#rv+1] = { v6, name }
254                         end
255                 end)
256                 return rv
257         end
258 end
259
260 function net.host_hints(callback)
261         if callback then
262                 _nethints(1, function(mac, v4, v6, name)
263                         if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then
264                                 callback(mac, v4, v6, name)
265                         end
266                 end)
267         else
268                 local rv = { }
269                 _nethints(1, function(mac, v4, v6, name)
270                         if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then
271                                 local e = { }
272                                 if v4   then e.ipv4 = v4   end
273                                 if v6   then e.ipv6 = v6   end
274                                 if name then e.name = name end
275                                 rv[mac] = e
276                         end
277                 end)
278                 return rv
279         end
280 end
281
282 function net.conntrack(callback)
283         local ok, nfct = pcall(io.lines, "/proc/net/nf_conntrack")
284         if not ok or not nfct then
285                 return nil
286         end
287
288         local line, connt = nil, (not callback) and { }
289         for line in nfct do
290                 local fam, l3, l4, timeout, tuples =
291                         line:match("^(ipv[46]) +(%d+) +%S+ +(%d+) +(%d+) +(.+)$")
292
293                 if fam and l3 and l4 and timeout and not tuples:match("^TIME_WAIT ") then
294                         l4 = nixio.getprotobynumber(l4)
295
296                         local entry = {
297                                 bytes = 0,
298                                 packets = 0,
299                                 layer3 = fam,
300                                 layer4 = l4 and l4.name or "unknown",
301                                 timeout = tonumber(timeout, 10)
302                         }
303
304                         local key, val
305                         for key, val in tuples:gmatch("(%w+)=(%S+)") do
306                                 if key == "bytes" or key == "packets" then
307                                         entry[key] = entry[key] + tonumber(val, 10)
308                                 elseif key == "src" or key == "dst" then
309                                         if entry[key] == nil then
310                                                 entry[key] = luci.ip.new(val):string()
311                                         end
312                                 elseif key == "sport" or key == "dport" then
313                                         if entry[key] == nil then
314                                                 entry[key] = val
315                                         end
316                                 elseif val then
317                                         entry[key] = val
318                                 end
319                         end
320
321                         if callback then
322                                 callback(entry)
323                         else
324                                 connt[#connt+1] = entry
325                         end
326                 end
327         end
328
329         return callback and true or connt
330 end
331
332 function net.devices()
333         local devs = {}
334         local seen = {}
335         for k, v in ipairs(nixio.getifaddrs()) do
336                 if v.name and not seen[v.name] then
337                         seen[v.name] = true
338                         devs[#devs+1] = v.name
339                 end
340         end
341         return devs
342 end
343
344 function net.duid_to_mac(duid)
345         local b1, b2, b3, b4, b5, b6
346
347         if type(duid) == "string" then
348                 -- DUID-LLT / Ethernet
349                 if #duid == 28 then
350                         b1, b2, b3, b4, b5, b6 = duid:match("^00010001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)%x%x%x%x%x%x%x%x$")
351
352                 -- DUID-LL / Ethernet
353                 elseif #duid == 20 then
354                         b1, b2, b3, b4, b5, b6 = duid:match("^00030001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$")
355
356                 -- DUID-LL / Ethernet (Without Header)
357                 elseif #duid == 12 then
358                         b1, b2, b3, b4, b5, b6 = duid:match("^(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$")
359                 end
360         end
361
362         return b1 and luci.ip.checkmac(table.concat({ b1, b2, b3, b4, b5, b6 }, ":"))
363 end
364
365 process = {}
366
367 function process.info(key)
368         local s = {uid = nixio.getuid(), gid = nixio.getgid()}
369         return not key and s or s[key]
370 end
371
372 function process.list()
373         local data = {}
374         local k
375         local ps = luci.util.execi("/bin/busybox top -bn1")
376
377         if not ps then
378                 return
379         end
380
381         for line in ps do
382                 local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match(
383                         "^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][<NW ][<N ]) +(%d+) +(%d+%%) +(%d+%%) +(.+)"
384                 )
385
386                 local idx = tonumber(pid)
387                 if idx and not cmd:match("top %-bn1") then
388                         data[idx] = {
389                                 ['PID']     = pid,
390                                 ['PPID']    = ppid,
391                                 ['USER']    = user,
392                                 ['STAT']    = stat,
393                                 ['VSZ']     = vsz,
394                                 ['%MEM']    = mem,
395                                 ['%CPU']    = cpu,
396                                 ['COMMAND'] = cmd
397                         }
398                 end
399         end
400
401         return data
402 end
403
404 function process.setgroup(gid)
405         return nixio.setgid(gid)
406 end
407
408 function process.setuser(uid)
409         return nixio.setuid(uid)
410 end
411
412 process.signal = nixio.kill
413
414 local function xclose(fd)
415         if fd and fd:fileno() > 2 then
416                 fd:close()
417         end
418 end
419
420 function process.exec(command, stdout, stderr, nowait)
421         local out_r, out_w, err_r, err_w
422         if stdout then out_r, out_w = nixio.pipe() end
423         if stderr then err_r, err_w = nixio.pipe() end
424
425         local pid = nixio.fork()
426         if pid == 0 then
427                 nixio.chdir("/")
428
429                 local null = nixio.open("/dev/null", "w+")
430                 if null then
431                         nixio.dup(out_w or null, nixio.stdout)
432                         nixio.dup(err_w or null, nixio.stderr)
433                         nixio.dup(null, nixio.stdin)
434                         xclose(out_w)
435                         xclose(out_r)
436                         xclose(err_w)
437                         xclose(err_r)
438                         xclose(null)
439                 end
440
441                 nixio.exec(unpack(command))
442                 os.exit(-1)
443         end
444
445         local _, pfds, rv = nil, {}, { code = -1, pid = pid }
446
447         xclose(out_w)
448         xclose(err_w)
449
450         if out_r then
451                 pfds[#pfds+1] = {
452                         fd = out_r,
453                         cb = type(stdout) == "function" and stdout,
454                         name = "stdout",
455                         events = nixio.poll_flags("in", "err", "hup")
456                 }
457         end
458
459         if err_r then
460                 pfds[#pfds+1] = {
461                         fd = err_r,
462                         cb = type(stderr) == "function" and stderr,
463                         name = "stderr",
464                         events = nixio.poll_flags("in", "err", "hup")
465                 }
466         end
467
468         while #pfds > 0 do
469                 local nfds, err = nixio.poll(pfds, -1)
470                 if not nfds and err ~= nixio.const.EINTR then
471                         break
472                 end
473
474                 local i
475                 for i = #pfds, 1, -1 do
476                         local rfd = pfds[i]
477                         if rfd.revents > 0 then
478                                 local chunk, err = rfd.fd:read(4096)
479                                 if chunk and #chunk > 0 then
480                                         if rfd.cb then
481                                                 rfd.cb(chunk)
482                                         else
483                                                 rfd.buf = rfd.buf or {}
484                                                 rfd.buf[#rfd.buf + 1] = chunk
485                                         end
486                                 else
487                                         table.remove(pfds, i)
488                                         if rfd.buf then
489                                                 rv[rfd.name] = table.concat(rfd.buf, "")
490                                         end
491                                         rfd.fd:close()
492                                 end
493                         end
494                 end
495         end
496
497         if not nowait then
498                 _, _, rv.code = nixio.waitpid(pid)
499         end
500
501         return rv
502 end
503
504
505 user = {}
506
507 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
508 user.getuser = nixio.getpw
509
510 function user.getpasswd(username)
511         local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
512         local pwh = pwe and (pwe.pwdp or pwe.passwd)
513         if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
514                 return nil, pwe
515         else
516                 return pwh, pwe
517         end
518 end
519
520 function user.checkpasswd(username, pass)
521         local pwh, pwe = user.getpasswd(username)
522         if pwe then
523                 return (pwh == nil or nixio.crypt(pass, pwh) == pwh)
524         end
525         return false
526 end
527
528 function user.setpasswd(username, password)
529         return os.execute("(echo %s; sleep 1; echo %s) | passwd %s >/dev/null 2>&1" %{
530                 luci.util.shellquote(password),
531                 luci.util.shellquote(password),
532                 luci.util.shellquote(username)
533         })
534 end
535
536
537 wifi = {}
538
539 function wifi.getiwinfo(ifname)
540         ntm.init()
541
542         local wnet = ntm:get_wifinet(ifname)
543         if wnet and wnet.iwinfo then
544                 return wnet.iwinfo
545         end
546
547         local wdev = ntm:get_wifidev(ifname)
548         if wdev and wdev.iwinfo then
549                 return wdev.iwinfo
550         end
551
552         return { ifname = ifname }
553 end
554
555
556 init = {}
557 init.dir = "/etc/init.d/"
558
559 function init.names()
560         local names = { }
561         for name in fs.glob(init.dir.."*") do
562                 names[#names+1] = fs.basename(name)
563         end
564         return names
565 end
566
567 function init.index(name)
568         if fs.access(init.dir..name) then
569                 return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null"
570                         %{ init.dir, name })
571         end
572 end
573
574 local function init_action(action, name)
575         if fs.access(init.dir..name) then
576                 return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action })
577         end
578 end
579
580 function init.enabled(name)
581         return (init_action("enabled", name) == 0)
582 end
583
584 function init.enable(name)
585         return (init_action("enable", name) == 0)
586 end
587
588 function init.disable(name)
589         return (init_action("disable", name) == 0)
590 end
591
592 function init.start(name)
593         return (init_action("start", name) == 0)
594 end
595
596 function init.stop(name)
597         return (init_action("stop", name) == 0)
598 end
599
600 function init.restart(name)
601         return (init_action("restart", name) == 0)
602 end
603
604 function init.reload(name)
605         return (init_action("reload", name) == 0)
606 end