luci-base: migrate luci/getWirelessDevices to C plugin
[project/luci.git] / modules / luci-base / root / usr / libexec / rpcd / luci
1 #!/usr/bin/env lua
2
3 local json = require "luci.jsonc"
4 local fs = require "nixio.fs"
5
6 local function readfile(path)
7 local s = fs.readfile(path)
8 return s and (s:gsub("^%s+", ""):gsub("%s+$", ""))
9 end
10
11 local methods = {
12 getInitList = {
13 args = { name = "name" },
14 call = function(args)
15 local sys = require "luci.sys"
16 local _, name, scripts = nil, nil, {}
17 for _, name in ipairs(args.name and { args.name } or sys.init.names()) do
18 local index = sys.init.index(name)
19 if index then
20 scripts[name] = { index = index, enabled = sys.init.enabled(name) }
21 else
22 return { error = "No such init script" }
23 end
24 end
25 return scripts
26 end
27 },
28
29 setInitAction = {
30 args = { name = "name", action = "action" },
31 call = function(args)
32 local sys = require "luci.sys"
33 if type(sys.init[args.action]) ~= "function" then
34 return { error = "Invalid action" }
35 end
36 return { result = sys.init[args.action](args.name) }
37 end
38 },
39
40 getLocaltime = {
41 call = function(args)
42 return { result = os.time() }
43 end
44 },
45
46 setLocaltime = {
47 args = { localtime = 0 },
48 call = function(args)
49 local sys = require "luci.sys"
50 local date = os.date("*t", args.localtime)
51 if date then
52 sys.call("date -s '%04d-%02d-%02d %02d:%02d:%02d' >/dev/null" %{ date.year, date.month, date.day, date.hour, date.min, date.sec })
53 sys.call("/etc/init.d/sysfixtime restart >/dev/null")
54 end
55 return { result = args.localtime }
56 end
57 },
58
59 getTimezones = {
60 call = function(args)
61 local util = require "luci.util"
62 local zones = require "luci.sys.zoneinfo"
63
64 local tz = readfile("/etc/TZ")
65 local res = util.ubus("uci", "get", {
66 config = "system",
67 section = "@system[0]",
68 option = "zonename"
69 })
70
71 local result = {}
72 local _, zone
73 for _, zone in ipairs(zones.TZ) do
74 result[zone[1]] = {
75 tzstring = zone[2],
76 active = (res and res.value == zone[1]) and true or nil
77 }
78 end
79 return result
80 end
81 },
82
83 getLEDs = {
84 call = function()
85 local iter = fs.dir("/sys/class/leds")
86 local result = { }
87
88 if iter then
89 local led
90 for led in iter do
91 local m, s
92
93 result[led] = { triggers = {} }
94
95 s = readfile("/sys/class/leds/"..led.."/trigger")
96 for s in (s or ""):gmatch("%S+") do
97 m = s:match("^%[(.+)%]$")
98 result[led].triggers[#result[led].triggers+1] = m or s
99 result[led].active_trigger = m or result[led].active_trigger
100 end
101
102 s = readfile("/sys/class/leds/"..led.."/brightness")
103 if s then
104 result[led].brightness = tonumber(s)
105 end
106
107 s = readfile("/sys/class/leds/"..led.."/max_brightness")
108 if s then
109 result[led].max_brightness = tonumber(s)
110 end
111 end
112 end
113
114 return result
115 end
116 },
117
118 getUSBDevices = {
119 call = function()
120 local fs = require "nixio.fs"
121 local iter = fs.glob("/sys/bus/usb/devices/[0-9]*/manufacturer")
122 local result = { }
123
124 if iter then
125 result.devices = {}
126
127 local p
128 for p in iter do
129 local id = p:match("/([^/]+)/manufacturer$")
130
131 result.devices[#result.devices+1] = {
132 id = id,
133 vid = readfile("/sys/bus/usb/devices/"..id.."/idVendor"),
134 pid = readfile("/sys/bus/usb/devices/"..id.."/idProduct"),
135 vendor = readfile("/sys/bus/usb/devices/"..id.."/manufacturer"),
136 product = readfile("/sys/bus/usb/devices/"..id.."/product"),
137 speed = tonumber((readfile("/sys/bus/usb/devices/"..id.."/product")))
138 }
139 end
140 end
141
142 iter = fs.glob("/sys/bus/usb/devices/*/*-port[0-9]*")
143
144 if iter then
145 result.ports = {}
146
147 local p
148 for p in iter do
149 local port = p:match("([^/]+)$")
150 local link = fs.readlink(p.."/device")
151
152 result.ports[#result.ports+1] = {
153 port = port,
154 device = link and fs.basename(link)
155 }
156 end
157 end
158
159 return result
160 end
161 },
162
163 getIfaddrs = {
164 call = function()
165 return { result = nixio.getifaddrs() }
166 end
167 },
168
169 getDUIDHints = {
170 call = function()
171 local fp = io.open('/var/hosts/odhcpd')
172 local result = { }
173 if fp then
174 for line in fp:lines() do
175 local dev, duid, name = string.match(line, '# (%S+)%s+(%S+)%s+%d+%s+(%S+)')
176 if dev and duid and name then
177 result[duid] = {
178 name = (name ~= "-") and name or nil,
179 device = dev
180 }
181 end
182 end
183 fp:close()
184 end
185 return result
186 end
187 },
188
189 getNetworkDevices = {
190 call = function(args)
191 local dir = fs.dir("/sys/class/net")
192 local result = { }
193 if dir then
194 local dev
195 for dev in dir do
196 if not result[dev] then
197 result[dev] = { name = dev }
198 end
199
200 if fs.access("/sys/class/net/"..dev.."/master") then
201 local brname = fs.basename(fs.readlink("/sys/class/net/"..dev.."/master"))
202 if not result[brname] then
203 result[brname] = { name = brname }
204 end
205
206 if not result[brname].ports then
207 result[brname].ports = { }
208 end
209
210 result[brname].ports[#result[brname].ports+1] = dev
211 elseif fs.access("/sys/class/net/"..dev.."/bridge") then
212 if not result[dev].ports then
213 result[dev].ports = { }
214 end
215
216 result[dev].id = readfile("/sys/class/net/"..dev.."/bridge/bridge_id")
217 result[dev].stp = (readfile("/sys/class/net/"..dev.."/bridge/stp_state") ~= "0")
218 result[dev].bridge = true
219 end
220
221 local opr = readfile("/sys/class/net/"..dev.."/operstate")
222
223 result[dev].up = (opr == "up" or opr == "unknown")
224 result[dev].type = tonumber(readfile("/sys/class/net/"..dev.."/type"))
225 result[dev].name = dev
226
227 local mtu = tonumber(readfile("/sys/class/net/"..dev.."/mtu"))
228 if mtu and mtu > 0 then
229 result[dev].mtu = mtu
230 end
231
232 local qlen = tonumber(readfile("/sys/class/net/"..dev.."/tx_queue_len"))
233 if qlen and qlen > 0 then
234 result[dev].qlen = qlen
235 end
236
237 local master = fs.readlink("/sys/class/net/"..dev.."/master")
238 if master then
239 result[dev].master = fs.basename(master)
240 end
241
242 local mac = readfile("/sys/class/net/"..dev.."/address")
243 if mac and #mac == 17 then
244 result[dev].mac = mac
245 end
246 end
247 end
248 return result
249 end
250 },
251
252 getConntrackHelpers = {
253 call = function()
254 local ok, fd = pcall(io.open, "/usr/share/fw3/helpers.conf", "r")
255 local rv = {}
256
257 if ok then
258 local entry
259
260 while true do
261 local line = fd:read("*l")
262 if not line then
263 break
264 end
265
266 if line:match("^%s*config%s") then
267 if entry then
268 rv[#rv+1] = entry
269 end
270 entry = {}
271 else
272 local opt, val = line:match("^%s*option%s+(%S+)%s+(%S.*)$")
273 if opt and val then
274 opt = opt:gsub("^'(.+)'$", "%1"):gsub('^"(.+)"$', "%1")
275 val = val:gsub("^'(.+)'$", "%1"):gsub('^"(.+)"$', "%1")
276 entry[opt] = val
277 end
278 end
279 end
280
281 if entry then
282 rv[#rv+1] = entry
283 end
284
285 fd:close()
286 end
287
288 return { result = rv }
289 end
290 },
291
292 getFeatures = {
293 call = function()
294 local fs = require "nixio.fs"
295 local rv = {}
296 local ok, fd
297
298 rv.firewall = fs.access("/sbin/fw3")
299 rv.opkg = fs.access("/bin/opkg")
300 rv.offloading = fs.access("/sys/module/xt_FLOWOFFLOAD/refcnt")
301 rv.br2684ctl = fs.access("/usr/sbin/br2684ctl")
302 rv.swconfig = fs.access("/sbin/swconfig")
303 rv.odhcpd = fs.access("/usr/sbin/odhcpd")
304 rv.zram = fs.access("/sys/class/zram-control")
305 rv.sysntpd = fs.readlink("/usr/sbin/ntpd") and true
306 rv.ipv6 = fs.access("/proc/net/ipv6_route")
307 rv.dropbear = fs.access("/usr/sbin/dropbear")
308
309 local wifi_features = { "eap", "11n", "11ac", "11r", "11w", "acs", "sae", "owe", "suiteb192" }
310
311 if fs.access("/usr/sbin/hostapd") then
312 rv.hostapd = { cli = fs.access("/usr/sbin/hostapd_cli") }
313
314 local _, feature
315 for _, feature in ipairs(wifi_features) do
316 rv.hostapd[feature] =
317 (os.execute(string.format("/usr/sbin/hostapd -v%s >/dev/null 2>/dev/null", feature)) == 0)
318 end
319 end
320
321 if fs.access("/usr/sbin/wpa_supplicant") then
322 rv.wpasupplicant = { cli = fs.access("/usr/sbin/wpa_cli") }
323
324 local _, feature
325 for _, feature in ipairs(wifi_features) do
326 rv.wpasupplicant[feature] =
327 (os.execute(string.format("/usr/sbin/wpa_supplicant -v%s >/dev/null 2>/dev/null", feature)) == 0)
328 end
329 end
330
331 ok, fd = pcall(io.popen, "dnsmasq --version 2>/dev/null")
332 if ok then
333 rv.dnsmasq = {}
334
335 while true do
336 local line = fd:read("*l")
337 if not line then
338 break
339 end
340
341 local opts = line:match("^Compile time options: (.+)$")
342 if opts then
343 local opt
344 for opt in opts:gmatch("%S+") do
345 local no = opt:match("^no%-(%S+)$")
346 rv.dnsmasq[string.lower(no or opt)] = not no
347 end
348 break
349 end
350 end
351
352 fd:close()
353 end
354
355 ok, fd = pcall(io.popen, "ipset --help 2>/dev/null")
356 if ok then
357 rv.ipset = {}
358
359 local sets = false
360
361 while true do
362 local line = fd:read("*l")
363 if not line then
364 break
365 elseif line:match("^Supported set types:") then
366 sets = true
367 elseif sets then
368 local set, ver = line:match("^%s+(%S+)%s+(%d+)")
369 if set and not rv.ipset[set] then
370 rv.ipset[set] = tonumber(ver)
371 end
372 end
373 end
374
375 fd:close()
376 end
377
378 return rv
379 end
380 },
381
382 getSwconfigFeatures = {
383 args = { switch = "switch0" },
384 call = function(args)
385 local util = require "luci.util"
386
387 -- Parse some common switch properties from swconfig help output.
388 local swc, err = io.popen("swconfig dev %s help 2>/dev/null" % util.shellquote(args.switch))
389 if swc then
390 local is_port_attr = false
391 local is_vlan_attr = false
392 local rv = {}
393
394 while true do
395 local line = swc:read("*l")
396 if not line then break end
397
398 if line:match("^%s+%-%-vlan") then
399 is_vlan_attr = true
400
401 elseif line:match("^%s+%-%-port") then
402 is_vlan_attr = false
403 is_port_attr = true
404
405 elseif line:match("cpu @") then
406 rv.switch_title = line:match("^switch%d: %w+%((.-)%)")
407 rv.num_vlans = tonumber(line:match("vlans: (%d+)")) or 16
408 rv.min_vid = 1
409
410 elseif line:match(": pvid") or line:match(": tag") or line:match(": vid") then
411 if is_vlan_attr then rv.vid_option = line:match(": (%w+)") end
412
413 elseif line:match(": enable_vlan4k") then
414 rv.vlan4k_option = "enable_vlan4k"
415
416 elseif line:match(": enable_vlan") then
417 rv.vlan_option = "enable_vlan"
418
419 elseif line:match(": enable_learning") then
420 rv.learning_option = "enable_learning"
421
422 elseif line:match(": enable_mirror_rx") then
423 rv.mirror_option = "enable_mirror_rx"
424
425 elseif line:match(": max_length") then
426 rv.jumbo_option = "max_length"
427 end
428 end
429
430 swc:close()
431
432 if not next(rv) then
433 return { error = "No such switch" }
434 end
435
436 return rv
437 else
438 return { error = err }
439 end
440 end
441 },
442
443 getSwconfigPortState = {
444 args = { switch = "switch0" },
445 call = function(args)
446 local util = require "luci.util"
447
448 local swc, err = io.popen("swconfig dev %s show 2>/dev/null" % util.shellquote(args.switch))
449 if swc then
450 local ports = { }
451
452 while true do
453 local line = swc:read("*l")
454 if not line then break end
455
456 local port, up = line:match("port:(%d+) link:(%w+)")
457 if port then
458 local speed = line:match(" speed:(%d+)")
459 local duplex = line:match(" (%w+)-duplex")
460 local txflow = line:match(" (txflow)")
461 local rxflow = line:match(" (rxflow)")
462 local auto = line:match(" (auto)")
463
464 ports[#ports+1] = {
465 port = tonumber(port) or 0,
466 speed = tonumber(speed) or 0,
467 link = (up == "up"),
468 duplex = (duplex == "full"),
469 rxflow = (not not rxflow),
470 txflow = (not not txflow),
471 auto = (not not auto)
472 }
473 end
474 end
475
476 swc:close()
477
478 if not next(ports) then
479 return { error = "No such switch" }
480 end
481
482 return { result = ports }
483 else
484 return { error = err }
485 end
486 end
487 },
488
489 setPassword = {
490 args = { username = "root", password = "password" },
491 call = function(args)
492 local util = require "luci.util"
493 return {
494 result = (os.execute("(echo %s; sleep 1; echo %s) | passwd %s >/dev/null 2>&1" %{
495 luci.util.shellquote(args.password),
496 luci.util.shellquote(args.password),
497 luci.util.shellquote(args.username)
498 }) == 0)
499 }
500 end
501 },
502
503 getBlockDevices = {
504 call = function()
505 local fs = require "nixio.fs"
506
507 local block = io.popen("/sbin/block info", "r")
508 if block then
509 local rv = {}
510
511 while true do
512 local ln = block:read("*l")
513 if not ln then
514 break
515 end
516
517 local dev = ln:match("^/dev/(.-):")
518 if dev then
519 local s = tonumber((fs.readfile("/sys/class/block/" .. dev .."/size")))
520 local e = {
521 dev = "/dev/" .. dev,
522 size = s and s * 512
523 }
524
525 local key, val = { }
526 for key, val in ln:gmatch([[(%w+)="(.-)"]]) do
527 e[key:lower()] = val
528 end
529
530 rv[dev] = e
531 end
532 end
533
534 block:close()
535
536 return rv
537 else
538 return { error = "Unable to execute block utility" }
539 end
540 end
541 },
542
543 setBlockDetect = {
544 call = function()
545 return { result = (os.execute("/sbin/block detect > /etc/config/fstab") == 0) }
546 end
547 },
548
549 getMountPoints = {
550 call = function()
551 local fs = require "nixio.fs"
552
553 local fd, err = io.open("/proc/mounts", "r")
554 if fd then
555 local rv = {}
556
557 while true do
558 local ln = fd:read("*l")
559 if not ln then
560 break
561 end
562
563 local device, mount, fstype, options, freq, pass = ln:match("^(%S*) (%S*) (%S*) (%S*) (%d+) (%d+)$")
564 if device and mount then
565 device = device:gsub("\\(%d+)", function(n) return string.char(tonumber(n, 8)) end)
566 mount = mount:gsub("\\(%d+)", function(n) return string.char(tonumber(n, 8)) end)
567
568 local stat = fs.statvfs(mount)
569 if stat and stat.blocks > 0 then
570 rv[#rv+1] = {
571 device = device,
572 mount = mount,
573 size = stat.bsize * stat.blocks,
574 avail = stat.bsize * stat.bavail,
575 free = stat.bsize * stat.bfree
576 }
577 end
578 end
579 end
580
581 fd:close()
582
583 return { result = rv }
584 else
585 return { error = err }
586 end
587 end
588 },
589
590 setUmount = {
591 args = { path = "/mnt" },
592 call = function(args)
593 local util = require "luci.util"
594 return { result = (os.execute(string.format("/bin/umount %s", util.shellquote(args.path))) == 0) }
595 end
596 },
597
598 setReboot = {
599 call = function()
600 return { result = (os.execute("/sbin/reboot >/dev/null 2>&1") == 0) }
601 end
602 }
603 }
604
605 local function parseInput()
606 local parse = json.new()
607 local done, err
608
609 while true do
610 local chunk = io.read(4096)
611 if not chunk then
612 break
613 elseif not done and not err then
614 done, err = parse:parse(chunk)
615 end
616 end
617
618 if not done then
619 print(json.stringify({ error = err or "Incomplete input" }))
620 os.exit(1)
621 end
622
623 return parse:get()
624 end
625
626 local function validateArgs(func, uargs)
627 local method = methods[func]
628 if not method then
629 print(json.stringify({ error = "Method not found" }))
630 os.exit(1)
631 end
632
633 if type(uargs) ~= "table" then
634 print(json.stringify({ error = "Invalid arguments" }))
635 os.exit(1)
636 end
637
638 uargs.ubus_rpc_session = nil
639
640 local k, v
641 local margs = method.args or {}
642 for k, v in pairs(uargs) do
643 if margs[k] == nil or
644 (v ~= nil and type(v) ~= type(margs[k]))
645 then
646 print(json.stringify({ error = "Invalid arguments" }))
647 os.exit(1)
648 end
649 end
650
651 return method
652 end
653
654 if arg[1] == "list" then
655 local _, method, rv = nil, nil, {}
656 for _, method in pairs(methods) do rv[_] = method.args or {} end
657 print((json.stringify(rv):gsub(":%[%]", ":{}")))
658 elseif arg[1] == "call" then
659 local args = parseInput()
660 local method = validateArgs(arg[2], args)
661 local result, code = method.call(args)
662 print((json.stringify(result):gsub("^%[%]$", "{}")))
663 os.exit(code or 0)
664 end