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