#!/bin/bash # Matrix (Synapse + Element) install hooks. # # Synapse takes essentially no configuration from the environment — it reads # homeserver.yaml and nothing else — so the real install work is done here: # generate the signing key, write homeserver.yaml from the template with the # server name and secrets filled in, write Element's config.json, and create the # first admin account once the homeserver is answering. matrix_install_pre() { local app_name="$1" if ! appInstallCheckRequirements "$app_name" "$CFG_MATRIX_REQUIRES"; then matrix=n return 1 fi } # The homeserver's public host — the value that becomes server_name. # # Built from the port arrays and $domain_full that variables_init_app has # already put in scope, NOT by reading DOMAINSUBNAME_TAG_1 out of the deployed # compose: install_post_compose runs before dockerConfigSetupFileWithData, so at # that point the compose still holds raw placeholders. # # port_subdomains[0] is CFG_MATRIX_PORT_1, the Synapse router — deliberately not # $host_setup, which for this two-host app resolves to Element instead. The # empty/@/root cases mirror tagsProcessorPortSubdomains so the name computed # here and the Traefik rule generated later cannot drift apart. _matrixServerName() { local sub="${port_subdomains[0]}" [[ -z "$domain_full" ]] && return 1 if [[ "$sub" == "@" || "$sub" == "root" ]]; then echo "$domain_full" elif [[ -n "$sub" ]]; then echo "${sub}.${domain_full}" else echo "matrix.${domain_full}" fi } matrix_install_post_compose() { local app_name="$1" local app_dir="$containers_dir$app_name" local data_dir="$app_dir/data" ((menu_number++)) echo "" echo "---- $menu_number. Generating the Synapse homeserver configuration" echo "" local server_name server_name=$(_matrixServerName) if [[ -z "$server_name" ]]; then isError "No domain is configured — cannot determine the Matrix server_name." isNotice "Set CFG_DOMAIN_${CFG_MATRIX_DOMAIN:-1} (General → Network), then reinstall." return 1 fi # Same value the compose will hand to Postgres. Taken from the config rather # than the compose because the compose is not substituted yet at this point; # the config is where the secret is generated and remembered, and the # #LIBREPORTAL|MATRIX_DB_PASSWORD_1_TAG| line is filled from this very # variable a step later, so the two cannot disagree. local db_password="$CFG_MATRIX_DB_PASSWORD_1" if [[ -z "$db_password" || "$db_password" == RANDOMIZEDPASSWORD* ]]; then isError "CFG_MATRIX_DB_PASSWORD_1 was not generated — aborting Synapse configuration." return 1 fi local result result=$(createFolders "loud" "$docker_install_user" "$data_dir" "$app_dir/element") checkSuccess "Creating $app_name data folders" # Synapse signs every federated event with this key, and a peer that has # seen one key will reject events signed by a different one. So: generate it # exactly once, and never regenerate it over an existing install. # # Upstream's `generate` command is used rather than hand-rolling the key # file, because the format encodes a key ID that other servers cache. It # also emits a homeserver.yaml and a log config, which we throw away in # favour of the templates below. if [[ ! -s "$data_dir/signing.key" ]]; then local synapse_image synapse_image=$(tagsManagerGetTagContent "$app_dir/docker-compose.yml" "MATRIX_VERSION_TAG") synapse_image="matrixdotorg/synapse:${synapse_image:-latest}" result=$(runFileOp docker run --rm \ -e SYNAPSE_SERVER_NAME="$server_name" \ -e SYNAPSE_REPORT_STATS=no \ -v "$data_dir":/data \ "$synapse_image" generate 2>&1) checkSuccess "Generating the Synapse signing key with $synapse_image" # `generate` names the key after the server; homeserver.yaml expects it # at a fixed path so the file does not have to be renamed if the app is # ever restored under a different name. if [[ -f "$data_dir/$server_name.signing.key" ]]; then result=$(runFileOp mv "$data_dir/$server_name.signing.key" "$data_dir/signing.key") checkSuccess "Storing the signing key at data/signing.key" fi # Ours replace both of these. result=$(runFileOp rm -f "$data_dir/homeserver.yaml" "$data_dir/$server_name.log.config") checkSuccess "Discarding the generated config in favour of the LibrePortal template" else isNotice "An existing signing key was found — keeping it (regenerating would break federation)." fi if [[ ! -s "$data_dir/signing.key" ]]; then isError "No signing key was produced — Synapse will not start. Check that the image could be pulled." return 1 fi result=$(copyResource "$app_name" "homeserver.yaml" "data" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) checkSuccess "Copying homeserver.yaml to $data_dir" result=$(copyResource "$app_name" "log.config" "data" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) checkSuccess "Copying log.config to $data_dir" local homeserver_file="$data_dir/homeserver.yaml" # Three independent secrets, each generated fresh. registration_shared_secret # can mint an account on this server, so it is as sensitive as an admin # password — it is why homeserver.yaml is chmod 600 below. local registration_secret macaroon_secret form_secret registration_secret=$(openssl rand -hex 32) macaroon_secret=$(openssl rand -hex 32) form_secret=$(openssl rand -hex 32) local enable_registration="false" [[ "$CFG_MATRIX_ENABLE_REGISTRATION" == "true" ]] && enable_registration="true" runFileOp sed -i "s|SYNAPSE_SERVER_NAME_PLACEHOLDER|$server_name|g" "$homeserver_file" runFileOp sed -i "s|SYNAPSE_DB_PASSWORD_PLACEHOLDER|$db_password|g" "$homeserver_file" runFileOp sed -i "s|SYNAPSE_REGISTRATION_SECRET_PLACEHOLDER|$registration_secret|g" "$homeserver_file" runFileOp sed -i "s|SYNAPSE_MACAROON_SECRET_PLACEHOLDER|$macaroon_secret|g" "$homeserver_file" runFileOp sed -i "s|SYNAPSE_FORM_SECRET_PLACEHOLDER|$form_secret|g" "$homeserver_file" runFileOp sed -i "s|SYNAPSE_ENABLE_REGISTRATION_PLACEHOLDER|$enable_registration|g" "$homeserver_file" checkSuccess "Writing homeserver.yaml (server_name=$server_name registration=$enable_registration)" runFileOp chmod 600 "$homeserver_file" "$data_dir/signing.key" runFileOp chown -R "$docker_install_user":"$docker_install_user" "$data_dir" checkSuccess "Restricting permissions on the Synapse secrets" # Element is a static bundle; config.json is the only thing that makes it # point at this homeserver rather than matrix.org. result=$(copyResource "$app_name" "element-config.json" "element" | runInstallWrite -a "$logs_dir/$docker_log_file" 2>&1) checkSuccess "Copying Element configuration to $app_dir/element" result=$(runFileOp mv "$app_dir/element/element-config.json" "$app_dir/element/config.json") checkSuccess "Renaming Element configuration to config.json" runFileOp sed -i "s|ELEMENT_HOMESERVER_PLACEHOLDER|$server_name|g" "$app_dir/element/config.json" runFileOp chown -R "$docker_install_user":"$docker_install_user" "$app_dir/element" checkSuccess "Pointing Element at https://$server_name" } matrix_install_post_start() { local app_name="$1" ((menu_number++)) echo "" echo "---- $menu_number. Creating the first Matrix admin account" echo "" # Synapse runs its database migrations on first boot, which on an empty # Postgres takes appreciably longer than the container takes to start. # /health answers only once it is actually serving. # # Probed with python rather than curl: the Synapse image is debian-slim with # no curl or wget in it, but python is what Synapse itself runs on, so it is # always there. local attempts=0 while ((attempts < 60)); do if runFileOp docker exec matrix-synapse python -c \ "import urllib.request; urllib.request.urlopen('http://localhost:8008/health', timeout=5)" >/dev/null 2>&1; then break fi sleep 2 ((attempts++)) done if ((attempts >= 60)); then isError "Synapse did not become ready in time — no admin account was created." isNotice "Check 'docker logs matrix-synapse'. Once it is up, create the account with:" isNotice " docker exec -it matrix-synapse register_new_matrix_user -c /data/homeserver.yaml http://localhost:8008" return 0 fi local admin_user="${CFG_MATRIX_ADMIN_USERNAME:-admin}" local admin_pass="${CFG_MATRIX_ADMIN_PASSWORD_1}" if [[ -z "$admin_pass" || "$admin_pass" == RANDOMIZEDPASSWORD* ]]; then isNotice "No admin password is set in matrix.config — skipping admin account creation." return 0 fi # Idempotent in practice: on a reinstall over existing data the account # already exists and register_new_matrix_user fails with "User ID already # taken", which is not worth failing the install over. local result result=$(runFileOp docker exec matrix-synapse register_new_matrix_user \ -u "$admin_user" -p "$admin_pass" -a \ -c /data/homeserver.yaml http://localhost:8008 2>&1) if [[ "$result" == *"already taken"* ]]; then isNotice "Matrix admin '$admin_user' already exists — leaving the existing account alone." else checkSuccess "Creating Matrix admin account '$admin_user'" fi } matrix_install_post() { local app_name="$1" local server_name server_name=$(_matrixServerName) local admin_user="${CFG_MATRIX_ADMIN_USERNAME:-admin}" echo "" isNotice "Matrix homeserver:" echo "" echo " Server name : ${server_name}" echo " Your user ID : @${admin_user}:${server_name}" echo " Password : ${CFG_MATRIX_ADMIN_PASSWORD_1}" echo "" echo " Sign in through the Element web interface, or any Matrix client" echo " (Element mobile/desktop, FluffyChat, Nheko) using the server name" echo " above." echo "" echo " Registration is ${CFG_MATRIX_ENABLE_REGISTRATION:-false}. To invite" echo " others while it stays closed, create their accounts with:" echo " docker exec -it matrix-synapse register_new_matrix_user \\" echo " -c /data/homeserver.yaml http://localhost:8008" echo "" }