1 // Copyright 2022 Jo-Philipp Wich <jo@mein.io>
2 // Licensed to the public under the Apache License 2.0.
6 import { stdin, access, dirname, basename, open, popen, glob, lsdir, readfile, readlink, error } from 'fs';
7 import { cursor } from 'uci';
9 import { init_list, init_index, init_enabled, init_action, conntrack_list, process_list } from 'luci.sys';
10 import { statvfs } from 'luci.core';
12 import timezones from 'luci.zoneinfo';
15 function shellquote(s) {
16 return `'${replace(s, "'", "'\\''")}'`;
21 args: { name: 'name' },
22 call: function(request) {
25 for (let name in filter(init_list(), i => !request.args.name || i == request.args.name)) {
26 let idx = init_index(name);
31 enabled: init_enabled(name)
35 return length(scripts) ? scripts : { error: 'No such init script' };
40 args: { name: 'name', action: 'action' },
41 call: function(request) {
42 switch (request.args.action) {
49 const rc = init_action(request.args.name, request.args.action);
52 return { error: 'No such init script' };
54 return { result: rc == 0 };
57 return { error: 'Invalid action' };
63 call: function(request) {
64 return { result: time() };
69 args: { localtime: 0 },
70 call: function(request) {
71 let t = localtime(request.args.localtime);
74 system(sprintf('date -s "%04d-%02d-%02d %02d:%02d:%02d" >/dev/null', t.year, t.mon, t.mday, t.hour, t.min, t.sec));
75 system('/etc/init.d/sysfixtime restart >/dev/null');
78 return { result: request.args.localtime };
83 call: function(request) {
84 let tz = trim(readfile('/etc/TZ'));
85 let zn = cursor()?.get?.('system', '@system[0]', 'zonename');
88 for (let zone, tzstring in timezones) {
89 result[zone] = { tzstring };
92 result[zone].active = true;
103 for (let led in lsdir('/sys/class/leds')) {
106 result[led] = { triggers: [] };
108 s = trim(readfile(`/sys/class/leds/${led}/trigger`));
109 for (let trigger in split(s, ' ')) {
110 push(result[led].triggers, trim(trigger, '[]'));
112 if (trigger != result[led].triggers[-1])
113 result[led].active_trigger = result[led].triggers[-1];
116 s = readfile(`/sys/class/leds/${led}/brightness`);
117 result[led].brightness = +s;
119 s = readfile(`/sys/class/leds/${led}/max_brightness`);
120 result[led].max_brightness = +s;
129 let result = { devices: [], ports: [] };
131 for (let path in glob('/sys/bus/usb/devices/[0-9]*/manufacturer')) {
132 let id = basename(dirname(path));
134 push(result.devices, {
136 vid: trim(readfile(`/sys/bus/usb/devices/${id}/idVendor`)),
137 pid: trim(readfile(`/sys/bus/usb/devices/${id}/idProduct`)),
138 vendor: trim(readfile(path)),
139 product: trim(readfile(`/sys/bus/usb/devices/${id}/product`)),
140 speed: +readfile(`/sys/bus/usb/devices/${id}/speed`)
144 for (let path in glob('/sys/bus/usb/devices/*/*-port[0-9]*')) {
145 let port = basename(path);
146 let link = readlink(`${path}/device`);
150 device: basename(link)
158 getConntrackHelpers: {
160 const uci = cursor();
163 uci.load('/usr/share/firewall4/helpers');
164 uci.load('/usr/share/fw3/helpers.conf');
166 uci.foreach('helpers', 'helper', (s) => {
169 description: s.description,
177 return { result: helpers };
184 firewall: access('/sbin/fw3') == true,
185 firewall4: access('/sbin/fw4') == true,
186 opkg: access('/bin/opkg') == true,
187 offloading: access('/sys/module/xt_FLOWOFFLOAD/refcnt') == true || access('/sys/module/nft_flow_offload/refcnt') == true,
188 br2684ctl: access('/usr/sbin/br2684ctl') == true,
189 swconfig: access('/sbin/swconfig') == true,
190 odhcpd: access('/usr/sbin/odhcpd') == true,
191 zram: access('/sys/class/zram-control') == true,
192 sysntpd: readlink('/usr/sbin/ntpd') != null,
193 ipv6: access('/proc/net/ipv6_route') == true,
194 dropbear: access('/usr/sbin/dropbear') == true,
195 cabundle: access('/etc/ssl/certs/ca-certificates.crt') == true,
196 relayd: access('/usr/sbin/relayd') == true,
199 const wifi_features = [ 'eap', '11n', '11ac', '11r', 'acs', 'sae', 'owe', 'suiteb192', 'wep', 'wps' ];
201 if (access('/usr/sbin/hostapd')) {
202 result.hostapd = { cli: access('/usr/sbin/hostapd_cli') == true };
204 for (let feature in wifi_features)
205 result.hostapd[feature] = system(`/usr/sbin/hostapd -v${feature} >/dev/null 2>/dev/null`) == 0;
208 if (access('/usr/sbin/wpa_supplicant')) {
209 result.wpasupplicant = { cli: access('/usr/sbin/wpa_cli') == true };
211 for (let feature in wifi_features)
212 result.wpasupplicant[feature] = system(`/usr/sbin/wpa_supplicant -v${feature} >/dev/null 2>/dev/null`) == 0;
215 let fd = popen('dnsmasq --version 2>/dev/null');
218 const m = match(fd.read('all'), /^Compile time options: (.+)$/s);
220 for (let opt in split(m?.[1], ' ')) {
221 let f = replace(opt, 'no-', '', 1);
223 result.dnsmasq ??= {};
224 result.dnsmasq[lc(f)] = (f == opt);
230 fd = popen('ipset --help 2>/dev/null');
233 for (let line = fd.read('line'), flag = false; length(line); line = fd.read('line')) {
234 if (line == 'Supported set types:\n') {
238 const m = match(line, /^ +([\w:,]+)\t+([0-9]+)\t/);
242 result.ipset[m[1]] ??= +m[2];
254 getSwconfigFeatures: {
255 args: { switch: 'switch0' },
256 call: function(request) {
257 // Parse some common switch properties from swconfig help output.
258 const swc = popen(`swconfig dev ${shellquote(request.args.switch)} help 2>/dev/null`);
261 let is_port_attr = false;
262 let is_vlan_attr = false;
265 for (let line = swc.read('line'); length(line); line = swc.read('line')) {
266 if (match(line, /^\s+--vlan/)) {
269 else if (match(line, /^\s+--port/)) {
270 is_vlan_attr = false;
273 else if (match(line, /cpu @/)) {
274 result.switch_title = match(line, /^switch[0-9]+: \w+\((.+)\)/)?.[1];
275 result.num_vlans = match(line, /vlans: ([0-9]+)/)?.[1] ?? 16;
278 else if (match(line, /: (pvid|tag|vid)/)) {
280 result.vid_option = match(line, /: (\w+)/)?.[1];
282 else if (match(line, /: enable_vlan4k/)) {
283 result.vlan4k_option = 'enable_vlan4k';
285 else if (match(line, /: enable_vlan/)) {
286 result.vlan_option = 'enable_vlan';
288 else if (match(line, /: enable_learning/)) {
289 result.learning_option = 'enable_learning';
291 else if (match(line, /: enable_mirror_rx/)) {
292 result.mirror_option = 'enable_mirror_rx';
294 else if (match(line, /: max_length/)) {
295 result.jumbo_option = 'max_length';
302 return { error: 'No such switch' };
307 return { error: error() };
312 getSwconfigPortState: {
313 args: { switch: 'switch0' },
314 call: function(request) {
315 const swc = popen(`swconfig dev ${shellquote(request.args.switch)} show 2>/dev/null`);
318 let ports = [], port;
320 for (let line = swc.read('line'); length(line); line = swc.read('line')) {
321 if (match(line, /^VLAN [0-9]+:/) && length(ports))
324 let pnum = match(line, /^Port ([0-9]+):/)?.[1];
343 if (match(line, /full[ -]duplex/))
346 if ((m = match(line, / speed:([0-9]+)/)) != null)
349 if ((m = match(line, /([0-9]+) Mbps/)) != null && !port.speed)
352 if ((m = match(line, /link: ([0-9]+)/)) != null && !port.speed)
355 if (match(line, /(link|status): ?up/))
358 if (match(line, /auto-negotiate|link:.*auto/))
361 if (match(line, /link:.*rxflow/))
364 if (match(line, /link:.*txflow/))
372 return { error: 'No such switch' };
374 return { result: ports };
377 return { error: error() };
383 args: { username: 'root', password: 'password' },
384 call: function(request) {
385 const u = shellquote(request.args.username);
386 const p = shellquote(request.args.password);
389 result: system(`(echo ${p}; sleep 1; echo ${p}) | /bin/busybox passwd ${u} >/dev/null 2>&1`) == 0
396 const block = popen('/sbin/block info 2>/dev/null');
401 for (let line = block.read('line'); length(line); line = block.read('line')) {
402 let dev = match(line, /^\/dev\/([^:]+):/)?.[1];
405 let e = result[dev] = {
407 size: +readfile(`/sys/class/block/${dev}/size`) * 512
410 for (m in match(line, / (\w+)="([^"]+)"/g))
420 return { error: 'Unable to execute block utility' };
427 return { result: system('/sbin/block detect > /etc/config/fstab') == 0 };
433 const fd = open('/proc/mounts', 'r');
438 for (let line = fd.read('line'); length(line); line = fd.read('line')) {
439 const m = split(line, ' ');
440 const device = replace(m[0], /\\([0-9][0-9][0-9])/g, (m, n) => char(int(n, 8)));
441 const mount = replace(m[1], /\\([0-9][0-9][0-9])/g, (m, n) => char(int(n, 8)));
442 const stat = statvfs(mount);
444 if (stat?.blocks > 0) {
447 size: stat.bsize * stat.blocks,
448 avail: stat.bsize * stat.bavail,
449 free: stat.bsize * stat.bfree
459 return { error: error() };
464 args: { mode: 'interface', device: 'eth0' },
465 call: function(request) {
468 if (request.args.mode == 'interface')
469 flags = `-i ${shellquote(request.args.device)}`;
470 else if (request.args.mode == 'wireless')
471 flags = `-r ${shellquote(request.args.device)}`;
472 else if (request.args.mode == 'conntrack')
474 else if (request.args.mode == 'load')
477 return { error: 'Invalid mode' };
479 const fd = popen(`luci-bwc ${flags}`, 'r');
485 result = { result: json(`[${fd.read('all')}]`) };
488 result = { error: err };
494 return { error: error() };
501 return { result: conntrack_list() };
507 return { result: process_list() };
512 return { luci: methods };