mirror of
https://github.com/openwrt/luci.git
synced 2026-05-31 02:21:50 +08:00
fdc8eb01d0
* ucode fixes:
- tighten `is_valid_lxc_name` regex to `^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$`
- apply the validator in `lxc_configuration_get` and `lxc_configuration_set` before any filesystem access
- reject the `'lxc error: …'` sentinel string returned by `lxc_get_config_path()` on failure,
rather than concatenating it into a path.
- shellquote `LXC_URL` in `lxc_get_downloadable` and `lxc_create`
* ACL fix: add `depends.acl = ["luci-app-lxc"]` to each of the five backend entries,
so the routes share the same authorization gate as the view
Signed-off-by: Dirk Brenken <dev@brenken.org>
207 lines
5.9 KiB
Ucode
207 lines
5.9 KiB
Ucode
// LXC management endpoint
|
|
// Copyright 2026 Paul Donald <newtwen+github@gmail.com>
|
|
// Licensed to the public under the Apache License 2.0.
|
|
// Built against the lxc API v6
|
|
'use strict';
|
|
|
|
import * as fs from 'fs';
|
|
import { cursor } from 'uci';
|
|
import { connect } from 'ubus';
|
|
const ctx = cursor();
|
|
const LXC_URL = ctx.get('lxc', 'lxc', 'url');
|
|
|
|
function shellquote(value) {
|
|
if (value == null)
|
|
value = '';
|
|
|
|
return "'" + replace(value, "'", "'\\''") + "'";
|
|
}
|
|
|
|
function is_valid_lxc_name(value) {
|
|
// LXC container names: start with alphanumeric or underscore, followed by
|
|
// alphanumeric, underscore, dash. No periods, no slashes, no leading dash
|
|
return type(value) == 'string' && match(value, /^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$/) != null;
|
|
}
|
|
|
|
function is_valid_lxc_template(value) {
|
|
return type(value) == 'string' && match(value, /^.+:.+$/) != null;
|
|
}
|
|
|
|
function statfs(path) {
|
|
let p = fs.popen('df -kP ' + path);
|
|
p.read('line'); // header
|
|
let line = p.read('line'); // data line
|
|
p.close();
|
|
|
|
if (!line) return null;
|
|
let cols = split(trim(line), /\s+/);
|
|
// return {
|
|
// filesystem: cols[0],
|
|
// blocks_kb: int(cols[1],10),
|
|
// used_kb: int(cols[2],10),
|
|
// avail_kb: int(cols[3],10),
|
|
// used_pct: cols[4],
|
|
// mount: cols.slice(5).join(' ')
|
|
// };
|
|
return int(cols[3],10);
|
|
}
|
|
|
|
const LXCController = {
|
|
|
|
lxc_get_downloadable: function() {
|
|
let target = this.lxc_get_arch_target(LXC_URL);
|
|
let templates = [];
|
|
let content = fs.popen(`sh /usr/share/lxc/templates/lxc-download --list --server ${shellquote(LXC_URL)} 2>/dev/null`, 'r').read('all');
|
|
content = split(content, '\n');
|
|
for (let line in content) {
|
|
let arr = match(line, /^(\S+)\s+(\S+)\s+(\S+)\s+default\s+(\S+)\s*$/);
|
|
if(length(arr) < 3) continue;
|
|
let dist = trim(arr[1]);
|
|
let version = trim(arr[2]);
|
|
let dist_target = trim(arr[3]);
|
|
if (dist && version && dist_target && dist_target == target)
|
|
push(templates, `${ dist }:${ version }`);
|
|
}
|
|
// content.close();
|
|
|
|
http.prepare_content('application/json');
|
|
http.write_json(templates);
|
|
},
|
|
|
|
lxc_create: function(lxc_name, lxc_template) {
|
|
http.prepare_content('text/plain');
|
|
if (!is_valid_lxc_name(lxc_name)) {
|
|
return;
|
|
}
|
|
if (!is_valid_lxc_template(lxc_template)) {
|
|
return;
|
|
}
|
|
|
|
let path = this.lxc_get_config_path();
|
|
if (!path) return;
|
|
let arr = match(lxc_template, /^(.+):(.+)$/);
|
|
let lxc_dist = arr[1], lxc_release = arr[2];
|
|
|
|
system(`/usr/bin/lxc-create --quiet --name ${shellquote(lxc_name)} --bdev best --template download -- --dist ${shellquote(lxc_dist)} --release ${shellquote(lxc_release)} --arch ${this.lxc_get_arch_target(LXC_URL)} --server ${shellquote(LXC_URL)}`);
|
|
|
|
while (fs.access(path + lxc_name + '/partial')) {
|
|
sleep(1000);
|
|
}
|
|
|
|
http.write('0');
|
|
},
|
|
|
|
lxc_action: function(lxc_action, lxc_name) {
|
|
let ubus = connect();
|
|
let data = ubus.call('lxc', lxc_action, { name: lxc_name });
|
|
|
|
http.prepare_content('application/json');
|
|
http.write_json(data ? data : '');
|
|
},
|
|
|
|
lxc_get_config_path: function() {
|
|
let content = fs.readfile('/etc/lxc/lxc.conf');
|
|
let ret = match(content, /^\s*lxc.lxcpath\s*=\s*(\S*)/);
|
|
if (ret && length(ret) == 2) {
|
|
if (fs.access(ret[1])) {
|
|
let min_space = int(ctx.get('lxc', 'lxc', 'min_space')) || 100000;
|
|
let free_space = statfs(ret[1]);
|
|
if (free_space && free_space >= min_space) {
|
|
let min_temp = int(ctx.get('lxc', 'lxc', 'min_temp')) || 100000;
|
|
let free_temp = statfs('/tmp');
|
|
if (free_temp && free_temp >= min_temp)
|
|
return ret[1] + '/';
|
|
else
|
|
return 'lxc error: not enough temporary space (< ' + min_temp + ' KB)';
|
|
}
|
|
else
|
|
return 'lxc error: not enough space (< ' + min_space + ' KB)';
|
|
}
|
|
else
|
|
return 'lxc error: directory not found';
|
|
}
|
|
else
|
|
return 'lxc error: config path is empty';
|
|
},
|
|
|
|
lxc_configuration_get: function(lxc_name) {
|
|
http.prepare_content('text/plain');
|
|
|
|
// Re-validate before fs.readfile as lxc_name,
|
|
// escapes the lxcpath base and reaches arbitrary files
|
|
if (!is_valid_lxc_name(lxc_name)) {
|
|
http.status(400, 'Bad Request');
|
|
return;
|
|
}
|
|
|
|
// lxc_get_config_path() returns an 'lxc error: …' string on failure
|
|
// rather than null. Refuse to proceed instead of concatenating that
|
|
// into a real filesystem path
|
|
let base = this.lxc_get_config_path();
|
|
if (!base || index(base, 'lxc error') == 0) {
|
|
http.status(500, base);
|
|
return;
|
|
}
|
|
let content = fs.readfile(base + lxc_name + '/config');
|
|
http.write(content);
|
|
},
|
|
|
|
lxc_configuration_set: function(lxc_name) {
|
|
http.prepare_content('text/plain');
|
|
|
|
// Re-validate before fs.writefile as lxc_name,
|
|
// escapes the lxcpath
|
|
if (!is_valid_lxc_name(lxc_name)) {
|
|
http.status(400, 'Bad Request');
|
|
return;
|
|
}
|
|
|
|
// lxc_get_config_path() returns an 'lxc error: …' string on failure
|
|
// rather than null. Refuse to proceed instead of concatenating that
|
|
// into a real filesystem path
|
|
let base = this.lxc_get_config_path();
|
|
if (!base || index(base, 'lxc error') == 0) {
|
|
http.status(500, base);
|
|
return;
|
|
}
|
|
let lxc_configuration = http.formvalue('lxc_conf');
|
|
lxc_configuration = http.urldecode(lxc_configuration, true);
|
|
if (!lxc_configuration) {
|
|
return 'lxc error: config formvalue is empty';
|
|
}
|
|
fs.writefile(base + lxc_name + '/config', lxc_configuration);
|
|
http.write('0');
|
|
},
|
|
|
|
lxc_get_arch_target: function(url) {
|
|
let target = split(fs.popen('uname -m', 'r').read('line'), '\n');
|
|
if (url && match(url, /images.linuxcontainers.org/)) {
|
|
let target_map = {
|
|
armv5: 'armel',
|
|
armv6: 'armel',
|
|
armv7: 'armhf',
|
|
armv8: 'arm64',
|
|
aarch64:'arm64',
|
|
i686 : 'i386',
|
|
x86_64: 'amd64',
|
|
};
|
|
for (let k, v in target_map) {
|
|
if (target[0] == k) {
|
|
return v;
|
|
}
|
|
}
|
|
}
|
|
return target[0];
|
|
},
|
|
};
|
|
|
|
// Export all handlers with automatic error wrapping
|
|
let controller = LXCController;
|
|
let exports = {};
|
|
for (let k, v in controller) {
|
|
if (type(v) == 'function')
|
|
exports[k] = v;
|
|
}
|
|
|
|
return exports;
|