diff --git a/files/generic/090-cover-index_files b/files/generic/090-cover-index_files index 8480059..feff66a 100755 --- a/files/generic/090-cover-index_files +++ b/files/generic/090-cover-index_files @@ -3,7 +3,7 @@ [ ! -f '/etc/rpcd_10_system.js' ] || \ mv -f '/etc/rpcd_10_system.js' '/www/luci-static/resources/view/status/include/10_system.js' [ ! -f '/etc/rpcd_luci' ] || \ - mv -f '/etc/rpcd_luci' '/usr/libexec/rpcd/luci' + mv -f '/etc/rpcd_luci' '/usr/share/rpcd/ucode/luci' /etc/init.d/rpcd restart exit 0 diff --git a/files/generic/10_system.js b/files/generic/10_system.js index aa1bc64..cbd0e8c 100644 --- a/files/generic/10_system.js +++ b/files/generic/10_system.js @@ -3,6 +3,11 @@ 'require fs'; 'require rpc'; +var callLuciVersion = rpc.declare({ + object: 'luci', + method: 'getVersion' +}); + var callSystemBoard = rpc.declare({ object: 'system', method: 'board' @@ -44,7 +49,7 @@ return baseclass.extend({ L.resolveDefault(callCPUInfo(), {}), L.resolveDefault(callCPUUsage(), {}), L.resolveDefault(callTempInfo(), {}), - fs.lines('/usr/lib/lua/luci/version.lua') + L.resolveDefault(callLuciVersion(), { revision: _('unknown version'), branch: 'LuCI' }) ]); }, @@ -57,11 +62,7 @@ return baseclass.extend({ tempinfo = data[5], luciversion = data[6]; - luciversion = luciversion.filter(function(l) { - return l.match(/^\s*(luciname|luciversion)\s*=/); - }).map(function(l) { - return l.replace(/^\s*\w+\s*=\s*['"]([^'"]+)['"].*$/, '$1'); - }).join(' '); + luciversion = luciversion.branch + ' ' + luciversion.revision; var datestr = null; diff --git a/files/generic/luci b/files/generic/luci index 83f4a35..864f07b 100755 --- a/files/generic/luci +++ b/files/generic/luci @@ -1,749 +1,644 @@ -#!/usr/bin/env lua +// Copyright 2022 Jo-Philipp Wich +// Licensed to the public under the Apache License 2.0. -local json = require "luci.jsonc" -local fs = require "nixio.fs" +'use strict'; -local function readfile(path) - local s = fs.readfile(path) - return s and (s:gsub("^%s+", ""):gsub("%s+$", "")) -end +import { stdin, access, dirname, basename, open, popen, glob, lsdir, readfile, readlink, error } from 'fs'; +import { cursor } from 'uci'; -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 - }, +import { init_list, init_index, init_enabled, init_action, conntrack_list, process_list } from 'luci.sys'; +import { revision, branch } from 'luci.version'; +import { statvfs } from 'luci.core'; - 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 - }, +import timezones from 'luci.zoneinfo'; - 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 - }, - - getConntrackHelpers = { - call = function() - local ok, fd = pcall(io.open, "/usr/share/fw3/helpers.conf", "r") - local rv = {} - - if not (ok and fd) then - ok, fd = pcall(io.open, "/usr/share/firewall4/helpers", "r") - end - - if ok and fd 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.firewall4 = fs.access("/sbin/fw4") - rv.opkg = fs.access("/bin/opkg") - rv.offloading = fs.access("/sys/module/xt_FLOWOFFLOAD/refcnt") or fs.access("/sys/module/nft_flow_offload/refcnt") - rv.fullcone = fs.access("/sys/module/xt_FULLCONENAT/refcnt") or fs.access("/sys/module/nft_fullcone/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") - rv.dropbear = fs.access("/usr/sbin/dropbear") - rv.cabundle = fs.access("/etc/ssl/certs/ca-certificates.crt") - rv.relayd = fs.access("/usr/sbin/relayd") - rv.dsl = fs.access("/sbin/dsl_cpe_control") or fs.access("/sbin/vdsl_cpe_control") - - local wifi_features = { "eap", "11n", "11ac", "11r", "acs", "sae", "owe", "suiteb192", "wep", "wps" } - - if fs.access("/usr/sbin/hostapd") then - rv.hostapd = { cli = fs.access("/usr/sbin/hostapd_cli") } - - 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 = { cli = fs.access("/usr/sbin/wpa_cli") } - - 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 - }, - - getSwconfigFeatures = { - args = { switch = "switch0" }, - call = function(args) - local util = require "luci.util" - - -- Parse some common switch properties from swconfig help output. - local swc, err = io.popen("swconfig dev %s help 2>/dev/null" % util.shellquote(args.switch)) - if swc then - local is_port_attr = false - local is_vlan_attr = false - local rv = {} - - while true do - local line = swc:read("*l") - if not line then break end - - if line:match("^%s+%-%-vlan") then - is_vlan_attr = true - - elseif line:match("^%s+%-%-port") then - is_vlan_attr = false - is_port_attr = true - - elseif line:match("cpu @") then - rv.switch_title = line:match("^switch%d: %w+%((.-)%)") - rv.num_vlans = tonumber(line:match("vlans: (%d+)")) or 16 - rv.min_vid = 1 - - elseif line:match(": pvid") or line:match(": tag") or line:match(": vid") then - if is_vlan_attr then rv.vid_option = line:match(": (%w+)") end - - elseif line:match(": enable_vlan4k") then - rv.vlan4k_option = "enable_vlan4k" - - elseif line:match(": enable_vlan") then - rv.vlan_option = "enable_vlan" - - elseif line:match(": enable_learning") then - rv.learning_option = "enable_learning" - - elseif line:match(": enable_mirror_rx") then - rv.mirror_option = "enable_mirror_rx" - - elseif line:match(": max_length") then - rv.jumbo_option = "max_length" - end - end - - swc:close() - - if not next(rv) then - return { error = "No such switch" } - end - - return rv - else - return { error = err } - end - end - }, - - getSwconfigPortState = { - args = { switch = "switch0" }, - call = function(args) - local util = require "luci.util" - - local swc, err = io.popen("swconfig dev %s show 2>/dev/null" % util.shellquote(args.switch)) - if swc then - local ports = { } - - while true do - local line = swc:read("*l") - if not line or (line:match("^VLAN %d+:") and #ports > 0) then - break - end - - local pnum = line:match("^Port (%d+):$") - if pnum then - port = { - port = tonumber(pnum), - duplex = false, - speed = 0, - link = false, - auto = false, - rxflow = false, - txflow = false - } - - ports[#ports+1] = port - end - - if port then - local m - - if line:match("full[%- ]duplex") then - port.duplex = true - end - - m = line:match(" speed:(%d+)") - if m then - port.speed = tonumber(m) - end - - m = line:match("(%d+) Mbps") - if m and port.speed == 0 then - port.speed = tonumber(m) - end - - m = line:match("link: (%d+)") - if m and port.speed == 0 then - port.speed = tonumber(m) - end - - if line:match("link: ?up") or line:match("status: ?up") then - port.link = true - end - - if line:match("auto%-negotiate") or line:match("link:.-auto") then - port.auto = true - end - - if line:match("link:.-rxflow") then - port.rxflow = true - end - - if line:match("link:.-txflow") then - port.txflow = true - end - end - end - - swc:close() - - if not next(ports) then - return { error = "No such switch" } - end - - return { result = ports } - else - return { error = err } - end - end - }, - - setPassword = { - args = { username = "root", password = "password" }, - call = function(args) - local util = require "luci.util" - return { - result = (os.execute("(echo %s; sleep 1; echo %s) | /bin/busybox passwd %s >/dev/null 2>&1" %{ - luci.util.shellquote(args.password), - luci.util.shellquote(args.password), - luci.util.shellquote(args.username) - }) == 0) - } - end - }, - - getBlockDevices = { - call = function() - local fs = require "nixio.fs" - - local block = io.popen("/sbin/block info", "r") - if block then - local rv = {} - - while true do - local ln = block:read("*l") - if not ln then - break - end - - local dev = ln:match("^/dev/(.-):") - if dev then - local s = tonumber((fs.readfile("/sys/class/block/" .. dev .."/size"))) - local e = { - dev = "/dev/" .. dev, - size = s and s * 512 - } - - local key, val = { } - for key, val in ln:gmatch([[(%w+)="(.-)"]]) do - e[key:lower()] = val - end - - rv[dev] = e - end - end - - block:close() - - return rv - else - return { error = "Unable to execute block utility" } - end - end - }, - - setBlockDetect = { - call = function() - return { result = (os.execute("/sbin/block detect > /etc/config/fstab") == 0) } - end - }, - - getMountPoints = { - call = function() - local fs = require "nixio.fs" - - local fd, err = io.open("/proc/mounts", "r") - if fd then - local rv = {} - - while true do - local ln = fd:read("*l") - if not ln then - break - end - - local device, mount, fstype, options, freq, pass = ln:match("^(%S*) (%S*) (%S*) (%S*) (%d+) (%d+)$") - if device and mount then - device = device:gsub("\\(%d+)", function(n) return string.char(tonumber(n, 8)) end) - mount = mount:gsub("\\(%d+)", function(n) return string.char(tonumber(n, 8)) end) - - local stat = fs.statvfs(mount) - if stat and stat.blocks > 0 then - rv[#rv+1] = { - device = device, - mount = mount, - size = stat.bsize * stat.blocks, - avail = stat.bsize * stat.bavail, - free = stat.bsize * stat.bfree - } - end - end - end - - fd:close() - - return { result = rv } - else - return { error = err } - end - end - }, - - getRealtimeStats = { - args = { mode = "interface", device = "eth0" }, - call = function(args) - local util = require "luci.util" - - local flags - if args.mode == "interface" then - flags = "-i %s" % util.shellquote(args.device) - elseif args.mode == "wireless" then - flags = "-r %s" % util.shellquote(args.device) - elseif args.mode == "conntrack" then - flags = "-c" - elseif args.mode == "load" then - flags = "-l" - else - return { error = "Invalid mode" } - end - - local fd, err = io.popen("luci-bwc %s" % flags, "r") - if fd then - local parse = json.new() - local done - - parse:parse("[") - - while true do - local ln = fd:read("*l") - if not ln then - break - end - - done, err = parse:parse((ln:gsub("%d+", "%1.0"))) - - if done then - err = "Unexpected JSON data" - end - - if err then - break - end - end - - fd:close() - - done, err = parse:parse("]") - - if err then - return { error = err } - elseif not done then - return { error = "Incomplete JSON data" } - else - return { result = parse:get() } - end - else - return { error = err } - end - end - }, - - getConntrackList = { - call = function() - local sys = require "luci.sys" - return { result = sys.net.conntrack() } - end - }, - - getProcessList = { - call = function() - local sys = require "luci.sys" - local res = {} - for _, v in pairs(sys.process.list()) do - res[#res + 1] = v - end - return { result = res } - end - }, - - getCPUBench = { - call = function() - local sys = require "luci.sys" - local cpubench = {} - - cpubench.cpubench = sys.exec("cat /etc/bench.log 2>/dev/null") - return cpubench - end - }, - - getCPUInfo = { - call = function() - local sys = require "luci.sys" - local cpuinfo = {} - - cpuinfo.cpuinfo = sys.exec("/sbin/cpuinfo") - - if (cpuinfo.cpuinfo == nil) or (cpuinfo.cpuinfo == "") then - cpuinfo.cpuinfo = "? x ? (2.33MHz, 2.33°C)" - end - - return cpuinfo - end - }, - - getCPUUsage = { - call = function() - local sys = require "luci.sys" - local cpuusage = {} - - cpuusage.cpuusage = sys.exec("busybox top -n1 | awk '/^CPU/ {printf(\"%d%%\", 100 - $8)}'") or "6%" - return cpuusage - end - }, - - getETHInfo = { - call = function() - local sys = require "luci.sys" - local rv = json.parse(sys.exec("/sbin/ethinfo")) or {} - - return { ethinfo = rv } - end - }, - - getOnlineUsers = { - call = function() - local sys = require "luci.sys" - local onlineusers = {} - - onlineusers.onlineusers = sys.exec("cat /proc/net/arp | grep 'br-lan' | grep '0x2' | wc -l") - return onlineusers - - end - }, - - getTempInfo = { - call = function() - local sys = require "luci.sys" - local tempinfo = {} - - tempinfo.tempinfo = sys.exec("/sbin/tempinfo 2>/dev/null") - return tempinfo - end - } +function shellquote(s) { + return `'${replace(s, "'", "'\\''")}'`; } -local function parseInput() - local parse = json.new() - local done, err +const methods = { + getVersion: { + call: function(request) { + return { revision, branch }; + } + }, - 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 + getInitList: { + args: { name: 'name' }, + call: function(request) { + let scripts = {}; - if not done then - print(json.stringify({ error = err or "Incomplete input" })) - os.exit(1) - end + for (let name in filter(init_list(), i => !request.args.name || i == request.args.name)) { + let idx = init_index(name); - return parse:get() -end + scripts[name] = { + index: idx?.[0], + stop: idx?.[1], + enabled: init_enabled(name) + }; + } -local function validateArgs(func, uargs) - local method = methods[func] - if not method then - print(json.stringify({ error = "Method not found" })) - os.exit(1) - end + return length(scripts) ? scripts : { error: 'No such init script' }; + } + }, - if type(uargs) ~= "table" then - print(json.stringify({ error = "Invalid arguments" })) - os.exit(1) - end + setInitAction: { + args: { name: 'name', action: 'action' }, + call: function(request) { + switch (request.args.action) { + case 'enable': + case 'disable': + case 'start': + case 'stop': + case 'restart': + case 'reload': + const rc = init_action(request.args.name, request.args.action); - uargs.ubus_rpc_session = nil + if (rc === false) + return { error: 'No such init script' }; - 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 { result: rc == 0 }; - return method -end + default: + return { error: 'Invalid action' }; + } + } + }, -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 + getLocaltime: { + call: function(request) { + return { result: time() }; + } + }, + + setLocaltime: { + args: { localtime: 0 }, + call: function(request) { + let t = localtime(request.args.localtime); + + if (t) { + system(sprintf('date -s "%04d-%02d-%02d %02d:%02d:%02d" >/dev/null', t.year, t.mon, t.mday, t.hour, t.min, t.sec)); + system('/etc/init.d/sysfixtime restart >/dev/null'); + } + + return { result: request.args.localtime }; + } + }, + + getTimezones: { + call: function(request) { + let tz = trim(readfile('/etc/TZ')); + let zn = cursor()?.get?.('system', '@system[0]', 'zonename'); + let result = {}; + + for (let zone, tzstring in timezones) { + result[zone] = { tzstring }; + + if (zn == zone) + result[zone].active = true; + }; + + return result; + } + }, + + getLEDs: { + call: function() { + let result = {}; + + for (let led in lsdir('/sys/class/leds')) { + let s; + + result[led] = { triggers: [] }; + + s = trim(readfile(`/sys/class/leds/${led}/trigger`)); + for (let trigger in split(s, ' ')) { + push(result[led].triggers, trim(trigger, '[]')); + + if (trigger != result[led].triggers[-1]) + result[led].active_trigger = result[led].triggers[-1]; + } + + s = readfile(`/sys/class/leds/${led}/brightness`); + result[led].brightness = +s; + + s = readfile(`/sys/class/leds/${led}/max_brightness`); + result[led].max_brightness = +s; + } + + return result; + } + }, + + getUSBDevices: { + call: function() { + let result = { devices: [], ports: [] }; + + for (let path in glob('/sys/bus/usb/devices/[0-9]*/manufacturer')) { + let id = basename(dirname(path)); + + push(result.devices, { + id, + vid: trim(readfile(`/sys/bus/usb/devices/${id}/idVendor`)), + pid: trim(readfile(`/sys/bus/usb/devices/${id}/idProduct`)), + vendor: trim(readfile(path)), + product: trim(readfile(`/sys/bus/usb/devices/${id}/product`)), + speed: +readfile(`/sys/bus/usb/devices/${id}/speed`) + }); + } + + for (let path in glob('/sys/bus/usb/devices/*/*-port[0-9]*')) { + let port = basename(path); + let link = readlink(`${path}/device`); + + push(result.ports, { + port, + device: basename(link) + }); + } + + return result; + } + }, + + getConntrackHelpers: { + call: function() { + const uci = cursor(); + let helpers = []; + let package; + + if (uci.load('/usr/share/firewall4/helpers')) + package = 'helpers'; + else if (uci.load('/usr/share/fw3/helpers.conf')) + package = 'helpers.conf'; + + if (package) { + uci.foreach(package, 'helper', (s) => { + push(helpers, { + name: s.name, + description: s.description, + module: s.module, + family: s.family, + proto: s.proto, + port: s.port + }); + }); + } + + return { result: helpers }; + } + }, + + getFeatures: { + call: function() { + let result = { + firewall: access('/sbin/fw3') == true, + firewall4: access('/sbin/fw4') == true, + opkg: access('/bin/opkg') == true, + offloading: access('/sys/module/xt_FLOWOFFLOAD/refcnt') == true || access('/sys/module/nft_flow_offload/refcnt') == true, + fullcone: access('/sys/module/xt_FULLCONENAT/refcnt') == true || access('/sys/module/nft_fullcone/refcnt') == true, + br2684ctl: access('/usr/sbin/br2684ctl') == true, + swconfig: access('/sbin/swconfig') == true, + odhcpd: access('/usr/sbin/odhcpd') == true, + zram: access('/sys/class/zram-control') == true, + sysntpd: readlink('/usr/sbin/ntpd') != null, + ipv6: access('/proc/net/ipv6_route') == true, + dropbear: access('/usr/sbin/dropbear') == true, + cabundle: access('/etc/ssl/certs/ca-certificates.crt') == true, + relayd: access('/usr/sbin/relayd') == true, + }; + + const wifi_features = [ 'eap', '11ac', '11ax', '11r', 'acs', 'sae', 'owe', 'suiteb192', 'wep', 'wps' ]; + + if (access('/usr/sbin/hostapd')) { + result.hostapd = { cli: access('/usr/sbin/hostapd_cli') == true }; + + for (let feature in wifi_features) + result.hostapd[feature] = system(`/usr/sbin/hostapd -v${feature} >/dev/null 2>/dev/null`) == 0; + } + + if (access('/usr/sbin/wpa_supplicant')) { + result.wpasupplicant = { cli: access('/usr/sbin/wpa_cli') == true }; + + for (let feature in wifi_features) + result.wpasupplicant[feature] = system(`/usr/sbin/wpa_supplicant -v${feature} >/dev/null 2>/dev/null`) == 0; + } + + let fd = popen('dnsmasq --version 2>/dev/null'); + + if (fd) { + const m = match(fd.read('all'), /^Compile time options: (.+)$/s); + + for (let opt in split(m?.[1], ' ')) { + let f = replace(opt, 'no-', '', 1); + + result.dnsmasq ??= {}; + result.dnsmasq[lc(f)] = (f == opt); + } + + fd.close(); + } + + fd = popen('ipset --help 2>/dev/null'); + + if (fd) { + for (let line = fd.read('line'), flag = false; length(line); line = fd.read('line')) { + if (line == 'Supported set types:\n') { + flag = true; + } + else if (flag) { + const m = match(line, /^ +([\w:,]+)\t+([0-9]+)\t/); + + if (m) { + result.ipset ??= {}; + result.ipset[m[1]] ??= +m[2]; + } + } + } + + fd.close(); + } + + return result; + } + }, + + getSwconfigFeatures: { + args: { switch: 'switch0' }, + call: function(request) { + // Parse some common switch properties from swconfig help output. + const swc = popen(`swconfig dev ${shellquote(request.args.switch)} help 2>/dev/null`); + + if (swc) { + let is_port_attr = false; + let is_vlan_attr = false; + let result = {}; + + for (let line = swc.read('line'); length(line); line = swc.read('line')) { + if (match(line, /^\s+--vlan/)) { + is_vlan_attr = true; + } + else if (match(line, /^\s+--port/)) { + is_vlan_attr = false; + is_port_attr = true; + } + else if (match(line, /cpu @/)) { + result.switch_title = match(line, /^switch[0-9]+: \w+\((.+)\)/)?.[1]; + result.num_vlans = match(line, /vlans: ([0-9]+)/)?.[1] ?? 16; + result.min_vid = 1; + } + else if (match(line, /: (pvid|tag|vid)/)) { + if (is_vlan_attr) + result.vid_option = match(line, /: (\w+)/)?.[1]; + } + else if (match(line, /: enable_vlan4k/)) { + result.vlan4k_option = 'enable_vlan4k'; + } + else if (match(line, /: enable_vlan/)) { + result.vlan_option = 'enable_vlan'; + } + else if (match(line, /: enable_learning/)) { + result.learning_option = 'enable_learning'; + } + else if (match(line, /: enable_mirror_rx/)) { + result.mirror_option = 'enable_mirror_rx'; + } + else if (match(line, /: max_length/)) { + result.jumbo_option = 'max_length'; + } + } + + swc.close(); + + if (!length(result)) + return { error: 'No such switch' }; + + return result; + } + else { + return { error: error() }; + } + } + }, + + getSwconfigPortState: { + args: { switch: 'switch0' }, + call: function(request) { + const swc = popen(`swconfig dev ${shellquote(request.args.switch)} show 2>/dev/null`); + + if (swc) { + let ports = [], port; + + for (let line = swc.read('line'); length(line); line = swc.read('line')) { + if (match(line, /^VLAN [0-9]+:/) && length(ports)) + break; + + let pnum = match(line, /^Port ([0-9]+):/)?.[1]; + + if (pnum) { + port = { + port: +pnum, + duplex: false, + speed: 0, + link: false, + auto: false, + rxflow: false, + txflow: false + }; + + push(ports, port); + } + + if (port) { + let m; + + if (match(line, /full[ -]duplex/)) + port.duplex = true; + + if ((m = match(line, / speed:([0-9]+)/)) != null) + port.speed = +m[1]; + + if ((m = match(line, /([0-9]+) Mbps/)) != null && !port.speed) + port.speed = +m[1]; + + if ((m = match(line, /link: ([0-9]+)/)) != null && !port.speed) + port.speed = +m[1]; + + if (match(line, /(link|status): ?up/)) + port.link = true; + + if (match(line, /auto-negotiate|link:.*auto/)) + port.auto = true; + + if (match(line, /link:.*rxflow/)) + port.rxflow = true; + + if (match(line, /link:.*txflow/)) + port.txflow = true; + } + } + + swc.close(); + + if (!length(ports)) + return { error: 'No such switch' }; + + return { result: ports }; + } + else { + return { error: error() }; + } + } + }, + + setPassword: { + args: { username: 'root', password: 'password' }, + call: function(request) { + const u = shellquote(request.args.username); + const p = shellquote(request.args.password); + + return { + result: system(`(echo ${p}; sleep 1; echo ${p}) | /bin/busybox passwd ${u} >/dev/null 2>&1`) == 0 + }; + } + }, + + getBlockDevices: { + call: function() { + const block = popen('/sbin/block info 2>/dev/null'); + + if (block) { + let result = {}; + + for (let line = block.read('line'); length(line); line = block.read('line')) { + let dev = match(line, /^\/dev\/([^:]+):/)?.[1]; + + if (dev) { + let e = result[dev] = { + dev: `/dev/${dev}`, + size: +readfile(`/sys/class/block/${dev}/size`) * 512 + }; + + for (let m in match(line, / (\w+)="([^"]+)"/g)) + e[lc(m[1])] = m[2]; + } + } + + block.close(); + + const swaps = open('/proc/swaps', 'r'); + + if (swaps) { + for (let line = swaps.read('line'); length(line); line = swaps.read('line')) { + let m = match(line, /^(\/\S+)\s+\S+\s+(\d+)/); + + if (m) { + let dev = replace(m[1], /\\(\d\d\d)/g, (_, n) => chr(int(n, 8))); + + result[`swap:${m[1]}`] = { + dev, + type: 'swap', + size: +m[2] * 1024 + }; + } + } + + swaps.close(); + } + + return result; + } + else { + return { error: 'Unable to execute block utility' }; + } + } + }, + + setBlockDetect: { + call: function() { + return { result: system('/sbin/block detect > /etc/config/fstab') == 0 }; + } + }, + + getMountPoints: { + call: function() { + const fd = open('/proc/mounts', 'r'); + + if (fd) { + let result = []; + + for (let line = fd.read('line'); length(line); line = fd.read('line')) { + const m = split(line, ' '); + const device = replace(m[0], /\\([0-9][0-9][0-9])/g, (m, n) => char(int(n, 8))); + const mount = replace(m[1], /\\([0-9][0-9][0-9])/g, (m, n) => char(int(n, 8))); + const stat = statvfs(mount); + + if (stat?.blocks > 0) { + push(result, { + device, mount, + size: stat.bsize * stat.blocks, + avail: stat.bsize * stat.bavail, + free: stat.bsize * stat.bfree + }); + } + } + + fd.close(); + + return { result }; + } + else { + return { error: error() }; + } + } + }, + getRealtimeStats: { + args: { mode: 'interface', device: 'eth0' }, + call: function(request) { + let flags; + + if (request.args.mode == 'interface') + flags = `-i ${shellquote(request.args.device)}`; + else if (request.args.mode == 'wireless') + flags = `-r ${shellquote(request.args.device)}`; + else if (request.args.mode == 'conntrack') + flags = '-c'; + else if (request.args.mode == 'load') + flags = '-l'; + else + return { error: 'Invalid mode' }; + + const fd = popen(`luci-bwc ${flags}`, 'r'); + + if (fd) { + let result; + + try { + result = { result: json(`[${fd.read('all')}]`) }; + } + catch (err) { + result = { error: err }; + } + + return result; + } + else { + return { error: error() }; + } + } + }, + + getConntrackList: { + call: function() { + return { result: conntrack_list() }; + } + }, + + getProcessList: { + call: function() { + return { result: process_list() }; + } + }, + + getCPUBench: { + call: function() { + return { cpubench: readfile('/etc/bench.log') || '' }; + } + }, + + getCPUInfo: { + call: function() { + if (!access('/sbin/cpuinfo')) + return {}; + + const fd = popen('/sbin/cpuinfo'); + if (fd) { + let cpuinfo = fd.read('all'); + if (!cpuinfo) + cpuinfo = '?'; + fd.close(); + + return { cpuinfo: cpuinfo }; + } else { + return { cpuinfo: error() }; + } + } + }, + + getCPUUsage: { + call: function() { + const fd = popen('busybox top -n1 | awk \'/^CPU/ {printf("%d%", 100 - $8)}\''); + if (fd) { + let cpuusage = fd.read('all'); + if (!cpuusage) + cpuusage = '?'; + fd.close(); + + return { cpuusage: cpuusage }; + } else { + return { cpuusage: error() }; + } + } + }, + + getETHInfo: { + call: function() { + if (!access('/sbin/ethinfo')) + return {}; + + const fd = popen('/sbin/ethinfo'); + if (fd) { + let ethinfo = fd.read('all'); + if (!ethinfo) + ethinfo = '{}'; + ethinfo = json(ethinfo); + fd.close(); + + return { ethinfo: ethinfo }; + } else { + return { ethinfo: error() }; + } + } + }, + + getTempInfo: { + call: function() { + if (!access('/sbin/tempinfo')) + return {}; + + const fd = popen('/sbin/tempinfo'); + if (fd) { + let tempinfo = fd.read('all'); + if (!tempinfo) + tempinfo = '?'; + fd.close(); + + return { tempinfo: tempinfo }; + } else { + return { tempinfo: error() }; + } + } + }, + + getOnlineUsers: { + call: function() { + const fd = open('/proc/net/arp', 'r'); + if (fd) { + let onlineusers = 0; + + for (let line = fd.read('line'); length(line); line = fd.read('line')) + if (match(trim(line), /^.*(0x2).*(br-lan)$/)) + onlineusers++; + + fd.close(); + + return { onlineusers: onlineusers }; + } else { + return { onlineusers: error() }; + } + } + } +}; + +return { luci: methods };