summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-base/root
diff options
context:
space:
mode:
Diffstat (limited to 'modules/luci-base/root')
-rw-r--r--modules/luci-base/root/etc/config/ucitrack2
-rwxr-xr-xmodules/luci-base/root/etc/init.d/ucitrack6
-rwxr-xr-xmodules/luci-base/root/usr/libexec/rpcd/luci677
-rw-r--r--modules/luci-base/root/usr/share/luci/menu.d/luci-base.json27
-rw-r--r--modules/luci-base/root/usr/share/rpcd/ucode/luci587
-rw-r--r--modules/luci-base/root/www/index.html25
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>