luci-proto-openconnect: bug fixes for cert read and write methods

follow-up to: aa955d6465

Minor refactor of ucode, and some GUI fixes to ensure certificates are
written properly.

Signed-off-by: Paul Donald <newtwen+github@gmail.com>
This commit is contained in:
Paul Donald
2025-07-25 20:55:49 +02:00
parent 9cd1573d96
commit 58575727e5
2 changed files with 52 additions and 94 deletions

View File

@ -4,14 +4,14 @@
'require network';
'require validation';
var callGetCertificateFiles = rpc.declare({
const callGetCertificateFiles = rpc.declare({
object: 'luci.openconnect',
method: 'getCertificates',
params: [ 'interface' ],
expect: { '': {} }
});
var callSetCertificateFiles = rpc.declare({
const callSetCertificateFiles = rpc.declare({
object: 'luci.openconnect',
method: 'setCertificates',
params: [ 'interface', 'user_certificate', 'user_privatekey', 'ca_certificate' ],
@ -22,14 +22,14 @@ network.registerPatternVirtual(/^vpn-.+$/);
function sanitizeCert(s) {
if (typeof(s) != 'string')
return null;
return '';
s = s.trim();
if (s == '')
return null;
return s;
s = s.replace(/\r\n?/g, '\n');
s = s.replace(/\r?\n/g, '\n');
if (!s.match(/\n$/))
s += '\n';
@ -38,24 +38,22 @@ function sanitizeCert(s) {
}
function validateCert(priv, section_id, value) {
var beg = priv ? /^-----BEGIN (RSA )?PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/,
end = priv ? /^-----END (RSA )?PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/,
lines = value.trim().split(/[\r\n]/),
start = false,
i;
if (value === null || value === '')
if (!value?.trim())
return true;
for (i = 0; i < lines.length; i++) {
if (lines[i].match(beg))
start = true;
else if (start && !lines[i].match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/))
break;
}
const beg = priv ? /^-----BEGIN (RSA )?PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/;
const end = priv ? /^-----END (RSA )?PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/;
const lines = value.trim().split(/[\r?\n]/);
const base64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
const errmsg = _('This does not look like a valid PEM file');
if (!start || i < lines.length - 1 || !lines[i].match(end))
return _('This does not look like a valid PEM file');
if (!lines?.[0].match(beg) || !lines.at(-1).match(end))
return errmsg;
for (let i = 1; i < lines.length - 1; i++)
if (!base64.test(lines[i]))
return errmsg;
return true;
}
@ -90,9 +88,9 @@ return network.registerProtocol('openconnect', {
},
renderFormOptions: function(s) {
var dev = this.getDevice().getName(),
certLoadPromise = null,
o;
const dev = this.getDevice().getName();
let certLoadPromise = null;
let o;
o = s.taboption('general', form.ListValue, 'vpn_protocol', _('VPN Protocol'));
o.value('anyconnect', 'OpenConnect or Cisco AnyConnect SSL VPN');
@ -106,7 +104,7 @@ return network.registerProtocol('openconnect', {
o = s.taboption('general', form.Value, 'uri', _('VPN Server'));
o.placeholder = 'https://example.com:443/usergroup';
o.validate = function(section_id, value) {
var m = String(value).match(/^(?:(\w+):\/\/|)(?:\[([0-9a-f:.]{2,45})\]|([^\/:]+))(?::([0-9]{1,5}))?(?:\/.*)?$/i);
const m = String(value).match(/^(?:(\w+):\/\/|)(?:\[([0-9a-f:.]{2,45})\]|([^\/:]+))(?::([0-9]{1,5}))?(?:\/.*)?$/i);
if (!m)
return _('Invalid server URL');
@ -163,7 +161,7 @@ return network.registerProtocol('openconnect', {
return certLoadPromise.then(function(certs) { return certs.user_certificate });
};
o.write = function(section_id, value) {
return callSetCertificateFiles(section_id, sanitizeCert(value), null, null);
return callSetCertificateFiles(section_id, sanitizeCert(value), '', '');
};
o = s.taboption('general', form.TextValue, 'userkey', _('User key (PEM encoded)'));
@ -175,7 +173,7 @@ return network.registerProtocol('openconnect', {
return certLoadPromise.then(function(certs) { return certs.user_privatekey });
};
o.write = function(section_id, value) {
return callSetCertificateFiles(section_id, null, sanitizeCert(value), null);
return callSetCertificateFiles(section_id, '', sanitizeCert(value), '');
};
o = s.taboption('general', form.TextValue, 'ca', _('CA certificate; if empty it will be saved after the first connection.'));
@ -187,7 +185,7 @@ return network.registerProtocol('openconnect', {
return certLoadPromise.then(function(certs) { return certs.ca_certificate });
};
o.write = function(section_id, value) {
return callSetCertificateFiles(section_id, null, null, sanitizeCert(value));
return callSetCertificateFiles(section_id, '', '', sanitizeCert(value));
};
o = s.taboption('advanced', form.Value, 'mtu', _('Override MTU'));

View File

@ -1,41 +1,32 @@
#!/usr/bin/env ucode
'use strict';
import { readfile, writefile, stat } from 'fs';
const interfaceregex = /^[a-zA-Z0-9_]+$/;
const user_certificate_string = "/etc/openconnect/user-cert-vpn-%s.pem";
const user_privatekey_string = "/etc/openconnect/user-key-vpn-%s.pem";
const ca_certificate_string = "/etc/openconnect/ca-vpn-%s.pem";
const paths = {
user_certificate: "/etc/openconnect/user-cert-vpn-%s.pem",
user_privatekey: "/etc/openconnect/user-key-vpn-%s.pem",
ca_certificate: "/etc/openconnect/ca-vpn-%s.pem"
};
// Utility to read a file
function _readfile(path) {
let _stat = stat(path);
if (_stat && _stat.type == "file") {
let content = readfile(path);
return content ? trim(content) : 'File empty';
}
return 'File not found';
let s = stat(path);
return (s?.type == 'file') ? trim(readfile(path) ?? '') || 'File empty' : null;
}
// Utility to write a file
function _writefile(path, data) {
if (!data) {
return false;
}
return writefile(path, data) == length(data);
return data ? writefile(path, data) == length(data) : false;
}
function is_valid_iface(ifname) {
return ifname && match(ifname, interfaceregex);
}
const methods = {
list:{
list: {
call: function() {
return {
getCertificates: {
interface: "interface"
},
getCertificates: { interface: "interface" },
setCertificates: {
interface: "interface",
user_certificate: "user_certificate",
@ -47,29 +38,16 @@ const methods = {
},
getCertificates: {
args: {
interface: "interface",
},
args: { interface: "interface" },
call: function(req) {
let iface = req.args?.interface;
if (!is_valid_iface(iface)) return;
const _interface = req.args?.interface;
if (!_interface || !match(_interface, interfaceregex)) {
// printf("Invalid interface name");
return;
}
const user_certificate_pem = _readfile(sprintf(user_certificate_string, _interface));
const user_privatekey_pem = _readfile(sprintf(user_privatekey_string, _interface));
const ca_certificate_pem = _readfile(sprintf(ca_certificate_string, _interface));
if(user_certificate_pem && user_privatekey_pem && ca_certificate_pem){
return {
user_certificate: user_certificate_pem,
user_privatekey: user_privatekey_pem,
ca_certificate: ca_certificate_pem,
};
}
let result = {};
for (let k in paths)
result[k] = _readfile(sprintf(paths[k], iface));
return result;
}
},
@ -81,35 +59,17 @@ const methods = {
ca_certificate: "ca_certificate",
},
call: function(req) {
let iface = req.args?.interface;
if (!is_valid_iface(iface)) return;
let result = false;
let _interface = req.args?.interface;
if (!_interface || !match(_interface, interfaceregex)) {
// printf("Invalid interface name");
return;
for (let k in paths) {
if (req.args?.[k])
result = _writefile(sprintf(paths[k], iface), req.args[k]);
}
/* the interface is set up to call 1 write per certificate,
with only one of the following arguments not null */
if (req.args?.user_certificate) {
result = _writefile(sprintf(user_certificate_string, _interface), req.args?.user_certificate);
}
if (req.args?.user_privatekey) {
result = _writefile(sprintf(user_privatekey_string, _interface), req.args?.user_privatekey);
}
if (req.args?.ca_certificate) {
result = _writefile(sprintf(ca_certificate_string, _interface), req.args?.ca_certificate);
}
return {
result: result,
};
return { result: result };
}
}
};
return { 'luci.openconnect': methods };