471 lines
13 KiB
Bash
Executable File
471 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
repo_root="$(cd "${script_dir}/.." && pwd)"
|
|
source "${script_dir}/lib-compose-project.sh"
|
|
|
|
compose_file=""
|
|
env_file=""
|
|
app_service="app"
|
|
verify_only="0"
|
|
|
|
usage() {
|
|
cat <<USAGE
|
|
Usage: ./scripts/ensure-custom-pwa-push-config.sh --compose-file <file> --env-file <file> [options]
|
|
|
|
Ensure CustomPWA push/VAPID configuration is present and synced:
|
|
- generates CUSTOM_PWA_VAPID_SUBJECT when missing
|
|
- generates CUSTOM_PWA_VAPID_PUBLIC_KEY / CUSTOM_PWA_VAPID_PRIVATE_KEY when missing
|
|
- enables/installs custom_pwa when INSTALL_CUSTOM_PWA is enabled
|
|
- syncs VAPID + fallback settings into Nextcloud app config
|
|
|
|
Options:
|
|
--compose-file <path> docker compose file to use (required)
|
|
--env-file <path> env file to use (required)
|
|
--app-service <name> Nextcloud app service name (default: app)
|
|
--verify-only Do not install/repair; only verify current state
|
|
-h, --help Show this help
|
|
USAGE
|
|
}
|
|
|
|
read_kv() {
|
|
local key="$1"
|
|
local line
|
|
line="$(grep -m1 -E "^${key}=" "${env_file}" || true)"
|
|
if [[ -z "${line}" ]]; then
|
|
return 1
|
|
fi
|
|
echo "${line#*=}"
|
|
}
|
|
|
|
read_kv_first() {
|
|
local value=""
|
|
local key
|
|
for key in "$@"; do
|
|
value="$(read_kv "${key}" || true)"
|
|
if [[ -n "${value}" ]]; then
|
|
echo "${value}"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
set_kv() {
|
|
local key="$1"
|
|
local value="$2"
|
|
local tmp esc
|
|
esc="${value//\\/\\\\}"
|
|
esc="${esc//&/\\&}"
|
|
esc="${esc//|/\\|}"
|
|
tmp="$(mktemp)"
|
|
if grep -q -E "^${key}=" "${env_file}"; then
|
|
sed -E "s|^${key}=.*$|${key}=${esc}|" "${env_file}" > "${tmp}"
|
|
else
|
|
cat "${env_file}" > "${tmp}"
|
|
printf '\n%s=%s\n' "${key}" "${value}" >> "${tmp}"
|
|
fi
|
|
mv "${tmp}" "${env_file}"
|
|
}
|
|
|
|
normalize_bool_01() {
|
|
local raw="${1:-}"
|
|
case "$(echo "${raw}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" in
|
|
1|true|yes|y|on)
|
|
echo "1"
|
|
;;
|
|
0|false|no|n|off)
|
|
echo "0"
|
|
;;
|
|
*)
|
|
echo ""
|
|
;;
|
|
esac
|
|
}
|
|
|
|
custom_pwa_should_install() {
|
|
local raw normalized
|
|
raw="$(read_kv_first "INSTALL_CUSTOM_PWA" || true)"
|
|
normalized="$(normalize_bool_01 "${raw}")"
|
|
if [[ -z "${normalized}" ]]; then
|
|
normalized="1"
|
|
fi
|
|
[[ "${normalized}" == "1" ]]
|
|
}
|
|
|
|
normalize_vapid_subject() {
|
|
local raw="${1:-}"
|
|
raw="$(echo "${raw}" | tr -d '\r' | xargs)"
|
|
if [[ -z "${raw}" ]]; then
|
|
echo ""
|
|
return 0
|
|
fi
|
|
if [[ "${raw}" == mailto:* || "${raw}" == http://* || "${raw}" == https://* ]]; then
|
|
echo "${raw}"
|
|
return 0
|
|
fi
|
|
if [[ "${raw}" == *"@"* ]]; then
|
|
echo "mailto:${raw}"
|
|
return 0
|
|
fi
|
|
echo "${raw}"
|
|
}
|
|
|
|
derive_email_domain_from_host() {
|
|
local raw_host="${1:-}"
|
|
local host parts_count last_part second_last third_last
|
|
raw_host="$(echo "${raw_host}" | tr -d '\r' | xargs)"
|
|
host="${raw_host#http://}"
|
|
host="${host#https://}"
|
|
host="${host%%/*}"
|
|
host="${host%%:*}"
|
|
host="${host#[}"
|
|
host="${host%]}"
|
|
host="${host,,}"
|
|
|
|
if [[ -z "${host}" ]]; then
|
|
echo ""
|
|
return 0
|
|
fi
|
|
if [[ "${host}" == "localhost" ]]; then
|
|
echo "localhost"
|
|
return 0
|
|
fi
|
|
if [[ "${host}" =~ ^[0-9.]+$ ]]; then
|
|
echo "${host}"
|
|
return 0
|
|
fi
|
|
if [[ "${host}" == *:* ]]; then
|
|
echo "${host}"
|
|
return 0
|
|
fi
|
|
|
|
parts_count="$(awk -F'.' '{print NF}' <<< "${host}")"
|
|
if [[ "${parts_count}" -le 2 ]]; then
|
|
echo "${host}"
|
|
return 0
|
|
fi
|
|
|
|
last_part="${host##*.}"
|
|
second_last="${host%.*}"
|
|
second_last="${second_last##*.}"
|
|
third_last="${host%.*.*}"
|
|
third_last="${third_last##*.}"
|
|
if [[ ${#last_part} -eq 2 && ${#second_last} -le 3 ]]; then
|
|
echo "${third_last}.${second_last}.${last_part}"
|
|
return 0
|
|
fi
|
|
echo "${second_last}.${last_part}"
|
|
}
|
|
|
|
ensure_docker_access() {
|
|
local docker_path=""
|
|
docker_path="$(type -P docker || true)"
|
|
if [[ -z "${docker_path}" ]]; then
|
|
echo "docker is required"
|
|
exit 1
|
|
fi
|
|
|
|
if "${docker_path}" info >/dev/null 2>&1; then
|
|
DOCKER_CMD=("${docker_path}")
|
|
return 0
|
|
fi
|
|
|
|
if ! command -v sudo >/dev/null 2>&1; then
|
|
echo "docker is installed but this user cannot access it, and sudo is unavailable."
|
|
exit 1
|
|
fi
|
|
|
|
if sudo -n "${docker_path}" info >/dev/null 2>&1; then
|
|
DOCKER_CMD=(sudo "${docker_path}")
|
|
return 0
|
|
fi
|
|
|
|
echo "docker requires elevated privileges on this machine."
|
|
if sudo "${docker_path}" info >/dev/null 2>&1; then
|
|
DOCKER_CMD=(sudo "${docker_path}")
|
|
return 0
|
|
fi
|
|
|
|
echo "Unable to access docker, even with sudo."
|
|
exit 1
|
|
}
|
|
|
|
docker() {
|
|
"${DOCKER_CMD[@]}" "$@"
|
|
}
|
|
|
|
compose_cmd() {
|
|
docker compose -f "${compose_file}" --env-file "${env_file}" "$@"
|
|
}
|
|
|
|
occ_exec_quiet_retry() {
|
|
local rc=1
|
|
local max_attempts="${OCC_RETRY_ATTEMPTS:-6}"
|
|
local retry_sleep="${OCC_RETRY_SLEEP_SECONDS:-4}"
|
|
local attempt
|
|
if [[ $# -eq 0 ]]; then
|
|
return 1
|
|
fi
|
|
for attempt in $(seq 1 "${max_attempts}"); do
|
|
compose_cmd exec -T --user www-data "${app_service}" php occ "$@" >/dev/null 2>&1
|
|
rc=$?
|
|
if [[ "${rc}" -eq 0 ]]; then
|
|
return 0
|
|
fi
|
|
sleep "${retry_sleep}"
|
|
done
|
|
return "${rc}"
|
|
}
|
|
|
|
occ_app_set_custom_pwa() {
|
|
local key="$1"
|
|
local value="$2"
|
|
compose_cmd exec -T --user www-data "${app_service}" php occ config:app:set custom_pwa "${key}" --value="${value}" >/dev/null 2>&1
|
|
}
|
|
|
|
occ_app_get_custom_pwa() {
|
|
local key="$1"
|
|
compose_cmd exec -T --user www-data "${app_service}" php occ config:app:get custom_pwa "${key}" 2>/dev/null | tr -d '\r'
|
|
}
|
|
|
|
generate_custom_pwa_vapid_keypair_openssl() {
|
|
local tmp_key key_text priv_hex pub_hex public_key private_key
|
|
tmp_key="$(mktemp)"
|
|
if ! openssl ecparam -name prime256v1 -genkey -noout -out "${tmp_key}" >/dev/null 2>&1; then
|
|
rm -f "${tmp_key}" >/dev/null 2>&1 || true
|
|
return 1
|
|
fi
|
|
key_text="$(openssl ec -in "${tmp_key}" -text -noout 2>/dev/null || true)"
|
|
rm -f "${tmp_key}" >/dev/null 2>&1 || true
|
|
if [[ -z "${key_text}" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
priv_hex="$(echo "${key_text}" | awk '/priv:/{flag=1;next}/pub:/{flag=0}flag' | tr -d '[:space:]:')"
|
|
pub_hex="$(echo "${key_text}" | awk '/pub:/{flag=1;next}/ASN1 OID:/{flag=0}flag' | tr -d '[:space:]:')"
|
|
if [[ -z "${priv_hex}" || -z "${pub_hex}" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
public_key="$(echo "${pub_hex}" | xxd -r -p | base64 | tr '+/' '-_' | tr -d '=' | tr -d '\n' || true)"
|
|
private_key="$(echo "${priv_hex}" | xxd -r -p | base64 | tr '+/' '-_' | tr -d '=' | tr -d '\n' || true)"
|
|
if [[ -z "${public_key}" || -z "${private_key}" ]]; then
|
|
return 1
|
|
fi
|
|
echo "PUBLIC=${public_key}"
|
|
echo "PRIVATE=${private_key}"
|
|
return 0
|
|
}
|
|
|
|
generate_custom_pwa_vapid_keypair() {
|
|
local output public_key private_key php_code
|
|
php_code='$cfg=["private_key_type"=>OPENSSL_KEYTYPE_EC,"curve_name"=>"prime256v1"];$res=openssl_pkey_new($cfg);if($res===false){fwrite(STDERR,"generate_failed\n");exit(1);}$details=openssl_pkey_get_details($res);if(!is_array($details)||!isset($details["ec"])||!is_array($details["ec"])||!isset($details["ec"]["x"],$details["ec"]["y"],$details["ec"]["d"])){fwrite(STDERR,"details_failed\n");exit(1);}$rawPublic="\x04".$details["ec"]["x"].$details["ec"]["y"];$b64=function($v){return rtrim(strtr(base64_encode($v),"+/","-_"),"=");};echo "PUBLIC=".$b64($rawPublic).PHP_EOL;echo "PRIVATE=".$b64($details["ec"]["d"]).PHP_EOL;'
|
|
|
|
output=""
|
|
if command -v php >/dev/null 2>&1; then
|
|
output="$(php -r "${php_code}" 2>/dev/null || true)"
|
|
fi
|
|
if [[ -z "${output}" ]]; then
|
|
output="$(compose_cmd exec -T "${app_service}" php -r "${php_code}" 2>/dev/null || true)"
|
|
fi
|
|
if [[ -z "${output}" && $(command -v openssl >/dev/null 2>&1; echo $?) -eq 0 ]]; then
|
|
output="$(generate_custom_pwa_vapid_keypair_openssl || true)"
|
|
fi
|
|
public_key="$(echo "${output}" | sed -n 's/^PUBLIC=//p' | head -n1)"
|
|
private_key="$(echo "${output}" | sed -n 's/^PRIVATE=//p' | head -n1)"
|
|
if [[ -z "${public_key}" || -z "${private_key}" ]]; then
|
|
return 1
|
|
fi
|
|
echo "${public_key}|${private_key}"
|
|
return 0
|
|
}
|
|
|
|
ensure_custom_pwa_vapid_env() {
|
|
local subject public_key private_key admin_email nextcloud_domain nextcloud_root_domain default_subject generated generated_public generated_private status
|
|
status=0
|
|
subject="$(read_kv_first "CUSTOM_PWA_VAPID_SUBJECT" || true)"
|
|
public_key="$(read_kv_first "CUSTOM_PWA_VAPID_PUBLIC_KEY" || true)"
|
|
private_key="$(read_kv_first "CUSTOM_PWA_VAPID_PRIVATE_KEY" || true)"
|
|
|
|
if [[ -z "${subject}" ]]; then
|
|
admin_email="$(read_kv "NEXTCLOUD_ADMIN_EMAIL" || true)"
|
|
nextcloud_domain="$(read_kv "NEXTCLOUD_DOMAIN" || true)"
|
|
nextcloud_root_domain="$(derive_email_domain_from_host "${nextcloud_domain}")"
|
|
if [[ -n "${admin_email}" ]]; then
|
|
default_subject="mailto:${admin_email}"
|
|
elif [[ -n "${nextcloud_root_domain}" ]]; then
|
|
default_subject="mailto:admin@${nextcloud_root_domain}"
|
|
else
|
|
default_subject="mailto:admin@localhost"
|
|
fi
|
|
subject="${default_subject}"
|
|
set_kv "CUSTOM_PWA_VAPID_SUBJECT" "${subject}"
|
|
else
|
|
subject="$(normalize_vapid_subject "${subject}")"
|
|
set_kv "CUSTOM_PWA_VAPID_SUBJECT" "${subject}"
|
|
fi
|
|
|
|
if [[ -z "${public_key}" || -z "${private_key}" ]]; then
|
|
generated="$(generate_custom_pwa_vapid_keypair || true)"
|
|
generated_public="${generated%%|*}"
|
|
generated_private="${generated#*|}"
|
|
if [[ -z "${generated_public}" || -z "${generated_private}" || "${generated_public}" == "${generated_private}" ]]; then
|
|
status=1
|
|
else
|
|
set_kv "CUSTOM_PWA_VAPID_PUBLIC_KEY" "${generated_public}"
|
|
set_kv "CUSTOM_PWA_VAPID_PRIVATE_KEY" "${generated_private}"
|
|
fi
|
|
fi
|
|
|
|
return "${status}"
|
|
}
|
|
|
|
sync_custom_pwa_settings() {
|
|
local status subject public_key private_key email_fallback sms_fallback normalized_bool
|
|
status=0
|
|
|
|
subject="$(read_kv_first "CUSTOM_PWA_VAPID_SUBJECT" || true)"
|
|
public_key="$(read_kv_first "CUSTOM_PWA_VAPID_PUBLIC_KEY" || true)"
|
|
private_key="$(read_kv_first "CUSTOM_PWA_VAPID_PRIVATE_KEY" || true)"
|
|
if [[ -n "${subject}" ]]; then
|
|
occ_app_set_custom_pwa "vapid_subject" "${subject}" || status=1
|
|
fi
|
|
if [[ -n "${public_key}" ]]; then
|
|
occ_app_set_custom_pwa "vapid_public_key" "${public_key}" || status=1
|
|
fi
|
|
if [[ -n "${private_key}" ]]; then
|
|
occ_app_set_custom_pwa "vapid_private_key" "${private_key}" || status=1
|
|
fi
|
|
|
|
email_fallback="$(read_kv_first "CUSTOM_PWA_EMAIL_FALLBACK_ENABLED" || true)"
|
|
normalized_bool="$(normalize_bool_01 "${email_fallback}")"
|
|
if [[ -n "${normalized_bool}" ]]; then
|
|
occ_app_set_custom_pwa "email_fallback_enabled" "${normalized_bool}" || status=1
|
|
else
|
|
occ_app_set_custom_pwa "email_fallback_enabled" "1" || status=1
|
|
fi
|
|
|
|
sms_fallback="$(read_kv_first "CUSTOM_PWA_SMS_CRITICAL_FALLBACK_ENABLED" || true)"
|
|
normalized_bool="$(normalize_bool_01 "${sms_fallback}")"
|
|
if [[ -n "${normalized_bool}" ]]; then
|
|
occ_app_set_custom_pwa "sms_critical_fallback_enabled" "${normalized_bool}" || status=1
|
|
else
|
|
occ_app_set_custom_pwa "sms_critical_fallback_enabled" "0" || status=1
|
|
fi
|
|
|
|
return "${status}"
|
|
}
|
|
|
|
verify_custom_pwa_settings() {
|
|
local vapid_subject vapid_public_key vapid_private_key
|
|
vapid_subject="$(occ_app_get_custom_pwa "vapid_subject" || true)"
|
|
vapid_public_key="$(occ_app_get_custom_pwa "vapid_public_key" || true)"
|
|
vapid_private_key="$(occ_app_get_custom_pwa "vapid_private_key" || true)"
|
|
|
|
if [[ -z "${vapid_subject}" || -z "${vapid_public_key}" || -z "${vapid_private_key}" ]]; then
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--compose-file)
|
|
compose_file="${2:-}"
|
|
shift 2
|
|
;;
|
|
--compose-file=*)
|
|
compose_file="${1#*=}"
|
|
shift
|
|
;;
|
|
--env-file)
|
|
env_file="${2:-}"
|
|
shift 2
|
|
;;
|
|
--env-file=*)
|
|
env_file="${1#*=}"
|
|
shift
|
|
;;
|
|
--app-service)
|
|
app_service="${2:-}"
|
|
shift 2
|
|
;;
|
|
--app-service=*)
|
|
app_service="${1#*=}"
|
|
shift
|
|
;;
|
|
--verify-only)
|
|
verify_only="1"
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "${compose_file}" || -z "${env_file}" ]]; then
|
|
usage
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${compose_file}" != /* ]]; then
|
|
compose_file="${repo_root}/${compose_file}"
|
|
fi
|
|
if [[ "${env_file}" != /* ]]; then
|
|
env_file="${repo_root}/${env_file}"
|
|
fi
|
|
|
|
if [[ ! -f "${compose_file}" ]]; then
|
|
echo "Missing compose file: ${compose_file}"
|
|
exit 1
|
|
fi
|
|
if [[ ! -f "${env_file}" ]]; then
|
|
echo "Missing env file: ${env_file}"
|
|
exit 1
|
|
fi
|
|
|
|
export_compose_project_name_from_env "${env_file}" || true
|
|
|
|
if ! custom_pwa_should_install; then
|
|
echo "CustomPWA install is disabled in ${env_file}; skipping push config sync."
|
|
exit 0
|
|
fi
|
|
|
|
ensure_docker_access
|
|
|
|
if ! compose_cmd exec -T --user www-data "${app_service}" php occ status >/dev/null 2>&1; then
|
|
echo "Nextcloud OCC is not ready; cannot ensure CustomPWA push config yet."
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${verify_only}" != "1" ]]; then
|
|
ensure_custom_pwa_vapid_env
|
|
occ_exec_quiet_retry app:enable custom_pwa >/dev/null 2>&1 || true
|
|
if ! occ_exec_quiet_retry app:enable --force custom_pwa; then
|
|
occ_exec_quiet_retry app:install custom_pwa
|
|
occ_exec_quiet_retry app:enable --force custom_pwa
|
|
fi
|
|
sync_custom_pwa_settings
|
|
else
|
|
if ! compose_cmd exec -T --user www-data "${app_service}" php occ app:list 2>/dev/null | grep -qE '^[[:space:]]+- custom_pwa([[:space:]:]|$)'; then
|
|
echo "custom_pwa is not enabled."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if ! verify_custom_pwa_settings; then
|
|
echo "CustomPWA push settings are still incomplete after sync."
|
|
exit 1
|
|
fi
|
|
|
|
echo "CustomPWA push config verified."
|