mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 02:55:13 +08:00
💋 Sync 2026-10-04 04:16:09
This commit is contained in:
1 parent
f6b2df73ec
commit
f62992fd80
745 files changed
+212416
-1173
No files matched your search
@@ -0,0 +1,479 @@
|
||||
'use strict';
|
||||
|
||||
import { open, stat, unlink } from 'fs';
|
||||
import * as uloop from 'uloop';
|
||||
import * as struct from 'struct';
|
||||
import { cursor } from 'uci';
|
||||
import { connect as ubus_connect } from 'ubus';
|
||||
import * as podman_socket from 'luci.podman_socket'; // ucode-lsp disable
|
||||
import { API_BASE } from 'luci.podman_socket'; // ucode-lsp disable
|
||||
import { validate_id } from 'luci.podman_validate'; // ucode-lsp disable
|
||||
import { build_request, parse_status } from 'luci.podman_http'; // ucode-lsp disable
|
||||
|
||||
const BLOCKSIZE = 4096;
|
||||
|
||||
const FLUSH_SIZE = 1500;
|
||||
const PAD_STR = (() => {
|
||||
let s = ''; let i = 0;
|
||||
while (i < FLUSH_SIZE) { s += ' '; i++; }
|
||||
return s;
|
||||
})();
|
||||
|
||||
/** @param {string} data */
|
||||
function http_write_padded(data) {
|
||||
let n = length(data);
|
||||
if (n >= FLUSH_SIZE)
|
||||
return http.write(data);
|
||||
return http.write(data + substr(PAD_STR, 0, FLUSH_SIZE - n)); // ucode-lsp disable
|
||||
}
|
||||
|
||||
const _uci_timeouts = (() => {
|
||||
let c = cursor();
|
||||
let s = c.get('uhttpd', 'main', 'script_timeout');
|
||||
let n = c.get('uhttpd', 'main', 'network_timeout');
|
||||
c.unload('uhttpd');
|
||||
return { script: s ? +s : 60, network: n ? +n : 30 };
|
||||
})();
|
||||
|
||||
|
||||
/**
|
||||
* @param {int} code
|
||||
* @param {string} message
|
||||
*/
|
||||
function error_response(code, message) {
|
||||
if (http.eoh) return; // ucode-lsp disable
|
||||
http.status(code, message);
|
||||
http.header('Content-Type', 'text/plain');
|
||||
http.write(message + '\n');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} sid
|
||||
*/
|
||||
function session_timer(sid) {
|
||||
let uci = cursor();
|
||||
let script_timeout = +(uci.get('uhttpd', 'main', 'script_timeout') ?? 60);
|
||||
let sessiontime = +(uci.get('luci', 'sauth', 'sessiontime') ?? 3600);
|
||||
let timeout_ms = (script_timeout - 5) * 1000;
|
||||
|
||||
if (!sid)
|
||||
return { timeout_ms, is_final: false, on_expire: () => uloop.end() };
|
||||
|
||||
let uconn = ubus_connect();
|
||||
let now = time();
|
||||
|
||||
let sdat = uconn.call('session', 'get', { ubus_rpc_session: sid });
|
||||
let deadline = +(sdat?.values?.podman_stream_deadline ?? 0);
|
||||
let last_seen = +(sdat?.values?.podman_stream_last_seen ?? 0);
|
||||
|
||||
let is_fresh = !deadline || (now - last_seen > script_timeout);
|
||||
|
||||
if (is_fresh)
|
||||
deadline = now + sessiontime;
|
||||
|
||||
uconn.call('session', 'set', {
|
||||
ubus_rpc_session: sid,
|
||||
values: { podman_stream_deadline: deadline, podman_stream_last_seen: now }
|
||||
});
|
||||
|
||||
let remaining_ms = (deadline - now) * 1000;
|
||||
|
||||
if (remaining_ms <= 0) {
|
||||
uconn.call('session', 'destroy', { ubus_rpc_session: sid });
|
||||
return null;
|
||||
}
|
||||
|
||||
let is_final = remaining_ms <= timeout_ms;
|
||||
let fire_ms = is_final ? remaining_ms : timeout_ms;
|
||||
|
||||
return {
|
||||
timeout_ms: fire_ms,
|
||||
is_final,
|
||||
on_expire: () => {
|
||||
if (is_final)
|
||||
uconn.call('session', 'destroy', { ubus_rpc_session: sid });
|
||||
uloop.end();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} api_path
|
||||
* @param {function} on_data
|
||||
* @param {boolean} early_headers
|
||||
* @param {int} timer
|
||||
*/
|
||||
function stream_podman(api_path, on_data, early_headers, timer) {
|
||||
let sock = podman_socket.connect();
|
||||
if (!sock) {
|
||||
error_response(502, 'Cannot connect to Podman socket');
|
||||
return;
|
||||
}
|
||||
|
||||
sock.send(build_request('GET', api_path, null));
|
||||
|
||||
let t = _uci_timeouts;
|
||||
let ka_ms = (t.network - 5) * 1000;
|
||||
|
||||
uloop.init();
|
||||
|
||||
let response_started = early_headers;
|
||||
if (early_headers) {
|
||||
http.status(200, 'OK');
|
||||
http.header('Content-Type', 'text/plain');
|
||||
http.write_headers();
|
||||
http.write('\n');
|
||||
}
|
||||
|
||||
let buf = '';
|
||||
let podman_headers_done = false;
|
||||
|
||||
let handle = uloop.handle(sock, () => {
|
||||
let chunk = sock.recv(BLOCKSIZE);
|
||||
if (type(chunk) !== 'string') return; // EAGAIN - keep waiting
|
||||
|
||||
if (!podman_headers_done) {
|
||||
// Phase 1: consuming Podman's HTTP response headers
|
||||
if (!length(chunk)) {
|
||||
// EOF before Podman sent headers
|
||||
if (!response_started)
|
||||
error_response(502, 'Podman closed connection');
|
||||
uloop.end();
|
||||
return;
|
||||
}
|
||||
buf += chunk;
|
||||
if (type(buf) !== 'string') return;
|
||||
let sep = index(buf, '\r\n\r\n');
|
||||
if (type(sep) !== 'int' || sep < 0) return; // headers not complete yet
|
||||
|
||||
let code = parse_status(buf) || 502;
|
||||
if (code != 200) {
|
||||
if (!response_started)
|
||||
error_response(code, substr(buf, sep + 4) || sprintf('Podman error %d', code));
|
||||
uloop.end();
|
||||
return;
|
||||
}
|
||||
|
||||
sep = sep + 4;
|
||||
let body_start = substr(buf, sep);
|
||||
buf = '';
|
||||
podman_headers_done = true;
|
||||
|
||||
if (!response_started) {
|
||||
// Stats: start our response now that Podman confirmed 200
|
||||
http.status(200, 'OK');
|
||||
http.header('Content-Type', 'application/x-ndjson');
|
||||
http.write_headers();
|
||||
http.write('\n');
|
||||
response_started = true;
|
||||
}
|
||||
|
||||
if (body_start && length(body_start)) {
|
||||
if (!on_data(body_start)) { uloop.end(); return; }
|
||||
}
|
||||
} else {
|
||||
// Phase 2: streaming data
|
||||
if (!length(chunk)) { uloop.end(); return; } // Podman closed stream
|
||||
if (!on_data(chunk)) uloop.end();
|
||||
}
|
||||
}, uloop.ULOOP_READ);
|
||||
|
||||
if (early_headers)
|
||||
uloop.timer(ka_ms, () => http_write_padded('\n'));
|
||||
|
||||
let effective_timeout = early_headers ? min(timer.timeout_ms, ka_ms * 2) : timer.timeout_ms;
|
||||
uloop.timer(effective_timeout, timer.on_expire);
|
||||
|
||||
uloop.run();
|
||||
|
||||
handle.delete();
|
||||
sock.close();
|
||||
|
||||
if (!response_started)
|
||||
error_response(504, 'Podman did not respond');
|
||||
}
|
||||
|
||||
return {
|
||||
container_logs: (id) => {
|
||||
if (validate_id(id)) { error_response(400, 'Invalid container ID'); return; }
|
||||
|
||||
let timer = session_timer(ctx?.authsession); // ucode-lsp disable
|
||||
if (!timer) { error_response(403, 'Session expired'); return; }
|
||||
|
||||
let tail = http.formvalue('tail') || '100';
|
||||
let since = http.formvalue('since') || '';
|
||||
let until = http.formvalue('until') || '';
|
||||
let follow = http.formvalue('follow') !== 'false';
|
||||
|
||||
if (tail !== 'all' && !match(`${tail}`, /^[0-9]+$/)) {
|
||||
error_response(400, 'Invalid tail parameter');
|
||||
return;
|
||||
}
|
||||
|
||||
if (since && !match(`${since}`, /^[0-9]+(\.[0-9]+)?$/)) {
|
||||
error_response(400, 'Invalid since parameter');
|
||||
return;
|
||||
}
|
||||
|
||||
if (until && !match(`${until}`, /^[0-9]+(\.[0-9]+)?$/)) {
|
||||
error_response(400, 'Invalid until parameter');
|
||||
return;
|
||||
}
|
||||
|
||||
let api_path = sprintf(
|
||||
'%s/containers/%s/logs?follow=%s&stdout=true&stderr=true×tamps=false',
|
||||
API_BASE, id, follow ? 'true' : 'false'
|
||||
);
|
||||
if (tail) api_path += sprintf('&tail=%s', tail);
|
||||
if (since) api_path += sprintf('&since=%s', since);
|
||||
if (until) api_path += sprintf('&until=%s', until);
|
||||
|
||||
let framebuf = '';
|
||||
|
||||
stream_podman(api_path, (chunk) => {
|
||||
framebuf += `${chunk}`;
|
||||
let out = '';
|
||||
while (length(framebuf) >= 8) {
|
||||
let hdr = struct.unpack('!BxxxI', substr(framebuf, 0, 8));
|
||||
if (!hdr) break;
|
||||
let stream_type = hdr[0];
|
||||
let payload_len = int(`${hdr[1]}`);
|
||||
if (length(framebuf) < 8 + payload_len) break; // wait for more data
|
||||
let payload = substr(framebuf, 8, payload_len);
|
||||
framebuf = substr(framebuf, 8 + payload_len);
|
||||
// Emit stdout (1) and stderr (2); skip others
|
||||
if (stream_type >= 1 && stream_type <= 2)
|
||||
out += payload;
|
||||
}
|
||||
if (length(out)) http_write_padded(out);
|
||||
return true;
|
||||
}, true, timer);
|
||||
},
|
||||
|
||||
container_top: (id) => {
|
||||
if (validate_id(id)) { error_response(400, 'Invalid container ID'); return; }
|
||||
|
||||
let timer = session_timer(ctx?.authsession); // ucode-lsp disable
|
||||
if (!timer) { error_response(403, 'Session expired'); return; }
|
||||
|
||||
let delay = http.formvalue('delay') || '5';
|
||||
let ps_args = http.formvalue('ps_args') || '';
|
||||
|
||||
if (!match(`${delay}`, /^[0-9]+$/) || +delay < 2) {
|
||||
error_response(400, 'Invalid delay parameter');
|
||||
return;
|
||||
}
|
||||
|
||||
if (ps_args && !match(`${ps_args}`, /^[-a-zA-Z0-9_, ]+$/)) {
|
||||
error_response(400, 'Invalid ps_args parameter');
|
||||
return;
|
||||
}
|
||||
|
||||
let api_path = sprintf('%s/containers/%s/top?stream=true&delay=%s', API_BASE, id, delay);
|
||||
|
||||
if (ps_args)
|
||||
api_path += sprintf('&ps_args=%s', replace(ps_args, / /g, '%20'));
|
||||
|
||||
stream_podman(api_path, (chunk) => {
|
||||
return http_write_padded(chunk);
|
||||
}, false, timer);
|
||||
},
|
||||
|
||||
container_stats: (id) => {
|
||||
if (validate_id(id)) { error_response(400, 'Invalid container ID'); return; }
|
||||
|
||||
let timer = session_timer(ctx?.authsession); // ucode-lsp disable
|
||||
if (!timer) { error_response(403, 'Session expired'); return; }
|
||||
|
||||
let interval = `${http.formvalue('interval') || '3'}`;
|
||||
if (!match(interval, /^[0-9]+$/) || +interval < 1) {
|
||||
error_response(400, 'Invalid interval parameter');
|
||||
return;
|
||||
}
|
||||
let api_path = sprintf('%s/containers/stats?containers=%s&stream=true&interval=%s',
|
||||
API_BASE, id, interval);
|
||||
|
||||
stream_podman(api_path, (chunk) => {
|
||||
return http_write_padded(chunk);
|
||||
}, false, timer);
|
||||
},
|
||||
|
||||
pod_top: (name) => {
|
||||
if (validate_id(name)) { error_response(400, 'Invalid pod name or ID'); return; }
|
||||
|
||||
let timer = session_timer(ctx?.authsession); // ucode-lsp disable
|
||||
if (!timer) { error_response(403, 'Session expired'); return; }
|
||||
|
||||
let delay = http.formvalue('delay') || '5';
|
||||
|
||||
if (!match(`${delay}`, /^[0-9]+$/) || +delay < 2) {
|
||||
error_response(400, 'Invalid delay parameter');
|
||||
return;
|
||||
}
|
||||
|
||||
let api_path = sprintf('%s/pods/%s/top?stream=true&delay=%s', API_BASE, name, delay);
|
||||
|
||||
stream_podman(api_path, (chunk) => {
|
||||
return http_write_padded(chunk);
|
||||
}, false, timer);
|
||||
},
|
||||
|
||||
pod_stats: (name) => {
|
||||
if (validate_id(name)) { error_response(400, 'Invalid pod name or ID'); return; }
|
||||
|
||||
let timer = session_timer(ctx?.authsession); // ucode-lsp disable
|
||||
if (!timer) { error_response(403, 'Session expired'); return; }
|
||||
|
||||
let api_path = sprintf('%s/pods/stats?namesOrIDs=%s&stream=true', API_BASE, name);
|
||||
|
||||
stream_podman(api_path, (chunk) => {
|
||||
return http_write_padded(chunk);
|
||||
}, false, timer);
|
||||
},
|
||||
|
||||
image_pull: () => {
|
||||
let timer = session_timer(ctx?.authsession); // ucode-lsp disable
|
||||
if (!timer) { error_response(403, 'Session expired'); return; }
|
||||
|
||||
let reference = `${http.formvalue('reference')}`;
|
||||
if (!reference || !match(reference, /^[a-zA-Z0-9._/:@-]+$/)) {
|
||||
error_response(400, 'Invalid or missing reference parameter');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!match(reference, /[/:@]/)) {
|
||||
error_response(400, 'Reference must be a registry reference (e.g. name:tag), not a raw image ID');
|
||||
return;
|
||||
}
|
||||
|
||||
let sid = ctx?.authsession; // ucode-lsp disable
|
||||
let ref_id = replace(reference, /[/:@]/g, '-');
|
||||
let logfile = '/tmp/podman-pull-' + ref_id + '.ndjson';
|
||||
let pidfile = '/tmp/podman-pull-' + ref_id + '.pid';
|
||||
|
||||
// Check if a worker is already running for this reference
|
||||
let pid = 0;
|
||||
let pf = open(pidfile, 'r');
|
||||
if (pf) { pid = +(trim(pf.read(200) || '0')); pf.close(); }
|
||||
let is_running = pid > 0 && stat('/proc/' + pid);
|
||||
|
||||
// Read (and reuse) the ubus connection for session offset
|
||||
let uconn_rw = sid ? ubus_connect() : null;
|
||||
let offset = 0;
|
||||
if (uconn_rw) {
|
||||
let sdat = uconn_rw.call('session', 'get', { ubus_rpc_session: sid });
|
||||
offset = +(sdat?.values?.['pull_' + ref_id] || 0);
|
||||
}
|
||||
|
||||
// Check if the existing logfile has bytes we haven't sent yet
|
||||
// (worker may have just finished between two reconnects)
|
||||
let lstat = stat(logfile);
|
||||
let has_unread = lstat && offset < lstat.size;
|
||||
|
||||
let is_completed_pull = false;
|
||||
if (!is_running && lstat && lstat.size > 0) {
|
||||
let f = open(logfile, 'r');
|
||||
if (f) {
|
||||
let tail_start = lstat.size > 512 ? lstat.size - 512 : 0;
|
||||
if (tail_start > 0) f.seek(tail_start);
|
||||
let tail = f.read(512);
|
||||
f.close();
|
||||
is_completed_pull = !!(tail && (index(tail, '"images"') >= 0 || index(tail, '"error"') >= 0));
|
||||
}
|
||||
}
|
||||
|
||||
// Fresh start: worker not running AND (previous pull completed, OR no unread
|
||||
// progress from a prior session, OR stale offset=0 logfile from a closed tab).
|
||||
let is_fresh_start = !is_running && (is_completed_pull || !(has_unread && offset > 0));
|
||||
|
||||
if (is_fresh_start) {
|
||||
// Clean up any stale files and reset offset
|
||||
unlink(logfile);
|
||||
unlink(pidfile);
|
||||
offset = 0;
|
||||
if (uconn_rw) {
|
||||
let vals = {};
|
||||
vals['pull_' + ref_id] = 0;
|
||||
uconn_rw.call('session', 'set', { ubus_rpc_session: sid, values: vals });
|
||||
}
|
||||
// Safety: single quotes are used as shell delimiters below.
|
||||
// The regex on line 295 already rejects them; this is a belt-and-suspenders guard.
|
||||
if (index(reference, "'") >= 0 || index(logfile, "'") >= 0 || index(pidfile, "'") >= 0) {
|
||||
error_response(400, 'Invalid characters in reference');
|
||||
return;
|
||||
}
|
||||
system(sprintf(
|
||||
"timeout 600 /usr/bin/ucode /usr/share/podman/pull-worker.uc '%s' '%s' '%s' </dev/null >/dev/null 2>/dev/null &",
|
||||
reference, logfile, pidfile
|
||||
));
|
||||
}
|
||||
|
||||
http.status(200, 'OK');
|
||||
http.header('Content-Type', 'text/plain');
|
||||
http.write_headers();
|
||||
http.write('\n');
|
||||
|
||||
let t = _uci_timeouts;
|
||||
let ka_ms = (t.network - 5) * 1000;
|
||||
let last_saved = offset;
|
||||
|
||||
uloop.init();
|
||||
|
||||
let poll_fn;
|
||||
poll_fn = () => {
|
||||
// Read and forward any new bytes written to the logfile
|
||||
let lf = open(logfile, 'r');
|
||||
if (lf) {
|
||||
if (offset > 0) lf.seek(offset);
|
||||
let chunk = lf.read(8192);
|
||||
lf.close();
|
||||
if (chunk && length(chunk)) {
|
||||
if (!http_write_padded(chunk)) { uloop.end(); return; }
|
||||
offset += length(chunk);
|
||||
// Persist offset to session (rate-limited)
|
||||
if (uconn_rw && offset - last_saved >= 1024) {
|
||||
let vals = {};
|
||||
vals['pull_' + ref_id] = offset;
|
||||
uconn_rw.call('session', 'set', { ubus_rpc_session: sid, values: vals });
|
||||
last_saved = offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If worker has finished and we've consumed all bytes: close cleanly
|
||||
let cpid = 0;
|
||||
let cpf = open(pidfile, 'r');
|
||||
if (cpf) { cpid = +(trim(cpf.read(200) || '0')); cpf.close(); }
|
||||
if (!cpid || !stat('/proc/' + cpid)) {
|
||||
let sf = stat(logfile);
|
||||
if (!sf || offset >= sf.size) {
|
||||
unlink(logfile);
|
||||
uloop.end();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uloop.timer(200, poll_fn);
|
||||
};
|
||||
uloop.timer(0, poll_fn);
|
||||
|
||||
// Self-rearming keepalive - prevents network_timeout during slow layer downloads
|
||||
let ka;
|
||||
ka = () => { http_write_padded('\n'); uloop.timer(ka_ms, ka); };
|
||||
uloop.timer(ka_ms, ka);
|
||||
|
||||
// Session expiry timer - same pattern as other streams
|
||||
uloop.timer(timer.timeout_ms, timer.on_expire);
|
||||
|
||||
uloop.run();
|
||||
|
||||
// Persist final offset before this script exits (next reconnect will resume here)
|
||||
if (uconn_rw && offset !== last_saved) {
|
||||
let vals = {};
|
||||
vals['pull_' + ref_id] = offset;
|
||||
uconn_rw.call('session', 'set', { ubus_rpc_session: sid, values: vals });
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright 2026 Christopher Söllinger
|
||||
// Licensed to the public under the Apache License 2.0.
|
||||
//
|
||||
// Small helpers for the wire-format HTTP/1.0 conversation the rpcd plugin,
|
||||
// the LuCI streaming controller and the pull-worker all have with the
|
||||
// Podman API socket. None of them need a full HTTP client — just request
|
||||
// construction and a couple of header parsers.
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @param {string} method GET / POST / PUT / DELETE
|
||||
* @param {string} path Request-URI including any query string
|
||||
* @param {string|null} body JSON body (or null/empty for GET-style requests)
|
||||
* @returns {string} The complete request bytes
|
||||
*/
|
||||
export function build_request(method, path, body) {
|
||||
let req = sprintf('%s %s HTTP/1.0\r\nHost: localhost\r\n', method, path);
|
||||
if (body != null && body !== '') {
|
||||
let s = `${body}`;
|
||||
req += sprintf('Content-Type: application/json\r\nContent-Length: %d\r\n', length(s));
|
||||
req += '\r\n';
|
||||
req += s;
|
||||
return req;
|
||||
}
|
||||
return req + '\r\n';
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract HTTP status code from the start of a response buffer.
|
||||
*
|
||||
* @param {string} buf Buffer that begins with the status line
|
||||
* @returns {number} Status code, or 0 if the line doesn't match
|
||||
*/
|
||||
export function parse_status(buf) {
|
||||
let m = match(buf, /^HTTP\/[0-9.]+ ([0-9]+)/);
|
||||
return m ? +m[1] : 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract a Content-Length value from a response header buffer.
|
||||
*
|
||||
* @param {string} buf Header buffer (status line + headers)
|
||||
* @returns {number} Bytes declared by Content-Length, or -1 if absent
|
||||
*/
|
||||
export function parse_content_length(buf) {
|
||||
let m = match(buf, /[Cc]ontent-[Ll]ength:\s*([0-9]+)/);
|
||||
return m ? +m[1] : -1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Blocking read of HTTP response headers from a connected socket. Reads
|
||||
* until the \r\n\r\n separator is seen, then returns the header block
|
||||
* (without trailing CRLFCRLF) and any body bytes that arrived together
|
||||
* with the headers. Returns null if the peer closes before headers are
|
||||
* complete.
|
||||
*
|
||||
* @param {any} sock Connected socket
|
||||
* @param {number|null} blocksize recv chunk size (default 65536)
|
||||
* @returns {{header_buf:string, body_remainder:string}|null}
|
||||
*/
|
||||
export function read_headers(sock, blocksize) {
|
||||
let bs = blocksize || 65536;
|
||||
let buf = '';
|
||||
while (true) {
|
||||
let chunk = sock.recv(bs);
|
||||
if (!chunk) return null;
|
||||
buf += `${chunk}`;
|
||||
let sep = index(buf, '\r\n\r\n');
|
||||
if (type(sep) !== 'int' || sep < 0) continue;
|
||||
return {
|
||||
header_buf: substr(buf, 0, sep),
|
||||
body_remainder: substr(buf, sep + 4)
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright 2026 Christopher Söllinger
|
||||
// Licensed to the public under the Apache License 2.0.
|
||||
//
|
||||
// Shared Podman socket helper for the rpcd plugin, the LuCI streaming
|
||||
// controller, the pull-worker and the podman-api CLI helper. Reads the
|
||||
// destination once at module load from UCI:
|
||||
// luci-podman.globals.socket_path
|
||||
// Accepted URI schemes:
|
||||
// unix:///path/to/socket AF_UNIX
|
||||
// tcp://host:port AF_INET
|
||||
// tcp6://[::1]:port AF_INET6
|
||||
// /path/to/socket bare path, treated as unix:///path
|
||||
// Fallback: unix:///run/podman/podman.sock.
|
||||
|
||||
'use strict';
|
||||
|
||||
import { cursor } from 'uci';
|
||||
import * as socket from 'socket';
|
||||
|
||||
const DEFAULT_DEST = 'unix:///run/podman/podman.sock';
|
||||
|
||||
/** Path prefix for all libpod REST API endpoints. */
|
||||
export const API_BASE = '/v5.0.0/libpod';
|
||||
|
||||
const _dest = (() => {
|
||||
let c = cursor();
|
||||
let v = c.get('luci-podman', 'globals', 'socket_path');
|
||||
c.unload('luci-podman');
|
||||
return v || DEFAULT_DEST;
|
||||
})();
|
||||
|
||||
const _parsed = (() => {
|
||||
if (substr(_dest, 0, 1) === '/')
|
||||
return { scheme: 'unix', addr: _dest };
|
||||
let m = match(_dest, /^([a-z0-9]+):\/\/(.+)$/i);
|
||||
if (!m)
|
||||
return null;
|
||||
return { scheme: lc(m[1]), addr: m[2] };
|
||||
})();
|
||||
|
||||
/** @returns {?Socket} connected socket, or null on failure */
|
||||
export function connect() {
|
||||
if (!_parsed)
|
||||
return null;
|
||||
|
||||
if (_parsed.scheme === 'unix') {
|
||||
let sock = socket.create(socket.AF_UNIX, socket.SOCK_STREAM);
|
||||
if (!sock) return null;
|
||||
if (!sock.connect(socket.sockaddr(_parsed.addr))) {
|
||||
sock.close();
|
||||
return null;
|
||||
}
|
||||
return sock;
|
||||
}
|
||||
|
||||
if (_parsed.scheme === 'tcp' || _parsed.scheme === 'tcp6') {
|
||||
let family = _parsed.scheme === 'tcp6' ? socket.AF_INET6 : socket.AF_INET;
|
||||
let raw = _parsed.addr;
|
||||
let colon = rindex(raw, ':');
|
||||
if (colon < 0) return null;
|
||||
let host = substr(raw, 0, colon);
|
||||
let port = +substr(raw, colon + 1);
|
||||
host = replace(host, /^\[|\]$/g, '');
|
||||
if (!port || !host) return null;
|
||||
|
||||
let ai = socket.addrinfo(host, `${port}`, { family, socktype: socket.SOCK_STREAM });
|
||||
if (!ai || !length(ai)) return null;
|
||||
let sock = socket.create(family, socket.SOCK_STREAM);
|
||||
if (!sock) return null;
|
||||
if (!sock.connect(ai[0].addr)) {
|
||||
sock.close();
|
||||
return null;
|
||||
}
|
||||
return sock;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/** @returns {boolean} true if destination is a TCP scheme */
|
||||
export function is_remote() {
|
||||
return _parsed && (_parsed.scheme === 'tcp' || _parsed.scheme === 'tcp6');
|
||||
};
|
||||
|
||||
/** @returns {string} the raw URI from UCI (or the default), for logging */
|
||||
export function get_dest() {
|
||||
return _dest;
|
||||
};
|
||||
|
||||
/**
|
||||
* @returns {?string} filesystem path of the unix socket, or null for tcp/tcp6.
|
||||
* Useful for pre-flight stat() checks before connect().
|
||||
*/
|
||||
export function get_local_path() {
|
||||
if (!_parsed || _parsed.scheme !== 'unix') return null;
|
||||
return _parsed.addr;
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2026 Christopher Söllinger
|
||||
// Licensed to the public under the Apache License 2.0.
|
||||
//
|
||||
// Shared parameter validators for the rpcd plugin, the LuCI streaming
|
||||
// controller and the podman-api CLI helper.
|
||||
//
|
||||
// Convention: every exported validator returns an error message **string**
|
||||
// on failure and `null` on success, so call sites can chain with || :
|
||||
//
|
||||
// let err = require_param('name', v) || validate_name(v);
|
||||
// if (err) return { error: err };
|
||||
|
||||
'use strict';
|
||||
|
||||
const VALID_RESTART_POLICIES = { 'no': true, 'always': true, 'on-failure': true, 'unless-stopped': true };
|
||||
|
||||
const CONTAINER_BODY_KEYS = {
|
||||
cpu: true, memory: true, blockIO: true, devices: true,
|
||||
hugepageLimits: true, network: true, pids: true, rdma: true, unified: true,
|
||||
BlkIOWeightDevice: true,
|
||||
DeviceReadBPs: true, DeviceReadIOPs: true,
|
||||
DeviceWriteBPs: true, DeviceWriteIOPs: true,
|
||||
Env: true, UnsetEnv: true,
|
||||
health_cmd: true, health_interval: true, health_log_destination: true,
|
||||
health_max_log_count: true, health_max_log_size: true,
|
||||
health_on_failure: true, health_retries: true, health_start_period: true,
|
||||
health_startup_cmd: true, health_startup_interval: true,
|
||||
health_startup_retries: true, health_startup_success: true,
|
||||
health_startup_timeout: true, health_timeout: true,
|
||||
no_healthcheck: true
|
||||
};
|
||||
|
||||
/** @param {string} id */
|
||||
export function validate_id(id) {
|
||||
if (!id || type(id) !== 'string' || !match(id, /^[a-zA-Z0-9][a-zA-Z0-9_.:-]*$/))
|
||||
return 'Invalid id format';
|
||||
};
|
||||
|
||||
/** @param {string} name */
|
||||
export function validate_name(name) {
|
||||
if (!name || type(name) !== 'string' || !match(name, /^[a-zA-Z0-9_.-]+$/))
|
||||
return 'Invalid name format';
|
||||
};
|
||||
|
||||
/** @param {string} ref */
|
||||
export function validate_image_ref(ref) {
|
||||
if (!ref || type(ref) !== 'string' || !match(ref, /^[a-zA-Z0-9_.:\/@-]+$/))
|
||||
return 'Invalid image reference';
|
||||
};
|
||||
|
||||
/** @param {string} query */
|
||||
export function validate_query_params(query) {
|
||||
if (!query || type(query) !== 'string' || !match(query, /^[a-zA-Z0-9=&_.,-]+$/))
|
||||
return 'Invalid query parameters';
|
||||
};
|
||||
|
||||
/** @param {string} policy */
|
||||
export function validate_restart_policy(policy) {
|
||||
if (policy && !(policy in VALID_RESTART_POLICIES))
|
||||
return 'Invalid restart policy';
|
||||
};
|
||||
|
||||
/** @param {string} val */
|
||||
export function validate_int(val) {
|
||||
if (!val || type(val) !== 'string' || !match(val, /^[0-9]+$/))
|
||||
return 'Invalid number';
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {string} name
|
||||
* @param {any} value
|
||||
*/
|
||||
export function require_param(name, value) {
|
||||
if (value == null || value === ''
|
||||
|| (type(value) === 'object' && length(keys(value)) === 0))
|
||||
return `Missing required parameter: ${name}`;
|
||||
};
|
||||
|
||||
export const BODY_KEYS = CONTAINER_BODY_KEYS;
|
||||
Reference in new issue
Block a user