/* Filter grammar
* SPDX-License-Identifier: GPL-3.0-or-later */
const dafg = {
key: {'qname': true, 'src': true, 'dst': true},
op: {'=': true, '~': true},
conj: {'and': true, 'or': true},
action: {'pass': true, 'deny': true, 'drop': true, 'truncate': true, 'forward': true, 'reroute': true, 'rewrite': true, 'mirror': true},
suggest: [
'QNAME = example.com',
'QNAME ~ %d+.example.com',
'SRC = 127.0.0.1',
'SRC = 127.0.0.1/8',
'DST = 127.0.0.1',
'DST = 127.0.0.1/8',
/* Action examples */
'PASS', 'DENY', 'DROP', 'TRUNCATE',
'FORWARD 127.0.0.1',
'MIRROR 127.0.0.1',
'REROUTE 127.0.0.1-192.168.1.1',
'REROUTE 127.0.0.1/24-192.168.1.0',
'REWRITE example.com A 127.0.0.1',
'REWRITE example.com AAAA ::1',
]
};
function setValidateHint(cls) {
var builderForm = $('#daf-builder-form');
builderForm.removeClass('has-error has-warning has-success');
if (cls) {
builderForm.addClass(cls);
}
}
function validateToken(tok, tbl) {
if (tok.length > 0 && tok[0].length > 0) {
if (tbl[tok[0].toLowerCase()]) {
setValidateHint('has-success');
return true;
} else { setValidateHint('has-error'); }
} else { setValidateHint('has-warning'); }
return false;
}
function parseOption(tok) {
var key = tok.shift().toLowerCase();
var op = null;
if (dafg.key[key]) {
op = tok.shift();
if (op) {
op = op.toLowerCase();
}
}
const item = {
text: key.toUpperCase() + ' ' + (op ? op.toUpperCase() : '') + ' ' + tok.join(' '),
};
if (dafg.key[key]) {
item.class = 'tag-default';
} else if (dafg.action[key]) {
item.class = 'tag-warning';
} else if (dafg.conj[key]) {
item.class = 'tag-success';
}
return item;
}
function createOption(input) {
const item = parseOption(input.split(' '));
item.value = input;
return item;
}
function dafComplete(form) {
const items = form.items;
for (var i in items) {
const tok = items[i].split(' ')[0].toLowerCase();
if (dafg.action[tok]) {
return true;
}
}
return false;
}
function formatRule(input) {
const tok = input.split(' ');
var res = [];
while (tok.length > 0) {
const key = tok.shift().toLowerCase();
if (dafg.key[key]) {
var item = parseOption([key, tok.shift(), tok.shift()]);
res.push(''+item.text+'');
} else if (dafg.action[key]) {
var item = parseOption([key].concat(tok));
res.push(''+item.text+'');
tok.splice(0, tok.length);
} else if (dafg.conj[key]) {
var item = parseOption([key]);
res.push(''+item.text+'');
}
}
return res.join('');
}
function toggleRule(row, span, enabled) {
if (!enabled) {
span.removeClass('glyphicon-pause');
span.addClass('glyphicon-play');
row.addClass('warning');
} else {
span.removeClass('glyphicon-play');
span.addClass('glyphicon-pause');
row.removeClass('warning');
}
}
function ruleControl(cell, type, url, action) {
const row = cell.parent();
$.ajax({
url: 'daf/' + row.data('rule-id') + url,
type: type,
success: action,
error: function (data) {
row.show();
const reason = data.responseText.length > 0 ? data.responseText : 'internal error';
cell.find('.alert').remove();
cell.append(
'
'+
'Failed (code: '+data.status+', reason: '+reason+').'+
'
'
);
},
});
}
function bindRuleControl(cell) {
const row = cell.parent();
cell.find('.daf-remove').click(function() {
row.hide();
ruleControl(cell, 'DELETE', '', function (data) {
cell.parent().remove();
});
});
cell.find('.daf-suspend').click(function() {
const span = $(this).find('span');
ruleControl(cell, 'PATCH', span.hasClass('glyphicon-pause') ? '/active/false' : '/active/true');
toggleRule(row, span, span.hasClass('glyphicon-play'));
});
}
function loadRule(rule, tbl) {
const row = $('
');
row.append('' + formatRule(rule.info) + ' | ');
row.append('' + rule.count + ' | ');
row.append(' | ');
row.append('' +
' ' +
'' +
'' +
' | ');
tbl.append(row);
/* Bind rule controls */
bindRuleControl(row.find('.daf-ctl'));
toggleRule(row, row.find('.daf-suspend span'), rule.active);
}
/* Load the filter table from JSON */
function loadTable(resp) {
const tbl = $('#daf-rules')
tbl.children().remove();
tbl.append('Rule | Matches | Rate | |
')
for (var i in resp) {
loadRule(resp[i], tbl);
}
}
document.addEventListener("DOMContentLoaded", () => {
/* Load the filter table. */
$.ajax({
url: 'daf',
type: 'get',
dataType: 'json',
success: loadTable
});
/* Listen for counter updates */
const wsStats = ('https:' == document.location.protocol ? 'wss://' : 'ws://') + location.host + '/daf';
const ws = new Socket(wsStats);
var lastRateUpdate = Date.now();
ws.onmessage = function(evt) {
var data = JSON.parse(evt.data);
/* Update heartbeat clock */
var now = Date.now();
var dt = now - lastRateUpdate;
lastRateUpdate = now;
/* Update match counts and rates */
$('#daf-rules .daf-rate span').text('');
for (var key in data) {
const row = $('tr[data-rule-id="'+key+'"]');
if (row) {
const cell = row.find('.daf-count');
const diff = data[key] - parseInt(cell.text());
cell.text(data[key]);
const badge = row.find('.daf-rate span');
if (diff > 0) {
/* Normalize difference to heartbeat (in msecs) */
const rate = Math.ceil((1000 * diff) / dt);
badge.text(rate + ' pps');
}
}
}
};
/* Rule builder UI */
$('#daf-builder').selectize({
delimiter: ',',
persist: true,
highlight: true,
closeAfterSelect: true,
onItemAdd: function (input, item) {
setValidateHint();
/* Prevent new rules when action is specified */
const tok = input.split(' ');
if (dafg.action[tok[0].toLowerCase()]) {
$('#daf-add').focus();
} else if(dafComplete(this)) {
/* No more rules after query is complete. */
item.remove();
}
},
createFilter: function (input) {
const tok = input.split(' ');
var key, op, expr;
/* If there are already filters, allow conjunctions. */
if (tok.length > 0 && this.items.length > 0 && dafg.conj[tok[0]]) {
setValidateHint();
return true;
}
/* First token is expected to be filter key,
* or any postrule with a parameter */
if (validateToken(tok, dafg.key)) {
key = tok.shift();
} else if (tok.length > 1 && validateToken(tok, dafg.action)) {
setValidateHint();
return true;
} else {
return false;
}
/* Input is a filter - second token must be operator */
if (validateToken(tok, dafg.op)) {
op = tok.shift();
} else {
return false;
}
/* Input is a filter - the rest of the tokens are RHS arguments. */
if (tok.length > 0 && tok[0].length > 0) {
expr = tok.join(' ');
} else {
setValidateHint('has-warning');
return false;
}
setValidateHint('has-success');
return true;
},
create: createOption,
render: {
item: function(item, escape) {
return '' + escape(item.text) + '';
},
},
});
/* Add default suggestions. */
const dafBuilder = $('#daf-builder')[0].selectize;
for (var i in dafg.suggest) {
dafBuilder.addOption(createOption(dafg.suggest[i]));
}
/* Rule builder submit */
$('#daf-add').click(function () {
const form = $('#daf-builder-form').parent();
if (dafBuilder.items.length == 0 || form.hasClass('has-error')) {
return;
}
/* Clear previous errors and resubmit. */
form.parent().find('.alert').remove();
$.post('daf', dafBuilder.items.join(' '))
.done(function (data) {
dafBuilder.clear();
loadRule(data, $('#daf-rules'));
})
.fail(function (data) {
const reason = data.responseText.length > 0 ? data.responseText : 'internal error';
form.after(
'
'+
'Couldn\'t add rule (code: '+data.status+', reason: '+reason+').'+
'
'
);
});
});
});