luci-mod-system: reimplement SSH key mgmt as client side view

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
This commit is contained in:
Jo-Philipp Wich 2019-09-15 20:00:36 +02:00
parent 3e1cf14ad1
commit a31d1d10e0
4 changed files with 98 additions and 122 deletions

View 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" ]
},

View 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
});

View 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

View file

@ -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%>