luci-base: implement http header plugins
authorPaul Donald <redacted>
Sun, 15 Mar 2026 18:18:33 +0000 (19:18 +0100)
committerPaul Donald <redacted>
Mon, 30 Mar 2026 22:13:54 +0000 (00:13 +0200)
This implements the injection of custom http headers via
the new plugin architecture.

Signed-off-by: Paul Donald <redacted>
modules/luci-base/ucode/http.uc
modules/luci-base/ucode/luciplugins.uc [new file with mode: 0644]

index e7f64ae6e9fe00bd1981518927565829a14b11ba..350bcc5eefc374162e8d1ac7c74c38aa09411641 100644 (file)
@@ -13,6 +13,12 @@ import {
        stdin, stdout, mkstemp
 } from 'fs';
 
+import {
+       openlog, syslog, closelog, LOG_NOTICE, LOG_LOCAL0
+} from 'log';
+
+import { run_plugins } from 'luciplugins';
+
 // luci.http module scope
 export let HTTP_MAX_CONTENT = 1024*100;                // 100 kB maximum content size
 
@@ -504,6 +510,37 @@ const Class = {
                if (!this.headers?.['x-content-type-options'])
                        this.header('X-Content-Type-Options', 'nosniff');
 
+               /* http header plugins */
+               let log_class = 'http.uc';
+               openlog(log_class);
+               for (let plugin_id, p_output in run_plugins('/luci/plugins/http/headers', 'http_headers_enabled')) {
+
+                       /* header plugins shall return e.g.: ['X-Header', 'foo'] */
+                       if (type(p_output) !== 'array' || length(p_output) !== 2)
+                               continue;
+
+                       if (type(p_output[0]) !== 'string' || type(p_output[1]) !== 'string')
+                               continue;
+
+                       if (!match(p_output[0], /^[A-Za-z0-9-]+$/)) {
+                               syslog(LOG_NOTICE|LOG_LOCAL0,
+                                       sprintf("Invalid header name from plugin %s output: %s", plugin_id, p_output[0]));
+                               continue;
+                       }
+
+                       /* header plugin values shall not contain line-feeds */
+                       if (match(p_output[1], /[\r\n]/)) {
+                               syslog(LOG_NOTICE|LOG_LOCAL0,
+                                       sprintf("\\r and/or \\n in plugin %s output", plugin_id));
+                               continue;
+                       }
+
+                       if(!this.headers?.[p_output[0]])
+                               this.header(p_output[0], p_output[1]);
+
+               }
+               closelog();
+
                this.output('Status: ');
                this.output(this.status_code);
                this.output(' ');
diff --git a/modules/luci-base/ucode/luciplugins.uc b/modules/luci-base/ucode/luciplugins.uc
new file mode 100644 (file)
index 0000000..364c839
--- /dev/null
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: Apache-2.0
+
+import {
+       lsdir
+} from 'fs';
+
+import {
+       syslog, LOG_NOTICE, LOG_LOCAL0
+} from 'log';
+
+import { cursor } from 'uci';
+
+
+/* generic plugin handler */
+export function run_plugins(plugin_class_path, plugin_class_enable) {
+       let uci = cursor();
+       const require_path = replace(plugin_class_path, '/', '.');
+
+       if (uci.get('luci_plugins', 'global', 'enabled') == 1 &&
+               uci.get('luci_plugins', 'global', plugin_class_enable) == 1) {
+               const PLUGINS_PATH = '/usr/share/ucode' + plugin_class_path;
+               const results = {};
+
+               for (let fn in lsdir(PLUGINS_PATH)) {
+                       const plugin_id = replace(fn, /.uc$/, '');
+                       /* plugins shall have a <32_char_UUID_no_hyphens>.uc filename */
+                       if (!match(plugin_id, /^[a-f0-9]+$/) || length(plugin_id) !== 32) {
+                               syslog(LOG_NOTICE|LOG_LOCAL0,
+                                       sprintf("Invalid plugin name: %s", plugin_id));
+                               continue;
+                       }
+
+                       if (uci.get('luci_plugins', plugin_id, 'enabled')) {
+                               const mod = require(require_path + `.${plugin_id}`);
+                               if (type(mod) === 'function') {
+                                       try {
+                                               results[plugin_id] = mod(plugin_id);
+                                       } catch (e) {
+                                               syslog(LOG_NOTICE|LOG_LOCAL0,
+                                                       sprintf("Could not execute plugin %s: %s",
+                                                       join('/', [PLUGINS_PATH, plugin_id]), e));
+                                       };
+                               }
+                       }
+               }
+
+               return results;
+       }
+};
git clone https://git.99rst.org/PROJECT