diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c093c2b..4a5ac6d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: run: yq eval "del(.services.*.ports)" -i compose.yml - name: Run the setup script - run: ./setup.sh + run: ./setup.sh --server-ip 127.0.0.1 --force # Testing whether any containers exit requires disabling restarts - name: Remove container restart configuration diff --git a/scripts/README.md b/scripts/README.md index 3075b4e..07967b7 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,33 +1,4 @@ # Scripts -These are important scripts that are used for container setup and administration. Here is a list of each one and what it -does. - -- `internal/`: These scripts are consider internal and are run by other scripts. -- `run-in-container/`: These are scripts that, as the name suggests, are run inside containers. They should not be run - directly, but rather through the other scripts. -- `backup.sh [backup_name]`: This backs up MongoDB, PostgreSQL, MinIO, Redis, and mitmproxy data to the `backups` - directory. Run this before doing something risky with the datbases to prevent data loss. -- `compile-custom-inkay.sh [--reset]`: This compiles a custom version of the Inkay patches that whitelists the mitmproxy - certificate for the Miiverse applet. It must be run if you want to use Juxt. Run with `--reset` to reset the - certificate to Pretendo's official certificate. -- `create-juxt-community.sh [icon image path] [banner image path]`: - This creates a community in Juxtaposition, which is required before posting anything. Specify a name, description, and - linked title IDs, and optionally set a custom icon and banner image. It should be run once for each community you want - to create. -- `get-boss-keys.sh [--write]`: This validates the BOSS keys from the dumped key files in `console-files`. Run with - `--write` to validate the keys and write them to the BOSS server environment variables file. -- `make-pnid-dev.sh `: This sets the access level of a PNID to developer, which gives it - administrative permissions across the servers. For example, this makes the PNID's posts "verified" on Juxt. It should - be run after creating a new PNID that should have administrative permissions. -- `restore.sh `: This restores MongoDB, PostgreSQL, MinIO, Redis, and mitmproxy data from a specific backup - in the `backups` directory. Note that it also re-runs `setup-environment.sh` to ensure the environment is correct. -- `setup-environment.sh [Wii U IP address] [3DS IP address]`: This sets up local environment - variables in `*.local.env` files, including randomly-generated secrets. Important external secrets are logged to - `secrets.txt` in the root of the repository. Specify the server's IP address (must be accessible to the console) and, - optionally, IP addresses for the consoles for automatic FTP uploads. It should be run whenever this repository is - updated and new servers are added or if the server or Wii U IP addresses change. -- `setup-submodule-patches.sh`: This sets up the various submodules in`repos` and applies patches to them. It should - also be run whenever this repository is updated and new servers are added. -- `upload-3ds-files.sh [--reset]`: This uploads required files from `/console-files` to your 3DS to help with connection - setup. Run with `--reset` to reset the Juxt certificate to Pretendo's official certificate. +These are important scripts that are used for container setup and administration. Run each script with `-h` or `--help` +to see a description and usage information. diff --git a/scripts/backup.sh b/scripts/backup.sh index c4b855a..e774e24 100755 --- a/scripts/backup.sh +++ b/scripts/backup.sh @@ -1,34 +1,36 @@ -#! /bin/sh +#!/usr/bin/env bash -set -eu +# shellcheck source=./internal/framework.sh +source "$(dirname "$(realpath "$0")")/internal/framework.sh" +set_description "This backs up MongoDB, PostgreSQL, MinIO, Redis, and mitmproxy data to the backups directory. Run \ +this before doing something risky with the datbases to prevent data loss." +add_positional_argument "backup-name" "backup_name" "Name of the backup directory, defaults to the current date and time" false +parse_arguments "$@" -git_base=$(git rev-parse --show-toplevel) -. "$git_base/scripts/internal/function-lib.sh" - -backup_name="${1-}" -if [ -z "$backup_name" ]; then +if [[ -z "$backup_name" ]]; then backup_name="backup_$(date +%Y-%m-%dT%H.%M.%S)" fi -backup_dir="$git_base/backups/$backup_name" -if [ -d "$backup_dir" ]; then - error "Backup directory $backup_dir already exists." +backup_dir="$git_base_dir/backups/$backup_name" +if [[ -d "$backup_dir" ]]; then + print_error "Backup directory $backup_dir already exists." + exit 1 fi mkdir -p "$backup_dir" -info "Backing up to $backup_dir" +print_info "Backing up to $backup_dir" docker compose up -d mitmproxy-pretendo mongodb postgres minio redis -info "Backing up MongoDB..." +print_info "Backing up MongoDB..." docker compose exec mongodb rm -rf /tmp/backup docker compose exec mongodb mongodump -o /tmp/backup --quiet docker compose cp mongodb:/tmp/backup "$backup_dir/mongodb" docker compose exec mongodb rm -rf /tmp/backup -info "Backing up Postgres..." +print_info "Backing up Postgres..." docker compose exec postgres sh -c 'pg_dumpall -U "$POSTGRES_USER" --clean' >"$backup_dir/postgres.sql" -info "Backing up MinIO..." +print_info "Backing up MinIO..." docker compose exec minio sh -c 'mc alias set minio http://minio.pretendo.cc "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"' docker compose exec minio rm -rf /tmp/backup docker compose exec minio mkdir -p /tmp/backup @@ -36,11 +38,11 @@ docker compose exec minio mc mirror minio/ /tmp/backup docker compose cp minio:/tmp/backup "$backup_dir/minio" docker compose exec minio rm -rf /tmp/backup -info "Backing up Redis..." +print_info "Backing up Redis..." docker compose exec redis redis-cli save docker compose cp redis:/data/dump.rdb "$backup_dir/redis.rdb" -info "Backing up Mitmproxy..." +print_info "Backing up Mitmproxy..." docker compose cp mitmproxy-pretendo:/home/mitmproxy/.mitmproxy "$backup_dir/mitmproxy" -success "Backup completed successfully." +print_success "Backup completed successfully." diff --git a/scripts/compile-custom-inkay.sh b/scripts/compile-custom-inkay.sh index 20a04ba..148667b 100755 --- a/scripts/compile-custom-inkay.sh +++ b/scripts/compile-custom-inkay.sh @@ -1,31 +1,28 @@ -#! /bin/sh +#!/usr/bin/env bash -set -eu +# shellcheck source=./internal/framework.sh +source "$(dirname "$(realpath "$0")")/internal/framework.sh" +set_description "This compiles a custom version of the Inkay patches that whitelists the mitmproxy certificate for the \ +Miiverse applet. It must be run if you want to use Juxt." +add_option "-r --reset" "should_reset" "Resets the Inkay CA certificate to Pretendo's instead of using the mitmproxy certificate" +parse_arguments "$@" -should_reset=false -if [ "${1-}" = "--reset" ]; then - should_reset=true -fi - -git_base=$(git rev-parse --show-toplevel) -. "$git_base/scripts/internal/function-lib.sh" - -if [ ! -f "$git_base/environment/system.local.env" ]; then - error "Missing environment file system.local.env. Did you run setup-environment.sh?" +if [[ ! -f "$git_base_dir/environment/server.local.env" ]]; then + print_error "Missing environment file server.local.env. Did you run setup-environment.sh?" exit 1 fi -. "$git_base/environment/system.local.env" -if [ -z "${WIIU_IP+x}" ]; then - warning "Missing environment variable WIIU_IP. Did you specify a Wii U IP address when you ran setup-environment.sh?" - info "Continuing without automatic FTP upload." +source "$git_base_dir/environment/server.local.env" +if [[ -z "${WIIU_IP:-}" ]]; then + print_warning "Missing environment variable WIIU_IP. Did you specify a Wii U IP address when you ran setup-environment.sh?" + print_info "Continuing without automatic FTP upload." fi -cd "$git_base/repos/Inkay" +cd "$git_base_dir/repos/Inkay" -if [ "$should_reset" = false ]; then +if [[ -z "$should_reset" ]]; then docker compose up -d mitmproxy-pretendo while ! docker compose exec mitmproxy-pretendo ls /home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem >/dev/null 2>&1; do - info "Waiting for mitmproxy to generate a certificate..." + print_info "Waiting for mitmproxy to generate a certificate..." sleep 1 done @@ -33,10 +30,10 @@ if [ "$should_reset" = false ]; then docker compose cp mitmproxy-pretendo:/home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem ./data/ca.pem else git restore ./data/ca.pem - info "Reset Inkay CA certificate." + print_info "Reset Inkay CA certificate." fi -rm ./*.elf ./*.wps || true +rm -f ./*.elf ./*.wps # Set up the Inkay build environment and then build the patches docker build . -t inkay-build @@ -44,7 +41,9 @@ docker run -it --rm -v .:/app -w /app inkay-build success "Inkay patches built successfully at $(pwd)/Inkay-pretendo.wps" # Upload the new Inkay patches to the Wii U -if [ -n "${WIIU_IP+x}" ]; then - ftp -u "ftp://user:pass@$WIIU_IP/fs/vol/external01/wiiu/environments/aroma/plugins/Inkay-pretendo.wps" ./Inkay-pretendo.wps - info "Reboot your Wii U now to apply the new patches." +if [[ -n "${WIIU_IP:-}" ]]; then + tnftp -u "ftp://user:pass@$WIIU_IP/fs/vol/external01/wiiu/environments/aroma/plugins/Inkay-pretendo.wps" ./Inkay-pretendo.wps + print_info "Reboot your Wii U now to apply the new patches." +else + print_warning "The modified patches were not uploaded to your Wii U because you did not set an IP address." fi diff --git a/scripts/create-juxt-community.sh b/scripts/create-juxt-community.sh index 85c17b2..cd38c9f 100755 --- a/scripts/create-juxt-community.sh +++ b/scripts/create-juxt-community.sh @@ -1,30 +1,28 @@ -#! /bin/sh +#!/usr/bin/env bash -set -eu +# shellcheck source=./internal/framework.sh +source "$(dirname "$(realpath "$0")")/internal/framework.sh" +set_description "This creates a community in Juxtaposition, which is required to post anything. It should be run once \ +for each community you want to create." +add_positional_argument "name" "name" "The new community's name" true +add_positional_argument "description" "description" "The new community's description" false +add_option_with_value "t --title-ids" "title_ids" "comma-separated-title-IDs" "Comma-separated list of title IDs to include in the community, like \"1,2,3\"" false +add_option_with_value "-i --icon-path" "icon_path" "image-path" "Path to an icon image for the community" false +add_option_with_value "-b --banner-path" "banner_path" "image-path" "Path to a banner image for the community" false +parse_arguments "$@" -git_base=$(git rev-parse --show-toplevel) -. "$git_base/scripts/internal/function-lib.sh" -create_community_script=$(cat "$git_base/scripts/run-in-container/create-juxt-community.js") - -if [ "$#" -lt 3 ]; then - echo "Usage: $0 [icon image path] [banner image path]" - exit 1 -fi +create_community_script=$(cat "$git_base_dir/scripts/run-in-container/create-juxt-community.js") # Clean up title IDs by removing non-alphanumeric characters, but keep commas -title_ids=$(echo "$3" | tr -dc "a-zA-Z0-9,") +title_ids=$(echo "$title_ids" | tr -dc "a-zA-Z0-9,") docker compose up -d juxtaposition-ui -icon_path= -if [ "$#" -ge 4 ]; then - docker compose cp "$4" juxtaposition-ui:/tmp/icon - icon_path="/tmp/icon" +if [[ -n "$icon_path" ]]; then + docker compose cp "$icon_path" juxtaposition-ui:/tmp/icon fi -banner_path= -if [ "$#" -eq 5 ]; then - docker compose cp "$5" juxtaposition-ui:/tmp/banner - banner_path="/tmp/banner" +if [[ -n "$banner_path" ]]; then + docker compose cp "$banner_path" juxtaposition-ui:/tmp/banner fi -docker compose exec juxtaposition-ui node -e "$create_community_script" "$1" "$2" "$title_ids" "$icon_path" "$banner_path" +docker compose exec juxtaposition-ui node -e "$create_community_script" "$name" "$description" "$title_ids" "${icon_path:+/tmp/icon}" "${banner_path:+/tmp/banner}" diff --git a/scripts/get-boss-keys.sh b/scripts/get-boss-keys.sh index 21ad2b2..4a9c90f 100755 --- a/scripts/get-boss-keys.sh +++ b/scripts/get-boss-keys.sh @@ -1,63 +1,61 @@ -#! /bin/sh +#!/usr/bin/env bash -set -eu - -write_keys=false -if [ "${1-}" = "--write" ]; then - write_keys=true -fi +# shellcheck source=./internal/framework.sh +source "$(dirname "$(realpath "$0")")/internal/framework.sh" +set_description "This validates the BOSS keys from the dumped key files in console-files by comparing them to expected \ +MD5 hashes." +add_option "-w --write" "write_keys" "Write the encryption keys to the BOSS environment file after validating" +parse_arguments "$@" expected_ds_aes_key_hash="86fbc2bb4cb703b2a4c6cc9961319926" expected_wiiu_aes_key_hash="5202ce5099232c3d365e28379790a919" expected_wiiu_hmac_key_hash="b4482fef177b0100090ce0dbeb8ce977" -git_base=$(git rev-parse --show-toplevel) -. "$git_base/scripts/internal/function-lib.sh" -cd "$git_base/console-files" +cd "$git_base_dir/console-files" -if ls ./boss_keys.bin 1>/dev/null 2>&1; then +if [[ -f "./boss_keys.bin" ]]; then wiiu_aes_key=$(head -c 16 <./boss_keys.bin) wiiu_hmac_key=$(tail -c +33 ./boss_keys.bin | head -c 64) - wiiu_aes_key_hash=$(printf "%s" "$wiiu_aes_key" | md5sum | cut -d ' ' -f 1) - wiiu_hmac_key_hash=$(printf "%s" "$wiiu_hmac_key" | md5sum | cut -d ' ' -f 1) + wiiu_aes_key_hash=$(echo -n "$wiiu_aes_key" | md5sum | cut -d ' ' -f 1) + wiiu_hmac_key_hash=$(echo -n "$wiiu_hmac_key" | md5sum | cut -d ' ' -f 1) - if [ "$wiiu_aes_key_hash" = "$expected_wiiu_aes_key_hash" ]; then - success "Found valid Wii U BOSS AES key." - if [ "$write_keys" = true ]; then - echo "PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY=$wiiu_aes_key" >>"$git_base/environment/boss.local.env" + if [[ "$wiiu_aes_key_hash" = "$expected_wiiu_aes_key_hash" ]]; then + print_success "Found valid Wii U BOSS AES key." + if [[ -n "$write_keys" ]]; then + echo "PN_BOSS_CONFIG_BOSS_WIIU_AES_KEY=$wiiu_aes_key" >>"$git_base_dir/environment/boss.local.env" fi else - error "Wii U BOSS AES key has the wrong hash! Try re-running full_keys_dumper." + print_error "Wii U BOSS AES key has the wrong hash! Try re-running full_keys_dumper." exit 1 fi - if [ "$wiiu_hmac_key_hash" = "$expected_wiiu_hmac_key_hash" ]; then - success "Found valid Wii U HMAC key." - if [ "$write_keys" = true ]; then - echo "PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY=$wiiu_hmac_key" >>"$git_base/environment/boss.local.env" + if [[ "$wiiu_hmac_key_hash" = "$expected_wiiu_hmac_key_hash" ]]; then + print_success "Found valid Wii U HMAC key." + if [[ -n "$write_keys" ]]; then + echo "PN_BOSS_CONFIG_BOSS_WIIU_HMAC_KEY=$wiiu_hmac_key" >>"$git_base_dir/environment/boss.local.env" fi else - error "Wii U BOSS HMAC key has the wrong hash! Try re-running full_keys_dumper." + print_error "Wii U BOSS HMAC key has the wrong hash! Try re-running full_keys_dumper." exit 1 fi else - warning "console-files/boss_keys.bin (Wii U) not found! Please read the README for instructions on dumping your BOSS keys." + print_warning "console-files/boss_keys.bin (Wii U) not found! Please read the README for instructions on dumping your BOSS keys." fi -if ls ./aes_keys.txt 1>/dev/null 2>&1; then +if [[ -f "./aes_keys.txt" ]]; then ds_aes_key=$(grep -oP 'slot0x38KeyN=\K.*' ./aes_keys.txt) - ds_aes_key_bin=$(printf "%s" "$ds_aes_key" | xxd -r -p) - ds_aes_key_hash=$(printf "%s" "$ds_aes_key_bin" | md5sum | cut -d ' ' -f 1) + ds_aes_key_bin=$(echo -n "$ds_aes_key" | xxd -r -p) + ds_aes_key_hash=$(echo -n "$ds_aes_key_bin" | md5sum | cut -d ' ' -f 1) - if [ "$ds_aes_key_hash" = "$expected_ds_aes_key_hash" ]; then - success "Found valid 3DS BOSS AES key." - if [ "$write_keys" = true ]; then - echo "PN_BOSS_CONFIG_BOSS_3DS_AES_KEY=$ds_aes_key" >>"$git_base/environment/boss.local.env" + if [[ "$ds_aes_key_hash" = "$expected_ds_aes_key_hash" ]]; then + print_success "Found valid 3DS BOSS AES key." + if [[ -n "$write_keys" ]]; then + echo "PN_BOSS_CONFIG_BOSS_3DS_AES_KEY=$ds_aes_key" >>"$git_base_dir/environment/boss.local.env" fi else - error "3DS BOSS AES key has the wrong hash! Try re-running DumpKeys.gm9." + print_error "3DS BOSS AES key has the wrong hash! Try re-running DumpKeys.gm9." exit 1 fi else - warning "console-files/aes_keys.txt (3DS) not found! Please read the README for instructions on dumping your BOSS keys." + print_warning "console-files/aes_keys.txt (3DS) not found! Please read the README for instructions on dumping your BOSS keys." fi diff --git a/scripts/internal/firstrun-minio-container.sh b/scripts/internal/firstrun-minio-container.sh index 376f127..d61db84 100755 --- a/scripts/internal/firstrun-minio-container.sh +++ b/scripts/internal/firstrun-minio-container.sh @@ -1,10 +1,10 @@ -#! /bin/sh +#!/usr/bin/env bash -set -eu +# shellcheck source=./framework.sh +source "$(dirname "$(realpath "$0")")/framework.sh" +parse_arguments "$@" -git_base=$(git rev-parse --show-toplevel) -. "$git_base/scripts/internal/function-lib.sh" -minio_init_script=$(cat "$git_base/scripts/run-in-container/minio-init.sh") +minio_init_script=$(cat "$git_base_dir/scripts/run-in-container/minio-init.sh") docker compose up -d minio diff --git a/scripts/internal/firstrun-mongodb-container.sh b/scripts/internal/firstrun-mongodb-container.sh index eba9636..a8961b0 100755 --- a/scripts/internal/firstrun-mongodb-container.sh +++ b/scripts/internal/firstrun-mongodb-container.sh @@ -1,10 +1,10 @@ -#! /bin/sh +#!/usr/bin/env bash -set -eu +# shellcheck source=./framework.sh +source "$(dirname "$(realpath "$0")")/framework.sh" +parse_arguments "$@" -git_base=$(git rev-parse --show-toplevel) -. "$git_base/scripts/internal/function-lib.sh" -mongodb_init_script=$(cat "$git_base/scripts/run-in-container/mongodb-init.js") +mongodb_init_script=$(cat "$git_base_dir/scripts/run-in-container/mongodb-init.js") docker compose up -d mongodb @@ -14,7 +14,7 @@ docker compose up -d mongodb # https://github.com/docker-library/mongo/issues/339 while ! docker compose exec mongodb mongosh --eval "db.adminCommand('ping')" >/dev/null 2>&1; do - info "Waiting for mongodb to be ready..." + print_info "Waiting for mongodb to be ready..." sleep 2 done diff --git a/scripts/internal/framework.sh b/scripts/internal/framework.sh new file mode 100644 index 0000000..6e65d8d --- /dev/null +++ b/scripts/internal/framework.sh @@ -0,0 +1,314 @@ +#!/usr/bin/env bash + +# Usage: +# # shellcheck source=./internal/framework.sh +# source "$(dirname "$(realpath "$0")")/internal/framework.sh" +# # Add options and arguments here +# parse_arguments "$@" + +# Update the script path to be the correct relative path to framework.sh + +# Basic setup +set -euo pipefail + +if [[ "${BASH_SOURCE[0]}" = "${0}" ]]; then + echo "The framework script needs to be sourced, not executed." + exit 1 +fi + +# Provide the Git base directory +framework_dir="$(dirname "$(realpath "${BASH_SOURCE[0]}")")" +git_base_dir="$(cd "$framework_dir" && git rev-parse --show-toplevel)" + +# Terminal styling codes +term_reset=$(tput sgr0) +term_bold=$(tput bold) +term_underline=$(tput smul) +term_nounderline=$(tput rmul) +term_reverse=$(tput rev) +term_standout=$(tput smso) +term_nostandout=$(tput rmso) +term_dim=$(tput dim) + +term_red=$(tput setaf 1) +term_green=$(tput setaf 2) +term_yellow=$(tput setaf 3) +term_blue=$(tput setaf 4) +term_magenta=$(tput setaf 5) +term_cyan=$(tput setaf 6) +term_white=$(tput setaf 7) + +term_bgred=$(tput setab 1) +term_bggreen=$(tput setab 2) +term_bgyellow=$(tput setab 3) +term_bgblue=$(tput setab 4) +term_bgmagenta=$(tput setab 5) +term_bgcyan=$(tput setab 6) +term_bgwhite=$(tput setab 7) + +# Set up the stage counter +stage_count=1 + +# Useful output functions +print_title() { + echo "${term_bold}${term_white}${term_bgmagenta}==================== ${*} ====================${term_reset}" + # Set the terminal emulator title + printf "\033]0;%s\a" "${*}" +} + +print_header() { + echo "${term_bold}${term_cyan}---------- ${*} ----------${term_reset}" +} + +print_stage() { + print_header "Stage ${stage_count}: ${*}" + stage_count=$((stage_count + 1)) +} + +print_error() { + echo "${term_bold}${term_red}Error: ${*}${term_reset}" >&2 +} + +print_warning() { + echo "${term_bold}${term_yellow}Warning: ${*}${term_reset}" >&2 +} + +print_info() { + echo "${term_cyan}${*}${term_reset}" +} + +print_success() { + echo "${term_bold}${term_green}${*}${term_reset}" +} + +# Argument parsing framework +# Uses a lot of Bash parameter expansion tricks: https://www.gnu.org/software/bash/manual/bash.html#Shell-Parameter-Expansion +argument_variables=() +required_argument_variable_indices=() +positional_argument_variables=() +option_cases="" + +help_text_description="" +help_text_usage_arguments=() +help_text_arguments=() +help_text_argument_descriptions=() + +# Sets the description of the overall script in the help text. +# +# Usage: set_description description +# Example: set_description "This script sets up the configuration." +set_description() { + help_text_description="$*" +} + +# Adds a boolean option to the script. The variable is set to true if the option is provided or an empty string if it +# is not. The description is shown in the help text. +# +# Usage: add_option options variable_name description +# Example: add_option "-o --option" "option_enabled" "Enables the option" +add_option() { + local options="${1:?${FUNCNAME[0]}: Option arguments are required}" + local variable="${2:?${FUNCNAME[0]}: Option variable is required}" + local description="${3:?${FUNCNAME[0]}: Option description is required}" + + argument_variables+=("$variable") + option_cases+=" + ${options// /|}) + $variable=true + shift + ;;" + + help_text_usage_arguments+=("[${options// / | }]") + help_text_arguments+=("${options// /, }") + help_text_argument_descriptions+=("$description") +} + +# Adds an option that allows specifying a value to the script. The variable is set to the value provided or an empty +# string if the option is not provided. The value name and description are shown in the help text. If the option is +# required, the script will exit with an error if the option is not provided. If a default value is set, the variable +# will be set to the default value if the option is provided without a value. +# +# Usage: add_option_with_value options variable_name value_name description required default_value +# Example: add_option_with_value "-o --option" "option_value" "option-value" "Sets the option value" false "default value" +add_option_with_value() { + local options="${1:?${FUNCNAME[0]}: Option arguments are required}" + local variable="${2:?${FUNCNAME[0]}: Option variable is required}" + local value_name="${3:?${FUNCNAME[0]}: Option value name is required}" + local description="${4:?${FUNCNAME[0]}: Option description is required}" + local required="${5:?${FUNCNAME[0]}: Option requirement boolean is required}" + local default_value="${6:-}" + + argument_variables+=("$variable") + + if [[ -n "$default_value" ]]; then + # If the argument after this is another option, it should not be set as this option's value and the + # arguments should only be shifted once. If this is the last argument, shift errors can be safely ignored. + option_cases+=" + ${options// /|}) + shift + $variable=\"\${1:-}\" + if [[ \"\$$variable\" == -* ]]; then + $variable= + else + shift || true + fi + if [[ -z \"\$$variable\" ]]; then + $variable=\"$default_value\" + fi + ;;" + else + option_cases+=" + ${options// /|}) + shift + $variable=\"\${1:-}\" + if [[ \"\$$variable\" == -* ]]; then + $variable= + else + shift || true + fi + if [[ -z \"\$$variable\" ]]; then + print_error \"Option ${options// /, } requires a value for <$value_name>\" + options_error=true + fi + ;;" + fi + + if [[ "$required" = true ]]; then + required_argument_variable_indices+=($((${#argument_variables[@]} - 1))) + help_text_usage_arguments+=("${options// / | } <$value_name>") + help_text_arguments+=("${options// /, } <$value_name>") + help_text_argument_descriptions+=("$description (required)") + else + help_text_usage_arguments+=("[${options// / | } <$value_name>]") + help_text_arguments+=("${options// /, } <$value_name>") + help_text_argument_descriptions+=("$description (optional${default_value:+, default: $default_value})") + fi +} + +# Adds a positional argument to the script. The variable is set to the value provided. If the argument is required, the +# script will exit with an error if the argument is not provided. +# +# Usage: add_positional_argument name variable_name description required +# Example: add_positional_argument "file-name" "file_name" "The file to process" true +add_positional_argument() { + local name="${1:?${FUNCNAME[0]}: Argument name is required}" + local variable="${2:?${FUNCNAME[0]}: Argument variable is required}" + local description="${3:?${FUNCNAME[0]}: Argument description is required}" + local required="${4:?${FUNCNAME[0]}: Argument requirement boolean is required}" + + argument_variables+=("$variable") + positional_argument_variables+=("$variable") + + if [[ "$required" = true ]]; then + required_argument_variable_indices+=($((${#argument_variables[@]} - 1))) + help_text_usage_arguments+=("<$name>") + help_text_arguments+=("<$name>") + help_text_argument_descriptions+=("$description (required)") + else + help_text_usage_arguments+=("[<$name>]") + help_text_arguments+=("<$name>") + help_text_argument_descriptions+=("$description (optional)") + fi +} + +# Parses the provided arguments and sets the variables based on the configured options and positional arguments. Check +# if options are enabled or arguments are provided by using `if [[ -n "$option_variable" ]]`. +# +# Usage: parse_arguments "$@" +parse_arguments() { + local positional_arguments=() + local variable + for variable in "${argument_variables[@]}"; do + # Variables should start out global and empty strings + declare -g "$variable=" + done + + # Split combined short options into individual options + local split_args=() + local arg + for arg in "$@"; do + # Regex matches a single dash followed by any characters are not other dashes + if [[ "$arg" =~ ^-[^-]+$ ]]; then + for ((i = 1; i < ${#arg}; i++)); do + split_args+=("-${arg:$i:1}") + done + else + split_args+=("$arg") + fi + done + + # Replace the original arguments with the split ones + set -- "${split_args[@]}" + + while [[ $# -gt 0 ]]; do + # It is very important to use \$1 instead of $1 here to prevent command injection + eval "case \"\$1\" in + $option_cases + -*) + print_error \"Unknown option: \$1\" + options_error=true + shift + ;; + *) + positional_arguments+=(\"\$1\") + shift + ;; + esac" + done + + # Show help before validating arguments + if [[ -n "$show_help" ]]; then + show_help + exit 0 + fi + + # Validate the provided arguments + if [[ -n "${options_error:-}" ]]; then + # Errors were already displayed in the case statement + exit 1 + fi + + # Validate the number of positional arguments before setting variables to avoid going out of range + if [[ "${#positional_arguments[@]}" -gt "${#positional_argument_variables[@]}" ]]; then + print_error "Too many positional arguments provided" + exit 1 + fi + local i + for i in "${!positional_arguments[@]}"; do + declare -g "${positional_argument_variables[i]}=${positional_arguments[$i]}" + done + + for i in "${required_argument_variable_indices[@]}"; do + if [[ -z "${!argument_variables[i]}" ]]; then + print_error "Required argument not provided: ${help_text_arguments[i]}" + exit 1 + fi + done +} + +# Shows the auto-generated help text based on the configured options and positional arguments. +show_help() { + echo "Usage: $0 ${help_text_usage_arguments[*]}" + if [[ -n "$help_text_description" ]]; then + echo "Description: $help_text_description" + fi + + if [[ "${#help_text_arguments[@]}" -ne 0 ]]; then + local max_argument_length=0 + local argument + for argument in "${help_text_arguments[@]}"; do + if [[ "${#argument}" -gt "$max_argument_length" ]]; then + max_argument_length="${#argument}" + fi + done + + echo + echo "Arguments:" + local i + for i in "${!help_text_arguments[@]}"; do + printf " %-${max_argument_length}s %s\n" "${help_text_arguments[$i]}" "${help_text_argument_descriptions[$i]}" + done + fi +} + +add_option "-h --help" "show_help" "Displays this help message" diff --git a/scripts/internal/function-lib.sh b/scripts/internal/function-lib.sh deleted file mode 100644 index 2d3d96a..0000000 --- a/scripts/internal/function-lib.sh +++ /dev/null @@ -1,62 +0,0 @@ -#! /bin/sh - -# Terminal styling codes -reset=$(tput sgr0) -bold=$(tput bold) -underline=$(tput smul) -nounderline=$(tput rmul) -reverse=$(tput rev) -standout=$(tput smso) -nostandout=$(tput rmso) -dim=$(tput dim) - -red=$(tput setaf 1) -green=$(tput setaf 2) -yellow=$(tput setaf 3) -blue=$(tput setaf 4) -magenta=$(tput setaf 5) -cyan=$(tput setaf 6) -white=$(tput setaf 7) - -bgred=$(tput setab 1) -bggreen=$(tput setab 2) -bgyellow=$(tput setab 3) -bgblue=$(tput setab 4) -bgmagenta=$(tput setab 5) -bgcyan=$(tput setab 6) -bgwhite=$(tput setab 7) - -# Set up the stage counter -stage=1 - -# Useful functions -title() { - echo "${bold}${white}${bgmagenta}==================== ${*} ====================${reset}" - # Set the terminal emulator title - printf "\033]0;%s\a" "${*}" -} - -header() { - echo "${bold}${cyan}---------- ${*} ----------${reset}" -} - -stage() { - header "Stage ${stage}: ${*}" - stage=$((stage + 1)) -} - -error() { - echo "${bold}${red}Error: ${*}${reset}" >&2 -} - -warning() { - echo "${bold}${yellow}Warning: ${*}${reset}" >&2 -} - -info() { - echo "${cyan}${*}${reset}" -} - -success() { - echo "${bold}${green}${*}${reset}" -} diff --git a/scripts/internal/update-account-servers-database.sh b/scripts/internal/update-account-servers-database.sh index e1e4d09..29a45c3 100755 --- a/scripts/internal/update-account-servers-database.sh +++ b/scripts/internal/update-account-servers-database.sh @@ -1,25 +1,25 @@ -#! /bin/sh +#!/usr/bin/env bash -set -eu +# shellcheck source=./framework.sh +source "$(dirname "$(realpath "$0")")/framework.sh" +parse_arguments "$@" -git_base=$(git rev-parse --show-toplevel) -. "$git_base/scripts/internal/function-lib.sh" -create_server_script=$(cat "$git_base/scripts/run-in-container/update-account-servers-database.js") +create_server_script=$(cat "$git_base_dir/scripts/run-in-container/update-account-servers-database.js") -if [ ! -f "$git_base/environment/system.local.env" ]; then - error "Missing environment file system.local.env. Did you run setup-environment.sh?" +if [[ ! -f "$git_base_dir/environment/server.local.env" ]]; then + print_error "Missing environment file server.local.env. Did you run setup-environment.sh?" exit 1 fi -. "$git_base/environment/system.local.env" +source "$git_base_dir/environment/server.local.env" -necessary_environment_files="friends miiverse-api wiiu-chat super-mario-maker" -for environment in $necessary_environment_files; do - if [ ! -f "$git_base/environment/$environment.local.env" ]; then - error "Missing environment file $environment.local.env. Did you run setup-environment.sh?" +necessary_environment_files=("friends" "miiverse-api" "wiiu-chat" "super-mario-maker") +for environment in "${necessary_environment_files[@]}"; do + if [[ ! -f "$git_base_dir/environment/$environment.local.env" ]]; then + print_error "Missing environment file $environment.local.env. Did you run setup-environment.sh?" exit 1 fi - . "$git_base/environment/$environment.env" - . "$git_base/environment/$environment.local.env" + source "$git_base_dir/environment/$environment.env" + source "$git_base_dir/environment/$environment.local.env" done docker compose up -d account diff --git a/scripts/internal/update-miiverse-endpoints.sh b/scripts/internal/update-miiverse-endpoints.sh index c8882c7..af5e93c 100755 --- a/scripts/internal/update-miiverse-endpoints.sh +++ b/scripts/internal/update-miiverse-endpoints.sh @@ -1,10 +1,10 @@ -#! /bin/sh +#!/usr/bin/env bash -set -eu +# shellcheck source=./framework.sh +source "$(dirname "$(realpath "$0")")/framework.sh" +parse_arguments "$@" -git_base=$(git rev-parse --show-toplevel) -. "$git_base/scripts/internal/function-lib.sh" -create_endpoint_script=$(cat "$git_base/scripts/run-in-container/update-miiverse-endpoints.js") +create_endpoint_script=$(cat "$git_base_dir/scripts/run-in-container/update-miiverse-endpoints.js") docker compose up -d miiverse-api diff --git a/scripts/internal/update-postgres-password.sh b/scripts/internal/update-postgres-password.sh index 7bbddd3..2552856 100755 --- a/scripts/internal/update-postgres-password.sh +++ b/scripts/internal/update-postgres-password.sh @@ -1,26 +1,26 @@ -#! /bin/sh +#!/usr/bin/env bash -set -eu +# shellcheck source=./framework.sh +source "$(dirname "$(realpath "$0")")/framework.sh" +parse_arguments "$@" -git_base=$(git rev-parse --show-toplevel) -. "$git_base/scripts/internal/function-lib.sh" -if [ ! -f "$git_base/environment/postgres.local.env" ]; then - error "Missing environment file postgres.local.env. Did you run setup-environment.sh?" +if [[ ! -f "$git_base_dir/environment/postgres.local.env" ]]; then + print_error "Missing environment file postgres.local.env. Did you run setup-environment.sh?" exit 1 fi -. "$git_base/environment/postgres.env" -. "$git_base/environment/postgres.local.env" +source "$git_base_dir/environment/postgres.env" +source "$git_base_dir/environment/postgres.local.env" docker compose up -d postgres while ! docker compose exec postgres psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" -c "\l" >/dev/null 2>&1; do - info "Waiting for PostgreSQL to start..." + print_info "Waiting for PostgreSQL to start..." sleep 2 done # During the first run, this sometimes fails because the entrypoint script # restarts the server after running the initdb scripts while ! docker compose exec postgres psql -U "$POSTGRES_USER" -c "ALTER USER $POSTGRES_USER PASSWORD '$POSTGRES_PASSWORD';"; do - warning "Failed to change Postgres password, retrying..." + print_warning "Failed to change Postgres password, retrying..." sleep 2 done diff --git a/scripts/make-pnid-dev.sh b/scripts/make-pnid-dev.sh index 32b0d09..2c9b256 100755 --- a/scripts/make-pnid-dev.sh +++ b/scripts/make-pnid-dev.sh @@ -1,14 +1,13 @@ -#! /bin/sh +#!/usr/bin/env bash -set -eu +# shellcheck source=./internal/framework.sh +source "$(dirname "$(realpath "$0")")/internal/framework.sh" +set_description "This sets the access level of a PNID to developer, which gives it administrative permissions across the \ +servers. For example, this makes the PNID's posts \"verified\" on Juxt. It should be run after creating a new PNID that \ +should have administrative permissions." +add_positional_argument "dev-pnid" "dev_pnid" "The PNID to give developer access" true +parse_arguments "$@" -git_base=$(git rev-parse --show-toplevel) -. "$git_base/scripts/internal/function-lib.sh" -update_pnid_access_level_script=$(cat "$git_base/scripts/run-in-container/make-pnid-dev.js") +update_pnid_access_level_script=$(cat "$git_base_dir/scripts/run-in-container/make-pnid-dev.js") -if [ "$#" -lt 1 ]; then - echo "Usage: $0 " - exit 1 -fi - -docker compose exec account node -e "$update_pnid_access_level_script" "$1" +docker compose exec account node -e "$update_pnid_access_level_script" "$dev_pnid" diff --git a/scripts/restore.sh b/scripts/restore.sh index 9153a7c..67f32c7 100755 --- a/scripts/restore.sh +++ b/scripts/restore.sh @@ -1,65 +1,63 @@ -#! /bin/sh +#!/usr/bin/env bash set -eu -git_base=$(git rev-parse --show-toplevel) -. "$git_base/scripts/internal/function-lib.sh" +# shellcheck source=./internal/framework.sh +source "$(dirname "$(realpath "$0")")/internal/framework.sh" +set_description "This restores MongoDB, PostgreSQL, MinIO, Redis, and mitmproxy data from a specific backup directory. \ +Note that it also re-runs setup-environment.sh to ensure the environment is consistent." +add_positional_argument "backup-directory" "backup_dir" "The backup directory to restore from" true +add_option "-f --force" "force" "Skips the restore confirmation prompt" +parse_arguments "$@" -if [ -z "${1-}" ]; then - echo "Usage: $0 " +if [[ ! -d "$backup_dir" ]]; then + print_error "Backup directory $backup_dir does not exist." exit 1 fi +print_info "Restoring from $backup_dir" -backup_name="$1" -backup_dir="$git_base/backups/$backup_name" -if [ ! -d "$backup_dir" ]; then - error "Backup directory $backup_dir does not exist." - exit 1 -fi -info "Restoring from $backup_dir" - -if [ "${2-}" != "--force" ]; then - warning "Restoring a backup will overwrite your current Pretendo server data. Backing up your data first is strongly recommended." +print_warning "Restoring a backup will overwrite your current Pretendo server data. Backing up your data first is strongly recommended." +if [[ -z "$force" ]]; then printf "Continue? [y/N] " read -r continue - if [ "$continue" != "Y" ] && [ "$continue" != "y" ]; then + if [[ "$continue" != "Y" && "$continue" != "y" ]]; then echo "Aborting." exit 1 fi fi -info "Stopping unnecessary services..." +print_info "Stopping unnecessary services..." docker compose down docker compose up -d mitmproxy-pretendo mongodb postgres minio redis -info "Restoring MongoDB..." +print_info "Restoring MongoDB..." docker compose exec mongodb rm -rf /tmp/backup docker compose cp "$backup_dir/mongodb" mongodb:/tmp/backup docker compose exec mongodb mongorestore /tmp/backup --drop --quiet docker compose exec mongodb rm -rf /tmp/backup -info "Restoring Postgres..." +print_info "Restoring Postgres..." # According to the pg_dumpall documentation, dropping and creating the superuser role is expected to cause an error +print_info "Note: the errors \"current user cannot be dropped\" and \"role ... already exists\" are expected and can be safely ignored." docker compose exec -T postgres sh -c 'psql -U "$POSTGRES_USER" -d postgres' <"$backup_dir/postgres.sql" >/dev/null -info "Restoring MinIO..." +print_info "Restoring MinIO..." docker compose exec minio /bin/sh -c 'mc alias set minio http://minio.pretendo.cc "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD"' docker compose exec minio rm -rf /tmp/backup docker compose cp "$backup_dir/minio" minio:/tmp/backup docker compose exec minio mc mirror /tmp/backup minio/ --overwrite --remove docker compose exec minio rm -rf /tmp/backup -info "Restoring Redis..." +print_info "Restoring Redis..." # Redis cannot be running when restoring a dump or it will overwrite the restored dump when it exits docker compose stop redis docker compose cp "$backup_dir/redis.rdb" redis:/data/dump.rdb docker compose start redis -info "Restoring Mitmproxy..." +print_info "Restoring Mitmproxy..." docker compose cp "$backup_dir/mitmproxy" mitmproxy-pretendo:/home/mitmproxy/.mitmproxy -# The restored Postgres backup might be using a different password than what is currently in the .env files -info "Updating the Postgres password from the environment..." -"$git_base/scripts/internal/update-postgres-password.sh" +# The restored backup might be using different secrets than what are currently in the .env files +"$git_base_dir/scripts/setup-environment.sh" --force -success "Restore completed successfully." +print_success "Restore completed successfully." diff --git a/scripts/run-in-container/create-juxt-community.js b/scripts/run-in-container/create-juxt-community.js index e1f6446..7dc5867 100644 --- a/scripts/run-in-container/create-juxt-community.js +++ b/scripts/run-in-container/create-juxt-community.js @@ -14,13 +14,6 @@ const s3 = new S3({ }); async function runAsync() { - if (process.argv.length < 4) { - console.log( - "Usage: [icon image path] [banner image path]" - ); - process.exit(1); - } - await connect(); await createMainCommunity( @@ -39,14 +32,7 @@ runAsync().then(() => { process.exit(0); }); -async function createMainCommunity( - platform_id, - name, - description, - title_ids, - iconPath, - bannerPath -) { +async function createMainCommunity(platform_id, name, description, title_ids, iconPath, bannerPath) { const communityId = Math.floor(Math.random() * 1000000) + 1; const newCommunity = new COMMUNITY({ @@ -95,6 +81,7 @@ async function uploadAssets(community_id, iconPath, bannerPath) { }; await s3.putObject(uploadParams).promise(); } + await fs.rm(iconPath); } if (bannerPath) { @@ -118,5 +105,6 @@ async function uploadAssets(community_id, iconPath, bannerPath) { }; await s3.putObject(uploadParams).promise(); } + await fs.rm(bannerPath); } } diff --git a/scripts/run-in-container/minio-init.sh b/scripts/run-in-container/minio-init.sh index bb7a58a..9c978f1 100644 --- a/scripts/run-in-container/minio-init.sh +++ b/scripts/run-in-container/minio-init.sh @@ -1,4 +1,4 @@ -#! /bin/sh +#!/bin/sh set -eu diff --git a/scripts/run-in-container/postgres-init.sh b/scripts/run-in-container/postgres-init.sh index e02ef0e..80d463c 100644 --- a/scripts/run-in-container/postgres-init.sh +++ b/scripts/run-in-container/postgres-init.sh @@ -1,4 +1,4 @@ -#! /bin/sh +#!/bin/sh set -eu diff --git a/scripts/setup-environment.sh b/scripts/setup-environment.sh index 349f86e..d057943 100755 --- a/scripts/setup-environment.sh +++ b/scripts/setup-environment.sh @@ -1,6 +1,16 @@ -#! /bin/sh +#!/usr/bin/env bash -set -eu +# shellcheck source=./internal/framework.sh +source "$(dirname "$(realpath "$0")")/internal/framework.sh" +set_description "This sets up local environment variables in the *.local.env files, including randomly-generated \ +secrets. Important external secrets are written to secrets.txt in the root of the repository. By default, the script \ +re-uses existing configuration values from server.local.env unless at least one is passed as an argument." +add_option_with_value "-s --server-ip" "server_ip" "ip-address" "The IP address of the Pretendo Network server (must be accessible to the clients)" false +add_option_with_value "-w --wiiu-ip" "wiiu_ip" "ip-address" "The IP address of the Wii U for FTP uploads" false +add_option_with_value "-3 --3ds-ip" "ds_ip" "ip-address" "The IP address of the 3DS for FTP uploads" false +add_option "-n --no-environment" "no_environment" "Disables reading existing configuration values from the environment" +add_option "-f --force" "force" "Skip the confirmation prompt and always overwrite existing local environment files" +parse_arguments "$@" generate_password() { length=$1 @@ -12,34 +22,34 @@ generate_hex() { head /dev/urandom | tr -dc "A-F0-9" | head -c "$length" } -# Validate arguments -if [ "$#" -lt 1 ]; then - echo "Usage: $0 [Wii U IP address] [3DS IP address]" +cd "$git_base_dir/environment" + +print_info "Setting up local environment variables..." + +if [[ -z "$no_environment" ]]; then + if [[ -f "./server.local.env" ]]; then + source ./server.local.env + fi + # Copy the configuration from server.local.env if necessary + : "${server_ip:=${SERVER_IP:-}}" + : "${wiiu_ip:=${WIIU_IP:-}}" + : "${ds_ip:=${DS_IP:-}}" +fi + +if [[ -z "$server_ip" ]]; then + print_error "A server IP address was neither passed as an argument nor found in the environment. Please provide one." exit 1 fi -server_ip=$1 -wiiu_ip= -if [ "$#" -ge 2 ]; then - wiiu_ip=$2 -fi -ds_ip= -if [ "$#" -ge 3 ]; then - ds_ip=$3 -fi -git_base=$(git rev-parse --show-toplevel) -. "$git_base/scripts/internal/function-lib.sh" -cd "$git_base/environment" - -info "Setting up local environment variables..." - -if ls ./*.local.env 1>/dev/null 2>&1; then - warning "Local environment files already exist. They will be overwritten if you continue." - printf "Continue? [y/N] " - read -r continue - if [ "$continue" != "Y" ] && [ "$continue" != "y" ]; then - echo "Aborting." - exit 1 +if [[ -n "$(echo ./*.local.env)" ]]; then + print_warning "Local environment files already exist. They will be overwritten if you continue." + if [[ -z "$force" ]]; then + printf "Continue? [y/N] " + read -r continue + if [[ "$continue" != "Y" && "$continue" != "y" ]]; then + echo "Aborting." + exit 1 + fi fi docker compose down @@ -112,33 +122,33 @@ boss_api_key=$(generate_password 32) echo "PN_BOSS_CONFIG_GRPC_BOSS_SERVER_API_KEY=$boss_api_key" >>./boss.local.env # Set up the server IP address -info "Using server IP address $server_ip." -echo "SERVER_IP=$server_ip" >>./system.local.env +print_info "Using server IP address $server_ip." +echo "SERVER_IP=$server_ip" >>./server.local.env echo "PN_FRIENDS_SECURE_SERVER_HOST=$server_ip" >>./friends.local.env echo "PN_WIIU_CHAT_SECURE_SERVER_LOCATION=$server_ip" >>./wiiu-chat.local.env echo "PN_SMM_SECURE_SERVER_HOST=$server_ip" >>./super-mario-maker.local.env # Get the Wii U IP address -if [ -n "$wiiu_ip" ]; then - info "Using Wii U IP address $wiiu_ip." - echo "WIIU_IP=$wiiu_ip" >>./system.local.env +if [[ -n "$wiiu_ip" ]]; then + print_info "Using Wii U IP address $wiiu_ip." + echo "WIIU_IP=$wiiu_ip" >>./server.local.env else - info "Skipping Wii U IP address." + print_info "Skipping Wii U IP address." fi # Get the 3DS IP address -if [ -n "$ds_ip" ]; then - info "Using 3DS IP address $ds_ip." - echo "DS_IP=$ds_ip" >>./system.local.env +if [[ -n "$ds_ip" ]]; then + print_info "Using 3DS IP address $ds_ip." + echo "DS_IP=$ds_ip" >>./server.local.env else - info "Skipping 3DS IP address." + print_info "Skipping 3DS IP address." fi # Get the BOSS keys -"$git_base"/scripts/get-boss-keys.sh --write +"$git_base_dir/scripts/get-boss-keys.sh" --write # Create a list of important secrets -cat >"$git_base/secrets.txt" <"$git_base_dir/secrets.txt" </dev/null -git submodule foreach --recursive "git reset --hard" >/dev/null -git submodule foreach --recursive "git clean -fd" >/dev/null -git submodule update --init --recursive --checkout --force >/dev/null +print_info "Resetting all submodules..." +git submodule sync >/dev/null +git submodule foreach "git reset --hard" >/dev/null +git submodule foreach "git clean -fd" >/dev/null +git submodule update --init --checkout >/dev/null +if [[ -n "$update_remote" ]]; then + print_info "Updating submodules from their remotes..." + git submodule update --remote +fi -info "Applying patches to submodules..." -num_patches=0 -for dir in "$git_base/patches/"*; do - if [ -d "$dir" ]; then +print_info "Applying patches to submodules..." +patch_count=0 +error_count=0 +for dir in "$git_base_dir/patches/"*; do + if [[ -d "$dir" ]]; then subdir=$(basename "$dir") - cd "$git_base/repos/$subdir" + cd "$git_base_dir/repos/$subdir" - for patch in "$git_base/patches/$subdir"/*; do - git apply "$patch" - num_patches=$((num_patches + 1)) + for patch in "$git_base_dir/patches/$subdir"/*; do + if [[ -n "$update_remote" ]]; then + print_info "Applying patch $patch..." + if ! git apply --reject "$patch"; then + print_error "Failed to apply patch $patch! Attempting 3-way merge..." + git add . + if ! git apply --3way "$patch"; then + print_error "There are merge conflicts with the patch that need to be resolved manually." + else + print_success "Successfully applied the patch with a 3-way merge. Make sure to re-generate the patch." + fi + error_count=$((error_count + 1)) + fi + else + git apply "$patch" + fi + patch_count=$((patch_count + 1)) done git add . fi done -success "Successfully applied $num_patches patches." + +if [[ "$error_count" -gt 0 ]]; then + print_error "Failed to apply $error_count patches out of $patch_count." + exit 1 +fi +print_success "Successfully applied $patch_count patches." diff --git a/scripts/upload-3ds-files.sh b/scripts/upload-3ds-files.sh index 0d15b09..425e7dd 100755 --- a/scripts/upload-3ds-files.sh +++ b/scripts/upload-3ds-files.sh @@ -1,48 +1,47 @@ -#! /bin/sh +#!/usr/bin/env bash -set -eu +# shellcheck source=./internal/framework.sh +source "$(dirname "$(realpath "$0")")/internal/framework.sh" +set_description "This uploads required files from console-files to your 3DS to connect to your Pretendo server." +add_option "-r --reset" "should_reset" "Reset the Juxt certificate to Pretendo's instead of using the mitmproxy certificate" +parse_arguments "$@" -should_reset=false -if [ "${1-}" = "--reset" ]; then - should_reset=true -fi - -git_base=$(git rev-parse --show-toplevel) -. "$git_base/scripts/internal/function-lib.sh" -cd "$git_base" - -if [ ! -f "$git_base/environment/system.local.env" ]; then - error "Missing environment file system.local.env. Did you run setup-environment.sh?" +if [[ ! -f "$git_base_dir/environment/server.local.env" ]]; then + print_error "Missing environment file server.local.env. Did you run setup-environment.sh?" exit 1 fi -. "$git_base/environment/system.local.env" -if [ -z "${DS_IP+x}" ]; then - warning "Missing environment variable DS_IP. Did you specify a 3DS IP address when you ran setup-environment.sh?" - info "Continuing without automatic FTP upload." +source "$git_base_dir/environment/server.local.env" +if [[ -z "${DS_IP:-}" ]]; then + print_warning "Missing environment variable DS_IP. Did you specify a 3DS IP address when you ran setup-environment.sh?" + print_info "Continuing without automatic FTP upload." fi -if [ "$should_reset" = false ]; then +cd "$git_base_dir/console-files" + +if [[ -z "$should_reset" ]]; then docker compose up -d mitmproxy-pretendo while ! docker compose exec mitmproxy-pretendo ls /home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem >/dev/null 2>&1; do - info "Waiting for mitmproxy to generate a certificate..." + print_info "Waiting for mitmproxy to generate a certificate..." sleep 1 done # Get the current certificate - docker compose cp mitmproxy-pretendo:/home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem ./console-files/mitmproxy-ca-cert.pem + docker compose cp mitmproxy-pretendo:/home/mitmproxy/.mitmproxy/mitmproxy-ca-cert.pem ./mitmproxy-ca-cert.pem else - info "Reset the Juxt certificate." + print_info "Reset the Juxt certificate." fi # Upload the required files -if [ -n "${DS_IP+x}" ]; then - if [ "$should_reset" = false ]; then - ftp -u "ftp://user:pass@$DS_IP:5000/3ds/juxt-prod.pem" ./console-files/mitmproxy-ca-cert.pem - ftp -u "ftp://user:pass@$DS_IP:5000/gm9/scripts/FriendsAccountSwitcher.gm9" ./console-files/FriendsAccountSwitcher.gm9 - ftp -u "ftp://user:pass@$DS_IP:5000/3ds/ResetFriendsTestAccount.3dsx" ./console-files/ResetFriendsTestAccount.3dsx +if [[ -n "${DS_IP:-}" ]]; then + if [[ -z "$should_reset" ]]; then + tnftp -u "ftp://user:pass@$DS_IP:5000/3ds/juxt-prod.pem" ./mitmproxy-ca-cert.pem + tnftp -u "ftp://user:pass@$DS_IP:5000/gm9/scripts/FriendsAccountSwitcher.gm9" ./FriendsAccountSwitcher.gm9 + tnftp -u "ftp://user:pass@$DS_IP:5000/3ds/ResetFriendsTestAccount.3dsx" ./ResetFriendsTestAccount.3dsx else - ftp -u "ftp://user:pass@$DS_IP:5000/3ds/juxt-prod.pem" ./console-files/juxt-prod.pem + tnftp -u "ftp://user:pass@$DS_IP:5000/3ds/juxt-prod.pem" ./juxt-prod.pem fi - success "Successfully uploaded the required files to your 3DS." + print_success "Successfully uploaded the required files to your 3DS." +else + print_warning "The required files were not uploaded to your 3DS because you did not set an IP address." fi diff --git a/setup.sh b/setup.sh index 8a27c66..7294433 100755 --- a/setup.sh +++ b/setup.sh @@ -1,131 +1,127 @@ -#! /bin/sh +#!/usr/bin/env bash -set -eu - -check_git_repository() { - # Temporary function because function-lib isn't sourced yet - error() { - echo "$(tput bold)$(tput setaf 1)Error: ${*}$(tput sgr0)" >&2 - } - - if ! git --version >/dev/null; then - error "Git is not installed. Please install it: https://git-scm.com/downloads/" - exit 1 - fi - if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then - error "This script must be run from inside the pretendo-docker Git repository." - echo "Make sure that you cloned the repository with Git, not downloaded it as a ZIP file from GitHub." - exit 1 - fi - if [ "$(basename -s .git "$(git config --get remote.origin.url)")" != "pretendo-docker" ]; then - error "This script must be run from inside the pretendo-docker Git repository." - echo "It looks like you are running it from a different Git repository at $(pwd) from $(git config --get remote.origin.url)." - exit 1 - fi - - git config --local submodule.recurse true -} +set -euo pipefail check_prerequisites() { - prerequisites_failed=false - prerequisites_warning=false + prerequisites_failed= + prerequisites_warning= if ! docker version >/dev/null; then - error "Docker is not installed. Please install it: https://docs.docker.com/get-docker/" - info "If you see a \"Permission denied while trying to connect to the Docker daemon\" error, you need to run this script with sudo." + print_error "Docker is not installed. Please install it: https://docs.docker.com/get-docker/" + print_info "If you see a \"Permission denied while trying to connect to the Docker daemon\" error, you need to \ +add your user to the docker group: https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user." prerequisites_failed=true fi if ! docker compose version >/dev/null; then - error "Docker Compose is not installed. Please install it: https://docs.docker.com/compose/install/" + print_error "Docker Compose is not installed. Please install it: https://docs.docker.com/compose/install/" prerequisites_failed=true fi # The tnftp "enhanced ftp client" has the -u option for direct uploads, # unlike the netkit-ftp "classical ftp client" if ! command -v tnftp >/dev/null; then - warning "tnftp is not installed. You will not be able to upload files to your consoles automatically." + print_warning "tnftp is not installed. You will not be able to upload files to your consoles automatically." prerequisites_warning=true fi - if [ "$prerequisites_failed" = true ]; then - error "Prerequisites check failed. Please install the missing prerequisites and try again." + if [[ "$prerequisites_failed" = true ]]; then + print_error "Prerequisites check failed. Please install the missing prerequisites and try again." exit 1 - elif [ "$prerequisites_warning" = true ]; then - warning "Prerequisites check completed with warnings." + elif [[ "$prerequisites_warning" = true ]]; then + print_warning "Prerequisites check completed with warnings." - if [ -z "${CI+x}" ]; then - # Optional prerequisites can be ignored in CI + if [[ -z "$force" ]]; then printf "Do you want to continue anyway (y/N)? " read -r continue_anyway - if [ "$continue_anyway" != "Y" ] && [ "$continue_anyway" != "y" ]; then + if [[ "$continue_anyway" != "Y" && "$continue_anyway" != "y" ]]; then + echo "Aborting." exit 1 fi fi else - success "All prerequisites are installed." + print_success "All prerequisites are installed." fi } setup_environment_variables() { - if [ -n "${CI+x}" ]; then - warning "It looks like the script is running in CI. Using test IP addresses." - server_ip="0.0.0.0" - wiiu_ip="" - ds_ip="" - else + if [[ -n "$reconfigure" || (-z "$server_ip" && ! -f "$git_base_dir/environment/server.local.env") ]]; then echo "Enter the IP address of your Pretendo Network server. It must be accessible to your console." read -r server_ip - echo "Enter the IP address of your Wii U (optional). It is only used for automatic FTP uploads of modified Inkay patches." + echo "Enter the IP address of your Wii U (optional). It is used for automatic FTP uploads." read -r wiiu_ip - echo "Enter the IP address of your 3DS (optional). It is only used for automatic FTP uploads." + echo "Enter the IP address of your 3DS (optional). It is used for automatic FTP uploads." read -r ds_ip fi - ./scripts/setup-environment.sh "$server_ip" "$wiiu_ip" "$ds_ip" + ./scripts/setup-environment.sh "$server_ip" "$wiiu_ip" "$ds_ip" ${force:+--force} ${reconfigure:+--no-environment} } setup_containers() { - info "Setting up MongoDB container..." + print_info "Setting up MongoDB container..." ./scripts/internal/firstrun-mongodb-container.sh - info "Setting up MinIO container..." + print_info "Setting up MinIO container..." ./scripts/internal/firstrun-minio-container.sh - info "Setting up Pretendo account servers database..." + print_info "Setting up Pretendo account servers database..." ./scripts/internal/update-account-servers-database.sh - info "Setting up Pretendo Miiverse endpoints database..." + print_info "Setting up Pretendo Miiverse endpoints database..." ./scripts/internal/update-miiverse-endpoints.sh - info "Updating Postgres password..." + print_info "Updating Postgres password..." ./scripts/internal/update-postgres-password.sh - info "Stopping containers after initial setup..." + print_info "Stopping containers after initial setup..." docker compose down } export PRETENDO_SETUP_IN_PROGRESS=true -check_git_repository +# Temporary function because the framework script isn't sourced yet and we don't know if tput is available +print_error() { + echo -e "\e[1;31mError: ${*}\e[0m" >&2 +} -git_base=$(git rev-parse --show-toplevel) -cd "$git_base" -. "$git_base/scripts/internal/function-lib.sh" +# The framework script requires git and tput, so check for them first +if ! tput setaf 0 >/dev/null; then + print_error "Either the tput command is not installed, or your \$TERM environment variable is not set correctly. \ +Please install your distribution's ncurses package (such as ncurses-bin) and/or configure your terminal to set \$TERM." + exit 1 +fi +if ! git --version >/dev/null; then + print_error "Git is not installed. Please install it: https://git-scm.com/downloads/" + exit 1 +fi -title "Unofficial Pretendo Network setup script" -header "Pretendo setup script started at $(date)." +git config --local submodule.recurse true -stage "Checking prerequisites." +# shellcheck source=./scripts/internal/framework.sh +source "$(dirname "$(realpath "$0")")/scripts/internal/framework.sh" +set_description "This is the main setup script for your self-hosted Pretendo Network server. By default, it will prompt \ +for configuration values the first run and re-use those values for future runs." +add_option "-r --reconfigure" "reconfigure" "Always shows configuration prompts, even if the values are already set. \ +Also disables reading configuration values from the environment." +add_option "-f --force" "force" "Ignores warnings and confirmation prompts during the setup process." +add_option_with_value "-s --server-ip" "server_ip" "IP-address" "The IP address of your Pretendo Network server. It \ +must be accessible to your console. Disables interactive prompts by default, unless --force-interactive is specified." false +add_option_with_value "-w --wiiu-ip" "wiiu_ip" "IP-address" "The IP address of your Wii U. It is used for automatic FTP uploads." false +add_option_with_value "-3 --3ds-ip" "ds_ip" "IP-address" "The IP address of your 3DS. It is used for automatic FTP uploads." false +parse_arguments "$@" + +print_title "Unofficial Pretendo Network server setup script started" + +print_stage "Checking prerequisites." check_prerequisites -stage "Setting up submodules and applying patches." +print_stage "Setting up submodules and applying patches." ./scripts/setup-submodule-patches.sh -stage "Setting up environment variables." +print_stage "Setting up environment variables." setup_environment_variables -stage "Pulling Docker images." +print_stage "Pulling Docker images." docker compose pull -stage "Building Docker images." +print_stage "Building Docker images." docker compose build -stage "Setting up containers with first-run scripts." +print_stage "Setting up containers with first-run scripts." setup_containers -success "Setup completed! You can now start your Pretendo Network server with \"docker compose up -d\"." -header "Pretendo setup script finished at $(date)." +print_title "Pretendo Network server setup script finished" +print_success "Setup completed! You can now start your Pretendo server with \"docker compose up -d\"."