luci-app-firewall: tools/firewall.js: honour readonly property
[oweals/luci.git] / applications / luci-app-firewall / htdocs / luci-static / resources / tools / firewall.js
1 'use strict';
2 'require baseclass';
3 'require dom';
4 'require ui';
5 'require uci';
6 'require form';
7 'require network';
8 'require firewall';
9 'require tools.prng as random';
10
11 var protocols = [
12         'ip', 0, 'IP',
13         'hopopt', 0, 'HOPOPT',
14         'icmp', 1, 'ICMP',
15         'igmp', 2, 'IGMP',
16         'ggp', 3 , 'GGP',
17         'ipencap', 4, 'IP-ENCAP',
18         'st', 5, 'ST',
19         'tcp', 6, 'TCP',
20         'egp', 8, 'EGP',
21         'igp', 9, 'IGP',
22         'pup', 12, 'PUP',
23         'udp', 17, 'UDP',
24         'hmp', 20, 'HMP',
25         'xns-idp', 22, 'XNS-IDP',
26         'rdp', 27, 'RDP',
27         'iso-tp4', 29, 'ISO-TP4',
28         'dccp', 33, 'DCCP',
29         'xtp', 36, 'XTP',
30         'ddp', 37, 'DDP',
31         'idpr-cmtp', 38, 'IDPR-CMTP',
32         'ipv6', 41, 'IPv6',
33         'ipv6-route', 43, 'IPv6-Route',
34         'ipv6-frag', 44, 'IPv6-Frag',
35         'idrp', 45, 'IDRP',
36         'rsvp', 46, 'RSVP',
37         'gre', 47, 'GRE',
38         'esp', 50, 'IPSEC-ESP',
39         'ah', 51, 'IPSEC-AH',
40         'skip', 57, 'SKIP',
41         'icmpv6', 58, 'IPv6-ICMP',
42         'ipv6-icmp', 58, 'IPv6-ICMP',
43         'ipv6-nonxt', 59, 'IPv6-NoNxt',
44         'ipv6-opts', 60, 'IPv6-Opts',
45         'rspf', 73, 'RSPF',
46         'rspf', 73, 'CPHB',
47         'vmtp', 81, 'VMTP',
48         'eigrp', 88, 'EIGRP',
49         'ospf', 89, 'OSPFIGP',
50         'ax.25', 93, 'AX.25',
51         'ipip', 94, 'IPIP',
52         'etherip', 97, 'ETHERIP',
53         'encap', 98, 'ENCAP',
54         'pim', 103, 'PIM',
55         'ipcomp', 108, 'IPCOMP',
56         'vrrp', 112, 'VRRP',
57         'l2tp', 115, 'L2TP',
58         'isis', 124, 'ISIS',
59         'sctp', 132, 'SCTP',
60         'fc', 133, 'FC',
61         'mh', 135, 'Mobility-Header',
62         'ipv6-mh', 135, 'Mobility-Header',
63         'mobility-header', 135, 'Mobility-Header',
64         'udplite', 136, 'UDPLite',
65         'mpls-in-ip', 137, 'MPLS-in-IP',
66         'manet', 138, 'MANET',
67         'hip', 139, 'HIP',
68         'shim6', 140, 'Shim6',
69         'wesp', 141, 'WESP',
70         'rohc', 142, 'ROHC',
71 ];
72
73 function lookupProto(x) {
74         if (x == null || x === '')
75                 return null;
76
77         var s = String(x).toLowerCase();
78
79         for (var i = 0; i < protocols.length; i += 3)
80                 if (s == protocols[i] || s == protocols[i+1])
81                         return [ protocols[i+1], protocols[i+2], protocols[i] ];
82
83         return [ -1, x, x ];
84 }
85
86 return baseclass.extend({
87         fmt: function(fmtstr, args, values) {
88                 var repl = [],
89                     wrap = false,
90                     tokens = [];
91
92                 if (values == null) {
93                         values = [];
94                         wrap = true;
95                 }
96
97                 var get = function(args, key) {
98                         var names = key.trim().split(/\./),
99                             obj = args,
100                             ctx = obj;
101
102                         for (var i = 0; i < names.length; i++) {
103                                 if (!L.isObject(obj))
104                                         return null;
105
106                                 ctx = obj;
107                                 obj = obj[names[i]];
108                         }
109
110                         if (typeof(obj) == 'function')
111                                 return obj.call(ctx);
112
113                         return obj;
114                 };
115
116                 var isset = function(val) {
117                         if (L.isObject(val) && !dom.elem(val)) {
118                                 for (var k in val)
119                                         if (val.hasOwnProperty(k))
120                                                 return true;
121
122                                 return false;
123                         }
124                         else if (Array.isArray(val)) {
125                                 return (val.length > 0);
126                         }
127                         else {
128                                 return (val !== null && val !== undefined && val !== '' && val !== false);
129                         }
130                 };
131
132                 var parse = function(tokens, text) {
133                         if (dom.elem(text)) {
134                                 tokens.push('<span data-fmt-placeholder="%d"></span>'.format(values.length));
135                                 values.push(text);
136                         }
137                         else {
138                                 tokens.push(String(text).replace(/\\(.)/g, '$1'));
139                         }
140                 };
141
142                 for (var i = 0, last = 0; i <= fmtstr.length; i++) {
143                         if (fmtstr.charAt(i) == '%' && fmtstr.charAt(i + 1) == '{') {
144                                 if (i > last)
145                                         parse(tokens, fmtstr.substring(last, i));
146
147                                 var j = i + 1,  nest = 0;
148
149                                 var subexpr = [];
150
151                                 for (var off = j + 1, esc = false; j <= fmtstr.length; j++) {
152                                         var ch = fmtstr.charAt(j);
153
154                                         if (esc) {
155                                                 esc = false;
156                                         }
157                                         else if (ch == '\\') {
158                                                 esc = true;
159                                         }
160                                         else if (ch == '{') {
161                                                 nest++;
162                                         }
163                                         else if (ch == '}') {
164                                                 if (--nest == 0) {
165                                                         subexpr.push(fmtstr.substring(off, j));
166                                                         break;
167                                                 }
168                                         }
169                                         else if (ch == '?' || ch == ':' || ch == '#') {
170                                                 if (nest == 1) {
171                                                         subexpr.push(fmtstr.substring(off, j));
172                                                         subexpr.push(ch);
173                                                         off = j + 1;
174                                                 }
175                                         }
176                                 }
177
178                                 var varname  = subexpr[0].trim(),
179                                     op1      = (subexpr[1] != null) ? subexpr[1] : '?',
180                                     if_set   = (subexpr[2] != null && subexpr[2] != '') ? subexpr[2] : '%{' + varname + '}',
181                                     op2      = (subexpr[3] != null) ? subexpr[3] : ':',
182                                     if_unset = (subexpr[4] != null) ? subexpr[4] : '';
183
184                                 /* Invalid expression */
185                                 if (nest != 0 || subexpr.length > 5 || varname == '') {
186                                         return fmtstr;
187                                 }
188
189                                 /* enumeration */
190                                 else if (op1 == '#' && subexpr.length == 3) {
191                                         var items = L.toArray(get(args, varname));
192
193                                         for (var k = 0; k < items.length; k++) {
194                                                 tokens.push.apply(tokens, this.fmt(if_set, Object.assign({}, args, {
195                                                         first: k == 0,
196                                                         next:  k > 0,
197                                                         last:  (k + 1) == items.length,
198                                                         item:  items[k]
199                                                 }), values));
200                                         }
201                                 }
202
203                                 /* ternary expression */
204                                 else if (op1 == '?' && op2 == ':' && (subexpr.length == 1 || subexpr.length == 3 || subexpr.length == 5)) {
205                                         var val = get(args, varname);
206
207                                         if (subexpr.length == 1)
208                                                 parse(tokens, isset(val) ? val : '');
209                                         else if (isset(val))
210                                                 tokens.push.apply(tokens, this.fmt(if_set, args, values));
211                                         else
212                                                 tokens.push.apply(tokens, this.fmt(if_unset, args, values));
213                                 }
214
215                                 /* unrecognized command */
216                                 else {
217                                         return fmtstr;
218                                 }
219
220                                 last = j + 1;
221                                 i = j;
222                         }
223                         else if (i >= fmtstr.length) {
224                                 if (i > last)
225                                         parse(tokens, fmtstr.substring(last, i));
226                         }
227                 }
228
229                 if (wrap) {
230                         var node = E('span', {}, tokens.join('')),
231                             repl = node.querySelectorAll('span[data-fmt-placeholder]');
232
233                         for (var i = 0; i < repl.length; i++)
234                                 repl[i].parentNode.replaceChild(values[repl[i].getAttribute('data-fmt-placeholder')], repl[i]);
235
236                         return node;
237                 }
238                 else {
239                         return tokens;
240                 }
241         },
242
243         map_invert: function(v, fn) {
244                 return L.toArray(v).map(function(v) {
245                         v = String(v);
246
247                         if (fn != null && typeof(v[fn]) == 'function')
248                                 v = v[fn].call(v);
249
250                         return {
251                                 ival: v,
252                                 inv: v.charAt(0) == '!',
253                                 val: v.replace(/^!\s*/, '')
254                         };
255                 });
256         },
257
258         lookupProto: lookupProto,
259
260         addDSCPOption: function(s, is_target) {
261                 var o = s.taboption(is_target ? 'general' : 'advanced', form.Value, is_target ? 'set_dscp' : 'dscp',
262                         is_target ? _('DSCP mark') : _('Match DSCP'),
263                         is_target ? _('Apply the given DSCP class or value to established connections.') : _('Matches traffic carrying the specified DSCP marking.'));
264
265                 o.modalonly = true;
266                 o.rmempty = !is_target;
267                 o.placeholder = _('any');
268
269                 if (is_target)
270                         o.depends('target', 'DSCP');
271
272                 o.value('CS0');
273                 o.value('CS1');
274                 o.value('CS2');
275                 o.value('CS3');
276                 o.value('CS4');
277                 o.value('CS5');
278                 o.value('CS6');
279                 o.value('CS7');
280                 o.value('BE');
281                 o.value('AF11');
282                 o.value('AF12');
283                 o.value('AF13');
284                 o.value('AF21');
285                 o.value('AF22');
286                 o.value('AF23');
287                 o.value('AF31');
288                 o.value('AF32');
289                 o.value('AF33');
290                 o.value('AF41');
291                 o.value('AF42');
292                 o.value('AF43');
293                 o.value('EF');
294                 o.validate = function(section_id, value) {
295                         if (value == '')
296                                 return is_target ? _('DSCP mark required') : true;
297
298                         if (!is_target)
299                                 value = String(value).replace(/^!\s*/, '');
300
301                         var m = value.match(/^(?:CS[0-7]|BE|AF[1234][123]|EF|(0x[0-9a-f]{1,2}|[0-9]{1,2}))$/);
302
303                         if (!m || (m[1] != null && +m[1] > 0x3f))
304                                 return _('Invalid DSCP mark');
305
306                         return true;
307                 };
308
309                 return o;
310         },
311
312         addMarkOption: function(s, is_target) {
313                 var o = s.taboption(is_target ? 'general' : 'advanced', form.Value,
314                         (is_target > 1) ? 'set_xmark' : (is_target ? 'set_mark' : 'mark'),
315                         (is_target > 1) ? _('XOR mark') : (is_target ? _('Set mark') : _('Match mark')),
316                         (is_target > 1) ? _('Apply a bitwise XOR of the given value and the existing mark value on established connections. Format is value[/mask]. If a mask is specified then those bits set in the mask are zeroed out.') :
317                                 (is_target ? _('Set the given mark value on established connections. Format is value[/mask]. If a mask is specified then only those bits set in the mask are modified.') :
318                                                 _('Matches a specific firewall mark or a range of different marks.')));
319
320                 o.modalonly = true;
321                 o.rmempty = true;
322
323                 if (is_target > 1)
324                         o.depends('target', 'MARK_XOR');
325                 else if (is_target)
326                         o.depends('target', 'MARK_SET');
327
328                 o.validate = function(section_id, value) {
329                         if (value == '')
330                                 return is_target ? _('Valid firewall mark required') : true;
331
332                         if (!is_target)
333                                 value = String(value).replace(/^!\s*/, '');
334
335                         var m = value.match(/^(0x[0-9a-f]{1,8}|[0-9]{1,10})(?:\/(0x[0-9a-f]{1,8}|[0-9]{1,10}))?$/i);
336
337                         if (!m || +m[1] > 0xffffffff || (m[2] != null && +m[2] > 0xffffffff))
338                                 return _('Expecting: %s').format(_('valid firewall mark'));
339
340                         return true;
341                 };
342
343                 return o;
344         },
345
346         addLimitOption: function(s) {
347                 var o = s.taboption('advanced', form.Value, 'limit',
348                         _('Limit matching'),
349                         _('Limits traffic matching to the specified rate.'));
350
351                 o.modalonly = true;
352                 o.rmempty = true;
353                 o.placeholder = _('unlimited');
354                 o.value('10/second');
355                 o.value('60/minute');
356                 o.value('3/hour');
357                 o.value('500/day');
358                 o.validate = function(section_id, value) {
359                         if (value == '')
360                                 return true;
361
362                         var m = String(value).toLowerCase().match(/^(?:0x[0-9a-f]{1,8}|[0-9]{1,10})\/([a-z]+)$/),
363                             u = ['second', 'minute', 'hour', 'day'],
364                             i = 0;
365
366                         if (m)
367                                 for (i = 0; i < u.length; i++)
368                                         if (u[i].indexOf(m[1]) == 0)
369                                                 break;
370
371                         if (!m || i >= u.length)
372                                 return _('Invalid limit value');
373
374                         return true;
375                 };
376
377                 return o;
378         },
379
380         addLimitBurstOption: function(s) {
381                 var o = s.taboption('advanced', form.Value, 'limit_burst',
382                         _('Limit burst'),
383                         _('Maximum initial number of packets to match: this number gets recharged by one every time the limit specified above is not reached, up to this number.'));
384
385                 o.modalonly = true;
386                 o.rmempty = true;
387                 o.placeholder = '5';
388                 o.datatype = 'uinteger';
389                 o.depends({ limit: null, '!reverse': true });
390
391                 return o;
392         },
393
394         transformHostHints: function(family, hosts) {
395                 var choice_values = [], choice_labels = {};
396
397                 if (!family || family == 'ipv4') {
398                         L.sortedKeys(hosts, 'ipv4', 'addr').forEach(function(mac) {
399                                 var val = hosts[mac].ipv4,
400                                     txt = hosts[mac].name || mac;
401
402                                 choice_values.push(val);
403                                 choice_labels[val] = E([], [ val, ' (', E('strong', {}, [txt]), ')' ]);
404                         });
405                 }
406
407                 if (!family || family == 'ipv6') {
408                         L.sortedKeys(hosts, 'ipv6', 'addr').forEach(function(mac) {
409                                 var val = hosts[mac].ipv6,
410                                     txt = hosts[mac].name || mac;
411
412                                 choice_values.push(val);
413                                 choice_labels[val] = E([], [ val, ' (', E('strong', {}, [txt]), ')' ]);
414                         });
415                 }
416
417                 return [choice_values, choice_labels];
418         },
419
420         updateHostHints: function(map, section_id, option, family, hosts) {
421                 var opt = map.lookupOption(option, section_id)[0].getUIElement(section_id),
422                     choices = this.transformHostHints(family, hosts);
423
424                 opt.clearChoices();
425                 opt.addChoices(choices[0], choices[1]);
426         },
427
428         addIPOption: function(s, tab, name, label, description, family, hosts, multiple) {
429                 var o = s.taboption(tab, multiple ? form.DynamicList : form.Value, name, label, description);
430
431                 o.modalonly = true;
432                 o.datatype = 'list(neg(ipmask))';
433                 o.placeholder = multiple ? _('-- add IP --') : _('any');
434
435                 if (family != null) {
436                         var choices = this.transformHostHints(family, hosts);
437
438                         for (var i = 0; i < choices[0].length; i++)
439                                 o.value(choices[0][i], choices[1][choices[0][i]]);
440                 }
441
442                 /* force combobox rendering */
443                 o.transformChoices = function() {
444                         return this.super('transformChoices', []) || {};
445                 };
446
447                 return o;
448         },
449
450         addLocalIPOption: function(s, tab, name, label, description, devices) {
451                 var o = s.taboption(tab, form.Value, name, label, description);
452
453                 o.modalonly = true;
454                 o.datatype = 'ip4addr("nomask")';
455                 o.placeholder = _('any');
456
457                 L.sortedKeys(devices, 'name').forEach(function(dev) {
458                         var ip4addrs = devices[dev].ipaddrs;
459
460                         if (!L.isObject(devices[dev].flags) || !Array.isArray(ip4addrs) || devices[dev].flags.loopback)
461                                 return;
462
463                         for (var i = 0; i < ip4addrs.length; i++) {
464                                 if (!L.isObject(ip4addrs[i]) || !ip4addrs[i].address)
465                                         continue;
466
467                                 o.value(ip4addrs[i].address, E([], [
468                                         ip4addrs[i].address, ' (', E('strong', {}, [dev]), ')'
469                                 ]));
470                         }
471                 });
472
473                 return o;
474         },
475
476         addMACOption: function(s, tab, name, label, description, hosts) {
477                 var o = s.taboption(tab, form.DynamicList, name, label, description);
478
479                 o.modalonly = true;
480                 o.datatype = 'list(macaddr)';
481                 o.placeholder = _('-- add MAC --');
482
483                 L.sortedKeys(hosts).forEach(function(mac) {
484                         o.value(mac, E([], [ mac, ' (', E('strong', {}, [
485                                 hosts[mac].name || hosts[mac].ipv4 || hosts[mac].ipv6 || '?'
486                         ]), ')' ]));
487                 });
488
489                 return o;
490         },
491
492         CBIProtocolSelect: form.MultiValue.extend({
493                 __name__: 'CBI.ProtocolSelect',
494
495                 addChoice: function(value, label) {
496                         if (!Array.isArray(this.keylist) || this.keylist.indexOf(value) == -1)
497                                 this.value(value, label);
498                 },
499
500                 load: function(section_id) {
501                         var cfgvalue = L.toArray(this.super('load', [section_id]) || this.default).sort();
502
503                         ['all', 'tcp', 'udp', 'icmp'].concat(cfgvalue).forEach(L.bind(function(value) {
504                                 switch (value) {
505                                 case 'all':
506                                 case 'any':
507                                 case '*':
508                                         this.addChoice('all', _('Any'));
509                                         break;
510
511                                 case 'tcpudp':
512                                         this.addChoice('tcp', 'TCP');
513                                         this.addChoice('udp', 'UDP');
514                                         break;
515
516                                 default:
517                                         var m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
518                                             p = lookupProto(m ? +m[1] : value);
519
520                                         this.addChoice(p[2], p[1]);
521                                         break;
522                                 }
523                         }, this));
524
525                         return cfgvalue;
526                 },
527
528                 renderWidget: function(section_id, option_index, cfgvalue) {
529                         var value = (cfgvalue != null) ? cfgvalue : this.default,
530                             choices = this.transformChoices();
531
532                         var widget = new ui.Dropdown(L.toArray(value), choices, {
533                                 id: this.cbid(section_id),
534                                 sort: this.keylist,
535                                 multiple: true,
536                                 optional: false,
537                                 display_items: 10,
538                                 dropdown_items: -1,
539                                 create: true,
540                                 disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
541                                 validate: function(value) {
542                                         var v = L.toArray(value);
543
544                                         for (var i = 0; i < v.length; i++) {
545                                                 if (v[i] == 'all')
546                                                         continue;
547
548                                                 var m = v[i].match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/);
549
550                                                 if (m ? (+m[1] > 255) : (lookupProto(v[i])[0] == -1))
551                                                         return _('Unrecognized protocol');
552                                         }
553
554                                         return true;
555                                 }
556                         });
557
558                         widget.createChoiceElement = function(sb, value) {
559                                 var m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
560                                     p = lookupProto(lookupProto(m ? +m[1] : value)[0]);
561
562                                 return ui.Dropdown.prototype.createChoiceElement.call(this, sb, p[2], p[1]);
563                         };
564
565                         widget.createItems = function(sb, value) {
566                                 var values = L.toArray(value).map(function(value) {
567                                         var m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
568                                             p = lookupProto(m ? +m[1] : value);
569
570                                         return (p[0] > -1) ? p[2] : value;
571                                 });
572
573                                 return ui.Dropdown.prototype.createItems.call(this, sb, values.join(' '));
574                         };
575
576                         widget.toggleItem = function(sb, li) {
577                                 var value = li.getAttribute('data-value'),
578                                     toggleFn = ui.Dropdown.prototype.toggleItem;
579
580                                 toggleFn.call(this, sb, li);
581
582                                 if (value == 'all') {
583                                         var items = li.parentNode.querySelectorAll('li[data-value]');
584
585                                         for (var j = 0; j < items.length; j++)
586                                                 if (items[j] !== li)
587                                                         toggleFn.call(this, sb, items[j], false);
588                                 }
589                                 else {
590                                         toggleFn.call(this, sb, li.parentNode.querySelector('li[data-value="all"]'), false);
591                                 }
592                         };
593
594                         return widget.render();
595                 }
596         }),
597
598         checkLegacySNAT: function() {
599                 var redirects = uci.sections('firewall', 'redirect');
600
601                 for (var i = 0; i < redirects.length; i++)
602                         if ((redirects[i]['target'] || '').toLowerCase() == 'snat')
603                                 return true;
604
605                 return false;
606         },
607
608         handleMigration: function(ev) {
609                 var redirects = uci.sections('firewall', 'redirect'),
610                     tasks = [];
611
612                 var mapping = {
613                         dest: 'src',
614                         reflection: null,
615                         reflection_src: null,
616                         src_dip: 'snat_ip',
617                         src_dport: 'snat_port',
618                         src: null
619                 };
620
621                 for (var i = 0; i < redirects.length; i++) {
622                         if ((redirects[i]['target'] || '').toLowerCase() != 'snat')
623                                 continue;
624
625                         var sid = uci.add('firewall', 'nat');
626
627                         for (var opt in redirects[i]) {
628                                 if (opt.charAt(0) == '.')
629                                         continue;
630
631                                 if (mapping[opt] === null)
632                                         continue;
633
634                                 uci.set('firewall', sid, mapping[opt] || opt, redirects[i][opt]);
635                         }
636
637                         uci.remove('firewall', redirects[i]['.name']);
638                 }
639
640                 return uci.save()
641                         .then(L.bind(ui.changes.init, ui.changes))
642                         .then(L.bind(ui.changes.apply, ui.changes));
643         },
644
645         renderMigration: function() {
646                 ui.showModal(_('Firewall configuration migration'), [
647                         E('p', _('The existing firewall configuration needs to be changed for LuCI to function properly.')),
648                         E('p', _('Upon pressing "Continue", "redirect" sections with target "SNAT" will be converted to "nat" sections and the firewall will be restarted to apply the updated configuration.')),
649                         E('div', { 'class': 'right' },
650                                 E('button', {
651                                         'class': 'btn cbi-button-action important',
652                                         'click': ui.createHandlerFn(this, 'handleMigration')
653                                 }, _('Continue')))
654                 ]);
655         },
656 });