Blame | Last modification | View Log
var enable_graph = false,
config,
dygraph,
dygraph_config,
dygraph_data,
dygraph_rangeselector_active,
dygraph_daterange,
dygraph_did_zoom,
footable_data,
date_range,
date_range_interval,
api_last_query,
api_graph_options,
api_flows_options,
api_statistics_options,
nfdump_translation = {ff: 'flow record flags in hex', ts: 'Start Time - first seen', te: 'End Time - last seen', tr: 'Time the flow was received by the collector', td: 'Duration', pr: 'Protocol', exp: 'Exporter ID', eng: 'Engine Type/ID', sa: 'Source Address', da: 'Destination Address', sap: 'Source Address:Port', dap: 'Destination Address:Port', sp: 'Source Port', dp: 'Destination Port', sn: 'Source Network (mask applied)', dn: 'Destination Network
(mask applied)', nh: 'Next-hop IP Address', nhb: 'BGP Next-hop IP Address', ra: 'Router IP Address', sas: 'Source AS', das: 'Destination AS', nas: 'Next AS', pas: 'Previous AS', in: 'Input Interface num', out: 'Output Interface num', pkt: 'Packets - default input', ipkt: 'Input Packets', opkt: 'Output Packets', byt: 'Bytes - default input', ibyt: 'Input Bytes', obyt: 'Output Bytes', fl: 'Flows', f
lg: 'TCP Flags', tos: 'Tos - default src', stos: 'Src Tos', dtos: 'Dst Tos', dir: 'Direction: ingress, egress', smk: 'Src mask', dmk: 'Dst mask', fwd: 'Forwarding Status', svln: 'Src vlan label', dvln: 'Dst vlan label', ismc: 'Input Src Mac Addr', odmc: 'Output Dst Mac Addr', idmc: 'Input Dst Mac Addr', osmc: 'Output Src Mac Addr', pps: 'Packets per second', bps: 'Bytes per second', bpp: 'Bytes per packet',
flP: 'Flows (%)', ipktP: 'Input Packets (%)', opktP: 'Output Packets (%)', ibytP: 'Input Bytes (%)', obytP: 'Output Bytes (%)', ipps: 'Input Packets/s', ibps: 'Input Bytes/s', ibpp: 'Input Bytes/Packet', pktP: 'Packets (%)', bytP: 'Bytes (%)'},
views_view_status = {graphs: false, flows: false, statistics: false},
ip_link_handler = (a) => {
const ip = a.innerHTML;
const ignoredFields = ['country_', 'timezone_', 'currency_'];
const checkIp = async (ip) => {
const ipWhoisResponse = await fetch('https://ipwhois.app/json/' + ip);
const ipWhoisData = await ipWhoisResponse.json();
const hostResponse = await fetch('../api/host/?ip=' + ip);
const hostData = (!hostResponse.ok) ? 'IP could not be resolved' : await hostResponse.json();
return {
ipWhoisData: ipWhoisData,
hostData: hostData
}
}
const modal = new bootstrap.Modal('#modal', {});
const modalTitle = document.querySelector('#modal .modal-title');
const modalBody = document.querySelector('#modal .modal-body');
const modalLoader = document.querySelector('#modal .modal-loader');
modalBody.innerHTML = modalLoader.outerHTML;
modalBody.querySelector('.modal-loader').classList.remove('d-none');
modalTitle.innerHTML = 'Info for IP: ' + ip;
modal.show();
// make request and display data
checkIp(ip).then((data) => {
console.log(data);
// create table
let markup = '<table class="table table-striped">';
for (const [key, value] of Object.entries(data.ipWhoisData)) {
// if key starts with any of ignoredFields values, skip it
if (ignoredFields.some(field => key.startsWith(field))) continue;
markup += '<tr><th>' + key + '</th><td>' + value + '</td></tr>';
}
markup += '</table>';
// add heading and flag
let flag = data.ipWhoisData.country_flag ? '<img src="' + data.ipWhoisData.country_flag + '" alt="' + data.ipWhoisData.country + '" title="' + data.ipWhoisData.country + '" style="width: 3rem" />' : '';
let heading = '<h3>' + ip + ' ' + flag + '</h3>';
heading += '<h4>Host: ' + data.hostData + '</h4>';
// replace loader with content
modalBody.innerHTML = heading + markup;
});
};
$(document).ready(function() {
/**
* get config from backend
* example data:
*
* config object {
* "sources": ["gate", "swi6"],
* "ports": [ 80, 23, 22 ],
* "stored_output_formats": [],
* "stored_filters": [],
* "daemon_running": true,
* }
*/
$.get('../api/config', function(data, status) {
if (status === 'success') {
config = data;
init();
if (config.daemon_running === true) {
var reload_seconds = 60;
if (typeof config.frontend.reload_interval !== 'undefined') reload_seconds = config.frontend.reload_interval;
display_message('info', 'Daemon is running, graph is reloading each ' + ((reload_seconds === 60) ? 'minute' : reload_seconds + ' seconds') + '.');
date_range_interval = setInterval(function() {
if (date_range.options.max === date_range.options.to) {
var now = new Date();
date_range.update({ max: now.getTime(), to: now.getTime() });
}
}, reload_seconds*1000);
}
} else {
display_message('danger', 'Error getting the config!')
}
});
/**
* general ajax error handler
*/
$(document).on('ajaxError', function(e, jqXHR) {
console.log(jqXHR);
if (typeof jqXHR === 'undefined') {
display_message('danger', 'General error, please file a ticket on github!');
} else if (typeof jqXHR.responseJSON === 'undefined') {
display_message('danger', 'General error: ' + jqXHR.responseText);
} else {
display_message('danger', 'Got ' + jqXHR.responseJSON.error);
}
});
/**
* navigation functionality
* show/hides the correct containers, which are identified by the data-view attribute
*/
$(document).on('click', 'header li a', function(e) {
e.preventDefault();
var view = $(this).attr('data-view');
var $filter = $('#filter').find('[data-view]');
var $content = $('#contentDiv').find('div.content');
$('header li a').removeClass('active');
$(this).addClass('active');
var showDivs = function(id, el) {
if ($(el).attr('data-view').indexOf(view) !== -1) $(el).removeClass('d-none');
else $(el).addClass('d-none');
};
// show the right divs
$filter.each(showDivs);
$content.each(showDivs);
// re-initialize form
if (view === 'graphs') $('#filterDisplaySelect').trigger('change');
if (view === 'flows') $('#statsFilterForSelection').val('record').trigger('change');
// trigger resize for the graph
if (typeof dygraph !== 'undefined') dygraph.resize();
// set defaults for the view
init_defaults(view);
// set view state to true
views_view_status[view] = true;
});
/**
* home-button functionality
* reloads the page
*/
$(document).on('click', 'header .reload', function(e) {
e.preventDefault();
window.location.reload(true);
});
/**
* date range slider
* set next/previous time slot
*/
$(document).on('click', '#date_slot_nav button', function() {
var slot = parseInt($('#date_slot').find('input[name=range]:checked').val()),
prev = $(this).hasClass('prev');
// if the date_range was modified manually, get the difference
if (isNaN(slot)) slot = date_range.options.to-date_range.options.from;
date_range.update({
from: prev === true ? date_range.options.from-slot : date_range.options.from+slot,
to: prev === true ? date_range.options.to-slot : date_range.options.to+slot
});
// disable buttons if slot is too big or end is near
check_daterange_boundaries(slot);
});
/**
* date range slider
* set predefined time range like day/week/month/year
*/
$(document).on('change', '#date_slot input[name=range]', function() {
var range = parseInt($(this).val());
date_range.update({
from: date_range.options.to - range,
to: date_range.options.to // the current "to" value should stay
});
check_daterange_boundaries(range);
});
/**
* sync button
* gets the time range from the graph and updates the date range slider
*/
$(document).on('click', '#date_syncing button.sync-date', function() {
var from = dygraph_daterange[0].getTime(),
to = dygraph_daterange[1].getTime();
date_range.update({
from: from,
to: to
});
// remove active state of date slot button
$('#date_slot').find('label.active').removeClass('active').find('input').prop('checked', false);
check_daterange_boundaries(to-from);
});
/**
* source filter
* reload the graph when the source selection changes
*/
$(document).on('change', '#filterSourcesSelect', updateGraph);
/**
* displays the right filter
*/
$(document).on('change', '#filterDisplaySelect', function() {
var display = $(this).val(), displayId;
var $filters = $('#filter').find('[data-display]').addClass('d-none');
// show only wanted filters
$filters.filter('[data-display*=' + display + ']').removeClass('d-none');
switch (display) {
case 'sources':
displayId = '#filterSources';
displaySourcesHelper();
break;
case 'protocols':
displayId = '#filterProtocols';
displayProtocolsHelper();
break;
case 'ports':
displayId = '#filterPorts';
displayPortsHelper();
break;
}
// initialize tooltips
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
// try to update graph
updateGraph();
});
/**
* protocols filter
* reload the graph when the protocol selection changes
*/
$(document).on('change', '#filterProtocols input', function() {
var $filter = $('#filterProtocols');
if ($(this).val() === 'any') {
// uncheck all other input elements
$(this).parent().addClass('active');
$filter.find('input[value!="any"]').each(function () {
$(this).prop('checked', false).parent().removeClass('active');
});
} else {
// uncheck 'any' input element
$filter.find('input[value="any"]').prop('checked', false).parent().removeClass('active');
}
// prevent having none checked - select 'any' as fallback
if ($filter.find('input:checked').length === 0) {
$filter.find('input[value="any"]').prop('checked', true).parent().addClass('active');
}
updateGraph();
});
/**
* datatype filter (flows/packets/traffic)
* reload the graph... you get it by now
*/
$(document).on('change', '#filterTypes input', updateGraph);
$(document).on('change', '#trafficUnit input', updateGraph);
$(document).on('change', '#filterPortsSelect', updateGraph);
/**
* show/hide series in the dygraph
* todo: check if this is needed at all, as it's the same like in the filter
*/
$(document).on('change', '#series input', function(e) {
var $checkbox = $(e.target);
dygraph.setVisibility($checkbox.parent().index(), $($checkbox).is(':checked'));
});
/**
* set graph display to curve or step plot
*/
$(document).on('change', '#graph_lineplot input', function() {
dygraph.updateOptions({
stepPlot: ($(this).val() === 'step')
});
})
/**
* set graph display to lines or stacked
*/
$(document).on('change', '#graph_linestacked input', function() {
var stacked = ($(this).val() === 'stacked');
dygraph.updateOptions({
stackedGraph: ($(this).val() === 'stacked'),
fillGraph: ($(this).val() !== 'line')
});
});
/**
* scale graph display linear or logarithmic
*/
$(document).on('change', '#graph_linlog input', function() {
var linear = ($(this).val() === 'linear');
dygraph.updateOptions({
logscale : !linear
});
});
/**
* disable aggregation fields if statistics "for" field is not "flow records"
*/
$(document).on('change', '#statsFilterForSelection', function() {
var disabled = ($(this).val() !== 'record');
$('#filterFlowAggregation').find('label, input, select, button').each(function() {
$(this).prop('disabled', disabled).toggleClass('disabled', disabled);
});
$('#filterOutputSelection').prop('disabled', disabled).toggleClass('disabled', disabled);
});
var setButtonLoading = function($button, setTo = true) {
$button.toggleClass('disabled', setTo);
if (setTo === false) {
if ($button.data('old-text') !== undefined) {
$button.html($button.data('old-text'));
$button.data('old-text', undefined);
}
} else {
$button.data('old-text', $button.html());
$button.html('<span class="spinner-border spinner-border-sm" aria-hidden="true"></span><span role="status"> Loading…</span>');
}
}
/**
* Process flows/statistics form submission
*/
$(document).on('click', '#filterCommands .submit', function () {
var current_view = $('.nav-link.active').attr('data-view'),
do_continue = true,
date_diff = date_range.options.to-date_range.options.from,
count_sources = $('#filterSourcesSelect').val().length,
count_days = Math.round(Number(date_diff/1000/24/60/60));
// warn user of long-running query
if (count_days > 7 && date_diff*count_sources > 1000*24*60*60*12) {
var calc_info = count_days + ' days and ' + count_sources + ' sources';
do_continue = confirm('Be aware that nfdump will scan 288 capture files per day and source. You selected ' + calc_info + '. This might take a long time and lots of server resources. Are you sure you want to submit this query?');
}
if (do_continue === false) return false;
if (current_view === 'statistics') submit_statistics();
if (current_view === 'flows') submit_flows();
// remove success errors
$('#error').find('div.alert-success').fadeOut(1500, function () {
$(this).remove();
});
// set button to loading state
setButtonLoading($(this));
});
/**
* Get a CSV of the currently selected data
*/
$(document).on('click', '#filterCommands .csv', function() {
$('#filterCommands .submit:visible').trigger('click');
window.open(api_last_query + '&csv', '_blank');
});
/**
* Reset flows/statistics form
*/
$(document).on('click', '#filterCommands .reset', function() {
var view = $('header').find('li.active a').attr('data-view'),
$filter = $('#filterContainer');
$filter.find('form').eq(0).trigger('reset');
$filter.find('input:visible, textarea:visible, select:visible, button:visible').trigger('change');
});
/**
* initialize the frontend
* - set the select-list of sources
* - initialize the range slider
* - load the graph
* - select default view if set in the config
*/
function init() {
// set version
$('#version').html(config.version);
var stored_filters = config['stored_filters'];
var local_filters = window.localStorage.getItem('stored_filters');
stored_filters = stored_filters.concat(JSON.parse( local_filters ));
stored_filters = Array.from(new Set(stored_filters));
window.localStorage.setItem('stored_filters', JSON.stringify(stored_filters) )
// load values for form
updateDropdown('sources', config['sources']);
updateDropdown('ports', config['ports']);
updateDropdown('filters', stored_filters);
init_rangeslider();
// load default view
if (typeof config.frontend.defaults !== 'undefined') {
$('header li a[data-view="' + config.frontend.defaults.view + '"]').trigger('click');
}
enable_graph = true;
// show graph for one year by default
$('#date_slot').find('[data-unit="y"]').trigger('click');
}
/**
* sets default values for the view (graphs, flows, statistics)
* hides unneeded controls if e.g. only one source or one port is defined
* @param view
*/
function init_defaults(view) {
var defaults = {graphs: {}, flows: {}, statistics: {}};
if (typeof config.frontend.defaults !== 'undefined') {
defaults = config.frontend.defaults;
}
// graphs defaults
if (view === 'graphs' && views_view_status.graphs === false) {
// graphs: set default display (sources, protocols, ports)
if (typeof defaults.graphs.display !== 'undefined') {
$('#filterDisplaySelect').val(defaults.graphs.display).trigger('change');
} else {
$('#filterDisplaySelect').trigger('change');
}
// graphs: set default datatype
if (typeof defaults.graphs.datatype !== 'undefined') {
$('#filterTypes input[value="' + defaults.graphs.datatype + '"]').trigger('click');
}
// graphs: set default protocols
if (typeof defaults.graphs.protocols !== 'undefined') {
// multiple possible if on protocols display
if (defaults.graphs.display === 'protocols') {
$('#filterProtocolButtons input[value="any"]').trigger('click');
$.each(defaults.graphs.protocols, function (i, proto) {
$('#filterProtocolButtons input[value="' + proto + '"]').trigger('click');
});
} else {
$('#filterProtocolButtons input[value="' + defaults.graphs.protocols[0] + '"]').trigger('click');
}
}
// graphs: hide unneeded controls
if (config['sources'].length === 1) { // only one source defined
$('#filterDisplaySelect option[value="sources"]').remove();
$('#filterSources').hide();
}
if (config['ports'].length === 0) { // only one port defined
$('#filterDisplaySelect option[value="ports"]').remove();
}
if ($('#filterDisplaySelect option').length === 1) { // only one display option left
$('#filterDisplay').hide();
}
}
// flows defaults
if (view === 'flows' && views_view_status.flows === false) {
// flows: limit
if (typeof defaults.flows.limit !== 'undefined') {
$('#flowsFilterLimitSelection').val(defaults.flows.limit);
}
}
// statistics defaults
if (view === 'statistics' && views_view_status.statistics === false) {
// statistics: order by
if (typeof defaults.statistics.orderby !== 'undefined') {
$('#statsFilterOrderBySelection').val(defaults.statistics.orderby);
}
}
}
/**
* initialize the range slider
*/
function init_rangeslider() {
// set default date range
var to = new Date();
var from = new Date(config.frontend.data_start * 1000 || to.getTime() - 1000*60*60*24*365*3);
dygraph_daterange = [from, to];
// initialize date range slider
$('#date_range').ionRangeSlider({
type: 'double',
grid: true,
min: dygraph_daterange[0].getTime(),
max: dygraph_daterange[1].getTime(),
force_edges: true,
drag_interval: true,
prettify: function(ut) {
var date = new Date(ut);
return date.toDateString();
},
onChange: function(data) {
// remove active state of date slot button
$('#date_slot').find('label.active').removeClass('active').find('input').prop('checked', false);
},
onFinish: function(data) {
dygraph_daterange = [new Date(data.from), new Date(data.to)];
date_range.update({ from: data.from, to: data.to });
check_daterange_boundaries(data.to-data.from);
// deactivate syncing button
$('#date_syncing').find('button.sync-date').prop('disabled', true);
updateGraph();
},
onUpdate: function(data) {
dygraph_daterange = [new Date(data.from), new Date(data.to)];
// deactivate syncing button
$('#date_syncing').find('button.sync-date').prop('disabled', true);
updateGraph();
}
});
date_range = $('#date_range').data('ionRangeSlider');
}
/**
* initialize two dygraph mods
* they are needed to dynamically load more detailed data as the user zooms in or pans around
* heavily influenced by https://github.com/kaliatech/dygraphs-dynamiczooming-example
*/
function init_dygraph_mods() {
dygraph_rangeselector_active = false;
var $rangeEl = $('#flowDiv').find('.dygraph-rangesel-fgcanvas, .dygraph-rangesel-zoomhandle');
// uninstall existing handler if already installed
$rangeEl.off('mousedown.dygraph touchstart.dygraph');
// install new mouse down handler
$rangeEl.on('mousedown.dygraph touchstart.dygraph', function () {
// track that mouse is down on range selector
dygraph_rangeselector_active = true;
// setup mouse up handler to initiate new data load
$(window).off('mouseup.dygraph touchend.dygraph'); //cancel any existing
$(window).on('mouseup.dygraph touchend.dygraph', function () {
$(window).off('mouseup.dygraph touchend.dygraph');
// mouse no longer down on range selector
dygraph_rangeselector_active = false;
// get the new detail window extents
var range = dygraph.xAxisRange();
dygraph_daterange = [new Date(range[0]), new Date(range[1])];
dygraph_did_zoom = true;
// activate syncing button
$('#date_syncing').find('button.sync-date').prop('disabled', false);
// update graph
updateGraph();
});
});
// save original endPan function
var origEndPan = Dygraph.defaultInteractionModel.endPan;
// replace built-in handling with our own function
Dygraph.defaultInteractionModel.endPan = function (event, g, context) {
// call the original to let it do it's magic
origEndPan(event, g, context);
// extract new start/end from the x-axis
var range = g.xAxisRange();
dygraph_daterange = [new Date(range[0]), new Date(range[1])];
dygraph_did_zoom = true;
updateGraph();
};
Dygraph.endPan = Dygraph.defaultInteractionModel.endPan; // see dygraph-interaction-model.js
}
/**
* zoom callback for dygraph
* updates the graph when the rangeselector is not active
* @param minDate
* @param maxDate
*/
function dygraph_zoom(minDate, maxDate) {
dygraph_daterange = [new Date(minDate), new Date(maxDate)];
//When zoom reset via double-click, there is no mouse-up event in chrome (maybe a bug?),
//so we initiate data load directly
if (dygraph.isZoomed('x') === false) {
dygraph_did_zoom = true;
$(window).off('mouseup touchend'); //Cancel current event handler if any
updateGraph();
return;
}
//The zoom callback is called when zooming via mouse drag on graph area, as well as when
//dragging the range selector bars. We only want to initiate dataload when mouse-drag zooming. The mouse
//up handler takes care of loading data when dragging range selector bars.
if (!dygraph_rangeselector_active) {
dygraph_did_zoom = true;
updateGraph();
}
}
/**
*
* @param e The event object for the click
* @param x The x value that was clicked (for dates, this is milliseconds since epoch)
* @param points The closest points along that date
*/
function dygraph_click(e, x, points) {
if (confirm('Zoom in to this data point?')) {
date_range.update({
from: x,
to: x+300000
});
// remove active state of date slot button
$('#date_slot').find('label.active').removeClass('active').find('input').prop('checked', false);
check_daterange_boundaries((x+300)-x);
}
}
/**
* reads options from api_graph_options, performs a request on the API
* and tries to display the received data in the dygraph.
*/
function updateGraph() {
if (enable_graph === false) return false;
var sources = $('#filterSourcesSelect').val(),
type = $('#filterTypes input:checked').val(),
ports = $('#filterPortsSelect').val(),
protocols = $('#filterProtocols').find('input:checked').map(function() { return $(this).val(); }).get(),
display = $('#filterDisplaySelect').val(),
title = type + ' for ';
// check if options valid to request new dygraph
if (typeof sources === 'string') sources = [sources];
if (sources.length === 0) {
if (display === 'ports')
sources = ['any'];
else return;
}
if ($('#flowDiv:visible').length === 0) return;
if (ports.length === 0) ports = [0];
if (type === 'traffic') type = $('#trafficUnit input:checked').val();
// set options
api_graph_options = {
datestart: parseInt(dygraph_daterange[0].getTime()/1000),
dateend: parseInt(dygraph_daterange[1].getTime()/1000),
type: type,
protocols: protocols.length > 0 ? protocols : ['any'],
sources: sources,
ports: ports,
display: display
};
// set title
var elements = eval(display);
var cat = elements.length > 1 ? display : display.substr(0, display.length-1); // plural
// if more than 4, only show number of sources instead of names
if (elements.length > 4) title += elements.length + ' ' + cat;
else title += cat + ' ' + elements.join(', ');
// make actual request
$.get('../api/graph', api_graph_options, function (data, status) {
if (status !== 'success') {
display_message('warning', 'There somehow was a problem getting data, please check your form values.');
return false;
}
if (data.data.length === 0) {
return false;
}
var labels = ['Date'], index_to_insert = false;
// iterate over labels
$('#series').empty();
$.each(data.legend, function (id, legend) {
labels.push(legend);
$('#series').append('<label><input type="checkbox" checked> ' + legend + '</label>');
});
// transform data to something Dygraph understands
if (dygraph_did_zoom !== true) {
// reset dygraph data to get a fresh load
dygraph_data = [];
} else {
// delete values to replace
for (var i = 0; i < dygraph_data.length; i++) {
if (dygraph_data[i][0].getTime() >= dygraph_daterange[0].getTime() && dygraph_data[i][0].getTime() <= dygraph_daterange[1].getTime()) {
// set start index for the new values
if (index_to_insert === false) index_to_insert = i;
// delete current element from array
dygraph_data.splice(i, 1);
// decrease current index, as all array elements moved left on deletion
i--;
}
}
}
// Calculate the difference between the server and local timezone offsets
var serverTimezoneOffset = config.tz_offset * 60 * 60;
var localTimezoneOffset = new Date().getTimezoneOffset() * -60;
var timezoneOffset = serverTimezoneOffset - localTimezoneOffset;
// iterate over API result
$.each(data.data, function (datetime, series) {
var position = [new Date((parseInt(datetime) + timezoneOffset) * 1000)] ;
// add all serie values to position array
$.each(series, function (y, val) {
position.push(val);
});
// push position array to dygraph data
if (dygraph_did_zoom !== true) {
dygraph_data.push(position);
} else {
// when zoomed in, insert position array at the start index of replacement data
dygraph_data.splice(index_to_insert, 0, position);
index_to_insert++; // increase index, or data will get inserted backwards
}
});
if (typeof dygraph === 'undefined') {
// initial dygraph config:
dygraph_config = {
title: title,
labels: labels,
ylabel: type.toUpperCase() + '/s',
xlabel: 'TIME',
labelsKMB: type === 'flows' || type === 'packets',
labelsKMG2: type === 'bits' || type === 'bytes', // only show KMG for traffic, not for packets or flows
labelsDiv: $('#legend')[0],
labelsSeparateLines: true,
legend: 'always',
stepPlot: true,
showRangeSelector: true,
dateWindow: [dygraph_data[0][0], dygraph_data[dygraph_data.length - 1][0]],
zoomCallback: dygraph_zoom,
clickCallback: dygraph_click,
highlightSeriesOpts: {
strokeWidth: 2,
strokeBorderWidth: 1,
highlightCircleSize: 5
},
rangeSelectorPlotStrokeColor: '#888888',
rangeSelectorPlotFillColor: '#cccccc',
stackedGraph: true,
fillGraph: true,
};
dygraph = new Dygraph($('#flowDiv')[0], dygraph_data, dygraph_config);
init_dygraph_mods();
} else {
// update dygraph config
dygraph_config = {
// series: series,
// axes: axes,
ylabel: type.toUpperCase() + '/s',
labelsKMB: type === 'flows' || type === 'packets',
labelsKMG2: type === 'bits' || type === 'bytes', // only show KMG for traffic, not for packets or flows
title: title,
labels: labels,
file: dygraph_data,
};
if (dygraph_did_zoom === true) {
dygraph_config.dateWindow = dygraph_daterange;
} else {
// reset date window if we want to show entirely new data
dygraph_config.dateWindow = null;
}
dygraph.updateOptions(dygraph_config);
}
dygraph_did_zoom = false;
});
}
/**
* Display a message in the frontend
* @param severity (success, info, warning, danger)
* @param message
*/
function display_message(severity, message) {
var current_view = $('header').find('li.active a').attr('data-view'),
$error = $('#error'),
$buttons = $('button.submit'),
icon;
switch (severity) {
case 'success': icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check-circle-fill" viewBox="0 0 16 16">\n' +
' <path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0m-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>\n' +
'</svg> '; break;
case 'info': icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-info-circle-fill" viewBox="0 0 16 16">\n' +
' <path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2"/>\n' +
'</svg> '; break;
case 'warning': icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-triangle" viewBox="0 0 16 16">\n' +
' <path d="M7.938 2.016A.13.13 0 0 1 8.002 2a.13.13 0 0 1 .063.016.15.15 0 0 1 .054.057l6.857 11.667c.036.06.035.124.002.183a.2.2 0 0 1-.054.06.1.1 0 0 1-.066.017H1.146a.1.1 0 0 1-.066-.017.2.2 0 0 1-.054-.06.18.18 0 0 1 .002-.183L7.884 2.073a.15.15 0 0 1 .054-.057m1.044-.45a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767z"/>\n' +
' <path d="M7.002 12a1 1 0 1 1 2 0 1 1 0 0 1-2 0M7.1 5.995a.905.905 0 1 1 1.8 0l-.35 3.507a.552.552 0 0 1-1.1 0z"/>\n' +
'</svg> '; break;
case 'danger': icon = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-exclamation-triangle-fill" viewBox="0 0 16 16">\n' +
' <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5m.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>\n' +
'</svg> '; break;
}
// create new error element
$error.append('<div class="alert alert-dismissible mt-2" role="alert"><button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>');
// fill
$error.find('div.alert').last().addClass('alert-' + severity).prepend(icon + message);
// set default text to buttons, if needed
$buttons.each(function() {
setButtonLoading($(this), false);
});
// empty data table
$('#contentDiv').find('table.table').empty();
}
/**
* checks if with supplied date range, navigation is still possible (e.g. plus 1 month)
* and disables navigation buttons if not
* @param range date difference in milliseconds
*/
function check_daterange_boundaries(range) {
var $buttons = $('#date_slot_nav').find('button');
// reset next/prev buttons (depending on selected range)
$buttons.filter('.next').prop('disabled', (date_range.options.to + range > date_range.options.max));
$buttons.filter('.prev').prop('disabled', (date_range.options.from - range < date_range.options.min));
}
/**
* Process flows form submission
*/
function submit_flows() {
var sources = $('#filterSourcesSelect').val(),
datestart = parseInt(dygraph_daterange[0].getTime()/1000),
dateend = parseInt(dygraph_daterange[1].getTime()/1000),
filter = '' + $('#filterNfdumpTextarea').val(),
limit = $('#flowsFilterLimitSelection').val(),
sort = '',
output = {
format: $('#filterOutputSelection').val(),
custom: $('#customListOutputFormatValue').val(),
};
// parse form values to generate a proper API request
var aggregate = parse_aggregation_fields();
if (typeof sources === 'string') sources = [sources];
if ($('#flowsFilterOther').find('[name=ordertstart]:checked').length > 0) {
sort = $('[name=ordertstart]:checked').val();
}
api_flows_options = {
datestart: datestart,
dateend: dateend,
sources: sources,
filter: filter,
limit: limit,
aggregate: aggregate,
sort: sort,
output: output
};
api_last_query = '../api/flows/?' + $.param( api_flows_options );
var req = $.get('../api/flows', api_flows_options, render_table);
}
/**
* Process statistics form submission
*/
function submit_statistics() {
var sources = $('#filterSourcesSelect').val(),
datestart = parseInt(dygraph_daterange[0].getTime() / 1000),
dateend = parseInt(dygraph_daterange[1].getTime() / 1000),
filter = '' + $('#filterNfdumpTextarea').val(),
top = $('#statsFilterTopSelection').val(),
s_for = $('#statsFilterForSelection').val(),
title = $('#statsFilterForSelection :selected').text(),
sort = $('#statsFilterOrderBySelection').val(),
fmt = $('#filterOutputSelection'),
output = {};
if (!fmt.prop('disabled')) {
output.format = fmt.val();
output.custom = $('#customListOutputFormatValue').val();
}
if (typeof sources === 'string') sources = [sources];
api_statistics_options = {
datestart: datestart,
dateend: dateend,
sources: sources,
filter: filter,
top: top,
for: s_for + '/' + sort,
title: title,
limit: '',
output: output
};
api_last_query = '../api/stats/?' + $.param( api_statistics_options );
var req = $.get('../api/stats', api_statistics_options, render_table);
}
/**
* Parse aggregation fields and return something meaningful, e.g. proto,srcip/24
* @returns string
*/
function parse_aggregation_fields() {
var $aggregation = $('#filterFlowAggregation');
if ($aggregation.find('[name=bidirectional]:checked').length === 0) {
var validAggregations = ['proto', 'dstport', 'srcport', 'srcip', 'dstip'],
aggregate = '';
$.each(validAggregations, function(id, val) {
if ($aggregation.find('[name=' + val + ']:checked').length > 0) {
aggregate += (aggregate === '') ? val : ',' + val;
} else {
var select = $aggregation.find('[name=' + val + ']').val();
if (select === 'none') return;
if (val === 'srcip') {
var prefix = parseInt($aggregation.find('[name=srcipprefix]:visible').val()),
srcprefix = (isNaN(prefix) || prefix === 'srcip') ? '' : '/' + prefix,
srcip = select + srcprefix;
aggregate += (aggregate === '') ? srcip : ',' + srcip;
} else if (val === 'dstip') {
var prefix = parseInt($aggregation.find('[name=dstipprefix]:visible').val()),
dstprefix = (isNaN(prefix) || prefix === 'dstip') ? '' : '/' + prefix,
dstip = select + dstprefix;
aggregate += (aggregate === '') ? dstip : ',' + dstip;
}
}
});
return aggregate;
} else return 'bidirectional';
}
/**
* @see https://stackoverflow.com/a/2901298/710921
* @param {number} x
* @returns {string}
*/
function numberWithCommas(x) {
var parts = x.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
}
/**
* parses the provided data, converts it into a better suitable format and populates a html table
* @param data
* @param status
* @returns boolean
*/
function render_table(data, status) {
if (status === 'success') {
footable_data = data;
// print nfdump command
if (typeof data[0] === 'string') {
display_message('success', '<b>nfdump command:</b> ' + data[0].toString())
}
// return if invalid data got returned
if (typeof data[1] !== 'object') {
display_message('warning', '<b>something went wrong.</b> ' + data[1].toString());
return false;
}
// generate table header
var tempcolumns = data[1],
columns = [];
// generate column definitions
$.each(tempcolumns, function (i, val) {
// todo optimize breakpoints
var title = (val === 'val') ? api_statistics_options.title : nfdump_translation[val],
column = {
name: val,
title: title,
type: 'text',
breakpoints: 'xs sm',
};
// add formatter for ip addresses
if (['sa', 'da'].indexOf(val) !== -1 || val.match(/ip$/i) || title && title.match(/IP address$/)) {
column['formatter'] = (ip) => "<a href='#' onclick='return ip_link_handler(this)'>" + ip + "</a>";
}
// todo add date formatter for timestamps?
if (['ts', 'te', 'tr'].indexOf(val) !== -1) {
column['breakpoints'] = '';
column['type'] = 'text'; // 'date' needs moment.js library...
}
// add formatter for bytes
if (['ibyt', 'obyt', 'bpp', 'bps', 'byt', 'ibps', 'obps', 'ibpp', 'obpp'].indexOf(val) !== -1) {
column['type'] = 'number';
column['formatter'] = (x) => filesize(x, {
base: 10, // todo make configurable
});
}
// add formatter for big numbers
if (['td', 'fl', 'pkt', 'ipkt', 'opkt', 'ipps', 'opps'].indexOf(val) !== -1) {
column['type'] = 'number';
column['formatter'] = numberWithCommas
}
// define rest of numbers
if (['sp', 'dp', 'flP', 'ipktP', 'opktP', 'ibytP', 'obytP', 'pktP', 'bytP'].indexOf(val) !== -1) {
column['type'] = 'number';
}
// ip addresses, protocol, value should not be hidden on small screens
if (['sa', 'da', 'pr', 'val'].indexOf(val) !== -1) {
column['breakpoints'] = '';
}
// least important columns should be hidden on small screens
if (['flg', 'fwd', 'in', 'out', 'sas', 'das'].indexOf(val) !== -1) {
column['breakpoints'] = 'all';
column['type'] = 'text';
}
// add column to columns array
columns.push(column);
});
// generate table data
var temprows = data.slice(2),
rows = [];
$.each(temprows, function (i, val) {
var row = {id: i};
$.each(val, function (j, col) {
row[tempcolumns[j]] = col;
});
rows.push(row);
});
// init footable
$('table.table:visible').footable({
columns: columns,
rows: rows
});
if (rows.length > 0) $('table.table:visible .footable-empty').remove();
// remove errors (except success)
$('#error').find('div.alert:not(.alert-success)').fadeOut(1500, function () {
$(this).remove();
});
}
// reset button label
setButtonLoading($('#filterCommands').find('.submit'), false);
}
/**
* hide or show the custom output filter
*/
$(document).on('change', '#filterOutputSelection', function() {
// if "custom" is selected, show "customFlowListOutputFormat" otherwise hide it
if ($(this).val() === 'custom') $('#customListOutputFormat').removeClass('d-none');
else $('#customListOutputFormat').addClass('d-none');
});
/**
* block not available options on "bi-direction" checked
*/
$(document).on('change', '#filterFlowAggregationGlobal input[name=bidirectional]', function() {
var $filterFlowAggregation = $('#filterFlowAggregation');
// if "bi-directional" is checked, block (disable) all other aggregation options
if ($(this).parent().hasClass('active')) {
$filterFlowAggregation.find('[data-disable-on="bi-directional"]').each(function() {
$(this).parent().removeClass('active').addClass('disabled');
$(this).prop('disabled', true);
if ($(this).prop('tagName') === 'SELECT') $(this).prop('selectedIndex', 0);
else $(this).val('');
});
} else {
$filterFlowAggregation.find('[data-disable-on="bi-directional"]').each(function() {
$(this).parent().removeClass('disabled');
$(this).prop('disabled', false);
});
}
});
/**
* handle "onchange" for source/destination address(es) in aggregation filter
*/
$(document).on('change', '#filterFlowAggregationSourceAddressSelect, #filterFlowAggregationDestinationAddressSelect', function() {
var kind = $(this).attr('data-kind'),
$prefixDiv = $('#' + kind + 'CIDRPrefixDiv');
switch ($(this).val()) {
case 'none':
case 'srcip':
case 'dstip':
$prefixDiv.addClass('d-none');
break;
case 'srcip4':
case 'dstip4':
$prefixDiv.removeClass('d-none');
$prefixDiv.find('input').attr('maxlength', 2).val('24');
break;
case 'srcip6':
case 'dstip6':
$prefixDiv.removeClass('d-none');
$prefixDiv.find('input').attr('maxlength', 3).val('128');
break;
}
});
/**
* handle "onchange/onclick" for filter Filters controls
*/
$(document).on('change', '#filterFiltersSelect', function() {
document.getElementById('filterNfdumpTextarea').value = event.target.value;
});
$(document).on('click', '#filterFiltersButtonRemove', function() {
var filter = [document.getElementById('filterNfdumpTextarea').value];
var select = document.getElementById('filterFiltersSelect');
var stored_filters = JSON.parse(window.localStorage.getItem('stored_filters'));
stored_filters = stored_filters.filter(element => { return !filter.includes(element); });
stored_filters = JSON.stringify(stored_filters);
window.localStorage.setItem('stored_filters', stored_filters);
select.innerHTML = '';
updateDropdown('filters', JSON.parse(stored_filters));
});
$(document).on('click', '#filterFiltersButtonSave', function() {
var stored_filters = JSON.parse(window.localStorage.getItem('stored_filters'));
var filter = [document.getElementById('filterNfdumpTextarea').value];
if (!stored_filters.includes(filter[0]))
{
stored_filters = JSON.stringify( filter.concat(stored_filters));
window.localStorage.setItem('stored_filters', stored_filters);
updateDropdown('filters', filter);
}
});
/**
* modify some GUI elements if the user selected "sources" to display
*/
function displaySourcesHelper() {
// add "multiple" to source selection
var $sourceSelect = $('#filterSourcesSelect'),
$protocolButtons = $('#filterProtocolButtons');
$sourceSelect.prop('multiple', true);
// disable 'any' in sources
$sourceSelect.find('option[value="any"]').prop('disabled', true);
// select all sources
$sourceSelect.find('option:not([disabled])').prop('selected', true);
// uncheck protocol buttons and transform to radio buttons
$protocolButtons.find('label').removeClass('active');
$protocolButtons.find('input').prop('checked', false).attr('type', 'radio');
// select Any proto as default
$protocolButtons.find('[for="filterProtocolAny"]').click();
}
/**
* modify some GUI elements if the user selected "protocols" to display
*/
function displayProtocolsHelper() {
// remove "multiple" from source select and select first source
var $sourceSelect = $('#filterSourcesSelect'),
$protocolButtons = $('#filterProtocolButtons');
$sourceSelect.prop('multiple', false);
// disable 'any' in sources
$sourceSelect.find('option[value="any"]').prop('disabled', true).prop('selected', false);
// select the first element
$sourceSelect.find('option:not([disabled]):first').prop('selected', true);
// protocol buttons become checkboxes and get checked by default
$protocolButtons.find('label').removeClass('active').filter(() => $(this).find('input').val() !== 'any').click();
$protocolButtons.find('input').attr('type', 'checkbox').prop('checked', false).filter('[value!="any"]').click();
}
/**
* modify some GUI elements if the user selected "ports" to display
*/
function displayPortsHelper() {
// remove "multiple" from source select
var $sourceSelect = $('#filterSourcesSelect'),
$portsSelect = $('#filterPortsSelect'),
$protocolButtons = $('#filterProtocolButtons');
$sourceSelect.attr('multiple', false);
// enable 'any' in sources
$sourceSelect.find('option[value="any"]').prop('disabled', false);
// uncheck protocol buttons and transform to radio buttons
$protocolButtons.find('label').removeClass('active');
$protocolButtons.find('label input').prop('checked', false).attr('type','radio');
// select TCP proto as default
$protocolButtons.find('label:first').addClass('active').find('input').prop('checked', true);
// select all ports
$portsSelect.find('option').prop('selected', true);
}
/**
* updates the filter dropdowns with data
* @param displaytype string: sources/ports/protocols
* @param array array: the values to add
*/
function updateDropdown(displaytype, array) {
var id = '#filter' + displaytype.charAt(0).toUpperCase() + displaytype.slice(1);
var $select = $(id).find('select');
$.each(array, function(key, value) {
$select
.append($('<option></option>')
.attr('value',value).text(value));
});
}
});
/*!
Filesize.js
2022 Jason Mulligan <jason.mulligan@avoidwork.com>
@version 9.0.11
*/
!function(i,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(i="undefined"!=typeof globalThis?globalThis:i||self).filesize=t()}(this,(function(){"use strict";function i(t){return i="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(i){return typeof i}:function
(i){return i&&"function"==typeof Symbol&&i.constructor===Symbol&&i!==Symbol.prototype?"symbol":typeof i},i(t)}var t="array",o="bits",e="byte",n="bytes",r="",b="exponent",l="function",a="iec",d="Invalid number",f="Invalid rounding method",u="jedec",s="object",c=".",p="round",y="kbit",m="string",v={symbol:{iec:{bits:["bit","Kibit","Mibit","Gibit","Tibit","Pibit","Eibit","Zibit","Yibit"],bytes:["B","KiB","MiB","GiB","TiB","PiB",
"EiB","ZiB","YiB"]},jedec:{bits:["bit","Kbit","Mbit","Gbit","Tbit","Pbit","Ebit","Zbit","Ybit"],bytes:["B","KB","MB","GB","TB","PB","EB","#BC8F8F">"ZB"
,"YB"]}},fullform:{iec:["","kibi","mebi","gibi","tebi","pebi","exbi","zebi","yobi"],jedec:["","kilo","mega","giga","tera","peta","exa","zetta","yotta"]}};function g(g){var h=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},B=h.bits,M=void 0!==B&&B,S=h.pad,T=void 0!==S&&S,w=h.base,x=void 0===w?-1:w,E=h.round,j=void 0===E?2:E,N=h.locale,P=void 0===N?r:N,k=h.localeOptions,G=void 0===k?{}:k,K=h.separator,Y=void 0===K?r:K,Z=h.spacer,z=void 0===Z?" ":Z,I=h.symbols,L=void 0===I?{}:I,O=h.standard,q=void 0===O?r:O,A=h.output,C=vo
id 0===A?m:A,D=h.fullform,F=void 0!==D&&D,H=h.fullforms,J=void 0===H?[]:H,Q=h.exponent,R=void 0===Q?-1:Q,U=h.roundingMethod,V=void 0===U?p:U,W=h.precision,X=void 0===W?0:W,$=R,_=Number(g),ii=[],ti=0,oi=r;-1===x&&0===q.length?(x=10,q=u):-1===x&&q.length>0?x=(q=q===a?a:u)===a?2:10:q=10===(x=2===x?2:10)||q===u?u:a;var ei=10===x?1e3:1024,ni=!0===F,ri=_<0,bi=Math[V];if(isNaN(g))throw new TypeError(d);if(i(bi)!==l)throw new TypeError(f);if(ri&&(_=-_),(-1===$||isNaN($))&am
p;&($=Math.floor(Math.log(_)/Math.log(ei)))<0&&($=0),$>8&&(X>0&&(X+=8-$),$=8),C===b)return $;if(0===_)ii[0]=0,oi=ii[1]=v.symbol[q][M?o:n][$];else{ti=_/(2===x?Math.pow(2,10*$):Math.pow(1e3,$)),M&&(ti*=8)>=ei&&$<8&&(ti/=ei,$++);var li=Math.pow(10,$>0?j:0);ii[0]=bi(ti*li)/li,ii[0]===ei&&$<8&&-1===R&&(ii[0]=1,$++),oi=ii[1]=10===x&&1===$?M?y:"kB":v.symbol[q][M?o:n][$]}if(ri&&(ii[0]=-ii[0]),X>0&&(ii[0]=ii[0].toPrecision(X)),ii[1]=L[ii[1]]||ii[1],!0===P?ii[0]=ii[0].toLocaleString():P.length>0?ii[0]=ii[0].toLocaleString(P,G):Y.length>0&&(ii[0]=ii[0].toString().replace(c,Y)),T&&!1===Number.isInteger(ii[0])&&j
>0){var ai=Y||c,di=ii[0].toString().split(ai),fi=di[1]||r,ui=fi.length,si=j-ui;ii[0]="".concat(di[0]).concat(ai).concat(fi.padEnd(ui+si,"0"))}return ni&&(ii[1]=J[$]?J[$]:v.fullform[q][$]+(M?"bit":e)+(1===ii[0]?r:"s")),C===t?ii:C===s?{value:ii[0],symbol:ii[1],exponent:$,unit:oi}:ii.join(z)}return g.partial=function(i){return function(t){return g(t,i)}},g}));
//# sourceMappingURL=filesize.min.js.map
Generated by GNU Enscript 1.6.6.