luci-mod-system: reimplement SSH key mgmt as client side view
authorJo-Philipp Wich <jo@mein.io>
Sun, 15 Sep 2019 18:00:36 +0000 (20:00 +0200)
committerJo-Philipp Wich <jo@mein.io>
Sun, 15 Sep 2019 18:00:36 +0000 (20:00 +0200)
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json
modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js
modules/luci-mod-system/luasrc/controller/admin/system.lua
modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm [deleted file]

index d364508c27cc2fe30b8bc2e309a22eaaf41d239c..e28bdfa72768aa22d68270bc5dbe0e61cb5d8e1d 100644 (file)
@@ -24,6 +24,7 @@
                                "/": [ "list" ],
                                "/*": [ "list" ],
                                "/etc/crontabs/root": [ "read" ],
+                               "/etc/dropbear/authorized_keys": [ "read" ],
                                "/etc/rc.local": [ "read" ],
                                "/proc/sys/kernel/hostname": [ "read" ]
                        },
@@ -42,6 +43,7 @@
                        "cgi-io": [ "upload", "/etc/luci-uploads/*" ],
                        "file": {
                                "/etc/crontabs/root": [ "write" ],
+                               "/etc/dropbear/authorized_keys": [ "write" ],
                                "/etc/luci-uploads/*": [ "write" ],
                                "/etc/rc.local": [ "write" ]
                        },
index d298b3be9842b8f7f2512f16313dc4d27aae5822..a68cb6b0bf800ce15885fe1c2a701963f7539f0e 100644 (file)
@@ -1,4 +1,7 @@
-SSHPubkeyDecoder.prototype = {
+'use strict';
+'require rpc';
+
+var SSHPubkeyDecoder = L.Class.singleton({
        lengthDecode: function(s, off)
        {
                var l = (s.charCodeAt(off++) << 24) |
@@ -85,19 +88,29 @@ SSHPubkeyDecoder.prototype = {
                        return null;
                }
        }
-};
+});
 
-function SSHPubkeyDecoder() {}
+var callFileRead = rpc.declare({
+       object: 'file',
+       method: 'read',
+       params: [ 'path' ],
+       expect: { data: '' }
+});
+
+var callFileWrite = rpc.declare({
+       object: 'file',
+       method: 'write',
+       params: [ 'path', 'data' ]
+});
 
 function renderKeys(keys) {
-       var list = document.querySelector('.cbi-dynlist[name="sshkeys"]'),
-           decoder = new SSHPubkeyDecoder();
+       var list = document.querySelector('.cbi-dynlist[name="sshkeys"]');
 
        while (!matchesElem(list.firstElementChild, '.add-item'))
                list.removeChild(list.firstElementChild);
 
        keys.forEach(function(key) {
-               var pubkey = decoder.decode(key);
+               var pubkey = SSHPubkeyDecoder.decode(key);
                if (pubkey)
                        list.insertBefore(E('div', {
                                class: 'item',
@@ -117,19 +130,16 @@ function renderKeys(keys) {
 }
 
 function saveKeys(keys) {
-       L.showModal(_('Add key'), E('div', { class: 'spinning' }, _('Saving keys…')));
-       L.post('admin/system/admin/sshkeys/json', { keys: JSON.stringify(keys) }, function(xhr, keys) {
-               renderKeys(keys);
-               L.hideModal();
-       });
+       return callFileWrite('/etc/dropbear/authorized_keys', keys.join('\n') + '\n')
+               .then(renderKeys.bind(this, keys))
+               .then(L.ui.hideModal);
 }
 
 function addKey(ev) {
-       var decoder = new SSHPubkeyDecoder(),
-           list = findParent(ev.target, '.cbi-dynlist'),
+       var list = findParent(ev.target, '.cbi-dynlist'),
            input = list.querySelector('input[type="text"]'),
            key = input.value.trim(),
-           pubkey = decoder.decode(key),
+           pubkey = SSHPubkeyDecoder.decode(key),
            keys = [];
 
        if (!key.length)
@@ -140,21 +150,26 @@ function addKey(ev) {
        });
 
        if (keys.indexOf(key) !== -1) {
-               L.showModal(_('Add key'), [
+               L.ui.showModal(_('Add key'), [
                        E('div', { class: 'alert-message warning' }, _('The given SSH public key has already been added.')),
                        E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close')))
                ]);
        }
        else if (!pubkey) {
-               L.showModal(_('Add key'), [
+               L.ui.showModal(_('Add key'), [
                        E('div', { class: 'alert-message warning' }, _('The given SSH public key is invalid. Please supply proper public RSA or ECDSA keys.')),
                        E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close')))
                ]);
        }
        else {
                keys.push(key);
-               saveKeys(keys);
                input.value = '';
+
+               return saveKeys(keys).then(function() {
+                       var added = list.querySelector('[data-key="%s"]'.format(key));
+                       if (added)
+                               added.classList.add('flash');
+               });
        }
 }
 
@@ -175,7 +190,7 @@ function removeKey(ev) {
                E('div', { class: 'right' }, [
                        E('div', { class: 'btn', click: L.hideModal }, _('Cancel')),
                        ' ',
-                       E('div', { class: 'btn danger', click: function(ev) { saveKeys(keys) } }, _('Delete key')),
+                       E('div', { class: 'btn danger', click: L.ui.createHandlerFn(this, saveKeys, keys) }, _('Delete key')),
                ])
        ]);
 }
@@ -205,11 +220,67 @@ function dropKey(ev) {
        ev.preventDefault();
 }
 
-window.addEventListener('dragover', function(ev) { ev.preventDefault() });
-window.addEventListener('drop', function(ev) { ev.preventDefault() });
+function handleWindowDragDropIgnore(ev) {
+       ev.preventDefault()
+}
 
-requestAnimationFrame(function() {
-       L.get('admin/system/admin/sshkeys/json', null, function(xhr, keys) {
-               renderKeys(keys);
-       });
+return L.view.extend({
+       load: function() {
+               return callFileRead('/etc/dropbear/authorized_keys').then(function(data) {
+                       return (data || '').split(/\n/).map(function(line) {
+                               return line.trim();
+                       }).filter(function(line) {
+                               return line.match(/^ssh-/) != null;
+                       });
+               });
+       },
+
+       render: function(keys) {
+               var list = E('div', { 'class': 'cbi-dynlist', 'dragover': dragKey, 'drop': dropKey }, [
+                       E('div', { 'class': 'add-item' }, [
+                               E('input', {
+                                       'class': 'cbi-input-text',
+                                       'type': 'text',
+                                       'placeholder': _('Paste or drag SSH key file…') ,
+                                       'keydown': function(ev) { if (ev.keyCode === 13) addKey(ev) }
+                               }),
+                               E('button', {
+                                       'class': 'cbi-button',
+                                       'click': L.ui.createHandlerFn(this, addKey)
+                               }, _('Add key'))
+                       ])
+               ]);
+
+               keys.forEach(L.bind(function(key) {
+                       var pubkey = SSHPubkeyDecoder.decode(key);
+                       if (pubkey)
+                               list.insertBefore(E('div', {
+                                       class: 'item',
+                                       click: L.ui.createHandlerFn(this, removeKey),
+                                       'data-key': key
+                               }, [
+                                       E('strong', pubkey.comment || _('Unnamed key')), E('br'),
+                                       E('small', [
+                                               '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
+                                               E('br'), E('code', pubkey.fprint)
+                                       ])
+                               ]), list.lastElementChild);
+               }, this));
+
+               if (list.firstElementChild === list.lastElementChild)
+                       list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild);
+
+               window.addEventListener('dragover', handleWindowDragDropIgnore);
+               window.addEventListener('drop', handleWindowDragDropIgnore);
+
+               return E('div', {}, [
+                       E('h2', _('SSH-Keys')),
+                       E('div', { 'class': 'cbi-section-descr' }, _('Public keys allow for the passwordless SSH logins with a higher security compared to the use of plain passwords. In order to upload a new key to the device, paste an OpenSSH compatible public key line or drag a <code>.pub</code> file into the input field.')),
+                       E('div', { 'class': 'cbi-section-node' }, list)
+               ]);
+       },
+
+       handleSaveApply: null,
+       handleSave: null,
+       handleReset: null
 });
index b9785994ad9b59e2153707dcb696574e8c5db40b..be00a3f678ef879b9089adf3ad4022e097b667b9 100644 (file)
@@ -17,8 +17,7 @@ function index()
 
        if fs.access("/etc/config/dropbear") then
                entry({"admin", "system", "admin", "dropbear"}, cbi("admin_system/dropbear"), _("SSH Access"), 2)
-               entry({"admin", "system", "admin", "sshkeys"}, template("admin_system/sshkeys"), _("SSH-Keys"), 3)
-               entry({"admin", "system", "admin", "sshkeys", "json"}, post_on({ keys = true }, "action_sshkeys"))
+               entry({"admin", "system", "admin", "sshkeys"}, view("system/sshkeys"), _("SSH-Keys"), 3)
        end
 
        entry({"admin", "system", "startup"}, view("system/startup"), _("Startup"), 45)
@@ -293,56 +292,6 @@ function action_password()
        luci.http.write_json({ code = luci.sys.user.setpasswd("root", password) })
 end
 
-function action_sshkeys()
-       local keys = luci.http.formvalue("keys")
-       if keys then
-               keys = luci.jsonc.parse(keys)
-               if not keys or type(keys) ~= "table" then
-                       luci.http.status(400, "Bad Request")
-                       return
-               end
-
-               local fd, err = io.open("/etc/dropbear/authorized_keys", "w")
-               if not fd then
-                       luci.http.status(503, err)
-                       return
-               end
-
-               local _, k
-               for _, k in ipairs(keys) do
-                       if type(k) == "string" and k:match("^%w+%-") then
-                               fd:write(k)
-                               fd:write("\n")
-                       end
-               end
-
-               fd:close()
-       end
-
-       local fd, err = io.open("/etc/dropbear/authorized_keys", "r")
-       if not fd then
-               luci.http.status(503, err)
-               return
-       end
-
-       local rv = {}
-       while true do
-               local ln = fd:read("*l")
-               if not ln then
-                       break
-               elseif ln:match("^[%w%-]+%s+[A-Za-z0-9+/=]+$") or
-                      ln:match("^[%w%-]+%s+[A-Za-z0-9+/=]+%s")
-               then
-                       rv[#rv+1] = ln
-               end
-       end
-
-       fd:close()
-
-       luci.http.prepare_content("application/json")
-       luci.http.write_json(rv)
-end
-
 function action_reboot()
        luci.sys.reboot()
 end
diff --git a/modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm b/modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm
deleted file mode 100644 (file)
index ac453f3..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-<%+header%>
-
-<style type="text/css">
-       .cbi-dynlist {
-               max-width: 100%;
-       }
-
-       .cbi-dynlist .item > small {
-               display: block;
-               direction: rtl;
-               overflow: hidden;
-               text-align: left;
-       }
-
-       .cbi-dynlist .item > small > code {
-               direction: ltr;
-               white-space: nowrap;
-               unicode-bidi: bidi-override;
-       }
-
-       @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) {
-               .cbi-dynlist .item > small { direction: ltr }
-       }
-</style>
-
-<div class="cbi-map">
-       <h2><%:SSH-Keys%></h2>
-
-       <div class="cbi-section-descr">
-               <%_Public keys allow for the passwordless SSH logins with a higher security compared to the use of plain passwords. In order to upload a new key to the device, paste an OpenSSH compatible public key line or drag a <code>.pub</code> file into the input field.%>
-       </div>
-
-       <div class="cbi-section-node">
-               <div class="cbi-dynlist" name="sshkeys">
-                       <p class="spinning"><%:Loading SSH keys…%></p>
-                       <div class="add-item" ondragover="dragKey(event)" ondrop="dropKey(event)">
-                               <input class="cbi-input-text" type="text" placeholder="<%:Paste or drag SSH key file…%>" onkeydown="if (event.keyCode === 13) addKey(event)" />
-                               <button class="cbi-button" onclick="addKey(event)"><%:Add key%></button>
-                       </div>
-               </div>
-       </div>
-</div>
-
-<script type="application/javascript" src="<%=resource%>/view/system/sshkeys.js"></script>
-
-<%+footer%>