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