2cec969724
also rearranged the regex groups to allow sk-ecdsa-sha2-... note: FIDO keys are now parsable, the backend only supports it with OpenSSH >= 8.2. This may be achieved with upgrading to PVE 7 or using a back port.
606 lines
13 KiB
JavaScript
606 lines
13 KiB
JavaScript
// Some configuration values are complex strings - so we need parsers/generators for them.
|
|
Ext.define('PVE.Parser', {
|
|
statics: {
|
|
|
|
// this class only contains static functions
|
|
|
|
printACME: function(value) {
|
|
if (Ext.isArray(value.domains)) {
|
|
value.domains = value.domains.join(';');
|
|
}
|
|
return PVE.Parser.printPropertyString(value);
|
|
},
|
|
|
|
parseACME: function(value) {
|
|
if (!value) {
|
|
return {};
|
|
}
|
|
|
|
let res = {};
|
|
try {
|
|
value.split(',').forEach(property => {
|
|
let [k, v] = property.split('=', 2);
|
|
if (Ext.isDefined(v)) {
|
|
res[k] = v;
|
|
} else {
|
|
throw `Failed to parse key-value pair: ${property}`;
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.warn(err);
|
|
return undefined;
|
|
}
|
|
|
|
if (res.domains !== undefined) {
|
|
res.domains = res.domains.split(/;/);
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
parseBoolean: function(value, default_value) {
|
|
if (!Ext.isDefined(value)) {
|
|
return default_value;
|
|
}
|
|
value = value.toLowerCase();
|
|
return value === '1' ||
|
|
value === 'on' ||
|
|
value === 'yes' ||
|
|
value === 'true';
|
|
},
|
|
|
|
parsePropertyString: function(value, defaultKey) {
|
|
let res = {};
|
|
|
|
if (typeof value !== 'string' || value === '') {
|
|
return res;
|
|
}
|
|
|
|
try {
|
|
value.split(',').forEach(property => {
|
|
let [k, v] = property.split('=', 2);
|
|
if (Ext.isDefined(v)) {
|
|
res[k] = v;
|
|
} else if (Ext.isDefined(defaultKey)) {
|
|
if (Ext.isDefined(res[defaultKey])) {
|
|
throw 'defaultKey may be only defined once in propertyString';
|
|
}
|
|
res[defaultKey] = k; // k ist the value in this case
|
|
} else {
|
|
throw `Failed to parse key-value pair: ${property}`;
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.warn(err);
|
|
return undefined;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printPropertyString: function(data, defaultKey) {
|
|
var stringparts = [],
|
|
gotDefaultKeyVal = false,
|
|
defaultKeyVal;
|
|
|
|
Ext.Object.each(data, function(key, value) {
|
|
if (defaultKey !== undefined && key === defaultKey) {
|
|
gotDefaultKeyVal = true;
|
|
defaultKeyVal = value;
|
|
} else if (value !== '') {
|
|
stringparts.push(key + '=' + value);
|
|
}
|
|
});
|
|
|
|
stringparts = stringparts.sort();
|
|
if (gotDefaultKeyVal) {
|
|
stringparts.unshift(defaultKeyVal);
|
|
}
|
|
|
|
return stringparts.join(',');
|
|
},
|
|
|
|
parseQemuNetwork: function(key, value) {
|
|
if (!(key && value)) {
|
|
return undefined;
|
|
}
|
|
|
|
let res = {},
|
|
errors = false;
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return undefined; // continue
|
|
}
|
|
|
|
let match_res;
|
|
|
|
if ((match_res = p.match(/^(ne2k_pci|e1000|e1000-82540em|e1000-82544gc|e1000-82545em|vmxnet3|rtl8139|pcnet|virtio|ne2k_isa|i82551|i82557b|i82559er)(=([0-9a-f]{2}(:[0-9a-f]{2}){5}))?$/i)) !== null) {
|
|
res.model = match_res[1].toLowerCase();
|
|
if (match_res[3]) {
|
|
res.macaddr = match_res[3];
|
|
}
|
|
} else if ((match_res = p.match(/^bridge=(\S+)$/)) !== null) {
|
|
res.bridge = match_res[1];
|
|
} else if ((match_res = p.match(/^rate=(\d+(\.\d+)?)$/)) !== null) {
|
|
res.rate = match_res[1];
|
|
} else if ((match_res = p.match(/^tag=(\d+(\.\d+)?)$/)) !== null) {
|
|
res.tag = match_res[1];
|
|
} else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
|
|
res.firewall = match_res[1];
|
|
} else if ((match_res = p.match(/^link_down=(\d+)$/)) !== null) {
|
|
res.disconnect = match_res[1];
|
|
} else if ((match_res = p.match(/^queues=(\d+)$/)) !== null) {
|
|
res.queues = match_res[1];
|
|
} else if ((match_res = p.match(/^trunks=(\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*)$/)) !== null) {
|
|
res.trunks = match_res[1];
|
|
} else if ((match_res = p.match(/^mtu=(\d+)$/)) !== null) {
|
|
res.mtu = match_res[1];
|
|
} else {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
return undefined; // continue
|
|
});
|
|
|
|
if (errors || !res.model) {
|
|
return undefined;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printQemuNetwork: function(net) {
|
|
var netstr = net.model;
|
|
if (net.macaddr) {
|
|
netstr += "=" + net.macaddr;
|
|
}
|
|
if (net.bridge) {
|
|
netstr += ",bridge=" + net.bridge;
|
|
if (net.tag) {
|
|
netstr += ",tag=" + net.tag;
|
|
}
|
|
if (net.firewall) {
|
|
netstr += ",firewall=" + net.firewall;
|
|
}
|
|
}
|
|
if (net.rate) {
|
|
netstr += ",rate=" + net.rate;
|
|
}
|
|
if (net.queues) {
|
|
netstr += ",queues=" + net.queues;
|
|
}
|
|
if (net.disconnect) {
|
|
netstr += ",link_down=" + net.disconnect;
|
|
}
|
|
if (net.trunks) {
|
|
netstr += ",trunks=" + net.trunks;
|
|
}
|
|
if (net.mtu) {
|
|
netstr += ",mtu=" + net.mtu;
|
|
}
|
|
return netstr;
|
|
},
|
|
|
|
parseQemuDrive: function(key, value) {
|
|
if (!(key && value)) {
|
|
return undefined;
|
|
}
|
|
|
|
const [, bus, index] = key.match(/^([a-z]+)(\d+)$/);
|
|
if (!bus) {
|
|
return undefined;
|
|
}
|
|
let res = {
|
|
'interface': bus,
|
|
index,
|
|
};
|
|
|
|
var errors = false;
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return undefined; // continue
|
|
}
|
|
let match = p.match(/^([a-z_]+)=(\S+)$/);
|
|
if (!match) {
|
|
if (!p.match(/[=]/)) {
|
|
res.file = p;
|
|
return undefined; // continue
|
|
}
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
let [, k, v] = match;
|
|
if (k === 'volume') {
|
|
k = 'file';
|
|
}
|
|
|
|
if (Ext.isDefined(res[k])) {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
|
|
if (k === 'cache' && v === 'off') {
|
|
v = 'none';
|
|
}
|
|
|
|
res[k] = v;
|
|
|
|
return undefined; // continue
|
|
});
|
|
|
|
if (errors || !res.file) {
|
|
return undefined;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printQemuDrive: function(drive) {
|
|
var drivestr = drive.file;
|
|
|
|
Ext.Object.each(drive, function(key, value) {
|
|
if (!Ext.isDefined(value) || key === 'file' ||
|
|
key === 'index' || key === 'interface') {
|
|
return; // continue
|
|
}
|
|
drivestr += ',' + key + '=' + value;
|
|
});
|
|
|
|
return drivestr;
|
|
},
|
|
|
|
parseIPConfig: function(key, value) {
|
|
if (!(key && value)) {
|
|
return undefined; // continue
|
|
}
|
|
|
|
let res = {};
|
|
try {
|
|
value.split(',').forEach(p => {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return; // continue
|
|
}
|
|
|
|
const match = p.match(/^(ip|gw|ip6|gw6)=(\S+)$/);
|
|
if (!match) {
|
|
throw `could not parse as IP config: ${p}`;
|
|
}
|
|
let [, k, v] = match;
|
|
res[k] = v;
|
|
});
|
|
} catch (err) {
|
|
console.warn(err);
|
|
return undefined; // continue
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printIPConfig: function(cfg) {
|
|
return Object.entries(cfg)
|
|
.filter(([k, v]) => v && k.match(/^(ip|gw|ip6|gw6)$/))
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join(',');
|
|
},
|
|
|
|
parseLxcNetwork: function(value) {
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
|
|
let data = {};
|
|
value.split(',').forEach(p => {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return; // continue
|
|
}
|
|
let match_res = p.match(/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|tag|rate)=(\S+)$/);
|
|
if (match_res) {
|
|
data[match_res[1]] = match_res[2];
|
|
} else if ((match_res = p.match(/^firewall=(\d+)$/)) !== null) {
|
|
data.firewall = PVE.Parser.parseBoolean(match_res[1]);
|
|
} else if (!p.match(/^type=\S+$/)) {
|
|
console.warn(`could not parse LXC network string ${p}`);
|
|
}
|
|
});
|
|
|
|
return data;
|
|
},
|
|
|
|
printLxcNetwork: function(config) {
|
|
let knownKeys = {
|
|
bridge: 1,
|
|
firewall: 1,
|
|
gw6: 1,
|
|
gw: 1,
|
|
hwaddr: 1,
|
|
ip6: 1,
|
|
ip: 1,
|
|
mtu: 1,
|
|
name: 1,
|
|
rate: 1,
|
|
tag: 1,
|
|
};
|
|
return Object.entries(config)
|
|
.filter(([k, v]) => v !== undefined && v !== '' && knownKeys[k])
|
|
.map(([k, v]) => `${k}=${v}`)
|
|
.join(',');
|
|
},
|
|
|
|
parseLxcMountPoint: function(value) {
|
|
if (!value) {
|
|
return undefined;
|
|
}
|
|
|
|
let res = {};
|
|
let errors = false;
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return undefined; // continue
|
|
}
|
|
let match = p.match(/^([a-z_]+)=(.+)$/);
|
|
if (!match) {
|
|
if (!p.match(/[=]/)) {
|
|
res.file = p;
|
|
return undefined; // continue
|
|
}
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
let [, k, v] = match;
|
|
if (k === 'volume') {
|
|
k = 'file';
|
|
}
|
|
|
|
if (Ext.isDefined(res[k])) {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
|
|
res[k] = v;
|
|
|
|
return undefined;
|
|
});
|
|
|
|
if (errors || !res.file) {
|
|
return undefined;
|
|
}
|
|
|
|
const match = res.file.match(/^([a-z][a-z0-9\-_.]*[a-z0-9]):/i);
|
|
if (match) {
|
|
res.storage = match[1];
|
|
res.type = 'volume';
|
|
} else if (res.file.match(/^\/dev\//)) {
|
|
res.type = 'device';
|
|
} else {
|
|
res.type = 'bind';
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printLxcMountPoint: function(mp) {
|
|
let drivestr = mp.file;
|
|
for (const [key, value] of Object.entries(mp)) {
|
|
if (!Ext.isDefined(value) || key === 'file' || key === 'type' || key === 'storage') {
|
|
continue;
|
|
}
|
|
drivestr += `,${key}=${value}`;
|
|
}
|
|
return drivestr;
|
|
},
|
|
|
|
parseStartup: function(value) {
|
|
if (value === undefined) {
|
|
return undefined;
|
|
}
|
|
|
|
let res = {};
|
|
try {
|
|
value.split(',').forEach(p => {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return; // continue
|
|
}
|
|
|
|
let match_res;
|
|
if ((match_res = p.match(/^(order)?=(\d+)$/)) !== null) {
|
|
res.order = match_res[2];
|
|
} else if ((match_res = p.match(/^up=(\d+)$/)) !== null) {
|
|
res.up = match_res[1];
|
|
} else if ((match_res = p.match(/^down=(\d+)$/)) !== null) {
|
|
res.down = match_res[1];
|
|
} else {
|
|
throw `could not parse startup config ${p}`;
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.warn(err);
|
|
return undefined;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printStartup: function(startup) {
|
|
let arr = [];
|
|
if (startup.order !== undefined && startup.order !== '') {
|
|
arr.push('order=' + startup.order);
|
|
}
|
|
if (startup.up !== undefined && startup.up !== '') {
|
|
arr.push('up=' + startup.up);
|
|
}
|
|
if (startup.down !== undefined && startup.down !== '') {
|
|
arr.push('down=' + startup.down);
|
|
}
|
|
|
|
return arr.join(',');
|
|
},
|
|
|
|
parseQemuSmbios1: function(value) {
|
|
let res = value.split(',').reduce((acc, currentValue) => {
|
|
const [k, v] = currentValue.split(/[=](.+)/);
|
|
acc[k] = v;
|
|
return acc;
|
|
}, {});
|
|
|
|
if (PVE.Parser.parseBoolean(res.base64, false)) {
|
|
for (const [k, v] of Object.entries(res)) {
|
|
if (k !== 'uuid') {
|
|
res[k] = Ext.util.Base64.decode(v);
|
|
}
|
|
}
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printQemuSmbios1: function(data) {
|
|
let base64 = false;
|
|
let datastr = Object.entries(data)
|
|
.map(([key, value]) => {
|
|
if (value === '') {
|
|
return undefined;
|
|
}
|
|
if (key !== 'uuid') {
|
|
base64 = true; // smbios values can be arbitrary, so encode and mark config as such
|
|
value = Ext.util.Base64.encode(value);
|
|
}
|
|
return `${key}=${value}`;
|
|
})
|
|
.filter(v => v !== undefined)
|
|
.join(',');
|
|
|
|
if (base64) {
|
|
datastr += ',base64=1';
|
|
}
|
|
return datastr;
|
|
},
|
|
|
|
parseTfaConfig: function(value) {
|
|
let res = {};
|
|
value.split(',').forEach(p => {
|
|
const [k, v] = p.split('=', 2);
|
|
res[k] = v;
|
|
});
|
|
|
|
return res;
|
|
},
|
|
|
|
parseTfaType: function(value) {
|
|
let match;
|
|
if (!value || !value.length) {
|
|
return undefined;
|
|
} else if (value === 'x!oath') {
|
|
return 'totp';
|
|
} else if ((match = value.match(/^x!(.+)$/)) !== null) {
|
|
return match[1];
|
|
} else {
|
|
return 1;
|
|
}
|
|
},
|
|
|
|
parseQemuCpu: function(value) {
|
|
if (!value) {
|
|
return {};
|
|
}
|
|
|
|
let res = {};
|
|
let errors = false;
|
|
Ext.Array.each(value.split(','), function(p) {
|
|
if (!p || p.match(/^\s*$/)) {
|
|
return undefined; // continue
|
|
}
|
|
|
|
if (!p.match(/[=]/)) {
|
|
if (Ext.isDefined(res.cpu)) {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
res.cputype = p;
|
|
return undefined; // continue
|
|
}
|
|
|
|
let match = p.match(/^([a-z_]+)=(\S+)$/);
|
|
if (!match || Ext.isDefined(res[match[1]])) {
|
|
errors = true;
|
|
return false; // break
|
|
}
|
|
|
|
let [, k, v] = match;
|
|
res[k] = v;
|
|
|
|
return undefined;
|
|
});
|
|
|
|
if (errors || !res.cputype) {
|
|
return undefined;
|
|
}
|
|
|
|
return res;
|
|
},
|
|
|
|
printQemuCpu: function(cpu) {
|
|
let cpustr = cpu.cputype;
|
|
let optstr = '';
|
|
|
|
Ext.Object.each(cpu, function(key, value) {
|
|
if (!Ext.isDefined(value) || key === 'cputype') {
|
|
return; // continue
|
|
}
|
|
optstr += ',' + key + '=' + value;
|
|
});
|
|
|
|
if (!cpustr) {
|
|
if (optstr) {
|
|
return 'kvm64' + optstr;
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
return cpustr + optstr;
|
|
},
|
|
|
|
parseSSHKey: function(key) {
|
|
// |--- options can have quotes--| type key comment
|
|
let keyre = /^(?:((?:[^\s"]|"(?:\\.|[^"\\])*")+)\s+)?(\S+)\s+(\S+)(?:\s+(.*))?$/;
|
|
let typere = /^(?:(?:sk-)?(?:ssh-(?:dss|rsa|ed25519)|ecdsa-sha2-nistp\d+)(?:@(?:[a-z0-9_-]+\.)+[a-z]{2,})?)$/;
|
|
|
|
let m = key.match(keyre);
|
|
if (!m || m.length < 3 || !m[2]) { // [2] is always either type or key
|
|
return null;
|
|
}
|
|
if (m[1] && m[1].match(typere)) {
|
|
return {
|
|
type: m[1],
|
|
key: m[2],
|
|
comment: m[3],
|
|
};
|
|
}
|
|
if (m[2].match(typere)) {
|
|
return {
|
|
options: m[1],
|
|
type: m[2],
|
|
key: m[3],
|
|
comment: m[4],
|
|
};
|
|
}
|
|
return null;
|
|
},
|
|
|
|
parseACMEPluginData: function(data) {
|
|
let res = {};
|
|
let extradata = [];
|
|
data.split('\n').forEach((line) => {
|
|
// capture everything after the first = as value
|
|
let [key, value] = line.split(/[=](.+)/);
|
|
if (value !== undefined) {
|
|
res[key] = value;
|
|
} else {
|
|
extradata.push(line);
|
|
}
|
|
});
|
|
return [res, extradata];
|
|
},
|
|
},
|
|
});
|