From 375aa260fd22b8db29f947baf306942dc47c1c11 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Mon, 14 Oct 2013 21:55:39 +0000 Subject: [PATCH] luci2: rework system administration view with fancier ssh pubkey handling --- luci2/htdocs/luci2/view/system.admin.js | 435 ++++++++++++++++++------ 1 file changed, 325 insertions(+), 110 deletions(-) diff --git a/luci2/htdocs/luci2/view/system.admin.js b/luci2/htdocs/luci2/view/system.admin.js index d9ae8a2..5582688 100644 --- a/luci2/htdocs/luci2/view/system.admin.js +++ b/luci2/htdocs/luci2/view/system.admin.js @@ -1,140 +1,355 @@ L.ui.view.extend({ - execute: function() { - var m = new L.cbi.Map('dropbear', { - caption: L.tr('SSH Access'), - description: L.tr('Dropbear offers SSH network shell access and an integrated SCP server'), - collabsible: true - }); + PubkeyListValue: L.cbi.AbstractValue.extend({ + base64Table: { + 'A': 0, 'B': 1, 'C': 2, 'D': 3, 'E': 4, 'F': 5, 'G': 6, + 'H': 7, 'I': 8, 'J': 9, 'K': 10, 'L': 11, 'M': 12, 'N': 13, + 'O': 14, 'P': 15, 'Q': 16, 'R': 17, 'S': 18, 'T': 19, 'U': 20, + 'V': 21, 'W': 22, 'X': 23, 'Y': 24, 'Z': 25, 'a': 26, 'b': 27, + 'c': 28, 'd': 29, 'e': 30, 'f': 31, 'g': 32, 'h': 33, 'i': 34, + 'j': 35, 'k': 36, 'l': 37, 'm': 38, 'n': 39, 'o': 40, 'p': 41, + 'q': 42, 'r': 43, 's': 44, 't': 45, 'u': 46, 'v': 47, 'w': 48, + 'x': 49, 'y': 50, 'z': 51, '0': 52, '1': 53, '2': 54, '3': 55, + '4': 56, '5': 57, '6': 58, '7': 59, '8': 60, '9': 61, '+': 62, + '/': 63, '=': 64 + }, - var s1 = m.section(L.cbi.DummySection, '__password', { - caption: L.tr('Router Password'), - description: L.tr('Changes the administrator password for accessing the device'), - readonly: !this.options.acls.admin - }); + base64Decode: function(s) + { + var i = 0; + var d = ''; - var p1 = s1.option(L.cbi.PasswordValue, 'pass1', { - caption: L.tr('Password'), - optional: true - }); + if (s.match(/[^A-Za-z0-9\+\/\=]/)) + return undefined; - var p2 = s1.option(L.cbi.PasswordValue, 'pass2', { - caption: L.tr('Confirmation'), - optional: true, - datatype: function(v) { - var v1 = p1.formvalue('__password'); - if (v1 && v1.length && v != v1) - return L.tr('Passwords must match!'); - return true; + while (i < s.length) + { + var e1 = this.base64Table[s.charAt(i++)]; + var e2 = this.base64Table[s.charAt(i++)]; + var e3 = this.base64Table[s.charAt(i++)]; + var e4 = this.base64Table[s.charAt(i++)]; + + var c1 = ( e1 << 2) | (e2 >> 4); + var c2 = ((e2 & 15) << 4) | (e3 >> 2); + var c3 = ((e3 & 3) << 6) | e4; + + d += String.fromCharCode(c1); + + if (e3 < 64) + d += String.fromCharCode(c2); + + if (e4 < 64) + d += String.fromCharCode(c3); } - }); - p1.save = function(sid) { }; - p2.save = function(sid) { - var v1 = p1.formvalue(sid); - var v2 = p2.formvalue(sid); - if (v2 && v2.length > 0 && v1 == v2) - return L.system.setPassword('root', v2); - }; - - - var s2 = m.section(L.cbi.TypedSection, 'dropbear', { - caption: L.tr('SSH Server'), - description: L.tr('This sections define listening instances of the builtin Dropbear SSH server'), - addremove: true, - add_caption: L.tr('Add instance ...'), - readonly: !this.options.acls.admin - }); + return d; + }, - s2.option(L.cbi.NetworkList, 'Interface', { - caption: L.tr('Interface'), - description: L.tr('Listen only on the given interface or, if unspecified, on all') - }); + lengthDecode: function(s, off) + { + var l = (s.charCodeAt(off++) << 24) | + (s.charCodeAt(off++) << 16) | + (s.charCodeAt(off++) << 8) | + s.charCodeAt(off++); - s2.option(L.cbi.InputValue, 'Port', { - caption: L.tr('Port'), - description: L.tr('Specifies the listening port of this Dropbear instance'), - datatype: 'port', - placeholder: 22, - optional: true - }); + if (l < 0 || (off + l) > s.length) + return -1; - s2.option(L.cbi.CheckboxValue, 'PasswordAuth', { - caption: L.tr('Password authentication'), - description: L.tr('Allow SSH password authentication'), - initial: true, - enabled: 'on', - disabled: 'off' - }); + return l; + }, - s2.option(L.cbi.CheckboxValue, 'RootPasswordAuth', { - caption: L.tr('Allow root logins with password'), - description: L.tr('Allow the root user to login with password'), - initial: true, - enabled: 'on', - disabled: 'off' - }); + pubkeyDecode: function(s) + { + var parts = s.split(/\s+/); + if (parts.length < 2) + return undefined; - s2.option(L.cbi.CheckboxValue, 'GatewayPorts', { - caption: L.tr('Gateway ports'), - description: L.tr('Allow remote hosts to connect to local SSH forwarded ports'), - initial: false, - enabled: 'on', - disabled: 'off' - }); + var key = this.base64Decode(parts[1]); + if (!key) + return undefined; + var off, len; - var s3 = m.section(L.cbi.TableSection, '__keys', { - caption: L.tr('SSH-Keys'), - description: L.tr('Here you can paste public SSH-Keys (one per line) for SSH public-key authentication.'), - addremove: true, - add_caption: L.tr('Add key ...'), - readonly: !this.options.acls.admin - }); + off = 0; + len = this.lengthDecode(key, off); + + if (len < 0) + return undefined; + + var type = key.substr(off + 4, len); + if (type != parts[0]) + return undefined; + + off += 4 + len; + + var len1 = this.lengthDecode(key, off); + if (len1 < 0) + return undefined; + + off += 4 + len1; + + var len2 = this.lengthDecode(key, off); + if (len2 < 0) + return undefined; + + if (len1 & 1) + len1--; + + if (len2 & 1) + len2--; + + switch (type) + { + case 'ssh-rsa': + return { type: 'RSA', bits: len2 * 8, comment: parts[2] }; - s3.sections = function() + case 'ssh-dss': + return { type: 'DSA', bits: len1 * 8, comment: parts[2] }; + + default: + return undefined; + } + }, + + _remove: function(ev) { - var k = [ ]; - var l = this.keys ? this.keys.length : 0; + var self = ev.data.self; - for (var i = 0; i < (l || 1); i++) - k.push({ '.name': i.toString() }); + self._keys.splice(ev.data.index, 1); + self._render(ev.data.div); + }, - return k; - }; + _add: function(ev) + { + var self = ev.data.self; - s3.add = function() { this.keys.push('') }; - s3.remove = function(sid) { this.keys.splice(parseInt(sid), 1) }; + var form = $('
') + .append($('

') + .text(L.tr('Paste the public key line into the field below and press "%s" to continue.').format(L.tr('Ok')))) + .append($('

') + .append($('') + .attr('type', 'text') + .attr('placeholder', L.tr('Paste key here')) + .css('width', '100%'))) + .append($('

') + .text(L.tr('Unrecognized public key! Please add only RSA or DSA keys.')) + .addClass('alert-message') + .hide()); - var c = s3.option(L.cbi.DummyValue, '__comment', { - caption: L.tr('Comment'), - width: '10%' - }); + L.ui.dialog(L.tr('Add new public key'), form, { + style: 'confirm', + confirm: function() { + var val = form.find('input').val(); + if (!val) + { + return; + } - c.ucivalue = function(sid) { - var key = (s3.keys[parseInt(sid)] || '').split(/\s+/) || [ ]; - return key[2] || '-'; - }; + var key = self.pubkeyDecode(val); + if (!key) + { + form.find('input').val(''); + form.find('.alert-message').show(); + return; + } - var k = s3.option(L.cbi.InputValue, '__key', { - caption: L.tr('Public key line'), - width: '90%', - datatype: function(key, elem) { - var elems = (key || '-').toString().split(/\s+/); + self._keys.push(val); + self._render(ev.data.div); - $('#' + elem.attr('id').replace(/key$/, 'comment')).text(elems[2] || ''); + L.ui.dialog(false); + } + }); + }, - if (elems.length < 2 || elems[0].indexOf('ssh-') != 0 || - !elems[1].match(/^(?:[A-Za-z0-9+/]{4})+(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/)) - return L.tr('Must be a valid SSH pubkey'); + _show: function(ev) + { + var self = ev.data.self; + + L.ui.dialog( + L.tr('Public key'), + $('

').text(self._keys[ev.data.index]),
+                { style: 'close' }
+            );
+        },
+
+        _render: function(div)
+        {
+            div.empty();
 
+            for (var i = 0; i < this._keys.length; i++)
+            {
+                var k = this.pubkeyDecode(this._keys[i] || '');
+
+                if (!k)
+                    continue;
+
+                $('
') + .addClass('cbi-input-dynlist') + .append($('') + .attr('type', 'text') + .prop('readonly', true) + .click({ self: this, index: i }, this._show) + .val('%dBit %s - %s'.format(k.bits, k.type, k.comment || '?'))) + .append($('') + .attr('src', L.globals.resource + '/icons/cbi/remove.gif') + .attr('title', L.tr('Remove public key')) + .click({ self: this, div: div, index: i }, this._remove) + .addClass('cbi-button')) + .appendTo(div); + } + + if (this._keys.length > 0) + $('
').appendTo(div); + + $('') + .attr('type', 'button') + .val(L.tr('Add public key …')) + .click({ self: this, div: div }, this._add) + .addClass('cbi-button') + .addClass('cbi-button-apply') + .appendTo(div); + }, + + widget: function(sid) + { + this._keys = [ ]; + + for (var i = 0; i < this.options.keys.length; i++) + this._keys.push(this.options.keys[i]); + + var d = $('
') + .attr('id', this.id(sid)); + + this._render(d); + + return d; + }, + + changed: function(sid) + { + if (this.options.keys.length != this._keys.length) return true; + + for (var i = 0; i < this.options.keys.length; i++) + if (this.options.keys[i] != this._keys[i]) + return true; + + return false; + }, + + save: function(sid) + { + if (this.changed(sid)) + { + this.options.keys = [ ]; + + for (var i = 0; i < this._keys.length; i++) + this.options.keys.push(this._keys[i]); + + return L.system.setSSHKeys(this._keys); } - }); - k.load = function(sid) { s3.keys = [ ]; return L.system.getSSHKeys().then(function(keys) { s3.keys = keys }) }; - k.save = function(sid) { if (sid == '0') return L.system.setSSHKeys(s3.keys) }; - k.ucivalue = function(sid) { return s3.keys[parseInt(sid)] }; + return undefined; + } + }), + + execute: function() { + var self = this; + return L.system.getSSHKeys().then(function(keys) { + var m = new L.cbi.Map('dropbear', { + caption: L.tr('SSH Access'), + description: L.tr('Dropbear offers SSH network shell access and an integrated SCP server') + }); + + var s1 = m.section(L.cbi.DummySection, '__password', { + caption: L.tr('Router Password'), + description: L.tr('Changes the administrator password for accessing the device'), + readonly: !self.options.acls.admin + }); + + var p1 = s1.option(L.cbi.PasswordValue, 'pass1', { + caption: L.tr('Password'), + optional: true + }); + + var p2 = s1.option(L.cbi.PasswordValue, 'pass2', { + caption: L.tr('Confirmation'), + optional: true, + datatype: function(v) { + var v1 = p1.formvalue('__password'); + if (v1 && v1.length && v != v1) + return L.tr('Passwords must match!'); + return true; + } + }); + + p1.save = function(sid) { }; + p2.save = function(sid) { + var v1 = p1.formvalue(sid); + var v2 = p2.formvalue(sid); + if (v2 && v2.length > 0 && v1 == v2) + return L.system.setPassword('root', v2); + }; + + + var s2 = m.section(L.cbi.DummySection, '__pubkeys', { + caption: L.tr('SSH-Keys'), + description: L.tr('Specifies public keys for passwordless SSH authentication'), + readonly: !self.options.acls.admin + }); - return m.insertInto('#map'); + var k = s2.option(self.PubkeyListValue, 'keys', { + caption: L.tr('Saved keys'), + keys: keys + }); + + + var s3 = m.section(L.cbi.TypedSection, 'dropbear', { + caption: L.tr('SSH Server'), + description: L.tr('This sections define listening instances of the builtin Dropbear SSH server'), + addremove: true, + add_caption: L.tr('Add instance ...'), + readonly: !self.options.acls.admin, + collabsible: true + }); + + s3.option(L.cbi.NetworkList, 'Interface', { + caption: L.tr('Interface'), + description: L.tr('Listen only on the given interface or, if unspecified, on all') + }); + + s3.option(L.cbi.InputValue, 'Port', { + caption: L.tr('Port'), + description: L.tr('Specifies the listening port of this Dropbear instance'), + datatype: 'port', + placeholder: 22, + optional: true + }); + + s3.option(L.cbi.CheckboxValue, 'PasswordAuth', { + caption: L.tr('Password authentication'), + description: L.tr('Allow SSH password authentication'), + initial: true, + enabled: 'on', + disabled: 'off' + }); + + s3.option(L.cbi.CheckboxValue, 'RootPasswordAuth', { + caption: L.tr('Allow root logins with password'), + description: L.tr('Allow the root user to login with password'), + initial: true, + enabled: 'on', + disabled: 'off' + }); + + s3.option(L.cbi.CheckboxValue, 'GatewayPorts', { + caption: L.tr('Gateway ports'), + description: L.tr('Allow remote hosts to connect to local SSH forwarded ports'), + initial: false, + enabled: 'on', + disabled: 'off' + }); + + return m.insertInto('#map'); + }); } }); -- 2.30.2