diff --git a/luci-app-adguardhome/.github/workflows/build-apk.yaml b/luci-app-adguardhome/.github/workflows/build-apk.yaml new file mode 100644 index 00000000..64b83777 --- /dev/null +++ b/luci-app-adguardhome/.github/workflows/build-apk.yaml @@ -0,0 +1,70 @@ +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 diff --git a/luci-app-adguardhome/.gitignore b/luci-app-adguardhome/.gitignore index 10d5c0e3..05dfba47 100644 --- a/luci-app-adguardhome/.gitignore +++ b/luci-app-adguardhome/.gitignore @@ -1,9 +1,2 @@ .DS_Store - -# Local debug artifacts from playwright-cli UI verification -adg-*.png -m-*.png -bs-*.png -*.png -!screenshots/*.png -.playwright-cli/ +.local \ No newline at end of file diff --git a/luci-app-adguardhome/Makefile b/luci-app-adguardhome/Makefile index a3d2ffc4..4eb97f6f 100644 --- a/luci-app-adguardhome/Makefile +++ b/luci-app-adguardhome/Makefile @@ -1,79 +1,109 @@ -# Copyright (C) 2018-2024 OpenWrt luci-app-adguardhome contributors # -# This is free software, licensed under the Apache License, Version 2.0 . +# Copyright (C) 2025 w9315273 # include $(TOPDIR)/rules.mk -PKG_NAME:=luci-app-adguardhome -PKG_MAINTAINER:=kenzok8 -PKG_VERSION:=1.0 -PKG_RELEASE:=9 +PKG_NAME:=luci-app-adguardhome-tk +PKG_VERSION:=2.6.0 +PKG_RELEASE:=3 -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 +PKG_MAINTAINER:=w9315273 +PKG_BUILD_DEPENDS:=luci-base/host -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. +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 endef -PKG_CONFIG_DEPENDS:= CONFIG_PACKAGE_$(PKG_NAME)_INCLUDE_binary +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 -define Package/luci-app-adguardhome/conffiles -/usr/share/AdGuardHome/links.txt +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 /etc/config/AdGuardHome endef -define Package/luci-app-adguardhome/postinst +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 #!/bin/sh - # 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 +[ -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 + exit 0 endef -define Package/luci-app-adguardhome/prerm +define Package/luci-app-adguardhome-tk/prerm #!/bin/sh if [ -z "$${IPKG_INSTROOT}" ]; then - /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 + /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 fi + exit 0 endef -include $(TOPDIR)/feeds/luci/luci.mk +$(eval $(call BuildPackage,luci-app-adguardhome-tk)) -# call BuildPackage - OpenWrt buildroot signature -$(eval $(call BuildPackage,luci-app-adguardhome)) +$(eval $(call BuildPackage,luci-i18n-adguardhome-tk-zh-cn)) diff --git a/luci-app-adguardhome/README.md b/luci-app-adguardhome/README.md deleted file mode 100644 index ecdba15b..00000000 --- a/luci-app-adguardhome/README.md +++ /dev/null @@ -1,81 +0,0 @@ -# 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 diff --git a/luci-app-adguardhome/docs/README.en.md b/luci-app-adguardhome/docs/README.en.md new file mode 100644 index 00000000..1dc3727a --- /dev/null +++ b/luci-app-adguardhome/docs/README.en.md @@ -0,0 +1,35 @@ +# 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) diff --git a/luci-app-adguardhome/docs/README.md b/luci-app-adguardhome/docs/README.md new file mode 100644 index 00000000..ae6ad614 --- /dev/null +++ b/luci-app-adguardhome/docs/README.md @@ -0,0 +1,35 @@ +# 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) diff --git a/luci-app-adguardhome/docs/screenshots/base-en.png b/luci-app-adguardhome/docs/screenshots/base-en.png new file mode 100644 index 00000000..6e67b273 Binary files /dev/null and b/luci-app-adguardhome/docs/screenshots/base-en.png differ diff --git a/luci-app-adguardhome/docs/screenshots/base-zh-cn.png b/luci-app-adguardhome/docs/screenshots/base-zh-cn.png new file mode 100644 index 00000000..221291f6 Binary files /dev/null and b/luci-app-adguardhome/docs/screenshots/base-zh-cn.png differ diff --git a/luci-app-adguardhome/docs/screenshots/log-en.png b/luci-app-adguardhome/docs/screenshots/log-en.png new file mode 100644 index 00000000..554ae409 Binary files /dev/null and b/luci-app-adguardhome/docs/screenshots/log-en.png differ diff --git a/luci-app-adguardhome/docs/screenshots/log-zh-cn.png b/luci-app-adguardhome/docs/screenshots/log-zh-cn.png new file mode 100644 index 00000000..c16c2aa7 Binary files /dev/null and b/luci-app-adguardhome/docs/screenshots/log-zh-cn.png differ diff --git a/luci-app-adguardhome/docs/screenshots/manual-en.png b/luci-app-adguardhome/docs/screenshots/manual-en.png new file mode 100644 index 00000000..c05878cc Binary files /dev/null and b/luci-app-adguardhome/docs/screenshots/manual-en.png differ diff --git a/luci-app-adguardhome/docs/screenshots/manual-zh-cn.png b/luci-app-adguardhome/docs/screenshots/manual-zh-cn.png new file mode 100644 index 00000000..870cafef Binary files /dev/null and b/luci-app-adguardhome/docs/screenshots/manual-zh-cn.png differ diff --git a/luci-app-adguardhome/htdocs/luci-static/resources/adguardhome/adg.css b/luci-app-adguardhome/htdocs/luci-static/resources/adguardhome/adg.css deleted file mode 100644 index a6ee5020..00000000 --- a/luci-app-adguardhome/htdocs/luci-static/resources/adguardhome/adg.css +++ /dev/null @@ -1,360 +0,0 @@ -/* 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; - } -} diff --git a/luci-app-adguardhome/htdocs/luci-static/resources/view/luci-app-adguardhome/index.js b/luci-app-adguardhome/htdocs/luci-static/resources/view/luci-app-adguardhome/index.js deleted file mode 100644 index e483ee42..00000000 --- a/luci-app-adguardhome/htdocs/luci-static/resources/view/luci-app-adguardhome/index.js +++ /dev/null @@ -1,14 +0,0 @@ -// 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…')); - } -}); diff --git a/luci-app-adguardhome/luasrc/controller/AdGuardHome.lua b/luci-app-adguardhome/luasrc/controller/AdGuardHome.lua deleted file mode 100644 index 658f34c0..00000000 --- a/luci-app-adguardhome/luasrc/controller/AdGuardHome.lua +++ /dev/null @@ -1,274 +0,0 @@ -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 diff --git a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua deleted file mode 100644 index 961201a2..00000000 --- a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/base.lua +++ /dev/null @@ -1,244 +0,0 @@ -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 diff --git a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/log.lua b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/log.lua deleted file mode 100644 index 5d18a88d..00000000 --- a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/log.lua +++ /dev/null @@ -1,16 +0,0 @@ -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 diff --git a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/manual.lua b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/manual.lua deleted file mode 100644 index d7bf82e9..00000000 --- a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/manual.lua +++ /dev/null @@ -1,121 +0,0 @@ -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 diff --git a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/overview.lua b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/overview.lua deleted file mode 100644 index 4ece44fd..00000000 --- a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/overview.lua +++ /dev/null @@ -1,16 +0,0 @@ -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 diff --git a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/tools.lua b/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/tools.lua deleted file mode 100644 index 55a520b0..00000000 --- a/luci-app-adguardhome/luasrc/model/cbi/AdGuardHome/tools.lua +++ /dev/null @@ -1,113 +0,0 @@ -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 diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm deleted file mode 100644 index 251374b0..00000000 Binary files a/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_check.htm and /dev/null differ diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm deleted file mode 100644 index 1c274fbf..00000000 --- a/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_chpass.htm +++ /dev/null @@ -1,51 +0,0 @@ -<%+AdGuardHome/head%> -<%+cbi/valueheader%> - -
- 0, "data-choices", { self.keylist, self.vallist }) - %> /> - <% if self.password then %><% end %> - - -
- -<%+cbi/valuefooter%> diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_status.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_status.htm deleted file mode 100644 index b0e8bb36..00000000 --- a/luci-app-adguardhome/luasrc/view/AdGuardHome/AdGuardHome_status.htm +++ /dev/null @@ -1,145 +0,0 @@ - - -
-
-
- <%:Collecting data...%> -
-
-
diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/enable_switch.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/enable_switch.htm deleted file mode 100644 index 96ff2b4d..00000000 --- a/luci-app-adguardhome/luasrc/view/AdGuardHome/enable_switch.htm +++ /dev/null @@ -1,169 +0,0 @@ -<%+AdGuardHome/head%> -<%+cbi/valueheader%> -<% - local value = self:cfgvalue(section) - local checked = value == "1" - local field_id = cbid -%> - -
- /> - - > -
- -<%+cbi/valuefooter%> diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/head.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/head.htm deleted file mode 100644 index 583a5ae5..00000000 --- a/luci-app-adguardhome/luasrc/view/AdGuardHome/head.htm +++ /dev/null @@ -1,83 +0,0 @@ - diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm deleted file mode 100644 index e38d8015..00000000 --- a/luci-app-adguardhome/luasrc/view/AdGuardHome/log.htm +++ /dev/null @@ -1,186 +0,0 @@ -<%+cbi/valueheader%> -<%+AdGuardHome/head%> -<%+AdGuardHome/log_empty%> - -
- - <%if self.timereplace then%> - - <%end%> -
-

-
- - -
- -<%+cbi/valuefooter%> diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/log_empty.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/log_empty.htm deleted file mode 100644 index a818b741..00000000 --- a/luci-app-adguardhome/luasrc/view/AdGuardHome/log_empty.htm +++ /dev/null @@ -1,27 +0,0 @@ -<% -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 %> -
-
!
-
-
<%:运行日志未配置%>
-
<%:请在「基础设置 → 路径与日志 → 运行日志」填入日志路径(如 /tmp/AdGuardHome.log),或填 syslog 写入系统日志。%>
- <%:去设置%> -
-
- -<% end %> diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/overview.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/overview.htm deleted file mode 100644 index a70c4333..00000000 --- a/luci-app-adguardhome/luasrc/view/AdGuardHome/overview.htm +++ /dev/null @@ -1,286 +0,0 @@ -<%+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 -%> - - - -
- -
- -
-
-
-
<%:服务状态%>
-
- <%:正在检测…%> -
-
- -
-
 
-
- - -
-
<%:重定向模式%>
-
- <%= redirect_text %> -
-
- <%:配置「基础设置 → 5553 重定向」%> -
-
- - -
-
<%:核心版本%>
-
- <% if has_bin then %> - <%= version %> - <% else %> - <%:未安装%> - <% end %> -
-
- <% if not has_config then %> - <%:配置文件缺失%> - <% elseif not has_bin then %> - <%:点击「基础设置 → 更新核心版本」下载%> - <% else %> - <%= binpath %> - <% end %> -
-
-
- - -
- - <%:打开 Web 控制台%> :<%=httpport%> - - <%:基础设置%> - <%:运维%> - <%:查看日志%> -
-
- - diff --git a/luci-app-adguardhome/luasrc/view/AdGuardHome/yamleditor.htm b/luci-app-adguardhome/luasrc/view/AdGuardHome/yamleditor.htm deleted file mode 100644 index 2ecdbd0a..00000000 --- a/luci-app-adguardhome/luasrc/view/AdGuardHome/yamleditor.htm +++ /dev/null @@ -1,84 +0,0 @@ -<%+AdGuardHome/head%> -<%+cbi/valueheader%> - - - - - - - - - - -<%fs=require"nixio.fs"%> -<%if fs.access("/tmp/AdGuardHometmpconfig.yaml") then%> - -<%end%> - -<%+cbi/valuefooter%> \ No newline at end of file diff --git a/luci-app-adguardhome/po/zh-cn/AdGuardHome.po b/luci-app-adguardhome/po/zh-cn/AdGuardHome.po new file mode 100644 index 00000000..c465821f --- /dev/null +++ b/luci-app-adguardhome/po/zh-cn/AdGuardHome.po @@ -0,0 +1,231 @@ +#/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 "无法应用详细日志设置" diff --git a/luci-app-adguardhome/root/etc/AdGuardHome.yaml b/luci-app-adguardhome/root/etc/AdGuardHome.yaml deleted file mode 100644 index 4bef352f..00000000 --- a/luci-app-adguardhome/root/etc/AdGuardHome.yaml +++ /dev/null @@ -1,226 +0,0 @@ -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 diff --git a/luci-app-adguardhome/root/etc/config/AdGuardHome b/luci-app-adguardhome/root/etc/config/AdGuardHome index 827b291d..fb562bb1 100644 --- a/luci-app-adguardhome/root/etc/config/AdGuardHome +++ b/luci-app-adguardhome/root/etc/config/AdGuardHome @@ -1,17 +1,11 @@ + config AdGuardHome 'AdGuardHome' option enabled '0' option httpport '3000' option redirect 'none' option configpath '/etc/AdGuardHome.yaml' - option workdir '/etc/AdGuardHome' + option workdir '/tmp/AdGuardHome' option logfile '/tmp/AdGuardHome.log' option verbose '0' - option binpath '/usr/bin/AdGuardHome' - option upxflag '' - option user '' - option group '' - option advanced_settings '0' - option gc '0' - option maxprocs '0' - option memlimit '0' - + option binpath '/usr/bin/AdGuardHome/AdGuardHome' + list wan_ifname 'eth0' diff --git a/luci-app-adguardhome/root/etc/init.d/AdGuardHome b/luci-app-adguardhome/root/etc/init.d/AdGuardHome index 651ecd90..dfa233b6 100755 --- a/luci-app-adguardhome/root/etc/init.d/AdGuardHome +++ b/luci-app-adguardhome/root/etc/init.d/AdGuardHome @@ -6,685 +6,648 @@ START=95 STOP=01 CONFIGURATION=AdGuardHome -CRON_FILE=/etc/crontabs/root +NFT_RULES_FILE="/var/etc/adguardhome.nft" +NFT_RULES_TPL="/usr/share/AdGuardHome/adguardhome.nft.tpl" +NFT_TABLE="adguardhome" -extra_command "do_redirect" "0 or 1" -extra_command "testbackup" "backup or restore" -extra_command "test_crontab" -extra_command "force_reload" -extra_command "isrunning" +EXTRA_COMMANDS="do_redirect force_reload isrunning" +EXTRA_HELP=" do_redirect 0 or 1\ + force_reload + isrunning" -set_forward_dnsmasq() -{ - local PORT="$1" - local addr="127.0.0.1#$PORT" - local OLD_SERVER="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)" - echo "$OLD_SERVER" | grep -q "^$addr" - [ $? -eq 0 ] && return +get_wan_return_line() { + local wan + wan="$(uci -q get AdGuardHome.AdGuardHome.wan_ifname 2>/dev/null)" - uci delete dhcp.@dnsmasq[0].server 2>/dev/null - uci add_list dhcp.@dnsmasq[0].server="$addr" - for server in $OLD_SERVER; do - [ "$server" = "$addr" ] && continue - done - uci delete dhcp.@dnsmasq[0].resolvfile 2>/dev/null - uci set dhcp.@dnsmasq[0].noresolv=1 - uci commit dhcp - /etc/init.d/dnsmasq restart + [ -z "$wan" ] && { echo ""; return 0; } + + local wan_set + wan_set="\"$(printf '%s' "$wan" | sed 's/ /\", \"/g')\"" + echo "iifname { $wan_set } return" } -stop_forward_dnsmasq() -{ - local OLD_PORT="$1" - local addr="127.0.0.1#$OLD_PORT" - local OLD_SERVER="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)" - echo "$OLD_SERVER" | grep -q "^$addr" - [ $? -ne 0 ] && return +set_nft_redirect() { + local port="$1" + case "$port" in + *[!0-9]*|"") return 1 ;; + esac + [ ! -f "$NFT_RULES_TPL" ] && return 1 - uci del_list dhcp.@dnsmasq[0].server="$addr" 2>/dev/null - local addrlist="$(uci get dhcp.@dnsmasq[0].server 2>/dev/null)" - if [ -z "$addrlist" ]; then - uci set dhcp.@dnsmasq[0].resolvfile=/tmp/resolv.conf.d/resolv.conf.auto 2>/dev/null - uci delete dhcp.@dnsmasq[0].noresolv 2>/dev/null - fi - uci commit dhcp - /etc/init.d/dnsmasq restart + local return_line + return_line="$(get_wan_return_line)" + + awk -v port="$port" -v wan_line="$return_line" '{ + gsub("__AGH_PORT__", port) + gsub("__AGH_WAN_RETURN_LINE__", wan_line) + print + }' "$NFT_RULES_TPL" > "$NFT_RULES_FILE" || return 1 + + [ ! -s "$NFT_RULES_FILE" ] && return 1 + + nft delete table inet "$NFT_TABLE" 2>/dev/null + nft -f "$NFT_RULES_FILE" 2>/dev/null } -set_iptable() -{ - local ipv6_server="$1" - local tcp_server="$2" +clear_nft_redirect() { + uci -q delete firewall.adguardhome_nft_include + uci -q commit firewall - uci -q batch <<-EOF >/dev/null 2>&1 - delete firewall.AdGuardHome - set firewall.AdGuardHome=include - set firewall.AdGuardHome.type=script - set firewall.AdGuardHome.path=/usr/share/AdGuardHome/firewall.start - set firewall.AdGuardHome.reload=1 - commit firewall - EOF - - local IPS="$(ip -4 addr show | grep inet | grep -v ':' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" - for IP in $IPS; do - [ -z "$IP" ] && continue - iptables -t nat -I PREROUTING -p udp -d "$IP" --dport 53 -j REDIRECT --to-ports "$AdGuardHome_PORT" >/dev/null 2>&1 - [ "$tcp_server" = "1" ] && iptables -t nat -I PREROUTING -p tcp -d "$IP" --dport 53 -j REDIRECT --to-ports "$AdGuardHome_PORT" >/dev/null 2>&1 - done - - [ "$ipv6_server" = "0" ] && return - - IPS="$(ip -6 addr show | grep inet6 | grep -v 'fe80::' | grep -v '::1' | grep 'Global' | awk '{print $2}' | cut -d'/' -f1)" - for IP in $IPS; do - [ -z "$IP" ] && continue - ip6tables -t nat -I PREROUTING -p udp -d "$IP" --dport 53 -j REDIRECT --to-ports "$AdGuardHome_PORT" >/dev/null 2>&1 - [ "$tcp_server" = "1" ] && ip6tables -t nat -I PREROUTING -p tcp -d "$IP" --dport 53 -j REDIRECT --to-ports "$AdGuardHome_PORT" >/dev/null 2>&1 - done + [ -f "$NFT_RULES_FILE" ] && rm -f "$NFT_RULES_FILE" + nft delete table inet "$NFT_TABLE" 2>/dev/null } -clear_iptable() -{ - uci -q batch <<-EOF >/dev/null 2>&1 - delete firewall.AdGuardHome - commit firewall - EOF +wait_for_pid_exit() { + local pid="$1" attempts=0 + case "$pid" in ''|*[!0-9]*) return 0 ;; esac + while kill -0 "$pid" 2>/dev/null; do + [ "$attempts" -lt 15 ] || return 1 + sleep 1 + attempts=$((attempts + 1)) + done +} - local OLD_PORT="$1" - local ipv6_server="$2" - local IPS="$(ip -4 addr show | grep inet | grep -v ':' | grep -v '127.0.0.1' | awk '{print $2}' | cut -d'/' -f1)" - for IP in $IPS; do - [ -z "$IP" ] && continue - iptables -t nat -D PREROUTING -p udp -d "$IP" --dport 53 -j REDIRECT --to-ports "$OLD_PORT" >/dev/null 2>&1 - iptables -t nat -D PREROUTING -p tcp -d "$IP" --dport 53 -j REDIRECT --to-ports "$OLD_PORT" >/dev/null 2>&1 - done +wait_for_dns_port_release() { + local port="$1" attempts=0 tables="" table busy + for table in /proc/net/tcp /proc/net/tcp6 /proc/net/udp /proc/net/udp6; do + [ -r "$table" ] && tables="$tables $table" + done + [ -n "$tables" ] || return 1 + port="$(printf '%04X' "$port")" || return 1 + while :; do + busy="$(awk -v port="$port" ' + FNR > 1 { + split($2, address, ":") + if (toupper(address[2]) == port && + ((FILENAME ~ /tcp/ && $4 == "0A") || + (FILENAME ~ /udp/ && $4 == "07"))) found = 1 + } + END { print found ? 1 : 0 } + ' $tables)" || return 1 + [ "$busy" = "0" ] && return 0 + [ "$attempts" -lt 15 ] || return 1 + sleep 1 + attempts=$((attempts + 1)) + done +} - [ "$ipv6_server" = "0" ] && return - echo "warn ip6tables nat mod is needed" - IPS="$(ip -6 addr show | grep inet6 | grep -v 'fe80::' | grep -v '::1' | grep 'Global' | awk '{print $2}' | cut -d'/' -f1)" - for IP in $IPS; do - [ -z "$IP" ] && continue - ip6tables -t nat -D PREROUTING -p udp -d "$IP" --dport 53 -j REDIRECT --to-ports "$OLD_PORT" >/dev/null 2>&1 - ip6tables -t nat -D PREROUTING -p tcp -d "$IP" --dport 53 -j REDIRECT --to-ports "$OLD_PORT" >/dev/null 2>&1 - done +restart_dnsmasq() { + local released_port="$1" service pids pid + service="$(ubus call service list '{"name":"dnsmasq"}' 2>/dev/null)" || return 1 + pids="$(printf '%s' "$service" | jsonfilter -e '@.dnsmasq.instances.*.pid')" + /etc/init.d/dnsmasq stop 8>&- 1000>&- || return 1 + for pid in $pids; do + if ! wait_for_pid_exit "$pid"; then + logger -t AdGuardHome 'dnsmasq did not stop; refusing to reuse its old jail.' + return 1 + fi + done + if [ -n "$released_port" ] && ! wait_for_dns_port_release "$released_port"; then + logger -t AdGuardHome "DNS port $released_port is still occupied after stopping dnsmasq." + /etc/init.d/dnsmasq start 8>&- 1000>&- + return 1 + fi + /etc/init.d/dnsmasq start 8>&- 1000>&- || return 1 + if [ -n "$released_port" ] && ! wait_for_dns_port_release "$released_port"; then + logger -t AdGuardHome "DNS port $released_port was occupied again after starting dnsmasq." + return 1 + fi + return 0 +} + +save_dnsmasq_upstream_config() { + local backup="AdGuardHome.dnsmasq_backup" option value server + [ "$(uci -q get "$backup.ready")" = "1" ] && return 0 + + uci -q delete "$backup" + uci set "$backup=dnsmasq_backup" || return 1 + value="$(uci -q get dhcp.@dnsmasq[0].server)" + for server in $value; do + uci add_list "$backup.server=$server" || return 1 + done + for option in resolvfile noresolv; do + if value="$(uci -q get "dhcp.@dnsmasq[0].$option")"; then + uci set "$backup.$option=$value" || return 1 + fi + done + uci set "$backup.ready=1" || return 1 + if ! uci commit AdGuardHome; then + uci -q revert "$backup" + return 1 + fi +} + +apply_dnsmasq_config() { + [ "${AGH_DEFER_DNSMASQ_RESTART:-0}" = "1" ] && return 0 + restart_dnsmasq "$@" +} + +is_scoped_dnsmasq_server() { + local domains="${1%/*}" + case "$1" in + /*) case "$domains/" in */\#/*) return 1 ;; esac; return 0 ;; + esac + return 1 +} + +set_forward_dnsmasq() ( + set -f + local addr="127.0.0.1#$1" servers desired server + + if [ "$(uci -q get AdGuardHome.dnsmasq_backup.ready)" != "1" ] && + [ "$2" = "dnsmasq-upstream" ] && [ "$3" = "1" ]; then + stop_forward_dnsmasq "$4" || return 1 + fi + save_dnsmasq_upstream_config || return 1 + + servers="$(uci -q get dhcp.@dnsmasq[0].server)" + desired="$addr" + for server in $servers; do + is_scoped_dnsmasq_server "$server" && desired="$desired $server" + done + if [ "$servers" = "$desired" ] && [ "$(uci -q get dhcp.@dnsmasq[0].noresolv)" = "1" ]; then + return 0 + fi + + uci -q delete dhcp.@dnsmasq[0].server + for server in $desired; do + uci add_list "dhcp.@dnsmasq[0].server=$server" || return 1 + done + uci set dhcp.@dnsmasq[0].noresolv=1 || return 1 + uci commit dhcp || return 1 + apply_dnsmasq_config >/dev/null 2>&1 +) + +stop_forward_dnsmasq() ( + set -f + local backup="AdGuardHome.dnsmasq_backup" option value server + local addr="127.0.0.1#$1" found=0 has_general=0 + + if [ "$(uci -q get "$backup.ready")" = "1" ]; then + uci -q delete dhcp.@dnsmasq[0].server + value="$(uci -q get "$backup.server")" + for server in $value; do + uci add_list "dhcp.@dnsmasq[0].server=$server" || return 1 + done + for option in resolvfile noresolv; do + if value="$(uci -q get "$backup.$option")"; then + uci set "dhcp.@dnsmasq[0].$option=$value" || return 1 + else + uci -q delete "dhcp.@dnsmasq[0].$option" + fi + done + uci commit dhcp || return 1 + apply_dnsmasq_config >/dev/null 2>&1 || return 1 + [ "${AGH_DEFER_DNSMASQ_RESTART:-0}" = "1" ] && return 0 + uci -q delete "$backup" + uci commit AdGuardHome + return $? + fi + + value="$(uci -q get dhcp.@dnsmasq[0].server)" + for server in $value; do + if [ "$server" = "$addr" ]; then + found=1 + elif ! is_scoped_dnsmasq_server "$server"; then + has_general=1 + fi + done + [ "$found" = "1" ] || return 0 + uci -q del_list "dhcp.@dnsmasq[0].server=$addr" || return 1 + if [ "$has_general" = "0" ]; then + if ! uci -q get dhcp.@dnsmasq[0].resolvfile >/dev/null; then + value="/tmp/resolv.conf.d/resolv.conf.auto" + [ -f "$value" ] || value="/tmp/resolv.conf.auto" + uci set "dhcp.@dnsmasq[0].resolvfile=$value" || return 1 + fi + uci -q delete dhcp.@dnsmasq[0].noresolv + fi + logger -t AdGuardHome 'No DNS snapshot from previous version; removed old upstream, original resolver settings are unavailable.' + uci commit dhcp || return 1 + apply_dnsmasq_config >/dev/null 2>&1 +) + +stop_core() { + local service instance pid + service="$(ubus call service list '{"name":"AdGuardHome"}' 2>/dev/null)" || return 1 + instance="$(printf '%s' "$service" | jsonfilter -e '@.AdGuardHome.instances.instance1')" + [ -n "$instance" ] || return 0 + pid="$(printf '%s' "$instance" | jsonfilter -e '@.pid')" + ubus call service delete '{"name":"AdGuardHome","instance":"instance1"}' >/dev/null || return 1 + if ! wait_for_pid_exit "$pid"; then + logger -t AdGuardHome 'Core did not stop; DNS port settings were not changed.' + return 1 + fi +} + +agh_reload() { + [ "${AGH_REDIRECT_RELOAD:-1}" = "1" ] || return 0 + if [ "${AGH_DEFER_DNSMASQ_RESTART:-0}" = "1" ]; then + AGH_RELOAD_PENDING=1 + return 0 + fi + ubus call service event '{"type":"config.change","data":{"package":"AdGuardHome"}}' >/dev/null 2>&1 +} + +rm_port53() { + local configpath + config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml" + + local AdGuardHome_PORT dnsmasq_port + AdGuardHome_PORT=$(config_editor "dns.port" "" "$configpath" "1") + dnsmasq_port=$(uci get dhcp.@dnsmasq[0].port 2>/dev/null) + [ -z "$dnsmasq_port" ] && dnsmasq_port="53" + + if [ "$dnsmasq_port" = "$AdGuardHome_PORT" ]; then + [ "$dnsmasq_port" = "53" ] && dnsmasq_port="1745" + elif [ "$dnsmasq_port" = "53" ]; then + return + fi + + stop_core || return 1 + config_editor "dns.port" "$dnsmasq_port" "$configpath" + [ "$(config_editor "dns.port" "" "$configpath" "1")" = "$dnsmasq_port" ] || return 1 + uci set dhcp.@dnsmasq[0].port="53" || return 1 + uci commit dhcp || return 1 + apply_dnsmasq_config "$dnsmasq_port" || return 1 + agh_reload +} + +use_port53() { + local configpath + config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml" + + local AdGuardHome_PORT dnsmasq_port + AdGuardHome_PORT=$(config_editor "dns.port" "" "$configpath" "1") + dnsmasq_port=$(uci get dhcp.@dnsmasq[0].port 2>/dev/null) + [ -z "$dnsmasq_port" ] && dnsmasq_port="53" + + if [ "$dnsmasq_port" = "$AdGuardHome_PORT" ]; then + [ "$dnsmasq_port" = "53" ] && AdGuardHome_PORT="1745" + elif [ "$AdGuardHome_PORT" = "53" ]; then + return + fi + + stop_core || return 1 + config_editor "dns.port" "53" "$configpath" + [ "$(config_editor "dns.port" "" "$configpath" "1")" = "53" ] || return 1 + uci set dhcp.@dnsmasq[0].port="$AdGuardHome_PORT" || return 1 + uci commit dhcp || return 1 + apply_dnsmasq_config "$dnsmasq_port" || return 1 + agh_reload +} + +mark_redirect_flag() { + local enabled="$1" redirect="$2" agh_port="$3" + + local configpath + config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml" + + local flag=0 + + if [ "$enabled" = "1" ] && [ "$redirect" != "none" ]; then + flag=1 + + if [ "$redirect" = "redirect" ]; then + nft list table inet "$NFT_TABLE" >/dev/null 2>&1 || flag=0 + + elif [ "$redirect" = "dnsmasq-upstream" ]; then + local s + s="$(uci -q get dhcp.@dnsmasq[0].server | tr '\n' ' ')" + printf '%s\n' "$s" | grep -q -E "(^|[[:space:]])127\.0\.0\.1#${agh_port}([[:space:]]|$)" || flag=0 + + elif [ "$redirect" = "exchange" ]; then + local cfgp dport + cfgp="$(config_editor "dns.port" "" "$configpath" "1")" + dport="$(uci -q get dhcp.@dnsmasq[0].port)" + if [ "$cfgp" != "53" ] || [ "$dport" = "53" ] ; then + flag=0 + fi + fi + fi + + echo -n "$flag" > /var/run/AdGredir +} + +finish_dnsmasq_config() { + case "$old_redirect:$redirect" in + *dnsmasq-upstream*|*exchange*) + if [ "$dnsmasq_before" != "$(uci -q export dhcp)" ] || [ "$previous_flag" != "1" ] || [ "$enabled" = "0" ]; then + local released_port="" + if [ "$enabled" = "1" ] && [ "$redirect" = "exchange" ]; then + released_port=53 + elif [ "$dnsmasq_previous_port" != "$(uci -q get dhcp.@dnsmasq[0].port)" ]; then + released_port="$dnsmasq_previous_port" + fi + restart_dnsmasq "$released_port" || return 1 + fi + ;; + esac + if { [ "$enabled" = "0" ] || [ "$redirect" != "dnsmasq-upstream" ]; } && + [ "$(uci -q get AdGuardHome.dnsmasq_backup.ready)" = "1" ]; then + uci -q delete AdGuardHome.dnsmasq_backup + uci commit AdGuardHome || return 1 + fi + return 0 +} + +_do_redirect() ( + exec 8>/var/lock/AdGuardHome-redirect.lock || return 1 + flock -w 10 8 || return 1 + local AGH_DEFER_DNSMASQ_RESTART=1 AGH_RELOAD_PENDING=0 + local dnsmasq_before dnsmasq_previous_port previous_flag + dnsmasq_before="$(uci -q export dhcp)" || return 1 + dnsmasq_previous_port="$(uci -q get dhcp.@dnsmasq[0].port)" + dnsmasq_previous_port="${dnsmasq_previous_port:-53}" + previous_flag="$(cat /var/run/AdGredir 2>/dev/null)" + echo -n "0" > /var/run/AdGredir + local section="$CONFIGURATION" + args="" + ipv6_server=1 + tcp_server=0 + enabled="$1" + AGH_REDIRECT_RELOAD="${2:-$enabled}" + + config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml" + AdGuardHome_PORT="$(config_editor "dns.port" "" "$configpath" "1")" + [ -z "$AdGuardHome_PORT" ] && AdGuardHome_PORT="0" + + config_get redirect "$section" redirect "none" + config_get old_redirect "$section" old_redirect "none" + config_get old_port "$section" old_port "0" + config_get old_enabled "$section" old_enabled "0" + + uci get dhcp.@dnsmasq[0].port >/dev/null 2>&1 || uci set dhcp.@dnsmasq[0].port="53" >/dev/null 2>&1 + uci commit dhcp + + if [ "$old_enabled" = "1" ] && [ "$old_redirect" = "exchange" ]; then + AdGuardHome_PORT="$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)" + fi + + if [ "$old_redirect" != "$redirect" ] || [ "$old_port" != "$AdGuardHome_PORT" ] || { [ "$old_enabled" = "1" ] && [ "$enabled" = "0" ]; }; then + if [ "$old_redirect" != "none" ]; then + if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then + clear_nft_redirect + elif [ "$old_redirect" = "dnsmasq-upstream" ]; then + if [ "$old_enabled" = "1" ] || [ "$(uci -q get AdGuardHome.dnsmasq_backup.ready)" = "1" ]; then + if ! stop_forward_dnsmasq "$old_port"; then + echo -n "0" > /var/run/AdGredir + return 1 + fi + fi + old_enabled=0 + elif [ "$old_redirect" = "exchange" ]; then + if ! rm_port53; then + echo -n "0" > /var/run/AdGredir + return 1 + fi + fi + fi + elif [ "$old_enabled" = "1" ] && [ "$enabled" = "1" ]; then + if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then + clear_nft_redirect + fi + fi + + uci delete AdGuardHome.@AdGuardHome[0].old_redirect 2>/dev/null + uci delete AdGuardHome.@AdGuardHome[0].old_port 2>/dev/null + uci delete AdGuardHome.@AdGuardHome[0].old_enabled 2>/dev/null + uci add_list AdGuardHome.@AdGuardHome[0].old_redirect="$redirect" 2>/dev/null + uci add_list AdGuardHome.@AdGuardHome[0].old_port="$AdGuardHome_PORT" 2>/dev/null + uci add_list AdGuardHome.@AdGuardHome[0].old_enabled="$enabled" 2>/dev/null + uci commit AdGuardHome + + if [ "$enabled" = "0" ] || [ "$AdGuardHome_PORT" = "0" ]; then + finish_dnsmasq_config || return 1 + echo -n "0" > /var/run/AdGredir + return 1 + fi + + if [ "$redirect" = "redirect" ]; then + set_nft_redirect "$AdGuardHome_PORT" + elif [ "$redirect" = "dnsmasq-upstream" ]; then + if ! set_forward_dnsmasq "$AdGuardHome_PORT" "$old_redirect" "$old_enabled" "$old_port"; then + echo -n "0" > /var/run/AdGredir + return 1 + fi + elif [ "$redirect" = "exchange" ] && [ "$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)" = "53" ]; then + if ! use_port53; then + echo -n "0" > /var/run/AdGredir + return 1 + fi + fi + + finish_dnsmasq_config || return 1 + mark_redirect_flag "$enabled" "$redirect" "$AdGuardHome_PORT" || return 1 + AGH_DEFER_DNSMASQ_RESTART=0 + [ "$AGH_RELOAD_PENDING" = "0" ] || agh_reload + return 0 +) + +do_redirect() { + config_load "${CONFIGURATION}" + _do_redirect "$1" } service_triggers() { - procd_add_reload_trigger "$CONFIGURATION" - [ "$(uci get AdGuardHome.AdGuardHome.redirect 2>/dev/null)" = "redirect" ] && procd_add_reload_trigger firewall + procd_add_reload_trigger "$CONFIGURATION" } -isrunning(){ - config_load "${CONFIGURATION}" - _isrunning - local r=$? - if [ "$r" = "0" ]; then - echo "running" - elif [ "$r" = "1" ]; then - echo "not run" - else - echo "no bin" - fi - return $r +isrunning() { + config_load "${CONFIGURATION}" + _isrunning + local r=$? + [ "$r" = "0" ] && echo "running" || ([ "$r" = "1" ] && echo "not run" || echo "no bin") + return $r } -_isrunning(){ - config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome" - [ ! -f "$binpath" ] && return 2 - # 用进程名匹配,避免 pgrep -f 自匹配 - pgrep "${binpath##*/}" >/dev/null 2>&1 && return 0 - return 1 +_isrunning() { + config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome/AdGuardHome" + [ ! -f "$binpath" ] && return 2 + pgrep -f "$(basename "$binpath")" >/dev/null 2>&1 && return 0 + return 1 } -force_reload(){ - config_load "${CONFIGURATION}" - if _isrunning; then - procd_send_signal "$CONFIGURATION" - else - start - fi +force_reload() { + config_load "${CONFIGURATION}" + _isrunning && procd_send_signal "$CONFIGURATION" || start } -get_tz() -{ - SET_TZ="" - - [ -e "/etc/localtime" ] && return - - for tzfile in /etc/TZ /var/etc/TZ; do - [ ! -e "$tzfile" ] && continue - tz="$(cat "$tzfile" 2>/dev/null)" - done - - [ -z "$tz" ] && return - SET_TZ="$tz" +get_tz() { + SET_TZ="" + [ -e "/etc/localtime" ] && return + for tzfile in /etc/TZ /var/etc/TZ; do + [ -e "$tzfile" ] && tz="$(cat "$tzfile" 2>/dev/null)" && break + done + [ -n "$tz" ] && SET_TZ=$tz } -rm_port53() -{ - local AdGuardHome_PORT - AdGuardHome_PORT="$(config_editor "dns.port" "" "$configpath" "1")" - local dnsmasq_port="$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)" - [ -z "$dnsmasq_port" ] && dnsmasq_port="53" - - if [ "$dnsmasq_port" = "$AdGuardHome_PORT" ]; then - [ "$dnsmasq_port" = "53" ] && dnsmasq_port="1745" - elif [ "$dnsmasq_port" = "53" ]; then - return - fi - - config_editor "dns.port" "$dnsmasq_port" "$configpath" - uci set dhcp.@dnsmasq[0].port="53" - uci commit dhcp - - config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome" - local binname="${binpath##*/}" - pkill -TERM "$binname" 2>/dev/null - sleep 2 - pkill -KILL "$binname" 2>/dev/null - /etc/init.d/dnsmasq restart +get_filesystem() { + echo "$1" | awk ' + BEGIN{ + while (("mount"| getline ret) > 0) { + split(ret,d); fs[d[3]]=d[5]; + m=index(d[1],":"); + if (m==0) { pt[d[3]]=d[1] } else { pt[d[3]]=substr(d[1],m+1) } + } + } + { + split($0,d,"/"); + if ("/" in fs) { result1=fs["/"]; } + if ("/" in pt) { result2=pt["/"]; } + for (i=2;i<=length(d);i++) { + p[i]=p[i-1]"/"d[i]; + if (p[i] in fs) { result1=fs[p[i]]; result2=pt[p[i]]; } + } + if (result2 in fs) { result=fs[result2] } else { result=result1 } + print(result); + }' } -use_port53() -{ - local AdGuardHome_PORT - AdGuardHome_PORT="$(config_editor "dns.port" "" "$configpath" "1")" - local dnsmasq_port="$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)" - [ -z "$dnsmasq_port" ] && dnsmasq_port="53" - # AGH 端口未配置或无法读取时,使用默认 1745 作为 dnsmasq 的新端口 - [ -z "$AdGuardHome_PORT" ] && AdGuardHome_PORT="1745" - - if [ "$dnsmasq_port" = "$AdGuardHome_PORT" ]; then - [ "$dnsmasq_port" = "53" ] && AdGuardHome_PORT="1745" - elif [ "$AdGuardHome_PORT" = "53" ]; then - return - fi - - config_editor "dns.port" "53" "$configpath" - uci set dhcp.@dnsmasq[0].port="$AdGuardHome_PORT" - uci commit dhcp - /etc/init.d/dnsmasq reload -} - -do_redirect() -{ - config_load "${CONFIGURATION}" - _do_redirect "$1" -} - -_do_redirect() -{ - local section="$CONFIGURATION" - ipv6_server=1 - tcp_server=0 - local enabled="$1" - - if [ "$enabled" = "1" ]; then - echo -n "1" > /var/run/AdGredir - else - echo -n "0" > /var/run/AdGredir - fi - - config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml" - AdGuardHome_PORT="$(config_editor "dns.port" "" "$configpath" "1")" - - [ ! -s "$configpath" ] && cp -f /usr/share/AdGuardHome/AdGuardHome_template.yaml "$configpath" - [ -z "$AdGuardHome_PORT" ] && AdGuardHome_PORT="0" - - config_get redirect "$section" redirect "none" - config_get old_redirect "$section" old_redirect "none" - config_get old_port "$section" old_port "0" - config_get old_enabled "$section" old_enabled "0" - - uci get dhcp.@dnsmasq[0].port >/dev/null 2>&1 || uci set dhcp.@dnsmasq[0].port="53" >/dev/null 2>&1 - - if [ "$old_enabled" = "1" ] && [ "$old_redirect" = "exchange" ]; then - AdGuardHome_PORT="$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)" - fi - - if [ "$old_redirect" != "$redirect" ] || [ "$old_port" != "$AdGuardHome_PORT" ] || { [ "$old_enabled" = "1" ] && [ "$enabled" = "0" ]; }; then - if [ "$old_redirect" != "none" ]; then - if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then - clear_iptable "$old_port" "$ipv6_server" - elif [ "$old_redirect" = "dnsmasq-upstream" ]; then - stop_forward_dnsmasq "$old_port" - elif [ "$old_redirect" = "exchange" ]; then - rm_port53 - fi - fi - elif [ "$old_enabled" = "1" ] && [ "$enabled" = "1" ]; then - if [ "$old_redirect" = "redirect" ] && [ "$old_port" != "0" ]; then - clear_iptable "$old_port" "$ipv6_server" - fi - fi - - uci delete AdGuardHome.@AdGuardHome[0].old_redirect 2>/dev/null - uci delete AdGuardHome.@AdGuardHome[0].old_port 2>/dev/null - uci delete AdGuardHome.@AdGuardHome[0].old_enabled 2>/dev/null - uci add_list AdGuardHome.@AdGuardHome[0].old_redirect="$redirect" 2>/dev/null - uci add_list AdGuardHome.@AdGuardHome[0].old_port="$AdGuardHome_PORT" 2>/dev/null - uci add_list AdGuardHome.@AdGuardHome[0].old_enabled="$enabled" 2>/dev/null - uci commit AdGuardHome - - [ "$enabled" = "0" ] && return 1 - [ "$AdGuardHome_PORT" = "0" ] && return 1 - - if [ "$redirect" = "redirect" ]; then - set_iptable "$ipv6_server" "$tcp_server" - elif [ "$redirect" = "dnsmasq-upstream" ]; then - set_forward_dnsmasq "$AdGuardHome_PORT" - elif [ "$redirect" = "exchange" ] && [ "$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)" = "53" ]; then - use_port53 - fi -} - -get_filesystem() -{ - local path="$1" - local mntpt="" - local fstype="" - - while IFS= read -r line; do - case "$line" in - /dev/root|/dev/ubi0*|/dev/mtdblock*) - continue - ;; - esac - local mp="$(echo "$line" | awk '{print $2}')" - local fs="$(echo "$line" | awk '{print $3}')" - case "$path" in - "$mp"*) - fstype="$fs" - mntpt="$mp" - break - ;; - esac - done < /proc/mounts - - if [ -z "$mntpt" ]; then - mntpt="/" - fstype="$(awk '$2=="/" {print $3}' /proc/mounts)" - fi - - echo "$fstype" -} - -config_editor() -{ - local yaml="$1" - local value="$2" - local file="$3" - local ro="$4" - - if [ "$ro" = "1" ]; then - awk -v yaml="$yaml" ' - BEGIN { - split(yaml, part, "\\.") - i = 1 - l = length(part) - s = "" - } - { - if (match($0, s "" part[i] ":")) { - if (i == l) { - sub(/^[^:]+: */, "") - print - exit - } - s = s "[- ]{2}" - i++ - } - }' "$file" - else - # 安全写入:用 sed 只改匹配行,不改其他内容 - # yaml 路径如 "dns.port" → 匹配带缩进的 "port:" 行 - local last_key - last_key="${yaml##*.}" - if grep -qE "^[[:space:]]*${last_key}:" "$file" 2>/dev/null; then - sed -i "s|^\\([[:space:]]*${last_key}:\\).*|\\1 ${value}|" "$file" - else - echo "${last_key}: ${value}" >> "$file" - fi - fi +config_editor() { + awk -v yaml="$1" -v value="$2" -v file="$3" -v ro="$4" ' + BEGIN{split(yaml,part,"\.");s="";i=1;l=length(part);} + { + if (match($0,s""part[i]":")) { + if (i==l) { + split($0,t,": "); + if (ro==""){ + system("sed -i '\''"FNR"c\\" t[1] ": " value "'\'' " file); + } else { + print(t[2]); + } + exit; + } + s=s"[- ]{2}"; + i++; + } + }' "$3" } boot_service() { - rm -f /var/run/AdGserverdis >/dev/null 2>&1 - config_load "${CONFIGURATION}" - config_get waitonboot $CONFIGURATION waitonboot "0" - config_get_bool enabled $CONFIGURATION enabled 0 - config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome" - - [ -f "$binpath" ] && start_service - - if [ "$enabled" = "1" ] && [ "$waitonboot" = "1" ]; then - procd_open_instance - procd_set_param command "/usr/share/AdGuardHome/waitnet.sh" - procd_close_instance - echo "AdGuardHome: waiting for network..." - fi -} - -testbackup(){ - config_load "${CONFIGURATION}" - if [ "$1" = "backup" ]; then - backup - elif [ "$1" = "restore" ]; then - restore - fi -} - -restore() -{ - config_get workdir $CONFIGURATION workdir "/etc/AdGuardHome" - config_get backupwdpath $CONFIGURATION backupwdpath "/etc/AdGuardHome" - [ -d "$backupwdpath/data" ] && cp -u -r -f "$backupwdpath/data" "$workdir/" -} - -backup() { - config_get backupwdpath $CONFIGURATION backupwdpath "/etc/AdGuardHome" - mkdir -p "$backupwdpath/data" - config_get workdir $CONFIGURATION workdir "/etc/AdGuardHome" - config_get backupfile $CONFIGURATION backupfile "" - - for one in $backupfile; do - while : ; do - local cpret - if [ -d "$backupwdpath/data/$one" ]; then - cpret="$(cp -u -r -f "$workdir/data/$one" "$backupwdpath/data" 2>&1)" - else - cpret="$(cp -u -r -f "$workdir/data/$one" "$backupwdpath/data/$one" 2>&1)" - fi - echo "$cpret" - echo "$cpret" | grep -q "no space left on device" - if [ $? -eq 0 ]; then - echo "disk full, deleting querylog and retrying..." - del_querylog && continue - rm -rf "$backupwdpath/data/filters" - rm -rf "$workdir/data/filters" && continue - echo "backup failed" - fi - break - done - done + rm -f /var/run/AdGserverdis 2>/dev/null + config_load "${CONFIGURATION}" + config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome/AdGuardHome" + [ -f "$binpath" ] && start_service } start_service() { - rm -f /var/run/AdGserverdis >/dev/null 2>&1 - config_load "${CONFIGURATION}" + stop_core || return 1 + rm -f /var/run/AdGserverdis 2>/dev/null + config_load "${CONFIGURATION}" + config_get hashpass $CONFIGURATION hashpass "" + config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml" + if [ -n "$hashpass" ]; then + config_editor "users.password" "$hashpass" "$configpath" + uci set $CONFIGURATION.$CONFIGURATION.hashpass="" + uci commit $CONFIGURATION + fi + local enabled + config_get_bool enabled $CONFIGURATION enabled 0 + if [ "$enabled" = "0" ]; then + _do_redirect 0 + return + fi + config_get workdir $CONFIGURATION workdir "/usr/bin/AdGuardHome" - config_get hashpass $CONFIGURATION hashpass "" - config_get configpath $CONFIGURATION configpath "/etc/AdGuardHome.yaml" + mkdir -p "$workdir/data" + local cwdfs + cwdfs=$(get_filesystem "$workdir") + echo "workdir is a $cwdfs filesystem" + if [ "$cwdfs" = "jffs2" ]; then + echo "fs error ln db to tmp $workdir $cwdfs" + logger "AdGuardHome" "warning db redirect to tmp" + touch "$workdir/data/stats.db" + if [ ! -L "$workdir/data/stats.db" ]; then + mv -f "$workdir/data/stats.db" /tmp/stats.db 2>/dev/null + ln -s /tmp/stats.db "$workdir/data/stats.db" 2>/dev/null + fi + touch "$workdir/data/sessions.db" + if [ ! -L "$workdir/data/sessions.db" ]; then + mv -f "$workdir/data/sessions.db" /tmp/sessions.db 2>/dev/null + ln -s /tmp/sessions.db "$workdir/data/sessions.db" 2>/dev/null + fi + fi + local ADDITIONAL_ARGS="" + config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome/AdGuardHome" - if [ -n "$hashpass" ]; then - config_editor "users.password" "$hashpass" "$configpath" - uci set "$CONFIGURATION"."$CONFIGURATION".hashpass="" - uci commit AdGuardHome - fi + mkdir -p "${binpath%/*}" + ADDITIONAL_ARGS="$ADDITIONAL_ARGS -c $configpath" + ADDITIONAL_ARGS="$ADDITIONAL_ARGS -w $workdir" + config_get httpport $CONFIGURATION httpport 3000 + ADDITIONAL_ARGS="$ADDITIONAL_ARGS -p $httpport" - local enabled - config_get_bool enabled $CONFIGURATION enabled 0 + printf '%s\n' "$binpath" "$configpath" > /lib/upgrade/keep.d/luci-app-adguardhome - do_crontab + config_get logfile $CONFIGURATION logfile "" + if [ -n "$logfile" ]; then + ADDITIONAL_ARGS="$ADDITIONAL_ARGS -l $logfile" + fi - if [ "$enabled" = "0" ]; then - _do_redirect 0 - local binpid - config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome" - # 用进程名匹配,避免 pgrep -f 自匹配 - binpid=$(pgrep "${binpath##*/}" 2>/dev/null | head -1) - if [ -n "$binpid" ]; then - kill -TERM "$binpid" 2>/dev/null - sleep 1 - kill -KILL "$binpid" 2>/dev/null - fi - return - fi + if [ ! -f "$binpath" ]; then + _do_redirect 0 + /usr/share/AdGuardHome/update_core.sh >/dev/null 2>&1 & + exit 0 + fi - config_get workdir $CONFIGURATION workdir "/etc/AdGuardHome" - config_get backupfile $CONFIGURATION backupfile "" - mkdir -p "$workdir/data" + config_get_bool verbose $CONFIGURATION verbose 0 + if [ "$verbose" -eq 1 ]; then + ADDITIONAL_ARGS="$ADDITIONAL_ARGS -v" + fi - if [ -n "$backupfile" ] && [ ! -d "$workdir/data" ]; then - restore - fi + if [ "$(uci -q get bypass.@global[0].global_server)" ] && [ "$(uci -q get bypass.@global[0].adguardhome)" = "1" ] && [ "$(uci -q get dhcp.@dnsmasq[0].port)" = "53" ]; then + uci -q set AdGuardHome.AdGuardHome.redirect='exchange' + uci -q commit AdGuardHome + config_load "${CONFIGURATION}" + fi - local cwdfs - cwdfs="$(get_filesystem "$workdir")" - echo "AdGuardHome: workdir is on $cwdfs filesystem" - if [ "$cwdfs" = "jffs2" ]; then - logger -t AdGuardHome -p daemon.warn "jffs2 detected, redirecting db to tmp" - mkdir -p /tmp/adg_tmp - for db in stats.db sessions.db; do - touch "$workdir/data/$db" - [ ! -L "$workdir/data/$db" ] && mv -f "$workdir/data/$db" "/tmp/adg_tmp/$db" 2>/dev/null - [ ! -e "/tmp/adg_tmp/$db" ] && touch "/tmp/adg_tmp/$db" - [ -f "/tmp/adg_tmp/$db" ] && ln -sf "/tmp/adg_tmp/$db" "$workdir/data/$db" - done - fi + if [ -f "$configpath" ]; then + if ! _do_redirect 1 0; then + [ "$(config_editor "dns.port" "" "$configpath" "1")" = "0" ] || return 1 + fi + else + _do_redirect 0 0 + fi - local ADDITIONAL_ARGS="" - config_get binpath $CONFIGURATION binpath "/usr/bin/AdGuardHome" - mkdir -p "${binpath%/*}" + procd_open_instance + get_tz + if [ -n "$SET_TZ" ]; then + procd_set_param env TZ="$SET_TZ" + fi + procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5} + procd_set_param limits core="unlimited" nofile="65535 65535" + procd_set_param stderr 1 + procd_set_param command "$binpath" $ADDITIONAL_ARGS + procd_set_param file "$configpath" "/etc/hosts" "/etc/config/AdGuardHome" + procd_close_instance + if [ ! -f "$configpath" ]; then + config_get "redirect" "AdGuardHome" "redirect" "none" + if [ "$redirect" != "none" ]; then + procd_open_instance "waitconfig" + procd_set_param command "/usr/share/AdGuardHome/watchconfig.sh" + procd_close_instance + echo "no config start watching" + fi + fi + echo "AdGuardHome service enabled" + echo "luci enable switch=$enabled" - ADDITIONAL_ARGS="-c $configpath -w $workdir" - config_get httpport $CONFIGURATION httpport 3000 - ADDITIONAL_ARGS="$ADDITIONAL_ARGS -p $httpport" - - config_get upprotect $CONFIGURATION upprotect "" - if [ -n "$upprotect" ]; then - { - for item in $upprotect; do - echo "$item" | sed 's/\\n/\n/g' - done - } > /lib/upgrade/keep.d/luci-app-adguardhome - fi - - config_get logfile $CONFIGURATION logfile "" - [ -n "$logfile" ] && ADDITIONAL_ARGS="$ADDITIONAL_ARGS -l $logfile" - - if [ ! -f "$binpath" ]; then - _do_redirect 0 - /usr/share/AdGuardHome/update_core.sh >/tmp/AdGuardHome_update.log 2>&1 & - exit 0 - fi - - config_get_bool verbose $CONFIGURATION verbose 0 - [ "$verbose" -eq 1 ] && ADDITIONAL_ARGS="$ADDITIONAL_ARGS -v" - - config_get user $CONFIGURATION user "" - config_get group $CONFIGURATION group "" - config_get gc $CONFIGURATION gc "0" - config_get maxprocs $CONFIGURATION maxprocs "0" - config_get memlimit $CONFIGURATION memlimit "0" - - procd_open_instance - get_tz - [ -n "$SET_TZ" ] && procd_set_param env TZ="$SET_TZ" - procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5} - procd_set_param limits core="unlimited" nofile="65535 65535" - procd_set_param stderr 1 - [ -n "$user" ] && procd_set_param user "$user" - [ -n "$group" ] && procd_set_param group "$group" - [ "$gc" != "0" ] && procd_set_param env GOGC="$gc" - [ "$maxprocs" != "0" ] && procd_set_param env GOMAXPROCS="$maxprocs" - [ -n "$memlimit" ] && [ "$memlimit" != "0" ] && procd_set_param env GOMEMLIMIT="${memlimit}MiB" - - # DynamicList 写入 uci 是 `list jail_mount '...'` 格式 - config_list_foreach $CONFIGURATION jail_mount procd_add_jail_mount - config_list_foreach $CONFIGURATION jail_mount_rw procd_add_jail_mount_rw - - # 兼容旧的 newline-separated `option jail_mount '...'` 格式(迁移期) - local jm jmrw - jm="$(uci -q get AdGuardHome.AdGuardHome.jail_mount 2>/dev/null)" - case "$jm" in - *$'\n'*) - printf '%s\n' "$jm" | while IFS= read -r mp; do - [ -n "$mp" ] && procd_add_jail_mount "$mp" - done - ;; - esac - jmrw="$(uci -q get AdGuardHome.AdGuardHome.jail_mount_rw 2>/dev/null)" - case "$jmrw" in - *$'\n'*) - printf '%s\n' "$jmrw" | while IFS= read -r mp; do - [ -n "$mp" ] && procd_add_jail_mount_rw "$mp" - done - ;; - esac - - procd_set_param command $binpath $ADDITIONAL_ARGS - procd_set_param file "$configpath" "/etc/hosts" "/etc/config/AdGuardHome" - procd_close_instance - - if [ -f "$configpath" ]; then - _do_redirect 1 - else - _do_redirect 0 - config_get redirect "$CONFIGURATION" redirect "none" - if [ "$redirect" != "none" ]; then - procd_open_instance - procd_set_param command "/usr/share/AdGuardHome/watchconfig.sh" - procd_close_instance - echo "AdGuardHome: no config found, watching for config file..." - fi - fi - - echo "AdGuardHome service enabled (switch=$enabled)" - - (sleep 10 && \ - pgrep "${binpath##*/}" >/dev/null 2>&1 || { \ - logger -t AdGuardHome "no process found after 10s, canceling redirect"; \ - _do_redirect 0; \ - }) & - - local bypass_global="$(uci get bypass.@global[0].global_server 2>/dev/null)" - local bypass_adg="$(uci get bypass.@global[0].adguardhome 2>/dev/null)" - local dnsmasq_port="$(uci get dhcp.@dnsmasq[0].port 2>/dev/null)" - if [ -n "$bypass_global" ] && [ "$bypass_adg" = "1" ] && [ "$dnsmasq_port" = "53" ]; then - uci -q set AdGuardHome.AdGuardHome.redirect='exchange' - uci commit AdGuardHome - do_redirect 1 - fi } -reload_service() -{ - rm -f /var/run/AdGlucitest >/dev/null 2>&1 - echo "AdGuardHome reloading" - start +reload_service() { + rm -f /var/run/AdGlucitest 2>/dev/null + echo "AdGuardHome reloading" + start } -del_querylog(){ - local backupwdpath - config_get backupwdpath $CONFIGURATION backupwdpath "/etc/AdGuardHome" - local workdir - config_get workdir $CONFIGURATION workdir "/etc/AdGuardHome" - - local btarget - btarget="$(ls "$backupwdpath/data" 2>/dev/null | grep "querylog.json" | sort -r | head -n1)" - local wtarget - wtarget="$(ls "$workdir/data" 2>/dev/null | grep "querylog.json" | sort -r | head -n1)" - - if [ "$btarget" = "$wtarget" ]; then - [ -z "$btarget" ] && return 1 - rm -f "$workdir/data/$wtarget" - rm -f "$backupwdpath/data/$btarget" - return 0 - fi - - if [ "$btarget" \> "$wtarget" ]; then - rm -f "$backupwdpath/data/$btarget" - else - rm -f "$workdir/data/$wtarget" - fi - return 0 -} - -stop_service() -{ - config_load "${CONFIGURATION}" - _do_redirect 0 - do_crontab - - if [ "$1" != "nobackup" ]; then - config_get backupfile $CONFIGURATION backupfile "0" - [ -n "$backupfile" ] && backup - fi - - echo "AdGuardHome service disabled" - touch /var/run/AdGserverdis +stop_service() { + stop_core || return 1 + config_load "${CONFIGURATION}" + _do_redirect 0 + echo "AdGuardHome service disabled" + touch /var/run/AdGserverdis } boot() { - rc_procd boot_service "$@" - if eval "type service_started" 2>/dev/null >/dev/null; then - service_started - fi -} - -test_crontab(){ - config_load "${CONFIGURATION}" - do_crontab -} - -do_crontab(){ - config_get_bool enabled $CONFIGURATION enabled 0 - config_get crontab $CONFIGURATION crontab "" - local cronreload=0 - - local findstr default cronenable - findstr="/usr/share/AdGuardHome/update_core.sh" - default="30 3 * * * /usr/share/AdGuardHome/update_core.sh" - [ "$enabled" = "0" ] || [ "${crontab//autoupdate/}" = "$crontab" ] && cronenable=0 || cronenable=1 - crontab_editor - - findstr="/usr/share/AdGuardHome/tailto.sh" - default="0 * * * * /usr/share/AdGuardHome/tailto.sh 2000 \$(uci get AdGuardHome.AdGuardHome.workdir)/data/querylog.json" - [ "$enabled" = "0" ] || [ "${crontab//cutquerylog/}" = "$crontab" ] && cronenable=0 || cronenable=1 - crontab_editor - - findstr="/usr/share/AdGuardHome/tailto.sh" - default="30 3 * * * /usr/share/AdGuardHome/tailto.sh 2000 \$(uci get AdGuardHome.AdGuardHome.logfile)" - [ "$enabled" = "0" ] || [ "${crontab//cutruntimelog/}" = "$crontab" ] && cronenable=0 || cronenable=1 - crontab_editor - - findstr="/usr/share/AdGuardHome/addhost.sh" - default="0 * * * * /usr/share/AdGuardHome/addhost.sh" - [ "$enabled" = "0" ] || [ "${crontab//autohost/}" = "$crontab" ] && cronenable=0 || cronenable=1 - crontab_editor - - [ "$cronreload" -gt 0 ] && /etc/init.d/cron restart -} - -crontab_editor(){ - local line - local reload - line="$(grep "$findstr" "$CRON_FILE" 2>/dev/null)" - - if [ "${line#\#}" != "$line" ]; then - if [ "$cronenable" = "1" ]; then - sed -i "\|$findstr|d" "$CRON_FILE" - echo "${line#\#}" >> "$CRON_FILE" - cronreload=$((cronreload + 1)) - elif [ -z "$reload" ]; then - sed -i "\|$findstr|d" "$CRON_FILE" - echo "$line" >> "$CRON_FILE" - fi - else - if [ "$cronenable" = "1" ]; then - if [ -z "$line" ]; then - line="$default" - reload=1 - fi - if [ -n "$reload" ]; then - sed -i "\|$findstr|d" "$CRON_FILE" - echo "$line" >> "$CRON_FILE" - cronreload=$((cronreload + 1)) - fi - elif [ -n "$line" ]; then - sed -i "\|$findstr|d" "$CRON_FILE" - echo "#$line" >> "$CRON_FILE" - cronreload=$((cronreload + 1)) - fi - fi -} + rc_procd boot_service "$@" + eval "type service_started" 2>/dev/null >/dev/null && service_started +} \ No newline at end of file diff --git a/luci-app-adguardhome/root/etc/uci-defaults/40_luci-AdGuardHome b/luci-app-adguardhome/root/etc/uci-defaults/40_luci-AdGuardHome old mode 100644 new mode 100755 index 37e192cd..e736ba60 --- a/luci-app-adguardhome/root/etc/uci-defaults/40_luci-AdGuardHome +++ b/luci-app-adguardhome/root/etc/uci-defaults/40_luci-AdGuardHome @@ -1,15 +1,14 @@ #!/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 -rm -f /tmp/luci-indexcache - -chmod +x /etc/init.d/AdGuardHome /usr/share/AdGuardHome/* +# 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/* exit 0 diff --git a/luci-app-adguardhome/root/lib/upgrade/keep.d/luci-app-adguardhome b/luci-app-adguardhome/root/lib/upgrade/keep.d/luci-app-adguardhome new file mode 100644 index 00000000..1a5bd6dc --- /dev/null +++ b/luci-app-adguardhome/root/lib/upgrade/keep.d/luci-app-adguardhome @@ -0,0 +1 @@ +/etc/AdGuardHome.yaml \ No newline at end of file diff --git a/luci-app-adguardhome/root/usr/lib/lua/luci/i18n/adguardhome.zh-cn.lmo b/luci-app-adguardhome/root/usr/lib/lua/luci/i18n/adguardhome.zh-cn.lmo deleted file mode 100644 index 7c23d77a..00000000 Binary files a/luci-app-adguardhome/root/usr/lib/lua/luci/i18n/adguardhome.zh-cn.lmo and /dev/null differ diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/AdGuardHome_template.yaml b/luci-app-adguardhome/root/usr/share/AdGuardHome/AdGuardHome_template.yaml old mode 100644 new mode 100755 index 612a5770..b6f63852 --- a/luci-app-adguardhome/root/usr/share/AdGuardHome/AdGuardHome_template.yaml +++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/AdGuardHome_template.yaml @@ -1,108 +1,123 @@ -bind_host: 0.0.0.0 -bind_port: 3000 -beta_bind_port: 0 +# 模板用户名为 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 users: -- name: root - password: $2y$10$dwn0hTYoECQMZETBErGlzOId2VANOVsPHsuH13TM/8KnysM5Dh/ve + - name: admin + password: $2y$10$iGkTW98QtGEZn4I/GC0J4eQhfuLQ53v7L4DDVauEJyWylJrXWwWeO auth_attempts: 5 block_auth_min: 15 http_proxy: "" -language: zh-cn -debug_pprof: false -web_session_ttl: 720 +language: "" +theme: auto dns: bind_hosts: - - 0.0.0.0 - port: 1745 - statistics_interval: 30 - querylog_enabled: true - querylog_file_enabled: true - querylog_interval: 6h - querylog_size_memory: 1000 + - 0.0.0.0 + port: 3053 anonymize_client_ip: false - 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: 20 + ratelimit_subnet_len_ipv4: 24 + ratelimit_subnet_len_ipv6: 56 ratelimit_whitelist: [] - refuse_any: false + refuse_any: true upstream_dns: - - 223.5.5.5 + - 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' upstream_dns_file: "" bootstrap_dns: - - 119.29.29.29 - - 223.5.5.5 - all_servers: false - fastest_addr: false + - 1.1.1.1 + - 1.0.0.1 + fallback_dns: + - quic://dns.adguard-dns.com + upstream_mode: parallel 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 + - 127.0.0.0/8 + - ::1/128 + cache_enabled: true cache_size: 4194304 cache_ttl_min: 0 cache_ttl_max: 0 - cache_optimistic: true + cache_optimistic: false + cache_optimistic_answer_ttl: 30s + cache_optimistic_max_age: 12h bogus_nxdomain: [] aaaa_disabled: false enable_dnssec: false - edns_client_subnet: false + edns_client_subnet: + custom_ip: "" + enabled: false + use_custom: false max_goroutines: 300 + handle_ddr: true 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: [] + ipset_file: "" + bootstrap_prefer_ipv6: false upstream_timeout: 10s - local_domain_name: lan - resolve_clients: true + private_networks: [] 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: 784 + port_dns_over_quic: 853 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/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 + - enabled: true + url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt + name: AdGuard DNS filter + id: 1 whitelist_filters: [] user_rules: [] dhcp: enabled: false interface_name: "" + local_domain_name: lan dhcpv4: gateway_ip: "" subnet_mask: "" @@ -116,16 +131,59 @@ dhcp: 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 +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 os: group: "" user: "" rlimit_nofile: 0 -schema_version: 12 +schema_version: 32 \ No newline at end of file diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/addhost.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/addhost.sh deleted file mode 100755 index a23a2efc..00000000 --- a/luci-app-adguardhome/root/usr/share/AdGuardHome/addhost.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/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" diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/adguardhome.nft.tpl b/luci-app-adguardhome/root/usr/share/AdGuardHome/adguardhome.nft.tpl new file mode 100644 index 00000000..67eef35e --- /dev/null +++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/adguardhome.nft.tpl @@ -0,0 +1,10 @@ +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__ + } +} diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/firewall.start b/luci-app-adguardhome/root/usr/share/AdGuardHome/firewall.start deleted file mode 100644 index 562117e5..00000000 --- a/luci-app-adguardhome/root/usr/share/AdGuardHome/firewall.start +++ /dev/null @@ -1,8 +0,0 @@ -#!/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 diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/getsyslog.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/getsyslog.sh index 908bdf63..e6387b26 100755 --- a/luci-app-adguardhome/root/usr/share/AdGuardHome/getsyslog.sh +++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/getsyslog.sh @@ -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) - if [ "$watchdog"x == "0"x ]; then - kill $pid + watchdog=$(cat /var/run/AdGuardHomesyslog 2>/dev/null) + if [ "${watchdog}x" = "0x" ]; 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 \ No newline at end of file diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/links.txt b/luci-app-adguardhome/root/usr/share/AdGuardHome/links.txt deleted file mode 100644 index e4f1c8fa..00000000 --- a/luci-app-adguardhome/root/usr/share/AdGuardHome/links.txt +++ /dev/null @@ -1,3 +0,0 @@ -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 diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/tailto.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/tailto.sh deleted file mode 100755 index 9ccc2190..00000000 --- a/luci-app-adguardhome/root/usr/share/AdGuardHome/tailto.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -tail -n $1 "$2" > /var/run/tailtmp -cat /var/run/tailtmp > "$2" -rm /var/run/tailtmp diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/update_core.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/update_core.sh index 1464be6a..2be7a26c 100755 --- a/luci-app-adguardhome/root/usr/share/AdGuardHome/update_core.sh +++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/update_core.sh @@ -1,321 +1,164 @@ #!/bin/sh PATH="/usr/sbin:/usr/bin:/sbin:/bin" +update_mode=$1 -binpath="$(uci get AdGuardHome.AdGuardHome.binpath 2>/dev/null)" +# 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) 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" -mkdir -p "${binpath%/*}" +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 +} -upxflag="$(uci get AdGuardHome.AdGuardHome.upxflag 2>/dev/null)" +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" +} -cleanup() { - rm -f /var/run/update_core 2>/dev/null - [ "$1" != "0" ] && touch /var/run/update_core_error +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(){ exit "$1" } -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" +Check_Updates "$update_mode" diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/waitnet.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/waitnet.sh deleted file mode 100755 index d45518af..00000000 --- a/luci-app-adguardhome/root/usr/share/AdGuardHome/waitnet.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/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 diff --git a/luci-app-adguardhome/root/usr/share/AdGuardHome/watchconfig.sh b/luci-app-adguardhome/root/usr/share/AdGuardHome/watchconfig.sh index 61ba09de..f6850663 100755 --- a/luci-app-adguardhome/root/usr/share/AdGuardHome/watchconfig.sh +++ b/luci-app-adguardhome/root/usr/share/AdGuardHome/watchconfig.sh @@ -10,4 +10,4 @@ do break fi done -return 0 +exit 0 \ No newline at end of file diff --git a/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json b/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json index 189428c2..6cdcf8bb 100644 --- a/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json +++ b/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json @@ -1,53 +1,45 @@ { - "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" - } - } + "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" ] + } + } } diff --git a/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json b/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json index 485aa620..41727fce 100644 --- a/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json +++ b/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json @@ -2,10 +2,20 @@ "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" ] } } -} +} \ No newline at end of file diff --git a/luci-app-adguardhome/root/usr/share/rpcd/ucode/luci.adguardhome b/luci-app-adguardhome/root/usr/share/rpcd/ucode/luci.adguardhome new file mode 100644 index 00000000..6e46f612 --- /dev/null +++ b/luci-app-adguardhome/root/usr/share/rpcd/ucode/luci.adguardhome @@ -0,0 +1,265 @@ +#!/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 2>&1 /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 }; + } + } + } +}; \ No newline at end of file diff --git a/luci-app-adguardhome/root/www/luci-static/resources/codemirror/addon/fold/foldcode.js b/luci-app-adguardhome/root/www/luci-static/resources/codemirror/addon/fold/foldcode.js deleted file mode 100644 index f93d42b7..00000000 --- a/luci-app-adguardhome/root/www/luci-static/resources/codemirror/addon/fold/foldcode.js +++ /dev/null @@ -1 +0,0 @@ -!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.linee.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=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.fromo.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&&ro))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}})}); \ No newline at end of file diff --git a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/base.js b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/base.js new file mode 100644 index 00000000..ae1163c4 --- /dev/null +++ b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/base.js @@ -0,0 +1,276 @@ +'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; + }); + } +}); diff --git a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/compact.css b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/compact.css new file mode 100644 index 00000000..ec755092 --- /dev/null +++ b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/compact.css @@ -0,0 +1,153 @@ +/* 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; +} diff --git a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/log.js b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/log.js new file mode 100644 index 00000000..8fc0bf9e --- /dev/null +++ b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/log.js @@ -0,0 +1,242 @@ +'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; + }); + } +}); \ No newline at end of file diff --git a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/manual.js b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/manual.js new file mode 100644 index 00000000..638604de --- /dev/null +++ b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/manual.js @@ -0,0 +1,232 @@ +'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; + } +}); diff --git a/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/status.js b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/status.js new file mode 100644 index 00000000..f976e2a9 --- /dev/null +++ b/luci-app-adguardhome/root/www/luci-static/resources/view/adguardhome/status.js @@ -0,0 +1,204 @@ +'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 }; + } +}); diff --git a/luci-app-adguardhome/scripts/build-apk.sh b/luci-app-adguardhome/scripts/build-apk.sh new file mode 100755 index 00000000..a1aacdc9 --- /dev/null +++ b/luci-app-adguardhome/scripts/build-apk.sh @@ -0,0 +1,34 @@ +#!/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 diff --git a/luci-app-adguardhome/scripts/collect-apks.sh b/luci-app-adguardhome/scripts/collect-apks.sh new file mode 100755 index 00000000..b9067ad2 --- /dev/null +++ b/luci-app-adguardhome/scripts/collect-apks.sh @@ -0,0 +1,11 @@ +#!/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 diff --git a/luci-app-adguardhome/scripts/install.sh b/luci-app-adguardhome/scripts/install.sh new file mode 100644 index 00000000..54121919 --- /dev/null +++ b/luci-app-adguardhome/scripts/install.sh @@ -0,0 +1,86 @@ +#!/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." diff --git a/luci-app-adguardhome/scripts/prepare-sdk.sh b/luci-app-adguardhome/scripts/prepare-sdk.sh new file mode 100755 index 00000000..f1c2f0e6 --- /dev/null +++ b/luci-app-adguardhome/scripts/prepare-sdk.sh @@ -0,0 +1,10 @@ +#!/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