sbwml 2371bd5c34 adapt openwrt-24.10
Signed-off-by: sbwml <admin@cooluc.com>
2024-10-05 04:24:14 +08:00

690 lines
16 KiB
Plaintext
Executable File

// 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,
fullcone: access('/sys/module/xt_FULLCONENAT/refcnt') == true || access('/sys/module/nft_fullcone/refcnt') == true,
shortcutfe: access('/sys/module/shortcut_fe/refcnt') == true,
natflow: access('/dev/natflow_ctl') == 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', 'ocv' ];
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 };
}
},
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 };