luci-base: dispatcher.uc: improve error reporting for actionless nodes
[project/luci.git] / modules / luci-base / ucode / dispatcher.uc
1 // Copyright 2022 Jo-Philipp Wich <jo@mein.io>
2 // Licensed to the public under the Apache License 2.0.
3
4 import { open, stat, glob, lsdir, unlink, basename } from 'fs';
5 import { striptags, entityencode } from 'html';
6 import { connect } from 'ubus';
7 import { cursor } from 'uci';
8 import { rand } from 'math';
9
10 import { hash, load_catalog, change_catalog, translate, ntranslate, getuid } from 'luci.core';
11 import { revision as luciversion, branch as luciname } from 'luci.version';
12 import { default as LuCIRuntime } from 'luci.runtime';
13 import { urldecode } from 'luci.http';
14
15 let ubus = connect();
16 let uci = cursor();
17
18 let indexcache = "/tmp/luci-indexcache";
19
20 let http, runtime, tree, luabridge;
21
22 function error404(msg) {
23 http.status(404, 'Not Found');
24
25 try {
26 runtime.render('error404', { message: msg ?? 'Not found' });
27 }
28 catch {
29 http.header('Content-Type', 'text/plain; charset=UTF-8');
30 http.write(msg ?? 'Not found');
31 }
32
33 return false;
34 }
35
36 function error500(msg, ex) {
37 if (!http.eoh) {
38 http.status(500, 'Internal Server Error');
39 http.header('Content-Type', 'text/html; charset=UTF-8');
40 }
41
42 try {
43 runtime.render('error500', {
44 title: ex?.type ?? 'Runtime exception',
45 message: replace(
46 msg,
47 /(\s)((\/[A-Za-z0-9_.-]+)+:\d+|\[string "[^"]+"\]:\d+)/g,
48 '$1<code>$2</code>'
49 ),
50 exception: ex
51 });
52 }
53 catch {
54 http.write('<!--]]>--><!--\'>--><!--">-->\n');
55 http.write(`<p>${trim(msg)}</p>\n`);
56
57 if (ex) {
58 http.write(`<p>${trim(ex.message)}</p>\n`);
59 http.write(`<pre>${trim(ex.stacktrace[0].context)}</pre>\n`);
60 }
61 }
62
63 exit(0);
64 }
65
66 function load_luabridge(optional) {
67 if (luabridge == null) {
68 try {
69 luabridge = require('lua');
70 }
71 catch (ex) {
72 luabridge = false;
73
74 if (!optional)
75 error500('No Lua runtime installed');
76 }
77 }
78
79 return luabridge;
80 }
81
82 function determine_request_language() {
83 let lang = uci.get('luci', 'main', 'lang') || 'auto';
84
85 if (lang == 'auto') {
86 for (let tag in split(http.getenv('HTTP_ACCEPT_LANGUAGE'), ',')) {
87 tag = split(trim(split(tag, ';')?.[0]), '-');
88
89 if (tag) {
90 let cc = tag[1] ? `${tag[0]}_${lc(tag[1])}` : null;
91
92 if (cc && uci.get('luci', 'languages', cc)) {
93 lang = cc;
94 break;
95 }
96 else if (uci.get('luci', 'languages', tag[0])) {
97 lang = tag[0];
98 break;
99 }
100 }
101 }
102 }
103
104 if (lang == 'auto')
105 lang = 'en';
106 else
107 lang = replace(lang, '_', '-');
108
109 if (load_catalog(lang, '/usr/lib/lua/luci/i18n'))
110 change_catalog(lang);
111
112 return lang;
113 }
114
115 function determine_version() {
116 let res = { luciname, luciversion };
117
118 for (let f = open("/etc/os-release"), l = f?.read?.("line"); l; l = f.read?.("line")) {
119 let kv = split(l, '=', 2);
120
121 switch (kv[0]) {
122 case 'NAME':
123 res.distname = trim(kv[1], '"\' \n');
124 break;
125
126 case 'VERSION':
127 res.distversion = trim(kv[1], '"\' \n');
128 break;
129
130 case 'HOME_URL':
131 res.disturl = trim(kv[1], '"\' \n');
132 break;
133
134 case 'BUILD_ID':
135 res.distrevision = trim(kv[1], '"\' \n');
136 break;
137 }
138 }
139
140 return res;
141 }
142
143 function read_jsonfile(path, defval) {
144 let rv;
145
146 try {
147 rv = json(open(path, "r"));
148 }
149 catch (e) {
150 rv = defval;
151 }
152
153 return rv;
154 }
155
156 function read_cachefile(file, reader) {
157 let euid = getuid(),
158 fstat = stat(file),
159 fuid = fstat?.uid,
160 perm = fstat?.perm;
161
162 if (euid != fuid ||
163 perm?.group_read || perm?.group_write || perm?.group_exec ||
164 perm?.other_read || perm?.other_write || perm?.other_exec)
165 return null;
166
167 return reader(file);
168 }
169
170 function check_fs_depends(spec) {
171 for (let path, kind in spec) {
172 if (kind == 'directory') {
173 if (!length(lsdir(path)))
174 return false;
175 }
176 else if (kind == 'executable') {
177 let fstat = stat(path);
178
179 if (fstat?.type != 'file' || fstat?.user_exec == false)
180 return false;
181 }
182 else if (kind == 'file') {
183 let fstat = stat(path);
184
185 if (fstat?.type != 'file')
186 return false;
187 }
188 else if (kind == 'absent') {
189 if (stat(path) != null)
190 return false;
191 }
192 }
193
194 return true;
195 }
196
197 function check_uci_depends_options(conf, s, opts) {
198 if (type(opts) == 'string') {
199 return (s['.type'] == opts);
200 }
201 else if (opts === true) {
202 for (let option, value in s)
203 if (ord(option) != 46)
204 return true;
205 }
206 else if (type(opts) == 'object') {
207 for (let option, value in opts) {
208 let sval = s[option];
209
210 if (type(sval) == 'array') {
211 if (!(value in sval))
212 return false;
213 }
214 else if (value === true) {
215 if (sval == null)
216 return false;
217 }
218 else {
219 if (sval != value)
220 return false;
221 }
222 }
223 }
224
225 return true;
226 }
227
228 function check_uci_depends_section(conf, sect) {
229 for (let section, options in sect) {
230 let stype = match(section, /^@([A-Za-z0-9_-]+)$/);
231
232 if (stype) {
233 let found = false;
234
235 uci.load(conf);
236 uci.foreach(conf, stype[1], (s) => {
237 if (check_uci_depends_options(conf, s, options)) {
238 found = true;
239 return false;
240 }
241 });
242
243 if (!found)
244 return false;
245 }
246 else {
247 let s = uci.get_all(conf, section);
248
249 if (!s || !check_uci_depends_options(conf, s, options))
250 return false;
251 }
252 }
253
254 return true;
255 }
256
257 function check_uci_depends(conf) {
258 for (let config, values in conf) {
259 if (values == true) {
260 let found = false;
261
262 uci.load(config);
263 uci.foreach(config, null, () => { found = true });
264
265 if (!found)
266 return false;
267 }
268 else if (type(values) == 'object') {
269 if (!check_uci_depends_section(config, values))
270 return false;
271 }
272 }
273
274 return true;
275 }
276
277 function check_depends(spec) {
278 if (type(spec?.depends?.fs) in ['array', 'object']) {
279 let satisfied = false;
280 let alternatives = (type(spec.depends.fs) == 'array') ? spec.depends.fs : [ spec.depends.fs ];
281
282 for (let alternative in alternatives) {
283 if (check_fs_depends(alternative)) {
284 satisfied = true;
285 break;
286 }
287 }
288
289 if (!satisfied)
290 return false;
291 }
292
293 if (type(spec?.depends?.uci) in ['array', 'object']) {
294 let satisfied = false;
295 let alternatives = (type(spec.depends.uci) == 'array') ? spec.depends.uci : [ spec.depends.uci ];
296
297 for (let alternative in alternatives) {
298 if (check_uci_depends(alternative)) {
299 satisfied = true;
300 break;
301 }
302 }
303
304 if (!satisfied)
305 return false;
306 }
307
308 return true;
309 }
310
311 function check_acl_depends(require_groups, groups) {
312 if (length(require_groups)) {
313 let writable = false;
314
315 for (let group in require_groups) {
316 let read = ('read' in groups?.[group]);
317 let write = ('write' in groups?.[group]);
318
319 if (!read && !write)
320 return null;
321
322 if (write)
323 writable = true;
324 }
325
326 return writable;
327 }
328
329 return true;
330 }
331
332 function hash_filelist(files) {
333 let hashval = 0x1b756362;
334
335 for (let file in files) {
336 let st = stat(file);
337
338 if (st)
339 hashval = hash(sprintf("%x|%x|%x", st.ino, st.mtime, st.size), hashval);
340 }
341
342 return hashval;
343 }
344
345 function build_pagetree() {
346 let tree = { action: { type: 'firstchild' } };
347
348 let schema = {
349 action: 'object',
350 auth: 'object',
351 cors: 'bool',
352 depends: 'object',
353 order: 'int',
354 setgroup: 'string',
355 setuser: 'string',
356 title: 'string',
357 wildcard: 'bool',
358 firstchild_ineligible: 'bool'
359 };
360
361 let files = glob('/usr/share/luci/menu.d/*.json', '/usr/lib/lua/luci/controller/*.lua', '/usr/lib/lua/luci/controller/*/*.lua');
362 let cachefile;
363
364 if (indexcache) {
365 cachefile = sprintf('%s.%08x.json', indexcache, hash_filelist(files));
366
367 let res = read_cachefile(cachefile, read_jsonfile);
368
369 if (res)
370 return res;
371
372 for (let path in glob(indexcache + '.*.json'))
373 unlink(path);
374 }
375
376 for (let file in files) {
377 let data;
378
379 if (substr(file, -5) == '.json')
380 data = read_jsonfile(file);
381 else if (load_luabridge(true))
382 data = runtime.call('luci.dispatcher', 'process_lua_controller', file);
383 else
384 warn(`Lua controller ${file} present but no Lua runtime installed.\n`);
385
386 if (type(data) == 'object') {
387 for (let path, spec in data) {
388 if (type(spec) == 'object') {
389 let node = tree;
390
391 for (let s in match(path, /[^\/]+/g)) {
392 if (s[0] == '*') {
393 node.wildcard = true;
394 break;
395 }
396
397 node.children ??= {};
398 node.children[s[0]] ??= { satisfied: true };
399 node = node.children[s[0]];
400 }
401
402 if (node !== tree) {
403 for (let k, t in schema)
404 if (type(spec[k]) == t)
405 node[k] = spec[k];
406
407 node.satisfied = check_depends(spec);
408 }
409 }
410 }
411 }
412 }
413
414 if (cachefile) {
415 let fd = open(cachefile, 'w', 0600);
416
417 if (fd) {
418 fd.write(tree);
419 fd.close();
420 }
421 }
422
423 return tree;
424 }
425
426 function apply_tree_acls(node, acl) {
427 for (let name, spec in node?.children)
428 apply_tree_acls(spec, acl);
429
430 if (node?.depends?.acl) {
431 switch (check_acl_depends(node.depends.acl, acl["access-group"])) {
432 case null: node.satisfied = false; break;
433 case false: node.readonly = true; break;
434 }
435 }
436 }
437
438 function menu_json(acl) {
439 tree ??= build_pagetree();
440
441 if (acl)
442 apply_tree_acls(tree, acl);
443
444 return tree;
445 }
446
447 function ctx_append(ctx, name, node) {
448 ctx.path ??= [];
449 push(ctx.path, name);
450
451 ctx.acls ??= [];
452 push(ctx.acls, ...(node?.depends?.acl || []));
453
454 ctx.auth = node.auth || ctx.auth;
455 ctx.cors = node.cors || ctx.cors;
456 ctx.suid = node.setuser || ctx.suid;
457 ctx.sgid = node.setgroup || ctx.sgid;
458
459 return ctx;
460 }
461
462 function session_retrieve(sid, allowed_users) {
463 let sdat = ubus.call("session", "get", { ubus_rpc_session: sid });
464 let sacl = ubus.call("session", "access", { ubus_rpc_session: sid });
465
466 if (type(sdat?.values?.token) == 'string' &&
467 (!length(allowed_users) || sdat?.values?.username in allowed_users)) {
468 // uci:set_session_id(sid)
469 return {
470 sid,
471 data: sdat.values,
472 acls: length(sacl) ? sacl : {}
473 };
474 }
475
476 return null;
477 }
478
479 function randomid(num_bytes) {
480 let bytes = [];
481
482 while (num_bytes-- > 0)
483 push(bytes, sprintf('%02x', rand() % 256));
484
485 return join('', bytes);
486 }
487
488 function syslog(prio, msg) {
489 warn(sprintf("[%s] %s\n", prio, msg));
490 }
491
492 function session_setup(user, pass, path) {
493 let timeout = uci.get('luci', 'sauth', 'sessiontime');
494 let login = ubus.call("session", "login", {
495 username: user,
496 password: pass,
497 timeout: timeout ? +timeout : null
498 });
499
500 if (type(login?.ubus_rpc_session) == 'string') {
501 ubus.call("session", "set", {
502 ubus_rpc_session: login.ubus_rpc_session,
503 values: { token: randomid(16) }
504 });
505 syslog("info", sprintf("luci: accepted login on /%s for %s from %s",
506 join('/', path), user || "?", http.getenv("REMOTE_ADDR") || "?"));
507
508 return session_retrieve(login.ubus_rpc_session);
509 }
510
511 syslog("info", sprintf("luci: failed login on /%s for %s from %s",
512 join('/', path), user || "?", http.getenv("REMOTE_ADDR") || "?"));
513 }
514
515 function check_authentication(method) {
516 let m = match(method, /^([[:alpha:]]+):(.+)$/);
517 let sid;
518
519 switch (m?.[1]) {
520 case 'cookie':
521 sid = http.getcookie(m[2]);
522 break;
523
524 case 'param':
525 sid = http.formvalue(m[2]);
526 break;
527
528 case 'query':
529 sid = http.formvalue(m[2], true);
530 break;
531 }
532
533 return sid ? session_retrieve(sid) : null;
534 }
535
536 function is_authenticated(auth) {
537 for (let method in auth?.methods) {
538 let session = check_authentication(method);
539
540 if (session)
541 return session;
542 }
543
544 return null;
545 }
546
547 function node_weight(node) {
548 let weight = min(node.order ?? 9999, 9999);
549
550 if (node.auth?.login)
551 weight += 10000;
552
553 return weight;
554 }
555
556 function clone(src) {
557 switch (type(src)) {
558 case 'array':
559 return map(src, clone);
560
561 case 'object':
562 let dest = {};
563
564 for (let k, v in src)
565 dest[k] = clone(v);
566
567 return dest;
568
569 default:
570 return src;
571 }
572 }
573
574 function resolve_firstchild(node, session, login_allowed, ctx) {
575 let candidate, candidate_ctx;
576
577 for (let name, child in node.children) {
578 if (!child.satisfied)
579 continue;
580
581 if (!session)
582 session = is_authenticated(node.auth);
583
584 let cacl = child.depends?.acl;
585 let login = login_allowed || child.auth?.login;
586
587 if (login || check_acl_depends(cacl, session?.acls?.["access-group"]) != null) {
588 if (child.title && type(child.action) == "object") {
589 let child_ctx = ctx_append(clone(ctx), name, child);
590 if (child.action.type == "firstchild") {
591 if (!candidate || node_weight(candidate) > node_weight(child)) {
592 let have_grandchild = resolve_firstchild(child, session, login, child_ctx);
593 if (have_grandchild) {
594 candidate = child;
595 candidate_ctx = child_ctx;
596 }
597 }
598 }
599 else if (!child.firstchild_ineligible) {
600 if (!candidate || node_weight(candidate) > node_weight(child)) {
601 candidate = child;
602 candidate_ctx = child_ctx;
603 }
604 }
605 }
606 }
607 }
608
609 if (!candidate)
610 return false;
611
612 for (let k, v in candidate_ctx)
613 ctx[k] = v;
614
615 return true;
616 }
617
618 function resolve_page(tree, request_path) {
619 let node = tree;
620 let login = false;
621 let session = null;
622 let ctx = {};
623
624 for (let i, s in request_path) {
625 node = node.children?.[s];
626
627 if (!node?.satisfied)
628 break;
629
630 ctx_append(ctx, s, node);
631
632 if (!session)
633 session = is_authenticated(node.auth);
634
635 if (!login && node.auth?.login)
636 login = true;
637
638 if (node.wildcard) {
639 ctx.request_args = [];
640 ctx.request_path = ctx.path ? [ ...ctx.path ] : [];
641
642 while (++i < length(request_path)) {
643 push(ctx.request_path, request_path[i]);
644 push(ctx.request_args, request_path[i]);
645 }
646
647 break;
648 }
649 }
650
651 if (node?.action?.type == 'firstchild')
652 resolve_firstchild(node, session, login, ctx);
653
654 ctx.acls ??= {};
655 ctx.path ??= [];
656 ctx.request_args ??= [];
657 ctx.request_path ??= request_path ? [ ...request_path ] : [];
658
659 ctx.authsession = session?.sid;
660 ctx.authtoken = session?.data?.token;
661 ctx.authuser = session?.data?.username;
662 ctx.authacl = session?.acls;
663
664 node = tree;
665
666 for (let s in ctx.path) {
667 node = node.children[s];
668 assert(node, "Internal node resolve error");
669 }
670
671 return { node, ctx, session };
672 }
673
674 function require_post_security(target, args) {
675 if (target?.type == 'arcombine')
676 return require_post_security(length(args) ? target?.targets?.[1] : target?.targets?.[0], args);
677
678 if (type(target?.post) == 'object') {
679 for (let param_name, required_val in target.post) {
680 let request_val = http.formvalue(param_name);
681
682 if ((type(required_val) == 'string' && request_val != required_val) ||
683 (required_val == true && request_val == null))
684 return false;
685 }
686
687 return true;
688 }
689
690 return (target?.post == true);
691 }
692
693 function test_post_security(authtoken) {
694 if (http.getenv("REQUEST_METHOD") != "POST") {
695 http.status(405, "Method Not Allowed");
696 http.header("Allow", "POST");
697
698 return false;
699 }
700
701 if (http.formvalue("token") != authtoken) {
702 http.status(403, "Forbidden");
703 runtime.render("csrftoken");
704
705 return false;
706 }
707
708 return true;
709 }
710
711 function build_url(...path) {
712 let url = [ http.getenv('SCRIPT_NAME') ?? '' ];
713
714 for (let p in path)
715 if (match(p, /^[A-Za-z0-9_%.\/,;-]+$/))
716 push(url, '/', p);
717
718 if (length(url) == 1)
719 push(url, '/');
720
721 return join('', url);
722 }
723
724 function lookup(...segments) {
725 let node = menu_json();
726 let path = [];
727
728 for (let segment in segments)
729 for (let name in split(segment, '/'))
730 push(path, name);
731
732 for (let name in path) {
733 node = node.children[name];
734
735 if (!node)
736 return null;
737
738 if (node.leaf)
739 break;
740 }
741
742 return { node, url: build_url(...path) };
743 }
744
745 function rollback_pending() {
746 const now = time();
747 const rv = ubus.call('session', 'get', {
748 ubus_rpc_session: '00000000000000000000000000000000',
749 keys: [ 'rollback' ]
750 });
751
752 if (type(rv?.values?.rollback?.token) != 'string' ||
753 type(rv?.values?.rollback?.session) != 'string' ||
754 type(rv?.values?.rollback?.timeout) != 'int' ||
755 rv.values.rollback.timeout <= now)
756 return false;
757
758 return {
759 remaining: rv.values.rollback.timeout - now,
760 session: rv.values.rollback.session,
761 token: rv.values.rollback.token
762 };
763 }
764
765 let dispatch;
766
767 function render_action(fn) {
768 const data = render(fn);
769
770 http.write_headers();
771 http.output(data);
772 }
773
774 function run_action(request_path, lang, tree, resolved, action) {
775 switch ((type(action) == 'object') ? action.type : 'none') {
776 case 'template':
777 if (runtime.is_ucode_template(action.path))
778 runtime.render(action.path, {});
779 else
780 render_action(() => {
781 runtime.call('luci.dispatcher', 'render_lua_template', action.path);
782 });
783 break;
784
785 case 'view':
786 runtime.render('view', { view: action.path });
787 break;
788
789 case 'call':
790 render_action(() => {
791 runtime.call(action.module, action.function,
792 ...(action.parameters ?? []),
793 ...resolved.ctx.request_args
794 );
795 });
796 break;
797
798 case 'function':
799 const mod = require(action.module);
800
801 assert(type(mod[action.function]) == 'function',
802 `Module '${action.module}' does not export function '${action.function}'`);
803
804 render_action(() => {
805 call(mod[action.function], mod, runtime.env,
806 ...(action.parameters ?? []),
807 ...resolved.ctx.request_args
808 );
809 });
810 break;
811
812 case 'cbi':
813 render_action(() => {
814 runtime.call('luci.dispatcher', 'invoke_cbi_action',
815 action.path, null,
816 ...resolved.ctx.request_args
817 );
818 });
819 break;
820
821 case 'form':
822 render_action(() => {
823 runtime.call('luci.dispatcher', 'invoke_form_action',
824 action.path,
825 ...resolved.ctx.request_args
826 );
827 });
828 break;
829
830 case 'alias':
831 dispatch(http, [ ...split(action.path, '/'), ...resolved.ctx.request_args ]);
832 break;
833
834 case 'rewrite':
835 dispatch(http, [
836 ...splice([ ...request_path ], 0, action.remove),
837 ...split(action.path, '/'),
838 ...resolved.ctx.request_args
839 ]);
840 break;
841
842 case 'firstchild':
843 if (!length(tree.children)) {
844 error404("No root node was registered, this usually happens if no module was installed.\n" +
845 "Install luci-mod-admin-full and retry. " +
846 "If the module is already installed, try removing the /tmp/luci-indexcache file.");
847 break;
848 }
849
850 /* fall through */
851
852 case 'none':
853 error404(`No page is registered at '/${entityencode(join("/", resolved.ctx.request_path))}'.\n` +
854 "If this url belongs to an extension, make sure it is properly installed.\n" +
855 "If the extension was recently installed, try removing the /tmp/luci-indexcache file.");
856 break;
857
858 default:
859 error500(`Unhandled action type ${action?.type ?? '?'}`);
860 }
861 }
862
863 dispatch = function(_http, path) {
864 http = _http;
865
866 let version = determine_version();
867 let lang = determine_request_language();
868
869 runtime = runtime || LuCIRuntime({
870 http,
871 ubus,
872 uci,
873 ctx: {},
874 version,
875 config: {
876 main: uci.get_all('luci', 'main') ?? {},
877 apply: uci.get_all('luci', 'apply') ?? {}
878 },
879 dispatcher: {
880 rollback_pending,
881 is_authenticated,
882 load_luabridge,
883 lookup,
884 menu_json,
885 build_url,
886 randomid,
887 error404,
888 error500,
889 lang
890 },
891 striptags,
892 entityencode,
893 _: (...args) => translate(...args) ?? args[0],
894 N_: (...args) => ntranslate(...args) ?? (args[0] == 1 ? args[1] : args[2]),
895 });
896
897 try {
898 let menu = menu_json();
899
900 path ??= map(match(http.getenv('PATH_INFO'), /[^\/]+/g), m => urldecode(m[0]));
901
902 let resolved = resolve_page(menu, path);
903
904 runtime.env.ctx = resolved.ctx;
905 runtime.env.dispatched = resolved.node;
906 runtime.env.requested ??= resolved.node;
907
908 if (length(resolved.ctx.auth)) {
909 let session = is_authenticated(resolved.ctx.auth);
910
911 if (!session && resolved.ctx.auth.login) {
912 let user = http.getenv('HTTP_AUTH_USER');
913 let pass = http.getenv('HTTP_AUTH_PASS');
914
915 if (user == null && pass == null) {
916 user = http.formvalue('luci_username');
917 pass = http.formvalue('luci_password');
918 }
919
920 if (user != null && pass != null)
921 session = session_setup(user, pass, resolved.ctx.request_path);
922
923 if (!session) {
924 resolved.ctx.path = [];
925
926 http.status(403, 'Forbidden');
927 http.header('X-LuCI-Login-Required', 'yes');
928
929 let scope = { duser: 'root', fuser: user };
930 let theme_sysauth = `themes/${basename(runtime.env.media)}/sysauth`;
931
932 if (runtime.is_ucode_template(theme_sysauth) || runtime.is_lua_template(theme_sysauth)) {
933 try {
934 return runtime.render(theme_sysauth, scope);
935 }
936 catch (e) {
937 runtime.env.media_error = `${e}`;
938 }
939 }
940
941 return runtime.render('sysauth', scope);
942 }
943
944 let cookie_name = (http.getenv('HTTPS') == 'on') ? 'sysauth_https' : 'sysauth_http',
945 cookie_secure = (http.getenv('HTTPS') == 'on') ? '; secure' : '';
946
947 http.header('Set-Cookie', `${cookie_name}=${session.sid}; path=${build_url()}; SameSite=strict; HttpOnly${cookie_secure}`);
948 http.redirect(build_url(...resolved.ctx.request_path));
949
950 return;
951 }
952
953 if (!session) {
954 http.status(403, 'Forbidden');
955 http.header('X-LuCI-Login-Required', 'yes');
956
957 return;
958 }
959
960 resolved.ctx.authsession ??= session.sid;
961 resolved.ctx.authtoken ??= session.data?.token;
962 resolved.ctx.authuser ??= session.data?.username;
963 resolved.ctx.authacl ??= session.acls;
964
965 /* In case the Lua runtime was already initialized, e.g. by probing legacy
966 * theme header templates, make sure to update the session ID of the uci
967 * module. */
968 if (runtime.L) {
969 runtime.L.invoke('require', 'luci.model.uci');
970 runtime.L.get('luci', 'model', 'uci').invoke('set_session_id', session.sid);
971 }
972 }
973
974 if (length(resolved.ctx.acls)) {
975 let perm = check_acl_depends(resolved.ctx.acls, resolved.ctx.authacl?.['access-group']);
976
977 if (perm == null) {
978 http.status(403, 'Forbidden');
979
980 return;
981 }
982
983 if (resolved.node)
984 resolved.node.readonly = !perm;
985 }
986
987 let action = resolved.node.action;
988
989 if (action?.type == 'arcombine')
990 action = length(resolved.ctx.request_args) ? action.targets?.[1] : action.targets?.[0];
991
992 if (resolved.ctx.cors && http.getenv('REQUEST_METHOD') == 'OPTIONS') {
993 http.status(200, 'OK');
994 http.header('Access-Control-Allow-Origin', http.getenv('HTTP_ORIGIN') ?? '*');
995 http.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
996
997 return;
998 }
999
1000 if (require_post_security(action) && !test_post_security(resolved.ctx.authtoken))
1001 return;
1002
1003 run_action(path, lang, menu, resolved, action);
1004 }
1005 catch (ex) {
1006 error500('Unhandled exception during request dispatching', ex);
1007 }
1008 };
1009
1010 export default dispatch;