Merge pull request #1735 from sumpfralle/olsr-jsoninfo-parser-handle-empty-result
[oweals/luci.git] / modules / luci-mod-system / htdocs / luci-static / resources / view / system / sshkeys.js
1 'use strict';
2 'require rpc';
3
4 var SSHPubkeyDecoder = L.Class.singleton({
5         lengthDecode: function(s, off)
6         {
7                 var l = (s.charCodeAt(off++) << 24) |
8                                 (s.charCodeAt(off++) << 16) |
9                                 (s.charCodeAt(off++) <<  8) |
10                                  s.charCodeAt(off++);
11
12                 if (l < 0 || (off + l) > s.length)
13                         return -1;
14
15                 return l;
16         },
17
18         decode: function(s)
19         {
20                 var parts = s.split(/\s+/);
21                 if (parts.length < 2)
22                         return null;
23
24                 var key = null;
25                 try { key = atob(parts[1]); } catch(e) {}
26                 if (!key)
27                         return null;
28
29                 var off, len;
30
31                 off = 0;
32                 len = this.lengthDecode(key, off);
33
34                 if (len <= 0)
35                         return null;
36
37                 var type = key.substr(off + 4, len);
38                 if (type !== parts[0])
39                         return null;
40
41                 off += 4 + len;
42
43                 var len1 = off < key.length ? this.lengthDecode(key, off) : 0;
44                 if (len1 <= 0)
45                         return null;
46
47                 var curve = null;
48                 if (type.indexOf('ecdsa-sha2-') === 0) {
49                         curve = key.substr(off + 4, len1);
50
51                         if (!len1 || type.substr(11) !== curve)
52                                 return null;
53
54                         type = 'ecdsa-sha2';
55                         curve = curve.replace(/^nistp(\d+)$/, 'NIST P-$1');
56                 }
57
58                 off += 4 + len1;
59
60                 var len2 = off < key.length ? this.lengthDecode(key, off) : 0;
61                 if (len2 < 0)
62                         return null;
63
64                 if (len1 & 1)
65                         len1--;
66
67                 if (len2 & 1)
68                         len2--;
69
70                 var comment = parts.slice(2).join(' '),
71                     fprint = parts[1].length > 68 ? parts[1].substr(0, 33) + '…' + parts[1].substr(-34) : parts[1];
72
73                 switch (type)
74                 {
75                 case 'ssh-rsa':
76                         return { type: 'RSA', bits: len2 * 8, comment: comment, fprint: fprint };
77
78                 case 'ssh-dss':
79                         return { type: 'DSA', bits: len1 * 8, comment: comment, fprint: fprint };
80
81                 case 'ssh-ed25519':
82                         return { type: 'ECDH', curve: 'Curve25519', comment: comment, fprint: fprint };
83
84                 case 'ecdsa-sha2':
85                         return { type: 'ECDSA', curve: curve, comment: comment, fprint: fprint };
86
87                 default:
88                         return null;
89                 }
90         }
91 });
92
93 var callFileRead = rpc.declare({
94         object: 'file',
95         method: 'read',
96         params: [ 'path' ],
97         expect: { data: '' }
98 });
99
100 var callFileWrite = rpc.declare({
101         object: 'file',
102         method: 'write',
103         params: [ 'path', 'data' ]
104 });
105
106 function renderKeys(keys) {
107         var list = document.querySelector('.cbi-dynlist[name="sshkeys"]');
108
109         while (!matchesElem(list.firstElementChild, '.add-item'))
110                 list.removeChild(list.firstElementChild);
111
112         keys.forEach(function(key) {
113                 var pubkey = SSHPubkeyDecoder.decode(key);
114                 if (pubkey)
115                         list.insertBefore(E('div', {
116                                 class: 'item',
117                                 click: removeKey,
118                                 'data-key': key
119                         }, [
120                                 E('strong', pubkey.comment || _('Unnamed key')), E('br'),
121                                 E('small', [
122                                         '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
123                                         E('br'), E('code', pubkey.fprint)
124                                 ])
125                         ]), list.lastElementChild);
126         });
127
128         if (list.firstElementChild === list.lastElementChild)
129                 list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild);
130 }
131
132 function saveKeys(keys) {
133         return callFileWrite('/etc/dropbear/authorized_keys', keys.join('\n') + '\n')
134                 .then(renderKeys.bind(this, keys))
135                 .then(L.ui.hideModal);
136 }
137
138 function addKey(ev) {
139         var list = findParent(ev.target, '.cbi-dynlist'),
140             input = list.querySelector('input[type="text"]'),
141             key = input.value.trim(),
142             pubkey = SSHPubkeyDecoder.decode(key),
143             keys = [];
144
145         if (!key.length)
146                 return;
147
148         list.querySelectorAll('.item').forEach(function(item) {
149                 keys.push(item.getAttribute('data-key'));
150         });
151
152         if (keys.indexOf(key) !== -1) {
153                 L.ui.showModal(_('Add key'), [
154                         E('div', { class: 'alert-message warning' }, _('The given SSH public key has already been added.')),
155                         E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close')))
156                 ]);
157         }
158         else if (!pubkey) {
159                 L.ui.showModal(_('Add key'), [
160                         E('div', { class: 'alert-message warning' }, _('The given SSH public key is invalid. Please supply proper public RSA or ECDSA keys.')),
161                         E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close')))
162                 ]);
163         }
164         else {
165                 keys.push(key);
166                 input.value = '';
167
168                 return saveKeys(keys).then(function() {
169                         var added = list.querySelector('[data-key="%s"]'.format(key));
170                         if (added)
171                                 added.classList.add('flash');
172                 });
173         }
174 }
175
176 function removeKey(ev) {
177         var list = findParent(ev.target, '.cbi-dynlist'),
178             delkey = ev.target.getAttribute('data-key'),
179             keys = [];
180
181         list.querySelectorAll('.item').forEach(function(item) {
182                 var key = item.getAttribute('data-key');
183                 if (key !== delkey)
184                         keys.push(key);
185         });
186
187         L.showModal(_('Delete key'), [
188                 E('div', _('Do you really want to delete the following SSH key?')),
189                 E('pre', delkey),
190                 E('div', { class: 'right' }, [
191                         E('div', { class: 'btn', click: L.hideModal }, _('Cancel')),
192                         ' ',
193                         E('div', { class: 'btn danger', click: L.ui.createHandlerFn(this, saveKeys, keys) }, _('Delete key')),
194                 ])
195         ]);
196 }
197
198 function dragKey(ev) {
199         ev.stopPropagation();
200         ev.preventDefault();
201         ev.dataTransfer.dropEffect = 'copy';
202 }
203
204 function dropKey(ev) {
205         var file = ev.dataTransfer.files[0],
206             input = ev.currentTarget.querySelector('input[type="text"]'),
207             reader = new FileReader();
208
209         if (file) {
210                 reader.onload = function(rev) {
211                         input.value = rev.target.result.trim();
212                         addKey(ev);
213                         input.value = '';
214                 };
215
216                 reader.readAsText(file);
217         }
218
219         ev.stopPropagation();
220         ev.preventDefault();
221 }
222
223 function handleWindowDragDropIgnore(ev) {
224         ev.preventDefault()
225 }
226
227 return L.view.extend({
228         load: function() {
229                 return callFileRead('/etc/dropbear/authorized_keys').then(function(data) {
230                         return (data || '').split(/\n/).map(function(line) {
231                                 return line.trim();
232                         }).filter(function(line) {
233                                 return line.match(/^ssh-/) != null;
234                         });
235                 });
236         },
237
238         render: function(keys) {
239                 var list = E('div', { 'class': 'cbi-dynlist', 'dragover': dragKey, 'drop': dropKey }, [
240                         E('div', { 'class': 'add-item' }, [
241                                 E('input', {
242                                         'class': 'cbi-input-text',
243                                         'type': 'text',
244                                         'placeholder': _('Paste or drag SSH key file…') ,
245                                         'keydown': function(ev) { if (ev.keyCode === 13) addKey(ev) }
246                                 }),
247                                 E('button', {
248                                         'class': 'cbi-button',
249                                         'click': L.ui.createHandlerFn(this, addKey)
250                                 }, _('Add key'))
251                         ])
252                 ]);
253
254                 keys.forEach(L.bind(function(key) {
255                         var pubkey = SSHPubkeyDecoder.decode(key);
256                         if (pubkey)
257                                 list.insertBefore(E('div', {
258                                         class: 'item',
259                                         click: L.ui.createHandlerFn(this, removeKey),
260                                         'data-key': key
261                                 }, [
262                                         E('strong', pubkey.comment || _('Unnamed key')), E('br'),
263                                         E('small', [
264                                                 '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)),
265                                                 E('br'), E('code', pubkey.fprint)
266                                         ])
267                                 ]), list.lastElementChild);
268                 }, this));
269
270                 if (list.firstElementChild === list.lastElementChild)
271                         list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild);
272
273                 window.addEventListener('dragover', handleWindowDragDropIgnore);
274                 window.addEventListener('drop', handleWindowDragDropIgnore);
275
276                 return E('div', {}, [
277                         E('h2', _('SSH-Keys')),
278                         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.')),
279                         E('div', { 'class': 'cbi-section-node' }, list)
280                 ]);
281         },
282
283         handleSaveApply: null,
284         handleSave: null,
285         handleReset: null
286 });