diff options
Diffstat (limited to 'modules/daf/daf.js')
-rw-r--r-- | modules/daf/daf.js | 295 |
1 files changed, 295 insertions, 0 deletions
diff --git a/modules/daf/daf.js b/modules/daf/daf.js new file mode 100644 index 0000000..05b171b --- /dev/null +++ b/modules/daf/daf.js @@ -0,0 +1,295 @@ +/* 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('<span class="label tag '+item.class+'">'+item.text+'</span>'); + } else if (dafg.action[key]) { + var item = parseOption([key].concat(tok)); + res.push('<span class="label tag '+item.class+'">'+item.text+'</span>'); + tok.splice(0, tok.length); + } else if (dafg.conj[key]) { + var item = parseOption([key]); + res.push('<span class="label tag '+item.class+'">'+item.text+'</span>'); + } + } + 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( + '<div class="alert alert-danger" role="alert">'+ + 'Failed (code: '+data.status+', reason: '+reason+').'+ + '</div>' + ); + }, + }); +} + +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 = $('<tr data-rule-id="'+rule.id+'" />'); + row.append('<td class="daf-rule">' + formatRule(rule.info) + '</td>'); + row.append('<td class="daf-count">' + rule.count + '</td>'); + row.append('<td class="daf-rate"><span class="badge"></span></td>'); + row.append('<td class="daf-ctl text-right">' + + '<div class="btn-group btn-group-xs">' + + '<button class="btn btn-default daf-suspend"><span class="glyphicon" aria="hidden" /></button>' + + '<button class="btn btn-default daf-remove"><span class="glyphicon glyphicon-remove" aria="hidden" /></button>' + + '</div></td>'); + 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('<tr><th>Rule</th><th>Matches</th><th>Rate</th><th></th></tr>') + 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 '<div class="name '+item.class+'">' + escape(item.text) + '</span>'; + }, + }, + }); + /* 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( + '<div class="alert alert-danger" role="alert">'+ + 'Couldn\'t add rule (code: '+data.status+', reason: '+reason+').'+ + '</div>' + ); + }); + }); +}); |