luci-mod-network: dhcp.js: allow lease hostname to be empty
[oweals/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('div', { 'id': 'lease_status_table', 'class': 'table' }, [
35                                 E('div', { 'class': 'tr table-titles' }, [
36                                         E('div', { 'class': 'th' }, _('Hostname')),
37                                         E('div', { 'class': 'th' }, _('IPv4-Address')),
38                                         E('div', { 'class': 'th' }, _('MAC-Address')),
39                                         E('div', { 'class': 'th' }, _('Lease time remaining'))
40                                 ]),
41                                 E('div', { 'class': 'tr placeholder' }, [
42                                         E('div', { '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('div', { 'id': 'lease6_status_table', 'class': 'table' }, [
54                                 E('div', { 'class': 'tr table-titles' }, [
55                                         E('div', { 'class': 'th' }, _('Host')),
56                                         E('div', { 'class': 'th' }, _('IPv6-Address')),
57                                         E('div', { 'class': 'th' }, _('DUID')),
58                                         E('div', { 'class': 'th' }, _('Lease time remaining'))
59                                 ]),
60                                 E('div', { 'class': 'tr placeholder' }, [
61                                         E('div', { '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.Flag, 'rebind_protection',
286                         _('Rebind protection'),
287                         _('Discard upstream RFC1918 responses'));
288
289                 o.rmempty = false;
290
291
292                 o = s.taboption('general', form.Flag, 'rebind_localhost',
293                         _('Allow localhost'),
294                         _('Allow upstream responses in the 127.0.0.0/8 range, e.g. for RBL services'));
295
296                 o.depends('rebind_protection', '1');
297
298
299                 o = s.taboption('general', form.DynamicList, 'rebind_domain',
300                         _('Domain whitelist'),
301                         _('List of domains to allow RFC1918 responses for'));
302                 o.optional = true;
303
304                 o.depends('rebind_protection', '1');
305                 o.placeholder = 'ihost.netflix.com';
306                 o.validate = validateAddressList;
307
308
309                 o = s.taboption('advanced', form.Value, 'port',
310                         _('<abbr title="Domain Name System">DNS</abbr> server port'),
311                         _('Listening port for inbound DNS queries'));
312
313                 o.optional = true;
314                 o.datatype = 'port';
315                 o.placeholder = 53;
316
317
318                 o = s.taboption('advanced', form.Value, 'queryport',
319                         _('<abbr title="Domain Name System">DNS</abbr> query port'),
320                         _('Fixed source port for outbound DNS queries'));
321
322                 o.optional = true;
323                 o.datatype = 'port';
324                 o.placeholder = _('any');
325
326
327                 o = s.taboption('advanced', form.Value, 'dhcpleasemax',
328                         _('<abbr title="maximal">Max.</abbr> <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> leases'),
329                         _('Maximum allowed number of active DHCP leases'));
330
331                 o.optional = true;
332                 o.datatype = 'uinteger';
333                 o.placeholder = _('unlimited');
334
335
336                 o = s.taboption('advanced', form.Value, 'ednspacket_max',
337                         _('<abbr title="maximal">Max.</abbr> <abbr title="Extension Mechanisms for Domain Name System">EDNS0</abbr> packet size'),
338                         _('Maximum allowed size of EDNS.0 UDP packets'));
339
340                 o.optional = true;
341                 o.datatype = 'uinteger';
342                 o.placeholder = 1280;
343
344
345                 o = s.taboption('advanced', form.Value, 'dnsforwardmax',
346                         _('<abbr title="maximal">Max.</abbr> concurrent queries'),
347                         _('Maximum allowed number of concurrent DNS queries'));
348
349                 o.optional = true;
350                 o.datatype = 'uinteger';
351                 o.placeholder = 150;
352
353                 o = s.taboption('advanced', form.Value, 'cachesize',
354                         _('Size of DNS query cache'),
355                         _('Number of cached DNS entries (max is 10000, 0 is no caching)'));
356                 o.optional = true;
357                 o.datatype = 'range(0,10000)';
358                 o.placeholder = 150;
359
360                 s.taboption('tftp', form.Flag, 'enable_tftp',
361                         _('Enable TFTP server')).optional = true;
362
363                 o = s.taboption('tftp', form.Value, 'tftp_root',
364                         _('TFTP server root'),
365                         _('Root directory for files served via TFTP'));
366
367                 o.optional = true;
368                 o.depends('enable_tftp', '1');
369                 o.placeholder = '/';
370
371
372                 o = s.taboption('tftp', form.Value, 'dhcp_boot',
373                         _('Network boot image'),
374                         _('Filename of the boot image advertised to clients'));
375
376                 o.optional = true;
377                 o.depends('enable_tftp', '1');
378                 o.placeholder = 'pxelinux.0';
379
380                 o = s.taboption('general', form.Flag, 'localservice',
381                         _('Local Service Only'),
382                         _('Limit DNS service to subnets interfaces on which we are serving DNS.'));
383                 o.optional = false;
384                 o.rmempty = false;
385
386                 o = s.taboption('general', form.Flag, 'nonwildcard',
387                         _('Non-wildcard'),
388                         _('Bind dynamically to interfaces rather than wildcard address (recommended as linux default)'));
389                 o.default = o.enabled;
390                 o.optional = false;
391                 o.rmempty = true;
392
393                 o = s.taboption('general', form.DynamicList, 'interface',
394                         _('Listen Interfaces'),
395                         _('Limit listening to these interfaces, and loopback.'));
396                 o.optional = true;
397
398                 o = s.taboption('general', form.DynamicList, 'notinterface',
399                         _('Exclude interfaces'),
400                         _('Prevent listening on these interfaces.'));
401                 o.optional = true;
402
403                 o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
404                         _('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 />' +
405                         _('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.'));
406
407                 ss = o.subsection;
408
409                 ss.addremove = true;
410                 ss.anonymous = true;
411
412                 so = ss.option(form.Value, 'name', _('Hostname'));
413                 so.validate = validateHostname;
414                 so.rmempty  = true;
415                 so.write = function(section, value) {
416                         uci.set('dhcp', section, 'name', value);
417                         uci.set('dhcp', section, 'dns', '1');
418                 };
419                 so.remove = function(section) {
420                         uci.unset('dhcp', section, 'name');
421                         uci.unset('dhcp', section, 'dns');
422                 };
423
424                 so = ss.option(form.Value, 'mac', _('<abbr title="Media Access Control">MAC</abbr>-Address'));
425                 so.datatype = 'list(unique(macaddr))';
426                 so.rmempty  = true;
427                 so.cfgvalue = function(section) {
428                         var macs = uci.get('dhcp', section, 'mac'),
429                             result = [];
430
431                         if (!Array.isArray(macs))
432                                 macs = (macs != null && macs != '') ? macs.split(/\ss+/) : [];
433
434                         for (var i = 0, mac; (mac = macs[i]) != null; i++)
435                                 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))
436                                         result.push('%02X:%02X:%02X:%02X:%02X:%02X'.format(
437                                                 parseInt(RegExp.$1, 16), parseInt(RegExp.$2, 16),
438                                                 parseInt(RegExp.$3, 16), parseInt(RegExp.$4, 16),
439                                                 parseInt(RegExp.$5, 16), parseInt(RegExp.$6, 16)));
440
441                         return result.length ? result.join(' ') : null;
442                 };
443                 so.renderWidget = function(section_id, option_index, cfgvalue) {
444                         var node = form.Value.prototype.renderWidget.apply(this, [section_id, option_index, cfgvalue]),
445                             ipopt = this.section.children.filter(function(o) { return o.option == 'ip' })[0];
446
447                         node.addEventListener('cbi-dropdown-change', L.bind(function(ipopt, section_id, ev) {
448                                 var mac = ev.detail.value.value;
449                                 if (mac == null || mac == '' || !hosts[mac] || !hosts[mac].ipv4)
450                                         return;
451
452                                 var ip = ipopt.formvalue(section_id);
453                                 if (ip != null && ip != '')
454                                         return;
455
456                                 var node = ipopt.map.findElement('id', ipopt.cbid(section_id));
457                                 if (node)
458                                         dom.callClassMethod(node, 'setValue', hosts[mac].ipv4);
459                         }, this, ipopt, section_id));
460
461                         return node;
462                 };
463                 Object.keys(hosts).forEach(function(mac) {
464                         var hint = hosts[mac].name || hosts[mac].ipv4;
465                         so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
466                 });
467
468                 so = ss.option(form.Value, 'ip', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Address'));
469                 so.datatype = 'or(ip4addr,"ignore")';
470                 so.validate = function(section, value) {
471                         var mac = this.map.lookupOption('mac', section),
472                             name = this.map.lookupOption('name', section),
473                             m = mac ? mac[0].formvalue(section) : null,
474                             n = name ? name[0].formvalue(section) : null;
475
476                         if ((m == null || m == '') && (n == null || n == ''))
477                                 return _('One of hostname or mac address must be specified!');
478
479                         return true;
480                 };
481                 Object.keys(hosts).forEach(function(mac) {
482                         if (hosts[mac].ipv4) {
483                                 var hint = hosts[mac].name;
484                                 so.value(hosts[mac].ipv4, hint ? '%s (%s)'.format(hosts[mac].ipv4, hint) : hosts[mac].ipv4);
485                         }
486                 });
487
488                 so = ss.option(form.Value, 'leasetime', _('Lease time'));
489                 so.rmempty = true;
490
491                 so = ss.option(form.Value, 'duid', _('<abbr title="The DHCP Unique Identifier">DUID</abbr>'));
492                 so.datatype = 'and(rangelength(20,36),hexstring)';
493                 Object.keys(duids).forEach(function(duid) {
494                         so.value(duid, '%s (%s)'.format(duid, duids[duid].hostname || duids[duid].macaddr || duids[duid].ip6addr || '?'));
495                 });
496
497                 so = ss.option(form.Value, 'hostid', _('<abbr title="Internet Protocol Version 6">IPv6</abbr>-Suffix (hex)'));
498
499                 o = s.taboption('leases', CBILeaseStatus, '__status__');
500
501                 if (has_dhcpv6)
502                         o = s.taboption('leases', CBILease6Status, '__status6__');
503
504                 return m.render().then(function(mapEl) {
505                         poll.add(function() {
506                                 return callDHCPLeases().then(function(leaseinfo) {
507                                         var leases = Array.isArray(leaseinfo.dhcp_leases) ? leaseinfo.dhcp_leases : [],
508                                             leases6 = Array.isArray(leaseinfo.dhcp6_leases) ? leaseinfo.dhcp6_leases : [];
509
510                                         cbi_update_table(mapEl.querySelector('#lease_status_table'),
511                                                 leases.map(function(lease) {
512                                                         var exp;
513
514                                                         if (lease.expires === false)
515                                                                 exp = E('em', _('unlimited'));
516                                                         else if (lease.expires <= 0)
517                                                                 exp = E('em', _('expired'));
518                                                         else
519                                                                 exp = '%t'.format(lease.expires);
520
521                                                         return [
522                                                                 lease.hostname || '?',
523                                                                 lease.ipaddr,
524                                                                 lease.macaddr,
525                                                                 exp
526                                                         ];
527                                                 }),
528                                                 E('em', _('There are no active leases')));
529
530                                         if (has_dhcpv6) {
531                                                 cbi_update_table(mapEl.querySelector('#lease6_status_table'),
532                                                         leases6.map(function(lease) {
533                                                                 var exp;
534
535                                                                 if (lease.expires === false)
536                                                                         exp = E('em', _('unlimited'));
537                                                                 else if (lease.expires <= 0)
538                                                                         exp = E('em', _('expired'));
539                                                                 else
540                                                                         exp = '%t'.format(lease.expires);
541
542                                                                 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
543                                                                     name = hint ? (hint.name || hint.ipv4 || hint.ipv6) : null,
544                                                                     host = null;
545
546                                                                 if (name && lease.hostname && lease.hostname != name && lease.ip6addr != name)
547                                                                         host = '%s (%s)'.format(lease.hostname, name);
548                                                                 else if (lease.hostname)
549                                                                         host = lease.hostname;
550                                                                 else if (name)
551                                                                         host = name;
552
553                                                                 return [
554                                                                         host || '-',
555                                                                         lease.ip6addrs ? lease.ip6addrs.join(' ') : lease.ip6addr,
556                                                                         lease.duid,
557                                                                         exp
558                                                                 ];
559                                                         }),
560                                                         E('em', _('There are no active leases')));
561                                         }
562                                 });
563                         });
564
565                         return mapEl;
566                 });
567         }
568 });