luci-proto-openfortivpn: add user, key, CA PEM support
authorMatthew Hagan <mnhagan88@gmail.com>
Thu, 14 Oct 2021 19:05:31 +0000 (20:05 +0100)
committerMatthew Hagan <mnhagan88@gmail.com>
Wed, 10 Aug 2022 22:03:54 +0000 (23:03 +0100)
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>
protocols/luci-proto-openfortivpn/htdocs/luci-static/resources/protocol/openfortivpn.js
protocols/luci-proto-openfortivpn/root/usr/libexec/rpcd/luci.openfortivpn [new file with mode: 0755]
protocols/luci-proto-openfortivpn/root/usr/share/rpcd/acl.d/luci-openfortivpn.json [new file with mode: 0644]

index d0e6bdb01169e93870630bc59022532e3b0058fd..a86875bce762ca6a9cd5e88a50b6d7be2a041510 100644 (file)
@@ -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 (executable)
index 0000000..caca8fc
--- /dev/null
@@ -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 (file)
index 0000000..5682928
--- /dev/null
@@ -0,0 +1,15 @@
+{
+       "luci-proto-openfortivpn": {
+               "description": "Grant access to LuCI openfortivpn procedures",
+               "read": {
+                       "ubus": {
+                               "luci.openfortivpn": [ "getCertificates" ]
+                       }
+               },
+               "write": {
+                       "ubus": {
+                               "luci.openfortivpn": [ "setCertificates" ]
+                       }
+               }
+       }
+}