luci-proto-yggdrasil: fix empty translation string
[project/luci.git] / protocols / luci-proto-yggdrasil / htdocs / luci-static / resources / protocol / yggdrasil.js
1 'use strict';
2 'require form';
3 'require network';
4 'require rpc';
5 'require tools.widgets as widgets';
6 'require uci';
7 'require ui';
8 network.registerPatternVirtual(/^yggdrasil-.+$/);
9
10 function validatePrivateKey(section_id,value) {
11 if (value.length == 0) {
12 return true;
13 };
14 if (!value.match(/^([0-9a-fA-F]){128}$/)) {
15 if (value != "auto") {
16 return _('Invalid private key string %s').format(value);
17 }
18 return true;
19 }
20 return true;
21 };
22
23 function validatePublicKey(section_id,value) {
24 if (value.length == 0) {
25 return true;
26 };
27 if (!value.match(/^([0-9a-fA-F]){64}$/))
28 return _('Invalid public key string %s').format(value);
29 return true;
30 };
31
32 function validateYggdrasilListenUri(section_id,value) {
33 if (value.length == 0) {
34 return true;
35 };
36 if (!value.match(/^(tls|tcp|unix|quic):\/\//))
37 return _('Unsupported URI scheme in %s').format(value);
38 return true;
39 };
40
41 function validateYggdrasilPeerUri(section_id,value) {
42 if (!value.match(/^(tls|tcp|unix|quic|socks|sockstls):\/\//))
43 return _('URI scheme %s not supported').format(value);
44 return true;
45 };
46
47 var cbiKeyPairGenerate = form.DummyValue.extend({
48 cfgvalue: function(section_id, value) {
49 return E('button', {
50 'class':'btn',
51 'click':ui.createHandlerFn(this, function(section_id,ev) {
52 var prv = this.section.getUIElement(section_id,'private_key'),
53 pub = this.section.getUIElement(section_id,'public_key'),
54 map = this.map;
55
56 return generateKey().then(function(keypair){
57 prv.setValue(keypair.priv);
58 pub.setValue(keypair.pub);
59 map.save(null,true);
60 });
61 },section_id)
62 },[_('Generate new key pair')]);
63 }
64 });
65
66 function updateActivePeers(ifname) {
67 getPeers(ifname).then(function(peers){
68 var table = document.querySelector('#yggdrasil-active-peerings-' + ifname);
69 if (table) {
70 while (table.rows.length > 1) { table.deleteRow(1); }
71 peers.forEach(function(peer) {
72 var row = table.insertRow(-1);
73 row.style.fontSize = "xx-small";
74 if (!peer.up) {
75 row.style.opacity = "66%";
76 }
77 var cell = row.insertCell(-1)
78 cell.className = "td"
79 cell.textContent = peer.remote;
80
81 cell = row.insertCell(-1)
82 cell.className = "td"
83 cell.textContent = peer.up ? "Up" : "Down";
84
85 cell = row.insertCell(-1)
86 cell.className = "td"
87 cell.textContent = peer.inbound ? "In" : "Out";
88
89 cell = row.insertCell(-1)
90 cell.className = "td"
91 cell.innerHTML = "<u style='cursor: default'>" + peer.address + "</u>"
92 cell.dataToggle = "tooltip";
93 cell.title = "Key: " + peer.key;
94
95 cell = row.insertCell(-1)
96 cell.className = "td"
97 cell.textContent = '%t'.format(peer.uptime);
98
99 cell = row.insertCell(-1)
100 cell.className = "td"
101 cell.textContent = '%.2mB'.format(peer.bytes_recvd);
102
103 cell = row.insertCell(-1)
104 cell.className = "td"
105 cell.textContent = '%.2mB'.format(peer.bytes_sent);
106
107 cell = row.insertCell(-1)
108 cell.className = "td"
109 cell.textContent = peer.priority;
110
111 cell = row.insertCell(-1)
112 cell.className = "td"
113 if (!peer.up) {
114 cell.innerHTML = "<u style='cursor: default'>%t ago</u>".format(peer.last_error_time)
115 cell.dataToggle = "tooltip"
116 cell.title = peer.last_error
117 } else {
118 cell.innerHTML = "-"
119 }
120 });
121 setTimeout(updateActivePeers.bind(this, ifname), 5000);
122 }
123 });
124 }
125
126 var cbiActivePeers = form.DummyValue.extend({
127 cfgvalue: function(section_id, value) {
128 updateActivePeers(this.option);
129 return E('table', {
130 'class': 'table',
131 'id': 'yggdrasil-active-peerings-' + this.option,
132 },[
133 E('tr', {'class': 'tr'}, [
134 E('th', {'class': 'th'}, _('URI')),
135 E('th', {'class': 'th'}, _('State')),
136 E('th', {'class': 'th'}, _('Dir')),
137 E('th', {'class': 'th'}, _('IP Address')),
138 E('th', {'class': 'th'}, _('Uptime')),
139 E('th', {'class': 'th'}, _('RX')),
140 E('th', {'class': 'th'}, _('TX')),
141 E('th', {'class': 'th'}, _('Priority')),
142 E('th', {'class': 'th'}, _('Last Error')),
143 ])
144 ]);
145 }
146 });
147
148 var generateKey = rpc.declare({
149 object:'luci.yggdrasil',
150 method:'generateKeyPair',
151 expect:{keys:{}}
152 });
153
154 var getPeers = rpc.declare({
155 object:'luci.yggdrasil',
156 method:'getPeers',
157 params:['interface'],
158 expect:{peers:[]}
159 });
160
161 var callIsJumperInstalled = rpc.declare({
162 object:'luci.yggdrasil-jumper',
163 method:'isInstalled',
164 expect:{isInstalled: false}
165 });
166
167 var callValidateJumperConfig = rpc.declare({
168 object:'luci.yggdrasil-jumper',
169 method:'validateConfig',
170 params:['config'],
171 expect:{output: "Unknown error."}
172 });
173
174 function validateJumperConfig(section) {
175 var last_input = "", last_output = "";
176
177 return function(section_id, input) {
178 if (last_input != input) {
179 last_input = input
180
181 callValidateJumperConfig(input).then(function(output) {
182 last_output = output;
183
184 var option = section.getUIElement(section_id).jumper_config;
185 option.triggerValidation(section_id);
186 });
187 }
188
189 if (last_output.length == 0) {
190 return true;
191 }
192
193 return _(last_output);
194 };
195 };
196
197 return network.registerProtocol('yggdrasil',
198 {
199 getI18n: function() {
200 return _('Yggdrasil Network');
201 },
202 getIfname: function() {
203 return this._ubus('l3_device') || this.sid;
204 },
205 getType: function() {
206 return "tunnel";
207 },
208 getOpkgPackage: function() {
209 return 'yggdrasil';
210 },
211 isFloating: function() {
212 return true;
213 },
214 isVirtual: function() {
215 return true;
216 },
217 getDevices: function() {
218 return null;
219 },
220 containsDevice: function(ifname) {
221 return(network.getIfnameOf(ifname)==this.getIfname());
222 },
223 renderFormOptions: function(s) {
224 var o, ss;
225 o=s.taboption('general',form.Value,'private_key',_('Private key'),_('The private key for your Yggdrasil node'));
226 o.optional=false;
227 o.password=true;
228 o.validate=validatePrivateKey;
229
230 o=s.taboption('general',form.Value,'public_key',_('Public key'),_('The public key for your Yggdrasil node'));
231 o.optional=true;
232 o.validate=validatePublicKey;
233
234 s.taboption('general',cbiKeyPairGenerate,'_gen_server_keypair',' ');
235
236 o=s.taboption('advanced',form.Value,'mtu',_('MTU'),_('A default MTU of 65535 is set by Yggdrasil. It is recomended to utilize the default.'));
237 o.optional=true;
238 o.placeholder=65535;
239 o.datatype='range(1280, 65535)';
240
241 o=s.taboption('general',form.TextValue,'node_info',_('Node info'),_('Optional node info. This must be a { "key": "value", ... } map or set as null. This is entirely optional but, if set, is visible to the whole network on request.'));
242 o.optional=true;
243 o.placeholder="{}";
244
245 o=s.taboption('general',form.Flag,'node_info_privacy',_('Node info privacy'),_('Enable node info privacy so that only items specified in "Node info" are sent back. Otherwise defaults including the platform, architecture and Yggdrasil version are included.'));
246 o.default=o.disabled;
247
248 try {
249 s.tab('peers',_('Peers'));
250 } catch(e) {};
251 o=s.taboption('peers', form.SectionValue, '_active', form.NamedSection, this.sid, "interface", _("Active peers"))
252 ss=o.subsection;
253 ss.option(cbiActivePeers, this.sid);
254
255 o=s.taboption('peers', form.SectionValue, '_listen', form.NamedSection, this.sid, "interface", _("Listen for peers"))
256 ss=o.subsection;
257
258 o=ss.option(form.DynamicList,'listen_address',_('Listen addresses'), _('Add listeners in order to accept incoming peerings from non-local nodes. Multicast peer discovery works regardless of listeners set here. URI Format: <code>tls://0.0.0.0:0</code> or <code>tls://[::]:0</code> to listen on all interfaces. Choose an acceptable URI <code>tls://</code>, <code>tcp://</code>, <code>unix://</code> or <code>quic://</code>'));
259 o.placeholder="tls://0.0.0.0:0"
260 o.validate=validateYggdrasilListenUri;
261
262 o=s.taboption('peers',form.DynamicList,'allowed_public_key',_('Accept from public keys'),_('If empty, all incoming connections will be allowed (default). This does not affect outgoing peerings, nor link-local peers discovered via multicast.'));
263 o.validate=validatePublicKey;
264
265 o=s.taboption('peers', form.SectionValue, '_peers', form.TableSection, 'yggdrasil_%s_peer'.format(this.sid), _("Peer addresses"))
266 ss=o.subsection;
267 ss.addremove=true;
268 ss.anonymous=true;
269 ss.addbtntitle=_("Add peer address");
270
271 o=ss.option(form.Value,"address",_("Peer URI"));
272 o.placeholder="tls://0.0.0.0:0"
273 o.validate=validateYggdrasilPeerUri;
274 ss.option(widgets.NetworkSelect,"interface",_("Peer interface"));
275
276 o=s.taboption('peers', form.SectionValue, '_interfaces', form.TableSection, 'yggdrasil_%s_interface'.format(this.sid), _("Multicast rules"))
277 ss=o.subsection;
278 ss.addbtntitle=_("Add multicast rule");
279 ss.addremove=true;
280 ss.anonymous=true;
281
282 o=ss.option(widgets.DeviceSelect,"interface",_("Devices"));
283 o.multiple=true;
284
285 ss.option(form.Flag,"beacon",_("Send multicast beacon"));
286
287 ss.option(form.Flag,"listen",_("Listen to multicast beacons"));
288
289 o=ss.option(form.Value,"port",_("Port"));
290 o.optional=true;
291 o.datatype='range(1, 65535)';
292
293 o=ss.option(form.Value,"password",_("Password"));
294 o.optional=true;
295
296 // Jumper tab
297 try {
298 s.tab('jumper',_('Jumper'));
299 } catch(e) {};
300
301 o=s.taboption(
302 'jumper',
303 form.HiddenValue,
304 'hidden_value',
305 ' ',
306 _('%s is an independent project that aims to transparently reduce latency of a connection over Yggdrasil network, utilizing NAT traversal to bypass intermediary nodes.'.format('<a href="https://github.com/one-d-wide/yggdrasil-jumper">Yggdrasil Jumper</a>'))
307 + ' ' + _('It periodically probes for active sessions and automatically establishes direct peerings over internet with remote nodes running Yggdrasil Jumper without requiring firewall or port configuration.')
308 );
309
310 o=s.taboption(
311 'jumper',
312 form.Flag,
313 'jumper_enable',
314 _('Enable Yggdrasil Jumper'),
315 _('The checkbox cannot be modified unless the <code>yggdrasil-jumper</code> package is installed.')
316 );
317 o.default=false;
318 o.rmempty=false;
319 o.readonly=true;
320
321 // Unlock enable option if jumper is installed
322 callIsJumperInstalled().then(function(isInstalled) {
323 if (isInstalled) {
324 var o = s.children.find(function(o) { return o.option == "jumper_enable"; });
325 o.readonly = false;
326 // Explicit rerendering request isn't needed because the protocol tab
327 // is constructed only after all async functions is done
328 }
329 });
330
331 o=s.taboption(
332 'jumper',
333 form.ListValue,
334 'jumper_loglevel',
335 _('Log level')
336 );
337 o.value('off', _('Off'));
338 o.value('error', _('Error'));
339 o.value('warn', _('Warn'));
340 o.value('info', _('Info'));
341 o.value('debug', _('Debug'));
342 o.value('trace', _('Trace'));
343 o.default='info';
344 o.rmempty=false;
345
346 o=s.taboption(
347 'jumper',
348 form.Flag,
349 'allocate_listen_addresses',
350 _('Allocate listen addresses'),
351 _('Allow Yggdrasil Jumper to automatically configure Yggdrasil with proper listen address and random port.')
352 );
353 o.default=true;
354 o.rmempty=false;
355
356 o=s.taboption(
357 'jumper',
358 form.Flag,
359 'jumper_autofill_listen_addresses',
360 _('Autofill listen addresses'),
361 _('Retrieve the listener addresses from the Yggdrasil interface configuration.')
362 );
363 o.default=true;
364 o.rmempty=false;
365
366 o=s.taboption(
367 'jumper',
368 form.TextValue,
369 'jumper_config',
370 _('Extra config'),
371 _('Additional configuration settings (in TOML format).')
372 );
373 o.optional=true;
374 o.validate=validateJumperConfig(s);
375
376 return;
377 },
378 deleteConfiguration: function() {
379 uci.sections('network', 'yggdrasil_%s_interface'.format(this.sid), function(s) {
380 uci.remove('network', s['.name']);
381 });
382 uci.sections('network', 'yggdrasil_%s_peer'.format(this.sid), function(s) {
383 uci.remove('network', s['.name']);
384 });
385 }
386 }
387 );