From cd5d5f0d4984eb304ba4a41f71031fd40849b8b2 Mon Sep 17 00:00:00 2001 From: Marek Vavrusa Date: Mon, 13 Jun 2016 10:21:26 -0700 Subject: [PATCH] modules/daf: a functional web interface the interface has a declarative rule builder that assists in building and validating rules, as well as seeing how much traffic do they match --- modules/daf/daf.js | 230 +++++++++++++++++++++++++++++++++++++++++++- modules/daf/daf.lua | 68 ++++++++++--- 2 files changed, 285 insertions(+), 13 deletions(-) diff --git a/modules/daf/daf.js b/modules/daf/daf.js index 82b8b8d0e..ea75b9607 100644 --- a/modules/daf/daf.js +++ b/modules/daf/daf.js @@ -1 +1,229 @@ -console.log('Hello from DAF!') +/* Filter grammar */ +const dafg = { + key: {'qname': true, 'src': true}, + op: {'=': true, '~': true}, + conj: {'and': true, 'or': true}, + action: {'pass': true, 'deny': true, 'drop': true, 'truncate': true, 'forward': true, 'reroute': true, 'rewrite': true}, + suggest: [ + 'QNAME = example.com', + 'QNAME ~ %d+.example.com', + 'SRC = 127.0.0.1', + 'SRC = 127.0.0.1/8', + /* Action examples */ + 'PASS', 'DENY', 'DROP', 'TRUNCATE', + 'FORWARD 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 loadRule(rule, tbl) { + const row = $(''); + row.append('' + formatRule(rule.info) + ''); + row.append('' + rule.count + ''); + row.append(''); + row.append(''); + tbl.append(row); +} + +/* 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); + } +} + +$(function() { + /* Load the filter table. */ + $.ajax({ + url: 'daf', + type: 'get', + dataType: 'json', + success: loadTable + }); + /* Listen for counter updates */ + const wsStats = (secure ? 'wss://' : 'ws://') + location.host + '/daf'; + const ws = new Socket(wsStats); + var lastRateUpdate = Date.now(); + ws.onmessage = function(evt) { + var data = $.parseJSON(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(Rickshaw.Fixtures.Number.formatKMBT(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 (tok.length > 0 && this.items.length > 0 && dafg.conj[tok[0]]) { + setValidateHint(); + return true; + } + if (validateToken(tok, dafg.key)) { + key = tok.shift(); + } else { + return false; + } + if (validateToken(tok, dafg.op)) { + op = tok.shift(); + } else { + return false; + } + 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'); + if (dafBuilder.items.length == 0 || form.hasClass('has-error')) { + return; + } + /* Clear previous errors and resubmit. */ + form.find('.alert').remove(); + $.post('daf', dafBuilder.items.join(' ')) + .done(function (data) { + dafBuilder.clear(); + loadRule(data, $('#daf-rules')); + }) + .fail(function (data) { + form.append( + '' + ); + }); + }); +}); \ No newline at end of file diff --git a/modules/daf/daf.lua b/modules/daf/daf.lua index e3b8269d3..ff629c3bb 100644 --- a/modules/daf/daf.lua +++ b/modules/daf/daf.lua @@ -52,7 +52,7 @@ local filters = { } local function parse_filter(tok, g) - local filter = filters[tok] + local filter = filters[tok:lower()] if not filter then error(string.format('invalid filter "%s"', tok)) end return filter(g) end @@ -80,6 +80,7 @@ end local function parse_query(g) local ok, actid, filter = pcall(parse_rule, g) if not ok then return nil, actid end + actid = actid:lower() if not actions[actid] then return nil, string.format('invalid action "%s"', actid) end -- Parse and interpret action local action = actions[actid] @@ -109,18 +110,45 @@ local M = { -- @function Public-facing API local function api(h, stream) - print('DAF: ') - for k,v in h:each() do print(k,v) end + local m = h:get(':method') + -- GET method + if m == 'GET' then + local ret = {} + for _, r in ipairs(M.rules) do + table.insert(ret, {info=r.info, id=r.rule.id, count=r.rule.count}) + end + return ret + -- POST method + elseif m == 'POST' then + local query = stream:get_body_as_string() + if query then + local ok, r = pcall(M.add, query) + if not ok then return 505 end + return {info=r.info, id=r.rule.id, count=r.rule.count} + end + return 400 + end end -- @function Publish DAF statistics local function publish(h, ws) - local ok = true + local ok, counters = true, {} while ok do - -- Publish stats updates periodically - local push = tojson({}) - ok = ws:send(push) - cqueues.sleep(0.5) + -- Check if we have new rule matches + local update = {} + for _, r in ipairs(M.rules) do + local id = r.rule.id + if counters[id] ~= r.rule.count then + -- Must have string keys for JSON object and not an array + update[tostring(id)] = r.rule.count + counters[id] = r.rule.count + end + end + -- Update counters when there is a new data + if next(update) ~= nil then + ws:send(tojson(update)) + end + cqueues.sleep(2) end ws:close() end @@ -143,7 +171,21 @@ function M.config(conf) -- Export snippet http.snippets['/daf'] = {'Application Firewall', [[ -
No rules here yet.
+
+
+
+ +
+ +
+
+
+
+
+ + +
No rules here yet.
+
]]} end @@ -155,13 +197,15 @@ function M.add(rule) local p = function (req, qry) return filter(req, qry) and action end - table.insert(M.rules, {rule=rule, action=id, policy=p}) + local desc = {info=rule, policy=p} -- Enforce in policy module, special actions are postrules if id == 'reroute' or id == 'rewrite' then - table.insert(policy.postrules, p) + desc.rule = policy:add(p, true) else - table.insert(policy.rules, p) + desc.rule = policy:add(p) end + table.insert(M.rules, desc) + return desc end return M \ No newline at end of file -- 2.47.2