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