dark mode and websockets
[kismet-logviewer.git] / logviewer / static / js / kismet.utils.js
1 /* jshint browser: true */
2 /* global define, module */
3 ( // Module boilerplate to support browser globals and browserify and AMD.
4   typeof define === "function" ? function (m) { define("kismet-js", m); } :
5   typeof exports === "object" ? function (m) { module.exports = m(); } :
6   function(m){ this.kismet = m(); }
7 )(function () {
8 "use strict";
9
10 var exports = {};
11
12 var local_uri_prefix = ""; 
13 if (typeof(KISMET_URI_PREFIX) !== 'undefined')
14     local_uri_prefix = KISMET_URI_PREFIX;
15
16 exports.timestamp_sec = 0;
17 exports.timestamp_usec = 0;
18
19 // Make a universal HTML5 storage handler
20 exports.storage = Storages.localStorage;
21
22 // Simple handler for getting stored values with defaults
23 exports.getStorage = function(key, def = undefined) {
24     if (exports.storage.isSet(key))
25         return exports.storage.get(key);
26
27     return def;
28 }
29
30 exports.putStorage = function(key, data) {
31     exports.storage.set(key, data);
32 }
33
34 // From http://stackoverflow.com/a/6491621
35 exports.ObjectByString = function(o, s) {
36     if (typeof(o) === 'undefined')
37         return;
38
39     s = s.replace(/\[('?"?-?[\w:]+'?"?)\]/g, '\/$1');
40     s = s.replace(/^\//, '');
41     s = s.replace(/\/$/, '');
42     s = s.replace(/\/+/, '\/');
43     var a = s.split('/');
44     for (var i = 0, n = a.length; i < n; ++i) {
45         var k = a[i];
46
47         if (typeof(o) !== 'object')
48             return;
49
50         if (k in o) {
51             o = o[k];
52         } else {
53             return;
54         }
55     }
56
57     return o;
58 }
59
60 exports.HumanReadableSize = function(sz) {
61     if (typeof(sz) === 'undefined')
62         return '0 B';
63
64     if (typeof(sz) !== 'number') 
65         sz = parseInt(sz);
66
67     if (sz < 1024) {
68         return sz + " B";
69     } else if (sz < 1024 * 1024) {
70         return (sz / 1024).toFixed(2) + " KB";
71     } else if (sz < 1024 * 1024 * 1024) {
72         return (sz / 1024 / 1024).toFixed(2) + " MB";
73     } else if (sz < 1024 * 1024 * 1024 * 1024) {
74         return (sz / 1024 / 1024 / 1024).toFixed(2) + " GB";
75     }
76
77     return sz;
78 }
79
80 exports.HumanReadableFrequency = function(f) {
81     // Kismet reports in *kHz* so all these values are scaled down by an order
82     // of magnitude
83     if (f < 1000)
84         return f + " KHz";
85     else if (f < 1000 * 1000)
86         return (f / 1000).toFixed(3) + " MHz";
87     else 
88         return (f / 1000 / 1000).toFixed(3) + " GHz";
89 }
90
91 // Modify a RRD minute record by fast-forwarding it to 'now', and optionally
92 // applying a transform function which could do something like average it
93
94 // Conversion factors / type definitions for RRD data arrays
95 exports.RRD_SECOND = 1; 
96 exports.RRD_MINUTE = 60;
97 exports.RRD_HOUR = 3600;
98
99 // exports.RRD_DAY = 86400
100
101 exports.RecalcRrdData = function(start, now, type, data, opt = {}) {
102     if (data == undefined) {
103         if (type == exports.RRD_SECOND || type == exports.RRD_MINUTE) {
104             data = [
105                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
106                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
107                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
108                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
109                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
110                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
111                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 
112             ];
113         } else if (type == exports.RRD_HOUR) {
114             data = [
115                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
116                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
117                 0, 0, 0, 0
118             ];
119         }
120     }
121
122     var rrd_len = data.length;
123
124     // Each type value is the number of seconds in each bin of the array
125     //
126     // A bin for a given time is (time / type) % len
127     //
128     // A completely expired RRD is (now - start) > (type * len) and should
129     // be filled with only zeroes
130     //
131     // To zero the array between "then" and "now", we simply calculate
132     // the bin for "then", the bin for "now", and increment-with-modulo 
133     // until we reach "now".
134
135     // Adjusted data we return
136     var adj_data = new Array();
137
138     // Check if we're past the recording bounds of the rrd for this type, if we
139     // are, we don't have to do any shifting or any manipulation, we just fill
140     // the array with zeroes.
141     if ((now - start) > (type * rrd_len)) {
142         for (var ri = 0; ri < rrd_len; ri++) {
143             adj_data.push(0);
144         }
145     } else {
146         // Otherwise, we're valid inside the range of the array.  We know we got
147         // no data between the time of the RRD and now, because if we had, the 
148         // time would be more current.  Figure out how many bins lie between
149         // 'then' and 'now', rescale the array to start at 'now', and fill
150         // in the time we got no data with zeroes
151         
152         var start_bin = (Math.floor(start / type) % rrd_len) + 1;
153         var now_bin = (Math.floor(now / type) % rrd_len) + 1;
154         var sec_offt = Math.max(0, now - start);
155
156         /*
157         console.log("we think we start in bin" + start_bin);
158         console.log("we think now is bin" + now_bin);
159         */
160
161         // Walk the entire array, starting with 'now', and copy zeroes
162         // when we fall into the blank spot between 'start' and 'now' when we
163         // know we received no data
164         for (var ri = 0; ri < rrd_len; ri++) {
165             var slot = (now_bin + ri) % rrd_len;
166
167             if (slot >= start_bin && slot < now_bin)
168                 adj_data.push(0);
169             else
170                 adj_data.push(data[slot]);
171         }
172     }
173
174     // If we have a transform function in the options, call it, otherwise
175     // return the shifted RRD entry
176     if ('transform' in opt && typeof(opt.transform) === 'function') {
177         var cbopt = {};
178
179         if ('transformopt' in opt)
180             cbopt = opt.cbopt;
181
182         return opt.transform(adj_data, cbopt);
183     }
184
185     return adj_data;
186 }
187
188 exports.RecalcRrdData2 = function(rrddata, type, opt = {}) {
189     var record;
190
191     if (type == exports.RRD_SECOND)
192         record = "kismet.common.rrd.minute_vec";
193     else if (type == exports.RRD_MINUTE)
194         record = "kismet.common.rrd.hour_vec";
195     else if (type == exports.RRD_HOUR)
196         record = "kismet.common.rrd.day_vec";
197     else
198         record = "kismet.common.rrd.minute_vec";
199
200     var data = [];
201     var rrd_len;
202     var now;
203     var start;
204
205     try {
206         data = rrddata[record];
207
208         if (typeof(data) === 'number')
209             throw(0);
210
211         now = rrddata['kismet.common.rrd.serial_time'];
212         start = rrddata['kismet.common.rrd.last_time'];
213     } catch (e) {
214         now = 0;
215         start = 0;
216
217         if (type == exports.RRD_SECOND || type == exports.RRD_MINUTE) {
218             data = [
219                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
220                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
221                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
222                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
223                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
224                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
225                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 
226             ];
227         } else if (type == exports.RRD_HOUR) {
228             data = [
229                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
230                 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 
231                 0, 0, 0, 0
232             ];
233         }
234     }
235
236     rrd_len = data.length;
237
238     // Each type value is the number of seconds in each bin of the array
239     //
240     // A bin for a given time is (time / type) % len
241     //
242     // A completely expired RRD is (now - start) > (type * len) and should
243     // be filled with only zeroes
244     //
245     // To zero the array between "then" and "now", we simply calculate
246     // the bin for "then", the bin for "now", and increment-with-modulo 
247     // until we reach "now".
248
249     // Adjusted data we return
250     var adj_data = new Array();
251
252     // Check if we're past the recording bounds of the rrd for this type, if we
253     // are, we don't have to do any shifting or any manipulation, we just fill
254     // the array with zeroes.
255     if ((now - start) > (type * rrd_len)) {
256         for (var ri = 0; ri < rrd_len; ri++) {
257             adj_data.push(0);
258         }
259     } else {
260         // Otherwise, we're valid inside the range of the array.  We know we got
261         // no data between the time of the RRD and now, because if we had, the 
262         // time would be more current.  Figure out how many bins lie between
263         // 'then' and 'now', rescale the array to start at 'now', and fill
264         // in the time we got no data with zeroes
265         
266         var start_bin = (Math.floor(start / type) % rrd_len) + 1;
267         var now_bin = (Math.floor(now / type) % rrd_len) + 1;
268         var sec_offt = Math.max(0, now - start);
269
270         /*
271         console.log("we think we start in bin" + start_bin);
272         console.log("we think now is bin" + now_bin);
273         */
274
275         // Walk the entire array, starting with 'now', and copy zeroes
276         // when we fall into the blank spot between 'start' and 'now' when we
277         // know we received no data
278         for (var ri = 0; ri < rrd_len; ri++) {
279             var slot = (now_bin + ri) % rrd_len;
280
281             if (slot >= start_bin && slot < now_bin)
282                 adj_data.push(0);
283             else
284                 adj_data.push(data[slot]);
285         }
286     }
287
288     // If we have a transform function in the options, call it, otherwise
289     // return the shifted RRD entry
290     if ('transform' in opt && typeof(opt.transform) === 'function') {
291         var cbopt = {};
292
293         if ('transformopt' in opt)
294             cbopt = opt.cbopt;
295
296         return opt.transform(adj_data, cbopt);
297     }
298
299     return adj_data;
300 }
301
302 exports.sanitizeId = function(s) {
303     return String(s).replace(/[:.&<>"'`=\/\(\)\[\] ]/g, function (s) {
304             return '_';
305     });
306 }
307
308 exports.sanitizeHTML = function(s) {
309     var remap = {
310         '&': '&amp;',
311         '<': '&lt;',
312         '>': '&gt;',
313         '"': '&quot;',
314         "'": '&#39;',
315         '`': '&#x60;',
316         '=': '&#x3D;',
317         '/': '&#x2F;'
318     };
319
320     return String(s).replace(/[&<>"'`=\/]/g, function (s) {
321             return remap[s];
322     });
323 }
324
325 /* Censor a mac-like string, if the global censor_macs option is turned on; must be called by each
326  * display component */
327 exports.censorMAC = function(t) {
328     try {
329         if (window['censor_macs'])
330             return t.replace(/([a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}):[a-fA-F0-9]{2}:[a-fA-F0-9]{2}:[a-fA-F0-9]{2}/g, "$1:XX:XX:XX");
331         else
332             return t;
333     } catch (e) {
334         return t;
335     }
336 }
337
338 /* Censor a location by rounding */
339 exports.censorLocation = function(t) {
340     try {
341         if (window['censor_location']) 
342             return `${Math.round(t)}.XXXXX`;
343         else
344             return t;
345     } catch (e) {
346         return t;
347     }
348 }
349
350 /* Censor a string by obscuring most of the contents */
351 exports.censorString = function(t) { 
352     try { 
353         if (window['censor_macs']) { 
354             if (t.length < 6) { 
355                 return new Array(t.length + 1).join('X');
356             } else { 
357                 return t.substring(0, 2) + (new Array(t.length - 3).join('X')) + t.substring(t.length - 2, t.length);
358             }
359         } else { 
360             return t;
361         }
362     } catch (e) { 
363         return t;
364     }
365 }
366
367 /* Recurse over a complete object (such as from json), finding all strings,
368  * and escaping them to be 'safe' */
369 exports.sanitizeObject = function(o) {
370     if (o === null) {
371         return o;
372     }
373
374     if (typeof(o) === 'string') {
375         return exports.sanitizeHTML(o);
376     }
377
378     Object.keys(o).forEach(function(key) {
379             o[key] = exports.sanitizeObject(o[key]);
380     });
381
382     return o;
383 }
384
385 String.prototype.escapeSpecialChars = function() {
386     var s = this.replace(/\n/g, "\\n")
387         .replace(/\r/g, "\\r")
388         .replace(/\t/g, "\\t");
389
390     return s;
391 };
392
393 String.prototype.convertNewlines = function() {
394     var s = this.replace(/\\n/g, "\n");
395     s = s.replace(/\\r/g, "");
396
397     return s;
398 }
399
400 String.prototype.MiddleShorten = function(len) { 
401     if (this.length > len) {
402         let epos = len / 2;
403         let lpos = this.length - (len / 2);
404
405         while (epos > 1 && this.substr(epos - 1, 1) == ' ') { 
406             epos = epos - 1;
407         }
408
409         while (lpos < len && this.substr(lpos, 1) == ' ') { 
410             lpos = lpos + 1;
411         }
412
413         return this.substr(0,epos) + '...' + this.substr(lpos, this.length);
414     }
415
416     return this;
417 }
418
419 exports.ExtractDeviceName = function(device) { 
420     var ret = device['kismet.device.base.username'];
421     if (ret != null && ret != '') { 
422         return exports.censorString(ret);
423     }
424
425     ret = device['kismet.device.base.name'];
426     if (ret != null && ret != '') { 
427         return exports.censorString(ret);
428     }
429
430     ret = device['kismet.device.base.commonname'];
431     if (ret != null && ret != '') { 
432         return exports.censorString(ret);
433     }
434
435     ret = device['kismet.device.base.macaddr'];
436     return exports.censorMAC(ret);
437 }
438
439 return exports;
440
441 });