Merge pull request #4770 from nickberry17/update_DummyValue
[project/luci.git] / modules / luci-mod-network / htdocs / luci-static / resources / view / network / dhcp.js
1 'use strict';
2 'require view';
3 'require dom';
4 'require poll';
5 'require rpc';
6 'require uci';
7 'require form';
8 'require validation';
9
10 var callHostHints, callDUIDHints, callDHCPLeases, CBILeaseStatus, CBILease6Status;
11
12 callHostHints = rpc.declare({
13 object: 'luci-rpc',
14 method: 'getHostHints',
15 expect: { '': {} }
16 });
17
18 callDUIDHints = rpc.declare({
19 object: 'luci-rpc',
20 method: 'getDUIDHints',
21 expect: { '': {} }
22 });
23
24 callDHCPLeases = rpc.declare({
25 object: 'luci-rpc',
26 method: 'getDHCPLeases',
27 expect: { '': {} }
28 });
29
30 CBILeaseStatus = form.DummyValue.extend({
31 renderWidget: function(section_id, option_id, cfgvalue) {
32 return E([
33 E('h4', _('Active DHCP Leases')),
34 E('table', { 'id': 'lease_status_table', 'class': 'table' }, [
35 E('tr', { 'class': 'tr table-titles' }, [
36 E('th', { 'class': 'th' }, _('Hostname')),
37 E('th', { 'class': 'th' }, _('IPv4-Address')),
38 E('th', { 'class': 'th' }, _('MAC-Address')),
39 E('th', { 'class': 'th' }, _('Lease time remaining'))
40 ]),
41 E('tr', { 'class': 'tr placeholder' }, [
42 E('td', { 'class': 'td' }, E('em', _('Collecting data...')))
43 ])
44 ])
45 ]);
46 }
47 });
48
49 CBILease6Status = form.DummyValue.extend({
50 renderWidget: function(section_id, option_id, cfgvalue) {
51 return E([
52 E('h4', _('Active DHCPv6 Leases')),
53 E('table', { 'id': 'lease6_status_table', 'class': 'table' }, [
54 E('tr', { 'class': 'tr table-titles' }, [
55 E('th', { 'class': 'th' }, _('Host')),
56 E('th', { 'class': 'th' }, _('IPv6-Address')),
57 E('th', { 'class': 'th' }, _('DUID')),
58 E('th', { 'class': 'th' }, _('Lease time remaining'))
59 ]),
60 E('tr', { 'class': 'tr placeholder' }, [
61 E('td', { 'class': 'td' }, E('em', _('Collecting data...')))
62 ])
63 ])
64 ]);
65 }
66 });
67
68 function validateHostname(sid, s) {
69 if (s == null || s == '')
70 return true;
71
72 if (s.length > 256)
73 return _('Expecting: %s').format(_('valid hostname'));
74
75 var labels = s.replace(/^\.+|\.$/g, '').split(/\./);
76
77 for (var i = 0; i < labels.length; i++)
78 if (!labels[i].match(/^[a-z0-9_](?:[a-z0-9-]{0,61}[a-z0-9])?$/i))
79 return _('Expecting: %s').format(_('valid hostname'));
80
81 return true;
82 }
83
84 function validateAddressList(sid, s) {
85 if (s == null || s == '')
86 return true;
87
88 var m = s.match(/^\/(.+)\/$/),
89 names = m ? m[1].split(/\//) : [ s ];
90
91 for (var i = 0; i < names.length; i++) {
92 var res = validateHostname(sid, names[i]);
93
94 if (res !== true)
95 return res;
96 }
97
98 return true;
99 }
100
101 function validateServerSpec(sid, s) {
102 if (s == null || s == '')
103 return true;
104
105 var m = s.match(/^(?:\/(.+)\/)?(.*)$/);
106 if (!m)
107 return _('Expecting: %s').format(_('valid hostname'));
108
109 var res = validateAddressList(sid, m[1]);
110 if (res !== true)
111 return res;
112
113 if (m[2] == '' || m[2] == '#')
114 return true;
115
116 // ipaddr%scopeid#srvport@source@interface#srcport
117
118 m = m[2].match(/^([0-9a-f:.]+)(?:%[^#@]+)?(?:#(\d+))?(?:@([0-9a-f:.]+)(?:@[^#]+)?(?:#(\d+))?)?$/);
119
120 if (!m)
121 return _('Expecting: %s').format(_('valid IP address'));
122
123 if (validation.parseIPv4(m[1])) {
124 if (m[3] != null && !validation.parseIPv4(m[3]))
125 return _('Expecting: %s').format(_('valid IPv4 address'));
126 }
127 else if (validation.parseIPv6(m[1])) {
128 if (m[3] != null && !validation.parseIPv6(m[3]))
129 return _('Expecting: %s').format(_('valid IPv6 address'));
130 }
131 else {
132 return _('Expecting: %s').format(_('valid IP address'));
133 }
134
135 if ((m[2] != null && +m[2] > 65535) || (m[4] != null && +m[4] > 65535))
136 return _('Expecting: %s').format(_('valid port value'));
137
138 return true;
139 }
140
141 return view.extend({
142 load: function() {
143 return Promise.all([
144 callHostHints(),
145 callDUIDHints()
146 ]);
147 },
148
149 render: function(hosts_duids) {
150 var has_dhcpv6 = L.hasSystemFeature('dnsmasq', 'dhcpv6') || L.hasSystemFeature('odhcpd'),
151 hosts = hosts_duids[0],
152 duids = hosts_duids[1],
153 m, s, o, ss, so;
154
155 m = new form.Map('dhcp', _('DHCP and DNS'), _('Dnsmasq is a combined <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-Server and <abbr title="Domain Name System">DNS</abbr>-Forwarder for <abbr title="Network Address Translation">NAT</abbr> firewalls'));
156
157 s = m.section(form.TypedSection, 'dnsmasq', _('Server Settings'));
158 s.anonymous = true;
159 s.addremove = false;
160
161 s.tab('general', _('General Settings'));
162 s.tab('files', _('Resolv and Hosts Files'));
163 s.tab('tftp', _('TFTP Settings'));
164 s.tab('advanced', _('Advanced Settings'));
165 s.tab('leases', _('Static Leases'));
166
167 s.taboption('general', form.Flag, 'domainneeded',
168 _('Domain required'),
169 _('Don\'t forward <abbr title="Domain Name System">DNS</abbr>-Requests without <abbr title="Domain Name System">DNS</abbr>-Name'));
170
171 s.taboption('general', form.Flag, 'authoritative',
172 _('Authoritative'),
173 _('This is the only <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> in the local network'));
174
175
176 s.taboption('files', form.Flag, 'readethers',
177 _('Use <code>/etc/ethers</code>'),
178 _('Read <code>/etc/ethers</code> to configure the <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-Server'));
179
180 s.taboption('files', form.Value, 'leasefile',
181 _('Leasefile'),
182 _('file where given <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>-leases will be stored'));
183
184 s.taboption('files', form.Flag, 'noresolv',
185 _('Ignore resolve file')).optional = true;
186
187 o = s.taboption('files', form.Value, 'resolvfile',
188 _('Resolve file'),
189 _('local <abbr title="Domain Name System">DNS</abbr> file'));
190
191 o.depends('noresolv', '0');
192 o.placeholder = '/tmp/resolv.conf.d/resolv.conf.auto';
193 o.optional = true;
194
195
196 s.taboption('files', form.Flag, 'nohosts',
197 _('Ignore <code>/etc/hosts</code>')).optional = true;
198
199 s.taboption('files', form.DynamicList, 'addnhosts',
200 _('Additional Hosts files')).optional = true;
201
202 o = s.taboption('advanced', form.Flag, 'quietdhcp',
203 _('Suppress logging'),
204 _('Suppress logging of the routine operation of these protocols'));
205 o.optional = true;
206
207 o = s.taboption('advanced', form.Flag, 'sequential_ip',
208 _('Allocate IP sequentially'),
209 _('Allocate IP addresses sequentially, starting from the lowest available address'));
210 o.optional = true;
211
212 o = s.taboption('advanced', form.Flag, 'boguspriv',
213 _('Filter private'),
214 _('Do not forward reverse lookups for local networks'));
215 o.default = o.enabled;
216
217 s.taboption('advanced', form.Flag, 'filterwin2k',
218 _('Filter useless'),
219 _('Do not forward requests that cannot be answered by public name servers'));
220
221
222 s.taboption('advanced', form.Flag, 'localise_queries',
223 _('Localise queries'),
224 _('Localise hostname depending on the requesting subnet if multiple IPs are available'));
225
226 if (L.hasSystemFeature('dnsmasq', 'dnssec')) {
227 o = s.taboption('advanced', form.Flag, 'dnssec',
228 _('DNSSEC'));
229 o.optional = true;
230
231 o = s.taboption('advanced', form.Flag, 'dnsseccheckunsigned',
232 _('DNSSEC check unsigned'),
233 _('Requires upstream supports DNSSEC; verify unsigned domain responses really come from unsigned domains'));
234 o.default = o.enabled;
235 o.optional = true;
236 }
237
238 s.taboption('general', form.Value, 'local',
239 _('Local server'),
240 _('Local domain specification. Names matching this domain are never forwarded and are resolved from DHCP or hosts files only'));
241
242 s.taboption('general', form.Value, 'domain',
243 _('Local domain'),
244 _('Local domain suffix appended to DHCP names and hosts file entries'));
245
246 s.taboption('advanced', form.Flag, 'expandhosts',
247 _('Expand hosts'),
248 _('Add local domain suffix to names served from hosts files'));
249
250 s.taboption('advanced', form.Flag, 'nonegcache',
251 _('No negative cache'),
252 _('Do not cache negative replies, e.g. for not existing domains'));
253
254 s.taboption('advanced', form.Value, 'serversfile',
255 _('Additional servers file'),
256 _('This file may contain lines like \'server=/domain/1.2.3.4\' or \'server=1.2.3.4\' for domain-specific or full upstream <abbr title="Domain Name System">DNS</abbr> servers.'));
257
258 s.taboption('advanced', form.Flag, 'strictorder',
259 _('Strict order'),
260 _('<abbr title="Domain Name System">DNS</abbr> servers will be queried in the order of the resolvfile')).optional = true;
261
262 s.taboption('advanced', form.Flag, 'allservers',
263 _('All Servers'),
264 _('Query all available upstream <abbr title="Domain Name System">DNS</abbr> servers')).optional = true;
265
266 o = s.taboption('advanced', form.DynamicList, 'bogusnxdomain', _('Bogus NX Domain Override'),
267 _('List of hosts that supply bogus NX domain results'));
268
269 o.optional = true;
270 o.placeholder = '67.215.65.132';
271
272
273 s.taboption('general', form.Flag, 'logqueries',
274 _('Log queries'),
275 _('Write received DNS requests to syslog')).optional = true;
276
277 o = s.taboption('general', form.DynamicList, 'server', _('DNS forwardings'),
278 _('List of <abbr title="Domain Name System">DNS</abbr> servers to forward requests to'));
279
280 o.optional = true;
281 o.placeholder = '/example.org/10.1.2.3';
282 o.validate = validateServerSpec;
283
284
285 o = s.taboption('general', form.DynamicList, 'address', _('Addresses'),
286 _('List of domains to force to an IP address.'));
287
288 o.optional = true;
289 o.placeholder = '/router.local/192.168.0.1';
290
291
292 o = s.taboption('general', form.Flag, 'rebind_protection',
293 _('Rebind protection'),
294 _('Discard upstream RFC1918 responses'));
295
296 o.rmempty = false;
297
298
299 o = s.taboption('general', form.Flag, 'rebind_localhost',
300 _('Allow localhost'),
301 _('Allow upstream responses in the 127.0.0.0/8 range, e.g. for RBL services'));
302
303 o.depends('rebind_protection', '1');
304
305
306 o = s.taboption('general', form.DynamicList, 'rebind_domain',
307 _('Domain whitelist'),
308 _('List of domains to allow RFC1918 responses for'));
309 o.optional = true;
310
311 o.depends('rebind_protection', '1');
312 o.placeholder = 'ihost.netflix.com';
313 o.validate = validateAddressList;
314
315
316 o = s.taboption('advanced', form.Value, 'port',
317 _('<abbr title="Domain Name System">DNS</abbr> server port'),
318 _('Listening port for inbound DNS queries'));
319
320 o.optional = true;
321 o.datatype = 'port';
322 o.placeholder = 53;
323
324
325 o = s.taboption('advanced', form.Value, 'queryport',
326 _('<abbr title="Domain Name System">DNS</abbr> query port'),
327 _('Fixed source port for outbound DNS queries'));
328
329 o.optional = true;
330 o.datatype = 'port';
331 o.placeholder = _('any');
332
333
334 o = s.taboption('advanced', form.Value, 'dhcpleasemax',
335 _('<abbr title="maximal">Max.</abbr> <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> leases'),
336 _('Maximum allowed number of active DHCP leases'));
337
338 o.optional = true;
339 o.datatype = 'uinteger';
340 o.placeholder = _('unlimited');
341
342
343 o = s.taboption('advanced', form.Value, 'ednspacket_max',
344 _('<abbr title="maximal">Max.</abbr> <abbr title="Extension Mechanisms for Domain Name System">EDNS0</abbr> packet size'),
345 _('Maximum allowed size of EDNS.0 UDP packets'));
346
347 o.optional = true;
348 o.datatype = 'uinteger';
349 o.placeholder = 1280;
350
351
352 o = s.taboption('advanced', form.Value, 'dnsforwardmax',
353 _('<abbr title="maximal">Max.</abbr> concurrent queries'),
354 _('Maximum allowed number of concurrent DNS queries'));
355
356 o.optional = true;
357 o.datatype = 'uinteger';
358 o.placeholder = 150;
359
360 o = s.taboption('advanced', form.Value, 'cachesize',
361 _('Size of DNS query cache'),
362 _('Number of cached DNS entries (max is 10000, 0 is no caching)'));
363 o.optional = true;
364 o.datatype = 'range(0,10000)';
365 o.placeholder = 150;
366
367 s.taboption('tftp', form.Flag, 'enable_tftp',
368 _('Enable TFTP server')).optional = true;
369
370 o = s.taboption('tftp', form.Value, 'tftp_root',
371 _('TFTP server root'),
372 _('Root directory for files served via TFTP'));
373
374 o.optional = true;
375 o.depends('enable_tftp', '1');
376 o.placeholder = '/';
377
378
379 o = s.taboption('tftp', form.Value, 'dhcp_boot',
380 _('Network boot image'),
381 _('Filename of the boot image advertised to clients'));
382
383 o.optional = true;
384 o.depends('enable_tftp', '1');
385 o.placeholder = 'pxelinux.0';
386
387 o = s.taboption('general', form.Flag, 'localservice',
388 _('Local Service Only'),
389 _('Limit DNS service to subnets interfaces on which we are serving DNS.'));
390 o.optional = false;
391 o.rmempty = false;
392
393 o = s.taboption('general', form.Flag, 'nonwildcard',
394 _('Non-wildcard'),
395 _('Bind dynamically to interfaces rather than wildcard address (recommended as linux default)'));
396 o.default = o.enabled;
397 o.optional = false;
398 o.rmempty = true;
399
400 o = s.taboption('general', form.DynamicList, 'interface',
401 _('Listen Interfaces'),
402 _('Limit listening to these interfaces, and loopback.'));
403 o.optional = true;
404
405 o = s.taboption('general', form.DynamicList, 'notinterface',
406 _('Exclude interfaces'),
407 _('Prevent listening on these interfaces.'));
408 o.optional = true;
409
410 o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
411 _('Static leases are used to assign fixed IP addresses and symbolic hostnames to DHCP clients. They are also required for non-dynamic interface configurations where only hosts with a corresponding lease are served.') + '<br />' +
412 _('Use the <em>Add</em> Button to add a new lease entry. The <em>MAC-Address</em> identifies the host, the <em>IPv4-Address</em> specifies the fixed address to use, and the <em>Hostname</em> is assigned as a symbolic name to the requesting host. The optional <em>Lease time</em> can be used to set non-standard host-specific lease time, e.g. 12h, 3d or infinite.'));
413
414 ss = o.subsection;
415
416 ss.addremove = true;
417 ss.anonymous = true;
418
419 so = ss.option(form.Value, 'name', _('Hostname'));
420 so.validate = validateHostname;
421 so.rmempty = true;
422 so.write = function(section, value) {
423 uci.set('dhcp', section, 'name', value);
424 uci.set('dhcp', section, 'dns', '1');
425 };
426 so.remove = function(section) {
427 uci.unset('dhcp', section, 'name');
428 uci.unset('dhcp', section, 'dns');
429 };
430
431 so = ss.option(form.Value, 'mac', _('<abbr title="Media Access Control">MAC</abbr>-Address'));
432 so.datatype = 'list(unique(macaddr))';
433 so.rmempty = true;
434 so.cfgvalue = function(section) {
435 var macs = L.toArray(uci.get('dhcp', section, 'mac')),
436 result = [];
437
438 for (var i = 0, mac; (mac = macs[i]) != null; i++)
439 if (/^([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2}):([0-9a-fA-F]{1,2})$/.test(mac))
440 result.push('%02X:%02X:%02X:%02X:%02X:%02X'.format(
441 parseInt(RegExp.$1, 16), parseInt(RegExp.$2, 16),
442 parseInt(RegExp.$3, 16), parseInt(RegExp.$4, 16),
443 parseInt(RegExp.$5, 16), parseInt(RegExp.$6, 16)));
444
445 return result.length ? result.join(' ') : null;
446 };
447 so.renderWidget = function(section_id, option_index, cfgvalue) {
448 var node = form.Value.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]),
449 ipopt = this.section.children.filter(function(o) { return o.option == 'ip' })[0];
450
451 node.addEventListener('cbi-dropdown-change', L.bind(function(ipopt, section_id, ev) {
452 var mac = ev.detail.value.value;
453 if (mac == null || mac == '' || !hosts[mac] || !hosts[mac].ipv4)
454 return;
455
456 var ip = ipopt.formvalue(section_id);
457 if (ip != null && ip != '')
458 return;
459
460 var node = ipopt.map.findElement('id', ipopt.cbid(section_id));
461 if (node)
462 dom.callClassMethod(node, 'setValue', hosts[mac].ipv4);
463 }, this, ipopt, section_id));
464
465 return node;
466 };
467 Object.keys(hosts).forEach(function(mac) {
468 var hint = hosts[mac].name || hosts[mac].ipv4;
469 so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
470 });
471
472 so.write = function(section, value) {
473 var ip = this.map.lookupOption('ip', section)[0].formvalue(section);
474 var hosts = uci.sections('dhcp', 'host');
475 var section_removed = false;
476
477 for (var i = 0; i < hosts.length; i++) {
478 if (ip == hosts[i].ip) {
479 uci.set('dhcp', hosts[i]['.name'], 'mac', [hosts[i].mac, value].join(' '));
480 uci.remove('dhcp', section);
481 section_removed = true;
482 break;
483 }
484 }
485
486 if (!section_removed) {
487 uci.set('dhcp', section, 'mac', value);
488 }
489 }
490
491 so = ss.option(form.Value, 'ip', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Address'));
492 so.datatype = 'or(ip4addr,"ignore")';
493 so.validate = function(section, value) {
494 var mac = this.map.lookupOption('mac', section),
495 name = this.map.lookupOption('name', section),
496 m = mac ? mac[0].formvalue(section) : null,
497 n = name ? name[0].formvalue(section) : null;
498
499 if ((m == null || m == '') && (n == null || n == ''))
500 return _('One of hostname or mac address must be specified!');
501
502 return true;
503 };
504 Object.keys(hosts).forEach(function(mac) {
505 if (hosts[mac].ipv4) {
506 var hint = hosts[mac].name;
507 so.value(hosts[mac].ipv4, hint ? '%s (%s)'.format(hosts[mac].ipv4, hint) : hosts[mac].ipv4);
508 }
509 });
510
511 so = ss.option(form.Value, 'leasetime', _('Lease time'));
512 so.rmempty = true;
513
514 so = ss.option(form.Value, 'duid', _('<abbr title="The DHCP Unique Identifier">DUID</abbr>'));
515 so.datatype = 'and(rangelength(20,36),hexstring)';
516 Object.keys(duids).forEach(function(duid) {
517 so.value(duid, '%s (%s)'.format(duid, duids[duid].hostname || duids[duid].macaddr || duids[duid].ip6addr || '?'));
518 });
519
520 so = ss.option(form.Value, 'hostid', _('<abbr title="Internet Protocol Version 6">IPv6</abbr>-Suffix (hex)'));
521
522 o = s.taboption('leases', CBILeaseStatus, '__status__');
523
524 if (has_dhcpv6)
525 o = s.taboption('leases', CBILease6Status, '__status6__');
526
527 return m.render().then(function(mapEl) {
528 poll.add(function() {
529 return callDHCPLeases().then(function(leaseinfo) {
530 var leases = Array.isArray(leaseinfo.dhcp_leases) ? leaseinfo.dhcp_leases : [],
531 leases6 = Array.isArray(leaseinfo.dhcp6_leases) ? leaseinfo.dhcp6_leases : [];
532
533 cbi_update_table(mapEl.querySelector('#lease_status_table'),
534 leases.map(function(lease) {
535 var exp;
536
537 if (lease.expires === false)
538 exp = E('em', _('unlimited'));
539 else if (lease.expires <= 0)
540 exp = E('em', _('expired'));
541 else
542 exp = '%t'.format(lease.expires);
543
544 return [
545 lease.hostname || '?',
546 lease.ipaddr,
547 lease.macaddr,
548 exp
549 ];
550 }),
551 E('em', _('There are no active leases')));
552
553 if (has_dhcpv6) {
554 cbi_update_table(mapEl.querySelector('#lease6_status_table'),
555 leases6.map(function(lease) {
556 var exp;
557
558 if (lease.expires === false)
559 exp = E('em', _('unlimited'));
560 else if (lease.expires <= 0)
561 exp = E('em', _('expired'));
562 else
563 exp = '%t'.format(lease.expires);
564
565 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
566 name = hint ? (hint.name || hint.ipv4 || hint.ipv6) : null,
567 host = null;
568
569 if (name && lease.hostname && lease.hostname != name && lease.ip6addr != name)
570 host = '%s (%s)'.format(lease.hostname, name);
571 else if (lease.hostname)
572 host = lease.hostname;
573 else if (name)
574 host = name;
575
576 return [
577 host || '-',
578 lease.ip6addrs ? lease.ip6addrs.join(' ') : lease.ip6addr,
579 lease.duid,
580 exp
581 ];
582 }),
583 E('em', _('There are no active leases')));
584 }
585 });
586 });
587
588 return mapEl;
589 });
590 }
591 });