The main sweep — ~260 call sites across ~100 files move from string
concatenation on a single root to appDir/storageAppDirs/storageAppConfigs.
On a single-root install the resolved paths are identical, so this is a
no-op until a location is registered.
Enumerators were the interesting half. `for d in "$containers_dir"/*/`
appears in the menus, the registry/artifact scanners and the DNS setup —
and a shell glob cannot list a rootless 751 tree at all, which is the
same bug config_find_file.sh already documents in a comment. Routing them
through storageAppDirs (which enumerates as the owning user) fixes that
alongside the multi-root work.
Three places needed judgement rather than substitution:
db_app_scan.sh deletes database rows and port allocations for apps whose
folder is missing, and reaps "empty" app dirs. With a storage location
unmounted, every app on it looks exactly like that. Each of those
branches now gates on appStorageAvailable first — an app on an unplugged
drive is skipped with a notice, never deleted.
instance_create.sh rewrites cloned hooks so an instance touches its own
directory instead of the base app's. Its sed matched ${containers_dir}<type>,
which this sweep just replaced with $(appDir <type>) — so it would have
silently stopped redirecting, and an instance would have written to the
original's files (the adguard auth adapter case its own comment warns
about). Now matches both appDir forms, verified against bare, quoted,
unrelated-app, legacy and prose cases.
peer_shell/peer_pull streamed and extracted relative to the primary root.
Both now use the app's own root, and peer_shell keeps a single-root
fallback since it runs as a restricted SSH shell with no LibrePortal env.
Also fixes a pre-existing bug found on the way: webui_app_config.sh
tested "$containers_dir/frontend/data/last_update", one level short of the
real tree under the libreportal app dir, so the WebUI refresh trigger
after a config update has never once fired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
143 lines
6.6 KiB
Bash
143 lines
6.6 KiB
Bash
#!/bin/bash
|
|
|
|
# AdGuard Home install hooks — drive the first-boot setup wizard via its
|
|
# HTTP API so the admin doesn't have to click through five pages, then
|
|
# pin the admin bind back to 0.0.0.0:3000 (matches the compose mapping)
|
|
# and health-check the result.
|
|
|
|
adguard_install_post_start()
|
|
{
|
|
local app_name="$1"
|
|
|
|
((menu_number++))
|
|
echo ""
|
|
echo "---- $menu_number. Completing AdGuardHome initial setup automatically"
|
|
echo ""
|
|
|
|
# The legacy `$usedport1` variable isn't populated by the current
|
|
# install pipeline; the resolved host port is stored in the PORTS_TAG_1
|
|
# docker-compose tag (format `external:internal`). Pull it from there
|
|
# so the curl + URL printout actually point somewhere real.
|
|
local adguard_compose_file="$(appDir "$app_name")/docker-compose.yml"
|
|
local adguard_port_pair
|
|
adguard_port_pair=$(tagsManagerGetTagContent "$adguard_compose_file" "PORTS_TAG_1")
|
|
local adguard_admin_port="${adguard_port_pair%%:*}"
|
|
|
|
if [[ -n "$public_ip_v4" && -n "$adguard_admin_port" ]]; then
|
|
echo " External : http://$public_ip_v4:$adguard_admin_port/"
|
|
fi
|
|
if [[ -n "$host_setup" ]]; then
|
|
echo " Hostname : http://$host_setup/"
|
|
fi
|
|
echo ""
|
|
|
|
# AdGuardHome ships a setup wizard that normally needs five clicks in
|
|
# a browser before the daemon writes its config file. Same wizard is
|
|
# exposed as an HTTP API (POST /control/install/configure), so drive
|
|
# it from here and skip the manual interaction. Pre-poll the admin
|
|
# endpoint until the container is up, then send the form, then let
|
|
# the post-install sed edits run against the freshly written
|
|
# AdGuardHome.yaml.
|
|
local adguard_setup_url="http://127.0.0.1:${adguard_admin_port}"
|
|
local adguard_attempts=0
|
|
local adguard_max_attempts=60
|
|
while ((adguard_attempts < adguard_max_attempts)); do
|
|
if curl -fsS -o /dev/null --max-time 2 "${adguard_setup_url}/control/status" 2>/dev/null \
|
|
|| curl -fsS -o /dev/null --max-time 2 "${adguard_setup_url}/control/install/get_addresses" 2>/dev/null; then
|
|
break
|
|
fi
|
|
sleep 2
|
|
((adguard_attempts++))
|
|
done
|
|
|
|
if ((adguard_attempts >= adguard_max_attempts)); then
|
|
isError "AdGuardHome admin endpoint did not respond on $adguard_setup_url within $((adguard_max_attempts * 2))s — open the URL and complete setup manually, then re-run the installer to apply the post-setup tweaks."
|
|
else
|
|
local adguard_user="${CFG_ADGUARD_USER:-admin}"
|
|
local adguard_pass="${CFG_ADGUARD_PASSWORD_1:-}"
|
|
if [[ -z "$adguard_pass" ]]; then
|
|
adguard_pass=$(generateRandomPassword)
|
|
updateConfigOption "CFG_ADGUARD_PASSWORD_1" "$adguard_pass" >/dev/null 2>&1 || true
|
|
isNotice "Generated a random AdGuardHome admin password and saved it to CFG_ADGUARD_PASSWORD_1."
|
|
fi
|
|
|
|
# Internal container ports are fixed (3000 admin, 53 DNS); host
|
|
# mapping is what `usedport1` etc. handle.
|
|
local adguard_payload
|
|
adguard_payload=$(cat <<JSON
|
|
{
|
|
"web": { "ip": "0.0.0.0", "port": 3000, "autofix": false },
|
|
"dns": { "ip": "0.0.0.0", "port": 53, "autofix": false },
|
|
"username": "${adguard_user}",
|
|
"password": "${adguard_pass}"
|
|
}
|
|
JSON
|
|
)
|
|
if curl -fsS -X POST \
|
|
-H 'Content-Type: application/json' \
|
|
--data "$adguard_payload" \
|
|
--max-time 15 \
|
|
"${adguard_setup_url}/control/install/configure" >/dev/null 2>&1; then
|
|
isSuccessful "AdGuardHome admin setup completed automatically (user: $adguard_user)."
|
|
else
|
|
# 422/403 here typically means setup was already done on a
|
|
# previous install; the post-setup tweaks below are still
|
|
# safe to run against the existing yaml.
|
|
isNotice "AdGuardHome /control/install/configure rejected the request — assuming it's already configured. If this is a fresh install, complete setup manually at $adguard_setup_url."
|
|
fi
|
|
fi
|
|
|
|
local result
|
|
if [[ "$public" == "true" ]]; then
|
|
result=$(runFileOp sed -i "s|allow_unencrypted_doh: false|allow_unencrypted_doh: true|g" "$(appDir "$app_name")/conf/AdGuardHome.yaml")
|
|
checkSuccess "Setting allow_unencrypted_doh to false for Traefik"
|
|
fi
|
|
|
|
result=$(runFileOp sed -i "s|anonymize_client_ip: false: false|anonymize_client_ip: true|g" "$(appDir "$app_name")/conf/AdGuardHome.yaml")
|
|
checkSuccess "Setting anonymize_client_ip to true for privacy reasons"
|
|
|
|
# Force the admin web bind back to 0.0.0.0:3000 inside the container.
|
|
# The docker-compose mapping is `<host_port>:3000`, so the container
|
|
# MUST listen on 3000 internally for the host port to reach it. After
|
|
# the install API call AdGuardHome sometimes ends up bound to
|
|
# 0.0.0.0:80 (its build-time default) — exactly what causes "unable
|
|
# to connect" on the host port.
|
|
local adguard_yaml="$(appDir "$app_name")/conf/AdGuardHome.yaml"
|
|
if [[ -f "$adguard_yaml" ]]; then
|
|
runFileOp sed -i 's|^\(\s*address:\s*\)0\.0\.0\.0:[0-9]\+|\10.0.0.0:3000|' "$adguard_yaml"
|
|
runFileOp sed -i 's|^\(\s*bind_host:\s*\).*|\10.0.0.0|' "$adguard_yaml"
|
|
runFileOp sed -i 's|^\(\s*bind_port:\s*\)[0-9]\+|\13000|' "$adguard_yaml"
|
|
checkSuccess "Pinned AdGuardHome admin bind to 0.0.0.0:3000 (matches the compose port mapping)."
|
|
fi
|
|
|
|
dockerComposeRestart "$app_name"
|
|
|
|
# Drop `-f` and accept any HTTP status code: now that the admin
|
|
# account is configured, /control/status returns 401 to an
|
|
# unauthenticated request — which is fine, it means the server is up
|
|
# and answering. We only care whether the connection succeeded at
|
|
# all, not what the response body says.
|
|
local adguard_health_attempts=0
|
|
local adguard_health_code
|
|
while ((adguard_health_attempts < 20)); do
|
|
adguard_health_code=$(curl -sS -o /dev/null --max-time 2 \
|
|
-w '%{http_code}' "${adguard_setup_url}/control/status" 2>/dev/null)
|
|
if [[ "$adguard_health_code" =~ ^[1-5][0-9][0-9]$ ]]; then
|
|
isSuccessful "AdGuardHome admin UI is reachable on $adguard_setup_url (HTTP $adguard_health_code)"
|
|
break
|
|
fi
|
|
sleep 1
|
|
((adguard_health_attempts++))
|
|
done
|
|
if ((adguard_health_attempts >= 20)); then
|
|
isError "AdGuardHome admin UI did not respond after restart on $adguard_setup_url. Check the container logs (\`docker logs adguard-service\`) and the conf/AdGuardHome.yaml bind address."
|
|
fi
|
|
}
|
|
|
|
adguard_install_message_data()
|
|
{
|
|
# Echo the admin user + password as space-separated tokens so they
|
|
# become $username $password positional args to menuShowFinalMessages.
|
|
echo "${CFG_ADGUARD_USER:-admin} $CFG_ADGUARD_PASSWORD_1"
|
|
}
|