Merge pull request #1735 from sumpfralle/olsr-jsoninfo-parser-handle-empty-result
[oweals/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         getHostHints = {
170                 call = function()
171                         local sys = require "luci.sys"
172                         return sys.net.host_hints()
173                 end
174         },
175
176         getDUIDHints = {
177                 call = function()
178                         local fp = io.open('/var/hosts/odhcpd')
179                         local result = { }
180                         if fp then
181                                 for line in fp:lines() do
182                                         local dev, duid, name = string.match(line, '# (%S+)%s+(%S+)%s+%d+%s+(%S+)')
183                                         if dev and duid and name then
184                                                 result[duid] = {
185                                                         name = (name ~= "-") and name or nil,
186                                                         device = dev
187                                                 }
188                                         end
189                                 end
190                                 fp:close()
191                         end
192                         return result
193                 end
194         },
195
196         getDHCPLeases = {
197                 args = { family = 0 },
198                 call = function(args)
199                         local s = require "luci.tools.status"
200
201                         if args.family == 4 then
202                                 return { dhcp_leases = s.dhcp_leases() }
203                         elseif args.family == 6 then
204                                 return { dhcp6_leases = s.dhcp6_leases() }
205                         else
206                                 return {
207                                         dhcp_leases = s.dhcp_leases(),
208                                         dhcp6_leases = s.dhcp6_leases()
209                                 }
210                         end
211                 end
212         },
213
214         getNetworkDevices = {
215                 call = function(args)
216                         local dir = fs.dir("/sys/class/net")
217                         local result = { }
218                         if dir then
219                                 local dev
220                                 for dev in dir do
221                                         if not result[dev] then
222                                                 result[dev] = { name = dev }
223                                         end
224
225                                         if fs.access("/sys/class/net/"..dev.."/master") then
226                                                 local brname = fs.basename(fs.readlink("/sys/class/net/"..dev.."/master"))
227                                                 if not result[brname] then
228                                                         result[brname] = { name = brname }
229                                                 end
230
231                                                 if not result[brname].ports then
232                                                         result[brname].ports = { }
233                                                 end
234
235                                                 result[brname].ports[#result[brname].ports+1] = dev
236                                         elseif fs.access("/sys/class/net/"..dev.."/bridge") then
237                                                 if not result[dev].ports then
238                                                         result[dev].ports = { }
239                                                 end
240
241                                                 result[dev].id = readfile("/sys/class/net/"..dev.."/bridge/bridge_id")
242                                                 result[dev].stp = (readfile("/sys/class/net/"..dev.."/bridge/stp_state") ~= "0")
243                                                 result[dev].bridge = true
244                                         end
245
246                                         local opr = readfile("/sys/class/net/"..dev.."/operstate")
247
248                                         result[dev].up = (opr == "up" or opr == "unknown")
249                                         result[dev].type = tonumber(readfile("/sys/class/net/"..dev.."/type"))
250                                         result[dev].name = dev
251
252                                         local mtu = tonumber(readfile("/sys/class/net/"..dev.."/mtu"))
253                                         if mtu and mtu > 0 then
254                                                 result[dev].mtu = mtu
255                                         end
256
257                                         local qlen = tonumber(readfile("/sys/class/net/"..dev.."/tx_queue_len"))
258                                         if qlen and qlen > 0 then
259                                                 result[dev].qlen = qlen
260                                         end
261
262                                         local master = fs.readlink("/sys/class/net/"..dev.."/master")
263                                         if master then
264                                                 result[dev].master = fs.basename(master)
265                                         end
266
267                                         local mac = readfile("/sys/class/net/"..dev.."/address")
268                                         if mac and #mac == 17 then
269                                                 result[dev].mac = mac
270                                         end
271                                 end
272                         end
273                         return result
274                 end
275         },
276
277         getWirelessDevices = {
278                 call = function(args)
279                         local ubus = require "ubus".connect()
280                         if not ubus then
281                                 return { error = "Unable to establish ubus connection" }
282                         end
283
284                         local status = ubus:call("network.wireless", "status", {})
285                         if type(status) == "table" then
286                                 local radioname, radiodata
287                                 for radioname, radiodata in pairs(status) do
288                                         if type(radiodata) == "table" then
289                                                 radiodata.iwinfo = ubus:call("iwinfo", "info", { device = radioname }) or {}
290                                                 radiodata.iwinfo.bitrate     = nil
291                                                 radiodata.iwinfo.bssid       = nil
292                                                 radiodata.iwinfo.encryption  = nil
293                                                 radiodata.iwinfo.mode        = nil
294                                                 radiodata.iwinfo.quality     = nil
295                                                 radiodata.iwinfo.quality_max = nil
296                                                 radiodata.iwinfo.ssid        = nil
297
298                                                 local iwdata = nil
299
300                                                 if type(radiodata.interfaces) == "table" then
301                                                         local _, interfacedata
302                                                         for _, interfacedata in ipairs(radiodata.interfaces) do
303                                                                 if type(interfacedata) == "table" and
304                                                                    type(interfacedata.ifname) == "string"
305                                                                 then
306                                                                         local iwinfo = ubus:call("iwinfo", "info", { device = interfacedata.ifname })
307
308                                                                         iwdata = iwdata or iwinfo
309                                                                         interfacedata.iwinfo = iwinfo or {}
310                                                                 end
311                                                         end
312                                                 end
313
314                                                 radiodata.iwinfo = {}
315
316                                                 local _, k, v
317                                                 for k, v in pairs(iwdata or ubus:call("iwinfo", "info", { device = radioname }) or {}) do
318                                                         if k ~= "bitrate" and k ~= "bssid" and k ~= "encryption" and
319                                                            k ~= "mode" and k ~= "quality" and k ~= "quality_max" and
320                                                            k ~= "ssid"
321                                                         then
322                                                                 if type(v) == "table" then
323                                                                         radiodata.iwinfo[k] = json.parse(json.stringify(v))
324                                                                 else
325                                                                         radiodata.iwinfo[k] = v
326                                                                 end
327                                                         end
328                                                 end
329                                         end
330                                 end
331                         end
332
333                         return status
334                 end
335         },
336
337         getBoardJSON = {
338                 call = function(args)
339                         local jsc = require "luci.jsonc"
340                         return jsc.parse(fs.readfile("/etc/board.json") or "")
341                 end
342         },
343
344         getConntrackHelpers = {
345                 call = function()
346                         local ok, fd = pcall(io.open, "/usr/share/fw3/helpers.conf", "r")
347                         local rv = {}
348
349                         if ok then
350                                 local entry
351
352                                 while true do
353                                         local line = fd:read("*l")
354                                         if not line then
355                                                 break
356                                         end
357
358                                         if line:match("^%s*config%s") then
359                                                 if entry then
360                                                         rv[#rv+1] = entry
361                                                 end
362                                                 entry = {}
363                                         else
364                                                 local opt, val = line:match("^%s*option%s+(%S+)%s+(%S.*)$")
365                                                 if opt and val then
366                                                         opt = opt:gsub("^'(.+)'$", "%1"):gsub('^"(.+)"$', "%1")
367                                                         val = val:gsub("^'(.+)'$", "%1"):gsub('^"(.+)"$', "%1")
368                                                         entry[opt] = val
369                                                 end
370                                         end
371                                 end
372
373                                 if entry then
374                                         rv[#rv+1] = entry
375                                 end
376
377                                 fd:close()
378                         end
379
380                         return { result = rv }
381                 end
382         },
383
384         getFeatures = {
385                 call = function()
386                         local fs = require "nixio.fs"
387                         local rv = {}
388                         local ok, fd
389
390                         rv.firewall      = fs.access("/sbin/fw3")
391                         rv.opkg          = fs.access("/bin/opkg")
392                         rv.offloading    = fs.access("/sys/module/xt_FLOWOFFLOAD/refcnt")
393                         rv.br2684ctl     = fs.access("/usr/sbin/br2684ctl")
394                         rv.swconfig      = fs.access("/sbin/swconfig")
395                         rv.odhcpd        = fs.access("/usr/sbin/odhcpd")
396                         rv.zram          = fs.access("/sys/class/zram-control")
397                         rv.sysntpd       = fs.readlink("/usr/sbin/ntpd") and true
398                         rv.ipv6          = fs.access("/proc/net/ipv6_route")
399                         rv.dropbear      = fs.access("/usr/sbin/dropbear")
400
401                         local wifi_features = { "eap", "11n", "11ac", "11r", "11w", "acs", "sae", "owe", "suiteb192" }
402
403                         if fs.access("/usr/sbin/hostapd") then
404                                 rv.hostapd = { cli = fs.access("/usr/sbin/hostapd_cli") }
405
406                                 local _, feature
407                                 for _, feature in ipairs(wifi_features) do
408                                         rv.hostapd[feature] =
409                                                 (os.execute(string.format("/usr/sbin/hostapd -v%s >/dev/null 2>/dev/null", feature)) == 0)
410                                 end
411                         end
412
413                         if fs.access("/usr/sbin/wpa_supplicant") then
414                                 rv.wpasupplicant = { cli = fs.access("/usr/sbin/wpa_cli") }
415
416                                 local _, feature
417                                 for _, feature in ipairs(wifi_features) do
418                                         rv.wpasupplicant[feature] =
419                                                 (os.execute(string.format("/usr/sbin/wpa_supplicant -v%s >/dev/null 2>/dev/null", feature)) == 0)
420                                 end
421                         end
422
423                         ok, fd = pcall(io.popen, "dnsmasq --version 2>/dev/null")
424                         if ok then
425                                 rv.dnsmasq = {}
426
427                                 while true do
428                                         local line = fd:read("*l")
429                                         if not line then
430                                                 break
431                                         end
432
433                                         local opts = line:match("^Compile time options: (.+)$")
434                                         if opts then
435                                                 local opt
436                                                 for opt in opts:gmatch("%S+") do
437                                                         local no = opt:match("^no%-(%S+)$")
438                                                         rv.dnsmasq[string.lower(no or opt)] = not no
439                                                 end
440                                                 break
441                                         end
442                                 end
443
444                                 fd:close()
445                         end
446
447                         ok, fd = pcall(io.popen, "ipset --help 2>/dev/null")
448                         if ok then
449                                 rv.ipset = {}
450
451                                 local sets = false
452
453                                 while true do
454                                         local line = fd:read("*l")
455                                         if not line then
456                                                 break
457                                         elseif line:match("^Supported set types:") then
458                                                 sets = true
459                                         elseif sets then
460                                                 local set, ver = line:match("^%s+(%S+)%s+(%d+)")
461                                                 if set and not rv.ipset[set] then
462                                                         rv.ipset[set] = tonumber(ver)
463                                                 end
464                                         end
465                                 end
466
467                                 fd:close()
468                         end
469
470                         return rv
471                 end
472         },
473
474         getSwconfigFeatures = {
475                 args = { switch = "switch0" },
476                 call = function(args)
477                         local util = require "luci.util"
478
479                         -- Parse some common switch properties from swconfig help output.
480                         local swc, err = io.popen("swconfig dev %s help 2>/dev/null" % util.shellquote(args.switch))
481                         if swc then
482                                 local is_port_attr = false
483                                 local is_vlan_attr = false
484                                 local rv = {}
485
486                                 while true do
487                                         local line = swc:read("*l")
488                                         if not line then break end
489
490                                         if line:match("^%s+%-%-vlan") then
491                                                 is_vlan_attr = true
492
493                                         elseif line:match("^%s+%-%-port") then
494                                                 is_vlan_attr = false
495                                                 is_port_attr = true
496
497                                         elseif line:match("cpu @") then
498                                                 rv.switch_title = line:match("^switch%d: %w+%((.-)%)")
499                                                 rv.num_vlans    = tonumber(line:match("vlans: (%d+)")) or 16
500                                                 rv.min_vid      = 1
501
502                                         elseif line:match(": pvid") or line:match(": tag") or line:match(": vid") then
503                                                 if is_vlan_attr then rv.vid_option = line:match(": (%w+)") end
504
505                                         elseif line:match(": enable_vlan4k") then
506                                                 rv.vlan4k_option = "enable_vlan4k"
507
508                                         elseif line:match(": enable_vlan") then
509                                                 rv.vlan_option = "enable_vlan"
510
511                                         elseif line:match(": enable_learning") then
512                                                 rv.learning_option = "enable_learning"
513
514                                         elseif line:match(": enable_mirror_rx") then
515                                                 rv.mirror_option = "enable_mirror_rx"
516
517                                         elseif line:match(": max_length") then
518                                                 rv.jumbo_option = "max_length"
519                                         end
520                                 end
521
522                                 swc:close()
523
524                                 if not next(rv) then
525                                         return { error = "No such switch" }
526                                 end
527
528                                 return rv
529                         else
530                                 return { error = err }
531                         end
532                 end
533         },
534
535         getSwconfigPortState = {
536                 args = { switch = "switch0" },
537                 call = function(args)
538                         local util = require "luci.util"
539
540                         local swc, err = io.popen("swconfig dev %s show 2>/dev/null" % util.shellquote(args.switch))
541                         if swc then
542                                 local ports = { }
543
544                                 while true do
545                                         local line = swc:read("*l")
546                                         if not line then break end
547
548                                         local port, up = line:match("port:(%d+) link:(%w+)")
549                                         if port then
550                                                 local speed  = line:match(" speed:(%d+)")
551                                                 local duplex = line:match(" (%w+)-duplex")
552                                                 local txflow = line:match(" (txflow)")
553                                                 local rxflow = line:match(" (rxflow)")
554                                                 local auto   = line:match(" (auto)")
555
556                                                 ports[#ports+1] = {
557                                                         port   = tonumber(port) or 0,
558                                                         speed  = tonumber(speed) or 0,
559                                                         link   = (up == "up"),
560                                                         duplex = (duplex == "full"),
561                                                         rxflow = (not not rxflow),
562                                                         txflow = (not not txflow),
563                                                         auto   = (not not auto)
564                                                 }
565                                         end
566                                 end
567
568                                 swc:close()
569
570                                 if not next(ports) then
571                                         return { error = "No such switch" }
572                                 end
573
574                                 return { result = ports }
575                         else
576                                 return { error = err }
577                         end
578                 end
579         },
580
581         setPassword = {
582                 args = { username = "root", password = "password" },
583                 call = function(args)
584                         local util = require "luci.util"
585                         return {
586                                 result = (os.execute("(echo %s; sleep 1; echo %s) | passwd %s >/dev/null 2>&1" %{
587                                         luci.util.shellquote(args.password),
588                                         luci.util.shellquote(args.password),
589                                         luci.util.shellquote(args.username)
590                                 }) == 0)
591                         }
592                 end
593         },
594
595         getBlockDevices = {
596                 call = function()
597                         local fs = require "nixio.fs"
598
599                         local block = io.popen("/sbin/block info", "r")
600                         if block then
601                                 local rv = {}
602
603                                 while true do
604                                         local ln = block:read("*l")
605                                         if not ln then
606                                                 break
607                                         end
608
609                                         local dev = ln:match("^/dev/(.-):")
610                                         if dev then
611                                                 local s = tonumber((fs.readfile("/sys/class/block/" .. dev .."/size")))
612                                                 local e = {
613                                                         dev = "/dev/" .. dev,
614                                                         size = s and s * 512
615                                                 }
616
617                                                 local key, val = { }
618                                                 for key, val in ln:gmatch([[(%w+)="(.-)"]]) do
619                                                         e[key:lower()] = val
620                                                 end
621
622                                                 rv[dev] = e
623                                         end
624                                 end
625
626                                 block:close()
627
628                                 return rv
629                         else
630                                 return { error = "Unable to execute block utility" }
631                         end
632                 end
633         },
634
635         setBlockDetect = {
636                 call = function()
637                         return { result = (os.execute("/sbin/block detect > /etc/config/fstab") == 0) }
638                 end
639         },
640
641         getMountPoints = {
642                 call = function()
643                         local fs = require "nixio.fs"
644
645                         local fd, err = io.open("/proc/mounts", "r")
646                         if fd then
647                                 local rv = {}
648
649                                 while true do
650                                         local ln = fd:read("*l")
651                                         if not ln then
652                                                 break
653                                         end
654
655                                         local device, mount, fstype, options, freq, pass = ln:match("^(%S*) (%S*) (%S*) (%S*) (%d+) (%d+)$")
656                                         if device and mount then
657                                                 device = device:gsub("\\(%d+)", function(n) return string.char(tonumber(n, 8)) end)
658                                                 mount = mount:gsub("\\(%d+)", function(n) return string.char(tonumber(n, 8)) end)
659
660                                                 local stat = fs.statvfs(mount)
661                                                 if stat and stat.blocks > 0 then
662                                                         rv[#rv+1] = {
663                                                                 device = device,
664                                                                 mount  = mount,
665                                                                 size   = stat.bsize * stat.blocks,
666                                                                 avail  = stat.bsize * stat.bavail,
667                                                                 free   = stat.bsize * stat.bfree
668                                                         }
669                                                 end
670                                         end
671                                 end
672
673                                 fd:close()
674
675                                 return { result = rv }
676                         else
677                                 return { error = err }
678                         end
679                 end
680         },
681
682         setUmount = {
683                 args = { path = "/mnt" },
684                 call = function(args)
685                         local util = require "luci.util"
686                         return { result = (os.execute(string.format("/bin/umount %s", util.shellquote(args.path))) == 0) }
687                 end
688         }
689 }
690
691 local function parseInput()
692         local parse = json.new()
693         local done, err
694
695         while true do
696                 local chunk = io.read(4096)
697                 if not chunk then
698                         break
699                 elseif not done and not err then
700                         done, err = parse:parse(chunk)
701                 end
702         end
703
704         if not done then
705                 print(json.stringify({ error = err or "Incomplete input" }))
706                 os.exit(1)
707         end
708
709         return parse:get()
710 end
711
712 local function validateArgs(func, uargs)
713         local method = methods[func]
714         if not method then
715                 print(json.stringify({ error = "Method not found" }))
716                 os.exit(1)
717         end
718
719         if type(uargs) ~= "table" then
720                 print(json.stringify({ error = "Invalid arguments" }))
721                 os.exit(1)
722         end
723
724         uargs.ubus_rpc_session = nil
725
726         local k, v
727         local margs = method.args or {}
728         for k, v in pairs(uargs) do
729                 if margs[k] == nil or
730                    (v ~= nil and type(v) ~= type(margs[k]))
731                 then
732                         print(json.stringify({ error = "Invalid arguments" }))
733                         os.exit(1)
734                 end
735         end
736
737         return method
738 end
739
740 if arg[1] == "list" then
741         local _, method, rv = nil, nil, {}
742         for _, method in pairs(methods) do rv[_] = method.args or {} end
743         print((json.stringify(rv):gsub(":%[%]", ":{}")))
744 elseif arg[1] == "call" then
745         local args = parseInput()
746         local method = validateArgs(arg[2], args)
747         local result, code = method.call(args)
748         print((json.stringify(result):gsub("^%[%]$", "{}")))
749         os.exit(code or 0)
750 end