luci2: rework system administration view with fancier ssh pubkey handling
authorJo-Philipp Wich <jow@openwrt.org>
Mon, 14 Oct 2013 21:55:39 +0000 (21:55 +0000)
committerJo-Philipp Wich <jow@openwrt.org>
Mon, 14 Oct 2013 21:55:39 +0000 (21:55 +0000)
luci2/htdocs/luci2/view/system.admin.js

index d9ae8a2bf31842ea497548662eb3d3eea0dcb484..55826882894c90781768f15ed7a47c19389b42f1 100644 (file)
 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 = $('<div />')
+                .append($('<p />')
+                    .text(L.tr('Paste the public key line into the field below and press "%s" to continue.').format(L.tr('Ok'))))
+                .append($('<p />')
+                    .append($('<input />')
+                        .attr('type', 'text')
+                        .attr('placeholder', L.tr('Paste key here'))
+                        .css('width', '100%')))
+                .append($('<p />')
+                    .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'),
+                $('<pre />').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;
+
+                $('<div />')
+                    .addClass('cbi-input-dynlist')
+                    .append($('<input />')
+                        .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($('<img />')
+                        .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)
+                $('<br />').appendTo(div);
+
+            $('<input />')
+                .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 = $('<div />')
+                .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');
+        });
     }
 });