mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-29 20:05:13 +08:00
💋 Sync 2026-10-04 04:16:09
This commit is contained in:
1 parent
f6b2df73ec
commit
f62992fd80
745 files changed
+212416
-1173
No files matched your search
@@ -0,0 +1,356 @@
|
||||
'use strict';
|
||||
'require dom';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
|
||||
const localFile = '/etc/banip/banip.allowlist';
|
||||
const maxSize = 100000;
|
||||
|
||||
function resetScroll() {
|
||||
document.querySelector('.main-right')?.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
function parseSections(rawText) {
|
||||
const lines = (rawText || '').split('\n');
|
||||
let currentSection = 'user'; // 'system' or 'user'
|
||||
const systemItems = [];
|
||||
const userItems = [];
|
||||
|
||||
lines.forEach(function (line) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
if (trimmed.startsWith('### --- SYSTEM') || trimmed.startsWith('### --- AUTO GENERATED')) {
|
||||
currentSection = 'system';
|
||||
return;
|
||||
}
|
||||
if (trimmed.startsWith('### --- USER') || trimmed.startsWith('### --- CUSTOM')) {
|
||||
currentSection = 'user';
|
||||
return;
|
||||
}
|
||||
|
||||
if (trimmed.startsWith('#')) return;
|
||||
|
||||
let val = trimmed;
|
||||
let cmt = '';
|
||||
const cmtIdx = trimmed.indexOf('#');
|
||||
if (cmtIdx >= 0) {
|
||||
val = trimmed.substring(0, cmtIdx).trim();
|
||||
cmt = trimmed.substring(cmtIdx + 1).trim();
|
||||
}
|
||||
|
||||
if (!val) return;
|
||||
|
||||
const entry = { value: val, comment: cmt };
|
||||
if (currentSection === 'system') {
|
||||
systemItems.push(entry);
|
||||
} else {
|
||||
userItems.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
return { systemItems: systemItems, userItems: userItems };
|
||||
}
|
||||
|
||||
function serializeAll(systemItems, userItems) {
|
||||
const output = [];
|
||||
if (systemItems && systemItems.length > 0) {
|
||||
output.push('### --- SYSTEM / AUTO GENERATED RULES ---');
|
||||
systemItems.forEach(function (item) {
|
||||
output.push(item.comment ? `${item.value} # ${item.comment}` : `${item.value} # System auto-rule`);
|
||||
});
|
||||
output.push('');
|
||||
}
|
||||
output.push('### --- USER CUSTOM RULES ---');
|
||||
if (userItems && userItems.length > 0) {
|
||||
userItems.forEach(function (item) {
|
||||
output.push(item.comment ? `${item.value} # ${item.comment}` : item.value);
|
||||
});
|
||||
}
|
||||
output.push('');
|
||||
return output.join('\n');
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
rawContent: '',
|
||||
isRawMode: false,
|
||||
systemItems: [],
|
||||
userItems: [],
|
||||
|
||||
load: function () {
|
||||
return L.resolveDefault(fs.stat(localFile), null)
|
||||
.then(function (stat) {
|
||||
if (!stat) {
|
||||
return fs.write(localFile, "").then(() => [{ size: 0 }, ""]);
|
||||
}
|
||||
return Promise.all([
|
||||
Promise.resolve(stat),
|
||||
L.resolveDefault(fs.read_direct(localFile), "")
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
render: function (allowlist) {
|
||||
const size = allowlist[0] ? allowlist[0].size : 0;
|
||||
this.rawContent = allowlist[1] != null ? allowlist[1] : '';
|
||||
const tooBig = size >= maxSize;
|
||||
const self = this;
|
||||
|
||||
const parsed = parseSections(this.rawContent);
|
||||
this.systemItems = parsed.systemItems;
|
||||
this.userItems = parsed.userItems;
|
||||
|
||||
if (tooBig) {
|
||||
resetScroll();
|
||||
ui.addNotification(null, E('p', _('The allowlist is too big, raw editor mode enforced.')), 'error');
|
||||
this.isRawMode = true;
|
||||
}
|
||||
|
||||
const mainContainer = E('div', { 'class': 'cbi-map' });
|
||||
|
||||
function syncAndRebuild() {
|
||||
self.rawContent = serializeAll(self.systemItems, self.userItems);
|
||||
renderContainer();
|
||||
}
|
||||
|
||||
/*
|
||||
1. Render System / Auto Generated Table (Pure OpenWrt Native)
|
||||
*/
|
||||
function renderSystemTable() {
|
||||
if (!self.systemItems || self.systemItems.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const boxes = self.systemItems.map(function (item) {
|
||||
return E('div', {
|
||||
'style': 'display: flex; align-items: center; justify-content: space-between; padding: .55em .85em; background: rgba(0, 0, 0, 0.025); border: 1px solid var(--cbi-border-color, #e5e7eb); border-radius: 6px;'
|
||||
}, [
|
||||
E('span', { 'style': 'font-family: monospace; font-weight: bold; font-size: .98em;' }, [item.value]),
|
||||
E('span', { 'style': 'font-size: .88em; color: #64748b; margin-left: .8em; white-space: nowrap;' }, [
|
||||
item.comment || _('Auto detected by SecBox / protected')
|
||||
])
|
||||
]);
|
||||
});
|
||||
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: center; margin-bottom: .4em;' }, [
|
||||
E('h3', { 'style': 'margin: 0;' }, [_('System Auto-Protected Rules')]),
|
||||
E('span', { 'style': 'font-size: .88em; color: #64748b;' }, [_('Auto detected by SecBox / protected')])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section-node', 'style': 'padding: .85em; background: var(--cbi-section-bg, #fff); border: 1px solid var(--cbi-border-color, #e5e7eb); border-radius: 6px;' }, [
|
||||
E('div', {
|
||||
'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(min(20em, 100%), 1fr)); gap: .6em .8em;'
|
||||
}, boxes)
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
2. Render User Custom Rules Table (Pure OpenWrt Native)
|
||||
*/
|
||||
function renderUserTable() {
|
||||
const rows = self.userItems.map(function (item, index) {
|
||||
return E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((index % 2) ? 2 : 1) }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'text-align: left !important; font-family: monospace;' }, [item.value]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'text-align: left !important;' }, [item.comment || '-']),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'text-align: right !important;' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-remove',
|
||||
'title': _('Delete this custom rule'),
|
||||
'click': function () {
|
||||
self.userItems.splice(index, 1);
|
||||
syncAndRebuild();
|
||||
}
|
||||
}, [_('Delete')])
|
||||
])
|
||||
]);
|
||||
});
|
||||
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', _('User Custom Rules')),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-titles' }, [
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 45%; text-align: left !important;' }, [_('Address / Domain / Network')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 40%; text-align: left !important;' }, [_('Comment')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 15%; text-align: right !important;' }, [_('Action')])
|
||||
]),
|
||||
E('tbody', {}, rows.length ? rows : [
|
||||
E('tr', { 'class': 'tr cbi-section-table-row' }, [
|
||||
E('td', { 'colspan': 3, 'class': 'td cbi-section-table-cell', 'style': 'text-align: center !important; color: #888; padding: 1.5em;' },
|
||||
[_('No custom allowlist entries yet. Add one below.')])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
3. Quick Add Section (Pure OpenWrt Native)
|
||||
*/
|
||||
function renderQuickAdd() {
|
||||
const valInput = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'style': 'width: 100%;',
|
||||
'placeholder': _('e.g. 192.168.1.100, 10.0.0.0/24, or example.com')
|
||||
});
|
||||
|
||||
const commentInput = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'style': 'width: 100%;',
|
||||
'placeholder': _('Optional comment (e.g. Office NAS)')
|
||||
});
|
||||
|
||||
function doAdd() {
|
||||
const val = valInput.value.trim();
|
||||
const cmt = commentInput.value.trim();
|
||||
if (!val) {
|
||||
ui.addNotification(null, E('p', _('Please specify a valid IP, CIDR, Domain or MAC address.')), 'warning');
|
||||
valInput.focus();
|
||||
return;
|
||||
}
|
||||
if (/[\s#]/.test(val)) {
|
||||
ui.addNotification(null, E('p', _('Format invalid: "%s" is not a valid address without spaces.').format(val)), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
self.userItems.unshift({ value: val, comment: cmt });
|
||||
valInput.value = '';
|
||||
commentInput.value = '';
|
||||
syncAndRebuild();
|
||||
ui.addNotification(null, E('p', _('Added custom rule "%s".').format(val)), 'info');
|
||||
}
|
||||
|
||||
valInput.addEventListener('keydown', function (ev) {
|
||||
if (ev.key === 'Enter') doAdd();
|
||||
});
|
||||
commentInput.addEventListener('keydown', function (ev) {
|
||||
if (ev.key === 'Enter') doAdd();
|
||||
});
|
||||
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', _('Add User Custom Rule')),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-titles' }, [
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 45%; text-align: left !important;' }, [_('Address or Domain')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 40%; text-align: left !important;' }, [_('Comment / Note')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 15%; text-align: right !important;' }, [_('Action')])
|
||||
]),
|
||||
E('tr', { 'class': 'tr cbi-section-table-row' }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'text-align: left !important;' }, [valInput]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'text-align: left !important;' }, [commentInput]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'text-align: right !important;' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-add',
|
||||
'click': doAdd
|
||||
}, [_('Add Rule')])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
4. Raw Textarea Editor
|
||||
*/
|
||||
function renderRawEditor() {
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', _('Raw Text Editor')),
|
||||
E('div', { 'class': 'cbi-section-descr' }, [
|
||||
_('One record per line. Supports IPv4, IPv6, CIDR, MAC, Domain and comments starting with "#".')
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('textarea', {
|
||||
'id': 'allowlist-raw-textarea',
|
||||
'class': 'cbi-input-textarea',
|
||||
'style': 'width: 100%; height: 480px; font-family: monospace; resize: vertical;',
|
||||
'spellcheck': 'false',
|
||||
'wrap': 'off',
|
||||
'input': function () {
|
||||
self.rawContent = this.value;
|
||||
}
|
||||
}, [self.rawContent || ''])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
Assemble the whole view
|
||||
*/
|
||||
function renderContainer() {
|
||||
dom.content(mainContainer, [
|
||||
E('h2', { 'name': 'content' }, [_('SecBox Allowlist')]),
|
||||
E('div', { 'class': 'cbi-map-descr' }, [
|
||||
_('Manage trusted local devices, subnets and domains that will never be blocked by SecBox.')
|
||||
]),
|
||||
E('div', { 'style': 'margin-bottom: 1em; text-align: right;' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-neutral',
|
||||
'click': function () {
|
||||
if (self.isRawMode) {
|
||||
const parsed = parseSections(self.rawContent);
|
||||
self.systemItems = parsed.systemItems;
|
||||
self.userItems = parsed.userItems;
|
||||
self.isRawMode = false;
|
||||
} else {
|
||||
self.rawContent = serializeAll(self.systemItems, self.userItems);
|
||||
self.isRawMode = true;
|
||||
}
|
||||
renderContainer();
|
||||
}
|
||||
}, [self.isRawMode ? _('Switch to Table View') : _('Switch to Raw Text Editor')])
|
||||
]),
|
||||
self.isRawMode
|
||||
? renderRawEditor()
|
||||
: E('div', {}, [
|
||||
renderQuickAdd(),
|
||||
renderUserTable(),
|
||||
renderSystemTable()
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
renderContainer();
|
||||
return mainContainer;
|
||||
},
|
||||
|
||||
handleSaveApply: function (ev, mode) {
|
||||
return this.handleSave(ev).then(function () {
|
||||
ui.showIndicator('banip-action', _('Reloading SecBox service...'));
|
||||
return fs.exec_direct('/etc/init.d/banip', ['reload'])
|
||||
.then(function () {
|
||||
ui.hideIndicator('banip-action');
|
||||
ui.addNotification(null, E('p', _('Allowlist saved and SecBox service reloaded successfully!')), 'info');
|
||||
})
|
||||
.catch(function (e) {
|
||||
ui.hideIndicator('banip-action');
|
||||
ui.addNotification(null, E('p', _('Saved allowlist, but failed to reload SecBox: %s').format(e.message)), 'warning');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
handleSave: function (ev) {
|
||||
const rawEl = document.getElementById('allowlist-raw-textarea');
|
||||
if (rawEl) {
|
||||
this.rawContent = rawEl.value;
|
||||
} else {
|
||||
this.rawContent = serializeAll(this.systemItems, this.userItems);
|
||||
}
|
||||
|
||||
return fs.write(localFile, this.rawContent || '')
|
||||
.then(function () {
|
||||
ui.addNotification(null, E('p', _('Allowlist changes saved.')), 'info');
|
||||
})
|
||||
.catch(function (e) {
|
||||
ui.addNotification(null, E('p', _('Unable to save changes: %s').format(e.message)), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,290 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require dom';
|
||||
|
||||
const localFile = '/etc/banip/banip.blocklist';
|
||||
const maxSize = 100000;
|
||||
|
||||
const resetScroll = () => {
|
||||
document.body.scrollTop = document.documentElement.scrollTop = 0;
|
||||
};
|
||||
|
||||
/* Regex definitions for validation */
|
||||
const ipv4Regex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
const cidrRegex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/([0-9]|[12][0-9]|3[0-2]))$/;
|
||||
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){1,7}[0-9a-fA-F]{1,4}(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$|^::1$|^([0-9a-fA-F]{1,4}:){1,7}:(\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8]))?$/;
|
||||
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
||||
const domainRegex = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
|
||||
const macIpBindingRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})\s+((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
|
||||
function isValidRule(token) {
|
||||
return macRegex.test(token) || cidrRegex.test(token) || ipv4Regex.test(token) ||
|
||||
macIpBindingRegex.test(token) || ipv6Regex.test(token) || domainRegex.test(token);
|
||||
}
|
||||
|
||||
function parseBlocklist(text) {
|
||||
const lines = (text || '').split('\n');
|
||||
const items = [];
|
||||
lines.forEach(function (raw, idx) {
|
||||
const line = raw.trim();
|
||||
if (!line || line.startsWith('#')) return;
|
||||
const parts = line.split(/\s+#\s*(.*)$/);
|
||||
const main = parts[0].trim();
|
||||
const comment = parts[1] ? parts[1].trim() : '';
|
||||
items.push({ id: idx, value: main, comment: comment, raw: line });
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
function serializeBlocklist(items) {
|
||||
return items.map(function (item) {
|
||||
return item.comment ? `${item.value} # ${item.comment}` : item.value;
|
||||
}).join('\n') + '\n';
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
rawContent: '',
|
||||
isRawMode: false,
|
||||
items: [],
|
||||
|
||||
load: function () {
|
||||
return L.resolveDefault(fs.stat(localFile), null)
|
||||
.then(function (stat) {
|
||||
if (!stat) {
|
||||
return fs.write(localFile, "").then(() => [{ size: 0 }, ""]);
|
||||
}
|
||||
return Promise.all([
|
||||
Promise.resolve(stat),
|
||||
L.resolveDefault(fs.read_direct(localFile), "")
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
render: function (blocklist) {
|
||||
const size = blocklist[0] ? blocklist[0].size : 0;
|
||||
this.rawContent = blocklist[1] != null ? blocklist[1] : '';
|
||||
const tooBig = size >= maxSize;
|
||||
const self = this;
|
||||
|
||||
this.items = parseBlocklist(this.rawContent);
|
||||
|
||||
if (tooBig) {
|
||||
resetScroll();
|
||||
ui.addNotification(null, E('p', _('The blocklist is too big, raw editor mode enforced.')), 'error');
|
||||
this.isRawMode = true;
|
||||
}
|
||||
|
||||
const mainContainer = E('div', { 'class': 'cbi-map' });
|
||||
|
||||
function syncAndRebuild() {
|
||||
self.rawContent = serializeBlocklist(self.items);
|
||||
renderContainer();
|
||||
}
|
||||
|
||||
/*
|
||||
1. Unified Blocklist Table (Pure OpenWrt Native)
|
||||
*/
|
||||
function renderTable() {
|
||||
const rows = self.items.map(function (item, index) {
|
||||
return E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((index % 2) ? 2 : 1) }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'text-align: left !important; font-family: monospace;' }, [item.value]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'text-align: left !important;' }, [item.comment || '-']),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'text-align: right !important;' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-remove',
|
||||
'title': _('Delete and unblock this rule'),
|
||||
'click': function () {
|
||||
self.items.splice(index, 1);
|
||||
syncAndRebuild();
|
||||
}
|
||||
}, [_('Delete')])
|
||||
])
|
||||
]);
|
||||
});
|
||||
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', _('Blocklist Entries')),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-titles' }, [
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 45%; text-align: left !important;' }, [_('Blocked Address / Domain / Network')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 40%; text-align: left !important;' }, [_('Comment / Source')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 15%; text-align: right !important;' }, [_('Action')])
|
||||
]),
|
||||
E('tbody', {}, rows.length ? rows : [
|
||||
E('tr', { 'class': 'tr cbi-section-table-row' }, [
|
||||
E('td', { 'colspan': 3, 'class': 'td cbi-section-table-cell', 'style': 'text-align: center !important; color: #888; padding: 2em;' },
|
||||
[_('Currently no blocked entries. Add one below.')])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
2. Quick Add Bar (Pure OpenWrt Native)
|
||||
*/
|
||||
function renderQuickAdd() {
|
||||
const valInput = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'style': 'width: 100%;',
|
||||
'placeholder': _('e.g. 198.51.100.1, 203.0.113.0/24, or malicious-site.com')
|
||||
});
|
||||
|
||||
const commentInput = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'style': 'width: 100%;',
|
||||
'placeholder': _('Optional comment (e.g. Malicious Bot)')
|
||||
});
|
||||
|
||||
function doAdd() {
|
||||
const val = valInput.value.trim();
|
||||
const cmt = commentInput.value.trim();
|
||||
if (!val) {
|
||||
ui.addNotification(null, E('p', _('Please specify a valid IP, CIDR, Domain or MAC address.')), 'warning');
|
||||
valInput.focus();
|
||||
return;
|
||||
}
|
||||
if (/[\s#]/.test(val)) {
|
||||
ui.addNotification(null, E('p', _('Format invalid: "%s" is not a valid address without spaces.').format(val)), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
self.items.unshift({ id: Date.now(), value: val, comment: cmt, raw: val });
|
||||
valInput.value = '';
|
||||
commentInput.value = '';
|
||||
syncAndRebuild();
|
||||
ui.addNotification(null, E('p', _('Added blocklist entry "%s".').format(val)), 'info');
|
||||
}
|
||||
|
||||
valInput.addEventListener('keydown', function (ev) {
|
||||
if (ev.key === 'Enter') doAdd();
|
||||
});
|
||||
commentInput.addEventListener('keydown', function (ev) {
|
||||
if (ev.key === 'Enter') doAdd();
|
||||
});
|
||||
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', _('Quick Add Block Rule')),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-titles' }, [
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 45%; text-align: left !important;' }, [_('Address or Domain')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 40%; text-align: left !important;' }, [_('Comment / Source')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 15%; text-align: right !important;' }, [_('Action')])
|
||||
]),
|
||||
E('tr', { 'class': 'tr cbi-section-table-row' }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'text-align: left !important;' }, [valInput]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'text-align: left !important;' }, [commentInput]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'text-align: right !important;' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-add',
|
||||
'click': doAdd
|
||||
}, [_('Add Rule')])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
3. Raw Textarea Editor
|
||||
*/
|
||||
function renderRawEditor() {
|
||||
return E('div', { 'class': 'cbi-section' }, [
|
||||
E('h3', _('Raw Text Editor')),
|
||||
E('div', { 'class': 'cbi-section-descr' }, [
|
||||
_('One record per line. Supports IPv4, IPv6, CIDR, MAC, Domain and comments starting with "#".')
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('textarea', {
|
||||
'id': 'blocklist-raw-textarea',
|
||||
'class': 'cbi-input-textarea',
|
||||
'style': 'width: 100%; height: 480px; font-family: monospace; resize: vertical;',
|
||||
'spellcheck': 'false',
|
||||
'wrap': 'off',
|
||||
'input': function () {
|
||||
self.rawContent = this.value;
|
||||
}
|
||||
}, [self.rawContent || ''])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
/*
|
||||
Assemble Container
|
||||
*/
|
||||
function renderContainer() {
|
||||
dom.content(mainContainer, [
|
||||
E('h2', { 'name': 'content' }, [_('SecBox Blocklist')]),
|
||||
E('div', { 'class': 'cbi-map-descr' }, [
|
||||
_('Manage locally blocked IP addresses, CIDR ranges, MAC addresses or domains.')
|
||||
]),
|
||||
E('div', { 'style': 'margin-bottom: 1em; text-align: right;' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-neutral',
|
||||
'click': function () {
|
||||
if (self.isRawMode) {
|
||||
self.items = parseBlocklist(self.rawContent);
|
||||
self.isRawMode = false;
|
||||
} else {
|
||||
self.rawContent = serializeBlocklist(self.items);
|
||||
self.isRawMode = true;
|
||||
}
|
||||
renderContainer();
|
||||
}
|
||||
}, [self.isRawMode ? _('Switch to Table View') : _('Switch to Raw Text Editor')])
|
||||
]),
|
||||
self.isRawMode
|
||||
? renderRawEditor()
|
||||
: E('div', {}, [
|
||||
renderQuickAdd(),
|
||||
renderTable()
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
renderContainer();
|
||||
return mainContainer;
|
||||
},
|
||||
|
||||
handleSaveApply: function (ev, mode) {
|
||||
return this.handleSave(ev).then(function () {
|
||||
ui.showIndicator('banip-action', _('Reloading SecBox service...'));
|
||||
return fs.exec_direct('/etc/init.d/banip', ['reload'])
|
||||
.then(function () {
|
||||
ui.hideIndicator('banip-action');
|
||||
ui.addNotification(null, E('p', _('Blocklist saved and SecBox service reloaded successfully!')), 'info');
|
||||
})
|
||||
.catch(function (e) {
|
||||
ui.hideIndicator('banip-action');
|
||||
ui.addNotification(null, E('p', _('Saved blocklist, but failed to reload SecBox: %s').format(e.message)), 'warning');
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
handleSave: function (ev) {
|
||||
const rawEl = document.getElementById('blocklist-raw-textarea');
|
||||
if (rawEl) {
|
||||
this.rawContent = rawEl.value;
|
||||
} else {
|
||||
this.rawContent = serializeBlocklist(this.items);
|
||||
}
|
||||
|
||||
return fs.write(localFile, this.rawContent || '')
|
||||
.then(function () {
|
||||
ui.addNotification(null, E('p', _('Blocklist changes saved.')), 'info');
|
||||
})
|
||||
.catch(function (e) {
|
||||
ui.addNotification(null, E('p', _('Unable to save changes: %s').format(e.message)), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
.cbi-input-text {
|
||||
width: 90% !important;
|
||||
margin-bottom: -5px;
|
||||
padding-top: 0rem;
|
||||
}
|
||||
.cbi-input-select {
|
||||
margin-bottom: -5px;
|
||||
padding-top: 0rem;
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require ui';
|
||||
|
||||
/*
|
||||
include custom CSS
|
||||
*/
|
||||
document.querySelector('head').appendChild(E('link', {
|
||||
'rel': 'stylesheet',
|
||||
'type': 'text/css',
|
||||
'href': L.resource('view/secbox/custom.css')
|
||||
}));
|
||||
|
||||
/*
|
||||
module-level file size set during render, read by observer
|
||||
*/
|
||||
let fileSize = 0;
|
||||
|
||||
/*
|
||||
button state helper
|
||||
*/
|
||||
function updateButtons() {
|
||||
const enable = function (id) {
|
||||
const btn = document.getElementById(id);
|
||||
if (btn) {
|
||||
btn.removeAttribute('disabled');
|
||||
}
|
||||
};
|
||||
if (fileSize === 0) {
|
||||
enable('btnCreate');
|
||||
enable('btnUpload');
|
||||
} else {
|
||||
enable('btnDownload');
|
||||
enable('btnClear');
|
||||
enable('btnSave');
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
observe DOM changes
|
||||
*/
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
if (mutations) {
|
||||
const inputs = document.querySelectorAll('input');
|
||||
inputs.forEach(function (input) {
|
||||
input.setAttribute('autocomplete', 'off')
|
||||
input.setAttribute('autocorrect', 'off')
|
||||
input.setAttribute('autocapitalize', 'off')
|
||||
input.setAttribute('spellcheck', false)
|
||||
})
|
||||
const labels = document.querySelectorAll('label[for^="widget.cbid.json"][for$="name"]');
|
||||
labels.forEach(function (label) {
|
||||
label.setAttribute("style", "font-weight: bold !important; color: #595 !important;");
|
||||
})
|
||||
updateButtons();
|
||||
}
|
||||
});
|
||||
|
||||
const targetNode = document.getElementById('view');
|
||||
const observerConfig = {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: false,
|
||||
characterData: false
|
||||
};
|
||||
observer.observe(targetNode, observerConfig);
|
||||
|
||||
/*
|
||||
button handling
|
||||
*/
|
||||
function handleEdit(ev) {
|
||||
if (ev === 'upload') {
|
||||
return ui.uploadFile('/etc/banip/banip.custom.feeds').then(function () {
|
||||
return L.resolveDefault(fs.read_direct('/etc/banip/banip.custom.feeds', 'json'), "").then(function (data) {
|
||||
if (data && Object.keys(data).length > 0) {
|
||||
for (let i = 0; i < Object.keys(data).length; i++) {
|
||||
let feed = Object.keys(data)[i];
|
||||
let descr = data[feed]?.descr;
|
||||
if (feed && descr) {
|
||||
continue;
|
||||
}
|
||||
return fs.write('/etc/banip/banip.custom.feeds', null).then(function () {
|
||||
ui.addNotification(null, E('p', _('Upload of the custom feed file failed.')), 'error');
|
||||
});
|
||||
}
|
||||
location.reload();
|
||||
} else {
|
||||
return fs.write('/etc/banip/banip.custom.feeds', null).then(function () {
|
||||
ui.addNotification(null, E('p', _('Upload of the custom feed file failed.')), 'error');
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(function () { });
|
||||
}
|
||||
if (ev === 'download') {
|
||||
return fs.read_direct('/etc/banip/banip.custom.feeds', 'blob').then(function (blob) {
|
||||
let url = window.URL.createObjectURL(blob),
|
||||
date = new Date(),
|
||||
name = 'banip.custom.feeds_%04d-%02d-%02d.json'.format(date.getFullYear(), date.getMonth() + 1, date.getDate()),
|
||||
link = E('a', { 'style': 'display:none', 'href': url, 'download': name });
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
}).catch(function () { });
|
||||
}
|
||||
if (ev === 'create') {
|
||||
return fs.read_direct('/etc/banip/banip.feeds', 'json').then(function (content) {
|
||||
fs.write('/etc/banip/banip.custom.feeds', JSON.stringify(content)).then(function () {
|
||||
location.reload();
|
||||
});
|
||||
});
|
||||
}
|
||||
if (ev === 'clear') {
|
||||
return fs.write('/etc/banip/banip.custom.feeds', null).then(function () {
|
||||
location.reload();
|
||||
});
|
||||
}
|
||||
if (ev === 'save') {
|
||||
const invalid = document.querySelectorAll('.cbi-input-invalid');
|
||||
if (invalid.length > 0) {
|
||||
document.body.scrollTop = document.documentElement.scrollTop = 0;
|
||||
return ui.addNotification(null, E('p', _('Invalid input values, unable to save modifications.')), 'error');
|
||||
}
|
||||
}
|
||||
/*
|
||||
gather all input data and fall through from 'save'
|
||||
*/
|
||||
const exportObj = {};
|
||||
const nodeKeys = document.querySelectorAll('[id^="widget.cbid.json"][id$="name"]');
|
||||
for (const keyNode of nodeKeys) {
|
||||
const keyValue = keyNode.value?.trim();
|
||||
if (!keyValue) continue;
|
||||
const idParts = keyNode.id.split(".");
|
||||
const ruleId = idParts[3];
|
||||
if (!ruleId) continue;
|
||||
const selector =
|
||||
`[id^="widget.cbid.json.${ruleId}."], ` +
|
||||
`[id^="cbid.json.${ruleId}.rule"]`;
|
||||
const elements = document.querySelectorAll(selector);
|
||||
const sub = {};
|
||||
for (const el of elements) {
|
||||
const parts = el.id.split(".");
|
||||
const key = parts[parts.length - 1];
|
||||
const value = el.value?.trim();
|
||||
if (!value) continue;
|
||||
if (["url_4", "url_6", "rule", "chain", "descr", "flag"].includes(key)) {
|
||||
sub[key] = value;
|
||||
}
|
||||
}
|
||||
/* require at least descr and rule to produce a valid feed entry */
|
||||
if (sub.descr && sub.rule && (sub.url_4 || sub.url_6)) {
|
||||
exportObj[keyValue] = sub;
|
||||
}
|
||||
}
|
||||
/*
|
||||
save to file and reload
|
||||
*/
|
||||
const exportJson = JSON.stringify(exportObj, null, 4);
|
||||
return fs.write('/etc/banip/banip.custom.feeds', exportJson)
|
||||
.then(() => location.reload());
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function () {
|
||||
return L.resolveDefault(fs.stat('/etc/banip/banip.custom.feeds'), null)
|
||||
.then(function (stat) {
|
||||
if (!stat) {
|
||||
return fs.write('/etc/banip/banip.custom.feeds', "").then(function () {
|
||||
return { size: 0, data: null };
|
||||
});
|
||||
}
|
||||
return L.resolveDefault(fs.read_direct('/etc/banip/banip.custom.feeds', 'json'), "")
|
||||
.then(function (data) {
|
||||
return { size: stat.size, data: data };
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function (result) {
|
||||
let m, s, o, feed, url_4, url_6, rule, chain, descr, flag;
|
||||
const data = result.data;
|
||||
fileSize = result.size;
|
||||
|
||||
m = new form.JSONMap(data, null, _('With this editor you can upload your local custom feed file or fill up an initial one (a 1:1 copy of the version shipped with the package). The file is located at \'/etc/banip/banip.custom.feeds\'. Then you can edit this file, delete entries, add new ones or make a local backup. To go back to the maintainers version just clear the custom feed file.'));
|
||||
for (let i = 0; i < Object.keys(m.data.data).length; i++) {
|
||||
feed = Object.keys(m.data.data)[i];
|
||||
url_4 = m.data.data[feed].url_4;
|
||||
url_6 = m.data.data[feed].url_6;
|
||||
rule = m.data.data[feed].rule;
|
||||
chain = m.data.data[feed].chain;
|
||||
descr = m.data.data[feed].descr;
|
||||
flag = m.data.data[feed].flag;
|
||||
|
||||
s = m.section(form.TypedSection, feed, null);
|
||||
s.addremove = true;
|
||||
s.anonymous = true;
|
||||
|
||||
o = s.option(form.Value, 'name', _('Feed Name'));
|
||||
o.ucioption = '.name';
|
||||
o.datatype = 'and(minlength(3),maxlength(15))';
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return _('Empty field not allowed');
|
||||
}
|
||||
if (!value.match(/^[a-z0-9]+$/)) {
|
||||
return _('Invalid characters');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'url_4', _('URLv4'));
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if (!value.match(/^https?:\/\/[A-Za-z0-9[\]/.?&+_@%=:~#-]+$/)) {
|
||||
return _('Protocol/URL format not supported');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'url_6', _('URLv6'));
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if (!value.match(/^https?:\/\/[A-Za-z0-9[\]/.?&+_@%=:~#-]+$/)) {
|
||||
return _('Protocol/URL format not supported');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'rule', _('Rule'));
|
||||
o.value('feed 1', _('<IP-Address>'));
|
||||
o.value('feed 1 ,', _('<IP-Address><CSV-Separator>'));
|
||||
o.value('feed 13', _('<IP-Address><Space><Netmask>'));
|
||||
o.value('feed 4 "', _('<JSON Lines><IP-Address><JSON Lines>'));
|
||||
o.value('suricata 1', _('<Suricata Syntax>'));
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.option(form.ListValue, 'chain', _('Chain'));
|
||||
o.value('in', _('Inbound'));
|
||||
o.value('out', _('Outbound'));
|
||||
o.value('inout', _('Inbound & Outbound'));
|
||||
o.default = 'in';
|
||||
|
||||
o = s.option(form.Value, 'descr', _('Description'));
|
||||
o.datatype = 'and(minlength(5),maxlength(30))';
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return _('Empty field not allowed');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
o = s.option(form.Value, 'flag', _('Flag'));
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if (!value.match(/^(\bgz\b|\btcp\b|\budp\b|\b[0-9-]+\b| )*$/)) {
|
||||
return _('Flag not supported');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
s = m.section(form.NamedSection, 'global');
|
||||
s.render = L.bind(function () {
|
||||
return E('div', { 'class': 'cbi-page-actions' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply',
|
||||
'id': 'btnSave',
|
||||
'disabled': 'disabled',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleEdit('save');
|
||||
})
|
||||
}, [_('Save')]),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'id': 'btnDownload',
|
||||
'disabled': 'disabled',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleEdit('download');
|
||||
})
|
||||
}, [_('Download')]),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'id': 'btnUpload',
|
||||
'disabled': 'disabled',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleEdit('upload');
|
||||
})
|
||||
}, [_('Upload')]),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'id': 'btnCreate',
|
||||
'disabled': 'disabled',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleEdit('create');
|
||||
})
|
||||
}, [_('Fill')]),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-reset',
|
||||
'id': 'btnClear',
|
||||
'disabled': 'disabled',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleEdit('clear');
|
||||
})
|
||||
}, [_('Clear')])
|
||||
]);
|
||||
});
|
||||
return m.render();
|
||||
},
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
'use strict';
|
||||
'require view.secbox.logtemplate as LogTemplate';
|
||||
|
||||
return LogTemplate.Logview('banIP/', _('Firewall Log'));
|
||||
@@ -0,0 +1,91 @@
|
||||
'use strict';
|
||||
'require rpc';
|
||||
'require view';
|
||||
'require dom';
|
||||
'require ui';
|
||||
|
||||
const callLogRead = rpc.declare({
|
||||
object: 'log',
|
||||
method: 'read',
|
||||
params: ['lines', 'stream', 'oneshot'],
|
||||
expect: {}
|
||||
});
|
||||
|
||||
return L.Class.extend({
|
||||
Logview: function (logtag, title) {
|
||||
return L.view.extend({
|
||||
load: () => Promise.resolve(),
|
||||
|
||||
render: function () {
|
||||
let autoScroll = true;
|
||||
|
||||
const pollFn = () => {
|
||||
return callLogRead(1000, false, true).then(res => {
|
||||
const logEl = document.getElementById('logfile');
|
||||
if (!logEl) return;
|
||||
const filtered = (res?.log ?? [])
|
||||
.filter(entry => {
|
||||
if (!logtag) return true;
|
||||
if (entry.msg.includes(logtag)) return true;
|
||||
const cleanTag = logtag.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
|
||||
return entry.msg.toLowerCase().includes(cleanTag);
|
||||
})
|
||||
.map(entry => {
|
||||
const d = new Date(entry.time);
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
const date = `${pad(d.getFullYear())}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
||||
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
return `[${date} ${time}] ${entry.msg}`;
|
||||
});
|
||||
logEl.value = filtered.length > 0
|
||||
? filtered.join('\n')
|
||||
: _('No related logs');
|
||||
if (autoScroll) {
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this._pollFn = pollFn;
|
||||
L.Poll.add(pollFn, 3);
|
||||
|
||||
const autoScrollBtn = E('button', {
|
||||
'class': 'btn cbi-button cbi-button-neutral',
|
||||
'style': 'margin-bottom: .6em;',
|
||||
'click': function () {
|
||||
autoScroll = !autoScroll;
|
||||
this.textContent = autoScroll ? _('Auto-scroll: ON') : _('Auto-scroll: OFF');
|
||||
}
|
||||
}, [_('Auto-scroll: ON')]);
|
||||
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', { 'name': 'content' }, [title]),
|
||||
E('div', { 'style': 'text-align: right;' }, [autoScrollBtn]),
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('textarea', {
|
||||
'id': 'logfile',
|
||||
'class': 'cbi-input-textarea',
|
||||
'style': 'width: 100% !important; box-sizing: border-box !important; height: calc(100vh - 270px); min-height: 520px; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12.5px; line-height: 1.5; resize: none !important; display: block;',
|
||||
'readonly': 'readonly',
|
||||
'spellcheck': 'false',
|
||||
'wrap': 'off'
|
||||
})
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
unload: function () {
|
||||
if (this._pollFn) {
|
||||
L.Poll.remove(this._pollFn);
|
||||
this._pollFn = null;
|
||||
}
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,394 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>SecBox Map</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||
<style>
|
||||
html {
|
||||
color-scheme: light dark;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.6em;
|
||||
padding: 0.5em 0.8em;
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
font-size: 14px;
|
||||
color: CanvasText;
|
||||
background: Canvas;
|
||||
border-bottom: 1px solid ButtonBorder;
|
||||
}
|
||||
|
||||
#setSelect {
|
||||
min-width: 16em;
|
||||
max-width: 70vw;
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
#map {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-size: small;
|
||||
font-family: monospace;
|
||||
white-space: nowrap;
|
||||
margin: 0.3em !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="toolbar">
|
||||
<label for="setSelect">Set:</label>
|
||||
<select id="setSelect"></select>
|
||||
</div>
|
||||
|
||||
<div id="map"></div>
|
||||
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
|
||||
<script>
|
||||
'use strict';
|
||||
|
||||
/* maximum display length of the free form GeoIP fields */
|
||||
const MAX_IP_LEN = 38;
|
||||
const MAX_AS_LEN = 38;
|
||||
const MAX_CITY_LEN = 33;
|
||||
|
||||
/* zoom level applied when the view is centered on a single point */
|
||||
const POINT_ZOOM = 6;
|
||||
|
||||
/* mapData key holding the geo data of the local uplink(s) */
|
||||
const HOME_KEY = 'homeIP';
|
||||
|
||||
const HOME_STYLE = { color: '#378242', opacity: 1.0, fillColor: '#378242', fillOpacity: 0.5, radius: 6 };
|
||||
const BLOCKED_STYLE = { color: '#C22121', opacity: 1.0, fillColor: '#C22121', fillOpacity: 0.5, radius: 3 };
|
||||
|
||||
/* initialize map, canvas rendering keeps the per Set marker volume cheap */
|
||||
const map = L.map('map', {
|
||||
zoom: 3,
|
||||
minZoom: 2,
|
||||
maxZoom: 18,
|
||||
center: [50, 10],
|
||||
worldCopyJump: true,
|
||||
preferCanvas: true
|
||||
});
|
||||
|
||||
L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map);
|
||||
|
||||
/* feature groups, so redrawing is a single clearLayers() and getBounds() is available */
|
||||
const homeLayer = L.featureGroup().addTo(map);
|
||||
const blockedLayer = L.featureGroup().addTo(map);
|
||||
|
||||
const setSelect = document.getElementById('setSelect');
|
||||
|
||||
/* accept a coordinate only if it is a finite number in range */
|
||||
function toLatLng(geo) {
|
||||
const lat = Number(geo.lat);
|
||||
const lon = Number(geo.lon);
|
||||
|
||||
if (geo.lat === null || geo.lat === undefined || geo.lat === '' ||
|
||||
geo.lon === null || geo.lon === undefined || geo.lon === '')
|
||||
return null;
|
||||
|
||||
if (!Number.isFinite(lat) || !Number.isFinite(lon))
|
||||
return null;
|
||||
|
||||
if (lat < -90 || lat > 90 || lon < -180 || lon > 180)
|
||||
return null;
|
||||
|
||||
return [lat, lon];
|
||||
}
|
||||
|
||||
/* GeoIP lookups regularly return empty city/as fields, keep the entry anyway */
|
||||
function fieldText(value, maxLength) {
|
||||
const text = String(value === null || value === undefined ? '' : value).trim();
|
||||
|
||||
if (!text)
|
||||
return 'n/a';
|
||||
|
||||
return text.length > maxLength ? text.slice(0, maxLength - 1) + '\u2026' : text;
|
||||
}
|
||||
|
||||
/*
|
||||
* Popups are built as DOM nodes, not as HTML strings, so the third party
|
||||
* GeoIP fields cannot be interpreted as markup. Passing a function to
|
||||
* bindPopup() defers this until the popup is actually opened.
|
||||
*/
|
||||
function buildPopup(title, entry) {
|
||||
const content = document.createElement('div');
|
||||
const heading = document.createElement('span');
|
||||
const body = document.createElement('p');
|
||||
const geo = entry.geo;
|
||||
const latlng = entry.latlng;
|
||||
const more = entry.count > 1 ? ' (+' + (entry.count - 1) + ' more)' : '';
|
||||
const lines = [
|
||||
'City: ' + fieldText(geo.city, MAX_CITY_LEN) + ' (' + fieldText(geo.countryCode, 8) + ')',
|
||||
'Latitude : ' + latlng[0],
|
||||
'Longitude: ' + latlng[1],
|
||||
'IP: ' + fieldText(geo.query, MAX_IP_LEN) + more,
|
||||
'AS: ' + fieldText(geo.as, MAX_AS_LEN)
|
||||
];
|
||||
|
||||
heading.className = 'title';
|
||||
heading.textContent = title;
|
||||
body.className = 'mono';
|
||||
|
||||
lines.forEach(function (line, index) {
|
||||
if (index)
|
||||
body.appendChild(document.createElement('br'));
|
||||
|
||||
body.appendChild(document.createTextNode(line));
|
||||
});
|
||||
|
||||
content.appendChild(heading);
|
||||
content.appendChild(body);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function addMarker(layer, title, entry, style) {
|
||||
L.circleMarker(entry.latlng, style)
|
||||
.bindPopup(function () {
|
||||
return buildPopup(title, entry);
|
||||
})
|
||||
.addTo(layer);
|
||||
}
|
||||
|
||||
/*
|
||||
* GeoIP resolves to city level, so several IPs of one Set regularly share a
|
||||
* coordinate. Stacked markers are indistinguishable and only the topmost one
|
||||
* can be opened, so collapse them into one marker and count the IPs behind it.
|
||||
* Collapsing happens per Set, otherwise the Set that reaches a coordinate first
|
||||
* would swallow the points of every later Set and make the counts depend on the
|
||||
* order of ban_map.jsn.
|
||||
*/
|
||||
function addEntry(list, index, geo, latlng) {
|
||||
const key = latlng[0] + ',' + latlng[1];
|
||||
const known = index.get(key);
|
||||
|
||||
if (known) {
|
||||
known.count++;
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = { geo: geo, latlng: latlng, count: 1 };
|
||||
|
||||
index.set(key, entry);
|
||||
list.push(entry);
|
||||
}
|
||||
|
||||
/*
|
||||
* mapData is an array of single key objects, the key being the SecBox Set name:
|
||||
*
|
||||
* [ {}, { "homeIP": {...} }, { "country_ir.v4": {...} }, ... ]
|
||||
*/
|
||||
function collectEntries(parsedData) {
|
||||
const result = { home: [], sets: {} };
|
||||
const homeIndex = new Map();
|
||||
const setIndex = new Map();
|
||||
|
||||
parsedData.forEach(function (item) {
|
||||
if (!item || typeof item !== 'object')
|
||||
return;
|
||||
|
||||
Object.keys(item).forEach(function (setName) {
|
||||
const geo = item[setName];
|
||||
|
||||
if (!geo || typeof geo !== 'object')
|
||||
return;
|
||||
|
||||
const latlng = toLatLng(geo);
|
||||
|
||||
if (!latlng)
|
||||
return;
|
||||
|
||||
if (setName === HOME_KEY) {
|
||||
addEntry(result.home, homeIndex, geo, latlng);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.sets[setName]) {
|
||||
result.sets[setName] = [];
|
||||
setIndex.set(setName, new Map());
|
||||
}
|
||||
|
||||
addEntry(result.sets[setName], setIndex.get(setName), geo, latlng);
|
||||
});
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function countIPs(entries) {
|
||||
let total = 0;
|
||||
|
||||
entries.forEach(function (entry) {
|
||||
total += entry.count;
|
||||
});
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
function setNames(sets) {
|
||||
return Object.keys(sets).sort();
|
||||
}
|
||||
|
||||
/*
|
||||
* An empty selection renders all Sets, otherwise the selected one only.
|
||||
* Every mapped location of a rendered Set is drawn, no cap is applied, so
|
||||
* the result does not depend on how f_report() truncates set_elements.
|
||||
*/
|
||||
function renderBlocked(sets, selected) {
|
||||
const names = selected ? [selected] : setNames(sets);
|
||||
|
||||
blockedLayer.clearLayers();
|
||||
|
||||
names.forEach(function (setName) {
|
||||
const entries = sets[setName] || [];
|
||||
|
||||
entries.forEach(function (entry) {
|
||||
addMarker(blockedLayer, 'blocked IP by ' + setName, entry, BLOCKED_STYLE);
|
||||
});
|
||||
});
|
||||
|
||||
/* the canvas renderer draws in insertion order, keep home on top */
|
||||
homeLayer.eachLayer(function (marker) {
|
||||
marker.bringToFront();
|
||||
});
|
||||
}
|
||||
|
||||
function fitLayer(layer) {
|
||||
const layers = layer.getLayers();
|
||||
|
||||
if (!layers.length)
|
||||
return false;
|
||||
|
||||
if (layers.length === 1)
|
||||
map.setView(layers[0].getLatLng(), POINT_ZOOM);
|
||||
else
|
||||
map.fitBounds(layer.getBounds(), { maxZoom: POINT_ZOOM, padding: [30, 30] });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* center on the local uplink(s), fall back to the blocked IPs */
|
||||
function resetView() {
|
||||
if (!fitLayer(homeLayer))
|
||||
fitLayer(blockedLayer);
|
||||
}
|
||||
|
||||
function addOption(value, label) {
|
||||
const option = document.createElement('option');
|
||||
|
||||
option.value = value;
|
||||
option.textContent = label;
|
||||
setSelect.appendChild(option);
|
||||
}
|
||||
|
||||
function setNotice(label) {
|
||||
setSelect.textContent = '';
|
||||
addOption('', label);
|
||||
setSelect.disabled = true;
|
||||
}
|
||||
|
||||
function plural(count, noun) {
|
||||
return count + ' ' + noun + (count === 1 ? '' : 's');
|
||||
}
|
||||
|
||||
function optionLabel(name, entries) {
|
||||
return name + ' (' + plural(countIPs(entries), 'IP') +
|
||||
' at ' + plural(entries.length, 'location') + ')';
|
||||
}
|
||||
|
||||
function fillSelect(sets) {
|
||||
const names = setNames(sets);
|
||||
let all = [];
|
||||
|
||||
names.forEach(function (setName) {
|
||||
all = all.concat(sets[setName]);
|
||||
});
|
||||
|
||||
if (!names.length) {
|
||||
setNotice('No mappable Set entries');
|
||||
return;
|
||||
}
|
||||
|
||||
setSelect.textContent = '';
|
||||
setSelect.disabled = false;
|
||||
addOption('', optionLabel('All Sets', all));
|
||||
|
||||
names.forEach(function (setName) {
|
||||
addOption(setName, optionLabel(setName, sets[setName]));
|
||||
});
|
||||
}
|
||||
|
||||
/* load the data handed over by the SecBox set report page */
|
||||
const mapData = sessionStorage.getItem('mapData');
|
||||
let parsedData = null;
|
||||
|
||||
if (mapData) {
|
||||
try {
|
||||
parsedData = JSON.parse(mapData);
|
||||
} catch (error) {
|
||||
console.error('SecBox: unable to parse mapData,', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (!mapData) {
|
||||
setNotice('No SecBox map data available');
|
||||
} else if (!Array.isArray(parsedData)) {
|
||||
setNotice('Unable to load SecBox map data');
|
||||
} else {
|
||||
const entries = collectEntries(parsedData);
|
||||
|
||||
entries.home.forEach(function (entry) {
|
||||
addMarker(homeLayer, 'local IP', entry, HOME_STYLE);
|
||||
});
|
||||
|
||||
fillSelect(entries.sets);
|
||||
renderBlocked(entries.sets, '');
|
||||
resetView();
|
||||
|
||||
setSelect.addEventListener('change', function () {
|
||||
renderBlocked(entries.sets, setSelect.value);
|
||||
|
||||
if (!setSelect.value || !fitLayer(blockedLayer))
|
||||
resetView();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,486 @@
|
||||
'use strict';
|
||||
'require dom';
|
||||
'require view';
|
||||
'require poll';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require uci';
|
||||
|
||||
/*
|
||||
service action handling
|
||||
*/
|
||||
function handleAction(ev) {
|
||||
ui.showIndicator('banip-action', _('Executing action: %s...').format(ev));
|
||||
return fs.exec_direct('/etc/init.d/banip', [ev])
|
||||
.then(function () {
|
||||
ui.hideIndicator('banip-action');
|
||||
ui.addNotification(null, E('p', _('SecBox service action "%s" executed successfully.').format(ev)), 'info');
|
||||
})
|
||||
.catch(function (e) {
|
||||
ui.hideIndicator('banip-action');
|
||||
ui.addNotification(null, E('p', _('Failed to execute action "%s": %s').format(ev, e.message)), 'error');
|
||||
});
|
||||
}
|
||||
|
||||
function handleToggleService(enable) {
|
||||
const action = enable ? 'start' : 'stop';
|
||||
const title = enable ? _('Enabling and starting SecBox service...') : _('Stopping and disabling SecBox service...');
|
||||
ui.showIndicator('banip-action', title);
|
||||
return uci.load('banip').then(function () {
|
||||
uci.set('banip', 'global', 'ban_enabled', enable ? '1' : '0');
|
||||
return uci.save();
|
||||
}).then(function () {
|
||||
return uci.apply();
|
||||
}).then(function () {
|
||||
return fs.exec_direct('/etc/init.d/banip', [enable ? 'restart' : 'stop']);
|
||||
}).then(function () {
|
||||
ui.hideIndicator('banip-action');
|
||||
ui.addNotification(null, E('p', enable ? _('SecBox service enabled and started.') : _('SecBox service stopped and disabled.')), 'info');
|
||||
setTimeout(function () {
|
||||
location.reload();
|
||||
}, 1000);
|
||||
}).catch(function (e) {
|
||||
ui.hideIndicator('banip-action');
|
||||
ui.addNotification(null, E('p', _('Failed to update service status: %s').format(e.message)), 'error');
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
runtime string helpers
|
||||
*/
|
||||
function parsePairs(text) {
|
||||
const pairs = [];
|
||||
(text || '').split(', ').forEach(function (item) {
|
||||
const idx = item.indexOf(': ');
|
||||
if (idx > 0) {
|
||||
pairs.push([item.substring(0, idx), item.substring(idx + 2)]);
|
||||
} else if (item.trim()) {
|
||||
pairs.push([null, item.trim()]);
|
||||
}
|
||||
});
|
||||
return pairs;
|
||||
}
|
||||
|
||||
function pickValue(pairs, key) {
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
if (pairs[i][0] === key) {
|
||||
return pairs[i][1];
|
||||
}
|
||||
}
|
||||
return '-';
|
||||
}
|
||||
|
||||
function expandPairs(pairs) {
|
||||
const result = [];
|
||||
pairs.forEach(function (pair) {
|
||||
if (!pair[0]) {
|
||||
result.push(pair);
|
||||
return;
|
||||
}
|
||||
const group = pair[0].match(/^(.*?)\s*\(([^()]*\/[^()]*)\)$/);
|
||||
if (group) {
|
||||
const names = group[2].split('/');
|
||||
const values = pair[1].split('/');
|
||||
if (names.length === values.length) {
|
||||
for (let i = 0; i < names.length; i++) {
|
||||
result.push([group[1] + ' ' + names[i], values[i]]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
result.push(pair);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function flagChips(text) {
|
||||
const pairs = expandPairs(parsePairs(text)).filter(p => p[0]);
|
||||
if (!pairs.length) return ['-'];
|
||||
return pairs.map(function (flag) {
|
||||
const on = flag[1] === '\u2714';
|
||||
return E('span', {
|
||||
'style': 'display: inline-block; margin: 0 .4em .35em 0; padding: .2em .55em; border-radius: 4px; font-size: .88em; white-space: nowrap;' +
|
||||
(on ? 'background: rgba(16, 185, 129, 0.15); color: #059669; border: 1px solid rgba(16, 185, 129, 0.3);' : 'background: rgba(0, 0, 0, 0.05); color: #64748b; border: 1px solid rgba(0, 0, 0, 0.1);')
|
||||
}, [
|
||||
(on ? '\u2714 ' : '\u2718 ') + flag[0]
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
function feedChips(feeds) {
|
||||
const chips = (Array.isArray(feeds) ? feeds : []).filter(f => f && f !== '-');
|
||||
if (!chips.length) return ['-'];
|
||||
return chips.map(function (feed) {
|
||||
return E('span', {
|
||||
'style': 'display: inline-block; margin: 0 .4em .35em 0; padding: .2em .55em; border-radius: 4px; font-size: .88em; white-space: nowrap; background: rgba(37, 99, 235, 0.1); color: #2563eb; border: 1px solid rgba(37, 99, 235, 0.25);'
|
||||
}, [feed]);
|
||||
});
|
||||
}
|
||||
|
||||
function sysPairs(text) {
|
||||
let plain = 0;
|
||||
return parsePairs(text).filter(function (pair) {
|
||||
return pair[0] || ++plain === 1;
|
||||
});
|
||||
}
|
||||
|
||||
function splitCount(text) {
|
||||
const value = text || '';
|
||||
const idx = value.indexOf(' (');
|
||||
if (idx < 0) {
|
||||
return { 'count': value || '-', 'detail': '-' };
|
||||
}
|
||||
return {
|
||||
'count': value.substring(0, idx) || '-',
|
||||
'detail': value.substring(idx + 2).replace(/\)$/, '') || '-'
|
||||
};
|
||||
}
|
||||
|
||||
function splitUplinks(list) {
|
||||
const v4 = [], v6 = [];
|
||||
(Array.isArray(list) ? list : []).forEach(function (addr) {
|
||||
if (addr) {
|
||||
(addr.indexOf(':') >= 0 ? v6 : v4).push(addr);
|
||||
}
|
||||
});
|
||||
return {
|
||||
'v4': v4.length ? v4.join(', ') : '-',
|
||||
'v6': v6.length ? v6.join(', ') : '-'
|
||||
};
|
||||
}
|
||||
|
||||
const keyMap = {
|
||||
'table': _('Table'),
|
||||
'priority': _('Priority'),
|
||||
'policy': _('Policy'),
|
||||
'proto 4': _('IPv4 Protocol'),
|
||||
'proto 6': _('IPv6 Protocol'),
|
||||
'limit icmp': _('ICMP Limit'),
|
||||
'limit syn': _('SYN Limit'),
|
||||
'limit udp': _('UDP Limit'),
|
||||
'cores': _('CPU Cores'),
|
||||
'log': _('Log Service'),
|
||||
'fetch': _('Download Tool'),
|
||||
'wan-dev': _('WAN Device'),
|
||||
'wan-if': _('WAN Interface'),
|
||||
'vlan-allow': _('Allowed VLAN'),
|
||||
'vlan-block': _('Blocked VLAN'),
|
||||
'uplink IPv4': _('Uplink IPv4'),
|
||||
'uplink IPv6': _('Uplink IPv6')
|
||||
};
|
||||
|
||||
const stateMap = {
|
||||
'active': _('Running (active)'),
|
||||
'processing': _('Processing...'),
|
||||
'disabled': _('Disabled'),
|
||||
'error': _('Error'),
|
||||
'stopped': _('Stopped'),
|
||||
'not running': _('Not Running')
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
uci.load('banip').catch(() => 0),
|
||||
L.resolveDefault(fs.stat('/var/run/banIP/banIP.runtime.json'), null)
|
||||
]);
|
||||
},
|
||||
|
||||
render: function (loadResult) {
|
||||
let isEnabled = (uci.get('banip', 'global', 'ban_enabled') === '1');
|
||||
|
||||
const setNodes = (id, nodes) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) dom.content(el, nodes);
|
||||
};
|
||||
|
||||
const setText = (id, value) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = value || '-';
|
||||
};
|
||||
|
||||
/*
|
||||
poll runtime information
|
||||
*/
|
||||
let parseErrCount = 0;
|
||||
poll.add(function () {
|
||||
return L.resolveDefault(fs.stat('/var/run/banIP/banIP.runtime.json'), null).then(function (stat) {
|
||||
if (!stat) return;
|
||||
return Promise.all([
|
||||
L.resolveDefault(fs.read_direct('/var/run/banIP/banIP.runtime.json'), 'null'),
|
||||
L.resolveDefault(fs.exec_direct('/etc/init.d/banip', ['actual']), '')
|
||||
]).then(function (results) {
|
||||
const res = results[0];
|
||||
const actual = results[1]?.trim() || '';
|
||||
const toggleBtn = document.getElementById('btn-toggle-service');
|
||||
const dotEl = document.getElementById('status-dot');
|
||||
let info = null;
|
||||
try {
|
||||
info = JSON.parse(res);
|
||||
parseErrCount = 0;
|
||||
} catch (e) {
|
||||
info = null;
|
||||
parseErrCount++;
|
||||
if (parseErrCount >= 3) {
|
||||
ui.addNotification(null, E('p', _('Unable to parse the SecBox runtime information!')), 'error');
|
||||
poll.stop();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (info) {
|
||||
const rawState = info.status || '-';
|
||||
const isRunning = (rawState === 'active' || rawState === 'processing');
|
||||
setText('state_text', stateMap[rawState] || rawState);
|
||||
setText('versions', `${info.frontend_ver || '-'} / ${info.backend_ver || '-'}`);
|
||||
setNodes('actual', actual ? flagChips(actual) : ['-']);
|
||||
|
||||
if (dotEl) {
|
||||
if (rawState === 'active') {
|
||||
dotEl.style.background = '#10b981';
|
||||
dotEl.style.boxShadow = '0 0 6px #10b981';
|
||||
} else if (rawState === 'processing') {
|
||||
dotEl.style.background = '#3b82f6';
|
||||
dotEl.style.boxShadow = 'none';
|
||||
} else if (rawState === 'error') {
|
||||
dotEl.style.background = '#ef4444';
|
||||
dotEl.style.boxShadow = 'none';
|
||||
} else {
|
||||
dotEl.style.background = '#94a3b8';
|
||||
dotEl.style.boxShadow = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (toggleBtn) {
|
||||
if (isRunning) {
|
||||
toggleBtn.textContent = _('Stop Service');
|
||||
toggleBtn.className = 'btn cbi-button cbi-button-reset';
|
||||
} else {
|
||||
toggleBtn.textContent = _('Start Service');
|
||||
toggleBtn.className = 'btn cbi-button cbi-button-save';
|
||||
}
|
||||
}
|
||||
|
||||
const elements = splitCount(info.element_count);
|
||||
const runPairs = parsePairs(info.last_run);
|
||||
const join = list => (Array.isArray(list) && list.length) ? list.join(', ') : '-';
|
||||
|
||||
setText('elements_count', elements.count);
|
||||
setText('elements_sub', elements.detail !== '-' ? elements.detail : '');
|
||||
setText('last_run_time', pickValue(runPairs, 'date / time'));
|
||||
setText('last_run_detail', [pickValue(runPairs, 'mode'), pickValue(runPairs, 'duration'), pickValue(runPairs, 'memory')].filter(v => v && v !== '-').join(', '));
|
||||
|
||||
setText('wan_dev', join(info.wan_devices));
|
||||
setText('wan_if', join(info.wan_interfaces));
|
||||
setText('vlan_allow', join(info.vlan_allow));
|
||||
setText('vlan_block', join(info.vlan_block));
|
||||
|
||||
const uplinks = splitUplinks(info.active_uplink);
|
||||
setText('uplink4', uplinks.v4);
|
||||
setText('uplink6', uplinks.v6);
|
||||
|
||||
// Render NFT pairs
|
||||
const nftPairs = expandPairs(parsePairs(info.nft_info));
|
||||
const nftNodes = [];
|
||||
nftPairs.forEach((p, idx) => {
|
||||
if (p[0]) {
|
||||
const k = keyMap[p[0].trim()] || p[0].trim();
|
||||
nftNodes.push(E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((idx % 2) ? 2 : 1) }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-col-key' }, [k]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-col-val', 'style': 'font-family: monospace;' }, [p[1]])
|
||||
]));
|
||||
}
|
||||
});
|
||||
setNodes('nft_tbody', nftNodes.length ? nftNodes : [
|
||||
E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-col-key' }, ['-']),
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-col-val' }, ['-'])
|
||||
])
|
||||
]);
|
||||
|
||||
// System Info (Full-width row, 16% key : 84% val)
|
||||
const sysList = sysPairs(info.system_info);
|
||||
const sysNodes = [];
|
||||
sysList.forEach((p, idx) => {
|
||||
if (p[0]) {
|
||||
const k = keyMap[p[0].trim()] || p[0].trim();
|
||||
sysNodes.push(E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((idx % 2) ? 2 : 1) }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-full-key' }, [k]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-full-val' }, [p[1]])
|
||||
]));
|
||||
} else {
|
||||
sysNodes.push(E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((idx % 2) ? 2 : 1) }, [
|
||||
E('td', { 'colspan': 2, 'class': 'td cbi-section-table-cell', 'style': 'text-align: left !important; color: #64748b; padding-left: .85em;' }, [p[1]])
|
||||
]));
|
||||
}
|
||||
});
|
||||
setNodes('sys_tbody', sysNodes.length ? sysNodes : [
|
||||
E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-full-key' }, ['-']),
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-full-val' }, ['-'])
|
||||
])
|
||||
]);
|
||||
|
||||
setNodes('feeds_container', feedChips(info.active_feeds));
|
||||
setNodes('flags_container', flagChips(info.run_flags));
|
||||
}
|
||||
});
|
||||
});
|
||||
}, 3);
|
||||
|
||||
const style = E('style', { 'type': 'text/css' }, [
|
||||
'#ban-op-grid .op-grid-2 { display: grid; gap: 1em; grid-template-columns: repeat(2, 1fr); margin-bottom: 1em; align-items: stretch; }' +
|
||||
'@media (max-width: 900px) { #ban-op-grid .op-grid-2 { grid-template-columns: 1fr; } }' +
|
||||
'#ban-op-grid .op-grid-1 { margin-bottom: 1em; width: 100%; }' +
|
||||
'#ban-op-grid .op-card { background: var(--cbi-section-bg, #fff); border: 1px solid var(--cbi-border-color, #e5e7eb); border-radius: 6px; overflow: hidden; display: flex; flex-direction: column; height: 100%; box-sizing: border-box; }' +
|
||||
'#ban-op-grid .op-card-header { background: rgba(0, 0, 0, 0.025); min-height: 2.85em; height: 2.85em; padding: 0 1em; box-sizing: border-box; border-bottom: 1px solid var(--cbi-border-color, #e5e7eb); font-weight: 700; font-size: 1.02em; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }' +
|
||||
'#ban-op-grid .op-card-body { padding: .85em 1em; flex: 1; display: flex; flex-direction: column; justify-content: center; }' +
|
||||
'#ban-op-grid .op-card-body-table { padding: 0 !important; justify-content: flex-start; }' +
|
||||
'#ban-op-grid .op-hero-val { font-size: 1.65em; font-weight: 700; line-height: 1.25; margin-bottom: .2em; }' +
|
||||
'#ban-op-grid .op-hero-sub { font-size: .88em; color: #64748b; }' +
|
||||
'#ban-op-grid table.table { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: fixed; margin: 0; }' +
|
||||
'#ban-op-grid table.table td { padding: .5em .85em; text-align: left !important; font-size: .95em; vertical-align: middle; }' +
|
||||
'#ban-op-grid .op-col-key { width: 32% !important; max-width: 32% !important; min-width: 32% !important; font-weight: 600; text-align: left !important; word-break: break-word; }' +
|
||||
'#ban-op-grid .op-col-val { width: 68% !important; text-align: left !important; word-break: break-all; }' +
|
||||
'#ban-op-grid .op-full-key { width: 16% !important; max-width: 16% !important; min-width: 16% !important; font-weight: 600; text-align: left !important; word-break: break-word; }' +
|
||||
'#ban-op-grid .op-full-val { width: 84% !important; text-align: left !important; word-break: break-word; }' +
|
||||
'@media (max-width: 768px) { #ban-op-grid .op-full-key { width: 28% !important; min-width: 28% !important; } #ban-op-grid .op-full-val { width: 72% !important; } }' +
|
||||
'@media (prefers-color-scheme: dark) {' +
|
||||
'#ban-op-grid .op-card { background: rgba(255, 255, 255, 0.03); border-color: rgba(255, 255, 255, 0.12); }' +
|
||||
'#ban-op-grid .op-card-header { background: rgba(255, 255, 255, 0.05); border-color: rgba(255, 255, 255, 0.12); }' +
|
||||
'}'
|
||||
]);
|
||||
|
||||
function ifRow(label, id, idx) {
|
||||
return E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((idx % 2) ? 2 : 1) }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-col-key' }, [label]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-col-val', 'id': id }, ['-'])
|
||||
]);
|
||||
}
|
||||
|
||||
return E('div', { 'class': 'cbi-map', 'id': 'ban-op-grid' }, [
|
||||
style,
|
||||
E('h2', { 'name': 'content' }, [_('SecBox Overview')]),
|
||||
E('div', { 'class': 'cbi-map-descr' }, [
|
||||
_('Real-time runtime status, active sets and blocking statistics for the SecBox service.')
|
||||
]),
|
||||
|
||||
/* Row 1: Strict 2 Columns (Status & Metrics) */
|
||||
E('div', { 'class': 'op-grid-2' }, [
|
||||
/* 1. Status */
|
||||
E('div', { 'class': 'op-card' }, [
|
||||
E('div', { 'class': 'op-card-header' }, [
|
||||
_('Status'),
|
||||
E('button', {
|
||||
'id': 'btn-toggle-service',
|
||||
'class': isEnabled ? 'btn cbi-button cbi-button-reset' : 'btn cbi-button cbi-button-save',
|
||||
'style': 'padding: .2em .65em; font-size: .88em; margin: 0;',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleToggleService(!isEnabled);
|
||||
})
|
||||
}, [isEnabled ? _('Stop Service') : _('Start Service')])
|
||||
]),
|
||||
E('div', { 'class': 'op-card-body' }, [
|
||||
E('div', { 'style': 'display: flex; align-items: center; gap: .5em; margin-bottom: .3em;' }, [
|
||||
E('span', { 'id': 'status-dot', 'style': 'width: .75em; height: .75em; border-radius: 50%; background: #94a3b8; display: inline-block;' }),
|
||||
E('span', { 'class': 'op-hero-val', 'id': 'state_text', 'style': 'margin-bottom: 0;' }, ['-'])
|
||||
]),
|
||||
E('div', { 'class': 'op-hero-sub' }, [
|
||||
_('Version'), ': ', E('span', { 'id': 'versions' }, ['-'])
|
||||
]),
|
||||
E('div', { 'id': 'actual', 'style': 'margin-top: .5em;' }, ['-'])
|
||||
])
|
||||
]),
|
||||
|
||||
/* 2. Statistics & Run Info */
|
||||
E('div', { 'class': 'op-card' }, [
|
||||
E('div', { 'class': 'op-card-header' }, [_('Rule Elements & Last Run')]),
|
||||
E('div', { 'class': 'op-card-body' }, [
|
||||
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: baseline;' }, [
|
||||
E('div', {}, [
|
||||
E('div', { 'class': 'op-hero-val', 'id': 'elements_count' }, ['-']),
|
||||
E('div', { 'class': 'op-hero-sub', 'id': 'elements_sub' }, ['-'])
|
||||
]),
|
||||
E('div', { 'style': 'text-align: right;' }, [
|
||||
E('div', { 'style': 'font-weight: 700; font-size: 1.15em;', 'id': 'last_run_time' }, ['-']),
|
||||
E('div', { 'class': 'op-hero-sub', 'id': 'last_run_detail' }, ['-'])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
/* Row 2: Strict 2 Columns (NFT Info & Interfaces) */
|
||||
E('div', { 'class': 'op-grid-2' }, [
|
||||
/* NFT Information */
|
||||
E('div', { 'class': 'op-card' }, [
|
||||
E('div', { 'class': 'op-card-header' }, [_('NFT Information')]),
|
||||
E('div', { 'class': 'op-card-body op-card-body-table' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tbody', { 'id': 'nft_tbody' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-col-key' }, ['-']),
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-col-val' }, ['-'])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
/* Interfaces */
|
||||
E('div', { 'class': 'op-card' }, [
|
||||
E('div', { 'class': 'op-card-header' }, [_('Interfaces')]),
|
||||
E('div', { 'class': 'op-card-body op-card-body-table' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
ifRow(_('WAN Device'), 'wan_dev', 0),
|
||||
ifRow(_('WAN Interface'), 'wan_if', 1),
|
||||
ifRow(_('Allowed VLAN'), 'vlan_allow', 2),
|
||||
ifRow(_('Blocked VLAN'), 'vlan_block', 3),
|
||||
ifRow(_('Uplink IPv4'), 'uplink4', 4),
|
||||
ifRow(_('Uplink IPv6'), 'uplink6', 5)
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
/* Row 3: Full-Width Row (System Info) */
|
||||
E('div', { 'class': 'op-grid-1' }, [
|
||||
E('div', { 'class': 'op-card' }, [
|
||||
E('div', { 'class': 'op-card-header' }, [_('System Info')]),
|
||||
E('div', { 'class': 'op-card-body op-card-body-table' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tbody', { 'id': 'sys_tbody' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-full-key' }, ['-']),
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-full-val' }, ['-'])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
/* Row 4: Full-Width Row (Active Feeds & Flags) */
|
||||
E('div', { 'class': 'op-grid-1' }, [
|
||||
E('div', { 'class': 'op-card' }, [
|
||||
E('div', { 'class': 'op-card-header' }, [_('Active Feeds & Flags')]),
|
||||
E('div', { 'class': 'op-card-body op-card-body-table' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-full-key' }, [_('Active Feeds')]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-full-val', 'id': 'feeds_container' }, ['-'])
|
||||
]),
|
||||
E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-2' }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-full-key' }, [_('Run Flags')]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell op-full-val', 'id': 'flags_container' }, ['-'])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
'use strict';
|
||||
'require view.secbox.logtemplate as LogTemplate';
|
||||
|
||||
return LogTemplate.Logview('banIP-', _('Processing Log'));
|
||||
@@ -0,0 +1,591 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require uci';
|
||||
|
||||
/*
|
||||
button handling
|
||||
*/
|
||||
function handleAction(report, ev) {
|
||||
if (ev === 'search') {
|
||||
const triggerSearch = function () {
|
||||
let searchInput = document.getElementById('search');
|
||||
let ip = searchInput ? searchInput.value.trim().toLowerCase() : '';
|
||||
|
||||
if (ip) {
|
||||
window._lastSearchedIP = ip;
|
||||
document.getElementById('result').textContent = _('Search is running, please wait...');
|
||||
if (window._banipPoller) {
|
||||
clearInterval(window._banipPoller);
|
||||
window._banipPoller = null;
|
||||
}
|
||||
L.resolveDefault(fs.write('/var/run/banIP/banIP.search', ''), '').then(function () {
|
||||
L.resolveDefault(fs.exec_direct('/etc/init.d/banip', ['search', ip]), '').then(function () {
|
||||
let attempts = 0;
|
||||
window._banipPoller = setInterval(function () {
|
||||
attempts++;
|
||||
L.resolveDefault(fs.read('/var/run/banIP/banIP.search'), '').then(function (res) {
|
||||
if (res && res.trim()) {
|
||||
clearInterval(window._banipPoller);
|
||||
window._banipPoller = null;
|
||||
document.getElementById('result').textContent = res.trim();
|
||||
} else if (attempts >= 40) {
|
||||
clearInterval(window._banipPoller);
|
||||
window._banipPoller = null;
|
||||
document.getElementById('result').textContent = _('Search timed out.');
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const appendToList = function (targetFile, listName) {
|
||||
const ip = window._lastSearchedIP || (document.getElementById('search')?.value || '').trim();
|
||||
if (!ip) {
|
||||
ui.addNotification(null, E('p', _('Please enter or search an IP first.')), 'warning');
|
||||
return;
|
||||
}
|
||||
return L.resolveDefault(fs.read_direct(targetFile), '').then(function (cur) {
|
||||
const lines = cur.split('\n').map(l => l.trim());
|
||||
if (lines.includes(ip)) {
|
||||
ui.addNotification(null, E('p', _('IP "%s" is already in %s.').format(ip, listName)), 'info');
|
||||
return;
|
||||
}
|
||||
const newContent = (cur.trim() ? cur.trim() + '\n' : '') + ip + '\n';
|
||||
return fs.write(targetFile, newContent).then(function () {
|
||||
ui.addNotification(null, E('p', _('Successfully added "%s" to %s!').format(ip, listName)), 'info');
|
||||
});
|
||||
}).catch(function (e) {
|
||||
ui.addNotification(null, E('p', _('Failed to update list: %s').format(e.message)), 'error');
|
||||
});
|
||||
};
|
||||
|
||||
ui.showModal(_('IP Search & Quick Action'), [
|
||||
E('p', _('Search the SecBox-related Sets for a specific IP and take quick actions.')),
|
||||
E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column; margin-bottom:.5em;' }, [
|
||||
E('label', { 'style': 'padding-top:.5em', 'id': 'run' }, [
|
||||
E('input', {
|
||||
'class': 'cbi-input-text',
|
||||
'placeholder': '192.168.0.1 or domain',
|
||||
'style': 'width:300px; padding: 4px 8px;',
|
||||
'spellcheck': 'false',
|
||||
'id': 'search',
|
||||
'keydown': function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
triggerSearch();
|
||||
}
|
||||
}
|
||||
})
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column' }, [
|
||||
E('h5', _('Result')),
|
||||
E('textarea', {
|
||||
'id': 'result',
|
||||
'style': 'width: 100% !important; margin-top:.5em; padding: 6px; font-family: monospace; font-size:12px; border-radius:4px;',
|
||||
'readonly': 'readonly',
|
||||
'wrap': 'off',
|
||||
'rows': 15
|
||||
})
|
||||
]),
|
||||
E('div', { 'style': 'display:flex; justify-content:space-between; align-items:center; margin-top:1em;' }, [
|
||||
E('div', {}, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'style': 'margin-right:.4em;',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return appendToList('/etc/banip/banip.allowlist', _('Allowlist'));
|
||||
})
|
||||
}, [_('+ Add to Allowlist')]),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-negative',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return appendToList('/etc/banip/banip.blocklist', _('Blocklist'));
|
||||
})
|
||||
}, [_('+ Add to Blocklist')])
|
||||
]),
|
||||
E('div', {}, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button',
|
||||
'style': 'margin-right:.4em;',
|
||||
'click': ui.hideModal
|
||||
}, _('Close')),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-positive important',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
triggerSearch();
|
||||
})
|
||||
}, _('Search IP'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
document.getElementById('search').focus();
|
||||
}
|
||||
if (ev === 'content') {
|
||||
let content, selectOption;
|
||||
let errMsg = false;
|
||||
|
||||
if (report[1]) {
|
||||
try {
|
||||
content = JSON.parse(report[1]);
|
||||
} catch (e) {
|
||||
content = "";
|
||||
if (!errMsg) {
|
||||
errMsg = true;
|
||||
return ui.addNotification(null, E('p', _('Unable to parse the ruleset file!')), 'error');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
selectOption = [E('option', { value: '' }, [_('-- Set Selection --')])];
|
||||
Object.keys(content.nftables)
|
||||
.filter(key => content.nftables[key].set?.name && content.nftables[key].set.table === 'SecBox')
|
||||
.sort((a, b) => content.nftables[a].set.name.localeCompare(content.nftables[b].set.name))
|
||||
.forEach(key => {
|
||||
selectOption.push(E('option', { 'value': content.nftables[key].set.name }, content.nftables[key].set.name));
|
||||
});
|
||||
ui.showModal(_('Set Content'), [
|
||||
E('p', _('List the elements of a specific SecBox-related Set.')),
|
||||
E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column' }, [
|
||||
E('label', { 'class': 'cbi-input-select', 'style': 'padding-top:.5em', 'id': 'run' }, [
|
||||
E('h5', _('Set')),
|
||||
E('select', { 'class': 'cbi-input-select', 'id': 'set' },
|
||||
selectOption
|
||||
)
|
||||
]),
|
||||
]),
|
||||
E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column' }, [
|
||||
E('label', { 'class': 'cbi-checkbox', 'style': 'padding-top:.5em' }, [
|
||||
E('input', {
|
||||
'class': 'cbi-checkbox',
|
||||
'data-update': 'click change',
|
||||
'type': 'checkbox',
|
||||
'id': 'chkFilter',
|
||||
'disabled': 'disabled',
|
||||
'value': 'true'
|
||||
}),
|
||||
E('span', { 'style': 'margin-left: .5em;' }, _('Show only Set elements with hits'))
|
||||
]),
|
||||
]),
|
||||
E('div', { 'class': 'left', 'style': 'display:flex; flex-direction:column' }, [
|
||||
E('h5', _('Result')),
|
||||
E('textarea', {
|
||||
'id': 'result',
|
||||
'style': 'width: 100% !important; margin-top:.5em; padding: 5px; font-family: monospace',
|
||||
'readonly': 'readonly',
|
||||
'wrap': 'off',
|
||||
'rows': 20
|
||||
})
|
||||
]),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button',
|
||||
'style': 'float:none; margin-right:.4em;',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, function (ev) {
|
||||
const checkbox = document.getElementById('chkFilter');
|
||||
const isChecked = checkbox.checked ? 'true' : 'false';
|
||||
let set = document.getElementById('set').value;
|
||||
if (set) {
|
||||
document.getElementById('result').textContent = 'Collecting Set content, please wait...';
|
||||
return L.resolveDefault(fs.exec_direct('/etc/init.d/banip', ['content', set, isChecked]), '').then(function (res) {
|
||||
let result = document.getElementById('result');
|
||||
result.textContent = res ? res.trim() : _('Network error');
|
||||
document.getElementById('set').value = '';
|
||||
});
|
||||
}
|
||||
document.getElementById('set').focus();
|
||||
})
|
||||
}, _('Show Content'))
|
||||
])
|
||||
]);
|
||||
if (uci.get('banip', 'global', 'ban_nftcount') === '1') {
|
||||
const chk = document.querySelector('#chkFilter');
|
||||
if (chk) {
|
||||
chk.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
document.getElementById('set').focus();
|
||||
}
|
||||
if (ev === 'map') {
|
||||
const modal = ui.showModal(null, [
|
||||
E('div', {
|
||||
id: 'mapModal',
|
||||
style: 'position: relative;'
|
||||
}, [
|
||||
E('iframe', {
|
||||
id: 'mapFrame',
|
||||
src: L.resource('view/secbox/map.html'),
|
||||
style: 'width: 100%; height: 80vh; border: none;'
|
||||
}),
|
||||
]),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button',
|
||||
'click': ui.createHandlerFn(this, function (ev) {
|
||||
ui.hideModal();
|
||||
sessionStorage.clear();
|
||||
location.reload();
|
||||
})
|
||||
}, _('Cancel')),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, function (ev) {
|
||||
let iframe = document.getElementById('mapFrame');
|
||||
iframe.contentWindow.location.reload();
|
||||
})
|
||||
}, _('Map Reset'))
|
||||
])
|
||||
]);
|
||||
modal.style.maxWidth = '90%';
|
||||
document.getElementById('mapModal').focus();
|
||||
}
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
L.resolveDefault(fs.exec_direct('/etc/init.d/banip', ['report', 'json']), '')
|
||||
.then(function (res) {
|
||||
if (res && res.trim()) return res;
|
||||
return L.resolveDefault(fs.read_direct('/tmp/banIP-report/ban_report.jsn'), '')
|
||||
.then(function (r) {
|
||||
if (r && r.trim()) {
|
||||
return L.resolveDefault(fs.read_direct('/tmp/banIP-report/ban_map.jsn'), '[]')
|
||||
.then(function (m) {
|
||||
let rObj = JSON.parse(r);
|
||||
let mObj = JSON.parse(m);
|
||||
return JSON.stringify([rObj[0] || rObj, mObj]);
|
||||
}).catch(() => r);
|
||||
}
|
||||
return '';
|
||||
});
|
||||
}),
|
||||
L.resolveDefault(fs.exec_direct('/usr/sbin/nft', ['-tj', 'list', 'sets']), ''),
|
||||
uci.load('banip')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function (report) {
|
||||
let content = [], rowSets, tblSets, notMsg;
|
||||
|
||||
if (report) {
|
||||
try {
|
||||
content = JSON.parse(report[0]);
|
||||
} catch (e) {
|
||||
content[0] = "";
|
||||
}
|
||||
} else {
|
||||
content[0] = "";
|
||||
}
|
||||
|
||||
function fmtCount(value) {
|
||||
const text = String(value ?? '').trim();
|
||||
if (!/^\d+$/.test(text)) {
|
||||
return text || '-';
|
||||
}
|
||||
return text.replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
|
||||
}
|
||||
|
||||
function hitStats(sets) {
|
||||
const stats = { 'sets': 0, 'counted': 0, 'total': 0, 'hits': [], 'worst': [] };
|
||||
|
||||
Object.keys(sets || {}).forEach(function (key) {
|
||||
const set = sets[key];
|
||||
stats.sets++;
|
||||
if (`${set.cnt_inbound ?? ''}${set.cnt_outbound ?? ''}` === '') {
|
||||
return;
|
||||
}
|
||||
const count = (parseInt(set.cnt_inbound, 10) || 0) + (parseInt(set.cnt_outbound, 10) || 0);
|
||||
const elements = parseInt(String(set.cnt_elements).replace(/\D/g, ''), 10) || 0;
|
||||
stats.counted++;
|
||||
stats.total += count;
|
||||
if (count > 0) {
|
||||
stats.hits.push({ 'name': key, 'elements': elements, 'hits': count, 'value': count });
|
||||
}
|
||||
stats.worst.push({ 'name': key, 'elements': elements, 'hits': count, 'value': elements });
|
||||
});
|
||||
stats.hits.sort(function (a, b) { return b.value - a.value; });
|
||||
stats.worst.sort(function (a, b) { return (a.hits - b.hits) || (b.value - a.value); });
|
||||
return stats;
|
||||
}
|
||||
|
||||
function dirNode(direction, count, bold) {
|
||||
const attrs = bold ? { 'style': 'font-weight: bold; text-align: left !important; word-break: break-all;' } : { 'style': 'text-align: left !important; word-break: break-all;' };
|
||||
if (!count) {
|
||||
return E('em', attrs, [direction]);
|
||||
}
|
||||
return E('em', attrs, [
|
||||
direction, bold ? ' (' : ': (',
|
||||
fmtCount(count),
|
||||
')'
|
||||
]);
|
||||
}
|
||||
|
||||
rowSets = [];
|
||||
tblSets = E('table', { 'class': 'table cbi-section-table', 'id': 'sets', 'style': 'table-layout: fixed; width: 100%; word-break: break-all;' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-titles' }, [
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 15%; text-align: left !important;' }, [_('Set')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 10%; text-align: left !important;' }, [_('Count')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 12%; text-align: left !important;' }, [_('Inbound (packets)')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 12%; text-align: left !important;' }, [_('Outbound (packets)')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 13%; text-align: left !important;' }, [_('Port / Protocol')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 38%; text-align: left !important;' }, [_('Elements (max. 50)')])
|
||||
])
|
||||
]);
|
||||
|
||||
if (content[0].sets) {
|
||||
Object.keys(content[0].sets).sort().forEach(function (key, idx) {
|
||||
rowSets.push([
|
||||
E('em', { 'style': 'text-align: left !important; word-break: break-all;' }, key),
|
||||
E('em', { 'style': 'text-align: left !important;' }, fmtCount(content[0].sets[key].cnt_elements)),
|
||||
dirNode(content[0].sets[key].inbound, content[0].sets[key].cnt_inbound, false),
|
||||
dirNode(content[0].sets[key].outbound, content[0].sets[key].cnt_outbound, false),
|
||||
E('em', { 'style': 'text-align: left !important;' }, content[0].sets[key].port),
|
||||
E('em', { 'style': 'text-align: left !important; word-break: break-all; white-space: normal; display: block; overflow-wrap: anywhere;' }, content[0].sets[key].set_elements.join(", "))
|
||||
]);
|
||||
});
|
||||
rowSets.push([
|
||||
E('em', { 'style': 'font-weight: bold; text-align: left !important; word-break: break-all;' }, content[0].sum_sets),
|
||||
E('em', { 'style': 'font-weight: bold; text-align: left !important;' }, fmtCount(content[0].sum_cntelements)),
|
||||
dirNode(content[0].sum_setinbound, content[0].sum_cntinbound, true),
|
||||
dirNode(content[0].sum_setoutbound, content[0].sum_cntoutbound, true),
|
||||
E('em', { 'style': 'font-weight: bold; text-align: left !important;' }, content[0].sum_setports),
|
||||
E('em', { 'style': 'font-weight: bold; text-align: left !important; word-break: break-all;' }, content[0].sum_setelements)
|
||||
]);
|
||||
}
|
||||
cbi_update_table(tblSets, rowSets);
|
||||
|
||||
const sum = content?.[0] || {};
|
||||
const hits = hitStats(sum.sets);
|
||||
|
||||
function renderStatRow(label, val, idx) {
|
||||
return E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((idx % 2) ? 2 : 1) }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 55% !important; max-width: 55% !important; min-width: 55% !important; text-align: left !important; font-weight: 600;' }, [label]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 45% !important; text-align: left !important; font-weight: bold;' }, [fmtCount(val)])
|
||||
]);
|
||||
}
|
||||
|
||||
function renderTopCard(title, list) {
|
||||
const rows = list.slice(0, 10).map(function (item, index) {
|
||||
return E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-' + ((index % 2) ? 2 : 1) }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 50% !important; text-align: left !important; font-family: monospace; word-break: break-all;' }, [item.name]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; text-align: left !important;' }, [fmtCount(item.elements)]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; text-align: left !important; font-weight: bold;' }, [fmtCount(item.hits)])
|
||||
]);
|
||||
});
|
||||
|
||||
return E('div', { 'class': 'op-card' }, [
|
||||
E('div', { 'class': 'op-card-header' }, [title]),
|
||||
E('div', { 'class': 'op-card-body', 'style': 'padding: 0;' }, [
|
||||
E('table', { 'class': 'table cbi-section-table', 'style': 'table-layout: fixed; width: 100%;' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-titles' }, [
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 50% !important; text-align: left !important;' }, [_('Set')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 25% !important; text-align: left !important;' }, [_('Elements')]),
|
||||
E('th', { 'class': 'th cbi-section-table-cell', 'style': 'width: 25% !important; text-align: left !important;' }, [_('Hits')])
|
||||
]),
|
||||
E('tbody', {}, rows.length ? rows : [
|
||||
E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [
|
||||
E('td', { 'colspan': 3, 'class': 'td cbi-section-table-cell', 'style': 'text-align: left !important; color: #888;' }, ['-'])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
const style = E('style', { 'type': 'text/css' }, [
|
||||
'#ban-report-grid .op-grid-2 { display: grid; gap: 1em; grid-template-columns: repeat(2, 1fr); margin-bottom: 1em; align-items: stretch; }' +
|
||||
'@media (max-width: 900px) { #ban-report-grid .op-grid-2 { grid-template-columns: 1fr; } }' +
|
||||
'#ban-report-grid .op-grid-1 { margin-bottom: 1em; width: 100%; }' +
|
||||
'#ban-report-grid .op-card { background: var(--cbi-section-bg, #fff); border: 1px solid var(--cbi-border-color, #e5e7eb); border-radius: 6px; overflow: hidden; display: flex; flex-direction: column; height: 100%; box-sizing: border-box; }' +
|
||||
'#ban-report-grid .op-card-header { background: rgba(0, 0, 0, 0.025); min-height: 2.85em; height: 2.85em; padding: 0 1em; box-sizing: border-box; border-bottom: 1px solid var(--cbi-border-color, #e5e7eb); font-weight: 700; font-size: 1.02em; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }' +
|
||||
'#ban-report-grid .op-card-body { padding: .85em 1em; flex: 1; display: flex; flex-direction: column; justify-content: center; }' +
|
||||
'#ban-report-grid .op-card-body-table { padding: 0 !important; justify-content: flex-start; }' +
|
||||
'#ban-report-grid table.table { width: 100%; border-collapse: separate; border-spacing: 0; table-layout: fixed; margin: 0; }' +
|
||||
'#ban-report-grid table.table td { padding: .5em .85em; text-align: left !important; font-size: .95em; vertical-align: middle; }' +
|
||||
'@media (prefers-color-scheme: dark) {' +
|
||||
'#ban-report-grid .op-card { background: rgba(255, 255, 255, 0.03); border-color: rgba(255, 255, 255, 0.12); }' +
|
||||
'#ban-report-grid .op-card-header { background: rgba(255, 255, 255, 0.05); border-color: rgba(255, 255, 255, 0.12); }' +
|
||||
'}'
|
||||
]);
|
||||
|
||||
const page = E('div', { 'class': 'cbi-map', 'id': 'ban-report-grid' }, [
|
||||
style,
|
||||
E('h2', { 'name': 'content' }, [_('SecBox Set Reporting')]),
|
||||
E('div', { 'class': 'cbi-map-descr' }, [
|
||||
_('This report shows the latest NFT Set statistics, press the \'Refresh\' button to get a new one. \
|
||||
You can also display the specific content of Sets, search for suspicious IPs and finally, these IPs can also be displayed on a map.')
|
||||
]),
|
||||
|
||||
/* Row 1: Strict 2 Columns (Summary Overview & Auto-added) */
|
||||
E('div', { 'class': 'op-grid-2' }, [
|
||||
/* Overview */
|
||||
E('div', { 'class': 'op-card' }, [
|
||||
E('div', { 'class': 'op-card-header' }, [_('Set Overview')]),
|
||||
E('div', { 'class': 'op-card-body op-card-body-table' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
renderStatRow(_('Sets'), sum.sum_sets, 0),
|
||||
renderStatRow(_('Elements'), sum.sum_cntelements, 1),
|
||||
renderStatRow(_('Timestamp'), sum.timestamp || '-', 2)
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
/* Auto-added IPs */
|
||||
E('div', { 'class': 'op-card' }, [
|
||||
E('div', { 'class': 'op-card-header' }, [_('Auto-added IPs')]),
|
||||
E('div', { 'class': 'op-card-body op-card-body-table' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
renderStatRow(_('Auto-added to Allowlist'), sum.autoadd_allow, 0),
|
||||
renderStatRow(_('Auto-added to Blocklist'), sum.autoadd_block, 1)
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
/* Row 2: Strict Full-Width (Blocked Packets 6 Metrics in 1 Full Card) */
|
||||
E('div', { 'class': 'op-grid-1' }, [
|
||||
E('div', { 'class': 'op-card' }, [
|
||||
E('div', { 'class': 'op-card-header' }, [_('Blocked Packets')]),
|
||||
E('div', { 'class': 'op-card-body op-card-body-table' }, [
|
||||
E('table', { 'class': 'table cbi-section-table', 'style': 'table-layout: fixed; width: 100%;' }, [
|
||||
E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: 600;' }, [_('syn-flood')]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: bold;' }, [fmtCount(sum.sum_synflood)]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: 600;' }, [_('invalid ct')]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: bold;' }, [fmtCount(sum.sum_ctinvalid)])
|
||||
]),
|
||||
E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-2' }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: 600;' }, [_('udp-flood')]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: bold;' }, [fmtCount(sum.sum_udpflood)]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: 600;' }, [_('invalid tcp')]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: bold;' }, [fmtCount(sum.sum_tcpinvalid)])
|
||||
]),
|
||||
E('tr', { 'class': 'tr cbi-section-table-row cbi-rowstyle-1' }, [
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: 600;' }, [_('icmp-flood')]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: bold;' }, [fmtCount(sum.sum_icmpflood)]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: 600;' }, [_('bcp38')]),
|
||||
E('td', { 'class': 'td cbi-section-table-cell', 'style': 'width: 25% !important; font-weight: bold;' }, [fmtCount(sum.sum_bcp38)])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
/* Row 3: Single Row Full-Width (Top Sets) */
|
||||
E('div', { 'class': 'op-grid-1' }, [
|
||||
renderTopCard(_('Top Sets (Most Hits)'), hits.hits)
|
||||
]),
|
||||
|
||||
/* Row 4: Single Row Full-Width (Worst Sets) */
|
||||
E('div', { 'class': 'op-grid-1' }, [
|
||||
renderTopCard(_('Worst Sets (Least Hits)'), hits.worst)
|
||||
]),
|
||||
|
||||
/* Bottom Full-Width Table */
|
||||
E('div', { 'class': 'cbi-section', 'style': 'overflow-x: auto;' }, [
|
||||
E('h3', _('Set details')),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
tblSets
|
||||
])
|
||||
]),
|
||||
|
||||
/* Actions */
|
||||
E('div', { 'class': 'cbi-page-actions' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply',
|
||||
'click': function () {
|
||||
const btn = this;
|
||||
document.querySelectorAll('.cbi-page-actions button').forEach(function (b) {
|
||||
b.disabled = true;
|
||||
});
|
||||
btn.blur();
|
||||
btn.classList.add('spinning');
|
||||
L.resolveDefault(fs.write('/var/run/banIP/banIP.report', ''), '').then(function () {
|
||||
L.resolveDefault(fs.exec_direct('/etc/init.d/banip', ['report', 'gen']), '');
|
||||
let attempts = 0;
|
||||
let poller = setInterval(function () {
|
||||
L.resolveDefault(fs.read('/var/run/banIP/banIP.report'), '').then(function (res) {
|
||||
res = (res || '').trim();
|
||||
if (res === '1') {
|
||||
clearInterval(poller);
|
||||
location.reload();
|
||||
} else if (res === '0') {
|
||||
// polling
|
||||
} else {
|
||||
attempts++;
|
||||
if (attempts >= 10) {
|
||||
clearInterval(poller);
|
||||
btn.classList.remove('spinning');
|
||||
document.querySelectorAll('.cbi-page-actions button').forEach(function (b) {
|
||||
b.disabled = false;
|
||||
});
|
||||
ui.addNotification(null, E('p', _('Failed to generate a SecBox report!')), 'error');
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 3000);
|
||||
});
|
||||
}
|
||||
}, [_('Refresh')]),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleAction(report, 'search');
|
||||
})
|
||||
}, [_('IP Search...')]),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleAction(report, 'content');
|
||||
})
|
||||
}, [_('Set Content...')]),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'id': 'btnMap',
|
||||
'disabled': 'disabled',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
if (Array.isArray(content[1]) && content[1].length > 1) {
|
||||
sessionStorage.setItem('mapData', JSON.stringify(content[1]));
|
||||
return handleAction(report, 'map');
|
||||
} else {
|
||||
if (!notMsg) {
|
||||
notMsg = true;
|
||||
return ui.addNotification(null, E('p', _('No GeoIP Map data!')), 'info');
|
||||
}
|
||||
}
|
||||
})
|
||||
}, [_('Map...')])
|
||||
])
|
||||
]);
|
||||
|
||||
if (uci.get('banip', 'global', 'ban_nftcount') === '1'
|
||||
&& uci.get('banip', 'global', 'ban_map') === '1'
|
||||
&& (uci.get('banip', 'global', 'ban_allowlistonly') !== '1'
|
||||
|| (uci.get('banip', 'global', 'ban_feedin') || "").includes("allowlist")
|
||||
|| (uci.get('banip', 'global', 'ban_feedout') || "").includes("allowlist"))) {
|
||||
const btn = page.querySelector('#btnMap');
|
||||
if (btn) {
|
||||
btn.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
return page;
|
||||
},
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -0,0 +1,750 @@
|
||||
'use strict';
|
||||
'require dom';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require uci';
|
||||
'require form';
|
||||
'require tools.widgets as widgets';
|
||||
|
||||
/*
|
||||
button handling
|
||||
*/
|
||||
function handleAction(ev) {
|
||||
if (ev === 'restart' || ev === 'reload') {
|
||||
let map = document.querySelector('.cbi-map');
|
||||
return dom.callClassMethod(map, 'save')
|
||||
.then(L.bind(ui.changes.apply, ui.changes))
|
||||
.then(function () {
|
||||
ui.showIndicator('banip-action', _('Applying configuration & reloading SecBox...'));
|
||||
return fs.exec_direct('/etc/init.d/banip', [ev])
|
||||
.then(function () {
|
||||
ui.hideIndicator('banip-action');
|
||||
ui.addNotification(null, E('p', _('SecBox configuration saved and service %sed successfully.').format(ev)), 'info');
|
||||
})
|
||||
.catch(function (e) {
|
||||
ui.hideIndicator('banip-action');
|
||||
ui.addNotification(null, E('p', _('Failed to %s SecBox: %s').format(ev, e.message)), 'error');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return fs.exec_direct('/etc/init.d/banip', [ev]);
|
||||
}
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
L.resolveDefault(fs.read_direct('/etc/banip/banip.custom.feeds'), ''),
|
||||
L.resolveDefault(fs.read_direct('/etc/banip/banip.feeds'), ''),
|
||||
L.resolveDefault(fs.read_direct('/etc/banip/banip.countries'), ''),
|
||||
uci.load('banip').catch(() => 0)
|
||||
]);
|
||||
},
|
||||
|
||||
render: function (result) {
|
||||
/*
|
||||
config check
|
||||
*/
|
||||
if (!result[3] || result[3].length === 0) {
|
||||
ui.addNotification(null, E('p', _('No SecBox config found!')), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
let m, s, o;
|
||||
const docLink = '<a style="color:#37c;font-weight:bold;" href="https://github.com/openwrt/packages/blob/master/net/banip/files/README.md" target="_blank" rel="noreferrer noopener">' + _('online documentation') + '</a>';
|
||||
const mapDescr = _('Configuration of the SecBox package to ban incoming and outgoing IPs via named nftables Sets. For further information please check the %s.').format(docLink);
|
||||
m = new form.Map('banip', _('SecBox Settings'), mapDescr);
|
||||
|
||||
/*
|
||||
tabbed config section
|
||||
*/
|
||||
s = m.section(form.NamedSection, 'global', 'banip', _('Settings'));
|
||||
s.addremove = false;
|
||||
s.tab('general', _('General Settings'));
|
||||
s.tab('advanced', _('Advanced Settings'));
|
||||
s.tab('adv_chain', _('Table/Chain Settings'));
|
||||
s.tab('adv_set', _('Feed/Set Settings'));
|
||||
s.tab('adv_log', _('Log Settings'));
|
||||
s.tab('adv_email', _('E-Mail Settings'));
|
||||
s.tab('feeds', _('Feed Selection'));
|
||||
|
||||
/*
|
||||
general settings tab
|
||||
*/
|
||||
o = s.taboption('general', form.DummyValue, '_sub');
|
||||
o.rawhtml = true;
|
||||
o.default = '<em style="color:#37c;font-weight:bold;">' + _('Changes on this tab need a SecBox service restart to take effect.') + '</em>'
|
||||
+ '<hr style="width: 200px; height: 1px;" />';
|
||||
|
||||
o = s.taboption('general', form.Flag, 'ban_debug', _('Verbose Debug Logging'), _('Enable verbose debug logging in case of processing errors.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('general', form.Flag, 'ban_autodetect', _('Auto Detection'), _('Detect relevant network devices, interfaces, subnets, protocols and utilities automatically.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('general', form.Flag, 'ban_protov4', _('IPv4 Support'), _('Enables IPv4 support.'));
|
||||
o.depends('ban_autodetect', '0');
|
||||
o.optional = true;
|
||||
o.retain = true;
|
||||
|
||||
o = s.taboption('general', form.Flag, 'ban_protov6', _('IPv6 Support'), _('Enables IPv6 support.'));
|
||||
o.depends('ban_autodetect', '0');
|
||||
o.optional = true;
|
||||
o.retain = true;
|
||||
|
||||
o = s.taboption('general', widgets.DeviceSelect, 'ban_dev', _('Network Devices'), _('Select the WAN network device(s).'));
|
||||
o.depends('ban_autodetect', '0');
|
||||
o.multiple = true;
|
||||
o.nocreate = true;
|
||||
o.optional = true;
|
||||
o.retain = true;
|
||||
|
||||
o = s.taboption('general', widgets.NetworkSelect, 'ban_ifv4', _('IPv4 Network Interfaces'), _('Select the logical WAN IPv4 network interface(s).'));
|
||||
o.depends('ban_autodetect', '0');
|
||||
o.multiple = true;
|
||||
o.nocreate = true;
|
||||
o.optional = true;
|
||||
o.retain = true;
|
||||
|
||||
o = s.taboption('general', widgets.NetworkSelect, 'ban_ifv6', _('IPv6 Network Interfaces'), _('Select the logical WAN IPv6 network interface(s).'));
|
||||
o.depends('ban_autodetect', '0');
|
||||
o.multiple = true;
|
||||
o.nocreate = true;
|
||||
o.optional = true;
|
||||
o.retain = true;
|
||||
|
||||
o = s.taboption('general', form.ListValue, 'ban_fetchcmd', _('Download Utility'), _('Select one of the pre-configured download utilities.'));
|
||||
o.depends('ban_autodetect', '0');
|
||||
o.value('uclient-fetch');
|
||||
o.value('wget');
|
||||
o.value('curl');
|
||||
o.optional = true;
|
||||
o.retain = true;
|
||||
|
||||
o = s.taboption('general', form.Value, 'ban_fetchparm', _('Download Parameters'), _('Override the pre-configured download options for the selected download utility. The output flag, e.g. \'-o\' for curl or \'-O\' for wget, must be the last parameter.'));
|
||||
o.depends('ban_autodetect', '0');
|
||||
o.optional = true;
|
||||
o.retain = true;
|
||||
|
||||
o = s.taboption('general', widgets.NetworkSelect, 'ban_trigger', _('Startup Trigger Interface'), _('List of available network interfaces to trigger the SecBox start.'));
|
||||
o.multiple = true;
|
||||
o.nocreate = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('general', form.Value, 'ban_triggerdelay', _('Trigger Delay'), _('Additional trigger delay in seconds before SecBox processing begins.'));
|
||||
o.placeholder = '10';
|
||||
o.datatype = 'range(1,300)';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('general', form.ListValue, 'ban_fetchretry', _('Download Retries'), _('Number of download attempts in case of an error (not supported by uclient-fetch).'));
|
||||
o.value('1');
|
||||
o.value('3');
|
||||
o.value('5');
|
||||
o.value('10');
|
||||
o.value('20');
|
||||
o.default = '5';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('general', form.Flag, 'ban_fetchinsecure', _('Download Insecure'), _('Don\'t check SSL server certificates during download.'));
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('general', form.Flag, 'ban_nftcount', _('Reporting Counters'), _('Enable NFT counters for Set elements and chain rules. Required for the GeoIP Map and packet statistics in the Set Reporting.'));
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('general', form.Flag, 'ban_map', _('Enable GeoIP Map'), _('Enable a GeoIP Map with suspicious Set elements. This requires external requests to get the map tiles and geolocation data.'));
|
||||
o.depends('ban_nftcount', '1');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
/*
|
||||
additional settings tab
|
||||
*/
|
||||
o = s.taboption('advanced', form.DummyValue, '_sub');
|
||||
o.rawhtml = true;
|
||||
o.default = '<em style="color:#37c;font-weight:bold;">' + _('Changes on this tab needs a SecBox service restart to take effect.') + '</em>'
|
||||
+ '<hr style="width: 200px; height: 1px;" />';
|
||||
|
||||
o = s.taboption('advanced', form.ListValue, 'ban_nicelimit', _('Nice Level'), _('The selected priority will be used for SecBox background processing.'));
|
||||
o.value('-20', _('Highest Priority'));
|
||||
o.value('-10', _('High Priority'));
|
||||
o.value('0', _('Normal Priority'));
|
||||
o.value('10', _('Less Priority'));
|
||||
o.value('19', _('Least Priority'));
|
||||
o.default = '0';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('advanced', form.ListValue, 'ban_filelimit', _('Max Open Files'), _('Increase the maximal number of open files, e.g. to handle the amount of temporary split files while loading the Sets.'));
|
||||
o.value('512');
|
||||
o.value('1024');
|
||||
o.value('2048');
|
||||
o.value('4096');
|
||||
o.default = '1024';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('advanced', form.ListValue, 'ban_cores', _('CPU Cores'), _('Limit the cpu cores used by SecBox to save RAM, autodetected by default.'));
|
||||
o.value('1');
|
||||
o.value('2');
|
||||
o.value('4');
|
||||
o.value('8');
|
||||
o.value('16');
|
||||
o.placeholder = _('-- default --');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('advanced', form.ListValue, 'ban_splitsize', _('Set Split Size'), _('Split external Set loading after every n members to save RAM, disabled by default.'));
|
||||
o.value('512');
|
||||
o.value('1024');
|
||||
o.value('2048');
|
||||
o.value('4096');
|
||||
o.value('8192');
|
||||
o.value('16384');
|
||||
o.placeholder = _('-- default --');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'ban_basedir', _('Base Directory'), _('Base working directory while SecBox processing.'));
|
||||
o.placeholder = '/tmp';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'ban_backupdir', _('Backup Directory'), _('Target directory for compressed feed backups.'));
|
||||
o.placeholder = '/tmp/SecBox-backup';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'ban_reportdir', _('Report Directory'), _('Target directory for SecBox-related report files.'));
|
||||
o.placeholder = '/tmp/SecBox-report';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'ban_errordir', _('Error Directory'), _('Target directory for SecBox-related error files.'));
|
||||
o.placeholder = '/tmp/SecBox-error';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('advanced', form.Flag, 'ban_deduplicate', _('Deduplicate IPs'), _('Deduplicate IP addresses across all active Sets and tidy up the local blocklist.'));
|
||||
o.default = 1;
|
||||
o.rmempty = false;
|
||||
|
||||
/*
|
||||
advanced chain settings tab
|
||||
*/
|
||||
o = s.taboption('adv_chain', form.DummyValue, '_sub');
|
||||
o.rawhtml = true;
|
||||
o.default = '<em style="color:#37c;font-weight:bold;">' + _('Changes on this tab needs a SecBox service restart to take effect.') + '</em>'
|
||||
+ '<hr style="width: 200px; height: 1px;" />';
|
||||
|
||||
o = s.taboption('adv_chain', form.ListValue, 'ban_nftpriority', _('Chain Priority'), _('Set the NFT chain priority within the SecBox table, lower values means higher priority.'));
|
||||
o.value('10');
|
||||
o.value('0');
|
||||
o.value('-100');
|
||||
o.value('-150');
|
||||
o.default = '-100';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_chain', form.Value, 'ban_allowflag', _('Allow Protocol/Ports'), _('Always allow a protocol (tcp/udp) with certain ports or port ranges in WAN-Input and WAN-Forward chain.'));
|
||||
o.placeholder = 'tcp 80 443-445';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_chain', widgets.DeviceSelect, 'ban_vlanallow', _('Allow VLAN Forwards'), _('Always allow certain VLAN forwards.'));
|
||||
o.multiple = true;
|
||||
o.nocreate = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_chain', widgets.DeviceSelect, 'ban_vlanblock', _('Block VLAN Forwards'), _('Always block certain VLAN forwards.'));
|
||||
o.multiple = true;
|
||||
o.nocreate = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_chain', form.Flag, 'ban_bcp38', _('Enable BCP38'), _('Block packets with spoofed source IP addresses in all supported chains.'));
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_chain', form.ListValue, 'ban_icmplimit', _('ICMP-Threshold'), _('ICMP-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to \'0\'.'));
|
||||
o.value('0');
|
||||
o.value('25');
|
||||
o.value('50');
|
||||
o.value('100');
|
||||
o.value('250');
|
||||
o.value('500');
|
||||
o.value('1000');
|
||||
o.default = '25';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_chain', form.ListValue, 'ban_synlimit', _('SYN-Threshold'), _('SYN-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to \'0\'.'));
|
||||
o.value('0');
|
||||
o.value('10');
|
||||
o.value('50');
|
||||
o.value('100');
|
||||
o.value('250');
|
||||
o.value('500');
|
||||
o.value('1000');
|
||||
o.default = '10';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_chain', form.ListValue, 'ban_udplimit', _('UDP-Threshold'), _('UDP-Threshold in packets per second to prevent WAN-DoS attacks. To disable this safeguard set it to \'0\'.'));
|
||||
o.value('0');
|
||||
o.value('100');
|
||||
o.value('250');
|
||||
o.value('500');
|
||||
o.value('1000');
|
||||
o.value('2500');
|
||||
o.value('5000');
|
||||
o.default = '100';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
/*
|
||||
advanced Set settings tab
|
||||
*/
|
||||
o = s.taboption('adv_set', form.DummyValue, '_sub');
|
||||
o.rawhtml = true;
|
||||
o.default = '<em style="color:#37c;font-weight:bold;">' + _('Changes on this tab needs a SecBox service restart to take effect.') + '</em>'
|
||||
+ '<hr style="width: 200px; height: 1px;" />';
|
||||
|
||||
o = s.taboption('adv_set', form.ListValue, 'ban_nftpolicy', _('Set Policy'), _('Set the NFT policy for SecBox-related Sets.'));
|
||||
o.value('memory', _('memory'));
|
||||
o.value('performance', _('performance'));
|
||||
o.default = 'memory';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_set', form.ListValue, 'ban_nftretry', _('Set Load Retries'), _('Number of Set load attempts in case of an error.'));
|
||||
o.value('1');
|
||||
o.value('3');
|
||||
o.value('5');
|
||||
o.default = '3';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_set', form.ListValue, 'ban_blockpolicy', _('Inbound Block Policy'), _('Drop packets silently or actively reject Inbound traffic.'));
|
||||
o.value('drop', _('drop'));
|
||||
o.value('reject', _('reject'));
|
||||
o.default = 'drop';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
/*
|
||||
feed parsing
|
||||
*/
|
||||
let feed, chain, descr;
|
||||
let feeds = null;
|
||||
if (result[0] && result[0].trim() !== "") {
|
||||
try {
|
||||
feeds = JSON.parse(result[0]);
|
||||
} catch (e) {
|
||||
ui.addNotification(null, E('p', _('Unable to parse the custom feed file!')), 'error');
|
||||
}
|
||||
}
|
||||
if (!feeds && result[1] && result[1].trim() !== "") {
|
||||
try {
|
||||
feeds = JSON.parse(result[1]);
|
||||
} catch (e) {
|
||||
ui.addNotification(null, E('p', _('Unable to parse the default feed file!')), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
if (feeds && Object.keys(feeds).length) {
|
||||
const feedKeys = Object.keys(feeds);
|
||||
|
||||
o = s.taboption('adv_set', form.MultiValue, 'ban_feedin', _('Inbound Feed'), _('Override the default feed configuration and apply the feed to the inbound chain only.'));
|
||||
o.value('allowlist', _('local allowlist'));
|
||||
o.value('blocklist', _('local blocklist'));
|
||||
feedKeys.forEach(f => o.value(f.trim()));
|
||||
o.placeholder = _('-- default --');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_set', form.MultiValue, 'ban_feedout', _('Outbound Feed'), _('Override the default feed configuration and apply the feed to the outbound chain only.'));
|
||||
o.value('allowlist', _('local allowlist'));
|
||||
o.value('blocklist', _('local blocklist'));
|
||||
feedKeys.forEach(f => o.value(f.trim()));
|
||||
o.placeholder = _('-- default --');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_set', form.MultiValue, 'ban_feedinout', _('Inbound & Outbound Feed'), _('Override the default feed configuration and apply the feed to the inbound and outbound chain.'));
|
||||
o.value('allowlist', _('local allowlist'));
|
||||
o.value('blocklist', _('local blocklist'));
|
||||
feedKeys.forEach(f => o.value(f.trim()));
|
||||
o.placeholder = _('-- default --');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_set', form.MultiValue, 'ban_feedreset', _('Feed Flag Reset'), _('Override the default feed configuration and remove existing port/protocol limitations.'));
|
||||
o.value('allowlist', _('local allowlist'));
|
||||
o.value('blocklist', _('local blocklist'));
|
||||
feedKeys.forEach(f => o.value(f.trim()));
|
||||
o.placeholder = _('-- default --');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_set', form.MultiValue, 'ban_feedcomplete', _('Feed Complete'), _('Opt out specific feeds from the deduplication process.'));
|
||||
o.value('allowlist', _('local allowlist'));
|
||||
o.value('blocklist', _('local blocklist'));
|
||||
feedKeys.forEach(f => o.value(f.trim()));
|
||||
o.placeholder = _('-- default --');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
}
|
||||
|
||||
/*
|
||||
advanced log settings tab
|
||||
*/
|
||||
o = s.taboption('adv_log', form.DummyValue, '_sub');
|
||||
o.rawhtml = true;
|
||||
o.default = '<em style="color:#37c;font-weight:bold;">' + _('Changes on this tab needs a SecBox service restart to take effect.') + '</em>'
|
||||
+ '<hr style="width: 200px; height: 1px;" />';
|
||||
|
||||
o = s.taboption('adv_log', form.ListValue, 'ban_nftloglevel', _('NFT Log Level'), _('Set the syslog level for NFT logging.'));
|
||||
o.value('emerg', _('emerg'));
|
||||
o.value('alert', _('alert'));
|
||||
o.value('crit', _('crit'));
|
||||
o.value('err', _('err'));
|
||||
o.value('warn', _('warn'));
|
||||
o.value('notice', _('notice'));
|
||||
o.value('info', _('info'));
|
||||
o.value('debug', _('debug'));
|
||||
o.default = 'warn';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_log', form.Flag, 'ban_logprerouting', _('Log Prerouting'), _('Log suspicious packets in the Prerouting chain.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('adv_log', form.Flag, 'ban_loginbound', _('Log Inbound'), _('Log suspicious packets in the WAN-Input and WAN-Forward chain.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('adv_log', form.Flag, 'ban_logoutbound', _('Log Outbound'), _('Log suspicious packets in the LAN-Forward chain.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('adv_log', form.Value, 'ban_logreadfile', _('Logfile Location'), _('Location for parsing the log file, e.g. via syslog-ng, to deactivate the standard parsing via logread.'));
|
||||
o.placeholder = '/var/log/messages';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_log', form.ListValue, 'ban_loglimit', _('Log Limit'), _('Parse only the last stated number of log entries for suspicious events. To disable the log monitor at all set it to \'0\'.'));
|
||||
o.value('0');
|
||||
o.value('50');
|
||||
o.value('100');
|
||||
o.value('250');
|
||||
o.value('500');
|
||||
o.value('1000');
|
||||
o.default = '100';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_log', form.Value, 'ban_logcount', _('Log Count'), _('Number of failed login attempts of the same IP in the log before blocking.'));
|
||||
o.placeholder = '1';
|
||||
o.datatype = 'range(1,10)';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_log', form.ListValue, 'ban_logratelimit', _('Log Rate Limit'), _('Rate (per second) for the shared NFT log limit, applied globally across all logged rules. Set to \'0\' to disable rate limiting entirely, e.g. when using ulogd or other userspace log handlers.'));
|
||||
o.value('0');
|
||||
o.value('1');
|
||||
o.value('5');
|
||||
o.value('10');
|
||||
o.value('25');
|
||||
o.value('50');
|
||||
o.value('100');
|
||||
o.default = '10';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_log', form.ListValue, 'ban_logburstlimit', _('Log Burst Limit'), _('Burst size in packets for the shared NFT log limit.'));
|
||||
o.depends({ ban_logratelimit: '0', '!reverse': true });
|
||||
o.value('1');
|
||||
o.value('5');
|
||||
o.value('10');
|
||||
o.value('25');
|
||||
o.value('50');
|
||||
o.default = '5';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_log', form.DynamicList, 'ban_logterm', _('Log Terms'),
|
||||
_('Regular expressions to detect suspicious IPs in the system log. Uses the last IP per line; prefix a term with \'first:\' for source-first formats, e.g. nginx.'));
|
||||
o.value('Exit before auth from', _('dropbear failed login'));
|
||||
o.value('luci: failed login', _('LuCI failed login'));
|
||||
o.value('error: maximum authentication attempts exceeded', _('sshd failed login'));
|
||||
o.value('sshd.*Connection closed by.*\\[preauth\\]', _('sshd closed connection'));
|
||||
o.value('SecurityEvent=\\"InvalidAccountID\\".*RemoteAddress=', _('asterisk invalid account'));
|
||||
o.value('TLS Error: could not determine wrapping from \\[AF_INET\\]', _('openvpn TLS error'));
|
||||
o.value('AdGuardHome.*\\[error\\].*/control/login: from ip', _('AdGuardHome login error'));
|
||||
o.value('received a suspicious remote IP', _('Remote logging Event'));
|
||||
o.placeholder = _('-- Please choose (optional) --');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_log', form.Flag, 'ban_remotelog', _('Enable Remote Logging'), _('Enable the cgi interface to receive remote logging events.'));
|
||||
o.default = 0;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_log', form.Value, 'ban_remotetoken', _('Remote Token'), _('Token to communicate with the cgi interface.'));
|
||||
o.depends('ban_remotelog', '1');
|
||||
o.datatype = 'and(minlength(3),maxlength(20))';
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return _('Empty field not allowed');
|
||||
}
|
||||
if (!value.match(/^[A-Za-z0-9.:]+$/)) {
|
||||
return _('Invalid characters');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
/*
|
||||
advanced email settings tab
|
||||
*/
|
||||
o = s.taboption('adv_email', form.DummyValue, '_sub');
|
||||
o.rawhtml = true;
|
||||
o.default = '<em style="color:#37c;font-weight:bold;">' + _('To enable email notifications, set up the \'msmtp\' package and specify a valid E-Mail receiver address.') + '</em>'
|
||||
+ '<hr style="width: 200px; height: 1px;" />';
|
||||
|
||||
o = s.taboption('adv_email', form.Flag, 'ban_mailnotification', _('E-Mail Notification'), _('Receive E-Mail notifications with every SecBox run.'));
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_email', form.Value, 'ban_mailreceiver', _('E-Mail Receiver Address'), _('Receiver address for SecBox notification E-Mails, this information is required to enable E-Mail functionality.'));
|
||||
o.placeholder = 'name@example.com';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_email', form.Value, 'ban_mailsender', _('E-Mail Sender Address'), _('Sender address for SecBox notification E-Mails.'));
|
||||
o.placeholder = 'no-reply@SecBox';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_email', form.Value, 'ban_mailtopic', _('E-Mail Topic'), _('Topic for SecBox notification E-Mails.'));
|
||||
o.placeholder = 'SecBox notification';
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('adv_email', form.Value, 'ban_mailprofile', _('E-Mail Profile'), _('Profile used by \'msmtp\' for SecBox notification E-Mails.'));
|
||||
o.placeholder = 'ban_notify';
|
||||
o.datatype = 'uciname';
|
||||
o.rmempty = true;
|
||||
|
||||
/*
|
||||
feeds tab
|
||||
*/
|
||||
o = s.taboption('feeds', form.DummyValue, '_sub');
|
||||
o.rawhtml = true;
|
||||
o.default = '<em style="color:#37c;font-weight:bold;">' + _('Changes on this tab needs a SecBox service reload to take effect.') + '</em>'
|
||||
+ '<hr style="width: 200px; height: 1px;" />'
|
||||
+ '<em style="color:#37c;font-weight:bold;">' + _('External Blocklist Feeds') + '</em>';
|
||||
|
||||
if (feeds && Object.keys(feeds).length) {
|
||||
o = s.taboption('feeds', form.MultiValue, 'ban_feed', _('Blocklist Feed'));
|
||||
for (let i = 0; i < Object.keys(feeds).length; i++) {
|
||||
feed = (Object.keys(feeds)[i] || '').trim();
|
||||
chain = (feeds[feed]?.chain || 'in').trim();
|
||||
descr = (feeds[feed]?.descr || '-').trim();
|
||||
o.value(feed, feed + ' (' + chain + ', ' + descr + ')');
|
||||
}
|
||||
o.placeholder = _('-- Please choose (optional) --');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
}
|
||||
|
||||
o = s.taboption('feeds', form.DummyValue, '_feeds1');
|
||||
o.rawhtml = true;
|
||||
o.default = '<hr style="width: 200px; height: 1px;" /><em style="color:#37c;font-weight:bold;">' + _('Country Selection') + '</em>';
|
||||
|
||||
let err, ccode, rir, country, countries = [];
|
||||
if (result[2] && result[2].trim() !== "") {
|
||||
countries = result[2].trim().split('\n');
|
||||
if (countries && countries.length) {
|
||||
o = s.taboption('feeds', form.MultiValue, 'ban_country', _('Countries') + ' (<abbr title="Regional Internet Registries">RIR</abbr>)');
|
||||
for (let i = 0; i < countries.length; i++) {
|
||||
try {
|
||||
ccode = countries[i].match(/^(\w+)\t/)[1].trim();
|
||||
rir = countries[i].match(/^\w+\t(\w+)\t/)[1].trim();
|
||||
country = countries[i].match(/^\w+\t\w+\t(.*$)/)[1].trim();
|
||||
o.value(ccode, country + ' (' + rir + ')');
|
||||
} catch (e) {
|
||||
countries[i] = "";
|
||||
if (!err) {
|
||||
ui.addNotification(null, E('p', _('Unable to parse the countries file!')), 'error');
|
||||
}
|
||||
err = e;
|
||||
}
|
||||
}
|
||||
o.placeholder = _('-- Please choose (optional) --');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
}
|
||||
}
|
||||
|
||||
o = s.taboption('feeds', form.MultiValue, 'ban_region', _('Regional Internet Registry'), _('Summary of countries based on the Regional Internet Registry (RIR).'));
|
||||
o.value('AFRINIC', _('AFRINIC - serving Africa and the Indian Ocean region'));
|
||||
o.value('APNIC', _('APNIC - serving the Asia Pacific region'));
|
||||
o.value('ARIN', _('ARIN - serving Canada and the United States'));
|
||||
o.value('LACNIC', _('LACNIC - serving the Latin American and Caribbean region'));
|
||||
o.value('RIPE', _('RIPE - serving Europe, Middle East and Central Asia'));
|
||||
o.placeholder = _('-- Please choose (optional) --');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('feeds', form.Flag, 'ban_countrysplit', _('Split Country Set'), _('The selected Countries are stored in separate Sets.'));
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('feeds', form.DummyValue, '_feeds2');
|
||||
o.rawhtml = true;
|
||||
o.default = '<hr style="width: 200px; height: 1px;" /><em style="color:#37c;font-weight:bold;">' + _('ASN Selection') + '</em>';
|
||||
|
||||
o = s.taboption('feeds', form.DynamicList, 'ban_asn', _('ASNs'), _('Collection of IP addresses based on Autonomous System Numbers.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('feeds', form.Flag, 'ban_asnsplit', _('Split ASN Set'), _('The selected ASNs are stored in separate Sets.'));
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('feeds', form.DummyValue, '_feeds3');
|
||||
o.rawhtml = true;
|
||||
o.default = '<hr style="width: 200px; height: 1px;" /><em style="color:#37c;font-weight:bold;">' + _('External Allowlist Feeds') + '</em>';
|
||||
|
||||
if (countries && countries.length) {
|
||||
o = s.taboption('feeds', form.DynamicList, 'ban_allowurl', _('Allowlist Feed URLs'));
|
||||
for (let i = 0; i < countries.length; i++) {
|
||||
try {
|
||||
ccode = countries[i].match(/^(\w+)\t/)[1].trim();
|
||||
rir = countries[i].match(/^\w+\t(\w+)\t/)[1].trim();
|
||||
country = countries[i].match(/^\w+\t\w+\t(.*$)/)[1].trim();
|
||||
o.value('https://www.ipdeny.com/ipblocks/data/aggregated/' + ccode + '-aggregated.zone', country + ' IPv4 (' + rir + ')');
|
||||
o.value('https://www.ipdeny.com/ipv6/ipaddresses/aggregated/' + ccode + '-aggregated.zone', country + ' IPv6 (' + rir + ')');
|
||||
} catch (e) {
|
||||
countries[i] = "";
|
||||
}
|
||||
}
|
||||
o.placeholder = _('-- Please choose (optional) --');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if (!value.match(/^(https?:\/\/)[A-Za-z0-9-]+\.[A-Za-z0-9.-]+(:[0-9]+)?(\/[A-Za-z0-9._\-?&+=:~#%]*)?$/)) {
|
||||
return _('Invalid URL format');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
o = s.taboption('feeds', form.DummyValue, '_feeds4');
|
||||
o.rawhtml = true;
|
||||
o.default = '<hr style="width: 200px; height: 1px;" /><em style="color:#37c;font-weight:bold;">' + _('Local Feed Settings') + '</em>';
|
||||
|
||||
o = s.taboption('feeds', form.Flag, 'ban_autoallowlist', _('Auto Allowlist'), _('Automatically add resolved domains and uplink IPs to the local SecBox allowlist.'));
|
||||
o.default = 1;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('feeds', form.ListValue, 'ban_autoallowuplink', _('Auto Allow Uplink'), _('Limit the uplink autoallow function.'));
|
||||
o.depends('ban_autoallowlist', '1');
|
||||
o.value('disable', _('Disable'));
|
||||
o.value('subnet', _('Subnet'));
|
||||
o.value('ip', _('IP'));
|
||||
o.default = 'subnet';
|
||||
o.placeholder = _('-- default --');
|
||||
o.create = true;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('feeds', form.Flag, 'ban_autoblocklist', _('Auto Blocklist'), _('Automatically add resolved domains and suspicious IPs to the local SecBox blocklist.'));
|
||||
o.default = 1;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('feeds', form.Flag, 'ban_autoblocksubnet', _('Auto Block Subnet'), _('Automatically add entire subnets to the blocklist Set based on an additional RDAP request with the suspicious IP.'));
|
||||
o.default = 0;
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
|
||||
o = s.taboption('feeds', form.Value, 'ban_nftexpiry', _('Blocklist Set Expiry'), _('Expiry time for auto added blocklist Set members.'));
|
||||
o.value('30ms');
|
||||
o.value('10s');
|
||||
o.value('1m');
|
||||
o.value('5m');
|
||||
o.value('1h');
|
||||
o.value('2h');
|
||||
o.value('1d');
|
||||
o.value('7d');
|
||||
o.value('2w');
|
||||
o.placeholder = _('-- default --');
|
||||
o.optional = true;
|
||||
o.rmempty = true;
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value) {
|
||||
return true;
|
||||
}
|
||||
if (!value.match(/^([1-9][0-9]*(ms|s|m|h|d|w))+$/)) {
|
||||
return _('Invalid expiry format, e.g. 5m, 2h, 1d or 1h30m');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
o = s.taboption('feeds', form.Flag, 'ban_allowlistonly', _('Allowlist Only'), _('Restrict the internet access from/to a small number of secure IPs.'));
|
||||
o.rmempty = false;
|
||||
|
||||
s = m.section(form.NamedSection, 'global');
|
||||
s.render = L.bind(function () {
|
||||
return E('div', { 'class': 'cbi-page-actions' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-apply',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleAction('reload');
|
||||
})
|
||||
}, [_('Save & Reload')]),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-save',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleAction('restart');
|
||||
})
|
||||
}, [_('Save & Restart')]),
|
||||
' ',
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-reset',
|
||||
'click': ui.createHandlerFn(this, function () {
|
||||
return handleAction('stop');
|
||||
})
|
||||
}, [_('Stop')])
|
||||
]);
|
||||
});
|
||||
return m.render();
|
||||
},
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null
|
||||
});
|
||||
Reference in new issue
Block a user