#!/bin/bash # Fix nginx to serve custom frontend from /var/www/html/ # Run this in VMID 5000 set -euo pipefail CONFIG_FILE="/etc/nginx/sites-available/blockscout" echo "==========================================" echo "Updating Nginx to Serve Custom Frontend" echo "==========================================" echo "" # Step 1: Backup current config echo "=== Step 1: Backing up nginx config ===" cp "$CONFIG_FILE" "${CONFIG_FILE}.backup.$(date +%Y%m%d_%H%M%S)" echo "✅ Backup created" echo "" # Step 2: Create new config that serves custom frontend echo "=== Step 2: Creating new nginx configuration ===" cat > "$CONFIG_FILE" << 'NGINX_EOF' # HTTP server server { listen 80; listen [::]:80; server_name explorer.d-bis.org 192.168.11.140; location /.well-known/acme-challenge/ { root /var/www/html; try_files $uri =404; } # Explorer backend API (auth, features, AI, explorer-owned v1 helpers) location /explorer-api/v1/ { proxy_pass http://127.0.0.1:8081/api/v1/; proxy_http_version 1.1; 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_read_timeout 60s; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type, Authorization"; } # Blockscout API endpoint - MUST come before the redirect location location /api/ { proxy_pass http://127.0.0.1:4000; proxy_http_version 1.1; 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_read_timeout 300s; proxy_connect_timeout 75s; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type"; } # Token-aggregation API for live route-tree, quotes, and market data location /token-aggregation/api/v1/ { proxy_pass http://127.0.0.1:3001/api/v1/; proxy_http_version 1.1; 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_read_timeout 60s; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type"; } # Explorer config API (token list, networks) - serve from /var/www/html/config/ location = /api/config/token-list { default_type application/json; add_header Access-Control-Allow-Origin *; add_header Cache-Control "public, max-age=3600"; alias /var/www/html/config/DUAL_CHAIN_TOKEN_LIST.tokenlist.json; } location = /api/config/networks { default_type application/json; add_header Access-Control-Allow-Origin *; add_header Cache-Control "public, max-age=3600"; alias /var/www/html/config/DUAL_CHAIN_NETWORKS.json; } location /health { access_log off; proxy_pass http://127.0.0.1:4000/api/v2/status; proxy_set_header Host $host; add_header Content-Type application/json; } # When NPMplus forwards HTTPS to this port as HTTP, do NOT redirect to HTTPS (avoids ERR_TOO_MANY_REDIRECTS) set $redirect_http_to_https 1; if ($http_x_forwarded_proto = "https") { set $redirect_http_to_https 0; } if ($http_x_forwarded_proto = "HTTPS") { set $redirect_http_to_https 0; } # Snap companion (must be before catch-all so /snap/ is served from disk) location = /snap { rewrite ^ /snap/ last; } location /snap/ { alias /var/www/html/snap/; try_files $uri $uri/ /snap/index.html; add_header Cache-Control "no-store, no-cache, must-revalidate"; } # Serve custom frontend for root path (no-cache so fixes show after refresh) # CSP with unsafe-eval required by ethers.js v5 (NPM proxies to port 80) location = / { root /var/www/html; add_header Cache-Control "no-store, no-cache, must-revalidate"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdn.jsdelivr.net https://unpkg.com https://cdnjs.cloudflare.com; style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; img-src 'self' data: https:; font-src 'self' https://cdnjs.cloudflare.com; connect-src 'self' https://explorer.d-bis.org wss://explorer.d-bis.org https://rpc-http-pub.d-bis.org wss://rpc-ws-pub.d-bis.org http://192.168.11.221:8545 ws://192.168.11.221:8546;" always; try_files /index.html =404; } location = /favicon.ico { root /var/www/html; try_files /favicon.ico =404; add_header Cache-Control "public, max-age=86400"; } location = /apple-touch-icon.png { root /var/www/html; try_files /apple-touch-icon.png =404; add_header Cache-Control "public, max-age=86400"; } # Serve static assets location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { root /var/www/html; expires 1y; add_header Cache-Control "public, immutable"; } # SPA paths on HTTP (for internal/LAN tests) - serve index.html before redirect location ~ ^/(address|tx|block|token|tokens|blocks|transactions|bridge|weth|liquidity|watchlist|nft|home|analytics|operator)(/|$) { root /var/www/html; try_files /index.html =404; add_header Cache-Control "no-store, no-cache, must-revalidate"; } # All other requests: redirect to HTTPS only when not already behind HTTPS proxy location / { if ($redirect_http_to_https = 1) { return 301 https://$host$request_uri; } root /var/www/html; try_files $uri $uri/ /index.html; add_header Cache-Control "no-store, no-cache, must-revalidate"; } } # HTTPS server - Blockscout Explorer server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name explorer.d-bis.org 192.168.11.140; # SSL configuration ssl_certificate /etc/letsencrypt/live/explorer.d-bis.org/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/explorer.d-bis.org/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'; ssl_prefer_server_ciphers off; ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; access_log /var/log/nginx/blockscout-access.log; error_log /var/log/nginx/blockscout-error.log; # Serve custom frontend for root path (no-cache so fixes show after refresh) location = / { root /var/www/html; add_header Cache-Control "no-store, no-cache, must-revalidate"; try_files /index.html =404; } # Chain 138 MetaMask Snap companion site (SPA at /snap/) # /snap (no trailing slash) -> internal redirect so client gets 200 with content location = /snap { rewrite ^ /snap/ last; } location /snap/ { alias /var/www/html/snap/; try_files $uri $uri/ /snap/index.html; add_header Cache-Control "no-store, no-cache, must-revalidate"; } # Icons (exact match to avoid 404s) location = /favicon.ico { root /var/www/html; try_files /favicon.ico =404; add_header Cache-Control "public, max-age=86400"; } location = /apple-touch-icon.png { root /var/www/html; try_files /apple-touch-icon.png =404; add_header Cache-Control "public, max-age=86400"; } # Serve static assets location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { root /var/www/html; expires 1y; add_header Cache-Control "public, immutable"; } # Explorer backend API (auth, features, AI, explorer-owned v1 helpers) location /explorer-api/v1/ { proxy_pass http://127.0.0.1:8081/api/v1/; proxy_http_version 1.1; 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_read_timeout 60s; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type, Authorization"; } # Token-aggregation API for the explorer SPA live route-tree and pool intelligence. location /token-aggregation/api/v1/ { proxy_pass http://127.0.0.1:3001/api/v1/; proxy_http_version 1.1; 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_read_timeout 60s; add_header Access-Control-Allow-Origin *; } # Explorer config API (token list, networks) - serve from /var/www/html/config/ location = /api/config/token-list { default_type application/json; add_header Access-Control-Allow-Origin *; add_header Cache-Control "public, max-age=3600"; alias /var/www/html/config/DUAL_CHAIN_TOKEN_LIST.tokenlist.json; } location = /api/config/networks { default_type application/json; add_header Access-Control-Allow-Origin *; add_header Cache-Control "public, max-age=3600"; alias /var/www/html/config/DUAL_CHAIN_NETWORKS.json; } # API endpoint (for Blockscout API) location /api/ { proxy_pass http://127.0.0.1:4000; proxy_http_version 1.1; 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_read_timeout 300s; proxy_connect_timeout 75s; add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type"; } location /health { access_log off; proxy_pass http://127.0.0.1:4000/api/v2/status; proxy_set_header Host $host; add_header Content-Type application/json; } # Proxy Blockscout UI paths (if needed) location /blockscout/ { proxy_pass http://127.0.0.1:4000/; proxy_http_version 1.1; 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_read_timeout 300s; proxy_connect_timeout 75s; } # SPA paths: /address, /tx, /block, /token, /tokens, /blocks, /transactions, /bridge, /weth, /liquidity, /watchlist, /nft, /home, /analytics, /operator # Must serve index.html so path-based routing works (regex takes precedence over proxy) location ~ ^/(address|tx|block|token|tokens|blocks|transactions|bridge|weth|liquidity|watchlist|nft|home|analytics|operator)(/|$) { root /var/www/html; try_files /index.html =404; add_header Cache-Control "no-store, no-cache, must-revalidate"; } # All other paths serve custom frontend (SPA fallback via try_files) location / { root /var/www/html; try_files $uri $uri/ /index.html; } } map $http_upgrade $connection_upgrade { default upgrade; '' close; } NGINX_EOF echo "✅ Configuration updated" echo "" # Step 3: Handle SSL certs if missing echo "=== Step 3: Checking SSL certificates ===" if [ ! -f /etc/letsencrypt/live/explorer.d-bis.org/fullchain.pem ]; then echo "⚠️ Let's Encrypt certificate not found, creating self-signed..." mkdir -p /etc/nginx/ssl openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ -keyout /etc/nginx/ssl/blockscout.key \ -out /etc/nginx/ssl/blockscout.crt \ -subj "/CN=explorer.d-bis.org" 2>/dev/null sed -i 's|ssl_certificate /etc/letsencrypt/live/explorer.d-bis.org/fullchain.pem;|ssl_certificate /etc/nginx/ssl/blockscout.crt;|' "$CONFIG_FILE" sed -i 's|ssl_certificate_key /etc/letsencrypt/live/explorer.d-bis.org/privkey.pem;|ssl_certificate_key /etc/nginx/ssl/blockscout.key;|' "$CONFIG_FILE" echo "✅ Self-signed certificate created" else echo "✅ Let's Encrypt certificate found" fi echo "" # Step 4: Ensure /var/www/html exists and has correct permissions echo "=== Step 4: Preparing frontend directory ===" mkdir -p /var/www/html chown -R www-data:www-data /var/www/html 2>/dev/null || true echo "✅ Directory prepared" echo "" # Step 5: Test and restart nginx echo "=== Step 5: Testing and restarting nginx ===" if nginx -t; then echo "✅ Configuration valid" systemctl restart nginx echo "✅ Nginx restarted" else echo "❌ Configuration has errors" echo "Restoring backup..." cp "${CONFIG_FILE}.backup."* "$CONFIG_FILE" 2>/dev/null || true exit 1 fi echo "" # Step 6: Verify echo "=== Step 6: Verifying deployment ===" sleep 2 # Check if custom frontend exists if [ -f /var/www/html/index.html ]; then echo "✅ Custom frontend file exists" if grep -q "SolaceScanScout" /var/www/html/index.html; then echo "✅ Custom frontend content verified" else echo "⚠️ Frontend file exists but may not be the custom one" echo " Deploy the custom frontend using:" echo " ./scripts/deploy-frontend-to-vmid5000.sh" fi else echo "⚠️ Custom frontend not found at /var/www/html/index.html" echo " Deploy the custom frontend using:" echo " ./scripts/deploy-frontend-to-vmid5000.sh" fi # Test HTTP endpoint (non-fatal: do not exit on curl/grep failure) echo "" echo "Testing HTTP endpoint:" HTTP_RESPONSE=$(curl -s --max-time 5 http://localhost/ 2>/dev/null | head -5) || true if echo "$HTTP_RESPONSE" | grep -q "SolaceScanScout\|