mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 17:32:51 +08:00
🤞 Sync 2026-09-27 22:16:29
This commit is contained in:
1 parent
8ede4a8320
commit
618f51e9dc
459 files changed
+44817
-2191
No files matched your search
@@ -0,0 +1 @@
|
||||
release
|
||||
@@ -0,0 +1 @@
|
||||
26.268.33051~dd3d1cb
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
PKG_NAME="$1"
|
||||
CURDIR="$2"
|
||||
PKG_BUILD_DIR="$3"
|
||||
|
||||
if [ -d "$CURDIR/.git" ]; then
|
||||
config="$CURDIR/.git/config"
|
||||
else
|
||||
config="$(sed "s|^gitdir:\s*|$CURDIR/|;s|$|/config|" "$CURDIR/.git")"
|
||||
fi
|
||||
if grep -q -E "url.*gitcode\.(com|net)" "$config"; then
|
||||
for d in htdocs patches po root; do
|
||||
rm -rf "$PKG_BUILD_DIR"/$d
|
||||
done
|
||||
mkdir -p "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view
|
||||
touch "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view/$PKG_NAME.js
|
||||
mkdir -p "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d
|
||||
touch "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d/$PKG_NAME.json
|
||||
fi
|
||||
exit 0
|
||||
@@ -0,0 +1,2 @@
|
||||
c3d5b719f5337ceefa8b5b1f6c1d7b96965a3812
|
||||
e192c5983e50169ed984d48b094798869a0a780b
|
||||
@@ -1,4 +1,4 @@
|
||||
Apache License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
|
||||
Executable → Regular
+25
-6
@@ -1,14 +1,33 @@
|
||||
# Copyright 2024- Douglas Orend <doug.orend2@gmail.com>
|
||||
# This is free software, licensed under the Apache License, Version 2.0
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
LUCI_NAME:=luci-app-adguardhome
|
||||
LUCI_MAINTAINER:=Jackie264 <OneNAS-space>
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
PKG_CPE_ID:=cpe:/a:Jackie264:luci-app-adguardhome
|
||||
|
||||
LUCI_TITLE:=LuCI support for AdguardHome
|
||||
LUCI_DEPENDS:=+adguardhome +luci-base +curl
|
||||
LUCI_TITLE:=LuCI support for AdGuard Home
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
LUCI_PKGARCH:=all
|
||||
USERID:=adguardhome=853:adguardhome=853
|
||||
|
||||
PKG_MAINTAINER:=Douglas Orend <doug.orend2@gmail.com>
|
||||
PKG_UNPACK:=$(CURDIR)/.prepare.sh $(PKG_NAME) $(CURDIR) $(PKG_BUILD_DIR)
|
||||
|
||||
define Package/luci-app-adguardhome/conffiles
|
||||
/etc/adguardhome/adguardhome.yaml
|
||||
/etc/config/adguardhome
|
||||
endef
|
||||
|
||||
define Package/luci-app-adguardhome/prerm
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
rm -f /tmp/luci-indexcache.*
|
||||
rm -rf /tmp/luci-modulecache/
|
||||
/etc/init.d/rpcd reload 2>/dev/null
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
@@ -1,13 +1,48 @@
|
||||
# LuCI App AdGuardHome
|
||||
|
||||
**NOTE:** Original Source found for LUCI app [here](https://github.com/openwrt/luci/tree/3253c2644215a1ea8136199044e06ad5e4fd9de9/applications/luci-app-adguardhome) by Duncan Hill (<openwrt-dev@cricalix.net>).
|
||||
|
||||
## Screenshots
|
||||
|
||||
### Configuration Tab
|
||||

|
||||
|
||||
### Status Tab
|
||||

|
||||
## luci-app-adguardhome
|
||||
#### (Modified version based on the official source code)
|
||||
|
||||
|
||||
### Overview of changes
|
||||
- Migrate from lua to luci standard js
|
||||
- Add a global switch
|
||||
- Add the UI interface to create/modify AGH password function
|
||||
- Add UI interface to modify AGH management binding ip:port function
|
||||
- You can choose AGH as dnsmasq upstream, redirect 53 port, switch dnsmasq 53 port or do nothing
|
||||
- Update the kernel to choose the Latest/Beta version, download from AGH mirror station/AGH github station
|
||||
- When updating the kernel, you can choose the Latest/Beta version, and to download from AGH mirror station/AGH github station.
|
||||
- Dynamically obtain ***WAN*** interface passed to nft rules
|
||||
|
||||
### Default behaviors
|
||||
> [!TIP]
|
||||
> Use the following rules to exclude inbound traffic from **WAN** to avoid exposing the router to !!️ **Public Parser**!!️
|
||||
>
|
||||
> This function is only effective in "Redirect" mode.
|
||||
> ```
|
||||
> iifname { __WAN_EXCLUDES__ } return
|
||||
> ```
|
||||
>
|
||||
> The rest of the `53` port traffic that matches the `target is local` will be redirected to the listening port of **AdGuard Home**
|
||||
> ```
|
||||
> fib daddr type local udp dport 53 redirect to :__AGH_PORT__
|
||||
> fib daddr type local tcp dport 53 redirect to :__AGH_PORT__
|
||||
> ```
|
||||
|
||||
> [!CAUTION]
|
||||
> *Only suitable for the OpenWRT official version, and questions generated by third parties will not be answered*
|
||||
>
|
||||
> *Only `nftables` is supported, the one is still using `iptables` may have problems*
|
||||
>
|
||||
> *The device name can be viewed through `ip a` / `ifconfig`*
|
||||
|
||||
### Attribution
|
||||
#### Reference
|
||||
- [OpenWRT official luci](https://github.com/openwrt/luci/tree/master/applications/luci-app-adguardhome)
|
||||
- [OpenWRT official packages](https://github.com/openwrt/packages/tree/master/net/adguardhome)
|
||||
|
||||
#### Acknowledgment
|
||||
Inspired by
|
||||
- [@w9315273 nft version](https://github.com/w9315273/luci-app-adguardhome)
|
||||
- [@rufengsuixing](https://github.com/rufengsuixing/luci-app-adguardhome)
|
||||
|
||||
#### Notice
|
||||
If the author of the original code has any objections, please contact me to deal with it.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 5.1.x | :white_check_mark: |
|
||||
| 5.0.x | :x: |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
|
||||
Tell them where to go, how often they can expect to get an update on a
|
||||
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||
declined, etc.
|
||||
@@ -1,16 +1,82 @@
|
||||
'use strict';
|
||||
'require rpc';
|
||||
|
||||
'require dom';
|
||||
'require form';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require view';
|
||||
|
||||
const DEFAULT_CONFIG_FILE = '/etc/adguardhome/adguardhome.yaml';
|
||||
const DEFAULT_WORK_DIR = '/var/lib/adguardhome';
|
||||
const DEFAULT_USER = 'adguardhome';
|
||||
const DEFAULT_GROUP = DEFAULT_USER;
|
||||
|
||||
const DEFAULT_GOGC = '0';
|
||||
const DEFAULT_GOMAXPROCS = '0';
|
||||
const DEFAULT_GOMEMLIMIT = '0';
|
||||
|
||||
const PATH_REGEX = new RegExp('^/etc(/[^/]+)?/?$');
|
||||
|
||||
const POLL_INTERVAL = 5;
|
||||
|
||||
const RUNNING_SPAN = `<span style="color: green; font-weight: bold">${_('Running')}</span>`;
|
||||
const NOT_RUNNING_SPAN = `<span style="color: red; font-weight: bold">${_('Not running')}</span>`;
|
||||
|
||||
const STORAGE_KEY = 'luci-app-adguardhome';
|
||||
const STORAGE_KEY_CORE = 'luci-app-adguardhome_core_update';
|
||||
|
||||
function getServiceInfo(name) {
|
||||
const fn = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
params: ['name'],
|
||||
expect: { [name]: { instances: { [name]: {} }}},
|
||||
});
|
||||
return () => fn(name);
|
||||
}
|
||||
|
||||
const getAGHServiceInfo = getServiceInfo('adguardhome');
|
||||
|
||||
async function getStatus() {
|
||||
try {
|
||||
const res = await getAGHServiceInfo();
|
||||
const isRunning = res?.instances?.adguardhome?.running;
|
||||
return isRunning ?? false;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getStatusValue(isRunning) {
|
||||
return isRunning ? RUNNING_SPAN : NOT_RUNNING_SPAN;
|
||||
}
|
||||
|
||||
async function getVersion() {
|
||||
try {
|
||||
const res = await fs.exec('/usr/bin/AdGuardHome', ['--version']);
|
||||
const version = res.stdout
|
||||
? (res.stdout.match(/version\s+(.*)/) || [null, res.stdout.trim()])[1]
|
||||
: '';
|
||||
return version;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(node) {
|
||||
const output = node?.querySelector('output');
|
||||
return output
|
||||
? async () => {
|
||||
const isRunning = await getStatus();
|
||||
dom.content(output, getStatusValue(isRunning));
|
||||
}
|
||||
: () => {};
|
||||
}
|
||||
|
||||
function validateConfigFile(_unused, value) {
|
||||
if (value == null || value === '') {
|
||||
return true;
|
||||
@@ -37,122 +103,518 @@ function validateWorkDir(_unused, value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderStatus(status, agh_config) {
|
||||
var spanTemp = '<span style="color:%s"><strong>%s %s</strong></span>';
|
||||
var renderHTML;
|
||||
if (status.running) {
|
||||
var button = String.format(' <a class="btn cbi-button" href="%s" target="_blank" rel="noreferrer noopener">%s</a>',
|
||||
agh_config.web_url, _('Open Web Interface'));
|
||||
renderHTML = spanTemp.format('green', _('adguardhome'), _('RUNNING')) + button;
|
||||
} else {
|
||||
renderHTML = spanTemp.format('red', _('adguardhome'), _('NOT RUNNING'));
|
||||
}
|
||||
|
||||
return renderHTML;
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load_adguardhome_config: rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'get_config'
|
||||
}),
|
||||
load_adguardhome_status: rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'get_status'
|
||||
}),
|
||||
load: function () {
|
||||
load() {
|
||||
return Promise.all([
|
||||
this.load_adguardhome_status(),
|
||||
this.load_adguardhome_config()
|
||||
getStatus(),
|
||||
getVersion(),
|
||||
uci.load('adguardhome').then(() => {
|
||||
const sections = uci.sections('adguardhome', 'adguardhome');
|
||||
const sec = sections.length > 0 ? sections[0] : {};
|
||||
const configFile = sec.config_file || DEFAULT_CONFIG_FILE;
|
||||
return fs.read(configFile).catch(() => null);
|
||||
})
|
||||
]);
|
||||
},
|
||||
render: function (data) {
|
||||
var status = data[0] || {};
|
||||
var agh_config = data[1] || {};
|
||||
// A basic configuration form; the luci.adguardhome script that
|
||||
// powers the other UI pages needs a username and password to
|
||||
// communicate with the AdguardHome REST API.
|
||||
var s, o;
|
||||
var m = new form.Map('adguardhome', _('AdGuard Home Configuration'),
|
||||
_('默认用户名admin 密码admin')
|
||||
|
||||
async render([isRunning, version, yamlContent]) {
|
||||
// If there is a version number, it means that the kernel exists. If there is no version number, it does not exist.
|
||||
const coreExists = Boolean(version);
|
||||
let dnsPort = '53';
|
||||
if (yamlContent) {
|
||||
const portMatches = yamlContent.match(/port:\s*(\d+)/g);
|
||||
if (portMatches && portMatches.length >= 2) {
|
||||
const actualPort = portMatches[1].match(/\d+/);
|
||||
if (actualPort) {
|
||||
dnsPort = actualPort[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 💡 Dynamic security value: UI rendering requires httpport to generate jump links
|
||||
const sections = uci.sections('adguardhome', 'adguardhome');
|
||||
const savedHttpPort = (sections.length > 0 && sections[0].httpport) ? sections[0].httpport : '3008';
|
||||
|
||||
const map = new form.Map('adguardhome', _('AdGuard Home'));
|
||||
|
||||
const statusSect = map.section(form.TypedSection, 'status');
|
||||
statusSect.anonymous = true;
|
||||
statusSect.cfgsections = () => ['status_section'];
|
||||
|
||||
const versionOpt = statusSect.option(form.DummyValue, '_version', _('Version'));
|
||||
versionOpt.cfgvalue = () => version || `<span style="color: var(--error-color-high); font-weight: bold;">${_('Not installed')}</span>`;
|
||||
versionOpt.rawhtml = true;
|
||||
|
||||
const statusOpt = statusSect.option(form.DummyValue, '_status', _('Service Status'));
|
||||
statusOpt.rawhtml = true;
|
||||
statusOpt.cfgvalue = () => getStatusValue(isRunning);
|
||||
|
||||
const mainSect = map.section(form.TypedSection, 'adguardhome');
|
||||
mainSect.anonymous = true;
|
||||
|
||||
mainSect.tab('general', _('General Settings'));
|
||||
mainSect.tab(
|
||||
'jail',
|
||||
_('File System Access'),
|
||||
_('Files and directories that AdGuard Home should have read-only or read-write access to.'),
|
||||
);
|
||||
mainSect.tab('dns_redirect', _('Services Settings'));
|
||||
mainSect.tab(
|
||||
'core_update',
|
||||
_('Core Update'),
|
||||
_('Settings and operations for updating the AdGuardHome core binary.')
|
||||
);
|
||||
mainSect.tab(
|
||||
'advanced',
|
||||
_('Advanced Settings'),
|
||||
_('Go environment variables that tune garbage collector and memory management.') +
|
||||
' ' + _('Modify at your own risk.'),
|
||||
);
|
||||
|
||||
s = m.section(form.TypedSection);
|
||||
s.anonymous = true;
|
||||
|
||||
s.render = function () {
|
||||
setTimeout(function() {
|
||||
poll.add(function () {
|
||||
var view = document.getElementById('service_status');
|
||||
view.innerHTML = renderStatus(status, agh_config);
|
||||
});
|
||||
}, 0);
|
||||
|
||||
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
|
||||
E('p', { id: 'service_status' }, _('Collecting data...'))
|
||||
]);
|
||||
// ==== Move here: global switch ====
|
||||
const enabledOpt = mainSect.taboption(
|
||||
'general',
|
||||
form.Flag,
|
||||
'enabled',
|
||||
_('Enable')
|
||||
);
|
||||
enabledOpt.default = '0';
|
||||
enabledOpt.rmempty = false;
|
||||
// If the kernel does not exist, additional warning prompts will guide users to turn on the service to download automatically.
|
||||
if (!coreExists) {
|
||||
enabledOpt.description = `<span style="color: var(--error-color-high); font-weight: bold;">${_('Core binary not found. Enable the service to trigger an automatic download.')}</span>`;
|
||||
}
|
||||
|
||||
s = m.section(form.NamedSection, 'config', 'adguardhome', _('General settings'));
|
||||
o = s.option(form.Flag, 'enabled', _('Enable'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'config_file', _('Configuration file'));
|
||||
o.datatype = 'string';
|
||||
o.placeholder = DEFAULT_CONFIG_FILE;
|
||||
o.validate = validateConfigFile;
|
||||
const configFileOpt = mainSect.taboption(
|
||||
'general',
|
||||
form.Value,
|
||||
'config_file',
|
||||
_('Configuration file'),
|
||||
_('Configuration file must be stored in its own directory, and not in \'/etc\'.') +
|
||||
'<br />' + _('Parent directory will be owned by the service user.') +
|
||||
'<br />' + _('If empty, defaults to') + ` '${DEFAULT_CONFIG_FILE}'.`,
|
||||
);
|
||||
configFileOpt.placeholder = DEFAULT_CONFIG_FILE;
|
||||
configFileOpt.validate = validateConfigFile;
|
||||
|
||||
o = s.option(form.Value, 'work_dir', _('Work Dir'));
|
||||
o.datatype = 'string';
|
||||
o.placeholder = DEFAULT_WORK_DIR;
|
||||
o.validate = validateWorkDir;
|
||||
const workDirOpt = mainSect.taboption(
|
||||
'general',
|
||||
form.Value,
|
||||
'work_dir',
|
||||
_('Working directory'),
|
||||
_('Directory where filters, logs, and statistics are stored.') +
|
||||
'<br />' + _('Will be owned by the service user.') +
|
||||
'<br />' + _('If empty, defaults to') + ` '${DEFAULT_WORK_DIR}'.`,
|
||||
);
|
||||
workDirOpt.placeholder = DEFAULT_WORK_DIR;
|
||||
workDirOpt.validate = validateWorkDir;
|
||||
|
||||
o = s.option(form.Value, 'user', _('Service user'), _('User the service runs under.'));
|
||||
o.datatype = 'string';
|
||||
o.placeholder = DEFAULT_USER;
|
||||
const userOpt = mainSect.taboption(
|
||||
'general',
|
||||
form.Value,
|
||||
'user',
|
||||
_('Service user'),
|
||||
_('User the service runs under.') + ' ' + _('If empty, defaults to') +
|
||||
` '${DEFAULT_USER}'.`,
|
||||
);
|
||||
userOpt.placeholder = DEFAULT_USER;
|
||||
|
||||
o = s.option(form.Value, 'group', _('Service group'), _('Group the service runs under.'));
|
||||
o.datatype = 'string';
|
||||
o.placeholder = DEFAULT_GROUP;
|
||||
const groupOpt = mainSect.taboption(
|
||||
'general',
|
||||
form.Value,
|
||||
'group',
|
||||
_('Service group'),
|
||||
_('Group the service runs under.') + ' ' + _('If empty, defaults to') +
|
||||
` '${DEFAULT_GROUP}'.`,
|
||||
|
||||
// 添加重定向模式选择
|
||||
o = s.option(form.ListValue, 'redirect', _('Redirect'), _('AdGuardHome redirect mode'));
|
||||
o.value('none', _('none'));
|
||||
o.value('dnsmasq-upstream', _('Run as dnsmasq upstream server'));
|
||||
o.value('redirect', _('Redirect 53 port to AdGuardHome'));
|
||||
o.value('exchange', _('Use port 53 replace dnsmasq'));
|
||||
o.default = 'none';
|
||||
);
|
||||
groupOpt.placeholder = DEFAULT_GROUP;
|
||||
|
||||
o = s.option(form.Value, 'web_username', _('Username for AdGuard Home'), _('The username you configured when you set up AdGuard Home'));
|
||||
o.default = 'admin';
|
||||
const verboseOpt = mainSect.taboption(
|
||||
'general',
|
||||
form.Flag,
|
||||
'verbose',
|
||||
_('Verbose logging'),
|
||||
);
|
||||
verboseOpt.default = '0';
|
||||
|
||||
o = s.option(form.Value, 'web_password', _('Password for AdGuard Home'), _('The password you configured when you set up AdGuard Home'));
|
||||
o.password = true;
|
||||
o.default = 'admin';
|
||||
const advSettingsOpt = mainSect.taboption(
|
||||
'general',
|
||||
form.Flag,
|
||||
'advanced_settings',
|
||||
_('Advanced Settings'),
|
||||
);
|
||||
advSettingsOpt.default = '0';
|
||||
advSettingsOpt.rmempty = false;
|
||||
advSettingsOpt.load = () => sessionStorage.getItem(STORAGE_KEY) || '0';
|
||||
advSettingsOpt.remove = () => {};
|
||||
advSettingsOpt.write = (_, value) => sessionStorage.setItem(STORAGE_KEY, value);
|
||||
|
||||
var ChangePassword = form.Button.extend({
|
||||
set_passwd: rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'set_passwd',
|
||||
params: [ 'username', 'hash' ],
|
||||
}),
|
||||
onclick: function() {
|
||||
var username = document.getElementById("widget.cbid.adguardhome.config.web_username").value;
|
||||
var password = document.getElementById("widget.cbid.adguardhome.config.web_password").value;
|
||||
var hash = TwinBcrypt.hashSync(password);
|
||||
return Promise.all([ this.set_passwd( username, hash ) ]);
|
||||
},
|
||||
// ==== New: The switch of Core Update on General tab ====
|
||||
const coreUpdateToggleOpt = mainSect.taboption(
|
||||
'general',
|
||||
form.Flag,
|
||||
'enable_core_update',
|
||||
_('Core Update'),
|
||||
_('Show the tab and settings for updating the AdGuardHome core binary.')
|
||||
);
|
||||
coreUpdateToggleOpt.default = '0';
|
||||
coreUpdateToggleOpt.rmempty = false;
|
||||
coreUpdateToggleOpt.load = () => sessionStorage.getItem(STORAGE_KEY_CORE) || '0';
|
||||
coreUpdateToggleOpt.remove = () => {};
|
||||
coreUpdateToggleOpt.write = (_, value) => sessionStorage.setItem(STORAGE_KEY_CORE, value);
|
||||
// ==========================================
|
||||
|
||||
mainSect.taboption('jail', form.DynamicList, 'jail_mount', _('Read-only access'));
|
||||
mainSect.taboption('jail', form.DynamicList, 'jail_mount_rw', _('Read-write access'));
|
||||
|
||||
const gcOpt = mainSect.taboption(
|
||||
'advanced',
|
||||
form.Value,
|
||||
'gc',
|
||||
'GOGC',
|
||||
_('Tunes the garbage collector\'s aggressiveness by setting the percentage of heap ' +
|
||||
'growth allowed before the next collection cycle triggers.') + '<br />' +
|
||||
_('If empty, defaults to') + ' ' + _('unset and 100') + '.',
|
||||
'<a href="https://go.dev/doc/gc-guide#GOGC" target="_blank">https://go.dev/doc/gc-guide#GOGC</a>'
|
||||
);
|
||||
gcOpt.datatype = 'uinteger';
|
||||
gcOpt.depends('advanced_settings', '1');
|
||||
gcOpt.placeholder = DEFAULT_GOGC;
|
||||
gcOpt.retain = true;
|
||||
|
||||
const maxProcsOpt = mainSect.taboption(
|
||||
'advanced',
|
||||
form.Value,
|
||||
'maxprocs',
|
||||
'GOMAXPROCS',
|
||||
_('The maximum number of operating system threads that can execute user-level Go code' +
|
||||
' simultaneously.') + '<br />' +
|
||||
_('If empty, defaults to') + ' ' + _('unset and matching the number of CPUs') + '.',
|
||||
);
|
||||
maxProcsOpt.datatype = 'uinteger';
|
||||
maxProcsOpt.depends('advanced_settings', '1');
|
||||
maxProcsOpt.placeholder = DEFAULT_GOMAXPROCS;
|
||||
maxProcsOpt.retain = true;
|
||||
|
||||
const memLimitOpt = mainSect.taboption(
|
||||
'advanced',
|
||||
form.Value,
|
||||
'memlimit',
|
||||
'GOMEMLIMIT',
|
||||
_('A soft memory cap for the Go runtime, allowing the garbage collector to run more ' +
|
||||
'frequently as usage approaches the limit to prevent Out-of-Memory (OOM) kills.') +
|
||||
'<br />' +
|
||||
_('If empty, defaults to') + ' ' + _('unset') + '.',
|
||||
);
|
||||
memLimitOpt.datatype = 'uinteger';
|
||||
memLimitOpt.depends('advanced_settings', '1');
|
||||
memLimitOpt.placeholder = DEFAULT_GOMEMLIMIT;
|
||||
memLimitOpt.retain = true;
|
||||
|
||||
// 💡 1. Extract the real listening address and port directly from YAML
|
||||
let realHttpAddress = '0.0.0.0:3008';
|
||||
if (yamlContent) {
|
||||
// Match the address: field below http: in YAML
|
||||
const addrMatch = yamlContent.match(/address:\s*([^\s]+)/);
|
||||
if (addrMatch && addrMatch[1]) {
|
||||
realHttpAddress = addrMatch[1];
|
||||
}
|
||||
}
|
||||
|
||||
// 💡 2. Analyze the real IP and port used by the jump button
|
||||
let linkIp = window.location.hostname;
|
||||
let linkPort = '3008';
|
||||
const addrParts = realHttpAddress.split(':');
|
||||
if (addrParts.length >= 2) {
|
||||
linkPort = addrParts.pop();
|
||||
let ipPart = addrParts.join(':').replace(/\[|\]/g, '');
|
||||
// If the binding is a full zero address, the IP accessed by the current router will be returned.
|
||||
if (ipPart !== '0.0.0.0' && ipPart !== '') {
|
||||
linkIp = ipPart;
|
||||
}
|
||||
}
|
||||
|
||||
// 💡 3. Upgrade the original httpport to http_address
|
||||
const isServiceEnabled = sections.length > 0 && sections[0].enabled === '1';
|
||||
const disabledHint = _('Service is disabled. Please go to "General Settings" to enable it.');
|
||||
|
||||
// 💡 4. WebUI button HTML to construct intelligent state
|
||||
const webuiBtnHtml = isServiceEnabled
|
||||
? `<a class="btn cbi-button cbi-button-link" style="font-weight:bold; display:inline-block; margin-top:5px;" href="http://${linkIp}:${linkPort}" target="_blank">${_('Open AdGuardHome WebUI')}</a>`
|
||||
: `<span title='${disabledHint}' style="display:inline-block; margin-top:5px; cursor:not-allowed;">
|
||||
<a class="btn cbi-button cbi-button-link" style="font-weight:bold; pointer-events:none; opacity:0.5; margin-top:0;" href="javascript:void(0);">${_('Open AdGuardHome WebUI')}</a>
|
||||
</span>`;
|
||||
|
||||
// 💡 5. Rendering components
|
||||
const httpAddressOpt = mainSect.taboption(
|
||||
'dns_redirect',
|
||||
form.Value,
|
||||
'http_address',
|
||||
_('WebUI listener (address:port)'),
|
||||
_('Bind to specific interface:port (e.g., 0.0.0.0:3008). Leave as 0.0.0.0 to listen on all interfaces.') +
|
||||
`<br />${webuiBtnHtml}`
|
||||
);
|
||||
httpAddressOpt.placeholder = '0.0.0.0:3008';
|
||||
httpAddressOpt.default = '0.0.0.0:3008';
|
||||
httpAddressOpt.datatype = 'hostport';
|
||||
httpAddressOpt.rmempty = false;
|
||||
|
||||
// 🎯 Overwrite the default reading UCI behavior and forcibly echo the real value in YAML
|
||||
httpAddressOpt.cfgvalue = function(section_id) {
|
||||
return realHttpAddress;
|
||||
};
|
||||
|
||||
// ==== Add the password modification function below the WebUI port ====
|
||||
const isPasswordEmpty = yamlContent ? /password:[ \t]*(\r?\n|$)/.test(yamlContent) : false;
|
||||
const hashPassOpt = mainSect.taboption(
|
||||
'dns_redirect',
|
||||
form.Value,
|
||||
'hashpass',
|
||||
_('Change password'),
|
||||
_('Enter the password here. Click the Load calculate Module button below and go on.') +
|
||||
'<br /><button class="btn cbi-button cbi-button-apply" type="button" id="btn-agh-calc-hash" style="display:inline-block; margin-top:5px;">' +
|
||||
_(' ... Load calculate Module ... ') +
|
||||
'</button>'
|
||||
);
|
||||
hashPassOpt.default = '';
|
||||
hashPassOpt.datatype = 'string';
|
||||
hashPassOpt.password = true;
|
||||
hashPassOpt.rmempty = true;
|
||||
hashPassOpt.placeholder = isPasswordEmpty ? _('Please create a new password.') : '';
|
||||
hashPassOpt.cfgvalue = function(section_id) {
|
||||
return '';
|
||||
};
|
||||
|
||||
const redirectOpt = mainSect.taboption(
|
||||
'dns_redirect',
|
||||
form.ListValue,
|
||||
'redirect',
|
||||
`${dnsPort} ` + _('Redirect'),
|
||||
_('AdGuardHome redirect mode')
|
||||
);
|
||||
redirectOpt.value('none', _('No redirect'));
|
||||
redirectOpt.value('dnsmasq-upstream', _('As the upstream server of dnsmasq'));
|
||||
redirectOpt.value('redirect', _('Redirect port 53 to AdGuardHome'));
|
||||
redirectOpt.value('exchange', _('Use port 53 to replace dnsmasq'));
|
||||
redirectOpt.default = 'none';
|
||||
redirectOpt.rmempty = false;
|
||||
// ==========================================
|
||||
|
||||
// ======== Core Update Control content ========
|
||||
const coreVersionOpt = mainSect.taboption(
|
||||
'core_update',
|
||||
form.ListValue,
|
||||
'core_version',
|
||||
_('Core Branch'),
|
||||
_('Select the branch for the core binary update.')
|
||||
);
|
||||
coreVersionOpt.value('latest', _('Latest Version'));
|
||||
coreVersionOpt.value('beta', _('Beta Version'));
|
||||
coreVersionOpt.default = 'latest';
|
||||
coreVersionOpt.depends('enable_core_update', '1');
|
||||
coreVersionOpt.retain = true;
|
||||
|
||||
const coreUrlOpt = mainSect.taboption(
|
||||
'core_update',
|
||||
form.ListValue,
|
||||
'update_url',
|
||||
_('Update URL'),
|
||||
_('Select the download link for the core update.')
|
||||
);
|
||||
coreUrlOpt.value('https://static.adtidy.org/adguardhome/release/AdGuardHome_linux_${Arch}.tar.gz', _('Official Mirror (AdTidy - Recommended)'));
|
||||
coreUrlOpt.value('https://github.com/AdguardTeam/AdGuardHome/releases/download/${Cloud_Version}/AdGuardHome_linux_${Arch}.tar.gz', _('GitHub Releases (Original)'));
|
||||
coreUrlOpt.default = 'https://static.adtidy.org/adguardhome/release/AdGuardHome_linux_${Arch}.tar.gz';
|
||||
coreUrlOpt.rmempty = false;
|
||||
coreUrlOpt.depends('enable_core_update', '1');
|
||||
coreUrlOpt.retain = true;
|
||||
|
||||
const updateActionOpt = mainSect.taboption(
|
||||
'core_update',
|
||||
form.DummyValue,
|
||||
'_update_action',
|
||||
_('Action')
|
||||
);
|
||||
updateActionOpt.rawhtml = true;
|
||||
updateActionOpt.cfgvalue = () => `
|
||||
<div id="agh-update-controls" style="display: flex; gap: 10px; margin-bottom: 10px;">
|
||||
<button class="btn cbi-button cbi-button-apply" type="button" id="btn-agh-update">${_('Update core version')}</button>
|
||||
<button class="btn cbi-button cbi-button-apply" type="button" id="btn-agh-force" style="display: none;">${_('Force update')}</button>
|
||||
</div>
|
||||
<div id="agh-update-log-container" style="display: none;">
|
||||
<textarea id="agh-update-log" class="cbi-input-textarea" style="width: 100%; display: block; font-family: monospace;" rows="10" readonly="readonly"></textarea>
|
||||
</div>
|
||||
`;
|
||||
updateActionOpt.depends('enable_core_update', '1');
|
||||
// ==========================================
|
||||
|
||||
const rendered = await map.render();
|
||||
|
||||
const statusNode = map.findElement('data-field', statusOpt.cbid('status_section'));
|
||||
poll.add(updateStatus(statusNode), POLL_INTERVAL);
|
||||
|
||||
// ========== Update the status and polling logic =========
|
||||
let updatePollId = null;
|
||||
|
||||
function startLogPolling() {
|
||||
if (updatePollId) clearInterval(updatePollId);
|
||||
const pollAction = () => {
|
||||
const btnU = document.getElementById('btn-agh-update');
|
||||
const btnF = document.getElementById('btn-agh-force');
|
||||
const logC = document.getElementById('agh-update-log-container');
|
||||
const logT = document.getElementById('agh-update-log');
|
||||
|
||||
if (btnU) btnU.disabled = true;
|
||||
if (btnF) btnF.style.display = 'inline-block';
|
||||
if (logC) logC.style.display = 'block';
|
||||
|
||||
fs.read('/tmp/AdGuardHome_update.log').then((txt) => {
|
||||
if (txt && logT) {
|
||||
logT.value = txt;
|
||||
logT.scrollTop = logT.scrollHeight;
|
||||
}
|
||||
}).catch(() => {});
|
||||
|
||||
Promise.all([
|
||||
fs.stat('/var/run/update_core').catch(() => null),
|
||||
fs.stat('/var/run/update_core_done').catch(() => null),
|
||||
fs.stat('/var/run/update_core_error').catch(() => null)
|
||||
]).then(([isCore, isDone, isError]) => {
|
||||
if (isDone) {
|
||||
clearInterval(updatePollId);
|
||||
fs.remove('/var/run/update_core_done').catch(() => {});
|
||||
if (btnU) {
|
||||
btnU.disabled = false;
|
||||
btnU.textContent = _('Updated');
|
||||
}
|
||||
} else if (isError) {
|
||||
clearInterval(updatePollId);
|
||||
if (btnU) {
|
||||
btnU.disabled = false;
|
||||
btnU.textContent = _('Failed');
|
||||
}
|
||||
} else if (!isCore && !isDone && !isError) {
|
||||
clearInterval(updatePollId);
|
||||
if (btnU) {
|
||||
btnU.disabled = false;
|
||||
btnU.textContent = _('Already up-to-date');
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
pollAction();
|
||||
updatePollId = setInterval(pollAction, 1500);
|
||||
}
|
||||
|
||||
function applyUpdate(isForce) {
|
||||
const btnU = document.getElementById('btn-agh-update');
|
||||
const logC = document.getElementById('agh-update-log-container');
|
||||
const logT = document.getElementById('agh-update-log');
|
||||
if (btnU) {
|
||||
btnU.textContent = _('Checking...');
|
||||
btnU.disabled = true;
|
||||
}
|
||||
|
||||
if (logC) logC.style.display = 'block';
|
||||
if (logT) logT.value = _('Checking and preparing...\n');
|
||||
|
||||
map.save().then(() => {
|
||||
const arg = isForce ? 'force' : '';
|
||||
fs.exec('/usr/share/AdGuardHome/update_core.sh', [arg]).catch((err) => {
|
||||
console.error('Failed to trigger update script:', err);
|
||||
});
|
||||
startLogPolling();
|
||||
}).catch((err) => {
|
||||
console.error('Config save failed:', err);
|
||||
if (btnU) {
|
||||
btnU.textContent = _('Save Failed');
|
||||
btnU.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
rendered.addEventListener('click', (e) => {
|
||||
if (e.target && e.target.id === 'btn-agh-update') {
|
||||
e.preventDefault();
|
||||
applyUpdate(false);
|
||||
} else if (e.target && e.target.id === 'btn-agh-force') {
|
||||
e.preventDefault();
|
||||
applyUpdate(true);
|
||||
}
|
||||
// ==== Handle front-end hash encryption logic ====
|
||||
else if (e.target && e.target.id === 'btn-agh-calc-hash') {
|
||||
e.preventDefault();
|
||||
const btn = e.target;
|
||||
|
||||
// Dynamically find the password input box on the page (matching elements with the suffix .hashpass)
|
||||
const inputs = rendered.querySelectorAll('input[type="text"], input[type="password"]');
|
||||
let passInput = null;
|
||||
for (const el of inputs) {
|
||||
if (el.id && el.id.endsWith('.hashpass')) {
|
||||
passInput = el;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!passInput) return;
|
||||
|
||||
// 1. If JS has not been loaded, it will be loaded dynamically.
|
||||
if (typeof window.TwinBcrypt === 'undefined') {
|
||||
btn.disabled = true;
|
||||
btn.textContent = _('Loading...');
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = L.resource('view/adguardhome/twin-bcrypt.min.js');
|
||||
script.type = 'text/javascript';
|
||||
|
||||
script.onload = () => {
|
||||
btn.textContent = _('Click here to Calculate');
|
||||
btn.disabled = false;
|
||||
};
|
||||
script.onerror = () => {
|
||||
btn.textContent = _(' ... Load Error ... ');
|
||||
btn.disabled = false;
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
// 2. If JS has been loaded, the calculation will be executed.
|
||||
else {
|
||||
if (passInput.value) {
|
||||
// Prevent repeated hashing of hashed strings (starting with $2a$ or $2y$)
|
||||
if (passInput.value.startsWith('$2a$') || passInput.value.startsWith('$2y$')) {
|
||||
btn.textContent = _('Calculation already DONE !');
|
||||
return;
|
||||
}
|
||||
|
||||
const hash = window.TwinBcrypt.hashSync(passInput.value);
|
||||
passInput.value = hash;
|
||||
|
||||
// Manually trigger native input and change events
|
||||
passInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
passInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
|
||||
btn.textContent = _(' ... Click Save/Apply ↘️ ... ');
|
||||
} else {
|
||||
btn.textContent = _(' ... Nothing inputted yet ... ');
|
||||
}
|
||||
}
|
||||
}
|
||||
// ==========================================
|
||||
});
|
||||
o = s.option(ChangePassword, 'change_password', _('Change Password'));
|
||||
|
||||
var BCryptInclude = form.DummyValue.extend({
|
||||
renderWidget: function(section_id, option_index, cfgvalue) {
|
||||
return E('script', { 'type':'text/javascript', 'src':'/luci-static/resources/view/twin-bcrypt.min.js' });
|
||||
Promise.all([
|
||||
fs.stat('/var/run/update_core').catch(() => null),
|
||||
fs.stat('/var/run/update_core_error').catch(() => null)
|
||||
]).then(([isCore, isError]) => {
|
||||
if (isCore || isError) {
|
||||
const btnU = document.getElementById('btn-agh-update');
|
||||
if (btnU) btnU.textContent = _('Checking...');
|
||||
startLogPolling();
|
||||
}
|
||||
});
|
||||
o = s.option(BCryptInclude, 'misc');
|
||||
// ==========================================
|
||||
|
||||
return m.render();
|
||||
return rendered;
|
||||
},
|
||||
})
|
||||
});
|
||||
@@ -1,44 +0,0 @@
|
||||
'use strict';
|
||||
'require dom';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
L.resolveDefault(fs.stat('/sbin/logread'), null),
|
||||
L.resolveDefault(fs.stat('/usr/sbin/logread'), null)
|
||||
]).then(function(stat) {
|
||||
var logger = stat[0] ? stat[0].path : stat[1] ? stat[1].path : null;
|
||||
|
||||
return fs.exec_direct(logger, [ '-e', 'AdGuardHome' ]).catch(function(err) {
|
||||
ui.addNotification(null, E('p', {}, _('Unable to load log data: ' + err.message)));
|
||||
return '';
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
render: function(logdata) {
|
||||
var loglines = logdata.trim().split(/\n/).reverse().slice(0, 50);
|
||||
|
||||
return E([], [
|
||||
E('h2', {}, [_('System Log (AdGuard Home)')]),
|
||||
E('div', {}, [_('Showing last 50 lines')]),
|
||||
E('div', { 'id': 'content_syslog' }, [
|
||||
E('textarea', {
|
||||
'id': 'syslog',
|
||||
'style': 'font-size:12px',
|
||||
'readonly': 'readonly',
|
||||
'wrap': 'off',
|
||||
'rows': loglines.length + 1
|
||||
}, [ loglines.join('\n') ])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -1,171 +0,0 @@
|
||||
'use strict';
|
||||
'require rpc';
|
||||
'require view';
|
||||
|
||||
return view.extend({
|
||||
load_adguardhome_config: rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'get_config'
|
||||
}),
|
||||
load_adguardhome_status: rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'get_status'
|
||||
}),
|
||||
load_adguardhome_statistics: rpc.declare({
|
||||
object: 'luci.adguardhome',
|
||||
method: 'get_statistics'
|
||||
}),
|
||||
|
||||
generic_failure: function(message) {
|
||||
return E('div', {'class': 'error'}, [_('RPC call failure: '), message])
|
||||
},
|
||||
render_list: function ( agh_list ) {
|
||||
var table = E('ul', {});
|
||||
for (var i = 0; i < agh_list.length; i++) {
|
||||
table.appendChild(E('li', {}, agh_list[i]));
|
||||
}
|
||||
return table;
|
||||
},
|
||||
render_status_table: function (status, agh_config) {
|
||||
if (status.error) {
|
||||
return this.generic_failure(status.error)
|
||||
}
|
||||
// Take a hint from the base LuCI module for the Overview page,
|
||||
// declare the fields and use a loop to build the tabular status view.
|
||||
// Written out as key/value pairs, but it's just an iterable of elements.
|
||||
const weburl = agh_config.web_url;
|
||||
const listen_addresses = L.isObject(status.dns_addresses) ? this.render_list(status.dns_addresses) : _('Not found');
|
||||
const bootstrap_dns = L.isObject(agh_config.dns_bootstrap_dns) ? this.render_list(agh_config.dns_bootstrap_dns) : _('Not found');
|
||||
const upstream_dns = L.isObject(agh_config.dns_upstream_dns) ? this.render_list(agh_config.dns_upstream_dns) : _('Not found');
|
||||
const fields = [
|
||||
_('Running'), status.running ? _('Yes') : _('No'),
|
||||
_('Protection enabled'), status.protection_enabled ? _('Yes') : _('No'),
|
||||
_('Statistics period (days)'), agh_config.statistics_interval,
|
||||
_('Web interface'), E('button', { 'class': "cbi-button cbi-button-button" }, E('a', { 'href': weburl, 'target': '_blank' }, weburl)),
|
||||
_('DNS listen port'), status.dns_port,
|
||||
_('DNS listen addresses'), listen_addresses,
|
||||
_('Bootstrap DNS addresses'), bootstrap_dns,
|
||||
_('Upstream DNS addresses'), upstream_dns,
|
||||
_('Version'), status.version,
|
||||
];
|
||||
|
||||
var table = E('table', { 'class': 'table', 'id': 'status' });
|
||||
for (var i = 0; i < fields.length; i += 2) {
|
||||
table.appendChild(E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%', 'vertical-align': 'text-top' }, [fields[i]]),
|
||||
E('td', { 'class': 'td left' }, [(fields[i + 1] != null) ? fields[i + 1] : _('Not found')])
|
||||
]));
|
||||
}
|
||||
return table;
|
||||
},
|
||||
render_statistics_table: function (statistics) {
|
||||
// High level statistics
|
||||
if (statistics.error) {
|
||||
return this.generic_failure(statistics.error)
|
||||
}
|
||||
const fields = [
|
||||
_('DNS queries'), statistics.num_dns_queries,
|
||||
_('DNS blocks'), statistics.num_blocked_filtering,
|
||||
_('DNS replacements (safesearch)'), statistics.num_replaced_safesearch,
|
||||
_('DNS replacements (malware/phishing)'), statistics.num_replaced_safebrowsing,
|
||||
_('Average processing time (seconds)'), statistics.avg_processing_time,
|
||||
];
|
||||
|
||||
var table = E('table', { 'class': 'table', 'id': 'statistics' });
|
||||
for (var i = 0; i < fields.length; i += 2) {
|
||||
table.appendChild(
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, [fields[i]]),
|
||||
E('td', { 'class': 'td left' }, [(fields[i + 1] != null) ? fields[i + 1] : _('Not found')])
|
||||
]));
|
||||
}
|
||||
return table;
|
||||
},
|
||||
render_top_table: function(table_id, objects) {
|
||||
var table = E('table', { 'class': 'table', 'id': table_id });
|
||||
for (var i = 0; i < objects.length; i++) {
|
||||
table.appendChild(
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, Object.keys(objects[i])[0]),
|
||||
E('td', { 'class': 'td left' }, Object.values(objects[i])[0])
|
||||
])
|
||||
);
|
||||
}
|
||||
return table;
|
||||
},
|
||||
render_top_queries_table: function (statistics) {
|
||||
// Top 5 queried domains table view
|
||||
if (statistics.error) {
|
||||
return this.generic_failure(statistics.error)
|
||||
}
|
||||
if (typeof statistics.top_queried_domains === 'undefined') { return ""; }
|
||||
const top_queries = statistics.top_queried_domains.slice(0, 5);
|
||||
return this.render_top_table('top_queries', top_queries)
|
||||
},
|
||||
render_top_blocked_table: function (statistics) {
|
||||
// Top 5 blocked domains table view
|
||||
if (statistics.error) {
|
||||
return this.generic_failure(statistics.error)
|
||||
}
|
||||
if (typeof statistics.top_blocked_domains === 'undefined') { return ""; }
|
||||
const top_blocked = statistics.top_blocked_domains.slice(0, 5);
|
||||
return this.render_top_table('top_blocked', top_blocked)
|
||||
},
|
||||
// Core LuCI functions from here on.
|
||||
load: function () {
|
||||
return Promise.all([
|
||||
this.load_adguardhome_status(),
|
||||
this.load_adguardhome_statistics(),
|
||||
this.load_adguardhome_config()
|
||||
]);
|
||||
},
|
||||
render: function (data) {
|
||||
// data[0] should be load_adguardhome_status() result
|
||||
var status = data[0] || {};
|
||||
// data[1] should be load_adguardhome_statistics() result
|
||||
var statistics = data[1] || {};
|
||||
// data[2] should be load_adguardhome_config() result
|
||||
var agh_config = data[2] || {};
|
||||
|
||||
// status.auth_error is only filled in when the config fetch failed
|
||||
// to get the credentials. That stops all activity, since the user must
|
||||
// first configure the username and password. Don't even bother trying
|
||||
// to make REST API calls without credentials.
|
||||
if (status.auth_error) {
|
||||
return E('div', { 'class': 'cbi-map', 'id': 'map' }, [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'class': 'left' }, [
|
||||
E('h2', _('AdGuard Home Status - Error')),
|
||||
E('div', { 'class': 'error' }, status.auth_error),
|
||||
E('div', { 'class': 'info' }, _('Please open the Configuration section, and provide the credentials.'))
|
||||
])
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
// Render all the status tables as one big block.
|
||||
return E('div', { 'class': 'cbi-map', 'id': 'map' }, [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'class': 'left' }, [
|
||||
E('h2', _('AdGuard Home Status')),
|
||||
this.render_status_table(status, agh_config)
|
||||
]),
|
||||
E('div', { 'class': 'left' }, [
|
||||
E('h3', _('AdGuard Home Statistics')),
|
||||
this.render_statistics_table(statistics)
|
||||
]),
|
||||
E('div', { 'class': 'left' }, [
|
||||
E('h3', _('Top Queried Domains')),
|
||||
this.render_top_queries_table(statistics)
|
||||
]),
|
||||
E('div', { 'class': 'left' }, [
|
||||
E('h3', _('Top Blocked Domains')),
|
||||
this.render_top_blocked_table(statistics)
|
||||
])
|
||||
]),
|
||||
]);
|
||||
},
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null
|
||||
})
|
||||
+1
-1
@@ -4,4 +4,4 @@
|
||||
* Licence: BSD-3-Clause
|
||||
*/
|
||||
!function(r,n){"use strict";function e(r){return y[g]=t.apply(n,r),g++}function t(r){var e=[].slice.call(arguments,1);return function(){"function"==typeof r?r.apply(n,e):new Function(""+r)()}}function o(r){if(m)setTimeout(t(o,r),0);else{var n=y[r];if(n){m=!0;try{n()}finally{a(r),m=!1}}}}function a(r){delete y[r]}function i(){p=function(){var r=e(arguments);return process.nextTick(t(o,r)),r}}function u(){if(r.postMessage&&!r.importScripts){var n=!0,e=r.onmessage;return r.onmessage=function(){n=!1},r.postMessage("","*"),r.onmessage=e,n}}function f(){var n="setImmediate$"+Math.random()+"$",t=function(e){e.source===r&&"string"==typeof e.data&&0===e.data.indexOf(n)&&o(+e.data.slice(n.length))};r.addEventListener?r.addEventListener("message",t,!1):r.attachEvent("onmessage",t),p=function(){var t=e(arguments);return r.postMessage(n+t,"*"),t}}function c(){var r=new MessageChannel;r.port1.onmessage=function(r){var n=r.data;o(n)},p=function(){var n=e(arguments);return r.port2.postMessage(n),n}}function s(){var r=v.documentElement;p=function(){var n=e(arguments),t=v.createElement("script");return t.onreadystatechange=function(){o(n),t.onreadystatechange=null,r.removeChild(t),t=null},r.appendChild(t),n}}function l(){p=function(){var r=e(arguments);return setTimeout(t(o,r),0),r}}if(!r.setImmediate){var p,g=1,y={},m=!1,v=r.document,d=Object.getPrototypeOf&&Object.getPrototypeOf(r);d=d&&d.setTimeout?d:r,"[object process]"==={}.toString.call(r.process)?i():u()?f():r.MessageChannel?c():v&&"onreadystatechange"in v.createElement("script")?s():l(),d.setImmediate=p,d.clearImmediate=a}}(new Function("return this")()),function(r){"object"==typeof exports?r(exports,require("crypto")):r(self.TwinBcrypt={},self.crypto||self.msCrypto)}(function(r,n){"use strict";function e(r){for(var n=unescape(encodeURIComponent(r)),e=n.length,t=new Array(e),o=0;e>o;o++)t[o]=n.charCodeAt(o);return t}function t(r){for(var n=r.length,e=new Array(n),t=0;n>t;t++)e[t]=r.charCodeAt(t);return e}function o(r,n){for(var e,t,o=0,a="";n>o;){if(e=255&r[o++],a+=B[e>>2],e=(3&e)<<4,o>=n){a+=B[e];break}if(t=255&r[o++],e|=t>>4,a+=B[e],e=(15&t)<<2,o>=n){a+=B[e];break}t=255&r[o++],e|=t>>6,a+=B[e],a+=B[63&t]}return a}function a(r){for(var n,e,t=new Array(16),o=0,a=0;;){if(n=D[r.charCodeAt(o++)-46],e=D[r.charCodeAt(o++)-46],t[a++]=255&(n<<2|e>>4),22===o)break;n=e<<4,e=D[r.charCodeAt(o++)-46],t[a++]=255&(n|e>>2),n=e<<6,e=D[r.charCodeAt(o++)-46],t[a++]=255&(n|e)}return t}function i(r){for(var n=r.length,e=new Array(72),t=0,o=0;72>o;)e[o++]=r[t++],t===n&&(t=0);return e}function u(r,n,e){for(var t=0,o=e>>2;t<n.length;t++,o++)r[o]=n[t]}function f(r,n,e){for(var t=0;72>t;)r[n++]=e[t++]<<24|e[t++]<<16|e[t++]<<8|e[t++]}function c(r){function n(n){for(var e=r,t=G>>2,o=t|O,f=n>>2,c=e[f]^e[t],s=e[1|f];o>t;)s^=(e[c>>>24]+e[a|c>>>16&255]^e[i|c>>>8&255])+e[u|255&c]^e[++t],c^=(e[s>>>24]+e[a|s>>>16&255]^e[i|s>>>8&255])+e[u|255&s]^e[++t];e[f]=s^e[S>>2],e[1|f]=c}function e(n){var e;for(r[L>>2]=0,r[L+4>>2]=0,e=0;M>e;e++)r[G>>2|e]^=r[(n>>2)+e];var t,o,f,c,s,l=r;for(e=0;M>e;e+=2){for(t=G>>2,o=t|O,f=L>>2,c=l[f]^l[t],s=l[1|f];o>t;)s^=(l[c>>>24]+l[a|c>>>16&255]^l[i|c>>>8&255])+l[u|255&c]^l[++t],c^=(l[s>>>24]+l[a|s>>>16&255]^l[i|s>>>8&255])+l[u|255&s]^l[++t];l[f]=s^l[S>>2],l[1|f]=c,r[G>>2|e]=l[f],r[G>>2|e+1]=c}for(e=0;T>e;e+=2){for(t=G>>2,o=t|O,f=L>>2,c=l[f]^l[t],s=l[1|f];o>t;)s^=(l[c>>>24]+l[a|c>>>16&255]^l[i|c>>>8&255])+l[u|255&c]^l[++t],c^=(l[s>>>24]+l[a|s>>>16&255]^l[i|s>>>8&255])+l[u|255&s]^l[++t];l[f]=s^l[S>>2],l[1|f]=c,r[e]=l[f],r[1|e]=c}}function t(r,n,t){for(var o=0;t>=o&&!(r>n);o++)e(R),e(j),r++;return r}var o=k>>2,a=o+256|0,i=a+256|0,u=i+256|0;return{encrypt:n,expandLoop:t}}function s(stdlib, foreign, heap) {"use asm";var HEAP32=new stdlib.Uint32Array(heap);var BLOWFISH_NUM_ROUNDS=16;var S_offset=0x0000;var S1_offset=0x0400;var S2_offset=0x0800;var S3_offset=0x0C00;var P_offset=0x1000;var P_last_offset=0x1044;var crypt_ciphertext_offset=0x1048;var LR_offset=0x01060;var password_offset=0x1068;var salt_offset=0x10b0;var P_LEN=18;var S_LEN=1024;function encrypt(offset) {offset=offset|0;var i=0;var n=0;var L=0;var R=0;var imax=0;imax=P_offset|BLOWFISH_NUM_ROUNDS<<2;L=HEAP32[offset>>2]|0;R=HEAP32[offset+4>>2]|0;L=L^HEAP32[P_offset>>2];for (i=P_offset; (i|0)<(imax|0);) {i=(i+4)>>>0;R=R^(((HEAP32[(L>>>22)>>2]>>>0) +(HEAP32[(S1_offset|(L>>>14&0x3ff))>>2]>>>0) ^(HEAP32[(S2_offset|(L>>>6&0x3ff))>>2])) +(HEAP32[(S3_offset|(L<<2&0x3ff))>>2]>>>0))^HEAP32[i>>2];i=(i+4)>>>0;L=L^(((HEAP32[(R>>>22)>>2]>>>0) +(HEAP32[(S1_offset|(R>>>14&0x3ff))>>2]>>>0) ^(HEAP32[(S2_offset|(R>>>6&0x3ff))>>2])) +(HEAP32[(S3_offset|(R<<2&0x3ff))>>2]>>>0))^HEAP32[i>>2];}HEAP32[offset>>2]=R^HEAP32[P_last_offset>>2];HEAP32[(offset+4)>>2]=L;}function expandKey(offset) {offset=offset|0;var i=0;var off=0;off=P_offset|0;for (i=0; (i|0)<(P_LEN|0); i=(i+1)|0) {HEAP32[off>>2]=HEAP32[off>>2]^HEAP32[offset>>2];offset=(offset+4)|0;off=(off+4)|0;}HEAP32[LR_offset>>2]=0;HEAP32[LR_offset+4>>2]=0;off=P_offset;for (i=0; (i|0)<(P_LEN|0); iLine truncated
|
||||
function l(r,n,e,t){var o,a,i,u=L>>2,f=u+1;for(t[u]=0,t[f]=0,a=0,o=0;M>o;o++)i=n[a++]<<24|n[a++]<<16|n[a++]<<8|n[a++],t[G>>2|o]^=i;for(a=0,o=0;M>o;o+=2)i=r[a++]<<24|r[a++]<<16|r[a++]<<8|r[a++],a&=65295,t[u]^=i,i=r[a++]<<24|r[a++]<<16|r[a++]<<8|r[a++],a&=65295,t[f]^=i,e.encrypt(L),t[G>>2|o]=t[u],t[G>>2|o+1]=t[f];var c=k>>2;for(o=0;T>o;o+=2)i=r[a++]<<24|r[a++]<<16|r[a++]<<8|r[a++],a&=65295,t[u]^=i,i=r[a++]<<24|r[a++]<<16|r[a++]<<8|r[a++],a&=65295,t[f]^=i,e.encrypt(L),t[c|o]=t[u],t[c|o+1]=t[f]}function p(r,n,e,t,o,a,i){for(var u=e;t>=u;){if(u=r.expandLoop(u,t,o),a){var f=a(u/(t+1));if(f===!1)return}if(u>t){if(i)return void setImmediate(g.bind(null,r,n,i));return}if(i)return void setImmediate(p.bind(null,r,n,u,t,o,a,i))}}function g(r,n,e){u(n,x,F);var t;for(t=0;64>t;t++)r.encrypt(F+0),r.encrypt(F+8),r.encrypt(F+16);var o,a=0,i=x.length,f=new Array(4*i);for(t=0;i>t;t++)o=n[(F>>2)+t],f[a++]=o>>24,f[a++]=o>>16&255,f[a++]=o>>8&255,f[a++]=255&o;return e&&e(f),f}function y(r,n){return r+o(n,23)}function m(n,o,m,v){var d,h=o.substr(0,29),w=+o.substr(4,2),A=o.substr(7,22);if("string"==typeof n)d=r.encodingMode===r.ENCODING_UTF8?e(n):t(n);else if(Array.isArray(n))d=n.map(function(r){return 255&r});else{if(!(n instanceof Uint8Array))throw new Error("Incorrect arguments");d=Array.prototype.slice.call(n)}d.push(0);var b,E,N=a(A,C),O=31>w?1<<w:2147483648,M=O-1,T=m?127:M;if(I){var x=new ArrayBuffer(8192);b=new Uint32Array(x),E=s({Uint32Array:Uint32Array},null,x)}else E=c(b=[]);return u(b,U,k),u(b,$,G),d=i(d),N=i(N),f(b,j>>2,N),f(b,R>>2,d),l(N,d,E,b),v?void p(E,b,0,M,T,m,function(r){v(y(h,r))}):(p(E,b,0,M,T,m),y(h,g(E,b)))}function v(r){if(!b)throw new Error("No cryptographically secure pseudorandom number generator available.");if(null==r&&(r=N),r=0|+r,isNaN(r)||4>r||r>31)throw new Error("Invalid cost parameter.");var n="$2y$";return 10>r&&(n+="0"),n+=r+"$",n+=o(b(C),C)}function d(r,n,e){if(n&&"number"!=typeof n){if("string"!=typeof n||!z.test(n))throw new Error("Invalid salt")}else n=v(n);return m(r,n,e)}function h(r,n,e,t){if(arguments.length<2)throw new Error("Incorrect arguments");if(2===arguments.length?(t=n,n=e=null):3===arguments.length&&(t=e,e=null,"function"==typeof n&&(e=n,n=null)),n&&"number"!=typeof n){if("string"!=typeof n||!z.test(n))throw new Error("Invalid salt")}else n=v(n);if(!t||"function"!=typeof t)throw new Error("No callback function was given.");m(r,n,e,t)}function w(r,n){if("string"!=typeof n||!Z.test(n))throw new Error("Incorrect arguments");var e=n.substr(0,n.length-31),t=d(r,e);return t===n}function A(r,n,e,t){if("string"!=typeof n||!Z.test(n))throw new Error("Incorrect arguments");if(t||(t=e,e=null),!t||"function"!=typeof t)throw new Error("No callback function was given.");var o=n.substr(0,n.length-31);h(r,o,e,function(r){t(r===n)})}var b,E="undefined"!=typeof InstallTrigger,I=E;n&&(b=n.randomBytes,n.getRandomValues&&(b=function(r){var e=new Uint8Array(r);return n.getRandomValues(e)}));var C=16,N=10,O=16,$=[608135816,2242054355,320440878,57701188,2752067618,698298832,137296536,3964562569,1160258022,953160567,3193202383,887688300,3232508343,3380367581,1065670069,3041331479,2450970073,2306472731],U=[3509652390,2564797868,805139163,3491422135,3101798381,1780907670,3128725573,4046225305,614570311,3012652279,134345442,2240740374,1667834072,1901547113,2757295779,4103290238,227898511,1921955416,1904987480,2182433518,2069144605,3260701109,2620446009,720527379,3318853667,677414384,3393288472,3101374703,2390351024,1614419982,1822297739,2954791486,3608508353,3174124327,2024746970,1432378464,3864339955,2857741204,1464375394,1676153920,1439316330,715854006,3033291828,289532110,2706671279,2087905683,3018724369,1668267050,732546397,1947742710,3462151702,2609353502,2950085171,1814351708,2050118529,680887927,999245976,1800124847,3300911131,1713906067,1641548236,4213287313,1216130144,1575780402,4018429277,3917837745,3693486850,3949271944,596196993,3549867205,258830323,2213823033,772490370,2760122372,1774776394,2652871518,566650946,4142492826,1728879713,2882767088,1783734482,3629395816,2517608232,2874225571,1861159788,326777828,3124490320,2130389656,2716951837,967770486,1724537150,2185432712,2364442137,1164943284,2105845187,998989502,3765401048,2244026483,1075463327,1455516326,1322494562,910128902,469688178,1117454909,936433444,3490320968,3675253459,1240580251,122909385,2157517691,634681816,4142456567,3825094682,3061402683,2540495037,79693498,3249098678,1084186820,1583128258,426386531,1761308591,1047286709,322548459,995290223,1845252383,2603652396,3431023940,2942221577,3202600964,3727903485,1712269319,422464435,3234572375,1170764815,3523960633,3117677531,1434042557,442511882,3600875718,1076654713,1738483198,4213154764,2393238008,3677496056,1014306527,4251020053,793779912,2902807211,842905082,4246964064,1395751752,1040244610,2656851899,3396308128,445077038,3742853595,3577915638,679411651,2892444358,2354009459,1767581616,3150600392,3791627101,3102740896,284835224,4246832056,1258075500,768725851,2589189241,3069724005Line truncated
|
||||
function l(r,n,e,t){var o,a,i,u=L>>2,f=u+1;for(t[u]=0,t[f]=0,a=0,o=0;M>o;o++)i=n[a++]<<24|n[a++]<<16|n[a++]<<8|n[a++],t[G>>2|o]^=i;for(a=0,o=0;M>o;o+=2)i=r[a++]<<24|r[a++]<<16|r[a++]<<8|r[a++],a&=65295,t[u]^=i,i=r[a++]<<24|r[a++]<<16|r[a++]<<8|r[a++],a&=65295,t[f]^=i,e.encrypt(L),t[G>>2|o]=t[u],t[G>>2|o+1]=t[f];var c=k>>2;for(o=0;T>o;o+=2)i=r[a++]<<24|r[a++]<<16|r[a++]<<8|r[a++],a&=65295,t[u]^=i,i=r[a++]<<24|r[a++]<<16|r[a++]<<8|r[a++],a&=65295,t[f]^=i,e.encrypt(L),t[c|o]=t[u],t[c|o+1]=t[f]}function p(r,n,e,t,o,a,i){for(var u=e;t>=u;){if(u=r.expandLoop(u,t,o),a){var f=a(u/(t+1));if(f===!1)return}if(u>t){if(i)return void setImmediate(g.bind(null,r,n,i));return}if(i)return void setImmediate(p.bind(null,r,n,u,t,o,a,i))}}function g(r,n,e){u(n,x,F);var t;for(t=0;64>t;t++)r.encrypt(F+0),r.encrypt(F+8),r.encrypt(F+16);var o,a=0,i=x.length,f=new Array(4*i);for(t=0;i>t;t++)o=n[(F>>2)+t],f[a++]=o>>24,f[a++]=o>>16&255,f[a++]=o>>8&255,f[a++]=255&o;return e&&e(f),f}function y(r,n){return r+o(n,23)}function m(n,o,m,v){var d,h=o.substr(0,29),w=+o.substr(4,2),A=o.substr(7,22);if("string"==typeof n)d=r.encodingMode===r.ENCODING_UTF8?e(n):t(n);else if(Array.isArray(n))d=n.map(function(r){return 255&r});else{if(!(n instanceof Uint8Array))throw new Error("Incorrect arguments");d=Array.prototype.slice.call(n)}d.push(0);var b,E,N=a(A,C),O=31>w?1<<w:2147483648,M=O-1,T=m?127:M;if(I){var x=new ArrayBuffer(8192);b=new Uint32Array(x),E=s({Uint32Array:Uint32Array},null,x)}else E=c(b=[]);return u(b,U,k),u(b,$,G),d=i(d),N=i(N),f(b,j>>2,N),f(b,R>>2,d),l(N,d,E,b),v?void p(E,b,0,M,T,m,function(r){v(y(h,r))}):(p(E,b,0,M,T,m),y(h,g(E,b)))}function v(r){if(!b)throw new Error("No cryptographically secure pseudorandom number generator available.");if(null==r&&(r=N),r=0|+r,isNaN(r)||4>r||r>31)throw new Error("Invalid cost parameter.");var n="$2y$";return 10>r&&(n+="0"),n+=r+"$",n+=o(b(C),C)}function d(r,n,e){if(n&&"number"!=typeof n){if("string"!=typeof n||!z.test(n))throw new Error("Invalid salt")}else n=v(n);return m(r,n,e)}function h(r,n,e,t){if(arguments.length<2)throw new Error("Incorrect arguments");if(2===arguments.length?(t=n,n=e=null):3===arguments.length&&(t=e,e=null,"function"==typeof n&&(e=n,n=null)),n&&"number"!=typeof n){if("string"!=typeof n||!z.test(n))throw new Error("Invalid salt")}else n=v(n);if(!t||"function"!=typeof t)throw new Error("No callback function was given.");m(r,n,e,t)}function w(r,n){if("string"!=typeof n||!Z.test(n))throw new Error("Incorrect arguments");var e=n.substr(0,n.length-31),t=d(r,e);return t===n}function A(r,n,e,t){if("string"!=typeof n||!Z.test(n))throw new Error("Incorrect arguments");if(t||(t=e,e=null),!t||"function"!=typeof t)throw new Error("No callback function was given.");var o=n.substr(0,n.length-31);h(r,o,e,function(r){t(r===n)})}var b,E="undefined"!=typeof InstallTrigger,I=E;n&&(b=n.randomBytes,n.getRandomValues&&(b=function(r){var e=new Uint8Array(r);return n.getRandomValues(e)}));var C=16,N=10,O=16,$=[608135816,2242054355,320440878,57701188,2752067618,698298832,137296536,3964562569,1160258022,953160567,3193202383,887688300,3232508343,3380367581,1065670069,3041331479,2450970073,2306472731],U=[3509652390,2564797868,805139163,3491422135,3101798381,1780907670,3128725573,4046225305,614570311,3012652279,134345442,2240740374,1667834072,1901547113,2757295779,4103290238,227898511,1921955416,1904987480,2182433518,2069144605,3260701109,2620446009,720527379,3318853667,677414384,3393288472,3101374703,2390351024,1614419982,1822297739,2954791486,3608508353,3174124327,2024746970,1432378464,3864339955,2857741204,1464375394,1676153920,1439316330,715854006,3033291828,289532110,2706671279,2087905683,3018724369,1668267050,732546397,1947742710,3462151702,2609353502,2950085171,1814351708,2050118529,680887927,999245976,1800124847,3300911131,1713906067,1641548236,4213287313,1216130144,1575780402,4018429277,3917837745,3693486850,3949271944,596196993,3549867205,258830323,2213823033,772490370,2760122372,1774776394,2652871518,566650946,4142492826,1728879713,2882767088,1783734482,3629395816,2517608232,2874225571,1861159788,326777828,3124490320,2130389656,2716951837,967770486,1724537150,2185432712,2364442137,1164943284,2105845187,998989502,3765401048,2244026483,1075463327,1455516326,1322494562,910128902,469688178,1117454909,936433444,3490320968,3675253459,1240580251,122909385,2157517691,634681816,4142456567,3825094682,3061402683,2540495037,79693498,3249098678,1084186820,1583128258,426386531,1761308591,1047286709,322548459,995290223,1845252383,2603652396,3431023940,2942221577,3202600964,3727903485,1712269319,422464435,3234572375,1170764815,3523960633,3117677531,1434042557,442511882,3600875718,1076654713,1738483198,4213154764,2393238008,3677496056,1014306527,4251020053,793779912,2902807211,842905082,4246964064,1395751752,1040244610,2656851899,3396308128,445077038,3742853595,3577915638,679411651,2892444358,2354009459,1767581616,3150600392,3791627101,3102740896,284835224,4246832056,1258075500,768725851,2589189241,3069724005Line truncated
|
||||
@@ -0,0 +1,51 @@
|
||||
From b65bfa5784b2cbec97740c96fa69d799f3203df0 Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Fri, 26 Jun 2026 06:50:26 +0800
|
||||
Subject: [PATCH] Modify Makefile for luci-app-adguardhome
|
||||
|
||||
Updated maintainer information and added package configuration.
|
||||
---
|
||||
Makefile | 26 ++++++++++++++++++++++----
|
||||
1 file changed, 22 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/Makefile b/Makefile
|
||||
index a48c04d..ec35f24 100644
|
||||
--- a/Makefile
|
||||
+++ b/Makefile
|
||||
@@ -3,14 +3,32 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_NAME:=luci-app-adguardhome
|
||||
-LUCI_MAINTAINER:=George Sapkin <george@sapk.in>
|
||||
+LUCI_MAINTAINER:=Jackie264 <OneNAS-space>
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
+PKG_CPE_ID:=cpe:/a:Jackie264:luci-app-adguardhome
|
||||
|
||||
LUCI_TITLE:=LuCI support for AdGuard Home
|
||||
-LUCI_DEPENDS:=+adguardhome +luci-base
|
||||
-LUCI_EXTRA_DEPENDS:=adguardhome (>=0.107.73-r3)
|
||||
+LUCI_DEPENDS:=+luci-base
|
||||
LUCI_PKGARCH:=all
|
||||
+USERID:=adguardhome=853:adguardhome=853
|
||||
|
||||
-include ../../luci.mk
|
||||
+PKG_UNPACK:=$(CURDIR)/.prepare.sh $(PKG_NAME) $(CURDIR) $(PKG_BUILD_DIR)
|
||||
+
|
||||
+define Package/luci-app-adguardhome/conffiles
|
||||
+/etc/adguardhome/adguardhome.yaml
|
||||
+/etc/config/adguardhome
|
||||
+endef
|
||||
+
|
||||
+define Package/luci-app-adguardhome/prerm
|
||||
+#!/bin/sh
|
||||
+[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
+ rm -f /tmp/luci-indexcache.*
|
||||
+ rm -rf /tmp/luci-modulecache/
|
||||
+ /etc/init.d/rpcd reload 2>/dev/null
|
||||
+}
|
||||
+exit 0
|
||||
+endef
|
||||
+
|
||||
+include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -0,0 +1,254 @@
|
||||
From 2cfa7fa8e6b2c20824bdb81a3d7449c9757ae793 Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Sat, 6 Jun 2026 23:21:32 +0800
|
||||
Subject: [PATCH] Enhance AdGuardHome init script with new features
|
||||
|
||||
Add extra commands and improve cron handling in AdGuardHome init script.
|
||||
---
|
||||
root/etc/init.d/adguardhome | 164 +++++++++++++++++++++++++++++++++++-
|
||||
1 file changed, 162 insertions(+), 2 deletions(-)
|
||||
|
||||
diff --git a/root/etc/init.d/adguardhome b/root/etc/init.d/adguardhome
|
||||
index 4712913..1383834 100755
|
||||
--- a/root/etc/init.d/adguardhome
|
||||
+++ b/root/etc/init.d/adguardhome
|
||||
@@ -3,6 +3,7 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# shellcheck disable=SC3043 # ash supports local
|
||||
+. /usr/share/AdGuardHome/helper.sh
|
||||
|
||||
# matches dnsmasq
|
||||
START=19
|
||||
@@ -12,22 +13,87 @@ STOP=89
|
||||
PROG=/usr/bin/AdGuardHome
|
||||
USE_PROCD=1
|
||||
|
||||
+EXTRA_COMMANDS="do_redirect force_reload isrunning"
|
||||
+EXTRA_HELP="\
|
||||
+\tdo_redirect \t0 or 1\n\
|
||||
+\tforce_reload\n\
|
||||
+\tisrunning"
|
||||
+
|
||||
+CONTROL_SCRIPT="/usr/share/AdGuardHome/control.sh"
|
||||
+CRON_FILE=/etc/crontabs/root
|
||||
+
|
||||
+get_tz() {
|
||||
+ SET_TZ=""
|
||||
+ local zonename
|
||||
+ zonename="$(uci get system.@system[0].zonename 2>/dev/null)"
|
||||
+ if [ -n "$zonename" ]; then
|
||||
+ SET_TZ="$zonename"
|
||||
+ return
|
||||
+ fi
|
||||
+ for tzfile in /etc/TZ /var/etc/TZ; do
|
||||
+ [ -e "$tzfile" ] && tz="$(cat "$tzfile" 2>/dev/null)" && break
|
||||
+ done
|
||||
+ [ -n "$tz" ] && SET_TZ=$tz && return
|
||||
+}
|
||||
+
|
||||
+do_crontab() {
|
||||
+ local enabled="$1"
|
||||
+ local crontab="$2"
|
||||
+ local findstr default cronenable cronreload=0
|
||||
+
|
||||
+ findstr="/usr/share/AdGuardHome/update_core.sh"
|
||||
+ default="30 3 * * * /usr/share/AdGuardHome/update_core.sh 2>&1"
|
||||
+ [ "$enabled" = "0" ] || [ "${crontab//autoupdate/}" = "$crontab" ] && cronenable=0 || cronenable=1
|
||||
+ crontab_editor
|
||||
+
|
||||
+ [ "$cronreload" -gt 0 ] && /etc/init.d/cron restart
|
||||
+}
|
||||
+
|
||||
+crontab_editor() {
|
||||
+ local line
|
||||
+ line="$(grep "$findstr" $CRON_FILE 2>/dev/null)"
|
||||
+ if [ "${line:0:1}" != "#" ]; then
|
||||
+ if [ "$cronenable" -eq 1 ]; then
|
||||
+ if [ -z "$line" ]; then
|
||||
+ echo "$default" >> $CRON_FILE
|
||||
+ cronreload=$((cronreload+1))
|
||||
+ fi
|
||||
+ elif [ -n "$line" ]; then
|
||||
+ sed -i "\,$findstr,d" $CRON_FILE
|
||||
+ echo "#$line" >> $CRON_FILE
|
||||
+ cronreload=$((cronreload+1))
|
||||
+ fi
|
||||
+ else
|
||||
+ if [ "$cronenable" -eq 1 ]; then
|
||||
+ sed -i "\,$findstr,d" $CRON_FILE
|
||||
+ echo "${line:1}" >> $CRON_FILE
|
||||
+ cronreload=$((cronreload+1))
|
||||
+ fi
|
||||
+ fi
|
||||
+}
|
||||
+
|
||||
boot() {
|
||||
ADGUARDHOME_BOOT=1
|
||||
start "$@"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
+ mkdir -p /var/etc
|
||||
+ [ ! -f /var/etc/adguardhome.nft ] && touch /var/etc/adguardhome.nft
|
||||
+ chmod 644 /var/etc/adguardhome.nft
|
||||
+
|
||||
if [ -n "$ADGUARDHOME_BOOT" ]; then
|
||||
# Do not start yet, wait for triggers
|
||||
return 0
|
||||
fi
|
||||
|
||||
+ rm -f /var/run/AdGserverdis 2>/dev/null
|
||||
config_load 'adguardhome'
|
||||
|
||||
local config_name='config'
|
||||
local config_file config group user verbose work_dir workdir
|
||||
local gc maxprocs memlimit
|
||||
+ local enabled hashpass crontab redirect
|
||||
|
||||
uci_validate_section 'adguardhome' 'adguardhome' "$config_name" \
|
||||
'gc:uinteger:0' \
|
||||
@@ -41,12 +107,46 @@ start_service() {
|
||||
'user:string:adguardhome' \
|
||||
'verbose:bool:0' \
|
||||
'workdir:string' \
|
||||
- 'work_dir:string:/var/lib/adguardhome'
|
||||
+ 'work_dir:string:/var/lib/adguardhome' \
|
||||
+ 'enabled:bool:0' \
|
||||
+ 'hashpass:string' \
|
||||
+ 'crontab:string' \
|
||||
+ 'redirect:string:none' \
|
||||
+ 'http_address:string'
|
||||
|
||||
# Compatibility with older configs
|
||||
[ -n "$config" ] && config_file="$config"
|
||||
[ -n "$workdir" ] && work_dir="$workdir"
|
||||
|
||||
+ # Handle hashpass clear text issuance and back brush removal
|
||||
+ if [ -n "$hashpass" ]; then
|
||||
+ config_editor "users.password" "$hashpass" "$config_file"
|
||||
+ uci set adguardhome."$config_name".hashpass=""
|
||||
+ uci commit adguardhome
|
||||
+ fi
|
||||
+
|
||||
+ # Synchronize the front-end incing WebUI listening address to YAML
|
||||
+ if [ -n "$http_address" ]; then
|
||||
+ config_editor "http.address" "$http_address" "$config_file"
|
||||
+ uci set adguardhome."$config_name".http_address=""
|
||||
+ uci commit adguardhome
|
||||
+ fi
|
||||
+
|
||||
+ # Isse the plan task change
|
||||
+ do_crontab "$enabled" "$crontab"
|
||||
+
|
||||
+ # If it is not enabled, the linkage cleans up the redirection firewall rules and exits safely.
|
||||
+ if [ "$enabled" = 0 ]; then
|
||||
+ $CONTROL_SCRIPT 0
|
||||
+ return 0
|
||||
+ fi
|
||||
+
|
||||
+ if [ "$user" = "adguardhome" ] && ! grep -q "^adguardhome:" /etc/passwd; then
|
||||
+ echo "luci-app: User 'adguardhome' is missing, injecting UID 853 into system files..." >&2
|
||||
+ grep -q "^adguardhome:" /etc/group || echo "adguardhome:x:853:" >> /etc/group
|
||||
+ echo "adguardhome:x:853:853:adguardhome:/var/run/adguardhome:/bin/false" >> /etc/passwd
|
||||
+ fi
|
||||
+
|
||||
local config_dir
|
||||
config_dir=$(dirname "$config_file")
|
||||
if [ "$config_dir" = '/etc' ]; then
|
||||
@@ -59,6 +159,13 @@ start_service() {
|
||||
mkdir -m 0700 -p "$work_dir"
|
||||
chown -R "$user":"$group" "$work_dir"
|
||||
|
||||
+ # Silently downgrade and call the kernel download script when the kernel is missing.
|
||||
+ if [ ! -f "$PROG" ]; then
|
||||
+ $CONTROL_SCRIPT 0
|
||||
+ /usr/share/AdGuardHome/update_core.sh >/tmp/AdGuardHome_update.log 2>&1 &
|
||||
+ return 0
|
||||
+ fi
|
||||
+
|
||||
procd_open_instance adguardhome
|
||||
|
||||
procd_set_param command "$PROG"
|
||||
@@ -67,12 +174,17 @@ start_service() {
|
||||
[ "$maxprocs" -le 0 ] || procd_append_param env GOMAXPROCS="$maxprocs"
|
||||
[ "$memlimit" -le 0 ] || procd_append_param env GOMEMLIMIT="$memlimit"
|
||||
|
||||
+ local SET_TZ
|
||||
+ get_tz
|
||||
+ [ -n "$SET_TZ" ] && procd_append_param env TZ="$SET_TZ"
|
||||
+
|
||||
procd_append_param command --config "$config_file"
|
||||
procd_append_param command --logfile syslog
|
||||
- procd_append_param command --no-check-update
|
||||
+ # procd_append_param command --no-check-update
|
||||
[ "$verbose" = 1 ] && procd_append_param command --verbose
|
||||
procd_append_param command --work-dir "$work_dir"
|
||||
|
||||
+ procd_set_param file /etc/config/adguardhome "$config_file"
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param user "$user"
|
||||
@@ -91,10 +203,58 @@ start_service() {
|
||||
|
||||
procd_add_jail_mount '/etc/hosts'
|
||||
procd_add_jail_mount '/etc/ssl/certs'
|
||||
+ [ -f /etc/TZ ] && procd_add_jail_mount '/etc/TZ'
|
||||
+
|
||||
config_list_foreach "$config_name" jail_mount procd_add_jail_mount
|
||||
config_list_foreach "$config_name" jail_mount_rw procd_add_jail_mount_rw
|
||||
|
||||
procd_close_instance
|
||||
+
|
||||
+ # After the main process is successfully submitted for start-up, it begins to intervene asynchronously and take over the control.
|
||||
+ if [ -f "$config_file" ]; then
|
||||
+ ($CONTROL_SCRIPT 1) &
|
||||
+ else
|
||||
+ $CONTROL_SCRIPT 0
|
||||
+ if [ "$redirect" != "none" ]; then
|
||||
+ procd_open_instance "waitconfig"
|
||||
+ procd_set_param command "/usr/share/AdGuardHome/watchconfig.sh"
|
||||
+ procd_close_instance
|
||||
+ fi
|
||||
+ fi
|
||||
+
|
||||
+ (sleep 10 && [ -z "$(pgrep -f "$(basename "$PROG")")" ] && logger -t adguardhome "no process in 10s cancel redirect" && $CONTROL_SCRIPT 0 ) &
|
||||
+}
|
||||
+
|
||||
+stop_service() {
|
||||
+ config_load 'adguardhome'
|
||||
+ local config_name='config'
|
||||
+ local crontab
|
||||
+ config_get crontab "$config_name" crontab ""
|
||||
+
|
||||
+ $CONTROL_SCRIPT 0
|
||||
+ do_crontab 0 "$crontab"
|
||||
+ touch /var/run/AdGserverdis
|
||||
+}
|
||||
+
|
||||
+do_redirect() {
|
||||
+ $CONTROL_SCRIPT "$1"
|
||||
+}
|
||||
+
|
||||
+_isrunning() {
|
||||
+ [ ! -f "$PROG" ] && return 2
|
||||
+ pgrep -f "$(basename "$PROG")" >/dev/null 2>&1 && return 0
|
||||
+ return 1
|
||||
+}
|
||||
+
|
||||
+isrunning() {
|
||||
+ _isrunning
|
||||
+ local r=$?
|
||||
+ [ "$r" = "0" ] && echo "running" || ([ "$r" = "1" ] && echo "not run" || echo "no bin")
|
||||
+ return $r
|
||||
+}
|
||||
+
|
||||
+force_reload() {
|
||||
+ _isrunning && procd_send_signal adguardhome || start
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
|
||||
reload_service() {
|
||||
restart
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
From 883995b0d027fcd32fb9ba4ff24d607e9c176ee2 Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Thu, 28 May 2026 10:14:54 +0800
|
||||
Subject: [PATCH] Update luci-app-adguardhome.json
|
||||
|
||||
---
|
||||
.../rpcd/acl.d/luci-app-adguardhome.json | 36 +++++++++++--------
|
||||
1 file changed, 21 insertions(+), 15 deletions(-)
|
||||
|
||||
diff --git a/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json b/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json
|
||||
index 748778a..33ba1fd 100644
|
||||
--- a/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json
|
||||
+++ b/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json
|
||||
@@ -1,17 +1,25 @@
|
||||
{
|
||||
- "luci-app-adguardhome": {
|
||||
- "description": "Grant permissions for the AdGuard Home LuCI app",
|
||||
- "read": {
|
||||
- "file": {
|
||||
- "/usr/bin/AdGuardHome --version": [ "exec" ]
|
||||
- },
|
||||
- "ubus": {
|
||||
- "service": [ "list" ]
|
||||
- },
|
||||
- "uci": [ "adguardhome" ]
|
||||
- },
|
||||
- "write": {
|
||||
- "uci": [ "adguardhome" ]
|
||||
- }
|
||||
- }
|
||||
+ "luci-app-adguardhome": {
|
||||
+ "description": "Grant permissions for the AdGuard Home LuCI app",
|
||||
+ "read": {
|
||||
+ "file": {
|
||||
+ "/tmp/AdGuardHome_update.log": [ "read" ],
|
||||
+ "/var/run/update_core": [ "stat" ],
|
||||
+ "/var/run/update_core_error": [ "stat" ],
|
||||
+ "/var/run/AdGredir": [ "read" ],
|
||||
+ "/var/run/AdGserverdis": [ "read" ],
|
||||
+ "/usr/bin/AdGuardHome --version": [ "exec" ]
|
||||
+ },
|
||||
+ "ubus": {
|
||||
+ "service": [ "list" ]
|
||||
+ },
|
||||
+ "uci": [ "adguardhome" ]
|
||||
+ },
|
||||
+ "write": {
|
||||
+ "file": {
|
||||
+ "/usr/share/AdGuardHome/update_core.sh": [ "exec" ]
|
||||
+ },
|
||||
+ "uci": [ "adguardhome" ]
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
From fc3e12a5860b5da4230293d7e6dc4f3b24b32b23 Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Sat, 4 Jul 2026 14:19:51 +0800
|
||||
Subject: [PATCH] Enhance AdGuard Home config with core update options
|
||||
|
||||
Updated service status indicators and added core update functionality.
|
||||
---
|
||||
.../resources/view/adguardhome/config.js | 369 +++++++++++++++++-
|
||||
1 file changed, 364 insertions(+), 5 deletions(-)
|
||||
|
||||
diff --git a/htdocs/luci-static/resources/view/adguardhome/config.js b/htdocs/luci-static/resources/view/adguardhome/config.js
|
||||
index 6dc2d45..12fc3ab 100644
|
||||
--- a/htdocs/luci-static/resources/view/adguardhome/config.js
|
||||
+++ b/htdocs/luci-static/resources/view/adguardhome/config.js
|
||||
@@ -5,6 +5,7 @@
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
+'require uci';
|
||||
'require view';
|
||||
|
||||
const DEFAULT_CONFIG_FILE = '/etc/adguardhome/adguardhome.yaml';
|
||||
@@ -20,10 +21,11 @@ const PATH_REGEX = new RegExp('^/etc(/[^/]+)?/?$');
|
||||
|
||||
const POLL_INTERVAL = 5;
|
||||
|
||||
-const RUNNING_SPAN = `<span style="color: var(--success-color-high); font-weight: bold">${_('Running')}</span>`;
|
||||
-const NOT_RUNNING_SPAN = `<span style="color: var(--error-color-high); font-weight: bold">${_('Not running')}</span>`;
|
||||
+const RUNNING_SPAN = `<span style="color: green; font-weight: bold">${_('Running')}</span>`;
|
||||
+const NOT_RUNNING_SPAN = `<span style="color: red; font-weight: bold">${_('Not running')}</span>`;
|
||||
|
||||
const STORAGE_KEY = 'luci-app-adguardhome';
|
||||
+const STORAGE_KEY_CORE = 'luci-app-adguardhome_core_update';
|
||||
|
||||
function getServiceInfo(name) {
|
||||
const fn = rpc.declare({
|
||||
@@ -61,7 +63,7 @@ async function getVersion() {
|
||||
return version;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
- return 'unknown version';
|
||||
+ return '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,10 +108,33 @@ return view.extend({
|
||||
return Promise.all([
|
||||
getStatus(),
|
||||
getVersion(),
|
||||
+ uci.load('adguardhome').then(() => {
|
||||
+ const sections = uci.sections('adguardhome', 'adguardhome');
|
||||
+ const sec = sections.length > 0 ? sections[0] : {};
|
||||
+ const configFile = sec.config_file || DEFAULT_CONFIG_FILE;
|
||||
+ return fs.read(configFile).catch(() => null);
|
||||
+ })
|
||||
]);
|
||||
},
|
||||
|
||||
- async render([isRunning, version]) {
|
||||
+ async render([isRunning, version, yamlContent]) {
|
||||
+ // If there is a version number, it means that the kernel exists. If there is no version number, it does not exist.
|
||||
+ const coreExists = Boolean(version);
|
||||
+ let dnsPort = '53';
|
||||
+ if (yamlContent) {
|
||||
+ const portMatches = yamlContent.match(/port:\s*(\d+)/g);
|
||||
+ if (portMatches && portMatches.length >= 2) {
|
||||
+ const actualPort = portMatches[1].match(/\d+/);
|
||||
+ if (actualPort) {
|
||||
+ dnsPort = actualPort[0];
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // 💡 Dynamic security value: UI rendering requires httpport to generate jump links
|
||||
+ const sections = uci.sections('adguardhome', 'adguardhome');
|
||||
+ const savedHttpPort = (sections.length > 0 && sections[0].httpport) ? sections[0].httpport : '3008';
|
||||
+
|
||||
const map = new form.Map('adguardhome', _('AdGuard Home'));
|
||||
|
||||
const statusSect = map.section(form.TypedSection, 'status');
|
||||
@@ -117,7 +142,8 @@ return view.extend({
|
||||
statusSect.cfgsections = () => ['status_section'];
|
||||
|
||||
const versionOpt = statusSect.option(form.DummyValue, '_version', _('Version'));
|
||||
- versionOpt.cfgvalue = () => version;
|
||||
+ versionOpt.cfgvalue = () => version || `<span style="color: var(--error-color-high); font-weight: bold;">${_('Not installed')}</span>`;
|
||||
+ versionOpt.rawhtml = true;
|
||||
|
||||
const statusOpt = statusSect.option(form.DummyValue, '_status', _('Service Status'));
|
||||
statusOpt.rawhtml = true;
|
||||
@@ -132,6 +158,12 @@ return view.extend({
|
||||
_('File System Access'),
|
||||
_('Files and directories that AdGuard Home should have read-only or read-write access to.'),
|
||||
);
|
||||
+ mainSect.tab('dns_redirect', _('Services Settings'));
|
||||
+ mainSect.tab(
|
||||
+ 'core_update',
|
||||
+ _('Core Update'),
|
||||
+ _('Settings and operations for updating the AdGuardHome core binary.')
|
||||
+ );
|
||||
mainSect.tab(
|
||||
'advanced',
|
||||
_('Advanced Settings'),
|
||||
@@ -139,6 +171,20 @@ return view.extend({
|
||||
' ' + _('Modify at your own risk.'),
|
||||
);
|
||||
|
||||
+ // ==== Move here: global switch ====
|
||||
+ const enabledOpt = mainSect.taboption(
|
||||
+ 'general',
|
||||
+ form.Flag,
|
||||
+ 'enabled',
|
||||
+ _('Enable')
|
||||
+ );
|
||||
+ enabledOpt.default = '0';
|
||||
+ enabledOpt.rmempty = false;
|
||||
+ // If the kernel does not exist, additional warning prompts will guide users to turn on the service to download automatically.
|
||||
+ if (!coreExists) {
|
||||
+ enabledOpt.description = `<span style="color: var(--error-color-high); font-weight: bold;">${_('Core binary not found. Enable the service to trigger an automatic download.')}</span>`;
|
||||
+ }
|
||||
+
|
||||
const configFileOpt = mainSect.taboption(
|
||||
'general',
|
||||
form.Value,
|
||||
@@ -204,6 +250,21 @@ return view.extend({
|
||||
advSettingsOpt.remove = () => {};
|
||||
advSettingsOpt.write = (_, value) => sessionStorage.setItem(STORAGE_KEY, value);
|
||||
|
||||
+ // ==== New: The switch of Core Update on General tab ====
|
||||
+ const coreUpdateToggleOpt = mainSect.taboption(
|
||||
+ 'general',
|
||||
+ form.Flag,
|
||||
+ 'enable_core_update',
|
||||
+ _('Core Update'),
|
||||
+ _('Show the tab and settings for updating the AdGuardHome core binary.')
|
||||
+ );
|
||||
+ coreUpdateToggleOpt.default = '0';
|
||||
+ coreUpdateToggleOpt.rmempty = false;
|
||||
+ coreUpdateToggleOpt.load = () => sessionStorage.getItem(STORAGE_KEY_CORE) || '0';
|
||||
+ coreUpdateToggleOpt.remove = () => {};
|
||||
+ coreUpdateToggleOpt.write = (_, value) => sessionStorage.setItem(STORAGE_KEY_CORE, value);
|
||||
+ // ==========================================
|
||||
+
|
||||
mainSect.taboption('jail', form.DynamicList, 'jail_mount', _('Read-only access'));
|
||||
mainSect.taboption('jail', form.DynamicList, 'jail_mount_rw', _('Read-write access'));
|
||||
|
||||
@@ -251,11 +312,309 @@ return view.extend({
|
||||
memLimitOpt.placeholder = DEFAULT_GOMEMLIMIT;
|
||||
memLimitOpt.retain = true;
|
||||
|
||||
+ // 💡 1. Extract the real listening address and port directly from YAML
|
||||
+ let realHttpAddress = '0.0.0.0:3008';
|
||||
+ if (yamlContent) {
|
||||
+ // Match the address: field below http: in YAML
|
||||
+ const addrMatch = yamlContent.match(/address:\s*([^\s]+)/);
|
||||
+ if (addrMatch && addrMatch[1]) {
|
||||
+ realHttpAddress = addrMatch[1];
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // 💡 2. Analyze the real IP and port used by the jump button
|
||||
+ let linkIp = window.location.hostname;
|
||||
+ let linkPort = '3008';
|
||||
+ const addrParts = realHttpAddress.split(':');
|
||||
+ if (addrParts.length >= 2) {
|
||||
+ linkPort = addrParts.pop();
|
||||
+ let ipPart = addrParts.join(':').replace(/\[|\]/g, '');
|
||||
+ // If the binding is a full zero address, the IP accessed by the current router will be returned.
|
||||
+ if (ipPart !== '0.0.0.0' && ipPart !== '') {
|
||||
+ linkIp = ipPart;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // 💡 3. Upgrade the original httpport to http_address
|
||||
+ const isServiceEnabled = sections.length > 0 && sections[0].enabled === '1';
|
||||
+ const disabledHint = _('Service is disabled. Please go to "General Settings" to enable it.');
|
||||
+
|
||||
+ // 💡 4. WebUI button HTML to construct intelligent state
|
||||
+ const webuiBtnHtml = isServiceEnabled
|
||||
+ ? `<a class="btn cbi-button cbi-button-link" style="font-weight:bold; display:inline-block; margin-top:5px;" href="http://${linkIp}:${linkPort}" target="_blank">${_('Open AdGuardHome WebUI')}</a>`
|
||||
+ : `<span title='${disabledHint}' style="display:inline-block; margin-top:5px; cursor:not-allowed;">
|
||||
+ <a class="btn cbi-button cbi-button-link" style="font-weight:bold; pointer-events:none; opacity:0.5; margin-top:0;" href="javascript:void(0);">${_('Open AdGuardHome WebUI')}</a>
|
||||
+ </span>`;
|
||||
+
|
||||
+ // 💡 5. Rendering components
|
||||
+ const httpAddressOpt = mainSect.taboption(
|
||||
+ 'dns_redirect',
|
||||
+ form.Value,
|
||||
+ 'http_address',
|
||||
+ _('WebUI listener (address:port)'),
|
||||
+ _('Bind to specific interface:port (e.g., 0.0.0.0:3008). Leave as 0.0.0.0 to listen on all interfaces.') +
|
||||
+ `<br />${webuiBtnHtml}`
|
||||
+ );
|
||||
+ httpAddressOpt.placeholder = '0.0.0.0:3008';
|
||||
+ httpAddressOpt.default = '0.0.0.0:3008';
|
||||
+ httpAddressOpt.datatype = 'hostport';
|
||||
+ httpAddressOpt.rmempty = false;
|
||||
+
|
||||
+ // 🎯 Overwrite the default reading UCI behavior and forcibly echo the real value in YAML
|
||||
+ httpAddressOpt.cfgvalue = function(section_id) {
|
||||
+ return realHttpAddress;
|
||||
+ };
|
||||
+
|
||||
+ // ==== Add the password modification function below the WebUI port ====
|
||||
+ const isPasswordEmpty = yamlContent ? /password:[ \t]*(\r?\n|$)/.test(yamlContent) : false;
|
||||
+ const hashPassOpt = mainSect.taboption(
|
||||
+ 'dns_redirect',
|
||||
+ form.Value,
|
||||
+ 'hashpass',
|
||||
+ _('Change password'),
|
||||
+ _('Enter the password here. Click the Load calculate Module button below and go on.') +
|
||||
+ '<br /><button class="btn cbi-button cbi-button-apply" type="button" id="btn-agh-calc-hash" style="display:inline-block; margin-top:5px;">' +
|
||||
+ _(' ... Load calculate Module ... ') +
|
||||
+ '</button>'
|
||||
+ );
|
||||
+ hashPassOpt.default = '';
|
||||
+ hashPassOpt.datatype = 'string';
|
||||
+ hashPassOpt.password = true;
|
||||
+ hashPassOpt.rmempty = true;
|
||||
+ hashPassOpt.placeholder = isPasswordEmpty ? _('Please create a new password.') : '';
|
||||
+ hashPassOpt.cfgvalue = function(section_id) {
|
||||
+ return '';
|
||||
+ };
|
||||
+
|
||||
+ const redirectOpt = mainSect.taboption(
|
||||
+ 'dns_redirect',
|
||||
+ form.ListValue,
|
||||
+ 'redirect',
|
||||
+ `${dnsPort} ` + _('Redirect'),
|
||||
+ _('AdGuardHome redirect mode')
|
||||
+ );
|
||||
+ redirectOpt.value('none', _('No redirect'));
|
||||
+ redirectOpt.value('dnsmasq-upstream', _('As the upstream server of dnsmasq'));
|
||||
+ redirectOpt.value('redirect', _('Redirect port 53 to AdGuardHome'));
|
||||
+ redirectOpt.value('exchange', _('Use port 53 to replace dnsmasq'));
|
||||
+ redirectOpt.default = 'none';
|
||||
+ redirectOpt.rmempty = false;
|
||||
+ // ==========================================
|
||||
+
|
||||
+ // ======== Core Update Control content ========
|
||||
+ const coreVersionOpt = mainSect.taboption(
|
||||
+ 'core_update',
|
||||
+ form.ListValue,
|
||||
+ 'core_version',
|
||||
+ _('Core Branch'),
|
||||
+ _('Select the branch for the core binary update.')
|
||||
+ );
|
||||
+ coreVersionOpt.value('latest', _('Latest Version'));
|
||||
+ coreVersionOpt.value('beta', _('Beta Version'));
|
||||
+ coreVersionOpt.default = 'latest';
|
||||
+ coreVersionOpt.depends('enable_core_update', '1');
|
||||
+ coreVersionOpt.retain = true;
|
||||
+
|
||||
+ const coreUrlOpt = mainSect.taboption(
|
||||
+ 'core_update',
|
||||
+ form.ListValue,
|
||||
+ 'update_url',
|
||||
+ _('Update URL'),
|
||||
+ _('Select the download link for the core update.')
|
||||
+ );
|
||||
+ coreUrlOpt.value('https://static.adtidy.org/adguardhome/release/AdGuardHome_linux_${Arch}.tar.gz', _('Official Mirror (AdTidy - Recommended)'));
|
||||
+ coreUrlOpt.value('https://github.com/AdguardTeam/AdGuardHome/releases/download/${Cloud_Version}/AdGuardHome_linux_${Arch}.tar.gz', _('GitHub Releases (Original)'));
|
||||
+ coreUrlOpt.default = 'https://static.adtidy.org/adguardhome/release/AdGuardHome_linux_${Arch}.tar.gz';
|
||||
+ coreUrlOpt.rmempty = false;
|
||||
+ coreUrlOpt.depends('enable_core_update', '1');
|
||||
+ coreUrlOpt.retain = true;
|
||||
+
|
||||
+ const updateActionOpt = mainSect.taboption(
|
||||
+ 'core_update',
|
||||
+ form.DummyValue,
|
||||
+ '_update_action',
|
||||
+ _('Action')
|
||||
+ );
|
||||
+ updateActionOpt.rawhtml = true;
|
||||
+ updateActionOpt.cfgvalue = () => `
|
||||
+ <div id="agh-update-controls" style="display: flex; gap: 10px; margin-bottom: 10px;">
|
||||
+ <button class="btn cbi-button cbi-button-apply" type="button" id="btn-agh-update">${_('Update core version')}</button>
|
||||
+ <button class="btn cbi-button cbi-button-apply" type="button" id="btn-agh-force" style="display: none;">${_('Force update')}</button>
|
||||
+ </div>
|
||||
+ <div id="agh-update-log-container" style="display: none;">
|
||||
+ <textarea id="agh-update-log" class="cbi-input-textarea" style="width: 100%; display: block; font-family: monospace;" rows="10" readonly="readonly"></textarea>
|
||||
+ </div>
|
||||
+ `;
|
||||
+ updateActionOpt.depends('enable_core_update', '1');
|
||||
+ // ==========================================
|
||||
+
|
||||
const rendered = await map.render();
|
||||
|
||||
const statusNode = map.findElement('data-field', statusOpt.cbid('status_section'));
|
||||
poll.add(updateStatus(statusNode), POLL_INTERVAL);
|
||||
|
||||
+ // ========== Update the status and polling logic =========
|
||||
+ let updatePollId = null;
|
||||
+
|
||||
+ function startLogPolling() {
|
||||
+ if (updatePollId) clearInterval(updatePollId);
|
||||
+ const pollAction = () => {
|
||||
+ const btnU = document.getElementById('btn-agh-update');
|
||||
+ const btnF = document.getElementById('btn-agh-force');
|
||||
+ const logC = document.getElementById('agh-update-log-container');
|
||||
+ const logT = document.getElementById('agh-update-log');
|
||||
+
|
||||
+ if (btnU) btnU.disabled = true;
|
||||
+ if (btnF) btnF.style.display = 'inline-block';
|
||||
+ if (logC) logC.style.display = 'block';
|
||||
+
|
||||
+ fs.read('/tmp/AdGuardHome_update.log').then((txt) => {
|
||||
+ if (txt && logT) {
|
||||
+ logT.value = txt;
|
||||
+ logT.scrollTop = logT.scrollHeight;
|
||||
+ }
|
||||
+ }).catch(() => {});
|
||||
+
|
||||
+ Promise.all([
|
||||
+ fs.stat('/var/run/update_core').catch(() => null),
|
||||
+ fs.stat('/var/run/update_core_done').catch(() => null),
|
||||
+ fs.stat('/var/run/update_core_error').catch(() => null)
|
||||
+ ]).then(([isCore, isDone, isError]) => {
|
||||
+ if (isDone) {
|
||||
+ clearInterval(updatePollId);
|
||||
+ fs.remove('/var/run/update_core_done').catch(() => {});
|
||||
+ if (btnU) {
|
||||
+ btnU.disabled = false;
|
||||
+ btnU.textContent = _('Updated');
|
||||
+ }
|
||||
+ } else if (isError) {
|
||||
+ clearInterval(updatePollId);
|
||||
+ if (btnU) {
|
||||
+ btnU.disabled = false;
|
||||
+ btnU.textContent = _('Failed');
|
||||
+ }
|
||||
+ } else if (!isCore && !isDone && !isError) {
|
||||
+ clearInterval(updatePollId);
|
||||
+ if (btnU) {
|
||||
+ btnU.disabled = false;
|
||||
+ btnU.textContent = _('Already up-to-date');
|
||||
+ }
|
||||
+ }
|
||||
+ });
|
||||
+ };
|
||||
+
|
||||
+ pollAction();
|
||||
+ updatePollId = setInterval(pollAction, 1500);
|
||||
+ }
|
||||
+
|
||||
+ function applyUpdate(isForce) {
|
||||
+ const btnU = document.getElementById('btn-agh-update');
|
||||
+ const logC = document.getElementById('agh-update-log-container');
|
||||
+ const logT = document.getElementById('agh-update-log');
|
||||
+ if (btnU) {
|
||||
+ btnU.textContent = _('Checking...');
|
||||
+ btnU.disabled = true;
|
||||
+ }
|
||||
+
|
||||
+ if (logC) logC.style.display = 'block';
|
||||
+ if (logT) logT.value = _('Checking and preparing...\n');
|
||||
+
|
||||
+ map.save().then(() => {
|
||||
+ const arg = isForce ? 'force' : '';
|
||||
+ fs.exec('/usr/share/AdGuardHome/update_core.sh', [arg]).catch((err) => {
|
||||
+ console.error('Failed to trigger update script:', err);
|
||||
+ });
|
||||
+ startLogPolling();
|
||||
+ }).catch((err) => {
|
||||
+ console.error('Config save failed:', err);
|
||||
+ if (btnU) {
|
||||
+ btnU.textContent = _('Save Failed');
|
||||
+ btnU.disabled = false;
|
||||
+ }
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
+ rendered.addEventListener('click', (e) => {
|
||||
+ if (e.target && e.target.id === 'btn-agh-update') {
|
||||
+ e.preventDefault();
|
||||
+ applyUpdate(false);
|
||||
+ } else if (e.target && e.target.id === 'btn-agh-force') {
|
||||
+ e.preventDefault();
|
||||
+ applyUpdate(true);
|
||||
+ }
|
||||
+ // ==== Handle front-end hash encryption logic ====
|
||||
+ else if (e.target && e.target.id === 'btn-agh-calc-hash') {
|
||||
+ e.preventDefault();
|
||||
+ const btn = e.target;
|
||||
+
|
||||
+ // Dynamically find the password input box on the page (matching elements with the suffix .hashpass)
|
||||
+ const inputs = rendered.querySelectorAll('input[type="text"], input[type="password"]');
|
||||
+ let passInput = null;
|
||||
+ for (const el of inputs) {
|
||||
+ if (el.id && el.id.endsWith('.hashpass')) {
|
||||
+ passInput = el;
|
||||
+ break;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if (!passInput) return;
|
||||
+
|
||||
+ // 1. If JS has not been loaded, it will be loaded dynamically.
|
||||
+ if (typeof window.TwinBcrypt === 'undefined') {
|
||||
+ btn.disabled = true;
|
||||
+ btn.textContent = _('Loading...');
|
||||
+
|
||||
+ const script = document.createElement('script');
|
||||
+ script.src = L.resource('view/adguardhome/twin-bcrypt.min.js');
|
||||
+ script.type = 'text/javascript';
|
||||
+
|
||||
+ script.onload = () => {
|
||||
+ btn.textContent = _('Click here to Calculate');
|
||||
+ btn.disabled = false;
|
||||
+ };
|
||||
+ script.onerror = () => {
|
||||
+ btn.textContent = _(' ... Load Error ... ');
|
||||
+ btn.disabled = false;
|
||||
+ };
|
||||
+ document.head.appendChild(script);
|
||||
+ }
|
||||
+ // 2. If JS has been loaded, the calculation will be executed.
|
||||
+ else {
|
||||
+ if (passInput.value) {
|
||||
+ // Prevent repeated hashing of hashed strings (starting with $2a$ or $2y$)
|
||||
+ if (passInput.value.startsWith('$2a$') || passInput.value.startsWith('$2y$')) {
|
||||
+ btn.textContent = _('Calculation already DONE !');
|
||||
+ return;
|
||||
+ }
|
||||
+
|
||||
+ const hash = window.TwinBcrypt.hashSync(passInput.value);
|
||||
+ passInput.value = hash;
|
||||
+
|
||||
+ // Manually trigger native input and change events
|
||||
+ passInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
+ passInput.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
+
|
||||
+ btn.textContent = _(' ... Click Save/Apply ↘️ ... ');
|
||||
+ } else {
|
||||
+ btn.textContent = _(' ... Nothing inputted yet ... ');
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ // ==========================================
|
||||
+ });
|
||||
+
|
||||
+ Promise.all([
|
||||
+ fs.stat('/var/run/update_core').catch(() => null),
|
||||
+ fs.stat('/var/run/update_core_error').catch(() => null)
|
||||
+ ]).then(([isCore, isError]) => {
|
||||
+ if (isCore || isError) {
|
||||
+ const btnU = document.getElementById('btn-agh-update');
|
||||
+ if (btnU) btnU.textContent = _('Checking...');
|
||||
+ startLogPolling();
|
||||
+ }
|
||||
+ });
|
||||
+ // ==========================================
|
||||
+
|
||||
return rendered;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
From 04b0fc3af8ce6b1277143093f1038493bfb3a3cd Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Mon, 1 Jun 2026 16:02:48 +0800
|
||||
Subject: [PATCH] Add configuration options for AdGuard Home
|
||||
|
||||
---
|
||||
root/etc/config/adguardhome | 4 ++++
|
||||
1 file changed, 4 insertions(+)
|
||||
|
||||
diff --git a/root/etc/config/adguardhome b/root/etc/config/adguardhome
|
||||
index 92d86e1..598f316 100644
|
||||
--- a/root/etc/config/adguardhome
|
||||
+++ b/root/etc/config/adguardhome
|
||||
@@ -7,6 +7,10 @@ config adguardhome 'config'
|
||||
option user 'adguardhome'
|
||||
option group 'adguardhome'
|
||||
option verbose '0'
|
||||
+ option enabled '0'
|
||||
+ option redirect 'none'
|
||||
+ option core_version 'latest'
|
||||
+ option update_url 'https://static.adtidy.org/adguardhome/release/AdGuardHome_linux_${Arch}.tar.gz'
|
||||
|
||||
# Files and directories that AdGuard Home has read-only access to
|
||||
# list jail_mount '/etc/ssl/adguardhome.crt'
|
||||
@@ -0,0 +1,26 @@
|
||||
From c8fd7d003099e858306ad6801ad10cc158bab461 Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Fri, 29 May 2026 07:46:16 +0800
|
||||
Subject: [PATCH] Create adguardhome.nft.tpl
|
||||
|
||||
---
|
||||
root/usr/share/AdGuardHome/adguardhome.nft.tpl | 10 ++++++++++
|
||||
1 file changed, 10 insertions(+)
|
||||
create mode 100644 root/usr/share/AdGuardHome/adguardhome.nft.tpl
|
||||
|
||||
diff --git a/root/usr/share/AdGuardHome/adguardhome.nft.tpl b/root/usr/share/AdGuardHome/adguardhome.nft.tpl
|
||||
new file mode 100644
|
||||
index 0000000..d11a9dd
|
||||
--- /dev/null
|
||||
+++ b/root/usr/share/AdGuardHome/adguardhome.nft.tpl
|
||||
@@ -0,0 +1,10 @@
|
||||
+table inet adguardhome {
|
||||
+ chain prerouting {
|
||||
+ type nat hook prerouting priority dstnat; policy accept;
|
||||
+
|
||||
+ iifname { __WAN_EXCLUDES__ } return
|
||||
+
|
||||
+ fib daddr type local udp dport 53 redirect to :__AGH_PORT__
|
||||
+ fib daddr type local tcp dport 53 redirect to :__AGH_PORT__
|
||||
+ }
|
||||
+}
|
||||
@@ -0,0 +1,17 @@
|
||||
From 9510be3c2646f7c1a7701072a1eb3e9fb77f6431 Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Fri, 29 May 2026 07:33:01 +0800
|
||||
Subject: [PATCH] Create 99-adguardhome.nft
|
||||
|
||||
---
|
||||
root/etc/nftables.d/99-adguardhome.nft | 1 +
|
||||
1 file changed, 1 insertion(+)
|
||||
create mode 100644 root/etc/nftables.d/99-adguardhome.nft
|
||||
|
||||
diff --git a/root/etc/nftables.d/99-adguardhome.nft b/root/etc/nftables.d/99-adguardhome.nft
|
||||
new file mode 100644
|
||||
index 0000000..78819ad
|
||||
--- /dev/null
|
||||
+++ b/root/etc/nftables.d/99-adguardhome.nft
|
||||
@@ -0,0 +1 @@
|
||||
+include "/var/etc/adguardhome.nft"
|
||||
@@ -0,0 +1,231 @@
|
||||
From 7a73fb81100d86127e9c27ad8f14af28e6bc66e3 Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Thu, 28 May 2026 14:23:18 +0800
|
||||
Subject: [PATCH] Create update_core.sh
|
||||
|
||||
---
|
||||
root/usr/share/AdGuardHome/update_core.sh | 215 ++++++++++++++++++++++
|
||||
1 file changed, 215 insertions(+)
|
||||
create mode 100644 root/usr/share/AdGuardHome/update_core.sh
|
||||
|
||||
diff --git a/root/usr/share/AdGuardHome/update_core.sh b/root/usr/share/AdGuardHome/update_core.sh
|
||||
new file mode 100644
|
||||
index 0000000..bd80165
|
||||
--- /dev/null
|
||||
+++ b/root/usr/share/AdGuardHome/update_core.sh
|
||||
@@ -0,0 +1,215 @@
|
||||
+#!/bin/sh
|
||||
+
|
||||
+# === Core security: self-background operation mechanism ===
|
||||
+if [ "$1" != "bg_run" ]; then
|
||||
+ rm -f /var/run/update_core* /tmp/AdGuardHome_update.log
|
||||
+ touch /var/run/update_core
|
||||
+ /usr/share/AdGuardHome/update_core.sh bg_run "$1" </dev/null >/tmp/AdGuardHome_update.log 2>&1 &
|
||||
+ exit 0
|
||||
+fi
|
||||
+
|
||||
+shift
|
||||
+# ==========================================================
|
||||
+
|
||||
+PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
+binpath="/usr/bin/AdGuardHome"
|
||||
+update_mode=$1
|
||||
+
|
||||
+core_version=$(uci get adguardhome.config.core_version 2>/dev/null || true)
|
||||
+update_url=$(uci get adguardhome.config.update_url 2>/dev/null || true)
|
||||
+
|
||||
+case "${core_version}" in
|
||||
+beta)
|
||||
+ core_api_url=https://api.github.com/repos/AdguardTeam/AdGuardHome/releases
|
||||
+ ;;
|
||||
+*)
|
||||
+ core_api_url=https://api.github.com/repos/AdguardTeam/AdGuardHome/releases/latest
|
||||
+ ;;
|
||||
+esac
|
||||
+
|
||||
+EXIT(){
|
||||
+ rm -rf /var/run/update_core /tmp/AdGuardHome_Update 2>/dev/null
|
||||
+ if [ "$1" != "0" ]; then
|
||||
+ touch /var/run/update_core_error
|
||||
+ fi
|
||||
+ exit $1
|
||||
+}
|
||||
+
|
||||
+trap "EXIT 1" SIGTERM SIGINT
|
||||
+
|
||||
+rm -rf /var/run/update_core_error /var/run/update_core_done 2>/dev/null
|
||||
+touch /var/run/update_core
|
||||
+
|
||||
+Check_Task(){
|
||||
+ running_tasks="$(ps w | grep -v grep | grep 'AdGuardHome' | grep 'update_core' | wc -l)"
|
||||
+ case $1 in
|
||||
+ force)
|
||||
+ echo "Force update requested"
|
||||
+ echo "Killing ${running_tasks} running tasks ..."
|
||||
+ ps w | grep -v grep | grep -v $$ | grep 'AdGuardHome' | grep 'update_core' | awk '{print $1}' | xargs kill -9 2>/dev/null
|
||||
+ ;;
|
||||
+ *)
|
||||
+ [ "${running_tasks}" -gt 2 ] && echo -e "There are ${running_tasks} update tasks already running. Please wait or stop them manually." && EXIT 2
|
||||
+ ;;
|
||||
+ esac
|
||||
+}
|
||||
+
|
||||
+Check_Downloader() {
|
||||
+ if command -v curl >/dev/null 2>&1; then
|
||||
+ PKG="curl"
|
||||
+ return
|
||||
+ fi
|
||||
+
|
||||
+ if command -v wget >/dev/null 2>&1; then
|
||||
+ PKG="wget"
|
||||
+ return
|
||||
+ fi
|
||||
+
|
||||
+ echo "Neither curl nor wget is installed, cannot check updates!" >&2
|
||||
+ EXIT 1
|
||||
+}
|
||||
+
|
||||
+Check_Updates(){
|
||||
+ Check_Downloader
|
||||
+ case "${PKG}" in
|
||||
+ curl)
|
||||
+ Downloader="curl -L -k -o"
|
||||
+ _Downloader="curl -s"
|
||||
+ ;;
|
||||
+ wget)
|
||||
+ Downloader="wget --no-check-certificate -T 5 -O"
|
||||
+ _Downloader="wget -q -O -"
|
||||
+ ;;
|
||||
+ esac
|
||||
+ echo "[${PKG}] Checking for updates ..."
|
||||
+ Cloud_Version="$(${_Downloader} ${core_api_url} 2>/dev/null | grep 'tag_name' | egrep -o "v[0-9].+[0-9.]" | awk 'NR==1')"
|
||||
+ if [ -z "${Cloud_Version}" ]; then
|
||||
+ echo "Failed to check updates, please check network." >&2
|
||||
+ EXIT 1
|
||||
+ fi
|
||||
+
|
||||
+ if [ -f "${binpath}" ]; then
|
||||
+ Current_Version="$(${binpath} --version 2>/dev/null | egrep -o "v[0-9].+[0-9]" | sed -r 's/(.*), c(.*)/\1/')"
|
||||
+ else
|
||||
+ Current_Version="unknown"
|
||||
+ fi
|
||||
+ [ -z "${Current_Version}" ] && Current_Version="unknown"
|
||||
+
|
||||
+ echo "Binary path: ${binpath%/*}"
|
||||
+ echo "Current version: ${Current_Version}"
|
||||
+ echo "Latest version: ${Cloud_Version}"
|
||||
+
|
||||
+ if [ ! "${Cloud_Version}" = "${Current_Version}" ] || [ "$1" = force ]; then
|
||||
+ Update_Core || EXIT 1
|
||||
+ else
|
||||
+ echo "Already up to date."
|
||||
+ EXIT 0
|
||||
+ fi
|
||||
+ EXIT 0
|
||||
+}
|
||||
+
|
||||
+Update_Core(){
|
||||
+ rm -rf "/tmp/AdGuardHome_Update" > /dev/null 2>&1
|
||||
+ mkdir -p "/tmp/AdGuardHome_Update" || { echo "Unable to create a temporary directory"; EXIT 1; }
|
||||
+
|
||||
+ GET_Arch
|
||||
+ eval link="${update_url}"
|
||||
+ echo "Download link: ${link}"
|
||||
+ echo "File name: ${link##*/}"
|
||||
+ echo "Downloading AdGuardHome core ..."
|
||||
+
|
||||
+ if ! $Downloader "/tmp/AdGuardHome_Update/${link##*/}" "${link}"; then
|
||||
+ echo "Download failed."
|
||||
+ rm -rf "/tmp/AdGuardHome_Update"
|
||||
+ EXIT 1
|
||||
+ fi
|
||||
+
|
||||
+ if [ "${link##*.}" = "gz" ]; then
|
||||
+ echo "Extracting AdGuardHome ..."
|
||||
+ if ! tar -zxf "/tmp/AdGuardHome_Update/${link##*/}" -C "/tmp/AdGuardHome_Update/"; then
|
||||
+ echo "Extraction failed!"
|
||||
+ rm -rf "/tmp/AdGuardHome_Update"
|
||||
+ EXIT 1
|
||||
+ fi
|
||||
+ if [ ! -e "/tmp/AdGuardHome_Update/AdGuardHome/AdGuardHome" ]; then
|
||||
+ echo "Extraction failed: binary not found!"
|
||||
+ rm -rf "/tmp/AdGuardHome_Update"
|
||||
+ EXIT 1
|
||||
+ fi
|
||||
+ downloadbin="/tmp/AdGuardHome_Update/AdGuardHome/AdGuardHome"
|
||||
+ else
|
||||
+ downloadbin="/tmp/AdGuardHome_Update/${link##*/}"
|
||||
+ fi
|
||||
+
|
||||
+ chmod +x "${downloadbin}" 2>/dev/null || true
|
||||
+ echo "Core size: $(awk 'BEGIN{printf "%.2fMB\n",'$((`ls -l $downloadbin | awk '{print $5}'`))'/1000000}')"
|
||||
+
|
||||
+ /etc/init.d/AdGuardHome stop > /dev/null 2>&1
|
||||
+ echo "Moving AdGuardHome binary to ${binpath%/*} ..."
|
||||
+
|
||||
+ if ! mv -f "${downloadbin}" "${binpath}"; then
|
||||
+ echo -e "The core movement failed! \nIt may be caused by insufficient space."
|
||||
+ rm -rf "/tmp/AdGuardHome_Update"
|
||||
+ EXIT 1
|
||||
+ fi
|
||||
+
|
||||
+ rm -rf /tmp/AdGuardHome_Update
|
||||
+ chmod +x ${binpath}
|
||||
+ echo "Restarting AdGuardHome service ..."
|
||||
+ /etc/init.d/adguardhome restart > /dev/null 2>&1
|
||||
+
|
||||
+ echo "AdGuardHome core updated successfully!"
|
||||
+ touch /var/run/update_core_done
|
||||
+ EXIT 0
|
||||
+}
|
||||
+
|
||||
+GET_Arch() {
|
||||
+ Archt="$(uname -m)"
|
||||
+ case "${Archt}" in
|
||||
+ i386|i686)
|
||||
+ Arch="i386"
|
||||
+ ;;
|
||||
+ x86_64|amd64)
|
||||
+ Arch="amd64"
|
||||
+ ;;
|
||||
+ mipsel|mipsel*)
|
||||
+ Arch="mipsle_softfloat"
|
||||
+ ;;
|
||||
+ mips|mips*)
|
||||
+ Arch="mips_softfloat"
|
||||
+ ;;
|
||||
+ mips64el)
|
||||
+ Arch="mips64le_softfloat"
|
||||
+ ;;
|
||||
+ mips64)
|
||||
+ Arch="mips64_softfloat"
|
||||
+ ;;
|
||||
+ armv5*|armv5l|armv5tel)
|
||||
+ Arch="armv5"
|
||||
+ ;;
|
||||
+ armv6*|armv6l)
|
||||
+ Arch="armv6"
|
||||
+ ;;
|
||||
+ armv7*|armv7l)
|
||||
+ Arch="armv7"
|
||||
+ ;;
|
||||
+ arm|armhf)
|
||||
+ Arch="armv7"
|
||||
+ ;;
|
||||
+ aarch64)
|
||||
+ Arch="arm64"
|
||||
+ ;;
|
||||
+ *)
|
||||
+ echo "Unsupported architecture: [${Archt}]"
|
||||
+ EXIT 1
|
||||
+ esac
|
||||
+ echo "Detected architecture: ${Arch}"
|
||||
+}
|
||||
+
|
||||
+
|
||||
+main(){
|
||||
+ Check_Task ${update_mode}
|
||||
+ Check_Updates ${update_mode}
|
||||
+}
|
||||
+
|
||||
+main
|
||||
@@ -0,0 +1,239 @@
|
||||
From 0e2440feea5f51d187189aae41d45d15102773bc Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Sun, 31 May 2026 09:38:02 +0800
|
||||
Subject: [PATCH] Create control.sh
|
||||
|
||||
---
|
||||
root/usr/share/AdGuardHome/control.sh | 223 ++++++++++++++++++++++++++
|
||||
1 file changed, 223 insertions(+)
|
||||
create mode 100644 root/usr/share/AdGuardHome/control.sh
|
||||
|
||||
diff --git a/root/usr/share/AdGuardHome/control.sh b/root/usr/share/AdGuardHome/control.sh
|
||||
new file mode 100644
|
||||
index 0000000..1686183
|
||||
--- /dev/null
|
||||
+++ b/root/usr/share/AdGuardHome/control.sh
|
||||
@@ -0,0 +1,223 @@
|
||||
+#!/bin/sh
|
||||
+# /usr/share/AdGuardHome/control.sh
|
||||
+. /usr/share/AdGuardHome/helper.sh
|
||||
+
|
||||
+ENABLED="$1"
|
||||
+CONFIGURATION="adguardhome"
|
||||
+NFT_RULES_TPL="/usr/share/AdGuardHome/adguardhome.nft.tpl"
|
||||
+NFT_RULES_FILE="/var/etc/adguardhome.nft"
|
||||
+NFT_TABLE="adguardhome"
|
||||
+
|
||||
+set_nft_redirect() {
|
||||
+ local port="$1"
|
||||
+ [ -z "$port" ] && return 1
|
||||
+ [ ! -f "$NFT_RULES_TPL" ] && return 1
|
||||
+
|
||||
+ local wan_section_name
|
||||
+ wan_section_name=$(uci show firewall | awk -F'.' '/\.name='\''wan'\''$/ {print $2}')
|
||||
+ local WAN_IFS=""
|
||||
+ if [ -n "$wan_section_name" ]; then
|
||||
+ WAN_IFS=$(uci get firewall.$wan_section_name.network 2>/dev/null | xargs)
|
||||
+ fi
|
||||
+
|
||||
+ local WAN_NFT_SET=""
|
||||
+ for IF in $WAN_IFS; do
|
||||
+ WAN_NFT_SET="${WAN_NFT_SET} \"$IF\","
|
||||
+ done
|
||||
+ WAN_NFT_SET=$(echo "$WAN_NFT_SET" | sed 's/,$//')
|
||||
+
|
||||
+ sed -e "s/__WAN_EXCLUDES__/${WAN_NFT_SET}/g" -e "s/__AGH_PORT__/${port}/g" "$NFT_RULES_TPL" > "$NFT_RULES_FILE"
|
||||
+
|
||||
+ nft delete table inet "$NFT_TABLE" 2>/dev/null
|
||||
+ fw4 reload
|
||||
+ logger -t adguardhome "nft table $NFT_TABLE applied on port $port, WAN excludes: $WAN_NFT_SET"
|
||||
+}
|
||||
+
|
||||
+clear_nft_redirect() {
|
||||
+ # 优化:只有当表存在时才清理和重载防火墙,避免无意义的性能开销
|
||||
+ if nft list table inet "$NFT_TABLE" >/dev/null 2>&1; then
|
||||
+ [ -f "$NFT_RULES_FILE" ] && > "$NFT_RULES_FILE"
|
||||
+ nft delete table inet "$NFT_TABLE" 2>/dev/null
|
||||
+ fw4 reload
|
||||
+ logger -t adguardhome "nft table $NFT_TABLE cleared"
|
||||
+ fi
|
||||
+}
|
||||
+
|
||||
+set_forward_dnsmasq() {
|
||||
+ local PORT="$1"
|
||||
+ local addr="127.0.0.1#$PORT"
|
||||
+ local OLD_SERVER
|
||||
+ OLD_SERVER="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)"
|
||||
+ echo "$OLD_SERVER" | grep -q "^$addr" && return 0
|
||||
+
|
||||
+ uci delete dhcp.@dnsmasq[0].server 2>/dev/null
|
||||
+ uci add_list dhcp.@dnsmasq[0].server="$addr"
|
||||
+ uci delete dhcp.@dnsmasq[0].resolvfile 2>/dev/null
|
||||
+ uci set dhcp.@dnsmasq[0].noresolv=1
|
||||
+ uci commit dhcp
|
||||
+ /etc/init.d/dnsmasq restart >/dev/null 2>&1
|
||||
+}
|
||||
+
|
||||
+stop_forward_dnsmasq() {
|
||||
+ local OLD_PORT="$1"
|
||||
+ local addr="127.0.0.1#$OLD_PORT"
|
||||
+ local OLD_SERVER
|
||||
+ OLD_SERVER="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)"
|
||||
+ echo "$OLD_SERVER" | grep -q "^$addr" || return 0
|
||||
+
|
||||
+ uci del_list dhcp.@dnsmasq[0].server="$addr" 2>/dev/null
|
||||
+ local addrlist
|
||||
+ addrlist="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)"
|
||||
+ if [ -z "$addrlist" ]; then
|
||||
+ local resolvfile="/tmp/resolv.conf.d/resolv.conf.auto"
|
||||
+ [ ! -f "$resolvfile" ] && resolvfile="/tmp/resolv.conf.auto"
|
||||
+ uci set dhcp.@dnsmasq[0].resolvfile="$resolvfile" 2>/dev/null
|
||||
+ uci delete dhcp.@dnsmasq[0].noresolv 2>/dev/null
|
||||
+ fi
|
||||
+ uci commit dhcp
|
||||
+ /etc/init.d/dnsmasq restart >/dev/null 2>&1
|
||||
+}
|
||||
+
|
||||
+agh_reload() {
|
||||
+ ubus call service event '{"type":"config.change","data":{"package":"adguardhome"}}' >/dev/null 2>&1
|
||||
+}
|
||||
+
|
||||
+rm_port53() {
|
||||
+ local configpath
|
||||
+ configpath=$(uci -q get adguardhome.config.config_file)
|
||||
+ [ -z "$configpath" ] && configpath="/etc/adguardhome/adguardhome.yaml"
|
||||
+
|
||||
+ local adguardhome_PORT dnsmasq_port
|
||||
+ adguardhome_PORT=$(config_editor "dns.port" "" "$configpath" "1")
|
||||
+ dnsmasq_port=$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)
|
||||
+ [ -z "$dnsmasq_port" ] && dnsmasq_port="53"
|
||||
+
|
||||
+ if [ "$dnsmasq_port" = "$adguardhome_PORT" ]; then
|
||||
+ [ "$dnsmasq_port" = "53" ] && dnsmasq_port="1745"
|
||||
+ elif [ "$dnsmasq_port" = "53" ]; then
|
||||
+ return
|
||||
+ fi
|
||||
+
|
||||
+ config_editor "dns.port" "$dnsmasq_port" "$configpath"
|
||||
+ uci set dhcp.@dnsmasq[0].port="53"
|
||||
+ uci commit dhcp
|
||||
+
|
||||
+ /etc/init.d/dnsmasq restart
|
||||
+ agh_reload
|
||||
+}
|
||||
+
|
||||
+use_port53() {
|
||||
+ local configpath
|
||||
+ configpath=$(uci -q get adguardhome.config.config_file)
|
||||
+ [ -z "$configpath" ] && configpath="/etc/adguardhome/adguardhome.yaml"
|
||||
+
|
||||
+ local adguardhome_PORT dnsmasq_port
|
||||
+ adguardhome_PORT=$(config_editor "dns.port" "" "$configpath" "1")
|
||||
+ dnsmasq_port=$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)
|
||||
+ [ -z "$dnsmasq_port" ] && dnsmasq_port="53"
|
||||
+
|
||||
+ if [ "$dnsmasq_port" = "$adguardhome_PORT" ]; then
|
||||
+ [ "$dnsmasq_port" = "53" ] && adguardhome_PORT="1745"
|
||||
+ elif [ "$adguardhome_PORT" = "53" ]; then
|
||||
+ return
|
||||
+ fi
|
||||
+
|
||||
+ config_editor "dns.port" "53" "$configpath"
|
||||
+ uci set dhcp.@dnsmasq[0].port="$adguardhome_PORT"
|
||||
+ uci commit dhcp
|
||||
+ /etc/init.d/dnsmasq restart
|
||||
+ agh_reload
|
||||
+}
|
||||
+
|
||||
+mark_redirect_flag() {
|
||||
+ local enabled="$1" redirect="$2" agh_port="$3"
|
||||
+ local configpath flag=0
|
||||
+ configpath=$(uci -q get adguardhome.config.config_file)
|
||||
+ [ -z "$configpath" ] && configpath="/etc/adguardhome/adguardhome.yaml"
|
||||
+
|
||||
+ if [ "$enabled" = "1" ] && [ "$redirect" != "none" ]; then
|
||||
+ flag=1
|
||||
+ if [ "$redirect" = "redirect" ]; then
|
||||
+ nft list table inet "$NFT_TABLE" >/dev/null 2>&1 || flag=0
|
||||
+ elif [ "$redirect" = "dnsmasq-upstream" ]; then
|
||||
+ local s
|
||||
+ s="$(uci -q get dhcp.@dnsmasq[0].server | tr '\n' ' ')"
|
||||
+ printf '%s\n' "$s" | grep -q -E "(^|[[:space:]])127\.0\.0\.1#${agh_port}([[:space:]]|$)" || flag=0
|
||||
+ elif [ "$redirect" = "exchange" ]; then
|
||||
+ local cfgp dport
|
||||
+ cfgp="$(config_editor "dns.port" "" "$configpath" "1")"
|
||||
+ dport="$(uci -q get dhcp.@dnsmasq[0].port)"
|
||||
+ if [ "$cfgp" != "53" ] || [ "$dport" = "53" ] ; then flag=0; fi
|
||||
+ fi
|
||||
+ fi
|
||||
+ echo -n "$flag" > /var/run/AdGredir
|
||||
+}
|
||||
+
|
||||
+_do_redirect() {
|
||||
+ local enabled="$1"
|
||||
+ local configpath
|
||||
+ configpath=$(uci -q get adguardhome.config.config_file)
|
||||
+ [ -z "$configpath" ] && configpath="/etc/adguardhome/adguardhome.yaml"
|
||||
+
|
||||
+ local adguardhome_PORT redirect old_redirect old_port old_enabled
|
||||
+ adguardhome_PORT="$(config_editor "dns.port" "" "$configpath" "1")"
|
||||
+ [ -z "$adguardhome_PORT" ] && adguardhome_PORT="0"
|
||||
+
|
||||
+ redirect=$(uci -q get adguardhome.config.redirect || echo "none")
|
||||
+
|
||||
+ # 【核心修复】:从内存文件中读取旧状态,彻底放弃污染 UCI
|
||||
+ if [ -f /var/run/adguardhome.state ]; then
|
||||
+ . /var/run/adguardhome.state
|
||||
+ else
|
||||
+ old_redirect="none"
|
||||
+ old_port="0"
|
||||
+ old_enabled="0"
|
||||
+ fi
|
||||
+
|
||||
+ uci get dhcp.@dnsmasq[0].port >/dev/null 2>&1 || uci set dhcp.@dnsmasq[0].port="53"
|
||||
+ uci commit dhcp
|
||||
+
|
||||
+ if [ "$old_enabled" = "1" ] && [ "$old_redirect" = "exchange" ]; then
|
||||
+ adguardhome_PORT="$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)"
|
||||
+ fi
|
||||
+
|
||||
+ if [ "$old_redirect" != "$redirect" ] || [ "$old_port" != "$adguardhome_PORT" ] || { [ "$old_enabled" = "1" ] && [ "$enabled" = "0" ]; }; then
|
||||
+ if [ "$old_redirect" != "none" ]; then
|
||||
+ if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then
|
||||
+ clear_nft_redirect
|
||||
+ elif [ "$old_redirect" = "dnsmasq-upstream" ]; then
|
||||
+ stop_forward_dnsmasq "$old_port"
|
||||
+ elif [ "$old_redirect" = "exchange" ]; then
|
||||
+ rm_port53
|
||||
+ fi
|
||||
+ fi
|
||||
+ elif [ "$old_enabled" = "1" ] && [ "$enabled" = "1" ]; then
|
||||
+ if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then
|
||||
+ clear_nft_redirect
|
||||
+ fi
|
||||
+ fi
|
||||
+
|
||||
+ # 【核心修复】:将当前状态覆盖写入内存文件,轻量、疾速、安全!
|
||||
+ cat <<EOF > /var/run/adguardhome.state
|
||||
+old_redirect="$redirect"
|
||||
+old_port="$adguardhome_PORT"
|
||||
+old_enabled="$enabled"
|
||||
+EOF
|
||||
+
|
||||
+ if [ "$enabled" = "0" ] || [ "$adguardhome_PORT" = "0" ]; then
|
||||
+ echo -n "0" > /var/run/AdGredir
|
||||
+ return 1
|
||||
+ fi
|
||||
+
|
||||
+ if [ "$redirect" = "redirect" ]; then
|
||||
+ set_nft_redirect "$adguardhome_PORT"
|
||||
+ elif [ "$redirect" = "dnsmasq-upstream" ]; then
|
||||
+ set_forward_dnsmasq "$adguardhome_PORT"
|
||||
+ elif [ "$redirect" = "exchange" ] && [ "$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)" = "53" ]; then
|
||||
+ use_port53
|
||||
+ fi
|
||||
+
|
||||
+ mark_redirect_flag "$enabled" "$redirect" "$adguardhome_PORT"
|
||||
+}
|
||||
+
|
||||
+_do_redirect "$ENABLED"
|
||||
@@ -0,0 +1,34 @@
|
||||
From b1391765b484c6f50feba7f09bdf50d3aa443356 Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Fri, 29 May 2026 14:27:56 +0800
|
||||
Subject: [PATCH] Add watchconfig.sh to monitor AdGuardHome config
|
||||
|
||||
---
|
||||
root/usr/share/AdGuardHome/watchconfig.sh | 18 ++++++++++++++++++
|
||||
1 file changed, 18 insertions(+)
|
||||
create mode 100644 root/usr/share/AdGuardHome/watchconfig.sh
|
||||
|
||||
diff --git a/root/usr/share/AdGuardHome/watchconfig.sh b/root/usr/share/AdGuardHome/watchconfig.sh
|
||||
new file mode 100644
|
||||
index 0000000..c7267bf
|
||||
--- /dev/null
|
||||
+++ b/root/usr/share/AdGuardHome/watchconfig.sh
|
||||
@@ -0,0 +1,18 @@
|
||||
+#!/bin/sh
|
||||
+# /usr/share/AdGuardHome/watchconfig.sh
|
||||
+
|
||||
+PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
+
|
||||
+configpath=$(uci -q get adguardhome.config.config_file)
|
||||
+[ -z "$configpath" ] && configpath="/etc/adguardhome/adguardhome.yaml"
|
||||
+
|
||||
+while :
|
||||
+do
|
||||
+ sleep 10
|
||||
+ if [ -f "$configpath" ]; then
|
||||
+ /etc/init.d/adguardhome do_redirect 1
|
||||
+ break
|
||||
+ fi
|
||||
+done
|
||||
+
|
||||
+exit 0
|
||||
@@ -0,0 +1,38 @@
|
||||
From 864c042e5cdbe61922f917d1785bf7342b3fc90d Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Sun, 31 May 2026 08:25:55 +0800
|
||||
Subject: [PATCH] Create helper.sh for YAML reading and writing
|
||||
|
||||
Added a helper script for YAML configuration management.
|
||||
---
|
||||
root/usr/share/AdGuardHome/helper.sh | 21 +++++++++++++++++++++
|
||||
1 file changed, 21 insertions(+)
|
||||
create mode 100644 root/usr/share/AdGuardHome/helper.sh
|
||||
|
||||
diff --git a/root/usr/share/AdGuardHome/helper.sh b/root/usr/share/AdGuardHome/helper.sh
|
||||
new file mode 100644
|
||||
index 0000000..35f0198
|
||||
--- /dev/null
|
||||
+++ b/root/usr/share/AdGuardHome/helper.sh
|
||||
@@ -0,0 +1,21 @@
|
||||
+#!/bin/sh
|
||||
+# Auxiliary functions specially used for reading and writing YAML
|
||||
+config_editor() {
|
||||
+ awk -v yaml="$1" -v value="$2" -v file="$3" -v ro="$4" '
|
||||
+ BEGIN{split(yaml,part,"\.");s="";i=1;l=length(part);}
|
||||
+ {
|
||||
+ if (match($0,s""part[i]":")) {
|
||||
+ if (i==l) {
|
||||
+ split($0,t,": ");
|
||||
+ if (ro==""){
|
||||
+ system("sed -i '\''"FNR"c\\" t[1] ": " value "'\'' " file " >/dev/null 2>&1");
|
||||
+ } else {
|
||||
+ print(t[2]);
|
||||
+ }
|
||||
+ exit;
|
||||
+ }
|
||||
+ s=s"[- ]{2}";
|
||||
+ i++;
|
||||
+ }
|
||||
+ }' "$3"
|
||||
+}
|
||||
@@ -0,0 +1,324 @@
|
||||
From 2cc00f18ce38e8c39af30ce5381f229e3c742adf Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Sat, 30 May 2026 17:40:57 +0800
|
||||
Subject: [PATCH] Add initial configuration for AdGuard Home
|
||||
|
||||
---
|
||||
root/etc/adguardhome/adguardhome.yaml | 308 ++++++++++++++++++++++++++
|
||||
1 file changed, 308 insertions(+)
|
||||
create mode 100644 root/etc/adguardhome/adguardhome.yaml
|
||||
|
||||
diff --git a/root/etc/adguardhome/adguardhome.yaml b/root/etc/adguardhome/adguardhome.yaml
|
||||
new file mode 100644
|
||||
index 0000000..f7a1a94
|
||||
--- /dev/null
|
||||
+++ b/root/etc/adguardhome/adguardhome.yaml
|
||||
@@ -0,0 +1,308 @@
|
||||
+http:
|
||||
+ pprof:
|
||||
+ port: 6060
|
||||
+ enabled: false
|
||||
+ doh:
|
||||
+ routes:
|
||||
+ - GET /dns-query
|
||||
+ - POST /dns-query
|
||||
+ - GET /dns-query/{ClientID}
|
||||
+ - POST /dns-query/{ClientID}
|
||||
+ insecure_enabled: false
|
||||
+ address: 0.0.0.0:3008
|
||||
+ session_ttl: 30d
|
||||
+users:
|
||||
+ - name: root
|
||||
+ password:
|
||||
+auth_attempts: 5
|
||||
+block_auth_min: 15
|
||||
+http_proxy: ""
|
||||
+language: ""
|
||||
+theme: auto
|
||||
+dns:
|
||||
+ bind_hosts:
|
||||
+ - 0.0.0.0
|
||||
+ port: 1745
|
||||
+ anonymize_client_ip: false
|
||||
+ ratelimit: 0
|
||||
+ ratelimit_subnet_len_ipv4: 24
|
||||
+ ratelimit_subnet_len_ipv6: 56
|
||||
+ ratelimit_whitelist: []
|
||||
+ refuse_any: true
|
||||
+ upstream_dns:
|
||||
+ - '#https://dns10.quad9.net/dns-query'
|
||||
+ - 223.5.5.5
|
||||
+ - 2400:3200::1
|
||||
+ - https://dns.alidns.com/dns-query
|
||||
+ upstream_dns_file: ""
|
||||
+ bootstrap_dns:
|
||||
+ - 119.29.29.29
|
||||
+ - 114.114.114.114
|
||||
+ fallback_dns:
|
||||
+ - 119.29.29.29
|
||||
+ - 113.31.119.88
|
||||
+ - 2400:7fc0:849e:200:62fd:1de3:1c90:1
|
||||
+ upstream_mode: parallel
|
||||
+ fastest_timeout: 1s
|
||||
+ allowed_clients: []
|
||||
+ disallowed_clients: []
|
||||
+ blocked_hosts:
|
||||
+ - version.bind
|
||||
+ - id.server
|
||||
+ - hostname.bind
|
||||
+ trusted_proxies:
|
||||
+ - 127.0.0.0/8
|
||||
+ - ::1/128
|
||||
+ cache_enabled: true
|
||||
+ cache_size: 4194304
|
||||
+ cache_ttl_min: 0
|
||||
+ cache_ttl_max: 0
|
||||
+ cache_optimistic: true
|
||||
+ cache_optimistic_answer_ttl: 30s
|
||||
+ cache_optimistic_max_age: 12h
|
||||
+ bogus_nxdomain: []
|
||||
+ aaaa_disabled: false
|
||||
+ enable_dnssec: true
|
||||
+ edns_client_subnet:
|
||||
+ custom_ip: ""
|
||||
+ enabled: true
|
||||
+ use_custom: false
|
||||
+ max_goroutines: 300
|
||||
+ handle_ddr: true
|
||||
+ ipset: []
|
||||
+ ipset_file: ""
|
||||
+ bootstrap_prefer_ipv6: false
|
||||
+ upstream_timeout: 10s
|
||||
+ private_networks: []
|
||||
+ use_private_ptr_resolvers: true
|
||||
+ local_ptr_upstreams:
|
||||
+ - 127.0.0.1:1745
|
||||
+ use_dns64: false
|
||||
+ dns64_prefixes: []
|
||||
+ serve_http3: false
|
||||
+ use_http3_upstreams: false
|
||||
+ serve_plain_dns: true
|
||||
+ hostsfile_enabled: true
|
||||
+ pending_requests:
|
||||
+ enabled: true
|
||||
+tls:
|
||||
+ enabled: false
|
||||
+ server_name: ""
|
||||
+ force_https: false
|
||||
+ port_https: 443
|
||||
+ port_dns_over_tls: 853
|
||||
+ port_dns_over_quic: 853
|
||||
+ port_dnscrypt: 0
|
||||
+ dnscrypt_config_file: ""
|
||||
+ certificate_chain: ""
|
||||
+ private_key: ""
|
||||
+ certificate_path: ""
|
||||
+ private_key_path: ""
|
||||
+ strict_sni_check: false
|
||||
+querylog:
|
||||
+ dir_path: ""
|
||||
+ ignored:
|
||||
+ - orirainbow.cc^
|
||||
+ - orirainwe.edu.kg^
|
||||
+ - coriseay.edu.kg^
|
||||
+ - google.com^
|
||||
+ interval: 7d
|
||||
+ size_memory: 1000
|
||||
+ enabled: true
|
||||
+ ignored_enabled: true
|
||||
+ file_enabled: true
|
||||
+statistics:
|
||||
+ dir_path: ""
|
||||
+ ignored:
|
||||
+ - orirainbow.cc^
|
||||
+ - orirainwe.edu.kg^
|
||||
+ - coriseay.edu.kg^
|
||||
+ - google.com^
|
||||
+ interval: 7d
|
||||
+ enabled: true
|
||||
+ ignored_enabled: true
|
||||
+filters:
|
||||
+ - enabled: true
|
||||
+ url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
|
||||
+ name: AdGuard DNS filter
|
||||
+ id: 1
|
||||
+ - enabled: false
|
||||
+ url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt
|
||||
+ name: AdAway Default Blocklist
|
||||
+ id: 2
|
||||
+whitelist_filters: []
|
||||
+user_rules:
|
||||
+ - '@@||ntfy.sh^'
|
||||
+ - '@@||pubnub.com^'
|
||||
+ - '@@||clarity.ms^$important'
|
||||
+ - '@@||sh-gsg.com^$important'
|
||||
+ - '@@||sh-mtgc.com^$important'
|
||||
+ - '@@||taobao.com^$important'
|
||||
+ - '@@||tmall.com^$important'
|
||||
+ - '@@||jd.com^important'
|
||||
+ - '@@||flyme.cn^$important'
|
||||
+ - '@@||meizu.com^$important'
|
||||
+ - '@@||flydigi.com^'
|
||||
+ - '@@||pv.sohu.com^$important'
|
||||
+ - '@@||trafficmanager.net^'
|
||||
+ - '||yify.foo^'
|
||||
+ - '||www.668cbb.com/static/adScript.js'
|
||||
+ - '||topnewsfeeds.net^'
|
||||
+ - '||nbryb.com^'
|
||||
+ - '||superbdolly.com^'
|
||||
+ - '||salutationcheerlessdemote.com^'
|
||||
+ - '||buildingdoodlesquare.com^'
|
||||
+ - '||colonistnobilityheroic.com^'
|
||||
+ - '#解除 小米 及其所有子域名的封锁;'
|
||||
+ - '@@||mi.com^'
|
||||
+ - '@@||mi.cn^'
|
||||
+ - '@@||xiaomi.com^'
|
||||
+ - '@@||miui.com^'
|
||||
+ - '@@||xiaomi.net^'
|
||||
+ - '@@||xiaomi-mipdx.cn.miaozhen.com^'
|
||||
+ - '@@||xiaomi-dtv.m.cn.miaozhen.com^'
|
||||
+ - '@@||miwifi.com^'
|
||||
+ - '@@||tracker.ai.xiaomi.com^$important'
|
||||
+ - '@@||tracking.miui.com^$important'
|
||||
+ - '#解除 Apple 及其所有子域名的封锁;'
|
||||
+ - '@@||apple.com^'
|
||||
+ - '@@||apple.com.cn^'
|
||||
+ - '@@||icloud.com^'
|
||||
+ - '@@||icloud.com.cn^'
|
||||
+ - '#解除 cloudflare 及其所有子域名的封锁;'
|
||||
+ - '@@||static.cloudflareinsights.com^$important'
|
||||
+ - '@@||cloudflare.com^'
|
||||
+ - '@@||cloudflare-cn.com^'
|
||||
+ - '@@||argotunnel.com^'
|
||||
+ - '#解除 microsoft 及其所有子域名的封锁;'
|
||||
+ - '@@||microsoft.com^$important'
|
||||
+ - '@@||windows.com^$important'
|
||||
+ - '@@||microsoftonline.com^$important'
|
||||
+ - '@@||windowsupdate.com^$important'
|
||||
+ - '@@||office.com^$important'
|
||||
+ - '@@||live.com^$important'
|
||||
+ - '@@||skype.com^$important'
|
||||
+ - '@@||msn.com^$important'
|
||||
+ - '@@||bing.com^$important'
|
||||
+ - '@@||msftconnecttest.com^$important'
|
||||
+ - '@@||res.public.onecdn.static.microsoft^$important'
|
||||
+ - '@@||inference.location.live.net^$important'
|
||||
+ - '@@||static.edge.microsoftapp.net^$important'
|
||||
+ - '@@||ipv6.msftconnecttest.com^$important'
|
||||
+ - '@@||sentry.io^$important'
|
||||
+ - '@@||metrics1.data.hicloud.com^$important'
|
||||
+ - '@@||otheve.beacon.qq.com^$important'
|
||||
+ - '@@||app-measurement.com^$important'
|
||||
+ - '@@||tmdb.org^'
|
||||
+ - '@@||themoviedb.org^'
|
||||
+ - /googleads.$~script,domain=~googleads.github.io
|
||||
+ - /pagead/lvz?
|
||||
+ - '||google.com/pagead/'
|
||||
+ - '||static.doubleclick.net^$domain=youtube.com'
|
||||
+ - '||youtube.com/get_midroll_'
|
||||
+ - '||5hz.org^'
|
||||
+ - '||evewan.com^'
|
||||
+ - '||stgowan.com^'
|
||||
+ - '||51gowan.com^'
|
||||
+ - '||www.66s6.net/d/js/acmsd/thea5.js'
|
||||
+ - '||www.66s6.net/d/js/acmsd/thea6.js'
|
||||
+ - '||sogowan.com^'
|
||||
+ - '||52xkdy.com^'
|
||||
+ - '||ttzj666.com^'
|
||||
+ - '||888dyy.cc^'
|
||||
+ - '||qisaoba.com^'
|
||||
+ - '||euqq.cn^'
|
||||
+ - '||wkyaoye.com^'
|
||||
+ - '||qisaoba.com^'
|
||||
+ - '@||adobe.io^'
|
||||
+ - '@||adobestats.io'
|
||||
+ - '@||adobe.com^'
|
||||
+ - '@||gocart-web-prod-ue1-alb-1461435473.us-east-1.elb.amazonaws.com'
|
||||
+ - '@@||dig.bdurl.net^$important'
|
||||
+ - '@@||android.bugly.qq.com^$important'
|
||||
+ - '@@||yun-hl.3g.qq.com^$important'
|
||||
+ - '@@||collector.github.com^$important'
|
||||
+ - '@@||geolocation.onetrust.com^$important'
|
||||
+ - '@@||google-analytics.com^$important'
|
||||
+ - '@@||client-upload.cn-shanghai.log.aliyuncs.com^$important'
|
||||
+ - '@@||twingate.com^'
|
||||
+ - '@@||syncthing.net^'
|
||||
+ - '@@||loggw-ex.alipay.com^$important'
|
||||
+ - '@@||hdfans.org^'
|
||||
+ - '@@||1ptba.com^'
|
||||
+ - '@@||ptcafe.club^'
|
||||
+ - '||nbpicture.xyz^'
|
||||
+ - ""
|
||||
+dhcp:
|
||||
+ enabled: false
|
||||
+ interface_name: ""
|
||||
+ local_domain_name: lan
|
||||
+ dhcpv4:
|
||||
+ gateway_ip: ""
|
||||
+ subnet_mask: ""
|
||||
+ range_start: ""
|
||||
+ range_end: ""
|
||||
+ lease_duration: 86400
|
||||
+ icmp_timeout_msec: 1000
|
||||
+ options: []
|
||||
+ dhcpv6:
|
||||
+ range_start: ""
|
||||
+ lease_duration: 86400
|
||||
+ ra_slaac_only: false
|
||||
+ ra_allow_slaac: false
|
||||
+filtering:
|
||||
+ blocking_ipv4: ""
|
||||
+ blocking_ipv6: ""
|
||||
+ blocked_services:
|
||||
+ schedule:
|
||||
+ time_zone: UTC
|
||||
+ ids: []
|
||||
+ protection_disabled_until: null
|
||||
+ safe_search:
|
||||
+ enabled: false
|
||||
+ bing: true
|
||||
+ duckduckgo: true
|
||||
+ ecosia: true
|
||||
+ google: true
|
||||
+ pixabay: true
|
||||
+ yandex: true
|
||||
+ youtube: true
|
||||
+ blocking_mode: default
|
||||
+ parental_block_host: family-block.dns.adguard.com
|
||||
+ safebrowsing_block_host: standard-block.dns.adguard.com
|
||||
+ rewrites: []
|
||||
+ safe_fs_patterns:
|
||||
+ - /var/lib/adguardhome/userfilters/*
|
||||
+ safebrowsing_cache_size: 1048576
|
||||
+ safesearch_cache_size: 1048576
|
||||
+ parental_cache_size: 1048576
|
||||
+ cache_time: 30
|
||||
+ filters_update_interval: 24
|
||||
+ blocked_response_ttl: 10
|
||||
+ filtering_enabled: true
|
||||
+ rewrites_enabled: true
|
||||
+ parental_enabled: false
|
||||
+ safebrowsing_enabled: false
|
||||
+ protection_enabled: true
|
||||
+clients:
|
||||
+ runtime_sources:
|
||||
+ whois: true
|
||||
+ arp: true
|
||||
+ rdns: true
|
||||
+ dhcp: true
|
||||
+ hosts: true
|
||||
+ persistent: []
|
||||
+log:
|
||||
+ enabled: true
|
||||
+ file: ""
|
||||
+ max_backups: 0
|
||||
+ max_size: 100
|
||||
+ max_age: 3
|
||||
+ compress: false
|
||||
+ local_time: false
|
||||
+ verbose: false
|
||||
+os:
|
||||
+ group: ""
|
||||
+ user: ""
|
||||
+ rlimit_nofile: 0
|
||||
+schema_version: 34
|
||||
@@ -0,0 +1,23 @@
|
||||
From bc9e4fac2e0be80730df3aa345acf3dc2b7784d0 Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Sun, 31 May 2026 20:27:21 +0800
|
||||
Subject: [PATCH] Create twin-bcrypt.min.js
|
||||
|
||||
---
|
||||
.../resources/view/adguardhome/twin-bcrypt.min.js | 7 +++++++
|
||||
1 file changed, 7 insertions(+)
|
||||
create mode 100644 htdocs/luci-static/resources/view/adguardhome/twin-bcrypt.min.js
|
||||
|
||||
diff --git a/htdocs/luci-static/resources/view/adguardhome/twin-bcrypt.min.js b/htdocs/luci-static/resources/view/adguardhome/twin-bcrypt.min.js
|
||||
new file mode 100644
|
||||
index 0000000..3a03268
|
||||
--- /dev/null
|
||||
+++ b/htdocs/luci-static/resources/view/adguardhome/twin-bcrypt.min.js
|
||||
@@ -0,0 +1,7 @@
|
||||
+/* @license
|
||||
+ * Twin-Bcrypt 2.2.0
|
||||
+ * https://github.com/fpirsch/twin-bcrypt
|
||||
+ * Licence: BSD-3-Clause
|
||||
+ */
|
||||
+!function(r,n){"use strict";function e(r){return y[g]=t.apply(n,r),g++}function t(r){var e=[].slice.call(arguments,1);return function(){"function"==typeof r?r.apply(n,e):new Function(""+r)()}}function o(r){if(m)setTimeout(t(o,r),0);else{var n=y[r];if(n){m=!0;try{n()}finally{a(r),m=!1}}}}function a(r){delete y[r]}function i(){p=function(){var r=e(arguments);return process.nextTick(t(o,r)),r}}function u(){if(r.postMessage&&!r.importScripts){var n=!0,e=r.onmessage;return r.onmessage=function(){n=!1},r.postMessage("","*"),r.onmessage=e,n}}function f(){var n="setImmediate$"+Math.random()+"$",t=function(e){e.source===r&&"string"==typeof e.data&&0===e.data.indexOf(n)&&o(+e.data.slice(n.length))};r.addEventListener?r.addEventListener("message",t,!1):r.attachEvent("onmessage",t),p=function(){var t=e(arguments);return r.postMessage(n+t,"*"),t}}function c(){var r=new MessageChannel;r.port1.onmessage=function(r){var n=r.data;o(n)},p=function(){var n=e(arguments);return r.port2.postMessage(n),n}}function s(){var r=v.documentElement;p=function(){var n=e(arguments),t=v.createElement("script");return t.onreadystatechange=function(){o(n),t.onreadystatechange=null,r.removeChild(t),t=null},r.appendChild(t),n}}function l(){p=function(){var r=e(arguments);return setTimeout(t(o,r),0),r}}if(!r.setImmediate){var p,g=1,y={},m=!1,v=r.document,d=Object.getPrototypeOf&&Object.getPrototypeOf(r);d=d&&d.setTimeout?d:r,"[object process]"==={}.toString.call(r.process)?i():u()?f():r.MessageChannel?c():v&&"onreadystatechange"in v.createElement("script")?s():l(),d.setImmediate=p,d.clearImmediate=a}}(new Function("return this")()),function(r){"object"==typeof exports?r(exports,require("crypto")):r(self.TwinBcrypt={},self.crypto||self.msCrypto)}(function(r,n){"use strict";function e(r){for(var n=unescape(encodeURIComponent(r)),e=n.length,t=new Array(e),o=0;e>o;o++)t[o]=n.charCodeAt(o);return t}function t(r){for(var n=r.length,e=new Array(n),t=0;n>t;t++)e[t]=r.charCodeAt(t);return e}function o(r,n){for(var e,t,o=0,a="";n>o;){if(e=255&r[o++],a+=B[e>>2],e=(3&e)<<4,o>=n){a+=B[e];break}if(t=255&r[o++],e|=t>>4,a+=B[e],e=(15&t)<<2,o>=n){a+=B[e];break}t=255&r[o++],e|=t>>6,a+=B[e],a+=B[63&t]}return a}function a(r){for(var n,e,t=new Array(16),o=0,a=0;;){if(n=D[r.charCodeAt(o++)-46],e=D[r.charCodeAt(o++)-46],t[a++]=255&(n<<2|e>>4),22===o)break;n=e<<4,e=D[r.charCodeAt(o++)-46],t[a++]=255&(n|e>>2),n=e<<6,e=D[r.charCodeAt(o++)-46],t[a++]=255&(n|e)}return t}function i(r){for(var n=r.length,e=new Array(72),t=0,o=0;72>o;)e[o++]=r[t++],t===n&&(t=0);return e}function u(r,n,e){for(var t=0,o=e>>2;t<n.length;t++,o++)r[o]=n[t]}function f(r,n,e){for(var t=0;72>t;)r[n++]=e[t++]<<24|e[t++]<<16|e[t++]<<8|e[t++]}function c(r){function n(n){for(var e=r,t=G>>2,o=t|O,f=n>>2,c=e[f]^e[t],s=e[1|f];o>t;)s^=(e[c>>>24]+e[a|c>>>16&255]^e[i|c>>>8&255])+e[u|255&c]^e[++t],c^=(e[s>>>24]+e[a|s>>>16&255]^e[i|s>>>8&255])+e[u|255&s]^e[++t];e[f]=s^e[S>>2],e[1|f]=c}function e(n){var e;for(r[L>>2]=0,r[L+4>>2]=0,e=0;M>e;e++)r[G>>2|e]^=r[(n>>2)+e];var t,o,f,c,s,l=r;for(e=0;M>e;e+=2){for(t=G>>2,o=t|O,f=L>>2,c=l[f]^l[t],s=l[1|f];o>t;)s^=(l[c>>>24]+l[a|c>>>16&255]^l[i|c>>>8&255])+l[u|255&c]^l[++t],c^=(l[s>>>24]+l[a|s>>>16&255]^l[i|s>>>8&255])+l[u|255&s]^l[++t];l[f]=s^l[S>>2],l[1|f]=c,r[G>>2|e]=l[f],r[G>>2|e+1]=c}for(e=0;T>e;e+=2){for(t=G>>2,o=t|O,f=L>>2,c=l[f]^l[t],s=l[1|f];o>t;)s^=(l[c>>>24]+l[a|c>>>16&255]^l[i|c>>>8&255])+l[u|255&c]^l[++t],c^=(l[s>>>24]+l[a|s>>>16&255]^l[i|s>>>8&255])+l[u|255&s]^l[++t];l[f]=s^l[S>>2],l[1|f]=c,r[e]=l[f],r[1|e]=c}}function t(r,n,t){for(var o=0;t>=o&&!(r>n);o++)e(R),e(j),r++;return r}var o=k>>2,a=o+256|0,i=a+256|0,u=i+256|0;return{encrypt:n,expandLoop:t}}function s(stdlib, foreign, heap) {"use asm";var HEAP32=new stdlib.Uint32Array(heap);var BLOWFISH_NUM_ROUNDS=16;var S_offset=0x0000;var S1_offset=0x0400;var S2_offset=0x0800;var S3_offset=0x0C00;var P_offset=0x1000;var P_last_offset=0x1044;var crypt_ciphertext_offset=0x1048;var LR_offset=0x01060;var password_offset=0x1068;var salt_offset=0x10b0;var P_LEN=18;var S_LEN=1024;function encrypt(offset) {offset=offset|0;var i=0;var n=0;var L=0;var R=0;var imax=0;imax=P_offset|BLOWFISH_NUM_ROUNDS<<2;L=HEAP32[offset>>2]|0;R=HEAP32[offset+4>>2]|0;L=L^HEAP32[P_offset>>2];for (i=P_offset; (i|0)<(imax|0);) {i=(i+4)>>>0;R=R^(((HEAP32[(L>>>22)>>2]>>>0) +(HEAP32[(S1_offset|(L>>>14&0x3ff))>>2]>>>0) ^(HEAP32[(S2_offset|(L>>>6&0x3ff))>>2])) +(HEAP32[(S3_offset|(L<<2&0x3ff))>>2]>>>0))^HEAP32[i>>2];i=(i+4)>>>0;L=L^(((HEAP32[(R>>>22)>>2]>>>0) +(HEAP32[(S1_offset|(R>>>14&0x3ff))>>2]>>>0) ^(HEAP32[(S2_offset|(R>>>6&0x3ff))>>2])) +(HEAP32[(S3_offset|(R<<2&0x3ff))>>2]>>>0))^HEAP32[i>>2];}HEAP32[offset>>2]=R^HEAP32[P_last_offset>>2];HEAP32[(offset+4)>>2]=L;}function expandKey(offset) {offset=offset|0;var i=0;var off=0;off=P_offset|0;for (i=0; (i|0)<(P_LEN|0); i=(i+1)|0) {HEAP32[off>>2]=HEAP32[off>>2]^HEAP32[offset>>2];offset=(offset+4)|0;off=(off+4)|0;}HEAP32[LR_offset>>2]=0;HEAP32[LR_offset+4>>2]=0;off=P_offset;for (i=0; (i|0)<(P_LEN|0);
Line truncated
|
||||
+function l(r,n,e,t){var o,a,i,u=L>>2,f=u+1;for(t[u]=0,t[f]=0,a=0,o=0;M>o;o++)i=n[a++]<<24|n[a++]<<16|n[a++]<<8|n[a++],t[G>>2|o]^=i;for(a=0,o=0;M>o;o+=2)i=r[a++]<<24|r[a++]<<16|r[a++]<<8|r[a++],a&=65295,t[u]^=i,i=r[a++]<<24|r[a++]<<16|r[a++]<<8|r[a++],a&=65295,t[f]^=i,e.encrypt(L),t[G>>2|o]=t[u],t[G>>2|o+1]=t[f];var c=k>>2;for(o=0;T>o;o+=2)i=r[a++]<<24|r[a++]<<16|r[a++]<<8|r[a++],a&=65295,t[u]^=i,i=r[a++]<<24|r[a++]<<16|r[a++]<<8|r[a++],a&=65295,t[f]^=i,e.encrypt(L),t[c|o]=t[u],t[c|o+1]=t[f]}function p(r,n,e,t,o,a,i){for(var u=e;t>=u;){if(u=r.expandLoop(u,t,o),a){var f=a(u/(t+1));if(f===!1)return}if(u>t){if(i)return void setImmediate(g.bind(null,r,n,i));return}if(i)return void setImmediate(p.bind(null,r,n,u,t,o,a,i))}}function g(r,n,e){u(n,x,F);var t;for(t=0;64>t;t++)r.encrypt(F+0),r.encrypt(F+8),r.encrypt(F+16);var o,a=0,i=x.length,f=new Array(4*i);for(t=0;i>t;t++)o=n[(F>>2)+t],f[a++]=o>>24,f[a++]=o>>16&255,f[a++]=o>>8&255,f[a++]=255&o;return e&&e(f),f}function y(r,n){return r+o(n,23)}function m(n,o,m,v){var d,h=o.substr(0,29),w=+o.substr(4,2),A=o.substr(7,22);if("string"==typeof n)d=r.encodingMode===r.ENCODING_UTF8?e(n):t(n);else if(Array.isArray(n))d=n.map(function(r){return 255&r});else{if(!(n instanceof Uint8Array))throw new Error("Incorrect arguments");d=Array.prototype.slice.call(n)}d.push(0);var b,E,N=a(A,C),O=31>w?1<<w:2147483648,M=O-1,T=m?127:M;if(I){var x=new ArrayBuffer(8192);b=new Uint32Array(x),E=s({Uint32Array:Uint32Array},null,x)}else E=c(b=[]);return u(b,U,k),u(b,$,G),d=i(d),N=i(N),f(b,j>>2,N),f(b,R>>2,d),l(N,d,E,b),v?void p(E,b,0,M,T,m,function(r){v(y(h,r))}):(p(E,b,0,M,T,m),y(h,g(E,b)))}function v(r){if(!b)throw new Error("No cryptographically secure pseudorandom number generator available.");if(null==r&&(r=N),r=0|+r,isNaN(r)||4>r||r>31)throw new Error("Invalid cost parameter.");var n="$2y$";return 10>r&&(n+="0"),n+=r+"$",n+=o(b(C),C)}function d(r,n,e){if(n&&"number"!=typeof n){if("string"!=typeof n||!z.test(n))throw new Error("Invalid salt")}else n=v(n);return m(r,n,e)}function h(r,n,e,t){if(arguments.length<2)throw new Error("Incorrect arguments");if(2===arguments.length?(t=n,n=e=null):3===arguments.length&&(t=e,e=null,"function"==typeof n&&(e=n,n=null)),n&&"number"!=typeof n){if("string"!=typeof n||!z.test(n))throw new Error("Invalid salt")}else n=v(n);if(!t||"function"!=typeof t)throw new Error("No callback function was given.");m(r,n,e,t)}function w(r,n){if("string"!=typeof n||!Z.test(n))throw new Error("Incorrect arguments");var e=n.substr(0,n.length-31),t=d(r,e);return t===n}function A(r,n,e,t){if("string"!=typeof n||!Z.test(n))throw new Error("Incorrect arguments");if(t||(t=e,e=null),!t||"function"!=typeof t)throw new Error("No callback function was given.");var o=n.substr(0,n.length-31);h(r,o,e,function(r){t(r===n)})}var b,E="undefined"!=typeof InstallTrigger,I=E;n&&(b=n.randomBytes,n.getRandomValues&&(b=function(r){var e=new Uint8Array(r);return n.getRandomValues(e)}));var C=16,N=10,O=16,$=[608135816,2242054355,320440878,57701188,2752067618,698298832,137296536,3964562569,1160258022,953160567,3193202383,887688300,3232508343,3380367581,1065670069,3041331479,2450970073,2306472731],U=[3509652390,2564797868,805139163,3491422135,3101798381,1780907670,3128725573,4046225305,614570311,3012652279,134345442,2240740374,1667834072,1901547113,2757295779,4103290238,227898511,1921955416,1904987480,2182433518,2069144605,3260701109,2620446009,720527379,3318853667,677414384,3393288472,3101374703,2390351024,1614419982,1822297739,2954791486,3608508353,3174124327,2024746970,1432378464,3864339955,2857741204,1464375394,1676153920,1439316330,715854006,3033291828,289532110,2706671279,2087905683,3018724369,1668267050,732546397,1947742710,3462151702,2609353502,2950085171,1814351708,2050118529,680887927,999245976,1800124847,3300911131,1713906067,1641548236,4213287313,1216130144,1575780402,4018429277,3917837745,3693486850,3949271944,596196993,3549867205,258830323,2213823033,772490370,2760122372,1774776394,2652871518,566650946,4142492826,1728879713,2882767088,1783734482,3629395816,2517608232,2874225571,1861159788,326777828,3124490320,2130389656,2716951837,967770486,1724537150,2185432712,2364442137,1164943284,2105845187,998989502,3765401048,2244026483,1075463327,1455516326,1322494562,910128902,469688178,1117454909,936433444,3490320968,3675253459,1240580251,122909385,2157517691,634681816,4142456567,3825094682,3061402683,2540495037,79693498,3249098678,1084186820,1583128258,426386531,1761308591,1047286709,322548459,995290223,1845252383,2603652396,3431023940,2942221577,3202600964,3727903485,1712269319,422464435,3234572375,1170764815,3523960633,3117677531,1434042557,442511882,3600875718,1076654713,1738483198,4213154764,2393238008,3677496056,1014306527,4251020053,793779912,2902807211,842905082,4246964064,1395751752,1040244610,2656851899,3396308128,445077038,3742853595,3577915638,679411651,2892444358,2354009459,1767581616,3150600392,3791627101,3102740896,284835224,4246832056,1258075500,768725851,2589189241,306972400
Line truncated
|
||||
@@ -0,0 +1,36 @@
|
||||
From 0b024c2ebe6be105c3535f82e55a51e57dd947bb Mon Sep 17 00:00:00 2001
|
||||
From: Jackie264 <125248570+Jackie264@users.noreply.github.com>
|
||||
Date: Sun, 7 Jun 2026 15:32:44 +0800
|
||||
Subject: [PATCH] Add .prepare.sh script for package preparation
|
||||
|
||||
The script prepares the package build directory by checking for a Git repository and creating necessary directories and files based on the package name.
|
||||
---
|
||||
.prepare.sh | 19 +++++++++++++++++++
|
||||
1 file changed, 19 insertions(+)
|
||||
create mode 100644 .prepare.sh
|
||||
|
||||
diff --git a/.prepare.sh b/.prepare.sh
|
||||
new file mode 100644
|
||||
index 0000000..92a72fb
|
||||
--- /dev/null
|
||||
+++ b/.prepare.sh
|
||||
@@ -0,0 +1,19 @@
|
||||
+PKG_NAME="$1"
|
||||
+CURDIR="$2"
|
||||
+PKG_BUILD_DIR="$3"
|
||||
+
|
||||
+if [ -d "$CURDIR/.git" ]; then
|
||||
+ config="$CURDIR/.git/config"
|
||||
+else
|
||||
+ config="$(sed "s|^gitdir:\s*|$CURDIR/|;s|$|/config|" "$CURDIR/.git")"
|
||||
+fi
|
||||
+if grep -q -E "url.*gitcode\.(com|net)" "$config"; then
|
||||
+ for d in htdocs patches po root; do
|
||||
+ rm -rf "$PKG_BUILD_DIR"/$d
|
||||
+ done
|
||||
+ mkdir -p "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view
|
||||
+ touch "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view/$PKG_NAME.js
|
||||
+ mkdir -p "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d
|
||||
+ touch "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d/$PKG_NAME.json
|
||||
+fi
|
||||
+exit 0
|
||||
@@ -0,0 +1,181 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: cs\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: Martin Olmer <m_iv@seznam.cz>\n"
|
||||
"Language-Team: Czech <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsadguardhome/cs/>\n"
|
||||
"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n"
|
||||
"PO-Revision-Date: 2026-08-27 18:20+0000\n"
|
||||
"X-Generator: Weblate 2026.9.dev0\n"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
"Překročitelný strop paměti pro běhové prostředí Go, umožňující častější "
|
||||
"spouštění úklidu použité paměti, tak jak se využití blíží limitu, aby se "
|
||||
"zabránilo vynucenému ukončování v docházející paměti (OOM)."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "Pokročilá nastavení"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr "Soubor s nastaveními"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr ""
|
||||
"Je třeba, aby soubor s nastavením byl uložen ve své vlastní složce a ne "
|
||||
"přímo v „/etc“."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr "Složka ve které jsou ukládány filtry, záznamy událostí a statistiky."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr "Přístup k souborovému systému"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
"Soubory a složky, ke kterým by AdGuard Home měl mít přístup pouze pro čtení "
|
||||
"nebo pro čtení i zápis."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "Obecná nastavení"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr ""
|
||||
"Proměnné prostředí pro jazyk Go, které upravují chování automatického "
|
||||
"uvolňování použité paměti a její správu."
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr "Udělit oprávnění pro LuCI aplikace AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr "Skupina, pod kterou služba běží."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr "Pokud nevyplněno, bude použito výchozí"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr "Měňte na své vlastní riziko."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "Nespuštěné"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr "Nadřazená složka bude vlastněna uživatelem, pod kterým služba běží."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr "Je třeba, aby popis umístění byl absolutní."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr "Je třeba, aby popis umístění nekončil lomítkem."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr "Přístup pouze pro čtení"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr "Přístup pro čtení a zápis"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr "Spuštěné"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr "Stav služby"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr "Skupina služby"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr "Uživatel služby"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
"Nejvyšší umožněný počet vláken operačního systému které mohou souběžně "
|
||||
"vykonávat kód v Go na uživatelské úrovni."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
"Upravuje agresivnost úklidu použité paměti nastavením procenta růstu "
|
||||
"„haldy“, dovoleného než bude spuštěn další cyklus úklidu."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr "Uživatel, pod kterým služba běží."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr "Podrobné zaznamenávání událostí"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr "Verze"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr "Bude vlastněno uživatelem, pod kterým služba běží."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr "Pracovní složka"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr "zrušit nastavení"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr "zrušit nastavení a 100"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr "zrušit nastavení a odpovídající počtu jader procesoru"
|
||||
@@ -0,0 +1,168 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: de\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: Pavel Borecki <pavel.borecki@gmail.com>\n"
|
||||
"Language-Team: German <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsadguardhome/de/>\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"PO-Revision-Date: 2026-09-23 19:35+0000\n"
|
||||
"X-Generator: Weblate 2026.10.dev0\n"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "Erweiterte Einstellungen"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "Allgemeine Einstellungen"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "Angehalten"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr "Läuft"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr "Dienststatus"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr "Ausführliche Protokollierung"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr "Version"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,188 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-23 22:27+0200\n"
|
||||
"PO-Revision-Date: 2026-09-23 19:06+0000\n"
|
||||
"Last-Translator: Franco Castillo <castillofrancodamian@gmail.com>\n"
|
||||
"Language-Team: Spanish <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsadguardhome/es/>\n"
|
||||
"Language: es\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.10.dev0\n"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
"Un límite de memoria flexible para el entorno de ejecución de Go, que "
|
||||
"permite que el recolector de basura se ejecute con mayor frecuencia a medida "
|
||||
"que el uso se acerca al límite para evitar bloqueos por falta de memoria "
|
||||
"(OOM, por sus siglas en inglés)."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "Configuración avanzada"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr "Archivo de configuración"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr ""
|
||||
"El archivo de configuración debe almacenarse en su propio directorio, y no "
|
||||
"en «/etc»."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr ""
|
||||
"Directorio donde se almacenan los filtros, los registros y las estadísticas."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr "Acceso al sistema de archivos"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
"Archivos y directorios a los que AdGuard Home debería tener acceso de solo "
|
||||
"lectura o de lectura y escritura."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "Configuración general"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr ""
|
||||
"Variables de entorno de Go que ajustan el recolector de basura y la gestión "
|
||||
"de memoria."
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr "Otorgar permisos para la aplicación LuCI AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr "Grupo al que pertenece el servicio."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr "Si está vacío, por defecto es"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr "Modifique bajo su propia responsabilidad."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "No se está ejecutando"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr "El directorio principal pertenecerá al usuario del servicio."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr "La ruta debe ser absoluta."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr "La ruta no debe terminar con una barra diagonal."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr "Acceso de solo lectura"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr "Acceso de lectura y escritura"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr "Ejecutándose"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr "Estado del servicio"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr "Grupo del servicio"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr "Usuario del servicio"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
"El número máximo de subprocesos del sistema operativo que pueden ejecutar "
|
||||
"código Go a nivel de usuario simultáneamente."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
"Permite ajustar la agresividad del recolector de basura estableciendo el "
|
||||
"porcentaje de crecimiento de la pila permitido antes de que se active el "
|
||||
"siguiente ciclo de recolección."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr "Usuario bajo el cual se ejecuta el servicio."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr "Registro detallado"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr "Versión"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr "Será propiedad del usuario del servicio."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr "Directorio de trabajo"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr "no establecido"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr "no establecido y 100"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr "no configurado y que coincide con el número de CPUs"
|
||||
@@ -0,0 +1,183 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: ga\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>\n"
|
||||
"Language-Team: Irish <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsadguardhome/ga/>\n"
|
||||
"Plural-Forms: nplurals=5; plural=n==1 ? 0 : n==2 ? 1 : (n>2 && n<7) ? 2 :(n>"
|
||||
"6 && n<11) ? 3 : 4;\n"
|
||||
"PO-Revision-Date: 2026-09-02 17:28+0000\n"
|
||||
"X-Generator: Weblate 2026.9.dev0\n"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
"Teorainn bhog chuimhne don am rite Go, rud a ligeann don bhailitheoir "
|
||||
"truflais rith níos minice de réir mar a thagann an úsáid i dtreo na teorann "
|
||||
"chun maruithe As Cuimhne (OOM) a chosc."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "Socruithe Ardleibhéil"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr "Comhad cumraíochta"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr ""
|
||||
"Ní mór an comhad cumraíochta a stóráil ina eolaire féin, agus ní i '/etc'."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr "Eolaire ina stóráiltear scagairí, logaí agus staitisticí."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr "Rochtain ar an gCóras Comhad"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
"Comhaid agus eolairí ar cheart go mbeadh rochtain léite amháin nó rochtana "
|
||||
"léite-scríofa ag AdGuard Home orthu."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "Socruithe Ginearálta"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr ""
|
||||
"Athróga timpeallachta Téigh a choigeartaíonn bailitheoir truflais agus "
|
||||
"bainistíocht cuimhne."
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr "Deonaigh ceadanna don aip AdGuard Home LuCI"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr "Grúpa faoina ritheann an tseirbhís."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr "Más folamh, réamhshocraítear é go"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr "Déan modhnú ar do phriacal féin."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "Gan rith"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr ""
|
||||
"Beidh eolaire na dtuismitheoirí faoi úinéireacht úsáideoir na seirbhíse."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr "Caithfidh an cosán a bheith absalóideach."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr "Ní mór don chosán a bheith críochnaithe le slais."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr "Rochtain léite amháin"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr "Rochtain léite-scríofa"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr "Ag rith"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr "Stádas Seirbhíse"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr "Grúpa seirbhíse"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr "Úsáideoir seirbhíse"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
"An líon uasta snáitheanna córais oibriúcháin ar féidir leo cód Go ar "
|
||||
"leibhéal an úsáideora a fhorghníomhú ag an am céanna."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
"Coigeartaíonn sé ionsaitheacht an bhailitheora bruscair tríd an gcéatadán "
|
||||
"fáis carn a cheadaítear sula dtosaíonn an chéad timthriall bailiúcháin eile "
|
||||
"a shocrú."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr "An t-úsáideoir a ritheann an tseirbhís faoi."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr "Logáil fhaclach"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr "Leagan"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr "Beidh sé faoi úinéireacht an úsáideora seirbhíse."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr "Eolaire oibre"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr "díchumasaigh"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr "díchumasaigh agus 100"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr "díchumasaigh agus ag teacht le líon na LAPanna"
|
||||
@@ -0,0 +1,178 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: ko\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: Hyeonjeong Lee <h9101654@gmail.com>\n"
|
||||
"Language-Team: Korean <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsadguardhome/ko/>\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"PO-Revision-Date: 2026-09-15 21:51+0000\n"
|
||||
"X-Generator: Weblate 2026.10.dev0\n"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
"Go 런타임의 소프트 메모리 상한선입니다. 사용량이 한계에 가까워질수록 쓰레기 "
|
||||
"수집기가 더 자주 작동하여 메모리 부족 (OOM)으로 인한 강제 종료를 방지합니다."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "고급 설정"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr "설정 파일"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr "설정 파일은 '/etc'가 아닌 전용 디렉터리에 저장해야 합니다."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr "필터, 로그, 통계가 저장되는 디렉터리입니다."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr "파일 시스템 접근"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
"AdGuard Home에서 읽기 전용 또는 읽기/쓰기 권한을 가져야 하는 파일 및 디렉터리"
|
||||
"입니다."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "일반 설정"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr ""
|
||||
"쓰레기 수집기(Garbage Collector) 및 메모리 관리를 조정하는 Go 환경 변수입니"
|
||||
"다."
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr "AdGuard Home LuCI 앱에 권한 부여"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr "서비스를 실행할 그룹입니다."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr "미입력 시 기본값:"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr "수정 시 책임은 사용자에게 있습니다."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "중지됨"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr "상위 디렉터리는 서비스 사용자의 소유로 설정됩니다."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr "절대 경로여야 합니다."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr "경로가 슬래시(/)로 끝나서는 안 됩니다."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr "읽기 전용 접근"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr "읽기/쓰기 접근"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr "실행 중"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr "서비스 상태"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr "서비스 그룹"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr "서비스 사용자"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
"사용자 수준의 Go 코드를 동시에 실행할 수 있는 운영 체제 스레드의 최대 개수입"
|
||||
"니다."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
"다음 수집 주기가 실행되기 전에 허용되는 힙 메모리 증가율(%)을 설정하여 쓰레"
|
||||
"기 수집기의 작동 빈도(Aggressiveness)를 조정합니다."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr "서비스를 실행할 사용자입니다."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr "상세 로그 기록"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr "버전"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr "서비스 사용자의 소유로 설정됩니다."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr "작업 디렉터리"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr "설정 안 함"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr "미설정 (100)"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr "미설정 (CPU 코어 수와 일치)"
|
||||
@@ -0,0 +1,168 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: lo\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: Martin Olmer <m_iv@seznam.cz>\n"
|
||||
"Language-Team: Lao <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsadguardhome/lo/>\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"PO-Revision-Date: 2026-08-28 18:08+0000\n"
|
||||
"X-Generator: Weblate 2026.9.dev0\n"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "ການຕັ້ງຄ່າຂັ້ນສູງ"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "ການຕັ້ງຄ່າທົ່ວໄປ"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "ບໍ່ເຮັດວຽກ"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr "ກຳລັງເຮັດວຽກ"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr "ການບັນທຶກລັອກແບບລະອຽດ"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr "ເວີຊັນ"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,169 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: lt\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: Martin Olmer <m_iv@seznam.cz>\n"
|
||||
"Language-Team: Lithuanian <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsadguardhome/lt/>\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"(n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"PO-Revision-Date: 2026-08-28 12:25+0000\n"
|
||||
"X-Generator: Weblate 2026.9.dev0\n"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "Pažangūs nustatymai"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "Bendri nustatymai"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "Nevykdomas/-a"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,183 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: pl\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: Matthaiks <kitynska@gmail.com>\n"
|
||||
"Language-Team: Polish <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsadguardhome/pl/>\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
|
||||
"|| n%100>=20) ? 1 : 2);\n"
|
||||
"PO-Revision-Date: 2026-08-24 16:48+0000\n"
|
||||
"X-Generator: Weblate 2026.9.dev0\n"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
"Miękki limit pamięci dla środowiska wykonawczego Go, umożliwiający modułowi "
|
||||
"zbierającemu nieużytki częstsze uruchamianie się, gdy wykorzystanie pamięci "
|
||||
"zbliża się do limitu, zapobiegając w ten sposób wyłączeniom z powodu braku "
|
||||
"pamięci (OOM)."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "Ustawienia zaawansowane"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr "Plik konfiguracyjny"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr ""
|
||||
"Plik konfiguracyjny musi być zapisany w osobnym katalogu, a nie w '/etc'."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr "Katalog, w którym przechowywane są filtry, dzienniki i statystyki."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr "Dostęp do systemu plików"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
"Pliki i katalogi, do których AdGuard Home powinien mieć dostęp tylko do "
|
||||
"odczytu lub do odczytu i zapisu."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "Ustawienia główne"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr ""
|
||||
"Zmienne środowiskowe Go, które dostrajają moduł zbierający nieużytki i "
|
||||
"zarządzanie pamięcią."
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr "Udziel uprawnień aplikacji LuCI AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr "Grupa, w ramach której działa usługa."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr "Jeśli puste, domyślnie"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr "Dokonujesz zmian na własne ryzyko."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "Nieuruchomione"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr "Właścicielem katalogu nadrzędnego będzie użytkownik usługi."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr "Ścieżka musi być bezwzględna."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr "Ścieżka nie może kończyć się ukośnikiem."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr "Dostęp tylko do odczytu"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr "Dostęp do odczytu i zapisu"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr "Uruchomione"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr "Status usługi"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr "Grupa usługi"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr "Użytkownik usługi"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
"Maksymalna liczba wątków systemu operacyjnego, które mogą jednocześnie "
|
||||
"wykonywać kod Go na poziomie użytkownika."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
"Dostosowuje agresywność modułu zbierającego nieużytki poprzez ustawienie "
|
||||
"procentowego wzrostu stosu dozwolonego przed wyzwoleniem następnego cyklu "
|
||||
"zbierania nieużytków."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr "Użytkownik, w ramach którego działa usługa."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr "Szczegółowe rejestrowanie"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr "Wersja"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr "Będzie własnością użytkownika usługi."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr "Katalog roboczy"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr "nieustawione"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr "nieustawione i 100"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr "nieustawione i dopasowanie liczby procesorów"
|
||||
@@ -0,0 +1,182 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: ru\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: SnIPeRSnIPeR <snipersniper@users.noreply.hosted.weblate.org>"
|
||||
"\n"
|
||||
"Language-Team: Russian <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsadguardhome/ru/>\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"PO-Revision-Date: 2026-08-28 22:28+0000\n"
|
||||
"X-Generator: Weblate 2026.9.dev0\n"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
"Мягкое ограничение памяти для среды выполнения Go, позволяющее сборщику "
|
||||
"мусора запускаться чаще по мере приближения к лимиту, чтобы предотвратить "
|
||||
"принудительное завершение процесса из-за нехватки памяти (OOM)."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "Расширенные настройки"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr "Файл конфигурации"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr ""
|
||||
"Файл конфигурации должен храниться в собственном каталоге, а не в '/etc'."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr "Каталог, в котором хранятся фильтры, журналы и статистика."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr "Доступ к файловой системе"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
"Файлы и каталоги, к которым AdGuard Home должен иметь доступ только для "
|
||||
"чтения или для чтения и записи."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "Основные настройки"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr ""
|
||||
"Переменные окружения Go, которые настраивают сборщик мусора и управление "
|
||||
"памятью."
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr "Предоставить разрешения для приложения AdGuard Home в LuCI"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr "Группа, под которой работает служба."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr "Если пусто, по умолчанию используется"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr "Изменяйте на свой страх и риск."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "Не запущено"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr "Родительский каталог будет принадлежать пользователю службы."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr "Путь должен быть абсолютным."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr "Путь не должен заканчиваться косой чертой."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr "Доступ только для чтения"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr "Доступ для чтения и записи"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr "Запущено"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr "Состояние службы"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr "Группа службы"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr "Пользователь службы"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
"Максимальное количество потоков операционной системы, которые могут "
|
||||
"одновременно выполнять код Go на уровне пользователя."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
"Настраивает агрессивность сборщика мусора, задавая процент роста кучи, "
|
||||
"допустимый до запуска следующего цикла сборки."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr "Пользователь, под которым работает служба."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr "Подробное ведение журнала"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr "Версия"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr "Будет принадлежать пользователю службы."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr "Рабочий каталог"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr "не задано"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr "не задано и 100"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr "не задано и соответствует количеству процессоров"
|
||||
@@ -0,0 +1,177 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: sv\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: Daniel Nylander <yeagersthlm@gmail.com>\n"
|
||||
"Language-Team: Swedish <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsadguardhome/sv/>\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"PO-Revision-Date: 2026-09-16 17:45+0000\n"
|
||||
"X-Generator: Weblate 2026.10.dev0\n"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
"En mjuk minnesgräns för Go-körmiljön som låter skräpsamlaren köras oftare "
|
||||
"när användningen närmar sig gränsen, för att förhindra att processer "
|
||||
"avslutas på grund av minnesbrist (OOM)."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "Avancerade inställningar"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr "Konfigurationsfil"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr "Konfigurationsfilen måste lagras i en egen katalog, inte i ”/etc”."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr "Katalog där filter, loggar och statistik lagras."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr "Åtkomst till filsystemet"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
"Filer och kataloger som AdGuard Home ska ha skrivskyddad eller läs- och "
|
||||
"skrivåtkomst till."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "Allmänna inställningar"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr "Go-miljövariabler som justerar skräpsamlaren och minneshanteringen."
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr "Bevilja behörigheter för AdGuard Home-appen i LuCI"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr "Gruppen som tjänsten körs under."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr "Om den är tom används standardvärdet"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr "Ändra på egen risk."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "Körs inte"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr "Den överordnade katalogen ägs av tjänstanvändaren."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr "Sökvägen måste vara absolut."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr "Sökvägen får inte sluta med ett snedstreck."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr "Skrivskyddad åtkomst"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr "Läs- och skrivåtkomst"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr "Körs"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr "Status för tjänsten"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr "Tjänstegrupp"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr "Tjänstanvändare"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
"Det högsta antalet operativsystemtrådar som kan köra Go-kod på användarnivå "
|
||||
"samtidigt."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
"Justerar skräpsamlarens aggressivitet genom att ange hur stor procentuell "
|
||||
"heapökning som tillåts innan nästa insamlingscykel startar."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr "Användaren som tjänsten körs under."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr "Detaljerad loggning"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr "Version"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr "Ägs av tjänstanvändaren."
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr "Arbetskatalog"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr "inte inställt"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr "inte inställt och 100"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr "inte inställt och samma antal som antalet processorer"
|
||||
@@ -0,0 +1,159 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,168 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: tr\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: Martin Olmer <m_iv@seznam.cz>\n"
|
||||
"Language-Team: Turkish <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsadguardhome/tr/>\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"PO-Revision-Date: 2026-08-31 19:11+0000\n"
|
||||
"X-Generator: Weblate 2026.9.dev0\n"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "Gelişmiş Ayarlar"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "Genel Ayarlar"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "Çalışmıyor"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr "Çalışıyor"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr "Hizmet Durumu"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr "Ayrıntılı günlük kaydı"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr "Sürüm"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,169 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: uk\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: Martin Olmer <m_iv@seznam.cz>\n"
|
||||
"Language-Team: Ukrainian <https://hosted.weblate.org/projects/openwrt/"
|
||||
"luciapplicationsadguardhome/uk/>\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
||||
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
||||
"PO-Revision-Date: 2026-08-31 06:59+0000\n"
|
||||
"X-Generator: Weblate 2026.9.dev0\n"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "Розширені налаштування"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "Загальні налаштування"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "Не працює"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr "Запущено"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr "Стан служби"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr "Докладне журналювання"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr "Версія"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr ""
|
||||
@@ -1,179 +1,173 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Last-Translator: Automatically generated\n"
|
||||
"Language-Team: none\n"
|
||||
"Language: zh\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Language: zh_Hans\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: Hosted Weblate user 54392 "
|
||||
"<hamburger2048@users.noreply.hosted.weblate.org>\n"
|
||||
"Language-Team: Chinese (Simplified Han script) <https://hosted.weblate.org/"
|
||||
"projects/openwrt/luciapplicationsadguardhome/zh_Hans/>\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"PO-Revision-Date: 2026-08-31 06:44+0000\n"
|
||||
"X-Generator: Weblate 2026.9.dev0\n"
|
||||
|
||||
msgid "AdGuard Home"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
"Go 运行时的软性内存限制,允许垃圾收集器在内存占用接近限制时更频繁地运行防止内"
|
||||
"存耗尽终止程序。"
|
||||
|
||||
msgid "AdGuard Home Configuration"
|
||||
msgstr "配置"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
msgid "AdGuard Home Statistics"
|
||||
msgstr "统计"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "高级设置"
|
||||
|
||||
msgid "AdGuard Home Status"
|
||||
msgstr "状态"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr "配置文件"
|
||||
|
||||
msgid "AdGuard Home Status - Error"
|
||||
msgstr "状态 - 错误"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr "配置文件必须存储在自己的目录中,不能在 '/etc' 中。"
|
||||
|
||||
msgid "Average processing time (seconds)"
|
||||
msgstr "平均处理时间(秒)"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr "存储过滤器、日志和统计数据的目录。"
|
||||
|
||||
msgid "Bootstrap DNS addresses"
|
||||
msgstr "Bootstrap DNS 地址"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr "文件系统访问"
|
||||
|
||||
msgid "Configuration"
|
||||
msgstr "配置"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr "AdGuard Home 应该有只读或读写权限的文件和目录。"
|
||||
|
||||
msgid "DNS blocks"
|
||||
msgstr "DNS 拦截"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "通用设置"
|
||||
|
||||
msgid "DNS listen addresses"
|
||||
msgstr "DNS 监听地址"
|
||||
|
||||
msgid "DNS listen port"
|
||||
msgstr "DNS 监听端口"
|
||||
|
||||
msgid "DNS queries"
|
||||
msgstr "DNS 查询"
|
||||
|
||||
msgid "DNS replacements (malware/phishing)"
|
||||
msgstr "DNS 替换(恶意软件/钓鱼)"
|
||||
|
||||
msgid "DNS replacements (safesearch)"
|
||||
msgstr "DNS 替换(安全搜索)"
|
||||
|
||||
msgid "General settings"
|
||||
msgstr "常规设置"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr "调节垃圾收集器和内存管理的 Go 环境变量。"
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr "授予 AdGuard Home LuCI 应用权限"
|
||||
msgstr "授予 AdGuard Home LuCI 应用的权限"
|
||||
|
||||
msgid "Logs"
|
||||
msgstr "日志"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr "服务运行在什么组下面。"
|
||||
|
||||
msgid "No"
|
||||
msgstr "否"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr "如为空,默认为"
|
||||
|
||||
msgid "Not found"
|
||||
msgstr "未找到"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr "修改风险自负。"
|
||||
|
||||
msgid "Password for AdGuard Home"
|
||||
msgstr "密码"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "未在运行"
|
||||
|
||||
msgid "Please open the Configuration section, and provide the credentials."
|
||||
msgstr "请打开配置部分,并提供凭证。"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr "主目录将被服务用户所拥有。"
|
||||
|
||||
msgid "Protection enabled"
|
||||
msgstr "保护已启用"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr "必须是绝对路径。"
|
||||
|
||||
msgid "RPC call failure:"
|
||||
msgstr "RPC 调用失败:"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr "路径不能用 / 结束。"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr "只读权限"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr "读写权限"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "Showing last 50 lines"
|
||||
msgstr "显示最近的50行"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr "服务状态"
|
||||
|
||||
msgid "Statistics period (days)"
|
||||
msgstr "统计周期(天)"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr "服务组"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
msgid "System Log (AdGuard Home)"
|
||||
msgstr "系统日志"
|
||||
|
||||
msgid "The password you configured when you set up AdGuard Home"
|
||||
msgstr "您在设置 AdGuard Home 时配置的密码"
|
||||
|
||||
msgid "The username you configured when you set up AdGuard Home"
|
||||
msgstr "您在设置 AdGuard Home 时配置的用户名"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr "服务用户"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"This application requires the username and password that were configured "
|
||||
"when you set up AdGuard Home, as the REST API for AdGuard Home is password "
|
||||
"protected. The password cannot be read from the YAML configuration file for "
|
||||
"AdGuard Home, as it is encrypted in that store. The credentials supplied "
|
||||
"here will be stored unencrypted in /etc/config/adguardhome on your device."
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr "可同时执行用户级 Go 代码的操作系统线程的最大数量。"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
"此应用程序需要您在设置 AdGuard Home 时配置的用户名和密码,因为 AdGuard Home 的 REST API 受密码保护。"
|
||||
"密码无法从 AdGuard Home 的 YAML 配置文件中读取,因为它在存储中是加密的。"
|
||||
"此处提供的凭证将以未加密的形式存储在设备上的 /etc/config/adguardhome 中。"
|
||||
"通过设置下个垃圾收集周期触发前允许的堆的百分比增速来调节垃圾收集器的激进程"
|
||||
"度。"
|
||||
|
||||
msgid "Top Blocked Domains"
|
||||
msgstr "最常被拦截的域名"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr "服务运行在什么用户下面。"
|
||||
|
||||
msgid "Top Queried Domains"
|
||||
msgstr "最常查询的域名"
|
||||
|
||||
msgid "Unable to load log data:"
|
||||
msgstr "无法加载日志数据:"
|
||||
|
||||
msgid "Upstream DNS addresses"
|
||||
msgstr "上游 DNS 地址"
|
||||
|
||||
msgid "Username for AdGuard Home"
|
||||
msgstr "用户名"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr "详细日志记录"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr "版本"
|
||||
|
||||
msgid "Web interface"
|
||||
msgstr "AdGuardHome 管理界面"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr "将被服务用户所拥有。"
|
||||
|
||||
msgid "Yes"
|
||||
msgstr "是"
|
||||
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
msgid "Collecting data..."
|
||||
msgstr "获取数据中..."
|
||||
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
|
||||
msgid "Redirect"
|
||||
msgstr "重定向"
|
||||
|
||||
msgid "none"
|
||||
msgstr "无"
|
||||
|
||||
msgid "Run as dnsmasq upstream server"
|
||||
msgstr "作为dnsmasq的上游服务器"
|
||||
|
||||
msgid "Redirect 53 port to AdGuardHome"
|
||||
msgstr "重定向53端口到AdGuardHome"
|
||||
|
||||
msgid "Use port 53 replace dnsmasq"
|
||||
msgstr "使用53端口替换dnsmasq"
|
||||
|
||||
msgid "AdGuardHome redirect mode"
|
||||
msgstr "重定向模式"
|
||||
|
||||
msgid "Change Password"
|
||||
msgstr "修改密码"
|
||||
|
||||
msgid "Work Dir"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr "工作目录"
|
||||
|
||||
msgid "Service user"
|
||||
msgstr "以此用户权限运行"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr "取消设置"
|
||||
|
||||
msgid "Service group"
|
||||
msgstr "以此用户组权限运行"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr "取消设置和 100"
|
||||
|
||||
msgid "Configuration file"
|
||||
msgstr "配置文件"
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr "取消设置并匹配 CPU 数目"
|
||||
@@ -0,0 +1,168 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: zh_Hant\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Last-Translator: 為什麼不加空格 <c++23@users.noreply.hosted.weblate.org>\n"
|
||||
"Language-Team: Chinese (Traditional Han script) <https://hosted.weblate.org/"
|
||||
"projects/openwrt/luciapplicationsadguardhome/zh_Hant/>\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"PO-Revision-Date: 2026-09-02 20:59+0000\n"
|
||||
"X-Generator: Weblate 2026.9.dev0\n"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:244
|
||||
msgid ""
|
||||
"A soft memory cap for the Go runtime, allowing the garbage collector to run "
|
||||
"more frequently as usage approaches the limit to prevent Out-of-Memory (OOM) "
|
||||
"kills."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:113
|
||||
#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3
|
||||
msgid "AdGuard Home"
|
||||
msgstr "AdGuard Home"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:137
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:199
|
||||
msgid "Advanced Settings"
|
||||
msgstr "進階設定"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:146
|
||||
msgid "Configuration file"
|
||||
msgstr "配置檔案"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:89
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:147
|
||||
msgid ""
|
||||
"Configuration file must be stored in its own directory, and not in '/etc'."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:159
|
||||
msgid "Directory where filters, logs, and statistics are stored."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:132
|
||||
msgid "File System Access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:133
|
||||
msgid ""
|
||||
"Files and directories that AdGuard Home should have read-only or read-write "
|
||||
"access to."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:129
|
||||
msgid "General Settings"
|
||||
msgstr "一般設定"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:138
|
||||
msgid ""
|
||||
"Go environment variables that tune garbage collector and memory management."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3
|
||||
msgid "Grant permissions for the AdGuard Home LuCI app"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
msgid "Group the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:149
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:161
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:181
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "If empty, defaults to"
|
||||
msgstr "如果為空,預設為"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:139
|
||||
msgid "Modify at your own risk."
|
||||
msgstr "修改風險自負。"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:24
|
||||
msgid "Not running"
|
||||
msgstr "未執行"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:148
|
||||
msgid "Parent directory will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:83
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:99
|
||||
msgid "Path must be absolute."
|
||||
msgstr "必須為絕對路徑。"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:86
|
||||
msgid "Path must not end with a slash."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:207
|
||||
msgid "Read-only access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:208
|
||||
msgid "Read-write access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:23
|
||||
msgid "Running"
|
||||
msgstr "執行中"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:122
|
||||
msgid "Service Status"
|
||||
msgstr "服務狀態"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:180
|
||||
msgid "Service group"
|
||||
msgstr "服務群組"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:170
|
||||
msgid "Service user"
|
||||
msgstr "服務使用者"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:230
|
||||
msgid ""
|
||||
"The maximum number of operating system threads that can execute user-level "
|
||||
"Go code simultaneously."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:215
|
||||
msgid ""
|
||||
"Tunes the garbage collector's aggressiveness by setting the percentage of "
|
||||
"heap growth allowed before the next collection cycle triggers."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:171
|
||||
msgid "User the service runs under."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:191
|
||||
msgid "Verbose logging"
|
||||
msgstr "詳細記錄"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:119
|
||||
msgid "Version"
|
||||
msgstr "版本"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:160
|
||||
msgid "Will be owned by the service user."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:158
|
||||
msgid "Working directory"
|
||||
msgstr "工作目錄"
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:247
|
||||
msgid "unset"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:217
|
||||
msgid "unset and 100"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:232
|
||||
msgid "unset and matching the number of CPUs"
|
||||
msgstr ""
|
||||
@@ -2,11 +2,18 @@ http:
|
||||
pprof:
|
||||
port: 6060
|
||||
enabled: false
|
||||
address: 0.0.0.0:3000
|
||||
session_ttl: 720h
|
||||
doh:
|
||||
routes:
|
||||
- GET /dns-query
|
||||
- POST /dns-query
|
||||
- GET /dns-query/{ClientID}
|
||||
- POST /dns-query/{ClientID}
|
||||
insecure_enabled: false
|
||||
address: 0.0.0.0:3008
|
||||
session_ttl: 30d
|
||||
users:
|
||||
- name: admin
|
||||
password: $2y$10$.ujnc.Bphsaz4Z.SbOoMiux2z8DxpJUaIrD5gcITed.vKx.qrZSfC
|
||||
- name: root
|
||||
password:
|
||||
auth_attempts: 5
|
||||
block_auth_min: 15
|
||||
http_proxy: ""
|
||||
@@ -15,22 +22,26 @@ theme: auto
|
||||
dns:
|
||||
bind_hosts:
|
||||
- 0.0.0.0
|
||||
port: 5333
|
||||
port: 1745
|
||||
anonymize_client_ip: false
|
||||
ratelimit: 20
|
||||
ratelimit: 0
|
||||
ratelimit_subnet_len_ipv4: 24
|
||||
ratelimit_subnet_len_ipv6: 56
|
||||
ratelimit_whitelist: []
|
||||
refuse_any: true
|
||||
upstream_dns:
|
||||
- https://dns10.quad9.net/dns-query
|
||||
- '#https://dns10.quad9.net/dns-query'
|
||||
- 223.5.5.5
|
||||
- 2400:3200::1
|
||||
- https://dns.alidns.com/dns-query
|
||||
upstream_dns_file: ""
|
||||
bootstrap_dns:
|
||||
- 9.9.9.10
|
||||
- 149.112.112.10
|
||||
- 2620:fe::10
|
||||
- 2620:fe::fe:10
|
||||
fallback_dns: []
|
||||
- 119.29.29.29
|
||||
- 114.114.114.114
|
||||
fallback_dns:
|
||||
- 119.29.29.29
|
||||
- 113.31.119.88
|
||||
- 2400:7fc0:849e:200:62fd:1de3:1c90:1
|
||||
upstream_mode: parallel
|
||||
fastest_timeout: 1s
|
||||
allowed_clients: []
|
||||
@@ -42,16 +53,19 @@ dns:
|
||||
trusted_proxies:
|
||||
- 127.0.0.0/8
|
||||
- ::1/128
|
||||
cache_enabled: true
|
||||
cache_size: 4194304
|
||||
cache_ttl_min: 0
|
||||
cache_ttl_max: 0
|
||||
cache_optimistic: false
|
||||
cache_optimistic: true
|
||||
cache_optimistic_answer_ttl: 30s
|
||||
cache_optimistic_max_age: 12h
|
||||
bogus_nxdomain: []
|
||||
aaaa_disabled: false
|
||||
enable_dnssec: false
|
||||
enable_dnssec: true
|
||||
edns_client_subnet:
|
||||
custom_ip: ""
|
||||
enabled: false
|
||||
enabled: true
|
||||
use_custom: false
|
||||
max_goroutines: 300
|
||||
handle_ddr: true
|
||||
@@ -61,13 +75,16 @@ dns:
|
||||
upstream_timeout: 10s
|
||||
private_networks: []
|
||||
use_private_ptr_resolvers: true
|
||||
local_ptr_upstreams: []
|
||||
local_ptr_upstreams:
|
||||
- 127.0.0.1:1745
|
||||
use_dns64: false
|
||||
dns64_prefixes: []
|
||||
serve_http3: false
|
||||
use_http3_upstreams: false
|
||||
serve_plain_dns: true
|
||||
hostsfile_enabled: true
|
||||
pending_requests:
|
||||
enabled: true
|
||||
tls:
|
||||
enabled: false
|
||||
server_name: ""
|
||||
@@ -77,7 +94,6 @@ tls:
|
||||
port_dns_over_quic: 853
|
||||
port_dnscrypt: 0
|
||||
dnscrypt_config_file: ""
|
||||
allow_unencrypted_doh: false
|
||||
certificate_chain: ""
|
||||
private_key: ""
|
||||
certificate_path: ""
|
||||
@@ -85,16 +101,26 @@ tls:
|
||||
strict_sni_check: false
|
||||
querylog:
|
||||
dir_path: ""
|
||||
ignored: []
|
||||
interval: 2160h
|
||||
ignored:
|
||||
- orirainbow.cc^
|
||||
- orirainwe.edu.kg^
|
||||
- coriseay.edu.kg^
|
||||
- google.com^
|
||||
interval: 7d
|
||||
size_memory: 1000
|
||||
enabled: true
|
||||
ignored_enabled: true
|
||||
file_enabled: true
|
||||
statistics:
|
||||
dir_path: ""
|
||||
ignored: []
|
||||
interval: 24h
|
||||
ignored:
|
||||
- orirainbow.cc^
|
||||
- orirainwe.edu.kg^
|
||||
- coriseay.edu.kg^
|
||||
- google.com^
|
||||
interval: 7d
|
||||
enabled: true
|
||||
ignored_enabled: true
|
||||
filters:
|
||||
- enabled: true
|
||||
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
|
||||
@@ -105,7 +131,108 @@ filters:
|
||||
name: AdAway Default Blocklist
|
||||
id: 2
|
||||
whitelist_filters: []
|
||||
user_rules: []
|
||||
user_rules:
|
||||
- '@@||ntfy.sh^'
|
||||
- '@@||pubnub.com^'
|
||||
- '@@||clarity.ms^$important'
|
||||
- '@@||sh-gsg.com^$important'
|
||||
- '@@||sh-mtgc.com^$important'
|
||||
- '@@||taobao.com^$important'
|
||||
- '@@||tmall.com^$important'
|
||||
- '@@||jd.com^important'
|
||||
- '@@||flyme.cn^$important'
|
||||
- '@@||meizu.com^$important'
|
||||
- '@@||flydigi.com^'
|
||||
- '@@||pv.sohu.com^$important'
|
||||
- '@@||trafficmanager.net^'
|
||||
- '||yify.foo^'
|
||||
- '||www.668cbb.com/static/adScript.js'
|
||||
- '||topnewsfeeds.net^'
|
||||
- '||nbryb.com^'
|
||||
- '||superbdolly.com^'
|
||||
- '||salutationcheerlessdemote.com^'
|
||||
- '||buildingdoodlesquare.com^'
|
||||
- '||colonistnobilityheroic.com^'
|
||||
- '#解除 小米 及其所有子域名的封锁;'
|
||||
- '@@||mi.com^'
|
||||
- '@@||mi.cn^'
|
||||
- '@@||xiaomi.com^'
|
||||
- '@@||miui.com^'
|
||||
- '@@||xiaomi.net^'
|
||||
- '@@||xiaomi-mipdx.cn.miaozhen.com^'
|
||||
- '@@||xiaomi-dtv.m.cn.miaozhen.com^'
|
||||
- '@@||miwifi.com^'
|
||||
- '@@||tracker.ai.xiaomi.com^$important'
|
||||
- '@@||tracking.miui.com^$important'
|
||||
- '#解除 Apple 及其所有子域名的封锁;'
|
||||
- '@@||apple.com^'
|
||||
- '@@||apple.com.cn^'
|
||||
- '@@||icloud.com^'
|
||||
- '@@||icloud.com.cn^'
|
||||
- '#解除 cloudflare 及其所有子域名的封锁;'
|
||||
- '@@||static.cloudflareinsights.com^$important'
|
||||
- '@@||cloudflare.com^'
|
||||
- '@@||cloudflare-cn.com^'
|
||||
- '@@||argotunnel.com^'
|
||||
- '#解除 microsoft 及其所有子域名的封锁;'
|
||||
- '@@||microsoft.com^$important'
|
||||
- '@@||windows.com^$important'
|
||||
- '@@||microsoftonline.com^$important'
|
||||
- '@@||windowsupdate.com^$important'
|
||||
- '@@||office.com^$important'
|
||||
- '@@||live.com^$important'
|
||||
- '@@||skype.com^$important'
|
||||
- '@@||msn.com^$important'
|
||||
- '@@||bing.com^$important'
|
||||
- '@@||msftconnecttest.com^$important'
|
||||
- '@@||res.public.onecdn.static.microsoft^$important'
|
||||
- '@@||inference.location.live.net^$important'
|
||||
- '@@||static.edge.microsoftapp.net^$important'
|
||||
- '@@||ipv6.msftconnecttest.com^$important'
|
||||
- '@@||sentry.io^$important'
|
||||
- '@@||metrics1.data.hicloud.com^$important'
|
||||
- '@@||otheve.beacon.qq.com^$important'
|
||||
- '@@||app-measurement.com^$important'
|
||||
- '@@||tmdb.org^'
|
||||
- '@@||themoviedb.org^'
|
||||
- /googleads.$~script,domain=~googleads.github.io
|
||||
- /pagead/lvz?
|
||||
- '||google.com/pagead/'
|
||||
- '||static.doubleclick.net^$domain=youtube.com'
|
||||
- '||youtube.com/get_midroll_'
|
||||
- '||5hz.org^'
|
||||
- '||evewan.com^'
|
||||
- '||stgowan.com^'
|
||||
- '||51gowan.com^'
|
||||
- '||www.66s6.net/d/js/acmsd/thea5.js'
|
||||
- '||www.66s6.net/d/js/acmsd/thea6.js'
|
||||
- '||sogowan.com^'
|
||||
- '||52xkdy.com^'
|
||||
- '||ttzj666.com^'
|
||||
- '||888dyy.cc^'
|
||||
- '||qisaoba.com^'
|
||||
- '||euqq.cn^'
|
||||
- '||wkyaoye.com^'
|
||||
- '||qisaoba.com^'
|
||||
- '@||adobe.io^'
|
||||
- '@||adobestats.io'
|
||||
- '@||adobe.com^'
|
||||
- '@||gocart-web-prod-ue1-alb-1461435473.us-east-1.elb.amazonaws.com'
|
||||
- '@@||dig.bdurl.net^$important'
|
||||
- '@@||android.bugly.qq.com^$important'
|
||||
- '@@||yun-hl.3g.qq.com^$important'
|
||||
- '@@||collector.github.com^$important'
|
||||
- '@@||geolocation.onetrust.com^$important'
|
||||
- '@@||google-analytics.com^$important'
|
||||
- '@@||client-upload.cn-shanghai.log.aliyuncs.com^$important'
|
||||
- '@@||twingate.com^'
|
||||
- '@@||syncthing.net^'
|
||||
- '@@||loggw-ex.alipay.com^$important'
|
||||
- '@@||hdfans.org^'
|
||||
- '@@||1ptba.com^'
|
||||
- '@@||ptcafe.club^'
|
||||
- '||nbpicture.xyz^'
|
||||
- ""
|
||||
dhcp:
|
||||
enabled: false
|
||||
interface_name: ""
|
||||
@@ -145,7 +272,7 @@ filtering:
|
||||
safebrowsing_block_host: standard-block.dns.adguard.com
|
||||
rewrites: []
|
||||
safe_fs_patterns:
|
||||
- /tmp/lib/adguardhome/userfilters/*
|
||||
- /var/lib/adguardhome/userfilters/*
|
||||
safebrowsing_cache_size: 1048576
|
||||
safesearch_cache_size: 1048576
|
||||
parental_cache_size: 1048576
|
||||
@@ -153,6 +280,7 @@ filtering:
|
||||
filters_update_interval: 24
|
||||
blocked_response_ttl: 10
|
||||
filtering_enabled: true
|
||||
rewrites_enabled: true
|
||||
parental_enabled: false
|
||||
safebrowsing_enabled: false
|
||||
protection_enabled: true
|
||||
@@ -177,4 +305,4 @@ os:
|
||||
group: ""
|
||||
user: ""
|
||||
rlimit_nofile: 0
|
||||
schema_version: 29
|
||||
schema_version: 34
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"bounding": [
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW"
|
||||
],
|
||||
"effective": [
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW"
|
||||
],
|
||||
"ambient": [
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW"
|
||||
],
|
||||
"permitted": [
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW"
|
||||
],
|
||||
"inheritable": [
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_RAW"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
|
||||
config adguardhome 'config'
|
||||
# All paths must be readable by the configured user
|
||||
option config_file '/etc/adguardhome/adguardhome.yaml'
|
||||
# Where to store persistent data by AdGuard Home
|
||||
option work_dir '/var/lib/adguardhome'
|
||||
option user 'adguardhome'
|
||||
option group 'adguardhome'
|
||||
option verbose '0'
|
||||
option enabled '0'
|
||||
option redirect 'none'
|
||||
option core_version 'latest'
|
||||
option update_url 'https://static.adtidy.org/adguardhome/release/AdGuardHome_linux_${Arch}.tar.gz'
|
||||
|
||||
# Files and directories that AdGuard Home has read-only access to
|
||||
# list jail_mount '/etc/ssl/adguardhome.crt'
|
||||
# list jail_mount '/etc/ssl/adguardhome.key'
|
||||
|
||||
# Files and directories that AdGuard Home has read-write access to
|
||||
# list jail_mount_rw '/path/to/dir'
|
||||
|
||||
# Advanced options. Modify at your own risk.
|
||||
|
||||
# More info: https://go.dev/doc/gc-guide#GOGC
|
||||
option gc '0'
|
||||
|
||||
# Max number of OS threads to use
|
||||
# 0 to match the number of CPUs (default)
|
||||
# >0 to explicitly specify concurrency
|
||||
option maxprocs '0'
|
||||
|
||||
# Soft memory limit in MB, 0 to disable
|
||||
option memlimit '0'
|
||||
@@ -1,136 +1,101 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
#
|
||||
# shellcheck disable=SC3043 # ash supports local
|
||||
. /usr/share/AdGuardHome/helper.sh
|
||||
|
||||
# Start after network (19 matches dnsmasq)
|
||||
# matches dnsmasq
|
||||
START=19
|
||||
# Stop before network stops
|
||||
# stops before networking stops
|
||||
STOP=89
|
||||
|
||||
PROG=/usr/bin/AdGuardHome
|
||||
USE_PROCD=1
|
||||
|
||||
EXTRA_COMMANDS="do_redirect force_reload isrunning"
|
||||
EXTRA_HELP="\
|
||||
\tdo_redirect \t0 or 1\n\
|
||||
\tforce_reload\n\
|
||||
\tisrunning"
|
||||
|
||||
CONTROL_SCRIPT="/usr/share/AdGuardHome/control.sh"
|
||||
CRON_FILE=/etc/crontabs/root
|
||||
|
||||
get_tz() {
|
||||
SET_TZ=""
|
||||
local zonename
|
||||
zonename="$(uci get system.@system[0].zonename 2>/dev/null)"
|
||||
if [ -n "$zonename" ]; then
|
||||
SET_TZ="$zonename"
|
||||
return
|
||||
fi
|
||||
for tzfile in /etc/TZ /var/etc/TZ; do
|
||||
[ -e "$tzfile" ] && tz="$(cat "$tzfile" 2>/dev/null)" && break
|
||||
done
|
||||
[ -n "$tz" ] && SET_TZ=$tz && return
|
||||
}
|
||||
|
||||
do_crontab() {
|
||||
local enabled="$1"
|
||||
local crontab="$2"
|
||||
local findstr default cronenable cronreload=0
|
||||
|
||||
findstr="/usr/share/AdGuardHome/update_core.sh"
|
||||
default="30 3 * * * /usr/share/AdGuardHome/update_core.sh 2>&1"
|
||||
[ "$enabled" = "0" ] || [ "${crontab//autoupdate/}" = "$crontab" ] && cronenable=0 || cronenable=1
|
||||
crontab_editor
|
||||
|
||||
[ "$cronreload" -gt 0 ] && /etc/init.d/cron restart
|
||||
}
|
||||
|
||||
crontab_editor() {
|
||||
local line
|
||||
line="$(grep "$findstr" $CRON_FILE 2>/dev/null)"
|
||||
if [ "${line:0:1}" != "#" ]; then
|
||||
if [ "$cronenable" -eq 1 ]; then
|
||||
if [ -z "$line" ]; then
|
||||
echo "$default" >> $CRON_FILE
|
||||
cronreload=$((cronreload+1))
|
||||
fi
|
||||
elif [ -n "$line" ]; then
|
||||
sed -i "\,$findstr,d" $CRON_FILE
|
||||
echo "#$line" >> $CRON_FILE
|
||||
cronreload=$((cronreload+1))
|
||||
fi
|
||||
else
|
||||
if [ "$cronenable" -eq 1 ]; then
|
||||
sed -i "\,$findstr,d" $CRON_FILE
|
||||
echo "${line:1}" >> $CRON_FILE
|
||||
cronreload=$((cronreload+1))
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
boot() {
|
||||
ADGUARDHOME_BOOT=1
|
||||
start "$@"
|
||||
}
|
||||
|
||||
# Configure DNS to use port 53
|
||||
use_port53() {
|
||||
local dns_port="$1"
|
||||
local adguard_port="$2"
|
||||
local config_file="$3"
|
||||
|
||||
[ "$dns_port" != "53" ] && return
|
||||
|
||||
# Update AdGuardHome config
|
||||
sed -i '/dns:/,/^[^ ]/{ /[[:space:]]*port:/ s/port:[[:space:]]*[0-9]\+/port: 53/ }' "$config_file"
|
||||
|
||||
# Update dnsmasq config
|
||||
uci set "dhcp.@dnsmasq[0].port=$adguard_port"
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq restart
|
||||
}
|
||||
|
||||
# Restore original port configuration
|
||||
rm_port53() {
|
||||
local current_port="$1"
|
||||
local new_port="$2"
|
||||
local config_file="$3"
|
||||
|
||||
[ "$current_port" != "53" ] && return
|
||||
|
||||
# Update AdGuardHome config
|
||||
sed -i '/dns:/,/^[^ ]/{ /[[:space:]]*port:/ s/port:[[:space:]]*[0-9]\+/port: '"$new_port"'/ }' "$config_file"
|
||||
|
||||
# Restore dnsmasq config
|
||||
uci -q set "dhcp.@dnsmasq[0].port=53"
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq restart
|
||||
}
|
||||
|
||||
# Set up firewall redirect rules
|
||||
set_redirect() {
|
||||
local dest_port="$1"
|
||||
local section
|
||||
|
||||
# Add new redirect section
|
||||
section=$(uci add firewall redirect)
|
||||
[ -z "$section" ] && return
|
||||
[ "$(uci -q get "firewall.$section.dest_port")" = "$dest_port" ] && return
|
||||
|
||||
# Configure redirect rules
|
||||
uci -q set "firewall.$section.target=DNAT"
|
||||
uci -q set "firewall.$section.name=AdGuard Home"
|
||||
uci -q set "firewall.$section.src=lan"
|
||||
uci -q set "firewall.$section.src_dport=53"
|
||||
uci -q set "firewall.$section.dest_port=$dest_port"
|
||||
|
||||
# Apply changes
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload
|
||||
echo "Firewall rules updated."
|
||||
}
|
||||
|
||||
# Clear all AdGuard Home redirect rules
|
||||
clear_redirect() {
|
||||
local redirects
|
||||
redirects=$(uci show firewall | grep "AdGuard Home" | cut -d'.' -f1-2)
|
||||
[ -z "$redirects" ] && return
|
||||
|
||||
for redirect in $redirects; do
|
||||
uci -q delete "$redirect"
|
||||
echo "Deleted redirect rule: $redirect"
|
||||
done
|
||||
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload
|
||||
echo "Firewall rules updated."
|
||||
}
|
||||
|
||||
# Configure dnsmasq to forward requests to AdGuard Home
|
||||
set_forward_dnsmasq() {
|
||||
local port="$1"
|
||||
local current_server="$2"
|
||||
local addr="127.0.0.1#$port"
|
||||
|
||||
echo "$current_server" | grep -q "^$addr" && return
|
||||
|
||||
uci -q add_list dhcp.@dnsmasq[0].server="$addr"
|
||||
uci -q delete "dhcp.@dnsmasq[0].resolvfile"
|
||||
uci set "dhcp.@dnsmasq[0].noresolv=1"
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq restart
|
||||
}
|
||||
|
||||
# Stop forwarding requests to AdGuard Home
|
||||
stop_forward_dnsmasq() {
|
||||
local port="$1"
|
||||
local current_server="$2"
|
||||
local addr="127.0.0.1#$port"
|
||||
|
||||
echo "$current_server" | grep -q "^$addr" || return
|
||||
|
||||
uci -q delete "dhcp.@dnsmasq[0].server"
|
||||
uci -q delete "dhcp.@dnsmasq[0].noresolv"
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq restart
|
||||
}
|
||||
|
||||
# Start AdGuard Home service
|
||||
start_service() {
|
||||
mkdir -p /var/etc
|
||||
[ ! -f /var/etc/adguardhome.nft ] && touch /var/etc/adguardhome.nft
|
||||
chmod 644 /var/etc/adguardhome.nft
|
||||
|
||||
if [ -n "$ADGUARDHOME_BOOT" ]; then
|
||||
# Do not start yet, wait for triggers
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f /var/run/AdGserverdis 2>/dev/null
|
||||
config_load 'adguardhome'
|
||||
|
||||
local config_name='config'
|
||||
local config_file config group user verbose work_dir workdir
|
||||
local gc maxprocs memlimit redirect enabled
|
||||
local gc maxprocs memlimit
|
||||
local enabled hashpass crontab redirect
|
||||
|
||||
uci_validate_section 'adguardhome' 'adguardhome' "$config_name" \
|
||||
'enabled:bool:0' \
|
||||
'redirect:string:none' \
|
||||
'gc:uinteger:0' \
|
||||
'group:string:adguardhome' \
|
||||
'config:string' \
|
||||
@@ -142,13 +107,45 @@ start_service() {
|
||||
'user:string:adguardhome' \
|
||||
'verbose:bool:0' \
|
||||
'workdir:string' \
|
||||
'work_dir:string:/var/lib/adguardhome'
|
||||
'work_dir:string:/var/lib/adguardhome' \
|
||||
'enabled:bool:0' \
|
||||
'hashpass:string' \
|
||||
'crontab:string' \
|
||||
'redirect:string:none' \
|
||||
'http_address:string'
|
||||
|
||||
# Compatibility with older configs
|
||||
[ -n "$config" ] && config_file="$config"
|
||||
[ -n "$workdir" ] && work_dir="$workdir"
|
||||
|
||||
[ "$enabled" -eq 0 ] && return 1
|
||||
# Handle hashpass clear text issuance and back brush removal
|
||||
if [ -n "$hashpass" ]; then
|
||||
config_editor "users.password" "$hashpass" "$config_file"
|
||||
uci set adguardhome."$config_name".hashpass=""
|
||||
uci commit adguardhome
|
||||
fi
|
||||
|
||||
# Synchronize the front-end incing WebUI listening address to YAML
|
||||
if [ -n "$http_address" ]; then
|
||||
config_editor "http.address" "$http_address" "$config_file"
|
||||
uci set adguardhome."$config_name".http_address=""
|
||||
uci commit adguardhome
|
||||
fi
|
||||
|
||||
# Isse the plan task change
|
||||
do_crontab "$enabled" "$crontab"
|
||||
|
||||
# If it is not enabled, the linkage cleans up the redirection firewall rules and exits safely.
|
||||
if [ "$enabled" = 0 ]; then
|
||||
$CONTROL_SCRIPT 0
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$user" = "adguardhome" ] && ! grep -q "^adguardhome:" /etc/passwd; then
|
||||
echo "luci-app: User 'adguardhome' is missing, injecting UID 853 into system files..." >&2
|
||||
grep -q "^adguardhome:" /etc/group || echo "adguardhome:x:853:" >> /etc/group
|
||||
echo "adguardhome:x:853:853:adguardhome:/var/run/adguardhome:/bin/false" >> /etc/passwd
|
||||
fi
|
||||
|
||||
local config_dir
|
||||
config_dir=$(dirname "$config_file")
|
||||
@@ -161,26 +158,15 @@ start_service() {
|
||||
|
||||
mkdir -m 0700 -p "$work_dir"
|
||||
chown -R "$user":"$group" "$work_dir"
|
||||
|
||||
# Get ports configuration
|
||||
local AdGuardHome_PORT=$(awk '/dns:/{f=1} f&&/port:/{split($0,a,": *");print a[2];exit}' "$config_file")
|
||||
local dnsmasq_port=$(uci -q get dhcp.@dnsmasq[0].port)
|
||||
local DNSMASQ_SERVER=$(uci -q get dhcp.@dnsmasq[0].server)
|
||||
|
||||
# Configure redirect mode
|
||||
case "$redirect" in
|
||||
"redirect")
|
||||
set_redirect "$AdGuardHome_PORT"
|
||||
;;
|
||||
"dnsmasq-upstream")
|
||||
set_forward_dnsmasq "$AdGuardHome_PORT" "$DNSMASQ_SERVER"
|
||||
;;
|
||||
"exchange")
|
||||
use_port53 "$dnsmasq_port" "$AdGuardHome_PORT" "$config_file"
|
||||
;;
|
||||
esac
|
||||
# Silently downgrade and call the kernel download script when the kernel is missing.
|
||||
if [ ! -f "$PROG" ]; then
|
||||
$CONTROL_SCRIPT 0
|
||||
/usr/share/AdGuardHome/update_core.sh >/tmp/AdGuardHome_update.log 2>&1 &
|
||||
return 0
|
||||
fi
|
||||
|
||||
procd_open_instance adguardhome
|
||||
procd_open_instance adguardhome
|
||||
|
||||
procd_set_param command "$PROG"
|
||||
|
||||
@@ -188,12 +174,17 @@ start_service() {
|
||||
[ "$maxprocs" -le 0 ] || procd_append_param env GOMAXPROCS="$maxprocs"
|
||||
[ "$memlimit" -le 0 ] || procd_append_param env GOMEMLIMIT="$memlimit"
|
||||
|
||||
local SET_TZ
|
||||
get_tz
|
||||
[ -n "$SET_TZ" ] && procd_append_param env TZ="$SET_TZ"
|
||||
|
||||
procd_append_param command --config "$config_file"
|
||||
procd_append_param command --logfile syslog
|
||||
procd_append_param command --no-check-update
|
||||
# procd_append_param command --no-check-update
|
||||
[ "$verbose" = 1 ] && procd_append_param command --verbose
|
||||
procd_append_param command --work-dir "$work_dir"
|
||||
|
||||
procd_set_param file /etc/config/adguardhome "$config_file"
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param user "$user"
|
||||
@@ -212,47 +203,70 @@ start_service() {
|
||||
|
||||
procd_add_jail_mount '/etc/hosts'
|
||||
procd_add_jail_mount '/etc/ssl/certs'
|
||||
[ -f /etc/TZ ] && procd_add_jail_mount '/etc/TZ'
|
||||
|
||||
config_list_foreach "$config_name" jail_mount procd_add_jail_mount
|
||||
config_list_foreach "$config_name" jail_mount_rw procd_add_jail_mount_rw
|
||||
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
# Stop AdGuard Home service
|
||||
stop_service() {
|
||||
if [ -n "$ADGUARDHOME_BOOT" ]; then
|
||||
# Do not start yet, wait for triggers
|
||||
return 0
|
||||
# After the main process is successfully submitted for start-up, it begins to intervene asynchronously and take over the control.
|
||||
if [ -f "$config_file" ]; then
|
||||
($CONTROL_SCRIPT 1) &
|
||||
else
|
||||
$CONTROL_SCRIPT 0
|
||||
if [ "$redirect" != "none" ]; then
|
||||
procd_open_instance "waitconfig"
|
||||
procd_set_param command "/usr/share/AdGuardHome/watchconfig.sh"
|
||||
procd_close_instance
|
||||
fi
|
||||
fi
|
||||
|
||||
local config_file='/etc/adguardhome/adguardhome.yaml'
|
||||
(sleep 10 && [ -z "$(pgrep -f "$(basename "$PROG")")" ] && logger -t adguardhome "no process in 10s cancel redirect" && $CONTROL_SCRIPT 0 ) &
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
config_load 'adguardhome'
|
||||
|
||||
# Get ports configuration
|
||||
local AdGuardHome_PORT=$(awk '/dns:/{f=1} f&&/port:/{split($0,a,": *");print a[2];exit}' "$config_file")
|
||||
local dnsmasq_port=$(uci -q get dhcp.@dnsmasq[0].port)
|
||||
local DNSMASQ_SERVER=$(uci -q get dhcp.@dnsmasq[0].server)
|
||||
|
||||
# Clean up configurations
|
||||
rm_port53 "$AdGuardHome_PORT" "$dnsmasq_port" "$config_file"
|
||||
clear_redirect
|
||||
stop_forward_dnsmasq "$AdGuardHome_PORT" "$DNSMASQ_SERVER"
|
||||
local config_name='config'
|
||||
local crontab
|
||||
config_get crontab "$config_name" crontab ""
|
||||
|
||||
$CONTROL_SCRIPT 0
|
||||
do_crontab 0 "$crontab"
|
||||
touch /var/run/AdGserverdis
|
||||
}
|
||||
|
||||
# Reload service
|
||||
reload_service() {
|
||||
restart
|
||||
do_redirect() {
|
||||
$CONTROL_SCRIPT "$1"
|
||||
}
|
||||
|
||||
_isrunning() {
|
||||
[ ! -f "$PROG" ] && return 2
|
||||
pgrep -f "$(basename "$PROG")" >/dev/null 2>&1 && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
isrunning() {
|
||||
_isrunning
|
||||
local r=$?
|
||||
[ "$r" = "0" ] && echo "running" || ([ "$r" = "1" ] && echo "not run" || echo "no bin")
|
||||
return $r
|
||||
}
|
||||
|
||||
force_reload() {
|
||||
_isrunning && procd_send_signal adguardhome || start
|
||||
}
|
||||
|
||||
# Service triggers
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger adguardhome
|
||||
|
||||
# Add interface trigger during boot
|
||||
if [ -n "$ADGUARDHOME_BOOT" ]; then
|
||||
# Wait for interfaces to be up before starting AdGuard Home
|
||||
# Prevents issues like https://github.com/openwrt/packages/issues/21868
|
||||
procd_add_raw_trigger 'interface.*.up' 5000 /etc/init.d/adguardhome restart
|
||||
fi
|
||||
procd_add_reload_trigger adguardhome
|
||||
|
||||
if [ -n "$ADGUARDHOME_BOOT" ]; then
|
||||
# Wait for interfaces to be up before starting AdGuard Home for real.
|
||||
# Prevents issues like https://github.com/openwrt/packages/issues/21868.
|
||||
procd_add_raw_trigger 'interface.*.up' 5000 /etc/init.d/adguardhome restart
|
||||
fi
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
restart
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
include "/var/etc/adguardhome.nft"
|
||||
@@ -0,0 +1,3 @@
|
||||
# quic-go expects larger UDP send/receive buffers
|
||||
net.core.rmem_max = 7500000
|
||||
net.core.wmem_max = 7500000
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
|
||||
# Migrate old config format only
|
||||
OLD_CONFIG_FILE=$(uci -q get adguardhome.config.config)
|
||||
OLD_CONFIG_FILE=${OLD_CONFIG_FILE:-/etc/adguardhome.yaml}
|
||||
OLD_CONFIG_NEW_OPT_FILE=$(uci -q get adguardhome.config.config_file)
|
||||
NEW_CONFIG_DIR=/etc/adguardhome
|
||||
NEW_CONFIG_FILE="$NEW_CONFIG_DIR/adguardhome.yaml"
|
||||
|
||||
start_service() {
|
||||
if ! /etc/init.d/adguardhome running; then
|
||||
/etc/init.d/adguardhome start
|
||||
fi
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
if /etc/init.d/adguardhome running; then
|
||||
/etc/init.d/adguardhome stop
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -f "$OLD_CONFIG_NEW_OPT_FILE" ] && [ "$OLD_CONFIG_NEW_OPT_FILE" != "$NEW_CONFIG_FILE" ]; then
|
||||
echo "Old AdGuard Home config found in '$OLD_CONFIG_NEW_OPT_FILE'"
|
||||
|
||||
USER=$(uci -q get adguardhome.config.user)
|
||||
USER=${USER:-adguardhome}
|
||||
|
||||
echo "AdGuard Home config is stored in a non-default path."
|
||||
echo "Ensure configured service user '$USER' can access it."
|
||||
|
||||
elif [ -f "$OLD_CONFIG_FILE" ] && [ "$OLD_CONFIG_FILE" != "$NEW_CONFIG_FILE" ]; then
|
||||
echo "Old AdGuard Home config found in '$OLD_CONFIG_FILE'"
|
||||
OLD_CONFIG_DIR=$(dirname "$OLD_CONFIG_FILE")
|
||||
|
||||
USER=$(uci -q get adguardhome.config.user)
|
||||
USER=${USER:-adguardhome}
|
||||
GROUP=$(uci -q get adguardhome.config.group)
|
||||
GROUP=${GROUP:-adguardhome}
|
||||
|
||||
CUR_CONFIG_FILE="$OLD_CONFIG_FILE"
|
||||
if [ "$OLD_CONFIG_DIR" = "/etc" ]; then
|
||||
echo "AdGuard Home config must be stored in its own directory. Migrating..."
|
||||
stop_service
|
||||
|
||||
echo "Using $USER:$GROUP for file ownership."
|
||||
|
||||
[ -d "$NEW_CONFIG_DIR" ] || mkdir -m 0700 -p "$NEW_CONFIG_DIR"
|
||||
mv "$OLD_CONFIG_FILE" "$NEW_CONFIG_FILE"
|
||||
chown -R "$USER":"$GROUP" "$NEW_CONFIG_DIR"
|
||||
CUR_CONFIG_FILE="$NEW_CONFIG_FILE"
|
||||
|
||||
echo "Config migrated to '$NEW_CONFIG_FILE'"
|
||||
|
||||
else
|
||||
echo "AdGuard Home config is stored in a non-default path."
|
||||
echo "Ensure configured service user '$USER' can access it."
|
||||
fi
|
||||
|
||||
uci set adguardhome.config.config_file="$CUR_CONFIG_FILE"
|
||||
uci -q delete adguardhome.config.config
|
||||
|
||||
# Use awk to split match on :, remove double quotes and trim leading and
|
||||
# trailing spaces
|
||||
cert_path=$(grep certificate_path: "$CUR_CONFIG_FILE" \
|
||||
| awk -F':' '{gsub(/"/, "", $2); gsub(/^ +| +$/, "", $2); print $2}')
|
||||
if [ -n "$cert_path" ]; then
|
||||
echo "Found custom 'certificate_path' pointing to '$cert_path'." \
|
||||
+ "Ensure configured service user '$USER' can access it."
|
||||
|
||||
stop_service
|
||||
|
||||
if ! uci -q show adguardhome.config.jail_mount | grep -q "$cert_path"; then
|
||||
uci add_list adguardhome.config.jail_mount="$cert_path"
|
||||
fi
|
||||
fi
|
||||
|
||||
private_key_path=$(grep private_key_path: "$CUR_CONFIG_FILE" \
|
||||
| awk -F':' '{gsub(/"/, "", $2); gsub(/^ +| +$/, "", $2); print $2}')
|
||||
if [ -n "$private_key_path" ]; then
|
||||
echo "Found custom 'private_key_path' pointing to '$private_key_path'." \
|
||||
+ "Ensure configured service user '$USER' can access it."
|
||||
|
||||
stop_service
|
||||
|
||||
if ! uci -q show adguardhome.config.jail_mount | grep -q "$private_key_path"; then
|
||||
uci add_list adguardhome.config.jail_mount="$private_key_path"
|
||||
fi
|
||||
fi
|
||||
|
||||
uci commit adguardhome
|
||||
start_service
|
||||
|
||||
elif [ -z "$OLD_CONFIG_NEW_OPT_FILE" ] && [ "$OLD_CONFIG_FILE" != "$NEW_CONFIG_FILE" ]; then
|
||||
echo "Old AdGuard Home config not found in '$OLD_CONFIG_FILE'"
|
||||
stop_service
|
||||
|
||||
# Service script will create the new config directory
|
||||
uci set adguardhome.config.config_file="$NEW_CONFIG_FILE"
|
||||
uci -q delete adguardhome.config.config
|
||||
echo "Config path changed to '$NEW_CONFIG_FILE'"
|
||||
|
||||
uci commit adguardhome
|
||||
start_service
|
||||
|
||||
else
|
||||
echo "AdGuard Home config is in its default path '$NEW_CONFIG_FILE'"
|
||||
fi
|
||||
|
||||
reload_service() {
|
||||
restart
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
#!/bin/sh
|
||||
. /usr/share/libubox/jshn.sh
|
||||
|
||||
############################################################################################
|
||||
# Functions to parse AdGuardHome YAML file:
|
||||
############################################################################################
|
||||
parse_yaml() {
|
||||
local prefix=$2
|
||||
local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034')
|
||||
sed -ne "s|,$s\]$s\$|]|" \
|
||||
-e ":1;s|^\($s\)\($w\)$s:$s\[$s\(.*\)$s,$s\(.*\)$s\]|\1\2: [\3]\n\1 - \4|;t1" \
|
||||
-e "s|^\($s\)\($w\)$s:$s\[$s\(.*\)$s\]|\1\2:\n\1 - \3|;p" $1 | \
|
||||
sed -ne "s|,$s}$s\$|}|" \
|
||||
-e ":1;s|^\($s\)-$s{$s\(.*\)$s,$s\($w\)$s:$s\(.*\)$s}|\1- {\2}\n\1 \3: \4|;t1" \
|
||||
-e "s|^\($s\)-$s{$s\(.*\)$s}|\1-\n\1 \2|;p" | \
|
||||
sed -ne "s|^\($s\):|\1|" \
|
||||
-e "s|^\($s\)-$s[\"']\(.*\)[\"']$s\$|\1$fs$fs\2|p" \
|
||||
-e "s|^\($s\)-$s\(.*\)$s\$|\1$fs$fs\2|p" \
|
||||
-e "s|^\($s\)\($w\)$s:$s[\"']\(.*\)[\"']$s\$|\1$fs\2$fs\3|p" \
|
||||
-e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" | \
|
||||
awk -F$fs '{
|
||||
indent = length($1)/2;
|
||||
vname[indent] = $2;
|
||||
for (i in vname) {if (i > indent) {delete vname[i]; idx[i]=0}}
|
||||
if(length($2)== 0){ vname[indent]= ++idx[indent] };
|
||||
if (length($3) > 0) {
|
||||
vn=""; for (i=0; i<indent; i++) { vn=(vn)(vname[i])("_")}
|
||||
printf("%s%s%s=\"%s\"\n", "'$prefix'",vn, vname[indent], $3);
|
||||
}
|
||||
}'
|
||||
}
|
||||
read_config() {
|
||||
if [ -f /etc/AdGuardHome/AdGuardHome.yaml ]; then
|
||||
parse_yaml /etc/AdGuardHome/AdGuardHome.yaml
|
||||
elif [ -f /etc/adguardhome/adguardhome.yaml ]; then
|
||||
parse_yaml /etc/adguardhome/adguardhome.yaml
|
||||
fi
|
||||
}
|
||||
|
||||
############################################################################################
|
||||
# Parse AGH yaml file and determine necessary variables in order to function:
|
||||
############################################################################################
|
||||
log_error()
|
||||
{
|
||||
logger -p err "[luci.AdGuardHome]: $@"
|
||||
}
|
||||
check_if_empty()
|
||||
{
|
||||
[[ ! -z "$2" ]] && return
|
||||
log_error $1
|
||||
echo "{\"auth_error\":\"$1\"}"
|
||||
exit 1
|
||||
}
|
||||
call_rest_api()
|
||||
{
|
||||
AGH_USER=$(uci get adguardhome.config.web_username 2> /dev/null)
|
||||
check_if_empty "Username not found in uci adguardhome" ${AGH_USER} || exit 1
|
||||
AGH_PASS=$(uci get adguardhome.config.web_password 2> /dev/null)
|
||||
check_if_empty "Password not found in uci adguardhome" ${AGH_PASS} || exit 1
|
||||
CODE=$(curl --silent -L -k -u ${AGH_USER}:${AGH_PASS} ${PROTO}://${HOST}:${PORT}/control/$1 -o $TFILE -w "%{http_code"})
|
||||
if [[ "${CODE}" == "200" ]]; then
|
||||
cat $TFILE | tr -d '\n'
|
||||
else
|
||||
MSG="REST call failed with '$(cat ${TFILE})'. Are the credentials correct?"
|
||||
log_error "$MSG"
|
||||
echo "{\"error\":\"$MSG\"}" > ${TFILE}
|
||||
fi
|
||||
}
|
||||
|
||||
############################################################################################
|
||||
# Function that groups variables starting with specified string into a single JSON object:
|
||||
############################################################################################
|
||||
group_vars() {
|
||||
local LOOK_FOR=${1/_1/}
|
||||
echo -n "\"$LOOK_FOR\":["
|
||||
cat ${TFILE} | grep "${LOOK_FOR}" | while read FOUND; do
|
||||
sed -i "/${FOUND%=*}/d" ${TFILE}
|
||||
echo "${FOUND##*=}"
|
||||
done | sed '$!s/$/,/' | tr -d '\n'
|
||||
echo -n "],"
|
||||
}
|
||||
|
||||
############################################################################################
|
||||
# Parse AGH yaml file and determine necessary variables in order to function:
|
||||
############################################################################################
|
||||
TFILE=/tmp/rpcd-adguardhome-$(date -Ins).tmp
|
||||
trap 'rm ${TFILE}; exit' EXIT
|
||||
read_config > ${TFILE}
|
||||
source ${TFILE}
|
||||
HOST=${http_address%:*}
|
||||
[[ "${HOST}" == "0.0.0.0" ]] && HOST=$(uci get system.@system[0].hostname).$(uci get dhcp.@dnsmasq[0].domain)
|
||||
HOST=$(echo ${HOST} | awk '{print tolower($0)}')
|
||||
PORT=${http_address##*:}
|
||||
PROTO="http"
|
||||
if [[ "${tls_enabled}" == "true" ]]; then
|
||||
PROTO="https"
|
||||
HOST=${tls_server_name}
|
||||
PORT=${tls_port_https}
|
||||
fi
|
||||
[[ -z "${http_address}" ]] && http_address=${bind_host}:${bind_port}
|
||||
|
||||
############################################################################################
|
||||
# Decision-making cade:
|
||||
############################################################################################
|
||||
case "$1" in
|
||||
list)
|
||||
echo '{ "get_config": {}, "get_statistics": {}, "get_status": {}, "set_passwd": {"username":"string", "hash":"string"} }'
|
||||
;;
|
||||
call)
|
||||
case "$2" in
|
||||
get_config)
|
||||
# Start the JSON array:
|
||||
echo -n "{"
|
||||
# Group any variables starting with "dns_" together:
|
||||
grep "_1=\"" ${TFILE} | grep dns_ | while read LINE; do group_vars ${LINE%=*}; done
|
||||
# Return the AGH configuration as a JSON table:
|
||||
cat ${TFILE} | sed "s|^|\"|g" | sed "s|=\"|\":\"|g" | sed "s|\"$|\",|g" | tr -d '\n'
|
||||
# Include web url, then close the JSON array:
|
||||
echo "\"web_url\":\"${PROTO}://${HOST}:${PORT}\"}"
|
||||
;;
|
||||
|
||||
get_statistics)
|
||||
# Return the AGH statistics as a JSON table:
|
||||
call_rest_api stats
|
||||
;;
|
||||
|
||||
get_status)
|
||||
# Return the AGH status as a JSON table:
|
||||
call_rest_api status
|
||||
;;
|
||||
|
||||
set_passwd)
|
||||
# Check if bcrypt-tool is available for use:
|
||||
# Read and load the argument into JSON array:
|
||||
read input
|
||||
json_load "$input"
|
||||
# Get username and password from JSON array and verify they aren't empty:
|
||||
json_get_var AGH_USER "username"
|
||||
check_if_empty "username parameter missing." "${AGH_USER}" || exit 1
|
||||
json_get_var AGH_HASH "hash"
|
||||
check_if_empty "hash parameter missing." "${AGH_HASH}" || exit 1
|
||||
# Change the username in the AGH configuration file:
|
||||
sed -i "s|\- name: .*|\- name: ${AGH_USER} |" /etc/adguardhome/adguardhome.yaml
|
||||
# Calculate and replace password hash in the AGH configuration file:
|
||||
sed -i "s|\password: .*|\password: ${AGH_HASH}|" /etc/adguardhome/adguardhome.yaml
|
||||
# Restart AdGaurdHome:
|
||||
service adguardhome restart
|
||||
# Return success to caller:
|
||||
echo "{\"success\":true}"
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,10 @@
|
||||
table inet adguardhome {
|
||||
chain prerouting {
|
||||
type nat hook prerouting priority dstnat; policy accept;
|
||||
|
||||
iifname { __WAN_EXCLUDES__ } return
|
||||
|
||||
fib daddr type local udp dport 53 redirect to :__AGH_PORT__
|
||||
fib daddr type local tcp dport 53 redirect to :__AGH_PORT__
|
||||
}
|
||||
}
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
#!/bin/sh
|
||||
# /usr/share/AdGuardHome/control.sh
|
||||
. /usr/share/AdGuardHome/helper.sh
|
||||
|
||||
ENABLED="$1"
|
||||
CONFIGURATION="adguardhome"
|
||||
NFT_RULES_TPL="/usr/share/AdGuardHome/adguardhome.nft.tpl"
|
||||
NFT_RULES_FILE="/var/etc/adguardhome.nft"
|
||||
NFT_TABLE="adguardhome"
|
||||
|
||||
set_nft_redirect() {
|
||||
local port="$1"
|
||||
[ -z "$port" ] && return 1
|
||||
[ ! -f "$NFT_RULES_TPL" ] && return 1
|
||||
|
||||
local wan_section_name
|
||||
wan_section_name=$(uci show firewall | awk -F'.' '/\.name='\''wan'\''$/ {print $2}')
|
||||
local WAN_IFS=""
|
||||
if [ -n "$wan_section_name" ]; then
|
||||
WAN_IFS=$(uci get firewall.$wan_section_name.network 2>/dev/null | xargs)
|
||||
fi
|
||||
|
||||
local WAN_NFT_SET=""
|
||||
for IF in $WAN_IFS; do
|
||||
WAN_NFT_SET="${WAN_NFT_SET} \"$IF\","
|
||||
done
|
||||
WAN_NFT_SET=$(echo "$WAN_NFT_SET" | sed 's/,$//')
|
||||
|
||||
sed -e "s/__WAN_EXCLUDES__/${WAN_NFT_SET}/g" -e "s/__AGH_PORT__/${port}/g" "$NFT_RULES_TPL" > "$NFT_RULES_FILE"
|
||||
|
||||
nft delete table inet "$NFT_TABLE" 2>/dev/null
|
||||
fw4 reload
|
||||
logger -t adguardhome "nft table $NFT_TABLE applied on port $port, WAN excludes: $WAN_NFT_SET"
|
||||
}
|
||||
|
||||
clear_nft_redirect() {
|
||||
# 优化:只有当表存在时才清理和重载防火墙,避免无意义的性能开销
|
||||
if nft list table inet "$NFT_TABLE" >/dev/null 2>&1; then
|
||||
[ -f "$NFT_RULES_FILE" ] && > "$NFT_RULES_FILE"
|
||||
nft delete table inet "$NFT_TABLE" 2>/dev/null
|
||||
fw4 reload
|
||||
logger -t adguardhome "nft table $NFT_TABLE cleared"
|
||||
fi
|
||||
}
|
||||
|
||||
set_forward_dnsmasq() {
|
||||
local PORT="$1"
|
||||
local addr="127.0.0.1#$PORT"
|
||||
local OLD_SERVER
|
||||
OLD_SERVER="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)"
|
||||
echo "$OLD_SERVER" | grep -q "^$addr" && return 0
|
||||
|
||||
uci delete dhcp.@dnsmasq[0].server 2>/dev/null
|
||||
uci add_list dhcp.@dnsmasq[0].server="$addr"
|
||||
uci delete dhcp.@dnsmasq[0].resolvfile 2>/dev/null
|
||||
uci set dhcp.@dnsmasq[0].noresolv=1
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq restart >/dev/null 2>&1
|
||||
}
|
||||
|
||||
stop_forward_dnsmasq() {
|
||||
local OLD_PORT="$1"
|
||||
local addr="127.0.0.1#$OLD_PORT"
|
||||
local OLD_SERVER
|
||||
OLD_SERVER="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)"
|
||||
echo "$OLD_SERVER" | grep -q "^$addr" || return 0
|
||||
|
||||
uci del_list dhcp.@dnsmasq[0].server="$addr" 2>/dev/null
|
||||
local addrlist
|
||||
addrlist="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)"
|
||||
if [ -z "$addrlist" ]; then
|
||||
local resolvfile="/tmp/resolv.conf.d/resolv.conf.auto"
|
||||
[ ! -f "$resolvfile" ] && resolvfile="/tmp/resolv.conf.auto"
|
||||
uci set dhcp.@dnsmasq[0].resolvfile="$resolvfile" 2>/dev/null
|
||||
uci delete dhcp.@dnsmasq[0].noresolv 2>/dev/null
|
||||
fi
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq restart >/dev/null 2>&1
|
||||
}
|
||||
|
||||
agh_reload() {
|
||||
ubus call service event '{"type":"config.change","data":{"package":"adguardhome"}}' >/dev/null 2>&1
|
||||
}
|
||||
|
||||
rm_port53() {
|
||||
local configpath
|
||||
configpath=$(uci -q get adguardhome.config.config_file)
|
||||
[ -z "$configpath" ] && configpath="/etc/adguardhome/adguardhome.yaml"
|
||||
|
||||
local adguardhome_PORT dnsmasq_port
|
||||
adguardhome_PORT=$(config_editor "dns.port" "" "$configpath" "1")
|
||||
dnsmasq_port=$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)
|
||||
[ -z "$dnsmasq_port" ] && dnsmasq_port="53"
|
||||
|
||||
if [ "$dnsmasq_port" = "$adguardhome_PORT" ]; then
|
||||
[ "$dnsmasq_port" = "53" ] && dnsmasq_port="1745"
|
||||
elif [ "$dnsmasq_port" = "53" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
config_editor "dns.port" "$dnsmasq_port" "$configpath"
|
||||
uci set dhcp.@dnsmasq[0].port="53"
|
||||
uci commit dhcp
|
||||
|
||||
/etc/init.d/dnsmasq restart
|
||||
agh_reload
|
||||
}
|
||||
|
||||
use_port53() {
|
||||
local configpath
|
||||
configpath=$(uci -q get adguardhome.config.config_file)
|
||||
[ -z "$configpath" ] && configpath="/etc/adguardhome/adguardhome.yaml"
|
||||
|
||||
local adguardhome_PORT dnsmasq_port
|
||||
adguardhome_PORT=$(config_editor "dns.port" "" "$configpath" "1")
|
||||
dnsmasq_port=$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)
|
||||
[ -z "$dnsmasq_port" ] && dnsmasq_port="53"
|
||||
|
||||
if [ "$dnsmasq_port" = "$adguardhome_PORT" ]; then
|
||||
[ "$dnsmasq_port" = "53" ] && adguardhome_PORT="1745"
|
||||
elif [ "$adguardhome_PORT" = "53" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
config_editor "dns.port" "53" "$configpath"
|
||||
uci set dhcp.@dnsmasq[0].port="$adguardhome_PORT"
|
||||
uci commit dhcp
|
||||
/etc/init.d/dnsmasq restart
|
||||
agh_reload
|
||||
}
|
||||
|
||||
mark_redirect_flag() {
|
||||
local enabled="$1" redirect="$2" agh_port="$3"
|
||||
local configpath flag=0
|
||||
configpath=$(uci -q get adguardhome.config.config_file)
|
||||
[ -z "$configpath" ] && configpath="/etc/adguardhome/adguardhome.yaml"
|
||||
|
||||
if [ "$enabled" = "1" ] && [ "$redirect" != "none" ]; then
|
||||
flag=1
|
||||
if [ "$redirect" = "redirect" ]; then
|
||||
nft list table inet "$NFT_TABLE" >/dev/null 2>&1 || flag=0
|
||||
elif [ "$redirect" = "dnsmasq-upstream" ]; then
|
||||
local s
|
||||
s="$(uci -q get dhcp.@dnsmasq[0].server | tr '\n' ' ')"
|
||||
printf '%s\n' "$s" | grep -q -E "(^|[[:space:]])127\.0\.0\.1#${agh_port}([[:space:]]|$)" || flag=0
|
||||
elif [ "$redirect" = "exchange" ]; then
|
||||
local cfgp dport
|
||||
cfgp="$(config_editor "dns.port" "" "$configpath" "1")"
|
||||
dport="$(uci -q get dhcp.@dnsmasq[0].port)"
|
||||
if [ "$cfgp" != "53" ] || [ "$dport" = "53" ] ; then flag=0; fi
|
||||
fi
|
||||
fi
|
||||
echo -n "$flag" > /var/run/AdGredir
|
||||
}
|
||||
|
||||
_do_redirect() {
|
||||
local enabled="$1"
|
||||
local configpath
|
||||
configpath=$(uci -q get adguardhome.config.config_file)
|
||||
[ -z "$configpath" ] && configpath="/etc/adguardhome/adguardhome.yaml"
|
||||
|
||||
local adguardhome_PORT redirect old_redirect old_port old_enabled
|
||||
adguardhome_PORT="$(config_editor "dns.port" "" "$configpath" "1")"
|
||||
[ -z "$adguardhome_PORT" ] && adguardhome_PORT="0"
|
||||
|
||||
redirect=$(uci -q get adguardhome.config.redirect || echo "none")
|
||||
|
||||
# 【核心修复】:从内存文件中读取旧状态,彻底放弃污染 UCI
|
||||
if [ -f /var/run/adguardhome.state ]; then
|
||||
. /var/run/adguardhome.state
|
||||
else
|
||||
old_redirect="none"
|
||||
old_port="0"
|
||||
old_enabled="0"
|
||||
fi
|
||||
|
||||
uci get dhcp.@dnsmasq[0].port >/dev/null 2>&1 || uci set dhcp.@dnsmasq[0].port="53"
|
||||
uci commit dhcp
|
||||
|
||||
if [ "$old_enabled" = "1" ] && [ "$old_redirect" = "exchange" ]; then
|
||||
adguardhome_PORT="$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)"
|
||||
fi
|
||||
|
||||
if [ "$old_redirect" != "$redirect" ] || [ "$old_port" != "$adguardhome_PORT" ] || { [ "$old_enabled" = "1" ] && [ "$enabled" = "0" ]; }; then
|
||||
if [ "$old_redirect" != "none" ]; then
|
||||
if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then
|
||||
clear_nft_redirect
|
||||
elif [ "$old_redirect" = "dnsmasq-upstream" ]; then
|
||||
stop_forward_dnsmasq "$old_port"
|
||||
elif [ "$old_redirect" = "exchange" ]; then
|
||||
rm_port53
|
||||
fi
|
||||
fi
|
||||
elif [ "$old_enabled" = "1" ] && [ "$enabled" = "1" ]; then
|
||||
if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then
|
||||
clear_nft_redirect
|
||||
fi
|
||||
fi
|
||||
|
||||
# 【核心修复】:将当前状态覆盖写入内存文件,轻量、疾速、安全!
|
||||
cat <<EOF > /var/run/adguardhome.state
|
||||
old_redirect="$redirect"
|
||||
old_port="$adguardhome_PORT"
|
||||
old_enabled="$enabled"
|
||||
EOF
|
||||
|
||||
if [ "$enabled" = "0" ] || [ "$adguardhome_PORT" = "0" ]; then
|
||||
echo -n "0" > /var/run/AdGredir
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$redirect" = "redirect" ]; then
|
||||
set_nft_redirect "$adguardhome_PORT"
|
||||
elif [ "$redirect" = "dnsmasq-upstream" ]; then
|
||||
set_forward_dnsmasq "$adguardhome_PORT"
|
||||
elif [ "$redirect" = "exchange" ] && [ "$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)" = "53" ]; then
|
||||
use_port53
|
||||
fi
|
||||
|
||||
mark_redirect_flag "$enabled" "$redirect" "$adguardhome_PORT"
|
||||
}
|
||||
|
||||
_do_redirect "$ENABLED"
|
||||
@@ -0,0 +1,21 @@
|
||||
#!/bin/sh
|
||||
# Auxiliary functions specially used for reading and writing YAML
|
||||
config_editor() {
|
||||
awk -v yaml="$1" -v value="$2" -v file="$3" -v ro="$4" '
|
||||
BEGIN{split(yaml,part,"\.");s="";i=1;l=length(part);}
|
||||
{
|
||||
if (match($0,s""part[i]":")) {
|
||||
if (i==l) {
|
||||
split($0,t,": ");
|
||||
if (ro==""){
|
||||
system("sed -i '\''"FNR"c\\" t[1] ": " value "'\'' " file " >/dev/null 2>&1");
|
||||
} else {
|
||||
print(t[2]);
|
||||
}
|
||||
exit;
|
||||
}
|
||||
s=s"[- ]{2}";
|
||||
i++;
|
||||
}
|
||||
}' "$3"
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
#!/bin/sh
|
||||
|
||||
# === Core security: self-background operation mechanism ===
|
||||
if [ "$1" != "bg_run" ]; then
|
||||
rm -f /var/run/update_core* /tmp/AdGuardHome_update.log
|
||||
touch /var/run/update_core
|
||||
/usr/share/AdGuardHome/update_core.sh bg_run "$1" </dev/null >/tmp/AdGuardHome_update.log 2>&1 &
|
||||
exit 0
|
||||
fi
|
||||
|
||||
shift
|
||||
# ==========================================================
|
||||
|
||||
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
binpath="/usr/bin/AdGuardHome"
|
||||
update_mode=$1
|
||||
|
||||
core_version=$(uci get adguardhome.config.core_version 2>/dev/null || true)
|
||||
update_url=$(uci get adguardhome.config.update_url 2>/dev/null || true)
|
||||
|
||||
case "${core_version}" in
|
||||
beta)
|
||||
core_api_url=https://api.github.com/repos/AdguardTeam/AdGuardHome/releases
|
||||
;;
|
||||
*)
|
||||
core_api_url=https://api.github.com/repos/AdguardTeam/AdGuardHome/releases/latest
|
||||
;;
|
||||
esac
|
||||
|
||||
EXIT(){
|
||||
rm -rf /var/run/update_core /tmp/AdGuardHome_Update 2>/dev/null
|
||||
if [ "$1" != "0" ]; then
|
||||
touch /var/run/update_core_error
|
||||
fi
|
||||
exit $1
|
||||
}
|
||||
|
||||
trap "EXIT 1" SIGTERM SIGINT
|
||||
|
||||
rm -rf /var/run/update_core_error /var/run/update_core_done 2>/dev/null
|
||||
touch /var/run/update_core
|
||||
|
||||
Check_Task(){
|
||||
running_tasks="$(ps w | grep -v grep | grep 'AdGuardHome' | grep 'update_core' | wc -l)"
|
||||
case $1 in
|
||||
force)
|
||||
echo "Force update requested"
|
||||
echo "Killing ${running_tasks} running tasks ..."
|
||||
ps w | grep -v grep | grep -v $$ | grep 'AdGuardHome' | grep 'update_core' | awk '{print $1}' | xargs kill -9 2>/dev/null
|
||||
;;
|
||||
*)
|
||||
[ "${running_tasks}" -gt 2 ] && echo -e "There are ${running_tasks} update tasks already running. Please wait or stop them manually." && EXIT 2
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
Check_Downloader() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
PKG="curl"
|
||||
return
|
||||
fi
|
||||
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
PKG="wget"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "Neither curl nor wget is installed, cannot check updates!" >&2
|
||||
EXIT 1
|
||||
}
|
||||
|
||||
Check_Updates(){
|
||||
Check_Downloader
|
||||
case "${PKG}" in
|
||||
curl)
|
||||
Downloader="curl -L -k -o"
|
||||
_Downloader="curl -s"
|
||||
;;
|
||||
wget)
|
||||
Downloader="wget --no-check-certificate -T 5 -O"
|
||||
_Downloader="wget -q -O -"
|
||||
;;
|
||||
esac
|
||||
echo "[${PKG}] Checking for updates ..."
|
||||
Cloud_Version="$(${_Downloader} ${core_api_url} 2>/dev/null | grep 'tag_name' | egrep -o "v[0-9].+[0-9.]" | awk 'NR==1')"
|
||||
if [ -z "${Cloud_Version}" ]; then
|
||||
echo "Failed to check updates, please check network." >&2
|
||||
EXIT 1
|
||||
fi
|
||||
|
||||
if [ -f "${binpath}" ]; then
|
||||
Current_Version="$(${binpath} --version 2>/dev/null | egrep -o "v[0-9].+[0-9]" | sed -r 's/(.*), c(.*)/\1/')"
|
||||
else
|
||||
Current_Version="unknown"
|
||||
fi
|
||||
[ -z "${Current_Version}" ] && Current_Version="unknown"
|
||||
|
||||
echo "Binary path: ${binpath%/*}"
|
||||
echo "Current version: ${Current_Version}"
|
||||
echo "Latest version: ${Cloud_Version}"
|
||||
|
||||
if [ ! "${Cloud_Version}" = "${Current_Version}" ] || [ "$1" = force ]; then
|
||||
Update_Core || EXIT 1
|
||||
else
|
||||
echo "Already up to date."
|
||||
EXIT 0
|
||||
fi
|
||||
EXIT 0
|
||||
}
|
||||
|
||||
Update_Core(){
|
||||
rm -rf "/tmp/AdGuardHome_Update" > /dev/null 2>&1
|
||||
mkdir -p "/tmp/AdGuardHome_Update" || { echo "Unable to create a temporary directory"; EXIT 1; }
|
||||
|
||||
GET_Arch
|
||||
eval link="${update_url}"
|
||||
echo "Download link: ${link}"
|
||||
echo "File name: ${link##*/}"
|
||||
echo "Downloading AdGuardHome core ..."
|
||||
|
||||
if ! $Downloader "/tmp/AdGuardHome_Update/${link##*/}" "${link}"; then
|
||||
echo "Download failed."
|
||||
rm -rf "/tmp/AdGuardHome_Update"
|
||||
EXIT 1
|
||||
fi
|
||||
|
||||
if [ "${link##*.}" = "gz" ]; then
|
||||
echo "Extracting AdGuardHome ..."
|
||||
if ! tar -zxf "/tmp/AdGuardHome_Update/${link##*/}" -C "/tmp/AdGuardHome_Update/"; then
|
||||
echo "Extraction failed!"
|
||||
rm -rf "/tmp/AdGuardHome_Update"
|
||||
EXIT 1
|
||||
fi
|
||||
if [ ! -e "/tmp/AdGuardHome_Update/AdGuardHome/AdGuardHome" ]; then
|
||||
echo "Extraction failed: binary not found!"
|
||||
rm -rf "/tmp/AdGuardHome_Update"
|
||||
EXIT 1
|
||||
fi
|
||||
downloadbin="/tmp/AdGuardHome_Update/AdGuardHome/AdGuardHome"
|
||||
else
|
||||
downloadbin="/tmp/AdGuardHome_Update/${link##*/}"
|
||||
fi
|
||||
|
||||
chmod +x "${downloadbin}" 2>/dev/null || true
|
||||
echo "Core size: $(awk 'BEGIN{printf "%.2fMB\n",'$((`ls -l $downloadbin | awk '{print $5}'`))'/1000000}')"
|
||||
|
||||
/etc/init.d/AdGuardHome stop > /dev/null 2>&1
|
||||
echo "Moving AdGuardHome binary to ${binpath%/*} ..."
|
||||
|
||||
if ! mv -f "${downloadbin}" "${binpath}"; then
|
||||
echo -e "The core movement failed! \nIt may be caused by insufficient space."
|
||||
rm -rf "/tmp/AdGuardHome_Update"
|
||||
EXIT 1
|
||||
fi
|
||||
|
||||
rm -rf /tmp/AdGuardHome_Update
|
||||
chmod +x ${binpath}
|
||||
echo "Restarting AdGuardHome service ..."
|
||||
/etc/init.d/adguardhome restart > /dev/null 2>&1
|
||||
|
||||
echo "AdGuardHome core updated successfully!"
|
||||
touch /var/run/update_core_done
|
||||
EXIT 0
|
||||
}
|
||||
|
||||
GET_Arch() {
|
||||
Archt="$(uname -m)"
|
||||
case "${Archt}" in
|
||||
i386|i686)
|
||||
Arch="i386"
|
||||
;;
|
||||
x86_64|amd64)
|
||||
Arch="amd64"
|
||||
;;
|
||||
mipsel|mipsel*)
|
||||
Arch="mipsle_softfloat"
|
||||
;;
|
||||
mips|mips*)
|
||||
Arch="mips_softfloat"
|
||||
;;
|
||||
mips64el)
|
||||
Arch="mips64le_softfloat"
|
||||
;;
|
||||
mips64)
|
||||
Arch="mips64_softfloat"
|
||||
;;
|
||||
armv5*|armv5l|armv5tel)
|
||||
Arch="armv5"
|
||||
;;
|
||||
armv6*|armv6l)
|
||||
Arch="armv6"
|
||||
;;
|
||||
armv7*|armv7l)
|
||||
Arch="armv7"
|
||||
;;
|
||||
arm|armhf)
|
||||
Arch="armv7"
|
||||
;;
|
||||
aarch64)
|
||||
Arch="arm64"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported architecture: [${Archt}]"
|
||||
EXIT 1
|
||||
esac
|
||||
echo "Detected architecture: ${Arch}"
|
||||
}
|
||||
|
||||
|
||||
main(){
|
||||
Check_Task ${update_mode}
|
||||
Check_Updates ${update_mode}
|
||||
}
|
||||
|
||||
main
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# /usr/share/AdGuardHome/watchconfig.sh
|
||||
|
||||
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
configpath=$(uci -q get adguardhome.config.config_file)
|
||||
[ -z "$configpath" ] && configpath="/etc/adguardhome/adguardhome.yaml"
|
||||
|
||||
while :
|
||||
do
|
||||
sleep 10
|
||||
if [ -f "$configpath" ]; then
|
||||
/etc/init.d/adguardhome do_redirect 1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
@@ -1,40 +1,15 @@
|
||||
{
|
||||
"admin/services/adguardhome": {
|
||||
"title": "AdGuard Home",
|
||||
"action": {
|
||||
"type": "firstchild"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [
|
||||
"luci-app-adguardhome"
|
||||
],
|
||||
"uci": {
|
||||
"adguardhome": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"admin/services/adguardhome/config": {
|
||||
"title": "Configuration",
|
||||
"order": 1,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "adguardhome/config"
|
||||
}
|
||||
},
|
||||
"admin/services/adguardhome/status": {
|
||||
"title": "Status",
|
||||
"order": 11,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "adguardhome/status"
|
||||
}
|
||||
},
|
||||
"admin/services/adguardhome/logs": {
|
||||
"title": "Logs",
|
||||
"order": 21,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "adguardhome/logs"
|
||||
}
|
||||
}
|
||||
}
|
||||
"admin/services/adguardhome": {
|
||||
"title": "AdGuard Home",
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "adguardhome/config"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-adguardhome" ],
|
||||
"uci": {
|
||||
"adguardhome": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,23 @@
|
||||
"luci-app-adguardhome": {
|
||||
"description": "Grant permissions for the AdGuard Home LuCI app",
|
||||
"read": {
|
||||
"ubus": {
|
||||
"luci.adguardhome": [
|
||||
"get_config",
|
||||
"get_status",
|
||||
"get_statistics",
|
||||
"set_passwd"
|
||||
]
|
||||
},
|
||||
"uci": [ "adguardhome" ],
|
||||
"file": {
|
||||
"/sbin/logread": [
|
||||
"stat",
|
||||
"exec"
|
||||
],
|
||||
"/sbin/logread -e AdGuardHome": [
|
||||
"exec"
|
||||
],
|
||||
"/etc/init.d/dnsmasq": [ "exec" ],
|
||||
"/etc/init.d/firewall": [ "exec" ]
|
||||
}
|
||||
"/tmp/AdGuardHome_update.log": [ "read" ],
|
||||
"/var/run/update_core": [ "stat" ],
|
||||
"/var/run/update_core_error": [ "stat" ],
|
||||
"/var/run/AdGredir": [ "read" ],
|
||||
"/var/run/AdGserverdis": [ "read" ],
|
||||
"/usr/bin/AdGuardHome --version": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [ "list" ]
|
||||
},
|
||||
"uci": [ "adguardhome" ]
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/usr/share/AdGuardHome/update_core.sh": [ "exec" ]
|
||||
},
|
||||
"uci": [ "adguardhome" ]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"config": "adguardhome",
|
||||
"init": "adguardhome"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# 保证脚本 / lua / shell 配置以 LF 入库, 避免 Windows 编辑后 CRLF 上传杀死 busybox ash
|
||||
* text=eol=lf
|
||||
|
||||
# 二进制例外
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ico binary
|
||||
*.bin binary
|
||||
*.zip binary
|
||||
*.ipk binary
|
||||
@@ -0,0 +1,2 @@
|
||||
/docs/
|
||||
*.md
|
||||
@@ -1,4 +1,5 @@
|
||||
# Copyright (C) 2016 Openwrt.org
|
||||
# luci-app-caddy - LuCI 管理 Caddy (反向代理 / ACME DNS-01 / Aliyun DDNS)
|
||||
# Copyright (C) 2026 andangel - https://github.com/andangel/luci-app-caddy
|
||||
#
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
#
|
||||
@@ -9,17 +10,18 @@ PKG_NAME:=luci-app-caddy
|
||||
|
||||
LUCI_TITLE:=LuCI support for caddy
|
||||
LUCI_DEPENDS:=+caddy
|
||||
PKG_VERSION:=1.0
|
||||
PKG_RELEASE:=2
|
||||
PKG_VERSION:=2.0
|
||||
PKG_RELEASE:=3
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/caddy
|
||||
/etc/config/caddy
|
||||
/etc/caddy/Caddyfile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postrm
|
||||
#!/bin/sh
|
||||
[ -f /etc/caddy/Caddyfile ] && rm -rf /etc/caddy/Caddyfile >/dev/null 2>&1
|
||||
return 0
|
||||
return 0
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
|
||||
@@ -1,30 +1,38 @@
|
||||
module("luci.controller.caddy", package.seeall)
|
||||
|
||||
local DL_DEFAULT = "https://github.com/andangel/luci-app-caddy/releases/latest/download/caddy_linux_arm64"
|
||||
local DL_API = "https://api.github.com/repos/andangel/luci-app-caddy/releases/latest"
|
||||
|
||||
function index()
|
||||
entry({"admin", "nas"}, firstchild(), _("NAS") , 45).dependent = false
|
||||
if not nixio.fs.access("/etc/config/caddy") then
|
||||
return
|
||||
end
|
||||
|
||||
local page = entry({"admin", "nas", "caddy"}, alias("admin", "nas", "caddy", "basic"), _("Caddy"), 20)
|
||||
page.dependent = true
|
||||
page.acl_depends = { "luci-app-caddy" }
|
||||
|
||||
entry({"admin", "nas"}, firstchild(), "NAS", 44).dependent = false
|
||||
entry({"admin", "nas", "caddy", "basic"}, cbi("caddy/caddy"), _("基本设置"), 1).leaf = true
|
||||
entry({"admin", "nas", "caddy", "log"}, cbi("caddy/caddy_log"), _("日志"), 2).leaf = true
|
||||
entry({"admin", "nas", "caddy", "caddy_status"}, call("caddy_status")).leaf = true
|
||||
entry({"admin", "nas", "caddy", "get_log"}, call("get_log")).leaf = true
|
||||
entry({"admin", "nas", "caddy", "clear_log"}, call("clear_log")).leaf = true
|
||||
entry({"admin", "nas", "caddy", "admin_info"}, call("admin_info")).leaf = true
|
||||
entry({"admin", "network", "caddy"}, alias("admin", "network", "caddy", "basic"), _("Caddy"), 30)
|
||||
entry({"admin", "network", "caddy", "basic"}, cbi("caddy/caddy"), _("状态"), 1).leaf = true
|
||||
entry({"admin", "network", "caddy", "domain"}, cbi("caddy/domain"), _("域名"), 2).leaf = true
|
||||
entry({"admin", "network", "caddy", "host"}, cbi("caddy/host"), _("主机"), 3).leaf = true
|
||||
entry({"admin", "network", "caddy", "caddyfile"}, cbi("caddy/caddyfile"), _("配置"), 4).leaf = true
|
||||
entry({"admin", "network", "caddy", "log"}, cbi("caddy/caddy_log"), _("日志"), 5).leaf = true
|
||||
entry({"admin", "network", "caddy", "caddy_status"}, call("caddy_status")).leaf = true
|
||||
entry({"admin", "network", "caddy", "get_log"}, call("get_log")).leaf = true
|
||||
entry({"admin", "network", "caddy", "clear_log"}, call("clear_log")).leaf = true
|
||||
entry({"admin", "network", "caddy", "admin_info"}, call("admin_info")).leaf = true
|
||||
entry({"admin", "network", "caddy", "download"}, call("download")).leaf = true
|
||||
entry({"admin", "network", "caddy", "download_status"}, call("download_status")).leaf = true
|
||||
entry({"admin", "network", "caddy", "download_ctl"}, call("download_ctl")).leaf = true
|
||||
end
|
||||
|
||||
function caddy_status()
|
||||
local e={}
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local port = tonumber(uci:get_first("caddy", "caddy", "port"))
|
||||
e.port = (port or 12311)
|
||||
-- Web 界面按钮地址: 取第一个 proxy host 的 域名:端口, 没有则留空
|
||||
local weburl = ""
|
||||
local pname = uci:get_first("caddy", "proxy")
|
||||
if pname then
|
||||
local pdom = uci:get("caddy", pname, "domain")
|
||||
local pprt = uci:get("caddy", pname, "port")
|
||||
if pdom then
|
||||
weburl = "http://" .. pdom .. (pprt and (":" .. pprt) or "")
|
||||
end
|
||||
end
|
||||
e.weburl = weburl
|
||||
e.running=luci.sys.call("pidof caddy >/dev/null")==0
|
||||
local tagfile = io.open("/tmp/caddy_time", "r")
|
||||
if tagfile then
|
||||
@@ -57,7 +65,8 @@ function caddy_status()
|
||||
e.caddyram = "unknown"
|
||||
end
|
||||
|
||||
local command4 = io.popen("([ -s /tmp/caddy.tag ] && cat /tmp/caddy.tag ) || (echo `$(uci -q get caddy.@caddy[0].bin_dir) -v |awk '{print $1}' | sed 's/[^0-9.]*//g'` > /tmp/caddy.tag && cat /tmp/caddy.tag)")
|
||||
-- 官方 CLI: caddy version (输出形如 v2.11.4 h1:xxx, 取版本号段)
|
||||
local command4 = io.popen("$(uci -q get caddy.@caddy[0].bin_dir) version 2>/dev/null | awk '{print $1}'")
|
||||
e.caddytag = command4:read("*all")
|
||||
command4:close()
|
||||
if e.caddytag == "" then
|
||||
@@ -83,9 +92,165 @@ function clear_log()
|
||||
end
|
||||
|
||||
function admin_info()
|
||||
local validate = luci.sys.exec("$(uci -q get caddy.@caddy[0].bin_dir) validate --config /etc/caddy/Caddyfile --adapter caddyfile 2>&1")
|
||||
|
||||
-- HOME=/root 兜底: LuCI CGI (uhttpd) 环境没 $HOME/$XDG_CONFIG_HOME,
|
||||
-- caddy validate 会 warn "unable to determine directory for user configuration; falling back to current directory".
|
||||
-- 显式给个家目录就没这条 warn; --config 已显式传, 结果不受 HOME 影响.
|
||||
local validate = luci.sys.exec("env HOME=/root $(uci -q get caddy.@caddy[0].bin_dir) validate --config /etc/caddy/Caddyfile --adapter caddyfile 2>&1")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({ validate = validate })
|
||||
end
|
||||
|
||||
function download_status()
|
||||
local st = { state = "idle", size = 0, total = 0, speed = 0, msg = "" }
|
||||
local f = io.open("/tmp/caddy_dl.status", "r")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
local k, v = line:match("^([%a_]+)=(.*)$")
|
||||
if k then st[k] = v end
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
st.size = tonumber(st.size) or 0
|
||||
st.total = tonumber(st.total) or 0
|
||||
st.speed = tonumber(st.speed) or 0
|
||||
-- 看门狗: 活动状态下 status 由下载进程每秒刷新一次并带 ts 时间戳 (该 lua 桥无 os.stat, 不靠文件 mtime),
|
||||
-- 若 >60s 没刷新说明进程已丢失 (被 OOM/清理 SIGKILL 不执行 trap), 状态文件残留会导致 UI 永远下载中
|
||||
local actives = { downloading = 1, paused = 1, verifying = 1 }
|
||||
local ts = tonumber(st.ts) or 0
|
||||
if actives[st.state] and ts > 0 and os.time() - ts > 60 then
|
||||
st.state = "lost"
|
||||
st.speed = 0
|
||||
st.msg = (st.size > 0) and "下载进程已丢失 (保留进度, 点[下载并重启]续传)" or "状态异常 (点[下载并重启]重新开始)"
|
||||
end
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(st)
|
||||
end
|
||||
|
||||
-- 暂停/继续/停止: 写一个控制命令到 /tmp/caddy_dl.ctl, 下载进程每秒轮询处理.
|
||||
-- 暂停=SIGSTOP 冻结 curl (连接仍挂着, 不额外撞 GitHub); 继续=SIGCONT; 停止=杀 curl 留已下部分.
|
||||
function download_ctl()
|
||||
-- 本 runtime (Luci openwrt-25.12 / ucode 桥) 的 luci.http 不提供 readrequest, 用 formvalue 读 POST 字段
|
||||
local act = tostring(luci.http.formvalue("act") or "")
|
||||
local ok, msg = false, ""
|
||||
if act == "pause" or act == "resume" or act == "stop" then
|
||||
local f = io.open("/tmp/caddy_dl.ctl", "w")
|
||||
if f then f:write(act .. "\n"); f:close(); ok = true; msg = "ok" end
|
||||
else
|
||||
msg = "unknown act: " .. act
|
||||
end
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({ ok = ok, msg = msg })
|
||||
end
|
||||
|
||||
function download()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local bin = uci:get("caddy", "caddy", "bin_dir") or "/usr/sbin/caddy"
|
||||
local u = uci:get("caddy", "caddy", "download_url")
|
||||
local url = (u and u ~= "") and u or DL_DEFAULT
|
||||
local use_sha = (url == DL_DEFAULT) and "1" or "0"
|
||||
|
||||
local sq = function(s) return s:gsub("'", "'\\''") end
|
||||
local L = {
|
||||
"#!/bin/sh",
|
||||
"URL='" .. sq(url) .. "'",
|
||||
"TMP='/tmp/caddy.dl'",
|
||||
"STATUS='/tmp/caddy_dl.status'",
|
||||
"CTL='/tmp/caddy_dl.ctl'",
|
||||
"BIN='" .. sq(bin) .. "'",
|
||||
"USE_SHA=" .. use_sha,
|
||||
"API='" .. DL_API .. "'",
|
||||
"RECEIPT='/tmp/caddy_dl.done'",
|
||||
"CURLPID=0; PAUSED=0; STOPTED=0",
|
||||
"rm -f \"$CTL\"",
|
||||
"",
|
||||
"# 写临时文件再原子 mv, 防状态端点读到半截文件而把 UI 误渲染成 idle",
|
||||
"ws(){ printf 'state=%s\\nsize=%s\\ntotal=%s\\nspeed=%s\\nmsg=%s\\nts=%s\\n' \"$1\" \"$2\" \"$3\" \"$4\" \"$5\" $(date +%s) > \"$STATUS.tmp\"; mv -f \"$STATUS.tmp\" \"$STATUS\"; }",
|
||||
"",
|
||||
"PIDFILE='/tmp/caddy_dl.pid'",
|
||||
"# 去重锁: 已有存活实例则直接退出 (防清理漏杀导致多实例竞争写同一文件 / 互相覆盖 status)",
|
||||
"if [ -f \"$PIDFILE\" ]; then OLD=$(cat \"$PIDFILE\" 2>/dev/null); if [ -n \"$OLD\" ] && kill -0 \"$OLD\" 2>/dev/null; then exit 0; fi; fi",
|
||||
"echo $$ > \"$PIDFILE\"",
|
||||
"trap 'rm -f \"$PIDFILE\"' EXIT",
|
||||
"",
|
||||
"# 取总大小: 发 Range bytes=0-0 只下 1 字节, 从 Content-Range: bytes 0-0/TOTAL 抽分母 (比完整 HEAD 稳); GitHub 抖则重试 3 次",
|
||||
"TOT=0",
|
||||
"i=0",
|
||||
'while [ "$i" -lt 3 ]; do',
|
||||
"TOT=$(curl -s -L -r 0-0 --connect-timeout 10 -D - -o /dev/null \"$URL\" 2>/dev/null | tr -d '\\r' | grep -i 'Content-Range:' | grep -oE '[0-9]+/[0-9]+' | awk -F/ '{print $2}' | head -1)",
|
||||
"if [ -n \"$TOT\" ] && [ \"$TOT\" -gt 0 ] 2>/dev/null; then break; fi",
|
||||
"i=$((i+1)); sleep 2",
|
||||
"done",
|
||||
"ws downloading $(wc -c < \"$TMP\" 2>/dev/null || echo 0) \"$TOT\" 0 \"\"",
|
||||
"",
|
||||
"# 已下载收据: 上次下载且 SHA 通过时, 记下当时的期望 sha (写入在成功替换后). 收据在 /tmp, 重启即清空 → 重启后再点会重新下载(拉到新版).",
|
||||
"# 本次点击若 API 最新 sha == 收据 == 当前二进制 sha, 说明盘上的就是刚下载的版本 → 不重复下载, 直接重启",
|
||||
"SKIP=0",
|
||||
"BINSIZE=$(wc -c < \"$BIN\" 2>/dev/null || echo 0)",
|
||||
"if [ \"$USE_SHA\" -eq 1 ] && [ -f \"$RECEIPT\" ]; then",
|
||||
"ws verifying \"$BINSIZE\" \"$BINSIZE\" 0 '核对已下载版本...'",
|
||||
"API_NOW=$(curl -L -sS --connect-timeout 10 --max-time 20 \"$API\" | grep -oE 'sha256:[0-9a-f]{64}' | head -1 | sed 's/sha256://')",
|
||||
"BIN_SHA=$(sha256sum \"$BIN\" 2>/dev/null | awk '{print $1}')",
|
||||
"if [ \"${#API_NOW}\" -ge 64 ] && [ \"${#BIN_SHA}\" -ge 64 ] && [ \"$(cat \"$RECEIPT\" 2>/dev/null)\" = \"$API_NOW\" ] && [ \"$BIN_SHA\" = \"$API_NOW\" ]; then SKIP=1; fi",
|
||||
"fi",
|
||||
"if [ $SKIP -eq 1 ]; then /etc/init.d/caddy restart >/dev/null 2>&1; ws done \"$BINSIZE\" \"$BINSIZE\" 0 '已是上次下载的最新版, 无需重复下载, 已直接重启 Caddy'; exit 0; fi",
|
||||
"",
|
||||
"# 直接后台运行 curl (不经函数包一层), 使 $! 即真实 curl pid, 暂停/停止(信号)才能直接作用于 curl",
|
||||
"# GitHub 直连不稳: 除网络类错误外, HTTP/2 reset / -C 续传冲突等瞬时错误也按 --retry 自动再来",
|
||||
"# speed-limit/speed-time: 低于 ~100B/s 持续 45s 视为假死, 触发重试 (比干等 5 分钟超时快)",
|
||||
"curl -L -C - -sS --retry 4 --retry-delay 8 --retry-all-errors --connect-timeout 20 --speed-limit 100 --speed-time 45 -o \"$TMP\" \"$URL\" >> /tmp/caddy_dl.log 2>&1 &",
|
||||
"CURLPID=$!; RC=",
|
||||
"LAST=$(wc -c < \"$TMP\" 2>/dev/null || echo 0); LTIME=$(date +%s)",
|
||||
"# 主循环: 每秒刷新进度 + 处理暂停/继续/停止控制命令 (与下载解耦, 控制端点只写 $CTL 文件)",
|
||||
'while kill -0 "$CURLPID" 2>/dev/null; do',
|
||||
'if [ -f "$CTL" ]; then',
|
||||
' CMD=$(cat "$CTL" 2>/dev/null); rm -f "$CTL"',
|
||||
' if [ "$CMD" = "pause" ] && [ $PAUSED -eq 0 ]; then kill -STOP $CURLPID 2>/dev/null; PAUSED=1',
|
||||
' elif [ "$CMD" = "resume" ] && [ $PAUSED -eq 1 ]; then kill -CONT $CURLPID 2>/dev/null; PAUSED=0',
|
||||
' elif [ "$CMD" = "stop" ]; then kill $CURLPID 2>/dev/null; STOPTED=1',
|
||||
' fi',
|
||||
'fi',
|
||||
'if [ $PAUSED -eq 1 ]; then ws paused $(wc -c < "$TMP" 2>/dev/null || echo 0) "$TOT" 0 ""; sleep 1; continue; fi',
|
||||
'SZ=$(wc -c < "$TMP" 2>/dev/null || echo 0)',
|
||||
'NOW=$(date +%s); DT=$((NOW - LTIME)); SP=0',
|
||||
'if [ "$DT" -gt 0 ]; then SP=$(( (SZ - LAST) / DT )); fi',
|
||||
'LAST=$SZ; LTIME=$NOW',
|
||||
'ws downloading "$SZ" "$TOT" "$SP" ""',
|
||||
"# 僵尸检测: curl 死掉但未 wait 收尸时 kill -0 仍成功会卡死主循环, /proc 状态 Z 即已死; 已下满按成功, 否则按失败",
|
||||
'CS=$(awk "{print $3}" /proc/$CURLPID/stat 2>/dev/null)',
|
||||
'if [ "$CS" = "Z" ]; then if [ "$TOT" -gt 0 ] 2>/dev/null && [ "$SZ" -ge "$TOT" ] 2>/dev/null; then RC=0; else RC=128; fi; break; fi',
|
||||
"sleep 1",
|
||||
"done",
|
||||
"# 主循环已判定 (z 检测设了 RC) 则不覆盖; 否则 wait 收尸取真实退出码",
|
||||
'if [ -z "$RC" ]; then wait $CURLPID 2>/dev/null; RC=$?; fi',
|
||||
'SZ=$(wc -c < "$TMP" 2>/dev/null || echo 0)',
|
||||
"if [ $STOPTED -eq 1 ]; then ws stopped \"$SZ\" \"$TOT\" 0 '已停止 (保留已下载部分, 点[下载并重启]可续传)'; exit 0; fi",
|
||||
"if [ \"$RC\" -ne 0 ]; then ws error \"$SZ\" \"$TOT\" 0 '下载失败 (GitHub 波动/网络), 点[下载并重启]可自动续传重试'; exit 0; fi",
|
||||
"if [ \"$USE_SHA\" -eq 1 ]; then",
|
||||
"API_SHA=$(curl -L -sS --connect-timeout 10 --max-time 20 \"$API\" | grep -oE 'sha256:[0-9a-f]{64}' | head -1 | sed 's/sha256://')",
|
||||
"LOC_SHA=$(sha256sum \"$TMP\" 2>/dev/null | awk '{print $1}')",
|
||||
'if [ "${#API_SHA}" -ge 64 ] && [ "${#LOC_SHA}" -ge 64 ] && [ "$API_SHA" != "$LOC_SHA" ]; then',
|
||||
'rm -f "$TMP"; ws error "$SZ" "$TOT" 0 "SHA256 不匹配, 文件可能损坏, 已丢弃, 请重试"; exit 0',
|
||||
"fi",
|
||||
"fi",
|
||||
"# curl -o 落盘不带可执行位, -h 前先 chmod, 否则任何架构 (包括正确 arm64) 都会 Permission denied",
|
||||
"chmod +x \"$TMP\" 2>/dev/null",
|
||||
"if ! \"$TMP\" -h >/dev/null 2>&1; then rm -f \"$TMP\"; ws error \"$SZ\" \"$TOT\" 0 '文件无法执行, 可能架构不匹配 (应为 linux/arm64 静态二进制)'; exit 0; fi",
|
||||
"if ! mv -f \"$TMP\" \"$BIN\"; then ws error \"$SZ\" \"$TOT\" 0 '替换 $BIN 失败'; exit 0; fi",
|
||||
"# 写收据: 本版本已在盘上, 下次点击命中 SKIP 不再重复下载 (仅默认 URL 有 API_SHA)",
|
||||
"[ \"$USE_SHA\" -eq 1 ] && echo \"$API_SHA\" > \"$RECEIPT\" 2>/dev/null",
|
||||
"if /etc/init.d/caddy restart >/dev/null 2>&1; then ws done \"$SZ\" \"$TOT\" 0 '下载完成, 已替换并重启 Caddy'; else ws done \"$SZ\" \"$TOT\" 0 '下载完成并已替换, 但重启 Caddy 失败, 请手动 /etc/init.d/caddy restart'; fi",
|
||||
}
|
||||
local f = io.open("/tmp/caddy_dl.sh", "w")
|
||||
f:write(table.concat(L, "\n") .. "\n")
|
||||
f:close()
|
||||
os.execute("chmod +x /tmp/caddy_dl.sh")
|
||||
-- 先清掉旧的下载进程 (脚本本体 + 它遗留的写 caddy.dl 的 curl 子进程, 否则多实例互相覆盖 status)
|
||||
-- ps 会截断命令行导致漏杀 curl, 故直接扫 /proc/*/cmdline; 用 [.] 自转义 glob 避免匹配到本命令自身
|
||||
os.execute("for p in /proc/[0-9]*; do c=$(tr '\\0' ' ' < $p/cmdline 2>/dev/null); case \"$c\" in *caddy_dl[.]sh*|*caddy[.]dl*) kill ${p##*/} 2>/dev/null;; esac; done; sleep 1; rm -f /tmp/caddy_dl.status")
|
||||
-- nohup 脱离 CGI, 页面刷新也不中断
|
||||
os.execute("nohup sh /tmp/caddy_dl.sh >/dev/null 2>&1 &")
|
||||
luci.http.prepare_content("application/json")
|
||||
-- 注意: call 端点内 _ 未注入 (只在 index 阶段有), 这里用字面量; 该消息仅作启动占位, 随即被进度轮询覆盖
|
||||
luci.http.write_json({ ok = true, msg = "已开始后台下载 (支持断点续传), 自动刷新进度..." })
|
||||
end
|
||||
|
||||
@@ -1,127 +1,56 @@
|
||||
local m, s
|
||||
m = Map("caddy", translate("Caddy"), translate("Caddy 是一个可扩展的服务器平台,具有自动 HTTPS 功能的快速且可扩展的多平台 HTTP/1-2-3 Web 服务器") .. "<br/>" .. "项目地址:" .. [[<a href="https://github.com/caddyserver/caddy" target="_blank">]] .. translate("github.com/caddyserver/caddy") .. "</a> " .. " caddy文档:" .. [[<a href="https://caddyserver.com/docs/" target="_blank">]] .. translate("caddyserver.com/docs/") .. [[</a>]])
|
||||
local m, s, o
|
||||
|
||||
m:section(SimpleSection).template = "caddy/caddy_status"
|
||||
m = Map("caddy", translate("Caddy 状态"),
|
||||
translate("全局配置: 启用 Caddy / 公共开关 / 程序与日志路径. 反向代理 Host 在 [Host] 标签页增删改查") .. "<br/>" ..
|
||||
[[<a href="https://github.com/andangel/luci-app-caddy" target="_blank">]] ..
|
||||
translate("github.com/andangel/luci-app-caddy") .. "</a> " ..
|
||||
[[<a href="https://caddyserver.com/docs/" target="_blank">]] ..
|
||||
translate("caddyserver.com/docs/ (Caddy 官方文档)") .. [[</a>]])
|
||||
|
||||
m:section(SimpleSection).template = "caddy/caddy_status"
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- 全局设置 (单例) - JSON 里的 global 对象, 只有一条, 只有"改", 没有增删
|
||||
----------------------------------------------------------------------
|
||||
s = m:section(TypedSection, "caddy")
|
||||
s.addremove = false
|
||||
s.anonymous = true
|
||||
|
||||
o = s:option(Flag, "enabled", translate("Enabled"))
|
||||
o = s:option(Flag, "enabled", translate("启用 Caddy"))
|
||||
o.rmempty = false
|
||||
o.default = 0
|
||||
o.default = 1
|
||||
|
||||
o = s:option(Button, "btnrm", translate("重启"))
|
||||
o.inputtitle = translate("重启")
|
||||
o.description = translate("在没有修改参数的情况下快速重新启动一次")
|
||||
o = s:option(Button, "restart", translate("重启 Caddy"))
|
||||
o.inputtitle = translate("重启 Caddy")
|
||||
o.description = translate("按全局 + Host 配置重新生成 Caddyfile 并重启 Caddy")
|
||||
o.inputstyle = "apply"
|
||||
o:depends("enabled", "1")
|
||||
o.write = function()
|
||||
os.execute("/etc/init.d/caddy restart")
|
||||
os.execute("/etc/init.d/caddy restart")
|
||||
end
|
||||
|
||||
e=s:option(ListValue,"cmd",translate("启动方式"),
|
||||
translate("自定义配置文件启动,若不懂参数请勿选择自定义"))
|
||||
e:value("默认")
|
||||
e:value("自定义")
|
||||
|
||||
o = s:option(TextValue, "caddyfile", translate("Caddyfile配置文件"),
|
||||
translate("这是caddy的启动配置文件Caddyfile,路径在/etc/caddy/Caddyfile<br>使用的命令是:caddy run --config /etc/caddy/Caddyfile --adapter caddyfile<br>如需设置密码,这里就不能使用明文密码,需要转换<br>转换命令:$(uci -q get caddy.@caddy[0].bin_dir) hash-password --plaintext 新密码"))
|
||||
o.rows = 3
|
||||
o.wrap = "off"
|
||||
o:depends("cmd", "自定义")
|
||||
|
||||
o = s:option(Value, "port", translate("端口"))
|
||||
o.datatype = "and(port,min(1))"
|
||||
o.default = "12311"
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Flag,"file_pass", translate("启用 认证"))
|
||||
o.datatype = "string"
|
||||
o.default = "0"
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Value,"file_username", translate("用户名"))
|
||||
o.datatype = "string"
|
||||
o.default = "admin"
|
||||
o:depends("file_pass", "1")
|
||||
|
||||
o = s:option(Value,"file_password", translate("密码"))
|
||||
o.datatype = "string"
|
||||
o.password = true
|
||||
o.default = "123456"
|
||||
o:depends("file_pass", "1")
|
||||
|
||||
o = s:option(Flag, "filezip", translate("启用 压缩"))
|
||||
o.default = 0
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Flag, "log", translate("启用日志"))
|
||||
o.default = 1
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Value, "log_dir", translate("日志路径"),
|
||||
translate("日志的存放路径,填写完整的路径及日志文件名<br>建议/tmp里,例如:/tmp/caddy/requests.log"))
|
||||
o.datatype = "string"
|
||||
o.default = "/tmp/caddy/requests.log"
|
||||
o:depends("log", "1")
|
||||
|
||||
o = s:option(Value, "bin_dir", translate("程序路径"),
|
||||
translate("caddy二进制的存放路径,填写完整的路径及caddy名称<br>例如:/usr/bin/caddy 例如:/tmp/caddy<br>可自行编译对应架构的二进制程序:<a href='https://www.right.com.cn/forum/forum.php?mod=viewthread&tid=6006472&highlight=caddy' target='_blank'>编译教程</a>"))
|
||||
o.datatype = "string"
|
||||
o.default = "/usr/bin/caddy"
|
||||
|
||||
o = s:option(Value, "data_dir", translate("指向路径"),
|
||||
translate("指向一个路径,可在web界面访问你的文件,默认为 /mnt"))
|
||||
o.datatype = "string"
|
||||
o.default = "/mnt"
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Flag, "webdav", translate("启用 webdav"))
|
||||
o.default = "0"
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Flag,"webdav_pass", translate("启用 认证"))
|
||||
o.datatype = "string"
|
||||
o:depends("webdav", "1")
|
||||
o.default = "0"
|
||||
|
||||
o = s:option(Value,"webdav_username", translate("用户名"))
|
||||
o.datatype = "string"
|
||||
o.default = "admin"
|
||||
o:depends("webdav_pass", "1")
|
||||
|
||||
o = s:option(Value,"webdav_password", translate("密码"))
|
||||
o.datatype = "string"
|
||||
o.password = true
|
||||
o.default = "123456"
|
||||
o:depends("webdav_pass", "1")
|
||||
|
||||
o = s:option(Value,"webdav_port", translate("webdav端口"))
|
||||
o.datatype = "and(port,min(1))"
|
||||
o.default = "12322"
|
||||
o:depends("webdav", "1")
|
||||
|
||||
o = s:option(Flag, "webzip", translate("启用 压缩"))
|
||||
o.default = 0
|
||||
o:depends("webdav", "1")
|
||||
|
||||
o = s:option(Value, "webdav_data_dir", translate("webdav指向路径"),
|
||||
translate("指向一个路径,使用webdav访问你的文件,默认为 /mnt<br>连接地址须加/dav后缀 如: 192.168.1.1:12311/dav"))
|
||||
o.datatype = "string"
|
||||
o:depends("webdav", "1")
|
||||
o.default = "/mnt"
|
||||
|
||||
o = s:option(Flag, "allow_wan", translate("允许从外网访问"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Flag, "api", translate("启用 API接口"))
|
||||
o:depends("cmd", "默认")
|
||||
|
||||
o = s:option(Button, "admin_info", translate("检测配置文件"),
|
||||
translate("验证Caddyfile配置文件是否正确,它会模拟启动caddy<br>但是并不会真的启动,会列出详细信息,以便修正配置文件"))
|
||||
|
||||
o = s:option(Button, "validate", translate("检测配置文件"))
|
||||
o.rawhtml = true
|
||||
o.template = "caddy/admin_info"
|
||||
|
||||
--- 主程序 ---
|
||||
o = s:option(Value, "bin_dir", translate("主程序"),
|
||||
translate("caddy 二进制完整路径"))
|
||||
o.default = "/usr/sbin/caddy"
|
||||
o.placeholder = "/usr/sbin/caddy"
|
||||
|
||||
o = s:option(Value, "download_url", translate("下载地址(可选)"),
|
||||
translate("留空用默认 GitHub latest Release. 填了则用该 URL (可换镜像/固定版本). 下载带重试+SHA256 校验"))
|
||||
o.placeholder = "https://github.com/andangel/luci-app-caddy/releases/latest/download/caddy_linux_arm64"
|
||||
|
||||
o = s:option(Button, "download", translate("下载并重启"))
|
||||
o.rawhtml = true
|
||||
o.template = "caddy/download"
|
||||
|
||||
--- 日志 ---
|
||||
o = s:option(Value, "log_dir", translate("日志"),
|
||||
translate("各 Host 的访问日志写入此文件, 建议放 /tmp 以免占用闪存"))
|
||||
o.default = "/tmp/caddy/requests.log"
|
||||
o.placeholder = "/tmp/caddy/requests.log"
|
||||
|
||||
return m
|
||||
@@ -0,0 +1,45 @@
|
||||
local m, s, o
|
||||
|
||||
-- 手动 Caddyfile 编辑: 路径从 UCI caddy_file 取 (默认 /etc/caddy/Caddyfile),
|
||||
-- 与 init.d 里 G_FILE 一致, 避免用户在 [状态] 页改 bin_dir/log_dir 后此处仍写死路径.
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
-- get_first 返回的是 section name 而非字段值, 必须用 get(cfg, section, key) 取字段
|
||||
local cf_file = uci:get("caddy", "caddy", "caddy_file") or "/etc/caddy/Caddyfile"
|
||||
|
||||
local cur_caddyfile
|
||||
local cfh = io.open(cf_file, "r")
|
||||
if cfh then
|
||||
cur_caddyfile = cfh:read("*a")
|
||||
cfh:close()
|
||||
end
|
||||
|
||||
m = Map("caddy", translate("Caddy 配置 (手动)"),
|
||||
translate("直接编辑 当前 Caddyfile (" .. cf_file .. "). 默认载入当前文件. 注意: 在 [主机] 页重启 Caddy 时会按表单重新生成并覆盖此页内容"))
|
||||
|
||||
s = m:section(TypedSection, "caddy")
|
||||
s.addremove = false
|
||||
s.anonymous = true
|
||||
|
||||
o = s:option(TextValue, "caddyfile", translate("Caddyfile"),
|
||||
translate("完整 Caddyfile. 要 Caddy 的任何功能 (forward_proxy / encode / basicauth / reverse_proxy ...) 都写在这里<br/>"
|
||||
.. "如需设置密码, 用命令生成 hash: caddy hash-password --plaintext 新密码"))
|
||||
o.default = cur_caddyfile
|
||||
o.rows = 25
|
||||
o.wrap = "off"
|
||||
|
||||
o = s:option(Button, "apply_caddyfile", translate("写入并重启 Caddy"))
|
||||
o.inputtitle = translate("写入并重启 Caddy")
|
||||
o.description = translate("把上方内容写入当前 Caddyfile 并重启 Caddy (下次 [主机] 页重启 Caddy 会覆盖)")
|
||||
o.inputstyle = "apply"
|
||||
o.write = function(_, value)
|
||||
-- 经 /tmp 中转, 避免写一半坏掉 Caddyfile; 写完 mv 原子替换
|
||||
local tmp = "/tmp/caddyfile_manual.tmp"
|
||||
local f = io.open(tmp, "w")
|
||||
if f then
|
||||
f:write(value or "")
|
||||
f:close()
|
||||
os.execute(string.format("cp '%s' '%s' && rm -f '%s' && /etc/init.d/caddy restart", tmp, cf_file, tmp))
|
||||
end
|
||||
end
|
||||
|
||||
return m
|
||||
@@ -0,0 +1,65 @@
|
||||
local m, s, o
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
|
||||
m = Map("caddy", translate("Caddy Domain"),
|
||||
translate("域名列表 (增删改查). 每条域名对应一个阿里云账号 (AK), 该域名的 TLS 证书和 DDNS 都用这份 AK"))
|
||||
|
||||
--- Domain (多条) - 增删改查, 每条 = 一个域名 + 对应的阿里云 AK + DDNS 参数 ---
|
||||
--- anonymous: 用户点"添加"即可, 无需填 UCI 标识符 (域名含 . 不合 UCI 命名规范) ---
|
||||
s = m:section(TypedSection, "domain")
|
||||
s.addremove = true
|
||||
s.anonymous = true
|
||||
s.nodeps = true
|
||||
s.cfgtitle = "domain_name"
|
||||
|
||||
o = s:option(Value, "domain_name", translate("域名"),
|
||||
translate("例如 example.com. Host 页的监听域名下拉从这里取"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "key_id", translate("阿里云 Access Key ID"),
|
||||
translate("该域名的 AliDNS 写权限 AK. 需要 AliyunDNS 的 Read/Write 权限"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Value, "key_secret", translate("阿里云 Access Key Secret"))
|
||||
o.password = true
|
||||
o.rmempty = false
|
||||
|
||||
--- DDNS 子块: 勾选 ddns_on 才显示以下字段 ---
|
||||
o = s:option(Flag, "ddns_on", translate("启用 DDNS (更新 A 记录)"),
|
||||
translate("勾选 = 生成的 Caddyfile 包含该域名的 aliyun_ddns 块, 定期把公网 IP 写入 A 记录"))
|
||||
o.default = 0
|
||||
|
||||
o = s:option(TextValue, "ddns_records", translate("额外记录名 (每行一条)"),
|
||||
translate("每行一个子域名前缀, 用换行分隔, 不要带主域名. 例如填 mail 会更新 mail.example.com, 填 www 会更新 www.example.com. 留空 = 只更新 @ 主域 A 记录"))
|
||||
o.rows = 3
|
||||
o.wrap = "off"
|
||||
o:depends("ddns_on", "1")
|
||||
|
||||
o = s:option(ListValue, "ddns_wan_iface", translate("IP 获取方式"),
|
||||
translate("public = 公网查询 (api.ipify.org 等取出口 IP); 选接口 = 直读该网卡 IPv4. 一般填 pppoe-wan, 下拉自动列出路由器实际接口"))
|
||||
o:value("public")
|
||||
local netf = io.popen("ls /sys/class/net 2>/dev/null")
|
||||
if netf then
|
||||
for line in netf:lines() do
|
||||
local name = line:match("%S+")
|
||||
-- 只保留 WAN 口 (名字含 wan, 如 pppoe-wan / eth0-wan), 过滤掉 LAN/WiFi/桥等无关设备
|
||||
if name and name:lower():find("wan", 1, true) then
|
||||
o:value(name)
|
||||
end
|
||||
end
|
||||
netf:close()
|
||||
end
|
||||
o.default = "pppoe-wan"
|
||||
o:depends("ddns_on", "1")
|
||||
|
||||
o = s:option(Value, "ddns_check_interval", translate("检查间隔"),
|
||||
translate("多久检查一次 A 记录是否需要更新"))
|
||||
o.default = "5m"
|
||||
o:depends("ddns_on", "1")
|
||||
|
||||
o = s:option(Value, "ddns_ttl", translate("TTL"),
|
||||
translate("A 记录 TTL, 影响公网 DNS 生效速度"))
|
||||
o.default = "60s"
|
||||
o:depends("ddns_on", "1")
|
||||
|
||||
return m
|
||||
@@ -0,0 +1,80 @@
|
||||
local m, s, gs, o
|
||||
|
||||
m = Map("caddy", translate("Caddy Host"),
|
||||
translate("反向代理 Host 列表 (增删改查). 每条 Host 对应 Caddyfile 里一段 域名:端口 { ... }"))
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- 通用开关 (全局, 单例 @caddy[0]). 放在 Host 列表上方: 跨所有 Host 生效,
|
||||
-- 不是某一条 Host 的属性. 后续全局项 (如日志开关) 也加在这个 section 里
|
||||
----------------------------------------------------------------------
|
||||
gs = m:section(TypedSection, "caddy")
|
||||
gs.addremove = false
|
||||
gs.anonymous = true
|
||||
gs.nodeps = true
|
||||
|
||||
o = gs:option(Flag, "disable_redirect", translate("保留 HTTP (禁用自动跳转 HTTPS)"),
|
||||
translate("勾选 = Caddyfile 写 auto_https disable_redirects; 不勾 = 不写, Caddy 默认 HTTP 自动 301 到 HTTPS"))
|
||||
o.default = 1
|
||||
|
||||
o = gs:option(Flag, "log_on", translate("请求日志"),
|
||||
translate("勾选 = 各 Host 写访问日志到 [状态] 页的 日志 路径; 不勾 = Caddyfile 不生成 log 块, 不写日志, 省资源"))
|
||||
o.default = 1
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- Host (多条) - 增删改查, NPM 的 Proxy Hosts
|
||||
----------------------------------------------------------------------
|
||||
s = m:section(TypedSection, "proxy")
|
||||
s.addremove = true
|
||||
s.anonymous = true
|
||||
s.nodeps = true
|
||||
s.cfgtitle = "name"
|
||||
|
||||
o = s:option(Value, "name", translate("名称"),
|
||||
translate("给这条 Host 起个名, 便于区分"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(Flag, "enabled", translate("启用"))
|
||||
o.default = 1
|
||||
|
||||
o = s:option(ListValue, "domain", translate("监听域名"),
|
||||
translate("从 [Domain] 页的域名列表中选择. 每个域名列两个选项: 裸域 (签 example.com 一张证书, 覆盖裸域) 和 *. 通配符 (签 裸域+通配符 各一张, 覆盖裸域+全部子域, 等价 acme.sh 两条 -d). 只要 Domain 页填了 AK 就一律走 DNS-01, 不依赖 80/443 入站. 需先在 Domain 页创建域名"))
|
||||
o.rmempty = false
|
||||
-- 从 UCI 文件读 domain_name: 直接读 /etc/config/caddy, 匹配有引号和没引号两种风格.
|
||||
-- 比 grep + sed 更稳 (不靠 shell pipeline 转义), 比 uci cursor 更简单 (不依赖 cursor API 版本差异).
|
||||
local f = io.open("/etc/config/caddy", "r")
|
||||
if f then
|
||||
for line in f:lines() do
|
||||
local dn = line:match("option%s+domain_name%s+'([^']+)'")
|
||||
or line:match("option%s+domain_name%s+([%w%.%-]+)")
|
||||
if dn and #dn > 0 then
|
||||
o:value(dn)
|
||||
o:value("*." .. dn)
|
||||
end
|
||||
end
|
||||
f:close()
|
||||
end
|
||||
|
||||
o = s:option(Value, "port", translate("监听端口"),
|
||||
translate("非标端口可防指纹扫描, 例如 443 / 8443"))
|
||||
o.datatype = "and(port,min(1))"
|
||||
o.default = "443"
|
||||
|
||||
o = s:option(Value, "target", translate("转发目标"),
|
||||
translate("内网后端地址, 形如 192.168.1.x:8080"))
|
||||
o.rmempty = false
|
||||
|
||||
o = s:option(ListValue, "tls_cert", translate("TLS 证书"),
|
||||
translate("auto = Caddy 自动签发+自动续签 (Domain 页填了 AK 走 DNS-01, 不依赖 80/443 入站); off = 纯 HTTP 不加密 (内网调试用)"))
|
||||
o.default = "auto"
|
||||
o:value("auto")
|
||||
o:value("off")
|
||||
|
||||
o = s:option(Value, "api_key", translate("API Key (可选)"),
|
||||
translate("留空 = Caddy 不做 key 校验 (交给后端); 填值 = Caddy 层校验 Authorization: Bearer <key>"))
|
||||
|
||||
o = s:option(TextValue, "extra", translate("自定义指令 (可选)"),
|
||||
translate("Caddyfile 语法, 原样插入此 Host 块末尾. 用于 path 路由 / rate_limit / encode / basicauth / handle 等表单没覆盖的指令"))
|
||||
o.rows = 6
|
||||
o.wrap = "off"
|
||||
|
||||
return m
|
||||
@@ -4,7 +4,7 @@
|
||||
{
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Collecting data...%>';
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "nas", "caddy", "admin_info")%>',
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "network", "caddy", "admin_info")%>',
|
||||
null,
|
||||
function(x,rv)
|
||||
{
|
||||
|
||||
@@ -1,124 +1,167 @@
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
/* caddy_log: 纯 LuCI 风格, XHR + DOM 拼接, 无外部框架 (与 caddy_status.htm 一致) */
|
||||
var LOG_URL = '<%=url("admin/network/caddy/get_log")%>';
|
||||
var CLEAR_URL = '<%=url("admin/network/caddy/clear_log")%>';
|
||||
var logPaused = false;
|
||||
var logFilter = 'all';
|
||||
|
||||
function logStatusClass(c) {
|
||||
if (c >= 500) return 'err';
|
||||
if (c >= 400) return 'warn';
|
||||
return 'ok';
|
||||
}
|
||||
|
||||
function logEscape(s) {
|
||||
var d = document.createElement('div');
|
||||
d.appendChild(document.createTextNode(String(s)));
|
||||
return d.innerHTML;
|
||||
}
|
||||
|
||||
function logRender() {
|
||||
var tb = document.getElementById('caddy_log_list');
|
||||
var cnt = document.getElementById('caddy_log_count');
|
||||
if (!tb) return;
|
||||
|
||||
XHR.get(LOG_URL, null, function(x) {
|
||||
if (!x || x.status !== 200) return;
|
||||
var logs = [];
|
||||
x.responseText.split('\n').filter(Boolean).forEach(function(l, i) {
|
||||
try {
|
||||
var o = JSON.parse(l);
|
||||
o.__id = i;
|
||||
logs.push(o);
|
||||
} catch (e) { }
|
||||
});
|
||||
logs.reverse();
|
||||
|
||||
if (logFilter !== 'all')
|
||||
logs = logs.filter(function(l) { return logStatusClass(l.status) === logFilter; });
|
||||
|
||||
if (cnt) cnt.innerHTML = logs.length + ' <em><%:条日志%></em>';
|
||||
|
||||
if (logs.length === 0) {
|
||||
tb.innerHTML = '<em><%:暂无日志%></em>';
|
||||
return;
|
||||
}
|
||||
|
||||
tb.innerHTML = logs.map(function(l) {
|
||||
var h = (l.request && l.request.headers) || {};
|
||||
var rows = [];
|
||||
if (l.ts)
|
||||
rows.push(['时间', new Date(l.ts * 1000).toLocaleString()]);
|
||||
if (l.request)
|
||||
rows.push(['访问', l.request.host + l.request.uri]);
|
||||
if (l.request && l.request.method)
|
||||
rows.push(['方法', l.request.method]);
|
||||
if (l.status)
|
||||
rows.push(['状态', l.status]);
|
||||
var ip = (h['Cf-Connecting-Ip'] && h['Cf-Connecting-Ip'][0]) || (l.request && l.request.client_ip);
|
||||
if (ip)
|
||||
rows.push(['IP', ip]);
|
||||
var ua = h['User-Agent'] ? h['User-Agent'][0] : null;
|
||||
if (ua)
|
||||
rows.push(['标识', ua]);
|
||||
if (l.duration)
|
||||
rows.push(['耗时', (l.duration * 1000).toFixed(2) + ' ms']);
|
||||
|
||||
return '<div class="log-card ' + logStatusClass(l.status) + '">' +
|
||||
rows.map(function(r) {
|
||||
return '<div class="log-row"><span class="log-label">' + logEscape(r[0]) + ':</span>' + logEscape(r[1]) + '</div>';
|
||||
}).join('') +
|
||||
'<div class="log-row"><input type="button" class="btn cbi-button view-json" data-idx="' + l.__id + '" value="<%:查看原始 JSON%>" /></div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
var list = document.getElementById('caddy_log_list');
|
||||
list.querySelectorAll('.view-json').forEach(function(btn) {
|
||||
btn.onclick = function() {
|
||||
logShowJson(logs[+btn.getAttribute('data-idx')]);
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function logShowJson(obj) {
|
||||
if (!obj) return;
|
||||
var copy = {};
|
||||
for (var k in obj) { if (k !== '__id') copy[k] = obj[k]; }
|
||||
document.getElementById('log_modal_body').textContent = JSON.stringify(copy, null, 2);
|
||||
document.getElementById('log_modal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function logCloseModal() {
|
||||
document.getElementById('log_modal').style.display = 'none';
|
||||
}
|
||||
|
||||
setInterval(function() {
|
||||
if (!logPaused) logRender();
|
||||
}, 3000);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
logRender();
|
||||
document.getElementById('log_toggle').onclick = function() {
|
||||
logPaused = !logPaused;
|
||||
this.value = logPaused ? '<%:继续刷新%>' : '<%:暂停刷新%>';
|
||||
};
|
||||
document.getElementById('log_clear').onclick = function() {
|
||||
XHR.get(CLEAR_URL, null, function() {
|
||||
var tb = document.getElementById('caddy_log_list');
|
||||
var cnt = document.getElementById('caddy_log_count');
|
||||
if (tb) tb.innerHTML = '<em><%:暂无日志%></em>';
|
||||
if (cnt) cnt.innerHTML = '0 <em><%:条日志%></em>';
|
||||
});
|
||||
};
|
||||
document.getElementById('log_filter').onchange = function() {
|
||||
logFilter = this.value;
|
||||
logRender();
|
||||
};
|
||||
});
|
||||
//]]></script>
|
||||
|
||||
<style>
|
||||
:root { --bg:#ffffff; --card:#f6f8fa; --text:#222; --border:#ddd; --muted:#666; }
|
||||
@media (prefers-color-scheme: dark) { :root { --bg:#0f1115; --card:#1b1e24; --text:#e5e7eb; --border:#333; --muted:#9ca3af; } }
|
||||
body { background:var(--bg); color:var(--text); margin:0; }
|
||||
|
||||
#app { all: unset; display:block; width:100%; padding:12px; font-family:system-ui,-apple-system; text-align:left; }
|
||||
.toolbar { display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; }
|
||||
.toolbar select { margin-left:4px; }
|
||||
|
||||
button { border-radius:6px; padding:6px 10px; border:1px solid var(--border); background:transparent; color:var(--text); cursor:pointer; transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.1s ease; }
|
||||
button:active { transform: scale(0.95); box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); background: rgba(0,0,0,0.05); }
|
||||
button.toggle-btn { background:#d0a026; color:#fff; border:none; }
|
||||
button.clear-log { background:#e74c3c; color:#fff; border:none; }
|
||||
button.view-json { background:#60a5fa; color:#fff; border:none; }
|
||||
|
||||
.log-card { all: unset; display:block; width:100%; padding:12px; margin-bottom:12px; background:var(--card); border-radius:10px; text-align:left; box-sizing:border-box; overflow:hidden; border-width:2px; border-style:solid; border-left-color:#888; }
|
||||
.log-card.ok { border-color:#2ecc71; }
|
||||
.log-card.warn { border-color:#f39c12; }
|
||||
.log-card.err { border-color:#e74c3c; }
|
||||
|
||||
.log-inner { all: unset; display:block; width:100%; text-align:left; overflow-wrap:break-word; word-break:break-word; }
|
||||
.log-row { all: unset; display:flex; width:100%; font-size:13px; line-height:1.8; word-break:break-all; margin-bottom:2px; text-align:left; align-items:flex-start; }
|
||||
.log-row .label { all: unset; flex-shrink:0; color:var(--muted); margin-right:8px; }
|
||||
.log-row a { color:#2563eb; text-decoration:underline; font-size:13px; }
|
||||
|
||||
.modal { display:none; position:fixed; z-index:9999; left:0; top:0; width:100%; height:100%; background-color: rgba(0,0,0,0.5); justify-content:center; align-items:center; overflow:auto; }
|
||||
.modal.show { display:flex; }
|
||||
.modal-content { background:#fff; border-radius:6px; max-width:700px; width:80%; max-height:70vh; overflow:auto; white-space: pre; position:relative; display:flex; flex-direction:column; }
|
||||
|
||||
.modal-header { display:flex; justify-content:space-between; align-items:center; padding:10px 16px; border-bottom:1px solid #ccc; background:#f3f3f3; border-top-left-radius:6px; border-top-right-radius:6px; flex-shrink:0; }
|
||||
.modal-header h3 { margin:0; font-size:16px; }
|
||||
.modal-close { padding:4px 8px; border:none; border-radius:6px; background:#393fec; color:#fff; cursor:pointer; }
|
||||
|
||||
.modal-body { padding:12px; overflow:auto; flex:1; }
|
||||
|
||||
#toTop { position:fixed; right:18px; bottom:120px; padding:8px 14px; border-radius:20px; background:#8644f1; color:#fff; display:none; z-index:9999; cursor:pointer; transition: transform 0.1s ease, box-shadow 0.1s ease, background 0.1s ease; }
|
||||
#toTop.show { display:block; }
|
||||
#toTop:active { transform: scale(0.95); box-shadow: inset 0 2px 4px rgba(0,0,0,0.2); background: rgba(0,0,0,0.05); }
|
||||
|
||||
@media (max-width:600px){ .log-row{font-size:12px;} }
|
||||
|
||||
@media (prefers-color-scheme: dark){
|
||||
body { background:#0f1115; color:#e5e7eb; }
|
||||
.log-card { background:#1b1e24; border-color:#333; color:#e5e7eb; }
|
||||
.modal-content { background:#1b1e24; color:#e5e7eb; }
|
||||
.modal-header { background:#2b2e36; border-bottom-color:#444; }
|
||||
button.toggle-btn { background:#d0a026; color:#fff; }
|
||||
button.clear-log { background:#b91c1c; color:#fff; }
|
||||
button.view-json { background:#2563eb; color:#fff; }
|
||||
.modal-close { background:#393fec; color:#fff; }
|
||||
.log-card {
|
||||
background:rgba(0,0,0,0.04);
|
||||
border-left:3px solid #2ecc71;
|
||||
border-radius:6px;
|
||||
padding:8px 10px;
|
||||
margin-bottom:10px;
|
||||
}
|
||||
.log-card.warn { border-left-color:#f39c12; }
|
||||
.log-card.err { border-left-color:#e74c3c; }
|
||||
.log-row { font-size:13px; line-height:1.8; word-break:break-all; }
|
||||
.log-label { opacity:0.6; margin-right:6px; }
|
||||
#log_modal { display:none; position:fixed; z-index:9999; left:0; top:0; width:100%; height:100%; background:rgba(0,0,0,0.5); }
|
||||
/* 弹窗固定暗色终端风: LuCI bootstrap-dark 主题走自己的 cascade.css,
|
||||
不触发 prefers-color-scheme, 媒体查询不可靠; 且 JSON 适合暗底亮字 */
|
||||
#log_modal .modal-box { background:#1e222a; color:#d8dee6; max-width:700px; width:85%; max-height:70vh; overflow:auto; border-radius:6px; border:1px solid rgba(255,255,255,0.15); margin:8vh auto 0; }
|
||||
#log_modal .modal-box b { color:#fff; }
|
||||
#log_modal_body { margin:8px 12px; white-space:pre-wrap; word-break:break-all; font-size:12px; color:#d8dee6; }
|
||||
</style>
|
||||
|
||||
<div id="app">
|
||||
<div class="toolbar">
|
||||
<fieldset class="cbi-section">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:8px;margin-bottom:10px;">
|
||||
<div>
|
||||
<button type="button" class="toggle-btn" @click="toggle">{{ paused ? '继续刷新' : '暂停刷新' }}</button>
|
||||
<button type="button" class="clear-log" @click="clear">清空日志</button>
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" id="log_toggle" value="<%:暂停刷新%>" />
|
||||
<input type="button" class="btn cbi-button" id="log_clear" value="<%:清空日志%>" />
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;">
|
||||
<label>
|
||||
<select v-model="filterType">
|
||||
<option value="all">全部</option>
|
||||
<option value="warn">警告</option>
|
||||
<option value="err">错误</option>
|
||||
</select>
|
||||
</label>
|
||||
<span>共 {{ filteredLogs.length }} 条日志</span>
|
||||
<div>
|
||||
<select id="log_filter">
|
||||
<option value="all"><%:全部%></option>
|
||||
<option value="warn"><%:警告%></option>
|
||||
<option value="err"><%:错误%></option>
|
||||
</select>
|
||||
<span id="caddy_log_count">0 <em><%:条日志%></em></span>
|
||||
</div>
|
||||
</div>
|
||||
<p id="caddy_log_list"><em><%:加载中...%></em></p>
|
||||
</fieldset>
|
||||
|
||||
<div v-for="log in filteredLogs" :key="log.id" class="log-card" :class="statusClass(log.status)">
|
||||
<div class="log-inner">
|
||||
<div class="log-row" v-if="log.ts"><span class="label">时间:</span>{{ time(log.ts) }}</div>
|
||||
<div class="log-row" v-if="log.request"><span class="label">访问:</span>{{ fullUrl(log) }}</div>
|
||||
<div class="log-row" v-if="log.request && log.request.method"><span class="label">方法:</span>{{ log.request.method }}</div>
|
||||
<div class="log-row" v-if="log.status"><span class="label">状态:</span>{{ log.status }}</div>
|
||||
<div class="log-row" v-if="realIp(log)"><span class="label">IP:</span>{{ realIp(log) }}</div>
|
||||
<div class="log-row" v-if="userAgent(log)"><span class="label">标识:</span>{{ userAgent(log) }}</div>
|
||||
<div class="log-row" v-if="log.duration"><span class="label">耗时:</span>{{ (log.duration*1000).toFixed(2) }} ms</div>
|
||||
|
||||
<div class="log-row">
|
||||
<button type="button" class="view-json" @click.prevent="showModal(log)">查看原始 JSON 日志</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" :class="{show: modalLog}" @click.self="closeModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>完整日志</h3>
|
||||
<button class="modal-close" @click.prevent="closeModal">关闭</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<pre>{{ modalLog ? JSON.stringify(modalLog,null,2) : '' }}</pre>
|
||||
</div>
|
||||
<div id="log_modal" onclick="if(event.target===this)logCloseModal()">
|
||||
<div class="modal-box">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid #ccc;">
|
||||
<b><%:完整日志%></b>
|
||||
<input type="button" class="btn cbi-button" value="<%:关闭%>" onclick="logCloseModal()" />
|
||||
</div>
|
||||
<pre id="log_modal_body"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toTop">返回顶部</div>
|
||||
|
||||
<script>
|
||||
const { createApp } = Vue;
|
||||
createApp({
|
||||
data(){ return { logs: [], paused:false, filterType:'all', modalLog:null }; },
|
||||
computed:{ filteredLogs(){ if(this.filterType==='all') return this.logs; return this.logs.filter(l=>this.statusClass(l.status)===this.filterType); }},
|
||||
methods:{
|
||||
fetch(){ if(this.paused) return; XHR.get('<%=url([[admin]], [[nas]], [[caddy]], [[get_log]])%>', null, x=>{ if(x && x.status===200){ this.logs = x.responseText.split('\n').filter(Boolean).map((l,i)=>{ try{ let obj=JSON.parse(l); obj.id=i; return obj; }catch{return null;} }).filter(Boolean).reverse(); } }); },
|
||||
toggle(){ this.paused = !this.paused; },
|
||||
clear(){ XHR.get('<%=url([[admin]], [[nas]], [[caddy]], [[clear_log]])%>'); this.logs=[]; },
|
||||
time(ts){ return new Date(ts*1000).toLocaleString(); },
|
||||
fullUrl(l){ const h=l.request.headers||{}; const p=h['X-Forwarded-Proto']?h['X-Forwarded-Proto'][0]:'http'; return p+'://'+l.request.host+l.request.uri; },
|
||||
realIp(l){ const h=l.request.headers||{}; return h['Cf-Connecting-Ip']?h['Cf-Connecting-Ip'][0]:l.request.client_ip; },
|
||||
userAgent(l){ const h=l.request.headers||{}; return h['User-Agent']?h['User-Agent'][0]:'未知'; },
|
||||
statusClass(c){ if(c>=500) return 'err'; if(c>=400) return 'warn'; return 'ok'; },
|
||||
showModal(log){ this.modalLog = log; document.body.style.overflow = 'hidden'; },
|
||||
closeModal(){ this.modalLog = null; document.body.style.overflow = ''; }
|
||||
},
|
||||
mounted(){ this.fetch(); setInterval(this.fetch,2000); const btn=document.getElementById('toTop'); btn.onclick=()=>window.scrollTo({top:0,behavior:'smooth'}); window.addEventListener('scroll',()=>btn.classList.toggle('show',window.scrollY>400)); }
|
||||
}).mount('#app');
|
||||
</script>
|
||||
@@ -1,26 +1,19 @@
|
||||
<%
|
||||
local uci = require 'luci.model.uci'.cursor()
|
||||
ssl = uci:get_first('caddy', 'caddy', 'ssl')
|
||||
if ssl == '1' then
|
||||
protocol="http://"
|
||||
else
|
||||
protocol="http://"
|
||||
end
|
||||
%>
|
||||
%>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
XHR.poll(5, '<%=url("admin/nas/caddy/caddy_status")%>', null,
|
||||
XHR.poll(5, '<%=url("admin/network/caddy/caddy_status")%>', null,
|
||||
function(x, st) {
|
||||
var tb = document.getElementById('caddy_status');
|
||||
if (st && tb) {
|
||||
|
||||
var caddyStatus = st.running ? "<span style='color:green;'> 运行中</span><img src='https://www.right.com.cn/forum/data/attachment/album/202401/30/073058is17bcmu7z3nmn2b.gif' width='30px' alt=''>" : "<span style='color:red;'> 未运行</span>";
|
||||
var caddyStatus = st.running ? "<span style='color:green;'> 运行中</span>" : "<span style='color:red;'> 未运行</span>";
|
||||
var caddystaContent = st.running ? "已运行:" + "<span style='color:#DA70D6;'>" + st.caddysta + "</span>" : "";
|
||||
var caddycpuContent = st.running ? "<br>" + "CPU占用:" + "<span style='color:#6A5ACD;'>" + st.caddycpu + "</span>" : "";
|
||||
var caddyramContent = st.running ? " " + " 内存占用:" + "<span style='color:#0000CD;'>" + st.caddyram + "</span>" : "";
|
||||
var caddytagContent = st.running ? "<br>" + "当前版本:" + "<span style='color:#CD853F;'>" + st.caddytag + "</span>" : "";
|
||||
var caddynewtagContent = st.running ? " " + "最新版本:" + "<span style='color:#FFA500;'>" + st.caddynewtag + "</span>" : "";
|
||||
var caddywebContent = st.running ? "<input class=\"cbi-button cbi-button-reload mar-10\" type=\"button\" value=\" <%:Web 界面%> \" onclick=\"window.open('<%=protocol%>" + window.location.hostname + ":" + st.port + "/')\"/>" : "";
|
||||
var caddywebContent = (st.running && st.weburl) ? "<input class=\"cbi-button cbi-button-reload mar-10\" type=\"button\" value=\" <%:Web 界面%> \" onclick=\"window.open('" + st.weburl + "/')\"/>" : "";
|
||||
tb.innerHTML = "<b><em>Caddy " + caddyStatus + caddystaContent + caddycpuContent + caddyramContent + caddytagContent + caddynewtagContent + caddywebContent + "</b></em>";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<%+cbi/valueheader%>
|
||||
<span style="color:#808080"><%:后台下载 (断点续传). 点击后进度自动刷新: 已下载 / 总大小 / 速度 / 百分比. 中途刷新页面不会中断, 可暂停/继续/停止, 再点[下载并重启]会从断点继续%></span>
|
||||
<div style="margin:6px 0">
|
||||
<input type="button" id="<%=self.option%>-btn" class="btn cbi-button cbi-button-apply" value="<%:下载并重启%>" style="width:auto" onclick="return caddy_dl_start(this)" />
|
||||
<input type="button" id="<%=self.option%>-btn-pause" class="btn cbi-button cbi-button-edit" value="<%:暂停%>" style="width:auto;display:none" onclick="return cdl_ctl('pause')" />
|
||||
<input type="button" id="<%=self.option%>-btn-resume" class="btn cbi-button cbi-button-edit" value="<%:继续%>" style="width:auto;display:none" onclick="return cdl_ctl('resume')" />
|
||||
<input type="button" id="<%=self.option%>-btn-stop" class="btn cbi-button cbi-button-remove" value="<%:停止%>" style="width:auto;display:none" onclick="return cdl_ctl('stop')" />
|
||||
<span id="<%=self.option%>-pct" style="margin-left:10px;font-family:monospace"></span>
|
||||
</div>
|
||||
<div id="<%=self.option%>-barbox" style="display:none;width:100%;max-width:520px;background:#2b2b2b;border:1px solid #555;border-radius:4px;height:18px;overflow:hidden;margin:4px 0">
|
||||
<div id="<%=self.option%>-bar" style="height:100%;width:0%;background:#008000;transition:width 1s linear"></div>
|
||||
</div>
|
||||
<div id="<%=self.option%>-stat" style="font-family:monospace;color:#bbb;font-size:12px"></div>
|
||||
<div id="<%=self.option%>-msg"></div>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var cdl_tid = null;
|
||||
// 点击后的宽限窗口(ms 时间戳): 控制器先删旧 status 再启脚本, 新脚本要过 TOT 探测(几秒~几十秒)才写第一个 status,
|
||||
// 这段空窗轮询回来是 idle, 若按普通 idle 停轮询就再也没人刷新了 → 窗口内的 idle 保持轮询, 终态(done/error/stopped)照常停
|
||||
var cdl_until = 0;
|
||||
function cdl_mb(n){ return (n/1048576).toFixed(2) + ' MB'; }
|
||||
function cdl_esc(s){ return (s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
function cdl_render(rv)
|
||||
{
|
||||
var bar = document.getElementById('<%=self.option%>-bar');
|
||||
var box = document.getElementById('<%=self.option%>-barbox');
|
||||
var pct = document.getElementById('<%=self.option%>-pct');
|
||||
var stat = document.getElementById('<%=self.option%>-stat');
|
||||
var msg = document.getElementById('<%=self.option%>-msg');
|
||||
var btn = document.getElementById('<%=self.option%>-btn');
|
||||
var bP = document.getElementById('<%=self.option%>-btn-pause');
|
||||
var bR = document.getElementById('<%=self.option%>-btn-resume');
|
||||
var bS = document.getElementById('<%=self.option%>-btn-stop');
|
||||
var size = rv.size || 0;
|
||||
var total = rv.total || 0;
|
||||
var speed = rv.speed || 0;
|
||||
var text = cdl_mb(size) + (total ? ' / ' + cdl_mb(total) : '');
|
||||
if (total > 0) {
|
||||
var p = Math.floor(size * 100 / total);
|
||||
text += ' · ' + p + '%';
|
||||
}
|
||||
if (speed > 0) text += ' · ' + cdl_mb(speed) + '/s';
|
||||
pct.innerHTML = text;
|
||||
var st = rv.state || 'idle';
|
||||
// 定时器由这里统一管理: 下载/暂停/校验中保持轮询, 结束(done/error/stopped)则停
|
||||
// 窗口内的 idle(启动空窗)也算活动, 否则首次轮询落在空窗里就把轮询停死, UI 永远 idle
|
||||
var active = (st === 'downloading' || st === 'paused' || st === 'verifying' || (st === 'idle' && Date.now() < cdl_until));
|
||||
if (active && !cdl_tid) cdl_tid = setInterval(cdl_poll, 1500);
|
||||
else if (!active && cdl_tid) { clearInterval(cdl_tid); cdl_tid = null; }
|
||||
// 进度条
|
||||
if (bar && box) {
|
||||
if (st === 'downloading' || st === 'paused' || st === 'done' || st === 'verifying') box.style.display = 'block';
|
||||
var w = total ? Math.min(100, Math.floor(size*100/total)) : 0;
|
||||
bar.style.width = w + '%';
|
||||
}
|
||||
// 主按钮: 有已下载部分(error/stopped/有进度)时点它就是续传
|
||||
if (btn) {
|
||||
if (st === 'downloading') { btn.disabled = true; btn.value = '<%:下载中...%>'; } else if (st === 'lost') { btn.disabled = false; btn.value = '<%:重新下载 (自动续传)%>'; } else if (st === 'paused') { btn.disabled = true; btn.value = '<%:已暂停 (点[继续])%>'; }
|
||||
else if (st === 'verifying') { btn.disabled = true; btn.value = '<%:校验/替换中...%>'; }
|
||||
else if (st === 'stopped' || st === 'lost') { btn.disabled = false; btn.value = '<%:继续下载 (自动续传)%>'; }
|
||||
else if (st === 'error') { btn.disabled = false; btn.value = '<%:重试 (自动续传)%>'; }
|
||||
else { btn.disabled = false; btn.value = '<%:下载并重启%>'; }
|
||||
}
|
||||
// 暂停/继续/停止 控制按钮的显示与可用性
|
||||
if (bP && bR && bS) {
|
||||
var showCtl = (st === 'downloading' || st === 'paused');
|
||||
bP.style.display = showCtl ? '' : 'none'; bP.disabled = (st !== 'downloading');
|
||||
bR.style.display = showCtl ? '' : 'none'; bR.disabled = (st !== 'paused');
|
||||
bS.style.display = showCtl ? '' : 'none'; bS.disabled = false;
|
||||
}
|
||||
if (stat) {
|
||||
var stmap = { idle:'', downloading:'<%:下载中...%>', paused:'<%:已暂停%>', verifying:'<%:校验中...%>', done:'<%:完成%>', stopped:'<%:已停止%>', lost:'<%:进程丢失%>', error:'<%:出错%>' };
|
||||
stat.innerHTML = (stmap[st] || st) + ' — ' + text;
|
||||
}
|
||||
if (msg) {
|
||||
if (st === 'done') msg.innerHTML = "<font color='#008000'>" + cdl_esc(rv.msg) + "</font>";
|
||||
else if (st === 'error' || st === 'lost') msg.innerHTML = "<font color='#DB7093'>" + cdl_esc(rv.msg || '') + "</font>";
|
||||
else if (st === 'stopped' || st === 'paused') msg.innerHTML = "<font color='#aaaaaa'>" + cdl_esc(rv.msg || '') + "</font>";
|
||||
else msg.innerHTML = "";
|
||||
}
|
||||
}
|
||||
function cdl_poll()
|
||||
{
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin","network","caddy","download_status")%>', null, function(x, rv){ cdl_render(rv); });
|
||||
}
|
||||
function cdl_ctl(act)
|
||||
{
|
||||
XHR.post('<%=luci.dispatcher.build_url("admin","network","caddy","download_ctl")%>', { act: act }, function(){ cdl_poll(); });
|
||||
return false;
|
||||
}
|
||||
function caddy_dl_start(btn)
|
||||
{
|
||||
btn.disabled = true; btn.value = '<%:启动中...%>';
|
||||
cdl_until = Date.now() + 60000; // 60s 宽限: 覆盖 控制器删旧 status → 新脚本 TOT 探测 → 第一个进度 的空窗
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin","network","caddy","download")%>', null, function(x, rv){
|
||||
cdl_poll();
|
||||
});
|
||||
return false;
|
||||
}
|
||||
// 页面加载时若上次在后台下载, 恢复进度显示
|
||||
window.addEventListener('load', function(){ cdl_poll(); });
|
||||
//]]></script>
|
||||
<%+cbi/valuefooter%>
|
||||
@@ -1,49 +1,59 @@
|
||||
# 此配置文件路径:/etc/caddy/Caddyfile
|
||||
|
||||
{ # 全局配置
|
||||
# 启动 cgi 模块
|
||||
order cgi before respond
|
||||
# 启动 webdav 模块
|
||||
order webdav before file_server
|
||||
# 关闭 API 端口
|
||||
admin off
|
||||
}
|
||||
# 此配置文件路径: /etc/caddy/Caddyfile
|
||||
# 下面是 proxy 模式生成 Caddyfile 的样例 (通用值).
|
||||
# 手动模式 (Caddyfile) 下, 本文件内容即你实际使用的配置, 可任意写 Caddy 支持的语法.
|
||||
|
||||
:12311 {
|
||||
#启用压缩
|
||||
encode gzip
|
||||
#认证,账号admin 密码123456
|
||||
basicauth {
|
||||
admin $2a$14$RdbOHzJhf5BaapSdlYTCbe.yWY9cEZjyDpfgwStY28K/qsM1tX8tu
|
||||
}
|
||||
#路径 /mnt
|
||||
root * /mnt
|
||||
#文件服务器 端口12311
|
||||
file_server browse
|
||||
|
||||
log {
|
||||
#日志 路径/tmp/caddy/requests.log
|
||||
output file /tmp/caddy/requests.log {
|
||||
roll_size 1MiB
|
||||
roll_local_time
|
||||
roll_keep 5
|
||||
roll_keep_for 120h
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto_https disable_redirects
|
||||
|
||||
aliyun_ddns {
|
||||
access_key_id <ACCESS_KEY_ID>
|
||||
access_key_secret <ACCESS_KEY_SECRET>
|
||||
region_id cn-hangzhou
|
||||
|
||||
domains {
|
||||
example.com @
|
||||
}
|
||||
|
||||
versions ipv4
|
||||
ip_source interface pppoe-wan
|
||||
check_interval 5m
|
||||
ttl 60s
|
||||
}
|
||||
}
|
||||
|
||||
:12322 {
|
||||
#启用压缩
|
||||
encode gzip
|
||||
#认证,账号admin 密码123456
|
||||
basicauth {
|
||||
admin $2a$14$RdbOHzJhf5BaapSdlYTCbe.yWY9cEZjyDpfgwStY28K/qsM1tX8tu
|
||||
}
|
||||
example.com:8443 {
|
||||
tls {
|
||||
dns alidns {
|
||||
access_key_id <ACCESS_KEY_ID>
|
||||
access_key_secret <ACCESS_KEY_SECRET>
|
||||
}
|
||||
}
|
||||
|
||||
# webdav 路径根目录 端口12322
|
||||
webdav /dav/* {
|
||||
prefix /dav
|
||||
root /
|
||||
}
|
||||
header -Server
|
||||
|
||||
request_body max_size 0
|
||||
|
||||
@health path /health
|
||||
@apikey header Authorization "Bearer <API_KEY>"
|
||||
|
||||
handle @health {
|
||||
reverse_proxy 192.168.1.x:8080 { flush_interval -1 }
|
||||
}
|
||||
|
||||
handle @apikey {
|
||||
reverse_proxy 192.168.1.x:8080 { flush_interval -1 }
|
||||
}
|
||||
|
||||
handle {
|
||||
respond 404 "Not Found" { close }
|
||||
}
|
||||
|
||||
log {
|
||||
output file /tmp/caddy/requests.log {
|
||||
roll_size 1MiB
|
||||
roll_local_time
|
||||
roll_keep 5
|
||||
roll_keep_for 120h
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,28 @@
|
||||
# luci-app-caddy 默认配置
|
||||
#
|
||||
# 数据模型:
|
||||
# config caddy -> 全局 (单条): 启用 / 路径 / 重定向开关
|
||||
# config domain -> 域名 (多条, 增删): 每条 = 一个域名 + AK + DDNS 参数
|
||||
# config proxy -> Host (多条, 增删): 每条 = 一段 Caddyfile 的 域名:端口 { ... }
|
||||
#
|
||||
# domain 示例 (在 LuCI [Domain] 页点 添加 创建):
|
||||
# config domain 'd1'
|
||||
# option domain_name 'example.com'
|
||||
# option key_id '<AK_ID>'
|
||||
# option key_secret '<AK_SECRET>'
|
||||
# option ddns_on '0'
|
||||
#
|
||||
# proxy 示例 (在 LuCI [Host] 页点 添加 创建):
|
||||
# config proxy 'host1'
|
||||
# option name 'my-host'
|
||||
# option enabled '1'
|
||||
# option domain 'example.com'
|
||||
# option port '8443'
|
||||
# option tls '1'
|
||||
# option target '192.168.1.x:8080'
|
||||
|
||||
config caddy
|
||||
option 'enabled' '0'
|
||||
option cmd '默认'
|
||||
option 'port' '12311'
|
||||
option 'webdav' '0'
|
||||
option 'filezip' '0'
|
||||
option 'webzip' '0'
|
||||
option 'cgi' '0'
|
||||
option 'api' '0'
|
||||
option 'data_dir' '/mnt'
|
||||
option 'file_pass' '0'
|
||||
option 'log' '1'
|
||||
option 'log_dir' '/tmp/caddy/requests.log'
|
||||
option allow_wan '0'
|
||||
option enabled '0'
|
||||
option disable_redirect '1'
|
||||
option bin_dir '/usr/sbin/caddy'
|
||||
option log_dir '/tmp/caddy/requests.log'
|
||||
@@ -2,178 +2,315 @@
|
||||
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
PROG="$(uci -q get caddy.@caddy[0].bin_dir)"
|
||||
|
||||
get_config() {
|
||||
config_get_bool enabled $1 enabled 0
|
||||
config_get cmd $1 cmd "默认"
|
||||
config_get_bool webdav $1 webdav 0
|
||||
config_get port $1 port 12311
|
||||
config_get webdav_port $1 webdav_port 12322
|
||||
config_get log $1 log 1
|
||||
config_get data_dir $1 data_dir "/mnt"
|
||||
config_get webdav_data_dir $1 webdav_data_dir "/mnt"
|
||||
config_get log_dir $1 log_dir "/tmp/caddy/requests.log"
|
||||
config_get caddy_file $1 caddy_file "/etc/caddy/Caddyfile"
|
||||
config_get caddyfile $1 caddyfile
|
||||
config_get file_pass $1 file_pass 0
|
||||
config_get file_username $1 file_username "admin"
|
||||
config_get file_password $1 file_password "123456"
|
||||
config_get webdav_pass $1 webdav_pass 0
|
||||
config_get webdav_username $1 webdav_username "admin"
|
||||
config_get webdav_password $1 webdav_password "123456"
|
||||
config_get api $1 api 0
|
||||
config_get filezip $1 filezip 0
|
||||
config_get webzip $1 webzip 0
|
||||
config_get allow_wan $1 allow_wan 0
|
||||
# ---- 全局值 (来自 @caddy[0]) ----
|
||||
G_BIN="$(uci -q get caddy.@caddy[0].bin_dir)"
|
||||
[ -z "$G_BIN" ] && G_BIN="/usr/sbin/caddy"
|
||||
G_FILE="$(uci -q get caddy.@caddy[0].caddy_file)"
|
||||
[ -z "$G_FILE" ] && G_FILE="/etc/caddy/Caddyfile"
|
||||
G_LOG="$(uci -q get caddy.@caddy[0].log_dir)"
|
||||
[ -z "$G_LOG" ] && G_LOG="/tmp/caddy/requests.log"
|
||||
|
||||
TMP="$G_FILE.new"
|
||||
|
||||
# 列出所有 domain section 的 UCI 路径 (支持命名+匿名)
|
||||
# 返回完整路径如 caddy.d1 或 caddy.@domain[0]
|
||||
list_all_domains() {
|
||||
uci show caddy 2>/dev/null | grep '\.domain_name' | sed 's/\.domain_name=.*//'
|
||||
}
|
||||
|
||||
set_firewall() {
|
||||
if [ "$external_access" = "allow" ]; then
|
||||
uci -q delete firewall.caddy
|
||||
uci set firewall.caddy=rule
|
||||
uci set firewall.caddy.name="caddy"
|
||||
uci set firewall.caddy.target="ACCEPT"
|
||||
uci set firewall.caddy.src="wan"
|
||||
uci set firewall.caddy.proto="tcp"
|
||||
uci set firewall.caddy.dest_port="$port"
|
||||
[ ! -z "$webdav_port" ] && uci set firewall.caddy.dest_port="$webdav_port"
|
||||
uci set firewall.caddy.enabled="1"
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload >/dev/null 2>&1
|
||||
elif [ "$external_access" = "deny" ]; then
|
||||
uci -q delete firewall.caddy
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload >/dev/null 2>&1
|
||||
# 按 domain_name 查找匹配的 section UCI 路径
|
||||
find_domain_by_name() {
|
||||
local target="$1" ref dn
|
||||
for ref in $(list_all_domains); do
|
||||
dn="$(uci -q get "$ref".domain_name)"
|
||||
[ "$dn" = "$target" ] && echo "$ref" && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# 列出所有 proxy section (匿名+命名都认, 按 target 字段识别)
|
||||
# 匿名 section 在 uci show 里形如 caddy.@proxy[N]
|
||||
list_all_proxies() {
|
||||
uci show caddy 2>/dev/null | grep '\.target=' | sed 's/\.target=.*//'
|
||||
}
|
||||
|
||||
# ---- 生成 Caddyfile ----
|
||||
# 逻辑: 全局块 (auto_https + 每个 ddns_on 的 domain 生成 aliyun_ddns) + 每个启用的 host
|
||||
generate_caddyfile() {
|
||||
local cf="$TMP"
|
||||
local disr
|
||||
local p dom prt tgt key tls_cert extra
|
||||
|
||||
disr="$(uci -q get caddy.@caddy[0].disable_redirect)"
|
||||
|
||||
{
|
||||
echo "# /etc/caddy/Caddyfile"
|
||||
echo "# generated by luci-app-caddy, edit via LuCI -> Network -> Caddy"
|
||||
echo
|
||||
echo "{"
|
||||
|
||||
# auto_https: 勾选才写
|
||||
if [ "$disr" = "1" ]; then
|
||||
echo " auto_https disable_redirects"
|
||||
fi
|
||||
|
||||
# CA 固定 Let's Encrypt, 避开 Caddy 默认可能跳 ZeroSSL
|
||||
echo " acme_ca https://acme-v02.api.letsencrypt.org/directory"
|
||||
|
||||
# --- DDNS: 遍历 domain, ddns_on=1 的生成 aliyun_ddns 块 ---
|
||||
for d in $(list_all_domains); do
|
||||
local don dkid dksec dname drec diface dint dttl
|
||||
don="$(uci -q get "$d".ddns_on)"
|
||||
[ "$don" != "1" ] && continue
|
||||
dname="$(uci -q get "$d".domain_name)"
|
||||
dkid="$(uci -q get "$d".key_id)"
|
||||
dksec="$(uci -q get "$d".key_secret)"
|
||||
drec="$(uci -q get "$d".ddns_records)"
|
||||
diface="$(uci -q get "$d".ddns_wan_iface)"
|
||||
dint="$(uci -q get "$d".ddns_check_interval)"
|
||||
dttl="$(uci -q get "$d".ddns_ttl)"
|
||||
[ -z "$dname" ] && continue
|
||||
[ -z "$dkid" ] && continue
|
||||
[ -z "$dksec" ] && continue
|
||||
[ -z "$dint" ] && dint="5m"
|
||||
[ -z "$dttl" ] && dttl="60s"
|
||||
|
||||
echo
|
||||
echo " # DDNS: $dname (@ = 主域 A 记录, * = 泛解析 *.domain 独立记录, 二者互不串用)"
|
||||
echo " aliyun_ddns {"
|
||||
echo " access_key_id $dkid"
|
||||
echo " access_key_secret $dksec"
|
||||
echo
|
||||
echo " domains {"
|
||||
echo " $dname @"
|
||||
printf '%s\n' "$drec" | while IFS= read -r r; do
|
||||
[ -z "$r" ] && continue
|
||||
echo " $dname $r"
|
||||
done
|
||||
echo " }"
|
||||
echo
|
||||
echo " versions ipv4"
|
||||
case "$diface" in
|
||||
public|"")
|
||||
: # 走插件默认 simple_http (api.ipify.org / icanhazip.com / ifconfig.me / ident.me)
|
||||
;;
|
||||
*)
|
||||
echo " ip_source interface $diface"
|
||||
;;
|
||||
esac
|
||||
echo " check_interval $dint"
|
||||
echo " ttl $dttl"
|
||||
echo " }"
|
||||
done
|
||||
|
||||
echo "}"
|
||||
|
||||
# ---- Host: 每个启用的 proxy (匿名/命名都认) ----
|
||||
for p in $(list_all_proxies); do
|
||||
local pen
|
||||
pen="$(uci -q get "$p".enabled)"
|
||||
[ "$pen" = "0" ] && continue
|
||||
|
||||
# 一次性迁移: 下拉现在只有 auto/off, 任何旧值 (tls Flag / 静态证书路径) 归到这两个
|
||||
local tlsval
|
||||
tlsval="$(uci -q get "$p".tls_cert)"
|
||||
case "$tlsval" in
|
||||
auto|off|"")
|
||||
if [ -z "$tlsval" ]; then
|
||||
# 从未设过: 按旧版 tls Flag 定 (1=auto, 其余 off)
|
||||
if [ "$(uci -q get "$p".tls)" = "1" ]; then
|
||||
tlsval=auto
|
||||
else
|
||||
tlsval=off
|
||||
fi
|
||||
uci -q set "$p".tls_cert="$tlsval"
|
||||
uci commit caddy
|
||||
fi
|
||||
;;
|
||||
*) # 旧值 (静态证书路径等) 一律归 auto, 写回 UCI 一次性归一
|
||||
uci -q set "$p".tls_cert=auto
|
||||
uci commit caddy
|
||||
tlsval=auto
|
||||
;;
|
||||
esac
|
||||
|
||||
dom="$(uci -q get "$p".domain)"
|
||||
prt="$(uci -q get "$p".port)"
|
||||
tgt="$(uci -q get "$p".target)"
|
||||
key="$(uci -q get "$p".api_key)"
|
||||
extra="$(uci -q get "$p".extra)"
|
||||
[ -z "$prt" ] && prt="443"
|
||||
[ -z "$dom" ] && continue
|
||||
[ -z "$tgt" ] && continue
|
||||
|
||||
# 取对应 Domain 的 AK (Host 域名剥掉 *. 前缀再查 Domain 页)
|
||||
# 注意: 必须用 \*.*) 转义前导星号, 否则 example.net 这种含点的裸域会误中 *.*
|
||||
local dmatch dkid dksec dlookup
|
||||
dlookup="$dom"
|
||||
case "$dlookup" in
|
||||
\*.*) dlookup="${dlookup#\*.}" ;;
|
||||
esac
|
||||
dmatch="$(find_domain_by_name "$dlookup")"
|
||||
dkid=""
|
||||
dksec=""
|
||||
if [ -n "$dmatch" ]; then
|
||||
dkid="$(uci -q get "$dmatch".key_id)"
|
||||
dksec="$(uci -q get "$dmatch".key_secret)"
|
||||
fi
|
||||
|
||||
# 监听地址: 完全由表单的 domain+port 决定, 代码不替用户加地址
|
||||
# 选了 *.x 时, 同一行带上裸域 x (官方"一行多名字"模式):
|
||||
# certmagic 逐名字签发 → 裸域、通配符各一张证书(各一条 order)
|
||||
# ≡ acme.sh -d x --dns + -d '*.x' --dns, 效果等价: 裸域+全部子域都覆盖
|
||||
local addr="$dom:$prt"
|
||||
case "$dom" in
|
||||
\*.*) addr="$dom:$prt ${dom#\*.}:$prt" ;;
|
||||
esac
|
||||
|
||||
echo
|
||||
echo "$addr {"
|
||||
|
||||
# TLS: 只有 auto / off 两个值 (下拉已砍掉静态证书选项)
|
||||
# auto + Domain 页有 AK: 一律 DNS-01 (裸域/通配符都不依赖 80/443 入站, 适合 NAT 家庭宽带)
|
||||
# auto + 无 AK: 回退官方 HTTP-01 (一行 tls 都不写, Caddy 自动处理)
|
||||
# 用上面迁移后的 $tlsval, 不重读 UCI (迁移刚写回时本轮即生效)
|
||||
case "$tlsval" in
|
||||
auto|"")
|
||||
if [ -n "$dkid" ] && [ -n "$dksec" ]; then
|
||||
echo " tls {"
|
||||
echo " dns alidns {"
|
||||
echo " access_key_id $dkid"
|
||||
echo " access_key_secret $dksec"
|
||||
echo " }"
|
||||
echo " }"
|
||||
echo
|
||||
fi
|
||||
;;
|
||||
off)
|
||||
: # 纯 HTTP, 不写 tls 块
|
||||
;;
|
||||
esac
|
||||
|
||||
echo " header -Server"
|
||||
echo
|
||||
echo " request_body max_size 0"
|
||||
|
||||
if [ -n "$key" ]; then
|
||||
echo
|
||||
echo " # Caddy 层 API key 校验: 除 @apikey 外一律 404 (/health 也需带 key)"
|
||||
echo " @apikey header Authorization \"Bearer $key\""
|
||||
echo
|
||||
echo " handle @apikey {"
|
||||
echo " reverse_proxy $tgt { flush_interval -1 }"
|
||||
echo " }"
|
||||
echo
|
||||
echo " handle {"
|
||||
# 无 key 的兜底: 空 body 的 404 (不回 "Not Found" 文本, 不泄露身份)
|
||||
# 注意: respond 带 body 文本 + 尾部 { close } 子块时 Caddyfile 参数错位
|
||||
# (404→body, 文本→status), 运行时 Atoi 炸 500. 故 body 留空.
|
||||
echo " respond 404 { close }"
|
||||
echo " }"
|
||||
else
|
||||
echo
|
||||
echo " reverse_proxy $tgt {"
|
||||
echo " flush_interval -1"
|
||||
echo " }"
|
||||
fi
|
||||
|
||||
# 自定义指令 (原样插入)
|
||||
if [ -n "$extra" ]; then
|
||||
echo
|
||||
printf '%s\n' "$extra" | while IFS= read -r line; do
|
||||
echo " $line"
|
||||
done
|
||||
fi
|
||||
|
||||
# 请求日志: 勾选 log_on 才生成, 关 = 不写日志, 省资源
|
||||
local lon
|
||||
lon="$(uci -q get caddy.@caddy[0].log_on)"
|
||||
if [ "$lon" != "0" ]; then
|
||||
echo
|
||||
echo " # 请求日志"
|
||||
echo " log {"
|
||||
echo " output file $G_LOG {"
|
||||
echo " roll_size 1MiB"
|
||||
echo " roll_local_time"
|
||||
echo " roll_keep 5"
|
||||
echo " roll_keep_for 120h"
|
||||
echo " }"
|
||||
echo " }"
|
||||
fi
|
||||
|
||||
echo "}"
|
||||
done
|
||||
} > "$cf"
|
||||
mv -f "$cf" "$G_FILE"
|
||||
# DDNS/AK secret 明文在文件里, 收紧权限只让 root 读写 (LuCI CGI 同以 root 运行, 不受影响)
|
||||
chmod 600 "$G_FILE"
|
||||
|
||||
# 一次性迁移: wildcard 旧字段. 通配符选择已挪到 Host 页下拉, Domain 页不再显示该字段,
|
||||
# 而 CBI 保存只写页面已定义字段、不删未定义键 → 旧 wildcard 键会永久残留 UCI. 有就删.
|
||||
local wcwipe
|
||||
wcwipe=0
|
||||
for d in $(list_all_domains); do
|
||||
case "$(uci -q get "$d".wildcard)" in
|
||||
""|auto|off) : ;;
|
||||
*) uci -q delete "$d".wildcard; wcwipe=1 ;;
|
||||
esac
|
||||
done
|
||||
[ "$wcwipe" = "1" ] && uci commit caddy
|
||||
|
||||
# 清 UCI 冻结值: [配置] 页 TextValue 点过「保存」后会把当时内容冻进 UCI,
|
||||
# 之后页面优先显示 UCI 而不再读磁盘 (表现: 这里改了但配置页看到的还是旧的).
|
||||
# 每次重新生成后把键删掉, 页面即回落 o.default(读磁盘真值).
|
||||
# 只在键存在时才 commit (commit 会动 /etc/config/caddy 触发 procd reload,
|
||||
# 避免无谓的额外重载: 键存在→删→commit→本轮 reload 重生成时键已不在, 不会死循环)
|
||||
if uci -q get caddy.@caddy[0].caddyfile >/dev/null 2>&1; then
|
||||
uci -q delete caddy.@caddy[0].caddyfile
|
||||
uci commit caddy
|
||||
fi
|
||||
}
|
||||
|
||||
check_prog() {
|
||||
if [ ! -f "$G_BIN" ]; then
|
||||
uci -q set caddy.@caddy[0].enabled=0
|
||||
uci commit caddy
|
||||
echo "caddy: $G_BIN 未找到" >>"$G_LOG" 2>&1
|
||||
exit 1
|
||||
else
|
||||
chmod +x "$G_BIN"
|
||||
if [ "$($G_BIN -h 2>&1 | wc -l)" -lt 3 ]; then
|
||||
uci -q set caddy.@caddy[0].enabled=0
|
||||
uci commit caddy
|
||||
echo "caddy: 程序不完整或架构不匹配" >>"$G_LOG" 2>&1
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load caddy
|
||||
config_foreach get_config caddy
|
||||
[ $enabled -ne 1 ] && return 1
|
||||
log_path="$(dirname $log_dir)"
|
||||
mkdir -p $log_path
|
||||
if [ ! -f "$PROG" ] ;then
|
||||
uci -q set caddy.@caddy[0].enabled=0
|
||||
uci commit caddy
|
||||
echo "$(TZ=UTC-8 date -R +%Y年%m月%d日\ %X) : $PROG 未找到,无法启动!请确认已上传程序" >>$log_dir 2>&1
|
||||
exit 1
|
||||
else
|
||||
chmod +x $PROG
|
||||
if [ $(($($PROG -h | wc -l))) -lt 3 ] ;then
|
||||
uci -q set caddy.@caddy[0].enabled=0
|
||||
uci commit caddy
|
||||
echo "$(TZ=UTC-8 date -R +%Y年%m月%d日\ %X) : $PROG 程序不完整或CPU架构不支持的程序,无法启动!" >>$log_dir 2>&1
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
if [ "$cmd" = "默认" ] ;then
|
||||
mkdir -p $data_dir
|
||||
if [ "$log" -eq 1 ] ;then
|
||||
|
||||
logs="log {
|
||||
output file $log_dir {
|
||||
roll_size 1MiB
|
||||
roll_local_time
|
||||
roll_keep 5
|
||||
roll_keep_for 120h
|
||||
}
|
||||
}"
|
||||
fi
|
||||
if [ "$file_pass" -eq 1 ] ;then
|
||||
filepassword="$($PROG hash-password --plaintext $file_password)"
|
||||
filebasicauth="basicauth {
|
||||
$file_username $filepassword
|
||||
}"
|
||||
fi
|
||||
if [ "$webdav" -eq 1 ] ;then
|
||||
mkdir -p $webdav_data_dir
|
||||
if [ "$webdav_pass" -eq 1 ] ;then
|
||||
password="$($PROG hash-password --plaintext $webdav_password)"
|
||||
basicauth="basicauth {
|
||||
$webdav_username $password
|
||||
}"
|
||||
fi
|
||||
webdavs=":$webdav_port {
|
||||
$basicauth
|
||||
webdav * {
|
||||
prefix /dav
|
||||
root $webdav_data_dir
|
||||
}
|
||||
}"
|
||||
orderweb="order webdav before file_server # 启动 webdav 模块"
|
||||
fi
|
||||
if [ "$filezip" -eq 1 ] ;then
|
||||
filezips="encode gzip"
|
||||
fi
|
||||
if [ "$webzip" -eq 1 ] ;then
|
||||
webzips="encode gzip"
|
||||
fi
|
||||
if [ "$api" != "1" ] ;then
|
||||
apis="admin off # 关闭 API 端口"
|
||||
fi
|
||||
if [ "$allow_wan" -eq "1" ]; then
|
||||
external_access="allow"
|
||||
else
|
||||
external_access="deny"
|
||||
fi
|
||||
if [ "$webdav" -eq 1 ] || [ "$api" != "1" ] ;then
|
||||
order="{ # 全局配置
|
||||
$orderweb
|
||||
$apis
|
||||
} "
|
||||
fi
|
||||
set_firewall
|
||||
|
||||
cat > $caddy_file <<EOF
|
||||
# 此配置文件路径:/etc/caddy/Caddyfile
|
||||
local enabled
|
||||
enabled="$(uci -q get caddy.@caddy[0].enabled)"
|
||||
[ -z "$enabled" ] && enabled=0
|
||||
[ "$enabled" != "1" ] && return 1
|
||||
|
||||
$order
|
||||
mkdir -p "$(dirname "$G_LOG")"
|
||||
check_prog
|
||||
|
||||
:$port {
|
||||
$filezips
|
||||
$filebasicauth
|
||||
root * $data_dir
|
||||
file_server browse
|
||||
|
||||
$logs
|
||||
|
||||
}
|
||||
generate_caddyfile
|
||||
|
||||
$webdavs
|
||||
EOF
|
||||
"$G_BIN" fmt --overwrite "$G_FILE" 2>/dev/null || true
|
||||
|
||||
uci -q set caddy.@caddy[0].caddyfile="$(cat $caddy_file)"
|
||||
uci commit caddy
|
||||
else
|
||||
caddyf="$(uci -q get caddy.@caddy[0].caddyfile)"
|
||||
logpath="$(echo $caddyf | grep "output file" | awk -F 'output file' '{print $2}' | tr -d '{ ')"
|
||||
[ ! -z "$logpath" ] && uci -q set caddy.@caddy[0].log_dir=$logpath && uci commit caddy
|
||||
if [ ! -z "$caddyf" ] ;then
|
||||
echo "$caddyf" >$caddy_file 2>&1
|
||||
else
|
||||
uci -q set caddy.@caddy[0].enabled=0
|
||||
uci commit caddy
|
||||
echo "$(TZ=UTC-8 date -R +%Y年%m月%d日\ %X) : $caddy_file 配置文件Caddyfile为空,无法启动!" >>$log_dir 2>&1
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
$PROG fmt --overwrite $caddy_file
|
||||
procd_open_instance caddy
|
||||
procd_set_param command $PROG
|
||||
procd_append_param command run
|
||||
procd_append_param command --config $caddy_file
|
||||
procd_append_param command --adapter caddyfile
|
||||
|
||||
procd_set_param stdout 0
|
||||
procd_set_param stderr 0
|
||||
procd_set_param command "$G_BIN" "run" "--config" "$G_FILE" "--adapter" caddyfile
|
||||
procd_set_param pidfile /var/run/caddy.pid
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
procd_close_instance caddy
|
||||
|
||||
echo `date +%s` > /tmp/caddy_time
|
||||
date +%s > /tmp/caddy_time
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
@@ -181,14 +318,26 @@ service_triggers() {
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
external_access="deny"
|
||||
set_firewall
|
||||
rm -rf /tmp/caddy.tag /tmp/caddynew.tag
|
||||
rm -rf $(uci -q get caddy.@caddy[0].log_dir)
|
||||
procd_kill caddy 2>/dev/null
|
||||
rm -f /tmp/caddynew.tag
|
||||
rm -f "$G_LOG" 2>/dev/null
|
||||
rm -f /var/run/caddy.pid
|
||||
return 0
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
# 覆盖 rc.common 的 restart(): 官方零停机热重载 (caddy reload, 等价 POST /load)
|
||||
# 有运行实例 -> 热重载, 旧连接不断; 无实例 -> 完整 stop/start
|
||||
restart() {
|
||||
generate_caddyfile
|
||||
"$G_BIN" fmt --overwrite "$G_FILE" 2>/dev/null || true
|
||||
if pidof caddy >/dev/null 2>&1; then
|
||||
"$G_BIN" reload --config "$G_FILE" --adapter caddyfile >/dev/null 2>&1 && return 0
|
||||
fi
|
||||
stop
|
||||
sleep 1
|
||||
start
|
||||
}
|
||||
|
||||
# reload: procd 模式 rc.common 调 reload_service, 委托给热重载
|
||||
reload_service() {
|
||||
restart
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
--- a/net/netfilter/Kconfig 2026-09-15 14:22:18.467281704 +0800
|
||||
+++ b/net/netfilter/Kconfig 2026-09-11 12:51:21.318346650 +0800
|
||||
@@ -443,6 +443,20 @@
|
||||
config NETFILTER_SYNPROXY
|
||||
tristate
|
||||
|
||||
+config NF_CONNTRACK_CUSTOM_TELEMETRY
|
||||
+ bool "Lightweight Layer 7 Payload Telemetry via conntrack"
|
||||
+ depends on NF_CONNTRACK
|
||||
+ default y
|
||||
+ help
|
||||
+ This option intercepts the first payload-carrying packet of any
|
||||
+ new connection (TCP/UDP) and stores its initial 8 bytes. This
|
||||
+ data is then appended to the /proc/net/nf_conntrack stream.
|
||||
+
|
||||
+ Perfect for lightweight gaming/video traffic identification
|
||||
+ without running heavy user-space DPI tools.
|
||||
+
|
||||
+ If unsure, say Y.
|
||||
+
|
||||
endif # NF_CONNTRACK
|
||||
|
||||
config NF_TABLES
|
||||
--- a/include/net/netfilter/nf_conntrack.h 2024-01-26 06:34:33.000000000 +0800
|
||||
+++ b/include/net/netfilter/nf_conntrack.h 2026-09-14 11:54:57.287192651 +0800
|
||||
@@ -103,6 +103,15 @@
|
||||
/* Extensions */
|
||||
struct nf_ct_ext *ext;
|
||||
|
||||
+#if IS_ENABLED(CONFIG_NF_CONNTRACK_CUSTOM_TELEMETRY)
|
||||
+ /*==================================================================
|
||||
+ * CUSTOM FIELDS FOR LIGHTWEIGHT L7 TELEMETRY (STRUCTURE SAFE)
|
||||
+ * Placed safely before the protocol union to avoid alignment bugs.
|
||||
+ *==================================================================*/
|
||||
+ u8 init_payload[64]; /* Stores the 64 bytes of L7 payload that maybe plain text rather than hex string */
|
||||
+ u8 has_custom_payload; /* Boolean flag: 1 if raw hex string was captured, 2 if plain text was captured */
|
||||
+#endif
|
||||
+
|
||||
/* Storage reserved for other modules, must be the last member */
|
||||
union nf_conntrack_proto proto;
|
||||
};
|
||||
--- a/net/netfilter/nf_conntrack_core.c 2026-09-15 14:22:18.467281704 +0800
|
||||
+++ b/net/netfilter/nf_conntrack_core.c 2026-09-15 14:19:35.912229303 +0800
|
||||
@@ -1710,6 +1710,156 @@
|
||||
goto out;
|
||||
}
|
||||
|
||||
+#if IS_ENABLED(CONFIG_NF_CONNTRACK_CUSTOM_TELEMETRY)
|
||||
+ /* =================== CUSTOM L7 TELEMETRY START =================== */
|
||||
+ /* 1. Only process successfully accepted packets if telemetry capture has not locked yet */
|
||||
+ if (ret == NF_ACCEPT && ct != NULL && !ct->has_custom_payload) {
|
||||
+ struct iphdr *iph = ip_hdr(skb);
|
||||
+ if (iph) {
|
||||
+ unsigned int l4_header_len = 0;
|
||||
+
|
||||
+ if (protonum == IPPROTO_TCP) {
|
||||
+ struct tcphdr *th = tcp_hdr(skb);
|
||||
+ if (th && th->doff >= 5 && th->doff <= 15) {
|
||||
+ l4_header_len = th->doff * 4;
|
||||
+ }
|
||||
+ } else if (protonum == IPPROTO_UDP) {
|
||||
+ l4_header_len = sizeof(struct udphdr);
|
||||
+ }
|
||||
+
|
||||
+ if (l4_header_len > 0) {
|
||||
+ unsigned int total_len = ntohs(iph->tot_len);
|
||||
+ unsigned int payload_offset = dataoff + l4_header_len;
|
||||
+
|
||||
+ if (total_len > payload_offset && protonum == IPPROTO_UDP) {
|
||||
+ unsigned int payload_len = total_len - payload_offset;
|
||||
+ if (payload_len >= 8) {
|
||||
+ u8 udp_buf[8];
|
||||
+ unsigned int target_grab_len = (payload_len > 8) ? 8 : payload_len;
|
||||
+ u8 *udp_ptr = skb_header_pointer(skb, payload_offset, target_grab_len, udp_buf);
|
||||
+
|
||||
+ if (udp_ptr) {
|
||||
+ memset(ct->init_payload, 0, 64);
|
||||
+ memcpy(ct->init_payload, udp_ptr, target_grab_len);
|
||||
+ ct->has_custom_payload = 1;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ if (total_len > payload_offset && protonum == IPPROTO_TCP) {
|
||||
+ unsigned int payload_len = total_len - payload_offset;
|
||||
+
|
||||
+ /* PRE-FILTER BYPASS ANCHOR: Fast drop non-TLS or ultra-short corrupted flows */
|
||||
+ if (payload_len > 44) {
|
||||
+ u8 tls_pre_buf[2];
|
||||
+ u8 *tls_pre = skb_header_pointer(skb, payload_offset, 2, tls_pre_buf);
|
||||
+
|
||||
+ if (tls_pre && tls_pre[0] == 0x16 && tls_pre[1] == 0x03) {
|
||||
+ unsigned int offset = 5; /* Step 1: Skip TLS Record Layer Header safely */
|
||||
+ u8 len_buf[2];
|
||||
+ u8 *p;
|
||||
+
|
||||
+ /* Step 2: Extract Handshake Type (1 byte) and confirm it is Client Hello (0x01) */
|
||||
+ p = skb_header_pointer(skb, payload_offset + offset, 1, len_buf);
|
||||
+ if (p && p[0] == 0x01) {
|
||||
+ offset += 4; /* Move past Handshake Type(1B) + Handshake Length(3B) */
|
||||
+ offset += 2; /* Move past Version (2B, e.g., 0x03 0x03) */
|
||||
+ offset += 32; /* FLY-OVER: Instantly blast through 32 bytes of static Random data via simple addition */
|
||||
+
|
||||
+ /* Step 3: Extract dynamic Session ID length field safely (1 byte) */
|
||||
+ p = skb_header_pointer(skb, payload_offset + offset, 1, len_buf);
|
||||
+ if (p) {
|
||||
+ u8 session_id_len = p[0];
|
||||
+ offset += 1 + session_id_len; /* Skip length descriptor + payload data bytes */
|
||||
+
|
||||
+ /* Step 4: Extract dynamic Cipher Suites length field safely (2 bytes) */
|
||||
+ p = skb_header_pointer(skb, payload_offset + offset, 2, len_buf);
|
||||
+ if (p) {
|
||||
+ /* Use safe shift operations to enforce clean endian alignment on all router CPUs */
|
||||
+ u16 cipher_suites_len = ((u16)p[0] << 8) | p[1];
|
||||
+ offset += 2 + cipher_suites_len; /* Leap over Ciphers grid completely */
|
||||
+
|
||||
+ /* Step 5: Extract dynamic Compression Methods length field safely (1 byte) */
|
||||
+ p = skb_header_pointer(skb, payload_offset + offset, 1, len_buf);
|
||||
+ if (p) {
|
||||
+ u8 compression_len = p[0];
|
||||
+ offset += 1 + compression_len; /* Skip Compression descriptors */
|
||||
+
|
||||
+ /* Step 6: We have successfully reached the Extensions Array Block! Read Extensions Total Length (2B) */
|
||||
+ p = skb_header_pointer(skb, payload_offset + offset, 2, len_buf);
|
||||
+ if (p) {
|
||||
+ u16 extensions_total_len = ((u16)p[0] << 8) | p[1];
|
||||
+ unsigned int extensions_end = offset + 2 + extensions_total_len;
|
||||
+ offset += 2; /* Shift layout offset right inside the core extensions list sequence */
|
||||
+
|
||||
+ /* Strict boundary sanity guard against malicious malformed padding packets */
|
||||
+ if (extensions_end <= payload_len) {
|
||||
+ u8 loop_guard = 0;
|
||||
+ u8 ext_buf[4];
|
||||
+ u8 *ext_ptr;
|
||||
+
|
||||
+ /* Step 7: High-Performance O(1) Leap Loop with anti-hang protection */
|
||||
+ while (offset + 4 <= extensions_end && loop_guard++ < 32) {
|
||||
+ ext_ptr = skb_header_pointer(skb, payload_offset + offset, 4, ext_buf);
|
||||
+ if (!ext_ptr) break;
|
||||
+
|
||||
+ u16 ext_type = ((u16)ext_ptr[0] << 8) | ext_ptr[1];
|
||||
+ u16 ext_len = ((u16)ext_ptr[2] << 8) | ext_ptr[3];
|
||||
+
|
||||
+ /* TARGET MATCHED: Extension Type 0x0000 matches Server Name Indication (SNI) perfectly! */
|
||||
+ if (ext_type == 0x0000) {
|
||||
+ unsigned int sni_ptr_offset = offset + 4;
|
||||
+ u8 sni_buf[5];
|
||||
+ p = skb_header_pointer(skb, payload_offset + sni_ptr_offset, 5, sni_buf);
|
||||
+ if (p) {
|
||||
+ /* Skip SNI List Length(2B) + Name Type(1B, 0=host_name) + Server Name Length(2B) */
|
||||
+ u16 server_name_len = ((u16)p[3] << 8) | p[4];
|
||||
+ unsigned int hostname_offset = sni_ptr_offset + 5;
|
||||
+
|
||||
+ if (hostname_offset + server_name_len <= payload_len && server_name_len > 0) {
|
||||
+ u8 name_buf[64];
|
||||
+ u8 target_len = (server_name_len > 63) ? 63 : server_name_len;
|
||||
+ u8 *name_ptr = skb_header_pointer(skb, payload_offset + hostname_offset, target_len, name_buf);
|
||||
+
|
||||
+ if (name_ptr) {
|
||||
+ memset(ct->init_payload, 0, 64);
|
||||
+ memcpy(ct->init_payload, name_ptr, target_len);
|
||||
+ ct->has_custom_payload = 2; /* Status '2' -> SNI text successfully harvested and locked! */
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ break; /* Mission accomplished, kill the loop instantly */
|
||||
+ }
|
||||
+
|
||||
+ /* ULTRASONIC JUMP: Jump directly over heavy cryptography chunks (e.g. skip key_share 1263 bytes instantly) */
|
||||
+ offset += 4 + ext_len;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ /* Fallback Route C: Standard original 8-byte hex binary telemetry backup if no structural domain was found */
|
||||
+ if (!ct->has_custom_payload) {
|
||||
+ u8 fallback_buf[8];
|
||||
+ u8 *fb_ptr = skb_header_pointer(skb, payload_offset, 8, fallback_buf);
|
||||
+ if (fb_ptr) {
|
||||
+ memset(ct->init_payload, 0, 64);
|
||||
+ memcpy(ct->init_payload, fb_ptr, 8);
|
||||
+ ct->has_custom_payload = 1;
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+ /* ==================== CUSTOM L7 TELEMETRY END ==================== */
|
||||
+#endif
|
||||
+
|
||||
if (ctinfo == IP_CT_ESTABLISHED_REPLY &&
|
||||
!test_and_set_bit(IPS_SEEN_REPLY_BIT, &ct->status))
|
||||
nf_conntrack_event_cache(IPCT_REPLY, ct);
|
||||
--- a/net/netfilter/nf_conntrack_standalone.c 2026-09-15 14:22:17.815253367 +0800
|
||||
+++ b/net/netfilter/nf_conntrack_standalone.c 2026-09-12 16:27:05.964393178 +0800
|
||||
@@ -369,6 +369,25 @@
|
||||
ct_show_zone(s, ct, NF_CT_DEFAULT_ZONE_DIR);
|
||||
ct_show_delta_time(s, ct);
|
||||
|
||||
+#if IS_ENABLED(CONFIG_NF_CONNTRACK_CUSTOM_TELEMETRY)
|
||||
+ /* =================== CUSTOM L7 OUTPUT START =================== */
|
||||
+ if (ct->has_custom_payload) {
|
||||
+ /* If status flag is '2', we successfully isolated an authentic plain-text ASCII string fragment! */
|
||||
+ if (ct->has_custom_payload == 2) {
|
||||
+ /* Enforce strict termination rendering using standard %s formatting safely bounded by your size allocation */
|
||||
+ seq_printf(s, "payload=%s ", (char *)ct->init_payload);
|
||||
+ } else {
|
||||
+ /* Fallback status '1': Print classic 8-byte hexadecimal string sequence safely to avoid data corruption */
|
||||
+ seq_printf(s, "payload=%02x%02x%02x%02x%02x%02x%02x%02x ",
|
||||
+ ct->init_payload[0], ct->init_payload[1],
|
||||
+ ct->init_payload[2], ct->init_payload[3],
|
||||
+ ct->init_payload[4], ct->init_payload[5],
|
||||
+ ct->init_payload[6], ct->init_payload[7]);
|
||||
+ }
|
||||
+ }
|
||||
+ /* ==================== CUSTOM L7 OUTPUT END ==================== */
|
||||
+#endif
|
||||
+
|
||||
seq_printf(s, "use=%u\n", atomic_read(&ct->ct_general.use));
|
||||
|
||||
if (seq_has_overflowed(s))
|
||||
@@ -0,0 +1,18 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=Realtime Connection Tracking Stream Monitor
|
||||
LUCI_DEPENDS:=+luci-lib-jsonc
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
LUCI_MINIFY:=0
|
||||
|
||||
PKG_NAME:=luci-app-conntrack
|
||||
PKG_VERSION:=1.0.4
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_MAINTAINER:=simonchen <xinyi78528@163.com>
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<img width="750" height="400" alt="image" src="https://github.com/user-attachments/assets/fdd8b243-608f-4522-abcc-40ce645c249f" />
|
||||
<img width="720" height="600" alt="image" src="https://github.com/user-attachments/assets/f1b1d812-2a47-48a2-a9af-5d16d461327a" />
|
||||
|
||||
# luci-app-conntrack
|
||||
|
||||
[English](#english) | [中文说明](#中文说明)
|
||||
|
||||
---
|
||||
|
||||
## English
|
||||
|
||||
A highly-optimized, lightweight **Realtime Connection Tracking & Network Audit Monitor** for OpenWrt/LEDE. This plugin provides single-direction (TX/RX split), high-frequency, millisecond-level traffic visualization directly within the LuCI web interface.
|
||||
|
||||
### 🚀 Key Features
|
||||
|
||||
* **Zero Kernel Contamination (No kmod):**
|
||||
Unlike legacy traffic accounting tools, it does not inject any custom hooks or proprietary kernel modules. It fully leverages the Linux native `/proc/net/nf_conntrack` subsystem—data that the kernel already maintains for NAT routing—guaranteeing **zero system pollution and maximum stability**.
|
||||
* **UI Features:**
|
||||
* **Realtime Connection Speed:** Displays instant throughput for each traffic direction in real time.
|
||||
* **Bit / B Unit Toggling:** Seamlessly switch between `Bit` (bps/Mbps) and `B` (B/s/MB/s) locally via JS without dropping the SSE pipeline.
|
||||
* **Original/Reply Traffic Splitting:** Tears apart conntrack rows to display precise directional analytics (TX/RX isolation).
|
||||
* **QUIC/HTTP3 Recognition:** Automatically sniffs out modern HTTP/3 streams via UDP 443 heuristic auditing.
|
||||
* **IPv4/v6 double stacks** supported.
|
||||
* **Adaptive Layout with Tooltips:** Prevents UI breakage from long IPv6 addresses using CSS ellipsis combined with native hover tooltips.
|
||||
* **Specific IP / Local & Loopback IP filter** supported.
|
||||
* **Local host name** supported.
|
||||
* **GeoIP info** supported.
|
||||
* **Experiment feature** Show app name(YouTube, Instagram, TikTok, etc.) associated with the connection, this will need to patch kmod-nf-conntrack (patch file: 999-netfilter-custom-payload-telemetry-nf_conntrack.patch).
|
||||
* **Cutomized App Icons***
|
||||
|
||||
### 🛠️ Architecture & Directory Tree
|
||||
|
||||
Conforms strictly to the `luci.mk` unified application build framework:
|
||||
|
||||
```text
|
||||
luci-app-conntrack/
|
||||
├── Makefile # Build script using feeds/luci/luci.mk
|
||||
├── luasrc/
|
||||
│ ├── controller/
|
||||
│ │ └── conntrack.lua # Non-blocking Event-Stream (SSE) backend server
|
||||
│ └── view/
|
||||
│ └── conntrack/
|
||||
│ └── conntrack.htm # Vanilla JS dynamic reconnect frontend view
|
||||
└── po/
|
||||
└── zh-cn/
|
||||
└── conntrack.po # I18n translation source file
|
||||
```
|
||||
|
||||
### 🧱 Installation & Compilation
|
||||
|
||||
```bash
|
||||
# Update compilation feeds
|
||||
./scripts/feeds update -i
|
||||
./scripts/feeds install -f luci-app-conntrack
|
||||
|
||||
# Compile the package independently
|
||||
make package/feeds/luci/luci-app-conntrack/compile V=s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 中文说明
|
||||
|
||||
一款为 OpenWrt/LEDE 深度定制、工业级稳定的**实时连接跟踪与单向流量审计看板**。该插件完全基于轻量级的 Event-Stream (SSE) 长连接架构下运行,无需刷新页面即可在 Web 界面实现真正高频、正反向物理隔离的网络吞吐审计。
|
||||
|
||||
### 🚀 核心优势
|
||||
|
||||
* **零内核污染 (无需 kmod):**
|
||||
不同于传统流量统计插件需要往内核安插臃肿的探针或第三方驱动,本插件完全基于 Linux 内核原生维护的 `/proc/net/nf_conntrack`(连接跟踪表)作为数据源。对系统内核**零开销、零污染**,保障极限负载下的系统稳定性。
|
||||
* **UI 特性:**
|
||||
* **实时连接速率:** 高频显示每一条活跃网络连接当前的实时瞬时物理吞吐速率。
|
||||
* **前端零延迟单位切换:** 表头集成智能交互按钮,支持在 `Bit`(比特率,Mbps)和 `B`(字节率,MB/s)之间 1 毫秒无缝切换,无需掐断或重启后台长连接。
|
||||
* **正反单向流量分离:** 将 conntrack 报文拆分为 Original(发起方)与 Reply(回应方),实现真正的单向 TX/RX 独立审计与降序排列。
|
||||
* **QUIC / HTTP3 智能识别:** 基于启发式行为审计,自动将隐藏在 UDP 443 端口下的现代高并发视频/下载流标记并显示为 `QUIC` 协议。
|
||||
* **IPv4/v6 双栈支持**
|
||||
* **超长 IPv6 弹性布局:** 引入 CSS 精确截断裁切,使超长 IPv6 地址自动变为省略号,鼠标悬停时原生 Tooltip 气泡自动浮现完整内容,排版永不撑破。
|
||||
* **手动指定IP或本地以及回环IP 过滤支持**
|
||||
* **本地主机名显示支持**
|
||||
* **地理IP信息显示支持**
|
||||
* **实验特色** 显示与连接相关的应用名称(YouTube, Instagram, TikTok, 王者荣耀..), 你需要给内核模块 kmod-nf-conntrack 打补丁 (补丁文件: 999-netfilter-custom-payload-telemetry-nf_conntrack.patch).
|
||||
* ** 定制化App图标库 **
|
||||
|
||||
### 🧱 依赖与安装
|
||||
|
||||
本插件在 OpenWrt 源码树下的标准相对路径为 `package/feeds/luci/luci-app-conntrack`:
|
||||
|
||||
```bash
|
||||
# 1. 刷新编译环境 feeds 映射
|
||||
./scripts/feeds update -i
|
||||
./scripts/feeds install -f luci-app-conntrack
|
||||
|
||||
# 2. 单包高效构建
|
||||
make package/feeds/luci/luci-app-conntrack/compile V=s
|
||||
```
|
||||
@@ -0,0 +1,48 @@
|
||||
if (typeof (Iconify) != undefined) {
|
||||
Iconify.addIcon('logos:honorofkings', {
|
||||
width: 146,
|
||||
height: 112,
|
||||
|
||||
body: `<g transform="translate(0.000000,112.000000) scale(0.100000,-0.100000)" fill="currentColor" stroke="none">
|
||||
<path d="M635 1105 c-27 -8 -53 -14 -56 -15 -3 0 -40 -33 -81 -74 -59 -58 -79 -85 -93 -126 -17 -52 -17 -61 1 -250 4 -49 2 -75 -10 -104 -9 -22 -15 -40 -13 -42 2 -2 38 32 80 74 l78 78 -22 48 c-16 38 -20 63 -17 112 5 82 41 137 117 176 68 35 127 37 197 4 l52 -24 37 38 37 38 -39 25 c-49 31 -134 57 -182 56 -20 0 -58 -7 -86 -14z"/>
|
||||
<path d="M981 1016 l-84 -84 22 -44 c40 -85 21 -198 -45 -260 -64 -60 -170 -75 -253 -37 l-45 20 -35 -36 -36 -36 23 -19 c46 -38 132 -62 207 -58 73 4 175 40 175 62 0 15 62 76 76 76 6 0 21 23 35 52 25 54 26 60 17 298 -2 66 1 92 15 118 10 17 16 32 15 32 -2 0 -41 -38 -87 -84z"/>
|
||||
<path d="M730 903 c0 -46 4 -62 20 -76 11 -9 20 -28 20 -42 0 -14 -9 -30 -20 -37 -15 -10 -20 -24 -20 -55 0 -50 15 -55 67 -23 82 50 91 191 14 237 -14 9 -38 24 -53 35 l-28 19 0 -58z"/>
|
||||
<path d="M650 908 c-51 -35 -70 -67 -72 -125 -3 -63 17 -95 85 -135 l47 -27 0 57 c0 46 -4 60 -20 70 -11 7 -20 23 -20 37 0 14 9 33 20 42 14 12 20 30 20 60 0 50 -11 54 -60 21z"/>
|
||||
<path d="M485 351 c3 -6 -16 -11 -42 -13 -36 -2 -49 -7 -51 -20 -3 -15 4 -18 42 -18 30 0 48 -5 54 -16 8 -14 10 -14 21 0 11 13 34 16 108 16 85 0 94 2 91 18 -3 14 -16 17 -81 20 -42 2 -77 7 -77 12 0 6 -16 10 -36 10 -19 0 -33 -4 -29 -9z"/>
|
||||
<path d="M765 350 c3 -5 -3 -10 -14 -10 -14 0 -21 -6 -21 -20 0 -23 -8 -22 155 -24 159 -3 204 13 133 48 -35 18 -93 22 -83 6 4 -6 -13 -10 -44 -10 -27 0 -53 5 -56 10 -3 6 -22 10 -41 10 -21 0 -33 -4 -29 -10z"/>
|
||||
<path d="M53 325 c-8 -23 1 -28 59 -29 l53 -1 3 -42 3 -43 -60 0 c-54 0 -61 -2 -61 -20 0 -18 7 -20 60 -20 l60 0 0 -46 0 -45 -60 2 c-51 2 -64 -1 -83 -19 l-21 -22 169 0 170 0 85 85 85 85 97 0 c54 0 98 4 98 8 0 5 -11 18 -23 31 -21 19 -25 20 -37 6 -14 -17 -90 -21 -90 -5 0 6 5 10 10 10 6 0 10 5 10 10 0 18 -56 11 -83 -10 -16 -13 -40 -20 -67 -20 -45 0 -53 -22 -10 -28 21 -3 17 -10 -41 -68 -48 -48 -70 -64 -91 -64 -26 0 -28 2 -28 45 l0 45 55 0 c48 0 55 2 55 20 0 18 -7 20 -55 20 l-55 0 0 46 0 46 55 -4 c51 -3 55 -2 55 20 0 22 -1 22 -155 22 -131 0 -157 -2 -162 -15z"/>
|
||||
<path d="M1070 269 c0 -62 2 -70 15 -59 21 18 21 130 0 130 -12 0 -15 -15 -15 -71z"/>
|
||||
<path d="M1110 277 c0 -51 -4 -68 -20 -82 -11 -10 -20 -30 -20 -44 0 -24 3 -22 55 29 53 52 55 56 55 107 0 40 -4 53 -14 53 -10 0 -16 -14 -19 -42 l-4 -43 -1 43 c-1 30 -6 42 -17 42 -12 0 -15 -14 -15 -63z"/>
|
||||
<path d="M1192 328 c3 -7 15 -15 29 -16 13 -2 34 -13 47 -25 l22 -20 0 36 0 37 -51 0 c-36 0 -49 -4 -47 -12z"/>
|
||||
<path d="M1300 326 c0 -11 9 -16 29 -16 16 0 34 -7 41 -15 19 -23 32 -18 28 13 -3 25 -7 27 -50 30 -38 2 -48 0 -48 -12z"/>
|
||||
<path d="M1210 292 c0 -4 5 -13 10 -18 8 -8 6 -15 -5 -24 -16 -13 -21 -40 -7 -40 5 0 20 13 35 28 27 28 27 29 9 45 -20 18 -42 23 -42 9z"/>
|
||||
<path d="M1320 285 c10 -12 10 -19 2 -27 -7 -7 -12 -21 -12 -32 0 -18 4 -17 32 11 27 27 29 34 18 47 -7 9 -21 16 -32 16 -17 0 -18 -2 -8 -15z"/>
|
||||
<path d="M730 245 c0 -28 4 -35 19 -35 10 0 21 7 25 15 4 12 27 15 116 15 89 0 112 -3 116 -15 4 -8 15 -15 26 -15 16 0 19 6 16 33 l-3 32 -157 3 -158 3 0 -36z"/>
|
||||
<path d="M1243 218 c-42 -43 -43 -45 -43 -111 l0 -67 111 0 c98 0 110 2 99 15 -7 8 -24 15 -39 16 -25 1 -25 1 -3 7 29 9 30 26 2 30 -25 4 -16 22 11 22 10 0 19 5 19 10 0 6 -9 10 -20 10 -11 0 -20 5 -20 10 0 6 9 10 20 10 11 0 20 5 20 10 0 6 -26 10 -59 10 -33 0 -63 -4 -66 -10 -3 -5 1 -10 9 -10 9 0 16 -4 16 -10 0 -5 -11 -10 -25 -10 -30 0 -32 16 -4 46 11 12 19 32 17 44 -3 19 -7 17 -45 -22z m57 -98 c0 -5 -12 -10 -26 -10 -14 0 -23 4 -19 10 3 6 15 10 26 10 10 0 19 -4 19 -10z m-7 -47 c-7 -2 -21 -2 -30 0 -10 3 -4 5 12 5 17 0 24 -2 18 -5z"/>
|
||||
<path d="M1372 248 c-18 -18 -14 -38 8 -38 15 0 20 7 20 25 0 27 -9 32 -28 13z"/>
|
||||
<path d="M853 208 c1 -9 -17 -14 -60 -16 -47 -2 -63 -7 -63 -17 0 -12 17 -15 83 -15 l83 0 59 -60 c58 -58 61 -60 112 -60 l53 0 0 57 0 56 -25 -23 c-14 -13 -25 -29 -25 -37 0 -24 -25 -13 -65 27 l-39 40 42 0 c31 0 42 4 42 15 0 12 -13 15 -60 15 -45 0 -62 4 -66 15 -8 19 -75 21 -71 3z"/>
|
||||
<path d="M1151 186 c-19 -20 -22 -33 -19 -82 3 -52 5 -59 26 -62 21 -3 22 0 22 66 0 39 3 77 6 86 9 24 -9 19 -35 -8z"/>
|
||||
<path d="M506 178 c-20 -17 -18 -17 57 -18 48 0 77 -4 77 -11 0 -7 -29 -10 -85 -8 -81 3 -86 2 -115 -26 -16 -16 -30 -39 -30 -52 0 -23 1 -23 135 -23 131 0 135 1 150 24 11 16 15 41 13 76 l-3 52 -89 1 c-72 2 -94 -1 -110 -15z m134 -88 c0 -6 -32 -10 -75 -10 -43 0 -75 4 -75 10 0 6 32 10 75 10 43 0 75 -4 75 -10z"/>
|
||||
<path d="M745 90 l-49 -50 35 0 c28 0 43 8 74 40 21 22 43 40 47 40 4 0 8 -18 8 -40 0 -38 2 -40 30 -40 28 0 30 2 30 38 0 50 -14 62 -75 62 -48 0 -54 -3 -100 -50z"/>
|
||||
</g>`
|
||||
});
|
||||
|
||||
Iconify.addIcon('logos:tencent-video', {
|
||||
width: 32,
|
||||
height: 32,
|
||||
|
||||
body: `<path d="M0 0 C9.05 0.15 14.79 3.96 21.82 9.41 C23 11 23 11 23.41 13.46 C22.76 17.5 20.35 18.88 17.29 21.35 C12.81 24.58 8.92 26.7 3.44 27.56 C-0.22 26.96 -2.73 26.42 -5.68 24.16 C-8.76 19.11 -7.69 11.69 -7 6 C-6.34 5.01 -5.68 4.02 -5 3 C-3.68 3 -2.36 3 -1 3 C-0.67 2.01 -0.34 1.02 0 0 Z " fill="#46E40D" transform="translate(8,2)"/>
|
||||
<path d="M0 0 C9.05 0.15 14.79 3.96 21.82 9.41 C23 11 23 11 23.41 13.46 C22.76 17.5 20.35 18.88 17.29 21.35 C11.75 25.34 7.04 27.99 0 27 C-0.66 26.01 -1.32 25.02 -2 24 C-1.04 23.6 -0.07 23.2 0.92 22.79 C2.2 22.26 3.47 21.72 4.75 21.19 C5.7 20.8 5.7 20.8 6.67 20.4 C10.13 18.94 13.12 17.42 16 15 C14.58 11.45 13.12 10.18 9.75 8.44 C8.55 7.81 8.55 7.81 7.33 7.17 C6.56 6.79 5.79 6.4 5 6 C3.82 5.4 2.65 4.8 1.44 4.19 C0.63 3.8 -0.17 3.4 -1 3 C-0.67 2.01 -0.34 1.02 0 0 Z " fill="#0199F8" transform="translate(8,2)"/>
|
||||
<path d="M0 0 C0.99 0 1.98 0 3 0 C3.33 6.93 3.66 13.86 4 21 C3.34 21.33 2.68 21.66 2 22 C0.02 21 0.02 21 -2 19 C-2.35 16.18 -2.47 13.8 -2.38 11 C-2.37 10.28 -2.37 9.55 -2.37 8.8 C-2.31 5.03 -2.14 3.2 0 0 Z " fill="#F78B00" transform="translate(3,5)"/>
|
||||
<path d="M0 0 C3.96 0.57 6.67 1.81 10 4 C7.87 6.34 5.94 7.81 3 9 C2.01 9 1.02 9 0 9 C0 6.03 0 3.06 0 0 Z " fill="#F2FDED" transform="translate(9,11)"/>`
|
||||
});
|
||||
|
||||
Iconify.addIcon('logos:iqiyi', {
|
||||
width: 125,
|
||||
height: 45,
|
||||
body: `<g>
|
||||
<path style=" stroke:none;fill-rule:nonzero;fill:rgb(0%,86.274511%,35.294119%);fill-opacity:1;" d="M 70.894531 44.335938 C 70.898438 44.4375 70.941406 44.535156 71.019531 44.605469 C 71.09375 44.671875 71.191406 44.710938 71.292969 44.707031 L 80.816406 44.707031 C 81.03125 44.714844 81.207031 44.550781 81.21875 44.335938 L 81.21875 1.511719 C 81.214844 1.410156 81.167969 1.3125 81.09375 1.242188 C 81.019531 1.171875 80.921875 1.136719 80.816406 1.140625 L 71.292969 1.140625 C 71.082031 1.132812 70.90625 1.296875 70.894531 1.507812 Z M 0.886719 44.335938 C 0.886719 44.441406 0.929688 44.539062 1.007812 44.609375 C 1.082031 44.679688 1.183594 44.71875 1.285156 44.714844 L 10.808594 44.714844 C 11.019531 44.722656 11.199219 44.558594 11.207031 44.34375 L 11.207031 17.375 C 11.199219 17.160156 11.019531 16.996094 10.808594 17.003906 L 1.285156 17.003906 C 1.074219 16.996094 0.894531 17.160156 0.886719 17.375 Z M 64.609375 37.347656 L 59.898438 33.550781 C 59.808594 33.480469 59.742188 33.378906 59.714844 33.265625 C 59.691406 33.148438 59.707031 33.023438 59.761719 32.914062 C 65.042969 22.929688 62.027344 10.558594 52.753906 4.144531 C 43.484375 -2.269531 30.894531 -0.6875 23.476562 7.820312 C 16.0625 16.328125 16.179688 29.0625 23.742188 37.4375 C 31.308594 45.808594 43.925781 47.160156 53.082031 40.578125 C 53.269531 40.4375 53.527344 40.4375 53.714844 40.578125 L 58.769531 44.648438 C 58.851562 44.714844 58.960938 44.746094 59.066406 44.734375 C 59.171875 44.722656 59.269531 44.667969 59.335938 44.582031 L 64.664062 37.917969 C 64.734375 37.835938 64.765625 37.726562 64.753906 37.621094 C 64.746094 37.515625 64.695312 37.414062 64.609375 37.347656 Z M 42.460938 35.316406 C 35.5625 36.566406 28.914062 31.839844 27.640625 24.777344 C 26.367188 17.71875 30.945312 10.949219 37.855469 9.699219 C 44.765625 8.445312 51.402344 13.171875 52.675781 20.234375 C 53.949219 27.300781 49.367188 34.070312 42.460938 35.316406 Z M 142.328125 1.140625 L 132.804688 1.140625 C 132.589844 1.132812 132.410156 1.296875 132.402344 1.507812 L 132.402344 44.335938 C 132.410156 44.550781 132.589844 44.714844 132.804688 44.707031 L 142.328125 44.707031 C 142.429688 44.710938 142.527344 44.671875 142.601562 44.605469 C 142.675781 44.535156 142.722656 44.4375 142.726562 44.335938 L 142.726562 1.511719 C 142.71875 1.300781 142.539062 1.132812 142.328125 1.140625 Z M 9.226562 11.523438 C 12.066406 9.761719 12.945312 6.019531 11.191406 3.167969 C 9.433594 0.316406 5.707031 -0.566406 2.867188 1.195312 C 0.0273438 2.960938 -0.851562 6.699219 0.902344 9.550781 C 2.660156 12.402344 6.386719 13.285156 9.226562 11.523438 Z M 127.363281 1.140625 L 117.214844 1.140625 C 117.042969 1.136719 116.886719 1.226562 116.796875 1.371094 L 107.371094 18.203125 C 107.371094 18.203125 107.300781 18.328125 107.207031 18.332031 C 107.113281 18.332031 107.046875 18.203125 107.046875 18.203125 L 97.617188 1.375 C 97.53125 1.226562 97.371094 1.136719 97.199219 1.140625 L 87.050781 1.140625 C 86.886719 1.140625 86.734375 1.230469 86.660156 1.378906 C 86.582031 1.527344 86.59375 1.703125 86.691406 1.839844 L 101.773438 27.316406 C 101.917969 27.5625 101.992188 27.84375 101.992188 28.128906 L 101.992188 44.261719 C 101.988281 44.378906 102.035156 44.492188 102.117188 44.578125 C 102.199219 44.664062 102.308594 44.714844 102.425781 44.714844 L 111.972656 44.714844 C 112.089844 44.714844 112.199219 44.667969 112.285156 44.585938 C 112.367188 44.5 112.410156 44.386719 112.410156 44.269531 L 112.410156 28.128906 C 112.410156 27.84375 112.488281 27.5625 112.632812 27.316406 L 127.722656 1.84375 C 127.820312 1.707031 127.835938 1.527344 127.757812 1.378906 C 127.683594 1.230469 127.53125 1.140625 127.363281 1.140625 Z M 127.363281 1.140625 "/>
|
||||
</g>`
|
||||
});
|
||||
|
||||
}
|
||||
@@ -0,0 +1,474 @@
|
||||
module("luci.controller.conntrack", package.seeall)
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/proc/net/nf_conntrack") then
|
||||
return
|
||||
end
|
||||
entry({"admin", "network", "conntrack"}, template("conntrack/conntrack"), _("ConnTrack"), 90).dependent = true
|
||||
entry({"admin", "network", "conntrack", "get_settings"}, call("action_get_settings"))
|
||||
entry({"admin", "network", "conntrack", "set_settings"}, call("action_set_settings"))
|
||||
entry({"admin", "network", "conntrack_stream"}, call("action_stream")).dependent = true
|
||||
end
|
||||
|
||||
function action_get_settings()
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
local jsonc = require("luci.jsonc")
|
||||
luci.http.prepare_content("application/json")
|
||||
|
||||
local res = {
|
||||
interval = uci:get("conntrack", "settings", "interval") or "1500",
|
||||
rows = uci:get("conntrack", "settings", "rows") or "50",
|
||||
filter_local = uci:get("conntrack", "settings", "filter_local") or "true",
|
||||
show_appname = uci:get("conntrack", "settings", "show_appname") or "false"
|
||||
}
|
||||
luci.http.write(jsonc.stringify(res))
|
||||
end
|
||||
|
||||
function action_set_settings()
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
local jsonc = require("luci.jsonc")
|
||||
luci.http.prepare_content("application/json")
|
||||
|
||||
local raw_body = luci.http.content()
|
||||
if raw_body then
|
||||
local data = jsonc.parse(raw_body)
|
||||
if data and type(data) == "table" then
|
||||
local has_settings = false
|
||||
uci:foreach("conntrack", "global", function(s)
|
||||
if s[".name"] == "settings" then
|
||||
has_settings = true
|
||||
return false
|
||||
end
|
||||
end)
|
||||
|
||||
if not has_settings then
|
||||
uci:section("conntrack", "global", "settings")
|
||||
end
|
||||
|
||||
if data.interval then uci:set("conntrack", "settings", "interval", tostring(data.interval)) end
|
||||
if data.rows then uci:set("conntrack", "settings", "rows", tostring(data.rows)) end
|
||||
if data.filter_local ~= nil then uci:set("conntrack", "settings", "filter_local", tostring(data.filter_local)) end
|
||||
if data.show_appname ~= nil then uci:set("conntrack", "settings", "show_appname", tostring(data.show_appname)) end
|
||||
|
||||
local commit_ok = uci:commit("conntrack")
|
||||
if commit_ok then
|
||||
luci.http.write('{"status":"ok"}')
|
||||
else
|
||||
luci.http.write('{"status":"commit_failed"}')
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
luci.http.write('{"status":"error"}')
|
||||
end
|
||||
|
||||
local function nixio_lines_fast(path)
|
||||
local nixio = require("nixio")
|
||||
local fd = nixio.open(path, "r")
|
||||
if not fd then return function() return nil end end
|
||||
|
||||
local chunks = {}
|
||||
while true do
|
||||
local chunk = fd:read(65536)
|
||||
if not chunk or #chunk == 0 then break end
|
||||
table.insert(chunks, chunk)
|
||||
end
|
||||
fd:close()
|
||||
|
||||
local content = table.concat(chunks)
|
||||
local pos = 1
|
||||
local content_len = #content
|
||||
|
||||
return function()
|
||||
if pos > content_len then
|
||||
return nil
|
||||
end
|
||||
|
||||
local newline = content:find("\n", pos, true)
|
||||
if newline then
|
||||
local line = content:sub(pos, newline - 1)
|
||||
pos = newline + 1
|
||||
if #line > 0 and line:sub(-1) == "\r" then line = line:sub(1, -2) end
|
||||
return line
|
||||
else
|
||||
local line = content:sub(pos)
|
||||
pos = content_len + 1
|
||||
if #line > 0 and line:sub(-1) == "\r" then line = line:sub(1, -2) end
|
||||
return #line > 0 and line or nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function file_exists(path)
|
||||
local f = io.open(path, "r")
|
||||
if f then f:close() return true else return false end
|
||||
end
|
||||
|
||||
local function create_file(path)
|
||||
local file, err = io.open(path, "w")
|
||||
if not file then return false else file:close() return true end
|
||||
end
|
||||
|
||||
local function remove_file(path)
|
||||
if file_exists(path) then
|
||||
os.remove(path)
|
||||
end
|
||||
end
|
||||
|
||||
-- Experiment: the performance wouldn't be quite good than realtime 'action_strem'
|
||||
function action_stream_ubus()
|
||||
local nixio = require "nixio"
|
||||
local jsonc = require "luci.jsonc"
|
||||
local http = require "luci.http"
|
||||
|
||||
local conntrack_lock_file = '/tmp/conntrack_event.lock'
|
||||
|
||||
local interval = tonumber(http.formvalue("interval")) or 1000
|
||||
local max_rows = tonumber(http.formvalue("rows")) or 50
|
||||
local filter_src = http.formvalue("filter_src") or "all"
|
||||
local filter_dst = http.formvalue("filter_dst") or "all"
|
||||
local filter_local = http.formvalue("filter_local") or "false"
|
||||
|
||||
http.header("Content-Type", "text/event-stream; charset=utf-8")
|
||||
http.header("Cache-Control", "no-cache")
|
||||
http.header("Connection", "keep-alive")
|
||||
http.context.redirect = false
|
||||
|
||||
http.write("retry: 1000\n\n")
|
||||
io.flush()
|
||||
|
||||
if not file_exists(conntrack_lock_file) then
|
||||
create_file(conntrack_lock_file)
|
||||
end
|
||||
local stat = nixio.fs.stat(conntrack_lock_file)
|
||||
nixio.fs.utimes(conntrack_lock_file, os.time(), stat.atime)
|
||||
|
||||
local handle = io.popen("ubus listen conntrack_event 2>/dev/null")
|
||||
if not handle then return end
|
||||
|
||||
for line in handle:lines() do
|
||||
nixio.fs.utimes(conntrack_lock_file, os.time(), stat.atime)
|
||||
local ct = jsonc.parse(line)
|
||||
if ct and ct.conntrack_event then
|
||||
for _, value in ipairs(ct.conntrack_event) do
|
||||
-- [Your custom code goes here]
|
||||
end
|
||||
|
||||
local success = pcall(function()
|
||||
io.write("data: " .. jsonc.stringify(ct.conntrack_event) .. "\n\n")
|
||||
io.flush()
|
||||
end)
|
||||
if not success then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if luci.http.getenv("HTTP_CONNECTION") == "close" then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
handle:close()
|
||||
end
|
||||
|
||||
function action_stream()
|
||||
local nixio = require "nixio"
|
||||
local jsonc = require "luci.jsonc"
|
||||
local http = require "luci.http"
|
||||
|
||||
local interval = tonumber(http.formvalue("interval")) or 1000
|
||||
local max_rows = tonumber(http.formvalue("rows")) or 50
|
||||
local filter_src = http.formvalue("filter_src") or "all"
|
||||
local filter_dst = http.formvalue("filter_dst") or "all"
|
||||
local filter_local = http.formvalue("filter_local") or "false"
|
||||
local sort = http.formvalue("sort") or "rate"
|
||||
|
||||
local lan_prefix = "192.168.1."
|
||||
local lan_ip = luci.util.exec("uci -q get network.lan.ipaddr"):gsub("%s+", "")
|
||||
if lan_ip and lan_ip ~= "" then
|
||||
if lan_ip:match("^192%.168%.") or lan_ip:match("^10%.") or lan_ip:match("^172%.") then
|
||||
lan_prefix = lan_ip:match("^([%d%.]+%.)%d+$") or "192.168.1."
|
||||
end
|
||||
end
|
||||
|
||||
local function is_local_ip(ip)
|
||||
if not ip then return true end
|
||||
local clean_ip = ip:lower():gsub("%s+", ""):gsub("%[", ""):gsub("%]", "")
|
||||
if clean_ip == "127.0.0.1" or clean_ip == "0.0.0.0" or clean_ip == "::1" or clean_ip == "::" then return true end
|
||||
local no_zeros = clean_ip:gsub("0", "")
|
||||
if no_zeros == ":::::::1" or no_zeros == "::::::::" then return true end
|
||||
if clean_ip:find("^192%.168%.") or clean_ip:find("^10%.") or clean_ip:find("^172%.1[6-9]%.") or clean_ip:find("^172%.2%d%.") or clean_ip:find("^172%.3%.") then
|
||||
return true
|
||||
end
|
||||
if clean_ip:find("^fc") or clean_ip:find("^fd") then
|
||||
return true
|
||||
end
|
||||
if clean_ip:find("^fe[89ab]") then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function normalize_ipv6(ip)
|
||||
if not ip or not ip:find(":") then return ip end
|
||||
local clean_ip = ip:lower():gsub("%s+", "")
|
||||
if clean_ip:find("::") then
|
||||
local _, count = clean_ip:gsub(":", "")
|
||||
local colons_to_add = 8 - count
|
||||
local replacement = ":"
|
||||
for i = 1, colons_to_add do replacement = replacement .. ":" end
|
||||
clean_ip = clean_ip:gsub("::", replacement)
|
||||
end
|
||||
local parts = {}
|
||||
for part in clean_ip:gmatch("([^:]+)") do
|
||||
while #part < 4 do part = "0" .. part end
|
||||
table.insert(parts, part)
|
||||
end
|
||||
while #parts < 8 do table.insert(parts, "0000") end
|
||||
return table.concat(parts, ":")
|
||||
end
|
||||
|
||||
local lease_map = {}
|
||||
local mac_host_map = {}
|
||||
local ipv6_mac_map = {}
|
||||
|
||||
local lf = io.open("/tmp/dhcp.leases", "r")
|
||||
if lf then
|
||||
for line in lf:lines() do
|
||||
local mac, ip, host = line:match("^%d+%s+([%a%d%:]+)%s+([%d%.%a%d%:]+)%s+([%w%-_]+)")
|
||||
if not mac or not host then
|
||||
ip, host = line:match("^%d+%s+[%a%d%:]+%s+([%d%.]+)%s+([%w%-_]+)")
|
||||
end
|
||||
if ip and host and host ~= "*" then
|
||||
lease_map[ip] = host
|
||||
end
|
||||
if mac and host and host ~= "*" then
|
||||
mac_host_map[mac:lower()] = host
|
||||
end
|
||||
end
|
||||
lf:close()
|
||||
end
|
||||
|
||||
local sys_host = luci.util.exec("uci -q get system.@system[0].hostname"):gsub("%s+", "")
|
||||
if sys_host == "" then sys_host = "lan" end
|
||||
if lan_ip and lan_ip ~= "" then
|
||||
lease_map[lan_ip] = sys_host
|
||||
end
|
||||
|
||||
-- adding ipv6 <-> host mapping
|
||||
local np = io.popen("ip -6 neighbor | grep -E -v 'FAILED'")
|
||||
if np then
|
||||
for line in np:lines() do
|
||||
local ip, mac = line:match("^([%a%d%:]+).-lladdr%s+([%a%d%:]+)")
|
||||
if ip and mac then
|
||||
local clean_ip = ip:lower():gsub("%s+", "")
|
||||
if not is_local_ip(clean_ip) then
|
||||
local norm_ip = normalize_ipv6(clean_ip)
|
||||
ipv6_mac_map[norm_ip] = mac:lower()
|
||||
end
|
||||
end
|
||||
end
|
||||
np:close()
|
||||
end
|
||||
|
||||
for ipv6, mac in pairs(ipv6_mac_map) do
|
||||
local host = mac_host_map[mac]
|
||||
if host then
|
||||
lease_map[ipv6] = host
|
||||
end
|
||||
end
|
||||
|
||||
if interval < 500 then interval = 500 end
|
||||
if interval > 10000 then interval = 10000 end
|
||||
|
||||
http.header("Content-Type", "text/event-stream; charset=utf-8")
|
||||
http.header("Cache-Control", "no-cache")
|
||||
http.header("Connection", "keep-alive")
|
||||
http.context.redirect = false
|
||||
|
||||
http.write("retry: 1000\n\n")
|
||||
io.flush()
|
||||
|
||||
local function get_current_time()
|
||||
local sec, usec = nixio.gettimeofday()
|
||||
return sec + (usec / 1000000)
|
||||
end
|
||||
|
||||
local last_connections = {}
|
||||
local last_timestamp = get_current_time()
|
||||
local layer3, proto, state, src1, dst1, sport1, dport1, bytes1, src2, dst2, sport2, dport2, bytes2, remain
|
||||
local type1, code1, id1, type2, code2, id2
|
||||
local srckey1, dstkey1, srckey2, dstkey2
|
||||
|
||||
while true do
|
||||
local sec = math.floor(interval / 1000)
|
||||
local nsec = (interval % 1000) * 1000000
|
||||
-- nixio.nanosleep(sec, nsec)
|
||||
|
||||
local current_timestamp = get_current_time()
|
||||
local current_connections = {}
|
||||
local ip_map = {}
|
||||
|
||||
-- local f = io.open("/proc/net/nf_conntrack", "r")
|
||||
-- if not f then break end
|
||||
|
||||
-- for line in f:lines() do
|
||||
for line in nixio_lines_fast("/proc/net/nf_conntrack") do
|
||||
local s_end = line:find("src=", 1, true)
|
||||
local has_state = true
|
||||
local remain = ""
|
||||
local payload = ""
|
||||
if s_end and line:byte(s_end-2) >= 48 and line:byte(s_end-2) <= 57 then
|
||||
has_state = false
|
||||
end
|
||||
if has_state then
|
||||
layer3, proto, state, src1, dst1, sport1, dport1, bytes1, src2, dst2, sport2, dport2, bytes2, remain = line:match("^(%w+)%s+%d+%s+(%w+)%s+%d+%s+%d+%s+(%S+)%s+src=([%a%d%.%:]+)%s+dst=([%a%d%.%:]+)%s+sport=(%d+)%s+dport=(%d+).-bytes=(%d+).-src=([%a%d%.%:]+)%s+dst=([%a%d%.%:]+)%s+sport=(%d+)%s+dport=(%d+).-bytes=(%d+)(.*)")
|
||||
else
|
||||
state = 'UNTRACKED'
|
||||
layer3, proto, src1, dst1, sport1, dport1, bytes1, src2, dst2, sport2, dport2, bytes2, remain = line:match("^(%w+)%s+%d+%s+(%w+)%s+%d+%s+%d+%s+src=([%a%d%.%:]+)%s+dst=([%a%d%.%:]+)%s+sport=(%d+)%s+dport=(%d+).-bytes=(%d+).-src=([%a%d%.%:]+)%s+dst=([%a%d%.%:]+)%s+sport=(%d+)%s+dport=(%d+).-bytes=(%d+)(.*)")
|
||||
end
|
||||
if not layer3 then -- icmp
|
||||
layer3, proto, src1, dst1, type1, code1, id1, bytes1, src2, dst2, type2, code2, id2, bytes2, remain = line:match("^(%w+)%s+%d+%s+(%w+)%s+%d+%s+%d+%s+src=([%a%d%.%:]+)%s+dst=([%a%d%.%:]+)%s+type=(%d+)%s+code=(%d+)%s+id=(%d+).-bytes=(%d+).-src=([%a%d%.%:]+)%s+dst=([%a%d%.%:]+)%s+type=(%d+)%s+code=(%d+)%s+id=(%d+).-bytes=(%d+)(.*)")
|
||||
if not layer3 then -- gre
|
||||
layer3, proto, src1, dst1, srckey1, dstkey1, bytes1, src2, dst2, srckey2, dstkey2, bytes2, remain = line:match("^(%w+)%s+%d+%s+(%w+)%s+%d+%s+%d+%s+src=([%a%d%.%:]+)%s+dst=([%a%d%.%:]+)%s+srckey=(%S+)%s+dstkey=(%S+).-bytes=(%d+).-src=([%a%d%.%:]+)%s+dst=([%a%d%.%:]+)%s+srckey=(%S+)%s+dstkey=(%S+).-bytes=(%d+)(.*)")
|
||||
end
|
||||
if layer3 then
|
||||
sport1, dport1, sport2, dport2 = "", "", "", ""
|
||||
end
|
||||
end
|
||||
|
||||
if remain then payload = remain:match("payload=(%S+)") or "" end
|
||||
|
||||
if layer3 then
|
||||
if bytes1 and bytes2 then
|
||||
local display_proto = proto
|
||||
if proto == "udp" and (sport1 == "443" or dport1 == "443" or sport2 == "443" or dport2 == "443") then
|
||||
display_proto = "quic"
|
||||
end
|
||||
|
||||
if filter_local == "true" and is_local_ip(src1) and is_local_ip(dst1) then
|
||||
-- do nothing
|
||||
else
|
||||
if layer3 == "ipv4" then
|
||||
-- IPv4 DNAT revert --
|
||||
if src2 == lan_ip or src2 == '127.0.0.1' then src2 = dst1 sport2 = dport1 end
|
||||
-- IPv4 SNAT revert --
|
||||
if dst2 ~= src1 then dst2 = src1 dport2 = sport1 end
|
||||
end
|
||||
|
||||
ip_map[src1] = true
|
||||
ip_map[dst1] = true
|
||||
ip_map[src2] = true
|
||||
ip_map[dst2] = true
|
||||
|
||||
local pass_orig = true
|
||||
if filter_src ~= "all" and src1 ~= filter_src then pass_orig = false end
|
||||
if filter_dst ~= "all" and dst1 ~= filter_dst then pass_orig = false end
|
||||
|
||||
if pass_orig then
|
||||
local key_orig = string.format("%s_%s_%s:%s->%s:%s_ORIG", layer3, display_proto, src1, sport1, dst1, dport1)
|
||||
current_connections[key_orig] = {
|
||||
l3 = layer3, proto = display_proto, state = state,
|
||||
src = src1, sport = sport1, dst = dst1, dport = dport1, bytes = tonumber(bytes1), speed = 0, payload = payload
|
||||
}
|
||||
end
|
||||
|
||||
local pass_repl = true
|
||||
if filter_src ~= "all" and src2 ~= filter_src then pass_repl = false end
|
||||
if filter_dst ~= "all" and dst2 ~= filter_dst then pass_repl = false end
|
||||
|
||||
if pass_repl then
|
||||
local key_repl = string.format("%s_%s_%s:%s->%s:%s_REPL", layer3, display_proto, src2, sport2, dst2, dport2)
|
||||
current_connections[key_repl] = {
|
||||
l3 = layer3, proto = display_proto, state = state,
|
||||
src = src2, sport = sport2, dst = dst2, dport = dport2, bytes = tonumber(bytes2), speed = 0, payload = payload
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- f:close()
|
||||
local lines_proc_time = get_current_time() - current_timestamp
|
||||
|
||||
local delta_time = current_timestamp - last_timestamp
|
||||
if delta_time > 0 then
|
||||
for key, curr in pairs(current_connections) do
|
||||
local prev = last_connections[key]
|
||||
if prev then
|
||||
local diff = curr.bytes - prev.bytes
|
||||
curr.speed = diff >= 0 and math.floor(diff / delta_time) or 0
|
||||
else
|
||||
curr.speed = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local sorted_list = current_connections
|
||||
local sorted_list = {}
|
||||
for _, conn in pairs(current_connections) do
|
||||
-- if conn.speed > 0 then
|
||||
table.insert(sorted_list, conn)
|
||||
-- end
|
||||
end
|
||||
|
||||
if sort == 'rate' then
|
||||
table.sort(sorted_list, function(a, b) return a.speed > b.speed end)
|
||||
elseif sort == 'flow' then
|
||||
table.sort(sorted_list, function(a, b) return a.bytes > b.bytes end)
|
||||
end
|
||||
|
||||
local output_list = {}
|
||||
for i = 1, math.min(#sorted_list, max_rows) do
|
||||
table.insert(output_list, sorted_list[i])
|
||||
end
|
||||
|
||||
local lan_ips = {}
|
||||
local v4_ips = {}
|
||||
local v6_ips = {}
|
||||
local lan_pattern = "^" .. lan_prefix:gsub("%.", "%%.")
|
||||
|
||||
for ip in pairs(ip_map) do
|
||||
if ip:find(":") then
|
||||
table.insert(v6_ips, ip)
|
||||
elseif ip:match(lan_pattern) then
|
||||
table.insert(lan_ips, ip)
|
||||
else
|
||||
table.insert(v4_ips, ip)
|
||||
end
|
||||
end
|
||||
table.sort(lan_ips)
|
||||
table.sort(v4_ips)
|
||||
table.sort(v6_ips)
|
||||
|
||||
local unique_ips = {}
|
||||
for _, ip in ipairs(lan_ips) do table.insert(unique_ips, ip) end
|
||||
for _, ip in ipairs(v4_ips) do table.insert(unique_ips, ip) end
|
||||
for _, ip in ipairs(v6_ips) do table.insert(unique_ips, ip) end
|
||||
|
||||
local response = {
|
||||
lines_proc_time = string.format("%.2f", lines_proc_time),
|
||||
delta = string.format("%.2f", delta_time),
|
||||
connections = output_list,
|
||||
unique_ips = unique_ips,
|
||||
host_map = lease_map
|
||||
}
|
||||
|
||||
local json_str = jsonc.stringify(response)
|
||||
local ok = pcall(function()
|
||||
io.write("data: " .. json_str .. "\n\n")
|
||||
io.flush()
|
||||
end)
|
||||
|
||||
if not ok then break end
|
||||
|
||||
if luci.http.getenv("HTTP_CONNECTION") == "close" then
|
||||
break
|
||||
end
|
||||
|
||||
last_connections = current_connections
|
||||
last_timestamp = current_timestamp
|
||||
|
||||
nixio.nanosleep(sec, nsec)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,92 @@
|
||||
module("luci.controller.conntrack_icons", package.seeall)
|
||||
|
||||
function index()
|
||||
entry({"admin", "network", "conntrack", "conntrack_icons_config"}, cbi("conntrack_icons"), nil, nil).dependent = true
|
||||
entry({"admin", "network", "conntrack", "conntrack_custom_js"}, call("action_generate_js"), nil, nil)
|
||||
end
|
||||
|
||||
local function base64_decode(data)
|
||||
local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
data = string.gsub(data, '[^'..b..'=]', '')
|
||||
return (data:gsub('.', function(x)
|
||||
if (x == '=') then return '' end
|
||||
local r, f = '', (b:find(x) - 1)
|
||||
for i = 6, 1, -1 do r = r .. (f % 2^i - f % 2^(i-1) >= 2^(i-1) and '1' or '0') end
|
||||
return r;
|
||||
end):gsub('%d%d%d%d%d%d%d%d', function(x)
|
||||
local c = 0
|
||||
for i = 1, 8 do c = c + (x:sub(i, i) == '1' and 2^(8-i) or 0) end
|
||||
return string.char(c)
|
||||
end))
|
||||
end
|
||||
|
||||
function action_generate_js()
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
luci.http.prepare_content("application/javascript")
|
||||
|
||||
local rules = {}
|
||||
local bundles = {}
|
||||
|
||||
uci:foreach("conntrack", "regex_icon", function(s)
|
||||
if s.pattern and s.icon then
|
||||
local raw_pattern, modifier = s.pattern:match("^/(.-)/(%a*)$")
|
||||
if raw_pattern then
|
||||
table.insert(rules, string.format("\t\t{ reg: new RegExp(%q, \"%s\"), icon: %q }", raw_pattern, modifier or "", s.icon))
|
||||
else
|
||||
table.insert(rules, string.format("\t\t{ reg: new RegExp(%q, \"i\"), icon: %q }", s.pattern, s.icon))
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
uci:foreach("conntrack", "my_custom_icons", function(s)
|
||||
if s.name and s.svg_body then
|
||||
-- local safe_svg = nixio.bin.b64decode(s.svg_body):gsub("[\r\n\t]", " ")
|
||||
table.insert(bundles, string.format("\t\t%q: { body: %q, width: %d, height: %d }",
|
||||
s.name, s.svg_body, tonumber(s.width) or 24, tonumber(s.height) or 24))
|
||||
end
|
||||
end)
|
||||
|
||||
luci.http.write("window._customIconRules = [\n" .. table.concat(rules, ",\n") .. "\n];\n\n")
|
||||
luci.http.write("window._customIconBundles = {\n" .. table.concat(bundles, ",\n") .. "\n};\n\n")
|
||||
|
||||
luci.http.write([[
|
||||
(function() {
|
||||
if (window.Iconify && window._customIconBundles) {
|
||||
for (var name in window._customIconBundles) {
|
||||
if (window._customIconBundles.hasOwnProperty(name)) {
|
||||
var b = window._customIconBundles[name];
|
||||
var parts = name.split(':');
|
||||
if (parts.length === 2) {
|
||||
var prefix = parts[0];
|
||||
var localName = parts[1];
|
||||
var iconData = { icons: {} };
|
||||
iconData.icons[localName] = { body: atob(b.body), width: b.width, height: b.height };
|
||||
iconData.prefix = prefix;
|
||||
Iconify.addIcon(name, iconData.icons[localName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
var previews = document.querySelectorAll(".iconify-live-preview");
|
||||
previews.forEach(function(span) {
|
||||
var iconName = span.getAttribute("data-icon");
|
||||
if (iconName && window.Iconify && typeof window.Iconify.getIcon === "function") {
|
||||
var iconObj = window.Iconify.getIcon(iconName);
|
||||
if (iconObj && iconObj.body) {
|
||||
var iw = 48, ih = 48;
|
||||
if (iconObj.width/iconObj.height > 2 ||
|
||||
iconObj.height/iconObj.width > 2) {
|
||||
iw = ""; ih = "";
|
||||
}
|
||||
span.innerHTML = '<svg xmlns="http://w3.org" viewBox="0 0 ' + (iconObj.width || 24) + ' ' + (iconObj.height || 24) + '" width="' + iw + '" height="' + ih + '" style="vertical-align:middle;">' + iconObj.body + '</svg>';
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
})();
|
||||
]])
|
||||
end
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
local i18n = require("luci.i18n")
|
||||
local nixio = require("nixio")
|
||||
|
||||
local m = Map("conntrack", i18n.translate("Conntrack Custom App Icons"), i18n.translate("Manage your custom application regex mappings and SVG icons here. Note that the regex and icon mappings only apply to tracked TCP connections that successfully return a domain name for matching, and will not affect existing hardcoded rules (such as YouTube detection). Custom regex expressions and icon mappings will be matched with the highest priority based on their ranking order."))
|
||||
|
||||
local old_map_render = m.render
|
||||
function m.render(self, ...)
|
||||
luci.http.write([[
|
||||
<script src="https://code.iconify.design/3/3.1.0/iconify.min.js"></script>
|
||||
<script type="text/javascript" src="]] .. luci.dispatcher.build_url('admin/network/conntrack/conntrack_custom_js') .. [["></script>
|
||||
<h2>
|
||||
<ul class="cbi-tabmenu" style="border-bottom:0 !important;">
|
||||
<li class="cbi-tab-disabled"><a href="]] .. luci.dispatcher.build_url('admin/network/conntrack') .. [[">]] .. i18n.translate("ConnTrack") .. [[</a></li>
|
||||
<li class="cbi-tab"><a href="]] .. luci.dispatcher.build_url('admin/network/conntrack/conntrack_icons_config') .. [["><strong>]] .. i18n.translate("Custom App Icons Config") .. [[</strong></a></li>
|
||||
</ul>
|
||||
</h2>
|
||||
]])
|
||||
return old_map_render(self, ...)
|
||||
end
|
||||
|
||||
local s1 = m:section(TypedSection, "regex_icon", i18n.translate("Regex Expression & Icon Mappings"))
|
||||
s1.description = i18n.translate('You can use an <a href="https://icon-sets.iconify.design/" target="_blank">existing Iconify ID</a> (e.g., logos:youtube) or a custom ID defined in the Custom SVG Icons Repository below.')
|
||||
s1.anonymous = true
|
||||
s1.addremove = true
|
||||
s1.sortable = true
|
||||
s1.template = "cbi/tblsection"
|
||||
|
||||
local p1 = s1:option(Value, "pattern", i18n.translate("Regex Pattern"))
|
||||
p1.rmempty = false
|
||||
p1.placeholder = "/\\.google\\./i"
|
||||
|
||||
local i1_hint = i18n.translate("You can use an existing Iconify ID (e.g., logos:youtube) or a custom ID defined in the Custom SVG Icons Repository below.")
|
||||
local i1 = s1:option(Value, "icon", string.format('%s <span title="%s" data-tooltip="%s" style="cursor:help; border-bottom:1px dashed #666;">(?)</span>', i18n.translate("Iconify ID"), i1_hint, i1_hint))
|
||||
i1.rmempty = false
|
||||
i1.placeholder = "conntrack:google"
|
||||
|
||||
local s2 = m:section(TypedSection, "my_custom_icons", i18n.translate("Custom SVG Icons Repository"))
|
||||
s2.anonymous = true
|
||||
s2.addremove = true
|
||||
s2.template = "cbi/tblsection"
|
||||
|
||||
local v2 = s2:option(DummyValue, "_preview", i18n.translate("Icon Preview"))
|
||||
function v2.cfgvalue(self, section)
|
||||
local name = self.map:get(section, "name") or "mdi:server-network"
|
||||
return string.format('<span class="iconify-live-preview" data-icon="%s" style="font-size:24px;vertical-align:middle;"></span>', name)
|
||||
end
|
||||
v2.rawhtml = true
|
||||
|
||||
local n2_hint = i18n.translate("The Iconify ID must start with 'conntrack:' prefix to avoid overriding existing Iconify datasets.")
|
||||
local n2 = s2:option(Value, "name", string.format('%s <span title="%s" data-tooltip="%s" style="cursor:help; color: #e00; border-bottom:1px dashed #e00;">(?)</span>', i18n.translate("Iconify ID"), i18n.translate(n2_hint), i18n.translate(n2_hint)))
|
||||
n2.rmempty = false
|
||||
n2.placeholder = "conntrack:google"
|
||||
function n2.validate(self, value)
|
||||
if value and value ~= "" then
|
||||
if not value:match("^conntrack:") then
|
||||
return nil, i18n.translate("Validation Error: The Iconify ID must begin with 'conntrack:' prefix (e.g., conntrack:custom_name).")
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
local w2 = s2:option(Value, "width", i18n.translate("Icon Width"))
|
||||
w2.rmempty = false
|
||||
w2.datatype = "uinteger"
|
||||
w2.placeholder = "24"
|
||||
w2.default = "24"
|
||||
|
||||
local h2 = s2:option(Value, "height", i18n.translate("Icon Height"))
|
||||
h2.rmempty = false
|
||||
h2.datatype = "uinteger"
|
||||
h2.placeholder = "24"
|
||||
h2.default = "24"
|
||||
|
||||
local b2 = s2:option(TextValue, "svg_body", i18n.translate("SVG Body (Raw XML)"))
|
||||
b2.rmempty = false
|
||||
b2.rows = 3
|
||||
|
||||
local function base64_encode(data)
|
||||
local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
return ((data:gsub('.', function(x)
|
||||
local r, b_val = '', x:byte()
|
||||
for i = 8, 1, -1 do r = r .. (b_val % 2^i - b_val % 2^(i-1) >= 2^(i-1) and '1' or '0') end
|
||||
return r
|
||||
end) .. '0000'):gsub('%d%d%d%d%d%d', function(x)
|
||||
if (#x < 6) then return '' end
|
||||
local c = 0
|
||||
for i = 1, 6 do c = c + (x:sub(i, i) == '1' and 2^(6-i) or 0) end
|
||||
return b:sub(c + 1, c + 1)
|
||||
end) .. ({ '', '==', '=' })[#data % 3 + 1])
|
||||
end
|
||||
|
||||
local function base64_decode(data)
|
||||
local b = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
|
||||
data = string.gsub(data, '[^'..b..'=]', '')
|
||||
return (data:gsub('.', function(x)
|
||||
if (x == '=') then return '' end
|
||||
local r, f = '', (b:find(x) - 1)
|
||||
for i = 6, 1, -1 do r = r .. (f % 2^i - f % 2^(i-1) >= 2^(i-1) and '1' or '0') end
|
||||
return r;
|
||||
end):gsub('%d%d%d%d%d%d%d%d', function(x)
|
||||
local c = 0
|
||||
for i = 1, 8 do c = c + (x:sub(i, i) == '1' and 2^(8-i) or 0) end
|
||||
return string.char(c)
|
||||
end))
|
||||
end
|
||||
|
||||
function b2.cfgvalue(self, section)
|
||||
local value = Value.cfgvalue(self, section)
|
||||
if value and value ~= "" then
|
||||
local pcall_ok, decoded = pcall(nixio.bin.b64decode, value)
|
||||
if pcall_ok then
|
||||
return decoded
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
function b2.validate(self, value)
|
||||
if value and value ~= "" then
|
||||
if not value:match("^%s*<svg") then
|
||||
return nil, i18n.translate("Invalid SVG content. Must start with <svg> tag.")
|
||||
end
|
||||
return nixio.bin.b64encode(value)
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
return m
|
||||
|
||||
@@ -0,0 +1,933 @@
|
||||
<%+header%>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.3.2/css/flag-icons.min.css" />
|
||||
<script src="https://code.iconify.design/3/3.1.0/iconify.min.js"></script>
|
||||
<script src="/luci-static/conntrack/js/iconify_custom_icons.js"></script>
|
||||
<script type="text/javascript" src="<%=pcdata(luci.dispatcher.build_url('admin', 'network', 'conntrack', 'conntrack_custom_js'))%>"></script>
|
||||
|
||||
<style>
|
||||
span.iconify {
|
||||
display:inline-block;
|
||||
font-size:14px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
</style>
|
||||
<style type="text/css">
|
||||
.ct-flag-inline {
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.fixed-table {
|
||||
width: 100%;
|
||||
/* table-layout: fixed !important; */
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.text-ellipsis {
|
||||
white-space: nowrap !important;
|
||||
overflow: hidden !important;
|
||||
text-overflow: ellipsis !important;
|
||||
word-break: break-all;
|
||||
}
|
||||
.unit-toggle {
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
background: #f0f0f0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 3px;
|
||||
margin-left: 5px;
|
||||
font-size: 11px;
|
||||
color: #333;
|
||||
user-select: none;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.unit-toggle:hover {
|
||||
background: #e0e0e0;
|
||||
border-color: #999;
|
||||
}
|
||||
span.active, span.inactive { padding: 2px; }
|
||||
span.active { cursor: pointer; color: #000; margin-left: 5px; }
|
||||
span.inactive { cursor: pointer; color: #000; margin-left: 5px; }
|
||||
.table-responsive-wrapper {
|
||||
width: 100%;
|
||||
overflow-x: auto !important;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
th { padding-left: 5px !important; padding-right: 5px !important; }
|
||||
.conntrack-table th, .conntrack-table td:not(.ip-col-limit) {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
@media screen and (min-width: 992px) {
|
||||
.ip-col-limit {
|
||||
max-width: 220px !important;
|
||||
}
|
||||
.icon-col-limit {
|
||||
max-width: 160px !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width: 991px) {
|
||||
.ip-col-limit {
|
||||
max-width: 180px !important;
|
||||
}
|
||||
.icon-col-limit {
|
||||
max-width: 120px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="cbi-map">
|
||||
<h2>
|
||||
<ul class="cbi-tabmenu" style="border-bottom:0 !important;">
|
||||
<li class="cbi-tab"><a href="<%=luci.dispatcher.build_url('admin/network/conntrack')%>"><strong><%:ConnTrack%></strong></a></li>
|
||||
<li class="cbi-tab-disabled"><a href="<%=luci.dispatcher.build_url('admin/network/conntrack/conntrack_icons_config')%>"><%:Custom App Icons Config%></a></li>
|
||||
</ul>
|
||||
</h2>
|
||||
<div class="cbi-map-descr"><%:Realtime single-direction traffic speed flow based on Event-Stream (SSE).%><br /><%:To show AppName, you'll need to patch kmod-nf-conntrack, please refers to%><a href="https://github.com/simonchen/luci-app-conntrack" target="_blank"> Github luci-app-conntrack</a></div>
|
||||
|
||||
<fieldset class="cbi-section">
|
||||
<div class="cbi-section-node">
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="interval_select"><%:Interval (Seconds)%></label>
|
||||
<div class="cbi-value-field">
|
||||
<select class="cbi-input-select" id="interval_select">
|
||||
<option value="1000" selected>1.0s</option>
|
||||
<option value="1500">1.5s</option>
|
||||
<option value="2000">2.0s</option>
|
||||
<option value="2500">2.5s</option>
|
||||
<option value="3000">3.0s</option>
|
||||
<option value="3500">3.5s</option>
|
||||
<option value="4000">4.0s</option>
|
||||
<option value="4500">4.5s</option>
|
||||
<option value="5000">5.0s</option>
|
||||
<option value="5500">5.5s</option>
|
||||
<option value="6000">6.0s</option>
|
||||
<option value="6500">6.5s</option>
|
||||
<option value="7000">7.0s</option>
|
||||
<option value="7500">7.5s</option>
|
||||
<option value="8000">8.0s</option>
|
||||
<option value="8500">8.5s</option>
|
||||
<option value="9000">9.0s</option>
|
||||
<option value="9500">9.5s</option>
|
||||
<option value="10000">10.0s</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-value">
|
||||
<label class="cbi-value-title" for="rows_select"><%:Display Rows%></label>
|
||||
<div class="cbi-value-field">
|
||||
<select class="cbi-input-select" id="rows_select">
|
||||
<option value="10">10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="30">30</option>
|
||||
<option value="40">40</option>
|
||||
<option value="50" selected>50</option>
|
||||
<option value="60">60</option>
|
||||
<option value="70">70</option>
|
||||
<option value="80">80</option>
|
||||
<option value="90">90</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cbi-map">
|
||||
<div class="cbi-value" style="display: flex; justify-content: space-between; align-items: center; width: 100%; height: 2.5em; box-sizing: border-box;">
|
||||
<div style="display: flex; align-items: center; height: 100%;">
|
||||
<label class="cbi-value-title" style="margin: 0; padding: 0; display: inline-block; vertical-align: middle;"><%:Actual Delta Time%></label>
|
||||
<div class="cbi-value-field" id="delta_label" style="font-weight: bold; color: #0071c5; padding-left: 10px; display: inline-block; vertical-align: middle;">0.0000s</div>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center; height: 100%; padding-right: 15px;">
|
||||
<label style="font-weight: bold; cursor: pointer; user-select: none; display: flex; align-items: center; margin: 0; height: 100%;">
|
||||
<input type="checkbox" checked id="filter_local_checkbox" style="margin: 0 6px 0 0; padding: 0; width: 14px; height: 14px; display: inline-block; vertical-align: middle;" />
|
||||
<span style="display: inline-block; vertical-align: middle; line-height: 14px;"><%:Filter Local/Loopback%></span>
|
||||
</label>
|
||||
<label style="font-weight: bold; cursor: pointer; user-select: none; display: flex; align-items: center; margin:0 15px; height: 100%;">
|
||||
<input type="checkbox" id="show_appname_checkbox" style="margin: 0 6px 0 0; padding: 0; width: 14px; height: 14px; display:
|
||||
<span style="display: inline-block; vertical-align: middle; line-height: 14px;"><%:Show AppName (experiment)%></span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="cbi-section">
|
||||
<table class="table cbi-section-table fixed-table" id="conntrack_table" style="width:100%; text-align:left;">
|
||||
<tr class="cbi-section-table-titles">
|
||||
<th class="cbi-section-table-cell" style="width:8%;"><%:L3%></th>
|
||||
<th class="cbi-section-table-cell" style="width:10%;"><%:Protocol%></th>
|
||||
<th class="cbi-section-table-cell" style="width:17%;"><%:State (Direction)%></th>
|
||||
<th class="cbi-section-table-cell text-ellipsis ip-col-limit" style="width:23%;">
|
||||
<select id="filter_src_select" style="width:120px; padding:2px; font-weight:bold; color:#333; border:1px solid #ccc; border-radius:3px;">
|
||||
<option value="all" selected><%:Source IP:Port%></option>
|
||||
</select>
|
||||
</th>
|
||||
<th class="cbi-section-table-cell text-ellipsis ip-col-limit" style="width:23%;">
|
||||
<select id="filter_dst_select" style="width:120px; padding:2px; font-weight:bold; color:#333; border:1px solid #ccc; border-radius:3px;">
|
||||
<option value="all" selected><%:Destination IP:Port%></option>
|
||||
</select>
|
||||
</th>
|
||||
<th class="cbi-section-table-cell" style="width:11%;"><%:Accumulated%><span id="arrow-flow" class="active">↓</span></th>
|
||||
<th class="cbi-section-table-cell" style="width:11%;">
|
||||
<%:Current Speed%><span id="arrow-rate" class="inactive">⇅</span><span id="toggle_btn" class="unit-toggle">Bit</span>
|
||||
</th>
|
||||
<th id="th_appname" class="cbi-section-table-cell text-ellipsis icon-col-limit" style="width:11%;"><%:AppName%></th>
|
||||
</tr>
|
||||
<tr class="cbi-section-table-row"><td colspan="7" style="text-align:center;" id="loading_msg"><%:Connecting to Event-Stream...%></td></tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var eventSource = null;
|
||||
var currentUnit = 'Bit';
|
||||
var lastCacheData = null;
|
||||
var localIsSrcFocus = false;
|
||||
var localIsDstFocus = false;
|
||||
var isSrcOpen = false;
|
||||
var isDstOpen = false;
|
||||
var globalHostMap = {};
|
||||
|
||||
var serverCache = {}; // Temp used to capture server names
|
||||
var geoIpCache = {};
|
||||
var geoIpQueue = [];
|
||||
var isGeoIpProcessing = false;
|
||||
var queuedIpMap = {};
|
||||
|
||||
const appMapper = {
|
||||
"0102b75000000001": "<%:Honor of Kings%>",
|
||||
|
||||
isDomainHonorOfKings: function(d) {
|
||||
if (!d) return -1;
|
||||
if (!d.match(/\.qq\.com/i) && !d.match(/\.tencent\.com/i)) return -1;
|
||||
if (d.match(/smoba\.qq\.com/i) || d.match(/sgame/i) || d.match(/tgpa\.qq\.com/i) || d.match(/kohesport\.qq\.com/i) || d.match(/tga\.qq\.com/i)) return 1;
|
||||
if (d.match(/crashsight\.qq\.com/i) || d.match(/perfsight\.qq\.com/i) || d.match(/gcloud\.qq\.com/i) || d.match(/msdk\.qq\.com/i) || d.match(/pandora\.qq\.com/i) || d.match(/game\.qq\.com/i) || d.match(/dlied\d*\.qq\.com/i)) return 1;
|
||||
return 0;
|
||||
},
|
||||
|
||||
iconifyIcon: function(org) {
|
||||
if (!org) return "";
|
||||
|
||||
if (window._customIconRules && window._customIconRules.length > 0) {
|
||||
for (var i = 0; i < window._customIconRules.length; i++) {
|
||||
var rule = window._customIconRules[i];
|
||||
if (org.match(rule.reg)) {
|
||||
return rule.icon;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (org.match(/<%:PCDN Stream Media%>/i))
|
||||
return 'glyphs-poly:stream';
|
||||
if (org.match(/<%:Honor of Kings%>/i))
|
||||
return 'logos:honorofkings';
|
||||
if (org.match(/telegram/i))
|
||||
return 'logos:telegram';
|
||||
if (org.match(/youtube/i) || org.match(/\.ggpht\./i) || org.match(/<%:YouTube%>/i))
|
||||
return 'logos:youtube';
|
||||
if (org.match(/azure/i))
|
||||
return 'logos:azure';
|
||||
if (org.match(/gmail/i))
|
||||
return 'selfhst:gmail';
|
||||
if (org.match(/google/i) || org.match(/gstatic/i))
|
||||
return 'logos:google';
|
||||
if (org.match(/github/i))
|
||||
return 'logos:github';
|
||||
if (org.match(/douyin/i) || org.match(/\.amemv\.com/i) || org.match(/<%:TikTok%>/i))
|
||||
return 'logos:tiktok';
|
||||
if (org.match(/<%:Instagram%>/i))
|
||||
return 'logos:instagram';
|
||||
if (org.match(/facebook/i))
|
||||
return 'logos:facebook';
|
||||
if (org.match(/<%:X (Twitter)%>/i) || org.match(/^x\.com/i) || org.match(/\.x\./i) || org.match(/\.twimg\./i))
|
||||
return 'logos:twittr';
|
||||
if (org.match(/<%:Netflix%>/i))
|
||||
return 'logos:netflix-icon';
|
||||
if (org.match(/cloudflare/i))
|
||||
return 'logos:cloudflare';
|
||||
if (org.match(/aws/i))
|
||||
return 'logos:aws';
|
||||
if (org.match(/<%:Bilibili%>/i) || org.match(/bili/i))
|
||||
return 'ant-design:bilibili-filled';
|
||||
if (org.match(/<%:Bilibili Video%>/i))
|
||||
return 'thesvg-color:bilibili';
|
||||
if (org.match(/miui/i) || org.match(/xiaomi/i))
|
||||
return 'thesvg-color:xiaomi';
|
||||
if (org.match(/weixin/i))
|
||||
return 'fa7-brands:weixin';
|
||||
if (org.match(/v.*?\.qq\.com/i))
|
||||
return 'logos:tencent-video';
|
||||
if (org.match(/tencent/i) || org.match(/\.qq\.com/i) || org.match(/\.tencent\.com/i))
|
||||
return 'thesvg-color:tencent';
|
||||
if (org.match(/china unicom/i) || org.match(/unicom/i))
|
||||
return 'arcticons:china-unicom';
|
||||
if (org.match(/china telecom/i) || org.match(/telecom/i) || org.match(/chinanet/i))
|
||||
return 'arcticons:china-telecom';
|
||||
if (org.match(/china mobile/i))
|
||||
return 'arcticons:china-mobile';
|
||||
if (org.match(/alibaba/i))
|
||||
return 'selfhst:alibaba';
|
||||
if (org.match(/alipay/i))
|
||||
return 'thesvg-color:alipay';
|
||||
if (org.match(/aliyun/i))
|
||||
return 'thesvg-color:alibaba-cloud';
|
||||
if (org.match(/baidu/i))
|
||||
return 'selfhst:baidu';
|
||||
if (org.match(/\.iqiyi\./i) || org.match(/\.qiyi\./i) || org.match(/\.qy\./i))
|
||||
return 'logos:iqiyi';
|
||||
if (org.match(/Skype/i) || org.match(/Microsoft Teams/i))
|
||||
return 'thesvg-color:microsoft-teams';
|
||||
if (org.match(/outlook/i) || org.match(/live\.com/i))
|
||||
return 'thesvg-color:microsoft-outlook';
|
||||
if (org.match(/microsoft/i) || org.match(/\.windows\./i) || org.match(/\.office\./i) || org.match(/\.msn\./i))
|
||||
return 'arcticons:microsoft';
|
||||
if (org.match(/^bing\./i) || org.match(/\.bing\./i))
|
||||
return 'logos:bing';
|
||||
if (org.match(/\.intel\./i))
|
||||
return 'thesvg-color:intel';
|
||||
if (org.match(/nvidia/i))
|
||||
return 'logos:nvidia';
|
||||
if (org.match(/amd/i))
|
||||
return 'thesvg-color:amd';
|
||||
if (org.match(/amazon/i))
|
||||
return 'selfhst:amazon';
|
||||
if (org.match(/linkedin/i))
|
||||
return 'logos:linkedin-icon';
|
||||
|
||||
return "";
|
||||
},
|
||||
|
||||
renderIcon: function(icon, appName, as) {
|
||||
var as_code = "";
|
||||
if (as) {
|
||||
var m = as.match(/(AS\d+)/ig);
|
||||
if (m) as_code = m[0];
|
||||
}
|
||||
function as_link(as_code, content) {
|
||||
if (!as_code) return content;
|
||||
return `<a href="https://ipinfo.io/${as_code}" target="_blank" style="color:#666;cursor:pointer;">${content}</a>`;
|
||||
}
|
||||
if (!icon) return as_link(as_code, appName);
|
||||
|
||||
if (typeof(Iconify) != 'undefined') {
|
||||
if (!Iconify.iconExists(icon)) {
|
||||
Iconify.loadIcon(icon);
|
||||
}
|
||||
var cp = {width:40, height:40};
|
||||
var icon_data = Iconify.getIcon(icon);
|
||||
if (icon_data) {
|
||||
if (icon_data.width/icon_data.height >= 2 ||
|
||||
icon_data.height/icon_data.width >= 2) {
|
||||
cp = {};
|
||||
}
|
||||
}
|
||||
var html = Iconify.renderHTML(icon, cp) || "";
|
||||
if (html) return as_link(as_code, html);
|
||||
}
|
||||
|
||||
return as_link(as_code, appName);
|
||||
},
|
||||
|
||||
getAppName: function(payload, geoInfo, proto, bytes) {
|
||||
var isPayloadHexStr = isPureHexString(payload);
|
||||
if (!isPayloadHexStr) { // domain - plain text
|
||||
serverCache[payload] = true;
|
||||
|
||||
if (payload.match(/googlevideo/i))
|
||||
return "<%:YouTube%>";
|
||||
if (payload.match(/bilivideo/i))
|
||||
return "<%:Bilibili Video%>";
|
||||
if (payload.match(/gmail/i))
|
||||
return "<%:Gmail%>";
|
||||
if (payload.match(/skype/i))
|
||||
return "<%:Skype%>";
|
||||
if (payload.match(/teams\.microsoft/i) || payload.match(/teams\.live\./i))
|
||||
return "<%:Microsoft Teams%>";
|
||||
if (this.isDomainHonorOfKings(payload) == 1)
|
||||
return "<%:Honor of Kings%>";
|
||||
|
||||
if (payload)
|
||||
return payload; // (i.e, the exact server name)
|
||||
}
|
||||
|
||||
const prefix = payload.substring(0, 6);
|
||||
const org = geoInfo && geoInfo.as ? geoInfo.as.toLowerCase() : "";
|
||||
org_as = geoInfo ? (geoInfo.as || geoInfo.org) : ""; org_as = org_as.replace(/^AS\d+\s+/ig, '');
|
||||
|
||||
// Intercept standard TLS (160301 / 160303) handshakes and match via ASN
|
||||
if (payload.charAt(0) === 'c' && payload.substring(2, 6) === '0000') { // UDP(QUIC) feature with payload
|
||||
if (org.includes("google") || org.includes("youtube")) {
|
||||
return "<%:YouTube%>";
|
||||
}
|
||||
if (org.includes("akamai") || org.includes('fastly') || org.includes("bytedance") || org.includes("tiktok")) {
|
||||
return "<%:TikTok%>";
|
||||
}
|
||||
}
|
||||
|
||||
if (prefix === "160301" || prefix === "160303") { // TCP HTTPS feature (443 port)
|
||||
if (org.includes("google") || org.includes("youtube")) {
|
||||
if (bytes && bytes >= 4194304) { // at least 4MB to ensure..
|
||||
return "<%:YouTube%>";
|
||||
}
|
||||
return "<%:Google Service%>";
|
||||
}
|
||||
if (org.includes("akamai") || org.includes('fastly') || org.includes("bytedance") || org.includes("tiktok")) {
|
||||
if (bytes && bytes >= 2097152) { // at least 2MB to ensure..
|
||||
return "<%:TikTok%>";
|
||||
}
|
||||
}
|
||||
if (org.includes("unicom") || org.includes("telecom") || org.includes("chinanet") || org.includes("cmnet")) {
|
||||
if (bytes && bytes >= 4194304) { // at least 4MB to ensure..
|
||||
return "<%:Bilibili%>";
|
||||
}
|
||||
}
|
||||
if (org.includes("tencent") || org.includes("qq")) return "<%:Tencent%>";
|
||||
if (org.includes("bilibili")) return "<%:Bilibili%>";
|
||||
if (org.includes("netflix")) return "<%:Netflix%>";
|
||||
if (org.includes("twitter") || org.includes("x corp")) return "<%:X (Twitter)%>";
|
||||
if (org.includes("instagram") || org.includes("facebook") || org.includes("meta")) return "<%:Instagram%>";
|
||||
|
||||
return org_as ? org_as : "<%:HTTPS Connection%>";
|
||||
} else { // Non HTTPS connection
|
||||
if (org.includes("google") || org.includes("youtube")) {
|
||||
if (bytes && bytes >= 4194304) { // at least 4MB to ensure..
|
||||
return "<%:YouTube%>";
|
||||
}
|
||||
}
|
||||
// General steram media? (Current payload equals to previous cache's ?)
|
||||
if (payload && (payload == geoInfo.payload) && proto && proto.toLowerCase() == 'udp' && bytes > 1000) {
|
||||
return "<%:PCDN Stream Media%>";
|
||||
}
|
||||
}
|
||||
|
||||
return org_as ? org_as : "";
|
||||
}
|
||||
};
|
||||
|
||||
function isPureHexString(payloadStr) {
|
||||
if (!payloadStr) return false;
|
||||
return /^[0-9a-fA-F]+$/.test(payloadStr);
|
||||
}
|
||||
|
||||
function isLocalIp(ip) {
|
||||
if (!ip) return true;
|
||||
let clean = ip.toLowerCase().replace(/[\s\[\]]/g, '');
|
||||
if (clean === '127.0.0.1' || clean === '0.0.0.0' || clean === '::1' || clean === '::') return true;
|
||||
if (clean.replace(/0/g, '') === ':::::::1' || clean.replace(/0/g, '') === '::::::::') return true;
|
||||
if (/^(192\.168\.|10\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/.test(clean)) return true;
|
||||
if (/^f[cd]/.test(clean)) return true;
|
||||
if (/^fe[89ab]/.test(clean)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function fastNormalizeIP(ip) {
|
||||
if (!ip) return '';
|
||||
let clean = ip.toLowerCase().replace(/[\s\[\]]/g, '');
|
||||
if (clean.includes('.') && !clean.includes(':')) {
|
||||
return ip.trim();
|
||||
}
|
||||
if (clean.startsWith('::ffff:') && clean.includes('.')) {
|
||||
return clean.substring(7);
|
||||
}
|
||||
let parts = [];
|
||||
if (clean.includes('::')) {
|
||||
const sides = clean.split('::');
|
||||
const left = sides[0] ? sides[0].split(':') : [];
|
||||
const right = sides[1] ? sides[1].split(':') : [];
|
||||
const missingCount = 8 - (left.length + right.length);
|
||||
parts = [...left, ...Array(missingCount).fill('0000'), ...right];
|
||||
} else {
|
||||
parts = clean.split(':');
|
||||
}
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
let segment = parts[i] || '0000';
|
||||
if (segment.length < 4) {
|
||||
segment = segment.padStart(4, '0');
|
||||
}
|
||||
parts[i] = segment;
|
||||
}
|
||||
while (parts.length < 8) {
|
||||
parts.push('0000');
|
||||
}
|
||||
return parts.slice(0, 8).join(':');
|
||||
}
|
||||
|
||||
var geoIpTimer = null;
|
||||
function fetchGeoIp(ip, srcId, dstId, payload) {
|
||||
if (isLocalIp(ip)) return;
|
||||
if (geoIpCache[ip]) {
|
||||
applyGeoIpToDom(ip, srcId, dstId);
|
||||
return;
|
||||
}
|
||||
if (!queuedIpMap[ip]) queuedIpMap[ip] = [];
|
||||
queuedIpMap[ip].push({ srcId: srcId, dstId: dstId, payload: payload });
|
||||
if (geoIpQueue.indexOf(ip) === -1) geoIpQueue.push(ip);
|
||||
|
||||
if (geoIpTimer) clearTimeout(geoIpTimer);
|
||||
|
||||
geoIpTimer = setTimeout(function() {
|
||||
if (!isGeoIpProcessing) {
|
||||
isGeoIpProcessing = true;
|
||||
processGeoIpBatch();
|
||||
}
|
||||
}, 150);
|
||||
}
|
||||
|
||||
function processGeoIpBatch() {
|
||||
if (geoIpQueue.length === 0) {
|
||||
isGeoIpProcessing = false;
|
||||
return;
|
||||
}
|
||||
var batchIps = [];
|
||||
while (geoIpQueue.length > 0 && batchIps.length < 100) {
|
||||
var ip = geoIpQueue.shift();
|
||||
if (geoIpCache[ip]) {
|
||||
var targets = queuedIpMap[ip] || [];
|
||||
targets.forEach(function(t) { applyGeoIpToDom(ip, t.srcId, t.dstId); });
|
||||
delete queuedIpMap[ip];
|
||||
} else {
|
||||
batchIps.push(ip);
|
||||
}
|
||||
}
|
||||
if (batchIps.length === 0) {
|
||||
setTimeout(processGeoIpBatch, 10);
|
||||
return;
|
||||
}
|
||||
fetch('http://ip-api.com/batch?lang=<%= (luci.util.exec("uci -q get luci.main.lang"):gsub("%s+", ""):find("zh") and "zh-CN" or "en") %>', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(batchIps)
|
||||
})
|
||||
.then(function(res) {
|
||||
if (res.status === 429) {
|
||||
batchIps.forEach(function(ip) { geoIpQueue.push(ip); });
|
||||
setTimeout(processGeoIpBatch, 5000);
|
||||
return null;
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(function(results) {
|
||||
if (!results) return;
|
||||
results.forEach(function(json) {
|
||||
if (json && json.status === 'success') {
|
||||
var queryIp = fastNormalizeIP(json.query);
|
||||
var region = json.country + ' ' + (json.regionName || '') + ' ' + (json.city || '') + ' (' + (json.isp || '') + ')';
|
||||
var flagHtml = '';
|
||||
if (json.countryCode && json.countryCode.length === 2) {
|
||||
var code = json.countryCode.toLowerCase();
|
||||
flagHtml = '<span class="fi fi-' + code + ' ct-flag-inline"></span>';
|
||||
}
|
||||
var appName = "", appIcon = "", payload = "", bytes = "";
|
||||
if (queuedIpMap[queryIp]) {
|
||||
var arr = queuedIpMap[queryIp][0].payload.split('|');
|
||||
payload = arr[0], proto = arr[1], bytes = arr[2];
|
||||
if (payload) {
|
||||
appName = appMapper[payload];
|
||||
if (!appName) appName = appMapper.getAppName(payload, json, proto, bytes);
|
||||
appIcon = appMapper.iconifyIcon(appName);
|
||||
//if (appName && !(appName in appMapper)) appMapper[payload] = appName;
|
||||
var payload_elems = document.querySelectorAll(`[payload="{payload}"]`);
|
||||
payload_elems.forEach(function(elem) { elem.innerHTML = appMapper.renderIcon(appIcon, appName, json.as); elem.title = appName; });
|
||||
}
|
||||
}
|
||||
geoIpCache[queryIp] = { org: json.org, as: json.as, region: region, flagHtml: flagHtml, appName: appName, appIcon: appIcon, payload: payload };
|
||||
var targets = queuedIpMap[queryIp] || [];
|
||||
targets.forEach(function(t) { applyGeoIpToDom(queryIp, t.srcId, t.dstId); });
|
||||
delete queuedIpMap[queryIp];
|
||||
}
|
||||
if (json && json.status == 'fail') {
|
||||
var queryIp = fastNormalizeIP(json.query);
|
||||
delete queuedIpMap[queryIp];
|
||||
}
|
||||
});
|
||||
setTimeout(processGeoIpBatch, 2000);
|
||||
})
|
||||
.catch(function(err) {
|
||||
console.error("GeoIP Batch failed:", err);
|
||||
batchIps.forEach(function(ip) { geoIpQueue.push(ip); });
|
||||
setTimeout(processGeoIpBatch, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function applyGeoIpToDom(ip, srcId, dstId) {
|
||||
var cached = geoIpCache[ip];
|
||||
if (!cached) return;
|
||||
var elSrc = document.getElementById(srcId);
|
||||
var elDst = document.getElementById(dstId);
|
||||
if (elSrc && elSrc.getAttribute('data-ip') === ip && !elSrc.getAttribute('data-geo-loaded')) {
|
||||
elSrc.title = elSrc.title + '\nGeo: ' + cached.region;
|
||||
if (cached.flagHtml || cached.region) {
|
||||
var infoStr = cached.flagHtml ? cached.flagHtml + ' ' + cached.region : cached.region;
|
||||
elSrc.innerHTML = elSrc.innerHTML + '<br /><span style="font-size:11px; color:#666; font-weight:normal;">' + infoStr + '</span>';
|
||||
}
|
||||
elSrc.setAttribute('data-geo-loaded', 'true');
|
||||
}
|
||||
if (elDst && elDst.getAttribute('data-ip') === ip && !elDst.getAttribute('data-geo-loaded')) {
|
||||
elDst.title = elDst.title + '\nGeo: ' + cached.region;
|
||||
if (cached.flagHtml || cached.region) {
|
||||
var infoStr = cached.flagHtml ? cached.flagHtml + ' ' + cached.region : cached.region;
|
||||
elDst.innerHTML = elDst.innerHTML + '<br /><span style="font-size:11px; color:#666; font-weight:normal;">' + infoStr + '</span>';
|
||||
}
|
||||
elDst.setAttribute('data-geo-loaded', 'true');
|
||||
}
|
||||
}
|
||||
|
||||
function formatSpeed(bytesPerSec) {
|
||||
if (currentUnit === 'Bit') {
|
||||
var bitsPerSec = bytesPerSec * 8;
|
||||
if (bitsPerSec >= 1000 * 1000 * 1000) return (bitsPerSec / 1000 / 1000 / 1000).toFixed(2) + ' Gbps';
|
||||
if (bitsPerSec >= 1000 * 1000) return (bitsPerSec / 1000 / 1000).toFixed(2) + ' Mbps';
|
||||
if (bitsPerSec >= 1000) return (bitsPerSec / 1000).toFixed(2) + ' Kbps';
|
||||
return bitsPerSec + ' bps';
|
||||
} else {
|
||||
if (bytesPerSec >= 1024 * 1024) return (bytesPerSec / 1024 / 1024).toFixed(2) + ' MB/s';
|
||||
if (bytesPerSec >= 1024) return (bytesPerSec / 1024).toFixed(2) + ' KB/s';
|
||||
return bytesPerSec + ' B/s';
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (bytes >= 1024 * 1024 * 1024) return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
||||
if (bytes >= 1024 * 1024) return (bytes / 1024 / 1024).toFixed(2) + ' MB';
|
||||
return (bytes / 1024).toFixed(2) + ' KB';
|
||||
}
|
||||
|
||||
function updateFilterOptions(selectId, ipList, currentValue) {
|
||||
var select = document.getElementById(selectId);
|
||||
var defaultText = select.options[0] ? select.options[0].text : (selectId === 'filter_src_select' ? '<%:Source IP:Port%>' : '<%:Destination IP:Port%>');
|
||||
select.options.length = 0;
|
||||
var defaultOpt = document.createElement('option');
|
||||
defaultOpt.value = 'all'; defaultOpt.text = defaultText;
|
||||
if (currentValue === 'all') defaultOpt.selected = true;
|
||||
select.appendChild(defaultOpt);
|
||||
ipList.forEach(function(ip) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = ip;
|
||||
var host = globalHostMap[ip];
|
||||
opt.text = host ? ip + ' (' + host + ')' : ip;
|
||||
if (ip === currentValue) opt.selected = true;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function renderTable(data, showAppName) {
|
||||
var table = document.getElementById('conntrack_table');
|
||||
while (table.rows.length > 1) {
|
||||
table.deleteRow(1);
|
||||
}
|
||||
|
||||
if (data.connections.length === 0) {
|
||||
var row = table.insertRow(-1);
|
||||
row.className = 'cbi-section-table-row';
|
||||
var cell = row.insertCell(0);
|
||||
cell.colSpan = 7;
|
||||
cell.style.textAlign = 'center';
|
||||
cell.innerHTML = '<%:No active traffic speed detected...%>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.connections.forEach(function(conn) {
|
||||
var row = table.insertRow(-1);
|
||||
row.className = 'cbi-section-table-row';
|
||||
|
||||
if (conn.payload && !isPureHexString(conn.payload)) {
|
||||
if (!(conn.src in globalHostMap)) {
|
||||
globalHostMap[conn.src] = conn.payload;
|
||||
}
|
||||
if (!(conn.dst in globalHostMap)) {
|
||||
globalHostMap[conn.dst] = conn.payload;
|
||||
}
|
||||
}
|
||||
|
||||
var src_host = globalHostMap[conn.src];
|
||||
var dst_host = globalHostMap[conn.dst];
|
||||
var src_display = src_host ? src_host + ':' + conn.sport : conn.src + ':' + conn.sport;
|
||||
var dst_display = dst_host ? dst_host + ':' + conn.dport : conn.dst + ':' + conn.dport;
|
||||
var src_title = conn.src + ':' + conn.sport + (src_host ? ' (' + src_host + ')' : '');
|
||||
var dst_title = conn.dst + ':' + conn.dport + (dst_host ? ' (' + dst_host + ')' : '');
|
||||
|
||||
var speedStr = formatSpeed(conn.speed);
|
||||
|
||||
var c0 = row.insertCell(0); c0.className = 'text-ellipsis'; c0.innerHTML = conn.l3.toUpperCase(); c0.title = conn.l3.toUpperCase();
|
||||
var c1 = row.insertCell(1); c1.className = 'text-ellipsis'; c1.innerHTML = '<strong>' + conn.proto.toUpperCase() + '</strong>'; c1.title = conn.proto.toUpperCase();
|
||||
var c2 = row.insertCell(2); c2.className = 'text-ellipsis'; c2.innerHTML = conn.state; c2.title = conn.state;
|
||||
|
||||
var c3 = row.insertCell(3); c3.className = 'text-ellipsis ip-col-limit'; c3.innerHTML = src_display; c3.title = src_title;
|
||||
var c4 = row.insertCell(4); c4.className = 'text-ellipsis ip-col-limit'; c4.innerHTML = dst_display; c4.title = dst_title;
|
||||
|
||||
var randId = Math.random().toString(36).substr(2, 9);
|
||||
var src_dom_id = 'src_' + randId;
|
||||
var dst_dom_id = 'dst_' + randId;
|
||||
c3.id = src_dom_id; c3.setAttribute('data-ip', conn.src);
|
||||
c4.id = dst_dom_id; c4.setAttribute('data-ip', conn.dst);
|
||||
|
||||
if (geoIpCache[conn.src]) { applyGeoIpToDom(conn.src, src_dom_id, dst_dom_id); }
|
||||
if (geoIpCache[conn.dst]) { applyGeoIpToDom(conn.dst, src_dom_id, dst_dom_id); }
|
||||
|
||||
if (!geoIpCache[conn.src]) { fetchGeoIp(conn.src, src_dom_id, dst_dom_id, conn.payload+'|'+conn.proto+'|'+conn.bytes); }
|
||||
if (!geoIpCache[conn.dst]) { fetchGeoIp(conn.dst, src_dom_id, dst_dom_id, conn.payload+'|'+conn.proto+'|'+conn.bytes); }
|
||||
|
||||
var c5 = row.insertCell(5); c5.className = 'text-ellipsis'; c5.innerHTML = formatBytes(conn.bytes); c5.title = formatBytes(conn.bytes);
|
||||
var c6 = row.insertCell(6); c6.className = 'text-ellipsis'; c6.innerHTML = '<span style="color:#468847; font-weight:bold;">' + speedStr + '</span>'; c6.title = speedStr;
|
||||
|
||||
var c7 = row.insertCell(7); c7.className = 'text-ellipsis icon-col-limit'; c7.style.display = showAppName ? '' : 'none';
|
||||
var payload = conn.payload, appName = "", as = "";
|
||||
if (payload) {
|
||||
c7.setAttribute("payload", payload);
|
||||
if (appMapper[payload]) {
|
||||
appName = appMapper[payload];
|
||||
}
|
||||
}
|
||||
if (!appName) {
|
||||
if (geoIpCache[conn.src]){ // Revise appName by conn.bytes
|
||||
appName = appMapper.getAppName(payload, geoIpCache[conn.src], conn.proto, conn.bytes);
|
||||
if (appName) geoIpCache[conn.src].appName = appName;
|
||||
as = geoIpCache[conn.src].as;
|
||||
} else if (geoIpCache[conn.dst]){ // Revise appName by conn.bytes
|
||||
appName = appMapper.getAppName(payload, geoIpCache[conn.dst], conn.proto, conn.bytes);
|
||||
if (appName) geoIpCache[conn.dst].appName = appName;
|
||||
as = geoIpCache[conn.dst].as;
|
||||
}
|
||||
}
|
||||
c7.innerHTML = appMapper.renderIcon(appMapper.iconifyIcon(appName), appName, as);
|
||||
c7.title = appName;
|
||||
});
|
||||
}
|
||||
|
||||
var streamRestartTimer = null;
|
||||
function startStream() {
|
||||
if (eventSource) {
|
||||
eventSource.close();
|
||||
}
|
||||
|
||||
if (streamRestartTimer) { clearTimeout(streamRestartTimer); streamRestartTimer = null; }
|
||||
|
||||
var interval = document.getElementById('interval_select').value;
|
||||
var rows = document.getElementById('rows_select').value;
|
||||
var filterSrc = document.getElementById('filter_src_select').value;
|
||||
var filterDst = document.getElementById('filter_dst_select').value;
|
||||
var filterLocal = document.getElementById('filter_local_checkbox').checked ? "true" : "false";
|
||||
var sort = isRateSorted ? "rate" : (isFlowSorted ? "flow" : "rate");
|
||||
|
||||
var url = '<%=luci.dispatcher.build_url("admin", "network", "conntrack_stream")%>' +
|
||||
'?interval=' + interval + '&rows=' + rows + '&filter_src=' + filterSrc + '&filter_dst=' + filterDst + '&filter_local=' + filterLocal + "&sort=" + sort;
|
||||
|
||||
eventSource = new EventSource(url);
|
||||
|
||||
eventSource.onmessage = function(event) {
|
||||
var showAppName = document.getElementById('show_appname_checkbox').checked ? true : false;
|
||||
document.getElementById('th_appname').style.display = showAppName ? '' : 'none';
|
||||
|
||||
if (!streamRestartTimer) {
|
||||
streamRestartTimer = setTimeout(function() {
|
||||
streamRestartTimer = null;
|
||||
startStream();
|
||||
}, 1800000);
|
||||
}
|
||||
|
||||
var loading = document.getElementById('loading_msg');
|
||||
if (loading) loading.style.display = 'none';
|
||||
|
||||
var data = JSON.parse(event.data);
|
||||
if (data.host_map) { globalHostMap = data.host_map; }
|
||||
document.getElementById('delta_label').innerHTML = data.delta + 's';
|
||||
lastCacheData = data;
|
||||
|
||||
if (!localIsSrcFocus && !localIsDstFocus) {
|
||||
var currentSrc = document.getElementById('filter_src_select').value;
|
||||
var currentDst = document.getElementById('filter_dst_select').value;
|
||||
renderTable(data, showAppName);
|
||||
updateFilterOptions('filter_src_select', data.unique_ips, currentSrc);
|
||||
updateFilterOptions('filter_dst_select', data.unique_ips, currentDst);
|
||||
}
|
||||
};
|
||||
|
||||
eventSource.onerror = function(err) {
|
||||
console.error("EventSource failed:", err);
|
||||
var loading = document.getElementById('loading_msg');
|
||||
if (loading) {
|
||||
loading.style.display = 'table-cell';
|
||||
loading.innerHTML = '<span style="color:red;"><%:Stream connection lost, retrying...%></span>';
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let isFlowSorted = true;
|
||||
let isRateSorted = false;
|
||||
|
||||
function updateArrows() {
|
||||
const f = document.getElementById('arrow-flow');
|
||||
const r = document.getElementById('arrow-rate');
|
||||
|
||||
f.innerHTML = isFlowSorted ? '↓' : '⇅';
|
||||
f.className = isFlowSorted ? 'active' : 'inactive';
|
||||
|
||||
r.innerHTML = isRateSorted ? '↓' : '⇅';
|
||||
r.className = isRateSorted ? 'active' : 'inactive';
|
||||
}
|
||||
|
||||
document.getElementById('arrow-flow').addEventListener('click', function() {
|
||||
isRateSorted = false;
|
||||
isFlowSorted = true;
|
||||
updateArrows();
|
||||
startStream();
|
||||
});
|
||||
document.getElementById('arrow-rate').addEventListener('click', function() {
|
||||
isRateSorted = true;
|
||||
isFlowSorted = false;
|
||||
updateArrows();
|
||||
startStream();
|
||||
});
|
||||
|
||||
document.getElementById('toggle_btn').addEventListener('click', function() {
|
||||
if (currentUnit === 'Bit') {
|
||||
currentUnit = 'B';
|
||||
this.innerHTML = 'B';
|
||||
} else {
|
||||
currentUnit = 'Bit';
|
||||
this.innerHTML = 'Bit';
|
||||
}
|
||||
if (lastCacheData) {
|
||||
renderTable(lastCacheData);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('interval_select').addEventListener('change', startStream);
|
||||
document.getElementById('rows_select').addEventListener('change', startStream);
|
||||
|
||||
document.getElementById('filter_src_select').addEventListener('change', function() {
|
||||
isSrcOpen = false;
|
||||
this.blur();
|
||||
localIsSrcFocus = false;
|
||||
startStream();
|
||||
});
|
||||
|
||||
document.getElementById('filter_dst_select').addEventListener('change', function() {
|
||||
isDstOpen = false;
|
||||
this.blur();
|
||||
localIsDstFocus = false;
|
||||
startStream();
|
||||
});
|
||||
|
||||
document.getElementById('filter_src_select').addEventListener('click', function() {
|
||||
isSrcOpen = !isSrcOpen;
|
||||
if (!isSrcOpen) { this.blur(); }
|
||||
});
|
||||
|
||||
document.getElementById('filter_src_select').addEventListener('blur', function() {
|
||||
isSrcOpen = false;
|
||||
});
|
||||
|
||||
document.getElementById('filter_dst_select').addEventListener('click', function() {
|
||||
isDstOpen = !isDstOpen;
|
||||
if (!isDstOpen) { this.blur(); }
|
||||
});
|
||||
|
||||
document.getElementById('filter_dst_select').addEventListener('blur', function() {
|
||||
isDstOpen = false;
|
||||
});
|
||||
|
||||
document.getElementById('filter_src_select').addEventListener('focus', function() { localIsSrcFocus = true; });
|
||||
document.getElementById('filter_src_select').addEventListener('blur', function() { localIsSrcFocus = false; });
|
||||
document.getElementById('filter_dst_select').addEventListener('focus', function() { localIsDstFocus = true; });
|
||||
document.getElementById('filter_dst_select').addEventListener('blur', function() { localIsDstFocus = false; });
|
||||
|
||||
document.getElementById('filter_local_checkbox').addEventListener('change', startStream);
|
||||
|
||||
window.addEventListener('DOMContentLoaded', startStream);
|
||||
//]]></script>
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
(function() {
|
||||
var getUrl = "<%=luci.dispatcher.build_url('admin/network/conntrack/get_settings')%>";
|
||||
var setUrl = "<%=luci.dispatcher.build_url('admin/network/conntrack/set_settings')%>";
|
||||
|
||||
var activeTimer = null;
|
||||
|
||||
var elements = {
|
||||
interval: document.getElementById("interval_select"),
|
||||
rows: document.getElementById("rows_select"),
|
||||
filter_local: document.getElementById("filter_local_checkbox"),
|
||||
show_appname: document.getElementById("show_appname_checkbox")
|
||||
};
|
||||
|
||||
window.conntrack_settings = {
|
||||
interval: 1.5,
|
||||
rows: 50,
|
||||
filter_local: true,
|
||||
show_appname: false
|
||||
};
|
||||
|
||||
var loadSavedSettings = function() {
|
||||
fetch(getUrl, { method: "GET", cache: "no-cache" })
|
||||
.then(function(res) { return res.json(); })
|
||||
.then(function(config) {
|
||||
if (config) {
|
||||
window.conntrack_settings.interval = parseFloat(config.interval) || 1.5;
|
||||
window.conntrack_settings.rows = parseInt(config.rows) || 50;
|
||||
window.conntrack_settings.filter_local = (config.filter_local === "true");
|
||||
window.conntrack_settings.show_appname = (config.show_appname === "true");
|
||||
|
||||
if (elements.interval) elements.interval.value = config.interval;
|
||||
if (elements.rows) elements.rows.value = config.rows;
|
||||
if (elements.filter_local) {
|
||||
if (elements.filter_local.type === "checkbox") elements.filter_local.checked = window.conntrack_settings.filter_local;
|
||||
else elements.filter_local.value = config.filter_local;
|
||||
}
|
||||
if (elements.show_appname) {
|
||||
if (elements.show_appname.type === "checkbox") elements.show_appname.checked = window.conntrack_settings.show_appname;
|
||||
else elements.show_appname.value = config.show_appname;
|
||||
}
|
||||
|
||||
if (typeof window.startStream === "function") {
|
||||
window.startStream();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
};
|
||||
|
||||
var bindAutoSyncEvents = function() {
|
||||
var postUpdate = function() {
|
||||
var payload = {
|
||||
interval: elements.interval ? elements.interval.value : "1500",
|
||||
rows: elements.rows ? elements.rows.value : "50",
|
||||
filter_local: elements.filter_local ? (elements.filter_local.type === "checkbox" ? elements.filter_local.checked : elements.filter_local.value === "true") : true,
|
||||
show_appname: elements.show_appname ? (elements.show_appname.type === "checkbox" ? elements.show_appname.checked : elements.show_appname.value === "true") : false
|
||||
};
|
||||
|
||||
window.conntrack_settings.interval = parseFloat(payload.interval);
|
||||
window.conntrack_settings.rows = parseInt(payload.rows);
|
||||
window.conntrack_settings.filter_local = (payload.filter_local === true || payload.filter_local === "true");
|
||||
window.conntrack_settings.show_appname = (payload.show_appname === true || payload.show_appname === "true");
|
||||
|
||||
fetch(setUrl, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(function(res) { return res.json(); })
|
||||
.then(function(data) {
|
||||
if (data && data.status === "ok" && typeof window.startStream === "function") {
|
||||
// window.startStream(); // have been processed (see above individual event handler)
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
};
|
||||
|
||||
for (var key in elements) {
|
||||
if (elements.hasOwnProperty(key) && elements[key]) {
|
||||
var evType = (elements[key].type === "checkbox" || elements[key].tagName === "SELECT") ? "change" : "input";
|
||||
elements[key].addEventListener(evType, postUpdate);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
loadSavedSettings();
|
||||
bindAutoSyncEvents();
|
||||
});
|
||||
})();
|
||||
//]]></script>
|
||||
</script>
|
||||
<%+footer%>
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
msgid "ConnTrack"
|
||||
msgstr "连接跟踪"
|
||||
|
||||
msgid "ConnTrack Connection Monitor"
|
||||
msgstr "ConnTrack 连接实时监视器"
|
||||
|
||||
msgid "Realtime single-direction traffic speed flow based on Event-Stream (SSE)."
|
||||
msgstr "基于 Event-Stream (SSE) 长连接架构的单向实时网速/协议审计看板。"
|
||||
|
||||
msgid "To show AppName, you'll need to patch kmod-nf-conntrack, please refers to"
|
||||
msgstr "想查看应用名称, 你需要给内核模块kmod-nf-connntrack打补丁, 详见"
|
||||
|
||||
msgid "AppName"
|
||||
msgstr "应用名称"
|
||||
|
||||
msgid "Show AppName (experiment)"
|
||||
msgstr "显示应用名称 (实验)"
|
||||
|
||||
msgid "Interval (Seconds)"
|
||||
msgstr "采样刷新时间间隔"
|
||||
|
||||
msgid "Display Rows"
|
||||
msgstr "屏幕显示最大行数"
|
||||
|
||||
msgid "Actual Delta Time"
|
||||
msgstr "内核实际采样物理间隔"
|
||||
|
||||
msgid "L3"
|
||||
msgstr "网络层"
|
||||
|
||||
msgid "Protocol"
|
||||
msgstr "传输协议"
|
||||
|
||||
msgid "State (Direction)"
|
||||
msgstr "连接状态 (单向流量方向)"
|
||||
|
||||
msgid "Source IP:Port"
|
||||
msgstr "源 IP 地址 : 端口号"
|
||||
|
||||
msgid "Destination IP:Port"
|
||||
msgstr "目的 IP 地址 : 端口号"
|
||||
|
||||
msgid "Accumulated"
|
||||
msgstr "该方向累计消耗流量"
|
||||
|
||||
msgid "Current Speed"
|
||||
msgstr "当前方向即时网速"
|
||||
|
||||
msgid "Connecting to Event-Stream..."
|
||||
msgstr "正在安全握手并建立 Event-Stream 毫秒级长连接数据通道..."
|
||||
|
||||
msgid "No active traffic speed detected..."
|
||||
msgstr "当前暂无活跃网速的大流量连接,表格已自动过滤零速率空闲链..."
|
||||
|
||||
msgid "Stream connection lost, retrying..."
|
||||
msgstr "Event-Stream 连接意外被系统或网页剥离,正在疯狂尝试自动重新重连..."
|
||||
|
||||
msgid "Filter Local/Loopback"
|
||||
msgstr "过滤本地或回环连接"
|
||||
|
||||
msgid "Honor of Kings"
|
||||
msgstr "王者荣耀"
|
||||
|
||||
msgid "YouTube"
|
||||
msgstr "油管"
|
||||
|
||||
msgid "TikTok"
|
||||
msgstr "抖音"
|
||||
|
||||
msgid "Tencent Video"
|
||||
msgstr "腾讯视频"
|
||||
|
||||
msgid "X (Twitter)"
|
||||
msgstr "X (推特)"
|
||||
|
||||
msgid "Instagram"
|
||||
msgstr "INS 照片墙"
|
||||
|
||||
msgid "HTTPS Connection"
|
||||
msgstr "HTTPS 连接"
|
||||
|
||||
msgid "Custom Icons Config"
|
||||
msgstr "自定义图标配置"
|
||||
|
||||
msgid "Manage your custom application regex mappings and SVG icons here. Note that the regex and icon mappings only apply to tracked TCP connections that successfully return a domain name for matching, and will not affect existing hardcoded rules (such as YouTube detection). Custom regex expressions and icon mappings will be matched with the highest priority based on their ranking order."
|
||||
msgstr "在此管理您的自定义应用正则表达式与 SVG 图标库。请注意,正则表达式与图标映射仅适用于能够成功返回明文域名进行匹配的跟踪 TCP 连接,且绝不会影响系统中已有的固定硬编码规则(例如 YouTube 判定)。自定义的正则表达式与图标映射将严格按照列表中的上下排名次序享有最高优先匹配权。"
|
||||
|
||||
msgid "Connection Tracking List"
|
||||
msgstr "连接跟踪列表"
|
||||
|
||||
msgid "Custom App Icons Config"
|
||||
msgstr "自定义应用图标配置"
|
||||
|
||||
msgid "Conntrack Custom App Icons"
|
||||
msgstr "连接跟踪自定义应用图标"
|
||||
|
||||
msgid "Regex Expression & Icon Mappings"
|
||||
msgstr "正则表达式与图标映射"
|
||||
|
||||
msgid "Regex Pattern"
|
||||
msgstr "正则模式"
|
||||
|
||||
msgid "Iconify ID"
|
||||
msgstr "Iconify ID"
|
||||
|
||||
msgid "Custom SVG Icons Repository"
|
||||
msgstr "自定义 SVG 图标库"
|
||||
|
||||
msgid "Icon Preview"
|
||||
msgstr "图标预览"
|
||||
|
||||
msgid "Icon Width"
|
||||
msgstr "宽度"
|
||||
|
||||
msgid "Icon Height"
|
||||
msgstr "高度"
|
||||
|
||||
msgid "SVG Body (Raw XML)"
|
||||
msgstr "SVG 源码 (原始 XML)"
|
||||
|
||||
msgid "You can use an <a href=\"https://icon-sets.iconify.design/\" target=\"_blank\">existing Iconify ID</a> (e.g., logos:youtube) or a custom ID defined in the Custom SVG Icons Repository below."
|
||||
msgstr "可以使用<a href=\"https://icon-sets.iconify.design/\" target=\"_blank\">已有的公共 Iconify ID</a>(例如 logos:youtube),或者下方【自定义 SVG 图标库】中已注册的专属 ID。"
|
||||
|
||||
msgid "You can use an existing Iconify ID (e.g., logos:youtube) or a custom ID defined in the Custom SVG Icons Repository below."
|
||||
msgstr "可以使用已有的公共 Iconify ID(例如 logos:youtube),或者下方【自定义 SVG 图标库】中已注册的专属 ID。"
|
||||
|
||||
msgid "The Iconify ID must start with 'conntrack:' prefix to avoid overriding existing Iconify datasets."
|
||||
msgstr "Iconify ID 必须以 'conntrack:' 前缀起始,以避免与系统已有的远程通用图标集产生命名空间冲突。"
|
||||
|
||||
msgid "Validation Error: The Iconify ID must begin with 'conntrack:' prefix (e.g., conntrack:custom_name)."
|
||||
msgstr "验证错误:Iconify ID 必须以 'conntrack:' 前缀起始 (例如 conntrack:custom_name)。"
|
||||
|
||||
msgid "Invalid SVG content. Must start with <svg> tag."
|
||||
msgstr "无效的 SVG 内容。必须以 <svg> 标签起始。"
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
zh-cn
|
||||
File renamed without changes.
@@ -0,0 +1,44 @@
|
||||
#
|
||||
# Copyright (C) 2026
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v3.0
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI DLNA Music Player
|
||||
LUCI_DESCRIPTION:=Turn an OpenWrt device into a music player: auto-detect the sound card, \
|
||||
configure MPD and upmpdcli (DLNA casting), browse and play local music.
|
||||
#
|
||||
# 依赖说明:
|
||||
# mpd-full / mpc / upmpdcli / alsa-utils → 播放与 DLNA 核心
|
||||
# 中文翻译:lmo 已内置在主包 root/usr/lib/lua/luci/i18n/ 里,
|
||||
# 勾上插件即带中文,无需额外选包
|
||||
# (翻译源文件在 i18n-src/,改名是为了避免 luci.mk 重复生成 i18n 包)
|
||||
# kmod-sound-* → 声卡驱动(覆盖常见板载 HDA + USB 声卡)
|
||||
#
|
||||
LUCI_DEPENDS:=+mpd-full +mpc +upmpdcli +alsa-utils \
|
||||
+kmod-sound-core +kmod-sound-seq \
|
||||
+kmod-sound-hda-core +kmod-sound-hda-intel \
|
||||
+kmod-sound-hda-codec-realtek +kmod-sound-hda-codec-conexant \
|
||||
+kmod-sound-hda-codec-analog +kmod-sound-hda-codec-hdmi \
|
||||
+kmod-sound-hda-codec-via +kmod-sound-hda-codec-idt \
|
||||
+kmod-sound-hda-codec-cirrus +kmod-sound-hda-codec-cmedia \
|
||||
+kmod-sound-hda-codec-ca0132 +kmod-sound-hda-codec-ca0110 \
|
||||
+kmod-usb-audio
|
||||
|
||||
PKG_NAME:=luci-app-dlna-player
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=sakura-builder1 <sakura-builder1@users.noreply.github.com>
|
||||
PKG_LICENSE:=GPL-3.0
|
||||
|
||||
# 配置文件:安装/升级时保留用户修改
|
||||
# 注意:必须在 include luci.mk 之前定义(luci.mk 内部会 eval 包定义)
|
||||
define Package/luci-app-dlna-player/conffiles
|
||||
/etc/config/dlna-player
|
||||
endef
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -0,0 +1,124 @@
|
||||
# luci-app-dlna-player
|
||||
|
||||
> 把 OpenWrt 设备(尤其是 x86 小主机)变成一台 **DLNA 音乐播放器** —— 自动探测声卡、配置 MPD 播放、支持手机投送。
|
||||
|
||||
> 🤖 **本插件由 AI 编写**(从需求梳理、代码实现到调试排错,全程由 AI 完成)。
|
||||
|
||||
[](LICENSE)
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 🔍 **声卡自动探测** —— 无需手动找 `hw:X,Y`,自动识别模拟输出设备
|
||||
- 🔊 **自动解除静音** —— ALSA 新装默认静音,脚本自动处理
|
||||
- 🎵 **MPD 自动配置** —— 自动生成可用的 `mpd.conf`
|
||||
- 📡 **DLNA 投送** —— 手机(网易云 / QQ音乐 / BubbleUPnP)直接投送
|
||||
- 🎚️ **实时音量 / 进度滑块** —— 网页上直接拖动调节
|
||||
- 🔍 **音乐库搜索** —— 歌曲多了也能快速找到
|
||||
- 🔁 **播放模式** —— 循环 / 随机 / 顺序播放
|
||||
- 💾 **持久化音量** —— 绕过 tmpfs,重启后音量不丢
|
||||
- 🌏 **中文界面** —— 内置翻译,开箱即用
|
||||
- 🔄 **完整开机自启** —— 服务顺序自动编排
|
||||
|
||||
## 📋 适用场景
|
||||
|
||||
- x86 小主机 / 软路由装了 OpenWrt,想用它的 **3.5mm 音频口**放歌
|
||||
- 想把 OpenWrt 变成一台 **DLNA 音箱**
|
||||
- 树莓派等带音频输出的设备
|
||||
|
||||
## 📦 安装
|
||||
|
||||
在 OpenWrt 源码根目录执行:
|
||||
|
||||
```bash
|
||||
# 1. 进入 package 目录,克隆插件
|
||||
cd package
|
||||
git clone https://github.com/sakura-builder1/luci-app-dlna-player.git
|
||||
|
||||
# 2. 回到源码根目录
|
||||
cd ..
|
||||
|
||||
# 3. 更新一下 feeds(首次编译需要)
|
||||
./scripts/feeds update -a && ./scripts/feeds install -a
|
||||
|
||||
# 4. 只编译这个插件
|
||||
make package/luci-app-dlna-player/compile V=s
|
||||
```
|
||||
|
||||
编译产物在:
|
||||
|
||||
```
|
||||
bin/packages/<架构>/base/luci-app-dlna-player_*.ipk
|
||||
```
|
||||
|
||||
例如 x86_64:
|
||||
|
||||
```
|
||||
bin/packages/x86_64/base/luci-app-dlna-player_1.0.0-r5_all.ipk
|
||||
```
|
||||
|
||||
把它拷到路由器上安装:
|
||||
|
||||
```bash
|
||||
scp bin/packages/x86_64/base/luci-app-dlna-player_*.ipk root@192.168.1.1:/tmp/
|
||||
ssh root@192.168.1.1
|
||||
opkg install /tmp/luci-app-dlna-player_*.ipk
|
||||
/etc/init.d/rpcd restart # 重要!否则菜单不显示
|
||||
```
|
||||
|
||||
> 💡 依赖会自动带上(mpd-full / upmpdcli / alsa-utils / 常见声卡驱动),不用手动选。
|
||||
|
||||
安装后在 **服务 → DLNA 音乐播放器** 里配置使用。
|
||||
|
||||
## ⚙️ 配置项
|
||||
|
||||
配置文件:`/etc/config/dlna-player`
|
||||
|
||||
```uci
|
||||
config audio 'main'
|
||||
option enabled '0' # 是否启用(默认关闭)
|
||||
option device '' # 声卡设备,留空=自动探测(如 hw:3,0)
|
||||
option music_dir '/srv/music' # 音乐目录
|
||||
option volume '40' # 默认音量 0-100
|
||||
option dlna_name '' # DLNA 显示名,留空=用主机名
|
||||
option unmute '1' # 自动解除 ALSA 静音
|
||||
option persist_volume '1' # 持久化音量
|
||||
```
|
||||
|
||||
修改后应用:
|
||||
|
||||
```bash
|
||||
uci set dlna-player.main.volume='60'
|
||||
uci commit dlna-player
|
||||
/etc/init.d/dlna-player start
|
||||
```
|
||||
|
||||
## 🏗️ 工作原理
|
||||
|
||||
```
|
||||
启动流程:
|
||||
S50 dlna-player → 探测声卡 + 修权限 + 解静音 + 生成 mpd.conf + 建目录
|
||||
S93 mpd → 启动 MPD(以 root 运行)
|
||||
S94 dlna-player-volume → 设置默认音量(有持久状态则跳过)
|
||||
S95 upmpdcli → 启动 DLNA 服务
|
||||
|
||||
播放链路:
|
||||
手机 App ──DLNA──▶ upmpdcli ──MPD协议──▶ MPD ──ALSA──▶ 声卡 ──▶ 🎵
|
||||
```
|
||||
|
||||
<img width="2191" height="1249" alt="image" src="https://github.com/user-attachments/assets/a1da170e-ccb9-431b-a01e-f13b0502f0a3" />
|
||||
|
||||
|
||||
## 🔧 兼容性
|
||||
|
||||
| OpenWrt 版本 | 状态 |
|
||||
|---|---|
|
||||
| 24.10 | ✅ 已测试 |
|
||||
| 23.05 | ⚠️ 理论可用,未测试 |
|
||||
| 25.12 | ⚠️ 需要适配 `apk` |
|
||||
|
||||
## 📄 License
|
||||
|
||||
GPL-3.0
|
||||
|
||||
本项目采用 **GNU General Public License v3.0** 授权 —— 你可以自由使用、修改、分发,
|
||||
但**衍生作品必须同样以 GPL-3.0 开源**,不能闭源独吞。
|
||||
@@ -0,0 +1,477 @@
|
||||
/* This is free software, licensed under the Apache License, Version 2.0 */
|
||||
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require fs';
|
||||
'require uci';
|
||||
'require ui';
|
||||
|
||||
var APLAY = '/usr/bin/aplay';
|
||||
var MPC = '/usr/bin/mpc';
|
||||
|
||||
/* LuCI 的 fs.exec 不会因非零退出码 reject,需自己判断 */
|
||||
function mpcText(args) {
|
||||
return fs.exec(MPC, args).then(function (rv) {
|
||||
return (rv && rv.code === 0) ? (rv.stdout || '') : '';
|
||||
}).catch(function () { return ''; });
|
||||
}
|
||||
function mpcRun(args) {
|
||||
return fs.exec(MPC, args).catch(function () { });
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
load: function () {
|
||||
function running(cmd) {
|
||||
return fs.exec('/bin/pidof', [ cmd ]).then(function (rv) {
|
||||
return !!(rv && rv.code === 0 && (rv.stdout || '').trim() !== '');
|
||||
}).catch(function () { return false; });
|
||||
}
|
||||
return Promise.all([
|
||||
uci.load('dlna-player'),
|
||||
L.resolveDefault(fs.exec(APLAY, ['-l']), { stdout: '' }),
|
||||
mpcText(['status']),
|
||||
running('mpd'),
|
||||
running('upmpdcli'),
|
||||
mpcText(['listall'])
|
||||
]);
|
||||
},
|
||||
|
||||
render: function (data) {
|
||||
var aplayOut = (data[1] && data[1].stdout) || '';
|
||||
var mpdUp = data[3];
|
||||
var upnpUp = data[4];
|
||||
var allFiles = (data[5] || '').split('\n').filter(function (s) { return s.trim() !== ''; });
|
||||
|
||||
var cards = [];
|
||||
aplayOut.split('\n').forEach(function (line) {
|
||||
var mm = line.match(/^card\s+(\d+):\s+(\S+)\s+\[([^\]]+)\],\s+device\s+(\d+):\s+([^\[]+)\[([^\]]+)\]/);
|
||||
if (mm) {
|
||||
cards.push({
|
||||
dev: 'hw:' + mm[1] + ',' + mm[4],
|
||||
label: 'card ' + mm[1] + ' / device ' + mm[4] + ' — ' + mm[3] + ' / ' + mm[6].trim()
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('dlna-player', _('DLNA Music Player'),
|
||||
_('Turn this device into a music player: auto-detect the sound card, configure MPD and upmpdcli (DLNA).'));
|
||||
|
||||
/* ================= 基础设置 ================= */
|
||||
s = m.section(form.TypedSection, 'audio', _('Basic settings'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enabled', _('Enable audio service'),
|
||||
_('Turn the audio setup on or off. When off, MPD and upmpdcli are stopped and disabled.'));
|
||||
o.rmempty = false;
|
||||
o.default = '0'; /* 默认关闭 */
|
||||
|
||||
o = s.option(form.DummyValue, '_status', _('Service status'));
|
||||
o.rawhtml = true;
|
||||
o.cfgvalue = function () {
|
||||
function badge(ok, name) {
|
||||
return '<span style="display:inline-block;padding:.15rem .5rem;border-radius:.25rem;' +
|
||||
'color:#fff;background:' + (ok ? '#3c9a3c' : '#b94a48') + ';margin-right:.4rem">' +
|
||||
name + ' ' + (ok ? _('running') : _('stopped')) + '</span>';
|
||||
}
|
||||
return badge(mpdUp, 'MPD') + badge(upnpUp, 'upmpdcli');
|
||||
};
|
||||
|
||||
o = s.option(form.Value, 'device', _('Sound card'),
|
||||
_('Leave empty to auto-detect. Pick a detected card below, or type a custom device such as <code>hw:0,0</code>.'));
|
||||
o.rmempty = true;
|
||||
o.placeholder = _('auto-detect');
|
||||
cards.forEach(function (c) { o.value(c.dev, c.label); });
|
||||
|
||||
o = s.option(form.Value, 'music_dir', _('Music directory'),
|
||||
_('Where your music files are stored (can be a mount point, e.g. /mnt/sda1/music).'));
|
||||
o.default = '/srv/music';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Value, 'volume', _('Default volume'),
|
||||
_('Default playback volume (0-100). Applied when you save, and on boot if "Remember volume" is off.'));
|
||||
o.datatype = 'range(0,100)';
|
||||
o.default = '40';
|
||||
o.rmempty = false;
|
||||
|
||||
/* DLNA 名称:默认留空 */
|
||||
o = s.option(form.Value, 'dlna_name', _('DLNA device name'),
|
||||
_('The name shown on your phone when casting music. Leave empty to use the hostname.'));
|
||||
o.rmempty = true;
|
||||
o.placeholder = _('use hostname');
|
||||
|
||||
o = s.option(form.Flag, 'unmute', _('Unmute on boot'),
|
||||
_('ALSA devices are muted by default. Enable to unmute automatically.'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.option(form.Flag, 'persist_volume', _('Remember volume'),
|
||||
_('Keep the playback volume across reboots.'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
return m.render().then(function (node) {
|
||||
if (!document.getElementById('dlna-player-layout-css')) {
|
||||
document.head.appendChild(E('style', { 'id': 'dlna-player-layout-css' }, [
|
||||
'.dlna-player-row { display:flex; gap:1rem; flex-wrap:wrap; align-items:stretch; }',
|
||||
'.dlna-player-row > .dlna-player-col { flex:1 1 15rem; min-width:14rem; max-width:100%; display:flex; flex-direction:column; }',
|
||||
'.dlna-player-row > .dlna-player-col > .cbi-section { flex:1 1 auto; }',
|
||||
'.dlna-player-name { flex:1; min-width:0; white-space:nowrap; overflow-x:auto; overflow-y:hidden; padding:.1rem 0; }',
|
||||
'.dlna-player-row .cbi-value { display:flex; align-items:flex-start; gap:.75rem; flex-wrap:wrap; }',
|
||||
'.dlna-player-row .cbi-value-title { text-align:left; flex:0 0 8.5rem; max-width:8.5rem; padding-top:.35rem; }',
|
||||
'.dlna-player-row .cbi-value-field { flex:1 1 auto; min-width:0; }',
|
||||
'.dlna-player-row .cbi-value-description { margin-left:9.25rem; }',
|
||||
'.dlna-player-line { display:flex; align-items:center; gap:.75rem; flex-wrap:wrap; margin-bottom:.5rem; padding-left:1rem; }',
|
||||
'.dlna-player-line > label { flex:0 0 8.5rem; max-width:8.5rem; text-align:left; }',
|
||||
/* 列表也留出同样的左边距,视觉更整齐 */
|
||||
'.dlna-player-row .dlna-player-list { margin-left:1rem; }'
|
||||
]));
|
||||
}
|
||||
|
||||
var h2 = node.querySelector('h2');
|
||||
if (h2) h2.style.width = '100%';
|
||||
var desc = node.querySelector('.cbi-map-descr');
|
||||
if (desc) desc.style.width = '100%';
|
||||
|
||||
var formSec = node.querySelector('.cbi-section') ||
|
||||
E('div', { 'class': 'cbi-section dlna-player-col' }, []);
|
||||
|
||||
var statusLine = E('code', {}, [ _('loading...') ]);
|
||||
var volSlider, volLabel, seekSlider, seekLabel;
|
||||
var repeatBtn, randomBtn, orderBtn;
|
||||
var filterInput, listFrame, listCount;
|
||||
var curFile = '';
|
||||
var fileRows = [];
|
||||
|
||||
/* ---------- 状态刷新(就地更新) ---------- */
|
||||
function refreshStatus() {
|
||||
return Promise.all([
|
||||
mpcText(['status']),
|
||||
mpcText(['-f', '%file%', 'current'])
|
||||
]).then(function (r) {
|
||||
var st = r[0] || '';
|
||||
var cur = (r[1] || '').trim();
|
||||
curFile = cur;
|
||||
|
||||
var line = st.split('\n').filter(function (l) { return l.trim() !== ''; })
|
||||
.slice(0, 2).join(' — ');
|
||||
statusLine.textContent = line || _('stopped');
|
||||
|
||||
/* 音量 */
|
||||
var vm = st.match(/volume:\s*(\d+)%/);
|
||||
if (vm && volSlider && document.activeElement !== volSlider) {
|
||||
volSlider.value = vm[1];
|
||||
volLabel.textContent = vm[1] + '%';
|
||||
}
|
||||
|
||||
/* 播放进度:数字始终更新;滑块仅在未拖动时同步 */
|
||||
var pm = st.match(/#(\d+)\/(\d+)\s+(\d+):(\d+)\/(\d+):(\d+)/);
|
||||
if (seekSlider && seekLabel) {
|
||||
if (pm) {
|
||||
var elapsed = parseInt(pm[3], 10) * 60 + parseInt(pm[4], 10);
|
||||
var total = parseInt(pm[5], 10) * 60 + parseInt(pm[6], 10);
|
||||
var pct = total > 0 ? Math.round(elapsed * 100 / total) : 0;
|
||||
/* 数字永远更新(修掉"拖动后数字不动") */
|
||||
seekLabel.textContent = pm[3] + ':' + pm[4] + ' / ' + pm[5] + ':' + pm[6];
|
||||
/* 滑块只在没被拖动时跟随 */
|
||||
if (document.activeElement !== seekSlider)
|
||||
seekSlider.value = pct;
|
||||
} else {
|
||||
/* 没有播放时归零 */
|
||||
seekLabel.textContent = '0:00 / 0:00';
|
||||
if (document.activeElement !== seekSlider)
|
||||
seekSlider.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 循环 / 随机 / 顺序 */
|
||||
var rep = /repeat:\s*on/.test(st);
|
||||
var rnd = /random:\s*on/.test(st);
|
||||
var sgl = /single:\s*on/.test(st);
|
||||
if (repeatBtn) {
|
||||
repeatBtn.textContent = rep ? _('Loop: ON') : _('Loop: OFF');
|
||||
repeatBtn.className = 'btn cbi-button cbi-button-' + (rep ? 'apply' : 'reset');
|
||||
}
|
||||
if (randomBtn) {
|
||||
randomBtn.textContent = rnd ? _('Shuffle: ON') : _('Shuffle: OFF');
|
||||
randomBtn.className = 'btn cbi-button cbi-button-' + (rnd ? 'apply' : 'reset');
|
||||
}
|
||||
if (orderBtn) {
|
||||
var seq = !rnd && !rep && !sgl; /* 三者都关 = 纯顺序播放 */
|
||||
orderBtn.textContent = seq ? _('Sequential: ON') : _('Sequential: OFF');
|
||||
orderBtn.className = 'btn cbi-button cbi-button-' + (seq ? 'apply' : 'reset');
|
||||
}
|
||||
|
||||
fileRows.forEach(function (item) {
|
||||
var on = (item.name === cur);
|
||||
item.el.style.background = on ? '#e8f5e9' : '';
|
||||
item.el.style.fontWeight = on ? 'bold' : '';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function act(args) {
|
||||
return mpcRun(args).then(function () { return refreshStatus(); });
|
||||
}
|
||||
|
||||
function nextLocalFile() {
|
||||
return mpcText(['-f', '%file%', 'current']).then(function (cur) {
|
||||
cur = (cur || '').trim();
|
||||
var i = allFiles.indexOf(cur);
|
||||
if (i >= 0 && i + 1 < allFiles.length) return allFiles[i + 1];
|
||||
return allFiles.length ? allFiles[0] : null;
|
||||
});
|
||||
}
|
||||
|
||||
/* 播放某首:把「全部歌曲」加入队列,再跳到这一首
|
||||
这样放完会自动播下一首(顺序播放)✅ */
|
||||
function playFile(f) {
|
||||
var idx = allFiles.indexOf(f);
|
||||
return mpcRun(['clear']).then(function () {
|
||||
return mpcRun(['add', '/']);
|
||||
}).then(function () {
|
||||
/* mpc play 用 1-based 序号;找不到时回退到第 1 首 */
|
||||
return mpcRun(['play', String(idx >= 0 ? idx + 1 : 1)]);
|
||||
}).then(function () { return refreshStatus(); });
|
||||
}
|
||||
|
||||
function pbtn(label, style, handler) {
|
||||
return E('button', {
|
||||
'class': 'btn cbi-button cbi-button-' + style,
|
||||
'click': ui.createHandlerFn(this, handler)
|
||||
}, [ label ]);
|
||||
}
|
||||
|
||||
/* ---------- 音乐列表(只滚动,不翻页) ---------- */
|
||||
function currentFilter() {
|
||||
return (filterInput && filterInput.value || '').toLowerCase();
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
var kw = currentFilter();
|
||||
var fl = kw ? allFiles.filter(function (f) { return f.toLowerCase().indexOf(kw) >= 0; })
|
||||
: allFiles;
|
||||
|
||||
fileRows = [];
|
||||
listFrame.innerHTML = '';
|
||||
if (!fl.length) {
|
||||
listFrame.appendChild(E('div', { 'style': 'padding:.8rem;color:#888;text-align:center' }, [
|
||||
_('No matching music.')
|
||||
]));
|
||||
} else {
|
||||
fl.forEach(function (f) {
|
||||
var row = E('div', {
|
||||
'style': 'display:flex;align-items:center;gap:.5rem;padding:.3rem .4rem;border-radius:.2rem'
|
||||
}, [
|
||||
pbtn(_('Play'), 'action', function () { return playFile(f); }),
|
||||
E('span', { 'class': 'dlna-player-name' }, [ f ]),
|
||||
pbtn(_('Add'), 'reset', function () {
|
||||
return mpcRun(['add', f]).then(function () { return refreshStatus(); });
|
||||
})
|
||||
]);
|
||||
fileRows.push({ name: f, el: row });
|
||||
listFrame.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
listCount.textContent = _('Showing %d / %d').format(fl.length, allFiles.length);
|
||||
|
||||
fileRows.forEach(function (item) {
|
||||
var on = (item.name === curFile);
|
||||
item.el.style.background = on ? '#e8f5e9' : '';
|
||||
item.el.style.fontWeight = on ? 'bold' : '';
|
||||
});
|
||||
}
|
||||
|
||||
/* ============ 播放器区域 ============ */
|
||||
var playerSec = E('div', { 'class': 'cbi-section dlna-player-col' }, [
|
||||
E('h3', {}, [ _('Player') ]),
|
||||
E('p', { 'class': 'cbi-section-descr' }, [ _('Current:') + ' ', statusLine ]),
|
||||
|
||||
/* 音量 */
|
||||
E('div', { 'class': 'dlna-player-line' }, [
|
||||
E('label', {}, [ _('Volume') ]),
|
||||
volSlider = E('input', {
|
||||
'type': 'range', 'min': '0', 'max': '100', 'step': '1',
|
||||
'style': 'flex:1;max-width:20rem;cursor:pointer'
|
||||
}, []),
|
||||
volLabel = E('code', { 'style': 'min-width:3rem' }, [ '--%' ])
|
||||
]),
|
||||
|
||||
/* 进度 */
|
||||
E('div', { 'class': 'dlna-player-line' }, [
|
||||
E('label', {}, [ _('Progress') ]),
|
||||
seekSlider = E('input', {
|
||||
'type': 'range', 'min': '0', 'max': '100', 'step': '1',
|
||||
'style': 'flex:1;max-width:20rem;cursor:pointer'
|
||||
}, []),
|
||||
seekLabel = E('code', { 'style': 'min-width:7.5rem' }, [ '0:00 / 0:00' ])
|
||||
]),
|
||||
|
||||
/* 播放控制(用 dlna-player-line 保证与左侧标签对齐一致) */
|
||||
E('div', { 'class': 'dlna-player-line' }, [
|
||||
E('label', {}, [ _('Player control') ]),
|
||||
E('div', { 'style': 'display:flex;gap:.5rem;flex-wrap:wrap' }, [
|
||||
pbtn(_('Play'), 'apply', function () { return act(['play']); }),
|
||||
pbtn(_('Pause'), 'reset', function () { return act(['pause']); }),
|
||||
pbtn(_('Stop'), 'reset', function () { return act(['stop']); }),
|
||||
pbtn(_('Next track'), 'action', function () {
|
||||
return nextLocalFile().then(function (f) {
|
||||
if (!f) return refreshStatus();
|
||||
return playFile(f);
|
||||
});
|
||||
}),
|
||||
pbtn(_('Rescan'), 'action', function () { return act(['update']); })
|
||||
])
|
||||
]),
|
||||
|
||||
/* 循环 / 随机 / 顺序 */
|
||||
E('div', { 'class': 'dlna-player-line' }, [
|
||||
E('label', {}, [ _('Play mode') ]),
|
||||
E('div', { 'style': 'display:flex;gap:.5rem;flex-wrap:wrap' }, [
|
||||
repeatBtn = pbtn(_('Loop: OFF'), 'reset', function () {
|
||||
return mpcText(['status']).then(function (st) {
|
||||
return act(['repeat', /repeat:\s*on/.test(st) ? 'off' : 'on']);
|
||||
});
|
||||
}),
|
||||
randomBtn = pbtn(_('Shuffle: OFF'), 'reset', function () {
|
||||
return mpcText(['status']).then(function (st) {
|
||||
return act(['random', /random:\s*on/.test(st) ? 'off' : 'on']);
|
||||
});
|
||||
}),
|
||||
orderBtn = pbtn(_('Sequential: ON'), 'apply', function () {
|
||||
/* 顺序播放:关闭 随机 / 循环 / 单曲 */
|
||||
return mpcRun(['random', 'off']).then(function () {
|
||||
return mpcRun(['repeat', 'off']);
|
||||
}).then(function () {
|
||||
return mpcRun(['single', 'off']);
|
||||
}).then(function () { return refreshStatus(); });
|
||||
})
|
||||
])
|
||||
]),
|
||||
|
||||
/* 队列操作 */
|
||||
E('div', { 'class': 'dlna-player-line' }, [
|
||||
E('label', {}, [ _('Play queue') ]),
|
||||
E('div', { 'style': 'display:flex;gap:.5rem;flex-wrap:wrap' }, [
|
||||
pbtn(_('Clear queue'), 'reset', function () { return act(['clear']); })
|
||||
])
|
||||
]),
|
||||
|
||||
/* 搜索 */
|
||||
E('div', { 'class': 'dlna-player-line' }, [
|
||||
E('label', {}, [ _('Search') ]),
|
||||
E('div', { 'style': 'flex:1;min-width:0;display:flex;flex-direction:column' }, [
|
||||
filterInput = E('input', {
|
||||
'type': 'text', 'class': 'cbi-input-text',
|
||||
'placeholder': _('type to filter...'),
|
||||
'style': 'width:100%;max-width:22rem'
|
||||
}, []),
|
||||
E('div', { 'style': 'margin-top:.2rem;color:#666;font-size:.9em' }, [
|
||||
listCount = E('span', {}, [ '' ])
|
||||
])
|
||||
])
|
||||
]),
|
||||
|
||||
/* 列表:只滚动,不翻页 */
|
||||
listFrame = E('div', {
|
||||
'class': 'dlna-player-list',
|
||||
'style': 'border:1px solid #ccc;border-radius:.3rem;' +
|
||||
'max-height:20rem;overflow-y:auto;overflow-x:hidden;background:#fff'
|
||||
}, [])
|
||||
]);
|
||||
|
||||
/* 响应式:宽屏并排,窄屏自动上下(靠 flex-wrap)
|
||||
注意:LuCI 保存后会重渲染表单,导致自定义布局被清掉,
|
||||
所以做成「可重复挂载」的函数,丢失时自动重建 ✅ */
|
||||
var layoutRow = null;
|
||||
|
||||
function mountLayout() {
|
||||
/* 布局还在 → 什么都不做 */
|
||||
if (layoutRow && document.body.contains(layoutRow) &&
|
||||
document.body.contains(playerSec))
|
||||
return;
|
||||
|
||||
/* 找到(或重新找到)基础设置那个 section */
|
||||
var fs = node.querySelector('.cbi-section.dlna-player-col') ||
|
||||
node.querySelector('.cbi-section');
|
||||
if (!fs) return;
|
||||
|
||||
/* 清掉可能残留的旧 row */
|
||||
var old = node.querySelector('.dlna-player-row');
|
||||
if (old) old.remove();
|
||||
|
||||
layoutRow = E('div', { 'class': 'dlna-player-row' }, [
|
||||
E('div', { 'class': 'dlna-player-col' }, [ fs ]),
|
||||
playerSec
|
||||
]);
|
||||
node.appendChild(layoutRow);
|
||||
}
|
||||
|
||||
mountLayout();
|
||||
|
||||
/* ---- 事件绑定 ---- */
|
||||
var volTimer = null;
|
||||
volSlider.addEventListener('input', function () {
|
||||
var v = volSlider.value;
|
||||
volLabel.textContent = v + '%';
|
||||
if (volTimer) clearTimeout(volTimer);
|
||||
volTimer = setTimeout(function () {
|
||||
mpcRun(['volume', v]).then(function () { return refreshStatus(); });
|
||||
}, 150);
|
||||
});
|
||||
|
||||
var seekTimer = null;
|
||||
seekSlider.addEventListener('input', function () {
|
||||
if (seekTimer) clearTimeout(seekTimer);
|
||||
seekTimer = setTimeout(function () {
|
||||
mpcRun(['seek', seekSlider.value + '%']).then(function () {
|
||||
/* 关键:拖完立即失焦,否则滑块会一直"被拖动中"而不再刷新 */
|
||||
seekSlider.blur();
|
||||
return refreshStatus();
|
||||
});
|
||||
}, 250);
|
||||
});
|
||||
|
||||
filterInput.addEventListener('input', function () { renderList(); });
|
||||
|
||||
renderList();
|
||||
refreshStatus();
|
||||
|
||||
/* 每 3 秒同步一次状态(进度条会跟着走),顺便补回被清掉的布局 */
|
||||
var syncTimer = setInterval(function () {
|
||||
if (!document.body.contains(statusLine)) { clearInterval(syncTimer); return; }
|
||||
mountLayout();
|
||||
refreshStatus();
|
||||
}, 3000);
|
||||
|
||||
return node;
|
||||
});
|
||||
},
|
||||
|
||||
handleSaveApply: function (ev, mode) {
|
||||
var self = this;
|
||||
return this.handleSave(ev).then(function () {
|
||||
return uci.apply();
|
||||
}).catch(function (e) {
|
||||
console.warn('dlna-player: uci apply failed', e);
|
||||
}).then(function () {
|
||||
return fs.exec('/etc/init.d/dlna-player', ['start']);
|
||||
}).then(function () {
|
||||
/* 应用「默认音量」——保存时就立即生效(不等下次开机) */
|
||||
var v = uci.get('dlna-player', 'main', 'volume');
|
||||
if (v !== null && v !== undefined && v !== '')
|
||||
return fs.exec('/usr/bin/mpc', ['volume', String(v)]).catch(function () { });
|
||||
}).then(function () {
|
||||
ui.addNotification(null, E('p', _('Settings applied. Services restarted.')));
|
||||
/* 保存后 LuCI 会重渲染,稍等再刷新页面让布局重建 */
|
||||
window.setTimeout(function () { location.reload(); }, 2000);
|
||||
}).catch(function (e) {
|
||||
ui.addNotification(null, E('p', _('Apply failed: %s').format(
|
||||
(e && (e.message || e.toString())) || 'unknown')));
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,153 @@
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Language: zh_Hans\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
|
||||
|
||||
msgid "ALSA devices are muted by default. Enable to unmute automatically."
|
||||
msgstr "ALSA 设备默认静音,启用后开机自动解除。"
|
||||
|
||||
msgid "Add"
|
||||
msgstr "加入队列"
|
||||
|
||||
msgid "Apply failed: %s"
|
||||
msgstr "应用失败:%s"
|
||||
|
||||
msgid "Basic settings"
|
||||
msgstr "基础设置"
|
||||
|
||||
msgid "Clear queue"
|
||||
msgstr "清空队列"
|
||||
|
||||
msgid "Current:"
|
||||
msgstr "当前播放:"
|
||||
|
||||
msgid "DLNA Music Player"
|
||||
msgstr "DLNA音乐播放器"
|
||||
|
||||
msgid "DLNA device name"
|
||||
msgstr "DLNA 设备名"
|
||||
|
||||
msgid "Default playback volume (0-100). Applied when you save, and on boot if \"Remember volume\" is off."
|
||||
msgstr "默认播放音量(0-100)。保存时立即生效;若关闭「记住音量」,开机也会应用。"
|
||||
|
||||
msgid "Default volume"
|
||||
msgstr "默认音量"
|
||||
|
||||
msgid "Enable audio service"
|
||||
msgstr "启用音频服务"
|
||||
|
||||
msgid "Keep the playback volume across reboots."
|
||||
msgstr "重启后仍保留播放音量。"
|
||||
|
||||
msgid "Leave empty to auto-detect. Pick a detected card below, or type a custom device such as <code>hw:0,0</code>."
|
||||
msgstr "留空则自动探测。可从下方列表选择,也可手填设备名(如 <code>hw:0,0</code>)。"
|
||||
|
||||
msgid "Loop: OFF"
|
||||
msgstr "循环:关"
|
||||
|
||||
msgid "Loop: ON"
|
||||
msgstr "循环:开"
|
||||
|
||||
msgid "Music directory"
|
||||
msgstr "音乐目录"
|
||||
|
||||
msgid "Next track"
|
||||
msgstr "下一首"
|
||||
|
||||
msgid "No matching music."
|
||||
msgstr "没有匹配的音乐。"
|
||||
|
||||
msgid "Pause"
|
||||
msgstr "暂停"
|
||||
|
||||
msgid "Play"
|
||||
msgstr "播放"
|
||||
|
||||
msgid "Play mode"
|
||||
msgstr "播放模式"
|
||||
|
||||
msgid "Play queue"
|
||||
msgstr "队列"
|
||||
|
||||
msgid "Player"
|
||||
msgstr "播放器"
|
||||
|
||||
msgid "Player control"
|
||||
msgstr "播放控制"
|
||||
|
||||
msgid "Progress"
|
||||
msgstr "进度"
|
||||
|
||||
msgid "Remember volume"
|
||||
msgstr "记住音量"
|
||||
|
||||
msgid "Rescan"
|
||||
msgstr "重新扫描"
|
||||
|
||||
msgid "Search"
|
||||
msgstr "搜索"
|
||||
|
||||
msgid "Sequential: OFF"
|
||||
msgstr "顺序播放:关"
|
||||
|
||||
msgid "Sequential: ON"
|
||||
msgstr "顺序播放:开"
|
||||
|
||||
msgid "Service status"
|
||||
msgstr "服务状态"
|
||||
|
||||
msgid "Settings applied. Services restarted."
|
||||
msgstr "设置已应用,服务已重启。"
|
||||
|
||||
msgid "Showing %d / %d"
|
||||
msgstr "显示 %d / %d 条"
|
||||
|
||||
msgid "Shuffle: OFF"
|
||||
msgstr "随机:关"
|
||||
|
||||
msgid "Shuffle: ON"
|
||||
msgstr "随机:开"
|
||||
|
||||
msgid "Sound card"
|
||||
msgstr "声卡"
|
||||
|
||||
msgid "Stop"
|
||||
msgstr "停止"
|
||||
|
||||
msgid "The name shown on your phone when casting music. Leave empty to use the hostname."
|
||||
msgstr "手机投送音乐时显示的名称。留空则使用主机名。"
|
||||
|
||||
msgid "Turn the audio setup on or off. When off, MPD and upmpdcli are stopped and disabled."
|
||||
msgstr "开启或关闭音频服务。关闭时会停止并禁用 MPD 与 upmpdcli。"
|
||||
|
||||
msgid "Turn this device into a music player: auto-detect the sound card, configure MPD and upmpdcli (DLNA)."
|
||||
msgstr "把本机变成音乐播放器:自动探测声卡,配置 MPD 与 upmpdcli(DLNA 投送)。"
|
||||
|
||||
msgid "Unmute on boot"
|
||||
msgstr "开机解除静音"
|
||||
|
||||
msgid "Volume"
|
||||
msgstr "音量"
|
||||
|
||||
msgid "Where your music files are stored (can be a mount point, e.g. /mnt/sda1/music)."
|
||||
msgstr "音乐文件存放位置(可以是挂载点,例如 /mnt/sda1/music)。"
|
||||
|
||||
msgid "auto-detect"
|
||||
msgstr "自动探测"
|
||||
|
||||
msgid "loading..."
|
||||
msgstr "加载中…"
|
||||
|
||||
msgid "running"
|
||||
msgstr "运行中"
|
||||
|
||||
msgid "stopped"
|
||||
msgstr "已停止"
|
||||
|
||||
msgid "type to filter..."
|
||||
msgstr "输入关键词筛选…"
|
||||
|
||||
msgid "use hostname"
|
||||
msgstr "使用主机名"
|
||||
@@ -0,0 +1,8 @@
|
||||
config audio 'main'
|
||||
option enabled '0'
|
||||
option device ''
|
||||
option music_dir '/srv/music'
|
||||
option volume '40'
|
||||
option dlna_name ''
|
||||
option unmute '1'
|
||||
option persist_volume '1'
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# DLNA Music Player - 声卡初始化 + MPD/upmpdcli 配置
|
||||
# 在 MPD 之前运行,负责把声卡准备好
|
||||
|
||||
START=50
|
||||
STOP=99
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
MUSIC_DIR_DEFAULT="/srv/music"
|
||||
MPD_CONF="/etc/mpd.conf"
|
||||
MPD_STATE_DIR="/etc/mpd"
|
||||
|
||||
log() { logger -t dlna-player "$@"; }
|
||||
|
||||
# 刷新前端文件 mtime,避免浏览器因"文件没变"而使用旧缓存
|
||||
refresh_web_cache() {
|
||||
touch /www/luci-static/resources/view/dlna-player2.js 2>/dev/null
|
||||
rm -f /tmp/luci-indexcache* 2>/dev/null
|
||||
}
|
||||
|
||||
# ---------- 自动探测声卡 ----------
|
||||
detect_device() {
|
||||
local dev
|
||||
|
||||
# 1) 优先选名字里带 Analog 的(板载模拟输出)
|
||||
dev=$(aplay -l 2>/dev/null \
|
||||
| grep -i 'analog' \
|
||||
| head -n1 \
|
||||
| sed -n 's/^card \([0-9]*\): .*device \([0-9]*\):.*/hw:\1,\2/p')
|
||||
[ -n "$dev" ] && { echo "$dev"; return 0; }
|
||||
|
||||
# 2) 退而求其次:排除 Dummy / pcsp / HDMI,取第一个可播放设备
|
||||
dev=$(aplay -l 2>/dev/null \
|
||||
| grep '^card ' \
|
||||
| grep -viE 'dummy|pcsp|hdmi|iec958' \
|
||||
| head -n1 \
|
||||
| sed -n 's/^card \([0-9]*\): .*device \([0-9]*\):.*/hw:\1,\2/p')
|
||||
[ -n "$dev" ] && { echo "$dev"; return 0; }
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------- 等声卡就绪 ----------
|
||||
wait_soundcard() {
|
||||
local i=0
|
||||
while [ $i -lt 30 ]; do
|
||||
[ -d /proc/asound ] && aplay -l >/dev/null 2>&1 && return 0
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# ---------- 生成 mpd.conf ----------
|
||||
gen_mpd_conf() {
|
||||
local dev="$1" music="$2"
|
||||
|
||||
cat > "$MPD_CONF" <<EOF
|
||||
# Generated by luci-app-dlna-player - do not edit manually
|
||||
music_directory "$music"
|
||||
playlist_directory "/var/lib/mpd/playlists"
|
||||
db_file "/var/lib/mpd/mpd.db"
|
||||
log_file "syslog"
|
||||
pid_file "/var/run/mpd.pid"
|
||||
state_file "$MPD_STATE_DIR/mpdstate"
|
||||
bind_to_address "127.0.0.1"
|
||||
port "6600"
|
||||
auto_update "yes"
|
||||
|
||||
audio_output {
|
||||
type "alsa"
|
||||
name "Analog Output"
|
||||
device "$dev"
|
||||
mixer_type "software"
|
||||
}
|
||||
|
||||
input {
|
||||
plugin "curl"
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
start() {
|
||||
local enabled device music volume dlna unmute persist
|
||||
|
||||
config_load dlna-player
|
||||
config_get enabled main enabled 1
|
||||
config_get device main device ''
|
||||
config_get music main music_dir "$MUSIC_DIR_DEFAULT"
|
||||
config_get volume main volume '40'
|
||||
config_get dlna main dlna_name ''
|
||||
config_get unmute main unmute '1'
|
||||
config_get persist main persist_volume '1'
|
||||
|
||||
[ "$enabled" = "1" ] || {
|
||||
# 关闭时:不只是"不配置",还要真正停掉服务
|
||||
log "disabled - stopping services"
|
||||
/etc/init.d/mpd stop 2>/dev/null
|
||||
/etc/init.d/mpd disable 2>/dev/null
|
||||
/etc/init.d/upmpdcli stop 2>/dev/null
|
||||
/etc/init.d/upmpdcli disable 2>/dev/null
|
||||
refresh_web_cache
|
||||
return 0
|
||||
}
|
||||
|
||||
# 启用时:确保两个服务开机自启,并且现在就跑起来
|
||||
/etc/init.d/mpd enable 2>/dev/null
|
||||
/etc/init.d/upmpdcli enable 2>/dev/null
|
||||
pidof mpd >/dev/null 2>&1 || /etc/init.d/mpd start 2>/dev/null
|
||||
# upmpdcli 用 restart:既保证运行,又能让新的 DLNA 名称生效
|
||||
/etc/init.d/upmpdcli restart 2>/dev/null
|
||||
|
||||
# --- 1. 等声卡 ---
|
||||
wait_soundcard
|
||||
|
||||
# --- 2. 探测设备 ---
|
||||
if [ -z "$device" ]; then
|
||||
device=$(detect_device)
|
||||
if [ -z "$device" ]; then
|
||||
log "no usable sound card found!"
|
||||
return 1
|
||||
fi
|
||||
log "auto-detected sound card: $device"
|
||||
else
|
||||
log "using configured sound card: $device"
|
||||
fi
|
||||
|
||||
# --- 2.1 检查 PulseAudio 冲突 ---
|
||||
# PulseAudio 会抢占 ALSA 设备,导致 MPD 无声
|
||||
if pidof pulseaudio >/dev/null 2>&1; then
|
||||
log "WARNING: pulseaudio is running and may grab the sound card!"
|
||||
log "WARNING: run '/etc/init.d/pulseaudio stop; /etc/init.d/pulseaudio disable' if MPD has no sound"
|
||||
fi
|
||||
|
||||
# --- 3. 声卡权限(/dev 是 tmpfs,每次开机都要修)---
|
||||
chmod 666 /dev/snd/* 2>/dev/null
|
||||
|
||||
# --- 4. 恢复混音器状态 + 解除静音 ---
|
||||
# 先尝试恢复上次保存的混音器状态(/etc 是持久目录)
|
||||
[ -f /etc/alsa/asound.state ] && alsactl restore -f /etc/alsa/asound.state 2>/dev/null
|
||||
|
||||
if [ "$unmute" = "1" ]; then
|
||||
local card="${device#hw:}" # 3,0
|
||||
card="${card%%,*}" # 3
|
||||
amixer -c "$card" sset 'Master' 100% unmute 2>/dev/null
|
||||
fi
|
||||
|
||||
# --- 5. 音乐目录 ---
|
||||
[ -d "$music" ] || mkdir -p "$music"
|
||||
chmod 755 "$music"
|
||||
chown -R mpd:mpd "$music" 2>/dev/null
|
||||
|
||||
# --- 6. MPD 运行目录(/var 是 tmpfs)---
|
||||
mkdir -p /var/lib/mpd/playlists
|
||||
chown -R mpd:mpd /var/lib/mpd 2>/dev/null
|
||||
mkdir -p "$MPD_STATE_DIR"
|
||||
|
||||
# --- 6.1 确保 MPD 以 root 运行 ---
|
||||
# 原因:PulseAudio 等其他服务会重置 /dev/snd 权限,mpd 用户可能打不开声卡
|
||||
# 每次开机都检查,避免 MPD 包升级后补丁丢失
|
||||
if [ -f /etc/init.d/mpd ]; then
|
||||
if grep -q '^USER="mpd"' /etc/init.d/mpd; then
|
||||
sed -i 's/^USER="mpd"/USER="root"/; s/^GROUP="mpd"/GROUP="root"/' /etc/init.d/mpd
|
||||
log "patched mpd init to run as root"
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- 7. 生成 mpd.conf ---
|
||||
gen_mpd_conf "$device" "$music"
|
||||
|
||||
# --- 8. upmpdcli 名称 ---
|
||||
[ -z "$dlna" ] && dlna="$(uci get system.@system[0].hostname 2>/dev/null)"
|
||||
[ -z "$dlna" ] && dlna="OpenWrt Audio"
|
||||
uci -q set upmpdcli.lan=upmpdcli
|
||||
uci -q set upmpdcli.lan.interface='br-lan'
|
||||
uci -q set upmpdcli.lan.friendly_name="$dlna"
|
||||
uci -q commit upmpdcli
|
||||
|
||||
log "dlna-player done (device=$device, volume=$volume)"
|
||||
refresh_web_cache
|
||||
}
|
||||
|
||||
stop() {
|
||||
# 关机时保存混音器状态(/etc 是持久目录,重启后能恢复)
|
||||
mkdir -p /etc/alsa 2>/dev/null
|
||||
alsactl store -f /etc/alsa/asound.state 2>/dev/null
|
||||
log "stopped, mixer state saved"
|
||||
}
|
||||
|
||||
reload() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Audio Volume - 在 MPD 启动后设置默认音量
|
||||
# 注意:仅在"没有持久状态文件"时设置,避免覆盖用户手动调过的音量
|
||||
|
||||
START=94
|
||||
STOP=98
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
log() { logger -t dlna-player-volume "$@"; }
|
||||
|
||||
start() {
|
||||
local enabled volume persist
|
||||
|
||||
config_load dlna-player
|
||||
config_get enabled main enabled 1
|
||||
config_get volume main volume '40'
|
||||
config_get persist main persist_volume '1'
|
||||
|
||||
[ "$enabled" = "1" ] || return 0
|
||||
|
||||
# 等 MPD 起来
|
||||
local i=0
|
||||
while [ $i -lt 15 ]; do
|
||||
mpc status >/dev/null 2>&1 && break
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
# 有持久状态就不动它
|
||||
if [ "$persist" = "1" ] && [ -f /etc/mpd/mpdstate ]; then
|
||||
log "persistent state found, keep user volume"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mpc volume "$volume" 2>/dev/null
|
||||
log "set default volume to ${volume}%"
|
||||
}
|
||||
|
||||
stop() { :; }
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
# 首次启动时执行一次:启用各服务
|
||||
|
||||
# 启用音频初始化脚本(START=50,早于 mpd)
|
||||
/etc/init.d/dlna-player enable
|
||||
|
||||
# 创建音乐目录
|
||||
MUSIC_DIR="$(uci -q get dlna-player.main.music_dir)"
|
||||
[ -z "$MUSIC_DIR" ] && MUSIC_DIR="/srv/music"
|
||||
mkdir -p "$MUSIC_DIR"
|
||||
chmod 755 "$MUSIC_DIR"
|
||||
|
||||
# MPD 需要以 root 运行(否则打不开声卡)
|
||||
if [ -f /etc/init.d/mpd ]; then
|
||||
sed -i 's/^USER="mpd"/USER="root"/; s/^GROUP="mpd"/GROUP="root"/' /etc/init.d/mpd
|
||||
/etc/init.d/mpd enable
|
||||
fi
|
||||
|
||||
# DLNA 服务
|
||||
[ -f /etc/init.d/upmpdcli ] && /etc/init.d/upmpdcli enable
|
||||
|
||||
exit 0
|
||||
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"admin/services/dlna-player": {
|
||||
"title": "DLNA 音乐播放器",
|
||||
"order": 50,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "dlna-player2"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-dlna-player" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"luci-app-dlna-player": {
|
||||
"description": "Grant access to DLNA Music Player",
|
||||
"read": {
|
||||
"file": {
|
||||
"/usr/bin/aplay": [ "exec" ],
|
||||
"/usr/bin/mpc": [ "exec" ],
|
||||
"/bin/pidof": [ "exec" ],
|
||||
"/etc/init.d/dlna-player": [ "exec" ]
|
||||
},
|
||||
"uci": [ "dlna-player" ]
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/usr/bin/mpc": [ "exec" ],
|
||||
"/etc/init.d/dlna-player": [ "exec" ],
|
||||
"/etc/init.d/dlna-player-volume": [ "exec" ],
|
||||
"/etc/init.d/upmpdcli": [ "exec" ],
|
||||
"/etc/init.d/mpd": [ "exec" ]
|
||||
},
|
||||
"uci": [ "dlna-player" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# 统一换行:仓库内存 LF,避免 Windows 上 checkout 出 CRLF 破坏 shell/Lua 脚本
|
||||
* text=auto eol=lf
|
||||
|
||||
# 二进制:不做任何转换
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.jpeg binary
|
||||
*.gif binary
|
||||
*.ico binary
|
||||
|
||||
# 明确脚本类型(在这些设备上必须是 LF,否则 #!/bin/sh 会带 \r 直接执行失败)
|
||||
*.sh text eol=lf
|
||||
*.lua text eol=lf
|
||||
*.htm text eol=lf
|
||||
dnsguard-apply text eol=lf
|
||||
dnsguard-selftest text eol=lf
|
||||
dnsguard-rollback text eol=lf
|
||||
dnsguard text eol=lf
|
||||
@@ -0,0 +1,23 @@
|
||||
# 临时/生成物
|
||||
*.bak
|
||||
*.bak-*
|
||||
*.orig
|
||||
*.rej
|
||||
*.swp
|
||||
*~
|
||||
__pycache__/
|
||||
*.pyc
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# 编辑器
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# 本地部署残留(deploy.sh 打包产物)
|
||||
/tmp/
|
||||
*.tgz
|
||||
|
||||
# 敏感物:绝不入库
|
||||
.env
|
||||
*.secret
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 dellzou (https://xiaozou123.cn)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,141 @@
|
||||
# luci-app-dnsguard —— DNS 防绕过(LuCI 应用)
|
||||
|
||||
把局域网内「绕过 AdGuard Home 的 DNS」拉回本机解析:**53 明文用透明重定向**(客户端无感),
|
||||
**853 DoT 用阻断**(逼其回落 53),**DoH 专用 IP** 可选阻断。全部可在 WebUI 开关。
|
||||
|
||||
- 入口:**LuCI → 服务 → DNS 防绕过**(`admin/services/dnsguard`)
|
||||
- 配置:`/etc/config/dnsguard`
|
||||
- 渲染产物:`/etc/nftables.d/20-dns-guard.nft`(由 fw4 在 `inet/fw4` 表上下文内 include)
|
||||
- 回滚点:`/mnt/data/backup-dnsguard/<时间戳>/`(含 `20-dns-guard.nft` + `dnsguard.uci`)
|
||||
|
||||
## 为什么需要它
|
||||
|
||||
OpenWrt 自带的「DNS 重定向」开关重定向到的是 **dnsmasq 自己的端口**。本机架构里 dnsmasq 已退居
|
||||
5353、53 由 AdGuard Home 接管,所以打开那个开关会让**全屋绕过 AGH、广告过滤失效**;
|
||||
而且它只匹配 `udp dport 53`,TCP 不覆盖。本应用是它的正确替代实现:
|
||||
|
||||
| 项 | OpenWrt 自带开关 | 本应用 |
|
||||
|---|---|---|
|
||||
| 重定向目标 | dnsmasq 的 `port`(本机 = 5353)❌ | AdGuard Home 的 `:53` ✅ |
|
||||
| 协议覆盖 | 仅 UDP | TCP + UDP |
|
||||
| 目标过滤 | 无(跨子网也劫持) | 排除内网/保留段 + 可配设备例外 |
|
||||
| 前置校验 | 无 | `nft -c` + `fw4 check` + 应用后核对链 |
|
||||
| 回滚 | 无 | 回滚点 + 延迟自动回滚 |
|
||||
|
||||
## 文件清单
|
||||
|
||||
| 路径(设备) | 作用 |
|
||||
|---|---|
|
||||
| `/etc/config/dnsguard` | UCI 配置(`main` / `limits` 两个节) |
|
||||
| `/usr/bin/dnsguard-apply` | **唯一写防火墙的地方**:UCI → 渲染 → 校验 → 落盘 → `fw4 reload` |
|
||||
| `/usr/bin/dnsguard-selftest` | 健康自检(AGH 进程 / :53 监听 / 解析 / 链与计数 / 回滚倒计时) |
|
||||
| `/usr/bin/dnsguard-rollback` | 回滚点管理 + 延迟自动回滚 |
|
||||
| `/etc/init.d/dnsguard` | `reload` / `stop`(boot 为空实现,开机不需要它做事) |
|
||||
| `controller/dnsguard.lua`、`view/dnsguard/main.htm`、`acl.d/luci-app-dnsguard.json` | 界面 |
|
||||
|
||||
## 关键技术点
|
||||
|
||||
### 1. 规则写在哪、优先级怎么排
|
||||
|
||||
`/etc/nftables.d/*.nft` 的文件是**被 include 进 `table inet fw4` 内部**的(见设备上 `/etc/nftables.d/README`),
|
||||
所以文件里只写 `set` / `chain`,**不要写 `table` 包裹**。
|
||||
|
||||
fw4 自己的 DNAT 链是 `dstnat`(`priority dstnat` = **-100**)。我们的重定向链用 `priority -105`,
|
||||
在它**之前**执行:
|
||||
|
||||
```nft
|
||||
chain dns_guard_redirect {
|
||||
type nat hook prerouting priority -105; policy accept;
|
||||
iifname "br-lan" meta l4proto { tcp, udp } th dport 53 ip daddr != @dns_never_hijack4 counter redirect to :53
|
||||
iifname "br-lan" meta l4proto { tcp, udp } th dport 53 ip6 daddr != @dns_never_hijack6 counter redirect to :53
|
||||
}
|
||||
```
|
||||
|
||||
`redirect to :53`(只给端口)= 改道到本机 :53。目标写死 AGH 的端口,**不是** dnsmasq 的 5353。
|
||||
|
||||
### 2. 「永不劫持」集合里不要放 192.168.0.0/16
|
||||
|
||||
默认排除 `192.168.1.0/24`、`172.16.0.0/12`、`127.0.0.0/8`、`169.254.0.0/16`、`224.0.0.0/4`。
|
||||
|
||||
> ⚠️ 仓库里带的默认配置用的是 `192.168.1.0/24` 这个**占位网段**。`install.sh` 在首次安装时会自动读
|
||||
> `br-lan` 的 IPv4 地址、算出真实网段并替换它(不影响已有配置)。如果你不是用 `install.sh` 装的,
|
||||
> 请到界面把「永不劫持(IPv4)」改成你自己的 LAN 网段。
|
||||
**故意不排除整个 `192.168.0.0/16`**:红米 K80 的静态 DNS 指向旧网段 `192.168.5.1`(该网段已不存在),
|
||||
若把 192.168/16 全排除,它会继续"DNS 打空";现在被重定向后反而恢复正常解析。
|
||||
|
||||
### 3. 安全闸门与三层校验
|
||||
|
||||
- **闸门**:`redirect53=1` 而 `AdGuardHome` 进程不存在 → **直接拒绝应用**(exit 3,不落盘)
|
||||
- **校验 1**:渲染片段包进临时表名做 `nft -c`,不通过绝不落盘
|
||||
- **校验 2**:落盘后 `fw4 check`(fw4 自带的全量校验器),不通过立即还原旧文件
|
||||
- **校验 3**:`fw4 reload` 后核对 `dns_guard_redirect` 链真的存在,不存在则还原 + 重载
|
||||
|
||||
### 4. 延迟自动回滚
|
||||
|
||||
DNS 是全家网的命门:改错了人可能连这台路由器都**访问不到**(域名解析不了)。
|
||||
所以「53 重定向」从**关改到开**时,控制器会:
|
||||
|
||||
1. 先把**当前 UCI + 当前规则文件**拍成回滚点(必须在写 UCI **之前**拍,否则快照里就是新配置)
|
||||
2. 起一个 `setsid` 倒计时进程(默认 10 分钟)
|
||||
3. 到点未点「确认保留」→ 自动还原 UCI + 规则文件 + `fw4 reload`
|
||||
|
||||
`/tmp/.dnsguard-rollback` 是唯一真相:`--disarm` 删掉它,倒计时进程醒来发现记录没了就什么都不做(僵尸进程无害)。
|
||||
|
||||
### 5. 幂等
|
||||
|
||||
文件头有「生成时间」,所以比较时**先剔除该行**再 `cmp`——否则每次都比出差异、白白重载一次防火墙。
|
||||
验收:连跑两次 `dnsguard-apply`,第二次必须报 `file:"unchanged"`。
|
||||
|
||||
## 命令行速查
|
||||
|
||||
```sh
|
||||
# 渲染并应用(输出一行 JSON)
|
||||
/usr/bin/dnsguard-apply
|
||||
|
||||
# 只渲染,不碰防火墙(界面「预览将要生成的规则」用的就是它)
|
||||
/usr/bin/dnsguard-apply --render-only --out /tmp/x.nft
|
||||
|
||||
# 健康自检
|
||||
/usr/bin/dnsguard-selftest
|
||||
|
||||
# 回滚
|
||||
/usr/bin/dnsguard-rollback # 列出回滚点
|
||||
/usr/bin/dnsguard-rollback latest # 还原到最近一个
|
||||
/usr/bin/dnsguard-rollback 20260926-182913
|
||||
/usr/bin/dnsguard-rollback --arm 10 # 挂 10 分钟自动回滚
|
||||
/usr/bin/dnsguard-rollback --disarm # 确认保留
|
||||
|
||||
# 服务
|
||||
/etc/init.d/dnsguard reload
|
||||
/etc/init.d/dnsguard stop # 临时摘链(配置不动,fw4 reload 后恢复)
|
||||
```
|
||||
|
||||
## 部署 / 回滚
|
||||
|
||||
```sh
|
||||
export PATH="/c/Users/27970/.workbuddy/binaries/PortableGit/versions/1.2.0/usr/bin:$PATH"
|
||||
sh deploy.sh --check # 静态检查(shell / JSON / Lua 控制器 / 模板内嵌 Lua)
|
||||
sh deploy.sh --install # 打包上传安装(会先备份旧文件)
|
||||
```
|
||||
|
||||
**安装阶段不碰防火墙**——规则文件只会在界面点「保存并应用」时生成。
|
||||
|
||||
回滚(install.sh 也会打印):
|
||||
|
||||
```sh
|
||||
cp -r /mnt/data/backup-dnsguard/<安装时间戳>/* / \
|
||||
&& rm -f /usr/lib/lua/luci/controller/dnsguard.lua \
|
||||
&& rm -rf /usr/lib/lua/luci/view/dnsguard /usr/share/rpcd/acl.d/luci-app-dnsguard.json /etc/init.d/dnsguard \
|
||||
&& rm -f /tmp/luci-indexcache*
|
||||
# 再删掉重定向链:
|
||||
nft delete chain inet fw4 dns_guard_redirect 2>/dev/null; fw4 reload
|
||||
```
|
||||
|
||||
## 开发时的三个坑(都真踩过)
|
||||
|
||||
1. **`while read` 丢末行**:`printf '%s' "$list" | ... | while read e` 会把**最后一个元素**丢掉,
|
||||
必须 `printf '%s\n'`。症状是 `224.0.0.0/4`、`140.207.198.6` 之类的**最后一个元素**神秘消失。
|
||||
2. **Lua 白名单漏 `/`**:控制器里清洗地址用的 `gsub("[^%w%.%:%-%s%,]", "")` 会把 CIDR 洗成
|
||||
`192.168.1.024`(打桩测试抓到的)。白名单必须含 `/`。
|
||||
3. **shell 里给变量赋值 PATH 在部分环境下有坑**:`PATH=/x:$PATH cmd` 在某些封装 shell 里会被改写成
|
||||
带 `$( )` 的形式而语法报错;测试时改用 `/usr/bin/env PATH=... cmd` 更稳。
|
||||
@@ -0,0 +1,31 @@
|
||||
0.1.0 (2026-09-26)
|
||||
|
||||
target : iStoreOS 25.12.5 (x86_64, apk) / LuCI 26.253 + luci-compat / fw4 + nftables 1.1.6 / kernel 6.12.94
|
||||
form : 纯文件 LuCI 应用(免 SDK 免编译)+ 三个 POSIX shell 脚本
|
||||
|
||||
功能:
|
||||
- 53 透明重定向 -> AdGuard Home(nat prerouting priority -105,早于 fw4 dstnat -100)
|
||||
- 853(DoT) 阻断 / DoH 专用 IP 阻断(可选,默认关)
|
||||
- 观察计数链(沿用阶段 1,只计数 + 抽样记日志)
|
||||
- 健康自检、规则预览、回滚点管理、延迟自动回滚(DNS 变更的安全绳)
|
||||
- 安全闸门:AdGuardHome 未运行时拒绝启用重定向;渲染片段先 nft -c 校验,落盘后 fw4 check 全量校验,
|
||||
应用后核对链是否真的存在,任一环节失败即自动还原旧规则
|
||||
- install.sh 首次安装会读 br-lan 的 IPv4 地址、算出真实 LAN 网段,替换默认配置里的占位网段
|
||||
|
||||
repo note:
|
||||
- 公开仓库里的默认配置使用 192.168.1.0/24 作占位网段,真实部署值以设备上的 UCI 为准
|
||||
- deploy.sh / install.sh 中出现的路由器地址是通用占位值,请按自己的环境替换
|
||||
- 不含任何截图二进制;README 中的图走博客图床外链
|
||||
|
||||
verified on device:
|
||||
- 静态检查(shell / JSON / Lua 控制器 / 模板内嵌 Lua)
|
||||
- nft 片段语法预检(临时表包裹,nft -c)通过
|
||||
- fw4 check 全量校验通过
|
||||
- UCI 解析自检 + 首次安装迁移现有防火墙状态
|
||||
- 渲染脚本幂等(第二次报 unchanged,不重载防火墙)
|
||||
- 53 重定向端到端:LAN 客户端查询 8.8.8.8 被收编到 AGH(计数增加、AGH 查询日志可见)
|
||||
- 例外设备(skip_devices)写入 dns_hijack_skip4/6 后确实不被重定向
|
||||
- 延迟自动回滚:--arm 后 --disarm 可取消;不取消则到点自动还原 UCI + 规则文件
|
||||
- AGH 停机时 dnsguard-apply 返回 status=error 且不落盘(安全闸门生效)
|
||||
- 控制器打桩直调 action_api:12 个分支全部返回合法 JSON
|
||||
- LAN 网段探测:/24、/16、/8、/12、/25、/30 六种前缀算出的网络地址均正确
|
||||
Executable
+96
@@ -0,0 +1,96 @@
|
||||
#!/bin/sh
|
||||
# deploy.sh —— 把「DNS 防绕过」LuCI 应用从电脑部署到路由器(在电脑上执行)
|
||||
#
|
||||
# ./deploy.sh --check 只做语法检查,不往路由器写任何文件
|
||||
# ./deploy.sh --install 上传并安装(会先备份旧文件到 /mnt/data/backup-dnsguard/)
|
||||
#
|
||||
# 生成:WorkBuddy 2026-09-26
|
||||
# 注意:本机 Git Bash 的 PATH 里可能没有 coreutils,先执行
|
||||
# export PATH="/c/Users/27970/.workbuddy/binaries/PortableGit/versions/1.2.0/usr/bin:$PATH"
|
||||
|
||||
set -u
|
||||
DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
MODE="${1:---check}"
|
||||
LUA_CTRL="$DIR/root/usr/lib/lua/luci/controller/dnsguard.lua"
|
||||
LUA_VIEW="$DIR/root/usr/lib/lua/luci/view/dnsguard/main.htm"
|
||||
SH_APPLY="$DIR/root/usr/bin/dnsguard-apply"
|
||||
SH_TEST="$DIR/root/usr/bin/dnsguard-selftest"
|
||||
SH_ROLL="$DIR/root/usr/bin/dnsguard-rollback"
|
||||
SH_INIT="$DIR/root/etc/init.d/dnsguard"
|
||||
SH_INSTALL="$DIR/install.sh"
|
||||
|
||||
echo "== 1) 本地 shell 语法检查 =="
|
||||
for f in "$SH_APPLY" "$SH_TEST" "$SH_ROLL" "$SH_INIT" "$SH_INSTALL"; do
|
||||
if [ -f "$f" ] && sh -n "$f" 2>/tmp/.dg-sh.err; then
|
||||
echo " OK $(basename "$f")"
|
||||
else
|
||||
echo " FAIL $(basename "$f")"; cat /tmp/.dg-sh.err 2>/dev/null; exit 1
|
||||
fi
|
||||
done
|
||||
rm -f /tmp/.dg-sh.err
|
||||
|
||||
echo "== 2) JSON 检查(acl 文件)=="
|
||||
if command -v python >/dev/null 2>&1; then
|
||||
python -c "import json,sys;json.load(sys.stdin);print(' OK acl json')" \
|
||||
< "$DIR/root/usr/share/rpcd/acl.d/luci-app-dnsguard.json" || exit 1
|
||||
else
|
||||
echo " SKIP 没有 python,跳过(内容很短,可肉眼核对)"
|
||||
fi
|
||||
|
||||
echo "== 3) Lua 语法检查(借路由器上的 lua 解释器,只读 stdin,不在设备上写文件)=="
|
||||
if ssh -o ConnectTimeout=8 router 'command -v lua >/dev/null 2>&1'; then
|
||||
if ssh router 'lua -e "local s=io.read(\"*a\"); local f,e=loadstring(s); if f then print(\" OK controller\") else print(\" FAIL \"..tostring(e)); os.exit(1) end"' < "$LUA_CTRL"; then
|
||||
echo " controller 语法通过"
|
||||
else
|
||||
echo " controller 语法失败"; exit 1
|
||||
fi
|
||||
# 视图里的 Lua 只取 <% ... %> 块(跳过 <%+include%> 与 <%=expr%>)
|
||||
awk '/^<%/ && $0 !~ /^<%[+="]/ { inb=1; sub(/^<%/,"") } inb { if (/%>/) { sub(/%>.*/,""); print; inb=0 } else print }' "$LUA_VIEW" > /tmp/.dg-view.lua
|
||||
echo " (视图内嵌 Lua 片段 $(wc -l < /tmp/.dg-view.lua) 行)"
|
||||
if ssh router 'lua -e "local s=io.read(\"*a\"); local f,e=loadstring(s); if f then print(\" OK view-lua\") else print(\" FAIL \"..tostring(e)); os.exit(1) end"' < /tmp/.dg-view.lua; then
|
||||
echo " 视图 Lua 语法通过"
|
||||
else
|
||||
echo " 视图 Lua 语法失败"; exit 1
|
||||
fi
|
||||
rm -f /tmp/.dg-view.lua
|
||||
else
|
||||
echo " SKIP 路由器上没有 lua 命令"
|
||||
fi
|
||||
|
||||
echo "== 4) 渲染脚本语法自检(在设备上以 --render-only 跑一次,不碰防火墙)=="
|
||||
if [ "$MODE" = "--check" ]; then
|
||||
if ssh -o ConnectTimeout=8 router 'test -x /usr/bin/dnsguard-apply'; then
|
||||
ssh router '/usr/bin/dnsguard-apply --render-only --out /tmp/dg-check.nft' 2>&1 | sed 's/^/ /'
|
||||
ssh router 'rm -f /tmp/dg-check.nft'
|
||||
else
|
||||
echo " SKIP 尚未安装到设备(安装后再跑本项)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "--check" ]; then
|
||||
echo
|
||||
echo "全部检查通过(未改动路由器上的任何文件)。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$MODE" != "--install" ]; then
|
||||
echo "未知参数:$MODE(可用 --check / --install)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "== 5) 打包上传 =="
|
||||
TARBALL=/tmp/luci-app-dnsguard.tgz
|
||||
STAGE=/tmp/.dg-stage.$$
|
||||
rm -rf "$STAGE"; mkdir -p "$STAGE"
|
||||
cp -r "$DIR/root" "$DIR/install.sh" "$STAGE/"
|
||||
tar czf "$TARBALL" -C "$STAGE" .
|
||||
rm -rf "$STAGE"
|
||||
echo " 包大小:$(wc -c < "$TARBALL") 字节"
|
||||
cat "$TARBALL" | ssh router 'cat > /tmp/luci-app-dnsguard.tgz'
|
||||
rm -f "$TARBALL"
|
||||
|
||||
echo "== 6) 在路由器上安装 =="
|
||||
ssh router 'cd /tmp && rm -rf /tmp/luci-app-dnsguard && mkdir -p /tmp/luci-app-dnsguard && tar xzf /tmp/luci-app-dnsguard.tgz -C /tmp/luci-app-dnsguard && sh /tmp/luci-app-dnsguard/install.sh'
|
||||
|
||||
echo
|
||||
echo "部署结束。请在浏览器打开 http://192.168.1.1 -> 服务 -> DNS 防绕过"
|
||||
Loaded 100 of 459 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user