ab6a7ecfeebaacd491bea4050644ca29d04cff53
[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
400                         local wifi_features = { "eap", "11n", "11ac", "11r", "11w", "acs", "sae", "owe", "suiteb192" }
401
402                         if fs.access("/usr/sbin/hostapd") then
403                                 rv.hostapd = { cli = fs.access("/usr/sbin/hostapd_cli") }
404
405                                 local _, feature
406                                 for _, feature in ipairs(wifi_features) do
407                                         rv.hostapd[feature] =
408                                                 (os.execute(string.format("/usr/sbin/hostapd -v%s >/dev/null 2>/dev/null", feature)) == 0)
409                                 end
410                         end
411
412                         if fs.access("/usr/sbin/wpa_supplicant") then
413                                 rv.wpasupplicant = { cli = fs.access("/usr/sbin/wpa_cli") }
414
415                                 local _, feature
416                                 for _, feature in ipairs(wifi_features) do
417                                         rv.wpasupplicant[feature] =
418                                                 (os.execute(string.format("/usr/sbin/wpa_supplicant -v%s >/dev/null 2>/dev/null", feature)) == 0)
419                                 end
420                         end
421
422                         ok, fd = pcall(io.popen, "dnsmasq --version 2>/dev/null")
423                         if ok then
424                                 rv.dnsmasq = {}
425
426                                 while true do
427                                         local line = fd:read("*l")
428                                         if not line then
429                                                 break
430                                         end
431
432                                         local opts = line:match("^Compile time options: (.+)$")
433                                         if opts then
434                                                 local opt
435                                                 for opt in opts:gmatch("%S+") do
436                                                         local no = opt:match("^no%-(%S+)$")
437                                                         rv.dnsmasq[string.lower(no or opt)] = not no
438                                                 end
439                                                 break
440                                         end
441                                 end
442
443                                 fd:close()
444                         end
445
446                         ok, fd = pcall(io.popen, "ipset --help 2>/dev/null")
447                         if ok then
448                                 rv.ipset = {}
449
450                                 local sets = false
451
452                                 while true do
453                                         local line = fd:read("*l")
454                                         if not line then
455                                                 break
456                                         elseif line:match("^Supported set types:") then
457                                                 sets = true
458                                         elseif sets then
459                                                 local set, ver = line:match("^%s+(%S+)%s+(%d+)")
460                                                 if set and not rv.ipset[set] then
461                                                         rv.ipset[set] = tonumber(ver)
462                                                 end
463                                         end
464                                 end
465
466                                 fd:close()
467                         end
468
469                         return rv
470                 end
471         },
472
473         getHostname = {
474                 call = function()
475                         local fs = require "nixio.fs"
476                         local hostname, code, err = fs.readfile("/proc/sys/kernel/hostname")
477                         if err then
478                                 return { error = err }, 1
479                         end
480                         return { result = hostname:gsub("\n$", "") }
481                 end
482         },
483
484         getTTYDevices = {
485                 args = { with_cdc = false, with_tts = true },
486                 call = function(args)
487                         local fs = require "nixio.fs"
488                         local iter = fs.glob("/dev/tty[A-Z]*")
489                         local rv = {}
490                         if iter then
491                                 local node
492                                 for node in iter do
493                                         rv[#rv+1] = node
494                                 end
495                         end
496                         if args.with_tts then
497                                 iter = fs.glob("/dev/tts/*")
498                                 if iter then
499                                         local node
500                                         for node in iter do
501                                                 rv[#rv+1] = node
502                                         end
503                                 end
504                         end
505                         if args.with_cdc then
506                                 iter = fs.glob("/dev/cdc-wdm*")
507                                 if iter then
508                                         local node
509                                         for node in iter do
510                                                 rv[#rv+1] = node
511                                         end
512                                 end
513                         end
514                         return { result = rv }
515                 end
516         }
517 }
518
519 local function parseInput()
520         local parse = json.new()
521         local done, err
522
523         while true do
524                 local chunk = io.read(4096)
525                 if not chunk then
526                         break
527                 elseif not done and not err then
528                         done, err = parse:parse(chunk)
529                 end
530         end
531
532         if not done then
533                 print(json.stringify({ error = err or "Incomplete input" }))
534                 os.exit(1)
535         end
536
537         return parse:get()
538 end
539
540 local function validateArgs(func, uargs)
541         local method = methods[func]
542         if not method then
543                 print(json.stringify({ error = "Method not found" }))
544                 os.exit(1)
545         end
546
547         if type(uargs) ~= "table" then
548                 print(json.stringify({ error = "Invalid arguments" }))
549                 os.exit(1)
550         end
551
552         uargs.ubus_rpc_session = nil
553
554         local k, v
555         local margs = method.args or {}
556         for k, v in pairs(uargs) do
557                 if margs[k] == nil or
558                    (v ~= nil and type(v) ~= type(margs[k]))
559                 then
560                         print(json.stringify({ error = "Invalid arguments" }))
561                         os.exit(1)
562                 end
563         end
564
565         return method
566 end
567
568 if arg[1] == "list" then
569         local _, method, rv = nil, nil, {}
570         for _, method in pairs(methods) do rv[_] = method.args or {} end
571         print((json.stringify(rv):gsub(":%[%]", ":{}")))
572 elseif arg[1] == "call" then
573         local args = parseInput()
574         local method = validateArgs(arg[2], args)
575         local result, code = method.call(args)
576         print((json.stringify(result):gsub("^%[%]$", "{}")))
577         os.exit(code or 0)
578 end