6e588fd0b574f9109f1e19e3dff2266f3b5dbab6
[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 // utf8 to unicode converter (used below in deoctalize()).
351 // "fatal: true" means that the converter will throw an 
352 // exception if the input is not valid utf8.
353 exports.decoder = new TextDecoder('utf8', {fatal: true});
354
355 /* De-octalize an escaped string, and decode it from utf8.
356  * If the input string contains anything unexpected (control
357  * characters, invalid values after the backslash, character
358  * sequences that are not valid utf8), return the input string.
359  * */
360 exports.deoctalize = function(str) {
361     var ret = new Array();
362
363     for (var i = 0; i < str.length; i++) {
364         // If the current character is not a backslash, 
365         // do not modify it.
366         if (str[i] != '\\') {
367             ret.push(str.charCodeAt(i))
368         // If the current character (a backslash) is followed by a 
369         // second backslash, remove the second backslash;
370         // no other modification needed.
371         } else if (i+1 < str.length && str[i+1] == '\\') {
372             ret.push(str.charCodeAt(i));
373             i++;
374         // If the backslash is followed by a 3-digit octal number
375         // in the range 000 to 377, replace the backslash and
376         // numerals by the corresponding octal character
377         } else if (i + 3 < str.length && str[i + 1] >= '0' && str[i+1] <= '3' &&
378                 str[i + 2] >= '0' && str[i+2] <= '7' &&
379                 str[i + 3] >= '0' && str[i+3] <= '7') {
380
381                 var sum = 
382                     ((str[i + 1] - '0') * 64) +
383                     ((str[i + 2] - '0') * 8) +
384                     ((str[i + 3] - '0'));
385
386                 // If the octal character is less than 32 decimal,
387                 // then it is a control (non-printing) character.
388                 // In this case, dont' de-octalize the input string;
389                 // immediately return the entire input string.
390                 if (sum < 32) {
391                     return str;
392                 } else {
393                     ret.push(sum);
394                 }
395
396                 i += 3;
397         // This clause is reached only if a backslash was encountered,
398         // but the backslash was not followed by either another
399         // backslash or by 3 valid octal digits.  This means that
400         // the input string is not a valid octalized string, so we
401         // don't know how to de-octalize it.  In this case, return
402         // the input string.
403         } else {
404             return str;
405         }
406     }
407
408     try {
409         // Try to convert the de-octalized string from utf8 to
410         // unicode.
411         return exports.decoder.decode(Uint8Array.from(ret))
412     } catch(e) {
413         // The de-octalized string was not valid utf8, so we don't
414         // know how to convert it.  In this case, return the input
415         // string.
416         return str;
417     }
418 }
419
420
421 /* Recurse over a complete object (such as from json), finding all strings,
422  * and escaping them to be 'safe' */
423 exports.sanitizeObject = function(o) {
424     if (o === null) {
425         return o;
426     }
427
428     if (typeof(o) === 'string') {
429         return exports.sanitizeHTML(exports.deoctalize(o));
430     }
431
432     Object.keys(o).forEach(function(key) {
433             o[key] = exports.sanitizeObject(o[key]);
434     });
435
436     return o;
437 }
438
439 String.prototype.escapeSpecialChars = function() {
440     var s = this.replace(/\n/g, "\\n")
441         .replace(/\r/g, "\\r")
442         .replace(/\t/g, "\\t");
443
444     return s;
445 };
446
447 String.prototype.convertNewlines = function() {
448     var s = this.replace(/\\n/g, "\n");
449     s = s.replace(/\\r/g, "");
450
451     return s;
452 }
453
454 return exports;
455
456 });