24eafff0f0720cdf539dc08d2104212076968a6a
[kismet-logviewer.git] / logviewer / static / js / kismet.ui.base.js
1 (
2   typeof define === "function" ? function (m) { define("kismet-ui-base-js", m); } :
3   typeof exports === "object" ? function (m) { module.exports = m(); } :
4   function(m){ this.kismet_ui_base = m(); }
5 )(function () {
6
7 "use strict";
8
9 var exports = {};
10
11 var local_uri_prefix = "";
12 if (typeof(KISMET_URI_PREFIX) !== 'undefined')
13     local_uri_prefix = KISMET_URI_PREFIX;
14
15 // Flag we're still loading
16 exports.load_complete = 0;
17
18 /* Fetch the system user */
19 $.get(local_uri_prefix + "system/user_status.json")
20 .done(function(data) {
21     exports.system_user = data['kismet.system.user'];
22 })
23 .fail(function() {
24     exports.system_user = "[unknown]";
25 });
26
27 // Load our css
28 $('<link>')
29     .appendTo('head')
30     .attr({
31         type: 'text/css',
32         rel: 'stylesheet',
33         href: local_uri_prefix + 'css/kismet.ui.base.css'
34     });
35
36 /* Call from index to set the required ajax parameters universally */
37 exports.ConfigureAjax = function() {
38     $.ajaxSetup({
39         beforeSend: function (xhr) {
40             var user = kismet.getStorage('kismet.base.login.username', 'kismet');
41             var pw =  kismet.getStorage('kismet.base.login.password', '');
42
43             xhr.setRequestHeader ("Authorization", "Basic " + btoa(user + ":" + pw));
44         },
45
46         /*
47         dataFilter: function(data, type) {
48             try {
49                 var json = JSON.parse(data);
50                 var sjson = kismet.sanitizeObject(json);
51                 console.log(JSON.stringify(sjson));
52                 return JSON.stringify(sjson);
53             } catch {
54                 return data;
55             }
56         },
57         */
58     });
59 }
60
61 exports.ConfigureAjax();
62
63 var eventbus_ws_listeners = [];
64
65 exports.eventbus_ws = null;
66
67 exports.SubscribeEventbus = function(topic, fields, callback) {
68     var sub = {
69         "topic": topic,
70         "callback": callback
71     }
72
73     if (fields.length > 0)
74         sub["fields"] = fields;
75
76     eventbus_ws_listeners.push(sub);
77
78     if (exports.eventbus_ws != null && exports.eventbus_ws.readyState == 1) {
79         var sub_req = {
80             "SUBSCRIBE": sub["topic"],
81         };
82
83         if ("fields" in sub)
84             sub_req["fields"] = sub["fields"]
85
86         exports.eventbus_ws.send(JSON.stringify(sub_req));
87     }
88 }
89
90 exports.OpenEventbusWs = function() {
91     var proto = "";
92
93     if (document.location.protocol == "https:")
94         proto = "wss"
95     else
96         proto = "ws"
97
98     var user = kismet.getStorage('kismet.base.login.username', 'kismet');
99     var pw =  kismet.getStorage('kismet.base.login.password', '');
100
101     var host = new URL(document.URL);
102
103     var ws_url = `${proto}://${host.host}/${KISMET_PROXY_PREFIX}eventbus/events.ws?user=${encodeURIComponent(user)}&password=${encodeURIComponent(pw)}`
104
105     exports.eventbus_ws = new WebSocket(ws_url);
106     
107     exports.eventbus_ws.onclose = function(event) {
108         console.log("eventbus ws closed");
109
110         setTimeout(function() { exports.OpenEventbusWs(); }, 500);
111     };
112
113     exports.eventbus_ws.onmessage = function(event) {
114         try {
115             var json = JSON.parse(event.data);
116
117             for (var x in json) {
118                 for (var sub of eventbus_ws_listeners) {
119                     if (sub["topic"] === x) {
120                         sub["callback"](json[x]);
121                     }
122                 }
123             }
124         } catch (e) {
125             console.log(e);
126         }
127     }
128
129     exports.eventbus_ws.onopen = function(event) {
130         for (var sub of eventbus_ws_listeners) {
131             var sub_req = {
132                 "SUBSCRIBE": sub["topic"],
133             };
134
135             if ("fields" in sub)
136                 sub_req["fields"] = sub["fields"]
137
138             exports.eventbus_ws.send(JSON.stringify(sub_req));
139         }
140     }
141 };
142
143 exports.SubscribeEventbus("TIMESTAMP", [], function(data) {
144     data = kismet.sanitizeObject(data);
145     kismet.timestamp_sec = data['kismet.system.timestamp.sec'];
146     kismet.timestamp_usec = data['kismet.system.timestamp.usec'];
147 });
148
149 // exports.SubscribeEventbus("MESSAGE", [], function(e) { console.log(e); });
150
151 /* Define some callback functions for the table */
152
153 exports.renderLastTime = function(data, type, row, meta) {
154     return (new Date(data * 1000).toString()).substring(4, 25);
155 }
156
157 exports.renderDataSize = function(data, type, row, meta) {
158     if (type === 'display')
159         return kismet.HumanReadableSize(data);
160
161     return data;
162 }
163
164 exports.renderMac = function(data, type, row, meta) {
165     if (typeof(data) === 'undefined') {
166         return "<i>n/a</i>";
167     }
168
169     return kismet.censorMAC(data);
170 }
171
172 exports.renderSignal = function(data, type, row, meta) {
173     if (data == 0)
174         return "<i>n/a</i>"
175     return data;
176 }
177
178 exports.renderChannel = function(data, type, row, meta) {
179     if (data == 0)
180         return "<i>n/a</i>"
181     return data;
182 }
183
184 exports.renderPackets = function(data, type, row, meta) {
185     return "<i>Preparing graph</i>";
186 }
187
188 exports.renderUsecTime = function(data, type, row, meta) {
189     if (data == 0)
190         return "<i>n/a</i>";
191
192     var data_sec = data / 1000000;
193
194     var days = Math.floor(data_sec / 86400);
195     var hours = Math.floor((data_sec / 3600) % 24);
196     var minutes = Math.floor((data_sec / 60) % 60);
197     var seconds = Math.floor(data_sec % 60);
198
199     var ret = "";
200
201     if (days > 0)
202         ret = ret + days + "d ";
203     if (hours > 0 || days > 0)
204         ret = ret + hours + "h ";
205     if (minutes > 0 || hours > 0 || days > 0)
206         ret = ret + minutes + "m ";
207     ret = ret + seconds + "s";
208
209     return ret;
210 }
211
212 exports.drawPackets = function(dyncolumn, table, row) {
213     // Find the column
214     var rid = table.column(dyncolumn.name + ':name').index();
215     var match = "td:eq(" + rid + ")";
216
217     var data = row.data();
218
219     // Simplify the RRD so that the bars are thicker in the graph, which
220     // I think looks better.  We do this with a transform function on the
221     // RRD function, and we take the peak value of each triplet of samples
222     // because it seems to be more stable, visually
223     //
224     // We use the aliased field names we extracted from just the minute
225     // component of the per-device packet RRD
226     var simple_rrd =
227         kismet.RecalcRrdData2(data, kismet.RRD_SECOND,
228             {
229                 transform: function(data, opt) {
230                     var slices = 3;
231                     var peak = 0;
232                     var ret = new Array();
233
234                     for (var ri = 0; ri < data.length; ri++) {
235                         peak = Math.max(peak, data[ri]);
236
237                         if ((ri % slices) == (slices - 1)) {
238                             ret.push(peak);
239                             peak = 0;
240                         }
241                     }
242
243                     return ret;
244                 }
245             });
246
247     // Render the sparkline
248     $(match, row.node()).sparkline(simple_rrd,
249         { type: "bar",
250             width: "100px",
251             height: 12,
252             barColor: '#000000',
253             nullColor: '#000000',
254             zeroColor: '#000000'
255         });
256 }
257
258 // Define the basic columns
259 kismet_ui.AddDeviceColumn('column_name', {
260     sTitle: 'Name',
261     field: 'kismet.device.base.commonname',
262     description: 'Device name',
263     width: "150px",
264     renderfunc: function(d, t, r, m) {
265         return kismet.censorMAC(d);
266         /*
267         var dname = kismet.censorMAC(d);
268         return (dname.length > 24) ? dname.substr(0, 23) + '&hellip;' : dname;
269         */
270     }
271 });
272
273 kismet_ui.AddDeviceColumn('column_type', {
274     sTitle: 'Type',
275     field: 'kismet.device.base.type',
276     description: 'Device type',
277     width: '75px',
278 });
279
280 kismet_ui.AddDeviceColumn('column_phy', {
281     sTitle: 'Phy',
282     field: 'kismet.device.base.phyname',
283     description: 'Capture Phy name',
284     width: "75px",
285 });
286
287 kismet_ui.AddDeviceColumn('column_crypto', {
288     sTitle: 'Crypto',
289     field: 'kismet.device.base.crypt',
290     description: 'Encryption',
291     width: "75px",
292     renderfunc: function(d, t, r, m) {
293         if (d == "") {
294             return "n/a";
295         }
296
297         return d;
298     },
299 });
300
301 kismet_ui.AddDeviceColumn('column_signal', {
302     sTitle: 'Sgn',
303     field: 'kismet.device.base.signal/kismet.common.signal.last_signal',
304     description: 'Last-seen signal',
305     width: "30px",
306     sClass: "dt-body-right",
307     renderfunc: function(d, t, r, m) {
308         return exports.renderSignal(d, t, r, m);
309     },
310 });
311
312 kismet_ui.AddDeviceColumn('column_channel', {
313     sTitle: 'Chan',
314     field: 'kismet.device.base.channel',
315     description: 'Last-seen channel',
316     width: "40px",
317     sClass: "dt-body-right",
318     renderfunc: function(d, t, r, m) {
319         if (d != 0) {
320             return d;
321         } else if ('kismet.device.base.frequency' in r &&
322             r['kismet.device.base_frequency'] != 0) {
323                 return kismet_ui.GetPhyConvertedChannel(r['kismet.device.base.phyname'], r['kismet.device.base.frequency']);
324         } else {
325             return "<i>n/a</i>";
326         }
327     },
328 });
329
330 kismet_ui.AddDeviceColumn('column_time', {
331     sTitle: 'Last Seen',
332     field: 'kismet.device.base.last_time',
333     description: 'Last-seen time',
334     renderfunc: function(d, t, r, m) {
335         return exports.renderLastTime(d, t, r, m);
336     },
337     searchable: true,
338     visible: false,
339     orderable: true,
340     width: "100px",
341 });
342
343 kismet_ui.AddDeviceColumn('column_first_time', {
344     sTitle: 'First Seen',
345     field: 'kismet.device.base.first_time',
346     description: 'First-seen time',
347     renderfunc: function(d, t, r, m) {
348         return exports.renderLastTime(d, t, r, m);
349     },
350     searchable: true,
351     visible: false,
352     orderable: true,
353     width: "100px",
354 });
355
356 kismet_ui.AddDeviceColumn('column_datasize', {
357     sTitle: 'Data',
358     field: 'kismet.device.base.datasize',
359     description: 'Data seen',
360     bUseRendered: false,
361     sClass: "dt-body-right",
362     width: "40px",
363     renderfunc: function(d, t, r, m) {
364         return exports.renderDataSize(d, t, r, m);
365     },
366 });
367
368 // Fetch just the last time field, we use the hidden rrd_min_data field to assemble
369 // the rrd.  This is a hack to be more efficient and not send the house or day
370 // rrd records along with it.
371 kismet_ui.AddDeviceColumn('column_packet_rrd', {
372     sTitle: 'Packets',
373     field: ['kismet.device.base.packets.rrd/kismet.common.rrd.last_time', 'packet.rrd.last_time'],
374     name: 'packets',
375     width: "110px",
376     description: 'Packet history graph',
377     renderfunc: function(d, t, r, m) {
378         return exports.renderPackets(d, t, r, m);
379     },
380     drawfunc: function(d, t, r) {
381         return exports.drawPackets(d, t, r);
382     },
383     orderable: false,
384     searchable: false,
385 });
386
387 // Hidden col for packet minute rrd data
388 // We MUST define ONE FIELD and then multiple additional fields are permitted
389 kismet_ui.AddDeviceColumn('column_rrd_minute_hidden', {
390     sTitle: 'packets_rrd_min_data',
391     field: 
392         ['kismet.device.base.packets.rrd/kismet.common.rrd.serial_time', 'kismet.common.rrd.serial_time'],
393     fields: [
394         ['kismet.device.base.packets.rrd/kismet.common.rrd.minute_vec', 'kismet.common.rrd.minute_vec'],
395         ['kismet.device.base.packets.rrd/kismet.common.rrd.last_time', 'kismet.common.rrd.last_time'],
396     ],
397     name: 'packets_rrd_min_data',
398     searchable: false,
399     visible: false,
400     selectable: false,
401     orderable: false
402 });
403
404 // Hidden col for key, mappable, we need to be sure to
405 // fetch it so we can use it as an index
406 kismet_ui.AddDeviceColumn('column_device_key_hidden', {
407     sTitle: 'Key',
408     field: 'kismet.device.base.key',
409     searchable: false,
410     orderable: false,
411     visible: false,
412     selectable: false,
413 });
414
415 // HIdden for phy to always turn it on
416 kismet_ui.AddDeviceColumn('column_phy_hidden', {
417     sTitle: 'Phy',
418     field: 'kismet.device.base.phyname',
419     searchable: true,
420     visible: false,
421     orderable: false,
422     selectable: false,
423 });
424
425 // Hidden col for mac address, searchable
426 kismet_ui.AddDeviceColumn('column_device_mac_hidden', {
427     sTitle: 'MAC',
428     field: 'kismet.device.base.macaddr',
429     searchable: true,
430     orderable: false,
431     visible: false,
432     selectable: false,
433 });
434
435 // Hidden col for mac address, searchable
436 kismet_ui.AddDeviceColumn('column_device_mac', {
437     sTitle: 'MAC',
438     field: 'kismet.device.base.macaddr',
439     description: 'MAC address',
440     searchable: true,
441     orderable: true,
442     visible: false,
443     width: "70px",
444     renderfunc: function(d, t, r, m) {
445         return exports.renderMac(d, t, r, m);
446     },
447 });
448
449 // Hidden column for computing freq in the absence of channel
450 kismet_ui.AddDeviceColumn('column_frequency_hidden', {
451     sTitle: 'Frequency',
452     field: 'kismet.device.base.frequency',
453     searchable: false,
454     visible: false,
455     orderable: false,
456     selectable: false,
457 });
458
459 kismet_ui.AddDeviceColumn('column_frequency', {
460     sTitle: 'Frequency',
461     field: 'kismet.device.base.frequency',
462     description: 'Frequency',
463     name: 'frequency',
464     searchable: false,
465     visible: false,
466     orderable: true,
467 });
468
469 // Manufacturer name
470 kismet_ui.AddDeviceColumn('column_manuf', {
471     sTitle: 'Manuf',
472     field: 'kismet.device.base.manuf',
473     description: 'Manufacturer',
474     name: 'manuf',
475     searchable: true,
476     visible: false,
477     orderable: true,
478     width: "70px",
479     renderfunc: function(d, t, r, m) {
480         return (d.length > 32) ? d.substr(0, 31) + '&hellip;' : d;
481     }
482 });
483
484
485 // Add the (quite complex) device details.
486 // It has a priority of -1000 because we want it to always come first.
487 //
488 // There is no filter function because we always have base device
489 // details
490 //
491 // There is no render function because we immediately fill it during draw.
492 //
493 // The draw function will populate the kismet devicedata when pinged
494 kismet_ui.AddDeviceDetail("base", "Device Info", -1000, {
495     draw: function(data, target, options, storage) {
496         target.devicedata(data, {
497             "id": "genericDeviceData",
498             "fields": [
499             {
500                 field: "kismet.device.base.name",
501                 title: "Name",
502                 help: "Device name, derived from device characteristics or set as a custom name by the user.",
503                 draw: function(opts) {
504                     var name = opts['data']['kismet.device.base.username'];
505                     
506                     if (typeof(name) == 'undefined' || name == "")
507                         name = opts['data']['kismet.device.base.commonname'];
508
509                     if (typeof(name) == 'undefined' || name == "")
510                         name = opts['data']['kismet.device.base.macaddr'];
511
512                     name = kismet.censorMAC(name);
513
514                     var nameobj = 
515                         $('<a>', {
516                             'href': '#'
517                         })
518                         .html(name);
519
520                     nameobj.editable({
521                         type: 'text',
522                         mode: 'inline',
523                         success: function(response, newvalue) {
524                             var jscmd = {
525                                 "username": newvalue
526                             };
527                             var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
528                             $.post(local_uri_prefix + "devices/by-key/" + opts['data']['kismet.device.base.key'] + "/set_name.cmd", postdata, "json");
529                         }
530                     });
531
532                     var container =
533                         $('<span>');
534                     container.append(nameobj);
535                     container.append(
536                         $('<i>', {
537                             'class': 'copyuri pseudolink fa fa-copy',
538                             'style': 'padding-left: 5px;',
539                             'data-clipboard-text': `${name}`, 
540                         })
541                     );
542
543                     return container;
544                 }
545             },
546
547             {
548                 field: "kismet.device.base.tags/notes",
549                 title: "Notes",
550                 help: "Abritrary notes",
551                 draw: function(opts) {
552                     var notes = "";
553
554                     if ('kismet.device.base.tags' in opts['data'])
555                         notes = opts['data']['kismet.device.base.tags']['notes'];
556
557                     if (notes == null)
558                         notes = "";
559                     
560                     var notesobj = 
561                         $('<a>', {
562                             'href': '#',
563                             'data-type': 'textarea',
564                         })
565                         .html(notes.convertNewlines());
566
567                     notesobj.editable({
568                         type: 'text',
569                         mode: 'inline',
570                         success: function(response, newvalue) {
571                             var jscmd = {
572                                 "tagname": "notes",
573                                 "tagvalue": newvalue.escapeSpecialChars(),
574                             };
575                             var postdata = "json=" + encodeURIComponent(JSON.stringify(jscmd));
576                             $.post(local_uri_prefix + "devices/by-key/" + opts['data']['kismet.device.base.key'] + "/set_tag.cmd", postdata, "json");
577                         }
578                     });
579
580                     return notesobj;
581                 }
582             },
583
584
585             {
586                 field: "kismet.device.base.macaddr",
587                 title: "MAC Address",
588                 help: "Unique per-phy address of the transmitting device, when available.  Not all phy types provide MAC addresses, however most do.",
589                 draw: function(opts) {
590                     var mac = kismet.censorMAC(opts['value']);
591
592                     var container =
593                         $('<span>');
594                     container.append(
595                         $('<span>').html(mac)
596                     );
597                     container.append(
598                         $('<i>', {
599                             'class': 'copyuri pseudolink fa fa-copy',
600                             'style': 'padding-left: 5px;',
601                             'data-clipboard-text': `${mac}`, 
602                         })
603                     );
604
605                     return container;
606                 }
607             },
608             {
609                 field: "kismet.device.base.manuf",
610                 title: "Manufacturer",
611                 empty: "<i>Unknown</i>",
612                 help: "Manufacturer of the device, derived from the MAC address.  Manufacturers are registered with the IEEE and resolved in the files specified in kismet.conf under 'manuf='",
613             },
614             {
615                 field: "kismet.device.base.type",
616                 liveupdate: true,
617                 title: "Type",
618                 empty: "<i>Unknown</i>"
619             },
620             {
621                 field: "kismet.device.base.first_time",
622                 liveupdate: true,
623                 title: "First Seen",
624                 draw: function(opts) {
625                     return new Date(opts['value'] * 1000);
626                 }
627             },
628             {
629                 field: "kismet.device.base.last_time",
630                 liveupdate: true,
631                 title: "Last Seen",
632                 draw: function(opts) {
633                     return new Date(opts['value'] * 1000);
634                 }
635             },
636             {
637                 field: "group_frequency",
638                 groupTitle: "Frequencies",
639                 id: "group_frequency",
640                 liveupdate: true,
641
642                 fields: [
643                 {
644                     field: "kismet.device.base.channel",
645                     title: "Channel",
646                     empty: "<i>None Advertised</i>",
647                     help: "The phy-specific channel of the device, if known.  The advertised channel defines a specific, known channel, which is not affected by channel overlap.  Not all phy types advertise fixed channels, and not all device types have fixed channels.  If an advertised channel is not available, the primary frequency is used.",
648                 },
649                 {
650                     field: "kismet.device.base.frequency",
651                     title: "Main Frequency",
652                     help: "The primary frequency of the device, if known.  Not all phy types advertise a fixed frequency in packets.",
653                     draw: function(opts) {
654                         return kismet.HumanReadableFrequency(opts['value']);
655                     },
656                     filterOnZero: true,
657                 },
658                 {
659                     field: "frequency_map",
660                     span: true,
661                     liveupdate: true,
662                     filter: function(opts) {
663                         try {
664                             return (Object.keys(opts['data']['kismet.device.base.freq_khz_map']).length >= 1);
665                         } catch (error) {
666                             return 0;
667                         }
668                     },
669                     render: function(opts) {
670                         var d = 
671                             $('<div>', {
672                                 style: 'width: 80%; height: 250px',
673                             })
674                             .append(
675                                 $('<canvas>', {
676                                     id: 'freqdist',
677                                 })
678                             );
679
680                         return d;
681
682                     },
683                     draw: function(opts) {
684                         var legend = new Array();
685                         var data = new Array();
686
687                         for (var fk in opts['data']['kismet.device.base.freq_khz_map']) {
688                             legend.push(kismet.HumanReadableFrequency(parseInt(fk)));
689                             data.push(opts['data']['kismet.device.base.freq_khz_map'][fk]);
690                         }
691
692                         var barChartData = {
693                             labels: legend,
694
695                             datasets: [{
696                                 label: 'Dataset 1',
697                                 backgroundColor: 'rgba(46, 99, 162, 1)',
698                                 borderWidth: 0,
699                                 data: data,
700                             }]
701
702                         };
703
704                         if ('freqchart' in window[storage]) {
705                             window[storage].freqchart.data.labels = legend;
706                             window[storage].freqchart.data.datasets[0].data = data;
707                             window[storage].freqchart.update();
708                         } else {
709                             window[storage].freqchart = 
710                                 new Chart($('canvas', opts['container']), {
711                                     type: 'bar',
712                                     data: barChartData,
713                                     options: {
714                                         maintainAspectRatio: false,
715                                         animation: false,
716                                         legend: {
717                                             display: false,
718                                         },
719                                         title: {
720                                             display: true,
721                                             text: 'Packet frequency distribution'
722                                         }
723                                     }
724                                 });
725
726                             window[storage].freqchart.update();
727                         }
728                     }
729                 },
730                 ]
731             },
732             {
733                 field: "group_signal_data",
734                 groupTitle: "Signal",
735                 id: "group_signal_data",
736
737                 filter: function(opts) {
738                     var db = kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.last_signal");
739
740                     if (db == 0)
741                         return false;
742
743                     return true;
744                 },
745
746                 fields: [
747                 {
748                     field: "kismet.device.base.signal/kismet.common.signal.signal_rrd",
749                     filterOnZero: true,
750                     title: "Monitor Signal",
751
752                     render: function(opts) {
753                         return '<div class="monitor pseudolink">Monitor</div>';
754                     },
755                     draw: function(opts) {
756                         $('div.monitor', opts['container'])
757                         .on('click', function() {
758                             exports.DeviceSignalDetails(opts['data']['kismet.device.base.key']);
759                         });
760                     },
761
762                     /* RRD - come back to this later
763                     render: function(opts) {
764                         return '<div class="rrd" id="' + opts['key'] + '" />';
765                     },
766                     draw: function(opts) {
767                         var rrdiv = $('div', opts['container']);
768
769                         var rrdata = kismet.RecalcRrdData(opts['data']['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.last_time'], last_devicelist_time, kismet.RRD_MINUTE, opts['data']['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.minute_vec'], {});
770
771                         // We assume the 'best' a signal can usefully be is -20dbm,
772                         // that means we're right on top of it.
773                         // We can assume that -100dbm is a sane floor value for
774                         // the weakest signal.
775                         // If a signal is 0 it means we haven't seen it at all so
776                         // just ignore that data point
777                         // We turn signals into a 'useful' graph by clamping to
778                         // -100 and -20 and then scaling it as a positive number.
779                         var moddata = new Array();
780
781                         for (var x = 0; x < rrdata.length; x++) {
782                             var d = rrdata[x];
783
784                             if (d == 0)
785                                 moddata.push(0);
786
787                             if (d < -100)
788                                 d = -100;
789
790                             if (d > -20)
791                                 d = -20;
792
793                             // Normalize to 0-80
794                             d = (d * -1) - 20;
795
796                             // Reverse (weaker is worse), get as percentage
797                             var rs = (80 - d) / 80;
798
799                             moddata.push(100*rs);
800                         }
801
802                         rrdiv.sparkline(moddata, { type: "bar",
803                             height: 12,
804                             barColor: '#000000',
805                             nullColor: '#000000',
806                             zeroColor: '#000000'
807                         });
808
809                     }
810                     */
811
812                 },
813                 {
814                     field: "kismet.device.base.signal/kismet.common.signal.last_signal",
815                     liveupdate: true,
816                     title: "Latest Signal",
817                     help: "Most recent signal level seen.  Signal levels may vary significantly depending on the data rates used by the device, and often, wireless drivers and devices cannot report strictly accurate signal levels.",
818                     draw: function(opts) {
819                         return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
820                     },
821                     filterOnZero: true,
822                 },
823                 { 
824                     field: "kismet.device.base.signal/kismet.common.signal.last_noise",
825                     liveupdate: true,
826                     title: "Latest Noise",
827                     help: "Most recent noise level seen.  Few drivers can report noise levels.",
828                     draw: function(opts) {
829                         return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
830                     },
831                     filterOnZero: true,
832                 },
833                 { 
834                     field: "kismet.device.base.signal/kismet.common.signal.min_signal",
835                     liveupdate: true,
836                     title: "Min. Signal",
837                     help: "Weakest signal level seen.  Signal levels may vary significantly depending on the data rates used by the device, and often, wireless drivers and devices cannot report strictly accurate signal levels.",
838                     draw: function(opts) {
839                         return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
840                     },
841                     filterOnZero: true,
842                 },
843
844                 { 
845                     field: "kismet.device.base.signal/kismet.common.signal.max_signal",
846                     liveupdate: true,
847                     title: "Max. Signal",
848                     help: "Strongest signal level seen.  Signal levels may vary significantly depending on the data rates used by the device, and often, wireless drivers and devices cannot report strictly accurate signal levels.",
849                     draw: function(opts) {
850                         return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
851                     },
852                     filterOnZero: true,
853                 },
854                 { 
855                     field: "kismet.device.base.signal/kismet.common.signal.min_noise",
856                     liveupdate: true,
857                     title: "Min. Noise",
858                     filterOnZero: true,
859                     help: "Least amount of interference or noise seen.  Most capture drivers are not capable of measuring noise levels.",
860                     draw: function(opts) {
861                         return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
862                     },
863                 },
864                 { 
865                     field: "kismet.device.base.signal/kismet.common.signal.max_noise",
866                     liveupdate: true,
867                     title: "Max. Noise",
868                     filterOnZero: true,
869                     help: "Largest amount of interference or noise seen.  Most capture drivers are not capable of measuring noise levels.",
870                     draw: function(opts) {
871                         return opts['value'] + " " + data["kismet.device.base.signal"]["kismet.common.signal.type"];
872                     },
873                 },
874                 { // Pseudo-field of aggregated location, only show when the location is valid
875                     field: "kismet.device.base.signal/kismet.common.signal.peak_loc",
876                     liveupdate: true,
877                     title: "Peak Location",
878                     help: "When a GPS location is available, the peak location is the coordinates at which the strongest signal level was recorded for this device.",
879                     filter: function(opts) {
880                         return kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.peak_loc/kismet.common.location.fix") >= 2;
881                     },
882                     draw: function(opts) {
883                         var loc =
884                             kismet.censorLocation(kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.peak_loc/kismet.common.location.geopoint[1]")) + ", " +
885                             kismet.censorLocation(kismet.ObjectByString(opts['data'], "kismet.device.base.signal/kismet.common.signal.peak_loc/kismet.common.location.geopoint[0]"));
886
887                         return loc;
888                     },
889                 },
890
891                 ],
892             },
893             {
894                 field: "group_packet_counts",
895                 groupTitle: "Packets",
896                 id: "group_packet_counts",
897
898                 fields: [
899                 {
900                     field: "graph_field_overall",
901                     span: true,
902                     liveupdate: true,
903                     render: function(opts) {
904                         var d = 
905                             $('<div>', {
906                                 style: 'width: 80%; height: 250px; padding-bottom: 5px;',
907                             })
908                             .append(
909                                 $('<canvas>', {
910                                     id: 'packetdonut',
911                                 })
912                             );
913
914                         return d;
915                     },
916                     draw: function(opts) {
917                         var legend = ['LLC/Management', 'Data'];
918                         var data = [
919                             opts['data']['kismet.device.base.packets.llc'],
920                             opts['data']['kismet.device.base.packets.data'],
921                         ];
922                         var colors = [
923                             'rgba(46, 99, 162, 1)',
924                             'rgba(96, 149, 212, 1)',
925                         ];
926
927                         var barChartData = {
928                             labels: legend,
929
930                             datasets: [{
931                                 label: 'Dataset 1',
932                                 backgroundColor: colors,
933                                 borderWidth: 0,
934                                 data: data,
935                             }],
936                         };
937
938                         if ('packetdonut' in window[storage]) {
939                             window[storage].packetdonut.data.datasets[0].data = data;
940                             window[storage].packetdonut.update();
941                         } else {
942                             window[storage].packetdonut = 
943                                 new Chart($('canvas', opts['container']), {
944                                     type: 'doughnut',
945                                     data: barChartData,
946                                     options: {
947                                         global: {
948                                             maintainAspectRatio: false,
949                                         },
950                                         animation: false,
951                                         legend: {
952                                             display: true,
953                                         },
954                                         title: {
955                                             display: true,
956                                             text: 'Packet Types'
957                                         },
958                                         height: '200px',
959                                     }
960                                 });
961
962                             window[storage].packetdonut.render();
963                         }
964                     },
965                 },
966                 {
967                     field: "kismet.device.base.packets.total",
968                     liveupdate: true,
969                     title: "Total Packets",
970                     help: "Count of all packets of all types",
971                 },
972                 {
973                     field: "kismet.device.base.packets.llc",
974                     liveupdate: true,
975                     title: "LLC/Management",
976                     help: "LLC (Link Layer Control) and Management packets are typically used for controlling and defining wireless networks.  Typically they do not carry data.",
977                 },
978                 {
979                     field: "kismet.device.base.packets.error",
980                     liveupdate: true,
981                     title: "Error/Invalid",
982                     help: "Error and invalid packets indicate a packet was received and was partially processable, but was damaged or incorrect in some way.  Most error packets are dropped completely as it is not possible to associate them with a specific device.",
983                 },
984                 {
985                     field: "kismet.device.base.packets.data",
986                     liveupdate: true,
987                     title: "Data",
988                     help: "Data frames carry messages and content for the device.",
989                 },
990                 {
991                     field: "kismet.device.base.packets.crypt",
992                     liveupdate: true,
993                     title: "Encrypted",
994                     help: "Some data frames can be identified by Kismet as carrying encryption, either by the contents or by packet flags, depending on the phy type",
995                 },
996                 {
997                     field: "kismet.device.base.packets.filtered",
998                     liveupdate: true,
999                     title: "Filtered",
1000                     help: "Filtered packets are ignored by Kismet",
1001                 },
1002                 {
1003                     field: "kismet.device.base.datasize",
1004                     liveupdate: true,
1005                     title: "Data Transferred",
1006                     help: "Amount of data transferred",
1007                     draw: function(opts) {
1008                         return kismet.HumanReadableSize(opts['value']);
1009                     }
1010                 }
1011
1012
1013                 ]
1014             },
1015
1016             {
1017                 // Location is its own group
1018                 groupTitle: "Avg. Location",
1019                 // Spoofed field for ID purposes
1020                 field: "group_avg_location",
1021                 // Sub-table ID
1022                 id: "group_avg_location",
1023
1024                 // Don't show location if we don't know it
1025                 filter: function(opts) {
1026                     return (kismet.ObjectByString(opts['data'], "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.fix") >= 2);
1027                 },
1028
1029                 // Fields in subgroup
1030                 fields: [
1031                 {
1032                     field: "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.geopoint",
1033                     title: "Location",
1034                     draw: function(opts) {
1035                         try {
1036                             if (opts['value'][1] == 0 || opts['value'][0] == 0)
1037                                 return "<i>Unknown</i>";
1038
1039                             return kismet.censorLocation(opts['value'][1]) + ", " + kismet.censorLocation(opts['value'][0]);
1040                         } catch (error) {
1041                             return "<i>Unknown</i>";
1042                         }
1043                     }
1044                 },
1045                 {
1046                     field: "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.alt",
1047                     title: "Altitude",
1048                     filter: function(opts) {
1049                         return (kismet.ObjectByString(opts['data'], "kismet.device.base.location/kismet.common.location.avg_loc/kismet.common.location.fix") >= 3);
1050                     },
1051                     draw: function(opts) {
1052                         try {
1053                             return kismet_ui.renderHeightDistance(opts['value']);
1054                         } catch (error) {
1055                             return "<i>Unknown</i>";
1056                         }
1057                     },
1058                 }
1059                 ],
1060             }
1061             ]
1062         }, storage);
1063     }
1064 });
1065
1066 kismet_ui.AddDeviceDetail("packets", "Packet Graphs", 10, {
1067     render: function(data) {
1068         // Make 3 divs for s, m, h RRD
1069         var ret = 
1070             '<b>Packet Rates</b><br /><br />' +
1071             'Packets per second (last minute)<br /><div /><br />' +
1072             'Packets per minute (last hour)<br /><div /><br />' +
1073             'Packets per hour (last day)<br /><div />';
1074
1075         if ('kismet.device.base.datasize.rrd' in data)
1076             ret += '<br /><b>Data</b><br /><br />' +
1077             'Data per second (last minute)<br /><div /><br />' +
1078             'Data per minute (last hour)<br /><div /><br />' +
1079             'Data per hour (last day)<br /><div />';
1080
1081         return ret;
1082     },
1083     draw: function(data, target) {
1084         var m = $('div:eq(0)', target);
1085         var h = $('div:eq(1)', target);
1086         var d = $('div:eq(2)', target);
1087
1088         var dm = $('div:eq(3)', target);
1089         var dh = $('div:eq(4)', target);
1090         var dd = $('div:eq(5)', target);
1091
1092         var mdata = [];
1093         var hdata = [];
1094         var ddata = [];
1095
1096         if (('kismet.device.base.packets.rrd' in data)) {
1097             mdata = kismet.RecalcRrdData2(data['kismet.device.base.packets.rrd'], kismet.RRD_SECOND);
1098             hdata = kismet.RecalcRrdData2(data['kismet.device.base.packets.rrd'], kismet.RRD_MINUTE);
1099             ddata = kismet.RecalcRrdData2(data['kismet.device.base.packets.rrd'], kismet.RRD_HOUR);
1100
1101             m.sparkline(mdata, { type: "bar",
1102                     height: 12,
1103                     barColor: '#000000',
1104                     nullColor: '#000000',
1105                     zeroColor: '#000000'
1106                 });
1107             h.sparkline(hdata,
1108                 { type: "bar",
1109                     height: 12,
1110                     barColor: '#000000',
1111                     nullColor: '#000000',
1112                     zeroColor: '#000000'
1113                 });
1114             d.sparkline(ddata,
1115                 { type: "bar",
1116                     height: 12,
1117                     barColor: '#000000',
1118                     nullColor: '#000000',
1119                     zeroColor: '#000000'
1120                 });
1121         } else {
1122             m.html("<i>No packet data available</i>");
1123             h.html("<i>No packet data available</i>");
1124             d.html("<i>No packet data available</i>");
1125         }
1126             
1127
1128         if ('kismet.device.base.datasize.rrd' in data) {
1129             var dmdata = kismet.RecalcRrdData2(data['kismet.device.base.datasize.rrd'], kismet.RRD_SECOND);
1130             var dhdata = kismet.RecalcRrdData2(data['kismet.device.base.datasize.rrd'], kismet.RRD_MINUTE);
1131             var dddata = kismet.RecalcRrdData2(data['kismet.device.base.datasize.rrd'], kismet.RRD_HOUR);
1132
1133         dm.sparkline(dmdata,
1134             { type: "bar",
1135                 height: 12,
1136                 barColor: '#000000',
1137                 nullColor: '#000000',
1138                 zeroColor: '#000000'
1139             });
1140         dh.sparkline(dhdata,
1141             { type: "bar",
1142                 height: 12,
1143                 barColor: '#000000',
1144                 nullColor: '#000000',
1145                 zeroColor: '#000000'
1146             });
1147         dd.sparkline(dddata,
1148             { type: "bar",
1149                 height: 12,
1150                 barColor: '#000000',
1151                 nullColor: '#000000',
1152                 zeroColor: '#000000'
1153             });
1154         }
1155
1156     }
1157 });
1158
1159 kismet_ui.AddDeviceDetail("seenby", "Seen By", 900, {
1160     filter: function(data) {
1161         return (Object.keys(data['kismet.device.base.seenby']).length > 0);
1162     },
1163     draw: function(data, target, options, storage) {
1164         target.devicedata(data, {
1165             id: "seenbyDeviceData",
1166
1167             fields: [
1168             {
1169                 field: "kismet.device.base.seenby",
1170                 id: "seenby_group",
1171                 groupIterate: true,
1172                 iterateTitle: function(opts) {
1173                     var this_uuid = opts['value'][opts['index']]['kismet.common.seenby.uuid'];
1174                     $.get(`${local_uri_prefix}datasource/by-uuid/${this_uuid}/source.json`)
1175                     .done(function(dsdata) {
1176                         dsdata = kismet.sanitizeObject(dsdata);
1177                         opts['title'].html(`${dsdata['kismet.datasource.name']} (${dsdata['kismet.datasource.capture_interface']}) ${dsdata['kismet.datasource.uuid']}`);
1178                     })
1179                     return opts['value'][opts['index']]['kismet.common.seenby.uuid'];
1180                 },
1181                 fields: [
1182                 {
1183                     field: "kismet.common.seenby.uuid",
1184                     title: "UUID",
1185                     empty: "<i>None</i>"
1186                 },
1187                 {
1188                     field: "kismet.common.seenby.first_time",
1189                     title: "First Seen",
1190                     draw: kismet_ui.RenderTrimmedTime,
1191                 },
1192                 {
1193                     field: "kismet.common.seenby.last_time",
1194                     title: "Last Seen",
1195                     draw: kismet_ui.RenderTrimmedTime,
1196                 },
1197                 ]
1198             }]
1199         });
1200     },
1201 });
1202
1203 kismet_ui.AddDeviceDetail("devel", "Dev/Debug Options", 10000, {
1204     render: function(data) {
1205         return 'Device JSON: <a href="devices/by-key/' + data['kismet.device.base.key'] + '/device.prettyjson" target="_new">link</a><br />';
1206     }});
1207
1208 /* Sidebar:  Memory monitor
1209  *
1210  * The memory monitor looks at system_status and plots the amount of
1211  * ram vs number of tracked devices from the RRD
1212  */
1213 kismet_ui_sidebar.AddSidebarItem({
1214     id: 'memory_sidebar',
1215     listTitle: '<i class="fa fa-tasks"></i> Memory Monitor',
1216     clickCallback: function() {
1217         exports.MemoryMonitor();
1218     },
1219 });
1220
1221 /*
1222 kismet_ui_sidebar.AddSidebarItem({
1223     id: 'pcap_sidebar',
1224     priority: 10000,
1225     listTitle: '<i class="fa fa-download"></i> Download Pcap-NG',
1226     clickCallback: function() {
1227         location.href = "datasource/pcap/all_sources.pcapng";
1228     },
1229 });
1230 */
1231
1232 var memoryupdate_tid;
1233 var memory_panel = null;
1234 var memory_chart = null;
1235
1236 exports.MemoryMonitor = function() {
1237     var w = $(window).width() * 0.75;
1238     var h = $(window).height() * 0.5;
1239     var offty = 20;
1240
1241     if ($(window).width() < 450 || $(window).height() < 450) {
1242         w = $(window).width() - 5;
1243         h = $(window).height() - 5;
1244         offty = 0;
1245     }
1246
1247     memory_chart = null;
1248
1249     var content = 
1250         $('<div>', {
1251             'style': 'width: 100%; height: 100%;'
1252         })
1253         .append(
1254             $('<div>', {
1255                 "style": "position: absolute; top: 0px; right: 10px; float: right;"
1256             })
1257             .append(
1258                 $('<span>', {
1259                     'id': 'k_mm_devs',
1260                     'class': 'padded',
1261                 }).html('#devices')
1262             )
1263             .append(
1264                 $('<span>', {
1265                     'id': 'k_mm_ram',
1266                     'class': 'padded',
1267                 }).html('#ram')
1268             )
1269         )
1270         .append(
1271             $('<canvas>', {
1272                 'id': 'k-mm-canvas',
1273                 'style': 'k-mm-canvas'
1274             })
1275         );
1276
1277     memory_panel = $.jsPanel({
1278         id: 'memory',
1279         headerTitle: '<i class="fa fa-tasks" /> Memory use',
1280         headerControls: {
1281             controls: 'closeonly',
1282             iconfont: 'jsglyph',
1283         },
1284         content: content,
1285         onclosed: function() {
1286             clearTimeout(memoryupdate_tid);
1287         }
1288     }).resize({
1289         width: w,
1290         height: h
1291     }).reposition({
1292         my: 'center-top',
1293         at: 'center-top',
1294         of: 'window',
1295         offsetY: offty
1296     });
1297
1298     memorydisplay_refresh();
1299 }
1300
1301 function memorydisplay_refresh() {
1302     clearTimeout(memoryupdate_tid);
1303
1304     if (memory_panel == null)
1305         return;
1306
1307     if (memory_panel.is(':hidden'))
1308         return;
1309
1310     $.get(local_uri_prefix + "system/status.json")
1311     .done(function(data) {
1312         // Common rrd type and source field
1313         var rrdtype = kismet.RRD_MINUTE;
1314         var rrddata = 'kismet.common.rrd.hour_vec';
1315
1316         // Common point titles
1317         var pointtitles = new Array();
1318
1319         for (var x = 60; x > 0; x--) {
1320             if (x % 5 == 0) {
1321                 pointtitles.push(x + 'm');
1322             } else {
1323                 pointtitles.push(' ');
1324             }
1325         }
1326
1327         var mem_linedata =
1328             kismet.RecalcRrdData2(data['kismet.system.memory.rrd'], rrdtype);
1329
1330         for (var p in mem_linedata) {
1331             mem_linedata[p] = Math.round(mem_linedata[p] / 1024);
1332         }
1333
1334         var dev_linedata =
1335             kismet.RecalcRrdData2(data['kismet.system.devices.rrd'], rrdtype);
1336
1337         $('#k_mm_devs', memory_panel.content).html(`${dev_linedata[dev_linedata.length - 1]} devices`);
1338         $('#k_mm_ram', memory_panel.content).html(`${mem_linedata[mem_linedata.length - 1]} MB`);
1339
1340         var datasets = [
1341             {
1342                 label: 'Memory (MB)',
1343                 fill: 'false',
1344                 // yAxisID: 'mem-axis',
1345                 borderColor: 'black',
1346                 backgroundColor: 'transparent',
1347                 data: mem_linedata,
1348             },
1349             {
1350                 label: 'Devices',
1351                 fill: 'false',
1352                 // yAxisID: 'dev-axis',
1353                 borderColor: 'blue',
1354                 backgroundColor: 'rgba(100, 100, 255, 0.33)',
1355                 data: dev_linedata,
1356             }
1357         ];
1358
1359         if (memory_chart == null) {
1360             var canvas = $('#k-mm-canvas', memory_panel.content);
1361
1362             memory_chart = new Chart(canvas, {
1363                 type: 'line',
1364                 options: {
1365                     responsive: true,
1366                     maintainAspectRatio: false,
1367                     scales: {
1368                         yAxes: [
1369                             {
1370                                 position: "left",
1371                                 "id": "mem-axis",
1372                                 ticks: {
1373                                     beginAtZero: true,
1374                                 }
1375                             },
1376 /*                          {
1377                                 position: "right",
1378                                 "id": "dev-axis",
1379                                 ticks: {
1380                                     beginAtZero: true,
1381                                 }
1382                             }
1383 */
1384                         ]
1385                     },
1386                 },
1387                 data: {
1388                     labels: pointtitles,
1389                     datasets: datasets
1390                 }
1391             });
1392
1393         } else {
1394             memory_chart.data.datasets = datasets;
1395             memory_chart.data.labels = pointtitles;
1396             memory_chart.update(0);
1397         }
1398     })
1399     .always(function() {
1400         memoryupdate_tid = setTimeout(memorydisplay_refresh, 5000);
1401     });
1402 };
1403
1404
1405 /* Sidebar:  Packet queue display
1406  *
1407  * Packet queue display graphs the amount of packets in the queue, the amount dropped, 
1408  * the # of duplicates, and so on
1409  */
1410 kismet_ui_sidebar.AddSidebarItem({
1411     id: 'packetqueue_sidebar',
1412     listTitle: '<i class="fa fa-area-chart"></i> Packet Rates',
1413     clickCallback: function() {
1414         exports.PacketQueueMonitor();
1415     },
1416 });
1417
1418 var packetqueueupdate_tid;
1419 var packetqueue_panel = null;
1420
1421 exports.PacketQueueMonitor = function() {
1422     var w = $(window).width() * 0.75;
1423     var h = $(window).height() * 0.5;
1424     var offty = 20;
1425
1426     if ($(window).width() < 450 || $(window).height() < 450) {
1427         w = $(window).width() - 5;
1428         h = $(window).height() - 5;
1429         offty = 0;
1430     }
1431
1432     var content =
1433         $('<div class="k-pqm-contentdiv">')
1434         .append(
1435             $('<div id="pqm-tabs" class="tabs-min">')
1436         );
1437
1438     packetqueue_panel = $.jsPanel({
1439         id: 'packetqueue',
1440         headerTitle: '<i class="fa fa-area-chart" /> Packet Rates',
1441         headerControls: {
1442             controls: 'closeonly',
1443             iconfont: 'jsglyph',
1444         },
1445         content: content,
1446         onclosed: function() {
1447             clearTimeout(packetqueue_panel.packetqueueupdate_tid);
1448         }
1449     }).resize({
1450         width: w,
1451         height: h
1452     }).reposition({
1453         my: 'center-top',
1454         at: 'center-top',
1455         of: 'window',
1456         offsetY: offty
1457     });
1458
1459     packetqueue_panel.packetqueue_chart = null;
1460     packetqueue_panel.datasource_chart = null;
1461
1462     var f_pqm_packetqueue = function(div) {
1463         packetqueue_panel.pq_content = div;
1464     };
1465
1466     var f_pqm_ds = function(div) {
1467         packetqueue_panel.ds_content = div;
1468     };
1469
1470     kismet_ui_tabpane.AddTab({
1471         id: 'packetqueue',
1472         tabTitle: 'Processing Queue',
1473         createCallback: f_pqm_packetqueue,
1474         priority: -1001
1475     }, 'pqm-tabs');
1476
1477     kismet_ui_tabpane.AddTab({
1478         id: 'datasources-graph',
1479         tabTitle: 'Per Datasource',
1480         createCallback: f_pqm_ds,
1481         priority: -1000
1482     }, 'pqm-tabs');
1483
1484     kismet_ui_tabpane.MakeTabPane($('#pqm-tabs', content), 'pqm-tabs');
1485
1486     packetqueuedisplay_refresh();
1487     datasourcepackets_refresh();
1488 }
1489
1490 function packetqueuedisplay_refresh() {
1491     if (packetqueue_panel == null)
1492         return;
1493
1494     clearTimeout(packetqueue_panel.packetqueueupdate_tid);
1495
1496     if (packetqueue_panel.is(':hidden'))
1497         return;
1498
1499     $.get(local_uri_prefix + "packetchain/packet_stats.json")
1500     .done(function(data) {
1501         // Common rrd type and source field
1502         var rrdtype = kismet.RRD_MINUTE;
1503
1504         // Common point titles
1505         var pointtitles = new Array();
1506
1507         for (var x = 60; x > 0; x--) {
1508             if (x % 5 == 0) {
1509                 pointtitles.push(x + 'm');
1510             } else {
1511                 pointtitles.push(' ');
1512             }
1513         }
1514
1515         var peak_linedata =
1516             kismet.RecalcRrdData2(data['kismet.packetchain.peak_packets_rrd'], rrdtype);
1517         var rate_linedata =
1518             kismet.RecalcRrdData2(data['kismet.packetchain.packets_rrd'], rrdtype);
1519         var queue_linedata =
1520             kismet.RecalcRrdData2(data['kismet.packetchain.queued_packets_rrd'], rrdtype);
1521         var drop_linedata =
1522             kismet.RecalcRrdData2(data['kismet.packetchain.dropped_packets_rrd'], rrdtype);
1523         var dupe_linedata =
1524             kismet.RecalcRrdData2(data['kismet.packetchain.dupe_packets_rrd'], rrdtype);
1525         var processing_linedata =
1526             kismet.RecalcRrdData2(data['kismet.packetchain.processed_packets_rrd'], rrdtype);
1527
1528         var datasets = [
1529             {
1530                 label: 'Processed',
1531                 fill: 'false',
1532                 borderColor: 'orange',
1533                 backgroundColor: 'transparent',
1534                 data: processing_linedata,
1535                 pointStyle: 'cross',
1536             },
1537             {
1538                 label: 'Incoming packets (peak)',
1539                 fill: 'false',
1540                 borderColor: 'black',
1541                 backgroundColor: 'rgba(100, 100, 100, 0.33)',
1542                 data: peak_linedata,
1543             },
1544             {
1545                 label: 'Incoming packets (1 min avg)',
1546                 fill: 'false',
1547                 borderColor: 'purple',
1548                 backgroundColor: 'transparent',
1549                 data: rate_linedata,
1550                 pointStyle: 'rect',
1551             },
1552             {
1553                 label: 'Queue',
1554                 fill: 'false',
1555                 borderColor: 'blue',
1556                 backgroundColor: 'transparent',
1557                 data: queue_linedata,
1558                 pointStyle: 'cross',
1559             },
1560             {
1561                 label: 'Dropped / error packets',
1562                 fill: 'false',
1563                 borderColor: 'red',
1564                 backgroundColor: 'transparent',
1565                 data: drop_linedata,
1566                 pointStyle: 'star',
1567             },
1568             {
1569                 label: 'Duplicates',
1570                 fill: 'false',
1571                 borderColor: 'green',
1572                 backgroundColor: 'transparent',
1573                 data: dupe_linedata,
1574                 pointStyle: 'triangle',
1575             },
1576         ];
1577
1578         if (packetqueue_panel.packetqueue_chart == null) {
1579             packetqueue_panel.pq_content.append(
1580                 $('<canvas>', {
1581                     "id": "pq-canvas",
1582                     "width": "100%",
1583                     "height": "100%",
1584                     "class": "k-mm-canvas",
1585                 })
1586             );
1587
1588             var canvas = $('#pq-canvas', packetqueue_panel.pq_content);
1589
1590             packetqueue_panel.packetqueue_chart = new Chart(canvas, {
1591                 type: 'line',
1592                 options: {
1593                     responsive: true,
1594                     maintainAspectRatio: false,
1595                     scales: {
1596                         yAxes: [
1597                             {
1598                                 position: "left",
1599                                 "id": "mem-axis",
1600                                 ticks: {
1601                                     beginAtZero: true,
1602                                 }
1603                             },
1604                         ]
1605                     },
1606                 },
1607                 data: {
1608                     labels: pointtitles,
1609                     datasets: datasets
1610                 }
1611             });
1612
1613         } else {
1614             packetqueue_panel.packetqueue_chart.data.datasets = datasets;
1615             packetqueue_panel.packetqueue_chart.data.labels = pointtitles;
1616             packetqueue_panel.packetqueue_chart.update(0);
1617         }
1618     })
1619     .always(function() {
1620         packetqueue_panel.packetqueueupdate_tid = setTimeout(packetqueuedisplay_refresh, 5000);
1621     });
1622 };
1623
1624 function datasourcepackets_refresh() {
1625     if (packetqueue_panel == null)
1626         return;
1627
1628     clearTimeout(packetqueue_panel.datasourceupdate_tid);
1629
1630     if (packetqueue_panel.is(':hidden'))
1631         return;
1632
1633     $.get(local_uri_prefix + "datasource/all_sources.json")
1634     .done(function(data) {
1635         var datasets = [];
1636         var num = 0;
1637
1638         // Common point titles
1639         var pointtitles = new Array();
1640
1641         var rval = $('#pq_ds_range', packetqueue_panel.ds_content).val();
1642         var range = kismet.RRD_SECOND;
1643
1644         if (rval == "hour")
1645             range = kismet.RRD_MINUTE;
1646         if (rval == "day")
1647             range = kismet.RRD_HOUR;
1648
1649         if (range == kismet.RRD_SECOND || range == kismet.RRD_MINUTE) {
1650             for (var x = 60; x > 0; x--) {
1651                 if (x % 5 == 0) {
1652                     if (range == kismet.RRD_SECOND)
1653                         pointtitles.push(x + 's');
1654                     else
1655                         pointtitles.push(x + 'm');
1656                 } else {
1657                     pointtitles.push(' ');
1658                 }
1659             }
1660         } else {
1661             for (var x = 23; x > 0; x--) {
1662                 pointtitles.push(x + 'h');
1663             }
1664         }
1665
1666         for (var source of data) {
1667             var color = parseInt(255 * (num / data.length))
1668
1669             var linedata;
1670
1671             if ($('#pq_ds_type', packetqueue_panel.ds_content).val() == "bps")
1672                 linedata =
1673                     kismet.RecalcRrdData2(source['kismet.datasource.packets_datasize_rrd'], 
1674                     range,
1675                     {
1676                         transform: function(data, opt) {
1677                             var ret = [];
1678
1679                             for (var d of data)
1680                                 ret.push(d / 1024);
1681
1682                             return ret;
1683                         }
1684                     });
1685             else
1686                 linedata =
1687                     kismet.RecalcRrdData2(source['kismet.datasource.packets_rrd'], range);
1688
1689             datasets.push({
1690                 "label": source['kismet.datasource.name'],
1691                 "borderColor": `hsl(${color}, 100%, 50%)`,
1692                 "data": linedata,
1693                 "fill": false,
1694             });
1695
1696             num = num + 1;
1697
1698         }
1699
1700         if (packetqueue_panel.datasource_chart == null) {
1701             packetqueue_panel.ds_content.append(
1702                 $('<div>', {
1703                     "style": "position: absolute; top: 0px; right: 10px; float: right;"
1704                 })
1705                 .append(
1706                     $('<select>', {
1707                         "id": "pq_ds_type",
1708                     })
1709                     .append(
1710                         $('<option>', {
1711                             "value": "pps",
1712                             "selected": "selected",
1713                         }).text("Packets")
1714                     )
1715                     .append(
1716                         $('<option>', {
1717                             "value": "bps",
1718                         }).text("Data (kB)")
1719                     )
1720                 )
1721                 .append(
1722                     $('<select>', {
1723                         "id": "pq_ds_range",
1724                     })
1725                     .append(
1726                         $('<option>', {
1727                             "value": "second",
1728                             "selected": "selected",
1729                         }).text("Past Minute")
1730                     )
1731                     .append(
1732                         $('<option>', {
1733                             "value": "hour",
1734                         }).text("Past Hour")
1735                     )
1736                     .append(
1737                         $('<option>', {
1738                             "value": "day",
1739                         }).text("Past Day")
1740                     )
1741                 )
1742             ).append(
1743                 $('<canvas>', {
1744                     "id": "dsg-canvas",
1745                     "width": "100%",
1746                     "height": "100%",
1747                     "class": "k-mm-canvas",
1748                 })
1749             );
1750
1751             packetqueue_panel.datasource_chart = 
1752                 new Chart($('#dsg-canvas', packetqueue_panel.ds_content), {
1753                 "type": "line",
1754                 "options": {
1755                     "responsive": true,
1756                     "maintainAspectRatio": false,
1757                     "scales": {
1758                         "yAxes": [
1759                             {
1760                                 "position": "left",
1761                                 "id": "pkts-axis",
1762                                 "ticks": {
1763                                     "beginAtZero": true,
1764                                 }
1765                             },
1766                         ],
1767                     },
1768                 },
1769                 "data": {
1770                     "labels": pointtitles,
1771                     "datasets": datasets,
1772                 }
1773             });
1774         } else {
1775             packetqueue_panel.datasource_chart.data.datasets = datasets;
1776             packetqueue_panel.datasource_chart.data.labels = pointtitles;
1777             packetqueue_panel.datasource_chart.update(0);
1778         }
1779     })
1780     .always(function() {
1781         packetqueue_panel.datasourceupdate_tid = setTimeout(datasourcepackets_refresh, 1000);
1782     });
1783 };
1784
1785 // Settings options
1786
1787 kismet_ui_settings.AddSettingsPane({
1788     id: 'gps_topbar',
1789     listTitle: "GPS Status",
1790     create: function(elem) {
1791         elem.append(
1792             $('<form>', {
1793                 id: 'form'
1794             })
1795             .append(
1796                 $('<fieldset>', {
1797                     id: 'set_gps'
1798                 })
1799                 .append(
1800                     $('<legend>', {})
1801                     .html("GPS Display")
1802                 )
1803                 .append(
1804                     $('<input>', {
1805                         type: 'radio',
1806                         id: 'gps_icon',
1807                         name: 'gps_status',
1808                         value: 'icon',
1809                     })
1810                 )
1811                 .append(
1812                     $('<label>', {
1813                         for: 'gps_icon'
1814                     })
1815                     .html("Icon only")
1816                 )
1817                 .append($('<div>', { class: 'spacer' }).html(" "))
1818                 .append(
1819                     $('<input>', {
1820                         type: 'radio',
1821                         id: 'gps_text',
1822                         name: 'gps_status',
1823                         value: 'text',
1824                     })
1825                 )
1826                 .append(
1827                     $('<label>', {
1828                         for: 'gps_text'
1829                     })
1830                     .html("Text only")
1831                 )
1832                 .append($('<div>', { class: 'spacer' }).html(" "))
1833                 .append(
1834                     $('<input>', {
1835                         type: 'radio',
1836                         id: 'gps_both',
1837                         name: 'gps_status',
1838                         value: 'both',
1839                     })
1840                 )
1841                 .append(
1842                     $('<label>', {
1843                         for: 'gps_both'
1844                     })
1845                     .html("Icon and Text")
1846                 )
1847             )
1848         );
1849
1850         $('#form', elem).on('change', function() {
1851             kismet_ui_settings.SettingsModified();
1852         });
1853
1854         if (kismet.getStorage('kismet.ui.gps.icon', 'True') === 'True') {
1855             if (kismet.getStorage('kismet.ui.gps.text', 'True') === 'True') {
1856                 $('#gps_both', elem).attr('checked', 'checked');
1857             } else {
1858                 $('#gps_icon', elem).attr('checked', 'checked');
1859             }
1860         } else {
1861             $('#gps_text', elem).attr('checked', 'checked');
1862         }
1863
1864         $('#set_gps', elem).controlgroup();
1865     },
1866     save: function(elem) {
1867         var val = $("input[name='gps_status']:checked", elem).val();
1868
1869         if (val === "both") {
1870             kismet.putStorage('kismet.ui.gps.text', 'True');
1871             kismet.putStorage('kismet.ui.gps.icon', 'True');
1872         } else if (val === "text") {
1873             kismet.putStorage('kismet.ui.gps.text', 'True');
1874             kismet.putStorage('kismet.ui.gps.icon', 'False');
1875         } else if (val === "icon") {
1876             kismet.putStorage('kismet.ui.gps.icon', 'True');
1877             kismet.putStorage('kismet.ui.gps.text', 'False');
1878         }
1879     }
1880 })
1881
1882 kismet_ui_settings.AddSettingsPane({
1883     id: 'base_units_measurements',
1884     listTitle: 'Units &amp; Measurements',
1885     create: function(elem) {
1886         elem.append(
1887             $('<form>', {
1888                 id: 'form'
1889             })
1890             .append(
1891                 $('<fieldset>', {
1892                     id: 'set_distance',
1893                 })
1894                 .append(
1895                     $('<legend>', { })
1896                     .html("Distance")
1897                 )
1898                 .append(
1899                     $('<input>', {
1900                         type: 'radio',
1901                         id: 'dst_metric',
1902                         name: 'distance',
1903                         value: 'metric',
1904                     })
1905                 )
1906                 .append(
1907                     $('<label>', {
1908                         for: 'dst_metric',
1909                     })
1910                     .html('Metric')
1911                 )
1912                 .append(
1913                     $('<input>', {
1914                         type: 'radio',
1915                         id: 'dst_imperial',
1916                         name: 'distance',
1917                         value: 'imperial',
1918                     })
1919                 )
1920                 .append(
1921                     $('<label>', {
1922                         for: 'dst_imperial',
1923                     })
1924                     .html('Imperial')
1925                 )
1926             )
1927             .append(
1928                 $('<br>', { })
1929             )
1930             .append(
1931                 $('<fieldset>', {
1932                     id: 'set_speed'
1933                 })
1934                 .append(
1935                     $('<legend>', { })
1936                     .html("Speed")
1937                 )
1938                 .append(
1939                     $('<input>', {
1940                         type: 'radio',
1941                         id: 'spd_metric',
1942                         name: 'speed',
1943                         value: 'metric',
1944                     })
1945                 )
1946                 .append(
1947                     $('<label>', {
1948                         for: 'spd_metric',
1949                     })
1950                     .html('Metric')
1951                 )
1952                 .append(
1953                     $('<input>', {
1954                         type: 'radio',
1955                         id: 'spd_imperial',
1956                         name: 'speed',
1957                         value: 'imperial',
1958                     })
1959                 )
1960                 .append(
1961                     $('<label>', {
1962                         for: 'spd_imperial',
1963                     })
1964                     .html('Imperial')
1965                 )
1966             )
1967             .append(
1968                 $('<br>', { })
1969             )
1970             .append(
1971                 $('<fieldset>', {
1972                     id: 'set_temp'
1973                 })
1974                 .append(
1975                     $('<legend>', { })
1976                     .html("Temperature")
1977                 )
1978                 .append(
1979                     $('<input>', {
1980                         type: 'radio',
1981                         id: 'temp_celsius',
1982                         name: 'temp',
1983                         value: 'celsius',
1984                     })
1985                 )
1986                 .append(
1987                     $('<label>', {
1988                         for: 'temp_celsius',
1989                     })
1990                     .html('Celsius')
1991                 )
1992                 .append(
1993                     $('<input>', {
1994                         type: 'radio',
1995                         id: 'temp_fahrenheit',
1996                         name: 'temp',
1997                         value: 'fahrenheit',
1998                     })
1999                 )
2000                 .append(
2001                     $('<label>', {
2002                         for: 'temp_fahrenheit',
2003                     })
2004                     .html('Fahrenheit')
2005                 )
2006             )
2007         );
2008
2009         $('#form', elem).on('change', function() {
2010             kismet_ui_settings.SettingsModified();
2011         });
2012
2013         if (kismet.getStorage('kismet.base.unit.distance', 'metric') === 'metric') {
2014             $('#dst_metric', elem).attr('checked', 'checked');
2015         } else {
2016             $('#dst_imperial', elem).attr('checked', 'checked');
2017         }
2018
2019         if (kismet.getStorage('kismet.base.unit.speed', 'metric') === 'metric') {
2020             $('#spd_metric', elem).attr('checked', 'checked');
2021         } else {
2022             $('#spd_imperial', elem).attr('checked', 'checked');
2023         }
2024
2025         if (kismet.getStorage('kismet.base.unit.temp', 'celsius') === 'celsius') {
2026             $('#temp_celsius', elem).attr('checked', 'checked');
2027         } else {
2028             $('#temp_fahrenheit', elem).attr('checked', 'checked');
2029         }
2030
2031         $('#set_distance', elem).controlgroup();
2032         $('#set_speed', elem).controlgroup();
2033         $('#set_temp', elem).controlgroup();
2034
2035     },
2036     save: function(elem) {
2037         var dist = $("input[name='distance']:checked", elem).val();
2038         kismet.putStorage('kismet.base.unit.distance', dist);
2039         var spd = $("input[name='speed']:checked", elem).val();
2040         kismet.putStorage('kismet.base.unit.speed', spd);
2041         var tmp = $("input[name='temp']:checked", elem).val();
2042         kismet.putStorage('kismet.base.unit.temp', tmp);
2043
2044         return true;
2045     },
2046 });
2047
2048 kismet_ui_settings.AddSettingsPane({
2049     id: 'base_plugins',
2050     listTitle: 'Plugins',
2051     create: function(elem) {
2052         elem.append($('<i>').html('Loading plugin data...'));
2053
2054         $.get(local_uri_prefix + "plugins/all_plugins.json")
2055         .done(function(data) {
2056             elem.empty();
2057     
2058             if (data.length == 0) {
2059                 elem.append($('<i>').html('No plugins loaded...'));
2060             }
2061
2062             for (var pi in data) {
2063                 var pl = data[pi];
2064
2065                 var sharedlib = $('<p>');
2066
2067                 if (pl['kismet.plugin.shared_object'].length > 0) {
2068                     sharedlib.html("Native code from " + pl['kismet.plugin.shared_object']);
2069                 } else {
2070                     sharedlib.html("No native code");
2071                 }
2072
2073                 elem.append(
2074                     $('<div>', { 
2075                         class: 'k-b-s-plugin-title',
2076                     })
2077                     .append(
2078                         $('<b>', {
2079                             class: 'k-b-s-plugin-title',
2080                         })
2081                         .html(pl['kismet.plugin.name'])
2082                     )
2083                     .append(
2084                         $('<span>', { })
2085                         .html(pl['kismet.plugin.version'])
2086                     )
2087                 )
2088                 .append(
2089                     $('<div>', {
2090                         class: 'k-b-s-plugin-content',
2091                     })
2092                     .append(
2093                         $('<p>', { })
2094                         .html(pl['kismet.plugin.description'])
2095                     )
2096                     .append(
2097                         $('<p>', { })
2098                         .html(pl['kismet.plugin.author'])
2099                     )
2100                     .append(sharedlib)
2101                 );
2102             }
2103         });
2104     },
2105     save: function(elem) {
2106
2107     },
2108 });
2109
2110
2111 kismet_ui_settings.AddSettingsPane({
2112     id: 'base_login_password',
2113     listTitle: 'Login &amp; Password',
2114     create: function(elem) {
2115         elem.append(
2116             $('<form>', {
2117                 id: 'form'
2118             })
2119             .append(
2120                 $('<fieldset>', {
2121                     id: 'fs_login'
2122                 })
2123                 .append(
2124                     $('<legend>', {})
2125                     .html('Server Login')
2126                 )
2127                 .append(
2128                     $('<p>')
2129                     .html('Kismet requires a username and password for functionality which changes the server, such as adding interfaces or changing configuration, or accessing some types of data.')
2130                 )
2131                 .append(
2132                     $('<p>')
2133                     .html('The Kismet password is stored in <code>~/.kismet/kismet_httpd.conf</code> in the home directory of the user running Kismet.  You will need this password to configure data sources, download pcap and other logs, or change server-side settings.<br>This server is running as <code>' + exports.system_user + '</code>, so the password can be found in <code>~' + exports.system_user + '/.kismet/kismet_httpd.conf</code>')
2134                 )
2135                 .append(
2136                     $('<p>')
2137                     .html('If you are a guest on this server you may continue without entering an admin password, but you will not be able to perform some actions or view some data.')
2138                 )
2139                 .append(
2140                     $('<br>')
2141                 )
2142                 .append(
2143                     $('<span style="display: inline-block; width: 8em;">')
2144                     .html('User name: ')
2145                 )
2146                 .append(
2147                     $('<input>', {
2148                         type: 'text',
2149                         name: 'user',
2150                         id: 'user'
2151                     })
2152                 )
2153                 .append(
2154                     $('<br>')
2155                 )
2156                 .append(
2157                     $('<span style="display: inline-block; width: 8em;">')
2158                     .html('Password: ')
2159                 )
2160                 .append(
2161                     $('<input>', {
2162                         type: 'password',
2163                         name: 'password',
2164                         id: 'password'
2165                     })
2166                 )
2167                 .append(
2168                     $('<span>', {
2169                         id: 'pwsuccessdiv',
2170                         style: 'padding-left: 5px',
2171                     })
2172                     .append(
2173                         $('<i>', {
2174                             id: 'pwsuccess',
2175                             class: 'fa fa-refresh fa-spin',
2176                         })
2177                     )
2178                     .append(
2179                         $('<span>', {
2180                             id: 'pwsuccesstext'
2181                         })
2182                     )
2183                     .hide()
2184                 )
2185             )
2186         );
2187
2188         $('#form', elem).on('change', function() {
2189             kismet_ui_settings.SettingsModified();
2190         });
2191
2192         var checker_cb = function() {
2193             // Cancel any pending timer
2194             if (pw_check_tid > -1)
2195                 clearTimeout(pw_check_tid);
2196
2197             var checkerdiv = $('#pwsuccessdiv', elem);
2198             var checker = $('#pwsuccess', checkerdiv);
2199             var checkertext = $('#pwsuccesstext', checkerdiv);
2200
2201             checker.removeClass('fa-exclamation-circle');
2202             checker.removeClass('fa-check-square');
2203
2204             checker.addClass('fa-spin');
2205             checker.addClass('fa-refresh');
2206             checkertext.text("  Checking...");
2207
2208             checkerdiv.show();
2209
2210             // Set a timer for a second from now to call the actual check 
2211             // in case the user is still typing
2212             pw_check_tid = setTimeout(function() {
2213                 exports.LoginCheck(function(success) {
2214                     if (!success) {
2215                         checker.removeClass('fa-check-square');
2216                         checker.removeClass('fa-spin');
2217                         checker.removeClass('fa-refresh');
2218                         checker.addClass('fa-exclamation-circle');
2219                         checkertext.text("  Invalid login");
2220                     } else {
2221                         checker.removeClass('fa-exclamation-circle');
2222                         checker.removeClass('fa-spin');
2223                         checker.removeClass('fa-refresh');
2224                         checker.addClass('fa-check-square');
2225                         checkertext.text("");
2226                     }
2227                 }, $('#user', elem).val(), $('#password', elem).val());
2228             }, 1000);
2229         };
2230
2231         var pw_check_tid = -1;
2232         jQuery('#password', elem).on('input propertychange paste', function() {
2233             kismet_ui_settings.SettingsModified();
2234             checker_cb();
2235         });
2236         jQuery('#user', elem).on('input propertychange paste', function() {
2237             kismet_ui_settings.SettingsModified();
2238             checker_cb();
2239         });
2240
2241         $('#user', elem).val(kismet.getStorage('kismet.base.login.username', 'kismet'));
2242         $('#password', elem).val(kismet.getStorage('kismet.base.login.password', 'kismet'));
2243
2244         if ($('#user', elem).val() === 'kismet' &&
2245         $('#password', elem).val() === 'kismet') {
2246             $('#defaultwarning').show();
2247         }
2248
2249         $('fs_login', elem).controlgroup();
2250
2251         // Check the current pw
2252         checker_cb();
2253     },
2254     save: function(elem) {
2255         kismet.putStorage('kismet.base.login.username', $('#user', elem).val());
2256         kismet.putStorage('kismet.base.login.password', $('#password', elem).val());
2257     },
2258 });
2259
2260 function show_role_help(role) {
2261     var rolehelp = `Unknown role ${role}; this could be assigned as a custom role for a Kismet plugin.`;
2262
2263     if (role === "admin")
2264         rolehelp = "The admin role is assigned to the primary web interface, external API plugins which automatically request API access, and other privileged instances.  The admin role has access to all endpoints.";
2265     else if (role === "readonly")
2266         rolehelp = "The readonly role has access to any endpoint which does not modify data.  It can not issue commands to the Kismet server, configure sources, or alter devices.  The readonly role is well suited for external data gathering from a Kismet server.";
2267     else if (role === "datasource")
2268         rolehelp = "The datasource role allows remote capture over websockets.  This role only has access to the remote capture datasource endpoint.";
2269     else if (role === "scanreport")
2270         rolehelp = "The scanreport role allows device scan reports.  This role only has access to the scan report endpoint."
2271     else if (role === "ADSB")
2272         rolehelp = "The ADSB role allows access to the combined and device-specific ADSB feeds."
2273     else if (role === "__explain__") {
2274         rolehelp = "<p>Kismet uses a basic role system to restrict access to API endpoints.  The default roles are:";
2275         rolehelp += "<p>&quot;admin&quot; which has access to all API endpoints.";
2276         rolehelp += "<p>&quot;readonly&quot; which only has access to endpoints which do not alter devices or change the configuration of the server";
2277         rolehelp += "<p>&quot;datasource&quot; which is used for websockets based remote capture and may not access any other endpoints";
2278         rolehelp += "<p>&quot;scanreport&quot; which is used for reporting scanning-mode devices";
2279         rolehelp += "<p>&quot;ADSB&quot; which is used for sharing ADSB feeds";
2280         rolehelp += "<p>Plugins or other code may define other roles.";
2281
2282         role = "Kismet API Roles";
2283     }
2284
2285     var h = $(window).height() / 4;
2286     var w = $(window).width() / 2;
2287
2288     if (w < 450) 
2289         w = $(window).width() - 5;
2290
2291     if (h < 200)
2292         h = $(window).height() - 5;
2293
2294     $.jsPanel({
2295         id: "item-help",
2296         headerTitle: `Role: ${role}`,
2297         headerControls: {
2298             controls: 'closeonly',
2299             iconfont: 'jsglyph',
2300         },
2301         contentSize: `${w} auto`,
2302         paneltype: 'modal',
2303         content: `<div style="padding: 10px;"><h3>${role}</h3><p>${rolehelp}`,
2304     })
2305     .reposition({
2306         my: 'center',
2307         at: 'center',
2308         of: 'window'
2309     });
2310 }
2311
2312 function delete_role(rolename, elem) {
2313     var deltd = $('.deltd', elem);
2314
2315     var delbt = 
2316         $('<button>', {
2317             'style': 'background-color: #DDAAAA',
2318         })
2319         .html(`Delete role &quot;${rolename}&quot;`)
2320         .button()
2321         .on('click', function() {
2322             var pd = {
2323                 'name': rolename,
2324             };
2325
2326             var postdata = "json=" + encodeURIComponent(JSON.stringify(pd));
2327
2328             $.post(local_uri_prefix + "auth/apikey/revoke.cmd", postdata)
2329             .done(function(data) {
2330                 var delt = elem.parent();
2331
2332                 elem.remove();
2333
2334                 if ($('tr', delt).length == 1) {
2335                     delt.append(
2336                         $('<tr>', {
2337                             'class': 'noapi'
2338                         })
2339                         .append(
2340                             $('<td>', {
2341                                 'colspan': 4
2342                             })
2343                             .html("<i>No API keys defined...</i>")
2344                         )
2345                     );
2346                 }
2347             })
2348         });
2349
2350     deltd.empty();
2351     deltd.append(delbt);
2352
2353 }
2354
2355 function make_role_help_closure(role) {
2356     return function() { show_role_help(role); };
2357 }
2358
2359 function make_role_delete_closure(rolename, elem) {
2360     return function() { delete_role(rolename, elem); };
2361 }
2362
2363 kismet_ui_settings.AddSettingsPane({
2364     id: 'base_api_logins',
2365     listTitle: "API Keys",
2366     create: function(elem) {
2367         elem.append($("p").html("Fetching API data..."));
2368
2369         $.get(local_uri_prefix + "auth/apikey/list.json")
2370         .done(function(data) {
2371             data = kismet.sanitizeObject(data);
2372             elem.empty();
2373
2374             var tb = $('<table>', {
2375                 'class': 'apitable',
2376                 'id': 'apikeytable',
2377             })
2378
2379             .append(
2380                 $('<tr>')
2381                 .append(
2382                     $('<th>', {
2383                         'class': 'apith',
2384                         'style': 'width: 16em;',
2385                     }).html("Name")
2386                 )
2387                 .append(
2388                     $('<th>', {
2389                         'class': 'apith',
2390                         'style': 'width: 8em;',
2391                     }).html("Role")
2392                 )
2393                 .append(
2394                     $('<th>', {
2395                         'class': 'apith',
2396                         'style': 'width: 30em;',
2397                     }).html("Key")
2398                 )
2399                 .append(
2400                     $('<th>')
2401                 )
2402             );
2403
2404             elem.append(tb);
2405
2406             if (data.length == 0) {
2407                 tb.append(
2408                     $('<tr>', {
2409                         'class': 'noapi'
2410                     })
2411                     .append(
2412                         $('<td>', {
2413                             'colspan': 4
2414                         })
2415                         .html("<i>No API keys defined...</i>")
2416                     )
2417                 );
2418             }
2419
2420             for (var user of data) {
2421                 var name = user['kismet.httpd.auth.name'];
2422                 var role = user['kismet.httpd.auth.role'];
2423
2424                 var key;
2425
2426                 if ('kismet.httpd.auth.token' in user) {
2427                     key = user['kismet.httpd.auth.token'];
2428                 } else {
2429                     key = "<i>Viewing auth tokens is disabled in the Kismet configuration.</i>";
2430                 }
2431
2432                 var tr = 
2433                     $('<tr>', {
2434                         'class': 'apihover'
2435                     });
2436
2437                 tr
2438                     .append(
2439                         $('<td>').html(name)
2440                     )
2441                     .append(
2442                         $('<td>').html(role)
2443                         .append(
2444                             $('<i>', {
2445                                 'class': 'pseudolink fa fa-question-circle',
2446                                 'style': 'padding-left: 5px;',
2447                             })
2448                             .on('click', make_role_help_closure(role))
2449                         )
2450                     )
2451                     .append(
2452                         $('<td>')
2453                         .append(
2454                             $('<input>', {
2455                                 'type': 'text',
2456                                 'value': key,
2457                                 'readonly': 'true',
2458                                 'size': 34,
2459                                 'id': name.replace(" ", "_"),
2460                             })
2461                         )
2462                         .append(
2463                             $('<i>', {
2464                                 'class': 'copyuri pseudolink fa fa-copy',
2465                                 'style': 'padding-left: 5px;',
2466                                 'data-clipboard-target': `#${name.replace(" ", "_")}`, 
2467                             })
2468                         )
2469                     )
2470                     .append(
2471                         $('<td>', {
2472                             'class': 'deltd'
2473                         })
2474                         .append(
2475                             $('<i>', {
2476                                 'class': 'pseudolink fa fa-trash',
2477                             })
2478                             .on('click', make_role_delete_closure(name, tr))
2479                         )
2480                     )
2481
2482                 tb.append(
2483                     tr
2484                 )
2485             }
2486
2487             var adddiv = 
2488                 $('<div>', {
2489                     'id': 'addapidiv'
2490                 })
2491                 .append(
2492                     $('<fieldset>')
2493                     .append(
2494                         $('<button>', {
2495                             'id': 'addapikeybutton',
2496                             'class': 'padded',
2497                         }).html(`<i class="fa fa-plus"> Create API Key`)
2498                     )
2499                     .append(
2500                         $('<label>', {
2501                             'for': 'addapiname',
2502                             'class': 'padded',
2503                         }).html("Name")
2504                     )
2505                     .append(
2506                         $('<input>', {
2507                         'name': 'addapiname',
2508                         'id': 'addapiname',
2509                         'type': 'text',
2510                         'size': 16,
2511                         })
2512                     )
2513                     .append(
2514                         $('<label>', {
2515                             'for': 'addapirole',
2516                             'class': 'padded',
2517                         }).html("Role")
2518                     )
2519                     .append(
2520                         $('<select>', {
2521                             'name': 'addapirole',
2522                             'id': 'addapirole'
2523                         })
2524                         .append(
2525                             $('<option>', {
2526                                 'value': 'readonly',
2527                                 'selected': 'true',
2528                             }).html("readonly")
2529                         )
2530                         .append(
2531                             $('<option>', {
2532                                 'value': 'datasource',
2533                             }).html("datasource")
2534                         )
2535                         .append(
2536                             $('<option>', {
2537                                 'value': 'scanreport',
2538                             }).html("scanreport")
2539                         )
2540                         .append(
2541                             $('<option>', {
2542                                 'value': 'admin',
2543                             }).html("admin")
2544                         )
2545                         .append(
2546                             $('<option>', {
2547                                 'value': 'ADSB',
2548                             }).html("ADSB")
2549                         )
2550                         .append(
2551                             $('<option>', {
2552                                 'value': 'custom',
2553                             }).html("<i>custom</i>")
2554                         )
2555                     )
2556                     .append(
2557                         $('<input>', {
2558                             'name': 'addapiroleother',
2559                             'id': 'addapiroleother',
2560                             'type': 'text',
2561                             'size': 16,
2562                         }).hide()
2563                     )
2564                     .append(
2565                         $('<i>', {
2566                             'class': 'pseudolink fa fa-question-circle',
2567                             'style': 'padding-left: 5px;',
2568                         })
2569                         .on('click', make_role_help_closure("__explain__"))
2570                     )
2571                     .append(
2572                         $('<div>', {
2573                             'id': 'addapierror',
2574                             'style': 'color: red;'
2575                         }).hide()
2576                     )
2577                 );
2578
2579             $('#addapikeybutton', adddiv)
2580                 .button()
2581                 .on('click', function() {
2582                     var name = $('#addapiname').val();
2583                     var role_select = $('#addapirole option:selected').text();
2584                     var role_input = $('#addapiroleother').val();
2585
2586                     if (name.length == 0) {
2587                         $('#addapierror').show().html("Missing name.");
2588                         return;
2589                     }
2590
2591                     if (role_select === "custom" && role_input.length == 0) {
2592                         $('#addapierror').show().html("Missing custom role.");
2593                         return;
2594                     }
2595
2596                     $('#addapierror').hide();
2597
2598                     var role = role_select;
2599
2600                     if (role_select === "custom")
2601                         role = role_input;
2602
2603                     var pd = {
2604                         'name': name,
2605                         'role': role,
2606                         'duration': 0,
2607                     };
2608
2609                     var postdata = "json=" + encodeURIComponent(JSON.stringify(pd));
2610
2611                     $.post(local_uri_prefix + "auth/apikey/generate.cmd", postdata)
2612                     .fail(function(response) {
2613                         var rt = kismet.sanitizeObject(response.responseText);
2614                         $('#addapierror').show().html(`Failed to add API key: ${rt}`);
2615                     })
2616                     .done(function(data) {
2617                         var key = kismet.sanitizeObject(data);
2618
2619                         var tr = 
2620                             $('<tr>', {
2621                                 'class': 'apihover'
2622                             });
2623
2624                         tr
2625                             .append(
2626                                 $('<td>').html(name)
2627                             )
2628                             .append(
2629                                 $('<td>').html(role)
2630                                 .append(
2631                                     $('<i>', {
2632                                         'class': 'pseudolink fa fa-question-circle',
2633                                         'style': 'padding-left: 5px;',
2634                                     })
2635                                     .on('click', make_role_help_closure(role))
2636                                 )
2637                             )
2638                             .append(
2639                                 $('<td>')
2640                                 .append(
2641                                     $('<input>', {
2642                                         'type': 'text',
2643                                         'value': key,
2644                                         'readonly': 'true',
2645                                         'size': 34,
2646                                         'id': name.replace(" ", "_"),
2647                                     })
2648                                 )
2649                                 .append(
2650                                     $('<i>', {
2651                                         'class': 'copyuri pseudolink fa fa-copy',
2652                                         'style': 'padding-left: 5px;',
2653                                         'data-clipboard-target': `#${name.replace(" ", "_")}`, 
2654                                     })
2655                                 )
2656                             )
2657                             .append(
2658                                 $('<td>', {
2659                                     'class': 'deltd'
2660                                 })
2661                                 .append(
2662                                     $('<i>', {
2663                                         'class': 'pseudolink fa fa-trash',
2664                                     })
2665                                     .on('click', make_role_delete_closure(name, tr))
2666                                 )
2667                             );
2668
2669                         $('#apikeytable').append(tr);
2670
2671                         $('#addapiname').val('');
2672                         $("#addapirole").prop("selectedIndex", 0);
2673                         $("#addapirole").show();
2674                         $("#addapiroleother").val('').hide();
2675                     });
2676                 });
2677
2678             $('#addapirole', adddiv).on('change', function(e) {
2679                 var val = $("#addapirole option:selected" ).text();
2680
2681                 if (val === "custom") {
2682                     $(this).hide();
2683                     $('#addapiroleother').show();
2684                 }
2685
2686             });
2687
2688             elem.append(adddiv);
2689
2690             new ClipboardJS('.copyuri');
2691         });
2692     },
2693     save: function(elem) {
2694         return true;
2695     },
2696 });
2697
2698
2699
2700 /* Add the messages and channels tabs */
2701 kismet_ui_tabpane.AddTab({
2702     id: 'messagebus',
2703     tabTitle: 'Messages',
2704     createCallback: function(div) {
2705         div.messagebus();
2706     },
2707     priority: -1001,
2708 }, 'south');
2709
2710 kismet_ui_tabpane.AddTab({
2711     id: 'channels',
2712     tabTitle: 'Channels',
2713     expandable: true,
2714     createCallback: function(div) {
2715         div.channels();
2716     },
2717     priority: -1000,
2718 }, 'south');
2719
2720 kismet_ui_tabpane.AddTab({
2721     id: 'devices',
2722     tabTitle: 'Devices',
2723     expandable: false,
2724     createCallback: function(div) {
2725         div.append(
2726             $('<div>', {
2727                 class: 'resize_wrapper',
2728             })
2729             .append(
2730                 $('<table>', {
2731                     id: 'devices',
2732                     class: 'fixeddt stripe hover nowrap',
2733                     'cell-spacing': 0,
2734                     width: '100%',
2735                 })
2736             )
2737         ).append(
2738             $('<div>', {
2739                 id: 'devices_status',
2740                 style: 'padding-bottom: 10px;',
2741             })
2742         );
2743
2744         kismet_ui.CreateDeviceTable($('#devices', div));
2745     },
2746     priority: -1000000,
2747 }, 'center');
2748
2749
2750 exports.DeviceSignalDetails = function(key) {
2751     var w = $(window).width() * 0.75;
2752     var h = $(window).height() * 0.5;
2753
2754     var devsignal_chart = null;
2755
2756     var devsignal_tid = -1;
2757
2758     var content =
2759         $('<div>', {
2760             class: 'k-dsd-container'
2761         })
2762         .append(
2763             $('<div>', {
2764                 class: 'k-dsd-info'
2765             })
2766             .append(
2767                 $('<div>', {
2768                     class: 'k-dsd-title'
2769                 })
2770                 .html("Signal")
2771             )
2772             .append(
2773                 $('<table>', {
2774                     class: 'k-dsd-table'
2775                 })
2776                 .append(
2777                     $('<tr>', {
2778                     })
2779                     .append(
2780                         $('<td>', {
2781                             width: '50%'
2782                         })
2783                         .html("Last Signal:")
2784                     )
2785                     .append(
2786                         $('<td>', {
2787                             width: '50%',
2788                         })
2789                         .append(
2790                             $('<span>', {
2791                                 class: 'k-dsd-lastsignal',
2792                             })
2793                         )
2794                         .append(
2795                             $('<i>', {
2796                                 class: 'fa k-dsd-arrow k-dsd-arrow-down',
2797                             })
2798                             .hide()
2799                         )
2800                     )
2801                 )
2802                 .append(
2803                     $('<tr>', {
2804                     })
2805                     .append(
2806                         $('<td>', {
2807                             width: '50%'
2808                         })
2809                         .html("Min Signal:")
2810                     )
2811                     .append(
2812                         $('<td>', {
2813                             width: '50%',
2814                             class: 'k-dsd-minsignal',
2815                         })
2816                         .html("n/a")
2817                     )
2818                 )
2819                 .append(
2820                     $('<tr>', {
2821                     })
2822                     .append(
2823                         $('<td>', {
2824                             width: '50%'
2825                         })
2826                         .html("Max Signal:")
2827                     )
2828                     .append(
2829                         $('<td>', {
2830                             width: '50%',
2831                             class: 'k-dsd-maxsignal',
2832                         })
2833                         .html("n/a")
2834                     )
2835                 )
2836             )
2837         )
2838         .append(
2839             $('<div>', {
2840                 class: 'k-dsd-graph'
2841             })
2842             .append(
2843                 $('<canvas>', {
2844                     id: 'k-dsd-canvas',
2845                     class: 'k-dsd-canvas'
2846                 })
2847             )
2848         );
2849
2850     var devsignal_panel = $.jsPanel({
2851         id: 'devsignal' + key,
2852         headerTitle: '<i class="fa fa-signal" /> Signal',
2853         headerControls: {
2854             iconfont: 'jsglyph',
2855         },
2856         content: content,
2857         onclosed: function() {
2858             clearTimeout(devsignal_tid);
2859         }
2860     }).resize({
2861         width: w,
2862         height: h
2863     }).reposition({
2864         my: 'center-top',
2865         at: 'center-top',
2866         of: 'window',
2867         offsetY: 20
2868     });
2869
2870     var emptyminute = new Array();
2871     for (var x = 0; x < 60; x++) {
2872         emptyminute.push(0);
2873     }
2874
2875     devsignal_tid = devsignal_refresh(key, devsignal_panel,
2876         devsignal_chart, devsignal_tid, 0, emptyminute);
2877 }
2878
2879 function devsignal_refresh(key, devsignal_panel, devsignal_chart,
2880     devsignal_tid, lastsignal, fakerrd) {
2881     clearTimeout(devsignal_tid);
2882
2883     if (devsignal_panel == null)
2884         return;
2885
2886     if (devsignal_panel.is(':hidden'))
2887         return;
2888
2889     var signal = lastsignal;
2890
2891     $.get(local_uri_prefix + "devices/by-key/" + key + "/device.json")
2892     .done(function(data) {
2893         var title = '<i class="fa fa-signal" /> Signal ' +
2894             kismet.censorMAC(data['kismet.device.base.macaddr']) + ' ' +
2895             kismet.censorMAC(data['kismet.device.base.name']);
2896         devsignal_panel.headerTitle(title);
2897
2898         var sigicon = $('.k-dsd-arrow', devsignal_panel.content);
2899
2900         sigicon.removeClass('k-dsd-arrow-up');
2901         sigicon.removeClass('k-dsd-arrow-down');
2902         sigicon.removeClass('fa-arrow-up');
2903         sigicon.removeClass('fa-arrow-down');
2904
2905         signal = data['kismet.device.base.signal']['kismet.common.signal.last_signal'];
2906
2907         if (signal < lastsignal) {
2908             sigicon.addClass('k-dsd-arrow-down');
2909             sigicon.addClass('fa-arrow-down');
2910             sigicon.show();
2911         } else {
2912             sigicon.addClass('k-dsd-arrow-up');
2913             sigicon.addClass('fa-arrow-up');
2914             sigicon.show();
2915         }
2916
2917         var typestr = "";
2918         if (data['kismet.device.base.signal']['kismet.common.signal.type'] == "dbm")
2919             typestr = " dBm";
2920         else if (data['kismet.device.base.signal']['kismet.common.signal.type'] == "rssi") 
2921             typestr = " RSSI";
2922
2923         $('.k-dsd-lastsignal', devsignal_panel.content)
2924             .text(signal + typestr);
2925
2926         $('.k-dsd-minsignal', devsignal_panel.content)
2927         .text(data['kismet.device.base.signal']['kismet.common.signal.min_signal'] + typestr);
2928
2929         $('.k-dsd-maxsignal', devsignal_panel.content)
2930         .text(data['kismet.device.base.signal']['kismet.common.signal.max_signal'] + typestr);
2931
2932         // Common point titles
2933         var pointtitles = new Array();
2934
2935         for (var x = 60; x > 0; x--) {
2936             if (x % 5 == 0) {
2937                 pointtitles.push(x + 's');
2938             } else {
2939                 pointtitles.push(' ');
2940             }
2941         }
2942
2943
2944         /*
2945         var rrdata = kismet.RecalcRrdData(
2946             data['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.last_time'],
2947             data['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.last_time'],
2948             kismet.RRD_SECOND,
2949             data['kismet.device.base.signal']['kismet.common.signal.signal_rrd']['kismet.common.rrd.minute_vec'], {});
2950
2951         // We assume the 'best' a signal can usefully be is -20dbm,
2952         // that means we're right on top of it.
2953         // We can assume that -100dbm is a sane floor value for
2954         // the weakest signal.
2955         // If a signal is 0 it means we haven't seen it at all so
2956         // just ignore that data point
2957         // We turn signals into a 'useful' graph by clamping to
2958         // -100 and -20 and then scaling it as a positive number.
2959         var moddata = new Array();
2960
2961         for (var x = 0; x < rrdata.length; x++) {
2962             var d = rrdata[x];
2963
2964             if (d == 0) {
2965                 moddata.push(0);
2966                 continue;
2967             }
2968
2969             if (d < -100)
2970                 d = -100;
2971
2972             if (d > -20)
2973                 d = -20;
2974
2975             // Normalize to 0-80
2976             d = (d * -1) - 20;
2977
2978             // Reverse (weaker is worse), get as percentage
2979             var rs = (80 - d) / 80;
2980
2981             moddata.push(100*rs);
2982         }
2983         */
2984
2985         var msignal = signal;
2986
2987         if (msignal == 0) {
2988             fakerrd.push(0);
2989         } else if (msignal < -100) {
2990             msignal = -100;
2991         } else if (msignal > -20) {
2992             msignal = -20;
2993         }
2994
2995         msignal = (msignal * -1) - 20;
2996         var rs = (80 - msignal) / 80;
2997
2998         fakerrd.push(100 * rs);
2999
3000         fakerrd.splice(0, 1);
3001
3002         var moddata = fakerrd;
3003
3004         var datasets = [
3005             {
3006                 label: 'Signal (%)',
3007                 fill: 'false',
3008                 borderColor: 'blue',
3009                 backgroundColor: 'rgba(100, 100, 255, 0.83)',
3010                 data: moddata,
3011             },
3012         ];
3013
3014         if (devsignal_chart == null) {
3015             var canvas = $('#k-dsd-canvas', devsignal_panel.content);
3016
3017             devsignal_chart = new Chart(canvas, {
3018                 type: 'bar',
3019                 options: {
3020                     responsive: true,
3021                     maintainAspectRatio: false,
3022                     animation: false,
3023                     scales: {
3024                         yAxes: [ {
3025                             ticks: {
3026                                 beginAtZero: true,
3027                                 max: 100,
3028                             }
3029                         }],
3030                     },
3031                 },
3032                 data: {
3033                     labels: pointtitles,
3034                     datasets: datasets
3035                 }
3036             });
3037         } else {
3038             devsignal_chart.data.datasets[0].data = moddata;
3039             devsignal_chart.update();
3040         }
3041
3042
3043     })
3044     .always(function() {
3045         devsignal_tid = setTimeout(function() {
3046                 devsignal_refresh(key, devsignal_panel,
3047                     devsignal_chart, devsignal_tid, signal, fakerrd);
3048         }, 1000);
3049     });
3050 };
3051
3052 exports.login_error = false;
3053 exports.login_pending = false;
3054
3055 exports.ProvisionedPasswordCheck = function(cb) {
3056     $.ajax({
3057         url: local_uri_prefix + "session/check_setup_ok",
3058
3059         error: function(jqXHR, textStatus, errorThrown) {
3060             cb(jqXHR.status);
3061         },
3062
3063         success: function(data, textStatus, jqHXR) {
3064             cb(200);
3065         },
3066     });
3067 }
3068
3069 exports.LoginCheck = function(cb, user, pw) {
3070     user = user || kismet.getStorage('kismet.base.login.username', 'kismet');
3071     pw = pw || kismet.getStorage('kismet.base.login.password', '');
3072
3073     $.ajax({
3074         url: local_uri_prefix + "session/check_login",
3075
3076         beforeSend: function (xhr) {
3077             xhr.setRequestHeader ("Authorization", "Basic " + btoa(user + ":" + pw));
3078         },
3079
3080         xhrFields: {
3081             withCredentials: false
3082         },
3083
3084         error: function(jqXHR, textStatus, errorThrown) {
3085             cb(false);
3086         },
3087
3088         success: function(data, textStatus, jqXHR) {
3089             cb(true);
3090         }
3091
3092     });
3093 }
3094
3095 exports.FirstLoginCheck = function(first_login_done_cb) {
3096     var loginpanel = null; 
3097     var username_deferred = $.Deferred();
3098
3099     $.get(local_uri_prefix + "system/user_status.json")
3100         .done(function(data) {
3101             username_deferred.resolve(data['kismet.system.user']);
3102         })
3103         .fail(function() {
3104             username_deferred.resolve("[unknown]");
3105         });
3106
3107     var username = "[incomplete]";
3108     $.when(username_deferred).done(function(v) {
3109
3110     username = v;
3111
3112     var required_login_content = 
3113     $('<div>', {
3114         style: 'padding: 10px;'
3115     })
3116     .append(
3117         $('<p>')
3118         .html('Kismet requires a login to access data.')
3119     )
3120     .append(
3121         $('<p>')
3122         .html('Your login is stored in in <code>.kismet/kismet_httpd.conf</code> in the <i>home directory of the user who launched Kismet</i>;  This server is running as ' + username + ', and the login will be saved in <code>~' + username + '/.kismet/kismet_httpd.conf</code>.')
3123     )
3124     .append(
3125         $('<div>', {
3126             id: 'form'
3127         })
3128         .append(
3129             $('<fieldset>', {
3130                 id: 'fs_login'
3131             })
3132             .append(
3133                 $('<span style="display: inline-block; width: 8em;">')
3134                 .html('User name: ')
3135             )
3136             .append(
3137                 $('<input>', {
3138                     type: 'text',
3139                     name: 'user',
3140                     id: 'req_user'
3141                 })
3142             )
3143             .append(
3144                 $('<br>')
3145             )
3146             .append(
3147                 $('<span style="display: inline-block; width: 8em;">')
3148                 .html('Password: ')
3149             )
3150             .append(
3151                 $('<input>', {
3152                     type: 'password',
3153                     name: 'password',
3154                     id: 'req_password'
3155                 })
3156             )
3157             .append(
3158                 $('<div>', {
3159                     style: 'padding-top: 10px;'
3160                 })
3161                 .append(
3162                     $('<button>', {
3163                         class: 'k-wl-button-close',
3164                         id: 'login_button',
3165                     })
3166                     .text('Log in')
3167                     .button()
3168                 )
3169                 .append(
3170                     $('<span>', {
3171                         id: 'pwsuccessdiv',
3172                         style: 'padding-left: 5px',
3173                     })
3174                     .append(
3175                         $('<i>', {
3176                             id: 'pwsuccess',
3177                             class: 'fa fa-refresh fa-spin',
3178                         })
3179                     )
3180                     .append(
3181                         $('<span>', {
3182                             id: 'pwsuccesstext'
3183                         })
3184                     )
3185                     .hide()
3186                 )
3187             )
3188         )
3189     );
3190
3191     var login_checker_cb = function(content) {
3192         var savebutton = $('#login_button', content);
3193
3194         var checkerdiv = $('#pwsuccessdiv', content);
3195         var checker = $('#pwsuccess', checkerdiv);
3196         var checkertext = $('#pwsuccesstext', checkerdiv);
3197
3198         checker.removeClass('fa-exclamation-circle');
3199         checker.removeClass('fa-check-square');
3200
3201         checker.addClass('fa-spin');
3202         checker.addClass('fa-refresh');
3203         checkertext.text("  Checking...");
3204
3205         checkerdiv.show();
3206
3207         exports.LoginCheck(function(success) {
3208             if (!success) {
3209                 checker.removeClass('fa-check-square');
3210                 checker.removeClass('fa-spin');
3211                 checker.removeClass('fa-refresh');
3212                 checker.addClass('fa-exclamation-circle');
3213                 checkertext.text("  Invalid login");
3214             } else {
3215                 /* Save the login info */
3216                 kismet.putStorage('kismet.base.login.username', $('#req_user', content).val());
3217                 kismet.putStorage('kismet.base.login.password', $('#req_password', content).val());
3218
3219                 loginpanel.close();
3220
3221                 /* Call the primary callback */
3222                 first_login_done_cb();
3223             }
3224         }, $('#req_user', content).val(), $('#req_password', content).val());
3225     };
3226
3227     $('#login_button', required_login_content)
3228         .button()
3229         .on('click', function() {
3230             login_checker_cb(required_login_content);
3231         });
3232
3233     $('fs_login', required_login_content).controlgroup();
3234
3235     var set_password_content = 
3236     $('<div>', {
3237         style: 'padding: 10px;'
3238     })
3239     .append(
3240         $('<p>')
3241         .html('To finish setting up Kismet, you need to configure a login.')
3242     )
3243     .append(
3244         $('<p>')
3245         .html('This login will be stored in <code>.kismet/kismet_httpd.conf</code> in the <i>home directory of the user who launched Kismet</i>;  This server is running as ' + username + ', and the login will be saved in <code>~' + username + '/.kismet/kismet_httpd.conf</code>.')
3246     )
3247     .append(
3248         $('<div>', {
3249             id: 'form'
3250         })
3251         .append(
3252             $('<fieldset>', {
3253                 id: 'fs_login'
3254             })
3255             .append(
3256                 $('<legend>', {})
3257                 .html('Set Login')
3258             )
3259             .append(
3260                 $('<span style="display: inline-block; width: 8em;">')
3261                 .html('User name: ')
3262             )
3263             .append(
3264                 $('<input>', {
3265                     type: 'text',
3266                     name: 'user',
3267                     id: 'user'
3268                 })
3269             )
3270             .append(
3271                 $('<br>')
3272             )
3273             .append(
3274                 $('<span style="display: inline-block; width: 8em;">')
3275                 .html('Password: ')
3276             )
3277             .append(
3278                 $('<input>', {
3279                     type: 'password',
3280                     name: 'password',
3281                     id: 'password'
3282                 })
3283             )
3284             .append(
3285                 $('<br>')
3286             )
3287             .append(
3288                 $('<span style="display: inline-block; width: 8em;">')
3289                 .html('Confirm: ')
3290             )
3291             .append(
3292                 $('<input>', {
3293                     type: 'password',
3294                     name: 'password2',
3295                     id: 'password2'
3296                 })
3297             )
3298             .append(
3299                 $('<span>', {
3300                     id: 'pwsuccessdiv',
3301                     style: 'padding-left: 5px',
3302                 })
3303                 .append(
3304                     $('<i>', {
3305                         id: 'pwsuccess',
3306                         class: 'fa fa-refresh fa-spin',
3307                     })
3308                 )
3309                 .append(
3310                     $('<span>', {
3311                         id: 'pwsuccesstext'
3312                     })
3313                 )
3314                 .hide()
3315             )
3316         )
3317     )
3318     .append(
3319         $('<div>', {
3320             style: 'padding-top: 10px;'
3321         })
3322         .append(
3323             $('<button>', {
3324                 class: 'k-wl-button-close',
3325                 id: 'save_password',
3326             })
3327             .text('Save')
3328             .button()
3329         )
3330     );
3331
3332     var checker_cb = function(content) {
3333         var savebutton = $('#save_password', content);
3334         var checkerdiv = $('#pwsuccessdiv', content);
3335         var checker = $('#pwsuccess', checkerdiv);
3336         var checkertext = $('#pwsuccesstext', checkerdiv);
3337
3338         savebutton.button("disable");
3339
3340         checker.removeClass('fa-exclamation-circle');
3341         checker.removeClass('fa-check-square');
3342
3343         checker.addClass('fa-spin');
3344         checker.addClass('fa-refresh');
3345         checkertext.text("");
3346
3347         checkerdiv.show();
3348
3349         if ($('#user', content).val().length == 0) {
3350             checker.removeClass('fa-check-square');
3351             checker.removeClass('fa-spin');
3352             checker.removeClass('fa-refresh');
3353             checker.addClass('fa-exclamation-circle');
3354             checkertext.text("  Username required");
3355             savebutton.button("disable");
3356             return;
3357         }
3358
3359         if ($('#password', content).val().length == 0) {
3360             checker.removeClass('fa-check-square');
3361             checker.removeClass('fa-spin');
3362             checker.removeClass('fa-refresh');
3363             checker.addClass('fa-exclamation-circle');
3364             checkertext.text("  Password required");
3365             savebutton.button("disable");
3366             return;
3367         }
3368
3369         if ($('#password', content).val() != $('#password2', content).val()) {
3370             checker.removeClass('fa-check-square');
3371             checker.removeClass('fa-spin');
3372             checker.removeClass('fa-refresh');
3373             checker.addClass('fa-exclamation-circle');
3374             checkertext.text("  Passwords don't match");
3375             savebutton.button("disable");
3376             return;
3377         }
3378
3379         checker.removeClass('fa-exclamation-circle');
3380         checker.removeClass('fa-spin');
3381         checker.removeClass('fa-refresh');
3382         checker.addClass('fa-check-square');
3383         checkertext.text("");
3384         savebutton.button("enable");
3385
3386     };
3387
3388     jQuery('#user', set_password_content).on('input propertychange paste', function() {
3389         checker_cb();
3390     });
3391     jQuery('#password', set_password_content).on('input propertychange paste', function() {
3392         checker_cb();
3393     });
3394     jQuery('#password2', set_password_content).on('input propertychange paste', function() {
3395         checker_cb();
3396     });
3397
3398     $('#save_password', set_password_content)
3399         .button()
3400         .on('click', function() {
3401             kismet.putStorage('kismet.base.login.username', $('#user', set_password_content).val());
3402             kismet.putStorage('kismet.base.login.password', $('#password', set_password_content).val());
3403
3404             var postdata = {
3405                 "username": $('#user', set_password_content).val(),
3406                 "password": $('#password', set_password_content).val()
3407             };
3408
3409             $.ajax({
3410                 type: "POST",
3411                 url: local_uri_prefix + "session/set_password",
3412                 data: postdata,
3413                 error: function(jqXHR, textStatus, errorThrown) {
3414                     alert("Could not set login, check your kismet server logs.")
3415                 },
3416             });
3417
3418             loginpanel.close();
3419
3420             /* Call the primary callback to load the UI */
3421             first_login_done_cb();
3422
3423             /* Check for the first-time running */
3424             exports.FirstTimeCheck();
3425         });
3426
3427     $('fs_login', set_password_content).controlgroup();
3428
3429     checker_cb(set_password_content);
3430
3431     var w = ($(window).width() / 2) - 5;
3432     if (w < 450) {
3433         w = $(window).width() - 5;
3434     }
3435
3436     var content = set_password_content;
3437
3438     exports.ProvisionedPasswordCheck(function(code) {
3439         if (code == 200 || code == 406) {
3440             /* Initial setup has been complete, now check the login itself */
3441             exports.LoginCheck(function(success) {
3442                 if (!success) {
3443                     loginpanel = $.jsPanel({
3444                         id: "login-alert",
3445                         headerTitle: '<i class="fa fa-exclamation-triangle"></i>Login Required',
3446                         headerControls: {
3447                             controls: 'closeonly',
3448                             iconfont: 'jsglyph',
3449                         },
3450                         contentSize: w + " auto",
3451                         paneltype: 'modal',
3452                         content: required_login_content,
3453                     });
3454
3455                     return true;
3456                 } else {
3457                     /* Otherwise we're all good, continue to loading the main UI via the callback */
3458                     first_login_done_cb();
3459                 }
3460             });
3461         } else if (code == 500) {
3462             loginpanel = $.jsPanel({
3463                 id: "login-alert",
3464                 headerTitle: '<i class="fa fa-exclamation-triangle"></i> Set Login',
3465                 headerControls: {
3466                     controls: 'closeonly',
3467                     iconfont: 'jsglyph',
3468                 },
3469                 contentSize: w + " auto",
3470                 paneltype: 'modal',
3471                 content: set_password_content,
3472             });
3473
3474             return true;
3475         } else {
3476             loginpanel = $.jsPanel({
3477                 id: "login-alert",
3478                 headerTitle: '<i class="fa fa-exclamation-triangle"></i> Error connecting',
3479                 headerControls: {
3480                     controls: 'closeonly',
3481                     iconfont: 'jsglyph',
3482                 },
3483                 contentSize: w + " auto",
3484                 paneltype: 'modal',
3485                 content: "Error connecting to Kismet and checking provisioning; try reloading the page!",
3486             });
3487
3488
3489         }
3490    });
3491
3492    // When clause
3493    });
3494 }
3495
3496 exports.FirstTimeCheck = function() {
3497     var welcomepanel = null; 
3498     if (kismet.getStorage('kismet.base.seen_welcome', false) == false) {
3499         var content = 
3500             $('<div>', {
3501                 style: 'padding: 10px;'
3502             })
3503             .append(
3504                 $('<p>', { }
3505                 )
3506                 .html("Welcome!")
3507             )
3508             .append(
3509                 $('<p>')
3510                 .html('This is the first time you\'ve used this Kismet server in this browser.')
3511             )
3512             .append(
3513                 $('<p>')
3514                 .html('Kismet stores local settings in the HTML5 storage of your browser.')
3515             )
3516             .append(
3517                 $('<p>')
3518                 .html('You should configure your preferences and login settings in the settings panel!')
3519             )
3520             .append(
3521                 $('<div>', {})
3522                 .append(
3523                     $('<button>', {
3524                         class: 'k-w-button-settings'
3525                     })
3526                     .text('Settings')
3527                     .button()
3528                     .on('click', function() {
3529                         welcomepanel.close();               
3530                         kismet_ui_settings.ShowSettings();
3531                     })
3532                 )
3533                 .append(
3534                     $('<button>', {
3535                         class: 'k-w-button-close',
3536                         style: 'position: absolute; right: 5px;',
3537                     })
3538                     .text('Continue')
3539                     .button()
3540                     .on('click', function() {
3541                         welcomepanel.close();
3542                     })
3543                 )
3544
3545             );
3546
3547         welcomepanel = $.jsPanel({
3548             id: "welcome-alert",
3549             headerTitle: '<i class="fa fa-power-off"></i> Welcome',
3550             headerControls: {
3551                 controls: 'closeonly',
3552                 iconfont: 'jsglyph',
3553             },
3554             contentSize: "auto auto",
3555             paneltype: 'modal',
3556             content: content,
3557         });
3558
3559         kismet.putStorage('kismet.base.seen_welcome', true);
3560
3561         return true;
3562     }
3563
3564     return false;
3565 }
3566
3567 // Keep trying to fetch the servername until we're able to
3568 var servername_tid = -1;
3569 exports.FetchServerName = function(cb) {
3570     $.get(local_uri_prefix + "system/status.json")
3571         .done(function (d) {
3572             d = kismet.sanitizeObject(d);
3573             cb(d['kismet.system.server_name']);
3574         })
3575         .fail(function () {
3576             servername_tid = setTimeout(function () {
3577                 exports.FetchServerName(cb);
3578             }, 1000);
3579         });
3580 }
3581
3582 /* Highlight active devices */
3583 kismet_ui.AddDeviceRowHighlight({
3584     name: "Active",
3585     description: "Device has been active in the past 10 seconds",
3586     priority: 500,
3587     defaultcolor: "#cee1ff",
3588     defaultenable: false,
3589     fields: [
3590         'kismet.device.base.last_time'
3591     ],
3592     selector: function(data) {
3593         var ts = data['kismet.device.base.last_time'];
3594
3595         return (kismet.timestamp_sec - ts < 10);
3596     }
3597 });
3598
3599 /* Bodycam hardware of various types */
3600 kismet_ui.AddDeviceRowHighlight({
3601     name: "Bodycams",
3602     description: "Body camera devices",
3603     priority: 500,
3604     defaultcolor: "#0089FF",
3605     defaultenable: true,
3606     fields: [
3607         'kismet.device.base.macaddr',
3608         'kismet.device.base.commonname',
3609     ],
3610     selector: function(data) {
3611         try {
3612             if (data['kismet.device.base.macaddr'].match("^00:25:DF") != null)
3613                 return true;
3614             if (data['kismet.device.base.macaddr'].match("^12:20:13") != null)
3615                 return true;
3616             if (data['kismet.device.base.common_name'].match("^Axon-X") != null)
3617                 return true;
3618         } catch (e) {
3619             return false;
3620         }
3621
3622         return false;
3623     }
3624 });
3625
3626 // We're done loading
3627 exports.load_complete = 1;
3628
3629 return exports;
3630
3631 });