mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 23:02:51 +08:00
357 lines
12 KiB
JavaScript
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
|
|
});
|