sys.lua _nethints(): read location of dhcp.lease file from uci
[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         cur:foreach("dhcp", "dnsmasq",
189                 function(s)
190                         if s.leasefile and fs.access(s.leasefile) then
191                                 for e in io.lines(s.leasefile) do
192                                         mac, ip, name = e:match("^%d+ (%S+) (%S+) (%S+)")
193                                         if mac and ip then
194                                                 _add(what, mac:upper(), ip, nil, name ~= "*" and name)
195                                         end
196                                 end
197                         end
198                 end
199         )
200
201         cur:foreach("dhcp", "host",
202                 function(s)
203                         for mac in luci.util.imatch(s.mac) do
204                                 _add(what, mac:upper(), s.ip, nil, s.name)
205                         end
206                 end)
207
208         for _, e in ipairs(nixio.getifaddrs()) do
209                 if e.name ~= "lo" then
210                         ifn[e.name] = ifn[e.name] or { }
211                         if e.family == "packet" and e.addr and #e.addr == 17 then
212                                 ifn[e.name][1] = e.addr:upper()
213                         elseif e.family == "inet" then
214                                 ifn[e.name][2] = e.addr
215                         elseif e.family == "inet6" then
216                                 ifn[e.name][3] = e.addr
217                         end
218                 end
219         end
220
221         for _, e in pairs(ifn) do
222                 if e[what] and (e[2] or e[3]) then
223                         _add(what, e[1], e[2], e[3], e[4])
224                 end
225         end
226
227         for _, e in luci.util.kspairs(hosts) do
228                 callback(e[1], e[2], e[3], e[4])
229         end
230 end
231
232 --          Each entry contains the values in the following order:
233 --          [ "mac", "name" ]
234 function net.mac_hints(callback)
235         if callback then
236                 _nethints(1, function(mac, v4, v6, name)
237                         name = name or nixio.getnameinfo(v4 or v6, nil, 100) or v4
238                         if name and name ~= mac then
239                                 callback(mac, name or nixio.getnameinfo(v4 or v6, nil, 100) or v4)
240                         end
241                 end)
242         else
243                 local rv = { }
244                 _nethints(1, function(mac, v4, v6, name)
245                         name = name or nixio.getnameinfo(v4 or v6, nil, 100) or v4
246                         if name and name ~= mac then
247                                 rv[#rv+1] = { mac, name or nixio.getnameinfo(v4 or v6, nil, 100) or v4 }
248                         end
249                 end)
250                 return rv
251         end
252 end
253
254 --          Each entry contains the values in the following order:
255 --          [ "ip", "name" ]
256 function net.ipv4_hints(callback)
257         if callback then
258                 _nethints(2, function(mac, v4, v6, name)
259                         name = name or nixio.getnameinfo(v4, nil, 100) or mac
260                         if name and name ~= v4 then
261                                 callback(v4, name)
262                         end
263                 end)
264         else
265                 local rv = { }
266                 _nethints(2, function(mac, v4, v6, name)
267                         name = name or nixio.getnameinfo(v4, nil, 100) or mac
268                         if name and name ~= v4 then
269                                 rv[#rv+1] = { v4, name }
270                         end
271                 end)
272                 return rv
273         end
274 end
275
276 --          Each entry contains the values in the following order:
277 --          [ "ip", "name" ]
278 function net.ipv6_hints(callback)
279         if callback then
280                 _nethints(3, function(mac, v4, v6, name)
281                         name = name or nixio.getnameinfo(v6, nil, 100) or mac
282                         if name and name ~= v6 then
283                                 callback(v6, name)
284                         end
285                 end)
286         else
287                 local rv = { }
288                 _nethints(3, function(mac, v4, v6, name)
289                         name = name or nixio.getnameinfo(v6, nil, 100) or mac
290                         if name and name ~= v6 then
291                                 rv[#rv+1] = { v6, name }
292                         end
293                 end)
294                 return rv
295         end
296 end
297
298 function net.host_hints(callback)
299         if callback then
300                 _nethints(1, function(mac, v4, v6, name)
301                         if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then
302                                 callback(mac, v4, v6, name)
303                         end
304                 end)
305         else
306                 local rv = { }
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                                 local e = { }
310                                 if v4   then e.ipv4 = v4   end
311                                 if v6   then e.ipv6 = v6   end
312                                 if name then e.name = name end
313                                 rv[mac] = e
314                         end
315                 end)
316                 return rv
317         end
318 end
319
320 function net.conntrack(callback)
321         local ok, nfct = pcall(io.lines, "/proc/net/nf_conntrack")
322         if not ok or not nfct then
323                 return nil
324         end
325
326         local line, connt = nil, (not callback) and { }
327         for line in nfct do
328                 local fam, l3, l4, timeout, state, tuples =
329                         line:match("^(ipv[46]) +(%d+) +%S+ +(%d+) +(%d+) +([A-Z_]+) +(.+)$")
330
331                 if fam and l3 and l4 and timeout and state and tuples and
332                    state ~= "TIME_WAIT"
333                 then
334                         l4 = nixio.getprotobynumber(l4)
335
336                         local entry = {
337                                 bytes = 0,
338                                 packets = 0,
339                                 layer3 = fam,
340                                 layer4 = l4 and l4.name or "unknown",
341                                 timeout = tonumber(timeout, 10)
342                         }
343
344                         local key, val
345                         for key, val in tuples:gmatch("(%w+)=(%S+)") do
346                                 if key == "bytes" or key == "packets" then
347                                         entry[key] = entry[key] + tonumber(val, 10)
348                                 elseif key == "src" or key == "dst" or key == "sport" or key == "dport" then
349                                         if entry[key] == nil then
350                                                 entry[key] = val
351                                         end
352                                 elseif val then
353                                         entry[key] = val
354                                 end
355                         end
356
357                         if callback then
358                                 callback(entry)
359                         else
360                                 connt[#connt+1] = entry
361                         end
362                 end
363         end
364
365         return callback and true or connt
366 end
367
368 function net.devices()
369         local devs = {}
370         for k, v in ipairs(nixio.getifaddrs()) do
371                 if v.family == "packet" then
372                         devs[#devs+1] = v.name
373                 end
374         end
375         return devs
376 end
377
378
379 function net.deviceinfo()
380         local devs = {}
381         for k, v in ipairs(nixio.getifaddrs()) do
382                 if v.family == "packet" then
383                         local d = v.data
384                         d[1] = d.rx_bytes
385                         d[2] = d.rx_packets
386                         d[3] = d.rx_errors
387                         d[4] = d.rx_dropped
388                         d[5] = 0
389                         d[6] = 0
390                         d[7] = 0
391                         d[8] = d.multicast
392                         d[9] = d.tx_bytes
393                         d[10] = d.tx_packets
394                         d[11] = d.tx_errors
395                         d[12] = d.tx_dropped
396                         d[13] = 0
397                         d[14] = d.collisions
398                         d[15] = 0
399                         d[16] = 0
400                         devs[v.name] = d
401                 end
402         end
403         return devs
404 end
405
406
407 --                      The following fields are defined for route entry tables:
408 --                      { "dest", "gateway", "metric", "refcount", "usecount", "irtt",
409 --                        "flags", "device" }
410 function net.routes(callback)
411         local routes = { }
412
413         for line in io.lines("/proc/net/route") do
414
415                 local dev, dst_ip, gateway, flags, refcnt, usecnt, metric,
416                           dst_mask, mtu, win, irtt = line:match(
417                         "([^%s]+)\t([A-F0-9]+)\t([A-F0-9]+)\t([A-F0-9]+)\t" ..
418                         "(%d+)\t(%d+)\t(%d+)\t([A-F0-9]+)\t(%d+)\t(%d+)\t(%d+)"
419                 )
420
421                 if dev then
422                         gateway  = luci.ip.Hex( gateway,  32, luci.ip.FAMILY_INET4 )
423                         dst_mask = luci.ip.Hex( dst_mask, 32, luci.ip.FAMILY_INET4 )
424                         dst_ip   = luci.ip.Hex(
425                                 dst_ip, dst_mask:prefix(dst_mask), luci.ip.FAMILY_INET4
426                         )
427
428                         local rt = {
429                                 dest     = dst_ip,
430                                 gateway  = gateway,
431                                 metric   = tonumber(metric),
432                                 refcount = tonumber(refcnt),
433                                 usecount = tonumber(usecnt),
434                                 mtu      = tonumber(mtu),
435                                 window   = tonumber(window),
436                                 irtt     = tonumber(irtt),
437                                 flags    = tonumber(flags, 16),
438                                 device   = dev
439                         }
440
441                         if callback then
442                                 callback(rt)
443                         else
444                                 routes[#routes+1] = rt
445                         end
446                 end
447         end
448
449         return routes
450 end
451
452 --                      The following fields are defined for route entry tables:
453 --                      { "source", "dest", "nexthop", "metric", "refcount", "usecount",
454 --                        "flags", "device" }
455 function net.routes6(callback)
456         if fs.access("/proc/net/ipv6_route", "r") then
457                 local routes = { }
458
459                 for line in io.lines("/proc/net/ipv6_route") do
460
461                         local dst_ip, dst_prefix, src_ip, src_prefix, nexthop,
462                                   metric, refcnt, usecnt, flags, dev = line:match(
463                                 "([a-f0-9]+) ([a-f0-9]+) " ..
464                                 "([a-f0-9]+) ([a-f0-9]+) " ..
465                                 "([a-f0-9]+) ([a-f0-9]+) " ..
466                                 "([a-f0-9]+) ([a-f0-9]+) " ..
467                                 "([a-f0-9]+) +([^%s]+)"
468                         )
469
470                         if dst_ip and dst_prefix and
471                            src_ip and src_prefix and
472                            nexthop and metric and
473                            refcnt and usecnt and
474                            flags and dev
475                         then
476                                 src_ip = luci.ip.Hex(
477                                         src_ip, tonumber(src_prefix, 16), luci.ip.FAMILY_INET6, false
478                                 )
479
480                                 dst_ip = luci.ip.Hex(
481                                         dst_ip, tonumber(dst_prefix, 16), luci.ip.FAMILY_INET6, false
482                                 )
483
484                                 nexthop = luci.ip.Hex( nexthop, 128, luci.ip.FAMILY_INET6, false )
485
486                                 local rt = {
487                                         source   = src_ip,
488                                         dest     = dst_ip,
489                                         nexthop  = nexthop,
490                                         metric   = tonumber(metric, 16),
491                                         refcount = tonumber(refcnt, 16),
492                                         usecount = tonumber(usecnt, 16),
493                                         flags    = tonumber(flags, 16),
494                                         device   = dev,
495
496                                         -- lua number is too small for storing the metric
497                                         -- add a metric_raw field with the original content
498                                         metric_raw = metric
499                                 }
500
501                                 if callback then
502                                         callback(rt)
503                                 else
504                                         routes[#routes+1] = rt
505                                 end
506                         end
507                 end
508
509                 return routes
510         end
511 end
512
513 function net.pingtest(host)
514         return os.execute("ping -c1 '"..host:gsub("'", '').."' >/dev/null 2>&1")
515 end
516
517
518 process = {}
519
520 function process.info(key)
521         local s = {uid = nixio.getuid(), gid = nixio.getgid()}
522         return not key and s or s[key]
523 end
524
525 function process.list()
526         local data = {}
527         local k
528         local ps = luci.util.execi("/bin/busybox top -bn1")
529
530         if not ps then
531                 return
532         end
533
534         for line in ps do
535                 local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match(
536                         "^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][W ][<N ]) +(%d+) +(%d+%%) +(%d+%%) +(.+)"
537                 )
538
539                 local idx = tonumber(pid)
540                 if idx then
541                         data[idx] = {
542                                 ['PID']     = pid,
543                                 ['PPID']    = ppid,
544                                 ['USER']    = user,
545                                 ['STAT']    = stat,
546                                 ['VSZ']     = vsz,
547                                 ['%MEM']    = mem,
548                                 ['%CPU']    = cpu,
549                                 ['COMMAND'] = cmd
550                         }
551                 end
552         end
553
554         return data
555 end
556
557 function process.setgroup(gid)
558         return nixio.setgid(gid)
559 end
560
561 function process.setuser(uid)
562         return nixio.setuid(uid)
563 end
564
565 process.signal = nixio.kill
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         if password then
593                 password = password:gsub("'", [['"'"']])
594         end
595
596         if username then
597                 username = username:gsub("'", [['"'"']])
598         end
599
600         return os.execute(
601                 "(echo '" .. password .. "'; sleep 1; echo '" .. password .. "') | " ..
602                 "passwd '" .. username .. "' >/dev/null 2>&1"
603         )
604 end
605
606
607 wifi = {}
608
609 function wifi.getiwinfo(ifname)
610         local stat, iwinfo = pcall(require, "iwinfo")
611
612         if ifname then
613                 local d, n = ifname:match("^(%w+)%.network(%d+)")
614                 local wstate = luci.util.ubus("network.wireless", "status") or { }
615
616                 d = d or ifname
617                 n = n and tonumber(n) or 1
618
619                 if type(wstate[d]) == "table" and
620                    type(wstate[d].interfaces) == "table" and
621                    type(wstate[d].interfaces[n]) == "table" and
622                    type(wstate[d].interfaces[n].ifname) == "string"
623                 then
624                         ifname = wstate[d].interfaces[n].ifname
625                 else
626                         ifname = d
627                 end
628
629                 local t = stat and iwinfo.type(ifname)
630                 local x = t and iwinfo[t] or { }
631                 return setmetatable({}, {
632                         __index = function(t, k)
633                                 if k == "ifname" then
634                                         return ifname
635                                 elseif x[k] then
636                                         return x[k](ifname)
637                                 end
638                         end
639                 })
640         end
641 end
642
643
644 init = {}
645 init.dir = "/etc/init.d/"
646
647 function init.names()
648         local names = { }
649         for name in fs.glob(init.dir.."*") do
650                 names[#names+1] = fs.basename(name)
651         end
652         return names
653 end
654
655 function init.index(name)
656         if fs.access(init.dir..name) then
657                 return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null"
658                         %{ init.dir, name })
659         end
660 end
661
662 local function init_action(action, name)
663         if fs.access(init.dir..name) then
664                 return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action })
665         end
666 end
667
668 function init.enabled(name)
669         return (init_action("enabled", name) == 0)
670 end
671
672 function init.enable(name)
673         return (init_action("enable", name) == 1)
674 end
675
676 function init.disable(name)
677         return (init_action("disable", name) == 0)
678 end
679
680 function init.start(name)
681         return (init_action("start", name) == 0)
682 end
683
684 function init.stop(name)
685         return (init_action("stop", name) == 0)
686 end