update 2026-11-01 17:41:31

This commit is contained in:
action committed 2026-11-01 17:41:31 +08:00
1 parent 98d078dd48
commit 1afa674165
62 files changed
+3735 -2921

No files matched your search

-70
View File
@@ -1,70 +0,0 @@
name: build-apk
on:
push:
tags:
- 'v*'
- 'dev-*'
workflow_dispatch:
permissions:
contents: write
env:
SDK_DIR: /builder
PKG_DIR_NAME: luci-app-adguardhome-tk
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 90
strategy:
matrix:
sdk: [x86-64-25.12.6]
container:
image: ghcr.io/openwrt/sdk:${{ matrix.sdk }}
options: --user root
defaults:
run:
shell: bash
steps:
- name: 检出代码
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: 缓存依赖源码下载
uses: actions/cache@v5
with:
path: ${{ env.SDK_DIR }}/dl
key: adguardhome-dl-v2-${{ runner.os }}-${{ matrix.sdk }}-${{ hashFiles('Makefile', '.github/workflows/build-apk.yaml', 'scripts/*.sh') }}
restore-keys: |
adguardhome-dl-v2-${{ runner.os }}-${{ matrix.sdk }}-
- name: 准备 SDK
run: bash scripts/prepare-sdk.sh
- name: 构建包
run: runuser -u buildbot -- bash scripts/build-apk.sh
- name: 整理 APK 产物
run: bash scripts/collect-apks.sh
- name: 上传 APK 压缩包
uses: actions/upload-artifact@v7
with:
name: adguardhome-apks
path: dist/*.apk
archive: true
if-no-files-found: error
- name: 发布到 Releases
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
prerelease: ${{ startsWith(github.ref_name, 'dev-') }}
make_latest: 'false'
fail_on_unmatched_files: true
files: dist/*.apk
+8 -1
View File
@@ -1,2 +1,9 @@
.DS_Store
.local
# Local debug artifacts from playwright-cli UI verification
adg-*.png
m-*.png
bs-*.png
*.png
!screenshots/*.png
.playwright-cli/
+55 -85
View File
@@ -1,109 +1,79 @@
# Copyright (C) 2018-2024 OpenWrt luci-app-adguardhome contributors
#
# Copyright (C) 2025 w9315273
# This is free software, licensed under the Apache License, Version 2.0 .
#
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-adguardhome-tk
PKG_VERSION:=2.6.0
PKG_RELEASE:=3
PKG_NAME:=luci-app-adguardhome
PKG_MAINTAINER:=kenzok8
PKG_VERSION:=1.0
PKG_RELEASE:=9
PKG_MAINTAINER:=w9315273
PKG_BUILD_DEPENDS:=luci-base/host
LUCI_TITLE:=LuCI app for AdGuardHome
LUCI_PKGARCH:=all
LUCI_DEPENDS:=+ca-certs +curl +wget-ssl +PACKAGE_$(PKG_NAME)_INCLUDE_binary:adguardhome
# luci-compat only exists in ucode-based LuCI (24.10+)
ifneq ($(wildcard $(TOPDIR)/feeds/luci/luci-compat/Makefile),)
LUCI_DEPENDS+= +luci-compat
endif
LUCI_DESCRIPTION:=LuCI support for AdGuardHome
include $(INCLUDE_DIR)/package.mk
define Package/luci-app-adguardhome-tk
SECTION:=luci
CATEGORY:=LuCI
SUBMENU:=3. Applications
TITLE:=LuCI app for AdGuardHome
PKG_MAINTAINER:=https://github.com/w9315273/luci-app-adguardhome
PKGARCH:=all
PROVIDES:=luci-app-adguardhome
CONFLICTS:=luci-app-adguardhome
DEPENDS:=+luci-base +rpcd-mod-ucode +uclient-fetch +jsonfilter +flock +ca-bundle +!(PACKAGE_libustream-openssl||PACKAGE_libustream-wolfssl):libustream-mbedtls +tar
define Package/$(PKG_NAME)/config
config PACKAGE_$(PKG_NAME)_INCLUDE_binary
bool "Include Binary File"
default n
help
When enabled, also pulls in the adguardhome binary package.
Disabled by default to avoid init.d/config name conflict with
the adguardhome package (uses lowercase init.d/adguardhome,
while this LuCI app uses /etc/init.d/AdGuardHome). The LuCI
app downloads the binary at runtime, so the package dependency
is only needed for offline / sysupgrade scenarios.
endef
define Package/luci-app-adguardhome-tk/description
LuCI support for AdGuardHome (nftables-based DNS redirect).
This package does NOT include the AdGuardHome core binary.
endef
PKG_CONFIG_DEPENDS:= CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_binary
define Package/luci-i18n-adguardhome-tk-zh-cn
SECTION:=luci
CATEGORY:=LuCI
HIDDEN:=1
TITLE:=AdGuardHome - Simplified Chinese translation
PKGARCH:=all
PROVIDES:=luci-i18n-adguardhome-zh-cn
CONFLICTS:=luci-i18n-adguardhome-zh-cn
DEPENDS:=luci-app-adguardhome-tk
DEFAULT:=LUCI_LANG_zh_Hans
endef
define Package/luci-i18n-adguardhome-tk-zh-cn/description
Simplified Chinese translation for luci-app-adguardhome-tk.
endef
define Build/Prepare
endef
define Build/Compile
endef
define Package/luci-app-adguardhome-tk/conffiles
define Package/luci-app-adguardhome/conffiles
/usr/share/AdGuardHome/links.txt
/etc/config/AdGuardHome
endef
define Package/luci-app-adguardhome-tk/install
cp -pR ./root/* $(1)/
endef
define Package/luci-i18n-adguardhome-tk-zh-cn/install
$(INSTALL_DIR) $(1)/etc/uci-defaults
echo "uci set luci.languages.zh_cn='简体中文 (Simplified Chinese)'; uci commit luci" \
> $(1)/etc/uci-defaults/luci-i18n-adguardhome-tk-zh-cn
$(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
po2lmo ./po/zh-cn/AdGuardHome.po \
$(1)/usr/lib/lua/luci/i18n/AdGuardHome.zh-cn.lmo
endef
define Package/luci-app-adguardhome-tk/postinst
define Package/luci-app-adguardhome/postinst
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] && exit 0
/etc/init.d/AdGuardHome enable >/dev/null 2>&1
enable=$$(uci get AdGuardHome.AdGuardHome.enabled 2>/dev/null)
if [ "$$enable" = "1" ]; then
/etc/init.d/AdGuardHome reload
fi
# Remove the former menu definition when upgrading.
rm -f /usr/share/luci/menu.d/luci-app-adguardhome-js.json
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
rm -f /tmp/luci-modulecache/*
/etc/init.d/rpcd reload
# Stop and disable the lowercase /etc/init.d/adguardhome that ships
# with the adguardhome binary package, so it can not race the LuCI
# app's /etc/init.d/AdGuardHome (issue #254).
if [ -x /etc/init.d/adguardhome ] && [ -x /etc/init.d/AdGuardHome ]; then
/etc/init.d/adguardhome stop >/dev/null 2>&1
/etc/init.d/adguardhome disable >/dev/null 2>&1
fi
/etc/init.d/AdGuardHome enable >/dev/null 2>&1
enable="$(uci get AdGuardHome.AdGuardHome.enabled 2>/dev/null)"
[ "$enable" = "1" ] && /etc/init.d/AdGuardHome reload >/dev/null 2>&1
rm -f /tmp/luci-indexcache
rm -f /tmp/luci-modulecache/* 2>/dev/null
exit 0
endef
define Package/luci-app-adguardhome-tk/prerm
define Package/luci-app-adguardhome/prerm
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
/etc/init.d/AdGuardHome disable
/etc/init.d/AdGuardHome stop 2>/dev/null || true
uci -q batch <<-EOF >/dev/null 2>&1
delete ucitrack.@AdGuardHome[-1]
commit ucitrack
EOF
/etc/init.d/AdGuardHome disable >/dev/null 2>&1
/etc/init.d/AdGuardHome stop >/dev/null 2>&1
ucitrack_del() {
local name="$1"
local track="$(uci show ucitrack 2>/dev/null | grep -F "$name\." | head -1)"
[ -n "$track" ] && uci -q del "$track" 2>/dev/null
}
ucitrack_del "AdGuardHome"
[ -n "$(uci show ucitrack 2>/dev/null | grep -c 'AdGuardHome')" ] || true
uci commit ucitrack 2>/dev/null
fi
exit 0
endef
$(eval $(call BuildPackage,luci-app-adguardhome-tk))
include $(TOPDIR)/feeds/luci/luci.mk
$(eval $(call BuildPackage,luci-i18n-adguardhome-tk-zh-cn))
# call BuildPackage - OpenWrt buildroot signature
$(eval $(call BuildPackage,luci-app-adguardhome))
+81
View File
@@ -0,0 +1,81 @@
# luci-app-adguardhome
适用于 OpenWrt / ImmortalWrt 的 AdGuard Home LuCI 控制面板。
> 基于 [rufengsuixing/luci-app-adguardhome](https://github.com/rufengsuixing/luci-app-adguardhome) 维护,
> 针对 **ImmortalWrt 24.10+** 进行了大量 UI 重构和 Bug 修复。
## 兼容性
| 系统 | 状态 |
|------|------|
| ImmortalWrt 24.10 / 25.12 | 主要测试目标,完全兼容 |
| OpenWrt 23.05 | 兼容 |
| OpenWrt 21.02 / 22.03 | 兼容 |
| OpenWrt 19.07 | 兼容(jail 字段回退为 TextValue) |
| OpenWrt 18.06 | 基本兼容(DynamicList 自动回退) |
## 截图
### 概览
![概览](https://raw.githubusercontent.com/kenzok8/kenzok8/main/screenshot/adguardhome/01-overview.png)
### 基础设置
![基础设置](https://raw.githubusercontent.com/kenzok8/kenzok8/main/screenshot/adguardhome/02-base-setting.png)
### 运维
![运维](https://raw.githubusercontent.com/kenzok8/kenzok8/main/screenshot/adguardhome/03-tools.png)
### 日志
![日志](https://raw.githubusercontent.com/kenzok8/kenzok8/main/screenshot/adguardhome/04-log.png)
### 手动配置
![手动配置](https://raw.githubusercontent.com/kenzok8/kenzok8/main/screenshot/adguardhome/05-manual.png)
## 功能
- **概览面板**:服务状态、重定向模式、核心版本、一键启停 + Web 控制台快捷入口
- **基础设置**:启用开关、管理端口、核心更新、5553 重定向(4 种模式)、路径配置、进程参数
- **工具与任务**:改密、sysupgrade 保留文件、关机备份、Crontab 计划任务
- **日志查看**:实时轮询、反向/本地时区切换、下载、清空
- **手动配置**:YAML 编辑器(CodeMirror 可选),保存前二次确认
- **核心自动更新**:GitHub/AdGuard 多源下载 → UPX 压缩 → 重启服务
- **UPX 压缩**:>8MB 二进制自动压缩(支持 x86_64 / arm64 / mips 等架构)
- **计划任务**:自动更新核心、切割日志、IPv6 hosts 同步
- **升级保护**:sysupgrade 保留文件 + 关机备份
## 安装
### Feed 方式
```bash
echo 'src-git adguardhome https://github.com/kenzok78/luci-app-adguardhome' >> feeds.conf.default
./scripts/feeds update -a && ./scripts/feeds install -a -p adguardhome
make package/luci-app-adguardhome/compile V=s
```
### 直接编译
```bash
git clone https://github.com/kenzok78/luci-app-adguardhome.git package/luci-app-adguardhome
make menuconfig # LuCI → Applications → luci-app-adguardhome
make -j$(nproc) V=s
```
## 重定向模式
| 模式 | 说明 |
|------|------|
| 不启用 | 仅运行,不拦截 DNS |
| 作为 dnsmasq 上游 | dnsmasq 转发到 AGH |
| 53 端口劫持 | iptables 重定向到 AGH |
| 替代 dnsmasq | AGH 直接监听 53 |
## 许可证
Apache-2.0
-35
View File
@@ -1,35 +0,0 @@
# AdGuard Home for LuCI
Manage AdGuard Home in OpenWrt's LuCI interface. LuCI pages are rewritten in JavaScript, with an rpcd + ucode backend and nftables-based DNS forwarding.
[简体中文](README.md) · [English](README.en.md)
## Features
- Service status and core updates
- DNS handling through firewall forwarding, dnsmasq upstream, or port 53 replacement
- YAML configuration editing, validation, and web login password changes
- Runtime logs with reverse order, local timestamps, download, and a verbose logging toggle
> [!TIP]
> **Quick installation**
>
> Run in the terminal of an OpenWrt router using the APK package manager:
>
> ```sh
> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/scripts/install.sh && sh /tmp/install-adguardhome.sh
> ```
## Screenshots
### Basic settings
![Basic settings](screenshots/base-en.png)
### Manual configuration
![Manual configuration](screenshots/manual-en.png)
### Logs
![Runtime logs](screenshots/log-en.png)
-35
View File
@@ -1,35 +0,0 @@
# AdGuard Home for LuCI
在 OpenWrt 的 LuCI 界面中管理 AdGuard Home。使用 JavaScript 重写 LuCI 页面,后端采用 rpcd + ucode,并适配 nftables DNS 转发。
[简体中文](README.md) · [English](README.en.md)
## 功能
- 服务状态查看与核心更新
- 防火墙转发、dnsmasq 上游、替代 dnsmasq 使用 53 端口等 DNS 接入方式
- YAML 配置编辑与校验、网页登录密码修改
- 运行日志查看,支持逆序、本地时间、下载和详细日志开关
> [!TIP]
> **一键安装**
>
> 在使用 APK 包管理器的 OpenWrt 路由器终端执行:
>
> ```sh
> uclient-fetch -O /tmp/install-adguardhome.sh https://raw.githubusercontent.com/w9315273/luci-app-adguardhome/main/scripts/install.sh && sh /tmp/install-adguardhome.sh zh-cn
> ```
## 截图
### 基础设置
![基础设置](screenshots/base-zh-cn.png)
### 手动设置
![手动设置](screenshots/manual-zh-cn.png)
### 日志
![运行日志](screenshots/log-zh-cn.png)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

@@ -0,0 +1,360 @@
/* luci-app-adguardhome card-style theme (inspired by luci-app-clashoo) */
.adg-wrap {
--adg-primary-rgb: 0, 122, 255;
--adg-primary: rgba(var(--adg-primary-rgb), 0.9);
--adg-card-bg: rgba(128, 128, 128, 0.04);
--adg-card-border: rgba(128, 128, 128, 0.16);
--adg-card-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
--adg-card-shadow-hover: 0 6px 24px rgba(0, 0, 0, 0.07);
--adg-radius: 12px;
--adg-label-muted: rgba(60, 66, 76, 0.85);
--adg-input-bg: rgba(128, 128, 128, 0.05);
--adg-input-border: rgba(128, 128, 128, 0.2);
--adg-ok: #2db84a;
--adg-warn: #e0a02f;
--adg-bad: #d05050;
}
/* Dark mode: triggered either by OS preference, or by an explicit class
set from head.htm when the active LuCI theme renders a dark background
regardless of OS preference (Argon mode=dark, bootstrap-dark, etc.) */
@media (prefers-color-scheme: dark) {
.adg-wrap {
--adg-card-bg: rgba(255, 255, 255, 0.05);
--adg-card-border: rgba(255, 255, 255, 0.16);
--adg-input-bg: rgba(255, 255, 255, 0.06);
--adg-input-border: rgba(255, 255, 255, 0.22);
--adg-label-muted: rgba(220, 226, 232, 0.88);
}
}
.adg-wrap.adg-dark {
--adg-card-bg: rgba(255, 255, 255, 0.05);
--adg-card-border: rgba(255, 255, 255, 0.16);
--adg-input-bg: rgba(255, 255, 255, 0.06);
--adg-input-border: rgba(255, 255, 255, 0.22);
--adg-label-muted: rgba(220, 226, 232, 0.88);
}
/* LuCI's selected theme is authoritative. If the OS/browser prefers dark but
Argon/bootstrap is in light mode, keep contrast tuned for a light surface. */
.adg-wrap:not(.adg-dark) {
--adg-card-bg: rgba(255, 255, 255, 0.45);
--adg-card-border: rgba(50, 50, 93, 0.12);
--adg-input-bg: rgba(255, 255, 255, 0.55);
--adg-input-border: rgba(50, 50, 93, 0.18);
--adg-label-muted: rgba(50, 50, 93, 0.78);
color: #32325d;
}
.adg-wrap:not(.adg-dark) .cbi-value-description,
.adg-wrap:not(.adg-dark) .cbi-section-descr,
.adg-wrap:not(.adg-dark) .cbi-map-descr,
.adg-wrap:not(.adg-dark) .adg-ov-card .title,
.adg-wrap:not(.adg-dark) .adg-ov-card .sub {
color: rgba(50, 50, 93, 0.70) !important;
opacity: 1 !important;
}
.adg-wrap:not(.adg-dark) .cbi-value > .cbi-value-title,
.adg-wrap:not(.adg-dark) .cbi-checkbox {
color: rgba(50, 50, 93, 0.82) !important;
opacity: 1 !important;
}
.adg-wrap:not(.adg-dark) .cbi-value-field input[type="text"],
.adg-wrap:not(.adg-dark) .cbi-value-field input[type="password"],
.adg-wrap:not(.adg-dark) .cbi-value-field input[type="number"],
.adg-wrap:not(.adg-dark) .cbi-value-field select,
.adg-wrap:not(.adg-dark) .cbi-value-field textarea {
color: #32325d !important;
-webkit-text-fill-color: #32325d;
opacity: 1 !important;
}
.adg-wrap:not(.adg-dark) .cbi-value-field input:disabled,
.adg-wrap:not(.adg-dark) .cbi-value-field select:disabled,
.adg-wrap:not(.adg-dark) .cbi-value-field textarea:disabled {
color: #5f6f86 !important;
-webkit-text-fill-color: #5f6f86;
opacity: 1 !important;
}
/* Some Argon dark rules set transparent input bg with !important — override
so our inputs stay visible. */
.adg-wrap.adg-dark .cbi-value-field input[type="text"],
.adg-wrap.adg-dark .cbi-value-field input[type="password"],
.adg-wrap.adg-dark .cbi-value-field input[type="number"],
.adg-wrap.adg-dark .cbi-value-field select,
.adg-wrap.adg-dark .cbi-value-field textarea {
background: var(--adg-input-bg) !important;
border-color: var(--adg-input-border) !important;
color: #e6eaf0 !important;
}
/* Help text + section descriptions — Argon paints them #ccc, bootstrap-dark
leaves them as default gray-on-dark which is also low-contrast. */
.adg-wrap.adg-dark .cbi-value-description,
.adg-wrap.adg-dark .cbi-section-descr,
.adg-wrap.adg-dark .cbi-map-descr {
color: rgba(220, 226, 232, 0.78) !important;
}
.adg-wrap.adg-dark .cbi-value > .cbi-value-title {
color: var(--adg-label-muted) !important;
}
.adg-wrap.adg-dark .cbi-section > h3,
.adg-wrap.adg-dark .cbi-section > legend {
color: #e6eaf0 !important;
}
/* Cards are constrained for comfortable reading; form fills its natural width.
Bootstrap theme has sidebar; Argon is full-width. Let the theme decide the
outer container width — we only cap the card content. */
.adg-wrap .cbi-section {
max-width: 1000px;
}
/* every CBI section becomes a card */
.adg-wrap .cbi-section {
background: var(--adg-card-bg);
border: 1px solid var(--adg-card-border);
border-radius: var(--adg-radius);
box-shadow: var(--adg-card-shadow);
padding: 18px 22px;
margin: 16px 0;
transition: box-shadow .2s ease;
}
.adg-wrap .cbi-section:hover {
box-shadow: var(--adg-card-shadow-hover);
}
/* section heading inside card */
.adg-wrap .cbi-section > h3,
.adg-wrap .cbi-section > legend {
background: transparent !important;
border: 0 !important;
box-shadow: none !important;
padding: 0 0 12px 0 !important;
margin: 0 0 12px 0 !important;
font-size: 15px;
font-weight: 600;
letter-spacing: .2px;
border-bottom: 1px solid var(--adg-card-border) !important;
}
/* label column slimmer (default LuCI is ~280px) */
.adg-wrap .cbi-value > .cbi-value-title {
width: 170px;
min-width: 170px;
padding-right: 12px;
color: var(--adg-label-muted);
font-weight: 500;
}
/* inputs */
.adg-wrap .cbi-value-field input[type="text"],
.adg-wrap .cbi-value-field input[type="password"],
.adg-wrap .cbi-value-field input[type="number"],
.adg-wrap .cbi-value-field select,
.adg-wrap .cbi-value-field textarea {
background: var(--adg-input-bg);
border: 1px solid var(--adg-input-border);
border-radius: 8px;
padding: 6px 10px;
max-width: 420px;
width: 100%;
box-shadow: none;
}
.adg-wrap .cbi-value-field textarea {
max-width: 420px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
/* long-form textareas need full width (URL lists / YAML editor) */
.adg-wrap .cbi-value-field textarea[id$="downloadlinks"],
.adg-wrap .cbi-value-field textarea[id$="escconf"],
.adg-wrap .cbi-value-field textarea#cbid\.logview\.1\.conf {
max-width: 100%;
}
.adg-wrap .cbi-value-field input:focus,
.adg-wrap .cbi-value-field select:focus,
.adg-wrap .cbi-value-field textarea:focus {
border-color: var(--adg-primary);
outline: none;
box-shadow: 0 0 0 3px rgba(var(--adg-primary-rgb), 0.15);
}
/* help text */
.adg-wrap .cbi-value-description {
color: var(--adg-label-muted);
font-size: 12.5px;
margin-top: 4px;
}
/* between-row separator gone (the card itself is the boundary) */
.adg-wrap .cbi-value {
border-bottom: 0 !important;
padding: 8px 0;
}
/* MultiValue checkbox groups — let items flow inline and wrap naturally */
.adg-wrap .cbi-value-field .cbi-checkbox {
display: inline-flex;
align-items: center;
gap: 4px;
margin-right: 16px;
margin-bottom: 4px;
font-size: 13px;
white-space: nowrap;
}
.adg-wrap .cbi-value-field .cbi-checkbox input[type="checkbox"] {
width: auto;
height: auto;
margin: 0;
flex-shrink: 0;
}
/* DynamicList — compact row layout */
.adg-wrap .cbi-dynlist .cbi-value-title { display: none; }
.adg-wrap .cbi-dynlist .cbi-value { padding: 4px 0; }
.adg-wrap .cbi-dynlist .cbi-value-field { flex: 1; max-width: 420px; }
.adg-wrap .cbi-dynlist .cbi-value-field input[type="text"] { max-width: 100%; }
/* AdGuardHome custom status row */
.adg-status-row {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 6px 0 18px 0;
}
.adg-status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 999px;
background: var(--adg-card-bg);
border: 1px solid var(--adg-card-border);
font-size: 13px;
color: var(--adg-label-muted);
}
.adg-status-pill .dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--adg-label-muted);
}
.adg-status-pill.ok .dot { background: var(--adg-ok); }
.adg-status-pill.warn .dot { background: var(--adg-warn); }
.adg-status-pill.bad .dot { background: var(--adg-bad); }
.adg-status-pill.ok { color: var(--adg-ok); border-color: rgba(45,184,74,.3); }
.adg-status-pill.bad { color: var(--adg-bad); border-color: rgba(208,80,80,.3); }
/* CBI button color fallback — ensure visibility on themes that use
transparent backgrounds (e.g. Argon light) */
.adg-wrap input.cbi-button-apply,
.adg-wrap button.cbi-button-apply {
background: #007aff !important;
border-color: #007aff !important;
color: #fff !important;
}
.adg-wrap input.cbi-button-apply:hover,
.adg-wrap button.cbi-button-apply:hover {
background: #0064d2 !important;
border-color: #0064d2 !important;
}
.adg-wrap input.cbi-button-remove,
.adg-wrap button.cbi-button-remove {
background: #d05050 !important;
border-color: #d05050 !important;
color: #fff !important;
}
.adg-wrap input.cbi-button-remove:hover,
.adg-wrap button.cbi-button-remove:hover {
background: #b03d3d !important;
border-color: #b03d3d !important;
}
.adg-wrap input.cbi-button-save,
.adg-wrap button.cbi-button-save {
background: #2d7d46 !important;
border-color: #2d7d46 !important;
color: #fff !important;
}
.adg-wrap input.cbi-button-save:hover,
.adg-wrap button.cbi-button-save:hover {
background: #226434 !important;
border-color: #226434 !important;
}
/* collapsible sections */
.adg-wrap .cbi-section.adg-collapsible > h3,
.adg-wrap .cbi-section.adg-collapsible > legend {
cursor: pointer;
position: relative;
padding-right: 28px !important;
user-select: none;
}
.adg-wrap .cbi-section.adg-collapsible > h3::after,
.adg-wrap .cbi-section.adg-collapsible > legend::after {
content: "";
position: absolute;
right: 4px;
top: 50%;
width: 8px;
height: 8px;
border-right: 2px solid var(--adg-label-muted);
border-bottom: 2px solid var(--adg-label-muted);
transform: translateY(-65%) rotate(45deg);
transition: transform .2s ease;
}
.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > h3::after,
.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > legend::after {
transform: translateY(-30%) rotate(-45deg);
}
.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > .cbi-value,
.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > div.cbi-section-node,
.adg-wrap .cbi-section.adg-collapsible.adg-collapsed > fieldset {
display: none;
}
/* mobile tweaks — reset table/flex layout entirely */
@media (max-width: 720px) {
.adg-wrap .cbi-section {
padding: 12px 10px;
margin: 8px 0;
}
/* force flex column, kill any theme table layout */
.adg-wrap .cbi-value {
display: flex !important;
flex-direction: column;
align-items: stretch;
border-bottom: 0 !important;
padding: 6px 0;
}
.adg-wrap .cbi-value > .cbi-value-title {
width: 100% !important;
min-width: 0 !important;
padding: 0 0 3px 0 !important;
flex: none;
text-align: left;
}
.adg-wrap .cbi-value > .cbi-value-field {
width: 100% !important;
flex: none;
}
.adg-wrap .cbi-value-field input[type="text"],
.adg-wrap .cbi-value-field input[type="password"],
.adg-wrap .cbi-value-field input[type="number"],
.adg-wrap .cbi-value-field select {
max-width: 100%;
}
.adg-wrap .cbi-value-field > div {
width: 100%;
}
/* checkboxes — single column */
.adg-wrap .cbi-value-field .cbi-checkbox {
display: flex;
white-space: normal;
margin-right: 0;
margin-bottom: 6px;
width: 100%;
}
/* DynamicList — expand */
.adg-wrap .cbi-dynlist .cbi-value-field { max-width: 100%; }
/* hide tooltips on touch */
.adg-wrap .cbi-tooltip-container,
.adg-wrap .cbi-tooltip {
display: none !important;
}
}
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
// NOTE: luci-app-adguardhome 使用多页面 CBI 模板架构,luci 23.05+ 通过 menu.d alias 跳转至 base 子页。
// 此 JS 文件不直接调用,保留供参考。
'use strict';
'require view';
'require rpc';
'require form';
return view.extend({
render: function() {
// 实际界面由 luasrc/model/cbi/AdGuardHome/ 下多个 CBI 模型提供
return E('p', {}, _('Loading…'));
}
});
@@ -0,0 +1,274 @@
module("luci.controller.AdGuardHome", package.seeall)
local fs = require "nixio.fs"
local http = require "luci.http"
local uci = require "luci.model.uci".cursor()
local function service_running()
local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
if not fs.access(binpath) then
return false
end
return luci.sys.call("/etc/init.d/AdGuardHome status >/dev/null 2>&1") == 0
end
function index()
-- luci 23.05+ 已通过 menu.d JSON 注册菜单,无需重复注册
if not nixio.fs.access("/usr/share/luci/menu.d/luci-app-adguardhome.json") then
local page = entry({"admin", "services", "AdGuardHome"},
alias("admin", "services", "AdGuardHome", "overview"),
_("AdGuard Home"), 10)
page.dependent = true
page.acl_depends = { "luci-app-adguardhome" }
entry({"admin", "services", "AdGuardHome", "overview"},
cbi("AdGuardHome/overview"), _("概览"), 1).leaf = true
entry({"admin", "services", "AdGuardHome", "base"},
cbi("AdGuardHome/base"), _("基础设置"), 2).leaf = true
entry({"admin", "services", "AdGuardHome", "tools"},
cbi("AdGuardHome/tools"), _("运维"), 3).leaf = true
entry({"admin", "services", "AdGuardHome", "log"},
form("AdGuardHome/log"), _("日志"), 4).leaf = true
entry({"admin", "services", "AdGuardHome", "manual"},
cbi("AdGuardHome/manual"), _("手动配置"), 5).leaf = true
end
-- API 路由在新旧版本均需注册
entry({"admin", "services", "AdGuardHome", "status"},
call("act_status"), nil).leaf = true
entry({"admin", "services", "AdGuardHome", "toggle"},
call("toggle_service"), nil).leaf = true
entry({"admin", "services", "AdGuardHome", "check"},
call("check_update"), nil)
entry({"admin", "services", "AdGuardHome", "doupdate"},
call("do_update"), nil)
entry({"admin", "services", "AdGuardHome", "getlog"},
call("get_log"), nil)
entry({"admin", "services", "AdGuardHome", "dodellog"},
call("do_dellog"), nil)
entry({"admin", "services", "AdGuardHome", "reloadconfig"},
call("reload_config"), nil)
entry({"admin", "services", "AdGuardHome", "gettemplateconfig"},
call("get_template_config"), nil)
end
local function gen_template_config()
local dns_servers = ""
local resolv_auto = "/tmp/resolv.conf.d/resolv.conf.auto"
if fs.access(resolv_auto) then
for line in io.lines(resolv_auto) do
local ns = line:match("^[^#]*nameserver%s+([^%s]+)")
if ns then
dns_servers = dns_servers .. " - " .. ns .. "\n"
end
end
end
local tmpl = "/usr/share/AdGuardHome/AdGuardHome_template.yaml"
local f = io.open(tmpl, "r")
if not f then return "" end
local lines = {}
for line in f:lines() do
if line == "#bootstrap_dns" then
table.insert(lines, dns_servers)
elseif line == "#upstream_dns" then
table.insert(lines, dns_servers)
else
table.insert(lines, line)
end
end
f:close()
return table.concat(lines, "\n")
end
function get_template_config()
http.prepare_content("text/plain; charset=utf-8")
http.write(gen_template_config())
end
function reload_config()
fs.remove("/tmp/AdGuardHometmpconfig.yaml")
http.prepare_content("application/json")
http.write('{}')
end
function act_status()
local result = {}
result.running = service_running()
local redir = fs.readfile("/var/run/AdGredir")
result.redirect = (redir == "1")
http.prepare_content("application/json")
http.write_json(result)
end
function toggle_service()
local enabled = http.formvalue("enabled") == "1" and "1" or "0"
local old_enabled = uci:get("AdGuardHome", "AdGuardHome", "enabled") == "1" and "1" or "0"
local result = {
enabled = (enabled == "1")
}
-- Preflight: when enabling, make sure the binary is actually present.
-- Without this, init.d reload silently fails and the UI only sees an
-- opaque "Operation failed" toast (issue #254).
if enabled == "1" then
local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
if not fs.access(binpath) then
result.success = false
result.enabled = (old_enabled == "1")
result.running = false
result.message = "找不到 AdGuardHome 二进制文件(" .. binpath ..
"),请先在「运维」页面下载。"
http.prepare_content("application/json")
http.write_json(result)
return
end
end
uci:set("AdGuardHome", "AdGuardHome", "enabled", enabled)
uci:commit("AdGuardHome")
local rc
if enabled == "1" then
-- Detach the reload from the CGI request so the HTTP call returns
-- immediately while the daemon comes up. Fully redirecting fds + `&`
-- is enough — avoid start-stop-daemon, whose busybox applet is not
-- enabled on some images (e.g. immortalwrt 25.10), where it would
-- exit 127 and make the toggle wrongly report a start failure (#254).
rc = luci.sys.call("/etc/init.d/AdGuardHome reload >/dev/null 2>&1 &")
else
rc = luci.sys.call("/etc/init.d/AdGuardHome stop >/dev/null 2>&1")
end
if enabled == "1" then
result.running = false
else
result.running = service_running()
for _ = 1, 2 do
if not result.running then
break
end
luci.sys.call("sleep 1")
result.running = service_running()
end
end
result.pending = (enabled == "1" and not result.running)
result.success = (rc == 0) and (enabled == "1" or not result.running)
if not result.success then
uci:set("AdGuardHome", "AdGuardHome", "enabled", old_enabled)
uci:commit("AdGuardHome")
result.enabled = (old_enabled == "1")
result.message = enabled == "1" and "AdGuardHome start failed" or "AdGuardHome stop failed"
end
http.prepare_content("application/json")
http.write_json(result)
end
function do_update()
fs.writefile("/var/run/lucilogpos", "0")
http.prepare_content("application/json")
http.write('{}')
local arg = ""
if luci.http.formvalue("force") == "1" then
arg = "force"
end
local script = "/usr/share/AdGuardHome/update_core.sh"
if fs.access("/var/run/update_core") then
if arg == "force" then
-- Security fix: script 路径是固定常量,arg 只能是 "force" 或 "",无注入风险
luci.sys.exec("pkill -f '" .. script .. "' 2>/dev/null; " .. script .. " " .. arg .. " >/tmp/AdGuardHome_update.log 2>&1 &")
end
else
luci.sys.exec(script .. " " .. arg .. " >/tmp/AdGuardHome_update.log 2>&1 &")
end
end
function get_log()
local logfile = uci:get("AdGuardHome", "AdGuardHome", "logfile")
if not logfile or logfile == "" then
http.write("no log available\n")
return
end
if logfile == "syslog" then
if not fs.access("/var/run/AdGuardHomesyslog") then
luci.sys.exec("(/usr/share/AdGuardHome/getsyslog.sh &); sleep 1;")
end
logfile = "/tmp/AdGuardHometmp.log"
fs.writefile("/var/run/AdGuardHomesyslog", "1")
elseif not fs.access(logfile) then
http.write("")
return
end
http.prepare_content("text/plain; charset=utf-8")
local fdp = 0
if fs.access("/var/run/lucilogreload") then
fdp = 0
fs.remove("/var/run/lucilogreload")
else
local pos = fs.readfile("/var/run/lucilogpos")
fdp = tonumber(pos) or 0
end
local f = io.open(logfile, "r")
if not f then
http.write("")
return
end
f:seek("set", fdp)
local content = f:read(2048000) or ""
fdp = f:seek()
f:close()
fs.writefile("/var/run/lucilogpos", tostring(fdp))
http.write(content)
end
function do_dellog()
local logfile = uci:get("AdGuardHome", "AdGuardHome", "logfile")
if logfile and logfile ~= "" and logfile ~= "syslog" then
fs.writefile(logfile, "")
end
http.prepare_content("application/json")
http.write('{}')
end
function check_update()
http.prepare_content("text/plain; charset=utf-8")
local pos = fs.readfile("/var/run/lucilogpos")
local fdp = tonumber(pos) or 0
local f = io.open("/tmp/AdGuardHome_update.log", "r")
if not f then
http.write("")
return
end
f:seek("set", fdp)
local content = f:read(2048000) or ""
fdp = f:seek()
f:close()
fs.writefile("/var/run/lucilogpos", tostring(fdp))
if fs.access("/var/run/update_core") then
http.write(content)
else
http.write(content .. "\0")
end
end
@@ -0,0 +1,244 @@
require("luci.sys")
require("luci.util")
require("io")
local m, s, o, o1
local fs = require "nixio.fs"
local uci = require "luci.model.uci".cursor()
local configpath = uci:get("AdGuardHome", "AdGuardHome", "configpath") or "/etc/AdGuardHome.yaml"
local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
local httpport = uci:get("AdGuardHome", "AdGuardHome", "httpport") or "3000"
local function service_running()
if not fs.access(binpath) then
return false
end
return luci.sys.call("/etc/init.d/AdGuardHome status >/dev/null 2>&1") == 0
end
m = Map("AdGuardHome", "AdGuard Home")
m.description = translate("Free and open source, powerful network-wide ads & trackers blocking DNS server.")
-- Inject card-style CSS for AGH pages
-- ============================================================
-- Section 1: Basic Settings
-- ============================================================
s = m:section(NamedSection, "AdGuardHome", "AdGuardHome", "基本设置")
o = s:option(Flag, "enabled", translate("Enable"))
o.default = "0"
o.optional = false
o.template = "AdGuardHome/enable_switch"
o.cfgvalue = function()
return service_running() and "1" or "0"
end
o = s:option(Value, "httpport", translate("Browser management port"))
o.placeholder = "3000"
o.default = "3000"
o.datatype = "port"
o.optional = false
o = s:option(Button, "restart", translate("Update"))
o.inputtitle = translate("Update core version")
o.template = "AdGuardHome/AdGuardHome_check"
o.showfastconfig = not fs.access(configpath)
o = s:option(ListValue, "release_channel", translate("更新通道"),
translate("正式版稳定;预发布版抢先体验 AdGuard 团队 beta 构建。"))
o:value("stable", translate("正式版 (stable)"))
o:value("beta", translate("预发布版 (beta)"))
o.default = "stable"
o.rmempty = false
local port = luci.sys.exec("awk '/^ port:/{print $2;exit}' " .. configpath .. " 2>/dev/null")
if not port or port == "" then
port = "?"
end
o = s:option(ListValue, "redirect", port .. translate("Redirect"), translate("AdGuardHome redirect mode"))
o:value("none", translate("不启用"))
o:value("dnsmasq-upstream", translate("作为 dnsmasq 上游服务器"))
o:value("redirect", translate("将 53 端口劫持到 AdGuardHome"))
o:value("exchange", translate("使用 53 端口替代 dnsmasq"))
o.default = "none"
o.optional = true
-- ============================================================
-- Section 2: Paths & Logs
-- ============================================================
s = m:section(NamedSection, "AdGuardHome", "AdGuardHome", "路径与日志")
o = s:option(Value, "binpath", translate("Bin Path"), translate("AdGuardHome Bin path if no bin will auto download"))
o.default = "/usr/bin/AdGuardHome"
o.datatype = "string"
o.optional = false
o.rmempty = false
o.validate = function(self, value)
if value == "" then return nil end
if fs.stat(value, "type") == "dir" then
fs.rmdir(value)
end
if fs.stat(value, "type") == "dir" then
m.message = (m.message or "") .. "\n" .. translate("Error: bin path is a directory")
return nil
end
return value
end
o = s:option(Value, "configpath", translate("Config Path"), translate("AdGuardHome config path"))
o.default = "/etc/AdGuardHome.yaml"
o.datatype = "string"
o.optional = false
o.rmempty = false
o.validate = function(self, value)
if not value then return nil end
if fs.stat(value, "type") == "dir" then
fs.rmdir(value)
end
if fs.stat(value, "type") == "dir" then
m.message = (m.message or "") .. "\n" .. translate("Error: config path is a directory")
return nil
end
return value
end
o = s:option(Value, "workdir", translate("Work dir"), translate("AdGuardHome work dir include rules,audit log and database"))
o.default = "/etc/AdGuardHome"
o.datatype = "string"
o.optional = false
o.rmempty = false
o.validate = function(self, value)
if value == "" then return nil end
if fs.stat(value, "type") == "reg" then
m.message = (m.message or "") .. "\n" .. translate("Error: work dir is a file")
return nil
end
if value:sub(-1) == "/" then
return value:sub(1, -2)
end
return value
end
o = s:option(Value, "logfile", translate("Runtime log file"), translate("AdGuardHome runtime Log file if 'syslog': write to system log;if empty no log"))
o.datatype = "string"
o.rmempty = true
o.validate = function(self, value)
if value and value ~= "" and fs.stat(value, "type") == "dir" then
fs.rmdir(value)
end
if value and value ~= "" and fs.stat(value, "type") == "dir" then
m.message = (m.message or "") .. "\n" .. translate("Error: log file is a directory")
return nil
end
return value
end
o = s:option(Flag, "verbose", translate("Verbose log"))
o.default = "0"
o.optional = true
-- ============================================================
-- Section 3: Advanced Settings
-- ============================================================
s = m:section(NamedSection, "AdGuardHome", "AdGuardHome", "高级设置")
o = s:option(ListValue, "upxflag", translate("use upx to compress bin after download"))
o:value("", translate("none"))
o:value("-1", translate("compress faster"))
o:value("-9", translate("compress better"))
o:value("--best", translate("compress best(can be slow for big files)"))
o:value("--brute", translate("try all available compression methods & filters [slow]"))
o:value("--ultra-brute", translate("try even more compression variants [very slow]"))
o.default = ""
o.description = translate("bin use less space,but may have compatibility issues")
o.rmempty = true
o = s:option(Value, "user", translate("Service user"), translate("User the service runs under. If empty, defaults to 'adguardhome'."))
o.placeholder = "adguardhome"
o.datatype = "string"
o.rmempty = true
o = s:option(Value, "group", translate("Service group"), translate("Group the service runs under. If empty, defaults to 'adguardhome'."))
o.placeholder = "adguardhome"
o.datatype = "string"
o.rmempty = true
-- Helper:兼容把旧的 newline-separated 字符串读成 table(仅一次性迁移期需要)
local function split_legacy_list(value)
if type(value) ~= "string" or value == "" then return value end
if not value:find("[\r\n]") then return value end
local t = {}
for line in value:gmatch("[^\r\n]+") do
line = line:match("^%s*(.-)%s*$")
if line ~= "" then t[#t+1] = line end
end
return t
end
o = s:option(DynamicList, "jail_mount", translate("只读文件访问"),
translate("AdGuardHome 可以读取的目录或文件路径,每行一项。"))
o.placeholder = "/etc/hosts"
o.rmempty = true
o.cfgvalue = function(self, section)
return split_legacy_list(uci:get("AdGuardHome", section, "jail_mount"))
end
o = s:option(DynamicList, "jail_mount_rw", translate("读写文件访问"),
translate("AdGuardHome 可以读写的目录或文件路径,每行一项。"))
o.placeholder = "/var/log/AdGuardHome"
o.rmempty = true
o.cfgvalue = function(self, section)
return split_legacy_list(uci:get("AdGuardHome", section, "jail_mount_rw"))
end
-- Probe system memory to suggest a sane MiB cap (≈50% of total RAM)
local total_mem_kb = tonumber(luci.sys.exec("awk '/MemTotal/{print $2}' /proc/meminfo 2>/dev/null")) or 0
local mem_suggest_mib = (total_mem_kb > 0) and math.floor(total_mem_kb / 1024 / 2) or 128
o = s:option(Value, "memlimit", translate("内存上限 (MiB)"),
translate("达到阈值时主动触发 GC,避免被系统 OOM 杀进程。留空 = 不限制。建议设为系统内存的 50%。"))
o.datatype = "uinteger"
o.placeholder = translate("推荐 ") .. tostring(mem_suggest_mib)
o.rmempty = true
o = s:option(Flag, "waitonboot", translate("On boot when network ok restart"))
o.default = "1"
o.rmempty = true
-- ============================================================
-- Commit handler
-- ============================================================
fs.writefile("/var/run/lucilogpos", "0")
function m.on_commit(map)
if fs.access("/var/run/AdGserverdis") then
io.popen("/etc/init.d/AdGuardHome reload &")
return
end
local ucitracktest = uci:get("AdGuardHome", "AdGuardHome", "ucitracktest")
if ucitracktest == "1" then
return
elseif ucitracktest == "0" then
io.popen("/etc/init.d/AdGuardHome reload &")
else
if fs.access("/var/run/AdGlucitest") then
uci:set("AdGuardHome", "AdGuardHome", "ucitracktest", "0")
io.popen("/etc/init.d/AdGuardHome reload &")
else
fs.writefile("/var/run/AdGlucitest", "")
if ucitracktest == "2" then
uci:set("AdGuardHome", "AdGuardHome", "ucitracktest", "1")
else
uci:set("AdGuardHome", "AdGuardHome", "ucitracktest", "2")
end
end
uci:save("AdGuardHome")
end
end
return m
@@ -0,0 +1,16 @@
local fs=require"nixio.fs"
local uci=require"luci.model.uci".cursor()
local f,t
f=SimpleForm("logview")
f.reset = false
f.submit = false
t=f:field(TextValue,"conf")
t.rmempty=true
t.rows=20
t.template="AdGuardHome/log"
t.readonly="readonly"
local logfile=uci:get("AdGuardHome","AdGuardHome","logfile") or ""
t.timereplace=(logfile~="syslog" and logfile~="" )
t.pollcheck=logfile~=""
fs.writefile("/var/run/lucilogreload","")
return f
@@ -0,0 +1,121 @@
local m, s, o
local fs = require "nixio.fs"
local uci = require "luci.model.uci".cursor()
local sys = require "luci.sys"
require("string")
require("io")
require("table")
m = Map("AdGuardHome")
local configpath = uci:get("AdGuardHome", "AdGuardHome", "configpath") or "/etc/AdGuardHome.yaml"
local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
s = m:section(TypedSection, "AdGuardHome")
s.anonymous = true
s.addremove = false
o = s:option(TextValue, "escconf")
o.rows = 66
o.wrap = "off"
o.rmempty = true
o.cfgvalue = function(self, section)
local tmp = fs.readfile("/tmp/AdGuardHometmpconfig.yaml")
if tmp then return tmp end
local cfg = fs.readfile(configpath)
if cfg then return cfg end
local dns_servers = ""
local resolv_auto = "/tmp/resolv.conf.d/resolv.conf.auto"
if fs.access(resolv_auto) then
for line in io.lines(resolv_auto) do
local ns = line:match("^[^#]*nameserver%s+([^%s]+)")
if ns then
dns_servers = dns_servers .. " - " .. ns .. "\n"
end
end
end
local tmpl = "/usr/share/AdGuardHome/AdGuardHome_template.yaml"
local f = io.open(tmpl, "r")
if not f then return "" end
local lines = {}
for line in f:lines() do
if line == "#bootstrap_dns" then
table.insert(lines, dns_servers)
elseif line == "#upstream_dns" then
table.insert(lines, dns_servers)
else
table.insert(lines, line)
end
end
f:close()
return table.concat(lines, "\n")
end
o.validate = function(self, value)
if not value then return nil end
fs.writefile("/tmp/AdGuardHometmpconfig.yaml", value:gsub("\r\n", "\n"))
if fs.access(binpath) then
local ret = sys.call(binpath .. " -c /tmp/AdGuardHometmpconfig.yaml --check-config 2>/tmp/AdGuardHometest.log")
if ret == 0 then
return value
end
else
return value
end
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "AdGuardHome", "manual"))
return nil
end
o.write = function(self, section, value)
if fs.access("/tmp/AdGuardHometmpconfig.yaml") then
fs.move("/tmp/AdGuardHometmpconfig.yaml", configpath)
end
end
o.remove = function(self, section, value)
fs.writefile(configpath, "")
end
o = s:option(DummyValue, "")
o.anonymous = true
o.template = "AdGuardHome/yamleditor"
if not fs.access(binpath) then
o.description = translate("WARNING: no binary found, config validation will be skipped")
end
if fs.access("/tmp/AdGuardHometest.log") then
local errlog = fs.readfile("/tmp/AdGuardHometest.log")
if errlog and errlog ~= "" then
o = s:option(TextValue, "")
o.readonly = true
o.rows = 5
o.rmempty = true
o.name = ""
o.cfgvalue = function(self, section)
return fs.readfile("/tmp/AdGuardHometest.log")
end
end
end
function m.on_commit(map)
local ucitracktest = uci:get("AdGuardHome", "AdGuardHome", "ucitracktest")
if ucitracktest == "1" then
return
elseif ucitracktest == "0" then
io.popen("/etc/init.d/AdGuardHome reload &")
else
fs.writefile("/var/run/AdGlucitest", "")
end
end
return m
@@ -0,0 +1,16 @@
require("luci.sys")
local m, s
local fs = require "nixio.fs"
local uci = require "luci.model.uci".cursor()
m = Map("AdGuardHome")
m:section(SimpleSection).template = "AdGuardHome/overview"
-- Read-only dashboard — hide form submit / reset buttons
m.submit = false
m.reset = false
m.apply_on_parse = false
return m
@@ -0,0 +1,113 @@
require("luci.sys")
require("luci.util")
local m, s, o, o1
local fs = require "nixio.fs"
local uci = require "luci.model.uci".cursor()
local workdir = uci:get("AdGuardHome", "AdGuardHome", "workdir") or "/etc/AdGuardHome"
m = Map("AdGuardHome", translate("运维"))
m.description = translate("改密、备份、计划任务、下载源")
s = m:section(NamedSection, "AdGuardHome", "AdGuardHome", "")
s.anonymous = true
s.addremove = false
-- Change password ------------------------------------------------
o = s:option(Value, "hashpass", translate("Change browser management password"),
translate("Press load culculate model and culculate finally save/apply"))
o.default = ""
o.datatype = "string"
o.template = "AdGuardHome/AdGuardHome_chpass"
o.rmempty = true
-- Upgrade keep files ---------------------------------------------
o = s:option(MultiValue, "upprotect", translate("Keep files when system upgrade"),
translate("刷固件 (sysupgrade) 时保留这些文件。普通用户按推荐勾选即可:核心 + 配置 + filters + sessions.db + stats.db。日志和 querylog 通常不需要保留(querylog 可能占数十 MB)。"))
o:value("$binpath", translate("核心执行文件 (推荐)"))
o:value("$configpath", translate("配置文件 (必选)"))
o:value("$logfile", translate("日志文件"))
o:value("$workdir/data/sessions.db", translate("登录会话 sessions.db (推荐)"))
o:value("$workdir/data/stats.db", translate("统计 stats.db (推荐)"))
o:value("$workdir/data/querylog.json", translate("查询日志 querylog.json (占空间)"))
o:value("$workdir/data/filters", translate("过滤规则 filters (推荐)"))
o.widget = "checkbox"
o.default = "$binpath $configpath $workdir/data/sessions.db $workdir/data/stats.db $workdir/data/filters"
o.optional = false
-- Backup workdir -------------------------------------------------
o = s:option(MultiValue, "backupfile", translate("Backup workdir files when shutdown"),
translate("关机时把这些文件复制到下方「备份工作目录路径」,下次开机如果 workdir/data 为空会自动恢复。仅当工作目录位于 tmpfs(如 /tmp)时才需要勾选,持久化路径无需备份。"))
o1 = s:option(Value, "backupwdpath", translate("Backup workdir path"))
o:value("filters", "filters")
o:value("stats.db", "stats.db")
o:value("querylog.json", "querylog.json")
o:value("sessions.db", "sessions.db")
o1:depends("backupfile", "filters")
o1:depends("backupfile", "stats.db")
o1:depends("backupfile", "querylog.json")
o1:depends("backupfile", "sessions.db")
if fs.access(workdir .. "/data") then
for name in fs.glob(workdir .. "/data/*") do
local basename = fs.basename(name)
if basename ~= "filters" and basename ~= "stats.db"
and basename ~= "querylog.json" and basename ~= "sessions.db" then
o:value(basename, basename)
o1:depends("backupfile", basename)
end
end
end
o.widget = "checkbox"
o.default = nil
o.optional = false
o.description = translate("Will be restore when workdir/data is empty")
o1.default = "/etc/AdGuardHome"
o1.datatype = "string"
o1.optional = false
o1.validate = function(self, value)
if value and value ~= "" and fs.stat(value, "type") == "reg" then
m.message = (m.message or "") .. "\n" .. translate("Error: backup dir is a file")
return nil
end
if value and value:sub(-1) == "/" then
return value:sub(1, -2)
end
return value
end
-- Crontab tasks --------------------------------------------------
o = s:option(MultiValue, "crontab", translate("Crontab task"),
translate("Please change time and args in crontab"))
o:value("autoupdate", translate("Auto update core"))
o:value("cutquerylog", translate("Auto tail querylog"))
o:value("cutruntimelog", translate("Auto tail runtime log"))
o:value("autohost", translate("Auto update ipv6 hosts and restart adh"))
o.widget = "checkbox"
o.default = nil
o.optional = false
-- Download links -------------------------------------------------
o = s:option(TextValue, "downloadlinks", translate("Download links for update"))
o.optional = false
o.rows = 4
o.wrap = "soft"
o.cfgvalue = function(self, section)
if fs.access("/usr/share/AdGuardHome/links.txt") then
return fs.readfile("/usr/share/AdGuardHome/links.txt")
end
return ""
end
o.write = function(self, section, value)
if value then
fs.writefile("/usr/share/AdGuardHome/links.txt", value:gsub("\r\n", "\n"))
end
end
return m
@@ -0,0 +1,51 @@
<%+AdGuardHome/head%>
<%+cbi/valueheader%>
<script src="<%=resource%>/twin-bcrypt.min.js"></script>
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:6px;">
<input data-update="change"<%=
attr("id", cbid) ..
attr("name", cbid) ..
attr("type", self.password and "password" or "text") ..
attr("class", self.password and "cbi-input-password" or "cbi-input-text") ..
attr("value", self:cfgvalue(section) or self.default) ..
ifattr(self.size, "size") ..
ifattr(self.placeholder, "placeholder") ..
ifattr(self.readonly, "readonly") ..
ifattr(self.maxlength, "maxlength") ..
ifattr(self.datatype, "data-type", self.datatype) ..
ifattr(self.datatype, "data-optional", self.optional or self.rmempty) ..
ifattr(self.combobox_manual, "data-manual", self.combobox_manual) ..
ifattr(#self.keylist > 0, "data-choices", { self.keylist, self.vallist })
%> />
<% if self.password then %><img src="<%=resource%>/cbi/reload.gif" style="vertical-align:middle" title="<%:Reveal/hide password%>" onclick="var e = document.getElementById('<%=cbid%>'); e.type = (e.type=='password') ? 'text' : 'password';" /><% end %>
<input type="button" class="btn cbi-button cbi-button-apply" id="<%=cbid%>.applychpass" value="<%:Load culculate model%>" onclick="return chpass(this)"/>
<input type="button" class="btn cbi-button cbi-button-save" id="<%=cbid%>.copypass" value="<%:Copy%>" style="display:none" onclick="return copypass()"/>
</div>
<script type="text/javascript">//<![CDATA[
var hashpassFieldId = '<%=cbid%>';
function chpass(btn) {
var lv = document.getElementById(hashpassFieldId);
if (!lv || lv.value === '') { btn.value = '<%:is empty%>'; return; }
if (typeof TwinBcrypt === 'undefined') { btn.value = '<%:loading...%>'; return; }
btn.disabled = true;
var hash = TwinBcrypt.hashSync(lv.value);
lv.value = hash;
lv.readOnly = true;
btn.style.display = 'none';
var copyBtn = document.getElementById(hashpassFieldId + '.copypass');
copyBtn.style.display = '';
copyBtn.value = '<%:Copy%>';
}
function copypass() {
var lv = document.getElementById(hashpassFieldId);
if (!lv) return;
lv.select();
try { document.execCommand('copy'); }
catch(e) { navigator.clipboard.writeText(lv.value).catch(function(){}); }
var copyBtn = document.getElementById(hashpassFieldId + '.copypass');
copyBtn.value = '<%:Copied%>';
setTimeout(function(){ copyBtn.value = '<%:Copy%>'; }, 2000);
}
//]]>
</script>
<%+cbi/valuefooter%>
@@ -0,0 +1,145 @@
<style>
/* === AdGuardHome Status Bar === */
.adg-status-bar {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
padding: 4px 0;
}
.adg-status-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 12px;
font-size: 13px;
font-weight: 600;
line-height: 1.5;
}
.adg-status-badge::before {
content: "";
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.adg-status-running {
background: rgba(74,160,101,.15);
color: #2d7d46;
}
.adg-status-running::before {
background: #2d7d46;
box-shadow: 0 0 0 2px rgba(74,160,101,.3);
}
.adg-status-stopped {
background: rgba(220,80,80,.12);
color: #b03030;
}
.adg-status-stopped::before {
background: #b03030;
box-shadow: 0 0 0 2px rgba(220,80,80,.25);
}
.adg-status-redirect-yes {
background: rgba(64,128,196,.12);
color: #2864a0;
}
.adg-status-redirect-yes::before {
background: #2864a0;
box-shadow: 0 0 0 2px rgba(64,128,196,.25);
}
.adg-status-redirect-no {
background: rgba(160,160,160,.10);
color: #888;
}
.adg-status-redirect-no::before {
background: #888;
}
/* === NamedSection card styling === */
.adg-section-card {
background: var(--cbi-background, #fff);
border: 1px solid var(--cbi-border, #e0e0e0);
border-radius: 8px;
margin-bottom: 16px;
overflow: hidden;
}
.adg-section-card .cbi-section-title {
background: var(--cbi-section-heading-bg, #f5f7fa);
padding: 10px 14px;
font-size: 14px;
font-weight: 600;
border-bottom: 1px solid var(--cbi-border, #e0e0e0);
margin: 0;
}
.adg-section-card .cbi-section-title h3 {
margin: 0;
font-size: inherit;
}
/* === TextValue inputs — match text input width === */
.cbi-value-field textarea.cbi-input-textarea {
max-width: 280px;
width: auto;
box-sizing: border-box;
}
/* === Mobile responsive === */
@media screen and (max-width: 600px) {
.adg-status-bar {
gap: 6px;
}
.adg-status-badge {
font-size: 12px;
padding: 2px 8px;
}
.adg-status-badge::before {
width: 6px;
height: 6px;
}
.adg-section-card {
border-radius: 6px;
margin-bottom: 10px;
}
.cbi-value-field .cbi-input-textarea {
max-width: 100%;
box-sizing: border-box;
}
/* fix download links textarea on mobile */
#cbid\\.AdGuardHome\\.AdGuardHome\\.downloadlinks {
max-width: 100% !important;
width: 100% !important;
box-sizing: border-box;
}
}
</style>
<script type="text/javascript">//<![CDATA[
XHR.poll(3, '<%=url([[admin]], [[services]], [[AdGuardHome]], [[status]])%>', null,
function(x, data) {
var tb = document.getElementById('AdGuardHome_status');
if (data && tb) {
var html = '<div class="adg-status-bar">';
if (data.running) {
html += '<span class="adg-status-badge adg-status-running">AdGuardHome <%:RUNNING%></span>';
} else {
html += '<span class="adg-status-badge adg-status-stopped">AdGuardHome <%:NOT RUNNING%></span>';
}
if (data.redirect) {
html += '<span class="adg-status-badge adg-status-redirect-yes"><%:Redirected%></span>';
} else {
html += '<span class="adg-status-badge adg-status-redirect-no"><%:Not redirect%></span>';
}
html += '</div>';
tb.innerHTML = html;
}
}
);
//]]>
</script>
<fieldset class="cbi-section">
<div id="AdGuardHome_status">
<div class="adg-status-bar">
<span class="adg-status-badge adg-status-stopped"><%:Collecting data...%></span>
</div>
</div>
</fieldset>
@@ -0,0 +1,169 @@
<%+AdGuardHome/head%>
<%+cbi/valueheader%>
<%
local value = self:cfgvalue(section)
local checked = value == "1"
local field_id = cbid
%>
<style>
.adg-enable-wrap {
display: inline-flex;
align-items: center;
gap: 10px;
min-height: 30px;
}
.adg-service-switch {
position: relative;
width: 48px;
height: 26px;
border: 0;
border-radius: 999px;
background: rgba(128,128,128,.24);
padding: 2px;
box-shadow: none;
cursor: pointer;
vertical-align: middle;
transition: background .18s ease, opacity .18s ease;
}
.adg-service-switch .adg-service-knob {
position: absolute;
top: 3px;
left: 3px;
width: 20px;
height: 20px;
border-radius: 50%;
background: rgba(255,255,255,.94);
box-shadow: 0 1px 4px rgba(0,0,0,.18);
transition: transform .18s ease, background .18s ease;
}
.adg-service-switch.is-on {
background: rgba(74,160,101,.52);
}
.adg-service-switch.is-on .adg-service-knob {
transform: translateX(22px);
}
.adg-service-switch:disabled {
opacity: .72;
cursor: not-allowed;
}
.adg-enable-status {
font-size: 12px;
color: rgba(128,128,128,.86);
}
</style>
<div class="adg-enable-wrap">
<input type="hidden" data-update="change"<%=attr("id", field_id) .. attr("name", field_id) .. attr("value", checked and "1" or "0")%> />
<button type="button" class="adg-service-switch <%=checked and 'is-on' or 'is-off'%>"<%=attr("id", field_id .. ".switch") .. attr("aria-pressed", checked and "true" or "false")%> aria-label="<%:Enable%>">
<span class="adg-service-knob"></span>
</button>
<span class="adg-enable-status"<%=attr("id", field_id .. ".status")%>></span>
</div>
<script type="text/javascript">//<![CDATA[
(function() {
var hidden = document.getElementById('<%=field_id%>');
var btn = document.getElementById('<%=field_id%>.switch');
var status = document.getElementById('<%=field_id%>.status');
var current = hidden && hidden.value === '1';
var pollSerial = 0;
function setState(on) {
current = !!on;
if (hidden)
hidden.value = current ? '1' : '0';
if (btn) {
btn.className = 'adg-service-switch ' + (current ? 'is-on' : 'is-off');
btn.setAttribute('aria-pressed', current ? 'true' : 'false');
}
}
function notify(message) {
if (window.L && L.ui && L.ui.addNotification) {
var p = document.createElement('p');
p.appendChild(document.createTextNode(message));
L.ui.addNotification(null, p);
} else {
alert(message);
}
}
function updateStatus(data) {
var tb = document.getElementById('AdGuardHome_status');
if (!data || !tb)
return;
if (data.running)
tb.innerHTML = '<em><b><font color=green>AdGuardHome <%:RUNNING%></font></b></em>';
else
tb.innerHTML = '<em><b><font color=red>AdGuardHome <%:NOT RUNNING%></font></b></em>';
if (data.redirect)
tb.innerHTML += '<em><b><font color=green><%:Redirected%></font></b></em>';
else
tb.innerHTML += '<em><b><font color=red><%:Not redirect%></font></b></em>';
}
function pollTarget(target, serial, left) {
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[status]])%>', null, function(_, statusData) {
if (serial !== pollSerial)
return;
updateStatus(statusData);
if (statusData && statusData.running === target) {
if (status)
status.innerHTML = '';
return;
}
if (left <= 0) {
setState(!target);
if (status)
status.innerHTML = '';
notify(target ? '<%:AdGuardHome start failed%>' : '<%:AdGuardHome stop failed%>');
return;
}
window.setTimeout(function() {
pollTarget(target, serial, left - 1);
}, 1000);
});
}
if (!btn || !hidden)
return;
btn.onclick = function(ev) {
if (ev)
ev.preventDefault();
var next = !current;
var previous = current;
var serial = ++pollSerial;
setState(next);
btn.disabled = true;
if (status)
status.innerHTML = next ? '<%:Starting...%>' : '<%:Stopping...%>';
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[toggle]])%>', { enabled: next ? 1 : 0 }, function(x, data) {
btn.disabled = false;
if (data && data.success) {
setState(data.enabled);
pollTarget(next, serial, 12);
return;
}
if (status)
status.innerHTML = '';
setState(previous);
var fallback = '<%:Operation failed%>';
if (x && x.status && x.status !== 200)
fallback += ' (HTTP ' + x.status + ')';
notify(data && data.message ? data.message : fallback);
});
return false;
};
})();
//]]>
</script>
<%+cbi/valuefooter%>
@@ -0,0 +1,83 @@
<script>
(function () {
// Load our CSS (CBI strips <link> tags; inject via JS)
var link = document.createElement('link');
link.rel = 'stylesheet';
link.href = '<%=resource%>/adguardhome/adg.css';
document.head.appendChild(link);
// Add .adg-wrap to the Argon main container (exists at page load)
var wrapEl;
(function addWrap() {
wrapEl = document.querySelector('.main') || document.querySelector('#maincontent') || document.body;
wrapEl.classList.add('adg-wrap');
})();
// Dark-mode detection: the LuCI theme (Argon dark / bootstrap-dark) may
// render a dark page regardless of OS prefers-color-scheme. Sample the
// body background and flip .adg-dark when luminance is low.
function detectDark() {
try {
var bg = getComputedStyle(document.body).backgroundColor || '';
var m = bg.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
if (!m) return;
var lum = 0.2126 * +m[1] + 0.7152 * +m[2] + 0.0722 * +m[3];
wrapEl.classList.toggle('adg-dark', lum < 128);
} catch (e) {}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', detectDark);
} else {
detectDark();
}
// Re-check shortly after — Argon injects dark CSS late on some pages.
setTimeout(detectDark, 300);
setTimeout(detectDark, 1200);
// Collapsible sections — wait for CBI to render (Argon loads it async)
var COLLAPSE_KEYS = ['路径与日志', '高级设置', 'Paths & Logs', 'Advanced Settings'];
var STORE_KEY = 'adg-collapsed';
function getStore() {
try { return JSON.parse(localStorage.getItem(STORE_KEY) || '{}'); }
catch (e) { return {}; }
}
function setStore(s) {
try { localStorage.setItem(STORE_KEY, JSON.stringify(s)); } catch (e) {}
}
function applyCollapse() {
var store = getStore();
var sections = document.querySelectorAll('.adg-wrap .cbi-section');
if (sections.length === 0) return false;
sections.forEach(function (sec) {
var head = sec.querySelector(':scope > h3, :scope > legend');
if (!head) return;
var title = (head.textContent || '').trim();
if (COLLAPSE_KEYS.indexOf(title) === -1) return;
if (sec.classList.contains('adg-collapsible')) return;
sec.classList.add('adg-collapsible');
var collapsed = (title in store) ? store[title] : true;
if (collapsed) sec.classList.add('adg-collapsed');
head.addEventListener('click', function () {
sec.classList.toggle('adg-collapsed');
store[title] = sec.classList.contains('adg-collapsed');
setStore(store);
});
});
return true;
}
// Try immediately, then retry up to 20 times (10 seconds total)
var tries = 0;
function retry() {
if (applyCollapse()) return;
if (++tries < 40) setTimeout(retry, 250);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { setTimeout(retry, 100); });
} else {
setTimeout(retry, 100);
}
})();
</script>
@@ -0,0 +1,186 @@
<%+cbi/valueheader%>
<%+AdGuardHome/head%>
<%+AdGuardHome/log_empty%>
<style>
.adg-log-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
margin-bottom: 6px;
}
.adg-log-controls label {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 13px;
cursor: pointer;
}
.adg-log-controls input[type="checkbox"] {
vertical-align: middle;
height: auto;
margin: 0;
}
.adg-log-pane {
height: 60vh;
min-height: 360px;
overflow: auto;
padding: 8px 10px;
border: 1px solid rgba(0,0,0,.08);
border-radius: 7px;
background: #1a1d21;
color: #cfd6df;
font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;
font-size: 11.5px;
line-height: 1.35 !important;
white-space: pre-wrap;
word-break: break-all;
width: 100%;
}
.adg-log-pane .adg-line { padding: 0 4px; border-radius: 3px; display: block; }
.adg-log-pane .adg-info { color: #cfd6df; }
.adg-log-pane .adg-warn { color: #e8b95a; }
.adg-log-pane .adg-error { color: #ea7878; background: rgba(234,120,120,.06); }
.adg-log-pane .adg-debug { color: #7a8290; opacity: .7; }
body.dark .adg-log-pane { border-color: rgba(255,255,255,.1); }
</style>
<div class="adg-log-controls">
<label><input type="checkbox" value="reverse" onclick="return reverselog()" checked><%:reverse%></input></label>
<%if self.timereplace then%>
<label><input type="checkbox" value="localtime" onclick="return chlogtime()" checked><%:localtime%></input></label>
<%end%>
</div>
<pre id="cbid.logview.1.conf" class="adg-log-pane" style="line-height:1.35 !important"></pre>
<div style="display:flex;gap:6px;margin-top:6px;flex-wrap:wrap;">
<input type="button" class="btn cbi-button cbi-button-apply" id="apply_update_button" value="<%:dellog%>" onclick="return apply_del_log()"/>
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:download log%>" onclick="return download_log()"/>
</div>
<script type="text/javascript">//<![CDATA[
var islogreverse = true;
var isutc2local = <%=tostring(self.timereplace)%>;
function hlLine(ln) {
var cls = '';
var l = ln.toLowerCase();
if (l.indexOf('error') >= 0 || l.indexOf('fatal') >= 0 || l.indexOf('panic') >= 0) cls = 'adg-error';
else if (l.indexOf('warn') >= 0) cls = 'adg-warn';
else if (l.indexOf('debug') >= 0 || l.indexOf('trace') >= 0) cls = 'adg-debug';
else cls = 'adg-info';
return '<span class="adg-line ' + cls + '">' + ln.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</span>';
}
function hlLines(text) {
return text.split('\n').map(hlLine).join('');
}
function createAndDownloadFile(fileName, content) {
var aTag = document.createElement('a');
var blob = new Blob([content]);
aTag.download = fileName;
aTag.href = URL.createObjectURL(blob);
aTag.click();
URL.revokeObjectURL(blob);
}
function download_log(){
var lv = document.getElementById('cbid.logview.1.conf');
var dt = new Date();
var timestamp = (dt.getMonth()+1)+"-"+dt.getDate()+"-"+dt.getHours()+"_"+dt.getMinutes();
createAndDownloadFile("AdGuardHome"+timestamp+".log",lv.textContent)
return
}
function apply_del_log(){
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[dodellog]])%>',null,function(x, data){
var lv = document.getElementById('cbid.logview.1.conf');
lv.innerHTML="";
}
);
return
}
function chlogtime(){
var lv = document.getElementById('cbid.logview.1.conf');
var text = lv.textContent;
if (isutc2local){
lv.innerHTML = hlLines(line_toUTC(text).join('\n'));
isutc2local=false;
}else{
lv.innerHTML = hlLines(line_tolocal(text).join('\n'));
isutc2local=true;
}
return
}
function reverselog(){
var lv = document.getElementById('cbid.logview.1.conf');
lv.innerHTML = lv.textContent.split('\n').reverse().map(hlLine).join('');
if (islogreverse){
islogreverse=false;
}else{
islogreverse=true;
}
return
}
function p(s) {
return s < 10 ? '0' + s: s;
}
function line_tolocal(str){
var strt=new Array();
str.trim().split('\n').forEach(function(v, i) {
var dt = new Date(v.substring(0,19)+" UTC");
if (dt != "Invalid Date"){
strt[i]=dt.getFullYear()+"/"+p(dt.getMonth()+1)+"/"+p(dt.getDate())+" "+p(dt.getHours())+":"+p(dt.getMinutes())+":"+p(dt.getSeconds())+v.substring(19);
}else{
strt[i]=v;}})
return strt
}
function line_toUTC(str){
var strt=new Array();
str.trim().split('\n').forEach(function(v, i) {
var dt = new Date(v.substring(0,19))
if (dt != "Invalid Date"){
strt[i]=dt.getUTCFullYear()+"/"+p(dt.getUTCMonth()+1)+"/"+p(dt.getUTCDate())+" "+p(dt.getUTCHours())+":"+p(dt.getUTCMinutes())+":"+p(dt.getUTCSeconds())+v.substring(19);
}else{
strt[i]=v;}})
return strt
}
function displayLog(text) {
var lv = document.getElementById('cbid.logview.1.conf');
if (!lv) return;
if (isutc2local) {
var lines = line_toUTC(text);
if (islogreverse) lines = lines.reverse();
text = lines.join('\n');
text = line_tolocal(text).join('\n');
} else if (islogreverse) {
text = text.split('\n').reverse().join('\n');
}
lv.innerHTML = hlLines(text);
}
function poll_check(){
XHR.poll(3, '<%=url([[admin]], [[services]], [[AdGuardHome]], [[getlog]])%>', null,
function(x, data) {
var lv = document.getElementById('cbid.logview.1.conf');
if (x.responseText && lv) {
var text = x.responseText;
if (isutc2local) {
var lines = line_toUTC(text);
if (islogreverse) lines = lines.reverse();
text = lines.join('\n');
text = line_tolocal(text).join('\n');
} else if (islogreverse) {
text = text.split('\n').reverse().join('\n');
}
if (islogreverse) {
lv.innerHTML = hlLines(text) + lv.innerHTML;
} else {
lv.innerHTML += hlLines(text);
}
}
}
);}
<%if self.pollcheck then%>
poll_check();
<%else%>
var lv = document.getElementById('cbid.logview.1.conf');
lv.innerHTML="<%:Please add log path in config to enable log%>"
<%end%>
//]]>
</script>
<%+cbi/valuefooter%>
@@ -0,0 +1,27 @@
<%
local fs = require "nixio.fs"
local uci = require "luci.model.uci".cursor()
local logfile = uci:get("AdGuardHome", "AdGuardHome", "logfile") or ""
local no_logfile = (logfile == "")
%>
<% if no_logfile then %>
<div class="adg-empty-state warn">
<div class="adg-empty-icon">!</div>
<div class="adg-empty-body">
<div class="adg-empty-title"><%:运行日志未配置%></div>
<div class="adg-empty-desc"><%:请在「基础设置 → 路径与日志 → 运行日志」填入日志路径(如 /tmp/AdGuardHome.log),或填 syslog 写入系统日志。%></div>
<a class="btn cbi-button cbi-button-apply" href="<%=luci.dispatcher.build_url('admin','services','AdGuardHome','base')%>"><%:去设置%></a>
</div>
</div>
<style>
.adg-empty-state { display:flex; align-items:flex-start; gap:14px; padding:14px 18px; margin:6px 0 14px 0; border-radius:10px; background:rgba(128,128,128,0.05); border:1px solid rgba(128,128,128,0.18); }
.adg-empty-state.warn { border-color:rgba(224,160,47,0.4); background:rgba(224,160,47,0.06); }
.adg-empty-icon { font-size:22px; line-height:1; flex:0 0 auto; padding-top:2px; color:#e0a02f; }
.adg-empty-body { flex:1; }
.adg-empty-title { font-weight:600; font-size:14px; margin-bottom:4px; }
.adg-empty-desc { font-size:13px; opacity:.8; margin-bottom:10px; line-height:1.55; }
.adg-empty-state a.btn { display:inline-block; }
</style>
<% end %>
@@ -0,0 +1,286 @@
<%+AdGuardHome/head%>
<%
local fs = require "nixio.fs"
local uci = require "luci.model.uci".cursor()
local sys = require "luci.sys"
local dsp = require "luci.dispatcher"
local binpath = uci:get("AdGuardHome", "AdGuardHome", "binpath") or "/usr/bin/AdGuardHome"
local configpath = uci:get("AdGuardHome", "AdGuardHome", "configpath") or "/etc/AdGuardHome.yaml"
local httpport = uci:get("AdGuardHome", "AdGuardHome", "httpport") or "3000"
local redirect = uci:get("AdGuardHome", "AdGuardHome", "redirect") or "none"
local version = uci:get("AdGuardHome", "AdGuardHome", "version") or "-"
local has_bin = fs.access(binpath)
local has_config = fs.access(configpath)
local toggle_url = dsp.build_url("admin", "services", "AdGuardHome", "toggle")
local base_url = dsp.build_url("admin", "services", "AdGuardHome", "base")
local tools_url = dsp.build_url("admin", "services", "AdGuardHome", "tools")
local log_url = dsp.build_url("admin", "services", "AdGuardHome", "log")
local redirect_label = {
none = translate("未启用"),
["dnsmasq-upstream"] = translate("作为 dnsmasq 上游"),
redirect = translate("53 端口劫持"),
exchange = translate("替代 dnsmasq")
}
local redirect_text = redirect_label[redirect] or redirect
%>
<style>
/* hide LuCI form footer (Save/Apply/Reset) on this read-only dashboard */
body:has(.adg-ov-root) .cbi-page-actions,
body:has(.adg-ov-root) form .cbi-section + .cbi-page-actions { display: none !important; }
.adg-ov-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 14px;
margin-bottom: 16px;
}
.adg-ov-card {
background: var(--adg-card-bg, rgba(128,128,128,0.04));
border: 1px solid var(--adg-card-border, rgba(128,128,128,0.16));
border-radius: 12px;
padding: 16px 18px;
display: flex;
flex-direction: column;
gap: 10px;
}
.adg-ov-card .head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.adg-ov-card .title {
font-size: 13px;
color: var(--adg-label-muted, #666);
font-weight: 500;
}
.adg-ov-card .value {
font-size: 18px;
font-weight: 600;
letter-spacing: .2px;
}
.adg-ov-card .sub {
font-size: 12.5px;
color: var(--adg-label-muted, #888);
}
/* status dot */
.adg-ov-dot {
display: inline-block;
width: 10px; height: 10px;
border-radius: 50%;
background: #b03030;
margin-right: 6px;
vertical-align: middle;
box-shadow: 0 0 0 3px rgba(176,48,48,.18);
}
.adg-ov-dot.ok { background: #2d7d46; box-shadow: 0 0 0 3px rgba(45,125,70,.18); }
.adg-ov-dot.warn { background: #e0a02f; box-shadow: 0 0 0 3px rgba(224,160,47,.18); }
/* big enable switch (iOS-ish) */
.adg-ov-switch {
position: relative;
display: inline-block;
width: 52px; height: 30px;
flex: 0 0 auto;
}
.adg-ov-switch input { display: none; }
.adg-ov-switch .track {
position: absolute;
inset: 0;
border-radius: 999px;
background: rgba(128,128,128,0.25);
transition: background .2s;
cursor: pointer;
}
.adg-ov-switch .knob {
position: absolute;
top: 3px; left: 3px;
width: 24px; height: 24px;
border-radius: 50%;
background: #fff;
box-shadow: 0 2px 4px rgba(0,0,0,.2);
transition: transform .2s;
}
.adg-ov-switch input:checked + .track { background: #2d7d46; }
.adg-ov-switch input:checked + .track .knob { transform: translateX(22px); }
.adg-ov-switch.busy .track { opacity: .6; cursor: wait; }
/* quick actions */
.adg-ov-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
margin: 10px 0 18px 0;
}
.adg-ov-actions a, .adg-ov-actions button {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border-radius: 8px;
font-size: 13px;
border: 1px solid var(--adg-card-border, rgba(128,128,128,0.18));
background: var(--adg-card-bg, rgba(128,128,128,0.04));
color: inherit;
text-decoration: none;
cursor: pointer;
transition: background .15s;
}
.adg-ov-actions a:hover, .adg-ov-actions button:hover {
background: rgba(0,122,255,0.08);
border-color: rgba(0,122,255,0.4);
}
.adg-ov-actions a.primary {
background: #007aff;
border-color: #007aff;
color: #fff;
}
.adg-ov-actions a.primary:hover { background: #0064d2; }
@media (max-width: 720px) {
.adg-ov-card .value { font-size: 16px; }
.adg-ov-actions a, .adg-ov-actions button { flex: 1 1 calc(50% - 5px); justify-content: center; }
}
</style>
<div class="cbi-section adg-ov-root">
<!-- Card grid -->
<div class="adg-ov-grid">
<!-- Status + Enable -->
<div class="adg-ov-card">
<div class="head">
<div>
<div class="title"><%:服务状态%></div>
<div class="value" id="adg-ov-status">
<span class="adg-ov-dot"></span><%:正在检测…%>
</div>
</div>
<label class="adg-ov-switch" id="adg-ov-switch">
<input type="checkbox" id="adg-ov-toggle">
<span class="track"><span class="knob"></span></span>
</label>
</div>
<div class="sub" id="adg-ov-status-sub">&nbsp;</div>
</div>
<!-- Redirect mode -->
<div class="adg-ov-card">
<div class="title"><%:重定向模式%></div>
<div class="value" id="adg-ov-redir">
<%= redirect_text %>
</div>
<div class="sub" id="adg-ov-redir-sub">
<%:配置「基础设置 → 5553 重定向」%>
</div>
</div>
<!-- Core version -->
<div class="adg-ov-card">
<div class="title"><%:核心版本%></div>
<div class="value">
<% if has_bin then %>
<%= version %>
<% else %>
<span style="color:#b03030;"><%:未安装%></span>
<% end %>
</div>
<div class="sub">
<% if not has_config then %>
<span style="color:#e0a02f;"><%:配置文件缺失%></span>
<% elseif not has_bin then %>
<%:点击「基础设置 → 更新核心版本」下载%>
<% else %>
<%= binpath %>
<% end %>
</div>
</div>
</div>
<!-- Quick actions -->
<div class="adg-ov-actions">
<a class="primary" id="adg-ov-webui" target="_blank" rel="noopener"
href="http://<%=luci.http.getenv('SERVER_NAME') or ''%>:<%=httpport%>/">
<%:打开 Web 控制台%> :<%=httpport%>
</a>
<a href="<%=base_url%>"><%:基础设置%></a>
<a href="<%=tools_url%>"><%:运维%></a>
<a href="<%=log_url%>"><%:查看日志%></a>
</div>
</div>
<script>
(function () {
var STATUS_URL = '<%=dsp.build_url("admin","services","AdGuardHome","status")%>';
var TOGGLE_URL = '<%=toggle_url%>';
var elStatus = document.getElementById('adg-ov-status');
var elStatusSub = document.getElementById('adg-ov-status-sub');
var elToggle = document.getElementById('adg-ov-toggle');
var elSwitch = document.getElementById('adg-ov-switch');
var elRedir = document.getElementById('adg-ov-redir');
var elRedirSub = document.getElementById('adg-ov-redir-sub');
var elWebUI = document.getElementById('adg-ov-webui');
// patch the webui link to use current hostname
if (elWebUI) {
var host = window.location.hostname;
elWebUI.href = 'http://' + host + ':<%=httpport%>/';
}
function paint(data) {
if (!data) return;
// status
if (data.running) {
elStatus.innerHTML = '<span class="adg-ov-dot ok"></span><%:运行中%>';
elStatusSub.textContent = '<%:AdGuardHome 正在处理 DNS 请求%>';
elToggle.checked = true;
} else {
elStatus.innerHTML = '<span class="adg-ov-dot"></span><%:未运行%>';
elStatusSub.textContent = data.message || '<%:服务未启动%>';
elToggle.checked = false;
}
// redirect
if (data.redirect) {
elRedir.innerHTML = '<span class="adg-ov-dot ok"></span><%= redirect_text %>';
} else {
elRedir.innerHTML = '<span class="adg-ov-dot warn"></span><%:未重定向%>';
}
}
function poll() {
XHR.get(STATUS_URL, null, function (x, data) { paint(data); });
}
elToggle.addEventListener('change', function (ev) {
var want = elToggle.checked ? '1' : '0';
elSwitch.classList.add('busy');
elToggle.disabled = true;
XHR.post(TOGGLE_URL, { enabled: want, token: L && L.env ? L.env.token : '' }, function (x, data) {
elSwitch.classList.remove('busy');
elToggle.disabled = false;
if (data) paint(data);
poll();
});
});
// hide bottom form action bar (Save/Apply/Reset) — fallback for browsers without :has
(function hideFooter() {
var bar = document.querySelector('.cbi-page-actions');
if (bar) bar.style.display = 'none';
})();
// initial fetch + 3s poll
poll();
if (typeof XHR.poll === 'function') {
XHR.poll(3, STATUS_URL, null, function (x, data) { paint(data); });
} else {
setInterval(poll, 3000);
}
})();
</script>
@@ -0,0 +1,84 @@
<%+AdGuardHome/head%>
<%+cbi/valueheader%>
<script src="/luci-static/resources/codemirror/lib/codemirror.js"></script>
<link rel="stylesheet" href="/luci-static/resources/codemirror/lib/codemirror.css"/>
<style>
.CodeMirror { line-height: 1.3; font-size: 11.5px; }
.CodeMirror pre.CodeMirror-line,
.CodeMirror pre.CodeMirror-line-like { line-height: 1.3; }
</style>
<script src="/luci-static/resources/codemirror/mode/yaml/yaml.js"></script>
<link rel="stylesheet" href="/luci-static/resources/codemirror/theme/dracula.css"/>
<link rel="stylesheet" href="/luci-static/resources/codemirror/addon/fold/foldgutter.css"/>
<script src="/luci-static/resources/codemirror/addon/fold/foldcode.js"></script>
<script src="/luci-static/resources/codemirror/addon/fold/foldgutter.js"></script>
<script src="/luci-static/resources/codemirror/addon/fold/indent-fold.js"></script>
<script type="text/javascript">//<![CDATA[
var editor = null;
try {
if (typeof CodeMirror !== 'undefined') {
editor = CodeMirror.fromTextArea(document.getElementById("cbid.AdGuardHome.AdGuardHome.escconf"), {
mode: "text/yaml",
styleActiveLine: true,
lineNumbers: true,
theme: "dracula",
lineWrapping: true,
foldGutter: true,
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
matchBrackets: true
});
editor.setSize("100%", null);
}
} catch (e) {
console.warn('CodeMirror init skipped:', e);
}
function reload_config(){
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[reloadconfig]])%>', null,
function(x, data) {
location.reload();
});}
function use_template(){
if (!confirm("<%:即将用模板覆盖当前 YAML,未保存的修改将丢失,是否继续?%>")) return;
XHR.get('<%=url([[admin]], [[services]], [[AdGuardHome]], [[gettemplateconfig]])%>', null,
function(x, data) {
if (editor) editor.setValue(x.responseText);
else document.getElementById("cbid.AdGuardHome.AdGuardHome.escconf").value = x.responseText;
});}
// Confirm before "保存并应用" / "保存" on the manual page.
// Buttons are rendered after this script by LuCI form footer, so we
// poll for them and wrap their inline onclick handlers.
(function () {
var msg = "<%:即将覆盖 AdGuardHome.yaml,请确认已备份配置。继续保存?%>";
function wrap() {
var targets = document.querySelectorAll('.cbi-button-save, .cbi-button-apply');
var wrapped = 0;
targets.forEach(function (btn) {
if (btn.__adgWrapped) return;
btn.__adgWrapped = true;
wrapped++;
var original = btn.onclick;
btn.onclick = function (ev) {
if (!confirm(msg)) {
if (ev) ev.preventDefault();
return false;
}
if (typeof original === 'function') return original.call(btn, ev);
};
});
return wrapped;
}
var attempts = 0;
var iv = setInterval(function () {
attempts++;
if (wrap() > 0 || attempts > 40) clearInterval(iv);
}, 100);
})();
//]]>
</script>
<%fs=require"nixio.fs"%>
<%if fs.access("/tmp/AdGuardHometmpconfig.yaml") then%>
<input type="button" id="apply_update_button" value="<%:Reload Config%>" onclick=" return reload_config() "/>
<%end%>
<input type="button" id="template_button" value="<%:Use template%>" onclick=" return use_template() "/>
<%+cbi/valuefooter%>
@@ -1,231 +0,0 @@
#/cgi-bin/luci/admin/services/AdGuardHome
msgid "Base Setting"
msgstr "基础设置"
msgid "Log"
msgstr "日志"
msgid "Basic Settings"
msgstr "基础设置"
msgid "Manual Config"
msgstr "手动设置"
msgid "Enable"
msgstr "启用"
msgid "Browser management port"
msgstr "网页管理端口"
msgid "DNS handling mode"
msgstr "DNS 接管方式"
msgid "Do not handle DNS"
msgstr "不接管 DNS"
msgid "Run as dnsmasq upstream server"
msgstr "作为 dnsmasq 的上游服务器"
msgid "Forward port 53 requests through the firewall"
msgstr "通过防火墙转发 53 端口请求"
msgid "Use port 53 instead of dnsmasq"
msgstr "使用 53 端口替代 dnsmasq"
msgid "Network devices excluded from forwarding"
msgstr "不参与转发的网络设备"
msgid "DNS requests arriving on these network devices are not forwarded to AdGuardHome"
msgstr "从这些网络设备进入的 DNS 请求不转发到 AdGuardHome"
msgid "Bin Path"
msgstr "执行文件路径"
msgid "Config Path"
msgstr "配置文件路径"
msgid "Work dir"
msgstr "工作目录"
msgid "Runtime log file"
msgstr "运行日志文件"
msgid "If set to \"syslog\", write to system log; if empty, disable file logging"
msgstr "设为 \"syslog\" 时写入系统日志;留空则禁用文件日志"
msgid "Verbose log"
msgstr "详细日志"
msgid "Upgrade Core"
msgstr "更新核心"
msgid "Update core version"
msgstr "更新核心版本"
msgid "Force update"
msgstr "强制更新"
msgid "Check..."
msgstr "检查中..."
msgid "Updated"
msgstr "已更新"
msgid "Update failed"
msgstr "更新失败"
msgid "Update in progress"
msgstr "更新正在进行中"
msgid "Unable to start update"
msgstr "无法启动更新"
msgid "Update status unavailable"
msgstr "更新状态不可用"
msgid "AdGuardHome Web"
msgstr "AdGuardHome 仪表盘"
msgid "Change browser management password"
msgstr "修改网页登录密码"
msgid "New password"
msgstr "新密码"
msgid "Change password"
msgstr "修改密码"
msgid "is empty"
msgstr "为空"
msgid "Changing password..."
msgstr "正在修改密码..."
msgid "Password changed"
msgstr "密码已修改"
msgid "Password change failed"
msgstr "密码修改失败"
msgid "Core"
msgstr "核心"
msgid "core error"
msgstr "核心错误"
msgid "no core"
msgstr "没有核心"
msgid "no config"
msgstr "没有配置文件"
msgid "RUNNING"
msgstr "运行中"
msgid "NOT RUNNING"
msgstr "未运行"
msgid "Redirect"
msgstr "重定向"
msgid "Redirected"
msgstr "已重定向"
msgid "Not redirect"
msgstr "未重定向"
msgid "Collecting data..."
msgstr "获取数据中..."
msgid "Status unavailable"
msgstr "状态不可用"
msgid "reverse"
msgstr "逆序"
msgid "localtime"
msgstr "本地时间"
msgid "Please add log path in config to enable log"
msgstr "请在设置中填写日志路径以启用日志"
msgid "dellog"
msgstr "删除日志"
msgid "download log"
msgstr "下载日志"
msgid "Use template"
msgstr "使用模板"
msgid "Reload Config"
msgstr "重新载入配置"
msgid "Save & Apply"
msgstr "保存并应用"
msgid "Unable to read config"
msgstr "无法读取配置"
msgid "Unable to load editor"
msgstr "无法加载编辑器"
msgid "Saving..."
msgstr "正在保存..."
msgid "Saved"
msgstr "已保存"
msgid "Save failed"
msgstr "保存失败"
msgid "Core is missing; configuration cannot be validated before saving."
msgstr "核心文件不存在,保存前无法校验配置。"
msgid "Saved without validation (core missing)."
msgstr "已保存,但因核心文件不存在而未校验。"
msgid "Configuration validation failed."
msgstr "配置校验失败。"
msgid "Service reload failed; previous config restored."
msgstr "服务重载失败,已恢复原配置。"
msgid "Restore failed; backup at"
msgstr "恢复失败,备份文件位于"
msgid "Config changed on disk; reload before saving."
msgstr "磁盘上的配置已变化,请重新读取后再保存。"
msgid "Unable to read runtime log"
msgstr "无法读取运行日志"
msgid "Switching"
msgstr "正在切换"
msgid "Checking service…"
msgstr "正在确认服务状态…"
msgid "Switch not completed; check the log"
msgstr "切换尚未完成,请检查日志"
msgid "Disabled"
msgstr "已停用"
msgid "Unable to read switch status"
msgstr "无法读取切换状态"
msgid "Forward port 53 requests through the firewall (recommended)"
msgstr "通过防火墙转发 53 端口请求(推荐)"
msgid "Log settings"
msgstr "日志设置"
msgid "Applying verbose logging…"
msgstr "正在应用详细日志设置…"
msgid "Saved, but service reload failed; check the log"
msgstr "已保存,但服务重载失败,请检查日志"
msgid "Unable to apply verbose logging"
msgstr "无法应用详细日志设置"
@@ -0,0 +1,226 @@
bind_host: 0.0.0.0
bind_port: 3000
beta_bind_port: 0
users:
- name: admin
password: $2y$10$vHRcARdPCieYG3RXWomV5evDYN.Nj/edtwEkQgQJZcK6z7qTLaIc6
auth_attempts: 5
block_auth_min: 15
http_proxy: ""
language: zh-cn
rlimit_nofile: 0
debug_pprof: false
web_session_ttl: 720
dns:
bind_hosts:
- 0.0.0.0
port: 5553
statistics_interval: 1
querylog_enabled: true
querylog_file_enabled: true
querylog_interval: 1
querylog_size_memory: 1000
anonymize_client_ip: false
protection_enabled: true
blocking_mode: default
blocking_ipv4: ""
blocking_ipv6: ""
blocked_response_ttl: 10
parental_block_host: family-block.dns.adguard.com
safebrowsing_block_host: standard-block.dns.adguard.com
ratelimit: 0
ratelimit_whitelist: []
refuse_any: false
upstream_dns:
- 223.5.5.5
upstream_dns_file: ""
bootstrap_dns:
- 119.29.29.29
- 223.5.5.5
all_servers: false
fastest_addr: false
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_size: 4194304
cache_ttl_min: 0
cache_ttl_max: 0
cache_optimistic: true
bogus_nxdomain: []
aaaa_disabled: false
enable_dnssec: false
edns_client_subnet: false
max_goroutines: 300
ipset: []
filtering_enabled: true
filters_update_interval: 24
parental_enabled: false
safesearch_enabled: false
safebrowsing_enabled: false
safebrowsing_cache_size: 1048576
safesearch_cache_size: 1048576
parental_cache_size: 1048576
cache_time: 30
rewrites: []
blocked_services: []
upstream_timeout: 10s
local_domain_name: lan
resolve_clients: true
use_private_ptr_resolvers: true
local_ptr_upstreams: []
tls:
enabled: false
server_name: ""
force_https: false
port_https: 443
port_dns_over_tls: 853
port_dns_over_quic: 784
port_dnscrypt: 0
dnscrypt_config_file: ""
allow_unencrypted_doh: false
strict_sni_check: false
certificate_chain: ""
private_key: ""
certificate_path: ""
private_key_path: ""
filters:
- enabled: true
url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt
name: AdGuard DNS filter
id: 1228750870
- enabled: true
url: http://sub.adtchrome.com/adt-chinalist-easylist.txt
name: 广告终结者使用的拦截规则,基于ChinaList+EasyList修正维护
id: 139789181
- enabled: true
url: https://easylist-downloads.adblockplus.org/easylist.txt
name: EasyList-去除国际网页中大多数广告,包括不需要的框架、图像和对象
id: 139789112
- enabled: true
url: https://easylist-downloads.adblockplus.org/easylistchina.txt
name: EasyList China-EasyList针对国内的补充规则
id: 139789121
- enabled: true
url: https://anti-ad.net/adguard.txt
name: anti-AD命中率最高列表
id: 139789122
- enabled: true
url: https://raw.iqiq.io/banbendalao/ADgk/master/ADgk.txt
name: ADgk去广告easylistchina补充规则
id: 139789131
- enabled: true
url: https://raw.iqiq.io/banbendalao/ADgk/master/kill-baidu-ad.txt
name: ADgk百度广告过滤
id: 139789133
- enabled: false
url: https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext
name: Peter Lowe’s Ad and tracking server list​
id: 139789152
- enabled: false
url: https://easylist.to/easylist/fanboy-social.txt
name: Fanboy's Social Blocking List
id: 139789153
- enabled: false
url: https://easylist.to/easylist/fanboy-annoyance.txt
name: Fanboy's Annoyance List
id: 139789154
- enabled: false
url: https://secure.fanboy.co.nz/fanboy-cookiemonster.txt
name: EasyList Cookie List
id: 139789155
- enabled: false
url: https://fanboy.co.nz/fanboy-antifacebook.txt
name: Anti-Facebook List
id: 139789161
- enabled: false
url: https://filters.adtidy.org/extension/ublock/filters/14.txt
name: AdGuard Annoyances filter
id: 139789162
- enabled: false
url: https://www.fanboy.co.nz/enhancedstats.txt
name: Fanboy's Enhanced Tracking List
id: 139789164
- enabled: false
url: https://filters.adtidy.org/extension/ublock/filters/3.txt
name: AdGuard Tracking Protection filter
id: 139789165
- enabled: false
url: https://easylist.to/easylist/easylist.txt
name: EasyList
id: 139789171
- enabled: false
url: https://filters.adtidy.org/extension/ublock/filters/11.txt
name: AdGuard Mobile Ads filter
id: 139789172
- enabled: false
url: https://filters.adtidy.org/extension/ublock/filters/2_without_easylist.txt
name: AdGuard Base filter
id: 139789173
- enabled: false
url: https://easylist-downloads.adblockplus.org/antiadblockfilters.txt
name: Adblock Warning Removal List
id: 139789174
- enabled: false
url: https://fanboy.co.nz/fanboy-problematic-sites.txt
name: Fanboy's problematic-sites
id: 139789175
- enabled: false
url: https://adaway.org/hosts.txt
name: Adaway HOST
id: 139789187
whitelist_filters: []
user_rules:
- '@@||taobao.com^$important'
- '@@||tmall.com^$important'
- '@@||jd.com^important'
- '@@||flyme.cn^$important'
- '@@||meizu.com^$important'
- '@@||wl.jd.com^$important'
- '@@||flydigi.com^'
- '@@||pv.sohu.com^$important'
- /googleads.$~script,domain=~googleads.github.io
- /pagead/lvz?
- '||google.com/pagead/'
- '||static.doubleclick.net^$domain=youtube.com'
- '||youtube.com/get_midroll_'
- '||5hz.org^'
- '@@||000714.xyz^'
- '@@||blueskyxn.com^'
- '||topnewsfeeds.net^'
- '||nbryb.com^'
- '||superbdolly.com^'
- '||salutationcheerlessdemote.com^'
- '||buildingdoodlesquare.com^'
- '||colonistnobilityheroic.com^'
dhcp:
enabled: false
interface_name: ""
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
clients: []
log_compress: false
log_localtime: false
log_max_backups: 0
log_max_size: 100
log_max_age: 3
log_file: ""
verbose: false
schema_version: 10
@@ -1,11 +1,17 @@
config AdGuardHome 'AdGuardHome'
option enabled '0'
option httpport '3000'
option redirect 'none'
option configpath '/etc/AdGuardHome.yaml'
option workdir '/tmp/AdGuardHome'
option workdir '/etc/AdGuardHome'
option logfile '/tmp/AdGuardHome.log'
option verbose '0'
option binpath '/usr/bin/AdGuardHome/AdGuardHome'
list wan_ifname 'eth0'
option binpath '/usr/bin/AdGuardHome'
option upxflag ''
option user ''
option group ''
option advanced_settings '0'
option gc '0'
option maxprocs '0'
option memlimit '0'
File diff suppressed because it is too large. Load diff
+6 -5
View File
@@ -1,14 +1,15 @@
#!/bin/sh
chmod +x /etc/init.d/AdGuardHome
uci -q batch <<-EOF >/dev/null 2>&1
delete ucitrack.@AdGuardHome[-1]
add ucitrack AdGuardHome
set ucitrack.@AdGuardHome[-1].init=AdGuardHome
commit ucitrack
delete AdGuardHome.AdGuardHome.ucitracktest
/etc/init.d/AdGuardHome restart
EOF
# Remove the former menu and both Lua and ucode menu caches.
rm -f /usr/share/luci/menu.d/luci-app-adguardhome-js.json
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.*
rm -f /tmp/luci-modulecache/*
rm -f /tmp/luci-indexcache
chmod +x /etc/init.d/AdGuardHome /usr/share/AdGuardHome/*
exit 0
@@ -1 +0,0 @@
/etc/AdGuardHome.yaml
+68 -126
View File
@@ -1,123 +1,108 @@
# 模板用户名为 admin 密码为 admin
# The username is admin and the password is admin.
# 保存并应用就可以直接用的配置模板文件
# a template config can be use with a apply
http:
pprof:
port: 6060
enabled: false
address: 0.0.0.0:3000
session_ttl: 720h
bind_host: 0.0.0.0
bind_port: 3000
beta_bind_port: 0
users:
- name: admin
password: $2y$10$iGkTW98QtGEZn4I/GC0J4eQhfuLQ53v7L4DDVauEJyWylJrXWwWeO
- name: root
password: $2y$10$dwn0hTYoECQMZETBErGlzOId2VANOVsPHsuH13TM/8KnysM5Dh/ve
auth_attempts: 5
block_auth_min: 15
http_proxy: ""
language: ""
theme: auto
language: zh-cn
debug_pprof: false
web_session_ttl: 720
dns:
bind_hosts:
- 0.0.0.0
port: 3053
- 0.0.0.0
port: 1745
statistics_interval: 30
querylog_enabled: true
querylog_file_enabled: true
querylog_interval: 6h
querylog_size_memory: 1000
anonymize_client_ip: false
ratelimit: 20
ratelimit_subnet_len_ipv4: 24
ratelimit_subnet_len_ipv6: 56
protection_enabled: true
blocking_mode: default
blocking_ipv4: ""
blocking_ipv6: ""
blocked_response_ttl: 60
parental_block_host: family-block.dns.adguard.com
safebrowsing_block_host: standard-block.dns.adguard.com
ratelimit: 0
ratelimit_whitelist: []
refuse_any: true
refuse_any: false
upstream_dns:
- https://dns.cloudflare.com/dns-query
- '#'
- '# 对 ts.net 相关域名启用 DNS 分流, 交由 Tailscale 解析, 如不使用 Tailscale 可以删除.'
- '# Enable DNS conditional forwarding for ts.net via Tailscale; remove if Tailscale is not used.'
- '[/ts.net/]100.100.100.100'
- 223.5.5.5
upstream_dns_file: ""
bootstrap_dns:
- 1.1.1.1
- 1.0.0.1
fallback_dns:
- quic://dns.adguard-dns.com
upstream_mode: parallel
- 119.29.29.29
- 223.5.5.5
all_servers: false
fastest_addr: false
fastest_timeout: 1s
allowed_clients: []
disallowed_clients: []
blocked_hosts:
- version.bind
- id.server
- hostname.bind
- version.bind
- id.server
- hostname.bind
trusted_proxies:
- 127.0.0.0/8
- ::1/128
cache_enabled: true
- 127.0.0.0/8
- ::1/128
cache_size: 4194304
cache_ttl_min: 0
cache_ttl_max: 0
cache_optimistic: false
cache_optimistic_answer_ttl: 30s
cache_optimistic_max_age: 12h
cache_optimistic: true
bogus_nxdomain: []
aaaa_disabled: false
enable_dnssec: false
edns_client_subnet:
custom_ip: ""
enabled: false
use_custom: false
edns_client_subnet: false
max_goroutines: 300
handle_ddr: true
ipset: []
ipset_file: ""
bootstrap_prefer_ipv6: false
filtering_enabled: true
filters_update_interval: 24
parental_enabled: false
safesearch_enabled: false
safebrowsing_enabled: false
safebrowsing_cache_size: 1048576
safesearch_cache_size: 1048576
parental_cache_size: 1048576
cache_time: 30
rewrites: []
blocked_services: []
upstream_timeout: 10s
private_networks: []
local_domain_name: lan
resolve_clients: true
use_private_ptr_resolvers: true
local_ptr_upstreams: []
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_dns_over_quic: 784
port_dnscrypt: 0
dnscrypt_config_file: ""
allow_unencrypted_doh: false
strict_sni_check: false
certificate_chain: ""
private_key: ""
certificate_path: ""
private_key_path: ""
strict_sni_check: false
querylog:
dir_path: ""
ignored: []
interval: 2160h
size_memory: 1000
enabled: true
file_enabled: true
statistics:
dir_path: ""
ignored: []
interval: 24h
enabled: true
filters:
- enabled: true
url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
name: AdGuard DNS filter
id: 1
- enabled: true
url: https://adguardteam.github.io/AdGuardSDNSFilter/Filters/filter.txt
name: AdGuard DNS filter
id: 1628750870
- enabled: true
url: https://anti-ad.net/easylist.txt
name: 'CHN: anti-AD'
id: 1628750871
whitelist_filters: []
user_rules: []
dhcp:
enabled: false
interface_name: ""
local_domain_name: lan
dhcpv4:
gateway_ip: ""
subnet_mask: ""
@@ -131,59 +116,16 @@ dhcp:
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:
- /tmp/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
clients: []
log_compress: false
log_localtime: false
log_max_backups: 0
log_max_size: 100
log_max_age: 3
log_file: ""
verbose: false
os:
group: ""
user: ""
rlimit_nofile: 0
schema_version: 32
schema_version: 12
+54
View File
@@ -0,0 +1,54 @@
#!/bin/sh
checkmd5(){
local nowmd5
nowmd5="$(md5sum /etc/hosts 2>/dev/null)"
nowmd5="${nowmd5%% *}"
local lastmd5
lastmd5="$(uci get AdGuardHome.AdGuardHome.hostsmd5 2>/dev/null)"
if [ "$nowmd5" != "$lastmd5" ]; then
uci set AdGuardHome.AdGuardHome.hostsmd5="$nowmd5"
uci commit AdGuardHome
[ "$1" != "noreload" ] && /etc/init.d/AdGuardHome reload >/dev/null 2>&1
fi
}
if [ "$1" = "del" ]; then
sed -i '/programaddstart/,/programaddend/d' /etc/hosts
checkmd5 "$2"
exit 0
fi
awk 'BEGIN {
dhcp[""] = ""
}
{
if (FILENAME == "/tmp/dhcp.leases" && (NF >= 4)) {
dhcp[$2] = $4
}
}
END {
cmd = "ip -6 neighbor show 2>/dev/null | grep -v fe80"
while ((cmd | getline line) > 0) {
split(line, f)
if (f[5] in dhcp && f[5] != "") {
print f[1] " " dhcp[f[5]]
}
}
close(cmd)
print "#programaddend"
}' /tmp/dhcp.leases > /tmp/tmphost 2>/dev/null
grep -q "programaddstart" /etc/hosts 2>/dev/null
if [ $? -eq 0 ]; then
sed -i '/programaddstart/,/programaddend/c\#programaddstart' /etc/hosts
sed -i '/programaddstart/r /tmp/tmphost' /etc/hosts
else
{
echo "#programaddstart"
cat /tmp/tmphost
} >> /etc/hosts
fi
rm -f /tmp/tmphost
checkmd5 "$2"
@@ -1,10 +0,0 @@
table inet adguardhome {
chain prerouting {
type nat hook prerouting priority dstnat; policy accept;
__AGH_WAN_RETURN_LINE__
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,8 @@
#!/bin/sh
AdGuardHome_enable=$(uci get AdGuardHome.AdGuardHome.enabled)
redirect=$(uci get AdGuardHome.AdGuardHome.redirect)
if [ $AdGuardHome_enable -eq 1 -a "$redirect" == "redirect" ]; then
/etc/init.d/AdGuardHome do_redirect 1
fi
@@ -4,17 +4,17 @@ PATH="/usr/sbin:/usr/bin:/sbin:/bin"
logread -e AdGuardHome > /tmp/AdGuardHometmp.log
logread -e AdGuardHome -f >> /tmp/AdGuardHometmp.log &
pid=$!
echo "1" > /var/run/AdGuardHomesyslog
echo "1">/var/run/AdGuardHomesyslog
while true
do
sleep 12
watchdog=$(cat /var/run/AdGuardHomesyslog 2>/dev/null)
if [ "${watchdog}x" = "0x" ]; then
kill "$pid"
watchdog=$(cat /var/run/AdGuardHomesyslog)
if [ "$watchdog"x == "0"x ]; then
kill $pid
rm /tmp/AdGuardHometmp.log
rm /var/run/AdGuardHomesyslog
exit 0
else
echo "0" > /var/run/AdGuardHomesyslog
echo "0">/var/run/AdGuardHomesyslog
fi
done
done
@@ -0,0 +1,3 @@
https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_${Arch}.tar.gz
https://github.com/AdguardTeam/AdGuardHome/releases/download/${latest_ver}/AdGuardHome_linux_${Arch}.tar.gz
https://static.adguard.com/adguardhome/release/AdGuardHome_linux_${Arch}.tar.gz
@@ -0,0 +1,5 @@
#!/bin/sh
tail -n $1 "$2" > /var/run/tailtmp
cat /var/run/tailtmp > "$2"
rm /var/run/tailtmp
@@ -1,164 +1,321 @@
#!/bin/sh
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
update_mode=$1
# Keep the lock file: unlinking it would allow a second lock on a new inode.
exec 9>/var/lock/AdGuardHome-update.lock || exit 1
# Allow a brief status probe to finish before treating the lock as busy.
if ! flock -w 1 9; then
printf '%s\n' '已经有更新任务正在运行,请等待其结束。'
exit 2
fi
# Only the lock owner may reset the log and update status.
exec >/tmp/AdGuardHome_update.log 2>&1
cleanup() {
local result=$?
trap - EXIT
[ "$result" -ne 0 ] && touch /var/run/update_core_error
rm -f /var/run/update_core
exit "$result"
}
trap cleanup EXIT
trap 'exit 1' TERM INT HUP
rm -f /var/run/update_core_error
touch /var/run/update_core
binpath=$(uci get AdGuardHome.AdGuardHome.binpath 2>/dev/null)
binpath="$(uci get AdGuardHome.AdGuardHome.binpath 2>/dev/null)"
if [ -z "$binpath" ]; then
uci set AdGuardHome.AdGuardHome.binpath="/tmp/AdGuardHome/AdGuardHome"
binpath="/tmp/AdGuardHome/AdGuardHome"
fi
[ ! -d "${binpath%/*}" ] && mkdir -p "${binpath%/*}"
enabled=$(uci get AdGuardHome.AdGuardHome.enabled 2>/dev/null)
core_api_url="https://api.github.com/repos/AdguardTeam/AdGuardHome/releases/latest"
Check_Updates(){
downloader_cmd="uclient-fetch"
downloader_opts="-T 5 -q -O"
downloader_quiet_opts="-q -O -"
printf "%s\n" "开始检查更新, 请耐心等待 ..."
Cloud_Version="$(${downloader_cmd} ${downloader_quiet_opts} "$core_api_url" 2>/dev/null | sed -n 's/.*"tag_name":[ ]*"\(v[^"]*\)".*/\1/p' | head -n 1)"
[ -z "$Cloud_Version" ] && printf "\n检查更新失败, 请检查网络或稍后重试!\n" && EXIT 1
if [ -f "$binpath" ]; then
Current_Version="$($binpath --version 2>/dev/null | sed -n 's/.*\(v[0-9][0-9.]*\).*/\1/p' | head -n 1)"
else
Current_Version="未知"
fi
[ -z "$Current_Version" ] && Current_Version="未知"
printf "\n执行文件路径: %s\n\n正在检查更新, 请耐心等待 ...\n" "${binpath%/*}"
printf "\n当前 AdGuardHome 版本: %s\n云端 AdGuardHome 版本: %s\n" "$Current_Version" "$Cloud_Version"
if [ "$Cloud_Version" != "$Current_Version" ] || [ "$1" = force ]; then
Update_Core
else
printf "\n已是最新版本, 无需更新!\n"
EXIT 0
fi
EXIT 0
}
mkdir -p "${binpath%/*}"
Update_Core(){
rm -rf /tmp/AdGuardHome_Update > /dev/null 2>&1
mkdir -p "/tmp/AdGuardHome_Update"
GET_Arch
link="https://github.com/AdguardTeam/AdGuardHome/releases/download/${Cloud_Version}/AdGuardHome_linux_${Arch}.tar.gz"
printf "%s\n" "下载链接:${link}"
printf "%s\n" "文件名称:${link##*/}"
printf "\n开始下载 AdGuardHome 核心文件 ...\n\n"
${downloader_cmd} ${downloader_opts} "/tmp/AdGuardHome_Update/${link##*/}" "$link" > /dev/null 2>&1
if [ $? -ne 0 ]; then
printf "\nAdGuardHome 核心下载失败,请检查网络、系统时间及 CA 证书 ...\n"
rm -rf /tmp/AdGuardHome_Update
EXIT 1
fi
if [ "${link##*.}" = "gz" ]; then
printf "\n正在解压 AdGuardHome ...\n"
tar -zxf "/tmp/AdGuardHome_Update/${link##*/}" -C "/tmp/AdGuardHome_Update/"
if [ ! -e /tmp/AdGuardHome_Update/AdGuardHome ]
then
printf "%s\n" "AdGuardHome 核心解压失败!"
rm -rf "/tmp/AdGuardHome_Update" > /dev/null 2>&1
EXIT 1
fi
downloadbin="/tmp/AdGuardHome_Update/AdGuardHome/AdGuardHome"
else
downloadbin="/tmp/AdGuardHome_Update/${link##*/}"
fi
chmod +x "$downloadbin"
size_bytes=$(wc -c < "$downloadbin" 2>/dev/null || echo 0)
printf "\nAdGuardHome 核心体积: %s\n" "$(awk -v bytes="$size_bytes" 'BEGIN{printf "%.2fMB", bytes/1000000}')"
/etc/init.d/AdGuardHome stop 9>&- > /dev/null 2>&1
printf "\n移动 AdGuardHome 核心文件到 %s ...\n" "${binpath%/*}"
if ! mv -f "$downloadbin" "$binpath" > /dev/null 2>&1 || [ ! -s "$binpath" ]; then
printf "AdGuardHome 核心移动失败!\n可能是设备空间不足导致, 请尝试更改 [执行文件路径] 为 /tmp/AdGuardHome\n"
EXIT 1
fi
rm -rf /tmp/AdGuardHome_Update
chmod +x "$binpath"
if [ "$enabled" = 1 ]; then
printf "\n正在重启 AdGuardHome 服务...\n"
/etc/init.d/AdGuardHome restart 9>&- > /dev/null 2>&1
fi
printf "\nAdGuardHome 核心更新成功!\n"
}
upxflag="$(uci get AdGuardHome.AdGuardHome.upxflag 2>/dev/null)"
GET_Arch() {
if command -v apk > /dev/null 2>&1; then
PM="apk"
elif command -v opkg > /dev/null 2>&1; then
PM="opkg"
else
printf "%s\n" "未找到包管理器 (apk/opkg)!" && EXIT 1
fi
case "${PM}" in
apk)
Archt="$(apk info -a kernel 2>/dev/null | grep Architecture | awk -F ":[ ]*" '{print $2}')"
[ -z "$Archt" ] && Archt="$(uname -m)"
;;
opkg)
Archt="$(opkg info kernel | grep Architecture | awk -F "[ _]" '{print($2)}')"
;;
esac
case "${Archt}" in
i386)
Arch=i386
;;
i686)
Arch=i386
;;
x86|x86_64)
Arch=amd64
;;
mipsel)
Arch=mipsle_softfloat
;;
mips)
Arch=mips_softfloat
;;
mips64el)
Arch=mips64le_softfloat
;;
mips64)
Arch=mips64_softfloat
;;
arm)
Arch=arm
;;
armeb)
Arch=armeb
;;
aarch64)
Arch=arm64
;;
*)
printf "\nAdGuardHome 暂不支持当前的设备架构: [%s]!\n" "$Archt"
EXIT 1
esac
printf "\n当前设备架构: %s\n" "$Arch"
}
EXIT(){
cleanup() {
rm -f /var/run/update_core 2>/dev/null
[ "$1" != "0" ] && touch /var/run/update_core_error
exit "$1"
}
Check_Updates "$update_mode"
check_already_running(){
# 先检查锁,再 touch(顺序关键:touch 在函数调用之后执行)
if [ -f /var/run/update_core ]; then
echo "已有任务在运行,退出"
exit 2
fi
}
check_wgetcurl(){
if which curl >/dev/null 2>&1; then
downloader="curl -L -k --retry 2 --connect-timeout 20 -o"
return 0
fi
if which wget-ssl >/dev/null 2>&1; then
downloader="wget-ssl --no-check-certificate -t 2 -T 20 -O"
return 0
fi
if [ -z "$1" ]; then
opkg update >/dev/null 2>&1 || { echo "错误:opkg 更新失败"; cleanup 1; }
fi
if [ -z "$1" ]; then
opkg remove wget wget-nossl --force-depends >/dev/null 2>&1
opkg install wget >/dev/null 2>&1 && check_wgetcurl 1 && return 0
fi
if [ "$1" = "1" ]; then
opkg install curl >/dev/null 2>&1 && check_wgetcurl 2 && return 0
fi
if [ "$1" = "2" ]; then
check_wgetcurl && return 0
fi
echo "错误:缺少 curl 和 wget"
cleanup 1
}
get_arch(){
# 尝试多种方式获取架构:opkg → apk → uname -m
local a
a="$(opkg info kernel 2>/dev/null | grep Architecture | awk '{print $2}')"
[ -z "$a" ] && a="$(apk info --architecture 2>/dev/null)"
[ -z "$a" ] && a="$(uname -m)"
echo "$a"
}
detect_arch(){
local Archt
Archt="$(get_arch)"
case "$Archt" in
i386|i686|i386|x86)
Arch="386"
;;
x86_64|x86-64|amd64)
Arch="amd64"
;;
mipsel|mipsle)
Arch="mipsle"
;;
mips64el|mips64le)
Arch="mips64le"
;;
mips)
Arch="mips"
;;
mips64)
Arch="mips64"
;;
arm|armv7*|armv8*)
Arch="arm"
;;
armeb)
Arch="armeb"
;;
aarch64|arm64)
Arch="arm64"
;;
*)
echo "错误:不支持的架构:$Archt"
cleanup 1
;;
esac
}
check_latest_version(){
check_wgetcurl
release_channel="$(uci get AdGuardHome.AdGuardHome.release_channel 2>/dev/null)"
[ -z "$release_channel" ] && release_channel="stable"
if [ "$release_channel" = "beta" ]; then
echo "正在检查最新版本(预发布通道)..."
# /releases 返回时间倒序的所有 release,含 prerelease。取第一条 = 最新 beta。
local api_url="https://api.github.com/repos/AdguardTeam/AdGuardHome/releases"
else
echo "正在检查最新版本(正式版通道)..."
local api_url="https://api.github.com/repos/AdguardTeam/AdGuardHome/releases/latest"
fi
local api_result
api_result="$($downloader - "$api_url" 2>/dev/null)"
latest_ver="$(echo "$api_result" | grep -oE '"tag_name": *"v[^"]+"' | head -1 | sed 's/.*"v\(.*\)".*/v\1/')"
if [ -z "${latest_ver}" ]; then
echo "检查最新版本失败,请稍后重试"
cleanup 1
fi
if [ -x "$binpath" ]; then
now_ver="$($binpath --version 2>/dev/null | grep -oE 'v[0-9.]+(-[A-Za-z0-9.]+)?' | head -1)"
[ -z "$now_ver" ] && now_ver="$($binpath -c /dev/null --check-config 2>&1 | grep -oE 'v[0-9.]+(-[A-Za-z0-9.]+)?' | head -1)"
else
now_ver=""
fi
echo "本地版本:${now_ver:-无},云端版本:${latest_ver}"
if [ "$1" = "force" ]; then
update_core
elif [ -z "$now_ver" ]; then
update_core
elif [ "$(echo "$latest_ver" | sed 's/[^0-9.]//g')" != "$(echo "$now_ver" | sed 's/[^0-9.]//g')" ]; then
# 只有云端版本号严格大于本地时才更新
local newer
newer="$(printf '%s\n%s' "$latest_ver" "$now_ver" | sed 's/^v//' | sort -Vr | head -1 | sed 's/^/v/')"
if [ "$newer" = "$latest_ver" ] && [ "$latest_ver" != "$now_ver" ]; then
update_core
else
echo "已是最新版本(本地版本更高或相同)"
cleanup 0
fi
else
echo "已是最新版本"
cleanup 0
fi
}
apply_upx(){
[ -z "$upxflag" ] && return
local target="${1:-$binpath}"
[ ! -f "$target" ] && return
local filesize
filesize="$(ls -l "$target" 2>/dev/null | awk '{print $5}')"
[ -z "$filesize" ] && return
[ "$filesize" -le 8000000 ] && return
echo "二进制文件大于 8MB,正在使用 upx 压缩..."
fetch_upx
local UPX_DIR="/tmp/upx-${upx_ver_nov}-${Arch}_linux"
local UPX_BIN="${UPX_DIR}/upx"
[ ! -x "$UPX_BIN" ] && { echo "upx 可执行文件未找到"; return; }
local upx_out="/tmp/AdGuardHomeupdate/upx-packed-$$"
$UPX_BIN $upxflag "$target" -o "$upx_out"
local upxret=$?
rm -rf "$UPX_DIR"
if [ $upxret -eq 0 ]; then
rm -f "$target"
mv -f "$upx_out" "$target"
chmod 755 "$target"
echo "upx 压缩完成"
else
rm -f "$upx_out"
fi
}
fetch_upx(){
local Archt_upx
Archt_upx="$(get_arch)"
case "$Archt_upx" in
i386|i686|x86|i386) Arch="i386";;
x86_64|x86-64|amd64) Arch="amd64";;
mipsel|mipsle) Arch="mipsel";;
mips64el|mips64le) Arch="mipsel";;
mips) Arch="mips";;
mips64) Arch="mips";;
arm|armv7*|armv8*) Arch="arm";;
aarch64|arm64) Arch="arm64";;
*)
echo "upx:不支持的架构 $Archt_upx"
return 1
;;
esac
upx_latest_ver="$($downloader - "https://api.github.com/repos/upx/upx/releases/latest" 2>/dev/null | grep -oE '"tag_name": *"[^"]+"' | head -1 | sed 's/.*"\(.*\)"/\1/')"
if [ -z "$upx_latest_ver" ]; then
echo "获取 upx 版本失败"
return 1
fi
upx_ver_nov="$(echo "$upx_latest_ver" | sed 's/^v//')"
local UPX_TGZ="/tmp/upx-${upx_ver_nov}-${Arch}_linux.tar.xz"
local UPX_URL="https://github.com/upx/upx/releases/download/${upx_latest_ver}/upx-${upx_ver_nov}-${Arch}_linux.tar.xz"
$downloader "$UPX_TGZ" "$UPX_URL" 2>&1
[ $? -ne 0 ] && { echo "upx 下载失败"; return 1; }
which xz >/dev/null 2>&1 || { opkg install xz >/dev/null 2>&1 || apk add xz >/dev/null 2>&1; } || { echo "xz 不可用"; return 1; }
xz -d -c "$UPX_TGZ" | tar -x -C "/tmp" >/dev/null 2>&1
[ ! -x "/tmp/upx-${upx_ver_nov}-${Arch}_linux/upx" ] && { echo "upx 解压失败"; return 1; }
rm -f "$UPX_TGZ"
}
update_core(){
echo "正在更新 AdGuardHome 核心..."
mkdir -p "/tmp/AdGuardHomeupdate"
rm -rf "/tmp/AdGuardHomeupdate/*" 2>/dev/null
detect_arch
echo "架构:$Arch"
echo "正在获取下载链接..."
mkdir -p /tmp/run
# 按通道生成下载源:beta 优先 static beta;stable 优先 static release
if [ "$release_channel" = "beta" ]; then
{
echo "https://static.adguard.com/adguardhome/beta/AdGuardHome_linux_${Arch}.tar.gz"
echo "https://github.com/AdguardTeam/AdGuardHome/releases/download/${latest_ver}/AdGuardHome_linux_${Arch}.tar.gz"
} > /tmp/run/AdHlinks.txt
else
{
echo "https://static.adguard.com/adguardhome/release/AdGuardHome_linux_${Arch}.tar.gz"
echo "https://github.com/AdguardTeam/AdGuardHome/releases/download/${latest_ver}/AdGuardHome_linux_${Arch}.tar.gz"
} > /tmp/run/AdHlinks.txt
fi
local downloadbin=""
local success=""
while read link; do
[ -z "$link" ] && continue
eval link="$link"
echo "尝试下载:$link"
$downloader "/tmp/AdGuardHomeupdate/${link##*/}" "$link" 2>&1
if [ $? -eq 0 ] && [ -s "/tmp/AdGuardHomeupdate/${link##*/}" ]; then
downloadbin="/tmp/AdGuardHomeupdate/${link##*/}"
success="1"
echo "下载成功"
break
else
echo "下载失败,尝试下一个..."
rm -f "/tmp/AdGuardHomeupdate/${link##*/}"
fi
done < /tmp/run/AdHlinks.txt
rm -f /tmp/run/AdHlinks.txt
if [ -z "$success" ]; then
echo "错误:所有下载源均失败"
cleanup 1
fi
if [ "${downloadbin##*.}" = "gz" ]; then
tar -zxf "$downloadbin" -C "/tmp/AdGuardHomeupdate/" 2>/dev/null
if [ -d "/tmp/AdGuardHomeupdate/AdGuardHome" ]; then
downloadbin="/tmp/AdGuardHomeupdate/AdGuardHome/AdGuardHome"
else
echo "错误:解压失败"
cleanup 1
fi
fi
chmod 755 "$downloadbin"
echo "下载完成,如已配置 upx 则进行压缩..."
apply_upx "$downloadbin"
echo "正在停止服务..."
/etc/init.d/AdGuardHome stop nobackup 2>/dev/null
echo "正在安装新二进制文件..."
rm -f "$binpath" 2>/dev/null
mv -f "$downloadbin" "$binpath" 2>/dev/null
[ $? -ne 0 ] && { echo "mv 失败 - 磁盘空间不足?"; cleanup 1; }
chmod 755 "$binpath"
rm -rf "/tmp/AdGuardHomeupdate" 2>/dev/null
echo "正在启动服务..."
/etc/init.d/AdGuardHome start 2>/dev/null
echo "AdGuardHome 已成功更新至 ${latest_ver}"
cleanup 0
}
trap "cleanup 1" SIGTERM SIGINT
check_already_running
touch /var/run/update_core
rm -f /var/run/update_core_error 2>/dev/null
check_latest_version "$1"
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
PATH="/usr/sbin:/usr/bin:/sbin:/bin"
count=0
while :; do
for host in www.baidu.com 223.5.5.5 www.google.com 8.8.8.8; do
ping -c 1 -W 2 -q "$host" >/dev/null 2>&1
if [ $? -eq 0 ]; then
/etc/init.d/AdGuardHome force_reload
exit 0
fi
done
count=$((count + 1))
if [ $count -gt 18 ]; then
/etc/init.d/AdGuardHome force_reload
exit 0
fi
sleep 5
done
@@ -10,4 +10,4 @@ do
break
fi
done
exit 0
return 0
@@ -1,45 +1,53 @@
{
"admin/services/AdGuardHome": {
"title": "AdGuard Home",
"order": 12,
"action": {
"type": "firstchild"
},
"depends": {
"acl": [ "luci-app-adguardhome" ]
}
},
"admin/services/AdGuardHome/base": {
"title": "Base Setting",
"order": 1,
"action": {
"type": "view",
"path": "adguardhome/base"
},
"depends": {
"acl": [ "luci-app-adguardhome" ]
}
},
"admin/services/AdGuardHome/log": {
"title": "Log",
"order": 3,
"action": {
"type": "view",
"path": "adguardhome/log"
},
"depends": {
"acl": [ "luci-app-adguardhome" ]
}
},
"admin/services/AdGuardHome/manual": {
"title": "Manual Config",
"order": 2,
"action": {
"type": "view",
"path": "adguardhome/manual"
},
"depends": {
"acl": [ "luci-app-adguardhome" ]
}
}
"admin/services/AdGuardHome": {
"title": "AdGuard Home",
"order": 10,
"action": {
"type": "alias",
"path": "admin/services/AdGuardHome/overview"
},
"depends": {
"acl": [ "luci-app-adguardhome" ]
}
},
"admin/services/AdGuardHome/overview": {
"title": "概览",
"order": 1,
"action": {
"type": "cbi",
"path": "AdGuardHome/overview"
}
},
"admin/services/AdGuardHome/base": {
"title": "基础设置",
"order": 2,
"action": {
"type": "cbi",
"path": "AdGuardHome/base"
}
},
"admin/services/AdGuardHome/tools": {
"title": "运维",
"order": 3,
"action": {
"type": "cbi",
"path": "AdGuardHome/tools"
}
},
"admin/services/AdGuardHome/log": {
"title": "日志",
"order": 4,
"action": {
"type": "form",
"path": "AdGuardHome/log"
}
},
"admin/services/AdGuardHome/manual": {
"title": "手动配置",
"order": 5,
"action": {
"type": "cbi",
"path": "AdGuardHome/manual"
}
}
}
@@ -2,20 +2,10 @@
"luci-app-adguardhome": {
"description": "Grant UCI access for luci-app-adguardhome",
"read": {
"file": {
"/var/run/AdGredir": [ "read" ]
},
"ubus": {
"service": [ "list" ],
"luci.adguardhome": [ "getCoreInfo", "getRedirectStatus", "getCoreUpdate", "getInterfaces", "getRuntimeLog", "getManualConfig" ]
},
"uci": [ "AdGuardHome" ]
},
"write": {
"ubus": {
"luci.adguardhome": [ "setVerboseLog", "startCoreUpdate", "changeWebPassword", "clearRuntimeLog", "setManualConfig" ]
},
"uci": [ "AdGuardHome" ]
}
}
}
}
@@ -1,265 +0,0 @@
#!/usr/bin/ucode
'use strict';
import { stat, open, popen, readfile, writefile, unlink, lsdir } from 'fs';
import { cursor } from 'uci';
function shellQuote(value) {
return "'" + replace(value, /'/g, "'\\''") + "'";
}
function coreUpdateRunning() {
// The kernel releases the lock even if the updater is killed.
const result = system(['/usr/bin/flock', '-n', '-E', '2',
'/var/lock/AdGuardHome-update.lock', '/bin/true'], 3000);
if (result != 0 && result != 2)
die('Unable to check core update lock');
return result == 2;
}
return {
'luci.adguardhome': {
getRedirectStatus: {
call: function() {
const conf = cursor();
function option(name, fallback) {
const value = conf.get('AdGuardHome', 'AdGuardHome', name);
return (type(value) == 'array' ? value[0] : value) || fallback;
}
const result = {
mode: option('redirect', 'none'),
enabled: option('enabled', '0') == '1',
applied_mode: option('old_redirect', 'none'),
applied_enabled: option('old_enabled', '0') == '1',
binpath: option('binpath', '/usr/bin/AdGuardHome/AdGuardHome'),
redirected: trim(readfile('/var/run/AdGredir') || '') == '1'
};
conf.unload('AdGuardHome');
const lock = system(['/usr/bin/flock', '-n', '-E', '2',
'/var/lock/AdGuardHome-redirect.lock', '/bin/true'], 3000);
if (lock != 0 && lock != 2)
die('Unable to check redirect status');
result.busy = lock == 2;
return result;
}
},
getManualConfig: {
call: function() {
const conf = cursor();
const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
'/etc/AdGuardHome.yaml';
conf.unload('AdGuardHome');
const template = readfile('/usr/share/AdGuardHome/AdGuardHome_template.yaml') || '';
const current = readfile(configpath);
if (current != null)
return { content: current, template: template, source: 'current' };
if (stat(configpath) != null)
return { content: '', template: template, source: 'error' };
return {
content: template,
template: template,
source: template ? 'template' : 'missing'
};
}
},
setManualConfig: {
args: { content: '' },
call: function(request) {
const content = request.args.content;
if (content == null)
return { saved: false, reason: 'invalid_content' };
const conf = cursor();
const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
'/etc/AdGuardHome.yaml';
const binpath = conf.get('AdGuardHome', 'AdGuardHome', 'binpath') ||
'/usr/bin/AdGuardHome/AdGuardHome';
conf.unload('AdGuardHome');
const temporaryConfig = '/tmp/AdGuardHometmpconfig.yaml';
const validationLog = '/tmp/AdGuardHometest.log';
if (writefile(temporaryConfig, replace(content, /\r\n/g, '\n')) == null)
return { saved: false, reason: 'write_failed' };
if (stat(binpath)?.type == 'file') {
const valid = system(['/bin/sh', '-c', '"$1" -c "$2" --check-config > "$3" 2>&1',
'sh', binpath, temporaryConfig, validationLog], 30000) == 0;
if (!valid)
return { saved: false, reason: 'validation_failed', log: readfile(validationLog) || '' };
}
if (writefile(configpath, readfile(temporaryConfig) || '') == null)
return { saved: false, reason: 'save_failed' };
unlink(temporaryConfig);
if (system(['/etc/init.d/AdGuardHome', 'reload'], 30000) != 0)
return { saved: false, reason: 'reload_failed' };
return { saved: true };
}
},
getInterfaces: {
call: function() {
return { interfaces: lsdir('/sys/class/net') || [] };
}
},
getCoreInfo: {
call: function() {
const conf = cursor();
const binpath = conf.get('AdGuardHome', 'AdGuardHome', 'binpath') ||
'/usr/bin/AdGuardHome/AdGuardHome';
const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
'/etc/AdGuardHome.yaml';
const result = {
core_exists: stat(binpath)?.type == 'file',
config_exists: stat(configpath)?.type == 'file',
version: null
};
if (result.core_exists) {
// Pass the configured path as a positional argument; capture both output streams.
const proc = popen('/bin/sh -c \'"$1" --version 2>&1\' sh ' + shellQuote(binpath), 'r');
if (proc) {
const output = proc.read('all') || '';
proc.close();
result.version = match(output, /version ([^, \t\r\n]+)/)?.[1] || null;
}
}
conf.unload('AdGuardHome');
return result;
}
},
getCoreUpdate: {
call: function() {
const running = coreUpdateRunning();
const failed = stat('/var/run/update_core_error') != null ||
(!running && stat('/var/run/update_core') != null);
const log = readfile('/tmp/AdGuardHome_update.log', 131072) || '';
return {
state: running ? 'running' : failed ? 'failure' : log ? 'finished' : 'idle',
log: log
};
}
},
startCoreUpdate: {
args: { force: false },
call: function(request) {
if (coreUpdateRunning())
return { started: false, reason: 'busy' };
const command = request.args.force
? '/bin/sh /usr/share/AdGuardHome/update_core.sh force >/dev/null 2>&1 </dev/null &'
: '/bin/sh /usr/share/AdGuardHome/update_core.sh >/dev/null 2>&1 </dev/null &';
return { started: system(['/bin/sh', '-c', command], 3000) == 0 };
}
},
changeWebPassword: {
args: { hash: '' },
call: function(request) {
const hash = request.args.hash;
if (!match(hash, /^\$2[aby]\$(0[4-9]|[12][0-9]|3[01])\$[.\/A-Za-z0-9]{53}$/))
return { changed: false, reason: 'invalid_hash' };
const conf = cursor();
const configpath = conf.get('AdGuardHome', 'AdGuardHome', 'configpath') ||
'/etc/AdGuardHome.yaml';
if (stat(configpath)?.type != 'file')
return { changed: false, reason: 'no_config' };
if (!conf.set('AdGuardHome', 'AdGuardHome', 'hashpass', hash) ||
!conf.commit('AdGuardHome'))
return { changed: false, reason: 'save_failed' };
conf.unload('AdGuardHome');
if (system(['/etc/init.d/AdGuardHome', 'reload'], 30000) != 0)
return { changed: false, reason: 'reload_failed' };
if (index(readfile(configpath) || '', hash) < 0)
return { changed: false, reason: 'verify_failed' };
return { changed: true };
}
},
setVerboseLog: {
args: { enabled: false },
call: function(request) {
const conf = cursor();
if (!conf.set('AdGuardHome', 'AdGuardHome', 'verbose', request.args.enabled ? '1' : '0') ||
!conf.commit('AdGuardHome'))
return { saved: false, applied: false };
conf.unload('AdGuardHome');
return {
saved: true,
applied: system(['/etc/init.d/AdGuardHome', 'reload'], 30000) == 0
};
}
},
getRuntimeLog: {
args: { offset: 0, identity: '' },
call: function(request) {
const conf = cursor();
const logfile = conf.get('AdGuardHome', 'AdGuardHome', 'logfile') || '';
conf.unload('AdGuardHome');
if (!logfile)
return { enabled: false, localtime: false, content: '', offset: 0 };
let source = logfile;
if (logfile == 'syslog') {
source = '/tmp/AdGuardHometmp.log';
writefile('/var/run/AdGuardHomesyslog', '1');
if (stat(source)?.type != 'file')
system(['/bin/sh', '-c', '/bin/sh /usr/share/AdGuardHome/getsyslog.sh >/dev/null 2>&1 &'], 3000);
}
// Bound memory and start with the most recent 256 KiB on a new view.
const limit = 262144;
const info = stat(source);
const identity = info ? source + ':' + info.dev.major + ':' + info.dev.minor + ':' + info.inode : '';
let offset = int(request.args.offset) || 0;
let reset = request.args.identity != identity || offset < 0 || offset > (info?.size || 0);
if (reset)
offset = 0;
if (info && info.size - offset > limit) {
offset = info.size - limit;
reset = true;
}
let content = '';
if (info?.type == 'file') {
const file = open(source, 'r');
if (!file)
die('Unable to open runtime log');
const positioned = file.seek(offset);
const chunk = positioned ? file.read(limit) : null;
file.close();
if (chunk == null)
die('Unable to read runtime log');
content = chunk;
}
return {
enabled: true,
localtime: logfile != 'syslog',
// ubus strings stop at NUL; cleared files can contain sparse zero-filled gaps.
content_b64: b64enc(content),
offset: offset + length(content),
identity: identity,
reset: reset
};
}
},
clearRuntimeLog: {
call: function() {
const conf = cursor();
const logfile = conf.get('AdGuardHome', 'AdGuardHome', 'logfile') || '';
conf.unload('AdGuardHome');
if (!logfile)
return { cleared: false, reason: 'no_logfile' };
const source = logfile == 'syslog' ? '/tmp/AdGuardHometmp.log' : logfile;
return { cleared: writefile(source, '') != null };
}
}
}
};
@@ -0,0 +1 @@
!function(n){"object"==typeof exports&&"object"==typeof module?n(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],n):n(CodeMirror)}(function(n){"use strict";function e(e,o,i,t){if(i&&i.call){var l=i;i=null}else l=r(e,i,"rangeFinder");"number"==typeof o&&(o=n.Pos(o,0));var f=r(e,i,"minFoldSize");function d(n){var r=l(e,o);if(!r||r.to.line-r.from.line<f)return null;for(var i=e.findMarksAt(r.from),d=0;d<i.length;++d)if(i[d].__isFold&&"fold"!==t){if(!n)return null;r.cleared=!0,i[d].clear()}return r}var u=d(!0);if(r(e,i,"scanUp"))for(;!u&&o.line>e.firstLine();)o=n.Pos(o.line-1,0),u=d(!1);if(u&&!u.cleared&&"unfold"!==t){var a=function(n,e){var o=r(n,e,"widget");if("string"==typeof o){var i=document.createTextNode(o);(o=document.createElement("span")).appendChild(i),o.className="CodeMirror-foldmarker"}else o&&(o=o.cloneNode(!0));return o}(e,i);n.on(a,"mousedown",function(e){c.clear(),n.e_preventDefault(e)});var c=e.markText(u.from,u.to,{replacedWith:a,clearOnEnter:r(e,i,"clearOnEnter"),__isFold:!0});c.on("clear",function(o,r){n.signal(e,"unfold",e,o,r)}),n.signal(e,"fold",e,u.from,u.to)}}n.newFoldFunction=function(n,o){return function(r,i){e(r,i,{rangeFinder:n,widget:o})}},n.defineExtension("foldCode",function(n,o,r){e(this,n,o,r)}),n.defineExtension("isFolded",function(n){for(var e=this.findMarksAt(n),o=0;o<e.length;++o)if(e[o].__isFold)return!0}),n.commands.toggleFold=function(n){n.foldCode(n.getCursor())},n.commands.fold=function(n){n.foldCode(n.getCursor(),null,"fold")},n.commands.unfold=function(n){n.foldCode(n.getCursor(),null,"unfold")},n.commands.foldAll=function(e){e.operation(function(){for(var o=e.firstLine(),r=e.lastLine();o<=r;o++)e.foldCode(n.Pos(o,0),null,"fold")})},n.commands.unfoldAll=function(e){e.operation(function(){for(var o=e.firstLine(),r=e.lastLine();o<=r;o++)e.foldCode(n.Pos(o,0),null,"unfold")})},n.registerHelper("fold","combine",function(){var n=Array.prototype.slice.call(arguments,0);return function(e,o){for(var r=0;r<n.length;++r){var i=n[r](e,o);if(i)return i}}}),n.registerHelper("fold","auto",function(n,e){for(var o=n.getHelpers(e,"fold"),r=0;r<o.length;r++){var i=o[r](n,e);if(i)return i}});var o={rangeFinder:n.fold.auto,widget:"↔",minFoldSize:0,scanUp:!1,clearOnEnter:!0};function r(n,e,r){if(e&&void 0!==e[r])return e[r];var i=n.options.foldOptions;return i&&void 0!==i[r]?i[r]:o[r]}n.defineOption("foldOptions",null),n.defineExtension("foldOption",function(n,e){return r(this,n,e)})});
@@ -0,0 +1 @@
.CodeMirror-foldmarker{color:blue;text-shadow:#b9f 1px 1px 2px,#b9f -1px -1px 2px,#b9f 1px -1px 2px,#b9f -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}.CodeMirror-foldgutter{width:.7em}.CodeMirror-foldgutter-open,.CodeMirror-foldgutter-folded{cursor:pointer}.CodeMirror-foldgutter-open:after{content:"\25BE"}.CodeMirror-foldgutter-folded:after{content:"\25B8"}
@@ -0,0 +1 @@
!function(t){"object"==typeof exports&&"object"==typeof module?t(require("../../lib/codemirror"),require("./foldcode")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","./foldcode"],t):t(CodeMirror)}(function(t){"use strict";t.defineOption("foldGutter",!1,function(o,e,r){r&&r!=t.Init&&(o.clearGutter(o.state.foldGutter.options.gutter),o.state.foldGutter=null,o.off("gutterClick",a),o.off("changes",d),o.off("viewportChange",u),o.off("fold",l),o.off("unfold",l),o.off("swapDoc",d)),e&&(o.state.foldGutter=new function(t){this.options=t,this.from=this.to=0}(function(t){!0===t&&(t={});null==t.gutter&&(t.gutter="CodeMirror-foldgutter");null==t.indicatorOpen&&(t.indicatorOpen="CodeMirror-foldgutter-open");null==t.indicatorFolded&&(t.indicatorFolded="CodeMirror-foldgutter-folded");return t}(e)),f(o),o.on("gutterClick",a),o.on("changes",d),o.on("viewportChange",u),o.on("fold",l),o.on("unfold",l),o.on("swapDoc",d))});var o=t.Pos;function e(t,e){for(var r=t.findMarks(o(e,0),o(e+1,0)),n=0;n<r.length;++n)if(r[n].__isFold){var i=r[n].find(-1);if(i&&i.line===e)return r[n]}}function r(t){if("string"==typeof t){var o=document.createElement("div");return o.className=t+" CodeMirror-guttermarker-subtle",o}return t.cloneNode(!0)}function n(t,n,f){var a=t.state.foldGutter.options,d=n-1,u=t.foldOption(a,"minFoldSize"),l=t.foldOption(a,"rangeFinder"),c="string"==typeof a.indicatorFolded&&i(a.indicatorFolded),s="string"==typeof a.indicatorOpen&&i(a.indicatorOpen);t.eachLine(n,f,function(n){++d;var i=null,f=n.gutterMarkers;if(f&&(f=f[a.gutter]),e(t,d)){if(c&&f&&c.test(f.className))return;i=r(a.indicatorFolded)}else{var p=o(d,0),m=l&&l(t,p);if(m&&m.to.line-m.from.line>=u){if(s&&f&&s.test(f.className))return;i=r(a.indicatorOpen)}}(i||f)&&t.setGutterMarker(n,a.gutter,i)})}function i(t){return new RegExp("(^|\\s)"+t+"(?:$|\\s)\\s*")}function f(t){var o=t.getViewport(),e=t.state.foldGutter;e&&(t.operation(function(){n(t,o.from,o.to)}),e.from=o.from,e.to=o.to)}function a(t,r,n){var i=t.state.foldGutter;if(i){var f=i.options;if(n==f.gutter){var a=e(t,r);a?a.clear():t.foldCode(o(r,0),f)}}}function d(t){var o=t.state.foldGutter;if(o){var e=o.options;o.from=o.to=0,clearTimeout(o.changeUpdate),o.changeUpdate=setTimeout(function(){f(t)},e.foldOnChangeTimeSpan||600)}}function u(t){var o=t.state.foldGutter;if(o){var e=o.options;clearTimeout(o.changeUpdate),o.changeUpdate=setTimeout(function(){var e=t.getViewport();o.from==o.to||e.from-o.to>20||o.from-e.to>20?f(t):t.operation(function(){e.from<o.from&&(n(t,e.from,o.from),o.from=e.from),e.to>o.to&&(n(t,o.to,e.to),o.to=e.to)})},e.updateViewportTimeSpan||400)}}function l(t,o){var e=t.state.foldGutter;if(e){var r=o.line;r>=e.from&&r<e.to&&n(t,r,r+1)}}});
@@ -0,0 +1 @@
!function(e){"object"==typeof exports&&"object"==typeof module?e(require("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],e):e(CodeMirror)}(function(e){"use strict";function n(n,t){var i=n.getLine(t),o=i.search(/\S/);return-1==o||/\bcomment\b/.test(n.getTokenTypeAt(e.Pos(t,o+1)))?-1:e.countColumn(i,null,n.getOption("tabSize"))}e.registerHelper("fold","indent",function(t,i){var o=n(t,i.line);if(!(o<0)){for(var r=null,l=i.line+1,f=t.lastLine();l<=f;++l){var u=n(t,l);if(-1==u);else{if(!(u>o))break;r=l}}return r?{from:e.Pos(i.line,t.getLine(i.line).length),to:e.Pos(r,t.getLine(r).length)}:void 0}})});
@@ -1,276 +0,0 @@
'use strict';
'require view';
'require form';
'require poll';
'require rpc';
'require ui';
'require view/adguardhome/status as status';
var callInterfaces = rpc.declare({
object: 'luci.adguardhome',
method: 'getInterfaces',
expect: { '': {} },
reject: true
});
var callCoreUpdate = rpc.declare({
object: 'luci.adguardhome',
method: 'getCoreUpdate',
expect: { '': {} },
reject: true
});
var callStartCoreUpdate = rpc.declare({
object: 'luci.adguardhome',
method: 'startCoreUpdate',
params: [ 'force' ],
expect: { '': {} },
reject: true
});
return view.extend({
handleSaveApply: function(ev, mode) {
var target = {
mode: this.redirectOption.formvalue('AdGuardHome') || 'none',
enabled: this.enabledOption.formvalue('AdGuardHome') === '1'
};
this.sharedStatus.beginSwitch(target);
return this.handleSave(ev).then(function() {
ui.changes.apply(mode == '0');
}).catch(function(error) {
this.sharedStatus.cancelSwitch();
throw error;
}.bind(this));
},
load: function() {
return callInterfaces().catch(function() { return { interfaces: [] }; });
},
render: function(data) {
let m, s, o;
var self = this;
var interfaces = data.interfaces || [];
m = new form.Map('AdGuardHome');
s = m.section(
form.TypedSection,
'AdGuardHome',
_('Basic Settings')
);
s.anonymous = true;
s.addremove = false;
o = s.option(
form.Flag,
'enabled',
_('Enable')
);
this.enabledOption = o;
o.default = '0';
o.rmempty = false;
o = s.option(
form.Value,
'httpport',
_('Browser management port')
);
o.datatype = 'port';
o.placeholder = '3000';
o.default = '3000';
o.rmempty = false;
o = s.option(
form.ListValue,
'redirect',
_('DNS handling mode')
);
this.redirectOption = o;
o.value('none', _('Do not handle DNS'));
o.value('redirect', _('Forward port 53 requests through the firewall (recommended)'));
o.value('dnsmasq-upstream', _('Run as dnsmasq upstream server'));
o.value('exchange', _('Use port 53 instead of dnsmasq'));
o.default = 'none';
o.rmempty = false;
o = s.option(
form.DynamicList,
'wan_ifname',
_('Network devices excluded from forwarding'),
_('DNS requests arriving on these network devices are not forwarded to AdGuardHome')
);
o.depends('redirect', 'redirect');
o.retain = true;
o.datatype = 'string';
o.rmempty = true;
interfaces.filter(function(name) { return name !== 'lo'; }).forEach(function(name) {
o.value(name, name);
});
o = s.option(
form.Value,
'binpath',
_('Bin Path')
);
o.default = '/usr/bin/AdGuardHome/AdGuardHome';
o.rmempty = false;
o = s.option(
form.Value,
'configpath',
_('Config Path')
);
o.default = '/etc/AdGuardHome.yaml';
o.rmempty = false;
o = s.option(
form.Value,
'workdir',
_('Work dir')
);
o.default = '/usr/bin/AdGuardHome';
o.rmempty = false;
return m.render().then(function(node) {
var sharedStatus = status.render();
self.sharedStatus = sharedStatus;
var updateButton = E('button', {
'class': 'btn cbi-button cbi-button-apply', 'type': 'button'
}, _('Update core version'));
var forceButton = E('button', {
'class': 'btn cbi-button', 'type': 'button'
}, _('Force update'));
var updateMessage = E('span', {}, '');
var reverseLog = E('input', { 'type': 'checkbox' });
var updateLog = E('textarea', {
'class': 'cbi-input-textarea agh-update-log', 'rows': 8, 'readonly': 'readonly',
'style': 'display:none;'
});
var logOptions = E('label', { 'class': 'agh-update-options', 'style': 'display:none;' }, [
reverseLog, E('span', {}, _('reverse'))
]);
var updatePanel = E('div', { 'class': 'cbi-value' }, [
E('div', { 'class': 'cbi-value-title' }, _('Upgrade Core')),
E('div', { 'class': 'cbi-value-field' }, E('div', { 'class': 'agh-update-content' }, [
E('div', { 'class': 'agh-toolbar' }, [updateButton, forceButton, updateMessage]),
logOptions, updateLog
]))
]);
var lastLog = '';
var startingAt = 0;
var activeUpdate = false;
var updateFinishedHere = false;
function showLog() {
updateLog.value = reverseLog.checked ? lastLog.split('\n').reverse().join('\n') : lastLog;
}
reverseLog.addEventListener('change', showLog);
function showUpdateStatus(result) {
var state = result.state || 'idle';
if (startingAt && state === 'idle' && Date.now() - startingAt < 10000)
return;
startingAt = 0;
if (state === 'running')
activeUpdate = true;
else if (activeUpdate && (state === 'finished' || state === 'failure')) {
activeUpdate = false;
updateFinishedHere = true;
sharedStatus.refresh();
}
updateButton.disabled = forceButton.disabled = state === 'running';
if (state === 'running' || updateFinishedHere) {
updateMessage.textContent = state === 'running' ? _('Check...') :
state === 'finished' ? _('Updated') : _('Update failed');
lastLog = result.log || '';
logOptions.style.display = lastLog ? 'inline-flex' : 'none';
updateLog.style.display = lastLog ? 'block' : 'none';
showLog();
} else {
updateMessage.textContent = '';
lastLog = '';
logOptions.style.display = updateLog.style.display = 'none';
}
}
function startUpdate(force) {
startingAt = Date.now();
activeUpdate = true;
updateFinishedHere = false;
updateButton.disabled = forceButton.disabled = true;
updateMessage.textContent = _('Check...');
lastLog = '';
showLog();
updateLog.style.display = logOptions.style.display = 'none';
callStartCoreUpdate(force).then(function(result) {
if (!result.started) {
startingAt = 0;
activeUpdate = false;
updateMessage.textContent = _('Update in progress');
return;
}
return callCoreUpdate().then(showUpdateStatus);
}).catch(function() {
startingAt = 0;
activeUpdate = false;
updateButton.disabled = forceButton.disabled = false;
updateMessage.textContent = _('Unable to start update');
});
}
updateButton.addEventListener('click', function() { startUpdate(false); });
forceButton.addEventListener('click', function() { startUpdate(true); });
poll.add(function() {
return callCoreUpdate().then(showUpdateStatus).catch(function() {
updateMessage.textContent = _('Update status unavailable');
});
}, 3);
var settingsCard = node.querySelector('.cbi-section');
settingsCard.classList.add('agh-compact-card', 'agh-base-card');
var settingsFields = settingsCard.querySelector('.cbi-section-node') || settingsCard;
settingsFields.appendChild(updatePanel);
node.insertBefore(sharedStatus.node, node.firstChild);
node.insertBefore(E('link', {
'rel': 'stylesheet', 'href': L.resource('view/adguardhome/compact.css')
}), node.firstChild);
var portInput = node.querySelector('[data-name="httpport"] input');
var portField = node.querySelector('[data-name="httpport"] .cbi-value-field');
if (portInput && portField) {
var webLink = E('input', {
'type': 'button',
'style': 'width:210px;border-color:Teal;text-align:center;font-weight:bold;color:#337ab7;background:#ffc800;',
'target': '_blank',
'value': _('AdGuardHome Web')
});
var linkWrap = E('div', { 'style': 'margin-top:0.35rem;' }, webLink);
var host = window.location.hostname;
if (host.indexOf(':') >= 0 && host.charAt(0) !== '[')
host = '[' + host + ']';
function updateWebLink() {
var port = portInput.value.trim();
var valid = /^[0-9]+$/.test(port) && +port >= 1 && +port <= 65535;
webLink.value = _('AdGuardHome Web') + ': ' + port;
webLink.onclick = valid ? function() {
window.open('http://' + host + ':' + port + '/');
} : null;
webLink.style.display = valid ? 'inline-block' : 'none';
}
portField.appendChild(linkWrap);
portInput.addEventListener('input', updateWebLink);
updateWebLink();
}
return node;
});
}
});
@@ -1,153 +0,0 @@
/* Scoped to the settings and log cards; shared status badges keep their layout. */
.agh-compact-card {
margin-bottom: 0.75rem;
}
.agh-compact-card .cbi-value {
margin-bottom: 0;
padding-top: 0.35rem;
padding-bottom: 0.35rem;
}
.agh-compact-card .cbi-value-description {
margin-top: 0.25rem;
}
.agh-compact-card .agh-toolbar {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem 0.75rem;
}
.agh-compact-card .agh-toolbar > * {
margin: 0;
}
.agh-compact-card .agh-update-content {
width: 100%;
max-width: 40rem;
min-width: 0;
}
.agh-compact-card .agh-update-options {
align-items: center;
gap: 0.35rem;
width: auto;
margin: 0.5rem 0 0;
padding: 0;
float: none;
}
.agh-compact-card .agh-update-options input[type="checkbox"] {
position: static;
flex: 0 0 auto;
margin: 0;
}
.agh-compact-card textarea.agh-update-log {
width: 100%;
height: 10rem;
min-height: 6rem;
box-sizing: border-box;
resize: vertical;
margin: 0.35rem 0 0;
}
.agh-log-view {
padding: 0.6em;
}
.agh-log-view textarea {
display: block;
width: 100%;
height: clamp(12rem, 32vh, 20rem);
min-height: 10rem;
box-sizing: border-box;
resize: vertical;
margin: 0 0 0.6em;
}
.agh-manual-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.5rem 1rem;
padding: 0;
}
.agh-compact-card .agh-password-controls label {
float: none;
width: auto;
padding: 0;
}
.agh-compact-card .agh-password-controls input {
width: 14rem;
max-width: 100%;
min-width: 0;
}
.agh-manual-card {
padding: 0.6em;
}
.agh-editor-wrap {
margin: 0 0 0.6em;
}
.agh-config-actions {
margin-left: auto;
}
/* Keep the base card heading in the theme's original position. */
.agh-base-card .cbi-section-node {
margin: 0;
padding: 0.6em;
}
.agh-log-view .agh-log-controls {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.6em 1.2em;
width: 100%;
margin: 0;
padding: 0;
text-align: left;
}
.agh-log-view .agh-log-toggles {
justify-content: flex-start;
flex: 0 1 auto;
}
.agh-log-view .agh-log-actions {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 0.6em 1.2em;
flex: 0 1 auto;
min-width: 0;
margin-left: auto;
}
.agh-log-view .agh-log-actions > .cbi-value {
display: flex;
align-items: center;
flex-wrap: wrap;
flex: 0 1 auto;
gap: 0.5em;
width: auto;
min-width: 0;
margin: 0;
padding: 0;
border: 0;
}
.agh-log-view .agh-log-actions .cbi-value-title {
float: none;
flex: 0 0 auto;
width: auto;
margin: 0;
padding: 0;
}
.agh-log-view .agh-log-actions .cbi-value-field {
flex: 0 1 auto;
width: auto;
min-width: 0;
margin: 0;
padding: 0;
}
.agh-log-view .agh-log-actions .cbi-value-field input {
width: 22em;
max-width: 100%;
box-sizing: border-box;
}
.agh-log-controls .agh-toolbar > span:empty {
display: none;
}
@@ -1,242 +0,0 @@
'use strict';
'require view';
'require form';
'require poll';
'require rpc';
'require view/adguardhome/status as status';
var callRuntimeLog = rpc.declare({
object: 'luci.adguardhome',
method: 'getRuntimeLog',
params: [ 'offset', 'identity' ],
expect: { '': {} },
reject: true
});
var callClearRuntimeLog = rpc.declare({
object: 'luci.adguardhome',
method: 'clearRuntimeLog',
expect: { '': {} },
reject: true
});
var callSetVerboseLog = rpc.declare({
object: 'luci.adguardhome',
method: 'setVerboseLog',
params: [ 'enabled' ],
expect: { '': {} },
reject: true,
timeout: 45000
});
function decodeLog(result) {
var content = result.content || '';
if (typeof result.content_b64 === 'string') {
var binary = atob(result.content_b64);
var bytes = new Uint8Array(binary.length);
for (var i = 0; i < binary.length; i++)
bytes[i] = binary.charCodeAt(i);
content = new TextDecoder('utf-8').decode(bytes);
}
// Truncating an open log does not reset a non-append writer's file position.
return content.replace(/\0/g, '');
}
function pad(value) {
return value < 10 ? '0' + value : value;
}
function localizeTimestamps(content) {
return content.split('\n').map(function(line) {
var date = new Date(line.substring(0, 19).replace(/\//g, '-') + ' UTC');
if (isNaN(date.getTime()))
return line;
return date.getFullYear() + '/' + pad(date.getMonth() + 1) + '/' + pad(date.getDate()) + ' ' +
pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds()) + line.substring(19);
}).join('\n');
}
return view.extend({
load: function() {
return callRuntimeLog(0, '').catch(function() {
return { error: true, localtime: false, content: '', offset: 0 };
});
},
render: function(initialLog) {
var m = new form.Map('AdGuardHome');
var s = m.section(form.TypedSection, 'AdGuardHome');
s.anonymous = true;
s.addremove = false;
var o;
o = s.option(
form.Value,
'logfile',
_('Runtime log file')
);
o.rmempty = true;
return m.render().then(function(settingsNode) {
var sharedStatus = status.render();
var logLimit = 262144;
var content = decodeLog(initialLog).slice(-logLimit);
var identity = initialLog.identity || '';
var generation = 0;
var offset = initialLog.offset || 0;
var enabled = initialLog.enabled;
var localtime = initialLog.localtime;
var reverse = true;
var convertTime = localtime;
var logArea = E('textarea', {
'class': 'cbi-input-textarea',
'rows': 16,
'readonly': 'readonly',
'style': 'width:100%;box-sizing:border-box;'
});
var toggleStyle = 'margin:0 0.35em 0 0;height:auto;flex:none;';
var reverseToggle = E('input', { 'type': 'checkbox', 'checked': true, 'style': toggleStyle });
var timeToggle = E('input', { 'type': 'checkbox', 'checked': true, 'style': toggleStyle });
var reverseLabel = E('label', { 'style': 'display:inline-flex;align-items:center;' }, [
reverseToggle, _('reverse')
]);
var timeLabel = E('label', { 'style': localtime ? 'display:inline-flex;align-items:center;' : 'display:none;' }, [
timeToggle, _('localtime')
]);
var verboseToggle = E('input', {
'type': 'checkbox', 'style': toggleStyle
});
// Boolean HTML attributes are true whenever present, even with a false value.
verboseToggle.checked = m.data.get('AdGuardHome', 'AdGuardHome', 'verbose') === '1';
verboseToggle.disabled = !!m.readonly;
var verboseMessage = E('span', { 'role': 'status' }, '');
var verboseLabel = E('label', { 'style': 'display:inline-flex;align-items:center;' }, [
verboseToggle, _('Verbose log')
]);
verboseToggle.addEventListener('change', function() {
var requested = verboseToggle.checked;
verboseToggle.disabled = true;
verboseMessage.textContent = _('Applying verbose logging…');
callSetVerboseLog(requested).then(function(result) {
if (!result.saved)
throw new Error('save failed');
verboseMessage.textContent = result.applied ? '' : _('Saved, but service reload failed; check the log');
}).catch(function() {
verboseToggle.checked = !requested;
verboseMessage.textContent = _('Unable to apply verbose logging');
}).then(function() {
verboseToggle.disabled = false;
});
});
var message = E('span', {}, '');
function renderLog() {
var displayed = convertTime ? localizeTimestamps(content) : content;
logArea.value = reverse ? displayed.split('\n').reverse().join('\n') : displayed;
}
function downloadLog() {
var now = new Date();
var link = document.createElement('a');
link.download = 'AdGuardHome' + (now.getMonth() + 1) + '-' + now.getDate() + '-' +
pad(now.getHours()) + '_' + pad(now.getMinutes()) + '.log';
link.href = URL.createObjectURL(new Blob([logArea.value], { type: 'text/plain' }));
link.click();
URL.revokeObjectURL(link.href);
}
var clearButton = E('button', {
'class': 'btn cbi-button cbi-button-apply', 'type': 'button'
}, _('dellog'));
var downloadButton = E('button', {
'class': 'btn cbi-button', 'type': 'button'
}, _('download log'));
reverseToggle.addEventListener('change', function() {
reverse = reverseToggle.checked;
renderLog();
});
timeToggle.addEventListener('change', function() {
convertTime = timeToggle.checked;
renderLog();
});
clearButton.addEventListener('click', function() {
clearButton.disabled = true;
generation++;
callClearRuntimeLog().then(function(result) {
if (!result.cleared)
throw new Error(result.reason || 'clear failed');
content = '';
offset = 0;
identity = '';
generation++;
message.textContent = '';
renderLog();
}).catch(function() {
message.textContent = _('Status unavailable');
}).then(function() {
clearButton.disabled = false;
});
});
downloadButton.addEventListener('click', downloadLog);
var settingsCard = settingsNode.querySelector('.cbi-section');
settingsCard.classList.add('agh-compact-card', 'agh-log-view');
var settingsFields = settingsCard.querySelector('.cbi-section-node');
var logFileField = settingsFields.querySelector('.cbi-value');
var logPath = logFileField.querySelector('input[type="text"]');
if (logPath)
logPath.title = _('If set to "syslog", write to system log; if empty, disable file logging');
var logControls = E('div', { 'class': 'agh-log-controls' }, [
E('div', { 'class': 'agh-toolbar agh-log-toggles' }, [
reverseLabel, timeLabel, verboseLabel, verboseMessage, message
]),
E('div', { 'class': 'agh-log-actions' }, [
E('div', { 'class': 'agh-toolbar' }, [clearButton, downloadButton]),
logFileField
])
]);
// Keep the form field inside its map, outside the theme's section layout.
settingsFields.hidden = true;
settingsCard.insertBefore(logControls, settingsFields);
settingsCard.insertBefore(logArea, logControls);
var page = E('div', {}, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('view/adguardhome/compact.css') }),
sharedStatus.node,
settingsNode
]);
if (initialLog.error)
message.textContent = _('Unable to read runtime log');
else if (!enabled)
message.textContent = _('Please add log path in config to enable log');
renderLog();
poll.add(function() {
if (clearButton.disabled)
return Promise.resolve();
var requestGeneration = generation;
return callRuntimeLog(offset, identity).then(function(result) {
if (requestGeneration !== generation)
return;
enabled = result.enabled;
if (!enabled) {
message.textContent = _('Please add log path in config to enable log');
return;
}
message.textContent = '';
if (result.reset || result.offset < offset)
content = '';
offset = result.offset;
identity = result.identity || '';
content = (content + decodeLog(result)).slice(-logLimit);
renderLog();
}).catch(function() {
message.textContent = _('Status unavailable');
});
}, 3);
return page;
});
}
});
@@ -1,232 +0,0 @@
'use strict';
'require view';
'require rpc';
'require view/adguardhome/status as status';
var callManualConfig = rpc.declare({
object: 'luci.adguardhome',
method: 'getManualConfig',
expect: { '': {} },
reject: true
});
var callSetManualConfig = rpc.declare({
object: 'luci.adguardhome',
method: 'setManualConfig',
params: [ 'content' ],
expect: { '': {} },
reject: true
});
var callChangeWebPassword = rpc.declare({
object: 'luci.adguardhome',
method: 'changeWebPassword',
params: [ 'hash' ],
expect: { '': {} },
reject: true
});
var codeMirrorLoader;
var bcryptLoader;
function loadBcrypt() {
if (window.TwinBcrypt)
return Promise.resolve(window.TwinBcrypt);
if (!bcryptLoader) {
bcryptLoader = new Promise(function(resolve, reject) {
var script = document.createElement('script');
script.src = L.resource('twin-bcrypt.min.js');
script.onload = function() {
if (window.TwinBcrypt)
resolve(window.TwinBcrypt);
else
reject(new Error('bcrypt unavailable'));
};
script.onerror = reject;
document.head.appendChild(script);
}).catch(function(error) {
bcryptLoader = null;
throw error;
});
}
return bcryptLoader;
}
function loadCodeMirror() {
if (window.CodeMirror)
return Promise.resolve(window.CodeMirror);
if (!codeMirrorLoader) {
codeMirrorLoader = new Promise(function(resolve, reject) {
var stylesheet = document.createElement('link');
stylesheet.rel = 'stylesheet';
stylesheet.href = L.resource('codemirror/lib/codemirror.css');
document.head.appendChild(stylesheet);
var theme = document.createElement('link');
theme.rel = 'stylesheet';
theme.href = L.resource('codemirror/theme/dracula.css');
document.head.appendChild(theme);
var script = document.createElement('script');
script.src = L.resource('codemirror/lib/codemirror.js');
script.onload = function() {
var yaml = document.createElement('script');
yaml.src = L.resource('codemirror/mode/yaml/yaml.js');
yaml.onload = function() { resolve(window.CodeMirror); };
yaml.onerror = reject;
document.head.appendChild(yaml);
};
script.onerror = reject;
document.head.appendChild(script);
});
}
return codeMirrorLoader;
}
return view.extend({
handleSave: null,
handleSaveApply: null,
handleReset: null,
load: function() {
return callManualConfig().catch(function() {
return { content: '', template: '', source: 'error' };
});
},
render: function(initial) {
var sharedStatus = status.render();
var config = initial;
var showingTemplate = false;
var editor;
var message = E('span', { 'style': 'margin-left:0.8em;' }, '');
var configArea = E('textarea', {
'class': 'cbi-input-textarea',
'rows': 20,
'wrap': 'off',
'style': 'width:100%;box-sizing:border-box;font-family:monospace;'
});
var editorWrap = E('div', { 'class': 'agh-editor-wrap' }, configArea);
var templateButton = E('button', {
'class': 'btn cbi-button', 'type': 'button'
}, _('Use template'));
var reloadButton = E('button', {
'class': 'btn cbi-button', 'type': 'button', 'style': 'margin-left:0.6em;'
}, _('Reload Config'));
var saveButton = E('button', {
'class': 'btn cbi-button cbi-button-apply', 'type': 'button', 'style': 'margin-left:0.6em;'
}, _('Save & Apply'));
var passwordInput = E('input', {
'class': 'cbi-input-password', 'type': 'password',
'id': 'agh-web-password', 'autocomplete': 'new-password', 'spellcheck': 'false'
});
var passwordButton = E('button', {
'class': 'btn cbi-button cbi-button-apply', 'type': 'button'
}, _('Change password'));
var passwordMessage = E('span', { 'style': 'margin-left:0.8em;' }, '');
var passwordPanel = E('div', { 'class': 'agh-toolbar agh-password-controls' }, [
E('label', { 'for': 'agh-web-password' }, _('Change browser management password')),
passwordInput, passwordButton, passwordMessage
]);
function showConfig() {
var content = showingTemplate ? config.template : config.content;
if (editor)
editor.setValue(content);
else
configArea.value = content;
templateButton.disabled = !config.template;
}
templateButton.addEventListener('click', function() {
showingTemplate = true;
message.textContent = '';
showConfig();
});
reloadButton.addEventListener('click', function() {
reloadButton.disabled = true;
message.textContent = '';
callManualConfig().then(function(result) {
config = result;
showingTemplate = false;
showConfig();
}).catch(function() {
message.textContent = _('Unable to read config');
}).then(function() {
reloadButton.disabled = false;
});
});
saveButton.addEventListener('click', function() {
var content = editor ? editor.getValue() : configArea.value;
saveButton.disabled = true;
message.textContent = _('Saving...');
callSetManualConfig(content).then(function(result) {
if (!result.saved)
throw new Error(result.log || result.reason || 'save failed');
config.content = content;
config.source = 'current';
showingTemplate = false;
message.textContent = _('Saved');
}).catch(function(error) {
message.textContent = error.message || _('Save failed');
}).then(function() {
saveButton.disabled = false;
});
});
passwordButton.addEventListener('click', function() {
var password = passwordInput.value;
if (!password) {
passwordMessage.textContent = _('New password') + ' ' + _('is empty');
return;
}
passwordButton.disabled = true;
passwordMessage.textContent = _('Changing password...');
loadBcrypt().then(function(bcrypt) {
var hash = bcrypt.hashSync(password);
password = null;
passwordInput.value = '';
return callChangeWebPassword(hash);
}).then(function(result) {
if (!result.changed)
throw new Error(result.reason || 'change failed');
passwordMessage.textContent = _('Password changed');
}).catch(function() {
passwordMessage.textContent = _('Password change failed');
}).then(function() {
passwordButton.disabled = false;
});
});
showConfig();
var page = E('div', {}, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('view/adguardhome/compact.css') }),
sharedStatus.node,
E('div', { 'class': 'cbi-section agh-compact-card agh-manual-card' }, [
editorWrap,
E('div', { 'class': 'agh-manual-toolbar' }, [
passwordPanel,
E('div', { 'class': 'agh-toolbar agh-config-actions' }, [message, templateButton, reloadButton, saveButton])
])
])
]);
loadCodeMirror().then(function(CodeMirror) {
editor = CodeMirror.fromTextArea(configArea, {
mode: 'text/yaml',
lineNumbers: true,
theme: 'dracula',
lineWrapping: true,
matchBrackets: true
});
editor.getWrapperElement().style.fontSize = '15px';
editor.getWrapperElement().style.lineHeight = '1.5';
// Twenty lines at 1.5em, plus CodeMirror's vertical content padding.
editor.setSize(null, 'calc(30em + 8px)');
editor.refresh();
showConfig();
}).catch(function() {
message.textContent = _('Unable to load editor');
});
return page;
}
});
@@ -1,204 +0,0 @@
'use strict';
'require baseclass';
'require poll';
'require rpc';
var callCoreInfo = rpc.declare({
object: 'luci.adguardhome',
method: 'getCoreInfo',
expect: { '': {} },
reject: true
});
var callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: [ 'name', 'verbose' ],
expect: { '': {} },
reject: true
});
var callRedirectStatus = rpc.declare({
object: 'luci.adguardhome',
method: 'getRedirectStatus',
expect: { '': {} },
reject: true
});
function modeLabel(mode) {
return {
none: _('Do not handle DNS'),
'dnsmasq-upstream': _('Run as dnsmasq upstream server'),
redirect: _('Forward port 53 requests through the firewall'),
exchange: _('Use port 53 instead of dnsmasq')
}[mode] || _('Status unavailable');
}
function coreIsRunning(services, binpath) {
var instances = services.AdGuardHome && services.AdGuardHome.instances;
if (!instances)
return false;
return Object.keys(instances).some(function(name) {
var instance = instances[name];
return instance.running === true && Array.isArray(instance.command) && instance.command[0] === binpath;
});
}
return baseclass.extend({
render: function() {
var badgeStyle = 'display:inline-flex;align-items:center;padding:0.4em 0.8em;' +
'border:1px solid;border-radius:999px;font-weight:600;line-height:1.4;';
var coreBadge = E('span', { 'style': badgeStyle }, _('Collecting data...'));
var serviceBadge = E('span', { 'style': badgeStyle }, _('Collecting data...'));
var redirectBadge = E('span', { 'style': badgeStyle }, _('Collecting data...'));
var node = E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'display:flex;flex-wrap:wrap;gap:0.6em;padding:0.6em;' }, [
coreBadge, serviceBadge, redirectBadge
])
]);
function setBadge(badge, label, state) {
badge.textContent = label;
badge.style.backgroundColor = state === 'pending' ? '#fef3c7' : state === true ? '#dcfce7' : state === false ? '#fee2e2' : '#e5e7eb';
badge.style.color = state === 'pending' ? '#92400e' : state === true ? '#14532d' : state === false ? '#7f1d1d' : '#374151';
badge.style.borderColor = state === 'pending' ? '#fcd34d' : state === true ? '#86efac' : state === false ? '#fca5a5' : '#d1d5db';
}
function refreshCore() {
return callCoreInfo().then(function(info) {
if (!info.core_exists)
setBadge(coreBadge, _('Core') + ': ' + _('no core'), false);
else if (!info.version)
setBadge(coreBadge, _('Core') + ': ' + _('core error'), false);
else if (!info.config_exists)
setBadge(coreBadge, _('Core') + ': ' + info.version + ' (' + _('no config') + ')', false);
else
setBadge(coreBadge, _('Core') + ': ' + info.version, true);
}).catch(function() {
setBadge(coreBadge, _('Core') + ': ' + _('Status unavailable'), null);
});
}
var storageKey = 'adguardhome.pendingSwitch';
var pending = null;
var generation = 0;
var stable = 0;
var nextRefresh = 0;
var inFlight = null;
try {
var saved = JSON.parse(sessionStorage.getItem(storageKey));
if (saved && typeof saved.mode === 'string' && typeof saved.enabled === 'boolean' &&
typeof saved.started === 'number')
pending = saved;
} catch (e) {}
function remember() {
try {
if (pending)
sessionStorage.setItem(storageKey, JSON.stringify(pending));
else
sessionStorage.removeItem(storageKey);
} catch (e) {}
}
function showPending(running) {
var label = Date.now() - pending.started >= 30000
? _('Switch not completed; check the log') : _('Switching');
if (typeof running === 'boolean')
setBadge(serviceBadge, running ? _('RUNNING') : _('NOT RUNNING'), running);
else
setBadge(serviceBadge, _('Checking service…'), 'pending');
setBadge(redirectBadge, label, 'pending');
}
function beginSwitch(target) {
generation++;
stable = 0;
nextRefresh = 0;
pending = { mode: target.mode, enabled: target.enabled, started: Date.now() };
remember();
showPending();
}
function cancelSwitch() {
generation++;
pending = null;
stable = 0;
nextRefresh = 0;
remember();
return refreshStatus();
}
function matches(info, running, target) {
return !info.busy && info.mode === target.mode && info.enabled === target.enabled &&
info.applied_enabled === target.enabled && running === target.enabled &&
(!target.enabled || info.applied_mode === target.mode) &&
info.redirected === (target.enabled && target.mode !== 'none');
}
function refreshStatus() {
if (inFlight)
return inFlight;
var currentGeneration = generation;
var requestedAt = Date.now();
inFlight = Promise.all([callRedirectStatus(), callServiceList('AdGuardHome', true)]).then(function(results) {
if (currentGeneration !== generation)
return;
var info = results[0];
var running = coreIsRunning(results[1], info.binpath);
// Recover switching feedback after navigation or an apply from another page.
if (!pending && (info.busy || info.mode !== info.applied_mode || info.enabled !== info.applied_enabled))
beginSwitch(info);
if (pending) {
stable = matches(info, running, pending) ? stable + 1 : 0;
// Two consecutive samples avoid declaring a short-lived process healthy.
if (stable < 2) {
showPending(running);
return;
}
pending = null;
remember();
}
setBadge(serviceBadge, running ? _('RUNNING') : _('NOT RUNNING'), running);
var active = info.redirected && running && info.mode === info.applied_mode;
var label = modeLabel(info.mode);
if (!info.enabled)
label += ' (' + _('Disabled') + ')';
else if (info.mode !== 'none' && !active)
label += ' (' + _('Not redirect') + ')';
setBadge(redirectBadge, label,
!info.enabled || info.mode === 'none' ? null : active);
}).catch(function() {
if (currentGeneration !== generation)
return;
stable = 0;
if (pending) {
showPending();
setBadge(serviceBadge, _('Status unavailable'), null);
setBadge(redirectBadge, _('Unable to read switch status') + ' (' + modeLabel(pending.mode) + ')', null);
} else {
setBadge(serviceBadge, _('Status unavailable'), null);
setBadge(redirectBadge, _('Status unavailable'), null);
}
}).then(function() {
nextRefresh = requestedAt + (pending ? 1000 : 3000);
inFlight = null;
});
return inFlight;
}
if (pending)
showPending();
refreshCore();
refreshStatus();
poll.add(function() {
if (Date.now() >= nextRefresh)
return refreshStatus();
}, 1);
return { node: node, refresh: function() {
return Promise.all([refreshCore(), refreshStatus()]);
}, beginSwitch: beginSwitch, cancelSwitch: cancelSwitch };
}
});
-34
View File
@@ -1,34 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$SDK_DIR"
mkdir -p package/${PKG_DIR_NAME}
rsync -a --delete "$GITHUB_WORKSPACE/" "package/${PKG_DIR_NAME}/" \
--exclude '.git*' --exclude '.local' --exclude '_sdk' --exclude 'dist'
./scripts/feeds update -a
./scripts/feeds install luci-base tar
touch .config
sed -i -E '/^(# )?CONFIG_(ALL|ALL_KMODS|ALL_NONSHARED|LUCI_LANG_zh_Hans|PACKAGE_TAR_XZ|PACKAGE_luci-app-adguardhome-tk|PACKAGE_luci-i18n-adguardhome-tk-zh-cn)(=| is not set)/d' .config
printf '%s\n' \
'CONFIG_ALL=n' \
'CONFIG_ALL_KMODS=n' \
'CONFIG_ALL_NONSHARED=n' \
'CONFIG_LUCI_LANG_zh_Hans=y' \
'# CONFIG_PACKAGE_TAR_XZ is not set' \
'CONFIG_PACKAGE_luci-app-adguardhome-tk=y' >> .config
make defconfig
if ! grep -Eq '^CONFIG_PACKAGE_luci-app-adguardhome-tk=(y|m)$' .config; then
echo '::error::luci-app-adguardhome-tk was not enabled after make defconfig'
grep -n 'luci-app-adguardhome-tk' .config || true
sed -n '/^[[:space:]]*config PACKAGE_luci-app-adguardhome-tk$/,/^[[:space:]]*help/p' tmp/.config-package.in
exit 1
fi
grep -Eq '^CONFIG_PACKAGE_luci-i18n-adguardhome-tk-zh-cn=(y|m)$' .config || {
echo '::error::Simplified Chinese translation package was not enabled'
exit 1
}
make package/compile -j"$(nproc)" V=sc
@@ -1,11 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
mkdir -p "$GITHUB_WORKSPACE/dist"
for package in "$PKG_DIR_NAME" luci-i18n-adguardhome-tk-zh-cn; do
mapfile -t packages < <(find "$SDK_DIR/bin/packages" -type f -name "${package}-*.apk")
if (( ${#packages[@]} == 0 )); then
echo "::error::Missing APK: ${package}"
exit 1
fi
cp "${packages[@]}" "$GITHUB_WORKSPACE/dist/"
done
-86
View File
@@ -1,86 +0,0 @@
#!/bin/sh
set -e
REPO="w9315273/luci-app-adguardhome"
LANGUAGE=${1:-en}
case "$LANGUAGE" in
en|zh-cn) ;;
*) echo "Usage: sh install.sh [en|zh-cn]"; exit 1 ;;
esac
[ "$#" -le 1 ] || { echo "Usage: sh install.sh [en|zh-cn]"; exit 1; }
echo "Getting latest version..."
RELEASE=$(uclient-fetch -qO- "https://api.github.com/repos/${REPO}/releases/latest" 2>/dev/null)
TAG=$(printf '%s\n' "$RELEASE" | sed -n 's/.*"tag_name":[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
[ -n "$TAG" ] || { echo "ERROR: Failed to get latest version"; exit 1; }
case "$TAG" in
*-*) ;;
*) echo "ERROR: Unexpected tag format: $TAG"; exit 1 ;;
esac
VERSION=${TAG#v}
VERSION=${VERSION%-*}-r${VERSION##*-}
PACKAGE=luci-app-adguardhome
I18N_PACKAGE=luci-i18n-adguardhome-zh-cn
if printf '%s\n' "$RELEASE" | grep -Eq '"name"[[:space:]]*:[[:space:]]*"luci-app-adguardhome-tk-[^"]*\.apk"'; then
PACKAGE=luci-app-adguardhome-tk
I18N_PACKAGE=luci-i18n-adguardhome-tk-zh-cn
fi
URL="https://github.com/${REPO}/releases/download/${TAG}/${PACKAGE}-${VERSION}.apk"
echo "Version: $VERSION"
echo "Downloading..."
work=$(mktemp -d /tmp/agh-install.XXXXXX)
trap 'rm -rf "$work"' EXIT
uclient-fetch -qO "$work/main.apk" "$URL"
set -- "$work/main.apk"
I18N="${I18N_PACKAGE}-${VERSION}.apk"
has_i18n=0
if [ "$LANGUAGE" = zh-cn ] && printf '%s\n' "$RELEASE" | grep -Eq "\"name\"[[:space:]]*:[[:space:]]*\"${I18N_PACKAGE}-[^\"]*\\.apk\""; then
uclient-fetch -qO "$work/zh-cn.apk" "https://github.com/${REPO}/releases/download/${TAG}/${I18N}"
set -- "$@" "$work/zh-cn.apk"
has_i18n=1
fi
if [ "$LANGUAGE" = zh-cn ] && [ "$PACKAGE" = luci-app-adguardhome-tk ] && [ "$has_i18n" != 1 ]; then
echo "ERROR: The release is missing the Chinese package"
exit 1
fi
legacy_packages=
if [ "$PACKAGE" = luci-app-adguardhome-tk ]; then
installed=$(apk info)
for old in luci-app-adguardhome luci-i18n-adguardhome-zh-cn; do
if printf '%s\n' "$installed" | grep -qx "$old"; then
if [ "$old" != luci-i18n-adguardhome-zh-cn ] || [ "$LANGUAGE" = zh-cn ]; then
set -- "$@" "$old"
fi
legacy_packages="$legacy_packages $old"
fi
done
fi
echo "Installing..."
apk add --allow-untrusted "$@"
if [ -n "$legacy_packages" ]; then
apk del $legacy_packages
fi
if [ "$LANGUAGE" = en ]; then
installed=$(apk info)
for translation in luci-i18n-adguardhome-zh-cn luci-i18n-adguardhome-tk-zh-cn; do
if printf '%s\n' "$installed" | grep -qx "$translation"; then
apk del "$translation"
fi
done
fi
echo "Refreshing LuCI..."
rm -f /tmp/luci-indexcache /tmp/luci-indexcache.* /tmp/luci-modulecache/* 2>/dev/null
/etc/init.d/rpcd reload >/dev/null 2>&1
echo "Done."
@@ -1,10 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$SDK_DIR"
if [[ ! -x scripts/feeds ]]; then
runuser -u buildbot -- bash ./setup.sh
fi
mkdir -p dl
chown -R buildbot:buildbot dl