Files
Qortal-Nextcloud-Integration/scripts/ensure-custom-pwa-push-config.sh

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."