From 5fc039e165a364bb31e67598b2f524668c2e2bce Mon Sep 17 00:00:00 2001 From: remittor Date: Fri, 11 Oct 2024 14:49:19 +0300 Subject: [PATCH] Add luci-app-zapret --- luci-app-zapret/Makefile | 12 + .../resources/view/zapret/service.js | 297 +++++++++++++++ .../resources/view/zapret/settings.js | 210 +++++++++++ .../resources/view/zapret/tools.js | 348 ++++++++++++++++++ .../share/luci/menu.d/luci-app-zapret.json | 36 ++ .../usr/share/rpcd/acl.d/luci-app-zapret.json | 27 ++ zapret/Makefile | 3 +- zapret/sync_config.sh | 67 ++++ 8 files changed, 999 insertions(+), 1 deletion(-) create mode 100644 luci-app-zapret/htdocs/luci-static/resources/view/zapret/service.js create mode 100644 luci-app-zapret/htdocs/luci-static/resources/view/zapret/settings.js create mode 100644 luci-app-zapret/htdocs/luci-static/resources/view/zapret/tools.js create mode 100644 luci-app-zapret/root/usr/share/luci/menu.d/luci-app-zapret.json create mode 100644 luci-app-zapret/root/usr/share/rpcd/acl.d/luci-app-zapret.json create mode 100755 zapret/sync_config.sh 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 +