e27149127c7748ec81b1996c952de0dc449715fa
[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         getBoardJSON = {
278                 call = function(args)
279                         local jsc = require "luci.jsonc"
280                         return jsc.parse(fs.readfile("/etc/board.json") or "")
281                 end
282         },
283
284         getConntrackHelpers = {
285                 call = function()
286                         local ok, fd = pcall(io.open, "/usr/share/fw3/helpers.conf", "r")
287                         local rv = {}
288
289                         if ok then
290                                 local entry
291
292                                 while true do
293                                         local line = fd:read("*l")
294                                         if not line then
295                                                 break
296                                         end
297
298                                         if line:match("^%s*config%s") then
299                                                 if entry then
300                                                         rv[#rv+1] = entry
301                                                 end
302                                                 entry = {}
303                                         else
304                                                 local opt, val = line:match("^%s*option%s+(%S+)%s+(%S.*)$")
305                                                 if opt and val then
306                                                         opt = opt:gsub("^'(.+)'$", "%1"):gsub('^"(.+)"$', "%1")
307                                                         val = val:gsub("^'(.+)'$", "%1"):gsub('^"(.+)"$', "%1")
308                                                         entry[opt] = val
309                                                 end
310                                         end
311                                 end
312
313                                 if entry then
314                                         rv[#rv+1] = entry
315                                 end
316
317                                 fd:close()
318                         end
319
320                         return { result = rv }
321                 end
322         },
323
324         getFeatures = {
325                 call = function()
326                         local fs = require "nixio.fs"
327                         local rv = {}
328                         local ok, fd
329
330                         rv.firewall      = fs.access("/sbin/fw3")
331                         rv.opkg          = fs.access("/bin/opkg")
332                         rv.offloading    = fs.access("/sys/module/xt_FLOWOFFLOAD/refcnt")
333                         rv.br2684ctl     = fs.access("/usr/sbin/br2684ctl")
334                         rv.swconfig      = fs.access("/sbin/swconfig")
335                         rv.odhcpd        = fs.access("/usr/sbin/odhcpd")
336                         rv.zram          = fs.access("/sys/class/zram-control")
337                         rv.sysntpd       = fs.readlink("/usr/sbin/ntpd") and true
338                         rv.ipv6          = fs.access("/proc/net/ipv6_route")
339
340                         local wifi_features = { "eap", "11n", "11ac", "11r", "11w", "acs", "sae", "owe", "suiteb192" }
341
342                         if fs.access("/usr/sbin/hostapd") then
343                                 rv.hostapd = {}
344
345                                 local _, feature
346                                 for _, feature in ipairs(wifi_features) do
347                                         rv.hostapd[feature] =
348                                                 (os.execute(string.format("/usr/sbin/hostapd -v%s >/dev/null 2>/dev/null", feature)) == 0)
349                                 end
350                         end
351
352                         if fs.access("/usr/sbin/wpa_supplicant") then
353                                 rv.wpasupplicant = {}
354
355                                 local _, feature
356                                 for _, feature in ipairs(wifi_features) do
357                                         rv.wpasupplicant[feature] =
358                                                 (os.execute(string.format("/usr/sbin/wpa_supplicant -v%s >/dev/null 2>/dev/null", feature)) == 0)
359                                 end
360                         end
361
362                         ok, fd = pcall(io.popen, "dnsmasq --version 2>/dev/null")
363                         if ok then
364                                 rv.dnsmasq = {}
365
366                                 while true do
367                                         local line = fd:read("*l")
368                                         if not line then
369                                                 break
370                                         end
371
372                                         local opts = line:match("^Compile time options: (.+)$")
373                                         if opts then
374                                                 local opt
375                                                 for opt in opts:gmatch("%S+") do
376                                                         local no = opt:match("^no%-(%S+)$")
377                                                         rv.dnsmasq[string.lower(no or opt)] = not no
378                                                 end
379                                                 break
380                                         end
381                                 end
382
383                                 fd:close()
384                         end
385
386                         ok, fd = pcall(io.popen, "ipset --help 2>/dev/null")
387                         if ok then
388                                 rv.ipset = {}
389
390                                 local sets = false
391
392                                 while true do
393                                         local line = fd:read("*l")
394                                         if not line then
395                                                 break
396                                         elseif line:match("^Supported set types:") then
397                                                 sets = true
398                                         elseif sets then
399                                                 local set, ver = line:match("^%s+(%S+)%s+(%d+)")
400                                                 if set and not rv.ipset[set] then
401                                                         rv.ipset[set] = tonumber(ver)
402                                                 end
403                                         end
404                                 end
405
406                                 fd:close()
407                         end
408
409                         return rv
410                 end
411         },
412
413         getHostname = {
414                 call = function()
415                         local fs = require "nixio.fs"
416                         local hostname, code, err = fs.readfile("/proc/sys/kernel/hostname")
417                         if err then
418                                 return { error = err }, 1
419                         end
420                         return { result = hostname:gsub("\n$", "") }
421                 end
422         },
423
424         getTTYDevices = {
425                 args = { with_cdc = false, with_tts = true },
426                 call = function(args)
427                         local fs = require "nixio.fs"
428                         local iter = fs.glob("/dev/tty[A-Z]*")
429                         local rv = {}
430                         if iter then
431                                 local node
432                                 for node in iter do
433                                         rv[#rv+1] = node
434                                 end
435                         end
436                         if args.with_tts then
437                                 iter = fs.glob("/dev/tts/*")
438                                 if iter then
439                                         local node
440                                         for node in iter do
441                                                 rv[#rv+1] = node
442                                         end
443                                 end
444                         end
445                         if args.with_cdc then
446                                 iter = fs.glob("/dev/cdc-wdm*")
447                                 if iter then
448                                         local node
449                                         for node in iter do
450                                                 rv[#rv+1] = node
451                                         end
452                                 end
453                         end
454                         return { result = rv }
455                 end
456         }
457 }
458
459 local function parseInput()
460         local parse = json.new()
461         local done, err
462
463         while true do
464                 local chunk = io.read(4096)
465                 if not chunk then
466                         break
467                 elseif not done and not err then
468                         done, err = parse:parse(chunk)
469                 end
470         end
471
472         if not done then
473                 print(json.stringify({ error = err or "Incomplete input" }))
474                 os.exit(1)
475         end
476
477         return parse:get()
478 end
479
480 local function validateArgs(func, uargs)
481         local method = methods[func]
482         if not method then
483                 print(json.stringify({ error = "Method not found" }))
484                 os.exit(1)
485         end
486
487         if type(uargs) ~= "table" then
488                 print(json.stringify({ error = "Invalid arguments" }))
489                 os.exit(1)
490         end
491
492         uargs.ubus_rpc_session = nil
493
494         local k, v
495         local margs = method.args or {}
496         for k, v in pairs(uargs) do
497                 if margs[k] == nil or
498                    (v ~= nil and type(v) ~= type(margs[k]))
499                 then
500                         print(json.stringify({ error = "Invalid arguments" }))
501                         os.exit(1)
502                 end
503         end
504
505         return method
506 end
507
508 if arg[1] == "list" then
509         local _, method, rv = nil, nil, {}
510         for _, method in pairs(methods) do rv[_] = method.args or {} end
511         print((json.stringify(rv):gsub(":%[%]", ":{}")))
512 elseif arg[1] == "call" then
513         local args = parseInput()
514         local method = validateArgs(arg[2], args)
515         local result, code = method.call(args)
516         print((json.stringify(result):gsub("^%[%]$", "{}")))
517         os.exit(code or 0)
518 end