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