treewide: import utility classes explicitly
[project/luci.git] / modules / luci-mod-status / htdocs / luci-static / resources / view / status / connections.js
1 'use strict';
2 'require view';
3 'require poll';
4 'require request';
5 'require rpc';
6
7 var callLuciRealtimeStats = rpc.declare({
8 object: 'luci',
9 method: 'getRealtimeStats',
10 params: [ 'mode', 'device' ],
11 expect: { result: [] }
12 });
13
14 var callLuciConntrackList = rpc.declare({
15 object: 'luci',
16 method: 'getConntrackList',
17 expect: { result: [] }
18 });
19
20 var callNetworkRrdnsLookup = rpc.declare({
21 object: 'network.rrdns',
22 method: 'lookup',
23 params: [ 'addrs', 'timeout', 'limit' ],
24 expect: { '': {} }
25 });
26
27 var graphPolls = [],
28 pollInterval = 3,
29 dns_cache = {},
30 enableLookups = false;
31
32 var recheck_lookup_queue = {};
33
34 Math.log2 = Math.log2 || function(x) { return Math.log(x) * Math.LOG2E; };
35
36 return view.extend({
37 load: function() {
38 return Promise.all([
39 this.loadSVG(L.resource('connections.svg'))
40 ]);
41 },
42
43 updateGraph: function(svg, lines, cb) {
44 var G = svg.firstElementChild;
45
46 var view = document.querySelector('#view');
47
48 var width = view.offsetWidth - 2;
49 var height = 300 - 2;
50 var step = 5;
51
52 var data_wanted = Math.floor(width / step);
53
54 var data_values = [],
55 line_elements = [];
56
57 for (var i = 0; i < lines.length; i++)
58 if (lines[i] != null)
59 data_values.push([]);
60
61 var info = {
62 line_current: [],
63 line_average: [],
64 line_peak: []
65 };
66
67 /* prefill datasets */
68 for (var i = 0; i < data_values.length; i++)
69 for (var j = 0; j < data_wanted; j++)
70 data_values[i][j] = 0;
71
72 /* plot horizontal time interval lines */
73 for (var i = width % (step * 60); i < width; i += step * 60) {
74 var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
75 line.setAttribute('x1', i);
76 line.setAttribute('y1', 0);
77 line.setAttribute('x2', i);
78 line.setAttribute('y2', '100%');
79 line.setAttribute('style', 'stroke:black;stroke-width:0.1');
80
81 var text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
82 text.setAttribute('x', i + 5);
83 text.setAttribute('y', 15);
84 text.setAttribute('style', 'fill:#eee; font-size:9pt; font-family:sans-serif; text-shadow:1px 1px 1px #000');
85 text.appendChild(document.createTextNode(Math.round((width - i) / step / 60) + 'm'));
86
87 G.appendChild(line);
88 G.appendChild(text);
89 }
90
91 info.interval = pollInterval;
92 info.timeframe = data_wanted / 60;
93
94 graphPolls.push({
95 svg: svg,
96 lines: lines,
97 cb: cb,
98 info: info,
99 width: width,
100 height: height,
101 step: step,
102 values: data_values,
103 timestamp: 0,
104 fill: 1
105 });
106 },
107
108 updateConntrack: function(conn) {
109 var lookup_queue = [ ];
110 var rows = [];
111
112 conn.sort(function(a, b) {
113 return b.bytes - a.bytes;
114 });
115
116 for (var i = 0; i < conn.length; i++)
117 {
118 var c = conn[i];
119
120 if ((c.src == '127.0.0.1' && c.dst == '127.0.0.1') ||
121 (c.src == '::1' && c.dst == '::1'))
122 continue;
123
124 if (!dns_cache[c.src] && lookup_queue.indexOf(c.src) == -1)
125 lookup_queue.push(c.src);
126
127 if (!dns_cache[c.dst] && lookup_queue.indexOf(c.dst) == -1)
128 lookup_queue.push(c.dst);
129
130 var src = dns_cache[c.src] || (c.layer3 == 'ipv6' ? '[' + c.src + ']' : c.src);
131 var dst = dns_cache[c.dst] || (c.layer3 == 'ipv6' ? '[' + c.dst + ']' : c.dst);
132
133 rows.push([
134 c.layer3.toUpperCase(),
135 c.layer4.toUpperCase(),
136 c.hasOwnProperty('sport') ? (src + ':' + c.sport) : src,
137 c.hasOwnProperty('dport') ? (dst + ':' + c.dport) : dst,
138 '%1024.2mB (%d %s)'.format(c.bytes, c.packets, _('Pkts.'))
139 ]);
140 }
141
142 cbi_update_table('#connections', rows, E('em', _('No information available')));
143
144 if (enableLookups && lookup_queue.length > 0) {
145 var reduced_lookup_queue = lookup_queue;
146
147 if (lookup_queue.length > 100)
148 reduced_lookup_queue = lookup_queue.slice(0, 100);
149
150 callNetworkRrdnsLookup(reduced_lookup_queue, 5000, 1000).then(function(replies) {
151 for (var index in reduced_lookup_queue) {
152 var address = reduced_lookup_queue[index];
153
154 if (!address)
155 continue;
156
157 if (replies[address]) {
158 dns_cache[address] = replies[address];
159 lookup_queue.splice(reduced_lookup_queue.indexOf(address), 1);
160 continue;
161 }
162
163 if (recheck_lookup_queue[address] > 2) {
164 dns_cache[address] = (address.match(/:/)) ? '[' + address + ']' : address;
165 lookup_queue.splice(index, 1);
166 }
167 else {
168 recheck_lookup_queue[address] = (recheck_lookup_queue[address] || 0) + 1;
169 }
170 }
171
172 var btn = document.querySelector('.btn.toggle-lookups');
173 if (btn) {
174 btn.firstChild.data = enableLookups ? _('Disable DNS lookups') : _('Enable DNS lookups');
175 btn.classList.remove('spinning');
176 btn.disabled = false;
177 }
178 });
179 }
180 },
181
182 pollData: function() {
183 poll.add(L.bind(function() {
184 var tasks = [
185 L.resolveDefault(callLuciConntrackList(), [])
186 ];
187
188 for (var i = 0; i < graphPolls.length; i++) {
189 var ctx = graphPolls[i];
190 tasks.push(L.resolveDefault(callLuciRealtimeStats('conntrack'), []));
191 }
192
193 return Promise.all(tasks).then(L.bind(function(datasets) {
194 this.updateConntrack(datasets[0]);
195
196 for (var gi = 0; gi < graphPolls.length; gi++) {
197 var ctx = graphPolls[gi],
198 data = datasets[gi + 1],
199 values = ctx.values,
200 lines = ctx.lines,
201 info = ctx.info;
202
203 var data_scale = 0;
204 var data_wanted = Math.floor(ctx.width / ctx.step);
205 var last_timestamp = NaN;
206
207 for (var i = 0, di = 0; di < lines.length; di++) {
208 if (lines[di] == null)
209 continue;
210
211 var multiply = (lines[di].multiply != null) ? lines[di].multiply : 1,
212 offset = (lines[di].offset != null) ? lines[di].offset : 0;
213
214 for (var j = ctx.timestamp ? 0 : 1; j < data.length; j++) {
215 /* skip overlapping entries */
216 if (data[j][0] <= ctx.timestamp)
217 continue;
218
219 if (i == 0) {
220 ctx.fill++;
221 last_timestamp = data[j][0];
222 }
223
224 info.line_current[i] = data[j][di + 1] * multiply;
225 info.line_current[i] -= Math.min(info.line_current[i], offset);
226 values[i].push(info.line_current[i]);
227 }
228
229 i++;
230 }
231
232 /* cut off outdated entries */
233 ctx.fill = Math.min(ctx.fill, data_wanted);
234
235 for (var i = 0; i < values.length; i++) {
236 var len = values[i].length;
237 values[i] = values[i].slice(len - data_wanted, len);
238
239 /* find peaks, averages */
240 info.line_peak[i] = NaN;
241 info.line_average[i] = 0;
242
243 for (var j = 0; j < values[i].length; j++) {
244 info.line_peak[i] = isNaN(info.line_peak[i]) ? values[i][j] : Math.max(info.line_peak[i], values[i][j]);
245 info.line_average[i] += values[i][j];
246 }
247
248 info.line_average[i] = info.line_average[i] / ctx.fill;
249 }
250
251 info.peak = Math.max.apply(Math, info.line_peak);
252
253 /* remember current timestamp, calculate horizontal scale */
254 if (!isNaN(last_timestamp))
255 ctx.timestamp = last_timestamp;
256
257 var size = Math.floor(Math.log2(info.peak)),
258 div = Math.pow(2, size - (size % 10)),
259 mult = info.peak / div,
260 mult = (mult < 5) ? 2 : ((mult < 50) ? 10 : ((mult < 500) ? 100 : 1000));
261
262 info.peak = info.peak + (mult * div) - (info.peak % (mult * div));
263
264 data_scale = ctx.height / info.peak;
265
266 /* plot data */
267 for (var i = 0, di = 0; di < lines.length; di++) {
268 if (lines[di] == null)
269 continue;
270
271 var el = ctx.svg.firstElementChild.getElementById(lines[di].line),
272 pt = '0,' + ctx.height,
273 y = 0;
274
275 if (!el)
276 continue;
277
278 for (var j = 0; j < values[i].length; j++) {
279 var x = j * ctx.step;
280
281 y = ctx.height - Math.floor(values[i][j] * data_scale);
282 //y -= Math.floor(y % (1 / data_scale));
283
284 pt += ' ' + x + ',' + y;
285 }
286
287 pt += ' ' + ctx.width + ',' + y + ' ' + ctx.width + ',' + ctx.height;
288
289 el.setAttribute('points', pt);
290
291 i++;
292 }
293
294 info.label_25 = 0.25 * info.peak;
295 info.label_50 = 0.50 * info.peak;
296 info.label_75 = 0.75 * info.peak;
297
298 if (typeof(ctx.cb) == 'function')
299 ctx.cb(ctx.svg, info);
300 }
301 }, this));
302 }, this), pollInterval);
303 },
304
305 loadSVG: function(src) {
306 return request.get(src).then(function(response) {
307 if (!response.ok)
308 throw new Error(response.statusText);
309
310 return E('div', {
311 'style': 'width:100%;height:300px;border:1px solid #000;background:#fff'
312 }, E(response.text()));
313 });
314 },
315
316 render: function(data) {
317 var svg = data[0];
318
319 var v = E([], [
320 svg,
321 E('div', { 'class': 'right' }, E('small', { 'id': 'scale' }, '-')),
322 E('br'),
323
324 E('div', { 'class': 'table', 'style': 'width:100%;table-layout:fixed' }, [
325 E('div', { 'class': 'tr' }, [
326 E('div', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid blue' }, [ _('UDP:') ])),
327 E('div', { 'class': 'td', 'id': 'lb_udp_cur' }, [ '0' ]),
328
329 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
330 E('div', { 'class': 'td', 'id': 'lb_udp_avg' }, [ '0' ]),
331
332 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
333 E('div', { 'class': 'td', 'id': 'lb_udp_peak' }, [ '0' ])
334 ]),
335 E('div', { 'class': 'tr' }, [
336 E('div', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid green' }, [ _('TCP:') ])),
337 E('div', { 'class': 'td', 'id': 'lb_tcp_cur' }, [ '0' ]),
338
339 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
340 E('div', { 'class': 'td', 'id': 'lb_tcp_avg' }, [ '0' ]),
341
342 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
343 E('div', { 'class': 'td', 'id': 'lb_tcp_peak' }, [ '0' ])
344 ]),
345 E('div', { 'class': 'tr' }, [
346 E('div', { 'class': 'td right top' }, E('strong', { 'style': 'border-bottom:2px solid red' }, [ _('Other:') ])),
347 E('div', { 'class': 'td', 'id': 'lb_otr_cur' }, [ '0' ]),
348
349 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Average:') ])),
350 E('div', { 'class': 'td', 'id': 'lb_otr_avg' }, [ '0' ]),
351
352 E('div', { 'class': 'td right top' }, E('strong', {}, [ _('Peak:') ])),
353 E('div', { 'class': 'td', 'id': 'lb_otr_peak' }, [ '0' ])
354 ])
355 ]),
356
357 E('div', { 'class': 'right' }, [
358 E('button', {
359 'class': 'btn toggle-lookups',
360 'click': function(ev) {
361 if (!enableLookups) {
362 ev.currentTarget.classList.add('spinning');
363 ev.currentTarget.disabled = true;
364 enableLookups = true;
365 }
366 else {
367 ev.currentTarget.firstChild.data = _('Enable DNS lookups');
368 enableLookups = false;
369 }
370
371 this.blur();
372 }
373 }, [ enableLookups ? _('Disable DNS lookups') : _('Enable DNS lookups') ])
374 ]),
375
376 E('br'),
377
378 E('div', { 'class': 'cbi-section-node' }, [
379 E('div', { 'class': 'table', 'id': 'connections' }, [
380 E('div', { 'class': 'tr table-titles' }, [
381 E('div', { 'class': 'th col-2 hide-xs' }, [ _('Network') ]),
382 E('div', { 'class': 'th col-2' }, [ _('Protocol') ]),
383 E('div', { 'class': 'th col-7' }, [ _('Source') ]),
384 E('div', { 'class': 'th col-7' }, [ _('Destination') ]),
385 E('div', { 'class': 'th col-4' }, [ _('Transfer') ])
386 ]),
387 E('div', { 'class': 'tr placeholder' }, [
388 E('div', { 'class': 'td' }, [
389 E('em', {}, [ _('Collecting data...') ])
390 ])
391 ])
392 ])
393 ])
394 ]);
395
396 this.updateGraph(svg, [ { line: 'udp' }, { line: 'tcp' }, { line: 'other' } ], function(svg, info) {
397 var G = svg.firstElementChild, tab = svg.parentNode;
398
399 G.getElementById('label_25').firstChild.data = '%d'.format(info.label_25);
400 G.getElementById('label_50').firstChild.data = '%d'.format(info.label_50);
401 G.getElementById('label_75').firstChild.data = '%d'.format(info.label_75);
402
403 tab.querySelector('#scale').firstChild.data = _('(%d minute window, %d second interval)').format(info.timeframe, info.interval);
404
405 tab.querySelector('#lb_udp_cur').firstChild.data = '%d'.format(info.line_current[0]);
406 tab.querySelector('#lb_udp_avg').firstChild.data = '%d'.format(info.line_average[0]);
407 tab.querySelector('#lb_udp_peak').firstChild.data = '%d'.format(info.line_peak[0]);
408
409 tab.querySelector('#lb_tcp_cur').firstChild.data = '%d'.format(info.line_current[1]);
410 tab.querySelector('#lb_tcp_avg').firstChild.data = '%d'.format(info.line_average[1]);
411 tab.querySelector('#lb_tcp_peak').firstChild.data = '%d'.format(info.line_peak[1]);
412
413 tab.querySelector('#lb_otr_cur').firstChild.data = '%d'.format(info.line_current[2]);
414 tab.querySelector('#lb_otr_avg').firstChild.data = '%d'.format(info.line_average[2]);
415 tab.querySelector('#lb_otr_peak').firstChild.data = '%d'.format(info.line_peak[2]);
416 });
417
418 this.pollData();
419
420 return v;
421 },
422
423 handleSaveApply: null,
424 handleSave: null,
425 handleReset: null
426 });