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 { revision, branch } from 'luci.version';
11 import { statvfs } from 'luci.core';
13 import timezones from 'luci.zoneinfo';
16 function shellquote(s) {
17 return `'${replace(s, "'", "'\\''")}'`;
22 call: function(request) {
23 return { revision, branch };
28 args: { name: 'name' },
29 call: function(request) {
32 for (let name in filter(init_list(), i => !request.args.name || i == request.args.name)) {
33 let idx = init_index(name);
38 enabled: init_enabled(name)
42 return length(scripts) ? scripts : { error: 'No such init script' };
47 args: { name: 'name', action: 'action' },
48 call: function(request) {
49 switch (request.args.action) {
56 const rc = init_action(request.args.name, request.args.action);
59 return { error: 'No such init script' };
61 return { result: rc == 0 };
64 return { error: 'Invalid action' };
70 call: function(request) {
71 return { result: time() };
76 args: { localtime: 0 },
77 call: function(request) {
78 let t = localtime(request.args.localtime);
81 system(sprintf('date -s "%04d-%02d-%02d %02d:%02d:%02d" >/dev/null', t.year, t.mon, t.mday, t.hour, t.min, t.sec));
82 system('/etc/init.d/sysfixtime restart >/dev/null');
85 return { result: request.args.localtime };
90 call: function(request) {
91 let tz = trim(readfile('/etc/TZ'));
92 let zn = cursor()?.get?.('system', '@system[0]', 'zonename');
95 for (let zone, tzstring in timezones) {
96 result[zone] = { tzstring };
99 result[zone].active = true;
110 for (let led in lsdir('/sys/class/leds')) {
113 result[led] = { triggers: [] };
115 s = trim(readfile(`/sys/class/leds/${led}/trigger`));
116 for (let trigger in split(s, ' ')) {
117 push(result[led].triggers, trim(trigger, '[]'));
119 if (trigger != result[led].triggers[-1])
120 result[led].active_trigger = result[led].triggers[-1];
123 s = readfile(`/sys/class/leds/${led}/brightness`);
124 result[led].brightness = +s;
126 s = readfile(`/sys/class/leds/${led}/max_brightness`);
127 result[led].max_brightness = +s;
136 let result = { devices: [], ports: [] };
138 for (let path in glob('/sys/bus/usb/devices/[0-9]*/manufacturer')) {
139 let id = basename(dirname(path));
141 push(result.devices, {
143 vid: trim(readfile(`/sys/bus/usb/devices/${id}/idVendor`)),
144 pid: trim(readfile(`/sys/bus/usb/devices/${id}/idProduct`)),
145 vendor: trim(readfile(path)),
146 product: trim(readfile(`/sys/bus/usb/devices/${id}/product`)),
147 speed: +readfile(`/sys/bus/usb/devices/${id}/speed`)
151 for (let path in glob('/sys/bus/usb/devices/*/*-port[0-9]*')) {
152 let port = basename(path);
153 let link = readlink(`${path}/device`);
157 device: basename(link)
165 getConntrackHelpers: {
167 const uci = cursor();
171 if (uci.load('/usr/share/firewall4/helpers'))
173 else if (uci.load('/usr/share/fw3/helpers.conf'))
174 package = 'helpers.conf';
177 uci.foreach(package, 'helper', (s) => {
180 description: s.description,
189 return { result: helpers };
196 firewall: access('/sbin/fw3') == true,
197 firewall4: access('/sbin/fw4') == true,
198 opkg: access('/bin/opkg') == true,
199 offloading: access('/sys/module/xt_FLOWOFFLOAD/refcnt') == true || access('/sys/module/nft_flow_offload/refcnt') == true,
200 br2684ctl: access('/usr/sbin/br2684ctl') == true,
201 swconfig: access('/sbin/swconfig') == true,
202 odhcpd: access('/usr/sbin/odhcpd') == true,
203 zram: access('/sys/class/zram-control') == true,
204 sysntpd: readlink('/usr/sbin/ntpd') != null,
205 ipv6: access('/proc/net/ipv6_route') == true,
206 dropbear: access('/usr/sbin/dropbear') == true,
207 cabundle: access('/etc/ssl/certs/ca-certificates.crt') == true,
208 relayd: access('/usr/sbin/relayd') == true,
211 const wifi_features = [ 'eap', '11ac', '11ax', '11r', 'acs', 'sae', 'owe', 'suiteb192', 'wep', 'wps' ];
213 if (access('/usr/sbin/hostapd')) {
214 result.hostapd = { cli: access('/usr/sbin/hostapd_cli') == true };
216 for (let feature in wifi_features)
217 result.hostapd[feature] = system(`/usr/sbin/hostapd -v${feature} >/dev/null 2>/dev/null`) == 0;
220 if (access('/usr/sbin/wpa_supplicant')) {
221 result.wpasupplicant = { cli: access('/usr/sbin/wpa_cli') == true };
223 for (let feature in wifi_features)
224 result.wpasupplicant[feature] = system(`/usr/sbin/wpa_supplicant -v${feature} >/dev/null 2>/dev/null`) == 0;
227 let fd = popen('dnsmasq --version 2>/dev/null');
230 const m = match(fd.read('all'), /^Compile time options: (.+)$/s);
232 for (let opt in split(m?.[1], ' ')) {
233 let f = replace(opt, 'no-', '', 1);
235 result.dnsmasq ??= {};
236 result.dnsmasq[lc(f)] = (f == opt);
242 fd = popen('ipset --help 2>/dev/null');
245 for (let line = fd.read('line'), flag = false; length(line); line = fd.read('line')) {
246 if (line == 'Supported set types:\n') {
250 const m = match(line, /^ +([\w:,]+)\t+([0-9]+)\t/);
254 result.ipset[m[1]] ??= +m[2];
266 getSwconfigFeatures: {
267 args: { switch: 'switch0' },
268 call: function(request) {
269 // Parse some common switch properties from swconfig help output.
270 const swc = popen(`swconfig dev ${shellquote(request.args.switch)} help 2>/dev/null`);
273 let is_port_attr = false;
274 let is_vlan_attr = false;
277 for (let line = swc.read('line'); length(line); line = swc.read('line')) {
278 if (match(line, /^\s+--vlan/)) {
281 else if (match(line, /^\s+--port/)) {
282 is_vlan_attr = false;
285 else if (match(line, /cpu @/)) {
286 result.switch_title = match(line, /^switch[0-9]+: \w+\((.+)\)/)?.[1];
287 result.num_vlans = match(line, /vlans: ([0-9]+)/)?.[1] ?? 16;
290 else if (match(line, /: (pvid|tag|vid)/)) {
292 result.vid_option = match(line, /: (\w+)/)?.[1];
294 else if (match(line, /: enable_vlan4k/)) {
295 result.vlan4k_option = 'enable_vlan4k';
297 else if (match(line, /: enable_vlan/)) {
298 result.vlan_option = 'enable_vlan';
300 else if (match(line, /: enable_learning/)) {
301 result.learning_option = 'enable_learning';
303 else if (match(line, /: enable_mirror_rx/)) {
304 result.mirror_option = 'enable_mirror_rx';
306 else if (match(line, /: max_length/)) {
307 result.jumbo_option = 'max_length';
314 return { error: 'No such switch' };
319 return { error: error() };
324 getSwconfigPortState: {
325 args: { switch: 'switch0' },
326 call: function(request) {
327 const swc = popen(`swconfig dev ${shellquote(request.args.switch)} show 2>/dev/null`);
330 let ports = [], port;
332 for (let line = swc.read('line'); length(line); line = swc.read('line')) {
333 if (match(line, /^VLAN [0-9]+:/) && length(ports))
336 let pnum = match(line, /^Port ([0-9]+):/)?.[1];
355 if (match(line, /full[ -]duplex/))
358 if ((m = match(line, / speed:([0-9]+)/)) != null)
361 if ((m = match(line, /([0-9]+) Mbps/)) != null && !port.speed)
364 if ((m = match(line, /link: ([0-9]+)/)) != null && !port.speed)
367 if (match(line, /(link|status): ?up/))
370 if (match(line, /auto-negotiate|link:.*auto/))
373 if (match(line, /link:.*rxflow/))
376 if (match(line, /link:.*txflow/))
384 return { error: 'No such switch' };
386 return { result: ports };
389 return { error: error() };
395 args: { username: 'root', password: 'password' },
396 call: function(request) {
397 const u = shellquote(request.args.username);
398 const p = shellquote(request.args.password);
401 result: system(`(echo ${p}; sleep 1; echo ${p}) | /bin/busybox passwd ${u} >/dev/null 2>&1`) == 0
408 const block = popen('/sbin/block info 2>/dev/null');
413 for (let line = block.read('line'); length(line); line = block.read('line')) {
414 let dev = match(line, /^\/dev\/([^:]+):/)?.[1];
417 let e = result[dev] = {
419 size: +readfile(`/sys/class/block/${dev}/size`) * 512
422 for (m in match(line, / (\w+)="([^"]+)"/g))
432 return { error: 'Unable to execute block utility' };
439 return { result: system('/sbin/block detect > /etc/config/fstab') == 0 };
445 const fd = open('/proc/mounts', 'r');
450 for (let line = fd.read('line'); length(line); line = fd.read('line')) {
451 const m = split(line, ' ');
452 const device = replace(m[0], /\\([0-9][0-9][0-9])/g, (m, n) => char(int(n, 8)));
453 const mount = replace(m[1], /\\([0-9][0-9][0-9])/g, (m, n) => char(int(n, 8)));
454 const stat = statvfs(mount);
456 if (stat?.blocks > 0) {
459 size: stat.bsize * stat.blocks,
460 avail: stat.bsize * stat.bavail,
461 free: stat.bsize * stat.bfree
471 return { error: error() };
476 args: { mode: 'interface', device: 'eth0' },
477 call: function(request) {
480 if (request.args.mode == 'interface')
481 flags = `-i ${shellquote(request.args.device)}`;
482 else if (request.args.mode == 'wireless')
483 flags = `-r ${shellquote(request.args.device)}`;
484 else if (request.args.mode == 'conntrack')
486 else if (request.args.mode == 'load')
489 return { error: 'Invalid mode' };
491 const fd = popen(`luci-bwc ${flags}`, 'r');
497 result = { result: json(`[${fd.read('all')}]`) };
500 result = { error: err };
506 return { error: error() };
513 return { result: conntrack_list() };
519 return { result: process_list() };
524 return { luci: methods };