diff options
author | Jo-Philipp Wich <jo@mein.io> | 2019-09-15 20:00:36 +0200 |
---|---|---|
committer | Jo-Philipp Wich <jo@mein.io> | 2019-09-15 20:00:36 +0200 |
commit | a31d1d10e09183a999ab9736e0625415bd87fe25 (patch) | |
tree | 64f730c2ac1f06c32d87cf820aa809eba5bed3d4 | |
parent | 3e1cf14ad16147ef749ef77e86470a6d83df50ff (diff) |
luci-mod-system: reimplement SSH key mgmt as client side view
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
4 files changed, 98 insertions, 122 deletions
diff --git a/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json b/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json index d364508c2..e28bdfa72 100644 --- a/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json +++ b/modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json @@ -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" ] }, diff --git a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js index d298b3be9..a68cb6b0b 100644 --- a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js +++ b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js @@ -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 }); diff --git a/modules/luci-mod-system/luasrc/controller/admin/system.lua b/modules/luci-mod-system/luasrc/controller/admin/system.lua index b9785994a..be00a3f67 100644 --- a/modules/luci-mod-system/luasrc/controller/admin/system.lua +++ b/modules/luci-mod-system/luasrc/controller/admin/system.lua @@ -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 index ac453f3f6..000000000 --- a/modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm +++ /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%> |