f4d204cf228415022c418a91d791778b81772595
[project/luci.git] / modules / luci-base / root / usr / share / rpcd / ucode / luci
1 // Copyright 2022 Jo-Philipp Wich <jo@mein.io>
2 // Licensed to the public under the Apache License 2.0.
3
4 'use strict';
5
6 import { stdin, access, dirname, basename, open, popen, glob, lsdir, readfile, readlink, error } from 'fs';
7 import { cursor } from 'uci';
8
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';
12
13 import timezones from 'luci.zoneinfo';
14
15
16 function shellquote(s) {
17 return `'${replace(s, "'", "'\\''")}'`;
18 }
19
20 const methods = {
21 getVersion: {
22 call: function(request) {
23 return { revision, branch };
24 }
25 },
26
27 getInitList: {
28 args: { name: 'name' },
29 call: function(request) {
30 let scripts = {};
31
32 for (let name in filter(init_list(), i => !request.args.name || i == request.args.name)) {
33 let idx = init_index(name);
34
35 scripts[name] = {
36 index: idx?.[0],
37 stop: idx?.[1],
38 enabled: init_enabled(name)
39 };
40 }
41
42 return length(scripts) ? scripts : { error: 'No such init script' };
43 }
44 },
45
46 setInitAction: {
47 args: { name: 'name', action: 'action' },
48 call: function(request) {
49 switch (request.args.action) {
50 case 'enable':
51 case 'disable':
52 case 'start':
53 case 'stop':
54 case 'restart':
55 case 'reload':
56 const rc = init_action(request.args.name, request.args.action);
57
58 if (rc === false)
59 return { error: 'No such init script' };
60
61 return { result: rc == 0 };
62
63 default:
64 return { error: 'Invalid action' };
65 }
66 }
67 },
68
69 getLocaltime: {
70 call: function(request) {
71 return { result: time() };
72 }
73 },
74
75 setLocaltime: {
76 args: { localtime: 0 },
77 call: function(request) {
78 let t = localtime(request.args.localtime);
79
80 if (t) {
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');
83 }
84
85 return { result: request.args.localtime };
86 }
87 },
88
89 getTimezones: {
90 call: function(request) {
91 let tz = trim(readfile('/etc/TZ'));
92 let zn = cursor()?.get?.('system', '@system[0]', 'zonename');
93 let result = {};
94
95 for (let zone, tzstring in timezones) {
96 result[zone] = { tzstring };
97
98 if (zn == zone)
99 result[zone].active = true;
100 };
101
102 return result;
103 }
104 },
105
106 getLEDs: {
107 call: function() {
108 let result = {};
109
110 for (let led in lsdir('/sys/class/leds')) {
111 let s;
112
113 result[led] = { triggers: [] };
114
115 s = trim(readfile(`/sys/class/leds/${led}/trigger`));
116 for (let trigger in split(s, ' ')) {
117 push(result[led].triggers, trim(trigger, '[]'));
118
119 if (trigger != result[led].triggers[-1])
120 result[led].active_trigger = result[led].triggers[-1];
121 }
122
123 s = readfile(`/sys/class/leds/${led}/brightness`);
124 result[led].brightness = +s;
125
126 s = readfile(`/sys/class/leds/${led}/max_brightness`);
127 result[led].max_brightness = +s;
128 }
129
130 return result;
131 }
132 },
133
134 getUSBDevices: {
135 call: function() {
136 let result = { devices: [], ports: [] };
137
138 for (let path in glob('/sys/bus/usb/devices/[0-9]*/manufacturer')) {
139 let id = basename(dirname(path));
140
141 push(result.devices, {
142 id,
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`)
148 });
149 }
150
151 for (let path in glob('/sys/bus/usb/devices/*/*-port[0-9]*')) {
152 let port = basename(path);
153 let link = readlink(`${path}/device`);
154
155 push(result.ports, {
156 port,
157 device: basename(link)
158 });
159 }
160
161 return result;
162 }
163 },
164
165 getConntrackHelpers: {
166 call: function() {
167 const uci = cursor();
168 let helpers = [];
169 let package;
170
171 if (uci.load('/usr/share/firewall4/helpers'))
172 package = 'helpers';
173 else if (uci.load('/usr/share/fw3/helpers.conf'))
174 package = 'helpers.conf';
175
176 if (package) {
177 uci.foreach(package, 'helper', (s) => {
178 push(helpers, {
179 name: s.name,
180 description: s.description,
181 module: s.module,
182 family: s.family,
183 proto: s.proto,
184 port: s.port
185 });
186 });
187 }
188
189 return { result: helpers };
190 }
191 },
192
193 getFeatures: {
194 call: function() {
195 let result = {
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,
209 };
210
211 const wifi_features = [ 'eap', '11ac', '11ax', '11r', 'acs', 'sae', 'owe', 'suiteb192', 'wep', 'wps' ];
212
213 if (access('/usr/sbin/hostapd')) {
214 result.hostapd = { cli: access('/usr/sbin/hostapd_cli') == true };
215
216 for (let feature in wifi_features)
217 result.hostapd[feature] = system(`/usr/sbin/hostapd -v${feature} >/dev/null 2>/dev/null`) == 0;
218 }
219
220 if (access('/usr/sbin/wpa_supplicant')) {
221 result.wpasupplicant = { cli: access('/usr/sbin/wpa_cli') == true };
222
223 for (let feature in wifi_features)
224 result.wpasupplicant[feature] = system(`/usr/sbin/wpa_supplicant -v${feature} >/dev/null 2>/dev/null`) == 0;
225 }
226
227 let fd = popen('dnsmasq --version 2>/dev/null');
228
229 if (fd) {
230 const m = match(fd.read('all'), /^Compile time options: (.+)$/s);
231
232 for (let opt in split(m?.[1], ' ')) {
233 let f = replace(opt, 'no-', '', 1);
234
235 result.dnsmasq ??= {};
236 result.dnsmasq[lc(f)] = (f == opt);
237 }
238
239 fd.close();
240 }
241
242 fd = popen('ipset --help 2>/dev/null');
243
244 if (fd) {
245 for (let line = fd.read('line'), flag = false; length(line); line = fd.read('line')) {
246 if (line == 'Supported set types:\n') {
247 flag = true;
248 }
249 else if (flag) {
250 const m = match(line, /^ +([\w:,]+)\t+([0-9]+)\t/);
251
252 if (m) {
253 result.ipset ??= {};
254 result.ipset[m[1]] ??= +m[2];
255 }
256 }
257 }
258
259 fd.close();
260 }
261
262 return result;
263 }
264 },
265
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`);
271
272 if (swc) {
273 let is_port_attr = false;
274 let is_vlan_attr = false;
275 let result = {};
276
277 for (let line = swc.read('line'); length(line); line = swc.read('line')) {
278 if (match(line, /^\s+--vlan/)) {
279 is_vlan_attr = true;
280 }
281 else if (match(line, /^\s+--port/)) {
282 is_vlan_attr = false;
283 is_port_attr = true;
284 }
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;
288 result.min_vid = 1;
289 }
290 else if (match(line, /: (pvid|tag|vid)/)) {
291 if (is_vlan_attr)
292 result.vid_option = match(line, /: (\w+)/)?.[1];
293 }
294 else if (match(line, /: enable_vlan4k/)) {
295 result.vlan4k_option = 'enable_vlan4k';
296 }
297 else if (match(line, /: enable_vlan/)) {
298 result.vlan_option = 'enable_vlan';
299 }
300 else if (match(line, /: enable_learning/)) {
301 result.learning_option = 'enable_learning';
302 }
303 else if (match(line, /: enable_mirror_rx/)) {
304 result.mirror_option = 'enable_mirror_rx';
305 }
306 else if (match(line, /: max_length/)) {
307 result.jumbo_option = 'max_length';
308 }
309 }
310
311 swc.close();
312
313 if (!length(result))
314 return { error: 'No such switch' };
315
316 return result;
317 }
318 else {
319 return { error: error() };
320 }
321 }
322 },
323
324 getSwconfigPortState: {
325 args: { switch: 'switch0' },
326 call: function(request) {
327 const swc = popen(`swconfig dev ${shellquote(request.args.switch)} show 2>/dev/null`);
328
329 if (swc) {
330 let ports = [], port;
331
332 for (let line = swc.read('line'); length(line); line = swc.read('line')) {
333 if (match(line, /^VLAN [0-9]+:/) && length(ports))
334 break;
335
336 let pnum = match(line, /^Port ([0-9]+):/)?.[1];
337
338 if (pnum) {
339 port = {
340 port: +pnum,
341 duplex: false,
342 speed: 0,
343 link: false,
344 auto: false,
345 rxflow: false,
346 txflow: false
347 };
348
349 push(ports, port);
350 }
351
352 if (port) {
353 let m;
354
355 if (match(line, /full[ -]duplex/))
356 port.duplex = true;
357
358 if ((m = match(line, / speed:([0-9]+)/)) != null)
359 port.speed = +m[1];
360
361 if ((m = match(line, /([0-9]+) Mbps/)) != null && !port.speed)
362 port.speed = +m[1];
363
364 if ((m = match(line, /link: ([0-9]+)/)) != null && !port.speed)
365 port.speed = +m[1];
366
367 if (match(line, /(link|status): ?up/))
368 port.link = true;
369
370 if (match(line, /auto-negotiate|link:.*auto/))
371 port.auto = true;
372
373 if (match(line, /link:.*rxflow/))
374 port.rxflow = true;
375
376 if (match(line, /link:.*txflow/))
377 port.txflow = true;
378 }
379 }
380
381 swc.close();
382
383 if (!length(ports))
384 return { error: 'No such switch' };
385
386 return { result: ports };
387 }
388 else {
389 return { error: error() };
390 }
391 }
392 },
393
394 setPassword: {
395 args: { username: 'root', password: 'password' },
396 call: function(request) {
397 const u = shellquote(request.args.username);
398 const p = shellquote(request.args.password);
399
400 return {
401 result: system(`(echo ${p}; sleep 1; echo ${p}) | /bin/busybox passwd ${u} >/dev/null 2>&1`) == 0
402 };
403 }
404 },
405
406 getBlockDevices: {
407 call: function() {
408 const block = popen('/sbin/block info 2>/dev/null');
409
410 if (block) {
411 let result = {};
412
413 for (let line = block.read('line'); length(line); line = block.read('line')) {
414 let dev = match(line, /^\/dev\/([^:]+):/)?.[1];
415
416 if (dev) {
417 let e = result[dev] = {
418 dev: `/dev/${dev}`,
419 size: +readfile(`/sys/class/block/${dev}/size`) * 512
420 };
421
422 for (m in match(line, / (\w+)="([^"]+)"/g))
423 e[lc(m[1])] = m[2];
424 }
425 }
426
427 block.close();
428
429 return result;
430 }
431 else {
432 return { error: 'Unable to execute block utility' };
433 }
434 }
435 },
436
437 setBlockDetect: {
438 call: function() {
439 return { result: system('/sbin/block detect > /etc/config/fstab') == 0 };
440 }
441 },
442
443 getMountPoints: {
444 call: function() {
445 const fd = open('/proc/mounts', 'r');
446
447 if (fd) {
448 let result = [];
449
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);
455
456 if (stat?.blocks > 0) {
457 push(result, {
458 device, mount,
459 size: stat.bsize * stat.blocks,
460 avail: stat.bsize * stat.bavail,
461 free: stat.bsize * stat.bfree
462 });
463 }
464 }
465
466 fd.close();
467
468 return { result };
469 }
470 else {
471 return { error: error() };
472 }
473 }
474 },
475 getRealtimeStats: {
476 args: { mode: 'interface', device: 'eth0' },
477 call: function(request) {
478 let flags;
479
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')
485 flags = '-c';
486 else if (request.args.mode == 'load')
487 flags = '-l';
488 else
489 return { error: 'Invalid mode' };
490
491 const fd = popen(`luci-bwc ${flags}`, 'r');
492
493 if (fd) {
494 let result;
495
496 try {
497 result = { result: json(`[${fd.read('all')}]`) };
498 }
499 catch (err) {
500 result = { error: err };
501 }
502
503 return result;
504 }
505 else {
506 return { error: error() };
507 }
508 }
509 },
510
511 getConntrackList: {
512 call: function() {
513 return { result: conntrack_list() };
514 }
515 },
516
517 getProcessList: {
518 call: function() {
519 return { result: process_list() };
520 }
521 }
522 };
523
524 return { luci: methods };