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