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