From d1f04b8b5e8e285ea2c51c75a0d09c929fcd7e3c Mon Sep 17 00:00:00 2001 From: root Date: Sun, 5 Oct 2025 20:32:09 +0000 Subject: [PATCH] sh startup --- scripts/provision_node.sh | 393 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100755 scripts/provision_node.sh diff --git a/scripts/provision_node.sh b/scripts/provision_node.sh new file mode 100755 index 0000000..43eb891 --- /dev/null +++ b/scripts/provision_node.sh @@ -0,0 +1,393 @@ +#!/usr/bin/env bash +set -euo pipefail + +if [[ $(id -u) -ne 0 ]]; then + echo "This script must be run as root (use sudo)." >&2 + exit 1 +fi + +if ! command -v lsb_release >/dev/null 2>&1; then + apt-get update -y + apt-get install -y lsb-release >/dev/null +fi + +UBUNTU_CODENAME=$(lsb_release -sc) +UBUNTU_MAJOR=$(lsb_release -rs | cut -d'.' -f1) +if [[ "$UBUNTU_MAJOR" != "22" ]]; then + echo "Warning: this script targets Ubuntu 22.04. Detected $(lsb_release -ds)." >&2 + read -r -p "Continue anyway? [y/N]: " _cont + _cont=${_cont:-N} + if [[ ! $_cont =~ ^[Yy]$ ]]; then + exit 1 + fi +fi + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +BACKEND_ROOT=$(cd "$SCRIPT_DIR/.." && pwd) +PROJECT_ROOT=$(cd "$BACKEND_ROOT/.." && pwd) +CONFIGS_DIR="$PROJECT_ROOT/configs" +FRONTEND_DIR="$PROJECT_ROOT/web2-client" + +if [[ ! -d "$CONFIGS_DIR" ]]; then + echo "Expected configs directory at $CONFIGS_DIR." >&2 + exit 1 +fi +if [[ ! -f "$CONFIGS_DIR/docker-compose.yml" ]]; then + echo "Missing docker-compose.yml in $CONFIGS_DIR." >&2 + exit 1 +fi +if [[ ! -d "$FRONTEND_DIR" ]]; then + echo "Warning: web2-client directory not found at $FRONTEND_DIR (frontend build will fail)." >&2 +fi + +ENV_FILE="$CONFIGS_DIR/.env" +ENV_EXAMPLE="$BACKEND_ROOT/env.example" + +trim() { + local val="$1" + val="${val#${val%%[![:space:]]*}}" + val="${val%${val##*[![:space:]]}}" + printf '%s' "$val" +} + +ini_val() { + local key="$1" + if [[ -f "$ENV_FILE" ]]; then + awk -F'=' -v k="$key" 'BEGIN{found=0} $1==k{print substr($0,index($0,$2)); found=1; exit} END{if(!found){} }' "$ENV_FILE" + fi +} + +update_env() { + local key="$1" + local value="$2" + if [[ -f "$ENV_FILE" ]]; then + if grep -qE "^${key}=" "$ENV_FILE"; then + sed -i "s|^${key}=.*$|${key}=${value}|" "$ENV_FILE" + return + fi + fi + echo "${key}=${value}" >> "$ENV_FILE" +} + +prompt_required() { + local var="$1" + local label="$2" + local default_val="${3:-}" + local value + while true; do + if [[ -n "$default_val" ]]; then + read -r -p "$label [$default_val]: " value || true + value=${value:-$default_val} + else + read -r -p "$label: " value || true + fi + value=$(trim "$value") + if [[ -n "$value" ]]; then + printf -v "$var" '%s' "$value" + return + fi + echo "Value is required." + done +} + +prompt_optional() { + local var="$1" + local label="$2" + local default_val="${3:-}" + local value + if [[ -n "$default_val" ]]; then + read -r -p "$label [$default_val]: " value || true + value=${value:-$default_val} + else + read -r -p "$label: " value || true + fi + value=$(trim "$value") + printf -v "$var" '%s' "$value" +} + +# Prepare environment file +if [[ ! -f "$ENV_FILE" ]]; then + echo "Creating $ENV_FILE from example template." >&2 + if [[ -f "$ENV_EXAMPLE" ]]; then + cp "$ENV_EXAMPLE" "$ENV_FILE" + else + cp "$CONFIGS_DIR/.env.example" "$ENV_FILE" 2>/dev/null || true + if [[ ! -f "$ENV_FILE" ]]; then + touch "$ENV_FILE" + fi + fi +fi + +# Install base dependencies +apt-get update -y +apt-get install -y ca-certificates curl gnupg apt-transport-https software-properties-common git make nginx certbot python3-certbot-nginx python3 jq openssl + +# Docker repository setup +install -m 0755 -d /etc/apt/keyrings +if [[ ! -f /etc/apt/keyrings/docker.gpg ]]; then + curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg +fi +cat </etc/apt/sources.list.d/docker.list +deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $UBUNTU_CODENAME stable +EOF_REPO +apt-get update -y +apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin +systemctl enable --now docker + +if [[ -n "${SUDO_USER:-}" ]]; then + usermod -aG docker "$SUDO_USER" +fi + +# Collect interactive inputs +EXISTING_DOMAIN=$(ini_val PUBLIC_HOST) +EXISTING_DOMAIN=${EXISTING_DOMAIN#https://} +EXISTING_DOMAIN=${EXISTING_DOMAIN#http://} +DEFAULT_DOMAIN=$(trim "$EXISTING_DOMAIN") +prompt_required DOMAIN "Public domain (e.g. node.example.com)" "$DEFAULT_DOMAIN" +DOMAIN=$(trim "$DOMAIN") +PUBLIC_HOST="https://$DOMAIN" + +prompt_required EMAIL "Email for Let's Encrypt notifications" "$(ini_val CERTBOT_EMAIL)" + +DEFAULT_SEEDS=$(trim "${DEFAULT_DOMAIN:+https://$DEFAULT_DOMAIN}") +if [[ -z "$DEFAULT_SEEDS" ]]; then + DEFAULT_SEEDS="https://my-public-node-8.projscale.dev" +fi +prompt_required BOOTSTRAP_SEEDS "Bootstrap seed URLs (comma-separated)" "$DEFAULT_SEEDS" +IFS=',' read -r -a _seed_array <<< "$BOOTSTRAP_SEEDS" +BOOTSTRAP_SEEDS="" +for entry in "${_seed_array[@]}"; do + entry=$(trim "$entry") + [[ -z "$entry" ]] && continue + if [[ $entry != http://* && $entry != https://* ]]; then + entry="https://$entry" + fi + if [[ -z "$BOOTSTRAP_SEEDS" ]]; then + BOOTSTRAP_SEEDS="$entry" + else + BOOTSTRAP_SEEDS="$BOOTSTRAP_SEEDS,$entry" + fi +done +if [[ -z "$BOOTSTRAP_SEEDS" ]]; then + echo 'At least one bootstrap seed is required.' >&2 + exit 1 +fi + +prompt_optional NODE_PRIVACY "Node privacy (public/private)" "$(ini_val NODE_PRIVACY)" +if [[ -z "$NODE_PRIVACY" ]]; then + NODE_PRIVACY="public" +fi +NODE_PRIVACY=$(echo "$NODE_PRIVACY" | tr '[:upper:]' '[:lower:]') +if [[ "$NODE_PRIVACY" != "public" && "$NODE_PRIVACY" != "private" ]]; then + echo "Invalid privacy option, defaulting to public." >&2 + NODE_PRIVACY="public" +fi + +DEFAULT_SANIC=$(ini_val SANIC_PORT) +DEFAULT_SANIC=${DEFAULT_SANIC:-13200} +prompt_optional SANIC_PORT "Internal backend port" "$DEFAULT_SANIC" +DEFAULT_BACKEND_PORT=$(ini_val BACKEND_HTTP_PORT) +DEFAULT_BACKEND_PORT=${DEFAULT_BACKEND_PORT:-13200} +prompt_optional BACKEND_HTTP_PORT "Public backend port" "$DEFAULT_BACKEND_PORT" +DEFAULT_FRONTEND_PORT=$(ini_val FRONTEND_HTTP_PORT) +DEFAULT_FRONTEND_PORT=${DEFAULT_FRONTEND_PORT:-13300} +prompt_optional FRONTEND_HTTP_PORT "Public frontend port" "$DEFAULT_FRONTEND_PORT" +DEFAULT_TUSD_PORT=$(ini_val TUSD_HTTP_PORT) +DEFAULT_TUSD_PORT=${DEFAULT_TUSD_PORT:-13400} +prompt_optional TUSD_HTTP_PORT "Public tusd port" "$DEFAULT_TUSD_PORT" +DEFAULT_PG_PORT=$(ini_val POSTGRES_FORWARD_PORT) +DEFAULT_PG_PORT=${DEFAULT_PG_PORT:-13580} +prompt_optional POSTGRES_FORWARD_PORT "Public Postgres port" "$DEFAULT_PG_PORT" + +prompt_required TELEGRAM_API_KEY "Telegram uploader bot token" "$(ini_val TELEGRAM_API_KEY)" +prompt_required CLIENT_TELEGRAM_API_KEY "Telegram client bot token" "$(ini_val CLIENT_TELEGRAM_API_KEY)" + +prompt_optional ADMIN_API_TOKEN "Admin API token" "$(ini_val ADMIN_API_TOKEN)" +if [[ -z "$ADMIN_API_TOKEN" ]]; then + ADMIN_API_TOKEN=$(openssl rand -hex 16) + echo "Generated ADMIN_API_TOKEN=$ADMIN_API_TOKEN" +fi + +prompt_required TONCENTER_API_KEY "TON Center API key" "$(ini_val TONCENTER_API_KEY)" +prompt_optional TON_INIT_HOT_SEED "TON hot wallet seed hex (leave blank to auto-generate)" "$(ini_val TON_INIT_HOT_SEED)" + +prompt_required CONTENT_KEY_KEK_B64 "CONTENT_KEY_KEK_B64 (32-byte base64)" "$(ini_val CONTENT_KEY_KEK_B64)" +if ! python3 - "$CONTENT_KEY_KEK_B64" <<'PY'; then +import base64, sys +val = sys.argv[1] +raw = base64.b64decode(val + '===' , validate=False) +if len(raw) != 32: + raise SystemExit(1) +PY + echo 'Invalid CONTENT_KEY_KEK_B64: must be base64-encoded 32 bytes.' >&2 + exit 1 +fi + +prompt_required SWARM_KEY_HEX "IPFS swarm secret (32-byte hex)" "" +SWARM_KEY_HEX=$(echo "$SWARM_KEY_HEX" | tr '[:lower:]' '[:upper:]') +if ! python3 - "$SWARM_KEY_HEX" <<'PY'; then +import sys +val = sys.argv[1].strip() +bytes.fromhex(val) +if len(val) != 64: + raise SystemExit(1) +PY + echo 'Invalid IPFS swarm key: must be 64 hex chars (32 bytes).' >&2 + exit 1 +fi + +update_env CERTBOT_EMAIL "$EMAIL" +update_env PUBLIC_HOST "$PUBLIC_HOST" +update_env PROJECT_HOST "$PUBLIC_HOST" +update_env NODE_PRIVACY "$NODE_PRIVACY" +update_env BOOTSTRAP_SEEDS "$BOOTSTRAP_SEEDS" +update_env SANIC_PORT "$SANIC_PORT" +update_env BACKEND_HTTP_PORT "$BACKEND_HTTP_PORT" +update_env FRONTEND_HTTP_PORT "$FRONTEND_HTTP_PORT" +update_env TUSD_HTTP_PORT "$TUSD_HTTP_PORT" +update_env POSTGRES_FORWARD_PORT "$POSTGRES_FORWARD_PORT" +update_env TELEGRAM_API_KEY "$TELEGRAM_API_KEY" +update_env CLIENT_TELEGRAM_API_KEY "$CLIENT_TELEGRAM_API_KEY" +update_env ADMIN_API_TOKEN "$ADMIN_API_TOKEN" +update_env TONCENTER_API_KEY "$TONCENTER_API_KEY" +if [[ -n "$TON_INIT_HOT_SEED" ]]; then + update_env TON_INIT_HOT_SEED "$TON_INIT_HOT_SEED" +fi +update_env CONTENT_KEY_KEK_B64 "$CONTENT_KEY_KEK_B64" +update_env HANDSHAKE_INTERVAL_SEC "60" +update_env BOOTSTRAP_REQUIRED "1" +update_env VITE_API_BASE_URL "${PUBLIC_HOST}/api/v1" +update_env VITE_API_BASE_STORAGE_URL "${PUBLIC_HOST}/api/v1.5/storage" +update_env VITE_TUS_ENDPOINT "${PUBLIC_HOST}/tus/files" +update_env TON_CONNECT_MANIFEST_URI "${PUBLIC_HOST}/api/tonconnect-manifest.json" + +# Ensure swarm key file +mkdir -p "$CONFIGS_DIR/ipfs" +cat <"$CONFIGS_DIR/ipfs/swarm.key" +/key/swarm/psk/1.0.0/ +/base16/ +$SWARM_KEY_HEX +EOF_SWARM +chmod 600 "$CONFIGS_DIR/ipfs/swarm.key" +update_env IPFS_SWARM_KEY_FILE "$CONFIGS_DIR/ipfs/swarm.key" + +# Issue TLS certificate +if [[ ! -d "/etc/letsencrypt/live/$DOMAIN" ]]; then + systemctl stop nginx || true + certbot certonly --standalone --agree-tos --non-interactive -m "$EMAIL" -d "$DOMAIN" + systemctl start nginx +fi +systemctl enable --now nginx + +# Nginx configuration +cat </etc/nginx/sites-available/my-network.conf +upstream backend_app { + server 127.0.0.1:$SANIC_PORT; + keepalive 32; +} + +upstream frontend_web { + server 127.0.0.1:$FRONTEND_HTTP_PORT; + keepalive 16; +} + +upstream tusd_backend { + server 127.0.0.1:$TUSD_HTTP_PORT; + keepalive 16; +} + +server { + listen 80; + server_name $DOMAIN; + return 301 https://$DOMAIN$request_uri; +} + +server { + listen 443 ssl http2; + server_name $DOMAIN; + + ssl_certificate /etc/letsencrypt/live/$DOMAIN/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/$DOMAIN/privkey.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 1d; + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options SAMEORIGIN always; + + client_max_body_size 10G; + proxy_read_timeout 300s; + + location / { + proxy_pass http://frontend_web; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /assets/ { + proxy_pass http://frontend_web; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_hide_header Cache-Control; + add_header Cache-Control "public, max-age=31536000, immutable" always; + } + + location /tus/ { + proxy_pass http://tusd_backend/; + proxy_request_buffering off; + proxy_buffering off; + proxy_max_temp_file_size 0; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /files/ { + proxy_pass http://tusd_backend; + proxy_request_buffering off; + proxy_buffering off; + proxy_max_temp_file_size 0; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api/ { + proxy_pass http://backend_app; + proxy_http_version 1.1; + proxy_request_buffering off; + proxy_buffering off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location = /health { + proxy_pass http://backend_app/api/system.version; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +EOF_NGX + +ln -sf /etc/nginx/sites-available/my-network.conf /etc/nginx/sites-enabled/my-network.conf +rm -f /etc/nginx/sites-enabled/default +nginx -t +systemctl reload nginx + +# Bootstrap docker-compose stack +make -C "$CONFIGS_DIR" bootstrap + +echo "\nNode provisioning complete." +echo "- Admin panel: ${PUBLIC_HOST}/admin (use ADMIN_API_TOKEN)" +echo "- To trust this node on peers, mark it via admin API on existing node." +echo "- Docker services: run 'docker ps -a' or 'make -C $CONFIGS_DIR ps'."