/* 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( '' ); }, }); } 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('RuleMatchesRate') 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( '' ); }); }); });