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