Commit cd5d5f0d authored by Marek Vavrusa's avatar Marek Vavrusa

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
parent e8371896
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('<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 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"></td>');
tbl.append(row);
}
/* 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);
}
}
$(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 '<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');
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(
'<div class="alert alert-danger" role="alert">'+
'Couldn\'t add rule (code: '+data.status+', reason: '+data.responseText+').'+
'</div>'
);
});
});
});
\ No newline at end of file
......@@ -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', [[
<script type="text/javascript" src="daf.js"></script>
<table id="daf-rules"><th><td>No rules here yet.</td></th></table>
<div class="row">
<form id="daf-builder-form">
<div class="input-group">
<input type="text" id="daf-builder" class="form-control" aria-label="..." />
<div class="input-group-btn">
<button type="button" id="daf-add" class="btn btn-default" style="margin-top: -5px;">Add</button>
</div>
</div>
</form>
</div>
<div class="row">
<table id="daf-rules" class="table table-striped table-responsive">
<th><td>No rules here yet.</td></th>
</table>
</div>
]]}
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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment