luci2: rework system administration view with fancier ssh pubkey handling
[project/luci2/ui.git] / luci2 / htdocs / luci2 / view / system.admin.js
1 L.ui.view.extend({
2     PubkeyListValue: L.cbi.AbstractValue.extend({
3         base64Table: {
4             'A':  0, 'B':  1, 'C':  2, 'D':  3, 'E':  4, 'F':  5, 'G':  6,
5             'H':  7, 'I':  8, 'J':  9, 'K': 10, 'L': 11, 'M': 12, 'N': 13,
6             'O': 14, 'P': 15, 'Q': 16, 'R': 17, 'S': 18, 'T': 19, 'U': 20,
7             'V': 21, 'W': 22, 'X': 23, 'Y': 24, 'Z': 25, 'a': 26, 'b': 27,
8             'c': 28, 'd': 29, 'e': 30, 'f': 31, 'g': 32, 'h': 33, 'i': 34,
9             'j': 35, 'k': 36, 'l': 37, 'm': 38, 'n': 39, 'o': 40, 'p': 41,
10             'q': 42, 'r': 43, 's': 44, 't': 45, 'u': 46, 'v': 47, 'w': 48,
11             'x': 49, 'y': 50, 'z': 51, '0': 52, '1': 53, '2': 54, '3': 55,
12             '4': 56, '5': 57, '6': 58, '7': 59, '8': 60, '9': 61, '+': 62,
13             '/': 63, '=': 64
14         },
15
16         base64Decode: function(s)
17         {
18             var i = 0;
19             var d = '';
20
21             if (s.match(/[^A-Za-z0-9\+\/\=]/))
22                 return undefined;
23
24             while (i < s.length)
25             {
26                 var e1 = this.base64Table[s.charAt(i++)];
27                 var e2 = this.base64Table[s.charAt(i++)];
28                 var e3 = this.base64Table[s.charAt(i++)];
29                 var e4 = this.base64Table[s.charAt(i++)];
30
31                 var c1 = ( e1       << 2) | (e2 >> 4);
32                 var c2 = ((e2 & 15) << 4) | (e3 >> 2);
33                 var c3 = ((e3 &  3) << 6) |  e4;
34
35                 d += String.fromCharCode(c1);
36
37                 if (e3 < 64)
38                     d += String.fromCharCode(c2);
39
40                 if (e4 < 64)
41                     d += String.fromCharCode(c3);
42             }
43
44             return d;
45         },
46
47         lengthDecode: function(s, off)
48         {
49             var l = (s.charCodeAt(off++) << 24) |
50                     (s.charCodeAt(off++) << 16) |
51                     (s.charCodeAt(off++) <<  8) |
52                      s.charCodeAt(off++);
53
54             if (l < 0 || (off + l) > s.length)
55                 return -1;
56
57             return l;
58         },
59
60         pubkeyDecode: function(s)
61         {
62             var parts = s.split(/\s+/);
63             if (parts.length < 2)
64                 return undefined;
65
66             var key = this.base64Decode(parts[1]);
67             if (!key)
68                 return undefined;
69
70             var off, len;
71
72             off = 0;
73             len = this.lengthDecode(key, off);
74
75             if (len < 0)
76                 return undefined;
77
78             var type = key.substr(off + 4, len);
79             if (type != parts[0])
80                 return undefined;
81
82             off += 4 + len;
83
84             var len1 = this.lengthDecode(key, off);
85             if (len1 < 0)
86                 return undefined;
87
88             off += 4 + len1;
89
90             var len2 = this.lengthDecode(key, off);
91             if (len2 < 0)
92                 return undefined;
93
94             if (len1 & 1)
95                 len1--;
96
97             if (len2 & 1)
98                 len2--;
99
100             switch (type)
101             {
102             case 'ssh-rsa':
103                 return { type: 'RSA', bits: len2 * 8, comment: parts[2] };
104
105             case 'ssh-dss':
106                 return { type: 'DSA', bits: len1 * 8, comment: parts[2] };
107
108             default:
109                 return undefined;
110             }
111         },
112
113         _remove: function(ev)
114         {
115             var self = ev.data.self;
116
117             self._keys.splice(ev.data.index, 1);
118             self._render(ev.data.div);
119         },
120
121         _add: function(ev)
122         {
123             var self = ev.data.self;
124
125             var form = $('<div />')
126                 .append($('<p />')
127                     .text(L.tr('Paste the public key line into the field below and press "%s" to continue.').format(L.tr('Ok'))))
128                 .append($('<p />')
129                     .append($('<input />')
130                         .attr('type', 'text')
131                         .attr('placeholder', L.tr('Paste key here'))
132                         .css('width', '100%')))
133                 .append($('<p />')
134                     .text(L.tr('Unrecognized public key! Please add only RSA or DSA keys.'))
135                     .addClass('alert-message')
136                     .hide());
137
138             L.ui.dialog(L.tr('Add new public key'), form, {
139                 style: 'confirm',
140                 confirm: function() {
141                     var val = form.find('input').val();
142                     if (!val)
143                     {
144                         return;
145                     }
146
147                     var key = self.pubkeyDecode(val);
148                     if (!key)
149                     {
150                         form.find('input').val('');
151                         form.find('.alert-message').show();
152                         return;
153                     }
154
155                     self._keys.push(val);
156                     self._render(ev.data.div);
157
158                     L.ui.dialog(false);
159                 }
160             });
161         },
162
163         _show: function(ev)
164         {
165             var self = ev.data.self;
166
167             L.ui.dialog(
168                 L.tr('Public key'),
169                 $('<pre />').text(self._keys[ev.data.index]),
170                 { style: 'close' }
171             );
172         },
173
174         _render: function(div)
175         {
176             div.empty();
177
178             for (var i = 0; i < this._keys.length; i++)
179             {
180                 var k = this.pubkeyDecode(this._keys[i] || '');
181
182                 if (!k)
183                     continue;
184
185                 $('<div />')
186                     .addClass('cbi-input-dynlist')
187                     .append($('<input />')
188                         .attr('type', 'text')
189                         .prop('readonly', true)
190                         .click({ self: this, index: i }, this._show)
191                         .val('%dBit %s - %s'.format(k.bits, k.type, k.comment || '?')))
192                     .append($('<img />')
193                         .attr('src', L.globals.resource + '/icons/cbi/remove.gif')
194                         .attr('title', L.tr('Remove public key'))
195                         .click({ self: this, div: div, index: i }, this._remove)
196                         .addClass('cbi-button'))
197                     .appendTo(div);
198             }
199
200             if (this._keys.length > 0)
201                 $('<br />').appendTo(div);
202
203             $('<input />')
204                 .attr('type', 'button')
205                 .val(L.tr('Add public key …'))
206                 .click({ self: this, div: div }, this._add)
207                 .addClass('cbi-button')
208                 .addClass('cbi-button-apply')
209                 .appendTo(div);
210         },
211
212         widget: function(sid)
213         {
214             this._keys = [ ];
215
216             for (var i = 0; i < this.options.keys.length; i++)
217                 this._keys.push(this.options.keys[i]);
218
219             var d = $('<div />')
220                 .attr('id', this.id(sid));
221
222             this._render(d);
223
224             return d;
225         },
226
227         changed: function(sid)
228         {
229             if (this.options.keys.length != this._keys.length)
230                 return true;
231
232             for (var i = 0; i < this.options.keys.length; i++)
233                 if (this.options.keys[i] != this._keys[i])
234                     return true;
235
236             return false;
237         },
238
239         save: function(sid)
240         {
241             if (this.changed(sid))
242             {
243                 this.options.keys = [ ];
244
245                 for (var i = 0; i < this._keys.length; i++)
246                     this.options.keys.push(this._keys[i]);
247
248                 return L.system.setSSHKeys(this._keys);
249             }
250
251             return undefined;
252         }
253     }),
254
255     execute: function() {
256         var self = this;
257         return L.system.getSSHKeys().then(function(keys) {
258             var m = new L.cbi.Map('dropbear', {
259                 caption:     L.tr('SSH Access'),
260                 description: L.tr('Dropbear offers SSH network shell access and an integrated SCP server')
261             });
262
263             var s1 = m.section(L.cbi.DummySection, '__password', {
264                 caption:     L.tr('Router Password'),
265                 description: L.tr('Changes the administrator password for accessing the device'),
266                 readonly:    !self.options.acls.admin
267             });
268
269             var p1 = s1.option(L.cbi.PasswordValue, 'pass1', {
270                 caption:     L.tr('Password'),
271                 optional:    true
272             });
273
274             var p2 = s1.option(L.cbi.PasswordValue, 'pass2', {
275                 caption:     L.tr('Confirmation'),
276                 optional:    true,
277                 datatype:    function(v) {
278                     var v1 = p1.formvalue('__password');
279                     if (v1 && v1.length && v != v1)
280                         return L.tr('Passwords must match!');
281                     return true;
282                 }
283             });
284
285             p1.save = function(sid) { };
286             p2.save = function(sid) {
287                 var v1 = p1.formvalue(sid);
288                 var v2 = p2.formvalue(sid);
289                 if (v2 && v2.length > 0 && v1 == v2)
290                     return L.system.setPassword('root', v2);
291             };
292
293
294             var s2 = m.section(L.cbi.DummySection, '__pubkeys', {
295                 caption:     L.tr('SSH-Keys'),
296                 description: L.tr('Specifies public keys for passwordless SSH authentication'),
297                 readonly:    !self.options.acls.admin
298             });
299
300             var k = s2.option(self.PubkeyListValue, 'keys', {
301                 caption:     L.tr('Saved keys'),
302                 keys:        keys
303             });
304
305
306             var s3 = m.section(L.cbi.TypedSection, 'dropbear', {
307                 caption:     L.tr('SSH Server'),
308                 description: L.tr('This sections define listening instances of the builtin Dropbear SSH server'),
309                 addremove:   true,
310                 add_caption: L.tr('Add instance ...'),
311                 readonly:    !self.options.acls.admin,
312                 collabsible: true
313             });
314
315             s3.option(L.cbi.NetworkList, 'Interface', {
316                 caption:     L.tr('Interface'),
317                 description: L.tr('Listen only on the given interface or, if unspecified, on all')
318             });
319
320             s3.option(L.cbi.InputValue, 'Port', {
321                 caption:     L.tr('Port'),
322                 description: L.tr('Specifies the listening port of this Dropbear instance'),
323                 datatype:    'port',
324                 placeholder: 22,
325                 optional:    true
326             });
327
328             s3.option(L.cbi.CheckboxValue, 'PasswordAuth', {
329                 caption:     L.tr('Password authentication'),
330                 description: L.tr('Allow SSH password authentication'),
331                 initial:     true,
332                 enabled:     'on',
333                 disabled:    'off'
334             });
335
336             s3.option(L.cbi.CheckboxValue, 'RootPasswordAuth', {
337                 caption:     L.tr('Allow root logins with password'),
338                 description: L.tr('Allow the root user to login with password'),
339                 initial:     true,
340                 enabled:     'on',
341                 disabled:    'off'
342             });
343
344             s3.option(L.cbi.CheckboxValue, 'GatewayPorts', {
345                 caption:     L.tr('Gateway ports'),
346                 description: L.tr('Allow remote hosts to connect to local SSH forwarded ports'),
347                 initial:     false,
348                 enabled:     'on',
349                 disabled:    'off'
350             });
351
352             return m.insertInto('#map');
353         });
354     }
355 });