dd58670694fd11d3a92a53acc520b3844a94b47f
[project/luci.git] / modules / luci-mod-status / htdocs / luci-static / resources / view / status / iptables.js
1 'use strict';
2 'require view';
3 'require poll';
4 'require fs';
5 'require ui';
6
7 var table_names = [ 'Filter', 'NAT', 'Mangle', 'Raw' ],
8 raw_style = 'font-family:monospace;font-size:smaller;text-align:right';
9
10 return view.extend({
11 load: function() {
12 return L.resolveDefault(fs.stat('/usr/sbin/ip6tables'));
13 },
14
15 createTableSection: function(is_ipv6, table) {
16 var idiv = document.querySelector('div[data-tab="%s"]'.format(is_ipv6 ? 'ip6tables' : 'iptables')),
17 tdiv = idiv.querySelector('[data-table="%s-%s"]'.format(is_ipv6 ? 'ipv6' : 'ipv4', table)),
18 title = '%s: %s'.format(_('Table'), table);
19
20 if (!tdiv) {
21 tdiv = E('div', { 'data-table': '%s-%s'.format(is_ipv6 ? 'ipv6' : 'ipv4', table) }, [
22 E('h3', {}, title),
23 E('div')
24 ]);
25
26 if (idiv.firstElementChild.nodeName.toLowerCase() === 'p')
27 idiv.removeChild(idiv.firstElementChild);
28
29 var added = false, thisIdx = table_names.indexOf(table);
30
31 idiv.querySelectorAll('[data-table]').forEach(function(child) {
32 var childIdx = table_names.indexOf(child.getAttribute('data-table').split(/-/)[1]);
33
34 if (added === false && childIdx > thisIdx) {
35 idiv.insertBefore(tdiv, child);
36 added = true;
37 }
38 });
39
40 if (added === false)
41 idiv.appendChild(tdiv);
42 }
43
44 return tdiv.lastElementChild;
45 },
46
47 createChainSection: function(is_ipv6, table, chain, policy, packets, bytes, references) {
48 var tdiv = this.createTableSection(is_ipv6, table),
49 cdiv = tdiv.querySelector('[data-chain="%s"]'.format(chain)),
50 title;
51
52 if (policy)
53 title = '%s <em>%s</em> <span>(%s: <em>%s</em>, %d %s, %.2mB %s)</span>'
54 .format(_('Chain'), chain, _('Policy'), policy, packets, _('Packets'), bytes, _('Traffic'));
55 else
56 title = '%s <em>%s</em> <span class="references">(%d %s)</span>'
57 .format(_('Chain'), chain, references, _('References'));
58
59 if (!cdiv) {
60 cdiv = E('div', { 'data-chain': chain }, [
61 E('h4', { 'id': 'rule_%s-%s_%s'.format(is_ipv6 ? 'ipv6' : 'ipv4', table.toLowerCase(), chain) }, title),
62 E('table', { 'class': 'table' }, [
63 E('tr', { 'class': 'tr table-titles' }, [
64 E('th', { 'class': 'th' }, _('Pkts.')),
65 E('th', { 'class': 'th' }, _('Traffic')),
66 E('th', { 'class': 'th' }, _('Target')),
67 E('th', { 'class': 'th' }, _('Prot.')),
68 E('th', { 'class': 'th' }, _('In')),
69 E('th', { 'class': 'th' }, _('Out')),
70 E('th', { 'class': 'th' }, _('Source')),
71 E('th', { 'class': 'th' }, _('Destination')),
72 E('th', { 'class': 'th' }, _('Options')),
73 E('th', { 'class': 'th' }, _('Comment'))
74 ])
75 ])
76 ]);
77
78 tdiv.appendChild(cdiv);
79 }
80 else {
81 cdiv.firstElementChild.innerHTML = title;
82 }
83
84 return cdiv.lastElementChild;
85 },
86
87 updateChainSection: function(chaintable, rows) {
88 if (!chaintable)
89 return;
90
91 cbi_update_table(chaintable, rows, _('No rules in this chain.'));
92
93 if (rows.length === 0 &&
94 document.querySelector('[data-hide-empty="true"]'))
95 chaintable.parentNode.style.display = 'none';
96 else
97 chaintable.parentNode.style.display = '';
98
99 chaintable.parentNode.setAttribute('data-empty', rows.length === 0);
100 },
101
102 parseIptablesDump: function(is_ipv6, table, s) {
103 var current_chain = null;
104 var current_rules = [];
105 var seen_chains = {};
106 var chain_refs = {};
107 var re = /([^\n]*)\n/g;
108 var m, m2;
109 var raw = document.querySelector('[data-raw-counters="true"]');
110
111 while ((m = re.exec(s)) != null) {
112 if (m[1].match(/^Chain (.+) \(policy (\w+) (\d+) packets, (\d+) bytes\)$/)) {
113 var chain = RegExp.$1,
114 policy = RegExp.$2,
115 packets = +RegExp.$3,
116 bytes = +RegExp.$4;
117
118 this.updateChainSection(current_chain, current_rules);
119
120 seen_chains[chain] = true;
121 current_chain = this.createChainSection(is_ipv6, table, chain, policy, packets, bytes);
122 current_rules = [];
123 }
124 else if (m[1].match(/^Chain (.+) \((\d+) references\)$/)) {
125 var chain = RegExp.$1,
126 references = +RegExp.$2;
127
128 this.updateChainSection(current_chain, current_rules);
129
130 seen_chains[chain] = true;
131 current_chain = this.createChainSection(is_ipv6, table, chain, null, null, null, references);
132 current_rules = [];
133 }
134 else if (m[1].match(/^num /)) {
135 continue;
136 }
137 else if ((m2 = m[1].match(/^(\d+) +(\d+) +(\d+) +(.*?) +(\S+) +(\S*) +(\S+) +(\S+) +(!?[a-f0-9:.]+(?:\/[a-f0-9:.]+)?) +(!?[a-f0-9:.]+(?:\/[a-f0-9:.]+)?) +(.+)$/)) !== null) {
138 var num = +m2[1],
139 pkts = +m2[2],
140 bytes = +m2[3],
141 target = m2[4],
142 proto = m2[5],
143 indev = m2[7],
144 outdev = m2[8],
145 srcnet = m2[9],
146 dstnet = m2[10],
147 options = m2[11] || '-',
148 comment = '-';
149
150 options = options.trim().replace(/(?:^| )\/\* (.+) \*\//,
151 function(m1, m2) {
152 comment = m2.replace(/^!fw3(: |$)/, '').trim() || '-';
153 return '';
154 }) || '-';
155
156 current_rules.push([
157 E('div', {
158 'class': 'nowrap',
159 'style': raw ? raw_style : null,
160 'data-format': '%.2m',
161 'data-value': pkts
162 }, (raw ? '%d' : '%.2m').format(pkts)),
163 E('div', {
164 'class': 'nowrap',
165 'style': raw ? raw_style : null,
166 'data-format': '%.2mB',
167 'data-value': bytes
168 }, (raw ? '%d' : '%.2mB').format(bytes)),
169 target ? '<span class="target">%s</span>'.format(target) : '-',
170 proto,
171 (indev !== '*') ? '<span class="ifacebadge nowrap">%s</span>'.format(indev) : '*',
172 (outdev !== '*') ? '<span class="ifacebadge nowrap">%s</span>'.format(outdev) : '*',
173 srcnet,
174 dstnet,
175 options,
176 [ comment ]
177 ]);
178
179 if (target) {
180 chain_refs[target] = chain_refs[target] || [];
181 chain_refs[target].push([ current_chain, num ]);
182 }
183 }
184 }
185
186 this.updateChainSection(current_chain, current_rules);
187
188 document.querySelectorAll('[data-table="%s-%s"] [data-chain]'.format(is_ipv6 ? 'ipv6' : 'ipv4', table)).forEach(L.bind(function(cdiv) {
189 if (!seen_chains[cdiv.getAttribute('data-chain')]) {
190 cdiv.parentNode.removeChild(cdiv);
191 return;
192 }
193
194 cdiv.querySelectorAll('.target').forEach(L.bind(function(tspan) {
195 if (seen_chains[tspan.textContent]) {
196 tspan.classList.add('jump');
197 tspan.addEventListener('click', this.handleJumpTarget);
198 }
199 }, this));
200
201 cdiv.querySelectorAll('.references').forEach(L.bind(function(rspan) {
202 var refs = chain_refs[cdiv.getAttribute('data-chain')];
203 if (refs && refs.length) {
204 rspan.classList.add('cbi-tooltip-container');
205 rspan.appendChild(E('small', { 'class': 'cbi-tooltip ifacebadge', 'style': 'top:1em; left:auto' }, [ E('ul') ]));
206
207 refs.forEach(L.bind(function(ref) {
208 var chain = ref[0].parentNode.getAttribute('data-chain'),
209 num = ref[1];
210
211 rspan.lastElementChild.lastElementChild.appendChild(E('li', {}, [
212 _('Chain'), ' ',
213 E('span', {
214 'class': 'jump',
215 'data-num': num,
216 'click': this.handleJumpTarget
217 }, chain),
218 ', %s #%d'.format(_('Rule'), num)
219 ]));
220 }, this));
221 }
222 }, this));
223 }, this));
224 },
225
226 pollFirewallLists: function(has_ip6tables) {
227 var cmds = [ '/usr/sbin/iptables' ];
228
229 if (has_ip6tables)
230 cmds.push('/usr/sbin/ip6tables');
231
232 poll.add(L.bind(function() {
233 var tasks = [];
234
235 for (var i = 0; i < cmds.length; i++) {
236 for (var j = 0; j < table_names.length; j++) {
237 tasks.push(L.resolveDefault(
238 fs.exec_direct(cmds[i], [ '--line-numbers', '-w', '-nvxL', '-t', table_names[j].toLowerCase() ])
239 .then(this.parseIptablesDump.bind(this, i > 0, table_names[j]))));
240 }
241 }
242
243 return Promise.all(tasks);
244 }, this));
245 },
246
247 handleJumpTarget: function(ev) {
248 var link = ev.target,
249 table = findParent(link, '[data-table]').getAttribute('data-table'),
250 chain = link.textContent,
251 num = +link.getAttribute('data-num'),
252 elem = document.getElementById('rule_%s_%s'.format(table.toLowerCase(), chain));
253
254 if (elem) {
255 (document.documentElement || document.body.parentNode || document.body).scrollTop = elem.offsetTop - 40;
256 elem.classList.remove('flash');
257 void elem.offsetWidth;
258 elem.classList.add('flash');
259
260 if (num) {
261 var rule = elem.nextElementSibling.childNodes[num];
262 if (rule) {
263 rule.classList.remove('flash');
264 void rule.offsetWidth;
265 rule.classList.add('flash');
266 }
267 }
268 }
269 },
270
271 handleRawCounters: function(ev) {
272 var btn = ev.currentTarget,
273 raw = (btn.getAttribute('data-raw-counters') === 'false');
274
275 btn.setAttribute('data-raw-counters', raw);
276 btn.firstChild.data = raw ? _('Human-readable counters') : _('Show raw counters');
277 btn.blur();
278
279 document.querySelectorAll('[data-value]')
280 .forEach(function(div) {
281 var fmt = raw ? '%d' : div.getAttribute('data-format');
282
283 div.style = raw ? raw_style : '';
284 div.innerText = fmt.format(div.getAttribute('data-value'));
285 });
286 },
287
288 handleHideEmpty: function(ev) {
289 var btn = ev.currentTarget,
290 hide = (btn.getAttribute('data-hide-empty') === 'false');
291
292 btn.setAttribute('data-hide-empty', hide);
293 btn.firstChild.data = hide ? _('Show empty chains') : _('Hide empty chains');
294 btn.blur();
295
296 document.querySelectorAll('[data-chain][data-empty="true"]')
297 .forEach(function(chaintable) {
298 chaintable.style.display = hide ? 'none' : '';
299 });
300 },
301
302 handleCounterReset: function(has_ip6tables, ev) {
303 return Promise.all([
304 fs.exec('/usr/sbin/iptables', [ '-Z' ])
305 .catch(function(err) { ui.addNotification(null, E('p', {}, _('Unable to reset iptables counters: %s').format(err.message))) }),
306 has_ip6tables ? fs.exec('/usr/sbin/ip6tables', [ '-Z' ])
307 .catch(function(err) { ui.addNotification(null, E('p', {}, _('Unable to reset ip6tables counters: %s').format(err.message))) }) : null
308 ]);
309 },
310
311 handleRestart: function(ev) {
312 return fs.exec_direct('/etc/init.d/firewall', [ 'restart' ])
313 .catch(function(err) { ui.addNotification(null, E('p', {}, _('Unable to restart firewall: %s').format(err.message))) });
314 },
315
316 render: function(has_ip6tables) {
317 var view = E([], [
318 E('style', { 'type': 'text/css' }, [
319 '.cbi-tooltip-container, span.jump { border-bottom:1px dotted #00f;cursor:pointer }',
320 'ul { list-style:none }',
321 '.references { position:relative }',
322 '.references .cbi-tooltip { left:0!important;top:1.5em!important }',
323 'h4>span { font-size:90% }'
324 ]),
325
326 E('h2', {}, [ _('Firewall Status') ]),
327 E('div', { 'class': 'right', 'style': 'margin-bottom:-1.5em' }, [
328 E('button', {
329 'class': 'cbi-button',
330 'data-hide-empty': false,
331 'click': ui.createHandlerFn(this, 'handleHideEmpty')
332 }, [ _('Hide empty chains') ]),
333 ' ',
334 E('button', {
335 'data-raw-counters': false,
336 'click': ui.createHandlerFn(this, 'handleRawCounters')
337 }, [ _('Show raw counters') ]),
338 ' ',
339 E('button', {
340 'class': 'cbi-button',
341 'click': ui.createHandlerFn(this, 'handleCounterReset', has_ip6tables)
342 }, [ _('Reset Counters') ]),
343 ' ',
344 E('button', {
345 'class': 'cbi-button',
346 'click': ui.createHandlerFn(this, 'handleRestart')
347 }, [ _('Restart Firewall') ])
348 ]),
349 E('div', {}, [
350 E('div', { 'data-tab': 'iptables', 'data-tab-title': has_ip6tables ? _('IPv4 Firewall') : null }, [
351 E('p', {}, E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]))
352 ]),
353 has_ip6tables ? E('div', { 'data-tab': 'ip6tables', 'data-tab-title': _('IPv6 Firewall') }, [
354 E('p', {}, E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]))
355 ]) : E([])
356 ])
357 ]);
358
359 if (has_ip6tables)
360 ui.tabs.initTabGroup(view.lastElementChild.childNodes);
361
362 this.pollFirewallLists(has_ip6tables);
363
364 return view;
365 },
366
367 handleSaveApply: null,
368 handleSave: null,
369 handleReset: null
370 });