LibrePortal/containers/mastodon/scripts/mastodon_install_hooks.sh
librelad 8b5e02c760 refactor(storage): resolve every app directory through appDir
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>
2026-08-24 04:09:51 +01:00

72 lines
3.4 KiB
Bash

#!/bin/bash
# Mastodon install hooks.
# Web Push identity.
# ---------------------------------------------------------------------------
# The two VAPID values are not independent secrets — they are the two halves of
# one P-256 keypair, and the browser verifies that the push request is signed by
# the private key matching the public key it subscribed with. The framework's
# RANDOMIZED* generators mint each placeholder on its own, so they can produce
# two well-formed strings but never a *pair*: what shipped before was two
# unrelated random values, and web push could not work with them. Stoat hits the
# same wall and solves it the same way, in its own install hook.
#
# Mastodon's webpush gem expects unpadded URL-safe base64 of the raw key
# material: the 32-byte private scalar (43 chars) and the 65-byte uncompressed
# public point (87 chars). Both are sliced out of the SEC1 DER, whose layout for
# prime256v1 is a fixed 121 bytes — a 2-byte SEQUENCE header, INTEGER 1, then
# `04 20` introducing the private scalar at offset 7, and the public point as
# the trailing field. Both slices were cross-checked against `openssl ec -text`.
#
# Runs at install_post_setup: the deployed config exists by then (so there is
# something to write to) and the compose has not been templated yet (so the pair
# reaches the compose on this same install).
#
# Generated once, then left alone. Rotating the pair invalidates every push
# subscription clients are holding, so an existing well-formed pair is never
# replaced — including across reinstalls.
mastodon_install_post_setup()
{
local app_name="${1:-mastodon}"
local config_file="$(appDir "$app_name")/${app_name}.config"
[[ -f "$config_file" ]] || return 0
# Keep a pair that already has the right shape. The length test is also what
# retires the old values: two independent RANDOMIZEDVAPID strings are the
# wrong length for the public half, so an install carrying them regenerates
# once and is correct from then on.
if [[ "$CFG_MASTODON_VAPID_PRIVATE_KEY_1" =~ ^[A-Za-z0-9_-]{43}$ \
&& "$CFG_MASTODON_VAPID_PUBLIC_KEY_1" =~ ^[A-Za-z0-9_-]{87}$ ]]; then
return 0
fi
local pem der
pem=$(mktemp) || return 0
der=$(mktemp) || { rm -f "$pem"; return 0; }
if ! openssl ecparam -name prime256v1 -genkey -noout -out "$pem" 2>/dev/null \
|| ! openssl ec -in "$pem" -outform DER -out "$der" 2>/dev/null; then
rm -f "$pem" "$der"
isError "Could not generate a VAPID keypair for $app_name — web push will not work."
return 0
fi
local vapid_private vapid_public
vapid_private=$(head -c 39 "$der" | tail -c 32 | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
vapid_public=$(tail -c 65 "$der" | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')
rm -f "$pem" "$der"
# Refuse to write a malformed pair over a working one — a short read or an
# openssl build with a different DER layout would otherwise quietly break
# push instead of leaving it as it was.
if [[ ! "$vapid_private" =~ ^[A-Za-z0-9_-]{43}$ || ! "$vapid_public" =~ ^[A-Za-z0-9_-]{87}$ ]]; then
isError "Generated VAPID keypair had an unexpected shape — leaving $app_name's push keys alone."
return 0
fi
updateConfigOption "CFG_MASTODON_VAPID_PRIVATE_KEY_1" "$vapid_private" "$config_file"
updateConfigOption "CFG_MASTODON_VAPID_PUBLIC_KEY_1" "$vapid_public" "$config_file"
isSuccessful "Generated a VAPID keypair for $app_name web push."
}