luci-app-lldpd: rebase i18n
[project/luci.git] / protocols / luci-proto-wireguard / htdocs / luci-static / resources / protocol / wireguard.js
1 'use strict';
2 'require ui';
3 'require uci';
4 'require rpc';
5 'require form';
6 'require network';
7
8 var generateKey = rpc.declare({
9 object: 'luci.wireguard',
10 method: 'generateKeyPair',
11 expect: { keys: {} }
12 });
13
14 var getPublicAndPrivateKeyFromPrivate = rpc.declare({
15 object: 'luci.wireguard',
16 method: 'getPublicAndPrivateKeyFromPrivate',
17 params: ['privkey'],
18 expect: { keys: {} }
19 });
20
21 var generateQrCode = rpc.declare({
22 object: 'luci.wireguard',
23 method: 'generateQrCode',
24 params: ['privkey', 'psk', 'allowed_ips'],
25 expect: { qr_code: '' }
26 });
27
28 var generatePsk = rpc.declare({
29 object: 'luci.wireguard',
30 method: 'generatePsk',
31 expect: { psk: '' }
32 });
33
34 function validateBase64(section_id, value) {
35 if (value.length == 0)
36 return true;
37
38 if (value.length != 44 || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/))
39 return _('Invalid Base64 key string');
40
41 if (value[43] != "=" )
42 return _('Invalid Base64 key string');
43
44 return true;
45 }
46
47 function findSection(sections, name) {
48 for (var i = 0; i < sections.length; i++) {
49 var section = sections[i];
50 if (section['.name'] == name) return section;
51 }
52
53 return null;
54 }
55
56 function generateDescription(name, texts) {
57 return E('li', { 'style': 'color: inherit;' }, [
58 E('span', name),
59 E('ul', texts.map(function (text) {
60 return E('li', { 'style': 'color: inherit;' }, text);
61 }))
62 ]);
63 }
64
65 return network.registerProtocol('wireguard', {
66 getI18n: function() {
67 return _('WireGuard VPN');
68 },
69
70 getIfname: function() {
71 return this._ubus('l3_device') || this.sid;
72 },
73
74 getOpkgPackage: function() {
75 return 'wireguard-tools';
76 },
77
78 isFloating: function() {
79 return true;
80 },
81
82 isVirtual: function() {
83 return true;
84 },
85
86 getDevices: function() {
87 return null;
88 },
89
90 containsDevice: function(ifname) {
91 return (network.getIfnameOf(ifname) == this.getIfname());
92 },
93
94 renderFormOptions: function(s) {
95 var o, ss;
96
97 // -- general ---------------------------------------------------------------------
98
99 o = s.taboption('general', form.Value, 'private_key', _('Private Key'), _('Required. Base64-encoded private key for this interface.'));
100 o.password = true;
101 o.validate = validateBase64;
102 o.rmempty = false;
103
104 var sections = uci.sections('network');
105 var serverName = this.getIfname();
106 var server = findSection(sections, serverName);
107
108 o = s.taboption('general', form.Value, 'public_key', _('Public Key'), _('Base64-encoded public key of this interface for sharing.'));
109 o.rmempty = false;
110 o.write = function() {/* write nothing */};
111
112 o.load = function(s) {
113 return getPublicAndPrivateKeyFromPrivate(server.private_key).then(
114 function(keypair) {
115 return keypair.pub || '';
116 },
117 function(error){
118 return _('Error getting PublicKey');
119 }, this)
120 };
121
122 o = s.taboption('general', form.Button, 'generate_key', _('Generate Key'));
123 o.inputstyle = 'apply';
124 o.onclick = ui.createHandlerFn(this, function(section_id, ev) {
125 return generateKey().then(function(keypair) {
126 var keyInput = document.getElementById('widget.cbid.network.%s.private_key'.format(section_id)),
127 changeEvent = new Event('change'),
128 pubKeyInput = document.getElementById('widget.cbid.network.%s.public_key'.format(section_id));
129
130 keyInput.value = keypair.priv || '';
131 pubKeyInput.value = keypair.pub || '';
132 keyInput.dispatchEvent(changeEvent);
133 });
134 }, s.section);
135
136 o = s.taboption('general', form.Value, 'listen_port', _('Listen Port'), _('Optional. UDP port used for outgoing and incoming packets.'));
137 o.datatype = 'port';
138 o.placeholder = _('random');
139 o.optional = true;
140
141 o = s.taboption('general', form.DynamicList, 'addresses', _('IP Addresses'), _('Recommended. IP addresses of the WireGuard interface.'));
142 o.datatype = 'ipaddr';
143 o.optional = true;
144
145 o = s.taboption('general', form.Flag, 'nohostroute', _('No Host Routes'), _('Optional. Do not create host routes to peers.'));
146 o.optional = true;
147
148 // -- advanced --------------------------------------------------------------------
149
150 o = s.taboption('advanced', form.Value, 'mtu', _('MTU'), _('Optional. Maximum Transmission Unit of tunnel interface.'));
151 o.datatype = 'range(0,8940)';
152 o.placeholder = '1420';
153 o.optional = true;
154
155 o = s.taboption('advanced', form.Value, 'fwmark', _('Firewall Mark'), _('Optional. 32-bit mark for outgoing encrypted packets. Enter value in hex, starting with <code>0x</code>.'));
156 o.optional = true;
157 o.validate = function(section_id, value) {
158 if (value.length > 0 && !value.match(/^0x[a-fA-F0-9]{1,8}$/))
159 return _('Invalid hexadecimal value');
160
161 return true;
162 };
163
164
165 // -- peers -----------------------------------------------------------------------
166
167 try {
168 s.tab('peers', _('Peers'), _('Further information about WireGuard interfaces and peers at <a href=\'http://wireguard.com\'>wireguard.com</a>.'));
169 }
170 catch(e) {}
171
172 o = s.taboption('peers', form.SectionValue, '_peers', form.GridSection, 'wireguard_%s'.format(s.section));
173 o.depends('proto', 'wireguard');
174
175 ss = o.subsection;
176 ss.anonymous = true;
177 ss.addremove = true;
178 ss.addbtntitle = _('Add peer');
179 ss.nodescriptions = true;
180 ss.modaltitle = _('Edit peer');
181
182 ss.renderSectionPlaceholder = function() {
183 return E([], [
184 E('br'),
185 E('em', _('No peers defined yet'))
186 ]);
187 };
188
189 o = ss.option(form.Flag, 'disabled', _('Peer disabled'), _('Enable / Disable peer. Restart wireguard interface to apply changes.'));
190 o.optional = true;
191 o.editable = true;
192
193 o = ss.option(form.Value, 'description', _('Description'), _('Optional. Description of peer.'));
194 o.placeholder = 'My Peer';
195 o.datatype = 'string';
196 o.optional = true;
197
198 o = ss.option(form.Value, 'description', _('QR-Code'));
199 o.modalonly = true;
200 o.render = L.bind(function (view, section_id) {
201 var sections = uci.sections('network');
202 var client = findSection(sections, section_id);
203 var serverName = this.getIfname();
204 var server = findSection(sections, serverName);
205
206 var interfaceTexts = [
207 'PrivateKey: ' + _('A random, on the fly generated "PrivateKey", the key will not be saved on the router')
208 ];
209
210 var peerTexts = [
211 'PublicKey: ' + _('The "PublicKey" of that wg interface'),
212 'AllowedIPs: ' + _('The list of this client\'s "AllowedIPs" or "0.0.0.0/0, ::/0" if not configured'),
213 'PresharedKey: ' + _('If available, the client\'s "PresharedKey"')
214 ];
215
216 var description = [
217 E('span', [
218 _('If there are any unsaved changes for this client, please save the configuration before generating a QR-Code'),
219 E('br'),
220 _('The QR-Code works per wg interface, it will be refreshed with every button click and transfers the following information:')
221 ]),
222 E('ul', [
223 generateDescription('[Interface]', interfaceTexts),
224 generateDescription('[Peer]', peerTexts)
225 ])
226 ];
227
228 return E('div', { 'class': 'cbi-value' }, [
229 E('label', { 'class': 'cbi-value-title' }, _('QR-Code')),
230 E('div', {
231 'class': 'cbi-value-field',
232 'style': 'display: flex; flex-direction: column; align-items: baseline;',
233 'id': 'qr-' + section_id
234 }, [
235 E('button', {
236 'class': 'btn cbi-button cbi-button-apply',
237 'click': ui.createHandlerFn(this, function (server, client, section_id) {
238 var qrDiv = document.getElementById('qr-' + section_id);
239 var qrEl = qrDiv.querySelector('value');
240 var qrBtn = qrDiv.querySelector('button');
241 var qrencodeErr = '<b>%q</b>'.format(
242 _('For QR-Code support please install the qrencode package!'));
243
244 if (qrEl.innerHTML != '' && qrEl.innerHTML != qrencodeErr) {
245 qrEl.innerHTML = '';
246 qrBtn.innerHTML = _('Generate New QR-Code')
247 } else {
248 qrEl.innerHTML = _('Loading QR-Code...');
249
250 generateQrCode(server.private_key, client.preshared_key,
251 client.allowed_ips).then(function (qrCode) {
252 if (qrCode == '') {
253 qrEl.innerHTML = qrencodeErr;
254 } else {
255 qrEl.innerHTML = qrCode;
256 qrBtn.innerHTML = _('Hide QR-Code');
257 }
258 });
259 }
260 }, server, client, section_id)
261 }, _('Generate new QR-Code')),
262 E('value', {
263 'class': 'cbi-section',
264 'style': 'margin: 0;'
265 }),
266 E('div', { 'class': 'cbi-value-description' }, description)
267 ])
268 ]);
269 }, this);
270
271 o = ss.option(form.Value, 'public_key', _('Public Key'), _('Required. Base64-encoded public key of peer.'));
272 o.modalonly = true;
273 o.validate = validateBase64;
274 o.rmempty = false;
275
276 o = ss.option(form.Value, 'preshared_key', _('Preshared Key'), _('Optional. Base64-encoded preshared key. Adds in an additional layer of symmetric-key cryptography for post-quantum resistance.'));
277 o.modalonly = true;
278 o.password = true;
279 o.validate = validateBase64;
280 o.optional = true;
281
282 o = ss.option(form.Button, 'generate_key', _('Generate Key'));
283 o.inputstyle = 'apply';
284 o.onclick = ui.createHandlerFn(this, function (section_id, ev, peer_id) {
285 return generatePsk().then(function (psk) {
286 var keyInput = document.getElementById('widget.cbid.network.%s.preshared_key'.format(peer_id)),
287 changeEvent = new Event('change');
288
289 keyInput.value = psk;
290 keyInput.dispatchEvent(changeEvent);
291 });
292 }, s.section);
293
294 o = ss.option(form.DynamicList, 'allowed_ips', _('Allowed IPs'), _("Optional. IP addresses and prefixes that this peer is allowed to use inside the tunnel. Usually the peer's tunnel IP addresses and the networks the peer routes through the tunnel."));
295 o.datatype = 'ipaddr';
296 o.optional = true;
297
298 o = ss.option(form.Flag, 'route_allowed_ips', _('Route Allowed IPs'), _('Optional. Create routes for Allowed IPs for this peer.'));
299 o.modalonly = true;
300
301 o = ss.option(form.Value, 'endpoint_host', _('Endpoint Host'), _('Optional. Host of peer. Names are resolved prior to bringing up the interface.'));
302 o.placeholder = 'vpn.example.com';
303 o.datatype = 'host';
304
305 o = ss.option(form.Value, 'endpoint_port', _('Endpoint Port'), _('Optional. Port of peer.'));
306 o.placeholder = '51820';
307 o.datatype = 'port';
308
309 o = ss.option(form.Value, 'persistent_keepalive', _('Persistent Keep Alive'), _('Optional. Seconds between keep alive messages. Default is 0 (disabled). Recommended value if this device is behind a NAT is 25.'));
310 o.modalonly = true;
311 o.datatype = 'range(0,65535)';
312 o.placeholder = '0';
313 },
314
315 deleteConfiguration: function() {
316 uci.sections('network', 'wireguard_%s'.format(this.sid), function(s) {
317 uci.remove('network', s['.name']);
318 });
319 }
320 });