diff options
Diffstat (limited to 'modules/luci-base/root')
-rw-r--r-- | modules/luci-base/root/etc/config/ucitrack | 2 | ||||
-rwxr-xr-x | modules/luci-base/root/etc/init.d/ucitrack | 6 | ||||
-rwxr-xr-x | modules/luci-base/root/usr/libexec/rpcd/luci | 677 | ||||
-rw-r--r-- | modules/luci-base/root/usr/share/luci/menu.d/luci-base.json | 27 | ||||
-rw-r--r-- | modules/luci-base/root/usr/share/rpcd/ucode/luci | 587 | ||||
-rw-r--r-- | modules/luci-base/root/www/index.html | 25 |
6 files changed, 623 insertions, 701 deletions
diff --git a/modules/luci-base/root/etc/config/ucitrack b/modules/luci-base/root/etc/config/ucitrack index e63986630c..bb4cdbc3c4 100644 --- a/modules/luci-base/root/etc/config/ucitrack +++ b/modules/luci-base/root/etc/config/ucitrack @@ -1,7 +1,6 @@ config network option init network list affects dhcp - list affects radvd config wireless list affects network @@ -54,3 +53,4 @@ config samba config tinyproxy option init tinyproxy + diff --git a/modules/luci-base/root/etc/init.d/ucitrack b/modules/luci-base/root/etc/init.d/ucitrack index c7ceb32905..57ac11857f 100755 --- a/modules/luci-base/root/etc/init.d/ucitrack +++ b/modules/luci-base/root/etc/init.d/ucitrack @@ -41,10 +41,10 @@ register_trigger() { fi for affected in $affects; do - logger -t "ucitrack" "Setting up /etc/config/$config reload dependency on /etc/config/$affected" - procd_add_config_trigger "config.change" "$affected" \ + logger -t "ucitrack" "Setting up /etc/config/$affected reload dependency on /etc/config/$config" + procd_add_config_trigger "config.change" "$config" \ ubus call service event \ - "$(printf '{"type":"config.change","data":{"package":"%s"}}' $config)" + "$(printf '{"type":"config.change","data":{"package":"%s"}}' $affected)" done } diff --git a/modules/luci-base/root/usr/libexec/rpcd/luci b/modules/luci-base/root/usr/libexec/rpcd/luci deleted file mode 100755 index 7f0c0b226c..0000000000 --- a/modules/luci-base/root/usr/libexec/rpcd/luci +++ /dev/null @@ -1,677 +0,0 @@ -#!/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 - }, - - 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") - rv.dropbear = fs.access("/usr/sbin/dropbear") - rv.cabundle = fs.access("/etc/ssl/certs/ca-certificates.crt") - rv.relayd = fs.access("/usr/sbin/relayd") - - local wifi_features = { "eap", "11n", "11ac", "11r", "11w", "acs", "sae", "owe", "suiteb192", "wep" } - - 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) | 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 - } -} - -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 diff --git a/modules/luci-base/root/usr/share/luci/menu.d/luci-base.json b/modules/luci-base/root/usr/share/luci/menu.d/luci-base.json index eb72c565dc..e6deb1fddb 100644 --- a/modules/luci-base/root/usr/share/luci/menu.d/luci-base.json +++ b/modules/luci-base/root/usr/share/luci/menu.d/luci-base.json @@ -7,7 +7,7 @@ "recurse": true }, "auth": { - "methods": [ "cookie:sysauth" ], + "methods": [ "cookie:sysauth_https", "cookie:sysauth_http" ], "login": true } }, @@ -61,7 +61,7 @@ "admin/translations/*": { "action": { - "type": "call", + "type": "function", "module": "luci.controller.admin.index", "function": "action_translations" }, @@ -70,7 +70,7 @@ "admin/ubus/*": { "action": { - "type": "call", + "type": "function", "module": "luci.controller.admin.index", "function": "action_ubus" }, @@ -78,16 +78,17 @@ }, "admin/logout": { - "title": "Logout", + "title": "Log out", "order": 999, "action": { - "type": "call", + "type": "function", "module": "luci.controller.admin.index", "function": "action_logout" }, "depends": { "acl": [ "luci-base" ] - } + }, + "firstchild_ineligible": true }, "admin/uci": { @@ -98,7 +99,7 @@ "admin/uci/revert": { "action": { - "type": "call", + "type": "function", "module": "luci.controller.admin.uci", "function": "action_revert", "post": true @@ -108,33 +109,33 @@ "admin/uci/apply_rollback": { "cors": true, "action": { - "type": "call", + "type": "function", "module": "luci.controller.admin.uci", "function": "action_apply_rollback", "post": true }, "auth": { - "methods": [ "cookie:sysauth" ] + "methods": [ "cookie:sysauth_https", "cookie:sysauth_http" ] } }, "admin/uci/apply_unchecked": { "cors": true, "action": { - "type": "call", + "type": "function", "module": "luci.controller.admin.uci", "function": "action_apply_unchecked", "post": true }, "auth": { - "methods": [ "cookie:sysauth" ] + "methods": [ "cookie:sysauth_https", "cookie:sysauth_http" ] } }, "admin/uci/confirm": { "cors": true, "action": { - "type": "call", + "type": "function", "module": "luci.controller.admin.uci", "function": "action_confirm" }, @@ -143,7 +144,7 @@ "admin/menu": { "action": { - "type": "call", + "type": "function", "module": "luci.controller.admin.index", "function": "action_menu" }, diff --git a/modules/luci-base/root/usr/share/rpcd/ucode/luci b/modules/luci-base/root/usr/share/rpcd/ucode/luci new file mode 100644 index 0000000000..3c4fea4691 --- /dev/null +++ b/modules/luci-base/root/usr/share/rpcd/ucode/luci @@ -0,0 +1,587 @@ +// Copyright 2022 Jo-Philipp Wich <jo@mein.io> +// Licensed to the public under the Apache License 2.0. + +'use strict'; + +import { stdin, access, dirname, basename, open, popen, glob, lsdir, readfile, readlink, error } from 'fs'; +import { cursor } from 'uci'; + +import { init_list, init_index, init_enabled, init_action, conntrack_list, process_list } from 'luci.sys'; +import { revision, branch } from 'luci.version'; +import { statvfs, uname } from 'luci.core'; + +import timezones from 'luci.zoneinfo'; + + +function shellquote(s) { + return `'${replace(s, "'", "'\\''")}'`; +} + +const methods = { + getVersion: { + call: function(request) { + return { revision, branch }; + } + }, + + getInitList: { + args: { name: 'name' }, + call: function(request) { + let scripts = {}; + + for (let name in filter(init_list(), i => !request.args.name || i == request.args.name)) { + let idx = init_index(name); + + scripts[name] = { + index: idx?.[0], + stop: idx?.[1], + enabled: init_enabled(name) + }; + } + + return length(scripts) ? scripts : { error: 'No such init script' }; + } + }, + + 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); + + if (rc === false) + return { error: 'No such init script' }; + + return { result: rc == 0 }; + + default: + return { error: 'Invalid action' }; + } + } + }, + + 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, + 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() }; + } + }, + + getBuiltinEthernetPorts: { + call: function() { + let fd = open('/etc/board.json', 'r'); + let board = fd ? json(fd) : {}; + let ports = []; + + for (let k in [ 'lan', 'wan' ]) { + if (!board?.network?.[k]) + continue; + + if (type(board.network[k].ports) == 'array') { + for (let ifname in board.network[k].ports) { + push(ports, { role: k, device: ifname }); + } + } + else if (type(board.network[k].device) == 'string') { + push(ports, { role: k, device: board.network[k].device }); + } + } + + /* Workaround for targets that do not enumerate all netdevs in board.json */ + if (uname().machine in [ 'x86_64' ] && + match(ports[0]?.device, /^eth\d+$/)) { + let bus = readlink(`/sys/class/net/${ports[0].device}/device/subsystem`); + + for (let netdev in lsdir('/sys/class/net')) { + if (!match(netdev, /^eth\d+$/)) + continue; + + if (length(filter(ports, port => port.device == netdev))) + continue; + + if (readlink(`/sys/class/net/${netdev}/device/subsystem`) != bus) + continue; + + push(ports, { role: 'unknown', device: netdev }); + } + } + + return { result: ports }; + } + } +}; + +return { luci: methods }; diff --git a/modules/luci-base/root/www/index.html b/modules/luci-base/root/www/index.html index 35c0ea0d32..d5f7d7209f 100644 --- a/modules/luci-base/root/www/index.html +++ b/modules/luci-base/root/www/index.html @@ -1,11 +1,22 @@ <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> -<head> -<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> -<meta http-equiv="refresh" content="0; URL=cgi-bin/luci/" /> -</head> -<body style="background-color: white"> -<a style="color: black; font-family: arial, helvetica, sans-serif;" href="cgi-bin/luci/">LuCI - Lua Configuration Interface</a> -</body> + <head> + <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" /> + <meta http-equiv="Pragma" content="no-cache" /> + <meta http-equiv="Expires" content="0" /> + <meta http-equiv="refresh" content="0; URL=cgi-bin/luci/" /> + <style type="text/css"> + body { background: white; font-family: arial, helvetica, sans-serif; } + a { color: black; } + + @media (prefers-color-scheme: dark) { + body { background: black; } + a { color: white; } + } + </style> + </head> + <body> + <a href="cgi-bin/luci/">LuCI - Lua Configuration Interface</a> + </body> </html> |