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