#!/usr/bin/env lua local json = require "luci.jsonc" local fs = require "nixio.fs" local function readfile(path) local s = fs.readfile(path) return s and (s:gsub("^%s+", ""):gsub("%s+$", "")) end local methods = { getInitList = { args = { name = "name" }, call = function(args) local sys = require "luci.sys" local _, name, scripts = nil, nil, {} for _, name in ipairs(args.name and { args.name } or sys.init.names()) do local index = sys.init.index(name) if index then scripts[name] = { index = index, enabled = sys.init.enabled(name) } else return { error = "No such init script" } end end return scripts end }, setInitAction = { args = { name = "name", action = "action" }, call = function(args) local sys = require "luci.sys" if type(sys.init[args.action]) ~= "function" then return { error = "Invalid action" } end return { result = sys.init[args.action](args.name) } end }, getLocaltime = { call = function(args) return { result = os.time() } end }, setLocaltime = { args = { localtime = 0 }, call = function(args) local sys = require "luci.sys" local date = os.date("*t", args.localtime) if date then sys.call("date -s '%04d-%02d-%02d %02d:%02d:%02d' >/dev/null" %{ date.year, date.month, date.day, date.hour, date.min, date.sec }) sys.call("/etc/init.d/sysfixtime restart >/dev/null") end return { result = args.localtime } end }, getTimezones = { call = function(args) local util = require "luci.util" local zones = require "luci.sys.zoneinfo" local tz = readfile("/etc/TZ") local res = util.ubus("uci", "get", { config = "system", section = "@system[0]", option = "zonename" }) local result = {} local _, zone for _, zone in ipairs(zones.TZ) do result[zone[1]] = { tzstring = zone[2], active = (res and res.value == zone[1]) and true or nil } end return result end }, getLEDs = { call = function() local iter = fs.dir("/sys/class/leds") local result = { } if iter then local led for led in iter do local m, s result[led] = { triggers = {} } s = readfile("/sys/class/leds/"..led.."/trigger") for s in (s or ""):gmatch("%S+") do m = s:match("^%[(.+)%]$") result[led].triggers[#result[led].triggers+1] = m or s result[led].active_trigger = m or result[led].active_trigger end s = readfile("/sys/class/leds/"..led.."/brightness") if s then result[led].brightness = tonumber(s) end s = readfile("/sys/class/leds/"..led.."/max_brightness") if s then result[led].max_brightness = tonumber(s) end end end return result end }, getUSBDevices = { call = function() local fs = require "nixio.fs" local iter = fs.glob("/sys/bus/usb/devices/[0-9]*/manufacturer") local result = { } if iter then result.devices = {} local p for p in iter do local id = p:match("/([^/]+)/manufacturer$") result.devices[#result.devices+1] = { id = id, vid = readfile("/sys/bus/usb/devices/"..id.."/idVendor"), pid = readfile("/sys/bus/usb/devices/"..id.."/idProduct"), vendor = readfile("/sys/bus/usb/devices/"..id.."/manufacturer"), product = readfile("/sys/bus/usb/devices/"..id.."/product"), speed = tonumber((readfile("/sys/bus/usb/devices/"..id.."/product"))) } end end iter = fs.glob("/sys/bus/usb/devices/*/*-port[0-9]*") if iter then result.ports = {} local p for p in iter do local port = p:match("([^/]+)$") local link = fs.readlink(p.."/device") result.ports[#result.ports+1] = { port = port, device = link and fs.basename(link) } end end return result end }, getIfaddrs = { call = function() return { result = nixio.getifaddrs() } end }, getHostHints = { call = function() local sys = require "luci.sys" return sys.net.host_hints() end }, getDUIDHints = { call = function() local fp = io.open('/var/hosts/odhcpd') local result = { } if fp then for line in fp:lines() do local dev, duid, name = string.match(line, '# (%S+)%s+(%S+)%s+%d+%s+(%S+)') if dev and duid and name then result[duid] = { name = (name ~= "-") and name or nil, device = dev } end end fp:close() end return result end }, getDHCPLeases = { args = { family = 0 }, call = function(args) local s = require "luci.tools.status" if args.family == 4 then return { dhcp_leases = s.dhcp_leases() } elseif args.family == 6 then return { dhcp6_leases = s.dhcp6_leases() } else return { dhcp_leases = s.dhcp_leases(), dhcp6_leases = s.dhcp6_leases() } end end }, getNetworkDevices = { call = function(args) local dir = fs.dir("/sys/class/net") local result = { } if dir then local dev for dev in dir do if not result[dev] then result[dev] = { name = dev } end if fs.access("/sys/class/net/"..dev.."/master") then local brname = fs.basename(fs.readlink("/sys/class/net/"..dev.."/master")) if not result[brname] then result[brname] = { name = brname } end if not result[brname].ports then result[brname].ports = { } end result[brname].ports[#result[brname].ports+1] = dev elseif fs.access("/sys/class/net/"..dev.."/bridge") then if not result[dev].ports then result[dev].ports = { } end result[dev].id = readfile("/sys/class/net/"..dev.."/bridge/bridge_id") result[dev].stp = (readfile("/sys/class/net/"..dev.."/bridge/stp_state") ~= "0") result[dev].bridge = true end local opr = readfile("/sys/class/net/"..dev.."/operstate") result[dev].up = (opr == "up" or opr == "unknown") result[dev].type = tonumber(readfile("/sys/class/net/"..dev.."/type")) result[dev].name = dev local mtu = tonumber(readfile("/sys/class/net/"..dev.."/mtu")) if mtu and mtu > 0 then result[dev].mtu = mtu end local qlen = tonumber(readfile("/sys/class/net/"..dev.."/tx_queue_len")) if qlen and qlen > 0 then result[dev].qlen = qlen end local master = fs.readlink("/sys/class/net/"..dev.."/master") if master then result[dev].master = fs.basename(master) end local mac = readfile("/sys/class/net/"..dev.."/address") if mac and #mac == 17 then result[dev].mac = mac end end end return result end }, getBoardJSON = { call = function(args) local jsc = require "luci.jsonc" return jsc.parse(fs.readfile("/etc/board.json") or "") end }, getConntrackHelpers = { call = function() local ok, fd = pcall(io.open, "/usr/share/fw3/helpers.conf", "r") local rv = {} if ok then local entry while true do local line = fd:read("*l") if not line then break end if line:match("^%s*config%s") then if entry then rv[#rv+1] = entry end entry = {} else local opt, val = line:match("^%s*option%s+(%S+)%s+(%S.*)$") if opt and val then opt = opt:gsub("^'(.+)'$", "%1"):gsub('^"(.+)"$', "%1") val = val:gsub("^'(.+)'$", "%1"):gsub('^"(.+)"$', "%1") entry[opt] = val end end end if entry then rv[#rv+1] = entry end fd:close() end return { result = rv } end }, getFeatures = { call = function() local fs = require "nixio.fs" local rv = {} local ok, fd rv.firewall = fs.access("/sbin/fw3") rv.opkg = fs.access("/bin/opkg") rv.offloading = fs.access("/sys/module/xt_FLOWOFFLOAD/refcnt") rv.br2684ctl = fs.access("/usr/sbin/br2684ctl") rv.swconfig = fs.access("/sbin/swconfig") rv.odhcpd = fs.access("/usr/sbin/odhcpd") rv.zram = fs.access("/sys/class/zram-control") rv.sysntpd = fs.readlink("/usr/sbin/ntpd") and true rv.ipv6 = fs.access("/proc/net/ipv6_route") local wifi_features = { "eap", "11n", "11ac", "11r", "11w", "acs", "sae", "owe", "suiteb192" } if fs.access("/usr/sbin/hostapd") then rv.hostapd = {} local _, feature for _, feature in ipairs(wifi_features) do rv.hostapd[feature] = (os.execute(string.format("/usr/sbin/hostapd -v%s >/dev/null 2>/dev/null", feature)) == 0) end end if fs.access("/usr/sbin/wpa_supplicant") then rv.wpasupplicant = {} local _, feature for _, feature in ipairs(wifi_features) do rv.wpasupplicant[feature] = (os.execute(string.format("/usr/sbin/wpa_supplicant -v%s >/dev/null 2>/dev/null", feature)) == 0) end end ok, fd = pcall(io.popen, "dnsmasq --version 2>/dev/null") if ok then rv.dnsmasq = {} while true do local line = fd:read("*l") if not line then break end local opts = line:match("^Compile time options: (.+)$") if opts then local opt for opt in opts:gmatch("%S+") do local no = opt:match("^no%-(%S+)$") rv.dnsmasq[string.lower(no or opt)] = not no end break end end fd:close() end ok, fd = pcall(io.popen, "ipset --help 2>/dev/null") if ok then rv.ipset = {} local sets = false while true do local line = fd:read("*l") if not line then break elseif line:match("^Supported set types:") then sets = true elseif sets then local set, ver = line:match("^%s+(%S+)%s+(%d+)") if set and not rv.ipset[set] then rv.ipset[set] = tonumber(ver) end end end fd:close() end return rv end }, getHostname = { call = function() local fs = require "nixio.fs" local hostname, code, err = fs.readfile("/proc/sys/kernel/hostname") if err then return { error = err }, 1 end return { result = hostname:gsub("\n$", "") } end }, getTTYDevices = { args = { with_cdc = false, with_tts = true }, call = function(args) local fs = require "nixio.fs" local iter = fs.glob("/dev/tty[A-Z]*") local rv = {} if iter then local node for node in iter do rv[#rv+1] = node end end if args.with_tts then iter = fs.glob("/dev/tts/*") if iter then local node for node in iter do rv[#rv+1] = node end end end if args.with_cdc then iter = fs.glob("/dev/cdc-wdm*") if iter then local node for node in iter do rv[#rv+1] = node end end end return { result = rv } end } } local function parseInput() local parse = json.new() local done, err while true do local chunk = io.read(4096) if not chunk then break elseif not done and not err then done, err = parse:parse(chunk) end end if not done then print(json.stringify({ error = err or "Incomplete input" })) os.exit(1) end return parse:get() end local function validateArgs(func, uargs) local method = methods[func] if not method then print(json.stringify({ error = "Method not found" })) os.exit(1) end if type(uargs) ~= "table" then print(json.stringify({ error = "Invalid arguments" })) os.exit(1) end uargs.ubus_rpc_session = nil local k, v local margs = method.args or {} for k, v in pairs(uargs) do if margs[k] == nil or (v ~= nil and type(v) ~= type(margs[k])) then print(json.stringify({ error = "Invalid arguments" })) os.exit(1) end end return method end if arg[1] == "list" then local _, method, rv = nil, nil, {} for _, method in pairs(methods) do rv[_] = method.args or {} end print((json.stringify(rv):gsub(":%[%]", ":{}"))) elseif arg[1] == "call" then local args = parseInput() local method = validateArgs(arg[2], args) local result, code = method.call(args) print((json.stringify(result):gsub("^%[%]$", "{}"))) os.exit(code or 0) end