luci-base: adds duid_to_mac to sys.lua
[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", "host",
194                 function(s)
195                         for mac in luci.util.imatch(s.mac) do
196                                 mac = luci.ip.checkmac(mac)
197                                 if mac then
198                                         _add(what, mac, s.ip, nil, s.name)
199                                 end
200                         end
201                 end)
202
203         for _, e in ipairs(nixio.getifaddrs()) do
204                 if e.name ~= "lo" then
205                         ifn[e.name] = ifn[e.name] or { }
206                         if e.family == "packet" and e.addr and #e.addr == 17 then
207                                 ifn[e.name][1] = e.addr:upper()
208                         elseif e.family == "inet" then
209                                 ifn[e.name][2] = e.addr
210                         elseif e.family == "inet6" then
211                                 ifn[e.name][3] = e.addr
212                         end
213                 end
214         end
215
216         for _, e in pairs(ifn) do
217                 if e[what] and (e[2] or e[3]) then
218                         _add(what, e[1], e[2], e[3], e[4])
219                 end
220         end
221
222         for _, e in pairs(hosts) do
223                 lookup[#lookup+1] = (what > 1) and e[what] or (e[2] or e[3])
224         end
225
226         if #lookup > 0 then
227                 lookup = luci.util.ubus("network.rrdns", "lookup", {
228                         addrs   = lookup,
229                         timeout = 250,
230                         limit   = 1000
231                 }) or { }
232         end
233
234         for _, e in luci.util.kspairs(hosts) do
235                 callback(e[1], e[2], e[3], lookup[e[2]] or lookup[e[3]] or e[4])
236         end
237 end
238
239 --          Each entry contains the values in the following order:
240 --          [ "mac", "name" ]
241 function net.mac_hints(callback)
242         if callback then
243                 _nethints(1, function(mac, v4, v6, name)
244                         name = name or v4
245                         if name and name ~= mac then
246                                 callback(mac, name or v4)
247                         end
248                 end)
249         else
250                 local rv = { }
251                 _nethints(1, function(mac, v4, v6, name)
252                         name = name or v4
253                         if name and name ~= mac then
254                                 rv[#rv+1] = { mac, name or v4 }
255                         end
256                 end)
257                 return rv
258         end
259 end
260
261 --          Each entry contains the values in the following order:
262 --          [ "ip", "name" ]
263 function net.ipv4_hints(callback)
264         if callback then
265                 _nethints(2, function(mac, v4, v6, name)
266                         name = name or mac
267                         if name and name ~= v4 then
268                                 callback(v4, name)
269                         end
270                 end)
271         else
272                 local rv = { }
273                 _nethints(2, function(mac, v4, v6, name)
274                         name = name or mac
275                         if name and name ~= v4 then
276                                 rv[#rv+1] = { v4, name }
277                         end
278                 end)
279                 return rv
280         end
281 end
282
283 --          Each entry contains the values in the following order:
284 --          [ "ip", "name" ]
285 function net.ipv6_hints(callback)
286         if callback then
287                 _nethints(3, function(mac, v4, v6, name)
288                         name = name or mac
289                         if name and name ~= v6 then
290                                 callback(v6, name)
291                         end
292                 end)
293         else
294                 local rv = { }
295                 _nethints(3, function(mac, v4, v6, name)
296                         name = name or mac
297                         if name and name ~= v6 then
298                                 rv[#rv+1] = { v6, name }
299                         end
300                 end)
301                 return rv
302         end
303 end
304
305 function net.host_hints(callback)
306         if callback then
307                 _nethints(1, function(mac, v4, v6, name)
308                         if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then
309                                 callback(mac, v4, v6, name)
310                         end
311                 end)
312         else
313                 local rv = { }
314                 _nethints(1, function(mac, v4, v6, name)
315                         if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then
316                                 local e = { }
317                                 if v4   then e.ipv4 = v4   end
318                                 if v6   then e.ipv6 = v6   end
319                                 if name then e.name = name end
320                                 rv[mac] = e
321                         end
322                 end)
323                 return rv
324         end
325 end
326
327 function net.conntrack(callback)
328         local ok, nfct = pcall(io.lines, "/proc/net/nf_conntrack")
329         if not ok or not nfct then
330                 return nil
331         end
332
333         local line, connt = nil, (not callback) and { }
334         for line in nfct do
335                 local fam, l3, l4, timeout, tuples =
336                         line:match("^(ipv[46]) +(%d+) +%S+ +(%d+) +(%d+) +(.+)$")
337
338                 if fam and l3 and l4 and timeout and not tuples:match("^TIME_WAIT ") then
339                         l4 = nixio.getprotobynumber(l4)
340
341                         local entry = {
342                                 bytes = 0,
343                                 packets = 0,
344                                 layer3 = fam,
345                                 layer4 = l4 and l4.name or "unknown",
346                                 timeout = tonumber(timeout, 10)
347                         }
348
349                         local key, val
350                         for key, val in tuples:gmatch("(%w+)=(%S+)") do
351                                 if key == "bytes" or key == "packets" then
352                                         entry[key] = entry[key] + tonumber(val, 10)
353                                 elseif key == "src" or key == "dst" then
354                                         if entry[key] == nil then
355                                                 entry[key] = luci.ip.new(val):string()
356                                         end
357                                 elseif key == "sport" or key == "dport" then
358                                         if entry[key] == nil then
359                                                 entry[key] = val
360                                         end
361                                 elseif val then
362                                         entry[key] = val
363                                 end
364                         end
365
366                         if callback then
367                                 callback(entry)
368                         else
369                                 connt[#connt+1] = entry
370                         end
371                 end
372         end
373
374         return callback and true or connt
375 end
376
377 function net.devices()
378         local devs = {}
379         local seen = {}
380         for k, v in ipairs(nixio.getifaddrs()) do
381                 if v.name and not seen[v.name] then
382                         seen[v.name] = true
383                         devs[#devs+1] = v.name
384                 end
385         end
386         return devs
387 end
388
389 function net.duid_to_mac(duid)
390         local b1, b2, b3, b4, b5, b6
391
392         if type(duid) == "string" then
393                 -- DUID-LLT / Ethernet
394                 if #duid == 28 then
395                         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$")
396
397                 -- DUID-LL / Ethernet
398                 elseif #duid == 20 then
399                         b1, b2, b3, b4, b5, b6 = duid:match("^00030001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$")
400
401                 -- DUID-LL / Ethernet (Without Header)
402                 elseif #duid == 12 then
403                         b1, b2, b3, b4, b5, b6 = duid:match("^(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$")
404                 end
405         end
406
407         return b1 and luci.ip.checkmac(table.concat({ b1, b2, b3, b4, b5, b6 }, ":"))
408 end
409
410 process = {}
411
412 function process.info(key)
413         local s = {uid = nixio.getuid(), gid = nixio.getgid()}
414         return not key and s or s[key]
415 end
416
417 function process.list()
418         local data = {}
419         local k
420         local ps = luci.util.execi("/bin/busybox top -bn1")
421
422         if not ps then
423                 return
424         end
425
426         for line in ps do
427                 local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match(
428                         "^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][W ][<N ]) +(%d+) +(%d+%%) +(%d+%%) +(.+)"
429                 )
430
431                 local idx = tonumber(pid)
432                 if idx then
433                         data[idx] = {
434                                 ['PID']     = pid,
435                                 ['PPID']    = ppid,
436                                 ['USER']    = user,
437                                 ['STAT']    = stat,
438                                 ['VSZ']     = vsz,
439                                 ['%MEM']    = mem,
440                                 ['%CPU']    = cpu,
441                                 ['COMMAND'] = cmd
442                         }
443                 end
444         end
445
446         return data
447 end
448
449 function process.setgroup(gid)
450         return nixio.setgid(gid)
451 end
452
453 function process.setuser(uid)
454         return nixio.setuid(uid)
455 end
456
457 process.signal = nixio.kill
458
459 local function xclose(fd)
460         if fd and fd:fileno() > 2 then
461                 fd:close()
462         end
463 end
464
465 function process.exec(command, stdout, stderr, nowait)
466         local out_r, out_w, err_r, err_w
467         if stdout then out_r, out_w = nixio.pipe() end
468         if stderr then err_r, err_w = nixio.pipe() end
469
470         local pid = nixio.fork()
471         if pid == 0 then
472                 nixio.chdir("/")
473
474                 local null = nixio.open("/dev/null", "w+")
475                 if null then
476                         nixio.dup(out_w or null, nixio.stdout)
477                         nixio.dup(err_w or null, nixio.stderr)
478                         nixio.dup(null, nixio.stdin)
479                         xclose(out_w)
480                         xclose(out_r)
481                         xclose(err_w)
482                         xclose(err_r)
483                         xclose(null)
484                 end
485
486                 nixio.exec(unpack(command))
487                 os.exit(-1)
488         end
489
490         local _, pfds, rv = nil, {}, { code = -1, pid = pid }
491
492         xclose(out_w)
493         xclose(err_w)
494
495         if out_r then
496                 pfds[#pfds+1] = {
497                         fd = out_r,
498                         cb = type(stdout) == "function" and stdout,
499                         name = "stdout",
500                         events = nixio.poll_flags("in", "err", "hup")
501                 }
502         end
503
504         if err_r then
505                 pfds[#pfds+1] = {
506                         fd = err_r,
507                         cb = type(stderr) == "function" and stderr,
508                         name = "stderr",
509                         events = nixio.poll_flags("in", "err", "hup")
510                 }
511         end
512
513         while #pfds > 0 do
514                 local nfds, err = nixio.poll(pfds, -1)
515                 if not nfds and err ~= nixio.const.EINTR then
516                         break
517                 end
518
519                 local i
520                 for i = #pfds, 1, -1 do
521                         local rfd = pfds[i]
522                         if rfd.revents > 0 then
523                                 local chunk, err = rfd.fd:read(4096)
524                                 if chunk and #chunk > 0 then
525                                         if rfd.cb then
526                                                 rfd.cb(chunk)
527                                         else
528                                                 rfd.buf = rfd.buf or {}
529                                                 rfd.buf[#rfd.buf + 1] = chunk
530                                         end
531                                 else
532                                         table.remove(pfds, i)
533                                         if rfd.buf then
534                                                 rv[rfd.name] = table.concat(rfd.buf, "")
535                                         end
536                                         rfd.fd:close()
537                                 end
538                         end
539                 end
540         end
541
542         if not nowait then
543                 _, _, rv.code = nixio.waitpid(pid)
544         end
545
546         return rv
547 end
548
549
550 user = {}
551
552 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
553 user.getuser = nixio.getpw
554
555 function user.getpasswd(username)
556         local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
557         local pwh = pwe and (pwe.pwdp or pwe.passwd)
558         if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
559                 return nil, pwe
560         else
561                 return pwh, pwe
562         end
563 end
564
565 function user.checkpasswd(username, pass)
566         local pwh, pwe = user.getpasswd(username)
567         if pwe then
568                 return (pwh == nil or nixio.crypt(pass, pwh) == pwh)
569         end
570         return false
571 end
572
573 function user.setpasswd(username, password)
574         return os.execute("(echo %s; sleep 1; echo %s) | passwd %s >/dev/null 2>&1" %{
575                 luci.util.shellquote(password),
576                 luci.util.shellquote(password),
577                 luci.util.shellquote(username)
578         })
579 end
580
581
582 wifi = {}
583
584 function wifi.getiwinfo(ifname)
585         ntm.init()
586
587         local wnet = ntm:get_wifinet(ifname)
588         if wnet and wnet.iwinfo then
589                 return wnet.iwinfo
590         end
591
592         local wdev = ntm:get_wifidev(ifname)
593         if wdev and wdev.iwinfo then
594                 return wdev.iwinfo
595         end
596
597         return { ifname = ifname }
598 end
599
600
601 init = {}
602 init.dir = "/etc/init.d/"
603
604 function init.names()
605         local names = { }
606         for name in fs.glob(init.dir.."*") do
607                 names[#names+1] = fs.basename(name)
608         end
609         return names
610 end
611
612 function init.index(name)
613         if fs.access(init.dir..name) then
614                 return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null"
615                         %{ init.dir, name })
616         end
617 end
618
619 local function init_action(action, name)
620         if fs.access(init.dir..name) then
621                 return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action })
622         end
623 end
624
625 function init.enabled(name)
626         return (init_action("enabled", name) == 0)
627 end
628
629 function init.enable(name)
630         return (init_action("enable", name) == 1)
631 end
632
633 function init.disable(name)
634         return (init_action("disable", name) == 0)
635 end
636
637 function init.start(name)
638         return (init_action("start", name) == 0)
639 end
640
641 function init.stop(name)
642         return (init_action("stop", name) == 0)
643 end