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