diff --git a/rootfs/standard/usr/bin/mynode_post_upgrade.sh b/rootfs/standard/usr/bin/mynode_post_upgrade.sh index 92718e323..ee1d28de6 100755 --- a/rootfs/standard/usr/bin/mynode_post_upgrade.sh +++ b/rootfs/standard/usr/bin/mynode_post_upgrade.sh @@ -585,6 +585,13 @@ if [ -f $LIT_VERSION_FILE ]; then CURRENT=$(cat $LIT_VERSION_FILE) fi if [ "$CURRENT" != "$LIT_VERSION" ]; then + # App-only reinstall flows skip the base-upgrade block above, including its + # signing-key imports. Ensure the release key is present before verifying + # Lightning Terminal so reinstalling an unrelated app cannot fail here. + if ! gpg --list-keys C20A78516A0944900EBFCA29961CC8259AE675D4 >/dev/null 2>&1; then + gpg --keyserver hkps://keyserver.ubuntu.com --recv-keys C20A78516A0944900EBFCA29961CC8259AE675D4 + fi + # Download and install lit rm -rf /opt/download mkdir -p /opt/download @@ -1289,4 +1296,4 @@ sync # Done! echo "UPGRADE COMPLETE!!!" -date \ No newline at end of file +date diff --git a/rootfs/standard/usr/share/mynode_apps/canary/app_data/docker-compose.yml b/rootfs/standard/usr/share/mynode_apps/canary/app_data/docker-compose.yml index dcede9f5b..2659d86f1 100644 --- a/rootfs/standard/usr/share/mynode_apps/canary/app_data/docker-compose.yml +++ b/rootfs/standard/usr/share/mynode_apps/canary/app_data/docker-compose.yml @@ -2,7 +2,8 @@ version: "3.8" services: backend: - image: canary-backend:latest + image: schjonhaug/canary-backend:v1.6.2 + user: "${CANARY_HOST_UID}:${CANARY_HOST_GID}" network_mode: host restart: unless-stopped stop_grace_period: 30s @@ -16,9 +17,10 @@ services: CANARY_NETWORK: mainnet CANARY_MODE: self-hosted CANARY_BIND_ADDRESS: 127.0.0.1:3004 + FRONTEND_URL: http://mynode.local:3005 frontend: - image: canary-frontend:latest + image: schjonhaug/canary-frontend:v1.6.2 network_mode: host restart: unless-stopped stop_grace_period: 30s diff --git a/rootfs/standard/usr/share/mynode_apps/canary/canary.json b/rootfs/standard/usr/share/mynode_apps/canary/canary.json index 03df14543..988177fda 100644 --- a/rootfs/standard/usr/share/mynode_apps/canary/canary.json +++ b/rootfs/standard/usr/share/mynode_apps/canary/canary.json @@ -1,22 +1,22 @@ { - "name": "Canary", + "name": "Canary Wallet", "short_name": "canary", "author": { - "name": "Canary Team", + "name": "Canary Wallet Team", "link": "https://github.com/schjonhaug/canary" }, "website": { - "name": "Canary", + "name": "Canary Wallet", "link": "https://github.com/schjonhaug/canary" }, "category": "bitcoin_app", "short_description": "Wallet Monitor", "description": [ - "Canary is a Bitcoin wallet monitoring and early warning system for cold storage.", + "Canary Wallet is a Bitcoin wallet monitoring and early warning system for cold storage.", "Get instant notifications when your bitcoins move via ntfy push notifications.", "Features include: transaction monitoring, RBF/CPFP detection, balance alerts, and deep wallet scanning." ], - "latest_version": "v1.5.2", + "latest_version": "v1.6.2", "supported_archs": ["amd64", "arm64"], "download_skip": true, "requires_docker_image_installation": true, @@ -26,7 +26,7 @@ "http_port": 3005, "show_on_homepage": true, "show_on_application_page": true, - "app_tile_name": "Canary", + "app_tile_name": "Canary Wallet", "app_tile_running_status_text": "Monitoring", "app_tile_default_status_text": "Wallet Monitor", "app_tile_button_text": "Info", diff --git a/rootfs/standard/usr/share/mynode_apps/canary/canary.service b/rootfs/standard/usr/share/mynode_apps/canary/canary.service index 3855ef468..1bd7ca116 100644 --- a/rootfs/standard/usr/share/mynode_apps/canary/canary.service +++ b/rootfs/standard/usr/share/mynode_apps/canary/canary.service @@ -1,5 +1,5 @@ [Unit] -Description=Canary +Description=Canary Wallet Wants=electrs.service docker_images.service After=electrs.service docker_images.service diff --git a/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-01.png b/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-01.png index f04dbc3d9..6f74eca83 100644 Binary files a/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-01.png and b/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-01.png differ diff --git a/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-02.png b/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-02.png index c9b1784b7..690343d20 100644 Binary files a/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-02.png and b/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-02.png differ diff --git a/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-03.png b/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-03.png index 7eeacf711..a90afd9f1 100644 Binary files a/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-03.png and b/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-03.png differ diff --git a/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-04.png b/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-04.png index f2db4f8a1..6579ae253 100644 Binary files a/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-04.png and b/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-04.png differ diff --git a/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-05.png b/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-05.png index ee1df8607..ef452ad52 100644 Binary files a/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-05.png and b/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-05.png differ diff --git a/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-06.png b/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-06.png new file mode 100644 index 000000000..f8d44fd87 Binary files /dev/null and b/rootfs/standard/usr/share/mynode_apps/canary/screenshots/screenshot-06.png differ diff --git a/rootfs/standard/usr/share/mynode_apps/canary/scripts/install_canary.sh b/rootfs/standard/usr/share/mynode_apps/canary/scripts/install_canary.sh index 10a0d9869..45abe1c42 100755 --- a/rootfs/standard/usr/share/mynode_apps/canary/scripts/install_canary.sh +++ b/rootfs/standard/usr/share/mynode_apps/canary/scripts/install_canary.sh @@ -9,22 +9,62 @@ set -e echo "==================== INSTALLING APP ====================" +pull_image() { + local image="$1" + local attempt + + for attempt in 1 2 3 4 5; do + if docker pull "$image"; then + return 0 + fi + + if [ "$attempt" -lt 5 ]; then + echo "Docker pull failed for $image (attempt $attempt/5); retrying..." + sleep $((attempt * 5)) + fi + done + + echo "ERROR: Docker pull failed for $image after 5 attempts" >&2 + return 1 +} + +write_compose_identity() { + local compose_env=".env" + local temp_file + + if [ -L "$compose_env" ] || { [ -e "$compose_env" ] && [ ! -f "$compose_env" ]; }; then + echo "Refusing to replace non-regular Canary Compose environment file: $compose_env" >&2 + return 1 + fi + + temp_file=$(mktemp .canary-compose-env.XXXXXX) + if ! printf 'CANARY_HOST_UID=%s\nCANARY_HOST_GID=%s\n' \ + "$(id -u bitcoin)" "$(id -g bitcoin)" > "$temp_file" || + ! chmod 600 "$temp_file" || + ! mv -f "$temp_file" "$compose_env"; then + rm -f "$temp_file" + return 1 + fi +} + mkdir -p /opt/mynode/canary || true mkdir -p /mnt/hdd/mynode/canary || true +chmod 700 /mnt/hdd/mynode/canary cp -f app_data/docker-compose.yml docker-compose.yml +write_compose_identity /usr/local/bin/docker-compose down --remove-orphans 2>/dev/null || true remove_docker_images_by_name "canary-backend" remove_docker_images_by_name "canary-frontend" -docker pull schjonhaug/canary-backend:$VERSION -docker pull schjonhaug/canary-frontend:$VERSION +pull_image "schjonhaug/canary-backend:$VERSION" +pull_image "schjonhaug/canary-frontend:$VERSION" docker tag schjonhaug/canary-backend:$VERSION canary-backend:latest docker tag schjonhaug/canary-frontend:$VERSION canary-frontend:latest -chown -R bitcoin:bitcoin /mnt/hdd/mynode/canary +chown bitcoin:bitcoin /mnt/hdd/mynode/canary echo "================== DONE INSTALLING APP =================" diff --git a/rootfs/standard/usr/share/mynode_apps/canary/scripts/pre_canary.sh b/rootfs/standard/usr/share/mynode_apps/canary/scripts/pre_canary.sh index ae8fe9784..ab7aee103 100755 --- a/rootfs/standard/usr/share/mynode_apps/canary/scripts/pre_canary.sh +++ b/rootfs/standard/usr/share/mynode_apps/canary/scripts/pre_canary.sh @@ -1,52 +1,162 @@ #!/bin/bash -set -e +set -eu + +# Secrets must be private from the moment their files are created. +umask 077 source /usr/share/mynode/mynode_functions.sh DATA_DIR="/mnt/hdd/mynode/canary" +INSTALL_DIR="/opt/mynode/canary" ADMIN_PASSWORD_FILE="$DATA_DIR/admin_password" JWT_SECRET_FILE="$DATA_DIR/jwt_secret" ENV_FILE="$DATA_DIR/canary.env" +COMPOSE_ENV_FILE="$INSTALL_DIR/.env" generate_secret() { - local length="$1" - tr -dc A-Za-z0-9 < /dev/urandom | head -c "$length" + local byte_count="$1" + od -An -N "$byte_count" -tx1 /dev/urandom | tr -d '[:space:]' } -cp -f app_data/docker-compose.yml docker-compose.yml +ensure_secret() { + local secret_file="$1" + local byte_count="$2" + local expected_length=$((byte_count * 2)) + local temp_file -# Ensure data directory exists before starting. -mkdir -p "$DATA_DIR" + # Never follow a path controlled from inside the bind-mounted data directory. + if [ -L "$secret_file" ]; then + echo "Refusing to use symlinked Canary secret: $secret_file" >&2 + return 1 + fi -if [ ! -s "$ADMIN_PASSWORD_FILE" ]; then - generate_secret 32 > "$ADMIN_PASSWORD_FILE" -fi + if [ -s "$secret_file" ]; then + if [ ! -f "$secret_file" ]; then + echo "Canary secret is not a regular file: $secret_file" >&2 + return 1 + fi + return 0 + fi -if [ ! -s "$JWT_SECRET_FILE" ]; then - generate_secret 64 > "$JWT_SECRET_FILE" -fi + if [ -e "$secret_file" ] && [ ! -f "$secret_file" ]; then + echo "Cannot replace non-regular Canary secret: $secret_file" >&2 + return 1 + fi + + if ! temp_file=$(mktemp "$DATA_DIR/.canary-secret.XXXXXX"); then + echo "Failed to create a temporary Canary secret file" >&2 + return 1 + fi + if ! generate_secret "$byte_count" > "$temp_file" || + [ "$(wc -c < "$temp_file")" -ne "$expected_length" ] || + ! chmod 600 "$temp_file" || + ! chown bitcoin:bitcoin "$temp_file" || + ! mv -f "$temp_file" "$secret_file"; then + rm -f "$temp_file" + echo "Failed to generate Canary secret: $secret_file" >&2 + return 1 + fi +} + +write_env_file() { + local admin_password + local jwt_secret + local temp_file + + if ! admin_password=$(cat "$ADMIN_PASSWORD_FILE") || + ! jwt_secret=$(cat "$JWT_SECRET_FILE"); then + echo "Failed to read Canary secrets" >&2 + return 1 + fi + + if ! temp_file=$(mktemp "$DATA_DIR/.canary.env.XXXXXX"); then + echo "Failed to create a temporary Canary environment file" >&2 + return 1 + fi + + if ! printf 'CANARY_SELF_HOSTED_ADMIN_PASSWORD=%s\nJWT_SECRET=%s\n' \ + "$admin_password" "$jwt_secret" > "$temp_file"; then + rm -f "$temp_file" + return 1 + fi + + if is_service_enabled mempool; then + if ! printf '%s\n' 'CANARY_MEMPOOL_PORT=4080' >> "$temp_file"; then + rm -f "$temp_file" + return 1 + fi + fi -cat > "$ENV_FILE" <> "$temp_file"; then + rm -f "$temp_file" + return 1 + fi + fi -has_local_tx_explorer=0 + if is_service_enabled mempool || is_service_enabled btcrpcexplorer; then + if ! printf '%s\n' 'CANARY_TX_EXPLORER_PLATFORM=mynode' >> "$temp_file"; then + rm -f "$temp_file" + return 1 + fi + fi -if is_service_enabled mempool; then - echo "CANARY_MEMPOOL_PORT=4080" >> "$ENV_FILE" - has_local_tx_explorer=1 + if ! chmod 600 "$temp_file" || + ! chown bitcoin:bitcoin "$temp_file" || + ! mv -f "$temp_file" "$ENV_FILE"; then + rm -f "$temp_file" + return 1 + fi +} + +write_compose_env_file() { + local temp_file + + if [ -L "$COMPOSE_ENV_FILE" ] || { [ -e "$COMPOSE_ENV_FILE" ] && [ ! -f "$COMPOSE_ENV_FILE" ]; }; then + echo "Refusing to replace non-regular Canary Compose environment file: $COMPOSE_ENV_FILE" >&2 + return 1 + fi + + if ! temp_file=$(mktemp "$INSTALL_DIR/.canary-compose-env.XXXXXX"); then + echo "Failed to create a temporary Canary Compose environment file" >&2 + return 1 + fi + + if ! printf 'CANARY_HOST_UID=%s\nCANARY_HOST_GID=%s\n' \ + "$(id -u bitcoin)" "$(id -g bitcoin)" > "$temp_file" || + ! chmod 600 "$temp_file" || + ! chown bitcoin:bitcoin "$temp_file" || + ! mv -f "$temp_file" "$COMPOSE_ENV_FILE"; then + rm -f "$temp_file" + return 1 + fi +} + +cp -f app_data/docker-compose.yml docker-compose.yml + +# Ensure data directory exists before starting. +mkdir -p "$INSTALL_DIR" +mkdir -p "$DATA_DIR" +chmod 700 "$DATA_DIR" + +if ! write_compose_env_file; then + echo "Failed to write Canary Compose environment file" >&2 + exit 1 fi -if is_service_enabled btcrpcexplorer; then - echo "CANARY_BTC_RPC_EXPLORER_PORT=3002" >> "$ENV_FILE" - has_local_tx_explorer=1 +ensure_secret "$ADMIN_PASSWORD_FILE" 24 +ensure_secret "$JWT_SECRET_FILE" 32 + +if [ -L "$ENV_FILE" ] || { [ -e "$ENV_FILE" ] && [ ! -f "$ENV_FILE" ]; }; then + echo "Refusing to replace non-regular Canary environment file: $ENV_FILE" >&2 + exit 1 fi -if [ "$has_local_tx_explorer" = "1" ]; then - echo "CANARY_TX_EXPLORER_PLATFORM=mynode" >> "$ENV_FILE" +if ! write_env_file; then + echo "Failed to write Canary environment file" >&2 + exit 1 fi -chown -R bitcoin:bitcoin "$DATA_DIR" +chown bitcoin:bitcoin "$DATA_DIR" "$ADMIN_PASSWORD_FILE" "$JWT_SECRET_FILE" "$ENV_FILE" chmod 600 "$ADMIN_PASSWORD_FILE" "$JWT_SECRET_FILE" "$ENV_FILE" diff --git a/rootfs/standard/usr/share/mynode_apps/canary/www/python/canary.py b/rootfs/standard/usr/share/mynode_apps/canary/www/python/canary.py index aceebaac0..be47bfdcf 100644 --- a/rootfs/standard/usr/share/mynode_apps/canary/www/python/canary.py +++ b/rootfs/standard/usr/share/mynode_apps/canary/www/python/canary.py @@ -3,18 +3,34 @@ from application_info import get_application, get_application_status, get_application_status_color from device_info import read_ui_settings import os +import stat mynode_canary = Blueprint("mynode_canary", __name__) CANARY_PASSWORD_FILE = "/mnt/hdd/mynode/canary/admin_password" +MAX_CANARY_PASSWORD_LENGTH = 1024 def get_canary_password(): - if not os.path.isfile(CANARY_PASSWORD_FILE): + password_fd = None + try: + password_fd = os.open(CANARY_PASSWORD_FILE, os.O_RDONLY | os.O_NOFOLLOW) + if not stat.S_ISREG(os.fstat(password_fd).st_mode): + return "" + + with os.fdopen(password_fd, "r", encoding="ascii") as password_file: + password_fd = None + password = password_file.read(MAX_CANARY_PASSWORD_LENGTH + 1).strip() + except (OSError, UnicodeError): return "" - with open(CANARY_PASSWORD_FILE, "r") as password_file: - return password_file.read().strip() + finally: + if password_fd is not None: + os.close(password_fd) + + if len(password) > MAX_CANARY_PASSWORD_LENGTH: + return "" + return password @mynode_canary.route("/info") diff --git a/rootfs/standard/usr/share/mynode_apps/canary/www/templates/README b/rootfs/standard/usr/share/mynode_apps/canary/www/templates/README index 8a978795f..e6e6a7cd0 100644 --- a/rootfs/standard/usr/share/mynode_apps/canary/www/templates/README +++ b/rootfs/standard/usr/share/mynode_apps/canary/www/templates/README @@ -1 +1 @@ -Canary uses MyNode's shared generic app template. +Canary Wallet uses MyNode's shared generic app template. diff --git a/rootfs/standard/usr/share/mynode_apps/canary/www/templates/canary.html b/rootfs/standard/usr/share/mynode_apps/canary/www/templates/canary.html index 190860e1d..89e95ecae 100644 --- a/rootfs/standard/usr/share/mynode_apps/canary/www/templates/canary.html +++ b/rootfs/standard/usr/share/mynode_apps/canary/www/templates/canary.html @@ -24,7 +24,7 @@ }); $("#copy_canary_password").on("click", function() { - var password = "{{canary_password}}"; + var password = $("#canary_password").text(); function showCopied() { showAlertPopup("alert_popup", "Password copied!"); }