mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-06 11:24:07 +08:00
This commit is contained in:
1 parent
0c172bd666
commit
fd75287766
13 files changed
+1088
-253
No files matched your search
+2
-2
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gately
|
||||
PKG_VERSION:=0.6.4
|
||||
PKG_RELEASE:=25
|
||||
PKG_VERSION:=0.6.4.1
|
||||
PKG_RELEASE:=26
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/wangwanjie/gately/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -70,6 +70,210 @@ function hasDotDotSegment(value) {
|
||||
return value.split('/').indexOf('..') !== -1;
|
||||
}
|
||||
|
||||
// ---- BEGIN new-release notice ------------------------------------------------
|
||||
// AdGuard Home's own /control/version.json needs HTTP basic auth (this page only
|
||||
// ever holds the password hash) and is served on another port, so the page asks
|
||||
// GitHub directly: the same source as the "Check update" button and update_core.sh.
|
||||
// The answer is cached in localStorage so opening the page does not cost an API
|
||||
// request every time (GitHub allows only 60 unauthenticated requests per hour).
|
||||
const STORAGE_KEY_LATEST = 'luci-app-adguardhome_latest_release';
|
||||
const RELEASE_CACHE_TTL = 6 * 60 * 60 * 1000; // after a successful check
|
||||
const RELEASE_RETRY_DELAY = 30 * 60 * 1000; // after a failed check
|
||||
const RELEASE_FETCH_TIMEOUT = 8000;
|
||||
|
||||
function storageGet(key) {
|
||||
try {
|
||||
return window.localStorage.getItem(key);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function storageSet(key, value) {
|
||||
try {
|
||||
window.localStorage.setItem(key, value);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// "v0.107.57-b.2" -> { nums: [0, 107, 57], pre: ['b', '2'] }, or null
|
||||
function parseVersion(str) {
|
||||
const m = /v?(\d+)\.(\d+)\.(\d+)(?:-([A-Za-z0-9.]+))?/.exec(String(str || ''));
|
||||
if (!m) {
|
||||
return null;
|
||||
}
|
||||
return { nums: [+m[1], +m[2], +m[3]], pre: m[4] ? m[4].split('.') : [] };
|
||||
}
|
||||
|
||||
function formatVersion(v) {
|
||||
return 'v' + v.nums.join('.') + (v.pre.length ? '-' + v.pre.join('.') : '');
|
||||
}
|
||||
|
||||
// Comparator result (<0, 0, >0). A pre-release sorts below its release.
|
||||
function compareVersions(a, b) {
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (a.nums[i] !== b.nums[i]) {
|
||||
return a.nums[i] - b.nums[i];
|
||||
}
|
||||
}
|
||||
if (!a.pre.length && !b.pre.length) {
|
||||
return 0;
|
||||
}
|
||||
if (!a.pre.length) {
|
||||
return 1;
|
||||
}
|
||||
if (!b.pre.length) {
|
||||
return -1;
|
||||
}
|
||||
const n = Math.max(a.pre.length, b.pre.length);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const x = a.pre[i];
|
||||
const y = b.pre[i];
|
||||
if (x === undefined) {
|
||||
return -1;
|
||||
}
|
||||
if (y === undefined) {
|
||||
return 1;
|
||||
}
|
||||
if (x === y) {
|
||||
continue;
|
||||
}
|
||||
const xNum = /^\d+$/.test(x);
|
||||
const yNum = /^\d+$/.test(y);
|
||||
if (xNum && yNum) {
|
||||
return +x - +y;
|
||||
}
|
||||
if (xNum) {
|
||||
return -1;
|
||||
}
|
||||
if (yNum) {
|
||||
return 1;
|
||||
}
|
||||
return x < y ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Newest release of the channel ('latest' or 'beta') as { tag, url }, cached.
|
||||
// "force" ignores the cache (used by the manual "Check update" button).
|
||||
// Rejects when GitHub cannot be reached or the answer is not what we expect.
|
||||
async function getLatestRelease(channel, force) {
|
||||
const cacheKey = STORAGE_KEY_LATEST + '_' + channel;
|
||||
const now = Date.now();
|
||||
let cached = null;
|
||||
try {
|
||||
cached = JSON.parse(storageGet(cacheKey) || 'null');
|
||||
} catch (e) {
|
||||
cached = null;
|
||||
}
|
||||
|
||||
if (!force && cached && typeof cached.time === 'number') {
|
||||
if (cached.failed) {
|
||||
if (now - cached.time < RELEASE_RETRY_DELAY) {
|
||||
throw new Error('Recent check failed');
|
||||
}
|
||||
} else if (now - cached.time < RELEASE_CACHE_TTL &&
|
||||
VERSION_TAG_REGEX.test(cached.tag) &&
|
||||
GITHUB_RELEASE_URL_REGEX.test(cached.url)) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const api = 'https://api.github.com/repos/AdguardTeam/AdGuardHome/releases' +
|
||||
(channel === 'beta' ? '?per_page=1' : '/latest');
|
||||
const ctrl = (typeof AbortController === 'function') ? new AbortController() : null;
|
||||
const timer = ctrl ? window.setTimeout(function() { ctrl.abort(); }, RELEASE_FETCH_TIMEOUT) : null;
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(api, ctrl ? { signal: ctrl.signal } : undefined);
|
||||
} finally {
|
||||
if (timer) {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error('HTTP ' + res.status);
|
||||
}
|
||||
let data = await res.json();
|
||||
if (Array.isArray(data)) {
|
||||
data = data[0];
|
||||
}
|
||||
if (!data || typeof data.tag_name !== 'string' || !VERSION_TAG_REGEX.test(data.tag_name)) {
|
||||
throw new Error('Invalid version data');
|
||||
}
|
||||
const url = (typeof data.html_url === 'string' && GITHUB_RELEASE_URL_REGEX.test(data.html_url))
|
||||
? data.html_url
|
||||
: 'https://github.com/AdguardTeam/AdGuardHome/releases/tag/' + data.tag_name;
|
||||
const entry = { tag: data.tag_name, url: url, time: now };
|
||||
storageSet(cacheKey, JSON.stringify(entry));
|
||||
return entry;
|
||||
} catch (e) {
|
||||
storageSet(cacheKey, JSON.stringify({ failed: true, time: now }));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
// Hint appended to the "Current Version" row; onUpdate runs when "Update" is clicked.
|
||||
// Built from DOM nodes only (no HTML strings).
|
||||
function buildVersionHint(release, onUpdate) {
|
||||
return E('span', { 'id': 'agh-version-hint', 'style': 'margin-left: 12px;' }, [
|
||||
E('strong', { 'style': 'color: var(--warn-color-high, #d9822b);' },
|
||||
'→ ' + _('New version %s available').replace('%s', release.tag)),
|
||||
' ',
|
||||
E('a', { 'href': release.url, 'target': '_blank', 'rel': 'noopener noreferrer' }, _('Release notes')),
|
||||
' | ',
|
||||
E('a', {
|
||||
'href': '#',
|
||||
'click': function(ev) {
|
||||
ev.preventDefault();
|
||||
onUpdate();
|
||||
}
|
||||
}, _('Update'))
|
||||
]);
|
||||
}
|
||||
|
||||
// Tighter spacing for the "Current Version" / "Service Status" rows. The themes
|
||||
// give every row a lot of padding; !important is needed to win over them.
|
||||
const STATUS_COMPACT_CSS =
|
||||
'.agh-status-row { margin: 0 !important; padding-top: 2px !important; padding-bottom: 2px !important; min-height: 0 !important; }' +
|
||||
'.agh-status-row .cbi-value-title, .agh-status-row .cbi-value-field { margin: 0 !important; padding-top: 0 !important; padding-bottom: 0 !important; min-height: 0 !important; line-height: 1.5 !important; }';
|
||||
|
||||
function findCoreUpdateTabLink(root) {
|
||||
return root.querySelector('a[href="#cbi-tab-adguardhome-core_update"]') ||
|
||||
root.querySelector('a[href="#core_update"]') ||
|
||||
root.querySelector('[data-tab="core_update"]');
|
||||
}
|
||||
|
||||
// The Core Update tab only exists while "Core Update" is ticked in General Settings.
|
||||
function isCoreUpdateTabEnabled(root) {
|
||||
const boxes = root.querySelectorAll('input[type="checkbox"]');
|
||||
for (let i = 0; i < boxes.length; i++) {
|
||||
if (boxes[i].id && boxes[i].id.endsWith('.enable_core_update')) {
|
||||
return boxes[i].checked;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Show the Core Update tab. If the tab is already available just switch to it;
|
||||
// otherwise switch the toggle on (it lives in sessionStorage) and reload, so the
|
||||
// tab is created and selected on the next load.
|
||||
function goToCoreUpdateTab(root) {
|
||||
if (isCoreUpdateTabEnabled(root)) {
|
||||
const link = findCoreUpdateTabLink(root);
|
||||
if (link) {
|
||||
link.click();
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
sessionStorage.setItem(STORAGE_KEY_CORE, '1');
|
||||
sessionStorage.setItem('agh_active_tab', 'core_update');
|
||||
} catch (e) {}
|
||||
window.location.reload();
|
||||
}
|
||||
// ---- END new-release notice --------------------------------------------------
|
||||
|
||||
function getServiceInfo(name) {
|
||||
const fn = rpc.declare({
|
||||
object: 'service',
|
||||
@@ -244,6 +448,7 @@ return view.extend({
|
||||
const yamlContent = data[2];
|
||||
|
||||
const coreExists = Boolean(version);
|
||||
const installed = parseVersion(version);
|
||||
let dnsPort = '53';
|
||||
if (yamlContent) {
|
||||
const portMatches = yamlContent.match(/port:\s*(\d+)/g);
|
||||
@@ -256,6 +461,7 @@ return view.extend({
|
||||
}
|
||||
|
||||
const sections = uci.sections('adguardhome', 'adguardhome');
|
||||
const savedChannel = (sections.length > 0 && sections[0].core_version === 'beta') ? 'beta' : 'latest';
|
||||
|
||||
const map = new form.Map('adguardhome', _('AdGuard Home'));
|
||||
|
||||
@@ -263,7 +469,7 @@ return view.extend({
|
||||
statusSect.anonymous = true;
|
||||
statusSect.cfgsections = function() { return ['status_section']; };
|
||||
|
||||
const versionOpt = statusSect.option(form.DummyValue, '_version', _('Version'));
|
||||
const versionOpt = statusSect.option(form.DummyValue, '_version', _('Current Version'));
|
||||
versionOpt.cfgvalue = function() {
|
||||
return version
|
||||
? esc(version)
|
||||
@@ -457,30 +663,131 @@ return view.extend({
|
||||
|
||||
const rendered = await map.render();
|
||||
|
||||
// Look elements up inside our own tree: it may not be attached to the document
|
||||
// yet when the background release check finishes.
|
||||
const byId = function(id) {
|
||||
return rendered.querySelector('#' + id);
|
||||
};
|
||||
|
||||
if (sessionStorage.getItem('agh_active_tab') === 'core_update') {
|
||||
sessionStorage.removeItem('agh_active_tab');
|
||||
window.location.hash = 'core_update';
|
||||
window.setTimeout(function() {
|
||||
const tabLink = rendered.querySelector('a[href="#cbi-tab-adguardhome-core_update"]') ||
|
||||
rendered.querySelector('a[href="#core_update"]') ||
|
||||
rendered.querySelector('[data-tab="core_update"]');
|
||||
const tabLink = findCoreUpdateTabLink(rendered);
|
||||
if (tabLink) tabLink.click();
|
||||
}, 50);
|
||||
}
|
||||
|
||||
// Compact "Current Version" / "Service Status" rows.
|
||||
const versionRow = map.findElement('data-field', versionOpt.cbid('status_section'));
|
||||
const statusRow = map.findElement('data-field', statusOpt.cbid('status_section'));
|
||||
if (versionRow) versionRow.classList.add('agh-status-row');
|
||||
if (statusRow) statusRow.classList.add('agh-status-row');
|
||||
rendered.appendChild(E('style', {}, STATUS_COMPACT_CSS));
|
||||
|
||||
const statusNode = map.findElement('data-field', statusOpt.cbid('status_section'));
|
||||
poll.add(updateStatus(statusNode), POLL_INTERVAL);
|
||||
|
||||
let updatePollId = null;
|
||||
let currentState = 'init';
|
||||
|
||||
// Channel ('latest' or 'beta') the Action button / version hint currently refer to.
|
||||
let detectedChannel = savedChannel;
|
||||
|
||||
function setDetail(node) {
|
||||
const detail = document.getElementById('btn-agh-detail');
|
||||
const detail = byId('btn-agh-detail');
|
||||
if (detail) {
|
||||
dom.content(detail, node || '');
|
||||
}
|
||||
}
|
||||
|
||||
// The channel selected in the Core Branch dropdown right now (saved one as fallback).
|
||||
function currentChannel() {
|
||||
try {
|
||||
const secId = sections.length > 0 ? sections[0]['.name'] : null;
|
||||
const v = secId ? coreVersionOpt.formvalue(secId) : null;
|
||||
if (CORE_VERSIONS.indexOf(v) !== -1) {
|
||||
return v;
|
||||
}
|
||||
} catch (err) {}
|
||||
return savedChannel;
|
||||
}
|
||||
|
||||
// "Click to update" state, shared by the manual and the background check.
|
||||
// release.url is always a github.com/AdguardTeam/AdGuardHome/releases URL (see getLatestRelease).
|
||||
function showUpdatable(release) {
|
||||
const btnAction = byId('btn-agh-action');
|
||||
const btnForce = byId('btn-agh-force');
|
||||
|
||||
currentState = 'updatable';
|
||||
if (btnAction) {
|
||||
btnAction.value = _('Click to update');
|
||||
btnAction.disabled = false;
|
||||
}
|
||||
if (btnForce) {
|
||||
btnForce.style.display = 'none';
|
||||
}
|
||||
setDetail(E('a', { 'href': release.url, 'target': '_blank', 'rel': 'noopener noreferrer' },
|
||||
E('em', { 'style': 'color: red;' }, release.tag)));
|
||||
}
|
||||
|
||||
// Back to the initial "Check update" state.
|
||||
function resetToCheck() {
|
||||
const btnAction = byId('btn-agh-action');
|
||||
const btnForce = byId('btn-agh-force');
|
||||
|
||||
currentState = 'init';
|
||||
if (btnAction) {
|
||||
btnAction.value = _('Check update');
|
||||
btnAction.disabled = false;
|
||||
}
|
||||
if (btnForce) {
|
||||
btnForce.style.display = 'none';
|
||||
}
|
||||
setDetail('');
|
||||
}
|
||||
|
||||
// Show (release) or remove (null) the new-version hint after the version number.
|
||||
function setVersionHint(release) {
|
||||
const field = versionRow
|
||||
? (versionRow.querySelector('output') || versionRow.querySelector('.cbi-value-field'))
|
||||
: null;
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
const old = field.querySelector('#agh-version-hint');
|
||||
if (old) {
|
||||
old.remove();
|
||||
}
|
||||
if (release) {
|
||||
field.appendChild(buildVersionHint(release, function() {
|
||||
goToCoreUpdateTab(rendered);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Background check: if a newer release exists, show the hint and, while the Action
|
||||
// button is still untouched, go straight to "Click to update".
|
||||
function detectNewRelease(channel) {
|
||||
if (!coreExists || !installed) {
|
||||
return;
|
||||
}
|
||||
getLatestRelease(channel).then(function(release) {
|
||||
if (channel !== detectedChannel) {
|
||||
return;
|
||||
}
|
||||
const latest = parseVersion(release.tag);
|
||||
if (!latest || compareVersions(latest, installed) <= 0) {
|
||||
setVersionHint(null);
|
||||
return;
|
||||
}
|
||||
setVersionHint(release);
|
||||
if (currentState === 'init') {
|
||||
showUpdatable(release);
|
||||
}
|
||||
}).catch(function() {});
|
||||
}
|
||||
|
||||
function startLogPolling() {
|
||||
if (updatePollId) clearInterval(updatePollId);
|
||||
|
||||
@@ -492,8 +799,8 @@ return view.extend({
|
||||
let targetPercent = 0;
|
||||
|
||||
const pollAction = function() {
|
||||
const btnAction = document.getElementById('btn-agh-action');
|
||||
const btnForce = document.getElementById('btn-agh-force');
|
||||
const btnAction = byId('btn-agh-action');
|
||||
const btnForce = byId('btn-agh-force');
|
||||
|
||||
if (currentState === 'updating') {
|
||||
if (displayPercent < targetPercent) {
|
||||
@@ -588,8 +895,8 @@ return view.extend({
|
||||
}
|
||||
|
||||
function triggerCheck() {
|
||||
const btnAction = document.getElementById('btn-agh-action');
|
||||
const btnForce = document.getElementById('btn-agh-force');
|
||||
const btnAction = byId('btn-agh-action');
|
||||
const btnForce = byId('btn-agh-force');
|
||||
|
||||
currentState = 'checking';
|
||||
if (btnAction) {
|
||||
@@ -598,66 +905,43 @@ return view.extend({
|
||||
}
|
||||
setDetail('');
|
||||
|
||||
fetch('https://api.github.com/repos/AdguardTeam/AdGuardHome/releases/latest')
|
||||
.then(function(res) {
|
||||
if (!res.ok) {
|
||||
throw new Error('HTTP ' + res.status);
|
||||
}
|
||||
return res.json();
|
||||
})
|
||||
.then(function(data) {
|
||||
const remoteVersion = (data && typeof data.tag_name === 'string') ? data.tag_name : null;
|
||||
if (!remoteVersion || !VERSION_TAG_REGEX.test(remoteVersion)) {
|
||||
throw new Error('Invalid version data');
|
||||
}
|
||||
const cleanLocal = coreExists ? version.replace(/^v/, '') : '';
|
||||
const cleanRemote = remoteVersion.replace(/^v/, '');
|
||||
const channel = currentChannel();
|
||||
getLatestRelease(channel, true).then(function(release) {
|
||||
detectedChannel = channel;
|
||||
const latest = parseVersion(release.tag);
|
||||
|
||||
if (!coreExists || cleanLocal !== cleanRemote) {
|
||||
currentState = 'updatable';
|
||||
if (btnAction) {
|
||||
btnAction.value = _('Click to update');
|
||||
btnAction.disabled = false;
|
||||
}
|
||||
if (btnForce) {
|
||||
btnForce.style.display = 'none';
|
||||
}
|
||||
|
||||
// Text only; link only if it points at the project's own releases.
|
||||
let node = E('em', { 'style': 'color: red;' }, remoteVersion);
|
||||
if (typeof data.html_url === 'string' && GITHUB_RELEASE_URL_REGEX.test(data.html_url)) {
|
||||
node = E('a', { 'href': data.html_url, 'target': '_blank', 'rel': 'noopener noreferrer' }, node);
|
||||
}
|
||||
setDetail(node);
|
||||
} else {
|
||||
currentState = 'latest';
|
||||
if (btnAction) {
|
||||
btnAction.value = _('It is the latest version');
|
||||
btnAction.disabled = true;
|
||||
}
|
||||
if (btnForce) {
|
||||
btnForce.style.display = 'inline-block';
|
||||
btnForce.disabled = false;
|
||||
}
|
||||
setDetail('');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
currentState = 'check_failed';
|
||||
if (!coreExists || !installed || !latest || compareVersions(latest, installed) > 0) {
|
||||
showUpdatable(release);
|
||||
setVersionHint(coreExists ? release : null);
|
||||
} else {
|
||||
currentState = 'latest';
|
||||
if (btnAction) {
|
||||
btnAction.value = _('Retry');
|
||||
btnAction.disabled = false;
|
||||
btnAction.value = _('It is the latest version');
|
||||
btnAction.disabled = true;
|
||||
}
|
||||
if (btnForce) {
|
||||
btnForce.style.display = 'none';
|
||||
btnForce.style.display = 'inline-block';
|
||||
btnForce.disabled = false;
|
||||
}
|
||||
setDetail(E('em', { 'style': 'color: red;' }, _('Check failed')));
|
||||
});
|
||||
setDetail('');
|
||||
setVersionHint(null);
|
||||
}
|
||||
}).catch(function() {
|
||||
currentState = 'check_failed';
|
||||
if (btnAction) {
|
||||
btnAction.value = _('Retry');
|
||||
btnAction.disabled = false;
|
||||
}
|
||||
if (btnForce) {
|
||||
btnForce.style.display = 'none';
|
||||
}
|
||||
setDetail(E('em', { 'style': 'color: red;' }, _('Check failed')));
|
||||
});
|
||||
}
|
||||
|
||||
function triggerUpdate(isForce) {
|
||||
const btnAction = document.getElementById('btn-agh-action');
|
||||
const btnForce = document.getElementById('btn-agh-force');
|
||||
const btnAction = byId('btn-agh-action');
|
||||
const btnForce = byId('btn-agh-force');
|
||||
|
||||
currentState = 'updating';
|
||||
if (btnAction) {
|
||||
@@ -762,7 +1046,7 @@ return view.extend({
|
||||
const isCore = results[0];
|
||||
const isError = results[1];
|
||||
if (isCore || isError) {
|
||||
const btnAction = document.getElementById('btn-agh-action');
|
||||
const btnAction = byId('btn-agh-action');
|
||||
currentState = 'updating';
|
||||
if (btnAction) {
|
||||
btnAction.value = _('Updating...') + ' 0%';
|
||||
@@ -772,6 +1056,25 @@ return view.extend({
|
||||
}
|
||||
});
|
||||
|
||||
// Background check for a newer release: it runs after the page is shown, never
|
||||
// delays it, and stays silent if GitHub cannot be reached.
|
||||
detectNewRelease(detectedChannel);
|
||||
|
||||
// Changing the Core Branch dropdown invalidates what the Action button shows.
|
||||
rendered.addEventListener('change', function() {
|
||||
if (['init', 'updatable', 'latest', 'check_failed'].indexOf(currentState) === -1) {
|
||||
return;
|
||||
}
|
||||
const channel = currentChannel();
|
||||
if (channel === detectedChannel) {
|
||||
return;
|
||||
}
|
||||
detectedChannel = channel;
|
||||
resetToCheck();
|
||||
setVersionHint(null);
|
||||
detectNewRelease(channel);
|
||||
});
|
||||
|
||||
return rendered;
|
||||
}
|
||||
});
|
||||
@@ -1,54 +1,39 @@
|
||||
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
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Jackie Han <jackie.han@gmail.com>
|
||||
Date: Wed, 4 Nov 2026 10:22:07 +0800
|
||||
Subject: [PATCH] luci-app-adguardhome: ACL for the core update flow
|
||||
|
||||
Allow the update log, the update flag files and update_core.sh, including
|
||||
update_core_done (stat to detect completion, write to remove it).
|
||||
---
|
||||
.../rpcd/acl.d/luci-app-adguardhome.json | 36 +++++++++++--------
|
||||
1 file changed, 21 insertions(+), 15 deletions(-)
|
||||
root/usr/share/rpcd/acl.d/luci-app-adguardhome.json | 10 ++++++++++
|
||||
1 file changed, 10 insertions(+)
|
||||
|
||||
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
|
||||
index 748778a..33862ec 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": {
|
||||
@@ -3,6 +3,12 @@
|
||||
"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_done": [ "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": {
|
||||
"/usr/bin/AdGuardHome --version": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
@@ -11,6 +17,10 @@
|
||||
"uci": [ "adguardhome" ]
|
||||
},
|
||||
"write": {
|
||||
+ "file": {
|
||||
+ "/usr/share/AdGuardHome/update_core.sh": [ "exec" ]
|
||||
+ "/usr/share/AdGuardHome/update_core.sh": [ "exec" ],
|
||||
+ "/var/run/update_core_done": [ "write" ]
|
||||
+ },
|
||||
+ "uci": [ "adguardhome" ]
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
"uci": [ "adguardhome" ]
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
From e0b6eccaf56a0b15a09c002e868f153032af5420 Mon Sep 17 00:00:00 2001
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: Jackie Han <jackie.han@gmail.com>
|
||||
Date: Sun, 25 Oct 2026 15:05:05 +0800
|
||||
Subject: [PATCH] Enhance config.js with validation and UI improvements
|
||||
Date: Wed, 4 Nov 2026 10:13:35 +0800
|
||||
Subject: [PATCH] luci-app-adguardhome: config.js hardening, update_source and
|
||||
new-version hint
|
||||
|
||||
Added validation functions for paths and passwords, improved error handling, and updated UI elements for better user experience.
|
||||
Hardens the LuCI page, replaces update_url with update_source, and shows
|
||||
a new-core-version hint with a one-click path to the Core Update tab.
|
||||
The Service Status pill doubles as the Enable switch, next to the version.
|
||||
---
|
||||
.../resources/view/adguardhome/config.js | 744 +++++++++++++++---
|
||||
1 file changed, 630 insertions(+), 114 deletions(-)
|
||||
.../resources/view/adguardhome/config.js | 1207 +++++++++++++++--
|
||||
1 file changed, 1076 insertions(+), 131 deletions(-)
|
||||
|
||||
diff --git a/htdocs/luci-static/resources/view/adguardhome/config.js b/htdocs/luci-static/resources/view/adguardhome/config.js
|
||||
index 6dc2d45..b0f0e06 100644
|
||||
index 6dc2d45..07aa5cc 100644
|
||||
--- a/htdocs/luci-static/resources/view/adguardhome/config.js
|
||||
+++ b/htdocs/luci-static/resources/view/adguardhome/config.js
|
||||
@@ -5,6 +5,7 @@
|
||||
@@ -20,7 +23,7 @@ index 6dc2d45..b0f0e06 100644
|
||||
'require view';
|
||||
|
||||
const DEFAULT_CONFIG_FILE = '/etc/adguardhome/adguardhome.yaml';
|
||||
@@ -18,12 +19,56 @@ const DEFAULT_GOMEMLIMIT = '0';
|
||||
@@ -18,12 +19,292 @@ const DEFAULT_GOMEMLIMIT = '0';
|
||||
|
||||
const PATH_REGEX = new RegExp('^/etc(/[^/]+)?/?$');
|
||||
|
||||
@@ -42,8 +45,6 @@ index 6dc2d45..b0f0e06 100644
|
||||
|
||||
-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';
|
||||
@@ -76,10 +77,248 @@ index 6dc2d45..b0f0e06 100644
|
||||
+function hasDotDotSegment(value) {
|
||||
+ return value.split('/').indexOf('..') !== -1;
|
||||
+}
|
||||
+
|
||||
+// ---- BEGIN new-release notice ------------------------------------------------
|
||||
+// AdGuard Home's own /control/version.json needs HTTP basic auth (this page only
|
||||
+// ever holds the password hash) and is served on another port, so the page asks
|
||||
+// GitHub directly: the same source as the "Check update" button and update_core.sh.
|
||||
+// The answer is cached in localStorage so opening the page does not cost an API
|
||||
+// request every time (GitHub allows only 60 unauthenticated requests per hour).
|
||||
+const STORAGE_KEY_LATEST = 'luci-app-adguardhome_latest_release';
|
||||
+const RELEASE_CACHE_TTL = 6 * 60 * 60 * 1000; // after a successful check
|
||||
+const RELEASE_RETRY_DELAY = 30 * 60 * 1000; // after a failed check
|
||||
+const RELEASE_FETCH_TIMEOUT = 8000;
|
||||
+
|
||||
+function storageGet(key) {
|
||||
+ try {
|
||||
+ return window.localStorage.getItem(key);
|
||||
+ } catch (e) {
|
||||
+ return null;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+function storageSet(key, value) {
|
||||
+ try {
|
||||
+ window.localStorage.setItem(key, value);
|
||||
+ } catch (e) {}
|
||||
+}
|
||||
+
|
||||
+// "v0.107.57-b.2" -> { nums: [0, 107, 57], pre: ['b', '2'] }, or null
|
||||
+function parseVersion(str) {
|
||||
+ const m = /v?(\d+)\.(\d+)\.(\d+)(?:-([A-Za-z0-9.]+))?/.exec(String(str || ''));
|
||||
+ if (!m) {
|
||||
+ return null;
|
||||
+ }
|
||||
+ return { nums: [+m[1], +m[2], +m[3]], pre: m[4] ? m[4].split('.') : [] };
|
||||
+}
|
||||
+
|
||||
+function formatVersion(v) {
|
||||
+ return 'v' + v.nums.join('.') + (v.pre.length ? '-' + v.pre.join('.') : '');
|
||||
+}
|
||||
+
|
||||
+// Comparator result (<0, 0, >0). A pre-release sorts below its release.
|
||||
+function compareVersions(a, b) {
|
||||
+ for (let i = 0; i < 3; i++) {
|
||||
+ if (a.nums[i] !== b.nums[i]) {
|
||||
+ return a.nums[i] - b.nums[i];
|
||||
+ }
|
||||
+ }
|
||||
+ if (!a.pre.length && !b.pre.length) {
|
||||
+ return 0;
|
||||
+ }
|
||||
+ if (!a.pre.length) {
|
||||
+ return 1;
|
||||
+ }
|
||||
+ if (!b.pre.length) {
|
||||
+ return -1;
|
||||
+ }
|
||||
+ const n = Math.max(a.pre.length, b.pre.length);
|
||||
+ for (let i = 0; i < n; i++) {
|
||||
+ const x = a.pre[i];
|
||||
+ const y = b.pre[i];
|
||||
+ if (x === undefined) {
|
||||
+ return -1;
|
||||
+ }
|
||||
+ if (y === undefined) {
|
||||
+ return 1;
|
||||
+ }
|
||||
+ if (x === y) {
|
||||
+ continue;
|
||||
+ }
|
||||
+ const xNum = /^\d+$/.test(x);
|
||||
+ const yNum = /^\d+$/.test(y);
|
||||
+ if (xNum && yNum) {
|
||||
+ return +x - +y;
|
||||
+ }
|
||||
+ if (xNum) {
|
||||
+ return -1;
|
||||
+ }
|
||||
+ if (yNum) {
|
||||
+ return 1;
|
||||
+ }
|
||||
+ return x < y ? -1 : 1;
|
||||
+ }
|
||||
+ return 0;
|
||||
+}
|
||||
+
|
||||
+// Newest release of the channel ('latest' or 'beta') as { tag, url }, cached.
|
||||
+// "force" ignores the cache (used by the manual "Check update" button).
|
||||
+// Rejects when GitHub cannot be reached or the answer is not what we expect.
|
||||
+async function getLatestRelease(channel, force) {
|
||||
+ const cacheKey = STORAGE_KEY_LATEST + '_' + channel;
|
||||
+ const now = Date.now();
|
||||
+ let cached = null;
|
||||
+ try {
|
||||
+ cached = JSON.parse(storageGet(cacheKey) || 'null');
|
||||
+ } catch (e) {
|
||||
+ cached = null;
|
||||
+ }
|
||||
+
|
||||
+ if (!force && cached && typeof cached.time === 'number') {
|
||||
+ if (cached.failed) {
|
||||
+ if (now - cached.time < RELEASE_RETRY_DELAY) {
|
||||
+ throw new Error('Recent check failed');
|
||||
+ }
|
||||
+ } else if (now - cached.time < RELEASE_CACHE_TTL &&
|
||||
+ VERSION_TAG_REGEX.test(cached.tag) &&
|
||||
+ GITHUB_RELEASE_URL_REGEX.test(cached.url)) {
|
||||
+ return cached;
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ try {
|
||||
+ const api = 'https://api.github.com/repos/AdguardTeam/AdGuardHome/releases' +
|
||||
+ (channel === 'beta' ? '?per_page=1' : '/latest');
|
||||
+ const ctrl = (typeof AbortController === 'function') ? new AbortController() : null;
|
||||
+ const timer = ctrl ? window.setTimeout(function() { ctrl.abort(); }, RELEASE_FETCH_TIMEOUT) : null;
|
||||
+ let res;
|
||||
+ try {
|
||||
+ res = await fetch(api, ctrl ? { signal: ctrl.signal } : undefined);
|
||||
+ } finally {
|
||||
+ if (timer) {
|
||||
+ window.clearTimeout(timer);
|
||||
+ }
|
||||
+ }
|
||||
+ if (!res.ok) {
|
||||
+ throw new Error('HTTP ' + res.status);
|
||||
+ }
|
||||
+ let data = await res.json();
|
||||
+ if (Array.isArray(data)) {
|
||||
+ data = data[0];
|
||||
+ }
|
||||
+ if (!data || typeof data.tag_name !== 'string' || !VERSION_TAG_REGEX.test(data.tag_name)) {
|
||||
+ throw new Error('Invalid version data');
|
||||
+ }
|
||||
+ const url = (typeof data.html_url === 'string' && GITHUB_RELEASE_URL_REGEX.test(data.html_url))
|
||||
+ ? data.html_url
|
||||
+ : 'https://github.com/AdguardTeam/AdGuardHome/releases/tag/' + data.tag_name;
|
||||
+ const entry = { tag: data.tag_name, url: url, time: now };
|
||||
+ storageSet(cacheKey, JSON.stringify(entry));
|
||||
+ return entry;
|
||||
+ } catch (e) {
|
||||
+ storageSet(cacheKey, JSON.stringify({ failed: true, time: now }));
|
||||
+ throw e;
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// Hint appended to the "Current Version" row. The whole text is the button: a click
|
||||
+// runs onUpdate (go to the Core Update tab). A span, not a link, so the theme's link
|
||||
+// colour / underline never applies. Built from DOM nodes only (no HTML strings).
|
||||
+function buildVersionHint(release, onUpdate) {
|
||||
+ return E('span', {
|
||||
+ 'id': 'agh-version-hint',
|
||||
+ 'role': 'button',
|
||||
+ 'tabindex': '0',
|
||||
+ 'title': _('Go to Core Update'),
|
||||
+ 'style': 'margin-left: 12px; cursor: pointer;',
|
||||
+ 'click': function(ev) {
|
||||
+ ev.preventDefault();
|
||||
+ onUpdate();
|
||||
+ },
|
||||
+ 'keydown': function(ev) {
|
||||
+ if (ev.key === 'Enter' || ev.key === ' ') {
|
||||
+ ev.preventDefault();
|
||||
+ onUpdate();
|
||||
+ }
|
||||
+ }
|
||||
+ }, E('strong', { 'style': 'color: var(--warn-color-high, #d9822b);' },
|
||||
+ '\u2192 ' + _('New version %s available').replace('%s', release.tag)));
|
||||
+}
|
||||
+
|
||||
+// Header card above the settings: one line with the Service Status pill (which is also
|
||||
+// the Enable switch) and the Current Version. The pill follows the style of the Main
|
||||
+// Switch of luci-app-bypass.
|
||||
+const HEAD_CSS =
|
||||
+ '.agh-head { --agh-on-fg:#1a7f37; --agh-on-bg:#dafbe1; --agh-warn-fg:#9a6700; --agh-warn-bg:#fff8c5; --agh-off-fg:#82071e; --agh-off-bg:#ffebe9; display:flex; flex-direction:column; gap:.5rem; margin:.5rem 0 1rem; padding:.7rem 1rem; border-radius:var(--radius-card, 6px); background:var(--surface-raised, rgba(128,128,128,.06)); box-shadow:0 4px 8px rgba(0,0,0,.03); }' +
|
||||
+ ':root[data-argon-mode="dark"] .agh-head, :root[data-darkmode="true"] .agh-head { --agh-on-fg:#7ee2a0; --agh-on-bg:rgba(46,160,67,.22); --agh-warn-fg:#e3b341; --agh-warn-bg:rgba(187,128,9,.22); --agh-off-fg:#ffa9ae; --agh-off-bg:rgba(248,81,73,.18); }' +
|
||||
+ '.agh-head-row { display:flex; align-items:center; flex-wrap:wrap; gap:.4rem 2rem; }' +
|
||||
+ '.agh-head-item { display:inline-flex; align-items:center; flex-wrap:wrap; gap:.25rem .75rem; line-height:1.5; }' +
|
||||
+ '.agh-head-note { line-height:1.5; }' +
|
||||
+ '.agh-switch { display:inline-flex; align-items:center; gap:.4rem; height:auto; min-height:0; line-height:1.4; padding:.25rem .8rem; margin:0; border-radius:999px; font-weight:600; font-size:.9rem; border:0; box-shadow:none; cursor:pointer; transition:filter .15s; }' +
|
||||
+ '.agh-switch:hover { filter:brightness(.95); }' +
|
||||
+ '.agh-switch::before { content:""; width:.55rem; height:.55rem; border-radius:50%; background:currentColor; }' +
|
||||
+ '.agh-switch.on { color:var(--agh-on-fg); background:var(--agh-on-bg); }' +
|
||||
+ '.agh-switch.warn { color:var(--agh-warn-fg); background:var(--agh-warn-bg); }' +
|
||||
+ '.agh-switch.off { color:var(--agh-off-fg); background:var(--agh-off-bg); }';
|
||||
+
|
||||
+// The clickable <a> of the Core Update tab in the tab menu (not the tab pane, which
|
||||
+// carries the same data-tab value).
|
||||
+function findCoreUpdateTabLink(root) {
|
||||
+ return root.querySelector('.cbi-tabmenu li[data-tab="core_update"] a') ||
|
||||
+ root.querySelector('li[data-tab="core_update"] a') ||
|
||||
+ root.querySelector('a[href="#cbi-tab-adguardhome-core_update"]') ||
|
||||
+ root.querySelector('a[href="#core_update"]');
|
||||
+}
|
||||
+
|
||||
+// Click the tab as soon as it exists and is visible; the tab menu is built and
|
||||
+// shown/hidden (depends) shortly after render, so retry for a couple of seconds.
|
||||
+function activateCoreUpdateTab(root, tries) {
|
||||
+ const link = findCoreUpdateTabLink(root);
|
||||
+ const li = link ? link.parentNode : null;
|
||||
+ // LuCI marks every inactive tab 'cbi-tab-disabled', so that class means nothing here;
|
||||
+ // a tab that is not available (no visible option yet) has display:none.
|
||||
+ const hidden = li && li.style.display === 'none';
|
||||
+ if (link && !hidden) {
|
||||
+ link.click();
|
||||
+ return;
|
||||
+ }
|
||||
+ if ((tries || 0) < 40) {
|
||||
+ window.setTimeout(function() { activateCoreUpdateTab(root, (tries || 0) + 1); }, 50);
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+// The Core Update tab only exists while "Core Update" is ticked in General Settings.
|
||||
+function isCoreUpdateTabEnabled(root) {
|
||||
+ const boxes = root.querySelectorAll('input[type="checkbox"]');
|
||||
+ for (let i = 0; i < boxes.length; i++) {
|
||||
+ if (boxes[i].id && boxes[i].id.endsWith('.enable_core_update')) {
|
||||
+ return boxes[i].checked;
|
||||
+ }
|
||||
+ }
|
||||
+ return false;
|
||||
+}
|
||||
+
|
||||
+// Show the Core Update tab. If the tab is already available just switch to it;
|
||||
+// otherwise switch the toggle on (it lives in sessionStorage) and reload, so the
|
||||
+// tab is created and selected on the next load.
|
||||
+function goToCoreUpdateTab(root) {
|
||||
+ if (isCoreUpdateTabEnabled(root)) {
|
||||
+ if (findCoreUpdateTabLink(root)) {
|
||||
+ activateCoreUpdateTab(root, 0);
|
||||
+ return;
|
||||
+ }
|
||||
+ }
|
||||
+ try {
|
||||
+ sessionStorage.setItem(STORAGE_KEY_CORE, '1');
|
||||
+ sessionStorage.setItem('agh_active_tab', 'core_update');
|
||||
+ } catch (e) {}
|
||||
+ window.location.reload();
|
||||
+}
|
||||
+// ---- END new-release notice --------------------------------------------------
|
||||
|
||||
function getServiceInfo(name) {
|
||||
const fn = rpc.declare({
|
||||
@@ -32,7 +77,9 @@ function getServiceInfo(name) {
|
||||
@@ -32,7 +313,9 @@ function getServiceInfo(name) {
|
||||
params: ['name'],
|
||||
expect: { [name]: { instances: { [name]: {} }}},
|
||||
});
|
||||
@@ -90,7 +329,7 @@ index 6dc2d45..b0f0e06 100644
|
||||
}
|
||||
|
||||
const getAGHServiceInfo = getServiceInfo('adguardhome');
|
||||
@@ -40,8 +87,8 @@ const getAGHServiceInfo = getServiceInfo('adguardhome');
|
||||
@@ -40,18 +323,14 @@ const getAGHServiceInfo = getServiceInfo('adguardhome');
|
||||
async function getStatus() {
|
||||
try {
|
||||
const res = await getAGHServiceInfo();
|
||||
@@ -101,7 +340,17 @@ index 6dc2d45..b0f0e06 100644
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return false;
|
||||
@@ -61,26 +108,40 @@ async function getVersion() {
|
||||
}
|
||||
}
|
||||
|
||||
-function getStatusValue(isRunning) {
|
||||
- return isRunning ? RUNNING_SPAN : NOT_RUNNING_SPAN;
|
||||
-}
|
||||
-
|
||||
async function getVersion() {
|
||||
try {
|
||||
const res = await fs.exec('/usr/bin/AdGuardHome', ['--version']);
|
||||
@@ -61,26 +340,30 @@ async function getVersion() {
|
||||
return version;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -110,19 +359,14 @@ index 6dc2d45..b0f0e06 100644
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(node) {
|
||||
-function updateStatus(node) {
|
||||
- const output = node?.querySelector('output');
|
||||
+ const output = node ? node.querySelector('output') : null;
|
||||
return output
|
||||
- return output
|
||||
- ? async () => {
|
||||
+ ? async function() {
|
||||
const isRunning = await getStatus();
|
||||
dom.content(output, getStatusValue(isRunning));
|
||||
}
|
||||
- const isRunning = await getStatus();
|
||||
- dom.content(output, getStatusValue(isRunning));
|
||||
- }
|
||||
- : () => {};
|
||||
+ : function() {};
|
||||
+}
|
||||
+
|
||||
+function validateAbsolutePath(value) {
|
||||
+ if (!value.startsWith('/')) {
|
||||
+ return _('Path must be absolute.');
|
||||
@@ -148,7 +392,7 @@ index 6dc2d45..b0f0e06 100644
|
||||
}
|
||||
if (value.endsWith('/')) {
|
||||
return _('Path must not end with a slash.');
|
||||
@@ -95,167 +156,622 @@ function validateWorkDir(_unused, value) {
|
||||
@@ -95,167 +378,829 @@ function validateWorkDir(_unused, value) {
|
||||
if (value == null || value === '') {
|
||||
return true;
|
||||
}
|
||||
@@ -238,12 +482,17 @@ index 6dc2d45..b0f0e06 100644
|
||||
},
|
||||
|
||||
- async render([isRunning, version]) {
|
||||
- const map = new form.Map('adguardhome', _('AdGuard Home'));
|
||||
+ render: async function(data) {
|
||||
+ const isRunning = data[0];
|
||||
+ const version = data[1];
|
||||
+ const yamlContent = data[2];
|
||||
+
|
||||
|
||||
- const statusSect = map.section(form.TypedSection, 'status');
|
||||
- statusSect.anonymous = true;
|
||||
- statusSect.cfgsections = () => ['status_section'];
|
||||
+ const coreExists = Boolean(version);
|
||||
+ const installed = parseVersion(version);
|
||||
+ let dnsPort = '53';
|
||||
+ if (yamlContent) {
|
||||
+ const portMatches = yamlContent.match(/port:\s*(\d+)/g);
|
||||
@@ -254,29 +503,16 @@ index 6dc2d45..b0f0e06 100644
|
||||
+ }
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ const sections = uci.sections('adguardhome', 'adguardhome');
|
||||
+
|
||||
const map = new form.Map('adguardhome', _('AdGuard Home'));
|
||||
|
||||
const statusSect = map.section(form.TypedSection, 'status');
|
||||
statusSect.anonymous = true;
|
||||
- statusSect.cfgsections = () => ['status_section'];
|
||||
+ statusSect.cfgsections = function() { return ['status_section']; };
|
||||
|
||||
const versionOpt = statusSect.option(form.DummyValue, '_version', _('Version'));
|
||||
- const versionOpt = statusSect.option(form.DummyValue, '_version', _('Version'));
|
||||
- versionOpt.cfgvalue = () => version;
|
||||
+ versionOpt.cfgvalue = function() {
|
||||
+ return version
|
||||
+ ? esc(version)
|
||||
+ : '<span style="color: var(--error-color-high); font-weight: bold;">' + esc(_('Not installed')) + '</span>';
|
||||
+ };
|
||||
+ versionOpt.rawhtml = true;
|
||||
+ const sections = uci.sections('adguardhome', 'adguardhome');
|
||||
+ const savedChannel = (sections.length > 0 && sections[0].core_version === 'beta') ? 'beta' : 'latest';
|
||||
|
||||
const statusOpt = statusSect.option(form.DummyValue, '_status', _('Service Status'));
|
||||
statusOpt.rawhtml = true;
|
||||
- const statusOpt = statusSect.option(form.DummyValue, '_status', _('Service Status'));
|
||||
- statusOpt.rawhtml = true;
|
||||
- statusOpt.cfgvalue = () => getStatusValue(isRunning);
|
||||
+ statusOpt.cfgvalue = function() { return getStatusValue(isRunning); };
|
||||
+ const map = new form.Map('adguardhome', _('AdGuard Home'));
|
||||
|
||||
const mainSect = map.section(form.TypedSection, 'adguardhome');
|
||||
mainSect.anonymous = true;
|
||||
@@ -311,9 +547,6 @@ index 6dc2d45..b0f0e06 100644
|
||||
+ const enabledOpt = mainSect.taboption('general', form.Flag, 'enabled', _('Enable'));
|
||||
+ enabledOpt.default = '0';
|
||||
+ enabledOpt.rmempty = false;
|
||||
+ if (!coreExists) {
|
||||
+ enabledOpt.description = '<span style="color: var(--error-color-high); font-weight: bold;">' + esc(_('Core binary not found. Enable the service to trigger an automatic download.')) + '</span>';
|
||||
+ }
|
||||
+
|
||||
+ const configFileOpt = mainSect.taboption('general', form.Value, 'config_file', _('Configuration file'));
|
||||
configFileOpt.placeholder = DEFAULT_CONFIG_FILE;
|
||||
@@ -555,30 +788,251 @@ index 6dc2d45..b0f0e06 100644
|
||||
+
|
||||
const rendered = await map.render();
|
||||
|
||||
- const statusNode = map.findElement('data-field', statusOpt.cbid('status_section'));
|
||||
- poll.add(updateStatus(statusNode), POLL_INTERVAL);
|
||||
+ // Look elements up inside our own tree: it may not be attached to the document
|
||||
+ // yet when the background release check finishes.
|
||||
+ const byId = function(id) {
|
||||
+ return rendered.querySelector('#' + id);
|
||||
+ };
|
||||
+
|
||||
+ if (sessionStorage.getItem('agh_active_tab') === 'core_update') {
|
||||
+ sessionStorage.removeItem('agh_active_tab');
|
||||
+ window.location.hash = 'core_update';
|
||||
+ window.setTimeout(function() {
|
||||
+ const tabLink = rendered.querySelector('a[href="#cbi-tab-adguardhome-core_update"]') ||
|
||||
+ rendered.querySelector('a[href="#core_update"]') ||
|
||||
+ rendered.querySelector('[data-tab="core_update"]');
|
||||
+ if (tabLink) tabLink.click();
|
||||
+ }, 50);
|
||||
+ activateCoreUpdateTab(rendered, 0);
|
||||
+ }
|
||||
+
|
||||
const statusNode = map.findElement('data-field', statusOpt.cbid('status_section'));
|
||||
poll.add(updateStatus(statusNode), POLL_INTERVAL);
|
||||
|
||||
+ // ---- Header: Service Status pill (= Enable switch) + Current Version -----------
|
||||
+ // The pill drives the (hidden) "Enable" checkbox of the form, so saving and applying
|
||||
+ // work exactly as before. LuCI re-renders the whole form after "Save" and throws away
|
||||
+ // everything we added, so decorate() builds the header again after every render.
|
||||
+ const enableSecId = sections.length > 0 ? sections[0]['.name'] : null;
|
||||
+
|
||||
+ function readEnabled() {
|
||||
+ const el = enableSecId ? enabledOpt.getUIElement(enableSecId) : null;
|
||||
+ if (el) {
|
||||
+ return el.getValue() === '1';
|
||||
+ }
|
||||
+ return sections.length > 0 && sections[0].enabled === '1';
|
||||
+ }
|
||||
+
|
||||
+ // The pill shows the live service state and, when clicked, flips the Enable setting.
|
||||
+ let running = Boolean(isRunning);
|
||||
+ let enableSwitch = null;
|
||||
+ let versionField = null;
|
||||
+ let head = null;
|
||||
+
|
||||
+ function paintEnableSwitch() {
|
||||
+ if (!enableSwitch) {
|
||||
+ return;
|
||||
+ }
|
||||
+ const enabled = readEnabled();
|
||||
+ let cls, text, hint;
|
||||
+
|
||||
+ if (running && enabled) {
|
||||
+ cls = 'on';
|
||||
+ text = _('Running');
|
||||
+ hint = _('Click to disable');
|
||||
+ } else if (running) {
|
||||
+ cls = 'warn';
|
||||
+ text = _('Running') + ' \u00b7 ' + _('Disabled');
|
||||
+ hint = _('Will stop after "Save & Apply". Click to keep it enabled.');
|
||||
+ } else if (enabled) {
|
||||
+ cls = 'warn';
|
||||
+ text = _('Not running');
|
||||
+ hint = _('Enabled but not running. Click "Save & Apply" to start it, or click to disable.');
|
||||
+ } else {
|
||||
+ cls = 'off';
|
||||
+ text = _('Disabled');
|
||||
+ hint = _('Click to enable');
|
||||
+ }
|
||||
+ enableSwitch.className = 'agh-switch ' + cls;
|
||||
+ dom.content(enableSwitch, text);
|
||||
+ enableSwitch.title = hint + ' - ' + _('Apply with "Save & Apply"');
|
||||
+ }
|
||||
+
|
||||
+ // Line the "Service Status" label up with the text of the first tab ("General
|
||||
+ // Settings") below. The offset depends on the theme, so measure it once the page
|
||||
+ // is laid out (and again when the window is resized).
|
||||
+ function alignHead(tries) {
|
||||
+ const box = head;
|
||||
+ const tab = rendered.querySelector('.cbi-tabmenu li a');
|
||||
+ let left = 0;
|
||||
+ if (!box) {
|
||||
+ return;
|
||||
+ }
|
||||
+ if (tab && tab.getBoundingClientRect().width > 0) {
|
||||
+ const range = document.createRange();
|
||||
+ range.selectNodeContents(tab);
|
||||
+ left = range.getBoundingClientRect().left;
|
||||
+ }
|
||||
+ if (left > 0 && box.getBoundingClientRect().width > 0) {
|
||||
+ box.style.paddingLeft = '0px';
|
||||
+ const pad = left - box.getBoundingClientRect().left;
|
||||
+ box.style.paddingLeft = Math.max(0, Math.round(pad)) + 'px';
|
||||
+ } else if (tries < 40) {
|
||||
+ window.setTimeout(function() { if (box === head) alignHead(tries + 1); }, 50);
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ function decorate() {
|
||||
+ const enableRow = enableSecId ? map.findElement('data-field', enabledOpt.cbid(enableSecId)) : null;
|
||||
+ if (enableRow) {
|
||||
+ enableRow.style.display = 'none';
|
||||
+ }
|
||||
+
|
||||
+ enableSwitch = E('button', {
|
||||
+ 'type': 'button',
|
||||
+ 'click': function(ev) {
|
||||
+ ev.preventDefault();
|
||||
+ const el = enableSecId ? enabledOpt.getUIElement(enableSecId) : null;
|
||||
+ if (el) {
|
||||
+ el.setValue(readEnabled() ? '0' : '1');
|
||||
+ }
|
||||
+ paintEnableSwitch();
|
||||
+ }
|
||||
+ });
|
||||
+ paintEnableSwitch();
|
||||
+
|
||||
+ versionField = E('span', {},
|
||||
+ coreExists
|
||||
+ ? version
|
||||
+ : E('span', { 'style': 'color: var(--error-color-high); font-weight: bold;' }, _('Not installed')));
|
||||
+
|
||||
+ const headRows = [
|
||||
+ E('div', { 'class': 'agh-head-row' }, [
|
||||
+ E('span', { 'class': 'agh-head-item' }, [ E('span', {}, _('Service Status')), enableSwitch ]),
|
||||
+ E('span', { 'class': 'agh-head-item' }, [ E('span', {}, _('Current Version')), versionField ])
|
||||
+ ])
|
||||
+ ];
|
||||
+ if (!coreExists) {
|
||||
+ headRows.push(E('div', { 'class': 'agh-head-note', 'style': 'color: var(--error-color-high); font-weight: bold;' },
|
||||
+ _('Core binary not found. Enable the service to trigger an automatic download.')));
|
||||
+ }
|
||||
+ head = E('div', { 'class': 'agh-head' }, headRows);
|
||||
+
|
||||
+ const title = rendered.querySelector('h2');
|
||||
+ rendered.insertBefore(head, title ? title.nextSibling : rendered.firstChild);
|
||||
+ rendered.appendChild(E('style', {}, HEAD_CSS));
|
||||
+ alignHead(0);
|
||||
+ }
|
||||
+
|
||||
+ decorate();
|
||||
+ window.addEventListener('resize', function() { alignHead(40); });
|
||||
+
|
||||
+ // After "Save" LuCI renders the form again into the same root element: add the
|
||||
+ // header back and bring the (also re-created) update controls up to date.
|
||||
+ let afterRerender = null;
|
||||
+ const origRenderContents = map.renderContents;
|
||||
+ map.renderContents = function() {
|
||||
+ return Promise.resolve(origRenderContents.apply(this, arguments)).then(function(res) {
|
||||
+ if (afterRerender) {
|
||||
+ afterRerender();
|
||||
+ }
|
||||
+ return res;
|
||||
+ });
|
||||
+ };
|
||||
+
|
||||
+ poll.add(async function() {
|
||||
+ running = Boolean(await getStatus());
|
||||
+ paintEnableSwitch();
|
||||
+ }, POLL_INTERVAL);
|
||||
+
|
||||
+ let updatePollId = null;
|
||||
+ let currentState = 'init';
|
||||
+
|
||||
+ // Channel ('latest' or 'beta') the Action button / version hint currently refer to.
|
||||
+ let detectedChannel = savedChannel;
|
||||
+
|
||||
+ function setDetail(node) {
|
||||
+ const detail = document.getElementById('btn-agh-detail');
|
||||
+ const detail = byId('btn-agh-detail');
|
||||
+ if (detail) {
|
||||
+ dom.content(detail, node || '');
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // The channel selected in the Core Branch dropdown right now (saved one as fallback).
|
||||
+ function currentChannel() {
|
||||
+ try {
|
||||
+ const secId = sections.length > 0 ? sections[0]['.name'] : null;
|
||||
+ const v = secId ? coreVersionOpt.formvalue(secId) : null;
|
||||
+ if (CORE_VERSIONS.indexOf(v) !== -1) {
|
||||
+ return v;
|
||||
+ }
|
||||
+ } catch (err) {}
|
||||
+ return savedChannel;
|
||||
+ }
|
||||
+
|
||||
+ // "Click to update" state, shared by the manual and the background check.
|
||||
+ // release.url is always a github.com/AdguardTeam/AdGuardHome/releases URL (see getLatestRelease).
|
||||
+ function showUpdatable(release) {
|
||||
+ const btnAction = byId('btn-agh-action');
|
||||
+ const btnForce = byId('btn-agh-force');
|
||||
+
|
||||
+ currentState = 'updatable';
|
||||
+ if (btnAction) {
|
||||
+ btnAction.value = _('Click to update');
|
||||
+ btnAction.disabled = false;
|
||||
+ }
|
||||
+ if (btnForce) {
|
||||
+ btnForce.style.display = 'none';
|
||||
+ }
|
||||
+ setDetail(E('a', { 'href': release.url, 'target': '_blank', 'rel': 'noopener noreferrer' },
|
||||
+ E('em', { 'style': 'color: red;' }, release.tag)));
|
||||
+ }
|
||||
+
|
||||
+ // Back to the initial "Check update" state.
|
||||
+ function resetToCheck() {
|
||||
+ const btnAction = byId('btn-agh-action');
|
||||
+ const btnForce = byId('btn-agh-force');
|
||||
+
|
||||
+ currentState = 'init';
|
||||
+ if (btnAction) {
|
||||
+ btnAction.value = _('Check update');
|
||||
+ btnAction.disabled = false;
|
||||
+ }
|
||||
+ if (btnForce) {
|
||||
+ btnForce.style.display = 'none';
|
||||
+ }
|
||||
+ setDetail('');
|
||||
+ }
|
||||
+
|
||||
+ // Show (release) or remove (null) the new-version hint after the version number.
|
||||
+ function setVersionHint(release) {
|
||||
+ const field = versionField;
|
||||
+ const old = field.querySelector('#agh-version-hint');
|
||||
+ if (old) {
|
||||
+ old.remove();
|
||||
+ }
|
||||
+ if (release) {
|
||||
+ field.appendChild(buildVersionHint(release, function() {
|
||||
+ goToCoreUpdateTab(rendered);
|
||||
+ }));
|
||||
+ }
|
||||
+ }
|
||||
+
|
||||
+ // Background check: if a newer release exists, show the hint and, while the Action
|
||||
+ // button is still untouched, go straight to "Click to update".
|
||||
+ function detectNewRelease(channel) {
|
||||
+ if (!coreExists || !installed) {
|
||||
+ return;
|
||||
+ }
|
||||
+ getLatestRelease(channel).then(function(release) {
|
||||
+ if (channel !== detectedChannel) {
|
||||
+ return;
|
||||
+ }
|
||||
+ const latest = parseVersion(release.tag);
|
||||
+ if (!latest || compareVersions(latest, installed) <= 0) {
|
||||
+ setVersionHint(null);
|
||||
+ return;
|
||||
+ }
|
||||
+ setVersionHint(release);
|
||||
+ if (currentState === 'init') {
|
||||
+ showUpdatable(release);
|
||||
+ }
|
||||
+ }).catch(function() {});
|
||||
+ }
|
||||
+
|
||||
+ function startLogPolling() {
|
||||
+ if (updatePollId) clearInterval(updatePollId);
|
||||
+
|
||||
@@ -590,8 +1044,8 @@ index 6dc2d45..b0f0e06 100644
|
||||
+ let targetPercent = 0;
|
||||
+
|
||||
+ const pollAction = function() {
|
||||
+ const btnAction = document.getElementById('btn-agh-action');
|
||||
+ const btnForce = document.getElementById('btn-agh-force');
|
||||
+ const btnAction = byId('btn-agh-action');
|
||||
+ const btnForce = byId('btn-agh-force');
|
||||
+
|
||||
+ if (currentState === 'updating') {
|
||||
+ if (displayPercent < targetPercent) {
|
||||
@@ -686,8 +1140,8 @@ index 6dc2d45..b0f0e06 100644
|
||||
+ }
|
||||
+
|
||||
+ function triggerCheck() {
|
||||
+ const btnAction = document.getElementById('btn-agh-action');
|
||||
+ const btnForce = document.getElementById('btn-agh-force');
|
||||
+ const btnAction = byId('btn-agh-action');
|
||||
+ const btnForce = byId('btn-agh-force');
|
||||
+
|
||||
+ currentState = 'checking';
|
||||
+ if (btnAction) {
|
||||
@@ -696,66 +1150,43 @@ index 6dc2d45..b0f0e06 100644
|
||||
+ }
|
||||
+ setDetail('');
|
||||
+
|
||||
+ fetch('https://api.github.com/repos/AdguardTeam/AdGuardHome/releases/latest')
|
||||
+ .then(function(res) {
|
||||
+ if (!res.ok) {
|
||||
+ throw new Error('HTTP ' + res.status);
|
||||
+ }
|
||||
+ return res.json();
|
||||
+ })
|
||||
+ .then(function(data) {
|
||||
+ const remoteVersion = (data && typeof data.tag_name === 'string') ? data.tag_name : null;
|
||||
+ if (!remoteVersion || !VERSION_TAG_REGEX.test(remoteVersion)) {
|
||||
+ throw new Error('Invalid version data');
|
||||
+ }
|
||||
+ const cleanLocal = coreExists ? version.replace(/^v/, '') : '';
|
||||
+ const cleanRemote = remoteVersion.replace(/^v/, '');
|
||||
+ const channel = currentChannel();
|
||||
+ getLatestRelease(channel, true).then(function(release) {
|
||||
+ detectedChannel = channel;
|
||||
+ const latest = parseVersion(release.tag);
|
||||
+
|
||||
+ if (!coreExists || cleanLocal !== cleanRemote) {
|
||||
+ currentState = 'updatable';
|
||||
+ if (btnAction) {
|
||||
+ btnAction.value = _('Click to update');
|
||||
+ btnAction.disabled = false;
|
||||
+ }
|
||||
+ if (btnForce) {
|
||||
+ btnForce.style.display = 'none';
|
||||
+ }
|
||||
+
|
||||
+ // Text only; link only if it points at the project's own releases.
|
||||
+ let node = E('em', { 'style': 'color: red;' }, remoteVersion);
|
||||
+ if (typeof data.html_url === 'string' && GITHUB_RELEASE_URL_REGEX.test(data.html_url)) {
|
||||
+ node = E('a', { 'href': data.html_url, 'target': '_blank', 'rel': 'noopener noreferrer' }, node);
|
||||
+ }
|
||||
+ setDetail(node);
|
||||
+ } else {
|
||||
+ currentState = 'latest';
|
||||
+ if (btnAction) {
|
||||
+ btnAction.value = _('It is the latest version');
|
||||
+ btnAction.disabled = true;
|
||||
+ }
|
||||
+ if (btnForce) {
|
||||
+ btnForce.style.display = 'inline-block';
|
||||
+ btnForce.disabled = false;
|
||||
+ }
|
||||
+ setDetail('');
|
||||
+ }
|
||||
+ })
|
||||
+ .catch(function() {
|
||||
+ currentState = 'check_failed';
|
||||
+ if (!coreExists || !installed || !latest || compareVersions(latest, installed) > 0) {
|
||||
+ showUpdatable(release);
|
||||
+ setVersionHint(coreExists ? release : null);
|
||||
+ } else {
|
||||
+ currentState = 'latest';
|
||||
+ if (btnAction) {
|
||||
+ btnAction.value = _('Retry');
|
||||
+ btnAction.disabled = false;
|
||||
+ btnAction.value = _('It is the latest version');
|
||||
+ btnAction.disabled = true;
|
||||
+ }
|
||||
+ if (btnForce) {
|
||||
+ btnForce.style.display = 'none';
|
||||
+ btnForce.style.display = 'inline-block';
|
||||
+ btnForce.disabled = false;
|
||||
+ }
|
||||
+ setDetail(E('em', { 'style': 'color: red;' }, _('Check failed')));
|
||||
+ });
|
||||
+ setDetail('');
|
||||
+ setVersionHint(null);
|
||||
+ }
|
||||
+ }).catch(function() {
|
||||
+ currentState = 'check_failed';
|
||||
+ if (btnAction) {
|
||||
+ btnAction.value = _('Retry');
|
||||
+ btnAction.disabled = false;
|
||||
+ }
|
||||
+ if (btnForce) {
|
||||
+ btnForce.style.display = 'none';
|
||||
+ }
|
||||
+ setDetail(E('em', { 'style': 'color: red;' }, _('Check failed')));
|
||||
+ });
|
||||
+ }
|
||||
+
|
||||
+ function triggerUpdate(isForce) {
|
||||
+ const btnAction = document.getElementById('btn-agh-action');
|
||||
+ const btnForce = document.getElementById('btn-agh-force');
|
||||
+ const btnAction = byId('btn-agh-action');
|
||||
+ const btnForce = byId('btn-agh-force');
|
||||
+
|
||||
+ currentState = 'updating';
|
||||
+ if (btnAction) {
|
||||
@@ -860,7 +1291,7 @@ index 6dc2d45..b0f0e06 100644
|
||||
+ const isCore = results[0];
|
||||
+ const isError = results[1];
|
||||
+ if (isCore || isError) {
|
||||
+ const btnAction = document.getElementById('btn-agh-action');
|
||||
+ const btnAction = byId('btn-agh-action');
|
||||
+ currentState = 'updating';
|
||||
+ if (btnAction) {
|
||||
+ btnAction.value = _('Updating...') + ' 0%';
|
||||
@@ -870,6 +1301,34 @@ index 6dc2d45..b0f0e06 100644
|
||||
+ }
|
||||
+ });
|
||||
+
|
||||
+ // Background check for a newer release: it runs after the page is shown, never
|
||||
+ // delays it, and stays silent if GitHub cannot be reached.
|
||||
+ detectNewRelease(detectedChannel);
|
||||
+
|
||||
+ afterRerender = function() {
|
||||
+ decorate();
|
||||
+ if (currentState !== 'updating') {
|
||||
+ currentState = 'init';
|
||||
+ detectedChannel = currentChannel();
|
||||
+ detectNewRelease(detectedChannel);
|
||||
+ }
|
||||
+ };
|
||||
+
|
||||
+ // Changing the Core Branch dropdown invalidates what the Action button shows.
|
||||
+ rendered.addEventListener('change', function() {
|
||||
+ if (['init', 'updatable', 'latest', 'check_failed'].indexOf(currentState) === -1) {
|
||||
+ return;
|
||||
+ }
|
||||
+ const channel = currentChannel();
|
||||
+ if (channel === detectedChannel) {
|
||||
+ return;
|
||||
+ }
|
||||
+ detectedChannel = channel;
|
||||
+ resetToCheck();
|
||||
+ setVersionHint(null);
|
||||
+ detectNewRelease(channel);
|
||||
+ });
|
||||
|
||||
return rendered;
|
||||
- },
|
||||
+ }
|
||||
|
||||
@@ -66,8 +66,8 @@ elif [ -f "$OLD_CONFIG_FILE" ] && [ "$OLD_CONFIG_FILE" != "$NEW_CONFIG_FILE" ];
|
||||
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."
|
||||
echo "Found custom 'certificate_path' pointing to '$cert_path'."
|
||||
echo "Ensure configured service user '$USER' can access it."
|
||||
|
||||
stop_service
|
||||
|
||||
@@ -79,8 +79,8 @@ elif [ -f "$OLD_CONFIG_FILE" ] && [ "$OLD_CONFIG_FILE" != "$NEW_CONFIG_FILE" ];
|
||||
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."
|
||||
echo "Found custom 'private_key_path' pointing to '$private_key_path'."
|
||||
echo "Ensure configured service user '$USER' can access it."
|
||||
|
||||
stop_service
|
||||
|
||||
@@ -107,3 +107,28 @@ elif [ -z "$OLD_CONFIG_NEW_OPT_FILE" ] && [ "$OLD_CONFIG_FILE" != "$NEW_CONFIG_F
|
||||
else
|
||||
echo "AdGuard Home config is in its default path '$NEW_CONFIG_FILE'"
|
||||
fi
|
||||
|
||||
# Migrate the core-update settings: the old 'update_url' option stored a full
|
||||
# download URL; it is replaced by 'update_source' (official|github), which
|
||||
# update_core.sh maps to a fixed URL. The old URL is only classified by its
|
||||
# host and is never used for downloading. Independent of the migration above.
|
||||
if OLD_UPDATE_URL=$(uci -q get adguardhome.config.update_url); then
|
||||
if [ -z "$(uci -q get adguardhome.config.update_source)" ]; then
|
||||
case "$OLD_UPDATE_URL" in
|
||||
https://github.com/*)
|
||||
NEW_UPDATE_SOURCE=github
|
||||
;;
|
||||
https://static.adtidy.org/*)
|
||||
NEW_UPDATE_SOURCE=official
|
||||
;;
|
||||
*)
|
||||
NEW_UPDATE_SOURCE=official
|
||||
echo "Custom 'update_url' is no longer supported; using the official mirror."
|
||||
;;
|
||||
esac
|
||||
uci set adguardhome.config.update_source="$NEW_UPDATE_SOURCE"
|
||||
echo "Core update source set to '$NEW_UPDATE_SOURCE'"
|
||||
fi
|
||||
uci -q delete adguardhome.config.update_url
|
||||
uci commit adguardhome
|
||||
fi
|
||||
@@ -5,6 +5,7 @@
|
||||
"file": {
|
||||
"/tmp/AdGuardHome_update.log": [ "read" ],
|
||||
"/var/run/update_core": [ "stat" ],
|
||||
"/var/run/update_core_done": [ "stat" ],
|
||||
"/var/run/update_core_error": [ "stat" ],
|
||||
"/var/run/AdGredir": [ "read" ],
|
||||
"/var/run/AdGserverdis": [ "read" ],
|
||||
@@ -17,7 +18,8 @@
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/usr/share/AdGuardHome/update_core.sh": [ "exec" ]
|
||||
"/usr/share/AdGuardHome/update_core.sh": [ "exec" ],
|
||||
"/var/run/update_core_done": [ "write" ]
|
||||
},
|
||||
"uci": [ "adguardhome" ]
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-open-box
|
||||
PKG_VERSION:=0.1.394
|
||||
PKG_RELEASE:=61
|
||||
PKG_VERSION:=0.1.397
|
||||
PKG_RELEASE:=62
|
||||
|
||||
PKG_MAINTAINER:=liandu2024
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-trafficctl
|
||||
PKG_VERSION:=1.26.1
|
||||
PKG_RELEASE:=20
|
||||
PKG_VERSION:=1.26.3
|
||||
PKG_RELEASE:=21
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=Denis Iusupov <yusdyr@gmail.com>
|
||||
|
||||
@@ -164,11 +164,12 @@ fi
|
||||
# Get rate limit
|
||||
RATE_LIM=0
|
||||
if [ "$TCTL_FW" = "nft" ]; then
|
||||
RATE_LIM=$(nft list table netdev tm_ratelimit 2>/dev/null | grep "daddr $IP" | \
|
||||
grep -oE '[0-9]+ kbytes' | awk '{print $1 * 8}')
|
||||
RATE_LIM=$(nft list table netdev tm_ratelimit 2>/dev/null | tctl_rl_rate_kbit "$IP")
|
||||
else
|
||||
RATE_LIM=$(iptables -t mangle -L FORWARD -nv 2>/dev/null | grep "rl_ratelimit" | grep "$IP" | \
|
||||
grep -oE '[0-9]+kbit' | head -1 | sed 's/kbit//')
|
||||
# The address as a whole field, not a substring (see tctl_rl_rate_kbit).
|
||||
RATE_LIM=$(iptables -t mangle -L FORWARD -nv 2>/dev/null | grep "rl_ratelimit" \
|
||||
| awk -v ip="$IP" '{ for (i = 1; i <= NF; i++) if ($i == ip || $i == ip "/32") { print; next } }' \
|
||||
| grep -oE '[0-9]+kbit' | head -1 | sed 's/kbit//')
|
||||
fi
|
||||
[ -z "$RATE_LIM" ] && RATE_LIM=0
|
||||
|
||||
|
||||
@@ -124,21 +124,21 @@ tctl_ratelimit_add() {
|
||||
# directions could differ. Not zero: a zero ceiling policed every packet and
|
||||
# would be a silent total block reported as a successful limit.
|
||||
[ -z "$rate_kbit_up" ] && rate_kbit_up="$rate_kbit"
|
||||
local rate_kbyte=$((rate_kbit / 8))
|
||||
[ "$rate_kbyte" -lt 1 ] && rate_kbyte=1
|
||||
local rate_kbyte_up=$((rate_kbit_up / 8))
|
||||
[ "$rate_kbyte_up" -lt 1 ] && rate_kbyte_up=1
|
||||
# bytes/second, not kbytes: see tctl_rate_bytes.
|
||||
local rate_b rate_b_up
|
||||
rate_b=$(tctl_rate_bytes "$rate_kbit")
|
||||
rate_b_up=$(tctl_rate_bytes "$rate_kbit_up")
|
||||
|
||||
local slug dl_expr ul_expr ul6_expr dl6_expr feed6_expr v6set mac
|
||||
slug=$(tctl_target_slug "$ip")
|
||||
# The two directions were already separate rules on separate hooks — they
|
||||
# were symmetric only because both took the same number.
|
||||
if [ "$mode" = "each" ]; then
|
||||
dl_expr="ip daddr $ip meter tctl_d_$slug { ip daddr limit rate over ${rate_kbyte} kbytes/second }"
|
||||
ul_expr="ip saddr $ip meter tctl_u_$slug { ip saddr limit rate over ${rate_kbyte_up} kbytes/second }"
|
||||
dl_expr="ip daddr $ip meter tctl_d_$slug { ip daddr limit rate over ${rate_b} bytes/second }"
|
||||
ul_expr="ip saddr $ip meter tctl_u_$slug { ip saddr limit rate over ${rate_b_up} bytes/second }"
|
||||
else
|
||||
dl_expr="ip daddr $ip limit rate over ${rate_kbyte} kbytes/second"
|
||||
ul_expr="ip saddr $ip limit rate over ${rate_kbyte_up} kbytes/second"
|
||||
dl_expr="ip daddr $ip limit rate over ${rate_b} bytes/second"
|
||||
ul_expr="ip saddr $ip limit rate over ${rate_b_up} bytes/second"
|
||||
fi
|
||||
|
||||
# Upload over IPv6, keyed on the MAC: at LAN ingress the frame is still
|
||||
@@ -180,9 +180,9 @@ tctl_ratelimit_add() {
|
||||
ul6_expr=""; dl6_expr=""; feed6_expr=""
|
||||
v6set="tctl_v6_$slug"
|
||||
if mac=$(tctl_target_mac "$ip"); then
|
||||
ul6_expr="meta protocol ip6 ether saddr $mac limit rate over ${rate_kbyte_up} kbytes/second"
|
||||
ul6_expr="meta protocol ip6 ether saddr $mac limit rate over ${rate_b_up} bytes/second"
|
||||
feed6_expr="meta protocol ip6 ether saddr $mac ip6 saddr != fe80::/10 update @$v6set { ip6 saddr }"
|
||||
dl6_expr="meta protocol ip6 ip6 daddr @$v6set limit rate over ${rate_kbyte} kbytes/second"
|
||||
dl6_expr="meta protocol ip6 ip6 daddr @$v6set limit rate over ${rate_b} bytes/second"
|
||||
fi
|
||||
|
||||
if [ "$TCTL_FW" = "nft" ]; then
|
||||
@@ -323,6 +323,63 @@ tctl_ratelimit_remove() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Download ceiling in kbit/s of the limit on one address, read from an
|
||||
# "nft list table netdev tm_ratelimit" dump on stdin; nothing when it has none.
|
||||
#
|
||||
# The address is compared as a whole FIELD. A grep for "daddr <ip>" also
|
||||
# matched every address it is a prefix of, so 192.168.1.5 reported
|
||||
# 192.168.1.50's limit, and the dots matched any character besides. Only the
|
||||
# first rule counts: the download rule is installed on every LAN device's
|
||||
# egress chain, so a router with several bridges holds several copies, and
|
||||
# printing all of them handed printf a multi-line "number".
|
||||
#
|
||||
# nft prints the rate in the largest unit that keeps it whole, and its units
|
||||
# step by 1024, not 1000: 1024 bytes/second lists as "1 kbytes/second" and
|
||||
# 1024 kbytes/second as "1 mbytes/second" (verified with nftables 1.0.9).
|
||||
# The readers that only knew "kbytes" turned the latter into 0 — or, in the
|
||||
# stats, into 8 kbit/s. Every unit goes back to bytes by nft's factor, and
|
||||
# bytes to kbit by the package's own, SI (see tctl_rate_bytes).
|
||||
tctl_rl_rate_kbit() {
|
||||
awk -v ip="$1" "$TCTL_NFT_RATE_AWK"'
|
||||
/limit rate over/ {
|
||||
hit = 0
|
||||
for (i = 1; i < NF; i++)
|
||||
if ($i == "daddr" && ($(i+1) == ip || $(i+1) == ip "/32")) { hit = 1; break }
|
||||
if (!hit) next
|
||||
for (j = i; j < NF - 1; j++)
|
||||
if ($j == "over") { printf "%.0f\n", tctl_rate_kbit($(j+1), $(j+2)); exit }
|
||||
}'
|
||||
}
|
||||
|
||||
# A kbit/s ceiling as the bytes/second an nft "limit rate" is written in.
|
||||
#
|
||||
# kbit/s here is SI, 1000 bits, as everywhere else in the package: the UI,
|
||||
# tc ("kbit" is 1000 bits to tc as well) and the speed graphs. nft's
|
||||
# "kbytes" is 1024 bytes, so writing rate_kbit / 8 kbytes — what the
|
||||
# limiter did — policed 2.4% above the ceiling it was given: 5000 kbit/s
|
||||
# became 5120. Bytes are exact (kbit * 1000 / 8 = kbit * 125).
|
||||
tctl_rate_bytes() {
|
||||
local b=$(( $1 * 125 ))
|
||||
[ "$b" -lt 1 ] && b=1
|
||||
echo "$b"
|
||||
}
|
||||
|
||||
# awk function shared by every reader of an nft rate: tctl_rate_kbit(n, unit)
|
||||
# turns "n <unit>/second" as nft lists it back into SI kbit/s. Prepended to
|
||||
# an awk program; a rule written before rates were stored in bytes reads as
|
||||
# what it actually polices (625 kbytes/second is 5120 kbit/s).
|
||||
TCTL_NFT_RATE_AWK='
|
||||
function tctl_rate_kbit(n, u, b) {
|
||||
if (u ~ /^bytes/) b = n
|
||||
else if (u ~ /^kbytes/) b = n * 1024
|
||||
else if (u ~ /^mbytes/) b = n * 1024 * 1024
|
||||
else if (u ~ /^gbytes/) b = n * 1024 * 1024 * 1024
|
||||
else return 0
|
||||
# Rounded here, so a reader printing %d agrees with one printing %.0f.
|
||||
return int(b * 8 / 1000 + 0.5)
|
||||
}
|
||||
'
|
||||
|
||||
tctl_ratelimit_list() {
|
||||
if [ "$TCTL_FW" = "nft" ]; then
|
||||
nft list table netdev tm_ratelimit 2>/dev/null
|
||||
|
||||
@@ -137,20 +137,19 @@ pfw_add_pause() {
|
||||
|
||||
pfw_add_limit() {
|
||||
local scope="$1" proto="$2" ip="$3" port="$4" kbit="$5"
|
||||
local cmt chain kbyte
|
||||
kbyte=$((kbit / 8))
|
||||
[ "$kbyte" -lt 1 ] && kbyte=1
|
||||
local cmt chain rate_b
|
||||
rate_b=$(tctl_rate_bytes "$kbit")
|
||||
cmt=$(pfw_comment "limit" "$proto" "${ip:-local}" "$port")
|
||||
if [ "$TCTL_FW" = "nft" ]; then
|
||||
ensure_pfw_table
|
||||
chain=$(pfw_chain_for_scope "$scope")
|
||||
if [ "$scope" = "input" ]; then
|
||||
nft add rule inet tctl_pfw "$chain" \
|
||||
"$proto" dport "$port" limit rate over "$kbyte" kbytes/second \
|
||||
"$proto" dport "$port" limit rate over "$rate_b" bytes/second \
|
||||
counter drop comment "\"$cmt\""
|
||||
else
|
||||
nft add rule inet tctl_pfw "$chain" \
|
||||
ip daddr "$ip" "$proto" dport "$port" limit rate over "$kbyte" kbytes/second \
|
||||
ip daddr "$ip" "$proto" dport "$port" limit rate over "$rate_b" bytes/second \
|
||||
counter drop comment "\"$cmt\""
|
||||
fi
|
||||
else
|
||||
@@ -371,7 +370,7 @@ do_list() {
|
||||
printf '%s' "$CTL_DUMP" | grep -q "$(pfw_comment pause "$p" "$cmt_ip" "$port")" && paused=true
|
||||
if [ "$limit_kbit" = "0" ]; then
|
||||
st=$(printf '%s' "$CTL_DUMP" | grep "$(pfw_comment limit "$p" "$cmt_ip" "$port")" | \
|
||||
grep -oE '[0-9]+ kbytes' | awk '{print $1 * 8; exit}')
|
||||
awk "$TCTL_NFT_RATE_AWK"'{ for (i = 1; i < NF - 1; i++) if ($i == "over") { printf "%.0f\n", tctl_rate_kbit($(i+1), $(i+2)); exit } }')
|
||||
[ -z "$st" ] && st=$(printf '%s' "$CTL_DUMP" | grep "$(pfw_comment limit "$p" "$cmt_ip" "$port")" | \
|
||||
grep -oE 'hashlimit-above [0-9]+' | awk '{print $2; exit}')
|
||||
[ -n "$st" ] && limit_kbit="$st"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
. /usr/local/bin/trafficctl-fw.sh
|
||||
|
||||
if [ "$TCTL_FW" = "nft" ]; then
|
||||
nft list table netdev tm_ratelimit 2>/dev/null | awk '
|
||||
nft list table netdev tm_ratelimit 2>/dev/null | awk "$TCTL_NFT_RATE_AWK"'
|
||||
/ip daddr/ && /limit rate/ && /counter/ {
|
||||
ip = ""
|
||||
rate = 0
|
||||
@@ -31,8 +31,9 @@ if [ "$TCTL_FW" = "nft" ]; then
|
||||
if ($i == "rate" && $(i+1) == "over") {
|
||||
val = $(i+2)
|
||||
gsub(/[^0-9]/, "", val)
|
||||
# stored as kbytes/second, convert back to kbit
|
||||
rate = val * 8
|
||||
# nft lists the largest whole unit, stepping by 1024
|
||||
# (see tctl_rl_rate_kbit); back to SI kbit/s.
|
||||
rate = tctl_rate_kbit(val, $(i+3))
|
||||
}
|
||||
if ($i == "counter") {
|
||||
# format: counter packets N bytes N
|
||||
|
||||
@@ -351,9 +351,12 @@ lookup_block_bytes() {
|
||||
lookup_rate_limit() {
|
||||
local ip="$1" r
|
||||
if [ "$TCTL_FW" = "nft" ]; then
|
||||
r=$(echo "$RL_DUMP" | grep "daddr $ip" | grep -oE '[0-9]+ kbytes' | awk '{print $1 * 8}')
|
||||
r=$(echo "$RL_DUMP" | tctl_rl_rate_kbit "$ip")
|
||||
else
|
||||
r=$(echo "$RL_DUMP" | grep "rl_ratelimit" | grep "$ip" | grep -oE '[0-9]+kbit' | head -1 | sed 's/kbit//')
|
||||
# The address as a whole field, not a substring (see tctl_rl_rate_kbit).
|
||||
r=$(echo "$RL_DUMP" | grep "rl_ratelimit" \
|
||||
| awk -v ip="$ip" '{ for (i = 1; i <= NF; i++) if ($i == ip || $i == ip "/32") { print; next } }' \
|
||||
| grep -oE '[0-9]+kbit' | head -1 | sed 's/kbit//')
|
||||
fi
|
||||
echo "${r:-0}"
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user