diff --git a/luci-app-zapret/Makefile b/luci-app-zapret/Makefile
index 54c1719..eb61792 100644
--- a/luci-app-zapret/Makefile
+++ b/luci-app-zapret/Makefile
@@ -2,3 +2,15 @@
# Copyright (с) 2024 remittor
#
+include $(TOPDIR)/rules.mk
+
+PKG_NAME:=luci-app-zapret
+PKG_RELEASE:=20241008
+PKG_VERSION:=1.63-$(PKG_RELEASE)
+PKG_LICENSE:=MIT
+
+LUCI_TITLE:=LuCI support for zapret
+LUCI_DEPENDS:=+zapret
+LUCI_PKGARCH:=all
+
+include $(TOPDIR)/feeds/luci/luci.mk
diff --git a/luci-app-zapret/htdocs/luci-static/resources/view/zapret/service.js b/luci-app-zapret/htdocs/luci-static/resources/view/zapret/service.js
new file mode 100644
index 0000000..43538da
--- /dev/null
+++ b/luci-app-zapret/htdocs/luci-static/resources/view/zapret/service.js
@@ -0,0 +1,297 @@
+'use strict';
+'require fs';
+'require poll';
+'require uci';
+'require ui';
+'require view';
+'require view.zapret.tools as tools';
+
+const btn_style_neutral = 'btn';
+const btn_style_action = 'btn cbi-button-action';
+const btn_style_positive = 'btn cbi-button-save important';
+const btn_style_negative = 'btn cbi-button-reset important';
+const btn_style_warning = 'btn cbi-button-negative important';
+
+return view.extend({
+ disableButtons: function(flag, 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");
+ let btn_update = elems[3] || document.getElementById("btn_update");
+
+ btn_start.disabled = flag;
+ btn_update.disabled = true; // TODO
+ btn_destroy.disabled = flag;
+ if (btn === btn_update) {
+ btn_enable.disabled = false;
+ } else {
+ btn_enable.disabled = flag;
+ }
+ },
+
+ getAppStatus: function() {
+ return Promise.all([
+ { code: -1 }, //fs.exec(tools.execPath, [ 'raw-status' ]),
+ { code: -1 }, //fs.exec(tools.execPath, [ 'vpn-route-status' ]),
+ tools.getInitStatus(tools.appName),
+ uci.load(tools.appName),
+ ]).catch(e => {
+ ui.addNotification(null, E('p', _('Unable to execute or read contents')
+ + ': %s [ %s | %s | %s ]'.format(
+ e.message, tools.execPath, 'tools.getInitStatus', 'uci.zapret'
+ )));
+ });
+ },
+
+ setAppStatus: function(status_array, elems = [ ], force_app_code = 0) {
+ let section = uci.get(tools.appName, 'config');
+ if (!status_array || section == null || typeof(section) !== 'object') {
+ (elems[0] || document.getElementById("status")).innerHTML = tools.makeStatusString(1);
+ ui.addNotification(null, E('p', _('Unable to read the contents') + ': setAppStatus()'));
+ this.disableButtons(true, null, elems);
+ return;
+ }
+
+ 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 z_fwtype = section.FWTYPE;
+ let z_mode = section.MODE;
+ let bllist_preset = 'user_only';
+
+ let btn_enable = elems[2] || document.getElementById('btn_enable');
+ if (enabled_flag == true) {
+ btn_enable.onclick = ui.createHandlerFn(this, this.serviceAction, 'disable', 'btn_enable');
+ btn_enable.textContent = _('Enabled');
+ btn_enable.className = btn_style_positive;
+ } else {
+ btn_enable.onclick = ui.createHandlerFn(this, this.serviceAction, 'enable', 'btn_enable');
+ btn_enable.textContent = _('Disabled');
+ btn_enable.className = btn_style_negative;
+ }
+
+ let btn_start = elems[1] || document.getElementById('btn_start');
+ let btn_update = elems[3] || document.getElementById('btn_update');
+ let btn_destroy = elems[4] || document.getElementById('btn_destroy');
+
+ let btnStartStateOn = () => {
+ btn_start.onclick = ui.createHandlerFn(this, this.appAction, 'stop', 'btn_start');
+ btn_start.textContent = _('Enabled');
+ btn_start.className = btn_style_positive;
+ };
+
+ let btnStartStateOff = () => {
+ btn_start.onclick = ui.createHandlerFn(this, this.appAction, 'start', 'btn_start');
+ btn_start.textContent = _('Disabled');
+ btn_start.className = btn_style_negative;
+ };
+
+ if (app_status_code == -1) {
+ this.disableButtons(false, null, elems);
+ btnStartStateOn();
+ }
+ else if (app_status_code == 0) {
+ this.disableButtons(false, null, elems);
+ btnStartStateOn();
+ btn_destroy.disabled = false;
+ btn_update.disabled = false;
+ }
+ else if (app_status_code == 2) {
+ this.disableButtons(false, null, elems);
+ btnStartStateOff();
+ btn_update.disabled = true;
+ }
+ else if (app_status_code == 3) {
+ btnStartStateOff();
+ this.disableButtons(true, btn_start, elems);
+ }
+ else if (app_status_code == 4) {
+ btnStartStateOn();
+ this.disableButtons(true, btn_update, elems);
+ }
+ else {
+ ui.addNotification(null, E('p', _('Error')
+ + ' %s: return code = %s'.format(tools.execPath, app_status_code)));
+ this.disableButtons(true, null, elems);
+ }
+
+ (elems[0] || document.getElementById("status")).innerHTML = tools.makeStatusString(app_status_code, z_fwtype, bllist_preset);
+
+ if (!poll.active()) {
+ poll.start();
+ }
+ },
+
+ serviceAction: function(action, button) {
+ if (button) {
+ let elem = document.getElementById(button);
+ this.disableButtons(true, elem);
+ }
+
+ poll.stop();
+
+ return tools.handleServiceAction(tools.appName, action).then(() => {
+ return this.getAppStatus().then(
+ (status_array) => {
+ this.setAppStatus(status_array);
+ }
+ );
+ });
+ },
+
+ appAction: function(action, button) {
+ if (button) {
+ let elem = document.getElementById(button);
+ this.disableButtons(true, elem);
+ }
+
+ poll.stop();
+
+ if (action === 'update') {
+ this.getAppStatus().then(
+ (status_array) => {
+ this.setAppStatus(status_array, [], 4);
+ }
+ );
+ }
+
+ return fs.exec_direct(tools.execPath, [ action ]).then(res => {
+ return this.getAppStatus().then(
+ (status_array) => {
+ this.setAppStatus(status_array);
+ ui.hideModal();
+ }
+ );
+ });
+ },
+
+ statusPoll: function() {
+ this.getAppStatus().then(
+ L.bind(this.setAppStatus, this)
+ );
+ },
+
+ 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. Continue?')),
+ ]),
+ E('div', { 'class': 'right' }, [
+ shutdown_btn,
+ ' ',
+ cancel_button,
+ ])
+ ]);
+ },
+
+ load: function() {
+ return this.getAppStatus();
+ },
+
+ render: function(status_array) {
+ if (!status_array) {
+ return;
+ }
+
+ let section = uci.get(tools.appName, 'config');
+
+ let status_string = E('div', {
+ 'id' : 'status',
+ 'name' : 'status',
+ 'class': 'cbi-section-node',
+ });
+
+ let layout = E('div', { 'class': 'cbi-section-node' });
+
+ function layout_append(elem, title, descr) {
+ descr = (descr) ? E('div', { 'class': 'cbi-value-description' }, descr) : '';
+ layout.append(
+ E('div', { 'class': 'cbi-value' }, [
+ E('label', { 'class': 'cbi-value-title', 'for': elem.id + '_hidden' || null }, title),
+ E('div', { 'class': 'cbi-value-field' }, [
+ E('div', {}, elem),
+ E('input', {
+ 'id' : elem.id + '_hidden',
+ 'type': 'hidden',
+ }),
+ descr,
+ ]),
+ ])
+ );
+ }
+
+ let btn_start = E('button', {
+ 'id' : 'btn_start',
+ 'name' : 'btn_start',
+ 'class': btn_style_action,
+ }, _('Enable'));
+ layout_append(btn_start, _('Service'));
+
+ let btn_enable = E('button', {
+ 'id' : 'btn_enable',
+ 'name' : 'btn_enable',
+ 'class': btn_style_positive,
+ }, _('Enable'));
+ layout_append(btn_enable, _('Run at startup'));
+
+ let btn_update = E('button', {
+ 'id' : 'btn_update',
+ 'name' : 'btn_update',
+ 'class': btn_style_action,
+ }, _('Update'));
+ btn_update.onclick = ui.createHandlerFn(this, () => { this.appAction('update', 'btn_update') });
+ layout_append(btn_update, _('Update blacklist'));
+
+ let btn_destroy = E('button', {
+ 'id' : 'btn_destroy',
+ 'name' : 'btn_destroy',
+ 'class': btn_style_negative,
+ }, _('Shutdown'));
+ btn_destroy.onclick = L.bind(this.dialogDestroy, this);
+
+ layout_append(btn_destroy, _('Shutdown'), _('Complete service shutdown'));
+
+ let elems = [ status_string, btn_start, btn_enable, btn_update, btn_destroy ];
+ this.setAppStatus(status_array, elems);
+
+ poll.add(L.bind(this.statusPoll, this));
+
+ let url1 = 'https://github.com/bol-van/zapret';
+ let url2 = 'https://github.com/remittor/zapret-openwrt';
+
+ return E([
+ E('h2', { 'class': 'fade-in' }, _('Zapret')),
+ E('div', { 'class': 'cbi-section-descr fade-in' },
+ E('a', { 'href': url1, 'target': '_blank' }, url1),
+ ),
+ E('div', { 'class': 'cbi-section-descr fade-in' },
+ E('a', { 'href': url2, 'target': '_blank' }, url2),
+ ),
+ E('div', { 'class': 'cbi-section fade-in' }, [
+ status_string,
+ E('hr'),
+ ]),
+ E('div', { 'class': 'cbi-section fade-in' },
+ layout
+ ),
+ ]);
+ },
+
+ handleSave : null,
+ handleSaveApply: null,
+ handleReset : null,
+});
diff --git a/luci-app-zapret/htdocs/luci-static/resources/view/zapret/settings.js b/luci-app-zapret/htdocs/luci-static/resources/view/zapret/settings.js
new file mode 100644
index 0000000..1e87b63
--- /dev/null
+++ b/luci-app-zapret/htdocs/luci-static/resources/view/zapret/settings.js
@@ -0,0 +1,210 @@
+'use strict';
+'require fs';
+'require form';
+'require tools.widgets as widgets';
+'require uci';
+'require ui';
+'require view';
+'require view.zapret.tools as tools';
+
+return view.extend({
+ parsers: { },
+
+ appStatusCode: null,
+
+ depends: function(elem, key, array, empty=true) {
+ if (empty && array.length === 0) {
+ elem.depends(key, '_dummy');
+ } else {
+ array.forEach(e => elem.depends(key, e));
+ }
+ },
+
+ validateIpPort: function(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) {
+ return (/^$|^https?:\/\/[\w.-]+(:[0-9]{2,5})?[\w\/~.&?+=-]*$/.test(value)) ? true : _('Expecting:')
+ + ` ${_('valid URL')}\n`;
+ },
+
+ load: function() {
+ return Promise.all([
+ { code: -1}, // L.resolveDefault(fs.exec(tools.execPath, [ 'raw-status' ]), 1),
+ null, // L.resolveDefault(fs.list(tools.parsersDir), null),
+ uci.load(tools.appName),
+ ]).catch(e => {
+ ui.addNotification(null, E('p', _('Unable to read the contents') + ': %s '.format(e.message) ));
+ });
+ },
+
+ render: function(data) {
+ if (!data) {
+ return;
+ }
+ this.appStatusCode = data[0].code;
+
+ let m, s, o, tabname;
+
+ m = new form.Map(tools.appName, _('Zapret') + ' - ' + _('Settings'));
+
+ s = m.section(form.NamedSection, 'config');
+ s.anonymous = true;
+ s.addremove = false;
+
+ /* Main settings tab */
+
+ tabname = 'main_settings';
+ s.tab(tabname, _('Main settings'));
+
+ o = s.taboption(tabname, form.ListValue, 'FWTYPE', _('FWTYPE'));
+ o.value('nftables', 'NFTables');
+ //o.value('iptables', 'IPTables');
+ //o.value('ipfw', 'ipfw');
+
+ o = s.taboption(tabname, form.ListValue, 'MODE', _('MODE'));
+ o.value('nfqws', 'nfqws');
+ //o.value('tpws', 'tpws');
+
+ o = s.taboption(tabname, form.ListValue, 'FLOWOFFLOAD', _('FLOWOFFLOAD'));
+ o.value('donttouch', 'donttouch');
+ o.value('none', 'none');
+ o.value('software', 'software');
+ o.value('hardware', 'hardware');
+
+ o = s.taboption(tabname, form.ListValue, 'INIT_APPLY_FW', _('INIT_APPLY_FW'));
+ o.value('0', 'False');
+ o.value('1', 'True');
+
+ o = s.taboption(tabname, form.ListValue, 'DISABLE_IPV4', _('DISABLE_IPV4'));
+ o.value('0', 'False');
+ o.value('1', 'True');
+
+ o = s.taboption(tabname, form.ListValue, 'DISABLE_IPV6', _('DISABLE_IPV6'));
+ o.value('0', 'False');
+ o.value('1', 'True');
+
+ o = s.taboption(tabname, form.ListValue, 'MODE_FILTER', _('MODE_FILTER'));
+ //o.value('none', 'none');
+ //o.value('ipset', 'ipset');
+ o.value('hostlist', 'hostlist');
+ o.value('autohostlist', 'autohostlist');
+
+ o = s.taboption(tabname, form.ListValue, 'MODE_HTTP', _('MODE_HTTP'));
+ o.value('0', 'False');
+ o.value('1', 'True');
+
+ o = s.taboption(tabname, form.ListValue, 'MODE_HTTP_KEEPALIVE', _('MODE_HTTP_KEEPALIVE'));
+ o.value('0', 'False');
+ o.value('1', 'True');
+
+ o = s.taboption(tabname, form.ListValue, 'MODE_HTTPS', _('MODE_HTTPS'));
+ o.value('0', 'False');
+ o.value('1', 'True');
+
+ o = s.taboption(tabname, form.ListValue, 'MODE_QUIC', _('MODE_QUIC'));
+ o.value('0', 'False');
+ o.value('1', 'True');
+
+ /* NFQWS_OPT_DESYNC tab */
+
+ tabname = 'nfqws_params';
+ s.tab(tabname, _('NFQWS options'));
+
+ let add_delim = function() {
+ o = s.taboption(tabname, form.DummyValue, '_hr');
+ o.rawhtml = true;
+ o.default = '
';
+ };
+
+ let add_param = function(param, locname = null, rows = 10) {
+ if (!locname)
+ locname = param;
+ o = s.taboption(tabname, form.Button, '_' + param + '_btn', locname);
+ o.onclick = () => new tools.longstrEditDialog('config', param, param, locname, rows).show();
+ o.inputtitle = _('Edit');
+ o.inputstyle = 'edit btn';
+ o = s.taboption(tabname, form.DummyValue, '_' + param);
+ o.rawhtml = false;
+ o.cfgvalue = function(section_id) {
+ var name = uci.get(tools.appName, section_id, param);
+ if (name == null || name == "")
+ name = "";
+ return name;
+ };
+ };
+
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC');
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC_SUFFIX');
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC_HTTP');
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC_HTTP_SUFFIX');
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC_HTTPS');
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC_HTTPS_SUFFIX');
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC_HTTP6');
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC_HTTP6_SUFFIX');
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC_HTTPS6');
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC_HTTPS6_SUFFIX');
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC_QUIC');
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC_QUIC_SUFFIX');
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC_QUIC6');
+ add_delim();
+ add_param('NFQWS_OPT_DESYNC_QUIC6_SUFFIX');
+
+ /* Blacklist settings */
+
+ tabname = 'blacklist_tab';
+ s.tab(tabname, _('Blacklist settings'));
+
+ let user_entries_edit = new tools.fileEditDialog(
+ tools.userEntriesFile,
+ _('User entries'),
+ _('One hostname per line.
Examples:'),
+ 'domain.net
sub.domain.com
googlevideo.com',
+ 15
+ );
+ o = s.taboption(tabname, form.Button, '_user_entries_btn', _('User hostname entries'));
+ o.onclick = () => user_entries_edit.show();
+ o.inputtitle = _('Edit');
+ o.inputstyle = 'edit btn';
+
+ let ip_filter_edit = new tools.fileEditDialog(
+ tools.ipFilterFile,
+ _('IP filter'),
+ _('Patterns can be strings or regular expressions. Each pattern in a separate line
Examples:'),
+ '128.199.0.0/16
34.217.90.52
162.13.190.77',
+ 15
+ );
+ o = s.taboption(tabname, form.Button, '_ip_filter_btn', _('User IP entries'));
+ o.onclick = () => ip_filter_edit.show();
+ o.inputtitle = _('Edit');
+ o.inputstyle = 'edit btn';
+
+ let map_promise = m.render();
+ map_promise.then(node => node.classList.add('fade-in'));
+ return map_promise;
+ },
+
+ handleSaveApply: function(ev, mode) {
+ return this.handleSave(ev).then(() => {
+ ui.changes.apply(mode == '0');
+ if (this.appStatusCode != 1 && this.appStatusCode != 2) {
+ window.setTimeout(() => fs.exec(tools.execPath, [ 'restart' ]), 3000);
+ }
+ });
+ },
+});
diff --git a/luci-app-zapret/htdocs/luci-static/resources/view/zapret/tools.js b/luci-app-zapret/htdocs/luci-static/resources/view/zapret/tools.js
new file mode 100644
index 0000000..836fa69
--- /dev/null
+++ b/luci-app-zapret/htdocs/luci-static/resources/view/zapret/tools.js
@@ -0,0 +1,348 @@
+'use strict';
+'require baseclass';
+'require fs';
+'require rpc';
+'require ui';
+'require uci';
+
+document.head.append(E('style', {'type': 'text/css'},
+`
+.label-status {
+ display: inline;
+ margin: 0 2px 0 0 !important;
+ padding: 2px 4px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ font-weight: bold;
+ color: #fff !important;
+}
+.starting {
+ background-color: #9c994c !important;
+}
+.running {
+ background-color: #2ea256 !important;
+}
+.updating {
+ background-color: #1e82ff !important;
+}
+.stopped {
+ background-color: #8a8a8a !important;
+}
+.error {
+ background-color: #ff4e54 !important;
+}
+`));
+
+return baseclass.extend({
+ appName : 'zapret',
+ execPath : '/opt/zapret/init.d/openwrt/zapret',
+ parsersDir : '/usr/libexec/ruantiblock',
+ userEntriesFile : '/opt/zapret/ipset/zapret-hosts-user.txt',
+ ipFilterFile : '/opt/zapret/ipset/zapret-ip-user.txt',
+
+ infoLabelStarting : '' + _('Starting') + '',
+ infoLabelRunning : '' + _('Enabled') + '',
+ infoLabelUpdating : '' + _('Updating') + '',
+ infoLabelStopped : '' + _('Disabled') + '',
+ infoLabelError : '' + _('Error') + '',
+
+ callInitStatus: rpc.declare({
+ object: 'luci',
+ method: 'getInitList',
+ params: [ 'name' ],
+ expect: { '': {} }
+ }),
+
+ callInitAction: rpc.declare({
+ object: 'luci',
+ method: 'setInitAction',
+ params: [ 'name', 'action' ],
+ expect: { result: false }
+ }),
+
+ getInitStatus: function(name) {
+ return this.callInitStatus(name).then(res => {
+ if (res) {
+ return res[name].enabled;
+ } else {
+ throw _('Command failed');
+ }
+ }).catch(e => {
+ ui.addNotification(null, E('p', _('Failed to get %s init status: %s').format(name, e)));
+ });
+ },
+
+ handleServiceAction: function(name, action) {
+ return this.callInitAction(name, action).then(success => {
+ if (!success) {
+ throw _('Command failed');
+ }
+ return true;
+ }).catch(e => {
+ ui.addNotification(null, E('p', _('Service action failed "%s %s": %s').format(name, action, e)));
+ });
+ },
+
+ normalizeValue: function(v) {
+ return (v && typeof(v) === 'string') ? v.trim().replace(/\r?\n/g, '') : v;
+ },
+
+ makeStatusString: function(app_status_code, fwtype, bllist_preset) {
+ let app_status_label;
+ let spinning = '';
+
+ switch(app_status_code) {
+ case 0:
+ app_status_label = this.infoLabelRunning;
+ break;
+ case 2:
+ app_status_label = this.infoLabelStopped;
+ break;
+ case 3:
+ app_status_label = this.infoLabelStarting;
+ spinning = ' spinning';
+ break;
+ case 4:
+ app_status_label = this.infoLabelUpdating;
+ spinning = ' spinning';
+ break;
+ default:
+ app_status_label = this.infoLabelError;
+ return `
+
+ |
+ ${_('Status')}:
+ |
+
+ ${app_status_label}
+ |
+
+
`;
+ }
+
+ return `
+
+ |
+ ${_('Status')}:
+ |
+
+ %s %s
+ |
+
+
+ |
+ ${_('FW type')}:
+ |
+
+ %s
+ |
+
+
+ |
+ ${_('Blacklist update mode')}:
+ |
+
+ %s
+ |
+
+
+ `.format(
+ spinning,
+ app_status_label,
+ '',
+ fwtype,
+ _('user entries only')
+ );
+ },
+
+ fileEditDialog: baseclass.extend({
+ __init__: function(file, title, desc, aux = null, rows = 10, callback, file_exists = false) {
+ this.file = file;
+ this.title = title;
+ this.desc = desc;
+ this.aux = aux;
+ this.rows = rows,
+ this.callback = callback;
+ this.file_exists = file_exists;
+ },
+
+ load: function() {
+ return L.resolveDefault(fs.read(this.file), '');
+ },
+
+ render: function(content) {
+ let descr = this.desc;
+ if (this.aux)
+ descr += '
' + this.aux;
+ ui.showModal(this.title, [
+ E('div', { 'class': 'cbi-section' }, [
+ E('div', { 'class': 'cbi-section-descr' }, descr),
+ E('div', { 'class': 'cbi-section' },
+ E('p', {},
+ E('textarea', {
+ 'id': 'widget.modal_content',
+ 'class': 'cbi-input-textarea',
+ 'style': 'width:100% !important',
+ 'rows': this.rows,
+ 'wrap': 'off',
+ 'spellcheck': 'false',
+ },
+ content)
+ )
+ ),
+ ]),
+ E('div', { 'class': 'right' }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': ui.hideModal,
+ }, _('Dismiss')),
+ ' ',
+ E('button', {
+ 'id': 'btn_save',
+ 'class': 'btn cbi-button-positive important',
+ 'click': ui.createHandlerFn(this, this.handleSave),
+ }, _('Save')),
+ ]),
+ ]);
+ },
+
+ handleSave: function(ev) {
+ let txt = document.getElementById('widget.modal_content');
+ let value = txt.value.trim().replace(/\r\n/g, '\n') + '\n';
+
+ return fs.write(this.file, value).then(async rc => {
+ txt.value = value;
+ 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)));
+ }).finally(() => {
+ ui.hideModal();
+ });
+ },
+
+ error: function(e) {
+ if (!this.file_exists && e instanceof Error && e.name === 'NotFoundError') {
+ return this.render();
+ } else {
+ ui.showModal(this.title, [
+ E('div', { 'class': 'cbi-section' },
+ E('p', {}, _('Unable to read the contents') + ': %s'.format(e.message))
+ ),
+ E('div', { 'class': 'right' },
+ E('button', {
+ 'class': 'btn',
+ 'click': ui.hideModal,
+ }, _('Dismiss'))
+ ),
+ ]);
+ }
+ },
+
+ show: function() {
+ 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);
+ });
+ },
+ }),
+
+ longstrEditDialog: baseclass.extend({
+ __init__: function(cfgsec, cfgparam, title, desc, rows = 10) {
+ this.cfgsec = cfgsec;
+ this.cfgparam = cfgparam;
+ this.title = title;
+ this.desc = desc;
+ this.rows = rows;
+ },
+
+ load: function() {
+ return uci.get('zapret', this.cfgsec, this.cfgparam);
+ },
+
+ render: function(content) {
+ ui.showModal(this.title, [
+ E('div', { 'class': 'cbi-section' }, [
+ E('div', { 'class': 'cbi-section-descr' }, this.desc),
+ E('div', { 'class': 'cbi-section' },
+ E('p', {},
+ E('textarea', {
+ 'id': 'widget.modal_content',
+ 'class': 'cbi-input-textarea',
+ 'style': 'width:100% !important',
+ 'rows': this.rows,
+ 'wrap': 'on',
+ 'spellcheck': 'false',
+ },
+ content)
+ )
+ ),
+ ]),
+ E('div', { 'class': 'right' }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': ui.hideModal,
+ }, _('Dismiss')),
+ ' ',
+ E('button', {
+ 'id': 'btn_save',
+ 'class': 'btn cbi-button-positive important',
+ 'click': ui.createHandlerFn(this, this.handleSave),
+ }, _('Save')),
+ ]),
+ ]);
+ },
+
+ handleSave: function(ev) {
+ let txt = document.getElementById('widget.modal_content');
+ let value = txt.value.trim().replace(/\r\n/g, ' ').replace(/\r/g, ' ').replace(/\n/g, ' ').trim();
+
+ uci.set('zapret', this.cfgsec, this.cfgparam, value);
+
+ return uci.save()
+ .then(L.bind(ui.changes.init, ui.changes))
+ //.then(L.bind(ui.changes.apply, ui.changes))
+ .then(ui.addNotification(null, E('p', _('Contents have been saved.')), 'info'))
+ .catch(e => {
+ ui.addNotification(null, E('p', _('Unable to save the contents') + ': %s'.format(e.message)));
+ }).finally(() => {
+ ui.hideModal();
+ });
+ },
+
+ error: function(e) {
+ let msg = (typeof(e) == 'object') ? e.message : ''+e;
+ ui.showModal(this.title, [
+ E('div', { 'class': 'cbi-section' },
+ E('p', {}, _('Unable to read the contents') + ': %s'.format(msg))
+ ),
+ E('div', { 'class': 'right' },
+ E('button', {
+ 'class': 'btn',
+ 'click': ui.hideModal,
+ }, _('Dismiss'))
+ ),
+ ]);
+ },
+
+ show: function() {
+ ui.showModal(null, E('p', { 'class': 'spinning' }, _('Loading')) );
+ let content = this.load();
+ ui.hideModal();
+ if (content == null) {
+ return this.error('Cannot load parameter');
+ }
+ return this.render(content);
+ },
+ }),
+
+});
diff --git a/luci-app-zapret/root/usr/share/luci/menu.d/luci-app-zapret.json b/luci-app-zapret/root/usr/share/luci/menu.d/luci-app-zapret.json
new file mode 100644
index 0000000..463c89d
--- /dev/null
+++ b/luci-app-zapret/root/usr/share/luci/menu.d/luci-app-zapret.json
@@ -0,0 +1,36 @@
+{
+ "admin/services/zapret": {
+ "title": "Zapret",
+ "order": 61,
+ "action": {
+ "type": "alias",
+ "path": "admin/services/zapret/service"
+ },
+ "depends": {
+ "acl": [ "luci-app-zapret" ],
+ "fs": {
+ "/opt/zapret/sync_config.sh": "executable",
+ "/etc/init.d/zapret": "executable"
+ },
+ "uci": { "zapret": true }
+ }
+ },
+
+ "admin/services/zapret/service": {
+ "title": "Service",
+ "order": 10,
+ "action": {
+ "type": "view",
+ "path": "zapret/service"
+ }
+ },
+
+ "admin/services/zapret/settings": {
+ "title": "Settings",
+ "order": 20,
+ "action": {
+ "type": "view",
+ "path": "zapret/settings"
+ }
+ }
+}
diff --git a/luci-app-zapret/root/usr/share/rpcd/acl.d/luci-app-zapret.json b/luci-app-zapret/root/usr/share/rpcd/acl.d/luci-app-zapret.json
new file mode 100644
index 0000000..969ffb8
--- /dev/null
+++ b/luci-app-zapret/root/usr/share/rpcd/acl.d/luci-app-zapret.json
@@ -0,0 +1,27 @@
+{
+ "luci-app-zapret": {
+ "description": "Grant access to zapret procedures",
+ "read": {
+ "cgi-io": [ "exec" ],
+ "file": {
+ "/opt/zapret/config": [ "read" ],
+ "/opt/zapret/ipset/*": [ "read" ],
+ "/etc/crontabs/root": [ "read" ],
+ "/etc/init.d/zapret*": [ "exec" ],
+ "/opt/zapret/sync_config.sh": [ "exec" ]
+ },
+ "uci": [ "zapret", "network", "firewall" ],
+ "ubus": {
+ "luci": [ "getInitList", "setInitAction" ]
+ }
+ },
+ "write": {
+ "file": {
+ "/opt/zapret/config": [ "write" ],
+ "/opt/zapret/ipset/*.txt": [ "write" ],
+ "/etc/crontabs/root": [ "write" ]
+ },
+ "uci": [ "zapret" ]
+ }
+ }
+}
diff --git a/zapret/Makefile b/zapret/Makefile
index 33c4b0e..4492970 100644
--- a/zapret/Makefile
+++ b/zapret/Makefile
@@ -1,5 +1,5 @@
#
-# Copyright (�) 2024 remittor
+# Copyright (c) 2024 remittor
#
include $(TOPDIR)/rules.mk
@@ -99,6 +99,7 @@ define Package/$(PKG_NAME)/install
$(INSTALL_CONF) ./ipset/zapret-ip-user.txt $(1)/opt/zapret/ipset/zapret-ip-user.txt
$(INSTALL_CONF) ./ipset/zapret-ip-user-exclude.txt $(1)/opt/zapret/ipset/zapret-ip-user-exclude.txt
$(INSTALL_CONF) ./ipset/zapret-ip-user-ipban.txt $(1)/opt/zapret/ipset/zapret-ip-user-ipban.txt
+ $(INSTALL_BIN) ./sync_config.sh $(1)/opt/zapret/sync_config.sh
endef
define Package/$(PKG_NAME)/postinst
diff --git a/zapret/sync_config.sh b/zapret/sync_config.sh
new file mode 100755
index 0000000..7f9f48a
--- /dev/null
+++ b/zapret/sync_config.sh
@@ -0,0 +1,67 @@
+#!/bin/sh
+# Copyright (c) 2024 remittor
+
+EXEDIR=/opt/zapret
+ZAPRET_BASE=/opt/zapret
+ZAPRET_CONFIG="$ZAPRET_BASE/config"
+ZAPRET_CFG=/etc/config/zapret
+
+function get_sed_compat
+{
+ local str=$( ( echo $1|sed -r 's/([\$\.\*\/\[\\^])/\\\1/g'|sed 's/[]]/\\]/g' )>&1 )
+ echo "$str"
+}
+
+function set_param_value
+{
+ local param=$1
+ local value=$( get_sed_compat "$2" )
+ local fname=${3:-$ZAPRET_CONFIG}
+ sed -i "s/^$param=.*/$param=$value/g" $fname
+}
+
+function set_param_value_str
+{
+ local param=$1
+ local value=$( get_sed_compat "$2" )
+ local fname=${3:-$ZAPRET_CONFIG}
+ sed -i "s/^$param=.*/$param=\"$value\"/g" $fname
+}
+
+function sync_param
+{
+ local param=$1
+ local vtype=$2
+ local value=$( uci -q get zapret.@main[0].$param )
+ if [ "$vtype" = "str" ]; then
+ set_param_value_str $param "$value"
+ else
+ set_param_value $param $value
+ fi
+}
+
+sync_param MODE
+sync_param FLOWOFFLOAD
+sync_param INIT_APPLY_FW
+sync_param DISABLE_IPV4
+sync_param DISABLE_IPV6
+sync_param MODE_FILTER
+sync_param NFQWS_OPT_DESYNC str
+sync_param NFQWS_OPT_DESYNC_SUFFIX str
+sync_param MODE_HTTP
+sync_param MODE_HTTP_KEEPALIVE
+sync_param NFQWS_OPT_DESYNC_HTTP str
+sync_param NFQWS_OPT_DESYNC_HTTP_SUFFIX str
+sync_param NFQWS_OPT_DESYNC_HTTP6 str
+sync_param NFQWS_OPT_DESYNC_HTTP6_SUFFIX str
+sync_param MODE_HTTPS
+sync_param NFQWS_OPT_DESYNC_HTTPS str
+sync_param NFQWS_OPT_DESYNC_HTTPS_SUFFIX str
+sync_param NFQWS_OPT_DESYNC_HTTPS6 str
+sync_param NFQWS_OPT_DESYNC_HTTPS6_SUFFIX str
+sync_param MODE_QUIC
+sync_param NFQWS_OPT_DESYNC_QUIC str
+sync_param NFQWS_OPT_DESYNC_QUIC_SUFFIX str
+sync_param NFQWS_OPT_DESYNC_QUIC6 str
+sync_param NFQWS_OPT_DESYNC_QUIC6_SUFFIX str
+