luci-base: adds duid_to_mac to sys.lua
[project/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