luci-base: sys.lua: lazy-load luci.model.network
[oweals/luci.git] / modules / luci-base / luasrc / sys.lua
1 -- Copyright 2008 Steven Barth <steven@midlink.org>
2 -- Licensed to the public under the Apache License 2.0.
3
4 local io     = require "io"
5 local os     = require "os"
6 local table  = require "table"
7 local nixio  = require "nixio"
8 local fs     = require "nixio.fs"
9 local uci    = require "luci.model.uci"
10
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, unpack =
16         tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select, unpack
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 -- containing the whole environment is returned otherwise this function returns
28 -- the corresponding string value for the given name or nil if no such variable
29 -- exists.
30 getenv = nixio.getenv
31
32 function hostname(newname)
33         if type(newname) == "string" and #newname > 0 then
34                 fs.writefile( "/proc/sys/kernel/hostname", newname )
35                 return newname
36         else
37                 return nixio.uname().nodename
38         end
39 end
40
41 function httpget(url, stream, target)
42         if not target then
43                 local source = stream and io.popen or luci.util.exec
44                 return source("wget -qO- %s" % luci.util.shellquote(url))
45         else
46                 return os.execute("wget -qO %s %s" %
47                         {luci.util.shellquote(target), luci.util.shellquote(url)})
48         end
49 end
50
51 function reboot()
52         return os.execute("reboot >/dev/null 2>&1")
53 end
54
55 function syslog()
56         return luci.util.exec("logread")
57 end
58
59 function dmesg()
60         return luci.util.exec("dmesg")
61 end
62
63 function uniqueid(bytes)
64         local rand = fs.readfile("/dev/urandom", bytes)
65         return rand and nixio.bin.hexlify(rand)
66 end
67
68 function uptime()
69         return nixio.sysinfo().uptime
70 end
71
72
73 net = {}
74
75 local function _nethints(what, callback)
76         local _, k, e, mac, ip, name, duid, iaid
77         local cur = uci.cursor()
78         local ifn = { }
79         local hosts = { }
80         local lookup = { }
81
82         local function _add(i, ...)
83                 local k = select(i, ...)
84                 if k then
85                         if not hosts[k] then hosts[k] = { } end
86                         hosts[k][1] = select(1, ...) or hosts[k][1]
87                         hosts[k][2] = select(2, ...) or hosts[k][2]
88                         hosts[k][3] = select(3, ...) or hosts[k][3]
89                         hosts[k][4] = select(4, ...) or hosts[k][4]
90                 end
91         end
92
93         luci.ip.neighbors(nil, function(neigh)
94                 if neigh.mac and neigh.family == 4 then
95                         _add(what, neigh.mac:string(), neigh.dest:string(), nil, nil)
96                 elseif neigh.mac and neigh.family == 6 then
97                         _add(what, neigh.mac:string(), nil, neigh.dest:string(), nil)
98                 end
99         end)
100
101         if fs.access("/etc/ethers") then
102                 for e in io.lines("/etc/ethers") do
103                         mac, name = e:match("^([a-fA-F0-9:-]+)%s+(%S+)")
104                         mac = luci.ip.checkmac(mac)
105                         if mac and name then
106                                 if luci.ip.checkip4(name) then
107                                         _add(what, mac, name, nil, nil)
108                                 else
109                                         _add(what, mac, nil, nil, name)
110                                 end
111                         end
112                 end
113         end
114
115         cur:foreach("dhcp", "dnsmasq",
116                 function(s)
117                         if s.leasefile and fs.access(s.leasefile) then
118                                 for e in io.lines(s.leasefile) do
119                                         mac, ip, name = e:match("^%d+ (%S+) (%S+) (%S+)")
120                                         mac = luci.ip.checkmac(mac)
121                                         if mac and ip then
122                                                 _add(what, mac, ip, nil, name ~= "*" and name)
123                                         end
124                                 end
125                         end
126                 end
127         )
128
129         cur:foreach("dhcp", "odhcpd",
130                 function(s)
131                         if type(s.leasefile) == "string" and fs.access(s.leasefile) then
132                                 for e in io.lines(s.leasefile) do
133                                         duid, iaid, name, _, ip = e:match("^# %S+ (%S+) (%S+) (%S+) (-?%d+) %S+ %S+ ([0-9a-f:.]+)/[0-9]+")
134                                         mac = net.duid_to_mac(duid)
135                                         if mac then
136                                                 if ip and iaid == "ipv4" then
137                                                         _add(what, mac, ip, nil, name ~= "*" and name)
138                                                 elseif ip then
139                                                         _add(what, mac, nil, ip, name ~= "*" and name)
140                                                 end
141                                         end
142                                 end
143                         end
144                 end
145         )
146
147         cur:foreach("dhcp", "host",
148                 function(s)
149                         for mac in luci.util.imatch(s.mac) do
150                                 mac = luci.ip.checkmac(mac)
151                                 if mac then
152                                         _add(what, mac, s.ip, nil, s.name)
153                                 end
154                         end
155                 end)
156
157         for _, e in ipairs(nixio.getifaddrs()) do
158                 if e.name ~= "lo" then
159                         ifn[e.name] = ifn[e.name] or { }
160                         if e.family == "packet" and e.addr and #e.addr == 17 then
161                                 ifn[e.name][1] = e.addr:upper()
162                         elseif e.family == "inet" then
163                                 ifn[e.name][2] = e.addr
164                         elseif e.family == "inet6" then
165                                 ifn[e.name][3] = e.addr
166                         end
167                 end
168         end
169
170         for _, e in pairs(ifn) do
171                 if e[what] and (e[2] or e[3]) then
172                         _add(what, e[1], e[2], e[3], e[4])
173                 end
174         end
175
176         for _, e in pairs(hosts) do
177                 lookup[#lookup+1] = (what > 1) and e[what] or (e[2] or e[3])
178         end
179
180         if #lookup > 0 then
181                 lookup = luci.util.ubus("network.rrdns", "lookup", {
182                         addrs   = lookup,
183                         timeout = 250,
184                         limit   = 1000
185                 }) or { }
186         end
187
188         for _, e in luci.util.kspairs(hosts) do
189                 callback(e[1], e[2], e[3], lookup[e[2]] or lookup[e[3]] or e[4])
190         end
191 end
192
193 --          Each entry contains the values in the following order:
194 --          [ "mac", "name" ]
195 function net.mac_hints(callback)
196         if callback then
197                 _nethints(1, function(mac, v4, v6, name)
198                         name = name or v4
199                         if name and name ~= mac then
200                                 callback(mac, name or v4)
201                         end
202                 end)
203         else
204                 local rv = { }
205                 _nethints(1, function(mac, v4, v6, name)
206                         name = name or v4
207                         if name and name ~= mac then
208                                 rv[#rv+1] = { mac, name or v4 }
209                         end
210                 end)
211                 return rv
212         end
213 end
214
215 --          Each entry contains the values in the following order:
216 --          [ "ip", "name" ]
217 function net.ipv4_hints(callback)
218         if callback then
219                 _nethints(2, function(mac, v4, v6, name)
220                         name = name or mac
221                         if name and name ~= v4 then
222                                 callback(v4, name)
223                         end
224                 end)
225         else
226                 local rv = { }
227                 _nethints(2, function(mac, v4, v6, name)
228                         name = name or mac
229                         if name and name ~= v4 then
230                                 rv[#rv+1] = { v4, name }
231                         end
232                 end)
233                 return rv
234         end
235 end
236
237 --          Each entry contains the values in the following order:
238 --          [ "ip", "name" ]
239 function net.ipv6_hints(callback)
240         if callback then
241                 _nethints(3, function(mac, v4, v6, name)
242                         name = name or mac
243                         if name and name ~= v6 then
244                                 callback(v6, name)
245                         end
246                 end)
247         else
248                 local rv = { }
249                 _nethints(3, function(mac, v4, v6, name)
250                         name = name or mac
251                         if name and name ~= v6 then
252                                 rv[#rv+1] = { v6, name }
253                         end
254                 end)
255                 return rv
256         end
257 end
258
259 function net.host_hints(callback)
260         if callback then
261                 _nethints(1, function(mac, v4, v6, name)
262                         if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then
263                                 callback(mac, v4, v6, name)
264                         end
265                 end)
266         else
267                 local rv = { }
268                 _nethints(1, function(mac, v4, v6, name)
269                         if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then
270                                 local e = { }
271                                 if v4   then e.ipv4 = v4   end
272                                 if v6   then e.ipv6 = v6   end
273                                 if name then e.name = name end
274                                 rv[mac] = e
275                         end
276                 end)
277                 return rv
278         end
279 end
280
281 function net.conntrack(callback)
282         local ok, nfct = pcall(io.lines, "/proc/net/nf_conntrack")
283         if not ok or not nfct then
284                 return nil
285         end
286
287         local line, connt = nil, (not callback) and { }
288         for line in nfct do
289                 local fam, l3, l4, timeout, tuples =
290                         line:match("^(ipv[46]) +(%d+) +%S+ +(%d+) +(%d+) +(.+)$")
291
292                 if fam and l3 and l4 and timeout and not tuples:match("^TIME_WAIT ") then
293                         l4 = nixio.getprotobynumber(l4)
294
295                         local entry = {
296                                 bytes = 0,
297                                 packets = 0,
298                                 layer3 = fam,
299                                 layer4 = l4 and l4.name or "unknown",
300                                 timeout = tonumber(timeout, 10)
301                         }
302
303                         local key, val
304                         for key, val in tuples:gmatch("(%w+)=(%S+)") do
305                                 if key == "bytes" or key == "packets" then
306                                         entry[key] = entry[key] + tonumber(val, 10)
307                                 elseif key == "src" or key == "dst" then
308                                         if entry[key] == nil then
309                                                 entry[key] = luci.ip.new(val):string()
310                                         end
311                                 elseif key == "sport" or key == "dport" then
312                                         if entry[key] == nil then
313                                                 entry[key] = val
314                                         end
315                                 elseif val then
316                                         entry[key] = val
317                                 end
318                         end
319
320                         if callback then
321                                 callback(entry)
322                         else
323                                 connt[#connt+1] = entry
324                         end
325                 end
326         end
327
328         return callback and true or connt
329 end
330
331 function net.devices()
332         local devs = {}
333         local seen = {}
334         for k, v in ipairs(nixio.getifaddrs()) do
335                 if v.name and not seen[v.name] then
336                         seen[v.name] = true
337                         devs[#devs+1] = v.name
338                 end
339         end
340         return devs
341 end
342
343 function net.duid_to_mac(duid)
344         local b1, b2, b3, b4, b5, b6
345
346         if type(duid) == "string" then
347                 -- DUID-LLT / Ethernet
348                 if #duid == 28 then
349                         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$")
350
351                 -- DUID-LL / Ethernet
352                 elseif #duid == 20 then
353                         b1, b2, b3, b4, b5, b6 = duid:match("^00030001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$")
354
355                 -- DUID-LL / Ethernet (Without Header)
356                 elseif #duid == 12 then
357                         b1, b2, b3, b4, b5, b6 = duid:match("^(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$")
358                 end
359         end
360
361         return b1 and luci.ip.checkmac(table.concat({ b1, b2, b3, b4, b5, b6 }, ":"))
362 end
363
364 process = {}
365
366 function process.info(key)
367         local s = {uid = nixio.getuid(), gid = nixio.getgid()}
368         return not key and s or s[key]
369 end
370
371 function process.list()
372         local data = {}
373         local k
374         local ps = luci.util.execi("/bin/busybox top -bn1")
375
376         if not ps then
377                 return
378         end
379
380         for line in ps do
381                 local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match(
382                         "^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][<NW ][<N ]) +(%d+) +(%d+%%) +(%d+%%) +(.+)"
383                 )
384
385                 local idx = tonumber(pid)
386                 if idx and not cmd:match("top %-bn1") then
387                         data[idx] = {
388                                 ['PID']     = pid,
389                                 ['PPID']    = ppid,
390                                 ['USER']    = user,
391                                 ['STAT']    = stat,
392                                 ['VSZ']     = vsz,
393                                 ['%MEM']    = mem,
394                                 ['%CPU']    = cpu,
395                                 ['COMMAND'] = cmd
396                         }
397                 end
398         end
399
400         return data
401 end
402
403 function process.setgroup(gid)
404         return nixio.setgid(gid)
405 end
406
407 function process.setuser(uid)
408         return nixio.setuid(uid)
409 end
410
411 process.signal = nixio.kill
412
413 local function xclose(fd)
414         if fd and fd:fileno() > 2 then
415                 fd:close()
416         end
417 end
418
419 function process.exec(command, stdout, stderr, nowait)
420         local out_r, out_w, err_r, err_w
421         if stdout then out_r, out_w = nixio.pipe() end
422         if stderr then err_r, err_w = nixio.pipe() end
423
424         local pid = nixio.fork()
425         if pid == 0 then
426                 nixio.chdir("/")
427
428                 local null = nixio.open("/dev/null", "w+")
429                 if null then
430                         nixio.dup(out_w or null, nixio.stdout)
431                         nixio.dup(err_w or null, nixio.stderr)
432                         nixio.dup(null, nixio.stdin)
433                         xclose(out_w)
434                         xclose(out_r)
435                         xclose(err_w)
436                         xclose(err_r)
437                         xclose(null)
438                 end
439
440                 nixio.exec(unpack(command))
441                 os.exit(-1)
442         end
443
444         local _, pfds, rv = nil, {}, { code = -1, pid = pid }
445
446         xclose(out_w)
447         xclose(err_w)
448
449         if out_r then
450                 pfds[#pfds+1] = {
451                         fd = out_r,
452                         cb = type(stdout) == "function" and stdout,
453                         name = "stdout",
454                         events = nixio.poll_flags("in", "err", "hup")
455                 }
456         end
457
458         if err_r then
459                 pfds[#pfds+1] = {
460                         fd = err_r,
461                         cb = type(stderr) == "function" and stderr,
462                         name = "stderr",
463                         events = nixio.poll_flags("in", "err", "hup")
464                 }
465         end
466
467         while #pfds > 0 do
468                 local nfds, err = nixio.poll(pfds, -1)
469                 if not nfds and err ~= nixio.const.EINTR then
470                         break
471                 end
472
473                 local i
474                 for i = #pfds, 1, -1 do
475                         local rfd = pfds[i]
476                         if rfd.revents > 0 then
477                                 local chunk, err = rfd.fd:read(4096)
478                                 if chunk and #chunk > 0 then
479                                         if rfd.cb then
480                                                 rfd.cb(chunk)
481                                         else
482                                                 rfd.buf = rfd.buf or {}
483                                                 rfd.buf[#rfd.buf + 1] = chunk
484                                         end
485                                 else
486                                         table.remove(pfds, i)
487                                         if rfd.buf then
488                                                 rv[rfd.name] = table.concat(rfd.buf, "")
489                                         end
490                                         rfd.fd:close()
491                                 end
492                         end
493                 end
494         end
495
496         if not nowait then
497                 _, _, rv.code = nixio.waitpid(pid)
498         end
499
500         return rv
501 end
502
503
504 user = {}
505
506 --                              { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" }
507 user.getuser = nixio.getpw
508
509 function user.getpasswd(username)
510         local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username)
511         local pwh = pwe and (pwe.pwdp or pwe.passwd)
512         if not pwh or #pwh < 1 or pwh == "!" or pwh == "x" then
513                 return nil, pwe
514         else
515                 return pwh, pwe
516         end
517 end
518
519 function user.checkpasswd(username, pass)
520         local pwh, pwe = user.getpasswd(username)
521         if pwe then
522                 return (pwh == nil or nixio.crypt(pass, pwh) == pwh)
523         end
524         return false
525 end
526
527 function user.setpasswd(username, password)
528         return os.execute("(echo %s; sleep 1; echo %s) | passwd %s >/dev/null 2>&1" %{
529                 luci.util.shellquote(password),
530                 luci.util.shellquote(password),
531                 luci.util.shellquote(username)
532         })
533 end
534
535
536 wifi = {}
537
538 function wifi.getiwinfo(ifname)
539         local ntm = require "luci.model.network"
540
541         ntm.init()
542
543         local wnet = ntm:get_wifinet(ifname)
544         if wnet and wnet.iwinfo then
545                 return wnet.iwinfo
546         end
547
548         local wdev = ntm:get_wifidev(ifname)
549         if wdev and wdev.iwinfo then
550                 return wdev.iwinfo
551         end
552
553         return { ifname = ifname }
554 end
555
556
557 init = {}
558 init.dir = "/etc/init.d/"
559
560 function init.names()
561         local names = { }
562         for name in fs.glob(init.dir.."*") do
563                 names[#names+1] = fs.basename(name)
564         end
565         return names
566 end
567
568 function init.index(name)
569         if fs.access(init.dir..name) then
570                 return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null"
571                         %{ init.dir, name })
572         end
573 end
574
575 local function init_action(action, name)
576         if fs.access(init.dir..name) then
577                 return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action })
578         end
579 end
580
581 function init.enabled(name)
582         return (init_action("enabled", name) == 0)
583 end
584
585 function init.enable(name)
586         return (init_action("enable", name) == 0)
587 end
588
589 function init.disable(name)
590         return (init_action("disable", name) == 0)
591 end
592
593 function init.start(name)
594         return (init_action("start", name) == 0)
595 end
596
597 function init.stop(name)
598         return (init_action("stop", name) == 0)
599 end
600
601 function init.restart(name)
602         return (init_action("restart", name) == 0)
603 end
604
605 function init.reload(name)
606         return (init_action("reload", name) == 0)
607 end