diff options
author | Matthew Hagan <mnhagan88@gmail.com> | 2021-10-14 20:05:31 +0100 |
---|---|---|
committer | Matthew Hagan <mnhagan88@gmail.com> | 2022-08-10 23:03:54 +0100 |
commit | 1f01a661c9e39da4e2fcf2a77cbd2bfeb3d0bc93 (patch) | |
tree | a351bd4a6578056844eeb3feb3546ef3e6ea3e12 | |
parent | ab25dd8e58f3654a394a24203ce6f04cecc951e0 (diff) |
luci-proto-openfortivpn: add user, key, CA PEM support
Add PEM inputs and file handling for user cert, key and CA cert. This
handling is largely based upon that used in luci-proto-openconnect.
Signed-off-by: Matthew Hagan <mnhagan88@gmail.com>
3 files changed, 188 insertions, 0 deletions
diff --git a/protocols/luci-proto-openfortivpn/htdocs/luci-static/resources/protocol/openfortivpn.js b/protocols/luci-proto-openfortivpn/htdocs/luci-static/resources/protocol/openfortivpn.js index d0e6bdb011..a86875bce7 100644 --- a/protocols/luci-proto-openfortivpn/htdocs/luci-static/resources/protocol/openfortivpn.js +++ b/protocols/luci-proto-openfortivpn/htdocs/luci-static/resources/protocol/openfortivpn.js @@ -4,8 +4,59 @@ 'require network'; 'require tools.widgets as widgets'; +var callGetCertificateFiles = rpc.declare({ + object: 'luci.openfortivpn', + method: 'getCertificates', + params: [ 'interface' ], + expect: { '': {} } +}); + +var callSetCertificateFiles = rpc.declare({ + object: 'luci.openfortivpn', + method: 'setCertificates', + params: [ 'interface', 'user_cert', 'user_key', 'ca_file' ], + expect: { '': {} } +}); + network.registerPatternVirtual(/^vpn-.+$/); +function sanitizeCert(s) { + if (typeof(s) != 'string') + return null; + + s = s.trim(); + + if (s == '') + return null; + + s = s.replace(/\r\n?/g, '\n'); + + if (!s.match(/\n$/)) + s += '\n'; + + return s; +} + +function validateCert(priv, section_id, value) { + var lines = value.trim().split(/[\r\n]/), + start = false, + i; + + if (value === null || value === '') + return true; + + for (i = 0; i < lines.length; i++) { + if (lines[i].match(/^-{5}BEGIN ((|RSA |DSA )PRIVATE KEY|(|TRUSTED |X509 )CERTIFICATE)-{5}$/)) + start = true; + else if (start && !lines[i].match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/)) + break; + } + + if (!start || i < lines.length - 1 || !lines[i].match(/^-{5}END ((|RSA |DSA )PRIVATE KEY|(|TRUSTED |X509 )CERTIFICATE)-{5}$/)) + return _('This does not look like a valid PEM file'); + + return true; +} return network.registerProtocol('openfortivpn', { getI18n: function() { @@ -52,6 +103,42 @@ return network.registerProtocol('openfortivpn', { o = s.taboption('general', form.Value, 'password', _('Password')); o.password = true; + o = s.taboption('general', form.TextValue, 'user_cert', _('User certificate (PEM encoded)')); + o.rows = 10; + o.monospace = true; + o.validate = L.bind(validateCert, o, false); + o.load = function(section_id) { + var certLoadPromise = certLoadPromise || callGetCertificateFiles(section_id); + return certLoadPromise.then(function(certs) { return certs.user_cert }); + }; + o.write = function(section_id, value) { + return callSetCertificateFiles(section_id, sanitizeCert(value), null, null); + }; + + o = s.taboption('general', form.TextValue, 'user_key', _('User key (PEM encoded)')); + o.rows = 10; + o.monospace = true; + o.validate = L.bind(validateCert, o, true); + o.load = function(section_id) { + var certLoadPromise = certLoadPromise || callGetCertificateFiles(section_id); + return certLoadPromise.then(function(certs) { return certs.user_key }); + }; + o.write = function(section_id, value) { + return callSetCertificateFiles(section_id, null, sanitizeCert(value), null); + }; + + o = s.taboption('general', form.TextValue, 'ca_file', _('CA certificate (PEM encoded; Use instead of system-wide store to verify the gateway certificate.')); + o.rows = 10; + o.monospace = true; + o.validate = L.bind(validateCert, o, false); + o.load = function(section_id) { + var certLoadPromise = certLoadPromise || callGetCertificateFiles(section_id); + return certLoadPromise.then(function(certs) { return certs.ca_file }); + }; + o.write = function(section_id, value) { + return callSetCertificateFiles(section_id, null, null, sanitizeCert(value)); + }; + o = s.taboption('advanced', widgets.NetworkSelect, 'tunlink', _('Bind interface'), _('Bind the tunnel to this interface (optional).')); o.exclude = s.section; o.nocreate = true; diff --git a/protocols/luci-proto-openfortivpn/root/usr/libexec/rpcd/luci.openfortivpn b/protocols/luci-proto-openfortivpn/root/usr/libexec/rpcd/luci.openfortivpn new file mode 100755 index 0000000000..caca8fcaa5 --- /dev/null +++ b/protocols/luci-proto-openfortivpn/root/usr/libexec/rpcd/luci.openfortivpn @@ -0,0 +1,86 @@ +#!/usr/bin/env lua + +local json = require "luci.jsonc" +local fs = require "nixio.fs" + +local function readfile(path) + if fs.stat(path, "type") == "reg" then + local s = fs.readfile(path) + return s and (s:gsub("^%s+", ""):gsub("%s+$", "")) + else + return null + end +end + +local function writefile(path, data) + local n = fs.writefile(path, data) + return (n == #data) +end + +local function parseInput() + local parse = json.new() + local done, err + + while true do + local chunk = io.read(4096) + if not chunk then + break + elseif not done and not err then + done, err = parse:parse(chunk) + end + end + + if not done then + print(json.stringify({ error = err or "Incomplete input" })) + os.exit(1) + end + + return parse:get() +end + +if arg[1] == "list" then + print(json.stringify({ + getCertificates = { + interface = "interface" + }, + setCertificates = { + interface = "interface", + user_cert = "user_cert", + user_key = "user_key", + ca_file = "ca_file" + } + })) +elseif arg[1] == "call" then + local args = parseInput() + + if not args.interface or + type(args.interface) ~= "string" or + not args.interface:match("^[a-zA-Z0-9_]+$") + then + print(json.stringify({ error = "Invalid interface name" })) + os.exit(1) + end + + local user_cert_pem = string.format("/etc/openfortivpn/user-cert-%s.pem", args.interface) + local user_key_pem = string.format("/etc/openfortivpn/user-key-%s.pem", args.interface) + local ca_file_pem = string.format("/etc/openfortivpn/ca-%s.pem", args.interface) + + if arg[2] == "getCertificates" then + print(json.stringify({ + user_cert = readfile(user_cert_pem), + user_key = readfile(user_key_pem), + ca_file = readfile(ca_file_pem) + })) + elseif arg[2] == "setCertificates" then + if args.user_cert then + writefile(user_cert_pem, args.user_cert) + end + if args.user_key then + writefile(user_key_pem, args.user_key) + end + if args.ca_file then + writefile(ca_file_pem, args.ca_file) + end + print(json.stringify({ result = true })) + end +end diff --git a/protocols/luci-proto-openfortivpn/root/usr/share/rpcd/acl.d/luci-openfortivpn.json b/protocols/luci-proto-openfortivpn/root/usr/share/rpcd/acl.d/luci-openfortivpn.json new file mode 100644 index 0000000000..5682928863 --- /dev/null +++ b/protocols/luci-proto-openfortivpn/root/usr/share/rpcd/acl.d/luci-openfortivpn.json @@ -0,0 +1,15 @@ +{ + "luci-proto-openfortivpn": { + "description": "Grant access to LuCI openfortivpn procedures", + "read": { + "ubus": { + "luci.openfortivpn": [ "getCertificates" ] + } + }, + "write": { + "ubus": { + "luci.openfortivpn": [ "setCertificates" ] + } + } + } +} |