Files
op-packages/luci-app-secbox/htdocs/luci-static/resources/view/secbox/allowlist.js
T

357 lines
12 KiB
JavaScript

'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
});