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