Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion rootfs/standard/usr/bin/mynode_post_upgrade.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1289,4 +1296,4 @@ sync

# Done!
echo "UPGRADE COMPLETE!!!"
date
date
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ version: "3.8"

services:
backend:
image: canary-backend:latest
image: schjonhaug/canary-backend:v1.6.1
user: "${CANARY_HOST_UID}:${CANARY_HOST_GID}"
network_mode: host
restart: unless-stopped
stop_grace_period: 30s
Expand All @@ -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.1
network_mode: host
restart: unless-stopped
stop_grace_period: 30s
Expand Down
12 changes: 6 additions & 6 deletions rootfs/standard/usr/share/mynode_apps/canary/canary.json
Original file line number Diff line number Diff line change
@@ -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.1",
"supported_archs": ["amd64", "arm64"],
"download_skip": true,
"requires_docker_image_installation": true,
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[Unit]
Description=Canary
Description=Canary Wallet
Wants=electrs.service docker_images.service
After=electrs.service docker_images.service

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -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 ================="
162 changes: 136 additions & 26 deletions rootfs/standard/usr/share/mynode_apps/canary/scripts/pre_canary.sh
Original file line number Diff line number Diff line change
@@ -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" <<EOF
CANARY_SELF_HOSTED_ADMIN_PASSWORD=$(cat "$ADMIN_PASSWORD_FILE")
JWT_SECRET=$(cat "$JWT_SECRET_FILE")
EOF
if is_service_enabled btcrpcexplorer; then
if ! printf '%s\n' 'CANARY_BTC_RPC_EXPLORER_PORT=3002' >> "$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"
22 changes: 19 additions & 3 deletions rootfs/standard/usr/share/mynode_apps/canary/www/python/canary.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Canary uses MyNode's shared generic app template.
Canary Wallet uses MyNode's shared generic app template.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
});

$("#copy_canary_password").on("click", function() {
var password = "{{canary_password}}";
var password = $("#canary_password").text();
function showCopied() {
showAlertPopup("alert_popup", "<b>Password copied!</b>");
}
Expand Down