mirror of
				https://git.openwrt.org/project/luci.git
				synced 2025-10-31 10:49:03 +08:00 
			
		
		
		
	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:
		| @ -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')); | ||||
|  | ||||
| @ -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 }; | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Paul Donald
					Paul Donald