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