849242abfffc15a5877666ad175f83a38242c3f6
[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 return network.registerProtocol('yggdrasil',
162 {
163 getI18n: function() {
164 return _('Yggdrasil Network');
165 },
166 getIfname: function() {
167 return this._ubus('l3_device') || this.sid;
168 },
169 getType: function() {
170 return "tunnel";
171 },
172 getOpkgPackage: function() {
173 return 'yggdrasil';
174 },
175 isFloating: function() {
176 return true;
177 },
178 isVirtual: function() {
179 return true;
180 },
181 getDevices: function() {
182 return null;
183 },
184 containsDevice: function(ifname) {
185 return(network.getIfnameOf(ifname)==this.getIfname());
186 },
187 renderFormOptions: function(s) {
188 var o, ss;
189 o=s.taboption('general',form.Value,'private_key',_('Private key'),_('The private key for your Yggdrasil node'));
190 o.optional=false;
191 o.password=true;
192 o.validate=validatePrivateKey;
193
194 o=s.taboption('general',form.Value,'public_key',_('Public key'),_('The public key for your Yggdrasil node'));
195 o.optional=true;
196 o.validate=validatePublicKey;
197
198 s.taboption('general',cbiKeyPairGenerate,'_gen_server_keypair',' ');
199
200 o=s.taboption('advanced',form.Value,'mtu',_('MTU'),_('A default MTU of 65535 is set by Yggdrasil. It is recomended to utilize the default.'));
201 o.optional=true;
202 o.placeholder=65535;
203 o.datatype='range(1280, 65535)';
204
205 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.'));
206 o.optional=true;
207 o.placeholder="{}";
208
209 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.'));
210 o.default=o.disabled;
211
212 try {
213 s.tab('peers',_('Peers'));
214 } catch(e) {};
215 o=s.taboption('peers', form.SectionValue, '_active', form.NamedSection, this.sid, "interface", _("Active peers"))
216 ss=o.subsection;
217 ss.option(cbiActivePeers, this.sid);
218
219 o=s.taboption('peers', form.SectionValue, '_listen', form.NamedSection, this.sid, "interface", _("Listen for peers"))
220 ss=o.subsection;
221
222 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>'));
223 o.placeholder="tls://0.0.0.0:0"
224 o.validate=validateYggdrasilListenUri;
225
226 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.'));
227 o.validate=validatePublicKey;
228
229 o=s.taboption('peers', form.SectionValue, '_peers', form.TableSection, 'yggdrasil_%s_peer'.format(this.sid), _("Peer addresses"))
230 ss=o.subsection;
231 ss.addremove=true;
232 ss.anonymous=true;
233 ss.addbtntitle=_("Add peer address");
234
235 o=ss.option(form.Value,"address",_("Peer URI"));
236 o.placeholder="tls://0.0.0.0:0"
237 o.validate=validateYggdrasilPeerUri;
238 ss.option(widgets.NetworkSelect,"interface",_("Peer interface"));
239
240 o=s.taboption('peers', form.SectionValue, '_interfaces', form.TableSection, 'yggdrasil_%s_interface'.format(this.sid), _("Multicast rules"))
241 ss=o.subsection;
242 ss.addbtntitle=_("Add multicast rule");
243 ss.addremove=true;
244 ss.anonymous=true;
245
246 o=ss.option(widgets.DeviceSelect,"interface",_("Devices"));
247 o.multiple=true;
248
249 ss.option(form.Flag,"beacon",_("Send multicast beacon"));
250
251 ss.option(form.Flag,"listen",_("Listen to multicast beacons"));
252
253 o=ss.option(form.Value,"port",_("Port"));
254 o.optional=true;
255 o.datatype='range(1, 65535)';
256
257 o=ss.option(form.Value,"password",_("Password"));
258 o.optional=true;
259
260 return;
261 },
262 deleteConfiguration: function() {
263 uci.sections('network', 'yggdrasil_%s_interface'.format(this.sid), function(s) {
264 uci.remove('network', s['.name']);
265 });
266 uci.sections('network', 'yggdrasil_%s_peer'.format(this.sid), function(s) {
267 uci.remove('network', s['.name']);
268 });
269 }
270 }
271 );