v2.0. Multi proxy for user entries. TProxy support.

This commit is contained in:
gSpot
2024-11-03 02:20:45 +03:00
parent 8184a68fe8
commit 06219e9328
42 changed files with 2185 additions and 764 deletions
@@ -10,26 +10,26 @@ return view.extend({
currentCrontabLines: [],
toDD: function(n){
toDD(n){
return String(n).replace(/^(\d)$/, "0$1");
},
cronStatusString: function(s) {
cronStatusString(s) {
return s || _('No Sсhedule');
},
stringifyRuabTasks: function(str_array) {
stringifyRuabTasks(str_array) {
let current_tasks = str_array.filter(s => s.match(this.crontabRegexp));
return current_tasks.join('\n');
},
setCronStatus: function(value) {
setCronStatus(value) {
document.getElementById('cron_status').value = this.cronStatusString(value);
document.getElementById("btn_cron_del").style.visibility = (value) ?
'visible' : 'hidden';
},
writeCronFile: function() {
writeCronFile() {
let btn_cron_add = document.getElementById('btn_cron_add');
let btn_cron_del = document.getElementById('btn_cron_del');
let crontab_string = this.currentCrontabLines.join('\n');
@@ -53,17 +53,17 @@ return view.extend({
});
},
delRuabShedules: function() {
delRuabShedules() {
this.currentCrontabLines = this.currentCrontabLines.filter(
s => s.match(this.crontabRegexp) ? false : true);
},
delCronSchedule: function(ev) {
delCronSchedule(ev) {
this.delRuabShedules();
return this.writeCronFile();
},
setCronSchedule: function(ev) {
setCronSchedule(ev) {
let hour_interval = document.getElementById('cron_hour_interval').value;
let day_interval = document.getElementById('cron_day_interval').value;
let hour = document.getElementById('cron_hour').value;
@@ -88,7 +88,7 @@ return view.extend({
return this.writeCronFile();
},
onchangeHourInterval: function(e) {
onchangeHourInterval(e) {
let value = e.target.value;
let bool = (value != '');
let cron_hour = document.getElementById('cron_hour');
@@ -106,7 +106,7 @@ return view.extend({
};
},
load: function() {
load() {
return fs.lines(tools.crontabFile).catch(e => {
ui.addNotification(null, E('p', _('Unable to read the contents')
+ ': %s [ %s ]'.format(
@@ -115,7 +115,7 @@ return view.extend({
});
},
render: function(content) {
render(content) {
this.currentCrontabLines = content;
let current_task = this.stringifyRuabTasks(content);
@@ -17,7 +17,7 @@ document.head.append(E('style', {'type': 'text/css'},
return view.extend({
pollInterval : L.env.pollinterval,
secToTimeString: function(value) {
secToTimeString(value) {
let string = '';
if(/^\d+$/.test(value)) {
value = Number(value);
@@ -42,50 +42,103 @@ return view.extend({
return string;
},
formatNftJson: function(data) {
let output = { 'rules': [] };
if(data.rules.nftables && data.rules.nftables.length > 1) {
for(let i of data.rules.nftables) {
if(!i.rule) continue;
let set, bytes;
i.rule.expr.forEach(e => {
if(e.match && e.match.left && e.match.left.payload) {
set = e.match.right.replace('@', '');
}
else if(e.counter) {
bytes = e.counter.bytes;
};
});
output.rules.push([ set, bytes ]);
};
formatNftJson(data) {
let output = { 'sink': [] };
if(data.sink.nftables && data.sink.nftables.length > 1) {
let rules = [];
function parseDnsmasqData(set) {
let sArray = [];
if(data[set].nftables && data[set].nftables.length > 1) {
data[set].nftables.forEach(e => {
if(e.set && e.set.elem) {
e.set.elem.forEach(i => {
if(i.elem) {
sArray.push([ i.elem.val, i.elem.expires ]);
};
});
for(let i of data.sink.nftables) {
if(i.rule) {
let instance = (i.rule.comment === ' ') ? '-main-' : i.rule.comment;
let proto, bytes;
i.rule.expr.forEach(e => {
if(e.match && e.match.left && e.match.left.meta && e.match.left.meta.key && e.match.left.meta.key == "l4proto") {
proto = e.match.right;
}
else if(e.counter) {
bytes = e.counter.bytes;
};
});
rules.push([ instance, proto, bytes ]);
} else {
continue;
};
return sArray;
};
if(data.dnsmasq) {
output.dnsmasq = parseDnsmasqData('dnsmasq');
if(rules.length > 0) {
output.sink = rules;
};
if(data.dnsmasq_bypass) {
output.dnsmasq_bypass = parseDnsmasqData('dnsmasq_bypass');
};
if(data.sink_local && data.sink_local.nftables && data.sink_local.nftables.length > 1) {
output.sink_local = [];
let rules = [];
for(let i of data.sink_local.nftables) {
if(i.rule) {
let instance = (i.rule.comment === ' ') ? '-main-' : i.rule.comment;
let proto, bytes;
i.rule.expr.forEach(e => {
if(e.match && e.match.left && e.match.left.meta && e.match.left.meta.key && e.match.left.meta.key == "l4proto") {
proto = e.match.right;
}
else if(e.counter) {
bytes = e.counter.bytes;
};
});
rules.push([ instance, proto, bytes ]);
} else {
continue;
};
};
if(rules.length > 0) {
output.sink_local = rules;
};
};
function parseDnsmasqData(set) {
let sArray = [];
if(set.nftables && set.nftables.length > 1) {
set.nftables.forEach(e => {
if(e.set && e.set.elem) {
e.set.elem.forEach(i => {
if(i.elem) {
sArray.push([ i.elem.val, i.elem.expires ]);
};
});
};
});
};
return sArray;
};
if(data.dnsmasq) {
output.dnsmasq = parseDnsmasqData(data.dnsmasq);
};
if(data.dnsmasq_bypass) {
output.dnsmasq_bypass = parseDnsmasqData(data.dnsmasq_bypass);
};
if(data.dnsmasq_user_instances) {
output.dnsmasq_user_instances = [];
if(data.dnsmasq_user_instances && data.dnsmasq_user_instances.length > 1) {
for(let i of data.dnsmasq_user_instances) {
if(i.nftables) {
let name;
i.nftables.forEach(e => {
if(e.set) {
name = e.set.name;
};
});
output.dnsmasq_user_instances.push([ name, parseDnsmasqData(i) ]);
};
};
};
};
return output;
},
makeDnsmasqTable: function(ipDataArray) {
makeDnsmasqTable(ipDataArray, title) {
let lines = `<tr class="tr"><td class="td center">${_('No entries available...')}</td></tr>`;
let ipTable = E('table', { 'id': 'ipTable', 'class': 'table' });
@@ -122,6 +175,7 @@ return view.extend({
};
return E([
E('h3', {}, title),
E('div', { 'class': 'log-entries-count' },
`${_('Entries')}: ${ipDataArray.length}`
),
@@ -129,7 +183,7 @@ return view.extend({
]);
},
pollInfo: function() {
pollInfo() {
return fs.exec_direct(tools.execPath, [ 'html-info' ], 'json').catch(e => {
ui.addNotification(null, E('p', _('Unable to execute or read contents')
+ ': %s [ %s ]'.format(e.message, tools.execPath)
@@ -174,11 +228,20 @@ return view.extend({
let nft_data = this.formatNftJson(data);
if(nft_data.rules.length > 0) {
for(let [set, bytes] of nft_data.rules) {
let elem = document.getElementById('rules.' + set);
if(nft_data.sink.length > 0) {
for(let i of nft_data.sink) {
let elem = document.getElementById('sink.' + i[0] + '.' + (i[1] || 'all'));
if(elem) {
elem.textContent = bytes;
elem.textContent = i[2];
};
};
};
if(nft_data.sink_local && nft_data.sink_local.length > 0) {
for(let i of nft_data.sink_local) {
let elem = document.getElementById('sink_local.' + i[0] + '.' + (i[1] || 'all'));
if(elem) {
elem.textContent = i[2];
};
};
};
@@ -186,15 +249,16 @@ return view.extend({
let rdTableWrapper = document.getElementById('rdTableWrapper');
if(rdTableWrapper) {
rdTableWrapper.innerHTML = '';
rdTableWrapper.append(this.makeDnsmasqTable(nft_data.dnsmasq));
rdTableWrapper.append(this.makeDnsmasqTable(nft_data.dnsmasq, _('Dnsmasq')));
};
let rdbTableWrapper = document.getElementById('rdbTableWrapper');
if(rdbTableWrapper) {
rdbTableWrapper.innerHTML = '';
rdbTableWrapper.append(this.makeDnsmasqTable(nft_data.dnsmasq_bypass));
let rdsTableWrapper = document.getElementById('rdsTableWrapper');
if(rdsTableWrapper) {
rdsTableWrapper.innerHTML = '';
for(let i of nft_data.dnsmasq_user_instances) {
rdsTableWrapper.append(this.makeDnsmasqTable(i[1], _('Dnsmasq') + ' ' + i[0]));
};
};
} else {
if(poll.active()) {
poll.stop();
@@ -203,7 +267,7 @@ return view.extend({
});
},
load: function() {
load() {
return fs.exec_direct(tools.execPath, [ 'html-info' ], 'json').catch(e => {
ui.addNotification(null, E('p', _('Unable to execute or read contents')
+ ': %s [ %s ]'.format(e.message, tools.execPath)
@@ -211,7 +275,7 @@ return view.extend({
})
},
render: function(data) {
render(data) {
if(!data) {
return;
};
@@ -220,11 +284,12 @@ return view.extend({
data = JSON.parse(data);
} catch(e) {};
let update_status = null,
user_entries = null,
rules = null,
dnsmasq = null,
dnsmasqBypass = null;
let update_status = null,
user_entries = null,
sink = null,
sink_local = null,
dnsmasq = null,
dnsmasqUserInstances = null;
if(data) {
if(data.status === 'enabled') {
@@ -285,39 +350,87 @@ return view.extend({
let nft_data = this.formatNftJson(data);
if(nft_data.rules) {
let table_rules = E('table', { 'class': 'table' }, [
if(nft_data.sink) {
let table = E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th left', 'style': 'min-width:33%' },
_('Match-set')),
_('Instance')),
E('th', { 'class': 'th left' }, _('Protocol')),
E('th', { 'class': 'th left' }, _('Bytes')),
]),
]);
for(let i of nft_data.sink) {
let instance = i[0];
let proto = (i[1] === undefined) ? _('all') : i[1];
let bytes = i[2];
for(let [set, bytes] of nft_data.rules) {
if(!set) {
if(!instance) {
continue;
};
table_rules.append(
table.append(
E('tr', { 'class': 'tr' }, [
E('td',{
'class' : 'td left',
'data-title': _('Match-set'),
}, set + ((set.length >= 1) ? (
' (' + set.replace(/^c/, 'CIDR').replace(/^i/, 'IP').replace(/^d/, 'dnsmasq').replace(/^bi/, 'bypass IP').replace(/^bd/, 'bypass dnsmasq').replace(/^fproxy/, 'full proxy') + ')'
) : '')),
E('td', {
'class' : 'td left',
'id' : 'rules.' + set,
'data-title': _('Instance'),
}, instance),
E('td', {
'class' : 'td left',
'data-title': _('Protocol'),
}, proto),
E('td', {
'class' : 'td left',
'id' : 'sink.' + instance + '.' + (i[1] || 'all'),
'data-title': _('Bytes'),
}, bytes),
])
);
};
rules = E([
E('h3', {}, _('Nftables rules')),
table_rules,
};
sink = E([
E('h3', {}, _('Transit traffic')),
table,
]);
};
if(nft_data.sink_local) {
let table = E('table', { 'class': 'table' }, [
E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th left', 'style': 'min-width:33%' },
_('Instance')),
E('th', { 'class': 'th left' }, _('Protocol')),
E('th', { 'class': 'th left' }, _('Bytes')),
]),
]);
for(let i of nft_data.sink_local) {
let instance = i[0];
let proto = (i[1] === undefined) ? _('all') : i[1];
let bytes = i[2];
if(!instance) {
continue;
};
table.append(
E('tr', { 'class': 'tr' }, [
E('td', {
'class' : 'td left',
'data-title': _('Instance'),
}, instance),
E('td', {
'class' : 'td left',
'data-title': _('Protocol'),
}, proto),
E('td', {
'class' : 'td left',
'id' : 'sink_local.' + instance + '.' + (i[1] || 'all'),
'data-title': _('Bytes'),
}, bytes),
])
);
};
sink_local = E([
E('h3', {}, _('Local traffic')),
table,
]);
};
@@ -325,24 +438,28 @@ return view.extend({
let rdTableWrapper = E('div', {
'id' : 'rdTableWrapper',
'style': 'width:100%'
}, this.makeDnsmasqTable(nft_data.dnsmasq));
}, this.makeDnsmasqTable(nft_data.dnsmasq, _('Dnsmasq')));
dnsmasq = E([
E('h3', {}, _('Dnsmasq')),
rdTableWrapper,
]);
};
if(nft_data.dnsmasq_bypass) {
let rdbTableWrapper = E('div', {
'id' : 'rdbTableWrapper',
if(nft_data.dnsmasq_user_instances) {
let rdsTableWrapper = E('div', {
'id' : 'rdsTableWrapper',
'style': 'width:100%'
}, this.makeDnsmasqTable(nft_data.dnsmasq_bypass));
});
dnsmasqBypass = E([
E('h3', {}, _('Dnsmasq bypass')),
rdbTableWrapper,
]);
for(let i of nft_data.dnsmasq_user_instances) {
rdsTableWrapper.append(this.makeDnsmasqTable(i[1], _('Dnsmasq') + ' ' + i[0]));
};
if(nft_data.dnsmasq_user_instances.length > 0) {
dnsmasqUserInstances = E([
rdsTableWrapper,
]);
};
};
poll.add(L.bind(this.pollInfo, this), this.pollInterval);
@@ -360,7 +477,7 @@ return view.extend({
E('div', { 'class': 'cbi-section-node' }, update_status)
),
E('div', { 'class': 'cbi-section fade-in' },
E('div', { 'class': 'cbi-section-node' }, rules)
E('div', { 'class': 'cbi-section-node' }, sink)
),
];
@@ -373,16 +490,24 @@ return view.extend({
);
}
if(dnsmasqBypass) {
if(sink_local) {
layout.splice(5, 0,
E('div', { 'class': 'cbi-section fade-in' },
E('div', { 'class': 'cbi-section-node' }, dnsmasqBypass)
E('div', { 'class': 'cbi-section-node' }, sink_local)
)
);
};
if(dnsmasqUserInstances) {
layout.splice(6, 0,
E('div', { 'class': 'cbi-section fade-in' },
E('div', { 'class': 'cbi-section-node' }, dnsmasqUserInstances)
)
);
};
if(dnsmasq) {
layout.splice(6, 0,
layout.splice(7, 0,
E('div', { 'class': 'cbi-section fade-in' },
E('div', { 'class': 'cbi-section-node' }, dnsmasq)
)
@@ -1,6 +1,7 @@
'use strict';
'require baseclass';
'require fs';
'require ui';
'require view.ruantiblock.log-widget as widget';
return baseclass.extend({
@@ -1,4 +1,5 @@
'use strict';
'require baseclass';
'require fs';
'require poll';
'require uci';
@@ -15,7 +16,121 @@ const btn_style_warning = 'btn cbi-button-negative important'
return view.extend({
statusTokenValue: null,
disableButtons: function(bool, btn, elems=[]) {
dialogDestroy: baseclass.extend({
__init__(context) {
this.context = context;
},
currentDnsmasqCfgDir: null,
dnsmasqCfgDirsSelect: null,
cancelButton : E('button', {
'id' : 'btn_cancel',
'class': btn_style_neutral,
'click': ui.hideModal,
}, _('Cancel')),
load() {
return L.resolveDefault(fs.list(tools.dnsmasqCfgDirsRoot), null);
},
render(data) {
let section = uci.get(tools.appName, 'config');
this.currentDnsmasqCfgDir = section.dnsmasq_cfg_dir;
let available_cfg_dirs = [];
let dnsmasq_cfg_dirs_arr = data;
if(dnsmasq_cfg_dirs_arr) {
dnsmasq_cfg_dirs_arr.forEach(e => {
let fname = e.name;
if(fname.startsWith('dnsmasq')) {
available_cfg_dirs.push([ fname, tools.dnsmasqCfgDirsRoot + '/' + fname ]);
};
});
};
this.dnsmasqCfgDirsSelect = E('select', {
'id' : 'dnsmasq_cfg_dirs_list',
'class': "cbi-input-select",
}),
available_cfg_dirs.forEach(e => {
this.dnsmasqCfgDirsSelect.append(
E('option', { 'value': e[1] }, e[0]));
});
this.dnsmasqCfgDirsSelect.value = this.currentDnsmasqCfgDir;
ui.showModal(this.title, [
E('h4', _('The service will be disabled and all blacklist data will be deleted. Continue?')),
E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-value' }, [
E('label', { 'class': 'cbi-value-title' },
_('Dnsmasq config directory')),
E('div', { 'class': 'cbi-value-field' }, [
this.dnsmasqCfgDirsSelect,
E('div', { 'class': 'cbi-value-description' },
_('Change dnsmasq config directory')),
]),
]),
]),
E('div', { 'class': 'right' }, [
this.cancelButton,
' ',
E('button', {
'id' : 'btn_apply',
'class': btn_style_warning,
'click': ui.createHandlerFn(this, this.handleApply),
}, _('Shutdown')),
]),
], 'cbi-modal');
},
handleApply(ev) {
this.cancelButton.disabled = true;
return this.context.appAction('destroy').then(() => {
if(this.dnsmasqCfgDirsSelect.value !== this.currentDnsmasqCfgDir) {
uci.set(tools.appName, 'config', 'dnsmasq_cfg_dir',
this.dnsmasqCfgDirsSelect.value);
uci.save();
uci.apply();
};
}).finally(() => {
this.cancelButton.disabled = false;
ui.hideModal();
});
},
error(e) {
ui.showModal(this.title, [
E('div', { 'class': 'cbi-section' },
E('p', {}, _('An error occurred')
+ ': %s'.format(e.message))
),
E('div', { 'class': 'right' },
E('button', {
'class': btn_style_neutral,
'click': ui.hideModal,
}, _('Dismiss'))
),
]);
},
show() {
ui.showModal(null,
E('p', { 'class': 'spinning' }, _('Loading'))
);
this.load().then(content => {
ui.hideModal();
return this.render(content);
}).catch(e => {
ui.hideModal();
return this.error(e);
})
},
}),
disableButtons(bool, btn, elems=[]) {
let btn_start = elems[1] || document.getElementById("btn_start");
let btn_destroy = elems[4] || document.getElementById("btn_destroy");
let btn_enable = elems[2] || document.getElementById("btn_enable");
@@ -31,7 +146,7 @@ return view.extend({
};
},
getAppStatus: function() {
getAppStatus() {
return Promise.all([
fs.exec(tools.execPath, [ 'raw-status' ]),
fs.exec(tools.execPath, [ 'vpn-route-status' ]),
@@ -46,7 +161,7 @@ return view.extend({
});
},
setAppStatus: function(status_array, elems=[], force_app_code) {
setAppStatus(status_array, elems=[], force_app_code) {
let section = uci.get(tools.appName, 'config');
if(!status_array || typeof(section) !== 'object') {
(elems[0] || document.getElementById("status")).innerHTML = tools.makeStatusString(1);
@@ -59,8 +174,7 @@ return view.extend({
let app_status_code = (force_app_code) ? force_app_code : status_array[0].code;
let vpn_route_status_code = status_array[1].code;
let enabled_flag = status_array[2];
let proxy_local_clients = section.proxy_local_clients;
let proxy_mode = section.proxy_mode;
let dnsmasq_cfg_dir = section.dnsmasq_cfg_dir;
let bllist_preset = section.bllist_preset;
let bllist_module = section.bllist_module;
@@ -122,7 +236,6 @@ return view.extend({
(elems[0] || document.getElementById("status")).innerHTML = tools.makeStatusString(
app_status_code,
proxy_mode,
bllist_preset,
bllist_module,
vpn_route_status_code);
@@ -132,7 +245,7 @@ return view.extend({
};
},
serviceAction: function(action, button) {
serviceAction(action, button) {
if(button) {
let elem = document.getElementById(button);
this.disableButtons(true, elem);
@@ -149,7 +262,7 @@ return view.extend({
});
},
appAction: function(action, button) {
appAction(action, button) {
if(button) {
let elem = document.getElementById(button);
this.disableButtons(true, elem);
@@ -167,13 +280,12 @@ return view.extend({
return this.getAppStatus().then(
(status_array) => {
this.setAppStatus(status_array);
ui.hideModal();
}
);
});
},
statusPoll: function() {
statusPoll() {
return fs.read(tools.tokenFile).then(v => {
v = tools.normalizeValue(v);
if(v != this.statusTokenValue) {
@@ -187,48 +299,21 @@ return view.extend({
});
},
dialogDestroy: function(ev) {
ev.target.blur();
let cancel_button = E('button', {
'class': btn_style_neutral,
'click': ui.hideModal,
}, _('Cancel'));
let shutdown_btn = E('button', {
'class': btn_style_warning,
}, _('Shutdown'));
shutdown_btn.onclick = ui.createHandlerFn(this, () => {
cancel_button.disabled = true;
return this.appAction('destroy');
});
ui.showModal(_('Shutdown'), [
E('div', { 'class': 'cbi-section' }, [
E('p', _('The service will be disabled and all blacklist data will be deleted. Continue?')),
]),
E('div', { 'class': 'right' }, [
shutdown_btn,
' ',
cancel_button,
])
]);
},
load: function() {
load() {
return this.getAppStatus();
},
render: function(status_array) {
render(status_array) {
if(!status_array) {
return;
};
let section = uci.get(tools.appName, 'config');
let proxy_local_clients = (typeof(section) === 'object') ?
section.proxy_local_clients : null;
this.statusTokenValue = (Array.isArray(status_array)) ?
tools.normalizeValue(status_array[4]) : null;
let dialog_destroy = new this.dialogDestroy(this);
let status_string = E('div', {
'id' : 'status',
'name' : 'status',
@@ -281,7 +366,7 @@ return view.extend({
'name' : 'btn_destroy',
'class': btn_style_negative,
}, _('Shutdown'));
btn_destroy.onclick = L.bind(this.dialogDestroy, this);
btn_destroy.onclick = () => dialog_destroy.show();
layout_append(btn_destroy, _('Shutdown'),
_('Complete service shutdown, as well as deleting nftsets and blacklist data'));
@@ -8,11 +8,11 @@
'require view.ruantiblock.tools as tools';
return view.extend({
parsers : {},
parsers : {},
appStatusCode: null,
appStatusCode : null,
depends : function(elem, key, array, empty=true) {
depends(elem, key, array, empty=true) {
if(empty && array.length === 0) {
elem.depends(key, '_dummy');
} else {
@@ -20,20 +20,82 @@ return view.extend({
};
},
validateIpPort: function(section, value) {
validateIpPort(section, value) {
return (/^$|^([0-9]{1,3}\.){3}[0-9]{1,3}(#[\d]{2,5})?$/.test(value)) ? true : _('Expecting:')
+ ` ${_('One of the following:')}\n - ${_('valid IP address')}\n - ${_('valid address#port')}\n`;
},
validateUrl: function(section, value) {
validateUrl(section, value) {
return (/^$|^https?:\/\/[\w.-]+(:[0-9]{2,5})?[\w\/~.&?+=-]*$/.test(value)) ? true : _('Expecting:')
+ ` ${_('valid URL')}\n`;
},
load: function() {
CBIBlockFileEdit: form.Value.extend({
__name__ : 'CBI.BlockFileEdit',
__init__(map, section, ctx, id, file, title, description, callback) {
this.map = map;
this.section = section;
this.ctx = ctx;
this.id = id,
this.optional = true;
this.rmempty = true;
this.file = file;
this.title = title;
this.description = description;
this.callback = callback;
this.content = '';
},
cfgvalue(section_id, option) {
return this.content;
},
formvalue(section_id) {
let value = this.content;
let textarea = document.getElementById('widget.file_edit.content.' + this.id);
if(textarea) {
value = textarea.value.trim().replace(/\r\n/g, '\n') + '\n';
};
return value;
},
write(section_id, formvalue) {
return fs.write(this.file, formvalue).then(rc => {
ui.addNotification(null, E('p', _('Contents have been saved.')),
'info');
if(this.callback) {
return this.callback(rc);
};
}).catch(e => {
ui.addNotification(null, E('p', _('Unable to save the contents')
+ ': %s'.format(e.message)));
});
},
load() {
return L.resolveDefault(fs.read(this.file), '').then(c => {
this.content = c;
});
},
renderWidget(section_id, option_index, cfgvalue) {
return E('textarea', {
'id' : 'widget.file_edit.content.' + this.id,
'class' : 'cbi-input-textarea',
'style' : 'width:100% !important;resize:vertical !important',
'rows' : 10,
'wrap' : 'off',
'spellcheck': 'false',
}, cfgvalue);
},
}),
load() {
return Promise.all([
L.resolveDefault(fs.exec(tools.execPath, [ 'raw-status' ]), 1),
L.resolveDefault(fs.list(tools.parsersDir), null),
L.resolveDefault(fs.list(tools.dnsmasqCfgDirsRoot), null),
uci.load(tools.appName),
]).catch(e => {
ui.addNotification(null, E('p', _('Unable to read the contents')
@@ -43,14 +105,14 @@ return view.extend({
});
},
render: function(data) {
render(data) {
if(!data) {
return;
};
this.appStatusCode = data[0].code;
let p_dir_arr = data[1];
let curent_module = uci.get(tools.appName, 'config', 'bllist_module');
let curent_preset = uci.get(tools.appName, 'config', 'bllist_preset');
this.appStatusCode = data[0].code;
let p_dir_arr = data[1];
let curent_module = uci.get(tools.appName, 'config', 'bllist_module');
let curent_preset = uci.get(tools.appName, 'config', 'bllist_preset');
if(p_dir_arr) {
p_dir_arr.forEach(e => {
@@ -129,7 +191,7 @@ return view.extend({
'<br /><code>#comment<br />domain.net<br />anotherdomain.com</code>'
);
let m, s, o;
let m, s, o, ss;
m = new form.Map(tools.appName, _('Ruantiblock') + ' - ' + _('Settings'));
@@ -140,39 +202,32 @@ return view.extend({
/* Main settings tab */
s.tab('main_settings', _('Main settings'));
// PROXY_MODE
o = s.taboption('main_settings', form.ListValue, 'proxy_mode',
_('Proxy mode'));
o.value('1', 'Tor');
o.value('2', 'VPN');
o.value('3', _('Transparent proxy'));
// PROXY_LOCAL_CLIENTS
let proxy_local_clients = s.taboption('main_settings', form.Flag, 'proxy_local_clients',
_('Apply proxy rules to router application traffic'));
proxy_local_clients.rmempty = false;
s.tab('main_tab', _('Main settings'));
// ENABLE_LOGGING
o = s.taboption('main_settings', form.Flag, 'enable_logging',
o = s.taboption('main_tab', form.Flag, 'enable_logging',
_('Logging events'));
o.rmempty = false;
// update_at_startup
o = s.taboption('main_settings', form.Flag, 'update_at_startup',
o = s.taboption('main_tab', form.Flag, 'update_at_startup',
_('Update at startup'));
o.description = _('Update blacklist after system startup');
o.rmempty = false;
// PROXY_LOCAL_CLIENTS
o = s.taboption('main_tab', form.Flag, 'proxy_local_clients',
_('Apply proxy rules to router application traffic'));
o.rmempty = false;
// NFTSET_CLEAR_SETS
o = s.taboption('main_settings', form.Flag, 'nftset_clear_sets',
o = s.taboption('main_tab', form.Flag, 'nftset_clear_sets',
_('Clean up nftsets before updating blacklist'));
o.description = _('Reduces RAM consumption during update');
o.rmempty = false;
// ALLOWED_HOSTS_MODE
o = s.taboption('main_settings', form.ListValue, 'allowed_hosts_mode',
o = s.taboption('main_tab', form.ListValue, 'allowed_hosts_mode',
_('Host filter'));
o.value('0', _('Disabled'));
o.value('1', _('Only listed hosts'));
@@ -180,40 +235,29 @@ return view.extend({
o.description = _('Restriction of hosts that are allowed to bypass blocking');
// ALLOWED_HOSTS_LIST
o = s.taboption('main_settings', form.DynamicList, 'allowed_hosts_list',
o = s.taboption('main_tab', form.DynamicList, 'allowed_hosts_list',
_('IP addresses for host filter'));
o.datatype = 'ip4addr';
// ENABLE_FPROXY
o = s.taboption('main_settings', form.Flag, 'enable_fproxy',
_('Enable full proxy mode'));
o.description = _('All traffic of the specified hosts passes through the proxy, without a blacklist');
o.rmempty = false;
// FPROXY_LIST
o = s.taboption('main_settings', form.DynamicList, 'fproxy_list',
_('IP addresses for full proxy mode'));
o.datatype = 'ip4addr';
/* Tor tab */
s.tab('tor_settings', _('Tor mode'));
s.tab('tor_tab', _('Tor mode'));
// TOR_TRANS_PORT
o = s.taboption('tor_settings', form.Value, 'tor_trans_port',
o = s.taboption('tor_tab', form.Value, 'tor_trans_port',
_('Transparent proxy port'));
o.rmempty = false;
o.datatype = 'port';
// ONION_DNS_ADDR
o = s.taboption('tor_settings', form.Value, 'onion_dns_addr',
o = s.taboption('tor_tab', form.Value, 'onion_dns_addr',
_("Optional DNS resolver for '.onion' zone"), '<code>ipaddress#port</code>');
o.rmempty = false;
o.validate = this.validateIpPort;
// Torrc edit dialog
o = s.taboption('tor_settings', form.Button, '_torrc_btn',
o = s.taboption('tor_tab', form.Button, '_torrc_btn',
_('Tor configuration file'));
o.onclick = () => torrc_edit.show();
o.inputtitle = _('Edit');
@@ -222,10 +266,10 @@ return view.extend({
/* VPN tab */
s.tab('vpn_settings', _('VPN mode'));
s.tab('vpn_tab', _('VPN mode'));
// IF_VPN
o = s.taboption('vpn_settings', widgets.DeviceSelect, 'if_vpn',
o = s.taboption('vpn_tab', widgets.DeviceSelect, 'if_vpn',
_('VPN interface'));
o.multiple = false;
o.noaliases = true;
@@ -233,13 +277,13 @@ return view.extend({
o.default = 'tun0';
// VPN_GW_IP
o = s.taboption('vpn_settings', form.Value, 'vpn_gw_ip',
o = s.taboption('vpn_tab', form.Value, 'vpn_gw_ip',
_('VPN gateway IP address'),
_('If not specified, the VPN interface address is used (or peer address for PPP protocols)'));
o.datatype = 'ip4addr(1)';
// VPN_ROUTE_CHECK
o = s.taboption('vpn_settings', form.ListValue, 'vpn_route_check',
o = s.taboption('vpn_tab', form.ListValue, 'vpn_route_check',
_('Type of adding a VPN rule to the routing table'));
o.value('0', 'hotplug.d');
o.value('1', 'ruab_route_check');
@@ -248,32 +292,46 @@ return view.extend({
_('ruab_route_check - script that regularly checks an entry in the routing table.');
/* Proxy tab */
/* Tproxy tab */
s.tab('proxy_settings', _('Transparent proxy mode'));
s.tab('tproxy_tab', _('Transparent proxy mode'));
// T_PROXY_TYPE
o = s.taboption('tproxy_tab', form.ListValue, 't_proxy_type',
_('Proxy type'));
o.value('0', _('redirect'));
o.value('1', _('tproxy'));
o.description = _('Statement in nftables rules');
// T_PROXY_PORT_TCP
o = s.taboption('proxy_settings', form.Value, 't_proxy_port_tcp',
o = s.taboption('tproxy_tab', form.Value, 't_proxy_port_tcp',
_('Transparent proxy TCP port'));
o.rmempty = false;
o.datatype = 'port';
// T_PROXY_ALLOW_UDP
o = s.taboption('proxy_settings', form.Flag, 't_proxy_allow_udp',
o = s.taboption('tproxy_tab', form.Flag, 't_proxy_allow_udp',
_('Send UDP traffic to transparent proxy'));
o.rmempty = false;
// T_PROXY_PORT_UDP
o = s.taboption('proxy_settings', form.Value, 't_proxy_port_udp',
o = s.taboption('tproxy_tab', form.Value, 't_proxy_port_udp',
_('Transparent proxy UDP port'));
o.rmempty = false;
o.datatype = 'port';
/* Blacklist module tab */
/* Blacklist tab */
s.tab('blacklist_tab', _('Blacklist settings'));
// PROXY_MODE
o = s.taboption('blacklist_tab', form.ListValue, 'proxy_mode',
_('Proxy mode'));
o.value('1', 'Tor');
o.value('2', 'VPN');
o.value('3', _('Transparent proxy'));
// BLLIST_PRESET
let bllist_preset = s.taboption('blacklist_tab', form.ListValue,
'bllist_preset', _('Blacklist update mode'));
@@ -312,29 +370,16 @@ return view.extend({
o.rmempty = false;
o.default = 0;
// ADD_USER_ENTRIES
o = s.taboption('blacklist_tab', form.Flag, 'add_user_entries',
_('Enable user entries'), _('Add user entries to the blacklist when updating'));
// ENABLE_FPROXY
o = s.taboption('blacklist_tab', form.Flag, 'enable_fproxy',
_('Enable full proxy mode'));
o.description = _('All traffic of the specified hosts passes through the proxy, without a blacklist');
o.rmempty = false;
o.default = 0;
o.depends({ bllist_preset: '', '!reverse': true });
// USER_ENTRIES edit dialog
o = s.taboption('blacklist_tab', form.Button, '_user_entries_btn',
_('User entries'));
o.onclick = () => user_entries_edit.show();
o.inputtitle = _('Edit');
o.inputstyle = 'edit btn';
// USER_ENTRIES_REMOTE
o = s.taboption('blacklist_tab', form.DynamicList, 'user_entries_remote',
_('URLs of remote user entries file'));
o.validate = this.validateUrl;
// USER_ENTRIES_DNS
o = s.taboption('blacklist_tab', form.Value, 'user_entries_dns',
_("DNS server that is used for the user's FQDN entries"), '<code>ipaddress[#port]</code>');
o.validate = this.validateIpPort;
// FPROXY_LIST
o = s.taboption('blacklist_tab', form.DynamicList, 'fproxy_list',
_('IP addresses for full proxy mode'));
o.datatype = 'ip4addr';
// BYPASS_MODE
o = s.taboption('blacklist_tab', form.Flag, 'bypass_mode',
@@ -354,6 +399,7 @@ return view.extend({
_('DNS server that is used for the FQDN entries of exclusion list'), '<code>ipaddress[#port]</code>');
o.validate = this.validateIpPort;
if(availableParsers) {
bllist_preset.description += '<br /> ( * - ' + _('requires installed blacklist module') + ' )';
@@ -398,10 +444,9 @@ return view.extend({
// BLLIST_GR_EXCLUDED_SLD_FILE edit dialog
o = s.taboption('parser_settings_tab', form.Button, '_gr_excluded_sld_btn',
_('2nd level domains that are excluded from optimization'));
o.onclick = () => gr_excluded_sld_edit.show();
o.inputtitle = _('Edit');
o.inputstyle = 'edit btn';
//o.description = _('e.g:') + ' <code>livejournal.com</code>';
o.onclick = () => gr_excluded_sld_edit.show();
o.inputtitle = _('Edit');
o.inputstyle = 'edit btn';
// BLLIST_ENABLE_IDN
o = s.taboption('parser_settings_tab', form.Flag, 'bllist_enable_idn',
@@ -447,10 +492,9 @@ return view.extend({
// BLLIST_GR_EXCLUDED_NETS_FILE edit dialog
o = s.taboption('parser_settings_tab', form.Button, '_gr_excluded_nets_btn',
_('IP subnet patterns (/24) that are excluded from optimization'));
o.onclick = () => gr_excluded_nets_edit.show();
o.inputtitle = _('Edit');
o.inputstyle = 'edit btn';
//o.description = _('e.g:') + ' <code>192.168.1.</code>';
o.onclick = () => gr_excluded_nets_edit.show();
o.inputtitle = _('Edit');
o.inputstyle = 'edit btn';
// BLLIST_SUMMARIZE_IP
o = s.taboption('parser_settings_tab', form.Flag, 'bllist_summarize_ip',
@@ -461,7 +505,180 @@ return view.extend({
o = s.taboption('parser_settings_tab', form.Flag, 'bllist_summarize_cidr',
_("Summarize '/24' networks"));
o.rmempty = false;
};
/* User entries tab */
s.tab('user_entries_tab', _('User entries'));
o = s.taboption('user_entries_tab', form.SectionValue, 'user_instance', form.GridSection,
'user_instance');
ss = o.subsection;
ss.addremove = false;
ss.sortable = false;
ss.nodescriptions = true;
ss.modaltitle = `${_('User entries')} - %s`;
ss.max_cols = 2;
/* User entries main settings tab */
ss.tab('u_main_tab', _('Main settings'));
// U_ENABLED
o = ss.taboption('u_main_tab', form.Flag, 'u_enabled',
_('Enabled'),
);
o.rmempty = false;
o.default = '1';
o.editable = true;
o.modalonly = false;
// description
o = ss.taboption('u_main_tab', form.Value, 'u_description',
_("Description"));
o.datatype = 'maxlength(100)';
o.modalonly = null;
// U_PROXY_MODE
o = ss.taboption('u_main_tab', form.ListValue, 'u_proxy_mode',
_('Proxy mode'));
o.value('1', 'Tor');
o.value('2', 'VPN');
o.value('3', _('Transparent proxy'));
o.default = '2';
o.modalonly = true;
// U_SKIP_MARKED_PACKETS
o = ss.taboption('u_main_tab', form.Flag, 'u_skip_marked_packets',
_('Lowest priority'));
o.description = _('This proxy will receive traffic last, even after the main blacklist');
o.rmempty = false;
o.modalonly = true;
// U_ENABLE_FPROXY
o = ss.taboption('u_main_tab', form.Flag, 'u_enable_fproxy',
_('Enable full proxy mode'));
o.description = _('All traffic of the specified hosts passes through the proxy, without a blacklist');
o.rmempty = false;
o.modalonly = true;
// U_FPROXY_LIST
o = ss.taboption('u_main_tab', form.DynamicList, 'u_fproxy_list',
_('IP addresses for full proxy mode'));
o.datatype = 'ip4addr';
o.modalonly = true;
/* User entries tor tab */
ss.tab('u_tor_tab', _('Tor mode'));
// U_TOR_TRANS_PORT
o = ss.taboption('u_tor_tab', form.Value, 'u_tor_trans_port',
_('Transparent proxy port'));
o.rmempty = false;
o.datatype = 'port';
o.modalonly = true;
// U_ONION_DNS_ADDR
o = ss.taboption('u_tor_tab', form.Value, 'u_onion_dns_addr',
_("Optional DNS resolver for '.onion' zone"), '<code>ipaddress#port</code>');
o.rmempty = false;
o.validate = this.validateIpPort;
o.modalonly = true;
/* User entries VPN tab */
ss.tab('u_vpn_tab', _('VPN mode'));
// U_IF_VPN
o = ss.taboption('u_vpn_tab', widgets.DeviceSelect, 'u_if_vpn',
_('VPN interface'));
o.multiple = false;
o.noaliases = true;
o.rmempty = false;
o.default = 'tun0';
o.modalonly = true;
// U_VPN_GW_IP
o = ss.taboption('u_vpn_tab', form.Value, 'u_vpn_gw_ip',
_('VPN gateway IP address'),
_('If not specified, the VPN interface address is used (or peer address for PPP protocols)'));
o.datatype = 'ip4addr(1)';
o.modalonly = true;
/* User entries tproxy tab */
ss.tab('u_tproxy_tab', _('Transparent proxy mode'));
// U_T_PROXY_TYPE
o = ss.taboption('u_tproxy_tab', form.ListValue, 'u_t_proxy_type',
_('Proxy type'));
o.value('0', _('redirect'));
o.value('1', _('tproxy'));
o.description = _('Statement in nftables rules');
// U_T_PROXY_PORT_TCP
o = ss.taboption('u_tproxy_tab', form.Value, 'u_t_proxy_port_tcp',
_('Transparent proxy TCP port'));
o.rmempty = false;
o.datatype = 'port';
o.modalonly = true;
// U_T_PROXY_ALLOW_UDP
o = ss.taboption('u_tproxy_tab', form.Flag, 'u_t_proxy_allow_udp',
_('Send UDP traffic to transparent proxy'));
o.rmempty = false;
o.modalonly = true;
// U_T_PROXY_PORT_UDP
o = ss.taboption('u_tproxy_tab', form.Value, 'u_t_proxy_port_udp',
_('Transparent proxy UDP port'));
o.rmempty = false;
o.datatype = 'port';
o.modalonly = true;
/* User entries items tab */
ss.tab('u_entries_tab', _('Entries'));
ss.addModalOptions = (s, section_id, ev) => {
// user entries edit dialog
o = s.taboption('u_entries_tab', this.CBIBlockFileEdit, this,
'user-entries',
tools.userListsDir + '/' + s.section,
_('Edit entries'),
_('One entry (IP, CIDR or FQDN) per line. In the FQDN records, you can specify the DNS server for resolving this domain (separated by a space). You can also comment on lines (<code>#</code> is the first character of a line).<br />Examples:') +
'<br /><code>#comment<br />domain.net<br />sub.domain.com 8.8.8.8<br />sub.domain.com 8.8.8.8#53<br />74.125.131.19<br />74.125.0.0/16</code>'
);
// DEBUG
console.log(tools.userListsDir + '/' + s.section);
o.modalonly = true;
// U_ENTRIES_REMOTE
o = s.taboption('u_entries_tab', form.DynamicList, 'u_entries_remote',
_('URLs of remote user entries file'));
o.validate = this.validateUrl;
o.modalonly = true;
// U_ENABLE_ENTRIES_REMOTE_PROXY
o = s.taboption('u_entries_tab', form.Flag, 'u_enable_entries_remote_proxy',
_('Downloading files via proxy'), _('Turn on if files are blocked'));
o.rmempty = false;
o.default = 0;
// U_ENTRIES_DNS
o = s.taboption('u_entries_tab', form.Value, 'u_entries_dns',
_("DNS server that is used for the user's FQDN entries"), '<code>ipaddress[#port]</code>');
o.validate = this.validateIpPort;
o.modalonly = true;
};
let map_promise = m.render();
@@ -469,12 +686,28 @@ return view.extend({
return map_promise;
},
handleSaveApply: function(ev, mode) {
return this.handleSave(ev).then(() => {
handleSave(ev, restart) {
let tasks = [];
document.getElementById('maincontent')
.querySelectorAll('.cbi-map').forEach((map, i, a) => {
let res = DOM.callClassMethod(map, 'save');
if(restart && i == a.length - 1 && this.appStatusCode != 1 && this.appStatusCode != 2) {
res.then(() => {
window.setTimeout(() => {
fs.exec_direct(tools.execPath, [ 'restart' ]).then(
() => console.log(tools.execPath + ' restarted...')
);
}, 1000);
});
};
tasks.push(res);
});
return Promise.all(tasks);
},
handleSaveApply(ev, mode) {
return this.handleSave(ev, true).then(() => {
ui.changes.apply(mode == '0');
if(this.appStatusCode != 1 && this.appStatusCode != 2) {
window.setTimeout(() => fs.exec(tools.execPath, [ 'restart' ]), 3000);
};
});
},
});
@@ -38,8 +38,10 @@ return baseclass.extend({
execPath : '/usr/bin/ruantiblock',
tokenFile : '/var/run/ruantiblock.token',
parsersDir : '/usr/libexec/ruantiblock',
dnsmasqCfgDirsRoot: '/tmp',
torrcFile : '/etc/tor/torrc',
userEntriesFile : '/etc/ruantiblock/user_entries',
userListsDir : '/etc/ruantiblock/user_lists',
bypassEntriesFile : '/etc/ruantiblock/bypass_entries',
fqdnFilterFile : '/etc/ruantiblock/fqdn_filter',
ipFilterFile : '/etc/ruantiblock/ip_filter',
@@ -76,7 +78,7 @@ return baseclass.extend({
expect: { result: false }
}),
getInitStatus: function(name) {
getInitStatus(name) {
return this.callInitStatus(name).then(res => {
if(res) {
return res[name].enabled;
@@ -89,7 +91,7 @@ return baseclass.extend({
});
},
handleServiceAction: function(name, action) {
handleServiceAction(name, action) {
return this.callInitAction(name, action).then(success => {
if(!success) {
throw _('Command failed');
@@ -101,13 +103,12 @@ return baseclass.extend({
});
},
normalizeValue: function(v) {
normalizeValue(v) {
return (v && typeof(v) === 'string') ? v.trim().replace(/\r?\n/g, '') : v;
},
makeStatusString: function(
app_status_code,
proxy_mode,
bllist_preset,
bllist_module,
vpn_route_status_code) {
@@ -152,14 +153,6 @@ return baseclass.extend({
%s %s
</td>
</tr>
<tr class="tr">
<td class="td left">
${_('Proxy mode')}:
</td>
<td class="td left">
%s
</td>
</tr>
<tr class="tr">
<td class="td left">
${_('Blacklist update mode')}:
@@ -172,10 +165,9 @@ return baseclass.extend({
`.format(
spinning,
app_status_label,
(app_status_code != 2 && proxy_mode == 2 && vpn_route_status_code != 0)
(app_status_code != 2 && vpn_route_status_code != 0)
? '<span class="label-status error">'
+ _('VPN routing error! Need restart') + '</span>' : '',
(proxy_mode == 3) ? _('Transparent proxy') : (proxy_mode == 2) ? 'VPN' : 'Tor',
(!bllist_preset || bllist_preset === '') ? _('user entries only') :
(this.blacklistPresets[bllist_preset]) ?
`<span style="cursor:help; border-bottom:1px dotted" data-tooltip="${this.blacklistPresets[bllist_preset][2]}">
@@ -185,7 +177,7 @@ return baseclass.extend({
},
fileEditDialog: baseclass.extend({
__init__: function(file, title, description, callback, file_exists=false) {
__init__(file, title, description, callback, file_exists=false) {
this.file = file;
this.title = title;
this.description = description;
@@ -193,11 +185,11 @@ return baseclass.extend({
this.file_exists = file_exists;
},
load: function() {
load() {
return L.resolveDefault(fs.read(this.file), '');
},
render: function(content) {
render(content) {
ui.showModal(this.title, [
E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'cbi-section-descr' }, this.description),
@@ -230,7 +222,7 @@ return baseclass.extend({
]);
},
handleSave: function(ev) {
handleSave(ev) {
let textarea = document.getElementById('widget.modal_content');
let value = textarea.value.trim().replace(/\r\n/g, '\n') + '\n';
@@ -249,7 +241,7 @@ return baseclass.extend({
});
},
error: function(e) {
error(e) {
if(!this.file_exists && e instanceof Error && e.name === 'NotFoundError') {
return this.render();
} else {
@@ -268,7 +260,7 @@ return baseclass.extend({
};
},
show: function() {
show() {
ui.showModal(null,
E('p', { 'class': 'spinning' }, _('Loading'))
);