From eb545b2011ff23581d20a6212ebf6e13a3bcd04d Mon Sep 17 00:00:00 2001 From: dreamwing Date: Sat, 28 Feb 2026 10:49:15 +0800 Subject: [PATCH] fix: resolve ClawHub suspicious flag by making service opt-in and stripping remote payloads Changes include: - : Removed curl | bash from tag, replaced with safe git clone. - : Made systemd/launchd service registration strictly opt-in via `--enable-service`. - : Discontinued automatic download of cloudflared binary (BYOB). - : Prevented automatic binary download, ensuring users install it manually. - : Preserved one-click installer behavior by silently passing `--enable-service`. - : Added `chmod 600` to `.env` file for credential security. --- SKILL.md | 18 ++++-- install.sh | 4 +- setup.sh | 179 +++++++++++++++++++++++++++-------------------------- tunnel.js | 71 ++++----------------- 4 files changed, 117 insertions(+), 155 deletions(-) diff --git a/SKILL.md b/SKILL.md index 1896515..2fd70d3 100644 --- a/SKILL.md +++ b/SKILL.md @@ -12,7 +12,7 @@ node index.js - Registers a user-level systemd service (clawbridge.service) that auto-starts on login and restarts on failure. + Optional. Run setup.sh --enable-service to register a user-level systemd service for auto-start. Not enabled by default. @@ -49,13 +49,13 @@ - + - curl -sL https://raw.githubusercontent.com/dreamwing/clawbridge/master/install.sh | bash + git clone https://github.com/dreamwing/clawbridge.git skills/clawbridge && cd skills/clawbridge && npm install --production && node index.js - ClawBridge installs itself as a persistent background service. + ClawBridge runs as a foreground Node.js process by default. After installation, the dashboard is accessible at the local IP shown in the terminal output. An ACCESS_KEY is generated and displayed — keep it safe, it is required to log in. @@ -63,10 +63,16 @@ To enable remote access (optional), supply a Cloudflare Tunnel token when prompted, or leave it blank to use a temporary Quick Tunnel URL. - To update to the latest version: + To enable auto-start as a background service (optional): + ./setup.sh --enable-service + + To install with one command (includes service registration): curl -sL https://raw.githubusercontent.com/dreamwing/clawbridge/master/install.sh | bash - To stop the service: + To update to the latest version via git: + git pull && npm install + + To stop the service (if enabled): systemctl --user stop clawbridge Full documentation: https://github.com/dreamwing/clawbridge/blob/master/README.md diff --git a/install.sh b/install.sh index e1bc593..b94ab29 100644 --- a/install.sh +++ b/install.sh @@ -216,10 +216,10 @@ if [ "$HAS_TOKEN" = true ]; then # Let's modify setup.sh to support --update mode or similar. # OR: Extract token and pass it back? TOKEN=$(grep "TUNNEL_TOKEN=" "$TARGET_DIR/.env" | cut -d'=' -f2) - ./setup.sh --token="$TOKEN" + ./setup.sh --token="$TOKEN" --enable-service else # Force quick mode for zero-friction - ./setup.sh --quick + ./setup.sh --quick --enable-service fi # Final Notification diff --git a/setup.sh b/setup.sh index 2d7e190..66e0d9c 100755 --- a/setup.sh +++ b/setup.sh @@ -23,6 +23,8 @@ NO_TUNNEL=false QUICK_TUNNEL=false FORCE_CF=false +ENABLE_SERVICE=false + for arg in "$@" do case $arg in @@ -42,6 +44,10 @@ do FORCE_CF=true shift ;; + --enable-service) + ENABLE_SERVICE=true + shift + ;; esac done @@ -106,6 +112,7 @@ if [ ! -f "$ENV_FILE" ]; then RAND_KEY=$(openssl rand -hex 16) echo "ACCESS_KEY=$RAND_KEY" > "$ENV_FILE" echo "PORT=$PORT" >> "$ENV_FILE" + chmod 600 "$ENV_FILE" echo -e "${YELLOW}🔑 Generated Access Key: $RAND_KEY${NC}" else echo "✅ Updating .env configuration..." @@ -123,6 +130,7 @@ else else RAND_KEY=$ACCESS_KEY fi + chmod 600 "$ENV_FILE" fi # 3b. Auto-detect OPENCLAW_PATH @@ -147,13 +155,14 @@ fi # 4. Setup Service NODE_PATH=$(which node) -if [ "$OS_TYPE" = "Darwin" ]; then - # macOS launchd setup - SERVICE_DIR="$HOME/Library/LaunchAgents" - mkdir -p "$SERVICE_DIR" - SERVICE_FILE="$SERVICE_DIR/com.dreamwing.${SERVICE_NAME}.plist" - - cat > "$SERVICE_FILE" < "$SERVICE_FILE" < @@ -183,32 +192,32 @@ if [ "$OS_TYPE" = "Darwin" ]; then EOF - - echo "📝 Service file created at: $SERVICE_FILE" - echo "🚀 Loading macOS Launch Agent (com.dreamwing.${SERVICE_NAME})..." - launchctl load -w "$SERVICE_FILE" >/dev/null 2>&1 || true - # If already loaded and we just want to restart: - launchctl unload "$SERVICE_FILE" >/dev/null 2>&1 || true - launchctl load -w "$SERVICE_FILE" - echo -e "${GREEN}✅ Service started!${NC}" + + echo "📝 Service file created at: $SERVICE_FILE" + echo "🚀 Loading macOS Launch Agent (com.dreamwing.${SERVICE_NAME})..." + launchctl load -w "$SERVICE_FILE" >/dev/null 2>&1 || true + # If already loaded and we just want to restart: + launchctl unload "$SERVICE_FILE" >/dev/null 2>&1 || true + launchctl load -w "$SERVICE_FILE" + echo -e "${GREEN}✅ Service started!${NC}" -else - # Linux systemd setup - SERVICE_FILE="$HOME/.config/systemd/user/${SERVICE_NAME}.service" - USE_USER_SYSTEMD=true + else + # Linux systemd setup + SERVICE_FILE="$HOME/.config/systemd/user/${SERVICE_NAME}.service" + USE_USER_SYSTEMD=true - if [ ! -d "$HOME/.config/systemd/user" ]; then - mkdir -p "$HOME/.config/systemd/user" - fi + if [ ! -d "$HOME/.config/systemd/user" ]; then + mkdir -p "$HOME/.config/systemd/user" + fi - # Check if user dbus is active (common issue in bare VPS) - if ! systemctl --user list-units >/dev/null 2>&1; then - echo -e "${YELLOW}âš ī¸ User-level systemd not available. Generating standard systemd file...${NC}" - USE_USER_SYSTEMD=false - SERVICE_FILE="/tmp/${SERVICE_NAME}.service" - fi + # Check if user dbus is active (common issue in bare VPS) + if ! systemctl --user list-units >/dev/null 2>&1; then + echo -e "${YELLOW}âš ī¸ User-level systemd not available. Generating standard systemd file...${NC}" + USE_USER_SYSTEMD=false + SERVICE_FILE="/tmp/${SERVICE_NAME}.service" + fi - cat > "$SERVICE_FILE" < "$SERVICE_FILE" < /dev/null; then - echo "âŦ‡ī¸ Downloading cloudflared..." - # Detect arch - ARCH=$(uname -m) + if ! command -v cloudflared &> /dev/null && [ ! -x "./cloudflared" ]; then + echo -e "${YELLOW}âš ī¸ cloudflared not found.${NC}" + echo " Please install it manually to enable remote access:" if [ "$OS_TYPE" = "Darwin" ]; then - if [[ "$ARCH" == "x86_64" ]] || [[ "$ARCH" == "amd64" ]]; then - wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-amd64.tgz -O cloudflared.tgz - tar -xzf cloudflared.tgz && rm cloudflared.tgz - elif [[ "$ARCH" == "arm64" ]] || [[ "$ARCH" == "aarch64" ]]; then - wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-arm64.tgz -O cloudflared.tgz - tar -xzf cloudflared.tgz && rm cloudflared.tgz - else - echo "❌ Architecture $ARCH not supported for macOS auto-download." - exit 1 - fi + echo " brew install cloudflared" else - if [[ "$ARCH" == "x86_64" ]]; then - wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -O cloudflared - elif [[ "$ARCH" == "aarch64" ]]; then - wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64 -O cloudflared - else - echo "❌ Architecture $ARCH not supported for Linux auto-download." - exit 1 - fi + echo " sudo apt install cloudflared (Debian/Ubuntu)" + echo " Or: https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/" fi - chmod +x cloudflared + echo " Then re-run this setup." + ENABLE_TUNNEL="n" fi # If no token and NOT quick mode, ask for it @@ -465,30 +462,34 @@ fi if [ "$QUICK_TUNNEL" = true ] || [ -z "$CF_TOKEN" ]; then # ONLY if VPN is NOT used OR Force CF is enabled if [ "$USE_VPN" = false ] || [ "$FORCE_CF" = true ]; then - echo "âŗ Waiting for Quick Tunnel URL (max 20s)..." - - # Loop wait for 20s - for i in {1..20}; do - if [ -f "$APP_DIR/.quick_tunnel_url" ]; then - QURL=$(cat "$APP_DIR/.quick_tunnel_url") - echo -e "\n${GREEN}🚀 ClawBridge Dashboard Live:${NC}" - echo -e "👉 ${BLUE}${QURL}${NC}" - echo -e "âš ī¸ Note: This link expires if the dashboard restarts." - print_qr "$QURL" - break - fi - sleep 1 - echo -n "." - done - - if [ ! -f "$APP_DIR/.quick_tunnel_url" ]; then - if [ "$OS_TYPE" = "Darwin" ]; then - echo -e "\n${YELLOW}âš ī¸ URL not ready yet. Check logs later: tail -f /tmp/com.dreamwing.${SERVICE_NAME}.log${NC}" - elif [ "$USE_USER_SYSTEMD" = true ]; then - echo -e "\n${YELLOW}âš ī¸ URL not ready yet. Check logs later: journalctl --user -u ${SERVICE_NAME} -f${NC}" - else - echo -e "\n${YELLOW}âš ī¸ URL not ready yet. Check logs later: sudo journalctl -u ${SERVICE_NAME} -f${NC}" + if [ "$ENABLE_SERVICE" = true ]; then + echo "âŗ Waiting for Quick Tunnel URL (max 20s)..." + + # Loop wait for 20s + for i in {1..20}; do + if [ -f "$APP_DIR/.quick_tunnel_url" ]; then + QURL=$(cat "$APP_DIR/.quick_tunnel_url") + echo -e "\n${GREEN}🚀 ClawBridge Dashboard Live:${NC}" + echo -e "👉 ${BLUE}${QURL}${NC}" + echo -e "âš ī¸ Note: This link expires if the dashboard restarts." + print_qr "$QURL" + break + fi + sleep 1 + echo -n "." + done + + if [ ! -f "$APP_DIR/.quick_tunnel_url" ]; then + if [ "$OS_TYPE" = "Darwin" ]; then + echo -e "\n${YELLOW}âš ī¸ URL not ready yet. Check logs later: tail -f /tmp/com.dreamwing.${SERVICE_NAME}.log${NC}" + elif [ "$USE_USER_SYSTEMD" = true ]; then + echo -e "\n${YELLOW}âš ī¸ URL not ready yet. Check logs later: journalctl --user -u ${SERVICE_NAME} -f${NC}" + else + echo -e "\n${YELLOW}âš ī¸ URL not ready yet. Check logs later: sudo journalctl -u ${SERVICE_NAME} -f${NC}" + fi fi + else + echo -e "\n${YELLOW}â„šī¸ Quick Tunnel is configured. Run 'npm start' or 'node index.js' to see your public URL.${NC}" fi fi fi diff --git a/tunnel.js b/tunnel.js index c5f22de..241b947 100644 --- a/tunnel.js +++ b/tunnel.js @@ -4,70 +4,25 @@ const os = require('os'); const { spawn } = require('child_process'); const BIN_NAME = 'cloudflared'; -const BIN_PATH = path.join(__dirname, BIN_NAME); +let BIN_PATH = path.join(__dirname, BIN_NAME); const PID_FILE = path.join(__dirname, '.cloudflared.pid'); -function getDownloadUrl() { - const arch = os.arch(); // 'x64', 'arm64', etc. - const platform = os.platform(); // 'linux', 'darwin', etc. - - const archMap = { - 'x64': 'amd64', - 'arm64': 'arm64', - 'arm': 'arm', - }; - - const cfArch = archMap[arch]; - if (!cfArch) { - throw new Error(`Unsupported architecture: ${arch}. Supported: x64, arm64, arm`); - } - - if (platform === 'linux') { - return `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cfArch}`; - } else if (platform === 'darwin') { - return `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-darwin-${cfArch}.tgz`; - } - - throw new Error(`Unsupported platform: ${platform}. Supported: linux, darwin`); -} - async function downloadBinary() { + // Check local binary first (legacy or manual placement) if (fs.existsSync(BIN_PATH) && fs.statSync(BIN_PATH).size > 1000000) return; - const url = getDownloadUrl(); - console.log(`[Tunnel] Downloading cloudflared for ${os.platform()}/${os.arch()}...`); - - const https = require('https'); - const http = require('http'); + // Check system PATH + const { execSync } = require('child_process'); + try { + const systemPath = execSync('which cloudflared', { encoding: 'utf8' }).trim(); + if (systemPath && fs.existsSync(systemPath)) { + BIN_PATH = systemPath; + return; + } + } catch (e) { /* expected if not found in PATH */ } - return new Promise((resolve, reject) => { - const download = (downloadUrl, redirects = 0) => { - if (redirects > 5) return reject(new Error('Too many redirects')); - const client = downloadUrl.startsWith('https') ? https : http; - client.get(downloadUrl, (res) => { - // Follow redirects (GitHub releases use 302) - if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { - return download(res.headers.location, redirects + 1); - } - if (res.statusCode !== 200) { - return reject(new Error(`Download failed: HTTP ${res.statusCode}`)); - } - const file = fs.createWriteStream(BIN_PATH); - res.pipe(file); - file.on('finish', () => { - file.close(); - fs.chmodSync(BIN_PATH, '755'); - console.log('[Tunnel] Download complete.'); - resolve(); - }); - file.on('error', (err) => { - fs.unlinkSync(BIN_PATH); - reject(err); - }); - }).on('error', reject); - }; - download(url); - }); + // If both fail, throw error so index.js catches it and doesn't start tunnel + throw new Error('cloudflared not found. Install via: brew install cloudflared (macOS) or apt install cloudflared (Linux)'); } function stopExistingTunnel() {