webui/js/blocky4.js (674 lines of code) (raw):

// Async METHOD to API async function METHOD(method = 'POST', url = '', data) { let payload = { method: method, mode: 'cors', cache: 'no-cache', credentials: 'same-origin', headers: data ? { 'Content-Type': 'application/json' } : {}, redirect: 'follow', referrerPolicy: 'no-referrer' } if (data) { payload.body = JSON.stringify(data); } try { const response = await fetch(url, payload).catch( (e) => {throw e}); if (response.ok !== true) throw `HTTP Error ${response.status}: ${response.statusText}` let js = response.json(); return js } catch (e) { alert(e); } } // HTTP methods let DELETE = (url, data) => METHOD('DELETE', url, data); let GET = (url, data) => METHOD('GET', url, data); let PATCH = (url, data) => METHOD('PATCH', url, data); let POST = (url, data) => METHOD('POST', url, data); let PUT = (url, data) => METHOD('PUT', url, data); // Prettifier for large numbers (adds commas) Number.prototype.pretty = function(fix) { if (fix) { return String(this.toFixed(fix)).replace(/(\d)(?=(\d{3})+\.)/g, '$1,'); } return String(this.toFixed(0)).replace(/(\d)(?=(\d{3})+$)/g, '$1,'); }; // HTML shortcuts let htmlobj = (type, text) => { let obj = document.createElement(type); obj.innerText = text ? text : ""; return obj } let _h1 = (title) => htmlobj('h1', title); let _h2 = (title) => htmlobj('h2', title); let _span = (title) => htmlobj('span', title); let _p = (title) => htmlobj('p', title); let _br = (title) => htmlobj('br'); let _hr = (title) => htmlobj('hr'); let _tr = () => htmlobj('tr'); let _td = (title) => htmlobj('td', title); let _th = (title, width) => { let obj = htmlobj('th', title); if (width) obj.style.width = width + "px"; return obj } let _table = () => htmlobj('table'); let _a = (txt) => htmlobj('a', txt); async function unblock_ip(_entry, force = true) { if (confirm(`This will unblock ${_entry.ip} and add it to the allow-list for 10 minutes. Continue?`)) { result = await POST('allow', { ip: _entry.ip, reason: "Temporarily allow-listed for 10 minutes to unblock IP", expires: parseInt(new Date().getTime()/1000) + 600, force: force }) if (result.success === true) { alert("IP Unblocked. Please allow for a few moments for it to apply across the servers."); location.reload(); } else { alert(result.message); } } return false; } async function remove_allow(_entry) { if (confirm(`This will remove ${_entry.ip} from the allow list. Continue?`)) { result = await DELETE('allow', { ip: _entry.ip, force: true }) if (result.success === true) { alert("IP Removed from allow-list."); location.reload(); } else { alert(result.message); } } return false; } function unblock_link(entry, allowlist=false) { let link = _a(allowlist ? 'Remove' : 'Unblock'); let _entry = entry; link.setAttribute("href", 'javascript:void(0);'); if (allowlist === true) { link.addEventListener('click', () => remove_allow(_entry)); } else {link.addEventListener('click', () => unblock_ip(_entry));} return link; } async function prime_frontpage() { let all = await GET("all?short=true"); let main = document.getElementById('main'); main.innerHTML = ""; let block_count = all.total_block.pretty(); let h1 = _h1(`Recent activity (${block_count} blocks in total)`); main.appendChild(h1); all.block.sort((a,b) => b.timestamp - a.timestamp); // sort desc by timestamp // Recent blocks let activity_table = _table(); activity_table.style.tableLayout = 'fixed'; main.appendChild(activity_table); let theader = _tr(); theader.appendChild(_th('Source IP', 300)); theader.appendChild(_th('Added', 120)); theader.appendChild(_th('Expires', 120)); theader.appendChild(_th('Reason', 500)); theader.appendChild(_th('Actions', 100)); activity_table.appendChild(theader); let results_shown = 0; for (const entry of all.block) { let tr = _tr(); let td_ip = _td(entry.ip); td_ip.style.fontFamily = "monospace"; if (entry.ip.length > 16) td_ip.style.fontSize = "0.8rem"; let td_added = _td(moment(entry.timestamp*1000.0).fromNow()); let td_expires = _td(entry.expires > 0 ? moment(entry.expires*1000.0).fromNow() : 'Never'); let td_reason = _td(entry.reason); let td_action = _td(); td_action.appendChild(unblock_link(entry)); tr.appendChild(td_ip); tr.appendChild(td_added); tr.appendChild(td_expires); tr.appendChild(td_reason); tr.appendChild(td_action); activity_table.appendChild(tr); results_shown++; } if (results_shown === 0) { let tr = _tr(); tr.innerText = "No activity found..."; activity_table.appendChild(_tr); } } async function prime_search(target, state) { if (!state) { window.history.pushState({}, '', `?search:${target}`); } let main = document.getElementById('main'); main.innerHTML = ''; if (target && target.length > 0) { let title = _h1("Search results for " + target + ":"); main.appendChild(title); let p = _p("Searching, please wait..."); main.appendChild(p); let results = await POST('search', {source: target}); if (results.success === false) { p.innerText = results.message; return } main.removeChild(p); // Allow list results let h2 = _h2(`Allow list results (${results.allow.length})`); main.appendChild(h2); let allow_table = _table(); allow_table.style.tableLayout = 'fixed'; main.appendChild(allow_table); let theader = _tr(); theader.appendChild(_th('Source IP', 300)); theader.appendChild(_th('Added', 120)); theader.appendChild(_th('Expires', 120)); theader.appendChild(_th('Reason', 500)); theader.appendChild(_th('Actions', 100)); allow_table.appendChild(theader); let results_shown = 0; results.allow.sort((a,b) => b.timestamp - a.timestamp); // sort desc by timestamp for (const entry of results.allow) { let tr = _tr(); let td_ip = _td(entry.ip); let td_added = _td(moment(entry.timestamp * 1000.0).fromNow()); let td_expires = _td(entry.expires > 0 ? moment(entry.expires * 1000.0).fromNow() : 'Never'); let td_reason = _td(entry.reason); let td_action = _td(); td_action.appendChild(unblock_link(entry, true)); td_ip.style.fontFamily = "monospace"; if (entry.ip.length > 16) td_ip.style.fontSize = "0.8rem"; tr.appendChild(td_ip); tr.appendChild(td_added); tr.appendChild(td_expires); tr.appendChild(td_reason); tr.appendChild(td_action); allow_table.appendChild(tr); results_shown++; if (results_shown > 25 && results.block.length > 25) { break } } if (results_shown === 0) { let tr = _tr(); tr.innerText = "No results found..."; allow_table.appendChild(tr); } // Block list results let bh2 = _h2(`Block list results (${results.block.length})`); main.appendChild(bh2); let block_table = _table(); block_table.style.tableLayout = 'fixed'; main.appendChild(block_table); let btheader = _tr(); btheader.appendChild(_th('Source IP', 300)); btheader.appendChild(_th('Added', 120)); btheader.appendChild(_th('Expires', 120)); btheader.appendChild(_th('Reason', 500)); btheader.appendChild(_th('Actions', 100)); block_table.appendChild(btheader); results_shown = 0; results.block.sort((a,b) => b.timestamp - a.timestamp); // sort desc by timestamp for (const entry of results.block) { let tr = _tr(); let td_ip = _td(entry.ip); let td_added = _td(moment(entry.timestamp * 1000.0).fromNow()); let td_expires = _td(entry.expires > 0 ? moment(entry.expires * 1000.0).fromNow() : 'Never'); let td_reason = _td(entry.reason); let td_actions = _td(''); td_actions.appendChild(unblock_link(entry)); td_ip.style.fontFamily = "monospace"; if (entry.ip.length > 16) td_ip.style.fontSize = "0.8rem"; tr.appendChild(td_ip); tr.appendChild(td_added); tr.appendChild(td_expires); tr.appendChild(td_reason); tr.appendChild(td_actions); block_table.appendChild(tr); results_shown++; if (results_shown > 25 && results.block.length > 25) { break } } if (results_shown === 0) { let tr = _tr(); tr.innerText = "No results found..."; block_table.appendChild(tr); } // iptables results let ih2 = _h2(`Local iptables results (${results.iptables.length})`); main.appendChild(ih2); let iptables_table = _table(); iptables_table.style.tableLayout = 'fixed'; main.appendChild(iptables_table); let itheader = _tr(); itheader.appendChild(_th('Source IP', 300)); itheader.appendChild(_th('Host', 200)); itheader.appendChild(_th('Chain', 100)); itheader.appendChild(_th('Reason', 450)); itheader.appendChild(_th('Actions', 100)); iptables_table.appendChild(itheader); results_shown = 0; for (const entry of results.iptables) { let tr = _tr(); let td_ip = _td(entry.source); let td_host = _td(entry.hostname); let td_chain = _td(entry.chain); let td_reason = _td(entry.extensions.replace(/\/\*\s*(.+)\s*\*\//, (b,a) => a)); let td_actions = _td(); td_actions.appendChild(unblock_iptables_link(entry)); td_ip.style.fontFamily = "monospace"; if (entry.source.length > 16) td_ip.style.fontSize = "0.8rem"; tr.appendChild(td_ip); tr.appendChild(td_host); tr.appendChild(td_chain); tr.appendChild(td_reason); tr.appendChild(td_actions); iptables_table.appendChild(tr); results_shown++; if (results_shown > 25 && results.block.length > 25) { break } } if (results_shown === 0) { let tr = _tr(); tr.innerText = "No results found..."; iptables_table.appendChild(tr); } } else { //let x = await sleep(10); let p = _p("Use the search bar in the top left corner for now..."); main.appendChild(p); } } function unblock_iptables_link(entry) { let link = _a('Soft Unblock'); let _entry = entry; _entry.ip = _entry.source; link.setAttribute("href", 'javascript:void(0);'); link.addEventListener('click', () => unblock_ip(_entry, false)); return link; } function rule_tr(rule) { let tr = _tr(); // Description let t_desc = _td(); let x_desc = document.createElement('input'); x_desc.setAttribute('type', 'text'); x_desc.setAttribute('id', `desc_${rule.id}`); x_desc.style.width = "95%"; if (rule.description) { x_desc.value = rule.description; } else { x_desc.placeholder = "Create a new rule here..."; } t_desc.appendChild(x_desc); tr.appendChild(t_desc); // AggType let t_agg = _td(); let x_agg = document.createElement('select'); x_agg.setAttribute('id', `agg_${rule.id}`); for (let opt of ['requests', 'bytes']) { let x_opt = document.createElement('option'); x_opt.value = opt; x_opt.textContent = opt; if (opt == rule.aggtype) x_opt.setAttribute('selected', 'selected'); x_agg.appendChild(x_opt); } t_agg.appendChild(x_agg); tr.appendChild(t_agg); // Limit let t_limit = _td(); let x_limit = document.createElement('input'); x_limit.setAttribute('type', 'number'); x_limit.value = rule.limit; x_limit.setAttribute('id', `limit_${rule.id}`); x_limit.style.width = "95%"; t_limit.appendChild(x_limit); tr.appendChild(t_limit); // Timespan let t_time = _td(); let x_time = document.createElement('input'); x_time.setAttribute('type', 'text'); x_time.setAttribute('id', `time_${rule.id}`); x_time.style.width = "95%"; x_time.value = rule.duration; t_time.appendChild(x_time); tr.appendChild(t_time); // Filters let t_filters = _td(); let x_filters = document.createElement('textarea'); x_filters.setAttribute('id', `filters_${rule.id}`); x_filters.style.width = "95%"; x_filters.textContent = rule.filters; t_filters.appendChild(x_filters) tr.appendChild(t_filters); // Actions let t_actions = _td(); t_actions.style.textAlign = 'center'; tr.appendChild(t_actions); if (rule.description) { let x_save = document.createElement('button'); x_save.innerText = 'Save'; x_save.addEventListener('click', () => patch_rule(rule)); t_actions.appendChild(x_save); let x_delete = document.createElement('button'); x_delete.style.marginLeft = '32px'; x_delete.innerText = 'Delete'; x_delete.addEventListener('click', () => delete_rule(rule)); t_actions.appendChild(x_delete); } else { let x_save = document.createElement('button'); x_save.innerText = 'Create Rule'; x_save.addEventListener('click', () => create_rule(rule)); t_actions.appendChild(x_save); } return tr } function fetch_rule_data(rule) { let desc = document.getElementById(`desc_${rule.id}`).value; let agg = document.getElementById(`agg_${rule.id}`).value; let limit = parseInt(document.getElementById(`limit_${rule.id}`).value); let duration = document.getElementById(`time_${rule.id}`).value; let filters = document.getElementById(`filters_${rule.id}`).value.trim(); return { description: desc, aggtype: agg, limit: limit, duration: duration, filter: filters } } async function delete_rule(rule) { if (confirm("Are you sure you wish to delete this rule?")) { let result = await DELETE('rules', {rule: rule.id}); alert(result.message); if (result.success === true) location.reload(); } } async function patch_rule(rule) { let new_rule = fetch_rule_data(rule); new_rule.rule = rule.id; let result = await PATCH('rules', new_rule); alert(result.message); if (result.success === true) location.reload(); } async function create_rule(rule) { let new_rule = fetch_rule_data(rule); let result = await PUT('rules', new_rule); alert(result.message); if (result.success === true) location.reload(); } async function prime_rules(target, state) { let main = document.getElementById('main'); main.innerHTML = ''; let title = _h1("Block rules"); main.appendChild(title); let t = _table(); t.style.tableLayout = 'fixed'; main.appendChild(t); let btheader = _tr(); btheader.appendChild(_th('Description', 500)); btheader.appendChild(_th('Aggregation Type', 160)); btheader.appendChild(_th('Limit', 140)); btheader.appendChild(_th('Timespan', 80)); btheader.appendChild(_th('Filters', 360)); btheader.appendChild(_th('Actions', 150)); t.appendChild(btheader); let rules = await GET('rules'); for (let rule of rules) { let tr = rule_tr(rule); t.appendChild(tr); } let tr = _tr(); let td = _td(); td.appendChild(_hr()); td.colSpan = 6; tr.appendChild(td); t.appendChild(tr); let new_rule = rule_tr({id: 9999, duration: "24h", limit: 100}); t.appendChild(new_rule); let p = _p("Filters support regular Lucene match (foo = bar), exact terms match (foo == bar), regexp (foo ~= ba[rz]). All matches can be negated with !, such as !=, !==, !~= etc") main.appendChild(p); } async function save_allow() { let ip = document.getElementById('add_source').value; let expiry = parseInt(document.getElementById('add_expiry').value); let reason = document.getElementById('add_reason').value; let host = document.getElementById('add_host').value; let true_expiry = parseInt(new Date().getTime() / 1000) + expiry; let force = document.getElementById('add_force').checked ? true : false; if (expiry == -1) true_expiry = -1; let result = await PUT('allow', { ip: ip, host: host, reason: reason, expires: true_expiry, force: force }); alert(result.message); if (result.success === true) location.reload(); } async function prime_allow() { let all = await GET("all?short=block"); let main = document.getElementById('main'); main.innerHTML = ""; let h2 = _h2("Add an allow rule:"); main.appendChild(h2); // Add an entry let add_table = _table(); add_table.style.tableLayout = 'fixed'; main.appendChild(add_table); let atheader = _tr(); atheader.appendChild(_th('Source IP', 300)); atheader.appendChild(_th('Expiry', 120)); atheader.appendChild(_th('Reason', 500)); atheader.appendChild(_th('Host', 100)); atheader.appendChild(_th('Force', 60)); atheader.appendChild(_th(' ', 100)); add_table.appendChild(atheader); let add_tr = _tr(); // source ip let add_source = _td(); let add_source_input = document.createElement('input'); add_source_input.placeholder = "CIDR, e.g. 127.0.0.1/32 or 2001:dead:beef::1" add_source_input.style.width = "95%"; add_source_input.id = "add_source" add_source.appendChild(add_source_input); add_tr.appendChild(add_source); // expiry let add_expiry = _td(); let add_expiry_input = document.createElement('select'); add_expiry_input.id = "add_expiry"; let options = { "10 minutes": 600, "1 hour": 3600, "2 hours": 7200, "12 hours": 43200, "24 hours": 86400, "7 days": 604800, "never": -1 } for (let key in options) { let x_opt = document.createElement('option'); x_opt.value = options[key]; x_opt.text = key; add_expiry_input.appendChild(x_opt); } add_expiry.appendChild(add_expiry_input); add_tr.appendChild(add_expiry); // Reason let add_reason = _td(); let add_reason_input = document.createElement('input'); add_reason_input.placeholder = "Enter a reason for allowing this IP/block." add_reason_input.style.width = "95%"; add_reason_input.id = "add_reason"; add_reason.appendChild(add_reason_input); add_tr.appendChild(add_reason); // Host let add_host = _td(); let add_host_input = document.createElement('input'); add_host_input.placeholder = "* or foo.apache.org"; add_host_input.value = "*"; add_host_input.style.width = "95%"; add_host_input.id = "add_host"; add_host.appendChild(add_host_input); add_tr.appendChild(add_host); // Force let add_force = _td(); let add_force_input = document.createElement('input'); add_force_input.setAttribute('type', 'checkbox'); add_force_input.setAttribute('value', 'true'); add_force_input.id = 'add_force'; add_force.appendChild(add_force_input); add_tr.appendChild(add_force); // Save button let add_save = _td(); let add_save_button = document.createElement('button'); add_save_button.innerText = "Add allow rule"; add_save_button.addEventListener('click', () => save_allow()); add_save.appendChild(add_save_button); add_tr.appendChild(add_save); add_table.appendChild(add_tr); main.appendChild(_hr()); let allow_count = all.allow.length.pretty(); let h1 = _h1(`Allowed IPs (${allow_count} entries in total)`); main.appendChild(h1); all.allow.sort((a,b) => b.timestamp - a.timestamp); // sort desc by timestamp // Current entries in allow list let activity_table = _table(); activity_table.style.tableLayout = 'fixed'; main.appendChild(activity_table); let theader = _tr(); theader.appendChild(_th('Source IP', 300)); theader.appendChild(_th('Added', 120)); theader.appendChild(_th('Expires', 120)); theader.appendChild(_th('Reason', 500)); theader.appendChild(_th('Host', 100)); theader.appendChild(_th('Actions', 100)); activity_table.appendChild(theader); let results_shown = 0; for (const entry of all.allow) { let tr = _tr(); let td_ip = _td(entry.ip); td_ip.style.fontFamily = "monospace"; if (entry.ip.length > 16) td_ip.style.fontSize = "0.8rem"; let td_added = _td(moment(entry.timestamp*1000.0).fromNow()); let td_expires = _td(entry.expires > 0 ? moment(entry.expires*1000.0).fromNow() : 'Never'); let td_reason = _td(entry.reason); let td_host = _td(entry.host); let td_action = _td(); td_action.appendChild(unblock_link(entry, true)); tr.appendChild(td_ip); tr.appendChild(td_added); tr.appendChild(td_expires); tr.appendChild(td_reason); tr.appendChild(td_host); tr.appendChild(td_action); activity_table.appendChild(tr); results_shown++; } if (results_shown === 0) { let tr = _tr(); tr.innerText = "No entries found..."; activity_table.appendChild(_tr); } } async function save_block() { let ip = document.getElementById('add_source').value; let expiry = parseInt(document.getElementById('add_expiry').value); let reason = document.getElementById('add_reason').value; let host = document.getElementById('add_host').value; let true_expiry = parseInt(new Date().getTime() / 1000) + expiry; let force = document.getElementById('add_force').checked ? true : false; if (expiry == -1) true_expiry = -1; let result = await PUT('block', { ip: ip, host: host, reason: reason, expires: true_expiry, force: force }); alert(result.message); if (result.success === true) location.reload(); } async function prime_block() { let all = await GET("all"); let main = document.getElementById('main'); main.innerHTML = ""; let h2 = _h1("Add a block rule:"); main.appendChild(h2); // Add an entry let add_table = _table(); add_table.style.tableLayout = 'fixed'; main.appendChild(add_table); let atheader = _tr(); atheader.appendChild(_th('Source IP', 240)); atheader.appendChild(_th('Expiry', 80)); atheader.appendChild(_th('Force Block', 100)); atheader.appendChild(_th('Reason', 500)); atheader.appendChild(_th('Host', 100)); atheader.appendChild(_th(' ', 100)); add_table.appendChild(atheader); let add_tr = _tr(); // source ip let add_source = _td(); let add_source_input = document.createElement('input'); add_source_input.placeholder = "CIDR, e.g. 127.0.0.1/32 or 2001:dead:beef::1" add_source_input.style.width = "95%"; add_source_input.id = "add_source" add_source.appendChild(add_source_input); add_tr.appendChild(add_source); // expiry let add_expiry = _td(); let add_expiry_input = document.createElement('select'); add_expiry_input.id = "add_expiry"; let options = { "1 hour": 3600, "2 hours": 7200, "12 hours": 43200, "24 hours": 86400, "7 days": 86400 * 7, "30 days": 86400 * 30, "60 days": 86400 * 60, "90 days": 86400 * 90, "never": -1 } for (let key in options) { let x_opt = document.createElement('option'); x_opt.value = options[key]; x_opt.text = key; add_expiry_input.appendChild(x_opt); } add_expiry.appendChild(add_expiry_input); add_tr.appendChild(add_expiry); // Force let add_force = _td(); let add_force_input = document.createElement('input'); add_force_input.setAttribute('type', 'checkbox'); add_force_input.setAttribute('value', 'true'); add_force_input.id = 'add_force'; add_force.appendChild(add_force_input); add_tr.appendChild(add_force); // Reason let add_reason = _td(); let add_reason_input = document.createElement('input'); add_reason_input.placeholder = "Enter a reason for allowing this IP/block." add_reason_input.style.width = "95%"; add_reason_input.id = "add_reason"; add_reason.appendChild(add_reason_input); add_tr.appendChild(add_reason); // Host let add_host = _td(); let add_host_input = document.createElement('input'); add_host_input.placeholder = "* or foo.apache.org"; add_host_input.value = "*"; add_host_input.style.width = "95%"; add_host_input.id = "add_host"; add_host.appendChild(add_host_input); add_tr.appendChild(add_host); // Save button let add_save = _td(); let add_save_button = document.createElement('button'); add_save_button.innerText = "Add block rule"; add_save_button.addEventListener('click', () => save_block()); add_save.appendChild(add_save_button); add_tr.appendChild(add_save); add_table.appendChild(add_tr); main.appendChild(_hr()); } let actions = { frontpage: prime_frontpage, allow: prime_allow, add: prime_block, search: prime_search, rules: prime_rules }; async function prime(args) { console.log(args); let segs = location.search.substr(1).match(/^([a-z]*):?(.*)$/); let action = segs[1]; let params = segs[2]; action_call = actions[action] ? actions[action] : actions['frontpage']; await action_call(params, args ? args.state : null); } window.onpopstate = prime;